mirror of https://github.com/apache/druid.git
This reverts commit 09432c099b
.
This commit is contained in:
parent
09432c099b
commit
f3e1f1e586
|
@ -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
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -18,8 +18,6 @@
|
|||
|
||||
const { createJsWithTsPreset } = require('ts-jest');
|
||||
|
||||
process.env.TZ = 'UTC';
|
||||
|
||||
module.exports = {
|
||||
testEnvironment: 'jsdom',
|
||||
transformIgnorePatterns: ['/node_modules/(?!(d3-.+)/)'],
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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() {
|
||||
|
|
|
@ -170,6 +170,7 @@ exports[`AutoForm matches snapshot 1`] = `
|
|||
</Memo(FormGroupWithInfo)>
|
||||
<Blueprint5.FormGroup>
|
||||
<Blueprint5.Button
|
||||
fill={true}
|
||||
minimal={true}
|
||||
onClick={[Function]}
|
||||
rightIcon="chevron-down"
|
||||
|
|
|
@ -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 }));
|
||||
}}
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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,
|
||||
);
|
||||
};
|
|
@ -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';
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -28,8 +28,4 @@
|
|||
.include-future {
|
||||
margin-left: 15px;
|
||||
}
|
||||
|
||||
.rule-detail {
|
||||
border: 1px solid rgba(255, 255, 255, 0.15);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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>
|
||||
`;
|
|
@ -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>
|
||||
`;
|
||||
|
|
|
@ -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)}
|
||||
/>
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
});
|
||||
});
|
|
@ -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}
|
||||
/>
|
||||
);
|
||||
}
|
|
@ -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)}
|
||||
/>
|
||||
);
|
||||
};
|
||||
});
|
||||
|
|
|
@ -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);
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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>
|
||||
);
|
||||
};
|
|
@ -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 />}
|
||||
</>
|
||||
);
|
||||
};
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
});
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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>
|
||||
);
|
||||
});
|
|
@ -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)));
|
||||
},
|
||||
});
|
||||
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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,
|
||||
]),
|
||||
);
|
||||
};
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
},
|
||||
});
|
||||
|
||||
|
|
|
@ -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';
|
||||
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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';
|
||||
|
||||
|
|
|
@ -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;
|
||||
},
|
||||
});
|
||||
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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;
|
||||
},
|
||||
});
|
||||
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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;
|
||||
},
|
||||
});
|
||||
|
||||
|
|
|
@ -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;
|
||||
},
|
||||
});
|
||||
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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';
|
||||
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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[];
|
||||
}
|
|
@ -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';
|
||||
|
||||
|
|
|
@ -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';
|
||||
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -56,7 +56,6 @@ body {
|
|||
position: absolute;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
z-index: 0;
|
||||
|
||||
.console-application {
|
||||
position: absolute;
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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' },
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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] };
|
||||
});
|
||||
|
|
|
@ -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));
|
||||
});
|
||||
});
|
|
@ -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));
|
||||
});
|
||||
});
|
|
@ -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,
|
||||
};
|
|
@ -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'),
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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';
|
||||
|
||||
|
|
|
@ -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';
|
||||
|
||||
|
|
|
@ -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 }[];
|
||||
|
|
|
@ -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');
|
||||
});
|
||||
});
|
||||
});
|
|
@ -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());
|
||||
}
|
||||
}
|
|
@ -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');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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',
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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';
|
||||
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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]),
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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];
|
||||
|
|
|
@ -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';
|
||||
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -109,7 +109,6 @@ exports[`DatasourcesView matches snapshot 1`] = `
|
|||
/>
|
||||
</Memo(ViewControlBar)>
|
||||
<SplitterLayout
|
||||
className="timeline-datasources-splitter"
|
||||
percentage={true}
|
||||
primaryIndex={1}
|
||||
primaryMinSize={20}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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) }}>■</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 && (
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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
Loading…
Reference in New Issue