Skip to content

Commit

Permalink
fix(ui): extract common code form terminate sync and sync panel
Browse files Browse the repository at this point in the history
Signed-off-by: linghaoSu <[email protected]>
  • Loading branch information
linghaoSu committed Nov 27, 2024
1 parent ab6123e commit d0a985f
Show file tree
Hide file tree
Showing 4 changed files with 262 additions and 257 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
import {FormField, NotificationType, SlidingPanel} from 'argo-ui';
import * as React from 'react';
import {Form, FormApi} from 'react-form';
import {ProgressPopup, Spinner} from '../../../shared/components';
import {Consumer, ContextApis} from '../../../shared/context';
import * as models from '../../../shared/models';
import {ApplicationRetryOptions} from '../application-retry-options/application-retry-options';
import {ApplicationManualSyncFlags, ApplicationSyncOptions, SyncFlags} from '../application-sync-options/application-sync-options';
import {ApplicationSelector} from '../../../shared/components';

interface Progress {
percentage: number;
title: string;
}

interface OperationPanelProps {
title: string;
buttonTitle: string;
show: boolean;
apps: models.Application[];
hide: () => void;
onSubmit: (
apps: models.Application[],
syncFlags: SyncFlags,
syncOptions: string[],
retryStrategy: models.RetryStrategy,
updateProgress: (newProgress: Progress) => void,
ctx: ContextApis
) => Promise<void>;
additionalFormFields?: React.ReactNode;
showOptions?: boolean;
selectionOptions?: {all: boolean; syncing?: boolean; outOfSync?: boolean; none: boolean};
}

export const ApplicationsOperationPanel: React.FC<OperationPanelProps> = ({
title,
buttonTitle,
show,
apps,
hide,
onSubmit,
additionalFormFields,
showOptions = true,
selectionOptions = {all: true, none: true}
}) => {
const [form, setForm] = React.useState<FormApi>(null);
const [progress, setProgress] = React.useState<Progress>(null);
const [isPending, setPending] = React.useState(false);

const validApps = React.useMemo(() => apps?.filter(app => app != null) || [], [apps]);

const getSelectedApps = (params: any) => validApps.filter((_, i) => params['app/' + i]);

const updateProgress = (newProgress: Progress) => {
const clampedPercentage = Math.min(100, Math.max(0, newProgress.percentage));
setProgress({
...newProgress,
percentage: clampedPercentage
});
};

const handleSubmit = async (params: any, ctx: ContextApis) => {
setPending(true);
const selectedApps = getSelectedApps(params);

if (selectedApps.length === 0) {
ctx.notifications.show({content: `No apps selected`, type: NotificationType.Error});
setPending(false);
return;
}

try {
await onSubmit(selectedApps, params.syncFlags || {}, params.syncOptions || [], params.retryStrategy, updateProgress, ctx);
} catch (e) {
console.error(e);
} finally {
setPending(false);
}
};

return (
<Consumer>
{ctx => (
<SlidingPanel
isMiddle={true}
isShown={show}
onClose={() => {
setProgress(null);
hide();
}}
header={
<div>
<button className='argo-button argo-button--base' disabled={isPending} onClick={() => form.submitForm(null)}>
<Spinner show={isPending} style={{marginRight: '5px'}} />
{buttonTitle}
</button>{' '}
<button onClick={() => hide()} className='argo-button argo-button--base-o'>
Cancel
</button>
</div>
}>
<Form defaultValues={{syncFlags: [], syncOptions: []}} onSubmit={params => handleSubmit(params, ctx)} getApi={setForm}>
{formApi => (
<div className='argo-form-row' style={{marginTop: 0}}>
<h4>{title}</h4>
{progress !== null && <ProgressPopup onClose={() => setProgress(null)} percentage={progress.percentage} title={progress.title} />}
{showOptions && (
<>
<div style={{marginBottom: '1em'}}>
<FormField formApi={formApi} field='syncFlags' component={ApplicationManualSyncFlags} />
</div>
<div style={{marginBottom: '1em'}}>
<label>Sync Options</label>
<ApplicationSyncOptions
options={formApi.values.syncOptions || []}
onChanged={opts => {
formApi.setTouched('syncOptions', true);
formApi.setValue('syncOptions', opts);
}}
id='applications-operation-panel'
/>
</div>
<ApplicationRetryOptions id='applications-operation-panel' formApi={formApi} />
</>
)}
{additionalFormFields}
<ApplicationSelector apps={validApps} formApi={formApi} selectionOptions={selectionOptions} />
</div>
)}
</Form>
</SlidingPanel>
)}
</Consumer>
);
};
Original file line number Diff line number Diff line change
@@ -1,159 +1,66 @@
import {ErrorNotification, FormField, NotificationType, SlidingPanel} from 'argo-ui';
import * as React from 'react';
import {Form, FormApi} from 'react-form';
import {ARGO_WARNING_COLOR, ProgressPopup, Spinner} from '../../../shared/components';
import {Consumer, ContextApis} from '../../../shared/context';
import * as models from '../../../shared/models';
import {services} from '../../../shared/services';
import {ApplicationRetryOptions} from '../application-retry-options/application-retry-options';
import {ApplicationManualSyncFlags, ApplicationSyncOptions, FORCE_WARNING, SyncFlags} from '../application-sync-options/application-sync-options';
import {ApplicationSelector} from '../../../shared/components';
import {confirmSyncingAppOfApps, getAppDefaultSource} from '../utils';

