go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/milo/ui/src/build/pages/builder_page/builder_page.tsx (about) 1 // Copyright 2023 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 { GrpcError } from '@chopsui/prpc-client'; 16 import styled from '@emotion/styled'; 17 import { Alert, AlertTitle, Grid, LinearProgress } from '@mui/material'; 18 import { useQuery } from '@tanstack/react-query'; 19 import { useParams } from 'react-router-dom'; 20 21 import { RecoverableErrorBoundary } from '@/common/components/error_handling'; 22 import { PageMeta } from '@/common/components/page_meta'; 23 import { UiPage } from '@/common/constants/view'; 24 import { usePrpcServiceClient } from '@/common/hooks/prpc_query'; 25 import { parseLegacyBucketId } from '@/common/tools/build_utils'; 26 import { 27 BuilderMask_BuilderMaskType, 28 BuildersClientImpl, 29 GetBuilderRequest, 30 } from '@/proto/go.chromium.org/luci/buildbucket/proto/builder_service.pb'; 31 32 import { BuilderIdBar } from './builder_id_bar'; 33 import { EndedBuildsSection } from './ended_builds_section'; 34 import { MachinePoolSection } from './machine_pool_section'; 35 import { PendingBuildsSection } from './pending_builds_section'; 36 import { StartedBuildsSection } from './started_builds_section'; 37 import { BuilderDescriptionSection } from './summary_section'; 38 import { ViewsSection } from './views_section'; 39 40 const ErrorDisplay = styled.pre({ 41 whiteSpace: 'pre-wrap', 42 overflowWrap: 'break-word', 43 }); 44 45 export function BuilderPage() { 46 const { project, bucket, builder } = useParams(); 47 if (!project || !bucket || !builder) { 48 throw new Error( 49 'invariant violated: project, bucket, builder should be set', 50 ); 51 } 52 53 const builderId = { 54 project, 55 // If the bucket ID is a legacy ID, convert it to the new format. 56 // 57 // TODO(weiweilin): add a unit test once the pRPC query calls are 58 // simplified. 59 bucket: parseLegacyBucketId(bucket)?.bucket ?? bucket, 60 builder, 61 }; 62 63 const client = usePrpcServiceClient({ 64 host: SETTINGS.buildbucket.host, 65 ClientImpl: BuildersClientImpl, 66 }); 67 const { data, error, isLoading } = useQuery({ 68 ...client.GetBuilder.query( 69 GetBuilderRequest.fromPartial({ 70 id: builderId, 71 mask: { 72 type: BuilderMask_BuilderMaskType.ALL, 73 }, 74 }), 75 ), 76 select: (res) => ({ 77 swarmingHost: res.config!.swarmingHost, 78 // Convert dimensions to StringPair[] and remove expirations. 79 dimensions: 80 res.config!.dimensions?.map((dim) => { 81 const parts = dim.split(':', 3); 82 if (parts.length === 3) { 83 return { key: parts[1], value: parts[2] }; 84 } 85 return { key: parts[0], value: parts[1] }; 86 }) || [], 87 descriptionHtml: res.config!.descriptionHtml, 88 metadata: res.metadata, 89 // TODO guterman: check reported date and whether it's expired 90 }), 91 }); 92 93 if (error && !(error instanceof GrpcError)) { 94 throw error; 95 } 96 97 return ( 98 <> 99 <PageMeta 100 project={project} 101 selectedPage={UiPage.Builders} 102 title={`${builderId.builder} | Builder`} 103 /> 104 <BuilderIdBar 105 builderId={builderId} 106 healthStatus={data?.metadata?.health} 107 /> 108 <LinearProgress 109 value={100} 110 variant={isLoading ? 'indeterminate' : 'determinate'} 111 color="primary" 112 /> 113 <Grid container spacing={2} sx={{ padding: '0 16px' }}> 114 {error instanceof GrpcError && ( 115 <Grid item md={12}> 116 <Alert severity="warning" sx={{ mt: 2 }}> 117 <AlertTitle> 118 Failed to query the builder. If you can see recent builds, the 119 builder might have been deleted recently. 120 </AlertTitle> 121 <ErrorDisplay>{`Original Error:\n${error.message}`}</ErrorDisplay> 122 </Alert> 123 </Grid> 124 )} 125 {data?.descriptionHtml && ( 126 <Grid item md={12}> 127 <BuilderDescriptionSection descriptionHtml={data.descriptionHtml} /> 128 </Grid> 129 )} 130 {data?.swarmingHost && ( 131 <Grid item md={5}> 132 <MachinePoolSection 133 swarmingHost={data.swarmingHost} 134 dimensions={data.dimensions} 135 /> 136 </Grid> 137 )} 138 <Grid item md={2}> 139 <StartedBuildsSection builderId={builderId} /> 140 </Grid> 141 <Grid item md={2}> 142 <PendingBuildsSection builderId={builderId} /> 143 </Grid> 144 <Grid item md={3}> 145 <ViewsSection builderId={builderId} /> 146 </Grid> 147 <Grid item md={12}> 148 <EndedBuildsSection builderId={builderId} /> 149 </Grid> 150 </Grid> 151 </> 152 ); 153 } 154 155 export const element = ( 156 // See the documentation for `<LoginPage />` for why we handle error this way. 157 <RecoverableErrorBoundary key="builder"> 158 <BuilderPage /> 159 </RecoverableErrorBoundary> 160 );