This is an automated email from the ASF dual-hosted git repository.

vogievetsky pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/druid.git


The following commit(s) were added to refs/heads/master by this push:
     new 8232fb519d2 Web console: fix supervisor actions menu not working well 
with lots of incremental loading (#18023)
8232fb519d2 is described below

commit 8232fb519d22da583fc4a0b95c17988d6ee9b583
Author: Vadim Ogievetsky <[email protected]>
AuthorDate: Fri May 30 16:49:58 2025 +0100

    Web console: fix supervisor actions menu not working well with lots of 
incremental loading (#18023)
    
    * fix supervisor action menu
    
    * refactor service view to get load queues though context
    
    * always load segment_id
    
    * add 'Go to segments' action
    
    * work correctly with slashes in path
    
    * cleanup
    
    * fix tests
    
    * jump to first page when filter changes
    
    * fix same issue in segments view
    
    * more paginator display flexibility
    
    * show a count in supervisors view
    
    * show a count in segments view
    
    * cleanup
---
 web-console/package.json                           |   1 +
 .../table-filterable-cell.tsx                      |  13 +-
 web-console/src/console-application.tsx            |   2 +-
 .../src/dialogs/status-dialog/status-dialog.tsx    |   4 +-
 .../src/react-table/react-table-filters.spec.ts    |   8 +-
 web-console/src/react-table/react-table-filters.ts |  13 +-
 .../react-table-pagination.tsx                     |   4 +-
 .../views/datasources-view/datasources-view.tsx    |  10 +-
 .../src/views/lookups-view/lookups-view.tsx        |   4 +-
 .../__snapshots__/segments-view.spec.tsx.snap      |   2 +-
 .../src/views/segments-view/segments-view.tsx      |  84 ++-
 .../__snapshots__/services-view.spec.tsx.snap      | 544 +++++++--------
 .../fill-indicator.scss}                           |  52 +-
 .../fill-indicator.tsx}                            |  55 +-
 .../src/views/services-view/services-view.scss     |  24 -
 .../src/views/services-view/services-view.spec.tsx |  66 +-
 .../src/views/services-view/services-view.tsx      | 635 +++++++++---------
 .../__snapshots__/supervisors-view.spec.tsx.snap   | 575 ++++++++--------
 .../views/supervisors-view/supervisors-view.tsx    | 728 ++++++++++++---------
 19 files changed, 1434 insertions(+), 1390 deletions(-)

diff --git a/web-console/package.json b/web-console/package.json
index ca9c78c0538..7a30c853ec9 100644
--- a/web-console/package.json
+++ b/web-console/package.json
@@ -21,6 +21,7 @@
     "coverage": "jest --coverage src",
     "update-snapshots": "jest -u --config jest.config.js",
     "autofix": "npm run eslint-fix && npm run sasslint-fix && npm run 
prettify",
+    "typecheck": "tsc --noEmit",
     "eslint": "eslint",
     "eslint-fix": "npm run eslint -- --fix",
     "eslint-changed-only": "git diff --diff-filter=ACMR --cached --name-only | 
grep -E \\.tsx\\?$ | xargs ./node_modules/.bin/eslint",
diff --git 
a/web-console/src/components/table-filterable-cell/table-filterable-cell.tsx 
b/web-console/src/components/table-filterable-cell/table-filterable-cell.tsx
index c03f0038cc7..80d9cc83d59 100644
--- a/web-console/src/components/table-filterable-cell/table-filterable-cell.tsx
+++ b/web-console/src/components/table-filterable-cell/table-filterable-cell.tsx
@@ -22,7 +22,7 @@ import React from 'react';
 import type { Filter } from 'react-table';
 
 import type { FilterMode } from '../../react-table';
-import { addFilter, filterModeToIcon } from '../../react-table';
+import { addOrUpdateFilter, combineModeAndNeedle, filterModeToIcon } from 
'../../react-table';
 import { Deferred } from '../deferred/deferred';
 
 import './table-filterable-cell.scss';
@@ -57,7 +57,14 @@ export const TableFilterableCell = React.memo(function 
TableFilterableCell(
                   key={i}
                   icon={filterModeToIcon(mode)}
                   text={value}
-                  onClick={() => onFiltersChange(addFilter(filters, field, 
mode, value))}
+                  onClick={() =>
+                    onFiltersChange(
+                      addOrUpdateFilter(filters, {
+                        id: field,
+                        value: combineModeAndNeedle(mode, value),
+                      }),
+                    )
+                  }
                 />
               ))}
             </Menu>
@@ -65,7 +72,7 @@ export const TableFilterableCell = React.memo(function 
TableFilterableCell(
         />
       }
     >
-      {children}
+      {children ?? value}
     </Popover>
   );
 });
diff --git a/web-console/src/console-application.tsx 
b/web-console/src/console-application.tsx
index 5dfbba77101..dc858cd3111 100644
--- a/web-console/src/console-application.tsx
+++ b/web-console/src/console-application.tsx
@@ -61,7 +61,7 @@ function viewFilterChange(tab: HeaderActiveTab) {
 }
 
 function pathWithFilter(tab: HeaderActiveTab) {
-  return [`/${tab}/:filters`, `/${tab}`];
+  return `/${tab}/:filters?`;
 }
 
 function switchTab(tab: HeaderActiveTab) {
diff --git a/web-console/src/dialogs/status-dialog/status-dialog.tsx 
b/web-console/src/dialogs/status-dialog/status-dialog.tsx
index e87eb64a58c..e07a3c2e325 100644
--- a/web-console/src/dialogs/status-dialog/status-dialog.tsx
+++ b/web-console/src/dialogs/status-dialog/status-dialog.tsx
@@ -72,9 +72,7 @@ export const StatusDialog = React.memo(function 
StatusDialog(props: StatusDialog
             value={row.value}
             filters={moduleFilter}
             onFiltersChange={setModuleFilter}
-          >
-            {row.value}
-          </TableFilterableCell>
+          />
         );
       };
     };
