github.com/freiheit-com/kuberpult@v1.24.2-0.20240328135542-315d5630abe6/services/frontend-service/src/ui/components/ReleaseCard/ReleaseCard.tsx (about) 1 /*This file is part of kuberpult. 2 3 Kuberpult is free software: you can redistribute it and/or modify 4 it under the terms of the Expat(MIT) License as published by 5 the Free Software Foundation. 6 7 Kuberpult is distributed in the hope that it will be useful, 8 but WITHOUT ANY WARRANTY; without even the implied warranty of 9 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 10 MIT License for more details. 11 12 You should have received a copy of the MIT License 13 along with kuberpult. If not, see <https://directory.fsf.org/wiki/License:Expat>. 14 15 Copyright 2023 freiheit.com*/ 16 import classNames from 'classnames'; 17 import React from 'react'; 18 import { 19 useCurrentlyDeployedAtGroup, 20 useOpenReleaseDialog, 21 useReleaseOrThrow, 22 useRolloutStatus, 23 EnvironmentGroupExtended, 24 } from '../../utils/store'; 25 import { Tooltip } from '../tooltip/tooltip'; 26 import { EnvironmentGroupChipList } from '../chip/EnvironmentGroupChip'; 27 import { FormattedDate } from '../FormattedDate/FormattedDate'; 28 import { RolloutStatus } from '../../../api/api'; 29 import { ReleaseVersion } from '../ReleaseVersion/ReleaseVersion'; 30 import { RolloutStatusDescription } from '../RolloutStatusDescription/RolloutStatusDescription'; 31 32 export type ReleaseCardProps = { 33 className?: string; 34 version: number; 35 app: string; 36 }; 37 38 const RolloutStatusIcon: React.FC<{ status: RolloutStatus }> = (props) => { 39 const { status } = props; 40 switch (status) { 41 case RolloutStatus.ROLLOUT_STATUS_SUCCESFUL: 42 return <span className="rollout__icon_successful">✓</span>; 43 case RolloutStatus.ROLLOUT_STATUS_PROGRESSING: 44 return <span className="rollout__icon_progressing">↻</span>; 45 case RolloutStatus.ROLLOUT_STATUS_PENDING: 46 return <span className="rollout__icon_pending">⧖</span>; 47 case RolloutStatus.ROLLOUT_STATUS_ERROR: 48 return <span className="rollout__icon_error">!</span>; 49 case RolloutStatus.ROLLOUT_STATUS_UNHEALTHY: 50 return <span className="rollout__icon_unhealthy">⚠</span>; 51 } 52 return <span className="rollout__icon_unknown">?</span>; 53 }; 54 55 // note that the order is important here. 56 // "most interesting" must come first. 57 // see `calculateDeploymentStatus` 58 // The same priority list is also implemented in pkg/service/broadcast.go. 59 const rolloutStatusPriority = [ 60 // Error is not recoverable by waiting and requires manual intervention 61 RolloutStatus.ROLLOUT_STATUS_ERROR, 62 63 // These states may resolve by waiting longer 64 RolloutStatus.ROLLOUT_STATUS_PROGRESSING, 65 RolloutStatus.ROLLOUT_STATUS_UNHEALTHY, 66 RolloutStatus.ROLLOUT_STATUS_PENDING, 67 RolloutStatus.ROLLOUT_STATUS_UNKNOWN, 68 69 // This is the only successful state 70 RolloutStatus.ROLLOUT_STATUS_SUCCESFUL, 71 ]; 72 73 const getRolloutStatusPriority = (status: RolloutStatus): number => { 74 const idx = rolloutStatusPriority.indexOf(status); 75 if (idx === -1) { 76 return rolloutStatusPriority.length; 77 } 78 return idx; 79 }; 80 81 type DeploymentStatus = { 82 environmentGroup: string; 83 rolloutStatus: RolloutStatus; 84 }; 85 86 const useDeploymentStatus = ( 87 app: string, 88 deployedAt: EnvironmentGroupExtended[] 89 ): [Array<DeploymentStatus>, RolloutStatus?] => { 90 const rolloutEnvGroups = useRolloutStatus((getter) => { 91 const groups: { [envGroup: string]: RolloutStatus } = {}; 92 deployedAt.forEach((envGroup) => { 93 const status = envGroup.environments.reduce((cur: RolloutStatus | undefined, env) => { 94 const appVersion: number | undefined = env.applications[app]?.version; 95 const status = getter.getAppStatus(app, appVersion, env.name); 96 if (cur === undefined) { 97 return status; 98 } 99 if (status === undefined) { 100 return cur; 101 } 102 if (getRolloutStatusPriority(status) < getRolloutStatusPriority(cur)) { 103 return status; 104 } 105 return cur; 106 }, undefined); 107 groups[envGroup.environmentGroupName] = status ?? RolloutStatus.ROLLOUT_STATUS_UNKNOWN; 108 }); 109 return groups; 110 }); 111 const rolloutEnvGroupsArray = Object.entries(rolloutEnvGroups).map((e) => ({ 112 environmentGroup: e[0], 113 rolloutStatus: e[1], 114 })); 115 rolloutEnvGroupsArray.sort((a, b) => { 116 if (a.environmentGroup < b.environmentGroup) { 117 return -1; 118 } else if (a.environmentGroup > b.environmentGroup) { 119 return 1; 120 } 121 return 0; 122 }); 123 // Calculates the most interesting rollout status according to the `rolloutStatusPriority`. 124 const mostInteresting = rolloutEnvGroupsArray.reduce( 125 (cur: RolloutStatus | undefined, item) => 126 cur === undefined 127 ? item.rolloutStatus 128 : getRolloutStatusPriority(item.rolloutStatus) < getRolloutStatusPriority(cur) 129 ? item.rolloutStatus 130 : cur, 131 undefined 132 ); 133 return [rolloutEnvGroupsArray, mostInteresting]; 134 }; 135 136 export const ReleaseCard: React.FC<ReleaseCardProps> = (props) => { 137 const { className, app, version } = props; 138 // the ReleaseCard only displays actual releases, so we can assume that it exists here: 139 const release = useReleaseOrThrow(app, version); 140 const { createdAt, sourceMessage, sourceAuthor, undeployVersion } = release; 141 const openReleaseDialog = useOpenReleaseDialog(app, version); 142 const deployedAt = useCurrentlyDeployedAtGroup(app, version); 143 144 const [rolloutEnvs, mostInteresting] = useDeploymentStatus(app, deployedAt); 145 146 const tooltipContents = ( 147 <div className="mdc-tooltip__title_ release__details"> 148 {!!sourceMessage && <b>{sourceMessage}</b>} 149 {!!sourceAuthor && ( 150 <div> 151 <span>Author:</span> {sourceAuthor} 152 </div> 153 )} 154 {!!createdAt && ( 155 <div className="release__metadata"> 156 <span>Created </span> 157 <FormattedDate className={'date'} createdAt={createdAt} /> 158 </div> 159 )} 160 {rolloutEnvs.length > 0 && ( 161 <table className="release__environment_status"> 162 <thead> 163 <tr> 164 <th>Environment group</th> 165 <th>Rollout</th> 166 </tr> 167 </thead> 168 <tbody> 169 {rolloutEnvs.map((env) => ( 170 <tr key={env.environmentGroup}> 171 <td>{env.environmentGroup}</td> 172 <td> 173 <RolloutStatusDescription status={env.rolloutStatus} /> 174 </td> 175 </tr> 176 ))} 177 </tbody> 178 </table> 179 )} 180 </div> 181 ); 182 183 const firstLine = sourceMessage.split('\n')[0]; 184 return ( 185 <Tooltip id={app + version} tooltipContent={tooltipContents}> 186 <div className="release-card__container"> 187 <div className="release__environments"> 188 <EnvironmentGroupChipList app={props.app} version={props.version} smallEnvChip /> 189 </div> 190 <div className={classNames('mdc-card release-card', className)}> 191 <div 192 className="mdc-card__primary-action release-card__description" 193 // ref={control} 194 tabIndex={0} 195 onClick={openReleaseDialog}> 196 <div className="release-card__header"> 197 <div className="release__title">{undeployVersion ? 'Undeploy Version' : firstLine}</div> 198 <ReleaseVersion release={release} /> 199 </div> 200 {mostInteresting !== undefined && ( 201 <div className="release__status"> 202 <RolloutStatusIcon status={mostInteresting} /> 203 </div> 204 )} 205 <div className="mdc-card__ripple" /> 206 </div> 207 </div> 208 </div> 209 </Tooltip> 210 ); 211 };