github.com/freiheit-com/kuberpult@v1.24.2-0.20240328135542-315d5630abe6/services/frontend-service/src/ui/components/ProductVersion/ProductVersion.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 './ProductVersion.scss'; 17 import * as React from 'react'; 18 import { 19 refreshTags, 20 useTags, 21 useEnvironmentGroups, 22 useEnvironments, 23 addAction, 24 showSnackbarError, 25 } from '../../utils/store'; 26 import { DisplayManifestLink, DisplaySourceLink } from '../../utils/Links'; 27 import { Spinner } from '../Spinner/Spinner'; 28 import { EnvironmentGroup, GetProductSummaryResponse, ProductSummary } from '../../../api/api'; 29 import { useSearchParams } from 'react-router-dom'; 30 import { Button } from '../button'; 31 import { EnvSelectionDialog } from '../ServiceLane/EnvSelectionDialog'; 32 import { useState } from 'react'; 33 import { useApi } from '../../utils/GrpcApi'; 34 35 export type TableProps = { 36 productSummary: ProductSummary[]; 37 teams: string[]; 38 }; 39 40 export const TableFiltered: React.FC<TableProps> = (props) => { 41 const versionToDisplay = (app: ProductSummary): string => { 42 if (app.displayVersion !== '') { 43 return app.displayVersion; 44 } 45 if (app.commitId !== '') { 46 return app.commitId; 47 } 48 return app.version; 49 }; 50 const displayTeams = props.teams; 51 if (displayTeams.includes('<No Team>')) { 52 displayTeams.filter((team, index) => team !== '<No Team>'); 53 displayTeams.push(''); 54 } 55 return ( 56 <table className="table"> 57 <tbody> 58 <tr className="table_title"> 59 <th>App Name</th> 60 <th>Version</th> 61 <th>Environment</th> 62 <th>Team</th> 63 <th>ManifestRepoLink</th> 64 <th>SourceRepoLink</th> 65 </tr> 66 {props.productSummary 67 .filter((row, index) => props.teams.length <= 0 || displayTeams.includes(row.team)) 68 .map((sum) => ( 69 <tr key={sum.app + sum.environment} className="table_data"> 70 <td>{sum.app}</td> 71 <td>{versionToDisplay(sum)}</td> 72 <td>{sum.environment}</td> 73 <td>{sum.team}</td> 74 <td> 75 <DisplayManifestLink 76 app={sum.app} 77 version={Number(sum.version)} 78 displayString="Manifest Link" 79 /> 80 </td> 81 <td> 82 <DisplaySourceLink commitId={sum.commitId} displayString={'Source Link'} /> 83 </td> 84 </tr> 85 ))} 86 </tbody> 87 </table> 88 ); 89 }; 90 91 // splits up a string like "dev:dev-de" into ["dev", "dev-de"] 92 const splitCombinedGroupName = (envName: string): string[] => { 93 const splitter = envName.split('/'); 94 if (splitter.length === 1) { 95 return ['', splitter[0]]; 96 } 97 return [splitter[1], '']; 98 }; 99 100 const useEnvironmentGroupCombinations = (envGroupResponse: EnvironmentGroup[]): string[] => { 101 const envList: string[] = []; 102 for (let i = 0; i < envGroupResponse.length; i++) { 103 envList.push(envGroupResponse[i].environmentGroupName); 104 for (let j = 0; j < envGroupResponse[i].environments.length; j++) { 105 envList.push(envGroupResponse[i].environmentGroupName + '/' + envGroupResponse[i].environments[j].name); 106 } 107 } 108 return envList; 109 }; 110 111 export const ProductVersion: React.FC = () => { 112 React.useEffect(() => { 113 refreshTags(); 114 }, []); 115 const envGroupResponse = useEnvironmentGroups(); 116 const envList = useEnvironmentGroupCombinations(envGroupResponse); 117 const [searchParams, setSearchParams] = useSearchParams(); 118 const [environment, setEnvironment] = React.useState(searchParams.get('env') || envList[0]); 119 const [showSummary, setShowSummary] = useState(false); 120 const [summaryLoading, setSummaryLoading] = useState(false); 121 const [productSummaries, setProductSummaries] = useState(Array<ProductSummary>()); 122 const teams = (searchParams.get('teams') || '').split(',').filter((val) => val !== ''); 123 const [selectedTag, setSelectedTag] = React.useState(''); 124 const envsList = useEnvironments(); 125 const tagsResponse = useTags(); 126 127 const onChangeTag = React.useCallback( 128 (e: React.ChangeEvent<HTMLSelectElement>) => { 129 setSelectedTag(e.target.value); 130 searchParams.set('tag', e.target.value); 131 setSearchParams(searchParams); 132 }, 133 [searchParams, setSearchParams] 134 ); 135 React.useEffect(() => { 136 let tag = searchParams.get('tag'); 137 if (tag === null) { 138 if (tagsResponse.response.tagData.length === 0) return; 139 tag = tagsResponse.response.tagData[0].commitId; 140 if (tag === null) return; 141 setSelectedTag(tag); 142 searchParams.set('tag', tag); 143 setSearchParams(searchParams); 144 } 145 const env = splitCombinedGroupName(environment); 146 setShowSummary(true); 147 setSummaryLoading(true); 148 useApi 149 .gitService() 150 .GetProductSummary({ commitHash: tag, environment: env[0], environmentGroup: env[1] }) 151 .then((result: GetProductSummaryResponse) => { 152 setProductSummaries(result.productSummary); 153 }) 154 .catch((e) => { 155 showSnackbarError(e.message); 156 }); 157 setSummaryLoading(false); 158 }, [tagsResponse, envGroupResponse, environment, searchParams, setSearchParams]); 159 160 const changeEnv = React.useCallback( 161 (e: React.ChangeEvent<HTMLSelectElement>) => { 162 searchParams.set('env', e.target.value); 163 setEnvironment(e.target.value); 164 setSearchParams(searchParams); 165 }, 166 [setSearchParams, searchParams] 167 ); 168 const [showReleaseTrainEnvs, setShowReleaseTrainEnvs] = React.useState(false); 169 const handleClose = React.useCallback(() => { 170 setShowReleaseTrainEnvs(false); 171 }, []); 172 const openDialog = React.useCallback(() => { 173 setShowReleaseTrainEnvs(true); 174 }, []); 175 const confirmReleaseTrainFunction = React.useCallback( 176 (selectedEnvs: string[]) => { 177 if (teams.length < 1) { 178 selectedEnvs.forEach((env) => { 179 addAction({ 180 action: { 181 $case: 'releaseTrain', 182 releaseTrain: { target: env, commitHash: selectedTag, team: '' }, 183 }, 184 }); 185 }); 186 return; 187 } 188 if (teams.length > 1) { 189 showSnackbarError('Can only run one release train action at a time, should only select one team'); 190 return; 191 } 192 selectedEnvs.forEach((env) => { 193 addAction({ 194 action: { 195 $case: 'releaseTrain', 196 releaseTrain: { target: env, commitHash: selectedTag, team: teams[0] }, 197 }, 198 }); 199 }); 200 return; 201 }, 202 [selectedTag, teams] 203 ); 204 205 if (!tagsResponse.tagsReady) { 206 return <Spinner message="Loading Git Tags" />; 207 } 208 if (summaryLoading) { 209 return <Spinner message="Loading Production Version" />; 210 } 211 212 const dialog = ( 213 <EnvSelectionDialog 214 environments={envsList 215 .filter((env, index) => splitCombinedGroupName(environment)[0] === env.config?.upstream?.environment) 216 .map((env) => env.name)} 217 open={showReleaseTrainEnvs} 218 onCancel={handleClose} 219 onSubmit={confirmReleaseTrainFunction} 220 envSelectionDialog={false} 221 /> 222 ); 223 224 return ( 225 <div className="product_version"> 226 <h1 className="environment_name">{'Product Version Page'}</h1> 227 {dialog} 228 {tagsResponse.response.tagData.length > 0 ? ( 229 <div className="space_apart_row"> 230 <div className="dropdown_div"> 231 <select 232 onChange={onChangeTag} 233 className="drop_down" 234 data-testid="drop_down" 235 value={selectedTag}> 236 <option value="default" disabled> 237 Select a Tag 238 </option> 239 {tagsResponse.response.tagData.map((tag) => ( 240 <option value={tag.commitId} key={tag.tag}> 241 {tag.tag.slice(10)} 242 </option> 243 ))} 244 </select> 245 <select className="env_drop_down" onChange={changeEnv} value={environment}> 246 <option value="default" disabled> 247 Select an Environment or Environment Group 248 </option> 249 {envList.map((env) => ( 250 <option value={env} key={env}> 251 {env} 252 </option> 253 ))} 254 </select> 255 </div> 256 <Button label={'Run Release Train'} className="release_train_button" onClick={openDialog} /> 257 </div> 258 ) : ( 259 <div /> 260 )} 261 <div> 262 {showSummary ? ( 263 <div className="table_padding"> 264 <TableFiltered productSummary={productSummaries} teams={teams} /> 265 </div> 266 ) : ( 267 <div className="page_description"> 268 { 269 'This page shows the version of the product for the selected environment based on tags to the repository. If there are no tags, then no data can be shown.' 270 } 271 </div> 272 )} 273 </div> 274 </div> 275 ); 276 };