diff --git a/src/components/PipelineRun/components/RerunPipelineButton.test.tsx b/src/components/PipelineRun/components/RerunPipelineButton.test.tsx
index 5eed25c0d..f845488d5 100644
--- a/src/components/PipelineRun/components/RerunPipelineButton.test.tsx
+++ b/src/components/PipelineRun/components/RerunPipelineButton.test.tsx
@@ -22,6 +22,7 @@ const {
mockIsAuthorized,
mockGetToken,
mockFetch,
+ mockUseExecutionDataOptional,
} = vi.hoisted(() => ({
navigateMock: vi.fn(),
notifyMock: vi.fn(),
@@ -31,6 +32,7 @@ const {
mockIsAuthorized: vi.fn(),
mockGetToken: vi.fn(),
mockFetch: vi.fn(),
+ mockUseExecutionDataOptional: vi.fn(),
}));
// Set up mocks
@@ -67,6 +69,10 @@ vi.mock("@/utils/submitPipeline", () => ({
submitPipelineRun: mockSubmitPipelineRun,
}));
+vi.mock("@/providers/ExecutionDataProvider", () => ({
+ useExecutionDataOptional: mockUseExecutionDataOptional,
+}));
+
const testOrigin = import.meta.env.VITE_BASE_URL || "http://localhost:3000";
Object.defineProperty(window, "location", {
@@ -108,6 +114,7 @@ describe("", () => {
mockIsAuthorized.mockReturnValue(true);
mockGetToken.mockReturnValue("mock-token");
mockAwaitAuthorization.mockClear();
+ mockUseExecutionDataOptional.mockReturnValue(undefined);
});
afterEach(async () => {
@@ -334,4 +341,81 @@ describe("", () => {
);
});
});
+
+ test("passes taskArguments from execution data to submitPipelineRun", async () => {
+ const mockTaskArguments = {
+ input_param: "test_value",
+ another_param: "another_value",
+ };
+
+ mockUseExecutionDataOptional.mockReturnValue({
+ rootDetails: {
+ task_spec: {
+ arguments: mockTaskArguments,
+ },
+ },
+ });
+
+ mockSubmitPipelineRun.mockImplementation(async (_, __, { onSuccess }) => {
+ onSuccess({ id: 456 });
+ });
+
+ await act(async () => {
+ renderWithProviders(
+ ,
+ );
+ });
+
+ const rerunButton = screen.getByTestId("rerun-pipeline-button");
+
+ await act(async () => {
+ fireEvent.click(rerunButton);
+ });
+
+ await waitFor(() => {
+ expect(mockSubmitPipelineRun).toHaveBeenCalledWith(
+ componentSpec,
+ expect.any(String),
+ expect.objectContaining({
+ taskArguments: mockTaskArguments,
+ authorizationToken: "mock-token",
+ onSuccess: expect.any(Function),
+ onError: expect.any(Function),
+ }),
+ );
+ });
+ });
+
+ test("passes undefined taskArguments when execution data is not available", async () => {
+ mockUseExecutionDataOptional.mockReturnValue(undefined);
+
+ mockSubmitPipelineRun.mockImplementation(async (_, __, { onSuccess }) => {
+ onSuccess({ id: 789 });
+ });
+
+ await act(async () => {
+ renderWithProviders(
+ ,
+ );
+ });
+
+ const rerunButton = screen.getByTestId("rerun-pipeline-button");
+
+ await act(async () => {
+ fireEvent.click(rerunButton);
+ });
+
+ await waitFor(() => {
+ expect(mockSubmitPipelineRun).toHaveBeenCalledWith(
+ componentSpec,
+ expect.any(String),
+ expect.objectContaining({
+ taskArguments: undefined,
+ authorizationToken: "mock-token",
+ onSuccess: expect.any(Function),
+ onError: expect.any(Function),
+ }),
+ );
+ });
+ });
});
diff --git a/src/components/PipelineRun/components/RerunPipelineButton.tsx b/src/components/PipelineRun/components/RerunPipelineButton.tsx
index 27e01e039..1fe5ea238 100644
--- a/src/components/PipelineRun/components/RerunPipelineButton.tsx
+++ b/src/components/PipelineRun/components/RerunPipelineButton.tsx
@@ -9,6 +9,7 @@ import { useAwaitAuthorization } from "@/components/shared/Authentication/useAwa
import TooltipButton from "@/components/shared/Buttons/TooltipButton";
import useToastNotification from "@/hooks/useToastNotification";
import { useBackend } from "@/providers/BackendProvider";
+import { useExecutionDataOptional } from "@/providers/ExecutionDataProvider";
import { APP_ROUTES } from "@/routes/router";
import type { PipelineRun } from "@/types/pipelineRun";
import type { ComponentSpec } from "@/utils/componentSpec";
@@ -24,6 +25,7 @@ export const RerunPipelineButton = ({
const { backendUrl } = useBackend();
const navigate = useNavigate();
const notify = useToastNotification();
+ const executionData = useExecutionDataOptional();
const { awaitAuthorization, isAuthorized } = useAwaitAuthorization();
const { getToken } = useAuthLocalStorage();
@@ -59,6 +61,7 @@ export const RerunPipelineButton = ({
return new Promise((resolve, reject) => {
submitPipelineRun(componentSpec, backendUrl, {
+ taskArguments: executionData?.rootDetails?.task_spec.arguments,
authorizationToken,
onSuccess: resolve,
onError: reject,
diff --git a/src/utils/submitPipeline.test.ts b/src/utils/submitPipeline.test.ts
index 52b2c69c3..9a1a03401 100644
--- a/src/utils/submitPipeline.test.ts
+++ b/src/utils/submitPipeline.test.ts
@@ -1,7 +1,6 @@
import yaml from "js-yaml";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
-import * as getArgumentsFromInputs from "@/components/shared/ReactFlow/FlowCanvas/utils/getArgumentsFromInputs";
import * as pipelineRunService from "@/services/pipelineRunService";
import type { PipelineRun } from "@/types/pipelineRun";
@@ -14,13 +13,6 @@ vi.mock("@/services/pipelineRunService", () => ({
savePipelineRun: vi.fn(),
}));
-vi.mock(
- "@/components/shared/ReactFlow/FlowCanvas/utils/getArgumentsFromInputs",
- () => ({
- getArgumentsFromInputs: vi.fn(),
- }),
-);
-
describe("submitPipelineRun", () => {
const mockBackendUrl = "https://api.example.com";
const mockAuthToken = "test-auth-token";
@@ -44,9 +36,6 @@ describe("submitPipelineRun", () => {
global.fetch = mockFetch;
// Setup default mocks
- vi.mocked(getArgumentsFromInputs.getArgumentsFromInputs).mockReturnValue(
- {},
- );
vi.mocked(pipelineRunService.createPipelineRun).mockResolvedValue(
mockPipelineRun,
);
@@ -103,18 +92,17 @@ describe("submitPipelineRun", () => {
expect(mockOnError).not.toHaveBeenCalled();
});
- it("should include arguments when getArgumentsFromInputs returns data", async () => {
+ it("should include arguments when componentSpec has inputs with values", async () => {
// Arrange
const componentSpec: ComponentSpec = {
name: "component-with-args",
+ inputs: [
+ { name: "param1", value: "value1" },
+ { name: "param2", value: "value2" },
+ ],
implementation: { container: { image: "test:latest" } },
};
- const mockArguments = { param1: "value1", param2: "value2" };
- vi.mocked(getArgumentsFromInputs.getArgumentsFromInputs).mockReturnValue(
- mockArguments,
- );
-
// Act
await submitPipelineRun(componentSpec, mockBackendUrl);
@@ -125,7 +113,7 @@ describe("submitPipelineRun", () => {
componentRef: {
spec: componentSpec,
},
- arguments: mockArguments,
+ arguments: { param1: "value1", param2: "value2" },
},
},
mockBackendUrl,
@@ -168,6 +156,211 @@ describe("submitPipelineRun", () => {
});
});
+ describe("taskArguments handling", () => {
+ it("should include taskArguments in payload when provided", async () => {
+ // Arrange
+ const componentSpec: ComponentSpec = {
+ name: "component-with-task-args",
+ implementation: { container: { image: "test:latest" } },
+ };
+
+ const mockTaskArguments = {
+ inputA: "valueA",
+ inputB: "valueB",
+ };
+
+ // Act
+ await submitPipelineRun(componentSpec, mockBackendUrl, {
+ taskArguments: mockTaskArguments,
+ });
+
+ // Assert
+ expect(pipelineRunService.createPipelineRun).toHaveBeenCalledWith(
+ {
+ root_task: {
+ componentRef: {
+ spec: componentSpec,
+ },
+ arguments: {
+ inputA: "valueA",
+ inputB: "valueB",
+ },
+ },
+ },
+ mockBackendUrl,
+ undefined,
+ );
+ });
+
+ it("should merge taskArguments with argumentsFromInputs", async () => {
+ // Arrange
+ const componentSpec: ComponentSpec = {
+ name: "merged-args-component",
+ inputs: [
+ { name: "fromInput1", value: "inputValue1" },
+ { name: "fromInput2", value: "inputValue2" },
+ ],
+ implementation: { container: { image: "test:latest" } },
+ };
+
+ const mockTaskArguments = {
+ taskArg1: "taskValue1",
+ taskArg2: "taskValue2",
+ };
+
+ // Act
+ await submitPipelineRun(componentSpec, mockBackendUrl, {
+ taskArguments: mockTaskArguments,
+ });
+
+ // Assert
+ expect(pipelineRunService.createPipelineRun).toHaveBeenCalledWith(
+ {
+ root_task: {
+ componentRef: {
+ spec: componentSpec,
+ },
+ arguments: {
+ fromInput1: "inputValue1",
+ fromInput2: "inputValue2",
+ taskArg1: "taskValue1",
+ taskArg2: "taskValue2",
+ },
+ },
+ },
+ mockBackendUrl,
+ undefined,
+ );
+ });
+
+ it("should override argumentsFromInputs with taskArguments when keys conflict", async () => {
+ // Arrange
+ const componentSpec: ComponentSpec = {
+ name: "override-args-component",
+ inputs: [
+ { name: "sharedKey", value: "fromInputValue" },
+ { name: "uniqueInputKey", value: "inputOnlyValue" },
+ ],
+ implementation: { container: { image: "test:latest" } },
+ };
+
+ const mockTaskArguments = {
+ sharedKey: "fromTaskValue",
+ uniqueTaskKey: "taskOnlyValue",
+ };
+
+ // Act
+ await submitPipelineRun(componentSpec, mockBackendUrl, {
+ taskArguments: mockTaskArguments,
+ });
+
+ // Assert - taskArguments should override argumentsFromInputs
+ expect(pipelineRunService.createPipelineRun).toHaveBeenCalledWith(
+ {
+ root_task: {
+ componentRef: {
+ spec: componentSpec,
+ },
+ arguments: {
+ sharedKey: "fromTaskValue",
+ uniqueInputKey: "inputOnlyValue",
+ uniqueTaskKey: "taskOnlyValue",
+ },
+ },
+ },
+ mockBackendUrl,
+ undefined,
+ );
+ });
+
+ it("should filter out non-string taskArguments values", async () => {
+ // Arrange
+ const componentSpec: ComponentSpec = {
+ name: "filter-args-component",
+ implementation: { container: { image: "test:latest" } },
+ };
+
+ const mockTaskArguments = {
+ stringArg: "validString",
+ objectArg: { nested: "value" },
+ numberArg: 123,
+ };
+
+ // Act
+ await submitPipelineRun(componentSpec, mockBackendUrl, {
+ taskArguments: mockTaskArguments as any,
+ });
+
+ // Assert - only string values should be included
+ expect(pipelineRunService.createPipelineRun).toHaveBeenCalledWith(
+ {
+ root_task: {
+ componentRef: {
+ spec: componentSpec,
+ },
+ arguments: {
+ stringArg: "validString",
+ objectArg: undefined,
+ numberArg: undefined,
+ },
+ },
+ },
+ mockBackendUrl,
+ undefined,
+ );
+ });
+
+ it("should work with empty taskArguments object", async () => {
+ // Arrange
+ const componentSpec: ComponentSpec = {
+ name: "empty-task-args-component",
+ inputs: [{ name: "inputArg", value: "inputValue" }],
+ implementation: { container: { image: "test:latest" } },
+ };
+
+ // Act
+ await submitPipelineRun(componentSpec, mockBackendUrl, {
+ taskArguments: {},
+ });
+
+ // Assert - should only contain argumentsFromInputs
+ expect(pipelineRunService.createPipelineRun).toHaveBeenCalledWith(
+ {
+ root_task: {
+ componentRef: {
+ spec: componentSpec,
+ },
+ arguments: {
+ inputArg: "inputValue",
+ },
+ },
+ },
+ mockBackendUrl,
+ undefined,
+ );
+ });
+
+ it("should call onSuccess callback with taskArguments submission", async () => {
+ // Arrange
+ const componentSpec: ComponentSpec = {
+ name: "success-callback-component",
+ implementation: { container: { image: "test:latest" } },
+ };
+
+ const mockOnSuccess = vi.fn();
+ const mockTaskArguments = { arg1: "value1" };
+
+ // Act
+ await submitPipelineRun(componentSpec, mockBackendUrl, {
+ taskArguments: mockTaskArguments,
+ onSuccess: mockOnSuccess,
+ });
+
+ // Assert
+ expect(mockOnSuccess).toHaveBeenCalledWith(mockPipelineRun);
+ });
+ });
+
describe("component processing", () => {
it("should fetch and process components with URLs", async () => {
// Arrange
diff --git a/src/utils/submitPipeline.ts b/src/utils/submitPipeline.ts
index f844c16b6..e64de2dbd 100644
--- a/src/utils/submitPipeline.ts
+++ b/src/utils/submitPipeline.ts
@@ -1,4 +1,7 @@
-import type { BodyCreateApiPipelineRunsPost } from "@/api/types.gen";
+import type {
+ BodyCreateApiPipelineRunsPost,
+ TaskSpecOutput,
+} from "@/api/types.gen";
import { getArgumentsFromInputs } from "@/components/shared/ReactFlow/FlowCanvas/utils/getArgumentsFromInputs";
import {
createPipelineRun,
@@ -7,12 +10,14 @@ import {
import type { PipelineRun } from "@/types/pipelineRun";
import type { ComponentReference, ComponentSpec } from "./componentSpec";
+import { getArgumentValue } from "./nodes/taskArguments";
import { componentSpecFromYaml } from "./yaml";
export async function submitPipelineRun(
componentSpec: ComponentSpec,
backendUrl: string,
options?: {
+ taskArguments?: TaskSpecOutput["arguments"];
authorizationToken?: string;
onSuccess?: (data: PipelineRun) => void;
onError?: (error: Error) => void;
@@ -31,13 +36,25 @@ export async function submitPipelineRun(
},
);
const argumentsFromInputs = getArgumentsFromInputs(fullyLoadedSpec);
+ const normalizedTaskArguments = options?.taskArguments
+ ? Object.fromEntries(
+ Object.entries(options.taskArguments).map(([key, _]) => [
+ key,
+ getArgumentValue(options.taskArguments, key),
+ ]),
+ )
+ : {};
+ const payloadArguments = {
+ ...argumentsFromInputs,
+ ...normalizedTaskArguments,
+ };
const payload = {
root_task: {
componentRef: {
spec: fullyLoadedSpec,
},
- ...(argumentsFromInputs ? { arguments: argumentsFromInputs } : {}),
+ ...(payloadArguments ? { arguments: payloadArguments } : {}),
},
};