Skip to content

Commit

Permalink
feat: Add/Delete support in Sources tab for multi-source apps(argopro…
Browse files Browse the repository at this point in the history
…j#18047)

Signed-off-by: Keith Chong <[email protected]>
  • Loading branch information
keithchong committed Jun 24, 2024
1 parent ad6508b commit ffaaca8
Show file tree
Hide file tree
Showing 9 changed files with 572 additions and 36 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -825,6 +825,7 @@ export class ApplicationDetails extends React.Component<RouteComponentProps<{app
isAppSelected={isAppSelected}
updateApp={(app: models.Application, query: {validate?: boolean}) => this.updateApp(app, query)}
selectedNode={selectedNode}
appCxt={this.context}
tab={tab}
/>
</SlidingPanel>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ export interface ApplicationParametersPanelProps<T> {
editBottom?: (formApi: FormApi) => React.ReactNode;
noReadonlyMode?: boolean;
collapsible?: boolean;
deleteSource: () => void;
}

interface ApplicationParametersPanelState {
Expand Down Expand Up @@ -67,6 +68,7 @@ export class ApplicationParametersSource<T = {}> extends React.Component<Applica
updateButtons={editClicked => {
this.setState({editBottom: editClicked});
}}
deleteSource={this.props.deleteSource}
/>
</React.Fragment>
{this.props.itemsTop && (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,10 @@
right: 1em;
}

.source-panel-buttons {
margin-bottom: 10px;
}

.argo-field {
line-height: 1.15;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import {AutocompleteField, DataLoader, FormField, FormSelect, getNestedField} from 'argo-ui';
import {AutocompleteField, DataLoader, ErrorNotification, FormField, FormSelect, getNestedField, NotificationType, SlidingPanel} from 'argo-ui';
import * as React from 'react';
import {FieldApi, FormApi, FormField as ReactFormField, Text, TextArea} from 'react-form';
import {cloneDeep} from 'lodash-es';
Expand All @@ -18,7 +18,8 @@ import {
Revision,
Repo,
EditablePanel,
EditablePanelItem
EditablePanelItem,
Spinner
} from '../../../shared/components';
import * as models from '../../../shared/models';
import {ApplicationSourceDirectory, Plugin} from '../../../shared/models';
Expand All @@ -27,13 +28,15 @@ import {ImageTagFieldEditor} from './kustomize';
import * as kustomize from './kustomize-image';
import {VarsInputField} from './vars-input-field';
import {concatMaps} from '../../../shared/utils';
import {getAppDefaultSource} from '../utils';
import {deleteSourceAction, getAppDefaultSource, helpTip} from '../utils';
import * as jsYaml from 'js-yaml';
import {RevisionFormField} from '../revision-form-field/revision-form-field';
import classNames from 'classnames';
import {ApplicationParametersSource} from './application-parameters-source';

import './application-parameters.scss';
import {AppContext} from '../../../shared/context';
import {SourcePanel} from './source-panel';

const TextWithMetadataField = ReactFormField((props: {metadata: {value: string}; fieldApi: FieldApi; className: string}) => {
const {
Expand Down Expand Up @@ -148,17 +151,30 @@ export const ApplicationParameters = (props: {
setPageNumber?: (x: number) => any;
collapsedSources?: boolean[];
handleCollapse?: (i: number, isCollapsed: boolean) => void;
appContext?: AppContext;
tempSource?: models.ApplicationSource;
}) => {
const app = cloneDeep(props.application);
const source = getAppDefaultSource(app); // For source field
const appSources = app?.spec.sources;
const [removedOverrides, setRemovedOverrides] = React.useState(new Array<boolean>());
const collapsible = props.collapsedSources !== undefined && props.handleCollapse !== undefined;
const [createApi, setCreateApi] = React.useState(null);
const [isAddingSource, setIsAddingSource] = React.useState(false);
const [isSavingSource, setIsSavingSource] = React.useState(false);
const [appParamsDeletedState, setAppParamsDeletedState] = React.useState([]);

if (app.spec.sources?.length > 0 && !props.details) {
// For multi-source case only
return (
<div className='application-parameters'>
<div className='source-panel-buttons'>
<button key={'add_source_button'} onClick={() => setIsAddingSource(true)} disabled={false} className='argo-button argo-button--base'>
{helpTip('Add a new source and append it to the sources field')}
<span style={{marginRight: '8px'}} />
Add Source
</button>
</div>
<Paginate
showHeader={false}
data={app.spec.sources}
Expand All @@ -176,18 +192,83 @@ export const ApplicationParameters = (props: {
return listOfPanels;
}}
</Paginate>
<SlidingPanel
isShown={isAddingSource}
onClose={() => setIsAddingSource(false)}
header={
<div>
<button
key={'source_panel_save_button'}
className='argo-button argo-button--base'
disabled={isSavingSource}
onClick={() => createApi && createApi.submitForm(null)}>
<Spinner show={isSavingSource} style={{marginRight: '5px'}} />
Save
</button>{' '}
<button
key={'source_panel_cancel_button_'}
onClick={() => {
setIsAddingSource(false);
setIsSavingSource(false);
}}
className='argo-button argo-button--base-o'>
Cancel
</button>
</div>
}>
<SourcePanel
appCurrent={props.application}
getFormApi={api => {
setCreateApi(api);
}}
onSubmitFailure={errors => {
props.appContext.apis.notifications.show({
content: 'Cannot add source: ' + errors.toString(),
type: NotificationType.Warning
});
}}
updateApp={async updatedAppSource => {
setIsSavingSource(true);
props.application.spec.sources.push(updatedAppSource.spec.source);
try {
await services.applications.update(props.application);
setIsAddingSource(false);
} catch (e) {
props.application.spec.sources.pop();
props.appContext.apis.notifications.show({
content: <ErrorNotification title='Unable to create source' e={e} />,
type: NotificationType.Error
});
} finally {
setIsSavingSource(false);
}
}}
/>
</SlidingPanel>
</div>
);
} else {
// For the other old/existings references of ApplicationParameters that have details already loaded. They are single source
// For the three other references of ApplicationParameters. They are single source.
// Create App, Add source, Rollback and History
let attributes: EditablePanelItem[] = [];
if (props.details) {
return getEditablePanel(
gatherDetails(0, props.details, attributes, source, app, setRemovedOverrides, removedOverrides, appParamsDeletedState, setAppParamsDeletedState, false),
gatherDetails(
0,
props.details,
attributes,
props.tempSource ? props.tempSource : source,
app,
setRemovedOverrides,
removedOverrides,
appParamsDeletedState,
setAppParamsDeletedState,
false
),
props.details
);
} else {
// For single source field, for resource details where we have to do the load.
// For single source field, details page where we have to do the load to retrieve repo details
return (
<DataLoader input={app} load={application => getSingleSource(application)}>
{(details: models.RepoAppDetails) => {
Expand Down Expand Up @@ -247,7 +328,10 @@ export const ApplicationParameters = (props: {
</div>
</React.Fragment>
)}
<DataLoader input={app.spec.sources[index]} load={src => getSourceFromAppSources(src, app.metadata.name, app.spec.project, index, 0)}>
<DataLoader
key={'app_params_source_' + index}
input={app.spec.sources[index]}
load={src => getSourceFromAppSources(src, app.metadata.name, app.spec.project, index, 0)}>
{(details: models.RepoAppDetails) => getEditablePanelForOneSource(details, index, source)}
</DataLoader>
</div>
Expand All @@ -270,10 +354,10 @@ export const ApplicationParameters = (props: {
function isDefinedWithVersion(item: any) {
return item !== null && item !== undefined && item.match(/:/);
}
if (updatedSrc.helm && updatedSrc.helm.parameters) {
if (updatedSrc && updatedSrc.helm?.parameters) {
updatedSrc.helm.parameters = updatedSrc.helm.parameters.filter(isDefined);
}
if (updatedSrc.kustomize && updatedSrc.kustomize.images) {
if (updatedSrc && updatedSrc.kustomize?.images) {
updatedSrc.kustomize.images = updatedSrc.kustomize.images.filter(isDefinedWithVersion);
}

Expand All @@ -295,15 +379,15 @@ export const ApplicationParameters = (props: {
params = params.filter(param => !appParamsDeletedState.includes(param.name));
input.spec.source.plugin.parameters = params;
}
if (input.spec.source.helm && input.spec.source.helm.valuesObject) {
if (input.spec.source && input.spec.source.helm?.valuesObject) {
input.spec.source.helm.valuesObject = jsYaml.load(input.spec.source.helm.values); // Deserialize json
input.spec.source.helm.values = '';
}
await props.save(input, {});
setRemovedOverrides(new Array<boolean>());
})
}
values={((repoAppDetails.plugin || app?.spec?.source?.plugin) && cloneDeep(app)) || app}
values={((repoAppDetails?.plugin || app?.spec?.source?.plugin) && cloneDeep(app)) || app}
validate={updatedApp => {
const errors = {} as any;

Expand All @@ -312,20 +396,20 @@ export const ApplicationParameters = (props: {
errors[fieldPath] = invalid.length > 0 ? 'All fields must have name' : null;
}

if (updatedApp.spec.source.helm && updatedApp.spec.source.helm.values) {
if (updatedApp.spec.source && updatedApp.spec.source.helm?.values) {
const parsedValues = jsYaml.load(updatedApp.spec.source.helm.values);
errors['spec.source.helm.values'] = typeof parsedValues === 'object' ? null : 'Values must be a map';
}

return errors;
}}
onModeSwitch={
repoAppDetails.plugin &&
repoAppDetails?.plugin &&
(() => {
setAppParamsDeletedState([]);
})
}
title={repoAppDetails.type.toLocaleUpperCase()}
title={repoAppDetails?.type?.toLocaleUpperCase()}
items={items as EditablePanelItem[]}
noReadonlyMode={props.noReadonlyMode}
hasMultipleSources={false}
Expand Down Expand Up @@ -402,7 +486,7 @@ export const ApplicationParameters = (props: {
saveBottom={
props.save &&
(async (input: models.Application) => {
const updatedSrc = input.spec.sources[ind];
const appSrc = input.spec.sources[ind];

function isDefined(item: any) {
return item !== null && item !== undefined;
Expand All @@ -411,11 +495,11 @@ export const ApplicationParameters = (props: {
return item !== null && item !== undefined && item.match(/:/);
}

if (updatedSrc.helm && updatedSrc.helm.parameters) {
updatedSrc.helm.parameters = updatedSrc.helm.parameters.filter(isDefined);
if (appSrc.helm && appSrc.helm.parameters) {
appSrc.helm.parameters = appSrc.helm.parameters.filter(isDefined);
}
if (updatedSrc.kustomize && updatedSrc.kustomize.images) {
updatedSrc.kustomize.images = updatedSrc.kustomize.images.filter(isDefinedWithVersion);
if (appSrc.kustomize && appSrc.kustomize.images) {
appSrc.kustomize.images = appSrc.kustomize.images.filter(isDefinedWithVersion);
}

let params = input.spec?.sources[ind]?.plugin?.parameters;
Expand All @@ -435,11 +519,11 @@ export const ApplicationParameters = (props: {
}

params = params.filter(param => !appParamsDeletedState.includes(param.name));
updatedSrc.plugin.parameters = params;
appSrc.plugin.parameters = params;
}
if (updatedSrc.helm && updatedSrc.helm.valuesObject) {
updatedSrc.helm.valuesObject = jsYaml.load(updatedSrc.helm.values); // Deserialize json
updatedSrc.helm.values = '';
if (appSrc.helm && appSrc.helm.valuesObject) {
appSrc.helm.valuesObject = jsYaml.load(appSrc.helm.values); // Deserialize json
appSrc.helm.values = '';
}

await props.save(input, {});
Expand Down Expand Up @@ -486,6 +570,9 @@ export const ApplicationParameters = (props: {
itemsTop={upperPanel as EditablePanelItem[]}
noReadonlyMode={props.noReadonlyMode}
collapsible={collapsible}
deleteSource={() => {
deleteSourceAction(app, app.spec.sources.at(ind), props.appContext);
}}
/>
);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
@import 'node_modules/argo-ui/src/styles/config';

.new-source-panel {

.checkbox-container {
margin: 0.5em ;
}

pre {
font-family: monospace;
line-height: normal;
white-space: pre;
}

.row.argo-form-row .columns {
padding-left: 0;
}
}
Loading

0 comments on commit ffaaca8

Please sign in to comment.