github.com/freiheit-com/kuberpult@v1.24.2-0.20240328135542-315d5630abe6/services/frontend-service/src/ui/components/ReleaseDialog/ReleaseDialog.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, { ReactElement, useCallback } from 'react'; 18 import { Environment, Environment_Application, EnvironmentGroup, Lock, LockBehavior, Release } from '../../../api/api'; 19 import { 20 addAction, 21 useCloseReleaseDialog, 22 useEnvironmentGroups, 23 useReleaseOptional, 24 useReleaseOrThrow, 25 useRolloutStatus, 26 useTeamFromApplication, 27 } from '../../utils/store'; 28 import { Button } from '../button'; 29 import { Close, Locks } from '../../../images'; 30 import { EnvironmentChip } from '../chip/EnvironmentGroupChip'; 31 import { FormattedDate } from '../FormattedDate/FormattedDate'; 32 import { 33 ArgoAppLink, 34 ArgoTeamLink, 35 DisplayManifestLink, 36 DisplaySourceLink, 37 DisplayCommitHistoryLink, 38 } from '../../utils/Links'; 39 import { ReleaseVersion } from '../ReleaseVersion/ReleaseVersion'; 40 import { PlainDialog } from '../dialog/ConfirmationDialog'; 41 import { ExpandButton } from '../button/ExpandButton'; 42 import { RolloutStatusDescription } from '../RolloutStatusDescription/RolloutStatusDescription'; 43 44 export type ReleaseDialogProps = { 45 className?: string; 46 app: string; 47 version: number; 48 }; 49 50 export const AppLock: React.FC<{ 51 env: Environment; 52 app: string; 53 lock: Lock; 54 }> = ({ env, app, lock }) => { 55 const deleteAppLock = useCallback(() => { 56 addAction({ 57 action: { 58 $case: 'deleteEnvironmentApplicationLock', 59 deleteEnvironmentApplicationLock: { environment: env.name, application: app, lockId: lock.lockId }, 60 }, 61 }); 62 }, [app, env.name, lock.lockId]); 63 return ( 64 <div 65 title={'App Lock Message: "' + lock.message + '" | ID: "' + lock.lockId + '" | Click to unlock. '} 66 onClick={deleteAppLock}> 67 <Button icon={<Locks className="env-card-app-lock" />} className={'button-lock'} /> 68 </div> 69 ); 70 }; 71 72 export type EnvironmentListItemProps = { 73 env: Environment; 74 envGroup: EnvironmentGroup; 75 app: string; 76 release: Release; 77 queuedVersion: number; 78 className?: string; 79 }; 80 81 type CommitIdProps = { 82 application: Environment_Application; 83 app: string; 84 env: Environment; 85 otherRelease?: Release; 86 }; 87 88 const DeployedVersion: React.FC<CommitIdProps> = ({ application, app, env, otherRelease }): ReactElement => { 89 if (!application || !otherRelease) { 90 return ( 91 <span> 92 "{app}" has no version deployed on "{env.name}" 93 </span> 94 ); 95 } 96 const firstLine = otherRelease.sourceMessage.split('\n')[0]; 97 return ( 98 <span> 99 <ReleaseVersion release={otherRelease} /> 100 {firstLine} 101 </span> 102 ); 103 }; 104 105 export const EnvironmentListItem: React.FC<EnvironmentListItemProps> = ({ 106 env, 107 envGroup, 108 app, 109 release, 110 queuedVersion, 111 className, 112 }) => { 113 const createAppLock = useCallback(() => { 114 addAction({ 115 action: { 116 $case: 'createEnvironmentApplicationLock', 117 createEnvironmentApplicationLock: { 118 environment: env.name, 119 application: app, 120 lockId: '', 121 message: '', 122 }, 123 }, 124 }); 125 }, [app, env.name]); 126 const deployAndLockClick = useCallback( 127 (shouldLockToo: boolean) => { 128 if (release.version) { 129 addAction({ 130 action: { 131 $case: 'deploy', 132 deploy: { 133 environment: env.name, 134 application: app, 135 version: release.version, 136 ignoreAllLocks: false, 137 lockBehavior: LockBehavior.IGNORE, 138 }, 139 }, 140 }); 141 if (shouldLockToo) { 142 createAppLock(); 143 } 144 } 145 }, 146 [release.version, app, env.name, createAppLock] 147 ); 148 149 const queueInfo = 150 queuedVersion === 0 ? null : ( 151 <div 152 className={classNames('env-card-data env-card-data-queue', className)} 153 title={ 154 'An attempt was made to deploy version ' + 155 queuedVersion + 156 ' either by a release train, or when a new version was created. However, there was a lock present at the time, so kuberpult did not deploy this version. ' 157 }> 158 Version {queuedVersion} was not deployed, because of a lock. 159 </div> 160 ); 161 const otherRelease = useReleaseOptional(app, env); 162 const application = env.applications[app]; 163 const getDeploymentMetadata = (): [String, JSX.Element] => { 164 if (!application) { 165 return ['', <></>]; 166 } 167 if (application.deploymentMetaData === null) { 168 return ['', <></>]; 169 } 170 const deployedBy = application.deploymentMetaData?.deployAuthor ?? 'unknown'; 171 const deployedUNIX = application.deploymentMetaData?.deployTime ?? ''; 172 if (deployedUNIX === '') { 173 return ['Deployed by ' + deployedBy, <></>]; 174 } 175 const deployedDate = new Date(+deployedUNIX * 1000); 176 const returnString = 'Deployed by ' + deployedBy + ' '; 177 const time = ( 178 <FormattedDate createdAt={deployedDate} className={classNames('release-dialog-createdAt', className)} /> 179 ); 180 181 return [returnString, time]; 182 }; 183 const appRolloutStatus = useRolloutStatus((getter) => getter.getAppStatus(app, application?.version, env.name)); 184 return ( 185 <li key={env.name} className={classNames('env-card', className)}> 186 <div className="env-card-header"> 187 <EnvironmentChip 188 env={env} 189 app={app} 190 envGroup={envGroup} 191 className={'release-environment'} 192 key={env.name} 193 groupNameOverride={undefined} 194 numberEnvsDeployed={undefined} 195 numberEnvsInGroup={undefined} 196 /> 197 <div className={classNames('env-card-app-locks')}> 198 {Object.values(env.applications) 199 .filter((application) => application.name === app) 200 .map((app) => app.locks) 201 .map((locks) => 202 Object.values(locks).map((lock) => ( 203 <AppLock key={lock.lockId} env={env} app={app} lock={lock} /> 204 )) 205 )} 206 {appRolloutStatus && <RolloutStatusDescription status={appRolloutStatus} />} 207 </div> 208 </div> 209 <div className="content-area"> 210 <div className="content-left"> 211 <div 212 className={classNames('env-card-data', className)} 213 title={ 214 'Shows the version that is currently deployed on ' + 215 env.name + 216 '. ' + 217 (release.undeployVersion ? undeployTooltipExplanation : '') 218 }> 219 <DeployedVersion app={app} env={env} application={application} otherRelease={otherRelease} /> 220 </div> 221 {queueInfo} 222 <div className={classNames('env-card-data', className)}> 223 {getDeploymentMetadata().flatMap((metadata, i) => ( 224 <div key={i}>{metadata} </div> 225 ))} 226 </div> 227 </div> 228 <div className="content-right"> 229 <div className="env-card-buttons"> 230 <Button 231 className="env-card-add-lock-btn" 232 label="Add lock" 233 onClick={createAppLock} 234 icon={<Locks className="icon" />} 235 /> 236 <div 237 title={ 238 'When doing manual deployments, it is usually best to also lock the app. If you omit the lock, an automatic release train or another person may deploy an unintended version. If you do not want a lock, click the arrow.' 239 }> 240 <ExpandButton onClickSubmit={deployAndLockClick} defaultButtonLabel={'Deploy & Lock'} /> 241 </div> 242 </div> 243 </div> 244 </div> 245 </li> 246 ); 247 }; 248 249 export const EnvironmentList: React.FC<{ 250 release: Release; 251 app: string; 252 version: number; 253 className?: string; 254 }> = ({ release, app, version, className }) => { 255 const allEnvGroups: EnvironmentGroup[] = useEnvironmentGroups(); 256 return ( 257 <div className="release-env-group-list"> 258 {allEnvGroups.map((envGroup) => ( 259 <ul className={classNames('release-env-list', className)} key={envGroup.environmentGroupName}> 260 {envGroup.environments.map((env) => ( 261 <EnvironmentListItem 262 key={env.name} 263 env={env} 264 envGroup={envGroup} 265 app={app} 266 release={release} 267 className={className} 268 queuedVersion={env.applications[app] ? env.applications[app].queuedVersion : 0} 269 /> 270 ))} 271 </ul> 272 ))} 273 </div> 274 ); 275 }; 276 277 export const undeployTooltipExplanation = 278 'This is the "undeploy" version. It is essentially an empty manifest. Deploying this means removing all kubernetes entities like deployments from the given environment. You must deploy this to all environments before kuberpult allows to delete the app entirely.'; 279 280 export const ReleaseDialog: React.FC<ReleaseDialogProps> = (props) => { 281 const { app, className, version } = props; 282 // the ReleaseDialog is only opened when there is a release, so we can assume that it exists here: 283 const release = useReleaseOrThrow(app, version); 284 const team = useTeamFromApplication(app); 285 const closeReleaseDialog = useCloseReleaseDialog(); 286 287 const dialog: JSX.Element | '' = ( 288 <PlainDialog 289 open={app !== ''} 290 onClose={closeReleaseDialog} 291 classNames={'release-dialog'} 292 disableBackground={true} 293 center={true}> 294 <> 295 <div className={classNames('release-dialog-app-bar', className)}> 296 <div className={classNames('release-dialog-app-bar-data')}> 297 <div className={classNames('release-dialog-message', className)}> 298 <span className={classNames('release-dialog-commitMessage', className)}> 299 {release?.sourceMessage} 300 </span> 301 </div> 302 <div className="source"> 303 <span> 304 {'Created '} 305 {release?.createdAt ? ( 306 <FormattedDate 307 createdAt={release.createdAt} 308 className={classNames('release-dialog-createdAt', className)} 309 /> 310 ) : ( 311 'at an unknown date' 312 )} 313 {' by '} 314 {release?.sourceAuthor ? release?.sourceAuthor : 'an unknown author'}{' '} 315 </span> 316 <span className="links"> 317 <DisplaySourceLink commitId={release.sourceCommitId} displayString={'Source'} /> 318 319 <DisplayManifestLink app={app} version={release.version} displayString="Manifest" /> 320 321 <DisplayCommitHistoryLink 322 commitId={release.sourceCommitId} 323 displayString={'Commit History'} 324 /> 325 </span> 326 </div> 327 <div className={classNames('release-dialog-app', className)}> 328 {'App: '} 329 <ArgoAppLink app={app} /> 330 <ArgoTeamLink team={team} /> 331 </div> 332 </div> 333 <Button 334 onClick={closeReleaseDialog} 335 className={classNames('release-dialog-close', className)} 336 icon={<Close />} 337 /> 338 </div> 339 <EnvironmentList app={app} className={className} release={release} version={version} /> 340 </> 341 </PlainDialog> 342 ); 343 return <div>{dialog}</div>; 344 };