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

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

commit 151a2060981328881e90e31103848ec45e87abbd
Author: Vadim Ogievetsky <[email protected]>
AuthorDate: Thu Apr 18 12:32:16 2024 -0700

    add rate and stats
---
 .../src/components/header-bar/header-bar.scss      |   5 +
 .../src/components/timed-button/timed-button.tsx   |   4 +-
 .../supervisor-statistics-table.spec.tsx           |   4 +
 .../supervisor-statistics-table.tsx                |  56 ++--
 .../supervisor-status/supervisor-status.ts         |  54 ++++
 web-console/src/utils/druid-query.ts               |   9 +-
 web-console/src/utils/general.tsx                  |  12 +
 web-console/src/utils/table-helpers.ts             |  19 ++
 .../src/views/segments-view/segments-view.tsx      |  20 +-
 .../views/supervisors-view/supervisors-view.tsx    | 312 +++++++++++++++------
 10 files changed, 350 insertions(+), 145 deletions(-)

diff --git a/web-console/src/components/header-bar/header-bar.scss 
b/web-console/src/components/header-bar/header-bar.scss
index 062768a22c4..752cc9bf316 100644
--- a/web-console/src/components/header-bar/header-bar.scss
+++ b/web-console/src/components/header-bar/header-bar.scss
@@ -89,4 +89,9 @@
       }
     }
   }
+
+  .#{$bp-ns}-navbar-group.#{$bp-ns}-align-right {
+    position: absolute;
+    right: 15px;
+  }
 }
diff --git a/web-console/src/components/timed-button/timed-button.tsx 
b/web-console/src/components/timed-button/timed-button.tsx
index 0b339a8d0e5..cb275370b27 100644
--- a/web-console/src/components/timed-button/timed-button.tsx
+++ b/web-console/src/components/timed-button/timed-button.tsx
@@ -25,7 +25,7 @@ import React, { useState } from 'react';
 
 import { useInterval } from '../../hooks';
 import type { LocalStorageKeys } from '../../utils';