diff --git a/web-console/src/react-table/react-table-filters.spec.ts 
b/web-console/src/react-table/react-table-filters.spec.ts
index 03737825cbd..c0a5476fcde 100644
--- a/web-console/src/react-table/react-table-filters.spec.ts
+++ b/web-console/src/react-table/react-table-filters.spec.ts
@@ -62,18 +62,18 @@ describe('react-table-utils', () => {
     expect(
       tableFiltersToString([
         { id: 'x', value: '~y' },
-        { id: 'z', value: '=w&' },
+        { id: 'z', value: '=w&%/' },
       ]),
-    ).toEqual('x~y&z=w%26');
+    ).toEqual('x~y&z=w%26%25%2F');
   });
 
   it('stringToTableFilters', () => {
     expect(stringToTableFilters(undefined)).toEqual([]);
     expect(stringToTableFilters('')).toEqual([]);
     expect(stringToTableFilters('x~y')).toEqual([{ id: 'x', value: '~y' }]);
-    expect(stringToTableFilters('x~y&z=w%26')).toEqual([
+    expect(stringToTableFilters('x~y&z=w%26%25%2F')).toEqual([
       { id: 'x', value: '~y' },
-      { id: 'z', value: '=w&' },
+      { id: 'z', value: '=w&%/' },
     ]);
     expect(stringToTableFilters('x<3&y<=3')).toEqual([
       { id: 'x', value: '<3' },
diff --git a/web-console/src/react-table/react-table-filters.ts 
b/web-console/src/react-table/react-table-filters.ts
index bb19dddae2d..3d446a210e7 100644
--- a/web-console/src/react-table/react-table-filters.ts
+++ b/web-console/src/react-table/react-table-filters.ts
@@ -84,15 +84,6 @@ interface FilterModeAndNeedle {
   needleParts: string[];
 }
 
-export function addFilter(
-  filters: readonly Filter[],
-  id: string,
-  mode: FilterMode,
-  needle: string,
-): Filter[] {
-  return addOrUpdateFilter(filters, { id, value: combineModeAndNeedle(mode, 
needle) });
-}
-
 export function parseFilterModeAndNeedle(
   filter: Filter,
   loose = false,
@@ -187,7 +178,7 @@ export function sqlQueryCustomTableFilters(filters: 
Filter[]): SqlExpression {
 
 export function tableFiltersToString(tableFilters: Filter[]): string {
   return tableFilters
-    .map(({ id, value }) => `${id}${value.replace(/[&%]/g, 
encodeURIComponent)}`)
+    .map(({ id, value }) => `${id}${value.replace(/[&%/]/g, 
encodeURIComponent)}`)
     .join('&');
 }
 
@@ -196,7 +187,7 @@ export function stringToTableFilters(str: string | 
undefined): Filter[] {
   // '~' | '=' | '!=' | '<' | '<=' | '>' | '>=';
   return filterMap(str.split('&'), clause => {
     const m = /^(\w+)((?:~|=|!=|<(?!=)|<=|>(?!=)|>=).*)$/.exec(
-      clause.replace(/%2[56]/g, decodeURIComponent),
+      clause.replace(/%2[56F]/g, decodeURIComponent),
     );
     if (!m) return;
     return { id: m[1], value: m[2] };
diff --git 
a/web-console/src/react-table/react-table-pagination/react-table-pagination.tsx 
b/web-console/src/react-table/react-table-pagination/react-table-pagination.tsx
index 634747dcd8b..2a21b0d61f1 100644
--- 
a/web-console/src/react-table/react-table-pagination/react-table-pagination.tsx
+++ 
b/web-console/src/react-table/react-table-pagination/react-table-pagination.tsx
@@ -109,8 +109,10 @@ export const ReactTablePagination = React.memo(function 
ReactTablePagination(
   } else {
     pageInfo += '...';
   }
-  if (ofText && nonEmptyArray(sortedData)) {
+  if (ofText === 'of' && nonEmptyArray(sortedData)) {
     pageInfo += ` ${ofText} ${formatInteger(sortedData.length)}`;
+  } else if (ofText) {
+    pageInfo += ` ${ofText}`;
   }
 
   const pageJumpMenuItem = renderPageJumpMenuItem();
diff --git a/web-console/src/views/datasources-view/datasources-view.tsx 
b/web-console/src/views/datasources-view/datasources-view.tsx
index 8ff4ec9f423..bf6ca1950d3 100644
--- a/web-console/src/views/datasources-view/datasources-view.tsx
+++ b/web-console/src/views/datasources-view/datasources-view.tsx
@@ -1004,7 +1004,7 @@ GROUP BY 1, 2`;
     rules: Rule[] | undefined,
     compactionInfo: CompactionInfo | undefined,
   ): BasicAction[] {
-    const { goToQuery, capabilities } = this.props;
+    const { goToQuery, goToSegments, capabilities } = this.props;
 
     const goToActions: BasicAction[] = [];
 
@@ -1016,6 +1016,14 @@ GROUP BY 1, 2`;
       });
     }
 
+    goToActions.push({
+      icon: IconNames.STACKED_CHART,
+      title: 'Go to segments',
+      onAction: () => {
+        goToSegments({ datasource });
+      },
+    });
+
     if (!capabilities.hasCoordinatorAccess()) {
       return goToActions;
     }
diff --git a/web-console/src/views/lookups-view/lookups-view.tsx 
b/web-console/src/views/lookups-view/lookups-view.tsx
index 39b7a920f1c..384305744e0 100644
--- a/web-console/src/views/lookups-view/lookups-view.tsx
+++ b/web-console/src/views/lookups-view/lookups-view.tsx
@@ -339,9 +339,7 @@ export class LookupsView extends 
React.PureComponent<LookupsViewProps, LookupsVi
           value={row.value}
           filters={filters}
           onFiltersChange={onFiltersChange}
-        >
-          {row.value}
-        </TableFilterableCell>
+        />
       );
     };
   }
diff --git 
a/web-console/src/views/segments-view/__snapshots__/segments-view.spec.tsx.snap 
b/web-console/src/views/segments-view/__snapshots__/segments-view.spec.tsx.snap
index e0b2847e2f7..0e6b64e844d 100755
--- 
a/web-console/src/views/segments-view/__snapshots__/segments-view.spec.tsx.snap
+++ 
b/web-console/src/views/segments-view/__snapshots__/segments-view.spec.tsx.snap
@@ -442,7 +442,7 @@ exports[`SegmentsView matches snapshot 1`] = `
       rowsText="rows"
       showPageJump={false}
       showPageSizeOptions={true}
-      showPagination={false}
+      showPagination={true}
       showPaginationBottom={true}
       showPaginationTop={false}
       sortable={true}
diff --git a/web-console/src/views/segments-view/segments-view.tsx 
b/web-console/src/views/segments-view/segments-view.tsx
index c60911a5e0c..4baebd6ffc2 100644
--- a/web-console/src/views/segments-view/segments-view.tsx
+++ b/web-console/src/views/segments-view/segments-view.tsx
@@ -56,9 +56,10 @@ import {
   STANDARD_TABLE_PAGE_SIZE_OPTIONS,
 } from '../../react-table';
 import { Api } from '../../singletons';
-import type { NumberLike, TableState } from '../../utils';
+import type { AuxiliaryQueryFn, NumberLike, TableState } from '../../utils';
 import {
   applySorting,
+  assemble,
   compact,
   countBy,
   filterMap,
@@ -74,6 +75,7 @@ import {
   queryDruidSql,
   QueryManager,
   QueryState,
+  ResultWithAuxiliaryWork,
   sortedToOrderByClause,
   twoLines,
 } from '../../utils';
@@ -217,6 +219,11 @@ interface SegmentQueryResultRow {
   is_overshadowed: number;
 }
 
+interface SegmentsWithAuxiliaryInfo {
+  readonly segments: SegmentQueryResultRow[];
+  readonly count: number;
+}
+
 export interface SegmentsViewProps {
   filters: Filter[];
   onFiltersChange(filters: Filter[]): void;
@@ -225,7 +232,7 @@ export interface SegmentsViewProps {
 }
 
 export interface SegmentsViewState {
-  segmentsState: QueryState<SegmentQueryResultRow[]>;
+  segmentsState: QueryState<SegmentsWithAuxiliaryInfo>;
   segmentTableActionDialogId?: string;
   datasourceTableActionDialogId?: string;
   actions: BasicAction[];
@@ -245,7 +252,7 @@ export interface SegmentsViewState {
 export class SegmentsView extends React.PureComponent<SegmentsViewProps, 
SegmentsViewState> {
   static baseQuery(visibleColumns: LocalStorageBackedVisibility) {
     const columns = compact([
-      visibleColumns.shown('Segment ID') && `"segment_id"`,
+      `"segment_id"`,
       visibleColumns.shown('Datasource') && `"datasource"`,
       `"start"`,
       `"end"`,
@@ -268,7 +275,7 @@ export class SegmentsView extends 
React.PureComponent<SegmentsViewProps, Segment
     return `WITH s AS (SELECT\n${columns.join(',\n')}\nFROM sys.segments)`;
   }
 
-  private readonly segmentsQueryManager: QueryManager<SegmentsQuery, 
SegmentQueryResultRow[]>;
+  private readonly segmentsQueryManager: QueryManager<SegmentsQuery, 
SegmentsWithAuxiliaryInfo>;
 
   constructor(props: SegmentsViewProps) {
     super(props);
@@ -296,6 +303,10 @@ export class SegmentsView extends 
React.PureComponent<SegmentsViewProps, Segment
         const { page, pageSize, filtered, sorted, visibleColumns, 
capabilities, groupByInterval } =
           query;
 
+        let segments: SegmentQueryResultRow[];
+        let count = -1;
+        const auxiliaryQueries: AuxiliaryQueryFn<SegmentsWithAuxiliaryInfo>[] 
= [];
+
         if (capabilities.hasSql()) {
           const whereExpression = segmentFiltersToExpression(filtered);
 
@@ -374,7 +385,27 @@ export class SegmentsView extends 
React.PureComponent<SegmentsViewProps, Segment
             }));
           }
 
-          return result as SegmentQueryResultRow[];
+          segments = result as SegmentQueryResultRow[];
+
+          auxiliaryQueries.push(async (segmentsWithAuxiliaryInfo, cancelToken) 
=> {
+            const sqlQuery = assemble(
+              'SELECT COUNT(*) AS "cnt"',
+              'FROM "sys"."segments"',
+              filterClause ? `WHERE ${filterClause}` : undefined,
+            ).join('\n');
+            const cnt: any = (
+              await queryDruidSql<{ cnt: number }>(
+                {
+                  query: sqlQuery,
+                },
+                cancelToken,
+              )
+            )[0].cnt;
+            return {
+              ...segmentsWithAuxiliaryInfo,
+              count: typeof cnt === 'number' ? cnt : -1,
+            };
+          });
         } else if (capabilities.hasCoordinatorAccess()) {
           let datasourceList: string[] = [];
           const datasourceFilter = filtered.find(({ id }) => id === 
'datasource');
@@ -428,11 +459,17 @@ export class SegmentsView extends 
React.PureComponent<SegmentsViewProps, Segment
             });
           }
 
+          count = results.length;
           const maxResults = (page + 1) * pageSize;
-          return applySorting(results, sorted).slice(page * pageSize, 
maxResults);
+          segments = applySorting(results, sorted).slice(page * pageSize, 
maxResults);
         } else {
           throw new Error('must have SQL or coordinator access to load this 
view');
         }
+
+        return new ResultWithAuxiliaryWork<SegmentsWithAuxiliaryInfo>(
+          { segments, count },
+          auxiliaryQueries,
+        );
       },
       onStateChange: segmentsState => {
         this.setState({
@@ -497,6 +534,17 @@ export class SegmentsView extends 
React.PureComponent<SegmentsViewProps, Segment
     });
   };
 
+  private readonly handleFilterChange = (filters: Filter[]) => {
+    this.goToFirstPage();
+    this.props.onFiltersChange(filters);
+  };
+
+  private goToFirstPage() {
+    if (this.state.page) {
+      this.setState({ page: 0 });
+    }
+  }
+
   private getSegmentActions(id: string, datasource: string): BasicAction[] {
     const actions: BasicAction[] = [];
     actions.push({
@@ -521,7 +569,8 @@ export class SegmentsView extends 
React.PureComponent<SegmentsViewProps, Segment
     enableComparisons = false,
     valueFn: (value: string) => ReactNode = String,
   ) {
-    const { filters, onFiltersChange } = this.props;
+    const { filters } = this.props;
+    const { handleFilterChange } = this;
 
     return function FilterableCell(row: { value: any }) {
       return (
@@ -529,7 +578,7 @@ export class SegmentsView extends 
React.PureComponent<SegmentsViewProps, Segment
           field={field}
           value={row.value}
           filters={filters}
-          onFiltersChange={onFiltersChange}
+          onFiltersChange={handleFilterChange}
           enableComparisons={enableComparisons}
         >
           {valueFn(row.value)}
@@ -539,7 +588,7 @@ export class SegmentsView extends 
React.PureComponent<SegmentsViewProps, Segment
   }
 
   renderSegmentsTable() {
-    const { capabilities, filters, onFiltersChange } = this.props;
+    const { capabilities, filters } = this.props;
     const {
       segmentsState,
       visibleColumns,
@@ -550,7 +599,10 @@ export class SegmentsView extends 
React.PureComponent<SegmentsViewProps, Segment
       showSegmentTimeline,
     } = this.state;
 
-    const segments = segmentsState.data || [];
+    const { segments, count } = segmentsState.data || {
+      segments: [],
+      count: -1,
+    };
 
     const sizeValues = segments.map(d => 
formatBytes(d.size)).concat('(realtime)');
 
@@ -570,7 +622,7 @@ export class SegmentsView extends 
React.PureComponent<SegmentsViewProps, Segment
     return (
       <ReactTable
         data={segments}
-        pages={10000000} // Dummy, we are hiding the page selector
+        pages={count >= 0 ? Math.ceil(count / pageSize) : 10000000}
         loading={segmentsState.loading}
         noDataText={
           segmentsState.isEmpty()
@@ -580,7 +632,7 @@ export class SegmentsView extends 
React.PureComponent<SegmentsViewProps, Segment
         manual
         filterable
         filtered={filters}
-        onFilteredChange={onFiltersChange}
+        onFilteredChange={this.handleFilterChange}
         sorted={sorted}
         onSortedChange={sorted => this.setState({ sorted })}
         page={page}
@@ -588,9 +640,9 @@ export class SegmentsView extends 
React.PureComponent<SegmentsViewProps, Segment
         pageSize={pageSize}
         onPageSizeChange={pageSize => this.setState({ pageSize })}
         pageSizeOptions={STANDARD_TABLE_PAGE_SIZE_OPTIONS}
-        showPagination={segments.length >= STANDARD_TABLE_PAGE_SIZE || page > 
0}
+        showPagination
         showPageJump={false}
-        ofText=""
+        ofText={count >= 0 ? `of ${formatInteger(count)}` : ''}
         pivotBy={groupByInterval ? ['interval'] : []}
         columns={[
           {
@@ -1013,7 +1065,7 @@ export class SegmentsView extends 
React.PureComponent<SegmentsViewProps, Segment
   }
 
   render() {
-    const { capabilities, filters, onFiltersChange } = this.props;
+    const { capabilities, filters } = this.props;
     const {
       segmentTableActionDialogId,
       datasourceTableActionDialogId,
@@ -1104,7 +1156,7 @@ export class SegmentsView extends 
React.PureComponent<SegmentsViewProps, Segment
                     small
                     rightIcon={IconNames.ARROW_DOWN}
                     onClick={() =>
-                      onFiltersChange(
+                      this.handleFilterChange(
                         compact([
                           start && { id: 'start', value: 
`>=${start.toISOString()}` },
                           end && { id: 'end', value: `<${end.toISOString()}` },
diff --git 
a/web-console/src/views/services-view/__snapshots__/services-view.spec.tsx.snap 
b/web-console/src/views/services-view/__snapshots__/services-view.spec.tsx.snap
index 2cf28b8b22c..329a7fd3562 100644
--- 
a/web-console/src/views/services-view/__snapshots__/services-view.spec.tsx.snap
+++ 
b/web-console/src/views/services-view/__snapshots__/services-view.spec.tsx.snap
@@ -66,283 +66,287 @@ exports[`ServicesView renders data 1`] = `
       tableColumnsHidden={[]}
     />
   </Memo(ViewControlBar)>
-  <ReactTable
-    AggregatedComponent={[Function]}
-    ExpanderComponent={[Function]}
-    FilterComponent={[Function]}
-    LoadingComponent={[Function]}
-    NoDataComponent={[Function]}
-    PadRowComponent={[Function]}
-    PaginationComponent={[Function]}
-    PivotValueComponent={[Function]}
-    ResizerComponent={[Function]}
-    TableComponent={[Function]}
-    TbodyComponent={[Function]}
-    TdComponent={[Function]}
-    TfootComponent={[Function]}
-    ThComponent={[Function]}
-    TheadComponent={[Function]}
-    TrComponent={[Function]}
-    TrGroupComponent={[Function]}
-    aggregatedKey="_aggregated"
-    className=""
-    collapseOnDataChange={true}
-    collapseOnPageChange={true}
-    collapseOnSortingChange={true}
-    column={
-      {
-        "Aggregated": undefined,
-        "Cell": undefined,
-        "Expander": undefined,
-        "Filter": undefined,
-        "Footer": undefined,
-        "Header": undefined,
-        "Pivot": undefined,
-        "PivotValue": undefined,
-        "Placeholder": undefined,
-        "aggregate": undefined,
-        "className": "",
-        "filterAll": false,
-        "filterMethod": undefined,
-        "filterable": undefined,
-        "footerClassName": "",
-        "footerStyle": {},
-        "getFooterProps": [Function],
-        "getHeaderProps": [Function],
-        "getProps": [Function],
-        "headerClassName": "",
-        "headerStyle": {},
-        "minResizeWidth": 11,
-        "minWidth": 100,
-        "resizable": undefined,
-        "show": true,
-        "sortMethod": undefined,
-        "sortable": undefined,
-        "style": {},
-      }
-    }
-    columns={
-      [
-        {
-          "Aggregated": [Function],
-          "Cell": [Function],
-          "Header": "Service",
-          "accessor": "service",
-          "show": true,
-          "width": 300,
-        },
-        {
-          "Cell": [Function],
-          "Filter": [Function],
-          "Header": "Type",
-          "accessor": "service_type",
-          "show": true,
-          "width": 150,
-        },
-        {
-          "Cell": [Function],
-          "Header": "Tier",
-          "accessor": [Function],
-          "id": "tier",
-          "show": true,
-          "width": 180,
-        },
-        {
-          "Aggregated": [Function],
-          "Cell": [Function],
-          "Header": "Host",
-          "accessor": "host",
-          "show": true,
-          "width": 200,
-        },
-        {
-          "Aggregated": [Function],
-          "Cell": [Function],
-          "Header": "Port",
-          "accessor": [Function],
-          "id": "port",
-          "show": true,
-          "width": 100,
-        },
-        {
-          "Aggregated": [Function],
-          "Cell": [Function],
-          "Header": "Current size",
-          "accessor": "curr_size",
-          "className": "padded",
-          "filterable": false,
-          "id": "curr_size",
-          "show": true,
-          "width": 100,
-        },
-        {
-          "Aggregated": [Function],
-          "Cell": [Function],
-          "Header": "Max size",
-          "accessor": "max_size",
-          "className": "padded",
-          "filterable": false,
-          "id": "max_size",
-          "show": true,
-          "width": 100,
-        },
-        {
-          "Aggregated": [Function],
-          "Cell": [Function],
-          "Header": "Usage",
-          "accessor": [Function],
-          "className": "padded",
-          "filterable": false,
-          "id": "usage",
-          "show": true,
-          "width": 140,
-        },
-        {
-          "Aggregated": [Function],
-          "Cell": [Function],
-          "Header": "Start time",
-          "accessor": "start_time",
-          "show": true,
-          "width": 200,
-        },
-        {
-          "Aggregated": [Function],
-          "Cell": [Function],
-          "Header": "Detail",
-          "accessor": [Function],
-          "className": "padded",
-          "filterable": false,
-          "id": "queue",
-          "show": true,
-          "width": 400,
-        },
+  <Context.Provider
+    value={{}}
+  >
+    <ReactTable
+      AggregatedComponent={[Function]}
+      ExpanderComponent={[Function]}
+      FilterComponent={[Function]}
+      LoadingComponent={[Function]}
+      NoDataComponent={[Function]}
+      PadRowComponent={[Function]}
+      PaginationComponent={[Function]}
+      PivotValueComponent={[Function]}
+      ResizerComponent={[Function]}
+      TableComponent={[Function]}
+      TbodyComponent={[Function]}
+      TdComponent={[Function]}
+      TfootComponent={[Function]}
+      ThComponent={[Function]}
+      TheadComponent={[Function]}
+      TrComponent={[Function]}
+      TrGroupComponent={[Function]}
+      aggregatedKey="_aggregated"
+      className=""
+      collapseOnDataChange={true}
+      collapseOnPageChange={true}
+      collapseOnSortingChange={true}
+      column={
         {
-          "Aggregated": [Function],
-          "Cell": [Function],
-          "Header": "Actions",
-          "accessor": [Function],
-          "filterable": false,
-          "id": "actions",
+          "Aggregated": undefined,
+          "Cell": undefined,
+          "Expander": undefined,
+          "Filter": undefined,
+          "Footer": undefined,
+          "Header": undefined,
+          "Pivot": undefined,
+          "PivotValue": undefined,
+          "Placeholder": undefined,
+          "aggregate": undefined,
+          "className": "",
+          "filterAll": false,
+          "filterMethod": undefined,
+          "filterable": undefined,
+          "footerClassName": "",
+          "footerStyle": {},
+          "getFooterProps": [Function],
+          "getHeaderProps": [Function],
+          "getProps": [Function],
+          "headerClassName": "",
+          "headerStyle": {},
+          "minResizeWidth": 11,
+          "minWidth": 100,
+          "resizable": undefined,
           "show": true,
-          "sortable": false,
-          "width": 70,
-        },
-      ]
-    }
-    data={
-      [
+          "sortMethod": undefined,
+          "sortable": undefined,
+          "style": {},
+        }
+      }
+      columns={
         [
           {
-            "curr_size": 0,
-            "host": "localhost",
-            "is_leader": 0,
-            "max_size": 0,
-            "plaintext_port": 8082,
-            "service": "localhost:8082",
-            "service_type": "broker",
-            "start_time": 0,
-            "tier": null,
-            "tls_port": -1,
+            "Aggregated": [Function],
+            "Cell": [Function],
+            "Header": "Service",
+            "accessor": "service",
+            "show": true,
+            "width": 300,
           },
           {
-            "curr_size": 179744287,
-            "host": "localhost",
-            "is_leader": 0,
-            "max_size": 3000000000n,
-            "plaintext_port": 8083,
-            "segmentsToDrop": 0,
-            "segmentsToDropSize": 0,
-            "segmentsToLoad": 0,
-            "segmentsToLoadSize": 0,
-            "service": "localhost:8083",
-            "service_type": "historical",
-            "start_time": 0,
-            "tier": "_default_tier",
-            "tls_port": -1,
+            "Cell": [Function],
+            "Filter": [Function],
+            "Header": "Type",
+            "accessor": "service_type",
+            "show": true,
+            "width": 150,
           },
-        ],
-      ]
-    }
-    defaultExpanded={{}}
-    defaultFilterMethod={[Function]}
-    defaultFiltered={[]}
-    defaultPage={0}
-    defaultPageSize={50}
-    defaultResized={[]}
-    defaultSortDesc={false}
-    defaultSortMethod={[Function]}
-    defaultSorted={[]}
-    expanderDefaults={
-      {
-        "filterable": false,
-        "resizable": false,
-        "sortable": false,
-        "width": 35,
+          {
+            "Cell": [Function],
+            "Header": "Tier",
+            "accessor": [Function],
+            "id": "tier",
+            "show": true,
+            "width": 180,
+          },
+          {
+            "Aggregated": [Function],
+            "Cell": [Function],
+            "Header": "Host",
+            "accessor": "host",
+            "show": true,
+            "width": 200,
+          },
+          {
+            "Aggregated": [Function],
+            "Cell": [Function],
+            "Header": "Port",
+            "accessor": [Function],
+            "id": "port",
+            "show": true,
+            "width": 100,
+          },
+          {
+            "Aggregated": [Function],
+            "Cell": [Function],
+            "Header": "Current size",
+            "accessor": "curr_size",
+            "className": "padded",
+            "filterable": false,
+            "id": "curr_size",
+            "show": true,
+            "width": 100,
+          },
+          {
+            "Aggregated": [Function],
+            "Cell": [Function],
+            "Header": "Max size",
+            "accessor": "max_size",
+            "className": "padded",
+            "filterable": false,
+            "id": "max_size",
+            "show": true,
+            "width": 100,
+          },
+          {
+            "Aggregated": [Function],
+            "Cell": [Function],
+            "Header": "Usage",
+            "accessor": [Function],
+            "className": "padded",
+            "filterable": false,
+            "id": "usage",
+            "show": true,
+            "width": 140,
+          },
+          {
+            "Aggregated": [Function],
+            "Cell": [Function],
+            "Header": "Start time",
+            "accessor": "start_time",
+            "show": true,
+            "width": 200,
+          },
+          {
+            "Aggregated": [Function],
+            "Cell": [Function],
+            "Header": "Detail",
+            "accessor": "service",
+            "className": "padded",
+            "filterable": false,
+            "id": "queue",
+            "show": true,
+            "width": 400,
+          },
+          {
+            "Aggregated": [Function],
+            "Cell": [Function],
+            "Header": "Actions",
+            "accessor": "service",
+            "filterable": false,
+            "id": "actions",
+            "show": true,
+            "sortable": false,
+            "width": 70,
+          },
+        ]
       }
-    }
-    filterable={true}
-    filtered={[]}
-    freezeWhenExpanded={false}
-    getLoadingProps={[Function]}
-    getNoDataProps={[Function]}
-    getPaginationProps={[Function]}
-    getProps={[Function]}
-    getResizerProps={[Function]}
-    getTableProps={[Function]}
-    getTbodyProps={[Function]}
-    getTdProps={[Function]}
-    getTfootProps={[Function]}
-    getTfootTdProps={[Function]}
-    getTfootTrProps={[Function]}
-    getTheadFilterProps={[Function]}
-    getTheadFilterThProps={[Function]}
-    getTheadFilterTrProps={[Function]}
-    getTheadGroupProps={[Function]}
-    getTheadGroupThProps={[Function]}
-    getTheadGroupTrProps={[Function]}
-    getTheadProps={[Function]}
-    getTheadThProps={[Function]}
-    getTheadTrProps={[Function]}
-    getTrGroupProps={[Function]}
-    getTrProps={[Function]}
-    groupedByPivotKey="_groupedByPivot"
-    indexKey="_index"
-    loading={false}
-    loadingText="Loading..."
-    multiSort={true}
-    nestingLevelKey="_nestingLevel"
-    nextText="Next"
-    noDataText=""
-    ofText="of"
-    onFetchData={[Function]}
-    onFilteredChange={[Function]}
-    originalKey="_original"
-    pageJumpText="jump to page"
-    pageSizeOptions={
-      [
-        50,
-        100,
-        200,
-      ]
-    }
-    pageText="Page"
-    pivotBy={[]}
-    pivotDefaults={{}}
-    pivotIDKey="_pivotID"
-    pivotValKey="_pivotVal"
-    previousText="Previous"
-    resizable={true}
-    resolveData={[Function]}
-    rowsSelectorText="rows per page"
-    rowsText="rows"
-    showPageJump={true}
-    showPageSizeOptions={true}
-    showPagination={false}
-    showPaginationBottom={true}
-    showPaginationTop={false}
-    sortable={true}
-    style={{}}
-    subRowsKey="_subRows"
-  />
+      data={
+        [
+          [
+            {
+              "curr_size": 0,
+              "host": "localhost",
+              "is_leader": 0,
+              "max_size": 0,
+              "plaintext_port": 8082,
+              "service": "localhost:8082",
+              "service_type": "broker",
+              "start_time": 0,
+              "tier": null,
+              "tls_port": -1,
+            },
+            {
+              "curr_size": 179744287,
+              "host": "localhost",
+              "is_leader": 0,
+              "max_size": 3000000000n,
+              "plaintext_port": 8083,
+              "segmentsToDrop": 0,
+              "segmentsToDropSize": 0,
+              "segmentsToLoad": 0,
+              "segmentsToLoadSize": 0,
+              "service": "localhost:8083",
+              "service_type": "historical",
+              "start_time": 0,
+              "tier": "_default_tier",
+              "tls_port": -1,
+            },
+          ],
+        ]
+      }
+      defaultExpanded={{}}
+      defaultFilterMethod={[Function]}
+      defaultFiltered={[]}
+      defaultPage={0}
+      defaultPageSize={50}
+      defaultResized={[]}
+      defaultSortDesc={false}
+      defaultSortMethod={[Function]}
+      defaultSorted={[]}
+      expanderDefaults={
+        {
+          "filterable": false,
+          "resizable": false,
+          "sortable": false,
+          "width": 35,
+        }
+      }
+      filterable={true}
+      filtered={[]}
+      freezeWhenExpanded={false}
+      getLoadingProps={[Function]}
+      getNoDataProps={[Function]}
+      getPaginationProps={[Function]}
+      getProps={[Function]}
+      getResizerProps={[Function]}
+      getTableProps={[Function]}
+      getTbodyProps={[Function]}
+      getTdProps={[Function]}
+      getTfootProps={[Function]}
+      getTfootTdProps={[Function]}
+      getTfootTrProps={[Function]}
+      getTheadFilterProps={[Function]}
+      getTheadFilterThProps={[Function]}
+      getTheadFilterTrProps={[Function]}
+      getTheadGroupProps={[Function]}
+      getTheadGroupThProps={[Function]}
+      getTheadGroupTrProps={[Function]}
+      getTheadProps={[Function]}
+      getTheadThProps={[Function]}
+      getTheadTrProps={[Function]}
+      getTrGroupProps={[Function]}
+      getTrProps={[Function]}
+      groupedByPivotKey="_groupedByPivot"
+      indexKey="_index"
+      loading={false}
+      loadingText="Loading..."
+      multiSort={true}
+      nestingLevelKey="_nestingLevel"
+      nextText="Next"
+      noDataText=""
+      ofText="of"
+      onFetchData={[Function]}
+      onFilteredChange={[Function]}
+      originalKey="_original"
+      pageJumpText="jump to page"
+      pageSizeOptions={
+        [
+          50,
+          100,
+          200,
+        ]
+      }
+      pageText="Page"
+      pivotBy={[]}
+      pivotDefaults={{}}
+      pivotIDKey="_pivotID"
+      pivotValKey="_pivotVal"
+      previousText="Previous"
+      resizable={true}
+      resolveData={[Function]}
+      rowsSelectorText="rows per page"
+      rowsText="rows"
+      showPageJump={true}
+      showPageSizeOptions={true}
+      showPagination={false}
+      showPaginationBottom={true}
+      showPaginationTop={false}
+      sortable={true}
+      style={{}}
+      subRowsKey="_subRows"
+    />
+  </Context.Provider>
 </div>
 `;
diff --git a/web-console/src/views/services-view/services-view.scss 
b/web-console/src/views/services-view/fill-indicator/fill-indicator.scss
similarity index 58%
copy from web-console/src/views/services-view/services-view.scss
copy to web-console/src/views/services-view/fill-indicator/fill-indicator.scss
index 6d1dcd0ef65..2ef5c9a40a6 100644
--- a/web-console/src/views/services-view/services-view.scss
+++ b/web-console/src/views/services-view/fill-indicator/fill-indicator.scss
@@ -16,48 +16,26 @@
  * limitations under the License.
  */
 
-@import '../../variables';
-
-.services-view {
-  height: 100%;
+.fill-indicator {
+  position: relative;
   width: 100%;
+  height: 18px;
+  background-color: #dadada;
+  border-radius: 2px;
 
-  .view-control-bar {
-    margin-bottom: $standard-padding;
+  .bar {
+    height: 100%;
+    background-color: #85cc00;
+    border-radius: 2px;
+    transition: all 0.2s ease-out;
   }
 
-  .ReactTable {
+  .label {
     position: absolute;
-    top: $view-control-bar-height + $standard-padding;
-    bottom: 0;
-    width: 100%;
-  }
-
-  ul {
-    line-height: 20px;
-  }
-
-  .fill-indicator {
-    position: relative;
+    height: 100%;
     width: 100%;
-    height: 18px;
-    background-color: #dadada;
-    border-radius: 2px;
-
-    .bar {
-      height: 100%;
-      background-color: #85cc00;
-      border-radius: 2px;
-      transition: all 0.2s ease-out;
-    }
-
-    .label {
-      position: absolute;
-      height: 100%;
-      width: 100%;
-      text-align: center;
-      top: 0;
-      color: #421890;
-    }
+    text-align: center;
+    top: 0;
+    color: #421890;
   }
 }
diff --git a/web-console/src/views/services-view/services-view.scss 
b/web-console/src/views/services-view/fill-indicator/fill-indicator.tsx
similarity index 52%
copy from web-console/src/views/services-view/services-view.scss
copy to web-console/src/views/services-view/fill-indicator/fill-indicator.tsx
index 6d1dcd0ef65..9403c8a32f1 100644
--- a/web-console/src/views/services-view/services-view.scss
+++ b/web-console/src/views/services-view/fill-indicator/fill-indicator.tsx
@@ -16,48 +16,19 @@
  * limitations under the License.
  */
 
-@import '../../variables';
+import './fill-indicator.scss';
 
-.services-view {
-  height: 100%;
-  width: 100%;
-
-  .view-control-bar {
-    margin-bottom: $standard-padding;
-  }
-
-  .ReactTable {
-    position: absolute;
-    top: $view-control-bar-height + $standard-padding;
-    bottom: 0;
-    width: 100%;
-  }
-
-  ul {
-    line-height: 20px;
-  }
-
-  .fill-indicator {
-    position: relative;
-    width: 100%;
-    height: 18px;
-    background-color: #dadada;
-    border-radius: 2px;
-
-    .bar {
-      height: 100%;
-      background-color: #85cc00;
-      border-radius: 2px;
-      transition: all 0.2s ease-out;
-    }
+export interface FillIndicatorProps {
+  value: number;
+}
 
-    .label {
-      position: absolute;
-      height: 100%;
-      width: 100%;
-      text-align: center;
-      top: 0;
-      color: #421890;
-    }
-  }
+export function FillIndicator({ value }: FillIndicatorProps) {
+  let formattedValue = (value * 100).toFixed(1);
+  if (formattedValue === '0.0' && value > 0) formattedValue = '~' + 
formattedValue;
+  return (
+    <div className="fill-indicator">
+      <div className="bar" style={{ width: `${value * 100}%` }} />
+      <div className="label">{formattedValue + '%'}</div>
+    </div>
+  );
 }
diff --git a/web-console/src/views/services-view/services-view.scss 
b/web-console/src/views/services-view/services-view.scss
index 6d1dcd0ef65..642b65c6683 100644
--- a/web-console/src/views/services-view/services-view.scss
+++ b/web-console/src/views/services-view/services-view.scss
@@ -36,28 +36,4 @@
   ul {
     line-height: 20px;
   }
-
-  .fill-indicator {
-    position: relative;
-    width: 100%;
-    height: 18px;
-    background-color: #dadada;
-    border-radius: 2px;
-
-    .bar {
-      height: 100%;
-      background-color: #85cc00;
-      border-radius: 2px;
-      transition: all 0.2s ease-out;
-    }
-
-    .label {
-      position: absolute;
-      height: 100%;
-      width: 100%;
-      text-align: center;
-      top: 0;
-      color: #421890;
-    }
-  }
 }
diff --git a/web-console/src/views/services-view/services-view.spec.tsx 
b/web-console/src/views/services-view/services-view.spec.tsx
index 0b2ff752576..35a147e6a2e 100644
--- a/web-console/src/views/services-view/services-view.spec.tsx
+++ b/web-console/src/views/services-view/services-view.spec.tsx
@@ -35,38 +35,42 @@ jest.mock('../../utils', () => {
     public runQuery() {
       this.onStateChange(
         new QueryState({
-          data: [
-            [
-              {
-                service: 'localhost:8082',
-                service_type: 'broker',
-                tier: null,
-                host: 'localhost',
-                plaintext_port: 8082,
-                tls_port: -1,
-                curr_size: 0,
-                max_size: 0,
-                is_leader: 0,
-                start_time: 0,
-              },
-              {
-                service: 'localhost:8083',
-                service_type: 'historical',
-                tier: '_default_tier',
-                host: 'localhost',
-                plaintext_port: 8083,
-                tls_port: -1,
-                curr_size: 179744287,
-                max_size: BigInt(3000000000),
-                is_leader: 0,
-                segmentsToLoad: 0,
-                segmentsToDrop: 0,
-                segmentsToLoadSize: 0,
-                segmentsToDropSize: 0,
-                start_time: 0,
-              },
+          data: {
+            services: [
+              [
+                {
+                  service: 'localhost:8082',
+                  service_type: 'broker',
+                  tier: null,
+                  host: 'localhost',
+                  plaintext_port: 8082,
+                  tls_port: -1,
+                  curr_size: 0,
+                  max_size: 0,
+                  is_leader: 0,
+                  start_time: 0,
+                },
+                {
+                  service: 'localhost:8083',
+                  service_type: 'historical',
+                  tier: '_default_tier',
+                  host: 'localhost',
+                  plaintext_port: 8083,
+                  tls_port: -1,
+                  curr_size: 179744287,
+                  max_size: BigInt(3000000000),
+                  is_leader: 0,
+                  segmentsToLoad: 0,
+                  segmentsToDrop: 0,
+                  segmentsToLoadSize: 0,
+                  segmentsToDropSize: 0,
+                  start_time: 0,
+                },
+              ],
             ],
-          ],
+            loadQueueInfo: {},
+            workerInfo: {},
+          },
         }) as any,
       );
     }
diff --git a/web-console/src/views/services-view/services-view.tsx 
b/web-console/src/views/services-view/services-view.tsx
index 58dca141034..ca962c545ee 100644
--- a/web-console/src/views/services-view/services-view.tsx
+++ b/web-console/src/views/services-view/services-view.tsx
@@ -19,8 +19,9 @@
 import { Button, ButtonGroup, Intent, Label, MenuItem, Tag } from 
'@blueprintjs/core';
 import { IconNames } from '@blueprintjs/icons';
 import { max, sum } from 'd3-array';
-import React from 'react';
-import type { Filter } from 'react-table';
+import memoize from 'memoize-one';
+import React, { createContext, useContext } from 'react';
+import type { Column, Filter } from 'react-table';
 import ReactTable from 'react-table';
 
 import {
@@ -67,6 +68,8 @@ import {
 } from '../../utils';
 import type { BasicAction } from '../../utils/basic-action';
 
+import { FillIndicator } from './fill-indicator/fill-indicator';
+
 import './services-view.scss';
 
 const TABLE_COLUMNS_BY_MODE: Record<CapabilitiesMode, 
TableColumnSelectorColumn[]> = {
@@ -119,7 +122,7 @@ export interface ServicesViewProps {
 }
 
 export interface ServicesViewState {
-  servicesState: QueryState<ServiceResultRow[]>;
+  servicesState: QueryState<ServicesWithAuxiliaryInfo>;
   groupServicesBy?: 'service_type' | 'tier';
 
   middleManagerDisableWorkerHost?: string;
@@ -139,10 +142,16 @@ interface ServiceResultRow {
   readonly plaintext_port: number;
   readonly tls_port: number;
   readonly start_time: string;
-  readonly loadQueueInfo?: LoadQueueInfo;
-  readonly workerInfo?: WorkerInfo;
 }
 
+interface ServicesWithAuxiliaryInfo {
+  readonly services: ServiceResultRow[];
+  readonly loadQueueInfo: Record<string, LoadQueueInfo>;
+  readonly workerInfo: Record<string, WorkerInfo>;
+}
+
+export const LoadQueueInfoContext = createContext<Record<string, 
LoadQueueInfo>>({});
+
 interface LoadQueueInfo {
   readonly segmentsToDrop: NumberLike;
   readonly segmentsToDropSize: NumberLike;
@@ -206,7 +215,7 @@ interface WorkerInfo {
 }
 
 export class ServicesView extends React.PureComponent<ServicesViewProps, 
ServicesViewState> {
-  private readonly serviceQueryManager: QueryManager<ServicesQuery, 
ServiceResultRow[]>;
+  private readonly serviceQueryManager: QueryManager<ServicesQuery, 
ServicesWithAuxiliaryInfo>;
 
   // Ranking
   //   coordinator => 8
@@ -284,10 +293,10 @@ ORDER BY
           throw new Error(`must have SQL or coordinator access`);
         }
 
-        const auxiliaryQueries: AuxiliaryQueryFn<ServiceResultRow[]>[] = [];
+        const auxiliaryQueries: AuxiliaryQueryFn<ServicesWithAuxiliaryInfo>[] 
= [];
 
-        if (capabilities.hasCoordinatorAccess() && 
visibleColumns.shown('Details')) {
-          auxiliaryQueries.push(async (services, cancelToken) => {
+        if (capabilities.hasCoordinatorAccess() && 
visibleColumns.shown('Detail')) {
+          auxiliaryQueries.push(async (servicesWithAuxiliaryInfo, cancelToken) 
=> {
             try {
               const loadQueueInfos = (
                 await Api.instance.get<Record<string, LoadQueueInfo>>(
@@ -295,23 +304,23 @@ ORDER BY
                   { cancelToken },
                 )
               ).data;
-              return services.map(s => ({
-                ...s,
-                loadQueueInfo: loadQueueInfos[s.service],
-              }));
+              return {
+                ...servicesWithAuxiliaryInfo,
+                loadQueueInfo: loadQueueInfos,
+              };
             } catch {
               AppToaster.show({
                 icon: IconNames.ERROR,
                 intent: Intent.DANGER,
                 message: 'There was an error getting the load queue info',
               });
-              return services;
+              return servicesWithAuxiliaryInfo;
             }
           });
         }
 
         if (capabilities.hasOverlordAccess()) {
-          auxiliaryQueries.push(async (services, cancelToken) => {
+          auxiliaryQueries.push(async (servicesWithAuxiliaryInfo, cancelToken) 
=> {
             try {
               const workerInfos = await getApiArray<WorkerInfo>(
                 '/druid/indexer/v1/workers',
@@ -323,10 +332,10 @@ ORDER BY
                 m => m.worker?.host,
               );
 
-              return services.map(s => ({
-                ...s,
-                workerInfo: workerInfoLookup[s.service],
-              }));
+              return {
+                ...servicesWithAuxiliaryInfo,
+                workerInfo: workerInfoLookup,
+              };
             } catch (e) {
               // Swallow this error because it simply a reflection of a local 
task runner.
               if (
@@ -338,12 +347,15 @@ ORDER BY
                   message: 'There was an error getting the worker info',
                 });
               }
-              return services;
+              return servicesWithAuxiliaryInfo;
             }
           });
         }
 
-        return new ResultWithAuxiliaryWork(services, auxiliaryQueries);
+        return new ResultWithAuxiliaryWork<ServicesWithAuxiliaryInfo>(
+          { services, loadQueueInfo: {}, workerInfo: {} },
+          auxiliaryQueries,
+        );
       },
       onStateChange: servicesState => {
         this.setState({
@@ -377,325 +389,324 @@ ORDER BY
           value={row.value}
           filters={filters}
           onFiltersChange={onFiltersChange}
-        >
-          {row.value}
-        </TableFilterableCell>
+        />
       );
     };
   }
 
   renderServicesTable() {
-    const { capabilities, filters, onFiltersChange } = this.props;
+    const { filters, onFiltersChange } = this.props;
     const { servicesState, groupServicesBy, visibleColumns } = this.state;
 
-    const fillIndicator = (value: number) => {
-      let formattedValue = (value * 100).toFixed(1);
-      if (formattedValue === '0.0' && value > 0) formattedValue = '~' + 
formattedValue;
-      return (
-        <div className="fill-indicator">
-          <div className="bar" style={{ width: `${value * 100}%` }} />
-          <div className="label">{formattedValue + '%'}</div>
-        </div>
-      );
+    const { services, loadQueueInfo, workerInfo } = servicesState.data || {
+      services: [],
+      loadQueueInfo: {},
+      workerInfo: {},
     };
 
-    const services = servicesState.data || [];
     return (
-      <ReactTable
-        data={services}
-        loading={servicesState.loading}
-        noDataText={
-          servicesState.isEmpty() ? 'No historicals' : 
servicesState.getErrorMessage() || ''
-        }
-        filterable
-        filtered={filters}
-        onFilteredChange={onFiltersChange}
-        pivotBy={groupServicesBy ? [groupServicesBy] : []}
-        defaultPageSize={STANDARD_TABLE_PAGE_SIZE}
-        pageSizeOptions={STANDARD_TABLE_PAGE_SIZE_OPTIONS}
-        showPagination={services.length > STANDARD_TABLE_PAGE_SIZE}
-        columns={[
-          {
-            Header: 'Service',
-            show: visibleColumns.shown('Service'),
-            accessor: 'service',
-            width: 300,
-            Cell: this.renderFilterableCell('service'),
-            Aggregated: () => '',
+      <LoadQueueInfoContext.Provider value={loadQueueInfo}>
+        <ReactTable
+          data={services}
+          loading={servicesState.loading}
+          noDataText={
+            servicesState.isEmpty() ? 'No services' : 
servicesState.getErrorMessage() || ''
+          }
+          filterable
+          filtered={filters}
+          onFilteredChange={onFiltersChange}
+          pivotBy={groupServicesBy ? [groupServicesBy] : []}
+          defaultPageSize={STANDARD_TABLE_PAGE_SIZE}
+          pageSizeOptions={STANDARD_TABLE_PAGE_SIZE_OPTIONS}
+          showPagination={services.length > STANDARD_TABLE_PAGE_SIZE}
+          columns={this.getTableColumns(visibleColumns, filters, 
onFiltersChange, workerInfo)}
+        />
+      </LoadQueueInfoContext.Provider>
+    );
+  }
+
+  private readonly getTableColumns = memoize(
+    (
+      visibleColumns: LocalStorageBackedVisibility,
+      _filters: Filter[],
+      _onFiltersChange: (filters: Filter[]) => void,
+      workerInfoLookup: Record<string, WorkerInfo>,
+    ): Column<ServiceResultRow>[] => {
+      const { capabilities } = this.props;
+      return [
+        {
+          Header: 'Service',
+          show: visibleColumns.shown('Service'),
+          accessor: 'service',
+          width: 300,
+          Cell: this.renderFilterableCell('service'),
+          Aggregated: () => '',
+        },
+        {
+          Header: 'Type',
+          show: visibleColumns.shown('Type'),
+          Filter: suggestibleFilterInput([
+            'coordinator',
+            'overlord',
+            'router',
+            'broker',
+            'historical',
+            'indexer',
+            'middle_manager',
+            'peon',
+          ]),
+          accessor: 'service_type',
+          width: 150,
+          Cell: this.renderFilterableCell('service_type'),
+        },
+        {
+          Header: 'Tier',
+          show: visibleColumns.shown('Tier'),
+          id: 'tier',
+          width: 180,
+          accessor: row => {
+            if (row.tier) return row.tier;
+            return workerInfoLookup[row.service]?.worker?.category;
           },
-          {
-            Header: 'Type',
-            show: visibleColumns.shown('Type'),
-            Filter: suggestibleFilterInput([
-              'coordinator',
-              'overlord',
-              'router',
-              'broker',
-              'historical',
-              'indexer',
-              'middle_manager',
-              'peon',
-            ]),
-            accessor: 'service_type',
-            width: 150,
-            Cell: this.renderFilterableCell('service_type'),
+          Cell: this.renderFilterableCell('tier'),
+        },
+        {
+          Header: 'Host',
+          show: visibleColumns.shown('Host'),
+          accessor: 'host',
+          width: 200,
+          Cell: this.renderFilterableCell('host'),
+          Aggregated: () => '',
+        },
+        {
+          Header: 'Port',
+          show: visibleColumns.shown('Port'),
+          id: 'port',
+          width: 100,
+          accessor: row => {
+            const ports: string[] = [];
+            if (row.plaintext_port !== -1) {
+              ports.push(`${row.plaintext_port} (plain)`);
+            }
+            if (row.tls_port !== -1) {
+              ports.push(`${row.tls_port} (TLS)`);
+            }
+            return ports.join(', ') || 'No port';
           },
-          {
-            Header: 'Tier',
-            show: visibleColumns.shown('Tier'),
-            id: 'tier',
-            width: 180,
-            accessor: row => {
-              if (row.tier) return row.tier;
-              return deepGet(row, 'workerInfo.worker.category');
-            },
-            Cell: this.renderFilterableCell('tier'),
+          Cell: this.renderFilterableCell('port'),
+          Aggregated: () => '',
+        },
+        {
+          Header: 'Current size',
+          show: visibleColumns.shown('Current size'),
+          id: 'curr_size',
+          width: 100,
+          filterable: false,
+          accessor: 'curr_size',
+          className: 'padded',
+          Aggregated: ({ subRows }) => {
+            const originalRows = subRows.map(r => r._original);
+            if (!originalRows.some(r => r.service_type === 'historical')) 
return '';
+            const totalCurr = sum(originalRows, s => s.curr_size);
+            return formatBytes(totalCurr);
           },
-          {
-            Header: 'Host',
-            show: visibleColumns.shown('Host'),
-            accessor: 'host',
-            width: 200,
-            Cell: this.renderFilterableCell('host'),
-            Aggregated: () => '',
+          Cell: ({ value, aggregated, original }) => {
+            if (aggregated || original.service_type !== 'historical') return 
'';
+            if (value === null) return '';
+            return formatBytes(value);
           },
-          {
-            Header: 'Port',
-            show: visibleColumns.shown('Port'),
-            id: 'port',
-            width: 100,
-            accessor: row => {
-              const ports: string[] = [];
-              if (row.plaintext_port !== -1) {
-                ports.push(`${row.plaintext_port} (plain)`);
-              }
-              if (row.tls_port !== -1) {
-                ports.push(`${row.tls_port} (TLS)`);
-              }
-              return ports.join(', ') || 'No port';
-            },
-            Cell: this.renderFilterableCell('port'),
-            Aggregated: () => '',
+        },
+        {
+          Header: 'Max size',
+          show: visibleColumns.shown('Max size'),
+          id: 'max_size',
+          width: 100,
+          filterable: false,
+          accessor: 'max_size',
+          className: 'padded',
+          Aggregated: ({ subRows }) => {
+            const originalRows = subRows.map(r => r._original);
+            if (!originalRows.some(r => r.service_type === 'historical')) 
return '';
+            const totalMax = sum(originalRows, s => s.max_size);
+            return formatBytes(totalMax);
           },
-          {
-            Header: 'Current size',
-            show: visibleColumns.shown('Current size'),
-            id: 'curr_size',
-            width: 100,
-            filterable: false,
-            accessor: 'curr_size',
-            className: 'padded',
-            Aggregated: ({ subRows }) => {
-              const originalRows = subRows.map(r => r._original);
-              if (!originalRows.some(r => r.service_type === 'historical')) 
return '';
-              const totalCurr = sum(originalRows, s => s.curr_size);
-              return formatBytes(totalCurr);
-            },
-            Cell: ({ value, aggregated, original }) => {
-              if (aggregated || original.service_type !== 'historical') return 
'';
-              if (value === null) return '';
-              return formatBytes(value);
-            },
+          Cell: ({ value, aggregated, original }) => {
+            if (aggregated || original.service_type !== 'historical') return 
'';
+            if (value === null) return '';
+            return formatBytes(value);
           },
-          {
-            Header: 'Max size',
-            show: visibleColumns.shown('Max size'),
-            id: 'max_size',
-            width: 100,
-            filterable: false,
-            accessor: 'max_size',
-            className: 'padded',
-            Aggregated: ({ subRows }) => {
-              const originalRows = subRows.map(r => r._original);
-              if (!originalRows.some(r => r.service_type === 'historical')) 
return '';
-              const totalMax = sum(originalRows, s => s.max_size);
-              return formatBytes(totalMax);
-            },
-            Cell: ({ value, aggregated, original }) => {
-              if (aggregated || original.service_type !== 'historical') return 
'';
-              if (value === null) return '';
-              return formatBytes(value);
-            },
+        },
+        {
+          Header: 'Usage',
+          show: visibleColumns.shown('Usage'),
+          id: 'usage',
+          width: 140,
+          filterable: false,
+          className: 'padded',
+          accessor: row => {
+            if (oneOf(row.service_type, 'middle_manager', 'indexer')) {
+              const workerInfo = workerInfoLookup[row.service];
+              if (!workerInfo) return 0;
+              return (
+                (Number(workerInfo.currCapacityUsed) || 0) / 
Number(workerInfo.worker?.capacity)
+              );
+            } else {
+              return row.max_size ? Number(row.curr_size) / 
Number(row.max_size) : null;
+            }
           },
-          {
-            Header: 'Usage',
-            show: visibleColumns.shown('Usage'),
-            id: 'usage',
-            width: 140,
-            filterable: false,
-            className: 'padded',
-            accessor: row => {
-              if (oneOf(row.service_type, 'middle_manager', 'indexer')) {
-                const { workerInfo } = row;
-                if (!workerInfo) return 0;
-                return (
-                  (Number(workerInfo.currCapacityUsed) || 0) / 
Number(workerInfo.worker?.capacity)
-                );
-              } else {
-                return row.max_size ? Number(row.curr_size) / 
Number(row.max_size) : null;
-              }
-            },
-            Aggregated: ({ subRows }) => {
-              const originalRows = subRows.map(r => r._original);
-
-              if (originalRows.some(r => r.service_type === 'historical')) {
-                const totalCurr = sum(originalRows, s => Number(s.curr_size));
-                const totalMax = sum(originalRows, s => Number(s.max_size));
-                return fillIndicator(totalCurr / totalMax);
-              } else if (
-                originalRows.some(
-                  r => r.service_type === 'indexer' || r.service_type === 
'middle_manager',
-                )
-              ) {
-                const workerInfos: WorkerInfo[] = filterMap(originalRows, r => 
r.workerInfo);
-                if (!workerInfos.length) return '';
-
-                const totalCurrCapacityUsed = sum(
-                  workerInfos,
-                  w => Number(w.currCapacityUsed) || 0,
-                );
-                const totalWorkerCapacity = sum(
-                  workerInfos,
-                  s => deepGet(s, 'worker.capacity') || 0,
-                );
-                return `Slots used: ${totalCurrCapacityUsed} of 
${totalWorkerCapacity}`;
-              } else {
-                return '';
-              }
-            },
-            Cell: ({ value, aggregated, original }) => {
-              if (aggregated) return '';
-              const { service_type } = original;
-              switch (service_type) {
-                case 'historical':
-                  return fillIndicator(value);
-
-                case 'indexer':
-                case 'middle_manager': {
-                  if (!deepGet(original, 'workerInfo')) return '';
-                  const currCapacityUsed = deepGet(original, 
'workerInfo.currCapacityUsed') || 0;
-                  const capacity = deepGet(original, 
'workerInfo.worker.capacity');
-                  if (typeof capacity === 'number') {
-                    return `Slots used: ${currCapacityUsed} of ${capacity}`;
-                  } else {
-                    return 'Slots used: -';
-                  }
-                }
+          Aggregated: ({ subRows }) => {
+            const originalRows = subRows.map(r => r._original);
+
+            if (originalRows.some(r => r.service_type === 'historical')) {
+              const totalCurr = sum(originalRows, s => Number(s.curr_size));
+              const totalMax = sum(originalRows, s => Number(s.max_size));
+              return <FillIndicator value={totalCurr / totalMax} />;
+            } else if (
+              originalRows.some(
+                r => r.service_type === 'indexer' || r.service_type === 
'middle_manager',
+              )
+            ) {
+              const workerInfos: WorkerInfo[] = filterMap(
+                originalRows,
+                r => workerInfoLookup[r.service],
+              );
 
-                default:
-                  return '';
-              }
-            },
-          },
-          {
-            Header: 'Start time',
-            show: visibleColumns.shown('Start time'),
-            accessor: 'start_time',
-            width: 200,
-            Cell: this.renderFilterableCell('start_time'),
-            Aggregated: () => '',
+              if (!workerInfos.length) return '';
+
+              const totalCurrCapacityUsed = sum(workerInfos, w => 
Number(w.currCapacityUsed) || 0);
+              const totalWorkerCapacity = sum(workerInfos, s => deepGet(s, 
'worker.capacity') || 0);
+              return `Slots used: ${totalCurrCapacityUsed} of 
${totalWorkerCapacity}`;
+            } else {
+              return '';
+            }
           },
-          {
-            Header: 'Detail',
-            show: visibleColumns.shown('Detail'),
-            id: 'queue',
-            width: 400,
-            filterable: false,
-            className: 'padded',
-            accessor: row => {
-              switch (row.service_type) {
-                case 'middle_manager':
-                case 'indexer': {
-                  const { workerInfo } = row;
-                  if (!workerInfo) return '';
-
-                  if (workerInfo.worker.version === '') return 'Disabled';
-
-                  const details: string[] = [];
-                  if (workerInfo.lastCompletedTaskTime) {
-                    details.push(
-                      `Last completed task: 
${prettyFormatIsoDateWithMsIfNeeded(
-                        workerInfo.lastCompletedTaskTime,
-                      )}`,
-                    );
-                  }
-                  if (workerInfo.blacklistedUntil) {
-                    details.push(
-                      `Blacklisted until: ${prettyFormatIsoDateWithMsIfNeeded(
-                        workerInfo.blacklistedUntil,
-                      )}`,
-                    );
-                  }
-                  return details.join(' ');
+          Cell: ({ value, aggregated, original }) => {
+            if (aggregated) return '';
+            const { service_type } = original;
+
+            switch (service_type) {
+              case 'historical':
+                return <FillIndicator value={value} />;
+
+              case 'indexer':
+              case 'middle_manager': {
+                const workerInfo = workerInfoLookup[original.service];
+                if (!workerInfo) return '';
+
+                const currCapacityUsed = workerInfo.currCapacityUsed || 0;
+                const capacity = deepGet(workerInfo, 'worker.capacity');
+                if (typeof capacity === 'number') {
+                  return `Slots used: ${currCapacityUsed} of ${capacity}`;
+                } else {
+                  return 'Slots used: -';
                 }
+              }
 
-                case 'coordinator':
-                case 'overlord':
-                  return row.is_leader === 1 ? 'Leader' : '';
-
-                case 'historical': {
-                  const { loadQueueInfo } = row;
-                  if (!loadQueueInfo) return 0;
-                  return (
-                    (Number(loadQueueInfo.segmentsToLoad) || 0) +
-                    (Number(loadQueueInfo.segmentsToDrop) || 0)
+              default:
+                return '';
+            }
+          },
+        },
+        {
+          Header: 'Start time',
+          show: visibleColumns.shown('Start time'),
+          accessor: 'start_time',
+          width: 200,
+          Cell: this.renderFilterableCell('start_time'),
+          Aggregated: () => '',
+        },
+        {
+          Header: 'Detail',
+          show: visibleColumns.shown('Detail'),
+          id: 'queue',
+          width: 400,
+          filterable: false,
+          className: 'padded',
+          accessor: 'service',
+          Cell: ({ original }) => {
+            const { service_type, service, is_leader } = original;
+            const loadQueueInfoContext = useContext(LoadQueueInfoContext);
+
+            switch (service_type) {
+              case 'middle_manager':
+              case 'indexer': {
+                const workerInfo = workerInfoLookup[service];
+                if (!workerInfo) return null;
+
+                if (workerInfo.worker.version === '') return 'Disabled';
+
+                const details: string[] = [];
+                if (workerInfo.lastCompletedTaskTime) {
+                  details.push(
+                    `Last completed task: ${prettyFormatIsoDateWithMsIfNeeded(
+                      workerInfo.lastCompletedTaskTime,
+                    )}`,
                   );
                 }
-
-                default:
-                  return 0;
-              }
-            },
-            Cell: ({ value, aggregated, original }) => {
-              if (aggregated) return '';
-              const { service_type } = original;
-              switch (service_type) {
-                case 'middle_manager':
-                case 'indexer':
-                case 'coordinator':
-                case 'overlord':
-                  return value;
-
-                case 'historical': {
-                  const { loadQueueInfo } = original;
-                  return loadQueueInfo ? formatLoadQueueInfo(loadQueueInfo) : 
'';
+                if (workerInfo.blacklistedUntil) {
+                  details.push(
+                    `Blacklisted until: ${prettyFormatIsoDateWithMsIfNeeded(
+                      workerInfo.blacklistedUntil,
+                    )}`,
+                  );
                 }
+                return details.join(' ') || null;
+              }
 
-                default:
-                  return '';
+              case 'coordinator':
+              case 'overlord':
+                return is_leader === 1 ? 'Leader' : '';
+
+              case 'historical': {
+                const loadQueueInfo = loadQueueInfoContext[service];
+                if (!loadQueueInfo) return null;
+
+                return formatLoadQueueInfo(loadQueueInfo);
               }
-            },
-            Aggregated: ({ subRows }) => {
-              const originalRows = subRows.map(r => r._original);
-              if (!originalRows.some(r => r.service_type === 'historical')) 
return '';
-              const loadQueueInfos: LoadQueueInfo[] = filterMap(originalRows, 
r => r.loadQueueInfo);
-              return loadQueueInfos.length
-                ? formatLoadQueueInfo(aggregateLoadQueueInfos(loadQueueInfos))
-                : '';
-            },
+
+              default:
+                return null;
+            }
           },
-          {
-            Header: ACTION_COLUMN_LABEL,
-            show: capabilities.hasOverlordAccess(),
-            id: ACTION_COLUMN_ID,
-            width: ACTION_COLUMN_WIDTH,
-            accessor: row => row.workerInfo,
-            filterable: false,
-            sortable: false,
-            Cell: ({ value, aggregated }) => {
-              if (aggregated) return '';
-              if (!value) return null;
-              const { worker } = value;
-              const disabled = worker.version === '';
-              const workerActions = this.getWorkerActions(worker.host, 
disabled);
-              return <ActionCell actions={workerActions} 
menuTitle={worker.host} />;
-            },
-            Aggregated: () => '',
+          Aggregated: ({ subRows }) => {
+            const loadQueueInfoContext = useContext(LoadQueueInfoContext);
+            const originalRows = subRows.map(r => r._original);
+            if (!originalRows.some(r => r.service_type === 'historical')) 
return '';
+
+            const loadQueueInfos: LoadQueueInfo[] = filterMap(
+              originalRows,
+              r => loadQueueInfoContext[r.service],
+            );
+
+            return loadQueueInfos.length
+              ? formatLoadQueueInfo(aggregateLoadQueueInfos(loadQueueInfos))
+              : '';
           },
-        ]}
-      />
-    );
-  }
+        },
+        {
+          Header: ACTION_COLUMN_LABEL,
+          show: capabilities.hasOverlordAccess(),
+          id: ACTION_COLUMN_ID,
+          width: ACTION_COLUMN_WIDTH,
+          accessor: 'service',
+          filterable: false,
+          sortable: false,
+          Cell: ({ value, aggregated }) => {
+            if (aggregated) return '';
+
+            const workerInfo = workerInfoLookup[value];
+            if (!workerInfo) return null;
+
+            const { worker } = workerInfo;
+            const disabled = worker.version === '';
+            const workerActions = this.getWorkerActions(worker.host, disabled);
+            return <ActionCell actions={workerActions} menuTitle={worker.host} 
/>;
+          },
+          Aggregated: () => '',
+        },
+      ];
+    },
+  );
 
   private getWorkerActions(workerHost: string, disabled: boolean): 
BasicAction[] {
     if (disabled) {
diff --git 
a/web-console/src/views/supervisors-view/__snapshots__/supervisors-view.spec.tsx.snap
 
b/web-console/src/views/supervisors-view/__snapshots__/supervisors-view.spec.tsx.snap
index 84b5cbe6e34..b94559423b6 100644
--- 
a/web-console/src/views/supervisors-view/__snapshots__/supervisors-view.spec.tsx.snap
+++ 
b/web-console/src/views/supervisors-view/__snapshots__/supervisors-view.spec.tsx.snap
@@ -97,321 +97,270 @@ exports[`SupervisorsView matches snapshot 1`] = `
       tableColumnsHidden={[]}
     />
   </Memo(ViewControlBar)>
-  <ReactTable
-    AggregatedComponent={[Function]}
-    ExpanderComponent={[Function]}
-    FilterComponent={[Function]}
-    LoadingComponent={[Function]}
-    NoDataComponent={[Function]}
-    PadRowComponent={[Function]}
-    PaginationComponent={[Function]}
-    PivotValueComponent={[Function]}
-    ResizerComponent={[Function]}
-    TableComponent={[Function]}
-    TbodyComponent={[Function]}
-    TdComponent={[Function]}
-    TfootComponent={[Function]}
-    ThComponent={[Function]}
-    TheadComponent={[Function]}
-    TrComponent={[Function]}
-    TrGroupComponent={[Function]}
-    aggregatedKey="_aggregated"
-    className=""
-    collapseOnDataChange={true}
-    collapseOnPageChange={true}
-    collapseOnSortingChange={true}
-    column={
-      {
-        "Aggregated": undefined,
-        "Cell": undefined,
-        "Expander": undefined,
-        "Filter": undefined,
-        "Footer": undefined,
-        "Header": undefined,
-        "Pivot": undefined,
-        "PivotValue": undefined,
-        "Placeholder": undefined,
-        "aggregate": undefined,
-        "className": "",
-        "filterAll": false,
-        "filterMethod": undefined,
-        "filterable": undefined,
-        "footerClassName": "",
-        "footerStyle": {},
-        "getFooterProps": [Function],
-        "getHeaderProps": [Function],
-        "getProps": [Function],
-        "headerClassName": "",
-        "headerStyle": {},
-        "minResizeWidth": 11,
-        "minWidth": 100,
-        "resizable": undefined,
-        "show": true,
-        "sortMethod": undefined,
-        "sortable": undefined,
-        "style": {},
-      }
-    }
-    columns={
-      [
-        {
-          "Cell": [Function],
-          "Header": <React.Fragment>
-            Supervisor ID
-            <br />
-            <i>
-              (datasource)
-            </i>
-          </React.Fragment>,
-          "accessor": "supervisor_id",
-          "id": "supervisor_id",
-          "show": true,
-          "width": 280,
-        },
-        {
-          "Cell": [Function],
-          "Header": "Type",
-          "accessor": "type",
-          "show": true,
-          "width": 120,
-        },
-        {
-          "Cell": [Function],
-          "Header": "Topic/Stream",
-          "accessor": "source",
-          "show": true,
-          "width": 200,
-        },
-        {
-          "Cell": [Function],
-          "Filter": [Function],
-          "Header": "Status",
-          "accessor": "detailed_state",
-          "id": "detailed_state",
-          "show": true,
-          "width": 170,
-        },
-        {
-          "Cell": [Function],
-          "Header": "Configured tasks",
-          "accessor": "spec",
-          "className": "padded",
-          "filterable": false,
-          "id": "configured_tasks",
-          "show": true,
-          "sortable": false,
-          "width": 130,
-        },
-        {
-          "Cell": [Function],
-          "Header": "Running tasks",
-          "accessor": "status.payload",
-          "filterable": false,
-          "id": "running_tasks",
-          "show": true,
-          "sortable": false,
-          "width": 150,
-        },
-        {
-          "Cell": [Function],
-          "Header": "Aggregate lag",
-          "accessor": "status.payload.aggregateLag",
-          "className": "padded",
-          "filterable": false,
-          "show": true,
-          "sortable": false,
-          "width": 200,
-        },
-        {
-          "Cell": [Function],
-          "Header": <React.Fragment>
-            Stats
-            <br />
-            <Blueprint5.Popover
-              boundary="clippingParents"
-              captureDismiss={false}
-              content={
-                <Blueprint5.Menu>
-                  <Blueprint5.MenuItem
-                    active={false}
-                    disabled={false}
-                    icon="circle"
-                    multiline={false}
-                    onClick={[Function]}
-                    popoverProps={{}}
-                    shouldDismissPopover={true}
-                    text="Rate over past 1 minute"
-                  />
-                  <Blueprint5.MenuItem
-                    active={false}
-                    disabled={false}
-                    icon="tick-circle"
-                    multiline={false}
-                    onClick={[Function]}
-                    popoverProps={{}}
-                    shouldDismissPopover={true}
-                    text="Rate over past 5 minutes"
-                  />
-                  <Blueprint5.MenuItem
-                    active={false}
-                    disabled={false}
-                    icon="circle"
-                    multiline={false}
-                    onClick={[Function]}
-                    popoverProps={{}}
-                    shouldDismissPopover={true}
-                    text="Rate over past 15 minutes"
-                  />
-                </Blueprint5.Menu>
-              }
-              defaultIsOpen={false}
-              disabled={false}
-              fill={false}
-              hasBackdrop={false}
-              hoverCloseDelay={300}
-              hoverOpenDelay={150}
-              inheritDarkTheme={true}
-              interactionKind="click"
-              matchTargetWidth={false}
-              minimal={false}
-              openOnTargetFocus={true}
-              position="bottom"
-              positioningStrategy="absolute"
-              shouldReturnFocusOnClose={false}
-              targetTagName="span"
-              transitionDuration={300}
-              usePortal={true}
-            >
-              <i
-                className="title-button"
-              >
-                Rate over past 5 minutes
-                 
-                <Blueprint5.Icon
-                  autoLoad={true}
-                  icon="caret-down"
-                  tagName="span"
-                />
-              </i>
-            </Blueprint5.Popover>
-          </React.Fragment>,
-          "accessor": "stats",
-          "className": "padded",
-          "filterable": false,
-          "id": "stats",
-          "show": true,
-          "sortable": false,
-          "width": 300,
-        },
-        {
-          "Cell": [Function],
-          "Header": "Recent errors",
-          "accessor": "status.payload.recentErrors",
-          "filterable": false,
-          "show": true,
-          "sortable": false,
-          "width": 150,
-        },
+  <Context.Provider
+    value={{}}
+  >
+    <Context.Provider
+      value={
         {
-          "Cell": [Function],
-          "Header": "Actions",
-          "accessor": "supervisor_id",
-          "filterable": false,
-          "id": "actions",
-          "sortable": false,
-          "width": 70,
-        },
-      ]
-    }
-    data={[]}
-    defaultExpanded={{}}
-    defaultFilterMethod={[Function]}
-    defaultFiltered={[]}
-    defaultPage={0}
-    defaultPageSize={20}
-    defaultResized={[]}
-    defaultSortDesc={false}
-    defaultSortMethod={[Function]}
-    defaultSorted={[]}
-    expanderDefaults={
-      {
-        "filterable": false,
-        "resizable": false,
-        "sortable": false,
-        "width": 35,
+          "stats": {},
+          "statsKey": "5m",
+        }
       }
-    }
-    filterable={true}
-    filtered={[]}
-    freezeWhenExpanded={false}
-    getLoadingProps={[Function]}
-    getNoDataProps={[Function]}
-    getPaginationProps={[Function]}
-    getProps={[Function]}
-    getResizerProps={[Function]}
-    getTableProps={[Function]}
-    getTbodyProps={[Function]}
-    getTdProps={[Function]}
-    getTfootProps={[Function]}
-    getTfootTdProps={[Function]}
-    getTfootTrProps={[Function]}
-    getTheadFilterProps={[Function]}
-    getTheadFilterThProps={[Function]}
-    getTheadFilterTrProps={[Function]}
-    getTheadGroupProps={[Function]}
-    getTheadGroupThProps={[Function]}
-    getTheadGroupTrProps={[Function]}
-    getTheadProps={[Function]}
-    getTheadThProps={[Function]}
-    getTheadTrProps={[Function]}
-    getTrGroupProps={[Function]}
-    getTrProps={[Function]}
-    groupedByPivotKey="_groupedByPivot"
-    indexKey="_index"
-    loading={true}
-    loadingText="Loading..."
-    manual={true}
-    multiSort={true}
-    nestingLevelKey="_nestingLevel"
-    nextText="Next"
-    noDataText=""
-    ofText=""
-    onFetchData={[Function]}
-    onFilteredChange={[Function]}
-    onPageChange={[Function]}
-    onPageSizeChange={[Function]}
-    onSortedChange={[Function]}
-    originalKey="_original"
-    page={0}
-    pageJumpText="jump to page"
-    pageSize={25}
-    pageSizeOptions={
-      [
-        25,
-        50,
-        100,
-      ]
-    }
-    pageText="Page"
-    pages={10000000}
-    pivotDefaults={{}}
-    pivotIDKey="_pivotID"
-    pivotValKey="_pivotVal"
-    previousText="Previous"
-    resizable={true}
-    resolveData={[Function]}
-    rowsSelectorText="rows per page"
-    rowsText="rows"
-    showPageJump={false}
-    showPageSizeOptions={true}
-    showPagination={false}
-    showPaginationBottom={true}
-    showPaginationTop={false}
-    sortable={true}
-    sorted={[]}
-    style={{}}
-    subRowsKey="_subRows"
-  />
+    >
+      <ReactTable
+        AggregatedComponent={[Function]}
+        ExpanderComponent={[Function]}
+        FilterComponent={[Function]}
+        LoadingComponent={[Function]}
+        NoDataComponent={[Function]}
+        PadRowComponent={[Function]}
+        PaginationComponent={[Function]}
+        PivotValueComponent={[Function]}
+        ResizerComponent={[Function]}
+        TableComponent={[Function]}
+        TbodyComponent={[Function]}
+        TdComponent={[Function]}
+        TfootComponent={[Function]}
+        ThComponent={[Function]}
+        TheadComponent={[Function]}
+        TrComponent={[Function]}
+        TrGroupComponent={[Function]}
+        aggregatedKey="_aggregated"
+        className=""
+        collapseOnDataChange={true}
+        collapseOnPageChange={true}
+        collapseOnSortingChange={true}
+        column={
+          {
+            "Aggregated": undefined,
+            "Cell": undefined,
+            "Expander": undefined,
+            "Filter": undefined,
+            "Footer": undefined,
+            "Header": undefined,
+            "Pivot": undefined,
+            "PivotValue": undefined,
+            "Placeholder": undefined,
+            "aggregate": undefined,
+            "className": "",
+            "filterAll": false,
+            "filterMethod": undefined,
+            "filterable": undefined,
+            "footerClassName": "",
+            "footerStyle": {},
+            "getFooterProps": [Function],
+            "getHeaderProps": [Function],
+            "getProps": [Function],
+            "headerClassName": "",
+            "headerStyle": {},
+            "minResizeWidth": 11,
+            "minWidth": 100,
+            "resizable": undefined,
+            "show": true,
+            "sortMethod": undefined,
+            "sortable": undefined,
+            "style": {},
+          }
+        }
+        columns={
+          [
+            {
+              "Cell": [Function],
+              "Header": <React.Fragment>
+                Supervisor ID
+                <br />
+                <i>
+                  (datasource)
+                </i>
+              </React.Fragment>,
+              "accessor": "supervisor_id",
+              "id": "supervisor_id",
+              "show": true,
+              "width": 280,
+            },
+            {
+              "Cell": [Function],
+              "Header": "Type",
+              "accessor": "type",
+              "show": true,
+              "width": 120,
+            },
+            {
+              "Cell": [Function],
+              "Header": "Topic/Stream",
+              "accessor": "source",
+              "show": true,
+              "width": 200,
+            },
+            {
+              "Cell": [Function],
+              "Filter": [Function],
+              "Header": "Status",
+              "accessor": "detailed_state",
+              "id": "detailed_state",
+              "show": true,
+              "width": 170,
+            },
+            {
+              "Cell": [Function],
+              "Header": "Configured tasks",
+              "accessor": "spec",
+              "className": "padded",
+              "filterable": false,
+              "id": "configured_tasks",
+              "show": true,
+              "sortable": false,
+              "width": 130,
+            },
+            {
+              "Cell": [Function],
+              "Header": "Running tasks",
+              "accessor": "supervisor_id",
+              "filterable": false,
+              "id": "running_tasks",
+              "show": true,
+              "sortable": false,
+              "width": 150,
+            },
+            {
+              "Cell": [Function],
+              "Header": "Aggregate lag",
+              "accessor": "supervisor_id",
+              "className": "padded",
+              "filterable": false,
+              "show": true,
+              "sortable": false,
+              "width": 200,
+            },
+            {
+              "Cell": [Function],
+              "Header": <React.Fragment>
+                Stats
+                <br />
+                <HeaderStatsKeySelector
+                  changeStatsKey={[Function]}
+                />
+              </React.Fragment>,
+              "accessor": "supervisor_id",
+              "className": "padded",
+              "filterable": false,
+              "id": "stats",
+              "show": true,
+              "sortable": false,
+              "width": 300,
+            },
+            {
+              "Cell": [Function],
+              "Header": "Recent errors",
+              "accessor": "supervisor_id",
+              "filterable": false,
+              "show": true,
+              "sortable": false,
+              "width": 150,
+            },
+            {
+              "Cell": [Function],
+              "Header": "Actions",
+              "accessor": "supervisor_id",
+              "filterable": false,
+              "id": "actions",
+              "sortable": false,
+              "width": 70,
+            },
+          ]
+        }
+        data={[]}
+        defaultExpanded={{}}
+        defaultFilterMethod={[Function]}
+        defaultFiltered={[]}
+        defaultPage={0}
+        defaultPageSize={20}
+        defaultResized={[]}
+        defaultSortDesc={false}
+        defaultSortMethod={[Function]}
+        defaultSorted={[]}
+        expanderDefaults={
+          {
+            "filterable": false,
+            "resizable": false,
+            "sortable": false,
+            "width": 35,
+          }
+        }
+        filterable={true}
+        filtered={[]}
+        freezeWhenExpanded={false}
+        getLoadingProps={[Function]}
+        getNoDataProps={[Function]}
+        getPaginationProps={[Function]}
+        getProps={[Function]}
+        getResizerProps={[Function]}
+        getTableProps={[Function]}
+        getTbodyProps={[Function]}
+        getTdProps={[Function]}
+        getTfootProps={[Function]}
+        getTfootTdProps={[Function]}
+        getTfootTrProps={[Function]}
+        getTheadFilterProps={[Function]}
+        getTheadFilterThProps={[Function]}
+        getTheadFilterTrProps={[Function]}
+        getTheadGroupProps={[Function]}
+        getTheadGroupThProps={[Function]}
+        getTheadGroupTrProps={[Function]}
+        getTheadProps={[Function]}
+        getTheadThProps={[Function]}
+        getTheadTrProps={[Function]}
+        getTrGroupProps={[Function]}
+        getTrProps={[Function]}
+        groupedByPivotKey="_groupedByPivot"
+        indexKey="_index"
+        loading={true}
+        loadingText="Loading..."
+        manual={true}
+        multiSort={true}
+        nestingLevelKey="_nestingLevel"
+        nextText="Next"
+        noDataText=""
+        ofText=""
+        onFetchData={[Function]}
+        onFilteredChange={[Function]}
+        onPageChange={[Function]}
+        onPageSizeChange={[Function]}
+        onSortedChange={[Function]}
+        originalKey="_original"
+        page={0}
+        pageJumpText="jump to page"
+        pageSize={25}
+        pageSizeOptions={
+          [
+            25,
+            50,
+            100,
+          ]
+        }
+        pageText="Page"
+        pages={10000000}
+        pivotDefaults={{}}
+        pivotIDKey="_pivotID"
+        pivotValKey="_pivotVal"
+        previousText="Previous"
+        resizable={true}
+        resolveData={[Function]}
+        rowsSelectorText="rows per page"
+        rowsText="rows"
+        showPageJump={false}
+        showPageSizeOptions={true}
+        showPagination={true}
+        showPaginationBottom={true}
+        showPaginationTop={false}
+        sortable={true}
+        sorted={[]}
+        style={{}}
+        subRowsKey="_subRows"
+      />
+    </Context.Provider>
+  </Context.Provider>
   <Memo(AlertDialog)
     confirmButtonText="OK"
     icon="error"
diff --git a/web-console/src/views/supervisors-view/supervisors-view.tsx 
b/web-console/src/views/supervisors-view/supervisors-view.tsx
index e8576865964..382066a7014 100644
--- a/web-console/src/views/supervisors-view/supervisors-view.tsx
+++ b/web-console/src/views/supervisors-view/supervisors-view.tsx
@@ -19,9 +19,10 @@
 import { Icon, Intent, Menu, MenuItem, Popover, Position, Tag } from 
'@blueprintjs/core';
 import { IconNames } from '@blueprintjs/icons';
 import * as JSONBig from 'json-bigint-native';
+import memoize from 'memoize-one';
 import type { JSX } from 'react';
-import React from 'react';
-import type { Filter, SortingRule } from 'react-table';
+import React, { createContext, useContext } from 'react';
+import type { Column, Filter, SortingRule } from 'react-table';
 import ReactTable from 'react-table';
 
 import type { TableColumnSelectorColumn } from '../../components';
@@ -41,14 +42,15 @@ import {
   AlertDialog,
   AsyncActionDialog,
   SpecDialog,
+  SupervisorResetOffsetsDialog,
   SupervisorTableActionDialog,
+  TaskGroupHandoffDialog,
 } from '../../dialogs';
-import { SupervisorResetOffsetsDialog } from 
'../../dialogs/supervisor-reset-offsets-dialog/supervisor-reset-offsets-dialog';
-import { TaskGroupHandoffDialog } from 
'../../dialogs/task-group-handoff-dialog/task-group-handoff-dialog';
 import type {
   IngestionSpec,
   QueryWithContext,
   RowStatsKey,
+  SupervisorStats,
   SupervisorStatus,
   SupervisorStatusTask,
 } from '../../druid-models';
@@ -64,7 +66,6 @@ import { Api, AppToaster } from '../../singletons';
 import type { AuxiliaryQueryFn, TableState } from '../../utils';
 import {
   assemble,
-  changeByIndex,
   checkedCircleIcon,
   deepGet,
   filterMap,
@@ -128,8 +129,48 @@ interface SupervisorQueryResultRow {
   readonly detailed_state: string;
   readonly spec?: IngestionSpec;
   readonly suspended: boolean;
-  readonly status?: SupervisorStatus;
-  readonly stats?: any;
+}
+
+interface SupervisorsWithAuxiliaryInfo {
+  readonly supervisors: SupervisorQueryResultRow[];
+  readonly count: number;
+  readonly status: Record<string, SupervisorStatus>;
+  readonly stats: Record<string, SupervisorStats>;
+}
+
+export const StatusContext = createContext<Record<string, 
SupervisorStatus>>({});
+export const StatsContext = createContext<{
+  stats: Record<string, SupervisorStats>;
+  statsKey: RowStatsKey;
+}>({ stats: {}, statsKey: '5m' });
+
+interface HeaderStatsKeySelectorProps {
+  changeStatsKey(statsKey: RowStatsKey): void;
+}
+
+function HeaderStatsKeySelector({ changeStatsKey }: 
HeaderStatsKeySelectorProps) {
+  const { statsKey } = useContext(StatsContext);
+  return (
+    <Popover
+      position={Position.BOTTOM}
+      content={
+        <Menu>
+          {ROW_STATS_KEYS.map(k => (
+            <MenuItem
+              key={k}
+              icon={checkedCircleIcon(k === statsKey)}
+              text={getRowStatsKeyTitle(k)}
+              onClick={() => changeStatsKey(k)}
+            />
+          ))}
+        </Menu>
+      }
+    >
+      <i className="title-button">
+        {getRowStatsKeyTitle(statsKey)} <Icon icon={IconNames.CARET_DOWN} />
+      </i>
+    </Popover>
+  );
 }
 
 export interface SupervisorsViewProps {
@@ -144,7 +185,7 @@ export interface SupervisorsViewProps {
 }
 
 export interface SupervisorsViewState {
-  supervisorsState: QueryState<SupervisorQueryResultRow[]>;
+  supervisorsState: QueryState<SupervisorsWithAuxiliaryInfo>;
   statsKey: RowStatsKey;
 
   resumeSupervisorId?: string;
@@ -209,7 +250,7 @@ export class SupervisorsView extends React.PureComponent<
 > {
   private readonly supervisorQueryManager: QueryManager<
     SupervisorQuery,
-    SupervisorQueryResultRow[]
+    SupervisorsWithAuxiliaryInfo
   >;
 
   constructor(props: SupervisorsViewProps) {
@@ -242,6 +283,8 @@ export class SupervisorsView extends React.PureComponent<
         setIntermediateQuery,
       ) => {
         let supervisors: SupervisorQueryResultRow[];
+        let count = -1;
+        const auxiliaryQueries: 
AuxiliaryQueryFn<SupervisorsWithAuxiliaryInfo>[] = [];
         if (capabilities.hasSql()) {
           const whereExpression = sqlQueryCustomTableFilters(filtered);
 
@@ -279,6 +322,26 @@ export class SupervisorsView extends React.PureComponent<
             if (typeof spec !== 'string') return supervisor;
             return { ...supervisor, spec: JSONBig.parse(spec) };
           });
+
+          auxiliaryQueries.push(async (supervisorsWithAuxiliaryInfo, 
cancelToken) => {
+            const sqlQuery = assemble(
+              'SELECT COUNT(*) AS "cnt"',
+              'FROM "sys"."supervisors"',
+              filterClause ? `WHERE ${filterClause}` : undefined,
+            ).join('\n');
+            const cnt: any = (
+              await queryDruidSql<{ cnt: number }>(
+                {
+                  query: sqlQuery,
+                },
+                cancelToken,
+              )
+            )[0].cnt;
+            return {
+              ...supervisorsWithAuxiliaryInfo,
+              count: typeof cnt === 'number' ? cnt : -1,
+            };
+          });
         } else if (capabilities.hasOverlordAccess()) {
           supervisors = (await 
getApiArray('/druid/indexer/v1/supervisor?full', cancelToken)).map(
             (sup: any) => {
@@ -296,6 +359,7 @@ export class SupervisorsView extends React.PureComponent<
               };
             },
           );
+          count = supervisors.length;
 
           const firstSorted = sorted[0];
           if (firstSorted) {
@@ -311,13 +375,12 @@ export class SupervisorsView extends React.PureComponent<
           throw new Error(`must have SQL or overlord access`);
         }
 
-        const auxiliaryQueries: AuxiliaryQueryFn<SupervisorQueryResultRow[]>[] 
= [];
         if (capabilities.hasOverlordAccess()) {
           if (visibleColumns.shown('Running tasks', 'Aggregate lag', 'Recent 
errors')) {
             auxiliaryQueries.push(
               ...supervisors.map(
-                (supervisor, i): AuxiliaryQueryFn<SupervisorQueryResultRow[]> 
=>
-                  async (rows, cancelToken) => {
+                (supervisor): AuxiliaryQueryFn<SupervisorsWithAuxiliaryInfo> =>
+                  async (supervisorsWithAuxiliaryInfo, cancelToken) => {
                     const status = (
                       await Api.instance.get(
                         `/druid/indexer/v1/supervisor/${Api.encodePath(
@@ -326,7 +389,13 @@ export class SupervisorsView extends React.PureComponent<
                         { cancelToken, timeout: STATUS_API_TIMEOUT },
                       )
                     ).data;
-                    return changeByIndex(rows, i, row => ({ ...row, status }));
+                    return {
+                      ...supervisorsWithAuxiliaryInfo,
+                      status: {
+                        ...supervisorsWithAuxiliaryInfo.status,
+                        [supervisor.supervisor_id]: status,
+                      },
+                    };
                   },
               ),
             );
@@ -336,9 +405,9 @@ export class SupervisorsView extends React.PureComponent<
             auxiliaryQueries.push(
               ...filterMap(
                 supervisors,
-                (supervisor, i): AuxiliaryQueryFn<SupervisorQueryResultRow[]> 
| undefined => {
+                (supervisor): AuxiliaryQueryFn<SupervisorsWithAuxiliaryInfo> | 
undefined => {
                   if (oneOf(supervisor.type, 'autocompact', 
'scheduled_batch')) return; // These supervisors do not report stats
-                  return async (rows, cancelToken) => {
+                  return async (supervisorsWithAuxiliaryInfo, cancelToken) => {
                     const stats = (
                       await Api.instance.get(
                         `/druid/indexer/v1/supervisor/${Api.encodePath(
@@ -347,7 +416,13 @@ export class SupervisorsView extends React.PureComponent<
                         { cancelToken, timeout: STATS_API_TIMEOUT },
                       )
                     ).data;
-                    return changeByIndex(rows, i, row => ({ ...row, stats }));
+                    return {
+                      ...supervisorsWithAuxiliaryInfo,
+                      stats: {
+                        ...supervisorsWithAuxiliaryInfo.stats,
+                        [supervisor.supervisor_id]: stats,
+                      },
+                    };
                   };
                 },
               ),
@@ -355,7 +430,10 @@ export class SupervisorsView extends React.PureComponent<
           }
         }
 
-        return new ResultWithAuxiliaryWork(supervisors, auxiliaryQueries);
+        return new ResultWithAuxiliaryWork<SupervisorsWithAuxiliaryInfo>(
+          { supervisors, count, status: {}, stats: {} },
+          auxiliaryQueries,
+        );
       },
       onStateChange: supervisorsState => {
         this.setState({
@@ -402,6 +480,17 @@ export class SupervisorsView extends React.PureComponent<
     });
   };
 
+  private readonly handleFilterChange = (filters: Filter[]) => {
+    this.goToFirstPage();
+    this.props.onFiltersChange(filters);
+  };
+
+  private goToFirstPage() {
+    if (this.state.page) {
+      this.setState({ page: 0 });
+    }
+  }
+
   private readonly closeSpecDialogs = () => {
     this.setState({
       supervisorSpecDialogOpen: false,
@@ -662,7 +751,8 @@ export class SupervisorsView extends React.PureComponent<
   }
 
   private renderSupervisorFilterableCell(field: string) {
-    const { filters, onFiltersChange } = this.props;
+    const { filters } = this.props;
+    const { handleFilterChange } = this;
 
     return function SupervisorFilterableCell(row: { value: any }) {
       return (
@@ -670,10 +760,8 @@ export class SupervisorsView extends React.PureComponent<
           field={field}
           value={row.value}
           filters={filters}
-          onFiltersChange={onFiltersChange}
-        >
-          {row.value}
-        </TableFilterableCell>
+          onFiltersChange={handleFilterChange}
+        />
       );
     };
   }
@@ -711,314 +799,320 @@ export class SupervisorsView extends 
React.PureComponent<
   }
 
   private renderSupervisorTable() {
-    const { filters, onFiltersChange } = this.props;
-    const { supervisorsState, statsKey, visibleColumns, page, pageSize, sorted 
} = this.state;
+    const { filters } = this.props;
+    const { supervisorsState, page, pageSize, sorted, statsKey, visibleColumns 
} = this.state;
 
-    const supervisors = supervisorsState.data || [];
+    const { supervisors, count, status, stats } = supervisorsState.data || {
+      supervisors: [],
+      count: -1,
+      status: {},
+      stats: {},
+    };
     return (
-      <ReactTable
-        data={supervisors}
-        pages={10000000} // Dummy, we are hiding the page selector
-        loading={supervisorsState.loading}
-        noDataText={
-          supervisorsState.isEmpty() ? 'No supervisors' : 
supervisorsState.getErrorMessage() || ''
-        }
-        manual
-        filterable
-        filtered={filters}
-        onFilteredChange={onFiltersChange}
-        sorted={sorted}
-        onSortedChange={sorted => this.setState({ sorted })}
-        page={page}
-        onPageChange={page => this.setState({ page })}
-        pageSize={pageSize}
-        onPageSizeChange={pageSize => this.setState({ pageSize })}
-        pageSizeOptions={SMALL_TABLE_PAGE_SIZE_OPTIONS}
-        showPagination={supervisors.length >= SMALL_TABLE_PAGE_SIZE || page > 
0}
-        showPageJump={false}
-        ofText=""
-        columns={[
-          {
-            Header: twoLines('Supervisor ID', <i>(datasource)</i>),
-            id: 'supervisor_id',
-            accessor: 'supervisor_id',
-            width: 280,
-            show: visibleColumns.shown('Supervisor ID'),
-            Cell: ({ value, original }) => (
-              <TableClickableCell
-                tooltip="Show detail"
-                onClick={() => this.onSupervisorDetail(original)}
-                hoverIcon={IconNames.SEARCH_TEMPLATE}
-              >
+      <StatusContext.Provider value={status}>
+        <StatsContext.Provider value={{ stats, statsKey }}>
+          <ReactTable
+            data={supervisors}
+            pages={count >= 0 ? Math.ceil(count / pageSize) : 10000000} // We 
are hiding the page selector
+            loading={supervisorsState.loading}
+            noDataText={
+              supervisorsState.isEmpty()
+                ? 'No supervisors'
+                : supervisorsState.getErrorMessage() || ''
+            }
+            manual
+            filterable
+            filtered={filters}
+            onFilteredChange={this.handleFilterChange}
+            sorted={sorted}
+            onSortedChange={sorted => this.setState({ sorted })}
+            page={page}
+            onPageChange={page => this.setState({ page })}
+            pageSize={pageSize}
+            onPageSizeChange={pageSize => this.setState({ pageSize })}
+            pageSizeOptions={SMALL_TABLE_PAGE_SIZE_OPTIONS}
+            showPagination
+            showPageJump={false}
+            ofText={count >= 0 ? `of ${formatInteger(count)}` : ''}
+            columns={this.getTableColumns(visibleColumns, filters)}
+          />
+        </StatsContext.Provider>
+      </StatusContext.Provider>
+    );
+  }
+
+  private readonly getTableColumns = memoize(
+    (
+      visibleColumns: LocalStorageBackedVisibility,
+      filters: Filter[],
+    ): Column<SupervisorQueryResultRow>[] => {
+      return [
+        {
+          Header: twoLines('Supervisor ID', <i>(datasource)</i>),
+          id: 'supervisor_id',
+          accessor: 'supervisor_id',
+          width: 280,
+          show: visibleColumns.shown('Supervisor ID'),
+          Cell: ({ value, original }) => (
+            <TableClickableCell
+              tooltip="Show detail"
+              onClick={() => this.onSupervisorDetail(original)}
+              hoverIcon={IconNames.SEARCH_TEMPLATE}
+            >
+              {value}
+            </TableClickableCell>
+          ),
+        },
+        {
+          Header: 'Type',
+          accessor: 'type',
+          width: 120,
+          Cell: this.renderSupervisorFilterableCell('type'),
+          show: visibleColumns.shown('Type'),
+        },
+        {
+          Header: 'Topic/Stream',
+          accessor: 'source',
+          width: 200,
+          Cell: this.renderSupervisorFilterableCell('source'),
+          show: visibleColumns.shown('Topic/Stream'),
+        },
+        {
+          Header: 'Status',
+          id: 'detailed_state',
+          width: 170,
+          Filter: suggestibleFilterInput([
+            'CONNECTING_TO_STREAM',
+            'CREATING_TASKS',
+            'DISCOVERING_INITIAL_TASKS',
+            'IDLE',
+            'INVALID_SPEC',
+            'LOST_CONTACT_WITH_STREAM',
+            'PENDING',
+            'RUNNING',
+            'SCHEDULER_STOPPED',
+            'STOPPING',
+            'SUSPENDED',
+            'UNABLE_TO_CONNECT_TO_STREAM',
+            'UNHEALTHY_SUPERVISOR',
+            'UNHEALTHY_TASKS',
+          ]),
+          accessor: 'detailed_state',
+          Cell: ({ value }) => (
+            <TableFilterableCell
+              field="detailed_state"
+              value={value}
+              filters={filters}
+              onFiltersChange={this.handleFilterChange}
+            >
+              <span>
+                <span style={{ color: detailedStateToColor(value) 
}}>&#x25cf;&nbsp;</span>
                 {value}
-              </TableClickableCell>
-            ),
-          },
-          {
-            Header: 'Type',
-            accessor: 'type',
-            width: 120,
-            Cell: this.renderSupervisorFilterableCell('type'),
-            show: visibleColumns.shown('Type'),
-          },
-          {
-            Header: 'Topic/Stream',
-            accessor: 'source',
-            width: 200,
-            Cell: this.renderSupervisorFilterableCell('source'),
-            show: visibleColumns.shown('Topic/Stream'),
-          },
-          {
-            Header: 'Status',
-            id: 'detailed_state',
-            width: 170,
-            Filter: suggestibleFilterInput([
-              'CONNECTING_TO_STREAM',
-              'CREATING_TASKS',
-              'DISCOVERING_INITIAL_TASKS',
-              'IDLE',
-              'INVALID_SPEC',
-              'LOST_CONTACT_WITH_STREAM',
-              'PENDING',
-              'RUNNING',
-              'SCHEDULER_STOPPED',
-              'STOPPING',
-              'SUSPENDED',
-              'UNABLE_TO_CONNECT_TO_STREAM',
-              'UNHEALTHY_SUPERVISOR',
-              'UNHEALTHY_TASKS',
-            ]),
-            accessor: 'detailed_state',
-            Cell: ({ value }) => (
-              <TableFilterableCell
-                field="detailed_state"
-                value={value}
-                filters={filters}
-                onFiltersChange={onFiltersChange}
-              >
-                <span>
-                  <span style={{ color: detailedStateToColor(value) 
}}>&#x25cf;&nbsp;</span>
-                  {value}
-                </span>
-              </TableFilterableCell>
-            ),
-            show: visibleColumns.shown('Status'),
-          },
-          {
-            Header: 'Configured tasks',
-            id: 'configured_tasks',
-            width: 130,
-            accessor: 'spec',
-            filterable: false,
-            sortable: false,
-            className: 'padded',
-            Cell: ({ value }) => {
-              if (!value) return null;
-              const taskCount = deepGet(value, 'spec.ioConfig.taskCount');
-              const replicas = deepGet(value, 'spec.ioConfig.replicas');
-              if (typeof taskCount !== 'number' || typeof replicas !== 
'number') return null;
-              return (
-                <div>
-                  <div>{formatInteger(taskCount * replicas)}</div>
-                  <div className="detail-line">
-                    {replicas === 1
-                      ? '(no replication)'
-                      : `(${pluralIfNeeded(taskCount, 'task')} × 
${pluralIfNeeded(
-                          replicas,
-                          'replica',
-                        )})`}
-                  </div>
+              </span>
+            </TableFilterableCell>
+          ),
+          show: visibleColumns.shown('Status'),
+        },
+        {
+          Header: 'Configured tasks',
+          id: 'configured_tasks',
+          width: 130,
+          accessor: 'spec',
+          filterable: false,
+          sortable: false,
+          className: 'padded',
+          Cell: ({ value }) => {
+            if (!value) return null;
+            const taskCount = deepGet(value, 'spec.ioConfig.taskCount');
+            const replicas = deepGet(value, 'spec.ioConfig.replicas');
+            if (typeof taskCount !== 'number' || typeof replicas !== 'number') 
return null;
+            return (
+              <div>
+                <div>{formatInteger(taskCount * replicas)}</div>
+                <div className="detail-line">
+                  {replicas === 1
+                    ? '(no replication)'
+                    : `(${pluralIfNeeded(taskCount, 'task')} × 
${pluralIfNeeded(
+                        replicas,
+                        'replica',
+                      )})`}
                 </div>
-              );
-            },
-            show: visibleColumns.shown('Configured tasks'),
+              </div>
+            );
           },
-          {
-            Header: 'Running tasks',
-            id: 'running_tasks',
-            width: 150,
-            accessor: 'status.payload',
-            filterable: false,
-            sortable: false,
-            Cell: ({ value, original }) => {
-              if (original.suspended) return;
-              let label: string | JSX.Element;
-              const { activeTasks, publishingTasks } = value || {};
-              if (Array.isArray(activeTasks)) {
-                label = pluralIfNeeded(activeTasks.length, 'active task');
-                if (nonEmptyArray(publishingTasks)) {
-                  label = (
-                    <>
-                      <div>{label}</div>
-                      <div>{pluralIfNeeded(publishingTasks.length, 'publishing 
task')}</div>
-                    </>
-                  );
-                }
-              } else {
-                label = '';
+          show: visibleColumns.shown('Configured tasks'),
+        },
+        {
+          Header: 'Running tasks',
+          id: 'running_tasks',
+          accessor: 'supervisor_id',
+          width: 150,
+          filterable: false,
+          sortable: false,
+          Cell: ({ value, original }) => {
+            const status = useContext(StatusContext);
+            if (original.suspended) return;
+            const { activeTasks, publishingTasks } = status[value]?.payload || 
{};
+            let label: string | JSX.Element;
+            if (Array.isArray(activeTasks)) {
+              label = pluralIfNeeded(activeTasks.length, 'active task');
+              if (nonEmptyArray(publishingTasks)) {
+                label = (
+                  <>
+                    <div>{label}</div>
+                    <div>{pluralIfNeeded(publishingTasks.length, 'publishing 
task')}</div>
+                  </>
+                );
               }
+            } else {
+              label = '';
+            }
 
-              return (
-                <TableClickableCell
-                  tooltip="Go to tasks"
-                  onClick={() => this.goToTasksForSupervisor(original)}
-                  hoverIcon={IconNames.ARROW_TOP_RIGHT}
-                >
-                  {label}
-                </TableClickableCell>
-              );
-            },
-            show: visibleColumns.shown('Running tasks'),
+            return (
+              <TableClickableCell
+                tooltip="Go to tasks"
+                onClick={() => this.goToTasksForSupervisor(original)}
+                hoverIcon={IconNames.ARROW_TOP_RIGHT}
+              >
+                {label}
+              </TableClickableCell>
+            );
           },
-          {
-            Header: 'Aggregate lag',
-            accessor: 'status.payload.aggregateLag',
-            width: 200,
-            filterable: false,
-            sortable: false,
-            className: 'padded',
-            show: visibleColumns.shown('Aggregate lag'),
-            Cell: ({ value }) => (isNumberLike(value) ? formatInteger(value) : 
null),
+          show: visibleColumns.shown('Running tasks'),
+        },
+        {
+          Header: 'Aggregate lag',
+          accessor: 'supervisor_id',
+          width: 200,
+          filterable: false,
+          sortable: false,
+          className: 'padded',
+          show: visibleColumns.shown('Aggregate lag'),
+          Cell: ({ value }) => {
+            const status = useContext(StatusContext);
+            const aggregateLag = status[value]?.payload?.aggregateLag;
+            return isNumberLike(aggregateLag) ? formatInteger(aggregateLag) : 
null;
           },
-          {
-            Header: twoLines(
-              'Stats',
-              <Popover
-                position={Position.BOTTOM}
-                content={
-                  <Menu>
-                    {ROW_STATS_KEYS.map(k => (
-                      <MenuItem
-                        key={k}
-                        icon={checkedCircleIcon(k === statsKey)}
-                        text={getRowStatsKeyTitle(k)}
-                        onClick={() => {
-                          this.setState({ statsKey: k });
-                        }}
-                      />
-                    ))}
-                  </Menu>
-                }
-              >
-                <i className="title-button">
-                  {getRowStatsKeyTitle(statsKey)} <Icon 
icon={IconNames.CARET_DOWN} />
-                </i>
-              </Popover>,
-            ),
-            id: 'stats',
-            width: 300,
-            filterable: false,
-            sortable: false,
-            className: 'padded',
-            accessor: 'stats',
-            Cell: ({ value, original }) => {
-              if (!value) return;
-              const activeTasks: SupervisorStatusTask[] | undefined = deepGet(
-                original,
-                'status.payload.activeTasks',
-              );
-              const activeTaskIds: string[] | undefined = 
Array.isArray(activeTasks)
-                ? activeTasks.map((t: SupervisorStatusTask) => t.id)
-                : undefined;
-
-              const c = getTotalSupervisorStats(value, statsKey, 
activeTaskIds);
-              const seconds = getRowStatsKeySeconds(statsKey);
-              const issues =
-                (c.processedWithError || 0) + (c.thrownAway || 0) + 
(c.unparseable || 0);
-              const totalLabel = `Total (past ${statsKey}): `;
-              return issues ? (
-                <div>
-                  <div
-                    data-tooltip={`${totalLabel}${formatBytes(c.processedBytes 
* seconds)}`}
-                  >{`Input: ${formatByteRate(c.processedBytes)}`}</div>
-                  {Boolean(c.processed) && (
-                    <div
-                      data-tooltip={`${totalLabel}${formatInteger(c.processed 
* seconds)} events`}
-                    >{`Processed: ${formatRate(c.processed)}`}</div>
-                  )}
-                  {Boolean(c.processedWithError) && (
-                    <div
-                      className="warning-line"
-                      data-tooltip={`${totalLabel}${formatInteger(
-                        c.processedWithError * seconds,
-                      )} events`}
-                    >
-                      Processed with error: {formatRate(c.processedWithError)}
-                    </div>
-                  )}
-                  {Boolean(c.thrownAway) && (
-                    <div
-                      className="warning-line"
-                      data-tooltip={`${totalLabel}${formatInteger(c.thrownAway 
* seconds)} events`}
-                    >
-                      Thrown away: {formatRate(c.thrownAway)}
-                    </div>
-                  )}
-                  {Boolean(c.unparseable) && (
-                    <div
-                      className="warning-line"
-                      
data-tooltip={`${totalLabel}${formatInteger(c.unparseable * seconds)} events`}
-                    >
-                      Unparseable: {formatRate(c.unparseable)}
-                    </div>
-                  )}
-                </div>
-              ) : c.processedBytes ? (
+        },
+        {
+          Header: twoLines(
+            'Stats',
+            <HeaderStatsKeySelector changeStatsKey={statsKey => 
this.setState({ statsKey })} />,
+          ),
+          id: 'stats',
+          accessor: 'supervisor_id',
+          width: 300,
+          filterable: false,
+          sortable: false,
+          className: 'padded',
+          show: visibleColumns.shown('Stats'),
+          Cell: ({ value, original }) => {
+            const { stats, statsKey } = useContext(StatsContext);
+            const s = stats[value];
+            if (!s) return;
+            const activeTasks: SupervisorStatusTask[] | undefined = deepGet(
+              original,
+              'status.payload.activeTasks',
+            );
+            const activeTaskIds: string[] | undefined = 
Array.isArray(activeTasks)
+              ? activeTasks.map((t: SupervisorStatusTask) => t.id)
+              : undefined;
+
+            const c = getTotalSupervisorStats(s, statsKey, activeTaskIds);
+            const seconds = getRowStatsKeySeconds(statsKey);
+            const issues = (c.processedWithError || 0) + (c.thrownAway || 0) + 
(c.unparseable || 0);
+            const totalLabel = `Total (past ${statsKey}): `;
+            return issues ? (
+              <div>
                 <div
-                  data-tooltip={`${totalLabel}${formatInteger(
-                    c.processed * seconds,
-                  )} events, ${formatBytes(c.processedBytes * seconds)}`}
-                >{`Processed: ${formatRate(c.processed)} (${formatByteRate(
-                  c.processedBytes,
-                )})`}</div>
-              ) : (
-                <div>No activity</div>
-              );
-            },
-            show: visibleColumns.shown('Stats'),
+                  data-tooltip={`${totalLabel}${formatBytes(c.processedBytes * 
seconds)}`}
+                >{`Input: ${formatByteRate(c.processedBytes)}`}</div>
+                {Boolean(c.processed) && (
+                  <div
+                    data-tooltip={`${totalLabel}${formatInteger(c.processed * 
seconds)} events`}
+                  >{`Processed: ${formatRate(c.processed)}`}</div>
+                )}
+                {Boolean(c.processedWithError) && (
+                  <div
+                    className="warning-line"
+                    data-tooltip={`${totalLabel}${formatInteger(
+                      c.processedWithError * seconds,
+                    )} events`}
+                  >
+                    Processed with error: {formatRate(c.processedWithError)}
+                  </div>
+                )}
+                {Boolean(c.thrownAway) && (
+                  <div
+                    className="warning-line"
+                    data-tooltip={`${totalLabel}${formatInteger(c.thrownAway * 
seconds)} events`}
+                  >
+                    Thrown away: {formatRate(c.thrownAway)}
+                  </div>
+                )}
+                {Boolean(c.unparseable) && (
+                  <div
+                    className="warning-line"
+                    data-tooltip={`${totalLabel}${formatInteger(c.unparseable 
* seconds)} events`}
+                  >
+                    Unparseable: {formatRate(c.unparseable)}
+                  </div>
+                )}
+              </div>
+            ) : c.processedBytes ? (
+              <div
+                data-tooltip={`${totalLabel}${formatInteger(
+                  c.processed * seconds,
+                )} events, ${formatBytes(c.processedBytes * seconds)}`}
+              >{`Processed: ${formatRate(c.processed)} 
(${formatByteRate(c.processedBytes)})`}</div>
+            ) : (
+              <div>No activity</div>
+            );
           },
-          {
-            Header: 'Recent errors',
-            accessor: 'status.payload.recentErrors',
-            width: 150,
-            filterable: false,
-            sortable: false,
-            show: visibleColumns.shown('Recent errors'),
-            Cell: ({ value, original }) => {
-              if (!value) return null;
-              return (
-                <TableClickableCell
-                  tooltip="Show errors"
-                  onClick={() => this.onSupervisorDetail(original)}
-                  hoverIcon={IconNames.SEARCH_TEMPLATE}
-                >
-                  {pluralIfNeeded(value.length, 'error')}
-                </TableClickableCell>
-              );
-            },
+        },
+        {
+          Header: 'Recent errors',
+          accessor: 'supervisor_id',
+          width: 150,
+          filterable: false,
+          sortable: false,
+          show: visibleColumns.shown('Recent errors'),
+          Cell: ({ value, original }) => {
+            const status = useContext(StatusContext);
+            const recentErrors = status[value]?.payload?.recentErrors;
+            if (!recentErrors) return null;
+            return (
+              <TableClickableCell
+                tooltip="Show errors"
+                onClick={() => this.onSupervisorDetail(original)}
+                hoverIcon={IconNames.SEARCH_TEMPLATE}
+              >
+                {pluralIfNeeded(recentErrors.length, 'error')}
+              </TableClickableCell>
+            );
           },
-          {
-            Header: ACTION_COLUMN_LABEL,
-            id: ACTION_COLUMN_ID,
-            accessor: 'supervisor_id',
-            width: ACTION_COLUMN_WIDTH,
-            filterable: false,
-            sortable: false,
-            Cell: ({ value: id, original }) => {
-              const supervisorActions = this.getSupervisorActions(original);
-              return (
-                <ActionCell
-                  onDetail={() => this.onSupervisorDetail(original)}
-                  actions={supervisorActions}
-                  menuTitle={id}
-                />
-              );
-            },
+        },
+        {
+          Header: ACTION_COLUMN_LABEL,
+          id: ACTION_COLUMN_ID,
+          accessor: 'supervisor_id',
+          width: ACTION_COLUMN_WIDTH,
+          filterable: false,
+          sortable: false,
+          Cell: ({ value: id, original }) => {
+            const supervisorActions = this.getSupervisorActions(original);
+            return (
+              <ActionCell
+                onDetail={() => this.onSupervisorDetail(original)}
+                actions={supervisorActions}
+                menuTitle={id}
+              />
+            );
           },
-        ]}
-      />
-    );
-  }
+        },
+      ];
+    },
+  );
 
   renderBulkSupervisorActions() {
     const { capabilities, goToQuery } = this.props;


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to