Skip to content

Commit e9b7d36

Browse files
committed
Rework Pipeline Action Buttons into Action Framework
1 parent f2cfc9c commit e9b7d36

File tree

4 files changed

+140
-128
lines changed

4 files changed

+140
-128
lines changed

src/components/Editor/Context/PipelineDetails.tsx

Lines changed: 70 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,36 +1,53 @@
11
import { useEffect, useState } from "react";
22

33
import { useValidationIssueNavigation } from "@/components/Editor/hooks/useValidationIssueNavigation";
4-
import { ActionBlock } from "@/components/shared/ContextPanel/Blocks/ActionBlock";
4+
import {
5+
ActionBlock,
6+
type Action,
7+
} from "@/components/shared/ContextPanel/Blocks/ActionBlock";
58
import { ContentBlock } from "@/components/shared/ContextPanel/Blocks/ContentBlock";
69
import { ListBlock } from "@/components/shared/ContextPanel/Blocks/ListBlock";
710
import { TextBlock } from "@/components/shared/ContextPanel/Blocks/TextBlock";
811
import { CopyText } from "@/components/shared/CopyText/CopyText";
912
import { BlockStack } from "@/components/ui/layout";
1013
import { useComponentSpec } from "@/providers/ComponentSpecProvider";
11-
import { getComponentFileFromList } from "@/utils/componentStore";
14+
import {
15+
getComponentFileFromList,
16+
renameComponentFileInList,
17+
} from "@/utils/componentStore";
1218
import { USER_PIPELINES_LIST_NAME } from "@/utils/constants";
1319

1420
import PipelineIO from "../../shared/Execution/PipelineIO";
1521
import { PipelineValidationList } from "./PipelineValidationList";
16-
import RenamePipeline from "./RenamePipeline";
17-
import TooltipButton from "@/components/shared/Buttons/TooltipButton";
18-
import { Icon } from "@/components/ui/icon";
1922
import { componentSpecToText } from "@/utils/yaml";
2023
import { CodeViewer } from "@/components/shared/CodeViewer";
24+
import { PipelineNameDialog } from "@/components/shared/Dialogs";
25+
import { APP_ROUTES } from "@/routes/router";
26+
import useToastNotification from "@/hooks/useToastNotification";
27+
import { useLocation, useNavigate } from "@tanstack/react-router";
2128

