Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 3 additions & 10 deletions src/components/Editor/Context/PipelineDetails.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand All @@ -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,
);
Expand Down Expand Up @@ -127,11 +122,9 @@ const PipelineDetails = () => {

<PipelineDescription componentSpec={componentSpec} />

{templatizedRunNameEnabled && (
<ContentBlock title="Run Name Template">
<PipelineRunNameTemplateEditor />
</ContentBlock>
)}
<ContentBlock title="Run Name Template">
<PipelineRunNameTemplateEditor />
</ContentBlock>

{digest && (
<TextBlock
Expand Down
2 changes: 1 addition & 1 deletion src/components/Learn/tips.json
Original file line number Diff line number Diff line change
Expand Up @@ -219,7 +219,7 @@
"id": "templatized-run-names",
"category": "Runs",
"title": "Templatize run names so you can tell experiments apart",
"body": "Turn on Templatized pipeline run name in Settings → Beta Features, then add a template so each submitted run gets a unique auto-generated name. Available placeholders: `${arguments.<input name>}`, `${date.timestamp}`, `${date.short}`, `${date.long}`, and `${annotations.<key>}`."
"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.<input name>}`, `${date.timestamp}`, `${date.short}`, `${date.long}`, and `${annotations.<key>}`."
},
{
"id": "submit-with-arguments",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -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();
Expand Down Expand Up @@ -85,7 +83,6 @@ export const RerunPipelineButton = ({
taskArguments: executionData?.rootDetails?.task_spec
.arguments as Record<string, ArgumentType>,
authorizationToken,
runNameOverride,
onSuccess: resolve,
onError: reject,
});
Expand Down
2 changes: 0 additions & 2 deletions src/components/shared/Submitters/Tangle/TangleSubmitter.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down Expand Up @@ -76,7 +75,6 @@ function useSubmitPipeline() {
submitPipelineRun(componentSpec, backendUrl, {
authorizationToken: authorizationToken.current,
taskArguments,
runNameOverride,
onSuccess: (data) => {
resolve(data);
onSuccess(data);
Expand Down
8 changes: 0 additions & 8 deletions src/flags.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
6 changes: 6 additions & 0 deletions src/models/componentSpec/annotations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ interface AnnotationTypeMap {
"flex-nodes": FlexNodeData[];
notes: string;
tags: string[];
"run-name-template": string;
}

type KnownAnnotationKey = keyof AnnotationTypeMap;
Expand Down Expand Up @@ -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<AnnotationTypeMap[K]>;
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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";

Expand All @@ -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 (
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -123,6 +139,17 @@ export const PipelineDetailsContent = observer(
testId="pipeline-notes-input"
/>
<Separator />
<PipelineDetailsTextField
title="Run name template"
id="pipeline-run-name-template"
initialValue={pipelineSpec.annotations.get(
RUN_NAME_TEMPLATE_ANNOTATION,
)}
onCommit={handleRunNameTemplateCommit}
placeholder="e.g. ${arguments.dataset}-${date.timestamp}"
testId="pipeline-run-name-template-input"
/>
<Separator />
<TagsBlock spec={pipelineSpec} />
</BlockStack>
</PipelineDetailsCollapsibleSection>
Expand Down
Original file line number Diff line number Diff line change
@@ -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: <T>(_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("");
});
});
16 changes: 16 additions & 0 deletions src/routes/v2/pages/Editor/store/actions/pipeline.actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {
updatePipelineDescription,
updatePipelineNotes,
updatePipelineTags,
updateRunNameTemplate,
} from "./pipeline.actions";

export function usePipelineActions() {
Expand All @@ -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),
};
}
3 changes: 0 additions & 3 deletions src/routes/v2/pages/RunView/hooks/useRerunPipelineRun.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand All @@ -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();
Expand All @@ -52,7 +50,6 @@ export function useRerunPipelineRun(componentSpec?: ComponentSpec) {
canonicalName,
taskArguments,
authorizationToken,
runNameOverride,
onSuccess: resolve,
onError: reject,
});
Expand Down
2 changes: 2 additions & 0 deletions src/utils/annotations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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,
Expand Down
65 changes: 65 additions & 0 deletions src/utils/submitPipeline.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Loading
Loading