feat(ui): improve sync warnings (#25524)

Signed-off-by: Jonathan Winters <wintersjonathan0@gmail.com>
This commit is contained in:
jwinters01
2025-12-12 10:55:32 -05:00
committed by GitHub
parent 53b0beae4a
commit b74c7aa31f
3 changed files with 169 additions and 34 deletions

View File

@@ -6,9 +6,36 @@ import * as ReactForm from 'react-form';
import './application-sync-options.scss';
import {services} from '../../../shared/services';
export const REPLACE_WARNING = `The resources will be synced using 'kubectl replace/create' command that is a potentially destructive action and might cause resources recreation. For example, it might cause a number of pods to be reset to the minimum number of replicas and cause them to become overloaded.`;
const ReplaceWarning = () => (
<div>
<p>
Argo CD will sync using <strong>kubectl replace/create</strong>. This operation <strong>forces resource deletion and recreation</strong>. Proceed only if you understand
the risks.
</p>
</div>
);
const PruneAllWarning = () => (
<div>
<p>
The resources will be synced using --prune, and all resources are marked to be pruned. Only continue if you want to{' '}
<strong>delete all of the Application's resources.</strong>
</p>
</div>
);
const PruneSomeWarning = () => (
<div>
<p>
The resources will be synced using --prune, and some resources are marked to be pruned. Only continue if you want to <strong>delete the pruned resources</strong>.
</p>
</div>
);
export const REPLACE_WARNING = <ReplaceWarning />;
export const FORCE_WARNING = `The resources will be synced using '--force' that is a potentially destructive action and will immediately remove resources from the API and bypasses graceful deletion. Immediate deletion of some resources may result in inconsistency or data loss.`;
export const PRUNE_ALL_WARNING = `The resources will be synced using '--prune', and all resources are marked to be pruned. Only continue if you want to delete all of the Application's resources.`;
export const PRUNE_ALL_WARNING = <PruneAllWarning />;
export const PRUNE_SOME_WARNING = <PruneSomeWarning />;
export interface ApplicationSyncOptionProps {
options: string[];
@@ -42,7 +69,7 @@ function selectOption(name: string, label: string, defaultVal: string, values: s
);
}
function booleanOption(name: string, label: string, defaultVal: boolean, props: ApplicationSyncOptionProps, invert: boolean, warning: string = null) {
function booleanOption(name: string, label: string, defaultVal: boolean, props: ApplicationSyncOptionProps, invert: boolean, warning: string | React.ReactNode = null) {
const options = [...(props.options || [])];
const prefix = `${name}=`;
const index = options.findIndex(item => item.startsWith(prefix));
@@ -64,7 +91,7 @@ function booleanOption(name: string, label: string, defaultVal: boolean, props:
<label htmlFor={`sync-option-${name}-${props.id}`}>{label}</label>{' '}
{warning && (
<>
<Tooltip content={warning}>
<Tooltip content={typeof warning === 'string' ? warning : 'Warning'}>
<i className='fa fa-exclamation-triangle' />
</Tooltip>
{checked && <div className='application-sync-options__warning'>{warning}</div>}
@@ -120,7 +147,7 @@ export const ApplicationSyncOptions = (props: ApplicationSyncOptionProps) => (
{syncWithReplaceAllowed =>
(syncWithReplaceAllowed && (
<div className='small-12' style={optionStyle}>
{booleanOption('Replace', 'Replace', false, props, false, REPLACE_WARNING)}
{booleanOption('Replace', 'Replace', false, props, false)}
</div>
)) ||
null

View File

@@ -13,7 +13,8 @@ import {
FORCE_WARNING,
SyncFlags,
REPLACE_WARNING,
PRUNE_ALL_WARNING
PRUNE_ALL_WARNING,
PRUNE_SOME_WARNING
} from '../application-sync-options/application-sync-options';
import {ComparisonStatusIcon, getAppDefaultSource, nodeKey} from '../utils';
@@ -68,14 +69,132 @@ export const ApplicationSyncPanel = ({application, selectedResource, hide}: {app
const allResourcesAreSelected = selectedResources.length === appResources.length;
const syncFlags = {...params.syncFlags} as SyncFlags;
const allRequirePruning = !selectedResources.some(resource => !resource?.requiresPruning);
if (syncFlags.Prune && allRequirePruning && allResourcesAreSelected) {
const confirmed = await ctx.popup.confirm('Prune all resources?', () => (
<div>
<i className='fa fa-exclamation-triangle' style={{color: ARGO_WARNING_COLOR}} />
{PRUNE_ALL_WARNING} Are you sure you want to continue?
</div>
));
const resourcesToPrune = selectedResources.filter(resource => resource?.requiresPruning);
const allRequirePruning = resourcesToPrune.length === selectedResources.length;
const anyRequirePruning = resourcesToPrune.length > 0;
const warnAgainstPruneAll = allRequirePruning && allResourcesAreSelected;
if (syncFlags.Prune) {
if (warnAgainstPruneAll) {
const confirmed = await ctx.popup.prompt(
'Prune all resources?',
api => (
<div>
<p>{PRUNE_ALL_WARNING}</p>
<p>
<strong>Resources to be deleted ({resourcesToPrune.length}):</strong>
</p>
<ul style={{maxHeight: '200px', overflowY: 'auto', marginBottom: '1em'}}>
{resourcesToPrune.map(resource => (
<li key={nodeKey(resource)}>
{resource.kind}/{resource.name}
{resource.namespace && ` (${resource.namespace})`}
</li>
))}
</ul>
<div className='argo-form-row'>
<FormField
label="Please type 'prune' to confirm this action"
formApi={api}
field='confirmText'
qeId='prune-all-field-confirmation'
component={Text}
/>
</div>
</div>
),
{
validate: vals => ({
confirmText: vals.confirmText !== 'prune' && "Type 'prune' to confirm"
}),
submit: async (vals, _, close) => {
close();
}
},
{name: 'argo-icon-warning', color: 'warning'},
'yellow'
);
if (!confirmed) {
setPending(false);
return;
}
} else if (anyRequirePruning && !warnAgainstPruneAll) {
const confirmed = await ctx.popup.prompt(
'Prune resources?',
api => (
<div>
<p>{PRUNE_SOME_WARNING}</p>
<p>
<strong>Resources to be deleted ({resourcesToPrune.length}):</strong>
</p>
<ul style={{maxHeight: '200px', overflowY: 'auto', marginBottom: '1em'}}>
{resourcesToPrune.map(resource => (
<li key={nodeKey(resource)}>
{resource.kind}/{resource.name}
{resource.namespace && ` (${resource.namespace})`}
</li>
))}
</ul>
<div className='argo-form-row'>
<FormField
label="Please type 'prune' to confirm this action"
formApi={api}
field='confirmText'
qeId='prune-some-field-confirmation'
component={Text}
/>
</div>
</div>
),
{
validate: vals => ({
confirmText: vals.confirmText !== 'prune' && "Type 'prune' to confirm"
}),
submit: async (vals, _, close) => {
close();
}
},
{name: 'argo-icon-warning', color: 'warning'},
'yellow'
);
if (!confirmed) {
setPending(false);
return;
}
}
}
const replace = params.syncOptions?.findIndex((opt: string) => opt === 'Replace=true') > -1;
if (replace) {
const confirmed = await ctx.popup.prompt(
'Synchronize using replace?',
api => (
<div>
<div>{REPLACE_WARNING}</div>
<p>
Are you sure you want to <strong>delete and recreate {selectedResources?.length || 0} resources</strong>?
</p>
<div className='argo-form-row'>
<FormField
label="Please type 'replace' to confirm this action"
formApi={api}
field='confirmText'
qeId='replace-field-confirmation'
component={Text}
/>
</div>
</div>
),
{
validate: vals => ({
confirmText: vals.confirmText !== 'replace' && "Type 'replace' to confirm"
}),
submit: async (vals, _, close) => {
close();
}
},
{name: 'argo-icon-warning', color: 'warning'},
'yellow'
);
if (!confirmed) {
setPending(false);
return;
@@ -84,18 +203,6 @@ export const ApplicationSyncPanel = ({application, selectedResource, hide}: {app
if (allResourcesAreSelected) {
selectedResources = null;
}
const replace = params.syncOptions?.findIndex((opt: string) => opt === 'Replace=true') > -1;
if (replace) {
const confirmed = await ctx.popup.confirm('Synchronize using replace?', () => (
<div>
<i className='fa fa-exclamation-triangle' style={{color: ARGO_WARNING_COLOR}} /> {REPLACE_WARNING} Are you sure you want to continue?
</div>
));
if (!confirmed) {
setPending(false);
return;
}
}
const force = syncFlags.Force || false;

View File

@@ -2442,7 +2442,7 @@ arg@^4.1.0:
"argo-ui@git+https://github.com/argoproj/argo-ui.git":
version "1.0.0"
resolved "git+https://github.com/argoproj/argo-ui.git#5cf36101733ce43eed57242a12389f2a7e40bd2b"
resolved "git+https://github.com/argoproj/argo-ui.git#6d8ee8b016bf2e1fa81b646cf625f4d0887dd06a"
dependencies:
"@fortawesome/fontawesome-free" "^6.2.1"
"@tippy.js/react" "^3.1.1"
@@ -2450,6 +2450,7 @@ arg@^4.1.0:
core-js "^3.32.1"
foundation-sites "^6.4.3"
history "^4.10.1"
minimatch "5.1.6"
prop-types "^15.8.1"
react-autocomplete "1.8.1"
react-form "^2.16.0"
@@ -6638,6 +6639,13 @@ minimalistic-assert@^1.0.0:
resolved "https://registry.yarnpkg.com/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz#2e194de044626d4a10e7f7fbc00ce73e83e4d5c7"
integrity sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==
minimatch@5.1.6, minimatch@^5.0.1:
version "5.1.6"
resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-5.1.6.tgz#1cfcb8cf5522ea69952cd2af95ae09477f122a96"
integrity sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==
dependencies:
brace-expansion "^2.0.1"
minimatch@^3.0.4, minimatch@^3.1.2:
version "3.1.2"
resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.1.2.tgz#19cd194bfd3e428f049a70817c038d89ab4be35b"
@@ -6645,13 +6653,6 @@ minimatch@^3.0.4, minimatch@^3.1.2:
dependencies:
brace-expansion "^1.1.7"
minimatch@^5.0.1:
version "5.1.6"
resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-5.1.6.tgz#1cfcb8cf5522ea69952cd2af95ae09477f122a96"
integrity sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==
dependencies:
brace-expansion "^2.0.1"
minimatch@^9.0.4:
version "9.0.4"
resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-9.0.4.tgz#8e49c731d1749cbec05050ee5145147b32496a51"