go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/milo/ui/src/build/pages/builder_group_page/builder_group_page.tsx (about)

     1  // Copyright 2024 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 { LinearProgress } from '@mui/material';
    16  import { useInfiniteQuery } from '@tanstack/react-query';
    17  import { useEffect, useMemo } from 'react';
    18  import { useParams } from 'react-router-dom';
    19  
    20  import { FilterableBuilderTable } from '@/build/components/filterable_builder_table';
    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 { useMiloInternalClient } from '@/common/hooks/prpc_clients';
    25  import { ListBuildersRequest } from '@/proto/go.chromium.org/luci/milo/proto/v1/rpc.pb';
    26  
    27  import { BuilderGroupIdBar } from './builder_group_id_bar';
    28  
    29  export function BuilderGroupPage() {
    30    const { project, group } = useParams();
    31    if (!project || !group) {
    32      throw new Error('invariant violated: project, group should be set');
    33    }
    34  
    35    const client = useMiloInternalClient();
    36    const { data, isLoading, error, isError, fetchNextPage, hasNextPage } =
    37      useInfiniteQuery(
    38        client.ListBuilders.queryPaged(
    39          ListBuildersRequest.fromPartial({
    40            project,
    41            group,
    42          }),
    43        ),
    44      );
    45  
    46    if (isError) {
    47      throw error;
    48    }
    49  
    50    // Keep loading until all pages have been loaded.
    51    useEffect(() => {
    52      if (isLoading || !hasNextPage) {
    53        return;
    54      }
    55      fetchNextPage();
    56    }, [isLoading, hasNextPage, data?.pages.length, fetchNextPage]);
    57  
    58    const builders = useMemo(
    59      () => data?.pages.flatMap((p) => p.builders.map((b) => b.id!)) || [],
    60      [data],
    61    );
    62  
    63    return (
    64      <>
    65        <PageMeta
    66          project={project}
    67          selectedPage={UiPage.Builders}
    68          title={`${project} | ${group} | Builders`}
    69        />
    70        <BuilderGroupIdBar project={project} group={group} />
    71        <LinearProgress
    72          value={100}
    73          variant={isLoading ? 'indeterminate' : 'determinate'}
    74          color="primary"
    75        />
    76        <FilterableBuilderTable
    77          builders={builders}
    78          // Each builder table row needs 3 SearchBuilds RPC. So the number should
    79          // be a multiple of 3 to achieve best results.
    80          // 9 is picked to achieve a balance between HTTP/server overhead and
    81          // RPC latency (< 1s). This can be adjust upwards once the SearchBuilds
    82          // RPC is optimized to support the builder table.
    83          maxBatchSize={9}
    84        />
    85      </>
    86    );
    87  }
    88  
    89  export const element = (
    90    // See the documentation for `<LoginPage />` for why we handle error this way.
    91    <RecoverableErrorBoundary key="builder-group">
    92      <BuilderGroupPage />
    93    </RecoverableErrorBoundary>
    94  );