]> git.apps.os.sepia.ceph.com Git - ceph.git/commitdiff
mgr/dashboard: replace usage or progress bar with carbon meter chart 60416/head
authorNaman Munet <namanmunet@li-ff83bccc-26af-11b2-a85c-a4b04bfb1003.ibm.com>
Mon, 21 Oct 2024 16:55:41 +0000 (22:25 +0530)
committerNaman Munet <naman.munet@ibm.com>
Thu, 26 Jun 2025 12:21:13 +0000 (17:51 +0530)
Fixes: https://tracker.ceph.com/issues/68258
Changes affect the following files:
- rbd-list.component.html
- cephfs-detail.component.html
- cephfs-subvolume-group.component.html
- cephfs-subvolume-list.componenet.html
- multi-cluster.component.html
- osd-list.component.html
- service-daemon-list.component.html
- pool-list.component.html
- rgw-bucket-list.component.html
- rgw-user-list.component.html

Signed-off-by: Naman Munet <naman.munet@ibm.com>
13 files changed:
src/pybind/mgr/dashboard/frontend/package-lock.json
src/pybind/mgr/dashboard/frontend/package.json
src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-subvolume-group/cephfs-subvolume-group.component.html
src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-subvolume-list/cephfs-subvolume-list.component.html
src/pybind/mgr/dashboard/frontend/src/app/shared/classes/css-helper.ts
src/pybind/mgr/dashboard/frontend/src/app/shared/components/components.module.ts
src/pybind/mgr/dashboard/frontend/src/app/shared/components/usage-bar/usage-bar.component.html
src/pybind/mgr/dashboard/frontend/src/app/shared/components/usage-bar/usage-bar.component.scss
src/pybind/mgr/dashboard/frontend/src/app/shared/components/usage-bar/usage-bar.component.spec.ts
src/pybind/mgr/dashboard/frontend/src/app/shared/components/usage-bar/usage-bar.component.ts
src/pybind/mgr/dashboard/frontend/src/app/shared/enum/usage-bar-chart.enum.ts [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/jestGlobalMocks.ts
src/pybind/mgr/dashboard/frontend/src/styles.scss

index 18ae010b14fa6a9a075f4faf7e6893d4f3f001ba..cea9b6ecbd5a896b560a8557706d7bfc388f7331 100644 (file)
@@ -18,6 +18,7 @@
         "@angular/platform-browser": "18.2.11",
         "@angular/platform-browser-dynamic": "18.2.11",
         "@angular/router": "18.2.11",
+        "@carbon/charts-angular": "1.23.9",
         "@carbon/icons": "11.41.0",
         "@carbon/styles": "1.83.0",
         "@ibm/plex": "6.4.0",
       "integrity": "sha512-Tbsj02wXCbqGmzdnXNk0SOF19ChhRU70BsroIi4Pm6Ehp56in6vch94mfbdQ17DozxkL3BAVjbZ4Qc1a0HFRAg==",
       "license": "MIT"
     },
+    "node_modules/@carbon/charts": {
+      "version": "1.23.9",
+      "resolved": "https://registry.npmjs.org/@carbon/charts/-/charts-1.23.9.tgz",
+      "integrity": "sha512-hYd+jN+EuM1OcA/apoHv3fPN0TGnvjKBRBJkDwDBjkrp1YwoMYf3arqL9VDrq+Gl5hy3MPu4j57elVAOCCMG0Q==",
+      "hasInstallScript": true,
+      "license": "Apache-2.0",
+      "dependencies": {
+        "@carbon/colors": "^11.31.0",
+        "@carbon/utils-position": "^1.3.0",
+        "@ibm/telemetry-js": "^1.9.1",
+        "@types/d3": "^7.4.3",
+        "@types/topojson": "^3.2.6",
+        "d3": "^7.9.0",
+        "d3-cloud": "^1.2.7",
+        "d3-sankey": "^0.12.3",
+        "date-fns": "^4.1.0",
+        "dompurify": "^3.2.6",
+        "html-to-image": "1.11.11",
+        "lodash-es": "^4.17.21",
+        "topojson-client": "^3.1.0",
+        "tslib": "^2.8.1"
+      }
+    },
+    "node_modules/@carbon/charts-angular": {
+      "version": "1.23.9",
+      "resolved": "https://registry.npmjs.org/@carbon/charts-angular/-/charts-angular-1.23.9.tgz",
+      "integrity": "sha512-vN+9BIvlv+LqeDT0LrlwbmQR9Qj1C1E9OQaMLHattGrCLInpwQG0YBtzJQ/Jrhp9IrAjeEMvDm6jPWek6EI3Hg==",
+      "license": "Apache-2.0",
+      "dependencies": {
+        "@carbon/charts": "1.23.9",
+        "@ibm/telemetry-js": "^1.9.1",
+        "tslib": "^2.8.1"
+      },
+      "peerDependencies": {
+        "@angular/common": "^19.0.6",
+        "@angular/core": "^19.0.6"
+      }
+    },
+    "node_modules/@carbon/charts-angular/node_modules/tslib": {
+      "version": "2.8.1",
+      "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
+      "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
+      "license": "0BSD"
+    },
+    "node_modules/@carbon/charts/node_modules/@carbon/utils-position": {
+      "version": "1.3.0",
+      "resolved": "https://registry.npmjs.org/@carbon/utils-position/-/utils-position-1.3.0.tgz",
+      "integrity": "sha512-bfar2dV+fQ15syIrH3n9ujY4PXd1Q+AF2VcTLJIC04IDe2f80zOnJlLNPc/RktHcWTZ7WSQm80cQo3abGcsfTA==",
+      "hasInstallScript": true,
+      "license": "MIT",
+      "dependencies": {
+        "@ibm/telemetry-js": "^1.5.1"
+      }
+    },
+    "node_modules/@carbon/charts/node_modules/dompurify": {
+      "version": "3.2.6",
+      "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.2.6.tgz",
+      "integrity": "sha512-/2GogDQlohXPZe6D6NOgQvXLPSYBqIWMnZ8zzOhn09REE4eyAzb+Hed3jhoM9OkuaJ8P6ZGTTVWQKAi8ieIzfQ==",
+      "license": "(MPL-2.0 OR Apache-2.0)",
+      "optionalDependencies": {
+        "@types/trusted-types": "^2.0.7"
+      }
+    },
+    "node_modules/@carbon/charts/node_modules/tslib": {
+      "version": "2.8.1",
+      "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
+      "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
+      "license": "0BSD"
+    },
     "node_modules/@carbon/colors": {
       "version": "11.34.0",
       "resolved": "https://registry.npmjs.org/@carbon/colors/-/colors-11.34.0.tgz",
       "integrity": "sha512-qS4TSISzcT5XeOstoSQznekMA9DM4HSXPN0rxg+Skh1gEB1YcjUgO59W9xVVj4yi6cKetfjYteqVZPNvdSxrZw==",
       "hasInstallScript": true,
+      "license": "Apache-2.0",
       "dependencies": {
         "@ibm/telemetry-js": "^1.5.0"
       }
       "integrity": "sha512-sK2/uU5CtmJ51zo0JF2Lc4iSw9Fy3xn9ewfewuooV5Qmeb5O+brAHuoXKMV7UWwRbBmd+txhAXAJoi4S5QLDRQ==",
       "dev": true
     },