interface Progress {
percentage: number;
title: string;
}
import {ApplicationsOperationPanel} from '../applications-operation-panel/applications-operation-panel';
import {getAppDefaultSource} from '../utils';
import {SyncFlags} from '../application-sync-options/application-sync-options';
import {ContextApis} from '../../../shared/context';
import {ErrorNotification, NotificationType} from 'argo-ui';

export const ApplicationsSyncPanel = ({show, apps, hide}: {show: boolean; apps: models.Application[]; hide: () => void}) => {
const [form, setForm] = React.useState<FormApi>(null);
const [progress, setProgress] = React.useState<Progress>(null);
const getSelectedApps = (params: any) => apps.filter((_, i) => params['app/' + i]);
const [isPending, setPending] = React.useState(false);
const syncHandler = (currentForm: FormApi, ctx: ContextApis, applications: models.Application[]) => {
const formValues = currentForm.getFormState().values;
const replaceChecked = formValues.syncOptions?.includes('Replace=true');
const selectedApps = [];
const selectedAppOfApps: models.Application[] = [];
let containAppOfApps = false;
const onSubmit = async (
selectedApps: models.Application[],
syncFlags: SyncFlags,
syncOptions: string[],
retryStrategy: models.RetryStrategy,
setProgress: (progress: {percentage: number; title: string}) => void,
ctx: ContextApis
) => {
const force = syncFlags.Force || false;
const syncStrategy: models.SyncStrategy = syncFlags.ApplyOnly ? {apply: {force}} : {hook: {force}};

for (const key in formValues) {
if (key.startsWith('app/') && formValues[key]) {
selectedApps.push(applications[parseInt(key.slice(key.lastIndexOf('/') + 1), 10)]);
}
}

selectedApps.forEach(app => {
if (app.isAppOfAppsPattern) {
containAppOfApps = true;
selectedAppOfApps.push(app);
}
});
setProgress({percentage: 0, title: 'Starting...'});
let completed = 0;
const total = selectedApps.length;

if (replaceChecked && containAppOfApps) {
confirmSyncingAppOfApps(selectedAppOfApps, ctx, currentForm).then(confirmed => {
setPending(confirmed ? true : false);
for (const app of selectedApps) {
setProgress({
percentage: completed / total,
title: `Syncing ${app.metadata.name} (${completed + 1}/${total})`
});
} else {
currentForm.submitForm(null);

try {
await services.applications.sync(
app.metadata.name,
app.metadata.namespace,
getAppDefaultSource(app).targetRevision,
syncFlags.Prune || false,
syncFlags.DryRun || false,
syncStrategy,
null,
syncOptions,
retryStrategy
);
completed++;
setProgress({
percentage: completed / total,
title: `Completed ${completed}/${total} applications`
});
} catch (e) {
setProgress({
percentage: completed / total,
title: `Failed to sync ${app.metadata.name} (${completed}/${total})`
});
ctx.notifications.show({
content: <ErrorNotification title={`Unable to sync ${app.metadata.name}`} e={e} />,
type: NotificationType.Error
});
}
}
setProgress({percentage: 100, title: `Successfully synced ${completed} applications`});
};
return (
<Consumer>
{ctx => (
<SlidingPanel
isMiddle={true}
isShown={show}
onClose={() => hide()}
header={
<div>
<button className='argo-button argo-button--base' disabled={isPending} onClick={() => syncHandler(form, ctx, apps)}>
<Spinner show={isPending} style={{marginRight: '5px'}} />
Sync
</button>{' '}
<button onClick={() => hide()} className='argo-button argo-button--base-o'>
Cancel
</button>
</div>
}>
<Form
defaultValues={{syncFlags: []}}
onSubmit={async (params: any) => {
setPending(true);
const selectedApps = getSelectedApps(params);
const syncFlags = {...params.syncFlags} as SyncFlags;
const force = syncFlags.Force || false;
if (force) {
const confirmed = await ctx.popup.confirm('Synchronize with force?', () => (
<div>
<i className='fa fa-exclamation-triangle' style={{color: ARGO_WARNING_COLOR}} /> {FORCE_WARNING} Are you sure you want to continue?
</div>
));
if (!confirmed) {
setPending(false);
return;
}
}
if (selectedApps.length === 0) {
ctx.notifications.show({content: `No apps selected`, type: NotificationType.Error});
setPending(false);
return;
}

const syncStrategy: models.SyncStrategy = syncFlags.ApplyOnly || false ? {apply: {force}} : {hook: {force}};

setProgress({percentage: 0, title: 'Starting...'});
let i = 0;
for (const app of selectedApps) {
await services.applications
.sync(
app.metadata.name,
app.metadata.namespace,
getAppDefaultSource(app).targetRevision,
syncFlags.Prune || false,
syncFlags.DryRun || false,
syncStrategy,
null,
params.syncOptions,
params.retryStrategy
)
.catch(e => {
ctx.notifications.show({
content: <ErrorNotification title={`Unable to sync ${app.metadata.name}`} e={e} />,
type: NotificationType.Error
});
})
.finally(() => {
setPending(false);
});
i++;
setProgress({
percentage: i / selectedApps.length,
title: `${i} of ${selectedApps.length} apps now syncing`
});
}
setProgress({percentage: 100, title: 'Complete'});
}}
getApi={setForm}>
{formApi => (
<React.Fragment>
<div className='argo-form-row' style={{marginTop: 0}}>
<h4>Sync app(s)</h4>
{progress !== null && <ProgressPopup onClose={() => setProgress(null)} percentage={progress.percentage} title={progress.title} />}
<div style={{marginBottom: '1em'}}>
<FormField formApi={formApi} field='syncFlags' component={ApplicationManualSyncFlags} />
</div>
<div style={{marginBottom: '1em'}}>
<label>Sync Options</label>
<ApplicationSyncOptions
options={formApi.values.syncOptions}
onChanged={opts => {
formApi.setTouched('syncOptions', true);
formApi.setValue('syncOptions', opts);
}}
id='applications-sync-panel'
/>
</div>

<ApplicationRetryOptions id='applications-sync-panel' formApi={formApi} />
const validApps = React.useMemo(() => apps?.filter(app => app != null) || [], [apps]);

<ApplicationSelector apps={apps} formApi={formApi} />
</div>
</React.Fragment>
)}
</Form>
</SlidingPanel>
)}
</Consumer>
);
return <ApplicationsOperationPanel title='Sync Applications' buttonTitle='Sync' show={show} apps={validApps} hide={hide} onSubmit={onSubmit} showOptions={true} />;
};
Loading

0 comments on commit d0a985f

Please sign in to comment.