From 78daa0bc5a1eb66d0bb0b88ccb559335294e44a7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=90=D1=80=D1=82=D1=91=D0=BC=20=D0=9C=D1=83=D1=84=D0=B0?= =?UTF-8?q?=D0=B7=D0=B0=D0=BB=D0=BE=D0=B2?= <67755036+artemmufazalov@users.noreply.github.com> Date: Thu, 25 Jan 2024 16:34:02 +0300 Subject: [PATCH] feat(TenantOverview): add charts (#657) --- package-lock.json | 620 ++++++++++++++---- package.json | 2 + src/components/MetricChart/MetricChart.scss | 34 + src/components/MetricChart/MetricChart.tsx | 198 ++++++ src/components/MetricChart/convertReponse.ts | 32 + src/components/MetricChart/getChartData.ts | 20 + .../MetricChart/getDefaultDataFormatter.ts | 36 + src/components/MetricChart/index.ts | 2 + src/components/MetricChart/reducer.ts | 86 +++ src/components/MetricChart/types.ts | 32 + .../TimeFrameSelector/TimeFrameSelector.scss | 5 + .../TimeFrameSelector/TimeFrameSelector.tsx | 33 + src/containers/App/Content.js | 28 +- .../TenantOverview/DefaultDashboard.tsx | 50 ++ .../MetricsCards/MetricsCards.tsx | 17 +- .../TenantOverview/TenantCpu/CpuDashboard.tsx | 18 + .../TenantOverview/TenantCpu/TenantCpu.tsx | 2 + .../TenantDashboard/TenantDashboard.scss | 14 + .../TenantDashboard/TenantDashboard.tsx | 71 ++ .../TenantMemory/MemoryDashboard.tsx | 21 + .../TenantMemory/TenantMemory.tsx | 8 +- .../TenantOverview/TenantOverview.tsx | 3 +- .../TenantStorage/StorageDashboard.tsx | 21 + .../TenantStorage/TenantStorage.tsx | 2 + .../Diagnostics/TenantOverview/i18n/en.json | 8 +- .../Diagnostics/TenantOverview/i18n/ru.json | 8 +- src/containers/UserSettings/i18n/en.json | 5 +- src/containers/UserSettings/i18n/ru.json | 5 +- src/containers/UserSettings/settings.ts | 13 +- src/services/api.ts | 18 + src/services/settings.ts | 2 + src/store/reducers/tenant/tenant.ts | 7 +- src/types/api/render.ts | 34 + src/utils/cn.ts | 3 + src/utils/constants.ts | 2 + src/utils/timeParsers/formatDuration.ts | 10 + src/utils/timeframes.ts | 10 + src/utils/versions/getVersionsColors.ts | 1 + 38 files changed, 1333 insertions(+), 148 deletions(-) create mode 100644 src/components/MetricChart/MetricChart.scss create mode 100644 src/components/MetricChart/MetricChart.tsx create mode 100644 src/components/MetricChart/convertReponse.ts create mode 100644 src/components/MetricChart/getChartData.ts create mode 100644 src/components/MetricChart/getDefaultDataFormatter.ts create mode 100644 src/components/MetricChart/index.ts create mode 100644 src/components/MetricChart/reducer.ts create mode 100644 src/components/MetricChart/types.ts create mode 100644 src/components/TimeFrameSelector/TimeFrameSelector.scss create mode 100644 src/components/TimeFrameSelector/TimeFrameSelector.tsx create mode 100644 src/containers/Tenant/Diagnostics/TenantOverview/DefaultDashboard.tsx create mode 100644 src/containers/Tenant/Diagnostics/TenantOverview/TenantCpu/CpuDashboard.tsx create mode 100644 src/containers/Tenant/Diagnostics/TenantOverview/TenantDashboard/TenantDashboard.scss create mode 100644 src/containers/Tenant/Diagnostics/TenantOverview/TenantDashboard/TenantDashboard.tsx create mode 100644 src/containers/Tenant/Diagnostics/TenantOverview/TenantMemory/MemoryDashboard.tsx create mode 100644 src/containers/Tenant/Diagnostics/TenantOverview/TenantStorage/StorageDashboard.tsx create mode 100644 src/types/api/render.ts create mode 100644 src/utils/cn.ts create mode 100644 src/utils/timeframes.ts diff --git a/package-lock.json b/package-lock.json index bc525212b..258a52f27 100644 --- a/package-lock.json +++ b/package-lock.json @@ -370,6 +370,7 @@ "version": "7.18.6", "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.18.6.tgz", "integrity": "sha512-duORpUiYrEpzKIop6iNbjnwKLAKnJ47csTyRACyEmWj0QdUrm5aqNJGHSSEQSUAvNW0ojX0dOmK9dZduvkfeXA==", + "dev": true, "requires": { "@babel/types": "^7.18.6" }, @@ -377,12 +378,14 @@ "@babel/helper-validator-identifier": { "version": "7.19.1", "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.19.1.tgz", - "integrity": "sha512-awrNfaMtnHUr653GgGEs++LlAvW6w+DcPrOliSMXWCKo597CwL5Acf/wWdNkf/tfEQE3mjkeD1YOVZOUV/od1w==" + "integrity": "sha512-awrNfaMtnHUr653GgGEs++LlAvW6w+DcPrOliSMXWCKo597CwL5Acf/wWdNkf/tfEQE3mjkeD1YOVZOUV/od1w==", + "dev": true }, "@babel/types": { "version": "7.20.7", "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.20.7.tgz", "integrity": "sha512-69OnhBxSSgK0OzTJai4kyPDiKTIe3j+ctaHdIGVbRahTLAT7L3R9oeXHC2aVSuGYt3cVnoAMDmOCgJ2yaiLMvg==", + "dev": true, "requires": { "@babel/helper-string-parser": "^7.19.4", "@babel/helper-validator-identifier": "^7.19.1", @@ -2310,6 +2313,7 @@ "version": "7.18.6", "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.18.6.tgz", "integrity": "sha512-6mmljtAedFGTWu2p/8WIORGwy+61PLgOMPOdazc7YoJ9ZCWUyFy3A6CpPkRKLKD1ToAesxX8KGEViAiLo9N+7Q==", + "dev": true, "requires": { "@babel/helper-plugin-utils": "^7.18.6" }, @@ -2317,7 +2321,8 @@ "@babel/helper-plugin-utils": { "version": "7.20.2", "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.20.2.tgz", - "integrity": "sha512-8RvlJG2mj4huQ4pZ+rU9lqKi9ZKiRmuvGuM2HlWmkmgOhbs6zEAw6IEiJ5cQqGbDzGZOhwuOQNtZMi/ENLjZoQ==" + "integrity": "sha512-8RvlJG2mj4huQ4pZ+rU9lqKi9ZKiRmuvGuM2HlWmkmgOhbs6zEAw6IEiJ5cQqGbDzGZOhwuOQNtZMi/ENLjZoQ==", + "dev": true } } }, @@ -4042,6 +4047,7 @@ "version": "7.20.13", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx/-/plugin-transform-react-jsx-7.20.13.tgz", "integrity": "sha512-MmTZx/bkUrfJhhYAYt3Urjm+h8DQGrPrnKQ94jLo7NLuOU+T89a7IByhKmrb8SKhrIYIQ0FN0CHMbnFRen4qNw==", + "dev": true, "requires": { "@babel/helper-annotate-as-pure": "^7.18.6", "@babel/helper-module-imports": "^7.18.6", @@ -4054,6 +4060,7 @@ "version": "7.18.6", "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.18.6.tgz", "integrity": "sha512-0NFvs3VkuSYbFi1x2Vd6tKrywq+z/cLeYC/RJNFrIX/30Bf5aiGYbtvGXolEktzJH8o5E5KJ3tT+nkxuuZFVlA==", + "dev": true, "requires": { "@babel/types": "^7.18.6" } @@ -4061,17 +4068,20 @@ "@babel/helper-plugin-utils": { "version": "7.20.2", "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.20.2.tgz", - "integrity": "sha512-8RvlJG2mj4huQ4pZ+rU9lqKi9ZKiRmuvGuM2HlWmkmgOhbs6zEAw6IEiJ5cQqGbDzGZOhwuOQNtZMi/ENLjZoQ==" + "integrity": "sha512-8RvlJG2mj4huQ4pZ+rU9lqKi9ZKiRmuvGuM2HlWmkmgOhbs6zEAw6IEiJ5cQqGbDzGZOhwuOQNtZMi/ENLjZoQ==", + "dev": true }, "@babel/helper-validator-identifier": { "version": "7.19.1", "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.19.1.tgz", - "integrity": "sha512-awrNfaMtnHUr653GgGEs++LlAvW6w+DcPrOliSMXWCKo597CwL5Acf/wWdNkf/tfEQE3mjkeD1YOVZOUV/od1w==" + "integrity": "sha512-awrNfaMtnHUr653GgGEs++LlAvW6w+DcPrOliSMXWCKo597CwL5Acf/wWdNkf/tfEQE3mjkeD1YOVZOUV/od1w==", + "dev": true }, "@babel/types": { "version": "7.20.7", "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.20.7.tgz", "integrity": "sha512-69OnhBxSSgK0OzTJai4kyPDiKTIe3j+ctaHdIGVbRahTLAT7L3R9oeXHC2aVSuGYt3cVnoAMDmOCgJ2yaiLMvg==", + "dev": true, "requires": { "@babel/helper-string-parser": "^7.19.4", "@babel/helper-validator-identifier": "^7.19.1", @@ -5264,14 +5274,12 @@ "@csstools/postcss-unset-value": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/@csstools/postcss-unset-value/-/postcss-unset-value-1.0.2.tgz", - "integrity": "sha512-c8J4roPBILnelAsdLr4XOAR/GsTm0GJi4XpcfvoWk3U6KiTCqiFYc63KhRMQQX35jYMp4Ao8Ij9+IZRgMfJp1g==", - "requires": {} + "integrity": "sha512-c8J4roPBILnelAsdLr4XOAR/GsTm0GJi4XpcfvoWk3U6KiTCqiFYc63KhRMQQX35jYMp4Ao8Ij9+IZRgMfJp1g==" }, "@csstools/selector-specificity": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/@csstools/selector-specificity/-/selector-specificity-2.1.1.tgz", - "integrity": "sha512-jwx+WCqszn53YHOfvFMJJRd/B2GqkCBt+1MJSG6o5/s8+ytHMvDZXsJgUEWLk12UnLd7HYKac4BYU5i/Ron1Cw==", - "requires": {} + "integrity": "sha512-jwx+WCqszn53YHOfvFMJJRd/B2GqkCBt+1MJSG6o5/s8+ytHMvDZXsJgUEWLk12UnLd7HYKac4BYU5i/Ron1Cw==" }, "@endemolshinegroup/cosmiconfig-typescript-loader": { "version": "3.0.2", @@ -5335,8 +5343,37 @@ "@gravity-ui/axios-wrapper": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/@gravity-ui/axios-wrapper/-/axios-wrapper-1.3.0.tgz", - "integrity": "sha512-5hR8aGLfRG1hyxFG4CxANSdX//Z6ADzs+mFj9M0sbf0NYynVAid7G3+/44234B/g5GK7jxrycLPMvlQ/sIlWRw==", - "requires": {} + "integrity": "sha512-5hR8aGLfRG1hyxFG4CxANSdX//Z6ADzs+mFj9M0sbf0NYynVAid7G3+/44234B/g5GK7jxrycLPMvlQ/sIlWRw==" + }, + "@gravity-ui/chartkit": { + "version": "4.15.0", + "resolved": "https://registry.npmjs.org/@gravity-ui/chartkit/-/chartkit-4.15.0.tgz", + "integrity": "sha512-7ITSxXLKvTedsMD9lecUVhZXy7roSDnaDNNspNhOB847QrJ0Nu13p4nmJVGf6QJBaeQQRjrfUgCCnI1quMYJdw==", + "requires": { + "@bem-react/classname": "^1.6.0", + "@gravity-ui/date-utils": "^1.4.1", + "@gravity-ui/yagr": "^4.1.0", + "afterframe": "^1.0.2", + "d3": "^7.8.5", + "lodash": "^4.17.21", + "react-split-pane": "^0.1.92" + }, + "dependencies": { + "@gravity-ui/date-utils": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/@gravity-ui/date-utils/-/date-utils-1.4.2.tgz", + "integrity": "sha512-q966eCe6fJkVVYMixgDnpoWYahMvlJGxMymKipP9pkZPJKR9epmQ4RKdKFs+m8umosj8aIHrXH/aVMRveQKUCQ==", + "requires": { + "dayjs": "^1.11.7", + "lodash": "^4.17.0" + } + }, + "lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" + } + } }, "@gravity-ui/components": { "version": "2.9.1", @@ -5665,8 +5702,7 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/@gravity-ui/prettier-config/-/prettier-config-1.0.1.tgz", "integrity": "sha512-VpTM+OiUMgXjwc7HBo0ekxBFghzELsnE/RFBYwbkbIqm0NrL4SiEPzne1IyEY/WsfI8om1hpn81t+qjGbicMSw==", - "dev": true, - "requires": {} + "dev": true }, "@gravity-ui/react-data-table": { "version": "1.0.3", @@ -5709,6 +5745,7 @@ "version": "5.24.0", "resolved": "https://registry.npmjs.org/@gravity-ui/uikit/-/uikit-5.24.0.tgz", "integrity": "sha512-NpMzh46rj4h0viW8DO9jE6qyGLqVPDK86fBWUKfxOvOT92eRzwNltVpEpGPgWUrtZQQWs7Vxb3BD/WRAcem3dQ==", + "dev": true, "requires": { "@bem-react/classname": "^1.6.0", "@gravity-ui/i18n": "^1.1.0", @@ -5731,20 +5768,31 @@ "@gravity-ui/i18n": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@gravity-ui/i18n/-/i18n-1.1.0.tgz", - "integrity": "sha512-Ck+LNE1S2GGaMKMijIYWogiM/tYk0wHPlqLrhZmzp6P5keIu8YnPDeIlwAzq4eYVSrtTudLmshFB9QmYAeLryQ==" + "integrity": "sha512-Ck+LNE1S2GGaMKMijIYWogiM/tYk0wHPlqLrhZmzp6P5keIu8YnPDeIlwAzq4eYVSrtTudLmshFB9QmYAeLryQ==", + "dev": true }, "lodash": { "version": "4.17.21", "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", - "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", + "dev": true }, "tslib": { "version": "2.6.2", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", - "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==" + "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==", + "dev": true } } }, + "@gravity-ui/yagr": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/@gravity-ui/yagr/-/yagr-4.1.0.tgz", + "integrity": "sha512-Je0NPvXiOcQJxef3HqmeHVBwclIq+PbPvwZJYUbZZjyjkDVGQbAxEkgbQfiiabST+GVmsfiZczudxiq/l/Lumw==", + "requires": { + "uplot": "1.6.27" + } + }, "@humanwhocodes/config-array": { "version": "0.11.8", "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.8.tgz", @@ -6425,7 +6473,8 @@ "@popperjs/core": { "version": "2.11.8", "resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.8.tgz", - "integrity": "sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==" + "integrity": "sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==", + "dev": true }, "@rollup/plugin-babel": { "version": "5.3.1", @@ -7500,6 +7549,16 @@ "resolved": "https://registry.npmjs.org/@xtuc/long/-/long-4.2.2.tgz", "integrity": "sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==" }, + "JSONStream": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/JSONStream/-/JSONStream-1.3.5.tgz", + "integrity": "sha512-E+iruNOY8VV9s4JEbe1aNEm6MiszPRr/UfcHMz0TQh1BXSxHK+ASV1R6W4HpjBhSeS+54PIsAMCBmwD06LLsqQ==", + "dev": true, + "requires": { + "jsonparse": "^1.2.0", + "through": ">=2.2.7 <3" + } + }, "abab": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/abab/-/abab-2.0.5.tgz", @@ -7546,14 +7605,12 @@ "acorn-import-assertions": { "version": "1.8.0", "resolved": "https://registry.npmjs.org/acorn-import-assertions/-/acorn-import-assertions-1.8.0.tgz", - "integrity": "sha512-m7VZ3jwz4eK6A4Vtt8Ew1/mNbP24u0FhdyfA7fSvnJR6LMdfOYnmuIrrJAgrYfYJ10F/otaHTtrtrtmHdMNzEw==", - "requires": {} + "integrity": "sha512-m7VZ3jwz4eK6A4Vtt8Ew1/mNbP24u0FhdyfA7fSvnJR6LMdfOYnmuIrrJAgrYfYJ10F/otaHTtrtrtmHdMNzEw==" }, "acorn-jsx": { "version": "5.3.2", "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", - "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", - "requires": {} + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==" }, "acorn-node": { "version": "1.8.2", @@ -7592,6 +7649,11 @@ "regex-parser": "^2.2.11" } }, + "afterframe": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/afterframe/-/afterframe-1.0.2.tgz", + "integrity": "sha512-0JeMZI7dIfVs5guqLgidQNV7c6jBC2HO0QNSekAUB82Hr7PdU9QXNAF3kpFkvATvHYDDTGto7FPsRu1ey+aKJQ==" + }, "agent-base": { "version": "6.0.2", "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", @@ -7650,8 +7712,7 @@ "ajv-keywords": { "version": "3.5.2", "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz", - "integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==", - "requires": {} + "integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==" }, "ansi-escapes": { "version": "4.3.2", @@ -7715,7 +7776,7 @@ "version": "4.1.3", "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==", - "devOptional": true + "dev": true }, "argparse": { "version": "1.0.10", @@ -8690,8 +8751,7 @@ "babel-plugin-named-asset-import": { "version": "0.3.8", "resolved": "https://registry.npmjs.org/babel-plugin-named-asset-import/-/babel-plugin-named-asset-import-0.3.8.tgz", - "integrity": "sha512-WXiAc++qo7XcJ1ZnTYGtLxmBCVbddAml3CEXgWaBzNzLNoxtQ8AiGEFDMOhot9XjTCQbvP5E77Fj9Gk924f00Q==", - "requires": {} + "integrity": "sha512-WXiAc++qo7XcJ1ZnTYGtLxmBCVbddAml3CEXgWaBzNzLNoxtQ8AiGEFDMOhot9XjTCQbvP5E77Fj9Gk924f00Q==" }, "babel-plugin-polyfill-corejs2": { "version": "0.3.3", @@ -8849,7 +8909,8 @@ "blueimp-md5": { "version": "2.19.0", "resolved": "https://registry.npmjs.org/blueimp-md5/-/blueimp-md5-2.19.0.tgz", - "integrity": "sha512-DRQrD6gJyy8FbiE4s+bDoXS9hiW3Vbx5uCdwvcCf3zLHL+Iv7LtGHLpr+GZV8rHG8tK766FGYBwRbu8pELTt+w==" + "integrity": "sha512-DRQrD6gJyy8FbiE4s+bDoXS9hiW3Vbx5uCdwvcCf3zLHL+Iv7LtGHLpr+GZV8rHG8tK766FGYBwRbu8pELTt+w==", + "dev": true }, "body-parser": { "version": "1.20.1", @@ -9513,8 +9574,8 @@ "integrity": "sha512-nK7sAtfi+QXbxHCYfhpZsfRtaitZLIA6889kFIouLvz6repszQDgxBu7wf2WbU+Dco7sAnNCJYERCwt54WPC2Q==", "dev": true, "requires": { - "is-text-path": "^1.0.1", "JSONStream": "^1.0.4", + "is-text-path": "^1.0.1", "lodash": "^4.17.15", "meow": "^8.0.0", "split2": "^3.0.0", @@ -9719,7 +9780,7 @@ "version": "1.1.1", "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==", - "devOptional": true + "dev": true }, "cross-spawn": { "version": "7.0.3", @@ -9769,6 +9830,7 @@ "version": "1.2.1", "resolved": "https://registry.npmjs.org/css-box-model/-/css-box-model-1.2.1.tgz", "integrity": "sha512-a7Vr4Q/kd/aw96bnJG332W9V9LkJO69JRcaCYDUqjp6/z0w6VcZjgAcTbgFxEPfBgdnAwlh3iwu+hLopa+flJw==", + "dev": true, "requires": { "tiny-invariant": "^1.0.6" } @@ -9776,8 +9838,7 @@ "css-declaration-sorter": { "version": "6.3.1", "resolved": "https://registry.npmjs.org/css-declaration-sorter/-/css-declaration-sorter-6.3.1.tgz", - "integrity": "sha512-fBffmak0bPAnyqc/HO8C3n2sHrp9wcqQz6ES9koRF2/mLOVAx9zIQ3Y7R29sYCteTPqMCwns4WYQoCX91Xl3+w==", - "requires": {} + "integrity": "sha512-fBffmak0bPAnyqc/HO8C3n2sHrp9wcqQz6ES9koRF2/mLOVAx9zIQ3Y7R29sYCteTPqMCwns4WYQoCX91Xl3+w==" }, "css-has-pseudo": { "version": "3.0.4", @@ -9880,8 +9941,7 @@ "css-prefers-color-scheme": { "version": "6.0.3", "resolved": "https://registry.npmjs.org/css-prefers-color-scheme/-/css-prefers-color-scheme-6.0.3.tgz", - "integrity": "sha512-4BqMbZksRkJQx2zAjrokiGMd07RqOa2IxIrrN10lyBe9xhn9DEvjUK79J6jkeiv9D9hQFXKb6g1jwU62jziJZA==", - "requires": {} + "integrity": "sha512-4BqMbZksRkJQx2zAjrokiGMd07RqOa2IxIrrN10lyBe9xhn9DEvjUK79J6jkeiv9D9hQFXKb6g1jwU62jziJZA==" }, "css-select": { "version": "2.1.0", @@ -9978,8 +10038,7 @@ "cssnano-utils": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/cssnano-utils/-/cssnano-utils-3.1.0.tgz", - "integrity": "sha512-JQNR19/YZhz4psLX/rQ9M83e3z2Wf/HdJbryzte4a3NSuafyp9w/I4U+hx5C2S9g41qlstH7DEWnZaaj83OuEA==", - "requires": {} + "integrity": "sha512-JQNR19/YZhz4psLX/rQ9M83e3z2Wf/HdJbryzte4a3NSuafyp9w/I4U+hx5C2S9g41qlstH7DEWnZaaj83OuEA==" }, "csso": { "version": "4.2.0", @@ -10030,6 +10089,279 @@ "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.0.10.tgz", "integrity": "sha512-2u44ZG2OcNUO9HDp/Jl8C07x6pU/eTR3ncV91SiK3dhG9TWvRVsCoJw14Ckx5DgWkzGA3waZWO3d7pgqpUI/XA==" }, + "d3": { + "version": "7.8.5", + "resolved": "https://registry.npmjs.org/d3/-/d3-7.8.5.tgz", + "integrity": "sha512-JgoahDG51ncUfJu6wX/1vWQEqOflgXyl4MaHqlcSruTez7yhaRKR9i8VjjcQGeS2en/jnFivXuaIMnseMMt0XA==", + "requires": { + "d3-array": "3", + "d3-axis": "3", + "d3-brush": "3", + "d3-chord": "3", + "d3-color": "3", + "d3-contour": "4", + "d3-delaunay": "6", + "d3-dispatch": "3", + "d3-drag": "3", + "d3-dsv": "3", + "d3-ease": "3", + "d3-fetch": "3", + "d3-force": "3", + "d3-format": "3", + "d3-geo": "3", + "d3-hierarchy": "3", + "d3-interpolate": "3", + "d3-path": "3", + "d3-polygon": "3", + "d3-quadtree": "3", + "d3-random": "3", + "d3-scale": "4", + "d3-scale-chromatic": "3", + "d3-selection": "3", + "d3-shape": "3", + "d3-time": "3", + "d3-time-format": "4", + "d3-timer": "3", + "d3-transition": "3", + "d3-zoom": "3" + } + }, + "d3-array": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-3.2.4.tgz", + "integrity": "sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==", + "requires": { + "internmap": "1 - 2" + } + }, + "d3-axis": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-axis/-/d3-axis-3.0.0.tgz", + "integrity": "sha512-IH5tgjV4jE/GhHkRV0HiVYPDtvfjHQlQfJHs0usq7M30XcSBvOotpmH1IgkcXsO/5gEQZD43B//fc7SRT5S+xw==" + }, + "d3-brush": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-brush/-/d3-brush-3.0.0.tgz", + "integrity": "sha512-ALnjWlVYkXsVIGlOsuWH1+3udkYFI48Ljihfnh8FZPF2QS9o+PzGLBslO0PjzVoHLZ2KCVgAM8NVkXPJB2aNnQ==", + "requires": { + "d3-dispatch": "1 - 3", + "d3-drag": "2 - 3", + "d3-interpolate": "1 - 3", + "d3-selection": "3", + "d3-transition": "3" + } + }, + "d3-chord": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-chord/-/d3-chord-3.0.1.tgz", + "integrity": "sha512-VE5S6TNa+j8msksl7HwjxMHDM2yNK3XCkusIlpX5kwauBfXuyLAtNg9jCp/iHH61tgI4sb6R/EIMWCqEIdjT/g==", + "requires": { + "d3-path": "1 - 3" + } + }, + "d3-color": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-color/-/d3-color-3.1.0.tgz", + "integrity": "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==" + }, + "d3-contour": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/d3-contour/-/d3-contour-4.0.2.tgz", + "integrity": "sha512-4EzFTRIikzs47RGmdxbeUvLWtGedDUNkTcmzoeyg4sP/dvCexO47AaQL7VKy/gul85TOxw+IBgA8US2xwbToNA==", + "requires": { + "d3-array": "^3.2.0" + } + }, + "d3-delaunay": { + "version": "6.0.4", + "resolved": "https://registry.npmjs.org/d3-delaunay/-/d3-delaunay-6.0.4.tgz", + "integrity": "sha512-mdjtIZ1XLAM8bm/hx3WwjfHt6Sggek7qH043O8KEjDXN40xi3vx/6pYSVTwLjEgiXQTbvaouWKynLBiUZ6SK6A==", + "requires": { + "delaunator": "5" + } + }, + "d3-dispatch": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-dispatch/-/d3-dispatch-3.0.1.tgz", + "integrity": "sha512-rzUyPU/S7rwUflMyLc1ETDeBj0NRuHKKAcvukozwhshr6g6c5d8zh4c2gQjY2bZ0dXeGLWc1PF174P2tVvKhfg==" + }, + "d3-drag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-drag/-/d3-drag-3.0.0.tgz", + "integrity": "sha512-pWbUJLdETVA8lQNJecMxoXfH6x+mO2UQo8rSmZ+QqxcbyA3hfeprFgIT//HW2nlHChWeIIMwS2Fq+gEARkhTkg==", + "requires": { + "d3-dispatch": "1 - 3", + "d3-selection": "3" + } + }, + "d3-dsv": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-dsv/-/d3-dsv-3.0.1.tgz", + "integrity": "sha512-UG6OvdI5afDIFP9w4G0mNq50dSOsXHJaRE8arAS5o9ApWnIElp8GZw1Dun8vP8OyHOZ/QJUKUJwxiiCCnUwm+Q==", + "requires": { + "commander": "7", + "iconv-lite": "0.6", + "rw": "1" + }, + "dependencies": { + "iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "requires": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + } + } + } + }, + "d3-ease": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-ease/-/d3-ease-3.0.1.tgz", + "integrity": "sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==" + }, + "d3-fetch": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-fetch/-/d3-fetch-3.0.1.tgz", + "integrity": "sha512-kpkQIM20n3oLVBKGg6oHrUchHM3xODkTzjMoj7aWQFq5QEM+R6E4WkzT5+tojDY7yjez8KgCBRoj4aEr99Fdqw==", + "requires": { + "d3-dsv": "1 - 3" + } + }, + "d3-force": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-force/-/d3-force-3.0.0.tgz", + "integrity": "sha512-zxV/SsA+U4yte8051P4ECydjD/S+qeYtnaIyAs9tgHCqfguma/aAQDjo85A9Z6EKhBirHRJHXIgJUlffT4wdLg==", + "requires": { + "d3-dispatch": "1 - 3", + "d3-quadtree": "1 - 3", + "d3-timer": "1 - 3" + } + }, + "d3-format": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-format/-/d3-format-3.1.0.tgz", + "integrity": "sha512-YyUI6AEuY/Wpt8KWLgZHsIU86atmikuoOmCfommt0LYHiQSPjvX2AcFc38PX0CBpr2RCyZhjex+NS/LPOv6YqA==" + }, + "d3-geo": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-geo/-/d3-geo-3.1.0.tgz", + "integrity": "sha512-JEo5HxXDdDYXCaWdwLRt79y7giK8SbhZJbFWXqbRTolCHFI5jRqteLzCsq51NKbUoX0PjBVSohxrx+NoOUujYA==", + "requires": { + "d3-array": "2.5.0 - 3" + } + }, + "d3-hierarchy": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/d3-hierarchy/-/d3-hierarchy-3.1.2.tgz", + "integrity": "sha512-FX/9frcub54beBdugHjDCdikxThEqjnR93Qt7PvQTOHxyiNCAlvMrHhclk3cD5VeAaq9fxmfRp+CnWw9rEMBuA==" + }, + "d3-interpolate": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-3.0.1.tgz", + "integrity": "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==", + "requires": { + "d3-color": "1 - 3" + } + }, + "d3-path": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-path/-/d3-path-3.1.0.tgz", + "integrity": "sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ==" + }, + "d3-polygon": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-polygon/-/d3-polygon-3.0.1.tgz", + "integrity": "sha512-3vbA7vXYwfe1SYhED++fPUQlWSYTTGmFmQiany/gdbiWgU/iEyQzyymwL9SkJjFFuCS4902BSzewVGsHHmHtXg==" + }, + "d3-quadtree": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-quadtree/-/d3-quadtree-3.0.1.tgz", + "integrity": "sha512-04xDrxQTDTCFwP5H6hRhsRcb9xxv2RzkcsygFzmkSIOJy3PeRJP7sNk3VRIbKXcog561P9oU0/rVH6vDROAgUw==" + }, + "d3-random": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-random/-/d3-random-3.0.1.tgz", + "integrity": "sha512-FXMe9GfxTxqd5D6jFsQ+DJ8BJS4E/fT5mqqdjovykEB2oFbTMDVdg1MGFxfQW+FBOGoB++k8swBrgwSHT1cUXQ==" + }, + "d3-scale": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/d3-scale/-/d3-scale-4.0.2.tgz", + "integrity": "sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==", + "requires": { + "d3-array": "2.10.0 - 3", + "d3-format": "1 - 3", + "d3-interpolate": "1.2.0 - 3", + "d3-time": "2.1.1 - 3", + "d3-time-format": "2 - 4" + } + }, + "d3-scale-chromatic": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-scale-chromatic/-/d3-scale-chromatic-3.0.0.tgz", + "integrity": "sha512-Lx9thtxAKrO2Pq6OO2Ua474opeziKr279P/TKZsMAhYyNDD3EnCffdbgeSYN5O7m2ByQsxtuP2CSDczNUIZ22g==", + "requires": { + "d3-color": "1 - 3", + "d3-interpolate": "1 - 3" + } + }, + "d3-selection": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-selection/-/d3-selection-3.0.0.tgz", + "integrity": "sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==" + }, + "d3-shape": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-3.2.0.tgz", + "integrity": "sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==", + "requires": { + "d3-path": "^3.1.0" + } + }, + "d3-time": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-time/-/d3-time-3.1.0.tgz", + "integrity": "sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==", + "requires": { + "d3-array": "2 - 3" + } + }, + "d3-time-format": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/d3-time-format/-/d3-time-format-4.1.0.tgz", + "integrity": "sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==", + "requires": { + "d3-time": "1 - 3" + } + }, + "d3-timer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-timer/-/d3-timer-3.0.1.tgz", + "integrity": "sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==" + }, + "d3-transition": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-transition/-/d3-transition-3.0.1.tgz", + "integrity": "sha512-ApKvfjsSR6tg06xrL434C0WydLr7JewBB3V+/39RMHsaXTOG0zmt/OAXeng5M5LBm0ojmxJrpomQVZ1aPvBL4w==", + "requires": { + "d3-color": "1 - 3", + "d3-dispatch": "1 - 3", + "d3-ease": "1 - 3", + "d3-interpolate": "1 - 3", + "d3-timer": "1 - 3" + } + }, + "d3-zoom": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-zoom/-/d3-zoom-3.0.0.tgz", + "integrity": "sha512-b8AmV3kfQaqWAuacbPuNbL6vahnOJflOhexLzMMNLga62+/nh0JzvJ0aO/5a5MVgUFGS7Hu1P9P03o3fJkDCyw==", + "requires": { + "d3-dispatch": "1 - 3", + "d3-drag": "2 - 3", + "d3-interpolate": "1 - 3", + "d3-selection": "2 - 3", + "d3-transition": "2 - 3" + } + }, "damerau-levenshtein": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/damerau-levenshtein/-/damerau-levenshtein-1.0.8.tgz", @@ -10284,6 +10616,14 @@ "resolved": "https://registry.npmjs.org/defined/-/defined-1.0.1.tgz", "integrity": "sha512-hsBd2qSVCRE+5PmNdHt1uzyrFu5d3RwmFDKzyNZMFq/EwDNJF7Ee5+D5oEKF0hU6LhtoUF1macFvOe4AskQC1Q==" }, + "delaunator": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/delaunator/-/delaunator-5.0.0.tgz", + "integrity": "sha512-AyLvtyJdbv/U1GkiS6gUUzclRoAY4Gs75qkMygJJhU75LW4DNuSF2RMzpxs9jw9Oz1BobHjTdkG3zdP55VxAqw==", + "requires": { + "robust-predicates": "^3.0.0" + } + }, "delayed-stream": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", @@ -10371,7 +10711,7 @@ "version": "4.0.2", "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz", "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==", - "devOptional": true + "dev": true }, "diff-sequences": { "version": "28.1.1", @@ -10952,8 +11292,7 @@ "version": "8.3.0", "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-8.3.0.tgz", "integrity": "sha512-BgZuLUSeKzvlL/VUjx/Yb787VQ26RU3gGjA3iiFvdsp/2bMfVIWUVP7tjxtjS0e+HP409cPlPvNkQloz8C91ew==", - "dev": true, - "requires": {} + "dev": true }, "eslint-config-react-app": { "version": "7.0.1", @@ -11856,8 +12195,7 @@ "eslint-plugin-react-hooks": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-4.3.0.tgz", - "integrity": "sha512-XslZy0LnMn+84NEG9jSGR6eGqaZB3133L8xewQo3fQagbQuGt7a63gf+P1NGKZavEYEC3UXaWEAA/AqDkuN6xA==", - "requires": {} + "integrity": "sha512-XslZy0LnMn+84NEG9jSGR6eGqaZB3133L8xewQo3fQagbQuGt7a63gf+P1NGKZavEYEC3UXaWEAA/AqDkuN6xA==" }, "eslint-plugin-security": { "version": "1.4.0", @@ -12415,6 +12753,7 @@ "version": "7.5.4", "resolved": "https://registry.npmjs.org/focus-trap/-/focus-trap-7.5.4.tgz", "integrity": "sha512-N7kHdlgsO/v+iD/dMoJKtsSqs5Dz/dXZVebRgJw23LDk+jMi/974zyiOYDziY2JPp8xivq9BmUGwIJMiuSBi7w==", + "dev": true, "requires": { "tabbable": "^6.2.0" } @@ -13247,8 +13586,7 @@ "icss-utils": { "version": "5.1.0", "resolved": "https://registry.npmjs.org/icss-utils/-/icss-utils-5.1.0.tgz", - "integrity": "sha512-soFhflCVWLfRNOPU3iv5Z9VUdT44xFRbzjLsEzSr5AQmgqPMTHdU3PMT1Cf1ssx8fLNJDA1juftYl+PUcv3MqA==", - "requires": {} + "integrity": "sha512-soFhflCVWLfRNOPU3iv5Z9VUdT44xFRbzjLsEzSr5AQmgqPMTHdU3PMT1Cf1ssx8fLNJDA1juftYl+PUcv3MqA==" }, "idb": { "version": "7.1.1", @@ -13363,6 +13701,11 @@ "side-channel": "^1.0.4" } }, + "internmap": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/internmap/-/internmap-2.0.3.tgz", + "integrity": "sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==" + }, "ip-regex": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/ip-regex/-/ip-regex-2.1.0.tgz", @@ -14694,8 +15037,7 @@ "jest-pnp-resolver": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/jest-pnp-resolver/-/jest-pnp-resolver-1.2.3.tgz", - "integrity": "sha512-+3NpwQEnRoIBtx4fyhblQDPgJI0H1IEIkX7ShLUjPGA7TtUTvI1oiKi3SR4oBR0hQhQR80l4WAe5RrXBwWMA8w==", - "requires": {} + "integrity": "sha512-+3NpwQEnRoIBtx4fyhblQDPgJI0H1IEIkX7ShLUjPGA7TtUTvI1oiKi3SR4oBR0hQhQR80l4WAe5RrXBwWMA8w==" }, "jest-regex-util": { "version": "27.5.1", @@ -15656,16 +15998,6 @@ "resolved": "https://registry.npmjs.org/jsonpointer/-/jsonpointer-5.0.1.tgz", "integrity": "sha512-p/nXbhSEcu3pZRdkW1OfJhpsVtW1gd4Wa1fnQc9YLiTfAjn0312eMKimbdIQzuZl9aa9xUGaRlP9T/CJE/ditQ==" }, - "JSONStream": { - "version": "1.3.5", - "resolved": "https://registry.npmjs.org/JSONStream/-/JSONStream-1.3.5.tgz", - "integrity": "sha512-E+iruNOY8VV9s4JEbe1aNEm6MiszPRr/UfcHMz0TQh1BXSxHK+ASV1R6W4HpjBhSeS+54PIsAMCBmwD06LLsqQ==", - "dev": true, - "requires": { - "jsonparse": "^1.2.0", - "through": ">=2.2.7 <3" - } - }, "jsprim": { "version": "1.4.2", "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-1.4.2.tgz", @@ -16063,7 +16395,7 @@ "version": "1.3.6", "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==", - "devOptional": true + "dev": true }, "makeerror": { "version": "1.0.12", @@ -16111,7 +16443,8 @@ "memoize-one": { "version": "5.2.1", "resolved": "https://registry.npmjs.org/memoize-one/-/memoize-one-5.2.1.tgz", - "integrity": "sha512-zYiwtZUcYyXKo/np96AGZAckk+FWWsUdJ3cHGGmld7+AhvcWmQyGCYUh1hc4Q/pkOhb65dQR/pqCyK0cOaHz4Q==" + "integrity": "sha512-zYiwtZUcYyXKo/np96AGZAckk+FWWsUdJ3cHGGmld7+AhvcWmQyGCYUh1hc4Q/pkOhb65dQR/pqCyK0cOaHz4Q==", + "dev": true }, "meow": { "version": "8.1.2", @@ -16344,8 +16677,7 @@ "mkdirp": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", - "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", - "devOptional": true + "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==" }, "monaco-editor": { "version": "0.24.0", @@ -17599,8 +17931,7 @@ "postcss-browser-comments": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/postcss-browser-comments/-/postcss-browser-comments-4.0.0.tgz", - "integrity": "sha512-X9X9/WN3KIvY9+hNERUqX9gncsgBA25XaeR+jshHz2j8+sYyHktHw1JdKuMjeLpGktXidqDhA7b/qm1mrBDmgg==", - "requires": {} + "integrity": "sha512-X9X9/WN3KIvY9+hNERUqX9gncsgBA25XaeR+jshHz2j8+sYyHktHw1JdKuMjeLpGktXidqDhA7b/qm1mrBDmgg==" }, "postcss-calc": { "version": "8.2.4", @@ -17765,26 +18096,22 @@ "postcss-discard-comments": { "version": "5.1.2", "resolved": "https://registry.npmjs.org/postcss-discard-comments/-/postcss-discard-comments-5.1.2.tgz", - "integrity": "sha512-+L8208OVbHVF2UQf1iDmRcbdjJkuBF6IS29yBDSiWUIzpYaAhtNl6JYnYm12FnkeCwQqF5LeklOu6rAqgfBZqQ==", - "requires": {} + "integrity": "sha512-+L8208OVbHVF2UQf1iDmRcbdjJkuBF6IS29yBDSiWUIzpYaAhtNl6JYnYm12FnkeCwQqF5LeklOu6rAqgfBZqQ==" }, "postcss-discard-duplicates": { "version": "5.1.0", "resolved": "https://registry.npmjs.org/postcss-discard-duplicates/-/postcss-discard-duplicates-5.1.0.tgz", - "integrity": "sha512-zmX3IoSI2aoenxHV6C7plngHWWhUOV3sP1T8y2ifzxzbtnuhk1EdPwm0S1bIUNaJ2eNbWeGLEwzw8huPD67aQw==", - "requires": {} + "integrity": "sha512-zmX3IoSI2aoenxHV6C7plngHWWhUOV3sP1T8y2ifzxzbtnuhk1EdPwm0S1bIUNaJ2eNbWeGLEwzw8huPD67aQw==" }, "postcss-discard-empty": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/postcss-discard-empty/-/postcss-discard-empty-5.1.1.tgz", - "integrity": "sha512-zPz4WljiSuLWsI0ir4Mcnr4qQQ5e1Ukc3i7UfE2XcrwKK2LIPIqE5jxMRxO6GbI3cv//ztXDsXwEWT3BHOGh3A==", - "requires": {} + "integrity": "sha512-zPz4WljiSuLWsI0ir4Mcnr4qQQ5e1Ukc3i7UfE2XcrwKK2LIPIqE5jxMRxO6GbI3cv//ztXDsXwEWT3BHOGh3A==" }, "postcss-discard-overridden": { "version": "5.1.0", "resolved": "https://registry.npmjs.org/postcss-discard-overridden/-/postcss-discard-overridden-5.1.0.tgz", - "integrity": "sha512-21nOL7RqWR1kasIVdKs8HNqQJhFxLsyRfAnUDm4Fe4t4mCWL9OJiHvlHPjcd8zc5Myu89b/7wZDnOSjFgeWRtw==", - "requires": {} + "integrity": "sha512-21nOL7RqWR1kasIVdKs8HNqQJhFxLsyRfAnUDm4Fe4t4mCWL9OJiHvlHPjcd8zc5Myu89b/7wZDnOSjFgeWRtw==" }, "postcss-double-position-gradients": { "version": "3.1.2", @@ -17806,8 +18133,7 @@ "postcss-flexbugs-fixes": { "version": "5.0.2", "resolved": "https://registry.npmjs.org/postcss-flexbugs-fixes/-/postcss-flexbugs-fixes-5.0.2.tgz", - "integrity": "sha512-18f9voByak7bTktR2QgDveglpn9DTbBWPUzSOe9g0N4WR/2eSt6Vrcbf0hmspvMI6YWGywz6B9f7jzpFNJJgnQ==", - "requires": {} + "integrity": "sha512-18f9voByak7bTktR2QgDveglpn9DTbBWPUzSOe9g0N4WR/2eSt6Vrcbf0hmspvMI6YWGywz6B9f7jzpFNJJgnQ==" }, "postcss-focus-visible": { "version": "6.0.4", @@ -17828,14 +18154,12 @@ "postcss-font-variant": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/postcss-font-variant/-/postcss-font-variant-5.0.0.tgz", - "integrity": "sha512-1fmkBaCALD72CK2a9i468mA/+tr9/1cBxRRMXOUaZqO43oWPR5imcyPjXwuv7PXbCid4ndlP5zWhidQVVa3hmA==", - "requires": {} + "integrity": "sha512-1fmkBaCALD72CK2a9i468mA/+tr9/1cBxRRMXOUaZqO43oWPR5imcyPjXwuv7PXbCid4ndlP5zWhidQVVa3hmA==" }, "postcss-gap-properties": { "version": "3.0.5", "resolved": "https://registry.npmjs.org/postcss-gap-properties/-/postcss-gap-properties-3.0.5.tgz", - "integrity": "sha512-IuE6gKSdoUNcvkGIqdtjtcMtZIFyXZhmFd5RUlg97iVEvp1BZKV5ngsAjCjrVy+14uhGBQl9tzmi1Qwq4kqVOg==", - "requires": {} + "integrity": "sha512-IuE6gKSdoUNcvkGIqdtjtcMtZIFyXZhmFd5RUlg97iVEvp1BZKV5ngsAjCjrVy+14uhGBQl9tzmi1Qwq4kqVOg==" }, "postcss-image-set-function": { "version": "4.0.7", @@ -17858,8 +18182,7 @@ "postcss-initial": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/postcss-initial/-/postcss-initial-4.0.1.tgz", - "integrity": "sha512-0ueD7rPqX8Pn1xJIjay0AZeIuDoF+V+VvMt/uOnn+4ezUKhZM/NokDeP6DwMNyIoYByuN/94IQnt5FEkaN59xQ==", - "requires": {} + "integrity": "sha512-0ueD7rPqX8Pn1xJIjay0AZeIuDoF+V+VvMt/uOnn+4ezUKhZM/NokDeP6DwMNyIoYByuN/94IQnt5FEkaN59xQ==" }, "postcss-js": { "version": "4.0.1", @@ -17907,14 +18230,12 @@ "postcss-logical": { "version": "5.0.4", "resolved": "https://registry.npmjs.org/postcss-logical/-/postcss-logical-5.0.4.tgz", - "integrity": "sha512-RHXxplCeLh9VjinvMrZONq7im4wjWGlRJAqmAVLXyZaXwfDWP73/oq4NdIp+OZwhQUMj0zjqDfM5Fj7qby+B4g==", - "requires": {} + "integrity": "sha512-RHXxplCeLh9VjinvMrZONq7im4wjWGlRJAqmAVLXyZaXwfDWP73/oq4NdIp+OZwhQUMj0zjqDfM5Fj7qby+B4g==" }, "postcss-media-minmax": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/postcss-media-minmax/-/postcss-media-minmax-5.0.0.tgz", - "integrity": "sha512-yDUvFf9QdFZTuCUg0g0uNSHVlJ5X1lSzDZjPSFaiCWvjgsvu8vEVxtahPrLMinIDEEGnx6cBe6iqdx5YWz08wQ==", - "requires": {} + "integrity": "sha512-yDUvFf9QdFZTuCUg0g0uNSHVlJ5X1lSzDZjPSFaiCWvjgsvu8vEVxtahPrLMinIDEEGnx6cBe6iqdx5YWz08wQ==" }, "postcss-media-query-parser": { "version": "0.2.3", @@ -18054,15 +18375,13 @@ "version": "5.1.0", "resolved": "https://registry.npmjs.org/icss-utils/-/icss-utils-5.1.0.tgz", "integrity": "sha512-soFhflCVWLfRNOPU3iv5Z9VUdT44xFRbzjLsEzSr5AQmgqPMTHdU3PMT1Cf1ssx8fLNJDA1juftYl+PUcv3MqA==", - "dev": true, - "requires": {} + "dev": true }, "postcss-modules-extract-imports": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/postcss-modules-extract-imports/-/postcss-modules-extract-imports-3.0.0.tgz", "integrity": "sha512-bdHleFnP3kZ4NYDhuGlVK+CMrQ/pqUm8bx/oGL93K6gVwiclvX5x0n76fYMKuIGKzlABOy13zsvqjb0f92TEXw==", - "dev": true, - "requires": {} + "dev": true }, "postcss-modules-local-by-default": { "version": "4.0.0", @@ -18098,8 +18417,7 @@ "postcss-modules-extract-imports": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/postcss-modules-extract-imports/-/postcss-modules-extract-imports-3.0.0.tgz", - "integrity": "sha512-bdHleFnP3kZ4NYDhuGlVK+CMrQ/pqUm8bx/oGL93K6gVwiclvX5x0n76fYMKuIGKzlABOy13zsvqjb0f92TEXw==", - "requires": {} + "integrity": "sha512-bdHleFnP3kZ4NYDhuGlVK+CMrQ/pqUm8bx/oGL93K6gVwiclvX5x0n76fYMKuIGKzlABOy13zsvqjb0f92TEXw==" }, "postcss-modules-local-by-default": { "version": "4.0.0", @@ -18179,8 +18497,7 @@ "postcss-normalize-charset": { "version": "5.1.0", "resolved": "https://registry.npmjs.org/postcss-normalize-charset/-/postcss-normalize-charset-5.1.0.tgz", - "integrity": "sha512-mSgUJ+pd/ldRGVx26p2wz9dNZ7ji6Pn8VWBajMXFf8jk7vUoSrZ2lt/wZR7DtlZYKesmZI680qjr2CeFF2fbUg==", - "requires": {} + "integrity": "sha512-mSgUJ+pd/ldRGVx26p2wz9dNZ7ji6Pn8VWBajMXFf8jk7vUoSrZ2lt/wZR7DtlZYKesmZI680qjr2CeFF2fbUg==" }, "postcss-normalize-display-values": { "version": "5.1.0", @@ -18279,8 +18596,7 @@ "postcss-opacity-percentage": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/postcss-opacity-percentage/-/postcss-opacity-percentage-1.1.3.tgz", - "integrity": "sha512-An6Ba4pHBiDtyVpSLymUUERMo2cU7s+Obz6BTrS+gxkbnSBNKSuD0AVUc+CpBMrpVPKKfoVz0WQCX+Tnst0i4A==", - "requires": {} + "integrity": "sha512-An6Ba4pHBiDtyVpSLymUUERMo2cU7s+Obz6BTrS+gxkbnSBNKSuD0AVUc+CpBMrpVPKKfoVz0WQCX+Tnst0i4A==" }, "postcss-ordered-values": { "version": "5.1.3", @@ -18302,8 +18618,7 @@ "postcss-page-break": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/postcss-page-break/-/postcss-page-break-3.0.4.tgz", - "integrity": "sha512-1JGu8oCjVXLa9q9rFTo4MbeeA5FMe00/9C7lN4va606Rdb+HkxXtXsmEDrIraQ11fGz/WvKWa8gMuCKkrXpTsQ==", - "requires": {} + "integrity": "sha512-1JGu8oCjVXLa9q9rFTo4MbeeA5FMe00/9C7lN4va606Rdb+HkxXtXsmEDrIraQ11fGz/WvKWa8gMuCKkrXpTsQ==" }, "postcss-place": { "version": "7.0.5", @@ -18464,8 +18779,7 @@ "postcss-replace-overflow-wrap": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/postcss-replace-overflow-wrap/-/postcss-replace-overflow-wrap-4.0.0.tgz", - "integrity": "sha512-KmF7SBPphT4gPPcKZc7aDkweHiKEEO8cla/GjcBK+ckKxiZslIu3C4GCRW3DNfL0o7yW7kMQu9xlZ1kXRXLXtw==", - "requires": {} + "integrity": "sha512-KmF7SBPphT4gPPcKZc7aDkweHiKEEO8cla/GjcBK+ckKxiZslIu3C4GCRW3DNfL0o7yW7kMQu9xlZ1kXRXLXtw==" }, "postcss-resolve-nested-selector": { "version": "0.1.1", @@ -18477,8 +18791,7 @@ "version": "4.0.2", "resolved": "https://registry.npmjs.org/postcss-scss/-/postcss-scss-4.0.2.tgz", "integrity": "sha512-xfdkU128CkKKKVAwkyt0M8OdnelJ3MRcIRAPPQkRpoPeuzWY3RIeg7piRCpZ79MK7Q16diLXMMAD9dN5mauPlQ==", - "dev": true, - "requires": {} + "dev": true }, "postcss-selector-not": { "version": "6.0.1", @@ -18512,8 +18825,7 @@ "version": "7.0.1", "resolved": "https://registry.npmjs.org/postcss-sorting/-/postcss-sorting-7.0.1.tgz", "integrity": "sha512-iLBFYz6VRYyLJEJsBJ8M3TCqNcckVzz4wFounSc5Oez35ogE/X+aoC5fFu103Ot7NyvjU3/xqIXn93Gp3kJk4g==", - "dev": true, - "requires": {} + "dev": true }, "postcss-svgo": { "version": "5.1.0", @@ -18791,7 +19103,8 @@ "raf-schd": { "version": "4.0.3", "resolved": "https://registry.npmjs.org/raf-schd/-/raf-schd-4.0.3.tgz", - "integrity": "sha512-tQkJl2GRWh83ui2DiPTJz9wEiMN20syf+5oKfB03yYP7ioZcJwsIK8FjrtLwH1m7C7e+Tt2yYBlrOpdT+dyeIQ==" + "integrity": "sha512-tQkJl2GRWh83ui2DiPTJz9wEiMN20syf+5oKfB03yYP7ioZcJwsIK8FjrtLwH1m7C7e+Tt2yYBlrOpdT+dyeIQ==", + "dev": true }, "randombytes": { "version": "2.1.0", @@ -18828,6 +19141,7 @@ "version": "17.0.2", "resolved": "https://registry.npmjs.org/react/-/react-17.0.2.tgz", "integrity": "sha512-gnhPt75i/dq/z3/6q/0asP78D0u592D5L1pd7M8P+dck6Fu/jJeL6iVVK23fptSUZj8Vjf++7wXA8UNclGQcbA==", + "dev": true, "requires": { "loose-envify": "^1.1.0", "object-assign": "^4.1.1" @@ -18867,6 +19181,7 @@ "version": "13.1.1", "resolved": "https://registry.npmjs.org/react-beautiful-dnd/-/react-beautiful-dnd-13.1.1.tgz", "integrity": "sha512-0Lvs4tq2VcrEjEgDXHjT98r+63drkKEgqyxdA7qD3mvKwga6a5SscbdLPO2IExotU1jW8L0Ksdl0Cj2AF67nPQ==", + "dev": true, "requires": { "@babel/runtime": "^7.9.2", "css-box-model": "^1.2.0", @@ -18881,6 +19196,7 @@ "version": "4.2.1", "resolved": "https://registry.npmjs.org/redux/-/redux-4.2.1.tgz", "integrity": "sha512-LAUYz4lc+Do8/g7aeRa8JkyDErK6ekstQaqWQrNRW//MY1TvCEpMtpTWvlQ+FPbWCx+Xixu/6SHt5N0HR+SB4w==", + "dev": true, "requires": { "@babel/runtime": "^7.9.2" } @@ -18891,6 +19207,7 @@ "version": "5.1.0", "resolved": "https://registry.npmjs.org/react-copy-to-clipboard/-/react-copy-to-clipboard-5.1.0.tgz", "integrity": "sha512-k61RsNgAayIJNoy9yDsYzDe/yAZAzEbEgcz3DZMhF686LEyukcE1hzurxe85JandPUG+yTfGVFzuEw3xt8WP/A==", + "dev": true, "requires": { "copy-to-clipboard": "^3.3.1", "prop-types": "^15.8.1" @@ -18985,6 +19302,7 @@ "version": "17.0.2", "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-17.0.2.tgz", "integrity": "sha512-s4h96KtLDUQlsENhMn1ar8t2bEa+q/YAtj8pPPdIjPDGBDIVNsrD9aXNWqspUe6AzKCIG0C1HZZLqLV7qpOBGA==", + "dev": true, "requires": { "loose-envify": "^1.1.0", "object-assign": "^4.1.1", @@ -19007,7 +19325,8 @@ "react-fast-compare": { "version": "3.2.2", "resolved": "https://registry.npmjs.org/react-fast-compare/-/react-fast-compare-3.2.2.tgz", - "integrity": "sha512-nsO+KSNgo1SbJqJEYRE9ERzo7YtYbou/OqjSQKxV7jcKox7+usiUVZOAC+XnDOABXggQTno0Y1CpVnuWEc1boQ==" + "integrity": "sha512-nsO+KSNgo1SbJqJEYRE9ERzo7YtYbou/OqjSQKxV7jcKox7+usiUVZOAC+XnDOABXggQTno0Y1CpVnuWEc1boQ==", + "dev": true }, "react-is": { "version": "17.0.2", @@ -19033,6 +19352,11 @@ } } }, + "react-lifecycles-compat": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/react-lifecycles-compat/-/react-lifecycles-compat-3.0.4.tgz", + "integrity": "sha512-fBASbA6LnOU9dOU2eW7aQ8xmYBSXUIWr+UmF9b1efZBazGNO+rcXT/icdKnYm2pTwcRylVUYwW7H1PHfLekVzA==" + }, "react-list": { "version": "0.8.11", "resolved": "https://registry.npmjs.org/react-list/-/react-list-0.8.11.tgz", @@ -19066,6 +19390,7 @@ "version": "2.3.0", "resolved": "https://registry.npmjs.org/react-popper/-/react-popper-2.3.0.tgz", "integrity": "sha512-e1hj8lL3uM+sgSR4Lxzn5h1GxBlpa4CQz0XLF8kx4MDrDRWY0Ena4c97PUeSX9i5W3UAfDP0z0FXCTQkoXUl3Q==", + "dev": true, "requires": { "react-fast-compare": "^3.0.1", "warning": "^4.0.2" @@ -19219,6 +19544,24 @@ "split.js": "^1.6.0" } }, + "react-split-pane": { + "version": "0.1.92", + "resolved": "https://registry.npmjs.org/react-split-pane/-/react-split-pane-0.1.92.tgz", + "integrity": "sha512-GfXP1xSzLMcLJI5BM36Vh7GgZBpy+U/X0no+VM3fxayv+p1Jly5HpMofZJraeaMl73b3hvlr+N9zJKvLB/uz9w==", + "requires": { + "prop-types": "^15.7.2", + "react-lifecycles-compat": "^3.0.4", + "react-style-proptype": "^3.2.2" + } + }, + "react-style-proptype": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/react-style-proptype/-/react-style-proptype-3.2.2.tgz", + "integrity": "sha512-ywYLSjNkxKHiZOqNlso9PZByNEY+FTyh3C+7uuziK0xFXu9xzdyfHwg4S9iyiRRoPCR4k2LqaBBsWVmSBwCWYQ==", + "requires": { + "prop-types": "^15.5.4" + } + }, "react-transition-group": { "version": "4.4.5", "resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-4.4.5.tgz", @@ -19234,12 +19577,13 @@ "version": "1.0.20", "resolved": "https://registry.npmjs.org/react-virtualized-auto-sizer/-/react-virtualized-auto-sizer-1.0.20.tgz", "integrity": "sha512-OdIyHwj4S4wyhbKHOKM1wLSj/UDXm839Z3Cvfg2a9j+He6yDa6i5p0qQvEiCnyQlGO/HyfSnigQwuxvYalaAXA==", - "requires": {} + "dev": true }, "react-window": { "version": "1.8.10", "resolved": "https://registry.npmjs.org/react-window/-/react-window-1.8.10.tgz", "integrity": "sha512-Y0Cx+dnU6NLa5/EvoHukUD0BklJ8qITCtVEPY1C/nL8wwoZ0b5aEw8Ff1dOVHw7fCzMt55XfJDd8S8W8LCaUCg==", + "dev": true, "requires": { "@babel/runtime": "^7.0.0", "memoize-one": ">=3.1.1 <6" @@ -19724,6 +20068,11 @@ "glob": "^7.1.3" } }, + "robust-predicates": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/robust-predicates/-/robust-predicates-3.0.2.tgz", + "integrity": "sha512-IXgzBWvWQwE6PrDI05OvmXUIruQTcoMDzRsOd5CDvHCVLcLHMTSYvOK5Cm46kWqlV3yAbuSpBZdJ5oP5OUoStg==" + }, "rollup": { "version": "2.79.1", "resolved": "https://registry.npmjs.org/rollup/-/rollup-2.79.1.tgz", @@ -19771,6 +20120,11 @@ "queue-microtask": "^1.2.2" } }, + "rw": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/rw/-/rw-1.3.3.tgz", + "integrity": "sha512-PdhdWy89SiZogBLaw42zdeqtRJ//zFd2PgQavcICDUgJT5oW10QCRKbJ6bg4r0/UY2M6BWd5tkxuGFRvCkgfHQ==" + }, "rxjs": { "version": "7.5.5", "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.5.5.tgz", @@ -19874,6 +20228,7 @@ "version": "0.20.2", "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.20.2.tgz", "integrity": "sha512-2eWfGgAqqWFGqtdMmcL5zCMK1U8KlXv8SQFGglL3CEtd0aDVDWgeF/YoCmvln55m5zSk3J/20hTaSBeSObsQDQ==", + "dev": true, "requires": { "loose-envify": "^1.1.0", "object-assign": "^4.1.1" @@ -19960,6 +20315,11 @@ "randombytes": "^2.1.0" } }, + "serialize-query-params": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/serialize-query-params/-/serialize-query-params-2.0.2.tgz", + "integrity": "sha512-1chMo1dST4pFA9RDXAtF0Rbjaut4is7bzFbI1Z26IuMub68pNCILku85aYmeFhvnY//BXUPUhoRMjYcsT93J/Q==" + }, "serve-index": { "version": "1.9.1", "resolved": "https://registry.npmjs.org/serve-index/-/serve-index-1.9.1.tgz", @@ -20359,14 +20719,6 @@ } } }, - "string_decoder": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", - "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", - "requires": { - "safe-buffer": "~5.2.0" - } - }, "string-argv": { "version": "0.3.1", "resolved": "https://registry.npmjs.org/string-argv/-/string-argv-0.3.1.tgz", @@ -20436,6 +20788,14 @@ "define-properties": "^1.1.3" } }, + "string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "requires": { + "safe-buffer": "~5.2.0" + } + }, "stringify-object": { "version": "3.3.0", "resolved": "https://registry.npmjs.org/stringify-object/-/stringify-object-3.3.0.tgz", @@ -20499,8 +20859,7 @@ "style-loader": { "version": "3.3.1", "resolved": "https://registry.npmjs.org/style-loader/-/style-loader-3.3.1.tgz", - "integrity": "sha512-GPcQ+LDJbrcxHORTRes6Jy2sfvK2kS6hpSfI/fXhPt+spVzxF6LJ1dHLN9zIGmVaaP044YKaIatFaufENRiDoQ==", - "requires": {} + "integrity": "sha512-GPcQ+LDJbrcxHORTRes6Jy2sfvK2kS6hpSfI/fXhPt+spVzxF6LJ1dHLN9zIGmVaaP044YKaIatFaufENRiDoQ==" }, "style-search": { "version": "0.1.0", @@ -20650,8 +21009,7 @@ "version": "6.0.0", "resolved": "https://registry.npmjs.org/postcss-safe-parser/-/postcss-safe-parser-6.0.0.tgz", "integrity": "sha512-FARHN8pwH+WiS2OPCxJI8FuRJpTVnn6ZNFiqAM2aeW2LwTHWWmWgIyKC6cUo0L8aeKiF/14MNvnpls6R2PBeMQ==", - "dev": true, - "requires": {} + "dev": true }, "resolve-from": { "version": "5.0.0", @@ -20687,8 +21045,7 @@ "version": "9.0.3", "resolved": "https://registry.npmjs.org/stylelint-config-prettier/-/stylelint-config-prettier-9.0.3.tgz", "integrity": "sha512-5n9gUDp/n5tTMCq1GLqSpA30w2sqWITSSEiAWQlpxkKGAUbjcemQ0nbkRvRUa0B1LgD3+hCvdL7B1eTxy1QHJg==", - "dev": true, - "requires": {} + "dev": true }, "stylelint-order": { "version": "5.0.0", @@ -20855,7 +21212,8 @@ "tabbable": { "version": "6.2.0", "resolved": "https://registry.npmjs.org/tabbable/-/tabbable-6.2.0.tgz", - "integrity": "sha512-Cat63mxsVJlzYvN51JmVXIgNoUokrIaT2zLclCXjRd8boZ0004U4KCs/sToJ75C6sdlByWxpYnb5Boif1VSFew==" + "integrity": "sha512-Cat63mxsVJlzYvN51JmVXIgNoUokrIaT2zLclCXjRd8boZ0004U4KCs/sToJ75C6sdlByWxpYnb5Boif1VSFew==", + "dev": true }, "table": { "version": "6.8.0", @@ -21327,7 +21685,7 @@ "version": "9.1.1", "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-9.1.1.tgz", "integrity": "sha512-hPlt7ZACERQGf03M253ytLY3dHbGNGrAq9qIHWUY9XHYl1z7wYngSr3OQ5xmui8o2AaxsONxIzjafLUiWBo1Fg==", - "devOptional": true, + "dev": true, "requires": { "arg": "^4.1.0", "create-require": "^1.1.0", @@ -21439,7 +21797,8 @@ "typescript": { "version": "4.6.3", "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.6.3.tgz", - "integrity": "sha512-yNIatDa5iaofVozS/uQJEl3JRWLKKGJKh6Yaiv0GLGSuhpFJe7P3SbHZ8/yjAHRQwKRoA6YZqlfjXWmVzoVSMw==" + "integrity": "sha512-yNIatDa5iaofVozS/uQJEl3JRWLKKGJKh6Yaiv0GLGSuhpFJe7P3SbHZ8/yjAHRQwKRoA6YZqlfjXWmVzoVSMw==", + "dev": true }, "unbox-primitive": { "version": "1.0.1", @@ -21528,6 +21887,11 @@ "picocolors": "^1.0.0" } }, + "uplot": { + "version": "1.6.27", + "resolved": "https://registry.npmjs.org/uplot/-/uplot-1.6.27.tgz", + "integrity": "sha512-78U4ss5YeU65kQkOC/QAKiyII+4uo+TYUJJKvuxRzeSpk/s5sjpY1TL0agkmhHBBShpvLtmbHIEiM7+C5lBULg==" + }, "uri-js": { "version": "4.4.1", "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", @@ -21565,7 +21929,15 @@ "version": "1.1.3", "resolved": "https://registry.npmjs.org/use-memo-one/-/use-memo-one-1.1.3.tgz", "integrity": "sha512-g66/K7ZQGYrI6dy8GLpVcMsBp4s17xNkYJVSMvTEevGy3nDxHOfE6z8BVE22+5G5x7t3+bhzrlTDB7ObrEE0cQ==", - "requires": {} + "dev": true + }, + "use-query-params": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/use-query-params/-/use-query-params-2.2.1.tgz", + "integrity": "sha512-i6alcyLB8w9i3ZK3caNftdb+UnbfBRNPDnc89CNQWkGRmDrm/gfydHvMBfVsQJRq3NoHOM2dt/ceBWG2397v1Q==", + "requires": { + "serialize-query-params": "^2.0.2" + } }, "util-deprecate": { "version": "1.0.2", @@ -21591,7 +21963,8 @@ "utility-types": { "version": "3.10.0", "resolved": "https://registry.npmjs.org/utility-types/-/utility-types-3.10.0.tgz", - "integrity": "sha512-O11mqxmi7wMKCo6HKFt5AhO4BwY3VV68YU07tgxfz8zJTIxr4BpsezN49Ffwy9j3ZpwwJp4fkRwjRzq3uWE6Rg==" + "integrity": "sha512-O11mqxmi7wMKCo6HKFt5AhO4BwY3VV68YU07tgxfz8zJTIxr4BpsezN49Ffwy9j3ZpwwJp4fkRwjRzq3uWE6Rg==", + "dev": true }, "utils-merge": { "version": "1.0.1", @@ -21697,6 +22070,7 @@ "version": "4.0.3", "resolved": "https://registry.npmjs.org/warning/-/warning-4.0.3.tgz", "integrity": "sha512-rpJyN222KWIvHJ/F53XSZv0Zl/accqHR8et1kpaMTD/fLCRxtV8iX8czMzY7sVZupTI3zcUTg8eycS2kNF9l6w==", + "dev": true, "requires": { "loose-envify": "^1.0.0" } @@ -21895,8 +22269,7 @@ "ws": { "version": "8.12.1", "resolved": "https://registry.npmjs.org/ws/-/ws-8.12.1.tgz", - "integrity": "sha512-1qo+M9Ba+xNhPB+YTWUlK6M17brTut5EXbcBaMRN5pH5dFrXz7lzz1ChFSUq3bOUl8yEvSenhHmYUNJxFzdJew==", - "requires": {} + "integrity": "sha512-1qo+M9Ba+xNhPB+YTWUlK6M17brTut5EXbcBaMRN5pH5dFrXz7lzz1ChFSUq3bOUl8yEvSenhHmYUNJxFzdJew==" } } }, @@ -22316,8 +22689,7 @@ "ws": { "version": "7.5.7", "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.7.tgz", - "integrity": "sha512-KMvVuFzpKBuiIXW3E4u3mySRO2/mCHSyZDJQM5NQ9Q9KHWHWh0NHgfbRMLLrceUK5qAL4ytALJbpRMjixFZh8A==", - "requires": {} + "integrity": "sha512-KMvVuFzpKBuiIXW3E4u3mySRO2/mCHSyZDJQM5NQ9Q9KHWHWh0NHgfbRMLLrceUK5qAL4ytALJbpRMjixFZh8A==" }, "xml-name-validator": { "version": "3.0.0", @@ -22400,7 +22772,7 @@ "version": "3.1.1", "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==", - "devOptional": true + "dev": true }, "yocto-queue": { "version": "0.1.0", diff --git a/package.json b/package.json index 6e1e5e90c..75c61ed5f 100644 --- a/package.json +++ b/package.json @@ -10,6 +10,7 @@ }, "dependencies": { "@gravity-ui/axios-wrapper": "^1.3.0", + "@gravity-ui/chartkit": "^4.15.0", "@gravity-ui/components": "^2.9.1", "@gravity-ui/date-utils": "^1.1.1", "@gravity-ui/i18n": "^1.0.0", @@ -45,6 +46,7 @@ "reselect": "4.1.6", "sass": "1.32.8", "url": "^0.11.0", + "use-query-params": "^2.2.1", "web-vitals": "1.1.2", "ydb-ui-components": "^3.6.0" }, diff --git a/src/components/MetricChart/MetricChart.scss b/src/components/MetricChart/MetricChart.scss new file mode 100644 index 000000000..1632146ce --- /dev/null +++ b/src/components/MetricChart/MetricChart.scss @@ -0,0 +1,34 @@ +.ydb-metric-chart { + display: flex; + flex-direction: column; + + padding: 16px 16px 8px; + + border: 1px solid var(--g-color-line-generic); + border-radius: 8px; + + &__title { + margin-bottom: 10px; + } + + &__chart { + position: relative; + + display: flex; + overflow: hidden; + + width: 100%; + height: 100%; + } + + &__error { + position: absolute; + z-index: 1; + top: 10%; + left: 50%; + + text-align: center; + + transform: translateX(-50%); + } +} diff --git a/src/components/MetricChart/MetricChart.tsx b/src/components/MetricChart/MetricChart.tsx new file mode 100644 index 000000000..a2927336b --- /dev/null +++ b/src/components/MetricChart/MetricChart.tsx @@ -0,0 +1,198 @@ +import {useCallback, useEffect, useReducer, useRef} from 'react'; + +import {RawSerieData, YagrPlugin, YagrWidgetData} from '@gravity-ui/chartkit/yagr'; +import ChartKit, {settings} from '@gravity-ui/chartkit'; + +import type {IResponseError} from '../../types/api/error'; +import type {TimeFrame} from '../../utils/timeframes'; +import {useAutofetcher} from '../../utils/hooks'; +import {COLORS} from '../../utils/versions'; +import {cn} from '../../utils/cn'; + +import {Loader} from '../Loader'; +import {ResponseError} from '../Errors/ResponseError'; + +import type {ChartOptions, MetricDescription, PreparedMetricsData} from './types'; +import {convertResponse} from './convertReponse'; +import {getDefaultDataFormatter} from './getDefaultDataFormatter'; +import {getChartData} from './getChartData'; +import { + chartReducer, + initialChartState, + setChartData, + setChartDataLoading, + setChartDataWasNotLoaded, + setChartError, +} from './reducer'; + +import './MetricChart.scss'; + +const b = cn('ydb-metric-chart'); + +settings.set({plugins: [YagrPlugin]}); + +const prepareWidgetData = ( + data: PreparedMetricsData, + options: ChartOptions = {}, +): YagrWidgetData => { + const {dataType} = options; + const defaultDataFormatter = getDefaultDataFormatter(dataType); + + const isDataEmpty = !data.metrics.length; + + const graphs: RawSerieData[] = data.metrics.map((metric, index) => { + return { + id: metric.target, + name: metric.title || metric.target, + color: metric.color || COLORS[index], + data: metric.data, + formatter: defaultDataFormatter, + }; + }); + + return { + data: { + timeline: data.timeline, + graphs, + }, + + libraryConfig: { + chart: { + size: { + // When empty data chart is displayed without axes it have different paddings + // To compensate it, additional paddings are applied + padding: isDataEmpty ? [10, 0, 10, 0] : undefined, + }, + series: { + type: 'line', + }, + select: { + zoom: false, + }, + }, + scales: { + y: { + type: 'linear', + range: 'nice', + }, + }, + axes: { + y: { + values: defaultDataFormatter + ? (_, ticks) => ticks.map(defaultDataFormatter) + : undefined, + }, + }, + tooltip: { + show: true, + tracking: 'sticky', + }, + }, + }; +}; + +interface DiagnosticsChartProps { + title?: string; + metrics: MetricDescription[]; + timeFrame?: TimeFrame; + + autorefresh?: boolean; + + height?: number; + width?: number; + + chartOptions?: ChartOptions; +} + +export const MetricChart = ({ + title, + metrics, + timeFrame = '1h', + autorefresh, + width = 400, + height = width / 1.5, + chartOptions, +}: DiagnosticsChartProps) => { + const mounted = useRef(false); + + useEffect(() => { + mounted.current = true; + return () => { + mounted.current = false; + }; + }, []); + + const [{loading, wasLoaded, data, error}, dispatch] = useReducer( + chartReducer, + initialChartState, + ); + + const fetchChartData = useCallback( + async (isBackground: boolean) => { + dispatch(setChartDataLoading()); + + if (!isBackground) { + dispatch(setChartDataWasNotLoaded()); + } + + try { + // maxDataPoints param is calculated based on width + // should be width > maxDataPoints to prevent points that cannot be selected + // more px per dataPoint - easier to select, less - chart is smoother + const response = await getChartData({ + metrics, + timeFrame, + maxDataPoints: width / 2, + }); + + // Hack to prevent setting value to state, if component unmounted + if (!mounted.current) return; + + // In some cases error could be in response with 200 status code + // It happens when request is OK, but chart data cannot be returned due to some reason + // Example: charts are not enabled in the DB ('GraphShard is not enabled' error) + if (Array.isArray(response)) { + const preparedData = convertResponse(response, metrics); + dispatch(setChartData(preparedData)); + } else { + dispatch(setChartError({statusText: response.error})); + } + } catch (err) { + if (!mounted.current) return; + + dispatch(setChartError(err as IResponseError)); + } + }, + [metrics, timeFrame, width], + ); + + useAutofetcher(fetchChartData, [fetchChartData], autorefresh); + + const convertedData = prepareWidgetData(data, chartOptions); + + const renderContent = () => { + if (loading && !wasLoaded) { + return ; + } + + return ( +
+ + {error && } +
+ ); + }; + + return ( +
+
{title}
+ {renderContent()} +
+ ); +}; diff --git a/src/components/MetricChart/convertReponse.ts b/src/components/MetricChart/convertReponse.ts new file mode 100644 index 000000000..1a4ced6c2 --- /dev/null +++ b/src/components/MetricChart/convertReponse.ts @@ -0,0 +1,32 @@ +import type {MetricData} from '../../types/api/render'; +import type {MetricDescription, PreparedMetric, PreparedMetricsData} from './types'; + +export const convertResponse = ( + data: MetricData[] = [], + metrics: MetricDescription[], +): PreparedMetricsData => { + const preparedMetrics = data + .map(({datapoints, target}) => { + const metricDescription = metrics.find((metric) => metric.target === target); + const chartData = datapoints.map((datapoint) => datapoint[0] || 0); + + if (!metricDescription) { + return undefined; + } + + return { + ...metricDescription, + data: chartData, + }; + }) + .filter((metric): metric is PreparedMetric => metric !== undefined); + + // Asuming all metrics in response have the same timeline + // Backend return data in seconds, while chart needs ms + const timeline = data[0].datapoints.map((datapoint) => datapoint[1] * 1000); + + return { + timeline, + metrics: preparedMetrics, + }; +}; diff --git a/src/components/MetricChart/getChartData.ts b/src/components/MetricChart/getChartData.ts new file mode 100644 index 000000000..99cec87d2 --- /dev/null +++ b/src/components/MetricChart/getChartData.ts @@ -0,0 +1,20 @@ +import {TIMEFRAMES, type TimeFrame} from '../../utils/timeframes'; +import type {MetricDescription} from './types'; + +interface GetChartDataParams { + metrics: MetricDescription[]; + timeFrame: TimeFrame; + maxDataPoints: number; +} + +export const getChartData = async ({metrics, timeFrame, maxDataPoints}: GetChartDataParams) => { + const targetString = metrics.map((metric) => `target=${metric.target}`).join('&'); + + const until = Math.round(Date.now() / 1000); + const from = until - TIMEFRAMES[timeFrame]; + + return window.api.getChartData( + {target: targetString, from, until, maxDataPoints}, + {concurrentId: `getChartData|${targetString}`}, + ); +}; diff --git a/src/components/MetricChart/getDefaultDataFormatter.ts b/src/components/MetricChart/getDefaultDataFormatter.ts new file mode 100644 index 000000000..d421f7f5b --- /dev/null +++ b/src/components/MetricChart/getDefaultDataFormatter.ts @@ -0,0 +1,36 @@ +import {formatBytes} from '../../utils/bytesParsers'; +import {roundToPrecision} from '../../utils/dataFormatters/dataFormatters'; +import {formatToMs} from '../../utils/timeParsers'; +import {isNumeric} from '../../utils/utils'; + +import type {ChartDataType, ChartValue} from './types'; + +export const getDefaultDataFormatter = (dataType?: ChartDataType) => { + switch (dataType) { + case 'ms': { + return formatChartValueToMs; + } + case 'size': { + return formatChartValueToSize; + } + default: + return undefined; + } +}; + +function formatChartValueToMs(value: ChartValue) { + return formatToMs(roundToPrecision(convertToNumber(value), 2)); +} + +function formatChartValueToSize(value: ChartValue) { + return formatBytes({value: convertToNumber(value), precision: 3}); +} + +// Numeric values expected, not numeric value should be displayd as 0 +function convertToNumber(value: unknown): number { + if (isNumeric(value)) { + return Number(value); + } + + return 0; +} diff --git a/src/components/MetricChart/index.ts b/src/components/MetricChart/index.ts new file mode 100644 index 000000000..0d3adfec5 --- /dev/null +++ b/src/components/MetricChart/index.ts @@ -0,0 +1,2 @@ +export type {MetricDescription, Metric, ChartOptions} from './types'; +export {MetricChart} from './MetricChart'; diff --git a/src/components/MetricChart/reducer.ts b/src/components/MetricChart/reducer.ts new file mode 100644 index 000000000..014d76b40 --- /dev/null +++ b/src/components/MetricChart/reducer.ts @@ -0,0 +1,86 @@ +import {createRequestActionTypes} from '../../store/utils'; +import type {IResponseError} from '../../types/api/error'; + +import type {PreparedMetricsData} from './types'; + +const FETCH_CHART_DATA = createRequestActionTypes('chart', 'FETCH_CHART_DATA'); +const SET_CHART_DATA_WAS_NOT_LOADED = 'chart/SET_DATA_WAS_NOT_LOADED'; + +export const setChartDataLoading = () => { + return { + type: FETCH_CHART_DATA.REQUEST, + } as const; +}; + +export const setChartData = (data: PreparedMetricsData) => { + return { + data, + type: FETCH_CHART_DATA.SUCCESS, + } as const; +}; + +export const setChartError = (error: IResponseError) => { + return { + error, + type: FETCH_CHART_DATA.FAILURE, + } as const; +}; + +export const setChartDataWasNotLoaded = () => { + return { + type: SET_CHART_DATA_WAS_NOT_LOADED, + } as const; +}; + +type ChartAction = + | ReturnType + | ReturnType + | ReturnType + | ReturnType; + +interface ChartState { + loading: boolean; + wasLoaded: boolean; + data: PreparedMetricsData; + error: IResponseError | undefined; +} + +export const initialChartState: ChartState = { + // Set chart initial state as loading, in order not to mount and unmount component in between requests + // as it leads to memory leak errors in console (not proper useEffect cleanups in chart component itself) + // TODO: possible fix (check needed): chart component is always present, but display: none for chart while loading + loading: true, + wasLoaded: false, + data: {timeline: [], metrics: []}, + error: undefined, +}; + +export const chartReducer = (state: ChartState, action: ChartAction) => { + switch (action.type) { + case FETCH_CHART_DATA.REQUEST: { + return {...state, loading: true}; + } + case FETCH_CHART_DATA.SUCCESS: { + return {...state, loading: false, wasLoaded: true, error: undefined, data: action.data}; + } + case FETCH_CHART_DATA.FAILURE: { + if (action.error?.isCancelled) { + return state; + } + + return { + ...state, + error: action.error, + // Clear data, so error will be displayed with empty chart + data: {timeline: [], metrics: []}, + loading: false, + wasLoaded: true, + }; + } + case SET_CHART_DATA_WAS_NOT_LOADED: { + return {...state, wasLoaded: false}; + } + default: + return state; + } +}; diff --git a/src/components/MetricChart/types.ts b/src/components/MetricChart/types.ts new file mode 100644 index 000000000..179250123 --- /dev/null +++ b/src/components/MetricChart/types.ts @@ -0,0 +1,32 @@ +export type Metric = + | 'queries.requests' + | 'queries.latencies.p50' + | 'queries.latencies.p75' + | 'queries.latencies.p90' + | 'queries.latencies.p99' + | 'resources.cpu.usage' + | 'resources.memory.used_bytes' + | 'resources.storage.used_bytes'; + +export interface MetricDescription { + target: Metric; + title?: string; + color?: string; +} + +export interface PreparedMetric extends MetricDescription { + data: number[]; +} + +export interface PreparedMetricsData { + timeline: number[]; + metrics: PreparedMetric[]; +} + +export type ChartValue = number | string | null; + +export type ChartDataType = 'ms' | 'size'; + +export interface ChartOptions { + dataType?: ChartDataType; +} diff --git a/src/components/TimeFrameSelector/TimeFrameSelector.scss b/src/components/TimeFrameSelector/TimeFrameSelector.scss new file mode 100644 index 000000000..f4ff043f2 --- /dev/null +++ b/src/components/TimeFrameSelector/TimeFrameSelector.scss @@ -0,0 +1,5 @@ +.ydb-timeframe-selector { + display: flex; + + gap: 2px; +} diff --git a/src/components/TimeFrameSelector/TimeFrameSelector.tsx b/src/components/TimeFrameSelector/TimeFrameSelector.tsx new file mode 100644 index 000000000..5729f2535 --- /dev/null +++ b/src/components/TimeFrameSelector/TimeFrameSelector.tsx @@ -0,0 +1,33 @@ +import {Button} from '@gravity-ui/uikit'; + +import {cn} from '../../utils/cn'; +import {TIMEFRAMES, type TimeFrame} from '../../utils/timeframes'; + +import './TimeFrameSelector.scss'; + +const b = cn('ydb-timeframe-selector'); + +interface TimeFrameSelectorProps { + value: TimeFrame; + onChange: (value: TimeFrame) => void; + className?: string; +} + +export const TimeFrameSelector = ({value, onChange, className}: TimeFrameSelectorProps) => { + return ( +
+ {Object.keys(TIMEFRAMES).map((timeFrame) => { + return ( + + ); + })} +
+ ); +}; diff --git a/src/containers/App/Content.js b/src/containers/App/Content.js index 9fa23a6b4..95856df74 100644 --- a/src/containers/App/Content.js +++ b/src/containers/App/Content.js @@ -1,5 +1,7 @@ import React from 'react'; import {Switch, Route, Redirect, Router, useLocation} from 'react-router-dom'; +import {QueryParamProvider} from 'use-query-params'; +import {ReactRouter5Adapter} from 'use-query-params/adapters/react-router-5'; import cn from 'bem-cn-lite'; import {connect} from 'react-redux'; @@ -80,18 +82,20 @@ function ContentWrapper(props) { {(history) => ( - - - - - - -
- {isAuthenticated ? props.children : } -
-
-
-
+ + + + + + + +
+ {isAuthenticated ? props.children : } +
+
+
+
+
)}
diff --git a/src/containers/Tenant/Diagnostics/TenantOverview/DefaultDashboard.tsx b/src/containers/Tenant/Diagnostics/TenantOverview/DefaultDashboard.tsx new file mode 100644 index 000000000..71d8d6dc6 --- /dev/null +++ b/src/containers/Tenant/Diagnostics/TenantOverview/DefaultDashboard.tsx @@ -0,0 +1,50 @@ +import {type ChartConfig, TenantDashboard} from './TenantDashboard/TenantDashboard'; +import i18n from './i18n'; + +const defaultDashboardConfig: ChartConfig[] = [ + { + title: i18n('charts.queries-per-second'), + metrics: [ + { + target: 'queries.requests', + title: i18n('charts.queries-per-second'), + }, + ], + }, + { + title: i18n('charts.transaction-latency', {percentile: ''}), + metrics: [ + { + target: 'queries.latencies.p50', + title: i18n('charts.transaction-latency', { + percentile: 'p50', + }), + }, + { + target: 'queries.latencies.p75', + title: i18n('charts.transaction-latency', { + percentile: 'p75', + }), + }, + { + target: 'queries.latencies.p90', + title: i18n('charts.transaction-latency', { + percentile: 'p90', + }), + }, + { + target: 'queries.latencies.p99', + title: i18n('charts.transaction-latency', { + percentile: 'p99', + }), + }, + ], + options: { + dataType: 'ms', + }, + }, +]; + +export const DefaultDashboard = () => { + return ; +}; diff --git a/src/containers/Tenant/Diagnostics/TenantOverview/MetricsCards/MetricsCards.tsx b/src/containers/Tenant/Diagnostics/TenantOverview/MetricsCards/MetricsCards.tsx index d2551e6ff..a26663afb 100644 --- a/src/containers/Tenant/Diagnostics/TenantOverview/MetricsCards/MetricsCards.tsx +++ b/src/containers/Tenant/Diagnostics/TenantOverview/MetricsCards/MetricsCards.tsx @@ -72,22 +72,31 @@ export function MetricsCards({ const queryParams = parseQuery(location); + // Allow tabs untoggle behaviour + const getTabIfNotActive = (tab: TenantMetricsTab) => { + if (tab === metricsTab) { + return ''; + } + + return tab; + }; + const tabLinks: Record = { [TENANT_METRICS_TABS_IDS.cpu]: getTenantPath({ ...queryParams, - [TenantTabsGroups.metricsTab]: TENANT_METRICS_TABS_IDS.cpu, + [TenantTabsGroups.metricsTab]: getTabIfNotActive(TENANT_METRICS_TABS_IDS.cpu), }), [TENANT_METRICS_TABS_IDS.storage]: getTenantPath({ ...queryParams, - [TenantTabsGroups.metricsTab]: TENANT_METRICS_TABS_IDS.storage, + [TenantTabsGroups.metricsTab]: getTabIfNotActive(TENANT_METRICS_TABS_IDS.storage), }), [TENANT_METRICS_TABS_IDS.memory]: getTenantPath({ ...queryParams, - [TenantTabsGroups.metricsTab]: TENANT_METRICS_TABS_IDS.memory, + [TenantTabsGroups.metricsTab]: getTabIfNotActive(TENANT_METRICS_TABS_IDS.memory), }), [TENANT_METRICS_TABS_IDS.healthcheck]: getTenantPath({ ...queryParams, - [TenantTabsGroups.metricsTab]: TENANT_METRICS_TABS_IDS.healthcheck, + [TenantTabsGroups.metricsTab]: getTabIfNotActive(TENANT_METRICS_TABS_IDS.healthcheck), }), }; diff --git a/src/containers/Tenant/Diagnostics/TenantOverview/TenantCpu/CpuDashboard.tsx b/src/containers/Tenant/Diagnostics/TenantOverview/TenantCpu/CpuDashboard.tsx new file mode 100644 index 000000000..dfb220f9f --- /dev/null +++ b/src/containers/Tenant/Diagnostics/TenantOverview/TenantCpu/CpuDashboard.tsx @@ -0,0 +1,18 @@ +import {type ChartConfig, TenantDashboard} from '../TenantDashboard/TenantDashboard'; +import i18n from '../i18n'; + +const cpuDashboardConfig: ChartConfig[] = [ + { + title: i18n('charts.cpu-usage'), + metrics: [ + { + target: 'resources.cpu.usage', + title: i18n('charts.cpu-usage'), + }, + ], + }, +]; + +export const CpuDashboard = () => { + return ; +}; diff --git a/src/containers/Tenant/Diagnostics/TenantOverview/TenantCpu/TenantCpu.tsx b/src/containers/Tenant/Diagnostics/TenantOverview/TenantCpu/TenantCpu.tsx index bf4df1d27..5fb16b71b 100644 --- a/src/containers/Tenant/Diagnostics/TenantOverview/TenantCpu/TenantCpu.tsx +++ b/src/containers/Tenant/Diagnostics/TenantOverview/TenantCpu/TenantCpu.tsx @@ -1,4 +1,5 @@ import type {AdditionalNodesProps} from '../../../../../types/additionalProps'; +import {CpuDashboard} from './CpuDashboard'; import {TopNodesByLoad} from './TopNodesByLoad'; import {TopNodesByCpu} from './TopNodesByCpu'; import {TopShards} from './TopShards'; @@ -12,6 +13,7 @@ interface TenantCpuProps { export function TenantCpu({path, additionalNodesProps}: TenantCpuProps) { return ( <> + diff --git a/src/containers/Tenant/Diagnostics/TenantOverview/TenantDashboard/TenantDashboard.scss b/src/containers/Tenant/Diagnostics/TenantOverview/TenantDashboard/TenantDashboard.scss new file mode 100644 index 000000000..b06414098 --- /dev/null +++ b/src/containers/Tenant/Diagnostics/TenantOverview/TenantDashboard/TenantDashboard.scss @@ -0,0 +1,14 @@ +.ydb-tenant-dashboard { + width: var(--diagnostics-section-table-width); + margin-bottom: var(--diagnostics-section-margin); + + &__controls { + margin-bottom: 10px; + } + + &__charts { + display: flex; + flex-flow: row wrap; + gap: 16px; + } +} diff --git a/src/containers/Tenant/Diagnostics/TenantOverview/TenantDashboard/TenantDashboard.tsx b/src/containers/Tenant/Diagnostics/TenantOverview/TenantDashboard/TenantDashboard.tsx new file mode 100644 index 000000000..467552ebc --- /dev/null +++ b/src/containers/Tenant/Diagnostics/TenantOverview/TenantDashboard/TenantDashboard.tsx @@ -0,0 +1,71 @@ +import {StringParam, useQueryParam} from 'use-query-params'; + +import {cn} from '../../../../../utils/cn'; +import type {TimeFrame} from '../../../../../utils/timeframes'; +import {useSetting, useTypedSelector} from '../../../../../utils/hooks'; +import {DISPLAY_CHARTS_IN_DB_DIAGNOSTICS_KEY} from '../../../../../utils/constants'; +import {TimeFrameSelector} from '../../../../../components/TimeFrameSelector/TimeFrameSelector'; +import { + type ChartOptions, + MetricChart, + type MetricDescription, +} from '../../../../../components/MetricChart'; + +import './TenantDashboard.scss'; + +const CHART_WIDTH = 428; +const CHART_WIDTH_FULL = 872; + +const b = cn('ydb-tenant-dashboard'); + +export interface ChartConfig { + metrics: MetricDescription[]; + title: string; + options?: ChartOptions; +} + +interface TenantDashboardProps { + charts: ChartConfig[]; +} + +export const TenantDashboard = ({charts}: TenantDashboardProps) => { + const [timeFrame = '1h', setTimeframe] = useQueryParam('timeframe', StringParam); + + const {autorefresh} = useTypedSelector((state) => state.schema); + + const [chartsEnabled] = useSetting(DISPLAY_CHARTS_IN_DB_DIAGNOSTICS_KEY); + + if (!chartsEnabled) { + return null; + } + + // If there is only one chart, display it with full width + const chartWidth = charts.length === 1 ? CHART_WIDTH_FULL : CHART_WIDTH; + const chartHeight = CHART_WIDTH / 1.5; + + const renderContent = () => { + return charts.map((chartConfig) => { + return ( + target).join('&')} + title={chartConfig.title} + metrics={chartConfig.metrics} + timeFrame={timeFrame as TimeFrame} + chartOptions={chartConfig.options} + autorefresh={autorefresh} + width={chartWidth} + height={chartHeight} + /> + ); + }); + }; + + return ( +
+
+ +
+
{renderContent()}
+
+ ); +}; diff --git a/src/containers/Tenant/Diagnostics/TenantOverview/TenantMemory/MemoryDashboard.tsx b/src/containers/Tenant/Diagnostics/TenantOverview/TenantMemory/MemoryDashboard.tsx new file mode 100644 index 000000000..0c600c3ea --- /dev/null +++ b/src/containers/Tenant/Diagnostics/TenantOverview/TenantMemory/MemoryDashboard.tsx @@ -0,0 +1,21 @@ +import {type ChartConfig, TenantDashboard} from '../TenantDashboard/TenantDashboard'; +import i18n from '../i18n'; + +const memoryDashboardConfig: ChartConfig[] = [ + { + title: i18n('charts.memory-usage'), + metrics: [ + { + target: 'resources.memory.used_bytes', + title: i18n('charts.memory-usage'), + }, + ], + options: { + dataType: 'size', + }, + }, +]; + +export const MemoryDashboard = () => { + return ; +}; diff --git a/src/containers/Tenant/Diagnostics/TenantOverview/TenantMemory/TenantMemory.tsx b/src/containers/Tenant/Diagnostics/TenantOverview/TenantMemory/TenantMemory.tsx index b7df68e05..ddc51a3d0 100644 --- a/src/containers/Tenant/Diagnostics/TenantOverview/TenantMemory/TenantMemory.tsx +++ b/src/containers/Tenant/Diagnostics/TenantOverview/TenantMemory/TenantMemory.tsx @@ -1,3 +1,4 @@ +import {MemoryDashboard} from './MemoryDashboard'; import {TopNodesByMemory} from './TopNodesByMemory'; interface TenantMemoryProps { @@ -5,5 +6,10 @@ interface TenantMemoryProps { } export function TenantMemory({path}: TenantMemoryProps) { - return ; + return ( + <> + + + + ); } diff --git a/src/containers/Tenant/Diagnostics/TenantOverview/TenantOverview.tsx b/src/containers/Tenant/Diagnostics/TenantOverview/TenantOverview.tsx index 95f71b7d8..0e971735d 100644 --- a/src/containers/Tenant/Diagnostics/TenantOverview/TenantOverview.tsx +++ b/src/containers/Tenant/Diagnostics/TenantOverview/TenantOverview.tsx @@ -17,6 +17,7 @@ import {HealthcheckDetails} from './Healthcheck/HealthcheckDetails'; import {MetricsCards, type TenantMetrics} from './MetricsCards/MetricsCards'; import {TenantStorage} from './TenantStorage/TenantStorage'; import {TenantMemory} from './TenantMemory/TenantMemory'; +import {DefaultDashboard} from './DefaultDashboard'; import {useHealthcheck} from './useHealthcheck'; import './TenantOverview.scss'; @@ -140,7 +141,7 @@ export function TenantOverview({ ); } default: { - return undefined; + return ; } } }; diff --git a/src/containers/Tenant/Diagnostics/TenantOverview/TenantStorage/StorageDashboard.tsx b/src/containers/Tenant/Diagnostics/TenantOverview/TenantStorage/StorageDashboard.tsx new file mode 100644 index 000000000..178094620 --- /dev/null +++ b/src/containers/Tenant/Diagnostics/TenantOverview/TenantStorage/StorageDashboard.tsx @@ -0,0 +1,21 @@ +import {type ChartConfig, TenantDashboard} from '../TenantDashboard/TenantDashboard'; +import i18n from '../i18n'; + +const storageDashboardConfig: ChartConfig[] = [ + { + title: i18n('charts.storage-usage'), + metrics: [ + { + target: 'resources.storage.used_bytes', + title: i18n('charts.storage-usage'), + }, + ], + options: { + dataType: 'size', + }, + }, +]; + +export const StorageDashboard = () => { + return ; +}; diff --git a/src/containers/Tenant/Diagnostics/TenantOverview/TenantStorage/TenantStorage.tsx b/src/containers/Tenant/Diagnostics/TenantOverview/TenantStorage/TenantStorage.tsx index 322f54033..290fcb89e 100644 --- a/src/containers/Tenant/Diagnostics/TenantOverview/TenantStorage/TenantStorage.tsx +++ b/src/containers/Tenant/Diagnostics/TenantOverview/TenantStorage/TenantStorage.tsx @@ -7,6 +7,7 @@ import {getSizeWithSignificantDigits} from '../../../../../utils/bytesParsers'; import '../TenantOverview.scss'; +import {StorageDashboard} from './StorageDashboard'; import {TopTables} from './TopTables'; import {TopGroups} from './TopGroups'; @@ -62,6 +63,7 @@ export function TenantStorage({tenantName, metrics}: TenantStorageProps) { ]; return ( <> + diff --git a/src/containers/Tenant/Diagnostics/TenantOverview/i18n/en.json b/src/containers/Tenant/Diagnostics/TenantOverview/i18n/en.json index 35465220a..57b543ac2 100644 --- a/src/containers/Tenant/Diagnostics/TenantOverview/i18n/en.json +++ b/src/containers/Tenant/Diagnostics/TenantOverview/i18n/en.json @@ -23,5 +23,11 @@ "by-load": "by load", "by-memory": "by memory", "by-usage": "by usage", - "by-size": "by size" + "by-size": "by size", + + "charts.queries-per-second": "Queries per second", + "charts.transaction-latency": "Transactions latencies {{percentile}}", + "charts.cpu-usage": "CPU usage", + "charts.storage-usage": "Storage usage", + "charts.memory-usage": "Memory usage" } diff --git a/src/containers/Tenant/Diagnostics/TenantOverview/i18n/ru.json b/src/containers/Tenant/Diagnostics/TenantOverview/i18n/ru.json index ba54bd517..eba6612ca 100644 --- a/src/containers/Tenant/Diagnostics/TenantOverview/i18n/ru.json +++ b/src/containers/Tenant/Diagnostics/TenantOverview/i18n/ru.json @@ -23,5 +23,11 @@ "by-load": "по нагрузке", "by-memory": "по памяти", "by-usage": "по потреблению", - "by-size": "по размеру" + "by-size": "по размеру", + + "charts.queries-per-second": "Количество запросов в секунду", + "charts.transaction-latency": "Задержка транзакций {{percentile}}", + "charts.cpu-usage": "Использование CPU", + "charts.storage-usage": "Использование хранилища", + "charts.memory-usage": "Использование памяти" } diff --git a/src/containers/UserSettings/i18n/en.json b/src/containers/UserSettings/i18n/en.json index 33d4cbf8f..1db384c37 100644 --- a/src/containers/UserSettings/i18n/en.json +++ b/src/containers/UserSettings/i18n/en.json @@ -23,5 +23,8 @@ "settings.useVirtualTables.popover": "It will increase performance, but could work unstable", "settings.queryUseMultiSchema.title": "Allow queries with multiple result sets", - "settings.queryUseMultiSchema.popover": "Use 'multi' schema for queries that enables queries with multiple result sets. Returns nothing on versions 23-3 and older" + "settings.queryUseMultiSchema.popover": "Use 'multi' schema for queries that enables queries with multiple result sets. Returns nothing on versions 23-3 and older", + + "settings.displayChartsInDbDiagnostics.title": "Display charts in database diagnostics", + "settings.displayChartsInDbDiagnostics.popover": "Incorrect data may be displayed (shows data only for the database related to the current node), does not work well on static nodes" } diff --git a/src/containers/UserSettings/i18n/ru.json b/src/containers/UserSettings/i18n/ru.json index 9e41c54e0..fd9f09497 100644 --- a/src/containers/UserSettings/i18n/ru.json +++ b/src/containers/UserSettings/i18n/ru.json @@ -23,5 +23,8 @@ "settings.useVirtualTables.popover": "Это улучшит производительность, но может работать нестабильно", "settings.queryUseMultiSchema.title": "Разрешить запросы с несколькими результатами", - "settings.queryUseMultiSchema.popover": "Использовать для запросов схему 'multi', которая позволяет выполнять запросы с несколькими результатами. На версиях 23-3 и старше результат не возвращается вообще" + "settings.queryUseMultiSchema.popover": "Использовать для запросов схему 'multi', которая позволяет выполнять запросы с несколькими результатами. На версиях 23-3 и старше результат не возвращается вообще", + + "settings.displayChartsInDbDiagnostics.title": "Показывать графики в диагностике базы данных", + "settings.displayChartsInDbDiagnostics.popover": "Могут отображаться неправильные данные (показывает данные только для базы, относящейся к текущей ноде), плохо работает на статических нодах" } diff --git a/src/containers/UserSettings/settings.ts b/src/containers/UserSettings/settings.ts index aeddfbf5c..866a55868 100644 --- a/src/containers/UserSettings/settings.ts +++ b/src/containers/UserSettings/settings.ts @@ -10,6 +10,7 @@ import { USE_BACKEND_PARAMS_FOR_TABLES_KEY, USE_NODES_ENDPOINT_IN_DIAGNOSTICS_KEY, QUERY_USE_MULTI_SCHEMA_KEY, + DISPLAY_CHARTS_IN_DB_DIAGNOSTICS_KEY, } from '../../utils/constants'; import {Lang, defaultLang} from '../../utils/i18n'; @@ -94,6 +95,11 @@ export const queryUseMultiSchemaSetting: SettingProps = { title: i18n('settings.queryUseMultiSchema.title'), helpPopoverContent: i18n('settings.queryUseMultiSchema.popover'), }; +export const displayChartsInDbDiagnosticsSetting: SettingProps = { + settingKey: DISPLAY_CHARTS_IN_DB_DIAGNOSTICS_KEY, + title: i18n('settings.displayChartsInDbDiagnostics.title'), + helpPopoverContent: i18n('settings.displayChartsInDbDiagnostics.popover'), +}; export const appearanceSection: SettingsSection = { id: 'appearanceSection', @@ -103,7 +109,12 @@ export const appearanceSection: SettingsSection = { export const experimentsSection: SettingsSection = { id: 'experimentsSection', title: i18n('section.experiments'), - settings: [useNodesEndpointSetting, useVirtualTables, queryUseMultiSchemaSetting], + settings: [ + useNodesEndpointSetting, + useVirtualTables, + queryUseMultiSchemaSetting, + displayChartsInDbDiagnosticsSetting, + ], }; export const generalPage: SettingsPage = { diff --git a/src/services/api.ts b/src/services/api.ts index 22c9504a7..a0ddcdb57 100644 --- a/src/services/api.ts +++ b/src/services/api.ts @@ -28,6 +28,7 @@ import type {DescribeTopicResult} from '../types/api/topic'; import type {TEvPDiskStateResponse} from '../types/api/pdisk'; import type {TEvVDiskStateResponse} from '../types/api/vdisk'; import type {TUserToken} from '../types/api/whoami'; +import type {JsonRenderRequestParams, JsonRenderResponse} from '../types/api/render'; import type {QuerySyntax} from '../types/store/query'; import type {ComputeApiRequestParams, NodesApiRequestParams} from '../store/reducers/nodes/types'; import type {StorageApiRequestParams} from '../store/reducers/storage/types'; @@ -377,7 +378,24 @@ export class YdbEmbeddedAPI extends AxiosWrapper { path_id: tenantId?.PathId, }); } + getChartData( + {target, from, until, maxDataPoints}: JsonRenderRequestParams, + {concurrentId}: AxiosOptions = {}, + ) { + const requestString = `${target}&from=${from}&until=${until}&maxDataPoints=${maxDataPoints}&format=json`; + return this.post( + this.getPath('/viewer/json/render'), + requestString, + {}, + { + concurrentId, + headers: { + 'Content-Type': 'application/x-www-form-urlencoded', + }, + }, + ); + } /** @deprecated use localStorage instead */ postSetting(settingsApi: string, name: string, value: string) { return this.request({ diff --git a/src/services/settings.ts b/src/services/settings.ts index ce3e02e50..54dea57c2 100644 --- a/src/services/settings.ts +++ b/src/services/settings.ts @@ -3,6 +3,7 @@ import {TENANT_PAGES_IDS} from '../store/reducers/tenant/constants'; import { ASIDE_HEADER_COMPACT_KEY, CLUSTER_INFO_HIDDEN_KEY, + DISPLAY_CHARTS_IN_DB_DIAGNOSTICS_KEY, INVERTED_DISKS_KEY, LANGUAGE_KEY, LAST_USED_QUERY_ACTION_KEY, @@ -37,6 +38,7 @@ export const DEFAULT_USER_SETTINGS: SettingsObject = { [PARTITIONS_HIDDEN_COLUMNS_KEY]: [], [CLUSTER_INFO_HIDDEN_KEY]: true, [USE_BACKEND_PARAMS_FOR_TABLES_KEY]: false, + [DISPLAY_CHARTS_IN_DB_DIAGNOSTICS_KEY]: false, }; class SettingsManager { diff --git a/src/store/reducers/tenant/tenant.ts b/src/store/reducers/tenant/tenant.ts index 91b076865..955139a53 100644 --- a/src/store/reducers/tenant/tenant.ts +++ b/src/store/reducers/tenant/tenant.ts @@ -24,7 +24,12 @@ const SET_METRICS_TAB = 'tenant/SET_METRICS_TAB'; const CLEAR_TENANT = 'tenant/CLEAR_TENANT'; const SET_DATA_WAS_NOT_LOADED = 'tenant/SET_DATA_WAS_NOT_LOADED'; -const initialState = {loading: false, wasLoaded: false}; +// Tenant diagnostics tab content was requested twice, +// because requests were sent before state was set as loading and after tenant data is fully loaded +// So tenant data is considered loading from the start, there is no attempt to load tab content +// TODO: try fix with 'display: none' for tenant diagnostics tab content while tenant data loading, +// but with parallel (not sequent) data requests +const initialState = {loading: true, wasLoaded: false}; const tenantReducer: Reducer = (state = initialState, action) => { switch (action.type) { diff --git a/src/types/api/render.ts b/src/types/api/render.ts new file mode 100644 index 000000000..09ccf959c --- /dev/null +++ b/src/types/api/render.ts @@ -0,0 +1,34 @@ +/** + * endpoint: /viewer/json/render + * + * source: https://github.com/ydb-platform/ydb/blob/main/ydb/core/viewer/json_render.h + */ + +export type JsonRenderResponse = + | { + error?: string; + status?: string; + } + | MetricData[]; + +export interface MetricData { + datapoints: MetricDatapoint[]; + target: string; + title: string; + tags: MetricTags; +} + +interface MetricTags { + name: string; +} + +/** [metric value - null or double, timestamp - seconds] */ +export type MetricDatapoint = [null | number, number]; + +export interface JsonRenderRequestParams { + /** metrics names in format "target=queries.latencies.p50&target=queries.latencies.p75&target=queries.latencies.p90" */ + target: string; + from: number; + until: number; + maxDataPoints: number; +} diff --git a/src/utils/cn.ts b/src/utils/cn.ts new file mode 100644 index 000000000..4510275f4 --- /dev/null +++ b/src/utils/cn.ts @@ -0,0 +1,3 @@ +import cn from 'bem-cn-lite'; + +export {cn}; diff --git a/src/utils/constants.ts b/src/utils/constants.ts index cc1a97e29..91f373ac2 100644 --- a/src/utils/constants.ts +++ b/src/utils/constants.ts @@ -125,3 +125,5 @@ export const USE_BACKEND_PARAMS_FOR_TABLES_KEY = 'useBackendParamsForTables'; // Enable schema that supports multiple resultsets export const QUERY_USE_MULTI_SCHEMA_KEY = 'queryUseMultiSchema'; + +export const DISPLAY_CHARTS_IN_DB_DIAGNOSTICS_KEY = 'displayChartsInDbDiagnostics'; diff --git a/src/utils/timeParsers/formatDuration.ts b/src/utils/timeParsers/formatDuration.ts index 0e0c271d1..651ce2994 100644 --- a/src/utils/timeParsers/formatDuration.ts +++ b/src/utils/timeParsers/formatDuration.ts @@ -1,4 +1,5 @@ import {DAY_IN_SECONDS, HOUR_IN_SECONDS} from '../constants'; +import {formatNumber} from '../dataFormatters/dataFormatters'; import i18n from './i18n'; @@ -6,6 +7,8 @@ import i18n from './i18n'; * Process time difference in ms and returns formated time. * By default only two major values are returned (days & hours, hours & minutes, minutes & seconds, etc.). * It can be altered with valuesCount arg + * + * value - duration in ms */ export const formatDurationToShortTimeFormat = (value: number, valuesCount: 1 | 2 = 2) => { const ms = value % 1000; @@ -62,3 +65,10 @@ export const formatDurationToShortTimeFormat = (value: number, valuesCount: 1 | return i18n('ms', duration); }; + +/** + * Parse ms duration to string + */ +export const formatToMs = (value: number) => { + return i18n('ms', {ms: formatNumber(value)}); +}; diff --git a/src/utils/timeframes.ts b/src/utils/timeframes.ts new file mode 100644 index 000000000..b28805856 --- /dev/null +++ b/src/utils/timeframes.ts @@ -0,0 +1,10 @@ +import {DAY_IN_SECONDS, HOUR_IN_SECONDS, MINUTE_IN_SECONDS} from './constants'; + +export const TIMEFRAMES = { + '30m': 30 * MINUTE_IN_SECONDS, + '1h': HOUR_IN_SECONDS, + '1d': DAY_IN_SECONDS, + '1w': 7 * DAY_IN_SECONDS, +} as const; + +export type TimeFrame = keyof typeof TIMEFRAMES; diff --git a/src/utils/versions/getVersionsColors.ts b/src/utils/versions/getVersionsColors.ts index 2363a9a8a..b132d7223 100644 --- a/src/utils/versions/getVersionsColors.ts +++ b/src/utils/versions/getVersionsColors.ts @@ -9,6 +9,7 @@ export const hashCode = (s: string) => { }, 0); }; +// TODO: colors used in charts as well, need to move to constants // 11 distinct colors from https://mokole.com/palette.html export const COLORS = [ '#008000', // green