github.com/freiheit-com/kuberpult@v1.24.2-0.20240328135542-315d5630abe6/services/frontend-service/src/ui/components/ServiceLane/ServiceLane.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 { 17 addAction, 18 EnvironmentGroupExtended, 19 showSnackbarError, 20 showSnackbarWarn, 21 useCurrentlyExistsAtGroup, 22 useDeployedReleases, 23 useFilteredApplicationLocks, 24 useNavigateWithSearchParams, 25 useVersionsForApp, 26 } from '../../utils/store'; 27 import { ReleaseCard } from '../ReleaseCard/ReleaseCard'; 28 import { DeleteWhite, HistoryWhite } from '../../../images'; 29 import { Application, Environment, UndeploySummary } from '../../../api/api'; 30 import * as React from 'react'; 31 import { AppLockSummary } from '../chip/EnvironmentGroupChip'; 32 import { WarningBoxes } from './Warnings'; 33 import { DotsMenu, DotsMenuButton } from './DotsMenu'; 34 import { useCallback, useState } from 'react'; 35 import { EnvSelectionDialog } from './EnvSelectionDialog'; 36 37 // number of releases on home. based on design 38 // we could update this dynamically based on viewport width 39 const numberOfDisplayedReleasesOnHome = 6; 40 41 const getReleasesToDisplay = (deployedReleases: number[], allReleases: number[]): number[] => { 42 // all deployed releases are important and the latest release is also important 43 const importantReleases = deployedReleases.includes(allReleases[0]) 44 ? deployedReleases 45 : [allReleases[0], ...deployedReleases]; 46 // number of remaining releases to get from history 47 const numOfTrailingReleases = numberOfDisplayedReleasesOnHome - importantReleases.length; 48 // find the index of the last deployed release e.g. Prod (or -1 when there's no deployed releases) 49 const oldestImportantReleaseIndex = importantReleases.length 50 ? allReleases.findIndex((version) => version === importantReleases.slice(-1)[0]) 51 : -1; 52 // take the deployed releases + a slice from the oldest element (or very first, see above) with total length 6 53 return [ 54 ...importantReleases, 55 ...allReleases.slice(oldestImportantReleaseIndex + 1, oldestImportantReleaseIndex + numOfTrailingReleases + 1), 56 ]; 57 }; 58 59 function getNumberOfReleasesBetween(releases: number[], higherVersion: number, lowerVersion: number): number { 60 // diff = index of lower version (older release) - index of higher version (newer release) - 1 61 return releases.findIndex((ver) => ver === lowerVersion) - releases.findIndex((ver) => ver === higherVersion) - 1; 62 } 63 64 const DiffElement: React.FC<{ diff: number; title: string }> = ({ diff, title }) => ( 65 <div className="service-lane__diff--container" title={title}> 66 <div className="service-lane__diff--dot" /> 67 <div className="service-lane__diff--dot" /> 68 <div className="service-lane__diff--number">{diff}</div> 69 <div className="service-lane__diff--dot" /> 70 <div className="service-lane__diff--dot" /> 71 </div> 72 ); 73 74 const deriveUndeployMessage = (undeploySummary: UndeploySummary): string | undefined => { 75 switch (undeploySummary) { 76 case UndeploySummary.UNDEPLOY: 77 return 'Delete Forever'; 78 case UndeploySummary.NORMAL: 79 return 'Prepare Undeploy Release'; 80 case UndeploySummary.MIXED: 81 return undefined; 82 default: 83 return undefined; 84 } 85 }; 86 87 export const ServiceLane: React.FC<{ application: Application }> = (props) => { 88 const { application } = props; 89 const deployedReleases = useDeployedReleases(application.name); 90 const allReleases = useVersionsForApp(application.name); 91 const { navCallback } = useNavigateWithSearchParams('releases/' + application.name); 92 const prepareUndeployOrUndeployText = deriveUndeployMessage(application.undeploySummary); 93 94 const prepareUndeployOrUndeploy = React.useCallback(() => { 95 switch (application.undeploySummary) { 96 case UndeploySummary.UNDEPLOY: 97 addAction({ 98 action: { 99 $case: 'undeploy', 100 undeploy: { application: application.name }, 101 }, 102 }); 103 break; 104 case UndeploySummary.NORMAL: 105 addAction({ 106 action: { 107 $case: 'prepareUndeploy', 108 prepareUndeploy: { application: application.name }, 109 }, 110 }); 111 break; 112 case UndeploySummary.MIXED: 113 showSnackbarError('Internal Error: Cannot prepare to undeploy or actual undeploy in mixed state.'); 114 break; 115 default: 116 showSnackbarError('Internal Error: Cannot prepare to undeploy or actual undeploy in unknown state.'); 117 break; 118 } 119 }, [application.name, application.undeploySummary]); 120 const releases = getReleasesToDisplay(deployedReleases, allReleases); 121 122 const releases_lane = 123 !!releases && 124 releases.map((rel, index) => { 125 // diff is releases between current card and the next. 126 // for the last card, diff is number of remaining releases in history 127 const diff = 128 index < releases.length - 1 129 ? getNumberOfReleasesBetween(allReleases, rel, releases[index + 1]) 130 : getNumberOfReleasesBetween(allReleases, rel, allReleases.slice(-1)[0]) + 1; 131 return ( 132 <div key={application.name + '-' + rel} className="service-lane__diff"> 133 <ReleaseCard app={application.name} version={rel} key={application.name + '-' + rel} /> 134 {!!diff && ( 135 <DiffElement 136 diff={diff} 137 title={'There are ' + diff + ' more releases hidden. Click on History to view more'} 138 /> 139 )} 140 </div> 141 ); 142 }); 143 144 const envs: Environment[] = useCurrentlyExistsAtGroup(application.name).flatMap( 145 (envGroup: EnvironmentGroupExtended) => envGroup.environments 146 ); 147 148 const [showEnvSelectionDialog, setShowEnvSelectionDialog] = useState(false); 149 150 const handleClose = useCallback(() => { 151 setShowEnvSelectionDialog(false); 152 }, []); 153 const confirmEnvAppDelete = useCallback( 154 (selectedEnvs: string[]) => { 155 if (selectedEnvs.length === envs.length) { 156 showSnackbarWarn("If you want to delete all environments, use 'prepare undeploy'"); 157 setShowEnvSelectionDialog(false); 158 return; 159 } 160 selectedEnvs.forEach((env) => { 161 addAction({ 162 action: { 163 $case: 'deleteEnvFromApp', 164 deleteEnvFromApp: { application: application.name, environment: env }, 165 }, 166 }); 167 }); 168 setShowEnvSelectionDialog(false); 169 }, 170 [application.name, envs] 171 ); 172 const buttons: DotsMenuButton[] = [ 173 { 174 label: 'View History', 175 icon: <HistoryWhite />, 176 onClick: (): void => { 177 navCallback(); 178 }, 179 }, 180 { 181 label: 'Remove environment from app', 182 icon: <DeleteWhite />, 183 onClick: (): void => { 184 setShowEnvSelectionDialog(true); 185 }, 186 }, 187 ]; 188 if (prepareUndeployOrUndeployText) { 189 buttons.push({ 190 label: prepareUndeployOrUndeployText, 191 onClick: prepareUndeployOrUndeploy, 192 icon: <DeleteWhite />, 193 }); 194 } 195 196 const dotsMenu = <DotsMenu buttons={buttons} />; 197 const appLocks = useFilteredApplicationLocks(application.name); 198 const dialog = ( 199 <EnvSelectionDialog 200 environments={envs.map((e) => e.name)} 201 open={showEnvSelectionDialog} 202 onSubmit={confirmEnvAppDelete} 203 onCancel={handleClose} 204 envSelectionDialog={true} 205 /> 206 ); 207 208 return ( 209 <div className="service-lane"> 210 {dialog} 211 <div className="service-lane__header"> 212 <div className="service__name"> 213 {(application.team ? application.team + ' | ' : '<No Team> | ') + application.name} 214 {appLocks.length >= 1 && ( 215 <div className={'test-app-lock-summary'}> 216 <AppLockSummary app={application.name} numLocks={appLocks.length} /> 217 </div> 218 )} 219 </div> 220 <div className="service__actions__">{dotsMenu}</div> 221 </div> 222 <div className="service__warnings"> 223 <WarningBoxes application={application} /> 224 </div> 225 <div className="service__releases">{releases_lane}</div> 226 </div> 227 ); 228 };