Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(ui): prefill parameters for workflow submit form. Fixes #12124 #13766

Open
wants to merge 6 commits into
base: main
Choose a base branch
from
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
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,7 @@ export function ClusterWorkflowTemplateDetails({history, location, match}: Route
entrypoint={template.spec.entrypoint}
templates={template.spec.templates || []}
workflowParameters={template.spec.arguments.parameters || []}
history={history}
/>
</SlidingPanel>
)}
Expand Down
37 changes: 37 additions & 0 deletions ui/src/app/shared/get_workflow_params.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
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&parameters[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&parameters[key2]=value2&test=123';
const result = getWorkflowParametersFromQuery(history);
expect(result).toEqual({
key1: 'value1',
key2: 'value2'
});
});
});
30 changes: 30 additions & 0 deletions ui/src/app/shared/get_workflow_params.ts
Original file line number Diff line number Diff line change
@@ -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;
}
11 changes: 5 additions & 6 deletions ui/src/app/shared/history.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,23 +6,22 @@ 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]) => {
const searchValue = '{' + k + '}';
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();
}
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,7 @@ export function WorkflowTemplateDetails({history, location, match}: RouteCompone
kind='WorkflowTemplate'
namespace={namespace}
name={name}
history={history}
entrypoint={template.spec.entrypoint}
templates={template.spec.templates || []}
workflowParameters={template.spec.arguments.parameters || []}
Expand Down
17 changes: 15 additions & 2 deletions ui/src/app/workflows/components/submit-workflow-panel.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import {Select} from 'argo-ui/src/components/select/select';
import React, {useContext, useMemo, useState} from 'react';
import React, {useContext, useEffect, useMemo, useState} from 'react';

import {Parameter, Template} from '../../../models';
import {Context} from '../../shared/context';
Expand All @@ -8,6 +8,8 @@ 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 {services} from '../../shared/services';
import {getWorkflowParametersFromQuery} from '../../shared/get_workflow_params';
import {History} from 'history';

interface Props {
kind: string;
Expand All @@ -16,6 +18,7 @@ interface Props {
entrypoint: string;
templates: Template[];
workflowParameters: Parameter[];
history: History;
}

const workflowEntrypoint = '<default>';
Expand All @@ -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<Parameter[]>([]);
const [workflowParameters, setWorkflowParameters] = useState<Parameter[]>(JSON.parse(JSON.stringify(props.workflowParameters)));
const [labels, setLabels] = useState(['submit-from-ui=true']);
const [error, setError] = useState<Error>();
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]);
Expand Down
10 changes: 9 additions & 1 deletion ui/src/app/workflows/components/workflow-creator.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import {Select} from 'argo-ui/src/components/select/select';
import * as React from 'react';
import {useEffect, useState} from 'react';
import {History} from 'history';

import {Workflow, WorkflowTemplate} from '../../../models';
import {Button} from '../../shared/components/button';
Expand All @@ -15,7 +16,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<WorkflowTemplate[]>();
const [workflowTemplate, setWorkflowTemplate] = useState<WorkflowTemplate>();
const [stage, setStage] = useState<Stage>('choose-method');
Expand Down Expand Up @@ -61,6 +62,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' && (
Expand Down Expand Up @@ -92,6 +99,7 @@ export function WorkflowCreator({namespace, onCreate}: {namespace: string; onCre
entrypoint={workflowTemplate.spec.entrypoint}
templates={workflowTemplate.spec.templates || []}
workflowParameters={workflowTemplate.spec.arguments.parameters || []}
history={history}
/>
<a onClick={() => setStage('full-editor')}>
Edit using full workflow options <i className='fa fa-caret-right' />
Expand Down
21 changes: 19 additions & 2 deletions ui/src/app/workflows/components/workflows-list/workflows-list.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -136,7 +136,7 @@ export function WorkflowsList({match, location, history}: RouteComponentProps<an
}
storage.setItem('options', options, {} as WorkflowListRenderOptions);

const params = new URLSearchParams();
const params = new URLSearchParams(history.location.search);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I found one side-effect of this change: if you visit http://localhost:8080/workflows?namespace=argo and refresh the page, it'll append &namespace=argo each time you refresh. That's happening because historyUrl() isn't deduplicating query parameters, which it should IMO. Here's a commit that fixes that: MasonM@8b48d5b

I haven't extensively tested that, so it's possible there's other side-effects

phases?.forEach(phase => params.append('phase', phase));
labels?.forEach(label => params.append('label', label));
if (pagination.offset) {
Expand Down Expand Up @@ -347,10 +347,27 @@ export function WorkflowsList({match, location, history}: RouteComponentProps<an
)}
</div>
</div>
<SlidingPanel isShown={!!getSidePanel()} onClose={() => navigation.goto('.', {sidePanel: null})}>
<SlidingPanel
isShown={!!getSidePanel()}
onClose={() => {
// Remove any lingering query params
const qParams: {[key: string]: string | null} = {
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is duplicate can be removed.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you explain why this is necessary? The navigation.goto() function already has logic to automatically strip out the query parameters if there's been a path change: https://github.com/argoproj/argo-ui/blob/54f36c723d9a0e5d3386689298cbb9c27767fa00/src/components/navigation.ts#L20-L21

If there hasn't been a path change, I think it's fine to leave these query parameters in.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So the workflow this would fix is

  • Open the submit page prefilled through the URL
  • Close the submit page
  • Click on Submit again,

In this case, if the parameters are preserved, we would open the prefilled page. Removing the parameters will ensure the developer can start the page fresh without pre filled data.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Okay, thanks. IMO, it'd be better to remove this, because if a developer wants to start fresh, they can click on the "Workflows" icon on the left bar (or navigate to another page and back again)

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Okay, I tried that workflow and the developer would need to go back to the workflows tab and re-trigger a workflow.

Most likely the developer who lands on this page through query parameters would try to Submit it.

Removing the extra added query parameters in this PR, will make the developer experience much better as they can recover quickly.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Okay, that makes sense. I checked and this is the only page using a<SlidingPanel> that calls navigation.goto() on close. Every other page just sets a parameter on close, e.g.

<SlidingPanel isShown={!!sidePanel} onClose={() => setSidePanel(null)} isMiddle={true}>

I'm not sure why this page is special in that respect, but it could be a developer experience thing

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' && (
<WorkflowCreator
namespace={nsUtils.getNamespaceWithDefault(namespace)}
history={history}
onCreate={wf => navigation.goto(uiUrl(`workflows/${wf.metadata.namespace}/${wf.metadata.name}`))}
/>
)}
Expand Down
Loading