diff --git a/web-console/lib/sql-docs.d.ts b/web-console/lib/sql-docs.d.ts
index a5af23211d70..5948206a2208 100644
--- a/web-console/lib/sql-docs.d.ts
+++ b/web-console/lib/sql-docs.d.ts
@@ -16,5 +16,5 @@
* limitations under the License.
*/
-export const SQL_DATA_TYPES: Record;
+export const SQL_DATA_TYPES: Record;
export const SQL_FUNCTIONS: Record;
diff --git a/web-console/src/ace-modes/__snapshots__/make-doc-html.spec.ts.snap b/web-console/src/ace-modes/__snapshots__/make-doc-html.spec.ts.snap
new file mode 100644
index 000000000000..21695129d99f
--- /dev/null
+++ b/web-console/src/ace-modes/__snapshots__/make-doc-html.spec.ts.snap
@@ -0,0 +1,8 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`makeDocHtml correctly formats helper HTML 1`] = `
+"
+COUNT
+COUNT(*)
+Counts the number of things
"
+`;
diff --git a/web-console/src/ace-modes/dsql.js b/web-console/src/ace-modes/dsql.ts
similarity index 56%
rename from web-console/src/ace-modes/dsql.js
rename to web-console/src/ace-modes/dsql.ts
index f2349ee98fcf..e57d17d51e7c 100644
--- a/web-console/src/ace-modes/dsql.js
+++ b/web-console/src/ace-modes/dsql.ts
@@ -21,36 +21,42 @@
// Originally licensed under the MIT license (https://github.com/thlorenz/brace/blob/master/LICENSE)
// This file was modified to make the list of keywords more closely adhere to what is found in DruidSQL
-var druidKeywords = require('../../lib/keywords');
-var druidFunctions = require('../../lib/sql-docs');
+import type { Ace } from 'ace-builds';
+import ace from 'ace-builds/src-noconflict/ace';
+
+import * as druidKeywords from '../../lib/keywords';
+import * as druidFunctions from '../../lib/sql-docs';
+
+import type { ItemDescription } from './make-doc-html';
+import { makeDocHtml } from './make-doc-html';
ace.define(
'ace/mode/dsql_highlight_rules',
['require', 'exports', 'module', 'ace/lib/oop', 'ace/mode/text_highlight_rules'],
- function (acequire, exports, module) {
+ function (acequire: any, exports: any) {
'use strict';
- var oop = acequire('../lib/oop');
- var TextHighlightRules = acequire('./text_highlight_rules').TextHighlightRules;
+ const oop = acequire('../lib/oop');
+ const TextHighlightRules = acequire('./text_highlight_rules').TextHighlightRules;
- var SqlHighlightRules = function () {
+ const SqlHighlightRules = function (this: any) {
// Stuff like: 'with|select|from|where|and|or|group|by|order|limit|having|as|case|'
- var keywords = druidKeywords.SQL_KEYWORDS.concat(druidKeywords.SQL_EXPRESSION_PARTS)
+ const keywords = druidKeywords.SQL_KEYWORDS.concat(druidKeywords.SQL_EXPRESSION_PARTS)
.join('|')
.replace(/\s/g, '|');
// Stuff like: 'true|false'
- var builtinConstants = druidKeywords.SQL_CONSTANTS.join('|');
+ const builtinConstants = druidKeywords.SQL_CONSTANTS.join('|');
// Stuff like: 'avg|count|first|last|max|min'
- var builtinFunctions = druidKeywords.SQL_DYNAMICS.concat(
+ const builtinFunctions = druidKeywords.SQL_DYNAMICS.concat(
Object.keys(druidFunctions.SQL_FUNCTIONS),
).join('|');
// Stuff like: 'int|numeric|decimal|date|varchar|char|bigint|float|double|bit|binary|text|set|timestamp'
- var dataTypes = Object.keys(druidFunctions.SQL_DATA_TYPES).join('|');
+ const dataTypes = Object.keys(druidFunctions.SQL_DATA_TYPES).join('|');
- var keywordMapper = this.createKeywordMapper(
+ const keywordMapper = this.createKeywordMapper(
{
'support.function': builtinFunctions,
'keyword': keywords,
@@ -122,24 +128,67 @@ ace.define(
ace.define(
'ace/mode/dsql',
['require', 'exports', 'module', 'ace/lib/oop', 'ace/mode/text', 'ace/mode/dsql_highlight_rules'],
- function (acequire, exports, module) {
+ function (acequire: any, exports: any) {
'use strict';
- var oop = acequire('../lib/oop');
- var TextMode = acequire('./text').Mode;
- var SqlHighlightRules = acequire('./dsql_highlight_rules').SqlHighlightRules;
-
- var Mode = function () {
+ const oop = acequire('../lib/oop');
+ const TextMode = acequire('./text').Mode;
+ const SqlHighlightRules = acequire('./dsql_highlight_rules').SqlHighlightRules;
+
+ const completions = ([] as Ace.Completion[]).concat(
+ druidKeywords.SQL_KEYWORDS.map(v => ({ name: v, value: v, score: 0, meta: 'keyword' })),
+ druidKeywords.SQL_EXPRESSION_PARTS.map(v => ({
+ name: v,
+ value: v,
+ score: 0,
+ meta: 'keyword',
+ })),
+ druidKeywords.SQL_CONSTANTS.map(v => ({ name: v, value: v, score: 0, meta: 'constant' })),
+ druidKeywords.SQL_DYNAMICS.map(v => ({ name: v, value: v, score: 0, meta: 'dynamic' })),
+ Object.entries(druidFunctions.SQL_DATA_TYPES).map(([name, [runtime, description]]) => {
+ const item: ItemDescription = {
+ name,
+ description,
+ syntax: `Druid runtime type: ${runtime}`,
+ };
+ return {
+ name,
+ value: name,
+ score: 0,
+ meta: 'type',
+ docHTML: makeDocHtml(item),
+ docText: description,
+ };
+ }),
+ Object.entries(druidFunctions.SQL_FUNCTIONS).flatMap(([name, versions]) => {
+ return versions.map(([args, description]) => {
+ const item = { name, description, syntax: `${name}(${args})` };
+ return {
+ name,
+ value: versions.length > 1 ? `${name}(${args})` : name,
+ score: 1100, // Use a high score to appear over the 'local' suggestions that have a score of 1000
+ meta: 'function',
+ docHTML: makeDocHtml(item),
+ docText: description,
+ completer: {
+ insertMatch: (editor: any, data: any) => {
+ editor.completer.insertMatch({ value: data.name });
+ },
+ },
+ } as Ace.Completion;
+ });
+ }),
+ );
+
+ const Mode = function (this: any) {
this.HighlightRules = SqlHighlightRules;
this.$behaviour = this.$defaultBehaviour;
- };
- oop.inherits(Mode, TextMode);
+ this.$id = 'ace/mode/dsql';
- (function () {
this.lineCommentStart = '--';
-
- this.$id = 'ace/mode/dsql';
- }).call(Mode.prototype);
+ this.getCompletions = () => completions;
+ };
+ oop.inherits(Mode, TextMode);
exports.Mode = Mode;
},
diff --git a/web-console/src/ace-modes/hjson.js b/web-console/src/ace-modes/hjson.ts
similarity index 89%
rename from web-console/src/ace-modes/hjson.js
rename to web-console/src/ace-modes/hjson.ts
index 316ce9870e90..1c58a5c79450 100644
--- a/web-console/src/ace-modes/hjson.js
+++ b/web-console/src/ace-modes/hjson.ts
@@ -22,16 +22,18 @@
// This file was modified to remove the folding functionality that did not play nice when loaded along side the
// sql mode (which does not have any folding function)
+import ace from 'ace-builds/src-noconflict/ace';
+
ace.define(
'ace/mode/hjson_highlight_rules',
['require', 'exports', 'module', 'ace/lib/oop', 'ace/mode/text_highlight_rules'],
- function (acequire, exports, module) {
+ function (acequire: any, exports: any) {
'use strict';
- var oop = acequire('../lib/oop');
- var TextHighlightRules = acequire('./text_highlight_rules').TextHighlightRules;
+ const oop = acequire('../lib/oop');
+ const TextHighlightRules = acequire('./text_highlight_rules').TextHighlightRules;
- var HjsonHighlightRules = function () {
+ const HjsonHighlightRules = function (this: any) {
this.$rules = {
'start': [
{
@@ -107,7 +109,7 @@ ace.define(
'#keyname': [
{
token: 'keyword',
- regex: /(?:[^,\{\[\}\]\s]+|"(?:[^"\\]|\\.)*")\s*(?=:)/,
+ regex: /(?:[^,{[}\]\s]+|"(?:[^"\\]|\\.)*")\s*(?=:)/,
},
],
'#mstring': [
@@ -166,7 +168,7 @@ ace.define(
'#rootObject': [
{
token: 'paren',
- regex: /(?=\s*(?:[^,\{\[\}\]\s]+|"(?:[^"\\]|\\.)*")\s*:)/,
+ regex: /(?=\s*(?:[^,{[}\]\s]+|"(?:[^"\\]|\\.)*")\s*:)/,
push: [
{
token: 'paren.rparen',
@@ -205,7 +207,7 @@ ace.define(
},
{
token: 'constant.language.escape',
- regex: /\\(?:["\\\/bfnrt]|u[0-9a-fA-F]{4})/,
+ regex: /\\(?:["\\/bfnrt]|u[0-9a-fA-F]{4})/,
},
{
token: 'invalid.illegal',
@@ -220,7 +222,7 @@ ace.define(
'#ustring': [
{
token: 'string',
- regex: /\b[^:,0-9\-\{\[\}\]\s].*$/,
+ regex: /\b[^:,0-9\-{[}\]\s].*$/,
},
],
'#value': [
@@ -277,19 +279,19 @@ ace.define(
'ace/mode/text',
'ace/mode/hjson_highlight_rules',
],
- function (acequire, exports, module) {
+ function (acequire: any, exports: any) {
'use strict';
- var oop = acequire('../lib/oop');
- var TextMode = acequire('./text').Mode;
- var HjsonHighlightRules = acequire('./hjson_highlight_rules').HjsonHighlightRules;
+ const oop = acequire('../lib/oop');
+ const TextMode = acequire('./text').Mode;
+ const HjsonHighlightRules = acequire('./hjson_highlight_rules').HjsonHighlightRules;
- var Mode = function () {
+ const Mode = function (this: any) {
this.HighlightRules = HjsonHighlightRules;
};
oop.inherits(Mode, TextMode);
- (function () {
+ (function (this: any) {
this.lineCommentStart = '//';
this.blockComment = { start: '/*', end: '*/' };
this.$id = 'ace/mode/hjson';
diff --git a/web-console/src/ace-modes/make-doc-html.spec.ts b/web-console/src/ace-modes/make-doc-html.spec.ts
new file mode 100644
index 000000000000..35172d0f5b95
--- /dev/null
+++ b/web-console/src/ace-modes/make-doc-html.spec.ts
@@ -0,0 +1,31 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import { makeDocHtml } from './make-doc-html';
+
+describe('makeDocHtml', () => {
+ it('correctly formats helper HTML', () => {
+ expect(
+ makeDocHtml({
+ name: 'COUNT',
+ syntax: 'COUNT(*)',
+ description: 'Counts the number of things',
+ }),
+ ).toMatchSnapshot();
+ });
+});
diff --git a/web-console/src/ace-modes/make-doc-html.ts b/web-console/src/ace-modes/make-doc-html.ts
new file mode 100644
index 000000000000..996541b3ec63
--- /dev/null
+++ b/web-console/src/ace-modes/make-doc-html.ts
@@ -0,0 +1,32 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import escape from 'lodash.escape';
+
+export interface ItemDescription {
+ name: string;
+ syntax: string;
+ description: string;
+}
+
+export function makeDocHtml(item: ItemDescription) {
+ return `
+${item.name}
+${escape(item.syntax)}
+${item.description}
`;
+}
diff --git a/web-console/src/components/header-bar/header-bar.scss b/web-console/src/components/header-bar/header-bar.scss
index 062768a22c48..752cc9bf316f 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/json-input/__snapshots__/json-input.spec.tsx.snap b/web-console/src/components/json-input/__snapshots__/json-input.spec.tsx.snap
index e96b7f6c4ec5..b71b692ee008 100644
--- a/web-console/src/components/json-input/__snapshots__/json-input.spec.tsx.snap
+++ b/web-console/src/components/json-input/__snapshots__/json-input.spec.tsx.snap
@@ -5,7 +5,7 @@ exports[`JsonInput matches snapshot (null) 1`] = `
class="json-input"
>
@@ -104,7 +104,7 @@ exports[`JsonInput matches snapshot (value) 1`] = `
class="json-input"
>
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 e45fd590e631..e04377c9c5b1 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 = (
{}}
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 2a0c2b5a4762..d838e98e04d3 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 = (
- {columns.map(column => (
- {
- if (!isColumnShown(column)) {
- setAdded(added + 1);
- }
- onChange(column);
- }}
- />
- ))}
+ {columns.map(column => {
+ const columnName = getColumnName(column);
+ return (
+ {
+ if (!isColumnShown(column)) {
+ setAdded(added + 1);
+ }
+ onChange(columnName);
+ }}
+ />
+ );
+ })}
);
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 b030fdb304bc..52fbee102425 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`] = `
(
handleSelection(delay)}
/>
diff --git a/web-console/src/console-application.tsx b/web-console/src/console-application.tsx
index 08eaf74f4b7c..e500af23e5d2 100644
--- a/web-console/src/console-application.tsx
+++ b/web-console/src/console-application.tsx
@@ -123,7 +123,9 @@ export class ConsoleApplication extends React.PureComponent<
return await Capabilities.detectCapacity(capabilities);
},
onStateChange: ({ data, loading, error }) => {
- console.error('There was an error retrieving the capabilities', error);
+ if (error) {
+ console.error('There was an error retrieving the capabilities', error);
+ }
this.setState({
capabilities: data || Capabilities.FULL,
capabilitiesLoading: loading,
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 4cdc916ee747..cb886d0483da 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
) : (
- There is no compaction history for {datasource}.
+ There is no compaction history for {datasource} .
)
) : 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 f95a5a5d3b89..dba85268d000 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{' '}
- {datasource} and removes them from deep storage.
+ {datasource} and removes them from deep storage.
>,
'I understand that this operation cannot be undone.',
]}
>
- Are you sure you want to permanently delete unused segments in {datasource}?
+ Are you sure you want to permanently delete unused segments in{' '}
+ {datasource} ?
This action is not reversible and the data deleted will be lost.
@@ -252,7 +252,7 @@ exports[`SpecDialog matches snapshot with initSpec 1`] = `
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 7aaa8b1afa2c..68cef1ace6ea 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`] = `
- Statistics
+ Task stats
- Payload
+ Spec
= 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 09749f852531..49525dfb870e 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;
- movingAverages: Record>;
-}
-
-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>,
+ 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({
+ const [supervisorStatisticsState, supervisorStatisticsQueryManager] = useQueryManager<
+ null,
+ SupervisorStatisticsTableRow[]
+ >({
+ initQuery: null,
processQuery: async () => {
- const resp = await Api.instance.get(endpoint);
+ const resp = await Api.instance.get(statsEndpoint);
return normalizeSupervisorStatisticsResults(resp.data);
},
- initQuery: null,
});
- function renderCell(cell: CellInfo) {
- const cellValue = cell.value;
- if (!cellValue) {
- return No data found
;
- }
+ useInterval(() => {
+ supervisorStatisticsQueryManager.rerunLastQuery(true);
+ }, 1500);
- return Object.keys(cellValue)
- .sort()
- .map(key => {`${key}: ${Number(cellValue[key]).toFixed(1)}`}
);
+ 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 (
+
+
{`Processed: ${formatNumber(c.processed)}${bytes}`}
+ {Boolean(c.processedWithError) && (
+
Processed with error: {formatNumber(c.processedWithError)}
+ )}
+ {Boolean(c.thrownAway) &&
Thrown away: {formatNumber(c.thrownAway)}
}
+ {Boolean(c.unparseable) &&
Unparseable: {formatNumber(c.unparseable)}
}
+
+ );
}
function renderTable() {
let columns: Column[] = [
+ {
+ 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')}
/>
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 02d9e3c28b88..5e3d9e500288 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
('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' && (
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 4c0ceed638dc..63e1e50a8633 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`] = `
>
@@ -116,7 +116,7 @@ exports[`TaskTableActionDialog matches snapshot 1`] = `
- Payload
+ Reports
@@ -144,7 +144,7 @@ exports[`TaskTableActionDialog matches snapshot 1`] = `
- Reports
+ Spec
- Logs
+ Log
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 a0a5dbbf13fd..9edc5d996f47 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('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' && (
- deepGet(x, 'payload') || x}
- downloadFilename={`task-payload-${taskId}.json`}
- />
- )}
- {activeTab === 'reports' && (
+ {activeTab === 'report' && (
deepGet(x, 'ingestionStatsAndErrors.payload') || x}
downloadFilename={`task-reports-${taskId}.json`}
/>
)}
+ {activeTab === 'spec' && (
+ deepGet(x, 'payload') || x}
+ downloadFilename={`task-payload-${taskId}.json`}
+ />
+ )}
{activeTab === 'log' && (
;
@@ -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>;
+
+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/setup-tests.ts b/web-console/src/setup-tests.ts
index e75cb3bffb09..518045d6b6da 100644
--- a/web-console/src/setup-tests.ts
+++ b/web-console/src/setup-tests.ts
@@ -17,6 +17,7 @@
*/
import 'core-js/stable';
+import './bootstrap/ace';
import { UrlBaser } from './singletons';
diff --git a/web-console/src/utils/druid-query.ts b/web-console/src/utils/druid-query.ts
index c94bfca3d1cd..154103297044 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): Promise(sqlQueryPayload: Record): Promise {
+export async function queryDruidSql(
+ sqlQueryPayload: Record,
+ cancelToken?: CancelToken,
+): Promise {
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 3a770c676307..b4537a63e08b 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 c335180056bb..f20031f2b8df 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 e864aef131f0..7eedd1acaab9 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 2cd926e01f97..b627b2e500ce 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 75541b82999c..713df9b18b1c 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 = {
'% Compacted',
'Left to be compacted',
'Retention',
- ACTION_COLUMN_LABEL,
],
'no-sql': [
'Datasource name',
@@ -114,7 +113,6 @@ const tableColumns: Record = {
'% Compacted',
'Left to be compacted',
'Retention',
- ACTION_COLUMN_LABEL,
],
'no-proxy': [
'Datasource name',
@@ -128,7 +126,6 @@ const tableColumns: Record = {
'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 b88cf8a70c2b..ad9e96667db5 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(
- Only dynamic partitioning supports appendToExisting: true. You
- have currently selected {partitionsSpecType} partitioning.
+ Only dynamic partitioning supports appendToExisting: true.
+ You have currently selected {partitionsSpecType} partitioning.
Your partitioning and sorting configuration is uncommon.
For best performance the first dimension in your schema (
- {firstDimensionName}), which is what the data will be primarily sorted
- on, commonly matches the partitioning dimension ({partitionDimension}).
+ {firstDimensionName} ), which is what the data will be primarily
+ sorted on, commonly matches the partitioning dimension (
+ {partitionDimension} ).
You have enabled type-aware schema discovery (
useSchemaDiscovery: true) to ingest data into the existing
- datasource {datasource}.
+ datasource {datasource} .
If you used string-based schema discovery when first ingesting data to{' '}
- {datasource}, using type-aware schema discovery now can cause
+ {datasource} , using type-aware schema discovery now can cause
problems with the values multi-value string dimensions.
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 f612725e298a..07fbc786424e 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 8f19e55d4ca6..af8207f6ab11 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 {
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 d37f1ebe3e6b..9c7e40197a62 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 ae40a8d641e4..7d7bcaeec468 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 = {
'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 {
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"`,
}}
>
- Are you sure you want to drop segment {terminateSegmentId}?
+ Are you sure you want to drop segment {terminateSegmentId} ?
This action is not reversible.
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 baedf5165d63..93e47b06e277 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 fd9c4caef243..3ff6eead2764 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 = {
'Usage',
'Start time',
'Detail',
- ACTION_COLUMN_LABEL,
],
'no-sql': [
'Service',
@@ -83,7 +82,6 @@ const tableColumns: Record = {
'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 6053d75102e8..f43b9d3eb90c 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":
+ Stats
+
+
+
+
+
+
+ }
+ 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}
+ >
+
+ Rate over past 5 minutes
+
+
+
+
+ ,
+ "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 edf04bc4e0d8..25c55e69b44c 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 fef74fbeebeb..f66044455876 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;
+ 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({
- 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(
+ {
+ 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;
- if (capabilities.hasSql()) {
- const runningTasks = await queryDruidSql({
- 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`;
}}
>
- Are you sure you want to resume supervisor {resumeSupervisorId}?
+ Are you sure you want to resume supervisor {resumeSupervisorId} ?
);
@@ -420,7 +508,7 @@ GROUP BY 1, 2`;
}}
>
- Are you sure you want to suspend supervisor {suspendSupervisorId}?
+ Are you sure you want to suspend supervisor {suspendSupervisorId} ?
);
@@ -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 {resetSupervisorId} will clear
+ checkpoints and may lead to data loss or duplication.
+ >,
'I understand that this operation cannot be undone.',
]}
>
- Are you sure you want to hard reset supervisor {resetSupervisorId}?
+ Are you sure you want to hard reset supervisor {resetSupervisorId} ?
- Hard resetting a supervisor will lead to data loss or data duplication.
+ Hard resetting a supervisor may lead to data loss or data duplication.
- 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.
);
@@ -506,7 +597,7 @@ GROUP BY 1, 2`;
}}
>
- Are you sure you want to terminate supervisor {terminateSupervisorId}?
+ Are you sure you want to terminate supervisor {terminateSupervisorId} ?
This action is not reversible.
@@ -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', (datasource) ),
id: 'supervisor_id',
accessor: 'supervisor_id',
- width: 300,
+ width: 280,
show: visibleColumns.shown('Supervisor ID'),
Cell: ({ value, original }) => (
(
{
+ 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 (
+
+
{formatInteger(taskCount * replicas)}
+
+ {replicas === 1
+ ? '(no replication)'
+ : `(${pluralIfNeeded(taskCount, 'task')} × ${pluralIfNeeded(
+ replicas,
+ 'replica',
+ )})`}
+
+
+ );
+ },
+ show: visibleColumns.shown('Configured tasks'),
+ },
{
Header: 'Running tasks',
id: 'running_tasks',
width: 150,
- accessor: 'running_tasks',
+ accessor: 'status.payload',
filterable: false,
- Cell: ({ value, original }) => (
- 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 = (
+ <>
+ {label}
+ {pluralIfNeeded(publishingTasks.length, 'publishing task')}
+ >
+ );
+ }
+ } else {
+ label = 'n/a';
+ }
+ return (
+ goToTasks(original.supervisor_id, `index_${original.type}`)}
+ hoverIcon={IconNames.ARROW_TOP_RIGHT}
+ title="Go to tasks"
+ >
+ {label}
+
+ );
+ },
+ 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',
+
+ {ROW_STATS_KEYS.map(k => (
+ {
+ this.setState({ statsKey: k });
+ }}
+ />
+ ))}
+
+ }
>
- {typeof value === 'undefined'
- ? 'n/a'
- : value > 0
- ? pluralIfNeeded(value, 'running task')
- : original.suspended
- ? ''
- : `No running tasks`}
-
+
+ {getRowStatsKeyTitle(statsKey)}
+
+ ,
),
- 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 (
+
+
{`Processed: ${formatRate(c.processed)}${bytes}`}
+ {Boolean(c.processedWithError) && (
+
+ Processed with error: {formatRate(c.processedWithError)}
+
+ )}
+ {Boolean(c.thrownAway) && (
+
+ Thrown away: {formatRate(c.thrownAway)}
+
+ )}
+ {Boolean(c.unparseable) && (
+
+ Unparseable: {formatRate(c.unparseable)}
+
+ )}
+
+ );
+ },
+ 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 (
+ this.onSupervisorDetail(original)}
+ hoverIcon={IconNames.SEARCH_TEMPLATE}
+ title="See errors"
+ >
+ {pluralIfNeeded(value?.length, 'error')}
+
+ );
+ },
},
{
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`;
goToQuery({ queryString: SupervisorsView.SUPERVISOR_SQL })}
+ onClick={() => goToQuery({ queryString: lastSupervisorQuery })}
/>
)}
{this.renderBulkSupervisorActions()}
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 93779dfc0bf5..178b50d9f579 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 795c9908412a..49a66b71cb1c 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();
}}
>
- {`Are you sure you want to kill task '${killTaskId}'?`}
+
+ Are you sure you want to kill task {killTaskId} ?
+
);
}
@@ -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/destination-pages-pane/destination-pages-pane.tsx b/web-console/src/views/workbench-view/destination-pages-pane/destination-pages-pane.tsx
index 8535db85a531..b58a75aea4b6 100644
--- a/web-console/src/views/workbench-view/destination-pages-pane/destination-pages-pane.tsx
+++ b/web-console/src/views/workbench-view/destination-pages-pane/destination-pages-pane.tsx
@@ -82,9 +82,11 @@ export const DestinationPagesPane = React.memo(function DestinationPagesPane(
const numTotalRows = destination?.numTotalRows;
- function getPageUrl(pageIndex: number) {
+ function getResultUrl(pageIndex: number) {
return UrlBaser.base(
- `/druid/v2/sql/statements/${id}/results?page=${pageIndex}&resultFormat=${desiredResultFormat}`,
+ `/druid/v2/sql/statements/${id}/results?${
+ pageIndex < 0 ? '' : `page=${pageIndex}&`
+ }resultFormat=${desiredResultFormat}`,
);
}
@@ -94,13 +96,10 @@ export const DestinationPagesPane = React.memo(function DestinationPagesPane(
return `${id}_page_${pageNumberString}_of_${numPagesString}.${desiredExtension}`;
}
- async function downloadAllPages() {
+ async function downloadAllData() {
if (!pages) return;
- const numPages = pages.length;
- for (let i = 0; i < pages.length; i++) {
- downloadUrl(getPageUrl(i), getPageFilename(i, numPages));
- await wait(100);
- }
+ downloadUrl(getResultUrl(-1), `${id}_all_data.${desiredExtension}`);
+ await wait(100);
}
const numPages = pages.length;
@@ -139,8 +138,8 @@ export const DestinationPagesPane = React.memo(function DestinationPagesPane(
void downloadAllPages()}
+ text="Download all data (concatenated)"
+ onClick={() => void downloadAllData()}
/>
)}
@@ -185,7 +184,7 @@ export const DestinationPagesPane = React.memo(function DestinationPagesPane(
icon={IconNames.DOWNLOAD}
text="Download"
minimal
- href={getPageUrl(value)}
+ href={getResultUrl(value)}
download={getPageFilename(value, numPages)}
/>
),
diff --git a/web-console/src/views/workbench-view/explain-dialog/__snapshots__/explain-dialog.spec.tsx.snap b/web-console/src/views/workbench-view/explain-dialog/__snapshots__/explain-dialog.spec.tsx.snap
index bbbca4bf4d58..c0332ad0b015 100644
--- a/web-console/src/views/workbench-view/explain-dialog/__snapshots__/explain-dialog.spec.tsx.snap
+++ b/web-console/src/views/workbench-view/explain-dialog/__snapshots__/explain-dialog.spec.tsx.snap
@@ -122,7 +122,7 @@ exports[`ExplainDialog matches snapshot on some data (many queries) 1`] = `
enableLiveAutocompletion={false}
enableSnippets={false}
focus={false}
- fontSize={13}
+ fontSize={12}
height="100%"
highlightActiveLine={true}
maxLines={null}
@@ -220,7 +220,7 @@ exports[`ExplainDialog matches snapshot on some data (many queries) 1`] = `
enableLiveAutocompletion={false}
enableSnippets={false}
focus={false}
- fontSize={13}
+ fontSize={12}
height="100%"
highlightActiveLine={true}
maxLines={null}
@@ -348,7 +348,7 @@ exports[`ExplainDialog matches snapshot on some data (one query) 1`] = `
enableLiveAutocompletion={false}
enableSnippets={false}
focus={false}
- fontSize={13}
+ fontSize={12}
height="100%"
highlightActiveLine={true}
maxLines={null}
diff --git a/web-console/src/views/workbench-view/explain-dialog/explain-dialog.tsx b/web-console/src/views/workbench-view/explain-dialog/explain-dialog.tsx
index 3c535ed5449a..4bab7e7bfb05 100644
--- a/web-console/src/views/workbench-view/explain-dialog/explain-dialog.tsx
+++ b/web-console/src/views/workbench-view/explain-dialog/explain-dialog.tsx
@@ -131,7 +131,7 @@ export const ExplainDialog = React.memo(function ExplainDialog(props: ExplainDia
theme="solarized_dark"
className="query-string"
name="ace-editor"
- fontSize={13}
+ fontSize={12}
width="100%"
height="100%"
showGutter
diff --git a/web-console/src/views/workbench-view/flexible-query-input/__snapshots__/flexible-query-input.spec.tsx.snap b/web-console/src/views/workbench-view/flexible-query-input/__snapshots__/flexible-query-input.spec.tsx.snap
index 0efa8f7f7a71..902b465fd23b 100644
--- a/web-console/src/views/workbench-view/flexible-query-input/__snapshots__/flexible-query-input.spec.tsx.snap
+++ b/web-console/src/views/workbench-view/flexible-query-input/__snapshots__/flexible-query-input.spec.tsx.snap
@@ -1,12 +1,5 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
-exports[`FlexibleQueryInput correctly formats helper HTML 1`] = `
-"
-COUNT
-COUNT(*)
-Counts the number of things
"
-`;
-
exports[`FlexibleQueryInput matches snapshot 1`] = `