You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@druid.apache.org by vo...@apache.org on 2022/08/24 23:17:20 UTC
[druid] branch master updated: Web console: Multi-stage query support (#12919)
This is an automated email from the ASF dual-hosted git repository.
vogievetsky pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/druid.git
The following commit(s) were added to refs/heads/master by this push:
new 04ee7abeff Web console: Multi-stage query support (#12919)
04ee7abeff is described below
commit 04ee7abeff7db86aca350a5daae8bf8cfa0da81f
Author: Vadim Ogievetsky <va...@ogievetsky.com>
AuthorDate: Wed Aug 24 16:17:12 2022 -0700
Web console: Multi-stage query support (#12919)
* MSQ web console
* fix typo in comments
* remove useless conditional
* wrap SQL_DATA_TYPES
* fixes sus regex
* rewrite regex
* remove problematic regex
* fix UTs
* convert PARTITIONED / CLUSTERED BY to ORDER BY for preview
* fix log
* updated to use shuffle
* Web console: Use Ace.Completion directly (#1405)
* Use Ace.Completion directly
* Another Ace.Completion
* better comment
* fix column ordering in e2e test
* add nested data example also
Co-authored-by: John Gozde <jo...@imply.io>
---
licenses.yaml | 14 +-
licenses/bin/zustand.MIT | 21 +
.../e2e-tests/component/datasources/overview.ts | 2 +-
.../e2e-tests/component/ingestion/overview.ts | 4 +-
.../e2e-tests/component/load-data/data-loader.ts | 2 +-
web-console/e2e-tests/component/query/overview.ts | 13 +-
.../component/{ingestion => workbench}/overview.ts | 38 +-
web-console/e2e-tests/multi-stage-query.spec.ts | 72 ++
web-console/lib/keywords.js | 6 +
web-console/lib/sql-docs.d.ts | 2 +-
web-console/package-lock.json | 421 ++++-----
web-console/package.json | 10 +-
web-console/script/create-sql-docs.js | 8 +-
web-console/script/druid | 2 +-
web-console/script/mv | 75 ++
web-console/src/ace-modes/dsql.js | 4 +-
web-console/src/bootstrap/react-table-defaults.tsx | 2 +-
.../src/components/braced-text/braced-text.scss | 1 +
.../__snapshots__/click-to-copy.spec.tsx.snap | 11 +
.../click-to-copy/click-to-copy.spec.tsx} | 8 +-
.../click-to-copy/click-to-copy.tsx} | 29 +-
.../datasource-columns-table.spec.tsx.snap | 668 --------------
.../fancy-tab-pane.scss} | 56 +-
.../components/fancy-tab-pane/fancy-tab-pane.tsx | 83 ++
.../form-group-with-info/form-group-with-info.tsx | 2 +-
.../__snapshots__/header-bar.spec.tsx.snap | 163 +++-
.../src/components/header-bar/header-bar.scss | 11 +-
.../src/components/header-bar/header-bar.spec.tsx | 2 +-
.../src/components/header-bar/header-bar.tsx | 152 +++-
web-console/src/components/index.ts | 12 +
.../components/interval-input/interval-input.tsx | 2 +-
.../__snapshots__/learn-more.spec.tsx.snap | 0
.../learn-more/learn-more.spec.tsx | 0
.../learn-more/learn-more.tsx | 2 +-
.../src/components/menu-checkbox/menu-checkbox.tsx | 5 +-
.../__snapshots__/menu-tristate.spec.tsx.snap | 103 +++
.../menu-tristate/menu-tristate.spec.tsx} | 29 +-
.../src/components/menu-tristate/menu-tristate.tsx | 71 ++
.../__snapshots__/more-button.spec.tsx.snap | 2 +
.../src/components/more-button/more-button.tsx | 2 +-
.../__snapshots__/query-error-pane.spec.tsx.snap} | 4 +-
.../query-error-pane.scss} | 11 +-
.../query-error-pane/query-error-pane.spec.tsx} | 6 +-
.../query-error-pane/query-error-pane.tsx} | 33 +-
.../record-table-pane/record-table-pane.scss | 113 +++
.../record-table-pane/record-table-pane.tsx | 282 ++++++
web-console/src/components/show-json/show-json.tsx | 2 +-
.../table-cell-unparseable.scss | 3 +
.../table-clickable-cell/table-clickable-cell.tsx | 7 +-
web-console/src/console-application.tsx | 127 ++-
.../async-action-dialog/async-action-dialog.tsx | 2 +-
.../__snapshots__/compaction-dialog.spec.tsx.snap | 56 ++
...coordinator-dynamic-config-dialog.spec.tsx.snap | 2 +-
.../datasource-table-action-dialog.spec.tsx.snap | 75 +-
.../datasource-columns-table.spec.tsx.snap | 648 ++++++++++++++
.../datasource-columns-table.scss | 20 +-
.../datasource-columns-table.spec.tsx | 6 +-
.../datasource-columns-table.tsx | 15 +-
.../datasource-preview-pane.scss} | 12 +-
.../datasource-preview-pane.tsx | 65 ++
.../datasource-table-action-dialog.spec.tsx | 2 +-
.../datasource-table-action-dialog.tsx | 35 +-
.../edit-context-dialog/edit-context-dialog.tsx | 2 +-
.../src/dialogs/history-dialog/history-dialog.tsx | 2 +-
web-console/src/dialogs/index.ts | 3 +
.../lookup-table-action-dialog.tsx | 3 +-
.../lookup-values-table.spec.tsx.snap | 0
.../lookup-values-table/lookup-values-table.scss | 0
.../lookup-values-table.spec.tsx | 0
.../lookup-values-table/lookup-values-table.tsx | 8 +-
.../numeric-input-dialog/numeric-input-dialog.tsx | 79 ++
.../overload-dynamic-config-dialog.spec.tsx.snap | 2 +-
.../__snapshots__/retention-dialog.spec.tsx.snap | 2 +-
.../segment-table-action-dialog.spec.tsx.snap | 27 +
.../segment-table-action-dialog.tsx | 12 +-
.../parse-segement-id.spec.ts | 65 ++
.../segments-preview-pane.scss} | 13 +-
.../segments-preview-pane.tsx | 119 +++
.../dialogs/status-dialog/status-dialog.spec.tsx | 16 +-
.../src/dialogs/status-dialog/status-dialog.tsx | 72 +-
.../string-input-dialog/string-input-dialog.tsx | 61 ++
.../supervisor-statistics-table.spec.tsx.snap | 0
.../supervisor-statistics-table.scss | 0
.../supervisor-statistics-table.spec.tsx | 4 +-
.../supervisor-statistics-table.tsx | 10 +-
.../supervisor-table-action-dialog.tsx | 3 +-
.../table-action-dialog/table-action-dialog.tsx | 2 +-
.../task-table-action-dialog.tsx | 6 +-
.../{ => compaction-config}/compaction-config.tsx | 21 +-
.../compaction-status.spec.ts | 3 +-
.../{ => compaction-status}/compaction-status.ts | 2 +-
.../coordinator-dynamic-config.tsx | 2 +-
.../{ => dimension-spec}/dimension-spec.spec.ts | 3 +-
.../{ => dimension-spec}/dimension-spec.ts | 11 +-
.../druid-engine.ts} | 22 +-
.../execution/execution-ingest-complete.mock.ts | 285 ++++++
.../execution/execution-ingest-error.mock.ts | 290 ++++++
.../src/druid-models/execution/execution.spec.ts | 530 +++++++++++
.../src/druid-models/execution/execution.ts | 467 ++++++++++
.../external-config/external-config.ts | 192 ++++
.../src/druid-models/{ => filter}/filter.tsx | 9 +-
.../{ => flatten-spec}/flatten-spec.spec.ts | 8 +-
.../{ => flatten-spec}/flatten-spec.tsx | 6 +-
web-console/src/druid-models/index.ts | 38 +-
.../ingest-query-pattern.spec.ts | 53 ++
.../ingest-query-pattern/ingest-query-pattern.ts | 314 +++++++
.../{ => ingestion-spec}/ingestion-spec.spec.ts | 42 +-
.../{ => ingestion-spec}/ingestion-spec.tsx | 105 ++-
.../{ => input-format}/input-format.tsx | 13 +-
web-console/src/druid-models/input-source.tsx | 91 --
.../src/druid-models/input-source/input-source.tsx | 590 +++++++++++++
.../{ => lookup-spec}/lookup-spec.spec.ts | 0
.../druid-models/{ => lookup-spec}/lookup-spec.tsx | 4 +-
.../{ => metric-spec}/metric-spec.spec.ts | 3 +-
.../druid-models/{ => metric-spec}/metric-spec.tsx | 11 +-
.../mocks.ts} | 5 +-
.../overlord-dynamic-config.tsx | 2 +-
.../druid-models/query-context/query-context.tsx | 217 +++++
web-console/src/druid-models/stages/stages.spec.ts | 881 ++++++++++++++++++
web-console/src/druid-models/stages/stages.ts | 524 +++++++++++
.../src/druid-models/{ => time}/time.spec.ts | 0
web-console/src/druid-models/{ => time}/time.ts | 2 +-
.../{ => timestamp-spec}/timestamp-spec.tsx | 11 +-
.../{ => transform-spec}/transform-spec.spec.ts | 0
.../{ => transform-spec}/transform-spec.tsx | 7 +-
.../workbench-query/workbench-query-part.ts | 256 ++++++
.../workbench-query/workbench-query.spec.ts | 592 +++++++++++++
.../workbench-query/workbench-query.ts | 665 ++++++++++++++
web-console/src/entry.ts | 2 +-
web-console/src/helpers/execution/general.ts | 70 ++
.../src/helpers/execution/sql-task-execution.ts | 264 ++++++
.../index.ts} | 6 +-
web-console/src/helpers/spec-conversion.spec.ts | 452 ++++++++++
web-console/src/helpers/spec-conversion.ts | 455 ++++++++++
web-console/src/hooks/use-query-manager.ts | 23 +-
web-console/{lib/sql-docs.d.ts => src/index.ts} | 11 +-
web-console/src/links.ts | 2 +-
.../src/react-table/react-table-base-styles.scss | 7 -
web-console/src/react-table/react-table-extra.scss | 2 +-
.../react-table-pagination.tsx | 11 +-
.../ace-editor-state-cache.ts} | 44 +-
web-console/src/singletons/api.spec.ts | 2 +-
web-console/src/singletons/api.ts | 6 +-
.../execution-state-cache.ts} | 28 +-
web-console/src/singletons/workbench-history.ts | 73 ++
.../src/singletons/workbench-running-promises.ts | 50 ++
web-console/src/utils/capabilities.ts | 27 +
.../src/utils/download-query-detail-archive.ts | 61 ++
.../download.spec.ts} | 18 +-
web-console/src/utils/download.ts | 92 ++
web-console/src/utils/druid-query.spec.ts | 37 +-
web-console/src/utils/druid-query.ts | 54 +-
web-console/src/utils/general.tsx | 56 +-
web-console/src/utils/index.tsx | 6 +-
web-console/src/utils/local-storage-keys.tsx | 10 +
web-console/src/utils/query-context.tsx | 86 --
web-console/src/utils/query-manager.tsx | 30 +-
web-console/src/utils/query-state.ts | 3 +
web-console/src/utils/sample-query.spec.tsx | 60 ++
web-console/src/utils/sample-query.tsx | 81 ++
.../test-fixtures.ts => utils/sampler.mock.ts} | 2 +-
web-console/src/utils/sql.ts | 60 ++
.../src/utils/{data-type-utils.ts => types.ts} | 36 +-
.../__snapshots__/datasources-view.spec.tsx.snap | 7 +-
.../views/datasources-view/datasources-view.tsx | 29 +-
.../__snapshots__/home-view.spec.tsx.snap | 12 +
.../home-view/services-card/services-card.tsx | 2 +-
.../src/views/home-view/tasks-card/tasks-card.tsx | 2 +-
web-console/src/views/index.ts | 2 +
.../__snapshots__/ingestion-view.spec.tsx.snap | 32 +-
.../views/ingestion-view/ingestion-view.spec.tsx | 4 +-
.../src/views/ingestion-view/ingestion-view.tsx | 133 +--
.../__snapshots__/load-data-view.spec.tsx.snap | 464 +++++++---
.../src/views/load-data-view/info-messages.tsx | 4 +-
.../src/views/load-data-view/load-data-view.scss | 71 +-
.../views/load-data-view/load-data-view.spec.tsx | 9 +-
.../src/views/load-data-view/load-data-view.tsx | 163 ++--
.../parse-data-table/parse-data-table.tsx | 3 +-
.../parse-time-table/parse-time-table.tsx | 3 +-
.../__snapshots__/query-view.spec.tsx.snap | 10 +-
.../query-view/column-tree/column-tree.spec.tsx | 4 +-
.../views/query-view/column-tree/column-tree.tsx | 13 +-
.../__snapshots__/explain-dialog.spec.tsx.snap | 117 ++-
.../query-view/explain-dialog/explain-dialog.scss | 2 +
.../explain-dialog/explain-dialog.spec.tsx | 5 +-
.../query-view/explain-dialog/explain-dialog.tsx | 94 +-
.../live-query-mode-button.spec.tsx.snap} | 26 +-
.../live-query-mode-button.scss} | 4 +-
.../live-query-mode-button.spec.tsx} | 8 +-
.../live-query-mode-button.tsx} | 26 +-
.../query-history-dialog.spec.tsx.snap | 0
.../query-history-dialog/query-history-dialog.scss | 2 +-
.../query-history-dialog.spec.tsx | 0
.../query-history-dialog/query-history-dialog.tsx | 4 +-
.../views/query-view/query-input/query-input.tsx | 14 +-
.../views/query-view/query-output/query-output.tsx | 16 +-
.../src/views/query-view/query-view.spec.tsx | 9 -
web-console/src/views/query-view/query-view.tsx | 75 +-
.../src/views/query-view/run-button/run-button.tsx | 18 +-
.../src/views/segments-view/segments-view.tsx | 50 +-
.../src/views/services-view/services-view.spec.tsx | 4 +-
.../src/views/services-view/services-view.tsx | 6 +-
.../column-actions/column-actions.scss} | 22 +-
.../column-actions/column-actions.tsx | 283 ++++++
.../column-editor/column-editor.scss} | 44 +-
.../column-editor/column-editor.tsx | 219 +++++
.../destination-dialog/destination-dialog.scss} | 6 +-
.../destination-dialog/destination-dialog.tsx | 89 ++
.../destination-form/destination-form.scss} | 7 +-
.../destination-form/destination-form.tsx | 128 +++
.../expression-editor-dialog.scss} | 27 +-
.../expression-editor-dialog.tsx | 107 +++
.../ingestion-progress-dialog.scss} | 25 +-
.../ingestion-progress-dialog.tsx | 132 +++
.../schema-step/column-list/column-list.scss} | 51 +-
.../schema-step/column-list/column-list.tsx | 156 ++++
.../expression-entry/expression-entry.scss} | 70 +-
.../expression-entry/expression-entry.tsx | 70 ++
.../schema-step/preview-error/preview-error.scss} | 25 +-
.../schema-step/preview-error/preview-error.tsx} | 24 +-
.../schema-step/preview-table/preview-table.scss | 146 +++
.../schema-step/preview-table/preview-table.tsx | 222 +++++
.../rollup-analysis-pane.scss} | 18 +-
.../rollup-analysis-pane/rollup-analysis-pane.tsx | 342 +++++++
.../schema-step/schema-step.scss | 147 +++
.../schema-step/schema-step.tsx | 983 +++++++++++++++++++++
.../sql-data-loader-view/sql-data-loader-view.scss | 85 ++
.../sql-data-loader-view/sql-data-loader-view.tsx | 201 +++++
.../title-frame/title-frame.scss} | 43 +-
.../title-frame/title-frame.tsx} | 27 +-
.../cancel-query-dialog/cancel-query-dialog.tsx} | 35 +-
.../connect-external-data-dialog.scss} | 25 +-
.../connect-external-data-dialog.tsx | 87 ++
.../src/views/workbench-view/demo-queries.ts | 312 +++++++
.../execution-details-dialog.scss} | 24 +-
.../execution-details-dialog.tsx | 58 ++
.../execution-details-pane-loader.tsx | 71 ++
.../execution-details-pane.scss} | 11 +-
.../execution-details-pane.tsx | 147 +++
.../execution-error-pane.spec.tsx.snap | 40 +
.../execution-error-pane.scss} | 13 +-
.../execution-error-pane.spec.tsx} | 13 +-
.../execution-error-pane/execution-error-pane.tsx | 94 ++
.../execution-progress-bar-pane.spec.tsx.snap | 23 +
.../execution-progress-bar-pane.scss} | 26 +-
.../execution-progress-bar-pane.spec.tsx} | 15 +-
.../execution-progress-bar-pane.tsx | 95 ++
.../execution-progress-pane.scss} | 53 +-
.../execution-progress-pane.tsx | 62 ++
.../execution-stages-pane-loader.tsx | 62 ++
.../execution-stages-pane.spec.tsx.snap | 427 +++++++++
.../execution-stages-pane.scss | 104 +++
.../execution-stages-pane.spec.tsx} | 15 +-
.../execution-stages-pane.tsx | 649 ++++++++++++++
.../execution-submit-dialog.scss} | 22 +-
.../execution-submit-dialog.tsx | 139 +++
.../execution-summary-panel.scss} | 5 +-
.../execution-summary-panel.tsx | 100 +++
.../execution-timer-panel.spec.tsx.snap | 59 ++
.../execution-timer-panel.scss} | 6 +-
.../execution-timer-panel.spec.tsx} | 28 +-
.../execution-timer-panel.tsx | 71 ++
.../execution-warnings-pane.scss} | 6 +-
.../execution-warnings-pane.tsx} | 26 +-
.../execution-warnings-pane/warning-callout.scss} | 6 +-
.../execution-warnings-pane/warning-callout.tsx | 53 ++
.../flexible-query-input.spec.tsx.snap | 111 +++
.../flexible-query-input.scss} | 10 +-
.../flexible-query-input.spec.tsx} | 24 +-
.../flexible-query-input/flexible-query-input.tsx} | 172 ++--
.../workbench-view/helper-query/helper-query.scss | 100 +++
.../workbench-view/helper-query/helper-query.tsx | 408 +++++++++
.../ingest-success-pane.spec.tsx.snap | 30 +
.../ingest-success-pane/ingest-success-pane.scss} | 16 +-
.../ingest-success-pane.spec.tsx} | 17 +-
.../ingest-success-pane/ingest-success-pane.tsx | 82 ++
.../input-format-step/input-format-step.scss} | 56 +-
.../input-format-step/input-format-step.tsx | 259 ++++++
.../input-source-step/example-inputs.ts | 159 ++++
.../input-source-step/input-source-info.tsx | 78 ++
.../input-source-step/input-source-step.scss | 131 +++
.../input-source-step/input-source-step.tsx | 343 +++++++
.../__snapshots__/max-tasks-button.spec.tsx.snap | 205 +++++
.../max-tasks-button/max-tasks-button.spec.tsx} | 9 +-
.../max-tasks-button/max-tasks-button.tsx | 115 +++
.../workbench-view/metadata-change-detector.tsx} | 42 +-
.../workbench-view/metadata-state-store.ts} | 28 +-
.../views/workbench-view/query-tab/query-tab.scss | 147 +++
.../views/workbench-view/query-tab/query-tab.tsx | 457 ++++++++++
.../recent-query-task-panel.scss | 121 +++
.../recent-query-task-panel.tsx | 275 ++++++
.../result-table-pane/result-table-pane.scss | 117 +++
.../result-table-pane/result-table-pane.tsx | 778 ++++++++++++++++
.../workbench-view/run-panel/run-panel.scss} | 9 +-
.../views/workbench-view/run-panel/run-panel.tsx | 422 +++++++++
.../tab-rename-dialog/tab-rename-dialog.tsx | 70 ++
.../time-floor-menu-item/time-floor-menu-item.tsx | 102 +++
.../workbench-view/work-state-store.ts} | 28 +-
.../workbench-history-dialog.scss} | 64 +-
.../workbench-history-dialog.tsx | 134 +++
.../src/views/workbench-view/workbench-view.scss | 122 +++
.../src/views/workbench-view/workbench-view.tsx | 805 +++++++++++++++++
302 files changed, 24094 insertions(+), 2731 deletions(-)
diff --git a/licenses.yaml b/licenses.yaml
index 226062861c..6c226d2242 100644
--- a/licenses.yaml
+++ b/licenses.yaml
@@ -5317,7 +5317,7 @@ license_category: binary
module: web-console
license_name: MIT License
copyright: Matt Zabriskie
-version: 0.21.4
+version: 0.26.1
license_file_path: licenses/bin/axios.MIT
---
@@ -5666,7 +5666,7 @@ license_category: binary
module: web-console
license_name: Apache License version 2.0
copyright: Imply Data
-version: 0.14.10
+version: 0.14.24
---
@@ -6536,4 +6536,14 @@ license_name: ISC License
copyright: Eemeli Aro
version: 1.10.2
license_file_path: licenses/bin/yaml.ISC
+
+---
+
+name: "zustand"
+license_category: binary
+module: web-console
+license_name: MIT License
+copyright: Paul Henschel
+version: 3.7.2
+license_file_path: licenses/bin/zustand.MIT
# Web console modules end
diff --git a/licenses/bin/zustand.MIT b/licenses/bin/zustand.MIT
new file mode 100644
index 0000000000..a2c2649dee
--- /dev/null
+++ b/licenses/bin/zustand.MIT
@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) 2019 Paul Henschel
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
diff --git a/web-console/e2e-tests/component/datasources/overview.ts b/web-console/e2e-tests/component/datasources/overview.ts
index fb52581112..1fa83c3672 100644
--- a/web-console/e2e-tests/component/datasources/overview.ts
+++ b/web-console/e2e-tests/component/datasources/overview.ts
@@ -141,7 +141,7 @@ export class DatasourcesOverview {
}
private async clickMoreButton(options: any): Promise<void> {
- await this.page.click('//button[span[@icon="more"]]', options);
+ await this.page.click('.more-button button', options);
await this.waitForPopupMenu();
}
}
diff --git a/web-console/e2e-tests/component/ingestion/overview.ts b/web-console/e2e-tests/component/ingestion/overview.ts
index 7c3b8cb3d7..3d1a7f2997 100644
--- a/web-console/e2e-tests/component/ingestion/overview.ts
+++ b/web-console/e2e-tests/component/ingestion/overview.ts
@@ -30,10 +30,10 @@ enum TaskColumn {
GROUP_ID,
TYPE,
DATASOURCE,
- LOCATION,
- CREATED_TIME,
STATUS,
+ CREATED_TIME,
DURATION,
+ LOCATION,
}
/**
diff --git a/web-console/e2e-tests/component/load-data/data-loader.ts b/web-console/e2e-tests/component/load-data/data-loader.ts
index 7bc1837f61..37586a22bb 100644
--- a/web-console/e2e-tests/component/load-data/data-loader.ts
+++ b/web-console/e2e-tests/component/load-data/data-loader.ts
@@ -34,7 +34,7 @@ export class DataLoader {
constructor(props: DataLoaderProps) {
Object.assign(this, props);
- this.baseUrl = props.unifiedConsoleUrl + '#load-data';
+ this.baseUrl = props.unifiedConsoleUrl + '#data-loader';
}
/**
diff --git a/web-console/e2e-tests/component/query/overview.ts b/web-console/e2e-tests/component/query/overview.ts
index 6feeebfa9c..b12909c815 100644
--- a/web-console/e2e-tests/component/query/overview.ts
+++ b/web-console/e2e-tests/component/query/overview.ts
@@ -18,7 +18,7 @@
import * as playwright from 'playwright-chromium';
-import { clickButton, clickText, setInput } from '../../util/playwright';
+import { clickButton, clickText } from '../../util/playwright';
import { extractTable } from '../../util/table';
/**
@@ -37,8 +37,9 @@ export class QueryOverview {
await this.page.goto(this.baseUrl);
await this.page.reload({ waitUntil: 'networkidle' });
- const input = await this.page.$('div.query-input textarea');
- await setInput(input!, query);
+ const input = await this.page.waitForSelector('div.query-input textarea');
+ await input.fill(query);
+
await clickButton(this.page, 'Run');
await this.page.waitForSelector('div.query-info');
@@ -49,10 +50,8 @@ export class QueryOverview {
await this.page.goto(this.baseUrl);
await this.page.reload({ waitUntil: 'networkidle' });
- await this.page.waitForSelector('div.query-input textarea');
- const input = await this.page.$('div.query-input textarea');
-
- await setInput(input!, query);
+ const input = await this.page.waitForSelector('div.query-input textarea');
+ await input.fill(query);
await Promise.all([
this.page.waitForRequest(
diff --git a/web-console/e2e-tests/component/ingestion/overview.ts b/web-console/e2e-tests/component/workbench/overview.ts
similarity index 66%
copy from web-console/e2e-tests/component/ingestion/overview.ts
copy to web-console/e2e-tests/component/workbench/overview.ts
index 7c3b8cb3d7..5447daf8f8 100644
--- a/web-console/e2e-tests/component/ingestion/overview.ts
+++ b/web-console/e2e-tests/component/workbench/overview.ts
@@ -18,48 +18,30 @@
import * as playwright from 'playwright-chromium';
+import { clickButton } from '../../util/playwright';
import { extractTable } from '../../util/table';
-import { IngestionTask } from './task';
-
-/**
- * Ingestion overview task table column identifiers.
- */
-enum TaskColumn {
- TASK_ID = 0,
- GROUP_ID,
- TYPE,
- DATASOURCE,
- LOCATION,
- CREATED_TIME,
- STATUS,
- DURATION,
-}
-
/**
- * Represents ingestion overview tab.
+ * Represents the workbench tab.
*/
-export class IngestionOverview {
+export class WorkbenchOverview {
private readonly page: playwright.Page;
private readonly baseUrl: string;
constructor(page: playwright.Page, unifiedConsoleUrl: string) {
this.page = page;
- this.baseUrl = unifiedConsoleUrl + '#ingestion';
+ this.baseUrl = unifiedConsoleUrl + '#workbench';
}
- async getTasks(): Promise<IngestionTask[]> {
+ async runQuery(query: string): Promise<string[][]> {
await this.page.goto(this.baseUrl);
await this.page.reload({ waitUntil: 'networkidle' });
- const data = await extractTable(this.page, 'div.bottom-pane div.rt-tr-group', 'div.rt-td');
+ const input = await this.page.waitForSelector('div.flexible-query-input textarea');
+ await input.fill(query);
+ await clickButton(this.page, 'Run');
+ await this.page.waitForSelector('div.result-table-pane', { timeout: 120000 });
- return data.map(
- row =>
- new IngestionTask({
- datasource: row[TaskColumn.DATASOURCE],
- status: row[TaskColumn.STATUS],
- }),
- );
+ return await extractTable(this.page, 'div.result-table-pane div.rt-tr-group', 'div.rt-td');
}
}
diff --git a/web-console/e2e-tests/multi-stage-query.spec.ts b/web-console/e2e-tests/multi-stage-query.spec.ts
new file mode 100644
index 0000000000..2766c5c8ff
--- /dev/null
+++ b/web-console/e2e-tests/multi-stage-query.spec.ts
@@ -0,0 +1,72 @@
+/*
+ * 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 * as playwright from 'playwright-chromium';
+
+import { WorkbenchOverview } from './component/workbench/overview';
+import { saveScreenshotIfError } from './util/debug';
+import { DRUID_EXAMPLES_QUICKSTART_TUTORIAL_DIR, UNIFIED_CONSOLE_URL } from './util/druid';
+import { createBrowser, createPage } from './util/playwright';
+import { waitTillWebConsoleReady } from './util/setup';
+
+jest.setTimeout(5 * 60 * 1000);
+
+describe('Multi-stage query', () => {
+ let browser: playwright.Browser;
+ let page: playwright.Page;
+
+ beforeAll(async () => {
+ await waitTillWebConsoleReady();
+ browser = await createBrowser();
+ });
+
+ beforeEach(async () => {
+ page = await createPage(browser);
+ });
+
+ afterAll(async () => {
+ await browser.close();
+ });
+
+ it('runs a query that reads external data', async () => {
+ await saveScreenshotIfError('multi-stage-query', page, async () => {
+ const workbench = new WorkbenchOverview(page, UNIFIED_CONSOLE_URL);
+ const results = await workbench.runQuery(`WITH ext AS (SELECT *
+FROM TABLE(
+ EXTERN(
+ '{"type":"local","filter":"wikiticker-2015-09-12-sampled.json.gz","baseDir":${JSON.stringify(
+ DRUID_EXAMPLES_QUICKSTART_TUTORIAL_DIR,
+ )}}',
+ '{"type":"json"}',
+ '[{"name":"channel","type":"string"}]'
+ )
+))
+SELECT
+ channel,
+ CAST(COUNT(*) AS VARCHAR) AS "CountString"
+FROM ext
+GROUP BY 1
+ORDER BY COUNT(*) DESC
+LIMIT 10`);
+ expect(results).toBeDefined();
+ expect(results.length).toBe(10);
+ expect(results[0]).toStrictEqual(['#en.wikipedia', '11549']);
+ expect(results[1]).toStrictEqual(['#vi.wikipedia', '9747']);
+ });
+ });
+});
diff --git a/web-console/lib/keywords.js b/web-console/lib/keywords.js
index accae44e4c..bf30e3578e 100644
--- a/web-console/lib/keywords.js
+++ b/web-console/lib/keywords.js
@@ -54,6 +54,12 @@ exports.SQL_KEYWORDS = [
'ROWS',
'ONLY',
'VALUES',
+ 'PARTITIONED BY',
+ 'CLUSTERED BY',
+ 'TIME',
+ 'INSERT INTO',
+ 'REPLACE INTO',
+ 'OVERWRITE',
];
exports.SQL_EXPRESSION_PARTS = [
diff --git a/web-console/lib/sql-docs.d.ts b/web-console/lib/sql-docs.d.ts
index ce9105cde5..a5af23211d 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: [name: string, runtime: string, description: string][];
+export const SQL_DATA_TYPES: Record<string, [runtime: string, description: string][]>;
export const SQL_FUNCTIONS: Record<string, [args: string, description: string][]>;
diff --git a/web-console/package-lock.json b/web-console/package-lock.json
index aa9e9e8d39..649906dc25 100644
--- a/web-console/package-lock.json
+++ b/web-console/package-lock.json
@@ -5262,16 +5262,6 @@
"integrity": "sha512-wBlsw+8n21e6eTd4yVv8YD/E3xq0O6nNnJIquutAsFGE7EyMKz7W6RNT6BRu1SmdgmlCZ9tb0X+j+D6HGr8pZw==",
"dev": true
},
- "@types/yauzl": {
- "version": "2.9.2",
- "resolved": "https://registry.npmjs.org/@types/yauzl/-/yauzl-2.9.2.tgz",
- "integrity": "sha512-8uALY5LTvSuHgloDVUvWP3pIauILm+8/0pDMokuDYIoNsOkSwd5AiHBTSEJjKTDcZr5z8UpgOWZkxBF4iJftoA==",
- "dev": true,
- "optional": true,
- "requires": {
- "@types/node": "*"
- }
- },
"@typescript-eslint/eslint-plugin": {
"version": "5.11.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.11.0.tgz",
@@ -6465,11 +6455,18 @@
"dev": true
},
"axios": {
- "version": "0.21.4",
- "resolved": "https://registry.npmjs.org/axios/-/axios-0.21.4.tgz",
- "integrity": "sha512-ut5vewkiu8jjGBdqpM44XxjuCjq9LAKeHVmoVfHVzy8eHgxxq8SbAVQNovDA8mVi05kP0Ea/n/UzcSHcTJQfNg==",
+ "version": "0.26.1",
+ "resolved": "https://registry.npmjs.org/axios/-/axios-0.26.1.tgz",
+ "integrity": "sha512-fPwcX4EvnSHuInCMItEhAGnaSEXRBjtzh9fOtsE6E1G6p7vl7edEeZe11QHf18+6+9gR5PbKV/sGKNaD8YaMeA==",
"requires": {
- "follow-redirects": "^1.14.0"
+ "follow-redirects": "^1.14.8"
+ },
+ "dependencies": {
+ "follow-redirects": {
+ "version": "1.15.1",
+ "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.1.tgz",
+ "integrity": "sha512-yLAMQs+k0b2m7cVxpS1VKJVvoz7SS9Td1zss3XRwXj+ZDH00RJgnuLx7E44wx02kQLrdM3aOOy+FpzS7+8OizA=="
+ }
}
},
"babel-jest": {
@@ -7003,12 +7000,6 @@
"node-int64": "^0.4.0"
}
},
- "buffer-crc32": {
- "version": "0.2.13",
- "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz",
- "integrity": "sha1-DTM+PwDqxQqhRUq9MO+MKl2ackI=",
- "dev": true
- },
"buffer-from": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.1.tgz",
@@ -8469,11 +8460,11 @@
}
},
"druid-query-toolkit": {
- "version": "0.14.10",
- "resolved": "https://registry.npmjs.org/druid-query-toolkit/-/druid-query-toolkit-0.14.10.tgz",
- "integrity": "sha512-Y720YxnT3EmqtE/x1QkrkEiomn5TdVArxI3+gdLRH8FYMRedpSPe2nkQVNYma9b7Lww/rzk4Q+a8mNWQ1YH9oQ==",
+ "version": "0.14.24",
+ "resolved": "https://registry.npmjs.org/druid-query-toolkit/-/druid-query-toolkit-0.14.24.tgz",
+ "integrity": "sha512-NBV9prXllZiiYLCfD/k5UmJZg7EU7aqsQPIfTYiYgl9XY4QheY4IO8c5mD7lW8qPpS2qEr4mM9CXjGPPZTQrmw==",
"requires": {
- "tslib": "^2.2.0"
+ "tslib": "^2.3.1"
}
},
"duplexer": {
@@ -10121,44 +10112,6 @@
}
}
},
- "extract-zip": {
- "version": "2.0.1",
- "resolved": "https://registry.npmjs.org/extract-zip/-/extract-zip-2.0.1.tgz",
- "integrity": "sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg==",
- "dev": true,
- "requires": {
- "@types/yauzl": "^2.9.1",
- "debug": "^4.1.1",
- "get-stream": "^5.1.0",
- "yauzl": "^2.10.0"
- },
- "dependencies": {
- "debug": {
- "version": "4.3.3",
- "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.3.tgz",
- "integrity": "sha512-/zxw5+vh1Tfv+4Qn7a5nsbcJKPaSvCDhojn6FEl9vupwK2VCSDtEiEtqr8DFtzYFOdz63LBkxec7DYuc2jon6Q==",
- "dev": true,
- "requires": {
- "ms": "2.1.2"
- }
- },
- "get-stream": {
- "version": "5.2.0",
- "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz",
- "integrity": "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==",
- "dev": true,
- "requires": {
- "pump": "^3.0.0"
- }
- },
- "ms": {
- "version": "2.1.2",
- "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
- "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==",
- "dev": true
- }
- }
- },
"extsprintf": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.3.0.tgz",
@@ -10290,15 +10243,6 @@
"bser": "2.1.1"
}
},
- "fd-slicer": {
- "version": "1.1.0",
- "resolved": "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.1.0.tgz",
- "integrity": "sha1-JcfInLH5B3+IkbvmHY85Dq4lbx4=",
- "dev": true,
- "requires": {
- "pend": "~1.2.0"
- }
- },
"file-entry-cache": {
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz",
@@ -14986,12 +14930,6 @@
}
}
},
- "jpeg-js": {
- "version": "0.4.3",
- "resolved": "https://registry.npmjs.org/jpeg-js/-/jpeg-js-0.4.3.tgz",
- "integrity": "sha512-ru1HWKek8octvUHFHvE5ZzQ1yAsJmIvRdGWvSoKV52XKyuyYA437QWDttXT8eZXDSbuMpHlLzPDZUPd6idIz+Q==",
- "dev": true
- },
"js-base64": {
"version": "2.5.2",
"resolved": "https://registry.npmjs.org/js-base64/-/js-base64-2.5.2.tgz",
@@ -17004,12 +16942,6 @@
"resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz",
"integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw=="
},
- "pend": {
- "version": "1.2.0",
- "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz",
- "integrity": "sha1-elfrVQpng/kRUzH89GY9XI4AelA=",
- "dev": true
- },
"performance-now": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz",
@@ -17095,67 +17027,19 @@
}
},
"playwright-chromium": {
- "version": "1.18.1",
- "resolved": "https://registry.npmjs.org/playwright-chromium/-/playwright-chromium-1.18.1.tgz",
- "integrity": "sha512-DHAOdzZhhu4pMe9yg2zL49JSGXLHTO+DL76duukoy807o+ccu1tEbqyUId46ogLYk7rOjblVK6o7YG/vyVCasQ==",
+ "version": "1.25.0",
+ "resolved": "https://registry.npmjs.org/playwright-chromium/-/playwright-chromium-1.25.0.tgz",
+ "integrity": "sha512-FH9ho3noAWVStCJx4XW78+D8QW0A99WDp53DDkYeVdEpJqCmAIKHCSE6dl5XtaDKrZPYC1ZG5hGXQh1K5H/p+g==",
"dev": true,
"requires": {
- "playwright-core": "=1.18.1"
+ "playwright-core": "1.25.0"
},
"dependencies": {
- "commander": {
- "version": "8.3.0",
- "resolved": "https://registry.npmjs.org/commander/-/commander-8.3.0.tgz",
- "integrity": "sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww==",
- "dev": true
- },
- "debug": {
- "version": "4.3.3",
- "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.3.tgz",
- "integrity": "sha512-/zxw5+vh1Tfv+4Qn7a5nsbcJKPaSvCDhojn6FEl9vupwK2VCSDtEiEtqr8DFtzYFOdz63LBkxec7DYuc2jon6Q==",
- "dev": true,
- "requires": {
- "ms": "2.1.2"
- }
- },
- "ms": {
- "version": "2.1.2",
- "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
- "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==",
- "dev": true
- },
"playwright-core": {
- "version": "1.18.1",
- "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.18.1.tgz",
- "integrity": "sha512-NALGl8R1GHzGLlhUApmpmfh6M1rrrPcDTygWvhTbprxwGB9qd/j9DRwyn4HTQcUB6o0/VOpo46fH9ez3+D/Rog==",
- "dev": true,
- "requires": {
- "commander": "^8.2.0",
- "debug": "^4.1.1",
- "extract-zip": "^2.0.1",
- "https-proxy-agent": "^5.0.0",
- "jpeg-js": "^0.4.2",
- "mime": "^2.4.6",
- "pngjs": "^5.0.0",
- "progress": "^2.0.3",
- "proper-lockfile": "^4.1.1",
- "proxy-from-env": "^1.1.0",
- "rimraf": "^3.0.2",
- "socks-proxy-agent": "^6.1.0",
- "stack-utils": "^2.0.3",
- "ws": "^7.4.6",
- "yauzl": "^2.10.0",
- "yazl": "^2.5.1"
- }
- },
- "rimraf": {
- "version": "3.0.2",
- "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz",
- "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==",
- "dev": true,
- "requires": {
- "glob": "^7.1.3"
- }
+ "version": "1.25.0",
+ "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.25.0.tgz",
+ "integrity": "sha512-kZ3Jwaf3wlu0GgU0nB8UMQ+mXFTqBIFz9h1svTlNduNKjnbPXFxw7mJanLVjqxHJRn62uBfmgBj93YHidk2N5Q==",
+ "dev": true
}
}
},
@@ -17165,12 +17049,6 @@
"integrity": "sha512-Nc3IT5yHzflTfbjgqWcCPpo7DaKy4FnpB0l/zCAW0Tc7jxAiuqSxHasntB3D7887LSrA93kDJ9IXovxJYxyLCA==",
"dev": true
},
- "pngjs": {
- "version": "5.0.0",
- "resolved": "https://registry.npmjs.org/pngjs/-/pngjs-5.0.0.tgz",
- "integrity": "sha512-40QW5YalBNfQo5yRYmiw7Yz6TKKVr3h6970B2YE+3fQpsWcrbj1PzJgxeJ19DRQjhMbKPIuMY8rFaXc8moolVw==",
- "dev": true
- },
"popper.js": {
"version": "1.16.1",
"resolved": "https://registry.npmjs.org/popper.js/-/popper.js-1.16.1.tgz",
@@ -18791,12 +18669,6 @@
"integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==",
"dev": true
},
- "progress": {
- "version": "2.0.3",
- "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz",
- "integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==",
- "dev": true
- },
"prompts": {
"version": "2.4.2",
"resolved": "https://registry.npmjs.org/prompts/-/prompts-2.4.2.tgz",
@@ -18828,25 +18700,6 @@
"reflect.ownkeys": "^0.2.0"
}
},
- "proper-lockfile": {
- "version": "4.1.2",
- "resolved": "https://registry.npmjs.org/proper-lockfile/-/proper-lockfile-4.1.2.tgz",
- "integrity": "sha512-TjNPblN4BwAWMXU8s9AEz4JmQxnD1NNL7bNOY/AKUzyamc379FWASUhc/K1pL2noVb+XmZKLL68cjzLsiOAMaA==",
- "dev": true,
- "requires": {
- "graceful-fs": "^4.2.4",
- "retry": "^0.12.0",
- "signal-exit": "^3.0.2"
- },
- "dependencies": {
- "graceful-fs": {
- "version": "4.2.9",
- "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.9.tgz",
- "integrity": "sha512-NtNxqUcXgpW2iMrfqSfR73Glt39K+BLwWsPs94yR63v45T0Wbej7eRmL5cWfwEgqXnmjQp3zaJTshdRW/qC2ZQ==",
- "dev": true
- }
- }
- },
"proxy-addr": {
"version": "2.0.6",
"resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.6.tgz",
@@ -18857,12 +18710,6 @@
"ipaddr.js": "1.9.1"
}
},
- "proxy-from-env": {
- "version": "1.1.0",
- "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
- "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==",
- "dev": true
- },
"prr": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/prr/-/prr-1.0.1.tgz",
@@ -19500,6 +19347,160 @@
"is-finite": "^1.0.0"
}
},
+ "replace": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/replace/-/replace-1.2.1.tgz",
+ "integrity": "sha512-KZCBe/tPanwBlbjSMQby4l+zjSiFi3CLEP/6VLClnRYgJ46DZ5u9tmA6ceWeFS8coaUnU4ZdGNb/puUGMHNSRg==",
+ "dev": true,
+ "requires": {
+ "chalk": "2.4.2",
+ "minimatch": "3.0.4",
+ "yargs": "^15.3.1"
+ },
+ "dependencies": {
+ "ansi-regex": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
+ "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
+ "dev": true
+ },
+ "ansi-styles": {
+ "version": "4.3.0",
+ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
+ "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
+ "dev": true,
+ "requires": {
+ "color-convert": "^2.0.1"
+ }
+ },
+ "cliui": {
+ "version": "6.0.0",
+ "resolved": "https://registry.npmjs.org/cliui/-/cliui-6.0.0.tgz",
+ "integrity": "sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ==",
+ "dev": true,
+ "requires": {
+ "string-width": "^4.2.0",
+ "strip-ansi": "^6.0.0",
+ "wrap-ansi": "^6.2.0"
+ }
+ },
+ "color-convert": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
+ "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
+ "dev": true,
+ "requires": {
+ "color-name": "~1.1.4"
+ }
+ },
+ "color-name": {
+ "version": "1.1.4",
+ "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
+ "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
+ "dev": true
+ },
+ "find-up": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz",
+ "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==",
+ "dev": true,
+ "requires": {
+ "locate-path": "^5.0.0",
+ "path-exists": "^4.0.0"
+ }
+ },
+ "is-fullwidth-code-point": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
+ "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==",
+ "dev": true
+ },
+ "locate-path": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz",
+ "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==",
+ "dev": true,
+ "requires": {
+ "p-locate": "^4.1.0"
+ }
+ },
+ "p-locate": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz",
+ "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==",
+ "dev": true,
+ "requires": {
+ "p-limit": "^2.2.0"
+ }
+ },
+ "path-exists": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz",
+ "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==",
+ "dev": true
+ },
+ "string-width": {
+ "version": "4.2.3",
+ "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
+ "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
+ "dev": true,
+ "requires": {
+ "emoji-regex": "^8.0.0",
+ "is-fullwidth-code-point": "^3.0.0",
+ "strip-ansi": "^6.0.1"
+ }
+ },
+ "strip-ansi": {
+ "version": "6.0.1",
+ "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
+ "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
+ "dev": true,
+ "requires": {
+ "ansi-regex": "^5.0.1"
+ }
+ },
+ "wrap-ansi": {
+ "version": "6.2.0",
+ "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz",
+ "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==",
+ "dev": true,
+ "requires": {
+ "ansi-styles": "^4.0.0",
+ "string-width": "^4.1.0",
+ "strip-ansi": "^6.0.0"
+ }
+ },
+ "yargs": {
+ "version": "15.4.1",
+ "resolved": "https://registry.npmjs.org/yargs/-/yargs-15.4.1.tgz",
+ "integrity": "sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A==",
+ "dev": true,
+ "requires": {
+ "cliui": "^6.0.0",
+ "decamelize": "^1.2.0",
+ "find-up": "^4.1.0",
+ "get-caller-file": "^2.0.1",
+ "require-directory": "^2.1.1",
+ "require-main-filename": "^2.0.0",
+ "set-blocking": "^2.0.0",
+ "string-width": "^4.2.0",
+ "which-module": "^2.0.0",
+ "y18n": "^4.0.0",
+ "yargs-parser": "^18.1.2"
+ }
+ },
+ "yargs-parser": {
+ "version": "18.1.3",
+ "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-18.1.3.tgz",
+ "integrity": "sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==",
+ "dev": true,
+ "requires": {
+ "camelcase": "^5.0.0",
+ "decamelize": "^1.2.0"
+ }
+ }
+ }
+ },
"request": {
"version": "2.88.2",
"resolved": "https://registry.npmjs.org/request/-/request-2.88.2.tgz",
@@ -20086,12 +20087,6 @@
"integrity": "sha1-VusCfWW00tzmyy4tMsTUr8nh1wc=",
"dev": true
},
- "smart-buffer": {
- "version": "4.2.0",
- "resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz",
- "integrity": "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==",
- "dev": true
- },
"snake-case": {
"version": "3.0.4",
"resolved": "https://registry.npmjs.org/snake-case/-/snake-case-3.0.4.tgz",
@@ -20279,44 +20274,6 @@
}
}
},
- "socks": {
- "version": "2.6.2",
- "resolved": "https://registry.npmjs.org/socks/-/socks-2.6.2.tgz",
- "integrity": "sha512-zDZhHhZRY9PxRruRMR7kMhnf3I8hDs4S3f9RecfnGxvcBHQcKcIH/oUcEWffsfl1XxdYlA7nnlGbbTvPz9D8gA==",
- "dev": true,
- "requires": {
- "ip": "^1.1.5",
- "smart-buffer": "^4.2.0"
- }
- },
- "socks-proxy-agent": {
- "version": "6.1.1",
- "resolved": "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-6.1.1.tgz",
- "integrity": "sha512-t8J0kG3csjA4g6FTbsMOWws+7R7vuRC8aQ/wy3/1OWmsgwA68zs/+cExQ0koSitUDXqhufF/YJr9wtNMZHw5Ew==",
- "dev": true,
- "requires": {
- "agent-base": "^6.0.2",
- "debug": "^4.3.1",
- "socks": "^2.6.1"
- },
- "dependencies": {
- "debug": {
- "version": "4.3.3",
- "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.3.tgz",
- "integrity": "sha512-/zxw5+vh1Tfv+4Qn7a5nsbcJKPaSvCDhojn6FEl9vupwK2VCSDtEiEtqr8DFtzYFOdz63LBkxec7DYuc2jon6Q==",
- "dev": true,
- "requires": {
- "ms": "2.1.2"
- }
- },
- "ms": {
- "version": "2.1.2",
- "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
- "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==",
- "dev": true
- }
- }
- },
"source-list-map": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/source-list-map/-/source-list-map-2.0.1.tgz",
@@ -23529,25 +23486,6 @@
"decamelize": "^1.2.0"
}
},
- "yauzl": {
- "version": "2.10.0",
- "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-2.10.0.tgz",
- "integrity": "sha1-x+sXyT4RLLEIb6bY5R+wZnt5pfk=",
- "dev": true,
- "requires": {
- "buffer-crc32": "~0.2.3",
- "fd-slicer": "~1.1.0"
- }
- },
- "yazl": {
- "version": "2.5.1",
- "resolved": "https://registry.npmjs.org/yazl/-/yazl-2.5.1.tgz",
- "integrity": "sha512-phENi2PLiHnHb6QBVot+dJnaAZ0xosj7p3fWl+znIjBDlnMI2PsZCJZ306BPTFOaHf5qdDEI8x5qFrSOBN5vrw==",
- "dev": true,
- "requires": {
- "buffer-crc32": "~0.2.3"
- }
- },
"yn": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz",
@@ -23560,6 +23498,11 @@
"integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==",
"dev": true
},
+ "zustand": {
+ "version": "3.7.2",
+ "resolved": "https://registry.npmjs.org/zustand/-/zustand-3.7.2.tgz",
+ "integrity": "sha512-PIJDIZKtokhof+9+60cpockVOq05sJzHCriyvaLBmEJixseQ1a5Kdov6fWZfWOu5SK9c+FhH1jU0tntLxRJYMA=="
+ },
"zwitch": {
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/zwitch/-/zwitch-1.0.5.tgz",
diff --git a/web-console/package.json b/web-console/package.json
index a57f669226..e62d6e7e82 100644
--- a/web-console/package.json
+++ b/web-console/package.json
@@ -71,7 +71,7 @@
"@blueprintjs/icons": "^4.1.1",
"@blueprintjs/popover2": "^1.0.3",
"ace-builds": "^1.4.13",
- "axios": "^0.21.4",
+ "axios": "^0.26.1",
"classnames": "^2.2.6",
"copy-to-clipboard": "^3.2.0",
"core-js": "^3.10.1",
@@ -79,7 +79,7 @@
"d3-axis": "^1.0.12",
"d3-scale": "^3.2.0",
"d3-selection": "^1.4.0",
- "druid-query-toolkit": "^0.14.10",
+ "druid-query-toolkit": "^0.14.24",
"file-saver": "^2.0.2",
"follow-redirects": "^1.14.7",
"fontsource-open-sans": "^3.0.9",
@@ -100,7 +100,8 @@
"react-splitter-layout": "^4.0.0",
"react-table": "~6.10.3",
"regenerator-runtime": "^0.13.7",
- "tslib": "^2.3.1"
+ "tslib": "^2.3.1",
+ "zustand": "^3.6.5"
},
"devDependencies": {
"@awesome-code-style/eslint-config": "^4.0.0",
@@ -154,11 +155,12 @@
"jest": "^27.5.0",
"license-checker": "^25.0.1",
"node-sass": "^5.0.0",
- "playwright-chromium": "^1.18.1",
+ "playwright-chromium": "^1.24.1",
"postcss": "^8.3.0",
"postcss-loader": "^5.3.0",
"postcss-preset-env": "^6.7.0",
"prettier": "^2.5.1",
+ "replace": "^1.2.1",
"sass-loader": "^11.0.1",
"snarkdown": "^2.0.0",
"style-loader": "^2.0.0",
diff --git a/web-console/script/create-sql-docs.js b/web-console/script/create-sql-docs.js
index 57fba5b81f..4258a0e99e 100755
--- a/web-console/script/create-sql-docs.js
+++ b/web-console/script/create-sql-docs.js
@@ -70,7 +70,7 @@ const readDoc = async () => {
const lines = data.split('\n');
const functionDocs = {};
- const dataTypeDocs = [];
+ const dataTypeDocs = {};
for (let line of lines) {
const functionMatch = line.match(/^\|\s*`(\w+)\(([^|]*)\)`\s*\|([^|]+)\|(?:([^|]+)\|)?$/);
if (functionMatch) {
@@ -84,11 +84,7 @@ const readDoc = async () => {
const dataTypeMatch = line.match(/^\|([A-Z]+)\|([A-Z]+)\|([^|]*)\|([^|]*)\|$/);
if (dataTypeMatch) {
- dataTypeDocs.push([
- dataTypeMatch[1],
- dataTypeMatch[2],
- convertMarkdownToHtml(dataTypeMatch[4]),
- ]);
+ dataTypeDocs[dataTypeMatch[1]] = [dataTypeMatch[2], convertMarkdownToHtml(dataTypeMatch[4])];
}
}
diff --git a/web-console/script/druid b/web-console/script/druid
index 9f54949b96..67651c66a2 100755
--- a/web-console/script/druid
+++ b/web-console/script/druid
@@ -61,7 +61,7 @@ function _build_distribution() {
&& tar xzf "apache-druid-$(_get_druid_version)-bin.tar.gz" \
&& cd apache-druid-$(_get_druid_version) \
&& bin/run-java -classpath "lib/*" org.apache.druid.cli.Main tools pull-deps -c org.apache.druid.extensions:druid-testing-tools \
- && echo -e "\n\ndruid.extensions.loadList=[\"druid-hdfs-storage\", \"druid-kafka-indexing-service\", \"druid-datasketches\", \"druid-testing-tools\"]" >> conf/druid/single-server/micro-quickstart/_common/common.runtime.properties \
+ && echo -e "\n\ndruid.extensions.loadList=[\"druid-hdfs-storage\", \"druid-kafka-indexing-service\", \"druid-datasketches\", \"druid-multi-stage-query\", \"druid-testing-tools\"]" >> conf/druid/single-server/micro-quickstart/_common/common.runtime.properties \
&& echo -e "\n\ndruid.server.http.allowedHttpMethods=[\"HEAD\"]" >> conf/druid/single-server/micro-quickstart/_common/common.runtime.properties \
)
}
diff --git a/web-console/script/mv b/web-console/script/mv
new file mode 100755
index 0000000000..c5bf8231d1
--- /dev/null
+++ b/web-console/script/mv
@@ -0,0 +1,75 @@
+#!/usr/bin/env node
+/*
+ * 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.
+ */
+
+const fs = require('fs-extra');
+const replace = require('replace');
+
+if (process.argv.length !== 5) {
+ console.log('Usage: mv <src-location> <old-component-name> <new-component-name>');
+ process.exit();
+}
+
+const location = process.argv[2];
+const oldName = process.argv[3];
+const newName = process.argv[4];
+
+if (!/^([a-z0-9-])+$/.test(oldName)) {
+ console.log('must be a hyphen case old name');
+ process.exit();
+}
+
+if (!/^([a-z0-9-])+$/.test(newName)) {
+ console.log('must be a hyphen case new name');
+ process.exit();
+}
+
+const oldPath = './src/' + location + '/' + oldName + '/';
+const newPath = './src/' + location + '/' + newName + '/';
+
+const camelOldName = oldName.replace(/(^|-)[a-z]/g, s => s.replace('-', '').toUpperCase());
+const camelNewName = newName.replace(/(^|-)[a-z]/g, s => s.replace('-', '').toUpperCase());
+
+console.log('Making path:', newPath);
+
+fs.moveSync(oldPath, newPath);
+fs.renameSync(newPath + oldName + '.tsx', newPath + newName + '.tsx');
+try {
+ fs.renameSync(newPath + oldName + '.scss', newPath + newName + '.scss');
+} catch {}
+try {
+ fs.renameSync(newPath + oldName + '.spec.tsx', newPath + newName + '.spec.tsx');
+} catch {}
+
+const replacePath = './src/';
+
+replace({
+ regex: oldName,
+ replacement: newName,
+ paths: [replacePath],
+ recursive: true,
+ silent: true,
+});
+
+replace({
+ regex: camelOldName,
+ replacement: camelNewName,
+ paths: [replacePath],
+ recursive: true,
+ silent: true,
+});
diff --git a/web-console/src/ace-modes/dsql.js b/web-console/src/ace-modes/dsql.js
index 61ff90f26d..c54970b17c 100644
--- a/web-console/src/ace-modes/dsql.js
+++ b/web-console/src/ace-modes/dsql.js
@@ -48,9 +48,7 @@ ace.define(
).join('|');
// Stuff like: 'int|numeric|decimal|date|varchar|char|bigint|float|double|bit|binary|text|set|timestamp'
- var dataTypes = druidFunctions.SQL_DATA_TYPES.map(function (f) {
- return f[0];
- }).join('|');
+ var dataTypes = Object.keys(druidFunctions.SQL_DATA_TYPES).join('|');
var keywordMapper = this.createKeywordMapper(
{
diff --git a/web-console/src/bootstrap/react-table-defaults.tsx b/web-console/src/bootstrap/react-table-defaults.tsx
index 9058b95839..4c31928064 100644
--- a/web-console/src/bootstrap/react-table-defaults.tsx
+++ b/web-console/src/bootstrap/react-table-defaults.tsx
@@ -28,7 +28,7 @@ import {
} from '../react-table';
import { countBy } from '../utils';
-const NoData = React.memo(function NoData(props) {
+const NoData = React.memo(function NoData(props: { children?: React.ReactNode }) {
const { children } = props;
if (!children) return null;
return <div className="rt-noData">{children}</div>;
diff --git a/web-console/src/components/braced-text/braced-text.scss b/web-console/src/components/braced-text/braced-text.scss
index 56f649397e..e9d390c12c 100644
--- a/web-console/src/components/braced-text/braced-text.scss
+++ b/web-console/src/components/braced-text/braced-text.scss
@@ -26,6 +26,7 @@
top: -50000px; // Send it into the stratosphere (get it out of the parent container to prevent the browser from adding '...')
opacity: 0;
pointer-events: none;
+ user-select: none;
}
.real-text {
diff --git a/web-console/src/components/click-to-copy/__snapshots__/click-to-copy.spec.tsx.snap b/web-console/src/components/click-to-copy/__snapshots__/click-to-copy.spec.tsx.snap
new file mode 100644
index 0000000000..a53da72f6d
--- /dev/null
+++ b/web-console/src/components/click-to-copy/__snapshots__/click-to-copy.spec.tsx.snap
@@ -0,0 +1,11 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`ClickToCopy matches snapshot 1`] = `
+<a
+ class="click-to-copy"
+ title="Click to copy:
+Hello world"
+>
+ Hello world
+</a>
+`;
diff --git a/web-console/src/views/load-data-view/learn-more/learn-more.spec.tsx b/web-console/src/components/click-to-copy/click-to-copy.spec.tsx
similarity index 83%
copy from web-console/src/views/load-data-view/learn-more/learn-more.spec.tsx
copy to web-console/src/components/click-to-copy/click-to-copy.spec.tsx
index 22968c7925..d554d392fe 100644
--- a/web-console/src/views/load-data-view/learn-more/learn-more.spec.tsx
+++ b/web-console/src/components/click-to-copy/click-to-copy.spec.tsx
@@ -19,13 +19,13 @@
import { render } from '@testing-library/react';
import React from 'react';
-import { LearnMore } from './learn-more';
+import { ClickToCopy } from './click-to-copy';
-describe('LearnMore', () => {
+describe('ClickToCopy', () => {
it('matches snapshot', () => {
- const learnMore = <LearnMore href="https://druid.apache.org/docs" />;
+ const arrayInput = <ClickToCopy text="Hello world" />;
- const { container } = render(learnMore);
+ const { container } = render(arrayInput);
expect(container.firstChild).toMatchSnapshot();
});
});
diff --git a/web-console/src/views/load-data-view/learn-more/learn-more.tsx b/web-console/src/components/click-to-copy/click-to-copy.tsx
similarity index 58%
copy from web-console/src/views/load-data-view/learn-more/learn-more.tsx
copy to web-console/src/components/click-to-copy/click-to-copy.tsx
index afbdf9d92f..5d1d2a8cf4 100644
--- a/web-console/src/views/load-data-view/learn-more/learn-more.tsx
+++ b/web-console/src/components/click-to-copy/click-to-copy.tsx
@@ -15,21 +15,32 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-
+import { Intent } from '@blueprintjs/core';
+import copy from 'copy-to-clipboard';
import React from 'react';
-import { ExternalLink } from '../../../components';
+import { AppToaster } from '../../singletons';
-export interface LearnMoreProps {
- href: string;
+export interface ClickToCopyProps {
+ text: string;
}
-export const LearnMore = React.memo(function LearnMore(props: LearnMoreProps) {
- const { href } = props;
+export const ClickToCopy = React.memo(function ClickToCopy(props: ClickToCopyProps) {
+ const { text } = props;
return (
- <p className="learn-more">
- <ExternalLink href={href}>Learn more</ExternalLink>
- </p>
+ <a
+ className="click-to-copy"
+ title={`Click to copy:\n${text}`}
+ onClick={() => {
+ copy(text, { format: 'text/plain' });
+ AppToaster.show({
+ message: `'${text}' copied to clipboard`,
+ intent: Intent.SUCCESS,
+ });
+ }}
+ >
+ {text}
+ </a>
);
});
diff --git a/web-console/src/components/datasource-columns-table/__snapshots__/datasource-columns-table.spec.tsx.snap b/web-console/src/components/datasource-columns-table/__snapshots__/datasource-columns-table.spec.tsx.snap
deleted file mode 100644
index dca59389af..0000000000
--- a/web-console/src/components/datasource-columns-table/__snapshots__/datasource-columns-table.spec.tsx.snap
+++ /dev/null
@@ -1,668 +0,0 @@
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[`DatasourceColumnsTable matches snapshot on error 1`] = `
-<div
- className="datasource-columns-table"
->
- <div
- className="main-area"
- >
- <ReactTable
- AggregatedComponent={[Function]}
- ExpanderComponent={[Function]}
- FilterComponent={[Function]}
- LoadingComponent={[Function]}
- NoDataComponent={[Function]}
- PadRowComponent={[Function]}
- PaginationComponent={[Function]}
- PivotValueComponent={[Function]}
- ResizerComponent={[Function]}
- TableComponent={[Function]}
- TbodyComponent={[Function]}
- TdComponent={[Function]}
- TfootComponent={[Function]}
- ThComponent={[Function]}
- TheadComponent={[Function]}
- TrComponent={[Function]}
- TrGroupComponent={[Function]}
- aggregatedKey="_aggregated"
- className=""
- collapseOnDataChange={true}
- collapseOnPageChange={true}
- collapseOnSortingChange={true}
- column={
- Object {
- "Aggregated": undefined,
- "Cell": undefined,
- "Expander": undefined,
- "Filter": undefined,
- "Footer": undefined,
- "Header": undefined,
- "Pivot": undefined,
- "PivotValue": undefined,
- "Placeholder": undefined,
- "aggregate": undefined,
- "className": "",
- "filterAll": false,
- "filterMethod": undefined,
- "filterable": undefined,
- "footerClassName": "",
- "footerStyle": Object {},
- "getFooterProps": [Function],
- "getHeaderProps": [Function],
- "getProps": [Function],
- "headerClassName": "",
- "headerStyle": Object {},
- "minResizeWidth": 11,
- "minWidth": 100,
- "resizable": undefined,
- "show": true,
- "sortMethod": undefined,
- "sortable": undefined,
- "style": Object {},
- }
- }
- columns={
- Array [
- Object {
- "Header": "Column name",
- "accessor": "COLUMN_NAME",
- "className": "padded",
- "width": 300,
- },
- Object {
- "Header": "Data type",
- "accessor": "DATA_TYPE",
- "className": "padded",
- "width": 200,
- },
- ]
- }
- data={Array []}
- defaultExpanded={Object {}}
- defaultFilterMethod={[Function]}
- defaultFiltered={Array []}
- defaultPage={0}
- defaultPageSize={25}
- defaultResized={Array []}
- defaultSortDesc={false}
- defaultSortMethod={[Function]}
- defaultSorted={Array []}
- expanderDefaults={
- Object {
- "filterable": false,
- "resizable": false,
- "sortable": false,
- "width": 35,
- }
- }
- filterable={true}
- freezeWhenExpanded={false}
- getLoadingProps={[Function]}
- getNoDataProps={[Function]}
- getPaginationProps={[Function]}
- getProps={[Function]}
- getResizerProps={[Function]}
- getTableProps={[Function]}
- getTbodyProps={[Function]}
- getTdProps={[Function]}
- getTfootProps={[Function]}
- getTfootTdProps={[Function]}
- getTfootTrProps={[Function]}
- getTheadFilterProps={[Function]}
- getTheadFilterThProps={[Function]}
- getTheadFilterTrProps={[Function]}
- getTheadGroupProps={[Function]}
- getTheadGroupThProps={[Function]}
- getTheadGroupTrProps={[Function]}
- getTheadProps={[Function]}
- getTheadThProps={[Function]}
- getTheadTrProps={[Function]}
- getTrGroupProps={[Function]}
- getTrProps={[Function]}
- groupedByPivotKey="_groupedByPivot"
- indexKey="_index"
- loading={false}
- loadingText="Loading..."
- multiSort={true}
- nestingLevelKey="_nestingLevel"
- nextText="Next"
- noDataText="test error"
- ofText="of"
- onFetchData={[Function]}
- originalKey="_original"
- pageJumpText="jump to page"
- pageSizeOptions={
- Array [
- 25,
- 50,
- 100,
- ]
- }
- pageText="Page"
- pivotDefaults={Object {}}
- pivotIDKey="_pivotID"
- pivotValKey="_pivotVal"
- previousText="Previous"
- resizable={true}
- resolveData={[Function]}
- rowsSelectorText="rows per page"
- rowsText="rows"
- showPageJump={true}
- showPageSizeOptions={true}
- showPagination={false}
- showPaginationBottom={true}
- showPaginationTop={false}
- sortable={true}
- style={Object {}}
- subRowsKey="_subRows"
- />
- </div>
-</div>
-`;
-
-exports[`DatasourceColumnsTable matches snapshot on init 1`] = `
-<div
- className="datasource-columns-table"
->
- <div
- className="main-area"
- >
- <ReactTable
- AggregatedComponent={[Function]}
- ExpanderComponent={[Function]}
- FilterComponent={[Function]}
- LoadingComponent={[Function]}
- NoDataComponent={[Function]}
- PadRowComponent={[Function]}
- PaginationComponent={[Function]}
- PivotValueComponent={[Function]}
- ResizerComponent={[Function]}
- TableComponent={[Function]}
- TbodyComponent={[Function]}
- TdComponent={[Function]}
- TfootComponent={[Function]}
- ThComponent={[Function]}
- TheadComponent={[Function]}
- TrComponent={[Function]}
- TrGroupComponent={[Function]}
- aggregatedKey="_aggregated"
- className=""
- collapseOnDataChange={true}
- collapseOnPageChange={true}
- collapseOnSortingChange={true}
- column={
- Object {
- "Aggregated": undefined,
- "Cell": undefined,
- "Expander": undefined,
- "Filter": undefined,
- "Footer": undefined,
- "Header": undefined,
- "Pivot": undefined,
- "PivotValue": undefined,
- "Placeholder": undefined,
- "aggregate": undefined,
- "className": "",
- "filterAll": false,
- "filterMethod": undefined,
- "filterable": undefined,
- "footerClassName": "",
- "footerStyle": Object {},
- "getFooterProps": [Function],
- "getHeaderProps": [Function],
- "getProps": [Function],
- "headerClassName": "",
- "headerStyle": Object {},
- "minResizeWidth": 11,
- "minWidth": 100,
- "resizable": undefined,
- "show": true,
- "sortMethod": undefined,
- "sortable": undefined,
- "style": Object {},
- }
- }
- columns={
- Array [
- Object {
- "Header": "Column name",
- "accessor": "COLUMN_NAME",
- "className": "padded",
- "width": 300,
- },
- Object {
- "Header": "Data type",
- "accessor": "DATA_TYPE",
- "className": "padded",
- "width": 200,
- },
- ]
- }
- data={Array []}
- defaultExpanded={Object {}}
- defaultFilterMethod={[Function]}
- defaultFiltered={Array []}
- defaultPage={0}
- defaultPageSize={25}
- defaultResized={Array []}
- defaultSortDesc={false}
- defaultSortMethod={[Function]}
- defaultSorted={Array []}
- expanderDefaults={
- Object {
- "filterable": false,
- "resizable": false,
- "sortable": false,
- "width": 35,
- }
- }
- filterable={true}
- freezeWhenExpanded={false}
- getLoadingProps={[Function]}
- getNoDataProps={[Function]}
- getPaginationProps={[Function]}
- getProps={[Function]}
- getResizerProps={[Function]}
- getTableProps={[Function]}
- getTbodyProps={[Function]}
- getTdProps={[Function]}
- getTfootProps={[Function]}
- getTfootTdProps={[Function]}
- getTfootTrProps={[Function]}
- getTheadFilterProps={[Function]}
- getTheadFilterThProps={[Function]}
- getTheadFilterTrProps={[Function]}
- getTheadGroupProps={[Function]}
- getTheadGroupThProps={[Function]}
- getTheadGroupTrProps={[Function]}
- getTheadProps={[Function]}
- getTheadThProps={[Function]}
- getTheadTrProps={[Function]}
- getTrGroupProps={[Function]}
- getTrProps={[Function]}
- groupedByPivotKey="_groupedByPivot"
- indexKey="_index"
- loading={false}
- loadingText="Loading..."
- multiSort={true}
- nestingLevelKey="_nestingLevel"
- nextText="Next"
- noDataText="No column data found"
- ofText="of"
- onFetchData={[Function]}
- originalKey="_original"
- pageJumpText="jump to page"
- pageSizeOptions={
- Array [
- 25,
- 50,
- 100,
- ]
- }
- pageText="Page"
- pivotDefaults={Object {}}
- pivotIDKey="_pivotID"
- pivotValKey="_pivotVal"
- previousText="Previous"
- resizable={true}
- resolveData={[Function]}
- rowsSelectorText="rows per page"
- rowsText="rows"
- showPageJump={true}
- showPageSizeOptions={true}
- showPagination={false}
- showPaginationBottom={true}
- showPaginationTop={false}
- sortable={true}
- style={Object {}}
- subRowsKey="_subRows"
- />
- </div>
-</div>
-`;
-
-exports[`DatasourceColumnsTable matches snapshot on loading 1`] = `
-<div
- className="datasource-columns-table"
->
- <div
- className="main-area"
- >
- <Memo(Loader) />
- </div>
-</div>
-`;
-
-exports[`DatasourceColumnsTable matches snapshot on no data 1`] = `
-<div
- className="datasource-columns-table"
->
- <div
- className="main-area"
- >
- <ReactTable
- AggregatedComponent={[Function]}
- ExpanderComponent={[Function]}
- FilterComponent={[Function]}
- LoadingComponent={[Function]}
- NoDataComponent={[Function]}
- PadRowComponent={[Function]}
- PaginationComponent={[Function]}
- PivotValueComponent={[Function]}
- ResizerComponent={[Function]}
- TableComponent={[Function]}
- TbodyComponent={[Function]}
- TdComponent={[Function]}
- TfootComponent={[Function]}
- ThComponent={[Function]}
- TheadComponent={[Function]}
- TrComponent={[Function]}
- TrGroupComponent={[Function]}
- aggregatedKey="_aggregated"
- className=""
- collapseOnDataChange={true}
- collapseOnPageChange={true}
- collapseOnSortingChange={true}
- column={
- Object {
- "Aggregated": undefined,
- "Cell": undefined,
- "Expander": undefined,
- "Filter": undefined,
- "Footer": undefined,
- "Header": undefined,
- "Pivot": undefined,
- "PivotValue": undefined,
- "Placeholder": undefined,
- "aggregate": undefined,
- "className": "",
- "filterAll": false,
- "filterMethod": undefined,
- "filterable": undefined,
- "footerClassName": "",
- "footerStyle": Object {},
- "getFooterProps": [Function],
- "getHeaderProps": [Function],
- "getProps": [Function],
- "headerClassName": "",
- "headerStyle": Object {},
- "minResizeWidth": 11,
- "minWidth": 100,
- "resizable": undefined,
- "show": true,
- "sortMethod": undefined,
- "sortable": undefined,
- "style": Object {},
- }
- }
- columns={
- Array [
- Object {
- "Header": "Column name",
- "accessor": "COLUMN_NAME",
- "className": "padded",
- "width": 300,
- },
- Object {
- "Header": "Data type",
- "accessor": "DATA_TYPE",
- "className": "padded",
- "width": 200,
- },
- ]
- }
- data={Array []}
- defaultExpanded={Object {}}
- defaultFilterMethod={[Function]}
- defaultFiltered={Array []}
- defaultPage={0}
- defaultPageSize={25}
- defaultResized={Array []}
- defaultSortDesc={false}
- defaultSortMethod={[Function]}
- defaultSorted={Array []}
- expanderDefaults={
- Object {
- "filterable": false,
- "resizable": false,
- "sortable": false,
- "width": 35,
- }
- }
- filterable={true}
- freezeWhenExpanded={false}
- getLoadingProps={[Function]}
- getNoDataProps={[Function]}
- getPaginationProps={[Function]}
- getProps={[Function]}
- getResizerProps={[Function]}
- getTableProps={[Function]}
- getTbodyProps={[Function]}
- getTdProps={[Function]}
- getTfootProps={[Function]}
- getTfootTdProps={[Function]}
- getTfootTrProps={[Function]}
- getTheadFilterProps={[Function]}
- getTheadFilterThProps={[Function]}
- getTheadFilterTrProps={[Function]}
- getTheadGroupProps={[Function]}
- getTheadGroupThProps={[Function]}
- getTheadGroupTrProps={[Function]}
- getTheadProps={[Function]}
- getTheadThProps={[Function]}
- getTheadTrProps={[Function]}
- getTrGroupProps={[Function]}
- getTrProps={[Function]}
- groupedByPivotKey="_groupedByPivot"
- indexKey="_index"
- loading={false}
- loadingText="Loading..."
- multiSort={true}
- nestingLevelKey="_nestingLevel"
- nextText="Next"
- noDataText="No column data found"
- ofText="of"
- onFetchData={[Function]}
- originalKey="_original"
- pageJumpText="jump to page"
- pageSizeOptions={
- Array [
- 25,
- 50,
- 100,
- ]
- }
- pageText="Page"
- pivotDefaults={Object {}}
- pivotIDKey="_pivotID"
- pivotValKey="_pivotVal"
- previousText="Previous"
- resizable={true}
- resolveData={[Function]}
- rowsSelectorText="rows per page"
- rowsText="rows"
- showPageJump={true}
- showPageSizeOptions={true}
- showPagination={false}
- showPaginationBottom={true}
- showPaginationTop={false}
- sortable={true}
- style={Object {}}
- subRowsKey="_subRows"
- />
- </div>
-</div>
-`;
-
-exports[`DatasourceColumnsTable matches snapshot on some data 1`] = `
-<div
- className="datasource-columns-table"
->
- <div
- className="main-area"
- >
- <ReactTable
- AggregatedComponent={[Function]}
- ExpanderComponent={[Function]}
- FilterComponent={[Function]}
- LoadingComponent={[Function]}
- NoDataComponent={[Function]}
- PadRowComponent={[Function]}
- PaginationComponent={[Function]}
- PivotValueComponent={[Function]}
- ResizerComponent={[Function]}
- TableComponent={[Function]}
- TbodyComponent={[Function]}
- TdComponent={[Function]}
- TfootComponent={[Function]}
- ThComponent={[Function]}
- TheadComponent={[Function]}
- TrComponent={[Function]}
- TrGroupComponent={[Function]}
- aggregatedKey="_aggregated"
- className=""
- collapseOnDataChange={true}
- collapseOnPageChange={true}
- collapseOnSortingChange={true}
- column={
- Object {
- "Aggregated": undefined,
- "Cell": undefined,
- "Expander": undefined,
- "Filter": undefined,
- "Footer": undefined,
- "Header": undefined,
- "Pivot": undefined,
- "PivotValue": undefined,
- "Placeholder": undefined,
- "aggregate": undefined,
- "className": "",
- "filterAll": false,
- "filterMethod": undefined,
- "filterable": undefined,
- "footerClassName": "",
- "footerStyle": Object {},
- "getFooterProps": [Function],
- "getHeaderProps": [Function],
- "getProps": [Function],
- "headerClassName": "",
- "headerStyle": Object {},
- "minResizeWidth": 11,
- "minWidth": 100,
- "resizable": undefined,
- "show": true,
- "sortMethod": undefined,
- "sortable": undefined,
- "style": Object {},
- }
- }
- columns={
- Array [
- Object {
- "Header": "Column name",
- "accessor": "COLUMN_NAME",
- "className": "padded",
- "width": 300,
- },
- Object {
- "Header": "Data type",
- "accessor": "DATA_TYPE",
- "className": "padded",
- "width": 200,
- },
- ]
- }
- data={
- Array [
- Object {
- "COLUMN_NAME": "channel",
- "DATA_TYPE": "VARCHAR",
- },
- Object {
- "COLUMN_NAME": "page",
- "DATA_TYPE": "VARCHAR",
- },
- ]
- }
- defaultExpanded={Object {}}
- defaultFilterMethod={[Function]}
- defaultFiltered={Array []}
- defaultPage={0}
- defaultPageSize={25}
- defaultResized={Array []}
- defaultSortDesc={false}
- defaultSortMethod={[Function]}
- defaultSorted={Array []}
- expanderDefaults={
- Object {
- "filterable": false,
- "resizable": false,
- "sortable": false,
- "width": 35,
- }
- }
- filterable={true}
- freezeWhenExpanded={false}
- getLoadingProps={[Function]}
- getNoDataProps={[Function]}
- getPaginationProps={[Function]}
- getProps={[Function]}
- getResizerProps={[Function]}
- getTableProps={[Function]}
- getTbodyProps={[Function]}
- getTdProps={[Function]}
- getTfootProps={[Function]}
- getTfootTdProps={[Function]}
- getTfootTrProps={[Function]}
- getTheadFilterProps={[Function]}
- getTheadFilterThProps={[Function]}
- getTheadFilterTrProps={[Function]}
- getTheadGroupProps={[Function]}
- getTheadGroupThProps={[Function]}
- getTheadGroupTrProps={[Function]}
- getTheadProps={[Function]}
- getTheadThProps={[Function]}
- getTheadTrProps={[Function]}
- getTrGroupProps={[Function]}
- getTrProps={[Function]}
- groupedByPivotKey="_groupedByPivot"
- indexKey="_index"
- loading={false}
- loadingText="Loading..."
- multiSort={true}
- nestingLevelKey="_nestingLevel"
- nextText="Next"
- noDataText="No column data found"
- ofText="of"
- onFetchData={[Function]}
- originalKey="_original"
- pageJumpText="jump to page"
- pageSizeOptions={
- Array [
- 25,
- 50,
- 100,
- ]
- }
- pageText="Page"
- pivotDefaults={Object {}}
- pivotIDKey="_pivotID"
- pivotValKey="_pivotVal"
- previousText="Previous"
- resizable={true}
- resolveData={[Function]}
- rowsSelectorText="rows per page"
- rowsText="rows"
- showPageJump={true}
- showPageSizeOptions={true}
- showPagination={false}
- showPaginationBottom={true}
- showPaginationTop={false}
- sortable={true}
- style={Object {}}
- subRowsKey="_subRows"
- />
- </div>
-</div>
-`;
diff --git a/web-console/src/components/braced-text/braced-text.scss b/web-console/src/components/fancy-tab-pane/fancy-tab-pane.scss
similarity index 58%
copy from web-console/src/components/braced-text/braced-text.scss
copy to web-console/src/components/fancy-tab-pane/fancy-tab-pane.scss
index 56f649397e..a32e2b463a 100644
--- a/web-console/src/components/braced-text/braced-text.scss
+++ b/web-console/src/components/fancy-tab-pane/fancy-tab-pane.scss
@@ -16,31 +16,47 @@
* limitations under the License.
*/
-.braced-text {
- position: relative;
- text-align: right;
- white-space: nowrap;
-
- .brace-text {
- position: relative;
- top: -50000px; // Send it into the stratosphere (get it out of the parent container to prevent the browser from adding '...')
- opacity: 0;
- pointer-events: none;
- }
+@import '../../variables';
+
+$side-bar-width: 120px;
- .real-text {
+.fancy-tab-pane {
+ .side-bar {
position: absolute;
- left: 0;
- right: 0;
top: 0;
- bottom: 0;
+ left: 0;
+ width: $side-bar-width;
+ height: 100%;
+ border-right: 1px solid #1f2832;
- .zero-pad {
- visibility: hidden;
- }
+ .tab-button {
+ width: 100%;
+ height: 10vh;
+ cursor: pointer;
+ display: flex;
+ flex-direction: column;
+ border-radius: 0;
+
+ &.active {
+ background-color: #2c74a8;
+ }
- .unselectable {
- user-select: none;
+ .#{$bp-ns}-icon {
+ margin-right: 0;
+ }
+
+ .#{$bp-ns}-button-text {
+ margin-top: 5px;
+ }
}
}
+
+ .main-section {
+ position: absolute;
+ top: 0;
+ left: $side-bar-width;
+ right: 0;
+ height: 100%;
+ padding: 10px 20px 15px 20px;
+ }
}
diff --git a/web-console/src/components/fancy-tab-pane/fancy-tab-pane.tsx b/web-console/src/components/fancy-tab-pane/fancy-tab-pane.tsx
new file mode 100644
index 0000000000..500c0f24dc
--- /dev/null
+++ b/web-console/src/components/fancy-tab-pane/fancy-tab-pane.tsx
@@ -0,0 +1,83 @@
+/*
+ * 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.
+ */
+
+/*
+ * 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 { Button, Icon, IconName, Intent } from '@blueprintjs/core';
+import classNames from 'classnames';
+import React, { ReactNode } from 'react';
+
+import { filterMap } from '../../utils';
+
+import './fancy-tab-pane.scss';
+
+export interface FancyTabButton {
+ id: string;
+ icon: IconName;
+ label: string;
+}
+
+interface FancyTabPaneProps {
+ className?: string;
+ tabs: (FancyTabButton | false | undefined)[];
+ activeTab: string;
+ onActivateTab(newActiveTab: string): void;
+ children?: ReactNode;
+}
+
+export const FancyTabPane = React.memo(function FancyTabPane(props: FancyTabPaneProps) {
+ const { className, tabs, activeTab, onActivateTab, children } = props;
+
+ return (
+ <div className={classNames('fancy-tab-pane', className)}>
+ <div className="side-bar">
+ {filterMap(tabs, d => {
+ if (!d) return;
+ return (
+ <Button
+ className="tab-button"
+ icon={<Icon icon={d.icon} size={20} />}
+ key={d.id}
+ text={d.label}
+ intent={activeTab === d.id ? Intent.PRIMARY : Intent.NONE}
+ minimal={activeTab !== d.id}
+ onClick={() => onActivateTab(d.id)}
+ />
+ );
+ })}
+ </div>
+ <div className="main-section">{children}</div>
+ </div>
+ );
+});
diff --git a/web-console/src/components/form-group-with-info/form-group-with-info.tsx b/web-console/src/components/form-group-with-info/form-group-with-info.tsx
index 1130e7fa7e..a42c4ca61d 100644
--- a/web-console/src/components/form-group-with-info/form-group-with-info.tsx
+++ b/web-console/src/components/form-group-with-info/form-group-with-info.tsx
@@ -37,7 +37,7 @@ export const FormGroupWithInfo = React.memo(function FormGroupWithInfo(
const popover = (
<Popover2 className="info-popover" content={info} position="left-bottom">
- <Icon icon={IconNames.INFO_SIGN} iconSize={14} />
+ <Icon icon={IconNames.INFO_SIGN} size={14} />
</Popover2>
);
diff --git a/web-console/src/components/header-bar/__snapshots__/header-bar.spec.tsx.snap b/web-console/src/components/header-bar/__snapshots__/header-bar.spec.tsx.snap
index b715283ca2..cc7f53463c 100644
--- a/web-console/src/components/header-bar/__snapshots__/header-bar.spec.tsx.snap
+++ b/web-console/src/components/header-bar/__snapshots__/header-bar.spec.tsx.snap
@@ -15,32 +15,109 @@ exports[`HeaderBar matches snapshot 1`] = `
<Blueprint4.NavbarDivider />
<Blueprint4.AnchorButton
active={true}
+ className="header-entry"
disabled={false}
- href="#load-data"
- icon="cloud-upload"
- intent="none"
+ href="#workbench"
+ icon="application"
minimal={true}
- text="Load data"
+ onClick={[Function]}
+ text="Query"
/>
+ <Blueprint4.Popover2
+ boundary="clippingParents"
+ captureDismiss={false}
+ content={
+ <Blueprint4.Menu>
+ <Blueprint4.MenuItem
+ active={false}
+ disabled={false}
+ href="#streaming-data-loader"
+ icon="feed"
+ multiline={false}
+ popoverProps={Object {}}
+ selected={false}
+ shouldDismissPopover={true}
+ text="Streaming"
+ />
+ <Blueprint4.MenuItem
+ active={false}
+ disabled={false}
+ href="#sql-data-loader"
+ icon="clean"
+ labelElement={
+ <Blueprint4.Tag
+ minimal={true}
+ >
+ multi-stage-query
+ </Blueprint4.Tag>
+ }
+ multiline={false}
+ popoverProps={Object {}}
+ selected={false}
+ shouldDismissPopover={true}
+ text="Batch - SQL"
+ />
+ <Blueprint4.MenuItem
+ active={false}
+ disabled={false}
+ href="#classic-batch-data-loader"
+ icon="list"
+ multiline={false}
+ popoverProps={Object {}}
+ selected={false}
+ shouldDismissPopover={true}
+ text="Batch - classic"
+ />
+ </Blueprint4.Menu>
+ }
+ defaultIsOpen={false}
+ disabled={false}
+ fill={false}
+ hasBackdrop={false}
+ hoverCloseDelay={300}
+ hoverOpenDelay={150}
+ inheritDarkTheme={true}
+ interactionKind="click"
+ minimal={false}
+ openOnTargetFocus={true}
+ position="bottom-left"
+ positioningStrategy="absolute"
+ shouldReturnFocusOnClose={false}
+ targetTagName="span"
+ transitionDuration={300}
+ usePortal={true}
+ >
+ <Blueprint4.Button
+ active={false}
+ className="header-entry"
+ disabled={false}
+ icon="cloud-upload"
+ minimal={true}
+ text="Load data"
+ />
+ </Blueprint4.Popover2>
<Blueprint4.NavbarDivider />
<Blueprint4.AnchorButton
active={false}
+ className="header-entry"
disabled={false}
- href="#ingestion"
- icon="gantt-chart"
+ href="#datasources"
+ icon="multi-select"
minimal={true}
- text="Ingestion"
+ text="Datasources"
/>
<Blueprint4.AnchorButton
active={false}
+ className="header-entry"
disabled={false}
- href="#datasources"
- icon="multi-select"
+ href="#ingestion"
+ icon="gantt-chart"
minimal={true}
- text="Datasources"
+ text="Ingestion"
/>
<Blueprint4.AnchorButton
active={false}
+ className="header-entry"
disabled={false}
href="#segments"
icon="stacked-chart"
@@ -49,21 +126,55 @@ exports[`HeaderBar matches snapshot 1`] = `
/>
<Blueprint4.AnchorButton
active={false}
+ className="header-entry"
disabled={false}
href="#services"
icon="database"
minimal={true}
text="Services"
/>
- <Blueprint4.NavbarDivider />
- <Blueprint4.AnchorButton
- active={false}
+ <Blueprint4.Popover2
+ boundary="clippingParents"
+ captureDismiss={false}
+ content={
+ <Blueprint4.Menu>
+ <Blueprint4.MenuItem
+ active={false}
+ disabled={false}
+ href="#lookups"
+ icon="properties"
+ multiline={false}
+ popoverProps={Object {}}
+ selected={false}
+ shouldDismissPopover={true}
+ text="Lookups"
+ />
+ </Blueprint4.Menu>
+ }
+ defaultIsOpen={false}
disabled={false}
- href="#query"
- icon="application"
- minimal={true}
- text="Query"
- />
+ fill={false}
+ hasBackdrop={false}
+ hoverCloseDelay={300}
+ hoverOpenDelay={150}
+ inheritDarkTheme={true}
+ interactionKind="click"
+ minimal={false}
+ openOnTargetFocus={true}
+ position="bottom-left"
+ positioningStrategy="absolute"
+ shouldReturnFocusOnClose={false}
+ targetTagName="span"
+ transitionDuration={300}
+ usePortal={true}
+ >
+ <Blueprint4.Button
+ active={false}
+ className="header-entry"
+ icon="more"
+ minimal={true}
+ />
+ </Blueprint4.Popover2>
</Blueprint4.NavbarGroup>
<Blueprint4.NavbarGroup
align="right"
@@ -72,6 +183,7 @@ exports[`HeaderBar matches snapshot 1`] = `
capabilities={
Capabilities {
"coordinator": true,
+ "multiStageQuery": true,
"overlord": true,
"queryType": "nativeAndSql",
}
@@ -116,17 +228,6 @@ exports[`HeaderBar matches snapshot 1`] = `
shouldDismissPopover={true}
text="Overlord dynamic config"
/>
- <Blueprint4.MenuItem
- active={false}
- disabled={false}
- href="#lookups"
- icon="properties"
- multiline={false}
- popoverProps={Object {}}
- selected={false}
- shouldDismissPopover={true}
- text="Lookups"
- />
<Blueprint4.MenuDivider />
<Blueprint4.MenuItem
active={false}
@@ -201,6 +302,7 @@ exports[`HeaderBar matches snapshot 1`] = `
usePortal={true}
>
<Blueprint4.Button
+ className="header-entry"
icon="cog"
minimal={true}
/>
@@ -224,7 +326,7 @@ exports[`HeaderBar matches snapshot 1`] = `
<Blueprint4.MenuItem
active={false}
disabled={false}
- href="https://druid.apache.org/docs/0.23.0"
+ href="https://druid.apache.org/docs/latest"
icon="th"
multiline={false}
popoverProps={Object {}}
@@ -289,6 +391,7 @@ exports[`HeaderBar matches snapshot 1`] = `
usePortal={true}
>
<Blueprint4.Button
+ className="header-entry"
icon="help"
minimal={true}
/>
diff --git a/web-console/src/components/header-bar/header-bar.scss b/web-console/src/components/header-bar/header-bar.scss
index 783b472cb8..062768a22c 100644
--- a/web-console/src/components/header-bar/header-bar.scss
+++ b/web-console/src/components/header-bar/header-bar.scss
@@ -63,10 +63,7 @@
margin: 0 11px;
}
- .#{$bp-ns}-button.#{$bp-ns}-minimal {
- border-radius: 20px;
- margin: 0 1px;
-
+ .header-entry {
.#{$bp-ns}-icon {
svg {
fill: $blue3;
@@ -76,6 +73,12 @@
}
}
}
+ }
+
+ .#{$bp-ns}-button.#{$bp-ns}-minimal {
+ border-radius: 20px;
+ margin: 0 1px;
+
.#{$bp-ns}-dark & {
&:hover {
background: rgba($dark-gray5, 0.5);
diff --git a/web-console/src/components/header-bar/header-bar.spec.tsx b/web-console/src/components/header-bar/header-bar.spec.tsx
index 61650aa7aa..2e17be7058 100644
--- a/web-console/src/components/header-bar/header-bar.spec.tsx
+++ b/web-console/src/components/header-bar/header-bar.spec.tsx
@@ -26,7 +26,7 @@ import { HeaderBar } from './header-bar';
describe('HeaderBar', () => {
it('matches snapshot', () => {
const headerBar = shallow(
- <HeaderBar active="load-data" capabilities={Capabilities.FULL} onUnrestrict={() => {}} />,
+ <HeaderBar active="workbench" capabilities={Capabilities.FULL} onUnrestrict={() => {}} />,
);
expect(headerBar).toMatchSnapshot();
});
diff --git a/web-console/src/components/header-bar/header-bar.tsx b/web-console/src/components/header-bar/header-bar.tsx
index 53042d91a2..2d5b56af4d 100644
--- a/web-console/src/components/header-bar/header-bar.tsx
+++ b/web-console/src/components/header-bar/header-bar.tsx
@@ -28,6 +28,7 @@ import {
NavbarDivider,
NavbarGroup,
Position,
+ Tag,
} from '@blueprintjs/core';
import { IconNames } from '@blueprintjs/icons';
import { Popover2 } from '@blueprintjs/popover2';
@@ -46,6 +47,7 @@ import {
LocalStorageKeys,
localStorageRemove,
localStorageSetJson,
+ oneOf,
} from '../../utils';
import { ExternalLink } from '../external-link/external-link';
import { PopoverText } from '../popover-text/popover-text';
@@ -56,12 +58,16 @@ const capabilitiesOverride = localStorageGetJson(LocalStorageKeys.CAPABILITIES_O
export type HeaderActiveTab =
| null
- | 'load-data'
+ | 'data-loader'
+ | 'streaming-data-loader'
+ | 'classic-batch-data-loader'
| 'ingestion'
| 'datasources'
| 'segments'
| 'services'
| 'query'
+ | 'workbench'
+ | 'sql-data-loader'
| 'lookups';
const DruidLogo = React.memo(function DruidLogo() {
@@ -233,7 +239,52 @@ export const HeaderBar = React.memo(function HeaderBar(props: HeaderBarProps) {
const [coordinatorDynamicConfigDialogOpen, setCoordinatorDynamicConfigDialogOpen] =
useState(false);
const [overlordDynamicConfigDialogOpen, setOverlordDynamicConfigDialogOpen] = useState(false);
- const loadDataPrimary = false;
+
+ const showSplitDataLoaderMenu = capabilities.hasMultiStageQuery();
+
+ const loadDataViewsMenuActive = oneOf(
+ active,
+ 'data-loader',
+ 'streaming-data-loader',
+ 'classic-batch-data-loader',
+ 'sql-data-loader',
+ );
+ const loadDataViewsMenu = (
+ <Menu>
+ <MenuItem
+ icon={IconNames.FEED}
+ text="Streaming"
+ href="#streaming-data-loader"
+ selected={active === 'streaming-data-loader'}
+ />
+ <MenuItem
+ icon={IconNames.CLEAN}
+ text="Batch - SQL"
+ href="#sql-data-loader"
+ labelElement={<Tag minimal>multi-stage-query</Tag>}
+ selected={active === 'sql-data-loader'}
+ />
+ <MenuItem
+ icon={IconNames.LIST}
+ text="Batch - classic"
+ href="#classic-batch-data-loader"
+ selected={active === 'classic-batch-data-loader'}
+ />
+ </Menu>
+ );
+
+ const moreViewsMenuActive = oneOf(active, 'lookups');
+ const moreViewsMenu = (
+ <Menu>
+ <MenuItem
+ icon={IconNames.PROPERTIES}
+ text="Lookups"
+ href="#lookups"
+ disabled={!capabilities.hasCoordinatorAccess()}
+ selected={active === 'lookups'}
+ />
+ </Menu>
+ );
const helpMenu = (
<Menu>
@@ -290,13 +341,7 @@ export const HeaderBar = React.memo(function HeaderBar(props: HeaderBarProps) {
onClick={() => setOverlordDynamicConfigDialogOpen(true)}
disabled={!capabilities.hasOverlordAccess()}
/>
- <MenuItem
- icon={IconNames.PROPERTIES}
- active={active === 'lookups'}
- text="Lookups"
- href="#lookups"
- disabled={!capabilities.hasCoordinatorAccess()}
- />
+
<MenuDivider />
<MenuItem icon={IconNames.COG} text="Console options">
{capabilitiesOverride ? (
@@ -339,28 +384,50 @@ export const HeaderBar = React.memo(function HeaderBar(props: HeaderBarProps) {
<a href="#">
<DruidLogo />
</a>
-
- <NavbarDivider />
- <AnchorButton
- icon={IconNames.CLOUD_UPLOAD}
- text="Load data"
- active={active === 'load-data'}
- href="#load-data"
- minimal={!loadDataPrimary}
- intent={loadDataPrimary ? Intent.PRIMARY : Intent.NONE}
- disabled={!capabilities.hasEverything()}
- />
-
<NavbarDivider />
<AnchorButton
+ className="header-entry"
minimal
- active={active === 'ingestion'}
- icon={IconNames.GANTT_CHART}
- text="Ingestion"
- href="#ingestion"
- disabled={!capabilities.hasSqlOrOverlordAccess()}
+ active={oneOf(active, 'workbench', 'query')}
+ icon={IconNames.APPLICATION}
+ text="Query"
+ href="#workbench"
+ disabled={!capabilities.hasQuerying()}
+ onClick={e => {
+ if (!e.altKey) return;
+ e.preventDefault();
+ location.hash = '#query';
+ }}
/>
+ {showSplitDataLoaderMenu ? (
+ <Popover2
+ content={loadDataViewsMenu}
+ disabled={!capabilities.hasEverything()}
+ position={Position.BOTTOM_LEFT}
+ >
+ <Button
+ className="header-entry"
+ icon={IconNames.CLOUD_UPLOAD}
+ text="Load data"
+ minimal
+ active={loadDataViewsMenuActive}
+ disabled={!capabilities.hasEverything()}
+ />
+ </Popover2>
+ ) : (
+ <AnchorButton
+ className="header-entry"
+ icon={IconNames.CLOUD_UPLOAD}
+ text="Load data"
+ href="#data-loader"
+ minimal
+ active={loadDataViewsMenuActive}
+ disabled={!capabilities.hasEverything()}
+ />
+ )}
+ <NavbarDivider />
<AnchorButton
+ className="header-entry"
minimal
active={active === 'datasources'}
icon={IconNames.MULTI_SELECT}
@@ -369,6 +436,16 @@ export const HeaderBar = React.memo(function HeaderBar(props: HeaderBarProps) {
disabled={!capabilities.hasSqlOrCoordinatorAccess()}
/>
<AnchorButton
+ className="header-entry"
+ minimal
+ active={active === 'ingestion'}
+ icon={IconNames.GANTT_CHART}
+ text="Ingestion"
+ href="#ingestion"
+ disabled={!capabilities.hasSqlOrOverlordAccess()}
+ />
+ <AnchorButton
+ className="header-entry"
minimal
active={active === 'segments'}
icon={IconNames.STACKED_CHART}
@@ -377,6 +454,7 @@ export const HeaderBar = React.memo(function HeaderBar(props: HeaderBarProps) {
disabled={!capabilities.hasSqlOrCoordinatorAccess()}
/>
<AnchorButton
+ className="header-entry"
minimal
active={active === 'services'}
icon={IconNames.DATABASE}
@@ -384,24 +462,22 @@ export const HeaderBar = React.memo(function HeaderBar(props: HeaderBarProps) {
href="#services"
disabled={!capabilities.hasSqlOrCoordinatorAccess()}
/>
-
- <NavbarDivider />
- <AnchorButton
- minimal
- active={active === 'query'}
- icon={IconNames.APPLICATION}
- text="Query"
- href="#query"
- disabled={!capabilities.hasQuerying()}
- />
+ <Popover2 content={moreViewsMenu} position={Position.BOTTOM_LEFT}>
+ <Button
+ className="header-entry"
+ minimal
+ icon={IconNames.MORE}
+ active={moreViewsMenuActive}
+ />
+ </Popover2>
</NavbarGroup>
<NavbarGroup align={Alignment.RIGHT}>
<RestrictedMode capabilities={capabilities} onUnrestrict={onUnrestrict} />
<Popover2 content={configMenu} position={Position.BOTTOM_RIGHT}>
- <Button minimal icon={IconNames.COG} />
+ <Button className="header-entry" minimal icon={IconNames.COG} />
</Popover2>
<Popover2 content={helpMenu} position={Position.BOTTOM_RIGHT}>
- <Button minimal icon={IconNames.HELP} />
+ <Button className="header-entry" minimal icon={IconNames.HELP} />
</Popover2>
</NavbarGroup>
{aboutDialogOpen && <AboutDialog onClose={() => setAboutDialogOpen(false)} />}
diff --git a/web-console/src/components/index.ts b/web-console/src/components/index.ts
index d3418fdc71..56da967599 100644
--- a/web-console/src/components/index.ts
+++ b/web-console/src/components/index.ts
@@ -23,27 +23,39 @@ export * from './auto-form/auto-form';
export * from './braced-text/braced-text';
export * from './center-message/center-message';
export * from './clearable-input/clearable-input';
+export * from './click-to-copy/click-to-copy';
+export * from './deferred/deferred';
export * from './deferred/deferred';
export * from './external-link/external-link';
+export * from './fancy-tab-pane/fancy-tab-pane';
+export * from './form-group-with-info/form-group-with-info';
export * from './form-json-selector/form-json-selector';
export * from './formatted-input/formatted-input';
export * from './header-bar/header-bar';
export * from './highlight-text/highlight-text';
export * from './json-collapse/json-collapse';
export * from './json-input/json-input';
+export * from './learn-more/learn-more';
export * from './loader/loader';
export * from './menu-checkbox/menu-checkbox';
+export * from './menu-tristate/menu-tristate';
export * from './more-button/more-button';
+export * from './plural-pair-if-needed/plural-pair-if-needed';
export * from './popover-text/popover-text';
+export * from './query-error-pane/query-error-pane';
+export * from './record-table-pane/record-table-pane';
export * from './refresh-button/refresh-button';
export * from './rule-editor/rule-editor';
export * from './segment-timeline/segment-timeline';
export * from './show-json/show-json';
export * from './show-log/show-log';
+export * from './show-value/show-value';
export * from './suggestion-menu/suggestion-menu';
export * from './table-cell/table-cell';
+export * from './table-cell-unparseable/table-cell-unparseable';
export * from './table-clickable-cell/table-clickable-cell';
export * from './table-column-selector/table-column-selector';
export * from './table-filterable-cell/table-filterable-cell';
export * from './timed-button/timed-button';
export * from './view-control-bar/view-control-bar';
+export * from './warning-checklist/warning-checklist';
diff --git a/web-console/src/components/interval-input/interval-input.tsx b/web-console/src/components/interval-input/interval-input.tsx
index 5cc5e26056..ccbe212dcc 100644
--- a/web-console/src/components/interval-input/interval-input.tsx
+++ b/web-console/src/components/interval-input/interval-input.tsx
@@ -60,7 +60,7 @@ export const IntervalInput = React.memo(function IntervalInput(props: IntervalIn
</div>
}
onChange={(e: any) => {
- const value = e.target.value.replace(/[^\-0-9T:/]/g, '').substring(0, 39);
+ const value = e.target.value.replace(/[^\-\dT:/]/g, '').substring(0, 39);
onValueChange(value);
}}
intent={intent}
diff --git a/web-console/src/views/load-data-view/learn-more/__snapshots__/learn-more.spec.tsx.snap b/web-console/src/components/learn-more/__snapshots__/learn-more.spec.tsx.snap
similarity index 100%
rename from web-console/src/views/load-data-view/learn-more/__snapshots__/learn-more.spec.tsx.snap
rename to web-console/src/components/learn-more/__snapshots__/learn-more.spec.tsx.snap
diff --git a/web-console/src/views/load-data-view/learn-more/learn-more.spec.tsx b/web-console/src/components/learn-more/learn-more.spec.tsx
similarity index 100%
copy from web-console/src/views/load-data-view/learn-more/learn-more.spec.tsx
copy to web-console/src/components/learn-more/learn-more.spec.tsx
diff --git a/web-console/src/views/load-data-view/learn-more/learn-more.tsx b/web-console/src/components/learn-more/learn-more.tsx
similarity index 94%
copy from web-console/src/views/load-data-view/learn-more/learn-more.tsx
copy to web-console/src/components/learn-more/learn-more.tsx
index afbdf9d92f..2f2e8d3fbe 100644
--- a/web-console/src/views/load-data-view/learn-more/learn-more.tsx
+++ b/web-console/src/components/learn-more/learn-more.tsx
@@ -18,7 +18,7 @@
import React from 'react';
-import { ExternalLink } from '../../../components';
+import { ExternalLink } from '../external-link/external-link';
export interface LearnMoreProps {
href: string;
diff --git a/web-console/src/components/menu-checkbox/menu-checkbox.tsx b/web-console/src/components/menu-checkbox/menu-checkbox.tsx
index 9477c94cc5..33aa72b7d5 100644
--- a/web-console/src/components/menu-checkbox/menu-checkbox.tsx
+++ b/web-console/src/components/menu-checkbox/menu-checkbox.tsx
@@ -17,10 +17,11 @@
*/
import { MenuItem, MenuItemProps } from '@blueprintjs/core';
-import { IconNames } from '@blueprintjs/icons';
import classNames from 'classnames';
import React from 'react';
+import { checkedCircleIcon } from '../../utils';
+
export interface MenuCheckboxProps extends Omit<MenuItemProps, 'icon' | 'onClick'> {
checked: boolean;
onChange: () => void;
@@ -32,7 +33,7 @@ export function MenuCheckbox(props: MenuCheckboxProps) {
return (
<MenuItem
className={classNames('menu-checkbox', className)}
- icon={checked ? IconNames.TICK_CIRCLE : IconNames.CIRCLE}
+ icon={checkedCircleIcon(checked)}
onClick={onChange}
shouldDismissPopover={shouldDismissPopover ?? false}
{...rest}
diff --git a/web-console/src/components/menu-tristate/__snapshots__/menu-tristate.spec.tsx.snap b/web-console/src/components/menu-tristate/__snapshots__/menu-tristate.spec.tsx.snap
new file mode 100644
index 0000000000..60942ec6a5
--- /dev/null
+++ b/web-console/src/components/menu-tristate/__snapshots__/menu-tristate.spec.tsx.snap
@@ -0,0 +1,103 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`MenuTristate matches snapshot false 1`] = `
+<li
+ class="bp4-submenu"
+>
+ <span
+ class="bp4-popover-wrapper"
+ >
+ <span
+ aria-haspopup="true"
+ class="bp4-popover-target"
+ >
+ <a
+ class="bp4-menu-item menu-tristate"
+ label="false"
+ tabindex="0"
+ >
+ <div
+ class="bp4-fill bp4-text-overflow-ellipsis"
+ >
+ hello
+ </div>
+ <span
+ class="bp4-menu-item-label"
+ >
+ false
+ </span>
+ <span
+ class="bp4-icon bp4-icon-caret-right bp4-submenu-icon"
+ icon="caret-right"
+ >
+ <svg
+ data-icon="caret-right"
+ height="16"
+ viewBox="0 0 16 16"
+ width="16"
+ >
+ <desc>
+ Open sub menu
+ </desc>
+ <path
+ d="M11 8c0-.15-.07-.28-.17-.37l-4-3.5A.495.495 0 006 4.5v7a.495.495 0 00.83.37l4-3.5c.1-.09.17-.22.17-.37z"
+ fill-rule="evenodd"
+ />
+ </svg>
+ </span>
+ </a>
+ </span>
+ </span>
+</li>
+`;
+
+exports[`MenuTristate matches snapshot undefined 1`] = `
+<li
+ class="bp4-submenu"
+>
+ <span
+ class="bp4-popover-wrapper"
+ >
+ <span
+ aria-haspopup="true"
+ class="bp4-popover-target"
+ >
+ <a
+ class="bp4-menu-item menu-tristate"
+ label="auto (true)"
+ tabindex="0"
+ >
+ <div
+ class="bp4-fill bp4-text-overflow-ellipsis"
+ >
+ hello
+ </div>
+ <span
+ class="bp4-menu-item-label"
+ >
+ auto (true)
+ </span>
+ <span
+ class="bp4-icon bp4-icon-caret-right bp4-submenu-icon"
+ icon="caret-right"
+ >
+ <svg
+ data-icon="caret-right"
+ height="16"
+ viewBox="0 0 16 16"
+ width="16"
+ >
+ <desc>
+ Open sub menu
+ </desc>
+ <path
+ d="M11 8c0-.15-.07-.28-.17-.37l-4-3.5A.495.495 0 006 4.5v7a.495.495 0 00.83.37l4-3.5c.1-.09.17-.22.17-.37z"
+ fill-rule="evenodd"
+ />
+ </svg>
+ </span>
+ </a>
+ </span>
+ </span>
+</li>
+`;
diff --git a/web-console/src/views/load-data-view/learn-more/learn-more.spec.tsx b/web-console/src/components/menu-tristate/menu-tristate.spec.tsx
similarity index 58%
copy from web-console/src/views/load-data-view/learn-more/learn-more.spec.tsx
copy to web-console/src/components/menu-tristate/menu-tristate.spec.tsx
index 22968c7925..40fbc54f0d 100644
--- a/web-console/src/views/load-data-view/learn-more/learn-more.spec.tsx
+++ b/web-console/src/components/menu-tristate/menu-tristate.spec.tsx
@@ -19,13 +19,32 @@
import { render } from '@testing-library/react';
import React from 'react';
-import { LearnMore } from './learn-more';
+import { MenuTristate } from './menu-tristate';
-describe('LearnMore', () => {
- it('matches snapshot', () => {
- const learnMore = <LearnMore href="https://druid.apache.org/docs" />;
+describe('MenuTristate', () => {
+ it('matches snapshot undefined', () => {
+ const menuCheckbox = (
+ <MenuTristate
+ text="hello"
+ value={undefined}
+ undefinedEffectiveValue
+ onValueChange={() => {}}
+ />
+ );
+ const { container } = render(menuCheckbox);
+ expect(container.firstChild).toMatchSnapshot();
+ });
- const { container } = render(learnMore);
+ it('matches snapshot false', () => {
+ const menuCheckbox = (
+ <MenuTristate
+ text="hello"
+ value={false}
+ undefinedEffectiveValue={false}
+ onValueChange={() => {}}
+ />
+ );
+ const { container } = render(menuCheckbox);
expect(container.firstChild).toMatchSnapshot();
});
});
diff --git a/web-console/src/components/menu-tristate/menu-tristate.tsx b/web-console/src/components/menu-tristate/menu-tristate.tsx
new file mode 100644
index 0000000000..7f54d1a319
--- /dev/null
+++ b/web-console/src/components/menu-tristate/menu-tristate.tsx
@@ -0,0 +1,71 @@
+/*
+ * 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 { MenuItem, MenuItemProps } from '@blueprintjs/core';
+import classNames from 'classnames';
+import React from 'react';
+
+import { tickIcon } from '../../utils';
+
+export interface MenuTristateProps extends Omit<MenuItemProps, 'label'> {
+ value: boolean | undefined;
+ onValueChange(value: boolean | undefined): void;
+ undefinedLabel?: string;
+ undefinedEffectiveValue?: boolean;
+}
+
+export function MenuTristate(props: MenuTristateProps) {
+ const {
+ value,
+ onValueChange,
+ undefinedLabel,
+ undefinedEffectiveValue,
+ className,
+ shouldDismissPopover,
+ ...rest
+ } = props;
+ const shouldDismiss = shouldDismissPopover ?? false;
+
+ function formatValue(value: boolean | undefined): string {
+ return String(value ?? undefinedLabel ?? 'auto');
+ }
+
+ return (
+ <MenuItem
+ className={classNames('menu-tristate', className)}
+ shouldDismissPopover={shouldDismiss}
+ label={
+ formatValue(value) +
+ (typeof value === 'undefined' && typeof undefinedEffectiveValue === 'boolean'
+ ? ` (${undefinedEffectiveValue})`
+ : '')
+ }
+ {...rest}
+ >
+ {[undefined, true, false].map((v, i) => (
+ <MenuItem
+ key={i}
+ icon={tickIcon(value === v)}
+ text={formatValue(v)}
+ onClick={() => onValueChange(v)}
+ shouldDismissPopover={shouldDismiss}
+ />
+ ))}
+ </MenuItem>
+ );
+}
diff --git a/web-console/src/components/more-button/__snapshots__/more-button.spec.tsx.snap b/web-console/src/components/more-button/__snapshots__/more-button.spec.tsx.snap
index 119b10c70d..21d4875e3f 100644
--- a/web-console/src/components/more-button/__snapshots__/more-button.spec.tsx.snap
+++ b/web-console/src/components/more-button/__snapshots__/more-button.spec.tsx.snap
@@ -9,6 +9,7 @@ exports[`MoreButton matches snapshot (empty) 1`] = `
class="bp4-button bp4-disabled"
disabled=""
tabindex="-1"
+ title="More actions"
type="button"
>
<span
@@ -40,6 +41,7 @@ exports[`MoreButton matches snapshot (full) 1`] = `
>
<button
class="bp4-button"
+ title="More actions"
type="button"
>
<span
diff --git a/web-console/src/components/more-button/more-button.tsx b/web-console/src/components/more-button/more-button.tsx
index a7ac38a6c2..16157b34b8 100644
--- a/web-console/src/components/more-button/more-button.tsx
+++ b/web-console/src/components/more-button/more-button.tsx
@@ -55,7 +55,7 @@ export const MoreButton = React.memo(function MoreButton(props: MoreButtonProps)
setOpenState(nextOpenState ? (e.altKey ? 'alt-open' : 'open') : undefined);
}}
>
- <Button icon={IconNames.MORE} disabled={!childCount} />
+ <Button icon={IconNames.MORE} disabled={!childCount} title="More actions" />
</Popover2>
);
});
diff --git a/web-console/src/views/query-view/query-error/__snapshots__/query-error.spec.tsx.snap b/web-console/src/components/query-error-pane/__snapshots__/query-error-pane.spec.tsx.snap
similarity index 56%
rename from web-console/src/views/query-view/query-error/__snapshots__/query-error.spec.tsx.snap
rename to web-console/src/components/query-error-pane/__snapshots__/query-error-pane.spec.tsx.snap
index 7630235852..2402e0dabb 100644
--- a/web-console/src/views/query-view/query-error/__snapshots__/query-error.spec.tsx.snap
+++ b/web-console/src/components/query-error-pane/__snapshots__/query-error-pane.spec.tsx.snap
@@ -1,8 +1,8 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
-exports[`QueryError matches snapshot 1`] = `
+exports[`QueryErrorPane matches snapshot 1`] = `
<div
- className="query-error"
+ className="query-error-pane"
>
something went wrong in line 7, column 8.
</div>
diff --git a/web-console/src/components/table-cell-unparseable/table-cell-unparseable.scss b/web-console/src/components/query-error-pane/query-error-pane.scss
similarity index 82%
copy from web-console/src/components/table-cell-unparseable/table-cell-unparseable.scss
copy to web-console/src/components/query-error-pane/query-error-pane.scss
index 43efe5085b..a1c62f2bcb 100644
--- a/web-console/src/components/table-cell-unparseable/table-cell-unparseable.scss
+++ b/web-console/src/components/query-error-pane/query-error-pane.scss
@@ -16,6 +16,13 @@
* limitations under the License.
*/
-.table-cell-unparseable {
- color: #9e2b0e;
+@import '../../blueprint-overrides/common/colors';
+@import '../../variables';
+
+.query-error-pane {
+ padding: 20px 22px;
+
+ .#{$bp-ns}-dark & {
+ background: $dark-gray3;
+ }
}
diff --git a/web-console/src/views/query-view/query-error/query-error.spec.tsx b/web-console/src/components/query-error-pane/query-error-pane.spec.tsx
similarity index 90%
copy from web-console/src/views/query-view/query-error/query-error.spec.tsx
copy to web-console/src/components/query-error-pane/query-error-pane.spec.tsx
index 52ece640aa..b85c2da6cc 100644
--- a/web-console/src/views/query-view/query-error/query-error.spec.tsx
+++ b/web-console/src/components/query-error-pane/query-error-pane.spec.tsx
@@ -19,12 +19,12 @@
import { shallow } from 'enzyme';
import React from 'react';
-import { QueryError } from './query-error';
+import { QueryErrorPane } from './query-error-pane';
-describe('QueryError', () => {
+describe('QueryErrorPane', () => {
it('matches snapshot', () => {
const queryError = shallow(
- <QueryError
+ <QueryErrorPane
error={new Error('something went wrong in line 7, column 8.')}
moveCursorTo={() => {}}
/>,
diff --git a/web-console/src/views/query-view/query-error/query-error.tsx b/web-console/src/components/query-error-pane/query-error-pane.tsx
similarity index 76%
rename from web-console/src/views/query-view/query-error/query-error.tsx
rename to web-console/src/components/query-error-pane/query-error-pane.tsx
index 2e0c8296bd..f2e23172dd 100644
--- a/web-console/src/views/query-view/query-error/query-error.tsx
+++ b/web-console/src/components/query-error-pane/query-error-pane.tsx
@@ -18,24 +18,24 @@
import React, { useState } from 'react';
-import { HighlightText } from '../../../components';
-import { DruidError, RowColumn } from '../../../utils';
+import { DruidError, RowColumn } from '../../utils';
+import { HighlightText } from '../highlight-text/highlight-text';
-import './query-error.scss';
+import './query-error-pane.scss';
-export interface QueryErrorProps {
+export interface QueryErrorPaneProps {
error: DruidError;
moveCursorTo: (rowColumn: RowColumn) => void;
queryString?: string;
onQueryStringChange?: (newQueryString: string, run?: boolean) => void;
}
-export const QueryError = React.memo(function QueryError(props: QueryErrorProps) {
+export const QueryErrorPane = React.memo(function QueryErrorPane(props: QueryErrorPaneProps) {
const { error, moveCursorTo, queryString, onQueryStringChange } = props;
const [showMode, setShowMore] = useState(false);
if (!error.errorMessage) {
- return <div className="query-error">{error.message}</div>;
+ return <div className="query-error-pane">{error.message}</div>;
}
const { position, suggestion } = error;
@@ -46,21 +46,20 @@ export const QueryError = React.memo(function QueryError(props: QueryErrorProps)
suggestionElement = (
<p>
Suggestion:{' '}
- <span
- className="suggestion"
+ <a
onClick={() => {
onQueryStringChange(newQuery, true);
}}
>
{suggestion.label}
- </span>
+ </a>
</p>
);
}
}
return (
- <div className="query-error">
+ <div className="query-error-pane">
{suggestionElement}
{error.error && <p>{`Error: ${error.error}`}</p>}
{error.errorMessageWithoutExpectation && (
@@ -70,14 +69,13 @@ export const QueryError = React.memo(function QueryError(props: QueryErrorProps)
text={error.errorMessageWithoutExpectation}
find={position.match}
replace={
- <span
- className="cursor-link"
+ <a
onClick={() => {
moveCursorTo(position);
}}
>
{position.match}
- </span>
+ </a>
}
/>
) : (
@@ -86,19 +84,14 @@ export const QueryError = React.memo(function QueryError(props: QueryErrorProps)
{error.expectation && !showMode && (
<>
{' '}
- <span className="more-or-less" onClick={() => setShowMore(true)}>
- More...
- </span>
+ <a onClick={() => setShowMore(true)}>More...</a>
</>
)}
</p>
)}
{error.expectation && showMode && (
<p>
- {error.expectation}{' '}
- <span className="more-or-less" onClick={() => setShowMore(false)}>
- Less...
- </span>
+ {error.expectation} <a onClick={() => setShowMore(false)}>Less...</a>
</p>
)}
{error.errorClass && <p>{error.errorClass}</p>}
diff --git a/web-console/src/components/record-table-pane/record-table-pane.scss b/web-console/src/components/record-table-pane/record-table-pane.scss
new file mode 100644
index 0000000000..d00a2e3d32
--- /dev/null
+++ b/web-console/src/components/record-table-pane/record-table-pane.scss
@@ -0,0 +1,113 @@
+/*
+ * 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 '../../variables';
+@import '../../blueprint-overrides/common/colors';
+
+.record-table-pane {
+ position: relative;
+
+ &.more-results .-totalPages {
+ // Hide the total page counter as it can be confusing due to the auto limit
+ display: none;
+ }
+
+ .dead-end {
+ position: absolute;
+ left: 50%;
+ top: 45%;
+ transform: translate(-50%, -50%);
+ width: 350px;
+
+ p {
+ text-align: center;
+ }
+
+ > * {
+ margin-bottom: 10px;
+ }
+ }
+
+ .ReactTable {
+ position: absolute;
+ top: 0;
+ bottom: 0;
+ width: 100%;
+ font-feature-settings: tnum;
+ font-variant-numeric: tabular-nums;
+
+ .rt-thead.-header {
+ box-shadow: 0 1px 0 0 rgba(black, 0.2); // This is a hack! this line is sometimes too weak in tables.
+
+ .rt-th {
+ &.aggregate-header {
+ background: rgba($druid-brand, 0.06);
+ }
+
+ .asc {
+ box-shadow: inset 0 3px 0 0 rgba(255, 255, 255, 0.6);
+ }
+
+ .desc {
+ box-shadow: inset 0 -3px 0 0 rgba(255, 255, 255, 0.6);
+ }
+
+ .#{$bp-ns}-icon {
+ margin-left: 3px;
+ }
+
+ .output-name {
+ overflow: hidden;
+ text-overflow: ellipsis;
+
+ > * {
+ display: inline-block;
+ vertical-align: top;
+ }
+
+ .type-icon {
+ margin-top: 3px;
+ margin-right: 5px;
+ }
+ }
+
+ .formula {
+ font-family: monospace;
+ font-size: 11px;
+ margin-top: 4px;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ }
+ }
+ }
+
+ .rt-td {
+ cursor: pointer;
+ }
+ }
+
+ .clickable-cell {
+ padding: $table-cell-v-padding $table-cell-h-padding;
+ cursor: pointer;
+ width: 100%;
+ }
+
+ .#{$bp-ns}-popover2-target {
+ width: 100%;
+ }
+}
diff --git a/web-console/src/components/record-table-pane/record-table-pane.tsx b/web-console/src/components/record-table-pane/record-table-pane.tsx
new file mode 100644
index 0000000000..699a4424d4
--- /dev/null
+++ b/web-console/src/components/record-table-pane/record-table-pane.tsx
@@ -0,0 +1,282 @@
+/*
+ * 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 { Button, Icon, Menu, MenuItem } from '@blueprintjs/core';
+import { IconName, IconNames } from '@blueprintjs/icons';
+import { Popover2 } from '@blueprintjs/popover2';
+import classNames from 'classnames';
+import {
+ Column,
+ QueryResult,
+ SqlExpression,
+ SqlLiteral,
+ SqlRef,
+ trimString,
+} from 'druid-query-toolkit';
+import React, { useEffect, useState } from 'react';
+import ReactTable from 'react-table';
+
+import { ShowValueDialog } from '../../dialogs/show-value-dialog/show-value-dialog';
+import { SMALL_TABLE_PAGE_SIZE, SMALL_TABLE_PAGE_SIZE_OPTIONS } from '../../react-table';
+import {
+ columnToIcon,
+ columnToWidth,
+ copyAndAlert,
+ filterMap,
+ formatNumber,
+ getNumericColumnBraces,
+ Pagination,
+ prettyPrintSql,
+ stringifyValue,
+} from '../../utils';
+import { BracedText } from '../braced-text/braced-text';
+import { Deferred } from '../deferred/deferred';
+import { TableCell } from '../table-cell/table-cell';
+
+import './record-table-pane.scss';
+
+function sqlLiteralForColumnValue(column: Column, value: unknown): SqlLiteral | undefined {
+ if (column.sqlType === 'TIMESTAMP') {
+ const asDate = new Date(value as any);
+ if (!isNaN(asDate.valueOf())) {
+ return SqlLiteral.create(asDate);
+ }
+ }
+
+ return SqlLiteral.maybe(value);
+}
+
+function isComparable(x: unknown): boolean {
+ return x !== null && x !== '';
+}
+
+export interface RecordTablePaneProps {
+ queryResult: QueryResult;
+ initPageSize?: number;
+ addFilter?(filter: string): void;
+}
+
+export const RecordTablePane = React.memo(function RecordTablePane(props: RecordTablePaneProps) {
+ const { queryResult, initPageSize, addFilter } = props;
+ const parsedQuery = queryResult.sqlQuery;
+ const [pagination, setPagination] = useState<Pagination>({
+ page: 0,
+ pageSize: initPageSize || 20,
+ });
+ const [showValue, setShowValue] = useState<string>();
+
+ // Reset page to 0 if number of results changes
+ useEffect(() => {
+ setPagination(pagination => {
+ return pagination.page ? { ...pagination, page: 0 } : pagination;
+ });
+ }, [queryResult.rows.length]);
+
+ function hasFilterOnHeader(header: string, headerIndex: number): boolean {
+ if (!parsedQuery || !parsedQuery.isRealOutputColumnAtSelectIndex(headerIndex)) return false;
+
+ return (
+ parsedQuery.getEffectiveWhereExpression().containsColumn(header) ||
+ parsedQuery.getEffectiveHavingExpression().containsColumn(header)
+ );
+ }
+
+ function filterOnMenuItem(icon: IconName, clause: SqlExpression) {
+ if (!parsedQuery || !addFilter) return;
+
+ return (
+ <MenuItem
+ icon={icon}
+ text={`Filter on: ${prettyPrintSql(clause)}`}
+ onClick={() => {
+ addFilter(clause.toString());
+ }}
+ />
+ );
+ }
+
+ function actionMenuItem(clause: SqlExpression) {
+ if (!addFilter) return;
+ const prettyLabel = prettyPrintSql(clause);
+ return (
+ <MenuItem
+ icon={IconNames.FILTER}
+ text={`Filter: ${prettyLabel}`}
+ onClick={() => addFilter(clause.toString())}
+ />
+ );
+ }
+
+ function getCellMenu(column: Column, headerIndex: number, value: unknown) {
+ const showFullValueMenuItem = (
+ <MenuItem
+ icon={IconNames.EYE_OPEN}
+ text="Show full value"
+ onClick={() => {
+ setShowValue(stringifyValue(value));
+ }}
+ />
+ );
+
+ const val = sqlLiteralForColumnValue(column, value);
+
+ if (parsedQuery) {
+ let ex: SqlExpression | undefined;
+ if (parsedQuery.hasStarInSelect()) {
+ ex = SqlRef.column(column.name);
+ } else {
+ const selectValue = parsedQuery.getSelectExpressionForIndex(headerIndex);
+ if (selectValue) {
+ ex = selectValue.getUnderlyingExpression();
+ }
+ }
+
+ const jsonColumn = column.nativeType === 'COMPLEX<json>';
+ return (
+ <Menu>
+ {ex && val && !jsonColumn && (
+ <>
+ {filterOnMenuItem(IconNames.FILTER, ex.equal(val))}
+ {filterOnMenuItem(IconNames.FILTER, ex.unequal(val))}
+ {isComparable(value) && (
+ <>
+ {filterOnMenuItem(IconNames.FILTER, ex.greaterThanOrEqual(val))}
+ {filterOnMenuItem(IconNames.FILTER, ex.lessThanOrEqual(val))}
+ </>
+ )}
+ </>
+ )}
+ {showFullValueMenuItem}
+ </Menu>
+ );
+ } else {
+ const ref = SqlRef.column(column.name);
+ const stringValue = stringifyValue(value);
+ const trimmedValue = trimString(stringValue, 50);
+ return (
+ <Menu>
+ <MenuItem
+ icon={IconNames.CLIPBOARD}
+ text={`Copy: ${trimmedValue}`}
+ onClick={() => copyAndAlert(stringValue, `${trimmedValue} copied to clipboard`)}
+ />
+ {val && (
+ <>
+ {actionMenuItem(ref.equal(val))}
+ {actionMenuItem(ref.unequal(val))}
+ </>
+ )}
+ {showFullValueMenuItem}
+ </Menu>
+ );
+ }
+ }
+
+ function getHeaderClassName(header: string) {
+ if (!parsedQuery) return;
+
+ const className = [];
+
+ const orderBy = parsedQuery.getOrderByForOutputColumn(header);
+ if (orderBy) {
+ className.push(orderBy.getEffectiveDirection() === 'DESC' ? '-sort-desc' : '-sort-asc');
+ }
+
+ return className.join(' ');
+ }
+
+ const outerLimit = queryResult.getSqlOuterLimit();
+ const hasMoreResults = queryResult.rows.length === outerLimit;
+ const finalPage =
+ hasMoreResults && Math.floor(queryResult.rows.length / pagination.pageSize) === pagination.page; // on the last page
+
+ const numericColumnBraces = getNumericColumnBraces(queryResult, pagination);
+ return (
+ <div className={classNames('record-table-pane', { 'more-results': hasMoreResults })}>
+ {finalPage ? (
+ <div className="dead-end">
+ <p>This is the end of the inline results but there are more results in this query.</p>
+ <Button
+ icon={IconNames.ARROW_LEFT}
+ text="Go to previous page"
+ fill
+ onClick={() => setPagination({ ...pagination, page: pagination.page - 1 })}
+ />
+ </div>
+ ) : (
+ <ReactTable
+ className="-striped -highlight"
+ data={queryResult.rows as any[][]}
+ ofText={hasMoreResults ? '' : 'of'}
+ noDataText={queryResult.rows.length ? '' : 'Query returned no data'}
+ page={pagination.page}
+ pageSize={pagination.pageSize}
+ onPageChange={page => setPagination({ ...pagination, page })}
+ onPageSizeChange={(pageSize, page) => setPagination({ page, pageSize })}
+ sortable={false}
+ defaultPageSize={SMALL_TABLE_PAGE_SIZE}
+ pageSizeOptions={SMALL_TABLE_PAGE_SIZE_OPTIONS}
+ showPagination={
+ queryResult.rows.length > Math.min(SMALL_TABLE_PAGE_SIZE, pagination.pageSize)
+ }
+ columns={filterMap(queryResult.header, (column, i) => {
+ const h = column.name;
+ const icon = columnToIcon(column);
+
+ return {
+ Header() {
+ return (
+ <div className="clickable-cell">
+ <div className="output-name">
+ {icon && <Icon className="type-icon" icon={icon} size={12} />}
+ {h}
+ {hasFilterOnHeader(h, i) && <Icon icon={IconNames.FILTER} size={14} />}
+ </div>
+ </div>
+ );
+ },
+ headerClassName: getHeaderClassName(h),
+ accessor: String(i),
+ Cell(row) {
+ const value = row.value;
+ return (
+ <div>
+ <Popover2 content={<Deferred content={() => getCellMenu(column, i, value)} />}>
+ {numericColumnBraces[i] ? (
+ <BracedText
+ className="table-padding"
+ text={formatNumber(value)}
+ braces={numericColumnBraces[i]}
+ padFractionalPart
+ />
+ ) : (
+ <TableCell value={value} unlimited />
+ )}
+ </Popover2>
+ </div>
+ );
+ },
+ width: columnToWidth(column),
+ };
+ })}
+ />
+ )}
+ {showValue && <ShowValueDialog onClose={() => setShowValue(undefined)} str={showValue} />}
+ </div>
+ );
+});
diff --git a/web-console/src/components/show-json/show-json.tsx b/web-console/src/components/show-json/show-json.tsx
index c840c95e24..2986febc4a 100644
--- a/web-console/src/components/show-json/show-json.tsx
+++ b/web-console/src/components/show-json/show-json.tsx
@@ -87,7 +87,7 @@ export const ShowJson = React.memo(function ShowJson(props: ShowJsonProps) {
) : (
<AceEditor
mode="hjson"
- theme="tomorrow"
+ theme="solarized_dark"
readOnly
fontSize={12}
width="100%"
diff --git a/web-console/src/components/table-cell-unparseable/table-cell-unparseable.scss b/web-console/src/components/table-cell-unparseable/table-cell-unparseable.scss
index 43efe5085b..9819fb4b1f 100644
--- a/web-console/src/components/table-cell-unparseable/table-cell-unparseable.scss
+++ b/web-console/src/components/table-cell-unparseable/table-cell-unparseable.scss
@@ -16,6 +16,9 @@
* limitations under the License.
*/
+@import '../../variables';
+
.table-cell-unparseable {
+ padding: $table-cell-v-padding $table-cell-h-padding;
color: #9e2b0e;
}
diff --git a/web-console/src/components/table-clickable-cell/table-clickable-cell.tsx b/web-console/src/components/table-clickable-cell/table-clickable-cell.tsx
index e2e79b1fcd..cc8cfd71e5 100644
--- a/web-console/src/components/table-clickable-cell/table-clickable-cell.tsx
+++ b/web-console/src/components/table-clickable-cell/table-clickable-cell.tsx
@@ -17,23 +17,26 @@
*/
import { Icon, IconName } from '@blueprintjs/core';
+import classNames from 'classnames';
import React, { MouseEventHandler, ReactNode } from 'react';
import './table-clickable-cell.scss';
export interface TableClickableCellProps {
+ className?: string;
onClick: MouseEventHandler<any>;
hoverIcon?: IconName;
+ title?: string;
children?: ReactNode;
}
export const TableClickableCell = React.memo(function TableClickableCell(
props: TableClickableCellProps,
) {
- const { onClick, hoverIcon, children } = props;
+ const { className, onClick, hoverIcon, title, children } = props;
return (
- <div className="table-clickable-cell" onClick={onClick}>
+ <div className={classNames('table-clickable-cell', className)} title={title} onClick={onClick}>
{children}
{hoverIcon && <Icon className="hover-icon" icon={hoverIcon} />}
</div>
diff --git a/web-console/src/console-application.tsx b/web-console/src/console-application.tsx
index 731002632c..d5dc61cebc 100644
--- a/web-console/src/console-application.tsx
+++ b/web-console/src/console-application.tsx
@@ -20,9 +20,11 @@ import { HotkeysProvider, Intent } from '@blueprintjs/core';
import { IconNames } from '@blueprintjs/icons';
import classNames from 'classnames';
import React from 'react';
+import { RouteComponentProps } from 'react-router';
import { HashRouter, Route, Switch } from 'react-router-dom';
import { HeaderActiveTab, HeaderBar, Loader } from './components';
+import { DruidEngine, QueryWithContext } from './druid-models';
import { AppToaster } from './singletons';
import { Capabilities, QueryManager } from './utils';
import {
@@ -34,6 +36,8 @@ import {
QueryView,
SegmentsView,
ServicesView,
+ SqlDataLoaderView,
+ WorkbenchView,
} from './views';
import './console-application.scss';
@@ -75,7 +79,7 @@ export class ConsoleApplication extends React.PureComponent<
private openDialog?: string;
private datasource?: string;
private onlyUnavailable?: boolean;
- private initQuery?: string;
+ private queryWithContext?: QueryWithContext;
constructor(props: ConsoleApplicationProps, context: any) {
super(props, context);
@@ -119,14 +123,19 @@ export class ConsoleApplication extends React.PureComponent<
this.openDialog = undefined;
this.datasource = undefined;
this.onlyUnavailable = undefined;
- this.initQuery = undefined;
+ this.queryWithContext = undefined;
}, 50);
}
- private readonly goToLoadData = (supervisorId?: string, taskId?: string) => {
- if (taskId) this.taskId = taskId;
+ private readonly goToStreamingDataLoader = (supervisorId?: string) => {
if (supervisorId) this.supervisorId = supervisorId;
- window.location.hash = 'load-data';
+ window.location.hash = 'streaming-data-loader';
+ this.resetInitialsWithDelay();
+ };
+
+ private readonly goToClassicBatchDataLoader = (taskId?: string) => {
+ if (taskId) this.taskId = taskId;
+ window.location.hash = 'classic-batch-data-loader';
this.resetInitialsWithDelay();
};
@@ -143,6 +152,12 @@ export class ConsoleApplication extends React.PureComponent<
this.resetInitialsWithDelay();
};
+ private readonly goToIngestionWithTaskId = (taskId?: string) => {
+ this.taskId = taskId;
+ window.location.hash = 'ingestion';
+ this.resetInitialsWithDelay();
+ };
+
private readonly goToIngestionWithTaskGroupId = (taskGroupId?: string, openDialog?: string) => {
this.taskGroupId = taskGroupId;
if (openDialog) this.openDialog = openDialog;
@@ -157,9 +172,9 @@ export class ConsoleApplication extends React.PureComponent<
this.resetInitialsWithDelay();
};
- private readonly goToQuery = (initQuery: string) => {
- this.initQuery = initQuery;
- window.location.hash = 'query';
+ private readonly goToQuery = (queryWithContext: QueryWithContext) => {
+ this.queryWithContext = queryWithContext;
+ window.location.hash = 'workbench';
this.resetInitialsWithDelay();
};
@@ -187,13 +202,41 @@ export class ConsoleApplication extends React.PureComponent<
return this.wrapInViewContainer(null, <HomeView capabilities={capabilities} />);
};
- private readonly wrappedLoadDataView = () => {
+ private readonly wrappedDataLoaderView = () => {
const { exampleManifestsUrl } = this.props;
return this.wrapInViewContainer(
- 'load-data',
+ 'data-loader',
+ <LoadDataView
+ mode="all"
+ initTaskId={this.taskId}
+ initSupervisorId={this.supervisorId}
+ exampleManifestsUrl={exampleManifestsUrl}
+ goToIngestion={this.goToIngestionWithTaskGroupId}
+ />,
+ 'narrow-pad',
+ );
+ };
+
+ private readonly wrappedStreamingDataLoaderView = () => {
+ return this.wrapInViewContainer(
+ 'streaming-data-loader',
<LoadDataView
+ mode="streaming"
initSupervisorId={this.supervisorId}
+ goToIngestion={this.goToIngestionWithTaskGroupId}
+ />,
+ 'narrow-pad',
+ );
+ };
+
+ private readonly wrappedClassicBatchDataLoaderView = () => {
+ const { exampleManifestsUrl } = this.props;
+
+ return this.wrapInViewContainer(
+ 'classic-batch-data-loader',
+ <LoadDataView
+ mode="batch"
initTaskId={this.taskId}
exampleManifestsUrl={exampleManifestsUrl}
goToIngestion={this.goToIngestionWithTaskGroupId}
@@ -208,7 +251,7 @@ export class ConsoleApplication extends React.PureComponent<
return this.wrapInViewContainer(
'query',
<QueryView
- initQuery={this.initQuery}
+ initQuery={this.queryWithContext?.queryString}
defaultQueryContext={defaultQueryContext}
mandatoryQueryContext={mandatoryQueryContext}
/>,
@@ -216,6 +259,43 @@ export class ConsoleApplication extends React.PureComponent<
);
};
+ private readonly wrappedWorkbenchView = (p: RouteComponentProps<any>) => {
+ const { defaultQueryContext, mandatoryQueryContext } = this.props;
+ const { capabilities } = this.state;
+
+ const queryEngines: DruidEngine[] = ['native'];
+ if (capabilities.hasSql()) {
+ queryEngines.push('sql-native');
+ }
+ if (capabilities.hasMultiStageQuery()) {
+ queryEngines.push('sql-msq-task');
+ }
+
+ return this.wrapInViewContainer(
+ 'workbench',
+ <WorkbenchView
+ tabId={p.match.params.tabId}
+ onTabChange={newTabId => {
+ location.hash = `#workbench/${newTabId}`;
+ }}
+ initQueryWithContext={this.queryWithContext}
+ defaultQueryContext={defaultQueryContext}
+ mandatoryQueryContext={mandatoryQueryContext}
+ queryEngines={queryEngines}
+ allowExplain
+ goToIngestion={this.goToIngestionWithTaskId}
+ />,
+ 'thin',
+ );
+ };
+
+ private readonly wrappedSqlDataLoaderView = () => {
+ return this.wrapInViewContainer(
+ 'sql-data-loader',
+ <SqlDataLoaderView goToQuery={this.goToQuery} goToIngestion={this.goToIngestionWithTaskId} />,
+ );
+ };
+
private readonly wrappedDatasourcesView = () => {
const { capabilities } = this.state;
return this.wrapInViewContainer(
@@ -248,12 +328,14 @@ export class ConsoleApplication extends React.PureComponent<
return this.wrapInViewContainer(
'ingestion',
<IngestionView
+ taskId={this.taskId}
taskGroupId={this.taskGroupId}
datasourceId={this.datasource}
openDialog={this.openDialog}
goToDatasource={this.goToDatasources}
goToQuery={this.goToQuery}
- goToLoadData={this.goToLoadData}
+ goToStreamingDataLoader={this.goToStreamingDataLoader}
+ goToClassicBatchDataLoader={this.goToClassicBatchDataLoader}
capabilities={capabilities}
/>,
);
@@ -263,11 +345,7 @@ export class ConsoleApplication extends React.PureComponent<
const { capabilities } = this.state;
return this.wrapInViewContainer(
'services',
- <ServicesView
- goToQuery={this.goToQuery}
- goToTask={this.goToIngestionWithTaskGroupId}
- capabilities={capabilities}
- />,
+ <ServicesView goToQuery={this.goToQuery} capabilities={capabilities} />,
);
};
@@ -291,7 +369,15 @@ export class ConsoleApplication extends React.PureComponent<
<HashRouter hashType="noslash">
<div className="console-application">
<Switch>
- <Route path="/load-data" component={this.wrappedLoadDataView} />
+ <Route path="/data-loader" component={this.wrappedDataLoaderView} />
+ <Route
+ path="/streaming-data-loader"
+ component={this.wrappedStreamingDataLoaderView}
+ />
+ <Route
+ path="/classic-batch-data-loader"
+ component={this.wrappedClassicBatchDataLoaderView}
+ />
<Route path="/ingestion" component={this.wrappedIngestionView} />
<Route path="/datasources" component={this.wrappedDatasourcesView} />
@@ -299,6 +385,11 @@ export class ConsoleApplication extends React.PureComponent<
<Route path="/services" component={this.wrappedServicesView} />
<Route path="/query" component={this.wrappedQueryView} />
+ <Route
+ path={['/workbench/:tabId', '/workbench']}
+ component={this.wrappedWorkbenchView}
+ />
+ <Route path="/sql-data-loader" component={this.wrappedSqlDataLoaderView} />
<Route path="/lookups" component={this.wrappedLookupsView} />
<Route component={this.wrappedHomeView} />
diff --git a/web-console/src/dialogs/async-action-dialog/async-action-dialog.tsx b/web-console/src/dialogs/async-action-dialog/async-action-dialog.tsx
index ada4620e58..f892936bab 100644
--- a/web-console/src/dialogs/async-action-dialog/async-action-dialog.tsx
+++ b/web-console/src/dialogs/async-action-dialog/async-action-dialog.tsx
@@ -30,7 +30,7 @@ import { IconNames } from '@blueprintjs/icons';
import classNames from 'classnames';
import React, { ReactNode, useState } from 'react';
-import { WarningChecklist } from '../../components/warning-checklist/warning-checklist';
+import { WarningChecklist } from '../../components';
import { AppToaster } from '../../singletons';
import './async-action-dialog.scss';
diff --git a/web-console/src/dialogs/compaction-dialog/__snapshots__/compaction-dialog.spec.tsx.snap b/web-console/src/dialogs/compaction-dialog/__snapshots__/compaction-dialog.spec.tsx.snap
index 888ee17ef3..c632071fe5 100644
--- a/web-console/src/dialogs/compaction-dialog/__snapshots__/compaction-dialog.spec.tsx.snap
+++ b/web-console/src/dialogs/compaction-dialog/__snapshots__/compaction-dialog.spec.tsx.snap
@@ -280,6 +280,20 @@ exports[`CompactionDialog matches snapshot with compactionConfig (dynamic partit
"name": "tuningConfig.maxNumConcurrentSubTasks",
"type": "number",
},
+ Object {
+ "defaultValue": -1,
+ "info": <React.Fragment>
+ <p>
+ Limit of the number of segments to merge in a single phase when merging segments for publishing. This limit affects the total number of columns present in a set of segments to merge. If the limit is exceeded, segment merging occurs in multiple phases. Druid merges at least 2 segments per phase, regardless of this setting.
+ </p>
+ <p>
+ Default: -1 (unlimited)
+ </p>
+ </React.Fragment>,
+ "min": -1,
+ "name": "tuningConfig.maxColumnsToMerge",
+ "type": "number",
+ },
Object {
"defaultValue": 10,
"defined": [Function],
@@ -642,6 +656,20 @@ exports[`CompactionDialog matches snapshot with compactionConfig (hashed partiti
"name": "tuningConfig.maxNumConcurrentSubTasks",
"type": "number",
},
+ Object {
+ "defaultValue": -1,
+ "info": <React.Fragment>
+ <p>
+ Limit of the number of segments to merge in a single phase when merging segments for publishing. This limit affects the total number of columns present in a set of segments to merge. If the limit is exceeded, segment merging occurs in multiple phases. Druid merges at least 2 segments per phase, regardless of this setting.
+ </p>
+ <p>
+ Default: -1 (unlimited)
+ </p>
+ </React.Fragment>,
+ "min": -1,
+ "name": "tuningConfig.maxColumnsToMerge",
+ "type": "number",
+ },
Object {
"defaultValue": 10,
"defined": [Function],
@@ -1004,6 +1032,20 @@ exports[`CompactionDialog matches snapshot with compactionConfig (range partitio
"name": "tuningConfig.maxNumConcurrentSubTasks",
"type": "number",
},
+ Object {
+ "defaultValue": -1,
+ "info": <React.Fragment>
+ <p>
+ Limit of the number of segments to merge in a single phase when merging segments for publishing. This limit affects the total number of columns present in a set of segments to merge. If the limit is exceeded, segment merging occurs in multiple phases. Druid merges at least 2 segments per phase, regardless of this setting.
+ </p>
+ <p>
+ Default: -1 (unlimited)
+ </p>
+ </React.Fragment>,
+ "min": -1,
+ "name": "tuningConfig.maxColumnsToMerge",
+ "type": "number",
+ },
Object {
"defaultValue": 10,
"defined": [Function],
@@ -1366,6 +1408,20 @@ exports[`CompactionDialog matches snapshot without compactionConfig 1`] = `
"name": "tuningConfig.maxNumConcurrentSubTasks",
"type": "number",
},
+ Object {
+ "defaultValue": -1,
+ "info": <React.Fragment>
+ <p>
+ Limit of the number of segments to merge in a single phase when merging segments for publishing. This limit affects the total number of columns present in a set of segments to merge. If the limit is exceeded, segment merging occurs in multiple phases. Druid merges at least 2 segments per phase, regardless of this setting.
+ </p>
+ <p>
+ Default: -1 (unlimited)
+ </p>
+ </React.Fragment>,
+ "min": -1,
+ "name": "tuningConfig.maxColumnsToMerge",
+ "type": "number",
+ },
Object {
"defaultValue": 10,
"defined": [Function],
diff --git a/web-console/src/dialogs/coordinator-dynamic-config-dialog/__snapshots__/coordinator-dynamic-config-dialog.spec.tsx.snap b/web-console/src/dialogs/coordinator-dynamic-config-dialog/__snapshots__/coordinator-dynamic-config-dialog.spec.tsx.snap
index e889ab8bde..3de1970e2e 100644
--- a/web-console/src/dialogs/coordinator-dynamic-config-dialog/__snapshots__/coordinator-dynamic-config-dialog.spec.tsx.snap
+++ b/web-console/src/dialogs/coordinator-dynamic-config-dialog/__snapshots__/coordinator-dynamic-config-dialog.spec.tsx.snap
@@ -12,7 +12,7 @@ exports[`CoordinatorDynamicConfigDialog matches snapshot 1`] = `
Edit the coordinator dynamic configuration on the fly. For more information please refer to the
<Memo(ExternalLink)
- href="https://druid.apache.org/docs/0.23.0/configuration/index.html#dynamic-configuration"
+ href="https://druid.apache.org/docs/latest/configuration/index.html#dynamic-configuration"
>
documentation
</Memo(ExternalLink)>
diff --git a/web-console/src/dialogs/datasource-table-action-dialog/__snapshots__/datasource-table-action-dialog.spec.tsx.snap b/web-console/src/dialogs/datasource-table-action-dialog/__snapshots__/datasource-table-action-dialog.spec.tsx.snap
index 4934b0e707..eb5ffe755a 100644
--- a/web-console/src/dialogs/datasource-table-action-dialog/__snapshots__/datasource-table-action-dialog.spec.tsx.snap
+++ b/web-console/src/dialogs/datasource-table-action-dialog/__snapshots__/datasource-table-action-dialog.spec.tsx.snap
@@ -65,6 +65,33 @@ exports[`Datasource table action dialog matches snapshot 1`] = `
<button
class="bp4-button bp4-intent-primary tab-button"
type="button"
+ >
+ <span
+ aria-hidden="true"
+ class="bp4-icon bp4-icon-th"
+ icon="th"
+ >
+ <svg
+ data-icon="th"
+ height="20"
+ viewBox="0 0 20 20"
+ width="20"
+ >
+ <path
+ d="M19 1H1c-.6 0-1 .5-1 1v16c0 .5.4 1 1 1h18c.5 0 1-.5 1-1V2c0-.5-.5-1-1-1zM7 17H2v-3h5v3zm0-4H2v-3h5v3zm0-4H2V6h5v3zm11 8H8v-3h10v3zm0-4H8v-3h10v3zm0-4H8V6h10v3z"
+ fill-rule="evenodd"
+ />
+ </svg>
+ </span>
+ <span
+ class="bp4-button-text"
+ >
+ Records
+ </span>
+ </button>
+ <button
+ class="bp4-button bp4-minimal tab-button"
+ type="button"
>
<span
aria-hidden="true"
@@ -94,43 +121,39 @@ exports[`Datasource table action dialog matches snapshot 1`] = `
class="main-section"
>
<div
- class="datasource-columns-table"
+ class="datasource-preview-pane"
>
<div
- class="main-area"
+ class="loader"
>
<div
- class="loader"
+ class="loader-logo"
>
- <div
- class="loader-logo"
+ <svg
+ viewBox="0 0 100 100"
>
- <svg
- viewBox="0 0 100 100"
- >
- <path
- class="one"
- d="M54.2,69.8h-2.7c-0.7,0-1.3-0.6-1.3-1.3c0-0.7,0.6-1.3,1.3-1.3h2.7c11.5,0,23.8-7.4,23.8-23.7
+ <path
+ class="one"
+ d="M54.2,69.8h-2.7c-0.7,0-1.3-0.6-1.3-1.3c0-0.7,0.6-1.3,1.3-1.3h2.7c11.5,0,23.8-7.4,23.8-23.7
c0-9.1-6.9-15.8-16.4-15.8H38c-0.7,0-1.3-0.6-1.3-1.3s0.6-1.3,1.3-1.3h23.6c5.3,0,10.1,1.9,13.6,5.3c3.5,3.4,5.4,8,5.4,13.1
c0,6.6-2.3,13-6.3,17.7C69.5,66.8,62.5,69.8,54.2,69.8z"
- />
- <path
- class="two"
- d="M55.7,59.5h-26c-0.7,0-1.3-0.6-1.3-1.3c0-0.7,0.6-1.3,1.3-1.3h26c7.5,0,11.5-5.8,11.5-11.5
+ />
+ <path
+ class="two"
+ d="M55.7,59.5h-26c-0.7,0-1.3-0.6-1.3-1.3c0-0.7,0.6-1.3,1.3-1.3h26c7.5,0,11.5-5.8,11.5-11.5
c0-4.2-3.2-7.3-7.7-7.3h-26c-0.7,0-1.3-0.6-1.3-1.3s0.6-1.3,1.3-1.3h26c5.9,0,10.3,4.3,10.3,9.9c0,3.7-1.3,7.2-3.7,9.8
C63.5,58,59.9,59.5,55.7,59.5z"
- />
- <path
- class="three"
- d="M27.2,38h-6.3c-0.7,0-1.3-0.6-1.3-1.3s0.6-1.3,1.3-1.3h6.3c0.7,0,1.3,0.6,1.3,1.3S27.9,38,27.2,38z"
- />
- <path
- class="four"
- d="M45.1,69.8h-5.8c-0.7,0-1.3-0.6-1.3-1.3c0-0.7,0.6-1.3,1.3-1.3h5.8c0.7,0,1.3,0.6,1.3,1.3
+ />
+ <path
+ class="three"
+ d="M27.2,38h-6.3c-0.7,0-1.3-0.6-1.3-1.3s0.6-1.3,1.3-1.3h6.3c0.7,0,1.3,0.6,1.3,1.3S27.9,38,27.2,38z"
+ />
+ <path
+ class="four"
+ d="M45.1,69.8h-5.8c-0.7,0-1.3-0.6-1.3-1.3c0-0.7,0.6-1.3,1.3-1.3h5.8c0.7,0,1.3,0.6,1.3,1.3
C46.4,69.2,45.8,69.8,45.1,69.8z"
- />
- </svg>
- </div>
+ />
+ </svg>
</div>
</div>
</div>
diff --git a/web-console/src/dialogs/datasource-table-action-dialog/datasource-columns-table/__snapshots__/datasource-columns-table.spec.tsx.snap b/web-console/src/dialogs/datasource-table-action-dialog/datasource-columns-table/__snapshots__/datasource-columns-table.spec.tsx.snap
new file mode 100644
index 0000000000..4d0c223ece
--- /dev/null
+++ b/web-console/src/dialogs/datasource-table-action-dialog/datasource-columns-table/__snapshots__/datasource-columns-table.spec.tsx.snap
@@ -0,0 +1,648 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`DatasourceColumnsTable matches snapshot on error 1`] = `
+<div
+ className="datasource-columns-table"
+>
+ <ReactTable
+ AggregatedComponent={[Function]}
+ ExpanderComponent={[Function]}
+ FilterComponent={[Function]}
+ LoadingComponent={[Function]}
+ NoDataComponent={[Function]}
+ PadRowComponent={[Function]}
+ PaginationComponent={[Function]}
+ PivotValueComponent={[Function]}
+ ResizerComponent={[Function]}
+ TableComponent={[Function]}
+ TbodyComponent={[Function]}
+ TdComponent={[Function]}
+ TfootComponent={[Function]}
+ ThComponent={[Function]}
+ TheadComponent={[Function]}
+ TrComponent={[Function]}
+ TrGroupComponent={[Function]}
+ aggregatedKey="_aggregated"
+ className=""
+ collapseOnDataChange={true}
+ collapseOnPageChange={true}
+ collapseOnSortingChange={true}
+ column={
+ Object {
+ "Aggregated": undefined,
+ "Cell": undefined,
+ "Expander": undefined,
+ "Filter": undefined,
+ "Footer": undefined,
+ "Header": undefined,
+ "Pivot": undefined,
+ "PivotValue": undefined,
+ "Placeholder": undefined,
+ "aggregate": undefined,
+ "className": "",
+ "filterAll": false,
+ "filterMethod": undefined,
+ "filterable": undefined,
+ "footerClassName": "",
+ "footerStyle": Object {},
+ "getFooterProps": [Function],
+ "getHeaderProps": [Function],
+ "getProps": [Function],
+ "headerClassName": "",
+ "headerStyle": Object {},
+ "minResizeWidth": 11,
+ "minWidth": 100,
+ "resizable": undefined,
+ "show": true,
+ "sortMethod": undefined,
+ "sortable": undefined,
+ "style": Object {},
+ }
+ }
+ columns={
+ Array [
+ Object {
+ "Header": "Column name",
+ "accessor": "COLUMN_NAME",
+ "className": "padded",
+ "width": 300,
+ },
+ Object {
+ "Header": "Data type",
+ "accessor": "DATA_TYPE",
+ "className": "padded",
+ "width": 200,
+ },
+ ]
+ }
+ data={Array []}
+ defaultExpanded={Object {}}
+ defaultFilterMethod={[Function]}
+ defaultFiltered={Array []}
+ defaultPage={0}
+ defaultPageSize={25}
+ defaultResized={Array []}
+ defaultSortDesc={false}
+ defaultSortMethod={[Function]}
+ defaultSorted={Array []}
+ expanderDefaults={
+ Object {
+ "filterable": false,
+ "resizable": false,
+ "sortable": false,
+ "width": 35,
+ }
+ }
+ filterable={true}
+ freezeWhenExpanded={false}
+ getLoadingProps={[Function]}
+ getNoDataProps={[Function]}
+ getPaginationProps={[Function]}
+ getProps={[Function]}
+ getResizerProps={[Function]}
+ getTableProps={[Function]}
+ getTbodyProps={[Function]}
+ getTdProps={[Function]}
+ getTfootProps={[Function]}
+ getTfootTdProps={[Function]}
+ getTfootTrProps={[Function]}
+ getTheadFilterProps={[Function]}
+ getTheadFilterThProps={[Function]}
+ getTheadFilterTrProps={[Function]}
+ getTheadGroupProps={[Function]}
+ getTheadGroupThProps={[Function]}
+ getTheadGroupTrProps={[Function]}
+ getTheadProps={[Function]}
+ getTheadThProps={[Function]}
+ getTheadTrProps={[Function]}
+ getTrGroupProps={[Function]}
+ getTrProps={[Function]}
+ groupedByPivotKey="_groupedByPivot"
+ indexKey="_index"
+ loading={false}
+ loadingText="Loading..."
+ multiSort={true}
+ nestingLevelKey="_nestingLevel"
+ nextText="Next"
+ noDataText="test error"
+ ofText="of"
+ onFetchData={[Function]}
+ originalKey="_original"
+ pageJumpText="jump to page"
+ pageSizeOptions={
+ Array [
+ 25,
+ 50,
+ 100,
+ ]
+ }
+ pageText="Page"
+ pivotDefaults={Object {}}
+ pivotIDKey="_pivotID"
+ pivotValKey="_pivotVal"
+ previousText="Previous"
+ resizable={true}
+ resolveData={[Function]}
+ rowsSelectorText="rows per page"
+ rowsText="rows"
+ showPageJump={true}
+ showPageSizeOptions={true}
+ showPagination={false}
+ showPaginationBottom={true}
+ showPaginationTop={false}
+ sortable={true}
+ style={Object {}}
+ subRowsKey="_subRows"
+ />
+</div>
+`;
+
+exports[`DatasourceColumnsTable matches snapshot on init 1`] = `
+<div
+ className="datasource-columns-table"
+>
+ <ReactTable
+ AggregatedComponent={[Function]}
+ ExpanderComponent={[Function]}
+ FilterComponent={[Function]}
+ LoadingComponent={[Function]}
+ NoDataComponent={[Function]}
+ PadRowComponent={[Function]}
+ PaginationComponent={[Function]}
+ PivotValueComponent={[Function]}
+ ResizerComponent={[Function]}
+ TableComponent={[Function]}
+ TbodyComponent={[Function]}
+ TdComponent={[Function]}
+ TfootComponent={[Function]}
+ ThComponent={[Function]}
+ TheadComponent={[Function]}
+ TrComponent={[Function]}
+ TrGroupComponent={[Function]}
+ aggregatedKey="_aggregated"
+ className=""
+ collapseOnDataChange={true}
+ collapseOnPageChange={true}
+ collapseOnSortingChange={true}
+ column={
+ Object {
+ "Aggregated": undefined,
+ "Cell": undefined,
+ "Expander": undefined,
+ "Filter": undefined,
+ "Footer": undefined,
+ "Header": undefined,
+ "Pivot": undefined,
+ "PivotValue": undefined,
+ "Placeholder": undefined,
+ "aggregate": undefined,
+ "className": "",
+ "filterAll": false,
+ "filterMethod": undefined,
+ "filterable": undefined,
+ "footerClassName": "",
+ "footerStyle": Object {},
+ "getFooterProps": [Function],
+ "getHeaderProps": [Function],
+ "getProps": [Function],
+ "headerClassName": "",
+ "headerStyle": Object {},
+ "minResizeWidth": 11,
+ "minWidth": 100,
+ "resizable": undefined,
+ "show": true,
+ "sortMethod": undefined,
+ "sortable": undefined,
+ "style": Object {},
+ }
+ }
+ columns={
+ Array [
+ Object {
+ "Header": "Column name",
+ "accessor": "COLUMN_NAME",
+ "className": "padded",
+ "width": 300,
+ },
+ Object {
+ "Header": "Data type",
+ "accessor": "DATA_TYPE",
+ "className": "padded",
+ "width": 200,
+ },
+ ]
+ }
+ data={Array []}
+ defaultExpanded={Object {}}
+ defaultFilterMethod={[Function]}
+ defaultFiltered={Array []}
+ defaultPage={0}
+ defaultPageSize={25}
+ defaultResized={Array []}
+ defaultSortDesc={false}
+ defaultSortMethod={[Function]}
+ defaultSorted={Array []}
+ expanderDefaults={
+ Object {
+ "filterable": false,
+ "resizable": false,
+ "sortable": false,
+ "width": 35,
+ }
+ }
+ filterable={true}
+ freezeWhenExpanded={false}
+ getLoadingProps={[Function]}
+ getNoDataProps={[Function]}
+ getPaginationProps={[Function]}
+ getProps={[Function]}
+ getResizerProps={[Function]}
+ getTableProps={[Function]}
+ getTbodyProps={[Function]}
+ getTdProps={[Function]}
+ getTfootProps={[Function]}
+ getTfootTdProps={[Function]}
+ getTfootTrProps={[Function]}
+ getTheadFilterProps={[Function]}
+ getTheadFilterThProps={[Function]}
+ getTheadFilterTrProps={[Function]}
+ getTheadGroupProps={[Function]}
+ getTheadGroupThProps={[Function]}
+ getTheadGroupTrProps={[Function]}
+ getTheadProps={[Function]}
+ getTheadThProps={[Function]}
+ getTheadTrProps={[Function]}
+ getTrGroupProps={[Function]}
+ getTrProps={[Function]}
+ groupedByPivotKey="_groupedByPivot"
+ indexKey="_index"
+ loading={false}
+ loadingText="Loading..."
+ multiSort={true}
+ nestingLevelKey="_nestingLevel"
+ nextText="Next"
+ noDataText="No column data found"
+ ofText="of"
+ onFetchData={[Function]}
+ originalKey="_original"
+ pageJumpText="jump to page"
+ pageSizeOptions={
+ Array [
+ 25,
+ 50,
+ 100,
+ ]
+ }
+ pageText="Page"
+ pivotDefaults={Object {}}
+ pivotIDKey="_pivotID"
+ pivotValKey="_pivotVal"
+ previousText="Previous"
+ resizable={true}
+ resolveData={[Function]}
+ rowsSelectorText="rows per page"
+ rowsText="rows"
+ showPageJump={true}
+ showPageSizeOptions={true}
+ showPagination={false}
+ showPaginationBottom={true}
+ showPaginationTop={false}
+ sortable={true}
+ style={Object {}}
+ subRowsKey="_subRows"
+ />
+</div>
+`;
+
+exports[`DatasourceColumnsTable matches snapshot on loading 1`] = `
+<div
+ className="datasource-columns-table"
+>
+ <Memo(Loader) />
+</div>
+`;
+
+exports[`DatasourceColumnsTable matches snapshot on no data 1`] = `
+<div
+ className="datasource-columns-table"
+>
+ <ReactTable
+ AggregatedComponent={[Function]}
+ ExpanderComponent={[Function]}
+ FilterComponent={[Function]}
+ LoadingComponent={[Function]}
+ NoDataComponent={[Function]}
+ PadRowComponent={[Function]}
+ PaginationComponent={[Function]}
+ PivotValueComponent={[Function]}
+ ResizerComponent={[Function]}
+ TableComponent={[Function]}
+ TbodyComponent={[Function]}
+ TdComponent={[Function]}
+ TfootComponent={[Function]}
+ ThComponent={[Function]}
+ TheadComponent={[Function]}
+ TrComponent={[Function]}
+ TrGroupComponent={[Function]}
+ aggregatedKey="_aggregated"
+ className=""
+ collapseOnDataChange={true}
+ collapseOnPageChange={true}
+ collapseOnSortingChange={true}
+ column={
+ Object {
+ "Aggregated": undefined,
+ "Cell": undefined,
+ "Expander": undefined,
+ "Filter": undefined,
+ "Footer": undefined,
+ "Header": undefined,
+ "Pivot": undefined,
+ "PivotValue": undefined,
+ "Placeholder": undefined,
+ "aggregate": undefined,
+ "className": "",
+ "filterAll": false,
+ "filterMethod": undefined,
+ "filterable": undefined,
+ "footerClassName": "",
+ "footerStyle": Object {},
+ "getFooterProps": [Function],
+ "getHeaderProps": [Function],
+ "getProps": [Function],
+ "headerClassName": "",
+ "headerStyle": Object {},
+ "minResizeWidth": 11,
+ "minWidth": 100,
+ "resizable": undefined,
+ "show": true,
+ "sortMethod": undefined,
+ "sortable": undefined,
+ "style": Object {},
+ }
+ }
+ columns={
+ Array [
+ Object {
+ "Header": "Column name",
+ "accessor": "COLUMN_NAME",
+ "className": "padded",
+ "width": 300,
+ },
+ Object {
+ "Header": "Data type",
+ "accessor": "DATA_TYPE",
+ "className": "padded",
+ "width": 200,
+ },
+ ]
+ }
+ data={Array []}
+ defaultExpanded={Object {}}
+ defaultFilterMethod={[Function]}
+ defaultFiltered={Array []}
+ defaultPage={0}
+ defaultPageSize={25}
+ defaultResized={Array []}
+ defaultSortDesc={false}
+ defaultSortMethod={[Function]}
+ defaultSorted={Array []}
+ expanderDefaults={
+ Object {
+ "filterable": false,
+ "resizable": false,
+ "sortable": false,
+ "width": 35,
+ }
+ }
+ filterable={true}
+ freezeWhenExpanded={false}
+ getLoadingProps={[Function]}
+ getNoDataProps={[Function]}
+ getPaginationProps={[Function]}
+ getProps={[Function]}
+ getResizerProps={[Function]}
+ getTableProps={[Function]}
+ getTbodyProps={[Function]}
+ getTdProps={[Function]}
+ getTfootProps={[Function]}
+ getTfootTdProps={[Function]}
+ getTfootTrProps={[Function]}
+ getTheadFilterProps={[Function]}
+ getTheadFilterThProps={[Function]}
+ getTheadFilterTrProps={[Function]}
+ getTheadGroupProps={[Function]}
+ getTheadGroupThProps={[Function]}
+ getTheadGroupTrProps={[Function]}
+ getTheadProps={[Function]}
+ getTheadThProps={[Function]}
+ getTheadTrProps={[Function]}
+ getTrGroupProps={[Function]}
+ getTrProps={[Function]}
+ groupedByPivotKey="_groupedByPivot"
+ indexKey="_index"
+ loading={false}
+ loadingText="Loading..."
+ multiSort={true}
+ nestingLevelKey="_nestingLevel"
+ nextText="Next"
+ noDataText="No column data found"
+ ofText="of"
+ onFetchData={[Function]}
+ originalKey="_original"
+ pageJumpText="jump to page"
+ pageSizeOptions={
+ Array [
+ 25,
+ 50,
+ 100,
+ ]
+ }
+ pageText="Page"
+ pivotDefaults={Object {}}
+ pivotIDKey="_pivotID"
+ pivotValKey="_pivotVal"
+ previousText="Previous"
+ resizable={true}
+ resolveData={[Function]}
+ rowsSelectorText="rows per page"
+ rowsText="rows"
+ showPageJump={true}
+ showPageSizeOptions={true}
+ showPagination={false}
+ showPaginationBottom={true}
+ showPaginationTop={false}
+ sortable={true}
+ style={Object {}}
+ subRowsKey="_subRows"
+ />
+</div>
+`;
+
+exports[`DatasourceColumnsTable matches snapshot on some data 1`] = `
+<div
+ className="datasource-columns-table"
+>
+ <ReactTable
+ AggregatedComponent={[Function]}
+ ExpanderComponent={[Function]}
+ FilterComponent={[Function]}
+ LoadingComponent={[Function]}
+ NoDataComponent={[Function]}
+ PadRowComponent={[Function]}
+ PaginationComponent={[Function]}
+ PivotValueComponent={[Function]}
+ ResizerComponent={[Function]}
+ TableComponent={[Function]}
+ TbodyComponent={[Function]}
+ TdComponent={[Function]}
+ TfootComponent={[Function]}
+ ThComponent={[Function]}
+ TheadComponent={[Function]}
+ TrComponent={[Function]}
+ TrGroupComponent={[Function]}
+ aggregatedKey="_aggregated"
+ className=""
+ collapseOnDataChange={true}
+ collapseOnPageChange={true}
+ collapseOnSortingChange={true}
+ column={
+ Object {
+ "Aggregated": undefined,
+ "Cell": undefined,
+ "Expander": undefined,
+ "Filter": undefined,
+ "Footer": undefined,
+ "Header": undefined,
+ "Pivot": undefined,
+ "PivotValue": undefined,
+ "Placeholder": undefined,
+ "aggregate": undefined,
+ "className": "",
+ "filterAll": false,
+ "filterMethod": undefined,
+ "filterable": undefined,
+ "footerClassName": "",
+ "footerStyle": Object {},
+ "getFooterProps": [Function],
+ "getHeaderProps": [Function],
+ "getProps": [Function],
+ "headerClassName": "",
+ "headerStyle": Object {},
+ "minResizeWidth": 11,
+ "minWidth": 100,
+ "resizable": undefined,
+ "show": true,
+ "sortMethod": undefined,
+ "sortable": undefined,
+ "style": Object {},
+ }
+ }
+ columns={
+ Array [
+ Object {
+ "Header": "Column name",
+ "accessor": "COLUMN_NAME",
+ "className": "padded",
+ "width": 300,
+ },
+ Object {
+ "Header": "Data type",
+ "accessor": "DATA_TYPE",
+ "className": "padded",
+ "width": 200,
+ },
+ ]
+ }
+ data={
+ Array [
+ Object {
+ "COLUMN_NAME": "channel",
+ "DATA_TYPE": "VARCHAR",
+ },
+ Object {
+ "COLUMN_NAME": "page",
+ "DATA_TYPE": "VARCHAR",
+ },
+ ]
+ }
+ defaultExpanded={Object {}}
+ defaultFilterMethod={[Function]}
+ defaultFiltered={Array []}
+ defaultPage={0}
+ defaultPageSize={25}
+ defaultResized={Array []}
+ defaultSortDesc={false}
+ defaultSortMethod={[Function]}
+ defaultSorted={Array []}
+ expanderDefaults={
+ Object {
+ "filterable": false,
+ "resizable": false,
+ "sortable": false,
+ "width": 35,
+ }
+ }
+ filterable={true}
+ freezeWhenExpanded={false}
+ getLoadingProps={[Function]}
+ getNoDataProps={[Function]}
+ getPaginationProps={[Function]}
+ getProps={[Function]}
+ getResizerProps={[Function]}
+ getTableProps={[Function]}
+ getTbodyProps={[Function]}
+ getTdProps={[Function]}
+ getTfootProps={[Function]}
+ getTfootTdProps={[Function]}
+ getTfootTrProps={[Function]}
+ getTheadFilterProps={[Function]}
+ getTheadFilterThProps={[Function]}
+ getTheadFilterTrProps={[Function]}
+ getTheadGroupProps={[Function]}
+ getTheadGroupThProps={[Function]}
+ getTheadGroupTrProps={[Function]}
+ getTheadProps={[Function]}
+ getTheadThProps={[Function]}
+ getTheadTrProps={[Function]}
+ getTrGroupProps={[Function]}
+ getTrProps={[Function]}
+ groupedByPivotKey="_groupedByPivot"
+ indexKey="_index"
+ loading={false}
+ loadingText="Loading..."
+ multiSort={true}
+ nestingLevelKey="_nestingLevel"
+ nextText="Next"
+ noDataText="No column data found"
+ ofText="of"
+ onFetchData={[Function]}
+ originalKey="_original"
+ pageJumpText="jump to page"
+ pageSizeOptions={
+ Array [
+ 25,
+ 50,
+ 100,
+ ]
+ }
+ pageText="Page"
+ pivotDefaults={Object {}}
+ pivotIDKey="_pivotID"
+ pivotValKey="_pivotVal"
+ previousText="Previous"
+ resizable={true}
+ resolveData={[Function]}
+ rowsSelectorText="rows per page"
+ rowsText="rows"
+ showPageJump={true}
+ showPageSizeOptions={true}
+ showPagination={false}
+ showPaginationBottom={true}
+ showPaginationTop={false}
+ sortable={true}
+ style={Object {}}
+ subRowsKey="_subRows"
+ />
+</div>
+`;
diff --git a/web-console/src/components/datasource-columns-table/datasource-columns-table.scss b/web-console/src/dialogs/datasource-table-action-dialog/datasource-columns-table/datasource-columns-table.scss
similarity index 82%
copy from web-console/src/components/datasource-columns-table/datasource-columns-table.scss
copy to web-console/src/dialogs/datasource-table-action-dialog/datasource-columns-table/datasource-columns-table.scss
index 1d0ba14e2d..f05ca0b061 100644
--- a/web-console/src/components/datasource-columns-table/datasource-columns-table.scss
+++ b/web-console/src/dialogs/datasource-table-action-dialog/datasource-columns-table/datasource-columns-table.scss
@@ -29,21 +29,11 @@
}
}
- .main-area {
- height: calc(100% - 5px);
-
- textarea {
- height: 100%;
- width: 100%;
- resize: none;
- }
-
- .loader {
- position: relative;
- }
+ .loader {
+ position: relative;
+ }
- .ReactTable {
- height: 100%;
- }
+ .ReactTable {
+ height: 100%;
}
}
diff --git a/web-console/src/components/datasource-columns-table/datasource-columns-table.spec.tsx b/web-console/src/dialogs/datasource-table-action-dialog/datasource-columns-table/datasource-columns-table.spec.tsx
similarity index 93%
rename from web-console/src/components/datasource-columns-table/datasource-columns-table.spec.tsx
rename to web-console/src/dialogs/datasource-table-action-dialog/datasource-columns-table/datasource-columns-table.spec.tsx
index 06860d9591..795b274d59 100644
--- a/web-console/src/components/datasource-columns-table/datasource-columns-table.spec.tsx
+++ b/web-console/src/dialogs/datasource-table-action-dialog/datasource-columns-table/datasource-columns-table.spec.tsx
@@ -19,12 +19,12 @@
import { shallow } from 'enzyme';
import React from 'react';
-import { QueryState } from '../../utils';
+import { QueryState } from '../../../utils';
import { DatasourceColumnsTable, DatasourceColumnsTableRow } from './datasource-columns-table';
let columnsState: QueryState<DatasourceColumnsTableRow[]> = QueryState.INIT;
-jest.mock('../../hooks', () => {
+jest.mock('../../../hooks', () => {
return {
useQueryManager: () => [columnsState],
};
@@ -32,7 +32,7 @@ jest.mock('../../hooks', () => {
describe('DatasourceColumnsTable', () => {
function makeDatasourceColumnsTable() {
- return <DatasourceColumnsTable datasourceId="test" downloadFilename="test" />;
+ return <DatasourceColumnsTable datasource="test" />;
}
it('matches snapshot on init', () => {
diff --git a/web-console/src/components/datasource-columns-table/datasource-columns-table.tsx b/web-console/src/dialogs/datasource-table-action-dialog/datasource-columns-table/datasource-columns-table.tsx
similarity index 87%
rename from web-console/src/components/datasource-columns-table/datasource-columns-table.tsx
rename to web-console/src/dialogs/datasource-table-action-dialog/datasource-columns-table/datasource-columns-table.tsx
index 007e54919b..cd56dda85f 100644
--- a/web-console/src/components/datasource-columns-table/datasource-columns-table.tsx
+++ b/web-console/src/dialogs/datasource-table-action-dialog/datasource-columns-table/datasource-columns-table.tsx
@@ -20,10 +20,10 @@ import { SqlLiteral } from 'druid-query-toolkit';
import React from 'react';
import ReactTable from 'react-table';
-import { useQueryManager } from '../../hooks';
-import { SMALL_TABLE_PAGE_SIZE, SMALL_TABLE_PAGE_SIZE_OPTIONS } from '../../react-table';
-import { ColumnMetadata, queryDruidSql } from '../../utils';
-import { Loader } from '../loader/loader';
+import { Loader } from '../../../components';
+import { useQueryManager } from '../../../hooks';
+import { SMALL_TABLE_PAGE_SIZE, SMALL_TABLE_PAGE_SIZE_OPTIONS } from '../../../react-table';
+import { ColumnMetadata, queryDruidSql } from '../../../utils';
import './datasource-columns-table.scss';
@@ -33,21 +33,20 @@ export interface DatasourceColumnsTableRow {
}
export interface DatasourceColumnsTableProps {
- datasourceId: string;
- downloadFilename?: string;
+ datasource: string;
}
export const DatasourceColumnsTable = React.memo(function DatasourceColumnsTable(
props: DatasourceColumnsTableProps,
) {
const [columnsState] = useQueryManager<string, DatasourceColumnsTableRow[]>({
+ initQuery: props.datasource,
processQuery: async (datasourceId: string) => {
return await queryDruidSql<ColumnMetadata>({
query: `SELECT COLUMN_NAME, DATA_TYPE FROM INFORMATION_SCHEMA.COLUMNS
WHERE TABLE_SCHEMA = 'druid' AND TABLE_NAME = ${SqlLiteral.create(datasourceId)}`,
});
},
- initQuery: props.datasourceId,
});
function renderTable() {
@@ -80,7 +79,7 @@ export const DatasourceColumnsTable = React.memo(function DatasourceColumnsTable
return (
<div className="datasource-columns-table">
- <div className="main-area">{columnsState.loading ? <Loader /> : renderTable()}</div>
+ {columnsState.loading ? <Loader /> : renderTable()}
</div>
);
});
diff --git a/web-console/src/components/table-cell-unparseable/table-cell-unparseable.scss b/web-console/src/dialogs/datasource-table-action-dialog/datasource-preview-pane/datasource-preview-pane.scss
similarity index 83%
copy from web-console/src/components/table-cell-unparseable/table-cell-unparseable.scss
copy to web-console/src/dialogs/datasource-table-action-dialog/datasource-preview-pane/datasource-preview-pane.scss
index 43efe5085b..f47703d052 100644
--- a/web-console/src/components/table-cell-unparseable/table-cell-unparseable.scss
+++ b/web-console/src/dialogs/datasource-table-action-dialog/datasource-preview-pane/datasource-preview-pane.scss
@@ -16,6 +16,14 @@
* limitations under the License.
*/
-.table-cell-unparseable {
- color: #9e2b0e;
+.datasource-preview-pane {
+ position: relative;
+ height: 100%;
+
+ .record-table-pane {
+ height: 100%;
+ }
+ .datasource-preview-error {
+ color: #9e2b0e;
+ }
}
diff --git a/web-console/src/dialogs/datasource-table-action-dialog/datasource-preview-pane/datasource-preview-pane.tsx b/web-console/src/dialogs/datasource-table-action-dialog/datasource-preview-pane/datasource-preview-pane.tsx
new file mode 100644
index 0000000000..71c20d2202
--- /dev/null
+++ b/web-console/src/dialogs/datasource-table-action-dialog/datasource-preview-pane/datasource-preview-pane.tsx
@@ -0,0 +1,65 @@
+/*
+ * 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 { QueryResult, QueryRunner, SqlTableRef } from 'druid-query-toolkit';
+import React from 'react';
+
+import { Loader, RecordTablePane } from '../../../components';
+import { useQueryManager } from '../../../hooks';
+import { DruidError } from '../../../utils';
+
+import './datasource-preview-pane.scss';
+
+const queryRunner = new QueryRunner({
+ inflateDateStrategy: 'none',
+});
+
+export interface DatasourcePreviewPaneProps {
+ datasource: string;
+}
+
+export const DatasourcePreviewPane = React.memo(function DatasourcePreviewPane(
+ props: DatasourcePreviewPaneProps,
+) {
+ const [recordState] = useQueryManager<string, QueryResult>({
+ initQuery: props.datasource,
+ processQuery: async (datasource, cancelToken) => {
+ let result: QueryResult;
+ try {
+ result = await queryRunner.runQuery({
+ query: `SELECT * FROM ${SqlTableRef.create(datasource)}`,
+ extraQueryContext: { sqlOuterLimit: 100 },
+ cancelToken,
+ });
+ } catch (e) {
+ throw new DruidError(e);
+ }
+ return result;
+ },
+ });
+
+ return (
+ <div className="datasource-preview-pane">
+ {recordState.loading && <Loader />}
+ {recordState.data && <RecordTablePane queryResult={recordState.data} />}
+ {recordState.error && (
+ <div className="datasource-preview-error">{recordState.error.message}</div>
+ )}
+ </div>
+ );
+});
diff --git a/web-console/src/dialogs/datasource-table-action-dialog/datasource-table-action-dialog.spec.tsx b/web-console/src/dialogs/datasource-table-action-dialog/datasource-table-action-dialog.spec.tsx
index 0f6d19ec76..fc069d9416 100644
--- a/web-console/src/dialogs/datasource-table-action-dialog/datasource-table-action-dialog.spec.tsx
+++ b/web-console/src/dialogs/datasource-table-action-dialog/datasource-table-action-dialog.spec.tsx
@@ -25,7 +25,7 @@ describe('Datasource table action dialog', () => {
it('matches snapshot', () => {
const datasourceTableActionDialog = (
<DatasourceTableActionDialog
- datasourceId="test"
+ datasource="test"
actions={[{ title: 'test', onAction: () => null }]}
onClose={() => {}}
/>
diff --git a/web-console/src/dialogs/datasource-table-action-dialog/datasource-table-action-dialog.tsx b/web-console/src/dialogs/datasource-table-action-dialog/datasource-table-action-dialog.tsx
index 86711a9922..ae1698514b 100644
--- a/web-console/src/dialogs/datasource-table-action-dialog/datasource-table-action-dialog.tsx
+++ b/web-console/src/dialogs/datasource-table-action-dialog/datasource-table-action-dialog.tsx
@@ -16,27 +16,36 @@
* limitations under the License.
*/
+import { IconNames } from '@blueprintjs/icons';
import React, { useState } from 'react';
-import { DatasourceColumnsTable } from '../../components/datasource-columns-table/datasource-columns-table';
import { BasicAction } from '../../utils/basic-action';
import { SideButtonMetaData, TableActionDialog } from '../table-action-dialog/table-action-dialog';
+import { DatasourceColumnsTable } from './datasource-columns-table/datasource-columns-table';
+import { DatasourcePreviewPane } from './datasource-preview-pane/datasource-preview-pane';
+
interface DatasourceTableActionDialogProps {
- datasourceId?: string;
+ datasource: string;
actions: BasicAction[];
- onClose: () => void;
+ onClose(): void;
}
export const DatasourceTableActionDialog = React.memo(function DatasourceTableActionDialog(
props: DatasourceTableActionDialogProps,
) {
- const { onClose, datasourceId, actions } = props;
- const [activeTab, setActiveTab] = useState('columns');
+ const { datasource, actions, onClose } = props;
+ const [activeTab, setActiveTab] = useState<'records' | 'columns'>('records');
- const taskTableSideButtonMetadata: SideButtonMetaData[] = [
+ const sideButtonMetadata: SideButtonMetaData[] = [
+ {
+ icon: IconNames.TH,
+ text: 'Records',
+ active: activeTab === 'records',
+ onClick: () => setActiveTab('records'),
+ },
{
- icon: 'list-columns',
+ icon: IconNames.LIST_COLUMNS,
text: 'Columns',
active: activeTab === 'columns',
onClick: () => setActiveTab('columns'),
@@ -45,17 +54,13 @@ export const DatasourceTableActionDialog = React.memo(function DatasourceTableAc
return (
<TableActionDialog
- sideButtonMetadata={taskTableSideButtonMetadata}
+ sideButtonMetadata={sideButtonMetadata}
onClose={onClose}
- title={`Datasource: ${datasourceId}`}
+ title={`Datasource: ${datasource}`}
actions={actions}
>
- {activeTab === 'columns' && (
- <DatasourceColumnsTable
- datasourceId={datasourceId ? datasourceId : ''}
- downloadFilename={`datasource-dimensions-${datasourceId}.json`}
- />
- )}
+ {activeTab === 'records' && <DatasourcePreviewPane datasource={datasource} />}
+ {activeTab === 'columns' && <DatasourceColumnsTable datasource={datasource} />}
</TableActionDialog>
);
});
diff --git a/web-console/src/dialogs/edit-context-dialog/edit-context-dialog.tsx b/web-console/src/dialogs/edit-context-dialog/edit-context-dialog.tsx
index c5fc45e69c..5eb85d5608 100644
--- a/web-console/src/dialogs/edit-context-dialog/edit-context-dialog.tsx
+++ b/web-console/src/dialogs/edit-context-dialog/edit-context-dialog.tsx
@@ -21,7 +21,7 @@ import Hjson from 'hjson';
import * as JSONBig from 'json-bigint-native';
import React, { useState } from 'react';
-import { QueryContext } from '../../utils/query-context';
+import { QueryContext } from '../../druid-models';
import './edit-context-dialog.scss';
diff --git a/web-console/src/dialogs/history-dialog/history-dialog.tsx b/web-console/src/dialogs/history-dialog/history-dialog.tsx
index 2b0e5315a1..9267d07d0b 100644
--- a/web-console/src/dialogs/history-dialog/history-dialog.tsx
+++ b/web-console/src/dialogs/history-dialog/history-dialog.tsx
@@ -22,7 +22,7 @@ import classNames from 'classnames';
import * as JSONBig from 'json-bigint-native';
import React, { useState } from 'react';
-import { ShowValue } from '../../components/show-value/show-value';
+import { ShowValue } from '../../components';
import { DiffDialog } from '../diff-dialog/diff-dialog';
import './history-dialog.scss';
diff --git a/web-console/src/dialogs/index.ts b/web-console/src/dialogs/index.ts
index d0e0e4ba8b..9509442c8b 100644
--- a/web-console/src/dialogs/index.ts
+++ b/web-console/src/dialogs/index.ts
@@ -22,12 +22,15 @@ export * from './compaction-dialog/compaction-dialog';
export * from './coordinator-dynamic-config-dialog/coordinator-dynamic-config-dialog';
export * from './diff-dialog/diff-dialog';
export * from './doctor-dialog/doctor-dialog';
+export * from './edit-context-dialog/edit-context-dialog';
export * from './history-dialog/history-dialog';
export * from './lookup-edit-dialog/lookup-edit-dialog';
+export * from './numeric-input-dialog/numeric-input-dialog';
export * from './overlord-dynamic-config-dialog/overlord-dynamic-config-dialog';
export * from './retention-dialog/retention-dialog';
export * from './snitch-dialog/snitch-dialog';
export * from './spec-dialog/spec-dialog';
+export * from './string-input-dialog/string-input-dialog';
export * from './supervisor-table-action-dialog/supervisor-table-action-dialog';
export * from './table-action-dialog/table-action-dialog';
export * from './task-table-action-dialog/task-table-action-dialog';
diff --git a/web-console/src/dialogs/lookup-table-action-dialog/lookup-table-action-dialog.tsx b/web-console/src/dialogs/lookup-table-action-dialog/lookup-table-action-dialog.tsx
index a44b3bd74e..e896d31f7f 100644
--- a/web-console/src/dialogs/lookup-table-action-dialog/lookup-table-action-dialog.tsx
+++ b/web-console/src/dialogs/lookup-table-action-dialog/lookup-table-action-dialog.tsx
@@ -18,10 +18,11 @@
import React, { useState } from 'react';
-import { LookupValuesTable } from '../../components/lookup-values-table/lookup-values-table';
import { BasicAction } from '../../utils/basic-action';
import { SideButtonMetaData, TableActionDialog } from '../table-action-dialog/table-action-dialog';
+import { LookupValuesTable } from './lookup-values-table/lookup-values-table';
+
interface LookupTableActionDialogProps {
lookupId?: string;
actions: BasicAction[];
diff --git a/web-console/src/components/lookup-values-table/__snapshots__/lookup-values-table.spec.tsx.snap b/web-console/src/dialogs/lookup-table-action-dialog/lookup-values-table/__snapshots__/lookup-values-table.spec.tsx.snap
similarity index 100%
rename from web-console/src/components/lookup-values-table/__snapshots__/lookup-values-table.spec.tsx.snap
rename to web-console/src/dialogs/lookup-table-action-dialog/lookup-values-table/__snapshots__/lookup-values-table.spec.tsx.snap
diff --git a/web-console/src/components/lookup-values-table/lookup-values-table.scss b/web-console/src/dialogs/lookup-table-action-dialog/lookup-values-table/lookup-values-table.scss
similarity index 100%
rename from web-console/src/components/lookup-values-table/lookup-values-table.scss
rename to web-console/src/dialogs/lookup-table-action-dialog/lookup-values-table/lookup-values-table.scss
diff --git a/web-console/src/components/lookup-values-table/lookup-values-table.spec.tsx b/web-console/src/dialogs/lookup-table-action-dialog/lookup-values-table/lookup-values-table.spec.tsx
similarity index 100%
rename from web-console/src/components/lookup-values-table/lookup-values-table.spec.tsx
rename to web-console/src/dialogs/lookup-table-action-dialog/lookup-values-table/lookup-values-table.spec.tsx
diff --git a/web-console/src/components/lookup-values-table/lookup-values-table.tsx b/web-console/src/dialogs/lookup-table-action-dialog/lookup-values-table/lookup-values-table.tsx
similarity index 93%
rename from web-console/src/components/lookup-values-table/lookup-values-table.tsx
rename to web-console/src/dialogs/lookup-table-action-dialog/lookup-values-table/lookup-values-table.tsx
index faa7810fd4..2ffa3ecc22 100644
--- a/web-console/src/components/lookup-values-table/lookup-values-table.tsx
+++ b/web-console/src/dialogs/lookup-table-action-dialog/lookup-values-table/lookup-values-table.tsx
@@ -20,10 +20,10 @@ import { SqlRef } from 'druid-query-toolkit';
import React from 'react';
import ReactTable from 'react-table';
-import { useQueryManager } from '../../hooks';
-import { SMALL_TABLE_PAGE_SIZE, SMALL_TABLE_PAGE_SIZE_OPTIONS } from '../../react-table';
-import { queryDruidSql } from '../../utils';
-import { Loader } from '../loader/loader';
+import { Loader } from '../../../components/loader/loader';
+import { useQueryManager } from '../../../hooks';
+import { SMALL_TABLE_PAGE_SIZE, SMALL_TABLE_PAGE_SIZE_OPTIONS } from '../../../react-table';
+import { queryDruidSql } from '../../../utils';
import './lookup-values-table.scss';
diff --git a/web-console/src/dialogs/numeric-input-dialog/numeric-input-dialog.tsx b/web-console/src/dialogs/numeric-input-dialog/numeric-input-dialog.tsx
new file mode 100644
index 0000000000..b115e035b8
--- /dev/null
+++ b/web-console/src/dialogs/numeric-input-dialog/numeric-input-dialog.tsx
@@ -0,0 +1,79 @@
+/*
+ * 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 { Button, Classes, Dialog, Intent, NumericInput } from '@blueprintjs/core';
+import React, { useState } from 'react';
+
+const DEFAULT_MIN_VALUE = 1;
+
+interface NumericInputDialogProps {
+ title: string;
+ message?: JSX.Element;
+ minValue?: number;
+ initValue: number;
+ onSubmit(value: number): void;
+ onClose(): void;
+}
+
+export const NumericInputDialog = React.memo(function NumericInputDialog(
+ props: NumericInputDialogProps,
+) {
+ const { title, message, minValue, initValue, onSubmit, onClose } = props;
+
+ const [value, setValue] = useState<number>(initValue);
+
+ return (
+ <Dialog
+ className="numeric-input-dialog"
+ onClose={onClose}
+ isOpen
+ title={title}
+ canOutsideClickClose={false}
+ >
+ <div className={Classes.DIALOG_BODY}>
+ {message}
+ <NumericInput
+ value={value}
+ onValueChange={(v: number) => {
+ if (isNaN(v)) return;
+ setValue(Math.max(v, DEFAULT_MIN_VALUE));
+ }}
+ min={minValue ?? DEFAULT_MIN_VALUE}
+ stepSize={1}
+ minorStepSize={null}
+ majorStepSize={10}
+ fill
+ autoFocus
+ />
+ </div>
+ <div className={Classes.DIALOG_FOOTER}>
+ <div className={Classes.DIALOG_FOOTER_ACTIONS}>
+ <Button text="Close" onClick={onClose} />
+ <Button
+ text="OK"
+ intent={Intent.PRIMARY}
+ onClick={() => {
+ onSubmit(value);
+ onClose();
+ }}
+ />
+ </div>
+ </div>
+ </Dialog>
+ );
+});
diff --git a/web-console/src/dialogs/overlord-dynamic-config-dialog/__snapshots__/overload-dynamic-config-dialog.spec.tsx.snap b/web-console/src/dialogs/overlord-dynamic-config-dialog/__snapshots__/overload-dynamic-config-dialog.spec.tsx.snap
index a2f2ce41f2..008ba4d10e 100644
--- a/web-console/src/dialogs/overlord-dynamic-config-dialog/__snapshots__/overload-dynamic-config-dialog.spec.tsx.snap
+++ b/web-console/src/dialogs/overlord-dynamic-config-dialog/__snapshots__/overload-dynamic-config-dialog.spec.tsx.snap
@@ -11,7 +11,7 @@ exports[`OverlordDynamicConfigDialog matches snapshot 1`] = `
Edit the overlord dynamic configuration on the fly. For more information please refer to the
<Memo(ExternalLink)
- href="https://druid.apache.org/docs/0.23.0/configuration/index.html#overlord-dynamic-configuration"
+ href="https://druid.apache.org/docs/latest/configuration/index.html#overlord-dynamic-configuration"
>
documentation
</Memo(ExternalLink)>
diff --git a/web-console/src/dialogs/retention-dialog/__snapshots__/retention-dialog.spec.tsx.snap b/web-console/src/dialogs/retention-dialog/__snapshots__/retention-dialog.spec.tsx.snap
index 6aaa2ffbf5..de558d3998 100644
--- a/web-console/src/dialogs/retention-dialog/__snapshots__/retention-dialog.spec.tsx.snap
+++ b/web-console/src/dialogs/retention-dialog/__snapshots__/retention-dialog.spec.tsx.snap
@@ -63,7 +63,7 @@ exports[`RetentionDialog matches snapshot 1`] = `
Druid uses rules to determine what data should be retained in the cluster. The rules are evaluated in order from top to bottom. For more information please refer to the
<a
- href="https://druid.apache.org/docs/0.23.0/operations/rule-configuration.html"
+ href="https://druid.apache.org/docs/latest/operations/rule-configuration.html"
rel="noopener noreferrer"
target="_blank"
>
diff --git a/web-console/src/dialogs/segments-table-action-dialog/__snapshots__/segment-table-action-dialog.spec.tsx.snap b/web-console/src/dialogs/segments-table-action-dialog/__snapshots__/segment-table-action-dialog.spec.tsx.snap
index f7cd0ea484..991b659a34 100644
--- a/web-console/src/dialogs/segments-table-action-dialog/__snapshots__/segment-table-action-dialog.spec.tsx.snap
+++ b/web-console/src/dialogs/segments-table-action-dialog/__snapshots__/segment-table-action-dialog.spec.tsx.snap
@@ -89,6 +89,33 @@ exports[`SegmentTableActionDialog matches snapshot 1`] = `
Metadata
</span>
</button>
+ <button
+ class="bp4-button bp4-minimal tab-button"
+ type="button"
+ >
+ <span
+ aria-hidden="true"
+ class="bp4-icon bp4-icon-th"
+ icon="th"
+ >
+ <svg
+ data-icon="th"
+ height="20"
+ viewBox="0 0 20 20"
+ width="20"
+ >
+ <path
+ d="M19 1H1c-.6 0-1 .5-1 1v16c0 .5.4 1 1 1h18c.5 0 1-.5 1-1V2c0-.5-.5-1-1-1zM7 17H2v-3h5v3zm0-4H2v-3h5v3zm0-4H2V6h5v3zm11 8H8v-3h10v3zm0-4H8v-3h10v3zm0-4H8V6h10v3z"
+ fill-rule="evenodd"
+ />
+ </svg>
+ </span>
+ <span
+ class="bp4-button-text"
+ >
+ Records
+ </span>
+ </button>
</div>
<div
class="main-section"
diff --git a/web-console/src/dialogs/segments-table-action-dialog/segment-table-action-dialog.tsx b/web-console/src/dialogs/segments-table-action-dialog/segment-table-action-dialog.tsx
index a33c3c3cf9..cd031f3881 100644
--- a/web-console/src/dialogs/segments-table-action-dialog/segment-table-action-dialog.tsx
+++ b/web-console/src/dialogs/segments-table-action-dialog/segment-table-action-dialog.tsx
@@ -16,6 +16,7 @@
* limitations under the License.
*/
+import { IconNames } from '@blueprintjs/icons';
import React, { useState } from 'react';
import { ShowJson } from '../../components';
@@ -23,6 +24,8 @@ import { Api } from '../../singletons';
import { BasicAction } from '../../utils/basic-action';
import { SideButtonMetaData, TableActionDialog } from '../table-action-dialog/table-action-dialog';
+import { SegmentsPreviewPane } from './segments-preview-pane/segments-preview-pane';
+
interface SegmentTableActionDialogProps {
segmentId: string;
datasourceId: string;
@@ -38,11 +41,17 @@ export const SegmentTableActionDialog = React.memo(function SegmentTableActionDi
const taskTableSideButtonMetadata: SideButtonMetaData[] = [
{
- icon: 'manually-entered-data',
+ icon: IconNames.MANUALLY_ENTERED_DATA,
text: 'Metadata',
active: activeTab === 'metadata',
onClick: () => setActiveTab('metadata'),
},
+ {
+ icon: IconNames.TH,
+ text: 'Records',
+ active: activeTab === 'records',
+ onClick: () => setActiveTab('records'),
+ },
];
return (
@@ -60,6 +69,7 @@ export const SegmentTableActionDialog = React.memo(function SegmentTableActionDi
downloadFilename={`Segment-metadata-${segmentId}.json`}
/>
)}
+ {activeTab === 'records' && <SegmentsPreviewPane segmentId={segmentId} />}
</TableActionDialog>
);
});
diff --git a/web-console/src/dialogs/segments-table-action-dialog/segments-preview-pane/parse-segement-id.spec.ts b/web-console/src/dialogs/segments-table-action-dialog/segments-preview-pane/parse-segement-id.spec.ts
new file mode 100644
index 0000000000..e5efec03c2
--- /dev/null
+++ b/web-console/src/dialogs/segments-table-action-dialog/segments-preview-pane/parse-segement-id.spec.ts
@@ -0,0 +1,65 @@
+/*
+ * 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 { parseSegmentId } from './segments-preview-pane';
+
+describe('parseSegmentId', () => {
+ it('correctly identifies segment ID parts', () => {
+ const segmentId =
+ 'kttm_reingest_2019-08-25T23:00:00.000Z_2019-08-26T00:00:00.000Z_2022-08-02T18:58:41.697Z';
+ expect(parseSegmentId(segmentId).datasource).toEqual('kttm_reingest');
+ expect(parseSegmentId(segmentId).interval).toEqual(
+ '2019-08-25T23:00:00.000Z/2019-08-26T00:00:00.000Z',
+ );
+ expect(parseSegmentId(segmentId).version).toEqual('2022-08-02T18:58:41.697Z');
+ expect(parseSegmentId(segmentId).partitionNumber).toEqual(0);
+ });
+
+ it('correctly identifies segment ID parts with partitionNumber', () => {
+ const segmentId =
+ 'test_segment_id1_2019-08-25T23:00:00.000Z_2019-08-26T00:00:00.000Z_2022-08-02T18:58:41.697Z_1';
+ expect(parseSegmentId(segmentId).datasource).toEqual('test_segment_id1');
+ expect(parseSegmentId(segmentId).interval).toEqual(
+ '2019-08-25T23:00:00.000Z/2019-08-26T00:00:00.000Z',
+ );
+ expect(parseSegmentId(segmentId).version).toEqual('2022-08-02T18:58:41.697Z');
+ expect(parseSegmentId(segmentId).partitionNumber).toEqual(1);
+ });
+
+ it('correctly identifies segment ID parts with without partition number and _ in name', () => {
+ const segmentId =
+ 'test___2019-08-25T23:00:00.000Z_2019-08-26T00:00:00.000Z_2022-08-02T18:58:41.697Z';
+ expect(parseSegmentId(segmentId).datasource).toEqual('test__');
+ expect(parseSegmentId(segmentId).interval).toEqual(
+ '2019-08-25T23:00:00.000Z/2019-08-26T00:00:00.000Z',
+ );
+ expect(parseSegmentId(segmentId).version).toEqual('2022-08-02T18:58:41.697Z');
+ expect(parseSegmentId(segmentId).partitionNumber).toEqual(0);
+ });
+
+ it('correctly identifies segment ID parts with long partition number', () => {
+ const segmentId =
+ 'test___2019-08-25T23:00:00.000Z_2019-08-26T00:00:00.000Z_2022-08-02T18:58:41.697Z_1234567';
+ expect(parseSegmentId(segmentId).datasource).toEqual('test__');
+ expect(parseSegmentId(segmentId).interval).toEqual(
+ '2019-08-25T23:00:00.000Z/2019-08-26T00:00:00.000Z',
+ );
+ expect(parseSegmentId(segmentId).version).toEqual('2022-08-02T18:58:41.697Z');
+ expect(parseSegmentId(segmentId).partitionNumber).toEqual(1234567);
+ });
+});
diff --git a/web-console/src/components/table-cell-unparseable/table-cell-unparseable.scss b/web-console/src/dialogs/segments-table-action-dialog/segments-preview-pane/segments-preview-pane.scss
similarity index 81%
copy from web-console/src/components/table-cell-unparseable/table-cell-unparseable.scss
copy to web-console/src/dialogs/segments-table-action-dialog/segments-preview-pane/segments-preview-pane.scss
index 43efe5085b..f072371285 100644
--- a/web-console/src/components/table-cell-unparseable/table-cell-unparseable.scss
+++ b/web-console/src/dialogs/segments-table-action-dialog/segments-preview-pane/segments-preview-pane.scss
@@ -16,6 +16,15 @@
* limitations under the License.
*/
-.table-cell-unparseable {
- color: #9e2b0e;
+.segments-preview-pane {
+ position: relative;
+ height: 100%;
+
+ .record-table-pane {
+ height: 100%;
+ }
+ .segments-preview-error {
+ color: #9e2b0e;
+ height: 100%;
+ }
}
diff --git a/web-console/src/dialogs/segments-table-action-dialog/segments-preview-pane/segments-preview-pane.tsx b/web-console/src/dialogs/segments-table-action-dialog/segments-preview-pane/segments-preview-pane.tsx
new file mode 100644
index 0000000000..6c5ace1c5f
--- /dev/null
+++ b/web-console/src/dialogs/segments-table-action-dialog/segments-preview-pane/segments-preview-pane.tsx
@@ -0,0 +1,119 @@
+/*
+ * 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 { QueryResult, QueryRunner } from 'druid-query-toolkit';
+import React from 'react';
+
+import { Loader, RecordTablePane } from '../../../components';
+// import { Loader, RecordTablePane } from '../../../components';
+import { useQueryManager } from '../../../hooks/use-query-manager';
+import { DruidError } from '../../../utils';
+
+import './segments-preview-pane.scss';
+
+const queryRunner = new QueryRunner({
+ inflateDateStrategy: 'none',
+});
+
+interface ParsedSegmentId {
+ datasource: string;
+ interval: string;
+ partitionNumber: number;
+ version: string;
+}
+
+export function parseSegmentId(segmentId: string): ParsedSegmentId {
+ const segmentIdParts = segmentId.split('_');
+ const tail = Number(segmentIdParts[segmentIdParts.length - 1]);
+ let bump = 1;
+ let partitionNumber = 0;
+
+ // Check if segmentId includes a partitionNumber
+ if (!isNaN(tail)) {
+ partitionNumber = tail;
+ bump++;
+ }
+
+ const version = segmentIdParts[segmentIdParts.length - bump];
+ const interval =
+ segmentIdParts[segmentIdParts.length - bump - 2] +
+ '/' +
+ segmentIdParts[segmentIdParts.length - bump - 1];
+ const datasource = segmentIdParts.slice(0, segmentIdParts.length - bump - 2).join('_');
+
+ return {
+ datasource: datasource,
+ version: version,
+ interval: interval,
+ partitionNumber: partitionNumber,
+ };
+}
+
+export interface DatasourcePreviewPaneProps {
+ segmentId: string;
+}
+
+export const SegmentsPreviewPane = React.memo(function DatasourcePreviewPane(
+ props: DatasourcePreviewPaneProps,
+) {
+ const segmentIdParts = parseSegmentId(props.segmentId);
+
+ const [recordState] = useQueryManager<string, QueryResult>({
+ initQuery: segmentIdParts.datasource,
+ processQuery: async (datasource, cancelToken) => {
+ let result: QueryResult;
+ try {
+ result = await queryRunner.runQuery({
+ query: {
+ queryType: 'scan',
+ dataSource: datasource,
+ intervals: {
+ type: 'segments',
+ segments: [
+ {
+ itvl: segmentIdParts.interval,
+ ver: segmentIdParts.version,
+ part: segmentIdParts.partitionNumber,
+ },
+ ],
+ },
+ resultFormat: 'compactedList',
+ limit: 1001,
+ columns: [],
+ granularity: 'all',
+ },
+ extraQueryContext: { sqlOuterLimit: 100 },
+ cancelToken,
+ });
+ } catch (e) {
+ throw new DruidError(e);
+ }
+ return result;
+ },
+ });
+
+ return (
+ <div className="segments-preview-pane">
+ {recordState.loading && <Loader />}
+ {recordState.data && <RecordTablePane queryResult={recordState.data} />}
+ {recordState.error && (
+ <div className="segments-preview-error">{recordState.error.message}</div>
+ )}
+ </div>
+ );
+});
diff --git a/web-console/src/dialogs/status-dialog/status-dialog.spec.tsx b/web-console/src/dialogs/status-dialog/status-dialog.spec.tsx
index 1aa8d9786f..11b9f61717 100644
--- a/web-console/src/dialogs/status-dialog/status-dialog.spec.tsx
+++ b/web-console/src/dialogs/status-dialog/status-dialog.spec.tsx
@@ -19,7 +19,7 @@
import { render } from '@testing-library/react';
import React from 'react';
-import { anywhereMatcher, StatusDialog } from './status-dialog';
+import { StatusDialog } from './status-dialog';
describe('StatusDialog', () => {
it('matches snapshot', () => {
@@ -27,18 +27,4 @@ describe('StatusDialog', () => {
render(statusDialog);
expect(document.body.lastChild).toMatchSnapshot();
});
-
- it('filters data that contains input', () => {
- const row = [
- 'org.apache.druid.common.gcp.GcpModule',
- 'org.apache.druid.common.aws.AWSModule',
- 'org.apache.druid.OtherModule',
- ];
-
- expect(anywhereMatcher({ id: '0', value: 'common' }, row)).toEqual(true);
- expect(anywhereMatcher({ id: '1', value: 'common' }, row)).toEqual(true);
- expect(anywhereMatcher({ id: '0', value: 'org' }, row)).toEqual(true);
- expect(anywhereMatcher({ id: '1', value: 'org' }, row)).toEqual(true);
- expect(anywhereMatcher({ id: '2', value: 'common' }, row)).toEqual(false);
- });
});
diff --git a/web-console/src/dialogs/status-dialog/status-dialog.tsx b/web-console/src/dialogs/status-dialog/status-dialog.tsx
index 3ae476d675..c7da95e424 100644
--- a/web-console/src/dialogs/status-dialog/status-dialog.tsx
+++ b/web-console/src/dialogs/status-dialog/status-dialog.tsx
@@ -17,20 +17,16 @@
*/
import { Button, Classes, Dialog, Intent } from '@blueprintjs/core';
-import React from 'react';
+import React, { useState } from 'react';
import ReactTable, { Filter } from 'react-table';
-import { Loader } from '../../components';
+import { Loader, TableFilterableCell } from '../../components';
import { useQueryManager } from '../../hooks';
import { SMALL_TABLE_PAGE_SIZE, SMALL_TABLE_PAGE_SIZE_OPTIONS } from '../../react-table';
import { Api, UrlBaser } from '../../singletons';
import './status-dialog.scss';
-export function anywhereMatcher(filter: Filter, row: any): boolean {
- return String(row[filter.id]).includes(filter.value);
-}
-
interface StatusModule {
artifact: string;
name: string;
@@ -43,64 +39,78 @@ interface StatusResponse {
}
interface StatusDialogProps {
- onClose: () => void;
+ onClose(): void;
}
export const StatusDialog = React.memo(function StatusDialog(props: StatusDialogProps) {
const { onClose } = props;
+ const [moduleFilter, setModuleFilter] = useState<Filter[]>([]);
+
const [responseState] = useQueryManager<null, StatusResponse>({
+ initQuery: null,
processQuery: async () => {
const resp = await Api.instance.get(`/status`);
return resp.data;
},
- initQuery: null,
});
function renderContent(): JSX.Element | undefined {
if (responseState.loading) return <Loader />;
if (responseState.error) {
- return <span>{`Error while loading status: ${responseState.error}`}</span>;
+ return <div>{`Error while loading status: ${responseState.error}`}</div>;
}
const response = responseState.data;
if (!response) return;
+ const renderModuleFilterableCell = (field: string) => {
+ return function ModuleFilterableCell(row: { value: any }) {
+ return (
+ <TableFilterableCell
+ field={field}
+ value={row.value}
+ filters={moduleFilter}
+ onFiltersChange={setModuleFilter}
+ >
+ {row.value}
+ </TableFilterableCell>
+ );
+ };
+ };
+
return (
<div className="main-container">
<div className="version">
- Version: <strong>{response.version}</strong>
+ Version: <strong>{response.version}</strong>
</div>
<ReactTable
data={response.modules}
loading={responseState.loading}
filterable
- defaultFilterMethod={anywhereMatcher}
+ filtered={moduleFilter}
+ onFilteredChange={setModuleFilter}
defaultPageSize={SMALL_TABLE_PAGE_SIZE}
pageSizeOptions={SMALL_TABLE_PAGE_SIZE_OPTIONS}
showPagination={response.modules.length > SMALL_TABLE_PAGE_SIZE}
columns={[
{
- columns: [
- {
- Header: 'Extension name',
- accessor: 'artifact',
- width: 200,
- className: 'padded',
- },
- {
- Header: 'Version',
- accessor: 'version',
- width: 200,
- className: 'padded',
- },
- {
- Header: 'Fully qualified name',
- accessor: 'name',
- width: 500,
- className: 'padded',
- },
- ],
+ Header: 'Extension name',
+ accessor: 'artifact',
+ width: 200,
+ Cell: renderModuleFilterableCell('artifact'),
+ },
+ {
+ Header: 'Version',
+ accessor: 'version',
+ width: 200,
+ Cell: renderModuleFilterableCell('version'),
+ },
+ {
+ Header: 'Fully qualified name',
+ accessor: 'name',
+ width: 500,
+ Cell: renderModuleFilterableCell('name'),
},
]}
/>
diff --git a/web-console/src/dialogs/string-input-dialog/string-input-dialog.tsx b/web-console/src/dialogs/string-input-dialog/string-input-dialog.tsx
new file mode 100644
index 0000000000..cee1161327
--- /dev/null
+++ b/web-console/src/dialogs/string-input-dialog/string-input-dialog.tsx
@@ -0,0 +1,61 @@
+/*
+ * 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 { Button, Classes, Dialog, InputGroup, Intent } from '@blueprintjs/core';
+import React, { useState } from 'react';
+
+export interface StringInputDialogProps {
+ title: string;
+ initValue?: string;
+ placeholder?: string;
+ maxLength?: number;
+ onSubmit(str: string): void;
+ onClose(): void;
+}
+
+export const StringInputDialog = React.memo(function StringSubmitDialog(
+ props: StringInputDialogProps,
+) {
+ const { title, initValue, placeholder, maxLength, onSubmit, onClose } = props;
+
+ const [value, setValue] = useState(initValue || '');
+
+ function handleSubmit() {
+ onSubmit(value);
+ onClose();
+ }
+
+ return (
+ <Dialog className="string-input-dialog" isOpen onClose={onClose} title={title}>
+ <div className={Classes.DIALOG_BODY}>
+ <InputGroup
+ value={value}
+ onChange={e => setValue(String(e.target.value).substring(0, maxLength || 280))}
+ autoFocus
+ placeholder={placeholder}
+ />
+ </div>
+ <div className={Classes.DIALOG_FOOTER}>
+ <div className={Classes.DIALOG_FOOTER_ACTIONS}>
+ <Button text="Close" onClick={onClose} />
+ <Button text="Submit" intent={Intent.PRIMARY} onClick={handleSubmit} />
+ </div>
+ </div>
+ </Dialog>
+ );
+});
diff --git a/web-console/src/components/supervisor-statistics-table/__snapshots__/supervisor-statistics-table.spec.tsx.snap b/web-console/src/dialogs/supervisor-table-action-dialog/supervisor-statistics-table/__snapshots__/supervisor-statistics-table.spec.tsx.snap
similarity index 100%
rename from web-console/src/components/supervisor-statistics-table/__snapshots__/supervisor-statistics-table.spec.tsx.snap
rename to web-console/src/dialogs/supervisor-table-action-dialog/supervisor-statistics-table/__snapshots__/supervisor-statistics-table.spec.tsx.snap
diff --git a/web-console/src/components/supervisor-statistics-table/supervisor-statistics-table.scss b/web-console/src/dialogs/supervisor-table-action-dialog/supervisor-statistics-table/supervisor-statistics-table.scss
similarity index 100%
rename from web-console/src/components/supervisor-statistics-table/supervisor-statistics-table.scss
rename to web-console/src/dialogs/supervisor-table-action-dialog/supervisor-statistics-table/supervisor-statistics-table.scss
diff --git a/web-console/src/components/supervisor-statistics-table/supervisor-statistics-table.spec.tsx b/web-console/src/dialogs/supervisor-table-action-dialog/supervisor-statistics-table/supervisor-statistics-table.spec.tsx
similarity index 97%
rename from web-console/src/components/supervisor-statistics-table/supervisor-statistics-table.spec.tsx
rename to web-console/src/dialogs/supervisor-table-action-dialog/supervisor-statistics-table/supervisor-statistics-table.spec.tsx
index 7a81495a08..ac71cd5a2c 100644
--- a/web-console/src/components/supervisor-statistics-table/supervisor-statistics-table.spec.tsx
+++ b/web-console/src/dialogs/supervisor-table-action-dialog/supervisor-statistics-table/supervisor-statistics-table.spec.tsx
@@ -19,7 +19,7 @@
import { shallow } from 'enzyme';
import React from 'react';
-import { QueryState } from '../../utils';
+import { QueryState } from '../../../utils';
import {
normalizeSupervisorStatisticsResults,
@@ -28,7 +28,7 @@ import {
} from './supervisor-statistics-table';
let supervisorStatisticsState: QueryState<SupervisorStatisticsTableRow[]> = QueryState.INIT;
-jest.mock('../../hooks', () => {
+jest.mock('../../../hooks', () => {
return {
useQueryManager: () => [supervisorStatisticsState],
};
diff --git a/web-console/src/components/supervisor-statistics-table/supervisor-statistics-table.tsx b/web-console/src/dialogs/supervisor-table-action-dialog/supervisor-statistics-table/supervisor-statistics-table.tsx
similarity index 95%
rename from web-console/src/components/supervisor-statistics-table/supervisor-statistics-table.tsx
rename to web-console/src/dialogs/supervisor-table-action-dialog/supervisor-statistics-table/supervisor-statistics-table.tsx
index fe66c4efb6..48c2f3f88a 100644
--- a/web-console/src/components/supervisor-statistics-table/supervisor-statistics-table.tsx
+++ b/web-console/src/dialogs/supervisor-table-action-dialog/supervisor-statistics-table/supervisor-statistics-table.tsx
@@ -20,11 +20,11 @@ import { Button, ButtonGroup } from '@blueprintjs/core';
import React from 'react';
import ReactTable, { CellInfo, Column } from 'react-table';
-import { useQueryManager } from '../../hooks';
-import { SMALL_TABLE_PAGE_SIZE, SMALL_TABLE_PAGE_SIZE_OPTIONS } from '../../react-table';
-import { Api, UrlBaser } from '../../singletons';
-import { deepGet } from '../../utils';
-import { Loader } from '../loader/loader';
+import { Loader } from '../../../components/loader/loader';
+import { useQueryManager } from '../../../hooks';
+import { SMALL_TABLE_PAGE_SIZE, SMALL_TABLE_PAGE_SIZE_OPTIONS } from '../../../react-table';
+import { Api, UrlBaser } from '../../../singletons';
+import { deepGet } from '../../../utils';
import './supervisor-statistics-table.scss';
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 3303f4fa52..fba476fdce 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
@@ -20,13 +20,14 @@ import React, { useState } from 'react';
import { ShowJson } from '../../components';
import { ShowHistory } from '../../components/show-history/show-history';
-import { SupervisorStatisticsTable } from '../../components/supervisor-statistics-table/supervisor-statistics-table';
import { cleanSpec } from '../../druid-models';
import { Api } from '../../singletons';
import { deepGet } from '../../utils';
import { BasicAction } from '../../utils/basic-action';
import { SideButtonMetaData, TableActionDialog } from '../table-action-dialog/table-action-dialog';
+import { SupervisorStatisticsTable } from './supervisor-statistics-table/supervisor-statistics-table';
+
interface SupervisorTableActionDialogProps {
supervisorId: string;
actions: BasicAction[];
diff --git a/web-console/src/dialogs/table-action-dialog/table-action-dialog.tsx b/web-console/src/dialogs/table-action-dialog/table-action-dialog.tsx
index da49bc9e1d..d66b747afb 100644
--- a/web-console/src/dialogs/table-action-dialog/table-action-dialog.tsx
+++ b/web-console/src/dialogs/table-action-dialog/table-action-dialog.tsx
@@ -53,7 +53,7 @@ export const TableActionDialog = React.memo(function TableActionDialog(
{sideButtonMetadata.map((d, i) => (
<Button
className="tab-button"
- icon={<Icon icon={d.icon} iconSize={20} />}
+ icon={<Icon icon={d.icon} size={20} />}
key={i}
text={d.text}
intent={d.active ? Intent.PRIMARY : Intent.NONE}
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 ecd8454761..fa6e58befb 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
@@ -75,21 +75,21 @@ export const TaskTableActionDialog = React.memo(function TaskTableActionDialog(
{activeTab === 'status' && (
<ShowJson
endpoint={`${taskEndpointBase}/status`}
- transform={x => deepGet(x, 'status')}
+ transform={x => deepGet(x, 'status') || x}
downloadFilename={`task-status-${taskId}.json`}
/>
)}
{activeTab === 'payload' && (
<ShowJson
endpoint={taskEndpointBase}
- transform={x => deepGet(x, 'payload')}
+ transform={x => deepGet(x, 'payload') || x}
downloadFilename={`task-payload-${taskId}.json`}
/>
)}
{activeTab === 'reports' && (
<ShowJson
endpoint={`${taskEndpointBase}/reports`}
- transform={x => deepGet(x, 'ingestionStatsAndErrors.payload')}
+ transform={x => deepGet(x, 'ingestionStatsAndErrors.payload') || x}
downloadFilename={`task-reports-${taskId}.json`}
/>
)}
diff --git a/web-console/src/druid-models/compaction-config.tsx b/web-console/src/druid-models/compaction-config/compaction-config.tsx
similarity index 94%
rename from web-console/src/druid-models/compaction-config.tsx
rename to web-console/src/druid-models/compaction-config/compaction-config.tsx
index 1d5cb1fb58..1958f2b263 100644
--- a/web-console/src/druid-models/compaction-config.tsx
+++ b/web-console/src/druid-models/compaction-config/compaction-config.tsx
@@ -19,8 +19,8 @@
import { Code } from '@blueprintjs/core';
import React from 'react';
-import { Field } from '../components';
-import { deepGet, deepSet, oneOf } from '../utils';
+import { Field } from '../../components';
+import { deepGet, deepSet, oneOf } from '../../utils';
export type CompactionConfig = Record<string, any>;
@@ -245,6 +245,23 @@ export const COMPACTION_CONFIG_FIELDS: Field<CompactionConfig>[] = [
</>
),
},
+ {
+ name: 'tuningConfig.maxColumnsToMerge',
+ type: 'number',
+ defaultValue: -1,
+ min: -1,
+ info: (
+ <>
+ <p>
+ Limit of the number of segments to merge in a single phase when merging segments for
+ publishing. This limit affects the total number of columns present in a set of segments to
+ merge. If the limit is exceeded, segment merging occurs in multiple phases. Druid merges
+ at least 2 segments per phase, regardless of this setting.
+ </p>
+ <p>Default: -1 (unlimited)</p>
+ </>
+ ),
+ },
{
name: 'tuningConfig.totalNumMergeTasks',
type: 'number',
diff --git a/web-console/src/druid-models/compaction-status.spec.ts b/web-console/src/druid-models/compaction-status/compaction-status.spec.ts
similarity index 97%
rename from web-console/src/druid-models/compaction-status.spec.ts
rename to web-console/src/druid-models/compaction-status/compaction-status.spec.ts
index 68b4cca339..8ed0c51413 100644
--- a/web-console/src/druid-models/compaction-status.spec.ts
+++ b/web-console/src/druid-models/compaction-status/compaction-status.spec.ts
@@ -16,7 +16,8 @@
* limitations under the License.
*/
-import { CompactionConfig } from './compaction-config';
+import { CompactionConfig } from '../compaction-config/compaction-config';
+
import {
CompactionStatus,
formatCompactionConfigAndStatus,
diff --git a/web-console/src/druid-models/compaction-status.ts b/web-console/src/druid-models/compaction-status/compaction-status.ts
similarity index 96%
rename from web-console/src/druid-models/compaction-status.ts
rename to web-console/src/druid-models/compaction-status/compaction-status.ts
index c637e44b8a..2982d9b69e 100644
--- a/web-console/src/druid-models/compaction-status.ts
+++ b/web-console/src/druid-models/compaction-status/compaction-status.ts
@@ -16,7 +16,7 @@
* limitations under the License.
*/
-import { CompactionConfig } from './compaction-config';
+import { CompactionConfig } from '../compaction-config/compaction-config';
function capitalizeFirst(str: string): string {
return str.slice(0, 1).toUpperCase() + str.slice(1).toLowerCase();
diff --git a/web-console/src/druid-models/coordinator-dynamic-config.tsx b/web-console/src/druid-models/coordinator-dynamic-config/coordinator-dynamic-config.tsx
similarity index 99%
rename from web-console/src/druid-models/coordinator-dynamic-config.tsx
rename to web-console/src/druid-models/coordinator-dynamic-config/coordinator-dynamic-config.tsx
index f646315a43..6683a721d1 100644
--- a/web-console/src/druid-models/coordinator-dynamic-config.tsx
+++ b/web-console/src/druid-models/coordinator-dynamic-config/coordinator-dynamic-config.tsx
@@ -19,7 +19,7 @@
import { Code } from '@blueprintjs/core';
import React from 'react';
-import { Field } from '../components';
+import { Field } from '../../components';
export interface CoordinatorDynamicConfig {
maxSegmentsToMove?: number;
diff --git a/web-console/src/druid-models/dimension-spec.spec.ts b/web-console/src/druid-models/dimension-spec/dimension-spec.spec.ts
similarity index 96%
rename from web-console/src/druid-models/dimension-spec.spec.ts
rename to web-console/src/druid-models/dimension-spec/dimension-spec.spec.ts
index 8b6a992f96..8d68d41df0 100644
--- a/web-console/src/druid-models/dimension-spec.spec.ts
+++ b/web-console/src/druid-models/dimension-spec/dimension-spec.spec.ts
@@ -16,8 +16,9 @@
* limitations under the License.
*/
+import { CSV_SAMPLE, JSON_SAMPLE } from '../../utils/sampler.mock';
+
import { getDimensionSpecs } from './dimension-spec';
-import { CSV_SAMPLE, JSON_SAMPLE } from './test-fixtures';
describe('dimension-spec', () => {
describe('getDimensionSpecs', () => {
diff --git a/web-console/src/druid-models/dimension-spec.ts b/web-console/src/druid-models/dimension-spec/dimension-spec.ts
similarity index 90%
rename from web-console/src/druid-models/dimension-spec.ts
rename to web-console/src/druid-models/dimension-spec/dimension-spec.ts
index b1b3a5491e..04144d545d 100644
--- a/web-console/src/druid-models/dimension-spec.ts
+++ b/web-console/src/druid-models/dimension-spec/dimension-spec.ts
@@ -16,11 +16,10 @@
* limitations under the License.
*/
-import { Field } from '../components';
-import { filterMap, typeIs } from '../utils';
-import { SampleHeaderAndRows } from '../utils/sampler';
-
-import { guessColumnTypeFromHeaderAndRows } from './ingestion-spec';
+import { Field } from '../../components';
+import { filterMap, typeIs } from '../../utils';
+import { SampleHeaderAndRows } from '../../utils/sampler';
+import { guessColumnTypeFromHeaderAndRows } from '../ingestion-spec/ingestion-spec';
export interface DimensionsSpec {
readonly dimensions?: (string | DimensionSpec)[];
@@ -46,7 +45,7 @@ export const DIMENSION_SPEC_FIELDS: Field<DimensionSpec>[] = [
name: 'type',
type: 'string',
required: true,
- suggestions: ['string', 'long', 'float', 'double'],
+ suggestions: ['string', 'long', 'float', 'double', 'json'],
},
{
name: 'createBitmapIndex',
diff --git a/web-console/src/druid-models/overlord-dynamic-config.tsx b/web-console/src/druid-models/druid-engine/druid-engine.ts
similarity index 68%
copy from web-console/src/druid-models/overlord-dynamic-config.tsx
copy to web-console/src/druid-models/druid-engine/druid-engine.ts
index 14cac82951..f1942e50c5 100644
--- a/web-console/src/druid-models/overlord-dynamic-config.tsx
+++ b/web-console/src/druid-models/druid-engine/druid-engine.ts
@@ -16,20 +16,12 @@
* limitations under the License.
*/
-import { Field } from '../components';
+export type DruidEngine = 'native' | 'sql-native' | 'sql-msq-task';
-export interface OverlordDynamicConfig {
- selectStrategy?: Record<string, any>;
- autoScaler?: Record<string, any>;
-}
+export const DRUID_ENGINES: DruidEngine[] = ['native', 'sql-native', 'sql-msq-task'];
-export const OVERLORD_DYNAMIC_CONFIG_FIELDS: Field<OverlordDynamicConfig>[] = [
- {
- name: 'selectStrategy',
- type: 'json',
- },
- {
- name: 'autoScaler',
- type: 'json',
- },
-];
+export function validDruidEngine(
+ possibleDruidEngine: string | undefined,
+): possibleDruidEngine is DruidEngine {
+ return Boolean(possibleDruidEngine && DRUID_ENGINES.includes(possibleDruidEngine as DruidEngine));
+}
diff --git a/web-console/src/druid-models/execution/execution-ingest-complete.mock.ts b/web-console/src/druid-models/execution/execution-ingest-complete.mock.ts
new file mode 100644
index 0000000000..b40d9ab0ec
--- /dev/null
+++ b/web-console/src/druid-models/execution/execution-ingest-complete.mock.ts
@@ -0,0 +1,285 @@
+/*
+ * 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 { Execution } from './execution';
+
+/*
+For query:
+
+REPLACE INTO "kttm_simple" OVERWRITE ALL
+SELECT TIME_PARSE("timestamp") AS "__time", agent_type
+FROM TABLE(
+ EXTERN(
+ '{"type":"http","uris":["https://static.imply.io/example-data/kttm-v2/kttm-v2-2019-08-25.json.gz"]}',
+ '{"type":"json"}',
+ '[{"name":"timestamp","type":"string"},{"name":"agent_type","type":"string"}]'
+ )
+)
+PARTITIONED BY ALL TIME
+*/
+
+export const EXECUTION_INGEST_COMPLETE = Execution.fromTaskPayloadAndReport(
+ {
+ task: 'query-32ced762-7679-4a25-9220-3915c5976961',
+ payload: {
+ type: 'query_controller',
+ id: 'query-32ced762-7679-4a25-9220-3915c5976961',
+ spec: {
+ query: {
+ queryType: 'scan',
+ dataSource: {
+ type: 'external',
+ inputSource: {
+ type: 'http',
+ uris: ['https://static.imply.io/example-data/kttm-v2/kttm-v2-2019-08-25.json.gz'],
+ httpAuthenticationUsername: null,
+ httpAuthenticationPassword: null,
+ },
+ inputFormat: {
+ type: 'json',
+ flattenSpec: null,
+ featureSpec: {},
+ keepNullColumns: false,
+ },
+ signature: [
+ { name: 'timestamp', type: 'STRING' },
+ { name: 'agent_type', type: 'STRING' },
+ ],
+ },
+ intervals: {
+ type: 'intervals',
+ intervals: ['-146136543-09-08T08:23:32.096Z/146140482-04-24T15:36:27.903Z'],
+ },
+ virtualColumns: [
+ {
+ type: 'expression',
+ name: 'v0',
+ expression: 'timestamp_parse("timestamp",null,\'UTC\')',
+ outputType: 'LONG',
+ },
+ ],
+ resultFormat: 'compactedList',
+ columns: ['agent_type', 'v0'],
+ legacy: false,
+ context: {
+ finalize: false,
+ finalizeAggregations: false,
+ groupByEnableMultiValueUnnesting: false,
+ scanSignature: '[{"name":"agent_type","type":"STRING"},{"name":"v0","type":"LONG"}]',
+ sqlInsertSegmentGranularity: '{"type":"all"}',
+ sqlQueryId: '32ced762-7679-4a25-9220-3915c5976961',
+ sqlReplaceTimeChunks: 'all',
+ },
+ granularity: { type: 'all' },
+ },
+ columnMappings: [
+ { queryColumn: 'v0', outputColumn: '__time' },
+ { queryColumn: 'agent_type', outputColumn: 'agent_type' },
+ ],
+ destination: {
+ type: 'dataSource',
+ dataSource: 'kttm_simple',
+ segmentGranularity: { type: 'all' },
+ replaceTimeChunks: ['-146136543-09-08T08:23:32.096Z/146140482-04-24T15:36:27.903Z'],
+ },
+ assignmentStrategy: 'max',
+ tuningConfig: { maxNumWorkers: 1, maxRowsInMemory: 100000, rowsPerSegment: 3000000 },
+ },
+ sqlQuery:
+ 'REPLACE INTO "kttm_simple" OVERWRITE ALL\nSELECT TIME_PARSE("timestamp") AS "__time", agent_type\nFROM TABLE(\n EXTERN(\n \'{"type":"http","uris":["https://static.imply.io/example-data/kttm-v2/kttm-v2-2019-08-25.json.gz"]}\',\n \'{"type":"json"}\',\n \'[{"name":"timestamp","type":"string"},{"name":"agent_type","type":"string"}]\'\n )\n)\nPARTITIONED BY ALL TIME',
+ sqlQueryContext: {
+ finalizeAggregations: false,
+ groupByEnableMultiValueUnnesting: false,
+ maxParseExceptions: 0,
+ sqlInsertSegmentGranularity: '{"type":"all"}',
+ sqlQueryId: '32ced762-7679-4a25-9220-3915c5976961',
+ sqlReplaceTimeChunks: 'all',
+ },
+ sqlTypeNames: ['TIMESTAMP', 'VARCHAR'],
+ context: { forceTimeChunkLock: true, useLineageBasedSegmentAllocation: true },
+ groupId: 'query-32ced762-7679-4a25-9220-3915c5976961',
+ dataSource: 'kttm_simple',
+ resource: {
+ availabilityGroup: 'query-32ced762-7679-4a25-9220-3915c5976961',
+ requiredCapacity: 1,
+ },
+ },
+ },
+
+ {
+ multiStageQuery: {
+ taskId: 'query-32ced762-7679-4a25-9220-3915c5976961',
+ payload: {
+ status: { status: 'SUCCESS', startTime: '2022-08-22T20:12:51.391Z', durationMs: 25097 },
+ stages: [
+ {
+ stageNumber: 0,
+ definition: {
+ id: '0b353011-6ea1-480a-8ca8-386771621672_0',
+ input: [
+ {
+ type: 'external',
+ inputSource: {
+ type: 'http',
+ uris: [
+ 'https://static.imply.io/example-data/kttm-v2/kttm-v2-2019-08-25.json.gz',
+ ],
+ httpAuthenticationUsername: null,
+ httpAuthenticationPassword: null,
+ },
+ inputFormat: {
+ type: 'json',
+ flattenSpec: null,
+ featureSpec: {},
+ keepNullColumns: false,
+ },
+ signature: [
+ { name: 'timestamp', type: 'STRING' },
+ { name: 'agent_type', type: 'STRING' },
+ ],
+ },
+ ],
+ processor: {
+ type: 'scan',
+ query: {
+ queryType: 'scan',
+ dataSource: { type: 'inputNumber', inputNumber: 0 },
+ intervals: {
+ type: 'intervals',
+ intervals: ['-146136543-09-08T08:23:32.096Z/146140482-04-24T15:36:27.903Z'],
+ },
+ virtualColumns: [
+ {
+ type: 'expression',
+ name: 'v0',
+ expression: 'timestamp_parse("timestamp",null,\'UTC\')',
+ outputType: 'LONG',
+ },
+ ],
+ resultFormat: 'compactedList',
+ columns: ['agent_type', 'v0'],
+ legacy: false,
+ context: {
+ __timeColumn: 'v0',
+ finalize: false,
+ finalizeAggregations: false,
+ groupByEnableMultiValueUnnesting: false,
+ scanSignature:
+ '[{"name":"agent_type","type":"STRING"},{"name":"v0","type":"LONG"}]',
+ sqlInsertSegmentGranularity: '{"type":"all"}',
+ sqlQueryId: '32ced762-7679-4a25-9220-3915c5976961',
+ sqlReplaceTimeChunks: 'all',
+ },
+ granularity: { type: 'all' },
+ },
+ },
+ signature: [
+ { name: '__boost', type: 'LONG' },
+ { name: 'agent_type', type: 'STRING' },
+ { name: 'v0', type: 'LONG' },
+ ],
+ shuffleSpec: {
+ type: 'targetSize',
+ clusterBy: { columns: [{ columnName: '__boost' }] },
+ targetSize: 3000000,
+ },
+ maxWorkerCount: 1,
+ shuffleCheckHasMultipleValues: true,
+ },
+ phase: 'FINISHED',
+ workerCount: 1,
+ partitionCount: 1,
+ startTime: '2022-08-22T20:12:53.790Z',
+ duration: 20229,
+ sort: true,
+ },
+ {
+ stageNumber: 1,
+ definition: {
+ id: '0b353011-6ea1-480a-8ca8-386771621672_1',
+ input: [{ type: 'stage', stage: 0 }],
+ processor: {
+ type: 'segmentGenerator',
+ dataSchema: {
+ dataSource: 'kttm_simple',
+ timestampSpec: { column: '__time', format: 'millis', missingValue: null },
+ dimensionsSpec: {
+ dimensions: [
+ {
+ type: 'string',
+ name: 'agent_type',
+ multiValueHandling: 'SORTED_ARRAY',
+ createBitmapIndex: true,
+ },
+ ],
+ dimensionExclusions: ['__time'],
+ includeAllDimensions: false,
+ },
+ metricsSpec: [],
+ granularitySpec: {
+ type: 'arbitrary',
+ queryGranularity: { type: 'none' },
+ rollup: false,
+ intervals: ['-146136543-09-08T08:23:32.096Z/146140482-04-24T15:36:27.903Z'],
+ },
+ transformSpec: { filter: null, transforms: [] },
+ },
+ columnMappings: [
+ { queryColumn: 'v0', outputColumn: '__time' },
+ { queryColumn: 'agent_type', outputColumn: 'agent_type' },
+ ],
+ tuningConfig: {
+ maxNumWorkers: 1,
+ maxRowsInMemory: 100000,
+ rowsPerSegment: 3000000,
+ },
+ },
+ signature: [],
+ maxWorkerCount: 1,
+ },
+ phase: 'FINISHED',
+ workerCount: 1,
+ partitionCount: 1,
+ startTime: '2022-08-22T20:13:13.991Z',
+ duration: 2497,
+ },
+ ],
+ counters: {
+ '0': {
+ '0': {
+ input0: { type: 'channel', rows: [465346], files: [1], totalFiles: [1] },
+ output: { type: 'channel', rows: [465346], bytes: [25430674], frames: [4] },
+ shuffle: { type: 'channel', rows: [465346], bytes: [23570446], frames: [38] },
+ sortProgress: {
+ type: 'sortProgress',
+ totalMergingLevels: 3,
+ levelToTotalBatches: { '0': 1, '1': 1, '2': 1 },
+ levelToMergedBatches: { '0': 1, '1': 1, '2': 1 },
+ totalMergersForUltimateLevel: 1,
+ progressDigest: 1.0,
+ },
+ },
+ },
+ '1': {
+ '0': { input0: { type: 'channel', rows: [465346], bytes: [23570446], frames: [38] } },
+ },
+ },
+ },
+ },
+ },
+);
diff --git a/web-console/src/druid-models/execution/execution-ingest-error.mock.ts b/web-console/src/druid-models/execution/execution-ingest-error.mock.ts
new file mode 100644
index 0000000000..b19e9f4549
--- /dev/null
+++ b/web-console/src/druid-models/execution/execution-ingest-error.mock.ts
@@ -0,0 +1,290 @@
+/*
+ * 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 { Execution } from './execution';
+
+/*
+For query:
+
+REPLACE INTO "kttm_simple" OVERWRITE ALL
+SELECT TIME_PARSE("timestamp") AS "__time", agent_type
+FROM TABLE(
+ EXTERN(
+ '{"type":"http","uris":["https://static.imply.io/example-data/kttm-v2/kttm-v2-2019-08-25.json.gz_"]}',
+ '{"type":"json"}',
+ '[{"name":"timestamp","type":"string"},{"name":"agent_type","type":"string"}]'
+ )
+)
+PARTITIONED BY ALL
+*/
+
+export const EXECUTION_INGEST_ERROR = Execution.fromTaskPayloadAndReport(
+ {
+ task: 'query-0cf1a40a-aaef-4d17-bda4-5afa7edf07e7',
+ payload: {
+ type: 'query_controller',
+ id: 'query-0cf1a40a-aaef-4d17-bda4-5afa7edf07e7',
+ spec: {
+ query: {
+ queryType: 'scan',
+ dataSource: {
+ type: 'external',
+ inputSource: {
+ type: 'http',
+ uris: ['https://static.imply.io/example-data/kttm-v2/kttm-v2-2019-08-25.json.gz_'],
+ httpAuthenticationUsername: null,
+ httpAuthenticationPassword: null,
+ },
+ inputFormat: {
+ type: 'json',
+ flattenSpec: null,
+ featureSpec: {},
+ keepNullColumns: false,
+ },
+ signature: [
+ { name: 'timestamp', type: 'STRING' },
+ { name: 'agent_type', type: 'STRING' },
+ ],
+ },
+ intervals: {
+ type: 'intervals',
+ intervals: ['-146136543-09-08T08:23:32.096Z/146140482-04-24T15:36:27.903Z'],
+ },
+ virtualColumns: [
+ {
+ type: 'expression',
+ name: 'v0',
+ expression: 'timestamp_parse("timestamp",null,\'UTC\')',
+ outputType: 'LONG',
+ },
+ ],
+ resultFormat: 'compactedList',
+ columns: ['agent_type', 'v0'],
+ legacy: false,
+ context: {
+ finalize: false,
+ finalizeAggregations: false,
+ groupByEnableMultiValueUnnesting: false,
+ scanSignature: '[{"name":"agent_type","type":"STRING"},{"name":"v0","type":"LONG"}]',
+ sqlInsertSegmentGranularity: '{"type":"all"}',
+ sqlQueryId: '0cf1a40a-aaef-4d17-bda4-5afa7edf07e7',
+ sqlReplaceTimeChunks: 'all',
+ },
+ granularity: { type: 'all' },
+ },
+ columnMappings: [
+ { queryColumn: 'v0', outputColumn: '__time' },
+ { queryColumn: 'agent_type', outputColumn: 'agent_type' },
+ ],
+ destination: {
+ type: 'dataSource',
+ dataSource: 'kttm_simple',
+ segmentGranularity: { type: 'all' },
+ replaceTimeChunks: ['-146136543-09-08T08:23:32.096Z/146140482-04-24T15:36:27.903Z'],
+ },
+ assignmentStrategy: 'max',
+ tuningConfig: { maxNumWorkers: 1, maxRowsInMemory: 100000, rowsPerSegment: 3000000 },
+ },
+ sqlQuery:
+ 'REPLACE INTO "kttm_simple" OVERWRITE ALL\nSELECT TIME_PARSE("timestamp") AS "__time", agent_type\nFROM TABLE(\n EXTERN(\n \'{"type":"http","uris":["https://static.imply.io/example-data/kttm-v2/kttm-v2-2019-08-25.json.gz_"]}\',\n \'{"type":"json"}\',\n \'[{"name":"timestamp","type":"string"},{"name":"agent_type","type":"string"}]\'\n )\n)\nPARTITIONED BY ALL',
+ sqlQueryContext: {
+ finalizeAggregations: false,
+ groupByEnableMultiValueUnnesting: false,
+ maxParseExceptions: 0,
+ sqlInsertSegmentGranularity: '{"type":"all"}',
+ sqlQueryId: '0cf1a40a-aaef-4d17-bda4-5afa7edf07e7',
+ sqlReplaceTimeChunks: 'all',
+ },
+ sqlTypeNames: ['TIMESTAMP', 'VARCHAR'],
+ context: { forceTimeChunkLock: true, useLineageBasedSegmentAllocation: true },
+ groupId: 'query-0cf1a40a-aaef-4d17-bda4-5afa7edf07e7',
+ dataSource: 'kttm_simple',
+ resource: {
+ availabilityGroup: 'query-0cf1a40a-aaef-4d17-bda4-5afa7edf07e7',
+ requiredCapacity: 1,
+ },
+ },
+ },
+
+ {
+ multiStageQuery: {
+ taskId: 'query-0cf1a40a-aaef-4d17-bda4-5afa7edf07e7',
+ payload: {
+ status: {
+ status: 'FAILED',
+ errorReport: {
+ taskId: 'query-0cf1a40a-aaef-4d17-bda4-5afa7edf07e7-worker0',
+ host: 'localhost:8101',
+ stageNumber: 0,
+ error: {
+ errorCode: 'UnknownError',
+ message:
+ 'java.lang.RuntimeException: java.lang.RuntimeException: Error occured while trying to read uri: https://static.imply.io/example-data/kttm-v2/kttm-v2-2019-08-25.json.gz_',
+ errorMessage:
+ 'java.lang.RuntimeException: java.lang.RuntimeException: Error occured while trying to read uri: https://static.imply.io/example-data/kttm-v2/kttm-v2-2019-08-25.json.gz_',
+ },
+ exceptionStackTrace:
+ 'java.lang.RuntimeException: java.lang.RuntimeException: Error occured while trying to read uri: https://static.imply.io/example-data/kttm-v2/kttm-v2-2019-08-25.json.gz_\n\tat org.apache.druid.java.util.common.Either.valueOrThrow(Either.java:91)\n\tat org.apache.druid.frame.processor.FrameProcessorExecutor$1ExecutorRunnable.runProcessorNow(FrameProcessorExecutor.java:258)\n\tat org.apache.druid.frame.processor.FrameProcessorExecutor$1ExecutorRunnable.run(FrameProcessorExecu [...]
+ },
+ startTime: '2022-08-22T20:15:04.783Z',
+ durationMs: 18297,
+ },
+ stages: [
+ {
+ stageNumber: 0,
+ definition: {
+ id: '36d4ec93-9367-419f-91c5-68aeaaab2397_0',
+ input: [
+ {
+ type: 'external',
+ inputSource: {
+ type: 'http',
+ uris: [
+ 'https://static.imply.io/example-data/kttm-v2/kttm-v2-2019-08-25.json.gz_',
+ ],
+ httpAuthenticationUsername: null,
+ httpAuthenticationPassword: null,
+ },
+ inputFormat: {
+ type: 'json',
+ flattenSpec: null,
+ featureSpec: {},
+ keepNullColumns: false,
+ },
+ signature: [
+ { name: 'timestamp', type: 'STRING' },
+ { name: 'agent_type', type: 'STRING' },
+ ],
+ },
+ ],
+ processor: {
+ type: 'scan',
+ query: {
+ queryType: 'scan',
+ dataSource: { type: 'inputNumber', inputNumber: 0 },
+ intervals: {
+ type: 'intervals',
+ intervals: ['-146136543-09-08T08:23:32.096Z/146140482-04-24T15:36:27.903Z'],
+ },
+ virtualColumns: [
+ {
+ type: 'expression',
+ name: 'v0',
+ expression: 'timestamp_parse("timestamp",null,\'UTC\')',
+ outputType: 'LONG',
+ },
+ ],
+ resultFormat: 'compactedList',
+ columns: ['agent_type', 'v0'],
+ legacy: false,
+ context: {
+ __timeColumn: 'v0',
+ finalize: false,
+ finalizeAggregations: false,
+ groupByEnableMultiValueUnnesting: false,
+ scanSignature:
+ '[{"name":"agent_type","type":"STRING"},{"name":"v0","type":"LONG"}]',
+ sqlInsertSegmentGranularity: '{"type":"all"}',
+ sqlQueryId: '0cf1a40a-aaef-4d17-bda4-5afa7edf07e7',
+ sqlReplaceTimeChunks: 'all',
+ },
+ granularity: { type: 'all' },
+ },
+ },
+ signature: [
+ { name: '__boost', type: 'LONG' },
+ { name: 'agent_type', type: 'STRING' },
+ { name: 'v0', type: 'LONG' },
+ ],
+ shuffleSpec: {
+ type: 'targetSize',
+ clusterBy: { columns: [{ columnName: '__boost' }] },
+ targetSize: 3000000,
+ },
+ maxWorkerCount: 1,
+ shuffleCheckHasMultipleValues: true,
+ },
+ workerCount: 1,
+ startTime: '2022-08-22T20:15:07.190Z',
+ duration: 15890,
+ sort: true,
+ },
+ {
+ stageNumber: 1,
+ definition: {
+ id: '36d4ec93-9367-419f-91c5-68aeaaab2397_1',
+ input: [{ type: 'stage', stage: 0 }],
+ processor: {
+ type: 'segmentGenerator',
+ dataSchema: {
+ dataSource: 'kttm_simple',
+ timestampSpec: { column: '__time', format: 'millis', missingValue: null },
+ dimensionsSpec: {
+ dimensions: [
+ {
+ type: 'string',
+ name: 'agent_type',
+ multiValueHandling: 'SORTED_ARRAY',
+ createBitmapIndex: true,
+ },
+ ],
+ dimensionExclusions: ['__time'],
+ includeAllDimensions: false,
+ },
+ metricsSpec: [],
+ granularitySpec: {
+ type: 'arbitrary',
+ queryGranularity: { type: 'none' },
+ rollup: false,
+ intervals: ['-146136543-09-08T08:23:32.096Z/146140482-04-24T15:36:27.903Z'],
+ },
+ transformSpec: { filter: null, transforms: [] },
+ },
+ columnMappings: [
+ { queryColumn: 'v0', outputColumn: '__time' },
+ { queryColumn: 'agent_type', outputColumn: 'agent_type' },
+ ],
+ tuningConfig: {
+ maxNumWorkers: 1,
+ maxRowsInMemory: 100000,
+ rowsPerSegment: 3000000,
+ },
+ },
+ signature: [],
+ maxWorkerCount: 1,
+ },
+ },
+ ],
+ counters: {
+ '0': {
+ '0': {
+ input0: { type: 'channel', totalFiles: [1] },
+ sortProgress: {
+ type: 'sortProgress',
+ totalMergingLevels: -1,
+ levelToTotalBatches: {},
+ levelToMergedBatches: {},
+ totalMergersForUltimateLevel: -1,
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+);
diff --git a/web-console/src/druid-models/execution/execution.spec.ts b/web-console/src/druid-models/execution/execution.spec.ts
new file mode 100644
index 0000000000..31ec5af457
--- /dev/null
+++ b/web-console/src/druid-models/execution/execution.spec.ts
@@ -0,0 +1,530 @@
+/*
+ * 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 { Execution } from './execution';
+import { EXECUTION_INGEST_COMPLETE } from './execution-ingest-complete.mock';
+
+describe('Execution', () => {
+ describe('.fromTaskDetail', () => {
+ it('fails for bad status (error: null)', () => {
+ expect(() =>
+ Execution.fromTaskPayloadAndReport(
+ {} as any,
+ {
+ asyncResultId: 'multi-stage-query-sql-1392d806-c17f-4937-94ee-8fa0a3ce1566',
+ error: null,
+ } as any,
+ ),
+ ).toThrowError('Invalid payload');
+ });
+
+ it('works in a general case', () => {
+ expect(EXECUTION_INGEST_COMPLETE).toMatchInlineSnapshot(`
+ Execution {
+ "_payload": Object {
+ "payload": Object {
+ "context": Object {
+ "forceTimeChunkLock": true,
+ "useLineageBasedSegmentAllocation": true,
+ },
+ "dataSource": "kttm_simple",
+ "groupId": "query-32ced762-7679-4a25-9220-3915c5976961",
+ "id": "query-32ced762-7679-4a25-9220-3915c5976961",
+ "resource": Object {
+ "availabilityGroup": "query-32ced762-7679-4a25-9220-3915c5976961",
+ "requiredCapacity": 1,
+ },
+ "spec": Object {
+ "assignmentStrategy": "max",
+ "columnMappings": Array [
+ Object {
+ "outputColumn": "__time",
+ "queryColumn": "v0",
+ },
+ Object {
+ "outputColumn": "agent_type",
+ "queryColumn": "agent_type",
+ },
+ ],
+ "destination": Object {
+ "dataSource": "kttm_simple",
+ "replaceTimeChunks": Array [
+ "-146136543-09-08T08:23:32.096Z/146140482-04-24T15:36:27.903Z",
+ ],
+ "segmentGranularity": Object {
+ "type": "all",
+ },
+ "type": "dataSource",
+ },
+ "query": Object {
+ "columns": Array [
+ "agent_type",
+ "v0",
+ ],
+ "context": Object {
+ "finalize": false,
+ "finalizeAggregations": false,
+ "groupByEnableMultiValueUnnesting": false,
+ "scanSignature": "[{\\"name\\":\\"agent_type\\",\\"type\\":\\"STRING\\"},{\\"name\\":\\"v0\\",\\"type\\":\\"LONG\\"}]",
+ "sqlInsertSegmentGranularity": "{\\"type\\":\\"all\\"}",
+ "sqlQueryId": "32ced762-7679-4a25-9220-3915c5976961",
+ "sqlReplaceTimeChunks": "all",
+ },
+ "dataSource": Object {
+ "inputFormat": Object {
+ "featureSpec": Object {},
+ "flattenSpec": null,
+ "keepNullColumns": false,
+ "type": "json",
+ },
+ "inputSource": Object {
+ "httpAuthenticationPassword": null,
+ "httpAuthenticationUsername": null,
+ "type": "http",
+ "uris": Array [
+ "https://static.imply.io/example-data/kttm-v2/kttm-v2-2019-08-25.json.gz",
+ ],
+ },
+ "signature": Array [
+ Object {
+ "name": "timestamp",
+ "type": "STRING",
+ },
+ Object {
+ "name": "agent_type",
+ "type": "STRING",
+ },
+ ],
+ "type": "external",
+ },
+ "granularity": Object {
+ "type": "all",
+ },
+ "intervals": Object {
+ "intervals": Array [
+ "-146136543-09-08T08:23:32.096Z/146140482-04-24T15:36:27.903Z",
+ ],
+ "type": "intervals",
+ },
+ "legacy": false,
+ "queryType": "scan",
+ "resultFormat": "compactedList",
+ "virtualColumns": Array [
+ Object {
+ "expression": "timestamp_parse(\\"timestamp\\",null,'UTC')",
+ "name": "v0",
+ "outputType": "LONG",
+ "type": "expression",
+ },
+ ],
+ },
+ "tuningConfig": Object {
+ "maxNumWorkers": 1,
+ "maxRowsInMemory": 100000,
+ "rowsPerSegment": 3000000,
+ },
+ },
+ "sqlQuery": "REPLACE INTO \\"kttm_simple\\" OVERWRITE ALL
+ SELECT TIME_PARSE(\\"timestamp\\") AS \\"__time\\", agent_type
+ FROM TABLE(
+ EXTERN(
+ '{\\"type\\":\\"http\\",\\"uris\\":[\\"https://static.imply.io/example-data/kttm-v2/kttm-v2-2019-08-25.json.gz\\"]}',
+ '{\\"type\\":\\"json\\"}',
+ '[{\\"name\\":\\"timestamp\\",\\"type\\":\\"string\\"},{\\"name\\":\\"agent_type\\",\\"type\\":\\"string\\"}]'
+ )
+ )
+ PARTITIONED BY ALL TIME",
+ "sqlQueryContext": Object {
+ "finalizeAggregations": false,
+ "groupByEnableMultiValueUnnesting": false,
+ "maxParseExceptions": 0,
+ "sqlInsertSegmentGranularity": "{\\"type\\":\\"all\\"}",
+ "sqlQueryId": "32ced762-7679-4a25-9220-3915c5976961",
+ "sqlReplaceTimeChunks": "all",
+ },
+ "sqlTypeNames": Array [
+ "TIMESTAMP",
+ "VARCHAR",
+ ],
+ "type": "query_controller",
+ },
+ "task": "query-32ced762-7679-4a25-9220-3915c5976961",
+ },
+ "destination": Object {
+ "dataSource": "kttm_simple",
+ "replaceTimeChunks": Array [
+ "-146136543-09-08T08:23:32.096Z/146140482-04-24T15:36:27.903Z",
+ ],
+ "segmentGranularity": Object {
+ "type": "all",
+ },
+ "type": "dataSource",
+ },
+ "duration": 25097,
+ "engine": "sql-msq-task",
+ "error": undefined,
+ "id": "query-32ced762-7679-4a25-9220-3915c5976961",
+ "nativeQuery": Object {
+ "columns": Array [
+ "agent_type",
+ "v0",
+ ],
+ "context": Object {
+ "finalize": false,
+ "finalizeAggregations": false,
+ "groupByEnableMultiValueUnnesting": false,
+ "scanSignature": "[{\\"name\\":\\"agent_type\\",\\"type\\":\\"STRING\\"},{\\"name\\":\\"v0\\",\\"type\\":\\"LONG\\"}]",
+ "sqlInsertSegmentGranularity": "{\\"type\\":\\"all\\"}",
+ "sqlQueryId": "32ced762-7679-4a25-9220-3915c5976961",
+ "sqlReplaceTimeChunks": "all",
+ },
+ "dataSource": Object {
+ "inputFormat": Object {
+ "featureSpec": Object {},
+ "flattenSpec": null,
+ "keepNullColumns": false,
+ "type": "json",
+ },
+ "inputSource": Object {
+ "httpAuthenticationPassword": null,
+ "httpAuthenticationUsername": null,
+ "type": "http",
+ "uris": Array [
+ "https://static.imply.io/example-data/kttm-v2/kttm-v2-2019-08-25.json.gz",
+ ],
+ },
+ "signature": Array [
+ Object {
+ "name": "timestamp",
+ "type": "STRING",
+ },
+ Object {
+ "name": "agent_type",
+ "type": "STRING",
+ },
+ ],
+ "type": "external",
+ },
+ "granularity": Object {
+ "type": "all",
+ },
+ "intervals": Object {
+ "intervals": Array [
+ "-146136543-09-08T08:23:32.096Z/146140482-04-24T15:36:27.903Z",
+ ],
+ "type": "intervals",
+ },
+ "legacy": false,
+ "queryType": "scan",
+ "resultFormat": "compactedList",
+ "virtualColumns": Array [
+ Object {
+ "expression": "timestamp_parse(\\"timestamp\\",null,'UTC')",
+ "name": "v0",
+ "outputType": "LONG",
+ "type": "expression",
+ },
+ ],
+ },
+ "queryContext": Object {
+ "finalizeAggregations": false,
+ "groupByEnableMultiValueUnnesting": false,
+ "maxParseExceptions": 0,
+ },
+ "result": undefined,
+ "sqlQuery": "REPLACE INTO \\"kttm_simple\\" OVERWRITE ALL
+ SELECT TIME_PARSE(\\"timestamp\\") AS \\"__time\\", agent_type
+ FROM TABLE(
+ EXTERN(
+ '{\\"type\\":\\"http\\",\\"uris\\":[\\"https://static.imply.io/example-data/kttm-v2/kttm-v2-2019-08-25.json.gz\\"]}',
+ '{\\"type\\":\\"json\\"}',
+ '[{\\"name\\":\\"timestamp\\",\\"type\\":\\"string\\"},{\\"name\\":\\"agent_type\\",\\"type\\":\\"string\\"}]'
+ )
+ )
+ PARTITIONED BY ALL TIME",
+ "stages": Stages {
+ "counters": Object {
+ "0": Object {
+ "0": Object {
+ "input0": Object {
+ "files": Array [
+ 1,
+ ],
+ "rows": Array [
+ 465346,
+ ],
+ "totalFiles": Array [
+ 1,
+ ],
+ "type": "channel",
+ },
+ "output": Object {
+ "bytes": Array [
+ 25430674,
+ ],
+ "frames": Array [
+ 4,
+ ],
+ "rows": Array [
+ 465346,
+ ],
+ "type": "channel",
+ },
+ "shuffle": Object {
+ "bytes": Array [
+ 23570446,
+ ],
+ "frames": Array [
+ 38,
+ ],
+ "rows": Array [
+ 465346,
+ ],
+ "type": "channel",
+ },
+ "sortProgress": Object {
+ "levelToMergedBatches": Object {
+ "0": 1,
+ "1": 1,
+ "2": 1,
+ },
+ "levelToTotalBatches": Object {
+ "0": 1,
+ "1": 1,
+ "2": 1,
+ },
+ "progressDigest": 1,
+ "totalMergersForUltimateLevel": 1,
+ "totalMergingLevels": 3,
+ "type": "sortProgress",
+ },
+ },
+ },
+ "1": Object {
+ "0": Object {
+ "input0": Object {
+ "bytes": Array [
+ 23570446,
+ ],
+ "frames": Array [
+ 38,
+ ],
+ "rows": Array [
+ 465346,
+ ],
+ "type": "channel",
+ },
+ },
+ },
+ },
+ "stages": Array [
+ Object {
+ "definition": Object {
+ "id": "0b353011-6ea1-480a-8ca8-386771621672_0",
+ "input": Array [
+ Object {
+ "inputFormat": Object {
+ "featureSpec": Object {},
+ "flattenSpec": null,
+ "keepNullColumns": false,
+ "type": "json",
+ },
+ "inputSource": Object {
+ "httpAuthenticationPassword": null,
+ "httpAuthenticationUsername": null,
+ "type": "http",
+ "uris": Array [
+ "https://static.imply.io/example-data/kttm-v2/kttm-v2-2019-08-25.json.gz",
+ ],
+ },
+ "signature": Array [
+ Object {
+ "name": "timestamp",
+ "type": "STRING",
+ },
+ Object {
+ "name": "agent_type",
+ "type": "STRING",
+ },
+ ],
+ "type": "external",
+ },
+ ],
+ "maxWorkerCount": 1,
+ "processor": Object {
+ "query": Object {
+ "columns": Array [
+ "agent_type",
+ "v0",
+ ],
+ "context": Object {
+ "__timeColumn": "v0",
+ "finalize": false,
+ "finalizeAggregations": false,
+ "groupByEnableMultiValueUnnesting": false,
+ "scanSignature": "[{\\"name\\":\\"agent_type\\",\\"type\\":\\"STRING\\"},{\\"name\\":\\"v0\\",\\"type\\":\\"LONG\\"}]",
+ "sqlInsertSegmentGranularity": "{\\"type\\":\\"all\\"}",
+ "sqlQueryId": "32ced762-7679-4a25-9220-3915c5976961",
+ "sqlReplaceTimeChunks": "all",
+ },
+ "dataSource": Object {
+ "inputNumber": 0,
+ "type": "inputNumber",
+ },
+ "granularity": Object {
+ "type": "all",
+ },
+ "intervals": Object {
+ "intervals": Array [
+ "-146136543-09-08T08:23:32.096Z/146140482-04-24T15:36:27.903Z",
+ ],
+ "type": "intervals",
+ },
+ "legacy": false,
+ "queryType": "scan",
+ "resultFormat": "compactedList",
+ "virtualColumns": Array [
+ Object {
+ "expression": "timestamp_parse(\\"timestamp\\",null,'UTC')",
+ "name": "v0",
+ "outputType": "LONG",
+ "type": "expression",
+ },
+ ],
+ },
+ "type": "scan",
+ },
+ "shuffleCheckHasMultipleValues": true,
+ "shuffleSpec": Object {
+ "clusterBy": Object {
+ "columns": Array [
+ Object {
+ "columnName": "__boost",
+ },
+ ],
+ },
+ "targetSize": 3000000,
+ "type": "targetSize",
+ },
+ "signature": Array [
+ Object {
+ "name": "__boost",
+ "type": "LONG",
+ },
+ Object {
+ "name": "agent_type",
+ "type": "STRING",
+ },
+ Object {
+ "name": "v0",
+ "type": "LONG",
+ },
+ ],
+ },
+ "duration": 20229,
+ "partitionCount": 1,
+ "phase": "FINISHED",
+ "sort": true,
+ "stageNumber": 0,
+ "startTime": "2022-08-22T20:12:53.790Z",
+ "workerCount": 1,
+ },
+ Object {
+ "definition": Object {
+ "id": "0b353011-6ea1-480a-8ca8-386771621672_1",
+ "input": Array [
+ Object {
+ "stage": 0,
+ "type": "stage",
+ },
+ ],
+ "maxWorkerCount": 1,
+ "processor": Object {
+ "columnMappings": Array [
+ Object {
+ "outputColumn": "__time",
+ "queryColumn": "v0",
+ },
+ Object {
+ "outputColumn": "agent_type",
+ "queryColumn": "agent_type",
+ },
+ ],
+ "dataSchema": Object {
+ "dataSource": "kttm_simple",
+ "dimensionsSpec": Object {
+ "dimensionExclusions": Array [
+ "__time",
+ ],
+ "dimensions": Array [
+ Object {
+ "createBitmapIndex": true,
+ "multiValueHandling": "SORTED_ARRAY",
+ "name": "agent_type",
+ "type": "string",
+ },
+ ],
+ "includeAllDimensions": false,
+ },
+ "granularitySpec": Object {
+ "intervals": Array [
+ "-146136543-09-08T08:23:32.096Z/146140482-04-24T15:36:27.903Z",
+ ],
+ "queryGranularity": Object {
+ "type": "none",
+ },
+ "rollup": false,
+ "type": "arbitrary",
+ },
+ "metricsSpec": Array [],
+ "timestampSpec": Object {
+ "column": "__time",
+ "format": "millis",
+ "missingValue": null,
+ },
+ "transformSpec": Object {
+ "filter": null,
+ "transforms": Array [],
+ },
+ },
+ "tuningConfig": Object {
+ "maxNumWorkers": 1,
+ "maxRowsInMemory": 100000,
+ "rowsPerSegment": 3000000,
+ },
+ "type": "segmentGenerator",
+ },
+ "signature": Array [],
+ },
+ "duration": 2497,
+ "partitionCount": 1,
+ "phase": "FINISHED",
+ "stageNumber": 1,
+ "startTime": "2022-08-22T20:13:13.991Z",
+ "workerCount": 1,
+ },
+ ],
+ },
+ "startTime": 2022-08-22T20:12:51.391Z,
+ "status": "SUCCESS",
+ "warnings": undefined,
+ }
+ `);
+ });
+ });
+});
diff --git a/web-console/src/druid-models/execution/execution.ts b/web-console/src/druid-models/execution/execution.ts
new file mode 100644
index 0000000000..5aeeb47a2a
--- /dev/null
+++ b/web-console/src/druid-models/execution/execution.ts
@@ -0,0 +1,467 @@
+/*
+ * 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 { Column, QueryResult, SqlExpression, SqlQuery, SqlWithQuery } from 'druid-query-toolkit';
+
+import { deepGet, deleteKeys, nonEmptyArray, oneOf } from '../../utils';
+import { DruidEngine, validDruidEngine } from '../druid-engine/druid-engine';
+import { QueryContext } from '../query-context/query-context';
+import { Stages } from '../stages/stages';
+
+const IGNORE_CONTEXT_KEYS = [
+ '__asyncIdentity__',
+ '__timeColumn',
+ 'queryId',
+ 'sqlQueryId',
+ 'sqlInsertSegmentGranularity',
+ 'signature',
+ 'scanSignature',
+ 'sqlReplaceTimeChunks',
+];
+
+// Hack around the concept that we might get back a SqlWithQuery and will need to unpack it
+function parseSqlQuery(queryString: string): SqlQuery | undefined {
+ const q = SqlExpression.maybeParse(queryString);
+ if (!q) return;
+ if (q instanceof SqlWithQuery) return q.flattenWith();
+ if (q instanceof SqlQuery) return q;
+ return;
+}
+
+export interface ExecutionError {
+ error: {
+ errorCode: string;
+ errorMessage?: string;
+ [key: string]: any;
+ };
+ host?: string;
+ taskId?: string;
+ stageNumber?: number;
+ exceptionStackTrace?: string;
+}
+
+type ExecutionDestination =
+ | {
+ type: 'taskReport';
+ }
+ | { type: 'dataSource'; dataSource: string; exists?: boolean }
+ | { type: 'download' };
+
+export type ExecutionStatus = 'RUNNING' | 'FAILED' | 'SUCCESS';
+
+export interface LastExecution {
+ engine: DruidEngine;
+ id: string;
+}
+
+export function validateLastExecution(possibleLastExecution: any): LastExecution | undefined {
+ if (
+ !possibleLastExecution ||
+ !validDruidEngine(possibleLastExecution.engine) ||
+ typeof possibleLastExecution.id !== 'string'
+ ) {
+ return;
+ }
+
+ return {
+ engine: possibleLastExecution.engine,
+ id: possibleLastExecution.id,
+ };
+}
+
+export interface ExecutionValue {
+ engine: DruidEngine;
+ id: string;
+ sqlQuery?: string;
+ nativeQuery?: any;
+ queryContext?: QueryContext;
+ status?: ExecutionStatus;
+ startTime?: Date;
+ duration?: number;
+ stages?: Stages;
+ destination?: ExecutionDestination;
+ result?: QueryResult;
+ error?: ExecutionError;
+ warnings?: ExecutionError[];
+ _payload?: { payload: any; task: string };
+}
+
+export class Execution {
+ static validAsyncStatus(
+ status: string | undefined,
+ ): status is 'INITIALIZED' | 'RUNNING' | 'COMPLETE' | 'FAILED' | 'UNDETERMINED' {
+ return oneOf(status, 'INITIALIZED', 'RUNNING', 'COMPLETE', 'FAILED', 'UNDETERMINED');
+ }
+
+ static validTaskStatus(
+ status: string | undefined,
+ ): status is 'WAITING' | 'PENDING' | 'RUNNING' | 'FAILED' | 'SUCCESS' {
+ return oneOf(status, 'WAITING', 'PENDING', 'RUNNING', 'FAILED', 'SUCCESS');
+ }
+
+ static normalizeAsyncStatus(
+ state: 'INITIALIZED' | 'RUNNING' | 'COMPLETE' | 'FAILED' | 'UNDETERMINED',
+ ): ExecutionStatus {
+ switch (state) {
+ case 'COMPLETE':
+ return 'SUCCESS';
+
+ case 'INITIALIZED':
+ case 'UNDETERMINED':
+ return 'RUNNING';
+
+ default:
+ return state;
+ }
+ }
+
+ // Treat WAITING as PENDING since they are all the same as far as the UI is concerned
+ static normalizeTaskStatus(
+ status: 'WAITING' | 'PENDING' | 'RUNNING' | 'FAILED' | 'SUCCESS',
+ ): ExecutionStatus {
+ switch (status) {
+ case 'SUCCESS':
+ case 'FAILED':
+ return status;
+
+ default:
+ return 'RUNNING';
+ }
+ }
+
+ static fromTaskSubmit(
+ taskSubmitResult: { state: any; taskId: string; error: any },
+ sqlQuery?: string,
+ queryContext?: QueryContext,
+ ): Execution {
+ const status = Execution.normalizeTaskStatus(taskSubmitResult.state);
+ return new Execution({
+ engine: 'sql-msq-task',
+ id: taskSubmitResult.taskId,
+ status: taskSubmitResult.error ? 'FAILED' : status,
+ sqlQuery,
+ queryContext,
+ error: taskSubmitResult.error
+ ? {
+ error: {
+ errorCode: 'AsyncError',
+ errorMessage: JSON.stringify(taskSubmitResult.error),
+ },
+ }
+ : status === 'FAILED'
+ ? {
+ error: {
+ errorCode: 'UnknownError',
+ errorMessage:
+ 'Execution failed, there is no detail information, and there is no error in the status response',
+ },
+ }
+ : undefined,
+ destination: undefined,
+ });
+ }
+
+ static fromTaskStatus(
+ taskStatus: { status: any; task: string },
+ sqlQuery?: string,
+ queryContext?: QueryContext,
+ ): Execution {
+ const status = Execution.normalizeTaskStatus(taskStatus.status.status);
+ return new Execution({
+ engine: 'sql-msq-task',
+ id: taskStatus.task,
+ status: taskStatus.status.error ? 'FAILED' : status,
+ sqlQuery,
+ queryContext,
+ error: taskStatus.status.error
+ ? {
+ error: {
+ errorCode: 'AsyncError',
+ errorMessage: JSON.stringify(taskStatus.status.error),
+ },
+ }
+ : status === 'FAILED'
+ ? {
+ error: {
+ errorCode: 'UnknownError',
+ errorMessage:
+ 'Execution failed, there is no detail information, and there is no error in the status response',
+ },
+ }
+ : undefined,
+ destination: undefined,
+ });
+ }
+
+ static fromTaskPayloadAndReport(
+ taskPayload: { payload: any; task: string },
+ taskReport: {
+ multiStageQuery: { payload: any; taskId: string };
+ error?: any;
+ },
+ ): Execution {
+ // Must have status set for a valid report
+ const id = deepGet(taskReport, 'multiStageQuery.taskId');
+ const status = deepGet(taskReport, 'multiStageQuery.payload.status.status');
+ const warnings = deepGet(taskReport, 'multiStageQuery.payload.status.warningReports');
+
+ if (typeof id !== 'string' || !Execution.validTaskStatus(status)) {
+ throw new Error('Invalid payload');
+ }
+
+ let error: ExecutionError | undefined;
+ if (status === 'FAILED') {
+ error =
+ deepGet(taskReport, 'multiStageQuery.payload.status.errorReport') ||
+ (typeof taskReport.error === 'string'
+ ? { error: { errorCode: 'UnknownError', errorMessage: taskReport.error } }
+ : undefined);
+ }
+
+ const stages = deepGet(taskReport, 'multiStageQuery.payload.stages');
+ const startTime = new Date(deepGet(taskReport, 'multiStageQuery.payload.status.startTime'));
+ const durationMs = deepGet(taskReport, 'multiStageQuery.payload.status.durationMs');
+
+ let result: QueryResult | undefined;
+ const resultsPayload: {
+ signature: { name: string; type: string }[];
+ sqlTypeNames: string[];
+ results: any[];
+ } = deepGet(taskReport, 'multiStageQuery.payload.results');
+ if (resultsPayload) {
+ const { signature, sqlTypeNames, results } = resultsPayload;
+ result = new QueryResult({
+ header: signature.map(
+ (sig, i: number) =>
+ new Column({ name: sig.name, nativeType: sig.type, sqlType: sqlTypeNames?.[i] }),
+ ),
+ rows: results,
+ }).inflateDatesFromSqlTypes();
+ }
+
+ let res = new Execution({
+ engine: 'sql-msq-task',
+ id,
+ status: Execution.normalizeTaskStatus(status),
+ startTime: isNaN(startTime.getTime()) ? undefined : startTime,
+ duration: typeof durationMs === 'number' ? durationMs : undefined,
+ stages: Array.isArray(stages)
+ ? new Stages(stages, deepGet(taskReport, 'multiStageQuery.payload.counters'))
+ : undefined,
+ error,
+ warnings: Array.isArray(warnings) ? warnings : undefined,
+ destination: deepGet(taskPayload, 'payload.spec.destination'),
+ result,
+ nativeQuery: deepGet(taskPayload, 'payload.spec.query'),
+
+ _payload: taskPayload,
+ });
+
+ if (deepGet(taskPayload, 'payload.sqlQuery')) {
+ res = res.changeSqlQuery(
+ deepGet(taskPayload, 'payload.sqlQuery'),
+ deleteKeys(deepGet(taskPayload, 'payload.sqlQueryContext'), IGNORE_CONTEXT_KEYS),
+ );
+ }
+
+ return res;
+ }
+
+ static fromResult(engine: DruidEngine, result: QueryResult): Execution {
+ return new Execution({
+ engine,
+ id: result.sqlQueryId || result.queryId || 'direct_result',
+ status: 'SUCCESS',
+ result,
+ duration: result.queryDuration,
+ });
+ }
+
+ public readonly engine: DruidEngine;
+ public readonly id: string;
+ public readonly sqlQuery?: string;
+ public readonly nativeQuery?: any;
+ public readonly queryContext?: QueryContext;
+ public readonly status?: ExecutionStatus;
+ public readonly startTime?: Date;
+ public readonly duration?: number;
+ public readonly stages?: Stages;
+ public readonly destination?: ExecutionDestination;
+ public readonly result?: QueryResult;
+ public readonly error?: ExecutionError;
+ public readonly warnings?: ExecutionError[];
+
+ public readonly _payload?: { payload: any; task: string };
+
+ constructor(value: ExecutionValue) {
+ this.engine = value.engine;
+ this.id = value.id;
+ if (!this.id) throw new Error('must have an id');
+ this.sqlQuery = value.sqlQuery;
+ this.nativeQuery = value.nativeQuery;
+ this.queryContext = value.queryContext;
+ this.status = value.status;
+ this.startTime = value.startTime;
+ this.duration = value.duration;
+ this.stages = value.stages;
+ this.destination = value.destination;
+ this.result = value.result;
+ this.error = value.error;
+ this.warnings = nonEmptyArray(value.warnings) ? value.warnings : undefined;
+
+ this._payload = value._payload;
+ }
+
+ valueOf(): ExecutionValue {
+ return {
+ engine: this.engine,
+ id: this.id,
+ sqlQuery: this.sqlQuery,
+ nativeQuery: this.nativeQuery,
+ queryContext: this.queryContext,
+ status: this.status,
+ startTime: this.startTime,
+ duration: this.duration,
+ stages: this.stages,
+ destination: this.destination,
+ result: this.result,
+ error: this.error,
+ warnings: this.warnings,
+
+ _payload: this._payload,
+ };
+ }
+
+ public changeSqlQuery(sqlQuery: string, queryContext?: QueryContext): Execution {
+ const value = this.valueOf();
+
+ value.sqlQuery = sqlQuery;
+ value.queryContext = queryContext;
+ const parsedQuery = parseSqlQuery(sqlQuery);
+ if (value.result && (parsedQuery || queryContext)) {
+ value.result = value.result.attachQuery({ context: queryContext }, parsedQuery);
+ }
+
+ return new Execution(value);
+ }
+
+ public changeDestination(destination: ExecutionDestination): Execution {
+ return new Execution({
+ ...this.valueOf(),
+ destination,
+ });
+ }
+
+ public changeResult(result: QueryResult): Execution {
+ return new Execution({
+ ...this.valueOf(),
+ result: result.attachQuery({}, this.sqlQuery ? parseSqlQuery(this.sqlQuery) : undefined),
+ });
+ }
+
+ public updateWith(newSummary: Execution): Execution {
+ let nextSummary = newSummary;
+ if (this.sqlQuery && !nextSummary.sqlQuery) {
+ nextSummary = nextSummary.changeSqlQuery(this.sqlQuery, this.queryContext);
+ }
+ if (this.destination && !nextSummary.destination) {
+ nextSummary = nextSummary.changeDestination(this.destination);
+ }
+
+ return nextSummary;
+ }
+
+ public attachErrorFromStatus(status: any): Execution {
+ const errorMsg = deepGet(status, 'status.errorMsg');
+
+ return new Execution({
+ ...this.valueOf(),
+ error: {
+ error: {
+ errorCode: 'UnknownError',
+ errorMessage: errorMsg,
+ },
+ },
+ });
+ }
+
+ public markDestinationDatasourceExists(): Execution {
+ const { destination } = this;
+ if (destination?.type !== 'dataSource') return this;
+
+ return new Execution({
+ ...this.valueOf(),
+ destination: {
+ ...destination,
+ exists: true,
+ },
+ });
+ }
+
+ public isProcessingData(): boolean {
+ const { status, stages } = this;
+ return Boolean(
+ status === 'RUNNING' &&
+ stages &&
+ stages.getTotalInputForStage(stages.getStage(0), 'rows') > 0,
+ );
+ }
+
+ public isWaitingForQuery(): boolean {
+ const { status } = this;
+ return status !== 'SUCCESS' && status !== 'FAILED';
+ }
+
+ public isFullyComplete(): boolean {
+ if (this.isWaitingForQuery()) return false;
+
+ const { status, destination } = this;
+ if (status === 'SUCCESS' && destination?.type === 'dataSource') {
+ return Boolean(destination.exists);
+ }
+
+ return true;
+ }
+
+ public getIngestDatasource(): string | undefined {
+ const { destination } = this;
+ if (destination?.type !== 'dataSource') return;
+ return destination.dataSource;
+ }
+
+ public isSuccessfulInsert(): boolean {
+ return Boolean(
+ this.isFullyComplete() && this.getIngestDatasource() && this.status === 'SUCCESS',
+ );
+ }
+
+ public getErrorMessage(): string | undefined {
+ const { error } = this;
+ if (!error) return;
+ return (
+ (error.error.errorCode ? `${error.error.errorCode}: ` : '') +
+ (error.error.errorMessage || (error.exceptionStackTrace || '').split('\n')[0])
+ );
+ }
+
+ public getEndTime(): Date | undefined {
+ const { startTime, duration } = this;
+ if (!startTime || !duration) return;
+ return new Date(startTime.valueOf() + duration);
+ }
+}
diff --git a/web-console/src/druid-models/external-config/external-config.ts b/web-console/src/druid-models/external-config/external-config.ts
new file mode 100644
index 0000000000..d9dcb97cd1
--- /dev/null
+++ b/web-console/src/druid-models/external-config/external-config.ts
@@ -0,0 +1,192 @@
+/*
+ * 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 {
+ filterMap,
+ SqlExpression,
+ SqlFunction,
+ SqlLiteral,
+ SqlQuery,
+ SqlRef,
+ SqlStar,
+} from 'druid-query-toolkit';
+import * as JSONBig from 'json-bigint-native';
+
+import { nonEmptyArray } from '../../utils';
+import { InputFormat } from '../input-format/input-format';
+import { InputSource } from '../input-source/input-source';
+
+export const MULTI_STAGE_QUERY_MAX_COLUMNS = 2000;
+const MAX_LINES = 10;
+
+function joinLinesMax(lines: string[], max: number) {
+ if (lines.length > max) {
+ lines = lines.slice(0, max).concat(`(and ${lines.length - max} more)`);
+ }
+ return lines.join('\n');
+}
+
+export interface ExternalConfig {
+ inputSource: InputSource;
+ inputFormat: InputFormat;
+ signature: SignatureColumn[];
+}
+
+export interface SignatureColumn {
+ name: string;
+ type: string;
+}
+
+export function summarizeInputSource(inputSource: InputSource, multiline: boolean): string {
+ switch (inputSource.type) {
+ case 'inline':
+ return `inline data`;
+
+ case 'local':
+ // ToDo: make this official
+ if (nonEmptyArray((inputSource as any).files)) {
+ let lines: string[] = (inputSource as any).files;
+ if (!multiline) lines = lines.slice(0, 1);
+ return joinLinesMax(lines, MAX_LINES);
+ }
+ return `${inputSource.baseDir || '?'}{${inputSource.filter || '?'}}`;
+
+ case 'http':
+ if (nonEmptyArray(inputSource.uris)) {
+ let lines: string[] = inputSource.uris;
+ if (!multiline) lines = lines.slice(0, 1);
+ return joinLinesMax(lines, MAX_LINES);
+ }
+ return '?';
+
+ case 's3':
+ case 'google':
+ case 'azure': {
+ const possibleLines = inputSource.uris || inputSource.prefixes;
+ if (nonEmptyArray(possibleLines)) {
+ let lines: string[] = possibleLines;
+ if (!multiline) lines = lines.slice(0, 1);
+ return joinLinesMax(lines, MAX_LINES);
+ }
+ if (nonEmptyArray(inputSource.objects)) {
+ let lines: string[] = inputSource.objects.map(({ bucket, path }) => `${bucket}:${path}`);
+ if (!multiline) lines = lines.slice(0, 1);
+ return joinLinesMax(lines, MAX_LINES);
+ }
+ return '?';
+ }
+
+ case 'hdfs': {
+ const paths =
+ typeof inputSource.paths === 'string' ? inputSource.paths.split(',') : inputSource.paths;
+ if (nonEmptyArray(paths)) {
+ let lines: string[] = paths;
+ if (!multiline) lines = lines.slice(0, 1);
+ return joinLinesMax(lines, MAX_LINES);
+ }
+ return '?';
+ }
+
+ default:
+ return String(inputSource.type);
+ }
+}
+
+export function summarizeInputFormat(inputFormat: InputFormat): string {
+ return String(inputFormat.type);
+}
+
+export function summarizeExternalConfig(externalConfig: ExternalConfig): string {
+ return `${summarizeInputSource(externalConfig.inputSource, false)} [${summarizeInputFormat(
+ externalConfig.inputFormat,
+ )}]`;
+}
+
+export function externalConfigToTableExpression(config: ExternalConfig): SqlExpression {
+ return SqlExpression.parse(`TABLE(
+ EXTERN(
+ ${SqlLiteral.create(JSONBig.stringify(config.inputSource))},
+ ${SqlLiteral.create(JSONBig.stringify(config.inputFormat))},
+ ${SqlLiteral.create(JSONBig.stringify(config.signature))}
+ )
+)`);
+}
+
+export function externalConfigToInitDimensions(
+ config: ExternalConfig,
+ isArrays: boolean[],
+ timeExpression: SqlExpression | undefined,
+): SqlExpression[] {
+ return (timeExpression ? [timeExpression.as('__time')] : [])
+ .concat(
+ filterMap(config.signature, ({ name }, i) => {
+ if (timeExpression && timeExpression.containsColumn(name)) return;
+ return SqlRef.column(name).applyIf(
+ isArrays[i],
+ ex => SqlFunction.simple('MV_TO_ARRAY', [ex]).as(name) as any,
+ );
+ }),
+ )
+ .slice(0, MULTI_STAGE_QUERY_MAX_COLUMNS);
+}
+
+export function fitExternalConfigPattern(query: SqlQuery): ExternalConfig {
+ if (!(query.getSelectExpressionForIndex(0) instanceof SqlStar)) {
+ throw new Error(`External SELECT must only be a star`);
+ }
+
+ const tableFn = query.fromClause?.expressions?.first();
+ if (!(tableFn instanceof SqlFunction) || tableFn.functionName !== 'TABLE') {
+ throw new Error(`External FROM must be a TABLE function`);
+ }
+
+ const externFn = tableFn.getArg(0);
+ if (!(externFn instanceof SqlFunction) || externFn.functionName !== 'EXTERN') {
+ throw new Error(`Within the TABLE function there must be an extern function`);
+ }
+
+ let inputSource: any;
+ try {
+ const arg0 = externFn.getArg(0);
+ inputSource = JSONBig.parse(arg0 instanceof SqlLiteral ? String(arg0.value) : '#');
+ } catch {
+ throw new Error(`The first argument to the extern function must be a string embedding JSON`);
+ }
+
+ let inputFormat: any;
+ try {
+ const arg1 = externFn.getArg(1);
+ inputFormat = JSONBig.parse(arg1 instanceof SqlLiteral ? String(arg1.value) : '#');
+ } catch {
+ throw new Error(`The second argument to the extern function must be a string embedding JSON`);
+ }
+
+ let signature: any;
+ try {
+ const arg2 = externFn.getArg(2);
+ signature = JSONBig.parse(arg2 instanceof SqlLiteral ? String(arg2.value) : '#');
+ } catch {
+ throw new Error(`The third argument to the extern function must be a string embedding JSON`);
+ }
+
+ return {
+ inputSource,
+ inputFormat,
+ signature,
+ };
+}
diff --git a/web-console/src/druid-models/filter.tsx b/web-console/src/druid-models/filter/filter.tsx
similarity index 95%
rename from web-console/src/druid-models/filter.tsx
rename to web-console/src/druid-models/filter/filter.tsx
index 89cb2b65b1..35b09a68c4 100644
--- a/web-console/src/druid-models/filter.tsx
+++ b/web-console/src/druid-models/filter/filter.tsx
@@ -18,11 +18,10 @@
import React from 'react';
-import { ExternalLink, Field } from '../components';
-import { getLink } from '../links';
-import { deepGet, EMPTY_ARRAY, oneOf, typeIs } from '../utils';
-
-import { IngestionSpec } from './ingestion-spec';
+import { ExternalLink, Field } from '../../components';
+import { getLink } from '../../links';
+import { deepGet, EMPTY_ARRAY, oneOf, typeIs } from '../../utils';
+import { IngestionSpec } from '../ingestion-spec/ingestion-spec';
export interface DruidFilter {
readonly type: string;
diff --git a/web-console/src/druid-models/flatten-spec.spec.ts b/web-console/src/druid-models/flatten-spec/flatten-spec.spec.ts
similarity index 91%
rename from web-console/src/druid-models/flatten-spec.spec.ts
rename to web-console/src/druid-models/flatten-spec/flatten-spec.spec.ts
index a475190125..a35c185381 100644
--- a/web-console/src/druid-models/flatten-spec.spec.ts
+++ b/web-console/src/druid-models/flatten-spec/flatten-spec.spec.ts
@@ -18,11 +18,11 @@
import { computeFlattenExprsForData } from './flatten-spec';
-describe('spec-utils', () => {
+describe('flatten-spec', () => {
describe('computeFlattenExprsForData', () => {
const data = [
{
- context: { host: 'clarity', topic: 'moon', bonus: { foo: 'bar' } },
+ context: { host: 'cla', topic: 'moon', bonus: { foo: 'bar' } },
tags: ['a', 'b', 'c'],
messages: [
{ metric: 'request/time', value: 122 },
@@ -32,7 +32,7 @@ describe('spec-utils', () => {
value: 5,
},
{
- context: { host: 'pivot', popic: 'sun' },
+ context: { host: 'piv', popic: 'sun' },
tags: ['a', 'd'],
messages: [
{ metric: 'request/time', value: 44 },
@@ -41,7 +41,7 @@ describe('spec-utils', () => {
value: 4,
},
{
- context: { host: 'imply', dopik: 'fun' },
+ context: { host: 'imp', dopik: 'fun' },
tags: ['x', 'y'],
messages: [
{ metric: 'request/time', value: 4 },
diff --git a/web-console/src/druid-models/flatten-spec.tsx b/web-console/src/druid-models/flatten-spec/flatten-spec.tsx
similarity index 96%
rename from web-console/src/druid-models/flatten-spec.tsx
rename to web-console/src/druid-models/flatten-spec/flatten-spec.tsx
index 68b8184ced..3845a83916 100644
--- a/web-console/src/druid-models/flatten-spec.tsx
+++ b/web-console/src/druid-models/flatten-spec/flatten-spec.tsx
@@ -18,9 +18,9 @@
import React from 'react';
-import { ExternalLink, Field } from '../components';
-import { getLink } from '../links';
-import { typeIs } from '../utils';
+import { ExternalLink, Field } from '../../components';
+import { getLink } from '../../links';
+import { typeIs } from '../../utils';
export interface FlattenSpec {
useFieldDiscovery?: boolean;
diff --git a/web-console/src/druid-models/index.ts b/web-console/src/druid-models/index.ts
index d5e80d3764..359ba70440 100644
--- a/web-console/src/druid-models/index.ts
+++ b/web-console/src/druid-models/index.ts
@@ -16,18 +16,26 @@
* limitations under the License.
*/
-export * from './compaction-config';
-export * from './compaction-status';
-export * from './coordinator-dynamic-config';
-export * from './dimension-spec';
-export * from './filter';
-export * from './flatten-spec';
-export * from './ingestion-spec';
-export * from './input-format';
-export * from './input-source';
-export * from './lookup-spec';
-export * from './metric-spec';
-export * from './overlord-dynamic-config';
-export * from './time';
-export * from './timestamp-spec';
-export * from './transform-spec';
+export * from './compaction-config/compaction-config';
+export * from './compaction-status/compaction-status';
+export * from './coordinator-dynamic-config/coordinator-dynamic-config';
+export * from './dimension-spec/dimension-spec';
+export * from './druid-engine/druid-engine';
+export * from './execution/execution';
+export * from './external-config/external-config';
+export * from './filter/filter';
+export * from './flatten-spec/flatten-spec';
+export * from './ingest-query-pattern/ingest-query-pattern';
+export * from './ingestion-spec/ingestion-spec';
+export * from './input-format/input-format';
+export * from './input-source/input-source';
+export * from './lookup-spec/lookup-spec';
+export * from './metric-spec/metric-spec';
+export * from './overlord-dynamic-config/overlord-dynamic-config';
+export * from './query-context/query-context';
+export * from './stages/stages';
+export * from './time/time';
+export * from './timestamp-spec/timestamp-spec';
+export * from './transform-spec/transform-spec';
+export * from './workbench-query/workbench-query';
+export * from './workbench-query/workbench-query-part';
diff --git a/web-console/src/druid-models/ingest-query-pattern/ingest-query-pattern.spec.ts b/web-console/src/druid-models/ingest-query-pattern/ingest-query-pattern.spec.ts
new file mode 100644
index 0000000000..d8ec89f172
--- /dev/null
+++ b/web-console/src/druid-models/ingest-query-pattern/ingest-query-pattern.spec.ts
@@ -0,0 +1,53 @@
+/*
+ * 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 { sane, SqlQuery } from 'druid-query-toolkit';
+
+import { fitIngestQueryPattern, ingestQueryPatternToQuery } from './ingest-query-pattern';
+
+describe('ingest-query-pattern', () => {
+ it('works', () => {
+ const query = SqlQuery.parse(sane`
+ INSERT INTO "kttm-2019"
+ WITH ext AS (SELECT *
+ FROM TABLE(
+ EXTERN(
+ '{"type":"http","uris":["https://example.com/data.json.gz"]}',
+ '{"type":"json"}',
+ '[{"name":"timestamp","type":"string"},{"name":"agent_category","type":"string"},{"name":"agent_type","type":"string"},{"name":"browser","type":"string"},{"name":"browser_version","type":"string"},{"name":"city","type":"string"},{"name":"continent","type":"string"},{"name":"country","type":"string"},{"name":"version","type":"string"},{"name":"event_type","type":"string"},{"name":"event_subtype","type":"string"},{"name":"loaded_image","type":"string"},{"name":"adblock_list","typ [...]
+ )
+ ))
+ SELECT
+ TIME_PARSE("timestamp") AS __time,
+ agent_category,
+ agent_type,
+ browser,
+ browser_version
+ FROM ext
+ PARTITIONED BY HOUR
+ CLUSTERED BY 4
+ `);
+
+ const insertQueryPattern = fitIngestQueryPattern(query);
+ expect(insertQueryPattern.partitionedBy).toEqual('hour');
+
+ const query2 = ingestQueryPatternToQuery(insertQueryPattern);
+
+ expect(query2.toString()).toEqual(query.toString());
+ });
+});
diff --git a/web-console/src/druid-models/ingest-query-pattern/ingest-query-pattern.ts b/web-console/src/druid-models/ingest-query-pattern/ingest-query-pattern.ts
new file mode 100644
index 0000000000..53a6f2b6b8
--- /dev/null
+++ b/web-console/src/druid-models/ingest-query-pattern/ingest-query-pattern.ts
@@ -0,0 +1,314 @@
+/*
+ * 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 {
+ SqlExpression,
+ SqlFunction,
+ SqlLiteral,
+ SqlPartitionedByClause,
+ SqlQuery,
+ SqlReplaceClause,
+ SqlTableRef,
+ SqlWithPart,
+} from 'druid-query-toolkit';
+
+import { filterMap, oneOf } from '../../utils';
+import {
+ ExternalConfig,
+ externalConfigToInitDimensions,
+ externalConfigToTableExpression,
+ fitExternalConfigPattern,
+} from '../external-config/external-config';
+import { guessDataSourceNameFromInputSource } from '../ingestion-spec/ingestion-spec';
+
+export type IngestMode = 'insert' | 'replace';
+
+function removeMvToArray(dimension: SqlExpression): SqlExpression {
+ return dimension.walk(ex =>
+ ex instanceof SqlFunction && ex.getEffectiveFunctionName() === 'MV_TO_ARRAY'
+ ? ex.getArg(0)
+ : ex,
+ ) as SqlExpression;
+}
+
+export interface IngestQueryPattern {
+ destinationTableName: string;
+ mode: IngestMode;
+ overwriteWhere?: SqlExpression;
+ mainExternalName: string;
+ mainExternalConfig: ExternalConfig;
+ filters: readonly SqlExpression[];
+ dimensions: readonly SqlExpression[];
+ metrics?: readonly SqlExpression[];
+ partitionedBy: string;
+ clusteredBy: readonly number[];
+}
+
+export function externalConfigToIngestQueryPattern(
+ config: ExternalConfig,
+ isArrays: boolean[],
+ timeExpression: SqlExpression | undefined,
+): IngestQueryPattern {
+ return {
+ destinationTableName: guessDataSourceNameFromInputSource(config.inputSource) || 'data',
+ mode: 'replace',
+ mainExternalName: 'ext',
+ mainExternalConfig: config,
+ filters: [],
+ dimensions: externalConfigToInitDimensions(config, isArrays, timeExpression),
+ partitionedBy: timeExpression ? 'day' : 'all',
+ clusteredBy: [],
+ };
+}
+
+export function getQueryPatternExpression(
+ pattern: IngestQueryPattern,
+ index: number,
+): SqlExpression | undefined {
+ const { dimensions, metrics } = pattern;
+ if (index < dimensions.length) {
+ return dimensions[index];
+ } else if (metrics) {
+ return metrics[index - dimensions.length];
+ }
+ return;
+}
+
+export function getQueryPatternExpressionType(
+ pattern: IngestQueryPattern,
+ index: number,
+): 'dimension' | 'metric' | undefined {
+ const { dimensions, metrics } = pattern;
+ if (index < dimensions.length) {
+ return 'dimension';
+ } else if (metrics) {
+ return 'metric';
+ }
+ return;
+}
+
+export function changeQueryPatternExpression(
+ pattern: IngestQueryPattern,
+ index: number,
+ type: 'dimension' | 'metric',
+ ex: SqlExpression | undefined,
+): IngestQueryPattern {
+ let { dimensions, metrics } = pattern;
+ if (index === -1) {
+ if (ex) {
+ if (type === 'dimension') {
+ dimensions = dimensions.concat(ex);
+ } else if (metrics) {
+ metrics = metrics.concat(ex);
+ }
+ }
+ } else if (index < dimensions.length) {
+ dimensions = filterMap(dimensions, (d, i) => (i === index ? ex : d));
+ } else if (metrics) {
+ const metricIndex = index - dimensions.length;
+ metrics = filterMap(metrics, (m, i) => (i === metricIndex ? ex : m));
+ }
+ return { ...pattern, dimensions, metrics };
+}
+
+function verifyHasOutputName(expression: SqlExpression): void {
+ if (expression.getOutputName()) return;
+ throw new Error(`${expression} must have an AS alias`);
+}
+
+export function fitIngestQueryPattern(query: SqlQuery): IngestQueryPattern {
+ if (query.explainClause) throw new Error(`Can not use EXPLAIN in the data loader flow`);
+ if (query.havingClause) throw new Error(`Can not use HAVING in the data loader flow`);
+ if (query.orderByClause) throw new Error(`Can not USE ORDER BY in the data loader flow`);
+ if (query.limitClause) throw new Error(`Can not use LIMIT in the data loader flow`);
+ if (query.offsetClause) throw new Error(`Can not use OFFSET in the data loader flow`);
+ if (query.unionQuery) throw new Error(`Can not use UNION in the data loader flow`);
+
+ if (query.hasStarInSelect()) {
+ throw new Error(
+ `Can not have * in SELECT in the data loader flow, the columns need to be explicitly listed out`,
+ );
+ }
+
+ let destinationTableName: string;
+ let mode: IngestMode;
+ let overwriteWhere: SqlExpression | undefined;
+ if (query.insertClause) {
+ mode = 'insert';
+ destinationTableName = query.insertClause.table.getTable();
+ } else if (query.replaceClause) {
+ mode = 'replace';
+ destinationTableName = query.replaceClause.table.getTable();
+ overwriteWhere = query.replaceClause.whereClause?.expression;
+ } else {
+ throw new Error(`Must have an INSERT or REPLACE clause`);
+ }
+
+ const withParts = query.getWithParts();
+ if (withParts.length !== 1) {
+ throw new Error(`Must have exactly one with part`);
+ }
+
+ const { table: withTable, columns: withColumns, query: withQuery } = withParts[0];
+ if (withColumns) {
+ throw new Error(`Can not have columns in the WITH expression`);
+ }
+ if (!(withQuery instanceof SqlQuery)) {
+ throw new Error(`The body of the WITH expression must be a query`);
+ }
+
+ const mainExternalName = withTable.name;
+ const mainExternalConfig = fitExternalConfigPattern(withQuery);
+
+ const filters = query.getWhereExpression()?.decomposeViaAnd() || [];
+
+ let dimensions: readonly SqlExpression[];
+ let metrics: readonly SqlExpression[] | undefined;
+ if (query.hasGroupBy()) {
+ dimensions = query.getGroupedSelectExpressions();
+ metrics = query.getAggregateSelectExpressions();
+ metrics.forEach(verifyHasOutputName);
+ } else {
+ dimensions = query.getSelectExpressionsArray();
+ }
+
+ dimensions.forEach(verifyHasOutputName);
+
+ const partitionedByClause = query.partitionedByClause;
+ if (!partitionedByClause) {
+ throw new Error(`Must have a PARTITIONED BY clause`);
+ }
+ let partitionedBy: string;
+ if (partitionedByClause.expression) {
+ if (partitionedByClause.expression instanceof SqlLiteral) {
+ partitionedBy = String(partitionedByClause.expression.value).toLowerCase();
+ } else {
+ partitionedBy = '';
+ }
+ } else {
+ partitionedBy = 'all';
+ }
+ if (!oneOf(partitionedBy, 'hour', 'day', 'month', 'year', 'all')) {
+ throw new Error(`Must partition by HOUR, DAY, MONTH, YEAR, or ALL TIME`);
+ }
+
+ const clusteredByExpressions = query.clusteredByClause
+ ? query.clusteredByClause.expressions.values
+ : [];
+ const clusteredBy = clusteredByExpressions.map(clusteredByExpression => {
+ const selectIndex = query.getSelectIndexForExpression(clusteredByExpression, true);
+ if (selectIndex === -1) {
+ throw new Error(
+ `Invalid CLUSTERED BY expression ${clusteredByExpression}, can only partition on a dimension`,
+ );
+ }
+ return selectIndex;
+ });
+
+ return {
+ destinationTableName,
+ mode,
+ overwriteWhere,
+ mainExternalName,
+ mainExternalConfig,
+ filters,
+ dimensions,
+ metrics,
+ partitionedBy,
+ clusteredBy,
+ };
+}
+
+export function ingestQueryPatternToQuery(
+ ingestQueryPattern: IngestQueryPattern,
+ preview?: boolean,
+ sampleDataQuery?: SqlQuery,
+): SqlQuery {
+ const {
+ destinationTableName,
+ mode,
+ overwriteWhere,
+ mainExternalName,
+ mainExternalConfig,
+ filters,
+ dimensions,
+ metrics,
+ partitionedBy,
+ clusteredBy,
+ } = ingestQueryPattern;
+ return SqlQuery.from(SqlTableRef.create(mainExternalName))
+ .applyIf(!preview, q =>
+ mode === 'insert'
+ ? q.changeInsertIntoTable(destinationTableName)
+ : q.changeReplaceClause(SqlReplaceClause.create(destinationTableName, overwriteWhere)),
+ )
+ .changeWithParts([
+ SqlWithPart.simple(
+ mainExternalName,
+ sampleDataQuery ||
+ SqlQuery.create(externalConfigToTableExpression(mainExternalConfig)).applyIf(preview, q =>
+ q.changeLimitValue(10000),
+ ),
+ ),
+ ])
+ .applyForEach(dimensions, (query, ex) =>
+ query.addSelect(preview ? removeMvToArray(ex) : ex, metrics ? { addToGroupBy: 'end' } : {}),
+ )
+ .applyForEach(metrics || [], (query, ex) => query.addSelect(ex))
+ .applyIf(filters.length, query => query.changeWhereExpression(SqlExpression.and(...filters)))
+ .applyIf(!preview, q =>
+ q
+ .changePartitionedByClause(
+ SqlPartitionedByClause.create(
+ partitionedBy !== 'all'
+ ? new SqlLiteral({
+ value: partitionedBy.toUpperCase(),
+ stringValue: partitionedBy.toUpperCase(),
+ })
+ : undefined,
+ ),
+ )
+ .changeClusteredByExpressions(clusteredBy.map(p => SqlLiteral.index(p))),
+ );
+}
+
+export type DestinationMode = 'new' | 'replace' | 'insert';
+
+export interface DestinationInfo {
+ mode: DestinationMode;
+ table: string;
+}
+
+export function getDestinationMode(
+ ingestQueryPattern: IngestQueryPattern,
+ existingTables: string[],
+): DestinationMode {
+ return existingTables.includes(ingestQueryPattern.destinationTableName)
+ ? ingestQueryPattern.mode
+ : 'new';
+}
+
+export function getDestinationInfo(
+ ingestQueryPattern: IngestQueryPattern,
+ existingTables: string[],
+): DestinationInfo {
+ return {
+ mode: getDestinationMode(ingestQueryPattern, existingTables),
+ table: ingestQueryPattern.destinationTableName,
+ };
+}
diff --git a/web-console/src/druid-models/ingestion-spec.spec.ts b/web-console/src/druid-models/ingestion-spec/ingestion-spec.spec.ts
similarity index 95%
rename from web-console/src/druid-models/ingestion-spec.spec.ts
rename to web-console/src/druid-models/ingestion-spec/ingestion-spec.spec.ts
index 768a061fd9..a846d29a81 100644
--- a/web-console/src/druid-models/ingestion-spec.spec.ts
+++ b/web-console/src/druid-models/ingestion-spec/ingestion-spec.spec.ts
@@ -16,6 +16,8 @@
* limitations under the License.
*/
+import { CSV_SAMPLE } from '../../utils/sampler.mock';
+
import {
adjustId,
cleanSpec,
@@ -26,7 +28,6 @@ import {
updateSchemaWithSample,
upgradeSpec,
} from './ingestion-spec';
-import { CSV_SAMPLE } from './test-fixtures';
describe('ingestion-spec', () => {
it('upgrades / downgrades task spec 1', () => {
@@ -170,6 +171,39 @@ describe('ingestion-spec', () => {
});
});
+ it('does not mangle a custom parser', () => {
+ expect(() =>
+ upgradeSpec({
+ type: 'index_parallel',
+ spec: {
+ ioConfig: {
+ type: 'index_parallel',
+ firehose: {
+ type: 'http',
+ uris: ['https://website.com/wikipedia.json.gz'],
+ },
+ },
+ tuningConfig: {
+ type: 'index_parallel',
+ },
+ dataSchema: {
+ dataSource: 'wikipedia',
+ granularitySpec: {
+ segmentGranularity: 'day',
+ queryGranularity: 'hour',
+ rollup: true,
+ },
+ parser: {
+ type: 'super_cool_custom_parser',
+ },
+ },
+ },
+ }),
+ ).toThrow(
+ "Can not rewrite parser of type 'super_cool_custom_parser', only 'string' is supported",
+ );
+ });
+
it('upgrades / downgrades task spec (without parser)', () => {
const oldTaskSpec = {
type: 'index_parallel',
@@ -693,8 +727,12 @@ describe('spec utils', () => {
expect(guessColumnTypeFromInput([1, [2], 3], false)).toEqual('string');
});
+ it('works for strange json', () => {
+ expect(guessColumnTypeFromInput([1, { hello: 'world' }, 3], false)).toEqual('COMPLEX<json>');
+ });
+
it('works for strange input (object with no prototype)', () => {
- expect(guessColumnTypeFromInput([1, Object.create(null), 3], false)).toEqual('string');
+ expect(guessColumnTypeFromInput([1, Object.create(null), 3], false)).toEqual('COMPLEX<json>');
});
});
diff --git a/web-console/src/druid-models/ingestion-spec.tsx b/web-console/src/druid-models/ingestion-spec/ingestion-spec.tsx
similarity index 96%
rename from web-console/src/druid-models/ingestion-spec.tsx
rename to web-console/src/druid-models/ingestion-spec/ingestion-spec.tsx
index 29881dfe0a..d7bf509163 100644
--- a/web-console/src/druid-models/ingestion-spec.tsx
+++ b/web-console/src/druid-models/ingestion-spec/ingestion-spec.tsx
@@ -20,8 +20,8 @@ import { Code } from '@blueprintjs/core';
import { range } from 'd3-array';
import React from 'react';
-import { AutoForm, ExternalLink, Field } from '../components';
-import { getLink } from '../links';
+import { AutoForm, ExternalLink, Field } from '../../components';
+import { getLink } from '../../links';
import {
allowKeys,
deepDelete,
@@ -35,25 +35,28 @@ import {
oneOf,
parseCsvLine,
typeIs,
-} from '../utils';
-import { SampleHeaderAndRows } from '../utils/sampler';
-
+} from '../../utils';
+import { SampleHeaderAndRows } from '../../utils/sampler';
import {
DimensionsSpec,
getDimensionSpecName,
getDimensionSpecs,
getDimensionSpecType,
-} from './dimension-spec';
-import { InputFormat, issueWithInputFormat } from './input-format';
-import { InputSource, issueWithInputSource } from './input-source';
+} from '../dimension-spec/dimension-spec';
+import { InputFormat, issueWithInputFormat } from '../input-format/input-format';
+import {
+ FILTER_SUGGESTIONS,
+ InputSource,
+ issueWithInputSource,
+} from '../input-source/input-source';
import {
getMetricSpecOutputType,
getMetricSpecs,
getMetricSpecSingleFieldName,
MetricSpec,
-} from './metric-spec';
-import { TimestampSpec } from './timestamp-spec';
-import { TransformSpec } from './transform-spec';
+} from '../metric-spec/metric-spec';
+import { TimestampSpec } from '../timestamp-spec/timestamp-spec';
+import { TransformSpec } from '../transform-spec/transform-spec';
export const MAX_INLINE_DATA_LENGTH = 65536;
@@ -349,7 +352,7 @@ export function cleanSpec(
) as IngestionSpec;
}
-export function upgradeSpec(spec: any): Partial<IngestionSpec> {
+export function upgradeSpec(spec: any, yolo = false): Partial<IngestionSpec> {
spec = nestSpecIfNeeded(spec);
// Upgrade firehose if exists
@@ -370,6 +373,15 @@ export function upgradeSpec(spec: any): Partial<IngestionSpec> {
// Decompose parser if exists
if (deepGet(spec, 'spec.dataSchema.parser')) {
+ if (!yolo) {
+ const parserType = deepGet(spec, 'spec.dataSchema.parser.type');
+ if (parserType !== 'string') {
+ throw new Error(
+ `Can not rewrite parser of type '${parserType}', only 'string' is supported`,
+ );
+ }
+ }
+
spec = deepMove(
spec,
'spec.dataSchema.parser.parseSpec.timestampSpec',
@@ -442,6 +454,24 @@ export function getIoConfigFormFields(ingestionComboType: IngestionComboType): F
),
};
+ const inputSourceFilter: Field<IoConfig> = {
+ name: 'inputSource.filter',
+ label: 'File filter',
+ type: 'string',
+ suggestions: FILTER_SUGGESTIONS,
+ placeholder: '*',
+ info: (
+ <p>
+ A wildcard filter for files. See{' '}
+ <ExternalLink href="https://commons.apache.org/proper/commons-io/apidocs/org/apache/commons/io/filefilter/WildcardFileFilter.html">
+ here
+ </ExternalLink>{' '}
+ for format information. Files matching the filter criteria are considered for ingestion.
+ Files not matching the filter criteria are ignored.
+ </p>
+ ),
+ };
+
switch (ingestionComboType) {
case 'index_parallel:http':
return [
@@ -499,16 +529,7 @@ export function getIoConfigFormFields(ingestionComboType: IngestionComboType): F
label: 'File filter',
type: 'string',
required: true,
- suggestions: [
- '*',
- '*.json',
- '*.json.gz',
- '*.csv',
- '*.tsv',
- '*.parquet',
- '*.orc',
- '*.avro',
- ],
+ suggestions: FILTER_SUGGESTIONS,
info: (
<>
<ExternalLink
@@ -521,7 +542,8 @@ export function getIoConfigFormFields(ingestionComboType: IngestionComboType): F
<ExternalLink href="https://commons.apache.org/proper/commons-io/apidocs/org/apache/commons/io/filefilter/WildcardFileFilter.html">
here
</ExternalLink>{' '}
- for format information.
+ for format information. Files matching the filter criteria are considered for
+ ingestion. Files not matching the filter criteria are ignored.
</p>
</>
),
@@ -629,7 +651,7 @@ export function getIoConfigFormFields(ingestionComboType: IngestionComboType): F
</>
),
},
-
+ inputSourceFilter,
{
name: 'inputSource.properties.accessKeyId.type',
label: 'Access key ID type',
@@ -687,7 +709,7 @@ export function getIoConfigFormFields(ingestionComboType: IngestionComboType): F
{
name: 'inputSource.properties.secretAccessKey.type',
- label: 'Secret key type',
+ label: 'Secret access key type',
type: 'string',
suggestions: [undefined, 'environment', 'default'],
placeholder: '(none)',
@@ -708,7 +730,7 @@ export function getIoConfigFormFields(ingestionComboType: IngestionComboType): F
},
{
name: 'inputSource.properties.secretAccessKey.variable',
- label: 'Secret key value',
+ label: 'Secret access key environment variable',
type: 'string',
placeholder: '(environment variable name)',
defined: ioConfig =>
@@ -717,7 +739,7 @@ export function getIoConfigFormFields(ingestionComboType: IngestionComboType): F
},
{
name: 'inputSource.properties.secretAccessKey.password',
- label: 'Secret key value',
+ label: 'Secret access key value',
type: 'string',
placeholder: '(secret key)',
defined: ioConfig =>
@@ -791,6 +813,7 @@ export function getIoConfigFormFields(ingestionComboType: IngestionComboType): F
</>
),
},
+ inputSourceFilter,
];
case 'index_parallel:google':
@@ -849,6 +872,7 @@ export function getIoConfigFormFields(ingestionComboType: IngestionComboType): F
</>
),
},
+ inputSourceFilter,
];
case 'index_parallel:hdfs':
@@ -1277,9 +1301,9 @@ function filterIsFilename(filter: string): boolean {
}
function filenameFromPath(path: string): string | undefined {
- const m = /([^/.]+)[^/]*?\/?$/.exec(path);
- if (!m) return;
- return m[1];
+ const parts = path.split('/');
+ while (parts.length && !parts[parts.length - 1]) parts.pop();
+ return parts.length ? basenameFromFilename(parts[parts.length - 1]) : undefined;
}
function basenameFromFilename(filename: string): string | undefined {
@@ -1751,6 +1775,24 @@ const TUNING_FORM_FIELDS: Field<IngestionSpec>[] = [
placeholder: 'Default: 1/6 of max JVM memory',
info: <>Used in determining when intermediate persists to disk should occur.</>,
},
+ {
+ name: 'spec.tuningConfig.maxColumnsToMerge',
+ type: 'number',
+ defaultValue: -1,
+ min: -1,
+ hideInMore: true,
+ info: (
+ <>
+ <p>
+ Limit of the number of segments to merge in a single phase when merging segments for
+ publishing. This limit affects the total number of columns present in a set of segments to
+ merge. If the limit is exceeded, segment merging occurs in multiple phases. Druid merges
+ at least 2 segments per phase, regardless of this setting.
+ </p>
+ <p>Default: -1 (unlimited)</p>
+ </>
+ ),
+ },
{
name: 'spec.tuningConfig.resetOffsetAutomatically',
type: 'boolean',
@@ -2303,6 +2345,9 @@ export function guessColumnTypeFromInput(
// If we see any arrays in the input this is a multi-value dimension that must be a string
if (definedValues.some(v => Array.isArray(v))) return 'string';
+ // If we see any JSON objects in the input assume COMPLEX<json>
+ if (definedValues.some(v => v && typeof v === 'object')) return 'COMPLEX<json>';
+
if (
definedValues.every(v => {
return (
diff --git a/web-console/src/druid-models/input-format.tsx b/web-console/src/druid-models/input-format/input-format.tsx
similarity index 93%
rename from web-console/src/druid-models/input-format.tsx
rename to web-console/src/druid-models/input-format/input-format.tsx
index 90fb8b800d..61ac6eda14 100644
--- a/web-console/src/druid-models/input-format.tsx
+++ b/web-console/src/druid-models/input-format/input-format.tsx
@@ -19,11 +19,10 @@
import { Code } from '@blueprintjs/core';
import React from 'react';
-import { AutoForm, ExternalLink, Field } from '../components';
-import { getLink } from '../links';
-import { oneOf, typeIs } from '../utils';
-
-import { FlattenSpec } from './flatten-spec';
+import { AutoForm, ExternalLink, Field } from '../../components';
+import { getLink } from '../../links';
+import { oneOf, typeIs } from '../../utils';
+import { FlattenSpec } from '../flatten-spec/flatten-spec';
export interface InputFormat {
readonly type: string;
@@ -31,7 +30,7 @@ export interface InputFormat {
readonly skipHeaderRows?: number;
readonly columns?: string[];
readonly delimiter?: string;
- readonly listDelimiter?: string;
+ readonly listDelimiter?: string | null;
readonly pattern?: string;
readonly function?: string;
readonly flattenSpec?: FlattenSpec | null;
@@ -44,7 +43,7 @@ export const INPUT_FORMAT_FIELDS: Field<InputFormat>[] = [
name: 'type',
label: 'Input format',
type: 'string',
- suggestions: ['json', 'csv', 'tsv', 'regex', 'parquet', 'orc', 'avro_ocf', 'avro_stream'],
+ suggestions: ['json', 'csv', 'tsv', 'parquet', 'orc', 'avro_ocf', 'avro_stream', 'regex'],
required: true,
info: (
<>
diff --git a/web-console/src/druid-models/input-source.tsx b/web-console/src/druid-models/input-source.tsx
deleted file mode 100644
index eb989de4f0..0000000000
--- a/web-console/src/druid-models/input-source.tsx
+++ /dev/null
@@ -1,91 +0,0 @@
-/*
- * 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 { nonEmptyArray } from '../utils';
-
-export interface InputSource {
- type: string;
- baseDir?: string;
- filter?: any;
- uris?: string[];
- prefixes?: string[];
- objects?: { bucket: string; path: string }[];
- fetchTimeout?: number;
-
- // druid
- dataSource?: string;
- interval?: string;
- dimensions?: string[];
- metrics?: string[];
- maxInputSegmentBytesPerTask?: number;
-
- // inline
- data?: string;
-
- // hdfs
- paths?: string;
-
- // http
- httpAuthenticationUsername?: any;
- httpAuthenticationPassword?: any;
-}
-
-export function issueWithInputSource(inputSource: InputSource | undefined): string | undefined {
- if (!inputSource) return 'does not exist';
- if (!inputSource.type) return 'missing a type';
- switch (inputSource.type) {
- case 'local':
- if (!inputSource.baseDir) return `must have a 'baseDir'`;
- if (!inputSource.filter) return `must have a 'filter'`;
- break;
-
- case 'http':
- if (!nonEmptyArray(inputSource.uris)) {
- return 'must have at least one uri';
- }
- break;
-
- case 'druid':
- if (!inputSource.dataSource) return `must have a 'dataSource'`;
- if (!inputSource.interval) return `must have an 'interval'`;
- break;
-
- case 'inline':
- if (!inputSource.data) return `must have 'data'`;
- break;
-
- case 's3':
- case 'azure':
- case 'google':
- if (
- !nonEmptyArray(inputSource.uris) &&
- !nonEmptyArray(inputSource.prefixes) &&
- !nonEmptyArray(inputSource.objects)
- ) {
- return 'must have at least one uri or prefix or object';
- }
- break;
-
- case 'hdfs':
- if (!inputSource.paths) {
- return 'must have paths';
- }
- break;
- }
- return;
-}
diff --git a/web-console/src/druid-models/input-source/input-source.tsx b/web-console/src/druid-models/input-source/input-source.tsx
new file mode 100644
index 0000000000..7f5ba63793
--- /dev/null
+++ b/web-console/src/druid-models/input-source/input-source.tsx
@@ -0,0 +1,590 @@
+/*
+ * 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 React from 'react';
+
+import { ExternalLink, Field } from '../../components';
+import { getLink } from '../../links';
+import { deepGet, deepSet, nonEmptyArray, typeIs } from '../../utils';
+
+export const FILTER_SUGGESTIONS: string[] = [
+ '*',
+ '*.json',
+ '*.json.gz',
+ '*.csv',
+ '*.tsv',
+ '*.parquet',
+ '*.orc',
+ '*.avro',
+];
+
+export interface InputSource {
+ type: string;
+ baseDir?: string;
+ filter?: any;
+ uris?: string[];
+ prefixes?: string[];
+ objects?: { bucket: string; path: string }[];
+ fetchTimeout?: number;
+
+ // druid
+ dataSource?: string;
+ interval?: string;
+ dimensions?: string[];
+ metrics?: string[];
+ maxInputSegmentBytesPerTask?: number;
+
+ // inline
+ data?: string;
+
+ // hdfs
+ paths?: string | string[];
+
+ // http
+ httpAuthenticationUsername?: any;
+ httpAuthenticationPassword?: any;
+}
+
+export type InputSourceDesc =
+ | {
+ type: 'inline';
+ data: string;
+ }
+ | {
+ type: 'local';
+ filter?: any;
+ baseDir?: string;
+ files?: string[];
+ }
+ | {
+ type: 'druid';
+ dataSource: string;
+ interval: string;
+ filter?: any;
+ dimensions?: string[]; // ToDo: these are not in the docs https://druid.apache.org/docs/latest/ingestion/native-batch-input-sources.html
+ metrics?: string[];
+ maxInputSegmentBytesPerTask?: number;
+ }
+ | {
+ type: 'http';
+ uris: string[];
+ httpAuthenticationUsername?: any;
+ httpAuthenticationPassword?: any;
+ }
+ | {
+ type: 's3';
+ uris?: string[];
+ prefixes?: string[];
+ objects?: { bucket: string; path: string }[];
+ properties?: {
+ accessKeyId?: any;
+ secretAccessKey?: any;
+ assumeRoleArn?: any;
+ assumeRoleExternalId?: any;
+ };
+ }
+ | {
+ type: 'google' | 'azure';
+ uris?: string[];
+ prefixes?: string[];
+ objects?: { bucket: string; path: string }[];
+ }
+ | {
+ type: 'hdfs';
+ paths?: string | string[];
+ }
+ | {
+ type: 'sql';
+ database: any;
+ foldCase?: boolean;
+ sqls: string[];
+ }
+ | {
+ type: 'combining';
+ delegates: InputSource[];
+ };
+
+export function issueWithInputSource(inputSource: InputSource | undefined): string | undefined {
+ if (!inputSource) return 'does not exist';
+ if (!inputSource.type) return 'missing a type';
+ switch (inputSource.type) {
+ case 'local':
+ if (!inputSource.baseDir) return `must have a 'baseDir'`;
+ if (!inputSource.filter) return `must have a 'filter'`;
+ return;
+
+ case 'http':
+ if (!nonEmptyArray(inputSource.uris)) {
+ return 'must have at least one uri';
+ }
+ return;
+
+ case 'druid':
+ if (!inputSource.dataSource) return `must have a 'dataSource'`;
+ if (!inputSource.interval) return `must have an 'interval'`;
+ return;
+
+ case 'inline':
+ if (!inputSource.data) return `must have 'data'`;
+ return;
+
+ case 's3':
+ case 'azure':
+ case 'google':
+ if (
+ !nonEmptyArray(inputSource.uris) &&
+ !nonEmptyArray(inputSource.prefixes) &&
+ !nonEmptyArray(inputSource.objects)
+ ) {
+ return 'must have at least one uri or prefix or object';
+ }
+ return;
+
+ case 'hdfs':
+ if (!inputSource.paths) {
+ return 'must have paths';
+ }
+ return;
+
+ default:
+ return;
+ }
+}
+
+export const INPUT_SOURCE_FIELDS: Field<InputSource>[] = [
+ // inline
+
+ {
+ name: 'data',
+ label: 'Inline data',
+ type: 'string',
+ defined: typeIs('inline'),
+ required: true,
+ placeholder: 'Paste your data here',
+ multiline: true,
+ info: <p>Put you inline data here</p>,
+ },
+
+ // http
+
+ {
+ name: 'uris',
+ label: 'URIs',
+ type: 'string-array',
+ placeholder: 'https://example.com/path/to/file1.ext, https://example.com/path/to/file2.ext',
+ defined: typeIs('http'),
+ required: true,
+ info: (
+ <p>
+ The full URI of your file. To ingest from multiple URIs, use commas to separate each
+ individual URI.
+ </p>
+ ),
+ },
+ {
+ name: 'httpAuthenticationUsername',
+ label: 'HTTP auth username',
+ type: 'string',
+ defined: typeIs('http'),
+ placeholder: '(optional)',
+ info: <p>Username to use for authentication with specified URIs</p>,
+ },
+ {
+ name: 'httpAuthenticationPassword',
+ label: 'HTTP auth password',
+ type: 'string',
+ defined: typeIs('http'),
+ placeholder: '(optional)',
+ info: <p>Password to use for authentication with specified URIs</p>,
+ },
+
+ // local
+
+ {
+ name: 'baseDir',
+ label: 'Base directory',
+ type: 'string',
+ placeholder: '/path/to/files/',
+ defined: typeIs('local'),
+ required: true,
+ info: (
+ <>
+ <ExternalLink href={`${getLink('DOCS')}/ingestion/native-batch.html#input-sources`}>
+ baseDir
+ </ExternalLink>
+ <p>Specifies the directory to search recursively for files to be ingested.</p>
+ </>
+ ),
+ },
+ {
+ name: 'filter',
+ label: 'File filter',
+ type: 'string',
+ defined: typeIs('local'),
+ required: true,
+ suggestions: FILTER_SUGGESTIONS,
+ info: (
+ <>
+ <ExternalLink href={`${getLink('DOCS')}/ingestion/native-batch.html#local-input-source`}>
+ filter
+ </ExternalLink>
+ <p>
+ A wildcard filter for files. See{' '}
+ <ExternalLink href="https://commons.apache.org/proper/commons-io/apidocs/org/apache/commons/io/filefilter/WildcardFileFilter.html">
+ here
+ </ExternalLink>{' '}
+ for format information.
+ </p>
+ </>
+ ),
+ },
+
+ // s3
+
+ {
+ name: 'uris',
+ label: 'S3 URIs',
+ type: 'string-array',
+ placeholder: 's3://your-bucket/some-file1.ext, s3://your-bucket/some-file2.ext',
+ defined: inputSource =>
+ inputSource.type === 's3' &&
+ !deepGet(inputSource, 'prefixes') &&
+ !deepGet(inputSource, 'objects'),
+ required: true,
+ info: (
+ <>
+ <p>
+ The full S3 URI of your file. To ingest from multiple URIs, use commas to separate each
+ individual URI.
+ </p>
+ <p>Either S3 URIs or prefixes or objects must be set.</p>
+ </>
+ ),
+ },
+ {
+ name: 'prefixes',
+ label: 'S3 prefixes',
+ type: 'string-array',
+ placeholder: 's3://your-bucket/some-path1, s3://your-bucket/some-path2',
+ defined: inputSource =>
+ inputSource.type === 's3' &&
+ !deepGet(inputSource, 'uris') &&
+ !deepGet(inputSource, 'objects'),
+ required: true,
+ info: (
+ <>
+ <p>A list of paths (with bucket) where your files are stored.</p>
+ <p>Either S3 URIs or prefixes or objects must be set.</p>
+ </>
+ ),
+ },
+ {
+ name: 'objects',
+ label: 'S3 objects',
+ type: 'json',
+ placeholder: '{"bucket":"your-bucket", "path":"some-file.ext"}',
+ defined: inputSource => inputSource.type === 's3' && deepGet(inputSource, 'objects'),
+ required: true,
+ info: (
+ <>
+ <p>
+ JSON array of{' '}
+ <ExternalLink href={`${getLink('DOCS')}/development/extensions-core/s3.html`}>
+ S3 Objects
+ </ExternalLink>
+ .
+ </p>
+ <p>Either S3 URIs or prefixes or objects must be set.</p>
+ </>
+ ),
+ },
+
+ // azure
+
+ {
+ name: 'uris',
+ label: 'Azure URIs',
+ type: 'string-array',
+ placeholder: 'azure://your-container/some-file1.ext, azure://your-container/some-file2.ext',
+ defined: inputSource =>
+ inputSource.type === 'azure' &&
+ !deepGet(inputSource, 'prefixes') &&
+ !deepGet(inputSource, 'objects'),
+ required: true,
+ info: (
+ <>
+ <p>
+ The full Azure URI of your file. To ingest from multiple URIs, use commas to separate each
+ individual URI.
+ </p>
+ <p>Either Azure URIs or prefixes or objects must be set.</p>
+ </>
+ ),
+ },
+ {
+ name: 'prefixes',
+ label: 'Azure prefixes',
+ type: 'string-array',
+ placeholder: 'azure://your-container/some-path1, azure://your-container/some-path2',
+ defined: inputSource =>
+ inputSource.type === 'azure' &&
+ !deepGet(inputSource, 'uris') &&
+ !deepGet(inputSource, 'objects'),
+ required: true,
+ info: (
+ <>
+ <p>A list of paths (with bucket) where your files are stored.</p>
+ <p>Either Azure URIs or prefixes or objects must be set.</p>
+ </>
+ ),
+ },
+ {
+ name: 'objects',
+ label: 'Azure objects',
+ type: 'json',
+ placeholder: '{"bucket":"your-container", "path":"some-file.ext"}',
+ defined: inputSource => inputSource.type === 'azure' && deepGet(inputSource, 'objects'),
+ required: true,
+ info: (
+ <>
+ <p>
+ JSON array of{' '}
+ <ExternalLink href={`${getLink('DOCS')}/development/extensions-core/azure.html`}>
+ S3 Objects
+ </ExternalLink>
+ .
+ </p>
+ <p>Either Azure URIs or prefixes or objects must be set.</p>
+ </>
+ ),
+ },
+
+ // google
+ {
+ name: 'uris',
+ label: 'Google Cloud Storage URIs',
+ type: 'string-array',
+ placeholder: 'gs://your-bucket/some-file1.ext, gs://your-bucket/some-file2.ext',
+ defined: inputSource =>
+ inputSource.type === 'google' &&
+ !deepGet(inputSource, 'prefixes') &&
+ !deepGet(inputSource, 'objects'),
+ required: true,
+ info: (
+ <>
+ <p>
+ The full Google Cloud Storage URI of your file. To ingest from multiple URIs, use commas
+ to separate each individual URI.
+ </p>
+ <p>Either Google Cloud Storage URIs or prefixes or objects must be set.</p>
+ </>
+ ),
+ },
+ {
+ name: 'prefixes',
+ label: 'Google Cloud Storage prefixes',
+ type: 'string-array',
+ placeholder: 'gs://your-bucket/some-path1, gs://your-bucket/some-path2',
+ defined: inputSource =>
+ inputSource.type === 'google' &&
+ !deepGet(inputSource, 'uris') &&
+ !deepGet(inputSource, 'objects'),
+ required: true,
+ info: (
+ <>
+ <p>A list of paths (with bucket) where your files are stored.</p>
+ <p>Either Google Cloud Storage URIs or prefixes or objects must be set.</p>
+ </>
+ ),
+ },
+ {
+ name: 'objects',
+ label: 'Google Cloud Storage objects',
+ type: 'json',
+ placeholder: '{"bucket":"your-bucket", "path":"some-file.ext"}',
+ defined: inputSource => inputSource.type === 'google' && deepGet(inputSource, 'objects'),
+ required: true,
+ info: (
+ <>
+ <p>
+ JSON array of{' '}
+ <ExternalLink href={`${getLink('DOCS')}/development/extensions-core/google.html`}>
+ Google Cloud Storage Objects
+ </ExternalLink>
+ .
+ </p>
+ <p>Either Google Cloud Storage URIs or prefixes or objects must be set.</p>
+ </>
+ ),
+ },
+
+ // Cloud common
+ {
+ name: 'filter',
+ label: 'File filter',
+ type: 'string',
+ suggestions: FILTER_SUGGESTIONS,
+ placeholder: '*',
+ defined: typeIs('s3', 'azure', 'google'),
+ info: (
+ <p>
+ A wildcard filter for files. See{' '}
+ <ExternalLink href="https://commons.apache.org/proper/commons-io/apidocs/org/apache/commons/io/filefilter/WildcardFileFilter.html">
+ here
+ </ExternalLink>{' '}
+ for format information. Files matching the filter criteria are considered for ingestion.
+ Files not matching the filter criteria are ignored.
+ </p>
+ ),
+ },
+
+ // S3 auth extra
+ {
+ name: 'properties.accessKeyId.type',
+ label: 'Access key ID type',
+ type: 'string',
+ suggestions: [undefined, 'environment', 'default'],
+ placeholder: '(none)',
+ defined: typeIs('s3'),
+ info: (
+ <>
+ <p>S3 access key type.</p>
+ <p>Setting this will override the default configuration provided in the config.</p>
+ <p>
+ The access key can be pulled from an environment variable or inlined in the ingestion spec
+ (default).
+ </p>
+ <p>
+ Note: Inlining the access key into the ingestion spec is dangerous as it might appear in
+ server log files and can be seen by anyone accessing this console.
+ </p>
+ </>
+ ),
+ adjustment: inputSource => {
+ return deepSet(
+ inputSource,
+ 'properties.secretAccessKey.type',
+ deepGet(inputSource, 'properties.accessKeyId.type'),
+ );
+ },
+ },
+ {
+ name: 'properties.accessKeyId.variable',
+ label: 'Access key ID environment variable',
+ type: 'string',
+ placeholder: '(environment variable name)',
+ defined: inputSource =>
+ inputSource.type === 's3' &&
+ deepGet(inputSource, 'properties.accessKeyId.type') === 'environment',
+ info: <p>The environment variable containing the S3 access key for this S3 bucket.</p>,
+ },
+ {
+ name: 'properties.accessKeyId.password',
+ label: 'Access key ID value',
+ type: 'string',
+ placeholder: '(access key)',
+ defined: inputSource =>
+ inputSource.type === 's3' &&
+ deepGet(inputSource, 'properties.accessKeyId.type') === 'default',
+ info: (
+ <>
+ <p>S3 access key for this S3 bucket.</p>
+ <p>
+ Note: Inlining the access key into the ingestion spec is dangerous as it might appear in
+ server log files and can be seen by anyone accessing this console.
+ </p>
+ </>
+ ),
+ },
+ {
+ name: 'properties.secretAccessKey.type',
+ label: 'Secret access key type',
+ type: 'string',
+ suggestions: [undefined, 'environment', 'default'],
+ placeholder: '(none)',
+ defined: typeIs('s3'),
+ info: (
+ <>
+ <p>S3 secret key type.</p>
+ <p>Setting this will override the default configuration provided in the config.</p>
+ <p>
+ The secret key can be pulled from an environment variable or inlined in the ingestion spec
+ (default).
+ </p>
+ <p>
+ Note: Inlining the secret key into the ingestion spec is dangerous as it might appear in
+ server log files and can be seen by anyone accessing this console.
+ </p>
+ </>
+ ),
+ },
+ {
+ name: 'properties.secretAccessKey.variable',
+ label: 'Secret access key environment variable',
+ type: 'string',
+ placeholder: '(environment variable name)',
+ defined: inputSource =>
+ deepGet(inputSource, 'properties.secretAccessKey.type') === 'environment',
+ info: <p>The environment variable containing the S3 secret key for this S3 bucket.</p>,
+ },
+ {
+ name: 'properties.secretAccessKey.password',
+ label: 'Secret access key value',
+ type: 'string',
+ placeholder: '(secret key)',
+ defined: inputSource => deepGet(inputSource, 'properties.secretAccessKey.type') === 'default',
+ info: (
+ <>
+ <p>S3 secret key for this S3 bucket.</p>
+ <p>
+ Note: Inlining the access key into the ingestion spec is dangerous as it might appear in
+ server log files and can be seen by anyone accessing this console.
+ </p>
+ </>
+ ),
+ },
+
+ // hdfs
+ {
+ name: 'paths',
+ label: 'Paths',
+ type: 'string',
+ placeholder: '/path/to/file.ext',
+ defined: typeIs('hdfs'),
+ required: true,
+ },
+
+ // sql
+ {
+ name: 'database.type',
+ label: 'Database type',
+ type: 'string',
+ suggestions: ['mysql', 'postgresql'],
+ defined: typeIs('sql'),
+ required: true,
+ info: (
+ <>
+ <p>
+ The full Google Cloud Storage URI of your file. To ingest from multiple URIs, use commas
+ to separate each individual URI.
+ </p>
+ <p>Either Google Cloud Storage URIs or prefixes or objects must be set.</p>
+ </>
+ ),
+ },
+];
diff --git a/web-console/src/druid-models/lookup-spec.spec.ts b/web-console/src/druid-models/lookup-spec/lookup-spec.spec.ts
similarity index 100%
rename from web-console/src/druid-models/lookup-spec.spec.ts
rename to web-console/src/druid-models/lookup-spec/lookup-spec.spec.ts
diff --git a/web-console/src/druid-models/lookup-spec.tsx b/web-console/src/druid-models/lookup-spec/lookup-spec.tsx
similarity index 99%
rename from web-console/src/druid-models/lookup-spec.tsx
rename to web-console/src/druid-models/lookup-spec/lookup-spec.tsx
index 67f12b6d39..b1328a8f12 100644
--- a/web-console/src/druid-models/lookup-spec.tsx
+++ b/web-console/src/druid-models/lookup-spec/lookup-spec.tsx
@@ -19,8 +19,8 @@
import { Code } from '@blueprintjs/core';
import React from 'react';
-import { AutoForm, Field } from '../components';
-import { deepGet, deepSet, oneOf, pluralIfNeeded, typeIs } from '../utils';
+import { AutoForm, Field } from '../../components';
+import { deepGet, deepSet, oneOf, pluralIfNeeded, typeIs } from '../../utils';
export interface ExtractionNamespaceSpec {
readonly type: string;
diff --git a/web-console/src/druid-models/metric-spec.spec.ts b/web-console/src/druid-models/metric-spec/metric-spec.spec.ts
similarity index 97%
rename from web-console/src/druid-models/metric-spec.spec.ts
rename to web-console/src/druid-models/metric-spec/metric-spec.spec.ts
index 70e0d5051c..e30b806344 100644
--- a/web-console/src/druid-models/metric-spec.spec.ts
+++ b/web-console/src/druid-models/metric-spec/metric-spec.spec.ts
@@ -16,8 +16,9 @@
* limitations under the License.
*/
+import { JSON_SAMPLE } from '../../utils/sampler.mock';
+
import { getMetricSpecs } from './metric-spec';
-import { JSON_SAMPLE } from './test-fixtures';
describe('metric-spec', () => {
describe('getMetricSecs', () => {
diff --git a/web-console/src/druid-models/metric-spec.tsx b/web-console/src/druid-models/metric-spec/metric-spec.tsx
similarity index 97%
rename from web-console/src/druid-models/metric-spec.tsx
rename to web-console/src/druid-models/metric-spec/metric-spec.tsx
index 3aa9c7b19c..6b3290272e 100644
--- a/web-console/src/druid-models/metric-spec.tsx
+++ b/web-console/src/druid-models/metric-spec/metric-spec.tsx
@@ -19,12 +19,11 @@
import { Code } from '@blueprintjs/core';
import React from 'react';
-import { ExternalLink, Field } from '../components';
-import { getLink } from '../links';
-import { filterMap, typeIs } from '../utils';
-import { SampleHeaderAndRows } from '../utils/sampler';
-
-import { guessColumnTypeFromHeaderAndRows } from './ingestion-spec';
+import { ExternalLink, Field } from '../../components';
+import { getLink } from '../../links';
+import { filterMap, typeIs } from '../../utils';
+import { SampleHeaderAndRows } from '../../utils/sampler';
+import { guessColumnTypeFromHeaderAndRows } from '../ingestion-spec/ingestion-spec';
export interface MetricSpec {
readonly type: string;
diff --git a/web-console/src/components/table-cell-unparseable/table-cell-unparseable.scss b/web-console/src/druid-models/mocks.ts
similarity index 87%
copy from web-console/src/components/table-cell-unparseable/table-cell-unparseable.scss
copy to web-console/src/druid-models/mocks.ts
index 43efe5085b..a07429577e 100644
--- a/web-console/src/components/table-cell-unparseable/table-cell-unparseable.scss
+++ b/web-console/src/druid-models/mocks.ts
@@ -16,6 +16,5 @@
* limitations under the License.
*/
-.table-cell-unparseable {
- color: #9e2b0e;
-}
+export * from './execution/execution-ingest-complete.mock';
+export * from './execution/execution-ingest-error.mock';
diff --git a/web-console/src/druid-models/overlord-dynamic-config.tsx b/web-console/src/druid-models/overlord-dynamic-config/overlord-dynamic-config.tsx
similarity index 96%
rename from web-console/src/druid-models/overlord-dynamic-config.tsx
rename to web-console/src/druid-models/overlord-dynamic-config/overlord-dynamic-config.tsx
index 14cac82951..2b090acd9d 100644
--- a/web-console/src/druid-models/overlord-dynamic-config.tsx
+++ b/web-console/src/druid-models/overlord-dynamic-config/overlord-dynamic-config.tsx
@@ -16,7 +16,7 @@
* limitations under the License.
*/
-import { Field } from '../components';
+import { Field } from '../../components';
export interface OverlordDynamicConfig {
selectStrategy?: Record<string, any>;
diff --git a/web-console/src/druid-models/query-context/query-context.tsx b/web-console/src/druid-models/query-context/query-context.tsx
new file mode 100644
index 0000000000..f915186285
--- /dev/null
+++ b/web-console/src/druid-models/query-context/query-context.tsx
@@ -0,0 +1,217 @@
+/*
+ * 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 { deepDelete, deepSet } from '../../utils';
+
+export interface QueryContext {
+ useCache?: boolean;
+ populateCache?: boolean;
+ useApproximateCountDistinct?: boolean;
+ useApproximateTopN?: boolean;
+ sqlTimeZone?: string;
+
+ // Multi-stage query
+ maxNumTasks?: number;
+ finalizeAggregations?: boolean;
+ durableShuffleStorage?: boolean;
+ maxParseExceptions?: number;
+ groupByEnableMultiValueUnnesting?: boolean;
+
+ [key: string]: any;
+}
+
+export interface QueryWithContext {
+ queryString: string;
+ queryContext?: QueryContext;
+ wrapQueryLimit?: number;
+}
+
+export function isEmptyContext(context: QueryContext | undefined): boolean {
+ return !context || Object.keys(context).length === 0;
+}
+
+// -----------------------------
+
+export function getUseCache(context: QueryContext): boolean {
+ const { useCache } = context;
+ return typeof useCache === 'boolean' ? useCache : true;
+}
+
+export function changeUseCache(context: QueryContext, useCache: boolean): QueryContext {
+ let newContext = context;
+ if (useCache) {
+ newContext = deepDelete(newContext, 'useCache');
+ newContext = deepDelete(newContext, 'populateCache');
+ } else {
+ newContext = deepSet(newContext, 'useCache', false);
+ newContext = deepSet(newContext, 'populateCache', false);
+ }
+ return newContext;
+}
+
+// -----------------------------
+
+export function getUseApproximateCountDistinct(context: QueryContext): boolean {
+ const { useApproximateCountDistinct } = context;
+ return typeof useApproximateCountDistinct === 'boolean' ? useApproximateCountDistinct : true;
+}
+
+export function changeUseApproximateCountDistinct(
+ context: QueryContext,
+ useApproximateCountDistinct: boolean,
+): QueryContext {
+ if (useApproximateCountDistinct) {
+ return deepDelete(context, 'useApproximateCountDistinct');
+ } else {
+ return deepSet(context, 'useApproximateCountDistinct', false);
+ }
+}
+
+// -----------------------------
+
+export function getUseApproximateTopN(context: QueryContext): boolean {
+ const { useApproximateTopN } = context;
+ return typeof useApproximateTopN === 'boolean' ? useApproximateTopN : true;
+}
+
+export function changeUseApproximateTopN(
+ context: QueryContext,
+ useApproximateTopN: boolean,
+): QueryContext {
+ if (useApproximateTopN) {
+ return deepDelete(context, 'useApproximateTopN');
+ } else {
+ return deepSet(context, 'useApproximateTopN', false);
+ }
+}
+
+// sqlTimeZone
+
+export function getTimezone(context: QueryContext): string | undefined {
+ return context.sqlTimeZone;
+}
+
+export function changeTimezone(context: QueryContext, timezone: string | undefined): QueryContext {
+ if (timezone) {
+ return deepSet(context, 'sqlTimeZone', timezone);
+ } else {
+ return deepDelete(context, 'sqlTimeZone');
+ }
+}
+
+// maxNumTasks
+
+export function getMaxNumTasks(context: QueryContext): number {
+ const { maxNumTasks } = context;
+ return Math.max(typeof maxNumTasks === 'number' ? maxNumTasks : 0, 2);
+}
+
+export function changeMaxNumTasks(
+ context: QueryContext,
+ maxNumTasks: number | undefined,
+): QueryContext {
+ return typeof maxNumTasks === 'number'
+ ? deepSet(context, 'maxNumTasks', maxNumTasks)
+ : deepDelete(context, 'maxNumTasks');
+}
+
+// taskAssignment
+
+export function getTaskAssigment(context: QueryContext): string {
+ const { taskAssignment } = context;
+ return taskAssignment ?? 'max';
+}
+
+export function changeTaskAssigment(
+ context: QueryContext,
+ taskAssignment: string | undefined,
+): QueryContext {
+ return typeof taskAssignment === 'string'
+ ? deepSet(context, 'taskAssignment', taskAssignment)
+ : deepDelete(context, 'taskAssignment');
+}
+
+// finalizeAggregations
+
+export function getFinalizeAggregations(context: QueryContext): boolean | undefined {
+ const { finalizeAggregations } = context;
+ return typeof finalizeAggregations === 'boolean' ? finalizeAggregations : undefined;
+}
+
+export function changeFinalizeAggregations(
+ context: QueryContext,
+ finalizeAggregations: boolean | undefined,
+): QueryContext {
+ return typeof finalizeAggregations === 'boolean'
+ ? deepSet(context, 'finalizeAggregations', finalizeAggregations)
+ : deepDelete(context, 'finalizeAggregations');
+}
+
+// finalizeAggregations
+
+export function getGroupByEnableMultiValueUnnesting(context: QueryContext): boolean | undefined {
+ const { groupByEnableMultiValueUnnesting } = context;
+ return typeof groupByEnableMultiValueUnnesting === 'boolean'
+ ? groupByEnableMultiValueUnnesting
+ : undefined;
+}
+
+export function changeGroupByEnableMultiValueUnnesting(
+ context: QueryContext,
+ groupByEnableMultiValueUnnesting: boolean | undefined,
+): QueryContext {
+ return typeof groupByEnableMultiValueUnnesting === 'boolean'
+ ? deepSet(context, 'groupByEnableMultiValueUnnesting', groupByEnableMultiValueUnnesting)
+ : deepDelete(context, 'groupByEnableMultiValueUnnesting');
+}
+
+// durableShuffleStorage
+
+export function getDurableShuffleStorage(context: QueryContext): boolean {
+ const { durableShuffleStorage } = context;
+ return Boolean(durableShuffleStorage);
+}
+
+export function changeDurableShuffleStorage(
+ context: QueryContext,
+ durableShuffleStorage: boolean,
+): QueryContext {
+ if (durableShuffleStorage) {
+ return deepSet(context, 'durableShuffleStorage', true);
+ } else {
+ return deepDelete(context, 'durableShuffleStorage');
+ }
+}
+
+// maxParseExceptions
+
+export function getMaxParseExceptions(context: QueryContext): number {
+ const { maxParseExceptions } = context;
+ return Number(maxParseExceptions) || 0;
+}
+
+export function changeMaxParseExceptions(
... 22691 lines suppressed ...
---------------------------------------------------------------------
To unsubscribe, e-mail: commits-unsubscribe@druid.apache.org
For additional commands, e-mail: commits-help@druid.apache.org