2229
const PipelineDetails = () => {
2330
const {
2431
componentSpec,
2532
digest,
2633
isComponentTreeValid,
2734
globalValidationIssues,
35+
saveComponentSpec,
2836
} = useComponentSpec();
2937

38+
const notify = useToastNotification();
39+
const navigate = useNavigate();
40+
41+
const location = useLocation();
42+
const pathname = location.pathname;
43+
44+
const title = componentSpec?.name;
45+
3046
const { handleIssueClick, groupedIssues } = useValidationIssueNavigation(
3147
globalValidationIssues,
3248
);
3349

50+
const [isRenameDialogOpen, setIsRenameDialogOpen] = useState(false);
3451
const [isYamlFullscreen, setIsYamlFullscreen] = useState(false);
3552

3653
// State for file metadata
@@ -40,6 +57,31 @@ const PipelineDetails = () => {
4057
createdBy?: string;
4158
}>({});
4259

60+
const isSubmitDisabled = (name: string) => {
61+
return name === title;
62+
};
63+
64+
const handleTitleUpdate = async (name: string) => {
65+
if (!componentSpec) {
66+
notify("Update failed: ComponentSpec not found", "error");
67+
return;
68+
}
69+
70+
await renameComponentFileInList(
71+
USER_PIPELINES_LIST_NAME,
72+
title ?? "",
73+
name,
74+
pathname,
75+
);
76+
77+
await saveComponentSpec(name);
78+
79+
const urlName = encodeURIComponent(name);
80+
const url = APP_ROUTES.PIPELINE_EDITOR.replace("$name", urlName);
81+
82+
navigate({ to: url });
83+
};
84+
4385
// Fetch file metadata on mount or when componentSpec.name changes
4486
useEffect(() => {
4587
const fetchMeta = async () => {
@@ -78,15 +120,19 @@ const PipelineDetails = () => {
78120
componentSpec.metadata?.annotations || {},
79121
).map(([key, value]) => ({ label: key, value: String(value) }));
80122

81-
const actions = [
82-
<RenamePipeline key="rename-pipeline-action" />,
83-
<TooltipButton
84-
variant="outline"
85-
tooltip="View YAML"
86-
onClick={() => setIsYamlFullscreen(true)}
87-
>
88-
<Icon name="FileCodeCorner" />
89-
</TooltipButton>,
123+
const actions: Action[] = [
124+
{
125+
label: "Rename Pipeline",
126+
icon: "PencilLine",
127+
onClick: () => setIsRenameDialogOpen(true),
128+
},
129+
{
130+
label: "View YAML",
131+
icon: "FileCodeCorner",
132+
onClick: () => {
133+
setIsYamlFullscreen(true);
134+
},
135+
},
90136
];
91137

92138
return (
@@ -142,6 +188,16 @@ const PipelineDetails = () => {
142188
onClose={() => setIsYamlFullscreen(false)}
143189
/>
144190
)}
191+
<PipelineNameDialog
192+
open={isRenameDialogOpen}
193+
onOpenChange={setIsRenameDialogOpen}
194+
title="Name Pipeline"
195+
description="Unsaved pipeline changes will be lost."
196+
initialName={title ?? ""}
197+
onSubmit={handleTitleUpdate}
198+
submitButtonText="Update Title"
199+
isSubmitDisabled={isSubmitDisabled}
200+
/>
145201
</>
146202
);
147203
};

src/components/Editor/Context/RenamePipeline.tsx

Lines changed: 0 additions & 64 deletions
This file was deleted.

src/components/shared/ContextPanel/Blocks/ActionBlock.tsx

Lines changed: 2 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -20,12 +20,9 @@ export type Action = {
2020
| { content: ReactNode; icon?: never }
2121
);
2222

23-
// Temporary: ReactNode included for backward compatibility with some existing buttons. In the long-term we should strive for only Action types.
24-
type ActionOrReactNode = Action | ReactNode;
25-
2623
interface ActionBlockProps {
2724
title?: string;
28-
actions: ActionOrReactNode[];
25+
actions: Action[];
2926
className?: string;
3027
}
3128

@@ -60,11 +57,7 @@ export const ActionBlock = ({
6057
<BlockStack className={className}>
6158
{title && <Heading level={3}>{title}</Heading>}
6259
<InlineStack gap="2">
63-
{actions.map((action, index) => {
64-
if (!action || typeof action !== "object" || !("label" in action)) {
65-
return <div key={index}>{action}</div>;
66-
}
67-
60+
{actions.map((action) => {
6861
if (action.hidden) {
6962
return null;
7063
}

src/components/shared/Dialogs/PipelineNameDialog.tsx

Lines changed: 68 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -23,20 +23,33 @@ import { Input } from "@/components/ui/input";
2323
import { BlockStack } from "@/components/ui/layout";
2424
import useLoadUserPipelines from "@/hooks/useLoadUserPipelines";
2525

26-
interface PipelineNameDialogProps {
27-
trigger: ReactNode;
26+
type PipelineNameDialogPropsBase = {
2827
title: string;
2928
description?: string;
3029
initialName: string;
3130
submitButtonText: string;
3231
submitButtonIcon?: ReactNode;
3332
onSubmit: (name: string) => void;
3433
isSubmitDisabled?: (name: string, error: string | null) => boolean;
35-
onOpenChange?: (open: boolean) => void;
36-
}
34+
};
35+
36+
type PipelineNameDialogProps = PipelineNameDialogPropsBase &
37+
(
38+
| {
39+
trigger: ReactNode;
40+
open?: never;
41+
onOpenChange?: never;
42+
}
43+
| {
44+
trigger?: never;
45+
open: boolean;
46+
onOpenChange: (open: boolean) => void;
47+
}
48+
);
3749

3850
const PipelineNameDialog = ({
3951
trigger,
52+
open,
4053
title,
4154
description = "Please, name your pipeline.",
4255
initialName,
@@ -78,14 +91,14 @@ const PipelineNameDialog = ({
7891
);
7992

8093
const handleDialogOpenChange = useCallback(
81-
(open: boolean) => {
82-
if (!open) {
94+
(newOpen: boolean) => {
95+
if (!newOpen) {
8396
setError(null);
8497
} else {
8598
setName(initialName);
8699
refetchUserPipelines();
87100
}
88-
onOpenChange?.(open);
101+
onOpenChange?.(newOpen);
89102
},
90103
[initialName, onOpenChange, refetchUserPipelines],
91104
);
@@ -100,43 +113,57 @@ const PipelineNameDialog = ({
100113
!name ||
101114
!!isSubmitDisabled?.(name, error);
102115

116+
const dialogContent = (
117+
<DialogContent className="sm:max-w-md">
118+
<DialogHeader>
119+
<DialogTitle>{title}</DialogTitle>
120+
<DialogDescription>{description}</DialogDescription>
121+
</DialogHeader>
122+
<BlockStack gap="2">
123+
<Input value={name} onChange={handleOnChange} />
124+
<Activity mode={error ? "visible" : "hidden"}>
125+
<Alert variant="destructive">
126+
<Icon name="CircleAlert" />
127+
<AlertDescription>{error}</AlertDescription>
128+
</Alert>
129+
</Activity>
130+
</BlockStack>
131+
<DialogFooter className="sm:justify-end">
132+
<DialogClose asChild>
133+
<Button type="button" variant="secondary">
134+
Close
135+
</Button>
136+
</DialogClose>
137+
<DialogClose asChild>
138+
<Button
139+
type="button"
140+
size="sm"
141+
className="px-3"
142+
onClick={handleSubmit}
143+
disabled={isDisabled}
144+
>
145+
{submitButtonIcon}
146+
{submitButtonText}
147+
</Button>
148+
</DialogClose>
149+
</DialogFooter>
150+
</DialogContent>
151+
);
152+
153+
// Controlled mode
154+
if (open !== undefined) {
155+
return (
156+
<Dialog open={open} onOpenChange={handleDialogOpenChange}>
157+
{dialogContent}
158+
</Dialog>
159+
);
160+
}
161+
162+
// Uncontrolled mode
103163
return (
104164
<Dialog onOpenChange={handleDialogOpenChange}>
105165
<DialogTrigger asChild>{trigger}</DialogTrigger>
106-
<DialogContent className="sm:max-w-md">
107-
<DialogHeader>
108-
<DialogTitle>{title}</DialogTitle>
109-
<DialogDescription>{description}</DialogDescription>
110-
</DialogHeader>
111-
<BlockStack gap="2">
112-
<Input value={name} onChange={handleOnChange} />
113-
<Activity mode={error ? "visible" : "hidden"}>
114-
<Alert variant="destructive">
115-
<Icon name="CircleAlert" />
116-
<AlertDescription>{error}</AlertDescription>
117-
</Alert>
118-
</Activity>
119-
</BlockStack>
120-
<DialogFooter className="sm:justify-end">
121-
<DialogClose asChild>
122-
<Button type="button" variant="secondary">
123-
Close
124-
</Button>
125-
</DialogClose>
126-
<DialogClose asChild>
127-
<Button
128-
type="button"
129-
size="sm"
130-
className="px-3"
131-
onClick={handleSubmit}
132-
disabled={isDisabled}
133-
>
134-
{submitButtonIcon}
135-
{submitButtonText}
136-
</Button>
137-
</DialogClose>
138-
</DialogFooter>
139-
</DialogContent>
166+
{dialogContent}
140167
</Dialog>
141168
);
142169
};

0 commit comments

Comments
 (0)