diff --git a/src/components/Editor/Context/PipelineDetails.tsx b/src/components/Editor/Context/PipelineDetails.tsx new file mode 100644 index 000000000..4436d4d44 --- /dev/null +++ b/src/components/Editor/Context/PipelineDetails.tsx @@ -0,0 +1,132 @@ +import { useEffect, useState } from "react"; + +import { useValidationIssueNavigation } from "@/components/Editor/hooks/useValidationIssueNavigation"; +import { ActionBlock } from "@/components/shared/ContextPanel/Blocks/ActionBlock"; +import { ContentBlock } from "@/components/shared/ContextPanel/Blocks/ContentBlock"; +import { ListBlock } from "@/components/shared/ContextPanel/Blocks/ListBlock"; +import { TextBlock } from "@/components/shared/ContextPanel/Blocks/TextBlock"; +import { CopyText } from "@/components/shared/CopyText/CopyText"; +import { TaskImplementation } from "@/components/shared/TaskDetails"; +import { BlockStack } from "@/components/ui/layout"; +import { useComponentSpec } from "@/providers/ComponentSpecProvider"; +import { getComponentFileFromList } from "@/utils/componentStore"; +import { USER_PIPELINES_LIST_NAME } from "@/utils/constants"; + +import PipelineIO from "../../shared/ArtifactsList/PipelineIO"; +import { PipelineValidationList } from "./PipelineValidationList"; +import RenamePipeline from "./RenamePipeline"; + +const PipelineDetails = () => { + const { + componentSpec, + digest, + isComponentTreeValid, + globalValidationIssues, + } = useComponentSpec(); + + const { handleIssueClick, groupedIssues } = useValidationIssueNavigation( + globalValidationIssues, + ); + + // State for file metadata + const [fileMeta, setFileMeta] = useState<{ + creationTime?: Date; + modificationTime?: Date; + createdBy?: string; + }>({}); + + // Fetch file metadata on mount or when componentSpec.name changes + useEffect(() => { + const fetchMeta = async () => { + if (!componentSpec.name) return; + const file = await getComponentFileFromList( + USER_PIPELINES_LIST_NAME, + componentSpec.name, + ); + if (file) { + setFileMeta({ + creationTime: file.creationTime, + modificationTime: file.modificationTime, + createdBy: file.componentRef.spec.metadata?.annotations?.author, + }); + } + }; + fetchMeta(); + }, [componentSpec.name]); + + const metadata = [ + { + label: "Created by", + value: fileMeta.createdBy, + }, + { + label: "Created at", + value: fileMeta.creationTime?.toLocaleString(), + }, + { + label: "Last updated", + value: fileMeta.modificationTime?.toLocaleString(), + }, + ]; + + const annotations = Object.entries( + componentSpec.metadata?.annotations || {}, + ).map(([key, value]) => ({ label: key, value: String(value) })); + + const actions = [ + , + , + ]; + + return ( + + + {componentSpec.name ?? "Unnamed Pipeline"} + + + + + + + {componentSpec.description && ( + + )} + + {digest && ( + + )} + + {annotations.length > 0 && ( + + )} + + + + + + + + ); +}; + +export default PipelineDetails; diff --git a/src/components/Editor/components/PipelineValidationList/PipelineValidationList.tsx b/src/components/Editor/Context/PipelineValidationList.tsx similarity index 98% rename from src/components/Editor/components/PipelineValidationList/PipelineValidationList.tsx rename to src/components/Editor/Context/PipelineValidationList.tsx index 80ad8c7b3..223128f98 100644 --- a/src/components/Editor/components/PipelineValidationList/PipelineValidationList.tsx +++ b/src/components/Editor/Context/PipelineValidationList.tsx @@ -20,7 +20,7 @@ import { cn } from "@/lib/utils"; import { pluralize } from "@/utils/string"; import type { ComponentValidationIssue } from "@/utils/validations"; -import type { ValidationIssueGroup } from "../../hooks/useValidationIssueNavigation"; +import type { ValidationIssueGroup } from "../hooks/useValidationIssueNavigation"; interface PipelineValidationListProps { isComponentTreeValid: boolean; @@ -75,13 +75,13 @@ export const PipelineValidationList = ({ title={`${totalIssueCount} ${pluralize(totalIssueCount, "issue")} detected`} > - {" "} Select an item to jump to its location in the pipeline. {groupedIssues.map((group) => { const isOpen = openGroups.has(group.pathKey); + return ( { const { componentSpec, saveComponentSpec } = useComponentSpec(); const notify = useToastNotification(); diff --git a/src/components/Editor/PipelineDetails.tsx b/src/components/Editor/PipelineDetails.tsx deleted file mode 100644 index 767f6cd7a..000000000 --- a/src/components/Editor/PipelineDetails.tsx +++ /dev/null @@ -1,281 +0,0 @@ -import { Frown } from "lucide-react"; -import { useEffect, useState } from "react"; - -import { PipelineValidationList } from "@/components/Editor/components/PipelineValidationList/PipelineValidationList"; -import { useValidationIssueNavigation } from "@/components/Editor/hooks/useValidationIssueNavigation"; -import { ArtifactsList } from "@/components/shared/ArtifactsList/ArtifactsList"; -import { CopyText } from "@/components/shared/CopyText/CopyText"; -import { Button } from "@/components/ui/button"; -import { Icon } from "@/components/ui/icon"; -import { BlockStack, InlineStack } from "@/components/ui/layout"; -import { Text } from "@/components/ui/typography"; -import useToastNotification from "@/hooks/useToastNotification"; -import { useComponentSpec } from "@/providers/ComponentSpecProvider"; -import { useContextPanel } from "@/providers/ContextPanelProvider"; -import { - type InputSpec, - type OutputSpec, - type TypeSpecType, -} from "@/utils/componentSpec"; -import { getComponentFileFromList } from "@/utils/componentStore"; -import { USER_PIPELINES_LIST_NAME } from "@/utils/constants"; - -import { TaskImplementation } from "../shared/TaskDetails"; -import { InputValueEditor } from "./IOEditor/InputValueEditor"; -import { OutputNameEditor } from "./IOEditor/OutputNameEditor"; -import RenamePipeline from "./RenamePipeline"; -import { getOutputConnectedDetails } from "./utils/getOutputConnectedDetails"; - -const PipelineDetails = () => { - const { setContent } = useContextPanel(); - const { - componentSpec, - graphSpec, - digest, - isComponentTreeValid, - globalValidationIssues, - } = useComponentSpec(); - - const notify = useToastNotification(); - - const { handleIssueClick, groupedIssues } = useValidationIssueNavigation( - globalValidationIssues, - ); - - // Utility function to convert TypeSpecType to string - const typeSpecToString = (typeSpec?: TypeSpecType): string => { - if (typeSpec === undefined) { - return "Any"; - } - if (typeof typeSpec === "string") { - return typeSpec; - } - return JSON.stringify(typeSpec); - }; - - // State for file metadata - const [fileMeta, setFileMeta] = useState<{ - creationTime?: Date; - modificationTime?: Date; - createdBy?: string; - }>({}); - - // Fetch file metadata on mount or when componentSpec.name changes - useEffect(() => { - const fetchMeta = async () => { - if (!componentSpec?.name) return; - const file = await getComponentFileFromList( - USER_PIPELINES_LIST_NAME, - componentSpec.name, - ); - if (file) { - setFileMeta({ - creationTime: file.creationTime, - modificationTime: file.modificationTime, - createdBy: file.componentRef.spec.metadata?.annotations?.author as - | string - | undefined, - }); - } - }; - fetchMeta(); - }, [componentSpec?.name]); - - const handleInputEdit = (input: InputSpec) => { - setContent(); - }; - - const handleOutputEdit = (output: OutputSpec) => { - const outputConnectedDetails = getOutputConnectedDetails( - graphSpec, - output.name, - ); - setContent( - , - ); - }; - - const handleDigestCopy = () => { - navigator.clipboard.writeText(digest); - notify("Digest copied to clipboard", "success"); - }; - - if (!componentSpec) { - return ( - - - - Error loading pipeline details. - - - ); - } - - const annotations = componentSpec.metadata?.annotations || {}; - - return ( - - - {componentSpec.name ?? "Unnamed Pipeline"} - - - - - - - {(fileMeta.createdBy || - fileMeta.creationTime || - fileMeta.modificationTime) && ( - - - Pipeline Info - - - {fileMeta.createdBy && ( - - - Created by: - - {fileMeta.createdBy} - - )} - {fileMeta.creationTime && ( - - - Created at: - - {new Date(fileMeta.creationTime).toLocaleString()} - - )} - {fileMeta.modificationTime && ( - - - Last updated: - - {new Date(fileMeta.modificationTime).toLocaleString()} - - )} - - - )} - - {componentSpec.description && ( - - - Description - - - {componentSpec.description} - - - )} - - {/* Component Digest */} - {digest && ( - - - Digest - - - - {digest} - - - - )} - - {/* Annotations */} - {Object.keys(annotations).length > 0 && ( - - - Annotations - - - {Object.entries(annotations).map(([key, value]) => ( - - - {key}: - {" "} - - {String(value)} - - - ))} - - - )} - - ({ - name: input.name, - type: typeSpecToString(input?.type), - value: input.value || input.default, - actions: ( - handleInputEdit(input)} - className="text-muted-foreground hover:text-foreground h-4 w-4" - > - - - ), - }))} - outputs={(componentSpec.outputs ?? []).map((output) => { - const connectedDetails = getOutputConnectedDetails( - graphSpec, - output.name, - ); - return { - name: output.name, - type: typeSpecToString(connectedDetails.outputType), - value: connectedDetails.outputName, - actions: ( - handleOutputEdit(output)} - className="text-muted-foreground hover:text-foreground h-4 w-4" - > - - - ), - }; - })} - /> - - {/* Validations */} - - - Validations - - - - - ); -}; - -export default PipelineDetails; diff --git a/src/components/Editor/PipelineEditor.tsx b/src/components/Editor/PipelineEditor.tsx index 43160f14a..8ffcd1ef7 100644 --- a/src/components/Editor/PipelineEditor.tsx +++ b/src/components/Editor/PipelineEditor.tsx @@ -22,7 +22,7 @@ import { ContextPanelProvider } from "@/providers/ContextPanelProvider"; import { LoadingScreen } from "../shared/LoadingScreen"; import { NodesOverlayProvider } from "../shared/ReactFlow/NodesOverlay/NodesOverlayProvider"; -import PipelineDetails from "./PipelineDetails"; +import PipelineDetails from "./Context/PipelineDetails"; const GRID_SIZE = 10; diff --git a/src/components/shared/ArtifactsList/PipelineIO.tsx b/src/components/shared/ArtifactsList/PipelineIO.tsx new file mode 100644 index 000000000..0c18f61c5 --- /dev/null +++ b/src/components/shared/ArtifactsList/PipelineIO.tsx @@ -0,0 +1,167 @@ +import { type ReactNode } from "react"; + +import { ContentBlock } from "@/components/shared/ContextPanel/Blocks/ContentBlock"; +import { KeyValuePair } from "@/components/shared/ContextPanel/Blocks/KeyValuePair"; +import { typeSpecToString } from "@/components/shared/ReactFlow/FlowCanvas/TaskNode/ArgumentsEditor/utils"; +import { Button } from "@/components/ui/button"; +import { Icon } from "@/components/ui/icon"; +import { BlockStack, InlineStack } from "@/components/ui/layout"; +import { Paragraph } from "@/components/ui/typography"; +import { useComponentSpec } from "@/providers/ComponentSpecProvider"; +import { useContextPanel } from "@/providers/ContextPanelProvider"; +import { type InputSpec, type OutputSpec } from "@/utils/componentSpec"; + +import { InputValueEditor } from "../../Editor/IOEditor/InputValueEditor"; +import { OutputNameEditor } from "../../Editor/IOEditor/OutputNameEditor"; +import { getOutputConnectedDetails } from "../../Editor/utils/getOutputConnectedDetails"; + +const PipelineIO = ({ readOnly }: { readOnly?: boolean }) => { + const { setContent } = useContextPanel(); + const { componentSpec, graphSpec } = useComponentSpec(); + + const handleInputEdit = (input: InputSpec) => { + setContent(); + }; + + const handleOutputEdit = (output: OutputSpec) => { + const outputConnectedDetails = getOutputConnectedDetails( + graphSpec, + output.name, + ); + setContent( + , + ); + }; + + const inputActions: IORowAction[] = [ + { + label: "Edit", + icon: , + hidden: readOnly, + onClick: () => handleInputEdit, + }, + ]; + const outputActions: IORowAction[] = []; + + if (!readOnly) { + inputActions.push(); + + outputActions.push({ + label: "Edit", + icon: , + hidden: readOnly, + onClick: () => handleOutputEdit, + }); + } + + return ( + + + {componentSpec.inputs && componentSpec.inputs.length > 0 ? ( + + {componentSpec.inputs.map((input) => ( + + ))} + + ) : ( + + No inputs + + )} + + + {componentSpec.outputs && componentSpec.outputs.length > 0 ? ( + + {componentSpec.outputs.map((output) => { + const connectedOutput = getOutputConnectedDetails( + graphSpec, + output.name, + ); + + return ( + + ); + })} + + ) : ( + + No outputs + + )} + + + ); +}; + +export default PipelineIO; + +type IORowAction = { + label: string; + icon?: ReactNode; + hidden?: boolean; + onClick: () => void; +}; + +interface IORowProps { + name: string; + value: string; + type: string; + actions?: IORowAction[]; +} + +function IORow({ name, value, type, actions }: IORowProps) { + return ( + + + + + + + + ({type}) + + {actions?.map( + (action) => + !action.hidden && ( + + {action.icon ?? action.label} + + ), + )} + + + ); +} diff --git a/src/components/ui/icon.tsx b/src/components/ui/icon.tsx index ca6b0a15e..a6b5a467d 100644 --- a/src/components/ui/icon.tsx +++ b/src/components/ui/icon.tsx @@ -10,6 +10,7 @@ const iconVariants = cva("", { sm: "!w-3.5 !h-3.5", md: "!w-4 !h-4", lg: "!w-5 !h-5", + xl: "!w-6 !h-6", fill: "!w-full !h-full", }, },