diff --git a/licenses.yaml b/licenses.yaml index 226062861c16..6c226d224255 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 000000000000..a2c2649deec2 --- /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 fb525811122e..1fa83c3672f7 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 { - 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 7c3b8cb3d7a6..3d1a7f2997b0 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 7bc1837f6103..37586a22bb7f 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 6feeebfa9c05..b12909c81527 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/workbench/overview.ts b/web-console/e2e-tests/component/workbench/overview.ts new file mode 100644 index 000000000000..5447daf8f8f2 --- /dev/null +++ b/web-console/e2e-tests/component/workbench/overview.ts @@ -0,0 +1,47 @@ +/* + * 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 { clickButton } from '../../util/playwright'; +import { extractTable } from '../../util/table'; + +/** + * Represents the workbench tab. + */ +export class WorkbenchOverview { + private readonly page: playwright.Page; + private readonly baseUrl: string; + + constructor(page: playwright.Page, unifiedConsoleUrl: string) { + this.page = page; + this.baseUrl = unifiedConsoleUrl + '#workbench'; + } + + async runQuery(query: string): Promise { + await this.page.goto(this.baseUrl); + await this.page.reload({ waitUntil: 'networkidle' }); + + 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 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 000000000000..2766c5c8ffe2 --- /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 accae44e4cf2..bf30e3578efd 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 ce9105cde503..a5af23211d70 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; export const SQL_FUNCTIONS: Record; diff --git a/web-console/package-lock.json b/web-console/package-lock.json index aa9e9e8d3924..649906dc25eb 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 a57f669226d4..e62d6e7e82d0 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 57fba5b81fb7..4258a0e99e4d 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 9f54949b96cd..67651c66a2dc 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 000000000000..c5bf8231d1b0 --- /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 '); + 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 61ff90f26d3b..c54970b17c67 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 9058b95839d0..4c31928064cd 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
{children}
; diff --git a/web-console/src/components/braced-text/braced-text.scss b/web-console/src/components/braced-text/braced-text.scss index 56f649397e49..e9d390c12c9a 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 000000000000..a53da72f6d18 --- /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`] = ` + + Hello world + +`; diff --git a/web-console/src/components/click-to-copy/click-to-copy.spec.tsx b/web-console/src/components/click-to-copy/click-to-copy.spec.tsx new file mode 100644 index 000000000000..d554d392febf --- /dev/null +++ b/web-console/src/components/click-to-copy/click-to-copy.spec.tsx @@ -0,0 +1,31 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { render } from '@testing-library/react'; +import React from 'react'; + +import { ClickToCopy } from './click-to-copy'; + +describe('ClickToCopy', () => { + it('matches snapshot', () => { + const arrayInput = ; + + const { container } = render(arrayInput); + expect(container.firstChild).toMatchSnapshot(); + }); +}); diff --git a/web-console/src/components/click-to-copy/click-to-copy.tsx b/web-console/src/components/click-to-copy/click-to-copy.tsx new file mode 100644 index 000000000000..5d1d2a8cf471 --- /dev/null +++ b/web-console/src/components/click-to-copy/click-to-copy.tsx @@ -0,0 +1,46 @@ +/* + * 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 { Intent } from '@blueprintjs/core'; +import copy from 'copy-to-clipboard'; +import React from 'react'; + +import { AppToaster } from '../../singletons'; + +export interface ClickToCopyProps { + text: string; +} + +export const ClickToCopy = React.memo(function ClickToCopy(props: ClickToCopyProps) { + const { text } = props; + + return ( + { + copy(text, { format: 'text/plain' }); + AppToaster.show({ + message: `'${text}' copied to clipboard`, + intent: Intent.SUCCESS, + }); + }} + > + {text} + + ); +}); 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 dca59389af97..000000000000 --- 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`] = ` -
-
- -
-
-`; - -exports[`DatasourceColumnsTable matches snapshot on init 1`] = ` -
-
- -
-
-`; - -exports[`DatasourceColumnsTable matches snapshot on loading 1`] = ` -
-
- -
-
-`; - -exports[`DatasourceColumnsTable matches snapshot on no data 1`] = ` -
-
- -
-
-`; - -exports[`DatasourceColumnsTable matches snapshot on some data 1`] = ` -
-
- -
-
-`; diff --git a/web-console/src/components/fancy-tab-pane/fancy-tab-pane.scss b/web-console/src/components/fancy-tab-pane/fancy-tab-pane.scss new file mode 100644 index 000000000000..a32e2b463aa4 --- /dev/null +++ b/web-console/src/components/fancy-tab-pane/fancy-tab-pane.scss @@ -0,0 +1,62 @@ +/* + * 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'; + +$side-bar-width: 120px; + +.fancy-tab-pane { + .side-bar { + position: absolute; + top: 0; + left: 0; + width: $side-bar-width; + height: 100%; + border-right: 1px solid #1f2832; + + .tab-button { + width: 100%; + height: 10vh; + cursor: pointer; + display: flex; + flex-direction: column; + border-radius: 0; + + &.active { + background-color: #2c74a8; + } + + .#{$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 000000000000..500c0f24dc74 --- /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 ( +
+
+ {filterMap(tabs, d => { + if (!d) return; + return ( +
+
{children}
+
+ ); +}); 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 1130e7fa7e06..a42c4ca61d16 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 = ( - + ); 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 b715283ca23c..cc7f53463cc5 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`] = ` + + + + multi-stage-query + + } + multiline={false} + popoverProps={Object {}} + selected={false} + shouldDismissPopover={true} + text="Batch - SQL" + /> + + + } + 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} + > + + - - + + + } + 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} + > + + - @@ -224,7 +326,7 @@ exports[`HeaderBar matches snapshot 1`] = ` diff --git a/web-console/src/components/header-bar/header-bar.scss b/web-console/src/components/header-bar/header-bar.scss index 783b472cb848..062768a22c48 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 61650aa7aa9c..2e17be7058b8 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( - {}} />, + {}} />, ); 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 53042d91a22d..2d5b56af4d2f 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 = ( + + + multi-stage-query} + selected={active === 'sql-data-loader'} + /> + + + ); + + const moreViewsMenuActive = oneOf(active, 'lookups'); + const moreViewsMenu = ( + + + + ); const helpMenu = ( @@ -290,13 +341,7 @@ export const HeaderBar = React.memo(function HeaderBar(props: HeaderBarProps) { onClick={() => setOverlordDynamicConfigDialogOpen(true)} disabled={!capabilities.hasOverlordAccess()} /> - + {capabilitiesOverride ? ( @@ -339,28 +384,50 @@ export const HeaderBar = React.memo(function HeaderBar(props: HeaderBarProps) { - - - - { + if (!e.altKey) return; + e.preventDefault(); + location.hash = '#query'; + }} /> + {showSplitDataLoaderMenu ? ( + + + +
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' && } ); }); 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 000000000000..e5efec03c2ee --- /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/dialogs/segments-table-action-dialog/segments-preview-pane/segments-preview-pane.scss b/web-console/src/dialogs/segments-table-action-dialog/segments-preview-pane/segments-preview-pane.scss new file mode 100644 index 000000000000..f072371285f2 --- /dev/null +++ b/web-console/src/dialogs/segments-table-action-dialog/segments-preview-pane/segments-preview-pane.scss @@ -0,0 +1,30 @@ +/* + * 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. + */ + +.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 000000000000..6c5ace1c5fa4 --- /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({ + 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 ( +
+ {recordState.loading && } + {recordState.data && } + {recordState.error && ( +
{recordState.error.message}
+ )} +
+ ); +}); 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 1aa8d9786f54..11b9f617173a 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 3ae476d6753e..c7da95e424f6 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([]); + const [responseState] = useQueryManager({ + 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 ; if (responseState.error) { - return {`Error while loading status: ${responseState.error}`}; + return
{`Error while loading status: ${responseState.error}`}
; } const response = responseState.data; if (!response) return; + const renderModuleFilterableCell = (field: string) => { + return function ModuleFilterableCell(row: { value: any }) { + return ( + + {row.value} + + ); + }; + }; + return (
- Version: {response.version} + Version: {response.version}
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 000000000000..cee116132795 --- /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 ( + +
+ setValue(String(e.target.value).substring(0, maxLength || 280))} + autoFocus + placeholder={placeholder} + /> +
+
+
+
+
+
+ ); +}); 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 7a81495a0876..ac71cd5a2c7f 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 = 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 fe66c4efb675..48c2f3f88a99 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 3303f4fa52de..fba476fdced9 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 da49bc9e1de1..d66b747afb4a 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) => (
+
+
+
+
+
+ + +

+ Please specify where your raw data is located. +

+
+
+
+
+`; + +exports[`LoadDataView matches snapshot streaming 1`] = ` +
+
- Tune parameters +
+ Connect and parse raw data +
+ + + + +
- - - - - -
-
+
+ Transform data and configure schema +
+ + + + + + +
- Verify and submit +
+ Tune parameters +
+ + + + +
- - - +
+ Verify and submit +
+ + + +

- Please specify where your raw data is located + Please specify where your raw data is located.

diff --git a/web-console/src/views/load-data-view/info-messages.tsx b/web-console/src/views/load-data-view/info-messages.tsx index 3377989c1266..7d7235bc3d36 100644 --- a/web-console/src/views/load-data-view/info-messages.tsx +++ b/web-console/src/views/load-data-view/info-messages.tsx @@ -19,12 +19,10 @@ import { Callout, Code, FormGroup } from '@blueprintjs/core'; import React from 'react'; -import { ExternalLink } from '../../components'; +import { ExternalLink, LearnMore } from '../../components'; import { DimensionMode, getIngestionDocLink, IngestionSpec } from '../../druid-models'; import { getLink } from '../../links'; -import { LearnMore } from './learn-more/learn-more'; - export interface ConnectMessageProps { inlineMode: boolean; spec: Partial; diff --git a/web-console/src/views/load-data-view/load-data-view.scss b/web-console/src/views/load-data-view/load-data-view.scss index 53bc7f050077..cec24de2bf3f 100644 --- a/web-console/src/views/load-data-view/load-data-view.scss +++ b/web-console/src/views/load-data-view/load-data-view.scss @@ -48,14 +48,17 @@ $actual-icon-height: 400px; .spec-card { display: inline-block; - width: 480px; - height: 100px; + vertical-align: top; + position: relative; + width: 440px; + height: 120px; margin: 10px; text-align: left; .spec-card-icon { - vertical-align: top; - margin-top: 6px; + position: absolute; + top: 26px; + left: 20px; svg { width: 20px; @@ -67,9 +70,7 @@ $actual-icon-height: 400px; font-size: 16px; font-weight: 600; line-height: 32px; - display: inline-block; - vertical-align: top; - padding-left: 16px; + padding-left: 36px; .spec-card-caption { font-size: 13px; @@ -91,23 +92,13 @@ $actual-icon-height: 400px; @include sunk-panel; display: grid; gap: $thin-padding; - grid-template-columns: 1fr 1fr 1fr 1fr; + grid-template-columns: repeat(auto-fill, minmax(225px, 1fr)); + grid-template-rows: repeat(auto-fill, 150px); padding: 10px; - - @media (max-width: 1200px) { - grid-template-columns: 1fr 1fr 1fr; - } - - @media (max-width: 900px) { - grid-template-columns: 1fr 1fr; - } - - @media (max-width: 600px) { - grid-template-columns: 1fr; - } + height: 100%; } - .#{$bp-ns}-card { + .ingestion-card { position: relative; display: inline-block; vertical-align: top; @@ -166,24 +157,32 @@ $actual-icon-height: 400px; .step-nav { grid-area: navi; - white-space: nowrap; - overflow: auto; - padding: 5px 5px 0 5px; + overflow: hidden; + position: relative; - .step-section { - display: inline-block; - vertical-align: top; - margin-right: $thin-padding; - } + .step-nav-inner { + position: absolute; + width: 100%; + height: 80px; + overflow: auto; + white-space: nowrap; + padding: 5px 5px 0 5px; - .step-nav-l1 { - height: 24px; - font-weight: 600; - color: #eeeeee; - } + .step-section { + display: inline-block; + vertical-align: top; + margin-right: $thin-padding; + } - .step-nav-l2 { - height: 30px; + .step-nav-l1 { + height: 24px; + font-weight: 600; + color: #eeeeee; + } + + .step-nav-l2 { + height: 30px; + } } } diff --git a/web-console/src/views/load-data-view/load-data-view.spec.tsx b/web-console/src/views/load-data-view/load-data-view.spec.tsx index 43b7164c26dd..ad6e48440bde 100644 --- a/web-console/src/views/load-data-view/load-data-view.spec.tsx +++ b/web-console/src/views/load-data-view/load-data-view.spec.tsx @@ -22,8 +22,13 @@ import React from 'react'; import { LoadDataView } from './load-data-view'; describe('LoadDataView', () => { - it('matches snapshot', () => { - const loadDataView = shallow( {}} />); + it('matches snapshot streaming', () => { + const loadDataView = shallow( {}} />); + expect(loadDataView).toMatchSnapshot(); + }); + + it('matches snapshot batch', () => { + const loadDataView = shallow( {}} />); expect(loadDataView).toMatchSnapshot(); }); }); diff --git a/web-console/src/views/load-data-view/load-data-view.tsx b/web-console/src/views/load-data-view/load-data-view.tsx index ac262796f80f..bf74ba10d4ff 100644 --- a/web-console/src/views/load-data-view/load-data-view.tsx +++ b/web-console/src/views/load-data-view/load-data-view.tsx @@ -23,17 +23,17 @@ import { ButtonGroup, Callout, Card, - Classes, Code, FormGroup, H5, - HTMLSelect, Icon, IconName, InputGroup, Intent, Menu, MenuItem, + Radio, + RadioGroup, Switch, TextArea, } from '@blueprintjs/core'; @@ -49,11 +49,11 @@ import { CenterMessage, ClearableInput, ExternalLink, + FormGroupWithInfo, JsonInput, Loader, PopoverText, } from '../../components'; -import { FormGroupWithInfo } from '../../components/form-group-with-info/form-group-with-info'; import { AsyncActionDialog } from '../../dialogs'; import { addTimestampTransform, @@ -135,13 +135,12 @@ import { EMPTY_OBJECT, filterMap, getDruidErrorMessage, - localStorageGet, + localStorageGetJson, LocalStorageKeys, - localStorageSet, + localStorageSetJson, moveElement, moveToIndex, oneOf, - parseJson, pluralIfNeeded, QueryState, } from '../../utils'; @@ -310,11 +309,14 @@ const VIEW_TITLE: Record = { loading: 'Loading', }; +export type LoadDataViewMode = 'all' | 'streaming' | 'batch'; + export interface LoadDataViewProps { + mode: LoadDataViewMode; initSupervisorId?: string; initTaskId?: string; exampleManifestsUrl?: string; - goToIngestion: (taskGroupId: string | undefined, supervisor?: string) => void; + goToIngestion: (taskGroupId: string | undefined, openDialog?: string) => void; } interface SelectedIndex { @@ -382,10 +384,19 @@ export interface LoadDataViewState { } export class LoadDataView extends React.PureComponent { + static MODE_TO_KEY: Record = { + all: LocalStorageKeys.INGESTION_SPEC, + streaming: LocalStorageKeys.STREAMING_INGESTION_SPEC, + batch: LocalStorageKeys.BATCH_INGESTION_SPEC, + }; + + private readonly localStorageKey: LocalStorageKeys; + constructor(props: LoadDataViewProps) { super(props); - let spec = parseJson(String(localStorageGet(LocalStorageKeys.INGESTION_SPEC))); + this.localStorageKey = LoadDataView.MODE_TO_KEY[props.mode]; + let spec = localStorageGetJson(this.localStorageKey); if (!spec || typeof spec !== 'object') spec = {}; this.state = { step: 'loading', @@ -521,13 +532,38 @@ export class LoadDataView extends React.PureComponent) => { newSpec = normalizeSpec(newSpec); - newSpec = upgradeSpec(newSpec); + try { + newSpec = upgradeSpec(newSpec); + } catch (e) { + newSpec = {}; + AppToaster.show({ + icon: IconNames.ERROR, + intent: Intent.DANGER, + timeout: 30000, + message: ( + <> +

+ This spec can not be used in the data loader because it can not be auto-converted to + the latest spec format: +

+

{e.message}

+

You can still submit it directly form the Ingestion view.

+ + ), + action: { + text: 'Go to Ingestion view', + onClick: () => { + this.props.goToIngestion(undefined); + }, + }, + }); + } const deltaState: Partial = { spec: newSpec }; if (!deepGet(newSpec, 'spec.ioConfig.type')) { deltaState.cacheRows = undefined; } this.setState(deltaState as LoadDataViewState); - localStorageSet(LocalStorageKeys.INGESTION_SPEC, JSONBig.stringify(newSpec)); + localStorageSetJson(this.localStorageKey, newSpec); }; private readonly updateSpecPreview = (newSpecPreview: Partial) => { @@ -537,7 +573,7 @@ export class LoadDataView extends React.PureComponent { this.setState(({ spec, nextSpec }) => { if (nextSpec) { - localStorageSet(LocalStorageKeys.INGESTION_SPEC, JSONBig.stringify(nextSpec)); + localStorageSetJson(this.localStorageKey, nextSpec); } return { spec: nextSpec ? nextSpec : { ...spec }, nextSpec: undefined }; // If applying again, make a shallow copy to force a refresh }); @@ -606,7 +642,7 @@ export class LoadDataView extends React.PureComponent void) { return ( - +
{title}
{caption}
@@ -616,21 +652,23 @@ export class LoadDataView extends React.PureComponent {this.renderActionCard( IconNames.ASTERISK, - 'Start a new spec', - 'Begin a new ingestion flow', + `Start a new ${type}spec`, + `Begin a new ${type}ingestion flow.`, this.handleResetSpec, )} {this.renderActionCard( IconNames.REPEAT, - 'Continue from previous spec', - 'Go back to the most recent spec you were working on', + `Continue from previous ${type}spec`, + `Go back to the most recent ${type}ingestion flow you were working on.`, this.handleContinueSpec, )}
@@ -683,25 +721,27 @@ export class LoadDataView extends React.PureComponent - {SECTIONS.map(section => ( -
-
{section.name}
- - {section.steps.map(s => ( -
- ))} +
+
+ {SECTIONS.map(section => ( +
+
{section.name}
+ + {section.steps.map(s => ( +
+ ))} +
); } @@ -760,7 +800,10 @@ export class LoadDataView extends React.PureComponent { @@ -784,7 +827,7 @@ export class LoadDataView extends React.PureComponent
- {this.renderIngestionCard('kafka')} - {this.renderIngestionCard('kinesis')} - {this.renderIngestionCard('azure-event-hubs')} - {this.renderIngestionCard('index_parallel:s3')} - {this.renderIngestionCard('index_parallel:azure')} - {this.renderIngestionCard('index_parallel:google')} - {this.renderIngestionCard('index_parallel:hdfs')} - {this.renderIngestionCard('index_parallel:druid')} - {this.renderIngestionCard('index_parallel:http')} - {this.renderIngestionCard('index_parallel:local')} - {this.renderIngestionCard('index_parallel:inline')} - {exampleManifestsUrl && this.renderIngestionCard('example', noExamples)} + {mode !== 'batch' && ( + <> + {this.renderIngestionCard('kafka')} + {this.renderIngestionCard('kinesis')} + {this.renderIngestionCard('azure-event-hubs')} + + )} + {mode !== 'streaming' && ( + <> + {this.renderIngestionCard('index_parallel:s3')} + {this.renderIngestionCard('index_parallel:azure')} + {this.renderIngestionCard('index_parallel:google')} + {this.renderIngestionCard('index_parallel:hdfs')} + {this.renderIngestionCard('index_parallel:druid')} + {this.renderIngestionCard('index_parallel:http')} + {this.renderIngestionCard('index_parallel:local')} + {this.renderIngestionCard('index_parallel:inline')} + {exampleManifestsUrl && this.renderIngestionCard('example', noExamples)} + + )} {this.renderIngestionCard('other')}
@@ -827,7 +878,7 @@ export class LoadDataView extends React.PureComponentPlease specify where your raw data is located

; + return

Please specify where your raw data is located.

; } const issue = this.selectedIngestionTypeIssue(); @@ -1244,13 +1295,13 @@ export class LoadDataView extends React.PureComponent - this.setState({ sampleStrategy: e.target.value as any })} + this.setState({ sampleStrategy: e.currentTarget.value as any })} > - - - + Start of stream + End of stream + )} {this.renderApplyButtonBar( diff --git a/web-console/src/views/load-data-view/parse-data-table/parse-data-table.tsx b/web-console/src/views/load-data-view/parse-data-table/parse-data-table.tsx index f285b3051263..af2c2e2c5fb2 100644 --- a/web-console/src/views/load-data-view/parse-data-table/parse-data-table.tsx +++ b/web-console/src/views/load-data-view/parse-data-table/parse-data-table.tsx @@ -21,8 +21,7 @@ import * as JSONBig from 'json-bigint-native'; import React from 'react'; import ReactTable from 'react-table'; -import { TableCell } from '../../../components'; -import { TableCellUnparseable } from '../../../components/table-cell-unparseable/table-cell-unparseable'; +import { TableCell, TableCellUnparseable } from '../../../components'; import { FlattenField } from '../../../druid-models'; import { DEFAULT_TABLE_CLASS_NAME, diff --git a/web-console/src/views/load-data-view/parse-time-table/parse-time-table.tsx b/web-console/src/views/load-data-view/parse-time-table/parse-time-table.tsx index 38921216ccd4..7e1396b4de1c 100644 --- a/web-console/src/views/load-data-view/parse-time-table/parse-time-table.tsx +++ b/web-console/src/views/load-data-view/parse-time-table/parse-time-table.tsx @@ -20,8 +20,7 @@ import classNames from 'classnames'; import React from 'react'; import ReactTable from 'react-table'; -import { TableCell } from '../../../components'; -import { TableCellUnparseable } from '../../../components/table-cell-unparseable/table-cell-unparseable'; +import { TableCell, TableCellUnparseable } from '../../../components'; import { getTimestampDetailFromSpec, getTimestampSpecColumnFromSpec, diff --git a/web-console/src/views/query-view/__snapshots__/query-view.spec.tsx.snap b/web-console/src/views/query-view/__snapshots__/query-view.spec.tsx.snap index fc2434807e26..886f92c883fb 100644 --- a/web-console/src/views/query-view/__snapshots__/query-view.spec.tsx.snap +++ b/web-console/src/views/query-view/__snapshots__/query-view.spec.tsx.snap @@ -24,7 +24,7 @@ exports[`QueryView matches snapshot 1`] = ` "tableRefName": undefined, "type": "ref", }, - "not": false, + "negated": false, "op": ">=", "rhs": SqlMulti { "args": SeparatedArray { @@ -132,9 +132,10 @@ exports[`QueryView matches snapshot 1`] = ` onChange={[Function]} /> - @@ -176,7 +177,7 @@ exports[`QueryView matches snapshot with query 1`] = ` "tableRefName": undefined, "type": "ref", }, - "not": false, + "negated": false, "op": ">=", "rhs": SqlMulti { "args": SeparatedArray { @@ -284,9 +285,10 @@ exports[`QueryView matches snapshot with query 1`] = ` onChange={[Function]} /> - diff --git a/web-console/src/views/query-view/column-tree/column-tree.spec.tsx b/web-console/src/views/query-view/column-tree/column-tree.spec.tsx index cae99dfc36b5..f06e02a98bfd 100644 --- a/web-console/src/views/query-view/column-tree/column-tree.spec.tsx +++ b/web-console/src/views/query-view/column-tree/column-tree.spec.tsx @@ -26,7 +26,7 @@ import { ColumnTree } from './column-tree'; describe('ColumnTree', () => { it('matches snapshot', () => { - const columnTree = shallow( + const comp = shallow( { return SqlQuery.parse(`SELECT channel, count(*) as cnt FROM wikipedia GROUP BY 1`); @@ -66,6 +66,6 @@ describe('ColumnTree', () => { />, ); - expect(columnTree).toMatchSnapshot(); + expect(comp).toMatchSnapshot(); }); }); diff --git a/web-console/src/views/query-view/column-tree/column-tree.tsx b/web-console/src/views/query-view/column-tree/column-tree.tsx index e7214ba18903..f70439f4f27e 100644 --- a/web-console/src/views/query-view/column-tree/column-tree.tsx +++ b/web-console/src/views/query-view/column-tree/column-tree.tsx @@ -30,10 +30,15 @@ import { } from 'druid-query-toolkit'; import React, { ChangeEvent } from 'react'; -import { Loader } from '../../../components'; -import { Deferred } from '../../../components/deferred/deferred'; -import { ColumnMetadata, copyAndAlert, groupBy, oneOf, prettyPrintSql } from '../../../utils'; -import { dataTypeToIcon } from '../../../utils/data-type-utils'; +import { Deferred, Loader } from '../../../components'; +import { + ColumnMetadata, + copyAndAlert, + dataTypeToIcon, + groupBy, + oneOf, + prettyPrintSql, +} from '../../../utils'; import { NumberMenuItems, StringMenuItems, TimeMenuItems } from './column-tree-menu'; diff --git a/web-console/src/views/query-view/explain-dialog/__snapshots__/explain-dialog.spec.tsx.snap b/web-console/src/views/query-view/explain-dialog/__snapshots__/explain-dialog.spec.tsx.snap index 95a2ba81a386..e581bbd3c4d3 100644 --- a/web-console/src/views/query-view/explain-dialog/__snapshots__/explain-dialog.spec.tsx.snap +++ b/web-console/src/views/query-view/explain-dialog/__snapshots__/explain-dialog.spec.tsx.snap @@ -113,10 +113,43 @@ exports[`ExplainDialog matches snapshot on some data (many queries) 1`] = ` > - - - { function makeExplainDialog() { return ( {}} - queryWithContext={{ queryString: 'test', queryContext: {}, wrapQueryLimit: undefined }} + onOpenQuery={() => {}} + queryWithContext={{ engine: 'sql-native', queryString: 'test', queryContext: {} }} onClose={() => {}} + openQueryLabel="Open query" /> ); } diff --git a/web-console/src/views/query-view/explain-dialog/explain-dialog.tsx b/web-console/src/views/query-view/explain-dialog/explain-dialog.tsx index dbd64bdd8fd2..a60567c994d9 100644 --- a/web-console/src/views/query-view/explain-dialog/explain-dialog.tsx +++ b/web-console/src/views/query-view/explain-dialog/explain-dialog.tsx @@ -25,51 +25,56 @@ import { Intent, Tab, Tabs, - TextArea, } from '@blueprintjs/core'; import { IconNames } from '@blueprintjs/icons'; import * as JSONBig from 'json-bigint-native'; import React from 'react'; +import AceEditor from 'react-ace'; import { Loader } from '../../../components'; +import { DruidEngine, isEmptyContext, QueryContext, QueryWithContext } from '../../../druid-models'; import { useQueryManager } from '../../../hooks'; +import { Api } from '../../../singletons'; import { + deepGet, formatSignature, getDruidErrorMessage, + nonEmptyArray, queryDruidSql, QueryExplanation, - QueryWithContext, - trimSemicolon, } from '../../../utils'; -import { isEmptyContext } from '../../../utils/query-context'; import './explain-dialog.scss'; function isExplainQuery(query: string): boolean { - return /EXPLAIN\sPLAN\sFOR/i.test(query); + return /^\s*EXPLAIN\sPLAN\sFOR/im.test(query); } function wrapInExplainIfNeeded(query: string): string { - query = trimSemicolon(query); if (isExplainQuery(query)) return query; return `EXPLAIN PLAN FOR ${query}`; } +export interface QueryContextEngine extends QueryWithContext { + engine: DruidEngine; +} + export interface ExplainDialogProps { - queryWithContext: QueryWithContext; + queryWithContext: QueryContextEngine; mandatoryQueryContext?: Record; onClose: () => void; - setQueryString: (queryString: string) => void; + openQueryLabel: string | undefined; + onOpenQuery: (queryString: string) => void; } export const ExplainDialog = React.memo(function ExplainDialog(props: ExplainDialogProps) { - const { queryWithContext, onClose, setQueryString, mandatoryQueryContext } = props; + const { queryWithContext, onClose, openQueryLabel, onOpenQuery, mandatoryQueryContext } = props; - const [explainState] = useQueryManager({ - processQuery: async (queryWithContext: QueryWithContext) => { - const { queryString, queryContext, wrapQueryLimit } = queryWithContext; + const [explainState] = useQueryManager({ + processQuery: async queryWithContext => { + const { engine, queryString, queryContext, wrapQueryLimit } = queryWithContext; - let context: Record | undefined; + let context: QueryContext | undefined; if (!isEmptyContext(queryContext) || wrapQueryLimit || mandatoryQueryContext) { context = { ...queryContext, @@ -81,19 +86,24 @@ export const ExplainDialog = React.memo(function ExplainDialog(props: ExplainDia } } - let result: any[] | undefined; + const payload: any = { + query: wrapInExplainIfNeeded(queryString), + context, + }; + + let result: any[]; try { - result = await queryDruidSql({ - query: wrapInExplainIfNeeded(queryString), - context, - }); + result = + engine === 'sql-msq-task' + ? (await Api.instance.post(`/druid/v2/sql/task`, payload)).data + : await queryDruidSql(payload); } catch (e) { throw new Error(getDruidErrorMessage(e)); } - const plan = result[0]['PLAN']; + const plan = deepGet(result, '0.PLAN'); if (typeof plan !== 'string') { - throw new Error(`unexpected result from server`); + throw new Error(`unexpected result from ${engine} API`); } try { @@ -113,23 +123,37 @@ export const ExplainDialog = React.memo(function ExplainDialog(props: ExplainDia const queryString = JSONBig.stringify(queryExplanation.query, undefined, 2); return (
- -