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 39ada8b9ade Web console: surface more info on the supervisor view
(#16318)
39ada8b9ade is described below
commit 39ada8b9adecc6beee635a5fd7ead27472904d3b
Author: Vadim Ogievetsky <[email protected]>
AuthorDate: Thu May 2 08:50:27 2024 -0700
Web console: surface more info on the supervisor view (#16318)
* add rate and stats
* better tabs
* detail
* add recent errors
* update tests
* don't let people hide the actions column because why
* don't sort on actions
* better way to agg
* add timeouts
* show error only once
* fix tests and Explain showing up
* only consider active tasks
* refresh
* fix tests
* better formatting
---
.../src/components/header-bar/header-bar.scss | 5 +
.../table-column-selector.spec.tsx | 2 +-
.../table-column-selector.tsx | 43 +-
.../__snapshots__/timed-button.spec.tsx.snap | 2 +-
.../src/components/timed-button/timed-button.tsx | 4 +-
.../compaction-history-dialog.tsx | 4 +-
.../kill-datasource-dialog.tsx | 7 +-
.../supervisor-table-action-dialog.spec.tsx.snap | 4 +-
.../supervisor-statistics-table.spec.tsx.snap | 57 ++-
.../supervisor-statistics-table.spec.tsx | 6 +-
.../supervisor-statistics-table.tsx | 91 +++--
.../supervisor-table-action-dialog.tsx | 14 +-
.../task-table-action-dialog.spec.tsx.snap | 22 +-
.../task-table-action-dialog.tsx | 40 +-
.../supervisor-status/supervisor-status.ts | 85 +++-
web-console/src/utils/druid-query.ts | 9 +-
web-console/src/utils/general.tsx | 12 +
.../src/utils/local-storage-backed-visibility.tsx | 4 +-
web-console/src/utils/table-helpers.ts | 19 +
.../__snapshots__/datasources-view.spec.tsx.snap | 3 +-
.../views/datasources-view/datasources-view.tsx | 10 +-
.../src/views/load-data-view/info-messages.tsx | 6 +-
.../src/views/load-data-view/load-data-view.tsx | 10 +-
.../__snapshots__/lookups-view.spec.tsx.snap | 3 +-
.../src/views/lookups-view/lookups-view.tsx | 3 +-
.../__snapshots__/segments-view.spec.tsx.snap | 2 +-
.../src/views/segments-view/segments-view.tsx | 46 +--
.../__snapshots__/services-view.spec.tsx.snap | 2 +-
.../src/views/services-view/services-view.tsx | 5 +-
.../__snapshots__/supervisors-view.spec.tsx.snap | 148 ++++++-
.../views/supervisors-view/supervisors-view.scss | 13 +
.../views/supervisors-view/supervisors-view.tsx | 451 ++++++++++++++++-----
.../__snapshots__/tasks-view.spec.tsx.snap | 3 +-
web-console/src/views/tasks-view/tasks-view.tsx | 9 +-
.../max-tasks-button/max-tasks-button.tsx | 11 +-
.../src/views/workbench-view/workbench-view.tsx | 18 +-
36 files changed, 853 insertions(+), 320 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/table-column-selector/table-column-selector.spec.tsx
b/web-console/src/components/table-column-selector/table-column-selector.spec.tsx
index e45fd590e63..e04377c9c5b 100644
---
a/web-console/src/components/table-column-selector/table-column-selector.spec.tsx
+++
b/web-console/src/components/table-column-selector/table-column-selector.spec.tsx
@@ -25,7 +25,7 @@ describe('TableColumnSelector', () => {
it('matches snapshot', () => {
const tableColumn = (
<TableColumnSelector
- columns={['a', 'b', 'c']}
+ columns={['a', 'b', { text: 'c', label: 'c-label' }]}
onChange={() => {}}
tableColumnsHidden={['b']}
/>
diff --git
a/web-console/src/components/table-column-selector/table-column-selector.tsx
b/web-console/src/components/table-column-selector/table-column-selector.tsx
index 2a0c2b5a476..d838e98e04d 100644
--- a/web-console/src/components/table-column-selector/table-column-selector.tsx
+++ b/web-console/src/components/table-column-selector/table-column-selector.tsx
@@ -25,9 +25,15 @@ import { MenuCheckbox } from
'../menu-checkbox/menu-checkbox';
import './table-column-selector.scss';
+export type TableColumnSelectorColumn = string | { text: string; label: string
};
+
+function getColumnName(c: TableColumnSelectorColumn) {
+ return typeof c === 'string' ? c : c.text;
+}
+
interface TableColumnSelectorProps {
- columns: string[];
- onChange: (column: string) => void;
+ columns: TableColumnSelectorColumn[];
+ onChange: (columnName: string) => void;
onClose?: (added: number) => void;
tableColumnsHidden: string[];
}
@@ -38,23 +44,28 @@ export const TableColumnSelector = React.memo(function
TableColumnSelector(
const { columns, onChange, onClose, tableColumnsHidden } = props;
const [added, setAdded] = useState(0);
- const isColumnShown = (column: string) =>
!tableColumnsHidden.includes(column);
+ const isColumnShown = (column: TableColumnSelectorColumn) =>
+ !tableColumnsHidden.includes(getColumnName(column));
const checkboxes = (
<Menu className="table-column-selector-menu">
- {columns.map(column => (
- <MenuCheckbox
- text={column}
- key={column}
- checked={isColumnShown(column)}
- onChange={() => {
- if (!isColumnShown(column)) {
- setAdded(added + 1);
- }
- onChange(column);
- }}
- />
- ))}
+ {columns.map(column => {
+ const columnName = getColumnName(column);
+ return (
+ <MenuCheckbox
+ text={columnName}
+ label={typeof column === 'string' ? undefined : column.label}
+ key={columnName}
+ checked={isColumnShown(column)}
+ onChange={() => {
+ if (!isColumnShown(column)) {
+ setAdded(added + 1);
+ }
+ onChange(columnName);
+ }}
+ />
+ );
+ })}
</Menu>
);
diff --git
a/web-console/src/components/timed-button/__snapshots__/timed-button.spec.tsx.snap
b/web-console/src/components/timed-button/__snapshots__/timed-button.spec.tsx.snap
index b030fdb304b..52fbee10242 100644
---
a/web-console/src/components/timed-button/__snapshots__/timed-button.spec.tsx.snap
+++
b/web-console/src/components/timed-button/__snapshots__/timed-button.spec.tsx.snap
@@ -18,7 +18,7 @@ exports[`TimedButton matches snapshot 1`] = `
<Blueprint4.MenuItem
active={false}
disabled={false}
- icon="selection"
+ icon="tick-circle"
multiline={false}
onClick={[Function]}
popoverProps={{}}
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/compaction-history-dialog/compaction-history-dialog.tsx
b/web-console/src/dialogs/compaction-history-dialog/compaction-history-dialog.tsx
index 4cdc916ee74..cb886d0483d 100644
---
a/web-console/src/dialogs/compaction-history-dialog/compaction-history-dialog.tsx
+++
b/web-console/src/dialogs/compaction-history-dialog/compaction-history-dialog.tsx
@@ -16,7 +16,7 @@
* limitations under the License.
*/
-import { Button, Callout, Classes, Code, Dialog, Tab, Tabs } from
'@blueprintjs/core';
+import { Button, Callout, Classes, Dialog, Tab, Tabs, Tag } from
'@blueprintjs/core';
import * as JSONBig from 'json-bigint-native';
import React, { useState } from 'react';
@@ -117,7 +117,7 @@ export const CompactionHistoryDialog = React.memo(function
CompactionHistoryDial
</Tabs>
) : (
<div>
- There is no compaction history for <Code>{datasource}</Code>.
+ There is no compaction history for <Tag
minimal>{datasource}</Tag>.
</div>
)
) : historyState.loading ? (
diff --git
a/web-console/src/dialogs/kill-datasource-dialog/kill-datasource-dialog.tsx
b/web-console/src/dialogs/kill-datasource-dialog/kill-datasource-dialog.tsx
index f95a5a5d3b8..dba85268d00 100644
--- a/web-console/src/dialogs/kill-datasource-dialog/kill-datasource-dialog.tsx
+++ b/web-console/src/dialogs/kill-datasource-dialog/kill-datasource-dialog.tsx
@@ -16,7 +16,7 @@
* limitations under the License.
*/
-import { Code, Intent } from '@blueprintjs/core';
+import { Intent, Tag } from '@blueprintjs/core';
import React, { useState } from 'react';
import { FormGroupWithInfo, PopoverText } from '../../components';
@@ -74,13 +74,14 @@ export const KillDatasourceDialog = function
KillDatasourceDialog(
warningChecks={[
<>
I understand that this operation will delete all metadata about the
unused segments of{' '}
- <Code>{datasource}</Code> and removes them from deep storage.
+ <Tag minimal>{datasource}</Tag> and removes them from deep storage.
</>,
'I understand that this operation cannot be undone.',
]}
>
<p>
- Are you sure you want to permanently delete unused segments in
<Code>{datasource}</Code>?
+ Are you sure you want to permanently delete unused segments in{' '}
+ <Tag minimal>{datasource}</Tag>?
</p>
<p>This action is not reversible and the data deleted will be lost.</p>
<FormGroupWithInfo
diff --git
a/web-console/src/dialogs/supervisor-table-action-dialog/__snapshots__/supervisor-table-action-dialog.spec.tsx.snap
b/web-console/src/dialogs/supervisor-table-action-dialog/__snapshots__/supervisor-table-action-dialog.spec.tsx.snap
index 7aaa8b1afa2..68cef1ace6e 100755
---
a/web-console/src/dialogs/supervisor-table-action-dialog/__snapshots__/supervisor-table-action-dialog.spec.tsx.snap
+++
b/web-console/src/dialogs/supervisor-table-action-dialog/__snapshots__/supervisor-table-action-dialog.spec.tsx.snap
@@ -116,7 +116,7 @@ exports[`SupervisorTableActionDialog matches snapshot 1`] =
`
<span
class="bp4-button-text"
>
- Statistics
+ Task stats
</span>
</button>
<button
@@ -144,7 +144,7 @@ exports[`SupervisorTableActionDialog matches snapshot 1`] =
`
<span
class="bp4-button-text"
>
- Payload
+ Spec
</span>
</button>
<button
diff --git
a/web-console/src/dialogs/supervisor-table-action-dialog/supervisor-statistics-table/__snapshots__/supervisor-statistics-table.spec.tsx.snap
b/web-console/src/dialogs/supervisor-table-action-dialog/supervisor-statistics-table/__snapshots__/supervisor-statistics-table.spec.tsx.snap
index 7ef6d412fb1..9f61d78e1ec 100644
---
a/web-console/src/dialogs/supervisor-table-action-dialog/supervisor-statistics-table/__snapshots__/supervisor-statistics-table.spec.tsx.snap
+++
b/web-console/src/dialogs/supervisor-table-action-dialog/supervisor-statistics-table/__snapshots__/supervisor-statistics-table.spec.tsx.snap
@@ -78,17 +78,22 @@ exports[`SupervisorStatisticsTable matches snapshot on
error 1`] = `
}
columns={
[
+ {
+ "Header": "Group ID",
+ "accessor": "groupId",
+ "className": "padded",
+ "width": 100,
+ },
{
"Header": "Task ID",
- "accessor": [Function],
+ "accessor": "taskId",
"className": "padded",
- "id": "task_id",
"width": 400,
},
{
"Cell": [Function],
"Header": "Totals",
- "accessor": [Function],
+ "accessor": "rowStats.totals.buildSegments",
"className": "padded",
"id": "total",
"width": 200,
@@ -256,17 +261,22 @@ exports[`SupervisorStatisticsTable matches snapshot on
init 1`] = `
}
columns={
[
+ {
+ "Header": "Group ID",
+ "accessor": "groupId",
+ "className": "padded",
+ "width": 100,
+ },
{
"Header": "Task ID",
- "accessor": [Function],
+ "accessor": "taskId",
"className": "padded",
- "id": "task_id",
"width": 400,
},
{
"Cell": [Function],
"Header": "Totals",
- "accessor": [Function],
+ "accessor": "rowStats.totals.buildSegments",
"className": "padded",
"id": "total",
"width": 200,
@@ -460,17 +470,22 @@ exports[`SupervisorStatisticsTable matches snapshot on no
data 1`] = `
}
columns={
[
+ {
+ "Header": "Group ID",
+ "accessor": "groupId",
+ "className": "padded",
+ "width": 100,
+ },
{
"Header": "Task ID",
- "accessor": [Function],
+ "accessor": "taskId",
"className": "padded",
- "id": "task_id",
"width": 400,
},
{
"Cell": [Function],
"Header": "Totals",
- "accessor": [Function],
+ "accessor": "rowStats.totals.buildSegments",
"className": "padded",
"id": "total",
"width": 200,
@@ -638,17 +653,22 @@ exports[`SupervisorStatisticsTable matches snapshot on
some data 1`] = `
}
columns={
[
+ {
+ "Header": "Group ID",
+ "accessor": "groupId",
+ "className": "padded",
+ "width": 100,
+ },
{
"Header": "Task ID",
- "accessor": [Function],
+ "accessor": "taskId",
"className": "padded",
- "id": "task_id",
"width": 400,
},
{
"Cell": [Function],
"Header": "Totals",
- "accessor": [Function],
+ "accessor": "rowStats.totals.buildSegments",
"className": "padded",
"id": "total",
"width": 200,
@@ -656,7 +676,7 @@ exports[`SupervisorStatisticsTable matches snapshot on some
data 1`] = `
{
"Cell": [Function],
"Header": "1m",
- "accessor": [Function],
+ "accessor": "rowStats.movingAverages.buildSegments.1m",
"className": "padded",
"id": "1m",
"width": 200,
@@ -664,7 +684,7 @@ exports[`SupervisorStatisticsTable matches snapshot on some
data 1`] = `
{
"Cell": [Function],
"Header": "5m",
- "accessor": [Function],
+ "accessor": "rowStats.movingAverages.buildSegments.5m",
"className": "padded",
"id": "5m",
"width": 200,
@@ -672,7 +692,7 @@ exports[`SupervisorStatisticsTable matches snapshot on some
data 1`] = `
{
"Cell": [Function],
"Header": "15m",
- "accessor": [Function],
+ "accessor": "rowStats.movingAverages.buildSegments.15m",
"className": "padded",
"id": "15m",
"width": 200,
@@ -682,23 +702,27 @@ exports[`SupervisorStatisticsTable matches snapshot on
some data 1`] = `
data={
[
{
- "summary": {
+ "groupId": "0",
+ "rowStats": {
"movingAverages": {
"buildSegments": {
"15m": {
"processed": 5.544749689510444,
+ "processedBytes": 20,
"processedWithError": 0,
"thrownAway": 0,
"unparseable": 0,
},
"1m": {
"processed": 4.593670088770785,
+ "processedBytes": 30,
"processedWithError": 0,
"thrownAway": 0,
"unparseable": 0,
},
"5m": {
"processed": 3.5455993615040584,
+ "processedBytes": 10,
"processedWithError": 0,
"thrownAway": 0,
"unparseable": 0,
@@ -708,6 +732,7 @@ exports[`SupervisorStatisticsTable matches snapshot on some
data 1`] = `
"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.spec.tsx
b/web-console/src/dialogs/supervisor-table-action-dialog/supervisor-statistics-table/supervisor-statistics-table.spec.tsx
index 3ea907dc0cf..922342550f7 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
@@ -28,7 +28,7 @@ import {
} from './supervisor-statistics-table';
let supervisorStatisticsState: QueryState<SupervisorStatisticsTableRow[]> =
QueryState.INIT;
-jest.mock('../../../hooks', () => {
+jest.mock('../../../hooks/use-query-manager', () => {
return {
useQueryManager: () => [supervisorStatisticsState],
};
@@ -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..49525dfb870 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,35 +22,26 @@ import type { CellInfo, Column } from 'react-table';
import ReactTable from 'react-table';
import { Loader } from '../../../components/loader/loader';
-import { useQueryManager } from '../../../hooks';
+import type { RowStats, RowStatsCounter, SupervisorStats } from
'../../../druid-models';
+import { useInterval, 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 {
+ groupId: string;
taskId: string;
- summary: TaskSummary;
+ rowStats: 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] })));
+ return Object.entries(data).flatMap(([groupId, v]) =>
+ Object.entries(v).map(([taskId, rowStats]) => ({ groupId, taskId, rowStats
})),
+ );
}
export interface SupervisorStatisticsTableProps {
@@ -62,34 +53,54 @@ export const SupervisorStatisticsTable =
React.memo(function SupervisorStatistic
props: SupervisorStatisticsTableProps,
) {
const { supervisorId } = props;
- const endpoint =
`/druid/indexer/v1/supervisor/${Api.encodePath(supervisorId)}/stats`;
+ const statsEndpoint =
`/druid/indexer/v1/supervisor/${Api.encodePath(supervisorId)}/stats`;
- const [supervisorStatisticsState] = useQueryManager<null,
SupervisorStatisticsTableRow[]>({
+ const [supervisorStatisticsState, supervisorStatisticsQueryManager] =
useQueryManager<
+ null,
+ SupervisorStatisticsTableRow[]
+ >({
+ initQuery: null,
processQuery: async () => {
- const resp = await Api.instance.get(endpoint);
+ const resp = await Api.instance.get<SupervisorStats>(statsEndpoint);
return normalizeSupervisorStatisticsResults(resp.data);
},
- initQuery: null,
});
- function renderCell(cell: CellInfo) {
- const cellValue = cell.value;
- if (!cellValue) {
- return <div>No data found</div>;
- }
+ useInterval(() => {
+ supervisorStatisticsQueryManager.rerunLastQuery(true);
+ }, 1500);
- return Object.keys(cellValue)
- .sort()
- .map(key => <div key={key}>{`${key}:
${Number(cellValue[key]).toFixed(1)}`}</div>);
+ function renderCounters(cell: CellInfo, isRate: boolean) {
+ const c: RowStatsCounter = cell.value;
+ if (!c) return null;
+
+ 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() {
let columns: Column<SupervisorStatisticsTableRow>[] = [
+ {
+ Header: 'Group ID',
+ accessor: 'groupId',
+ className: 'padded',
+ width: 100,
+ },
{
Header: 'Task ID',
- id: 'task_id',
+ accessor: 'taskId',
className: 'padded',
- accessor: d => d.taskId,
width: 400,
},
{
@@ -97,16 +108,14 @@ export const SupervisorStatisticsTable =
React.memo(function SupervisorStatistic
id: 'total',
className: 'padded',
width: 200,
- accessor: d => {
- return deepGet(d, 'summary.totals.buildSegments') as StatsEntry;
- },
- Cell: renderCell,
+ accessor: 'rowStats.totals.buildSegments',
+ Cell: c => renderCounters(c, false),
},
];
const movingAveragesBuildSegments = deepGet(
supervisorStatisticsState.data as any,
- '0.summary.movingAverages.buildSegments',
+ '0.rowStats.movingAverages.buildSegments',
);
if (movingAveragesBuildSegments) {
columns = columns.concat(
@@ -118,10 +127,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: `rowStats.movingAverages.buildSegments.${interval}`,
+ Cell: c => renderCounters(c, true),
};
}),
);
@@ -148,7 +155,7 @@ export const SupervisorStatisticsTable =
React.memo(function SupervisorStatistic
text="View raw"
disabled={supervisorStatisticsState.loading}
minimal
- onClick={() => window.open(UrlBaser.base(endpoint), '_blank')}
+ onClick={() => window.open(UrlBaser.base(statsEndpoint), '_blank')}
/>
</ButtonGroup>
</div>
diff --git
a/web-console/src/dialogs/supervisor-table-action-dialog/supervisor-table-action-dialog.tsx
b/web-console/src/dialogs/supervisor-table-action-dialog/supervisor-table-action-dialog.tsx
index 02d9e3c28b8..5e3d9e50028 100644
---
a/web-console/src/dialogs/supervisor-table-action-dialog/supervisor-table-action-dialog.tsx
+++
b/web-console/src/dialogs/supervisor-table-action-dialog/supervisor-table-action-dialog.tsx
@@ -28,6 +28,8 @@ import { TableActionDialog } from
'../table-action-dialog/table-action-dialog';
import { SupervisorStatisticsTable } from
'./supervisor-statistics-table/supervisor-statistics-table';
+type SupervisorTableActionDialogTab = 'status' | 'stats' | 'spec' | 'history';
+
interface SupervisorTableActionDialogProps {
supervisorId: string;
actions: BasicAction[];
@@ -38,7 +40,7 @@ export const SupervisorTableActionDialog =
React.memo(function SupervisorTableAc
props: SupervisorTableActionDialogProps,
) {
const { supervisorId, actions, onClose } = props;
- const [activeTab, setActiveTab] = useState('status');
+ const [activeTab, setActiveTab] =
useState<SupervisorTableActionDialogTab>('status');
const supervisorTableSideButtonMetadata: SideButtonMetaData[] = [
{
@@ -49,15 +51,15 @@ export const SupervisorTableActionDialog =
React.memo(function SupervisorTableAc
},
{
icon: 'chart',
- text: 'Statistics',
+ text: 'Task stats',
active: activeTab === 'stats',
onClick: () => setActiveTab('stats'),
},
{
icon: 'align-left',
- text: 'Payload',
- active: activeTab === 'payload',
- onClick: () => setActiveTab('payload'),
+ text: 'Spec',
+ active: activeTab === 'spec',
+ onClick: () => setActiveTab('spec'),
},
{
icon: 'history',
@@ -88,7 +90,7 @@ export const SupervisorTableActionDialog =
React.memo(function SupervisorTableAc
downloadFilename={`supervisor-stats-${supervisorId}.json`}
/>
)}
- {activeTab === 'payload' && (
+ {activeTab === 'spec' && (
<ShowJson
endpoint={supervisorEndpointBase}
transform={x => cleanSpec(x, true)}
diff --git
a/web-console/src/dialogs/task-table-action-dialog/__snapshots__/task-table-action-dialog.spec.tsx.snap
b/web-console/src/dialogs/task-table-action-dialog/__snapshots__/task-table-action-dialog.spec.tsx.snap
index 4c0ceed638d..63e1e50a863 100644
---
a/web-console/src/dialogs/task-table-action-dialog/__snapshots__/task-table-action-dialog.spec.tsx.snap
+++
b/web-console/src/dialogs/task-table-action-dialog/__snapshots__/task-table-action-dialog.spec.tsx.snap
@@ -97,18 +97,18 @@ exports[`TaskTableActionDialog matches snapshot 1`] = `
>
<span
aria-hidden="true"
- class="bp4-icon bp4-icon-align-left"
- icon="align-left"
+ class="bp4-icon bp4-icon-comparison"
+ icon="comparison"
>
<svg
- data-icon="align-left"
+ data-icon="comparison"
height="20"
role="img"
viewBox="0 0 20 20"
width="20"
>
<path
- d="M1 7h10c.55 0 1-.45 1-1s-.45-1-1-1H1c-.55 0-1 .45-1
1s.45 1 1 1zm0-4h18c.55 0 1-.45 1-1s-.45-1-1-1H1c-.55 0-1 .45-1 1s.45 1 1 1zm14
14H1c-.55 0-1 .45-1 1s.45 1 1 1h14c.55 0 1-.45 1-1s-.45-1-1-1zm4-8H1c-.55 0-1
.45-1 1s.45 1 1 1h18c.55 0 1-.45 1-1s-.45-1-1-1zM1 15h6c.55 0 1-.45
1-1s-.45-1-1-1H1c-.55 0-1 .45-1 1s.45 1 1 1z"
+ d="M6 8H1c-.55 0-1 .45-1 1v2c0 .55.45 1 1 1h5c.55 0 1-.45
1-1V9c0-.55-.45-1-1-1zm13-6h-5c-.55 0-1 .45-1 1v2c0 .55.45 1 1 1h5c.55 0 1-.45
1-1V3c0-.55-.45-1-1-1zm0 3h-5V3h5v2zM6 14H1c-.55 0-1 .45-1 1v2c0 .55.45 1 1
1h5c.55 0 1-.45 1-1v-2c0-.55-.45-1-1-1zM6 2H1c-.55 0-1 .45-1 1v2c0 .55.45 1 1
1h5c.55 0 1-.45 1-1V3c0-.55-.45-1-1-1zm4-2c-.55 0-1 .45-1 1v18c0 .55.45 1 1
1s1-.45 1-1V1c0-.55-.45-1-1-1zm9 14h-5c-.55 0-1 .45-1 1v2c0 .55.45 1 1 1h5c.55
0 1-.45 1-1v-2c0-.55-.45-1 [...]
fill-rule="evenodd"
/>
</svg>
@@ -116,7 +116,7 @@ exports[`TaskTableActionDialog matches snapshot 1`] = `
<span
class="bp4-button-text"
>
- Payload
+ Reports
</span>
</button>
<button
@@ -125,18 +125,18 @@ exports[`TaskTableActionDialog matches snapshot 1`] = `
>
<span
aria-hidden="true"
- class="bp4-icon bp4-icon-comparison"
- icon="comparison"
+ class="bp4-icon bp4-icon-align-left"
+ icon="align-left"
>
<svg
- data-icon="comparison"
+ data-icon="align-left"
height="20"
role="img"
viewBox="0 0 20 20"
width="20"
>
<path
- d="M6 8H1c-.55 0-1 .45-1 1v2c0 .55.45 1 1 1h5c.55 0 1-.45
1-1V9c0-.55-.45-1-1-1zm13-6h-5c-.55 0-1 .45-1 1v2c0 .55.45 1 1 1h5c.55 0 1-.45
1-1V3c0-.55-.45-1-1-1zm0 3h-5V3h5v2zM6 14H1c-.55 0-1 .45-1 1v2c0 .55.45 1 1
1h5c.55 0 1-.45 1-1v-2c0-.55-.45-1-1-1zM6 2H1c-.55 0-1 .45-1 1v2c0 .55.45 1 1
1h5c.55 0 1-.45 1-1V3c0-.55-.45-1-1-1zm4-2c-.55 0-1 .45-1 1v18c0 .55.45 1 1
1s1-.45 1-1V1c0-.55-.45-1-1-1zm9 14h-5c-.55 0-1 .45-1 1v2c0 .55.45 1 1 1h5c.55
0 1-.45 1-1v-2c0-.55-.45-1 [...]
+ d="M1 7h10c.55 0 1-.45 1-1s-.45-1-1-1H1c-.55 0-1 .45-1
1s.45 1 1 1zm0-4h18c.55 0 1-.45 1-1s-.45-1-1-1H1c-.55 0-1 .45-1 1s.45 1 1 1zm14
14H1c-.55 0-1 .45-1 1s.45 1 1 1h14c.55 0 1-.45 1-1s-.45-1-1-1zm4-8H1c-.55 0-1
.45-1 1s.45 1 1 1h18c.55 0 1-.45 1-1s-.45-1-1-1zM1 15h6c.55 0 1-.45
1-1s-.45-1-1-1H1c-.55 0-1 .45-1 1s.45 1 1 1z"
fill-rule="evenodd"
/>
</svg>
@@ -144,7 +144,7 @@ exports[`TaskTableActionDialog matches snapshot 1`] = `
<span
class="bp4-button-text"
>
- Reports
+ Spec
</span>
</button>
<button
@@ -172,7 +172,7 @@ exports[`TaskTableActionDialog matches snapshot 1`] = `
<span
class="bp4-button-text"
>
- Logs
+ Log
</span>
</button>
</div>
diff --git
a/web-console/src/dialogs/task-table-action-dialog/task-table-action-dialog.tsx
b/web-console/src/dialogs/task-table-action-dialog/task-table-action-dialog.tsx
index a0a5dbbf13f..9edc5d996f4 100644
---
a/web-console/src/dialogs/task-table-action-dialog/task-table-action-dialog.tsx
+++
b/web-console/src/dialogs/task-table-action-dialog/task-table-action-dialog.tsx
@@ -25,18 +25,20 @@ import type { BasicAction } from '../../utils/basic-action';
import type { SideButtonMetaData } from
'../table-action-dialog/table-action-dialog';
import { TableActionDialog } from '../table-action-dialog/table-action-dialog';
+type TaskTableActionDialogTab = 'status' | 'report' | 'spec' | 'log';
+
interface TaskTableActionDialogProps {
taskId: string;
actions: BasicAction[];
- onClose: () => void;
status: string;
+ onClose(): void;
}
export const TaskTableActionDialog = React.memo(function TaskTableActionDialog(
props: TaskTableActionDialogProps,
) {
const { taskId, actions, onClose, status } = props;
- const [activeTab, setActiveTab] = useState('status');
+ const [activeTab, setActiveTab] =
useState<TaskTableActionDialogTab>('status');
const taskTableSideButtonMetadata: SideButtonMetaData[] = [
{
@@ -45,21 +47,21 @@ export const TaskTableActionDialog = React.memo(function
TaskTableActionDialog(
active: activeTab === 'status',
onClick: () => setActiveTab('status'),
},
- {
- icon: 'align-left',
- text: 'Payload',
- active: activeTab === 'payload',
- onClick: () => setActiveTab('payload'),
- },
{
icon: 'comparison',
text: 'Reports',
- active: activeTab === 'reports',
- onClick: () => setActiveTab('reports'),
+ active: activeTab === 'report',
+ onClick: () => setActiveTab('report'),
+ },
+ {
+ icon: 'align-left',
+ text: 'Spec',
+ active: activeTab === 'spec',
+ onClick: () => setActiveTab('spec'),
},
{
icon: 'align-justify',
- text: 'Logs',
+ text: 'Log',
active: activeTab === 'log',
onClick: () => setActiveTab('log'),
},
@@ -80,20 +82,20 @@ export const TaskTableActionDialog = React.memo(function
TaskTableActionDialog(
downloadFilename={`task-status-${taskId}.json`}
/>
)}
- {activeTab === 'payload' && (
- <ShowJson
- endpoint={taskEndpointBase}
- transform={x => deepGet(x, 'payload') || x}
- downloadFilename={`task-payload-${taskId}.json`}
- />
- )}
- {activeTab === 'reports' && (
+ {activeTab === 'report' && (
<ShowJson
endpoint={`${taskEndpointBase}/reports`}
transform={x => deepGet(x, 'ingestionStatsAndErrors.payload') || x}
downloadFilename={`task-reports-${taskId}.json`}
/>
)}
+ {activeTab === 'spec' && (
+ <ShowJson
+ endpoint={taskEndpointBase}
+ transform={x => deepGet(x, 'payload') || x}
+ downloadFilename={`task-payload-${taskId}.json`}
+ />
+ )}
{activeTab === 'log' && (
<ShowLog
tail={status === 'RUNNING'}
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..3004cf350e7 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 { max, sum } from 'd3-array';
+
import type { NumberLike } from '../../utils';
+import { deepGet, filterMap } from '../../utils';
export type SupervisorOffsetMap = Record<string, NumberLike>;
@@ -39,16 +42,94 @@ export interface SupervisorStatus {
healthy: boolean;
state: string;
detailedState: string;
- recentErrors: any[];
+ recentErrors: SupervisorError[];
};
}
export interface SupervisorStatusTask {
id: string;
startingOffsets: SupervisorOffsetMap;
- startTime: '2024-04-12T21:35:34.834Z';
+ startTime: string;
remainingSeconds: number;
type: string;
currentOffsets: SupervisorOffsetMap;
lag: SupervisorOffsetMap;
}
+
+export interface SupervisorError {
+ timestamp: string;
+ exceptionClass: string;
+ message: string;
+ streamException: boolean;
+}
+
+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 sumRowStatsCounter(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 maxRowStatsCounter(rowStats: RowStatsCounter[]): RowStatsCounter {
+ return {
+ processed: max(rowStats, d => d.processed) ?? 0,
+ processedBytes: max(rowStats, d => d.processedBytes) ?? 0,
+ processedWithError: max(rowStats, d => d.processedWithError) ?? 0,
+ thrownAway: max(rowStats, d => d.thrownAway) ?? 0,
+ unparseable: max(rowStats, d => d.unparseable) ?? 0,
+ };
+}
+
+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,
+ activeTaskIds: string[] | undefined,
+): RowStatsCounter {
+ return sumRowStatsCounter(
+ Object.values(stats).map(s =>
+ maxRowStatsCounter(
+ filterMap(Object.entries(s), ([taskId, rs]) =>
+ !activeTaskIds || activeTaskIds.includes(taskId)
+ ? getRowStatsCounter(rs, key)
+ : undefined,
+ ),
+ ),
+ ),
+ );
+}
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/local-storage-backed-visibility.tsx
b/web-console/src/utils/local-storage-backed-visibility.tsx
index c335180056b..f20031f2b8d 100644
--- a/web-console/src/utils/local-storage-backed-visibility.tsx
+++ b/web-console/src/utils/local-storage-backed-visibility.tsx
@@ -65,7 +65,7 @@ export class LocalStorageBackedVisibility {
return new LocalStorageBackedVisibility(this.key, defaultHidden,
newVisibility);
}
- public shown(value: string): boolean {
- return this.visibility[value] ?? !this.defaultHidden.includes(value);
+ public shown(...values: string[]): boolean {
+ return values.some(value => this.visibility[value] ??
!this.defaultHidden.includes(value));
}
}
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/datasources-view/__snapshots__/datasources-view.spec.tsx.snap
b/web-console/src/views/datasources-view/__snapshots__/datasources-view.spec.tsx.snap
index 2cd926e01f9..b627b2e500c 100644
---
a/web-console/src/views/datasources-view/__snapshots__/datasources-view.spec.tsx.snap
+++
b/web-console/src/views/datasources-view/__snapshots__/datasources-view.spec.tsx.snap
@@ -81,7 +81,6 @@ exports[`DatasourcesView matches snapshot 1`] = `
"% Compacted",
"Left to be compacted",
"Retention",
- "Actions",
]
}
onChange={[Function]}
@@ -338,7 +337,7 @@ exports[`DatasourcesView matches snapshot 1`] = `
"accessor": "datasource",
"filterable": false,
"id": "actions",
- "show": true,
+ "sortable": false,
"width": 70,
},
]
diff --git a/web-console/src/views/datasources-view/datasources-view.tsx
b/web-console/src/views/datasources-view/datasources-view.tsx
index 75541b82999..713df9b18b1 100644
--- a/web-console/src/views/datasources-view/datasources-view.tsx
+++ b/web-console/src/views/datasources-view/datasources-view.tsx
@@ -102,7 +102,6 @@ const tableColumns: Record<CapabilitiesMode, string[]> = {
'% Compacted',
'Left to be compacted',
'Retention',
- ACTION_COLUMN_LABEL,
],
'no-sql': [
'Datasource name',
@@ -114,7 +113,6 @@ const tableColumns: Record<CapabilitiesMode, string[]> = {
'% Compacted',
'Left to be compacted',
'Retention',
- ACTION_COLUMN_LABEL,
],
'no-proxy': [
'Datasource name',
@@ -128,7 +126,6 @@ const tableColumns: Record<CapabilitiesMode, string[]> = {
'Total rows',
'Avg. row size',
'Replicated size',
- ACTION_COLUMN_LABEL,
],
};
@@ -338,12 +335,11 @@ export class DatasourcesView extends React.PureComponent<
const columns = compact(
[
visibleColumns.shown('Datasource name') && `datasource`,
- (visibleColumns.shown('Availability') || visibleColumns.shown('Segment
granularity')) && [
+ visibleColumns.shown('Availability', 'Segment granularity') && [
`COUNT(*) FILTER (WHERE is_active = 1) AS num_segments`,
`COUNT(*) FILTER (WHERE is_published = 1 AND is_overshadowed = 0 AND
replication_factor = 0) AS num_zero_replica_segments`,
],
- (visibleColumns.shown('Availability') ||
- visibleColumns.shown('Historical load/drop queues')) && [
+ visibleColumns.shown('Availability', 'Historical load/drop queues') &&
[
`COUNT(*) FILTER (WHERE is_published = 1 AND is_overshadowed = 0 AND
is_available = 0 AND replication_factor > 0) AS num_segments_to_load`,
`COUNT(*) FILTER (WHERE is_available = 1 AND is_active = 0) AS
num_segments_to_drop`,
],
@@ -1577,11 +1573,11 @@ GROUP BY 1, 2`;
},
{
Header: ACTION_COLUMN_LABEL,
- show: visibleColumns.shown(ACTION_COLUMN_LABEL),
accessor: 'datasource',
id: ACTION_COLUMN_ID,
width: ACTION_COLUMN_WIDTH,
filterable: false,
+ sortable: false,
Cell: ({ value: datasource, original }) => {
const { unused, rules, compaction } = original as Datasource;
const datasourceActions = this.getDatasourceActions(
diff --git a/web-console/src/views/load-data-view/info-messages.tsx
b/web-console/src/views/load-data-view/info-messages.tsx
index b88cf8a70c2..ad9e96667db 100644
--- a/web-console/src/views/load-data-view/info-messages.tsx
+++ b/web-console/src/views/load-data-view/info-messages.tsx
@@ -16,7 +16,7 @@
* limitations under the License.
*/
-import { Button, Callout, Code, FormGroup, Intent } from '@blueprintjs/core';
+import { Button, Callout, Code, FormGroup, Intent, Tag } from
'@blueprintjs/core';
import React from 'react';
import { ExternalLink, LearnMore } from '../../components';
@@ -236,8 +236,8 @@ export const AppendToExistingIssue = React.memo(function
AppendToExistingIssue(
<FormGroup>
<Callout intent={Intent.DANGER}>
<p>
- Only <Code>dynamic</Code> partitioning supports
<Code>appendToExisting: true</Code>. You
- have currently selected <Code>{partitionsSpecType}</Code>
partitioning.
+ Only <Tag minimal>dynamic</Tag> partitioning supports
<Code>appendToExisting: true</Code>.
+ You have currently selected <Tag minimal>{partitionsSpecType}</Tag>
partitioning.
</p>
<Button
intent={Intent.SUCCESS}
diff --git a/web-console/src/views/load-data-view/load-data-view.tsx
b/web-console/src/views/load-data-view/load-data-view.tsx
index 2bd3623fa0e..5e907a24267 100644
--- a/web-console/src/views/load-data-view/load-data-view.tsx
+++ b/web-console/src/views/load-data-view/load-data-view.tsx
@@ -34,6 +34,7 @@ import {
Radio,
RadioGroup,
Switch,
+ Tag,
TextArea,
} from '@blueprintjs/core';
import { IconNames } from '@blueprintjs/icons';
@@ -3073,8 +3074,9 @@ export class LoadDataView extends
React.PureComponent<LoadDataViewProps, LoadDat
<p>Your partitioning and sorting configuration is uncommon.</p>
<p>
For best performance the first dimension in your schema (
- <Code>{firstDimensionName}</Code>), which is what the data
will be primarily sorted
- on, commonly matches the partitioning dimension
(<Code>{partitionDimension}</Code>).
+ <Tag minimal>{firstDimensionName}</Tag>), which is what the
data will be primarily
+ sorted on, commonly matches the partitioning dimension (
+ <Tag minimal>{partitionDimension}</Tag>).
</p>
<p>
<Button
@@ -3451,11 +3453,11 @@ export class LoadDataView extends
React.PureComponent<LoadDataViewProps, LoadDat
<p>
You have enabled type-aware schema discovery (
<Code>useSchemaDiscovery: true</Code>) to ingest data into
the existing
- datasource <Code>{datasource}</Code>.
+ datasource <Tag minimal>{datasource}</Tag>.
</p>
<p>
If you used string-based schema discovery when first
ingesting data to{' '}
- <Code>{datasource}</Code>, using type-aware schema
discovery now can cause
+ <Tag minimal>{datasource}</Tag>, using type-aware schema
discovery now can cause
problems with the values multi-value string dimensions.
</p>
<p>
diff --git
a/web-console/src/views/lookups-view/__snapshots__/lookups-view.spec.tsx.snap
b/web-console/src/views/lookups-view/__snapshots__/lookups-view.spec.tsx.snap
index f612725e298..07fbc786424 100755
---
a/web-console/src/views/lookups-view/__snapshots__/lookups-view.spec.tsx.snap
+++
b/web-console/src/views/lookups-view/__snapshots__/lookups-view.spec.tsx.snap
@@ -25,7 +25,6 @@ exports[`LookupsView matches snapshot 1`] = `
"Version",
"Poll period",
"Summary",
- "Actions",
]
}
onChange={[Function]}
@@ -148,7 +147,7 @@ exports[`LookupsView matches snapshot 1`] = `
"accessor": "id",
"filterable": false,
"id": "actions",
- "show": true,
+ "sortable": false,
"width": 70,
},
]
diff --git a/web-console/src/views/lookups-view/lookups-view.tsx
b/web-console/src/views/lookups-view/lookups-view.tsx
index 8f19e55d4ca..af8207f6ab1 100644
--- a/web-console/src/views/lookups-view/lookups-view.tsx
+++ b/web-console/src/views/lookups-view/lookups-view.tsx
@@ -60,7 +60,6 @@ const tableColumns: string[] = [
'Version',
'Poll period',
'Summary',
- ACTION_COLUMN_LABEL,
];
const DEFAULT_LOOKUP_TIER = '__default';
@@ -442,10 +441,10 @@ export class LookupsView extends
React.PureComponent<LookupsViewProps, LookupsVi
},
{
Header: ACTION_COLUMN_LABEL,
- show: visibleColumns.shown(ACTION_COLUMN_LABEL),
id: ACTION_COLUMN_ID,
width: ACTION_COLUMN_WIDTH,
filterable: false,
+ sortable: false,
accessor: 'id',
Cell: ({ original }) => {
const lookupId = original.id;
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 d37f1ebe3e6..9c7e40197a6 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
@@ -70,7 +70,6 @@ exports[`SegmentsView matches snapshot 1`] = `
"Is realtime",
"Is published",
"Is overshadowed",
- "Actions",
]
}
onChange={[Function]}
@@ -356,6 +355,7 @@ exports[`SegmentsView matches snapshot 1`] = `
"filterable": false,
"id": "actions",
"show": true,
+ "sortable": false,
"width": 70,
},
]
diff --git a/web-console/src/views/segments-view/segments-view.tsx
b/web-console/src/views/segments-view/segments-view.tsx
index ae40a8d641e..7d7bcaeec46 100644
--- a/web-console/src/views/segments-view/segments-view.tsx
+++ b/web-console/src/views/segments-view/segments-view.tsx
@@ -16,7 +16,7 @@
* limitations under the License.
*/
-import { Button, ButtonGroup, Code, Intent, Label, MenuItem, Switch } from
'@blueprintjs/core';
+import { Button, ButtonGroup, Intent, Label, MenuItem, Switch, Tag } from
'@blueprintjs/core';
import { IconNames } from '@blueprintjs/icons';
import { C, L, SqlComparison, SqlExpression } from '@druid-toolkit/query';
import classNames from 'classnames';
@@ -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';
@@ -96,18 +97,8 @@ const tableColumns: Record<CapabilitiesMode, string[]> = {
'Is realtime',
'Is published',
'Is overshadowed',
- ACTION_COLUMN_LABEL,
- ],
- 'no-sql': [
- 'Segment ID',
- 'Datasource',
- 'Start',
- 'End',
- 'Version',
- 'Partition',
- 'Size',
- ACTION_COLUMN_LABEL,
],
+ 'no-sql': ['Segment ID', 'Datasource', 'Start', 'End', 'Version',
'Partition', 'Size'],
'no-proxy': [
'Segment ID',
'Datasource',
@@ -134,23 +125,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;
@@ -217,7 +191,7 @@ export class SegmentsView extends
React.PureComponent<SegmentsViewProps, Segment
WHEN "start" LIKE '%:00.000Z' AND "end" LIKE '%:00.000Z' THEN 'Minute'
ELSE 'Sub minute'
END AS "time_span"`,
- (visibleColumns.shown('Shard type') || visibleColumns.shown('Shard
spec')) && `"shard_spec"`,
+ visibleColumns.shown('Shard type', 'Shard spec') && `"shard_spec"`,
visibleColumns.shown('Partition') && `"partition_num"`,
visibleColumns.shown('Size') && `"size"`,
visibleColumns.shown('Num rows') && `"num_rows"`,
@@ -471,7 +445,8 @@ END AS "time_span"`,
const { capabilities } = this.props;
const { visibleColumns } = this.state;
if (tableState) this.lastTableState = tableState;
- const { page, pageSize, filtered, sorted } = this.lastTableState!;
+ if (!this.lastTableState) return;
+ const { page, pageSize, filtered, sorted } = this.lastTableState;
this.segmentsQueryManager.runQuery({
page,
pageSize,
@@ -895,11 +870,12 @@ END AS "time_span"`,
},
{
Header: ACTION_COLUMN_LABEL,
- show: capabilities.hasCoordinatorAccess() &&
visibleColumns.shown(ACTION_COLUMN_LABEL),
+ show: capabilities.hasCoordinatorAccess(),
id: ACTION_COLUMN_ID,
accessor: 'segment_id',
width: ACTION_COLUMN_WIDTH,
filterable: false,
+ sortable: false,
Cell: row => {
if (row.aggregated) return '';
const id = row.value;
@@ -935,7 +911,7 @@ END AS "time_span"`,
);
return resp.data;
}}
- confirmButtonText="Drop Segment"
+ confirmButtonText="Drop segment"
successText="Segment drop request acknowledged, next time the
coordinator runs segment will be dropped"
failText="Could not drop segment"
intent={Intent.DANGER}
@@ -947,7 +923,7 @@ END AS "time_span"`,
}}
>
<p>
- Are you sure you want to drop segment
<Code>{terminateSegmentId}</Code>?
+ Are you sure you want to drop segment <Tag
minimal>{terminateSegmentId}</Tag>?
</p>
<p>This action is not reversible.</p>
</AsyncActionDialog>
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 baedf5165d6..93e47b06e27 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
@@ -60,7 +60,6 @@ exports[`ServicesView renders data 1`] = `
"Usage",
"Start time",
"Detail",
- "Actions",
]
}
onChange={[Function]}
@@ -224,6 +223,7 @@ exports[`ServicesView renders data 1`] = `
"filterable": false,
"id": "actions",
"show": true,
+ "sortable": false,
"width": 70,
},
]
diff --git a/web-console/src/views/services-view/services-view.tsx
b/web-console/src/views/services-view/services-view.tsx
index fd9c4caef24..3ff6eead276 100644
--- a/web-console/src/views/services-view/services-view.tsx
+++ b/web-console/src/views/services-view/services-view.tsx
@@ -71,7 +71,6 @@ const tableColumns: Record<CapabilitiesMode, string[]> = {
'Usage',
'Start time',
'Detail',
- ACTION_COLUMN_LABEL,
],
'no-sql': [
'Service',
@@ -83,7 +82,6 @@ const tableColumns: Record<CapabilitiesMode, string[]> = {
'Max size',
'Usage',
'Detail',
- ACTION_COLUMN_LABEL,
],
'no-proxy': [
'Service',
@@ -646,11 +644,12 @@ ORDER BY
},
{
Header: ACTION_COLUMN_LABEL,
- show: capabilities.hasOverlordAccess() &&
visibleColumns.shown(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;
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 6053d75102e..f43b9d3eb90 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
@@ -78,8 +78,23 @@ exports[`SupervisorsView matches snapshot 1`] = `
"Type",
"Topic/Stream",
"Status",
- "Running tasks",
- "Actions",
+ "Configured tasks",
+ {
+ "label": "status API",
+ "text": "Running tasks",
+ },
+ {
+ "label": "status API",
+ "text": "Aggregate lag",
+ },
+ {
+ "label": "status API",
+ "text": "Recent errors",
+ },
+ {
+ "label": "stats API",
+ "text": "Stats",
+ },
]
}
onChange={[Function]}
@@ -155,37 +170,150 @@ exports[`SupervisorsView matches snapshot 1`] = `
"accessor": "supervisor_id",
"id": "supervisor_id",
"show": true,
- "width": 300,
+ "width": 280,
},
{
"Cell": [Function],
"Header": "Type",
"accessor": "type",
"show": true,
- "width": 100,
+ "width": 80,
},
{
"Cell": [Function],
"Header": "Topic/Stream",
"accessor": "source",
"show": true,
- "width": 300,
+ "width": 200,
},
{
"Cell": [Function],
"Header": "Status",
"accessor": "detailed_state",
- "id": "status",
+ "id": "detailed_state",
+ "show": true,
+ "width": 130,
+ },
+ {
+ "Cell": [Function],
+ "Header": "Configured tasks",
+ "accessor": "spec",
+ "className": "padded",
+ "filterable": false,
+ "id": "configured_tasks",
"show": true,
- "width": 250,
+ "sortable": false,
+ "width": 130,
},
{
"Cell": [Function],
"Header": "Running tasks",
- "accessor": "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 />
+ <Blueprint4.Popover2
+ boundary="clippingParents"
+ captureDismiss={false}
+ content={
+ <Blueprint4.Menu>
+ <Blueprint4.MenuItem
+ active={false}
+ disabled={false}
+ icon="circle"
+ multiline={false}
+ onClick={[Function]}
+ popoverProps={{}}
+ selected={false}
+ shouldDismissPopover={true}
+ text="Rate over past 1 minute"
+ />
+ <Blueprint4.MenuItem
+ active={false}
+ disabled={false}
+ icon="tick-circle"
+ multiline={false}
+ onClick={[Function]}
+ popoverProps={{}}
+ selected={false}
+ shouldDismissPopover={true}
+ text="Rate over past 5 minutes"
+ />
+ <Blueprint4.MenuItem
+ active={false}
+ disabled={false}
+ icon="circle"
+ multiline={false}
+ onClick={[Function]}
+ popoverProps={{}}
+ selected={false}
+ shouldDismissPopover={true}
+ text="Rate over past 15 minutes"
+ />
+ </Blueprint4.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
+
+ <Blueprint4.Icon
+ icon="caret-down"
+ />
+ </i>
+ </Blueprint4.Popover2>
+ </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,
},
{
@@ -194,7 +322,7 @@ exports[`SupervisorsView matches snapshot 1`] = `
"accessor": "supervisor_id",
"filterable": false,
"id": "actions",
- "show": true,
+ "sortable": false,
"width": 70,
},
]
@@ -244,7 +372,7 @@ exports[`SupervisorsView matches snapshot 1`] = `
getTrProps={[Function]}
groupedByPivotKey="_groupedByPivot"
indexKey="_index"
- loading={true}
+ loading={false}
loadingText="Loading..."
multiSort={true}
nestingLevelKey="_nestingLevel"
diff --git a/web-console/src/views/supervisors-view/supervisors-view.scss
b/web-console/src/views/supervisors-view/supervisors-view.scss
index edf04bc4e0d..25c55e69b44 100644
--- a/web-console/src/views/supervisors-view/supervisors-view.scss
+++ b/web-console/src/views/supervisors-view/supervisors-view.scss
@@ -28,5 +28,18 @@
top: $view-control-bar-height + $standard-padding;
bottom: 0;
width: 100%;
+
+ .title-button {
+ cursor: pointer;
+ }
+
+ .detail-line {
+ font-style: italic;
+ opacity: 0.6;
+ }
+
+ .warning-line {
+ color: $orange4;
+ }
}
}
diff --git a/web-console/src/views/supervisors-view/supervisors-view.tsx
b/web-console/src/views/supervisors-view/supervisors-view.tsx
index fef74fbeebe..f6604445587 100644
--- a/web-console/src/views/supervisors-view/supervisors-view.tsx
+++ b/web-console/src/views/supervisors-view/supervisors-view.tsx
@@ -16,12 +16,16 @@
* limitations under the License.
*/
-import { Code, Intent, MenuItem } from '@blueprintjs/core';
+import { Icon, Intent, Menu, MenuItem, Position, Tag } from
'@blueprintjs/core';
import { IconNames } from '@blueprintjs/icons';
+import { Popover2 } from '@blueprintjs/popover2';
+import * as JSONBig from 'json-bigint-native';
+import type { JSX } from 'react';
import React from 'react';
import type { Filter } from 'react-table';
import ReactTable from 'react-table';
+import type { TableColumnSelectorColumn } from '../../components';
import {
ACTION_COLUMN_ID,
ACTION_COLUMN_LABEL,
@@ -41,39 +45,72 @@ import {
SupervisorTableActionDialog,
} from '../../dialogs';
import { SupervisorResetOffsetsDialog } from
'../../dialogs/supervisor-reset-offsets-dialog/supervisor-reset-offsets-dialog';
-import type { QueryWithContext } from '../../druid-models';
+import type {
+ IngestionSpec,
+ QueryWithContext,
+ RowStatsKey,
+ SupervisorStatus,
+ SupervisorStatusTask,
+} 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,
+ nonEmptyArray,
oneOf,
pluralIfNeeded,
queryDruidSql,
QueryManager,
QueryState,
+ sortedToOrderByClause,
twoLines,
} from '../../utils';
import type { BasicAction } from '../../utils/basic-action';
import './supervisors-view.scss';
-const supervisorTableColumns: string[] = [
+const SUPERVISOR_TABLE_COLUMNS: TableColumnSelectorColumn[] = [
'Supervisor ID',
'Type',
'Topic/Stream',
'Status',
- 'Running tasks',
- ACTION_COLUMN_LABEL,
+ 'Configured tasks',
+ { text: 'Running tasks', label: 'status API' },
+ { text: 'Aggregate lag', label: 'status API' },
+ { text: 'Recent errors', label: 'status API' },
+ { text: 'Stats', label: 'stats API' },
];
-interface SupervisorQuery {
+const ROW_STATS_KEYS: RowStatsKey[] = ['1m', '5m', '15m'];
+const STATUS_API_TIMEOUT = 5000;
+const STATS_API_TIMEOUT = 5000;
+
+function getRowStatsKeyTitle(key: RowStatsKey) {
+ return `Rate over past ${pluralIfNeeded(parseInt(key, 10), 'minute')}`;
+}
+
+function getRowStatsKeySeconds(key: RowStatsKey): number {
+ return parseInt(key, 10) * 60;
+}
+
+interface SupervisorQuery extends TableState {
capabilities: Capabilities;
visibleColumns: LocalStorageBackedVisibility;
}
@@ -83,14 +120,10 @@ interface SupervisorQueryResultRow {
type: string;
source: string;
detailed_state: string;
+ spec?: IngestionSpec;
suspended: boolean;
- running_tasks?: number;
-}
-
-interface RunningTaskRow {
- datasource: string;
- type: string;
- num_running_tasks: number;
+ status?: SupervisorStatus;
+ stats?: any;
}
export interface SupervisorsViewProps {
@@ -106,6 +139,7 @@ export interface SupervisorsViewProps {
export interface SupervisorsViewState {
supervisorsState: QueryState<SupervisorQueryResultRow[]>;
+ statsKey: RowStatsKey;
resumeSupervisorId?: string;
suspendSupervisorId?: string;
@@ -165,25 +199,12 @@ export class SupervisorsView extends React.PureComponent<
SupervisorQueryResultRow[]
>;
- static SUPERVISOR_SQL = `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`;
-
constructor(props: SupervisorsViewProps) {
super(props);
this.state = {
supervisorsState: QueryState.INIT,
+ statsKey: '5m',
showResumeAllSupervisors: false,
showSuspendAllSupervisors: false,
@@ -199,14 +220,49 @@ 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(
+ 'WITH s AS (SELECT',
+ ' "supervisor_id",',
+ ' "type",',
+ ' "source",',
+ ` CASE WHEN "suspended" = 0 THEN "detailed_state" ELSE
'SUSPENDED' END AS "detailed_state",`,
+ visibleColumns.shown('Configured tasks') ? ' "spec",' : undefined,
+ ' "suspended" = 1 AS "suspended"',
+ 'FROM "sys"."supervisors")',
+ '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,
+ );
+
+ for (const supervisor of supervisors) {
+ const spec: any = supervisor.spec;
+ if (typeof spec === 'string') {
+ supervisor.spec = JSONBig.parse(spec);
+ }
+ }
} 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`);
}
@@ -220,48 +276,69 @@ GROUP BY 1, 2`;
'n/a',
state: deepGet(sup, 'state'),
detailed_state: deepGet(sup, 'detailedState'),
+ spec: sup.spec,
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,
- });
-
- 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) {
+ if (capabilities.hasOverlordAccess()) {
+ let showIssue = (message: string) => {
+ showIssue = () => {}; // Only show once
AppToaster.show({
icon: IconNames.ERROR,
intent: Intent.DANGER,
- message: 'Could not get running task counts',
+ message,
});
+ };
+
+ if (visibleColumns.shown('Running tasks', 'Aggregate lag', 'Recent
errors')) {
+ 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, timeout: STATUS_API_TIMEOUT },
+ )
+ ).data;
+ }
+ } catch (e) {
+ showIssue('Could not get status');
+ }
+ }
+
+ 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, timeout: STATS_API_TIMEOUT },
+ )
+ ).data;
+ }
+ } catch (e) {
+ showIssue('Could not get stats');
+ }
}
}
@@ -275,17 +352,28 @@ 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;
+ if (!this.lastTableState) return;
+ const { page, pageSize, filtered, sorted } = this.lastTableState;
+ this.supervisorQueryManager.runQuery({
+ page,
+ pageSize,
+ filtered,
+ sorted,
+ visibleColumns,
+ capabilities,
+ });
+ };
+
private readonly closeSpecDialogs = () => {
this.setState({
supervisorSpecDialogOpen: false,
@@ -389,7 +477,7 @@ GROUP BY 1, 2`;
}}
>
<p>
- Are you sure you want to resume supervisor
<Code>{resumeSupervisorId}</Code>?
+ Are you sure you want to resume supervisor <Tag
minimal>{resumeSupervisorId}</Tag>?
</p>
</AsyncActionDialog>
);
@@ -420,7 +508,7 @@ GROUP BY 1, 2`;
}}
>
<p>
- Are you sure you want to suspend supervisor
<Code>{suspendSupervisorId}</Code>?
+ Are you sure you want to suspend supervisor <Tag
minimal>{suspendSupervisorId}</Tag>?
</p>
</AsyncActionDialog>
);
@@ -465,17 +553,20 @@ GROUP BY 1, 2`;
this.supervisorQueryManager.rerunLastQuery();
}}
warningChecks={[
- `I understand that resetting ${resetSupervisorId} will clear
checkpoints and therefore lead to data loss or duplication.`,
+ <>
+ I understand that resetting <Tag minimal>{resetSupervisorId}</Tag>
will clear
+ checkpoints and may lead to data loss or duplication.
+ </>,
'I understand that this operation cannot be undone.',
]}
>
<p>
- Are you sure you want to hard reset supervisor
<Code>{resetSupervisorId}</Code>?
+ Are you sure you want to hard reset supervisor <Tag
minimal>{resetSupervisorId}</Tag>?
</p>
- <p>Hard resetting a supervisor will lead to data loss or data
duplication.</p>
+ <p>Hard resetting a supervisor may lead to data loss or data
duplication.</p>
<p>
- The reason for using this operation is to recover from a state in
which the supervisor
- ceases operating due to missing offsets.
+ Use this operation to restore functionality when the supervisor
stops operating due to
+ missing offsets.
</p>
</AsyncActionDialog>
);
@@ -506,7 +597,7 @@ GROUP BY 1, 2`;
}}
>
<p>
- Are you sure you want to terminate supervisor
<Code>{terminateSupervisorId}</Code>?
+ Are you sure you want to terminate supervisor <Tag
minimal>{terminateSupervisorId}</Tag>?
</p>
<p>This action is not reversible.</p>
</AsyncActionDialog>
@@ -541,7 +632,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 +645,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 +656,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 +670,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: 130,
accessor: 'detailed_state',
Cell: ({ value }) => (
<TableFilterableCell
@@ -607,28 +701,176 @@ GROUP BY 1, 2`;
),
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>
+ </div>
+ );
+ },
+ show: visibleColumns.shown('Configured tasks'),
+ },
{
Header: 'Running tasks',
id: 'running_tasks',
width: 150,
- accessor: 'running_tasks',
+ accessor: 'status.payload',
filterable: false,
- Cell: ({ value, original }) => (
- <TableClickableCell
- onClick={() => goToTasks(original.supervisor_id,
`index_${original.type}`)}
- hoverIcon={IconNames.ARROW_TOP_RIGHT}
- title="Go to tasks"
+ 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 = 'n/a';
+ }
+ return (
+ <TableClickableCell
+ onClick={() => goToTasks(original.supervisor_id,
`index_${original.type}`)}
+ hoverIcon={IconNames.ARROW_TOP_RIGHT}
+ title="Go to tasks"
+ >
+ {label}
+ </TableClickableCell>
+ );
+ },
+ show: visibleColumns.shown('Running tasks'),
+ },
+ {
+ Header: 'Aggregate lag',
+ accessor: 'status.payload.aggregateLag',
+ width: 200,
+ filterable: false,
+ sortable: false,
+ className: 'padded',
+ show: visibleColumns.shown('Aggregate lag'),
+ Cell: ({ value }) => formatInteger(value),
+ },
+ {
+ Header: twoLines(
+ 'Stats',
+ <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>
+ }
>
- {typeof value === 'undefined'
- ? 'n/a'
- : value > 0
- ? pluralIfNeeded(value, 'running task')
- : original.suspended
- ? ''
- : `No running tasks`}
- </TableClickableCell>
+ <i className="title-button">
+ {getRowStatsKeyTitle(statsKey)} <Icon
icon={IconNames.CARET_DOWN} />
+ </i>
+ </Popover2>,
),
- show: visibleColumns.shown('Running tasks'),
+ id: 'stats',
+ width: 300,
+ filterable: false,
+ sortable: false,
+ className: 'padded',
+ accessor: 'stats',
+ Cell: ({ value, original }) => {
+ if (!value) return;
+ const activeTaskIds: string[] | undefined = deepGet(
+ original,
+ 'status.payload.activeTasks',
+ )?.map((t: SupervisorStatusTask) => t.id);
+ const c = getTotalSupervisorStats(value, statsKey,
activeTaskIds);
+ const seconds = getRowStatsKeySeconds(statsKey);
+ const totalLabel = `Total over ${statsKey}: `;
+ const bytes = c.processedBytes ? `
(${formatByteRate(c.processedBytes)})` : '';
+ return (
+ <div>
+ <div
+ title={`${totalLabel}${formatInteger(c.processed *
seconds)} (${formatBytes(
+ c.processedBytes * seconds,
+ )})`}
+ >{`Processed: ${formatRate(c.processed)}${bytes}`}</div>
+ {Boolean(c.processedWithError) && (
+ <div
+ className="warning-line"
+
title={`${totalLabel}${formatInteger(c.processedWithError * seconds)}`}
+ >
+ Processed with error: {formatRate(c.processedWithError)}
+ </div>
+ )}
+ {Boolean(c.thrownAway) && (
+ <div
+ className="warning-line"
+ title={`${totalLabel}${formatInteger(c.thrownAway *
seconds)}`}
+ >
+ Thrown away: {formatRate(c.thrownAway)}
+ </div>
+ )}
+ {Boolean(c.unparseable) && (
+ <div
+ className="warning-line"
+ title={`${totalLabel}${formatInteger(c.unparseable *
seconds)}`}
+ >
+ Unparseable: {formatRate(c.unparseable)}
+ </div>
+ )}
+ </div>
+ );
+ },
+ show: visibleColumns.shown('Stats'),
+ },
+ {
+ Header: 'Recent errors',
+ accessor: 'status.payload.recentErrors',
+ width: 150,
+ filterable: false,
+ sortable: false,
+ show: visibleColumns.shown('Recent errors'),
+ Cell: ({ value, original }) => {
+ return (
+ <TableClickableCell
+ onClick={() => this.onSupervisorDetail(original)}
+ hoverIcon={IconNames.SEARCH_TEMPLATE}
+ title="See errors"
+ >
+ {pluralIfNeeded(value?.length, 'error')}
+ </TableClickableCell>
+ );
+ },
},
{
Header: ACTION_COLUMN_LABEL,
@@ -636,6 +878,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;
@@ -648,7 +891,6 @@ GROUP BY 1, 2`;
/>
);
},
- show: visibleColumns.shown(ACTION_COLUMN_LABEL),
},
]}
/>
@@ -657,6 +899,7 @@ GROUP BY 1, 2`;
renderBulkSupervisorActions() {
const { capabilities, goToQuery } = this.props;
+ const lastSupervisorQuery =
this.supervisorQueryManager.getLastIntermediateQuery();
return (
<>
@@ -665,7 +908,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
@@ -796,7 +1039,7 @@ GROUP BY 1, 2`;
/>
{this.renderBulkSupervisorActions()}
<TableColumnSelector
- columns={supervisorTableColumns}
+ columns={SUPERVISOR_TABLE_COLUMNS}
onChange={column =>
this.setState(prevState => ({
visibleColumns: prevState.visibleColumns.toggle(column),
diff --git
a/web-console/src/views/tasks-view/__snapshots__/tasks-view.spec.tsx.snap
b/web-console/src/views/tasks-view/__snapshots__/tasks-view.spec.tsx.snap
index 93779dfc0bf..178b50d9f57 100644
--- a/web-console/src/views/tasks-view/__snapshots__/tasks-view.spec.tsx.snap
+++ b/web-console/src/views/tasks-view/__snapshots__/tasks-view.spec.tsx.snap
@@ -81,7 +81,6 @@ exports[`TasksView matches snapshot 1`] = `
"Created time",
"Duration",
"Location",
- "Actions",
]
}
onChange={[Function]}
@@ -217,7 +216,7 @@ exports[`TasksView matches snapshot 1`] = `
"accessor": "task_id",
"filterable": false,
"id": "actions",
- "show": true,
+ "sortable": false,
"width": 70,
},
]
diff --git a/web-console/src/views/tasks-view/tasks-view.tsx
b/web-console/src/views/tasks-view/tasks-view.tsx
index 795c9908412..49a66b71cb1 100644
--- a/web-console/src/views/tasks-view/tasks-view.tsx
+++ b/web-console/src/views/tasks-view/tasks-view.tsx
@@ -16,7 +16,7 @@
* limitations under the License.
*/
-import { Button, ButtonGroup, Intent, Label, MenuItem } from
'@blueprintjs/core';
+import { Button, ButtonGroup, Intent, Label, MenuItem, Tag } from
'@blueprintjs/core';
import { IconNames } from '@blueprintjs/icons';
import React from 'react';
import type { Filter } from 'react-table';
@@ -65,7 +65,6 @@ const taskTableColumns: string[] = [
'Created time',
'Duration',
'Location',
- ACTION_COLUMN_LABEL,
];
interface TaskQueryResultRow {
@@ -310,7 +309,9 @@ ORDER BY
this.taskQueryManager.rerunLastQuery();
}}
>
- <p>{`Are you sure you want to kill task '${killTaskId}'?`}</p>
+ <p>
+ Are you sure you want to kill task <Tag minimal>{killTaskId}</Tag>?
+ </p>
</AsyncActionDialog>
);
}
@@ -494,6 +495,7 @@ ORDER BY
accessor: 'task_id',
width: ACTION_COLUMN_WIDTH,
filterable: false,
+ sortable: false,
Cell: row => {
if (row.aggregated) return '';
const id = row.value;
@@ -507,7 +509,6 @@ ORDER BY
);
},
Aggregated: () => '',
- show: visibleColumns.shown(ACTION_COLUMN_LABEL),
},
]}
/>
diff --git
a/web-console/src/views/workbench-view/max-tasks-button/max-tasks-button.tsx
b/web-console/src/views/workbench-view/max-tasks-button/max-tasks-button.tsx
index bf6bb165c85..9c53c375760 100644
--- a/web-console/src/views/workbench-view/max-tasks-button/max-tasks-button.tsx
+++ b/web-console/src/views/workbench-view/max-tasks-button/max-tasks-button.tsx
@@ -53,10 +53,15 @@ export const MaxTasksButton = function
MaxTasksButton(props: MaxTasksButtonProps
const maxNumTasks = getMaxNumTasks(queryContext);
const taskAssigment = getTaskAssigment(queryContext);
- const fullClusterCapacity = `${clusterCapacity} (full cluster capacity)`;
+ const fullClusterCapacity =
+ typeof clusterCapacity === 'number'
+ ? `${formatInteger(clusterCapacity)} (full cluster capacity)`
+ : undefined;
+
const shownMaxNumTaskOptions = clusterCapacity
? MAX_NUM_TASK_OPTIONS.filter(_ => _ <= clusterCapacity)
: MAX_NUM_TASK_OPTIONS;
+
return (
<>
<Popover2
@@ -65,7 +70,7 @@ export const MaxTasksButton = function MaxTasksButton(props:
MaxTasksButtonProps
content={
<Menu>
<MenuDivider title="Maximum number of tasks to launch" />
- {Boolean(clusterCapacity) && (
+ {Boolean(fullClusterCapacity) && (
<MenuItem
icon={tickIcon(typeof maxNumTasks === 'undefined')}
text={fullClusterCapacity}
@@ -115,7 +120,7 @@ export const MaxTasksButton = function
MaxTasksButton(props: MaxTasksButtonProps
? clusterCapacity
? fullClusterCapacity
: 2
- : maxNumTasks
+ : formatInteger(maxNumTasks)
}`}
rightIcon={IconNames.CARET_DOWN}
/>
diff --git a/web-console/src/views/workbench-view/workbench-view.tsx
b/web-console/src/views/workbench-view/workbench-view.tsx
index 76699f52a08..c251a50f19a 100644
--- a/web-console/src/views/workbench-view/workbench-view.tsx
+++ b/web-console/src/views/workbench-view/workbench-view.tsx
@@ -629,6 +629,7 @@ export class WorkbenchView extends
React.PureComponent<WorkbenchViewProps, Workb
this.props;
const { columnMetadataState } = this.state;
const currentTabEntry = this.getCurrentTabEntry();
+ const effectiveEngine = currentTabEntry.query.getEffectiveEngine();
return (
<div className="center-panel">
@@ -650,14 +651,15 @@ export class WorkbenchView extends
React.PureComponent<WorkbenchViewProps, Workb
goToTask={goToTask}
runMoreMenu={
<Menu>
- {allowExplain && (
- <MenuItem
- icon={IconNames.CLEAN}
- text="Explain SQL query"
- onClick={this.openExplainDialog}
- />
- )}
- {currentTabEntry.query.getEffectiveEngine() !== 'sql-msq-task'
&& (
+ {allowExplain &&
+ (effectiveEngine === 'sql-native' || effectiveEngine ===
'sql-msq-task') && (
+ <MenuItem
+ icon={IconNames.CLEAN}
+ text="Explain SQL query"
+ onClick={this.openExplainDialog}
+ />
+ )}
+ {effectiveEngine !== 'sql-msq-task' && (
<MenuItem
icon={IconNames.HISTORY}
text="Query history"
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]