github.com/argoproj/argo-cd/v3@v3.2.1/ui/src/app/applications/components/application-status-panel/application-status-panel.tsx (about) 1 import {HelpIcon} from 'argo-ui'; 2 import * as React from 'react'; 3 import {ARGO_GRAY6_COLOR, DataLoader} from '../../../shared/components'; 4 import {Revision} from '../../../shared/components/revision'; 5 import {Timestamp} from '../../../shared/components/timestamp'; 6 import * as models from '../../../shared/models'; 7 import {services} from '../../../shared/services'; 8 import { 9 ApplicationSyncWindowStatusIcon, 10 ComparisonStatusIcon, 11 getAppDefaultSource, 12 getAppDefaultSyncRevisionExtra, 13 getAppOperationState, 14 HydrateOperationPhaseIcon, 15 hydrationStatusMessage, 16 getProgressiveSyncStatusColor, 17 getProgressiveSyncStatusIcon 18 } from '../utils'; 19 import {getConditionCategory, HealthStatusIcon, OperationState, syncStatusMessage, getAppDefaultSyncRevision, getAppDefaultOperationSyncRevision} from '../utils'; 20 import {RevisionMetadataPanel} from './revision-metadata-panel'; 21 import * as utils from '../utils'; 22 import {COLORS} from '../../../shared/components/colors'; 23 24 import './application-status-panel.scss'; 25 26 interface Props { 27 application: models.Application; 28 showDiff?: () => any; 29 showOperation?: () => any; 30 showHydrateOperation?: () => any; 31 showConditions?: () => any; 32 showExtension?: (id: string) => any; 33 showMetadataInfo?: (revision: string) => any; 34 } 35 36 interface SectionInfo { 37 title: string; 38 helpContent?: string; 39 } 40 41 const sectionLabel = (info: SectionInfo) => ( 42 <label style={{fontSize: '12px', fontWeight: 600, color: ARGO_GRAY6_COLOR}}> 43 {info.title} 44 {info.helpContent && <HelpIcon title={info.helpContent} />} 45 </label> 46 ); 47 48 const sectionHeader = (info: SectionInfo, onClick?: () => any) => { 49 return ( 50 <div style={{display: 'flex', alignItems: 'center', marginBottom: '0.5em'}}> 51 {sectionLabel(info)} 52 {onClick && ( 53 <button className='argo-button application-status-panel__more-button' onClick={onClick}> 54 <i className='fa fa-ellipsis-h' /> 55 </button> 56 )} 57 </div> 58 ); 59 }; 60 61 const getApplicationSetOwnerRef = (application: models.Application) => { 62 return application.metadata.ownerReferences?.find(ref => ref.kind === 'ApplicationSet'); 63 }; 64 65 const ProgressiveSyncStatus = ({application}: {application: models.Application}) => { 66 const appSetRef = getApplicationSetOwnerRef(application); 67 if (!appSetRef) { 68 return null; 69 } 70 71 return ( 72 <DataLoader 73 input={application} 74 errorRenderer={() => { 75 // For any errors, show a minimal error state 76 return ( 77 <div className='application-status-panel__item'> 78 {sectionHeader({ 79 title: 'PROGRESSIVE SYNC', 80 helpContent: 'Shows the current status of progressive sync for applications managed by an ApplicationSet.' 81 })} 82 <div className='application-status-panel__item-value'> 83 <i className='fa fa-exclamation-triangle' style={{color: COLORS.sync.unknown}} /> Error 84 </div> 85 <div className='application-status-panel__item-name'>Unable to load Progressive Sync status</div> 86 </div> 87 ); 88 }} 89 load={async () => { 90 // Find ApplicationSet by searching all namespaces dynamically 91 const appSetList = await services.applications.listApplicationSets(); 92 const appSet = appSetList.items?.find(item => item.metadata.name === appSetRef.name); 93 94 return {appSet}; 95 }}> 96 {({appSet}: {appSet: models.ApplicationSet}) => { 97 // Hide panel if: Progressive Sync disabled, no permission, or not RollingSync strategy 98 if (!appSet || !appSet.status?.applicationStatus || appSet?.spec?.strategy?.type !== 'RollingSync') { 99 return null; 100 } 101 102 // Get the current application's status from the ApplicationSet applicationStatus 103 const appResource = appSet.status?.applicationStatus?.find(status => status.application === application.metadata.name); 104 105 // If no application status is found, show a default status 106 if (!appResource) { 107 return ( 108 <div className='application-status-panel__item'> 109 {sectionHeader({ 110 title: 'PROGRESSIVE SYNC', 111 helpContent: 'Shows the current status of progressive sync for applications managed by an ApplicationSet with RollingSync strategy.' 112 })} 113 <div className='application-status-panel__item-value'> 114 <i className='fa fa-clock' style={{color: COLORS.sync.out_of_sync}} /> Waiting 115 </div> 116 <div className='application-status-panel__item-name'>Application status not yet available from ApplicationSet</div> 117 </div> 118 ); 119 } 120 121 // Get last transition time from application status 122 const lastTransitionTime = appResource?.lastTransitionTime; 123 124 return ( 125 <div className='application-status-panel__item'> 126 {sectionHeader({ 127 title: 'PROGRESSIVE SYNC', 128 helpContent: 'Shows the current status of progressive sync for applications managed by an ApplicationSet with RollingSync strategy.' 129 })} 130 <div className='application-status-panel__item-value' style={{color: getProgressiveSyncStatusColor(appResource.status)}}> 131 {getProgressiveSyncStatusIcon({status: appResource.status})} {appResource.status} 132 </div> 133 {appResource?.step && <div className='application-status-panel__item-value'>Wave: {appResource.step}</div>} 134 {lastTransitionTime && ( 135 <div className='application-status-panel__item-name' style={{marginBottom: '0.5em'}}> 136 Last Transition: <br /> 137 <Timestamp date={lastTransitionTime} /> 138 </div> 139 )} 140 {appResource?.message && <div className='application-status-panel__item-name'>{appResource.message}</div>} 141 </div> 142 ); 143 }} 144 </DataLoader> 145 ); 146 }; 147 148 export const ApplicationStatusPanel = ({application, showDiff, showOperation, showHydrateOperation, showConditions, showExtension, showMetadataInfo}: Props) => { 149 const [showProgressiveSync, setShowProgressiveSync] = React.useState(false); 150 151 React.useEffect(() => { 152 // Only show Progressive Sync if the application has an ApplicationSet parent 153 // The actual strategy validation will be done inside ProgressiveSyncStatus component 154 setShowProgressiveSync(!!getApplicationSetOwnerRef(application)); 155 }, [application]); 156 157 const today = new Date(); 158 159 let daysSinceLastSynchronized = 0; 160 const history = application.status.history || []; 161 if (history.length > 0) { 162 const deployDate = new Date(history[history.length - 1].deployedAt); 163 daysSinceLastSynchronized = Math.round(Math.abs((today.getTime() - deployDate.getTime()) / (24 * 60 * 60 * 1000))); 164 } 165 const cntByCategory = (application.status.conditions || []).reduce( 166 (map, next) => map.set(getConditionCategory(next), (map.get(getConditionCategory(next)) || 0) + 1), 167 new Map<string, number>() 168 ); 169 const appOperationState = getAppOperationState(application); 170 if (application.metadata.deletionTimestamp && !appOperationState) { 171 showOperation = null; 172 } 173 174 const statusExtensions = services.extensions.getStatusPanelExtensions(); 175 176 const revision = getAppDefaultSyncRevision(application); 177 const operationStateRevision = getAppDefaultOperationSyncRevision(application); 178 const infos = cntByCategory.get('info'); 179 const warnings = cntByCategory.get('warning'); 180 const errors = cntByCategory.get('error'); 181 const source = getAppDefaultSource(application); 182 const hasMultipleSources = application.spec.sources?.length > 0; 183 const revisionType = source?.repoURL?.startsWith('oci://') ? 'oci' : source?.chart ? 'helm' : 'git'; 184 return ( 185 <div className='application-status-panel row'> 186 <div className='application-status-panel__item'> 187 <div style={{lineHeight: '19.5px', marginBottom: '0.3em'}}>{sectionLabel({title: 'APP HEALTH', helpContent: 'The health status of your app'})}</div> 188 <div className='application-status-panel__item-value'> 189 <HealthStatusIcon state={application.status.health} /> 190 191 {application.status.health.status} 192 </div> 193 {application.status.health.message && <div className='application-status-panel__item-name'>{application.status.health.message}</div>} 194 </div> 195 {application.spec.sourceHydrator && application.status?.sourceHydrator?.currentOperation && ( 196 <div className='application-status-panel__item'> 197 <div style={{lineHeight: '19.5px', marginBottom: '0.3em'}}> 198 {sectionLabel({ 199 title: 'SOURCE HYDRATOR', 200 helpContent: 'The source hydrator reads manifests from git, hydrates (renders) them, and pushes them to a different location in git.' 201 })} 202 </div> 203 <div className='application-status-panel__item-value'> 204 <a className='application-status-panel__item-value__hydrator-link' onClick={() => showHydrateOperation && showHydrateOperation()}> 205 <HydrateOperationPhaseIcon operationState={application.status.sourceHydrator.currentOperation} isButton={true} /> 206 207 {application.status.sourceHydrator.currentOperation.phase} 208 </a> 209 <div className='application-status-panel__item-value__revision show-for-large'>{hydrationStatusMessage(application)}</div> 210 </div> 211 <div className='application-status-panel__item-name' style={{marginBottom: '0.5em'}}> 212 {application.status.sourceHydrator.currentOperation.phase}{' '} 213 <Timestamp date={application.status.sourceHydrator.currentOperation.finishedAt || application.status.sourceHydrator.currentOperation.startedAt} /> 214 </div> 215 {application.status.sourceHydrator.currentOperation.message && ( 216 <div className='application-status-panel__item-name'>{application.status.sourceHydrator.currentOperation.message}</div> 217 )} 218 <div className='application-status-panel__item-name'> 219 {application.status.sourceHydrator.currentOperation.drySHA && ( 220 <RevisionMetadataPanel 221 appName={application.metadata.name} 222 appNamespace={application.metadata.namespace} 223 type={''} 224 revision={application.status.sourceHydrator.currentOperation.drySHA} 225 versionId={utils.getAppCurrentVersion(application)} 226 /> 227 )} 228 </div> 229 </div> 230 )} 231 <div className='application-status-panel__item'> 232 {sectionHeader( 233 { 234 title: 'SYNC STATUS', 235 helpContent: 'Whether or not the version of your app is up to date with your repo. You may wish to sync your app if it is out-of-sync.' 236 }, 237 () => showMetadataInfo(application.status.sync ? 'SYNC_STATUS_REVISION' : null) 238 )} 239 <div className={`application-status-panel__item-value${appOperationState?.phase ? ` application-status-panel__item-value--${appOperationState.phase}` : ''}`}> 240 <div> 241 {application.status.sync.status === models.SyncStatuses.OutOfSync ? ( 242 <a onClick={() => showDiff && showDiff()}> 243 <ComparisonStatusIcon status={application.status.sync.status} label={true} isButton={true} /> 244 </a> 245 ) : ( 246 <ComparisonStatusIcon status={application.status.sync.status} label={true} /> 247 )} 248 </div> 249 <div className='application-status-panel__item-value__revision show-for-large'>{syncStatusMessage(application)}</div> 250 </div> 251 <div className='application-status-panel__item-name' style={{marginBottom: '0.5em'}}> 252 {application.spec.syncPolicy?.automated && application.spec.syncPolicy.automated.enabled !== false ? 'Auto sync is enabled.' : 'Auto sync is not enabled.'} 253 </div> 254 {application.status && 255 application.status.sync && 256 (hasMultipleSources 257 ? application.status.sync.revisions && application.status.sync.revisions[0] && application.spec.sources && !application.spec.sources[0].chart 258 : application.status.sync.revision && !application.spec?.source?.chart) && ( 259 <div className='application-status-panel__item-name'> 260 <RevisionMetadataPanel 261 appName={application.metadata.name} 262 appNamespace={application.metadata.namespace} 263 type={revisionType} 264 revision={revision} 265 versionId={utils.getAppCurrentVersion(application)} 266 /> 267 </div> 268 )} 269 </div> 270 {appOperationState && ( 271 <div className='application-status-panel__item'> 272 {sectionHeader( 273 { 274 title: 'LAST SYNC', 275 helpContent: 276 'Whether or not your last app sync was successful. It has been ' + 277 daysSinceLastSynchronized + 278 ' days since last sync. Click for the status of that sync.' 279 }, 280 () => 281 showMetadataInfo( 282 appOperationState.syncResult && (appOperationState.syncResult.revisions || appOperationState.syncResult.revision) 283 ? 'OPERATION_STATE_REVISION' 284 : null 285 ) 286 )} 287 <div className={`application-status-panel__item-value application-status-panel__item-value--${appOperationState.phase}`}> 288 <a onClick={() => showOperation && showOperation()}> 289 <OperationState app={application} isButton={true} />{' '} 290 </a> 291 {appOperationState.syncResult && (appOperationState.syncResult.revision || appOperationState.syncResult.revisions) && ( 292 <div className='application-status-panel__item-value__revision show-for-large'> 293 to <Revision repoUrl={source.repoURL} revision={operationStateRevision} /> {getAppDefaultSyncRevisionExtra(application)} 294 </div> 295 )} 296 </div> 297 <div className='application-status-panel__item-name' style={{marginBottom: '0.5em'}}> 298 {appOperationState.phase} <Timestamp date={appOperationState.finishedAt || appOperationState.startedAt} /> 299 </div> 300 {(appOperationState.syncResult && operationStateRevision && ( 301 <RevisionMetadataPanel 302 appName={application.metadata.name} 303 appNamespace={application.metadata.namespace} 304 type={revisionType} 305 revision={operationStateRevision} 306 versionId={utils.getAppCurrentVersion(application)} 307 /> 308 )) || <div className='application-status-panel__item-name'>{appOperationState.message}</div>} 309 </div> 310 )} 311 {application.status.conditions && ( 312 <div className={`application-status-panel__item`}> 313 {sectionLabel({title: 'APP CONDITIONS'})} 314 <div className='application-status-panel__item-value application-status-panel__conditions' onClick={() => showConditions && showConditions()}> 315 {infos && ( 316 <a className='info'> 317 <i className='fa fa-info-circle application-status-panel__item-value__status-button' /> {infos} Info 318 </a> 319 )} 320 {warnings && ( 321 <a className='warning'> 322 <i className='fa fa-exclamation-triangle application-status-panel__item-value__status-button' /> {warnings} Warning{warnings !== 1 && 's'} 323 </a> 324 )} 325 {errors && ( 326 <a className='error'> 327 <i className='fa fa-exclamation-circle application-status-panel__item-value__status-button' /> {errors} Error{errors !== 1 && 's'} 328 </a> 329 )} 330 </div> 331 </div> 332 )} 333 <DataLoader 334 noLoaderOnInputChange={true} 335 input={application} 336 load={async app => { 337 return await services.applications.getApplicationSyncWindowState(app.metadata.name, app.metadata.namespace); 338 }}> 339 {(data: models.ApplicationSyncWindowState) => ( 340 <React.Fragment> 341 {data?.assignedWindows && ( 342 <div className='application-status-panel__item' style={{position: 'relative'}}> 343 {sectionLabel({ 344 title: 'SYNC WINDOWS', 345 helpContent: 346 'The aggregate state of sync windows for this app. ' + 347 'Red: no syncs allowed. ' + 348 'Yellow: manual syncs allowed. ' + 349 'Green: all syncs allowed' 350 })} 351 <div className='application-status-panel__item-value' style={{margin: 'auto 0'}}> 352 <ApplicationSyncWindowStatusIcon project={application.spec.project} state={data} /> 353 </div> 354 </div> 355 )} 356 </React.Fragment> 357 )} 358 </DataLoader> 359 {showProgressiveSync && <ProgressiveSyncStatus application={application} />} 360 {statusExtensions && statusExtensions.map(ext => <ext.component key={ext.title} application={application} openFlyout={() => showExtension && showExtension(ext.id)} />)} 361 </div> 362 ); 363 };