diff --git a/src/components/Editor/Context/PipelineDetails.tsx b/src/components/Editor/Context/PipelineDetails.tsx index 10205fff14..453bcee2ea 100644 --- a/src/components/Editor/Context/PipelineDetails.tsx +++ b/src/components/Editor/Context/PipelineDetails.tsx @@ -10,7 +10,6 @@ import { CopyText } from "@/components/shared/CopyText/CopyText"; import { FavoriteToggle } from "@/components/shared/FavoriteToggle"; import { PipelineDescription } from "@/components/shared/PipelineDescription/PipelineDescription"; import { PipelineRunNameTemplateEditor } from "@/components/shared/PipelineRunNameTemplate/PipelineRunNameTemplateEditor"; -import { useFlagValue } from "@/components/shared/Settings/useFlags"; import { BlockStack } from "@/components/ui/layout"; import useToastNotification from "@/hooks/useToastNotification"; import { useComponentSpec } from "@/providers/ComponentSpecProvider"; @@ -29,10 +28,6 @@ const PipelineDetails = () => { const notify = useToastNotification(); const { componentSpec, digest, globalValidationIssues } = useComponentSpec(); - const templatizedRunNameEnabled = useFlagValue( - "templatized-pipeline-run-name", - ); - const { handleIssueClick, groupedIssues } = useValidationIssueNavigation( globalValidationIssues, ); @@ -127,11 +122,9 @@ const PipelineDetails = () => { - {templatizedRunNameEnabled && ( - - - - )} + + + {digest && ( }`, `${date.timestamp}`, `${date.short}`, `${date.long}`, and `${annotations.}`." + "body": "Set a Run name template in the pipeline's configuration panel and every submitted run gets its own auto-generated name, while the pipeline keeps its own. Available placeholders: `${arguments.}`, `${date.timestamp}`, `${date.short}`, `${date.long}`, and `${annotations.}`." }, { "id": "submit-with-arguments", diff --git a/src/components/PipelineRun/components/RerunPipelineButton.tsx b/src/components/PipelineRun/components/RerunPipelineButton.tsx index 97c883d4db..87bd653533 100644 --- a/src/components/PipelineRun/components/RerunPipelineButton.tsx +++ b/src/components/PipelineRun/components/RerunPipelineButton.tsx @@ -7,7 +7,6 @@ import { useAuthLocalStorage } from "@/components/shared/Authentication/useAuthL import { useAwaitAuthorization } from "@/components/shared/Authentication/useAwaitAuthorization"; import TooltipButton from "@/components/shared/Buttons/TooltipButton"; import { buildTaskSpecShape } from "@/components/shared/PipelineRunNameTemplate/types"; -import { useFlagValue } from "@/components/shared/Settings/useFlags"; import { Icon } from "@/components/ui/icon"; import useToastNotification from "@/hooks/useToastNotification"; import { useBackend } from "@/providers/BackendProvider"; @@ -35,7 +34,6 @@ export const RerunPipelineButton = ({ showTooltip = true, ...rest }: RerunPipelineButtonProps) => { - const runNameOverride = useFlagValue("templatized-pipeline-run-name"); const { backendUrl } = useBackend(); const navigate = useNavigate(); const notify = useToastNotification(); @@ -85,7 +83,6 @@ export const RerunPipelineButton = ({ taskArguments: executionData?.rootDetails?.task_spec .arguments as Record, authorizationToken, - runNameOverride, onSuccess: resolve, onError: reject, }); diff --git a/src/components/shared/Submitters/Tangle/TangleSubmitter.tsx b/src/components/shared/Submitters/Tangle/TangleSubmitter.tsx index f9081b063b..0dc7c3e53d 100644 --- a/src/components/shared/Submitters/Tangle/TangleSubmitter.tsx +++ b/src/components/shared/Submitters/Tangle/TangleSubmitter.tsx @@ -43,7 +43,6 @@ interface TangleSubmitterProps { } function useSubmitPipeline() { - const runNameOverride = useFlagValue("templatized-pipeline-run-name"); const { awaitAuthorization, isAuthorized } = useAwaitAuthorization(); const queryClient = useQueryClient(); const { getToken } = useAuthLocalStorage(); @@ -76,7 +75,6 @@ function useSubmitPipeline() { submitPipelineRun(componentSpec, backendUrl, { authorizationToken: authorizationToken.current, taskArguments, - runNameOverride, onSuccess: (data) => { resolve(data); onSuccess(data); diff --git a/src/flags.ts b/src/flags.ts index 2d7a932197..3f1d7749ff 100644 --- a/src/flags.ts +++ b/src/flags.ts @@ -31,14 +31,6 @@ export const ExistingFlags: ConfigFlags = { category: "setting", }, - ["templatized-pipeline-run-name"]: { - name: "Templatized pipeline run name", - description: - "Enable the templatized pipeline run name feature. This will generate a run name for each pipeline run based on the template with placeholders.", - default: false, - category: "beta", - }, - ["input-aggregator"]: { name: "Input Aggregator Component", description: diff --git a/src/models/componentSpec/annotations.ts b/src/models/componentSpec/annotations.ts index b57929af13..de2a25bddb 100644 --- a/src/models/componentSpec/annotations.ts +++ b/src/models/componentSpec/annotations.ts @@ -38,6 +38,7 @@ interface AnnotationTypeMap { "flex-nodes": FlexNodeData[]; notes: string; tags: string[]; + "run-name-template": string; } type KnownAnnotationKey = keyof AnnotationTypeMap; @@ -125,6 +126,11 @@ const codecs = { : [], defaultValue: [] as string[], }, + "run-name-template": { + serialize: (value: string) => value, + deserialize: (raw: unknown) => (typeof raw === "string" ? raw : ""), + defaultValue: "", + }, } satisfies { [K in KnownAnnotationKey]: AnnotationCodec; }; diff --git a/src/routes/v2/pages/Editor/components/PipelineDetailsContent/PipelineDetailsContent.tsx b/src/routes/v2/pages/Editor/components/PipelineDetailsContent/PipelineDetailsContent.tsx index 0ac6e37049..1c3a844c17 100644 --- a/src/routes/v2/pages/Editor/components/PipelineDetailsContent/PipelineDetailsContent.tsx +++ b/src/routes/v2/pages/Editor/components/PipelineDetailsContent/PipelineDetailsContent.tsx @@ -16,6 +16,7 @@ import { useSpec } from "@/routes/v2/shared/providers/SpecContext"; import { useSharedStores } from "@/routes/v2/shared/store/SharedStoreContext"; import { PIPELINE_NOTES_ANNOTATION, + RUN_NAME_TEMPLATE_ANNOTATION, SYSTEM_ANNOTATIONS, } from "@/utils/annotations"; @@ -32,8 +33,12 @@ export const PipelineDetailsContent = observer( const { navigation } = useSharedStores(); const pipelineSpec = useSpec(); const notify = useToastNotification(); - const { updatePipelineDescription, updatePipelineNotes, renameSubgraph } = - usePipelineActions(); + const { + updatePipelineDescription, + updatePipelineNotes, + updateRunNameTemplate, + renameSubgraph, + } = usePipelineActions(); if (!pipelineSpec) { return ( @@ -69,6 +74,17 @@ export const PipelineDetailsContent = observer( } }; + const handleRunNameTemplateCommit = (value: string | undefined) => { + const currentTemplate = + pipelineSpec.annotations.get(RUN_NAME_TEMPLATE_ANNOTATION) || undefined; + if (value !== currentTemplate) { + updateRunNameTemplate(pipelineSpec, value); + track( + "v2.pipeline_editor.configuration_panel.run_name_template.updated", + ); + } + }; + const handleRenameSubgraph = (newName: string): boolean => { const success = renameSubgraph(newName); if (success) { @@ -123,6 +139,17 @@ export const PipelineDetailsContent = observer( testId="pipeline-notes-input" /> + + diff --git a/src/routes/v2/pages/Editor/store/actions/__tests__/pipeline.actions.test.ts b/src/routes/v2/pages/Editor/store/actions/__tests__/pipeline.actions.test.ts new file mode 100644 index 0000000000..1182d41bda --- /dev/null +++ b/src/routes/v2/pages/Editor/store/actions/__tests__/pipeline.actions.test.ts @@ -0,0 +1,38 @@ +import { describe, expect, it } from "vitest"; + +import { ComponentSpec, serializeComponentSpec } from "@/models/componentSpec"; +import { updateRunNameTemplate } from "@/routes/v2/pages/Editor/store/actions/pipeline.actions"; +import { RUN_NAME_TEMPLATE_ANNOTATION } from "@/utils/annotations"; + +const noopUndo = { + withGroup: (_label: string, fn: () => T): T => fn(), +}; + +const makeSpec = () => new ComponentSpec({ $id: "spec_1", name: "Pipeline" }); + +describe("updateRunNameTemplate", () => { + it("stores the template so it survives serialization to the wire spec", () => { + const spec = makeSpec(); + + updateRunNameTemplate(noopUndo, spec, "${arguments.dataset}-run"); + + expect(spec.annotations.get(RUN_NAME_TEMPLATE_ANNOTATION)).toBe( + "${arguments.dataset}-run", + ); + expect( + serializeComponentSpec(spec).metadata?.annotations?.[ + RUN_NAME_TEMPLATE_ANNOTATION + ], + ).toBe("${arguments.dataset}-run"); + }); + + it("removes the annotation when the template is cleared", () => { + const spec = makeSpec(); + updateRunNameTemplate(noopUndo, spec, "${date.timestamp}"); + + updateRunNameTemplate(noopUndo, spec, undefined); + + expect(spec.annotations.has(RUN_NAME_TEMPLATE_ANNOTATION)).toBe(false); + expect(spec.annotations.get(RUN_NAME_TEMPLATE_ANNOTATION)).toBe(""); + }); +}); diff --git a/src/routes/v2/pages/Editor/store/actions/pipeline.actions.ts b/src/routes/v2/pages/Editor/store/actions/pipeline.actions.ts index d4710e671c..4bb945751a 100644 --- a/src/routes/v2/pages/Editor/store/actions/pipeline.actions.ts +++ b/src/routes/v2/pages/Editor/store/actions/pipeline.actions.ts @@ -12,6 +12,7 @@ import { EDITOR_POSITION_ANNOTATION, PIPELINE_NOTES_ANNOTATION, PIPELINE_TAGS_ANNOTATION, + RUN_NAME_TEMPLATE_ANNOTATION, } from "@/utils/annotations"; import { idGen } from "./utils"; @@ -63,6 +64,21 @@ export function updatePipelineNotes( }); } +export function updateRunNameTemplate( + undo: UndoGroupable, + spec: ComponentSpec, + template: string | undefined, +): boolean { + return undo.withGroup("Update run name template", () => { + if (template) { + spec.annotations.set(RUN_NAME_TEMPLATE_ANNOTATION, template); + } else { + spec.annotations.remove(RUN_NAME_TEMPLATE_ANNOTATION); + } + return true; + }); +} + export function updatePipelineTags( undo: UndoGroupable, spec: ComponentSpec, diff --git a/src/routes/v2/pages/Editor/store/actions/usePipelineActions.ts b/src/routes/v2/pages/Editor/store/actions/usePipelineActions.ts index a24ce7cf4c..f93180cee6 100644 --- a/src/routes/v2/pages/Editor/store/actions/usePipelineActions.ts +++ b/src/routes/v2/pages/Editor/store/actions/usePipelineActions.ts @@ -8,6 +8,7 @@ import { updatePipelineDescription, updatePipelineNotes, updatePipelineTags, + updateRunNameTemplate, } from "./pipeline.actions"; export function usePipelineActions() { @@ -20,6 +21,7 @@ export function usePipelineActions() { updatePipelineDescription: updatePipelineDescription.bind(null, undo), updatePipelineNotes: updatePipelineNotes.bind(null, undo), updatePipelineTags: updatePipelineTags.bind(null, undo), + updateRunNameTemplate: updateRunNameTemplate.bind(null, undo), createSubgraph: createSubgraph.bind(null, undo), }; } diff --git a/src/routes/v2/pages/RunView/hooks/useRerunPipelineRun.ts b/src/routes/v2/pages/RunView/hooks/useRerunPipelineRun.ts index 3c624080f3..9ce63e3bc8 100644 --- a/src/routes/v2/pages/RunView/hooks/useRerunPipelineRun.ts +++ b/src/routes/v2/pages/RunView/hooks/useRerunPipelineRun.ts @@ -5,7 +5,6 @@ import { isAuthorizationRequired } from "@/components/shared/Authentication/help import { useAuthLocalStorage } from "@/components/shared/Authentication/useAuthLocalStorage"; import { useAwaitAuthorization } from "@/components/shared/Authentication/useAwaitAuthorization"; import { buildTaskSpecShape } from "@/components/shared/PipelineRunNameTemplate/types"; -import { useFlagValue } from "@/components/shared/Settings/useFlags"; import useToastNotification from "@/hooks/useToastNotification"; import { useBackend } from "@/providers/BackendProvider"; import { useExecutionData } from "@/providers/ExecutionDataProvider"; @@ -25,7 +24,6 @@ export function useRerunPipelineRun(componentSpec?: ComponentSpec) { const navigate = useNavigate(); const notify = useToastNotification(); const { backendUrl } = useBackend(); - const runNameOverride = useFlagValue("templatized-pipeline-run-name"); const { awaitAuthorization, isAuthorized } = useAwaitAuthorization(); const { getToken } = useAuthLocalStorage(); const { rootDetails } = useExecutionData(); @@ -52,7 +50,6 @@ export function useRerunPipelineRun(componentSpec?: ComponentSpec) { canonicalName, taskArguments, authorizationToken, - runNameOverride, onSuccess: resolve, onError: reject, }); diff --git a/src/utils/annotations.ts b/src/utils/annotations.ts index ab427e1fd1..89f4b4782e 100644 --- a/src/utils/annotations.ts +++ b/src/utils/annotations.ts @@ -11,6 +11,7 @@ import { FLEX_NODES_ANNOTATION, PIPELINE_NOTES_ANNOTATION, PIPELINE_TAGS_ANNOTATION, + RUN_NAME_TEMPLATE_ANNOTATION, SDK_ANNOTATION, TASK_COLOR_ANNOTATION, TASK_DISPLAY_NAME_ANNOTATION, @@ -26,6 +27,7 @@ const PIPELINE_AGGREGATOR_ANNOTATION = "is_input_aggregator"; export const SYSTEM_ANNOTATIONS = [ PIPELINE_NOTES_ANNOTATION, PIPELINE_TAGS_ANNOTATION, + RUN_NAME_TEMPLATE_ANNOTATION, FLEX_NODES_ANNOTATION, SDK_ANNOTATION, EDITOR_POSITION_ANNOTATION, diff --git a/src/utils/submitPipeline.test.ts b/src/utils/submitPipeline.test.ts index 4561a8635d..0ad1af19b2 100644 --- a/src/utils/submitPipeline.test.ts +++ b/src/utils/submitPipeline.test.ts @@ -235,6 +235,71 @@ describe("submitPipelineRun", () => { }); }); + describe("run name template", () => { + const templatedSpec = (template: string): ComponentSpec => ({ + name: "my-pipeline", + inputs: [{ name: "dataset", default: "iris" }], + implementation: { container: { image: "test:latest" } }, + metadata: { annotations: { "run-name-template": template } }, + }); + + it("resolves the template into the submitted run name", async () => { + await submitPipelineRun( + templatedSpec("${arguments.dataset}-run"), + mockBackendUrl, + ); + + const [payload] = vi.mocked(pipelineRunService.createPipelineRun).mock + .calls[0]!; + expect(payload.root_task.componentRef.spec!.name).toBe("iris-run"); + }); + + it("preserves the pipeline name as the canonical name annotation", async () => { + await submitPipelineRun( + templatedSpec("${arguments.dataset}-run"), + mockBackendUrl, + ); + + const [payload] = vi.mocked(pipelineRunService.createPipelineRun).mock + .calls[0]!; + expect(payload.root_task.annotations).toEqual({ + "canonical-pipeline-name": "my-pipeline", + }); + expect(pipelineRunService.savePipelineRun).toHaveBeenCalledWith( + mockPipelineRun, + "my-pipeline", + undefined, + "iris-run", + ); + }); + + it("prefers taskArguments over input defaults when resolving placeholders", async () => { + await submitPipelineRun( + templatedSpec("${arguments.dataset}-run"), + mockBackendUrl, + { taskArguments: { dataset: "titanic" } }, + ); + + const [payload] = vi.mocked(pipelineRunService.createPipelineRun).mock + .calls[0]!; + expect(payload.root_task.componentRef.spec!.name).toBe("titanic-run"); + }); + + it("leaves the pipeline name untouched when there is no template", async () => { + const componentSpec: ComponentSpec = { + name: "my-pipeline", + implementation: { container: { image: "test:latest" } }, + }; + + await submitPipelineRun(componentSpec, mockBackendUrl); + + const [payload] = vi.mocked(pipelineRunService.createPipelineRun).mock + .calls[0]!; + expect(payload.root_task.componentRef.spec!.name).toBe("my-pipeline"); + expect(payload.root_task.annotations).toEqual({}); + }); + }); + describe("taskArguments handling", () => { it("should include taskArguments in payload when provided", async () => { // Arrange diff --git a/src/utils/submitPipeline.ts b/src/utils/submitPipeline.ts index 08c88efcbc..cf95bacadc 100644 --- a/src/utils/submitPipeline.ts +++ b/src/utils/submitPipeline.ts @@ -28,7 +28,6 @@ export async function submitPipelineRun( options?: { taskArguments?: Record; authorizationToken?: string; - runNameOverride?: boolean; canonicalName?: string; onSuccess?: (data: PipelineRun) => void; onError?: (error: Error) => void; @@ -75,16 +74,15 @@ export async function submitPipelineRun( .map(([k, v]) => [k, v as string]), ); - const runNameOverride = options?.runNameOverride - ? processTemplate(getRunNameTemplate(transformedSpec) ?? "", { - componentRef: { - spec: transformedSpec, - }, - arguments: stringArguments, - }) || undefined - : undefined; + const templatizedRunName = + processTemplate(getRunNameTemplate(transformedSpec) ?? "", { + componentRef: { + spec: transformedSpec, + }, + arguments: stringArguments, + }) || undefined; - const taskAnnotations = runNameOverride + const taskAnnotations = templatizedRunName ? buildAnnotationsWithCanonicalName(pipelineName) : {}; @@ -96,7 +94,7 @@ export async function submitPipelineRun( componentRef: { spec: { ...transformedSpec, - name: runNameOverride ?? pipelineName, + name: templatizedRunName ?? pipelineName, } as ComponentSpecInput, }, ...(payloadArguments ? { arguments: payloadArguments } : {}), @@ -115,7 +113,7 @@ export async function submitPipelineRun( responseData, pipelineName, componentSpec.metadata?.annotations?.digest as string | undefined, - runNameOverride, + templatizedRunName, ); } options?.onSuccess?.(responseData);