mirror of
https://github.com/argoproj/argo-cd
synced 2026-04-21 17:07:16 +00:00
Signed-off-by: SY <shiiyan79@gmail.com> Signed-off-by: Alexandre Gaudreault <alexandre_gaudreault@intuit.com> Co-authored-by: Alexandre Gaudreault <alexandre_gaudreault@intuit.com>
796 lines
39 KiB
TypeScript
796 lines
39 KiB
TypeScript
/* eslint-disable no-prototype-builtins */
|
|
import {AutocompleteField, Checkbox, DropDownMenu, ErrorNotification, FormField, FormSelect, HelpIcon, NotificationType} from 'argo-ui';
|
|
import * as React from 'react';
|
|
import {FormApi, Text} from 'react-form';
|
|
import {
|
|
ClipboardText,
|
|
Cluster,
|
|
DataLoader,
|
|
EditablePanel,
|
|
EditablePanelItem,
|
|
EditablePanelContent,
|
|
Expandable,
|
|
MapInputField,
|
|
NumberField,
|
|
Repo,
|
|
Revision,
|
|
RevisionHelpIcon
|
|
} from '../../../shared/components';
|
|
import {BadgePanel} from '../../../shared/components';
|
|
import {AuthSettingsCtx, Consumer, ContextApis} from '../../../shared/context';
|
|
import * as models from '../../../shared/models';
|
|
import {services} from '../../../shared/services';
|
|
|
|
import {ApplicationSyncOptionsField} from '../application-sync-options/application-sync-options';
|
|
import {RevisionFormField} from '../revision-form-field/revision-form-field';
|
|
import {ComparisonStatusIcon, HealthStatusIcon, syncStatusMessage, urlPattern, formatCreationTimestamp, getAppDefaultSource, getAppSpecDefaultSource, appRBACName} from '../utils';
|
|
import {ApplicationRetryOptions} from '../application-retry-options/application-retry-options';
|
|
import {ApplicationRetryView} from '../application-retry-view/application-retry-view';
|
|
import {Link} from 'react-router-dom';
|
|
import {EditNotificationSubscriptions, useEditNotificationSubscriptions} from './edit-notification-subscriptions';
|
|
import {EditAnnotations} from './edit-annotations';
|
|
|
|
import './application-summary.scss';
|
|
import {DeepLinks} from '../../../shared/components/deep-links';
|
|
|
|
function swap(array: any[], a: number, b: number) {
|
|
array = array.slice();
|
|
[array[a], array[b]] = [array[b], array[a]];
|
|
return array;
|
|
}
|
|
|
|
function processPath(path: string) {
|
|
if (path !== null && path !== undefined) {
|
|
if (path === '.') {
|
|
return '(root)';
|
|
}
|
|
return path;
|
|
}
|
|
return '';
|
|
}
|
|
|
|
export interface ApplicationSummaryProps {
|
|
app: models.Application;
|
|
updateApp: (app: models.Application, query: {validate?: boolean}) => Promise<any>;
|
|
}
|
|
|
|
export const ApplicationSummary = (props: ApplicationSummaryProps) => {
|
|
const app = JSON.parse(JSON.stringify(props.app)) as models.Application;
|
|
const source = getAppDefaultSource(app);
|
|
const isHelm = source.hasOwnProperty('chart');
|
|
const initialState = app.spec.destination.server === undefined ? 'NAME' : 'URL';
|
|
const useAuthSettingsCtx = React.useContext(AuthSettingsCtx);
|
|
const [destFormat, setDestFormat] = React.useState(initialState);
|
|
const [, setChangeSync] = React.useState(false);
|
|
const [isHydratorEnabled, setIsHydratorEnabled] = React.useState(!!app.spec.sourceHydrator);
|
|
const [savedSyncSource, setSavedSyncSource] = React.useState(app.spec.sourceHydrator?.syncSource || {targetBranch: '', path: ''});
|
|
|
|
const notificationSubscriptions = useEditNotificationSubscriptions(app.metadata.annotations || {});
|
|
const updateApp = notificationSubscriptions.withNotificationSubscriptions(props.updateApp);
|
|
|
|
const updateAppSource = async (input: models.Application, query: {validate?: boolean}) => {
|
|
const hydrateTo = input.spec.sourceHydrator?.hydrateTo;
|
|
if (hydrateTo && !hydrateTo.targetBranch?.trim() && input.spec.sourceHydrator) {
|
|
delete input.spec.sourceHydrator.hydrateTo;
|
|
}
|
|
return updateApp(input, query);
|
|
};
|
|
|
|
const hasMultipleSources = app.spec.sources && app.spec.sources.length > 0;
|
|
const isHydrator = isHydratorEnabled;
|
|
const repoType = source.repoURL.startsWith('oci://') ? 'oci' : (source.hasOwnProperty('chart') && 'helm') || 'git';
|
|
|
|
const attributes = [
|
|
{
|
|
title: 'PROJECT',
|
|
view: <Link to={'/settings/projects/' + app.spec.project}>{app.spec.project}</Link>,
|
|
edit: (formApi: FormApi) => (
|
|
<DataLoader load={() => services.projects.list('items.metadata.name').then(projs => projs.map(item => item.metadata.name))}>
|
|
{projects => <FormField formApi={formApi} field='spec.project' component={FormSelect} componentProps={{options: projects}} />}
|
|
</DataLoader>
|
|
)
|
|
},
|
|
{
|
|
title: 'LABELS',
|
|
view: Object.keys(app.metadata.labels || {})
|
|
.map(label => `${label}=${app.metadata.labels[label]}`)
|
|
.join(' '),
|
|
edit: (formApi: FormApi) => <FormField formApi={formApi} field='metadata.labels' component={MapInputField} />
|
|
},
|
|
{
|
|
title: 'ANNOTATIONS',
|
|
view: (
|
|
<Expandable height={48}>
|
|
{Object.keys(app.metadata.annotations || {})
|
|
.map(annotation => `${annotation}=${app.metadata.annotations[annotation]}`)
|
|
.join(' ')}
|
|
</Expandable>
|
|
),
|
|
edit: (formApi: FormApi) => <EditAnnotations formApi={formApi} app={app} />
|
|
},
|
|
{
|
|
title: 'NOTIFICATION SUBSCRIPTIONS',
|
|
view: false, // eventually the subscription input values will be merged in 'ANNOTATIONS', therefore 'ANNOATIONS' section is responsible to represent subscription values,
|
|
edit: () => <EditNotificationSubscriptions {...notificationSubscriptions} />
|
|
},
|
|
{
|
|
title: 'CLUSTER',
|
|
view: <Cluster server={app.spec.destination.server} name={app.spec.destination.name} showUrl={true} />,
|
|
edit: (formApi: FormApi) => (
|
|
<DataLoader load={() => services.clusters.list().then(clusters => clusters.sort())}>
|
|
{clusters => {
|
|
return (
|
|
<div className='row'>
|
|
{(destFormat.toUpperCase() === 'URL' && (
|
|
<div className='columns small-10'>
|
|
<FormField
|
|
formApi={formApi}
|
|
field='spec.destination.server'
|
|
componentProps={{items: clusters.map(cluster => cluster.server)}}
|
|
component={AutocompleteField}
|
|
/>
|
|
</div>
|
|
)) || (
|
|
<div className='columns small-10'>
|
|
<FormField
|
|
formApi={formApi}
|
|
field='spec.destination.name'
|
|
componentProps={{items: clusters.map(cluster => cluster.name)}}
|
|
component={AutocompleteField}
|
|
/>
|
|
</div>
|
|
)}
|
|
<div className='columns small-2'>
|
|
<div>
|
|
<DropDownMenu
|
|
anchor={() => (
|
|
<p>
|
|
{destFormat.toUpperCase()} <i className='fa fa-caret-down' />
|
|
</p>
|
|
)}
|
|
items={['URL', 'NAME'].map((type: 'URL' | 'NAME') => ({
|
|
title: type,
|
|
action: () => {
|
|
if (destFormat !== type) {
|
|
const updatedApp = formApi.getFormState().values as models.Application;
|
|
if (type === 'URL') {
|
|
updatedApp.spec.destination.server = '';
|
|
delete updatedApp.spec.destination.name;
|
|
} else {
|
|
updatedApp.spec.destination.name = '';
|
|
delete updatedApp.spec.destination.server;
|
|
}
|
|
formApi.setAllValues(updatedApp);
|
|
setDestFormat(type);
|
|
}
|
|
}
|
|
}))}
|
|
/>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
}}
|
|
</DataLoader>
|
|
)
|
|
},
|
|
{
|
|
title: 'NAMESPACE',
|
|
view: <ClipboardText text={app.spec.destination.namespace} />,
|
|
edit: (formApi: FormApi) => <FormField formApi={formApi} field='spec.destination.namespace' component={Text} />
|
|
},
|
|
{
|
|
title: 'CREATED AT',
|
|
view: formatCreationTimestamp(app.metadata.creationTimestamp)
|
|
},
|
|
{
|
|
title: 'REVISION HISTORY LIMIT',
|
|
view: app.spec.revisionHistoryLimit,
|
|
edit: (formApi: FormApi) => (
|
|
<div style={{position: 'relative'}}>
|
|
<FormField
|
|
formApi={formApi}
|
|
field='spec.revisionHistoryLimit'
|
|
componentProps={{style: {paddingRight: '1em', right: '1em'}, placeholder: '10'}}
|
|
component={NumberField}
|
|
/>
|
|
<div style={{position: 'absolute', right: '0', top: '0'}}>
|
|
<HelpIcon
|
|
title='This limits the number of items kept in the apps revision history.
|
|
This should only be changed in exceptional circumstances.
|
|
Setting to zero will store no history. This will reduce storage used.
|
|
Increasing will increase the space used to store the history, so we do not recommend increasing it.
|
|
Default is 10.'
|
|
/>
|
|
</div>
|
|
</div>
|
|
)
|
|
},
|
|
{
|
|
title: 'SYNC OPTIONS',
|
|
view: (
|
|
<div style={{display: 'flex', flexWrap: 'wrap'}}>
|
|
{((app.spec.syncPolicy || {}).syncOptions || []).map(opt =>
|
|
opt.endsWith('=true') || opt.endsWith('=false') ? (
|
|
<div key={opt} style={{marginRight: '10px'}}>
|
|
<i className={`fa fa-${opt.includes('=true') ? 'check-square' : 'times'}`} /> {opt.replace('=true', '').replace('=false', '')}
|
|
</div>
|
|
) : (
|
|
<div key={opt} style={{marginRight: '10px'}}>
|
|
{opt}
|
|
</div>
|
|
)
|
|
)}
|
|
</div>
|
|
),
|
|
edit: (formApi: FormApi) => (
|
|
<div>
|
|
<FormField formApi={formApi} field='spec.syncPolicy.syncOptions' component={ApplicationSyncOptionsField} />
|
|
</div>
|
|
)
|
|
},
|
|
{
|
|
title: 'RETRY OPTIONS',
|
|
view: <ApplicationRetryView initValues={app.spec.syncPolicy ? app.spec.syncPolicy.retry : null} />,
|
|
edit: (formApi: FormApi) => (
|
|
<div>
|
|
<ApplicationRetryOptions formApi={formApi} initValues={app.spec.syncPolicy ? app.spec.syncPolicy.retry : null} field='spec.syncPolicy.retry' />
|
|
</div>
|
|
)
|
|
},
|
|
{
|
|
title: 'STATUS',
|
|
view: (
|
|
<span>
|
|
<ComparisonStatusIcon status={app.status.sync.status} /> {app.status.sync.status} {syncStatusMessage(app)}
|
|
</span>
|
|
)
|
|
},
|
|
{
|
|
title: 'HEALTH',
|
|
view: (
|
|
<span>
|
|
<HealthStatusIcon state={app.status.health} /> {app.status.health.status}
|
|
</span>
|
|
)
|
|
},
|
|
{
|
|
title: 'LINKS',
|
|
view: (
|
|
<DataLoader load={() => services.applications.getLinks(app.metadata.name, app.metadata.namespace)} input={app} key='appLinks'>
|
|
{(links: models.LinksResponse) => <DeepLinks links={links.items} />}
|
|
</DataLoader>
|
|
)
|
|
}
|
|
];
|
|
|
|
const urls = app.status.summary.externalURLs || [];
|
|
if (urls.length > 0) {
|
|
attributes.push({
|
|
title: 'URLs',
|
|
view: (
|
|
<div className='application-summary__links-rows'>
|
|
{urls
|
|
.map(item => item.split('|'))
|
|
.map((parts, i) => (
|
|
<div className='application-summary__links-row'>
|
|
<a key={i} href={parts.length > 1 ? parts[1] : parts[0]} target='_blank'>
|
|
{parts[0]}
|
|
</a>
|
|
</div>
|
|
))}
|
|
</div>
|
|
)
|
|
});
|
|
}
|
|
|
|
if ((app.status.summary.images || []).length) {
|
|
attributes.push({
|
|
title: 'IMAGES',
|
|
view: (
|
|
<div className='application-summary__labels'>
|
|
{(app.status.summary.images || []).sort().map(image => (
|
|
<span className='application-summary__label' key={image}>
|
|
{image}
|
|
</span>
|
|
))}
|
|
</div>
|
|
)
|
|
});
|
|
}
|
|
|
|
const standardSourceItems: EditablePanelItem[] = useAuthSettingsCtx?.hydratorEnabled
|
|
? [
|
|
{
|
|
title: 'ENABLE HYDRATOR',
|
|
hint: 'Enable Source Hydrator to render and push manifests to a Git branch.',
|
|
view: false, // Hide in view mode
|
|
edit: (formApi: FormApi) => (
|
|
<div className='checkbox-container'>
|
|
<Checkbox
|
|
onChange={(val: boolean) => {
|
|
const updatedApp = formApi.getFormState().values as models.Application;
|
|
if (val) {
|
|
// Enable hydrator - move source to sourceHydrator.drySource
|
|
if (!updatedApp.spec.sourceHydrator) {
|
|
updatedApp.spec.sourceHydrator = {
|
|
drySource: {
|
|
repoURL: updatedApp.spec.source.repoURL,
|
|
targetRevision: updatedApp.spec.source.targetRevision,
|
|
path: updatedApp.spec.source.path
|
|
},
|
|
syncSource: savedSyncSource
|
|
};
|
|
delete updatedApp.spec.source;
|
|
}
|
|
} else {
|
|
// Disable hydrator - save sync source values and move drySource back to source
|
|
if (updatedApp.spec.sourceHydrator) {
|
|
setSavedSyncSource(updatedApp.spec.sourceHydrator.syncSource);
|
|
updatedApp.spec.source = updatedApp.spec.sourceHydrator.drySource;
|
|
delete updatedApp.spec.sourceHydrator;
|
|
}
|
|
}
|
|
formApi.setAllValues(updatedApp);
|
|
setIsHydratorEnabled(val);
|
|
}}
|
|
checked={!!(formApi.getFormState().values as models.Application).spec.sourceHydrator}
|
|
id='enable-hydrator'
|
|
/>
|
|
<label htmlFor='enable-hydrator'>SOURCE HYDRATOR ENABLED</label>
|
|
</div>
|
|
)
|
|
}
|
|
]
|
|
: [];
|
|
|
|
const sourceItems: EditablePanelItem[] = [
|
|
{
|
|
title: 'REPO URL',
|
|
view: <Repo url={app.spec.source?.repoURL} />,
|
|
edit: (formApi: FormApi) => <FormField formApi={formApi} field='spec.source.repoURL' component={Text} />
|
|
},
|
|
...(isHelm
|
|
? [
|
|
{
|
|
title: 'CHART',
|
|
view: <span>{source && `${source.chart}:${source.targetRevision}`}</span>,
|
|
edit: (formApi: FormApi) => (
|
|
<DataLoader
|
|
input={{repoURL: getAppSpecDefaultSource(formApi.getFormState().values.spec).repoURL}}
|
|
load={src => services.repos.charts(src.repoURL).catch(() => new Array<models.HelmChart>())}>
|
|
{(charts: models.HelmChart[]) => (
|
|
<div className='row'>
|
|
<div className='columns small-8'>
|
|
<FormField
|
|
formApi={formApi}
|
|
field='spec.source.chart'
|
|
component={AutocompleteField}
|
|
componentProps={{items: charts.map(chart => chart.name), filterSuggestions: true}}
|
|
/>
|
|
</div>
|
|
<DataLoader
|
|
input={{charts, chart: getAppSpecDefaultSource(formApi.getFormState().values.spec).chart}}
|
|
load={async data => {
|
|
const chartInfo = data.charts.find(chart => chart.name === data.chart);
|
|
return (chartInfo && chartInfo.versions) || new Array<string>();
|
|
}}>
|
|
{(versions: string[]) => (
|
|
<div className='columns small-4'>
|
|
<FormField
|
|
formApi={formApi}
|
|
field='spec.source.targetRevision'
|
|
component={AutocompleteField}
|
|
componentProps={{items: versions}}
|
|
/>
|
|
<RevisionHelpIcon type='helm' top='0' />
|
|
</div>
|
|
)}
|
|
</DataLoader>
|
|
</div>
|
|
)}
|
|
</DataLoader>
|
|
)
|
|
}
|
|
]
|
|
: [
|
|
{
|
|
title: 'TARGET REVISION',
|
|
view: <Revision repoUrl={source.repoURL} revision={source.targetRevision || 'HEAD'} />,
|
|
edit: (formApi: FormApi) => (
|
|
<RevisionFormField hideLabel={true} formApi={formApi} fieldValue='spec.source.targetRevision' repoURL={source.repoURL} repoType={repoType} />
|
|
)
|
|
},
|
|
{
|
|
title: 'PATH',
|
|
view: (
|
|
<Revision repoUrl={source.repoURL} revision={source.targetRevision || 'HEAD'} path={source.path} isForPath={true}>
|
|
{processPath(source.path)}
|
|
</Revision>
|
|
),
|
|
edit: (formApi: FormApi) => <FormField formApi={formApi} field='spec.source.path' component={Text} />
|
|
}
|
|
])
|
|
];
|
|
|
|
const drySourceItems: EditablePanelItem[] = [
|
|
{
|
|
title: 'REPO URL',
|
|
hint: 'Git repo containing the unrendered source (Helm/Kustomize/manifests).',
|
|
view: <Repo url={app.spec.sourceHydrator?.drySource?.repoURL} />,
|
|
edit: (formApi: FormApi) => <FormField formApi={formApi} field='spec.sourceHydrator.drySource.repoURL' component={Text} />
|
|
},
|
|
{
|
|
title: 'TARGET REVISION',
|
|
hint: 'Git revision to read the dry source from (branch/tag/SHA).',
|
|
view: <Revision repoUrl={app.spec.sourceHydrator?.drySource?.repoURL} revision={app.spec.sourceHydrator?.drySource?.targetRevision || 'HEAD'} />,
|
|
edit: (formApi: FormApi) => (
|
|
<RevisionFormField
|
|
hideLabel={true}
|
|
formApi={formApi}
|
|
fieldValue='spec.sourceHydrator.drySource.targetRevision'
|
|
repoURL={app.spec.sourceHydrator?.drySource?.repoURL}
|
|
repoType={repoType}
|
|
/>
|
|
)
|
|
},
|
|
{
|
|
title: 'PATH',
|
|
hint: 'Directory in the dry repo with the unrendered source.',
|
|
view: (
|
|
<Revision
|
|
repoUrl={app.spec.sourceHydrator?.drySource?.repoURL}
|
|
revision={app.spec.sourceHydrator?.drySource?.targetRevision || 'HEAD'}
|
|
path={app.spec.sourceHydrator?.drySource?.path}
|
|
isForPath={true}>
|
|
{processPath(app.spec.sourceHydrator?.drySource?.path)}
|
|
</Revision>
|
|
),
|
|
edit: (formApi: FormApi) => <FormField formApi={formApi} field='spec.sourceHydrator.drySource.path' component={Text} />
|
|
}
|
|
];
|
|
|
|
const syncSourceItems: EditablePanelItem[] = [
|
|
{
|
|
title: 'TARGET BRANCH',
|
|
hint: 'Branch where hydrated manifests are written and synced from.',
|
|
view: <Revision repoUrl={app.spec.sourceHydrator?.drySource?.repoURL} revision={app.spec.sourceHydrator?.syncSource?.targetBranch || 'HEAD'} />,
|
|
edit: (formApi: FormApi) => (
|
|
<RevisionFormField
|
|
helpIconTop={'0'}
|
|
hideLabel={true}
|
|
formApi={formApi}
|
|
fieldValue='spec.sourceHydrator.syncSource.targetBranch'
|
|
repoURL={source.repoURL}
|
|
repoType='git'
|
|
revisionType='Branches'
|
|
/>
|
|
)
|
|
},
|
|
{
|
|
title: 'PATH',
|
|
hint: 'Output directory for hydrated manifests; must be a non-root path.',
|
|
view: (
|
|
<Revision
|
|
repoUrl={app.spec.sourceHydrator?.drySource?.repoURL}
|
|
revision={app.spec.sourceHydrator?.syncSource?.targetBranch || 'HEAD'}
|
|
path={app.spec.sourceHydrator?.syncSource?.path}
|
|
isForPath={true}>
|
|
{processPath(app.spec.sourceHydrator?.syncSource?.path)}
|
|
</Revision>
|
|
),
|
|
edit: (formApi: FormApi) => <FormField formApi={formApi} field='spec.sourceHydrator.syncSource.path' component={Text} />
|
|
}
|
|
];
|
|
|
|
const hydrateToItems: EditablePanelItem[] = [
|
|
{
|
|
title: 'TARGET BRANCH (OPTIONAL)',
|
|
hint: 'Optional staging branch to write hydrated output before syncing.',
|
|
view: (
|
|
<Revision
|
|
repoUrl={app.spec.sourceHydrator?.drySource?.repoURL}
|
|
revision={
|
|
app.spec.sourceHydrator?.hydrateTo?.targetBranch ||
|
|
(app.spec.sourceHydrator?.syncSource?.targetBranch ? `${app.spec.sourceHydrator.syncSource.targetBranch}-next` : 'HEAD')
|
|
}
|
|
/>
|
|
),
|
|
edit: (formApi: FormApi) => (
|
|
<RevisionFormField
|
|
helpIconTop={'0'}
|
|
hideLabel={true}
|
|
formApi={formApi}
|
|
fieldValue='spec.sourceHydrator.hydrateTo.targetBranch'
|
|
repoURL={source.repoURL}
|
|
repoType='git'
|
|
revisionType='Branches'
|
|
/>
|
|
)
|
|
}
|
|
];
|
|
|
|
const sourceAttributes: (EditablePanelItem | EditablePanelContent)[] = isHydrator
|
|
? [
|
|
...standardSourceItems,
|
|
{
|
|
sectionName: 'Dry Source',
|
|
items: drySourceItems
|
|
},
|
|
{
|
|
sectionName: 'Sync Source',
|
|
items: syncSourceItems
|
|
},
|
|
{
|
|
sectionName: 'Hydrate To',
|
|
items: hydrateToItems
|
|
}
|
|
]
|
|
: [...standardSourceItems, ...sourceItems];
|
|
|
|
async function toggleAutoSync(ctx: ContextApis) {
|
|
const isEnabled = app.spec.syncPolicy?.automated && app.spec.syncPolicy.automated.enabled !== false;
|
|
const confirmationTitle = isEnabled ? 'Disable Auto-Sync?' : 'Enable Auto-Sync?';
|
|
const confirmationText = isEnabled
|
|
? 'Are you sure you want to disable automated application synchronization'
|
|
: 'If checked, application will automatically sync when changes are detected';
|
|
const confirmed = await ctx.popup.confirm(confirmationTitle, confirmationText);
|
|
if (confirmed) {
|
|
try {
|
|
setChangeSync(true);
|
|
const canUpdate = await services.accounts.canI('applications', 'update', appRBACName(app)).catch(() => false);
|
|
if (canUpdate) {
|
|
const updatedApp = JSON.parse(JSON.stringify(props.app)) as models.Application;
|
|
if (!updatedApp.spec.syncPolicy) {
|
|
updatedApp.spec.syncPolicy = {};
|
|
}
|
|
const existingAutomated = updatedApp.spec.syncPolicy.automated;
|
|
updatedApp.spec.syncPolicy.automated = {
|
|
prune: existingAutomated?.prune ?? false,
|
|
selfHeal: existingAutomated?.selfHeal ?? false,
|
|
enabled: !isEnabled
|
|
};
|
|
await updateApp(updatedApp, {validate: false});
|
|
} else {
|
|
await services.applications.runResourceAction(
|
|
app.metadata.name,
|
|
app.metadata.namespace,
|
|
{
|
|
name: app.metadata.name,
|
|
namespace: app.metadata.namespace,
|
|
group: 'argoproj.io',
|
|
kind: 'Application',
|
|
version: 'v1alpha1'
|
|
} as models.ResourceNode,
|
|
'toggle-auto-sync',
|
|
[]
|
|
);
|
|
}
|
|
} catch (e) {
|
|
ctx.notifications.show({
|
|
content: <ErrorNotification title={`Unable to "${confirmationTitle.replace(/\?/g, '')}"`} e={e} />,
|
|
type: NotificationType.Error
|
|
});
|
|
} finally {
|
|
setChangeSync(false);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Handler for the PRUNE RESOURCES and SELF HEAL checkboxes only; auto-sync toggling lives in `toggleAutoSync` above.
|
|
async function setAutoSync(ctx: ContextApis, confirmationTitle: string, confirmationText: string, prune: boolean, selfHeal: boolean, enable: boolean) {
|
|
const confirmed = await ctx.popup.confirm(confirmationTitle, confirmationText);
|
|
if (confirmed) {
|
|
try {
|
|
setChangeSync(true);
|
|
const updatedApp = JSON.parse(JSON.stringify(props.app)) as models.Application;
|
|
if (!updatedApp.spec.syncPolicy) {
|
|
updatedApp.spec.syncPolicy = {};
|
|
}
|
|
|
|
updatedApp.spec.syncPolicy.automated = {prune, selfHeal, enabled: enable};
|
|
await updateApp(updatedApp, {validate: false});
|
|
} catch (e) {
|
|
ctx.notifications.show({
|
|
content: <ErrorNotification title={`Unable to "${confirmationTitle.replace(/\?/g, '')}"`} e={e} />,
|
|
type: NotificationType.Error
|
|
});
|
|
} finally {
|
|
setChangeSync(false);
|
|
}
|
|
}
|
|
}
|
|
|
|
const items = app.spec.info || [];
|
|
const [adjustedCount, setAdjustedCount] = React.useState(0);
|
|
|
|
const added = new Array<{name: string; value: string; key: string}>();
|
|
for (let i = 0; i < adjustedCount; i++) {
|
|
added.push({name: '', value: '', key: (items.length + i).toString()});
|
|
}
|
|
for (let i = 0; i > adjustedCount; i--) {
|
|
items.pop();
|
|
}
|
|
const allItems = items.concat(added);
|
|
const infoItems: EditablePanelItem[] = allItems
|
|
.map((info, i) => ({
|
|
key: i.toString(),
|
|
title: info.name,
|
|
view: info.value.match(urlPattern) ? (
|
|
<a href={info.value} target='__blank'>
|
|
{info.value}
|
|
</a>
|
|
) : (
|
|
info.value
|
|
),
|
|
titleEdit: (formApi: FormApi) => (
|
|
<React.Fragment>
|
|
{i > 0 && (
|
|
<i
|
|
className='fa fa-sort-up application-summary__sort-icon'
|
|
onClick={() => {
|
|
formApi.setValue('spec.info', swap(formApi.getFormState().values.spec.info || [], i, i - 1));
|
|
}}
|
|
/>
|
|
)}
|
|
<FormField formApi={formApi} field={`spec.info[${[i]}].name`} component={Text} componentProps={{style: {width: '99%'}}} />
|
|
{i < allItems.length - 1 && (
|
|
<i
|
|
className='fa fa-sort-down application-summary__sort-icon'
|
|
onClick={() => {
|
|
formApi.setValue('spec.info', swap(formApi.getFormState().values.spec.info || [], i, i + 1));
|
|
}}
|
|
/>
|
|
)}
|
|
</React.Fragment>
|
|
),
|
|
edit: (formApi: FormApi) => (
|
|
<React.Fragment>
|
|
<FormField formApi={formApi} field={`spec.info[${[i]}].value`} component={Text} />
|
|
<i
|
|
className='fa fa-times application-summary__remove-icon'
|
|
onClick={() => {
|
|
const values = (formApi.getFormState().values.spec.info || []) as Array<any>;
|
|
formApi.setValue('spec.info', [...values.slice(0, i), ...values.slice(i + 1, values.length)]);
|
|
setAdjustedCount(adjustedCount - 1);
|
|
}}
|
|
/>
|
|
</React.Fragment>
|
|
)
|
|
}))
|
|
.concat({
|
|
key: '-1',
|
|
title: '',
|
|
titleEdit: () => (
|
|
<button
|
|
className='argo-button argo-button--base'
|
|
onClick={() => {
|
|
setAdjustedCount(adjustedCount + 1);
|
|
}}>
|
|
ADD NEW ITEM
|
|
</button>
|
|
),
|
|
view: null as any,
|
|
edit: null
|
|
});
|
|
|
|
return (
|
|
<div className='application-summary'>
|
|
<EditablePanel
|
|
save={updateApp}
|
|
view={hasMultipleSources ? <>This is a multi-source app, see the Sources tab for repository URLs and source-related information.</> : null}
|
|
validate={input => ({
|
|
'spec.project': !input.spec.project && 'Project name is required',
|
|
'spec.destination.server': !input.spec.destination.server && input.spec.destination.hasOwnProperty('server') && 'Cluster server is required',
|
|
'spec.destination.name': !input.spec.destination.name && input.spec.destination.hasOwnProperty('name') && 'Cluster name is required'
|
|
})}
|
|
values={app}
|
|
title={app.metadata.name.toLocaleUpperCase()}
|
|
items={attributes}
|
|
onModeSwitch={() => notificationSubscriptions.onResetNotificationSubscriptions()}
|
|
/>
|
|
{!hasMultipleSources && (
|
|
<EditablePanel
|
|
save={updateAppSource}
|
|
values={app}
|
|
title='SOURCE'
|
|
items={sourceAttributes}
|
|
hasMultipleSources={hasMultipleSources}
|
|
onModeSwitch={() => notificationSubscriptions.onResetNotificationSubscriptions()}
|
|
/>
|
|
)}
|
|
<Consumer>
|
|
{ctx => (
|
|
<div className='white-box'>
|
|
<div className='white-box__details'>
|
|
<p>SYNC POLICY</p>
|
|
<div className='row white-box__details-row'>
|
|
<div className='columns small-3'>
|
|
{app.spec.syncPolicy?.automated && app.spec.syncPolicy.automated.enabled !== false ? <span>AUTOMATED</span> : <span>NONE</span>}
|
|
</div>
|
|
</div>
|
|
<div className='row white-box__details-row'>
|
|
<div className='columns small-12'>
|
|
<div className='checkbox-container'>
|
|
<Checkbox
|
|
onChange={async () => {
|
|
await toggleAutoSync(ctx);
|
|
}}
|
|
checked={app.spec.syncPolicy?.automated && app.spec.syncPolicy.automated.enabled !== false}
|
|
id='enable-auto-sync'
|
|
/>
|
|
<label htmlFor='enable-auto-sync'>ENABLE AUTO-SYNC</label>
|
|
<HelpIcon title='If checked, application will automatically sync when changes are detected' />
|
|
</div>
|
|
</div>
|
|
</div>
|
|
{app.spec.syncPolicy && app.spec.syncPolicy.automated && (
|
|
<React.Fragment>
|
|
<div className='row white-box__details-row'>
|
|
<div className='columns small-12'>
|
|
<div className='checkbox-container'>
|
|
<Checkbox
|
|
onChange={async (prune: boolean) => {
|
|
const automated = app.spec.syncPolicy?.automated || {selfHeal: false, enabled: false};
|
|
setAutoSync(
|
|
ctx,
|
|
prune ? 'Enable Prune Resources?' : 'Disable Prune Resources?',
|
|
prune
|
|
? 'Are you sure you want to enable resource pruning during automated application synchronization?'
|
|
: 'Are you sure you want to disable resource pruning during automated application synchronization?',
|
|
prune,
|
|
automated.selfHeal,
|
|
automated.enabled
|
|
);
|
|
}}
|
|
checked={!!app.spec.syncPolicy?.automated?.prune}
|
|
id='prune-resources'
|
|
/>
|
|
<label htmlFor='prune-resources'>PRUNE RESOURCES</label>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div className='row white-box__details-row'>
|
|
<div className='columns small-12'>
|
|
<div className='checkbox-container'>
|
|
<Checkbox
|
|
onChange={async (selfHeal: boolean) => {
|
|
const automated = app.spec.syncPolicy?.automated || {prune: false, enabled: false};
|
|
setAutoSync(
|
|
ctx,
|
|
selfHeal ? 'Enable Self Heal?' : 'Disable Self Heal?',
|
|
selfHeal
|
|
? 'If checked, application will automatically sync when changes are detected'
|
|
: 'If unchecked, application will not automatically sync when changes are detected',
|
|
automated.prune,
|
|
selfHeal,
|
|
automated.enabled
|
|
);
|
|
}}
|
|
checked={!!app.spec.syncPolicy?.automated?.selfHeal}
|
|
id='self-heal'
|
|
/>
|
|
<label htmlFor='self-heal'>SELF HEAL</label>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</React.Fragment>
|
|
)}
|
|
</div>
|
|
</div>
|
|
)}
|
|
</Consumer>
|
|
<BadgePanel app={props.app.metadata.name} appNamespace={props.app.metadata.namespace} nsEnabled={useAuthSettingsCtx?.appsInAnyNamespaceEnabled} />
|
|
<EditablePanel
|
|
save={updateApp}
|
|
values={app}
|
|
title='INFO'
|
|
items={infoItems}
|
|
onModeSwitch={() => {
|
|
setAdjustedCount(0);
|
|
notificationSubscriptions.onResetNotificationSubscriptions();
|
|
}}
|
|
/>
|
|
</div>
|
|
);
|
|
};
|