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