diff --git a/package-lock.json b/package-lock.json index a02a7fba..424b55b7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,6 +11,8 @@ "license": "Apache-2.0", "dependencies": { "@babel/runtime": "^7.28.6", + "@carbon/react": "^1.100.0", + "@carbon/styles": "^1.99.0", "@date-io/core": "^3.2.0", "@date-io/date-fns": "^3.2.1", "@emotion/react": "^11.14.0", @@ -71,6 +73,7 @@ "integrity": "sha512-H3mcG6ZDLTlYfaSNi0iOKkigqMFvkTKlGUYlD8GW7nNOYRrevuA46iTypPyv+06V3fEmvvazfntkBU34L0azAw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@babel/code-frame": "^7.28.6", "@babel/generator": "^7.28.6", @@ -546,6 +549,185 @@ "node": ">=6.9.0" } }, + "node_modules/@carbon/colors": { + "version": "11.46.0", + "resolved": "https://registry.npmjs.org/@carbon/colors/-/colors-11.46.0.tgz", + "integrity": "sha512-YL4BH2hxHkUT0+wMn8cO3sYN7rb9Nnp7rGttoblM0iTy83n/urwRPcxudifRwJLtASQpravCyLHdIC9WnTtIAA==", + "hasInstallScript": true, + "license": "Apache-2.0", + "dependencies": { + "@ibm/telemetry-js": "^1.5.0" + } + }, + "node_modules/@carbon/feature-flags": { + "version": "0.32.0", + "resolved": "https://registry.npmjs.org/@carbon/feature-flags/-/feature-flags-0.32.0.tgz", + "integrity": "sha512-a1rFplSEFPwJ4ZsuwvOaKHgoLqPNhjCJdWY6VTgCoytRZqtgYWqwYFEqQkm9/f1mX1lHr6oK/eBpAcmi0Izuvg==", + "hasInstallScript": true, + "license": "Apache-2.0", + "dependencies": { + "@ibm/telemetry-js": "^1.5.0" + } + }, + "node_modules/@carbon/grid": { + "version": "11.49.0", + "resolved": "https://registry.npmjs.org/@carbon/grid/-/grid-11.49.0.tgz", + "integrity": "sha512-zZfj/sbwJpXboduVFNUXUdV6LmsEH39fNQQMye4V+788sdvs+ErO8L3onBZFpsek5gI4ebwjpWJu2g5szu2+kQ==", + "hasInstallScript": true, + "license": "Apache-2.0", + "dependencies": { + "@carbon/layout": "^11.47.0", + "@ibm/telemetry-js": "^1.5.0" + } + }, + "node_modules/@carbon/icon-helpers": { + "version": "10.71.0", + "resolved": "https://registry.npmjs.org/@carbon/icon-helpers/-/icon-helpers-10.71.0.tgz", + "integrity": "sha512-T6KcxkNIa609jPC+8A7u5husSY+mH60lCNNa3ivcOyuREoVYHwnieM7GIECigF/oaGaF5eBzrxYFx2+8mLRk1A==", + "hasInstallScript": true, + "license": "Apache-2.0", + "dependencies": { + "@ibm/telemetry-js": "^1.5.0" + } + }, + "node_modules/@carbon/icons-react": { + "version": "11.74.0", + "resolved": "https://registry.npmjs.org/@carbon/icons-react/-/icons-react-11.74.0.tgz", + "integrity": "sha512-tP/ZwM3e86zDm/8mup1NoObdaBl2xqZlroWP/Z1PQ9bCYOOFelR6r34aObWiDBJVpKb5YwwZWYUrl+/98fmDRQ==", + "hasInstallScript": true, + "license": "Apache-2.0", + "dependencies": { + "@carbon/icon-helpers": "^10.71.0", + "@ibm/telemetry-js": "^1.5.0", + "prop-types": "^15.8.1" + }, + "peerDependencies": { + "react": ">=16" + } + }, + "node_modules/@carbon/layout": { + "version": "11.47.0", + "resolved": "https://registry.npmjs.org/@carbon/layout/-/layout-11.47.0.tgz", + "integrity": "sha512-2XR4TVp3uf2IB0WdoZuDcBbc9C8EN/JvZAw9BdHJ3njng8FlUAQUkTFvfoUsJl10868rqA6YeClCElBS4BHofg==", + "hasInstallScript": true, + "license": "Apache-2.0", + "dependencies": { + "@ibm/telemetry-js": "^1.5.0" + } + }, + "node_modules/@carbon/motion": { + "version": "11.40.0", + "resolved": "https://registry.npmjs.org/@carbon/motion/-/motion-11.40.0.tgz", + "integrity": "sha512-QjvjMcC3G289GKYDvrf5dDuyol7SXm0TYaFltx+AkJdU6fptDCJ/qjUL5SdVrsLse3jFuI8rada9tRAL5xHS1g==", + "hasInstallScript": true, + "license": "Apache-2.0", + "dependencies": { + "@ibm/telemetry-js": "^1.5.0" + } + }, + "node_modules/@carbon/react": { + "version": "1.100.0", + "resolved": "https://registry.npmjs.org/@carbon/react/-/react-1.100.0.tgz", + "integrity": "sha512-QlJ/bqiQn3fF3EX1YfVN3+zYvbqiGuMULtsI+wtuMaoEOJcR9FBwUYSP4w7lXTCxQ7WkB/wTLrekVcGgRoNquQ==", + "hasInstallScript": true, + "license": "Apache-2.0", + "dependencies": { + "@babel/runtime": "^7.27.3", + "@carbon/feature-flags": ">=0.32.0", + "@carbon/icons-react": "^11.74.0", + "@carbon/layout": "^11.47.0", + "@carbon/styles": "^1.99.0", + "@carbon/utilities": "^0.15.0", + "@floating-ui/react": "^0.27.4", + "@ibm/telemetry-js": "^1.5.0", + "classnames": "2.5.1", + "copy-to-clipboard": "^3.3.1", + "downshift": "9.0.10", + "es-toolkit": "^1.27.0", + "flatpickr": "4.6.13", + "invariant": "^2.2.3", + "prop-types": "^15.8.1", + "react-fast-compare": "^3.2.2", + "tabbable": "^6.2.0" + }, + "peerDependencies": { + "react": "^16.8.6 || ^17.0.1 || ^18.2.0 || ^19.0.0", + "react-dom": "^16.8.6 || ^17.0.1 || ^18.2.0 || ^19.0.0", + "react-is": "^16.13.1 || ^17.0.2 || ^18.3.1 || ^19.0.0", + "sass": "^1.33.0" + } + }, + "node_modules/@carbon/styles": { + "version": "1.99.0", + "resolved": "https://registry.npmjs.org/@carbon/styles/-/styles-1.99.0.tgz", + "integrity": "sha512-71iypyzR97h6Z94XRZyel3IEo4+n9TRylKdsYUJASNs7GNIjsIBlwKRn+upUktsyWVNTV1iQ9uzo3UkFcRiEFQ==", + "hasInstallScript": true, + "license": "Apache-2.0", + "dependencies": { + "@carbon/colors": "^11.46.0", + "@carbon/feature-flags": ">=0.32.0", + "@carbon/grid": "^11.49.0", + "@carbon/layout": "^11.47.0", + "@carbon/motion": "^11.40.0", + "@carbon/themes": "^11.67.0", + "@carbon/type": "^11.53.0", + "@ibm/plex": "6.0.0-next.6", + "@ibm/plex-mono": "1.1.0", + "@ibm/plex-sans": "1.1.0", + "@ibm/plex-sans-arabic": "1.1.0", + "@ibm/plex-sans-devanagari": "1.1.0", + "@ibm/plex-sans-hebrew": "1.1.0", + "@ibm/plex-sans-thai": "1.1.0", + "@ibm/plex-sans-thai-looped": "1.1.0", + "@ibm/plex-serif": "1.1.0", + "@ibm/telemetry-js": "^1.5.0" + }, + "peerDependencies": { + "sass": "^1.33.0" + }, + "peerDependenciesMeta": { + "sass": { + "optional": true + } + } + }, + "node_modules/@carbon/themes": { + "version": "11.67.0", + "resolved": "https://registry.npmjs.org/@carbon/themes/-/themes-11.67.0.tgz", + "integrity": "sha512-sCjmwxvM7nUdsDPef9g2v07Motvd4EYZJJqJyklMfhm9ZJ1oUfwecpW8rLzXylDsOBhrX9s1oCKWG/JqZF3kig==", + "hasInstallScript": true, + "license": "Apache-2.0", + "dependencies": { + "@carbon/colors": "^11.46.0", + "@carbon/layout": "^11.47.0", + "@carbon/type": "^11.53.0", + "@ibm/telemetry-js": "^1.5.0", + "color": "^4.0.0" + } + }, + "node_modules/@carbon/type": { + "version": "11.53.0", + "resolved": "https://registry.npmjs.org/@carbon/type/-/type-11.53.0.tgz", + "integrity": "sha512-x3GeJrkvM8wdpBwYbRr6jUsmR2wSRVbIxmPl7kamSFih32+czp7xpt/frG02EAY5xgaEk3N9YCNYspwco42raA==", + "hasInstallScript": true, + "license": "Apache-2.0", + "dependencies": { + "@carbon/grid": "^11.49.0", + "@carbon/layout": "^11.47.0", + "@ibm/telemetry-js": "^1.5.0" + } + }, + "node_modules/@carbon/utilities": { + "version": "0.15.0", + "resolved": "https://registry.npmjs.org/@carbon/utilities/-/utilities-0.15.0.tgz", + "integrity": "sha512-bwneNtLk8khoSIsilr6fBl115BMBrCMqxo/LjwAYiA+GiHes4URC4QYUihg+Ida5bCDpMixDx3RI9IW1UodXLQ==", + "hasInstallScript": true, + "license": "Apache-2.0", + "dependencies": { + "@ibm/telemetry-js": "^1.6.1", + "@internationalized/number": "^3.6.1" + } + }, "node_modules/@date-io/core": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/@date-io/core/-/core-3.2.0.tgz", @@ -633,6 +815,7 @@ "resolved": "https://registry.npmjs.org/@emotion/react/-/react-11.14.0.tgz", "integrity": "sha512-O000MLDBDdk/EohJPFUqvnp4qnHeYkVP5B0xEG0D/L7cOKP9kefu2DXn8dj74cQfsEzUqh+sr1RzFqiL1o+PpA==", "license": "MIT", + "peer": true, "dependencies": { "@babel/runtime": "^7.18.3", "@emotion/babel-plugin": "^11.13.5", @@ -676,6 +859,7 @@ "resolved": "https://registry.npmjs.org/@emotion/styled/-/styled-11.14.1.tgz", "integrity": "sha512-qEEJt42DuToa3gurlH4Qqc1kVpNq8wO8cJtDzU46TjlzWjDlsVyevtYCRijVq3SrHsROS+gVQ8Fnea108GnKzw==", "license": "MIT", + "peer": true, "dependencies": { "@babel/runtime": "^7.18.3", "@emotion/babel-plugin": "^11.13.5", @@ -721,6 +905,166 @@ "integrity": "sha512-snKqtPW01tN0ui7yu9rGv69aJXr/a/Ywvl11sUjNtEcRc+ng/mQriFL0wLXMef74iHa/EkftbDzU9F8iFbH+zg==", "license": "MIT" }, + "node_modules/@floating-ui/core": { + "version": "1.7.4", + "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.7.4.tgz", + "integrity": "sha512-C3HlIdsBxszvm5McXlB8PeOEWfBhcGBTZGkGlWc2U0KFY5IwG5OQEuQ8rq52DZmcHDlPLd+YFBK+cZcytwIFWg==", + "license": "MIT", + "dependencies": { + "@floating-ui/utils": "^0.2.10" + } + }, + "node_modules/@floating-ui/dom": { + "version": "1.7.5", + "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.7.5.tgz", + "integrity": "sha512-N0bD2kIPInNHUHehXhMke1rBGs1dwqvC9O9KYMyyjK7iXt7GAhnro7UlcuYcGdS/yYOlq0MAVgrow8IbWJwyqg==", + "license": "MIT", + "dependencies": { + "@floating-ui/core": "^1.7.4", + "@floating-ui/utils": "^0.2.10" + } + }, + "node_modules/@floating-ui/react": { + "version": "0.27.17", + "resolved": "https://registry.npmjs.org/@floating-ui/react/-/react-0.27.17.tgz", + "integrity": "sha512-LGVZKHwmWGg6MRHjLLgsfyaX2y2aCNgnD1zT/E6B+/h+vxg+nIJUqHPAlTzsHDyqdgEpJ1Np5kxWuFEErXzoGg==", + "license": "MIT", + "dependencies": { + "@floating-ui/react-dom": "^2.1.7", + "@floating-ui/utils": "^0.2.10", + "tabbable": "^6.0.0" + }, + "peerDependencies": { + "react": ">=17.0.0", + "react-dom": ">=17.0.0" + } + }, + "node_modules/@floating-ui/react-dom": { + "version": "2.1.7", + "resolved": "https://registry.npmjs.org/@floating-ui/react-dom/-/react-dom-2.1.7.tgz", + "integrity": "sha512-0tLRojf/1Go2JgEVm+3Frg9A3IW8bJgKgdO0BN5RkF//ufuz2joZM63Npau2ff3J6lUVYgDSNzNkR+aH3IVfjg==", + "license": "MIT", + "dependencies": { + "@floating-ui/dom": "^1.7.5" + }, + "peerDependencies": { + "react": ">=16.8.0", + "react-dom": ">=16.8.0" + } + }, + "node_modules/@floating-ui/utils": { + "version": "0.2.10", + "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.10.tgz", + "integrity": "sha512-aGTxbpbg8/b5JfU1HXSrbH3wXZuLPJcNEcZQFMxLs3oSzgtVu6nFPkbbGGUvBcUjKV2YyB9Wxxabo+HEH9tcRQ==", + "license": "MIT" + }, + "node_modules/@ibm/plex": { + "version": "6.0.0-next.6", + "resolved": "https://registry.npmjs.org/@ibm/plex/-/plex-6.0.0-next.6.tgz", + "integrity": "sha512-B3uGruTn2rS5gweynLmfSe7yCawSRsJguJJQHVQiqf4rh2RNgJFu8YLE2Zd/JHV0ZXoVMOslcXP2k3hMkxKEyA==", + "license": "OFL-1.1", + "engines": { + "node": ">=14" + } + }, + "node_modules/@ibm/plex-mono": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@ibm/plex-mono/-/plex-mono-1.1.0.tgz", + "integrity": "sha512-hpsdRxR3BRJkC6wGM4MZcUFD6C8M+mmK76RtAy/hlsfPro9FzpXVdIWC+G3jeQOXof109dxlUvmeKxpeKUG68A==", + "hasInstallScript": true, + "license": "OFL-1.1", + "dependencies": { + "@ibm/telemetry-js": "^1.6.1" + } + }, + "node_modules/@ibm/plex-sans": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@ibm/plex-sans/-/plex-sans-1.1.0.tgz", + "integrity": "sha512-WPgvO6Yfj2w5YbhyAr1tv95RUz4LRJlqN+CmYvBglabXteufP1D1E9BABMde+ZIKdRbFJDoKF5eQzfhpnbgZcQ==", + "hasInstallScript": true, + "license": "OFL-1.1", + "dependencies": { + "@ibm/telemetry-js": "^1.6.1" + } + }, + "node_modules/@ibm/plex-sans-arabic": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@ibm/plex-sans-arabic/-/plex-sans-arabic-1.1.0.tgz", + "integrity": "sha512-u8wIS6szLAOFvlBjCFZmtpKIqbhuIuniG2N0J+sio8vV6INH58hP0t0QNYrSl9SZtCv2Fwb4oQGuZJY3kJ4+QA==", + "hasInstallScript": true, + "license": "OFL-1.1", + "dependencies": { + "@ibm/telemetry-js": "^1.6.1" + } + }, + "node_modules/@ibm/plex-sans-devanagari": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@ibm/plex-sans-devanagari/-/plex-sans-devanagari-1.1.0.tgz", + "integrity": "sha512-IVNV9NxXZDzcGZRao/xj+kiFwkdLkcw5vNiKwY8wEzzkpjApXJnPhJ0a7mIKNAh8oIadTIF68+iGtzRKK3nXAQ==", + "hasInstallScript": true, + "license": "OFL-1.1", + "dependencies": { + "@ibm/telemetry-js": "^1.6.1" + } + }, + "node_modules/@ibm/plex-sans-hebrew": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@ibm/plex-sans-hebrew/-/plex-sans-hebrew-1.1.0.tgz", + "integrity": "sha512-iix0rLpUD0E8dE8q+/t3B7u1or7h6gEzoy6TK9NwP41AN31WE55f2cFwQAXomBDwr0Ozc9sHYy97NutEukZXzQ==", + "hasInstallScript": true, + "license": "OFL-1.1", + "dependencies": { + "@ibm/telemetry-js": "^1.6.1" + } + }, + "node_modules/@ibm/plex-sans-thai": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@ibm/plex-sans-thai/-/plex-sans-thai-1.1.0.tgz", + "integrity": "sha512-vk7IrjdO69eEElJpFBppCha/wvU48DFyVuDewcfIf5L6Z11s0vbROANCvKipVPRUz1LE4ron8KoitWGcl3AlfA==", + "hasInstallScript": true, + "license": "OFL-1.1", + "dependencies": { + "@ibm/telemetry-js": "^1.6.1" + } + }, + "node_modules/@ibm/plex-sans-thai-looped": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@ibm/plex-sans-thai-looped/-/plex-sans-thai-looped-1.1.0.tgz", + "integrity": "sha512-9zbDGzmtscHgBRTF88y3/92zQx6lmKjz5ZxhgcljilwOpj08BAytDc3mzUl9XGUh+DmOMl0Ql1lk6ecsEYYg2w==", + "hasInstallScript": true, + "license": "OFL-1.1", + "dependencies": { + "@ibm/telemetry-js": "^1.6.1" + } + }, + "node_modules/@ibm/plex-serif": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@ibm/plex-serif/-/plex-serif-1.1.0.tgz", + "integrity": "sha512-ORIyWlK8t8mvpFI7AAfhVFH+sacink2l9AjLiKZscmAzLVSa2dqFckrPOXqx4dK/cax567gWwCpJNEYk7xWxBQ==", + "hasInstallScript": true, + "license": "OFL-1.1", + "dependencies": { + "@ibm/telemetry-js": "^1.6.1" + } + }, + "node_modules/@ibm/telemetry-js": { + "version": "1.11.0", + "resolved": "https://registry.npmjs.org/@ibm/telemetry-js/-/telemetry-js-1.11.0.tgz", + "integrity": "sha512-RO/9j+URJnSfseWg9ZkEX9p+a3Ousd33DBU7rOafoZB08RqdzxFVYJ2/iM50dkBuD0o7WX7GYt1sLbNgCoE+pA==", + "license": "Apache-2.0", + "bin": { + "ibmtelemetry": "dist/collect.js" + } + }, + "node_modules/@internationalized/number": { + "version": "3.6.5", + "resolved": "https://registry.npmjs.org/@internationalized/number/-/number-3.6.5.tgz", + "integrity": "sha512-6hY4Kl4HPBvtfS62asS/R22JzNNy8vi/Ssev7x6EobfCp+9QIB2hKvI2EtbdJ0VSQacxVNtqhE/NmF/NZ0gm6g==", + "license": "Apache-2.0", + "dependencies": { + "@swc/helpers": "^0.5.0" + } + }, "node_modules/@jridgewell/gen-mapping": { "version": "0.3.13", "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", @@ -1008,6 +1352,7 @@ "resolved": "https://registry.npmjs.org/@mui/material/-/material-7.3.7.tgz", "integrity": "sha512-6bdIxqzeOtBAj2wAsfhWCYyMKPLkRO9u/2o5yexcL0C3APqyy91iGSWgT3H7hg+zR2XgE61+WAu12wXPON8b6A==", "license": "MIT", + "peer": true, "dependencies": { "@babel/runtime": "^7.28.4", "@mui/core-downloads-tracker": "^7.3.7", @@ -1118,6 +1463,7 @@ "resolved": "https://registry.npmjs.org/@mui/system/-/system-7.3.7.tgz", "integrity": "sha512-DovL3k+FBRKnhmatzUMyO5bKkhMLlQ9L7Qw5qHrre3m8zCZmE+31NDVBFfqrbrA7sq681qaEIHdkWD5nmiAjyQ==", "license": "MIT", + "peer": true, "dependencies": { "@babel/runtime": "^7.28.4", "@mui/private-theming": "^7.3.7", @@ -1423,6 +1769,7 @@ "integrity": "sha512-b9ll4jaFYfXSv6NZAOJ2P0uuyT/Doel7ho2AHLSUz2thtcL6HEb2+qdV2f9wriVvbEoPAj9VuSOgNc0t0f5iMw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@mischnic/json-sourcemap": "^0.1.1", "@parcel/cache": "2.16.3", @@ -2709,7 +3056,7 @@ "version": "2.5.1", "resolved": "https://registry.npmjs.org/@parcel/watcher/-/watcher-2.5.1.tgz", "integrity": "sha512-dfUnCxiN9H4ap84DvD2ubjw+3vUNpstxa0TneY/Paat8a3R4uQZDLSvWjmznAY/DoahqTHl9V46HF/Zs3F29pg==", - "dev": true, + "devOptional": true, "hasInstallScript": true, "license": "MIT", "dependencies": { @@ -3307,7 +3654,6 @@ "version": "0.5.17", "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.17.tgz", "integrity": "sha512-5IKx/Y13RsYd+sauPb2x+U/xZikHjolzfuDgTAl/Tdf3Q8rslRvC19NKDLgAJQ6wsqADk10ntlv08nPFw/gO/A==", - "dev": true, "license": "Apache-2.0", "dependencies": { "tslib": "^2.8.0" @@ -3558,7 +3904,7 @@ "version": "3.0.3", "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "fill-range": "^7.1.1" @@ -3587,6 +3933,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "baseline-browser-mapping": "^2.8.19", "caniuse-lite": "^1.0.30001751", @@ -3686,6 +4033,21 @@ "url": "https://github.com/chalk/chalk?sponsor=1" } }, + "node_modules/chokidar": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz", + "integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==", + "license": "MIT", + "dependencies": { + "readdirp": "^4.0.1" + }, + "engines": { + "node": ">= 14.16.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, "node_modules/chrome-trace-event": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/chrome-trace-event/-/chrome-trace-event-1.0.4.tgz", @@ -3696,6 +4058,12 @@ "node": ">=6.0" } }, + "node_modules/classnames": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/classnames/-/classnames-2.5.1.tgz", + "integrity": "sha512-saHYOzhIQs6wy2sVxTM6bUDsQO4F50V9RQ22qBpEdCW+I+/Wmke2HOl6lS6dTpdxVhb88/I6+Hs+438c3lfUow==", + "license": "MIT" + }, "node_modules/clone": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/clone/-/clone-2.1.2.tgz", @@ -3715,11 +4083,23 @@ "node": ">=6" } }, + "node_modules/color": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/color/-/color-4.2.3.tgz", + "integrity": "sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A==", + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1", + "color-string": "^1.9.0" + }, + "engines": { + "node": ">=12.5.0" + } + }, "node_modules/color-convert": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, "license": "MIT", "dependencies": { "color-name": "~1.1.4" @@ -3732,9 +4112,18 @@ "version": "1.1.4", "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true, "license": "MIT" }, + "node_modules/color-string": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/color-string/-/color-string-1.9.1.tgz", + "integrity": "sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg==", + "license": "MIT", + "dependencies": { + "color-name": "^1.0.0", + "simple-swizzle": "^0.2.2" + } + }, "node_modules/combined-stream": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", @@ -3757,6 +4146,12 @@ "node": ">=18" } }, + "node_modules/compute-scroll-into-view": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/compute-scroll-into-view/-/compute-scroll-into-view-3.1.1.tgz", + "integrity": "sha512-VRhuHOLoKYOy4UbilLbUzbYg93XLjv2PncJC50EuTWPA3gaja1UjBsUP/D/9/juV3vQFr6XBEzn9KCAHdUvOHw==", + "license": "MIT" + }, "node_modules/convert-source-map": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", @@ -3773,6 +4168,15 @@ "node": ">=18" } }, + "node_modules/copy-to-clipboard": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/copy-to-clipboard/-/copy-to-clipboard-3.3.3.tgz", + "integrity": "sha512-2KV8NhB5JqC3ky0r9PMCAZKbUHSwtEo4CwCs0KXgruG43gX5PMqDEBbVU4OUzw2MuAWUfsuFmWvEKG5QRfSnJA==", + "license": "MIT", + "dependencies": { + "toggle-selection": "^1.0.6" + } + }, "node_modules/core-js-compat": { "version": "3.46.0", "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.46.0.tgz", @@ -3935,6 +4339,7 @@ "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-4.1.0.tgz", "integrity": "sha512-Ukq0owbQXxa/U3EGtsdVBkR1w7KOQ5gIBqdH2hkvknzZPYvBxb/aa6E8L7tmjFtkwZBu3UXBbjIgPo/Ez4xaNg==", "license": "MIT", + "peer": true, "funding": { "type": "github", "url": "https://github.com/sponsors/kossnocorp" @@ -3976,7 +4381,7 @@ "version": "1.0.3", "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-1.0.3.tgz", "integrity": "sha512-pGjwhsmsp4kL2RTz08wcOlGN83otlqHeD/Z5T8GXZB+/YcpQ/dgo+lbU8ZsGxV0HIvqqxo9l7mqYwyYMD9bKDg==", - "dev": true, + "devOptional": true, "license": "Apache-2.0", "bin": { "detect-libc": "bin/detect-libc.js" @@ -4079,6 +4484,28 @@ "url": "https://dotenvx.com" } }, + "node_modules/downshift": { + "version": "9.0.10", + "resolved": "https://registry.npmjs.org/downshift/-/downshift-9.0.10.tgz", + "integrity": "sha512-TP/iqV6bBok6eGD5tZ8boM8Xt7/+DZvnVNr8cNIhbAm2oUBd79Tudiccs2hbcV9p7xAgS/ozE7Hxy3a9QqS6Mw==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.24.5", + "compute-scroll-into-view": "^3.1.0", + "prop-types": "^15.8.1", + "react-is": "18.2.0", + "tslib": "^2.6.2" + }, + "peerDependencies": { + "react": ">=16.12.0" + } + }, + "node_modules/downshift/node_modules/react-is": { + "version": "18.2.0", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.2.0.tgz", + "integrity": "sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==", + "license": "MIT" + }, "node_modules/dunder-proto": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", @@ -4208,7 +4635,7 @@ "version": "7.1.1", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "to-regex-range": "^5.0.1" @@ -4223,6 +4650,12 @@ "integrity": "sha512-NKfW6bec6GfKc0SGx1e07QZY9PE99u0Bft/0rzSD5k3sO/vwkVUpDUKVm5Gpp5Ue3YfShPFTX2070tDs5kB9Ng==", "license": "MIT" }, + "node_modules/flatpickr": { + "version": "4.6.13", + "resolved": "https://registry.npmjs.org/flatpickr/-/flatpickr-4.6.13.tgz", + "integrity": "sha512-97PMG/aywoYpB4IvbvUJi0RQi8vearvU0oov1WW3k0WZPBMrTQVqekSX5CjSG/M4Q3i6A/0FKXC7RyAoAUUSPw==", + "license": "MIT" + }, "node_modules/follow-redirects": { "version": "1.15.11", "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz", @@ -4481,6 +4914,12 @@ "url": "https://opencollective.com/immer" } }, + "node_modules/immutable": { + "version": "5.1.4", + "resolved": "https://registry.npmjs.org/immutable/-/immutable-5.1.4.tgz", + "integrity": "sha512-p6u1bG3YSnINT5RQmx/yRZBpenIl30kVxkTLDyHLIMk0gict704Q9n+thfDI7lTRm9vXdDYutVzXhzcThxTnXA==", + "license": "MIT" + }, "node_modules/import-fresh": { "version": "3.3.1", "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", @@ -4506,6 +4945,15 @@ "node": ">=12" } }, + "node_modules/invariant": { + "version": "2.2.4", + "resolved": "https://registry.npmjs.org/invariant/-/invariant-2.2.4.tgz", + "integrity": "sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.0.0" + } + }, "node_modules/is-arrayish": { "version": "0.2.1", "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", @@ -4531,7 +4979,7 @@ "version": "2.1.1", "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", - "dev": true, + "devOptional": true, "license": "MIT", "engines": { "node": ">=0.10.0" @@ -4541,7 +4989,7 @@ "version": "4.0.3", "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "is-extglob": "^2.1.1" @@ -4554,7 +5002,7 @@ "version": "7.0.0", "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", - "dev": true, + "devOptional": true, "license": "MIT", "engines": { "node": ">=0.12.0" @@ -5000,7 +5448,7 @@ "version": "4.0.8", "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "braces": "^3.0.3", @@ -5101,7 +5549,7 @@ "version": "7.1.1", "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-7.1.1.tgz", "integrity": "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==", - "dev": true, + "devOptional": true, "license": "MIT" }, "node_modules/node-gyp-build-optional-packages": { @@ -5248,7 +5696,7 @@ "version": "2.3.1", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", - "dev": true, + "devOptional": true, "license": "MIT", "engines": { "node": ">=8.6" @@ -5318,6 +5766,7 @@ "resolved": "https://registry.npmjs.org/react/-/react-19.2.3.tgz", "integrity": "sha512-Ku/hhYbVjOQnXDZFv2+RibmLFGwFdeeKHFcOTlrt7xplBnya5OGn/hIRDsqDiSUcfORsDC7MPxwork8jBwsIWA==", "license": "MIT", + "peer": true, "engines": { "node": ">=0.10.0" } @@ -5327,6 +5776,7 @@ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.3.tgz", "integrity": "sha512-yELu4WmLPw5Mr/lmeEpox5rw3RETacE++JgHqQzd2dg+YbJuat3jH4ingc+WPZhxaoFzdv9y33G+F7Nl5O0GBg==", "license": "MIT", + "peer": true, "dependencies": { "scheduler": "^0.27.0" }, @@ -5334,17 +5784,25 @@ "react": "^19.2.3" } }, + "node_modules/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==", + "license": "MIT" + }, "node_modules/react-is": { "version": "19.2.3", "resolved": "https://registry.npmjs.org/react-is/-/react-is-19.2.3.tgz", "integrity": "sha512-qJNJfu81ByyabuG7hPFEbXqNcWSU3+eVus+KJs+0ncpGfMyYdvSmxiJxbWR65lYi1I+/0HBcliO029gc4F+PnA==", - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/react-redux": { "version": "9.2.0", "resolved": "https://registry.npmjs.org/react-redux/-/react-redux-9.2.0.tgz", "integrity": "sha512-ROY9fvHhwOD9ySfrF0wmvu//bKCQ6AeZZq1nJNtbDC+kk5DuSuNX/n6YWYF/SYy7bSba4D4FSz8DJeKY/S/r+g==", "license": "MIT", + "peer": true, "dependencies": { "@types/use-sync-external-store": "^0.0.6", "use-sync-external-store": "^1.4.0" @@ -5411,6 +5869,19 @@ "react-dom": ">=16.6.0" } }, + "node_modules/readdirp": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz", + "integrity": "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==", + "license": "MIT", + "engines": { + "node": ">= 14.18.0" + }, + "funding": { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + }, "node_modules/recharts": { "version": "3.6.0", "resolved": "https://registry.npmjs.org/recharts/-/recharts-3.6.0.tgz", @@ -5445,7 +5916,8 @@ "version": "5.0.1", "resolved": "https://registry.npmjs.org/redux/-/redux-5.0.1.tgz", "integrity": "sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==", - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/redux-thunk": { "version": "3.1.0", @@ -5519,6 +5991,27 @@ ], "license": "MIT" }, + "node_modules/sass": { + "version": "1.97.3", + "resolved": "https://registry.npmjs.org/sass/-/sass-1.97.3.tgz", + "integrity": "sha512-fDz1zJpd5GycprAbu4Q2PV/RprsRtKC/0z82z0JLgdytmcq0+ujJbJ/09bPGDxCLkKY3Np5cRAOcWiVkLXJURg==", + "license": "MIT", + "peer": true, + "dependencies": { + "chokidar": "^4.0.0", + "immutable": "^5.0.2", + "source-map-js": ">=0.6.2 <2.0.0" + }, + "bin": { + "sass": "sass.js" + }, + "engines": { + "node": ">=14.0.0" + }, + "optionalDependencies": { + "@parcel/watcher": "^2.4.1" + } + }, "node_modules/scheduler": { "version": "0.27.0", "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz", @@ -5560,6 +6053,21 @@ "node": ">=11.0" } }, + "node_modules/simple-swizzle": { + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/simple-swizzle/-/simple-swizzle-0.2.4.tgz", + "integrity": "sha512-nAu1WFPQSMNr2Zn9PGSZK9AGn4t/y97lEm+MXTtUDwfP0ksAIX4nO+6ruD9Jwut4C49SB1Ws+fbXsm/yScWOHw==", + "license": "MIT", + "dependencies": { + "is-arrayish": "^0.3.1" + } + }, + "node_modules/simple-swizzle/node_modules/is-arrayish": { + "version": "0.3.4", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.3.4.tgz", + "integrity": "sha512-m6UrgzFVUYawGBh1dUsWR5M2Clqic9RVXC/9f8ceNlv2IcO9j9J/z8UoCLPqtsPBFNzEpfR3xftohbfqDx8EQA==", + "license": "MIT" + }, "node_modules/source-map": { "version": "0.5.7", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", @@ -5569,6 +6077,15 @@ "node": ">=0.10.0" } }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/stylis": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/stylis/-/stylis-4.2.0.tgz", @@ -5600,6 +6117,12 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/tabbable": { + "version": "6.4.0", + "resolved": "https://registry.npmjs.org/tabbable/-/tabbable-6.4.0.tgz", + "integrity": "sha512-05PUHKSNE8ou2dwIxTngl4EzcnsCDZGJ/iCLtDflR/SHB/ny14rXc+qU5P4mG9JkusiV7EivzY9Mhm55AzAvCg==", + "license": "MIT" + }, "node_modules/term-size": { "version": "2.2.1", "resolved": "https://registry.npmjs.org/term-size/-/term-size-2.2.1.tgz", @@ -5623,7 +6146,7 @@ "version": "5.0.1", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "is-number": "^7.0.0" @@ -5632,11 +6155,16 @@ "node": ">=8.0" } }, + "node_modules/toggle-selection": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/toggle-selection/-/toggle-selection-1.0.6.tgz", + "integrity": "sha512-BiZS+C1OS8g/q2RRbJmy59xpyghNBqrr6k5L/uKBGRsTfxmu3ffiRnd8mlGPUVayg8pvfi5urfnu8TU7DVOkLQ==", + "license": "MIT" + }, "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==", - "dev": true, "license": "0BSD" }, "node_modules/type-fest": { diff --git a/package.json b/package.json index c1856034..856c1c96 100644 --- a/package.json +++ b/package.json @@ -15,6 +15,8 @@ ], "dependencies": { "@babel/runtime": "^7.28.6", + "@carbon/react": "^1.100.0", + "@carbon/styles": "^1.99.0", "@date-io/core": "^3.2.0", "@date-io/date-fns": "^3.2.1", "@emotion/react": "^11.14.0", diff --git a/src/components/Nav/SidebarNav.js b/src/components/Nav/SidebarNav.js index 781184d7..03a2c113 100644 --- a/src/components/Nav/SidebarNav.js +++ b/src/components/Nav/SidebarNav.js @@ -2,7 +2,7 @@ import * as React from "react"; import { Drawer, List } from "@mui/material"; import { NavItem } from "./NavItem"; -import { BarChart, Cloud } from "@mui/icons-material"; +import { BarChart, Cloud, Storage } from "@mui/icons-material"; const logo = new URL("../../images/logo.png", import.meta.url).href; @@ -25,6 +25,7 @@ const SidebarNav = ({ active }) => { }, { name: "Cloud Costs", href: "/cloud", icon: }, { name: "External Costs", href: "/external-costs", icon: }, + { name: "Assets", href: "/assets", icon: }, ]; return ( diff --git a/src/components/Page.js b/src/components/Page.js index 769dcc67..699f35aa 100644 --- a/src/components/Page.js +++ b/src/components/Page.js @@ -24,11 +24,9 @@ const Page = (props) => {
diff --git a/src/components/assets/AssetsCostBreakdown.js b/src/components/assets/AssetsCostBreakdown.js new file mode 100644 index 00000000..248f7bbe --- /dev/null +++ b/src/components/assets/AssetsCostBreakdown.js @@ -0,0 +1,116 @@ +import React from "react"; +import { PieChart, Pie, Cell, ResponsiveContainer, Tooltip } from "recharts"; +import { toCurrency } from "../../util"; + +const ASSET_TYPE_COLORS = { + Node: "#0043ce", + Disk: "#8a3ffc", + Network: "#00539a", + LoadBalancer: "#009d9a", + ClusterManagement: "#da1e28", + Cluster: "#ff832b", + Cloud: "#be95ff", + default: "#6b7280", +}; + +const AssetsCostBreakdown = ({ costByType, totalCost, currency }) => { + if (!costByType || Object.keys(costByType).length === 0) { + return ( +
+

+ No cost breakdown data available. +

+
+ ); + } + + const chartData = Object.entries(costByType) + .map(([type, cost]) => ({ + name: type, + value: cost, + percentage: totalCost > 0 ? ((cost / totalCost) * 100).toFixed(1) : 0, + })) + .sort((a, b) => b.value - a.value); + + const getColor = (type) => + ASSET_TYPE_COLORS[type] || ASSET_TYPE_COLORS.default; + + const CustomTooltip = ({ active, payload }) => { + if (active && payload && payload.length) { + const data = payload[0].payload; + return ( +
+

{data.name}

+

+ {toCurrency(data.value, currency)} +

+

{data.percentage}%

+
+ ); + } + return null; + }; + + return ( +
+
+ {chartData.map(({ name, value, percentage }) => { + const tagClass = `assets-type-tag assets-type-tag--${name.toLowerCase()}`; + return ( +
+ {name} — {toCurrency(value, currency)} ({percentage}%) +
+ ); + })} +
+
+
+
+ Total Cost +
+
+ {toCurrency(totalCost, currency)} +
+
+ + + + {chartData.map((entry) => ( + + ))} + + } /> + + +
+
+ ); +}; + +export default AssetsCostBreakdown; diff --git a/src/components/assets/AssetsDataTable.js b/src/components/assets/AssetsDataTable.js new file mode 100644 index 00000000..c0ea4313 --- /dev/null +++ b/src/components/assets/AssetsDataTable.js @@ -0,0 +1,118 @@ +import React, { useState, useMemo } from "react"; +import { + DataTable, + Table, + TableHead, + TableRow, + TableHeader, + TableBody, + TableCell, + TableContainer, + TableToolbar, + TableToolbarContent, + TableToolbarSearch, +} from "@carbon/react"; +import { toCurrency, bytesToString } from "../../util"; + +const headers = [ + { key: "name", header: "Name" }, + { key: "type", header: "Type" }, + { key: "provider", header: "Provider" }, + { key: "cluster", header: "Cluster" }, + { key: "cpuCores", header: "CPU Cores" }, + { key: "ramBytes", header: "RAM" }, + { key: "totalCost", header: "Total Cost" }, +]; + +const AssetsDataTable = ({ assets, currency }) => { + const [searchTerm, setSearchTerm] = useState(""); + + const safeAssets = Array.isArray(assets) ? assets : []; + + const sortedAssets = useMemo(() => { + return [...safeAssets].sort( + (a, b) => (b.totalCost || 0) - (a.totalCost || 0), + ); + }, [safeAssets]); + + const filteredAssets = useMemo(() => { + if (!searchTerm) return sortedAssets; + const lowerSearch = searchTerm.toLowerCase(); + return sortedAssets.filter( + (asset) => + (asset.name || "").toLowerCase().includes(lowerSearch) || + (asset.type || "").toLowerCase().includes(lowerSearch) || + (asset.provider || "").toLowerCase().includes(lowerSearch) || + (asset.cluster || "").toLowerCase().includes(lowerSearch), + ); + }, [sortedAssets, searchTerm]); + + const rows = filteredAssets.map((asset, index) => ({ + id: asset.key || `${asset.name || "asset"}-${index}`, + name: asset.name || "-", + type: asset.type || "-", + provider: asset.provider || "-", + cluster: asset.cluster || "-", + cpuCores: asset.cpuCores != null ? asset.cpuCores.toFixed(2) : "-", + ramBytes: asset.ramBytes != null ? bytesToString(asset.ramBytes) : "-", + totalCost: toCurrency(asset.totalCost || 0, currency), + rawCost: asset.totalCost || 0, + })); + + return ( +
+ + {({ + rows, + headers, + getHeaderProps, + getRowProps, + getTableProps, + getTableContainerProps, + getToolbarProps, + }) => ( + + + + setSearchTerm(e.target.value)} + /> + + + + + + {headers.map((header) => ( + + {header.header} + + ))} + + + + {rows.map((row) => ( + + {row.cells.map((cell) => ( + {cell.value} + ))} + + ))} + +
+
+ )} +
+
+ ); +}; + +export default AssetsDataTable; diff --git a/src/components/assets/AssetsFilters.js b/src/components/assets/AssetsFilters.js new file mode 100644 index 00000000..8d54a948 --- /dev/null +++ b/src/components/assets/AssetsFilters.js @@ -0,0 +1,24 @@ +import React from "react"; +import AssetsSelectWindow from "./AssetsSelectWindow"; +import AssetsSelectType from "./AssetsSelectType"; + +const AssetsFilters = ({ + window, + setWindow, + assetType, + setAssetType, + assetTypeOptions, +}) => { + return ( +
+ + +
+ ); +}; + +export default AssetsFilters; diff --git a/src/components/assets/AssetsSelectType.js b/src/components/assets/AssetsSelectType.js new file mode 100644 index 00000000..449c6570 --- /dev/null +++ b/src/components/assets/AssetsSelectType.js @@ -0,0 +1,121 @@ +import React, { useState } from "react"; +import TextField from "@mui/material/TextField"; +import Popover from "@mui/material/Popover"; + +const AssetsSelectType = ({ assetType, setAssetType, assetTypeOptions }) => { + const [anchorEl, setAnchorEl] = useState(null); + + const typeOptions = [ + { id: "all", text: "All Types" }, + ...assetTypeOptions.map((type) => ({ id: type, text: type })), + ]; + + const handleClick = (event) => { + setAnchorEl(event.currentTarget); + }; + + const handleClose = () => { + setAnchorEl(null); + }; + + const handleSelect = (id) => { + setAssetType(id); + handleClose(); + }; + + const open = Boolean(anchorEl); + const selectedText = + typeOptions.find((opt) => opt.id === assetType)?.text || "All Types"; + + return ( + <> + + +
+ {typeOptions.map((opt) => ( + handleSelect(opt.id)} + style={{ + color: assetType === opt.id ? "#0f62fe" : "#525252", + fontWeight: assetType === opt.id ? 600 : 400, + fontSize: 14, + cursor: "pointer", + transition: "color 0.15s ease", + }} + onMouseEnter={(e) => { + e.target.style.color = "#0f62fe"; + e.target.style.textDecoration = "underline"; + }} + onMouseLeave={(e) => { + e.target.style.color = + assetType === opt.id ? "#0f62fe" : "#525252"; + e.target.style.textDecoration = "none"; + }} + > + {opt.text} + + ))} +
+
+ + ); +}; + +export default AssetsSelectType; diff --git a/src/components/assets/AssetsSelectWindow.js b/src/components/assets/AssetsSelectWindow.js new file mode 100644 index 00000000..c3914db1 --- /dev/null +++ b/src/components/assets/AssetsSelectWindow.js @@ -0,0 +1,244 @@ +import React, { useEffect, useState } from "react"; +import { endOfDay, startOfDay, isValid } from "date-fns"; +import { DatePicker } from "@mui/x-date-pickers/DatePicker"; +import Button from "@mui/material/Button"; +import Popover from "@mui/material/Popover"; +import TextField from "@mui/material/TextField"; +import { find, get } from "lodash"; + +const windowOptions = [ + { name: "Today", value: "today" }, + { name: "Yesterday", value: "yesterday" }, + { name: "Last 24h", value: "24h" }, + { name: "Last 48h", value: "48h" }, + { name: "Week-to-date", value: "week" }, + { name: "Last week", value: "lastweek" }, + { name: "Last 7 days", value: "7d" }, + { name: "Last 14 days", value: "14d" }, +]; + +const AssetsSelectWindow = ({ window, setWindow }) => { + const [anchorEl, setAnchorEl] = useState(null); + const [startDate, setStartDate] = useState(null); + const [endDate, setEndDate] = useState(null); + const [intervalString, setIntervalString] = useState(null); + + const handleClick = (event) => { + setAnchorEl(event.currentTarget); + }; + + const handleClose = () => { + setAnchorEl(null); + }; + + const handleStartDateChange = (date) => { + if (isValid(date)) { + setStartDate(startOfDay(date)); + } + }; + + const handleEndDateChange = (date) => { + if (isValid(date)) { + setEndDate(endOfDay(date)); + } + }; + + const handlePresetClick = (value) => { + setWindow(value); + setStartDate(null); + setEndDate(null); + handleClose(); + }; + + const handleApplyCustom = () => { + if (intervalString !== null) { + setWindow(intervalString); + handleClose(); + } + }; + + useEffect(() => { + if (startDate !== null && endDate !== null) { + let adjustedStartDate = new Date( + startDate - startDate.getTimezoneOffset() * 60000, + ); + let adjustedEndDate = new Date( + endDate - endDate.getTimezoneOffset() * 60000, + ); + setIntervalString( + adjustedStartDate.toISOString().split(".")[0] + + "Z" + + "," + + adjustedEndDate.toISOString().split(".")[0] + + "Z", + ); + } + }, [startDate, endDate]); + + const open = Boolean(anchorEl); + const selectedName = get( + find(windowOptions, { value: window }), + "name", + "Custom", + ); + + return ( + <> + + +
+
+ + + +
+ + {/* Presets Section */} +
+ {windowOptions.map((opt) => ( + handlePresetClick(opt.value)} + style={{ + color: window === opt.value ? "#0f62fe" : "#525252", + fontWeight: window === opt.value ? 600 : 400, + fontSize: 14, + cursor: "pointer", + transition: "color 0.15s ease", + }} + onMouseEnter={(e) => { + e.target.style.color = "#0f62fe"; + e.target.style.textDecoration = "underline"; + }} + onMouseLeave={(e) => { + e.target.style.color = + window === opt.value ? "#0f62fe" : "#525252"; + e.target.style.textDecoration = "none"; + }} + > + {opt.name} + + ))} +
+
+
+ + ); +}; + +export default AssetsSelectWindow; diff --git a/src/components/assets/AssetsSkeleton.js b/src/components/assets/AssetsSkeleton.js new file mode 100644 index 00000000..1de50b20 --- /dev/null +++ b/src/components/assets/AssetsSkeleton.js @@ -0,0 +1,78 @@ +import React from "react"; +import { DataTableSkeleton } from "@carbon/react"; + +const AssetsSkeleton = () => { + return ( +
+
+ {[1, 2, 3, 4].map((i) => ( +
+ ))} +
+ +
+
+
+ +
+ +
+ +
+ ); +}; + +export default AssetsSkeleton; diff --git a/src/components/assets/AssetsSummaryTiles.js b/src/components/assets/AssetsSummaryTiles.js new file mode 100644 index 00000000..df874880 --- /dev/null +++ b/src/components/assets/AssetsSummaryTiles.js @@ -0,0 +1,55 @@ +import { Tile } from "@carbon/react"; +import { toCurrency } from "../../util"; + +const AssetsSummaryTiles = ({ + assets, + totalCost, + assetTypes, + avgCost, + currency, +}) => { + return ( +
+
+ +
Total Assets
+
{assets}
+
+
+
+ +
Total Cost
+
+ {toCurrency(totalCost, currency)} +
+
+
+
+ +
Asset Types
+
{assetTypes}
+
+
+
+ +
+ Average Cost Per Asset +
+
+ {toCurrency(avgCost, currency)} +
+
+
+
+ ); +}; + +export default AssetsSummaryTiles; diff --git a/src/components/assets/assets.css b/src/components/assets/assets.css new file mode 100644 index 00000000..2768c377 --- /dev/null +++ b/src/components/assets/assets.css @@ -0,0 +1,289 @@ +/* Assets Page Styles */ +.assets-page-zoom { + zoom: 95%; +} + +.assets-summary-grid { + margin-bottom: 32px; +} + +.assets-summary-tile { + height: 100px; + display: flex; + flex-direction: column; + justify-content: center; + padding: 16px 20px 16px 24px; + border-radius: 8px; + background: #ffffff !important; + border: 1px solid #e0e0e0; + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.04); + position: relative; + overflow: hidden; + transition: all 0.2s ease; +} + +.assets-summary-tile::before { + content: ""; + position: absolute; + left: 0; + top: 0; + bottom: 0; + width: 6px; +} + +.assets-summary-tile:hover { + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08); + transform: translateY(-1px); +} + +.assets-summary-tile--primary::before { + background-color: #0043ce; /* Brighter Blue */ +} + +.assets-summary-tile--cost::before { + background-color: #24a148; /* Brighter Green */ +} + +.assets-summary-tile--types::before { + background-color: #a020f0; /* Brighter Purple */ +} + +.assets-summary-tile--avg::before { + background-color: #0072c3; /* Bright Blue */ +} + +.assets-summary-tile__label { + font-size: 11px; + font-weight: 500; + margin-bottom: 8px; + color: #525252; + text-transform: uppercase; + letter-spacing: 0.5px; +} + +.assets-summary-tile__value { + font-size: 2.5rem; + font-weight: 700; + line-height: 1; + color: #161616; +} + +/* Cost Section - White Card Container */ +.assets-cost-section { + margin-top: 32px; + padding: 24px; + background: #ffffff; + border-radius: 8px; + border: 1px solid #e0e0e0; + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.04); +} + +/* Cost Breakdown - Two Column Layout */ +.assets-cost-breakdown { + display: grid; + grid-template-columns: 30% 70%; + gap: 24px; /* Reduced gap slightly to fit better if needed, or keep standard */ + align-items: center; +} + +.assets-cost-breakdown__tags { + display: flex; + flex-direction: column; + gap: 8px; +} + +.assets-cost-breakdown__chart { + width: 100%; + height: 280px; + display: flex; + justify-content: center; + align-items: center; + position: relative; +} + +.assets-type-tag { + display: inline-block; + padding: 6px 16px; + border-radius: 16px; + font-size: 14px; + font-weight: 500; +} + +.assets-type-tag--node { + background: rgba(15, 98, 254, 0.1); + color: #0043ce; +} + +.assets-type-tag--clustermanagement { + background: rgba(159, 24, 83, 0.1); + color: #da1e28; +} +.assets-type-tag--disk { + background: rgba(138, 63, 252, 0.1); + color: #8a3ffc; +} + +.assets-type-tag--loadbalancer { + background: rgba(0, 93, 93, 0.1); + color: #009d9a; +} +.assets-type-tag--network { + background: rgba(0, 114, 195, 0.1); + color: #00539a; +} + +.assets-type-tag--cluster { + background: rgba(250, 77, 86, 0.1); + color: #ff832b; +} +.assets-type-tag--cloud { + background: rgba(105, 41, 196, 0.1); + color: #be95ff; +} + +.assets-type-tag--default { + background: rgba(107, 114, 128, 0.1); + color: #6b7280; +} +.assets-chart-tooltip { + background: #ffffff; + border: 1px solid #e5e7eb; + border-radius: 8px; + padding: 12px 16px; + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1); + color: #161616; +} + +.assets-chart-tooltip__label { + font-weight: 600; + margin: 0 0 4px 0; + color: #374151; +} + +.assets-chart-tooltip__value { + font-size: 1.125rem; + margin: 0 0 2px 0; + color: #059669; + font-weight: 600; +} + +.assets-chart-tooltip__percent { + font-size: 0.8125rem; + color: #6b7280; + margin: 0; +} + +.assets-filters { + display: flex; + gap: 16px; + flex-wrap: wrap; + align-items: flex-end; + margin-bottom: 16px; +} + +.assets-filters .cds--dropdown { + min-width: 180px; + background-color: #ffffff !important; +} + +.assets-filters .cds--dropdown .cds--list-box__label, +.assets-filters .cds--list-box__label { + color: #161616 !important; +} + +.assets-filters .cds--dropdown .cds--list-box__field, +.assets-filters .cds--list-box__field { + background-color: #ffffff !important; + border: 1px solid #8d8d8d !important; +} + +.assets-filters .cds--dropdown .cds--list-box__menu, +.assets-filters .cds--list-box__menu { + background-color: #ffffff !important; + z-index: 9999 !important; +} + +.assets-filters .cds--dropdown .cds--list-box__menu-item, +.assets-filters .cds--list-box__menu-item { + color: #161616 !important; + background-color: #ffffff !important; +} + +.assets-filters .cds--dropdown .cds--list-box__menu-item:hover, +.assets-filters .cds--list-box__menu-item:hover { + background-color: #e8e8e8 !important; +} + +.assets-filters .cds--dropdown .cds--list-box__menu-item--active, +.assets-filters .cds--list-box__menu-item--active { + background-color: #e0e0e0 !important; +} + +.assets-filters .cds--dropdown .cds--list-box__menu-item__option, +.assets-filters .cds--list-box__menu-item__option { + color: #161616 !important; +} + +.assets-filters .cds--form__label, +.assets-filters .cds--label { + color: #161616 !important; +} + +.assets-data-table { + margin-top: 32px; +} + +.assets-data-table .cds--data-table-container { + padding-top: 0; +} + +.assets-data-table .cds--data-table thead th { + background-color: #393939; + color: #ffffff; +} + +.assets-data-table .cds--data-table tbody tr:hover { + background-color: #e8e8e8; +} + +.assets-data-table .cds--table-toolbar { + background: #f4f4f4; +} + +@media (max-width: 768px) { + .assets-cost-breakdown { + grid-template-columns: 1fr; + gap: 24px; + } + + .assets-cost-breakdown__chart { + max-width: 100%; + } +} + +/* Custom minimal tooltip for refresh button */ +.assets-refresh-btn { + position: relative !important; +} + +.assets-refresh-btn::after { + content: "Refresh"; + visibility: hidden; + opacity: 0; + position: absolute; + top: 100%; + right: 0; + background-color: transparent; + color: #161616; + font-weight: 600; + font-size: 12px; + padding: 4px 6px 0 0; + white-space: nowrap; + pointer-events: none; + transition: opacity 0.2s; +} + +.assets-refresh-btn:hover::after { + visibility: visible; + opacity: 1; +} diff --git a/src/components/assets/index.js b/src/components/assets/index.js new file mode 100644 index 00000000..4271815b --- /dev/null +++ b/src/components/assets/index.js @@ -0,0 +1,5 @@ +export { default as AssetsSummaryTiles } from "./AssetsSummaryTiles"; +export { default as AssetsCostBreakdown } from "./AssetsCostBreakdown"; +export { default as AssetsFilters } from "./AssetsFilters"; +export { default as AssetsDataTable } from "./AssetsDataTable"; +export { default as AssetsSkeleton } from "./AssetsSkeleton"; diff --git a/src/css/index.css b/src/css/index.css index 8ace1e31..bb85ece1 100644 --- a/src/css/index.css +++ b/src/css/index.css @@ -1,10 +1,25 @@ @import "../../node_modules/material-design-icons-iconfont/dist/material-design-icons.css"; +@import "../../node_modules/@carbon/styles/css/styles.css"; + +*, +*::before, +*::after { + font-family: + "Inter", + -apple-system, + BlinkMacSystemFont, + sans-serif !important; +} body { background-color: #f3f3f3; display: flex; flex-flow: column; - font-family: "Roboto", sans-serif; + font-family: + "Inter", + -apple-system, + BlinkMacSystemFont, + sans-serif; margin: 0px; overflow-y: scroll; } diff --git a/src/index.html b/src/index.html index 43e4459b..0e949b56 100644 --- a/src/index.html +++ b/src/index.html @@ -3,6 +3,12 @@ + + + diff --git a/src/pages/Assets.js b/src/pages/Assets.js new file mode 100644 index 00000000..26338cef --- /dev/null +++ b/src/pages/Assets.js @@ -0,0 +1,236 @@ +import React, { useEffect, useState, useMemo } from "react"; +import { useLocation, useNavigate } from "react-router"; +import IconButton from "@mui/material/IconButton"; +import { Renew } from "@carbon/icons-react"; +import Paper from "@mui/material/Paper"; +import Typography from "@mui/material/Typography"; + +import Page from "../components/Page"; +import Header from "../components/Header"; +import Footer from "../components/Footer"; +import Warnings from "../components/Warnings"; +import AssetsService from "../services/assets"; + +import { + AssetsSummaryTiles, + AssetsCostBreakdown, + AssetsFilters, + AssetsDataTable, + AssetsSkeleton, +} from "../components/assets"; + +import "../components/assets/assets.css"; + +const Assets = () => { + const [loading, setLoading] = useState(true); + const [errors, setErrors] = useState([]); + const [rawAssets, setRawAssets] = useState({}); + const [assetTypeOptions, setAssetTypeOptions] = useState([]); + + const routerLocation = useLocation(); + const searchParams = new URLSearchParams(routerLocation.search); + const navigate = useNavigate(); + + const window = searchParams.get("window") || "7d"; + const assetType = searchParams.get("type") || "all"; + const currency = searchParams.get("currency") || "USD"; + + const setWindow = (win) => { + searchParams.set("window", win); + navigate({ search: `?${searchParams.toString()}` }); + }; + + const setAssetType = (type) => { + searchParams.set("type", type); + navigate({ search: `?${searchParams.toString()}` }); + }; + + async function fetchData() { + setLoading(true); + setErrors([]); + + try { + const resp = await AssetsService.fetchAssets(window); + + if (resp && resp.data) { + const assetsData = resp.data; + setRawAssets(assetsData); + + const types = new Set(); + Object.values(assetsData).forEach((asset) => { + if (asset.type) { + types.add(asset.type); + } + }); + setAssetTypeOptions(Array.from(types).sort()); + } else { + setRawAssets({}); + setAssetTypeOptions([]); + } + } catch (err) { + if (err.message && err.message.indexOf("404") === 0) { + setErrors([ + { + primary: "Assets API not available", + secondary: + "Please ensure your OpenCost instance supports the Assets API.", + }, + ]); + } else { + setErrors([ + { + primary: "Failed to load assets data", + secondary: err.message || "Please try again later.", + }, + ]); + } + setRawAssets({}); + } + + setLoading(false); + } + + useEffect(() => { + fetchData(); + }, [window]); + + const processedAssets = useMemo(() => { + const assets = []; + + Object.entries(rawAssets).forEach(([key, asset]) => { + if (!asset || !asset.properties) return; + + const props = asset.properties; + assets.push({ + key, + name: props.name || key, + type: asset.type || "Unknown", + provider: props.provider || "-", + cluster: props.cluster || "-", + category: props.category || "-", + cpuCores: asset.cpuCores || null, + ramBytes: asset.ramBytes || null, + totalCost: asset.totalCost || 0, + }); + }); + + if (assetType !== "all") { + return assets.filter((a) => a.type === assetType); + } + return assets; + }, [rawAssets, assetType]); + + const summaryMetrics = useMemo(() => { + const totalAssets = processedAssets.length; + const totalCost = processedAssets.reduce((sum, a) => sum + a.totalCost, 0); + const types = new Set(processedAssets.map((a) => a.type)); + const assetTypesCount = types.size; + const avgCost = totalAssets > 0 ? totalCost / totalAssets : 0; + + return { totalAssets, totalCost, assetTypesCount, avgCost }; + }, [processedAssets]); + + const costByType = useMemo(() => { + const costs = {}; + processedAssets.forEach((asset) => { + costs[asset.type] = (costs[asset.type] || 0) + asset.totalCost; + }); + return costs; + }, [processedAssets]); + + return ( + +
+ fetchData()} + style={{ padding: 12 }} + > + + +
+ + {!loading && errors.length > 0 && ( +
+ +
+ )} + + +
+
+ Infrastructure Assets + + {window === "7d" + ? "Last 7 days" + : window === "14d" + ? "Last 14 days" + : window === "30d" + ? "Last 30 days" + : window} + +
+ +
+ + {loading && } + + {!loading && processedAssets.length === 0 && errors.length === 0 && ( +
+ + No assets found + + + No infrastructure assets were found for the selected time window. + +
+ )} + + {!loading && processedAssets.length > 0 && ( + <> + + +
+ + Cost by asset type + + +
+ +
+ +
+ + )} +
+ +