diff --git a/src/components/shared/Buttons/TooltipButton.tsx b/src/components/shared/Buttons/TooltipButton.tsx index 7337ab756..24c0daf9b 100644 --- a/src/components/shared/Buttons/TooltipButton.tsx +++ b/src/components/shared/Buttons/TooltipButton.tsx @@ -8,7 +8,7 @@ import { TooltipTrigger, } from "@/components/ui/tooltip"; -export interface TooltipButtonProps extends ButtonProps { +interface TooltipButtonProps extends ButtonProps { tooltip: React.ReactNode; tooltipSide?: "top" | "right" | "bottom" | "left"; tooltipAlign?: "start" | "center" | "end"; diff --git a/src/components/shared/ContextPanel/Blocks/ActionBlock.tsx b/src/components/shared/ContextPanel/Blocks/ActionBlock.tsx index 39d395bcd..9b7b9ba8d 100644 --- a/src/components/shared/ContextPanel/Blocks/ActionBlock.tsx +++ b/src/components/shared/ContextPanel/Blocks/ActionBlock.tsx @@ -21,7 +21,7 @@ export type Action = { ); // Temporary: ReactNode included for backward compatibility with some existing buttons. In the long-term we should strive for only Action types. -export type ActionOrReactNode = Action | ReactNode; +type ActionOrReactNode = Action | ReactNode; interface ActionBlockProps { title?: string; diff --git a/src/components/shared/Dialogs/ComponentDetailsDialog.tsx b/src/components/shared/Dialogs/ComponentDetailsDialog.tsx index e6879bccb..2f467fc87 100644 --- a/src/components/shared/Dialogs/ComponentDetailsDialog.tsx +++ b/src/components/shared/Dialogs/ComponentDetailsDialog.tsx @@ -17,8 +17,6 @@ import { useHydrateComponentReference } from "@/hooks/useHydrateComponentReferen import type { ComponentReference } from "@/utils/componentSpec"; import InfoIconButton from "../Buttons/InfoIconButton"; -import TooltipButton from "../Buttons/TooltipButton"; -import { ComponentEditorDialog } from "../ComponentEditor/ComponentEditorDialog"; import { ComponentFavoriteToggle } from "../FavoriteComponentToggle"; import { InfoBox } from "../InfoBox"; import { PublishComponent } from "../ManageComponent/PublishComponent"; @@ -32,7 +30,6 @@ interface ComponentDetailsProps { component: ComponentReference; displayName: string; trigger?: ReactNode; - actions?: ReactNode[]; onClose?: () => void; onDelete?: () => void; } @@ -64,12 +61,7 @@ const ComponentDetailsDialogContentSkeleton = () => { }; const ComponentDetailsDialogContent = withSuspenseWrapper( - ({ - component, - displayName, - actions = [], - onDelete, - }: ComponentDetailsProps) => { + ({ component, displayName, onDelete }: ComponentDetailsProps) => { const remoteComponentLibrarySearchEnabled = useBetaFlagValue( "remote-component-library-search", ); @@ -138,7 +130,6 @@ const ComponentDetailsDialogContent = withSuspenseWrapper( componentSpec={componentSpec} componentDigest={componentDigest} url={url} - actions={actions} onDelete={onDelete} /> @@ -175,18 +166,12 @@ const ComponentDetails = ({ component, displayName, trigger, - actions = [], onClose, onDelete, }: ComponentDetailsProps) => { - const hasEnabledInAppEditor = useBetaFlagValue("in-app-component-editor"); - - const [isEditDialogOpen, setIsEditDialogOpen] = useState(false); const [open, setOpen] = useState(false); const dialogTriggerButton = trigger || ; - const componentText = component.text; - const dialogContextValue = useMemo( () => ({ name: "ComponentDetails", @@ -197,10 +182,6 @@ const ComponentDetails = ({ [], ); - const handleCloseEditDialog = useCallback(() => { - setIsEditDialogOpen(false); - }, []); - const onOpenChange = useCallback((open: boolean) => { setOpen(open); if (!open) { @@ -208,68 +189,38 @@ const ComponentDetails = ({ } }, []); - const handleEditComponent = useCallback(() => { - setIsEditDialogOpen(true); - }, []); - - const actionsWithEdit = useMemo(() => { - if (!hasEnabledInAppEditor) return actions; - - const EditButton = ( - - - - ); - - return [...actions, EditButton]; - }, [actions, hasEnabledInAppEditor, handleEditComponent]); - return ( - <> - - {dialogTriggerButton} + + {dialogTriggerButton} - - {`${displayName} component details`} - - - - - {displayName} - - - - - - - - - - {isEditDialogOpen && ( - - )} - > + + {`${displayName} component details`} + + + + + {displayName} + + + + + + + + + ); }; diff --git a/src/components/shared/ReactFlow/FlowCanvas/TaskNode/TaskNodeCard/TaskNodeCard.tsx b/src/components/shared/ReactFlow/FlowCanvas/TaskNode/TaskNodeCard/TaskNodeCard.tsx index 3511c7b61..83df609fe 100644 --- a/src/components/shared/ReactFlow/FlowCanvas/TaskNode/TaskNodeCard/TaskNodeCard.tsx +++ b/src/components/shared/ReactFlow/FlowCanvas/TaskNode/TaskNodeCard/TaskNodeCard.tsx @@ -1,8 +1,8 @@ import { useNavigate } from "@tanstack/react-router"; import { useCallback, useEffect, useMemo, useRef, useState } from "react"; -import type { TooltipButtonProps } from "@/components/shared/Buttons/TooltipButton"; -import { ComponentEditorDialog } from "@/components/shared/ComponentEditor/ComponentEditorDialog"; +import { CodeViewer } from "@/components/shared/CodeViewer"; +import type { Action } from "@/components/shared/ContextPanel/Blocks/ActionBlock"; import { PublishedComponentBadge } from "@/components/shared/ManageComponent/PublishedComponentBadge"; import { trimDigest } from "@/components/shared/ManageComponent/utils/digest"; import { useBetaFlagValue } from "@/components/shared/Settings/useBetaFlags"; @@ -36,7 +36,6 @@ const TaskNodeCard = () => { "remote-component-library-search", ); const isSubgraphNavigationEnabled = useBetaFlagValue("subgraph-navigation"); - const isInAppEditorEnabled = useBetaFlagValue("in-app-component-editor"); const { registerNode } = useNodesOverlay(); const taskNode = useTaskNode(); const { @@ -52,7 +51,7 @@ const TaskNodeCard = () => { const nodeRef = useRef(null); const contentRef = useRef(null); - const [isEditDialogOpen, setIsEditDialogOpen] = useState(false); + const [isYamlFullscreen, setIsYamlFullscreen] = useState(false); const [updateOverlayDialogOpen, setUpdateOverlayDialogOpen] = useState< UpdateOverlayMessage["data"] | undefined >(); @@ -113,67 +112,65 @@ const TaskNodeCard = () => { } }, []); - const handleEditComponent = useCallback(() => { - setIsEditDialogOpen(true); - }, []); - - const handleCloseEditDialog = useCallback(() => { - setIsEditDialogOpen(false); - }, []); + const handleDuplicateTask = useCallback(() => { + callbacks.onDuplicate?.(); + }, [callbacks]); - const taskConfigMarkup = useMemo(() => { - const actions: Array = []; - - if (!readOnly) { - 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, - }); - } + const handleUpgradeTask = useCallback(() => { + callbacks.onUpgrade?.(); + }, [callbacks]); - if (isSubgraphNode && taskId && isSubgraphNavigationEnabled) { - actions.push({ - children: , - variant: "outline", - tooltip: `Enter Subgraph: ${subgraphDescription}`, - onClick: () => navigateToSubgraph(taskId), - }); - } - - if (isInAppEditorEnabled) { - actions.push({ - children: , - variant: "outline", - tooltip: "Edit Component Definition", - onClick: handleEditComponent, - }); + const handleEnterSubgraph = useCallback(() => { + if (taskId) { + navigateToSubgraph(taskId); } + }, [navigateToSubgraph, taskId]); - return ; + const taskConfigMarkup = useMemo(() => { + const customActions: Action[] = [ + { + label: "Duplicate Task", + icon: "Copy", + hidden: readOnly, + onClick: handleDuplicateTask, + }, + { + label: "Update Task from Source URL", + icon: "CircleFadingArrowUp", + hidden: readOnly || isCustomComponent, + onClick: handleUpgradeTask, + }, + { + label: `Enter Subgraph: ${subgraphDescription}`, + icon: "Workflow", + hidden: !isSubgraphNode || !isSubgraphNavigationEnabled, + onClick: handleEnterSubgraph, + }, + { + label: "View YAML", + icon: "FileCodeCorner", + onClick: () => setIsYamlFullscreen(true), + }, + ]; + + return ( + + ); }, [ taskNode, nodeId, readOnly, callbacks.onDuplicate, callbacks.onUpgrade, - isInAppEditorEnabled, isCustomComponent, isSubgraphNode, taskId, subgraphDescription, navigateToSubgraph, - handleEditComponent, ]); const handleInputSectionClick = useCallback(() => { @@ -249,6 +246,8 @@ const TaskNodeCard = () => { ); + const componentText = taskSpec.componentRef?.text; + return ( <> { ) : null} - {isEditDialogOpen && ( - setIsYamlFullscreen(false)} /> )} > diff --git a/src/components/shared/ReactFlow/FlowCanvas/TaskNode/TaskOverview/TaskOverview.tsx b/src/components/shared/ReactFlow/FlowCanvas/TaskNode/TaskOverview/TaskOverview.tsx index 840294d24..657580208 100644 --- a/src/components/shared/ReactFlow/FlowCanvas/TaskNode/TaskOverview/TaskOverview.tsx +++ b/src/components/shared/ReactFlow/FlowCanvas/TaskNode/TaskOverview/TaskOverview.tsx @@ -5,11 +5,8 @@ import { LogsIcon, Parentheses, } from "lucide-react"; -import { useState } from "react"; -import type { TooltipButtonProps } from "@/components/shared/Buttons/TooltipButton"; -import TooltipButton from "@/components/shared/Buttons/TooltipButton"; -import { CodeViewer } from "@/components/shared/CodeViewer"; +import type { Action } from "@/components/shared/ContextPanel/Blocks/ActionBlock"; import { ComponentDetailsDialog } from "@/components/shared/Dialogs"; import { ComponentFavoriteToggle } from "@/components/shared/FavoriteComponentToggle"; import { StatusIcon } from "@/components/shared/Status"; @@ -21,7 +18,6 @@ import { Text } from "@/components/ui/typography"; import { useExecutionDataOptional } from "@/providers/ExecutionDataProvider"; import { type TaskNodeContextType } from "@/providers/TaskNodeProvider"; import { isGraphImplementation } from "@/utils/componentSpec"; -import { componentSpecToText } from "@/utils/yaml"; import ArgumentsSection from "../ArgumentsEditor/ArgumentsSection"; import ConfigurationSection from "./ConfigurationSection"; @@ -32,14 +28,12 @@ import RenameTask from "./RenameTask"; interface TaskOverviewProps { taskNode: TaskNodeContextType; - actions?: TooltipButtonProps[]; + customActions?: Action[]; } -const TaskOverview = ({ taskNode, actions }: TaskOverviewProps) => { +const TaskOverview = ({ taskNode, customActions }: TaskOverviewProps) => { const { name, taskSpec, taskId, state, callbacks } = taskNode; - const [isYamlFullscreen, setIsYamlFullscreen] = useState(false); - const executionData = useExecutionDataOptional(); const details = executionData?.details; @@ -63,134 +57,106 @@ const TaskOverview = ({ taskNode, actions }: TaskOverviewProps) => { const executionId = details?.child_task_execution_ids?.[taskId]; const canRename = !readOnly && isSubgraph; - const detailActions = [ - ...(actions?.map((action) => ( - - )) ?? []), - setIsYamlFullscreen(true)} - key="view-yaml-action" - > - - , - ]; - return ( - <> - - - {isSubgraph && } - - {name} - - {canRename && } - - - {readOnly && } - + + + {isSubgraph && } + + {name} + + {canRename && } + + + {readOnly && } + + + + + + + {readOnly ? ( + + ) : ( + + )} + {readOnly ? "Artifacts" : "Arguments"} + + + + Details + - - - - - {readOnly ? ( - - ) : ( - - )} - {readOnly ? "Artifacts" : "Arguments"} + {readOnly && !isSubgraph && ( + + + Logs - - - Details + )} + {!readOnly && ( + + + Configuration - - {readOnly && !isSubgraph && ( - - - Logs - - )} - {!readOnly && ( - - - Configuration - - )} - - - + + + + + {!readOnly && ( + <> + + + + > + )} + {readOnly && ( + - - - {!readOnly && ( - <> - + {readOnly && !isSubgraph && ( + + {!!executionId && ( + + - - - > - )} - {readOnly && ( - + )} + - {readOnly && !isSubgraph && ( - - {!!executionId && ( - - - - )} - - - )} - {!readOnly && ( - - - - )} - - - - {isYamlFullscreen && ( - setIsYamlFullscreen(false)} - /> - )} - > + )} + {!readOnly && ( + + + + )} + + + ); }; diff --git a/src/components/shared/TaskDetails/Actions.tsx b/src/components/shared/TaskDetails/Actions.tsx index 6e8ddcb1f..33f7361fb 100644 --- a/src/components/shared/TaskDetails/Actions.tsx +++ b/src/components/shared/TaskDetails/Actions.tsx @@ -1,20 +1,19 @@ -import { type ReactNode } from "react"; +import { useState } 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 copyToYaml, { componentSpecToText } from "@/utils/yaml"; -import { - ActionBlock, - type ActionOrReactNode, -} from "../ContextPanel/Blocks/ActionBlock"; +import { ComponentEditorDialog } from "../ComponentEditor/ComponentEditorDialog"; +import { type Action, ActionBlock } from "../ContextPanel/Blocks/ActionBlock"; +import { useBetaFlagValue } from "../Settings/useBetaFlags"; interface TaskActionsProps { displayName: string; componentSpec: ComponentSpec; - actions?: ReactNode[]; + customActions?: Action[]; onDelete?: () => void; readOnly?: boolean; className?: string; @@ -23,13 +22,24 @@ interface TaskActionsProps { const TaskActions = ({ displayName, componentSpec, - actions = [], + customActions = [], onDelete, readOnly = false, className, }: TaskActionsProps) => { + const hasEnabledInAppEditor = useBetaFlagValue("in-app-component-editor"); const notify = useToastNotification(); + const [isEditDialogOpen, setIsEditDialogOpen] = useState(false); + + const handleEditComponent = () => { + setIsEditDialogOpen(true); + }; + + const handleCloseEditDialog = () => { + setIsEditDialogOpen(false); + }; + const pythonOriginalCode = componentSpec?.metadata?.annotations?.original_python_code; @@ -67,8 +77,7 @@ const TaskActions = ({ notify(`Error deleting component`, "error"); } }; - - const orderedActions: ActionOrReactNode[] = [ + const sharedActions: Action[] = [ { label: "Download YAML", icon: "Download", @@ -85,7 +94,12 @@ const TaskActions = ({ icon: "Clipboard", onClick: handleCopyYaml, }, - ...actions, + { + label: "Edit Component Definition", + icon: "FilePenLine", + hidden: !hasEnabledInAppEditor, + onClick: handleEditComponent, + }, { label: "Delete Component", icon: "Trash", @@ -95,7 +109,20 @@ const TaskActions = ({ }, ]; - return ; + const allActions: Action[] = [...customActions, ...sharedActions]; + + return ( + <> + + + {isEditDialogOpen && ( + + )} + > + ); }; export default TaskActions; diff --git a/src/components/shared/TaskDetails/Details.tsx b/src/components/shared/TaskDetails/Details.tsx index 4d4c2da2e..4db455f4f 100644 --- a/src/components/shared/TaskDetails/Details.tsx +++ b/src/components/shared/TaskDetails/Details.tsx @@ -3,6 +3,7 @@ import { type ReactNode } from "react"; import { BlockStack } from "@/components/ui/layout"; import type { ComponentSpec } from "@/utils/componentSpec"; +import type { Action } from "../ContextPanel/Blocks/ActionBlock"; import { ContentBlock } from "../ContextPanel/Blocks/ContentBlock"; import { TextBlock } from "../ContextPanel/Blocks/TextBlock"; import TaskActions from "./Actions"; @@ -16,7 +17,7 @@ interface TaskDetailsProps { taskId?: string; componentDigest?: string; url?: string; - actions?: ReactNode[]; + customActions?: Action[]; onDelete?: () => void; status?: string; readOnly?: boolean; @@ -36,7 +37,7 @@ const TaskDetails = ({ taskId, componentDigest, url, - actions = [], + customActions = [], onDelete, status, readOnly = false, @@ -128,7 +129,7 @@ const TaskDetails = ({