github.com/argoproj/argo-cd@v1.8.7/ui/src/app/applications/components/utils.tsx (about) 1 import {Checkbox, NotificationType} from 'argo-ui'; 2 import * as React from 'react'; 3 import {Observable, Observer, Subscription} from 'rxjs'; 4 5 import {COLORS, ErrorNotification, Revision} from '../../shared/components'; 6 import {ContextApis} from '../../shared/context'; 7 import * as appModels from '../../shared/models'; 8 import {services} from '../../shared/services'; 9 10 export interface NodeId { 11 kind: string; 12 namespace: string; 13 name: string; 14 group: string; 15 } 16 17 export function nodeKey(node: NodeId) { 18 return [node.group, node.kind, node.namespace, node.name].join('/'); 19 } 20 21 export function isSameNode(first: NodeId, second: NodeId) { 22 return nodeKey(first) === nodeKey(second); 23 } 24 25 export async function deleteApplication(appName: string, apis: ContextApis): Promise<boolean> { 26 let cascade = false; 27 const confirmationForm = class extends React.Component<{}, {cascade: boolean}> { 28 constructor(props: any) { 29 super(props); 30 this.state = {cascade: true}; 31 } 32 33 public render() { 34 return ( 35 <div> 36 <p>Are you sure you want to delete the application '{appName}'?</p> 37 <p> 38 <Checkbox checked={this.state.cascade} onChange={val => this.setState({cascade: val})} /> Cascade 39 </p> 40 </div> 41 ); 42 } 43 44 public componentWillUnmount() { 45 cascade = this.state.cascade; 46 } 47 }; 48 const confirmed = await apis.popup.confirm('Delete application', confirmationForm); 49 if (confirmed) { 50 try { 51 await services.applications.delete(appName, cascade); 52 return true; 53 } catch (e) { 54 apis.notifications.show({ 55 content: <ErrorNotification title='Unable to delete application' e={e} />, 56 type: NotificationType.Error 57 }); 58 } 59 } 60 return false; 61 } 62 63 export const OperationPhaseIcon = ({app}: {app: appModels.Application}) => { 64 const operationState = getAppOperationState(app); 65 if (operationState === undefined) { 66 return <React.Fragment />; 67 } 68 let className = ''; 69 let color = ''; 70 switch (operationState.phase) { 71 case appModels.OperationPhases.Succeeded: 72 className = 'fa fa-check-circle'; 73 color = COLORS.operation.success; 74 break; 75 case appModels.OperationPhases.Error: 76 className = 'fa fa-times-circle'; 77 color = COLORS.operation.error; 78 break; 79 case appModels.OperationPhases.Failed: 80 className = 'fa fa-times-circle'; 81 color = COLORS.operation.failed; 82 break; 83 default: 84 className = 'fa fa-circle-notch fa-spin'; 85 color = COLORS.operation.running; 86 break; 87 } 88 return <i title={getOperationStateTitle(app)} qe-id='utils-operations-status-title' className={className} style={{color}} />; 89 }; 90 91 export const ComparisonStatusIcon = ({status, resource, label}: {status: appModels.SyncStatusCode; resource?: {requiresPruning?: boolean}; label?: boolean}) => { 92 let className = 'fa fa-ghost'; 93 let color = COLORS.sync.unknown; 94 let title: string = 'Unknown'; 95 96 switch (status) { 97 case appModels.SyncStatuses.Synced: 98 className = 'fa fa-check-circle'; 99 color = COLORS.sync.synced; 100 title = 'Synced'; 101 break; 102 case appModels.SyncStatuses.OutOfSync: 103 const requiresPruning = resource && resource.requiresPruning; 104 className = requiresPruning ? 'fa fa-times-circle' : 'fa fa-arrow-alt-circle-up'; 105 title = 'OutOfSync'; 106 if (requiresPruning) { 107 title = `${title} (requires pruning)`; 108 } 109 color = COLORS.sync.out_of_sync; 110 break; 111 case appModels.SyncStatuses.Unknown: 112 className = 'fa fa-circle-notch fa-spin'; 113 break; 114 } 115 return ( 116 <React.Fragment> 117 <i qe-id='utils-sync-status-title' title={title} className={className} style={{color}} /> {label && title} 118 </React.Fragment> 119 ); 120 }; 121 122 export function syncStatusMessage(app: appModels.Application) { 123 const rev = app.status.sync.revision || app.spec.source.targetRevision || 'HEAD'; 124 let message = app.spec.source.targetRevision || 'HEAD'; 125 if (app.status.sync.revision) { 126 if (app.spec.source.chart) { 127 message += ' (' + app.status.sync.revision + ')'; 128 } else if (app.status.sync.revision.length >= 7 && !app.status.sync.revision.startsWith(app.spec.source.targetRevision)) { 129 message += ' (' + app.status.sync.revision.substr(0, 7) + ')'; 130 } 131 } 132 switch (app.status.sync.status) { 133 case appModels.SyncStatuses.Synced: 134 return ( 135 <span> 136 To{' '} 137 <Revision repoUrl={app.spec.source.repoURL} revision={rev}> 138 {message} 139 </Revision>{' '} 140 </span> 141 ); 142 case appModels.SyncStatuses.OutOfSync: 143 return ( 144 <span> 145 From{' '} 146 <Revision repoUrl={app.spec.source.repoURL} revision={rev}> 147 {message} 148 </Revision>{' '} 149 </span> 150 ); 151 default: 152 return <span>{message}</span>; 153 } 154 } 155 156 export const HealthStatusIcon = ({state}: {state: appModels.HealthStatus}) => { 157 let color = COLORS.health.unknown; 158 let icon = 'fa-ghost'; 159 160 switch (state.status) { 161 case appModels.HealthStatuses.Healthy: 162 color = COLORS.health.healthy; 163 icon = 'fa-heart'; 164 break; 165 case appModels.HealthStatuses.Suspended: 166 color = COLORS.health.suspended; 167 icon = 'fa-heart'; 168 break; 169 case appModels.HealthStatuses.Degraded: 170 color = COLORS.health.degraded; 171 icon = 'fa-heart-broken'; 172 break; 173 case appModels.HealthStatuses.Progressing: 174 color = COLORS.health.progressing; 175 icon = 'fa fa-circle-notch fa-spin'; 176 break; 177 } 178 let title: string = state.status; 179 if (state.message) { 180 title = `${state.status}: ${state.message};`; 181 } 182 return <i qe-id='utils-health-status-title' title={title} className={'fa ' + icon} style={{color}} />; 183 }; 184 185 export const ResourceResultIcon = ({resource}: {resource: appModels.ResourceResult}) => { 186 let color = COLORS.sync_result.unknown; 187 let icon = 'fa-ghost'; 188 189 if (!resource.hookType && resource.status) { 190 switch (resource.status) { 191 case appModels.ResultCodes.Synced: 192 color = COLORS.sync_result.synced; 193 icon = 'fa-heart'; 194 break; 195 case appModels.ResultCodes.Pruned: 196 color = COLORS.sync_result.pruned; 197 icon = 'fa-heart'; 198 break; 199 case appModels.ResultCodes.SyncFailed: 200 color = COLORS.sync_result.failed; 201 icon = 'fa-heart-broken'; 202 break; 203 case appModels.ResultCodes.PruneSkipped: 204 icon = 'fa-heart'; 205 break; 206 } 207 let title: string = resource.message; 208 if (resource.message) { 209 title = `${resource.status}: ${resource.message}`; 210 } 211 return <i title={title} className={'fa ' + icon} style={{color}} />; 212 } 213 if (resource.hookType && resource.hookPhase) { 214 let className = ''; 215 switch (resource.hookPhase) { 216 case appModels.OperationPhases.Running: 217 color = COLORS.operation.running; 218 className = 'fa fa-circle-notch fa-spin'; 219 break; 220 case appModels.OperationPhases.Failed: 221 color = COLORS.operation.failed; 222 className = 'fa fa-heart-broken'; 223 break; 224 case appModels.OperationPhases.Error: 225 color = COLORS.operation.error; 226 className = 'fa fa-heart-broken'; 227 break; 228 case appModels.OperationPhases.Succeeded: 229 color = COLORS.operation.success; 230 className = 'fa fa-heart'; 231 break; 232 case appModels.OperationPhases.Terminating: 233 color = COLORS.operation.terminating; 234 className = 'fa fa-circle-notch fa-spin'; 235 break; 236 } 237 let title: string = resource.message; 238 if (resource.message) { 239 title = `${resource.hookPhase}: ${resource.message};`; 240 } 241 return <i title={title} className={className} style={{color}} />; 242 } 243 return null; 244 }; 245 246 export const getAppOperationState = (app: appModels.Application): appModels.OperationState => { 247 if (app.metadata.deletionTimestamp) { 248 return { 249 phase: appModels.OperationPhases.Running, 250 startedAt: app.metadata.deletionTimestamp 251 } as appModels.OperationState; 252 } else if (app.operation) { 253 return { 254 phase: appModels.OperationPhases.Running, 255 message: (app.status && app.status.operationState && app.status.operationState.message) || 'waiting to start', 256 startedAt: new Date().toISOString(), 257 operation: { 258 sync: {} 259 } 260 } as appModels.OperationState; 261 } else { 262 return app.status.operationState; 263 } 264 }; 265 266 export function getOperationType(application: appModels.Application) { 267 if (application.metadata.deletionTimestamp) { 268 return 'Delete'; 269 } 270 const operation = application.operation || (application.status.operationState && application.status.operationState.operation); 271 if (operation && operation.sync) { 272 return 'Sync'; 273 } 274 return 'Unknown'; 275 } 276 277 const getOperationStateTitle = (app: appModels.Application) => { 278 const appOperationState = getAppOperationState(app); 279 const operationType = getOperationType(app); 280 switch (operationType) { 281 case 'Delete': 282 return 'Deleting'; 283 case 'Sync': 284 switch (appOperationState.phase) { 285 case 'Running': 286 return 'Syncing'; 287 case 'Error': 288 return 'Sync error'; 289 case 'Failed': 290 return 'Sync failed'; 291 case 'Succeeded': 292 return 'Sync OK'; 293 case 'Terminating': 294 return 'Terminated'; 295 } 296 } 297 return 'Unknown'; 298 }; 299 300 export const OperationState = ({app, quiet}: {app: appModels.Application; quiet?: boolean}) => { 301 const appOperationState = getAppOperationState(app); 302 if (appOperationState === undefined) { 303 return <React.Fragment />; 304 } 305 if (quiet && [appModels.OperationPhases.Running, appModels.OperationPhases.Failed, appModels.OperationPhases.Error].indexOf(appOperationState.phase) === -1) { 306 return <React.Fragment />; 307 } 308 309 return ( 310 <React.Fragment> 311 <OperationPhaseIcon app={app} /> {getOperationStateTitle(app)} 312 </React.Fragment> 313 ); 314 }; 315 316 export function getPodStateReason(pod: appModels.State): {message: string; reason: string} { 317 let reason = pod.status.phase; 318 let message = ''; 319 if (pod.status.reason) { 320 reason = pod.status.reason; 321 } 322 323 let initializing = false; 324 for (const container of (pod.status.initContainerStatuses || []).slice().reverse()) { 325 if (container.state.terminated && container.state.terminated.exitCode === 0) { 326 continue; 327 } 328 329 if (container.state.terminated) { 330 if (container.state.terminated.reason) { 331 reason = `Init:ExitCode:${container.state.terminated.exitCode}`; 332 } else { 333 reason = `Init:${container.state.terminated.reason}`; 334 message = container.state.terminated.message; 335 } 336 } else if (container.state.waiting && container.state.waiting.reason && container.state.waiting.reason !== 'PodInitializing') { 337 reason = `Init:${container.state.waiting.reason}`; 338 message = `Init:${container.state.waiting.message}`; 339 } else { 340 reason = `Init: ${(pod.spec.initContainers || []).length})`; 341 } 342 initializing = true; 343 break; 344 } 345 346 if (!initializing) { 347 let hasRunning = false; 348 for (const container of pod.status.containerStatuses || []) { 349 if (container.state.waiting && container.state.waiting.reason) { 350 reason = container.state.waiting.reason; 351 message = container.state.waiting.message; 352 } else if (container.state.terminated && container.state.terminated.reason) { 353 reason = container.state.terminated.reason; 354 message = container.state.terminated.message; 355 } else if (container.state.terminated && container.state.terminated.reason) { 356 if (container.state.terminated.signal !== 0) { 357 reason = `Signal:${container.state.terminated.signal}`; 358 message = ''; 359 } else { 360 reason = `ExitCode:${container.state.terminated.exitCode}`; 361 message = ''; 362 } 363 } else if (container.ready && container.state.running) { 364 hasRunning = true; 365 } 366 } 367 368 // change pod status back to 'Running' if there is at least one container still reporting as 'Running' status 369 if (reason === 'Completed' && hasRunning) { 370 reason = 'Running'; 371 message = ''; 372 } 373 } 374 375 if ((pod as any).metadata.deletionTimestamp && pod.status.reason === 'NodeLost') { 376 reason = 'Unknown'; 377 message = ''; 378 } else if ((pod as any).metadata.deletionTimestamp) { 379 reason = 'Terminating'; 380 message = ''; 381 } 382 383 return {reason, message}; 384 } 385 386 export function getConditionCategory(condition: appModels.ApplicationCondition): 'error' | 'warning' | 'info' { 387 if (condition.type.endsWith('Error')) { 388 return 'error'; 389 } else if (condition.type.endsWith('Warning')) { 390 return 'warning'; 391 } else { 392 return 'info'; 393 } 394 } 395 396 export function isAppNode(node: appModels.ResourceNode) { 397 return node.kind === 'Application' && node.group === 'argoproj.io'; 398 } 399 400 export function getAppOverridesCount(app: appModels.Application) { 401 if (app.spec.source.ksonnet && app.spec.source.ksonnet.parameters) { 402 return app.spec.source.ksonnet.parameters.length; 403 } 404 if (app.spec.source.kustomize && app.spec.source.kustomize.images) { 405 return app.spec.source.kustomize.images.length; 406 } 407 if (app.spec.source.helm && app.spec.source.helm.parameters) { 408 return app.spec.source.helm.parameters.length; 409 } 410 return 0; 411 } 412 413 export function isAppRefreshing(app: appModels.Application) { 414 return !!(app.metadata.annotations && app.metadata.annotations[appModels.AnnotationRefreshKey]); 415 } 416 417 export function setAppRefreshing(app: appModels.Application) { 418 if (!app.metadata.annotations) { 419 app.metadata.annotations = {}; 420 } 421 if (!app.metadata.annotations[appModels.AnnotationRefreshKey]) { 422 app.metadata.annotations[appModels.AnnotationRefreshKey] = 'refreshing'; 423 } 424 } 425 426 export function refreshLinkAttrs(app: appModels.Application) { 427 return {disabled: isAppRefreshing(app)}; 428 } 429 430 export const SyncWindowStatusIcon = ({state, window}: {state: appModels.SyncWindowsState; window: appModels.SyncWindow}) => { 431 let className = ''; 432 let color = ''; 433 let current = ''; 434 435 if (state.windows === undefined) { 436 current = 'Inactive'; 437 } else { 438 for (const w of state.windows) { 439 if (w.kind === window.kind && w.schedule === window.schedule && w.duration === window.duration) { 440 current = 'Active'; 441 break; 442 } else { 443 current = 'Inactive'; 444 } 445 } 446 } 447 448 switch (current + ':' + window.kind) { 449 case 'Active:deny': 450 case 'Inactive:allow': 451 className = 'fa fa-stop-circle'; 452 if (window.manualSync) { 453 color = COLORS.sync_window.manual; 454 } else { 455 color = COLORS.sync_window.deny; 456 } 457 break; 458 case 'Active:allow': 459 case 'Inactive:deny': 460 className = 'fa fa-check-circle'; 461 color = COLORS.sync_window.allow; 462 break; 463 default: 464 className = 'fa fa-ghost'; 465 color = COLORS.sync_window.unknown; 466 current = 'Unknown'; 467 break; 468 } 469 470 return ( 471 <React.Fragment> 472 <i title={current} className={className} style={{color}} /> {current} 473 </React.Fragment> 474 ); 475 }; 476 477 export const ApplicationSyncWindowStatusIcon = ({project, state}: {project: string; state: appModels.ApplicationSyncWindowState}) => { 478 let className = ''; 479 let color = ''; 480 let deny = false; 481 let allow = false; 482 let inactiveAllow = false; 483 if (state.assignedWindows !== undefined && state.assignedWindows.length > 0) { 484 if (state.activeWindows !== undefined && state.activeWindows.length > 0) { 485 for (const w of state.activeWindows) { 486 if (w.kind === 'deny') { 487 deny = true; 488 } else if (w.kind === 'allow') { 489 allow = true; 490 } 491 } 492 } 493 for (const a of state.assignedWindows) { 494 if (a.kind === 'allow') { 495 inactiveAllow = true; 496 } 497 } 498 } else { 499 allow = true; 500 } 501 502 if (deny || (!deny && !allow && inactiveAllow)) { 503 className = 'fa fa-stop-circle'; 504 if (state.canSync) { 505 color = COLORS.sync_window.manual; 506 } else { 507 color = COLORS.sync_window.deny; 508 } 509 } else { 510 className = 'fa fa-check-circle'; 511 color = COLORS.sync_window.allow; 512 } 513 514 return ( 515 <a href={`/settings/projects/${project}?tab=windows`} style={{color}}> 516 <i className={className} style={{color}} /> SyncWindow 517 </a> 518 ); 519 }; 520 521 /** 522 * Automatically stops and restarts the given observable when page visibility changes. 523 */ 524 export function handlePageVisibility<T>(src: () => Observable<T>): Observable<T> { 525 return new Observable<T>((observer: Observer<T>) => { 526 let subscription: Subscription; 527 const ensureUnsubscribed = () => { 528 if (subscription) { 529 subscription.unsubscribe(); 530 subscription = null; 531 } 532 }; 533 const start = () => { 534 ensureUnsubscribed(); 535 subscription = src().subscribe((item: T) => observer.next(item), err => observer.error(err), () => observer.complete()); 536 }; 537 538 if (!document.hidden) { 539 start(); 540 } 541 542 const visibilityChangeSubscription = Observable.fromEvent(document, 'visibilitychange') 543 // wait until user stop clicking back and forth to avoid restarting observable too often 544 .debounceTime(500) 545 .subscribe(() => { 546 if (document.hidden && subscription) { 547 ensureUnsubscribed(); 548 } else if (!document.hidden && !subscription) { 549 start(); 550 } 551 }); 552 553 return () => { 554 visibilityChangeSubscription.unsubscribe(); 555 ensureUnsubscribed(); 556 }; 557 }); 558 } 559 560 export function parseApiVersion(apiVersion: string): {group: string; version: string} { 561 const parts = apiVersion.split('/'); 562 if (parts.length > 1) { 563 return {group: parts[0], version: parts[1]}; 564 } 565 return {version: parts[0], group: ''}; 566 }