Revert "Web console: refactor and improve the segment timeline (#17508)" (#17520)

This reverts commit 09432c099b.
This commit is contained in:
Vadim Ogievetsky 2024-11-27 09:38:48 -08:00 committed by GitHub
parent 09432c099b
commit f3e1f1e586
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
200 changed files with 2416 additions and 5617 deletions

View File

@ -5125,6 +5125,15 @@ version: 5.2.5
---
name: "@druid-toolkit/query"
license_category: binary
module: web-console
license_name: Apache License version 2.0
copyright: Imply Data
version: 0.22.23
---
name: "@emotion/cache"
license_category: binary
module: web-console
@ -5215,16 +5224,6 @@ license_file_path: licenses/bin/@emotion-weak-memoize.MIT
---
name: "@flatten-js/interval-tree"
license_category: binary
module: web-console
license_name: MIT License
copyright: Alex Bol
version: 1.1.3
license_file_path: licenses/bin/@flatten-js-interval-tree.MIT
---
name: "@fontsource/open-sans"
license_category: binary
module: web-console
@ -5235,15 +5234,6 @@ license_file_path: licenses/bin/@fontsource-open-sans.OFL
---
name: "@internationalized/date"
license_category: binary
module: web-console
license_name: Apache License version 2.0
copyright: Adobe
version: 3.5.6
---
name: "@popperjs/core"
license_category: binary
module: web-console
@ -5254,15 +5244,6 @@ license_file_path: licenses/bin/@popperjs-core.MIT
---
name: "@swc/helpers"
license_category: binary
module: web-console
license_name: Apache License version 2.0
copyright: 강동윤
version: 0.5.13
---
name: "@types/parse-json"
license_category: binary
module: web-console
@ -5423,6 +5404,15 @@ license_file_path: licenses/bin/change-case.MIT
---
name: "chronoshift"
license_category: binary
module: web-console
license_name: Apache License version 2.0
copyright: Vadim Ogievetsky
version: 0.10.0
---
name: "classnames"
license_category: binary
module: web-console
@ -5712,15 +5702,6 @@ license_file_path: licenses/bin/dot-case.MIT
---
name: "druid-query-toolkit"
license_category: binary
module: web-console
license_name: Apache License version 2.0
copyright: Imply Data
version: 1.0.0
---
name: "echarts"
license_category: binary
module: web-console
@ -5820,6 +5801,16 @@ license_file_path: licenses/bin/has-flag.MIT
---
name: "has-own-prop"
license_category: binary
module: web-console
license_name: MIT License
copyright: Sindre Sorhus
version: 2.0.0
license_file_path: licenses/bin/has-own-prop.MIT
---
name: "hasown"
license_category: binary
module: web-console
@ -5880,6 +5871,15 @@ license_file_path: licenses/bin/iconv-lite.MIT
---
name: "immutable-class"
license_category: binary
module: web-console
license_name: Apache License version 2.0
copyright: Vadim Ogievetsky
version: 0.11.2
---
name: "import-fresh"
license_category: binary
module: web-console
@ -6060,6 +6060,26 @@ license_file_path: licenses/bin/mime-types.MIT
---
name: "moment-timezone"
license_category: binary
module: web-console
license_name: MIT License
copyright: Tim Wood
version: 0.5.43
license_file_path: licenses/bin/moment-timezone.MIT
---
name: "moment"
license_category: binary
module: web-console
license_name: MIT License
copyright: Iskren Ivov Chernev
version: 2.29.4
license_file_path: licenses/bin/moment.MIT
---
name: "no-case"
license_category: binary
module: web-console

View File

@ -16,7 +16,7 @@
* limitations under the License.
*/
import { T } from 'druid-query-toolkit';
import { T } from '@druid-toolkit/query';
import type * as playwright from 'playwright-chromium';
import { DatasourcesOverview } from './component/datasources/overview';

View File

@ -18,8 +18,6 @@
const { createJsWithTsPreset } = require('ts-jest');
process.env.TZ = 'UTC';
module.exports = {
testEnvironment: 'jsdom',
transformIgnorePatterns: ['/node_modules/(?!(d3-.+)/)'],

View File

@ -100,23 +100,15 @@ export const SQL_EXPRESSION_PARTS = [
'TRAILING',
'EPOCH',
'SECOND',
'SECONDS',
'MINUTE',
'MINUTES',
'HOUR',
'HOURS',
'DAY',
'DAYS',
'DOW',
'DOY',
'WEEK',
'WEEKS',
'MONTH',
'MONTHS',
'QUARTER',
'QUARTERS',
'YEAR',
'YEARS',
'TIMESTAMP',
'INTERVAL',
'CSV',

View File

@ -14,11 +14,11 @@
"@blueprintjs/datetime2": "^2.3.11",
"@blueprintjs/icons": "^5.13.0",
"@blueprintjs/select": "^5.2.5",
"@flatten-js/interval-tree": "^1.1.3",
"@druid-toolkit/query": "^0.22.23",
"@fontsource/open-sans": "^5.0.30",
"@internationalized/date": "^3.5.6",
"ace-builds": "~1.5.3",
"axios": "^1.7.7",
"chronoshift": "^0.10.0",
"classnames": "^2.2.6",
"copy-to-clipboard": "^3.3.3",
"d3-array": "^3.2.4",
@ -28,7 +28,6 @@
"d3-scale-chromatic": "^3.1.0",
"d3-selection": "^3.0.0",
"date-fns": "^2.28.0",
"druid-query-toolkit": "^1.0.0",
"echarts": "^5.5.1",
"file-saver": "^2.0.5",
"hjson": "^3.2.2",
@ -2086,6 +2085,15 @@
"node": ">=10.0.0"
}
},
"node_modules/@druid-toolkit/query": {
"version": "0.22.23",
"resolved": "https://registry.npmjs.org/@druid-toolkit/query/-/query-0.22.23.tgz",
"integrity": "sha512-yQOUAQJP63rzsTCdLcqNB8aRtsYPw8rYBfPSXc4zfAA4y/GJc9OJeHcLFRMdUtpwBtm0ueARMUlTSQcTsyV8gQ==",
"license": "Apache-2.0",
"dependencies": {
"tslib": "^2.5.2"
}
},
"node_modules/@dual-bundle/import-meta-resolve": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/@dual-bundle/import-meta-resolve/-/import-meta-resolve-4.1.0.tgz",
@ -2365,9 +2373,9 @@
}
},
"node_modules/@eslint/plugin-kit": {
"version": "0.2.3",
"resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.2.3.tgz",
"integrity": "sha512-2b/g5hRmpbb1o4GnTZax9N9m0FXzz9OV42ZzI4rDDMDuHUqigAiQCEWChBWCY4ztAGVRjoWT19v0yMmc5/L5kA==",
"version": "0.2.0",
"resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.2.0.tgz",
"integrity": "sha512-vH9PiIMMwvhCx31Af3HiGzsVNULDbyVkHXwlemn/B0TFj/00ho3y55efXrUZTfQipxoHC5u4xq6zblww1zm1Ig==",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
@ -2377,12 +2385,6 @@
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
}
},
"node_modules/@flatten-js/interval-tree": {
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/@flatten-js/interval-tree/-/interval-tree-1.1.3.tgz",
"integrity": "sha512-xhFWUBoHJFF77cJO1D6REjdgJEMRf2Y2Z+eKEPav8evGKcLSnj1ud5pLXQSbGuxF3VSvT1rWhMfVpXEKJLTL+A==",
"license": "MIT"
},
"node_modules/@fontsource/open-sans": {
"version": "5.1.0",
"resolved": "https://registry.npmjs.org/@fontsource/open-sans/-/open-sans-5.1.0.tgz",
@ -2440,15 +2442,6 @@
"url": "https://github.com/sponsors/nzakas"
}
},
"node_modules/@internationalized/date": {
"version": "3.5.6",
"resolved": "https://registry.npmjs.org/@internationalized/date/-/date-3.5.6.tgz",
"integrity": "sha512-jLxQjefH9VI5P9UQuqB6qNKnvFt1Ky1TPIzHGsIlCi7sZZoMR8SdYbBGRvM0y+Jtb+ez4ieBzmiAUcpmPYpyOw==",
"license": "Apache-2.0",
"dependencies": {
"@swc/helpers": "^0.5.0"
}
},
"node_modules/@istanbuljs/load-nyc-config": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz",
@ -3480,15 +3473,6 @@
"url": "https://opencollective.com/eslint"
}
},
"node_modules/@swc/helpers": {
"version": "0.5.13",
"resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.13.tgz",
"integrity": "sha512-UoKGxQ3r5kYI9dALKJapMmuK+1zWM/H17Z1+iwnNmzcJRnfFuevZs375TA5rW31pu4BS4NoSy1fRsexDXfWn5w==",
"license": "Apache-2.0",
"dependencies": {
"tslib": "^2.4.0"
}
},
"node_modules/@testing-library/dom": {
"version": "10.4.0",
"resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-10.4.0.tgz",
@ -5818,6 +5802,16 @@
"node": ">=6.0"
}
},
"node_modules/chronoshift": {
"version": "0.10.0",
"resolved": "https://registry.npmjs.org/chronoshift/-/chronoshift-0.10.0.tgz",
"integrity": "sha512-dNvumPg7R6ACUOKbGo1zH6DtmTo5ut9/LNbzqaKGnpC9VdArIos8+kApHOVIZH4FCpm9M9XYh++jwlRHhc1PyA==",
"dependencies": {
"immutable-class": "^0.11.0",
"moment-timezone": "^0.5.26",
"tslib": "^2.3.1"
}
},
"node_modules/ci-info": {
"version": "3.3.0",
"resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.3.0.tgz",
@ -6211,11 +6205,10 @@
"dev": true
},
"node_modules/cross-spawn": {
"version": "7.0.6",
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
"integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==",
"version": "7.0.3",
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz",
"integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==",
"dev": true,
"license": "MIT",
"dependencies": {
"path-key": "^3.1.0",
"shebang-command": "^2.0.0",
@ -7020,15 +7013,6 @@
"tslib": "^2.0.3"
}
},
"node_modules/druid-query-toolkit": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/druid-query-toolkit/-/druid-query-toolkit-1.0.0.tgz",
"integrity": "sha512-yBQR4uDcks0lcsRSWoLQy16YQ4dx264m6i7TNQDFrACUKHlMtnw5l+4+UDZKbXbpUFLMLWCr/kLhmXzLJk50+Q==",
"license": "Apache-2.0",
"dependencies": {
"tslib": "^2.5.2"
}
},
"node_modules/duplexer": {
"version": "0.1.2",
"resolved": "https://registry.npmjs.org/duplexer/-/duplexer-0.1.2.tgz",
@ -8925,6 +8909,14 @@
"node": ">=4"
}
},
"node_modules/has-own-prop": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/has-own-prop/-/has-own-prop-2.0.0.tgz",
"integrity": "sha512-Pq0h+hvsVm6dDEa8x82GnLSYHOzNDt7f0ddFa3FqcQlgzEiptPqL+XrOJNavjOzSYiYWIrgeVYYgGlLmnxwilQ==",
"engines": {
"node": ">=8"
}
},
"node_modules/has-property-descriptors": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz",
@ -9221,11 +9213,10 @@
"dev": true
},
"node_modules/http-proxy-middleware": {
"version": "2.0.7",
"resolved": "https://registry.npmjs.org/http-proxy-middleware/-/http-proxy-middleware-2.0.7.tgz",
"integrity": "sha512-fgVY8AV7qU7z/MmXJ/rxwbrtQH4jBQ9m7kp3llF0liB7glmFeVZFBepQb32T3y8n8k2+AEYuMPCpinYW+/CuRA==",
"version": "2.0.6",
"resolved": "https://registry.npmjs.org/http-proxy-middleware/-/http-proxy-middleware-2.0.6.tgz",
"integrity": "sha512-ya/UeJ6HVBYxrgYotAZo1KvPWlgB48kUJLDePFeneHsVujFaW5WNj2NgWCAE//B1Dl02BIfYlpNgBy8Kf8Rjmw==",
"dev": true,
"license": "MIT",
"dependencies": {
"@types/http-proxy": "^1.17.8",
"http-proxy": "^1.18.1",
@ -9363,6 +9354,15 @@
"integrity": "sha512-0AOCmOip+xgJwEVTQj1EfiDDOkPmuyllDuTuEX+DDXUgapLAsBIfkg3sxCYyCEA8mQqZrrxPUGjcOQ2JS3WLkg==",
"dev": true
},
"node_modules/immutable-class": {
"version": "0.11.2",
"resolved": "https://registry.npmjs.org/immutable-class/-/immutable-class-0.11.2.tgz",
"integrity": "sha512-CzkVPkJXzkspt6RX+ipNgtvt16+rzEBUlA3yNPLkK5/S042c9wvuyfE4F5TfMfPJ6XF86Fp+OCwu6eeAnMICuw==",
"dependencies": {
"has-own-prop": "^2.0.0",
"tslib": "^2.3.1"
}
},
"node_modules/import-fresh": {
"version": "3.3.0",
"resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz",
@ -12889,6 +12889,25 @@
"mkdirp": "bin/cmd.js"
}
},
"node_modules/moment": {
"version": "2.29.4",
"resolved": "https://registry.npmjs.org/moment/-/moment-2.29.4.tgz",
"integrity": "sha512-5LC9SOxjSc2HF6vO2CyuTDNivEdoz2IvyJJGj6X8DJ0eFyfszE0QiEd+iXmBvUP3WHxSjFH/vIsA0EN00cgr8w==",
"engines": {
"node": "*"
}
},
"node_modules/moment-timezone": {
"version": "0.5.43",
"resolved": "https://registry.npmjs.org/moment-timezone/-/moment-timezone-0.5.43.tgz",
"integrity": "sha512-72j3aNyuIsDxdF1i7CEgV2FfxM1r6aaqJyLB2vwb33mXYyoyLly+F1zbWqhA3/bVIoJ4szlUoMbUnVdid32NUQ==",
"dependencies": {
"moment": "^2.29.4"
},
"engines": {
"node": "*"
}
},
"node_modules/mrmime": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/mrmime/-/mrmime-1.0.1.tgz",
@ -19541,6 +19560,14 @@
"integrity": "sha512-dBVuXR082gk3jsFp7Rd/JI4kytwGHecnCoTtXFb7DB6CNHp4rg5k1bhg0nWdLGLnOV71lmDzGQaLMy8iPLY0pw==",
"dev": true
},
"@druid-toolkit/query": {
"version": "0.22.23",
"resolved": "https://registry.npmjs.org/@druid-toolkit/query/-/query-0.22.23.tgz",
"integrity": "sha512-yQOUAQJP63rzsTCdLcqNB8aRtsYPw8rYBfPSXc4zfAA4y/GJc9OJeHcLFRMdUtpwBtm0ueARMUlTSQcTsyV8gQ==",
"requires": {
"tslib": "^2.5.2"
}
},
"@dual-bundle/import-meta-resolve": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/@dual-bundle/import-meta-resolve/-/import-meta-resolve-4.1.0.tgz",
@ -19747,19 +19774,14 @@
"dev": true
},
"@eslint/plugin-kit": {
"version": "0.2.3",
"resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.2.3.tgz",
"integrity": "sha512-2b/g5hRmpbb1o4GnTZax9N9m0FXzz9OV42ZzI4rDDMDuHUqigAiQCEWChBWCY4ztAGVRjoWT19v0yMmc5/L5kA==",
"version": "0.2.0",
"resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.2.0.tgz",
"integrity": "sha512-vH9PiIMMwvhCx31Af3HiGzsVNULDbyVkHXwlemn/B0TFj/00ho3y55efXrUZTfQipxoHC5u4xq6zblww1zm1Ig==",
"dev": true,
"requires": {
"levn": "^0.4.1"
}
},
"@flatten-js/interval-tree": {
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/@flatten-js/interval-tree/-/interval-tree-1.1.3.tgz",
"integrity": "sha512-xhFWUBoHJFF77cJO1D6REjdgJEMRf2Y2Z+eKEPav8evGKcLSnj1ud5pLXQSbGuxF3VSvT1rWhMfVpXEKJLTL+A=="
},
"@fontsource/open-sans": {
"version": "5.1.0",
"resolved": "https://registry.npmjs.org/@fontsource/open-sans/-/open-sans-5.1.0.tgz",
@ -19793,14 +19815,6 @@
"integrity": "sha512-JBxkERygn7Bv/GbN5Rv8Ul6LVknS+5Bp6RgDC/O8gEBU/yeH5Ui5C/OlWrTb6qct7LjjfT6Re2NxB0ln0yYybA==",
"dev": true
},
"@internationalized/date": {
"version": "3.5.6",
"resolved": "https://registry.npmjs.org/@internationalized/date/-/date-3.5.6.tgz",
"integrity": "sha512-jLxQjefH9VI5P9UQuqB6qNKnvFt1Ky1TPIzHGsIlCi7sZZoMR8SdYbBGRvM0y+Jtb+ez4ieBzmiAUcpmPYpyOw==",
"requires": {
"@swc/helpers": "^0.5.0"
}
},
"@istanbuljs/load-nyc-config": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz",
@ -20600,14 +20614,6 @@
}
}
},
"@swc/helpers": {
"version": "0.5.13",
"resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.13.tgz",
"integrity": "sha512-UoKGxQ3r5kYI9dALKJapMmuK+1zWM/H17Z1+iwnNmzcJRnfFuevZs375TA5rW31pu4BS4NoSy1fRsexDXfWn5w==",
"requires": {
"tslib": "^2.4.0"
}
},
"@testing-library/dom": {
"version": "10.4.0",
"resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-10.4.0.tgz",
@ -22405,6 +22411,16 @@
"integrity": "sha512-p3KULyQg4S7NIHixdwbGX+nFHkoBiA4YQmyWtjb8XngSKV124nJmRysgAeujbUVb15vh+RvFUfCPqU7rXk+hZg==",
"dev": true
},
"chronoshift": {
"version": "0.10.0",
"resolved": "https://registry.npmjs.org/chronoshift/-/chronoshift-0.10.0.tgz",
"integrity": "sha512-dNvumPg7R6ACUOKbGo1zH6DtmTo5ut9/LNbzqaKGnpC9VdArIos8+kApHOVIZH4FCpm9M9XYh++jwlRHhc1PyA==",
"requires": {
"immutable-class": "^0.11.0",
"moment-timezone": "^0.5.26",
"tslib": "^2.3.1"
}
},
"ci-info": {
"version": "3.3.0",
"resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.3.0.tgz",
@ -22719,9 +22735,9 @@
"dev": true
},
"cross-spawn": {
"version": "7.0.6",
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
"integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==",
"version": "7.0.3",
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz",
"integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==",
"dev": true,
"requires": {
"path-key": "^3.1.0",
@ -23260,14 +23276,6 @@
"tslib": "^2.0.3"
}
},
"druid-query-toolkit": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/druid-query-toolkit/-/druid-query-toolkit-1.0.0.tgz",
"integrity": "sha512-yBQR4uDcks0lcsRSWoLQy16YQ4dx264m6i7TNQDFrACUKHlMtnw5l+4+UDZKbXbpUFLMLWCr/kLhmXzLJk50+Q==",
"requires": {
"tslib": "^2.5.2"
}
},
"duplexer": {
"version": "0.1.2",
"resolved": "https://registry.npmjs.org/duplexer/-/duplexer-0.1.2.tgz",
@ -24642,6 +24650,11 @@
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz",
"integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0="
},
"has-own-prop": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/has-own-prop/-/has-own-prop-2.0.0.tgz",
"integrity": "sha512-Pq0h+hvsVm6dDEa8x82GnLSYHOzNDt7f0ddFa3FqcQlgzEiptPqL+XrOJNavjOzSYiYWIrgeVYYgGlLmnxwilQ=="
},
"has-property-descriptors": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz",
@ -24871,9 +24884,9 @@
}
},
"http-proxy-middleware": {
"version": "2.0.7",
"resolved": "https://registry.npmjs.org/http-proxy-middleware/-/http-proxy-middleware-2.0.7.tgz",
"integrity": "sha512-fgVY8AV7qU7z/MmXJ/rxwbrtQH4jBQ9m7kp3llF0liB7glmFeVZFBepQb32T3y8n8k2+AEYuMPCpinYW+/CuRA==",
"version": "2.0.6",
"resolved": "https://registry.npmjs.org/http-proxy-middleware/-/http-proxy-middleware-2.0.6.tgz",
"integrity": "sha512-ya/UeJ6HVBYxrgYotAZo1KvPWlgB48kUJLDePFeneHsVujFaW5WNj2NgWCAE//B1Dl02BIfYlpNgBy8Kf8Rjmw==",
"dev": true,
"requires": {
"@types/http-proxy": "^1.17.8",
@ -24969,6 +24982,15 @@
"integrity": "sha512-0AOCmOip+xgJwEVTQj1EfiDDOkPmuyllDuTuEX+DDXUgapLAsBIfkg3sxCYyCEA8mQqZrrxPUGjcOQ2JS3WLkg==",
"dev": true
},
"immutable-class": {
"version": "0.11.2",
"resolved": "https://registry.npmjs.org/immutable-class/-/immutable-class-0.11.2.tgz",
"integrity": "sha512-CzkVPkJXzkspt6RX+ipNgtvt16+rzEBUlA3yNPLkK5/S042c9wvuyfE4F5TfMfPJ6XF86Fp+OCwu6eeAnMICuw==",
"requires": {
"has-own-prop": "^2.0.0",
"tslib": "^2.3.1"
}
},
"import-fresh": {
"version": "3.3.0",
"resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz",
@ -27535,6 +27557,19 @@
"minimist": "^1.2.5"
}
},
"moment": {
"version": "2.29.4",
"resolved": "https://registry.npmjs.org/moment/-/moment-2.29.4.tgz",
"integrity": "sha512-5LC9SOxjSc2HF6vO2CyuTDNivEdoz2IvyJJGj6X8DJ0eFyfszE0QiEd+iXmBvUP3WHxSjFH/vIsA0EN00cgr8w=="
},
"moment-timezone": {
"version": "0.5.43",
"resolved": "https://registry.npmjs.org/moment-timezone/-/moment-timezone-0.5.43.tgz",
"integrity": "sha512-72j3aNyuIsDxdF1i7CEgV2FfxM1r6aaqJyLB2vwb33mXYyoyLly+F1zbWqhA3/bVIoJ4szlUoMbUnVdid32NUQ==",
"requires": {
"moment": "^2.29.4"
}
},
"mrmime": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/mrmime/-/mrmime-1.0.1.tgz",

View File

@ -55,11 +55,11 @@
"@blueprintjs/datetime2": "^2.3.11",
"@blueprintjs/icons": "^5.13.0",
"@blueprintjs/select": "^5.2.5",
"@flatten-js/interval-tree": "^1.1.3",
"@druid-toolkit/query": "^0.22.23",
"@fontsource/open-sans": "^5.0.30",
"@internationalized/date": "^3.5.6",
"ace-builds": "~1.5.3",
"axios": "^1.7.7",
"chronoshift": "^0.10.0",
"classnames": "^2.2.6",
"copy-to-clipboard": "^3.3.3",
"d3-array": "^3.2.4",
@ -69,7 +69,6 @@
"d3-scale-chromatic": "^3.1.0",
"d3-selection": "^3.0.0",
"date-fns": "^2.28.0",
"druid-query-toolkit": "^1.0.0",
"echarts": "^5.5.1",
"file-saver": "^2.0.5",
"hjson": "^3.2.2",

View File

