Skip to content

Commit

Permalink
feat: UI Resubmit workflows with parameter (#4662) (#11083)
Browse files Browse the repository at this point in the history
Signed-off-by: toyamagu2021@gmail.com <toyamagu2021@gmail.com>
  • Loading branch information
toyamagu-2021 committed Jul 16, 2023
1 parent 22d4e17 commit 869e42d
Show file tree
Hide file tree
Showing 10 changed files with 277 additions and 78 deletions.
80 changes: 80 additions & 0 deletions ui/src/app/shared/components/parameters-input/parameters-input.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
import {Select, Tooltip} from 'argo-ui';
import * as React from 'react';
import {Parameter} from '../../../../models';
import {Utils} from '../../utils';

interface ParametersInputProps {
parameters: Parameter[];
onChange?: (parameters: Parameter[]) => void;
}

export class ParametersInput extends React.Component<ParametersInputProps, {parameters: Parameter[]}> {
constructor(props: ParametersInputProps) {
super(props);
this.state = {parameters: props.parameters || []};
}

public render() {
return (
<>
{this.props.parameters.map((parameter, index) => (
<div key={parameter.name + '_' + index} style={{marginBottom: 14}}>
<label>{parameter.name}</label>
{parameter.description && (
<Tooltip content={parameter.description}>
<i className='fa fa-question-circle' style={{marginLeft: 4}} />
</Tooltip>
)}
{(parameter.enum && this.displaySelectFieldForEnumValues(parameter)) || this.displayInputFieldForSingleValue(parameter)}
</div>
))}
</>
);
}

private displaySelectFieldForEnumValues(parameter: Parameter) {
return (
<Select
key={parameter.name}
value={Utils.getValueFromParameter(parameter)}
options={parameter.enum.map(value => ({
value,
title: value
}))}
onChange={e => {
const newParameters: Parameter[] = this.state.parameters.map(p => ({
name: p.name,
value: p.name === parameter.name ? e.value : Utils.getValueFromParameter(p),
enum: p.enum
}));
this.setState({parameters: newParameters});
this.onParametersChange(newParameters);
}}
/>
);
}

private displayInputFieldForSingleValue(parameter: Parameter) {
return (
<textarea
className='argo-field'
value={Utils.getValueFromParameter(parameter)}
onChange={e => {
const newParameters: Parameter[] = this.state.parameters.map(p => ({
name: p.name,
value: p.name === parameter.name ? e.target.value : Utils.getValueFromParameter(p),
enum: p.enum
}));
this.setState({parameters: newParameters});
this.onParametersChange(newParameters);
}}
/>
);
}

private onParametersChange(parameters: Parameter[]) {
if (this.props.onChange) {
this.props.onChange(parameters);
}
}
}
8 changes: 6 additions & 2 deletions ui/src/app/shared/services/workflows-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import {EMPTY, from, Observable, of} from 'rxjs';
import {catchError, filter, map, mergeMap, switchMap} from 'rxjs/operators';
import * as models from '../../../models';
import {Event, LogEntry, NodeStatus, Workflow, WorkflowList, WorkflowPhase} from '../../../models';
import {ResubmitOpts} from '../../../models/resubmit-opts';
import {SubmitOpts} from '../../../models/submit-opts';
import {uiUrl} from '../base';
import {Pagination} from '../pagination';
Expand Down Expand Up @@ -121,8 +122,11 @@ export const WorkflowsService = {
return requests.put(`api/v1/workflows/${namespace}/${name}/retry`).then(res => res.body as Workflow);
},

resubmit(name: string, namespace: string) {
return requests.put(`api/v1/workflows/${namespace}/${name}/resubmit`).then(res => res.body as Workflow);
resubmit(name: string, namespace: string, opts?: ResubmitOpts) {
return requests
.put(`api/v1/workflows/${namespace}/${name}/resubmit`)
.send(opts)
.then(res => res.body as Workflow);
},

suspend(name: string, namespace: string) {
Expand Down
10 changes: 9 additions & 1 deletion ui/src/app/shared/utils.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import * as models from '../../models';
import {NODE_PHASE} from '../../models';
import {NODE_PHASE, Parameter} from '../../models';
import {Pagination} from './pagination';

const managedNamespaceKey = 'managedNamespace';
Expand Down Expand Up @@ -190,5 +190,13 @@ export const Utils = {
labelSelector += labels.join(',');
}
return labelSelector;
},

getValueFromParameter(p: Parameter) {
if (p.value === undefined) {
return p.default;
} else {
return p.value;
}
}
};
2 changes: 1 addition & 1 deletion ui/src/app/shared/workflow-operations-map.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ export const WorkflowOperationsMap: WorkflowOperations = {
title: 'RESUBMIT',
iconClassName: 'fa fa-plus-circle',
disabled: () => false,
action: (wf: Workflow) => services.workflows.resubmit(wf.metadata.name, wf.metadata.namespace)
action: (wf: Workflow) => services.workflows.resubmit(wf.metadata.name, wf.metadata.namespace, null)
},
SUSPEND: {
title: 'SUSPEND',
Expand Down
106 changes: 106 additions & 0 deletions ui/src/app/workflows/components/resubmit-workflow-panel.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
import {Checkbox} from 'argo-ui';
import * as React from 'react';
import {Parameter, ResubmitOpts, Workflow} from '../../../models';
import {uiUrl} from '../../shared/base';
import {ErrorNotice} from '../../shared/components/error-notice';
import {ParametersInput} from '../../shared/components/parameters-input/parameters-input';
import {services} from '../../shared/services';
import {Utils} from '../../shared/utils';

interface Props {
workflow: Workflow;
}

interface State {
overrideParameters: boolean;
workflowParameters: Parameter[];
memoized: boolean;
error?: Error;
isSubmitting: boolean;
}

export class ResubmitWorkflowPanel extends React.Component<Props, State> {
constructor(props: any) {
super(props);
const state: State = {
workflowParameters: JSON.parse(JSON.stringify(this.props.workflow.spec.arguments.parameters || [])),
memoized: false,
isSubmitting: false,
overrideParameters: false
};
this.state = state;
}

public render() {
return (
<>
<h4>Resubmit Workflow</h4>
<h5>
{this.props.workflow.metadata.namespace}/{this.props.workflow.metadata.name}
</h5>
{this.state.error && <ErrorNotice error={this.state.error} />}
<div className='white-box'>
<div key='override-parameters' style={{marginBottom: 25}}>
<label>Override Parameters</label>
<div className='columns small-9'>
<Checkbox checked={this.state.overrideParameters} onChange={overrideParameters => this.setState({overrideParameters})} />
</div>
</div>

{this.state.overrideParameters && (
<div key='parameters' style={{marginBottom: 25}}>
<label>Parameters</label>
{this.state.workflowParameters.length > 0 && (
<ParametersInput parameters={this.state.workflowParameters} onChange={workflowParameters => this.setState({workflowParameters})} />
)}
{this.state.workflowParameters.length === 0 ? (
<>
<br />
<label>No parameters</label>
</>
) : (
<></>
)}
</div>
)}

<div key='memoized' style={{marginBottom: 25}}>
<label>Memoized</label>
<div className='columns small-9'>
<Checkbox checked={this.state.memoized} onChange={memoized => this.setState({memoized})} />
</div>
</div>

{this.state.overrideParameters && this.state.memoized && (
<div key='warning-override-with-memoized'>
<i className='fa fa-exclamation-triangle' style={{color: '#f4c030'}} />
Overriding parameters on memoized submitted workflows may have unexpected results.
</div>
)}

<div key='resubmit'>
<button onClick={() => this.submit()} className='argo-button argo-button--base' disabled={this.state.isSubmitting}>
<i className='fa fa-plus' /> {this.state.isSubmitting ? 'Loading...' : 'Resubmit'}
</button>
</div>
</div>
</>
);
}

private submit() {
this.setState({isSubmitting: true});
const parameters: ResubmitOpts['parameters'] = this.state.overrideParameters
? [...this.state.workflowParameters.filter(p => Utils.getValueFromParameter(p) !== undefined).map(p => p.name + '=' + Utils.getValueFromParameter(p))]
: [];
const opts: ResubmitOpts = {
parameters,
memoized: this.state.memoized
};

services.workflows
.resubmit(this.props.workflow.metadata.name, this.props.workflow.metadata.namespace, opts)
.then((submitted: Workflow) => (document.location.href = uiUrl(`workflows/${submitted.metadata.namespace}/${submitted.metadata.name}`)))
.catch(error => this.setState({error, isSubmitting: false}));
}
}
82 changes: 9 additions & 73 deletions ui/src/app/workflows/components/submit-workflow-panel.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
import {Select, Tooltip} from 'argo-ui';
import {Select} from 'argo-ui';
import * as React from 'react';
import {Parameter, Template, Workflow} from '../../../models';
import {uiUrl} from '../../shared/base';
import {ErrorNotice} from '../../shared/components/error-notice';
import {ParametersInput} from '../../shared/components/parameters-input/parameters-input';
import {TagsInput} from '../../shared/components/tags-input/tags-input';
import {services} from '../../shared/services';
import {Utils} from '../../shared/utils';

interface Props {
kind: string;
Expand All @@ -28,7 +30,6 @@ interface State {
}

const workflowEntrypoint = '<default>';
type ParamSelector = 'parameters' | 'workflowParameters';

export class SubmitWorkflowPanel extends React.Component<Props, State> {
constructor(props: any) {
Expand Down Expand Up @@ -81,8 +82,10 @@ export class SubmitWorkflowPanel extends React.Component<Props, State> {
</div>
<div key='parameters' style={{marginBottom: 25}}>
<label>Parameters</label>
{this.state.workflowParameters.length > 0 && this.renderParameters(this.state.workflowParameters, 'workflowParameters')}
{this.state.parameters.length > 0 && this.renderParameters(this.state.parameters, 'parameters')}
{this.state.workflowParameters.length > 0 && (
<ParametersInput parameters={this.state.workflowParameters} onChange={workflowParameters => this.setState({workflowParameters})} />
)}
{this.state.parameters.length > 0 && <ParametersInput parameters={this.state.parameters} onChange={parameters => this.setState({parameters})} />}
{this.state.workflowParameters.length === 0 && this.state.parameters.length === 0 ? (
<>
<br />
Expand Down Expand Up @@ -115,81 +118,14 @@ export class SubmitWorkflowPanel extends React.Component<Props, State> {
return null;
}

private displaySelectFieldForEnumValues(parameter: Parameter, parameterStateName: ParamSelector) {
return (
<Select
key={parameter.name}
value={this.getValue(parameter)}
options={parameter.enum.map(value => ({
value,
title: value
}))}
onChange={event => {
const update = {} as State;
update[parameterStateName] = this.state[parameterStateName].map(p => ({
name: p.name,
value: p.name === parameter.name ? event.value : this.getValue(p),
enum: p.enum
}));
this.setState(update);
}}
/>
);
}

private displayInputFieldForSingleValue(parameter: Parameter, parameterStateName: ParamSelector) {
return (
<textarea
className='argo-field'
value={this.getValue(parameter)}
onChange={event => {
const update = {} as State;
update[parameterStateName] = this.state[parameterStateName].map(p => ({
name: p.name,
value: p.name === parameter.name ? event.target.value : this.getValue(p),
enum: p.enum
}));
this.setState(update);
}}
/>
);
}

private renderParameters(parameters: Parameter[], parameterStateName: ParamSelector) {
return (
<>
{parameters.map((parameter, index) => (
<div key={parameter.name + '_' + index} style={{marginBottom: 14}}>
<label>{parameter.name}</label>
{parameter.description && (
<Tooltip content={parameter.description}>
<i className='fa fa-question-circle' style={{marginLeft: 4}} />
</Tooltip>
)}
{(parameter.enum && this.displaySelectFieldForEnumValues(parameter, parameterStateName)) ||
this.displayInputFieldForSingleValue(parameter, parameterStateName)}
</div>
))}
</>
);
}

private getValue(p: Parameter) {
if (p.value === undefined) {
return p.default;
} else {
return p.value;
}
}

private submit() {
this.setState({isSubmitting: true});
services.workflows
.submit(this.props.kind, this.props.name, this.props.namespace, {
entryPoint: this.state.entrypoint === workflowEntrypoint ? null : this.state.entrypoint,
parameters: [
...this.state.workflowParameters.filter(p => this.getValue(p) !== undefined).map(p => p.name + '=' + this.getValue(p)),
...this.state.parameters.filter(p => this.getValue(p) !== undefined).map(p => p.name + '=' + this.getValue(p))
...this.state.workflowParameters.filter(p => Utils.getValueFromParameter(p) !== undefined).map(p => p.name + '=' + Utils.getValueFromParameter(p)),
...this.state.parameters.filter(p => Utils.getValueFromParameter(p) !== undefined).map(p => p.name + '=' + Utils.getValueFromParameter(p))
],
labels: this.state.labels.join(',')
})
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import * as Operations from '../../../shared/workflow-operations-map';
import {WorkflowOperations} from '../../../shared/workflow-operations-map';
import {WidgetGallery} from '../../../widgets/widget-gallery';
import {EventsPanel} from '../events-panel';
import {ResubmitWorkflowPanel} from '../resubmit-workflow-panel';
import {WorkflowArtifacts} from '../workflow-artifacts';
import {WorkflowLogsViewer} from '../workflow-logs-viewer/workflow-logs-viewer';
import {WorkflowNodeInfo} from '../workflow-node-info/workflow-node-info';
Expand Down Expand Up @@ -211,6 +212,8 @@ export const WorkflowDetails = ({history, location, match}: RouteComponentProps<
window.location.reload();
}
});
} else if (workflowOperation.title === 'RESUBMIT') {
setSidePanel('resubmit');
} else {
popup.confirm('Confirm', `Are you sure you want to ${workflowOperation.title.toLowerCase()} this workflow?`).then(yes => {
if (yes) {
Expand Down Expand Up @@ -547,7 +550,7 @@ export const WorkflowDetails = ({history, location, match}: RouteComponentProps<
))}
</div>
{workflow && (
<SlidingPanel isShown={!!sidePanel} onClose={() => setSidePanel(null)}>
<SlidingPanel isShown={!!sidePanel} onClose={() => setSidePanel(null)} isMiddle={parsedSidePanel.type === 'resubmit'}>
{parsedSidePanel.type === 'logs' && (
<WorkflowLogsViewer
workflow={workflow}
Expand All @@ -560,6 +563,7 @@ export const WorkflowDetails = ({history, location, match}: RouteComponentProps<
{parsedSidePanel.type === 'events' && <EventsPanel namespace={namespace} kind='Pod' name={podName} />}
{parsedSidePanel.type === 'share' && <WidgetGallery namespace={namespace} name={name} />}
{parsedSidePanel.type === 'yaml' && <WorkflowYamlViewer workflow={workflow} selectedNode={selectedNode} />}
{parsedSidePanel.type === 'resubmit' && <ResubmitWorkflowPanel workflow={workflow} />}
{!parsedSidePanel}
</SlidingPanel>
)}
Expand Down
Loading

0 comments on commit 869e42d

Please sign in to comment.