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: Add/Delete support in Sources tab for multi-source apps (#18047) #18804

Merged
Show file tree
Hide file tree
Changes from 1 commit
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 @@ -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 @@ -24,8 +24,10 @@ export interface ApplicationParametersPanelProps<T> {
viewBottom?: string | React.ReactNode;
editTop?: (formApi: FormApi) => React.ReactNode;
editBottom?: (formApi: FormApi) => React.ReactNode;
numberOfSources?: number;
noReadonlyMode?: boolean;
collapsible?: boolean;
deleteSource: () => void;
}

interface ApplicationParametersPanelState {
Expand Down Expand Up @@ -64,9 +66,11 @@ export class ApplicationParametersSource<T = {}> extends React.Component<Applica
ctx={ctx}
isTopSection={true}
disabledState={this.state.editTop || this.state.editTop === null}
disabledDelete={this.props.numberOfSources <= 1}
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, app.spec.sources[index])}
</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,10 @@ export const ApplicationParameters = (props: {
itemsTop={upperPanel as EditablePanelItem[]}
noReadonlyMode={props.noReadonlyMode}
collapsible={collapsible}
numberOfSources={app?.spec?.sources.length}
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