From 3d5ba1b8747265db206a5bd4cd4564547163ece6 Mon Sep 17 00:00:00 2001 From: Sairam Arunachalam Date: Wed, 11 Dec 2024 09:50:24 -0800 Subject: [PATCH] feat(ui): prefill parameters for workflow submit form. Fixes #12124 (#13922) Signed-off-by: Sairam Arunachalam --- .../cluster-workflow-template-details.tsx | 1 + ui/src/shared/get_workflow_params.test.ts | 38 +++++++++++++++++++ ui/src/shared/get_workflow_params.ts | 30 +++++++++++++++ ui/src/shared/history.ts | 11 +++--- .../workflow-template-details.tsx | 1 + .../components/submit-workflow-panel.tsx | 17 ++++++++- .../workflows/components/workflow-creator.tsx | 10 ++++- .../workflows-list/workflows-list.tsx | 20 +++++++++- 8 files changed, 117 insertions(+), 11 deletions(-) create mode 100644 ui/src/shared/get_workflow_params.test.ts create mode 100644 ui/src/shared/get_workflow_params.ts diff --git a/ui/src/cluster-workflow-templates/cluster-workflow-template-details.tsx b/ui/src/cluster-workflow-templates/cluster-workflow-template-details.tsx index 8930097cdd5e..04d3392ab4c1 100644 --- a/ui/src/cluster-workflow-templates/cluster-workflow-template-details.tsx +++ b/ui/src/cluster-workflow-templates/cluster-workflow-template-details.tsx @@ -159,6 +159,7 @@ export function ClusterWorkflowTemplateDetails({history, location, match}: Route entrypoint={template.spec.entrypoint} templates={template.spec.templates || []} workflowParameters={template.spec.arguments.parameters || []} + history={history} /> )} diff --git a/ui/src/shared/get_workflow_params.test.ts b/ui/src/shared/get_workflow_params.test.ts new file mode 100644 index 000000000000..e0542d143d2f --- /dev/null +++ b/ui/src/shared/get_workflow_params.test.ts @@ -0,0 +1,38 @@ +import {createBrowserHistory} from 'history'; + +import {getWorkflowParametersFromQuery} from './get_workflow_params'; + +describe('get_workflow_params', () => { + it('should return an empty object when there are no query parameters', () => { + const history = createBrowserHistory(); + const result = getWorkflowParametersFromQuery(history); + expect(result).toEqual({}); + }); + + it('should return the parameters provided in the URL', () => { + const history = createBrowserHistory(); + history.location.search = '?parameters[key1]=value1¶meters[key2]=value2'; + const result = getWorkflowParametersFromQuery(history); + expect(result).toEqual({ + key1: 'value1', + key2: 'value2' + }); + }); + + it('should not return any key value pairs which are not in parameters query ', () => { + const history = createBrowserHistory(); + history.location.search = '?retryparameters[key1]=value1&retryparameters[key2]=value2'; + const result = getWorkflowParametersFromQuery(history); + expect(result).toEqual({}); + }); + + it('should only return the parameters provided in the URL', () => { + const history = createBrowserHistory(); + history.location.search = '?parameters[key1]=value1¶meters[key2]=value2&test=123'; + const result = getWorkflowParametersFromQuery(history); + expect(result).toEqual({ + key1: 'value1', + key2: 'value2' + }); + }); +}); diff --git a/ui/src/shared/get_workflow_params.ts b/ui/src/shared/get_workflow_params.ts new file mode 100644 index 000000000000..9df9a15325d8 --- /dev/null +++ b/ui/src/shared/get_workflow_params.ts @@ -0,0 +1,30 @@ +import {History} from 'history'; + +function extractKey(inputString: string): string | null { + // Use regular expression to match the key within square brackets + const match = inputString.match(/^parameters\[(.*?)\]$/); + + // If a match is found, return the captured key + if (match) { + return match[1]; + } + + // If no match is found, return null or an empty string + return null; // Or return ''; +} +/** + * Returns the workflow parameters from the query parameters. + */ +export function getWorkflowParametersFromQuery(history: History): {[key: string]: string} { + const queryParams = new URLSearchParams(history.location.search); + + const parameters: {[key: string]: string} = {}; + for (const [key, value] of queryParams.entries()) { + const q = extractKey(key); + if (q) { + parameters[q] = value; + } + } + + return parameters; +} diff --git a/ui/src/shared/history.ts b/ui/src/shared/history.ts index 87736e32a28d..3295c08d5c0a 100644 --- a/ui/src/shared/history.ts +++ b/ui/src/shared/history.ts @@ -6,8 +6,7 @@ import * as nsUtils from './namespaces'; * Only "truthy" values are put into the query parameters. I.e. "falsey" values include null, undefined, false, "", 0. */ export function historyUrl(path: string, params: {[key: string]: any}) { - const queryParams: string[] = []; - let extraSearchParams: URLSearchParams; + const queryParams = new URLSearchParams(); Object.entries(params) .filter(([, v]) => v !== null) .forEach(([k, v]) => { @@ -15,14 +14,14 @@ export function historyUrl(path: string, params: {[key: string]: any}) { if (path.includes(searchValue)) { path = path.replace(searchValue, v != null ? v : ''); } else if (k === 'extraSearchParams') { - extraSearchParams = v; + (v as URLSearchParams).forEach((value, key) => queryParams.set(key, value)); } else if (v) { - queryParams.push(k + '=' + v); + queryParams.set(k, v); } if (k === 'namespace') { nsUtils.setCurrentNamespace(v); } }); - const extraString = extraSearchParams ? '&' + extraSearchParams.toString() : ''; - return uiUrl(path.replace(/{[^}]*}/g, '')) + '?' + queryParams.join('&') + extraString; + + return uiUrl(path.replace(/{[^}]*}/g, '')) + '?' + queryParams.toString(); } diff --git a/ui/src/workflow-templates/workflow-template-details.tsx b/ui/src/workflow-templates/workflow-template-details.tsx index cdf3bd0778a2..aca250a64449 100644 --- a/ui/src/workflow-templates/workflow-template-details.tsx +++ b/ui/src/workflow-templates/workflow-template-details.tsx @@ -157,6 +157,7 @@ export function WorkflowTemplateDetails({history, location, match}: RouteCompone entrypoint={template.spec.entrypoint} templates={template.spec.templates || []} workflowParameters={template.spec.arguments.parameters || []} + history={history} /> )} {sidePanel === 'share' && } diff --git a/ui/src/workflows/components/submit-workflow-panel.tsx b/ui/src/workflows/components/submit-workflow-panel.tsx index 8b93d3b53b3e..02a3d8103c85 100644 --- a/ui/src/workflows/components/submit-workflow-panel.tsx +++ b/ui/src/workflows/components/submit-workflow-panel.tsx @@ -1,11 +1,13 @@ import {Select} from 'argo-ui/src/components/select/select'; -import React, {useContext, useMemo, useState} from 'react'; +import {History} from 'history'; +import React, {useContext, useEffect, useMemo, useState} from 'react'; import {uiUrl} from '../../shared/base'; import {ErrorNotice} from '../../shared/components/error-notice'; import {getValueFromParameter, ParametersInput} from '../../shared/components/parameters-input'; import {TagsInput} from '../../shared/components/tags-input/tags-input'; import {Context} from '../../shared/context'; +import {getWorkflowParametersFromQuery} from '../../shared/get_workflow_params'; import {Parameter, Template} from '../../shared/models'; import {services} from '../../shared/services'; @@ -16,6 +18,7 @@ interface Props { entrypoint: string; templates: Template[]; workflowParameters: Parameter[]; + history: History; } const workflowEntrypoint = ''; @@ -28,13 +31,23 @@ const defaultTemplate: Template = { export function SubmitWorkflowPanel(props: Props) { const {navigation} = useContext(Context); - const [entrypoint, setEntrypoint] = useState(workflowEntrypoint); + const [entrypoint, setEntrypoint] = useState(props.entrypoint || workflowEntrypoint); const [parameters, setParameters] = useState([]); const [workflowParameters, setWorkflowParameters] = useState(JSON.parse(JSON.stringify(props.workflowParameters))); const [labels, setLabels] = useState(['submit-from-ui=true']); const [error, setError] = useState(); const [isSubmitting, setIsSubmitting] = useState(false); + useEffect(() => { + const templatePropertiesInQuery = getWorkflowParametersFromQuery(props.history); + // Get the user arguments from the query params + const updatedParams = workflowParameters.map(param => ({ + name: param.name, + value: templatePropertiesInQuery[param.name] || param.value + })); + setWorkflowParameters(updatedParams); + }, [props.history, setWorkflowParameters]); + const templates = useMemo(() => { return [defaultTemplate].concat(props.templates); }, [props.templates]); diff --git a/ui/src/workflows/components/workflow-creator.tsx b/ui/src/workflows/components/workflow-creator.tsx index 5be458fb82ce..b87ff9277213 100644 --- a/ui/src/workflows/components/workflow-creator.tsx +++ b/ui/src/workflows/components/workflow-creator.tsx @@ -1,4 +1,5 @@ import {Select} from 'argo-ui/src/components/select/select'; +import {History} from 'history'; import * as React from 'react'; import {useEffect, useState} from 'react'; @@ -16,7 +17,7 @@ import {WorkflowEditor} from './workflow-editor'; type Stage = 'choose-method' | 'submit-workflow' | 'full-editor'; -export function WorkflowCreator({namespace, onCreate}: {namespace: string; onCreate: (workflow: Workflow) => void}) { +export function WorkflowCreator({namespace, onCreate, history}: {namespace: string; onCreate: (workflow: Workflow) => void; history: History}) { const [workflowTemplates, setWorkflowTemplates] = useState(); const [workflowTemplate, setWorkflowTemplate] = useState(); const [stage, setStage] = useState('choose-method'); @@ -62,6 +63,12 @@ export function WorkflowCreator({namespace, onCreate}: {namespace: string; onCre } }, [workflowTemplate]); + useEffect(() => { + const queryParams = new URLSearchParams(history.location.search); + const template = queryParams.get('template'); + setWorkflowTemplate((workflowTemplates || []).find(tpl => tpl.metadata.name === template)); + }, [workflowTemplates, setWorkflowTemplate, history]); + return ( <> {stage === 'choose-method' && ( @@ -93,6 +100,7 @@ export function WorkflowCreator({namespace, onCreate}: {namespace: string; onCre entrypoint={workflowTemplate.spec.entrypoint} templates={workflowTemplate.spec.templates || []} workflowParameters={workflowTemplate.spec.arguments.parameters || []} + history={history} /> setStage('full-editor')}> Edit using full workflow options diff --git a/ui/src/workflows/components/workflows-list/workflows-list.tsx b/ui/src/workflows/components/workflows-list/workflows-list.tsx index 92876713a7ff..00d57401547a 100644 --- a/ui/src/workflows/components/workflows-list/workflows-list.tsx +++ b/ui/src/workflows/components/workflows-list/workflows-list.tsx @@ -135,7 +135,7 @@ export function WorkflowsList({match, location, history}: RouteComponentProps params.append('phase', phase)); labels?.forEach(label => params.append('label', label)); if (pagination.offset) { @@ -346,10 +346,26 @@ export function WorkflowsList({match, location, history}: RouteComponentProps - navigation.goto('.', {sidePanel: null})}> + { + const qParams: {[key: string]: string | null} = { + sidePanel: null + }; + // Remove any lingering query params + for (const key of queryParams.keys()) { + qParams[key] = null; + } + // Add back the pagination and namespace params. + qParams.limit = pagination.limit.toString(); + qParams.offset = pagination.offset || null; + qParams.namespace = namespace; + navigation.goto('.', qParams); + }}> {getSidePanel() === 'submit-new-workflow' && ( navigation.goto(uiUrl(`workflows/${wf.metadata.namespace}/${wf.metadata.name}`))} /> )}