mirror of https://github.com/apache/druid.git
Web console: making the cell filter menu more functional, removing the old query view, and updating d3 (#13169)
* remove old query view * update tests * add filter * fix test * bump d3 things to latest versions * rent too far into the future with d3 * make config dialogs load * goodies * update snapshots * only compute duration when running or pending
This commit is contained in:
parent
25c1d55dd6
commit
573e12c75f
|
@ -5494,7 +5494,7 @@ license_category: binary
|
|||
module: web-console
|
||||
license_name: BSD-3-Clause License
|
||||
copyright: Mike Bostock
|
||||
version: 2.3.3
|
||||
version: 2.12.1
|
||||
license_file_path: licenses/bin/d3-array.BSD3
|
||||
|
||||
---
|
||||
|
@ -5504,7 +5504,7 @@ license_category: binary
|
|||
module: web-console
|
||||
license_name: BSD-3-Clause License
|
||||
copyright: Mike Bostock
|
||||
version: 1.0.12
|
||||
version: 2.1.0
|
||||
license_file_path: licenses/bin/d3-axis.BSD3
|
||||
|
||||
---
|
||||
|
@ -5514,7 +5514,7 @@ license_category: binary
|
|||
module: web-console
|
||||
license_name: BSD-3-Clause License
|
||||
copyright: Mike Bostock
|
||||
version: 1.4.0
|
||||
version: 2.0.0
|
||||
license_file_path: licenses/bin/d3-color.BSD3
|
||||
|
||||
---
|
||||
|
@ -5534,7 +5534,7 @@ license_category: binary
|
|||
module: web-console
|
||||
license_name: BSD-3-Clause License
|
||||
copyright: Mike Bostock
|
||||
version: 1.3.2
|
||||
version: 2.0.1
|
||||
license_file_path: licenses/bin/d3-interpolate.BSD3
|
||||
|
||||
---
|
||||
|
@ -5544,7 +5544,7 @@ license_category: binary
|
|||
module: web-console
|
||||
license_name: BSD-3-Clause License
|
||||
copyright: Mike Bostock
|
||||
version: 3.2.0
|
||||
version: 3.3.0
|
||||
license_file_path: licenses/bin/d3-scale.BSD3
|
||||
|
||||
---
|
||||
|
@ -5554,7 +5554,7 @@ license_category: binary
|
|||
module: web-console
|
||||
license_name: BSD-3-Clause License
|
||||
copyright: Mike Bostock
|
||||
version: 1.4.0
|
||||
version: 2.0.0
|
||||
license_file_path: licenses/bin/d3-selection.BSD3
|
||||
|
||||
---
|
||||
|
@ -5653,7 +5653,7 @@ license_category: binary
|
|||
module: web-console
|
||||
license_name: Apache License version 2.0
|
||||
copyright: Imply Data
|
||||
version: 0.15.1
|
||||
version: 0.15.3
|
||||
|
||||
---
|
||||
|
||||
|
@ -5837,6 +5837,16 @@ license_file_path: licenses/bin/import-fresh.MIT
|
|||
|
||||
---
|
||||
|
||||
name: "internmap"
|
||||
license_category: binary
|
||||
module: web-console
|
||||
license_name: ISC License
|
||||
copyright: Mike Bostock
|
||||
version: 1.0.1
|
||||
license_file_path: licenses/bin/internmap.ISC
|
||||
|
||||
---
|
||||
|
||||
name: "is-arguments"
|
||||
license_category: binary
|
||||
module: web-console
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
Copyright 2010-2018 Mike Bostock
|
||||
Copyright 2010-2020 Mike Bostock
|
||||
All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without modification,
|
||||
|
|
|
@ -0,0 +1,13 @@
|
|||
Copyright 2010-2022 Mike Bostock
|
||||
|
||||
Permission to use, copy, modify, and/or distribute this software for any purpose
|
||||
with or without fee is hereby granted, provided that the above copyright notice
|
||||
and this permission notice appear in all copies.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
|
||||
REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND
|
||||
FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
|
||||
INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS
|
||||
OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER
|
||||
TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF
|
||||
THIS SOFTWARE.
|
|
@ -0,0 +1,13 @@
|
|||
Copyright 2010-2021 Mike Bostock
|
||||
|
||||
Permission to use, copy, modify, and/or distribute this software for any purpose
|
||||
with or without fee is hereby granted, provided that the above copyright notice
|
||||
and this permission notice appear in all copies.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
|
||||
REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND
|
||||
FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
|
||||
INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS
|
||||
OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER
|
||||
TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF
|
||||
THIS SOFTWARE.
|
|
@ -0,0 +1,13 @@
|
|||
Copyright 2010-2022 Mike Bostock
|
||||
|
||||
Permission to use, copy, modify, and/or distribute this software for any purpose
|
||||
with or without fee is hereby granted, provided that the above copyright notice
|
||||
and this permission notice appear in all copies.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
|
||||
REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND
|
||||
FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
|
||||
INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS
|
||||
OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER
|
||||
TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF
|
||||
THIS SOFTWARE.
|
|
@ -0,0 +1,13 @@
|
|||
Copyright 2010-2021 Mike Bostock
|
||||
|
||||
Permission to use, copy, modify, and/or distribute this software for any purpose
|
||||
with or without fee is hereby granted, provided that the above copyright notice
|
||||
and this permission notice appear in all copies.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
|
||||
REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND
|
||||
FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
|
||||
INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS
|
||||
OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER
|
||||
TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF
|
||||
THIS SOFTWARE.
|
|
@ -0,0 +1,13 @@
|
|||
Copyright 2010-2021 Mike Bostock
|
||||
|
||||
Permission to use, copy, modify, and/or distribute this software for any purpose
|
||||
with or without fee is hereby granted, provided that the above copyright notice
|
||||
and this permission notice appear in all copies.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
|
||||
REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND
|
||||
FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
|
||||
INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS
|
||||
OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER
|
||||
TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF
|
||||
THIS SOFTWARE.
|
|
@ -0,0 +1,13 @@
|
|||
Copyright 2010-2021 Mike Bostock
|
||||
|
||||
Permission to use, copy, modify, and/or distribute this software for any purpose
|
||||
with or without fee is hereby granted, provided that the above copyright notice
|
||||
and this permission notice appear in all copies.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
|
||||
REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND
|
||||
FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
|
||||
INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS
|
||||
OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER
|
||||
TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF
|
||||
THIS SOFTWARE.
|
|
@ -0,0 +1,13 @@
|
|||
Copyright 2021 Mike Bostock
|
||||
|
||||
Permission to use, copy, modify, and/or distribute this software for any purpose
|
||||
with or without fee is hereby granted, provided that the above copyright notice
|
||||
and this permission notice appear in all copies.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
|
||||
REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND
|
||||
FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
|
||||
INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS
|
||||
OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER
|
||||
TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF
|
||||
THIS SOFTWARE.
|
|
@ -30,27 +30,27 @@ export class QueryOverview {
|
|||
|
||||
constructor(page: playwright.Page, unifiedConsoleUrl: string) {
|
||||
this.page = page;
|
||||
this.baseUrl = unifiedConsoleUrl + '#query';
|
||||
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.query-input textarea');
|
||||
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.query-info');
|
||||
await this.page.waitForSelector('div.result-table-pane');
|
||||
|
||||
return await extractTable(this.page, 'div.query-output div.rt-tr-group', 'div.rt-td');
|
||||
return await extractTable(this.page, 'div.result-table-pane div.rt-tr-group', 'div.rt-td');
|
||||
}
|
||||
|
||||
async cancelQuery(query: string): Promise<number> {
|
||||
await this.page.goto(this.baseUrl);
|
||||
await this.page.reload({ waitUntil: 'networkidle' });
|
||||
|
||||
const input = await this.page.waitForSelector('div.query-input textarea');
|
||||
const input = await this.page.waitForSelector('div.flexible-query-input textarea');
|
||||
await input.fill(query);
|
||||
|
||||
await Promise.all([
|
||||
|
|
|
@ -18,11 +18,11 @@
|
|||
"classnames": "^2.2.6",
|
||||
"copy-to-clipboard": "^3.2.0",
|
||||
"core-js": "^3.10.1",
|
||||
"d3-array": "^2.3.3",
|
||||
"d3-axis": "^1.0.12",
|
||||
"d3-scale": "^3.2.0",
|
||||
"d3-selection": "^1.4.0",
|
||||
"druid-query-toolkit": "^0.15.1",
|
||||
"d3-array": "^2.12.1",
|
||||
"d3-axis": "^2.1.0",
|
||||
"d3-scale": "^3.3.0",
|
||||
"d3-selection": "^2.0.0",
|
||||
"druid-query-toolkit": "^0.15.3",
|
||||
"file-saver": "^2.0.2",
|
||||
"follow-redirects": "^1.14.7",
|
||||
"fontsource-open-sans": "^3.0.9",
|
||||
|
@ -54,10 +54,10 @@
|
|||
"@babel/preset-env": "^7.14.4",
|
||||
"@testing-library/react": "^8.0.9",
|
||||
"@types/classnames": "^2.2.9",
|
||||
"@types/d3-array": "^2.0.0",
|
||||
"@types/d3-axis": "^1.0.12",
|
||||
"@types/d3-scale": "^2.1.1",
|
||||
"@types/d3-selection": "^1.4.1",
|
||||
"@types/d3-array": "^2.12.3",
|
||||
"@types/d3-axis": "^2.1.3",
|
||||
"@types/d3-scale": "^3.3.2",
|
||||
"@types/d3-selection": "^2.0.1",
|
||||
"@types/enzyme": "^3.10.3",
|
||||
"@types/enzyme-adapter-react-16": "^1.0.5",
|
||||
"@types/file-saver": "^2.0.1",
|
||||
|
@ -4600,39 +4600,39 @@
|
|||
"dev": true
|
||||
},
|
||||
"node_modules/@types/d3-array": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@types/d3-array/-/d3-array-2.0.0.tgz",
|
||||
"integrity": "sha512-rGqfPVowNDTszSFvwoZIXvrPG7s/qKzm9piCRIH6xwTTRu7pPZ3ootULFnPkTt74B6i5lN0FpLQL24qGOw1uZA==",
|
||||
"version": "2.12.3",
|
||||
"resolved": "https://registry.npmjs.org/@types/d3-array/-/d3-array-2.12.3.tgz",
|
||||
"integrity": "sha512-hN879HLPTVqZV3FQEXy7ptt083UXwguNbnxdTGzVW4y4KjX5uyNKljrQixZcSJfLyFirbpUokxpXtvR+N5+KIg==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/@types/d3-axis": {
|
||||
"version": "1.0.12",
|
||||
"resolved": "https://registry.npmjs.org/@types/d3-axis/-/d3-axis-1.0.12.tgz",
|
||||
"integrity": "sha512-BZISgSD5M8TgURyNtcPAmUB9sk490CO1Thb6/gIn0WZTt3Y50IssX+2Z0vTccoqZksUDTep0b+o4ofXslvNbqg==",
|
||||
"version": "2.1.3",
|
||||
"resolved": "https://registry.npmjs.org/@types/d3-axis/-/d3-axis-2.1.3.tgz",
|
||||
"integrity": "sha512-QjXjwZ0xzyrW2ndkmkb09ErgWDEYtbLBKGui73QLMFm3woqWpxptfD5Y7vqQdybMcu7WEbjZ5q+w2w5+uh2IjA==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@types/d3-selection": "*"
|
||||
"@types/d3-selection": "^2"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/d3-scale": {
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@types/d3-scale/-/d3-scale-2.1.1.tgz",
|
||||
"integrity": "sha512-kNTkbZQ+N/Ip8oX9PByXfDLoCSaZYm+VUOasbmsa6KD850/ziMdYepg/8kLg2plHzoLANdMqPoYQbvExevLUHg==",
|
||||
"version": "3.3.2",
|
||||
"resolved": "https://registry.npmjs.org/@types/d3-scale/-/d3-scale-3.3.2.tgz",
|
||||
"integrity": "sha512-gGqr7x1ost9px3FvIfUMi5XA/F/yAf4UkUDtdQhpH92XCT0Oa7zkkRzY61gPVJq+DxpHn/btouw5ohWkbBsCzQ==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@types/d3-time": "*"
|
||||
"@types/d3-time": "^2"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/d3-selection": {
|
||||
"version": "1.4.1",
|
||||
"resolved": "https://registry.npmjs.org/@types/d3-selection/-/d3-selection-1.4.1.tgz",
|
||||
"integrity": "sha512-bv8IfFYo/xG6dxri9OwDnK3yCagYPeRIjTlrcdYJSx+FDWlCeBDepIHUpqROmhPtZ53jyna0aUajZRk0I3rXNA==",
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@types/d3-selection/-/d3-selection-2.0.1.tgz",
|
||||
"integrity": "sha512-3mhtPnGE+c71rl/T5HMy+ykg7migAZ4T6gzU0HxpgBFKcasBrSnwRbYV1/UZR6o5fkpySxhWxAhd7yhjj8jL7g==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/@types/d3-time": {
|
||||
"version": "1.0.10",
|
||||
"resolved": "https://registry.npmjs.org/@types/d3-time/-/d3-time-1.0.10.tgz",
|
||||
"integrity": "sha512-aKf62rRQafDQmSiv1NylKhIMmznsjRN+MnXRXTqHoqm0U/UZzVpdrtRnSIfdiLS616OuC1soYeX1dBg2n1u8Xw==",
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@types/d3-time/-/d3-time-2.1.1.tgz",
|
||||
"integrity": "sha512-9MVYlmIgmRR31C5b4FVSWtuMmBHh2mOWQYfl7XAYOa8dsnb7iEmUmRSWSFgXFtkjxO65d7hTUHQC+RhR/9IWFg==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/@types/dom4": {
|
||||
|
@ -8465,19 +8465,22 @@
|
|||
"integrity": "sha512-KxnUB0ZMlnUWCsx2Z8MUsr6qV6ja1w9ArPErJaJaF8a5SOWoHLIszeCTKGRGRgtLgYrs1E8CHkNSP1VZTTPc9g=="
|
||||
},
|
||||
"node_modules/d3-array": {
|
||||
"version": "2.3.3",
|
||||
"resolved": "https://registry.npmjs.org/d3-array/-/d3-array-2.3.3.tgz",
|
||||
"integrity": "sha512-syv3wp0U5aB6toP2zb2OdBkhTy1MWDsCAaYk6OXJZv+G4u7bSWEmYgxLoFyc88RQUhZYGCebW9a9UD1gFi5+MQ=="
|
||||
"version": "2.12.1",
|
||||
"resolved": "https://registry.npmjs.org/d3-array/-/d3-array-2.12.1.tgz",
|
||||
"integrity": "sha512-B0ErZK/66mHtEsR1TkPEEkwdy+WDesimkM5gpZr5Dsg54BiTA5RXtYW5qTLIAcekaS9xfZrzBLF/OAkB3Qn1YQ==",
|
||||
"dependencies": {
|
||||
"internmap": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/d3-axis": {
|
||||
"version": "1.0.12",
|
||||
"resolved": "https://registry.npmjs.org/d3-axis/-/d3-axis-1.0.12.tgz",
|
||||
"integrity": "sha512-ejINPfPSNdGFKEOAtnBtdkpr24c4d4jsei6Lg98mxf424ivoDP2956/5HDpIAtmHo85lqT4pruy+zEgvRUBqaQ=="
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/d3-axis/-/d3-axis-2.1.0.tgz",
|
||||
"integrity": "sha512-z/G2TQMyuf0X3qP+Mh+2PimoJD41VOCjViJzT0BHeL/+JQAofkiWZbWxlwFGb1N8EN+Cl/CW+MUKbVzr1689Cw=="
|
||||
},
|
||||
"node_modules/d3-color": {
|
||||
"version": "1.4.0",
|
||||
"resolved": "https://registry.npmjs.org/d3-color/-/d3-color-1.4.0.tgz",
|
||||
"integrity": "sha512-TzNPeJy2+iEepfiL92LAAB7fvnp/dV2YwANPVHdDWmYMm23qIJBYww3qT8I8C1wXrmrg4UWs7BKc2tKIgyjzHg=="
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/d3-color/-/d3-color-2.0.0.tgz",
|
||||
"integrity": "sha512-SPXi0TSKPD4g9tw0NMZFnR95XVgUZiBH+uUTqQuDu1OsE2zomHU7ho0FISciaPvosimixwHFl3WHLGabv6dDgQ=="
|
||||
},
|
||||
"node_modules/d3-format": {
|
||||
"version": "1.4.1",
|
||||
|
@ -8485,29 +8488,37 @@
|
|||
"integrity": "sha512-TUswGe6hfguUX1CtKxyG2nymO+1lyThbkS1ifLX0Sr+dOQtAD5gkrffpHnx+yHNKUZ0Bmg5T4AjUQwugPDrm0g=="
|
||||
},
|
||||
"node_modules/d3-interpolate": {
|
||||
"version": "1.3.2",
|
||||
"resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-1.3.2.tgz",
|
||||
"integrity": "sha512-NlNKGopqaz9qM1PXh9gBF1KSCVh+jSFErrSlD/4hybwoNX/gt1d8CDbDW+3i+5UOHhjC6s6nMvRxcuoMVNgL2w==",
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-2.0.1.tgz",
|
||||
"integrity": "sha512-c5UhwwTs/yybcmTpAVqwSFl6vrQ8JZJoT5F7xNFK9pymv5C0Ymcc9/LIJHtYIggg/yS9YHw8i8O8tgb9pupjeQ==",
|
||||
"dependencies": {
|
||||
"d3-color": "1"
|
||||
"d3-color": "1 - 2"
|
||||
}
|
||||
},
|
||||
"node_modules/d3-scale": {
|
||||
"version": "3.2.0",
|
||||
"resolved": "https://registry.npmjs.org/d3-scale/-/d3-scale-3.2.0.tgz",
|
||||
"integrity": "sha512-1RnLYPmH3f2E96hSsCr3ok066myuAxoH3+pnlJAedeMOp7jeW7A+GZHAyVWWaStfphyPEBiDoLFA9zl+DcnC2Q==",
|
||||
"version": "3.3.0",
|
||||
"resolved": "https://registry.npmjs.org/d3-scale/-/d3-scale-3.3.0.tgz",
|
||||
"integrity": "sha512-1JGp44NQCt5d1g+Yy+GeOnZP7xHo0ii8zsQp6PGzd+C1/dl0KGsp9A7Mxwp+1D1o4unbTTxVdU/ZOIEBoeZPbQ==",
|
||||
"dependencies": {
|
||||
"d3-array": "1.2.0 - 2",
|
||||
"d3-format": "1",
|
||||
"d3-interpolate": "1",
|
||||
"d3-time": "1",
|
||||
"d3-time-format": "2"
|
||||
"d3-array": "^2.3.0",
|
||||
"d3-format": "1 - 2",
|
||||
"d3-interpolate": "1.2.0 - 2",
|
||||
"d3-time": "^2.1.1",
|
||||
"d3-time-format": "2 - 3"
|
||||
}
|
||||
},
|
||||
"node_modules/d3-scale/node_modules/d3-time": {
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/d3-time/-/d3-time-2.1.1.tgz",
|
||||
"integrity": "sha512-/eIQe/eR4kCQwq7yxi7z4c6qEXf2IYGcjoWB5OOQy4Tq9Uv39/947qlDcN2TLkiTzQWzvnsuYPB9TrWaNfipKQ==",
|
||||
"dependencies": {
|
||||
"d3-array": "2"
|
||||
}
|
||||
},
|
||||
"node_modules/d3-selection": {
|
||||
"version": "1.4.0",
|
||||
"resolved": "https://registry.npmjs.org/d3-selection/-/d3-selection-1.4.0.tgz",
|
||||
"integrity": "sha512-EYVwBxQGEjLCKF2pJ4+yrErskDnz5v403qvAid96cNdCMr8rmCYfY5RGzWz24mdIbxmDf6/4EAH+K9xperD5jg=="
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/d3-selection/-/d3-selection-2.0.0.tgz",
|
||||
"integrity": "sha512-XoGGqhLUN/W14NmaqcO/bb1nqjDAw5WtSYb2X8wiuQWvSZUsUVYsOSkOybUrNvcBjaywBdYPy03eXHMXjk9nZA=="
|
||||
},
|
||||
"node_modules/d3-time": {
|
||||
"version": "1.1.0",
|
||||
|
@ -8956,9 +8967,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/druid-query-toolkit": {
|
||||
"version": "0.15.1",
|
||||
"resolved": "https://registry.npmjs.org/druid-query-toolkit/-/druid-query-toolkit-0.15.1.tgz",
|
||||
"integrity": "sha512-yklAD1Ksokh2s+llEaR/lvvMHjW6CgdCG7X+JFnWsttM/xITpTFMu4Zo5/MqVvoo9021/deg87QQMCOKNfo3wQ==",
|
||||
"version": "0.15.3",
|
||||
"resolved": "https://registry.npmjs.org/druid-query-toolkit/-/druid-query-toolkit-0.15.3.tgz",
|
||||
"integrity": "sha512-AtKnmCDWSHh1+bHFs36drJAKQ60914ttF62x3wIxXbD1zoUEmwwsX4Ks2zD3SItantVS4fTj3cyjMl/c0fojpA==",
|
||||
"dependencies": {
|
||||
"tslib": "^2.3.1"
|
||||
},
|
||||
|
@ -12599,6 +12610,11 @@
|
|||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/internmap": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/internmap/-/internmap-1.0.1.tgz",
|
||||
"integrity": "sha512-lDB5YccMydFBtasVtxnZ3MRBHuaoE8GKsppq+EchKL2U4nK/DmEpPHNH8MZe5HkMtpSiTSOZwfN0tzYjO/lJEw=="
|
||||
},
|
||||
"node_modules/interpret": {
|
||||
"version": "2.2.0",
|
||||
"resolved": "https://registry.npmjs.org/interpret/-/interpret-2.2.0.tgz",
|
||||
|
@ -31092,39 +31108,39 @@
|
|||
"dev": true
|
||||
},
|
||||
"@types/d3-array": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@types/d3-array/-/d3-array-2.0.0.tgz",
|
||||
"integrity": "sha512-rGqfPVowNDTszSFvwoZIXvrPG7s/qKzm9piCRIH6xwTTRu7pPZ3ootULFnPkTt74B6i5lN0FpLQL24qGOw1uZA==",
|
||||
"version": "2.12.3",
|
||||
"resolved": "https://registry.npmjs.org/@types/d3-array/-/d3-array-2.12.3.tgz",
|
||||
"integrity": "sha512-hN879HLPTVqZV3FQEXy7ptt083UXwguNbnxdTGzVW4y4KjX5uyNKljrQixZcSJfLyFirbpUokxpXtvR+N5+KIg==",
|
||||
"dev": true
|
||||
},
|
||||
"@types/d3-axis": {
|
||||
"version": "1.0.12",
|
||||
"resolved": "https://registry.npmjs.org/@types/d3-axis/-/d3-axis-1.0.12.tgz",
|
||||
"integrity": "sha512-BZISgSD5M8TgURyNtcPAmUB9sk490CO1Thb6/gIn0WZTt3Y50IssX+2Z0vTccoqZksUDTep0b+o4ofXslvNbqg==",
|
||||
"version": "2.1.3",
|
||||
"resolved": "https://registry.npmjs.org/@types/d3-axis/-/d3-axis-2.1.3.tgz",
|
||||
"integrity": "sha512-QjXjwZ0xzyrW2ndkmkb09ErgWDEYtbLBKGui73QLMFm3woqWpxptfD5Y7vqQdybMcu7WEbjZ5q+w2w5+uh2IjA==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@types/d3-selection": "*"
|
||||
"@types/d3-selection": "^2"
|
||||
}
|
||||
},
|
||||
"@types/d3-scale": {
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@types/d3-scale/-/d3-scale-2.1.1.tgz",
|
||||
"integrity": "sha512-kNTkbZQ+N/Ip8oX9PByXfDLoCSaZYm+VUOasbmsa6KD850/ziMdYepg/8kLg2plHzoLANdMqPoYQbvExevLUHg==",
|
||||
"version": "3.3.2",
|
||||
"resolved": "https://registry.npmjs.org/@types/d3-scale/-/d3-scale-3.3.2.tgz",
|
||||
"integrity": "sha512-gGqr7x1ost9px3FvIfUMi5XA/F/yAf4UkUDtdQhpH92XCT0Oa7zkkRzY61gPVJq+DxpHn/btouw5ohWkbBsCzQ==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@types/d3-time": "*"
|
||||
"@types/d3-time": "^2"
|
||||
}
|
||||
},
|
||||
"@types/d3-selection": {
|
||||
"version": "1.4.1",
|
||||
"resolved": "https://registry.npmjs.org/@types/d3-selection/-/d3-selection-1.4.1.tgz",
|
||||
"integrity": "sha512-bv8IfFYo/xG6dxri9OwDnK3yCagYPeRIjTlrcdYJSx+FDWlCeBDepIHUpqROmhPtZ53jyna0aUajZRk0I3rXNA==",
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@types/d3-selection/-/d3-selection-2.0.1.tgz",
|
||||
"integrity": "sha512-3mhtPnGE+c71rl/T5HMy+ykg7migAZ4T6gzU0HxpgBFKcasBrSnwRbYV1/UZR6o5fkpySxhWxAhd7yhjj8jL7g==",
|
||||
"dev": true
|
||||
},
|
||||
"@types/d3-time": {
|
||||
"version": "1.0.10",
|
||||
"resolved": "https://registry.npmjs.org/@types/d3-time/-/d3-time-1.0.10.tgz",
|
||||
"integrity": "sha512-aKf62rRQafDQmSiv1NylKhIMmznsjRN+MnXRXTqHoqm0U/UZzVpdrtRnSIfdiLS616OuC1soYeX1dBg2n1u8Xw==",
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@types/d3-time/-/d3-time-2.1.1.tgz",
|
||||
"integrity": "sha512-9MVYlmIgmRR31C5b4FVSWtuMmBHh2mOWQYfl7XAYOa8dsnb7iEmUmRSWSFgXFtkjxO65d7hTUHQC+RhR/9IWFg==",
|
||||
"dev": true
|
||||
},
|
||||
"@types/dom4": {
|
||||
|
@ -34140,19 +34156,22 @@
|
|||
"integrity": "sha512-KxnUB0ZMlnUWCsx2Z8MUsr6qV6ja1w9ArPErJaJaF8a5SOWoHLIszeCTKGRGRgtLgYrs1E8CHkNSP1VZTTPc9g=="
|
||||
},
|
||||
"d3-array": {
|
||||
"version": "2.3.3",
|
||||
"resolved": "https://registry.npmjs.org/d3-array/-/d3-array-2.3.3.tgz",
|
||||
"integrity": "sha512-syv3wp0U5aB6toP2zb2OdBkhTy1MWDsCAaYk6OXJZv+G4u7bSWEmYgxLoFyc88RQUhZYGCebW9a9UD1gFi5+MQ=="
|
||||
"version": "2.12.1",
|
||||
"resolved": "https://registry.npmjs.org/d3-array/-/d3-array-2.12.1.tgz",
|
||||
"integrity": "sha512-B0ErZK/66mHtEsR1TkPEEkwdy+WDesimkM5gpZr5Dsg54BiTA5RXtYW5qTLIAcekaS9xfZrzBLF/OAkB3Qn1YQ==",
|
||||
"requires": {
|
||||
"internmap": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"d3-axis": {
|
||||
"version": "1.0.12",
|
||||
"resolved": "https://registry.npmjs.org/d3-axis/-/d3-axis-1.0.12.tgz",
|
||||
"integrity": "sha512-ejINPfPSNdGFKEOAtnBtdkpr24c4d4jsei6Lg98mxf424ivoDP2956/5HDpIAtmHo85lqT4pruy+zEgvRUBqaQ=="
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/d3-axis/-/d3-axis-2.1.0.tgz",
|
||||
"integrity": "sha512-z/G2TQMyuf0X3qP+Mh+2PimoJD41VOCjViJzT0BHeL/+JQAofkiWZbWxlwFGb1N8EN+Cl/CW+MUKbVzr1689Cw=="
|
||||
},
|
||||
"d3-color": {
|
||||
"version": "1.4.0",
|
||||
"resolved": "https://registry.npmjs.org/d3-color/-/d3-color-1.4.0.tgz",
|
||||
"integrity": "sha512-TzNPeJy2+iEepfiL92LAAB7fvnp/dV2YwANPVHdDWmYMm23qIJBYww3qT8I8C1wXrmrg4UWs7BKc2tKIgyjzHg=="
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/d3-color/-/d3-color-2.0.0.tgz",
|
||||
"integrity": "sha512-SPXi0TSKPD4g9tw0NMZFnR95XVgUZiBH+uUTqQuDu1OsE2zomHU7ho0FISciaPvosimixwHFl3WHLGabv6dDgQ=="
|
||||
},
|
||||
"d3-format": {
|
||||
"version": "1.4.1",
|
||||
|
@ -34160,29 +34179,39 @@
|
|||
"integrity": "sha512-TUswGe6hfguUX1CtKxyG2nymO+1lyThbkS1ifLX0Sr+dOQtAD5gkrffpHnx+yHNKUZ0Bmg5T4AjUQwugPDrm0g=="
|
||||
},
|
||||
"d3-interpolate": {
|
||||
"version": "1.3.2",
|
||||
"resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-1.3.2.tgz",
|
||||
"integrity": "sha512-NlNKGopqaz9qM1PXh9gBF1KSCVh+jSFErrSlD/4hybwoNX/gt1d8CDbDW+3i+5UOHhjC6s6nMvRxcuoMVNgL2w==",
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-2.0.1.tgz",
|
||||
"integrity": "sha512-c5UhwwTs/yybcmTpAVqwSFl6vrQ8JZJoT5F7xNFK9pymv5C0Ymcc9/LIJHtYIggg/yS9YHw8i8O8tgb9pupjeQ==",
|
||||
"requires": {
|
||||
"d3-color": "1"
|
||||
"d3-color": "1 - 2"
|
||||
}
|
||||
},
|
||||
"d3-scale": {
|
||||
"version": "3.2.0",
|
||||
"resolved": "https://registry.npmjs.org/d3-scale/-/d3-scale-3.2.0.tgz",
|
||||
"integrity": "sha512-1RnLYPmH3f2E96hSsCr3ok066myuAxoH3+pnlJAedeMOp7jeW7A+GZHAyVWWaStfphyPEBiDoLFA9zl+DcnC2Q==",
|
||||
"version": "3.3.0",
|
||||
"resolved": "https://registry.npmjs.org/d3-scale/-/d3-scale-3.3.0.tgz",
|
||||
"integrity": "sha512-1JGp44NQCt5d1g+Yy+GeOnZP7xHo0ii8zsQp6PGzd+C1/dl0KGsp9A7Mxwp+1D1o4unbTTxVdU/ZOIEBoeZPbQ==",
|
||||
"requires": {
|
||||
"d3-array": "1.2.0 - 2",
|
||||
"d3-format": "1",
|
||||
"d3-interpolate": "1",
|
||||
"d3-time": "1",
|
||||
"d3-time-format": "2"
|
||||
"d3-array": "^2.3.0",
|
||||
"d3-format": "1 - 2",
|
||||
"d3-interpolate": "1.2.0 - 2",
|
||||
"d3-time": "^2.1.1",
|
||||
"d3-time-format": "2 - 3"
|
||||
},
|
||||
"dependencies": {
|
||||
"d3-time": {
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/d3-time/-/d3-time-2.1.1.tgz",
|
||||
"integrity": "sha512-/eIQe/eR4kCQwq7yxi7z4c6qEXf2IYGcjoWB5OOQy4Tq9Uv39/947qlDcN2TLkiTzQWzvnsuYPB9TrWaNfipKQ==",
|
||||
"requires": {
|
||||
"d3-array": "2"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"d3-selection": {
|
||||
"version": "1.4.0",
|
||||
"resolved": "https://registry.npmjs.org/d3-selection/-/d3-selection-1.4.0.tgz",
|
||||
"integrity": "sha512-EYVwBxQGEjLCKF2pJ4+yrErskDnz5v403qvAid96cNdCMr8rmCYfY5RGzWz24mdIbxmDf6/4EAH+K9xperD5jg=="
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/d3-selection/-/d3-selection-2.0.0.tgz",
|
||||
"integrity": "sha512-XoGGqhLUN/W14NmaqcO/bb1nqjDAw5WtSYb2X8wiuQWvSZUsUVYsOSkOybUrNvcBjaywBdYPy03eXHMXjk9nZA=="
|
||||
},
|
||||
"d3-time": {
|
||||
"version": "1.1.0",
|
||||
|
@ -34562,9 +34591,9 @@
|
|||
}
|
||||
},
|
||||
"druid-query-toolkit": {
|
||||
"version": "0.15.1",
|
||||
"resolved": "https://registry.npmjs.org/druid-query-toolkit/-/druid-query-toolkit-0.15.1.tgz",
|
||||
"integrity": "sha512-yklAD1Ksokh2s+llEaR/lvvMHjW6CgdCG7X+JFnWsttM/xITpTFMu4Zo5/MqVvoo9021/deg87QQMCOKNfo3wQ==",
|
||||
"version": "0.15.3",
|
||||
"resolved": "https://registry.npmjs.org/druid-query-toolkit/-/druid-query-toolkit-0.15.3.tgz",
|
||||
"integrity": "sha512-AtKnmCDWSHh1+bHFs36drJAKQ60914ttF62x3wIxXbD1zoUEmwwsX4Ks2zD3SItantVS4fTj3cyjMl/c0fojpA==",
|
||||
"requires": {
|
||||
"tslib": "^2.3.1"
|
||||
}
|
||||
|
@ -37367,6 +37396,11 @@
|
|||
"side-channel": "^1.0.4"
|
||||
}
|
||||
},
|
||||
"internmap": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/internmap/-/internmap-1.0.1.tgz",
|
||||
"integrity": "sha512-lDB5YccMydFBtasVtxnZ3MRBHuaoE8GKsppq+EchKL2U4nK/DmEpPHNH8MZe5HkMtpSiTSOZwfN0tzYjO/lJEw=="
|
||||
},
|
||||
"interpret": {
|
||||
"version": "2.2.0",
|
||||
"resolved": "https://registry.npmjs.org/interpret/-/interpret-2.2.0.tgz",
|
||||
|
|
|
@ -75,11 +75,11 @@
|
|||
"classnames": "^2.2.6",
|
||||
"copy-to-clipboard": "^3.2.0",
|
||||
"core-js": "^3.10.1",
|
||||
"d3-array": "^2.3.3",
|
||||
"d3-axis": "^1.0.12",
|
||||
"d3-scale": "^3.2.0",
|
||||
"d3-selection": "^1.4.0",
|
||||
"druid-query-toolkit": "^0.15.1",
|
||||
"d3-array": "^2.12.1",
|
||||
"d3-axis": "^2.1.0",
|
||||
"d3-scale": "^3.3.0",
|
||||
"d3-selection": "^2.0.0",
|
||||
"druid-query-toolkit": "^0.15.3",
|
||||
"file-saver": "^2.0.2",
|
||||
"follow-redirects": "^1.14.7",
|
||||
"fontsource-open-sans": "^3.0.9",
|
||||
|
@ -111,10 +111,10 @@
|
|||
"@babel/preset-env": "^7.14.4",
|
||||
"@testing-library/react": "^8.0.9",
|
||||
"@types/classnames": "^2.2.9",
|
||||
"@types/d3-array": "^2.0.0",
|
||||
"@types/d3-axis": "^1.0.12",
|
||||
"@types/d3-scale": "^2.1.1",
|
||||
"@types/d3-selection": "^1.4.1",
|
||||
"@types/d3-array": "^2.12.3",
|
||||
"@types/d3-axis": "^2.1.3",
|
||||
"@types/d3-scale": "^3.3.2",
|
||||
"@types/d3-selection": "^2.0.1",
|
||||
"@types/enzyme": "^3.10.3",
|
||||
"@types/enzyme-adapter-react-16": "^1.0.5",
|
||||
"@types/file-saver": "^2.0.1",
|
||||
|
|
|
@ -0,0 +1,200 @@
|
|||
/*
|
||||
* 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 { Menu, MenuItem } from '@blueprintjs/core';
|
||||
import { IconNames } from '@blueprintjs/icons';
|
||||
import {
|
||||
Column,
|
||||
SqlComparison,
|
||||
SqlExpression,
|
||||
SqlLiteral,
|
||||
SqlQuery,
|
||||
SqlRecord,
|
||||
SqlRef,
|
||||
trimString,
|
||||
} from 'druid-query-toolkit';
|
||||
import React from 'react';
|
||||
|
||||
import { copyAndAlert, prettyPrintSql, QueryAction, stringifyValue } from '../../utils';
|
||||
|
||||
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 !== '';
|
||||
}
|
||||
|
||||
function addToClause(clause: SqlExpression, newValue: SqlLiteral): SqlExpression | undefined {
|
||||
if (!(clause instanceof SqlComparison)) return;
|
||||
const { op, lhs, rhs } = clause;
|
||||
|
||||
switch (op) {
|
||||
case '=':
|
||||
if (!(rhs instanceof SqlLiteral)) return;
|
||||
if (rhs.equals(newValue)) return;
|
||||
return lhs.in([rhs, newValue]);
|
||||
|
||||
case '<>':
|
||||
if (!(rhs instanceof SqlLiteral)) return;
|
||||
if (rhs.equals(newValue)) return;
|
||||
return lhs.notIn([rhs, newValue]);
|
||||
|
||||
case 'IN':
|
||||
if (!(rhs instanceof SqlRecord)) return;
|
||||
if (rhs.contains(newValue)) return;
|
||||
return clause.changeRhs(rhs.prepend(newValue));
|
||||
|
||||
default:
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
function clipboardMenuItem(clause: SqlExpression) {
|
||||
const prettyLabel = prettyPrintSql(clause);
|
||||
return (
|
||||
<MenuItem
|
||||
icon={IconNames.CLIPBOARD}
|
||||
text={`Copy: ${prettyLabel}`}
|
||||
onClick={() => copyAndAlert(clause.toString(), `${prettyLabel} copied to clipboard`)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export interface CellFilterMenuProps {
|
||||
column: Column;
|
||||
value: unknown;
|
||||
headerIndex: number;
|
||||
runeMode?: boolean;
|
||||
query: SqlQuery | undefined;
|
||||
onQueryAction?(action: QueryAction): void;
|
||||
onShowFullValue?(valueString: string): void;
|
||||
}
|
||||
|
||||
export function CellFilterMenu(props: CellFilterMenuProps) {
|
||||
const { column, value, runeMode, headerIndex, query, onQueryAction, onShowFullValue } = props;
|
||||
|
||||
const showFullValueMenuItem = onShowFullValue ? (
|
||||
<MenuItem
|
||||
icon={IconNames.EYE_OPEN}
|
||||
text="Show full value"
|
||||
onClick={() => {
|
||||
onShowFullValue(stringifyValue(value));
|
||||
}}
|
||||
/>
|
||||
) : undefined;
|
||||
|
||||
const val = sqlLiteralForColumnValue(column, value);
|
||||
|
||||
if (query) {
|
||||
let ex: SqlExpression | undefined;
|
||||
let having = false;
|
||||
if (query.hasStarInSelect()) {
|
||||
ex = SqlRef.column(column.name);
|
||||
} else {
|
||||
const selectValue = query.getSelectExpressionForIndex(headerIndex);
|
||||
if (selectValue) {
|
||||
const outputName = selectValue.getOutputName();
|
||||
having = query.isAggregateSelectIndex(headerIndex);
|
||||
if (having && outputName) {
|
||||
ex = SqlRef.column(outputName);
|
||||
} else {
|
||||
ex = selectValue.getUnderlyingExpression();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const filterOnMenuItem = (clause: SqlExpression) => {
|
||||
if (!onQueryAction) return;
|
||||
return (
|
||||
<MenuItem
|
||||
icon={IconNames.FILTER}
|
||||
text={`${having ? 'Having' : 'Filter on'}: ${prettyPrintSql(clause)}`}
|
||||
onClick={() => {
|
||||
const column = clause.getUsedColumns()[0];
|
||||
onQueryAction(
|
||||
having
|
||||
? q => q.removeFromHaving(column).addHaving(clause)
|
||||
: q => q.removeColumnFromWhere(column).addWhere(clause),
|
||||
);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
const currentFilterExpression = having
|
||||
? query.getHavingExpression()
|
||||
: query.getWhereExpression();
|
||||
|
||||
const currentClauses =
|
||||
currentFilterExpression
|
||||
?.decomposeViaAnd()
|
||||
?.filter(ex => String(ex.getUsedColumns()) === column.name) || [];
|
||||
|
||||
const updatedClause =
|
||||
currentClauses.length === 1 && val ? addToClause(currentClauses[0], val) : undefined;
|
||||
console.log(updatedClause, currentClauses);
|
||||
|
||||
const jsonColumn = column.nativeType === 'COMPLEX<json>';
|
||||
return (
|
||||
<Menu>
|
||||
{ex?.getFirstColumn() && val && !jsonColumn && (
|
||||
<>
|
||||
{updatedClause && filterOnMenuItem(updatedClause)}
|
||||
{filterOnMenuItem(ex.equal(val))}
|
||||
{filterOnMenuItem(ex.unequal(val))}
|
||||
{isComparable(value) && (
|
||||
<>
|
||||
{filterOnMenuItem(ex.greaterThanOrEqual(val))}
|
||||
{filterOnMenuItem(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`)}
|
||||
/>
|
||||
{!runeMode && val && (
|
||||
<>
|
||||
{clipboardMenuItem(ref.equal(val))}
|
||||
{clipboardMenuItem(ref.unequal(val))}
|
||||
</>
|
||||
)}
|
||||
{showFullValueMenuItem}
|
||||
</Menu>
|
||||
);
|
||||
}
|
||||
}
|
|
@ -20,7 +20,6 @@ exports[`HeaderBar matches snapshot 1`] = `
|
|||
href="#workbench"
|
||||
icon="application"
|
||||
minimal={true}
|
||||
onClick={[Function]}
|
||||
text="Query"
|
||||
/>
|
||||
<Blueprint4.Popover2
|
||||
|
|
|
@ -65,7 +65,6 @@ export type HeaderActiveTab =
|
|||
| 'datasources'
|
||||
| 'segments'
|
||||
| 'services'
|
||||
| 'query'
|
||||
| 'workbench'
|
||||
| 'sql-data-loader'
|
||||
| 'lookups';
|
||||
|
@ -388,16 +387,11 @@ export const HeaderBar = React.memo(function HeaderBar(props: HeaderBarProps) {
|
|||
<AnchorButton
|
||||
className="header-entry"
|
||||
minimal
|
||||
active={oneOf(active, 'workbench', 'query')}
|
||||
active={active === 'workbench'}
|
||||
icon={IconNames.APPLICATION}
|
||||
text="Query"
|
||||
href="#workbench"
|
||||
disabled={!capabilities.hasQuerying()}
|
||||
onClick={e => {
|
||||
if (!e.altKey) return;
|
||||
e.preventDefault();
|
||||
location.hash = '#query';
|
||||
}}
|
||||
/>
|
||||
{showSplitDataLoaderMenu ? (
|
||||
<Popover2
|
||||
|
|
|
@ -84,6 +84,10 @@
|
|||
margin-top: 3px;
|
||||
margin-right: 5px;
|
||||
}
|
||||
|
||||
.filter-icon {
|
||||
margin-top: 2px;
|
||||
}
|
||||
}
|
||||
|
||||
.formula {
|
||||
|
|
|
@ -16,18 +16,11 @@
|
|||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { Button, Icon, Menu, MenuItem } from '@blueprintjs/core';
|
||||
import { IconName, IconNames } from '@blueprintjs/icons';
|
||||
import { Button, Icon } from '@blueprintjs/core';
|
||||
import { 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 { Column, QueryResult } from 'druid-query-toolkit';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import ReactTable from 'react-table';
|
||||
|
||||
|
@ -36,43 +29,25 @@ import { SMALL_TABLE_PAGE_SIZE, SMALL_TABLE_PAGE_SIZE_OPTIONS } from '../../reac
|
|||
import {
|
||||
columnToIcon,
|
||||
columnToWidth,
|
||||
copyAndAlert,
|
||||
filterMap,
|
||||
formatNumber,
|
||||
getNumericColumnBraces,
|
||||
Pagination,
|
||||
prettyPrintSql,
|
||||
stringifyValue,
|
||||
} from '../../utils';
|
||||
import { BracedText } from '../braced-text/braced-text';
|
||||
import { CellFilterMenu } from '../cell-filter-menu/cell-filter-menu';
|
||||
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 { queryResult, initPageSize } = props;
|
||||
const parsedQuery = queryResult.sqlQuery;
|
||||
const [pagination, setPagination] = useState<Pagination>({
|
||||
page: 0,
|
||||
|
@ -96,95 +71,17 @@ export const RecordTablePane = React.memo(function RecordTablePane(props: Record
|
|||
);
|
||||
}
|
||||
|
||||
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));
|
||||
}}
|
||||
return (
|
||||
<CellFilterMenu
|
||||
column={column}
|
||||
value={value}
|
||||
headerIndex={headerIndex}
|
||||
query={parsedQuery}
|
||||
onQueryAction={undefined}
|
||||
onShowFullValue={setShowValue}
|
||||
/>
|
||||
);
|
||||
|
||||
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) {
|
||||
|
|
|
@ -20,7 +20,7 @@ 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 { Redirect, RouteComponentProps } from 'react-router';
|
||||
import { HashRouter, Route, Switch } from 'react-router-dom';
|
||||
|
||||
import { HeaderActiveTab, HeaderBar, Loader } from './components';
|
||||
|
@ -33,7 +33,6 @@ import {
|
|||
IngestionView,
|
||||
LoadDataView,
|
||||
LookupsView,
|
||||
QueryView,
|
||||
SegmentsView,
|
||||
ServicesView,
|
||||
SqlDataLoaderView,
|
||||
|
@ -245,20 +244,6 @@ export class ConsoleApplication extends React.PureComponent<
|
|||
);
|
||||
};
|
||||
|
||||
private readonly wrappedQueryView = () => {
|
||||
const { defaultQueryContext, mandatoryQueryContext } = this.props;
|
||||
|
||||
return this.wrapInViewContainer(
|
||||
'query',
|
||||
<QueryView
|
||||
initQuery={this.queryWithContext?.queryString}
|
||||
defaultQueryContext={defaultQueryContext}
|
||||
mandatoryQueryContext={mandatoryQueryContext}
|
||||
/>,
|
||||
'thin',
|
||||
);
|
||||
};
|
||||
|
||||
private readonly wrappedWorkbenchView = (p: RouteComponentProps<any>) => {
|
||||
const { defaultQueryContext, mandatoryQueryContext } = this.props;
|
||||
const { capabilities } = this.state;
|
||||
|
@ -384,7 +369,9 @@ export class ConsoleApplication extends React.PureComponent<
|
|||
<Route path="/segments" component={this.wrappedSegmentsView} />
|
||||
<Route path="/services" component={this.wrappedServicesView} />
|
||||
|
||||
<Route path="/query" component={this.wrappedQueryView} />
|
||||
<Route path="/query">
|
||||
<Redirect to="/workbench" />
|
||||
</Route>
|
||||
<Route
|
||||
path={['/workbench/:tabId', '/workbench']}
|
||||
component={this.wrappedWorkbenchView}
|
||||
|
|
|
@ -8,210 +8,6 @@ exports[`CoordinatorDynamicConfigDialog matches snapshot 1`] = `
|
|||
saveDisabled={false}
|
||||
title="Coordinator dynamic config"
|
||||
>
|
||||
<p>
|
||||
Edit the coordinator dynamic configuration on the fly. For more information please refer to the
|
||||
|
||||
<Memo(ExternalLink)
|
||||
href="https://druid.apache.org/docs/latest/configuration/index.html#dynamic-configuration"
|
||||
>
|
||||
documentation
|
||||
</Memo(ExternalLink)>
|
||||
.
|
||||
</p>
|
||||
<Memo(FormJsonSelector)
|
||||
onChange={[Function]}
|
||||
tab="form"
|
||||
/>
|
||||
<AutoForm
|
||||
fields={
|
||||
Array [
|
||||
Object {
|
||||
"defaultValue": 5,
|
||||
"info": <React.Fragment>
|
||||
The maximum number of segments that can be moved at any given time.
|
||||
</React.Fragment>,
|
||||
"name": "maxSegmentsToMove",
|
||||
"type": "number",
|
||||
},
|
||||
Object {
|
||||
"defaultValue": 1,
|
||||
"info": <React.Fragment>
|
||||
Thread pool size for computing moving cost of segments in segment balancing. Consider increasing this if you have a lot of segments and moving segments starts to get stuck.
|
||||
</React.Fragment>,
|
||||
"name": "balancerComputeThreads",
|
||||
"type": "number",
|
||||
},
|
||||
Object {
|
||||
"defaultValue": false,
|
||||
"info": <React.Fragment>
|
||||
Boolean flag for whether or not we should emit balancing stats. This is an expensive operation.
|
||||
</React.Fragment>,
|
||||
"name": "emitBalancingStats",
|
||||
"type": "boolean",
|
||||
},
|
||||
Object {
|
||||
"defaultValue": false,
|
||||
"info": <React.Fragment>
|
||||
Send kill tasks for ALL dataSources if property
|
||||
<Unknown>
|
||||
druid.coordinator.kill.on
|
||||
</Unknown>
|
||||
is true. If this is set to true then
|
||||
<Unknown>
|
||||
killDataSourceWhitelist
|
||||
</Unknown>
|
||||
must not be specified or be empty list.
|
||||
</React.Fragment>,
|
||||
"name": "killAllDataSources",
|
||||
"type": "boolean",
|
||||
},
|
||||
Object {
|
||||
"emptyValue": Array [],
|
||||
"info": <React.Fragment>
|
||||
List of dataSources for which kill tasks are sent if property
|
||||
|
||||
<Unknown>
|
||||
druid.coordinator.kill.on
|
||||
</Unknown>
|
||||
is true. This can be a list of comma-separated dataSources or a JSON array.
|
||||
</React.Fragment>,
|
||||
"name": "killDataSourceWhitelist",
|
||||
"type": "string-array",
|
||||
},
|
||||
Object {
|
||||
"emptyValue": Array [],
|
||||
"info": <React.Fragment>
|
||||
List of dataSources for which pendingSegments are NOT cleaned up if property
|
||||
|
||||
<Unknown>
|
||||
druid.coordinator.kill.pendingSegments.on
|
||||
</Unknown>
|
||||
is true. This can be a list of comma-separated dataSources or a JSON array.
|
||||
</React.Fragment>,
|
||||
"name": "killPendingSegmentsSkipList",
|
||||
"type": "string-array",
|
||||
},
|
||||
Object {
|
||||
"defaultValue": 0,
|
||||
"info": <React.Fragment>
|
||||
The maximum number of segments that could be queued for loading to any given server. This parameter could be used to speed up segments loading process, especially if there are "slow" nodes in the cluster (with low loading speed) or if too much segments scheduled to be replicated to some particular node (faster loading could be preferred to better segments distribution). Desired value depends on segments loading speed, acceptable replication time and number of nodes. Value 1000 could be a start point for a rather big cluster. Default value is 0 (loading queue is unbounded)
|
||||
</React.Fragment>,
|
||||
"name": "maxSegmentsInNodeLoadingQueue",
|
||||
"type": "number",
|
||||
},
|
||||
Object {
|
||||
"defaultValue": 524288000,
|
||||
"info": <React.Fragment>
|
||||
The maximum total uncompressed size in bytes of segments to merge.
|
||||
</React.Fragment>,
|
||||
"name": "mergeBytesLimit",
|
||||
"type": "size-bytes",
|
||||
},
|
||||
Object {
|
||||
"defaultValue": 100,
|
||||
"info": <React.Fragment>
|
||||
The maximum number of segments that can be in a single append task.
|
||||
</React.Fragment>,
|
||||
"name": "mergeSegmentsLimit",
|
||||
"type": "number",
|
||||
},
|
||||
Object {
|
||||
"defaultValue": 900000,
|
||||
"info": <React.Fragment>
|
||||
How long does the Coordinator need to be active before it can start removing (marking unused) segments in metadata storage.
|
||||
</React.Fragment>,
|
||||
"name": "millisToWaitBeforeDeleting",
|
||||
"type": "number",
|
||||
},
|
||||
Object {
|
||||
"defaultValue": 15,
|
||||
"info": <React.Fragment>
|
||||
The maximum number of Coordinator runs for a segment to be replicated before we start alerting.
|
||||
</React.Fragment>,
|
||||
"name": "replicantLifetime",
|
||||
"type": "number",
|
||||
},
|
||||
Object {
|
||||
"defaultValue": 10,
|
||||
"info": <React.Fragment>
|
||||
The maximum number of segments that can be replicated at one time.
|
||||
</React.Fragment>,
|
||||
"name": "replicationThrottleLimit",
|
||||
"type": "number",
|
||||
},
|
||||
Object {
|
||||
"emptyValue": Array [],
|
||||
"info": <React.Fragment>
|
||||
List of historical services to 'decommission'. Coordinator will not assign new segments to 'decommissioning' services, and segments will be moved away from them to be placed on non-decommissioning services at the maximum rate specified by
|
||||
|
||||
<Unknown>
|
||||
decommissioningMaxPercentOfMaxSegmentsToMove
|
||||
</Unknown>
|
||||
.
|
||||
</React.Fragment>,
|
||||
"name": "decommissioningNodes",
|
||||
"type": "string-array",
|
||||
},
|
||||
Object {
|
||||
"defaultValue": 70,
|
||||
"info": <React.Fragment>
|
||||
The maximum number of segments that may be moved away from 'decommissioning' services to non-decommissioning (that is, active) services during one Coordinator run. This value is relative to the total maximum segment movements allowed during one run which is determined by
|
||||
<Unknown>
|
||||
maxSegmentsToMove
|
||||
</Unknown>
|
||||
. If
|
||||
<Unknown>
|
||||
decommissioningMaxPercentOfMaxSegmentsToMove
|
||||
</Unknown>
|
||||
is 0, segments will neither be moved from or to 'decommissioning' services, effectively putting them in a sort of "maintenance" mode that will not participate in balancing or assignment by load rules. Decommissioning can also become stalled if there are no available active services to place the segments. By leveraging the maximum percent of decommissioning segment movements, an operator can prevent active services from overload by prioritizing balancing, or decrease decommissioning time instead. The value should be between 0 and 100.
|
||||
</React.Fragment>,
|
||||
"name": "decommissioningMaxPercentOfMaxSegmentsToMove",
|
||||
"type": "number",
|
||||
},
|
||||
Object {
|
||||
"defaultValue": false,
|
||||
"info": <React.Fragment>
|
||||
Boolean flag for whether or not we should use the Reservoir Sampling with a reservoir of size k instead of fixed size 1 to pick segments to move. This option can be enabled to speed up segment balancing process, especially if there are huge number of segments in the cluster or if there are too many segments to move.
|
||||
</React.Fragment>,
|
||||
"name": "useBatchedSegmentSampler",
|
||||
"type": "boolean",
|
||||
},
|
||||
Object {
|
||||
"defaultValue": 100,
|
||||
"info": <React.Fragment>
|
||||
Deprecated. This will eventually be phased out by the batched segment sampler. You can enable the batched segment sampler now by setting the dynamic Coordinator config, useBatchedSegmentSampler, to true. Note that if you choose to enable the batched segment sampler, percentOfSegmentsToConsiderPerMove will no longer have any effect on balancing. If useBatchedSegmentSampler == false, this config defines the percentage of the total number of segments in the cluster that are considered every time a segment needs to be selected for a move. Druid orders servers by available capacity ascending (the least available capacity first) and then iterates over the servers. For each server, Druid iterates over the segments on the server, considering them for moving. The default config of 100% means that every segment on every server is a candidate to be moved. This should make sense for most small to medium-sized clusters. However, an admin may find it preferable to drop this value lower if they don't think that it is worthwhile to consider every single segment in the cluster each time it is looking for a segment to move.
|
||||
</React.Fragment>,
|
||||
"name": "percentOfSegmentsToConsiderPerMove",
|
||||
"type": "number",
|
||||
},
|
||||
Object {
|
||||
"defaultValue": false,
|
||||
"info": <React.Fragment>
|
||||
Boolean flag for whether or not the coordinator should execute its various duties of coordinating the cluster. Setting this to true essentially pauses all coordination work while allowing the API to remain up.
|
||||
</React.Fragment>,
|
||||
"name": "pauseCoordination",
|
||||
"type": "boolean",
|
||||
},
|
||||
Object {
|
||||
"defaultValue": false,
|
||||
"info": <React.Fragment>
|
||||
Boolean flag for whether or not additional replication is needed for segments that have failed to load due to the expiry of coordinator load timeout. If this is set to true, the coordinator will attempt to replicate the failed segment on a different historical server.
|
||||
</React.Fragment>,
|
||||
"name": "replicateAfterLoadTimeout",
|
||||
"type": "boolean",
|
||||
},
|
||||
Object {
|
||||
"defaultValue": 2147483647,
|
||||
"info": <React.Fragment>
|
||||
The maximum number of non-primary replicants to load in a single Coordinator cycle. Once this limit is hit, only primary replicants will be loaded for the remainder of the cycle. Tuning this value lower can help reduce the delay in loading primary segments when the cluster has a very large number of non-primary replicants to load (such as when a single historical drops out of the cluster leaving many under-replicated segments).
|
||||
</React.Fragment>,
|
||||
"name": "maxNonPrimaryReplicantsToLoad",
|
||||
"type": "number",
|
||||
},
|
||||
]
|
||||
}
|
||||
model={Object {}}
|
||||
onChange={[Function]}
|
||||
/>
|
||||
<Memo(Loader) />
|
||||
</SnitchDialog>
|
||||
`;
|
||||
|
|
|
@ -24,6 +24,12 @@
|
|||
top: 5%;
|
||||
}
|
||||
|
||||
.loader {
|
||||
position: relative;
|
||||
height: 60vh;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.#{$bp-ns}-dialog-body {
|
||||
max-height: 70vh;
|
||||
|
||||
|
|
|
@ -26,6 +26,7 @@ import {
|
|||
FormJsonSelector,
|
||||
FormJsonTabs,
|
||||
JsonInput,
|
||||
Loader,
|
||||
} from '../../components';
|
||||
import { COORDINATOR_DYNAMIC_CONFIG_FIELDS, CoordinatorDynamicConfig } from '../../druid-models';
|
||||
import { useQueryManager } from '../../hooks';
|
||||
|
@ -37,7 +38,7 @@ import { SnitchDialog } from '..';
|
|||
import './coordinator-dynamic-config-dialog.scss';
|
||||
|
||||
export interface CoordinatorDynamicConfigDialogProps {
|
||||
onClose: () => void;
|
||||
onClose(): void;
|
||||
}
|
||||
|
||||
export const CoordinatorDynamicConfigDialog = React.memo(function CoordinatorDynamicConfigDialog(
|
||||
|
@ -45,18 +46,19 @@ export const CoordinatorDynamicConfigDialog = React.memo(function CoordinatorDyn
|
|||
) {
|
||||
const { onClose } = props;
|
||||
const [currentTab, setCurrentTab] = useState<FormJsonTabs>('form');
|
||||
const [dynamicConfig, setDynamicConfig] = useState<CoordinatorDynamicConfig>({});
|
||||
const [dynamicConfig, setDynamicConfig] = useState<CoordinatorDynamicConfig | undefined>();
|
||||
const [jsonError, setJsonError] = useState<Error | undefined>();
|
||||
|
||||
const [historyRecordsState] = useQueryManager<null, any[]>({
|
||||
initQuery: null,
|
||||
processQuery: async () => {
|
||||
const historyResp = await Api.instance.get(`/druid/coordinator/v1/config/history?count=100`);
|
||||
return historyResp.data;
|
||||
},
|
||||
initQuery: null,
|
||||
});
|
||||
|
||||
useQueryManager<null, Record<string, any>>({
|
||||
initQuery: null,
|
||||
processQuery: async () => {
|
||||
try {
|
||||
const configResp = await Api.instance.get('/druid/coordinator/v1/config');
|
||||
|
@ -67,12 +69,10 @@ export const CoordinatorDynamicConfigDialog = React.memo(function CoordinatorDyn
|
|||
intent: Intent.DANGER,
|
||||
message: `Could not load coordinator dynamic config: ${getDruidErrorMessage(e)}`,
|
||||
});
|
||||
setDynamicConfig({});
|
||||
onClose();
|
||||
}
|
||||
return {};
|
||||
},
|
||||
initQuery: null,
|
||||
});
|
||||
|
||||
async function saveConfig(comment: string) {
|
||||
|
@ -107,31 +107,39 @@ export const CoordinatorDynamicConfigDialog = React.memo(function CoordinatorDyn
|
|||
title="Coordinator dynamic config"
|
||||
historyRecords={historyRecordsState.data}
|
||||
>
|
||||
<p>
|
||||
Edit the coordinator dynamic configuration on the fly. For more information please refer to
|
||||
the{' '}
|
||||
<ExternalLink href={`${getLink('DOCS')}/configuration/index.html#dynamic-configuration`}>
|
||||
documentation
|
||||
</ExternalLink>
|
||||
.
|
||||
</p>
|
||||
<FormJsonSelector tab={currentTab} onChange={setCurrentTab} />
|
||||
{currentTab === 'form' ? (
|
||||
<AutoForm
|
||||
fields={COORDINATOR_DYNAMIC_CONFIG_FIELDS}
|
||||
model={dynamicConfig}
|
||||
onChange={setDynamicConfig}
|
||||
/>
|
||||
{dynamicConfig ? (
|
||||
<>
|
||||
<p>
|
||||
Edit the coordinator dynamic configuration on the fly. For more information please refer
|
||||
to the{' '}
|
||||
<ExternalLink
|
||||
href={`${getLink('DOCS')}/configuration/index.html#dynamic-configuration`}
|
||||
>
|
||||
documentation
|
||||
</ExternalLink>
|
||||
.
|
||||
</p>
|
||||
<FormJsonSelector tab={currentTab} onChange={setCurrentTab} />
|
||||
{currentTab === 'form' ? (
|
||||
<AutoForm
|
||||
fields={COORDINATOR_DYNAMIC_CONFIG_FIELDS}
|
||||
model={dynamicConfig}
|
||||
onChange={setDynamicConfig}
|
||||
/>
|
||||
) : (
|
||||
<JsonInput
|
||||
value={dynamicConfig}
|
||||
height="50vh"
|
||||
onChange={v => {
|
||||
setDynamicConfig(v);
|
||||
setJsonError(undefined);
|
||||
}}
|
||||
onError={setJsonError}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
<JsonInput
|
||||
value={dynamicConfig}
|
||||
height="50vh"
|
||||
onChange={v => {
|
||||
setDynamicConfig(v);
|
||||
setJsonError(undefined);
|
||||
}}
|
||||
onError={setJsonError}
|
||||
/>
|
||||
<Loader />
|
||||
)}
|
||||
</SnitchDialog>
|
||||
);
|
||||
|
|
|
@ -7,31 +7,6 @@ exports[`OverlordDynamicConfigDialog matches snapshot 1`] = `
|
|||
onSave={[Function]}
|
||||
title="Overlord dynamic config"
|
||||
>
|
||||
<p>
|
||||
Edit the overlord dynamic configuration on the fly. For more information please refer to the
|
||||
|
||||
<Memo(ExternalLink)
|
||||
href="https://druid.apache.org/docs/latest/configuration/index.html#overlord-dynamic-configuration"
|
||||
>
|
||||
documentation
|
||||
</Memo(ExternalLink)>
|
||||
.
|
||||
</p>
|
||||
<AutoForm
|
||||
fields={
|
||||
Array [
|
||||
Object {
|
||||
"name": "selectStrategy",
|
||||
"type": "json",
|
||||
},
|
||||
Object {
|
||||
"name": "autoScaler",
|
||||
"type": "json",
|
||||
},
|
||||
]
|
||||
}
|
||||
model={Object {}}
|
||||
onChange={[Function]}
|
||||
/>
|
||||
<Memo(Loader) />
|
||||
</SnitchDialog>
|
||||
`;
|
||||
|
|
|
@ -25,6 +25,12 @@
|
|||
width: 600px;
|
||||
}
|
||||
|
||||
.loader {
|
||||
position: relative;
|
||||
height: 60vh;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.#{$bp-ns}-dialog-body {
|
||||
max-height: 70vh;
|
||||
|
||||
|
|
|
@ -20,7 +20,7 @@ import { Intent } from '@blueprintjs/core';
|
|||
import { IconNames } from '@blueprintjs/icons';
|
||||
import React, { useState } from 'react';
|
||||
|
||||
import { AutoForm, ExternalLink } from '../../components';
|
||||
import { AutoForm, ExternalLink, Loader } from '../../components';
|
||||
import { OVERLORD_DYNAMIC_CONFIG_FIELDS, OverlordDynamicConfig } from '../../druid-models';
|
||||
import { useQueryManager } from '../../hooks';
|
||||
import { getLink } from '../../links';
|
||||
|
@ -31,24 +31,25 @@ import { SnitchDialog } from '..';
|
|||
import './overlord-dynamic-config-dialog.scss';
|
||||
|
||||
export interface OverlordDynamicConfigDialogProps {
|
||||
onClose: () => void;
|
||||
onClose(): void;
|
||||
}
|
||||
|
||||
export const OverlordDynamicConfigDialog = React.memo(function OverlordDynamicConfigDialog(
|
||||
props: OverlordDynamicConfigDialogProps,
|
||||
) {
|
||||
const { onClose } = props;
|
||||
const [dynamicConfig, setDynamicConfig] = useState<OverlordDynamicConfig>({});
|
||||
const [dynamicConfig, setDynamicConfig] = useState<OverlordDynamicConfig | undefined>();
|
||||
|
||||
const [historyRecordsState] = useQueryManager<null, any[]>({
|
||||
initQuery: null,
|
||||
processQuery: async () => {
|
||||
const historyResp = await Api.instance.get(`/druid/indexer/v1/worker/history?count=100`);
|
||||
return historyResp.data;
|
||||
},
|
||||
initQuery: null,
|
||||
});
|
||||
|
||||
useQueryManager<null, Record<string, any>>({
|
||||
initQuery: null,
|
||||
processQuery: async () => {
|
||||
try {
|
||||
const configResp = await Api.instance.get(`/druid/indexer/v1/worker`);
|
||||
|
@ -59,12 +60,10 @@ export const OverlordDynamicConfigDialog = React.memo(function OverlordDynamicCo
|
|||
intent: Intent.DANGER,
|
||||
message: `Could not load overlord dynamic config: ${getDruidErrorMessage(e)}`,
|
||||
});
|
||||
setDynamicConfig({});
|
||||
onClose();
|
||||
}
|
||||
return {};
|
||||
},
|
||||
initQuery: null,
|
||||
});
|
||||
|
||||
async function saveConfig(comment: string) {
|
||||
|
@ -98,20 +97,27 @@ export const OverlordDynamicConfigDialog = React.memo(function OverlordDynamicCo
|
|||
title="Overlord dynamic config"
|
||||
historyRecords={historyRecordsState.data}
|
||||
>
|
||||
<p>
|
||||
Edit the overlord dynamic configuration on the fly. For more information please refer to the{' '}
|
||||
<ExternalLink
|
||||
href={`${getLink('DOCS')}/configuration/index.html#overlord-dynamic-configuration`}
|
||||
>
|
||||
documentation
|
||||
</ExternalLink>
|
||||
.
|
||||
</p>
|
||||
<AutoForm
|
||||
fields={OVERLORD_DYNAMIC_CONFIG_FIELDS}
|
||||
model={dynamicConfig}
|
||||
onChange={setDynamicConfig}
|
||||
/>
|
||||
{dynamicConfig ? (
|
||||
<>
|
||||
<p>
|
||||
Edit the overlord dynamic configuration on the fly. For more information please refer to
|
||||
the{' '}
|
||||
<ExternalLink
|
||||
href={`${getLink('DOCS')}/configuration/index.html#overlord-dynamic-configuration`}
|
||||
>
|
||||
documentation
|
||||
</ExternalLink>
|
||||
.
|
||||
</p>
|
||||
<AutoForm
|
||||
fields={OVERLORD_DYNAMIC_CONFIG_FIELDS}
|
||||
model={dynamicConfig}
|
||||
onChange={setDynamicConfig}
|
||||
/>
|
||||
</>
|
||||
) : (
|
||||
<Loader />
|
||||
)}
|
||||
</SnitchDialog>
|
||||
);
|
||||
});
|
||||
|
|
|
@ -40,7 +40,6 @@ export const LocalStorageKeys = {
|
|||
QUERY_KEY: 'druid-console-query' as const,
|
||||
QUERY_CONTEXT: 'query-context' as const,
|
||||
INGESTION_VIEW_PANE_SIZE: 'ingestion-view-pane-size' as const,
|
||||
QUERY_VIEW_PANE_SIZE: 'query-view-pane-size' as const,
|
||||
TASKS_REFRESH_RATE: 'task-refresh-rate' as const,
|
||||
DATASOURCES_REFRESH_RATE: 'datasources-refresh-rate' as const,
|
||||
SEGMENTS_REFRESH_RATE: 'segments-refresh-rate' as const,
|
||||
|
|
|
@ -21,7 +21,6 @@ export * from './home-view/home-view';
|
|||
export * from './ingestion-view/ingestion-view';
|
||||
export * from './load-data-view/load-data-view';
|
||||
export * from './lookups-view/lookups-view';
|
||||
export * from './query-view/query-view';
|
||||
export * from './segments-view/segments-view';
|
||||
export * from './services-view/services-view';
|
||||
export * from './sql-data-loader-view/sql-data-loader-view';
|
||||
|
|
|
@ -921,7 +921,7 @@ ORDER BY
|
|||
if (value > 0) {
|
||||
return formatDuration(value);
|
||||
}
|
||||
if (original.created_time) {
|
||||
if (oneOf(original.status, 'RUNNING', 'PENDING') && original.created_time) {
|
||||
// Compute running duration from the created time if it exists
|
||||
return formatDuration(Date.now() - Date.parse(original.created_time));
|
||||
}
|
||||
|
|
|
@ -1,163 +0,0 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`QueryView matches snapshot 1`] = `
|
||||
<div
|
||||
className="query-view app-view"
|
||||
>
|
||||
<ColumnTree
|
||||
columnMetadataLoading={true}
|
||||
defaultSchema="druid"
|
||||
defaultWhere={"__time >= CURRENT_TIMESTAMP - INTERVAL '1' DAY"}
|
||||
getParsedQuery={[Function]}
|
||||
onQueryChange={[Function]}
|
||||
/>
|
||||
<t
|
||||
customClassName=""
|
||||
onDragEnd={null}
|
||||
onDragStart={null}
|
||||
onSecondaryPaneSizeChange={[Function]}
|
||||
percentage={true}
|
||||
primaryIndex={0}
|
||||
primaryMinSize={30}
|
||||
secondaryInitialSize={60}
|
||||
secondaryMinSize={30}
|
||||
vertical={true}
|
||||
>
|
||||
<div
|
||||
className="control-pane"
|
||||
>
|
||||
<QueryInput
|
||||
currentSchema="druid"
|
||||
onQueryStringChange={[Function]}
|
||||
queryString="test"
|
||||
runeMode={false}
|
||||
/>
|
||||
<div
|
||||
className="query-control-bar"
|
||||
>
|
||||
<Memo(RunButton)
|
||||
loading={true}
|
||||
onEditContext={[Function]}
|
||||
onExplain={[Function]}
|
||||
onHistory={[Function]}
|
||||
onPrettier={[Function]}
|
||||
onQueryContextChange={[Function]}
|
||||
onRun={[Function]}
|
||||
queryContext={Object {}}
|
||||
runeMode={false}
|
||||
/>
|
||||
<Blueprint4.Tooltip2
|
||||
content="Automatically wrap the query with a limit to protect against queries with very large result sets."
|
||||
hoverCloseDelay={0}
|
||||
hoverOpenDelay={800}
|
||||
minimal={false}
|
||||
transitionDuration={100}
|
||||
>
|
||||
<Blueprint4.Switch
|
||||
checked={true}
|
||||
className="auto-limit"
|
||||
label="Auto limit"
|
||||
onChange={[Function]}
|
||||
/>
|
||||
</Blueprint4.Tooltip2>
|
||||
<Memo(LiveQueryModeButton)
|
||||
autoLiveQueryModeShouldRun={true}
|
||||
liveQueryMode="auto"
|
||||
minimal={true}
|
||||
onLiveQueryModeChange={[Function]}
|
||||
/>
|
||||
<Memo(QueryTimer) />
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className="output-pane"
|
||||
>
|
||||
<Memo(Loader)
|
||||
cancelText="Cancel query"
|
||||
onCancel={[Function]}
|
||||
/>
|
||||
</div>
|
||||
</t>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`QueryView matches snapshot with query 1`] = `
|
||||
<div
|
||||
className="query-view app-view"
|
||||
>
|
||||
<ColumnTree
|
||||
columnMetadataLoading={true}
|
||||
defaultSchema="druid"
|
||||
defaultWhere={"__time >= CURRENT_TIMESTAMP - INTERVAL '1' DAY"}
|
||||
getParsedQuery={[Function]}
|
||||
onQueryChange={[Function]}
|
||||
/>
|
||||
<t
|
||||
customClassName=""
|
||||
onDragEnd={null}
|
||||
onDragStart={null}
|
||||
onSecondaryPaneSizeChange={[Function]}
|
||||
percentage={true}
|
||||
primaryIndex={0}
|
||||
primaryMinSize={30}
|
||||
secondaryInitialSize={60}
|
||||
secondaryMinSize={30}
|
||||
vertical={true}
|
||||
>
|
||||
<div
|
||||
className="control-pane"
|
||||
>
|
||||
<QueryInput
|
||||
currentSchema="druid"
|
||||
onQueryStringChange={[Function]}
|
||||
queryString="SELECT +3"
|
||||
runeMode={false}
|
||||
/>
|
||||
<div
|
||||
className="query-control-bar"
|
||||
>
|
||||
<Memo(RunButton)
|
||||
loading={true}
|
||||
onEditContext={[Function]}
|
||||
onExplain={[Function]}
|
||||
onHistory={[Function]}
|
||||
onPrettier={[Function]}
|
||||
onQueryContextChange={[Function]}
|
||||
onRun={[Function]}
|
||||
queryContext={Object {}}
|
||||
runeMode={false}
|
||||
/>
|
||||
<Blueprint4.Tooltip2
|
||||
content="Automatically wrap the query with a limit to protect against queries with very large result sets."
|
||||
hoverCloseDelay={0}
|
||||
hoverOpenDelay={800}
|
||||
minimal={false}
|
||||
transitionDuration={100}
|
||||
>
|
||||
<Blueprint4.Switch
|
||||
checked={true}
|
||||
className="auto-limit"
|
||||
label="Auto limit"
|
||||
onChange={[Function]}
|
||||
/>
|
||||
</Blueprint4.Tooltip2>
|
||||
<Memo(LiveQueryModeButton)
|
||||
autoLiveQueryModeShouldRun={true}
|
||||
liveQueryMode="auto"
|
||||
minimal={true}
|
||||
onLiveQueryModeChange={[Function]}
|
||||
/>
|
||||
<Memo(QueryTimer) />
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className="output-pane"
|
||||
>
|
||||
<Memo(Loader)
|
||||
cancelText="Cancel query"
|
||||
onCancel={[Function]}
|
||||
/>
|
||||
</div>
|
||||
</t>
|
||||
</div>
|
||||
`;
|
|
@ -1,153 +0,0 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`LiveQueryModeButton matches snapshot auto 1`] = `
|
||||
<Blueprint4.Popover2
|
||||
boundary="clippingParents"
|
||||
captureDismiss={false}
|
||||
content={
|
||||
<Blueprint4.Menu>
|
||||
<Blueprint4.MenuItem
|
||||
active={false}
|
||||
className="auto auto-on"
|
||||
disabled={false}
|
||||
icon="tick"
|
||||
multiline={false}
|
||||
onClick={[Function]}
|
||||
popoverProps={Object {}}
|
||||
selected={false}
|
||||
shouldDismissPopover={true}
|
||||
text="Auto"
|
||||
/>
|
||||
<Blueprint4.MenuItem
|
||||
active={false}
|
||||
className="on"
|
||||
disabled={false}
|
||||
icon="blank"
|
||||
multiline={false}
|
||||
onClick={[Function]}
|
||||
popoverProps={Object {}}
|
||||
selected={false}
|
||||
shouldDismissPopover={true}
|
||||
text="On"
|
||||
/>
|
||||
<Blueprint4.MenuItem
|
||||
active={false}
|
||||
className="off"
|
||||
disabled={false}
|
||||
icon="blank"
|
||||
multiline={false}
|
||||
onClick={[Function]}
|
||||
popoverProps={Object {}}
|
||||
selected={false}
|
||||
shouldDismissPopover={true}
|
||||
text="Off"
|
||||
/>
|
||||
</Blueprint4.Menu>
|
||||
}
|
||||
defaultIsOpen={false}
|
||||
disabled={false}
|
||||
fill={false}
|
||||
hasBackdrop={false}
|
||||
hoverCloseDelay={300}
|
||||
hoverOpenDelay={150}
|
||||
inheritDarkTheme={true}
|
||||
interactionKind="click"
|
||||
minimal={false}
|
||||
openOnTargetFocus={true}
|
||||
portalClassName="live-query-mode-button-portal"
|
||||
position="bottom-left"
|
||||
positioningStrategy="absolute"
|
||||
shouldReturnFocusOnClose={false}
|
||||
targetTagName="span"
|
||||
transitionDuration={300}
|
||||
usePortal={true}
|
||||
>
|
||||
<Blueprint4.Button
|
||||
className="live-query-mode-button"
|
||||
>
|
||||
Live query:
|
||||
|
||||
<span
|
||||
className="auto auto-on"
|
||||
>
|
||||
Auto
|
||||
</span>
|
||||
</Blueprint4.Button>
|
||||
</Blueprint4.Popover2>
|
||||
`;
|
||||
|
||||
exports[`LiveQueryModeButton matches snapshot on 1`] = `
|
||||
<Blueprint4.Popover2
|
||||
boundary="clippingParents"
|
||||
captureDismiss={false}
|
||||
content={
|
||||
<Blueprint4.Menu>
|
||||
<Blueprint4.MenuItem
|
||||
active={false}
|
||||
className="auto auto-on"
|
||||
disabled={false}
|
||||
icon="blank"
|
||||
multiline={false}
|
||||
onClick={[Function]}
|
||||
popoverProps={Object {}}
|
||||
selected={false}
|
||||
shouldDismissPopover={true}
|
||||
text="Auto"
|
||||
/>
|
||||
<Blueprint4.MenuItem
|
||||
active={false}
|
||||
className="on"
|
||||
disabled={false}
|
||||
icon="tick"
|
||||
multiline={false}
|
||||
onClick={[Function]}
|
||||
popoverProps={Object {}}
|
||||
selected={false}
|
||||
shouldDismissPopover={true}
|
||||
text="On"
|
||||
/>
|
||||
<Blueprint4.MenuItem
|
||||
active={false}
|
||||
className="off"
|
||||
disabled={false}
|
||||
icon="blank"
|
||||
multiline={false}
|
||||
onClick={[Function]}
|
||||
popoverProps={Object {}}
|
||||
selected={false}
|
||||
shouldDismissPopover={true}
|
||||
text="Off"
|
||||
/>
|
||||
</Blueprint4.Menu>
|
||||
}
|
||||
defaultIsOpen={false}
|
||||
disabled={false}
|
||||
fill={false}
|
||||
hasBackdrop={false}
|
||||
hoverCloseDelay={300}
|
||||
hoverOpenDelay={150}
|
||||
inheritDarkTheme={true}
|
||||
interactionKind="click"
|
||||
minimal={false}
|
||||
openOnTargetFocus={true}
|
||||
portalClassName="live-query-mode-button-portal"
|
||||
position="bottom-left"
|
||||
positioningStrategy="absolute"
|
||||
shouldReturnFocusOnClose={false}
|
||||
targetTagName="span"
|
||||
transitionDuration={300}
|
||||
usePortal={true}
|
||||
>
|
||||
<Blueprint4.Button
|
||||
className="live-query-mode-button"
|
||||
>
|
||||
Live query:
|
||||
|
||||
<span
|
||||
className="on auto-on"
|
||||
>
|
||||
On
|
||||
</span>
|
||||
</Blueprint4.Button>
|
||||
</Blueprint4.Popover2>
|
||||
`;
|
|
@ -1,40 +0,0 @@
|
|||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
@import '../../../variables';
|
||||
@import '../../../blueprint-overrides/common/colors';
|
||||
|
||||
.live-query-mode-button {
|
||||
.auto.auto-on {
|
||||
color: $druid-brand;
|
||||
}
|
||||
|
||||
.on {
|
||||
color: $druid-brand;
|
||||
}
|
||||
}
|
||||
|
||||
.live-query-mode-button-portal {
|
||||
.auto.auto-on .#{$bp-ns}-text-overflow-ellipsis {
|
||||
color: $druid-brand;
|
||||
}
|
||||
|
||||
.on .#{$bp-ns}-text-overflow-ellipsis {
|
||||
color: $druid-brand;
|
||||
}
|
||||
}
|
|
@ -1,48 +0,0 @@
|
|||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { shallow } from 'enzyme';
|
||||
import React from 'react';
|
||||
|
||||
import { LiveQueryModeButton } from './live-query-mode-button';
|
||||
|
||||
describe('LiveQueryModeButton', () => {
|
||||
it('matches snapshot on', () => {
|
||||
const liveQueryModeSelector = shallow(
|
||||
<LiveQueryModeButton
|
||||
liveQueryMode="on"
|
||||
onLiveQueryModeChange={() => {}}
|
||||
autoLiveQueryModeShouldRun
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(liveQueryModeSelector).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('matches snapshot auto', () => {
|
||||
const liveQueryModeSelector = shallow(
|
||||
<LiveQueryModeButton
|
||||
liveQueryMode="auto"
|
||||
onLiveQueryModeChange={() => {}}
|
||||
autoLiveQueryModeShouldRun
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(liveQueryModeSelector).toMatchSnapshot();
|
||||
});
|
||||
});
|
|
@ -1,80 +0,0 @@
|
|||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { Button, Menu, MenuItem, PopoverPosition } from '@blueprintjs/core';
|
||||
import { Popover2 } from '@blueprintjs/popover2';
|
||||
import classNames from 'classnames';
|
||||
import React from 'react';
|
||||
|
||||
import { tickIcon } from '../../../utils';
|
||||
|
||||
import './live-query-mode-button.scss';
|
||||
|
||||
export type LiveQueryMode = 'auto' | 'on' | 'off';
|
||||
export const LIVE_QUERY_MODES: LiveQueryMode[] = ['auto', 'on', 'off'];
|
||||
export const LIVE_QUERY_MODE_TITLE: Record<LiveQueryMode, string> = {
|
||||
auto: 'Auto',
|
||||
on: 'On',
|
||||
off: 'Off',
|
||||
};
|
||||
|
||||
export interface LiveQueryModeButtonProps {
|
||||
liveQueryMode: LiveQueryMode;
|
||||
onLiveQueryModeChange(liveQueryMode: LiveQueryMode): void;
|
||||
autoLiveQueryModeShouldRun: boolean;
|
||||
minimal?: boolean;
|
||||
}
|
||||
|
||||
export const LiveQueryModeButton = React.memo(function LiveQueryModeButton(
|
||||
props: LiveQueryModeButtonProps,
|
||||
) {
|
||||
const { liveQueryMode, onLiveQueryModeChange, autoLiveQueryModeShouldRun, minimal } = props;
|
||||
|
||||
return (
|
||||
<Popover2
|
||||
portalClassName="live-query-mode-button-portal"
|
||||
minimal={minimal}
|
||||
position={PopoverPosition.BOTTOM_LEFT}
|
||||
content={
|
||||
<Menu>
|
||||
{LIVE_QUERY_MODES.map(m => (
|
||||
<MenuItem
|
||||
className={classNames(
|
||||
m,
|
||||
m === 'auto' ? (autoLiveQueryModeShouldRun ? 'auto-on' : 'auto-off') : undefined,
|
||||
)}
|
||||
key={m}
|
||||
icon={tickIcon(m === liveQueryMode)}
|
||||
text={LIVE_QUERY_MODE_TITLE[m]}
|
||||
onClick={() => onLiveQueryModeChange(m)}
|
||||
/>
|
||||
))}
|
||||
</Menu>
|
||||
}
|
||||
>
|
||||
<Button className="live-query-mode-button" minimal={minimal}>
|
||||
Live query:{' '}
|
||||
<span
|
||||
className={classNames(liveQueryMode, autoLiveQueryModeShouldRun ? 'auto-on' : 'auto-off')}
|
||||
>
|
||||
{LIVE_QUERY_MODE_TITLE[liveQueryMode]}
|
||||
</span>
|
||||
</Button>
|
||||
</Popover2>
|
||||
);
|
||||
});
|
|
@ -1,51 +0,0 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`QueryExtraInfo matches snapshot 1`] = `
|
||||
<div
|
||||
class="query-extra-info"
|
||||
>
|
||||
<div
|
||||
class="query-info"
|
||||
>
|
||||
<span
|
||||
aria-haspopup="true"
|
||||
class="bp4-popover2-target"
|
||||
>
|
||||
<span
|
||||
class=""
|
||||
tabindex="0"
|
||||
>
|
||||
0 results in 8.00s
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
<span
|
||||
aria-haspopup="true"
|
||||
class="download-button bp4-popover2-target"
|
||||
>
|
||||
<button
|
||||
class="bp4-button bp4-minimal"
|
||||
type="button"
|
||||
>
|
||||
<span
|
||||
aria-hidden="true"
|
||||
class="bp4-icon bp4-icon-download"
|
||||
icon="download"
|
||||
tabindex="0"
|
||||
>
|
||||
<svg
|
||||
data-icon="download"
|
||||
height="16"
|
||||
viewBox="0 0 16 16"
|
||||
width="16"
|
||||
>
|
||||
<path
|
||||
d="M7.99-.01c-4.42 0-8 3.58-8 8s3.58 8 8 8 8-3.58 8-8-3.58-8-8-8zM11.7 9.7l-3 3c-.18.18-.43.29-.71.29s-.53-.11-.71-.29l-3-3A1.003 1.003 0 015.7 8.28l1.29 1.29V3.99c0-.55.45-1 1-1s1 .45 1 1v5.59l1.29-1.29a1.003 1.003 0 011.71.71c0 .27-.11.52-.29.7z"
|
||||
fill-rule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
</span>
|
||||
</button>
|
||||
</span>
|
||||
</div>
|
||||
`;
|
|
@ -1,29 +0,0 @@
|
|||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
.query-extra-info {
|
||||
& > * {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.query-info {
|
||||
line-height: 30px;
|
||||
white-space: nowrap;
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
|
@ -1,40 +0,0 @@
|
|||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { render } from '@testing-library/react';
|
||||
import { QueryResult } from 'druid-query-toolkit';
|
||||
import React from 'react';
|
||||
|
||||
import { QueryExtraInfo } from './query-extra-info';
|
||||
|
||||
describe('QueryExtraInfo', () => {
|
||||
it('matches snapshot', () => {
|
||||
const queryExtraInfo = (
|
||||
<QueryExtraInfo
|
||||
queryResult={QueryResult.BLANK.attachQueryId(
|
||||
'e3ee781b-c0b6-4385-9d99-a8a1994bebac',
|
||||
).changeQueryDuration(8000)}
|
||||
onDownload={() => {}}
|
||||
onLoadMore={() => {}}
|
||||
/>
|
||||
);
|
||||
|
||||
const { container } = render(queryExtraInfo);
|
||||
expect(container.firstChild).toMatchSnapshot();
|
||||
});
|
||||
});
|
|
@ -1,108 +0,0 @@
|
|||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { Button, Intent, Menu, MenuDivider, MenuItem, Position } from '@blueprintjs/core';
|
||||
import { IconNames } from '@blueprintjs/icons';
|
||||
import { Popover2, Tooltip2 } from '@blueprintjs/popover2';
|
||||
import copy from 'copy-to-clipboard';
|
||||
import { QueryResult } from 'druid-query-toolkit';
|
||||
import React, { MouseEvent } from 'react';
|
||||
|
||||
import { AppToaster } from '../../../singletons';
|
||||
import { pluralIfNeeded } from '../../../utils';
|
||||
|
||||
import './query-extra-info.scss';
|
||||
|
||||
export interface QueryExtraInfoProps {
|
||||
queryResult: QueryResult;
|
||||
onDownload: (filename: string, format: string) => void;
|
||||
onLoadMore: () => void;
|
||||
}
|
||||
|
||||
export const QueryExtraInfo = React.memo(function QueryExtraInfo(props: QueryExtraInfoProps) {
|
||||
const { queryResult, onDownload, onLoadMore } = props;
|
||||
const wrapQueryLimit = queryResult.getSqlOuterLimit();
|
||||
const hasMoreResults = queryResult.getNumResults() === wrapQueryLimit;
|
||||
|
||||
function handleQueryInfoClick(e: MouseEvent<HTMLDivElement>) {
|
||||
if (e.altKey) {
|
||||
if (hasMoreResults) {
|
||||
onLoadMore();
|
||||
}
|
||||
} else {
|
||||
const id = queryResult.queryId || queryResult.sqlQueryId;
|
||||
if (!id) return;
|
||||
|
||||
copy(id, { format: 'text/plain' });
|
||||
AppToaster.show({
|
||||
message: 'Query ID copied to clipboard',
|
||||
intent: Intent.SUCCESS,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function handleDownload(format: string) {
|
||||
const id = queryResult.queryId || queryResult.sqlQueryId;
|
||||
if (!id) return;
|
||||
|
||||
onDownload(`query-${id}.${format}`, format);
|
||||
}
|
||||
|
||||
const downloadMenu = (
|
||||
<Menu className="download-format-menu">
|
||||
<MenuDivider title="Download as:" />
|
||||
<MenuItem text="CSV" onClick={() => handleDownload('csv')} />
|
||||
<MenuItem text="TSV" onClick={() => handleDownload('tsv')} />
|
||||
<MenuItem text="JSON (new line delimited)" onClick={() => handleDownload('json')} />
|
||||
</Menu>
|
||||
);
|
||||
|
||||
const resultCount = hasMoreResults
|
||||
? `${queryResult.getNumResults() - 1}+ results`
|
||||
: pluralIfNeeded(queryResult.getNumResults(), 'result');
|
||||
|
||||
let tooltipContent: JSX.Element | undefined;
|
||||
if (queryResult.queryId) {
|
||||
tooltipContent = (
|
||||
<>
|
||||
Query ID: <strong>{queryResult.queryId}</strong> (click to copy)
|
||||
</>
|
||||
);
|
||||
} else if (queryResult.sqlQueryId) {
|
||||
tooltipContent = (
|
||||
<>
|
||||
SQL query ID: <strong>{queryResult.sqlQueryId}</strong> (click to copy)
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="query-extra-info">
|
||||
{typeof queryResult.queryDuration !== 'undefined' && (
|
||||
<div className="query-info" onClick={handleQueryInfoClick}>
|
||||
<Tooltip2 content={tooltipContent} hoverOpenDelay={500} placement="top-start">
|
||||
{`${resultCount} in ${(queryResult.queryDuration / 1000).toFixed(2)}s`}
|
||||
</Tooltip2>
|
||||
</div>
|
||||
)}
|
||||
<Popover2 className="download-button" content={downloadMenu} position={Position.BOTTOM_RIGHT}>
|
||||
<Button icon={IconNames.DOWNLOAD} minimal />
|
||||
</Popover2>
|
||||
</div>
|
||||
);
|
||||
});
|
|
@ -1,98 +0,0 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`QueryHistoryDialog matches snapshot 1`] = `
|
||||
<div
|
||||
class="bp4-portal"
|
||||
>
|
||||
<div
|
||||
aria-live="polite"
|
||||
class="bp4-overlay bp4-overlay-open bp4-overlay-scroll-container"
|
||||
>
|
||||
<div
|
||||
class="bp4-overlay-start-focus-trap bp4-overlay-appear bp4-overlay-appear-active"
|
||||
tabindex="0"
|
||||
/>
|
||||
<div
|
||||
class="bp4-overlay-backdrop bp4-overlay-appear bp4-overlay-appear-active"
|
||||
/>
|
||||
<div
|
||||
class="bp4-dialog-container bp4-overlay-content bp4-overlay-appear bp4-overlay-appear-active"
|
||||
>
|
||||
<div
|
||||
aria-labelledby="title-bp-dialog-0"
|
||||
class="bp4-dialog query-history-dialog"
|
||||
role="dialog"
|
||||
>
|
||||
<div
|
||||
class="bp4-dialog-header"
|
||||
>
|
||||
<h4
|
||||
class="bp4-heading"
|
||||
id="title-bp-dialog-0"
|
||||
>
|
||||
Query history
|
||||
</h4>
|
||||
<button
|
||||
aria-label="Close"
|
||||
class="bp4-button bp4-minimal bp4-dialog-close-button"
|
||||
type="button"
|
||||
>
|
||||
<span
|
||||
aria-hidden="true"
|
||||
class="bp4-icon bp4-icon-small-cross"
|
||||
icon="small-cross"
|
||||
>
|
||||
<svg
|
||||
data-icon="small-cross"
|
||||
height="20"
|
||||
viewBox="0 0 20 20"
|
||||
width="20"
|
||||
>
|
||||
<path
|
||||
d="M11.41 10l3.29-3.29c.19-.18.3-.43.3-.71a1.003 1.003 0 00-1.71-.71L10 8.59l-3.29-3.3a1.003 1.003 0 00-1.42 1.42L8.59 10 5.3 13.29c-.19.18-.3.43-.3.71a1.003 1.003 0 001.71.71l3.29-3.3 3.29 3.29c.18.19.43.3.71.3a1.003 1.003 0 00.71-1.71L11.41 10z"
|
||||
fill-rule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
<div
|
||||
class="bp4-dialog-body"
|
||||
>
|
||||
<div
|
||||
class="center-message bp4-input"
|
||||
>
|
||||
<div
|
||||
class="center-message-inner"
|
||||
>
|
||||
The query history is empty.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="bp4-dialog-footer"
|
||||
>
|
||||
<div
|
||||
class="bp4-dialog-footer-actions"
|
||||
>
|
||||
<button
|
||||
class="bp4-button"
|
||||
type="button"
|
||||
>
|
||||
<span
|
||||
class="bp4-button-text"
|
||||
>
|
||||
Close
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="bp4-overlay-end-focus-trap bp4-overlay-appear bp4-overlay-appear-active"
|
||||
tabindex="0"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
|
@ -1,39 +0,0 @@
|
|||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
@import '../../../variables';
|
||||
|
||||
.query-history-dialog {
|
||||
&.#{$bp-ns}-dialog {
|
||||
width: 900px;
|
||||
}
|
||||
|
||||
.panel {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.text-area {
|
||||
width: 100%;
|
||||
height: 500px;
|
||||
resize: none;
|
||||
}
|
||||
|
||||
.center-message {
|
||||
height: 400px;
|
||||
}
|
||||
}
|
|
@ -1,32 +0,0 @@
|
|||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { render } from '@testing-library/react';
|
||||
import React from 'react';
|
||||
|
||||
import { QueryHistoryDialog } from './query-history-dialog';
|
||||
|
||||
describe('QueryHistoryDialog', () => {
|
||||
it('matches snapshot', () => {
|
||||
const queryPlanDialog = (
|
||||
<QueryHistoryDialog setQueryString={() => null} queryRecords={[]} onClose={() => {}} />
|
||||
);
|
||||
render(queryPlanDialog);
|
||||
expect(document.body.lastChild).toMatchSnapshot();
|
||||
});
|
||||
});
|
|
@ -1,88 +0,0 @@
|
|||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { Button, Classes, Dialog, Intent, Tab, Tabs, TextArea } from '@blueprintjs/core';
|
||||
import React, { useState } from 'react';
|
||||
|
||||
import { CenterMessage } from '../../../components';
|
||||
import { QueryRecord } from '../../../utils/query-history';
|
||||
|
||||
import './query-history-dialog.scss';
|
||||
|
||||
export interface QueryHistoryDialogProps {
|
||||
queryRecords: readonly QueryRecord[];
|
||||
setQueryString: (queryString: string, queryContext: Record<string, any>) => void;
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
export const QueryHistoryDialog = React.memo(function QueryHistoryDialog(
|
||||
props: QueryHistoryDialogProps,
|
||||
) {
|
||||
const [activeTab, setActiveTab] = useState(0);
|
||||
const { queryRecords, setQueryString, onClose } = props;
|
||||
|
||||
function handleSelect() {
|
||||
const queryRecord = queryRecords[activeTab];
|
||||
setQueryString(queryRecord.queryString, queryRecord.queryContext || {});
|
||||
onClose();
|
||||
}
|
||||
|
||||
function renderContent(): JSX.Element {
|
||||
if (!queryRecords.length) {
|
||||
return <CenterMessage>The query history is empty.</CenterMessage>;
|
||||
}
|
||||
|
||||
const versions = queryRecords.map((record, index) => (
|
||||
<Tab
|
||||
id={index}
|
||||
key={index}
|
||||
title={record.version}
|
||||
panel={<TextArea readOnly value={record.queryString} className="text-area" />}
|
||||
panelClassName="panel"
|
||||
/>
|
||||
));
|
||||
|
||||
return (
|
||||
<Tabs
|
||||
animate
|
||||
renderActiveTabPanelOnly
|
||||
vertical
|
||||
className="tab-area"
|
||||
selectedTabId={activeTab}
|
||||
onChange={(t: number) => setActiveTab(t)}
|
||||
>
|
||||
{versions}
|
||||
<Tabs.Expander />
|
||||
</Tabs>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Dialog className="query-history-dialog" isOpen onClose={onClose} title="Query history">
|
||||
<div className={Classes.DIALOG_BODY}>{renderContent()}</div>
|
||||
<div className={Classes.DIALOG_FOOTER}>
|
||||
<div className={Classes.DIALOG_FOOTER_ACTIONS}>
|
||||
<Button text="Close" onClick={onClose} />
|
||||
{Boolean(queryRecords.length) && (
|
||||
<Button text="Open" intent={Intent.PRIMARY} onClick={handleSelect} />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</Dialog>
|
||||
);
|
||||
});
|
|
@ -1,111 +0,0 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`QueryInput correctly formats helper HTML 1`] = `
|
||||
"
|
||||
<div class=\\"doc-name\\">COUNT</div>
|
||||
<div class=\\"doc-syntax\\">COUNT(*)</div>
|
||||
<div class=\\"doc-description\\">Counts the number of things</div>"
|
||||
`;
|
||||
|
||||
exports[`QueryInput matches snapshot 1`] = `
|
||||
<div
|
||||
class="query-input"
|
||||
>
|
||||
<div
|
||||
class="ace-container"
|
||||
>
|
||||
<div
|
||||
class=" ace_editor ace_hidpi ace-tm no-background placeholder-padding ace_focus"
|
||||
id="ace-editor"
|
||||
style="width: 100%; height: 200px; font-size: 13px;"
|
||||
>
|
||||
<textarea
|
||||
autocapitalize="off"
|
||||
autocorrect="off"
|
||||
class="ace_text-input"
|
||||
spellcheck="false"
|
||||
style="opacity: 0; font-size: 1px; position: fixed; top: 0px;"
|
||||
wrap="off"
|
||||
/>
|
||||
<div
|
||||
aria-hidden="true"
|
||||
class="ace_gutter"
|
||||
>
|
||||
<div
|
||||
class="ace_layer ace_gutter-layer ace_folding-enabled"
|
||||
style="height: 1000000px;"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
class="ace_scroller"
|
||||
style="line-height: 0px;"
|
||||
>
|
||||
<div
|
||||
class="ace_content"
|
||||
>
|
||||
<div
|
||||
class="ace_layer ace_print-margin-layer"
|
||||
>
|
||||
<div
|
||||
class="ace_print-margin"
|
||||
style="left: 10px; visibility: hidden;"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
class="ace_layer ace_marker-layer"
|
||||
/>
|
||||
<div
|
||||
class="ace_layer ace_text-layer"
|
||||
style="height: 1000000px; margin: 0px 10px;"
|
||||
/>
|
||||
<div
|
||||
class="ace_layer ace_marker-layer"
|
||||
/>
|
||||
<div
|
||||
class="ace_layer ace_cursor-layer"
|
||||
>
|
||||
<div
|
||||
class="ace_cursor"
|
||||
style="animation-duration: 1000ms;"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="ace_scrollbar ace_scrollbar-v"
|
||||
style="display: none; width: 20px;"
|
||||
>
|
||||
<div
|
||||
class="ace_scrollbar-inner"
|
||||
style="width: 20px;"
|
||||
>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="ace_scrollbar ace_scrollbar-h"
|
||||
style="display: none; height: 20px;"
|
||||
>
|
||||
<div
|
||||
class="ace_scrollbar-inner"
|
||||
style="height: 20px;"
|
||||
>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
style="height: auto; width: auto; top: 0px; left: 0px; visibility: hidden; position: absolute; white-space: pre; overflow: hidden;"
|
||||
>
|
||||
<div
|
||||
style="height: auto; width: auto; top: 0px; left: 0px; visibility: hidden; position: absolute; white-space: pre; overflow: visible;"
|
||||
/>
|
||||
<div
|
||||
style="height: auto; width: auto; top: 0px; left: 0px; visibility: hidden; position: absolute; white-space: pre; overflow: visible;"
|
||||
>
|
||||
XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
|
@ -1,27 +0,0 @@
|
|||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
.query-input {
|
||||
position: relative;
|
||||
|
||||
.ace-container {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
}
|
|
@ -1,43 +0,0 @@
|
|||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { render } from '@testing-library/react';
|
||||
import React from 'react';
|
||||
|
||||
import { QueryInput } from './query-input';
|
||||
|
||||
describe('QueryInput', () => {
|
||||
it('matches snapshot', () => {
|
||||
const sqlControl = (
|
||||
<QueryInput queryString="hello world" onQueryStringChange={() => {}} runeMode={false} />
|
||||
);
|
||||
|
||||
const { container } = render(sqlControl);
|
||||
expect(container.firstChild).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('correctly formats helper HTML', () => {
|
||||
expect(
|
||||
QueryInput.makeDocHtml({
|
||||
name: 'COUNT',
|
||||
syntax: 'COUNT(*)',
|
||||
description: 'Counts the number of things',
|
||||
}),
|
||||
).toMatchSnapshot();
|
||||
});
|
||||
});
|
|
@ -1,279 +0,0 @@
|
|||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { ResizeEntry } from '@blueprintjs/core';
|
||||
import { ResizeSensor2 } from '@blueprintjs/popover2';
|
||||
import type { Ace } from 'ace-builds';
|
||||
import ace from 'ace-builds';
|
||||
import { SqlRef, SqlTableRef } from 'druid-query-toolkit';
|
||||
import escape from 'lodash.escape';
|
||||
import React from 'react';
|
||||
import AceEditor from 'react-ace';
|
||||
|
||||
import {
|
||||
SQL_CONSTANTS,
|
||||
SQL_DYNAMICS,
|
||||
SQL_EXPRESSION_PARTS,
|
||||
SQL_KEYWORDS,
|
||||
} from '../../../../lib/keywords';
|
||||
import { SQL_DATA_TYPES, SQL_FUNCTIONS } from '../../../../lib/sql-docs';
|
||||
import { ColumnMetadata, RowColumn, uniq } from '../../../utils';
|
||||
|
||||
import './query-input.scss';
|
||||
|
||||
const langTools = ace.require('ace/ext/language_tools');
|
||||
|
||||
const COMPLETER = {
|
||||
insertMatch: (editor: any, data: Ace.Completion) => {
|
||||
editor.completer.insertMatch({ value: data.name });
|
||||
},
|
||||
};
|
||||
|
||||
interface ItemDescription {
|
||||
name: string;
|
||||
syntax: string;
|
||||
description: string;
|
||||
}
|
||||
|
||||
export interface QueryInputProps {
|
||||
queryString: string;
|
||||
onQueryStringChange: (newQueryString: string) => void;
|
||||
runeMode: boolean;
|
||||
columnMetadata?: readonly ColumnMetadata[];
|
||||
currentSchema?: string;
|
||||
currentTable?: string;
|
||||
}
|
||||
|
||||
export interface QueryInputState {
|
||||
// For reasons (https://github.com/securingsincity/react-ace/issues/415) react ace editor needs an explicit height
|
||||
// Since this component will grown and shrink dynamically we will measure its height and then set it.
|
||||
editorHeight: number;
|
||||
completions: any[];
|
||||
prevColumnMetadata?: readonly ColumnMetadata[];
|
||||
prevCurrentTable?: string;
|
||||
prevCurrentSchema?: string;
|
||||
}
|
||||
|
||||
export class QueryInput extends React.PureComponent<QueryInputProps, QueryInputState> {
|
||||
private aceEditor: Ace.Editor | undefined;
|
||||
|
||||
static replaceDefaultAutoCompleter(): void {
|
||||
if (!langTools) return;
|
||||
|
||||
const keywordList = ([] as any[]).concat(
|
||||
SQL_KEYWORDS.map(v => ({ name: v, value: v, score: 0, meta: 'keyword' })),
|
||||
SQL_EXPRESSION_PARTS.map(v => ({ name: v, value: v, score: 0, meta: 'keyword' })),
|
||||
SQL_CONSTANTS.map(v => ({ name: v, value: v, score: 0, meta: 'constant' })),
|
||||
SQL_DYNAMICS.map(v => ({ name: v, value: v, score: 0, meta: 'dynamic' })),
|
||||
Object.entries(SQL_DATA_TYPES).map(([name, [runtime, description]]) => ({
|
||||
name,
|
||||
value: name,
|
||||
score: 0,
|
||||
meta: 'type',
|
||||
syntax: `Druid runtime type: ${runtime}`,
|
||||
description,
|
||||
})),
|
||||
);
|
||||
|
||||
langTools.setCompleters([
|
||||
langTools.snippetCompleter,
|
||||
langTools.textCompleter,
|
||||
{
|
||||
getCompletions: (_editor: any, _session: any, _pos: any, _prefix: any, callback: any) => {
|
||||
return callback(null, keywordList);
|
||||
},
|
||||
getDocTooltip: (item: any) => {
|
||||
if (item.meta === 'type') {
|
||||
item.docHTML = QueryInput.makeDocHtml(item);
|
||||
}
|
||||
},
|
||||
},
|
||||
]);
|
||||
}
|
||||
|
||||
static addFunctionAutoCompleter(): void {
|
||||
if (!langTools) return;
|
||||
|
||||
const functionList: any[] = Object.entries(SQL_FUNCTIONS).flatMap(([name, versions]) => {
|
||||
return versions.map(([args, description]) => ({
|
||||
name: name,
|
||||
value: versions.length > 1 ? `${name}(${args})` : name,
|
||||
score: 1100, // Use a high score to appear over the 'local' suggestions that have a score of 1000
|
||||
meta: 'function',
|
||||
syntax: `${name}(${args})`,
|
||||
description,
|
||||
completer: COMPLETER,
|
||||
}));
|
||||
});
|
||||
|
||||
langTools.addCompleter({
|
||||
getCompletions: (_editor: any, _session: any, _pos: any, _prefix: any, callback: any) => {
|
||||
callback(null, functionList);
|
||||
},
|
||||
getDocTooltip: (item: any) => {
|
||||
if (item.meta === 'function') {
|
||||
item.docHTML = QueryInput.makeDocHtml(item);
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
static makeDocHtml(item: ItemDescription) {
|
||||
return `
|
||||
<div class="doc-name">${item.name}</div>
|
||||
<div class="doc-syntax">${escape(item.syntax)}</div>
|
||||
<div class="doc-description">${item.description}</div>`;
|
||||
}
|
||||
|
||||
static getDerivedStateFromProps(props: QueryInputProps, state: QueryInputState) {
|
||||
const { columnMetadata, currentSchema, currentTable } = props;
|
||||
|
||||
if (
|
||||
columnMetadata &&
|
||||
(columnMetadata !== state.prevColumnMetadata ||
|
||||
currentSchema !== state.prevCurrentSchema ||
|
||||
currentTable !== state.prevCurrentTable)
|
||||
) {
|
||||
const completions = ([] as any[]).concat(
|
||||
uniq(columnMetadata.map(d => d.TABLE_SCHEMA)).map(v => ({
|
||||
value: SqlTableRef.create(v).toString(),
|
||||
score: 10,
|
||||
meta: 'schema',
|
||||
})),
|
||||
uniq(
|
||||
columnMetadata
|
||||
.filter(d => (currentSchema ? d.TABLE_SCHEMA === currentSchema : true))
|
||||
.map(d => d.TABLE_NAME),
|
||||
).map(v => ({
|
||||
value: SqlTableRef.create(v).toString(),
|
||||
score: 49,
|
||||
meta: 'datasource',
|
||||
})),
|
||||
uniq(
|
||||
columnMetadata
|
||||
.filter(d =>
|
||||
currentTable && currentSchema
|
||||
? d.TABLE_NAME === currentTable && d.TABLE_SCHEMA === currentSchema
|
||||
: true,
|
||||
)
|
||||
.map(d => d.COLUMN_NAME),
|
||||
).map(v => ({
|
||||
value: SqlRef.column(v).toString(),
|
||||
score: 50,
|
||||
meta: 'column',
|
||||
})),
|
||||
);
|
||||
|
||||
return {
|
||||
completions,
|
||||
prevColumnMetadata: columnMetadata,
|
||||
prevCurrentSchema: currentSchema,
|
||||
prevCurrentTable: currentTable,
|
||||
};
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
constructor(props: QueryInputProps, context: any) {
|
||||
super(props, context);
|
||||
this.state = {
|
||||
editorHeight: 200,
|
||||
completions: [],
|
||||
};
|
||||
}
|
||||
|
||||
componentDidMount(): void {
|
||||
QueryInput.replaceDefaultAutoCompleter();
|
||||
QueryInput.addFunctionAutoCompleter();
|
||||
if (langTools) {
|
||||
langTools.addCompleter({
|
||||
getCompletions: (_editor: any, _session: any, _pos: any, _prefix: any, callback: any) => {
|
||||
callback(null, this.state.completions);
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private readonly handleAceContainerResize = (entries: ResizeEntry[]) => {
|
||||
if (entries.length !== 1) return;
|
||||
this.setState({ editorHeight: entries[0].contentRect.height });
|
||||
};
|
||||
|
||||
private readonly handleChange = (value: string) => {
|
||||
// This gets the event as a second arg
|
||||
const { onQueryStringChange } = this.props;
|
||||
onQueryStringChange(value);
|
||||
};
|
||||
|
||||
public goToPosition(rowColumn: RowColumn) {
|
||||
const { aceEditor } = this;
|
||||
if (!aceEditor) return;
|
||||
aceEditor.focus(); // Grab the focus
|
||||
aceEditor.getSelection().moveCursorTo(rowColumn.row, rowColumn.column);
|
||||
if (rowColumn.endRow && rowColumn.endColumn) {
|
||||
aceEditor
|
||||
.getSelection()
|
||||
.selectToPosition({ row: rowColumn.endRow, column: rowColumn.endColumn });
|
||||
}
|
||||
}
|
||||
|
||||
render(): JSX.Element {
|
||||
const { queryString, runeMode } = this.props;
|
||||
const { editorHeight } = this.state;
|
||||
|
||||
// Set the key in the AceEditor to force a rebind and prevent an error that happens otherwise
|
||||
return (
|
||||
<div className="query-input">
|
||||
<ResizeSensor2 onResize={this.handleAceContainerResize}>
|
||||
<div className="ace-container">
|
||||
<AceEditor
|
||||
mode={runeMode ? 'hjson' : 'dsql'}
|
||||
theme="solarized_dark"
|
||||
className="no-background placeholder-padding"
|
||||
name="ace-editor"
|
||||
onChange={this.handleChange}
|
||||
focus
|
||||
fontSize={13}
|
||||
width="100%"
|
||||
height={`${editorHeight}px`}
|
||||
showPrintMargin={false}
|
||||
value={queryString}
|
||||
editorProps={{
|
||||
$blockScrolling: Infinity,
|
||||
}}
|
||||
setOptions={{
|
||||
enableBasicAutocompletion: !runeMode,
|
||||
enableLiveAutocompletion: !runeMode,
|
||||
showLineNumbers: true,
|
||||
tabSize: 2,
|
||||
newLineMode: 'unix' as any, // newLineMode is incorrectly assumed to be boolean in the typings
|
||||
}}
|
||||
style={{}}
|
||||
placeholder="SELECT * FROM ..."
|
||||
onLoad={editor => {
|
||||
editor.renderer.setPadding(10);
|
||||
editor.renderer.setScrollMargin(10, 10, 0, 0);
|
||||
this.aceEditor = editor;
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</ResizeSensor2>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
File diff suppressed because it is too large
Load Diff
|
@ -1,13 +0,0 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`ColumnRenameInput matches snapshot 1`] = `
|
||||
<Blueprint4.InputGroup
|
||||
autoFocus={true}
|
||||
className="column-rename-input"
|
||||
onBlur={[Function]}
|
||||
onChange={[Function]}
|
||||
onKeyDown={[Function]}
|
||||
small={true}
|
||||
value="hello"
|
||||
/>
|
||||
`;
|
|
@ -1,30 +0,0 @@
|
|||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { shallow } from 'enzyme';
|
||||
import React from 'react';
|
||||
|
||||
import { ColumnRenameInput } from './column-rename-input';
|
||||
|
||||
describe('ColumnRenameInput', () => {
|
||||
it('matches snapshot', () => {
|
||||
const columnRenameInput = shallow(<ColumnRenameInput initialName="hello" onDone={() => {}} />);
|
||||
|
||||
expect(columnRenameInput).toMatchSnapshot();
|
||||
});
|
||||
});
|
|
@ -1,62 +0,0 @@
|
|||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { InputGroup } from '@blueprintjs/core';
|
||||
import React, { useState } from 'react';
|
||||
|
||||
export interface ColumnRenameInputProps {
|
||||
initialName: string;
|
||||
onDone: (newName?: string) => void;
|
||||
}
|
||||
|
||||
export const ColumnRenameInput = React.memo(function ColumnRenameInput(
|
||||
props: ColumnRenameInputProps,
|
||||
) {
|
||||
const { initialName, onDone } = props;
|
||||
const [newName, setNewName] = useState<string>(initialName);
|
||||
|
||||
function maybeDone() {
|
||||
if (newName && newName !== initialName) {
|
||||
onDone(newName);
|
||||
} else {
|
||||
onDone();
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<InputGroup
|
||||
className="column-rename-input"
|
||||
value={newName}
|
||||
onChange={(e: any) => setNewName(e.target.value)}
|
||||
onKeyDown={(e: React.KeyboardEvent<HTMLInputElement>) => {
|
||||
switch (e.key) {
|
||||
case 'Enter':
|
||||
maybeDone();
|
||||
break;
|
||||
|
||||
case 'Escape':
|
||||
onDone();
|
||||
break;
|
||||
}
|
||||
}}
|
||||
onBlur={maybeDone}
|
||||
small
|
||||
autoFocus
|
||||
/>
|
||||
);
|
||||
});
|
|
@ -1,76 +0,0 @@
|
|||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
@import '../../../variables';
|
||||
@import '../../../blueprint-overrides/common/colors';
|
||||
|
||||
.query-output {
|
||||
.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);
|
||||
}
|
||||
|
||||
&.renaming {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.rt-td {
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
|
||||
.clickable-cell {
|
||||
padding: $table-cell-v-padding $table-cell-h-padding;
|
||||
cursor: pointer;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.#{$bp-ns}-popover2-target {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.aggregate-column {
|
||||
background-color: rgba($druid-brand, 0.06);
|
||||
}
|
||||
}
|
|
@ -1,61 +0,0 @@
|
|||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { render } from '@testing-library/react';
|
||||
import { QueryResult, sane, SqlQuery } from 'druid-query-toolkit';
|
||||
import React from 'react';
|
||||
|
||||
import { QueryOutput } from './query-output';
|
||||
|
||||
describe('QueryOutput', () => {
|
||||
it('matches snapshot', () => {
|
||||
const parsedQuery = SqlQuery.parse(sane`
|
||||
SELECT
|
||||
"language",
|
||||
COUNT(*) AS "Count", COUNT(DISTINCT "language") AS "dist_language", COUNT(*) FILTER (WHERE "language"= 'xxx') AS "language_filtered_count"
|
||||
FROM "github"
|
||||
WHERE "__time" >= CURRENT_TIMESTAMP - INTERVAL '1' DAY AND "language" != 'TypeScript'
|
||||
GROUP BY 1
|
||||
HAVING "Count" != 37392
|
||||
ORDER BY "Count" DESC
|
||||
`);
|
||||
|
||||
const queryOutput = (
|
||||
<QueryOutput
|
||||
runeMode={false}
|
||||
queryResult={QueryResult.fromRawResult(
|
||||
[
|
||||
['language', 'Count', 'dist_language', 'language_filtered_count'],
|
||||
['', 6881, 1, 0],
|
||||
['JavaScript', 166, 1, 0],
|
||||
['Python', 62, 1, 0],
|
||||
['HTML', 46, 1, 0],
|
||||
[],
|
||||
],
|
||||
false,
|
||||
true,
|
||||
).attachQuery({}, parsedQuery)}
|
||||
onQueryAction={() => {}}
|
||||
onLoadMore={() => {}}
|
||||
/>
|
||||
);
|
||||
|
||||
const { container } = render(queryOutput);
|
||||
expect(container.firstChild).toMatchSnapshot();
|
||||
});
|
||||
});
|
|
@ -1,422 +0,0 @@
|
|||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { Icon, Menu, MenuItem } from '@blueprintjs/core';
|
||||
import { IconName, IconNames } from '@blueprintjs/icons';
|
||||
import { Popover2 } from '@blueprintjs/popover2';
|
||||
import classNames from 'classnames';
|
||||
import { QueryResult, SqlExpression, SqlLiteral, SqlRef, trimString } from 'druid-query-toolkit';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import ReactTable from 'react-table';
|
||||
|
||||
import { BracedText, Deferred, TableCell } from '../../../components';
|
||||
import { ShowValueDialog } from '../../../dialogs/show-value-dialog/show-value-dialog';
|
||||
import { SMALL_TABLE_PAGE_SIZE, SMALL_TABLE_PAGE_SIZE_OPTIONS } from '../../../react-table';
|
||||
import {
|
||||
changePage,
|
||||
columnToWidth,
|
||||
copyAndAlert,
|
||||
formatNumber,
|
||||
getNumericColumnBraces,
|
||||
Pagination,
|
||||
prettyPrintSql,
|
||||
QueryAction,
|
||||
stringifyValue,
|
||||
} from '../../../utils';
|
||||
import { BasicAction, basicActionsToMenu } from '../../../utils/basic-action';
|
||||
|
||||
import { ColumnRenameInput } from './column-rename-input/column-rename-input';
|
||||
|
||||
import './query-output.scss';
|
||||
|
||||
function isComparable(x: unknown): boolean {
|
||||
return x !== null && x !== '' && !isNaN(Number(x));
|
||||
}
|
||||
|
||||
export interface QueryOutputProps {
|
||||
queryResult: QueryResult;
|
||||
onQueryAction(action: QueryAction): void;
|
||||
onLoadMore: () => void;
|
||||
runeMode: boolean;
|
||||
}
|
||||
|
||||
export const QueryOutput = React.memo(function QueryOutput(props: QueryOutputProps) {
|
||||
const { queryResult, onQueryAction, onLoadMore, runeMode } = props;
|
||||
const parsedQuery = queryResult.sqlQuery;
|
||||
const [pagination, setPagination] = useState<Pagination>({
|
||||
page: 0,
|
||||
pageSize: SMALL_TABLE_PAGE_SIZE,
|
||||
});
|
||||
const [showValue, setShowValue] = useState<string>();
|
||||
const [renamingColumn, setRenamingColumn] = useState<number>(-1);
|
||||
|
||||
// Reset page to 0 if number of results changes
|
||||
useEffect(() => {
|
||||
setPagination(pagination => {
|
||||
return pagination.page ? changePage(pagination, 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 getHeaderMenu(header: string, headerIndex: number) {
|
||||
const ref = SqlRef.column(header);
|
||||
const prettyRef = prettyPrintSql(ref);
|
||||
|
||||
if (parsedQuery) {
|
||||
const orderByExpression = parsedQuery.isValidSelectIndex(headerIndex)
|
||||
? SqlLiteral.index(headerIndex)
|
||||
: SqlRef.column(header);
|
||||
const descOrderBy = orderByExpression.toOrderByExpression('DESC');
|
||||
const ascOrderBy = orderByExpression.toOrderByExpression('ASC');
|
||||
const orderBy = parsedQuery.getOrderByForSelectIndex(headerIndex);
|
||||
|
||||
const basicActions: BasicAction[] = [];
|
||||
if (orderBy) {
|
||||
const reverseOrderBy = orderBy.reverseDirection();
|
||||
const reverseOrderByDirection = reverseOrderBy.getEffectiveDirection();
|
||||
basicActions.push({
|
||||
icon: reverseOrderByDirection === 'ASC' ? IconNames.SORT_ASC : IconNames.SORT_DESC,
|
||||
title: `Order ${reverseOrderByDirection === 'ASC' ? 'ascending' : 'descending'}`,
|
||||
onAction: () => {
|
||||
onQueryAction(q => q.changeOrderByExpressions([reverseOrderBy]));
|
||||
},
|
||||
});
|
||||
} else {
|
||||
basicActions.push(
|
||||
{
|
||||
icon: IconNames.SORT_DESC,
|
||||
title: `Order descending`,
|
||||
onAction: () => {
|
||||
onQueryAction(q => q.changeOrderByExpressions([descOrderBy]));
|
||||
},
|
||||
},
|
||||
{
|
||||
icon: IconNames.SORT_ASC,
|
||||
title: `Order ascending`,
|
||||
onAction: () => {
|
||||
onQueryAction(q => q.changeOrderByExpressions([ascOrderBy]));
|
||||
},
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
if (parsedQuery.isRealOutputColumnAtSelectIndex(headerIndex)) {
|
||||
const whereExpression = parsedQuery.getWhereExpression();
|
||||
if (whereExpression && whereExpression.containsColumn(header)) {
|
||||
basicActions.push({
|
||||
icon: IconNames.FILTER_REMOVE,
|
||||
title: `Remove from WHERE clause`,
|
||||
onAction: () => {
|
||||
onQueryAction(q =>
|
||||
q.changeWhereExpression(whereExpression.removeColumnFromAnd(header)),
|
||||
);
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
const havingExpression = parsedQuery.getHavingExpression();
|
||||
if (havingExpression && havingExpression.containsColumn(header)) {
|
||||
basicActions.push({
|
||||
icon: IconNames.FILTER_REMOVE,
|
||||
title: `Remove from HAVING clause`,
|
||||
onAction: () => {
|
||||
onQueryAction(q =>
|
||||
q.changeHavingExpression(havingExpression.removeColumnFromAnd(header)),
|
||||
);
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (!parsedQuery.hasStarInSelect()) {
|
||||
basicActions.push(
|
||||
{
|
||||
icon: IconNames.EDIT,
|
||||
title: 'Rename column',
|
||||
onAction: () => {
|
||||
setRenamingColumn(headerIndex);
|
||||
},
|
||||
},
|
||||
{
|
||||
icon: IconNames.CROSS,
|
||||
title: 'Remove column',
|
||||
onAction: () => {
|
||||
onQueryAction(q => q.removeOutputColumn(header));
|
||||
},
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
return basicActionsToMenu(basicActions)!;
|
||||
} else {
|
||||
const orderByExpression = SqlRef.column(header);
|
||||
const descOrderBy = orderByExpression.toOrderByExpression('DESC');
|
||||
const ascOrderBy = orderByExpression.toOrderByExpression('ASC');
|
||||
const descOrderByPretty = prettyPrintSql(descOrderBy);
|
||||
const ascOrderByPretty = prettyPrintSql(descOrderBy);
|
||||
return (
|
||||
<Menu>
|
||||
<MenuItem
|
||||
icon={IconNames.CLIPBOARD}
|
||||
text={`Copy: ${prettyRef}`}
|
||||
onClick={() => {
|
||||
copyAndAlert(String(ref), `${prettyRef}' copied to clipboard`);
|
||||
}}
|
||||
/>
|
||||
{!runeMode && (
|
||||
<>
|
||||
<MenuItem
|
||||
icon={IconNames.CLIPBOARD}
|
||||
text={`Copy: ${descOrderByPretty}`}
|
||||
onClick={() =>
|
||||
copyAndAlert(descOrderBy.toString(), `'${descOrderByPretty}' copied to clipboard`)
|
||||
}
|
||||
/>
|
||||
<MenuItem
|
||||
icon={IconNames.CLIPBOARD}
|
||||
text={`Copy: ${ascOrderByPretty}`}
|
||||
onClick={() =>
|
||||
copyAndAlert(ascOrderBy.toString(), `'${ascOrderByPretty}' copied to clipboard`)
|
||||
}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</Menu>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function filterOnMenuItem(icon: IconName, clause: SqlExpression, having: boolean) {
|
||||
const { onQueryAction } = props;
|
||||
if (!parsedQuery) return;
|
||||
|
||||
return (
|
||||
<MenuItem
|
||||
icon={icon}
|
||||
text={`${having ? 'Having' : 'Filter on'}: ${prettyPrintSql(clause)}`}
|
||||
onClick={() => {
|
||||
const column = clause.getUsedColumns()[0];
|
||||
onQueryAction(
|
||||
having
|
||||
? q => q.removeFromHaving(column).addHaving(clause)
|
||||
: q => q.removeColumnFromWhere(column).addWhere(clause),
|
||||
);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function clipboardMenuItem(clause: SqlExpression) {
|
||||
const prettyLabel = prettyPrintSql(clause);
|
||||
return (
|
||||
<MenuItem
|
||||
icon={IconNames.CLIPBOARD}
|
||||
text={`Copy: ${prettyLabel}`}
|
||||
onClick={() => copyAndAlert(clause.toString(), `${prettyLabel} copied to clipboard`)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function getCellMenu(header: string, headerIndex: number, value: unknown) {
|
||||
const { runeMode } = props;
|
||||
|
||||
const val = SqlLiteral.maybe(value);
|
||||
const showFullValueMenuItem = (
|
||||
<MenuItem
|
||||
icon={IconNames.EYE_OPEN}
|
||||
text="Show full value"
|
||||
onClick={() => {
|
||||
setShowValue(stringifyValue(value));
|
||||
}}
|
||||
/>
|
||||
);
|
||||
|
||||
if (parsedQuery) {
|
||||
let ex: SqlExpression | undefined;
|
||||
let having = false;
|
||||
const selectValue = parsedQuery.getSelectExpressionForIndex(headerIndex);
|
||||
if (selectValue) {
|
||||
const outputName = selectValue.getOutputName();
|
||||
having = parsedQuery.isAggregateSelectIndex(headerIndex);
|
||||
if (having && outputName) {
|
||||
ex = SqlRef.column(outputName);
|
||||
} else {
|
||||
ex = selectValue.getUnderlyingExpression();
|
||||
}
|
||||
} else if (parsedQuery.hasStarInSelect()) {
|
||||
ex = SqlRef.column(header);
|
||||
}
|
||||
|
||||
return (
|
||||
<Menu>
|
||||
{ex && val && (
|
||||
<>
|
||||
{isComparable(value) && (
|
||||
<>
|
||||
{filterOnMenuItem(IconNames.FILTER, ex.greaterThanOrEqual(val), having)}
|
||||
{filterOnMenuItem(IconNames.FILTER, ex.lessThanOrEqual(val), having)}
|
||||
</>
|
||||
)}
|
||||
{filterOnMenuItem(IconNames.FILTER, ex.equal(val), having)}
|
||||
{filterOnMenuItem(IconNames.FILTER, ex.unequal(val), having)}
|
||||
</>
|
||||
)}
|
||||
{showFullValueMenuItem}
|
||||
</Menu>
|
||||
);
|
||||
} else {
|
||||
const ref = SqlRef.column(header);
|
||||
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`)}
|
||||
/>
|
||||
{!runeMode && val && (
|
||||
<>
|
||||
{clipboardMenuItem(ref.equal(val))}
|
||||
{clipboardMenuItem(ref.unequal(val))}
|
||||
</>
|
||||
)}
|
||||
{showFullValueMenuItem}
|
||||
</Menu>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function getHeaderClassName(header: string, i: number) {
|
||||
if (!parsedQuery) return;
|
||||
|
||||
const className = [];
|
||||
const orderBy = parsedQuery.getOrderByForOutputColumn(header);
|
||||
if (orderBy) {
|
||||
className.push(orderBy.getEffectiveDirection() === 'DESC' ? '-sort-desc' : '-sort-asc');
|
||||
}
|
||||
|
||||
if (parsedQuery.isAggregateOutputColumn(header)) {
|
||||
className.push('aggregate-header');
|
||||
}
|
||||
|
||||
if (i === renamingColumn) {
|
||||
className.push('renaming');
|
||||
}
|
||||
|
||||
return className.join(' ');
|
||||
}
|
||||
|
||||
function renameColumnTo(renameTo: string | undefined) {
|
||||
setRenamingColumn(-1);
|
||||
if (renameTo && parsedQuery) {
|
||||
if (parsedQuery.hasStarInSelect()) return;
|
||||
const selectExpression = parsedQuery.getSelectExpressionForIndex(renamingColumn);
|
||||
if (!selectExpression) return;
|
||||
onQueryAction(q => q.changeSelect(renamingColumn, selectExpression.as(renameTo)));
|
||||
}
|
||||
}
|
||||
|
||||
const outerLimit = queryResult.getSqlOuterLimit();
|
||||
const hasMoreResults = queryResult.rows.length === outerLimit;
|
||||
|
||||
function changePagination(pagination: Pagination) {
|
||||
if (
|
||||
hasMoreResults &&
|
||||
Math.floor(queryResult.rows.length / pagination.pageSize) === pagination.page // on the last page
|
||||
) {
|
||||
onLoadMore();
|
||||
}
|
||||
setPagination(pagination);
|
||||
}
|
||||
|
||||
const numericColumnBraces = getNumericColumnBraces(queryResult, pagination);
|
||||
return (
|
||||
<div className={classNames('query-output', { 'more-results': hasMoreResults })}>
|
||||
<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 => changePagination(changePage(pagination, page))}
|
||||
onPageSizeChange={(pageSize, page) => changePagination({ 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={queryResult.header.map((column, i) => {
|
||||
const h = column.name;
|
||||
|
||||
return {
|
||||
Header:
|
||||
i === renamingColumn && parsedQuery
|
||||
? () => <ColumnRenameInput initialName={h} onDone={renameColumnTo} />
|
||||
: () => {
|
||||
return (
|
||||
<Popover2 content={<Deferred content={() => getHeaderMenu(h, i)} />}>
|
||||
<div className="clickable-cell">
|
||||
{h}
|
||||
{hasFilterOnHeader(h, i) && <Icon icon={IconNames.FILTER} size={14} />}
|
||||
</div>
|
||||
</Popover2>
|
||||
);
|
||||
},
|
||||
headerClassName: getHeaderClassName(h, i),
|
||||
width: columnToWidth(column),
|
||||
accessor: String(i),
|
||||
Cell(row) {
|
||||
const value = row.value;
|
||||
return (
|
||||
<Popover2 content={<Deferred content={() => getCellMenu(h, i, value)} />}>
|
||||
{numericColumnBraces[i] ? (
|
||||
<BracedText
|
||||
className="table-padding"
|
||||
text={formatNumber(value)}
|
||||
braces={numericColumnBraces[i]}
|
||||
padFractionalPart
|
||||
unselectableThousandsSeparator
|
||||
/>
|
||||
) : (
|
||||
<TableCell value={value} unlimited />
|
||||
)}
|
||||
</Popover2>
|
||||
);
|
||||
},
|
||||
className:
|
||||
parsedQuery && parsedQuery.isAggregateOutputColumn(h)
|
||||
? 'aggregate-column'
|
||||
: undefined,
|
||||
};
|
||||
})}
|
||||
/>
|
||||
{showValue && <ShowValueDialog onClose={() => setShowValue(undefined)} str={showValue} />}
|
||||
</div>
|
||||
);
|
||||
});
|
|
@ -1,32 +0,0 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`QueryTimer matches snapshot 1`] = `
|
||||
<div
|
||||
class="query-timer"
|
||||
>
|
||||
1.85s
|
||||
<button
|
||||
class="bp4-button bp4-minimal"
|
||||
type="button"
|
||||
>
|
||||
<span
|
||||
aria-hidden="true"
|
||||
class="bp4-icon bp4-icon-stopwatch"
|
||||
icon="stopwatch"
|
||||
tabindex="0"
|
||||
>
|
||||
<svg
|
||||
data-icon="stopwatch"
|
||||
height="16"
|
||||
viewBox="0 0 16 16"
|
||||
width="16"
|
||||
>
|
||||
<path
|
||||
d="M9 2v1.083A6.002 6.002 0 018 15 6 6 0 017 3.083V2H6a1 1 0 110-2h4a1 1 0 010 2H9zM8 5a4 4 0 104 4H8V5z"
|
||||
fill-rule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
`;
|
|
@ -1,23 +0,0 @@
|
|||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
.query-timer {
|
||||
line-height: 30px;
|
||||
white-space: nowrap;
|
||||
pointer-events: none;
|
||||
}
|
|
@ -1,42 +0,0 @@
|
|||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { render } from '@testing-library/react';
|
||||
import React from 'react';
|
||||
|
||||
import { QueryTimer } from './query-timer';
|
||||
|
||||
describe('QueryTimer', () => {
|
||||
beforeEach(() => {
|
||||
let nowCalls = 0;
|
||||
jest.spyOn(Date, 'now').mockImplementation(() => {
|
||||
return 1619201218452 + 2000 * nowCalls++;
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.restoreAllMocks();
|
||||
});
|
||||
|
||||
it('matches snapshot', () => {
|
||||
const queryTimer = <QueryTimer />;
|
||||
|
||||
const { container } = render(queryTimer);
|
||||
expect(container.firstChild).toMatchSnapshot();
|
||||
});
|
||||
});
|
|
@ -1,48 +0,0 @@
|
|||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { Button } from '@blueprintjs/core';
|
||||
import { IconNames } from '@blueprintjs/icons';
|
||||
import React, { useState } from 'react';
|
||||
|
||||
import { useInterval } from '../../../hooks';
|
||||
import { formatDurationHybrid } from '../../../utils';
|
||||
|
||||
import './query-timer.scss';
|
||||
|
||||
// This is roughly the time in ms that it takes the component to mount and unmount, without this the timer appears to over count a little bit
|
||||
const FUDGE_OFFSET = 150;
|
||||
|
||||
export const QueryTimer = React.memo(function QueryTimer() {
|
||||
const [startTime] = useState(Date.now() + FUDGE_OFFSET);
|
||||
const [currentTime, setCurrentTime] = useState(Date.now());
|
||||
|
||||
useInterval(() => {
|
||||
setCurrentTime(Date.now());
|
||||
}, 25);
|
||||
|
||||
const elapsed = currentTime - startTime;
|
||||
if (elapsed <= 0) return null;
|
||||
|
||||
return (
|
||||
<div className="query-timer">
|
||||
{formatDurationHybrid(elapsed)}
|
||||
<Button icon={IconNames.STOPWATCH} minimal />
|
||||
</div>
|
||||
);
|
||||
});
|
|
@ -1,125 +0,0 @@
|
|||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
@import '../../variables';
|
||||
|
||||
$nav-width: 250px;
|
||||
$vertical-gap: 6px;
|
||||
|
||||
.query-view {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
|
||||
.column-tree {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
height: 100%;
|
||||
width: $nav-width;
|
||||
@include card-like;
|
||||
}
|
||||
|
||||
.splitter-layout {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
height: 100%;
|
||||
left: $nav-width + $thin-padding;
|
||||
right: 0;
|
||||
width: auto;
|
||||
|
||||
.control-pane {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
top: 0;
|
||||
bottom: $vertical-gap;
|
||||
|
||||
.query-input {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 30px + $vertical-gap;
|
||||
@include card-like;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.query-control-bar {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
width: 100%;
|
||||
|
||||
& > * {
|
||||
vertical-align: bottom;
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
.auto-limit {
|
||||
display: inline-block;
|
||||
margin-bottom: 6px;
|
||||
font-size: $pt-font-size;
|
||||
|
||||
.#{$bp-ns}-control-indicator {
|
||||
margin-top: -1px;
|
||||
}
|
||||
}
|
||||
|
||||
.query-extra-info,
|
||||
.query-timer {
|
||||
position: absolute;
|
||||
right: 0;
|
||||
top: 0;
|
||||
margin-right: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.splitter-layout-vertical > .layout-splitter {
|
||||
height: 3px;
|
||||
background-color: $gray1;
|
||||
}
|
||||
|
||||
.output-pane {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
top: $vertical-gap;
|
||||
bottom: 0;
|
||||
@include card-like;
|
||||
overflow: hidden;
|
||||
|
||||
> * {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.init-state {
|
||||
text-align: center;
|
||||
|
||||
p {
|
||||
position: relative;
|
||||
top: 38%;
|
||||
font-size: 15px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.hide-column-tree .splitter-layout {
|
||||
left: 0;
|
||||
}
|
||||
}
|
|
@ -1,34 +0,0 @@
|
|||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { shallow } from 'enzyme';
|
||||
import React from 'react';
|
||||
|
||||
import { QueryView } from './query-view';
|
||||
|
||||
describe('QueryView', () => {
|
||||
it('matches snapshot', () => {
|
||||
const sqlView = shallow(<QueryView initQuery="test" />);
|
||||
expect(sqlView).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('matches snapshot with query', () => {
|
||||
const sqlView = shallow(<QueryView initQuery="SELECT +3" />);
|
||||
expect(sqlView).toMatchSnapshot();
|
||||
});
|
||||
});
|
|
@ -1,617 +0,0 @@
|
|||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { Code, Intent, Switch } from '@blueprintjs/core';
|
||||
import { Tooltip2 } from '@blueprintjs/popover2';
|
||||
import classNames from 'classnames';
|
||||
import { QueryResult, QueryRunner, SqlExpression, SqlQuery } from 'druid-query-toolkit';
|
||||
import Hjson from 'hjson';
|
||||
import * as JSONBig from 'json-bigint-native';
|
||||
import memoizeOne from 'memoize-one';
|
||||
import React, { RefObject } from 'react';
|
||||
import SplitterLayout from 'react-splitter-layout';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
|
||||
import { Loader, QueryErrorPane } from '../../components';
|
||||
import { EditContextDialog } from '../../dialogs';
|
||||
import { QueryContext, QueryWithContext } from '../../druid-models';
|
||||
import { Api, AppToaster } from '../../singletons';
|
||||
import {
|
||||
ColumnMetadata,
|
||||
downloadQueryResults,
|
||||
DruidError,
|
||||
findEmptyLiteralPosition,
|
||||
localStorageGet,
|
||||
localStorageGetJson,
|
||||
LocalStorageKeys,
|
||||
localStorageSet,
|
||||
localStorageSetJson,
|
||||
QueryAction,
|
||||
queryDruidSql,
|
||||
QueryManager,
|
||||
QueryState,
|
||||
RowColumn,
|
||||
} from '../../utils';
|
||||
import { QueryRecord, QueryRecordUtil } from '../../utils/query-history';
|
||||
|
||||
import { ColumnTree } from './column-tree/column-tree';
|
||||
import { ExplainDialog, QueryContextEngine } from './explain-dialog/explain-dialog';
|
||||
import {
|
||||
LIVE_QUERY_MODES,
|
||||
LiveQueryMode,
|
||||
LiveQueryModeButton,
|
||||
} from './live-query-mode-button/live-query-mode-button';
|
||||
import { QueryExtraInfo } from './query-extra-info/query-extra-info';
|
||||
import { QueryHistoryDialog } from './query-history-dialog/query-history-dialog';
|
||||
import { QueryInput } from './query-input/query-input';
|
||||
import { QueryOutput } from './query-output/query-output';
|
||||
import { QueryTimer } from './query-timer/query-timer';
|
||||
import { RunButton } from './run-button/run-button';
|
||||
|
||||
import './query-view.scss';
|
||||
|
||||
const LAST_DAY = SqlExpression.parse(`__time >= CURRENT_TIMESTAMP - INTERVAL '1' DAY`);
|
||||
|
||||
const parser = memoizeOne((sql: string): SqlQuery | undefined => {
|
||||
try {
|
||||
return SqlQuery.parse(sql);
|
||||
} catch {
|
||||
return;
|
||||
}
|
||||
});
|
||||
|
||||
export interface QueryViewProps {
|
||||
initQuery: string | undefined;
|
||||
defaultQueryContext?: Record<string, any>;
|
||||
mandatoryQueryContext?: Record<string, any>;
|
||||
}
|
||||
|
||||
export interface QueryViewState {
|
||||
queryString: string;
|
||||
parsedQuery?: SqlQuery;
|
||||
queryContext: QueryContext;
|
||||
wrapQueryLimit: number | undefined;
|
||||
liveQueryMode: LiveQueryMode;
|
||||
|
||||
columnMetadataState: QueryState<readonly ColumnMetadata[]>;
|
||||
|
||||
queryResultState: QueryState<QueryResult, DruidError>;
|
||||
|
||||
explainDialogQuery?: QueryContextEngine;
|
||||
|
||||
defaultSchema?: string;
|
||||
defaultTable?: string;
|
||||
|
||||
editContextDialogOpen: boolean;
|
||||
historyDialogOpen: boolean;
|
||||
queryHistory: readonly QueryRecord[];
|
||||
}
|
||||
|
||||
export class QueryView extends React.PureComponent<QueryViewProps, QueryViewState> {
|
||||
static isEmptyQuery(query: string): boolean {
|
||||
return query.trim() === '';
|
||||
}
|
||||
|
||||
static isJsonLike(queryString: string): boolean {
|
||||
return queryString.trim().startsWith('{');
|
||||
}
|
||||
|
||||
static isSql(query: any): boolean {
|
||||
if (typeof query === 'string') return true;
|
||||
return typeof query.query === 'string';
|
||||
}
|
||||
|
||||
static validRune(queryString: string): boolean {
|
||||
try {
|
||||
Hjson.parse(queryString);
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private readonly metadataQueryManager: QueryManager<null, ColumnMetadata[]>;
|
||||
private readonly queryManager: QueryManager<QueryWithContext, QueryResult>;
|
||||
|
||||
private readonly queryInputRef: RefObject<QueryInput>;
|
||||
|
||||
constructor(props: QueryViewProps, context: any) {
|
||||
super(props, context);
|
||||
const { mandatoryQueryContext } = props;
|
||||
|
||||
this.queryInputRef = React.createRef();
|
||||
|
||||
const queryString = props.initQuery || localStorageGet(LocalStorageKeys.QUERY_KEY) || '';
|
||||
const parsedQuery = queryString ? parser(queryString) : undefined;
|
||||
|
||||
const queryContext =
|
||||
localStorageGetJson(LocalStorageKeys.QUERY_CONTEXT) || props.defaultQueryContext || {};
|
||||
|
||||
const possibleQueryHistory = localStorageGetJson(LocalStorageKeys.QUERY_HISTORY);
|
||||
const queryHistory = Array.isArray(possibleQueryHistory) ? possibleQueryHistory : [];
|
||||
|
||||
const possibleLiveQueryMode = localStorageGetJson(LocalStorageKeys.LIVE_QUERY_MODE);
|
||||
const liveQueryMode = LIVE_QUERY_MODES.includes(possibleLiveQueryMode)
|
||||
? possibleLiveQueryMode
|
||||
: 'auto';
|
||||
|
||||
this.state = {
|
||||
queryString,
|
||||
parsedQuery,
|
||||
queryContext,
|
||||
wrapQueryLimit: 100,
|
||||
liveQueryMode,
|
||||
|
||||
columnMetadataState: QueryState.INIT,
|
||||
|
||||
queryResultState: QueryState.INIT,
|
||||
|
||||
editContextDialogOpen: false,
|
||||
historyDialogOpen: false,
|
||||
queryHistory,
|
||||
};
|
||||
|
||||
this.metadataQueryManager = new QueryManager({
|
||||
processQuery: async () => {
|
||||
return await queryDruidSql<ColumnMetadata>({
|
||||
query: `SELECT TABLE_SCHEMA, TABLE_NAME, COLUMN_NAME, DATA_TYPE FROM INFORMATION_SCHEMA.COLUMNS`,
|
||||
});
|
||||
},
|
||||
onStateChange: columnMetadataState => {
|
||||
if (columnMetadataState.error) {
|
||||
AppToaster.show({
|
||||
message: 'Could not load SQL metadata',
|
||||
intent: Intent.DANGER,
|
||||
});
|
||||
}
|
||||
this.setState({
|
||||
columnMetadataState,
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
const queryRunner = new QueryRunner({
|
||||
inflateDateStrategy: 'none',
|
||||
});
|
||||
|
||||
this.queryManager = new QueryManager({
|
||||
processQuery: async (
|
||||
queryWithContext: QueryWithContext,
|
||||
cancelToken,
|
||||
): Promise<QueryResult> => {
|
||||
const { queryString, queryContext, wrapQueryLimit } = queryWithContext;
|
||||
const query = QueryView.isJsonLike(queryString) ? Hjson.parse(queryString) : queryString;
|
||||
const isSql = QueryView.isSql(query);
|
||||
const extraQueryContext = { ...queryContext, ...(mandatoryQueryContext || {}) };
|
||||
|
||||
if (isSql && typeof wrapQueryLimit !== 'undefined') {
|
||||
extraQueryContext.sqlOuterLimit = wrapQueryLimit + 1;
|
||||
}
|
||||
|
||||
const queryIdKey = isSql ? 'sqlQueryId' : 'queryId';
|
||||
// Look for an existing queryId in the JSON itself or in the extra context object.
|
||||
let cancelQueryId = query.context?.[queryIdKey] || extraQueryContext[queryIdKey];
|
||||
if (!cancelQueryId) {
|
||||
// If the queryId (sqlQueryId) is not explicitly set on the context generate one thus making it possible to cancel the query.
|
||||
cancelQueryId = extraQueryContext[queryIdKey] = uuidv4();
|
||||
}
|
||||
|
||||
void cancelToken.promise
|
||||
.then(() => {
|
||||
return Api.instance.delete(
|
||||
`/druid/v2${isSql ? '/sql' : ''}/${Api.encodePath(cancelQueryId)}`,
|
||||
);
|
||||
})
|
||||
.catch(() => {});
|
||||
|
||||
try {
|
||||
return await queryRunner.runQuery({
|
||||
query,
|
||||
extraQueryContext,
|
||||
cancelToken,
|
||||
});
|
||||
} catch (e) {
|
||||
throw new DruidError(e);
|
||||
}
|
||||
},
|
||||
onStateChange: queryResultState => {
|
||||
this.setState({
|
||||
queryResultState,
|
||||
});
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
componentDidMount(): void {
|
||||
const { initQuery } = this.props;
|
||||
const { liveQueryMode } = this.state;
|
||||
|
||||
this.metadataQueryManager.runQuery(null);
|
||||
|
||||
if (liveQueryMode !== 'off' && initQuery) {
|
||||
this.handleRun();
|
||||
}
|
||||
}
|
||||
|
||||
componentWillUnmount(): void {
|
||||
this.metadataQueryManager.terminate();
|
||||
this.queryManager.terminate();
|
||||
}
|
||||
|
||||
private prettyPrintJson(): void {
|
||||
this.setState(prevState => {
|
||||
let parsed: any;
|
||||
try {
|
||||
parsed = Hjson.parse(prevState.queryString);
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
return {
|
||||
queryString: JSONBig.stringify(parsed, undefined, 2),
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
private readonly handleDownload = (filename: string, format: string) => {
|
||||
const { queryResultState } = this.state;
|
||||
const queryResult = queryResultState.data;
|
||||
if (!queryResult) return;
|
||||
|
||||
downloadQueryResults(queryResult, filename, format);
|
||||
};
|
||||
|
||||
private readonly handleLoadMore = () => {
|
||||
this.setState(
|
||||
({ wrapQueryLimit }) => ({
|
||||
wrapQueryLimit: wrapQueryLimit ? wrapQueryLimit * 10 : undefined,
|
||||
}),
|
||||
this.handleRun,
|
||||
);
|
||||
};
|
||||
|
||||
private renderExplainDialog() {
|
||||
const { mandatoryQueryContext } = this.props;
|
||||
const { explainDialogQuery } = this.state;
|
||||
if (!explainDialogQuery) return;
|
||||
|
||||
return (
|
||||
<ExplainDialog
|
||||
queryWithContext={explainDialogQuery}
|
||||
mandatoryQueryContext={mandatoryQueryContext}
|
||||
onOpenQuery={this.handleQueryStringChange}
|
||||
onClose={() => this.setState({ explainDialogQuery: undefined })}
|
||||
openQueryLabel="Open query"
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
private renderHistoryDialog() {
|
||||
const { historyDialogOpen, queryHistory } = this.state;
|
||||
if (!historyDialogOpen) return;
|
||||
|
||||
return (
|
||||
<QueryHistoryDialog
|
||||
queryRecords={queryHistory}
|
||||
setQueryString={(queryString, queryContext) => {
|
||||
this.handleQueryContextChange(queryContext);
|
||||
this.handleQueryStringChange(queryString);
|
||||
}}
|
||||
onClose={() => this.setState({ historyDialogOpen: false })}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
private renderEditContextDialog() {
|
||||
const { editContextDialogOpen, queryContext } = this.state;
|
||||
if (!editContextDialogOpen) return;
|
||||
|
||||
return (
|
||||
<EditContextDialog
|
||||
onQueryContextChange={this.handleQueryContextChange}
|
||||
onClose={() => {
|
||||
this.setState({ editContextDialogOpen: false });
|
||||
}}
|
||||
queryContext={queryContext}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
private renderLiveQueryModeButton() {
|
||||
const { liveQueryMode, queryString } = this.state;
|
||||
if (QueryView.isJsonLike(queryString)) return;
|
||||
|
||||
return (
|
||||
<LiveQueryModeButton
|
||||
liveQueryMode={liveQueryMode}
|
||||
onLiveQueryModeChange={this.handleLiveQueryModeChange}
|
||||
autoLiveQueryModeShouldRun={this.autoLiveQueryModeShouldRun()}
|
||||
minimal
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
private renderWrapQueryLimitSelector() {
|
||||
const { wrapQueryLimit, queryString } = this.state;
|
||||
if (QueryView.isJsonLike(queryString)) return;
|
||||
|
||||
return (
|
||||
<Tooltip2
|
||||
content="Automatically wrap the query with a limit to protect against queries with very large result sets."
|
||||
hoverOpenDelay={800}
|
||||
>
|
||||
<Switch
|
||||
className="auto-limit"
|
||||
checked={Boolean(wrapQueryLimit)}
|
||||
label="Auto limit"
|
||||
onChange={() => this.handleWrapQueryLimitChange(wrapQueryLimit ? undefined : 100)}
|
||||
/>
|
||||
</Tooltip2>
|
||||
);
|
||||
}
|
||||
|
||||
private renderMainArea() {
|
||||
const { queryString, queryContext, queryResultState, columnMetadataState } = this.state;
|
||||
const emptyQuery = QueryView.isEmptyQuery(queryString);
|
||||
const queryResult = queryResultState.data;
|
||||
|
||||
let currentSchema: string | undefined;
|
||||
let currentTable: string | undefined;
|
||||
|
||||
if (queryResult && queryResult.sqlQuery) {
|
||||
currentSchema = queryResult.sqlQuery.getFirstSchema();
|
||||
currentTable = queryResult.sqlQuery.getFirstTableName();
|
||||
} else if (localStorageGet(LocalStorageKeys.QUERY_KEY)) {
|
||||
const defaultQueryString = localStorageGet(LocalStorageKeys.QUERY_KEY);
|
||||
|
||||
const defaultQueryAst: SqlQuery | undefined = defaultQueryString
|
||||
? parser(defaultQueryString)
|
||||
: undefined;
|
||||
|
||||
if (defaultQueryAst) {
|
||||
currentSchema = defaultQueryAst.getFirstSchema();
|
||||
currentTable = defaultQueryAst.getFirstTableName();
|
||||
}
|
||||
}
|
||||
|
||||
const someQueryResult = queryResultState.getSomeData();
|
||||
const runeMode = QueryView.isJsonLike(queryString);
|
||||
return (
|
||||
<SplitterLayout
|
||||
vertical
|
||||
percentage
|
||||
secondaryInitialSize={Number(localStorageGet(LocalStorageKeys.QUERY_VIEW_PANE_SIZE)!) || 60}
|
||||
primaryMinSize={30}
|
||||
secondaryMinSize={30}
|
||||
onSecondaryPaneSizeChange={this.handleSecondaryPaneSizeChange}
|
||||
>
|
||||
<div className="control-pane">
|
||||
<QueryInput
|
||||
ref={this.queryInputRef}
|
||||
currentSchema={currentSchema ? currentSchema : 'druid'}
|
||||
currentTable={currentTable}
|
||||
queryString={queryString}
|
||||
onQueryStringChange={this.handleQueryStringChange}
|
||||
runeMode={runeMode}
|
||||
columnMetadata={columnMetadataState.data}
|
||||
/>
|
||||
<div className="query-control-bar">
|
||||
<RunButton
|
||||
onEditContext={() => this.setState({ editContextDialogOpen: true })}
|
||||
runeMode={runeMode}
|
||||
queryContext={queryContext}
|
||||
onQueryContextChange={this.handleQueryContextChange}
|
||||
onRun={emptyQuery ? undefined : this.handleRun}
|
||||
onExplain={emptyQuery ? undefined : this.handleExplain}
|
||||
onHistory={() => this.setState({ historyDialogOpen: true })}
|
||||
onPrettier={() => this.prettyPrintJson()}
|
||||
loading={queryResultState.loading}
|
||||
/>
|
||||
{this.renderWrapQueryLimitSelector()}
|
||||
{this.renderLiveQueryModeButton()}
|
||||
{queryResult && (
|
||||
<QueryExtraInfo
|
||||
queryResult={queryResult}
|
||||
onDownload={this.handleDownload}
|
||||
onLoadMore={this.handleLoadMore}
|
||||
/>
|
||||
)}
|
||||
{queryResultState.loading && <QueryTimer />}
|
||||
</div>
|
||||
</div>
|
||||
<div className="output-pane">
|
||||
{someQueryResult && (
|
||||
<QueryOutput
|
||||
runeMode={runeMode}
|
||||
queryResult={someQueryResult}
|
||||
onQueryAction={this.handleQueryAction}
|
||||
onLoadMore={this.handleLoadMore}
|
||||
/>
|
||||
)}
|
||||
{queryResultState.error && (
|
||||
<QueryErrorPane
|
||||
error={queryResultState.error}
|
||||
moveCursorTo={position => {
|
||||
this.moveToPosition(position);
|
||||
}}
|
||||
queryString={queryString}
|
||||
onQueryStringChange={this.handleQueryStringChange}
|
||||
/>
|
||||
)}
|
||||
{queryResultState.loading && (
|
||||
<Loader
|
||||
cancelText="Cancel query"
|
||||
onCancel={() => {
|
||||
this.queryManager.cancelCurrent();
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{queryResultState.isInit() && (
|
||||
<div className="init-state">
|
||||
{emptyQuery ? (
|
||||
<p>
|
||||
Enter a query and click <Code>Run</Code>
|
||||
</p>
|
||||
) : (
|
||||
<p>
|
||||
Click <Code>Run</Code> to execute the query
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</SplitterLayout>
|
||||
);
|
||||
}
|
||||
|
||||
private moveToPosition(position: RowColumn) {
|
||||
const currentQueryInput = this.queryInputRef.current;
|
||||
if (!currentQueryInput) return;
|
||||
currentQueryInput.goToPosition(position);
|
||||
}
|
||||
|
||||
private readonly handleQueryChange = (query: SqlQuery, preferablyRun?: boolean): void => {
|
||||
this.handleQueryStringChange(query.toString(), preferablyRun);
|
||||
|
||||
// Possibly move the cursor of the QueryInput to the empty literal position
|
||||
const emptyLiteralPosition = findEmptyLiteralPosition(query);
|
||||
if (emptyLiteralPosition) {
|
||||
// Introduce a delay to let the new text appear
|
||||
setTimeout(() => {
|
||||
this.moveToPosition(emptyLiteralPosition);
|
||||
}, 10);
|
||||
}
|
||||
};
|
||||
|
||||
private readonly handleQueryAction = (queryAction: QueryAction): void => {
|
||||
const { parsedQuery } = this.state;
|
||||
if (!parsedQuery) return;
|
||||
this.handleQueryChange(parsedQuery.apply(queryAction), true);
|
||||
};
|
||||
|
||||
private readonly handleQueryStringChange = (
|
||||
queryString: string,
|
||||
preferablyRun?: boolean,
|
||||
): void => {
|
||||
const parsedQuery = parser(queryString);
|
||||
this.setState({ queryString, parsedQuery }, preferablyRun ? this.handleRunIfLive : undefined);
|
||||
};
|
||||
|
||||
private readonly handleQueryContextChange = (queryContext: QueryContext) => {
|
||||
this.setState({ queryContext });
|
||||
};
|
||||
|
||||
private readonly handleLiveQueryModeChange = (liveQueryMode: LiveQueryMode) => {
|
||||
this.setState({ liveQueryMode });
|
||||
localStorageSetJson(LocalStorageKeys.LIVE_QUERY_MODE, liveQueryMode);
|
||||
};
|
||||
|
||||
private readonly handleWrapQueryLimitChange = (wrapQueryLimit: number | undefined) => {
|
||||
this.setState({ wrapQueryLimit });
|
||||
};
|
||||
|
||||
private readonly handleRun = () => {
|
||||
const { queryString, queryContext, wrapQueryLimit, queryHistory } = this.state;
|
||||
if (QueryView.isJsonLike(queryString) && !QueryView.validRune(queryString)) return;
|
||||
|
||||
const newQueryHistory = QueryRecordUtil.addQueryToHistory(
|
||||
queryHistory,
|
||||
queryString,
|
||||
queryContext,
|
||||
);
|
||||
|
||||
localStorageSetJson(LocalStorageKeys.QUERY_HISTORY, newQueryHistory);
|
||||
localStorageSet(LocalStorageKeys.QUERY_KEY, queryString);
|
||||
localStorageSetJson(LocalStorageKeys.QUERY_CONTEXT, queryContext);
|
||||
|
||||
this.setState({ queryHistory: newQueryHistory });
|
||||
this.queryManager.runQuery({ queryString, queryContext, wrapQueryLimit });
|
||||
};
|
||||
|
||||
private autoLiveQueryModeShouldRun() {
|
||||
const { queryResultState } = this.state;
|
||||
return (
|
||||
!queryResultState.data ||
|
||||
!queryResultState.data.queryDuration ||
|
||||
queryResultState.data.queryDuration < 10000
|
||||
);
|
||||
}
|
||||
|
||||
private readonly handleRunIfLive = () => {
|
||||
const { liveQueryMode } = this.state;
|
||||
if (liveQueryMode === 'off') return;
|
||||
if (liveQueryMode === 'auto' && !this.autoLiveQueryModeShouldRun()) return;
|
||||
this.handleRun();
|
||||
};
|
||||
|
||||
private readonly handleExplain = () => {
|
||||
const { queryString, queryContext, wrapQueryLimit } = this.state;
|
||||
|
||||
this.setState({
|
||||
explainDialogQuery: {
|
||||
engine: 'sql-native',
|
||||
queryString,
|
||||
queryContext,
|
||||
wrapQueryLimit,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
private readonly handleSecondaryPaneSizeChange = (secondaryPaneSize: number) => {
|
||||
localStorageSet(LocalStorageKeys.QUERY_VIEW_PANE_SIZE, String(secondaryPaneSize));
|
||||
};
|
||||
|
||||
private readonly getParsedQuery = () => {
|
||||
const { parsedQuery } = this.state;
|
||||
return parsedQuery;
|
||||
};
|
||||
|
||||
render(): JSX.Element {
|
||||
const { columnMetadataState, parsedQuery } = this.state;
|
||||
|
||||
let defaultSchema;
|
||||
let defaultTable;
|
||||
if (parsedQuery instanceof SqlQuery) {
|
||||
defaultSchema = parsedQuery.getFirstSchema();
|
||||
defaultTable = parsedQuery.getFirstTableName();
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className={classNames('query-view app-view', {
|
||||
'hide-column-tree': columnMetadataState.isError(),
|
||||
})}
|
||||
>
|
||||
{!columnMetadataState.isError() && (
|
||||
<ColumnTree
|
||||
getParsedQuery={this.getParsedQuery}
|
||||
columnMetadataLoading={columnMetadataState.loading}
|
||||
columnMetadata={columnMetadataState.data}
|
||||
defaultWhere={LAST_DAY}
|
||||
onQueryChange={this.handleQueryChange}
|
||||
defaultSchema={defaultSchema ? defaultSchema : 'druid'}
|
||||
defaultTable={defaultTable}
|
||||
/>
|
||||
)}
|
||||
{this.renderMainArea()}
|
||||
{this.renderExplainDialog()}
|
||||
{this.renderHistoryDialog()}
|
||||
{this.renderEditContextDialog()}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
|
@ -1,129 +0,0 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`RunButton matches snapshot loading state 1`] = `
|
||||
<div
|
||||
class="bp4-button-group run-button"
|
||||
>
|
||||
<button
|
||||
class="bp4-button bp4-disabled bp4-intent-primary"
|
||||
disabled=""
|
||||
tabindex="-1"
|
||||
type="button"
|
||||
>
|
||||
<span
|
||||
aria-hidden="true"
|
||||
class="bp4-icon bp4-icon-caret-right"
|
||||
icon="caret-right"
|
||||
tabindex="-1"
|
||||
>
|
||||
<svg
|
||||
data-icon="caret-right"
|
||||
height="16"
|
||||
viewBox="0 0 16 16"
|
||||
width="16"
|
||||
>
|
||||
<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>
|
||||
<span
|
||||
class="bp4-button-text"
|
||||
>
|
||||
Run
|
||||
</span>
|
||||
</button>
|
||||
<span
|
||||
aria-haspopup="true"
|
||||
class="bp4-popover2-target"
|
||||
>
|
||||
<button
|
||||
class="bp4-button bp4-intent-primary"
|
||||
type="button"
|
||||
>
|
||||
<span
|
||||
aria-hidden="true"
|
||||
class="bp4-icon bp4-icon-more"
|
||||
icon="more"
|
||||
tabindex="0"
|
||||
>
|
||||
<svg
|
||||
data-icon="more"
|
||||
height="16"
|
||||
viewBox="0 0 16 16"
|
||||
width="16"
|
||||
>
|
||||
<path
|
||||
d="M2 6.03a2 2 0 100 4 2 2 0 100-4zM14 6.03a2 2 0 100 4 2 2 0 100-4zM8 6.03a2 2 0 100 4 2 2 0 100-4z"
|
||||
fill-rule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
</span>
|
||||
</button>
|
||||
</span>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`RunButton matches snapshot non-loading state 1`] = `
|
||||
<div
|
||||
class="bp4-button-group run-button"
|
||||
>
|
||||
<button
|
||||
class="bp4-button bp4-intent-primary"
|
||||
type="button"
|
||||
>
|
||||
<span
|
||||
aria-hidden="true"
|
||||
class="bp4-icon bp4-icon-caret-right"
|
||||
icon="caret-right"
|
||||
tabindex="-1"
|
||||
>
|
||||
<svg
|
||||
data-icon="caret-right"
|
||||
height="16"
|
||||
viewBox="0 0 16 16"
|
||||
width="16"
|
||||
>
|
||||
<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>
|
||||
<span
|
||||
class="bp4-button-text"
|
||||
>
|
||||
Run
|
||||
</span>
|
||||
</button>
|
||||
<span
|
||||
aria-haspopup="true"
|
||||
class="bp4-popover2-target"
|
||||
>
|
||||
<button
|
||||
class="bp4-button bp4-intent-primary"
|
||||
type="button"
|
||||
>
|
||||
<span
|
||||
aria-hidden="true"
|
||||
class="bp4-icon bp4-icon-more"
|
||||
icon="more"
|
||||
tabindex="0"
|
||||
>
|
||||
<svg
|
||||
data-icon="more"
|
||||
height="16"
|
||||
viewBox="0 0 16 16"
|
||||
width="16"
|
||||
>
|
||||
<path
|
||||
d="M2 6.03a2 2 0 100 4 2 2 0 100-4zM14 6.03a2 2 0 100 4 2 2 0 100-4zM8 6.03a2 2 0 100 4 2 2 0 100-4z"
|
||||
fill-rule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
</span>
|
||||
</button>
|
||||
</span>
|
||||
</div>
|
||||
`;
|
|
@ -1,23 +0,0 @@
|
|||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
.run-button {
|
||||
.rune-button {
|
||||
background-color: #2ca89e !important;
|
||||
}
|
||||
}
|
|
@ -1,62 +0,0 @@
|
|||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { render } from '@testing-library/react';
|
||||
import React from 'react';
|
||||
|
||||
import { RunButton } from './run-button';
|
||||
|
||||
describe('RunButton', () => {
|
||||
it('matches snapshot non-loading state', () => {
|
||||
const runButton = (
|
||||
<RunButton
|
||||
loading={false}
|
||||
onHistory={() => {}}
|
||||
onEditContext={() => {}}
|
||||
runeMode={false}
|
||||
queryContext={{ f: 3 }}
|
||||
onQueryContextChange={() => {}}
|
||||
onRun={() => {}}
|
||||
onExplain={() => {}}
|
||||
onPrettier={() => {}}
|
||||
/>
|
||||
);
|
||||
|
||||
const { container } = render(runButton);
|
||||
expect(container.firstChild).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('matches snapshot loading state', () => {
|
||||
const runButton = (
|
||||
<RunButton
|
||||
loading
|
||||
onHistory={() => {}}
|
||||
onEditContext={() => {}}
|
||||
runeMode={false}
|
||||
queryContext={{ f: 3 }}
|
||||
onQueryContextChange={() => {}}
|
||||
onRun={() => {}}
|
||||
onExplain={() => {}}
|
||||
onPrettier={() => {}}
|
||||
/>
|
||||
);
|
||||
|
||||
const { container } = render(runButton);
|
||||
expect(container.firstChild).toMatchSnapshot();
|
||||
});
|
||||
});
|
|
@ -1,196 +0,0 @@
|
|||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import {
|
||||
Button,
|
||||
ButtonGroup,
|
||||
Intent,
|
||||
Menu,
|
||||
MenuDivider,
|
||||
MenuItem,
|
||||
Position,
|
||||
useHotkeys,
|
||||
} from '@blueprintjs/core';
|
||||
import { IconNames } from '@blueprintjs/icons';
|
||||
import { Popover2 } from '@blueprintjs/popover2';
|
||||
import React, { useCallback, useMemo } from 'react';
|
||||
|
||||
import { MenuCheckbox } from '../../../components';
|
||||
import {
|
||||
changeUseApproximateCountDistinct,
|
||||
changeUseApproximateTopN,
|
||||
changeUseCache,
|
||||
getUseApproximateCountDistinct,
|
||||
getUseApproximateTopN,
|
||||
getUseCache,
|
||||
QueryContext,
|
||||
} from '../../../druid-models';
|
||||
import { getLink } from '../../../links';
|
||||
import { pluralIfNeeded } from '../../../utils';
|
||||
|
||||
import './run-button.scss';
|
||||
|
||||
export interface RunButtonProps {
|
||||
runeMode: boolean;
|
||||
queryContext: QueryContext;
|
||||
onQueryContextChange: (newQueryContext: QueryContext) => void;
|
||||
onRun: (() => void) | undefined;
|
||||
loading: boolean;
|
||||
onExplain: (() => void) | undefined;
|
||||
onEditContext: () => void;
|
||||
onHistory: () => void;
|
||||
onPrettier: () => void;
|
||||
}
|
||||
|
||||
const RunButtonExtraMenu = (props: RunButtonProps) => {
|
||||
const {
|
||||
runeMode,
|
||||
onExplain,
|
||||
queryContext,
|
||||
onQueryContextChange,
|
||||
onEditContext,
|
||||
onHistory,
|
||||
onPrettier,
|
||||
} = props;
|
||||
|
||||
const useCache = getUseCache(queryContext);
|
||||
const useApproximateCountDistinct = getUseApproximateCountDistinct(queryContext);
|
||||
const useApproximateTopN = getUseApproximateTopN(queryContext);
|
||||
const numContextKeys = Object.keys(queryContext).length;
|
||||
|
||||
return (
|
||||
<Menu>
|
||||
<MenuItem
|
||||
icon={IconNames.HELP}
|
||||
text={runeMode ? 'Native query documentation' : 'DruidSQL documentation'}
|
||||
href={getLink(runeMode ? 'DOCS_RUNE' : 'DOCS_SQL')}
|
||||
target="_blank"
|
||||
/>
|
||||
<MenuItem icon={IconNames.HISTORY} text="Query history" onClick={onHistory} />
|
||||
{!runeMode && onExplain && (
|
||||
<MenuItem icon={IconNames.CLEAN} text="Explain SQL query" onClick={onExplain} />
|
||||
)}
|
||||
{runeMode && (
|
||||
<MenuItem icon={IconNames.ALIGN_LEFT} text="Prettify JSON" onClick={onPrettier} />
|
||||
)}
|
||||
<MenuItem
|
||||
icon={IconNames.PROPERTIES}
|
||||
text="Edit context"
|
||||
onClick={onEditContext}
|
||||
label={numContextKeys ? pluralIfNeeded(numContextKeys, 'key') : undefined}
|
||||
/>
|
||||
<MenuDivider />
|
||||
{!runeMode && (
|
||||
<>
|
||||
<MenuCheckbox
|
||||
checked={useApproximateCountDistinct}
|
||||
text="Use approximate COUNT(DISTINCT)"
|
||||
onChange={() => {
|
||||
onQueryContextChange(
|
||||
changeUseApproximateCountDistinct(queryContext, !useApproximateCountDistinct),
|
||||
);
|
||||
}}
|
||||
/>
|
||||
<MenuCheckbox
|
||||
checked={useApproximateTopN}
|
||||
text="Use approximate TopN"
|
||||
onChange={() => {
|
||||
onQueryContextChange(changeUseApproximateTopN(queryContext, !useApproximateTopN));
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
<MenuCheckbox
|
||||
checked={useCache}
|
||||
text="Use cache"
|
||||
onChange={() => {
|
||||
onQueryContextChange(changeUseCache(queryContext, !useCache));
|
||||
}}
|
||||
/>
|
||||
</Menu>
|
||||
);
|
||||
};
|
||||
|
||||
export const RunButton = React.memo(function RunButton(props: RunButtonProps) {
|
||||
const { runeMode, onRun, loading, onExplain } = props;
|
||||
|
||||
const handleRun = useCallback(() => {
|
||||
if (!onRun) return;
|
||||
onRun();
|
||||
}, [onRun]);
|
||||
|
||||
const hotkeys = useMemo(() => {
|
||||
return [
|
||||
{
|
||||
allowInInput: true,
|
||||
global: true,
|
||||
group: 'Query',
|
||||
combo: 'mod + enter',
|
||||
label: 'Runs the current query',
|
||||
onKeyDown: handleRun,
|
||||
},
|
||||
{
|
||||
allowInInput: true,
|
||||
global: true,
|
||||
group: 'Query',
|
||||
combo: 'mod + e',
|
||||
label: 'Explain the current query',
|
||||
onKeyDown: onExplain,
|
||||
},
|
||||
{
|
||||
allowInInput: true,
|
||||
global: true,
|
||||
group: 'X-Legacy', // This is prefixed with X so it appears in the bottom of the list
|
||||
combo: 'ctrl + enter',
|
||||
label: 'Runs the current query (old shortcut)',
|
||||
onKeyDown: handleRun,
|
||||
},
|
||||
];
|
||||
}, [handleRun, onExplain]);
|
||||
|
||||
useHotkeys(hotkeys);
|
||||
|
||||
return (
|
||||
<ButtonGroup className="run-button">
|
||||
{onRun ? (
|
||||
<Button
|
||||
className={runeMode ? 'rune-button' : undefined}
|
||||
disabled={loading}
|
||||
icon={IconNames.CARET_RIGHT}
|
||||
onClick={handleRun}
|
||||
text="Run"
|
||||
intent={Intent.PRIMARY}
|
||||
/>
|
||||
) : (
|
||||
<Button
|
||||
className={runeMode ? 'rune-button' : undefined}
|
||||
icon={IconNames.CARET_RIGHT}
|
||||
text="Run"
|
||||
disabled
|
||||
/>
|
||||
)}
|
||||
<Popover2 position={Position.BOTTOM_LEFT} content={<RunButtonExtraMenu {...props} />}>
|
||||
<Button
|
||||
className={runeMode ? 'rune-button' : undefined}
|
||||
icon={IconNames.MORE}
|
||||
intent={onRun ? Intent.PRIMARY : undefined}
|
||||
/>
|
||||
</Popover2>
|
||||
</ButtonGroup>
|
||||
);
|
||||
});
|
|
@ -89,7 +89,7 @@
|
|||
}
|
||||
|
||||
.filter-icon {
|
||||
margin-top: 1px;
|
||||
margin-top: 2px;
|
||||
margin-left: 3px;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -16,32 +16,23 @@
|
|||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { Icon, Menu, MenuItem } from '@blueprintjs/core';
|
||||
import { IconName, IconNames } from '@blueprintjs/icons';
|
||||
import { Icon } from '@blueprintjs/core';
|
||||
import { IconNames } from '@blueprintjs/icons';
|
||||
import { Popover2 } from '@blueprintjs/popover2';
|
||||
import classNames from 'classnames';
|
||||
import {
|
||||
Column,
|
||||
QueryResult,
|
||||
SqlAlias,
|
||||
SqlExpression,
|
||||
SqlLiteral,
|
||||
SqlQuery,
|
||||
SqlStar,
|
||||
} from 'druid-query-toolkit';
|
||||
import { Column, QueryResult, SqlAlias, SqlQuery, SqlStar } from 'druid-query-toolkit';
|
||||
import React, { useState } from 'react';
|
||||
import ReactTable from 'react-table';
|
||||
|
||||
import { BracedText, Deferred, TableCell } from '../../../../components';
|
||||
import { CellFilterMenu } from '../../../../components/cell-filter-menu/cell-filter-menu';
|
||||
import { ShowValueDialog } from '../../../../dialogs/show-value-dialog/show-value-dialog';
|
||||
import {
|
||||
columnToIcon,
|
||||
columnToWidth,
|
||||
filterMap,
|
||||
getNumericColumnBraces,
|
||||
prettyPrintSql,
|
||||
QueryAction,
|
||||
stringifyValue,
|
||||
} from '../../../../utils';
|
||||
|
||||
import './preview-table.scss';
|
||||
|
@ -50,10 +41,6 @@ function isDate(v: any): v is Date {
|
|||
return Boolean(v && typeof v.toISOString === 'function');
|
||||
}
|
||||
|
||||
function isComparable(x: unknown): boolean {
|
||||
return x !== null && x !== '' && !isNaN(Number(x));
|
||||
}
|
||||
|
||||
function getExpressionIfAlias(query: SqlQuery, selectIndex: number): string {
|
||||
const ex = query.getSelectExpressionForIndex(selectIndex);
|
||||
|
||||
|
@ -94,55 +81,16 @@ export const PreviewTable = React.memo(function PreviewTable(props: PreviewTable
|
|||
);
|
||||
}
|
||||
|
||||
function filterOnMenuItem(icon: IconName, clause: SqlExpression) {
|
||||
return (
|
||||
<MenuItem
|
||||
icon={icon}
|
||||
text={`Filter on: ${prettyPrintSql(clause)}`}
|
||||
onClick={() => {
|
||||
onQueryAction(q => q.addWhere(clause));
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function getCellMenu(column: Column, headerIndex: number, value: unknown) {
|
||||
const val = SqlLiteral.maybe(value);
|
||||
const showFullValueMenuItem = (
|
||||
<MenuItem
|
||||
icon={IconNames.EYE_OPEN}
|
||||
text="Show full value"
|
||||
onClick={() => {
|
||||
setShowValue(stringifyValue(value));
|
||||
}}
|
||||
/>
|
||||
);
|
||||
|
||||
let ex: SqlExpression | undefined;
|
||||
if (!parsedQuery.isAggregateSelectIndex(headerIndex)) {
|
||||
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>
|
||||
<CellFilterMenu
|
||||
column={column}
|
||||
value={value}
|
||||
headerIndex={headerIndex}
|
||||
query={parsedQuery}
|
||||
onQueryAction={onQueryAction}
|
||||
onShowFullValue={setShowValue}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
@ -62,7 +62,7 @@
|
|||
|
||||
.open-query {
|
||||
position: absolute;
|
||||
top: 30px;
|
||||
top: 6px;
|
||||
right: 6px;
|
||||
}
|
||||
}
|
|
@ -84,6 +84,10 @@
|
|||
margin-top: 3px;
|
||||
margin-right: 5px;
|
||||
}
|
||||
|
||||
.filter-icon {
|
||||
margin-top: 2px;
|
||||
}
|
||||
}
|
||||
|
||||
.formula {
|
||||
|
|
|
@ -17,7 +17,7 @@
|
|||
*/
|
||||
|
||||
import { Button, Icon, Intent, Menu, MenuItem } from '@blueprintjs/core';
|
||||
import { IconName, IconNames } from '@blueprintjs/icons';
|
||||
import { IconNames } from '@blueprintjs/icons';
|
||||
import { Popover2 } from '@blueprintjs/popover2';
|
||||
import classNames from 'classnames';
|
||||
import {
|
||||
|
@ -30,13 +30,13 @@ import {
|
|||
SqlQuery,
|
||||
SqlRef,
|
||||
SqlStar,
|
||||
trimString,
|
||||
} from 'druid-query-toolkit';
|
||||
import * as JSONBig from 'json-bigint-native';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import ReactTable from 'react-table';
|
||||
|
||||
import { BracedText, Deferred, TableCell } from '../../../components';
|
||||
import { CellFilterMenu } from '../../../components/cell-filter-menu/cell-filter-menu';
|
||||
import { ShowValueDialog } from '../../../dialogs/show-value-dialog/show-value-dialog';
|
||||
import {
|
||||
computeFlattenExprsForData,
|
||||
|
@ -56,7 +56,6 @@ import {
|
|||
Pagination,
|
||||
prettyPrintSql,
|
||||
QueryAction,
|
||||
stringifyValue,
|
||||
timeFormatToSql,
|
||||
} from '../../../utils';
|
||||
import { ExpressionEditorDialog } from '../../sql-data-loader-view/expression-editor-dialog/expression-editor-dialog';
|
||||
|
@ -64,17 +63,6 @@ import { TimeFloorMenuItem } from '../time-floor-menu-item/time-floor-menu-item'
|
|||
|
||||
import './result-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);
|
||||
}
|
||||
|
||||
const CAST_TARGETS: string[] = ['VARCHAR', 'BIGINT', 'DOUBLE'];
|
||||
|
||||
function jsonValue(ex: SqlExpression, path: string): SqlExpression {
|
||||
|
@ -85,10 +73,6 @@ function getJsonPaths(jsons: Record<string, any>[]): string[] {
|
|||
return ['$.'].concat(computeFlattenExprsForData(jsons, 'include-arrays', true));
|
||||
}
|
||||
|
||||
function isComparable(x: unknown): boolean {
|
||||
return x !== null && x !== '';
|
||||
}
|
||||
|
||||
function getExpressionIfAlias(query: SqlQuery, selectIndex: number): string {
|
||||
const ex = query.getSelectExpressionForIndex(selectIndex);
|
||||
|
||||
|
@ -542,106 +526,18 @@ export const ResultTablePane = React.memo(function ResultTablePane(props: Result
|
|||
return <Menu>{menuItems}</Menu>;
|
||||
}
|
||||
|
||||
function filterOnMenuItem(icon: IconName, clause: SqlExpression, having: boolean) {
|
||||
if (!parsedQuery) return;
|
||||
|
||||
return (
|
||||
<MenuItem
|
||||
icon={icon}
|
||||
text={`${having ? 'Having' : 'Filter on'}: ${prettyPrintSql(clause)}`}
|
||||
onClick={() => {
|
||||
const column = clause.getUsedColumns()[0];
|
||||
onQueryAction(
|
||||
having
|
||||
? q => q.removeFromHaving(column).addHaving(clause)
|
||||
: q => q.removeColumnFromWhere(column).addWhere(clause),
|
||||
);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function clipboardMenuItem(clause: SqlExpression) {
|
||||
const prettyLabel = prettyPrintSql(clause);
|
||||
return (
|
||||
<MenuItem
|
||||
icon={IconNames.CLIPBOARD}
|
||||
text={`Copy: ${prettyLabel}`}
|
||||
onClick={() => copyAndAlert(clause.toString(), `${prettyLabel} copied to clipboard`)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function getCellMenu(column: Column, headerIndex: number, value: unknown) {
|
||||
const showFullValueMenuItem = (
|
||||
<MenuItem
|
||||
icon={IconNames.EYE_OPEN}
|
||||
text="Show full value"
|
||||
onClick={() => {
|
||||
setShowValue(stringifyValue(value));
|
||||
}}
|
||||
return (
|
||||
<CellFilterMenu
|
||||
column={column}
|
||||
value={value}
|
||||
headerIndex={headerIndex}
|
||||
runeMode={runeMode}
|
||||
query={parsedQuery}
|
||||
onQueryAction={onQueryAction}
|
||||
onShowFullValue={setShowValue}
|
||||
/>
|
||||
);
|
||||
|
||||
const val = sqlLiteralForColumnValue(column, value);
|
||||
|
||||
if (parsedQuery) {
|
||||
let ex: SqlExpression | undefined;
|
||||
let having = false;
|
||||
if (parsedQuery.hasStarInSelect()) {
|
||||
ex = SqlRef.column(column.name);
|
||||
} else {
|
||||
const selectValue = parsedQuery.getSelectExpressionForIndex(headerIndex);
|
||||
if (selectValue) {
|
||||
const outputName = selectValue.getOutputName();
|
||||
having = parsedQuery.isAggregateSelectIndex(headerIndex);
|
||||
if (having && outputName) {
|
||||
ex = SqlRef.column(outputName);
|
||||
} else {
|
||||
ex = selectValue.getUnderlyingExpression();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const jsonColumn = column.nativeType === 'COMPLEX<json>';
|
||||
return (
|
||||
<Menu>
|
||||
{ex && val && !jsonColumn && (
|
||||
<>
|
||||
{filterOnMenuItem(IconNames.FILTER, ex.equal(val), having)}
|
||||
{filterOnMenuItem(IconNames.FILTER, ex.unequal(val), having)}
|
||||
{isComparable(value) && (
|
||||
<>
|
||||
{filterOnMenuItem(IconNames.FILTER, ex.greaterThanOrEqual(val), having)}
|
||||
{filterOnMenuItem(IconNames.FILTER, ex.lessThanOrEqual(val), having)}
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
{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`)}
|
||||
/>
|
||||
{!runeMode && val && (
|
||||
<>
|
||||
{clipboardMenuItem(ref.equal(val))}
|
||||
{clipboardMenuItem(ref.unequal(val))}
|
||||
</>
|
||||
)}
|
||||
{showFullValueMenuItem}
|
||||
</Menu>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function getHeaderClassName(header: string) {
|
||||
|
@ -724,7 +620,9 @@ export const ResultTablePane = React.memo(function ResultTablePane(props: Result
|
|||
<div className="output-name">
|
||||
{icon && <Icon className="type-icon" icon={icon} size={12} />}
|
||||
{h}
|
||||
{hasFilterOnHeader(h, i) && <Icon icon={IconNames.FILTER} size={14} />}
|
||||
{hasFilterOnHeader(h, i) && (
|
||||
<Icon className="filter-icon" icon={IconNames.FILTER} size={14} />
|
||||
)}
|
||||
</div>
|
||||
{parsedQuery && (
|
||||
<div className="formula">{getExpressionIfAlias(parsedQuery, i)}</div>
|
||||
|
|
|
@ -52,14 +52,14 @@ import {
|
|||
QueryManager,
|
||||
QueryState,
|
||||
} from '../../utils';
|
||||
import { ColumnTree } from '../query-view/column-tree/column-tree';
|
||||
import { ExplainDialog } from '../query-view/explain-dialog/explain-dialog';
|
||||
|
||||
import { ColumnTree } from './column-tree/column-tree';
|
||||
import { ConnectExternalDataDialog } from './connect-external-data-dialog/connect-external-data-dialog';
|
||||
import { getDemoQueries } from './demo-queries';
|
||||
import { ExecutionDetailsDialog } from './execution-details-dialog/execution-details-dialog';
|
||||
import { ExecutionDetailsTab } from './execution-details-pane/execution-details-pane';
|
||||
import { ExecutionSubmitDialog } from './execution-submit-dialog/execution-submit-dialog';
|
||||
import { ExplainDialog } from './explain-dialog/explain-dialog';
|
||||
import { MetadataChangeDetector } from './metadata-change-detector';
|
||||
import { QueryTab } from './query-tab/query-tab';
|
||||
import { RecentQueryTaskPanel } from './recent-query-task-panel/recent-query-task-panel';
|
||||
|
|
Loading…
Reference in New Issue