forked from argoproj/argo-cd
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
fix(ui): extract common code form terminate sync and sync panel
Signed-off-by: linghaoSu <[email protected]>
- Loading branch information
Showing
4 changed files
with
262 additions
and
257 deletions.
There are no files selected for viewing
135 changes: 135 additions & 0 deletions
135
...app/applications/components/applications-operation-panel/applications-operation-panel.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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> | ||
); | ||
}; |
199 changes: 53 additions & 146 deletions
199
ui/src/app/applications/components/applications-sync-panel/applications-sync-panel.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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} />; | ||
}; |
Oops, something went wrong.