+    "node_modules/@types/d3": {
+      "version": "7.4.3",
+      "resolved": "https://registry.npmjs.org/@types/d3/-/d3-7.4.3.tgz",
+      "integrity": "sha512-lZXZ9ckh5R8uiFVt8ogUNf+pIrK4EsWrx2Np75WvF/eTpJ0FMHNhjXk8CKEx/+gpHbNQyJWehbFaTvqmHWB3ww==",
+      "license": "MIT",
+      "dependencies": {
+        "@types/d3-array": "*",
+        "@types/d3-axis": "*",
+        "@types/d3-brush": "*",
+        "@types/d3-chord": "*",
+        "@types/d3-color": "*",
+        "@types/d3-contour": "*",
+        "@types/d3-delaunay": "*",
+        "@types/d3-dispatch": "*",
+        "@types/d3-drag": "*",
+        "@types/d3-dsv": "*",
+        "@types/d3-ease": "*",
+        "@types/d3-fetch": "*",
+        "@types/d3-force": "*",
+        "@types/d3-format": "*",
+        "@types/d3-geo": "*",
+        "@types/d3-hierarchy": "*",
+        "@types/d3-interpolate": "*",
+        "@types/d3-path": "*",
+        "@types/d3-polygon": "*",
+        "@types/d3-quadtree": "*",
+        "@types/d3-random": "*",
+        "@types/d3-scale": "*",
+        "@types/d3-scale-chromatic": "*",
+        "@types/d3-selection": "*",
+        "@types/d3-shape": "*",
+        "@types/d3-time": "*",
+        "@types/d3-time-format": "*",
+        "@types/d3-timer": "*",
+        "@types/d3-transition": "*",
+        "@types/d3-zoom": "*"
+      }
+    },
+    "node_modules/@types/d3-array": {
+      "version": "3.2.1",
+      "resolved": "https://registry.npmjs.org/@types/d3-array/-/d3-array-3.2.1.tgz",
+      "integrity": "sha512-Y2Jn2idRrLzUfAKV2LyRImR+y4oa2AntrgID95SHJxuMUrkNXmanDSed71sRNZysveJVt1hLLemQZIady0FpEg==",
+      "license": "MIT"
+    },
+    "node_modules/@types/d3-axis": {
+      "version": "3.0.6",
+      "resolved": "https://registry.npmjs.org/@types/d3-axis/-/d3-axis-3.0.6.tgz",
+      "integrity": "sha512-pYeijfZuBd87T0hGn0FO1vQ/cgLk6E1ALJjfkC0oJ8cbwkZl3TpgS8bVBLZN+2jjGgg38epgxb2zmoGtSfvgMw==",
+      "license": "MIT",
+      "dependencies": {
+        "@types/d3-selection": "*"
+      }
+    },
+    "node_modules/@types/d3-brush": {
+      "version": "3.0.6",
+      "resolved": "https://registry.npmjs.org/@types/d3-brush/-/d3-brush-3.0.6.tgz",
+      "integrity": "sha512-nH60IZNNxEcrh6L1ZSMNA28rj27ut/2ZmI3r96Zd+1jrZD++zD3LsMIjWlvg4AYrHn/Pqz4CF3veCxGjtbqt7A==",
+      "license": "MIT",
+      "dependencies": {
+        "@types/d3-selection": "*"
+      }
+    },
+    "node_modules/@types/d3-chord": {
+      "version": "3.0.6",
+      "resolved": "https://registry.npmjs.org/@types/d3-chord/-/d3-chord-3.0.6.tgz",
+      "integrity": "sha512-LFYWWd8nwfwEmTZG9PfQxd17HbNPksHBiJHaKuY1XeqscXacsS2tyoo6OdRsjf+NQYeB6XrNL3a25E3gH69lcg==",
+      "license": "MIT"
+    },
+    "node_modules/@types/d3-color": {
+      "version": "3.1.3",
+      "resolved": "https://registry.npmjs.org/@types/d3-color/-/d3-color-3.1.3.tgz",
+      "integrity": "sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A==",
+      "license": "MIT"
+    },
+    "node_modules/@types/d3-contour": {
+      "version": "3.0.6",
+      "resolved": "https://registry.npmjs.org/@types/d3-contour/-/d3-contour-3.0.6.tgz",
+      "integrity": "sha512-BjzLgXGnCWjUSYGfH1cpdo41/hgdWETu4YxpezoztawmqsvCeep+8QGfiY6YbDvfgHz/DkjeIkkZVJavB4a3rg==",
+      "license": "MIT",
+      "dependencies": {
+        "@types/d3-array": "*",
+        "@types/geojson": "*"
+      }
+    },
+    "node_modules/@types/d3-delaunay": {
+      "version": "6.0.4",
+      "resolved": "https://registry.npmjs.org/@types/d3-delaunay/-/d3-delaunay-6.0.4.tgz",
+      "integrity": "sha512-ZMaSKu4THYCU6sV64Lhg6qjf1orxBthaC161plr5KuPHo3CNm8DTHiLw/5Eq2b6TsNP0W0iJrUOFscY6Q450Hw==",
+      "license": "MIT"
+    },
+    "node_modules/@types/d3-dispatch": {
+      "version": "3.0.6",
+      "resolved": "https://registry.npmjs.org/@types/d3-dispatch/-/d3-dispatch-3.0.6.tgz",
+      "integrity": "sha512-4fvZhzMeeuBJYZXRXrRIQnvUYfyXwYmLsdiN7XXmVNQKKw1cM8a5WdID0g1hVFZDqT9ZqZEY5pD44p24VS7iZQ==",
+      "license": "MIT"
+    },
+    "node_modules/@types/d3-drag": {
+      "version": "3.0.7",
+      "resolved": "https://registry.npmjs.org/@types/d3-drag/-/d3-drag-3.0.7.tgz",
+      "integrity": "sha512-HE3jVKlzU9AaMazNufooRJ5ZpWmLIoc90A37WU2JMmeq28w1FQqCZswHZ3xR+SuxYftzHq6WU6KJHvqxKzTxxQ==",
+      "license": "MIT",
+      "dependencies": {
+        "@types/d3-selection": "*"
+      }
+    },
+    "node_modules/@types/d3-dsv": {
+      "version": "3.0.7",
+      "resolved": "https://registry.npmjs.org/@types/d3-dsv/-/d3-dsv-3.0.7.tgz",
+      "integrity": "sha512-n6QBF9/+XASqcKK6waudgL0pf/S5XHPPI8APyMLLUHd8NqouBGLsU8MgtO7NINGtPBtk9Kko/W4ea0oAspwh9g==",
+      "license": "MIT"
+    },
+    "node_modules/@types/d3-ease": {
+      "version": "3.0.2",
+      "resolved": "https://registry.npmjs.org/@types/d3-ease/-/d3-ease-3.0.2.tgz",
+      "integrity": "sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA==",
+      "license": "MIT"
+    },
+    "node_modules/@types/d3-fetch": {
+      "version": "3.0.7",
+      "resolved": "https://registry.npmjs.org/@types/d3-fetch/-/d3-fetch-3.0.7.tgz",
+      "integrity": "sha512-fTAfNmxSb9SOWNB9IoG5c8Hg6R+AzUHDRlsXsDZsNp6sxAEOP0tkP3gKkNSO/qmHPoBFTxNrjDprVHDQDvo5aA==",
+      "license": "MIT",
+      "dependencies": {
+        "@types/d3-dsv": "*"
+      }
+    },
+    "node_modules/@types/d3-force": {
+      "version": "3.0.10",
+      "resolved": "https://registry.npmjs.org/@types/d3-force/-/d3-force-3.0.10.tgz",
+      "integrity": "sha512-ZYeSaCF3p73RdOKcjj+swRlZfnYpK1EbaDiYICEEp5Q6sUiqFaFQ9qgoshp5CzIyyb/yD09kD9o2zEltCexlgw==",
+      "license": "MIT"
+    },
+    "node_modules/@types/d3-format": {
+      "version": "3.0.4",
+      "resolved": "https://registry.npmjs.org/@types/d3-format/-/d3-format-3.0.4.tgz",
+      "integrity": "sha512-fALi2aI6shfg7vM5KiR1wNJnZ7r6UuggVqtDA+xiEdPZQwy/trcQaHnwShLuLdta2rTymCNpxYTiMZX/e09F4g==",
+      "license": "MIT"
+    },
+    "node_modules/@types/d3-geo": {
+      "version": "3.1.0",
+      "resolved": "https://registry.npmjs.org/@types/d3-geo/-/d3-geo-3.1.0.tgz",
+      "integrity": "sha512-856sckF0oP/diXtS4jNsiQw/UuK5fQG8l/a9VVLeSouf1/PPbBE1i1W852zVwKwYCBkFJJB7nCFTbk6UMEXBOQ==",
+      "license": "MIT",
+      "dependencies": {
+        "@types/geojson": "*"
+      }
+    },
+    "node_modules/@types/d3-hierarchy": {
+      "version": "3.1.7",
+      "resolved": "https://registry.npmjs.org/@types/d3-hierarchy/-/d3-hierarchy-3.1.7.tgz",
+      "integrity": "sha512-tJFtNoYBtRtkNysX1Xq4sxtjK8YgoWUNpIiUee0/jHGRwqvzYxkq0hGVbbOGSz+JgFxxRu4K8nb3YpG3CMARtg==",
+      "license": "MIT"
+    },
+    "node_modules/@types/d3-interpolate": {
+      "version": "3.0.4",
+      "resolved": "https://registry.npmjs.org/@types/d3-interpolate/-/d3-interpolate-3.0.4.tgz",
+      "integrity": "sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA==",
+      "license": "MIT",
+      "dependencies": {
+        "@types/d3-color": "*"
+      }
+    },
+    "node_modules/@types/d3-path": {
+      "version": "3.1.1",
+      "resolved": "https://registry.npmjs.org/@types/d3-path/-/d3-path-3.1.1.tgz",
+      "integrity": "sha512-VMZBYyQvbGmWyWVea0EHs/BwLgxc+MKi1zLDCONksozI4YJMcTt8ZEuIR4Sb1MMTE8MMW49v0IwI5+b7RmfWlg==",
+      "license": "MIT"
+    },
+    "node_modules/@types/d3-polygon": {
+      "version": "3.0.2",
+      "resolved": "https://registry.npmjs.org/@types/d3-polygon/-/d3-polygon-3.0.2.tgz",
+      "integrity": "sha512-ZuWOtMaHCkN9xoeEMr1ubW2nGWsp4nIql+OPQRstu4ypeZ+zk3YKqQT0CXVe/PYqrKpZAi+J9mTs05TKwjXSRA==",
+      "license": "MIT"
+    },
+    "node_modules/@types/d3-quadtree": {
+      "version": "3.0.6",
+      "resolved": "https://registry.npmjs.org/@types/d3-quadtree/-/d3-quadtree-3.0.6.tgz",
+      "integrity": "sha512-oUzyO1/Zm6rsxKRHA1vH0NEDG58HrT5icx/azi9MF1TWdtttWl0UIUsjEQBBh+SIkrpd21ZjEv7ptxWys1ncsg==",
+      "license": "MIT"
+    },
+    "node_modules/@types/d3-random": {
+      "version": "3.0.3",
+      "resolved": "https://registry.npmjs.org/@types/d3-random/-/d3-random-3.0.3.tgz",
+      "integrity": "sha512-Imagg1vJ3y76Y2ea0871wpabqp613+8/r0mCLEBfdtqC7xMSfj9idOnmBYyMoULfHePJyxMAw3nWhJxzc+LFwQ==",
+      "license": "MIT"
+    },
+    "node_modules/@types/d3-scale": {
+      "version": "4.0.9",
+      "resolved": "https://registry.npmjs.org/@types/d3-scale/-/d3-scale-4.0.9.tgz",
+      "integrity": "sha512-dLmtwB8zkAeO/juAMfnV+sItKjlsw2lKdZVVy6LRr0cBmegxSABiLEpGVmSJJ8O08i4+sGR6qQtb6WtuwJdvVw==",
+      "license": "MIT",
+      "dependencies": {
+        "@types/d3-time": "*"
+      }
+    },
+    "node_modules/@types/d3-scale-chromatic": {
+      "version": "3.1.0",
+      "resolved": "https://registry.npmjs.org/@types/d3-scale-chromatic/-/d3-scale-chromatic-3.1.0.tgz",
+      "integrity": "sha512-iWMJgwkK7yTRmWqRB5plb1kadXyQ5Sj8V/zYlFGMUBbIPKQScw+Dku9cAAMgJG+z5GYDoMjWGLVOvjghDEFnKQ==",
+      "license": "MIT"
+    },
+    "node_modules/@types/d3-selection": {
+      "version": "3.0.11",
+      "resolved": "https://registry.npmjs.org/@types/d3-selection/-/d3-selection-3.0.11.tgz",
+      "integrity": "sha512-bhAXu23DJWsrI45xafYpkQ4NtcKMwWnAC/vKrd2l+nxMFuvOT3XMYTIj2opv8vq8AO5Yh7Qac/nSeP/3zjTK0w==",
+      "license": "MIT"
+    },
+    "node_modules/@types/d3-shape": {
+      "version": "3.1.7",
+      "resolved": "https://registry.npmjs.org/@types/d3-shape/-/d3-shape-3.1.7.tgz",
+      "integrity": "sha512-VLvUQ33C+3J+8p+Daf+nYSOsjB4GXp19/S/aGo60m9h1v6XaxjiT82lKVWJCfzhtuZ3yD7i/TPeC/fuKLLOSmg==",
+      "license": "MIT",
+      "dependencies": {
+        "@types/d3-path": "*"
+      }
+    },
+    "node_modules/@types/d3-time": {
+      "version": "3.0.4",
+      "resolved": "https://registry.npmjs.org/@types/d3-time/-/d3-time-3.0.4.tgz",
+      "integrity": "sha512-yuzZug1nkAAaBlBBikKZTgzCeA+k1uy4ZFwWANOfKw5z5LRhV0gNA7gNkKm7HoK+HRN0wX3EkxGk0fpbWhmB7g==",
+      "license": "MIT"
+    },
+    "node_modules/@types/d3-time-format": {
+      "version": "4.0.3",
+      "resolved": "https://registry.npmjs.org/@types/d3-time-format/-/d3-time-format-4.0.3.tgz",
+      "integrity": "sha512-5xg9rC+wWL8kdDj153qZcsJ0FWiFt0J5RB6LYUNZjwSnesfblqrI/bJ1wBdJ8OQfncgbJG5+2F+qfqnqyzYxyg==",
+      "license": "MIT"
+    },
+    "node_modules/@types/d3-timer": {
+      "version": "3.0.2",
+      "resolved": "https://registry.npmjs.org/@types/d3-timer/-/d3-timer-3.0.2.tgz",
+      "integrity": "sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw==",
+      "license": "MIT"
+    },
+    "node_modules/@types/d3-transition": {
+      "version": "3.0.9",
+      "resolved": "https://registry.npmjs.org/@types/d3-transition/-/d3-transition-3.0.9.tgz",
+      "integrity": "sha512-uZS5shfxzO3rGlu0cC3bjmMFKsXv+SmZZcgp0KD22ts4uGXp5EVYGzu/0YdwZeKmddhcAccYtREJKkPfXkZuCg==",
+      "license": "MIT",
+      "dependencies": {
+        "@types/d3-selection": "*"
+      }
+    },
+    "node_modules/@types/d3-zoom": {
+      "version": "3.0.8",
+      "resolved": "https://registry.npmjs.org/@types/d3-zoom/-/d3-zoom-3.0.8.tgz",
+      "integrity": "sha512-iqMC4/YlFCSlO8+2Ii1GGGliCAY4XdeG748w5vQUbevlbDu0zSjH/+jojorQVBK/se0j6DUFNPBGSqD3YWYnDw==",
+      "license": "MIT",
+      "dependencies": {
+        "@types/d3-interpolate": "*",
+        "@types/d3-selection": "*"
+      }
+    },
     "node_modules/@types/estree": {
       "version": "1.0.8",
       "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz",
       "resolved": "https://registry.npmjs.org/@types/file-saver/-/file-saver-2.0.7.tgz",
       "integrity": "sha512-dNKVfHd/jk0SkR/exKGj2ggkB45MAkzvWCaqLUUgkyjITkGNzH8H+yUwr+BLJUBjZOe9w8X3wgmXhZDRg1ED6A=="
     },
