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:
Vadim Ogievetsky 2022-10-07 12:44:40 -07:00 committed by GitHub
parent 25c1d55dd6
commit 573e12c75f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
85 changed files with 588 additions and 5612 deletions

View File

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

View File

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

13
licenses/bin/d3-array.ISC Normal file
View File

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

13
licenses/bin/d3-axis.ISC Normal file
View File

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

13
licenses/bin/d3-color.ISC Normal file
View File

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

View File

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

13
licenses/bin/d3-scale.ISC Normal file
View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -20,7 +20,6 @@ exports[`HeaderBar matches snapshot 1`] = `
href="#workbench"
icon="application"
minimal={true}
onClick={[Function]}
text="Query"
/>
<Blueprint4.Popover2

View File

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

View File

@ -84,6 +84,10 @@
margin-top: 3px;
margin-right: 5px;
}
.filter-icon {
margin-top: 2px;
}
}
.formula {

View File

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

View File

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

View File

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

View File

@ -24,6 +24,12 @@
top: 5%;
}
.loader {
position: relative;
height: 60vh;
width: 100%;
}
.#{$bp-ns}-dialog-body {
max-height: 70vh;

View File

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

View File

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

View File

@ -25,6 +25,12 @@
width: 600px;
}
.loader {
position: relative;
height: 60vh;
width: 100%;
}
.#{$bp-ns}-dialog-body {
max-height: 70vh;

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -89,7 +89,7 @@
}
.filter-icon {
margin-top: 1px;
margin-top: 2px;
margin-left: 3px;
}
}

View File

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

View File

@ -62,7 +62,7 @@
.open-query {
position: absolute;
top: 30px;
top: 6px;
right: 6px;
}
}

View File

@ -84,6 +84,10 @@
margin-top: 3px;
margin-right: 5px;
}
.filter-icon {
margin-top: 2px;
}
}
.formula {

View File

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

View File

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