@ -39,12 +39,6 @@ const initialFunctionDocs = {
),
],
],
UNNEST: [
[
'arrayExpression',
convertMarkdownToHtml("Unnests ARRAY typed values. The source for UNNEST can be an array type column, or an input that's been transformed into an array, such as with helper functions like `MV_TO_ARRAY` or `ARRAY`.")
]
]
};
function hasHtmlTags(str) {

View File

@ -193,7 +193,6 @@ checker.init(
if (name === 'diff-match-patch') publisher = 'Google';
if (name === 'esutils') publisher = 'Yusuke Suzuki'; // https://github.com/estools/esutils#license
if (name === 'echarts') publisher = 'Apache Software Foundation';
if (name === '@internationalized/date') publisher = 'Adobe';
}
if (!publisher) {

View File

@ -16,7 +16,7 @@
* limitations under the License.
*/
import { QueryResult } from 'druid-query-toolkit';
import { QueryResult } from '@druid-toolkit/query';
import * as JSONBig from 'json-bigint-native';
export function bootstrapJsonParse() {

View File

@ -170,6 +170,7 @@ exports[`AutoForm matches snapshot 1`] = `
</Memo(FormGroupWithInfo)>
<Blueprint5.FormGroup>
<Blueprint5.Button
fill={true}
minimal={true}
onClick={[Function]}
rightIcon="chevron-down"

View File

@ -539,6 +539,7 @@ export class AutoForm<T extends Record<string, any>> extends React.PureComponent
text={showMore ? 'Show less' : 'Show more'}
rightIcon={showMore ? IconNames.CHEVRON_UP : IconNames.CHEVRON_DOWN}
minimal
fill
onClick={() => {
this.setState(({ showMore }) => ({ showMore: !showMore }));
}}

View File

@ -18,8 +18,8 @@
import { Menu, MenuItem } from '@blueprintjs/core';
import { IconNames } from '@blueprintjs/icons';
import type { Column, SqlExpression, SqlQuery } from 'druid-query-toolkit';
import { C, L, SqlComparison, SqlLiteral, SqlRecord, trimString } from 'druid-query-toolkit';
import type { Column, SqlExpression, SqlQuery } from '@druid-toolkit/query';
import { C, L, SqlComparison, SqlLiteral, SqlRecord, trimString } from '@druid-toolkit/query';
import type { QueryAction } from '../../utils';
import { copyAndAlert, prettyPrintSql, stringifyValue } from '../../utils';

View File

@ -19,8 +19,8 @@
import type { InputGroupProps2, Intent } from '@blueprintjs/core';
import { Button, ButtonGroup, Classes, ControlGroup, InputGroup, Keys } from '@blueprintjs/core';
import { IconNames } from '@blueprintjs/icons';
import { SqlExpression, SqlFunction, SqlLiteral, SqlMulti } from '@druid-toolkit/query';
import classNames from 'classnames';
import { SqlExpression, SqlFunction, SqlLiteral, SqlMulti } from 'druid-query-toolkit';
import React, { useEffect, useState } from 'react';
import { clamp } from '../../utils';

View File

@ -43,7 +43,6 @@ export * from './menu-checkbox/menu-checkbox';
export * from './more-button/more-button';
export * from './plural-pair-if-needed/plural-pair-if-needed';
export * from './popover-text/popover-text';
export * from './portal-bubble/portal-bubble';
export * from './query-error-pane/query-error-pane';
export * from './record-table-pane/record-table-pane';
export * from './refresh-button/refresh-button';

View File

@ -1,86 +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';
$shpitz-size: 9px;
.portal-bubble {
position: absolute;
@include card-like;
.#{$bp-ns}-dark & {
background: $dark-gray1;
}
&.up {
transform: translate(-50%, -100%);
}
&.down {
transform: translate(-50%, 0);
}
&.mute {
pointer-events: none;
}
& > .shpitz {
content: '';
position: absolute;
transform: translate(-50%, 0);
border-right: $shpitz-size solid transparent;
border-left: $shpitz-size solid transparent;
}
&.up > .shpitz {
bottom: -$shpitz-size;
border-top: $shpitz-size solid $dark-gray1;
}
&.down > .shpitz {
top: -$shpitz-size;
border-bottom: $shpitz-size solid $dark-gray1;
}
& > .bubble-title-bar {
position: relative;
padding: 5px 5px 0 5px;
white-space: nowrap;
font-weight: bold;
&.with-close {
padding-right: 26px;
.close-button {
position: absolute;
top: 0;
right: 0;
}
}
}
& > .bubble-content {
padding: 5px;
white-space: nowrap;
}
.bubble-title-bar + .bubble-content {
padding-top: 0;
}
}

View File

@ -1,93 +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 classNames from 'classnames';
import type { ReactNode } from 'react';
import { useState } from 'react';
import { createPortal } from 'react-dom';
import { clamp } from '../../utils';
import './portal-bubble.scss';
const SHPITZ_SIZE = 10;
export interface PortalBubbleOpenOn {
x: number;
y: number;
title?: string;
text: ReactNode;
}
export interface PortalBubbleProps {
className?: string;
openOn: PortalBubbleOpenOn | undefined;
direction?: 'up' | 'down';
onClose?(): void;
mute?: boolean;
minimal?: boolean;
}
export const PortalBubble = function PortalBubble(props: PortalBubbleProps) {
const { className, openOn, direction = 'up', onClose, mute, minimal } = props;
const [myWidth, setMyWidth] = useState(200);
if (!openOn) return null;
const halfMyWidth = myWidth / 2;
const x = clamp(openOn.x, halfMyWidth, window.innerWidth - halfMyWidth);
const offset = clamp(x - openOn.x, -halfMyWidth, halfMyWidth);
return createPortal(
<div
className={classNames('portal-bubble', className, direction, {
mute: mute && !onClose,
})}
ref={element => {
if (!element) return;
setMyWidth(element.offsetWidth);
}}
style={{
left: x,
top: openOn.y + (minimal ? 0 : direction === 'up' ? -SHPITZ_SIZE : SHPITZ_SIZE),
}}
>
{(openOn.title || onClose) && (
<div className={classNames('bubble-title-bar', { 'with-close': Boolean(onClose) })}>
{openOn.title}
{onClose && (
<Button
className="close-button"
icon={IconNames.CROSS}
small
minimal
onClick={onClose}
/>
)}
</div>
)}
<div className="bubble-content">{openOn.text}</div>
{!minimal && (
<div className="shpitz" style={{ left: offset ? `calc(50% - ${offset}px)` : '50%' }} />
)}
</div>,
document.body,
);
};

View File

@ -18,8 +18,8 @@
import { Button, Icon, Popover } from '@blueprintjs/core';
import { IconNames } from '@blueprintjs/icons';
import type { Column, QueryResult } from '@druid-toolkit/query';
import classNames from 'classnames';
import type { Column, QueryResult } from 'druid-query-toolkit';
import React, { useEffect, useState } from 'react';
import type { RowRenderProps } from 'react-table';
import ReactTable from 'react-table';

View File

@ -70,7 +70,7 @@ exports[`RuleEditor matches snapshot no tier in rule 1`] = `
style="transform: translateY(0); transition: none;"
>
<div
class="rule-detail bp5-card bp5-elevation-2"
class="bp5-card bp5-elevation-2"
>
<div
class="bp5-form-group"
@ -451,7 +451,7 @@ exports[`RuleEditor matches snapshot with broadcast rule 1`] = `
style="transform: translateY(0); transition: none;"
>
<div
class="rule-detail bp5-card bp5-elevation-2"
class="bp5-card bp5-elevation-2"
>
<div
class="bp5-form-group"
@ -629,7 +629,7 @@ exports[`RuleEditor matches snapshot with existing tier and non existing tier in
style="transform: translateY(0); transition: none;"
>
<div
class="rule-detail bp5-card bp5-elevation-2"
class="bp5-card bp5-elevation-2"
>
<div
class="bp5-form-group"
@ -1190,7 +1190,7 @@ exports[`RuleEditor matches snapshot with existing tier in rule 1`] = `
style="transform: translateY(0); transition: none;"
>
<div
class="rule-detail bp5-card bp5-elevation-2"
class="bp5-card bp5-elevation-2"
>
<div
class="bp5-form-group"
@ -1581,7 +1581,7 @@ exports[`RuleEditor matches snapshot with non existing tier in rule 1`] = `
style="transform: translateY(0); transition: none;"
>
<div
class="rule-detail bp5-card bp5-elevation-2"
class="bp5-card bp5-elevation-2"
>
<div
class="bp5-form-group"

View File

@ -28,8 +28,4 @@
.include-future {
margin-left: 15px;
}
.rule-detail {
border: 1px solid rgba(255, 255, 255, 0.15);
}
}

View File

@ -30,9 +30,9 @@ import {
import { IconNames } from '@blueprintjs/icons';
import React, { useState } from 'react';
import type { Rule } from '../../druid-models';
import { RuleUtil } from '../../druid-models';
import { durationSanitizer } from '../../utils';
import type { Rule } from '../../utils/load-rule';
import { RuleUtil } from '../../utils/load-rule';
import { SuggestibleInput } from '../suggestible-input/suggestible-input';
import './rule-editor.scss';
@ -170,7 +170,7 @@ export const RuleEditor = React.memo(function RuleEditor(props: RuleEditorProps)
</div>
<Collapse isOpen={isOpen}>
<Card className="rule-detail" elevation={2}>
<Card elevation={2}>
<FormGroup>
<ControlGroup>
<HTMLSelect

View File

@ -0,0 +1,13 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`BarUnit matches snapshot 1`] = `
<svg>
<rect
class="bar-unit"
height="10"
width="10"
x="10"
y="10"
/>
</svg>
`;

View File

@ -4,309 +4,191 @@ exports[`SegmentTimeline matches snapshot 1`] = `
<div
class="segment-timeline"
>
<div
class="control-bar"
>
<div>
<div
class="bp5-button-group"
class="loader"
>
<div
aria-controls="listbox-0"
aria-disabled="false"
aria-expanded="false"
aria-haspopup="listbox"
class="bp5-popover-target"
role="combobox"
class="loader-logo"
>
<button
class="bp5-button bp5-small"
type="button"
<svg
viewBox="0 0 100 100"
>
<span
class="bp5-button-text"
>
Datasource: all
</span>
<span
aria-hidden="true"
class="bp5-icon bp5-icon-caret-down"
>
<svg
data-icon="caret-down"
height="16"
role="img"
viewBox="0 0 16 16"
width="16"
>
<path
d="M12 6.5c0-.28-.22-.5-.5-.5h-7a.495.495 0 00-.37.83l3.5 4c.09.1.22.17.37.17s.28-.07.37-.17l3.5-4c.08-.09.13-.2.13-.33z"
fill-rule="evenodd"
/>
</svg>
</span>
</button>
<path
class="one"
d="M54.2,69.8h-2.7c-0.7,0-1.3-0.6-1.3-1.3c0-0.7,0.6-1.3,1.3-1.3h2.7c11.5,0,23.8-7.4,23.8-23.7
c0-9.1-6.9-15.8-16.4-15.8H38c-0.7,0-1.3-0.6-1.3-1.3s0.6-1.3,1.3-1.3h23.6c5.3,0,10.1,1.9,13.6,5.3c3.5,3.4,5.4,8,5.4,13.1
c0,6.6-2.3,13-6.3,17.7C69.5,66.8,62.5,69.8,54.2,69.8z"
/>
<path
class="two"
d="M55.7,59.5h-26c-0.7,0-1.3-0.6-1.3-1.3c0-0.7,0.6-1.3,1.3-1.3h26c7.5,0,11.5-5.8,11.5-11.5
c0-4.2-3.2-7.3-7.7-7.3h-26c-0.7,0-1.3-0.6-1.3-1.3s0.6-1.3,1.3-1.3h26c5.9,0,10.3,4.3,10.3,9.9c0,3.7-1.3,7.2-3.7,9.8
C63.5,58,59.9,59.5,55.7,59.5z"
/>
<path
class="three"
d="M27.2,38h-6.3c-0.7,0-1.3-0.6-1.3-1.3s0.6-1.3,1.3-1.3h6.3c0.7,0,1.3,0.6,1.3,1.3S27.9,38,27.2,38z"
/>
<path
class="four"
d="M45.1,69.8h-5.8c-0.7,0-1.3-0.6-1.3-1.3c0-0.7,0.6-1.3,1.3-1.3h5.8c0.7,0,1.3,0.6,1.3,1.3
C46.4,69.2,45.8,69.8,45.1,69.8z"
/>
</svg>
</div>
</div>
<span
class="bp5-popover-target"
>
<button
aria-expanded="false"
aria-haspopup="menu"
class="bp5-button bp5-small"
type="button"
>
<span
class="bp5-button-text"
>
Show: Size
</span>
<span
aria-hidden="true"
class="bp5-icon bp5-icon-caret-down"
>
<svg
data-icon="caret-down"
height="16"
role="img"
viewBox="0 0 16 16"
width="16"
>
<path
d="M12 6.5c0-.28-.22-.5-.5-.5h-7a.495.495 0 00-.37.83l3.5 4c.09.1.22.17.37.17s.28-.07.37-.17l3.5-4c.08-.09.13-.2.13-.33z"
fill-rule="evenodd"
/>
</svg>
</span>
</button>
</span>
<div
class="expander"
/>
<div
class="bp5-button-group"
>
<button
class="bp5-button bp5-small"
data-tooltip="Previous time period
2024-10-15 → 2024-11-01"
type="button"
>
<span
aria-hidden="true"
class="bp5-icon bp5-icon-caret-left"
>
<svg
data-icon="caret-left"
height="16"
role="img"
viewBox="0 0 16 16"
width="16"
>
<path
d="M9.5 4c-.13 0-.24.05-.33.13l-4 3.5c-.1.09-.17.22-.17.37s.07.28.17.37l4 3.5a.495.495 0 00.83-.37v-7c0-.28-.22-.5-.5-.5z"
fill-rule="evenodd"
/>
</svg>
</span>
</button>
<button
class="bp5-button bp5-small"
data-tooltip="Zoom out
2024-10-15 → 2024-11-20"
type="button"
>
<span
aria-hidden="true"
class="bp5-icon bp5-icon-zoom-out"
>
<svg
data-icon="zoom-out"
height="16"
role="img"
viewBox="0 0 16 16"
width="16"
>
<path
d="M3.99 5.99c-.55 0-1 .45-1 1s.45 1 1 1h6c.55 0 1-.45 1-1s-.45-1-1-1h-6zm11.56 7.44l-2.67-2.68a6.94 6.94 0 001.11-3.76c0-3.87-3.13-7-7-7s-7 3.13-7 7 3.13 7 7 7c1.39 0 2.68-.42 3.76-1.11l2.68 2.67a1.498 1.498 0 102.12-2.12zm-8.56-1.44c-2.76 0-5-2.24-5-5s2.24-5 5-5 5 2.24 5 5-2.24 5-5 5z"
fill-rule="evenodd"
/>
</svg>
</span>
</button>
<button
class="bp5-button bp5-small"
data-tooltip="Next time period
2024-11-18 → 2024-12-05"
type="button"
>
<span
aria-hidden="true"
class="bp5-icon bp5-icon-caret-right"
>
<svg
data-icon="caret-right"
height="16"
role="img"
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>
</button>
</div>
<div
class="bp5-button-group"
>
<button
class="bp5-button bp5-small"
data-tooltip="Show last day
2024-11-19 → 2024-11-20"
type="button"
>
<span
class="bp5-button-text"
>
1D
</span>
</button>
<button
class="bp5-button bp5-small"
data-tooltip="Show last week
2024-11-13 → 2024-11-20"
type="button"
>
<span
class="bp5-button-text"
>
1W
</span>
</button>
<button
class="bp5-button bp5-small"
data-tooltip="Show last month
2024-10-20 → 2024-11-20"
type="button"
>
<span
class="bp5-button-text"
>
1M
</span>
</button>
<button
class="bp5-button bp5-small"
data-tooltip="Show last 3 months
2024-08-20 → 2024-11-20"
type="button"
>
<span
class="bp5-button-text"
>
3M
</span>
</button>
<button
class="bp5-button bp5-small"
data-tooltip="Show last year
2023-11-20 → 2024-11-20"
type="button"
>
<span
class="bp5-button-text"
>
1Y
</span>
</button>
<button
class="bp5-button bp5-small"
data-tooltip="Show last 5 years
2019-11-20 → 2024-11-20"
type="button"
>
<span
class="bp5-button-text"
>
5Y
</span>
</button>
<button
class="bp5-button bp5-small"
data-tooltip="Show last 10 years
2014-11-20 → 2024-11-20"
type="button"
>
<span
class="bp5-button-text"
>
10Y
</span>
</button>
<span
class="bp5-popover-target"
>
<button
aria-expanded="false"
aria-haspopup="menu"
class="bp5-button bp5-small"
data-tooltip="Select a custom date range"
type="button"
>
<span
aria-hidden="true"
class="bp5-icon bp5-icon-calendar"
>
<svg
data-icon="calendar"
height="16"
role="img"
viewBox="0 0 16 16"
width="16"
>
<path
d="M11 3c.6 0 1-.5 1-1V1c0-.6-.4-1-1-1s-1 .4-1 1v1c0 .5.4 1 1 1zm3-2h-1v1c0 1.1-.9 2-2 2s-2-.9-2-2V1H6v1c0 1.1-.9 2-2 2s-2-.9-2-2V1H1c-.6 0-1 .5-1 1v12c0 .6.4 1 1 1h13c.6 0 1-.4 1-1V2c0-.6-.5-1-1-1zM5 13H2v-3h3v3zm0-4H2V6h3v3zm4 4H6v-3h3v3zm0-4H6V6h3v3zm4 4h-3v-3h3v3zm0-4h-3V6h3v3zM4 3c.6 0 1-.5 1-1V1c0-.6-.4-1-1-1S3 .4 3 1v1c0 .5.4 1 1 1z"
fill-rule="evenodd"
/>
</svg>
</span>
<span
class="bp5-button-text"
>
2024-11-01 → 2024-11-18
</span>
</button>
</span>
<button
class="bp5-button bp5-small"
data-tooltip="Auto determine date range to fit datasource"
type="button"
>
<span
aria-hidden="true"
class="bp5-icon bp5-icon-pin"
>
<svg
data-icon="pin"
height="16"
role="img"
viewBox="0 0 16 16"
width="16"
>
<path
d="M9.41.92c-.51.51-.41 1.5.15 2.56L4.34 7.54C2.8 6.48 1.45 6.05.92 6.58l3.54 3.54-3.54 4.95 4.95-3.54 3.54 3.54c.53-.53.1-1.88-.96-3.42l4.06-5.22c1.06.56 2.04.66 2.55.15L9.41.92z"
fill-rule="evenodd"
/>
</svg>
</span>
</button>
</div>
</div>
<div
class="chart-container"
/>
class="side-control"
>
<div
class="bp5-form-group"
>
<label
class="bp5-label"
>
Show
<span
class="bp5-text-muted"
/>
</label>
<div
class="bp5-form-content"
>
<div
class="bp5-segmented-control bp5-fill"
role="radiogroup"
>
<button
aria-checked="true"
class="bp5-button"
role="radio"
tabindex="0"
type="button"
>
<span
class="bp5-button-text"
>
Total size
</span>
</button>
<button
aria-checked="false"
class="bp5-button bp5-minimal"
role="radio"
tabindex="-1"
type="button"
>
<span
class="bp5-button-text"
>
Segment count
</span>
</button>
</div>
</div>
</div>
<div
class="bp5-form-group"
>
<label
class="bp5-label"
>
Interval
<span
class="bp5-text-muted"
/>
</label>
<div
class="bp5-form-content"
>
<div
aria-expanded="false"
aria-haspopup="menu"
class="bp5-control-group bp5-date-range-input bp5-popover-target bp5-fill"
>
<div
class="bp5-input-group bp5-fill"
>
<input
aria-disabled="false"
autocomplete="off"
class="bp5-input"
placeholder="Start date"
type="text"
value="2021-03-09"
/>
</div>
<div
class="bp5-input-group bp5-fill"
>
<input
aria-disabled="false"
autocomplete="off"
class="bp5-input"
placeholder="End date"
type="text"
value="2021-06-09"
/>
</div>
</div>
</div>
</div>
<div
class="bp5-form-group"
>
<label
class="bp5-label"
>
Datasource
<span
class="bp5-text-muted"
/>
</label>
<div
class="bp5-form-content"
>
<div
aria-controls="listbox-1"
aria-expanded="false"
aria-haspopup="listbox"
class="bp5-popover-target bp5-fill"
role="combobox"
>
<button
class="bp5-button bp5-fill"
type="button"
>
<span
class="bp5-button-text"
>
Show all
</span>
<span
aria-hidden="true"
class="bp5-icon bp5-icon-caret-down"
>
<svg
data-icon="caret-down"
height="16"
role="img"
viewBox="0 0 16 16"
width="16"
>
<path
d="M12 6.5c0-.28-.22-.5-.5-.5h-7a.495.495 0 00-.37.83l3.5 4c.09.1.22.17.37.17s.28-.07.37-.17l3.5-4c.08-.09.13-.2.13-.33z"
fill-rule="evenodd"
/>
</svg>
</span>
</button>
</div>
</div>
</div>
</div>
</div>
`;

View File

@ -0,0 +1,75 @@
/*
* 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 type { AxisScale } from 'd3-axis';
import React from 'react';
import { BarUnit } from './bar-unit';
import type { BarUnitData, HoveredBarInfo } from './stacked-bar-chart';
interface BarGroupProps {
dataToRender: BarUnitData[];
changeActiveDatasource: (dataSource: string) => void;
formatTick: (e: number) => string;
xScale: AxisScale<Date>;
yScale: AxisScale<number>;
barWidth: number;
onHoverBar?: (e: any) => void;
offHoverBar?: () => void;
hoverOn?: HoveredBarInfo | null;
}
export class BarGroup extends React.Component<BarGroupProps> {
shouldComponentUpdate(nextProps: BarGroupProps): boolean {
return nextProps.hoverOn === this.props.hoverOn;
}
render() {
const { dataToRender, changeActiveDatasource, xScale, yScale, onHoverBar, barWidth } =
this.props;
if (dataToRender === undefined) return null;
return dataToRender.map((entry: BarUnitData, i: number) => {
const y0 = yScale(entry.y0 || 0) || 0;
const x = xScale(new Date(entry.x + 'T00:00:00Z'));
const y = yScale((entry.y0 || 0) + entry.y) || 0;
const height = Math.max(y0 - y, 0);
const barInfo: HoveredBarInfo = {
xCoordinate: x,
yCoordinate: y,
height,
datasource: entry.datasource,
xValue: entry.x,
yValue: entry.y,
dailySize: entry.dailySize,
};
return (
<BarUnit
key={i}
x={x}
y={y}
width={barWidth}
height={height}
style={{ fill: entry.color }}
onClick={() => changeActiveDatasource(entry.datasource)}
onHover={() => onHoverBar && onHoverBar(barInfo)}
/>
);
});
}
}

View File

@ -16,6 +16,18 @@
* limitations under the License.
*/
.segment-bar-chart {
position: relative;
}
import { render } from '@testing-library/react';
import { BarUnit } from './bar-unit';
describe('BarUnit', () => {
it('matches snapshot', () => {
const barGroup = (
<svg>
<BarUnit x={10} y={10} width={10} height={10} />
</svg>
);
const { container } = render(barGroup);
expect(container.firstChild).toMatchSnapshot();
});
});

View File

@ -16,26 +16,30 @@
* limitations under the License.
*/
import { shallow } from '../../../utils/shallow-renderer';
interface BarChartUnitProps {
x: number | undefined;
y: number;
width: number;
height: number;
style?: any;
onClick?: () => void;
onHover?: () => void;
offHover?: () => void;
}
import { TimezoneMenuItems } from './timezone-menu-items';
jest.useFakeTimers('modern').setSystemTime(Date.parse('2024-06-08T12:34:56Z'));
describe('TimezoneMenuItems', () => {
it('ensure UTC', () => {
expect(new Date().getTimezoneOffset()).toBe(0);
});
it('matches snapshot', () => {
const comp = shallow(
<TimezoneMenuItems
sqlTimeZone="Blah"
setSqlTimeZone={() => {}}
defaultSqlTimeZone="Etc/UTC"
/>,
);
expect(comp).toMatchSnapshot();
});
});
export function BarUnit(props: BarChartUnitProps) {
const { x, y, width, height, style, onClick, onHover, offHover } = props;
return (
<rect
className="bar-unit"
x={x}
y={y}
width={width}
height={height}
style={style}
onClick={onClick}
onMouseOver={onHover}
onMouseLeave={offHover}
/>
);
}

View File

@ -16,22 +16,22 @@
* limitations under the License.
*/
import type { Axis } from 'd3-axis';
import { select } from 'd3-selection';
import React from 'react';
interface ChartAxisProps {
className?: string;
transform?: string;
axis: Axis<any>;
scale: any;
className?: string;
}
export const ChartAxis = function ChartAxis(props: ChartAxisProps) {
const { transform, axis, className } = props;
export const ChartAxis = React.memo(function ChartAxis(props: ChartAxisProps) {
const { transform, scale, className } = props;
return (
<g
className={`chart-axis ${className}`}
transform={transform}
ref={node => select(node).call(axis as any)}
ref={node => select(node).call(scale)}
/>
);
};
});

View File

@ -1,87 +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 { sum } from 'd3-array';
import type { Duration } from '../../utils';
import { formatBytes, formatInteger } from '../../utils';
export type IntervalStat = 'segments' | 'size' | 'rows';
export const INTERVAL_STATS: IntervalStat[] = ['segments', 'size', 'rows'];
export function getIntervalStatTitle(intervalStat: IntervalStat): string {
switch (intervalStat) {
case 'segments':
return 'Num. segments';
case 'size':
return 'Size';
case 'rows':
return 'Rows';
default:
return intervalStat;
}
}
export function aggregateSegmentStats(
xs: readonly Record<IntervalStat, number>[],
): Record<IntervalStat, number> {
return {
segments: sum(xs, s => s.segments),
size: sum(xs, s => s.size),
rows: sum(xs, s => s.rows),
};
}
export function formatIntervalStat(stat: IntervalStat, n: number) {
switch (stat) {
case 'segments':
case 'rows':
return formatInteger(n);
case 'size':
return formatBytes(n);
default:
return '';
}
}
export interface IntervalRow extends Record<IntervalStat, number> {
start: Date;
end: Date;
datasource: string;
realtime: boolean;
originalTimeSpan: Duration;
}
export interface TrimmedIntervalRow extends IntervalRow {
shownDays: number;
normalized: Record<IntervalStat, number>;
}
export interface IntervalBar extends TrimmedIntervalRow {
offset: Record<IntervalStat, number>;
}
export function formatIsoDateOnly(date: Date): string {
return date.toISOString().slice(0, 10);
}

View File

@ -1,169 +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';
.segment-bar-chart-render {
position: relative;
overflow: hidden;
@keyframes pulseOpacity {
0% {
opacity: 0.8;
}
100% {
opacity: 0.95;
}
}
svg {
position: absolute;
.chart-axis text {
user-select: none;
}
.hover-highlight {
fill: white;
fill-opacity: 0.1;
}
.hovered-bar {
fill: none;
stroke: #ffffff;
stroke-width: 1.5px;
}
.selection {
fill: transparent;
stroke: #ffffff;
stroke-width: 1px;
opacity: 0.8;
&.done {
opacity: 1;
}
}
.shifter {
fill: white;
fill-opacity: 0.2;
filter: blur(1px);
}
.time-shift-indicator {
fill: white;
fill-opacity: 0.001;
cursor: grab;
&:hover {
fill-opacity: 0.1;
}
&.shifting {
fill-opacity: 0.2;
cursor: grabbing;
}
}
.gridline-x {
line {
stroke-dasharray: 5, 5;
opacity: 0.5;
}
}
.now-line {
stroke: $orange4;
stroke-dasharray: 2, 2;
opacity: 0.7;
}
.bar-unit {
&.realtime {
animation: pulseOpacity 3s alternate infinite;
}
}
}
.rule-tape {
position: absolute;
top: 5px;
height: 15px;
font-size: 10px;
.rule-error {
@include pin-full();
background-color: $red3;
color: $white;
}
.load-rule {
position: absolute;
overflow: hidden;
padding-left: 2px;
border-left: 1px solid $dark-gray2;
border-right: 1px solid $dark-gray2;
top: 0;
height: 100%;
text-overflow: ellipsis;
&.load {
background-color: $green1;
&:nth-child(even) {
background-color: $green3;
}
}
&.drop {
background-color: $dark-gray5;
&:nth-child(even) {
background-color: $gray1;
}
}
&.broadcast {
background-color: $indigo1;
&:nth-child(even) {
background-color: $indigo3;
}
}
}
}
.empty-placeholder {
@include pin-full;
display: flex;
align-items: center;
justify-content: center;
font-size: 20px;
user-select: none;
pointer-events: none;
}
}
.segment-bar-chart-bubble {
.button-bar {
padding-top: 5px;
display: flex;
gap: 5px;
}
}

View File

@ -1,793 +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 } from '@blueprintjs/core';
import type { NonNullDateRange } from '@blueprintjs/datetime';
import { IconNames } from '@blueprintjs/icons';
import IntervalTree from '@flatten-js/interval-tree';
import classNames from 'classnames';
import { max, sort, sum } from 'd3-array';
import { axisBottom, axisLeft } from 'd3-axis';
import { scaleLinear, scaleUtc } from 'd3-scale';
import type { MouseEvent as ReactMouseEvent, ReactNode } from 'react';
import { useMemo, useRef, useState } from 'react';
import type { Rule } from '../../druid-models';
import { getDatasourceColor, RuleUtil } from '../../druid-models';
import { useClock, useGlobalEventListener } from '../../hooks';
import {
allSameValue,
arraysEqualByElement,
clamp,
day,
Duration,
formatBytes,
formatNumber,
groupBy,
groupByAsMap,
minute,
month,
pluralIfNeeded,
TZ_UTC,
uniq,
} from '../../utils';
import type { Margin, Stage } from '../../utils/stage';
import type { PortalBubbleOpenOn } from '../portal-bubble/portal-bubble';
import { PortalBubble } from '../portal-bubble/portal-bubble';
import { ChartAxis } from './chart-axis';
import type { IntervalBar, IntervalRow, IntervalStat, TrimmedIntervalRow } from './common';
import { aggregateSegmentStats, formatIntervalStat, formatIsoDateOnly } from './common';
import './segment-bar-chart-render.scss';
const CHART_MARGIN: Margin = { top: 20, right: 0, bottom: 25, left: 70 };
const MIN_BAR_WIDTH = 4;
const POSSIBLE_GRANULARITIES = [
new Duration('PT15M'),
new Duration('PT1H'),
new Duration('PT6H'),
new Duration('P1D'),
new Duration('P1M'),
new Duration('P1Y'),
];
const EXTEND_X_SCALE_DOMAIN_BY = 1;
function formatStartDuration(start: Date, duration: Duration): string {
let sliceLength;
const { singleSpan } = duration;
switch (singleSpan) {
case 'year':
sliceLength = 4;
break;
case 'month':
sliceLength = 7;
break;
case 'day':
sliceLength = 10;
break;
case 'hour':
sliceLength = 13;
break;
case 'minute':
sliceLength = 16;
break;
default:
sliceLength = 19;
break;
}
return `${start.toISOString().slice(0, sliceLength)}/${duration}`;
}
// ---------------------------------------
// Load rule stuff
function loadRuleToBaseType(loadRule: Rule): string {
const m = /^(load|drop|broadcast)/.exec(loadRule.type);
return m ? m[1] : 'load';
}
const NEGATIVE_INFINITY_DATE = new Date(Date.UTC(1000, 0, 1));
const POSITIVE_INFINITY_DATE = new Date(Date.UTC(3000, 0, 1));
function loadRuleToDateRange(loadRule: Rule): NonNullDateRange {
switch (loadRule.type) {
case 'loadByInterval':
case 'dropByInterval':
case 'broadcastByInterval':
return String(loadRule.interval)
.split('/')
.map(d => new Date(d)) as NonNullDateRange;
case 'loadByPeriod':
case 'dropByPeriod':
case 'broadcastByPeriod':
return [
new Duration(loadRule.period || 'P1D').shift(new Date(), TZ_UTC, -1),
loadRule.includeFuture ? POSITIVE_INFINITY_DATE : new Date(),
];
case 'dropBeforeByPeriod':
return [
NEGATIVE_INFINITY_DATE,
new Duration(loadRule.period || 'P1D').shift(new Date(), TZ_UTC, -1),
];
default:
return [NEGATIVE_INFINITY_DATE, POSITIVE_INFINITY_DATE];
}
}
// ---------------------------------------
function offsetDateRange(dateRange: NonNullDateRange, offset: number): NonNullDateRange {
return [new Date(dateRange[0].valueOf() + offset), new Date(dateRange[1].valueOf() + offset)];
}
function stackIntervalRows(trimmedIntervalRows: TrimmedIntervalRow[]): {
intervalBars: IntervalBar[];
intervalTree: IntervalTree;
} {
// Total size of the datasource will be user as an ordering tiebreaker
const datasourceToTotalSize = groupByAsMap(
trimmedIntervalRows,
intervalRow => intervalRow.datasource,
intervalRows => sum(intervalRows, intervalRow => intervalRow.size),
);
const sortedIntervalRows = sort(trimmedIntervalRows, (a, b) => {
const shownDaysDiff = b.shownDays - a.shownDays;
if (shownDaysDiff) return shownDaysDiff;
const timeSpanDiff =
b.originalTimeSpan.getCanonicalLength() - a.originalTimeSpan.getCanonicalLength();
if (timeSpanDiff) return timeSpanDiff;
const totalSizeDiff = datasourceToTotalSize[b.datasource] - datasourceToTotalSize[a.datasource];
if (totalSizeDiff) return totalSizeDiff;
return Number(a.realtime) - Number(b.realtime);
});
const intervalTree = new IntervalTree();
const intervalBars = sortedIntervalRows.map(intervalRow => {
const startMs = intervalRow.start.valueOf();
const endMs = intervalRow.end.valueOf();
const intervalRowsBelow = intervalTree.search([startMs + 1, startMs + 2]) as IntervalBar[];
const intervalBar: IntervalBar = {
...intervalRow,
offset: aggregateSegmentStats(intervalRowsBelow.map(i => i.normalized)),
};
intervalTree.insert([startMs, endMs], intervalBar);
return intervalBar;
});
return {
intervalBars,
intervalTree,
};
}
interface BubbleInfo {
start: Date;
end: Date;
timeLabel: string;
intervalBars: IntervalBar[];
}
interface SelectionRange {
start: Date;
end: Date;
done?: boolean;
}
export interface DatasourceRules {
loadRules: Rule[];
defaultLoadRules: Rule[];
}
export interface SegmentBarChartRenderProps {
intervalRows: IntervalRow[];
datasourceRules: DatasourceRules | undefined;
datasourceRulesError: string | undefined;
stage: Stage;
dateRange: NonNullDateRange;
changeDateRange(dateRange: NonNullDateRange): void;
shownIntervalStat: IntervalStat;
shownDatasource: string | undefined;
changeShownDatasource(datasource: string | undefined): void;
getIntervalActionButton?(
start: Date,
end: Date,
datasource?: string,
realtime?: boolean,
): ReactNode;
}
export const SegmentBarChartRender = function SegmentBarChartRender(
props: SegmentBarChartRenderProps,
) {
const {
intervalRows,
datasourceRules,
datasourceRulesError,
stage,
shownIntervalStat,
dateRange,
changeDateRange,
shownDatasource,
changeShownDatasource,
getIntervalActionButton,
} = props;
const [mouseDownAt, setMouseDownAt] = useState<
{ time: Date; action: 'select' | 'shift' } | undefined
>();
const [selection, setSelection] = useState<SelectionRange | undefined>();
function setSelectionIfNeeded(newSelection: SelectionRange) {
if (
selection &&
selection.start.valueOf() === newSelection.start.valueOf() &&
selection.end.valueOf() === newSelection.end.valueOf() &&
selection.done === newSelection.done
) {
return;
}
setSelection(newSelection);
}
const [bubbleInfo, setBubbleInfo] = useState<BubbleInfo | undefined>();
function setBubbleInfoIfNeeded(newBubbleInfo: BubbleInfo) {
if (
bubbleInfo &&
bubbleInfo.start.valueOf() === newBubbleInfo.start.valueOf() &&
bubbleInfo.end.valueOf() === newBubbleInfo.end.valueOf() &&
bubbleInfo.timeLabel === newBubbleInfo.timeLabel &&
arraysEqualByElement(bubbleInfo.intervalBars, newBubbleInfo.intervalBars)
) {
return;
}
setBubbleInfo(newBubbleInfo);
}
const [shiftOffset, setShiftOffset] = useState<number | undefined>();
const now = useClock(minute.canonicalLength);
const svgRef = useRef<SVGSVGElement | null>(null);
const trimGranularity = useMemo(() => {
return Duration.pickSmallestGranularityThatFits(
POSSIBLE_GRANULARITIES,
dateRange[1].valueOf() - dateRange[0].valueOf(),
Math.floor(stage.width / MIN_BAR_WIDTH),
).toString();
}, [dateRange, stage.width]);
const { intervalBars, intervalTree } = useMemo(() => {
const shownIntervalRows = intervalRows.filter(
({ start, end, datasource }) =>
start <= dateRange[1] &&
dateRange[0] < end &&
(!shownDatasource || datasource === shownDatasource),
);
const averageRowSizeByDatasource = groupByAsMap(
shownIntervalRows.filter(intervalRow => intervalRow.size > 0 && intervalRow.rows > 0),
intervalRow => intervalRow.datasource,
intervalRows => sum(intervalRows, d => d.size) / sum(intervalRows, d => d.rows),
);
const trimDuration = new Duration(trimGranularity);
const trimmedIntervalRows = shownIntervalRows.map(intervalRow => {
const { start, end, segments, size, rows } = intervalRow;
const startTrimmed = trimDuration.floor(start, TZ_UTC);
let endTrimmed = trimDuration.ceil(end, TZ_UTC);
// Special handling to catch WEEK intervals when trimming to month.
if (trimGranularity === 'P1M' && intervalRow.originalTimeSpan.toString() === 'P7D') {
endTrimmed = trimDuration.shift(startTrimmed, TZ_UTC);
}
const shownDays = (endTrimmed.valueOf() - startTrimmed.valueOf()) / day.canonicalLength;
const shownSize =
size === 0 ? rows * averageRowSizeByDatasource[intervalRow.datasource] : size;
return {
...intervalRow,
start: startTrimmed,
end: endTrimmed,
shownDays,
size: shownSize,
normalized: {
size: shownSize / shownDays,
rows: rows / shownDays,
segments: segments / shownDays,
},
};
});
const fullyGroupedSegmentRows = groupBy(
trimmedIntervalRows,
trimmedIntervalRow =>
[
trimmedIntervalRow.start.toISOString(),
trimmedIntervalRow.end.toISOString(),
trimmedIntervalRow.originalTimeSpan,
trimmedIntervalRow.datasource,
trimmedIntervalRow.realtime,
].join('/'),
(trimmedIntervalRows): TrimmedIntervalRow => {
const firstIntervalRow = trimmedIntervalRows[0];
return {
...firstIntervalRow,
...aggregateSegmentStats(trimmedIntervalRows),
normalized: aggregateSegmentStats(trimmedIntervalRows.map(t => t.normalized)),
};
},
);
return stackIntervalRows(fullyGroupedSegmentRows);
}, [intervalRows, trimGranularity, dateRange, shownDatasource]);
const innerStage = stage.applyMargin(CHART_MARGIN);
const baseTimeScale = scaleUtc()
.domain(dateRange)
.range([EXTEND_X_SCALE_DOMAIN_BY, innerStage.width - EXTEND_X_SCALE_DOMAIN_BY]);
const timeScale = shiftOffset
? baseTimeScale.copy().domain(offsetDateRange(dateRange, shiftOffset))
: baseTimeScale;
const maxNormalizedStat = max(
intervalBars,
d => d.normalized[shownIntervalStat] + d.offset[shownIntervalStat],
);
const statScale = scaleLinear()
.rangeRound([innerStage.height, 0])
.domain([0, (maxNormalizedStat ?? 1) * 1.05]);
const formatTickRate = (n: number) => {
switch (shownIntervalStat) {
case 'segments':
return formatNumber(n); // + ' seg/day';
case 'rows':
return formatNumber(n); // + ' row/day';
case 'size':
return formatBytes(n);
}
};
function handleMouseDown(e: ReactMouseEvent) {
const svg = svgRef.current;
if (!svg) return;
e.preventDefault();
if (selection) {
setSelection(undefined);
} else {
const rect = svg.getBoundingClientRect();
const x = e.clientX - rect.x - CHART_MARGIN.left;
const y = e.clientY - rect.y - CHART_MARGIN.top;
const time = baseTimeScale.invert(x);
const action = y > innerStage.height || e.shiftKey ? 'shift' : 'select';
setBubbleInfo(undefined);
setMouseDownAt({
time,
action,
});
}
}
useGlobalEventListener('mousemove', (e: MouseEvent) => {
const svg = svgRef.current;
if (!svg) return;
const rect = svg.getBoundingClientRect();
const x = e.clientX - rect.x - CHART_MARGIN.left;
const y = e.clientY - rect.y - CHART_MARGIN.top;
if (mouseDownAt) {
e.preventDefault();
const b = baseTimeScale.invert(x);
if (mouseDownAt.action === 'shift' || e.shiftKey) {
setShiftOffset(mouseDownAt.time.valueOf() - b.valueOf());
} else {
if (mouseDownAt.time < b) {
setSelectionIfNeeded({
start: day.floor(mouseDownAt.time, TZ_UTC),
end: day.ceil(b, TZ_UTC),
});
} else {
setSelectionIfNeeded({
start: day.floor(b, TZ_UTC),
end: day.ceil(mouseDownAt.time, TZ_UTC),
});
}
}
} else if (!selection) {
if (
0 <= x &&
x <= innerStage.width &&
0 <= y &&
y <= innerStage.height + CHART_MARGIN.bottom
) {
const time = baseTimeScale.invert(x);
const shifter =
new Duration(trimGranularity).getCanonicalLength() > day.canonicalLength * 25
? month
: day;
const start = shifter.floor(time, TZ_UTC);
const end = shifter.ceil(time, TZ_UTC);
let intervalBars: IntervalBar[] = [];
if (y <= innerStage.height) {
const bars = intervalTree.search([
time.valueOf() + 1,
time.valueOf() + 2,
]) as IntervalBar[];
if (bars.length) {
const stat = statScale.invert(y);
const hoverBar = bars.find(
bar =>
bar.offset[shownIntervalStat] <= stat &&
stat < bar.offset[shownIntervalStat] + bar.normalized[shownIntervalStat],
);
intervalBars = hoverBar ? [hoverBar] : bars;
}
}
setBubbleInfoIfNeeded({
start,
end,
timeLabel: start.toISOString().slice(0, shifter === day ? 10 : 7),
intervalBars,
});
} else {
setBubbleInfo(undefined);
}
}
});
useGlobalEventListener('mouseup', (e: MouseEvent) => {
if (!mouseDownAt) return;
e.preventDefault();
setMouseDownAt(undefined);
const svg = svgRef.current;
if (!svg) return;
const rect = svg.getBoundingClientRect();
const x = e.clientX - rect.x - CHART_MARGIN.left;
const y = e.clientY - rect.y - CHART_MARGIN.top;
if (shiftOffset || selection) {
setShiftOffset(undefined);
if (mouseDownAt.action === 'shift' || e.shiftKey) {
if (shiftOffset) {
changeDateRange(offsetDateRange(dateRange, shiftOffset));
}
} else {
if (selection) {
setSelection({ ...selection, done: true });
}
}
} else if (0 <= x && x <= innerStage.width && 0 <= y && y <= innerStage.height) {
const time = baseTimeScale.invert(x);
const bars = intervalTree.search([time.valueOf() + 1, time.valueOf() + 2]) as IntervalBar[];
if (bars.length) {
const stat = statScale.invert(y);
const hoverBar = bars.find(
bar =>
bar.offset[shownIntervalStat] <= stat &&
stat < bar.offset[shownIntervalStat] + bar.normalized[shownIntervalStat],
);
if (hoverBar) {
changeShownDatasource(shownDatasource ? undefined : hoverBar.datasource);
}
}
}
});
useGlobalEventListener('keydown', (e: KeyboardEvent) => {
if (e.key === 'Escape' && mouseDownAt) {
setMouseDownAt(undefined);
setSelection(undefined);
}
});
function startEndToXWidth({ start, end }: { start: Date; end: Date }) {
const xStart = clamp(timeScale(start), 0, innerStage.width);
const xEnd = clamp(timeScale(end), 0, innerStage.width);
return {
x: xStart,
width: Math.max(xEnd - xStart - 1, 1),
};
}
function segmentBarToRect(intervalBar: IntervalBar) {
const y0 = statScale(intervalBar.offset[shownIntervalStat]);
const y = statScale(
intervalBar.normalized[shownIntervalStat] + intervalBar.offset[shownIntervalStat],
);
return {
...startEndToXWidth(intervalBar),
y: y,
height: y0 - y,
};
}
let hoveredOpenOn: PortalBubbleOpenOn | undefined;
if (svgRef.current) {
const rect = svgRef.current.getBoundingClientRect();
if (bubbleInfo) {
const hoveredIntervalBars = bubbleInfo.intervalBars;
let title: string | undefined;
let text: ReactNode;
if (hoveredIntervalBars.length === 0) {
title = bubbleInfo.timeLabel;
text = '';
} else if (hoveredIntervalBars.length === 1) {
const hoveredIntervalBar = hoveredIntervalBars[0];
title = `${formatStartDuration(
hoveredIntervalBar.start,
hoveredIntervalBar.originalTimeSpan,
)}${hoveredIntervalBar.realtime ? ' (realtime)' : ''}`;
text = (
<>
{!shownDatasource && <div>{`Datasource: ${hoveredIntervalBar.datasource}`}</div>}
<div>{`Size: ${
hoveredIntervalBar.realtime
? 'estimated for realtime'
: formatIntervalStat('size', hoveredIntervalBar.size)
}`}</div>
<div>{`Rows: ${formatIntervalStat('rows', hoveredIntervalBar.rows)}`}</div>
<div>{`Segments: ${formatIntervalStat('segments', hoveredIntervalBar.segments)}`}</div>
</>
);
} else {
const datasources = uniq(hoveredIntervalBars.map(b => b.datasource));
const agg = aggregateSegmentStats(hoveredIntervalBars);
title = bubbleInfo.timeLabel;
text = (
<>
{!shownDatasource && (
<div>{`Totals for ${pluralIfNeeded(datasources.length, 'datasource')}`}</div>
)}
<div>{`Size: ${formatIntervalStat('size', agg.size)}`}</div>
<div>{`Rows: ${formatIntervalStat('rows', agg.rows)}`}</div>
<div>{`Segments: ${formatIntervalStat('segments', agg.segments)}`}</div>
</>
);
}
hoveredOpenOn = {
x:
rect.x +
CHART_MARGIN.left +
timeScale(new Date((bubbleInfo.start.valueOf() + bubbleInfo.end.valueOf()) / 2)),
y: rect.y + CHART_MARGIN.top,
title,
text,
};
} else if (selection) {
const selectedBars = intervalTree.search([
selection.start.valueOf() + 1,
selection.end.valueOf() - 1,
]) as IntervalBar[];
const datasources = uniq(selectedBars.map(b => b.datasource));
const realtime = allSameValue(selectedBars.map(b => b.realtime));
const agg = aggregateSegmentStats(selectedBars);
hoveredOpenOn = {
x:
rect.x +
CHART_MARGIN.left +
timeScale(new Date((selection.start.valueOf() + selection.end.valueOf()) / 2)),
y: rect.y + CHART_MARGIN.top,
title: `${formatIsoDateOnly(selection.start)}${formatIsoDateOnly(selection.end)}`,
text: (
<>
{selectedBars.length ? (
<>
{!shownDatasource && (
<div>{`Totals for ${pluralIfNeeded(datasources.length, 'datasource')}`}</div>
)}
<div>{`Size: ${formatIntervalStat('size', agg.size)}`}</div>
<div>{`Rows: ${formatIntervalStat('rows', agg.rows)}`}</div>
<div>{`Segments: ${formatIntervalStat('segments', agg.segments)}`}</div>
</>
) : (
<div>No segments in this interval</div>
)}
{selection.done && (
<div className="button-bar">
<Button
icon={IconNames.ZOOM_IN}
text="Zoom in"
intent={Intent.PRIMARY}
small
onClick={() => {
if (!selection) return;
setSelection(undefined);
changeDateRange([selection.start, selection.end]);
}}
/>
{getIntervalActionButton?.(
selection.start,
selection.end,
datasources.length === 1 ? datasources[0] : undefined,
realtime,
)}
</div>
)}
</>
),
};
}
}
function renderLoadRule(loadRule: Rule, i: number, isDefault: boolean) {
const [start, end] = loadRuleToDateRange(loadRule);
const { x, width } = startEndToXWidth({ start, end });
const title = RuleUtil.ruleToString(loadRule) + (isDefault ? ' (cluster default)' : '');
return (
<div
key={i}
className={classNames('load-rule', loadRuleToBaseType(loadRule))}
data-tooltip={title}
style={{
left: x,
width,
}}
>
{title}
</div>
);
}
const nowX = timeScale(now);
return (
<div className="segment-bar-chart-render">
<svg
ref={svgRef}
width={stage.width}
height={stage.height}
viewBox={`0 0 ${stage.width} ${stage.height}`}
preserveAspectRatio="xMinYMin meet"
onMouseDown={handleMouseDown}
>
<g transform={`translate(${CHART_MARGIN.left},${CHART_MARGIN.top})`}>
<ChartAxis
className="gridline-x"
transform="translate(0,0)"
axis={axisLeft(statScale)
.tickValues(statScale.ticks(3).filter(v => v !== 0))
.tickSize(-innerStage.width)
.tickFormat(() => '')
.tickSizeOuter(0)}
/>
<ChartAxis
className="axis-x"
transform={`translate(0,${innerStage.height})`}
axis={axisBottom(timeScale)}
/>
<rect
className={classNames('time-shift-indicator', {
shifting: typeof shiftOffset === 'number',
})}
x={0}
y={innerStage.height}
width={innerStage.width}
height={CHART_MARGIN.bottom}
/>
<ChartAxis
className="axis-y"
axis={axisLeft(statScale)
.ticks(3)
.tickFormat(e => formatTickRate(e.valueOf()))}
/>
<g className="bar-group">
{bubbleInfo && (
<rect
className="hover-highlight"
{...startEndToXWidth(bubbleInfo)}
y={0}
height={innerStage.height}
/>
)}
{0 < nowX && nowX < innerStage.width && (
<line className="now-line" x1={nowX} x2={nowX} y1={0} y2={innerStage.height + 8} />
)}
{intervalBars.map((intervalBar, i) => {
return (
<rect
key={i}
className={classNames('bar-unit', { realtime: intervalBar.realtime })}
{...segmentBarToRect(intervalBar)}
fill={getDatasourceColor(intervalBar.datasource)}
/>
);
})}
{bubbleInfo?.intervalBars.length === 1 &&
bubbleInfo.intervalBars.map((intervalBar, i) => (
<rect key={i} className="hovered-bar" {...segmentBarToRect(intervalBar)} />
))}
{selection && (
<rect
className={classNames('selection', { done: selection.done })}
{...startEndToXWidth(selection)}
y={0}
height={innerStage.height}
/>
)}
{!!shiftOffset && (
<rect
className="shifter"
x={shiftOffset > 0 ? timeScale(dateRange[1]) : 0}
y={0}
height={innerStage.height}
width={
shiftOffset > 0
? innerStage.width - timeScale(dateRange[1])
: timeScale(dateRange[0])
}
/>
)}
</g>
</g>
</svg>
{(datasourceRules || datasourceRulesError) && (
<div className="rule-tape" style={{ left: CHART_MARGIN.left, right: CHART_MARGIN.right }}>
{datasourceRules?.defaultLoadRules.map((rule, index) =>
renderLoadRule(rule, index, true),
)}
{datasourceRules?.loadRules.map((rule, index) => renderLoadRule(rule, index, false))}
{datasourceRulesError && (
<div className="rule-error">Rule loading error: {datasourceRulesError}</div>
)}
</div>
)}
{!intervalRows.length && (
<div className="empty-placeholder">
<div className="no-data-text">There are no segments in the selected range</div>
</div>
)}
<PortalBubble
className="segment-bar-chart-bubble"
openOn={hoveredOpenOn}
onClose={selection?.done ? () => setSelection(undefined) : undefined}
mute
direction="up"
/>
</div>
);
};

View File

@ -1,162 +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 { C, F, L, N, sql, SqlExpression, SqlQuery } from 'druid-query-toolkit';
import { useMemo } from 'react';
import { END_OF_TIME_DATE, type Rule, RuleUtil, START_OF_TIME_DATE } from '../../druid-models';
import type { Capabilities } from '../../helpers';
import { useQueryManager } from '../../hooks';
import { Api } from '../../singletons';
import { Duration, filterMap, getApiArray, queryDruidSql, TZ_UTC } from '../../utils';
import { Loader } from '../loader/loader';
import type { IntervalRow } from './common';
import type { SegmentBarChartRenderProps } from './segment-bar-chart-render';
import { SegmentBarChartRender } from './segment-bar-chart-render';
import './segment-bar-chart.scss';
export interface SegmentBarChartProps
extends Omit<
SegmentBarChartRenderProps,
'intervalRows' | 'datasourceRules' | 'datasourceRulesError'
> {
capabilities: Capabilities;
}
export const SegmentBarChart = function SegmentBarChart(props: SegmentBarChartProps) {
const { capabilities, dateRange, shownDatasource, ...otherProps } = props;
const intervalsQuery = useMemo(
() => ({ capabilities, dateRange, shownDatasource: shownDatasource }),
[capabilities, dateRange, shownDatasource],
);
const [intervalRowsState] = useQueryManager({
query: intervalsQuery,
processQuery: async ({ capabilities, dateRange, shownDatasource }, cancelToken) => {
if (capabilities.hasSql()) {
const query = SqlQuery.from(N('sys').table('segments'))
.changeWhereExpression(
SqlExpression.and(
sql`"start" <= '${dateRange[1].toISOString()}' AND '${dateRange[0].toISOString()}' < "end"`,
C('start').unequal(START_OF_TIME_DATE),
C('end').unequal(END_OF_TIME_DATE),
C('is_overshadowed').equal(0),
shownDatasource ? C('datasource').equal(L(shownDatasource)) : undefined,
),
)
.addSelect(C('start'), { addToGroupBy: 'end' })
.addSelect(C('end'), { addToGroupBy: 'end' })
.addSelect(C('datasource'), { addToGroupBy: 'end' })
.addSelect(C('is_realtime').as('realtime'), { addToGroupBy: 'end' })
.addSelect(F.count().as('segments'))
.addSelect(F.sum(C('size')).as('size'))
.addSelect(F.sum(C('num_rows')).as('rows'))
.toString();
return (await queryDruidSql({ query }, cancelToken)).map(sr => {
const start = new Date(sr.start);
const end = new Date(sr.end);
return {
...sr,
start,
end,
realtime: Boolean(sr.realtime),
originalTimeSpan: Duration.fromRange(start, end, TZ_UTC),
} as IntervalRow;
});
} else {
return filterMap(
await getApiArray(
`/druid/coordinator/v1/metadata/segments?includeOvershadowedStatus&includeRealtimeSegments&${
shownDatasource ? `datasources=${Api.encodePath(shownDatasource)}` : ''
}`,
cancelToken,
),
(segment: any) => {
if (segment.overshadowed) return; // We have to include overshadowed segments to get the realtime segments in this API
const [startStr, endStr] = segment.interval.split('/');
if (startStr === START_OF_TIME_DATE && endStr === END_OF_TIME_DATE) return;
const start = new Date(startStr);
const end = new Date(endStr);
if (!(start <= dateRange[1] && dateRange[0] < end)) return;
return {
start,
end,
datasource: segment.dataSource,
realtime: Boolean(segment.realtime),
originalTimeSpan: Duration.fromRange(start, end, TZ_UTC),
segments: 1,
size: segment.size,
rows: segment.num_rows || 0, // segment.num_rows is really null on this API :-(
} as IntervalRow;
},
);
}
},
});
const [allLoadRulesState] = useQueryManager({
query: shownDatasource ? '' : undefined,
processQuery: async (_, cancelToken) => {
return (
await Api.instance.get<Record<string, Rule[]>>('/druid/coordinator/v1/rules', {
cancelToken,
})
).data;
},
});
const datasourceRules = useMemo(() => {
const allLoadRules = allLoadRulesState.data;
if (!allLoadRules || !shownDatasource) return;
return {
loadRules: (allLoadRules[shownDatasource] || []).toReversed(),
defaultLoadRules: (allLoadRules[RuleUtil.DEFAULT_RULES_KEY] || []).toReversed(),
};
}, [allLoadRulesState.data, shownDatasource]);
if (intervalRowsState.error) {
return (
<div className="empty-placeholder">
<span className="error-text">{`Error when loading data: ${intervalRowsState.getErrorMessage()}`}</span>
</div>
);
}
const intervalRows = intervalRowsState.getSomeData();
return (
<>
{intervalRows && (
<SegmentBarChartRender
intervalRows={intervalRows}
datasourceRules={datasourceRules}
datasourceRulesError={allLoadRulesState.getErrorMessage()}
dateRange={dateRange}
shownDatasource={shownDatasource}
{...otherProps}
/>
)}
{intervalRowsState.loading && <Loader />}
</>
);
};

View File

@ -16,20 +16,12 @@
* limitations under the License.
*/
@import '../../variables';
.segment-timeline {
.control-bar {
@include card-like;
height: 34px;
display: flex;
align-items: start;
padding: 5px;
gap: 10px;
display: grid;
grid-template-columns: 1fr 220px;
& > .expander {
flex: 1;
}
.loader {
width: 85%;
}
.loading-error {
@ -39,16 +31,14 @@
transform: translate(-50%, -50%);
}
.chart-container {
.no-data-text {
position: absolute;
top: 34px;
width: 100%;
bottom: 0;
overflow: hidden;
left: 30vw;
top: 15vh;
font-size: 20px;
}
.segment-bar-chart,
.segment-bar-chart-render {
@include pin-full;
}
.side-control {
padding-top: 20px;
}
}

View File

@ -16,42 +16,40 @@
* limitations under the License.
*/
import { sane } from '@druid-toolkit/query';
import { render } from '@testing-library/react';
import { Capabilities } from '../../helpers';
import { QueryState } from '../../utils';
import { SegmentTimeline } from './segment-timeline';
jest.useFakeTimers('modern').setSystemTime(Date.parse('2024-11-19T12:34:56Z'));
jest.mock('../../hooks', () => {
return {
useQueryManager: (options: any) => {
if (options.initQuery instanceof Capabilities) {
// This is a query for data sources
return [new QueryState({ data: ['ds1', 'ds2'] })];
}
if (options.query === null) {
// This is a query for the data source time range
return [
new QueryState({
data: [new Date('2024-11-01 00:00:00Z'), new Date('2024-11-18 00:00:00Z')],
}),
];
}
return new QueryState({ error: new Error('not covered') });
},
};
});
jest.useFakeTimers('modern').setSystemTime(Date.parse('2021-06-08T12:34:56Z'));
describe('SegmentTimeline', () => {
it('.getSqlQuery', () => {
expect(
SegmentTimeline.getSqlQuery([
new Date('2020-01-01T00:00:00Z'),
new Date('2021-02-01T00:00:00Z'),
]),
).toEqual(sane`
SELECT
"start", "end", "datasource",
COUNT(*) AS "count",
SUM("size") AS "size"
FROM sys.segments
WHERE
'2020-01-01T00:00:00.000Z' <= "start" AND
"end" <= '2021-02-01T00:00:00.000Z' AND
is_published = 1 AND
is_overshadowed = 0
GROUP BY 1, 2, 3
ORDER BY "start" DESC
`);
});
it('matches snapshot', () => {
const segmentTimeline = (
<SegmentTimeline capabilities={Capabilities.FULL} datasource={undefined} />
);
const segmentTimeline = <SegmentTimeline capabilities={Capabilities.FULL} />;
const { container } = render(segmentTimeline);
expect(container.firstChild).toMatchSnapshot();
});

View File

@ -16,361 +16,628 @@
* limitations under the License.
*/
import {
Button,
ButtonGroup,
Intent,
Menu,
MenuItem,
Popover,
Position,
ResizeSensor,
} from '@blueprintjs/core';
import type { NonNullDateRange } from '@blueprintjs/datetime';
import { DateRangePicker3 } from '@blueprintjs/datetime2';
import { Button, FormGroup, MenuItem, ResizeSensor, SegmentedControl } from '@blueprintjs/core';
import type { DateRange, NonNullDateRange } from '@blueprintjs/datetime';
import { DateRangeInput3 } from '@blueprintjs/datetime2';
import { IconNames } from '@blueprintjs/icons';
import type { ItemPredicate, ItemRenderer } from '@blueprintjs/select';
import { Select } from '@blueprintjs/select';
import { C, L, N, SqlExpression, SqlQuery } from 'druid-query-toolkit';
import { useEffect, useMemo, useState } from 'react';
import type { AxisScale } from 'd3-axis';
import { scaleLinear, scaleUtc } from 'd3-scale';
import enUS from 'date-fns/locale/en-US';
import React from 'react';
import { END_OF_TIME_DATE, START_OF_TIME_DATE } from '../../druid-models';
import type { Capabilities } from '../../helpers';
import { useQueryManager } from '../../hooks';
import { Api } from '../../singletons';
import {
checkedCircleIcon,
day,
Duration,
getApiArray,
ceilToUtcDay,
formatBytes,
formatInteger,
isNonNullRange,
localToUtcDateRange,
maxDate,
queryDruidSql,
TZ_UTC,
QueryManager,
uniq,
utcToLocalDateRange,
} from '../../utils';
import { Stage } from '../../utils/stage';
import { Loader } from '../loader/loader';
import type { IntervalStat } from './common';
import { formatIsoDateOnly, getIntervalStatTitle, INTERVAL_STATS } from './common';
import type { SegmentBarChartProps } from './segment-bar-chart';
import { SegmentBarChart } from './segment-bar-chart';
import type { BarUnitData } from './stacked-bar-chart';
import { StackedBarChart } from './stacked-bar-chart';
import './segment-timeline.scss';
const DEFAULT_SHOWN_DURATION = new Duration('P1Y');
const SHOWN_DURATION_OPTIONS: Duration[] = [
new Duration('P1D'),
new Duration('P1W'),
new Duration('P1M'),
new Duration('P3M'),
new Duration('P1Y'),
new Duration('P5Y'),
new Duration('P10Y'),
];
function getDateRange(shownDuration: Duration): NonNullDateRange {
const end = day.ceil(new Date(), TZ_UTC);
return [shownDuration.shift(end, TZ_UTC, -1), end];
}
function formatDateRange(dateRange: NonNullDateRange): string {
return `${formatIsoDateOnly(dateRange[0])}${formatIsoDateOnly(dateRange[1])}`;
}
function dateRangesEqual(dr1: NonNullDateRange, dr2: NonNullDateRange): boolean {
return dr1[0].valueOf() === dr2[0].valueOf() && dr2[1].valueOf() === dr2[1].valueOf();
}
interface SegmentTimelineProps extends Pick<SegmentBarChartProps, 'getIntervalActionButton'> {
interface SegmentTimelineProps {
capabilities: Capabilities;
datasource: string | undefined;
}
export const SegmentTimeline = function SegmentTimeline(props: SegmentTimelineProps) {
const { capabilities, datasource, ...otherProps } = props;
const [stage, setStage] = useState<Stage | undefined>();
const [activeSegmentStat, setActiveSegmentStat] = useState<IntervalStat>('size');
const [shownDatasource, setShownDatasource] = useState<string | undefined>(datasource);
const [dateRange, setDateRange] = useState<NonNullDateRange | undefined>();
const [showCustomDatePicker, setShowCustomDatePicker] = useState(false);
type ActiveDataType = 'sizeData' | 'countData';
useEffect(() => {
setShownDatasource(datasource);
}, [datasource]);
interface SegmentTimelineState {
chartHeight: number;
chartWidth: number;
data?: Record<string, any>;
datasources: string[];
stackedData?: Record<string, BarUnitData[]>;
singleDatasourceData?: Record<string, Record<string, BarUnitData[]>>;
activeDatasource: string | null;
activeDataType: ActiveDataType;
dataToRender: BarUnitData[];
loading: boolean;
error?: Error;
xScale: AxisScale<Date> | null;
yScale: AxisScale<number> | null;
dateRange: NonNullDateRange;
selectedDateRange?: DateRange;
}
const defaultDateRange = useMemo(() => {
return getDateRange(DEFAULT_SHOWN_DURATION);
}, []);
interface BarChartScales {
xScale: AxisScale<Date>;
yScale: AxisScale<number>;
}
const [datasourcesState] = useQueryManager<Capabilities, string[]>({
initQuery: capabilities,
processQuery: async (capabilities, cancelToken) => {
if (capabilities.hasSql()) {
const tables = await queryDruidSql<{ TABLE_NAME: string }>(
{
query: `SELECT TABLE_NAME FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_TYPE = 'TABLE'`,
},
cancelToken,
);
interface IntervalRow {
start: string;
end: string;
datasource: string;
count: number;
size: number;
}
return tables.map(d => d.TABLE_NAME);
} else {
return await getApiArray(`/druid/coordinator/v1/datasources`, cancelToken);
}
},
});
const DEFAULT_TIME_SPAN_MONTHS = 3;
const [initDatasourceDateRangeState] = useQueryManager<string | null, NonNullDateRange>({
query: dateRange ? undefined : shownDatasource ?? null,
processQuery: async (datasource, cancelToken) => {
let queriedStart: Date;
let queriedEnd: Date;
if (capabilities.hasSql()) {
const baseQuery = SqlQuery.from(N('sys').table('segments'))
.changeWhereExpression(
SqlExpression.and(
C('start').unequal(START_OF_TIME_DATE),
C('end').unequal(END_OF_TIME_DATE),
C('is_overshadowed').equal(0),
datasource ? C('datasource').equal(L(datasource)) : undefined,
),
)
.changeLimitValue(1);
function getDefaultDateRange(): NonNullDateRange {
const start = ceilToUtcDay(new Date());
const end = new Date(start.valueOf());
start.setUTCMonth(start.getUTCMonth() - DEFAULT_TIME_SPAN_MONTHS);
return [start, end];
}
const endQuery = baseQuery
.addSelect(C('end'), { addToOrderBy: 'end', direction: 'DESC' })
.toString();
export class SegmentTimeline extends React.PureComponent<
SegmentTimelineProps,
SegmentTimelineState
> {
static COLORS = [
'#b33040',
'#d25c4d',
'#f2b447',
'#d9d574',
'#4FAA7E',
'#57ceff',
'#789113',
'#098777',
'#b33040',
'#d2757b',
'#f29063',
'#d9a241',
'#80aa61',
'#c4ff9e',
'#915412',
'#87606c',
];
const endRes = await queryDruidSql<{ end: string }>({ query: endQuery }, cancelToken).catch(
() => [],
);
if (endRes.length !== 1) {
return getDateRange(DEFAULT_SHOWN_DURATION);
}
queriedEnd = day.ceil(new Date(endRes[0].end), TZ_UTC);
const startQuery = baseQuery
.addSelect(C('start'), { addToOrderBy: 'end', direction: 'ASC' })
.toString();
const startRes = await queryDruidSql<{ start: string }>(
{ query: startQuery },
cancelToken,
).catch(() => []);
if (startRes.length !== 1) {
return [DEFAULT_SHOWN_DURATION.shift(queriedEnd, TZ_UTC, -1), queriedEnd]; // Should not really get here
}
queriedStart = day.floor(new Date(startRes[0].start), TZ_UTC);
} else {
// Don't bother querying if there is no SQL
return getDateRange(DEFAULT_SHOWN_DURATION);
}
return [
maxDate(queriedStart, DEFAULT_SHOWN_DURATION.shift(queriedEnd, TZ_UTC, -1)),
queriedEnd,
];
},
});
const effectiveDateRange =
dateRange ||
initDatasourceDateRangeState.data ||
(initDatasourceDateRangeState.isLoading() ? undefined : defaultDateRange);
let previousDateRange: NonNullDateRange | undefined;
let zoomedOutDateRange: NonNullDateRange | undefined;
let nextDateRange: NonNullDateRange | undefined;
if (effectiveDateRange) {
const d = Duration.fromRange(effectiveDateRange[0], effectiveDateRange[1], TZ_UTC);
const shiftStartBack = d.shift(effectiveDateRange[0], TZ_UTC, -1);
const shiftEndForward = d.shift(effectiveDateRange[1], TZ_UTC);
const now = day.ceil(new Date(), TZ_UTC);
previousDateRange = [shiftStartBack, effectiveDateRange[0]];
zoomedOutDateRange = [shiftStartBack, shiftEndForward < now ? shiftEndForward : now];
nextDateRange = [effectiveDateRange[1], shiftEndForward];
static getColor(index: number): string {
return SegmentTimeline.COLORS[index % SegmentTimeline.COLORS.length];
}
return (
<div className="segment-timeline">
<div className="control-bar">
<ButtonGroup>
<Select<string>
items={datasourcesState.data || []}
disabled={datasourcesState.isError()}
onItemSelect={setShownDatasource}
itemRenderer={(val, { handleClick, handleFocus, modifiers }) => {
if (!modifiers.matchesPredicate) return null;
return (
<MenuItem
key={val}
disabled={modifiers.disabled}
active={modifiers.active}
onClick={handleClick}
onFocus={handleFocus}
roleStructure="listoption"
text={val}
/>
);
}}
noResults={<MenuItem disabled text="No results" roleStructure="listoption" />}
itemPredicate={(query, val, _index, exactMatch) => {
const normalizedTitle = val.toLowerCase();
const normalizedQuery = query.toLowerCase();
static getSqlQuery(dateRange: NonNullDateRange): string {
return `SELECT
"start", "end", "datasource",
COUNT(*) AS "count",
SUM("size") AS "size"
FROM sys.segments
WHERE
'${dateRange[0].toISOString()}' <= "start" AND
"end" <= '${dateRange[1].toISOString()}' AND
is_published = 1 AND
is_overshadowed = 0
GROUP BY 1, 2, 3
ORDER BY "start" DESC`;
}
if (exactMatch) {
return normalizedTitle === normalizedQuery;
} else {
return normalizedTitle.includes(normalizedQuery);
}
}}
>
<Button
text={`Datasource: ${shownDatasource ?? 'all'}`}
small
rightIcon={IconNames.CARET_DOWN}
intent={datasourcesState.isError() ? Intent.WARNING : undefined}
data-tooltip={
datasourcesState.isError()
? `Error: ${datasourcesState.getErrorMessage()}`
: undefined
}
/>
</Select>
{shownDatasource && (
<Button icon={IconNames.CROSS} small onClick={() => setShownDatasource(undefined)} />
)}
</ButtonGroup>
<Popover
position={Position.BOTTOM_LEFT}
content={
<Menu>
{INTERVAL_STATS.map(stat => (
<MenuItem
key={stat}
icon={checkedCircleIcon(stat === activeSegmentStat)}
text={getIntervalStatTitle(stat)}
onClick={() => setActiveSegmentStat(stat)}
/>
))}
</Menu>
static processRawData(data: IntervalRow[]) {
if (data === null) return [];
const countData: Record<string, any> = {};
const sizeData: Record<string, any> = {};
data.forEach(entry => {
const start = entry.start;
const day = start.split('T')[0];
const datasource = entry.datasource;
const count = entry.count;
const segmentSize = entry.size;
if (countData[day] === undefined) {
countData[day] = {
day,
[datasource]: count,
total: count,
};
sizeData[day] = {
day,
[datasource]: segmentSize,
total: segmentSize,
};
} else {
const countDataEntry: number | undefined = countData[day][datasource];
countData[day][datasource] = count + (countDataEntry === undefined ? 0 : countDataEntry);
const sizeDataEntry: number | undefined = sizeData[day][datasource];
sizeData[day][datasource] = segmentSize + (sizeDataEntry === undefined ? 0 : sizeDataEntry);
countData[day].total += count;
sizeData[day].total += segmentSize;
}
});
const countDataArray = Object.keys(countData)
.reverse()
.map((time: any) => {
return countData[time];
});
const sizeDataArray = Object.keys(sizeData)
.reverse()
.map((time: any) => {
return sizeData[time];
});
return { countData: countDataArray, sizeData: sizeDataArray };
}
static calculateStackedData(
data: Record<string, any>,
datasources: string[],
): Record<string, BarUnitData[]> {
const newStackedData: Record<string, BarUnitData[]> = {};
Object.keys(data).forEach((type: any) => {
const stackedData: any = data[type].map((d: any) => {
let y0 = 0;
return datasources.map((datasource: string, i) => {
const barUnitData = {
x: d.day,
y: d[datasource] === undefined ? 0 : d[datasource],
y0,
datasource,
color: SegmentTimeline.getColor(i),
dailySize: d.total,
};
y0 += d[datasource] === undefined ? 0 : d[datasource];
return barUnitData;
});
});
newStackedData[type] = stackedData.flat();
});
return newStackedData;
}
static calculateSingleDatasourceData(
data: Record<string, any>,
datasources: string[],
): Record<string, Record<string, BarUnitData[]>> {
const singleDatasourceData: Record<string, Record<string, BarUnitData[]>> = {};
Object.keys(data).forEach(dataType => {
singleDatasourceData[dataType] = {};
datasources.forEach((datasource, i) => {
const currentData = data[dataType];
if (currentData.length === 0) return;
const dataResult = currentData.map((d: any) => {
let y = 0;
if (d[datasource] !== undefined) {
y = d[datasource];
}
return {
x: d.day,
y,
datasource,
color: SegmentTimeline.getColor(i),
dailySize: d.total,
};
});
if (!dataResult.every((d: any) => d.y === 0)) {
singleDatasourceData[dataType][datasource] = dataResult;
}
});
});
return singleDatasourceData;
}
private readonly dataQueryManager: QueryManager<
{ capabilities: Capabilities; dateRange: NonNullDateRange },
any
>;
private readonly chartMargin = { top: 40, right: 15, bottom: 20, left: 60 };
constructor(props: SegmentTimelineProps) {
super(props);
const dateRange = getDefaultDateRange();
this.state = {
chartWidth: 1, // Dummy init values to be replaced
chartHeight: 1, // after first render
data: {},
datasources: [],
stackedData: {},
singleDatasourceData: {},
dataToRender: [],
activeDatasource: null,
activeDataType: 'sizeData',
loading: true,
xScale: null,
yScale: null,
dateRange,
};
this.dataQueryManager = new QueryManager({
processQuery: async ({ capabilities, dateRange }, cancelToken) => {
let intervals: IntervalRow[];
let datasources: string[];
if (capabilities.hasSql()) {
intervals = await queryDruidSql(
{
query: SegmentTimeline.getSqlQuery(dateRange),
},
cancelToken,
);
datasources = uniq(intervals.map(r => r.datasource).sort());
} else if (capabilities.hasCoordinatorAccess()) {
const startIso = dateRange[0].toISOString();
datasources = (
await Api.instance.get(`/druid/coordinator/v1/datasources`, { cancelToken })
).data;
intervals = (
await Promise.all(
datasources.map(async datasource => {
const intervalMap = (
await Api.instance.get(
`/druid/coordinator/v1/datasources/${Api.encodePath(
datasource,
)}/intervals?simple`,
{ cancelToken },
)
).data;
return Object.keys(intervalMap)
.map(interval => {
const [start, end] = interval.split('/');
const { count, size } = intervalMap[interval];
return {
start,
end,
datasource,
count,
size,
};
})
.filter(a => startIso < a.start);
}),
)
)
.flat()
.sort((a, b) => b.start.localeCompare(a.start));
} else {
throw new Error(`must have SQL or coordinator access`);
}
const data = SegmentTimeline.processRawData(intervals);
const stackedData = SegmentTimeline.calculateStackedData(data, datasources);
const singleDatasourceData = SegmentTimeline.calculateSingleDatasourceData(
data,
datasources,
);
return { data, datasources, stackedData, singleDatasourceData };
},
onStateChange: ({ data, loading, error }) => {
this.setState({
data: data ? data.data : undefined,
datasources: data ? data.datasources : [],
stackedData: data ? data.stackedData : undefined,
singleDatasourceData: data ? data.singleDatasourceData : undefined,
loading,
error,
});
},
});
}
componentDidMount(): void {
const { capabilities } = this.props;
const { dateRange } = this.state;
if (isNonNullRange(dateRange)) {
this.dataQueryManager.runQuery({ capabilities, dateRange });
}
}
componentWillUnmount(): void {
this.dataQueryManager.terminate();
}
componentDidUpdate(_prevProps: SegmentTimelineProps, prevState: SegmentTimelineState): void {
const { activeDatasource, activeDataType, singleDatasourceData, stackedData } = this.state;
if (
prevState.data !== this.state.data ||
prevState.activeDataType !== this.state.activeDataType ||
prevState.activeDatasource !== this.state.activeDatasource ||
prevState.chartWidth !== this.state.chartWidth ||
prevState.chartHeight !== this.state.chartHeight
) {
const scales: BarChartScales | undefined = this.calculateScales();
const dataToRender: BarUnitData[] | undefined = activeDatasource
? singleDatasourceData
? singleDatasourceData[activeDataType][activeDatasource]
: undefined
: stackedData
? stackedData[activeDataType]
: undefined;
if (scales && dataToRender) {
this.setState({
dataToRender,
xScale: scales.xScale,
yScale: scales.yScale,
});
}
}
}
private calculateScales(): BarChartScales | undefined {
const {
chartWidth,
chartHeight,
data,
activeDataType,
activeDatasource,
singleDatasourceData,
dateRange,
} = this.state;
if (!data || !Object.keys(data).length || !isNonNullRange(dateRange)) return;
const activeData = data[activeDataType];
let yDomain: number[] = [
0,
activeData.length === 0
? 0
: activeData.reduce((max: any, d: any) => (max.total > d.total ? max : d)).total,
];
if (
activeDatasource !== null &&
singleDatasourceData![activeDataType][activeDatasource] !== undefined
) {
yDomain = [
0,
singleDatasourceData![activeDataType][activeDatasource].reduce((max: any, d: any) =>
max.y > d.y ? max : d,
).y,
];
}
const xScale: AxisScale<Date> = scaleUtc()
.domain(dateRange)
.range([0, chartWidth - this.chartMargin.left - this.chartMargin.right]);
const yScale: AxisScale<number> = scaleLinear()
.rangeRound([chartHeight - this.chartMargin.top - this.chartMargin.bottom, 0])
.domain(yDomain);
return {
xScale,
yScale,
};
}
private readonly formatTick = (n: number) => {
if (isNaN(n)) return '';
const { activeDataType } = this.state;
if (activeDataType === 'countData') {
return formatInteger(n);
} else {
return formatBytes(n);
}
};
private readonly handleResize = (entries: ResizeObserverEntry[]) => {
const chartRect = entries[0].contentRect;
this.setState({
chartWidth: chartRect.width,
chartHeight: chartRect.height,
});
};
renderStackedBarChart() {
const {
chartWidth,
chartHeight,
loading,
dataToRender,
activeDataType,
error,
xScale,
yScale,
data,
activeDatasource,
dateRange,
} = this.state;
if (loading) {
return (
<div>
<Loader loading={loading} />
</div>
);
}
if (error) {
return (
<div>
<span className="no-data-text">Error when loading data: {error.message}</span>
</div>
);
}
if (xScale === null || yScale === null) {
return (
<div>
<span className="no-data-text">Error when calculating scales</span>
</div>
);
}
if (data![activeDataType].length === 0) {
return (
<div>
<span className="no-data-text">There are no segments for the selected interval</span>
</div>
);
}
if (
activeDatasource !== null &&
data![activeDataType].every((d: any) => d[activeDatasource] === undefined)
) {
return (
<div>
<span className="no-data-text">
No data available for <i>{activeDatasource}</i>
</span>
</div>
);
}
const millisecondsPerDay = 24 * 60 * 60 * 1000;
const barCounts = (dateRange[1].getTime() - dateRange[0].getTime()) / millisecondsPerDay;
const barWidth = Math.max(
0,
(chartWidth - this.chartMargin.left - this.chartMargin.right) / barCounts,
);
return (
<ResizeSensor onResize={this.handleResize}>
<StackedBarChart
dataToRender={dataToRender}
svgHeight={chartHeight}
svgWidth={chartWidth}
margin={this.chartMargin}
changeActiveDatasource={(datasource: string | null) =>
this.setState(prevState => ({
activeDatasource: prevState.activeDatasource ? null : datasource,
}))
}
activeDataType={activeDataType}
formatTick={(n: number) => this.formatTick(n)}
xScale={xScale}
yScale={yScale}
barWidth={barWidth}
/>
</ResizeSensor>
);
}
render() {
const { capabilities } = this.props;
const { datasources, activeDataType, activeDatasource, dateRange, selectedDateRange } =
this.state;
const filterDatasource: ItemPredicate<string> = (query, val, _index, exactMatch) => {
const normalizedTitle = val.toLowerCase();
const normalizedQuery = query.toLowerCase();
if (exactMatch) {
return normalizedTitle === normalizedQuery;
} else {
return ` ${normalizedTitle}`.includes(normalizedQuery);
}
};
const datasourceRenderer: ItemRenderer<string> = (
val,
{ handleClick, handleFocus, modifiers },
) => {
if (!modifiers.matchesPredicate) {
return null;
}
return (
<MenuItem
key={val}
disabled={modifiers.disabled}
active={modifiers.active}
onClick={handleClick}
onFocus={handleFocus}
roleStructure="listoption"
text={val}
/>
);
};
const DatasourceSelect: React.FC = () => {
const showAll = 'Show all';
const handleItemSelected = (selectedItem: string) => {
this.setState({
activeDatasource: selectedItem === showAll ? null : selectedItem,
});
};
const datasourcesWzAll = [showAll].concat(datasources);
return (
<Select<string>
items={datasourcesWzAll}
onItemSelect={handleItemSelected}
itemRenderer={datasourceRenderer}
noResults={<MenuItem disabled text="No results." roleStructure="listoption" />}
itemPredicate={filterDatasource}
fill
>
<Button
text={`Show: ${getIntervalStatTitle(activeSegmentStat)}`}
small
text={activeDatasource === null ? showAll : activeDatasource}
fill
rightIcon={IconNames.CARET_DOWN}
/>
</Popover>
<div className="expander" />
<ButtonGroup>
<Button
icon={IconNames.CARET_LEFT}
data-tooltip={
previousDateRange && `Previous time period\n${formatDateRange(previousDateRange)}`
}
small
disabled={!previousDateRange}
onClick={() => setDateRange(previousDateRange)}
/>
<Button
icon={IconNames.ZOOM_OUT}
data-tooltip={zoomedOutDateRange && `Zoom out\n${formatDateRange(zoomedOutDateRange)}`}
small
disabled={!zoomedOutDateRange}
onClick={() => setDateRange(zoomedOutDateRange)}
/>
<Button
icon={IconNames.CARET_RIGHT}
data-tooltip={nextDateRange && `Next time period\n${formatDateRange(nextDateRange)}`}
small
disabled={!nextDateRange}
onClick={() => setDateRange(nextDateRange)}
/>
</ButtonGroup>
<ButtonGroup>
{SHOWN_DURATION_OPTIONS.map((d, i) => {
const dr = getDateRange(d);
return (
<Button
key={i}
text={d.toString().replace('P', '')}
data-tooltip={`Show last ${d.getDescription()}\n${formatDateRange(dr)}`}
active={effectiveDateRange && dateRangesEqual(effectiveDateRange, dr)}
small
onClick={() => setDateRange(dr)}
/>
);
})}
<Popover
isOpen={showCustomDatePicker}
onInteraction={setShowCustomDatePicker}
content={
<DateRangePicker3
defaultValue={utcToLocalDateRange(
effectiveDateRange || getDateRange(DEFAULT_SHOWN_DURATION),
)}
onChange={newDateRange => {
const newUtcDateRange = localToUtcDateRange(newDateRange);
if (!isNonNullRange(newUtcDateRange)) return;
setDateRange(newUtcDateRange);
setShowCustomDatePicker(false);
}}
contiguousCalendarMonths={false}
reverseMonthAndYearMenus
timePickerProps={undefined}
shortcuts={false}
/>
}
>
<Button
icon={IconNames.CALENDAR}
text={
effectiveDateRange
? formatDateRange(effectiveDateRange)
: `Loading datasource date range`
</Select>
);
};
return (
<div className="segment-timeline">
{this.renderStackedBarChart()}
<div className="side-control">
<FormGroup label="Show">
<SegmentedControl
value={activeDataType}
onValueChange={activeDataType =>
this.setState({ activeDataType: activeDataType as ActiveDataType })
}
data-tooltip={showCustomDatePicker ? undefined : `Select a custom date range`}
small
options={[
{
label: 'Total size',
value: 'sizeData',
},
{
label: 'Segment count',
value: 'countData',
},
]}
fill
/>
</Popover>
<Button
icon={IconNames.PIN}
data-tooltip={
dateRange ? 'Pin the date range' : 'Auto determine date range to fit datasource'
}
active={Boolean(dateRange)}
disabled={!effectiveDateRange}
small
onClick={() => setDateRange(dateRange ? undefined : effectiveDateRange)}
/>
</ButtonGroup>
</div>
<ResizeSensor
onResize={(entries: ResizeObserverEntry[]) => {
const rect = entries[0].contentRect;
setStage(new Stage(rect.width, rect.height));
}}
>
<div className="chart-container">
{stage && effectiveDateRange && (
<SegmentBarChart
capabilities={capabilities}
stage={stage}
dateRange={effectiveDateRange}
changeDateRange={setDateRange}
shownIntervalStat={activeSegmentStat}
shownDatasource={shownDatasource}
changeShownDatasource={setShownDatasource}
{...otherProps}
</FormGroup>
<FormGroup label="Interval">
<DateRangeInput3
value={utcToLocalDateRange(selectedDateRange || dateRange)}
onChange={newDateRange => {
const newUtcDateRange = localToUtcDateRange(newDateRange);
if (!isNonNullRange(newUtcDateRange)) return;
this.setState({ dateRange: newUtcDateRange, selectedDateRange: undefined }, () => {
this.dataQueryManager.runQuery({ capabilities, dateRange: newUtcDateRange });
});
}}
fill
locale={enUS}
/>
)}
{initDatasourceDateRangeState.isLoading() && <Loader />}
</FormGroup>
<FormGroup label="Datasource">
<DatasourceSelect />
</FormGroup>
</div>
</ResizeSensor>
</div>
);
};
</div>
);
}
}

View File

@ -16,32 +16,35 @@
* limitations under the License.
*/
import { hashJoaat } from '../../utils';
.stacked-bar-chart {
position: relative;
overflow: hidden;
const COLORS = [
'#1f77b4',
'#aec7e8',
'#ff7f0e',
'#ffbb78',
'#2ca02c',
'#98df8a',
'#d62728',
'#ff9896',
'#9467bd',
'#c5b0d5',
'#8c564b',
'#c49c94',
'#e377c2',
'#f7b6d2',
'#7f7f7f',
'#c7c7c7',
'#bcbd22',
'#dbdb8d',
'#17becf',
'#9edae5',
];
.bar-chart-tooltip {
position: absolute;
left: 100px;
right: 0;
export function getDatasourceColor(datasource: string) {
const hash = hashJoaat(datasource);
return COLORS[hash % COLORS.length];
div {
display: inline-block;
width: 230px;
}
}
svg {
position: absolute;
.hovered-bar {
fill: transparent;
stroke: #ffffff;
stroke-width: 1.5px;
}
.gridline-x {
line {
stroke-dasharray: 5, 5;
opacity: 0.5;
}
}
}
}

View File

@ -0,0 +1,174 @@
/*
* 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 type { AxisScale } from 'd3-axis';
import { axisBottom, axisLeft } from 'd3-axis';
import React, { useState } from 'react';
import { BarGroup } from './bar-group';
import { ChartAxis } from './chart-axis';
import './stacked-bar-chart.scss';
export interface BarUnitData {
x: number;
y: number;
y0?: number;
width: number;
datasource: string;
color: string;
dailySize?: number;
}
export interface BarChartMargin {
top: number;
right: number;
bottom: number;
left: number;
}
export interface HoveredBarInfo {
xCoordinate?: number;
yCoordinate?: number;
height?: number;
width?: number;
datasource?: string;
xValue?: number;
yValue?: number;
dailySize?: number;
}
interface StackedBarChartProps {
svgWidth: number;
svgHeight: number;
margin: BarChartMargin;
activeDataType?: string;
dataToRender: BarUnitData[];
changeActiveDatasource: (e: string | null) => void;
formatTick: (e: number) => string;
xScale: AxisScale<Date>;
yScale: AxisScale<number>;
barWidth: number;
}
export const StackedBarChart = React.forwardRef(function StackedBarChart(
props: StackedBarChartProps,
ref,
) {
const {
activeDataType,
svgWidth,
svgHeight,
margin,
formatTick,
xScale,
yScale,
dataToRender,
changeActiveDatasource,
barWidth,
} = props;
const [hoverOn, setHoverOn] = useState<HoveredBarInfo>();
const width = svgWidth - margin.left - margin.right;
const height = svgHeight - margin.top - margin.bottom;
function renderBarChart() {
return (
<svg
width={svgWidth}
height={svgHeight}
viewBox={`0 0 ${svgWidth} ${svgHeight}`}
preserveAspectRatio="xMinYMin meet"
>
<g
transform={`translate(${margin.left}, ${margin.top})`}
onMouseLeave={() => setHoverOn(undefined)}
>
<ChartAxis
className="gridline-x"
transform="translate(0, 0)"
scale={axisLeft(yScale)
.ticks(5)
.tickSize(-width)
.tickFormat(() => '')
.tickSizeOuter(0)}
/>
<BarGroup
dataToRender={dataToRender}
changeActiveDatasource={changeActiveDatasource}
formatTick={formatTick}
xScale={xScale}
yScale={yScale}
onHoverBar={(e: HoveredBarInfo) => setHoverOn(e)}
hoverOn={hoverOn}
barWidth={barWidth}
/>
<ChartAxis
className="axis-x"
transform={`translate(0, ${height})`}
scale={axisBottom(xScale)}
/>
<ChartAxis
className="axis-y"
scale={axisLeft(yScale)
.ticks(5)
.tickFormat((e: number) => formatTick(e))}
/>
{hoverOn && (
<g
className="hovered-bar"
onClick={() => {
setHoverOn(undefined);
changeActiveDatasource(hoverOn.datasource ?? null);
}}
>
<rect
x={hoverOn.xCoordinate}
y={hoverOn.yCoordinate}
width={barWidth}
height={hoverOn.height}
/>
</g>
)}
</g>
</svg>
);
}
return (
<div className="stacked-bar-chart" ref={ref as any}>
{hoverOn && (
<div className="bar-chart-tooltip">
<div>Datasource: {hoverOn.datasource}</div>
<div>Time: {hoverOn.xValue}</div>
<div>
{`${
activeDataType === 'countData' ? 'Daily total count:' : 'Daily total size:'
} ${formatTick(hoverOn.dailySize!)}`}
</div>
<div>
{`${activeDataType === 'countData' ? 'Count:' : 'Size:'} ${formatTick(
hoverOn.yValue!,
)}`}
</div>
</div>
)}
{renderBarChart()}
</div>
);
});

View File

@ -25,7 +25,7 @@ import type { IngestionSpec } from '../../druid-models';
import { cleanSpec } from '../../druid-models';
import { useQueryManager } from '../../hooks';
import { Api } from '../../singletons';
import { deepSet, getApiArray } from '../../utils';
import { deepSet } from '../../utils';
import { Loader } from '../loader/loader';
import { ShowValue } from '../show-value/show-value';
@ -49,12 +49,11 @@ export const SupervisorHistoryPanel = React.memo(function SupervisorHistoryPanel
const [historyState] = useQueryManager<string, SupervisorHistoryEntry[]>({
initQuery: supervisorId,
processQuery: async (supervisorId, cancelToken) => {
return (
await getApiArray<SupervisorHistoryEntry>(
`/druid/indexer/v1/supervisor/${Api.encodePath(supervisorId)}/history`,
cancelToken,
)
).map(vs => deepSet(vs, 'spec', cleanSpec(vs.spec)));
const resp = await Api.instance.get(
`/druid/indexer/v1/supervisor/${Api.encodePath(supervisorId)}/history`,
{ cancelToken },
);
return resp.data.map((vs: SupervisorHistoryEntry) => deepSet(vs, 'spec', cleanSpec(vs.spec)));
},
});

View File

@ -28,7 +28,7 @@ export interface TableClickableCellProps {
className?: string;
onClick: MouseEventHandler<any>;
hoverIcon?: IconName;
tooltip?: string;
title?: string;
disabled?: boolean;
children?: ReactNode;
}
@ -36,13 +36,12 @@ export interface TableClickableCellProps {
export const TableClickableCell = React.memo(function TableClickableCell(
props: TableClickableCellProps,
) {
const { className, onClick, hoverIcon, disabled, children, tooltip, ...rest } = props;
const { className, onClick, hoverIcon, disabled, children, ...rest } = props;
return (
<div
className={classNames('table-clickable-cell', className, { disabled })}
onClick={disabled ? undefined : onClick}
data-tooltip={tooltip}
{...rest}
>
{children}

View File

@ -27,7 +27,7 @@ import { Deferred } from '../deferred/deferred';
import './table-filterable-cell.scss';
const FILTER_MODES: FilterMode[] = ['=', '!=', '<', '>='];
const FILTER_MODES: FilterMode[] = ['=', '!=', '<=', '>='];
const FILTER_MODES_NO_COMPARISONS: FilterMode[] = ['=', '!='];
export interface TableFilterableCellProps {

View File

@ -182,24 +182,12 @@ export class ConsoleApplication extends React.PureComponent<
changeTabWithFilter('datasources', [{ id: 'datasource', value: `=${datasource}` }]);
};
private readonly goToSegments = ({
start,
end,
datasource,
realtime,
}: {
start?: Date;
end?: Date;
datasource?: string;
realtime?: boolean;
}) => {
private readonly goToSegments = (datasource: string, onlyUnavailable = false) => {
changeTabWithFilter(
'segments',
compact([
start && { id: 'start', value: `>=${start.toISOString()}` },
end && { id: 'end', value: `<${end.toISOString()}` },
datasource && { id: 'datasource', value: `=${datasource}` },
typeof realtime === 'boolean' ? { id: 'is_realtime', value: `=${realtime}` } : undefined,
{ id: 'datasource', value: `=${datasource}` },
onlyUnavailable ? { id: 'is_available', value: '=false' } : undefined,
]),
);
};

View File

@ -24,7 +24,7 @@ import { Loader, ShowValue } from '../../components';
import type { CompactionConfig } from '../../druid-models';
import { useQueryManager } from '../../hooks';
import { Api } from '../../singletons';
import { formatInteger, formatPercent, getApiArray } from '../../utils';
import { formatInteger, formatPercent } from '../../utils';
import { DiffDialog } from '../diff-dialog/diff-dialog';
import './compaction-history-dialog.scss';
@ -65,10 +65,11 @@ export const CompactionHistoryDialog = React.memo(function CompactionHistoryDial
initQuery: datasource,
processQuery: async (datasource, cancelToken) => {
try {
return await getApiArray<CompactionHistoryEntry>(
const resp = await Api.instance.get(
`/druid/coordinator/v1/config/compaction/${Api.encodePath(datasource)}/history?count=20`,
cancelToken,
{ cancelToken },
);
return resp.data;
} catch (e) {
if (e.response?.status === 404) return [];
throw e;

View File

@ -27,7 +27,7 @@ import { COORDINATOR_DYNAMIC_CONFIG_FIELDS } from '../../druid-models';
import { useQueryManager } from '../../hooks';
import { getLink } from '../../links';
import { Api, AppToaster } from '../../singletons';
import { getApiArray, getDruidErrorMessage } from '../../utils';
import { getDruidErrorMessage } from '../../utils';
import { SnitchDialog } from '..';
import './coordinator-dynamic-config-dialog.scss';
@ -47,7 +47,10 @@ export const CoordinatorDynamicConfigDialog = React.memo(function CoordinatorDyn
const [historyRecordsState] = useQueryManager<null, any[]>({
initQuery: null,
processQuery: async (_, cancelToken) => {
return await getApiArray(`/druid/coordinator/v1/config/history?count=100`, cancelToken);
const historyResp = await Api.instance.get(`/druid/coordinator/v1/config/history?count=100`, {
cancelToken,
});
return historyResp.data;
},
});

View File

@ -16,7 +16,7 @@
* limitations under the License.
*/
import { L } from 'druid-query-toolkit';
import { L } from '@druid-toolkit/query';
import React from 'react';
import ReactTable from 'react-table';

View File

@ -16,8 +16,8 @@
* limitations under the License.
*/
import type { QueryResult } from 'druid-query-toolkit';
import { QueryRunner, T } from 'druid-query-toolkit';
import type { QueryResult } from '@druid-toolkit/query';
import { QueryRunner, T } from '@druid-toolkit/query';
import React from 'react';
import { Loader, RecordTablePane } from '../../../components';

View File

@ -16,7 +16,7 @@
* limitations under the License.
*/
import { N } from 'druid-query-toolkit';
import { N } from '@druid-toolkit/query';
import React from 'react';
import ReactTable from 'react-table';

View File

@ -27,7 +27,7 @@ import { OVERLORD_DYNAMIC_CONFIG_FIELDS } from '../../druid-models';
import { useQueryManager } from '../../hooks';
import { getLink } from '../../links';
import { Api, AppToaster } from '../../singletons';
import { getApiArray, getDruidErrorMessage } from '../../utils';
import { getDruidErrorMessage } from '../../utils';
import { SnitchDialog } from '..';
import './overlord-dynamic-config-dialog.scss';
@ -47,7 +47,10 @@ export const OverlordDynamicConfigDialog = React.memo(function OverlordDynamicCo
const [historyRecordsState] = useQueryManager<null, any[]>({
initQuery: null,
processQuery: async (_, cancelToken) => {
return await getApiArray(`/druid/indexer/v1/worker/history?count=100`, cancelToken);
const historyResp = await Api.instance.get(`/druid/indexer/v1/worker/history?count=100`, {
cancelToken,
});
return historyResp.data;
},
});

View File

@ -188,7 +188,7 @@ exports[`RetentionDialog matches snapshot 1`] = `
style="transform: translateY(0); transition: none;"
>
<div
class="rule-detail bp5-card bp5-elevation-2"
class="bp5-card bp5-elevation-2"
>
<div
class="bp5-form-group"
@ -653,7 +653,7 @@ exports[`RetentionDialog matches snapshot 1`] = `
style="transform: translateY(0); transition: none;"
>
<div
class="rule-detail bp5-card bp5-elevation-2"
class="bp5-card bp5-elevation-2"
>
<div
class="bp5-form-group"

View File

@ -22,12 +22,12 @@ import React, { useState } from 'react';
import type { FormJsonTabs } from '../../components';
import { ExternalLink, FormJsonSelector, JsonInput, RuleEditor } from '../../components';
import type { Rule } from '../../druid-models';
import type { Capabilities } from '../../helpers';
import { useQueryManager } from '../../hooks';
import { getLink } from '../../links';
import { Api } from '../../singletons';
import { filterMap, getApiArray, queryDruidSql, swapElements } from '../../utils';
import { filterMap, queryDruidSql, swapElements } from '../../utils';
import type { Rule } from '../../utils/load-rule';
import { SnitchDialog } from '..';
import './retention-dialog.scss';
@ -67,9 +67,11 @@ ORDER BY 1`,
return sqlResp.map(d => d.tier);
} else if (capabilities.hasCoordinatorAccess()) {
return filterMap(
await getApiArray('/druid/coordinator/v1/servers?simple', cancelToken),
(s: any) => (s.type === 'historical' ? s.tier : undefined),
const allServiceResp = await Api.instance.get('/druid/coordinator/v1/servers?simple', {
cancelToken,
});
return filterMap(allServiceResp.data, (s: any) =>
s.type === 'historical' ? s.tier : undefined,
);
} else {
throw new Error(`must have sql or coordinator access`);
@ -82,10 +84,11 @@ ORDER BY 1`,
const [historyQueryState] = useQueryManager<string, any[]>({
initQuery: props.datasource,
processQuery: async (datasource, cancelToken) => {
return await getApiArray(
const historyResp = await Api.instance.get(
`/druid/coordinator/v1/rules/${Api.encodePath(datasource)}/history?count=200`,
cancelToken,
{ cancelToken },
);
return historyResp.data;
},
});

View File

@ -16,8 +16,8 @@
* limitations under the License.
*/
import type { QueryResult } from 'druid-query-toolkit';
import { QueryRunner } from 'druid-query-toolkit';
import type { QueryResult } from '@druid-toolkit/query';
import { QueryRunner } from '@druid-toolkit/query';
import React from 'react';
import { Loader, RecordTablePane } from '../../../components';

View File

@ -50,7 +50,8 @@ export const StatusDialog = React.memo(function StatusDialog(props: StatusDialog
const [responseState] = useQueryManager<null, StatusResponse>({
initQuery: null,
processQuery: async (_, cancelToken) => {
return (await Api.instance.get(`/status`, { cancelToken })).data;
const resp = await Api.instance.get(`/status`, { cancelToken });
return resp.data;
},
});

View File

@ -106,12 +106,11 @@ export const SupervisorResetOffsetsDialog = React.memo(function SupervisorResetO
const [statusResp] = useQueryManager<string, SupervisorStatus>({
initQuery: supervisorId,
processQuery: async (supervisorId, cancelToken) => {
return (
await Api.instance.get(
`/druid/indexer/v1/supervisor/${Api.encodePath(supervisorId)}/status`,
{ cancelToken },
)
).data;
const statusResp = await Api.instance.get(
`/druid/indexer/v1/supervisor/${Api.encodePath(supervisorId)}/status`,
{ cancelToken },
);
return statusResp.data;
},
});

View File

@ -16,7 +16,7 @@
* limitations under the License.
*/
import { Column, QueryResult, SqlExpression, SqlQuery, SqlWithQuery } from 'druid-query-toolkit';
import { Column, QueryResult, SqlExpression, SqlQuery, SqlWithQuery } from '@druid-toolkit/query';
import { maybeGetClusterCapacity } from '../../helpers';
import {

View File

@ -16,7 +16,7 @@
* limitations under the License.
*/
import type { SqlQuery } from 'druid-query-toolkit';
import type { SqlQuery } from '@druid-toolkit/query';
import {
C,
F,
@ -28,7 +28,7 @@ import {
SqlLiteral,
SqlStar,
SqlType,
} from 'druid-query-toolkit';
} from '@druid-toolkit/query';
import * as JSONBig from 'json-bigint-native';
import { nonEmptyArray } from '../../utils';

View File

@ -21,7 +21,6 @@ export * from './compaction-config/compaction-config';
export * from './compaction-status/compaction-status';
export * from './coordinator-dynamic-config/coordinator-dynamic-config';
export * from './dart/dart-query-entry';
export * from './datasource/datasource';
export * from './dimension-spec/dimension-spec';
export * from './druid-engine/druid-engine';
export * from './execution/execution';
@ -33,12 +32,10 @@ export * from './ingest-query-pattern/ingest-query-pattern';
export * from './ingestion-spec/ingestion-spec';
export * from './input-format/input-format';
export * from './input-source/input-source';
export * from './load-rule/load-rule';
export * from './lookup-spec/lookup-spec';
export * from './metric-spec/metric-spec';
export * from './overlord-dynamic-config/overlord-dynamic-config';
export * from './query-context/query-context';
export * from './segment/segment';
export * from './stages/stages';
export * from './supervisor-status/supervisor-status';
export * from './task/task';

View File

@ -16,7 +16,7 @@
* limitations under the License.
*/
import { sane, SqlQuery } from 'druid-query-toolkit';
import { sane, SqlQuery } from '@druid-toolkit/query';
import { fitIngestQueryPattern, ingestQueryPatternToQuery } from './ingest-query-pattern';

View File

@ -26,7 +26,7 @@ import {
SqlTable,
SqlWithPart,
T,
} from 'druid-query-toolkit';
} from '@druid-toolkit/query';
import { filterMap, oneOf } from '../../utils';
import type { ExternalConfig } from '../external-config/external-config';

View File

@ -1,50 +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 { Duration } from '../../utils';
export const START_OF_TIME_DATE = '-146136543-09-08T08:23:32.096Z';
export const END_OF_TIME_DATE = '146140482-04-24T15:36:27.903Z';
export function computeSegmentTimeSpan(start: string, end: string): string {
if (start === START_OF_TIME_DATE && end === END_OF_TIME_DATE) {
return 'All';
}
const startDate = new Date(start);
if (isNaN(startDate.valueOf())) {
return 'Invalid start';
}
const endDate = new Date(end);
if (isNaN(endDate.valueOf())) {
return 'Invalid end';
}
return Duration.fromRange(startDate, endDate, 'Etc/UTC').getDescription(true);
}
export interface ShardSpec {
type: string;
partitionNum?: number;
partitions?: number;
dimensions?: string[];
partitionDimensions?: string[];
start?: string[];
end?: string[];
}

View File

@ -16,7 +16,7 @@
* limitations under the License.
*/
import { C } from 'druid-query-toolkit';
import { C } from '@druid-toolkit/query';
import type { Counters, StageDefinition } from '../stages/stages';

View File

@ -16,7 +16,7 @@
* limitations under the License.
*/
import { sane } from 'druid-query-toolkit';
import { sane } from '@druid-toolkit/query';
import { WorkbenchQuery } from './workbench-query';

View File

@ -22,7 +22,7 @@ import type {
SqlClusteredByClause,
SqlExpression,
SqlPartitionedByClause,
} from 'druid-query-toolkit';
} from '@druid-toolkit/query';
import {
C,
F,
@ -30,7 +30,7 @@ import {
SqlOrderByClause,
SqlOrderByExpression,
SqlQuery,
} from 'druid-query-toolkit';
} from '@druid-toolkit/query';
import Hjson from 'hjson';
import * as JSONBig from 'json-bigint-native';
import { v4 as uuidv4 } from 'uuid';

View File

@ -56,7 +56,6 @@ body {
position: absolute;
height: 100%;
width: 100%;
z-index: 0;
.console-application {
position: absolute;

View File

@ -20,7 +20,7 @@ import 'regenerator-runtime/runtime';
import './bootstrap/ace';
import { OverlaysProvider } from '@blueprintjs/core';
import { QueryRunner } from 'druid-query-toolkit';
import { QueryRunner } from '@druid-toolkit/query';
import { createRoot } from 'react-dom/client';
import { bootstrapJsonParse } from './bootstrap/json-parser';

View File

@ -211,10 +211,6 @@ export class Capabilities {
};
}
public clone(): Capabilities {
return new Capabilities(this.valueOf());
}
public getMode(): CapabilitiesMode {
if (!this.hasSql()) return 'no-sql';
if (!this.hasCoordinatorAccess()) return 'no-proxy';

View File

@ -16,8 +16,8 @@
* limitations under the License.
*/
import type { QueryResult } from '@druid-toolkit/query';
import type { CancelToken } from 'axios';
import type { QueryResult } from 'druid-query-toolkit';
import type { Execution } from '../../druid-models';
import { IntermediateQueryState } from '../../utils';

View File

@ -16,8 +16,8 @@
* limitations under the License.
*/
import { QueryResult } from '@druid-toolkit/query';
import type { AxiosResponse, CancelToken } from 'axios';
import { QueryResult } from 'druid-query-toolkit';
import type { AsyncStatusResponse, MsqTaskPayloadResponse, QueryContext } from '../../druid-models';
import { Execution } from '../../druid-models';

View File

@ -25,7 +25,7 @@ import {
SqlExpression,
SqlType,
T,
} from 'druid-query-toolkit';
} from '@druid-toolkit/query';
import * as JSONBig from 'json-bigint-native';
import type {

View File

@ -16,9 +16,7 @@
* limitations under the License.
*/
import { useState } from 'react';
import { useInterval } from './use-interval';
import { useEffect, useState } from 'react';
function getNowToSecond(): Date {
const now = new Date();
@ -26,12 +24,18 @@ function getNowToSecond(): Date {
return now;
}
export function useClock(updateInterval = 1000) {
export function useClock() {
const [now, setNow] = useState<Date>(getNowToSecond);
useInterval(() => {
setNow(getNowToSecond());
}, updateInterval);
useEffect(() => {
const checkInterval = setInterval(() => {
setNow(getNowToSecond());
}, 1000);
return () => {
clearInterval(checkInterval);
};
}, []);
return now;
}

View File

@ -75,9 +75,5 @@ describe('react-table-utils', () => {
{ id: 'x', value: '~y' },
{ id: 'z', value: '=w&' },
]);
expect(stringToTableFilters('x<3&y<=3')).toEqual([
{ id: 'x', value: '<3' },
{ id: 'y', value: '<=3' },
]);
});
});

View File

@ -18,7 +18,8 @@
import type { IconName } from '@blueprintjs/core';
import { IconNames } from '@blueprintjs/icons';
import { C, F, SqlExpression } from 'druid-query-toolkit';
import type { SqlExpression } from '@druid-toolkit/query';
import { C, F } from '@druid-toolkit/query';
import type { Filter } from 'react-table';
import { addOrUpdate, caseInsensitiveContains, filterMap } from '../utils';
@ -31,9 +32,9 @@ export const STANDARD_TABLE_PAGE_SIZE_OPTIONS = [50, 100, 200];
export const SMALL_TABLE_PAGE_SIZE = 25;
export const SMALL_TABLE_PAGE_SIZE_OPTIONS = [25, 50, 100];
export type FilterMode = '~' | '=' | '!=' | '<' | '<=' | '>' | '>=';
export type FilterMode = '~' | '=' | '!=' | '<=' | '>=';
export const FILTER_MODES: FilterMode[] = ['~', '=', '!=', '<', '<=', '>', '>='];
export const FILTER_MODES: FilterMode[] = ['~', '=', '!=', '<=', '>='];
export const FILTER_MODES_NO_COMPARISON: FilterMode[] = ['~', '=', '!='];
export function filterModeToIcon(mode: FilterMode): IconName {
@ -44,12 +45,8 @@ export function filterModeToIcon(mode: FilterMode): IconName {
return IconNames.EQUALS;
case '!=':
return IconNames.NOT_EQUAL_TO;
case '<':
return IconNames.LESS_THAN;
case '<=':
return IconNames.LESS_THAN_OR_EQUAL_TO;
case '>':
return IconNames.GREATER_THAN;
case '>=':
return IconNames.GREATER_THAN_OR_EQUAL_TO;
default:
@ -65,12 +62,8 @@ export function filterModeToTitle(mode: FilterMode): string {
return 'Equals';
case '!=':
return 'Not equals';
case '<':
return 'Less than';
case '<=':
return 'Less than or equal';
case '>':
return 'Greater than';
case '>=':
return 'Greater than or equal';
default:
@ -96,7 +89,7 @@ export function parseFilterModeAndNeedle(
filter: Filter,
loose = false,
): FilterModeAndNeedle | undefined {
const m = /^(~|=|!=|<(?!=)|<=|>(?!=)|>=)?(.*)$/.exec(String(filter.value));
const m = /^(~|=|!=|<=|>=)?(.*)$/.exec(String(filter.value));
if (!m) return;
if (!loose && !m[2]) return;
const mode = (m[1] as FilterMode) || '~';
@ -119,28 +112,21 @@ export function booleanCustomTableFilter(filter: Filter, value: unknown): boolea
const modeAndNeedle = parseFilterModeAndNeedle(filter);
if (!modeAndNeedle) return true;
const { mode, needle } = modeAndNeedle;
const strValue = String(value);
switch (mode) {
case '=':
return strValue === needle;
return String(value) === needle;
case '!=':
return strValue !== needle;
case '<':
return strValue < needle;
return String(value) !== needle;
case '<=':
return strValue <= needle;
case '>':
return strValue > needle;
return String(value) <= needle;
case '>=':
return strValue >= needle;
return String(value) >= needle;
default:
return caseInsensitiveContains(strValue, needle);
return caseInsensitiveContains(String(value), needle);
}
}
@ -156,15 +142,9 @@ export function sqlQueryCustomTableFilter(filter: Filter): SqlExpression | undef
case '!=':
return column.unequal(needle);
case '<':
return column.lessThan(needle);
case '<=':
return column.lessThanOrEqual(needle);
case '>':
return column.greaterThan(needle);
case '>=':
return column.greaterThanOrEqual(needle);
@ -173,10 +153,6 @@ export function sqlQueryCustomTableFilter(filter: Filter): SqlExpression | undef
}
}
export function sqlQueryCustomTableFilters(filters: Filter[]): SqlExpression {
return SqlExpression.and(...filterMap(filters, sqlQueryCustomTableFilter));
}
export function tableFiltersToString(tableFilters: Filter[]): string {
return tableFilters
.map(({ id, value }) => `${id}${value.replace(/[&%]/g, encodeURIComponent)}`)
@ -185,11 +161,9 @@ export function tableFiltersToString(tableFilters: Filter[]): string {
export function stringToTableFilters(str: string | undefined): Filter[] {
if (!str) return [];
// '~' | '=' | '!=' | '<' | '<=' | '>' | '>=';
// '~' | '=' | '!=' | '<=' | '>=';
return filterMap(str.split('&'), clause => {
const m = /^(\w+)((?:~|=|!=|<(?!=)|<=|>(?!=)|>=).*)$/.exec(
clause.replace(/%2[56]/g, decodeURIComponent),
);
const m = /^(\w+)((?:~|=|!=|<=|>=).*)$/.exec(clause.replace(/%2[56]/g, decodeURIComponent));
if (!m) return;
return { id: m[1], value: m[2] };
});

View File

@ -1,169 +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 { shifters } from './date-floor-shift-ceil';
function pairwise<T>(array: T[], callback: (t1: T, t2: T) => void) {
for (let i = 0; i < array.length - 1; i++) {
callback(array[i], array[i + 1]);
}
}
describe('floor, shift, ceil (UTC)', () => {
const tz = 'Etc/UTC';
it('moves seconds', () => {
const dates: Date[] = [
new Date('2012-11-04T00:00:00Z'),
new Date('2012-11-04T00:00:03Z'),
new Date('2012-11-04T00:00:06Z'),
new Date('2012-11-04T00:00:09Z'),
new Date('2012-11-04T00:00:12Z'),
];
pairwise(dates, (d1, d2) => expect(shifters.second.shift(d1, tz, 3)).toEqual(d2));
});
it('rounds minutes', () => {
expect(shifters.minute.round(new Date('2012-11-04T00:29:00Z'), 15, tz)).toEqual(
new Date('2012-11-04T00:15:00Z'),
);
expect(shifters.minute.round(new Date('2012-11-04T00:29:00Z'), 4, tz)).toEqual(
new Date('2012-11-04T00:28:00Z'),
);
});
it('moves minutes', () => {
const dates: Date[] = [
new Date('2012-11-04T00:00:00Z'),
new Date('2012-11-04T00:03:00Z'),
new Date('2012-11-04T00:06:00Z'),
new Date('2012-11-04T00:09:00Z'),
new Date('2012-11-04T00:12:00Z'),
];
pairwise(dates, (d1, d2) => expect(shifters.minute.shift(d1, tz, 3)).toEqual(d2));
});
it('floors hour correctly', () => {
expect(shifters.hour.floor(new Date('2012-11-04T00:30:00Z'), tz)).toEqual(
new Date('2012-11-04T00:00:00Z'),
);
expect(shifters.hour.floor(new Date('2012-11-04T01:30:00Z'), tz)).toEqual(
new Date('2012-11-04T01:00:00Z'),
);
expect(shifters.hour.floor(new Date('2012-11-04T01:30:00Z'), tz)).toEqual(
new Date('2012-11-04T01:00:00Z'),
);
expect(shifters.hour.floor(new Date('2012-11-04T02:30:00Z'), tz)).toEqual(
new Date('2012-11-04T02:00:00Z'),
);
expect(shifters.hour.floor(new Date('2012-11-04T03:30:00Z'), tz)).toEqual(
new Date('2012-11-04T03:00:00Z'),
);
});
it('moves hour', () => {
const dates: Date[] = [
new Date('2012-11-04T00:00:00Z'),
new Date('2012-11-04T01:00:00Z'),
new Date('2012-11-04T02:00:00Z'),
new Date('2012-11-04T03:00:00Z'),
];
pairwise(dates, (d1, d2) => expect(shifters.hour.shift(d1, tz, 1)).toEqual(d2));
});
it('moves day', () => {
const dates: Date[] = [
new Date('2012-11-03T00:00:00Z'),
new Date('2012-11-04T00:00:00Z'),
new Date('2012-11-05T00:00:00Z'),
new Date('2012-11-06T00:00:00Z'),
];
pairwise(dates, (d1, d2) => expect(shifters.day.shift(d1, tz, 1)).toEqual(d2));
});
it('ceils day', () => {
let d1 = new Date('2014-12-11T22:11:57.469Z');
let d2 = new Date('2014-12-12T00:00:00.000Z');
expect(shifters.day.ceil(d1, tz)).toEqual(d2);
d1 = new Date('2014-12-08T00:00:00.000Z');
d2 = new Date('2014-12-08T00:00:00.000Z');
expect(shifters.day.ceil(d1, tz)).toEqual(d2);
});
it('moves week', () => {
const dates: Date[] = [
new Date('2012-10-29T00:00:00Z'),
new Date('2012-11-05T00:00:00Z'),
new Date('2012-11-12T00:00:00Z'),
new Date('2012-11-19T00:00:00Z'),
];
pairwise(dates, (d1, d2) => expect(shifters.week.shift(d1, tz, 1)).toEqual(d2));
});
it('floors week correctly', () => {
let d1 = new Date('2014-12-11T22:11:57.469Z');
let d2 = new Date('2014-12-08T00:00:00.000Z');
expect(shifters.week.floor(d1, tz)).toEqual(d2);
d1 = new Date('2014-12-07T12:11:57.469Z');
d2 = new Date('2014-12-01T00:00:00.000Z');
expect(shifters.week.floor(d1, tz)).toEqual(d2);
});
it('ceils week correctly', () => {
let d1 = new Date('2014-12-11T22:11:57.469Z');
let d2 = new Date('2014-12-15T00:00:00.000Z');
expect(shifters.week.ceil(d1, tz)).toEqual(d2);
d1 = new Date('2014-12-07T12:11:57.469Z');
d2 = new Date('2014-12-08T00:00:00.000Z');
expect(shifters.week.ceil(d1, tz)).toEqual(d2);
});
it('moves month', () => {
const dates: Date[] = [
new Date('2012-11-01T00:00:00Z'),
new Date('2012-12-01T00:00:00Z'),
new Date('2013-01-01T00:00:00Z'),
new Date('2013-02-01T00:00:00Z'),
];
pairwise(dates, (d1, d2) => expect(shifters.month.shift(d1, tz, 1)).toEqual(d2));
});
it('shifts month on the 31st', () => {
const d1 = new Date('2016-03-31T00:00:00.000Z');
const d2 = new Date('2016-05-01T00:00:00.000Z');
expect(shifters.month.shift(d1, tz, 1)).toEqual(d2);
});
it('moves year', () => {
const dates: Date[] = [
new Date('2010-01-01T00:00:00Z'),
new Date('2011-01-01T00:00:00Z'),
new Date('2012-01-01T00:00:00Z'),
new Date('2013-01-01T00:00:00Z'),
];
pairwise(dates, (d1, d2) => expect(shifters.year.shift(d1, tz, 1)).toEqual(d2));
});
});

View File

@ -1,181 +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 { shifters } from './date-floor-shift-ceil';
function pairwise<T>(array: T[], callback: (t1: T, t2: T) => void) {
for (let i = 0; i < array.length - 1; i++) {
callback(array[i], array[i + 1]);
}
}
describe('floor/shift/ceil', () => {
const tz = 'America/Los_Angeles';
it('shifts seconds', () => {
const dates: Date[] = [
new Date('2012-11-04T00:00:00-07:00'),
new Date('2012-11-04T00:00:03-07:00'),
new Date('2012-11-04T00:00:06-07:00'),
new Date('2012-11-04T00:00:09-07:00'),
new Date('2012-11-04T00:00:12-07:00'),
];
pairwise(dates, (d1, d2) => expect(shifters.second.shift(d1, tz, 3)).toEqual(d2));
});
it('shifts minutes', () => {
const dates: Date[] = [
new Date('2012-11-04T00:00:00-07:00'),
new Date('2012-11-04T00:03:00-07:00'),
new Date('2012-11-04T00:06:00-07:00'),
new Date('2012-11-04T00:09:00-07:00'),
new Date('2012-11-04T00:12:00-07:00'),
];
pairwise(dates, (d1, d2) => expect(shifters.minute.shift(d1, tz, 3)).toEqual(d2));
});
it('floors hour correctly', () => {
expect(shifters.hour.floor(new Date('2012-11-04T00:30:00-07:00'), tz)).toEqual(
new Date('2012-11-04T00:00:00-07:00'),
);
expect(shifters.hour.floor(new Date('2012-11-04T01:30:00-07:00'), tz)).toEqual(
new Date('2012-11-04T01:00:00-07:00'),
);
expect(shifters.hour.floor(new Date('2012-11-04T01:30:00-08:00'), tz)).toEqual(
new Date('2012-11-04T01:00:00-07:00'),
);
expect(shifters.hour.floor(new Date('2012-11-04T02:30:00-08:00'), tz)).toEqual(
new Date('2012-11-04T02:00:00-08:00'),
);
expect(shifters.hour.floor(new Date('2012-11-04T03:30:00-08:00'), tz)).toEqual(
new Date('2012-11-04T03:00:00-08:00'),
);
});
it('shifting 24 hours over DST is not the same as shifting a day', () => {
const start = new Date('2012-11-04T07:00:00Z');
const shift1Day = shifters.day.shift(start, tz, 1);
const shift24Hours = shifters.hour.shift(start, tz, 24);
expect(shift1Day).toEqual(new Date('2012-11-05T08:00:00Z'));
expect(shift24Hours).toEqual(new Date('2012-11-05T07:00:00Z'));
});
it('shifts hour over DST 1', () => {
const dates: Date[] = [
new Date('2012-11-04T00:00:00-07:00'),
new Date('2012-11-04T08:00:00Z'),
new Date('2012-11-04T09:00:00Z'),
new Date('2012-11-04T10:00:00Z'),
new Date('2012-11-04T11:00:00Z'),
];
pairwise(dates, (d1, d2) => expect(shifters.hour.shift(d1, tz, 1)).toEqual(d2));
});
it('floors hour over DST 1', () => {
expect(shifters.hour.floor(new Date('2012-11-04T00:05:00-07:00'), tz)).toEqual(
new Date('2012-11-04T00:00:00-07:00'),
);
expect(shifters.hour.floor(new Date('2012-11-04T01:05:00-07:00'), tz)).toEqual(
new Date('2012-11-04T01:00:00-07:00'),
);
expect(shifters.hour.floor(new Date('2012-11-04T02:05:00-07:00'), tz)).toEqual(
new Date('2012-11-04T01:00:00-07:00'),
);
expect(shifters.hour.floor(new Date('2012-11-04T03:05:00-07:00'), tz)).toEqual(
new Date('2012-11-04T03:00:00-07:00'),
);
});
it('shifts hour over DST 2', () => {
// "2018-03-11T09:00:00Z"
const dates: Date[] = [
new Date('2018-03-11T01:00:00-07:00'),
new Date('2018-03-11T09:00:00Z'),
new Date('2018-03-11T10:00:00Z'),
new Date('2018-03-11T11:00:00Z'),
new Date('2018-03-11T12:00:00Z'),
];
pairwise(dates, (d1, d2) => expect(shifters.hour.shift(d1, tz, 1)).toEqual(d2));
});
it('shifts day over DST', () => {
const dates: Date[] = [
new Date('2012-11-03T00:00:00-07:00'),
new Date('2012-11-04T00:00:00-07:00'),
new Date('2012-11-05T00:00:00-08:00'),
new Date('2012-11-06T00:00:00-08:00'),
];
pairwise(dates, (d1, d2) => expect(shifters.day.shift(d1, tz, 1)).toEqual(d2));
});
it('shifts week over DST', () => {
const dates: Date[] = [
new Date('2012-10-29T00:00:00-07:00'),
new Date('2012-11-05T00:00:00-08:00'),
new Date('2012-11-12T00:00:00-08:00'),
new Date('2012-11-19T00:00:00-08:00'),
];
pairwise(dates, (d1, d2) => expect(shifters.week.shift(d1, tz, 1)).toEqual(d2));
});
it('floors week correctly', () => {
let d1 = new Date('2014-12-11T22:11:57.469Z');
let d2 = new Date('2014-12-08T08:00:00.000Z');
expect(shifters.week.floor(d1, tz)).toEqual(d2);
d1 = new Date('2014-12-07T12:11:57.469Z');
d2 = new Date('2014-12-01T08:00:00.000Z');
expect(shifters.week.floor(d1, tz)).toEqual(d2);
});
it('ceils week correctly', () => {
let d1 = new Date('2014-12-11T22:11:57.469Z');
let d2 = new Date('2014-12-15T08:00:00.000Z');
expect(shifters.week.ceil(d1, tz)).toEqual(d2);
d1 = new Date('2014-12-07T12:11:57.469Z');
d2 = new Date('2014-12-08T08:00:00.000Z');
expect(shifters.week.ceil(d1, tz)).toEqual(d2);
});
it('shifts month over DST', () => {
const dates: Date[] = [
new Date('2012-11-01T00:00:00-07:00'),
new Date('2012-12-01T00:00:00-08:00'),
new Date('2013-01-01T00:00:00-08:00'),
new Date('2013-02-01T00:00:00-08:00'),
];
pairwise(dates, (d1, d2) => expect(shifters.month.shift(d1, tz, 1)).toEqual(d2));
});
it('shifts year', () => {
const dates: Date[] = [
new Date('2010-01-01T00:00:00-08:00'),
new Date('2011-01-01T00:00:00-08:00'),
new Date('2012-01-01T00:00:00-08:00'),
new Date('2013-01-01T00:00:00-08:00'),
];
pairwise(dates, (d1, d2) => expect(shifters.year.shift(d1, tz, 1)).toEqual(d2));
});
});

View File

@ -1,296 +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 { fromDate, startOfWeek } from '@internationalized/date';
export type AlignFn = (dt: Date, tz: string) => Date;
export type ShiftFn = (dt: Date, tz: string, step: number) => Date;
export type RoundFn = (dt: Date, roundTo: number, tz: string) => Date;
export interface TimeShifterNoCeil {
canonicalLength: number;
siblings?: number;
floor: AlignFn;
round: RoundFn;
shift: ShiftFn;
}
export interface TimeShifter extends TimeShifterNoCeil {
ceil: AlignFn;
}
function isUTC(tz: string): boolean {
return tz === 'Etc/UTC';
}
function adjustDay(day: number): number {
return (day + 6) % 7;
}
function floorTo(n: number, roundTo: number): number {
return Math.floor(n / roundTo) * roundTo;
}
function timeShifterFiller(tm: TimeShifterNoCeil): TimeShifter {
const { floor, shift } = tm;
return {
...tm,
ceil: (dt: Date, tz: string) => {
const floored = floor(dt, tz);
if (floored.valueOf() === dt.valueOf()) return dt; // Just like ceil(3) is 3 and not 4
return shift(floored, tz, 1);
},
};
}
export const second = timeShifterFiller({
canonicalLength: 1000,
siblings: 60,
floor: (dt, _tz) => {
// Seconds do not actually need a timezone because all timezones align on seconds... for now...
dt = new Date(dt.valueOf());
dt.setUTCMilliseconds(0);
return dt;
},
round: (dt, roundTo, _tz) => {
const cur = dt.getUTCSeconds();
const adj = floorTo(cur, roundTo);
if (cur !== adj) dt.setUTCSeconds(adj);
return dt;
},
shift: (dt, _tz, step) => {
dt = new Date(dt.valueOf());
dt.setUTCSeconds(dt.getUTCSeconds() + step);
return dt;
},
});
export const minute = timeShifterFiller({
canonicalLength: 60000,
siblings: 60,
floor: (dt, _tz) => {
// Minutes do not actually need a timezone because all timezones align on minutes... for now...
dt = new Date(dt.valueOf());
dt.setUTCSeconds(0, 0);
return dt;
},
round: (dt, roundTo, _tz) => {
const cur = dt.getUTCMinutes();
const adj = floorTo(cur, roundTo);
if (cur !== adj) dt.setUTCMinutes(adj);
return dt;
},
shift: (dt, _tz, step) => {
dt = new Date(dt.valueOf());
dt.setUTCMinutes(dt.getUTCMinutes() + step);
return dt;
},
});
// Movement by hour is tz independent because in every timezone an hour is 60 min
function hourMove(dt: Date, _tz: string, step: number) {
dt = new Date(dt.valueOf());
dt.setUTCHours(dt.getUTCHours() + step);
return dt;
}
export const hour = timeShifterFiller({
canonicalLength: 3600000,
siblings: 24,
floor: (dt, tz) => {
if (isUTC(tz)) {
dt = new Date(dt.valueOf());
dt.setUTCMinutes(0, 0, 0);
return dt;
} else {
return fromDate(dt, tz).set({ second: 0, minute: 0, millisecond: 0 }).toDate();
}
},
round: (dt, roundTo, tz) => {
if (isUTC(tz)) {
const cur = dt.getUTCHours();
const adj = floorTo(cur, roundTo);
if (cur !== adj) dt.setUTCHours(adj);
} else {
const cur = fromDate(dt, tz).hour;
const adj = floorTo(cur, roundTo);
if (cur !== adj) return hourMove(dt, tz, adj - cur);
}
return dt;
},
shift: hourMove,
});
export const day = timeShifterFiller({
canonicalLength: 24 * 3600000,
floor: (dt, tz) => {
if (isUTC(tz)) {
dt = new Date(dt.valueOf());
dt.setUTCHours(0, 0, 0, 0);
return dt;
} else {
return fromDate(dt, tz).set({ hour: 0, second: 0, minute: 0, millisecond: 0 }).toDate();
}
},
shift: (dt, tz, step) => {
if (isUTC(tz)) {
dt = new Date(dt.valueOf());
dt.setUTCDate(dt.getUTCDate() + step);
return dt;
} else {
return fromDate(dt, tz).add({ days: step }).toDate();
}
},
round: () => {
throw new Error('missing day round');
},
});
export const week = timeShifterFiller({
canonicalLength: 7 * 24 * 3600000,
floor: (dt, tz) => {
if (isUTC(tz)) {
dt = new Date(dt.valueOf());
dt.setUTCHours(0, 0, 0, 0);
dt.setUTCDate(dt.getUTCDate() - adjustDay(dt.getUTCDay()));
} else {
const zd = fromDate(dt, tz);
return startOfWeek(
zd.set({ hour: 0, second: 0, minute: 0, millisecond: 0 }),
'fr-FR', // We want the week to start on Monday
).toDate();
}
return dt;
},
shift: (dt, tz, step) => {
if (isUTC(tz)) {
dt = new Date(dt.valueOf());
dt.setUTCDate(dt.getUTCDate() + step * 7);
return dt;
} else {
return fromDate(dt, tz).add({ weeks: step }).toDate();
}
},
round: () => {
throw new Error('missing week round');
},
});
function monthShift(dt: Date, tz: string, step: number) {
if (isUTC(tz)) {
dt = new Date(dt.valueOf());
dt.setUTCMonth(dt.getUTCMonth() + step);
return dt;
} else {
return fromDate(dt, tz).add({ months: step }).toDate();
}
}
export const month = timeShifterFiller({
canonicalLength: 30 * 24 * 3600000,
siblings: 12,
floor: (dt, tz) => {
if (isUTC(tz)) {
dt = new Date(dt.valueOf());
dt.setUTCHours(0, 0, 0, 0);
dt.setUTCDate(1);
return dt;
} else {
return fromDate(dt, tz)
.set({ day: 1, hour: 0, second: 0, minute: 0, millisecond: 0 })
.toDate();
}
},
round: (dt, roundTo, tz) => {
if (isUTC(tz)) {
const cur = dt.getUTCMonth();
const adj = floorTo(cur, roundTo);
if (cur !== adj) dt.setUTCMonth(adj);
} else {
const cur = fromDate(dt, tz).month - 1; // Needs to be zero indexed
const adj = floorTo(cur, roundTo);
if (cur !== adj) return monthShift(dt, tz, adj - cur);
}
return dt;
},
shift: monthShift,
});
function yearShift(dt: Date, tz: string, step: number) {
if (isUTC(tz)) {
dt = new Date(dt.valueOf());
dt.setUTCFullYear(dt.getUTCFullYear() + step);
return dt;
} else {
return fromDate(dt, tz).add({ years: step }).toDate();
}
}
export const year = timeShifterFiller({
canonicalLength: 365 * 24 * 3600000,
siblings: 1000,
floor: (dt, tz) => {
if (isUTC(tz)) {
dt = new Date(dt.valueOf());
dt.setUTCHours(0, 0, 0, 0);
dt.setUTCMonth(0, 1);
return dt;
} else {
return fromDate(dt, tz)
.set({ month: 1, day: 1, hour: 0, second: 0, minute: 0, millisecond: 0 })
.toDate();
}
},
round: (dt, roundTo, tz) => {
if (isUTC(tz)) {
const cur = dt.getUTCFullYear();
const adj = floorTo(cur, roundTo);
if (cur !== adj) dt.setUTCFullYear(adj);
} else {
const cur = fromDate(dt, tz).year;
const adj = floorTo(cur, roundTo);
if (cur !== adj) return yearShift(dt, tz, adj - cur);
}
return dt;
},
shift: yearShift,
});
export interface Shifters {
second: TimeShifter;
minute: TimeShifter;
hour: TimeShifter;
day: TimeShifter;
week: TimeShifter;
month: TimeShifter;
year: TimeShifter;
[key: string]: TimeShifter;
}
export const shifters: Shifters = {
second,
minute,
hour,
day,
week,
month,
year,
};

View File

@ -17,6 +17,7 @@
*/
import {
ceilToUtcDay,
dateToIsoDateString,
intervalToLocalDateRange,
localDateRangeToInterval,
@ -59,4 +60,12 @@ describe('date', () => {
expect(localDateRangeToInterval(intervalToLocalDateRange(interval))).toEqual(interval);
});
});
describe('ceilToUtcDay', () => {
it('works', () => {
expect(ceilToUtcDay(new Date('2021-02-03T12:03:02.001Z'))).toEqual(
new Date('2021-02-04T00:00:00Z'),
);
});
});
});

View File

@ -99,10 +99,9 @@ export function localDateRangeToInterval(localRange: DateRange): string {
}`;
}
export function maxDate(a: Date, b: Date): Date {
return a > b ? a : b;
}
export function minDate(a: Date, b: Date): Date {
return a < b ? a : b;
export function ceilToUtcDay(date: Date): Date {
date = new Date(date.valueOf());
date.setUTCHours(0, 0, 0, 0);
date.setUTCDate(date.getUTCDate() + 1);
return date;
}

View File

@ -16,7 +16,7 @@
* limitations under the License.
*/
import type { QueryResult } from 'druid-query-toolkit';
import type { QueryResult } from '@druid-toolkit/query';
import FileSaver from 'file-saver';
import * as JSONBig from 'json-bigint-native';

View File

@ -16,7 +16,7 @@
* limitations under the License.
*/
import { sane } from 'druid-query-toolkit';
import { sane } from '@druid-toolkit/query';
import { DruidError, getDruidErrorMessage } from './druid-query';

View File

@ -16,9 +16,9 @@
* limitations under the License.
*/
import { C } from '@druid-toolkit/query';
import type { AxiosResponse, CancelToken } from 'axios';
import axios from 'axios';
import { C } from 'druid-query-toolkit';
import { Api } from '../singletons';
@ -358,12 +358,6 @@ export async function queryDruidSqlDart<T = any>(
return sqlResultResp.data;
}
export async function getApiArray<T = any>(url: string, cancelToken?: CancelToken): Promise<T[]> {
const result = (await Api.instance.get(url, { cancelToken })).data;
if (!Array.isArray(result)) throw new Error('unexpected result');
return result;
}
export interface QueryExplanation {
query: any;
signature: { name: string; type: string }[];

View File

@ -1,505 +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 { Duration } from './duration';
describe('Duration', () => {
const TZ_LA = 'America/Los_Angeles';
const TZ_JUNEAU = 'America/Juneau';
describe('errors', () => {
it('throws error if invalid duration', () => {
expect(() => new Duration('')).toThrow("Can not parse duration ''");
expect(() => new Duration('P00')).toThrow("Can not parse duration 'P00'");
expect(() => new Duration('P')).toThrow('Duration can not be empty');
expect(() => new Duration('P0YT0H')).toThrow('Duration can not be empty');
expect(() => new Duration('P0W').shift(new Date(), TZ_LA)).toThrow(
'Duration can not have empty weeks',
);
expect(() => new Duration('P0Y0MT0H0M0S').shift(new Date(), TZ_LA)).toThrow(
'Duration can not be empty',
);
});
it('throws error if fromJS is not given a string', () => {
expect(() => new Duration(new Date() as any)).toThrow('Duration can not be empty');
});
});
describe('#toString', () => {
it('gives back the correct string', () => {
let durationStr: string;
durationStr = 'P3Y';
expect(new Duration(durationStr).toString()).toEqual(durationStr);
durationStr = 'P2W';
expect(new Duration(durationStr).toString()).toEqual(durationStr);
durationStr = 'PT5H';
expect(new Duration(durationStr).toString()).toEqual(durationStr);
durationStr = 'P3DT15H';
expect(new Duration(durationStr).toString()).toEqual(durationStr);
});
it('eliminates 0', () => {
expect(new Duration('P0DT15H').toString()).toEqual('PT15H');
});
});
describe('fromCanonicalLength', () => {
it('handles zero', () => {
expect(() => {
Duration.fromCanonicalLength(0);
}).toThrow('length must be positive');
});
it('works 1', () => {
expect(Duration.fromCanonicalLength(86400000).toString()).toEqual('P1D');
});
it('works 2', () => {
const len =
new Date('2018-03-01T00:00:00Z').valueOf() - new Date('2016-02-22T00:00:00Z').valueOf();
expect(Duration.fromCanonicalLength(len).toString()).toEqual('P2Y8D');
});
it('works 3', () => {
const len =
new Date('2018-09-15T00:00:00Z').valueOf() - new Date('2018-09-04T00:00:00Z').valueOf();
expect(Duration.fromCanonicalLength(len).toString()).toEqual('P11D');
});
it('works with months', () => {
expect(Duration.fromCanonicalLength(2592000000).toString()).toEqual('P1M');
expect(Duration.fromCanonicalLength(2678400000).toString()).toEqual('P1M1D');
});
it('works without months', () => {
expect(Duration.fromCanonicalLength(2592000000, true).toString()).toEqual('P30D');
expect(Duration.fromCanonicalLength(2678400000, true).toString()).toEqual('P31D');
});
});
describe('construct from span', () => {
it('parses days over DST', () => {
expect(
Duration.fromRange(
new Date('2012-10-29T00:00:00-07:00'),
new Date('2012-11-05T00:00:00-08:00'),
TZ_LA,
).toString(),
).toEqual('P7D');
expect(
Duration.fromRange(
new Date('2012-10-29T00:00:00-07:00'),
new Date('2012-11-12T00:00:00-08:00'),
TZ_LA,
).toString(),
).toEqual('P14D');
});
it('parses complex case', () => {
expect(
Duration.fromRange(
new Date('2012-10-29T00:00:00-07:00'),
new Date(new Date('2012-11-05T00:00:00-08:00').valueOf() - 1000),
TZ_LA,
).toString(),
).toEqual('P6DT24H59M59S');
expect(
Duration.fromRange(
new Date('2012-01-01T00:00:00-08:00'),
new Date('2013-03-04T04:05:06-08:00'),
TZ_LA,
).toString(),
).toEqual('P1Y2M3DT4H5M6S');
});
});
describe('#isFloorable', () => {
const floorable = 'P1Y P5Y P10Y P100Y P1M P2M P3M P4M P1D'.split(' ');
for (const v of floorable) {
it(`works on floorable ${v}`, () => {
expect(new Duration(v).isFloorable()).toEqual(true);
});
}
const unfloorable = 'P1Y1M P5M P2D P3D'.split(' ');
for (const v of unfloorable) {
it(`works on not floorable ${v}`, () => {
expect(new Duration(v).isFloorable()).toEqual(false);
});
}
});
describe('#floor', () => {
it('throws error if complex duration', () => {
expect(() => new Duration('P1Y2D').floor(new Date(), TZ_LA)).toThrow(
'Can not floor on a complex duration',
);
expect(() => new Duration('P3DT15H').floor(new Date(), TZ_LA)).toThrow(
'Can not floor on a complex duration',
);
expect(() => new Duration('PT5H').floor(new Date(), TZ_LA)).toThrow(
'Can not floor on a hour duration that does not divide into 24',
);
});
it('works for year', () => {
const p1y = new Duration('P1Y');
expect(p1y.floor(new Date('2013-09-29T01:02:03.456-07:00'), TZ_LA)).toEqual(
new Date('2013-01-01T00:00:00.000-08:00'),
);
});
it('works for PT2M', () => {
const pt2h = new Duration('PT2M');
expect(pt2h.floor(new Date('2013-09-29T03:03:03.456-07:00'), TZ_LA)).toEqual(
new Date('2013-09-29T03:02:00.000-07:00'),
);
});
it('works for P2H', () => {
const pt2h = new Duration('PT2H');
expect(pt2h.floor(new Date('2013-09-29T03:02:03.456-07:00'), TZ_LA)).toEqual(
new Date('2013-09-29T02:00:00.000-07:00'),
);
});
it('works for PT12H', () => {
const pt12h = new Duration('PT12H');
expect(pt12h.floor(new Date('2015-09-12T13:05:00-08:00'), TZ_JUNEAU)).toEqual(
new Date('2015-09-12T12:00:00-08:00'),
);
});
it('works for P1W', () => {
const p1w = new Duration('P1W');
expect(p1w.floor(new Date('2013-09-29T01:02:03.456-07:00'), TZ_LA)).toEqual(
new Date('2013-09-23T07:00:00.000Z'),
);
expect(p1w.floor(new Date('2013-10-03T01:02:03.456-07:00'), TZ_LA)).toEqual(
new Date('2013-09-30T00:00:00.000-07:00'),
);
});
it('works for P3M', () => {
const p3m = new Duration('P3M');
expect(p3m.floor(new Date('2013-09-29T03:02:03.456-07:00'), TZ_LA)).toEqual(
new Date('2013-07-01T00:00:00.000-07:00'),
);
expect(p3m.floor(new Date('2013-02-29T03:02:03.456-07:00'), TZ_LA)).toEqual(
new Date('2013-01-01T00:00:00.000-08:00'),
);
});
it('works for P4Y', () => {
const p4y = new Duration('P4Y');
expect(p4y.floor(new Date('2013-09-29T03:02:03.456-07:00'), TZ_LA)).toEqual(
new Date('2012-01-01T00:00:00.000-08:00'),
);
});
});
describe('#shift', () => {
it('works for weeks', () => {
let p1w = new Duration('P1W');
expect(p1w.shift(new Date('2012-10-29T00:00:00-07:00'), TZ_LA)).toEqual(
new Date('2012-11-05T00:00:00-08:00'),
);
p1w = new Duration('P1W');
expect(p1w.shift(new Date('2012-10-29T00:00:00-07:00'), TZ_LA, 2)).toEqual(
new Date('2012-11-12T00:00:00-08:00'),
);
const p2w = new Duration('P2W');
expect(p2w.shift(new Date('2012-10-29T05:16:17-07:00'), TZ_LA)).toEqual(
new Date('2012-11-12T05:16:17-08:00'),
);
});
it('works for general complex case', () => {
const pComplex = new Duration('P1Y2M3DT4H5M6S');
expect(pComplex.shift(new Date('2012-01-01T00:00:00-08:00'), TZ_LA)).toEqual(
new Date('2013-03-04T04:05:06-08:00'),
);
});
});
describe('#materialize', () => {
it('works for weeks', () => {
const p1w = new Duration('P1W');
expect(
p1w.materialize(
new Date('2012-10-29T00:00:00-07:00'),
new Date('2012-12-01T00:00:00-08:00'),
TZ_LA,
),
).toEqual([
new Date('2012-10-29T07:00:00.000Z'),
new Date('2012-11-05T08:00:00.000Z'),
new Date('2012-11-12T08:00:00.000Z'),
new Date('2012-11-19T08:00:00.000Z'),
new Date('2012-11-26T08:00:00.000Z'),
]);
expect(
p1w.materialize(
new Date('2012-10-29T00:00:00-07:00'),
new Date('2012-12-01T00:00:00-08:00'),
TZ_LA,
2,
),
).toEqual([
new Date('2012-10-29T07:00:00.000Z'),
new Date('2012-11-12T08:00:00.000Z'),
new Date('2012-11-26T08:00:00.000Z'),
]);
});
});
describe('#isAligned', () => {
it('works for weeks', () => {
const p1w = new Duration('P1W');
expect(p1w.isAligned(new Date('2012-10-29T00:00:00-07:00'), TZ_LA)).toEqual(true);
expect(p1w.isAligned(new Date('2012-10-29T00:00:00-07:00'), 'Etc/UTC')).toEqual(false);
});
});
describe('#dividesBy', () => {
const divisible = 'P5Y/P1Y P1D/P1D P1M/P1D P1W/P1D P1D/PT6H PT3H/PT1H'.split(' ');
for (const v of divisible) {
it(`works for ${v} (true)`, () => {
const p = v.split('/');
expect(new Duration(p[0]).dividesBy(new Duration(p[1]))).toEqual(true);
});
}
const undivisible = 'P1D/P1M PT5H/PT1H'.split(' ');
for (const v of undivisible) {
it(`works for ${v} (false)`, () => {
const p = v.split('/');
expect(new Duration(p[0]).dividesBy(new Duration(p[1]))).toEqual(false);
});
}
});
describe('#getCanonicalLength', () => {
it('gives back the correct canonical length', () => {
let durationStr: string;
durationStr = 'P3Y';
expect(new Duration(durationStr).getCanonicalLength()).toEqual(94608000000);
durationStr = 'P2W';
expect(new Duration(durationStr).getCanonicalLength()).toEqual(1209600000);
durationStr = 'PT5H';
expect(new Duration(durationStr).getCanonicalLength()).toEqual(18000000);
durationStr = 'P3DT15H';
expect(new Duration(durationStr).getCanonicalLength()).toEqual(313200000);
});
});
describe('#add()', () => {
it('works with a simple duration', () => {
const d1 = new Duration('P1D');
const d2 = new Duration('P1D');
expect(d1.add(d2).toString()).toEqual('P2D');
});
it('works with heterogeneous spans', () => {
const d1 = new Duration('P1D');
const d2 = new Duration('P1Y');
expect(d1.add(d2).toString()).toEqual('P1Y1D');
});
it('works with weeks', () => {
let d1 = new Duration('P1W');
let d2 = new Duration('P2W');
expect(d1.add(d2).toString()).toEqual('P3W');
d1 = new Duration('P6D');
d2 = new Duration('P1D');
expect(d1.add(d2).toString()).toEqual('P1W');
});
});
describe('#subtract()', () => {
it('works with a simple duration', () => {
const d1 = new Duration('P1DT2H');
const d2 = new Duration('PT1H');
expect(d1.subtract(d2).toString()).toEqual('P1DT1H');
});
it('works with a less simple duration', () => {
const d1 = new Duration('P1D');
const d2 = new Duration('PT1H');
expect(d1.subtract(d2).toString()).toEqual('PT23H');
});
it('works with weeks', () => {
const d1 = new Duration('P1W');
const d2 = new Duration('P1D');
expect(d1.subtract(d2).toString()).toEqual('P6D');
});
it('throws an error if result is going to be negative', () => {
const d1 = new Duration('P1D');
const d2 = new Duration('P2D');
expect(() => d1.subtract(d2)).toThrow();
});
});
describe('#multiply()', () => {
it('works with a simple duration', () => {
const d = new Duration('P1D');
expect(d.multiply(5).toString()).toEqual('P5D');
});
it('works with a less simple duration', () => {
const d = new Duration('P1DT2H');
expect(d.multiply(2).toString()).toEqual('P2DT4H');
});
it('works with weeks', () => {
const d = new Duration('P1W');
expect(d.multiply(5).toString()).toEqual('P5W');
});
it('throws an error if result is going to be negative', () => {
const d = new Duration('P1D');
expect(() => d.multiply(-1)).toThrow('Multiplier must be positive non-zero');
});
it('gets description properly', () => {
const d = new Duration('P2D');
expect(d.multiply(2).getDescription(true)).toEqual('4 Days');
});
});
describe('#getDescription()', () => {
it('gives back the correct description', () => {
let durationStr: string;
durationStr = 'P1D';
expect(new Duration(durationStr).getDescription()).toEqual('day');
durationStr = 'P1DT2H';
expect(new Duration(durationStr).getDescription()).toEqual('1 day, 2 hours');
durationStr = 'P3Y';
expect(new Duration(durationStr).getDescription()).toEqual('3 years');
durationStr = 'P2W';
expect(new Duration(durationStr).getDescription()).toEqual('2 weeks');
durationStr = 'PT5H';
expect(new Duration(durationStr).getDescription()).toEqual('5 hours');
durationStr = 'P3DT15H';
expect(new Duration(durationStr).getDescription()).toEqual('3 days, 15 hours');
durationStr = 'P3DT15H';
expect(new Duration(durationStr).getDescription(true)).toEqual('3 Days, 15 Hours');
});
});
describe('#getSingleSpan()', () => {
it('gives back the correct span', () => {
let durationStr: string;
durationStr = 'P1D';
expect(new Duration(durationStr).getSingleSpan()).toEqual('day');
durationStr = 'P3Y';
expect(new Duration(durationStr).getSingleSpan()).toEqual('year');
durationStr = 'P2W';
expect(new Duration(durationStr).getSingleSpan()).toEqual('week');
durationStr = 'PT5H';
expect(new Duration(durationStr).getSingleSpan()).toEqual('hour');
durationStr = 'P3DT15H';
expect(new Duration(durationStr).getSingleSpan()).toBeUndefined();
durationStr = 'P3DT15H';
expect(new Duration(durationStr).getSingleSpan()).toBeUndefined();
});
});
describe('#getSingleSpanValue()', () => {
it('gives back the correct span value', () => {
let durationStr: string;
durationStr = 'P1D';
expect(new Duration(durationStr).getSingleSpanValue()).toEqual(1);
durationStr = 'P3Y';
expect(new Duration(durationStr).getSingleSpanValue()).toEqual(3);
durationStr = 'P2W';
expect(new Duration(durationStr).getSingleSpanValue()).toEqual(2);
durationStr = 'PT5H';
expect(new Duration(durationStr).getSingleSpanValue()).toEqual(5);
durationStr = 'P3DT15H';
expect(new Duration(durationStr).getSingleSpanValue()).toBeUndefined();
durationStr = 'P3DT15H';
expect(new Duration(durationStr).getSingleSpanValue()).toBeUndefined();
});
});
describe('#limitToDays', () => {
it('works', () => {
expect(new Duration('P6D').limitToDays().toString()).toEqual('P6D');
expect(new Duration('P1M').limitToDays().toString()).toEqual('P30D');
expect(new Duration('P1Y').limitToDays().toString()).toEqual('P365D');
expect(new Duration('P1Y2M').limitToDays().toString()).toEqual('P425D');
});
});
});

View File

@ -1,388 +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 { second, shifters } from '../date-floor-shift-ceil/date-floor-shift-ceil';
import { capitalizeFirst, pluralIfNeeded } from '../general';
export const TZ_UTC = 'Etc/UTC';
export type DurationSpan = 'year' | 'month' | 'week' | 'day' | 'hour' | 'minute' | 'second';
const SPANS_WITH_WEEK: DurationSpan[] = [
'year',
'month',
'week',
'day',
'hour',
'minute',
'second',
];
const SPANS_WITHOUT_WEEK: DurationSpan[] = ['year', 'month', 'day', 'hour', 'minute', 'second'];
const SPANS_WITHOUT_WEEK_OR_MONTH: DurationSpan[] = ['year', 'day', 'hour', 'minute', 'second'];
const SPANS_UP_TO_DAY: DurationSpan[] = ['day', 'hour', 'minute', 'second'];
export type DurationValue = Partial<Record<DurationSpan, number>>;
const periodWeekRegExp = /^P(\d+)W$/;
const periodRegExp = /^P(?:(\d+)Y)?(?:(\d+)M)?(?:(\d+)D)?(?:T(?:(\d+)H)?(?:(\d+)M)?(?:(\d+)S)?)?$/;
// P (year ) (month ) (day ) T(hour ) (minute ) (second )
function getSpansFromString(durationStr: string): DurationValue {
const spans: DurationValue = {};
let matches: RegExpExecArray | null;
if ((matches = periodWeekRegExp.exec(durationStr))) {
spans.week = Number(matches[1]);
if (!spans.week) throw new Error('Duration can not have empty weeks');
} else if ((matches = periodRegExp.exec(durationStr))) {
const nums = matches.map(Number);
for (let i = 0; i < SPANS_WITHOUT_WEEK.length; i++) {
const span = SPANS_WITHOUT_WEEK[i];
const value = nums[i + 1];
if (value) spans[span] = value;
}
} else {
throw new Error("Can not parse duration '" + durationStr + "'");
}
return spans;
}
function getSpansFromStartEnd(start: Date, end: Date, timezone: string): DurationValue {
start = second.floor(start, timezone);
end = second.floor(end, timezone);
if (end <= start) throw new Error('start must come before end');
const spans: DurationValue = {};
let iterator: Date = start;
for (let i = 0; i < SPANS_WITHOUT_WEEK.length; i++) {
const span = SPANS_WITHOUT_WEEK[i];
let spanCount = 0;
// Shortcut
const length = end.valueOf() - iterator.valueOf();
const canonicalLength: number = shifters[span].canonicalLength;
if (length < canonicalLength / 4) continue;
const numberToFit = Math.min(0, Math.floor(length / canonicalLength) - 1);
let iteratorMove: Date;
if (numberToFit > 0) {
// try to skip by numberToFit
iteratorMove = shifters[span].shift(iterator, timezone, numberToFit);
if (iteratorMove <= end) {
spanCount += numberToFit;
iterator = iteratorMove;
}
}
while (true) {
iteratorMove = shifters[span].shift(iterator, timezone, 1);
if (iteratorMove <= end) {
iterator = iteratorMove;
spanCount++;
} else {
break;
}
}
if (spanCount) {
spans[span] = spanCount;
}
}
return spans;
}
function removeZeros(spans: DurationValue): DurationValue {
const newSpans: DurationValue = {};
for (let i = 0; i < SPANS_WITH_WEEK.length; i++) {
const span = SPANS_WITH_WEEK[i];
if (Number(spans[span]) > 0) {
newSpans[span] = spans[span];
}
}
return newSpans;
}
function fitIntoSpans(length: number, spansToCheck: DurationSpan[]): DurationValue {
const spans: DurationValue = {};
let lengthLeft = length;
for (let i = 0; i < spansToCheck.length; i++) {
const span = spansToCheck[i];
const spanLength = shifters[span].canonicalLength;
const count = Math.floor(lengthLeft / spanLength);
if (count) {
lengthLeft -= spanLength * count;
spans[span] = count;
}
}
return spans;
}
/**
* Represents an ISO duration like P1DT3H
*/
export class Duration {
public readonly singleSpan?: DurationSpan;
public readonly spans: Readonly<DurationValue>;
static fromCanonicalLength(length: number, skipMonths = false): Duration {
if (length <= 0) throw new Error('length must be positive');
let spans = fitIntoSpans(length, skipMonths ? SPANS_WITHOUT_WEEK_OR_MONTH : SPANS_WITHOUT_WEEK);
if (
length % shifters['week'].canonicalLength === 0 && // Weeks fits
(Object.keys(spans).length > 1 || // We already have a more complex span
spans['day']) // or... we only have days and it might be simpler to express as weeks
) {
spans = { week: length / shifters['week'].canonicalLength };
}
return new Duration(spans);
}
static fromCanonicalLengthUpToDays(length: number): Duration {
if (length <= 0) throw new Error('length must be positive');
return new Duration(fitIntoSpans(length, SPANS_UP_TO_DAY));
}
static fromRange(start: Date, end: Date, timezone: string): Duration {
return new Duration(getSpansFromStartEnd(start, end, timezone));
}
static pickSmallestGranularityThatFits(
granularities: Duration[],
span: number,
maxEntities: number,
): Duration {
for (const granularity of granularities) {
if (span / granularity.getCanonicalLength() < maxEntities) return granularity;
}
return granularities[granularities.length - 1];
}
constructor(spans: DurationValue | string) {
const effectiveSpans: DurationValue =
typeof spans === 'string' ? getSpansFromString(spans) : removeZeros(spans);
const usedSpans = Object.keys(effectiveSpans) as DurationSpan[];
if (!usedSpans.length) throw new Error('Duration can not be empty');
if (usedSpans.length === 1) {
this.singleSpan = usedSpans[0];
} else if (effectiveSpans.week) {
throw new Error("Can not mix 'week' and other spans");
}
this.spans = effectiveSpans;
}
public toString() {
const strArr: string[] = ['P'];
const spans = this.spans;
if (spans.week) {
strArr.push(String(spans.week), 'W');
} else {
let addedT = false;
for (let i = 0; i < SPANS_WITHOUT_WEEK.length; i++) {
const span = SPANS_WITHOUT_WEEK[i];
const value = spans[span];
if (!value) continue;
if (!addedT && i >= 3) {
strArr.push('T');
addedT = true;
}
strArr.push(String(value), span[0].toUpperCase());
}
}
return strArr.join('');
}
public add(duration: Duration): Duration {
return Duration.fromCanonicalLength(this.getCanonicalLength() + duration.getCanonicalLength());
}
public subtract(duration: Duration): Duration {
const newCanonicalDuration = this.getCanonicalLength() - duration.getCanonicalLength();
if (newCanonicalDuration < 0) throw new Error('A duration can not be negative.');
return Duration.fromCanonicalLength(newCanonicalDuration);
}
public multiply(multiplier: number): Duration {
if (multiplier <= 0) throw new Error('Multiplier must be positive non-zero');
if (multiplier === 1) return this;
const newCanonicalDuration = this.getCanonicalLength() * multiplier;
return Duration.fromCanonicalLength(newCanonicalDuration);
}
public valueOf() {
return this.spans;
}
public equals(other: Duration | undefined): boolean {
return other instanceof Duration && this.toString() === other.toString();
}
public isSimple(): boolean {
const { singleSpan } = this;
if (!singleSpan) return false;
return this.spans[singleSpan] === 1;
}
public isFloorable(): boolean {
const { singleSpan } = this;
if (!singleSpan) return false;
const span = Number(this.spans[singleSpan]);
if (span === 1) return true;
const { siblings } = shifters[singleSpan];
if (!siblings) return false;
return siblings % span === 0;
}
/**
* Floors the date according to this duration.
* @param date The date to floor
* @param timezone The timezone within which to floor
*/
public floor(date: Date, timezone: string): Date {
const { singleSpan } = this;
if (!singleSpan) throw new Error('Can not floor on a complex duration');
const span = this.spans[singleSpan]!;
const mover = shifters[singleSpan];
let dt = mover.floor(date, timezone);
if (span !== 1) {
if (!mover.siblings) {
throw new Error(`Can not floor on a ${singleSpan} duration that is not 1`);
}
if (mover.siblings % span !== 0) {
throw new Error(
`Can not floor on a ${singleSpan} duration that does not divide into ${mover.siblings}`,
);
}
dt = mover.round(dt, span, timezone);
}
return dt;
}
/**
* Moves the given date by 'step' times of the duration
* Negative step value will move back in time.
* @param date The date to move
* @param timezone The timezone within which to make the move
* @param step The number of times to step by the duration
*/
public shift(date: Date, timezone: string, step = 1): Date {
const spans = this.spans;
for (const span of SPANS_WITH_WEEK) {
const value = spans[span];
if (value) date = shifters[span].shift(date, timezone, step * value);
}
return date;
}
public ceil(date: Date, timezone: string): Date {
const floored = this.floor(date, timezone);
if (floored.valueOf() === date.valueOf()) return date; // Just like ceil(3) is 3 and not 4
return this.shift(floored, timezone, 1);
}
public round(date: Date, timezone: string): Date {
const floorDate = this.floor(date, timezone);
const ceilDate = this.ceil(date, timezone);
const distanceToFloor = Math.abs(date.valueOf() - floorDate.valueOf());
const distanceToCeil = Math.abs(date.valueOf() - ceilDate.valueOf());
return distanceToFloor < distanceToCeil ? floorDate : ceilDate;
}
/**
* Materializes all the values of this duration form start to end
* @param start The date to start on
* @param end The date to start on
* @param timezone The timezone within which to materialize
* @param step The number of times to step by the duration
*/
public materialize(start: Date, end: Date, timezone: string, step = 1): Date[] {
const values: Date[] = [];
let iter = this.floor(start, timezone);
while (iter <= end) {
values.push(iter);
iter = this.shift(iter, timezone, step);
}
return values;
}
/**
* Checks to see if date is aligned to this duration within the timezone (floors to itself)
* @param date The date to check
* @param timezone The timezone within which to make the check
*/
public isAligned(date: Date, timezone: string): boolean {
return this.floor(date, timezone).valueOf() === date.valueOf();
}
/**
* Check to see if this duration can be divided by the given duration
* @param smaller The smaller duration to divide by
*/
public dividesBy(smaller: Duration): boolean {
const myCanonicalLength = this.getCanonicalLength();
const smallerCanonicalLength = smaller.getCanonicalLength();
return (
myCanonicalLength % smallerCanonicalLength === 0 &&
this.isFloorable() &&
smaller.isFloorable()
);
}
public getCanonicalLength(): number {
const spans = this.spans;
let length = 0;
for (const span of SPANS_WITH_WEEK) {
const value = spans[span];
if (value) length += value * shifters[span].canonicalLength;
}
return length;
}
public getDescription(capitalize?: boolean): string {
const spans = this.spans;
const description: string[] = [];
for (const span of SPANS_WITH_WEEK) {
const value = spans[span];
const spanTitle = capitalize ? capitalizeFirst(span) : span;
if (value) {
if (value === 1 && this.singleSpan) {
description.push(spanTitle);
} else {
description.push(pluralIfNeeded(value, spanTitle));
}
}
}
return description.join(', ');
}
public getSingleSpan(): string | undefined {
return this.singleSpan;
}
public getSingleSpanValue(): number | undefined {
if (!this.singleSpan) return;
return this.spans[this.singleSpan];
}
public limitToDays(): Duration {
return Duration.fromCanonicalLengthUpToDays(this.getCanonicalLength());
}
}

View File

@ -29,8 +29,8 @@ import {
hashJoaat,
moveElement,
moveToIndex,
objectHash,
offsetToRowColumn,
OVERLAY_OPEN_SELECTOR,
parseCsvLine,
swapElements,
} from './general';
@ -178,6 +178,12 @@ describe('general', () => {
});
});
describe('objectHash', () => {
it('works', () => {
expect(objectHash({ hello: 'world1' })).toEqual('cc14ad13');
});
});
describe('offsetToRowColumn', () => {
it('works', () => {
const str = 'Hello\nThis is a test\nstring.';
@ -211,10 +217,4 @@ describe('general', () => {
expect(caseInsensitiveEquals(undefined, '')).toEqual(false);
});
});
describe('OVERLAY_OPEN_SELECTOR', () => {
it('is what it is', () => {
expect(OVERLAY_OPEN_SELECTOR).toEqual('.bp5-portal .bp5-overlay-open');
});
});
});

View File

@ -59,10 +59,6 @@ export function isSimpleArray(a: any): a is (string | number | boolean)[] {
);
}
export function arraysEqualByElement<T>(xs: T[], ys: T[]): boolean {
return xs.length === ys.length && xs.every((x, i) => x === ys[i]);
}
export function wait(ms: number): Promise<void> {
return new Promise(resolve => {
setTimeout(resolve, ms);
@ -255,14 +251,6 @@ export function uniq(array: readonly string[]): string[] {
});
}
export function allSameValue<T>(xs: readonly T[]): T | undefined {
const sameValue: T | undefined = xs[0];
for (let i = 1; i < xs.length; i++) {
if (sameValue !== xs[i]) return;
}
return sameValue;
}
// ----------------------------
export function formatEmpty(str: string): string {
@ -385,14 +373,6 @@ export function formatDurationHybrid(ms: NumberLike): string {
}
}
export function timezoneOffsetInMinutesToString(offsetInMinutes: number, padHour: boolean): string {
const sign = offsetInMinutes < 0 ? '-' : '+';
const absOffset = Math.abs(offsetInMinutes);
const h = Math.floor(absOffset / 60);
const m = absOffset % 60;
return `${sign}${padHour ? pad2(h) : h}:${pad2(m)}`;
}
function pluralize(word: string): string {
// Ignoring irregular plurals.
if (/(s|x|z|ch|sh)$/.test(word)) {
@ -632,10 +612,12 @@ export function hashJoaat(str: string): number {
return (hash & 4294967295) >>> 0;
}
export const OVERLAY_OPEN_SELECTOR = `.${Classes.PORTAL} .${Classes.OVERLAY_OPEN}`;
export function objectHash(obj: any): string {
return hashJoaat(JSONBig.stringify(obj)).toString(16).padStart(8);
}
export function hasOverlayOpen(): boolean {
return Boolean(document.querySelector(OVERLAY_OPEN_SELECTOR));
export function hasPopoverOpen(): boolean {
return Boolean(document.querySelector(`${Classes.PORTAL} ${Classes.OVERLAY} ${Classes.POPOVER}`));
}
export function checkedCircleIcon(checked: boolean): IconName {

View File

@ -19,12 +19,10 @@
export * from './base64-url';
export * from './column-metadata';
export * from './date';
export * from './date-floor-shift-ceil/date-floor-shift-ceil';
export * from './download';
export * from './download-query-detail-archive';
export * from './druid-lookup';
export * from './druid-query';
export * from './duration/duration';
export * from './formatter';
export * from './general';
export * from './local-storage-backed-visibility';

View File

@ -18,7 +18,7 @@
import { sum } from 'd3-array';
import { deepMove, deepSet } from '../../utils';
import { deepMove, deepSet } from './object-change';
export type RuleType =
| 'loadForever'
@ -41,7 +41,6 @@ export interface Rule {
}
export class RuleUtil {
static DEFAULT_RULES_KEY = '_default';
static TYPES: RuleType[] = [
'loadForever',
'loadByInterval',

View File

@ -16,6 +16,6 @@
* limitations under the License.
*/
import type { SqlQuery } from 'druid-query-toolkit';
import type { SqlQuery } from '@druid-toolkit/query';
export type QueryAction = (query: SqlQuery) => SqlQuery;

View File

@ -288,7 +288,7 @@ export class QueryManager<Q, R, I = never, E extends Error = Error> {
return this.lastQuery;
}
public getLastIntermediateQuery(): unknown {
public getLastIntermediateQuery(): any {
return this.lastIntermediateQuery;
}

View File

@ -16,8 +16,8 @@
* limitations under the License.
*/
import { dedupe, F, SqlExpression, SqlFunction } from '@druid-toolkit/query';
import type { CancelToken } from 'axios';
import { dedupe, F, SqlExpression, SqlFunction } from 'druid-query-toolkit';
import * as JSONBig from 'json-bigint-native';
import type {

View File

@ -16,7 +16,7 @@
* limitations under the License.
*/
import { sane } from 'druid-query-toolkit';
import { sane } from '@druid-toolkit/query';
import { findAllSqlQueriesInText, findSqlQueryPrefix } from './sql';

View File

@ -16,7 +16,7 @@
* limitations under the License.
*/
import type { SqlBase } from 'druid-query-toolkit';
import type { SqlBase } from '@druid-toolkit/query';
import {
SqlColumn,
SqlExpression,
@ -24,7 +24,7 @@ import {
SqlLiteral,
SqlQuery,
SqlStar,
} from 'druid-query-toolkit';
} from '@druid-toolkit/query';
import type { RowColumn } from './general';
import { offsetToRowColumn } from './general';

View File

@ -16,10 +16,9 @@
* limitations under the License.
*/
import { ascending, descending, sort } from 'd3-array';
import type { QueryResult, SqlExpression } from 'druid-query-toolkit';
import { C } from 'druid-query-toolkit';
import type { Filter, SortingRule } from 'react-table';
import type { QueryResult, SqlExpression } from '@druid-toolkit/query';
import { C } from '@druid-toolkit/query';
import type { Filter } from 'react-table';
import { filterMap, formatNumber, isNumberLike, oneOf } from './general';
import { deepSet } from './object-change';
@ -68,24 +67,19 @@ export function getNumericColumnBraces(
return numericColumnBraces;
}
export interface Sorted {
id: string;
desc: boolean;
}
export interface TableState {
page: number;
pageSize: number;
filtered: Filter[];
sorted: SortingRule[];
sorted: Sorted[];
}
export function sortedToOrderByClause(sorted: SortingRule[]): string | undefined {
export function sortedToOrderByClause(sorted: Sorted[]): string | undefined {
if (!sorted.length) return;
return 'ORDER BY ' + sorted.map(sort => `${C(sort.id)} ${sort.desc ? 'DESC' : 'ASC'}`).join(', ');
}
export function applySorting(xs: any[], sorted: SortingRule[]): any[] {
const firstSortingRule = sorted[0];
if (!firstSortingRule) return xs;
const { id, desc } = firstSortingRule;
return sort(
xs,
desc ? (d1, d2) => descending(d1[id], d2[id]) : (d1, d2) => ascending(d1[id], d2[id]),
);
}

View File

@ -18,7 +18,7 @@
import type { IconName } from '@blueprintjs/core';
import { IconNames } from '@blueprintjs/icons';
import type { Column } from 'druid-query-toolkit';
import type { Column } from '@druid-toolkit/query';
export function columnToSummary(column: Column): string {
const lines: string[] = [column.name];

View File

@ -16,7 +16,7 @@
* limitations under the License.
*/
import { QueryResult, sane } from 'druid-query-toolkit';
import { QueryResult, sane } from '@druid-toolkit/query';
import { queryResultToValuesQuery } from './values-query';

View File

@ -16,7 +16,7 @@
* limitations under the License.
*/
import type { Column, QueryResult, SqlExpression } from 'druid-query-toolkit';
import type { Column, QueryResult, SqlExpression } from '@druid-toolkit/query';
import {
C,
F,
@ -28,7 +28,7 @@ import {
SqlRecord,
SqlType,
SqlValues,
} from 'druid-query-toolkit';
} from '@druid-toolkit/query';
import * as JSONBig from 'json-bigint-native';
import { oneOf } from './general';

View File

@ -109,7 +109,6 @@ exports[`DatasourcesView matches snapshot 1`] = `
/>
</Memo(ViewControlBar)>
<SplitterLayout
className="timeline-datasources-splitter"
percentage={true}
primaryIndex={1}
primaryMinSize={20}

View File

@ -23,7 +23,7 @@
width: 100%;
overflow: auto;
.splitter-layout.timeline-datasources-splitter {
.splitter-layout {
position: absolute;
top: $view-control-bar-height + $standard-padding;
bottom: 0;
@ -45,13 +45,6 @@
.ReactTable {
@include pin-full;
.datasource-color-tab {
display: inline-block;
width: 10px;
height: 20px;
border-radius: 3px;
}
.clickable-cell {
cursor: pointer;
}

View File

@ -16,10 +16,10 @@
* limitations under the License.
*/
import { Button, FormGroup, InputGroup, Intent, MenuItem, Switch, Tag } from '@blueprintjs/core';
import { FormGroup, InputGroup, Intent, MenuItem, Switch, Tag } from '@blueprintjs/core';
import { IconNames } from '@blueprintjs/icons';
import { SqlQuery, T } from '@druid-toolkit/query';
import { sum } from 'd3-array';
import { SqlQuery, T } from 'druid-query-toolkit';
import React from 'react';
import type { Filter } from 'react-table';
import ReactTable from 'react-table';
@ -51,16 +51,8 @@ import type {
CompactionInfo,
CompactionStatus,
QueryWithContext,
Rule,
} from '../../druid-models';
import {
END_OF_TIME_DATE,
formatCompactionInfo,
getDatasourceColor,
RuleUtil,
START_OF_TIME_DATE,
zeroCompactionStatus,
} from '../../druid-models';
import { formatCompactionInfo, zeroCompactionStatus } from '../../druid-models';
import type { Capabilities, CapabilitiesMode } from '../../helpers';
import { STANDARD_TABLE_PAGE_SIZE, STANDARD_TABLE_PAGE_SIZE_OPTIONS } from '../../react-table';
import { Api, AppToaster } from '../../singletons';
@ -74,10 +66,9 @@ import {
formatInteger,
formatMillions,
formatPercent,
getApiArray,
getDruidErrorMessage,
groupByAsMap,
hasOverlayOpen,
hasPopoverOpen,
isNumberLikeNaN,
LocalStorageBackedVisibility,
LocalStorageKeys,
@ -91,6 +82,8 @@ import {
twoLines,
} from '../../utils';
import type { BasicAction } from '../../utils/basic-action';
import type { Rule } from '../../utils/load-rule';
import { RuleUtil } from '../../utils/load-rule';
import './datasources-view.scss';
@ -138,6 +131,8 @@ const TABLE_COLUMNS_BY_MODE: Record<CapabilitiesMode, TableColumnSelectorColumn[
],
};
const DEFAULT_RULES_KEY = '_default';
function formatLoadDrop(segmentsToLoad: NumberLike, segmentsToDrop: NumberLike): string {
const loadDrop: string[] = [];
if (segmentsToLoad) {
@ -245,7 +240,6 @@ interface DatasourcesAndDefaultRules {
interface RetentionDialogOpenOn {
readonly datasource: string;
readonly rules: Rule[];
readonly defaultRules: Rule[];
}
interface CompactionConfigDialogOpenOn {
@ -305,12 +299,7 @@ export interface DatasourcesViewProps {
onFiltersChange(filters: Filter[]): void;
goToQuery(queryWithContext: QueryWithContext): void;
goToTasks(datasource?: string): void;
goToSegments(options: {
start?: Date;
end?: Date;
datasource?: string;
realtime?: boolean;
}): void;
goToSegments(datasource: string, onlyUnavailable?: boolean): void;
capabilities: Capabilities;
}
@ -328,7 +317,7 @@ export interface DatasourcesViewState {
useUnuseInterval: string;
showForceCompact: boolean;
visibleColumns: LocalStorageBackedVisibility;
showSegmentTimeline?: { capabilities: Capabilities; datasource?: string };
showSegmentTimeline: boolean;
datasourceTableActionDialogId?: string;
actions: BasicAction[];
@ -373,7 +362,7 @@ export class DatasourcesView extends React.PureComponent<
`COUNT(*) FILTER (WHERE is_active = 1 AND "start" LIKE '%T00:00:00.000Z' AND "end" LIKE '%T00:00:00.000Z') AS day_aligned_segments`,
`COUNT(*) FILTER (WHERE is_active = 1 AND "start" LIKE '%-01T00:00:00.000Z' AND "end" LIKE '%-01T00:00:00.000Z') AS month_aligned_segments`,
`COUNT(*) FILTER (WHERE is_active = 1 AND "start" LIKE '%-01-01T00:00:00.000Z' AND "end" LIKE '%-01-01T00:00:00.000Z') AS year_aligned_segments`,
`COUNT(*) FILTER (WHERE is_active = 1 AND "start" = '${START_OF_TIME_DATE}' AND "end" = '${END_OF_TIME_DATE}') AS all_granularity_segments`,
`COUNT(*) FILTER (WHERE is_active = 1 AND "start" = '-146136543-09-08T08:23:32.096Z' AND "end" = '146140482-04-24T15:36:27.903Z') AS all_granularity_segments`,
],
visibleColumns.shown('Total rows') &&
`SUM("num_rows") FILTER (WHERE is_active = 1) AS total_rows`,
@ -429,6 +418,7 @@ GROUP BY 1, 2`;
LocalStorageKeys.DATASOURCE_TABLE_COLUMN_SELECTION,
['Segment size', 'Segment granularity'],
),
showSegmentTimeline: false,
actions: [],
};
@ -445,15 +435,15 @@ GROUP BY 1, 2`;
setIntermediateQuery(query);
datasources = await queryDruidSql({ query }, cancelToken);
} else if (capabilities.hasCoordinatorAccess()) {
const datasourcesResp = await getApiArray(
const datasourcesResp = await Api.instance.get(
'/druid/coordinator/v1/datasources?simple',
cancelToken,
{ cancelToken },
);
const loadstatusResp = await Api.instance.get('/druid/coordinator/v1/loadstatus?simple', {
cancelToken,
});
const loadstatus = loadstatusResp.data;
datasources = datasourcesResp.map((d: any): DatasourceQueryResultRow => {
datasources = datasourcesResp.data.map((d: any): DatasourceQueryResultRow => {
const totalDataSize = deepGet(d, 'properties.segments.size') || -1;
const segmentsToLoad = Number(loadstatus[d.name] || 0);
const availableSegments = Number(deepGet(d, 'properties.segments.count'));
@ -531,10 +521,9 @@ GROUP BY 1, 2`;
if (capabilities.hasOverlordAccess()) {
auxiliaryQueries.push(async (datasourcesAndDefaultRules, cancelToken) => {
try {
const taskList = await getApiArray(
`/druid/indexer/v1/tasks?state=running`,
cancelToken,
);
const taskList = (
await Api.instance.get(`/druid/indexer/v1/tasks?state=running`, { cancelToken })
).data;
const runningTasksByDatasource = groupByAsMap(
taskList,
@ -573,10 +562,10 @@ GROUP BY 1, 2`;
if (showUnused) {
try {
unused = (
await getApiArray<string>(
await Api.instance.get<string[]>(
'/druid/coordinator/v1/metadata/datasources?includeUnused',
)
).filter(d => !seen[d]);
).data.filter(d => !seen[d]);
} catch {
AppToaster.show({
icon: IconNames.ERROR,
@ -589,7 +578,7 @@ GROUP BY 1, 2`;
// Rules
auxiliaryQueries.push(async (datasourcesAndDefaultRules, cancelToken) => {
try {
const rules = (
const rules: Record<string, Rule[]> = (
await Api.instance.get<Record<string, Rule[]>>('/druid/coordinator/v1/rules', {
cancelToken,
})
@ -600,7 +589,7 @@ GROUP BY 1, 2`;
...ds,
rules: rules[ds.datasource] || [],
})),
defaultRules: rules[RuleUtil.DEFAULT_RULES_KEY],
defaultRules: rules[DEFAULT_RULES_KEY],
};
} catch {
AppToaster.show({
@ -668,19 +657,8 @@ GROUP BY 1, 2`;
}
private readonly refresh = (auto: boolean): void => {
if (auto && hasOverlayOpen()) return;
if (auto && hasPopoverOpen()) return;
this.datasourceQueryManager.rerunLastQuery(auto);
const { showSegmentTimeline } = this.state;
if (showSegmentTimeline) {
// Create a new capabilities object to force the segment timeline to re-render
this.setState(({ showSegmentTimeline }) => ({
showSegmentTimeline: {
...showSegmentTimeline,
capabilities: this.props.capabilities.clone(),
},
}));
}
};
private fetchDatasourceData() {
@ -868,9 +846,9 @@ GROUP BY 1, 2`;
<MenuItem
icon={IconNames.APPLICATION}
text="View SQL query for table"
disabled={typeof lastDatasourcesQuery !== 'string'}
disabled={!lastDatasourcesQuery}
onClick={() => {
if (typeof lastDatasourcesQuery !== 'string') return;
if (!lastDatasourcesQuery) return;
goToQuery({ queryString: lastDatasourcesQuery });
}}
/>
@ -943,7 +921,6 @@ GROUP BY 1, 2`;
retentionDialogOpenOn: {
datasource: '_default',
rules: defaultRules,
defaultRules,
},
};
});
@ -1053,13 +1030,10 @@ GROUP BY 1, 2`;
icon: IconNames.AUTOMATIC_UPDATES,
title: 'Edit retention rules',
onAction: () => {
const defaultRules = this.state.datasourcesAndDefaultRulesState.data?.defaultRules;
if (!defaultRules) return;
this.setState({
retentionDialogOpenOn: {
datasource,
rules: rules || [],
defaultRules,
},
});
},
@ -1123,8 +1097,9 @@ GROUP BY 1, 2`;
private renderRetentionDialog() {
const { capabilities } = this.props;
const { retentionDialogOpenOn } = this.state;
if (!retentionDialogOpenOn) return;
const { retentionDialogOpenOn, datasourcesAndDefaultRulesState } = this.state;
const defaultRules = datasourcesAndDefaultRulesState.data?.defaultRules;
if (!retentionDialogOpenOn || !defaultRules) return;
return (
<RetentionDialog
@ -1132,7 +1107,7 @@ GROUP BY 1, 2`;
rules={retentionDialogOpenOn.rules}
capabilities={capabilities}
onEditDefaults={this.editDefaultRules}
defaultRules={retentionDialogOpenOn.defaultRules}
defaultRules={defaultRules}
onCancel={() => this.setState({ retentionDialogOpenOn: undefined })}
onSave={this.saveRules}
/>
@ -1164,9 +1139,8 @@ GROUP BY 1, 2`;
}
private renderDatasourcesTable() {
const { goToTasks, capabilities, filters, onFiltersChange } = this.props;
const { datasourcesAndDefaultRulesState, showUnused, visibleColumns, showSegmentTimeline } =
this.state;
const { goToSegments, goToTasks, capabilities, filters, onFiltersChange } = this.props;
const { datasourcesAndDefaultRulesState, showUnused, visibleColumns } = this.state;
let { datasources, defaultRules } = datasourcesAndDefaultRulesState.data || { datasources: [] };
@ -1220,19 +1194,12 @@ GROUP BY 1, 2`;
show: visibleColumns.shown('Datasource name'),
accessor: 'datasource',
width: 150,
Cell: ({ value, original }) => (
Cell: row => (
<TableClickableCell
onClick={() => this.onDetail(original)}
onClick={() => this.onDetail(row.original)}
hoverIcon={IconNames.SEARCH_TEMPLATE}
tooltip="Show detail"
>
{showSegmentTimeline ? (
<>
<span style={{ color: getDatasourceColor(value) }}>&#9632;</span> {value}
</>
) : (
value
)}
{row.value}
</TableClickableCell>
),
},
@ -1258,12 +1225,7 @@ GROUP BY 1, 2`;
const hasZeroReplicationRule = RuleUtil.hasZeroReplicaRule(rules, defaultRules);
const descriptor = hasZeroReplicationRule ? 'pre-cached' : 'available';
const segmentsEl = (
<a
onClick={() =>
this.setState({ showSegmentTimeline: { capabilities, datasource } })
}
data-tooltip="Show in segment timeline"
>
<a onClick={() => goToSegments(datasource)}>
{pluralIfNeeded(num_segments, 'segment')}
</a>
);
@ -1356,7 +1318,7 @@ GROUP BY 1, 2`;
<TableClickableCell
onClick={() => goToTasks(original.datasource)}
hoverIcon={IconNames.ARROW_TOP_RIGHT}
tooltip="Go to tasks"
title="Go to tasks"
>
{formatRunningTasks(runningTasks)}
</TableClickableCell>
@ -1536,7 +1498,6 @@ GROUP BY 1, 2`;
if (!compaction) return;
return (
<TableClickableCell
tooltip="Open compaction configuration"
disabled={!compaction}
onClick={() => {
if (!compaction) return;
@ -1653,7 +1614,6 @@ GROUP BY 1, 2`;
return (
<TableClickableCell
tooltip="Open retention (load rule) configuration"
disabled={!defaultRules}
onClick={() => {
if (!defaultRules) return;
@ -1661,7 +1621,6 @@ GROUP BY 1, 2`;
retentionDialogOpenOn: {
datasource,
rules,
defaultRules,
},
});
}}
@ -1709,7 +1668,7 @@ GROUP BY 1, 2`;
}
render() {
const { capabilities, goToSegments } = this.props;
const { capabilities } = this.props;
const {
showUnused,
visibleColumns,
@ -1722,7 +1681,9 @@ GROUP BY 1, 2`;
<div className="datasources-view app-view">
<ViewControlBar label="Datasources">
<RefreshButton
onRefresh={this.refresh}
onRefresh={auto => {
this.refresh(auto);
}}
localStorageKey={LocalStorageKeys.DATASOURCES_REFRESH_RATE}
/>
{this.renderBulkDatasourceActions()}
@ -1733,13 +1694,9 @@ GROUP BY 1, 2`;
disabled={!capabilities.hasCoordinatorAccess()}
/>
<Switch
checked={Boolean(showSegmentTimeline)}
checked={showSegmentTimeline}
label="Show segment timeline"
onChange={() =>
this.setState({
showSegmentTimeline: showSegmentTimeline ? undefined : { capabilities },
})
}
onChange={() => this.setState({ showSegmentTimeline: !showSegmentTimeline })}
disabled={!capabilities.hasSqlOrCoordinatorAccess()}
/>
<TableColumnSelector
@ -1757,7 +1714,6 @@ GROUP BY 1, 2`;
/>
</ViewControlBar>
<SplitterLayout
className="timeline-datasources-splitter"
vertical
percentage
secondaryInitialSize={35}
@ -1765,22 +1721,7 @@ GROUP BY 1, 2`;
primaryMinSize={20}
secondaryMinSize={10}
>
{showSegmentTimeline && (
<SegmentTimeline
capabilities={showSegmentTimeline.capabilities}
datasource={showSegmentTimeline.datasource}
getIntervalActionButton={(start, end, datasource, realtime) => {
return (
<Button
text="Open in segments view"
small
rightIcon={IconNames.ARROW_TOP_RIGHT}
onClick={() => goToSegments({ start, end, datasource, realtime })}
/>
);
}}
/>
)}
{showSegmentTimeline && <SegmentTimeline capabilities={capabilities} />}
{this.renderDatasourcesTable()}
</SplitterLayout>
{datasourceTableActionDialogId && (

View File

@ -19,8 +19,8 @@
import type { IconName } from '@blueprintjs/core';
import { Icon, InputGroup, Menu, MenuItem } from '@blueprintjs/core';
import { IconNames } from '@blueprintjs/icons';
import type { Column } from '@druid-toolkit/query';
import classNames from 'classnames';
import type { Column } from 'druid-query-toolkit';
import { useState } from 'react';
import { caseInsensitiveContains, columnToIcon, filterMap } from '../../../../utils';

View File

@ -18,7 +18,7 @@
import { Button, Menu, MenuItem, Popover, Position } from '@blueprintjs/core';
import { IconNames } from '@blueprintjs/icons';
import type { Column } from 'druid-query-toolkit';
import type { Column } from '@druid-toolkit/query';
import React, { useState } from 'react';
import { columnToIcon } from '../../../../utils';

View File

@ -27,8 +27,8 @@ import {
Tag,
} from '@blueprintjs/core';
import { IconNames } from '@blueprintjs/icons';
import type { Column, SqlExpression } from 'druid-query-toolkit';
import { SqlColumn } from 'druid-query-toolkit';
import type { Column, SqlExpression } from '@druid-toolkit/query';
import { SqlColumn } from '@druid-toolkit/query';
import type { JSX } from 'react';
import {

View File

@ -29,8 +29,8 @@ import {
Position,
} from '@blueprintjs/core';
import { IconNames } from '@blueprintjs/icons';
import type { Column } from 'druid-query-toolkit';
import { SqlColumn, SqlExpression } from 'druid-query-toolkit';
import type { Column } from '@druid-toolkit/query';
import { SqlColumn, SqlExpression } from '@druid-toolkit/query';
import { useState } from 'react';
import { AppToaster } from '../../../../singletons';

View File

@ -30,8 +30,8 @@ import {
Position,
} from '@blueprintjs/core';
import { IconNames } from '@blueprintjs/icons';
import type { Column } from 'druid-query-toolkit';
import { SqlAlias, SqlExpression } from 'druid-query-toolkit';
import type { Column } from '@druid-toolkit/query';
import { SqlAlias, SqlExpression } from '@druid-toolkit/query';
import { useState } from 'react';
import { AppToaster } from '../../../../singletons';

View File

@ -16,8 +16,8 @@
* limitations under the License.
*/
import type { Column } from '@druid-toolkit/query';
import classNames from 'classnames';
import type { Column } from 'druid-query-toolkit';
import type React from 'react';
import { forwardRef, useState } from 'react';

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