github.com/argoproj/argo-cd/v2@v2.10.9/ui/src/app/applications/components/application-sync-panel/application-sync-panel.tsx (about) 1 import {ErrorNotification, FormField, NotificationType, SlidingPanel, Tooltip} from 'argo-ui'; 2 import * as React from 'react'; 3 import {Form, FormApi, Text} from 'react-form'; 4 5 import {ARGO_WARNING_COLOR, CheckboxField, Spinner} from '../../../shared/components'; 6 import {Consumer} from '../../../shared/context'; 7 import * as models from '../../../shared/models'; 8 import {services} from '../../../shared/services'; 9 import {ApplicationRetryOptions} from '../application-retry-options/application-retry-options'; 10 import { 11 ApplicationManualSyncFlags, 12 ApplicationSyncOptions, 13 FORCE_WARNING, 14 SyncFlags, 15 REPLACE_WARNING, 16 PRUNE_ALL_WARNING 17 } from '../application-sync-options/application-sync-options'; 18 import {ComparisonStatusIcon, getAppDefaultSource, nodeKey} from '../utils'; 19 20 import './application-sync-panel.scss'; 21 22 export const ApplicationSyncPanel = ({application, selectedResource, hide}: {application: models.Application; selectedResource: string; hide: () => any}) => { 23 const [form, setForm] = React.useState<FormApi>(null); 24 const isVisible = !!(selectedResource && application); 25 const appResources = ((application && selectedResource && application.status && application.status.resources) || []) 26 .sort((first, second) => nodeKey(first).localeCompare(nodeKey(second))) 27 .filter(item => !item.hook); 28 const syncResIndex = appResources.findIndex(item => nodeKey(item) === selectedResource); 29 const syncStrategy = {} as models.SyncStrategy; 30 const [isPending, setPending] = React.useState(false); 31 const source = getAppDefaultSource(application); 32 33 return ( 34 <Consumer> 35 {ctx => ( 36 <SlidingPanel 37 isMiddle={true} 38 isShown={isVisible} 39 onClose={() => hide()} 40 header={ 41 <div> 42 <button 43 qe-id='application-sync-panel-button-synchronize' 44 className='argo-button argo-button--base' 45 disabled={isPending} 46 onClick={() => form.submitForm(null)}> 47 <Spinner show={isPending} style={{marginRight: '5px'}} /> 48 Synchronize 49 </button>{' '} 50 <button onClick={() => hide()} className='argo-button argo-button--base-o'> 51 Cancel 52 </button> 53 </div> 54 }> 55 {isVisible && ( 56 <Form 57 defaultValues={{ 58 revision: new URLSearchParams(ctx.history.location.search).get('revision') || source.targetRevision || 'HEAD', 59 resources: appResources.map((_, i) => i === syncResIndex || syncResIndex === -1), 60 syncOptions: application.spec.syncPolicy ? application.spec.syncPolicy.syncOptions : [] 61 }} 62 validateError={values => ({ 63 resources: values.resources.every((item: boolean) => !item) && 'Select at least one resource' 64 })} 65 onSubmit={async (params: any) => { 66 setPending(true); 67 let selectedResources = appResources.filter((_, i) => params.resources[i]); 68 const allResourcesAreSelected = selectedResources.length === appResources.length; 69 const syncFlags = {...params.syncFlags} as SyncFlags; 70 71 const allRequirePruning = !selectedResources.some(resource => !resource?.requiresPruning); 72 if (syncFlags.Prune && allRequirePruning && allResourcesAreSelected) { 73 const 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 )); 79 if (!confirmed) { 80 setPending(false); 81 return; 82 } 83 } 84 if (allResourcesAreSelected) { 85 selectedResources = null; 86 } 87 const replace = params.syncOptions?.findIndex((opt: string) => opt === 'Replace=true') > -1; 88 if (replace) { 89 const 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 )); 94 if (!confirmed) { 95 setPending(false); 96 return; 97 } 98 } 99 100 const force = syncFlags.Force || false; 101 102 if (syncFlags.ApplyOnly) { 103 syncStrategy.apply = {force}; 104 } else { 105 syncStrategy.hook = {force}; 106 } 107 if (force) { 108 const 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 )); 113 if (!confirmed) { 114 setPending(false); 115 return; 116 } 117 } 118 119 try { 120 await services.applications.sync( 121 application.metadata.name, 122 application.metadata.namespace, 123 params.revision, 124 syncFlags.Prune || false, 125 syncFlags.DryRun || false, 126 syncStrategy, 127 selectedResources, 128 params.syncOptions, 129 params.retryStrategy 130 ); 131 hide(); 132 } catch (e) { 133 ctx.notifications.show({ 134 content: <ErrorNotification title='Unable to sync' e={e} />, 135 type: NotificationType.Error 136 }); 137 } finally { 138 setPending(false); 139 } 140 }} 141 getApi={setForm}> 142 {formApi => ( 143 <form role='form' className='width-control' onSubmit={formApi.submitForm}> 144 <h6> 145 Synchronizing 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 158 options={formApi.values.syncOptions} 159 onChanged={opts => { 160 formApi.setTouched('syncOptions', true); 161 formApi.setValue('syncOptions', opts); 162 }} 163 id='application-sync-panel' 164 /> 165 </div> 166 167 <ApplicationRetryOptions 168 id='application-sync-panel' 169 formApi={formApi} 170 initValues={application.spec.syncPolicy ? application.spec.syncPolicy.retry : null} 171 /> 172 173 <label>Synchronize resources:</label> 174 <div style={{float: 'right'}}> 175 <a 176 onClick={() => 177 formApi.setValue( 178 'resources', 179 formApi.values.resources.map(() => true) 180 ) 181 }> 182 all 183 </a>{' '} 184 /{' '} 185 <a 186 onClick={() => 187 formApi.setValue( 188 'resources', 189 application.status.resources 190 .filter(item => !item.hook) 191 .map((resource: models.ResourceStatus) => resource.status === models.SyncStatuses.OutOfSync) 192 ) 193 }> 194 out of sync 195 </a>{' '} 196 /{' '} 197 <a 198 onClick={() => 199 formApi.setValue( 200 'resources', 201 formApi.values.resources.map(() => false) 202 ) 203 }> 204 none 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) => { 214 const resKey = nodeKey(item); 215 const contentStart = resKey.substr(0, Math.floor(resKey.length / 2)); 216 let 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. 219 const firstLetter = /[a-z]/i.exec(contentEnd); 220 if (firstLetter) { 221 contentEnd = contentEnd.slice(firstLetter.index); 222 } 223 const isLongLabel = resKey.length > 68; 224 return ( 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 };