+    "node_modules/@types/geojson": {
+      "version": "7946.0.16",
+      "resolved": "https://registry.npmjs.org/@types/geojson/-/geojson-7946.0.16.tgz",
+      "integrity": "sha512-6C8nqWur3j98U6+lXDfTUWIfgvZU+EumvpHKcYjujKH7woYyLj2sUmff0tRhrqM7BohUw7Pz3ZB1jj2gW9Fvmg==",
+      "license": "MIT"
+    },
     "node_modules/@types/glob": {
       "version": "8.1.0",
       "resolved": "https://registry.npmjs.org/@types/glob/-/glob-8.1.0.tgz",
       "integrity": "sha512-SlufixEmh+8CLHNgTfAfCT1icNOF7bXboWabhHr1+hIolqlvfwYJGe7HgRcpI3ChE7HWASmEKLkMu34rxseJjQ==",
       "dev": true
     },
+    "node_modules/@types/topojson": {
+      "version": "3.2.6",
+      "resolved": "https://registry.npmjs.org/@types/topojson/-/topojson-3.2.6.tgz",
+      "integrity": "sha512-ppfdlxjxofWJ66XdLgIlER/85RvpGyfOf8jrWf+3kVIjEatFxEZYD/Ea83jO672Xu1HRzd/ghwlbcZIUNHTskw==",
+      "license": "MIT",
+      "dependencies": {
+        "@types/geojson": "*",
+        "@types/topojson-client": "*",
+        "@types/topojson-server": "*",
+        "@types/topojson-simplify": "*",
+        "@types/topojson-specification": "*"
+      }
+    },
+    "node_modules/@types/topojson-client": {
+      "version": "3.1.5",
+      "resolved": "https://registry.npmjs.org/@types/topojson-client/-/topojson-client-3.1.5.tgz",
+      "integrity": "sha512-C79rySTyPxnQNNguTZNI1Ct4D7IXgvyAs3p9HPecnl6mNrJ5+UhvGNYcZfpROYV2lMHI48kJPxwR+F9C6c7nmw==",
+      "license": "MIT",
+      "dependencies": {
+        "@types/geojson": "*",
+        "@types/topojson-specification": "*"
+      }
+    },
+    "node_modules/@types/topojson-server": {
+      "version": "3.0.4",
+      "resolved": "https://registry.npmjs.org/@types/topojson-server/-/topojson-server-3.0.4.tgz",
+      "integrity": "sha512-5+ieK8ePfP+K2VH6Vgs1VCt+fO1U8XZHj0UsF+NktaF0DavAo1q3IvCBXgokk/xmtvoPltSUs6vxuR/zMdOE1g==",
+      "license": "MIT",
+      "dependencies": {
+        "@types/geojson": "*",
+        "@types/topojson-specification": "*"
+      }
+    },
+    "node_modules/@types/topojson-simplify": {
+      "version": "3.0.3",
+      "resolved": "https://registry.npmjs.org/@types/topojson-simplify/-/topojson-simplify-3.0.3.tgz",
+      "integrity": "sha512-sBO5UZ0O2dB0bNwo0vut2yLHhj3neUGi9uL7/ROdm8Gs6dtt4jcB9OGDKr+M2isZwQM2RuzVmifnMZpxj4IGNw==",
+      "license": "MIT",
+      "dependencies": {
+        "@types/geojson": "*",
+        "@types/topojson-specification": "*"
+      }
+    },
+    "node_modules/@types/topojson-specification": {
+      "version": "1.0.5",
+      "resolved": "https://registry.npmjs.org/@types/topojson-specification/-/topojson-specification-1.0.5.tgz",
+      "integrity": "sha512-C7KvcQh+C2nr6Y2Ub4YfgvWvWCgP2nOQMtfhlnwsRL4pYmmwzBS7HclGiS87eQfDOU/DLQpX6GEscviaz4yLIQ==",
+      "license": "MIT",
+      "dependencies": {
+        "@types/geojson": "*"
+      }
+    },
     "node_modules/@types/tough-cookie": {
       "version": "4.0.5",
       "resolved": "https://registry.npmjs.org/@types/tough-cookie/-/tough-cookie-4.0.5.tgz",
       "integrity": "sha512-/Ad8+nIOV7Rl++6f1BdKxFSMgmoqEoYbHRpPcx3JEfv8VRsQe9Z4mCXeJBzxs7mbHY/XOZZuXlRNfhpVPbs6ZA==",
       "dev": true
     },