-import { isInBackground, localStorageGet, localStorageSet } from '../../utils';
+import { checkedCircleIcon, isInBackground, localStorageGet, localStorageSet } 
from '../../utils';
 
 export interface DelayLabel {
   label: string;
@@ -84,7 +84,7 @@ export const TimedButton = React.memo(function 
TimedButton(props: TimedButtonPro
             {delays.map(({ label, delay }, i) => (
               <MenuItem
                 key={i}
-                icon={selectedDelay === delay ? IconNames.SELECTION : 
IconNames.CIRCLE}
+                icon={checkedCircleIcon(selectedDelay === delay)}
                 text={label}
                 onClick={() => handleSelection(delay)}
               />
diff --git 
a/web-console/src/dialogs/supervisor-table-action-dialog/supervisor-statistics-table/supervisor-statistics-table.spec.tsx
 
b/web-console/src/dialogs/supervisor-table-action-dialog/supervisor-statistics-table/supervisor-statistics-table.spec.tsx
index 3ea907dc0cf..8934f761207 100644
--- 
a/web-console/src/dialogs/supervisor-table-action-dialog/supervisor-statistics-table/supervisor-statistics-table.spec.tsx
+++ 
b/web-console/src/dialogs/supervisor-table-action-dialog/supervisor-statistics-table/supervisor-statistics-table.spec.tsx
@@ -72,18 +72,21 @@ describe('SupervisorStatisticsTable', () => {
               buildSegments: {
                 '5m': {
                   processed: 3.5455993615040584,
+                  processedBytes: 10,
                   unparseable: 0,
                   thrownAway: 0,
                   processedWithError: 0,
                 },
                 '15m': {
                   processed: 5.544749689510444,
+                  processedBytes: 20,
                   unparseable: 0,
                   thrownAway: 0,
                   processedWithError: 0,
                 },
                 '1m': {
                   processed: 4.593670088770785,
+                  processedBytes: 30,
                   unparseable: 0,
                   thrownAway: 0,
                   processedWithError: 0,
@@ -93,6 +96,7 @@ describe('SupervisorStatisticsTable', () => {
             totals: {
               buildSegments: {
                 processed: 7516,
+                processedBytes: 60,
                 processedWithError: 0,
                 thrownAway: 0,
                 unparseable: 0,
diff --git 
a/web-console/src/dialogs/supervisor-table-action-dialog/supervisor-statistics-table/supervisor-statistics-table.tsx
 
b/web-console/src/dialogs/supervisor-table-action-dialog/supervisor-statistics-table/supervisor-statistics-table.tsx
index 09749f85253..26cfd62f333 100644
--- 
a/web-console/src/dialogs/supervisor-table-action-dialog/supervisor-statistics-table/supervisor-statistics-table.tsx
+++ 
b/web-console/src/dialogs/supervisor-table-action-dialog/supervisor-statistics-table/supervisor-statistics-table.tsx
@@ -22,33 +22,21 @@ import type { CellInfo, Column } from 'react-table';
 import ReactTable from 'react-table';
 
 import { Loader } from '../../../components/loader/loader';
+import type { RowStats, RowStatsCounter, SupervisorStats } from 
'../../../druid-models';
 import { useQueryManager } from '../../../hooks';
 import { SMALL_TABLE_PAGE_SIZE, SMALL_TABLE_PAGE_SIZE_OPTIONS } from 
'../../../react-table';
 import { Api, UrlBaser } from '../../../singletons';
-import { deepGet } from '../../../utils';
+import { deepGet, formatByteRate, formatBytes, formatInteger, formatRate } 
from '../../../utils';
 
 import './supervisor-statistics-table.scss';
 
-export interface TaskSummary {
-  totals: Record<string, StatsEntry>;
-  movingAverages: Record<string, Record<string, StatsEntry>>;
-}
-
-export interface StatsEntry {
-  processed?: number;
-  processedWithError?: number;
-  thrownAway?: number;
-  unparseable?: number;
-  [key: string]: number | undefined;
-}
-
 export interface SupervisorStatisticsTableRow {
   taskId: string;
-  summary: TaskSummary;
+  summary: RowStats;
 }
 
 export function normalizeSupervisorStatisticsResults(
-  data: Record<string, Record<string, TaskSummary>>,
+  data: SupervisorStats,
 ): SupervisorStatisticsTableRow[] {
   return Object.values(data).flatMap(v => Object.keys(v).map(k => ({ taskId: 
k, summary: v[k] })));
 }
@@ -66,21 +54,29 @@ export const SupervisorStatisticsTable = 
React.memo(function SupervisorStatistic
 
   const [supervisorStatisticsState] = useQueryManager<null, 
SupervisorStatisticsTableRow[]>({
     processQuery: async () => {
-      const resp = await Api.instance.get(endpoint);
+      const resp = await Api.instance.get<SupervisorStats>(endpoint);
       return normalizeSupervisorStatisticsResults(resp.data);
     },
     initQuery: null,
   });
 
-  function renderCell(cell: CellInfo) {
-    const cellValue = cell.value;
-    if (!cellValue) {
-      return <div>No data found</div>;
-    }
+  function renderCounters(cell: CellInfo, isRate: boolean) {
+    const c: RowStatsCounter = cell.value;
+    if (!c) return null;
 
-    return Object.keys(cellValue)
-      .sort()
-      .map(key => <div key={key}>{`${key}: 
${Number(cellValue[key]).toFixed(1)}`}</div>);
+    const formatNumber = isRate ? formatRate : formatInteger;
+    const formatData = isRate ? formatByteRate : formatBytes;
+    const bytes = c.processedBytes ? ` (${formatData(c.processedBytes)})` : '';
+    return (
+      <div>
+        <div>{`Processed: ${formatNumber(c.processed)}${bytes}`}</div>
+        {Boolean(c.processedWithError) && (
+          <div>Processed with error: {formatNumber(c.processedWithError)}</div>
+        )}
+        {Boolean(c.thrownAway) && <div>Thrown away: 
{formatNumber(c.thrownAway)}</div>}
+        {Boolean(c.unparseable) && <div>Unparseable: 
{formatNumber(c.unparseable)}</div>}
+      </div>
+    );
   }
 
   function renderTable() {
@@ -98,9 +94,9 @@ export const SupervisorStatisticsTable = React.memo(function 
SupervisorStatistic
         className: 'padded',
         width: 200,
         accessor: d => {
-          return deepGet(d, 'summary.totals.buildSegments') as StatsEntry;
+          return deepGet(d, 'summary.totals.buildSegments') as RowStatsCounter;
         },
-        Cell: renderCell,
+        Cell: c => renderCounters(c, false),
       },
     ];
 
@@ -118,10 +114,8 @@ export const SupervisorStatisticsTable = 
React.memo(function SupervisorStatistic
               id: interval,
               className: 'padded',
               width: 200,
-              accessor: d => {
-                return deepGet(d, 
`summary.movingAverages.buildSegments.${interval}`);
-              },
-              Cell: renderCell,
+              accessor: `summary.movingAverages.buildSegments.${interval}`,
+              Cell: c => renderCounters(c, true),
             };
           }),
       );
diff --git 
a/web-console/src/druid-models/supervisor-status/supervisor-status.ts 
b/web-console/src/druid-models/supervisor-status/supervisor-status.ts
index b25c9f5fb1f..bdbc3187736 100644
--- a/web-console/src/druid-models/supervisor-status/supervisor-status.ts
+++ b/web-console/src/druid-models/supervisor-status/supervisor-status.ts
@@ -16,7 +16,10 @@
  * limitations under the License.
  */
 
+import { sum } from 'd3-array';
+
 import type { NumberLike } from '../../utils';
+import { compact, deepGet } from '../../utils';
 
 export type SupervisorOffsetMap = Record<string, NumberLike>;
 
@@ -52,3 +55,54 @@ export interface SupervisorStatusTask {
   currentOffsets: SupervisorOffsetMap;
   lag: SupervisorOffsetMap;
 }
+
+export type SupervisorStats = Record<string, Record<string, RowStats>>;
+
+export type RowStatsKey = 'totals' | '1m' | '5m' | '15m';
+
+export interface RowStats {
+  movingAverages: {
+    buildSegments: {
+      '1m': RowStatsCounter;
+      '5m': RowStatsCounter;
+      '15m': RowStatsCounter;
+    };
+  };
+  totals: {
+    buildSegments: RowStatsCounter;
+  };
+}
+
+export interface RowStatsCounter {
+  processed: number;
+  processedBytes: number;
+  processedWithError: number;
+  thrownAway: number;
+  unparseable: number;
+}
+
+function aggregateRowStatsCounter(rowStats: RowStatsCounter[]): 
RowStatsCounter {
+  return {
+    processed: sum(rowStats, d => d.processed),
+    processedBytes: sum(rowStats, d => d.processedBytes),
+    processedWithError: sum(rowStats, d => d.processedWithError),
+    thrownAway: sum(rowStats, d => d.thrownAway),
+    unparseable: sum(rowStats, d => d.unparseable),
+  };
+}
+
+function getRowStatsCounter(rowStats: RowStats, key: RowStatsKey): 
RowStatsCounter | undefined {
+  if (key === 'totals') {
+    return deepGet(rowStats, 'totals.buildSegments');
+  } else {
+    return deepGet(rowStats, `movingAverages.buildSegments.${key}`);
+  }
+}
+
+export function getTotalSupervisorStats(stats: SupervisorStats, key: 
RowStatsKey): RowStatsCounter {
+  return aggregateRowStatsCounter(
+    compact(
+      Object.values(stats).flatMap(s => Object.values(s).map(rs => 
getRowStatsCounter(rs, key))),
+    ),
+  );
+}
diff --git a/web-console/src/utils/druid-query.ts 
b/web-console/src/utils/druid-query.ts
index c94bfca3d1c..15410329704 100644
--- a/web-console/src/utils/druid-query.ts
+++ b/web-console/src/utils/druid-query.ts
@@ -17,7 +17,7 @@
  */
 
 import { C } from '@druid-toolkit/query';
-import type { AxiosResponse } from 'axios';
+import type { AxiosResponse, CancelToken } from 'axios';
 import axios from 'axios';
 
 import { Api } from '../singletons';
@@ -329,10 +329,13 @@ export async function queryDruidRune(runeQuery: 
Record<string, any>): Promise<an
   return runeResultResp.data;
 }
 
-export async function queryDruidSql<T = any>(sqlQueryPayload: Record<string, 
any>): Promise<T[]> {
+export async function queryDruidSql<T = any>(
+  sqlQueryPayload: Record<string, any>,
+  cancelToken?: CancelToken,
+): Promise<T[]> {
   let sqlResultResp: AxiosResponse;
   try {
-    sqlResultResp = await Api.instance.post('/druid/v2/sql', sqlQueryPayload);
+    sqlResultResp = await Api.instance.post('/druid/v2/sql', sqlQueryPayload, 
{ cancelToken });
   } catch (e) {
     throw new Error(getDruidErrorMessage(e));
   }
diff --git a/web-console/src/utils/general.tsx 
b/web-console/src/utils/general.tsx
index 3a770c67630..b4537a63e08 100644
--- a/web-console/src/utils/general.tsx
+++ b/web-console/src/utils/general.tsx
@@ -239,14 +239,26 @@ export function formatNumber(n: NumberLike): string {
   return n.toLocaleString('en-US', { maximumFractionDigits: 20 });
 }
 
+export function formatRate(n: NumberLike) {
+  return numeral(n).format('0,0.0') + '/s';
+}
+
 export function formatBytes(n: NumberLike): string {
   return numeral(n).format('0.00 b');
 }
 
+export function formatByteRate(n: NumberLike): string {
+  return numeral(n).format('0.00 b') + '/s';
+}
+
 export function formatBytesCompact(n: NumberLike): string {
   return numeral(n).format('0.00b');
 }
 
+export function formatByteRateCompact(n: NumberLike): string {
+  return numeral(n).format('0.00b') + '/s';
+}
+
 export function formatMegabytes(n: NumberLike): string {
   return numeral(Number(n) / 1048576).format('0,0.0');
 }
diff --git a/web-console/src/utils/table-helpers.ts 
b/web-console/src/utils/table-helpers.ts
index e864aef131f..7eedd1acaab 100644
--- a/web-console/src/utils/table-helpers.ts
+++ b/web-console/src/utils/table-helpers.ts
@@ -17,6 +17,8 @@
  */
 
 import type { QueryResult } from '@druid-toolkit/query';
+import { C } from '@druid-toolkit/query';
+import type { Filter } from 'react-table';
 
 import { filterMap, formatNumber, oneOf } from './general';
 import { deepSet } from './object-change';
@@ -56,3 +58,20 @@ export function getNumericColumnBraces(
 
   return numericColumnBraces;
 }
+
+export interface Sorted {
+  id: string;
+  desc: boolean;
+}
+
+export interface TableState {
+  page: number;
+  pageSize: number;
+  filtered: Filter[];
+  sorted: Sorted[];
+}
+
+export function sortedToOrderByClause(sorted: Sorted[]): string | undefined {
+  if (!sorted.length) return;
+  return 'ORDER BY ' + sorted.map(sort => `${C(sort.id)} ${sort.desc ? 'DESC' 
: 'ASC'}`).join(', ');
+}
diff --git a/web-console/src/views/segments-view/segments-view.tsx 
b/web-console/src/views/segments-view/segments-view.tsx
index ae40a8d641e..44d59f6fa95 100644
--- a/web-console/src/views/segments-view/segments-view.tsx
+++ b/web-console/src/views/segments-view/segments-view.tsx
@@ -53,7 +53,7 @@ import {
   STANDARD_TABLE_PAGE_SIZE_OPTIONS,
 } from '../../react-table';
 import { Api } from '../../singletons';
-import type { NumberLike } from '../../utils';
+import type { NumberLike, TableState } from '../../utils';
 import {
   compact,
   countBy,
@@ -69,6 +69,7 @@ import {
   queryDruidSql,
   QueryManager,
   QueryState,
+  sortedToOrderByClause,
   twoLines,
 } from '../../utils';
 import type { BasicAction } from '../../utils/basic-action';
@@ -134,23 +135,6 @@ function formatRangeDimensionValue(dimension: any, value: 
any): string {
   return `${C(String(dimension))}=${L(String(value))}`;
 }
 
-interface Sorted {
-  id: string;
-  desc: boolean;
-}
-
-function sortedToOrderByClause(sorted: Sorted[]): string | undefined {
-  if (!sorted.length) return;
-  return 'ORDER BY ' + sorted.map(sort => `${C(sort.id)} ${sort.desc ? 'DESC' 
: 'ASC'}`).join(', ');
-}
-
-interface TableState {
-  page: number;
-  pageSize: number;
-  filtered: Filter[];
-  sorted: Sorted[];
-}
-
 interface SegmentsQuery extends TableState {
   visibleColumns: LocalStorageBackedVisibility;
   capabilities: Capabilities;
diff --git a/web-console/src/views/supervisors-view/supervisors-view.tsx 
b/web-console/src/views/supervisors-view/supervisors-view.tsx
index fef74fbeebe..6e872792220 100644
--- a/web-console/src/views/supervisors-view/supervisors-view.tsx
+++ b/web-console/src/views/supervisors-view/supervisors-view.tsx
@@ -16,8 +16,9 @@
  * limitations under the License.
  */
 
-import { Code, Intent, MenuItem } from '@blueprintjs/core';
+import { Button, Code, Intent, Menu, MenuItem, Position } from 
'@blueprintjs/core';
 import { IconNames } from '@blueprintjs/icons';
+import { Popover2 } from '@blueprintjs/popover2';
 import React from 'react';
 import type { Filter } from 'react-table';
 import ReactTable from 'react-table';
@@ -41,23 +42,34 @@ import {
   SupervisorTableActionDialog,
 } from '../../dialogs';
 import { SupervisorResetOffsetsDialog } from 
'../../dialogs/supervisor-reset-offsets-dialog/supervisor-reset-offsets-dialog';
-import type { QueryWithContext } from '../../druid-models';
+import type { QueryWithContext, RowStatsKey, SupervisorStatus } from 
'../../druid-models';
+import { getTotalSupervisorStats } from '../../druid-models';
 import type { Capabilities } from '../../helpers';
-import { SMALL_TABLE_PAGE_SIZE, SMALL_TABLE_PAGE_SIZE_OPTIONS } from 
'../../react-table';
+import {
+  SMALL_TABLE_PAGE_SIZE,
+  SMALL_TABLE_PAGE_SIZE_OPTIONS,
+  sqlQueryCustomTableFilter,
+} from '../../react-table';
 import { Api, AppToaster } from '../../singletons';
+import type { TableState } from '../../utils';
 import {
+  assemble,
+  checkedCircleIcon,
   deepGet,
+  formatByteRate,
+  formatBytes,
+  formatInteger,
+  formatRate,
   getDruidErrorMessage,
-  groupByAsMap,
   hasPopoverOpen,
   LocalStorageBackedVisibility,
   LocalStorageKeys,
-  lookupBy,
   oneOf,
   pluralIfNeeded,
   queryDruidSql,
   QueryManager,
   QueryState,
+  sortedToOrderByClause,
   twoLines,
 } from '../../utils';
 import type { BasicAction } from '../../utils/basic-action';
@@ -69,11 +81,20 @@ const supervisorTableColumns: string[] = [
   'Type',
   'Topic/Stream',
   'Status',
-  'Running tasks',
+  'Active tasks',
+  'Aggregate lag',
+  'Stats',
   ACTION_COLUMN_LABEL,
 ];
 
-interface SupervisorQuery {
+const ROW_STATS_KEYS: RowStatsKey[] = ['totals', '1m', '5m', '15m'];
+
+function getRowStatsKeyTitle(key: RowStatsKey) {
+  if (key === 'totals') return 'Total';
+  return `Rate over past ${pluralIfNeeded(parseInt(key, 10), 'minute')}`;
+}
+
+interface SupervisorQuery extends TableState {
   capabilities: Capabilities;
   visibleColumns: LocalStorageBackedVisibility;
 }
@@ -84,13 +105,8 @@ interface SupervisorQueryResultRow {
   source: string;
   detailed_state: string;
   suspended: boolean;
-  running_tasks?: number;
-}
-
-interface RunningTaskRow {
-  datasource: string;
-  type: string;
-  num_running_tasks: number;
+  status?: SupervisorStatus;
+  stats?: any;
 }
 
 export interface SupervisorsViewProps {
@@ -106,6 +122,7 @@ export interface SupervisorsViewProps {
 
 export interface SupervisorsViewState {
   supervisorsState: QueryState<SupervisorQueryResultRow[]>;
+  statsKey: RowStatsKey;
 
   resumeSupervisorId?: string;
   suspendSupervisorId?: string;
@@ -165,25 +182,20 @@ export class SupervisorsView extends React.PureComponent<
     SupervisorQueryResultRow[]
   >;
 
-  static SUPERVISOR_SQL = `SELECT
+  static SUPERVISOR_SQL_BASE = `WITH s AS (SELECT
   "supervisor_id",
   "type",
   "source",
   CASE WHEN "suspended" = 0 THEN "detailed_state" ELSE 'SUSPENDED' END AS 
"detailed_state",
   "suspended" = 1 AS "suspended"
-FROM "sys"."supervisors"
-ORDER BY "supervisor_id"`;
-
-  static RUNNING_TASK_SQL = `SELECT
-  "datasource", "type", COUNT(*) AS "num_running_tasks"
-FROM "sys"."tasks" WHERE "status" = 'RUNNING' AND "runner_status" = 'RUNNING'
-GROUP BY 1, 2`;
+FROM "sys"."supervisors")`;
 
   constructor(props: SupervisorsViewProps) {
     super(props);
 
     this.state = {
       supervisorsState: QueryState.INIT,
+      statsKey: '5m',
 
       showResumeAllSupervisors: false,
       showSuspendAllSupervisors: false,
@@ -199,14 +211,35 @@ GROUP BY 1, 2`;
     };
 
     this.supervisorQueryManager = new QueryManager({
-      processQuery: async ({ capabilities, visibleColumns }) => {
+      processQuery: async (
+        { capabilities, visibleColumns, filtered, sorted, page, pageSize },
+        cancelToken,
+        setIntermediateQuery,
+      ) => {
         let supervisors: SupervisorQueryResultRow[];
         if (capabilities.hasSql()) {
-          supervisors = await queryDruidSql<SupervisorQueryResultRow>({
-            query: SupervisorsView.SUPERVISOR_SQL,
-          });
+          const sqlQuery = assemble(
+            SupervisorsView.SUPERVISOR_SQL_BASE,
+            'SELECT *',
+            'FROM s',
+            filtered.length
+              ? `WHERE ${filtered.map(sqlQueryCustomTableFilter).join(' AND 
')}`
+              : undefined,
+            sortedToOrderByClause(sorted),
+            `LIMIT ${pageSize}`,
+            page ? `OFFSET ${page * pageSize}` : undefined,
+          ).join('\n');
+          setIntermediateQuery(sqlQuery);
+          supervisors = await queryDruidSql<SupervisorQueryResultRow>(
+            {
+              query: sqlQuery,
+            },
+            cancelToken,
+          );
         } else if (capabilities.hasOverlordAccess()) {
-          const supervisorList = (await 
Api.instance.get('/druid/indexer/v1/supervisor?full')).data;
+          const supervisorList = (
+            await Api.instance.get('/druid/indexer/v1/supervisor?full', { 
cancelToken })
+          ).data;
           if (!Array.isArray(supervisorList)) {
             throw new Error(`Unexpected result from 
/druid/indexer/v1/supervisor?full`);
           }
@@ -223,45 +256,64 @@ GROUP BY 1, 2`;
               suspended: Boolean(deepGet(sup, 'suspended')),
             };
           });
+
+          const firstSorted = sorted[0];
+          if (firstSorted) {
+            const { id, desc } = firstSorted;
+            supervisors.sort((s1: any, s2: any) => {
+              return (
+                String(s1[id]).localeCompare(String(s2[id]), undefined, { 
numeric: true }) *
+                (desc ? -1 : 1)
+              );
+            });
+          }
         } else {
           throw new Error(`must have SQL or overlord access`);
         }
 
-        if (visibleColumns.shown('Running tasks')) {
-          try {
-            let runningTaskLookup: Record<string, number>;
-            if (capabilities.hasSql()) {
-              const runningTasks = await queryDruidSql<RunningTaskRow>({
-                query: SupervisorsView.RUNNING_TASK_SQL,
+        if (capabilities.hasOverlordAccess()) {
+          if (visibleColumns.shown('Active tasks') || 
visibleColumns.shown('Aggregate lag')) {
+            try {
+              for (const supervisor of supervisors) {
+                cancelToken.throwIfRequested();
+                supervisor.status = (
+                  await Api.instance.get(
+                    `/druid/indexer/v1/supervisor/${Api.encodePath(
+                      supervisor.supervisor_id,
+                    )}/status`,
+                    { cancelToken },
+                  )
+                ).data;
+              }
+            } catch (e) {
+              AppToaster.show({
+                icon: IconNames.ERROR,
+                intent: Intent.DANGER,
+                message: 'Could not get status',
               });
-
-              runningTaskLookup = lookupBy(
-                runningTasks,
-                ({ datasource, type }) => `${datasource}_${type}`,
-                ({ num_running_tasks }) => num_running_tasks,
-              );
-            } else if (capabilities.hasOverlordAccess()) {
-              const taskList = (await 
Api.instance.get(`/druid/indexer/v1/tasks?state=running`))
-                .data;
-              runningTaskLookup = groupByAsMap(
-                taskList,
-                (t: any) => `${t.dataSource}_${t.type}`,
-                xs => xs.length,
-              );
-            } else {
-              throw new Error(`must have SQL or overlord access`);
             }
+          }
 
-            supervisors.forEach(supervisor => {
-              supervisor.running_tasks =
-                
runningTaskLookup[`${supervisor.supervisor_id}_index_${supervisor.type}`] || 0;
-            });
-          } catch (e) {
-            AppToaster.show({
-              icon: IconNames.ERROR,
-              intent: Intent.DANGER,
-              message: 'Could not get running task counts',
-            });
+          if (visibleColumns.shown('Stats')) {
+            try {
+              for (const supervisor of supervisors) {
+                cancelToken.throwIfRequested();
+                supervisor.stats = (
+                  await Api.instance.get(
+                    `/druid/indexer/v1/supervisor/${Api.encodePath(
+                      supervisor.supervisor_id,
+                    )}/stats`,
+                    { cancelToken },
+                  )
+                ).data;
+              }
+            } catch (e) {
+              AppToaster.show({
+                icon: IconNames.ERROR,
+                intent: Intent.DANGER,
+                message: 'Could not get stats',
+              });
+            }
           }
         }
 
@@ -275,17 +327,27 @@ GROUP BY 1, 2`;
     });
   }
 
-  componentDidMount(): void {
-    const { capabilities } = this.props;
-    const { visibleColumns } = this.state;
-
-    this.supervisorQueryManager.runQuery({ capabilities, visibleColumns: 
visibleColumns });
-  }
+  private lastTableState: TableState | undefined;
 
   componentWillUnmount(): void {
     this.supervisorQueryManager.terminate();
   }
 
+  private readonly fetchData = (tableState?: TableState) => {
+    const { capabilities } = this.props;
+    const { visibleColumns } = this.state;
+    if (tableState) this.lastTableState = tableState;
+    const { page, pageSize, filtered, sorted } = this.lastTableState!;
+    this.supervisorQueryManager.runQuery({
+      page,
+      pageSize,
+      filtered,
+      sorted,
+      visibleColumns,
+      capabilities,
+    });
+  };
+
   private readonly closeSpecDialogs = () => {
     this.setState({
       supervisorSpecDialogOpen: false,
@@ -541,7 +603,7 @@ GROUP BY 1, 2`;
 
   private renderSupervisorTable() {
     const { goToTasks, filters, onFiltersChange } = this.props;
-    const { supervisorsState, visibleColumns } = this.state;
+    const { supervisorsState, statsKey, visibleColumns } = this.state;
 
     const supervisors = supervisorsState.data || [];
     return (
@@ -554,6 +616,9 @@ GROUP BY 1, 2`;
         filtered={filters}
         onFilteredChange={onFiltersChange}
         filterable
+        onFetchData={tableState => {
+          this.fetchData(tableState);
+        }}
         defaultPageSize={SMALL_TABLE_PAGE_SIZE}
         pageSizeOptions={SMALL_TABLE_PAGE_SIZE_OPTIONS}
         showPagination={supervisors.length > SMALL_TABLE_PAGE_SIZE}
@@ -562,7 +627,7 @@ GROUP BY 1, 2`;
             Header: twoLines('Supervisor ID', <i>(datasource)</i>),
             id: 'supervisor_id',
             accessor: 'supervisor_id',
-            width: 300,
+            width: 280,
             show: visibleColumns.shown('Supervisor ID'),
             Cell: ({ value, original }) => (
               <TableClickableCell
@@ -576,21 +641,21 @@ GROUP BY 1, 2`;
           {
             Header: 'Type',
             accessor: 'type',
-            width: 100,
+            width: 80,
             Cell: this.renderSupervisorFilterableCell('type'),
             show: visibleColumns.shown('Type'),
           },
           {
             Header: 'Topic/Stream',
             accessor: 'source',
-            width: 300,
+            width: 200,
             Cell: this.renderSupervisorFilterableCell('source'),
             show: visibleColumns.shown('Topic/Stream'),
           },
           {
             Header: 'Status',
-            id: 'status',
-            width: 250,
+            id: 'detailed_state',
+            width: 150,
             accessor: 'detailed_state',
             Cell: ({ value }) => (
               <TableFilterableCell
@@ -608,27 +673,67 @@ GROUP BY 1, 2`;
             show: visibleColumns.shown('Status'),
           },
           {
-            Header: 'Running tasks',
-            id: 'running_tasks',
+            Header: 'Active tasks',
+            id: 'active_tasks',
             width: 150,
-            accessor: 'running_tasks',
+            accessor: 'status.payload.activeTasks',
             filterable: false,
-            Cell: ({ value, original }) => (
-              <TableClickableCell
-                onClick={() => goToTasks(original.supervisor_id, 
`index_${original.type}`)}
-                hoverIcon={IconNames.ARROW_TOP_RIGHT}
-                title="Go to tasks"
-              >
-                {typeof value === 'undefined'
-                  ? 'n/a'
-                  : value > 0
-                  ? pluralIfNeeded(value, 'running task')
-                  : original.suspended
-                  ? ''
-                  : `No running tasks`}
-              </TableClickableCell>
-            ),
-            show: visibleColumns.shown('Running tasks'),
+            sortable: false,
+            Cell: ({ value, original }) => {
+              if (original.suspended) return;
+              return (
+                <TableClickableCell
+                  onClick={() => goToTasks(original.supervisor_id, 
`index_${original.type}`)}
+                  hoverIcon={IconNames.ARROW_TOP_RIGHT}
+                  title="Go to tasks"
+                >
+                  {typeof value === 'undefined'
+                    ? 'n/a'
+                    : value.length > 0
+                    ? pluralIfNeeded(value.length, 'running task')
+                    : `No running tasks`}
+                </TableClickableCell>
+              );
+            },
+            show: visibleColumns.shown('Active tasks'),
+          },
+          {
+            Header: 'Aggregate lag',
+            accessor: 'status.aggregateLag',
+            width: 200,
+            filterable: false,
+            sortable: false,
+            className: 'padded',
+            show: visibleColumns.shown('Aggregate lag'),
+            Cell: ({ value }) => formatInteger(value),
+          },
+          {
+            Header: twoLines('Stats', <i>{getRowStatsKeyTitle(statsKey)}</i>),
+            id: 'stats',
+            width: 220,
+            filterable: false,
+            sortable: false,
+            className: 'padded',
+            accessor: 'stats',
+            Cell: ({ value }) => {
+              if (!value) return;
+              const c = getTotalSupervisorStats(value, statsKey);
+              const isRate = statsKey !== 'totals';
+              const formatNumber = isRate ? formatRate : formatInteger;
+              const formatData = isRate ? formatByteRate : formatBytes;
+              const bytes = c.processedBytes ? ` 
(${formatData(c.processedBytes)})` : '';
+              return (
+                <div>
+                  <div>{`Processed: 
${formatNumber(c.processed)}${bytes}`}</div>
+                  {Boolean(c.processedWithError) && (
+                    <div>Processed with error: 
{formatNumber(c.processedWithError)}</div>
+                  )}
+                  {Boolean(c.thrownAway) && <div>Thrown away: 
{formatNumber(c.thrownAway)}</div>}
+                  {Boolean(c.unparseable) && <div>Unparseable: 
{formatNumber(c.unparseable)}</div>}
+                </div>
+              );
+            },
+            show: visibleColumns.shown('Stats'),
           },
           {
             Header: ACTION_COLUMN_LABEL,
@@ -636,6 +741,7 @@ GROUP BY 1, 2`;
             accessor: 'supervisor_id',
             width: ACTION_COLUMN_WIDTH,
             filterable: false,
+            sortable: false,
             Cell: row => {
               const id = row.value;
               const type = row.original.type;
@@ -657,6 +763,7 @@ GROUP BY 1, 2`;
 
   renderBulkSupervisorActions() {
     const { capabilities, goToQuery } = this.props;
+    const lastSupervisorQuery = 
this.supervisorQueryManager.getLastIntermediateQuery();
 
     return (
       <>
@@ -665,7 +772,7 @@ GROUP BY 1, 2`;
             <MenuItem
               icon={IconNames.APPLICATION}
               text="View SQL query for table"
-              onClick={() => goToQuery({ queryString: 
SupervisorsView.SUPERVISOR_SQL })}
+              onClick={() => goToQuery({ queryString: lastSupervisorQuery })}
             />
           )}
           <MenuItem
@@ -782,6 +889,7 @@ GROUP BY 1, 2`;
       supervisorTableActionDialogId,
       supervisorTableActionDialogActions,
       visibleColumns,
+      statsKey,
     } = this.state;
 
     return (
@@ -794,6 +902,28 @@ GROUP BY 1, 2`;
               this.supervisorQueryManager.rerunLastQuery(auto);
             }}
           />
+          <Popover2
+            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>
+            }
+          >
+            <Button
+              text={`Stats: ${getRowStatsKeyTitle(statsKey)}`}
+              rightIcon={IconNames.CARET_DOWN}
+            />
+          </Popover2>
           {this.renderBulkSupervisorActions()}
           <TableColumnSelector
             columns={supervisorTableColumns}


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

Reply via email to