github.com/argoproj/argo-cd/v3@v3.2.1/ui/src/app/applications/components/application-operation-state/application-operation-state.tsx (about) 1 import {Checkbox, DropDown, Duration, NotificationType, Ticker, HelpIcon, Tooltip} from 'argo-ui'; 2 import * as moment from 'moment'; 3 import * as PropTypes from 'prop-types'; 4 import * as React from 'react'; 5 6 import {ErrorNotification, Revision, Timestamp} from '../../../shared/components'; 7 import {AppContext} from '../../../shared/context'; 8 import * as models from '../../../shared/models'; 9 import {services} from '../../../shared/services'; 10 import * as utils from '../utils'; 11 12 import './application-operation-state.scss'; 13 14 interface Props { 15 application: models.Application; 16 operationState: models.OperationState; 17 } 18 const buildResourceUniqueId = (res: Omit<models.ResourceRef, 'uid'>) => `${res.group || ''}-${res.kind || ''}-${res.version || ''}-${res.namespace || ''}-${res.name}`; 19 const FilterableMessageStatuses = ['Changed', 'Unchanged']; 20 21 const Filter = (props: {filters: string[]; setFilters: (f: string[]) => void; options: string[]; title: string; style?: React.CSSProperties}) => { 22 const {filters, setFilters, options, title, style} = props; 23 return ( 24 <DropDown 25 isMenu={true} 26 anchor={() => ( 27 <div title='Filter' style={style}> 28 <button className='argo-button argo-button--base'> 29 {title} <i className='argo-icon-filter' aria-hidden='true' /> 30 </button> 31 </div> 32 )}> 33 {options.map(f => ( 34 <div key={f} style={{minWidth: '150px', lineHeight: '2em', padding: '5px'}}> 35 <Checkbox 36 checked={filters.includes(f)} 37 onChange={checked => { 38 const selectedValues = [...filters]; 39 const idx = selectedValues.indexOf(f); 40 if (idx > -1 && !checked) { 41 selectedValues.splice(idx, 1); 42 } else { 43 selectedValues.push(f); 44 } 45 setFilters(selectedValues); 46 }} 47 /> 48 <label htmlFor={`filter__${f}`}>{f}</label> 49 </div> 50 ))} 51 </DropDown> 52 ); 53 }; 54 55 export const ApplicationOperationState: React.StatelessComponent<Props> = ({application, operationState}, ctx: AppContext) => { 56 const [messageFilters, setMessageFilters] = React.useState([]); 57 58 const operationAttributes = [ 59 {title: 'OPERATION', value: utils.getOperationType(application)}, 60 {title: 'PHASE', value: operationState.phase}, 61 ...(operationState.message 62 ? [ 63 { 64 title: 'MESSAGE', 65 value: ( 66 <pre 67 style={{ 68 whiteSpace: 'pre-wrap', 69 wordBreak: 'break-word', 70 margin: 0, 71 fontFamily: 'inherit' 72 }}> 73 {utils.formatOperationMessage(operationState.message)} 74 </pre> 75 ) 76 } 77 ] 78 : []), 79 {title: 'STARTED AT', value: <Timestamp date={operationState.startedAt} />}, 80 { 81 title: 'DURATION', 82 value: ( 83 <Ticker> 84 {time => ( 85 <Duration durationS={((operationState.finishedAt && moment(operationState.finishedAt)) || moment(time)).diff(moment(operationState.startedAt)) / 1000} /> 86 )} 87 </Ticker> 88 ) 89 } 90 ]; 91 92 if (operationState.finishedAt && operationState.phase !== 'Running') { 93 operationAttributes.push({title: 'FINISHED AT', value: <Timestamp date={operationState.finishedAt} />}); 94 } else if (operationState.phase !== 'Terminating') { 95 operationAttributes.push({ 96 title: '', 97 value: ( 98 <button 99 className='argo-button argo-button--base' 100 onClick={async () => { 101 const confirmed = await ctx.apis.popup.confirm('Terminate operation', 'Are you sure you want to terminate operation?'); 102 if (confirmed) { 103 try { 104 await services.applications.terminateOperation(application.metadata.name, application.metadata.namespace); 105 } catch (e) { 106 ctx.apis.notifications.show({ 107 content: <ErrorNotification title='Unable to terminate operation' e={e} />, 108 type: NotificationType.Error 109 }); 110 } 111 } 112 }}> 113 Terminate 114 </button> 115 ) 116 }); 117 } 118 if (operationState.syncResult) { 119 operationAttributes.push({ 120 title: 'REVISION', 121 value: ( 122 <div> 123 <Revision repoUrl={utils.getAppDefaultSource(application).repoURL} revision={utils.getAppDefaultOperationSyncRevision(application)} /> 124 {utils.getAppDefaultOperationSyncRevisionExtra(application)} 125 </div> 126 ) 127 }); 128 } 129 let initiator = ''; 130 if (operationState.operation.initiatedBy) { 131 if (operationState.operation.initiatedBy.automated) { 132 initiator = 'automated sync policy'; 133 } else { 134 initiator = operationState.operation.initiatedBy.username; 135 } 136 } 137 operationAttributes.push({title: 'INITIATED BY', value: initiator || 'Unknown'}); 138 139 const resultAttributes: {title: string; value: string}[] = []; 140 const syncResult = operationState.syncResult; 141 if (operationState.finishedAt) { 142 if (syncResult) { 143 (syncResult.resources || []).forEach(res => { 144 resultAttributes.push({ 145 title: `${res.namespace}/${res.kind}:${res.name}`, 146 value: res.message 147 }); 148 }); 149 } 150 } 151 const [filters, setFilters] = React.useState([]); 152 const [healthFilters, setHealthFilters] = React.useState([]); 153 154 const Healths = Object.keys(models.HealthStatuses); 155 const Statuses = Object.keys(models.ResultCodes); 156 const OperationPhases = Object.keys(models.OperationPhases); 157 // const syncPhases = ['PreSync', 'Sync', 'PostSync', 'SyncFail']; 158 // const hookPhases = ['Running', 'Terminating', 'Failed', 'Error', 'Succeeded']; 159 const resourceHealth = application.status.resources.reduce( 160 (acc, res) => { 161 acc[buildResourceUniqueId(res)] = { 162 health: res.health, 163 syncWave: res.syncWave 164 }; 165 166 return acc; 167 }, 168 {} as Record< 169 string, 170 { 171 health: models.HealthStatus; 172 syncWave: number; 173 } 174 > 175 ); 176 177 const combinedHealthSyncResult: models.SyncResourceResult[] = syncResult?.resources?.map(syncResultItem => { 178 const uniqueResourceName = buildResourceUniqueId(syncResultItem); 179 180 const healthStatus = resourceHealth[uniqueResourceName]; 181 182 const syncResultWithHealth: models.SyncResourceResult = { 183 ...syncResultItem 184 }; 185 186 if (healthStatus?.health) { 187 syncResultWithHealth.health = healthStatus.health; 188 } 189 190 syncResultWithHealth.syncWave = healthStatus?.syncWave; 191 192 return syncResultWithHealth; 193 }); 194 let filtered: models.SyncResourceResult[] = []; 195 196 if (combinedHealthSyncResult && combinedHealthSyncResult.length > 0) { 197 filtered = combinedHealthSyncResult.filter(r => { 198 if (filters.length === 0 && healthFilters.length === 0 && messageFilters.length === 0) { 199 return true; 200 } 201 202 let pass = true; 203 if (filters.length !== 0 && !filters.includes(getStatus(r))) { 204 pass = false; 205 } 206 207 if (pass && healthFilters.length !== 0 && !healthFilters.includes(r.health?.status)) { 208 pass = false; 209 } 210 211 if (pass && messageFilters.length !== 0) { 212 pass = messageFilters.some(filter => { 213 if (filter === 'Changed') { 214 return r.message?.toLowerCase().includes('configured'); 215 } 216 return r.message?.toLowerCase().includes(filter.toLowerCase()); 217 }); 218 } 219 220 return pass; 221 }); 222 } 223 224 return ( 225 <div> 226 <div className='white-box'> 227 <div className='white-box__details'> 228 {operationAttributes.map(attr => ( 229 <div className='row white-box__details-row' key={attr.title}> 230 <div className='columns small-3'>{attr.title}</div> 231 <div className='columns small-9'>{attr.value}</div> 232 </div> 233 ))} 234 </div> 235 </div> 236 {syncResult && syncResult.resources && syncResult.resources.length > 0 && ( 237 <React.Fragment> 238 <div style={{display: 'flex'}}> 239 <label style={{display: 'block', marginBottom: '1em'}}>RESULT</label> 240 <div style={{marginLeft: 'auto'}}> 241 <Filter options={Healths} filters={healthFilters} setFilters={setHealthFilters} title='HEALTH' style={{marginRight: '5px'}} /> 242 <Filter options={Statuses} filters={filters} setFilters={setFilters} title='STATUS' style={{marginRight: '5px'}} /> 243 <Filter options={OperationPhases} filters={filters} setFilters={setFilters} title='HOOK' /> 244 <Tooltip placement='top-start' content='Filter on resources that have changed or remained unchanged'> 245 <div style={{display: 'inline-block'}}> 246 <Filter options={FilterableMessageStatuses} filters={messageFilters} setFilters={setMessageFilters} title='MESSAGE' /> 247 </div> 248 </Tooltip> 249 </div> 250 </div> 251 <div className='argo-table-list'> 252 <div className='argo-table-list__head'> 253 <div className='row'> 254 <div className='columns large-1 show-for-large application-operation-state__icons_container_padding'>SYNC WAVE</div> 255 <div className='columns large-1 show-for-large application-operation-state__icons_container_padding'>KIND</div> 256 <div className='columns large-1 show-for-large'>NAMESPACE</div> 257 <div className='columns large-2 small-2'>NAME</div> 258 <div className='columns large-1 small-2'>STATUS</div> 259 <div className='columns large-1 small-2'>HEALTH</div> 260 <div className='columns large-1 show-for-large'>HOOK</div> 261 <div className='columns large-3 small-4'>MESSAGE</div> 262 <div className='columns large-1 small-2'>IMAGES</div> 263 </div> 264 </div> 265 {filtered.length > 0 ? ( 266 filtered.map((resource, i) => ( 267 <div className='argo-table-list__row' key={i}> 268 <div className='row'> 269 <div className='columns large-1 show-for-large application-operation-state__icons_container_padding' style={{textAlign: 'center'}}> 270 <div className='application-operation-state__icons_container'> 271 {resource.hookType && <i title='Resource lifecycle hook' className='fa fa-anchor' />} 272 </div> 273 {resource.syncWave || '0'} 274 </div> 275 <div className='columns large-1 show-for-large'> 276 <span title={getKind(resource)}>{getKind(resource)}</span> 277 </div> 278 <div className='columns large-1 show-for-large' title={resource.namespace}> 279 {resource.namespace} 280 </div> 281 <div className='columns large-2 small-2' title={resource.name}> 282 {resource.name} 283 </div> 284 <div className='columns large-1 small-2' title={getStatus(resource)}> 285 <utils.ResourceResultIcon resource={resource} /> {getStatus(resource)} 286 </div> 287 <div className='columns large-1 small-2'> 288 {resource.health ? ( 289 <div> 290 <utils.HealthStatusIcon state={resource?.health} /> {resource.health?.status} 291 {resource.health.message && <HelpIcon title={resource.health.message} />} 292 </div> 293 ) : ( 294 <>{'-'}</> 295 )} 296 </div> 297 <div className='columns large-1 show-for-large' title={resource.hookType}> 298 {resource.hookType} 299 </div> 300 <div className='columns large-3 small-4' title={resource.message}> 301 <div className='application-operation-state__message'>{resource.message}</div> 302 </div> 303 <div className='columns large-1 small-2'> 304 {resource.images && resource.images.length > 0 ? ( 305 <Tooltip 306 placement='top' 307 content={ 308 <div> 309 <ul className='application-operation-state__images-list' style={{margin: '10px'}}> 310 {resource.images.map((image, idx) => ( 311 <li key={idx}>{image}</li> 312 ))} 313 </ul> 314 </div> 315 }> 316 <span className='application-operation-state__images-count'> 317 {resource.images.length} image{resource.images.length !== 1 ? 's' : ''} 318 </span> 319 </Tooltip> 320 ) : ( 321 '-' 322 )} 323 </div> 324 </div> 325 </div> 326 )) 327 ) : ( 328 <div style={{textAlign: 'center', marginTop: '2em', fontSize: '20px'}}>No Sync Results match filter</div> 329 )} 330 </div> 331 </React.Fragment> 332 )} 333 </div> 334 ); 335 }; 336 337 const getKind = (resource: models.ResourceResult): string => { 338 return (resource.group ? `${resource.group}/${resource.version}` : resource.version) + `/${resource.kind}`; 339 }; 340 341 const getStatus = (resource: models.ResourceResult): string => { 342 return resource.hookType ? resource.hookPhase : resource.status; 343 }; 344 345 ApplicationOperationState.contextTypes = { 346 apis: PropTypes.object 347 };