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
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`] = ` >
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.