+    "node_modules/@types/trusted-types": {
+      "version": "2.0.7",
+      "resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz",
+      "integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==",
+      "license": "MIT",
+      "optional": true
+    },
     "node_modules/@types/unist": {
       "version": "2.0.11",
       "resolved": "https://registry.npmjs.org/@types/unist/-/unist-2.0.11.tgz",
         "node": ">=0.12"
       }
     },
+    "node_modules/d3": {
+      "version": "7.9.0",
+      "resolved": "https://registry.npmjs.org/d3/-/d3-7.9.0.tgz",
+      "integrity": "sha512-e1U46jVP+w7Iut8Jt8ri1YsPOvFpg46k+K8TpCb0P+zjCkjkPnV7WzfDJzMHy1LnA+wj5pLT1wjO901gLXeEhA==",
+      "license": "ISC",
+      "dependencies": {
+        "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"
+      },
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/d3-array": {
+      "version": "3.2.4",
+      "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-3.2.4.tgz",
+      "integrity": "sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==",
+      "license": "ISC",
+      "dependencies": {
+        "internmap": "1 - 2"
+      },
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/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==",
+      "license": "ISC",
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/d3-brush": {
+      "version": "3.0.0",
+      "resolved": "https://registry.npmjs.org/d3-brush/-/d3-brush-3.0.0.tgz",
+      "integrity": "sha512-ALnjWlVYkXsVIGlOsuWH1+3udkYFI48Ljihfnh8FZPF2QS9o+PzGLBslO0PjzVoHLZ2KCVgAM8NVkXPJB2aNnQ==",
+      "license": "ISC",
+      "dependencies": {
+        "d3-dispatch": "1 - 3",
+        "d3-drag": "2 - 3",
+        "d3-interpolate": "1 - 3",
+        "d3-selection": "3",
+        "d3-transition": "3"
+      },
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/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==",
+      "license": "ISC",
+      "dependencies": {
+        "d3-path": "1 - 3"
+      },
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/d3-cloud": {
+      "version": "1.2.7",
+      "resolved": "https://registry.npmjs.org/d3-cloud/-/d3-cloud-1.2.7.tgz",
+      "integrity": "sha512-8TrgcgwRIpoZYQp7s3fGB7tATWfhckRb8KcVd1bOgqkNdkJRDGWfdSf4HkHHzZxSczwQJdSxvfPudwir5IAJ3w==",
+      "license": "BSD-3-Clause",
+      "dependencies": {
+        "d3-dispatch": "^1.0.3"
+      }
+    },
+    "node_modules/d3-cloud/node_modules/d3-dispatch": {
+      "version": "1.0.6",
+      "resolved": "https://registry.npmjs.org/d3-dispatch/-/d3-dispatch-1.0.6.tgz",
+      "integrity": "sha512-fVjoElzjhCEy+Hbn8KygnmMS7Or0a9sI2UzGwoB7cCtvI1XpVN9GpoYlnb3xt2YV66oXYb1fLJ8GMvP4hdU1RA==",
+      "license": "BSD-3-Clause"
+    },
+    "node_modules/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==",
+      "license": "ISC",
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/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==",
+      "license": "ISC",
+      "dependencies": {
+        "d3-array": "^3.2.0"
+      },
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/d3-delaunay": {
+      "version": "6.0.4",
+      "resolved": "https://registry.npmjs.org/d3-delaunay/-/d3-delaunay-6.0.4.tgz",
+      "integrity": "sha512-mdjtIZ1XLAM8bm/hx3WwjfHt6Sggek7qH043O8KEjDXN40xi3vx/6pYSVTwLjEgiXQTbvaouWKynLBiUZ6SK6A==",
+      "license": "ISC",
+      "dependencies": {
+        "delaunator": "5"
+      },
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/d3-dispatch": {
+      "version": "3.0.1",
+      "resolved": "https://registry.npmjs.org/d3-dispatch/-/d3-dispatch-3.0.1.tgz",
+      "integrity": "sha512-rzUyPU/S7rwUflMyLc1ETDeBj0NRuHKKAcvukozwhshr6g6c5d8zh4c2gQjY2bZ0dXeGLWc1PF174P2tVvKhfg==",
+      "license": "ISC",
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/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==",
+      "license": "ISC",
+      "dependencies": {
+        "d3-dispatch": "1 - 3",
+        "d3-selection": "3"
+      },
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/d3-dsv": {
+      "version": "3.0.1",
+      "resolved": "https://registry.npmjs.org/d3-dsv/-/d3-dsv-3.0.1.tgz",
+      "integrity": "sha512-UG6OvdI5afDIFP9w4G0mNq50dSOsXHJaRE8arAS5o9ApWnIElp8GZw1Dun8vP8OyHOZ/QJUKUJwxiiCCnUwm+Q==",
+      "license": "ISC",
+      "dependencies": {
+        "commander": "7",
+        "iconv-lite": "0.6",
+        "rw": "1"
+      },
+      "bin": {
+        "csv2json": "bin/dsv2json.js",
+        "csv2tsv": "bin/dsv2dsv.js",
+        "dsv2dsv": "bin/dsv2dsv.js",
+        "dsv2json": "bin/dsv2json.js",
+        "json2csv": "bin/json2dsv.js",
+        "json2dsv": "bin/json2dsv.js",
+        "json2tsv": "bin/json2dsv.js",
+        "tsv2csv": "bin/dsv2dsv.js",
+        "tsv2json": "bin/dsv2json.js"
+      },
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/d3-dsv/node_modules/commander": {
+      "version": "7.2.0",
+      "resolved": "https://registry.npmjs.org/commander/-/commander-7.2.0.tgz",
+      "integrity": "sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 10"
+      }
+    },
+    "node_modules/d3-dsv/node_modules/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==",
+      "license": "MIT",
+      "dependencies": {
+        "safer-buffer": ">= 2.1.2 < 3.0.0"
+      },
+      "engines": {
+        "node": ">=0.10.0"
+      }
+    },
+    "node_modules/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==",
+      "license": "BSD-3-Clause",
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/d3-fetch": {
+      "version": "3.0.1",
+      "resolved": "https://registry.npmjs.org/d3-fetch/-/d3-fetch-3.0.1.tgz",
+      "integrity": "sha512-kpkQIM20n3oLVBKGg6oHrUchHM3xODkTzjMoj7aWQFq5QEM+R6E4WkzT5+tojDY7yjez8KgCBRoj4aEr99Fdqw==",
+      "license": "ISC",
+      "dependencies": {
+        "d3-dsv": "1 - 3"
+      },
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/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==",
+      "license": "ISC",
+      "dependencies": {
+        "d3-dispatch": "1 - 3",
+        "d3-quadtree": "1 - 3",
+        "d3-timer": "1 - 3"
+      },
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/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==",
+      "license": "ISC",
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/d3-geo": {
+      "version": "3.1.1",
+      "resolved": "https://registry.npmjs.org/d3-geo/-/d3-geo-3.1.1.tgz",
+      "integrity": "sha512-637ln3gXKXOwhalDzinUgY83KzNWZRKbYubaG+fGVuc/dxO64RRljtCTnf5ecMyE1RIdtqpkVcq0IbtU2S8j2Q==",
+      "license": "ISC",
+      "dependencies": {
+        "d3-array": "2.5.0 - 3"
+      },
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/d3-hierarchy": {
+      "version": "3.1.2",
+      "resolved": "https://registry.npmjs.org/d3-hierarchy/-/d3-hierarchy-3.1.2.tgz",
+      "integrity": "sha512-FX/9frcub54beBdugHjDCdikxThEqjnR93Qt7PvQTOHxyiNCAlvMrHhclk3cD5VeAaq9fxmfRp+CnWw9rEMBuA==",
+      "license": "ISC",
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/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==",
+      "license": "ISC",
+      "dependencies": {
+        "d3-color": "1 - 3"
+      },
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/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==",
+      "license": "ISC",
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/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==",
+      "license": "ISC",
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/d3-quadtree": {
+      "version": "3.0.1",
+      "resolved": "https://registry.npmjs.org/d3-quadtree/-/d3-quadtree-3.0.1.tgz",
+      "integrity": "sha512-04xDrxQTDTCFwP5H6hRhsRcb9xxv2RzkcsygFzmkSIOJy3PeRJP7sNk3VRIbKXcog561P9oU0/rVH6vDROAgUw==",
+      "license": "ISC",
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/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==",
+      "license": "ISC",
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/d3-sankey": {
+      "version": "0.12.3",
+      "resolved": "https://registry.npmjs.org/d3-sankey/-/d3-sankey-0.12.3.tgz",
+      "integrity": "sha512-nQhsBRmM19Ax5xEIPLMY9ZmJ/cDvd1BG3UVvt5h3WRxKg5zGRbvnteTyWAbzeSvlh3tW7ZEmq4VwR5mB3tutmQ==",
+      "license": "BSD-3-Clause",
+      "dependencies": {
+        "d3-array": "1 - 2",
+        "d3-shape": "^1.2.0"
+      }
+    },
+    "node_modules/d3-sankey/node_modules/d3-array": {
+      "version": "2.12.1",
+      "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-2.12.1.tgz",
+      "integrity": "sha512-B0ErZK/66mHtEsR1TkPEEkwdy+WDesimkM5gpZr5Dsg54BiTA5RXtYW5qTLIAcekaS9xfZrzBLF/OAkB3Qn1YQ==",
+      "license": "BSD-3-Clause",
+      "dependencies": {
+        "internmap": "^1.0.0"
+      }
+    },
+    "node_modules/d3-sankey/node_modules/d3-path": {
+      "version": "1.0.9",
+      "resolved": "https://registry.npmjs.org/d3-path/-/d3-path-1.0.9.tgz",
+      "integrity": "sha512-VLaYcn81dtHVTjEHd8B+pbe9yHWpXKZUC87PzoFmsFrJqgFwDe/qxfp5MlfsfM1V5E/iVt0MmEbWQ7FVIXh/bg==",
+      "license": "BSD-3-Clause"
+    },
+    "node_modules/d3-sankey/node_modules/d3-shape": {
+      "version": "1.3.7",
+      "resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-1.3.7.tgz",
+      "integrity": "sha512-EUkvKjqPFUAZyOlhY5gzCxCeI0Aep04LwIRpsZ/mLFelJiUfnK56jo5JMDSE7yyP2kLSb6LtF+S5chMk7uqPqw==",
+      "license": "BSD-3-Clause",
+      "dependencies": {
+        "d3-path": "1"
+      }
+    },
+    "node_modules/d3-sankey/node_modules/internmap": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/internmap/-/internmap-1.0.1.tgz",
+      "integrity": "sha512-lDB5YccMydFBtasVtxnZ3MRBHuaoE8GKsppq+EchKL2U4nK/DmEpPHNH8MZe5HkMtpSiTSOZwfN0tzYjO/lJEw==",
+      "license": "ISC"
+    },
+    "node_modules/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==",
+      "license": "ISC",
+      "dependencies": {
+        "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"
+      },
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/d3-scale-chromatic": {
+      "version": "3.1.0",
+      "resolved": "https://registry.npmjs.org/d3-scale-chromatic/-/d3-scale-chromatic-3.1.0.tgz",
+      "integrity": "sha512-A3s5PWiZ9YCXFye1o246KoscMWqf8BsD9eRiJ3He7C9OBaxKhAd5TFCdEx/7VbKtxxTsu//1mMJFrEt572cEyQ==",
+      "license": "ISC",
+      "dependencies": {
+        "d3-color": "1 - 3",
+        "d3-interpolate": "1 - 3"
+      },
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/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==",
+      "license": "ISC",
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/d3-shape": {
+      "version": "3.2.0",
+      "resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-3.2.0.tgz",
+      "integrity": "sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==",
+      "license": "ISC",
+      "dependencies": {
+        "d3-path": "^3.1.0"
+      },
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/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==",
+      "license": "ISC",
+      "dependencies": {
+        "d3-array": "2 - 3"
+      },
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/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==",
+      "license": "ISC",
+      "dependencies": {
+        "d3-time": "1 - 3"
+      },
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/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==",
+      "license": "ISC",
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/d3-transition": {
+      "version": "3.0.1",
+      "resolved": "https://registry.npmjs.org/d3-transition/-/d3-transition-3.0.1.tgz",
+      "integrity": "sha512-ApKvfjsSR6tg06xrL434C0WydLr7JewBB3V+/39RMHsaXTOG0zmt/OAXeng5M5LBm0ojmxJrpomQVZ1aPvBL4w==",
+      "license": "ISC",
+      "dependencies": {
+        "d3-color": "1 - 3",
+        "d3-dispatch": "1 - 3",
+        "d3-ease": "1 - 3",
+        "d3-interpolate": "1 - 3",
+        "d3-timer": "1 - 3"
+      },
+      "engines": {
+        "node": ">=12"
+      },
+      "peerDependencies": {
+        "d3-selection": "2 - 3"
+      }
+    },
+    "node_modules/d3-zoom": {
+      "version": "3.0.0",
+      "resolved": "https://registry.npmjs.org/d3-zoom/-/d3-zoom-3.0.0.tgz",
+      "integrity": "sha512-b8AmV3kfQaqWAuacbPuNbL6vahnOJflOhexLzMMNLga62+/nh0JzvJ0aO/5a5MVgUFGS7Hu1P9P03o3fJkDCyw==",
+      "license": "ISC",
+      "dependencies": {
+        "d3-dispatch": "1 - 3",
+        "d3-drag": "2 - 3",
+        "d3-interpolate": "1 - 3",
+        "d3-selection": "2 - 3",
+        "d3-transition": "2 - 3"
+      },
+      "engines": {
+        "node": ">=12"
+      }
+    },
     "node_modules/dargs": {
       "version": "7.0.0",
       "resolved": "https://registry.npmjs.org/dargs/-/dargs-7.0.0.tgz",
         "url": "https://github.com/sponsors/ljharb"
       }
     },
