diff --git a/src/components/shared/ReactFlow/FlowCanvas/TaskNode/TaskNodeCard/TaskNodeCard.tsx b/src/components/shared/ReactFlow/FlowCanvas/TaskNode/TaskNodeCard/TaskNodeCard.tsx index f3680e5fc..3511c7b61 100644 --- a/src/components/shared/ReactFlow/FlowCanvas/TaskNode/TaskNodeCard/TaskNodeCard.tsx +++ b/src/components/shared/ReactFlow/FlowCanvas/TaskNode/TaskNodeCard/TaskNodeCard.tsx @@ -1,5 +1,4 @@ import { useNavigate } from "@tanstack/react-router"; -import { CircleFadingArrowUp, CopyIcon } from "lucide-react"; import { useCallback, useEffect, useMemo, useRef, useState } from "react"; import type { TooltipButtonProps } from "@/components/shared/Buttons/TooltipButton"; @@ -126,38 +125,26 @@ const TaskNodeCard = () => { const actions: Array = []; if (!readOnly) { - actions.push( - { - children: ( -
- -
- ), - variant: "outline", - tooltip: "Duplicate Task", - onClick: callbacks.onDuplicate, - }, - { - children: ( -
- -
- ), - variant: "outline", - className: cn(isCustomComponent && "hidden"), - tooltip: "Update Task from Source URL", - onClick: callbacks.onUpgrade, - }, - ); + actions.push({ + children: , + variant: "outline", + tooltip: "Duplicate Task", + onClick: callbacks.onDuplicate, + }); + } + + if (!readOnly && !isCustomComponent) { + actions.push({ + children: , + variant: "outline", + tooltip: "Update Task from Source URL", + onClick: callbacks.onUpgrade, + }); } if (isSubgraphNode && taskId && isSubgraphNavigationEnabled) { actions.push({ - children: ( -
- -
- ), + children: , variant: "outline", tooltip: `Enter Subgraph: ${subgraphDescription}`, onClick: () => navigateToSubgraph(taskId), @@ -166,11 +153,7 @@ const TaskNodeCard = () => { if (isInAppEditorEnabled) { actions.push({ - children: ( -
- -
- ), + children: , variant: "outline", tooltip: "Edit Component Definition", onClick: handleEditComponent, diff --git a/src/components/shared/ReactFlow/FlowCanvas/TaskNode/TaskOverview/TaskOverview.tsx b/src/components/shared/ReactFlow/FlowCanvas/TaskNode/TaskOverview/TaskOverview.tsx index 92b272e17..7bdb7d43c 100644 --- a/src/components/shared/ReactFlow/FlowCanvas/TaskNode/TaskOverview/TaskOverview.tsx +++ b/src/components/shared/ReactFlow/FlowCanvas/TaskNode/TaskOverview/TaskOverview.tsx @@ -128,7 +128,6 @@ const TaskOverview = ({ taskNode, actions }: TaskOverviewProps) => { url={taskSpec.componentRef.url} onDelete={callbacks.onDelete} status={status} - hasDeletionConfirmation={false} readOnly={readOnly} actions={detailActions} /> diff --git a/src/components/shared/TaskDetails/Actions.tsx b/src/components/shared/TaskDetails/Actions.tsx new file mode 100644 index 000000000..6e8ddcb1f --- /dev/null +++ b/src/components/shared/TaskDetails/Actions.tsx @@ -0,0 +1,101 @@ +import { type ReactNode } from "react"; +import { FaPython } from "react-icons/fa"; + +import useToastNotification from "@/hooks/useToastNotification"; +import type { ComponentSpec } from "@/utils/componentSpec"; +import { downloadYamlFromComponentText } from "@/utils/URL"; +import copyToYaml from "@/utils/yaml"; + +import { + ActionBlock, + type ActionOrReactNode, +} from "../ContextPanel/Blocks/ActionBlock"; + +interface TaskActionsProps { + displayName: string; + componentSpec: ComponentSpec; + actions?: ReactNode[]; + onDelete?: () => void; + readOnly?: boolean; + className?: string; +} + +const TaskActions = ({ + displayName, + componentSpec, + actions = [], + onDelete, + readOnly = false, + className, +}: TaskActionsProps) => { + const notify = useToastNotification(); + + const pythonOriginalCode = + componentSpec?.metadata?.annotations?.original_python_code; + + const stringToPythonCodeDownload = () => { + if (!pythonOriginalCode) return; + + const blob = new Blob([pythonOriginalCode], { type: "text/x-python" }); + const url = URL.createObjectURL(blob); + const a = document.createElement("a"); + a.href = url; + a.download = `${componentSpec?.name || displayName}.py`; + document.body.appendChild(a); + a.click(); + document.body.removeChild(a); + URL.revokeObjectURL(url); + }; + + const handleDownloadYaml = () => { + downloadYamlFromComponentText(componentSpec, displayName); + }; + + const handleCopyYaml = () => { + copyToYaml( + componentSpec, + (message) => notify(message, "success"), + (message) => notify(message, "error"), + ); + }; + + const handleDelete = () => { + try { + onDelete?.(); + } catch (error) { + console.error("Error deleting component:", error); + notify(`Error deleting component`, "error"); + } + }; + + const orderedActions: ActionOrReactNode[] = [ + { + label: "Download YAML", + icon: "Download", + onClick: handleDownloadYaml, + }, + { + label: "Download Python Code", + content: , + hidden: !pythonOriginalCode, + onClick: stringToPythonCodeDownload, + }, + { + label: "Copy YAML", + icon: "Clipboard", + onClick: handleCopyYaml, + }, + ...actions, + { + label: "Delete Component", + icon: "Trash", + destructive: true, + hidden: !onDelete || readOnly, + onClick: handleDelete, + }, + ]; + + return ; +}; + +export default TaskActions; diff --git a/src/components/shared/TaskDetails/Details.tsx b/src/components/shared/TaskDetails/Details.tsx index 83d0ff692..4d4c2da2e 100644 --- a/src/components/shared/TaskDetails/Details.tsx +++ b/src/components/shared/TaskDetails/Details.tsx @@ -1,36 +1,13 @@ -import { - ChevronsUpDown, - ClipboardIcon, - DownloadIcon, - TrashIcon, -} from "lucide-react"; -import { type ReactNode, useState } from "react"; -import { FaPython } from "react-icons/fa"; +import { type ReactNode } from "react"; -import { Button } from "@/components/ui/button"; -import { - Collapsible, - CollapsibleContent, - CollapsibleTrigger, -} from "@/components/ui/collapsible"; -import { Link } from "@/components/ui/link"; -import { - Tooltip, - TooltipContent, - TooltipTrigger, -} from "@/components/ui/tooltip"; -import { useCopyToClipboard } from "@/hooks/useCopyToClip"; -import useToastNotification from "@/hooks/useToastNotification"; -import { cn } from "@/lib/utils"; +import { BlockStack } from "@/components/ui/layout"; import type { ComponentSpec } from "@/utils/componentSpec"; -import { - convertGithubUrlToDirectoryUrl, - downloadYamlFromComponentText, - isGithubUrl, -} from "@/utils/URL"; -import copyToYaml from "@/utils/yaml"; +import { ContentBlock } from "../ContextPanel/Blocks/ContentBlock"; +import { TextBlock } from "../ContextPanel/Blocks/TextBlock"; +import TaskActions from "./Actions"; import { ExecutionDetails } from "./ExecutionDetails"; +import { GithubDetails } from "./GithubDetails"; interface TaskDetailsProps { displayName: string; @@ -41,7 +18,6 @@ interface TaskDetailsProps { url?: string; actions?: ReactNode[]; onDelete?: () => void; - hasDeletionConfirmation?: boolean; status?: string; readOnly?: boolean; additionalSection?: { @@ -51,6 +27,8 @@ interface TaskDetailsProps { }[]; } +const BASE_BLOCK_CLASS = "px-3 py-2"; + const TaskDetails = ({ displayName, componentSpec, @@ -60,23 +38,11 @@ const TaskDetails = ({ url, actions = [], onDelete, - hasDeletionConfirmation = true, status, readOnly = false, additionalSection = [], }: TaskDetailsProps) => { - const notify = useToastNotification(); - const [confirmDelete, setConfirmDelete] = useState(false); - const { isCopied, isTooltipOpen, handleCopy, handleTooltipOpen } = - useCopyToClipboard(componentDigest); - const canonicalUrl = componentSpec?.metadata?.annotations?.canonical_location; - const pythonOriginalCode = (componentSpec?.metadata?.annotations - ?.original_python_code || - componentSpec?.metadata?.annotations?.python_original_code) as - | string - | undefined; - let reconstructedUrl; if (!url) { // Try reconstruct the url from componentSpec.metadata.annotations @@ -103,292 +69,72 @@ const TaskDetails = ({ } } - const stringToPythonCodeDownload = () => { - if (!pythonOriginalCode) return; - - const blob = new Blob([pythonOriginalCode], { type: "text/x-python" }); - const url = URL.createObjectURL(blob); - const a = document.createElement("a"); - a.href = url; - a.download = `${componentSpec?.name || displayName}.py`; - document.body.appendChild(a); - a.click(); - document.body.removeChild(a); - URL.revokeObjectURL(url); - }; - - const handleDownloadYaml = () => { - downloadYamlFromComponentText(componentSpec, displayName); - }; - - const handleCopyYaml = () => { - copyToYaml( - componentSpec, - (message) => notify(message, "success"), - (message) => notify(message, "error"), - ); - }; - - const handleDelete = () => { - if (confirmDelete || !hasDeletionConfirmation) { - try { - onDelete?.(); - } catch (error) { - console.error("Error deleting component:", error); - } - } else if (hasDeletionConfirmation) { - setConfirmDelete(true); - } - }; + const author = componentSpec?.metadata?.annotations?.author; + const description = componentSpec?.description; return ( -
-
- {taskId && ( -
-
- Task ID -
-
- {taskId} -
-
- )} - {status && ( -
-
- Run Status -
-
- {status} -
-
- )} - - {executionId && ( - - )} - - {componentSpec?.metadata?.annotations?.author && ( -
-
- Author -
-
- {componentSpec.metadata.annotations?.author} -
-
- )} - - 0 ? url : reconstructedUrl} - canonicalUrl={canonicalUrl} + + + + + + {executionId && ( + + )} - {componentSpec?.description && ( -
- -
- Description - - - -
- - -
- {componentSpec.description} -
-
-
-
- )} - - {componentDigest && ( -
-
- Digest -
-
- - {componentDigest} - - - - - - - {isCopied ? "Copied" : "Copy Digest"} - - -
-
- )} - {additionalSection.map((section) => ( -
- - -
- {section.title} - - -
-
- - - {section.component} - -
-
- ))} - -
- - - - - Download YAML - - {pythonOriginalCode && ( - - - - - Download Python Code - - )} - - - - - Copy YAML - - - {actions} - - {onDelete && !readOnly && ( - - - - - - {confirmDelete || !hasDeletionConfirmation - ? "Confirm Delete. This action cannot be undone." - : "Delete Component"} - - - )} -
-
-
+ + + 0 ? url : reconstructedUrl} + canonicalUrl={canonicalUrl} + className={BASE_BLOCK_CLASS} + /> + + + + + + {additionalSection.map((section) => ( + + {section.component} + + ))} + + + ); }; export default TaskDetails; - -function LinkBlock({ - url, - canonicalUrl, -}: { - url?: string; - canonicalUrl?: string; -}) { - if (!url && !canonicalUrl) return null; - - return ( -
-
URL
- {url && ( - <> -
- - View raw component.yaml - -
-
- - View directory on GitHub - -
- - )} - {canonicalUrl && ( - <> -
- - View raw canonical URL - -
-
- - View canonical URL on GitHub - -
- - )} -
- ); -} diff --git a/src/components/shared/TaskDetails/GithubDetails.tsx b/src/components/shared/TaskDetails/GithubDetails.tsx new file mode 100644 index 000000000..5ec2b43e9 --- /dev/null +++ b/src/components/shared/TaskDetails/GithubDetails.tsx @@ -0,0 +1,58 @@ +import { BlockStack } from "@/components/ui/layout"; +import { Link } from "@/components/ui/link"; +import { Heading } from "@/components/ui/typography"; +import { convertGithubUrlToDirectoryUrl, isGithubUrl } from "@/utils/URL"; + +const linkProps = { + size: "xs", + variant: "classic", + external: true, + target: "_blank", + rel: "noopener noreferrer", +} as const; + +export function GithubDetails({ + url, + canonicalUrl, + className, +}: { + url?: string; + canonicalUrl?: string; + className?: string; +}) { + if (!url && !canonicalUrl) return null; + + return ( + + URL + {url && ( + <> + + View raw component.yaml + + + + View directory on GitHub + + + )} + {canonicalUrl && ( + <> + + View canonical URL + + + + View canonical URL on GitHub + + + )} + + ); +} diff --git a/src/hooks/useCopyToClip.ts b/src/hooks/useCopyToClip.ts deleted file mode 100644 index 0ece06fa5..000000000 --- a/src/hooks/useCopyToClip.ts +++ /dev/null @@ -1,42 +0,0 @@ -import { useEffect, useRef, useState } from "react"; - -import { copyToClipboard } from "@/utils/string"; - -export function useCopyToClipboard(text?: string | null) { - const [isCopied, setIsCopied] = useState(false); - const [isTooltipOpen, setIsTooltipOpen] = useState(false); - const tooltipTimerRef = useRef(null); - - useEffect(() => { - return () => { - if (tooltipTimerRef.current) clearTimeout(tooltipTimerRef.current); - }; - }, []); - - const handleTooltipOpen = (open: boolean) => { - if (!open) { - if (tooltipTimerRef.current) clearTimeout(tooltipTimerRef.current); - setIsCopied(false); - } - setIsTooltipOpen(open); - }; - - const handleCopy = () => { - if (!text) return; - copyToClipboard(text); - setIsCopied(true); - setIsTooltipOpen(true); - - if (tooltipTimerRef.current) clearTimeout(tooltipTimerRef.current); - tooltipTimerRef.current = setTimeout(() => { - setIsTooltipOpen(false); - }, 1500); - }; - - return { - isCopied, - isTooltipOpen, - handleCopy, - handleTooltipOpen, - }; -}