Skip to content
Open
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
7 changes: 2 additions & 5 deletions src/components/shared/Submitters/Oasis/OasisSubmitter.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import {
type ComponentSpec,
isGraphImplementation,
} from "@/utils/componentSpec";
import { getFileExtension } from "@/utils/csvBulkArgumentImport";
import { submitPipelineRun } from "@/utils/submitPipeline";
import { validateArguments } from "@/utils/validations";

Expand Down Expand Up @@ -366,15 +367,11 @@ const OasisSubmitter = ({
const file = e.dataTransfer.files[0];
if (!file) return;

const extension = file.name.includes(".")
? `.${file.name.split(".").pop()?.toLowerCase()}`
: "";

const reader = new FileReader();
reader.onload = (event) => {
const text = event.target?.result;
if (typeof text === "string") {
setPendingImportFile({ text, extension });
setPendingImportFile({ text, extension: getFileExtension(file.name) });
setIsArgumentsDialogOpen(true);
}
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,10 @@ import {
type InputSpec,
isSecretArgument,
} from "@/utils/componentSpec";
import { mapCsvToArguments } from "@/utils/csvBulkArgumentImport";
import {
getFileExtension,
mapCsvToArguments,
} from "@/utils/csvBulkArgumentImport";
import { mapJsonToArguments } from "@/utils/jsonBulkArgumentImport";
import { extractTaskArguments } from "@/utils/nodes/taskArguments";
import { pluralize } from "@/utils/string";
Expand Down Expand Up @@ -118,13 +121,6 @@ export const SubmitTaskArgumentsDialog = ({
!hasBulkMismatch &&
bulkRunCount > 0;

useEffect(() => {
if (initialImportFile && open) {
handleFileImport(initialImportFile.text, initialImportFile.extension);
onImportComplete?.();
}
}, [initialImportFile, open]);

const handleCopyFromRun = (args: Record<string, string>) => {
const diff = Object.entries(args).filter(
([key, value]) => taskArguments[key] !== value,
Expand Down Expand Up @@ -226,6 +222,13 @@ export const SubmitTaskArgumentsDialog = ({
notify(message, hasWarnings ? "warning" : "success");
};

useEffect(() => {
if (initialImportFile && open) {
handleFileImport(initialImportFile.text, initialImportFile.extension);
onImportComplete?.();
}
}, [initialImportFile, open, handleFileImport, onImportComplete]);

const handleConfirm = () =>
onConfirm(taskArguments, runNotes, bulkInputNames);

Expand Down Expand Up @@ -390,10 +393,6 @@ const CopyFromRunPopover = ({
isPending: isCopyingFromRun,
isError,
} = useMutation({
/**
* @param run - The run to copy arguments from. Can be a run ID or a run object.
* @returns
*/
mutationFn: async (run: PipelineRun | string) => {
const executionId =
typeof run === "string"
Expand Down Expand Up @@ -717,15 +716,11 @@ const ImportFileButton = ({
const file = e.target.files?.[0];
if (!file) return;

const extension = file.name.includes(".")
? `.${file.name.split(".").pop()?.toLowerCase()}`
: "";

const reader = new FileReader();
reader.onload = (event) => {
const text = event.target?.result;
if (typeof text === "string") {
onImport(text, extension);
onImport(text, getFileExtension(file.name));
}
};
reader.readAsText(file);
Expand Down
6 changes: 1 addition & 5 deletions src/utils/bulkSubmission.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,7 @@ import {
getBulkRunCount,
parseBulkValues,
} from "./bulkSubmission";
import type { DynamicDataArgument } from "./componentSpec";

function makeSecretArg(name: string): DynamicDataArgument {
return { dynamicData: { secret: { name } } };
}
import { makeSecretArg } from "./testHelpers";

describe("parseBulkValues", () => {
it("splits comma-separated values and trims whitespace", () => {
Expand Down
10 changes: 1 addition & 9 deletions src/utils/csvBulkArgumentImport.test.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,7 @@
import { describe, expect, it } from "vitest";

import type { DynamicDataArgument, InputSpec } from "./componentSpec";
import { mapCsvToArguments, parseCsv } from "./csvBulkArgumentImport";

function makeSecretArg(name: string): DynamicDataArgument {
return { dynamicData: { secret: { name } } };
}

function makeInput(name: string, optional = false): InputSpec {
return { name, optional };
}
import { makeInput, makeSecretArg } from "./testHelpers";

describe("parseCsv", () => {
it("parses simple CSV", () => {
Expand Down
15 changes: 12 additions & 3 deletions src/utils/csvBulkArgumentImport.ts
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,16 @@ export function parseCsv(text: string): string[][] {
return rows;
}

interface CsvImportResult {
/**
* Extracts the file extension (e.g. ".csv", ".json") from a filename.
* Returns empty string if no extension is found.
*/
export function getFileExtension(filename: string): string {
const dotIdx = filename.lastIndexOf(".");
return dotIdx >= 0 ? `.${filename.slice(dotIdx + 1).toLowerCase()}` : "";
}

export interface FileImportResult {
values: Record<string, string>;
changedInputNames: string[];
enableBulk: boolean;
Expand All @@ -88,10 +97,10 @@ export function mapCsvToArguments(
csvText: string,
inputs: InputSpec[],
currentArgs: Record<string, ArgumentType>,
): CsvImportResult {
): FileImportResult {
const rows = parseCsv(csvText);

const empty: CsvImportResult = {
const empty: FileImportResult = {
values: {},
changedInputNames: [],
enableBulk: false,
Expand Down
10 changes: 1 addition & 9 deletions src/utils/jsonBulkArgumentImport.test.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,7 @@
import { describe, expect, it } from "vitest";

import type { DynamicDataArgument, InputSpec } from "./componentSpec";
import { mapJsonToArguments } from "./jsonBulkArgumentImport";

function makeSecretArg(name: string): DynamicDataArgument {
return { dynamicData: { secret: { name } } };
}

function makeInput(name: string, optional = false): InputSpec {
return { name, optional };
}
import { makeInput, makeSecretArg } from "./testHelpers";

describe("mapJsonToArguments", () => {
const inputs = [
Expand Down
14 changes: 3 additions & 11 deletions src/utils/jsonBulkArgumentImport.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import {
type InputSpec,
isSecretArgument,
} from "./componentSpec";
import type { FileImportResult } from "./csvBulkArgumentImport";

function isPlainObject(value: unknown): value is Record<string, unknown> {
return typeof value === "object" && value !== null && !Array.isArray(value);
Expand All @@ -21,15 +22,6 @@ function valueToString(value: unknown): string {
return JSON.stringify(value);
}

interface JsonImportResult {
values: Record<string, string>;
changedInputNames: string[];
enableBulk: boolean;
unmatchedColumns: string[];
skippedSecretInputs: string[];
rowCount: number;
}

/**
* Maps JSON data onto pipeline input arguments.
*
Expand All @@ -46,8 +38,8 @@ export function mapJsonToArguments(
jsonText: string,
inputs: InputSpec[],
currentArgs: Record<string, ArgumentType>,
): JsonImportResult {
const empty: JsonImportResult = {
): FileImportResult {
const empty: FileImportResult = {
values: {},
changedInputNames: [],
enableBulk: false,
Expand Down
11 changes: 2 additions & 9 deletions src/utils/templateExport.test.ts
Original file line number Diff line number Diff line change
@@ -1,19 +1,12 @@
import { describe, expect, it } from "vitest";

import type { DynamicDataArgument, InputSpec } from "./componentSpec";
import type { InputSpec } from "./componentSpec";
import {
generateCsvTemplate,
generateJsonTemplate,
generateYamlTemplate,
} from "./templateExport";

function makeSecretArg(name: string): DynamicDataArgument {
return { dynamicData: { secret: { name } } };
}

function makeInput(name: string, optional = false): InputSpec {
return { name, optional };
}
import { makeInput, makeSecretArg } from "./testHelpers";

describe("generateCsvTemplate", () => {
const inputs = [
Expand Down
80 changes: 24 additions & 56 deletions src/utils/templateExport.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,62 +18,7 @@ function quoteCsvField(value: string): string {
}

/**
* Generates a CSV with input names as column headers.
*
* Bulk inputs are expanded into separate rows (one value per row).
* Non-bulk inputs repeat their value across all rows.
* Skips inputs that currently hold secret values.
*
* { experiment_key: "12345, 1, 2, 9", predictions: "1234, 4, 6, 5" }
* with both as bulk →
* experiment_key,predictions
* 12345,1234
* 1,4
* 2,6
* 9,5
*/
export function generateCsvTemplate(
inputs: InputSpec[],
currentArgs: Record<string, ArgumentType>,
bulkInputNames: Set<string> = new Set(),
): string {
const nonSecretInputs = inputs.filter(
(input) => !isSecretArgument(currentArgs[input.name]),
);

if (nonSecretInputs.length === 0) return "";

const headers = nonSecretInputs.map((input) => input.name);

const columns = nonSecretInputs.map((input) => {
const current = currentArgs[input.name];
const raw =
typeof current === "string" && current.length > 0
? current
: (input.default ?? "");

if (bulkInputNames.has(input.name) && raw.length > 0) {
return parseBulkValues(raw);
}
return [raw];
});

const rowCount = Math.max(...columns.map((col) => col.length), 1);

const rows: string[] = [headers.join(",")];
for (let i = 0; i < rowCount; i++) {
const row = columns.map((col) => {
const value = i < col.length ? col[i] : (col[0] ?? "");
return quoteCsvField(value);
});
rows.push(row.join(","));
}

return rows.join("\n");
}

/**
* Builds row data shared by JSON and YAML template generators.
* Builds row data shared by all template generators.
* Returns an array of objects keyed by input name.
*/
function buildTemplateRows(
Expand Down Expand Up @@ -115,6 +60,29 @@ function buildTemplateRows(
return rows;
}

/**
* Generates a CSV with input names as column headers.
*
* Bulk inputs are expanded into separate rows (one value per row).
* Non-bulk inputs repeat their value across all rows.
* Skips inputs that currently hold secret values.
*/
export function generateCsvTemplate(
inputs: InputSpec[],
currentArgs: Record<string, ArgumentType>,
bulkInputNames: Set<string> = new Set(),
): string {
const rows = buildTemplateRows(inputs, currentArgs, bulkInputNames);
if (rows.length === 0) return "";

const headers = Object.keys(rows[0]);
const csvRows = [headers.join(",")];
for (const row of rows) {
csvRows.push(headers.map((h) => quoteCsvField(row[h])).join(","));
}
return csvRows.join("\n");
}

/**
* Generates a JSON template from current input values.
* Single row → object, multiple rows → array of objects.
Expand Down
9 changes: 9 additions & 0 deletions src/utils/testHelpers.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import type { DynamicDataArgument, InputSpec } from "./componentSpec";

export function makeSecretArg(name: string): DynamicDataArgument {
return { dynamicData: { secret: { name } } };
}

export function makeInput(name: string, optional = false): InputSpec {
return { name, optional };
}
Loading