go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/milo/ui/src/build/legacy/build_page/build_page.tsx (about) 1 // Copyright 2020 The LUCI Authors. 2 // 3 // Licensed under the Apache License, Version 2.0 (the "License"); 4 // you may not use this file except in compliance with the License. 5 // You may obtain a copy of the License at 6 // 7 // http://www.apache.org/licenses/LICENSE-2.0 8 // 9 // Unless required by applicable law or agreed to in writing, software 10 // distributed under the License is distributed on an "AS IS" BASIS, 11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 // See the License for the specific language governing permissions and 13 // limitations under the License. 14 15 import { css } from '@emotion/react'; 16 import { LinearProgress, Link } from '@mui/material'; 17 import { observer } from 'mobx-react-lite'; 18 import { useEffect } from 'react'; 19 import { Link as RouterLink, useParams } from 'react-router-dom'; 20 21 import grayFavicon from '@/common/assets/favicons/gray-32.png'; 22 import greenFavicon from '@/common/assets/favicons/green-32.png'; 23 import miloFavicon from '@/common/assets/favicons/milo-32.png'; 24 import purpleFavicon from '@/common/assets/favicons/purple-32.png'; 25 import redFavicon from '@/common/assets/favicons/red-32.png'; 26 import tealFavicon from '@/common/assets/favicons/teal-32.png'; 27 import yellowFavicon from '@/common/assets/favicons/yellow-32.png'; 28 import { RecoverableErrorBoundary } from '@/common/components/error_handling'; 29 import { usePageSpecificConfig } from '@/common/components/page_config_state_provider'; 30 import { PageMeta } from '@/common/components/page_meta/page_meta'; 31 import { AppRoutedTab, AppRoutedTabs } from '@/common/components/routed_tabs'; 32 import { 33 BUILD_STATUS_CLASS_MAP, 34 BUILD_STATUS_COLOR_THEME_MAP, 35 BUILD_STATUS_DISPLAY_MAP, 36 } from '@/common/constants/legacy'; 37 import { UiPage } from '@/common/constants/view'; 38 import { BuildbucketStatus } from '@/common/services/buildbucket'; 39 import { useStore } from '@/common/store'; 40 import { InvocationProvider } from '@/common/store/invocation_state'; 41 import { displayDuration, LONG_TIME_FORMAT } from '@/common/tools/time_utils'; 42 import { 43 getBuilderURLPath, 44 getLegacyBuildURLPath, 45 getProjectURLPath, 46 } from '@/common/tools/url_utils'; 47 48 import { CountIndicator } from '../../../test_verdict/legacy/test_results_tab/count_indicator'; 49 50 import { BuildLitEnvProvider } from './build_lit_env_provider'; 51 import { ChangeConfigDialog } from './change_config_dialog'; 52 import { BuildContextProvider } from './context'; 53 import { CustomBugLink } from './custom_bug_link'; 54 55 const STATUS_FAVICON_MAP = Object.freeze({ 56 [BuildbucketStatus.Scheduled]: grayFavicon, 57 [BuildbucketStatus.Started]: yellowFavicon, 58 [BuildbucketStatus.Success]: greenFavicon, 59 [BuildbucketStatus.Failure]: redFavicon, 60 [BuildbucketStatus.InfraFailure]: purpleFavicon, 61 [BuildbucketStatus.Canceled]: tealFavicon, 62 }); 63 64 const delimiter = css({ 65 borderLeft: '1px solid var(--divider-color)', 66 width: '1px', 67 marginLeft: '10px', 68 marginRight: '10px', 69 '& + &': { 70 display: 'none', 71 }, 72 }); 73 74 export const BuildPage = observer(() => { 75 const { project, bucket, builder, buildNumOrId } = useParams(); 76 const store = useStore(); 77 78 if (!project || !bucket || !builder || !buildNumOrId) { 79 throw new Error( 80 'invariant violated: project, bucket, builder, buildNumOrId should be set', 81 ); 82 } 83 84 const [showConfigDialog, setShowConfigDialog] = usePageSpecificConfig(); 85 86 useEffect(() => { 87 store.buildPage.setParams({ project, bucket, builder }, buildNumOrId); 88 }, [store, project, bucket, builder, buildNumOrId]); 89 90 const build = store.buildPage.build; 91 92 const status = build?.data?.status; 93 const statusDisplay = status ? BUILD_STATUS_DISPLAY_MAP[status] : 'loading'; 94 const documentTitle = `${statusDisplay} - ${builder} ${buildNumOrId}`; 95 96 const faviconUrl = build 97 ? STATUS_FAVICON_MAP[build.data.status] 98 : miloFavicon; 99 useEffect(() => { 100 document.getElementById('favicon')?.setAttribute('href', faviconUrl); 101 }, [faviconUrl]); 102 103 const handleSwitchVersion = ( 104 e: React.MouseEvent<HTMLAnchorElement, MouseEvent>, 105 ) => { 106 const switchVerTemporarily = 107 e.metaKey || e.shiftKey || e.ctrlKey || e.altKey; 108 109 if (switchVerTemporarily) { 110 return; 111 } 112 113 const expires = new Date( 114 Date.now() + 365 * 24 * 60 * 60 * 1000, 115 ).toUTCString(); 116 document.cookie = `showNewBuildPage=false; expires=${expires}; path=/`; 117 store.redirectSw?.unregister(); 118 }; 119 120 return ( 121 <BuildContextProvider build={store.buildPage.build?.data || null}> 122 <InvocationProvider value={store.buildPage.invocation}> 123 <BuildLitEnvProvider> 124 <PageMeta 125 project={project} 126 selectedPage={UiPage.Builders} 127 title={documentTitle} 128 /> 129 <ChangeConfigDialog 130 open={showConfigDialog} 131 onClose={() => setShowConfigDialog(false)} 132 /> 133 <div 134 css={{ 135 backgroundColor: 'var(--block-background-color)', 136 padding: '6px 16px', 137 display: 'flex', 138 }} 139 > 140 <div css={{ flex: '0 auto' }}> 141 <span css={{ color: 'var(--light-text-color)' }}>Build </span> 142 <Link component={RouterLink} to={getProjectURLPath(project)}> 143 {project} 144 </Link> 145 <span> / </span> 146 <span>{bucket}</span> 147 <span> / </span> 148 <Link 149 component={RouterLink} 150 to={getBuilderURLPath({ project, bucket, builder })} 151 > 152 {builder} 153 </Link> 154 <span> / </span> 155 <span>{buildNumOrId}</span> 156 </div> 157 <div css={delimiter}></div> 158 <CustomBugLink project={project} build={build?.data} /> 159 <div css={delimiter}></div> 160 <Link 161 onClick={handleSwitchVersion} 162 href={getLegacyBuildURLPath( 163 { project, bucket, builder }, 164 buildNumOrId, 165 )} 166 > 167 Switch to the legacy build page 168 </Link> 169 <div 170 css={{ 171 marginLeft: 'auto', 172 flex: '0 auto', 173 }} 174 > 175 {build && ( 176 <> 177 <i 178 className={`status ${ 179 BUILD_STATUS_CLASS_MAP[build.data.status] 180 }`} 181 > 182 {BUILD_STATUS_DISPLAY_MAP[build.data.status] || 183 'unknown status'}{' '} 184 </i> 185 {(() => { 186 switch (build.data.status) { 187 case BuildbucketStatus.Scheduled: 188 return `since ${build.createTime.toFormat( 189 LONG_TIME_FORMAT, 190 )}`; 191 case BuildbucketStatus.Started: 192 return `since ${build.startTime!.toFormat( 193 LONG_TIME_FORMAT, 194 )}`; 195 case BuildbucketStatus.Canceled: 196 return `after ${displayDuration( 197 build.endTime!.diff(build.createTime), 198 )} by ${build.data.canceledBy || 'unknown'}`; 199 case BuildbucketStatus.Failure: 200 case BuildbucketStatus.InfraFailure: 201 case BuildbucketStatus.Success: 202 return `after ${displayDuration( 203 build.endTime!.diff( 204 build.startTime || build.createTime, 205 ), 206 )}`; 207 default: 208 return ''; 209 } 210 })()} 211 </> 212 )} 213 </div> 214 </div> 215 <LinearProgress 216 value={100} 217 variant={build ? 'determinate' : 'indeterminate'} 218 color={ 219 build 220 ? BUILD_STATUS_COLOR_THEME_MAP[build.data.status] 221 : 'primary' 222 } 223 /> 224 <AppRoutedTabs> 225 <AppRoutedTab label="Overview" value="overview" to="overview" /> 226 <AppRoutedTab 227 label="Test Results" 228 value="test-results" 229 to="test-results" 230 hideWhenInactive={ 231 !store.buildPage.hasInvocation || 232 !store.buildPage.canReadTestVerdicts 233 } 234 icon={<CountIndicator />} 235 iconPosition="end" 236 /> 237 <AppRoutedTab 238 label="Steps & Logs" 239 value="steps" 240 to="steps" 241 hideWhenInactive={!store.buildPage.canReadFullBuild} 242 /> 243 <AppRoutedTab 244 label="Related Builds" 245 value="related-builds" 246 to="related-builds" 247 hideWhenInactive={!store.buildPage.canReadFullBuild} 248 /> 249 <AppRoutedTab 250 label="Timeline" 251 value="timeline" 252 to="timeline" 253 hideWhenInactive={!store.buildPage.canReadFullBuild} 254 /> 255 <AppRoutedTab 256 label="Blamelist" 257 value="blamelist" 258 to="blamelist" 259 hideWhenInactive={!store.buildPage.canReadFullBuild} 260 /> 261 </AppRoutedTabs> 262 </BuildLitEnvProvider> 263 </InvocationProvider> 264 </BuildContextProvider> 265 ); 266 }); 267 268 export const element = ( 269 // See the documentation for `<LoginPage />` for why we handle error this way. 270 <RecoverableErrorBoundary key="build-long-link"> 271 <BuildPage /> 272 </RecoverableErrorBoundary> 273 );