+    "node_modules/date-fns": {
+      "version": "4.1.0",
+      "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-4.1.0.tgz",
+      "integrity": "sha512-Ukq0owbQXxa/U3EGtsdVBkR1w7KOQ5gIBqdH2hkvknzZPYvBxb/aa6E8L7tmjFtkwZBu3UXBbjIgPo/Ez4xaNg==",
+      "license": "MIT",
+      "funding": {
+        "type": "github",
+        "url": "https://github.com/sponsors/kossnocorp"
+      }
+    },
     "node_modules/dayjs": {
       "version": "1.11.13",
       "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.13.tgz",
         "url": "https://github.com/sponsors/ljharb"
       }
     },
+    "node_modules/delaunator": {
+      "version": "5.0.1",
+      "resolved": "https://registry.npmjs.org/delaunator/-/delaunator-5.0.1.tgz",
+      "integrity": "sha512-8nvh+XBe96aCESrGOqMp/84b13H9cdKbG5P2ejQCh4d4sK9RL4371qou9drQjMhvnPmhWl5hnmqbEE0fXr9Xnw==",
+      "license": "ISC",
+      "dependencies": {
+        "robust-predicates": "^3.0.2"
+      }
+    },
     "node_modules/delayed-stream": {
       "version": "1.0.0",
       "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
         "url": "https://github.com/sponsors/sindresorhus"
       }
     },
+    "node_modules/html-to-image": {
+      "version": "1.11.11",
+      "resolved": "https://registry.npmjs.org/html-to-image/-/html-to-image-1.11.11.tgz",
+      "integrity": "sha512-9gux8QhvjRO/erSnDPv28noDZcPZmYE7e1vFsBLKLlRlKDSqNJYebj6Qz1TGd5lsRV+X+xYyjCKjuZdABinWjA==",
+      "license": "MIT"
+    },
     "node_modules/htmlescape": {
       "version": "1.1.1",
       "resolved": "https://registry.npmjs.org/htmlescape/-/htmlescape-1.1.1.tgz",
         "node": ">= 0.4"
       }
     },
+    "node_modules/internal-slot/node_modules/hasown": {
+      "version": "2.0.2",
+      "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
+      "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==",
+      "dev": true,
+      "dependencies": {
+        "function-bind": "^1.1.2"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      }
+    },
+    "node_modules/internal-slot/node_modules/object-inspect": {
+      "version": "1.13.4",
+      "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz",
+      "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==",
+      "dev": true,
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/internal-slot/node_modules/side-channel": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz",
+      "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==",
+      "dev": true,
+      "dependencies": {
+        "es-errors": "^1.3.0",
+        "object-inspect": "^1.13.3",
+        "side-channel-list": "^1.0.0",
+        "side-channel-map": "^1.0.1",
+        "side-channel-weakmap": "^1.0.2"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/internmap": {
+      "version": "2.0.3",
+      "resolved": "https://registry.npmjs.org/internmap/-/internmap-2.0.3.tgz",
+      "integrity": "sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==",
+      "license": "ISC",
+      "engines": {
+        "node": ">=12"
+      }
+    },
     "node_modules/invariant": {
       "version": "2.2.4",
       "resolved": "https://registry.npmjs.org/invariant/-/invariant-2.2.4.tgz",
         "inherits": "^2.0.1"
       }
     },
