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]
