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 <john.gozde@imply.io>
This commit is contained in:
Vadim Ogievetsky 2022-08-24 16:17:12 -07:00 committed by GitHub
parent 02914c17b9
commit 04ee7abeff
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
302 changed files with 25649 additions and 2150 deletions

View File

@ -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

21
licenses/bin/zustand.MIT Normal file
View File

@ -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.

View File

@ -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();
}
}

View File

@ -30,10 +30,10 @@ enum TaskColumn {
GROUP_ID,
TYPE,
DATASOURCE,
LOCATION,
CREATED_TIME,
STATUS,
CREATED_TIME,
DURATION,
LOCATION,
}
/**

View File

@ -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';
}
/**

View File

@ -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(

View File

@ -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<string[][]> {
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');
}
}

View File

@ -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']);
});
});
});

View File

@ -54,6 +54,12 @@ exports.SQL_KEYWORDS = [
'ROWS',
'ONLY',
'VALUES',
'PARTITIONED BY',
'CLUSTERED BY',
'TIME',
'INSERT INTO',
'REPLACE INTO',
'OVERWRITE',
];
exports.SQL_EXPRESSION_PARTS = [

View File

@ -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][]>;

View File

@ -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",

View File

@ -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",

View File

@ -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])];
}
}

View File

@ -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 \
)
}

75
web-console/script/mv Executable file
View File

@ -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,
});

View File

@ -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(
{

View File

@ -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>;

View File

@ -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 {

View File

@ -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>
`;

View File

@ -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 = <ClickToCopy text="Hello world" />;
const { container } = render(arrayInput);
expect(container.firstChild).toMatchSnapshot();
});
});

View File

@ -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 (
<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>
);
});

View File

@ -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>
`;

View File

@ -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;
}
}

View File

@ -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>
);
});

View File

@ -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>
);

View File

@ -15,24 +15,91 @@ 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}
disabled={false}
href="#ingestion"
icon="gantt-chart"
minimal={true}
text="Ingestion"
/>
<Blueprint4.AnchorButton
active={false}
className="header-entry"
disabled={false}
href="#datasources"
icon="multi-select"
@ -41,6 +108,16 @@ exports[`HeaderBar matches snapshot 1`] = `
/>
<Blueprint4.AnchorButton
active={false}
className="header-entry"
disabled={false}
href="#ingestion"
icon="gantt-chart"
minimal={true}
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}
/>

View File

@ -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);

View File

@ -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();
});

View File

@ -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)} />}

View File

@ -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';

View File

@ -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}

View File

@ -18,7 +18,7 @@
import React from 'react';
import { ExternalLink } from '../../../components';
import { ExternalLink } from '../external-link/external-link';
export interface LearnMoreProps {
href: string;

View File

@ -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}

View File

@ -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>
`;

View File

@ -0,0 +1,50 @@
/*
* 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 { MenuTristate } from './menu-tristate';
describe('MenuTristate', () => {
it('matches snapshot undefined', () => {
const menuCheckbox = (
<MenuTristate
text="hello"
value={undefined}
undefinedEffectiveValue
onValueChange={() => {}}
/>
);
const { container } = render(menuCheckbox);
expect(container.firstChild).toMatchSnapshot();
});
it('matches snapshot false', () => {
const menuCheckbox = (
<MenuTristate
text="hello"
value={false}
undefinedEffectiveValue={false}
onValueChange={() => {}}
/>
);
const { container } = render(menuCheckbox);
expect(container.firstChild).toMatchSnapshot();
});
});

View File

@ -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>
);
}

View File

@ -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

View File

@ -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>
);
});

View File

@ -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>

View File

@ -0,0 +1,28 @@
/*
* 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 '../../blueprint-overrides/common/colors';
@import '../../variables';
.query-error-pane {
padding: 20px 22px;
.#{$bp-ns}-dark & {
background: $dark-gray3;
}
}

View File

@ -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={() => {}}
/>,

View File

@ -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>}

View File

@ -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%;
}
}

View File

@ -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>
);
});

View File

@ -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%"

View File

@ -16,6 +16,9 @@
* limitations under the License.
*/
@import '../../variables';
.table-cell-unparseable {
padding: $table-cell-v-padding $table-cell-h-padding;
color: #9e2b0e;
}

View File

@ -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>

View File

@ -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} />

View File

@ -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';

View File

@ -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],

View File

@ -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)>

View File

@ -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>

View File

@ -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>
`;

View File

@ -29,21 +29,11 @@
}
}
.main-area {
height: calc(100% - 5px);
.loader {
position: relative;
}
textarea {
height: 100%;
width: 100%;
resize: none;
}
.loader {
position: relative;
}
.ReactTable {
height: 100%;
}
.ReactTable {
height: 100%;
}
}

View File

@ -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', () => {

View File

@ -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>
);
});

View File

@ -0,0 +1,29 @@
/*
* 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.
*/
.datasource-preview-pane {
position: relative;
height: 100%;
.record-table-pane {
height: 100%;
}
.datasource-preview-error {
color: #9e2b0e;
}
}

View File

@ -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>
);
});

View File

@ -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={() => {}}
/>

View File

@ -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: 'list-columns',
icon: IconNames.TH,
text: 'Records',
active: activeTab === 'records',
onClick: () => setActiveTab('records'),
},
{
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>
);
});

View File

@ -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';

View File

@ -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';

View File

@ -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';

View File

@ -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[];

View File

@ -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';

View File

@ -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>
);
});

View File

@ -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)>

View File

@ -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"
>

View File

@ -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"

View File

@ -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>
);
});

View File

@ -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);
});
});

View File

@ -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%;
}
}

View File

@ -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>
);
});

View File

@ -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);
});
});

View File

@ -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:&nbsp;<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'),
},
]}
/>

View File

@ -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>
);
});

View File

@ -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],
};

View File

@ -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';

View File

@ -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[];

View File

@ -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}

View File

@ -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`}
/>
)}

View File

@ -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',

View File

@ -16,7 +16,8 @@
* limitations under the License.
*/
import { CompactionConfig } from './compaction-config';
import { CompactionConfig } from '../compaction-config/compaction-config';
import {
CompactionStatus,
formatCompactionConfigAndStatus,

View File

@ -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();

View File

@ -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;

View File

@ -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', () => {

View File

@ -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',

View File

@ -0,0 +1,27 @@
/*
* 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.
*/
export type DruidEngine = 'native' | 'sql-native' | 'sql-msq-task';
export const DRUID_ENGINES: DruidEngine[] = ['native', 'sql-native', 'sql-msq-task'];
export function validDruidEngine(
possibleDruidEngine: string | undefined,
): possibleDruidEngine is DruidEngine {
return Boolean(possibleDruidEngine && DRUID_ENGINES.includes(possibleDruidEngine as DruidEngine));
}

View File

@ -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] } },
},
},
},
},
},
);

File diff suppressed because one or more lines are too long

View File

@ -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,
}
`);
});
});
});

View File

@ -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);
}
}

View File

@ -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,
};
}

Some files were not shown because too many files have changed in this diff Show More