+    "node_modules/robust-predicates": {
+      "version": "3.0.2",
+      "resolved": "https://registry.npmjs.org/robust-predicates/-/robust-predicates-3.0.2.tgz",
+      "integrity": "sha512-IXgzBWvWQwE6PrDI05OvmXUIruQTcoMDzRsOd5CDvHCVLcLHMTSYvOK5Cm46kWqlV3yAbuSpBZdJ5oP5OUoStg==",
+      "license": "Unlicense"
+    },
     "node_modules/rollup": {
       "version": "4.22.4",
       "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.22.4.tgz",
         "queue-microtask": "^1.2.2"
       }
     },
+    "node_modules/rw": {
+      "version": "1.3.3",
+      "resolved": "https://registry.npmjs.org/rw/-/rw-1.3.3.tgz",
+      "integrity": "sha512-PdhdWy89SiZogBLaw42zdeqtRJ//zFd2PgQavcICDUgJT5oW10QCRKbJ6bg4r0/UY2M6BWd5tkxuGFRvCkgfHQ==",
+      "license": "BSD-3-Clause"
+    },
     "node_modules/rxjs": {
       "version": "6.6.3",
       "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.6.3.tgz",
     "node_modules/safer-buffer": {
       "version": "2.1.2",
       "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
-      "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==",
-      "dev": true
+      "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="
     },
     "node_modules/sass": {
       "version": "1.77.6",
         "node": ">=0.6"
       }
     },
