argo-cd
251 строка · 15.4 Кб
1import {ErrorNotification, FormField, NotificationType, SlidingPanel, Tooltip} from 'argo-ui';
2import * as React from 'react';
3import {Form, FormApi, Text} from 'react-form';
4
5import {ARGO_WARNING_COLOR, CheckboxField, Spinner} from '../../../shared/components';
6import {Consumer} from '../../../shared/context';
7import * as models from '../../../shared/models';
8import {services} from '../../../shared/services';
9import {ApplicationRetryOptions} from '../application-retry-options/application-retry-options';
10import {
11ApplicationManualSyncFlags,
12ApplicationSyncOptions,
13FORCE_WARNING,
14SyncFlags,
15REPLACE_WARNING,
16PRUNE_ALL_WARNING
17} from '../application-sync-options/application-sync-options';
18import {ComparisonStatusIcon, getAppDefaultSource, nodeKey} from '../utils';
19
20import './application-sync-panel.scss';
21
22export const ApplicationSyncPanel = ({application, selectedResource, hide}: {application: models.Application; selectedResource: string; hide: () => any}) => {
23const [form, setForm] = React.useState<FormApi>(null);
24const isVisible = !!(selectedResource && application);
25const appResources = ((application && selectedResource && application.status && application.status.resources) || [])
26.sort((first, second) => nodeKey(first).localeCompare(nodeKey(second)))
27.filter(item => !item.hook);
28const syncResIndex = appResources.findIndex(item => nodeKey(item) === selectedResource);
29const syncStrategy = {} as models.SyncStrategy;
30const [isPending, setPending] = React.useState(false);
31const source = getAppDefaultSource(application);
32
33return (
34<Consumer>
35{ctx => (
36<SlidingPanel
37isMiddle={true}
38isShown={isVisible}
39onClose={() => hide()}
40header={
41<div>
42<button
43qe-id='application-sync-panel-button-synchronize'
44className='argo-button argo-button--base'
45disabled={isPending}
46onClick={() => form.submitForm(null)}>
47<Spinner show={isPending} style={{marginRight: '5px'}} />
48Synchronize
49</button>{' '}
50<button onClick={() => hide()} className='argo-button argo-button--base-o'>
51Cancel
52</button>
53</div>
54}>
55{isVisible && (
56<Form
57defaultValues={{
58revision: new URLSearchParams(ctx.history.location.search).get('revision') || source.targetRevision || 'HEAD',
59resources: appResources.map((_, i) => i === syncResIndex || syncResIndex === -1),
60syncOptions: application.spec.syncPolicy ? application.spec.syncPolicy.syncOptions : []
61}}
62validateError={values => ({
63resources: values.resources.every((item: boolean) => !item) && 'Select at least one resource'
64})}
65onSubmit={async (params: any) => {
66setPending(true);
67let selectedResources = appResources.filter((_, i) => params.resources[i]);
68const allResourcesAreSelected = selectedResources.length === appResources.length;
69const syncFlags = {...params.syncFlags} as SyncFlags;
70
71const allRequirePruning = !selectedResources.some(resource => !resource?.requiresPruning);
72if (syncFlags.Prune && allRequirePruning && allResourcesAreSelected) {
73const confirmed = await ctx.popup.confirm('Prune all resources?', () => (
74<div>
75<i className='fa fa-exclamation-triangle' style={{color: ARGO_WARNING_COLOR}} />
76{PRUNE_ALL_WARNING} Are you sure you want to continue?
77</div>
78));
79if (!confirmed) {
80setPending(false);
81return;
82}
83}
84if (allResourcesAreSelected) {
85selectedResources = null;
86}
87const replace = params.syncOptions?.findIndex((opt: string) => opt === 'Replace=true') > -1;
88if (replace) {
89const confirmed = await ctx.popup.confirm('Synchronize using replace?', () => (
90<div>
91<i className='fa fa-exclamation-triangle' style={{color: ARGO_WARNING_COLOR}} /> {REPLACE_WARNING} Are you sure you want to continue?
92</div>
93));
94if (!confirmed) {
95setPending(false);
96return;
97}
98}
99
100const force = syncFlags.Force || false;
101
102if (syncFlags.ApplyOnly) {
103syncStrategy.apply = {force};
104} else {
105syncStrategy.hook = {force};
106}
107if (force) {
108const confirmed = await ctx.popup.confirm('Synchronize with force?', () => (
109<div>
110<i className='fa fa-exclamation-triangle' style={{color: ARGO_WARNING_COLOR}} /> {FORCE_WARNING} Are you sure you want to continue?
111</div>
112));
113if (!confirmed) {
114setPending(false);
115return;
116}
117}
118
119try {
120await services.applications.sync(
121application.metadata.name,
122application.metadata.namespace,
123params.revision,
124syncFlags.Prune || false,
125syncFlags.DryRun || false,
126syncStrategy,
127selectedResources,
128params.syncOptions,
129params.retryStrategy
130);
131hide();
132} catch (e) {
133ctx.notifications.show({
134content: <ErrorNotification title='Unable to sync' e={e} />,
135type: NotificationType.Error
136});
137} finally {
138setPending(false);
139}
140}}
141getApi={setForm}>
142{formApi => (
143<form role='form' className='width-control' onSubmit={formApi.submitForm}>
144<h6>
145Synchronizing application manifests from <a href={source.repoURL}>{source.repoURL}</a>
146</h6>
147<div className='argo-form-row'>
148<FormField formApi={formApi} label='Revision' field='revision' component={Text} />
149</div>
150
151<div className='argo-form-row'>
152<div style={{marginBottom: '1em'}}>
153<FormField formApi={formApi} field='syncFlags' component={ApplicationManualSyncFlags} />
154</div>
155<div style={{marginBottom: '1em'}}>
156<label>Sync Options</label>
157<ApplicationSyncOptions
158options={formApi.values.syncOptions}
159onChanged={opts => {
160formApi.setTouched('syncOptions', true);
161formApi.setValue('syncOptions', opts);
162}}
163id='application-sync-panel'
164/>
165</div>
166
167<ApplicationRetryOptions
168id='application-sync-panel'
169formApi={formApi}
170initValues={application.spec.syncPolicy ? application.spec.syncPolicy.retry : null}
171/>
172
173<label>Synchronize resources:</label>
174<div style={{float: 'right'}}>
175<a
176onClick={() =>
177formApi.setValue(
178'resources',
179formApi.values.resources.map(() => true)
180)
181}>
182all
183</a>{' '}
184/{' '}
185<a
186onClick={() =>
187formApi.setValue(
188'resources',
189application.status.resources
190.filter(item => !item.hook)
191.map((resource: models.ResourceStatus) => resource.status === models.SyncStatuses.OutOfSync)
192)
193}>
194out of sync
195</a>{' '}
196/{' '}
197<a
198onClick={() =>
199formApi.setValue(
200'resources',
201formApi.values.resources.map(() => false)
202)
203}>
204none
205</a>
206</div>
207<div className='application-details__warning'>
208{!formApi.values.resources.every((item: boolean) => item) && <div>WARNING: partial synchronization is not recorded in history</div>}
209</div>
210<div>
211{application.status.resources
212.filter(item => !item.hook)
213.map((item, i) => {
214const resKey = nodeKey(item);
215const contentStart = resKey.substr(0, Math.floor(resKey.length / 2));
216let contentEnd = resKey.substr(-Math.floor(resKey.length / 2));
217// We want the ellipsis to be in the middle of our text, so we use RTL layout to put it there.
218// Unfortunately, strong LTR characters get jumbled around, so make sure that the last character isn't strong.
219const firstLetter = /[a-z]/i.exec(contentEnd);
220if (firstLetter) {
221contentEnd = contentEnd.slice(firstLetter.index);
222}
223const isLongLabel = resKey.length > 68;
224return (
225<div key={resKey} className='application-sync-panel__resource'>
226<CheckboxField id={resKey} field={`resources[${i}]`} />
227<Tooltip content={<div style={{wordBreak: 'break-all'}}>{resKey}</div>}>
228<div className='container'>
229{isLongLabel ? (
230<label htmlFor={resKey} content-start={contentStart} content-end={contentEnd} />
231) : (
232<label htmlFor={resKey}>{resKey}</label>
233)}
234</div>
235</Tooltip>
236<ComparisonStatusIcon status={item.status} resource={item} />
237</div>
238);
239})}
240{formApi.errors.resources && <div className='argo-form-row__error-msg'>{formApi.errors.resources}</div>}
241</div>
242</div>
243</form>
244)}
245</Form>
246)}
247</SlidingPanel>
248)}
249</Consumer>
250);
251};
252