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  );