+    "node_modules/topojson-client": {
+      "version": "3.1.0",
+      "resolved": "https://registry.npmjs.org/topojson-client/-/topojson-client-3.1.0.tgz",
+      "integrity": "sha512-605uxS6bcYxGXw9qi62XyrV6Q3xwbndjachmNxu8HWTtVPxZfEJN9fd/SZS1Q54Sn2y0TMyMxFj/cJINqGHrKw==",
+      "license": "ISC",
+      "dependencies": {
+        "commander": "2"
+      },
+      "bin": {
+        "topo2geo": "bin/topo2geo",
+        "topomerge": "bin/topomerge",
+        "topoquantize": "bin/topoquantize"
+      }
+    },
+    "node_modules/topojson-client/node_modules/commander": {
+      "version": "2.20.3",
+      "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz",
+      "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==",
+      "license": "MIT"
+    },
     "node_modules/tough-cookie": {
       "version": "4.1.4",
       "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-4.1.4.tgz",
index 1392d18b85e2a26a3296441f7de887e52b4df524..4d681cac9d9a18b7aceebb7d0ef0997ac193de77 100644 (file)
@@ -51,6 +51,7 @@
     "@angular/platform-browser": "18.2.11",
     "@angular/platform-browser-dynamic": "18.2.11",
     "@angular/router": "18.2.11",
+    "@carbon/charts-angular": "1.23.9",
     "@carbon/icons": "11.41.0",
     "@carbon/styles": "1.83.0",
     "@ibm/plex": "6.4.0",
index 472a1cf32eaaaf9f58ed201b19209693cdd3dc81..b224f6f2a7055f75da669860a12c4696e1b3883c 100644 (file)
                 [total]="row.info.bytes_quota"
                 [used]="row.info.bytes_used"
                 [title]="row.name"
-                [showFreeToolTip]="false"
-                customLegend="Quota"
-                [customLegendValue]="row.info.bytes_quota"
                 decimals="2"></cd-usage-bar>
-
   <ng-template #noLimitTpl>
     <span ngbTooltip="Quota limit is not set"
           *ngIf="row.info.bytes_pcent === 'undefined'"
index bfe66c864b3785bec641d9176ac8923071da4d13..d03e5dbfe6d6d14565c616e7a882f4d6a6c3fa45 100644 (file)
@@ -33,9 +33,6 @@
                 [total]="row.info.bytes_quota"
                 [used]="row.info.bytes_used"
                 [title]="row.name"
-                [showFreeToolTip]="false"
-                customLegend="Quota"
-                [customLegendValue]="row.info.bytes_quota"
                 decimals="2"></cd-usage-bar>
 
   <ng-template #noLimitTpl>
index e5caef761d46e36cee6728bea7e26c8d4b05fa38..e423834b53ad7bbedf30549018289f9afc802efe 100644 (file)
@@ -1,5 +1,11 @@
 export class CssHelper {
-  propertyValue(propertyName: string): string {
-    return getComputedStyle(document.body).getPropertyValue(`--${propertyName}`);
+  /**
+   * Gets the value of a CSS custom property (CSS variable).
+   * @param propertyName The name of the variable without `--`.
+   * @param element Optional: HTMLElement to scope the variable lookup. Defaults to `document.body`.
+   */
+  propertyValue(propertyName: string, element?: HTMLElement): string {
+    const target = element ?? document.body;
+    return getComputedStyle(target).getPropertyValue(`--${propertyName}`);
   }
 }
index 80ae2e7d8211cc2fb03e6852f0f0ba87bc8a4043..64bed45df778a96ea4aafd2f7b4300d23142e318 100644 (file)
@@ -88,6 +88,7 @@ import { SidePanelComponent } from './side-panel/side-panel.component';
 import InfoIcon from '@carbon/icons/es/information/16';
 import CopyIcon from '@carbon/icons/es/copy/32';
 import downloadIcon from '@carbon/icons/es/download/16';
+import { ChartsModule } from '@carbon/charts-angular';
 
 @NgModule({
   imports: [
@@ -127,7 +128,8 @@ import downloadIcon from '@carbon/icons/es/download/16';
     ComboBoxModule,
     ProgressIndicatorModule,
     BaseChartDirective,
-    PanelModule
+    PanelModule,
+    ChartsModule
   ],
   declarations: [
     SparklineComponent,
index e29b0dd8b4be227e2950d0ca39ccb079cb55e3c8..fc9990b7422ac2bb15071ddf5137199055b0e8f9 100644 (file)
@@ -1,45 +1,15 @@
-<ng-template #usageTooltipTpl>
-  <table *ngIf="!showMultisiteTooltip">
-    <tr>
-      <td class="text-left me-1">Used:</td>
-      <td class="text-right"><strong> {{ isBinary ? (used | dimlessBinary) : (used | dimless) }}</strong></td>
-    </tr>
-    <tr *ngIf="calculatePerc && showFreeToolTip">
-      <td class="text-left me-1">Free:</td>
-      <td class="'text-right"><strong>{{ isBinary ? (total - used | dimlessBinary) : (total - used | dimless) }}</strong></td>
-    </tr>
-    <tr *ngIf="customLegend">
-      <td class="text-left me-1">{{ customLegend }}:</td>
-      <td class="text-right"><strong>{{ isBinary ? (customLegendValue | dimlessBinary) : (customLegend[1] | dimless) }}</strong></td>
-    </tr>
-  </table>
-  <table *ngIf="showMultisiteTooltip">
-    <tr>
-      <td class="text-left">Total Shards:&nbsp;</td>
-      <td class="text-right"><strong> {{ total }}</strong></td>
-    </tr>
-    <tr *ngIf="calculatePerc">
-      <td class="text-left">Transferred Shards:&nbsp;</td>
-      <td class="'text-right"><strong>{{ used }}</strong></td>
-    </tr>
-  </table>
+<ng-container *ngIf="customLabel">
+  <ng-container *ngIf="isCustomLabelTemplate; else stringLabel">
+    <ng-container *ngTemplateOutlet="customLabel"></ng-container>
+  </ng-container>
+  <ng-template #stringLabel>
+    <ng-container i18n>{{ customLabel }}</ng-container>
+  </ng-template>
+</ng-container>
+<ibm-meter-chart *ngIf="used >=0 && total > 0; else noDataFound"
+                 [data]="data"
+                 [options]="options">
+</ibm-meter-chart>
+<ng-template #noDataFound>
+  <span i18n>{{noDataText}}</span>
 </ng-template>
-
-<div class="progress"
-     data-placement="left"
-     [ngbTooltip]="usageTooltipTpl">
-  <div class="progress-bar bg-info"
-       [ngClass]="{'bg-warning': warningThreshold && (warningThreshold >= 0) && (usedPercentage/100 >= warningThreshold), 'bg-danger': errorThreshold && (errorThreshold >= 0) && (usedPercentage/100 >= errorThreshold)}"
-       role="progressbar"
-       [attr.aria-label]="{ title }"
-       i18n-aria-label="The title of this usage bar is { title }"
-       [style.width]="usedPercentage + '%'">
-    <span [style.color]="usedPercentage < 60 ? 'black' : 'white'">{{ usedPercentage | number: '1.0-' + decimals }}%</span>
-  </div>
-  <div class="progress-bar bg-freespace"
-       role="progressbar"
-       [attr.aria-label]="{ title }"
-       i18n-aria-label="The title of this usage bar is { title }"
-       [style.width]="freePercentage + '%'">
-  </div>
-</div>
index 3c57015fec799f2421c59c1244f75c6a66e3490e..bba76c558ae82cf6d250c801f68f93134e51e9c7 100644 (file)
@@ -1,35 +1,22 @@
-@use './src/styles/vendor/variables' as vv;
+@use '@carbon/type';
 
-.bg-info {
-  background-color: vv.$primary !important;
-}
-
-.bg-warning {
-  background-color: vv.$warning !important;
-}
-
-.bg-danger {
-  background-color: vv.$danger !important;
-}
+::ng-deep {
+  .cds--cc--chart-wrapper {
+    text {
+      @include type.type-style('heading-01');
 
-.bg-freespace {
-  background-color: vv.$gray-400 !important;
-}
+      font-family: inherit;
+      font-weight: normal;
+    }
+  }
 
-.progress {
-  height: 20px;
-  margin-bottom: 0;
-  position: relative;
+  .meter-tooltip {
+    @include type.type-style('helper-text-01');
 
-  div.progress-bar {
-    position: static;
-  }
+    padding-top: 0.25rem;
 
-  span {
-    color: vv.$white;
-    display: block;
-    font-weight: normal;
-    position: absolute;
-    width: 100%;
+    div {
+      padding-bottom: 0.25rem;
+    }
   }
 }
index 45e6a06b6d37a3d58fcebe17873eca522c338274..a3b6b305feca76f4a54bc28a17345c8c3d398d3c 100644 (file)
@@ -1,10 +1,14 @@
 import { ComponentFixture, TestBed } from '@angular/core/testing';
-
 import { NgbTooltipModule } from '@ng-bootstrap/ng-bootstrap';
-
 import { PipesModule } from '~/app/shared/pipes/pipes.module';
 import { configureTestBed } from '~/testing/unit-test-helper';
 import { UsageBarComponent } from './usage-bar.component';
+import { ElementRef } from '@angular/core';
+import { CssHelper } from '../../classes/css-helper';
+
+const mockElementRef = {
+  nativeElement: {}
+};
 
 describe('UsageBarComponent', () => {
   let component: UsageBarComponent;
@@ -12,16 +16,140 @@ describe('UsageBarComponent', () => {
 
   configureTestBed({
     imports: [PipesModule, NgbTooltipModule],
-    declarations: [UsageBarComponent]
+    declarations: [UsageBarComponent],
+    providers: [{ provide: ElementRef, useValue: mockElementRef }, CssHelper]
   });
 
   beforeEach(() => {
     fixture = TestBed.createComponent(UsageBarComponent);
     component = fixture.componentInstance;
+
+    window.getComputedStyle = jest.fn().mockReturnValue({
+      getPropertyValue: (name: string) => {
+        const mockStyles: Record<string, string> = {
+          '--cds-support-info': '#00f',
+          '--cds-support-warning': '#ff0',
+          '--cds-support-danger': '#f00'
+        };
+        return mockStyles[name] || '';
+      }
+    });
+
     fixture.detectChanges();
   });
 
   it('should create', () => {
     expect(component).toBeTruthy();
   });
+
+  it('should get correct CSS variable value', () => {
+    const value = component['getCssVariableValue']('cds-support-info');
+    expect(value.trim()).toBe('#00f');
+  });
+
+  it('should return the correct tooltip string for defaultTooltip', () => {
+    component.total = 100000;
+    component.used = 40000;
+    component.isBinary = false;
+    component.decimals = 2;
+    const normalize = (str: string) => str.replace(/\s+/g, ' ').trim();
+    const tooltip = component['defaultTooltip']();
+    expect(normalize(tooltip)).toContain(
+      normalize(`<div class="meter-tooltip">
+    <div><strong>Used:</strong> 40 k</div>
+    <div><strong>Available:</strong> 60 k</div>
+    <div><strong>Total:</strong> 100 k</div>
+  </div>`)
+    );
+  });
+  it('should return custom breakdown format when customBreakdownFormatter is provided', () => {
+    component.customBreakdownFormatter = jest.fn().mockReturnValue('Custom breakdown format');
+    const breakdownFormatted = component['getBreakdownFormatter']();
+    expect(breakdownFormatted).toBe('Custom breakdown format');
+    expect(component.customBreakdownFormatter).toHaveBeenCalledWith({
+      used: component.used,
+      total: component.total
+    });
+  });
+
+  it('should return custom total format when customTotalFormatter is provided', () => {
+    component.customTotalFormatter = jest.fn().mockReturnValue('Custom total format');
+    const totalFormatted = component['getTotalFormatter']();
+    expect(totalFormatted).toBe('Custom total format');
+    expect(component.customTotalFormatter).toHaveBeenCalledWith(component.total);
+  });
+
+  describe('when enablePercentageLabel is true', () => {
+    beforeEach(() => {
+      component.total = 200;
+      component.used = 50;
+      component.decimals = 2;
+      component.enablePercentageLabel = true;
+    });
+
+    it('should return empty total formatter', () => {
+      const result = component['getTotalFormatter']();
+      expect(result).toBe('');
+    });
+
+    it('should return percentage breakdown formatter', () => {
+      const result = component['getBreakdownFormatter']();
+      expect(result).toBe('25.00%');
+    });
+
+    it('should return "0%" when used is 0', () => {
+      component.used = 0;
+      component.total = 100;
+      expect(component['getBreakdownFormatter']()).toBe('0%');
+    });
+
+    it('should clamp small percentage to 0% if less than 0.01%, otherwise format normally', () => {
+      component.total = 100000000;
+
+      component.used = 5; // 0.000005 => 0.0005%
+      expect(component['getBreakdownFormatter']()).toBe('0%');
+
+      component.used = 10000; // 10000 / 100000000 = 0.01%
+      expect(component['getBreakdownFormatter']()).toBe('0.01%');
+
+      component.used = 500000; // 0.5%
+      expect(component['getBreakdownFormatter']()).toBe('0.50%');
+    });
+  });
+
+  describe('when enablePercentageLabel is false', () => {
+    beforeEach(() => {
+      component.total = 10242424;
+      component.used = 5122424;
+      component.enablePercentageLabel = false;
+      component.decimals = 2;
+      component.isBinary = true;
+    });
+
+    it('should return total with formatted binary value', () => {
+      const result = component['getTotalFormatter']();
+      expect(result).toContain('9.77 MiB total');
+    });
+
+    it('should return used with formatted binary value in breakdown', () => {
+      const result = component['getBreakdownFormatter']();
+      expect(result).toContain('used');
+      expect(result).toContain('4.89 MiB used');
+    });
+
+    it('should return default total format when customTotalFormatter is not provided', () => {
+      component.total = 100000;
+      component.customTotalFormatter = undefined;
+      const totalFormatted = component['getTotalFormatter']();
+      expect(totalFormatted).toContain('97.66 KiB total');
+    });
+
+    it('should return default breakdown format when customBreakdownFormatter is not provided', () => {
+      component.used = 30000;
+      component.total = 100000;
+      component.customBreakdownFormatter = undefined;
+      const breakdownFormatted = component['getBreakdownFormatter']();
+      expect(breakdownFormatted).toContain('29.3 KiB used');
+    });
+  });
 });
index f147227eb1bc5415aac73890cca22c47412b1ac6..15177f1b1322e2be6a764f5fa7a08e2d5656ae82 100644 (file)
-import { Component, Input, OnChanges } from '@angular/core';
+import { Component, Input, ElementRef, OnInit, TemplateRef } from '@angular/core';
+import { ChartTabularData, MeterChartOptions, Statuses } from '@carbon/charts-angular';
+import { StatusToCssMap } from '../../enum/usage-bar-chart.enum';
+import { CssHelper } from '../../classes/css-helper';
+import { DimlessBinaryPipe } from '../../pipes/dimless-binary.pipe';
+import { DimlessPipe } from '../../pipes/dimless.pipe';
 
-import _ from 'lodash';
+const PRIMARY_COLOR = 'primary'; // Default theme primary color variable
 
 @Component({
   selector: 'cd-usage-bar',
   templateUrl: './usage-bar.component.html',
   styleUrls: ['./usage-bar.component.scss']
 })
-export class UsageBarComponent implements OnChanges {
-  @Input()
-  total: number;
-  @Input()
-  used: any;
-  @Input()
-  warningThreshold?: number;
-  @Input()
-  errorThreshold?: number;
-  @Input()
-  isBinary = true;
-  @Input()
-  decimals = 0;
-  @Input()
-  calculatePerc = true;
-  @Input()
-  title = $localize`usage`;
-  @Input()
-  customLegend?: string;
-  @Input()
-  customLegendValue?: string;
-  @Input()
-  showFreeToolTip = true;
-  @Input()
-  showMultisiteTooltip = false;
-
-  usedPercentage: number;
-  freePercentage: number;
-
-  ngOnChanges() {
-    if (this.calculatePerc) {
-      this.usedPercentage = this.total > 0 ? (this.used / this.total) * 100 : 0;
-      this.freePercentage = 100 - this.usedPercentage;
-    } else {
-      if (this.used) {
-        this.used = this.used.slice(0, -1);
-        this.usedPercentage = Number(this.used);
-        this.freePercentage = 100 - this.usedPercentage;
-      } else {
-        this.usedPercentage = 0;
+export class UsageBarComponent implements OnInit {
+  // Total amount of resource (e.g., disk, memory) available, in bytes.
+  @Input({ required: true }) total: number = 0;
+
+  // Amount of resource currently used, in bytes.
+  @Input({ required: true }) used: number = 0;
+
+  // Optional label or title for the chart, used in legends and tooltips.
+  @Input() title: string = '';
+
+  // Threshold (0–1) to mark the start of the "warning" zone (e.g., 0.5 = 50%).
+  @Input() warningThreshold: number;
+
+  // Threshold (0–1) to mark the start of the "danger" zone (e.g., 0.8 = 80%).
+  @Input() errorThreshold: number;
+
+  /**
+   * If true, values are formatted using binary units (e.g., KiB, MiB).
+   * If false, decimal units are used (e.g., kB, MB).
+   */
+  @Input() isBinary: boolean = true;
+
+  // Number of decimal places to show in formatted numbers.
+  @Input() decimals: number = 2;
+
+  // Height of the chart component (CSS units like `rem`, `px`, `%`).
+  @Input() height: string = '2rem';
+
+  // Width of the chart component (CSS units like `rem`, `px`, `%`).
+  @Input() width: string = '15rem';
+
+  // Custom unit label for the total value (e.g., "B", "kB", "requests").
+  @Input() totalUnit: string = 'B';
+
+  // Whether chart animations are enabled (e.g., grow bars on change).
+  @Input() animations: boolean = false;
+
+  // Enables the legend section (useful for grouped charts).
+  @Input() legendEnabled: boolean = false;
+
+  // Enables the toolbar (for exporting, toggling datasets, etc., depending on chart lib).
+  @Input() toolbarEnabled: boolean = false;
+
+  // Text to display when `total` is zero or there’s no data.
+  @Input() noDataText: string = '-';
+
+  /**
+   * If true, breakdown is shown as a percentage (e.g., "75% used").
+   * When false, raw values like "750 MB used" are shown instead.
+   */
+  @Input() enablePercentageLabel: boolean = true;
+
+  // Optional custom label (e.g., <ng-template><div>Capacity</div></<ng-template>) (overrides default label).
+  @Input() customLabel: TemplateRef<any> | string = '';
+
+  // Custom tooltip options passed directly to the chart library.
+  @Input() tooltipOptions?: MeterChartOptions['tooltip'] = {};
+
+  // Optional override for the default color scale (e.g., { Memory: "#ff0000" }).
+  @Input() customColorScale?: { [key: string]: string };
+
+  /**
+   * Optional custom formatter for the total value (overrides default formatter).
+   * Example usage: (total) => `${total} requests`
+   */
+  @Input() customTotalFormatter?: (total: number) => string;
+
+  /**
+   * Optional custom formatter for the breakdown section (overrides default formatter).
+   * Example usage: ({ used, total }) => `${used} of ${total} used`
+   */
+  @Input() customBreakdownFormatter?: (x: { used: number; total: number }) => string;
+
+  data: ChartTabularData = [];
+  options: MeterChartOptions = {};
+
+  constructor(
+    private elementRef: ElementRef,
+    private cssHelper: CssHelper,
+    private dimlessPipe: DimlessPipe,
+    private dimlessBinaryPipe: DimlessBinaryPipe
+  ) {}
+
+  get isCustomLabelTemplate(): boolean {
+    return this.customLabel instanceof TemplateRef;
+  }
+
+  ngOnInit(): void {
+    const chartData: ChartTabularData = [{ group: this.title || '', value: this.used }];
+    let thresholds: MeterChartOptions['meter']['status']['ranges'] = [];
+    if (this.warningThreshold && this.errorThreshold) {
+      thresholds = [
+        {
+          range: [this.total * this.warningThreshold, this.total * this.errorThreshold],
+          status: Statuses.WARNING
+        },
+        { range: [this.total * this.errorThreshold, this.total], status: Statuses.DANGER }
+      ];
+    }
+    const colorScale = this.customColorScale || {
+      [this.title || '']: this.getStatusFromThresholds(this.used, thresholds)
+    };
+
+    const chartOptions: MeterChartOptions = {
+      legend: { enabled: this.legendEnabled },
+      toolbar: { enabled: this.toolbarEnabled },
+      tooltip: {
+        enabled: true,
+        ...this.tooltipOptions,
+        customHTML: this.tooltipOptions?.customHTML || (() => this.defaultTooltip())
+      },
+      animations: this.animations,
+      height: this.height,
+      width: this.width,
+      color: { scale: colorScale },
+      meter: {
+        showLabels: !this.customLabel,
+        proportional: {
+          total: this.total,
+          unit: this.totalUnit,
+          totalFormatter: () => this.getTotalFormatter(),
+          breakdownFormatter: () => this.getBreakdownFormatter()
+        }
+      }
+    };
+    if (thresholds.length > 0) {
+      chartOptions.meter = {
+        ...chartOptions.meter,
+        status: {
+          ranges: thresholds
+        }
+      };
+    }
+    this.data = chartData;
+    this.options = chartOptions;
+  }
+
+  private getStatusFromThresholds(
+    value: number,
+    thresholds: MeterChartOptions['meter']['status']['ranges']
+  ): string {
+    for (const threshold of thresholds) {
+      const [min, max] = threshold.range;
+      if (value >= min && value < max) {
+        return this.getCssVariableValue(
+          StatusToCssMap[threshold.status as keyof typeof StatusToCssMap]
+        );
+      }
+    }
+    return this.getCssVariableValue(PRIMARY_COLOR);
+  }
+
+  private getCssVariableValue(variableName: string): string {
+    return this.cssHelper.propertyValue(variableName, this.elementRef.nativeElement);
+  }
+
+  private formatValue(value: number): string {
+    return this.isBinary
+      ? this.dimlessBinaryPipe.transform(value, this.decimals)
+      : this.dimlessPipe.transform(value, this.decimals);
+  }
+
+  private defaultTooltip(): string {
+    const used = this.formatValue(this.used);
+    const available = this.formatValue(this.total - this.used);
+    const total = this.formatValue(this.total);
+    return `<div class="meter-tooltip">
+      <div><strong>Used:</strong> ${used}</div>
+      <div><strong>Available:</strong> ${available}</div>
+      <div><strong>Total:</strong> ${total}</div>
+    </div>`;
+  }
+
+  private getTotalFormatter(): string {
+    if (this.customTotalFormatter) {
+      return this.customTotalFormatter!(this.total);
+    }
+    if (this.enablePercentageLabel && this.total > 0) {
+      return '';
+    }
+
+    return $localize`${this.formatValue(this.total)} total`;
+  }
+
+  private getBreakdownFormatter(): string {
+    if (this.customBreakdownFormatter) {
+      return this.customBreakdownFormatter!({ used: this.used, total: this.total });
+    }
+
+    if (this.enablePercentageLabel && this.total > 0) {
+      const percentage = (this.used / this.total) * 100;
+      if (percentage < 0.01) {
+        return '0%';
       }
+
+      const formatted = percentage.toFixed(this.decimals);
+      return `${formatted}%`;
     }
+
+    return $localize`${this.formatValue(this.used)} used`;
   }
 }
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/enum/usage-bar-chart.enum.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/enum/usage-bar-chart.enum.ts
new file mode 100644 (file)
index 0000000..16a388a
--- /dev/null
@@ -0,0 +1,7 @@
+import { Statuses } from '@carbon/charts-angular';
+
+export const StatusToCssMap: Record<Statuses, string> = {
+  [Statuses.SUCCESS]: 'cds-support-success',
+  [Statuses.WARNING]: 'cds-support-warning',
+  [Statuses.DANGER]: 'cds-support-error'
+};
index cb2d8c64fe8de1b3fb2fb7ce8fc4b9240bc88d5f..f971cf529f36d04630f240bcc17a9c9bbcc094da 100644 (file)
@@ -5,3 +5,22 @@ Object.defineProperty(window, 'getComputedStyle', {
     }
   })
 });
+
+/* Carbon Charts (via @carbon/charts or @carbon/charts-angular) uses ResizeObserver
+  to automatically adjust chart size on container resize, and it expects it to be
+  available globally — which isn’t the case in unit tests.
+*/
+Object.defineProperty(window, 'ResizeObserver', {
+  writable: true,
+  configurable: true,
+  value: class {
+    observe() {}
+    unobserve() {}
+    disconnect() {}
+  }
+});
+
+Object.defineProperty(SVGElement.prototype, 'getComputedTextLength', {
+  configurable: true,
+  value: () => 100 // or any realistic number
+});
index 0953f8282285aa98b9365fcf58fb97be37aafbf9..f67298732645505a75909fe2aacafb307ff7fa0a 100644 (file)
@@ -1,5 +1,6 @@
 /* You can add global styles to this file, and also import other style files */
 @use './src/styles/defaults' as *;
+@use '@carbon/charts/styles.css' as *;
 
 @import './src/styles/carbon-defaults.scss';