diff --git a/.eslintrc.json b/.eslintrc.json deleted file mode 100644 index 920d7fdc..00000000 --- a/.eslintrc.json +++ /dev/null @@ -1,69 +0,0 @@ -{ - "env": { - "browser": true, - "es2024": true, - "jest": true, - "node": true - }, - "extends": [ - "eslint:recommended", - "next", - "next/core-web-vitals", - "plugin:@typescript-eslint/eslint-recommended", - "plugin:@typescript-eslint/recommended", - "plugin:prettier/recommended" - ], - "parser": "@typescript-eslint/parser", - "parserOptions": { - "ecmaFeatures": { - "jsx": true - }, - "ecmaVersion": 2024, - "project": ["src/ui/tsconfig.json", "tsconfig.test.json", "tsconfig.cypress.json"], - "sourceType": "module" - }, - "plugins": ["@typescript-eslint", "import", "jest", "no-secrets", "react"], - "root": true, - "rules": { - "complexity": ["warn", { "max": 8 }], - "curly": ["error", "all"], - "import/order": [ - "error", - { - "groups": [ - ["builtin", "external"], - ["internal", "parent", "sibling", "index"] - ], - "newlines-between": "always-and-inside-groups", - "pathGroups": [ - { - "pattern": "@{app,assets,classes,components,hooks,lib,pages,store,tests,types,utils}/**", - "group": "internal", - "position": "before" - }, - { - "pattern": "{.,..}/**", - "group": "internal", - "position": "after" - } - ], - "pathGroupsExcludedImportTypes": ["builtin"], - "alphabetize": { "order": "asc", "caseInsensitive": true } - } - ], - "import/no-extraneous-dependencies": ["error"], - "no-secrets/no-secrets": ["error", { "additionalRegexes": {}, "ignoreContent": [] }] - }, - "settings": { - "next": { "rootDir": ["src/ui/", "tests/ui/"] }, - "import/resolver": { - "typescript": { - "project": [ - "src/ui/tsconfig.json", - "tsconfig.test.json", - "tsconfig.cypress.json" - ] - } - } - } -} diff --git a/.gitignore b/.gitignore index ede4e03d..ebbf9b09 100644 --- a/.gitignore +++ b/.gitignore @@ -223,8 +223,8 @@ src/ui/next-env.d.ts # Root-level UI config files that should be tracked !package.json !package-lock.json -!.eslintrc.json !tsconfig.json !tsconfig.*.json !src/ui/lib !src/ui/public/manifest.json +.eslintcache diff --git a/.husky/pre-commit b/.husky/pre-commit new file mode 100755 index 00000000..2312dc58 --- /dev/null +++ b/.husky/pre-commit @@ -0,0 +1 @@ +npx lint-staged diff --git a/.prettierignore b/.prettierignore index cae160a1..5d4291c5 100644 --- a/.prettierignore +++ b/.prettierignore @@ -12,7 +12,7 @@ !tests/ui/**/*.json # Root-level configs to format -!/.eslintrc.json +!/.eslint.config.js !/tsconfig*.json !/*.config.{js,ts} !/jest.setup.ts diff --git a/DEVELOPING.md b/DEVELOPING.md index 1a3cc921..dde51744 100644 --- a/DEVELOPING.md +++ b/DEVELOPING.md @@ -185,7 +185,13 @@ The GuideLLM project includes a frontend UI located in `src/ui`, built using [Ne ### Getting Started -To start the local development server: +First, install dependencies: + +```bash +npm install +``` + +Then, start the local development server: ```bash npm run dev @@ -215,6 +221,12 @@ npm run build npm run test:integration ``` +- **Integration+Unit tests with coverage**: + + ```bash + npm run coverage + ``` + - **End-to-end tests** (using Cypress, ensure live dev server): ```bash @@ -241,6 +253,20 @@ npm run build npm run type-checks ``` +##### Tagging Tests + +Reference [https://www.npmjs.com/package/jest-runner-groups](jest-runner-groups) Add @group with the tag in a docblock at the top of the test file to indicate which types of tests are contained within. Can't distinguish between different types of tests in the same file. + +``` +/** + * Admin dashboard tests + * + * @group smoke + * @group sanity + * @group regression + */ +``` + ## Additional Resources - [CONTRIBUTING.md](https://github.com/neuralmagic/guidellm/blob/main/CONTRIBUTING.md): Guidelines for contributing to the project. diff --git a/README.md b/README.md index e31c0e44..fb70f072 100644 --- a/README.md +++ b/README.md @@ -153,6 +153,48 @@ The `guidellm benchmark` command is used to run benchmarks against a generative - `--output-path`: Defines the path to save the benchmark results. Supports JSON, YAML, or CSV formats. If a directory is provided, the results will be saved as `benchmarks.json` in that directory. If not set, the results will be saved in the current working directory. +### GuideLLM UI + +GuideLLM UI is a companion frontend for visualizing the results of a GuideLLM benchmark run. + +### 🛠Running the UI + +1. Use the Hosted Build (Recommended for Most Users) + +After running a benchmark with GuideLLM, a report.html file will be generated (by default at guidellm_report/report.html). This file references the latest stable version of the UI hosted at: + +``` +https://neuralmagic.github.io/guidellm/ui/dev/ +``` + +Open the file in your browser and you're done—no setup required. + +2. Build and Serve the UI Locally (For Development) This option is useful if: + +- You are actively developing the UI + +- You want to test changes to the UI before publishing + +- You want full control over how the report is displayed + +```bash +npm install +npm run build +npx serve out +``` + +This will start a local server (e.g., at http://localhost:3000). Then, in your GuideLLM config or CLI flags, point to this local server as the asset base for report generation. + +### 🧪 Development Notes + +During UI development, it can be helpful to view sample data. We include a sample benchmark run wired into the Redux store under: + +``` +src/lib/store/[runInfo/workloadDetails/benchmarks]WindowData.ts +``` + +In the future this will be replaced by a configurable untracked file for dev use. + ## Resources ### Documentation diff --git a/cypress.config.ts b/cypress.config.ts index 846b2111..e7bdffc8 100644 --- a/cypress.config.ts +++ b/cypress.config.ts @@ -4,6 +4,6 @@ export default defineConfig({ e2e: { specPattern: 'tests/ui/cypress/e2e/**/*.cy.{js,jsx,ts,tsx}', supportFile: 'tests/ui/cypress/support/e2e.ts', - baseUrl: 'http://localhost:3000', // optional, but good practice + baseUrl: 'http://localhost:3000', // optional }, }); diff --git a/eslint.config.js b/eslint.config.js new file mode 100644 index 00000000..178aaccb --- /dev/null +++ b/eslint.config.js @@ -0,0 +1,181 @@ +// @ts-check + +import eslint from '@eslint/js'; +import typescriptPlugin from '@typescript-eslint/eslint-plugin'; +import typescriptParser from '@typescript-eslint/parser'; +import { FlatCompat } from '@eslint/eslintrc'; +import reactPlugin from 'eslint-plugin-react'; +import hooksPlugin from 'eslint-plugin-react-hooks'; +import importPlugin from 'eslint-plugin-import'; +import jestPlugin from 'eslint-plugin-jest'; +import noSecretsPlugin from 'eslint-plugin-no-secrets'; +import prettierPlugin from 'eslint-plugin-prettier'; +import prettierConfig from 'eslint-config-prettier'; +import globals from 'globals'; +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename); + +const compat = new FlatCompat({ + baseDirectory: __dirname, + recommendedConfig: eslint.configs.recommended, +}); + +export default [ + // Base configuration + eslint.configs.recommended, + + // Next.js configuration using FlatCompat + ...compat.extends('next/core-web-vitals'), + + // --- Main Configuration --- + { + files: ['src/**/*.{js,jsx,ts,tsx}', 'tests/**/*.{js,jsx,ts,tsx}'], + languageOptions: { + parser: typescriptParser, + ecmaVersion: 2024, + sourceType: 'module', + globals: { + ...globals.browser, + ...globals.node, + ...globals.jest, + }, + parserOptions: { + ecmaFeatures: { + jsx: true, + }, + project: [ + './src/ui/tsconfig.json', + './tsconfig.test.json', + './tsconfig.cypress.json', + ], + tsconfigRootDir: import.meta.dirname, + noWarnOnMultipleProjects: true, + }, + }, + plugins: { + '@typescript-eslint': typescriptPlugin, + react: reactPlugin, + 'react-hooks': hooksPlugin, + import: importPlugin, + jest: jestPlugin, + 'no-secrets': noSecretsPlugin, + prettier: prettierPlugin, + }, + rules: { + // Ccustom rules + complexity: ['warn', { max: 8 }], + curly: ['error', 'all'], + 'no-unused-vars': 'off', + + // TypeScript rules + '@typescript-eslint/no-unused-vars': [ + 'warn', + { + argsIgnorePattern: '^_', + varsIgnorePattern: '^_', + caughtErrorsIgnorePattern: '^_', + }, + ], + '@typescript-eslint/no-explicit-any': 'warn', + + // Next.js overrides + '@next/next/no-img-element': 'off', // Allow img tags if needed + '@next/next/no-page-custom-font': 'warn', + + // React rules + 'react/react-in-jsx-scope': 'off', // Not needed in Next.js + 'react/prop-types': 'off', // Using TypeScript + 'react-hooks/rules-of-hooks': 'error', + 'react-hooks/exhaustive-deps': 'warn', + + // Import rules + 'import/no-extraneous-dependencies': [ + 'error', + { + devDependencies: [ + '**/*.test.{js,jsx,ts,tsx}', + '**/*.d.ts', + '**/*.interfaces.ts', + '**/*.setup.{js,ts}', + '**/*.config.{js,mjs,ts}', + 'tests/**/*', + 'cypress/**/*', + ], + optionalDependencies: false, + peerDependencies: false, + }, + ], + 'import/order': [ + 'error', + { + groups: [ + ['builtin', 'external'], + ['internal', 'parent', 'sibling', 'index'], + ], + 'newlines-between': 'always-and-inside-groups', + pathGroups: [ + { + pattern: + '@{app,assets,classes,components,hooks,lib,pages,store,tests,types,utils}/**', + group: 'internal', + position: 'before', + }, + { + pattern: '{.,..}/**', + group: 'internal', + position: 'after', + }, + ], + pathGroupsExcludedImportTypes: ['builtin'], + alphabetize: { order: 'asc', caseInsensitive: true }, + }, + ], + + // Security + 'no-secrets/no-secrets': ['error', { additionalRegexes: {}, ignoreContent: [] }], + + // Prettier + 'prettier/prettier': 'error', + }, + settings: { + next: { + rootDir: ['src/ui/', 'tests/ui/'], + }, + 'import/resolver': { + typescript: { + project: [ + './src/ui/tsconfig.json', + './tsconfig.test.json', + './tsconfig.cypress.json', + ], + noWarnOnMultipleProjects: true, + }, + }, + react: { + version: 'detect', + }, + }, + }, + + // Jest-specific rules for test files + { + files: [ + 'tests/**/*.{js,jsx,ts,tsx}', + '**/*.test.{js,jsx,ts,tsx}', + '**/*.spec.{js,jsx,ts,tsx}', + ], + rules: { + 'jest/expect-expect': 'error', + 'jest/no-focused-tests': 'error', + 'jest/no-identical-title': 'error', + 'jest/prefer-to-have-length': 'warn', + 'jest/valid-expect': 'error', + }, + }, + + // Prettier config (disables conflicting rules) + prettierConfig, +]; diff --git a/jest.config.js b/jest.config.cjs similarity index 91% rename from jest.config.js rename to jest.config.cjs index 35e6ad25..7c46d874 100644 --- a/jest.config.js +++ b/jest.config.cjs @@ -6,11 +6,11 @@ const createJestConfig = nextJest({ }); const customJestConfig = { - collectCoverage: true, + collectCoverage: false, collectCoverageFrom: ['./src/ui/**/*.{ts,tsx}'], coverageDirectory: './coverage', coverageProvider: 'v8', - coverageReporters: ['json', 'text-summary', 'lcov'], + coverageReporters: ['text-summary', 'lcov', 'json-summary'], moduleFileExtensions: ['ts', 'tsx', 'js'], moduleNameMapper: { '^.+\\.(svg)$': '<rootDir>/tests/ui/__mocks__/svg.js', diff --git a/jest.setup.ts b/jest.setup.ts index 7b0828bf..eb162bb7 100644 --- a/jest.setup.ts +++ b/jest.setup.ts @@ -1 +1,22 @@ import '@testing-library/jest-dom'; +import 'cross-fetch/polyfill'; + +jest.mock('@nivo/bar'); +jest.mock('@nivo/line'); +jest.mock('@nivo/core'); + +jest.mock('next/dynamic', () => ({ + __esModule: true, + default: (...props: any[]) => { + const dynamicModule = jest.requireActual('next/dynamic'); + const dynamicActualComp = dynamicModule.default; + const RequiredComponent = dynamicActualComp(props[0]); + // eslint-disable-next-line no-unused-expressions, @typescript-eslint/no-unused-expressions + RequiredComponent.preload + ? RequiredComponent.preload() + : RequiredComponent.render.preload(); + return RequiredComponent; + }, +})); + +global.fetch = jest.fn(); diff --git a/package-lock.json b/package-lock.json index 2603ffa4..7bf63e9a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -13,15 +13,23 @@ "@emotion/styled": "^11.14.0", "@mui/material": "^5.11.7", "@mui/material-nextjs": "^5.16.6", + "@nivo/bar": "^0.88.0", + "@nivo/core": "^0.88.0", + "@nivo/line": "^0.88.0", + "@nivo/scales": "^0.88.0", + "@nivo/tooltip": "^0.88.0", "@reduxjs/toolkit": "^2.2.7", + "filesize": "^10.1.6", "next": "15.3.2", "react": "^18.2.0", "react-dom": "^18.2.0", + "react-material-ui-carousel": "^3.4.2", "react-redux": "^9.1.2" }, "devDependencies": { "@eslint/eslintrc": "^3", "@mui/types": "^7.2.14", + "@next/eslint-plugin-next": "^15.3.3", "@svgr/webpack": "^8.1.0", "@testing-library/jest-dom": "^5.16.5", "@testing-library/react": "^16.0.0", @@ -33,6 +41,7 @@ "@types/testing-library__jest-dom": "^5.14.9", "@typescript-eslint/eslint-plugin": "^8.33.1", "@typescript-eslint/parser": "^8.33.1", + "cross-fetch": "^4.1.0", "cypress": "^13.13.3", "eslint": "^9.0.0", "eslint-config-next": "15.3.2", @@ -45,18 +54,22 @@ "eslint-plugin-prettier": "^5.4.0", "eslint-plugin-react": "^7.31.10", "eslint-plugin-react-hooks": "^5.2.0", + "globals": "^16.2.0", "husky": "^9.1.7", "jest": "^29.7.0", "jest-coverage-badges": "^1.1.2", "jest-environment-jsdom": "^29.7.0", "jest-runner-groups": "^2.2.0", "jest-transform-stub": "^2.0.0", - "nyc": "^17.1.0", "prettier": "^3.5.3", "typescript": "^5" }, "engines": { "node": ">=22" + }, + "optionalDependencies": { + "@next/swc-linux-x64-gnu": "^15.3.3", + "@next/swc-linux-x64-musl": "^15.3.3" } }, "node_modules/@adobe/css-tools": { @@ -135,15 +148,12 @@ "url": "https://opencollective.com/babel" } }, - "node_modules/@babel/core/node_modules/semver": { - "version": "6.3.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", - "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "node_modules/@babel/core/node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - } + "license": "MIT" }, "node_modules/@babel/generator": { "version": "7.27.5", @@ -191,16 +201,6 @@ "node": ">=6.9.0" } }, - "node_modules/@babel/helper-compilation-targets/node_modules/semver": { - "version": "6.3.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", - "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", - "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - } - }, "node_modules/@babel/helper-create-class-features-plugin": { "version": "7.27.1", "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.27.1.tgz", @@ -223,16 +223,6 @@ "@babel/core": "^7.0.0" } }, - "node_modules/@babel/helper-create-class-features-plugin/node_modules/semver": { - "version": "6.3.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", - "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", - "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - } - }, "node_modules/@babel/helper-create-regexp-features-plugin": { "version": "7.27.1", "resolved": "https://registry.npmjs.org/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.27.1.tgz", @@ -251,16 +241,6 @@ "@babel/core": "^7.0.0" } }, - "node_modules/@babel/helper-create-regexp-features-plugin/node_modules/semver": { - "version": "6.3.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", - "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", - "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - } - }, "node_modules/@babel/helper-define-polyfill-provider": { "version": "0.6.4", "resolved": "https://registry.npmjs.org/@babel/helper-define-polyfill-provider/-/helper-define-polyfill-provider-0.6.4.tgz", @@ -440,14 +420,14 @@ } }, "node_modules/@babel/helpers": { - "version": "7.27.4", - "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.27.4.tgz", - "integrity": "sha512-Y+bO6U+I7ZKaM5G5rDUZiYfUvQPUibYmAFe7EnKdnKBbVXDZxvp+MWOH5gYciY0EPk4EScsuFMQBbEfpdRKSCQ==", + "version": "7.27.6", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.27.6.tgz", + "integrity": "sha512-muE8Tt8M22638HU31A3CgfSUciwz1fhATfoVai05aPXGor//CdWDCbnlY1yvBPo07njuVOCNGCSp/GTt12lIug==", "dev": true, "license": "MIT", "dependencies": { "@babel/template": "^7.27.2", - "@babel/types": "^7.27.3" + "@babel/types": "^7.27.6" }, "engines": { "node": ">=6.9.0" @@ -1873,16 +1853,6 @@ "@babel/core": "^7.0.0-0" } }, - "node_modules/@babel/preset-env/node_modules/semver": { - "version": "6.3.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", - "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", - "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - } - }, "node_modules/@babel/preset-modules": { "version": "0.1.6-no-external-plugins", "resolved": "https://registry.npmjs.org/@babel/preset-modules/-/preset-modules-0.1.6-no-external-plugins.tgz", @@ -1940,9 +1910,9 @@ } }, "node_modules/@babel/runtime": { - "version": "7.27.4", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.27.4.tgz", - "integrity": "sha512-t3yaEOuGu9NlIZ+hIeGbBjFtZT7j2cb2tg0fuaJKeGotchRjjLfrBA9Kwf8quhpP1EUuxModQg04q/mBwyg8uA==", + "version": "7.27.6", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.27.6.tgz", + "integrity": "sha512-vbavdySgbTTrmFE+EsiqUTzlOr5bzlnJtUv9PynGCAKvfQqjIXbvFdumPM/GxMDfyuGMJaJAU6TO4zc1Jf1i8Q==", "license": "MIT", "engines": { "node": ">=6.9.0" @@ -1990,9 +1960,9 @@ } }, "node_modules/@babel/types": { - "version": "7.27.3", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.27.3.tgz", - "integrity": "sha512-Y1GkI4ktrtvmawoSq+4FCVHNryea6uR+qUQy0AGxLSsjCX0nVmkYQMBLHDkXZuo5hGx7eYdnIaslsdBFm7zbUw==", + "version": "7.27.6", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.27.6.tgz", + "integrity": "sha512-ETyHEk2VHHvl9b9jZP5IHPavHYk57EhanlRRuae9XCpb/j5bDCbPPMOBfCWhnl/7EDJz0jEMCi/RhccCE8r1+Q==", "license": "MIT", "dependencies": { "@babel/helper-string-parser": "^7.27.1", @@ -2071,40 +2041,6 @@ "ms": "^2.1.1" } }, - "node_modules/@emnapi/core": { - "version": "1.4.3", - "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.4.3.tgz", - "integrity": "sha512-4m62DuCE07lw01soJwPiBGC0nAww0Q+RY70VZ+n49yDIO13yyinhbWCeNnaob0lakDtWQzSdtNWzJeOJt2ma+g==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "@emnapi/wasi-threads": "1.0.2", - "tslib": "^2.4.0" - } - }, - "node_modules/@emnapi/runtime": { - "version": "1.4.3", - "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.4.3.tgz", - "integrity": "sha512-pBPWdu6MLKROBX05wSNKcNb++m5Er+KQ9QkB+WVM+pW2Kx9hoSrVTnu3BdkI5eBLZoKu/J6mW/B6i6bJB2ytXQ==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "tslib": "^2.4.0" - } - }, - "node_modules/@emnapi/wasi-threads": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.0.2.tgz", - "integrity": "sha512-5n3nTJblwRi8LlXkJ9eBzu+kZR8Yxcc7ubakyQTFzPMtIhFpUBRbsnc2Dv88IZDIbCDlBiWrknhB4Lsz7mg6BA==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "tslib": "^2.4.0" - } - }, "node_modules/@emotion/babel-plugin": { "version": "11.13.5", "resolved": "https://registry.npmjs.org/@emotion/babel-plugin/-/babel-plugin-11.13.5.tgz", @@ -2124,21 +2060,6 @@ "stylis": "4.2.0" } }, - "node_modules/@emotion/babel-plugin/node_modules/convert-source-map": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.9.0.tgz", - "integrity": "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==", - "license": "MIT" - }, - "node_modules/@emotion/babel-plugin/node_modules/source-map": { - "version": "0.5.7", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", - "integrity": "sha512-LbrmJOMUSdEVxIKvdcJzQC+nQhe8FUZQTXQy6+I75skNgn3OoQ0DZA8YnFa7gp8tqtL3KPf1kmo0R5DoApeSGQ==", - "license": "BSD-3-Clause", - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/@emotion/cache": { "version": "11.14.0", "resolved": "https://registry.npmjs.org/@emotion/cache/-/cache-11.14.0.tgz", @@ -2357,6 +2278,19 @@ "url": "https://opencollective.com/eslint" } }, + "node_modules/@eslint/eslintrc/node_modules/globals": { + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", + "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/@eslint/js": { "version": "9.28.0", "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.28.0.tgz", @@ -2525,6 +2459,16 @@ "sprintf-js": "~1.0.2" } }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/camelcase": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", + "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/@istanbuljs/load-nyc-config/node_modules/find-up": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", @@ -2940,6 +2884,13 @@ "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, + "node_modules/@jest/transform/node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "license": "MIT" + }, "node_modules/@jest/types": { "version": "29.6.3", "resolved": "https://registry.npmjs.org/@jest/types/-/types-29.6.3.tgz", @@ -3016,6 +2967,32 @@ "url": "https://opencollective.com/mui-org" } }, + "node_modules/@mui/icons-material": { + "version": "5.17.1", + "resolved": "https://registry.npmjs.org/@mui/icons-material/-/icons-material-5.17.1.tgz", + "integrity": "sha512-CN86LocjkunFGG0yPlO4bgqHkNGgaEOEc3X/jG5Bzm401qYw79/SaLrofA7yAKCCXAGdIGnLoMHohc3+ubs95A==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.23.9" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mui-org" + }, + "peerDependencies": { + "@mui/material": "^5.0.0", + "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0", + "react": "^17.0.0 || ^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, "node_modules/@mui/material": { "version": "5.17.1", "resolved": "https://registry.npmjs.org/@mui/material/-/material-5.17.1.tgz", @@ -3110,12 +3087,6 @@ } } }, - "node_modules/@mui/material/node_modules/react-is": { - "version": "19.1.0", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-19.1.0.tgz", - "integrity": "sha512-Oe56aUPnkHyyDxxkvqtd7KkdQP5uIUfHxd5XTb3wE9d/kRnZLmKbDB0GWk919tdQ+mxxPtG6EAs6RMT6i1qtHg==", - "license": "MIT" - }, "node_modules/@mui/private-theming": { "version": "5.17.1", "resolved": "https://registry.npmjs.org/@mui/private-theming/-/private-theming-5.17.1.tgz", @@ -3291,25 +3262,6 @@ } } }, - "node_modules/@mui/utils/node_modules/react-is": { - "version": "19.1.0", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-19.1.0.tgz", - "integrity": "sha512-Oe56aUPnkHyyDxxkvqtd7KkdQP5uIUfHxd5XTb3wE9d/kRnZLmKbDB0GWk919tdQ+mxxPtG6EAs6RMT6i1qtHg==", - "license": "MIT" - }, - "node_modules/@napi-rs/wasm-runtime": { - "version": "0.2.10", - "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-0.2.10.tgz", - "integrity": "sha512-bCsCyeZEwVErsGmyPNSzwfwFn4OdxBj0mmv6hOFucB/k81Ojdu68RbZdxYsRQUPc9l6SU5F/cG+bXgWs3oUgsQ==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "@emnapi/core": "^1.4.3", - "@emnapi/runtime": "^1.4.3", - "@tybys/wasm-util": "^0.9.0" - } - }, "node_modules/@next/env": { "version": "15.3.2", "resolved": "https://registry.npmjs.org/@next/env/-/env-15.3.2.tgz", @@ -3317,9 +3269,9 @@ "license": "MIT" }, "node_modules/@next/eslint-plugin-next": { - "version": "15.3.2", - "resolved": "https://registry.npmjs.org/@next/eslint-plugin-next/-/eslint-plugin-next-15.3.2.tgz", - "integrity": "sha512-ijVRTXBgnHT33aWnDtmlG+LJD+5vhc9AKTJPquGG5NKXjpKNjc62woIhFtrAcWdBobt8kqjCoaJ0q6sDQoX7aQ==", + "version": "15.3.3", + "resolved": "https://registry.npmjs.org/@next/eslint-plugin-next/-/eslint-plugin-next-15.3.3.tgz", + "integrity": "sha512-VKZJEiEdpKkfBmcokGjHu0vGDG+8CehGs90tBEy/IDoDDKGngeyIStt2MmE5FYNyU9BhgR7tybNWTAJY/30u+Q==", "dev": true, "license": "MIT", "dependencies": { @@ -3418,12 +3370,13 @@ } }, "node_modules/@next/swc-linux-x64-gnu": { - "version": "15.3.2", - "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-15.3.2.tgz", - "integrity": "sha512-uRBo6THWei0chz+Y5j37qzx+BtoDRFIkDzZjlpCItBRXyMPIg079eIkOCl3aqr2tkxL4HFyJ4GHDes7W8HuAUg==", + "version": "15.3.3", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-15.3.3.tgz", + "integrity": "sha512-jJ8HRiF3N8Zw6hGlytCj5BiHyG/K+fnTKVDEKvUCyiQ/0r5tgwO7OgaRiOjjRoIx2vwLR+Rz8hQoPrnmFbJdfw==", "cpu": [ "x64" ], + "license": "MIT", "optional": true, "os": [ "linux" @@ -3433,12 +3386,13 @@ } }, "node_modules/@next/swc-linux-x64-musl": { - "version": "15.3.2", - "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-15.3.2.tgz", - "integrity": "sha512-+uxFlPuCNx/T9PdMClOqeE8USKzj8tVz37KflT3Kdbx/LOlZBRI2yxuIcmx1mPNK8DwSOMNCr4ureSet7eyC0w==", + "version": "15.3.3", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-15.3.3.tgz", + "integrity": "sha512-HrUcTr4N+RgiiGn3jjeT6Oo208UT/7BuTr7K0mdKRBtTbT4v9zJqCDKO97DUqqoBK1qyzP1RwvrWTvU6EPh/Cw==", "cpu": [ "x64" ], + "license": "MIT", "optional": true, "os": [ "linux" @@ -3477,6 +3431,198 @@ "node": ">= 10" } }, + "node_modules/@nivo/annotations": { + "version": "0.88.0", + "resolved": "https://registry.npmjs.org/@nivo/annotations/-/annotations-0.88.0.tgz", + "integrity": "sha512-NXE+1oIUn+EGWMQpnpeRMLgi2wyuzhGDoJQY4OUHissCUiNotid2oNQ/PXJwN0toiu+/j9SyhzI32xr70OPi7Q==", + "license": "MIT", + "dependencies": { + "@nivo/colors": "0.88.0", + "@nivo/core": "0.88.0", + "@react-spring/web": "9.4.5 || ^9.7.2", + "lodash": "^4.17.21" + }, + "peerDependencies": { + "react": ">= 16.14.0 < 19.0.0" + } + }, + "node_modules/@nivo/axes": { + "version": "0.88.0", + "resolved": "https://registry.npmjs.org/@nivo/axes/-/axes-0.88.0.tgz", + "integrity": "sha512-jF7aIxzTNayV5cI1J/b9Q1FfpMBxTXGk3OwSigXMSfYWlliskDn2u0qGRLiYhuXFdQAWIp4oXsO1GcAQ0eRVdg==", + "license": "MIT", + "dependencies": { + "@nivo/core": "0.88.0", + "@nivo/scales": "0.88.0", + "@react-spring/web": "9.4.5 || ^9.7.2", + "@types/d3-format": "^1.4.1", + "@types/d3-time-format": "^2.3.1", + "d3-format": "^1.4.4", + "d3-time-format": "^3.0.0" + }, + "peerDependencies": { + "react": ">= 16.14.0 < 19.0.0" + } + }, + "node_modules/@nivo/bar": { + "version": "0.88.0", + "resolved": "https://registry.npmjs.org/@nivo/bar/-/bar-0.88.0.tgz", + "integrity": "sha512-wckwuHWeCikxGvvdRfGL+dVFsUD9uHk1r9s7bWUfOD+p8BWhxtYqfXpHolEfgGg3UyPaHtpGA7P4zgE5vgo7gQ==", + "license": "MIT", + "dependencies": { + "@nivo/annotations": "0.88.0", + "@nivo/axes": "0.88.0", + "@nivo/colors": "0.88.0", + "@nivo/core": "0.88.0", + "@nivo/legends": "0.88.0", + "@nivo/scales": "0.88.0", + "@nivo/tooltip": "0.88.0", + "@react-spring/web": "9.4.5 || ^9.7.2", + "@types/d3-scale": "^4.0.8", + "@types/d3-shape": "^3.1.6", + "d3-scale": "^4.0.2", + "d3-shape": "^3.2.0", + "lodash": "^4.17.21" + }, + "peerDependencies": { + "react": ">= 16.14.0 < 19.0.0" + } + }, + "node_modules/@nivo/colors": { + "version": "0.88.0", + "resolved": "https://registry.npmjs.org/@nivo/colors/-/colors-0.88.0.tgz", + "integrity": "sha512-IZ+leYIqAlo7dyLHmsQwujanfRgXyoQ5H7PU3RWLEn1PP0zxDKLgEjFEDADpDauuslh2Tx0L81GNkWR6QSP0Mw==", + "license": "MIT", + "dependencies": { + "@nivo/core": "0.88.0", + "@types/d3-color": "^3.0.0", + "@types/d3-scale": "^4.0.8", + "@types/d3-scale-chromatic": "^3.0.0", + "@types/prop-types": "^15.7.2", + "d3-color": "^3.1.0", + "d3-scale": "^4.0.2", + "d3-scale-chromatic": "^3.0.0", + "lodash": "^4.17.21", + "prop-types": "^15.7.2" + }, + "peerDependencies": { + "react": ">= 16.14.0 < 19.0.0" + } + }, + "node_modules/@nivo/core": { + "version": "0.88.0", + "resolved": "https://registry.npmjs.org/@nivo/core/-/core-0.88.0.tgz", + "integrity": "sha512-XjUkA5MmwjLP38bdrJwn36Gj7T5SYMKD55LYQp/1nIJPdxqJ38dUfE4XyBDfIEgfP6yrHOihw3C63cUdnUBoiw==", + "license": "MIT", + "dependencies": { + "@nivo/tooltip": "0.88.0", + "@react-spring/web": "9.4.5 || ^9.7.2", + "@types/d3-shape": "^3.1.6", + "d3-color": "^3.1.0", + "d3-format": "^1.4.4", + "d3-interpolate": "^3.0.1", + "d3-scale": "^4.0.2", + "d3-scale-chromatic": "^3.0.0", + "d3-shape": "^3.2.0", + "d3-time-format": "^3.0.0", + "lodash": "^4.17.21", + "prop-types": "^15.7.2" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/nivo/donate" + }, + "peerDependencies": { + "react": ">= 16.14.0 < 19.0.0" + } + }, + "node_modules/@nivo/legends": { + "version": "0.88.0", + "resolved": "https://registry.npmjs.org/@nivo/legends/-/legends-0.88.0.tgz", + "integrity": "sha512-d4DF9pHbD8LmGJlp/Gp1cF4e8y2wfQTcw3jVhbZj9zkb7ZWB7JfeF60VHRfbXNux9bjQ9U78/SssQqueVDPEmg==", + "license": "MIT", + "dependencies": { + "@nivo/colors": "0.88.0", + "@nivo/core": "0.88.0", + "@types/d3-scale": "^4.0.8", + "d3-scale": "^4.0.2" + }, + "peerDependencies": { + "react": ">= 16.14.0 < 19.0.0" + } + }, + "node_modules/@nivo/line": { + "version": "0.88.0", + "resolved": "https://registry.npmjs.org/@nivo/line/-/line-0.88.0.tgz", + "integrity": "sha512-hFTyZ3BdAZvq2HwdwMj2SJGUeodjEW+7DLtFMIIoVIxmjZlAs3z533HcJ9cJd3it928fDm8SF/rgHs0TztYf9Q==", + "license": "MIT", + "dependencies": { + "@nivo/annotations": "0.88.0", + "@nivo/axes": "0.88.0", + "@nivo/colors": "0.88.0", + "@nivo/core": "0.88.0", + "@nivo/legends": "0.88.0", + "@nivo/scales": "0.88.0", + "@nivo/tooltip": "0.88.0", + "@nivo/voronoi": "0.88.0", + "@react-spring/web": "9.4.5 || ^9.7.2", + "d3-shape": "^3.2.0" + }, + "peerDependencies": { + "react": ">= 16.14.0 < 19.0.0" + } + }, + "node_modules/@nivo/scales": { + "version": "0.88.0", + "resolved": "https://registry.npmjs.org/@nivo/scales/-/scales-0.88.0.tgz", + "integrity": "sha512-HbpxkQp6tHCltZ1yDGeqdLcaJl5ze54NPjurfGtx/Uq+H5IQoBd4Tln49bUar5CsFAMsXw8yF1HQvASr7I1SIA==", + "license": "MIT", + "dependencies": { + "@types/d3-scale": "^4.0.8", + "@types/d3-time": "^1.1.1", + "@types/d3-time-format": "^3.0.0", + "d3-scale": "^4.0.2", + "d3-time": "^1.0.11", + "d3-time-format": "^3.0.0", + "lodash": "^4.17.21" + } + }, + "node_modules/@nivo/scales/node_modules/@types/d3-time-format": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/d3-time-format/-/d3-time-format-3.0.4.tgz", + "integrity": "sha512-or9DiDnYI1h38J9hxKEsw513+KVuFbEVhl7qdxcaudoiqWWepapUen+2vAriFGexr6W5+P4l9+HJrB39GG+oRg==", + "license": "MIT" + }, + "node_modules/@nivo/tooltip": { + "version": "0.88.0", + "resolved": "https://registry.npmjs.org/@nivo/tooltip/-/tooltip-0.88.0.tgz", + "integrity": "sha512-iEjVfQA8gumAzg/yUinjTwswygCkE5Iwuo8opwnrbpNIqMrleBV+EAKIgB0PrzepIoW8CFG/SJhoiRfbU8jhOw==", + "license": "MIT", + "dependencies": { + "@nivo/core": "0.88.0", + "@react-spring/web": "9.4.5 || ^9.7.2" + }, + "peerDependencies": { + "react": ">= 16.14.0 < 19.0.0" + } + }, + "node_modules/@nivo/voronoi": { + "version": "0.88.0", + "resolved": "https://registry.npmjs.org/@nivo/voronoi/-/voronoi-0.88.0.tgz", + "integrity": "sha512-MyiNLvODthFoMjQ7Wjp693nogbTmVEx8Yn/7QkJhyPQbFyyA37TF/D1a/ox4h2OslXtP6K9QFN+42gB/zu7ixw==", + "license": "MIT", + "dependencies": { + "@nivo/core": "0.88.0", + "@nivo/tooltip": "0.88.0", + "@types/d3-delaunay": "^6.0.4", + "@types/d3-scale": "^4.0.8", + "d3-delaunay": "^6.0.4", + "d3-scale": "^4.0.2" + }, + "peerDependencies": { + "react": ">= 16.14.0 < 19.0.0" + } + }, "node_modules/@nodelib/fs.scandir": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", @@ -3548,38 +3694,110 @@ "url": "https://opencollective.com/popperjs" } }, - "node_modules/@reduxjs/toolkit": { - "version": "2.8.2", - "resolved": "https://registry.npmjs.org/@reduxjs/toolkit/-/toolkit-2.8.2.tgz", - "integrity": "sha512-MYlOhQ0sLdw4ud48FoC5w0dH9VfWQjtCjreKwYTT3l+r427qYC5Y8PihNutepr8XrNaBUDQo9khWUwQxZaqt5A==", + "node_modules/@react-spring/animated": { + "version": "9.7.5", + "resolved": "https://registry.npmjs.org/@react-spring/animated/-/animated-9.7.5.tgz", + "integrity": "sha512-Tqrwz7pIlsSDITzxoLS3n/v/YCUHQdOIKtOJf4yL6kYVSDTSmVK1LI1Q3M/uu2Sx4X3pIWF3xLUhlsA6SPNTNg==", "license": "MIT", "dependencies": { - "@standard-schema/spec": "^1.0.0", - "@standard-schema/utils": "^0.3.0", - "immer": "^10.0.3", - "redux": "^5.0.1", - "redux-thunk": "^3.1.0", - "reselect": "^5.1.0" + "@react-spring/shared": "~9.7.5", + "@react-spring/types": "~9.7.5" }, "peerDependencies": { - "react": "^16.9.0 || ^17.0.0 || ^18 || ^19", - "react-redux": "^7.2.1 || ^8.1.3 || ^9.0.0" - }, - "peerDependenciesMeta": { - "react": { - "optional": true - }, - "react-redux": { - "optional": true - } + "react": "^16.8.0 || ^17.0.0 || ^18.0.0" } }, - "node_modules/@rtsao/scc": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@rtsao/scc/-/scc-1.1.0.tgz", - "integrity": "sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g==", - "dev": true, - "license": "MIT" + "node_modules/@react-spring/core": { + "version": "9.7.5", + "resolved": "https://registry.npmjs.org/@react-spring/core/-/core-9.7.5.tgz", + "integrity": "sha512-rmEqcxRcu7dWh7MnCcMXLvrf6/SDlSokLaLTxiPlAYi11nN3B5oiCUAblO72o+9z/87j2uzxa2Inm8UbLjXA+w==", + "license": "MIT", + "dependencies": { + "@react-spring/animated": "~9.7.5", + "@react-spring/shared": "~9.7.5", + "@react-spring/types": "~9.7.5" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/react-spring/donate" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0" + } + }, + "node_modules/@react-spring/rafz": { + "version": "9.7.5", + "resolved": "https://registry.npmjs.org/@react-spring/rafz/-/rafz-9.7.5.tgz", + "integrity": "sha512-5ZenDQMC48wjUzPAm1EtwQ5Ot3bLIAwwqP2w2owG5KoNdNHpEJV263nGhCeKKmuA3vG2zLLOdu3or6kuDjA6Aw==", + "license": "MIT" + }, + "node_modules/@react-spring/shared": { + "version": "9.7.5", + "resolved": "https://registry.npmjs.org/@react-spring/shared/-/shared-9.7.5.tgz", + "integrity": "sha512-wdtoJrhUeeyD/PP/zo+np2s1Z820Ohr/BbuVYv+3dVLW7WctoiN7std8rISoYoHpUXtbkpesSKuPIw/6U1w1Pw==", + "license": "MIT", + "dependencies": { + "@react-spring/rafz": "~9.7.5", + "@react-spring/types": "~9.7.5" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0" + } + }, + "node_modules/@react-spring/types": { + "version": "9.7.5", + "resolved": "https://registry.npmjs.org/@react-spring/types/-/types-9.7.5.tgz", + "integrity": "sha512-HVj7LrZ4ReHWBimBvu2SKND3cDVUPWKLqRTmWe/fNY6o1owGOX0cAHbdPDTMelgBlVbrTKrre6lFkhqGZErK/g==", + "license": "MIT" + }, + "node_modules/@react-spring/web": { + "version": "9.7.5", + "resolved": "https://registry.npmjs.org/@react-spring/web/-/web-9.7.5.tgz", + "integrity": "sha512-lmvqGwpe+CSttsWNZVr+Dg62adtKhauGwLyGE/RRyZ8AAMLgb9x3NDMA5RMElXo+IMyTkPp7nxTB8ZQlmhb6JQ==", + "license": "MIT", + "dependencies": { + "@react-spring/animated": "~9.7.5", + "@react-spring/core": "~9.7.5", + "@react-spring/shared": "~9.7.5", + "@react-spring/types": "~9.7.5" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0", + "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0" + } + }, + "node_modules/@reduxjs/toolkit": { + "version": "2.8.2", + "resolved": "https://registry.npmjs.org/@reduxjs/toolkit/-/toolkit-2.8.2.tgz", + "integrity": "sha512-MYlOhQ0sLdw4ud48FoC5w0dH9VfWQjtCjreKwYTT3l+r427qYC5Y8PihNutepr8XrNaBUDQo9khWUwQxZaqt5A==", + "license": "MIT", + "dependencies": { + "@standard-schema/spec": "^1.0.0", + "@standard-schema/utils": "^0.3.0", + "immer": "^10.0.3", + "redux": "^5.0.1", + "redux-thunk": "^3.1.0", + "reselect": "^5.1.0" + }, + "peerDependencies": { + "react": "^16.9.0 || ^17.0.0 || ^18 || ^19", + "react-redux": "^7.2.1 || ^8.1.3 || ^9.0.0" + }, + "peerDependenciesMeta": { + "react": { + "optional": true + }, + "react-redux": { + "optional": true + } + } + }, + "node_modules/@rtsao/scc": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@rtsao/scc/-/scc-1.1.0.tgz", + "integrity": "sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g==", + "dev": true, + "license": "MIT" }, "node_modules/@rushstack/eslint-patch": { "version": "1.11.0", @@ -3811,19 +4029,6 @@ "url": "https://github.com/sponsors/gregberge" } }, - "node_modules/@svgr/core/node_modules/camelcase": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", - "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/@svgr/hast-util-to-babel-ast": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/@svgr/hast-util-to-babel-ast/-/hast-util-to-babel-ast-8.0.0.tgz", @@ -3842,19 +4047,6 @@ "url": "https://github.com/sponsors/gregberge" } }, - "node_modules/@svgr/hast-util-to-babel-ast/node_modules/entities": { - "version": "4.5.0", - "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", - "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", - "dev": true, - "license": "BSD-2-Clause", - "engines": { - "node": ">=0.12" - }, - "funding": { - "url": "https://github.com/fb55/entities?sponsor=1" - } - }, "node_modules/@svgr/plugin-jsx": { "version": "8.1.0", "resolved": "https://registry.npmjs.org/@svgr/plugin-jsx/-/plugin-jsx-8.1.0.tgz", @@ -4062,17 +4254,6 @@ "node": ">=10.13.0" } }, - "node_modules/@tybys/wasm-util": { - "version": "0.9.0", - "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.9.0.tgz", - "integrity": "sha512-6+7nlbMVX/PVDCwaIQ8nTOPveOcFLSt8GcXdx8hD0bt39uWxYT88uXzqTd4fTvqta7oeUJqudepapKNt2DYJFw==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "tslib": "^2.4.0" - } - }, "node_modules/@types/aria-query": { "version": "5.0.4", "resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-5.0.4.tgz", @@ -4126,10 +4307,70 @@ "@babel/types": "^7.20.7" } }, + "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-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-format": { + "version": "1.4.5", + "resolved": "https://registry.npmjs.org/@types/d3-format/-/d3-format-1.4.5.tgz", + "integrity": "sha512-mLxrC1MSWupOSncXN/HOlWUAAIffAEBaI4+PKy2uMPsKe4FNZlk7qrbTjmzJXITQQqBHivaks4Td18azgqnotA==", + "license": "MIT" + }, + "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-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-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": "1.1.4", + "resolved": "https://registry.npmjs.org/@types/d3-time/-/d3-time-1.1.4.tgz", + "integrity": "sha512-JIvy2HjRInE+TXOmIGN5LCmeO0hkFZx5f9FZ7kiN+D+YTcc8pptsiLiuHsvwxwC7VVKmJ2ExHUgNlAiV7vQM9g==", + "license": "MIT" + }, + "node_modules/@types/d3-time-format": { + "version": "2.3.4", + "resolved": "https://registry.npmjs.org/@types/d3-time-format/-/d3-time-format-2.3.4.tgz", + "integrity": "sha512-xdDXbpVO74EvadI3UDxjxTdR6QIxm1FKzEA/+F8tL4GWWUg/hgvBqf6chql64U5A9ZUGWo7pEu4eNlyLwbKdhg==", + "license": "MIT" + }, "node_modules/@types/estree": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.7.tgz", - "integrity": "sha512-w28IoSUCJpidD/TGviZwwMJckNESJZXFu7NBZ5YJ4mEUnNraUn9Pm8HSZm/jDF1pDWYKspWE7oVphigUPRakIQ==", + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", "dev": true, "license": "MIT" }, @@ -4208,9 +4449,9 @@ "license": "MIT" }, "node_modules/@types/node": { - "version": "22.15.29", - "resolved": "https://registry.npmjs.org/@types/node/-/node-22.15.29.tgz", - "integrity": "sha512-LNdjOkUDlU1RZb8e1kOIUpN1qQUlzGkEtbVNo53vbrwDg5om6oduhm4SiUaPW5ASTXhAiP0jInWG8Qx9fVlOeQ==", + "version": "22.15.30", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.15.30.tgz", + "integrity": "sha512-6Q7lr06bEHdlfplU6YRbgG1SFBdlsfNC4/lX+SkhiTs0cpJkOElmWls8PxDFv4yY/xKb8Y6SO0OmSX4wgqTZbA==", "dev": true, "license": "MIT", "dependencies": { @@ -4331,17 +4572,17 @@ } }, "node_modules/@typescript-eslint/eslint-plugin": { - "version": "8.33.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.33.1.tgz", - "integrity": "sha512-TDCXj+YxLgtvxvFlAvpoRv9MAncDLBV2oT9Bd7YBGC/b/sEURoOYuIwLI99rjWOfY3QtDzO+mk0n4AmdFExW8A==", + "version": "8.34.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.34.0.tgz", + "integrity": "sha512-QXwAlHlbcAwNlEEMKQS2RCgJsgXrTJdjXT08xEgbPFa2yYQgVjBymxP5DrfrE7X7iodSzd9qBUHUycdyVJTW1w==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/regexpp": "^4.10.0", - "@typescript-eslint/scope-manager": "8.33.1", - "@typescript-eslint/type-utils": "8.33.1", - "@typescript-eslint/utils": "8.33.1", - "@typescript-eslint/visitor-keys": "8.33.1", + "@typescript-eslint/scope-manager": "8.34.0", + "@typescript-eslint/type-utils": "8.34.0", + "@typescript-eslint/utils": "8.34.0", + "@typescript-eslint/visitor-keys": "8.34.0", "graphemer": "^1.4.0", "ignore": "^7.0.0", "natural-compare": "^1.4.0", @@ -4355,7 +4596,7 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "@typescript-eslint/parser": "^8.33.1", + "@typescript-eslint/parser": "^8.34.0", "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <5.9.0" } @@ -4371,16 +4612,16 @@ } }, "node_modules/@typescript-eslint/parser": { - "version": "8.33.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.33.1.tgz", - "integrity": "sha512-qwxv6dq682yVvgKKp2qWwLgRbscDAYktPptK4JPojCwwi3R9cwrvIxS4lvBpzmcqzR4bdn54Z0IG1uHFskW4dA==", + "version": "8.34.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.34.0.tgz", + "integrity": "sha512-vxXJV1hVFx3IXz/oy2sICsJukaBrtDEQSBiV48/YIV5KWjX1dO+bcIr/kCPrW6weKXvsaGKFNlwH0v2eYdRRbA==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/scope-manager": "8.33.1", - "@typescript-eslint/types": "8.33.1", - "@typescript-eslint/typescript-estree": "8.33.1", - "@typescript-eslint/visitor-keys": "8.33.1", + "@typescript-eslint/scope-manager": "8.34.0", + "@typescript-eslint/types": "8.34.0", + "@typescript-eslint/typescript-estree": "8.34.0", + "@typescript-eslint/visitor-keys": "8.34.0", "debug": "^4.3.4" }, "engines": { @@ -4396,14 +4637,14 @@ } }, "node_modules/@typescript-eslint/project-service": { - "version": "8.33.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.33.1.tgz", - "integrity": "sha512-DZR0efeNklDIHHGRpMpR5gJITQpu6tLr9lDJnKdONTC7vvzOlLAG/wcfxcdxEWrbiZApcoBCzXqU/Z458Za5Iw==", + "version": "8.34.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.34.0.tgz", + "integrity": "sha512-iEgDALRf970/B2YExmtPMPF54NenZUf4xpL3wsCRx/lgjz6ul/l13R81ozP/ZNuXfnLCS+oPmG7JIxfdNYKELw==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/tsconfig-utils": "^8.33.1", - "@typescript-eslint/types": "^8.33.1", + "@typescript-eslint/tsconfig-utils": "^8.34.0", + "@typescript-eslint/types": "^8.34.0", "debug": "^4.3.4" }, "engines": { @@ -4418,14 +4659,14 @@ } }, "node_modules/@typescript-eslint/scope-manager": { - "version": "8.33.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.33.1.tgz", - "integrity": "sha512-dM4UBtgmzHR9bS0Rv09JST0RcHYearoEoo3pG5B6GoTR9XcyeqX87FEhPo+5kTvVfKCvfHaHrcgeJQc6mrDKrA==", + "version": "8.34.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.34.0.tgz", + "integrity": "sha512-9Ac0X8WiLykl0aj1oYQNcLZjHgBojT6cW68yAgZ19letYu+Hxd0rE0veI1XznSSst1X5lwnxhPbVdwjDRIomRw==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.33.1", - "@typescript-eslint/visitor-keys": "8.33.1" + "@typescript-eslint/types": "8.34.0", + "@typescript-eslint/visitor-keys": "8.34.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -4436,9 +4677,9 @@ } }, "node_modules/@typescript-eslint/tsconfig-utils": { - "version": "8.33.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.33.1.tgz", - "integrity": "sha512-STAQsGYbHCF0/e+ShUQ4EatXQ7ceh3fBCXkNU7/MZVKulrlq1usH7t2FhxvCpuCi5O5oi1vmVaAjrGeL71OK1g==", + "version": "8.34.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.34.0.tgz", + "integrity": "sha512-+W9VYHKFIzA5cBeooqQxqNriAP0QeQ7xTiDuIOr71hzgffm3EL2hxwWBIIj4GuofIbKxGNarpKqIq6Q6YrShOA==", "dev": true, "license": "MIT", "engines": { @@ -4453,14 +4694,14 @@ } }, "node_modules/@typescript-eslint/type-utils": { - "version": "8.33.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.33.1.tgz", - "integrity": "sha512-1cG37d9xOkhlykom55WVwG2QRNC7YXlxMaMzqw2uPeJixBFfKWZgaP/hjAObqMN/u3fr5BrTwTnc31/L9jQ2ww==", + "version": "8.34.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.34.0.tgz", + "integrity": "sha512-n7zSmOcUVhcRYC75W2pnPpbO1iwhJY3NLoHEtbJwJSNlVAZuwqu05zY3f3s2SDWWDSo9FdN5szqc73DCtDObAg==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/typescript-estree": "8.33.1", - "@typescript-eslint/utils": "8.33.1", + "@typescript-eslint/typescript-estree": "8.34.0", + "@typescript-eslint/utils": "8.34.0", "debug": "^4.3.4", "ts-api-utils": "^2.1.0" }, @@ -4477,9 +4718,9 @@ } }, "node_modules/@typescript-eslint/types": { - "version": "8.33.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.33.1.tgz", - "integrity": "sha512-xid1WfizGhy/TKMTwhtVOgalHwPtV8T32MS9MaH50Cwvz6x6YqRIPdD2WvW0XaqOzTV9p5xdLY0h/ZusU5Lokg==", + "version": "8.34.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.34.0.tgz", + "integrity": "sha512-9V24k/paICYPniajHfJ4cuAWETnt7Ssy+R0Rbcqo5sSFr3QEZ/8TSoUi9XeXVBGXCaLtwTOKSLGcInCAvyZeMA==", "dev": true, "license": "MIT", "engines": { @@ -4491,16 +4732,16 @@ } }, "node_modules/@typescript-eslint/typescript-estree": { - "version": "8.33.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.33.1.tgz", - "integrity": "sha512-+s9LYcT8LWjdYWu7IWs7FvUxpQ/DGkdjZeE/GGulHvv8rvYwQvVaUZ6DE+j5x/prADUgSbbCWZ2nPI3usuVeOA==", + "version": "8.34.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.34.0.tgz", + "integrity": "sha512-rOi4KZxI7E0+BMqG7emPSK1bB4RICCpF7QD3KCLXn9ZvWoESsOMlHyZPAHyG04ujVplPaHbmEvs34m+wjgtVtg==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/project-service": "8.33.1", - "@typescript-eslint/tsconfig-utils": "8.33.1", - "@typescript-eslint/types": "8.33.1", - "@typescript-eslint/visitor-keys": "8.33.1", + "@typescript-eslint/project-service": "8.34.0", + "@typescript-eslint/tsconfig-utils": "8.34.0", + "@typescript-eslint/types": "8.34.0", + "@typescript-eslint/visitor-keys": "8.34.0", "debug": "^4.3.4", "fast-glob": "^3.3.2", "is-glob": "^4.0.3", @@ -4545,17 +4786,30 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/semver": { + "version": "7.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", + "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/@typescript-eslint/utils": { - "version": "8.33.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.33.1.tgz", - "integrity": "sha512-52HaBiEQUaRYqAXpfzWSR2U3gxk92Kw006+xZpElaPMg3C4PgM+A5LqwoQI1f9E5aZ/qlxAZxzm42WX+vn92SQ==", + "version": "8.34.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.34.0.tgz", + "integrity": "sha512-8L4tWatGchV9A1cKbjaavS6mwYwp39jql8xUmIIKJdm+qiaeHy5KMKlBrf30akXAWBzn2SqKsNOtSENWUwg7XQ==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/eslint-utils": "^4.7.0", - "@typescript-eslint/scope-manager": "8.33.1", - "@typescript-eslint/types": "8.33.1", - "@typescript-eslint/typescript-estree": "8.33.1" + "@typescript-eslint/scope-manager": "8.34.0", + "@typescript-eslint/types": "8.34.0", + "@typescript-eslint/typescript-estree": "8.34.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -4570,13 +4824,13 @@ } }, "node_modules/@typescript-eslint/visitor-keys": { - "version": "8.33.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.33.1.tgz", - "integrity": "sha512-3i8NrFcZeeDHJ+7ZUuDkGT+UHq+XoFGsymNK2jZCOHcfEzRQ0BdpRtdpSx/Iyf3MHLWIcLS0COuOPibKQboIiQ==", + "version": "8.34.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.34.0.tgz", + "integrity": "sha512-qHV7pW7E85A0x6qyrFn+O+q1k1p3tQCsqIZ1KZ5ESLXY57aTvUd3/a4rdPTeXisvhXn2VQG0VSKUqs8KHF2zcA==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.33.1", + "@typescript-eslint/types": "8.34.0", "eslint-visitor-keys": "^4.2.0" }, "engines": { @@ -4588,9 +4842,9 @@ } }, "node_modules/@typescript-eslint/visitor-keys/node_modules/eslint-visitor-keys": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.0.tgz", - "integrity": "sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw==", + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", + "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", "dev": true, "license": "Apache-2.0", "engines": { @@ -4601,9 +4855,9 @@ } }, "node_modules/@unrs/resolver-binding-darwin-arm64": { - "version": "1.7.11", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-darwin-arm64/-/resolver-binding-darwin-arm64-1.7.11.tgz", - "integrity": "sha512-i3/wlWjQJXMh1uiGtiv7k1EYvrrS3L1hdwmWJJiz1D8jWy726YFYPIxQWbEIVPVAgrfRR0XNlLrTQwq17cuCGw==", + "version": "1.7.12", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-darwin-arm64/-/resolver-binding-darwin-arm64-1.7.12.tgz", + "integrity": "sha512-C//UObaqVcGKpRMMThzBCDxbqM9YQg2dtWy3OwcERLu+qzLa781AqvGdgqwqakRO+cWCK6dl75ebAcsSozmARg==", "cpu": [ "arm64" ], @@ -4614,233 +4868,6 @@ "darwin" ] }, - "node_modules/@unrs/resolver-binding-darwin-x64": { - "version": "1.7.11", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-darwin-x64/-/resolver-binding-darwin-x64-1.7.11.tgz", - "integrity": "sha512-8XXyFvc6w6kmMmi6VYchZhjd5CDcp+Lv6Cn1YmUme0ypsZ/0Kzd+9ESrWtDrWibKPTgSteDTxp75cvBOY64FQQ==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ] - }, - "node_modules/@unrs/resolver-binding-freebsd-x64": { - "version": "1.7.11", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-freebsd-x64/-/resolver-binding-freebsd-x64-1.7.11.tgz", - "integrity": "sha512-0qJBYzP8Qk24CZ05RSWDQUjdiQUeIJGfqMMzbtXgCKl/a5xa6thfC0MQkGIr55LCLd6YmMyO640ifYUa53lybQ==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ] - }, - "node_modules/@unrs/resolver-binding-linux-arm-gnueabihf": { - "version": "1.7.11", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm-gnueabihf/-/resolver-binding-linux-arm-gnueabihf-1.7.11.tgz", - "integrity": "sha512-1sGwpgvx+WZf0GFT6vkkOm6UJ+mlsVnjw+Yv9esK71idWeRAG3bbpkf3AoY8KIqKqmnzJExi0uKxXpakQ5Pcbg==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@unrs/resolver-binding-linux-arm-musleabihf": { - "version": "1.7.11", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm-musleabihf/-/resolver-binding-linux-arm-musleabihf-1.7.11.tgz", - "integrity": "sha512-D/1F/2lTe+XAl3ohkYj51NjniVly8sIqkA/n1aOND3ZMO418nl2JNU95iVa1/RtpzaKcWEsNTtHRogykrUflJg==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@unrs/resolver-binding-linux-arm64-gnu": { - "version": "1.7.11", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm64-gnu/-/resolver-binding-linux-arm64-gnu-1.7.11.tgz", - "integrity": "sha512-7vFWHLCCNFLEQlmwKQfVy066ohLLArZl+AV/AdmrD1/pD1FlmqM+FKbtnONnIwbHtgetFUCV/SRi1q4D49aTlw==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@unrs/resolver-binding-linux-arm64-musl": { - "version": "1.7.11", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm64-musl/-/resolver-binding-linux-arm64-musl-1.7.11.tgz", - "integrity": "sha512-tYkGIx8hjWPh4zcn17jLEHU8YMmdP2obRTGkdaB3BguGHh31VCS3ywqC4QjTODjmhhNyZYkj/1Dz/+0kKvg9YA==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@unrs/resolver-binding-linux-ppc64-gnu": { - "version": "1.7.11", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-ppc64-gnu/-/resolver-binding-linux-ppc64-gnu-1.7.11.tgz", - "integrity": "sha512-6F328QIUev29vcZeRX6v6oqKxfUoGwIIAhWGD8wSysnBYFY0nivp25jdWmAb1GildbCCaQvOKEhCok7YfWkj4Q==", - "cpu": [ - "ppc64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@unrs/resolver-binding-linux-riscv64-gnu": { - "version": "1.7.11", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-riscv64-gnu/-/resolver-binding-linux-riscv64-gnu-1.7.11.tgz", - "integrity": "sha512-NqhWmiGJGdzbZbeucPZIG9Iav4lyYLCarEnxAceguMx9qlpeEF7ENqYKOwB8Zqk7/CeuYMEcLYMaW2li6HyDzQ==", - "cpu": [ - "riscv64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@unrs/resolver-binding-linux-riscv64-musl": { - "version": "1.7.11", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-riscv64-musl/-/resolver-binding-linux-riscv64-musl-1.7.11.tgz", - "integrity": "sha512-J2RPIFKMdTrLtBdfR1cUMKl8Gcy05nlQ+bEs/6al7EdWLk9cs3tnDREHZ7mV9uGbeghpjo4i8neNZNx3PYUY9w==", - "cpu": [ - "riscv64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@unrs/resolver-binding-linux-s390x-gnu": { - "version": "1.7.11", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-s390x-gnu/-/resolver-binding-linux-s390x-gnu-1.7.11.tgz", - "integrity": "sha512-bDpGRerHvvHdhun7MmFUNDpMiYcJSqWckwAVVRTJf8F+RyqYJOp/mx04PDc7DhpNPeWdnTMu91oZRMV+gGaVcQ==", - "cpu": [ - "s390x" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@unrs/resolver-binding-linux-x64-gnu": { - "version": "1.7.11", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-x64-gnu/-/resolver-binding-linux-x64-gnu-1.7.11.tgz", - "integrity": "sha512-G9U7bVmylzRLma3cK39RBm3guoD1HOvY4o0NS4JNm37AD0lS7/xyMt7kn0JejYyc0Im8J+rH69/dXGM9DAJcSQ==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@unrs/resolver-binding-linux-x64-musl": { - "version": "1.7.11", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-x64-musl/-/resolver-binding-linux-x64-musl-1.7.11.tgz", - "integrity": "sha512-7qL20SBKomekSunm7M9Fe5L93bFbn+FbHiGJbfTlp0RKhPVoJDP73vOxf1QrmJHyDPECsGWPFnKa/f8fO2FsHw==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@unrs/resolver-binding-wasm32-wasi": { - "version": "1.7.11", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-wasm32-wasi/-/resolver-binding-wasm32-wasi-1.7.11.tgz", - "integrity": "sha512-jisvIva8MidjI+B1lFRZZMfCPaCISePgTyR60wNT1MeQvIh5Ksa0G3gvI+Iqyj3jqYbvOHByenpa5eDGcSdoSg==", - "cpu": [ - "wasm32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "@napi-rs/wasm-runtime": "^0.2.10" - }, - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/@unrs/resolver-binding-win32-arm64-msvc": { - "version": "1.7.11", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-win32-arm64-msvc/-/resolver-binding-win32-arm64-msvc-1.7.11.tgz", - "integrity": "sha512-G+H5nQZ8sRZ8ebMY6mRGBBvTEzMYEcgVauLsNHpvTUavZoCCRVP1zWkCZgOju2dW3O22+8seTHniTdl1/uLz3g==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ] - }, - "node_modules/@unrs/resolver-binding-win32-ia32-msvc": { - "version": "1.7.11", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-win32-ia32-msvc/-/resolver-binding-win32-ia32-msvc-1.7.11.tgz", - "integrity": "sha512-Hfy46DBfFzyv0wgR0MMOwFFib2W2+Btc8oE5h4XlPhpelnSyA6nFxkVIyTgIXYGTdFaLoZFNn62fmqx3rjEg3A==", - "cpu": [ - "ia32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ] - }, - "node_modules/@unrs/resolver-binding-win32-x64-msvc": { - "version": "1.7.11", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-win32-x64-msvc/-/resolver-binding-win32-x64-msvc-1.7.11.tgz", - "integrity": "sha512-7L8NdsQlCJ8T106Gbz/AjzM4QKWVsoQbKpB9bMBGcIZswUuAnJMHpvbqGW3RBqLHCIwX4XZ5fxSBHEFcK2h9wA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ] - }, "node_modules/abab": { "version": "2.0.6", "resolved": "https://registry.npmjs.org/abab/-/abab-2.0.6.tgz", @@ -4850,9 +4877,9 @@ "license": "BSD-3-Clause" }, "node_modules/acorn": { - "version": "8.14.1", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.1.tgz", - "integrity": "sha512-OvQ/2pUDKmgfCg++xsTX1wGxfTaszcHVcTctW4UJB4hibJx2HXxxO5UmVgyjMa+ZDsiaf5wWLXYpRWMmBI0QHg==", + "version": "8.15.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", + "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "dev": true, "license": "MIT", "bin": { @@ -5006,19 +5033,6 @@ "node": ">= 8" } }, - "node_modules/append-transform": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/append-transform/-/append-transform-2.0.0.tgz", - "integrity": "sha512-7yeyCEurROLQJFv5Xj4lEGTy0borxepjFv1g22oAdqFu//SrAlDl1O1Nxx15SH1RoliUml6p8dwJW9jvZughhg==", - "dev": true, - "license": "MIT", - "dependencies": { - "default-require-extensions": "^3.0.0" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/arch": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/arch/-/arch-2.2.0.tgz", @@ -5040,13 +5054,6 @@ ], "license": "MIT" }, - "node_modules/archy": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/archy/-/archy-1.0.0.tgz", - "integrity": "sha512-Xg+9RwCg/0p32teKdGMPTPnVXKD0w3DfHnFTficozsAgsvq2XenPJq/MYpzzQ/v8zrOyJn6Ds39VA4JIDwFfqw==", - "dev": true, - "license": "MIT" - }, "node_modules/argparse": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", @@ -5404,16 +5411,6 @@ "node": ">=8" } }, - "node_modules/babel-plugin-istanbul/node_modules/semver": { - "version": "6.3.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", - "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", - "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - } - }, "node_modules/babel-plugin-jest-hoist": { "version": "29.6.3", "resolved": "https://registry.npmjs.org/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-29.6.3.tgz", @@ -5476,16 +5473,6 @@ "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" } }, - "node_modules/babel-plugin-polyfill-corejs2/node_modules/semver": { - "version": "6.3.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", - "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", - "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - } - }, "node_modules/babel-plugin-polyfill-corejs3": { "version": "0.11.1", "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs3/-/babel-plugin-polyfill-corejs3-0.11.1.tgz", @@ -5746,61 +5733,6 @@ "node": ">=6" } }, - "node_modules/caching-transform": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/caching-transform/-/caching-transform-4.0.0.tgz", - "integrity": "sha512-kpqOvwXnjjN44D89K5ccQC+RUrsy7jB/XLlRrx0D7/2HNcTPqzsb6XgYoErwko6QsV184CA2YgS1fxDiiDZMWA==", - "dev": true, - "license": "MIT", - "dependencies": { - "hasha": "^5.0.0", - "make-dir": "^3.0.0", - "package-hash": "^4.0.0", - "write-file-atomic": "^3.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/caching-transform/node_modules/make-dir": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz", - "integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==", - "dev": true, - "license": "MIT", - "dependencies": { - "semver": "^6.0.0" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/caching-transform/node_modules/semver": { - "version": "6.3.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", - "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", - "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - } - }, - "node_modules/caching-transform/node_modules/write-file-atomic": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-3.0.3.tgz", - "integrity": "sha512-AvHcyZ5JnSfq3ioSyjrBkH9yW4m7Ayk8/9My/DD9onKeu/94fwrMocemO2QAJFAlnnDN+ZDS+ZjAR5ua1/PV/Q==", - "dev": true, - "license": "ISC", - "dependencies": { - "imurmurhash": "^0.1.4", - "is-typedarray": "^1.0.0", - "signal-exit": "^3.0.2", - "typedarray-to-buffer": "^3.1.5" - } - }, "node_modules/call-bind": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.8.tgz", @@ -5861,13 +5793,16 @@ } }, "node_modules/camelcase": { - "version": "5.3.1", - "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", - "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", + "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==", "dev": true, "license": "MIT", "engines": { - "node": ">=6" + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/caniuse-lite": { @@ -6146,13 +6081,6 @@ "node": ">=4.0.0" } }, - "node_modules/commondir": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/commondir/-/commondir-1.0.1.tgz", - "integrity": "sha512-W9pAhw0ja1Edb5GVdIF1mjZw/ASI0AlShXM83UUGe2DVr5TdAPEA1OA8m/g8zWp9x6On7gqufY+FatDbC3MDQg==", - "dev": true, - "license": "MIT" - }, "node_modules/concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", @@ -6161,20 +6089,19 @@ "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", - "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", - "dev": true, + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.9.0.tgz", + "integrity": "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==", "license": "MIT" }, "node_modules/core-js-compat": { - "version": "3.42.0", - "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.42.0.tgz", - "integrity": "sha512-bQasjMfyDGyaeWKBIu33lHh9qlSR0MFE/Nmc6nMjf/iU9b3rSMdAYz1Baxrv4lPdGUsTqZudHA4jIGSJy0SWZQ==", + "version": "3.43.0", + "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.43.0.tgz", + "integrity": "sha512-2GML2ZsCc5LR7hZYz4AXmjQw8zuy2T//2QntwdnpuYI7jteT6GVYJL7F6C2C57R7gSYrcqVW3lAALefdbhBLDA==", "dev": true, "license": "MIT", "dependencies": { - "browserslist": "^4.24.4" + "browserslist": "^4.25.0" }, "funding": { "type": "opencollective", @@ -6237,6 +6164,16 @@ "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, + "node_modules/cross-fetch": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/cross-fetch/-/cross-fetch-4.1.0.tgz", + "integrity": "sha512-uKm5PU+MHTootlWEY+mZ4vvXoCn4fLQxT9dSc1sXVMSFkINTJVN8cAQROpwcKm8bJ/c7rgZVIBWzH5T78sNZZw==", + "dev": true, + "license": "MIT", + "dependencies": { + "node-fetch": "^2.7.0" + } + }, "node_modules/cross-spawn": { "version": "7.0.6", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", @@ -6431,6 +6368,19 @@ "node": "^16.0.0 || ^18.0.0 || >=20.0.0" } }, + "node_modules/cypress/node_modules/semver": { + "version": "7.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", + "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/cypress/node_modules/supports-color": { "version": "8.1.1", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", @@ -6447,6 +6397,134 @@ "url": "https://github.com/chalk/supports-color?sponsor=1" } }, + "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-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-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-format": { + "version": "1.4.5", + "resolved": "https://registry.npmjs.org/d3-format/-/d3-format-1.4.5.tgz", + "integrity": "sha512-J0piedu6Z8iB6TbIGfZgDzfXxUFN3qQRMofy2oPdXzQibYGqPB/9iMcxr/TGalU+2RsyDO+U4f33id8tbnSRMQ==", + "license": "BSD-3-Clause" + }, + "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-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-scale/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-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": "1.1.0", + "resolved": "https://registry.npmjs.org/d3-time/-/d3-time-1.1.0.tgz", + "integrity": "sha512-Xh0isrZ5rPYYdqhAVk8VLnMEidhz5aP7htAADH6MfzgmmicPkTo8LhkLxci61/lCB7n7UmE3bN0leRt+qvkLxA==", + "license": "BSD-3-Clause" + }, + "node_modules/d3-time-format": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-time-format/-/d3-time-format-3.0.0.tgz", + "integrity": "sha512-UXJh6EKsHBTjopVqZBhFysQcoXSv/5yLONZvkQ5Kk3qbwiUYkdX17Xa1PT6U1ZWXGGfB1ey5L8dKMlFq2DO0Ag==", + "license": "BSD-3-Clause", + "dependencies": { + "d3-time": "1 - 2" + } + }, "node_modules/damerau-levenshtein": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/damerau-levenshtein/-/damerau-levenshtein-1.0.8.tgz", @@ -6560,16 +6638,6 @@ } } }, - "node_modules/decamelize": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", - "integrity": "sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/decimal.js": { "version": "10.5.0", "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.5.0.tgz", @@ -6604,25 +6672,9 @@ "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==", "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/default-require-extensions": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/default-require-extensions/-/default-require-extensions-3.0.1.tgz", - "integrity": "sha512-eXTJmRbm2TIt9MgWTsOH1wEuhew6XGZcMeGKCtLedIg/NCsg1iBePXkceTdK4Fii7pzmN9tGsZhKzZ4h7O/fxw==", - "dev": true, - "license": "MIT", - "dependencies": { - "strip-bom": "^4.0.0" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "license": "MIT", + "engines": { + "node": ">=0.10.0" } }, "node_modules/define-data-property": { @@ -6661,6 +6713,15 @@ "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", @@ -6756,19 +6817,6 @@ "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1" } }, - "node_modules/dom-serializer/node_modules/entities": { - "version": "4.5.0", - "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", - "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", - "dev": true, - "license": "BSD-2-Clause", - "engines": { - "node": ">=0.12" - }, - "funding": { - "url": "https://github.com/fb55/entities?sponsor=1" - } - }, "node_modules/domelementtype": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz", @@ -6865,9 +6913,9 @@ } }, "node_modules/electron-to-chromium": { - "version": "1.5.165", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.165.tgz", - "integrity": "sha512-naiMx1Z6Nb2TxPU6fiFrUrDTjyPMLdTtaOd2oLmG8zVSg2hCWGkhPyxwk+qRmZ1ytwVqUv0u7ZcDA5+ALhaUtw==", + "version": "1.5.166", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.166.tgz", + "integrity": "sha512-QPWqHL0BglzPYyJJ1zSSmwFFL6MFXhbACOCcsCdUMCkzPdS9/OIBVxg516X/Ado2qwAq8k0nJJ7phQPCqiaFAw==", "dev": true, "license": "ISC" }, @@ -6916,9 +6964,9 @@ } }, "node_modules/entities": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/entities/-/entities-6.0.0.tgz", - "integrity": "sha512-aKstq2TDOndCn4diEyp9Uq/Flu2i1GlLkc6XIDQSDMuaFE3OPW5OphLCyQ5SpSJZTb4reN+kTcYru5yIfXoRPw==", + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", + "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", "dev": true, "license": "BSD-2-Clause", "engines": { @@ -7114,13 +7162,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/es6-error": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/es6-error/-/es6-error-4.1.1.tgz", - "integrity": "sha512-Um/+FxMr9CISWh0bi5Zv0iOD+4cFh5qLeks1qhAopKVAJw3drgKbKySikp7wGhDL0HPeaja0P5ULZrxLkniUVg==", - "dev": true, - "license": "MIT" - }, "node_modules/escalade": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", @@ -7165,6 +7206,17 @@ "source-map": "~0.6.1" } }, + "node_modules/escodegen/node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "license": "BSD-3-Clause", + "optional": true, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/eslint": { "version": "9.28.0", "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.28.0.tgz", @@ -7254,6 +7306,16 @@ } } }, + "node_modules/eslint-config-next/node_modules/@next/eslint-plugin-next": { + "version": "15.3.2", + "resolved": "https://registry.npmjs.org/@next/eslint-plugin-next/-/eslint-plugin-next-15.3.2.tgz", + "integrity": "sha512-ijVRTXBgnHT33aWnDtmlG+LJD+5vhc9AKTJPquGG5NKXjpKNjc62woIhFtrAcWdBobt8kqjCoaJ0q6sDQoX7aQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-glob": "3.3.1" + } + }, "node_modules/eslint-config-next/node_modules/eslint-import-resolver-typescript": { "version": "3.10.1", "resolved": "https://registry.npmjs.org/eslint-import-resolver-typescript/-/eslint-import-resolver-typescript-3.10.1.tgz", @@ -7289,6 +7351,36 @@ } } }, + "node_modules/eslint-config-next/node_modules/fast-glob": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.1.tgz", + "integrity": "sha512-kNFPyjhh5cKjrUltxs+wFx+ZkbRaxxmZ+X0ZU31SOsxCEtP9VPgtq2teZw1DebupL5GmDaNQ6yKMMVcM41iqDg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.4" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/eslint-config-next/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/eslint-config-prettier": { "version": "8.10.0", "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-8.10.0.tgz", @@ -7456,20 +7548,10 @@ "ms": "^2.1.1" } }, - "node_modules/eslint-plugin-import/node_modules/semver": { - "version": "6.3.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", - "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", - "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - } - }, "node_modules/eslint-plugin-jest": { - "version": "28.12.0", - "resolved": "https://registry.npmjs.org/eslint-plugin-jest/-/eslint-plugin-jest-28.12.0.tgz", - "integrity": "sha512-J6zmDp8WiQ9tyvYXE+3RFy7/+l4hraWLzmsabYXyehkmmDd36qV4VQFc7XzcsD8C1PTNt646MSx25bO1mdd9Yw==", + "version": "28.13.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-jest/-/eslint-plugin-jest-28.13.0.tgz", + "integrity": "sha512-4AuBcFWOriOeEqy6s4Zup/dQ7E1EPTyyfDaMYmM2YP9xEWPWwK3yYifH1dzY6aHRvyx7y53qMSIyT5s+jrorsQ==", "dev": true, "license": "MIT", "dependencies": { @@ -7641,20 +7723,10 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/eslint-plugin-react/node_modules/semver": { - "version": "6.3.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", - "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", - "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - } - }, "node_modules/eslint-scope": { - "version": "8.3.0", - "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.3.0.tgz", - "integrity": "sha512-pUNxi75F8MJ/GdeKtVLSbYg4ZI34J6C0C7sbL4YOp2exGwen7ZsuBqKzUhXd0qMQ362yET3z+uPwKeg/0C2XCQ==", + "version": "8.4.0", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.4.0.tgz", + "integrity": "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==", "dev": true, "license": "BSD-2-Clause", "dependencies": { @@ -7682,9 +7754,9 @@ } }, "node_modules/eslint/node_modules/eslint-visitor-keys": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.0.tgz", - "integrity": "sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw==", + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", + "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", "dev": true, "license": "Apache-2.0", "engines": { @@ -7695,15 +7767,15 @@ } }, "node_modules/espree": { - "version": "10.3.0", - "resolved": "https://registry.npmjs.org/espree/-/espree-10.3.0.tgz", - "integrity": "sha512-0QYC8b24HWY8zjRnDTL6RiHfDbAWn63qb4LMj1Z4b076A4une81+z03Kg7l7mn/48PUTqoLptSXez8oknU8Clg==", + "version": "10.4.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-10.4.0.tgz", + "integrity": "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==", "dev": true, "license": "BSD-2-Clause", "dependencies": { - "acorn": "^8.14.0", + "acorn": "^8.15.0", "acorn-jsx": "^5.3.2", - "eslint-visitor-keys": "^4.2.0" + "eslint-visitor-keys": "^4.2.1" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -7713,9 +7785,9 @@ } }, "node_modules/espree/node_modules/eslint-visitor-keys": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.0.tgz", - "integrity": "sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw==", + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", + "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", "dev": true, "license": "Apache-2.0", "engines": { @@ -8097,6 +8169,15 @@ "node": ">=16.0.0" } }, + "node_modules/filesize": { + "version": "10.1.6", + "resolved": "https://registry.npmjs.org/filesize/-/filesize-10.1.6.tgz", + "integrity": "sha512-sJslQKU2uM33qH5nqewAwVB2QgR6w1aMNsYUp3aN5rMRyXEwJGmZvaWzeJFNTOXWlHQyBFCWrdj3fV/fsTOX8w==", + "license": "BSD-3-Clause", + "engines": { + "node": ">= 10.4.0" + } + }, "node_modules/fill-range": { "version": "7.1.1", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", @@ -8110,50 +8191,6 @@ "node": ">=8" } }, - "node_modules/find-cache-dir": { - "version": "3.3.2", - "resolved": "https://registry.npmjs.org/find-cache-dir/-/find-cache-dir-3.3.2.tgz", - "integrity": "sha512-wXZV5emFEjrridIgED11OoUKLxiYjAcqot/NJdAkOhlJ+vGzwhOAfcG5OX1jP+S0PcjEn8bdMJv+g2jwQ3Onig==", - "dev": true, - "license": "MIT", - "dependencies": { - "commondir": "^1.0.1", - "make-dir": "^3.0.2", - "pkg-dir": "^4.1.0" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/avajs/find-cache-dir?sponsor=1" - } - }, - "node_modules/find-cache-dir/node_modules/make-dir": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz", - "integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==", - "dev": true, - "license": "MIT", - "dependencies": { - "semver": "^6.0.0" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/find-cache-dir/node_modules/semver": { - "version": "6.3.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", - "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", - "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - } - }, "node_modules/find-root": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/find-root/-/find-root-1.1.0.tgz", @@ -8214,36 +8251,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/foreground-child": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", - "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==", - "dev": true, - "license": "ISC", - "dependencies": { - "cross-spawn": "^7.0.6", - "signal-exit": "^4.0.1" - }, - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/foreground-child/node_modules/signal-exit": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", - "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", - "dev": true, - "license": "ISC", - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, "node_modules/forever-agent": { "version": "0.6.1", "resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz", @@ -8255,41 +8262,67 @@ } }, "node_modules/form-data": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.2.tgz", - "integrity": "sha512-hGfm/slu0ZabnNt4oaRZ6uREyfCj6P4fT/n6A1rGV+Z0VdGXjfOhVUpkn6qVQONHGIFwmveGXyDs75+nr6FM8w==", + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.3.tgz", + "integrity": "sha512-qsITQPfmvMOSAdeyZ+12I1c+CKSstAFAwu+97zrnWAbIr5u8wfsExUzCesVLC8NgHuRUqNN4Zy6UPWUTRGslcA==", "dev": true, "license": "MIT", "dependencies": { "asynckit": "^0.4.0", "combined-stream": "^1.0.8", "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.2", "mime-types": "^2.1.12" }, "engines": { "node": ">= 6" } }, - "node_modules/fromentries": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/fromentries/-/fromentries-1.3.2.tgz", - "integrity": "sha512-cHEpEQHUg0f8XdtZCc2ZAhrHzKzT0MrFUTcvx+hfxYu7rGMDc5SKoXFh+n4YigxsHXRzc6OrCshdR1bWH6HHyg==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT" + "node_modules/framer-motion": { + "version": "4.1.17", + "resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-4.1.17.tgz", + "integrity": "sha512-thx1wvKzblzbs0XaK2X0G1JuwIdARcoNOW7VVwjO8BUltzXPyONGAElLu6CiCScsOQRI7FIk/45YTFtJw5Yozw==", + "license": "MIT", + "dependencies": { + "framesync": "5.3.0", + "hey-listen": "^1.0.8", + "popmotion": "9.3.6", + "style-value-types": "4.1.4", + "tslib": "^2.1.0" + }, + "optionalDependencies": { + "@emotion/is-prop-valid": "^0.8.2" + }, + "peerDependencies": { + "react": ">=16.8 || ^17.0.0", + "react-dom": ">=16.8 || ^17.0.0" + } + }, + "node_modules/framer-motion/node_modules/@emotion/is-prop-valid": { + "version": "0.8.8", + "resolved": "https://registry.npmjs.org/@emotion/is-prop-valid/-/is-prop-valid-0.8.8.tgz", + "integrity": "sha512-u5WtneEAr5IDG2Wv65yhunPSMLIpuKsbuOktRojfrEiEvRyC85LgPMZI63cr7NUqT8ZIGdSVg8ZKGxIug4lXcA==", + "license": "MIT", + "optional": true, + "dependencies": { + "@emotion/memoize": "0.7.4" + } + }, + "node_modules/framer-motion/node_modules/@emotion/memoize": { + "version": "0.7.4", + "resolved": "https://registry.npmjs.org/@emotion/memoize/-/memoize-0.7.4.tgz", + "integrity": "sha512-Ja/Vfqe3HpuzRsG1oBtWTHk2PGZ7GR+2Vz5iYGelAw8dx32K0y7PjVuxK6z1nMpZOqAFsRUPCkK1YjJ56qJlgw==", + "license": "MIT", + "optional": true + }, + "node_modules/framesync": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/framesync/-/framesync-5.3.0.tgz", + "integrity": "sha512-oc5m68HDO/tuK2blj7ZcdEBRx3p1PjrgHazL8GYEpvULhrtGIFbQArN6cQS2QhW8mitffaB+VYzMjDqBxxQeoA==", + "license": "MIT", + "dependencies": { + "tslib": "^2.1.0" + } }, "node_modules/fs-extra": { "version": "9.1.0", @@ -8557,9 +8590,9 @@ } }, "node_modules/globals": { - "version": "14.0.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", - "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==", + "version": "16.2.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-16.2.0.tgz", + "integrity": "sha512-O+7l9tPdHCU320IigZZPj5zmRCFG9xHmx9cU8FqU2Rp+JN714seHV+2S9+JslCpY4gJwU2vOGox0wzgae/MCEg==", "dev": true, "license": "MIT", "engines": { @@ -8694,33 +8727,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/hasha": { - "version": "5.2.2", - "resolved": "https://registry.npmjs.org/hasha/-/hasha-5.2.2.tgz", - "integrity": "sha512-Hrp5vIK/xr5SkeN2onO32H0MgNZ0f17HRNH39WfL0SYUNOTZ5Lz1TJ8Pajo/87dYGEFlLMm7mIc/k/s6Bvz9HQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "is-stream": "^2.0.0", - "type-fest": "^0.8.0" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/hasha/node_modules/type-fest": { - "version": "0.8.1", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.8.1.tgz", - "integrity": "sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA==", - "dev": true, - "license": "(MIT OR CC0-1.0)", - "engines": { - "node": ">=8" - } - }, "node_modules/hasown": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", @@ -8733,6 +8739,12 @@ "node": ">= 0.4" } }, + "node_modules/hey-listen": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/hey-listen/-/hey-listen-1.0.8.tgz", + "integrity": "sha512-COpmrF2NOg4TBWUJ5UVyaCU2A88wEMkUPK4hNqyCkqHbxT92BbvfjoSozkAIIm6XhicGlJHhFdullInrdhwU8Q==", + "license": "MIT" + }, "node_modules/hoist-non-react-statics": { "version": "3.3.2", "resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz", @@ -8992,6 +9004,15 @@ "node": ">= 0.4" } }, + "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/is-array-buffer": { "version": "3.0.5", "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.5.tgz", @@ -9079,6 +9100,19 @@ "semver": "^7.7.1" } }, + "node_modules/is-bun-module/node_modules/semver": { + "version": "7.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", + "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/is-callable": { "version": "1.2.7", "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz", @@ -9485,16 +9519,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/is-windows": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-windows/-/is-windows-1.0.2.tgz", - "integrity": "sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/isarray": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", @@ -9519,22 +9543,9 @@ "node_modules/istanbul-lib-coverage": { "version": "3.2.2", "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz", - "integrity": "sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==", - "dev": true, - "license": "BSD-3-Clause", - "engines": { - "node": ">=8" - } - }, - "node_modules/istanbul-lib-hook": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/istanbul-lib-hook/-/istanbul-lib-hook-3.0.0.tgz", - "integrity": "sha512-Pt/uge1Q9s+5VAZ+pCo16TYMWPBIl+oaNIjgLQxcX0itS6ueeaA+pEfThZpH8WxhFgCiEb8sAJY6MdUKgiIWaQ==", + "integrity": "sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==", "dev": true, "license": "BSD-3-Clause", - "dependencies": { - "append-transform": "^2.0.0" - }, "engines": { "node": ">=8" } @@ -9556,35 +9567,17 @@ "node": ">=10" } }, - "node_modules/istanbul-lib-processinfo": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/istanbul-lib-processinfo/-/istanbul-lib-processinfo-2.0.3.tgz", - "integrity": "sha512-NkwHbo3E00oybX6NGJi6ar0B29vxyvNwoC7eJ4G4Yq28UfY758Hgn/heV8VRFhevPED4LXfFz0DQ8z/0kw9zMg==", + "node_modules/istanbul-lib-instrument/node_modules/semver": { + "version": "7.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", + "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==", "dev": true, "license": "ISC", - "dependencies": { - "archy": "^1.0.0", - "cross-spawn": "^7.0.3", - "istanbul-lib-coverage": "^3.2.0", - "p-map": "^3.0.0", - "rimraf": "^3.0.0", - "uuid": "^8.3.2" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/istanbul-lib-processinfo/node_modules/p-map": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/p-map/-/p-map-3.0.0.tgz", - "integrity": "sha512-d3qXVTF/s+W+CdJ5A29wywV2n8CQQYahlgz2bFiA+4eVNJbHJodPZ+/gXwPGh0bOqA+j8S+6+ckmvLGPk1QpxQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "aggregate-error": "^3.0.0" + "bin": { + "semver": "bin/semver.js" }, "engines": { - "node": ">=8" + "node": ">=10" } }, "node_modules/istanbul-lib-report": { @@ -9617,6 +9610,16 @@ "node": ">=10" } }, + "node_modules/istanbul-lib-source-maps/node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/istanbul-reports": { "version": "3.1.7", "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.1.7.tgz", @@ -10567,6 +10570,19 @@ "dev": true, "license": "MIT" }, + "node_modules/jest-snapshot/node_modules/semver": { + "version": "7.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", + "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/jest-transform-stub": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/jest-transform-stub/-/jest-transform-stub-2.0.0.tgz", @@ -10639,19 +10655,6 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, - "node_modules/jest-validate/node_modules/camelcase": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", - "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/jest-validate/node_modules/pretty-format": { "version": "29.7.0", "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", @@ -11063,7 +11066,6 @@ "version": "4.17.21", "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", - "dev": true, "license": "MIT" }, "node_modules/lodash.debounce": { @@ -11073,13 +11075,6 @@ "dev": true, "license": "MIT" }, - "node_modules/lodash.flattendeep": { - "version": "4.4.0", - "resolved": "https://registry.npmjs.org/lodash.flattendeep/-/lodash.flattendeep-4.4.0.tgz", - "integrity": "sha512-uHaJFihxmJcEX3kT4I23ABqKKalJ/zDrDg0lsFtc1h+3uw49SIJ5beyhx5ExVRti3AvKoOJngIj7xz3oylPdWQ==", - "dev": true, - "license": "MIT" - }, "node_modules/lodash.merge": { "version": "4.6.2", "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", @@ -11222,6 +11217,19 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/make-dir/node_modules/semver": { + "version": "7.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", + "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/makeerror": { "version": "1.0.12", "resolved": "https://registry.npmjs.org/makeerror/-/makeerror-1.0.12.tgz", @@ -11468,312 +11476,152 @@ } } }, - "node_modules/no-case": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/no-case/-/no-case-3.0.4.tgz", - "integrity": "sha512-fgAN3jGAh+RoxUGZHTSOLJIqUc2wmoBwGR4tbpNAKmmovFoWq0OdRkb0VkldReO2a2iBT/OEulG9XSUc10r3zg==", - "dev": true, - "license": "MIT", - "dependencies": { - "lower-case": "^2.0.2", - "tslib": "^2.0.3" - } - }, - "node_modules/node-int64": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz", - "integrity": "sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw==", - "dev": true, - "license": "MIT" - }, - "node_modules/node-preload": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/node-preload/-/node-preload-0.2.1.tgz", - "integrity": "sha512-RM5oyBy45cLEoHqCeh+MNuFAxO0vTFBLskvQbOKnEE7YTTSN4tbN8QWDIPQ6L+WvKsB/qLEGpYe2ZZ9d4W9OIQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "process-on-spawn": "^1.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/node-releases": { - "version": "2.0.19", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.19.tgz", - "integrity": "sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw==", - "dev": true, - "license": "MIT" - }, - "node_modules/normalize-path": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", - "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/npm-run-path": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz", - "integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==", - "dev": true, + "node_modules/next/node_modules/@next/swc-linux-x64-gnu": { + "version": "15.3.2", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-15.3.2.tgz", + "integrity": "sha512-uRBo6THWei0chz+Y5j37qzx+BtoDRFIkDzZjlpCItBRXyMPIg079eIkOCl3aqr2tkxL4HFyJ4GHDes7W8HuAUg==", + "cpu": [ + "x64" + ], "license": "MIT", - "dependencies": { - "path-key": "^3.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/nth-check": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz", - "integrity": "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "boolbase": "^1.0.0" - }, - "funding": { - "url": "https://github.com/fb55/nth-check?sponsor=1" - } - }, - "node_modules/nwsapi": { - "version": "2.2.20", - "resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.20.tgz", - "integrity": "sha512-/ieB+mDe4MrrKMT8z+mQL8klXydZWGR5Dowt4RAGKbJ3kIGEx3X4ljUo+6V73IXtUPWgfOlU5B9MlGxFO5T+cA==", - "dev": true, - "license": "MIT" - }, - "node_modules/nyc": { - "version": "17.1.0", - "resolved": "https://registry.npmjs.org/nyc/-/nyc-17.1.0.tgz", - "integrity": "sha512-U42vQ4czpKa0QdI1hu950XuNhYqgoM+ZF1HT+VuUHL9hPfDPVvNQyltmMqdE9bUHMVa+8yNbc3QKTj8zQhlVxQ==", - "dev": true, - "license": "ISC", - "dependencies": { - "@istanbuljs/load-nyc-config": "^1.0.0", - "@istanbuljs/schema": "^0.1.2", - "caching-transform": "^4.0.0", - "convert-source-map": "^1.7.0", - "decamelize": "^1.2.0", - "find-cache-dir": "^3.2.0", - "find-up": "^4.1.0", - "foreground-child": "^3.3.0", - "get-package-type": "^0.1.0", - "glob": "^7.1.6", - "istanbul-lib-coverage": "^3.0.0", - "istanbul-lib-hook": "^3.0.0", - "istanbul-lib-instrument": "^6.0.2", - "istanbul-lib-processinfo": "^2.0.2", - "istanbul-lib-report": "^3.0.0", - "istanbul-lib-source-maps": "^4.0.0", - "istanbul-reports": "^3.0.2", - "make-dir": "^3.0.0", - "node-preload": "^0.2.1", - "p-map": "^3.0.0", - "process-on-spawn": "^1.0.0", - "resolve-from": "^5.0.0", - "rimraf": "^3.0.0", - "signal-exit": "^3.0.2", - "spawn-wrap": "^2.0.0", - "test-exclude": "^6.0.0", - "yargs": "^15.0.2" - }, - "bin": { - "nyc": "bin/nyc.js" - }, + "optional": true, + "os": [ + "linux" + ], "engines": { - "node": ">=18" - } - }, - "node_modules/nyc/node_modules/cliui": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/cliui/-/cliui-6.0.0.tgz", - "integrity": "sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ==", - "dev": true, - "license": "ISC", - "dependencies": { - "string-width": "^4.2.0", - "strip-ansi": "^6.0.0", - "wrap-ansi": "^6.2.0" + "node": ">= 10" } }, - "node_modules/nyc/node_modules/convert-source-map": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.9.0.tgz", - "integrity": "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==", - "dev": true, - "license": "MIT" - }, - "node_modules/nyc/node_modules/find-up": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", - "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", - "dev": true, + "node_modules/next/node_modules/@next/swc-linux-x64-musl": { + "version": "15.3.2", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-15.3.2.tgz", + "integrity": "sha512-+uxFlPuCNx/T9PdMClOqeE8USKzj8tVz37KflT3Kdbx/LOlZBRI2yxuIcmx1mPNK8DwSOMNCr4ureSet7eyC0w==", + "cpu": [ + "x64" + ], "license": "MIT", - "dependencies": { - "locate-path": "^5.0.0", - "path-exists": "^4.0.0" - }, + "optional": true, + "os": [ + "linux" + ], "engines": { - "node": ">=8" + "node": ">= 10" } }, - "node_modules/nyc/node_modules/locate-path": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", - "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "node_modules/no-case": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/no-case/-/no-case-3.0.4.tgz", + "integrity": "sha512-fgAN3jGAh+RoxUGZHTSOLJIqUc2wmoBwGR4tbpNAKmmovFoWq0OdRkb0VkldReO2a2iBT/OEulG9XSUc10r3zg==", "dev": true, "license": "MIT", "dependencies": { - "p-locate": "^4.1.0" - }, - "engines": { - "node": ">=8" + "lower-case": "^2.0.2", + "tslib": "^2.0.3" } }, - "node_modules/nyc/node_modules/make-dir": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz", - "integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==", + "node_modules/node-fetch": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", + "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", "dev": true, "license": "MIT", "dependencies": { - "semver": "^6.0.0" + "whatwg-url": "^5.0.0" }, "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/nyc/node_modules/p-limit": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", - "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", - "dev": true, - "license": "MIT", - "dependencies": { - "p-try": "^2.0.0" + "node": "4.x || >=6.0.0" }, - "engines": { - "node": ">=6" + "peerDependencies": { + "encoding": "^0.1.0" }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "peerDependenciesMeta": { + "encoding": { + "optional": true + } } }, - "node_modules/nyc/node_modules/p-locate": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", - "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "node_modules/node-fetch/node_modules/tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", "dev": true, - "license": "MIT", - "dependencies": { - "p-limit": "^2.2.0" - }, - "engines": { - "node": ">=8" - } + "license": "MIT" }, - "node_modules/nyc/node_modules/p-map": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/p-map/-/p-map-3.0.0.tgz", - "integrity": "sha512-d3qXVTF/s+W+CdJ5A29wywV2n8CQQYahlgz2bFiA+4eVNJbHJodPZ+/gXwPGh0bOqA+j8S+6+ckmvLGPk1QpxQ==", + "node_modules/node-fetch/node_modules/webidl-conversions": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==", "dev": true, - "license": "MIT", - "dependencies": { - "aggregate-error": "^3.0.0" - }, - "engines": { - "node": ">=8" - } + "license": "BSD-2-Clause" }, - "node_modules/nyc/node_modules/resolve-from": { + "node_modules/node-fetch/node_modules/whatwg-url": { "version": "5.0.0", - "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", - "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", + "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", "dev": true, "license": "MIT", - "engines": { - "node": ">=8" + "dependencies": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" } }, - "node_modules/nyc/node_modules/semver": { - "version": "6.3.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", - "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "node_modules/node-int64": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz", + "integrity": "sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw==", "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - } + "license": "MIT" }, - "node_modules/nyc/node_modules/wrap-ansi": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz", - "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==", + "node_modules/node-releases": { + "version": "2.0.19", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.19.tgz", + "integrity": "sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw==", + "dev": true, + "license": "MIT" + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", "dev": true, "license": "MIT", - "dependencies": { - "ansi-styles": "^4.0.0", - "string-width": "^4.1.0", - "strip-ansi": "^6.0.0" - }, "engines": { - "node": ">=8" + "node": ">=0.10.0" } }, - "node_modules/nyc/node_modules/y18n": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.3.tgz", - "integrity": "sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ==", - "dev": true, - "license": "ISC" - }, - "node_modules/nyc/node_modules/yargs": { - "version": "15.4.1", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-15.4.1.tgz", - "integrity": "sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A==", + "node_modules/npm-run-path": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz", + "integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==", "dev": true, "license": "MIT", "dependencies": { - "cliui": "^6.0.0", - "decamelize": "^1.2.0", - "find-up": "^4.1.0", - "get-caller-file": "^2.0.1", - "require-directory": "^2.1.1", - "require-main-filename": "^2.0.0", - "set-blocking": "^2.0.0", - "string-width": "^4.2.0", - "which-module": "^2.0.0", - "y18n": "^4.0.0", - "yargs-parser": "^18.1.2" + "path-key": "^3.0.0" }, "engines": { "node": ">=8" } }, - "node_modules/nyc/node_modules/yargs-parser": { - "version": "18.1.3", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-18.1.3.tgz", - "integrity": "sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==", + "node_modules/nth-check": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz", + "integrity": "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==", "dev": true, - "license": "ISC", + "license": "BSD-2-Clause", "dependencies": { - "camelcase": "^5.0.0", - "decamelize": "^1.2.0" + "boolbase": "^1.0.0" }, - "engines": { - "node": ">=6" + "funding": { + "url": "https://github.com/fb55/nth-check?sponsor=1" } }, + "node_modules/nwsapi": { + "version": "2.2.20", + "resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.20.tgz", + "integrity": "sha512-/ieB+mDe4MrrKMT8z+mQL8klXydZWGR5Dowt4RAGKbJ3kIGEx3X4ljUo+6V73IXtUPWgfOlU5B9MlGxFO5T+cA==", + "dev": true, + "license": "MIT" + }, "node_modules/object-assign": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", @@ -12023,22 +11871,6 @@ "node": ">=6" } }, - "node_modules/package-hash": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/package-hash/-/package-hash-4.0.0.tgz", - "integrity": "sha512-whdkPIooSu/bASggZ96BWVvZTRMOFxnyUG5PnTSGKoJE2gd5mbVNmR2Nj20QFzxYYgAXpoqC+AiXzl+UMRh7zQ==", - "dev": true, - "license": "ISC", - "dependencies": { - "graceful-fs": "^4.1.15", - "hasha": "^5.0.0", - "lodash.flattendeep": "^4.4.0", - "release-zalgo": "^1.0.0" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/parent-module": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", @@ -12082,6 +11914,19 @@ "url": "https://github.com/inikulin/parse5?sponsor=1" } }, + "node_modules/parse5/node_modules/entities": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz", + "integrity": "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, "node_modules/path-exists": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", @@ -12249,6 +12094,18 @@ "node": ">=8" } }, + "node_modules/popmotion": { + "version": "9.3.6", + "resolved": "https://registry.npmjs.org/popmotion/-/popmotion-9.3.6.tgz", + "integrity": "sha512-ZTbXiu6zIggXzIliMi8LGxXBF5ST+wkpXGEjeTUDUOCdSQ356hij/xjeUdv0F8zCQNeqB1+PR5/BB+gC+QLAPw==", + "license": "MIT", + "dependencies": { + "framesync": "5.3.0", + "hey-listen": "^1.0.8", + "style-value-types": "4.1.4", + "tslib": "^2.1.0" + } + }, "node_modules/possible-typed-array-names": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz", @@ -12367,6 +12224,13 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, + "node_modules/pretty-format/node_modules/react-is": { + "version": "17.0.2", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", + "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==", + "dev": true, + "license": "MIT" + }, "node_modules/process": { "version": "0.11.10", "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz", @@ -12377,19 +12241,6 @@ "node": ">= 0.6.0" } }, - "node_modules/process-on-spawn": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/process-on-spawn/-/process-on-spawn-1.1.0.tgz", - "integrity": "sha512-JOnOPQ/8TZgjs1JIH/m9ni7FfimjNa/PRx7y/Wb5qdItsnhO0jE4AT7fC0HjC28DUQWDr50dwSYZLdRMlqDq3Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "fromentries": "^1.2.0" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/prompts": { "version": "2.4.2", "resolved": "https://registry.npmjs.org/prompts/-/prompts-2.4.2.tgz", @@ -12549,12 +12400,34 @@ } }, "node_modules/react-is": { - "version": "17.0.2", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", - "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==", - "dev": true, + "version": "19.1.0", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-19.1.0.tgz", + "integrity": "sha512-Oe56aUPnkHyyDxxkvqtd7KkdQP5uIUfHxd5XTb3wE9d/kRnZLmKbDB0GWk919tdQ+mxxPtG6EAs6RMT6i1qtHg==", "license": "MIT" }, + "node_modules/react-material-ui-carousel": { + "version": "3.4.2", + "resolved": "https://registry.npmjs.org/react-material-ui-carousel/-/react-material-ui-carousel-3.4.2.tgz", + "integrity": "sha512-jUbC5aBWqbbbUOOdUe3zTVf4kMiZFwKJqwhxzHgBfklaXQbSopis4iWAHvEOLcZtSIJk4JAGxKE0CmxDoxvUuw==", + "license": "MIT", + "dependencies": { + "@emotion/react": "^11.7.1", + "@emotion/styled": "^11.6.0", + "@mui/icons-material": "^5.4.1", + "@mui/material": "^5.4.1", + "@mui/system": "^5.4.1", + "framer-motion": "^4.1.17" + }, + "peerDependencies": { + "@emotion/react": "^11.4.1", + "@emotion/styled": "^11.3.0", + "@mui/icons-material": "^5.0.0", + "@mui/material": "^5.0.0", + "@mui/system": "^5.0.0", + "react": "^17.0.1 || ^18.0.0", + "react-dom": "^17.0.2 || ^18.0.0" + } + }, "node_modules/react-redux": { "version": "9.2.0", "resolved": "https://registry.npmjs.org/react-redux/-/react-redux-9.2.0.tgz", @@ -12738,19 +12611,6 @@ "node": ">=6" } }, - "node_modules/release-zalgo": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/release-zalgo/-/release-zalgo-1.0.0.tgz", - "integrity": "sha512-gUAyHVHPPC5wdqX/LG4LWtRYtgjxyX78oanFNTMMyFEfOqdC54s3eE82imuWKbOeqYht2CrNf64Qb8vgmmtZGA==", - "dev": true, - "license": "ISC", - "dependencies": { - "es6-error": "^4.0.1" - }, - "engines": { - "node": ">=4" - } - }, "node_modules/request-progress": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/request-progress/-/request-progress-3.0.0.tgz", @@ -12771,13 +12631,6 @@ "node": ">=0.10.0" } }, - "node_modules/require-main-filename": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz", - "integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==", - "dev": true, - "license": "ISC" - }, "node_modules/requires-port": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz", @@ -12895,22 +12748,11 @@ "dev": true, "license": "MIT" }, - "node_modules/rimraf": { + "node_modules/robust-predicates": { "version": "3.0.2", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", - "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", - "deprecated": "Rimraf versions prior to v4 are no longer supported", - "dev": true, - "license": "ISC", - "dependencies": { - "glob": "^7.1.3" - }, - "bin": { - "rimraf": "bin.js" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } + "resolved": "https://registry.npmjs.org/robust-predicates/-/robust-predicates-3.0.2.tgz", + "integrity": "sha512-IXgzBWvWQwE6PrDI05OvmXUIruQTcoMDzRsOd5CDvHCVLcLHMTSYvOK5Cm46kWqlV3yAbuSpBZdJ5oP5OUoStg==", + "license": "Unlicense" }, "node_modules/run-parallel": { "version": "1.2.0", @@ -13052,25 +12894,15 @@ } }, "node_modules/semver": { - "version": "7.7.2", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", - "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==", - "devOptional": true, + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, "license": "ISC", "bin": { "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" } }, - "node_modules/set-blocking": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", - "integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==", - "dev": true, - "license": "ISC" - }, "node_modules/set-function-length": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", @@ -13162,6 +12994,19 @@ "@img/sharp-win32-x64": "0.34.2" } }, + "node_modules/sharp/node_modules/semver": { + "version": "7.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", + "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==", + "license": "ISC", + "optional": true, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/shebang-command": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", @@ -13329,10 +13174,9 @@ } }, "node_modules/source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "dev": true, + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", + "integrity": "sha512-LbrmJOMUSdEVxIKvdcJzQC+nQhe8FUZQTXQy6+I75skNgn3OoQ0DZA8YnFa7gp8tqtL3KPf1kmo0R5DoApeSGQ==", "license": "BSD-3-Clause", "engines": { "node": ">=0.10.0" @@ -13358,62 +13202,14 @@ "source-map": "^0.6.0" } }, - "node_modules/spawn-wrap": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/spawn-wrap/-/spawn-wrap-2.0.0.tgz", - "integrity": "sha512-EeajNjfN9zMnULLwhZZQU3GWBoFNkbngTUPfaawT4RkMiviTxcX0qfhVbGey39mfctfDHkWtuecgQ8NJcyQWHg==", - "dev": true, - "license": "ISC", - "dependencies": { - "foreground-child": "^2.0.0", - "is-windows": "^1.0.2", - "make-dir": "^3.0.0", - "rimraf": "^3.0.0", - "signal-exit": "^3.0.2", - "which": "^2.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/spawn-wrap/node_modules/foreground-child": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-2.0.0.tgz", - "integrity": "sha512-dCIq9FpEcyQyXKCkyzmlPTFNgrCzPudOe+mhvJU5zAtlBnGVy2yKxtfsxK2tQBThwq225jcvBjpw1Gr40uzZCA==", - "dev": true, - "license": "ISC", - "dependencies": { - "cross-spawn": "^7.0.0", - "signal-exit": "^3.0.2" - }, - "engines": { - "node": ">=8.0.0" - } - }, - "node_modules/spawn-wrap/node_modules/make-dir": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz", - "integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==", + "node_modules/source-map-support/node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", "dev": true, - "license": "MIT", - "dependencies": { - "semver": "^6.0.0" - }, + "license": "BSD-3-Clause", "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/spawn-wrap/node_modules/semver": { - "version": "6.3.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", - "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", - "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver.js" + "node": ">=0.10.0" } }, "node_modules/sprintf-js": { @@ -13719,6 +13515,16 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/style-value-types": { + "version": "4.1.4", + "resolved": "https://registry.npmjs.org/style-value-types/-/style-value-types-4.1.4.tgz", + "integrity": "sha512-LCJL6tB+vPSUoxgUBt9juXIlNJHtBMy8jkXzUJSBzeHWdBu6lhzHqCvLVkXFGsFIlNa2ln1sQHya/gzaFmB2Lg==", + "license": "MIT", + "dependencies": { + "hey-listen": "^1.0.8", + "tslib": "^2.1.0" + } + }, "node_modules/styled-jsx": { "version": "5.1.6", "resolved": "https://registry.npmjs.org/styled-jsx/-/styled-jsx-5.1.6.tgz", @@ -14191,16 +13997,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/typedarray-to-buffer": { - "version": "3.1.5", - "resolved": "https://registry.npmjs.org/typedarray-to-buffer/-/typedarray-to-buffer-3.1.5.tgz", - "integrity": "sha512-zdu8XMNEDepKKR+XYOXAVPtWui0ly0NtohUscw+UmaHiAWT8hrV1rr//H6V+0DvJ3OQ19S979M0laLfX8rm82Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "is-typedarray": "^1.0.0" - } - }, "node_modules/typescript": { "version": "5.8.3", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz", @@ -14296,9 +14092,9 @@ } }, "node_modules/unrs-resolver": { - "version": "1.7.11", - "resolved": "https://registry.npmjs.org/unrs-resolver/-/unrs-resolver-1.7.11.tgz", - "integrity": "sha512-OhuAzBImFPjKNgZ2JwHMfGFUA6NSbRegd1+BPjC1Y0E6X9Y/vJ4zKeGmIMqmlYboj6cMNEwKI+xQisrg4J0HaQ==", + "version": "1.7.12", + "resolved": "https://registry.npmjs.org/unrs-resolver/-/unrs-resolver-1.7.12.tgz", + "integrity": "sha512-pfcdDxrVoUc5ZB3VCVJNSWbs63lgQVYLVw4k/rCr8Smi/V2Sxi1odEckVq6Zf803OtbYia1+YpiGCZoODfWLsQ==", "dev": true, "hasInstallScript": true, "license": "MIT", @@ -14309,23 +14105,23 @@ "url": "https://opencollective.com/unrs-resolver" }, "optionalDependencies": { - "@unrs/resolver-binding-darwin-arm64": "1.7.11", - "@unrs/resolver-binding-darwin-x64": "1.7.11", - "@unrs/resolver-binding-freebsd-x64": "1.7.11", - "@unrs/resolver-binding-linux-arm-gnueabihf": "1.7.11", - "@unrs/resolver-binding-linux-arm-musleabihf": "1.7.11", - "@unrs/resolver-binding-linux-arm64-gnu": "1.7.11", - "@unrs/resolver-binding-linux-arm64-musl": "1.7.11", - "@unrs/resolver-binding-linux-ppc64-gnu": "1.7.11", - "@unrs/resolver-binding-linux-riscv64-gnu": "1.7.11", - "@unrs/resolver-binding-linux-riscv64-musl": "1.7.11", - "@unrs/resolver-binding-linux-s390x-gnu": "1.7.11", - "@unrs/resolver-binding-linux-x64-gnu": "1.7.11", - "@unrs/resolver-binding-linux-x64-musl": "1.7.11", - "@unrs/resolver-binding-wasm32-wasi": "1.7.11", - "@unrs/resolver-binding-win32-arm64-msvc": "1.7.11", - "@unrs/resolver-binding-win32-ia32-msvc": "1.7.11", - "@unrs/resolver-binding-win32-x64-msvc": "1.7.11" + "@unrs/resolver-binding-darwin-arm64": "1.7.12", + "@unrs/resolver-binding-darwin-x64": "1.7.12", + "@unrs/resolver-binding-freebsd-x64": "1.7.12", + "@unrs/resolver-binding-linux-arm-gnueabihf": "1.7.12", + "@unrs/resolver-binding-linux-arm-musleabihf": "1.7.12", + "@unrs/resolver-binding-linux-arm64-gnu": "1.7.12", + "@unrs/resolver-binding-linux-arm64-musl": "1.7.12", + "@unrs/resolver-binding-linux-ppc64-gnu": "1.7.12", + "@unrs/resolver-binding-linux-riscv64-gnu": "1.7.12", + "@unrs/resolver-binding-linux-riscv64-musl": "1.7.12", + "@unrs/resolver-binding-linux-s390x-gnu": "1.7.12", + "@unrs/resolver-binding-linux-x64-gnu": "1.7.12", + "@unrs/resolver-binding-linux-x64-musl": "1.7.12", + "@unrs/resolver-binding-wasm32-wasi": "1.7.12", + "@unrs/resolver-binding-win32-arm64-msvc": "1.7.12", + "@unrs/resolver-binding-win32-ia32-msvc": "1.7.12", + "@unrs/resolver-binding-win32-x64-msvc": "1.7.12" } }, "node_modules/untildify": { @@ -14424,6 +14220,13 @@ "node": ">=10.12.0" } }, + "node_modules/v8-to-istanbul/node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "license": "MIT" + }, "node_modules/verror": { "version": "1.10.0", "resolved": "https://registry.npmjs.org/verror/-/verror-1.10.0.tgz", @@ -14592,13 +14395,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/which-module": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.1.tgz", - "integrity": "sha512-iBdZ57RDvnOR9AGBhML2vFZf7h8vmBjhoaZqODJBFWHVtKkDmKuHai3cx5PgVMrX5YDNp27AofYbAwctSS+vhQ==", - "dev": true, - "license": "ISC" - }, "node_modules/which-typed-array": { "version": "1.1.19", "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.19.tgz", diff --git a/package.json b/package.json index f9fa04ee..958d3491 100644 --- a/package.json +++ b/package.json @@ -1,41 +1,45 @@ { "name": "guidellm", "version": "0.1.0", + "type": "module", "scripts": { "dev": "next dev src/ui", "build": "next build src/ui", "lint": "next lint src/ui", "type-check": "tsc -p src/ui/tsconfig.json --noEmit && tsc -p tsconfig.test.json --noEmit && tsc -p tsconfig.cypress.json --noEmit", "format": "prettier --write .", - "prepare": "husky install", - "test": "jest --config jest.config.js tests/ui", - "test:unit": "jest --config jest.config.js --coverage --coverageDirectory=coverage/unit tests/ui/unit", - "test:integration": "jest --config jest.config.js --coverage --coverageDirectory=coverage/integration tests/ui/integration", + "prepare": "husky", + "test": "jest --config jest.config.cjs tests/ui", + "test:watch": "jest --watch tests/ui", + "test:unit": "jest --config jest.config.cjs tests/ui/unit", + "test:integration": "jest --config jest.config.cjs tests/ui/integration", "test:e2e": "cypress run --headless", - "coverage:merge": "nyc merge coverage/unit > coverage/tmp.json && nyc merge coverage/integration coverage/tmp.json && mv coverage/tmp.json coverage/coverage-final.json", - "coverage:collect": - "mkdir -p .nyc_output && cp coverage/coverage-final.json .nyc_output/merged.json", - "coverage:report": - "nyc report --reporter=json-summary --reporter=lcov --report-dir=coverage", - "coverage:badge": - "jest-coverage-badges --input coverage/coverage-summary.json --output coverage/.coverage" + "coverage": "jest --config jest.config.cjs --coverage tests/ui", + "coverage:badge": "jest-coverage-badges --input coverage/coverage-summary.json --output coverage/.coverage" }, - "dependencies": { "@emotion/cache": "^11.13.1", "@emotion/react": "^11.14.0", "@emotion/styled": "^11.14.0", "@mui/material": "^5.11.7", "@mui/material-nextjs": "^5.16.6", + "@nivo/bar": "^0.88.0", + "@nivo/core": "^0.88.0", + "@nivo/line": "^0.88.0", + "@nivo/scales": "^0.88.0", + "@nivo/tooltip": "^0.88.0", "@reduxjs/toolkit": "^2.2.7", + "filesize": "^10.1.6", "next": "15.3.2", "react": "^18.2.0", "react-dom": "^18.2.0", + "react-material-ui-carousel": "^3.4.2", "react-redux": "^9.1.2" }, "devDependencies": { "@eslint/eslintrc": "^3", "@mui/types": "^7.2.14", + "@next/eslint-plugin-next": "^15.3.3", "@svgr/webpack": "^8.1.0", "@testing-library/jest-dom": "^5.16.5", "@testing-library/react": "^16.0.0", @@ -47,6 +51,7 @@ "@types/testing-library__jest-dom": "^5.14.9", "@typescript-eslint/eslint-plugin": "^8.33.1", "@typescript-eslint/parser": "^8.33.1", + "cross-fetch": "^4.1.0", "cypress": "^13.13.3", "eslint": "^9.0.0", "eslint-config-next": "15.3.2", @@ -59,16 +64,20 @@ "eslint-plugin-prettier": "^5.4.0", "eslint-plugin-react": "^7.31.10", "eslint-plugin-react-hooks": "^5.2.0", + "globals": "^16.2.0", "husky": "^9.1.7", "jest": "^29.7.0", "jest-coverage-badges": "^1.1.2", "jest-environment-jsdom": "^29.7.0", "jest-runner-groups": "^2.2.0", "jest-transform-stub": "^2.0.0", - "nyc": "^17.1.0", "prettier": "^3.5.3", "typescript": "^5" }, + "optionalDependencies": { + "@next/swc-linux-x64-gnu": "^15.3.3", + "@next/swc-linux-x64-musl": "^15.3.3" + }, "lint-staged": { "*.js": "eslint --cache --fix", "*.ts": "eslint --cache --fix", diff --git a/src/guidellm/benchmark/benchmark.py b/src/guidellm/benchmark/benchmark.py index 9f683f8e..1e2a5f4b 100644 --- a/src/guidellm/benchmark/benchmark.py +++ b/src/guidellm/benchmark/benchmark.py @@ -815,10 +815,7 @@ def from_stats( req.first_token_time or req.start_time for req in total_with_output_first ], - iter_counts=[ - req.output_tokens - for req in total_with_output_first - ], + iter_counts=[req.output_tokens for req in total_with_output_first], first_iter_counts=[ req.prompt_tokens for req in total_with_output_first ], diff --git a/src/ui/.env.development b/src/ui/.env.development new file mode 100644 index 00000000..66c8d235 --- /dev/null +++ b/src/ui/.env.development @@ -0,0 +1,3 @@ +ASSET_PREFIX=https://review.neuralmagic.com/guidellm-ui/dev/_next +BASE_PATH=/guidellm-ui/dev +NEXT_PUBLIC_USE_MOCK_API=true diff --git a/src/ui/.env.example b/src/ui/.env.example new file mode 100644 index 00000000..06812a30 --- /dev/null +++ b/src/ui/.env.example @@ -0,0 +1,3 @@ +ASSET_PREFIX=http://localhost:3000 +BASE_PATH=http://localhost:3000 +NEXT_PUBLIC_USE_MOCK_API=true diff --git a/src/ui/.env.local b/src/ui/.env.local new file mode 100644 index 00000000..44ab168b --- /dev/null +++ b/src/ui/.env.local @@ -0,0 +1,4 @@ +ASSET_PREFIX=http://localhost:3000 +BASE_PATH=http://localhost:3000 +NEXT_PUBLIC_USE_MOCK_API=true +USE_MOCK_DATA=true diff --git a/src/ui/.env.production b/src/ui/.env.production new file mode 100644 index 00000000..66c8d235 --- /dev/null +++ b/src/ui/.env.production @@ -0,0 +1,3 @@ +ASSET_PREFIX=https://review.neuralmagic.com/guidellm-ui/dev/_next +BASE_PATH=/guidellm-ui/dev +NEXT_PUBLIC_USE_MOCK_API=true diff --git a/src/ui/.env.staging b/src/ui/.env.staging new file mode 100644 index 00000000..20142e5d --- /dev/null +++ b/src/ui/.env.staging @@ -0,0 +1,3 @@ +ASSET_PREFIX=https://staging.guidellm.neuralmagic.com +BASE_PATH=/guidellm-ui/staging +NEXT_PUBLIC_USE_MOCK_API=true diff --git a/src/ui/app/assets/icons/guidellm-icon-dark.png b/src/ui/app/assets/icons/guidellm-icon-dark.png new file mode 100644 index 00000000..2e570c52 Binary files /dev/null and b/src/ui/app/assets/icons/guidellm-icon-dark.png differ diff --git a/src/ui/app/assets/icons/guidellm-logo-light.png b/src/ui/app/assets/icons/guidellm-logo-light.png new file mode 100644 index 00000000..7c8e2d6b Binary files /dev/null and b/src/ui/app/assets/icons/guidellm-logo-light.png differ diff --git a/src/ui/app/assets/icons/index.tsx b/src/ui/app/assets/icons/index.tsx index be01d7a6..f3ac0773 100644 --- a/src/ui/app/assets/icons/index.tsx +++ b/src/ui/app/assets/icons/index.tsx @@ -2,18 +2,20 @@ import ArrowDown from './arrow-down.svg'; import ArrowUp from './arrow-up.svg'; import CheckCircle from './check-circle.svg'; import Expand from './expand.svg'; +import guideLLMIconDark from './guidellm-icon-dark.png'; +import guideLLMLogoLight from './guidellm-logo-light.png'; import Info from './info.svg'; -import NeuralMagicTitleV2 from './nm-logo-with-name.svg'; import Open from './open.svg'; import WarningCircle from './warning-circle.svg'; export { ArrowDown, ArrowUp, - Expand, CheckCircle, - WarningCircle, + Expand, + guideLLMIconDark, + guideLLMLogoLight, Info, - NeuralMagicTitleV2, Open, + WarningCircle, }; diff --git a/src/ui/app/layout.tsx b/src/ui/app/layout.tsx index 04102556..99696f12 100644 --- a/src/ui/app/layout.tsx +++ b/src/ui/app/layout.tsx @@ -1,6 +1,9 @@ import type { Metadata, Viewport } from 'next'; import React from 'react'; +import { benchmarksScript } from '@/lib/store/benchmarksWindowData'; +import { runInfoScript } from '@/lib/store/runInfoWindowData'; +import { workloadDetailsScript } from '@/lib/store/workloadDetailsWindowData'; import './globals.css'; export async function generateMetadata(): Promise<Metadata> { @@ -10,7 +13,7 @@ export async function generateMetadata(): Promise<Metadata> { title: 'GuideLLM', description: 'LLM Benchmarking Tool', icons: { - icon: `${assetPrefix}/favicon.ico`, + icon: `${assetPrefix}/favicon.png`, apple: `${assetPrefix}/favicon-192x192.png`, }, manifest: `${assetPrefix}/manifest.json`, @@ -28,8 +31,39 @@ export default function RootLayout({ }: Readonly<{ children: React.ReactNode; }>) { + const emptyDataScript = ( + <script + dangerouslySetInnerHTML={{ + __html: + 'window.run_info = {}; window.workload_details = {}; window.benchmarks = {};', + }} + /> + ); + const mockDataScript = ( + <> + <script + dangerouslySetInnerHTML={{ + __html: runInfoScript, + }} + /> + <script + dangerouslySetInnerHTML={{ + __html: workloadDetailsScript, + }} + /> + <script + dangerouslySetInnerHTML={{ + __html: benchmarksScript, + }} + /> + </> + ); + const dataScript = + process.env.USE_MOCK_DATA === 'true' ? mockDataScript : emptyDataScript; + return ( <html lang="en"> + <head>{dataScript}</head> <body>{children}</body> </html> ); diff --git a/src/ui/app/not-found.tsx b/src/ui/app/not-found.tsx new file mode 100644 index 00000000..60b5a096 --- /dev/null +++ b/src/ui/app/not-found.tsx @@ -0,0 +1,4 @@ +'use client'; +export default function Custom404() { + return <h1>404 - Page Not Found</h1>; +} diff --git a/src/ui/app/page.tsx b/src/ui/app/page.tsx index 13102b0b..21c53304 100644 --- a/src/ui/app/page.tsx +++ b/src/ui/app/page.tsx @@ -1,16 +1,19 @@ 'use client'; -import { Typography, useTheme } from '@mui/material'; +import { useTheme } from '@mui/material'; import { ThemeProvider } from '@mui/material/styles'; import { AppRouterCacheProvider } from '@mui/material-nextjs/v13-appRouter'; import Script from 'next/script'; import React, { ReactNode } from 'react'; import { muiThemeV3Dark } from '@/app/theme'; +import { MetricsSummary } from '@/lib/components/MetricsSummary'; import { PageFooter } from '@/lib/components/PageFooter'; - -import { FullPageWithHeaderAndFooterLayout } from '../lib/layouts/FullPageWithHeaderAndFooterLayout'; -import { ContentCenterer } from '../lib/layouts/helpers/ContentCenterer'; -import { ReduxProvider } from '../lib/store/provider'; +import { PageHeader } from '@/lib/components/PageHeader'; +import { WorkloadDetails } from '@/lib/components/WorkloadDetails'; +import { WorkloadMetrics } from '@/lib/components/WorkloadMetrics'; +import { FullPageWithHeaderAndFooterLayout } from '@/lib/layouts/FullPageWithHeaderAndFooterLayout'; +import { ContentCenterer } from '@/lib/layouts/helpers/ContentCenterer'; +import { ReduxProvider } from '@/lib/store/provider'; interface MyProps { children?: ReactNode; @@ -25,9 +28,7 @@ const Content = () => { const header = ( <ContentCenterer> - <Typography color="white" variant="h1"> - Header - </Typography> + <PageHeader /> </ContentCenterer> ); const footer = ( @@ -37,9 +38,9 @@ const Content = () => { ); const body = ( <ContentCenterer> - <Typography color="white" variant="h3"> - GuideLLM - </Typography> + <WorkloadDetails /> + <MetricsSummary /> + <WorkloadMetrics /> </ContentCenterer> ); return ( diff --git a/src/ui/app/types/images.d.ts b/src/ui/app/types/images.d.ts new file mode 100644 index 00000000..546475dd --- /dev/null +++ b/src/ui/app/types/images.d.ts @@ -0,0 +1,4 @@ +declare module '*.png' { + const content: import('next/image').StaticImageData; + export default content; +} diff --git a/src/ui/lib/components/Badge/Badge.component.tsx b/src/ui/lib/components/Badge/Badge.component.tsx new file mode 100644 index 00000000..16f6e35f --- /dev/null +++ b/src/ui/lib/components/Badge/Badge.component.tsx @@ -0,0 +1,8 @@ +import { FC } from 'react'; + +import { BadgeProps } from './Badge.interfaces'; +import { StyledTypography } from './Badge.styles'; + +export const Component: FC<BadgeProps> = ({ label }) => { + return <StyledTypography variant="body2">{label}</StyledTypography>; +}; diff --git a/src/ui/lib/components/Badge/Badge.interfaces.ts b/src/ui/lib/components/Badge/Badge.interfaces.ts new file mode 100644 index 00000000..854f38e1 --- /dev/null +++ b/src/ui/lib/components/Badge/Badge.interfaces.ts @@ -0,0 +1,3 @@ +export interface BadgeProps { + label: string; +} diff --git a/src/ui/lib/components/Badge/Badge.styles.tsx b/src/ui/lib/components/Badge/Badge.styles.tsx new file mode 100644 index 00000000..e1469946 --- /dev/null +++ b/src/ui/lib/components/Badge/Badge.styles.tsx @@ -0,0 +1,11 @@ +import { styled, Typography } from '@mui/material'; + +export const StyledTypography = styled(Typography)(({ theme }) => ({ + backgroundColor: theme.palette.surface.surfaceContainerHigh, + borderRadius: '6px', + padding: '6px', + marginRight: '6px', + display: 'inline-block', + paddingTop: '8px', + color: theme.palette.surface.onSurfaceAccent, +})); diff --git a/src/ui/lib/components/Badge/index.tsx b/src/ui/lib/components/Badge/index.tsx new file mode 100644 index 00000000..6ddc5f53 --- /dev/null +++ b/src/ui/lib/components/Badge/index.tsx @@ -0,0 +1,2 @@ +export { Component as Badge } from './Badge.component'; +export type { BadgeProps } from './Badge.interfaces'; diff --git a/src/ui/lib/components/BlockHeader/BlockHeader.component.tsx b/src/ui/lib/components/BlockHeader/BlockHeader.component.tsx new file mode 100644 index 00000000..8e8587ee --- /dev/null +++ b/src/ui/lib/components/BlockHeader/BlockHeader.component.tsx @@ -0,0 +1,26 @@ +'use client'; +import { Box, Typography, useTheme } from '@mui/material'; + +import { Info } from '@assets/icons'; + +import { SvgContainer } from '@/lib/utils/SvgContainer'; + +import { BlockHeaderProps } from './BlockHeader.interfaces'; +import { CustomDivider } from './BlockHeader.styles'; + +export const Component = ({ label, withDivider = false }: BlockHeaderProps) => { + const theme = useTheme(); + return ( + <Box display="flex" alignItems="center" my={3}> + <Typography variant="h5" color="surface.onSurface"> + {label} + </Typography> + <Box ml={2}> + <SvgContainer color={theme.palette.surface.onSurfaceAccent}> + <Info /> + </SvgContainer> + </Box> + {withDivider && <CustomDivider />} + </Box> + ); +}; diff --git a/src/ui/lib/components/BlockHeader/BlockHeader.interfaces.ts b/src/ui/lib/components/BlockHeader/BlockHeader.interfaces.ts new file mode 100644 index 00000000..fffb9e88 --- /dev/null +++ b/src/ui/lib/components/BlockHeader/BlockHeader.interfaces.ts @@ -0,0 +1,4 @@ +export interface BlockHeaderProps { + label: string; + withDivider?: boolean; +} diff --git a/src/ui/lib/components/BlockHeader/BlockHeader.styles.tsx b/src/ui/lib/components/BlockHeader/BlockHeader.styles.tsx new file mode 100644 index 00000000..9f063d8f --- /dev/null +++ b/src/ui/lib/components/BlockHeader/BlockHeader.styles.tsx @@ -0,0 +1,8 @@ +import { Divider, styled } from '@mui/material'; + +export const CustomDivider = styled(Divider)(({ theme }) => ({ + height: '1px', + flex: 1, + marginLeft: '48px', + backgroundColor: theme.palette.outline.subdued, +})); diff --git a/src/ui/lib/components/BlockHeader/index.tsx b/src/ui/lib/components/BlockHeader/index.tsx new file mode 100644 index 00000000..7035c622 --- /dev/null +++ b/src/ui/lib/components/BlockHeader/index.tsx @@ -0,0 +1,2 @@ +export { Component as BlockHeader } from './BlockHeader.component'; +export type { BlockHeaderProps } from './BlockHeader.interfaces'; diff --git a/src/ui/lib/components/Carousel/Carousel.component.tsx b/src/ui/lib/components/Carousel/Carousel.component.tsx new file mode 100644 index 00000000..079e494c --- /dev/null +++ b/src/ui/lib/components/Carousel/Carousel.component.tsx @@ -0,0 +1,58 @@ +import { Box, Typography, useTheme } from '@mui/material'; +import dynamic from 'next/dynamic'; +import { FC } from 'react'; + +import { CarouselProps } from './Carousel.interfaces'; +import { PromptWrapper } from './Carousel.styles'; + +const Carousel = dynamic( + () => import('react-material-ui-carousel').then((mod) => mod.default), + { + ssr: false, + } +); + +function truncateString(str: string, limit: number) { + if (str.length <= limit) { + return str; + } + let cutIndex = limit; + while ( + cutIndex < str.length && + (str[cutIndex] !== ' ' || /[.,!?]/.test(str[cutIndex - 1])) + ) { + cutIndex++; + } + return cutIndex < str.length ? str.slice(0, cutIndex) + '...' : str; +} + +export const Component: FC<CarouselProps> = ({ label, items }) => { + const theme = useTheme(); + return ( + <Box sx={{ width: '100%' }}> + <Typography + variant="overline2" + color="surface.onSurface" + textTransform="uppercase" + > + {label} + </Typography> + <Carousel + interval={10000} + duration={1000} + animation="fade" + IndicatorIcon={null} + navButtonsAlwaysInvisible={true} + sx={{ marginTop: '6px' }} + > + {items.map((item, i) => ( + <PromptWrapper key={i} data-id="prompt-wrapper"> + <Typography variant="body2" color={theme.palette.primary.main}> + {truncateString(item, 200)} + </Typography> + </PromptWrapper> + ))} + </Carousel> + </Box> + ); +}; diff --git a/src/ui/lib/components/Carousel/Carousel.interfaces.ts b/src/ui/lib/components/Carousel/Carousel.interfaces.ts new file mode 100644 index 00000000..5cc386f5 --- /dev/null +++ b/src/ui/lib/components/Carousel/Carousel.interfaces.ts @@ -0,0 +1,4 @@ +export interface CarouselProps { + label: string; + items: string[]; +} diff --git a/src/ui/lib/components/Carousel/Carousel.styles.tsx b/src/ui/lib/components/Carousel/Carousel.styles.tsx new file mode 100644 index 00000000..249c10d5 --- /dev/null +++ b/src/ui/lib/components/Carousel/Carousel.styles.tsx @@ -0,0 +1,11 @@ +import { Box, styled } from '@mui/material'; + +export const PromptWrapper = styled(Box)(({ theme }) => ({ + backgroundColor: theme.palette.primary.container, + borderRadius: '8px', + padding: '8px', + minWidth: '304px', + minHeight: '84px', + width: 'auto', + overflow: 'hidden', +})); diff --git a/src/ui/lib/components/Carousel/index.tsx b/src/ui/lib/components/Carousel/index.tsx new file mode 100644 index 00000000..0f7ee7e5 --- /dev/null +++ b/src/ui/lib/components/Carousel/index.tsx @@ -0,0 +1,2 @@ +export { Component as Carousel } from './Carousel.component'; +export type { CarouselProps } from './Carousel.interfaces'; diff --git a/src/ui/lib/components/Charts/Combined/Combined.component.tsx b/src/ui/lib/components/Charts/Combined/Combined.component.tsx new file mode 100644 index 00000000..68b53496 --- /dev/null +++ b/src/ui/lib/components/Charts/Combined/Combined.component.tsx @@ -0,0 +1,153 @@ +import { useTheme } from '@mui/material'; +import { BarCustomLayerProps, ResponsiveBar } from '@nivo/bar'; +import { Point } from '@nivo/core'; +import React from 'react'; + +import { CombinedProps } from './Combined.interfaces'; +import CustomBars from './components/CustomBars'; +import CustomGrid from './components/CustomGrid'; +import { CustomLegendLayer } from './components/CustomLegendLayer'; +import CustomTick from './components/CustomTick'; +import DottedLines from './components/DottedLines'; +import useChartScales from '../common/useChartScales'; + +export const Component = ({ + bars, + lines, + width, + height, + margins, + xLegend, + yLegend, +}: CombinedProps) => { + const theme = useTheme(); + const combinedGraphTheme = { + axis: { + legend: { + text: { + fill: theme.palette.surface.onSurface, + fontSize: theme.typography.axisTitle.fontSize, + fontWeight: theme.typography.axisTitle.fontWeight, + fontFamily: theme.typography.axisTitle.fontFamily, + }, + }, + ticks: { + text: { + fill: theme.palette.surface.onSurface, + }, + }, + }, + }; + const defaultMargins = { top: 10, left: 10, right: 10, bottom: 10 }; + const finalMargins = { + ...defaultMargins, + ...margins, + bottom: (margins?.bottom || defaultMargins.bottom) + 20, + }; + const { xTicks, yTicks, fnScaleX, fnScaleY, innerHeight } = useChartScales({ + bars, + lines, + width, + height, + margins: finalMargins, + }); + + const CustomGridLayer = () => { + const scaledWidth = fnScaleX(xTicks[xTicks.length - 1]); + const scaledHeight = innerHeight - fnScaleY(yTicks[0]); + + return ( + <CustomGrid + xScale={(d: number) => fnScaleX(d)} + yScale={(d: number) => innerHeight - fnScaleY(d)} + width={scaledWidth} + height={innerHeight} + xTicks={xTicks} + yTicks={yTicks} + scaledHeight={scaledHeight} + /> + ); + }; + + const CustomBarLayer = (props: BarCustomLayerProps<Point>) => { + const heightOffset = innerHeight - fnScaleY(yTicks[0]); + return ( + <CustomBars + {...props} + xScaleFunc={(d: number) => fnScaleX(d)} + yScaleFunc={(d: number) => innerHeight - fnScaleY(d)} + heightOffset={heightOffset} + /> + ); + }; + return ( + <div style={{ width: width + 'px', height: height + 'px', position: 'relative' }}> + <ResponsiveBar + animate={true} + data={bars} + keys={['y']} + indexBy="x" + margin={{ + top: finalMargins.top, + right: finalMargins.right, + bottom: finalMargins.bottom, + left: finalMargins.left, + }} + padding={0.5} + // TODO: change colors scheme + // colors={{ scheme: 'category10' }} + colors={[theme.palette.primary.shades.B80]} + axisLeft={{ + tickValues: yTicks, + legend: yLegend, + legendPosition: 'middle', + legendOffset: -30, + tickSize: 5, + tickPadding: 5, + tickRotation: 0, + renderTick: (tick) => ( + <CustomTick + isXAxis={false} + scale={(d: number) => Math.abs(innerHeight - fnScaleY(d))} + tick={yTicks[tick.tickIndex]} + isFirst={tick.tickIndex === 0} + isLast={tick.tickIndex === yTicks.length - 1} + /> + ), + }} + axisBottom={{ + tickValues: xTicks, + legend: xLegend, + legendPosition: 'middle', + legendOffset: 30, + tickSize: 5, + tickPadding: 5, + tickRotation: 0, + renderTick: (tick) => ( + <CustomTick + isXAxis={true} + scale={(d: number) => fnScaleX(d)} + tick={xTicks[tick.tickIndex]} + isFirst={tick.tickIndex === 0} + isLast={tick.tickIndex === xTicks.length - 1} + /> + ), + }} + layers={[ + 'axes', + CustomGridLayer, + CustomBarLayer, + () => <CustomLegendLayer series={lines} height={height} />, + ]} + theme={combinedGraphTheme} + /> + <DottedLines + lines={lines} + leftMargin={finalMargins.left} + topMargin={finalMargins.top} + innerHeight={innerHeight} + xScale={fnScaleX} + /> + </div> + ); +}; diff --git a/src/ui/lib/components/Charts/Combined/Combined.interfaces.ts b/src/ui/lib/components/Charts/Combined/Combined.interfaces.ts new file mode 100644 index 00000000..a81a9f7c --- /dev/null +++ b/src/ui/lib/components/Charts/Combined/Combined.interfaces.ts @@ -0,0 +1,11 @@ +import { LinesSeries, Margins, Point } from '../common/interfaces'; + +export interface CombinedProps { + bars: Point[]; + lines: LinesSeries[]; + width: number; + height: number; + margins?: Margins; + xLegend: string; + yLegend: string; +} diff --git a/src/ui/lib/components/Charts/Combined/components/CustomBars/CustomBars.interfaces.ts b/src/ui/lib/components/Charts/Combined/components/CustomBars/CustomBars.interfaces.ts new file mode 100644 index 00000000..faad401e --- /dev/null +++ b/src/ui/lib/components/Charts/Combined/components/CustomBars/CustomBars.interfaces.ts @@ -0,0 +1,7 @@ +import { BarCustomLayerProps } from '@nivo/bar'; + +export interface CustomBarsProps<T> extends BarCustomLayerProps<T> { + xScaleFunc: (d: number) => number; + yScaleFunc: (d: number) => number; + heightOffset: number; +} diff --git a/src/ui/lib/components/Charts/Combined/components/CustomBars/index.tsx b/src/ui/lib/components/Charts/Combined/components/CustomBars/index.tsx new file mode 100644 index 00000000..20c38203 --- /dev/null +++ b/src/ui/lib/components/Charts/Combined/components/CustomBars/index.tsx @@ -0,0 +1,55 @@ +import { ComputedBarDatum } from '@nivo/bar'; +import { Point } from '@nivo/core'; +import { useTooltip, BasicTooltip } from '@nivo/tooltip'; +import React from 'react'; + +import { CustomBarsProps } from './CustomBars.interfaces'; + +const CustomBars = ({ + bars, + xScaleFunc, + yScaleFunc, + heightOffset, +}: CustomBarsProps<Point>) => { + const { showTooltipFromEvent, hideTooltip } = useTooltip(); + + const handleMouseEnter = ( + event: React.MouseEvent<SVGRectElement>, + bar: ComputedBarDatum<Point> + ) => { + showTooltipFromEvent( + <BasicTooltip + id={`x: ${bar.data.data.x}, y: ${bar.data.data.y}`} + enableChip={true} + color={bar.color} + />, + event + ); + }; + + const handleMouseLeave = () => { + hideTooltip(); + }; + + return ( + <g transform={`translate(0,-${heightOffset})`}> + {bars.map((bar) => { + return ( + <rect + key={bar.key} + x={xScaleFunc(Number(bar.data.data.x)) - bar.width / 2} + y={yScaleFunc(Number(bar.data.data.y))} + width={bar.width} + height={bar.height} + fill={bar.color} + onMouseEnter={(event) => handleMouseEnter(event, bar)} + onMouseLeave={handleMouseLeave} + rx={bar.height > 8 ? 8 : 1} + /> + ); + })} + </g> + ); +}; + +export default CustomBars; diff --git a/src/ui/lib/components/Charts/Combined/components/CustomGrid/CustomGrid.interfaces.ts b/src/ui/lib/components/Charts/Combined/components/CustomGrid/CustomGrid.interfaces.ts new file mode 100644 index 00000000..2e440fba --- /dev/null +++ b/src/ui/lib/components/Charts/Combined/components/CustomGrid/CustomGrid.interfaces.ts @@ -0,0 +1,10 @@ +export interface CustomGridProps { + xScale: (d: number) => number; + yScale: (d: number) => number; + width: number; + height: number; + xTicks: number[]; + yTicks: number[]; + scaledHeight: number; + fullGrid?: boolean; +} diff --git a/src/ui/lib/components/Charts/Combined/components/CustomGrid/index.tsx b/src/ui/lib/components/Charts/Combined/components/CustomGrid/index.tsx new file mode 100644 index 00000000..69726062 --- /dev/null +++ b/src/ui/lib/components/Charts/Combined/components/CustomGrid/index.tsx @@ -0,0 +1,72 @@ +import { useTheme } from '@mui/material'; + +import { CustomGridProps } from './CustomGrid.interfaces'; + +const CustomGrid = ({ + xScale, + yScale, + width, + height, + xTicks, + yTicks, + scaledHeight, + fullGrid = false, +}: CustomGridProps) => { + const theme = useTheme(); + const xTick = xTicks[0]; + const yTick = yTicks[yTicks.length - 1]; + + const renderAxlesOnly = ( + <> + <line + key={`x${xTick}`} + x1={xScale(xTick)} + x2={xScale(xTick)} + y1={scaledHeight} + y2={height} + stroke={theme.palette.outline.subdued} + /> + <line + key={`y${yTick}`} + x1={0} + x2={width} + y1={yScale(yTick)} + y2={yScale(yTick)} + stroke={theme.palette.outline.subdued} + /> + </> + ); + + const renderFullGrid = ( + <> + {xTicks.map((tick) => ( + <line + key={`x${tick}`} + x1={xScale(tick)} + x2={xScale(tick)} + y1={scaledHeight} + y2={height} + stroke={theme.palette.outline.subdued} + /> + ))} + {yTicks.map((tick) => ( + <line + key={`y${tick}`} + x1={0} + x2={width} + y1={yScale(tick)} + y2={yScale(tick)} + stroke={theme.palette.outline.subdued} + /> + ))} + </> + ); + + return ( + <g transform={`translate(0,-${scaledHeight})`} id="grid"> + {fullGrid ? renderFullGrid : renderAxlesOnly} + </g> + ); +}; + +export default CustomGrid; diff --git a/src/ui/lib/components/Charts/Combined/components/CustomLegendLayer/CustomLegendLayer.interfaces.ts b/src/ui/lib/components/Charts/Combined/components/CustomLegendLayer/CustomLegendLayer.interfaces.ts new file mode 100644 index 00000000..de0abde5 --- /dev/null +++ b/src/ui/lib/components/Charts/Combined/components/CustomLegendLayer/CustomLegendLayer.interfaces.ts @@ -0,0 +1,6 @@ +import { LinesSeries } from '../../../common/interfaces'; + +export interface CustomLegendLayerProps { + series: LinesSeries[]; + height: number; +} diff --git a/src/ui/lib/components/Charts/Combined/components/CustomLegendLayer/index.tsx b/src/ui/lib/components/Charts/Combined/components/CustomLegendLayer/index.tsx new file mode 100644 index 00000000..3f390695 --- /dev/null +++ b/src/ui/lib/components/Charts/Combined/components/CustomLegendLayer/index.tsx @@ -0,0 +1,42 @@ +import { useTheme } from '@mui/material'; + +import useLineColors from '../../../common/useLineColors'; + +import { CustomLegendLayerProps } from './CustomLegendLayer.interfaces'; + +const LEGEND_HEIGHT = 20; + +export const CustomLegendLayer = ({ series, ...rest }: CustomLegendLayerProps) => { + const theme = useTheme(); + const lineColor = useLineColors(); + return ( + <g transform={`translate(20, ${rest.height - LEGEND_HEIGHT})`}> + {series.map((item, index) => ( + <g key={item.id} transform={`translate(${index * 100}, 0)`}> + <line + x1="0" + y1="0" + x2="20" + y2="0" + stroke={lineColor[index]} + strokeWidth={2} + strokeDasharray={'4,4'} + /> + <text + x="30" + y="0" + fill={theme.palette.surface.onSurface} + style={{ + fontSize: theme.typography.caption.fontSize, + fontWeight: theme.typography.caption.fontWeight, + fontFamily: theme.typography.caption.fontFamily, + }} + alignmentBaseline="middle" + > + {item.id} + </text> + </g> + ))} + </g> + ); +}; diff --git a/src/ui/lib/components/Charts/Combined/components/CustomTick/CustomTick.interfaces.ts b/src/ui/lib/components/Charts/Combined/components/CustomTick/CustomTick.interfaces.ts new file mode 100644 index 00000000..efe5ef04 --- /dev/null +++ b/src/ui/lib/components/Charts/Combined/components/CustomTick/CustomTick.interfaces.ts @@ -0,0 +1,8 @@ +export interface CustomTickProps { + scale: (d: number) => number; + isXAxis: boolean; + tick: number; + withTicks?: boolean; + isFirst: boolean; + isLast: boolean; +} diff --git a/src/ui/lib/components/Charts/Combined/components/CustomTick/index.tsx b/src/ui/lib/components/Charts/Combined/components/CustomTick/index.tsx new file mode 100644 index 00000000..6358d5be --- /dev/null +++ b/src/ui/lib/components/Charts/Combined/components/CustomTick/index.tsx @@ -0,0 +1,67 @@ +import { useTheme } from '@mui/material'; + +import { CustomTickProps } from './CustomTick.interfaces'; + +function CustomTick({ + isXAxis, + tick, + scale, + withTicks = false, + isFirst, + isLast, +}: CustomTickProps) { + const theme = useTheme(); + + function getGroupPosition() { + if (isXAxis) { + let x = scale(tick); + if (isFirst) { + x = 4; + } + if (isLast) { + x -= 4; + } + return { x, y: 0 }; + } + return { x: 0, y: scale(tick) }; + } + + function renderTick() { + const commonProps = { + fontFamily: theme.typography.axisLabel.fontFamily, + fontWeight: theme.typography.axisLabel.fontWeight, + fontSize: theme.typography.axisLabel.fontSize, + fill: theme.palette.surface.onSurface, + }; + + return isXAxis ? ( + <> + {withTicks && ( + <line x1={0} y1={0} x2={0} y2={6} stroke={theme.palette.surface.onSurface} /> + )} + <text textAnchor="middle" y={10} {...commonProps}> + {tick} + </text> + </> + ) : ( + <> + {withTicks && ( + <line x1={0} y1={0} x2={-6} y2={0} stroke={theme.palette.surface.onSurface} /> + )} + <text textAnchor="end" x={-5} dominantBaseline="middle" {...commonProps}> + {tick} + </text> + </> + ); + } + + const { x, y } = getGroupPosition(); + + return ( + <g key={tick} transform={`translate(${x}, ${y})`}> + {renderTick()} + </g> + ); +} + +export default CustomTick; diff --git a/src/ui/lib/components/Charts/Combined/components/DottedLines/DottedLines.interfaces.ts b/src/ui/lib/components/Charts/Combined/components/DottedLines/DottedLines.interfaces.ts new file mode 100644 index 00000000..9044e15b --- /dev/null +++ b/src/ui/lib/components/Charts/Combined/components/DottedLines/DottedLines.interfaces.ts @@ -0,0 +1,9 @@ +import { LinesSeries } from '../../../common/interfaces'; + +export interface DottedLinesProps { + lines: LinesSeries[]; + leftMargin: number; + topMargin: number; + innerHeight: number; + xScale: (d: number) => number; +} diff --git a/src/ui/lib/components/Charts/Combined/components/DottedLines/index.tsx b/src/ui/lib/components/Charts/Combined/components/DottedLines/index.tsx new file mode 100644 index 00000000..b4df2145 --- /dev/null +++ b/src/ui/lib/components/Charts/Combined/components/DottedLines/index.tsx @@ -0,0 +1,39 @@ +import React from 'react'; + +import useLineColors from '../../../common/useLineColors'; + +import { DottedLinesProps } from './DottedLines.interfaces'; + +const DottedLines = ({ + lines, + leftMargin, + topMargin, + xScale, + innerHeight, +}: DottedLinesProps) => { + const lineColor = useLineColors(); + return ( + <svg + style={{ position: 'absolute', top: 0, left: 0, pointerEvents: 'none' }} + width="100%" + height="100%" + > + <g transform={`translate(${leftMargin}, ${topMargin})`}> + {lines.map((line, i) => ( + <line + key={i} + x1={xScale(line.x)} + y1="0" + x2={xScale(line.x)} + y2={innerHeight} + stroke={lineColor[i]} + strokeWidth="1.5" + strokeDasharray="3,3" + /> + ))} + </g> + </svg> + ); +}; + +export default DottedLines; diff --git a/src/ui/lib/components/Charts/Combined/index.tsx b/src/ui/lib/components/Charts/Combined/index.tsx new file mode 100644 index 00000000..199b1554 --- /dev/null +++ b/src/ui/lib/components/Charts/Combined/index.tsx @@ -0,0 +1,2 @@ +export { Component as Combined } from './Combined.component'; +export type { CombinedProps } from './Combined.interfaces'; diff --git a/src/ui/lib/components/Charts/DashedLine/DashedLine.component.tsx b/src/ui/lib/components/Charts/DashedLine/DashedLine.component.tsx new file mode 100644 index 00000000..a3b9ab87 --- /dev/null +++ b/src/ui/lib/components/Charts/DashedLine/DashedLine.component.tsx @@ -0,0 +1,139 @@ +import { useTheme } from '@mui/material'; +import { ResponsiveLine, Serie } from '@nivo/line'; + +import { CustomLegendLayer } from './components/CustomLegendLayer'; +import { DashedSolidLine } from './components/DashedSolidLine'; +import { DashedLineProps, ScaleType } from './DashedLine.interfaces'; +import { spacedLogValues } from './helpers'; + +export const getMinTick = (data: readonly Serie[]) => { + return Math.max( + ...data.map((lineData) => + Math.min(...lineData.data.map((point) => point.y as number)) + ) + ); +}; + +export const getMaxTick = (data: readonly Serie[]) => { + return Math.max( + ...data.map((lineData) => + Math.max(...lineData.data.map((point) => point.y as number)) + ) + ); +}; + +export const Component = ({ + data, + xLegend, + yLegend, + margins, + minX, + yScaleType = ScaleType.log, +}: DashedLineProps) => { + const theme = useTheme(); + const defaultMargins = { top: 10, left: 10, right: 10, bottom: 10 }; + const finalMargins = { + ...defaultMargins, + ...margins, + bottom: (margins?.bottom || defaultMargins.bottom) + 50, + }; + + const dashedLineTheme = { + textColor: theme.palette.surface.onSurface, + fontSize: 14, + axis: { + domain: { + line: { + stroke: theme.palette.outline.subdued, + strokeWidth: 1, + }, + }, + ticks: { + line: { + stroke: theme.palette.outline.subdued, + strokeWidth: 1, + }, + text: { + fill: theme.palette.surface.onSurface, + fontSize: theme.typography.axisTitle.fontSize, + fontFamily: theme.typography.axisTitle.fontFamily, + fontWeight: theme.typography.axisTitle.fontWeight, + }, + }, + legend: { + text: { + fill: theme.palette.surface.onSurface, + fontSize: theme.typography.axisTitle.fontSize, + fontFamily: theme.typography.axisTitle.fontFamily, + fontWeight: theme.typography.axisTitle.fontWeight, + }, + }, + }, + grid: { + line: { + stroke: theme.palette.outline.subdued, + strokeWidth: 1, + }, + }, + }; + + let extraLeftAxisOptions = {}; + let extraYScaleOptions = {}; + if (yScaleType === ScaleType.log) { + const ticks = spacedLogValues(getMinTick(data), getMaxTick(data), 6); + extraLeftAxisOptions = { + tickValues: ticks, + }; + extraYScaleOptions = { + max: ticks[ticks.length - 1], + }; + } + + return ( + <div style={{ height: '100%', width: '100%' }}> + <ResponsiveLine + data={data} + enablePoints={false} + curve="monotoneX" + margin={finalMargins} + axisTop={null} + axisRight={null} + axisBottom={{ + tickSize: 0, + tickPadding: 5, + tickRotation: 0, + legend: xLegend, + legendOffset: 36, + legendPosition: 'middle', + }} + axisLeft={{ + tickSize: 0, + tickPadding: 5, + tickRotation: 0, + legend: yLegend, + legendOffset: -40, + legendPosition: 'middle', + ...extraLeftAxisOptions, + }} + xScale={{ + min: minX, + type: 'linear', + }} + yScale={{ + type: yScaleType, + ...extraYScaleOptions, + }} + colors={{ scheme: 'category10' }} + layers={[ + 'markers', + 'axes', + 'points', + 'legends', + DashedSolidLine, + CustomLegendLayer, + ]} + theme={dashedLineTheme} + /> + </div> + ); +}; diff --git a/src/ui/lib/components/Charts/DashedLine/DashedLine.interfaces.ts b/src/ui/lib/components/Charts/DashedLine/DashedLine.interfaces.ts new file mode 100644 index 00000000..16193ab4 --- /dev/null +++ b/src/ui/lib/components/Charts/DashedLine/DashedLine.interfaces.ts @@ -0,0 +1,16 @@ +import { LineSvgProps } from '@nivo/line'; + +import { Margins } from '../common/interfaces'; + +export enum ScaleType { + log = 'symlog', + linear = 'linear', +} + +export interface DashedLineProps extends LineSvgProps { + margins?: Margins; + xLegend: string; + yLegend: string; + minX?: number; + yScaleType?: ScaleType; +} diff --git a/src/ui/lib/components/Charts/DashedLine/components/CustomLegendLayer/CustomLegendLayer.interfaces.ts b/src/ui/lib/components/Charts/DashedLine/components/CustomLegendLayer/CustomLegendLayer.interfaces.ts new file mode 100644 index 00000000..813bb5e2 --- /dev/null +++ b/src/ui/lib/components/Charts/DashedLine/components/CustomLegendLayer/CustomLegendLayer.interfaces.ts @@ -0,0 +1,5 @@ +import { CustomLayerProps } from '@nivo/line'; + +export interface CustomLegendLayerProps extends CustomLayerProps { + height?: number; +} diff --git a/src/ui/lib/components/Charts/DashedLine/components/CustomLegendLayer/index.tsx b/src/ui/lib/components/Charts/DashedLine/components/CustomLegendLayer/index.tsx new file mode 100644 index 00000000..81bd8c20 --- /dev/null +++ b/src/ui/lib/components/Charts/DashedLine/components/CustomLegendLayer/index.tsx @@ -0,0 +1,64 @@ +import { useTheme } from '@mui/material'; + +import { CustomLegendLayerProps } from './CustomLegendLayer.interfaces'; + +const LEGEND_HEIGHT = 40; + +export const CustomLegendLayer = ({ series, ...rest }: CustomLegendLayerProps) => { + const theme = useTheme(); + const palette = theme.palette; + + const colors = [ + palette.surface.onSurface, + palette.secondary.main, + palette.tertiary.main, + palette.quarternary.main, + ]; + const solidColor = palette.primary.main; + + const getColor = (isSolid = false) => { + if (isSolid) { + return solidColor; + } + + if (colors.length === 0) { + throw new Error('No more colors available'); + } + + return colors.splice(0, 1)[0]; + }; + return ( + <g + transform={`translate(20, ${(rest?.height || rest.innerHeight) - LEGEND_HEIGHT})`} + > + {series.map((item, index) => { + return ( + <g key={item.id} transform={`translate(${index * 100}, 0)`}> + <line + x1="0" + y1="0" + x2="20" + y2="0" + stroke={getColor(item.solid)} + strokeWidth={2} + strokeDasharray={item?.solid ? '' : '4,4'} + /> + <text + x="30" + y="0" + fill={theme.palette.surface.onSurface} + style={{ + fontSize: theme.typography.caption.fontSize, + fontWeight: theme.typography.caption.fontWeight, + fontFamily: theme.typography.caption.fontFamily, + }} + alignmentBaseline="middle" + > + {item.id} + </text> + </g> + ); + })} + </g> + ); +}; diff --git a/src/ui/lib/components/Charts/DashedLine/components/DashedSolidLine/DashedSolidLine.interfaces.ts b/src/ui/lib/components/Charts/DashedLine/components/DashedSolidLine/DashedSolidLine.interfaces.ts new file mode 100644 index 00000000..fdb4537a --- /dev/null +++ b/src/ui/lib/components/Charts/DashedLine/components/DashedSolidLine/DashedSolidLine.interfaces.ts @@ -0,0 +1,7 @@ +import type { CustomLayerProps } from '@nivo/line'; +import type { ScaleLinear } from '@nivo/scales'; + +export type DashedSolidLineProps = Omit<CustomLayerProps, 'xScale' | 'yScale'> & { + xScale: ScaleLinear<number>; + yScale: ScaleLinear<number>; +}; diff --git a/src/ui/lib/components/Charts/DashedLine/components/DashedSolidLine/index.tsx b/src/ui/lib/components/Charts/DashedLine/components/DashedSolidLine/index.tsx new file mode 100644 index 00000000..dd786b6a --- /dev/null +++ b/src/ui/lib/components/Charts/DashedLine/components/DashedSolidLine/index.tsx @@ -0,0 +1,76 @@ +import { useTheme } from '@mui/material'; + +import { toNumberValue } from '../../helpers'; + +import { DashedSolidLineProps } from './DashedSolidLine.interfaces'; + +export const DashedSolidLine = ({ + series, + lineGenerator, + xScale, + yScale, +}: DashedSolidLineProps) => { + const theme = useTheme(); + const palette = theme.palette; + + const colors = [ + palette.surface.onSurface, + palette.secondary.main, + palette.tertiary.main, + palette.quarternary.main, + ]; + const solidColor = palette.primary.main; + + const getColor = (isSolid: boolean) => { + if (isSolid) { + return solidColor; + } + + if (colors.length === 0) { + throw new Error('No more colors available'); + } + + return colors.splice(0, 1)[0]; + }; + + return series.map(({ id, data, solid = false }) => { + return ( + <g key={id}> + <path + key={id} + d={(() => { + const linePath = lineGenerator( + data.map((d) => ({ + x: xScale(toNumberValue(d.data.x)), + y: yScale(toNumberValue(d.data.y)), + })) + ); + return linePath !== null ? linePath : undefined; + })()} + fill="none" + stroke={getColor(solid)} + style={ + !solid + ? { + strokeDasharray: '2.4 2.4', + strokeWidth: 1.5, + } + : { + strokeWidth: 1.5, + } + } + /> + {solid && + data.map((d, pointIndex) => ( + <circle + key={`${id}-${pointIndex}`} + cx={xScale(toNumberValue(d.data.x))} + cy={yScale(toNumberValue(d.data.y))} + r={4} + fill={getColor(solid)} + /> + ))} + </g> + ); + }); +}; diff --git a/src/ui/lib/components/Charts/DashedLine/helpers.ts b/src/ui/lib/components/Charts/DashedLine/helpers.ts new file mode 100644 index 00000000..c73405ed --- /dev/null +++ b/src/ui/lib/components/Charts/DashedLine/helpers.ts @@ -0,0 +1,78 @@ +import { DatumValue } from '@nivo/line'; + +type NumberValue = number | { valueOf(): number }; + +export const toNumberValue = (value: DatumValue | null | undefined): NumberValue => { + if (value === null || value === undefined) { + return 0; + } + return value as NumberValue; +}; + +const allowedMultipliers = [ + 1, 1.2, 1.4, 1.5, 1.6, 1.8, 2, 2.5, 3, 3.5, 4, 5, 6, 7, 7.5, 8, 9, 10, +]; + +export function roundUpNice(x: number) { + if (x <= 0) { + return x; + } + const exponent = Math.floor(Math.log10(x)); + const base = Math.pow(10, exponent); + const fraction = x / base; + for (const m of allowedMultipliers) { + if (m >= fraction) { + return Math.round(m * base); + } + } + return Math.round(10 * base); +} + +export function roundNearestNice(x: number) { + if (x <= 0) { + return x; + } + const exponent = Math.floor(Math.log10(x)); + const base = Math.pow(10, exponent); + const fraction = x / base; + let best = allowedMultipliers[0]; + let bestDiff = Math.abs(fraction - best); + for (const m of allowedMultipliers) { + const diff = Math.abs(fraction - m); + if (diff < bestDiff) { + best = m; + bestDiff = diff; + } + } + return Math.round(best * base); +} + +export function spacedLogValues(min: number, max: number, steps: number) { + if (steps < 2) { + return []; + } + + if (min === 0) { + const nonzeroCount = steps - 1; + const exponent = Math.floor(Math.log10(max)) - (nonzeroCount - 1); + const lowerNonZero = roundNearestNice(Math.pow(10, exponent)); + const upperTick = roundUpNice(max); + const r = Math.pow(upperTick / lowerNonZero, 1 / (nonzeroCount - 1)); + const ticks = [0]; + for (let i = 0; i < nonzeroCount; i++) { + const value = lowerNonZero * Math.pow(r, i); + ticks.push(roundNearestNice(value)); + } + return ticks; + } else { + const lowerTick = roundUpNice(min); + const upperTick = roundUpNice(max); + const r = Math.pow(upperTick / lowerTick, 1 / (steps - 1)); + const ticks = []; + for (let i = 0; i < steps; i++) { + const value = lowerTick * Math.pow(r, i); + ticks.push(roundNearestNice(value)); + } + return ticks; + } +} diff --git a/src/ui/lib/components/Charts/DashedLine/index.tsx b/src/ui/lib/components/Charts/DashedLine/index.tsx new file mode 100644 index 00000000..67fbf071 --- /dev/null +++ b/src/ui/lib/components/Charts/DashedLine/index.tsx @@ -0,0 +1,2 @@ +export { Component as DashedLine } from './DashedLine.component'; +export type { DashedLineProps } from './DashedLine.interfaces'; diff --git a/src/ui/lib/components/Charts/MetricLine/MetricLine.component.tsx b/src/ui/lib/components/Charts/MetricLine/MetricLine.component.tsx new file mode 100644 index 00000000..f99c98f1 --- /dev/null +++ b/src/ui/lib/components/Charts/MetricLine/MetricLine.component.tsx @@ -0,0 +1,122 @@ +import { useTheme } from '@mui/material'; +import { ResponsiveLine } from '@nivo/line'; +import React, { FC } from 'react'; + +import { MetricLineProps } from '.'; +import { useColor } from '@/lib/hooks/useColor'; + +import CustomAxes from './components/CustomAxes'; +import ThresholdBar from './components/ThresholdBar'; +import { ScaleType } from '../DashedLine/DashedLine.interfaces'; + +export const Component: FC<MetricLineProps> = ({ + data, + threshold, + lineColor, + yScaleType = ScaleType.log, +}) => { + const theme = useTheme(); + const selectedColor = useColor(lineColor); + const lineTheme = { + axis: { + legend: { + text: { + fill: theme.palette.surface.onSurface, + fontSize: theme.typography.axisTitle.fontSize, + fontWeight: theme.typography.axisTitle.fontWeight, + fontFamily: theme.typography.axisTitle.fontFamily, + }, + }, + ticks: { + text: { + fill: theme.palette.surface.onSurface, + }, + }, + }, + }; + const xValues = data[0].data.map((d) => d.x) as Array<number>; + const yValues = data[0].data.map((d) => d.y) as Array<number>; + + const maxX = Math.max(...xValues); + const minX = Math.min(...xValues); + const maxY = Math.ceil(Math.max(...yValues)); + const minY = Math.floor(Math.min(...yValues)); + + let extraYScaleOptions = {}; + if (yScaleType === ScaleType.linear) { + extraYScaleOptions = { + stacked: true, + reverse: false, + }; + } + + return ( + <ResponsiveLine + curve="monotoneX" + data={data} + colors={[selectedColor]} + margin={{ top: 20, right: 10, bottom: 20, left: 35.5 }} + xScale={{ type: 'linear', min: minX }} + yScale={{ + type: yScaleType, + min: 'auto', + max: 'auto', + ...extraYScaleOptions, + }} + axisBottom={null} + axisLeft={{ + legendOffset: -30, + tickRotation: 0, + tickSize: 5, + tickPadding: 5, + tickValues: [minY, maxY], + renderTick: ({ value, x, y, tickIndex }) => { + return ( + <g transform={`translate(${x},${y})`} data-id="ticks"> + <text + x={-4} + y={tickIndex === 0 ? 0 : 6} + textAnchor="end" + style={{ + fontFamily: theme.typography.axisTitle.fontFamily, + fontWeight: theme.typography.axisLabel.fontWeight, + fontSize: theme.typography.axisLabel.fontSize, + fill: theme.palette.surface.onSurfaceSubdued, + }} + > + {value} + </text> + </g> + ); + }, + }} + enableGridX={false} + enableGridY={false} + pointSize={0} + useMesh={true} + layers={[ + CustomAxes, + ({ + xScale, + yScale, + }: { + xScale: (value: number) => number; + yScale: (value: number) => number; + }) => ( + <ThresholdBar + threshold={threshold} + yScale={yScale} + xScale={xScale} + minX={minX} + maxX={maxX} + minY={minY} + maxY={maxY} + /> + ), + 'axes', + 'lines', + ]} + theme={lineTheme} + /> + ); +}; diff --git a/src/ui/lib/components/Charts/MetricLine/MetricLine.interface.ts b/src/ui/lib/components/Charts/MetricLine/MetricLine.interface.ts new file mode 100644 index 00000000..6df9fb47 --- /dev/null +++ b/src/ui/lib/components/Charts/MetricLine/MetricLine.interface.ts @@ -0,0 +1,15 @@ +import { LineSvgProps } from '@nivo/line'; + +import { ScaleType } from '../DashedLine/DashedLine.interfaces'; + +export enum LineColor { + Primary, + Secondary, + Tertiary, + Quarternary, +} +export interface MetricLineProps extends LineSvgProps { + threshold?: number; + lineColor: LineColor; + yScaleType?: ScaleType; +} diff --git a/src/ui/lib/components/Charts/MetricLine/components/CustomAxes/CustomAxes.interfaces.ts b/src/ui/lib/components/Charts/MetricLine/components/CustomAxes/CustomAxes.interfaces.ts new file mode 100644 index 00000000..66fad024 --- /dev/null +++ b/src/ui/lib/components/Charts/MetricLine/components/CustomAxes/CustomAxes.interfaces.ts @@ -0,0 +1,6 @@ +import { CustomLayerProps } from '@nivo/line'; +import type { ScaleLinear } from '@nivo/scales'; + +export type CustomLineLayerProps = Omit<CustomLayerProps, 'xScale' | 'yScale'> & { + yScale: ScaleLinear<number>; +}; diff --git a/src/ui/lib/components/Charts/MetricLine/components/CustomAxes/index.tsx b/src/ui/lib/components/Charts/MetricLine/components/CustomAxes/index.tsx new file mode 100644 index 00000000..b5275349 --- /dev/null +++ b/src/ui/lib/components/Charts/MetricLine/components/CustomAxes/index.tsx @@ -0,0 +1,23 @@ +import { useTheme } from '@mui/material'; +import React from 'react'; + +import { CustomLineLayerProps } from './CustomAxes.interfaces'; + +const CustomAxes = ({ yScale }: CustomLineLayerProps) => { + const theme = useTheme(); + const minY2 = yScale.domain()[0]; + const maxY2 = yScale.domain()[1]; + return ( + <> + <line + x1={0} + x2={0} + y1={yScale(minY2)} + y2={yScale(maxY2)} + stroke={theme.palette.surface.onSurfaceSubdued} + /> + </> + ); +}; + +export default CustomAxes; diff --git a/src/ui/lib/components/Charts/MetricLine/components/CustomGrid/CustomGrid.interfaces.ts b/src/ui/lib/components/Charts/MetricLine/components/CustomGrid/CustomGrid.interfaces.ts new file mode 100644 index 00000000..2e440fba --- /dev/null +++ b/src/ui/lib/components/Charts/MetricLine/components/CustomGrid/CustomGrid.interfaces.ts @@ -0,0 +1,10 @@ +export interface CustomGridProps { + xScale: (d: number) => number; + yScale: (d: number) => number; + width: number; + height: number; + xTicks: number[]; + yTicks: number[]; + scaledHeight: number; + fullGrid?: boolean; +} diff --git a/src/ui/lib/components/Charts/MetricLine/components/CustomGrid/index.tsx b/src/ui/lib/components/Charts/MetricLine/components/CustomGrid/index.tsx new file mode 100644 index 00000000..69726062 --- /dev/null +++ b/src/ui/lib/components/Charts/MetricLine/components/CustomGrid/index.tsx @@ -0,0 +1,72 @@ +import { useTheme } from '@mui/material'; + +import { CustomGridProps } from './CustomGrid.interfaces'; + +const CustomGrid = ({ + xScale, + yScale, + width, + height, + xTicks, + yTicks, + scaledHeight, + fullGrid = false, +}: CustomGridProps) => { + const theme = useTheme(); + const xTick = xTicks[0]; + const yTick = yTicks[yTicks.length - 1]; + + const renderAxlesOnly = ( + <> + <line + key={`x${xTick}`} + x1={xScale(xTick)} + x2={xScale(xTick)} + y1={scaledHeight} + y2={height} + stroke={theme.palette.outline.subdued} + /> + <line + key={`y${yTick}`} + x1={0} + x2={width} + y1={yScale(yTick)} + y2={yScale(yTick)} + stroke={theme.palette.outline.subdued} + /> + </> + ); + + const renderFullGrid = ( + <> + {xTicks.map((tick) => ( + <line + key={`x${tick}`} + x1={xScale(tick)} + x2={xScale(tick)} + y1={scaledHeight} + y2={height} + stroke={theme.palette.outline.subdued} + /> + ))} + {yTicks.map((tick) => ( + <line + key={`y${tick}`} + x1={0} + x2={width} + y1={yScale(tick)} + y2={yScale(tick)} + stroke={theme.palette.outline.subdued} + /> + ))} + </> + ); + + return ( + <g transform={`translate(0,-${scaledHeight})`} id="grid"> + {fullGrid ? renderFullGrid : renderAxlesOnly} + </g> + ); +}; + +export default CustomGrid; diff --git a/src/ui/lib/components/Charts/MetricLine/components/CustomTick/CustomTick.interfaces.ts b/src/ui/lib/components/Charts/MetricLine/components/CustomTick/CustomTick.interfaces.ts new file mode 100644 index 00000000..efe5ef04 --- /dev/null +++ b/src/ui/lib/components/Charts/MetricLine/components/CustomTick/CustomTick.interfaces.ts @@ -0,0 +1,8 @@ +export interface CustomTickProps { + scale: (d: number) => number; + isXAxis: boolean; + tick: number; + withTicks?: boolean; + isFirst: boolean; + isLast: boolean; +} diff --git a/src/ui/lib/components/Charts/MetricLine/components/CustomTick/index.tsx b/src/ui/lib/components/Charts/MetricLine/components/CustomTick/index.tsx new file mode 100644 index 00000000..3784ff86 --- /dev/null +++ b/src/ui/lib/components/Charts/MetricLine/components/CustomTick/index.tsx @@ -0,0 +1,73 @@ +import { useTheme } from '@mui/material'; + +import { CustomTickProps } from './CustomTick.interfaces'; + +const CustomTick = ({ + isXAxis, + tick, + scale, + withTicks = false, + isFirst, + isLast, +}: CustomTickProps) => { + const theme = useTheme(); + + const getGroupPosition = () => { + if (!isXAxis) { + return { x: 0, y: scale(tick) }; + } + + let x = scale(tick); + if (isFirst) { + x = 4; + } + if (isLast) { + x -= 4; + } + return { x, y: 0 }; + }; + + const renderTickContent = ( + textAnchor: 'middle' | 'end', + x: number, + y: number, + lineX: number, + lineY: number + ) => ( + <> + {withTicks && ( + <line + x1={0} + y1={0} + x2={lineX} + y2={lineY} + stroke={theme.palette.surface.onSurface} + /> + )} + <text + textAnchor={textAnchor} + x={x} + y={y} + dominantBaseline={isXAxis ? undefined : 'middle'} + fontFamily={theme.typography.axisLabel.fontFamily} + fontWeight={theme.typography.axisLabel.fontWeight} + fontSize={theme.typography.axisLabel.fontSize} + fill={theme.palette.surface.onSurface} + > + {tick} + </text> + </> + ); + + const { x, y } = getGroupPosition(); + + return ( + <g key={tick} transform={`translate(${x}, ${y})`}> + {isXAxis + ? renderTickContent('middle', 0, 10, 0, 6) + : renderTickContent('end', -5, 0, -6, 0)} + </g> + ); +}; + +export default CustomTick; diff --git a/src/ui/lib/components/Charts/MetricLine/components/ThresholdBar/ThresholdBar.interfaces.ts b/src/ui/lib/components/Charts/MetricLine/components/ThresholdBar/ThresholdBar.interfaces.ts new file mode 100644 index 00000000..266b4c16 --- /dev/null +++ b/src/ui/lib/components/Charts/MetricLine/components/ThresholdBar/ThresholdBar.interfaces.ts @@ -0,0 +1,9 @@ +export interface ThresholdBarProps { + xScale: (value: number) => number; + yScale: (value: number) => number; + threshold?: number; + minX: number; + minY: number; + maxX: number; + maxY: number; +} diff --git a/src/ui/lib/components/Charts/MetricLine/components/ThresholdBar/index.tsx b/src/ui/lib/components/Charts/MetricLine/components/ThresholdBar/index.tsx new file mode 100644 index 00000000..c4027104 --- /dev/null +++ b/src/ui/lib/components/Charts/MetricLine/components/ThresholdBar/index.tsx @@ -0,0 +1,53 @@ +import { useTheme } from '@mui/material'; +import React, { FC } from 'react'; + +import { ThresholdBarProps } from './ThresholdBar.interfaces'; + +const ThresholdBar: FC<ThresholdBarProps> = ({ + xScale, + yScale, + threshold, + minX, + maxX, + minY, + maxY, +}) => { + const theme = useTheme(); + + if (threshold === undefined || threshold <= 0 || threshold > maxY) { + return null; + } + + const x0 = xScale(minX); + const y0 = yScale(minY); + const xMax = xScale(maxX); + const yThreshold = yScale(threshold); + + if ([x0, y0, xMax, yThreshold].some((value) => value === undefined)) { + return null; + } + + return ( + <g> + <line + x1={x0} + y1={yThreshold} + x2={xMax - x0} + y2={yThreshold} + stroke={theme.palette.outline.main} + strokeWidth="1.5" + strokeDasharray="2.4 2.4" + /> + <rect + id="threshold" + x={x0} + y={yThreshold} + width={xMax - x0} + height={y0 - yThreshold} + fill={theme.palette.surface.surfaceContainerHighest} + /> + </g> + ); +}; + +export default ThresholdBar; diff --git a/src/ui/lib/components/Charts/MetricLine/index.tsx b/src/ui/lib/components/Charts/MetricLine/index.tsx new file mode 100644 index 00000000..22a9636d --- /dev/null +++ b/src/ui/lib/components/Charts/MetricLine/index.tsx @@ -0,0 +1,3 @@ +export { Component as MetricLine } from './MetricLine.component'; +export { LineColor } from './MetricLine.interface'; +export type { MetricLineProps } from './MetricLine.interface'; diff --git a/src/ui/lib/components/Charts/MiniCombined/MiniCombined.component.tsx b/src/ui/lib/components/Charts/MiniCombined/MiniCombined.component.tsx new file mode 100644 index 00000000..96341cfe --- /dev/null +++ b/src/ui/lib/components/Charts/MiniCombined/MiniCombined.component.tsx @@ -0,0 +1,131 @@ +import { useTheme } from '@mui/material'; +import { BarCustomLayerProps, ResponsiveBar } from '@nivo/bar'; +import { Point } from '@nivo/core'; +import React from 'react'; + +import useChartScales from '../common/useChartScales'; +import CustomBars from './components/CustomBars'; +import CustomGrid from './components/CustomGrid'; +import CustomTick from './components/CustomTick'; +import { MiniCombinedWithResizeProps } from './MiniCombined.interfaces'; + +export const Component = ({ + bars, + lines, + margins, + xLegend, + containerSize, +}: MiniCombinedWithResizeProps) => { + const theme = useTheme(); + const combinedGraphTheme = { + axis: { + legend: { + text: { + fill: theme.palette.surface.onSurface, + fontSize: theme.typography.axisTitle.fontSize, + fontWeight: theme.typography.axisTitle.fontWeight, + fontFamily: theme.typography.axisTitle.fontFamily, + }, + }, + ticks: { + text: { + fill: theme.palette.surface.onSurface, + }, + }, + }, + }; + const defaultMargins = { top: 10, left: 10, right: 10, bottom: 10 }; + const finalMargins = { + ...defaultMargins, + ...margins, + bottom: margins?.bottom || defaultMargins.bottom, + }; + const { xTicks, yTicks, fnScaleX, fnScaleY, innerHeight } = useChartScales({ + bars, + lines, + width: containerSize.width, + height: containerSize.height, + margins: finalMargins, + }); + + const CustomGridLayer = () => { + const scaledWidth = fnScaleX(xTicks[xTicks.length - 1]); + const scaledHeight = innerHeight - fnScaleY(yTicks[0]); + + return ( + <CustomGrid + xScale={(d: number) => fnScaleX(d)} + yScale={(d: number) => innerHeight - fnScaleY(d)} + width={scaledWidth} + height={innerHeight} + xTicks={xTicks} + yTicks={yTicks} + scaledHeight={scaledHeight} + /> + ); + }; + + const CustomBarLayer = (props: BarCustomLayerProps<Point>) => { + const heightOffset = innerHeight - fnScaleY(yTicks[0]); + return ( + <CustomBars + {...props} + xScaleFunc={(d: number) => fnScaleX(d)} + yScaleFunc={(d: number) => innerHeight - fnScaleY(d)} + heightOffset={heightOffset} + /> + ); + }; + return ( + <div + style={{ + width: containerSize.width + 'px', + height: containerSize.height + 'px', + position: 'relative', + }} + > + <ResponsiveBar + animate={true} + data={bars} + keys={['y']} + indexBy="x" + margin={{ + top: finalMargins.top, + right: finalMargins.right, + bottom: finalMargins.bottom, + left: finalMargins.left, + }} + padding={0.5} + colors={[theme.palette.primary.shades['0']]} + axisLeft={null} + axisBottom={{ + tickValues: xTicks, + legend: xLegend, + legendPosition: 'middle', + legendOffset: 20, + tickSize: 5, + tickPadding: 5, + tickRotation: 0, + renderTick: (tick) => ( + <CustomTick + isXAxis={true} + scale={(d: number) => fnScaleX(d)} + tick={xTicks[tick.tickIndex]} + isFirst={tick.tickIndex === 0} + isLast={tick.tickIndex === xTicks.length - 1} + /> + ), + }} + layers={['axes', CustomGridLayer, CustomBarLayer]} + theme={combinedGraphTheme} + /> + {/* <DottedLines + lines={lines} + leftMargin={finalMargins.left} + topMargin={finalMargins.top} + innerHeight={innerHeight} + xScale={fnScaleX} + /> */} + </div> + ); +}; diff --git a/src/ui/lib/components/Charts/MiniCombined/MiniCombined.interfaces.ts b/src/ui/lib/components/Charts/MiniCombined/MiniCombined.interfaces.ts new file mode 100644 index 00000000..6e2b6b13 --- /dev/null +++ b/src/ui/lib/components/Charts/MiniCombined/MiniCombined.interfaces.ts @@ -0,0 +1,15 @@ +import { LinesSeries, Margins, Point } from '../common/interfaces'; +import { ContainerSize } from './components/ContainerSizeWrapper'; + +export interface MiniCombinedProps { + bars: Point[]; + lines: LinesSeries[]; + width: number; + height: number; + margins?: Margins; + xLegend: string; +} + +export interface MiniCombinedWithResizeProps extends MiniCombinedProps { + containerSize: ContainerSize; +} diff --git a/src/ui/lib/components/Charts/MiniCombined/components/ContainerSizeWrapper/index.tsx b/src/ui/lib/components/Charts/MiniCombined/components/ContainerSizeWrapper/index.tsx new file mode 100644 index 00000000..032df6f8 --- /dev/null +++ b/src/ui/lib/components/Charts/MiniCombined/components/ContainerSizeWrapper/index.tsx @@ -0,0 +1,42 @@ +import React, { useState, useEffect, useRef } from 'react'; + +interface ContainerSizeWrapperProps { + children: (containerSize: ContainerSize) => React.ReactNode; +} + +export interface ContainerSize { + width: number; + height: number; +} + +const ContainerSizeWrapper: React.FC<ContainerSizeWrapperProps> = ({ children }) => { + const [containerSize, setContainerSize] = useState<ContainerSize>({ + width: 0, + height: 0, + }); + const containerRef = useRef<HTMLDivElement>(null); + + useEffect(() => { + const updateSize = () => { + if (containerRef.current) { + setContainerSize({ + width: containerRef.current.offsetWidth, + height: containerRef.current.offsetHeight, + }); + } + }; + + updateSize(); + window.addEventListener('resize', updateSize); + + return () => window.removeEventListener('resize', updateSize); + }, []); + + return ( + <div ref={containerRef} style={{ width: '100%', height: '100%' }}> + {children(containerSize)} + </div> + ); +}; + +export default ContainerSizeWrapper; diff --git a/src/ui/lib/components/Charts/MiniCombined/components/CustomBars/CustomBars.interfaces.ts b/src/ui/lib/components/Charts/MiniCombined/components/CustomBars/CustomBars.interfaces.ts new file mode 100644 index 00000000..faad401e --- /dev/null +++ b/src/ui/lib/components/Charts/MiniCombined/components/CustomBars/CustomBars.interfaces.ts @@ -0,0 +1,7 @@ +import { BarCustomLayerProps } from '@nivo/bar'; + +export interface CustomBarsProps<T> extends BarCustomLayerProps<T> { + xScaleFunc: (d: number) => number; + yScaleFunc: (d: number) => number; + heightOffset: number; +} diff --git a/src/ui/lib/components/Charts/MiniCombined/components/CustomBars/index.tsx b/src/ui/lib/components/Charts/MiniCombined/components/CustomBars/index.tsx new file mode 100644 index 00000000..5ecc6e27 --- /dev/null +++ b/src/ui/lib/components/Charts/MiniCombined/components/CustomBars/index.tsx @@ -0,0 +1,94 @@ +import { ComputedBarDatum } from '@nivo/bar'; +import { Point } from '@nivo/core'; +import { useTooltip, BasicTooltip } from '@nivo/tooltip'; +import React from 'react'; + +import { CustomBarsProps } from './CustomBars.interfaces'; + +const CustomBars = ({ + bars, + xScaleFunc, + yScaleFunc, + heightOffset, +}: CustomBarsProps<Point>) => { + const { showTooltipFromEvent, hideTooltip } = useTooltip(); + const minX = + bars.length === 1 + ? 0 + : Math.min(...bars.map((bar) => xScaleFunc(bar.data.data.x || 0))); + const maxX = Math.max(...bars.map((bar) => xScaleFunc(bar.data.data.x || 0))); + const handleMouseEnter = ( + event: React.MouseEvent<SVGPathElement>, + bar: ComputedBarDatum<Point> + ) => { + const x = xScaleFunc(bar.data.data.x || 0) || 0; + const normalizedPosition = (x - minX) / (maxX - minX); + const maxPadding = 80; + + let paddingLeft = 0; + let paddingRight = 0; + // log scale padding so that tooltip doesn't get cut off by the edges + if (normalizedPosition < 0.5) { + const logFactor = -Math.log(2 * normalizedPosition + 0.01) / Math.log(100); + paddingLeft = Math.min(maxPadding, Math.max(0, logFactor * maxPadding)); + } else { + const logFactor = -Math.log(2 * (1 - normalizedPosition) + 0.01) / Math.log(100); + paddingRight = Math.min(maxPadding, Math.max(0, logFactor * maxPadding)); + } + showTooltipFromEvent( + <div + style={{ + paddingLeft: `${paddingLeft}px`, + paddingRight: `${paddingRight}px`, + }} + > + <BasicTooltip + id={`x: ${bar.data.data.x || 0}, y: ${bar.data.data.y}`} + enableChip={true} + color={bar.color} + /> + </div>, + event + ); + }; + + const handleMouseLeave = () => { + hideTooltip(); + }; + + return ( + <g transform={`translate(0,-${heightOffset})`}> + {bars.map((bar) => { + const barWidth = Math.min(10, bar.width); + const maxRadius = Math.floor(barWidth / 2); + const x = xScaleFunc(Number(bar.data.data.x || 0)) - barWidth / 2; + const y = yScaleFunc(Number(bar.data.data.y)); + let r; + if (bar.height < 4) { + r = Math.min(1, maxRadius); + } else if (bar.height < barWidth) { + r = Math.min(Math.floor(bar.height / 2), maxRadius); + } else { + r = maxRadius; + } + r = Math.min(r, Math.floor(bar.height / 2)); + const v = Math.max(0, bar.height - r); + const path = `M ${x + r},${y} h ${ + barWidth - 2 * r + } a ${r},${r} 0 0 1 ${r},${r} v ${v} h -${barWidth} v -${v} a ${r},${r} 0 0 1 ${r},-${r} z`; + + return ( + <path + key={bar.key} + d={path} + fill={bar.color} + onMouseEnter={(event) => handleMouseEnter(event, bar)} + onMouseLeave={handleMouseLeave} + /> + ); + })} + </g> + ); +}; + +export default CustomBars; diff --git a/src/ui/lib/components/Charts/MiniCombined/components/CustomGrid/CustomGrid.interfaces.ts b/src/ui/lib/components/Charts/MiniCombined/components/CustomGrid/CustomGrid.interfaces.ts new file mode 100644 index 00000000..2e440fba --- /dev/null +++ b/src/ui/lib/components/Charts/MiniCombined/components/CustomGrid/CustomGrid.interfaces.ts @@ -0,0 +1,10 @@ +export interface CustomGridProps { + xScale: (d: number) => number; + yScale: (d: number) => number; + width: number; + height: number; + xTicks: number[]; + yTicks: number[]; + scaledHeight: number; + fullGrid?: boolean; +} diff --git a/src/ui/lib/components/Charts/MiniCombined/components/CustomGrid/index.tsx b/src/ui/lib/components/Charts/MiniCombined/components/CustomGrid/index.tsx new file mode 100644 index 00000000..0ef400b3 --- /dev/null +++ b/src/ui/lib/components/Charts/MiniCombined/components/CustomGrid/index.tsx @@ -0,0 +1,72 @@ +import { useTheme } from '@mui/material'; + +import { CustomGridProps } from './CustomGrid.interfaces'; + +const CustomGrid = ({ + xScale, + yScale, + width, + height, + xTicks, + yTicks, + scaledHeight, + fullGrid = false, +}: CustomGridProps) => { + const theme = useTheme(); + // const xTick = xTicks[0]; + const yTick = yTicks[yTicks.length - 1]; + + const renderAxlesOnly = ( + <> + {/*<line*/} + {/* key={`x${xTick}`}*/} + {/* x1={xScale(xTick)}*/} + {/* x2={xScale(xTick)}*/} + {/* y1={scaledHeight}*/} + {/* y2={height}*/} + {/* stroke={theme.palette.outline.subdued}*/} + {/*/>*/} + <line + key={`y${yTick}`} + x1={0} + x2={width} + y1={yScale(yTick)} + y2={yScale(yTick)} + stroke={theme.palette.outline.subdued} + /> + </> + ); + + const renderFullGrid = ( + <> + {xTicks.map((tick) => ( + <line + key={`x${tick}`} + x1={xScale(tick)} + x2={xScale(tick)} + y1={scaledHeight} + y2={height} + stroke={theme.palette.outline.subdued} + /> + ))} + {yTicks.map((tick) => ( + <line + key={`y${tick}`} + x1={0} + x2={width} + y1={yScale(tick)} + y2={yScale(tick)} + stroke={theme.palette.outline.subdued} + /> + ))} + </> + ); + + return ( + <g transform={`translate(0,-${scaledHeight})`} id="grid"> + {fullGrid ? renderFullGrid : renderAxlesOnly} + </g> + ); +}; + +export default CustomGrid; diff --git a/src/ui/lib/components/Charts/MiniCombined/components/CustomTick/CustomTick.interfaces.ts b/src/ui/lib/components/Charts/MiniCombined/components/CustomTick/CustomTick.interfaces.ts new file mode 100644 index 00000000..efe5ef04 --- /dev/null +++ b/src/ui/lib/components/Charts/MiniCombined/components/CustomTick/CustomTick.interfaces.ts @@ -0,0 +1,8 @@ +export interface CustomTickProps { + scale: (d: number) => number; + isXAxis: boolean; + tick: number; + withTicks?: boolean; + isFirst: boolean; + isLast: boolean; +} diff --git a/src/ui/lib/components/Charts/MiniCombined/components/CustomTick/index.tsx b/src/ui/lib/components/Charts/MiniCombined/components/CustomTick/index.tsx new file mode 100644 index 00000000..c7941c97 --- /dev/null +++ b/src/ui/lib/components/Charts/MiniCombined/components/CustomTick/index.tsx @@ -0,0 +1,62 @@ +import { useTheme } from '@mui/material'; + +import { CustomTickProps } from './CustomTick.interfaces'; + +const CustomTick = ({ + isXAxis, + tick, + scale, + withTicks = false, + isFirst, + isLast, +}: CustomTickProps) => { + const theme = useTheme(); + + if (isXAxis && !isFirst && !isLast) { + return null; + } + + const getGroupPosition = () => { + if (isXAxis) { + let x = scale(tick); + if (isFirst) { + x = 4; + } + if (isLast) { + x -= 4; + } + return { x, y: 0 }; + } + return { x: 0, y: scale(tick) }; + }; + + const renderTickContent = (textAnchor: 'middle' | 'end', x: number, y: number) => ( + <> + {withTicks && ( + <line x1={0} y1={0} x2={x} y2={y} stroke={theme.palette.surface.onSurface} /> + )} + <text + textAnchor={textAnchor} + x={x === 0 ? undefined : x} + y={y === 0 ? undefined : y} + dominantBaseline={isXAxis ? undefined : 'middle'} + fontFamily={theme.typography.axisLabel.fontFamily} + fontWeight={theme.typography.axisLabel.fontWeight} + fontSize={theme.typography.axisLabel.fontSize} + fill={theme.palette.surface.onSurface} + > + {tick} + </text> + </> + ); + + const { x, y } = getGroupPosition(); + + return ( + <g key={tick} transform={`translate(${x}, ${y})`}> + {isXAxis ? renderTickContent('middle', 0, 10) : renderTickContent('end', -5, 0)} + </g> + ); +}; + +export default CustomTick; diff --git a/src/ui/lib/components/Charts/MiniCombined/components/DottedLines/DottedLines.interfaces.ts b/src/ui/lib/components/Charts/MiniCombined/components/DottedLines/DottedLines.interfaces.ts new file mode 100644 index 00000000..be9085ee --- /dev/null +++ b/src/ui/lib/components/Charts/MiniCombined/components/DottedLines/DottedLines.interfaces.ts @@ -0,0 +1,9 @@ +import { Point } from '../../../common/interfaces'; + +export interface DottedLinesProps { + lines: Point[]; + leftMargin: number; + topMargin: number; + innerHeight: number; + xScale: (d: number) => number; +} diff --git a/src/ui/lib/components/Charts/MiniCombined/components/DottedLines/index.tsx b/src/ui/lib/components/Charts/MiniCombined/components/DottedLines/index.tsx new file mode 100644 index 00000000..52b8cbce --- /dev/null +++ b/src/ui/lib/components/Charts/MiniCombined/components/DottedLines/index.tsx @@ -0,0 +1,37 @@ +import useLineColors from '../../../common/useLineColors'; + +import { DottedLinesProps } from './DottedLines.interfaces'; + +const DottedLines = ({ + lines, + leftMargin, + topMargin, + xScale, + innerHeight, +}: DottedLinesProps) => { + const lineColor = useLineColors(); + return ( + <svg + style={{ position: 'absolute', top: 0, left: 0, pointerEvents: 'none' }} + width="100%" + height="100%" + > + <g transform={`translate(${leftMargin}, ${topMargin})`}> + {lines.map((line, i) => ( + <line + key={i} + x1={xScale(line.x)} + y1="0" + x2={xScale(line.x)} + y2={innerHeight} + stroke={lineColor[i]} + strokeWidth="1.5" + strokeDasharray="3,3" + /> + ))} + </g> + </svg> + ); +}; + +export default DottedLines; diff --git a/src/ui/lib/components/Charts/MiniCombined/index.tsx b/src/ui/lib/components/Charts/MiniCombined/index.tsx new file mode 100644 index 00000000..6e363fdc --- /dev/null +++ b/src/ui/lib/components/Charts/MiniCombined/index.tsx @@ -0,0 +1,5 @@ +export { Component as MiniCombined } from './MiniCombined.component'; +export type { + MiniCombinedProps, + MiniCombinedWithResizeProps, +} from './MiniCombined.interfaces'; diff --git a/src/ui/lib/components/Charts/common/interfaces.ts b/src/ui/lib/components/Charts/common/interfaces.ts new file mode 100644 index 00000000..468fb4af --- /dev/null +++ b/src/ui/lib/components/Charts/common/interfaces.ts @@ -0,0 +1,34 @@ +import { MiniCombinedProps } from '../MiniCombined/MiniCombined.interfaces'; + +export type Point = { + x: number; + y: number; +}; + +export type LinesSeries = Point & { + id: string; +}; + +export type Margins = { + top?: number; + bottom?: number; + left?: number; + right?: number; +}; + +type RequiredCombinedProps = Required<MiniCombinedProps>; +type RequiredMargins = Required<Margins>; +type BaseChartScalesProps = Omit<RequiredCombinedProps, 'xLegend' | 'yLegend'>; +export type ChartScalesProps = BaseChartScalesProps & { + margins: RequiredMargins; +}; + +export interface CombinedProps { + bars: Point[]; + lines: LinesSeries[]; + width: number; + height: number; + margins?: Margins; + xLegend: string; + yLegend: string; +} diff --git a/src/ui/lib/components/Charts/common/useChartScales.ts b/src/ui/lib/components/Charts/common/useChartScales.ts new file mode 100644 index 00000000..43f0a81f --- /dev/null +++ b/src/ui/lib/components/Charts/common/useChartScales.ts @@ -0,0 +1,51 @@ +import { useMemo } from 'react'; + +import { ChartScalesProps } from './interfaces'; + +export const calculateStep = (min: number, max: number) => { + const range = max - min; + const approxStep = range / 10; + const magnitude = Math.pow(10, Math.floor(Math.log10(approxStep))); + return Math.ceil(approxStep / magnitude) * magnitude; +}; + +const useChartScales = ({ bars, width, height, margins }: ChartScalesProps) => { + return useMemo(() => { + const xValues = bars.map((d) => d.x); + const yValues = bars.map((d) => d.y); + + const xMin = xValues.length === 1 ? 0 : Math.min(...xValues); + const xMax = Math.max(...xValues); + const yMin = Math.min(...yValues) > 0 ? 0 : Math.min(...yValues); + const yMax = Math.max(...yValues); + + const xStep = calculateStep(xMin, xMax); + const yStep = calculateStep(yMin, yMax); + + const xTicks = Array.from( + { length: Math.ceil((xMax - xMin) / xStep) + 1 }, + (_, i) => xMin + i * xStep + ); + const yTicks = Array.from( + { length: Math.ceil((yMax - yMin) / yStep) + 1 }, + (_, i) => yMin + i * yStep + ).reverse(); + + const innerHeight = height - margins.top - margins.bottom; + const innerWidth = width - margins.left - margins.right; + const fnScaleX = (d: number) => + innerWidth * Math.min(1, (d - xMin) / (xMax - xMin)); + const fnScaleY = (d: number) => (innerHeight * (d - yMin)) / (yMax - yMin); + + return { + xTicks, + yTicks, + fnScaleX, + fnScaleY, + innerWidth, + innerHeight, + }; + }, [bars, width, height, margins]); +}; + +export default useChartScales; diff --git a/src/ui/lib/components/Charts/common/useLineColors.tsx b/src/ui/lib/components/Charts/common/useLineColors.tsx new file mode 100644 index 00000000..b0c9fe07 --- /dev/null +++ b/src/ui/lib/components/Charts/common/useLineColors.tsx @@ -0,0 +1,14 @@ +import { useTheme } from '@mui/material'; + +const useLineColors = () => { + const theme = useTheme(); + const palette = theme.palette; + return [ + palette.primary.shades.W100, + palette.secondary.main, + palette.tertiary.main, + palette.quarternary.main, + ]; +}; + +export default useLineColors; diff --git a/src/ui/lib/components/DataPanel/DataPanel.component.tsx b/src/ui/lib/components/DataPanel/DataPanel.component.tsx new file mode 100644 index 00000000..19d94e30 --- /dev/null +++ b/src/ui/lib/components/DataPanel/DataPanel.component.tsx @@ -0,0 +1,35 @@ +import { Typography } from '@mui/material'; + +import { DataPanelProps } from './DataPanel.interfaces'; +import { + BottomCell, + InnerContainer, + TopCell, + HeaderContainer, +} from './DataPanel.styles'; + +export const Component = ({ + header, + topContainer, + bottomContainer, +}: DataPanelProps) => { + return ( + <InnerContainer item xs={4}> + <HeaderContainer item xs={12}> + <Typography variant="overline1" color="surface.onSurface"> + {header} + </Typography> + </HeaderContainer> + + <TopCell item xs={12} data-id="top-cell"> + {topContainer} + </TopCell> + + {bottomContainer && ( + <BottomCell item xs={12} data-id="bottom-cell"> + {bottomContainer} + </BottomCell> + )} + </InnerContainer> + ); +}; diff --git a/src/ui/lib/components/DataPanel/DataPanel.interfaces.ts b/src/ui/lib/components/DataPanel/DataPanel.interfaces.ts new file mode 100644 index 00000000..3ea55a8d --- /dev/null +++ b/src/ui/lib/components/DataPanel/DataPanel.interfaces.ts @@ -0,0 +1,7 @@ +import { ReactNode } from 'react'; + +export interface DataPanelProps { + header: string; + topContainer: ReactNode; + bottomContainer?: ReactNode; +} diff --git a/src/ui/lib/components/DataPanel/DataPanel.styles.tsx b/src/ui/lib/components/DataPanel/DataPanel.styles.tsx new file mode 100644 index 00000000..7c364ab8 --- /dev/null +++ b/src/ui/lib/components/DataPanel/DataPanel.styles.tsx @@ -0,0 +1,35 @@ +import { Grid, styled } from '@mui/material'; + +import { HeaderContainer as HeaderContainerBase } from '../MetricsContainer/MetricsContainer.styles'; + +export const InnerContainer = styled(Grid)(({ theme }) => ({ + borderWidth: '1px', + borderStyle: 'solid', + borderColor: theme.palette.outline.subdued, + borderRadius: '8px', + overflow: 'hidden', + backgroundColor: theme.palette.surface.surfaceContainerLow, +})); + +export const TopCell = styled(Grid)({ + display: 'flex', + alignItems: 'flex-start', + height: '180px', + padding: '24px !important', +}); + +export const BottomCell = styled(Grid)(({ theme }) => ({ + display: 'flex', + alignItems: 'center', + minHeight: '220px', + height: '180px', + borderTopWidth: '1px', + borderTopStyle: 'solid', + borderTopColor: theme.palette.outline.subdued, + padding: '24px !important', +})); + +export const HeaderContainer = styled(HeaderContainerBase)({ + display: 'flex', + justifyContent: 'flex-start', +}); diff --git a/src/ui/lib/components/DataPanel/index.tsx b/src/ui/lib/components/DataPanel/index.tsx new file mode 100644 index 00000000..73cfde05 --- /dev/null +++ b/src/ui/lib/components/DataPanel/index.tsx @@ -0,0 +1,2 @@ +export { Component as DataPanel } from './DataPanel.component'; +export type { DataPanelProps } from './DataPanel.interfaces'; diff --git a/src/ui/lib/components/DistributionPercentiles/DistributionPercentiles.component.tsx b/src/ui/lib/components/DistributionPercentiles/DistributionPercentiles.component.tsx new file mode 100644 index 00000000..b9cadc06 --- /dev/null +++ b/src/ui/lib/components/DistributionPercentiles/DistributionPercentiles.component.tsx @@ -0,0 +1,26 @@ +import { Box, Typography } from '@mui/material'; + +import { Badge } from '../Badge'; +import { Section } from '../Section'; +import { DistributionPercentilesProps } from './DistributionPercentiles.interfaces'; + +export const Component = ({ list, rpsValue, units }: DistributionPercentilesProps) => { + return ( + <Box> + <Box flexDirection="row" display="flex" alignItems="center" gap={'12px'}> + <Typography + variant="overline1" + color="surface.onSurfaceSubdued" + textTransform="uppercase" + > + Distribution At + </Typography> + <Badge label={`${rpsValue} rps`} /> + </Box> + + {list.map((item) => ( + <Section key={item.label} label={item.label} value={`${item.value} ${units}`} /> + ))} + </Box> + ); +}; diff --git a/src/ui/lib/components/DistributionPercentiles/DistributionPercentiles.interfaces.tsx b/src/ui/lib/components/DistributionPercentiles/DistributionPercentiles.interfaces.tsx new file mode 100644 index 00000000..23e86cfb --- /dev/null +++ b/src/ui/lib/components/DistributionPercentiles/DistributionPercentiles.interfaces.tsx @@ -0,0 +1,10 @@ +export type PercentileItem = { + label: string; + value: string; +}; + +export interface DistributionPercentilesProps { + list: PercentileItem[]; + rpsValue: number; + units: string; +} diff --git a/src/ui/lib/components/DistributionPercentiles/DistributionPercentiles.styles.tsx b/src/ui/lib/components/DistributionPercentiles/DistributionPercentiles.styles.tsx new file mode 100644 index 00000000..19c7bd34 --- /dev/null +++ b/src/ui/lib/components/DistributionPercentiles/DistributionPercentiles.styles.tsx @@ -0,0 +1,8 @@ +import { Box, styled } from '@mui/material'; + +export const BadgeContainer = styled(Box)(({ theme }) => ({ + backgroundColor: theme.palette.surface.surfaceContainerHigh, + borderRadius: '6px', + padding: '4px 6px', + marginLeft: '12px', +})); diff --git a/src/ui/lib/components/DistributionPercentiles/index.tsx b/src/ui/lib/components/DistributionPercentiles/index.tsx new file mode 100644 index 00000000..2e75fad0 --- /dev/null +++ b/src/ui/lib/components/DistributionPercentiles/index.tsx @@ -0,0 +1,5 @@ +export { Component as DistributionPercentiles } from './DistributionPercentiles.component'; +export type { + PercentileItem, + DistributionPercentilesProps, +} from './DistributionPercentiles.interfaces'; diff --git a/src/ui/lib/components/GraphTitle/GraphTitle.component.tsx b/src/ui/lib/components/GraphTitle/GraphTitle.component.tsx new file mode 100644 index 00000000..a7a40602 --- /dev/null +++ b/src/ui/lib/components/GraphTitle/GraphTitle.component.tsx @@ -0,0 +1,16 @@ +import { Typography, useTheme } from '@mui/material'; + +import { GraphTitleProps } from './GraphTitle.interfaces'; + +export const Component = ({ title }: GraphTitleProps) => { + const theme = useTheme(); + return ( + <Typography + variant="subtitle2" + color={theme.palette.surface.onSurfaceSubdued} + mb={2} + > + {title} + </Typography> + ); +}; diff --git a/src/ui/lib/components/GraphTitle/GraphTitle.interfaces.ts b/src/ui/lib/components/GraphTitle/GraphTitle.interfaces.ts new file mode 100644 index 00000000..70853edb --- /dev/null +++ b/src/ui/lib/components/GraphTitle/GraphTitle.interfaces.ts @@ -0,0 +1,3 @@ +export interface GraphTitleProps { + title: string; +} diff --git a/src/ui/lib/components/GraphTitle/index.tsx b/src/ui/lib/components/GraphTitle/index.tsx new file mode 100644 index 00000000..0dad3c2b --- /dev/null +++ b/src/ui/lib/components/GraphTitle/index.tsx @@ -0,0 +1,2 @@ +export { Component as GraphTitle } from './GraphTitle.component'; +export type { GraphTitleProps } from './GraphTitle.interfaces'; diff --git a/src/ui/lib/components/Input/Input.component.tsx b/src/ui/lib/components/Input/Input.component.tsx new file mode 100644 index 00000000..2fd68b6b --- /dev/null +++ b/src/ui/lib/components/Input/Input.component.tsx @@ -0,0 +1,64 @@ +import { Box, Typography } from '@mui/material'; +import React, { FC } from 'react'; + +import { useColor } from '@/lib/hooks/useColor'; + +import { InputProps } from './Input.interfaces'; +import { StyledTextField, InputContainer, ErrorMessage } from './Input.styles'; + +export const Component: FC<InputProps> = ({ + label, + prefix, + disabled = false, + value, + onChange, + fullWidth = false, + fontColor, + error, + isNumber = false, +}) => { + const selectedColor = useColor(fontColor); + return ( + <Box> + <InputContainer + className={disabled ? 'disabled' : ''} + fullWidth={fullWidth} + data-id="input-wrapper" + > + <Typography variant="overline2" color="surface.onSurface"> + {label} + </Typography> + <Box display="flex" alignItems="baseline"> + {prefix && ( + <Typography + variant="body1" + color="surface.onSurfaceAccent" + sx={{ marginRight: 1 }} + > + {prefix} + </Typography> + )} + <StyledTextField + fontColor={selectedColor} + disabled={disabled} + placeholder={label} + variant="standard" + type="number" + value={value} + onChange={onChange} + {...(isNumber && { + inputProps: { + type: 'number', + step: '1', + min: '0', + }, + })} + /> + </Box> + </InputContainer> + <ErrorMessage variant="caption" color="error.main"> + {error || ' '} + </ErrorMessage> + </Box> + ); +}; diff --git a/src/ui/lib/components/Input/Input.interfaces.ts b/src/ui/lib/components/Input/Input.interfaces.ts new file mode 100644 index 00000000..820132c3 --- /dev/null +++ b/src/ui/lib/components/Input/Input.interfaces.ts @@ -0,0 +1,23 @@ +import { ChangeEvent } from 'react'; + +import { LineColor } from '@/lib/components/Charts/MetricLine'; + +export interface InputProps { + label: string; + prefix?: string; + disabled?: boolean; + value: number | undefined; + onChange: (event: ChangeEvent<HTMLInputElement>) => void; + fullWidth?: boolean; + fontColor?: LineColor; + error?: string | undefined; + isNumber?: boolean; +} + +export type CustomInputContainer = { + fullWidth?: boolean; +}; + +export type CustomInputProps = { + fontColor: string; +}; diff --git a/src/ui/lib/components/Input/Input.styles.tsx b/src/ui/lib/components/Input/Input.styles.tsx new file mode 100644 index 00000000..1d5a832e --- /dev/null +++ b/src/ui/lib/components/Input/Input.styles.tsx @@ -0,0 +1,60 @@ +import { Box, styled, TextField, Typography } from '@mui/material'; + +import { CustomInputContainer, CustomInputProps } from './Input.interfaces'; + +export const StyledTextField = styled(TextField, { + shouldForwardProp: (propName) => propName !== 'fontColor', +})<CustomInputProps>(({ theme, fontColor }) => ({ + '& .MuiInputBase-input': { + color: fontColor, + fontSize: theme.typography.body1.fontSize, + fontWeight: theme.typography.body1.fontWeight, + padding: '0', + }, + '& .MuiInput-underline:before': { + borderBottomColor: 'transparent', + }, + '& .MuiInput-underline:hover:not(.Mui-disabled):before': { + borderBottomColor: 'transparent', + }, + '& .MuiInput-underline:after': { + borderBottomColor: 'transparent', + }, + '& input[type=number]': { + MozAppearance: 'textfield', + }, + '& input[type=number]::-webkit-outer-spin-button': { + WebkitAppearance: 'none', + margin: 0, + }, + '& input[type=number]::-webkit-inner-spin-button': { + WebkitAppearance: 'none', + margin: 0, + }, + '& .Mui-disabled': { + color: theme.palette.surface.onSurfaceSubdued + ' !important', + WebkitTextFillColor: theme.palette.surface.onSurfaceSubdued + ' !important', + }, +})); + +export const InputContainer = styled(Box, { + shouldForwardProp: (propName) => propName !== 'fullWidth', +})<CustomInputContainer>(({ theme, fullWidth }) => ({ + backgroundColor: theme.palette.surface.surfaceContainerHigh, + marginBottom: '8px', + padding: '8px 12px 11px 12px', + display: fullWidth ? 'block' : 'inline-block', + borderRadius: '8px', + minWidth: '168px', + width: 'auto', + flex: 1, + '&.disabled': { opacity: 0.4 }, +})); + +export const ErrorMessage = styled(Typography)({ + display: 'flex', + justifyContent: 'center', + alignItems: 'center', + textAlign: 'center', + minHeight: '15px', +}); diff --git a/src/ui/lib/components/Input/index.tsx b/src/ui/lib/components/Input/index.tsx new file mode 100644 index 00000000..70ca783d --- /dev/null +++ b/src/ui/lib/components/Input/index.tsx @@ -0,0 +1,2 @@ +export { Component as Input } from './Input.component'; +export type { InputProps } from './Input.interfaces'; diff --git a/src/ui/lib/components/MeanMetricSummary/MeanMetricSummary.component.tsx b/src/ui/lib/components/MeanMetricSummary/MeanMetricSummary.component.tsx new file mode 100644 index 00000000..904db20a --- /dev/null +++ b/src/ui/lib/components/MeanMetricSummary/MeanMetricSummary.component.tsx @@ -0,0 +1,30 @@ +import { Box, Typography } from '@mui/material'; + +import { Badge } from '../Badge'; +import { MeanMetricSummaryProps } from './MeanMetricSummary.interfaces'; + +export const Component = ({ + meanValue, + meanUnit, + rpsValue, +}: MeanMetricSummaryProps) => { + return ( + <Box flexDirection="column"> + <Box flexDirection="row" display="flex" alignItems="center" gap={'12px'}> + <Typography + variant="overline1" + color="surface.onSurfaceSubdued" + textTransform="uppercase" + > + Mean At + </Typography> + <Badge label={`${rpsValue} rps`} /> + </Box> + <Box flexDirection="row" display="flex"> + <Typography variant="h4" color="surface.onSurfaceAccent" pt={1}> + {meanValue} {meanUnit} + </Typography> + </Box> + </Box> + ); +}; diff --git a/src/ui/lib/components/MeanMetricSummary/MeanMetricSummary.interfaces.tsx b/src/ui/lib/components/MeanMetricSummary/MeanMetricSummary.interfaces.tsx new file mode 100644 index 00000000..d7561593 --- /dev/null +++ b/src/ui/lib/components/MeanMetricSummary/MeanMetricSummary.interfaces.tsx @@ -0,0 +1,5 @@ +export interface MeanMetricSummaryProps { + meanValue: string; + meanUnit: string; + rpsValue: number; +} diff --git a/src/ui/lib/components/MeanMetricSummary/index.tsx b/src/ui/lib/components/MeanMetricSummary/index.tsx new file mode 100644 index 00000000..86d23f77 --- /dev/null +++ b/src/ui/lib/components/MeanMetricSummary/index.tsx @@ -0,0 +1,2 @@ +export { Component as MeanMetricSummary } from './MeanMetricSummary.component'; +export type { MeanMetricSummaryProps } from './MeanMetricSummary.interfaces'; diff --git a/src/ui/lib/components/MetricsContainer/MetricsContainer.component.tsx b/src/ui/lib/components/MetricsContainer/MetricsContainer.component.tsx new file mode 100644 index 00000000..456d14d4 --- /dev/null +++ b/src/ui/lib/components/MetricsContainer/MetricsContainer.component.tsx @@ -0,0 +1,68 @@ +import { Grid, Typography } from '@mui/material'; + +import { MetricsContainerProps } from './MetricsContainer.interfaces'; +import { + HeaderContainer, + InnerContainer, + MainContainer, + RightColumn, +} from './MetricsContainer.styles'; + +export const Component = ({ + header, + leftColumn, + rightColumn, + children, +}: MetricsContainerProps) => { + return ( + <InnerContainer container spacing={2}> + <HeaderContainer + xs={12} + item + flexDirection="row" + display="flex" + justifyContent="space-between" + > + <Typography variant="overline1" color="surface.onSurface"> + {header} + </Typography> + </HeaderContainer> + + <MainContainer + xs={12} + item + padding="24px" + flexDirection="column" + justifyContent="center" + alignItems="center" + display="flex" + > + {children} + </MainContainer> + + {/* middle containers */} + <Grid + xs={rightColumn ? 6 : 12} + item + display="flex" + alignItems="center" + padding="24px" + minHeight="232px" + > + {leftColumn} + </Grid> + {rightColumn && ( + <RightColumn + xs={6} + item + display="flex" + alignItems="center" + padding="24px" + minHeight="232px" + > + {rightColumn} + </RightColumn> + )} + </InnerContainer> + ); +}; diff --git a/src/ui/lib/components/MetricsContainer/MetricsContainer.interfaces.ts b/src/ui/lib/components/MetricsContainer/MetricsContainer.interfaces.ts new file mode 100644 index 00000000..cd709539 --- /dev/null +++ b/src/ui/lib/components/MetricsContainer/MetricsContainer.interfaces.ts @@ -0,0 +1,8 @@ +import { ReactNode } from 'react'; + +export interface MetricsContainerProps { + header: string; + leftColumn: ReactNode; + rightColumn?: ReactNode; + children: ReactNode; +} diff --git a/src/ui/lib/components/MetricsContainer/MetricsContainer.styles.tsx b/src/ui/lib/components/MetricsContainer/MetricsContainer.styles.tsx new file mode 100644 index 00000000..2117afaa --- /dev/null +++ b/src/ui/lib/components/MetricsContainer/MetricsContainer.styles.tsx @@ -0,0 +1,33 @@ +import { Grid, styled } from '@mui/material'; + +export const InnerContainer = styled(Grid)(({ theme }) => ({ + borderWidth: '1px', + borderStyle: 'solid', + borderColor: theme.palette.outline.subdued, + borderRadius: '8px', + overflow: 'hidden', + backgroundColor: theme.palette.surface.surfaceContainerLow, + marginLeft: 0, + marginTop: 0, +})); + +export const HeaderContainer = styled(Grid)(({ theme }) => ({ + backgroundColor: theme.palette.surface.surfaceContainer, + borderBottomWidth: '1px', + borderBottomStyle: 'solid', + borderBottomColor: theme.palette.outline.subdued, + padding: '18px 24px', + margin: 0, +})); + +export const RightColumn = styled(Grid)(({ theme }) => ({ + borderLeftWidth: '1px', + borderLeftStyle: 'solid', + borderLeftColor: theme.palette.outline.subdued, +})); + +export const MainContainer = styled(Grid)(({ theme }) => ({ + borderBottomWidth: '1px', + borderBottomStyle: 'solid', + borderBottomColor: theme.palette.outline.subdued, +})); diff --git a/src/ui/lib/components/MetricsContainer/index.tsx b/src/ui/lib/components/MetricsContainer/index.tsx new file mode 100644 index 00000000..d6bbc5f2 --- /dev/null +++ b/src/ui/lib/components/MetricsContainer/index.tsx @@ -0,0 +1,2 @@ +export { Component as MetricsContainer } from './MetricsContainer.component'; +export type { MetricsContainerProps } from './MetricsContainer.interfaces'; diff --git a/src/ui/lib/components/MetricsSummary/MetricSummary.interfaces.ts b/src/ui/lib/components/MetricsSummary/MetricSummary.interfaces.ts new file mode 100644 index 00000000..b806b4e6 --- /dev/null +++ b/src/ui/lib/components/MetricsSummary/MetricSummary.interfaces.ts @@ -0,0 +1,3 @@ +export type CustomSelectProps = { + placeholder?: string; +}; diff --git a/src/ui/lib/components/MetricsSummary/MetricsSummary.component.tsx b/src/ui/lib/components/MetricsSummary/MetricsSummary.component.tsx new file mode 100644 index 00000000..ef10d209 --- /dev/null +++ b/src/ui/lib/components/MetricsSummary/MetricsSummary.component.tsx @@ -0,0 +1,311 @@ +import { Box, Button, Typography, useTheme } from '@mui/material'; +import React, { ElementType } from 'react'; +import { useDispatch, useSelector } from 'react-redux'; + +import { Expand } from '@assets/icons'; + +import { + selectInterpolatedMetrics, + selectMetricsSummaryLineData, + useGetBenchmarksQuery, +} from '../../store/slices/benchmarks'; +import { selectRunInfo } from '../../store/slices/runInfo'; +import { formatNumber } from '../../utils/helpers'; +import { MetricLine, LineColor } from '@/lib/components/Charts/MetricLine'; +import { selectSloState } from '@/lib/store/slices/slo/slo.selectors'; +import { setCurrentRequestRate } from '@/lib/store/slices/slo/slo.slice'; + +import { BlockHeader } from '../BlockHeader'; +import { Input } from '../Input'; +import { MetricValue } from './components/MetricValue'; +import { + CustomSlider, + FieldCell, + FieldsContainer, + FooterLeftCell, + FooterRightCell, + GraphContainer, + HeaderLeftCell, + HeaderRightCell, + MetricsSummaryContainer, + MiddleColumn, + InputContainer, + StyledSelect, + StyledInputLabel, + OptionItem, + StyledFormControl, +} from './MetricsSummary.styles'; +import { useSummary } from './useSummary'; + +const percentileOptions = ['p50', 'p90', 'p95', 'p99']; + +export const Component = () => { + const theme = useTheme(); + const dispatch = useDispatch(); + const { data } = useGetBenchmarksQuery(); + + const lineDataByRps = useSelector(selectMetricsSummaryLineData); + const interpolatedMetricData = useSelector(selectInterpolatedMetrics); + const runInfo = useSelector(selectRunInfo); + + const { currentRequestRate } = useSelector(selectSloState); + const handleSliderChange = (event: Event, newValue: number | number[]) => { + dispatch(setCurrentRequestRate(newValue as number)); + }; + + const { + ttft: ttftSLO, + tpot: tpotSLO, + timePerRequest: timePerRequestSLO, + throughput: throughputSLO, + percentile, + minX, + maxX, + errors, + handleTtft, + handleTpot, + handleTimePerRequest, + handleThroughput, + handlePercentileChange, + handleReset, + } = useSummary(); + + const isTtftMatch = Boolean( + ttftSLO && interpolatedMetricData.ttft.enforcedPercentileValue <= ttftSLO + ); + const isTpotMatch = Boolean( + tpotSLO && interpolatedMetricData.tpot.enforcedPercentileValue <= tpotSLO + ); + const isTprMatch = Boolean( + timePerRequestSLO && + interpolatedMetricData.timePerRequest.enforcedPercentileValue <= timePerRequestSLO + ); + const isThroughputMatch = Boolean( + throughputSLO && + interpolatedMetricData.throughput.enforcedPercentileValue >= throughputSLO + ); + + const sliderMarks = [ + { + value: minX, + label: minX, + }, + { + value: maxX, + label: maxX, + }, + ]; + + if ((data?.benchmarks?.length ?? 0) <= 1) { + return <></>; + } + + return ( + <> + <BlockHeader label="Metrics Summary" /> + <MetricsSummaryContainer container> + <HeaderLeftCell item xs={9}> + <Box display="flex" flexDirection="row" justifyContent="space-between"> + <Typography variant="h6" color="surface.onSurface" mb={2}> + Service Level Objectives for{' '} + <span style={{ color: theme.palette.surface.onSurfaceAccent }}> + [{runInfo?.task || 'N/A'}] + </span> + : + </Typography> + + <Button onClick={handleReset}> + <Typography variant="button" color="surface.onSurfaceAccent" mb={2}> + RESET TO DEFAULT + </Typography> + </Button> + </Box> + + <FieldsContainer data-id="fields-container"> + <FieldCell data-id="field-cell-1"> + <Input + label="TTFT (ms)" + value={ttftSLO} + onChange={handleTtft} + fullWidth + fontColor={LineColor.Primary} + error={errors?.ttft} + /> + </FieldCell> + <FieldCell data-id="field-cell-2"> + <Input + label="TPOT (ms)" + value={tpotSLO} + onChange={handleTpot} + fullWidth + fontColor={LineColor.Secondary} + error={errors?.tpot} + /> + </FieldCell> + <FieldCell data-id="field-cell-3"> + <Input + label="TIME PER REQUEST (Ms)" + value={timePerRequestSLO} + onChange={handleTimePerRequest} + fullWidth + fontColor={LineColor.Tertiary} + error={errors?.timePerRequest} + /> + </FieldCell> + <FieldCell data-id="field-cell-4"> + <Input + label="THROUGHPUT (tok/s)" + value={throughputSLO} + onChange={handleThroughput} + fullWidth + fontColor={LineColor.Quarternary} + error={errors?.throughput} + /> + </FieldCell> + </FieldsContainer> + </HeaderLeftCell> + <HeaderRightCell item xs={3}> + <Typography variant="h6" color="surface.onSurface" mb={'23px'}> + Observed Values at: + </Typography> + + <InputContainer> + <StyledFormControl + fullWidth + sx={{ fieldset: { legend: { maxWidth: '100%' } } }} + > + <StyledInputLabel shrink={true}>percentile</StyledInputLabel> + <StyledSelect + value={percentile} + onChange={handlePercentileChange} + autoWidth + placeholder="Select percentile" + IconComponent={Expand as ElementType} + displayEmpty + MenuProps={{ + PaperProps: { + sx: { + backgroundColor: theme.palette.surface.surfaceContainerLow, + borderRadius: '8px', + }, + }, + }} + > + {percentileOptions.map((value) => ( + <OptionItem key={value} value={value} sx={{ minWidth: '168px' }}> + {value} + </OptionItem> + ))} + </StyledSelect> + </StyledFormControl> + </InputContainer> + </HeaderRightCell> + {/* graphs */} + + <MiddleColumn sx={{ paddingLeft: '0px !important' }} item xs={9}> + <GraphContainer> + <MetricLine + data={[{ id: 'ttft', data: lineDataByRps.ttft || [] }]} + threshold={ttftSLO} + lineColor={LineColor.Primary} + /> + </GraphContainer> + </MiddleColumn> + <MiddleColumn item xs={3}> + <MetricValue + label="TTFT" + value={`${formatNumber(interpolatedMetricData.ttft.enforcedPercentileValue)} ms`} + match={isTtftMatch} + valueColor={LineColor.Primary} + /> + </MiddleColumn> + + <MiddleColumn sx={{ paddingLeft: '0px !important' }} item xs={9}> + <GraphContainer> + <MetricLine + data={[{ id: 'tpot', data: lineDataByRps.tpot || [] }]} + threshold={tpotSLO} + lineColor={LineColor.Secondary} + /> + </GraphContainer> + </MiddleColumn> + <MiddleColumn item xs={3}> + <MetricValue + label="TPOT" + value={`${formatNumber(interpolatedMetricData.tpot.enforcedPercentileValue)} ms`} + match={isTpotMatch} + valueColor={LineColor.Secondary} + /> + </MiddleColumn> + + <MiddleColumn sx={{ paddingLeft: '0px !important' }} item xs={9}> + <GraphContainer> + <MetricLine + data={[ + { id: 'time per request', data: lineDataByRps.timePerRequest || [] }, + ]} + threshold={timePerRequestSLO} + lineColor={LineColor.Tertiary} + /> + </GraphContainer> + </MiddleColumn> + <MiddleColumn item xs={3}> + <MetricValue + label="time per request" + value={`${formatNumber( + interpolatedMetricData.timePerRequest.enforcedPercentileValue + )} ms`} + match={isTprMatch} + valueColor={LineColor.Tertiary} + /> + </MiddleColumn> + + <MiddleColumn sx={{ paddingLeft: '0px !important' }} item xs={9}> + <GraphContainer> + <MetricLine + data={[{ id: 'throughput', data: lineDataByRps.throughput || [] }]} + threshold={throughputSLO} + lineColor={LineColor.Quarternary} + /> + </GraphContainer> + </MiddleColumn> + <MiddleColumn item xs={3}> + <MetricValue + value={`${formatNumber( + interpolatedMetricData.throughput.enforcedPercentileValue + )} tok/s`} + label="throughput" + match={isThroughputMatch} + valueColor={LineColor.Quarternary} + /> + </MiddleColumn> + + {/* slider */} + <FooterLeftCell item xs={9}> + <CustomSlider + size="medium" + step={0.01} + value={formatNumber(currentRequestRate, 2)} + min={minX} + max={maxX} + marks={sliderMarks} + valueLabelDisplay="on" + onChange={handleSliderChange} + /> + </FooterLeftCell> + <FooterRightCell item xs={3}> + <Typography + variant="overline1" + color="surface.onSurface" + textTransform="uppercase" + > + Maximum RPS per gpu + </Typography> + <Typography variant="metric1" color="primary"> + {formatNumber(currentRequestRate)} rps + </Typography> + </FooterRightCell> + </MetricsSummaryContainer> + </> + ); +}; diff --git a/src/ui/lib/components/MetricsSummary/MetricsSummary.styles.tsx b/src/ui/lib/components/MetricsSummary/MetricsSummary.styles.tsx new file mode 100644 index 00000000..83b5ce1d --- /dev/null +++ b/src/ui/lib/components/MetricsSummary/MetricsSummary.styles.tsx @@ -0,0 +1,213 @@ +import { + Box, + FormControl, + Grid, + InputLabel, + MenuItem, + Select, + Slider, + styled, +} from '@mui/material'; + +import { CustomSelectProps } from './MetricSummary.interfaces'; + +export const MetricsSummaryContainer = styled(Grid)(({ theme }) => ({ + borderWidth: '1px', + borderStyle: 'solid', + borderColor: theme.palette.outline.subdued, + borderRadius: '8px', + overflow: 'hidden', + backgroundColor: theme.palette.surface.surfaceContainerLow, +})); + +export const MiddleColumn = styled(Grid)(({ theme }) => ({ + borderBottomWidth: 1, + borderBottomColor: theme.palette.outline.subdued, + borderBottomStyle: 'solid', + padding: '16px', +})); + +export const FieldsContainer = styled('div')({ + display: 'flex', + justifyContent: 'space-between', + gap: '16px', +}); + +export const FieldCell = styled('div')({ + flex: 1, + minWidth: 'calc((100% - 3 * 16px) / 4)', + boxSizing: 'border-box', +}); + +export const HeaderLeftCell = styled(Grid)(({ theme }) => ({ + borderBottomWidth: 1, + borderBottomColor: theme.palette.outline.subdued, + borderBottomStyle: 'solid', + paddingTop: '16px', + paddingLeft: '16px', + paddingBottom: '16px', + paddingRight: '8px', +})); + +export const HeaderRightCell = styled(Grid)(({ theme }) => ({ + borderBottomWidth: 1, + borderBottomColor: theme.palette.outline.subdued, + borderBottomStyle: 'solid', + borderLeftWidth: 1, + borderLeftColor: theme.palette.outline.subdued, + borderLeftStyle: 'solid', + paddingTop: '16px', + paddingLeft: '8px', + paddingBottom: '16px', + paddingRight: '24px', +})); + +export const FooterRightCell = styled(Grid)(({ theme }) => ({ + backgroundColor: theme.palette.surface.surfaceContainerHigh, + display: 'flex', + flexDirection: 'column', + alignItems: 'flex-end', + padding: '16px', +})); + +export const FooterLeftCell = styled(Grid)(({ theme }) => ({ + backgroundColor: theme.palette.surface.surfaceContainerHigh, + paddingTop: '32px', + paddingBottom: '16px', + paddingLeft: '36px', + paddingRight: '26px', +})); + +export const CustomSlider = styled(Slider)(({ theme }) => ({ + '& .MuiSlider-valueLabel': { + backgroundColor: theme.palette.surface.surfaceContainer, + color: theme.palette.surface.onSurface, + opacity: 1, + fontSize: theme.typography.caption.fontSize, + fontFamily: theme.typography.caption.fontFamily, + fontWeight: theme.typography.caption.fontWeight, + lineHeight: theme.typography.caption.lineHeight, + }, + '& .MuiSlider-markLabel': { + color: theme.palette.surface.onSurface, + opacity: 1, + fontSize: theme.typography.caption.fontSize, + fontFamily: theme.typography.caption.fontFamily, + fontWeight: theme.typography.caption.fontWeight, + lineHeight: theme.typography.caption.lineHeight, + }, + '& .MuiSlider-thumb': { + position: 'relative', + '&::before, &::after': { + borderRadius: 0, + content: '""', + position: 'absolute', + top: '-510px', + left: '50%', + transform: 'translateX(-50%)', + width: '1px', + height: '470px', + display: 'block', + borderLeftColor: theme.palette.primary.main, + borderLeftWidth: '1px', + borderLeftStyle: 'dashed', + }, + // '&::after': { + // left: 'calc(50% - 1px)', + // }, + }, +})); + +export const GraphContainer = styled('div')({ + width: '100%', + height: '90px', + overflow: 'visible', +}); + +export const InputContainer = styled(Box)(({ theme }) => ({ + backgroundColor: theme.palette.surface.surfaceContainerHigh, + padding: '8px 12px', + display: 'block', + borderRadius: '8px', + minWidth: '168px', + width: 'auto', + height: '50px', + overflow: 'hidden', + '&.disabled': { opacity: 0.4 }, +})); + +export const StyledSelect = styled(Select, { + shouldForwardProp: (propName) => propName !== 'placeholder', +})<CustomSelectProps>(({ theme, placeholder }) => ({ + '& .MuiSelect-select .notranslate::after': placeholder + ? { + content: `"${placeholder}"`, + opacity: 0.42, + } + : {}, + '& .MuiOutlinedInput-root': { + '& fieldset': { + borderColor: 'transparent', + }, + '&:hover fieldset': { + borderColor: 'transparent', + }, + '&.Mui-focused fieldset': { + borderColor: 'transparent', + }, + }, + '& .MuiSelect-select': { + padding: '24px 14px', + }, + '& .MuiInputLabel-root': { + color: theme.palette.text.secondary, + marginTop: '8px', + marginBottom: '8px', + }, + '& .MuiInputLabel-root.Mui-focused': { + color: theme.palette.surface.onSurfaceSubdued, + }, + '& .MuiInputLabel-root.MuiFormLabel-filled': { + color: theme.palette.surface.onSurfaceSubdued, + }, + '& .MuiInputBase-input': { + color: theme.palette.surface.onSurfaceSubdued, + }, +})); + +export const StyledInputLabel = styled(InputLabel)(({ theme }) => ({ + color: theme.palette.surface.onSurface, + textTransform: 'uppercase', + marginTop: '6px', + '&.MuiInputLabel-shrink': { + transform: 'translate(14px, 0px) scale(0.75)', + color: theme.palette.surface.onSurface, + }, +})); + +export const StyledFormControl = styled(FormControl)({ + '& .MuiOutlinedInput-root': { + '& fieldset': { + borderColor: 'transparent', + }, + '&:hover fieldset': { + borderColor: 'transparent', + }, + '&.Mui-focused fieldset': { + borderColor: 'transparent', + }, + }, +}); + +export const OptionItem = styled(MenuItem)(({ theme }) => ({ + backgroundColor: theme.palette.surface.surfaceContainerLow, + color: theme.palette.surface.onSurface, + '&:hover': { + backgroundColor: theme.palette.surface.surfaceContainerHigh, + color: theme.palette.surface.onSurface, + }, + '&.Mui-selected': { + backgroundColor: theme.palette.surface.surfaceContainerHigh, + color: theme.palette.surface.onSurface, + }, +})); diff --git a/src/ui/lib/components/MetricsSummary/components/MetricValue/MetricValue.component.tsx b/src/ui/lib/components/MetricsSummary/components/MetricValue/MetricValue.component.tsx new file mode 100644 index 00000000..4d524858 --- /dev/null +++ b/src/ui/lib/components/MetricsSummary/components/MetricValue/MetricValue.component.tsx @@ -0,0 +1,37 @@ +import { Box, Typography } from '@mui/material'; +import { FC } from 'react'; + +import { CheckCircle, WarningCircle } from '@assets/icons'; + +import { useColor } from '@/lib/hooks/useColor'; + +import { MetricValueProps } from './MetricValue.interfaces'; + +export const Component: FC<MetricValueProps> = ({ + label, + value, + match, + valueColor, +}) => { + const selectedColor = useColor(valueColor); + return ( + <Box display="flex" flexDirection="column" alignItems="flex-end" gap={1}> + <Box display="flex" flexDirection="row"> + <Typography + variant="overline2" + color="surface.onSurface" + textTransform="uppercase" + > + {label} + </Typography> + <Typography variant="overline2" color="surface.onSurfaceSubdued" pl={0.5}> + (observed) + </Typography> + </Box> + <Typography variant="metric1" color={selectedColor}> + {value} + </Typography> + {match ? <CheckCircle /> : <WarningCircle />} + </Box> + ); +}; diff --git a/src/ui/lib/components/MetricsSummary/components/MetricValue/MetricValue.interfaces.ts b/src/ui/lib/components/MetricsSummary/components/MetricValue/MetricValue.interfaces.ts new file mode 100644 index 00000000..fb3155e3 --- /dev/null +++ b/src/ui/lib/components/MetricsSummary/components/MetricValue/MetricValue.interfaces.ts @@ -0,0 +1,8 @@ +import { LineColor } from '@/lib/components/Charts/MetricLine'; + +export interface MetricValueProps { + label: string; + value: string; + match: boolean; + valueColor: LineColor; +} diff --git a/src/ui/lib/components/MetricsSummary/components/MetricValue/index.tsx b/src/ui/lib/components/MetricsSummary/components/MetricValue/index.tsx new file mode 100644 index 00000000..45775c65 --- /dev/null +++ b/src/ui/lib/components/MetricsSummary/components/MetricValue/index.tsx @@ -0,0 +1,2 @@ +export { Component as MetricValue } from './MetricValue.component'; +export type { MetricValueProps } from './MetricValue.interfaces'; diff --git a/src/ui/lib/components/MetricsSummary/index.tsx b/src/ui/lib/components/MetricsSummary/index.tsx new file mode 100644 index 00000000..ef1a8e00 --- /dev/null +++ b/src/ui/lib/components/MetricsSummary/index.tsx @@ -0,0 +1 @@ +export { Component as MetricsSummary } from './MetricsSummary.component'; diff --git a/src/ui/lib/components/MetricsSummary/useSummary.ts b/src/ui/lib/components/MetricsSummary/useSummary.ts new file mode 100644 index 00000000..e9445266 --- /dev/null +++ b/src/ui/lib/components/MetricsSummary/useSummary.ts @@ -0,0 +1,120 @@ +import { SelectChangeEvent } from '@mui/material'; +import { ChangeEvent, useState } from 'react'; +import { useDispatch, useSelector } from 'react-redux'; + +import { selectMetricsSummaryLineData } from '../../store/slices/benchmarks'; +import { selectSloState } from '../../store/slices/slo/slo.selectors'; +import { setEnforcedPercentile, setSloValue } from '../../store/slices/slo/slo.slice'; +import { ceil, floor } from '../../utils/helpers'; +import { Point } from '@/lib/components/Charts/common/interfaces'; + +type Errors = { [key: string]: string | undefined }; + +const initErrorsState: Errors = { + ttft: undefined, + tpot: undefined, + timePerRequest: undefined, + throughput: undefined, +}; + +const findMinMax = (lineData: Point[]) => { + let minX = Infinity; + let maxX = -Infinity; + let minY = Infinity; + let maxY = -Infinity; + + for (let i = 0; i < lineData.length; i++) { + const { x, y } = lineData[i]; + if (x < minX) { + minX = ceil(x, 2); + } + if (x > maxX) { + maxX = floor(x, 2); + } + if (y < minY) { + minY = ceil(y, 2); + } + if (y > maxY) { + maxY = floor(y, 2); + } + } + + return { minX, maxX, minY, maxY }; +}; + +export const useSummary = () => { + const dispatch = useDispatch(); + + const { current, enforcedPercentile, tasksDefaults } = useSelector(selectSloState); + const { ttft, tpot, timePerRequest, throughput } = useSelector( + selectMetricsSummaryLineData + ); + + const [errors, setErrors] = useState<Errors>(initErrorsState); + + const ttftLimits = findMinMax(ttft || []); + const tpotLimits = findMinMax(tpot || []); + const timePerRequestLimits = findMinMax(timePerRequest || []); + const throughputLimits = findMinMax(throughput || []); + + const limitsByMetric = { + ttft: ttftLimits, + tpot: tpotLimits, + timePerRequest: timePerRequestLimits, + throughput: throughputLimits, + }; + + const validateInput = (field: keyof typeof current, value?: number) => { + let error: string | undefined; + const limits = limitsByMetric[field]; + if (value === undefined) { + error = 'Invalid value'; + } else if (value > limits.maxY) { + error = 'Error: Larger than maximum'; + } else if (value < limits.minY) { + error = 'Error: Smaller than minimum'; + } + setErrors((prev) => ({ ...prev, [field]: error })); + }; + + const sanitizeInput = (value: string) => { + const sanitizedValue = value.replace(/\D/g, ''); + return sanitizedValue !== '' ? Number(sanitizedValue) : undefined; + }; + + const handleChange = + (metric: keyof typeof current) => (event: ChangeEvent<HTMLInputElement>) => { + const newValue = sanitizeInput(event.target.value); + validateInput(metric, newValue); + if (newValue !== undefined) { + dispatch(setSloValue({ metric, value: newValue })); + } + }; + + const handlePercentileChange = (event: SelectChangeEvent<unknown>) => { + // TODO: need to validate slos on percentile change + const newValue = `${event.target.value}`; + dispatch(setEnforcedPercentile(newValue)); + }; + + const handleReset = () => { + Object.entries(tasksDefaults).forEach(([metric, value]) => { + dispatch(setSloValue({ metric: metric as keyof typeof current, value })); + }); + setErrors(initErrorsState); + }; + + return { + ...current, + percentile: enforcedPercentile, + minX: ttftLimits.minX, + maxX: ttftLimits.maxX, + errors, + handleTtft: handleChange('ttft'), + handleTpot: handleChange('tpot'), + handleTimePerRequest: handleChange('timePerRequest'), + handleThroughput: handleChange('throughput'), + handlePercentileChange, + handleReset, + }; +}; diff --git a/src/ui/lib/components/PageFooter/PageFooter.component.tsx b/src/ui/lib/components/PageFooter/PageFooter.component.tsx index 1efb29f4..64082f9c 100644 --- a/src/ui/lib/components/PageFooter/PageFooter.component.tsx +++ b/src/ui/lib/components/PageFooter/PageFooter.component.tsx @@ -2,7 +2,7 @@ import { Box, Link, Typography } from '@mui/material'; import React from 'react'; -import { NeuralMagicTitleV2 } from '@assets/icons'; +import { guideLLMLogoLight } from '@assets/icons'; export const Component = () => { return ( @@ -26,7 +26,7 @@ export const Component = () => { </Link> </Box> <Box> - <NeuralMagicTitleV2 /> + <img width="150" alt="guidellm logo" src={guideLLMLogoLight.src} /> </Box> </Box> ); diff --git a/src/ui/lib/components/PageHeader/PageHeader.component.tsx b/src/ui/lib/components/PageHeader/PageHeader.component.tsx new file mode 100644 index 00000000..92fc9bc8 --- /dev/null +++ b/src/ui/lib/components/PageHeader/PageHeader.component.tsx @@ -0,0 +1,99 @@ +'use client'; +import { Box, Link, Typography, useTheme } from '@mui/material'; +import dynamic from 'next/dynamic'; +import NextLink from 'next/link'; + +import { Open } from '@assets/icons'; + +import { useGetRunInfoQuery } from '../../store/slices/runInfo'; +import { formateDate, getFileSize } from '../../utils/helpers'; +import { SvgContainer } from '../../utils/SvgContainer'; + +import { SpecBadge } from '../SpecBadge'; +import { HeaderCell, HeaderWrapper } from './PageHeader.styles'; + +const Component = () => { + const theme = useTheme(); + const { data } = useGetRunInfoQuery(); + const modelSize = getFileSize(data?.model?.size || 0); + + return ( + <Box py={2}> + <Typography variant="subtitle2" color="surface.onSurfaceAccent"> + GuideLLM + </Typography> + <Typography variant="h4" color="surface.onSurface" my={'12px'}> + Workload Report + </Typography> + <HeaderWrapper container> + <HeaderCell item xs={5} withDivider sx={{ paddingLeft: 0 }}> + <SpecBadge + label="Model" + value={data?.model?.name || 'N/A'} + variant="metric2" + withTooltip + /> + <SpecBadge + label="Model size" + value={data?.model?.size ? `${modelSize?.size} ${modelSize?.units}` : '0B'} + variant="body1" + /> + </HeaderCell> + {/*<HeaderCell item xs={2} withDivider>*/} + {/* <SpecBadge*/} + {/* label="Hardware"*/} + {/* value="A10"*/} + {/* variant="metric2"*/} + {/* additionalValue={*/} + {/* <Chip*/} + {/* label="x3"*/} + {/* color="primary"*/} + {/* variant="filled"*/} + {/* size="small"*/} + {/* sx={{ padding: '0 5px' }}*/} + {/* />*/} + {/* }*/} + {/* key="Hardware"*/} + {/* />*/} + {/* <SpecBadge label="Interconnect" value="PCIE" variant="body1" key="Interconnect" />*/} + {/* <SpecBadge*/} + {/* label="Version"*/} + {/* value={reportData.server.hardware_driver}*/} + {/* variant="body1"*/} + {/* key="Version"*/} + {/* />*/} + {/*</HeaderCell>*/} + <HeaderCell item xs={2} withDivider> + <SpecBadge label="Task" value={data?.task || 'n/a'} variant="metric2" /> + </HeaderCell> + <HeaderCell item xs={3} withDivider> + <SpecBadge + label="Dataset" + value={data?.dataset?.name || 'n/a'} + variant="metric2" + additionalValue={ + <Link href="https://example.com" target="_blank" component={NextLink}> + <SvgContainer color={theme.palette.primary.main}> + <Open /> + </SvgContainer> + </Link> + } + /> + </HeaderCell> + <HeaderCell item xs={2} sx={{ paddingRight: 0 }}> + <SpecBadge + label="Time Stamp" + value={data?.timestamp ? formateDate(data?.timestamp) : 'n/a'} + variant="caption" + /> + </HeaderCell> + </HeaderWrapper> + </Box> + ); +}; + +const DynamicComponent = dynamic(() => Promise.resolve(Component), { + ssr: false, +}); + +export { DynamicComponent as Component }; diff --git a/src/ui/lib/components/PageHeader/PageHeader.interfaces.ts b/src/ui/lib/components/PageHeader/PageHeader.interfaces.ts new file mode 100644 index 00000000..af994cdc --- /dev/null +++ b/src/ui/lib/components/PageHeader/PageHeader.interfaces.ts @@ -0,0 +1,3 @@ +export type HeaderCellProps = { + withDivider?: boolean; +}; diff --git a/src/ui/lib/components/PageHeader/PageHeader.styles.tsx b/src/ui/lib/components/PageHeader/PageHeader.styles.tsx new file mode 100644 index 00000000..a292becf --- /dev/null +++ b/src/ui/lib/components/PageHeader/PageHeader.styles.tsx @@ -0,0 +1,19 @@ +import { Grid, styled } from '@mui/material'; + +import { HeaderCellProps } from './PageHeader.interfaces'; + +export const HeaderWrapper = styled(Grid)(({ theme }) => ({ + backgroundColor: theme.palette.surface.surfaceContainerLow, + padding: '0 16px', + borderRadius: '8px', +})); + +export const HeaderCell = styled(Grid, { + shouldForwardProp: (propName) => propName !== 'withDivider', +})<HeaderCellProps>(({ theme, withDivider }) => ({ + overflow: 'hidden', + padding: '8px', + ...(withDivider && { + borderRight: `1px solid ${theme.palette.outline.subdued}`, + }), +})); diff --git a/src/ui/lib/components/PageHeader/index.tsx b/src/ui/lib/components/PageHeader/index.tsx new file mode 100644 index 00000000..86e81a23 --- /dev/null +++ b/src/ui/lib/components/PageHeader/index.tsx @@ -0,0 +1 @@ +export { Component as PageHeader } from './PageHeader.component'; diff --git a/src/ui/lib/components/RequestOverTime/RequestOverTime.component.tsx b/src/ui/lib/components/RequestOverTime/RequestOverTime.component.tsx new file mode 100644 index 00000000..3d508960 --- /dev/null +++ b/src/ui/lib/components/RequestOverTime/RequestOverTime.component.tsx @@ -0,0 +1,62 @@ +import { Box, Grid, Typography } from '@mui/material'; +import { FC } from 'react'; + +import { Badge } from '../../components/Badge'; +import { SpecBadge } from '../../components/SpecBadge'; + +import { RequestOverTimeProps } from './RequestOverTime.interfaces'; +import { MiniCombined } from '../Charts/MiniCombined'; +import ContainerSizeWrapper, { + ContainerSize, +} from '../Charts/MiniCombined/components/ContainerSizeWrapper'; + +export const Component: FC<RequestOverTimeProps> = ({ + benchmarksCount, + barData, + rateType, + lines = [], +}) => ( + <Box + display="flex" + flexDirection="column" + sx={{ width: '100%' }} + justifyItems="center" + > + <Grid container spacing={2}> + <Grid item xs={12}> + <SpecBadge + label="Number of Benchmarks" + value={benchmarksCount?.toString()} + variant="body2" + /> + </Grid> + <Grid item> + <Typography + variant="overline2" + color="surface.onSurface" + textTransform="uppercase" + > + Rate Type + </Typography> + </Grid> + <Grid item> + <Badge label={rateType} /> + </Grid> + </Grid> + <div style={{ width: '100%', height: '85px' }}> + <ContainerSizeWrapper> + {(containerSize: ContainerSize) => ( + <MiniCombined + bars={barData} + lines={lines} + width={312} + height={85} + xLegend="time" + margins={{ bottom: 30 }} + containerSize={containerSize} + /> + )} + </ContainerSizeWrapper> + </div> + </Box> +); diff --git a/src/ui/lib/components/RequestOverTime/RequestOverTime.interfaces.ts b/src/ui/lib/components/RequestOverTime/RequestOverTime.interfaces.ts new file mode 100644 index 00000000..b40c7da2 --- /dev/null +++ b/src/ui/lib/components/RequestOverTime/RequestOverTime.interfaces.ts @@ -0,0 +1,9 @@ +export interface RequestOverTimeProps { + benchmarksCount: number; + barData: { + x: number; + y: number; + }[]; + rateType: string; + lines?: { x: number; y: number; id: string }[]; +} diff --git a/src/ui/lib/components/RequestOverTime/index.tsx b/src/ui/lib/components/RequestOverTime/index.tsx new file mode 100644 index 00000000..cdb73593 --- /dev/null +++ b/src/ui/lib/components/RequestOverTime/index.tsx @@ -0,0 +1,2 @@ +export { Component as RequestOverTime } from './RequestOverTime.component'; +export type { RequestOverTimeProps } from './RequestOverTime.interfaces'; diff --git a/src/ui/lib/components/RowContainer/RowContainer.component.tsx b/src/ui/lib/components/RowContainer/RowContainer.component.tsx new file mode 100644 index 00000000..31c5ac82 --- /dev/null +++ b/src/ui/lib/components/RowContainer/RowContainer.component.tsx @@ -0,0 +1,18 @@ +import { Grid, Typography } from '@mui/material'; + +import { RowContainerProps } from './RowContainer.interfaces'; + +export const Component = ({ label, children }: RowContainerProps) => { + return ( + <Grid container alignItems="center"> + <Grid item xs={2}> + <Typography variant="h6" color="surface.onSurface"> + {label} + </Typography> + </Grid> + <Grid item xs={10}> + {children} + </Grid> + </Grid> + ); +}; diff --git a/src/ui/lib/components/RowContainer/RowContainer.interfaces.tsx b/src/ui/lib/components/RowContainer/RowContainer.interfaces.tsx new file mode 100644 index 00000000..d25aab89 --- /dev/null +++ b/src/ui/lib/components/RowContainer/RowContainer.interfaces.tsx @@ -0,0 +1,6 @@ +import { ReactNode } from 'react'; + +export interface RowContainerProps { + label: string; + children: ReactNode; +} diff --git a/src/ui/lib/components/RowContainer/index.tsx b/src/ui/lib/components/RowContainer/index.tsx new file mode 100644 index 00000000..c9aa63de --- /dev/null +++ b/src/ui/lib/components/RowContainer/index.tsx @@ -0,0 +1,2 @@ +export { Component as RowContainer } from './RowContainer.component'; +export type { RowContainerProps } from './RowContainer.interfaces'; diff --git a/src/ui/lib/components/Section/Section.component.tsx b/src/ui/lib/components/Section/Section.component.tsx new file mode 100644 index 00000000..11969a7a --- /dev/null +++ b/src/ui/lib/components/Section/Section.component.tsx @@ -0,0 +1,19 @@ +import { Grid, Typography } from '@mui/material'; + +import { SectionProps } from './Section.interfaces'; +import { ValueCell } from './Section.styles'; + +export const Component = ({ label, value }: SectionProps) => { + return ( + <Grid container display="flex" flexDirection="row" m="6px" sx={{ width: 'auto' }}> + <Grid item alignContent="center"> + <Typography variant="subtitle2" color="surface.onSurfaceSubdued"> + {label} + </Typography> + </Grid> + <ValueCell alignContent="center" item> + {value} + </ValueCell> + </Grid> + ); +}; diff --git a/src/ui/lib/components/Section/Section.interfaces.ts b/src/ui/lib/components/Section/Section.interfaces.ts new file mode 100644 index 00000000..792bfe67 --- /dev/null +++ b/src/ui/lib/components/Section/Section.interfaces.ts @@ -0,0 +1,4 @@ +export interface SectionProps { + label: string; + value: string; +} diff --git a/src/ui/lib/components/Section/Section.styles.tsx b/src/ui/lib/components/Section/Section.styles.tsx new file mode 100644 index 00000000..4ae5d1f2 --- /dev/null +++ b/src/ui/lib/components/Section/Section.styles.tsx @@ -0,0 +1,10 @@ +import { Grid, styled } from '@mui/material'; + +export const ValueCell = styled(Grid)(({ theme }) => ({ + backgroundColor: theme.palette.surface.surfaceContainerHigh, + color: theme.palette.surface.onSurface, + borderRadius: '6px', + padding: '6px', + gap: 10, + marginLeft: '12px', +})); diff --git a/src/ui/lib/components/Section/index.tsx b/src/ui/lib/components/Section/index.tsx new file mode 100644 index 00000000..8182a403 --- /dev/null +++ b/src/ui/lib/components/Section/index.tsx @@ -0,0 +1,2 @@ +export { Component as Section } from './Section.component'; +export type { SectionProps } from './Section.interfaces'; diff --git a/src/ui/lib/components/SectionContainer/SectionContainer.component.tsx b/src/ui/lib/components/SectionContainer/SectionContainer.component.tsx new file mode 100644 index 00000000..cec8a89e --- /dev/null +++ b/src/ui/lib/components/SectionContainer/SectionContainer.component.tsx @@ -0,0 +1,74 @@ +'use client'; +import { Grid, useTheme } from '@mui/material'; +import { MutableRefObject, useEffect, useRef, useState } from 'react'; + +import { ArrowDown, ArrowUp } from '@assets/icons'; + +import { SvgContainer } from '../../utils/SvgContainer'; + +import { SectionContainerProps } from './SectionContainer.interfaces'; +import { Container, RoundedButton } from './SectionContainer.styles'; + +export const Component = ({ children }: SectionContainerProps) => { + const theme = useTheme(); + const [expanded, setExpanded] = useState(false); + const [showButton, setShowButton] = useState(false); + const containerRef: MutableRefObject<HTMLDivElement | null> = useRef(null); + + useEffect(() => { + const checkOverflow = () => { + const container = containerRef.current; + if (container) { + const hasOverflow = container.scrollHeight > container.clientHeight; + setShowButton(hasOverflow); + } + }; + + checkOverflow(); + window.addEventListener('resize', checkOverflow); + + return () => { + window.removeEventListener('resize', checkOverflow); + }; + }, []); + + return ( + <Container display="flex"> + <Grid + flexGrow={1} + flexWrap="wrap" + direction="row" + display="flex" + ref={containerRef} + sx={{ maxHeight: expanded ? 'none' : '42px', overflow: 'hidden' }} + > + {children} + </Grid> + <Grid + sx={{ width: '72px' }} + justifyContent="flex-end" + alignItems="flex-start" + display="flex" + > + {showButton && ( + <RoundedButton + onClick={() => setExpanded(!expanded)} + sx={{ marginTop: '4px' }} + > + <SvgContainer color={theme.palette.primary.onContainer}> + {expanded ? ( + <SvgContainer color={theme.palette.primary.onContainer}> + <ArrowDown /> + </SvgContainer> + ) : ( + <SvgContainer color={theme.palette.primary.onContainer}> + <ArrowUp /> + </SvgContainer> + )} + </SvgContainer> + </RoundedButton> + )} + </Grid> + </Container> + ); +}; diff --git a/src/ui/lib/components/SectionContainer/SectionContainer.interfaces.ts b/src/ui/lib/components/SectionContainer/SectionContainer.interfaces.ts new file mode 100644 index 00000000..629b06b6 --- /dev/null +++ b/src/ui/lib/components/SectionContainer/SectionContainer.interfaces.ts @@ -0,0 +1,5 @@ +import { ReactElement } from 'react'; + +export interface SectionContainerProps { + children: ReactElement; +} diff --git a/src/ui/lib/components/SectionContainer/SectionContainer.styles.tsx b/src/ui/lib/components/SectionContainer/SectionContainer.styles.tsx new file mode 100644 index 00000000..9e4e3695 --- /dev/null +++ b/src/ui/lib/components/SectionContainer/SectionContainer.styles.tsx @@ -0,0 +1,21 @@ +import { Grid, styled } from '@mui/material'; + +export const Container = styled(Grid)(({ theme }) => ({ + backgroundColor: theme.palette.surface.surfaceContainerLow, + borderRadius: '6px', + padding: '12px', + width: 'auto', + margin: '8px', +})); + +export const RoundedButton = styled('div')(({ theme }) => ({ + backgroundColor: theme.palette.primary.shades.B80, + width: '32px', + height: '32px', + borderRadius: '16px', + justifyContent: 'center', + alignItems: 'center', + display: 'flex', + cursor: 'pointer', + userSelect: 'none', +})); diff --git a/src/ui/lib/components/SectionContainer/index.tsx b/src/ui/lib/components/SectionContainer/index.tsx new file mode 100644 index 00000000..1b857fe8 --- /dev/null +++ b/src/ui/lib/components/SectionContainer/index.tsx @@ -0,0 +1,2 @@ +export { Component as SectionContainer } from './SectionContainer.component'; +export type { SectionContainerProps } from './SectionContainer.interfaces'; diff --git a/src/ui/lib/components/SpecBadge/SpecBadge.component.tsx b/src/ui/lib/components/SpecBadge/SpecBadge.component.tsx new file mode 100644 index 00000000..f3c63c8d --- /dev/null +++ b/src/ui/lib/components/SpecBadge/SpecBadge.component.tsx @@ -0,0 +1,34 @@ +import { Box, Typography, Tooltip } from '@mui/material'; +import { FC } from 'react'; + +import { SpecBadgeProps } from './SpecBadge.interfaces'; +import { Container, EllipsisTypography, ValueWrapper } from './SpecBadge.styles'; + +export const Component: FC<SpecBadgeProps> = ({ + label, + value, + variant, + additionalValue, + withTooltip = false, +}) => { + const tooltipContent = ( + <EllipsisTypography variant={variant} color="primary"> + {value} + </EllipsisTypography> + ); + return ( + <Container> + <Typography variant="overline2" color="surface.onSurface"> + {label} + </Typography> + <ValueWrapper> + {withTooltip ? ( + <Tooltip title={value}>{tooltipContent}</Tooltip> + ) : ( + tooltipContent + )} + {additionalValue && <Box ml={1}>{additionalValue}</Box>} + </ValueWrapper> + </Container> + ); +}; diff --git a/src/ui/lib/components/SpecBadge/SpecBadge.interfaces.ts b/src/ui/lib/components/SpecBadge/SpecBadge.interfaces.ts new file mode 100644 index 00000000..5bc778b3 --- /dev/null +++ b/src/ui/lib/components/SpecBadge/SpecBadge.interfaces.ts @@ -0,0 +1,12 @@ +import { Variant } from '@mui/material/styles/createTypography'; +import { TypographyPropsVariantOverrides } from '@mui/material/Typography/Typography'; +import { OverridableStringUnion } from '@mui/types'; +import { ReactNode } from 'react'; + +export interface SpecBadgeProps { + label: string; + value: string; + variant: OverridableStringUnion<Variant | 'inherit', TypographyPropsVariantOverrides>; + additionalValue?: ReactNode; + withTooltip?: boolean; +} diff --git a/src/ui/lib/components/SpecBadge/SpecBadge.styles.tsx b/src/ui/lib/components/SpecBadge/SpecBadge.styles.tsx new file mode 100644 index 00000000..3057d7f2 --- /dev/null +++ b/src/ui/lib/components/SpecBadge/SpecBadge.styles.tsx @@ -0,0 +1,21 @@ +import { Box, styled, Typography } from '@mui/material'; + +export const EllipsisTypography = styled(Typography)({ + whiteSpace: 'nowrap', + overflow: 'hidden', + textOverflow: 'ellipsis', +}); + +export const Container = styled(Box)({ + display: 'flex', + flexDirection: 'column', + alignItems: 'flex-start', + marginBottom: '8px', +}); + +export const ValueWrapper = styled(Box)({ + display: 'flex', + alignItems: 'center', + maxWidth: '100%', + width: 'auto', +}); diff --git a/src/ui/lib/components/SpecBadge/index.tsx b/src/ui/lib/components/SpecBadge/index.tsx new file mode 100644 index 00000000..54052cc2 --- /dev/null +++ b/src/ui/lib/components/SpecBadge/index.tsx @@ -0,0 +1,2 @@ +export { Component as SpecBadge } from './SpecBadge.component'; +export type { SpecBadgeProps } from './SpecBadge.interfaces'; diff --git a/src/ui/lib/components/TokenLength/TokenLength.component.tsx b/src/ui/lib/components/TokenLength/TokenLength.component.tsx new file mode 100644 index 00000000..1b8f6681 --- /dev/null +++ b/src/ui/lib/components/TokenLength/TokenLength.component.tsx @@ -0,0 +1,47 @@ +import { Box, Typography } from '@mui/material'; +import { FC } from 'react'; + +import { MiniCombined } from '../../components/Charts/MiniCombined'; +import ContainerSizeWrapper, { + ContainerSize, +} from '../../components/Charts/MiniCombined/components/ContainerSizeWrapper'; + +import { TokenLengthProps } from './TokenLength.interfaces'; + +export const Component: FC<TokenLengthProps> = ({ label, tokenCount, bars, lines }) => ( + <Box + display="flex" + flexDirection="column" + sx={{ width: '100%' }} + justifyItems="center" + alignItems="space-between" + > + <Box display="flex" flexDirection="column"> + <Box> + <Typography variant="overline2" color="surface.onSurface"> + {label} + </Typography> + </Box> + <Box mt="8px" mb="16px"> + <Typography variant="metric1" color="primary"> + {`${tokenCount} token${tokenCount !== 1 ? 's' : ''}`} + </Typography> + </Box> + </Box> + <div style={{ width: '100%', height: '85px' }}> + <ContainerSizeWrapper> + {(containerSize: ContainerSize) => ( + <MiniCombined + bars={bars} + lines={lines} + width={312} + height={85} + xLegend="length (tokens)" + margins={{ bottom: 30 }} + containerSize={containerSize} + /> + )} + </ContainerSizeWrapper> + </div> + </Box> +); diff --git a/src/ui/lib/components/TokenLength/TokenLength.interfaces.ts b/src/ui/lib/components/TokenLength/TokenLength.interfaces.ts new file mode 100644 index 00000000..d8e9dd15 --- /dev/null +++ b/src/ui/lib/components/TokenLength/TokenLength.interfaces.ts @@ -0,0 +1,6 @@ +export interface TokenLengthProps { + label: string; + tokenCount: number; + bars: { x: number; y: number }[]; + lines: { x: number; y: number; id: string }[]; +} diff --git a/src/ui/lib/components/TokenLength/index.tsx b/src/ui/lib/components/TokenLength/index.tsx new file mode 100644 index 00000000..e0eba389 --- /dev/null +++ b/src/ui/lib/components/TokenLength/index.tsx @@ -0,0 +1,2 @@ +export { Component as TokenLength } from './TokenLength.component'; +export type { TokenLengthProps } from './TokenLength.interfaces'; diff --git a/src/ui/lib/components/WorkloadDetails/WorkloadDetails.component.tsx b/src/ui/lib/components/WorkloadDetails/WorkloadDetails.component.tsx new file mode 100644 index 00000000..3ed90c8e --- /dev/null +++ b/src/ui/lib/components/WorkloadDetails/WorkloadDetails.component.tsx @@ -0,0 +1,117 @@ +'use client'; +import { Box, Grid } from '@mui/material'; +import dynamic from 'next/dynamic'; +import { useSelector } from 'react-redux'; + +import { + selectGenerationsHistogramBarData, + selectGenerationsHistogramLineData, + selectPromptsHistogramBarData, + selectPromptsHistogramLineData, + selectRequestOverTimeBarData, + useGetWorkloadDetailsQuery, +} from '../../store/slices/workloadDetails'; + +import { formatNumber, parseUrlParts } from '../../utils/helpers'; + +import { BlockHeader } from '../BlockHeader'; +import { Carousel } from '../Carousel'; +import { DataPanel } from '../DataPanel'; +import { RequestOverTime } from '../RequestOverTime'; +import { SpecBadge } from '../SpecBadge'; + +import { TokenLength } from '../TokenLength'; + +const Component = () => { + const { data } = useGetWorkloadDetailsQuery(); + const promptsBarData = useSelector(selectPromptsHistogramBarData); + const promptsLineData = useSelector(selectPromptsHistogramLineData); + const generationsBarData = useSelector(selectGenerationsHistogramBarData); + const generationsLineData = useSelector(selectGenerationsHistogramLineData); + const { barChartData: requestOverTimeBarData } = useSelector( + selectRequestOverTimeBarData + ); + const { type, target, port } = parseUrlParts(data?.server?.target || ''); + + return ( + <> + <BlockHeader label="Workload Details" /> + <Grid + container + spacing={0} + gap={2} + data-id="workload-details" + sx={{ marginLeft: 0, flexWrap: 'nowrap' }} + justifyContent="space-between" + > + <DataPanel + header="Prompt" + topContainer={ + <Carousel items={data?.prompts?.samples || []} label="Sample Prompt" /> + } + bottomContainer={ + <TokenLength + label={'Mean Prompt Length'} + tokenCount={formatNumber( + data?.prompts?.tokenDistributions.statistics.mean ?? 0 + )} + bars={promptsBarData || []} + lines={promptsLineData} + /> + } + key="dp-1" + /> + <DataPanel + header="Server" + topContainer={ + <Box display="flex" flexDirection="column" sx={{ width: '100%' }}> + <SpecBadge label="Target" value={target} variant="body2" /> + <Grid container spacing={2}> + <Grid item xs={6}> + <SpecBadge label="Type" value={type} variant="body2" /> + </Grid> + <Grid item xs={6}> + <SpecBadge label="Port" value={port} variant="body2" /> + </Grid> + </Grid> + </Box> + } + bottomContainer={ + <RequestOverTime + benchmarksCount={data?.requestsOverTime?.numBenchmarks || 0} + barData={requestOverTimeBarData || []} + rateType={data?.rateType ?? ''} + /> + } + key="dp-2" + /> + <DataPanel + header="Generated" + topContainer={ + <Carousel + items={data?.generations?.samples || []} + label="Sample Generated" + /> + } + bottomContainer={ + <TokenLength + label={'Mean Generated Length'} + tokenCount={formatNumber( + data?.generations?.tokenDistributions.statistics.mean ?? 0 + )} + bars={generationsBarData || []} + lines={generationsLineData} + /> + } + key="dp-3" + /> + </Grid> + </> + ); +}; + +const DynamicComponent = dynamic(() => Promise.resolve(Component), { + ssr: false, +}); + +export { DynamicComponent as Component }; diff --git a/src/ui/lib/components/WorkloadDetails/index.tsx b/src/ui/lib/components/WorkloadDetails/index.tsx new file mode 100644 index 00000000..702f16ad --- /dev/null +++ b/src/ui/lib/components/WorkloadDetails/index.tsx @@ -0,0 +1 @@ +export { Component as WorkloadDetails } from './WorkloadDetails.component'; diff --git a/src/ui/lib/components/WorkloadMetrics/WorkloadMetricSummary.interfaces.tsx b/src/ui/lib/components/WorkloadMetrics/WorkloadMetricSummary.interfaces.tsx new file mode 100644 index 00000000..e69de29b diff --git a/src/ui/lib/components/WorkloadMetrics/WorkloadMetrics.component.tsx b/src/ui/lib/components/WorkloadMetrics/WorkloadMetrics.component.tsx new file mode 100644 index 00000000..8162dffa --- /dev/null +++ b/src/ui/lib/components/WorkloadMetrics/WorkloadMetrics.component.tsx @@ -0,0 +1,152 @@ +'use client'; +import { Box } from '@mui/material'; +import { useSelector } from 'react-redux'; + +import { DashedLine } from '../../components/Charts/DashedLine'; +import { + DistributionPercentiles, + PercentileItem, +} from '../../components/DistributionPercentiles'; +import { MeanMetricSummary } from '../../components/MeanMetricSummary'; +import { + selectInterpolatedMetrics, + selectMetricsDetailsLineData, + useGetBenchmarksQuery, +} from '../../store/slices/benchmarks'; + +import { selectSloState } from '../../store/slices/slo/slo.selectors'; + +import { formatNumber } from '../../utils/helpers'; + +import { BlockHeader } from '../BlockHeader'; +import { GraphTitle } from '../GraphTitle'; +import { MetricsContainer } from '../MetricsContainer'; +import { GraphsWrapper } from './WorkloadMetrics.styles'; + +export const columnContent = ( + rpsValue: number, + percentiles: PercentileItem[], + units: string +) => <DistributionPercentiles list={percentiles} rpsValue={rpsValue} units={units} />; + +export const leftColumn = (rpsValue: number, value: number, units: string) => ( + <MeanMetricSummary meanValue={`${value}`} meanUnit={units} rpsValue={rpsValue} /> +); + +export const leftColumn3 = (rpsValue: number, value: number, units: string) => ( + <MeanMetricSummary meanValue={`${value}`} meanUnit={units} rpsValue={rpsValue} /> +); + +export const Component = () => { + const { data } = useGetBenchmarksQuery(); + const { ttft, tpot, timePerRequest, throughput } = useSelector( + selectMetricsDetailsLineData + ); + const { currentRequestRate } = useSelector(selectSloState); + const formattedRequestRate = formatNumber(currentRequestRate); + const { + ttft: ttftAtRPS, + tpot: tpotAtRPS, + timePerRequest: timePerRequestAtRPS, + throughput: throughputAtRPS, + } = useSelector(selectInterpolatedMetrics); + + const minX = Math.floor( + Math.min(...(data?.benchmarks?.map((bm) => bm.requestsPerSecond) || [])) + ); + if ((data?.benchmarks?.length ?? 0) <= 1) { + return <></>; + } + return ( + <> + <BlockHeader label="Metrics Details" /> + <Box display="flex" flexDirection="row" gap={3} mt={3}> + <MetricsContainer + header="TTFT" + leftColumn={leftColumn( + formattedRequestRate, + formatNumber(ttftAtRPS.mean), + 'ms' + )} + rightColumn={columnContent(formattedRequestRate, ttftAtRPS.percentiles, 'ms')} + > + <GraphTitle title="TTFS vs RPS" /> + <GraphsWrapper> + <DashedLine + data={ttft} + margins={{ left: 50, bottom: 50 }} + xLegend="request per sec" + yLegend="ttft (ms)" + minX={minX} + /> + </GraphsWrapper> + </MetricsContainer> + <MetricsContainer + header="TPOT" + leftColumn={leftColumn3( + formattedRequestRate, + formatNumber(tpotAtRPS.mean), + 'ms' + )} + rightColumn={columnContent(formattedRequestRate, tpotAtRPS.percentiles, 'ms')} + > + <GraphTitle title="TPOT vs RPS" /> + <GraphsWrapper> + <DashedLine + data={tpot} + margins={{ left: 50, bottom: 50 }} + xLegend="request per sec" + yLegend="tpot (ms)" + minX={minX} + /> + </GraphsWrapper> + </MetricsContainer> + </Box> + <Box display="flex" flexDirection="row" gap={3} mt={3}> + <MetricsContainer + header="E2E Latency" + leftColumn={leftColumn( + formattedRequestRate, + formatNumber(timePerRequestAtRPS.mean), + 'ms' + )} + rightColumn={columnContent( + formattedRequestRate, + timePerRequestAtRPS.percentiles, + 'ms' + )} + > + <GraphTitle title="E2E Latency vs RPS" /> + <GraphsWrapper> + <DashedLine + data={timePerRequest} + margins={{ left: 50, bottom: 50 }} + xLegend="request per sec" + yLegend="latency (ms)" + minX={minX} + /> + </GraphsWrapper> + </MetricsContainer> + <MetricsContainer + header="Throughput" + leftColumn={leftColumn3( + formattedRequestRate, + formatNumber(throughputAtRPS.mean), + 'ms' + )} + > + <GraphTitle title="Throughput vs RPS" /> + <GraphsWrapper> + <DashedLine + data={throughput} + margins={{ left: 50, bottom: 50 }} + xLegend="request per sec" + yLegend="throughput (tok/s)" + minX={minX} + /> + </GraphsWrapper> + </MetricsContainer> + </Box> + </> + ); +}; diff --git a/src/ui/lib/components/WorkloadMetrics/WorkloadMetrics.styles.tsx b/src/ui/lib/components/WorkloadMetrics/WorkloadMetrics.styles.tsx new file mode 100644 index 00000000..2e59c0f5 --- /dev/null +++ b/src/ui/lib/components/WorkloadMetrics/WorkloadMetrics.styles.tsx @@ -0,0 +1,6 @@ +import { styled } from '@mui/material'; + +export const GraphsWrapper = styled('div')({ + width: '540px', + height: '288px', +}); diff --git a/src/ui/lib/components/WorkloadMetrics/index.tsx b/src/ui/lib/components/WorkloadMetrics/index.tsx new file mode 100644 index 00000000..611578ba --- /dev/null +++ b/src/ui/lib/components/WorkloadMetrics/index.tsx @@ -0,0 +1 @@ +export { Component as WorkloadMetrics } from './WorkloadMetrics.component'; diff --git a/src/ui/lib/hooks/useColor.ts b/src/ui/lib/hooks/useColor.ts new file mode 100644 index 00000000..bc519bad --- /dev/null +++ b/src/ui/lib/hooks/useColor.ts @@ -0,0 +1,19 @@ +import { useTheme } from '@mui/material'; + +import { LineColor } from '@/lib/components/Charts/MetricLine'; + +export const useColor = (colorType: LineColor | undefined) => { + const theme = useTheme(); + switch (colorType) { + case LineColor.Primary: + return theme.palette.primary.main; + case LineColor.Secondary: + return theme.palette.secondary.main; + case LineColor.Tertiary: + return theme.palette.tertiary.main as string; + case LineColor.Quarternary: + return theme.palette.quarternary.main as string; + default: + return theme.palette.surface.onSurfaceAccent; + } +}; diff --git a/src/ui/lib/store/benchmarksWindowData.ts b/src/ui/lib/store/benchmarksWindowData.ts new file mode 100644 index 00000000..e8a5cc40 --- /dev/null +++ b/src/ui/lib/store/benchmarksWindowData.ts @@ -0,0 +1,1154 @@ +export const benchmarksScript = `window.benchmarks = { + "benchmarks": [ + { + "requestsPerSecond": 0.6668550387660497, + "tpot": { + "statistics": { + "total": 80, + "mean": 23.00635663936911, + "median": 22.959455611213805, + "min": 22.880917503720237, + "max": 24.14080301920573, + "std": 0.18918760384209338 + }, + "percentiles": [ + { + "percentile": "p50", + "value": 22.959455611213805 + }, + { + "percentile": "p90", + "value": 23.01789086962503 + }, + { + "percentile": "p95", + "value": 23.30297423947242 + }, + { + "percentile": "p99", + "value": 24.14080301920573 + } + ] + }, + "ttft": { + "statistics": { + "total": 80, + "mean": 49.64659512042999, + "median": 49.23129081726074, + "min": 44.538259506225586, + "max": 55.47308921813965, + "std": 1.7735485090634995 + }, + "percentiles": [ + { + "percentile": "p50", + "value": 49.23129081726074 + }, + { + "percentile": "p90", + "value": 50.16160011291504 + }, + { + "percentile": "p95", + "value": 54.918766021728516 + }, + { + "percentile": "p99", + "value": 55.47308921813965 + } + ] + }, + "throughput": { + "statistics": { + "total": 210, + "mean": 42.58702991319684, + "median": 43.536023084668, + "min": 0.0, + "max": 43.68247620237872, + "std": 4.559764488536857 + }, + "percentiles": [ + { + "percentile": "p50", + "value": 43.536023084668 + }, + { + "percentile": "p90", + "value": 43.62613633999709 + }, + { + "percentile": "p95", + "value": 43.64020767654067 + }, + { + "percentile": "p99", + "value": 43.68202126662431 + } + ] + }, + "timePerRequest": { + "statistics": { + "total": 80, + "mean": 1496.706646680832, + "median": 1496.1087703704834, + "min": 1490.584135055542, + "max": 1505.8784484863281, + "std": 3.4553340533022667 + }, + "percentiles": [ + { + "percentile": "p50", + "value": 1496.1087703704834 + }, + { + "percentile": "p90", + "value": 1500.9305477142334 + }, + { + "percentile": "p95", + "value": 1505.3200721740723 + }, + { + "percentile": "p99", + "value": 1505.8784484863281 + } + ] + } + }, + { + "requestsPerSecond": 28.075330129628725, + "tpot": { + "statistics": { + "total": 3416, + "mean": 126.08707076148656, + "median": 125.30853256346687, + "min": 23.034303907364134, + "max": 138.08223756693178, + "std": 3.508992115582193 + }, + "percentiles": [ + { + "percentile": "p50", + "value": 125.30853256346687 + }, + { + "percentile": "p90", + "value": 129.21135009281218 + }, + { + "percentile": "p95", + "value": 129.52291770059554 + }, + { + "percentile": "p99", + "value": 132.21229490686636 + } + ] + }, + "ttft": { + "statistics": { + "total": 3416, + "mean": 8585.486161415694, + "median": 8965.316534042358, + "min": 110.53991317749023, + "max": 12575.379610061646, + "std": 1929.5632525234505 + }, + "percentiles": [ + { + "percentile": "p50", + "value": 8965.316534042358 + }, + { + "percentile": "p90", + "value": 9231.79316520691 + }, + { + "percentile": "p95", + "value": 9485.00108718872 + }, + { + "percentile": "p99", + "value": 12096.465587615967 + } + ] + }, + "throughput": { + "statistics": { + "total": 15981, + "mean": 1795.4403743554367, + "median": 670.1236619268253, + "min": 0.0, + "max": 838860.8, + "std": 5196.545581836957 + }, + "percentiles": [ + { + "percentile": "p50", + "value": 670.1236619268253 + }, + { + "percentile": "p90", + "value": 4068.1901066925316 + }, + { + "percentile": "p95", + "value": 6374.322188449848 + }, + { + "percentile": "p99", + "value": 16194.223938223939 + } + ] + }, + "timePerRequest": { + "statistics": { + "total": 3416, + "mean": 16526.811318389147, + "median": 17058.441638946533, + "min": 1711.3444805145264, + "max": 20646.55351638794, + "std": 2054.9553770234484 + }, + "percentiles": [ + { + "percentile": "p50", + "value": 17058.441638946533 + }, + { + "percentile": "p90", + "value": 17143.84412765503 + }, + { + "percentile": "p95", + "value": 17248.060703277588 + }, + { + "percentile": "p99", + "value": 20116.52660369873 + } + ] + } + }, + { + "requestsPerSecond": 4.071681142252993, + "tpot": { + "statistics": { + "total": 488, + "mean": 24.898151556004148, + "median": 24.889995181371294, + "min": 24.822999560643755, + "max": 26.217273871103924, + "std": 0.11227504505081555 + }, + "percentiles": [ + { + "percentile": "p50", + "value": 24.889995181371294 + }, + { + "percentile": "p90", + "value": 24.90483389960395 + }, + { + "percentile": "p95", + "value": 24.965975019666885 + }, + { + "percentile": "p99", + "value": 25.306613214554325 + } + ] + }, + "ttft": { + "statistics": { + "total": 488, + "mean": 58.341102033364976, + "median": 58.38632583618164, + "min": 44.857025146484375, + "max": 111.23061180114746, + "std": 8.190008649880411 + }, + "percentiles": [ + { + "percentile": "p50", + "value": 58.38632583618164 + }, + { + "percentile": "p90", + "value": 67.66843795776367 + }, + { + "percentile": "p95", + "value": 68.76754760742188 + }, + { + "percentile": "p99", + "value": 71.46525382995605 + } + ] + }, + "throughput": { + "statistics": { + "total": 11338, + "mean": 260.42072092623033, + "median": 47.630070406540995, + "min": 0.0, + "max": 838860.8, + "std": 886.8274389295076 + }, + "percentiles": [ + { + "percentile": "p50", + "value": 47.630070406540995 + }, + { + "percentile": "p90", + "value": 604.8895298528987 + }, + { + "percentile": "p95", + "value": 1621.9273008507348 + }, + { + "percentile": "p99", + "value": 3054.846321922797 + } + ] + }, + "timePerRequest": { + "statistics": { + "total": 488, + "mean": 1626.5668087318297, + "median": 1626.236915588379, + "min": 1611.9341850280762, + "max": 1690.2406215667725, + "std": 8.871477705542668 + }, + "percentiles": [ + { + "percentile": "p50", + "value": 1626.236915588379 + }, + { + "percentile": "p90", + "value": 1635.761022567749 + }, + { + "percentile": "p95", + "value": 1637.390375137329 + }, + { + "percentile": "p99", + "value": 1643.500804901123 + } + ] + } + }, + { + "requestsPerSecond": 7.466101414346809, + "tpot": { + "statistics": { + "total": 895, + "mean": 27.56459906601014, + "median": 27.525402250744047, + "min": 26.69054911686824, + "max": 29.5785041082473, + "std": 0.18545649185329754 + }, + "percentiles": [ + { + "percentile": "p50", + "value": 27.525402250744047 + }, + { + "percentile": "p90", + "value": 27.62497795952691 + }, + { + "percentile": "p95", + "value": 27.947206345815506 + }, + { + "percentile": "p99", + "value": 28.41202157442687 + } + ] + }, + "ttft": { + "statistics": { + "total": 895, + "mean": 64.73036744741088, + "median": 62.484025955200195, + "min": 48.038482666015625, + "max": 256.4809322357178, + "std": 21.677914089867077 + }, + "percentiles": [ + { + "percentile": "p50", + "value": 62.484025955200195 + }, + { + "percentile": "p90", + "value": 72.04723358154297 + }, + { + "percentile": "p95", + "value": 72.50738143920898 + }, + { + "percentile": "p99", + "value": 229.35032844543457 + } + ] + }, + "throughput": { + "statistics": { + "total": 12465, + "mean": 477.5134940335642, + "median": 49.76925541382379, + "min": 0.0, + "max": 1677721.6, + "std": 2472.852317203968 + }, + "percentiles": [ + { + "percentile": "p50", + "value": 49.76925541382379 + }, + { + "percentile": "p90", + "value": 1191.5636363636363 + }, + { + "percentile": "p95", + "value": 2501.075730471079 + }, + { + "percentile": "p99", + "value": 7025.634840871022 + } + ] + }, + "timePerRequest": { + "statistics": { + "total": 895, + "mean": 1800.9132816804852, + "median": 1797.5835800170898, + "min": 1756.2305927276611, + "max": 1994.28129196167, + "std": 24.24935353039552 + }, + "percentiles": [ + { + "percentile": "p50", + "value": 1797.5835800170898 + }, + { + "percentile": "p90", + "value": 1808.2549571990967 + }, + { + "percentile": "p95", + "value": 1813.141107559204 + }, + { + "percentile": "p99", + "value": 1967.8056240081787 + } + ] + } + }, + { + "requestsPerSecond": 10.83989165148388, + "tpot": { + "statistics": { + "total": 1300, + "mean": 31.6048062981453, + "median": 31.577579558841766, + "min": 30.171105355927438, + "max": 33.10690323511759, + "std": 0.15146862300990216 + }, + "percentiles": [ + { + "percentile": "p50", + "value": 31.577579558841766 + }, + { + "percentile": "p90", + "value": 31.63230986822219 + }, + { + "percentile": "p95", + "value": 31.682415614052424 + }, + { + "percentile": "p99", + "value": 32.138043834317116 + } + ] + }, + "ttft": { + "statistics": { + "total": 1300, + "mean": 66.61205951984113, + "median": 65.78803062438965, + "min": 51.81550979614258, + "max": 244.69709396362305, + "std": 14.858653160342651 + }, + "percentiles": [ + { + "percentile": "p50", + "value": 65.78803062438965 + }, + { + "percentile": "p90", + "value": 76.70044898986816 + }, + { + "percentile": "p95", + "value": 77.78120040893555 + }, + { + "percentile": "p99", + "value": 88.29903602600098 + } + ] + }, + "throughput": { + "statistics": { + "total": 12708, + "mean": 693.3695002980695, + "median": 55.59272071785492, + "min": 0.0, + "max": 838860.8, + "std": 2454.288991845712 + }, + "percentiles": [ + { + "percentile": "p50", + "value": 55.59272071785492 + }, + { + "percentile": "p90", + "value": 1897.875113122172 + }, + { + "percentile": "p95", + "value": 2931.030048916841 + }, + { + "percentile": "p99", + "value": 7108.989830508474 + } + ] + }, + "timePerRequest": { + "statistics": { + "total": 1300, + "mean": 2057.3723330864545, + "median": 2056.5311908721924, + "min": 2027.0307064056396, + "max": 2233.853578567505, + "std": 16.334707021033957 + }, + "percentiles": [ + { + "percentile": "p50", + "value": 2056.5311908721924 + }, + { + "percentile": "p90", + "value": 2065.953254699707 + }, + { + "percentile": "p95", + "value": 2067.810297012329 + }, + { + "percentile": "p99", + "value": 2087.8031253814697 + } + ] + } + }, + { + "requestsPerSecond": 14.211845819540324, + "tpot": { + "statistics": { + "total": 1704, + "mean": 35.695500394825224, + "median": 35.60370869106717, + "min": 34.798149078611345, + "max": 38.94662857055664, + "std": 0.24967658675392423 + }, + "percentiles": [ + { + "percentile": "p50", + "value": 35.60370869106717 + }, + { + "percentile": "p90", + "value": 35.84100708128914 + }, + { + "percentile": "p95", + "value": 36.09923778041716 + }, + { + "percentile": "p99", + "value": 36.71476489207784 + } + ] + }, + "ttft": { + "statistics": { + "total": 1704, + "mean": 74.19940031750102, + "median": 71.50626182556152, + "min": 53.643226623535156, + "max": 322.6609230041504, + "std": 23.98415146629138 + }, + "percentiles": [ + { + "percentile": "p50", + "value": 71.50626182556152 + }, + { + "percentile": "p90", + "value": 83.71734619140625 + }, + { + "percentile": "p95", + "value": 98.2356071472168 + }, + { + "percentile": "p99", + "value": 113.44718933105469 + } + ] + }, + "throughput": { + "statistics": { + "total": 15532, + "mean": 908.715763654939, + "median": 98.84067397195712, + "min": 0.0, + "max": 838860.8, + "std": 3628.67537220603 + }, + "percentiles": [ + { + "percentile": "p50", + "value": 98.84067397195712 + }, + { + "percentile": "p90", + "value": 2205.2071503680336 + }, + { + "percentile": "p95", + "value": 3775.251125112511 + }, + { + "percentile": "p99", + "value": 10512.040100250626 + } + ] + }, + "timePerRequest": { + "statistics": { + "total": 1704, + "mean": 2321.92987861208, + "median": 2313.3785724639893, + "min": 2290.93074798584, + "max": 2594.4881439208984, + "std": 29.46118583560937 + }, + "percentiles": [ + { + "percentile": "p50", + "value": 2313.3785724639893 + }, + { + "percentile": "p90", + "value": 2339.4439220428467 + }, + { + "percentile": "p95", + "value": 2341.9249057769775 + }, + { + "percentile": "p99", + "value": 2370.450496673584 + } + ] + } + }, + { + "requestsPerSecond": 17.5623040970073, + "tpot": { + "statistics": { + "total": 2106, + "mean": 39.546438065771135, + "median": 39.47442675393725, + "min": 38.74176740646362, + "max": 43.32651032341851, + "std": 0.3121106751660994 + }, + "percentiles": [ + { + "percentile": "p50", + "value": 39.47442675393725 + }, + { + "percentile": "p90", + "value": 39.722594003828746 + }, + { + "percentile": "p95", + "value": 40.083578654697966 + }, + { + "percentile": "p99", + "value": 40.73049983040231 + } + ] + }, + "ttft": { + "statistics": { + "total": 2106, + "mean": 85.68002797259905, + "median": 89.88213539123535, + "min": 57.360172271728516, + "max": 362.8504276275635, + "std": 27.802786177158218 + }, + "percentiles": [ + { + "percentile": "p50", + "value": 89.88213539123535 + }, + { + "percentile": "p90", + "value": 101.7305850982666 + }, + { + "percentile": "p95", + "value": 103.26790809631348 + }, + { + "percentile": "p99", + "value": 138.88931274414062 + } + ] + }, + "throughput": { + "statistics": { + "total": 15121, + "mean": 1123.0284569989917, + "median": 99.91909855397003, + "min": 0.0, + "max": 932067.5555555555, + "std": 4358.833642800455 + }, + "percentiles": [ + { + "percentile": "p50", + "value": 99.91909855397003 + }, + { + "percentile": "p90", + "value": 2868.8809849521203 + }, + { + "percentile": "p95", + "value": 4848.906358381503 + }, + { + "percentile": "p99", + "value": 12905.55076923077 + } + ] + }, + "timePerRequest": { + "statistics": { + "total": 2106, + "mean": 2575.916517267653, + "median": 2573.6281871795654, + "min": 2533.904790878296, + "max": 2894.4458961486816, + "std": 33.18594265783404 + }, + "percentiles": [ + { + "percentile": "p50", + "value": 2573.6281871795654 + }, + { + "percentile": "p90", + "value": 2588.9015197753906 + }, + { + "percentile": "p95", + "value": 2591.136932373047 + }, + { + "percentile": "p99", + "value": 2700.568437576294 + } + ] + } + }, + { + "requestsPerSecond": 20.885632360055222, + "tpot": { + "statistics": { + "total": 2505, + "mean": 44.20494748431818, + "median": 44.02147020612444, + "min": 42.981475591659546, + "max": 52.62617986710345, + "std": 1.0422073399474652 + }, + "percentiles": [ + { + "percentile": "p50", + "value": 44.02147020612444 + }, + { + "percentile": "p90", + "value": 44.47330747331892 + }, + { + "percentile": "p95", + "value": 45.131300316482296 + }, + { + "percentile": "p99", + "value": 50.400745301019576 + } + ] + }, + "ttft": { + "statistics": { + "total": 2505, + "mean": 98.4621736103903, + "median": 95.84355354309082, + "min": 61.09285354614258, + "max": 524.099588394165, + "std": 34.20521833421915 + }, + "percentiles": [ + { + "percentile": "p50", + "value": 95.84355354309082 + }, + { + "percentile": "p90", + "value": 109.4822883605957 + }, + { + "percentile": "p95", + "value": 111.46354675292969 + }, + { + "percentile": "p99", + "value": 334.31243896484375 + } + ] + }, + "throughput": { + "statistics": { + "total": 14779, + "mean": 1335.7133120200747, + "median": 104.45284522475407, + "min": 0.0, + "max": 1677721.6, + "std": 5200.1934248077005 + }, + "percentiles": [ + { + "percentile": "p50", + "value": 104.45284522475407 + }, + { + "percentile": "p90", + "value": 3472.1059602649007 + }, + { + "percentile": "p95", + "value": 5882.6143057503505 + }, + { + "percentile": "p99", + "value": 15768.060150375939 + } + ] + }, + "timePerRequest": { + "statistics": { + "total": 2505, + "mean": 2882.6246785070603, + "median": 2869.71378326416, + "min": 2826.8485069274902, + "max": 3324.9876499176025, + "std": 78.07038363701177 + }, + "percentiles": [ + { + "percentile": "p50", + "value": 2869.71378326416 + }, + { + "percentile": "p90", + "value": 2888.715982437134 + }, + { + "percentile": "p95", + "value": 2937.7262592315674 + }, + { + "percentile": "p99", + "value": 3282.898426055908 + } + ] + } + }, + { + "requestsPerSecond": 24.179871480414207, + "tpot": { + "statistics": { + "total": 2900, + "mean": 51.023722283946924, + "median": 50.24327550615583, + "min": 47.58137645143451, + "max": 60.63385087935651, + "std": 2.0749227872708285 + }, + "percentiles": [ + { + "percentile": "p50", + "value": 50.24327550615583 + }, + { + "percentile": "p90", + "value": 52.928451507810564 + }, + { + "percentile": "p95", + "value": 57.28437408568367 + }, + { + "percentile": "p99", + "value": 58.51330454387362 + } + ] + }, + "ttft": { + "statistics": { + "total": 2900, + "mean": 123.56691516678907, + "median": 115.33927917480469, + "min": 88.05131912231445, + "max": 594.1901206970215, + "std": 44.50765227271787 + }, + "percentiles": [ + { + "percentile": "p50", + "value": 115.33927917480469 + }, + { + "percentile": "p90", + "value": 141.8297290802002 + }, + { + "percentile": "p95", + "value": 144.49095726013184 + }, + { + "percentile": "p99", + "value": 375.5221366882324 + } + ] + }, + "throughput": { + "statistics": { + "total": 14925, + "mean": 1546.3194569459229, + "median": 138.59511614843208, + "min": 0.0, + "max": 1677721.6, + "std": 5844.302138842639 + }, + "percentiles": [ + { + "percentile": "p50", + "value": 138.59511614843208 + }, + { + "percentile": "p90", + "value": 3916.250233426704 + }, + { + "percentile": "p95", + "value": 6678.828025477707 + }, + { + "percentile": "p99", + "value": 17924.37606837607 + } + ] + }, + "timePerRequest": { + "statistics": { + "total": 2900, + "mean": 3336.9750574539444, + "median": 3282.672882080078, + "min": 3228.010654449463, + "max": 3863.8863563537598, + "std": 141.37106520368962 + }, + "percentiles": [ + { + "percentile": "p50", + "value": 3282.672882080078 + }, + { + "percentile": "p90", + "value": 3561.7692470550537 + }, + { + "percentile": "p95", + "value": 3737.921953201294 + }, + { + "percentile": "p99", + "value": 3811.5434646606445 + } + ] + } + }, + { + "requestsPerSecond": 27.382251189847466, + "tpot": { + "statistics": { + "total": 3285, + "mean": 62.44881585866599, + "median": 60.908238093058266, + "min": 58.94644298250713, + "max": 72.59870383699061, + "std": 2.9764436606898887 + }, + "percentiles": [ + { + "percentile": "p50", + "value": 60.908238093058266 + }, + { + "percentile": "p90", + "value": 68.3861043718126 + }, + { + "percentile": "p95", + "value": 69.21934324597555 + }, + { + "percentile": "p99", + "value": 70.13290269034249 + } + ] + }, + "ttft": { + "statistics": { + "total": 3285, + "mean": 142.7834399758953, + "median": 129.18686866760254, + "min": 92.2248363494873, + "max": 802.5562763214111, + "std": 54.896961282893 + }, + "percentiles": [ + { + "percentile": "p50", + "value": 129.18686866760254 + }, + { + "percentile": "p90", + "value": 158.26964378356934 + }, + { + "percentile": "p95", + "value": 166.79859161376953 + }, + { + "percentile": "p99", + "value": 422.8503704071045 + } + ] + }, + "throughput": { + "statistics": { + "total": 15706, + "mean": 1751.1720673421933, + "median": 318.5950626661603, + "min": 0.0, + "max": 1677721.6, + "std": 6434.120608249914 + }, + "percentiles": [ + { + "percentile": "p50", + "value": 318.5950626661603 + }, + { + "percentile": "p90", + "value": 4165.147964250248 + }, + { + "percentile": "p95", + "value": 7194.346483704974 + }, + { + "percentile": "p99", + "value": 19878.218009478675 + } + ] + }, + "timePerRequest": { + "statistics": { + "total": 3285, + "mean": 4076.002237894764, + "median": 3972.564697265625, + "min": 3890.990972518921, + "max": 4623.138666152954, + "std": 197.81266460135544 + }, + "percentiles": [ + { + "percentile": "p50", + "value": 3972.564697265625 + }, + { + "percentile": "p90", + "value": 4444.445371627808 + }, + { + "percentile": "p95", + "value": 4506.659030914307 + }, + { + "percentile": "p99", + "value": 4553.745985031128 + } + ] + } + } + ] +};`; diff --git a/src/ui/lib/store/index.ts b/src/ui/lib/store/index.ts index c1671d75..39ec0aa4 100644 --- a/src/ui/lib/store/index.ts +++ b/src/ui/lib/store/index.ts @@ -1,21 +1,28 @@ -import { configureStore, createSlice, PayloadAction } from '@reduxjs/toolkit'; +import { configureStore } from '@reduxjs/toolkit'; -const defaultSlice = createSlice({ - name: 'model.state', - initialState: { model: 'meta-llama/Llama-2-7b' }, - reducers: { - setDefaultData: (state, action: PayloadAction<string>) => { - return { ...state, model: action.payload }; - }, - }, -}); - -export const { setDefaultData } = defaultSlice.actions; +import { benchmarksApi, benchmarksReducer } from './slices/benchmarks'; +import metricsReducer from './slices/metrics/metrics.slice'; +import { runInfoApi, infoReducer } from './slices/runInfo'; +import sloReducer from './slices/slo/slo.slice'; +import { workloadDetailsApi, workloadDetailsReducer } from './slices/workloadDetails'; export const store = configureStore({ reducer: { - default: defaultSlice.reducer, + metrics: metricsReducer, + slo: sloReducer, + runInfo: infoReducer, + [runInfoApi.reducerPath]: runInfoApi.reducer, + benchmarks: benchmarksReducer, + [benchmarksApi.reducerPath]: benchmarksApi.reducer, + workloadDetails: workloadDetailsReducer, + [workloadDetailsApi.reducerPath]: workloadDetailsApi.reducer, }, + middleware: (getDefaultMiddleware) => + getDefaultMiddleware().concat( + runInfoApi.middleware, + benchmarksApi.middleware, + workloadDetailsApi.middleware + ), }); export type RootState = ReturnType<typeof store.getState>; diff --git a/src/ui/lib/store/mockData.ts b/src/ui/lib/store/mockData.ts new file mode 100644 index 00000000..8295c60c --- /dev/null +++ b/src/ui/lib/store/mockData.ts @@ -0,0 +1,191 @@ +export const runInfo = { + model: { + name: 'Model name', + size: 12345678, + }, + task: 'Task name', + dataset: { + name: 'Dataset name', + }, + timestamp: '1686700800', +}; + +export const workloadDetails = { + prompts: { + samples: ['string'], + tokenDistributions: { + statistics: { + total: 0, + mean: 0, + std: 0, + median: 0, + min: 0, + max: 0, + }, + percentiles: [ + { + percentile: 'p50', + value: 0, + }, + ], + buckets: [ + { + value: 0, + count: 0, + }, + ], + bucketWidth: 0, + }, + }, + generations: { + samples: ['string'], + tokenDistributions: { + statistics: { + total: 0, + mean: 0, + std: 0, + median: 0, + min: 0, + max: 0, + }, + percentiles: [ + { + percentile: 'p50', + value: 0, + }, + ], + buckets: [ + { + value: 0, + count: 0, + }, + ], + bucketWidth: 0, + }, + }, + server: { + target: '128.0.0.1', + protocolType: 'string', + port: '8000', + }, +}; + +export const benchmarks = [ + { + ttft: { + statistics: { + total: 0, + mean: 0, + std: 0, + median: 0, + min: 0, + max: 0, + }, + percentiles: [ + { + percentile: 'p50', + value: 0, + }, + ], + buckets: [ + { + value: 0, + count: 0, + }, + ], + bucketWidth: 0, + }, + tpot: { + statistics: { + total: 0, + mean: 0, + std: 0, + median: 0, + min: 0, + max: 0, + }, + percentiles: [ + { + percentile: 'p50', + value: 0, + }, + ], + buckets: [ + { + value: 0, + count: 0, + }, + ], + bucketWidth: 0, + }, + timePerRequest: { + statistics: { + total: 0, + mean: 0, + std: 0, + median: 0, + min: 0, + max: 0, + }, + percentiles: [ + { + percentile: 'p50', + value: 0, + }, + ], + buckets: [ + { + value: 0, + count: 0, + }, + ], + bucketWidth: 0, + }, + requestOverTime: { + statistics: { + total: 0, + mean: 0, + std: 0, + median: 0, + min: 0, + max: 0, + }, + percentiles: [ + { + percentile: 'p50', + value: 0, + }, + ], + buckets: [ + { + value: 0, + count: 0, + }, + ], + bucketWidth: 0, + }, + throughput: { + statistics: { + total: 0, + mean: 0, + std: 0, + median: 0, + min: 0, + max: 0, + }, + percentiles: [ + { + percentile: 'p50', + value: 0, + }, + ], + buckets: [ + { + value: 0, + count: 0, + }, + ], + bucketWidth: 0, + }, + }, +]; diff --git a/src/ui/lib/store/runInfoWindowData.ts b/src/ui/lib/store/runInfoWindowData.ts new file mode 100644 index 00000000..1fe0e21d --- /dev/null +++ b/src/ui/lib/store/runInfoWindowData.ts @@ -0,0 +1,11 @@ +export const runInfoScript = `window.run_info = { + "model": { + "name": "neuralmagic/Qwen2.5-7B-quantized.w8a8", + "size": 0 + }, + "task": "N/A", + "dataset": { + "name": "N/A" + }, + "timestamp": 1744310555.0286171 +};`; diff --git a/src/ui/lib/store/slices/benchmarks/benchmarks.api.ts b/src/ui/lib/store/slices/benchmarks/benchmarks.api.ts new file mode 100644 index 00000000..f1a7b52e --- /dev/null +++ b/src/ui/lib/store/slices/benchmarks/benchmarks.api.ts @@ -0,0 +1,97 @@ +import { ThunkDispatch, UnknownAction } from '@reduxjs/toolkit'; +import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react'; + +import { formatNumber } from '../../../utils/helpers'; + +import { Benchmarks, MetricData } from './benchmarks.interfaces'; +import { defaultPercentile } from '../slo/slo.constants'; +import { SloState } from '../slo/slo.interfaces'; +import { setSloData } from '../slo/slo.slice'; + +const USE_MOCK_API = process.env.NEXT_PUBLIC_USE_MOCK_API === 'true'; + +const fetchBenchmarks = async () => { + return { data: window.benchmarks as Benchmarks }; +}; + +const getAverageValueForPercentile = ( + firstMetric: MetricData, + lastMetric: MetricData, + percentile: string +) => { + const firstPercentile = firstMetric.percentiles.find( + (p) => p.percentile === percentile + ); + const lastPercentile = lastMetric.percentiles.find( + (p) => p.percentile === percentile + ); + return ((firstPercentile?.value ?? 0) + (lastPercentile?.value ?? 0)) / 2; +}; + +const setDefaultSLOs = ( + data: Benchmarks, + dispatch: ThunkDispatch<SloState, unknown, UnknownAction> +) => { + // temporarily set default slo values, long term the backend should set default slos that will not just be the avg at the default percentile + const firstBM = data.benchmarks[0]; + const lastBM = data.benchmarks[data.benchmarks.length - 1]; + + const ttftAvg = getAverageValueForPercentile( + firstBM.ttft, + lastBM.ttft, + defaultPercentile + ); + const tpotAvg = getAverageValueForPercentile( + firstBM.tpot, + lastBM.tpot, + defaultPercentile + ); + const timePerRequestAvg = getAverageValueForPercentile( + firstBM.timePerRequest, + lastBM.timePerRequest, + defaultPercentile + ); + const throughputAvg = getAverageValueForPercentile( + firstBM.throughput, + lastBM.throughput, + defaultPercentile + ); + + dispatch( + setSloData({ + currentRequestRate: firstBM.requestsPerSecond, + current: { + ttft: formatNumber(ttftAvg, 0), + tpot: formatNumber(tpotAvg, 0), + timePerRequest: formatNumber(timePerRequestAvg, 0), + throughput: formatNumber(throughputAvg, 0), + }, + tasksDefaults: { + ttft: formatNumber(ttftAvg, 0), + tpot: formatNumber(tpotAvg, 0), + timePerRequest: formatNumber(timePerRequestAvg, 0), + throughput: formatNumber(throughputAvg, 0), + }, + }) + ); +}; + +export const benchmarksApi = createApi({ + reducerPath: 'benchmarksApi', + baseQuery: USE_MOCK_API ? fetchBenchmarks : fetchBaseQuery({ baseUrl: '/api' }), + endpoints: (builder) => ({ + getBenchmarks: builder.query<Benchmarks, void>({ + query: () => 'benchmarks', + async onQueryStarted(_, { dispatch, queryFulfilled }) { + try { + const { data } = await queryFulfilled; + setDefaultSLOs(data, dispatch); + } catch (err) { + console.error('Failed to fetch benchmarks:', err); + } + }, + }), + }), +}); + +export const { useGetBenchmarksQuery } = benchmarksApi; diff --git a/src/ui/lib/store/slices/benchmarks/benchmarks.constants.ts b/src/ui/lib/store/slices/benchmarks/benchmarks.constants.ts new file mode 100644 index 00000000..deb444b2 --- /dev/null +++ b/src/ui/lib/store/slices/benchmarks/benchmarks.constants.ts @@ -0,0 +1,7 @@ +import { Benchmarks, Name } from './benchmarks.interfaces'; + +export const name: Readonly<Name> = 'benchmarks'; + +export const initialState: Benchmarks = { + benchmarks: [], +}; diff --git a/src/ui/lib/store/slices/benchmarks/benchmarks.interfaces.ts b/src/ui/lib/store/slices/benchmarks/benchmarks.interfaces.ts new file mode 100644 index 00000000..4dc755b2 --- /dev/null +++ b/src/ui/lib/store/slices/benchmarks/benchmarks.interfaces.ts @@ -0,0 +1,44 @@ +export type Name = 'benchmarks'; + +interface Statistics { + total: number; + mean: number; + std: number; + median: number; + min: number; + max: number; +} + +export type PercentileValues = 'p50' | 'p90' | 'p95' | 'p99'; + +interface Percentile { + percentile: string; + value: number; +} + +interface Bucket { + value: number; + count: number; +} + +export interface MetricData { + statistics: Statistics; + percentiles: Percentile[]; + buckets: Bucket[]; + bucketWidth: number; +} + +export interface BenchmarkMetrics { + ttft: MetricData; + tpot: MetricData; + timePerRequest: MetricData; + throughput: MetricData; +} + +export interface Benchmark extends BenchmarkMetrics { + requestsPerSecond: number; +} + +export type Benchmarks = { + benchmarks: Benchmark[]; +}; diff --git a/src/ui/lib/store/slices/benchmarks/benchmarks.selectors.ts b/src/ui/lib/store/slices/benchmarks/benchmarks.selectors.ts new file mode 100644 index 00000000..4a86d422 --- /dev/null +++ b/src/ui/lib/store/slices/benchmarks/benchmarks.selectors.ts @@ -0,0 +1,173 @@ +import { createSelector } from '@reduxjs/toolkit'; + +import { PercentileItem } from '../../../components/DistributionPercentiles'; +import { formatNumber } from '../../../utils/helpers'; +import { createMonotoneSpline } from '../../../utils/interpolation'; +import { RootState } from '../../index'; +import { Point } from '@/lib/components/Charts/common/interfaces'; + +import { BenchmarkMetrics, PercentileValues } from './benchmarks.interfaces'; +import { selectSloState } from '../slo/slo.selectors'; + +export const selectBenchmarks = (state: RootState) => state.benchmarks.data; + +export const selectMetricsSummaryLineData = createSelector( + [selectBenchmarks, selectSloState], + (benchmarks, sloState) => { + const sortedByRPS = benchmarks?.benchmarks + ?.slice() + ?.sort((bm1, bm2) => (bm1.requestsPerSecond > bm2.requestsPerSecond ? 1 : -1)); + const selectedPercentile = sloState.enforcedPercentile; + + const lineData: { [K in keyof BenchmarkMetrics]: Point[] } = { + ttft: [], + tpot: [], + timePerRequest: [], + throughput: [], + }; + const metrics: (keyof BenchmarkMetrics)[] = [ + 'ttft', + 'tpot', + 'timePerRequest', + 'throughput', + ]; + metrics.forEach((metric) => { + const data: Point[] = []; + sortedByRPS?.forEach((benchmark) => { + const percentile = benchmark[metric].percentiles.find( + (p) => p.percentile === selectedPercentile + ); + data.push({ + x: benchmark.requestsPerSecond, + y: percentile?.value ?? 0, + }); + }); + + lineData[metric] = data; + }); + return lineData; + } +); + +const getDefaultMetricValues = () => ({ + enforcedPercentileValue: 0, + mean: 0, + percentiles: [], +}); + +export const selectInterpolatedMetrics = createSelector( + [selectBenchmarks, selectSloState], + (benchmarks, sloState) => { + const sortedByRPS = benchmarks?.benchmarks + ?.slice() + ?.sort((bm1, bm2) => (bm1.requestsPerSecond > bm2.requestsPerSecond ? 1 : -1)); + const requestRates = sortedByRPS?.map((bm) => bm.requestsPerSecond) || []; + const { enforcedPercentile, currentRequestRate } = sloState; + const metricData: { + [K in keyof BenchmarkMetrics | 'mean']: { + enforcedPercentileValue: number; + mean: number; + percentiles: PercentileItem[]; + }; + } = { + ttft: getDefaultMetricValues(), + tpot: getDefaultMetricValues(), + timePerRequest: getDefaultMetricValues(), + throughput: getDefaultMetricValues(), + mean: getDefaultMetricValues(), + }; + const metrics: (keyof BenchmarkMetrics)[] = [ + 'ttft', + 'tpot', + 'timePerRequest', + 'throughput', + ]; + if (!sortedByRPS || sortedByRPS.length === 0) { + return metricData; + } + const invalidRps = + currentRequestRate < sortedByRPS[0].requestsPerSecond || + currentRequestRate > sortedByRPS[sortedByRPS.length - 1].requestsPerSecond; + if (invalidRps) { + return metricData; + } + metrics.forEach((metric) => { + const meanValues = sortedByRPS.map((bm) => bm[metric].statistics.mean); + const interpolateMeanAt = createMonotoneSpline(requestRates, meanValues); + const interpolatedMeanValue: number = interpolateMeanAt(currentRequestRate) || 0; + const percentiles: PercentileValues[] = ['p50', 'p90', 'p95', 'p99']; + const valuesByPercentile = percentiles.map((p) => { + const bmValuesAtP = sortedByRPS.map((bm) => { + const result = + bm[metric].percentiles.find((percentile) => percentile.percentile === p) + ?.value || 0; + return result; + }); + const interpolateValueAtP = createMonotoneSpline(requestRates, bmValuesAtP); + const interpolatedValueAtP = formatNumber( + interpolateValueAtP(currentRequestRate) + ); + return { label: p, value: `${interpolatedValueAtP}` } as PercentileItem; + }); + const interpolatedPercentileValue = + Number(valuesByPercentile.find((p) => p.label === enforcedPercentile)?.value) || + 0; + metricData[metric] = { + enforcedPercentileValue: interpolatedPercentileValue, + mean: interpolatedMeanValue, + percentiles: valuesByPercentile, + }; + }); + return metricData; + } +); + +export const selectMetricsDetailsLineData = createSelector( + [selectBenchmarks], + (benchmarks) => { + const sortedByRPS = + benchmarks?.benchmarks + ?.slice() + ?.sort((bm1, bm2) => + bm1.requestsPerSecond > bm2.requestsPerSecond ? 1 : -1 + ) || []; + + const lineData: { + [K in keyof BenchmarkMetrics]: { data: Point[]; id: string; solid?: boolean }[]; + } = { + ttft: [], + tpot: [], + timePerRequest: [], + throughput: [], + }; + const props: (keyof BenchmarkMetrics)[] = [ + 'ttft', + 'tpot', + 'timePerRequest', + 'throughput', + ]; + props.forEach((prop) => { + if (sortedByRPS.length === 0) { + return; + } + const data: { [key: string]: { data: Point[]; id: string; solid?: boolean } } = + {}; + sortedByRPS[0].ttft.percentiles.forEach((p) => { + data[p.percentile] = { data: [], id: p.percentile }; + }); + data.mean = { data: [], id: 'mean', solid: true }; + sortedByRPS?.forEach((benchmark) => { + const rps = benchmark.requestsPerSecond; + benchmark[prop].percentiles.forEach((p) => { + data[p.percentile].data.push({ x: rps, y: p.value }); + }); + const mean = benchmark[prop].statistics.mean; + data.mean.data.push({ x: rps, y: mean }); + }); + lineData[prop] = Object.keys(data).map((key) => { + return data[key]; + }); + }); + return lineData; + } +); diff --git a/src/ui/lib/store/slices/benchmarks/benchmarks.slice.ts b/src/ui/lib/store/slices/benchmarks/benchmarks.slice.ts new file mode 100644 index 00000000..1090da3e --- /dev/null +++ b/src/ui/lib/store/slices/benchmarks/benchmarks.slice.ts @@ -0,0 +1,34 @@ +import { createSlice, PayloadAction } from '@reduxjs/toolkit'; + +import { benchmarksApi } from './benchmarks.api'; +import * as Constants from './benchmarks.constants'; +import { Benchmarks } from './benchmarks.interfaces'; + +interface BenchmarksState { + data: Benchmarks | null; +} + +const initialState: BenchmarksState = { + data: null, +}; + +const benchmarksSlice = createSlice({ + name: Constants.name, + initialState, + reducers: { + setBenchmarks: (state, action: PayloadAction<Benchmarks>) => { + state.data = action.payload; + }, + }, + extraReducers: (builder) => { + builder.addMatcher( + benchmarksApi.endpoints.getBenchmarks.matchFulfilled, + (state, action) => { + state.data = action.payload; + } + ); + }, +}); + +export const { setBenchmarks } = benchmarksSlice.actions; +export default benchmarksSlice.reducer; diff --git a/src/ui/lib/store/slices/benchmarks/index.ts b/src/ui/lib/store/slices/benchmarks/index.ts new file mode 100644 index 00000000..953b52c8 --- /dev/null +++ b/src/ui/lib/store/slices/benchmarks/index.ts @@ -0,0 +1,4 @@ +export * from './benchmarks.api'; +export { default as benchmarksReducer } from './benchmarks.slice'; +export * from './benchmarks.selectors'; +export * from './benchmarks.interfaces'; diff --git a/src/ui/lib/store/slices/metrics/metrics.constants.ts b/src/ui/lib/store/slices/metrics/metrics.constants.ts new file mode 100644 index 00000000..a9ae8414 --- /dev/null +++ b/src/ui/lib/store/slices/metrics/metrics.constants.ts @@ -0,0 +1,10 @@ +import { MetricsState, Name } from './metrics.interfaces'; +export const name: Readonly<Name> = 'metrics.state'; + +export const initialState: MetricsState = { + currentRequestRate: 0, + timePerRequest: { valuesByRps: {} }, + ttft: { valuesByRps: {} }, + tpot: { valuesByRps: {} }, + throughput: { valuesByRps: {} }, +}; diff --git a/src/ui/lib/store/slices/metrics/metrics.interfaces.ts b/src/ui/lib/store/slices/metrics/metrics.interfaces.ts new file mode 100644 index 00000000..b38dc98b --- /dev/null +++ b/src/ui/lib/store/slices/metrics/metrics.interfaces.ts @@ -0,0 +1,13 @@ +export type Name = 'metrics.state'; + +export interface MetricsState { + currentRequestRate: number; + timePerRequest: SingleMetricsState; + ttft: SingleMetricsState; + tpot: SingleMetricsState; + throughput: SingleMetricsState; +} + +export type SingleMetricsState = { + valuesByRps: Record<number, number>; +}; diff --git a/src/ui/lib/store/slices/metrics/metrics.selectors.ts b/src/ui/lib/store/slices/metrics/metrics.selectors.ts new file mode 100644 index 00000000..9aa4d46a --- /dev/null +++ b/src/ui/lib/store/slices/metrics/metrics.selectors.ts @@ -0,0 +1,3 @@ +import { RootState } from '../../index'; + +export const selectMetricsState = (state: RootState) => state.metrics; diff --git a/src/ui/lib/store/slices/metrics/metrics.slice.ts b/src/ui/lib/store/slices/metrics/metrics.slice.ts new file mode 100644 index 00000000..40f16dc1 --- /dev/null +++ b/src/ui/lib/store/slices/metrics/metrics.slice.ts @@ -0,0 +1,20 @@ +import { createSlice, PayloadAction } from '@reduxjs/toolkit'; + +import * as Constants from './metrics.constants'; +import { MetricsState } from './metrics.interfaces'; + +const metricsSlice = createSlice({ + name: Constants.name, + initialState: Constants.initialState, + reducers: { + setMetricsData: (state, action: PayloadAction<MetricsState>) => { + return { ...state, ...action.payload }; + }, + setSliderRps: (state, action: PayloadAction<number>) => { + state.currentRequestRate = action.payload; + }, + }, +}); + +export const { setMetricsData, setSliderRps } = metricsSlice.actions; +export default metricsSlice.reducer; diff --git a/src/ui/lib/store/slices/runInfo/index.ts b/src/ui/lib/store/slices/runInfo/index.ts new file mode 100644 index 00000000..5595ffac --- /dev/null +++ b/src/ui/lib/store/slices/runInfo/index.ts @@ -0,0 +1,4 @@ +export * from './runInfo.api'; +export { default as infoReducer } from './runInfo.slice'; +export * from './runInfo.selectors'; +export * from './runInfo.interfaces'; diff --git a/src/ui/lib/store/slices/runInfo/runInfo.api.ts b/src/ui/lib/store/slices/runInfo/runInfo.api.ts new file mode 100644 index 00000000..9c510b5b --- /dev/null +++ b/src/ui/lib/store/slices/runInfo/runInfo.api.ts @@ -0,0 +1,21 @@ +import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react'; + +import { RunInfo } from './runInfo.interfaces'; + +const USE_MOCK_API = process.env.NEXT_PUBLIC_USE_MOCK_API === 'true'; + +const fetchRunInfo = async () => { + return { data: window.run_info as RunInfo }; +}; + +export const runInfoApi = createApi({ + reducerPath: 'runInfoApi', + baseQuery: USE_MOCK_API ? fetchRunInfo : fetchBaseQuery({ baseUrl: '/api' }), + endpoints: (builder) => ({ + getRunInfo: builder.query<RunInfo, void>({ + query: () => 'run-info', + }), + }), +}); + +export const { useGetRunInfoQuery } = runInfoApi; diff --git a/src/ui/lib/store/slices/runInfo/runInfo.constants.ts b/src/ui/lib/store/slices/runInfo/runInfo.constants.ts new file mode 100644 index 00000000..af773759 --- /dev/null +++ b/src/ui/lib/store/slices/runInfo/runInfo.constants.ts @@ -0,0 +1,3 @@ +import { Name } from './runInfo.interfaces'; + +export const name: Readonly<Name> = 'runInfo'; diff --git a/src/ui/lib/store/slices/runInfo/runInfo.interfaces.ts b/src/ui/lib/store/slices/runInfo/runInfo.interfaces.ts new file mode 100644 index 00000000..6fb8b393 --- /dev/null +++ b/src/ui/lib/store/slices/runInfo/runInfo.interfaces.ts @@ -0,0 +1,13 @@ +export type Name = 'runInfo'; + +export interface RunInfo { + model: { + name: string; + size: number; + }; + task: string; + dataset: { + name: string; + }; + timestamp: string; +} diff --git a/src/ui/lib/store/slices/runInfo/runInfo.selectors.ts b/src/ui/lib/store/slices/runInfo/runInfo.selectors.ts new file mode 100644 index 00000000..1f8338ca --- /dev/null +++ b/src/ui/lib/store/slices/runInfo/runInfo.selectors.ts @@ -0,0 +1,3 @@ +import { RootState } from '../../index'; + +export const selectRunInfo = (state: RootState) => state.runInfo.data; diff --git a/src/ui/lib/store/slices/runInfo/runInfo.slice.ts b/src/ui/lib/store/slices/runInfo/runInfo.slice.ts new file mode 100644 index 00000000..8bb48425 --- /dev/null +++ b/src/ui/lib/store/slices/runInfo/runInfo.slice.ts @@ -0,0 +1,34 @@ +import { createSlice, PayloadAction } from '@reduxjs/toolkit'; + +import { runInfoApi } from './runInfo.api'; +import * as Constants from './runInfo.constants'; +import { RunInfo } from './runInfo.interfaces'; + +interface RunInfoState { + data: RunInfo | null; +} + +const initialState: RunInfoState = { + data: null, +}; + +const runInfoSlice = createSlice({ + name: Constants.name, + initialState, + reducers: { + setRunInfo: (state, action: PayloadAction<RunInfo>) => { + state.data = action.payload; + }, + }, + extraReducers: (builder) => { + builder.addMatcher( + runInfoApi.endpoints.getRunInfo.matchFulfilled, + (state, action) => { + state.data = action.payload; + } + ); + }, +}); + +export const { setRunInfo } = runInfoSlice.actions; +export default runInfoSlice.reducer; diff --git a/src/ui/lib/store/slices/slo/slo.constants.ts b/src/ui/lib/store/slices/slo/slo.constants.ts new file mode 100644 index 00000000..f58ccc05 --- /dev/null +++ b/src/ui/lib/store/slices/slo/slo.constants.ts @@ -0,0 +1,22 @@ +import { Name, SloState } from './slo.interfaces'; + +export const name: Readonly<Name> = 'slo.state'; + +export const defaultPercentile = 'p90'; + +export const initialState: SloState = { + currentRequestRate: 0, + enforcedPercentile: defaultPercentile, + current: { + timePerRequest: 0, + ttft: 0, + tpot: 0, + throughput: 0, + }, + tasksDefaults: { + timePerRequest: 0, + ttft: 0, + tpot: 0, + throughput: 0, + }, +}; diff --git a/src/ui/lib/store/slices/slo/slo.interfaces.ts b/src/ui/lib/store/slices/slo/slo.interfaces.ts new file mode 100644 index 00000000..0d59baa2 --- /dev/null +++ b/src/ui/lib/store/slices/slo/slo.interfaces.ts @@ -0,0 +1,18 @@ +export type Name = 'slo.state'; + +export interface SloState { + currentRequestRate: number; + enforcedPercentile: string; + current: { + timePerRequest: number; + ttft: number; + tpot: number; + throughput: number; + }; + tasksDefaults: { + timePerRequest: number; + ttft: number; + tpot: number; + throughput: number; + }; +} diff --git a/src/ui/lib/store/slices/slo/slo.selectors.ts b/src/ui/lib/store/slices/slo/slo.selectors.ts new file mode 100644 index 00000000..38000830 --- /dev/null +++ b/src/ui/lib/store/slices/slo/slo.selectors.ts @@ -0,0 +1,3 @@ +import { RootState } from '../../index'; + +export const selectSloState = (state: RootState) => state.slo; diff --git a/src/ui/lib/store/slices/slo/slo.slice.ts b/src/ui/lib/store/slices/slo/slo.slice.ts new file mode 100644 index 00000000..5b3b8853 --- /dev/null +++ b/src/ui/lib/store/slices/slo/slo.slice.ts @@ -0,0 +1,47 @@ +import { createSlice, PayloadAction } from '@reduxjs/toolkit'; + +import * as Constants from './slo.constants'; +import { SloState } from './slo.interfaces'; + +const sloSlice = createSlice({ + name: Constants.name, + initialState: Constants.initialState, + reducers: { + setSloData: (state, action: PayloadAction<Partial<SloState>>) => { + if (action.payload.enforcedPercentile !== undefined) { + state.enforcedPercentile = action.payload.enforcedPercentile; + } + if (action.payload.current) { + state.current = { ...state.current, ...action.payload.current }; + } + if (action.payload.tasksDefaults) { + state.tasksDefaults = { + ...state.tasksDefaults, + ...action.payload.tasksDefaults, + }; + } + if (action.payload.currentRequestRate) { + state.currentRequestRate = action.payload.currentRequestRate; + } + }, + setEnforcedPercentile: (state, action: PayloadAction<string>) => { + state.enforcedPercentile = action.payload; + }, + setCurrentRequestRate: (state, action: PayloadAction<number>) => { + state.currentRequestRate = action.payload; + }, + setSloValue: ( + state, + action: PayloadAction<{ metric: keyof SloState['current']; value: number }> + ) => { + const { metric, value } = action.payload; + if (value >= 0) { + state.current[metric] = value; + } + }, + }, +}); + +export const { setCurrentRequestRate, setEnforcedPercentile, setSloData, setSloValue } = + sloSlice.actions; +export default sloSlice.reducer; diff --git a/src/ui/lib/store/slices/workloadDetails/index.ts b/src/ui/lib/store/slices/workloadDetails/index.ts new file mode 100644 index 00000000..e5cd556b --- /dev/null +++ b/src/ui/lib/store/slices/workloadDetails/index.ts @@ -0,0 +1,4 @@ +export * from './workloadDetails.api'; +export { default as workloadDetailsReducer } from './workloadDetails.slice'; +export * from './workloadDetails.selectors'; +export * from './workloadDetails.interfaces'; diff --git a/src/ui/lib/store/slices/workloadDetails/workloadDetails.api.ts b/src/ui/lib/store/slices/workloadDetails/workloadDetails.api.ts new file mode 100644 index 00000000..6720f865 --- /dev/null +++ b/src/ui/lib/store/slices/workloadDetails/workloadDetails.api.ts @@ -0,0 +1,21 @@ +import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react'; + +import { WorkloadDetails } from './workloadDetails.interfaces'; + +const USE_MOCK_API = process.env.NEXT_PUBLIC_USE_MOCK_API === 'true'; + +const fetchWorkloadDetails = async () => { + return { data: window.workload_details as WorkloadDetails }; +}; + +export const workloadDetailsApi = createApi({ + reducerPath: 'workloadDetailsApi', + baseQuery: USE_MOCK_API ? fetchWorkloadDetails : fetchBaseQuery({ baseUrl: '/api' }), + endpoints: (builder) => ({ + getWorkloadDetails: builder.query<WorkloadDetails, void>({ + query: () => 'workload-details', + }), + }), +}); + +export const { useGetWorkloadDetailsQuery } = workloadDetailsApi; diff --git a/src/ui/lib/store/slices/workloadDetails/workloadDetails.constants.ts b/src/ui/lib/store/slices/workloadDetails/workloadDetails.constants.ts new file mode 100644 index 00000000..c45efa76 --- /dev/null +++ b/src/ui/lib/store/slices/workloadDetails/workloadDetails.constants.ts @@ -0,0 +1,58 @@ +import { Name, WorkloadDetails } from './workloadDetails.interfaces'; + +export const name: Readonly<Name> = 'workloadDetails'; + +export const initialState: WorkloadDetails = { + prompts: { + samples: [], + tokenDistributions: { + statistics: { + total: 0, + mean: 0, + std: 0, + median: 0, + min: 0, + max: 0, + }, + percentiles: [], + buckets: [], + bucketWidth: 0, + }, + }, + generations: { + samples: [], + tokenDistributions: { + statistics: { + total: 0, + mean: 0, + std: 0, + median: 0, + min: 0, + max: 0, + }, + percentiles: [], + buckets: [], + bucketWidth: 0, + }, + }, + requestsOverTime: { + numBenchmarks: 0, + requestsOverTime: { + statistics: { + total: 0, + mean: 0, + std: 0, + median: 0, + min: 0, + max: 0, + }, + percentiles: [], + buckets: [], + bucketWidth: 0, + }, + }, + rateType: '', + server: { + target: '', + }, +}; diff --git a/src/ui/lib/store/slices/workloadDetails/workloadDetails.interfaces.ts b/src/ui/lib/store/slices/workloadDetails/workloadDetails.interfaces.ts new file mode 100644 index 00000000..2aa7619f --- /dev/null +++ b/src/ui/lib/store/slices/workloadDetails/workloadDetails.interfaces.ts @@ -0,0 +1,49 @@ +export type Name = 'workloadDetails'; + +interface Statistics { + total: number; + mean: number; + std: number; + median: number; + min: number; + max: number; +} + +interface Percentile { + percentile: string; + value: number; +} + +interface Bucket { + value: number; + count: number; +} + +interface Distribution { + statistics: Statistics; + percentiles: Percentile[]; + buckets: Bucket[]; + bucketWidth: number; +} + +interface TokenData { + samples: string[]; + tokenDistributions: Distribution; +} + +interface BenchmarkData { + numBenchmarks: number; + requestsOverTime: Distribution; +} + +interface Server { + target: string; +} + +export interface WorkloadDetails { + prompts: TokenData; + generations: TokenData; + requestsOverTime: BenchmarkData; + rateType: string; + server: Server; +} diff --git a/src/ui/lib/store/slices/workloadDetails/workloadDetails.selectors.ts b/src/ui/lib/store/slices/workloadDetails/workloadDetails.selectors.ts new file mode 100644 index 00000000..a3fde606 --- /dev/null +++ b/src/ui/lib/store/slices/workloadDetails/workloadDetails.selectors.ts @@ -0,0 +1,79 @@ +import { createSelector } from '@reduxjs/toolkit'; + +import { formatNumber } from '../../../utils/helpers'; +import { RootState } from '../../index'; + +export const selectWorkloadDetails = (state: RootState) => state.workloadDetails.data; + +export const selectPromptsHistogramBarData = createSelector( + [selectWorkloadDetails], + (workloadDetails) => { + return workloadDetails?.prompts?.tokenDistributions.buckets.map((bucket) => ({ + x: formatNumber(bucket.value), + y: bucket.count, + })); + } +); + +export const selectGenerationsHistogramBarData = createSelector( + [selectWorkloadDetails], + (workloadDetails) => { + return workloadDetails?.generations?.tokenDistributions.buckets.map((bucket) => ({ + x: formatNumber(bucket.value), + y: bucket.count, + })); + } +); + +export const selectPromptsHistogramLineData = createSelector( + [selectWorkloadDetails], + (workloadDetails) => [ + { + x: formatNumber( + workloadDetails?.prompts?.tokenDistributions.statistics.mean ?? 0 + ), + y: 35, + id: 'mean', + }, + { + x: formatNumber( + workloadDetails?.prompts?.tokenDistributions.statistics.median ?? 0 + ), + y: 35, + id: 'median', + }, + ] +); + +export const selectGenerationsHistogramLineData = createSelector( + [selectWorkloadDetails], + (workloadDetails) => [ + { + x: formatNumber( + workloadDetails?.generations?.tokenDistributions.statistics.mean ?? 0 + ), + y: 35, + id: 'mean', + }, + { + x: formatNumber( + workloadDetails?.generations?.tokenDistributions.statistics.median ?? 0 + ), + y: 35, + id: 'median', + }, + ] +); + +export const selectRequestOverTimeBarData = createSelector( + [selectWorkloadDetails], + (workloadDetails) => { + const requestObjs = workloadDetails?.requestsOverTime?.requestsOverTime; + return { + barChartData: requestObjs?.buckets?.map((bucket) => ({ + x: formatNumber(bucket.value), + y: bucket.count, + })), + }; + } +); diff --git a/src/ui/lib/store/slices/workloadDetails/workloadDetails.slice.ts b/src/ui/lib/store/slices/workloadDetails/workloadDetails.slice.ts new file mode 100644 index 00000000..7d05a0d7 --- /dev/null +++ b/src/ui/lib/store/slices/workloadDetails/workloadDetails.slice.ts @@ -0,0 +1,34 @@ +import { createSlice, PayloadAction } from '@reduxjs/toolkit'; + +import { workloadDetailsApi } from './workloadDetails.api'; +import * as Constants from './workloadDetails.constants'; +import { WorkloadDetails } from './workloadDetails.interfaces'; + +interface WorkloadDetailsState { + data: WorkloadDetails | null; +} + +const initialState: WorkloadDetailsState = { + data: null, +}; + +const workloadDetailsSlice = createSlice({ + name: Constants.name, + initialState, + reducers: { + setWorkloadDetails: (state, action: PayloadAction<WorkloadDetails>) => { + state.data = action.payload; + }, + }, + extraReducers: (builder) => { + builder.addMatcher( + workloadDetailsApi.endpoints.getWorkloadDetails.matchFulfilled, + (state, action) => { + state.data = action.payload; + } + ); + }, +}); + +export const { setWorkloadDetails } = workloadDetailsSlice.actions; +export default workloadDetailsSlice.reducer; diff --git a/src/ui/lib/store/workloadDetailsWindowData.ts b/src/ui/lib/store/workloadDetailsWindowData.ts new file mode 100644 index 00000000..8913f5c2 --- /dev/null +++ b/src/ui/lib/store/workloadDetailsWindowData.ts @@ -0,0 +1,167 @@ +export const workloadDetailsScript = `window.workload_details = { + "prompts": { + "samples": [ + "such a sacrifice to her advantage as years of gratitude cannot enough acknowledge. By this time she is actually with them! If such goodness does not make her miserable now, she will never deserve to be happy! What a meeting for her, when she first sees my aunt! We must endeavour to forget all that has passed on either side, said Jane I hope and trust they will yet be happy. His consenting to marry her is a proof, I will believe, that he is come to a right way of thinking. Their mutual affection will steady them; and I flatter myself they will settle so quietly, and live in so rational a manner", + "a reconciliation; and, after a little further resistance on the part of his aunt, her resentment gave way, either to her affection for him, or her curiosity to see how his wife conducted herself; and she condescended to wait on them at Pemberley, in spite of that pollution which its woods had received, not merely from the presence of such a mistress, but the visits of her uncle and aunt from the city. With the Gardiners they were always on the most intimate terms. Darcy, as well as Elizabeth, really loved them; and they were both ever sensible of the warmest gratitude towards the persons who,", + "struck her, that _she_ was selected from among her sisters as worthy of being the mistress of Hunsford Parsonage, and of assisting to form a quadrille table at Rosings, in the absence of more eligible visitors. The idea soon reached to conviction, as she observed his increasing civilities towards herself, and heard his frequent attempt at a compliment on her wit and vivacity; and though more astonished than gratified herself by this effect of her charms, it was not long before her mother gave her to understand that the probability of their marriage was exceedingly agreeable to _her_. Elizabeth, however, did not choose", + "were comfortable on this subject. Day after day passed away without bringing any other tidings of him than the report which shortly prevailed in Meryton of his coming no more to Netherfield the whole winter; a report which highly incensed Mrs. Bennet, and which she never failed to contradict as a most scandalous falsehood. Even Elizabeth began to fear not that Bingley was indifferent but that his sisters would be successful in keeping him away. Unwilling as she was to admit an idea so destructive to Jane s happiness, and so dishonourable to the stability of her lover, she could not prevent its frequently recurring", + "? cried Elizabeth, brightening up for a moment. Upon my word, said Mrs. Gardiner, I begin to be of your uncle s opinion. It is really too great a violation of decency, honour, and interest, for him to be guilty of it. I cannot think so very ill of Wickham. Can you, yourself, Lizzie, so wholly give him up, as to believe him capable of it? Not perhaps of neglecting his own interest. But of every other neglect I can believe him capable. If, indeed, it should be so! But I dare not hope it. Why should they not go on" + ], + "tokenDistributions": { + "statistics": { + "mean": 128.07115246019785 + }, + "buckets": [ + { + "value": 128, + "count": 14389 + }, + { + "value": 130, + "count": 182 + }, + { + "value": 129, + "count": 677 + }, + { + "value": 131, + "count": 15 + } + ] + } + }, + "generations": { + "samples": [ + ", that his relations could not choose but be struck with their pleasing and advantageous change. Ten years of reproofs, threats, and chastisements, may not have given them all they ought to know; but surely evidence and conviction will soon do the rest, thanks to the happy truth-so-discovered is Bess", + " for her sake, had generously given up the one interest, and risked the other, the only real friends whom she could ever be able to acknowledge. Miss Gardiner had attended her nephew, the season before, and it was some consolation to Elizabeth to see his sisters again at Pemberley. Henry, of course", + " to attend to any reasoning. He was already so far her friend, and the habit of their acquaintance, attended as it was by a new share of regard, could produce but good offices among them, and we must take our comfort as well as we may. This comfort he soon united with, in possessesing her esteem", + " to her by the conduct of her mother and sisters. Charlotte was ins el amorous, silly, and headstrong; but he is, or seems to be, really in love with her. El , The rury waning of his best songs, and no man over 50", + ", with a certain tolerable income, in the genteel style they had known before? Why should not you, your father, and their cousins, protect and assist them, if you could prevail on your uncle to indemnify them sufficiently? -- But to deal out money recklessly, is indeed disgraceful. Had he" + ], + "tokenDistributions": { + "statistics": { + "mean": 63.951778811504944 + }, + "buckets": [ + { + "value": 64, + "count": 14618 + }, + { + "value": 62, + "count": 147 + }, + { + "value": 63, + "count": 431 + }, + { + "value": 61, + "count": 19 + }, + { + "value": 65, + "count": 40 + }, + { + "value": 66, + "count": 4 + }, + { + "value": 67, + "count": 2 + }, + { + "value": 60, + "count": 2 + } + ] + } + }, + "requestsOverTime": { + "numBenchmarks": 10, + "requestsOverTime": { + "statistic": {}, + "percentiles": [], + "buckets": [ + { + "value": 0.12647485733032227, + "count": 46 + }, + { + "value": 68.87534944216411, + "count": 831 + }, + { + "value": 137.6242240269979, + "count": 2076 + }, + { + "value": 206.37309861183167, + "count": 629 + }, + { + "value": 275.12197319666546, + "count": 282 + }, + { + "value": 343.87084778149926, + "count": 397 + }, + { + "value": 412.619722366333, + "count": 517 + }, + { + "value": 481.3685969511668, + "count": 669 + }, + { + "value": 550.1174715360006, + "count": 732 + }, + { + "value": 618.8663461208344, + "count": 958 + }, + { + "value": 687.6152207056682, + "count": 962 + }, + { + "value": 756.364095290502, + "count": 1224 + }, + { + "value": 825.1129698753357, + "count": 1197 + }, + { + "value": 893.8618444601696, + "count": 1458 + }, + { + "value": 962.6107190450033, + "count": 1435 + }, + { + "value": 1031.359593629837, + "count": 1695 + }, + { + "value": 1100.108468214671, + "count": 1640 + }, + { + "value": 1168.8573427995047, + "count": 1931 + } + ] + } + }, + "rateType": "sweep", + "server": { + "target": "http://192.168.4.13:8000" + } +};`; diff --git a/src/ui/lib/utils/helpers.ts b/src/ui/lib/utils/helpers.ts new file mode 100644 index 00000000..e96d2ea6 --- /dev/null +++ b/src/ui/lib/utils/helpers.ts @@ -0,0 +1,68 @@ +import { filesize } from 'filesize'; + +export const formatValue = (value: number, fractionDigits = 2) => + `$ ${value.toFixed(fractionDigits)}`; + +export const ceil = (number: number, precision = 0) => { + const n = number * Math.pow(10, precision); + return Math.ceil(n) / Math.pow(10, precision); +}; + +export const floor = (number: number, precision = 0) => { + const n = number * Math.pow(10, precision); + return Math.floor(n) / Math.pow(10, precision); +}; + +export const formatNumber = (number: number, precision = 2) => + Number(number.toFixed(precision)); + +export const parseUrlParts = (urlString: string) => { + try { + const url = new URL(urlString); + return { + type: url.protocol.replace(':', ''), + target: url.hostname, + port: url.port || '', + path: url.pathname, + }; + } catch (_) { + return { + type: '', + target: '', + port: '', + path: '', + }; + } +}; + +export const getFileSize = ( + size?: string | number | null, + roundDecimal?: number, + bits?: boolean +): { size: string; units: string } | undefined => { + if (size) { + const round = roundDecimal === 0 ? 0 : roundDecimal || 1; + const fileSize = `${filesize(size, { round, bits })}`.split(' '); + + return { + size: fileSize[0], + units: fileSize[1].toUpperCase(), + }; + } +}; + +export const formateDate = (timestamp: string) => { + const date = new Date(Number(timestamp) * 1000); + + const options: Intl.DateTimeFormatOptions = { + year: 'numeric', + month: 'long', + day: 'numeric', + hour: '2-digit', + minute: '2-digit', + second: '2-digit', + hour12: false, + }; + + return date.toLocaleString('en-US', options).replace(',', ''); +}; diff --git a/src/ui/lib/utils/interpolation.ts b/src/ui/lib/utils/interpolation.ts new file mode 100644 index 00000000..50e559a0 --- /dev/null +++ b/src/ui/lib/utils/interpolation.ts @@ -0,0 +1,67 @@ +export function createMonotoneSpline(xs: number[], ys: number[]) { + const n = xs.length; + if (n < 2) { + throw new Error('Need at least two points'); + } + const dx = new Array<number>(n - 1); + const dy = new Array<number>(n - 1); + const m = new Array<number>(n - 1); + const c1 = new Array<number>(n); + + for (let i = 0; i < n - 1; i++) { + dx[i] = xs[i + 1] - xs[i]; + if (dx[i] === 0) { + throw new Error(`xs[${i}] == xs[${i + 1}]`); + } + dy[i] = ys[i + 1] - ys[i]; + m[i] = dy[i] / dx[i]; + } + + c1[0] = m[0]; + for (let i = 1; i < n - 1; i++) { + if (m[i - 1] * m[i] <= 0) { + c1[i] = 0; + } else { + const dx1 = dx[i - 1], + dx2 = dx[i], + common = dx1 + dx2; + c1[i] = (3 * common) / ((common + dx2) / m[i - 1] + (common + dx1) / m[i]); + } + } + c1[n - 1] = m[n - 2]; + + return function (x: number) { + // Binary search for interval i: xs[i] <= x < xs[i+1] + let lo = 0, + hi = n - 2; + while (lo <= hi) { + const mid = (lo + hi) >> 1; + if (x < xs[mid]) { + hi = mid - 1; + } else if (x > xs[mid + 1]) { + lo = mid + 1; + } else { + lo = mid; + break; + } + } + let i = lo; + if (i < 0) { + i = 0; + } else if (i > n - 2) { + i = n - 2; + } + + const h = dx[i]; + const t = (x - xs[i]) / h; + const t2 = t * t, + t3 = t2 * t; + + const h00 = 2 * t3 - 3 * t2 + 1; + const h10 = t3 - 2 * t2 + t; + const h01 = -2 * t3 + 3 * t2; + const h11 = t3 - t2; + + return h00 * ys[i] + h10 * h * c1[i] + h01 * ys[i + 1] + h11 * h * c1[i + 1]; + }; +} diff --git a/src/ui/next.config.ts b/src/ui/next.config.ts index 0e244f5b..a9b9d123 100644 --- a/src/ui/next.config.ts +++ b/src/ui/next.config.ts @@ -1,6 +1,7 @@ import type { NextConfig } from 'next'; const nextConfig: NextConfig = { + images: { unoptimized: true }, output: 'export', webpack(config) { // Grab the existing rule that handles SVG imports diff --git a/src/ui/public/android-chrome-192x192.png b/src/ui/public/android-chrome-192x192.png index 6694fc06..60cb9c65 100644 Binary files a/src/ui/public/android-chrome-192x192.png and b/src/ui/public/android-chrome-192x192.png differ diff --git a/src/ui/public/apple-touch-icon.png b/src/ui/public/apple-touch-icon.png index 06b38634..2e570c52 100644 Binary files a/src/ui/public/apple-touch-icon.png and b/src/ui/public/apple-touch-icon.png differ diff --git a/src/ui/public/favicon-16x16.png b/src/ui/public/favicon-16x16.png index 0573eede..3e7203d6 100644 Binary files a/src/ui/public/favicon-16x16.png and b/src/ui/public/favicon-16x16.png differ diff --git a/src/ui/public/favicon-192x192.png b/src/ui/public/favicon-192x192.png index 6694fc06..60cb9c65 100644 Binary files a/src/ui/public/favicon-192x192.png and b/src/ui/public/favicon-192x192.png differ diff --git a/src/ui/public/favicon-32x32.png b/src/ui/public/favicon-32x32.png index 155c0930..56e58bd7 100644 Binary files a/src/ui/public/favicon-32x32.png and b/src/ui/public/favicon-32x32.png differ diff --git a/src/ui/public/favicon.png b/src/ui/public/favicon.png new file mode 100644 index 00000000..2e570c52 Binary files /dev/null and b/src/ui/public/favicon.png differ diff --git a/src/ui/public/file.svg b/src/ui/public/file.svg deleted file mode 100644 index 16fe3d3a..00000000 --- a/src/ui/public/file.svg +++ /dev/null @@ -1 +0,0 @@ -<svg fill="none" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg"><path d="M14.5 13.5V5.41a1 1 0 0 0-.3-.7L9.8.29A1 1 0 0 0 9.08 0H1.5v13.5A2.5 2.5 0 0 0 4 16h8a2.5 2.5 0 0 0 2.5-2.5m-1.5 0v-7H8v-5H3v12a1 1 0 0 0 1 1h8a1 1 0 0 0 1-1M9.5 5V2.12L12.38 5zM5.13 5h-.62v1.25h2.12V5zm-.62 3h7.12v1.25H4.5zm.62 3h-.62v1.25h7.12V11z" clip-rule="evenodd" fill="#666" fill-rule="evenodd"/></svg> diff --git a/src/ui/public/globe.svg b/src/ui/public/globe.svg deleted file mode 100644 index c7215fe0..00000000 --- a/src/ui/public/globe.svg +++ /dev/null @@ -1 +0,0 @@ -<svg fill="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><g clip-path="url(#a)"><path fill-rule="evenodd" clip-rule="evenodd" d="M10.27 14.1a6.5 6.5 0 0 0 3.67-3.45q-1.24.21-2.7.34-.31 1.83-.97 3.1M8 16A8 8 0 1 0 8 0a8 8 0 0 0 0 16m.48-1.52a7 7 0 0 1-.96 0H7.5a4 4 0 0 1-.84-1.32q-.38-.89-.63-2.08a40 40 0 0 0 3.92 0q-.25 1.2-.63 2.08a4 4 0 0 1-.84 1.31zm2.94-4.76q1.66-.15 2.95-.43a7 7 0 0 0 0-2.58q-1.3-.27-2.95-.43a18 18 0 0 1 0 3.44m-1.27-3.54a17 17 0 0 1 0 3.64 39 39 0 0 1-4.3 0 17 17 0 0 1 0-3.64 39 39 0 0 1 4.3 0m1.1-1.17q1.45.13 2.69.34a6.5 6.5 0 0 0-3.67-3.44q.65 1.26.98 3.1M8.48 1.5l.01.02q.41.37.84 1.31.38.89.63 2.08a40 40 0 0 0-3.92 0q.25-1.2.63-2.08a4 4 0 0 1 .85-1.32 7 7 0 0 1 .96 0m-2.75.4a6.5 6.5 0 0 0-3.67 3.44 29 29 0 0 1 2.7-.34q.31-1.83.97-3.1M4.58 6.28q-1.66.16-2.95.43a7 7 0 0 0 0 2.58q1.3.27 2.95.43a18 18 0 0 1 0-3.44m.17 4.71q-1.45-.12-2.69-.34a6.5 6.5 0 0 0 3.67 3.44q-.65-1.27-.98-3.1" fill="#666"/></g><defs><clipPath id="a"><path fill="#fff" d="M0 0h16v16H0z"/></clipPath></defs></svg> diff --git a/src/ui/public/manifest.json b/src/ui/public/manifest.json index 3f06b147..89605c2d 100644 --- a/src/ui/public/manifest.json +++ b/src/ui/public/manifest.json @@ -3,19 +3,14 @@ "name": "guidellm-ui", "icons": [ { - "src": "favicon.ico", - "sizes": "64x64 32x32 24x24 16x16", - "type": "image/x-icon" + "src": "favicon.png", + "sizes": "119x119", + "type": "image/png" }, { "src": "favicon-192x192.png", "type": "image/png", "sizes": "192x192" - }, - { - "src": "favicon-512x512.png", - "type": "image/png", - "sizes": "512x512" } ], "start_url": ".", diff --git a/src/ui/public/next.svg b/src/ui/public/next.svg deleted file mode 100644 index 5bb00d40..00000000 --- a/src/ui/public/next.svg +++ /dev/null @@ -1 +0,0 @@ -<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 394 80"><path fill="#000" d="M262 0h68.5v12.7h-27.2v66.6h-13.6V12.7H262V0ZM149 0v12.7H94v20.4h44.3v12.6H94v21h55v12.6H80.5V0h68.7zm34.3 0h-17.8l63.8 79.4h17.9l-32-39.7 32-39.6h-17.9l-23 28.6-23-28.6zm18.3 56.7-9-11-27.1 33.7h17.8l18.3-22.7z"/><path fill="#000" d="M81 79.3 17 0H0v79.3h13.6V17l50.2 62.3H81Zm252.6-.4c-1 0-1.8-.4-2.5-1s-1.1-1.6-1.1-2.6.3-1.8 1-2.5 1.6-1 2.6-1 1.8.3 2.5 1a3.4 3.4 0 0 1 .6 4.3 3.7 3.7 0 0 1-3 1.8zm23.2-33.5h6v23.3c0 2.1-.4 4-1.3 5.5a9.1 9.1 0 0 1-3.8 3.5c-1.6.8-3.5 1.3-5.7 1.3-2 0-3.7-.4-5.3-1s-2.8-1.8-3.7-3.2c-.9-1.3-1.4-3-1.4-5h6c.1.8.3 1.6.7 2.2s1 1.2 1.6 1.5c.7.4 1.5.5 2.4.5 1 0 1.8-.2 2.4-.6a4 4 0 0 0 1.6-1.8c.3-.8.5-1.8.5-3V45.5zm30.9 9.1a4.4 4.4 0 0 0-2-3.3 7.5 7.5 0 0 0-4.3-1.1c-1.3 0-2.4.2-3.3.5-.9.4-1.6 1-2 1.6a3.5 3.5 0 0 0-.3 4c.3.5.7.9 1.3 1.2l1.8 1 2 .5 3.2.8c1.3.3 2.5.7 3.7 1.2a13 13 0 0 1 3.2 1.8 8.1 8.1 0 0 1 3 6.5c0 2-.5 3.7-1.5 5.1a10 10 0 0 1-4.4 3.5c-1.8.8-4.1 1.2-6.8 1.2-2.6 0-4.9-.4-6.8-1.2-2-.8-3.4-2-4.5-3.5a10 10 0 0 1-1.7-5.6h6a5 5 0 0 0 3.5 4.6c1 .4 2.2.6 3.4.6 1.3 0 2.5-.2 3.5-.6 1-.4 1.8-1 2.4-1.7a4 4 0 0 0 .8-2.4c0-.9-.2-1.6-.7-2.2a11 11 0 0 0-2.1-1.4l-3.2-1-3.8-1c-2.8-.7-5-1.7-6.6-3.2a7.2 7.2 0 0 1-2.4-5.7 8 8 0 0 1 1.7-5 10 10 0 0 1 4.3-3.5c2-.8 4-1.2 6.4-1.2 2.3 0 4.4.4 6.2 1.2 1.8.8 3.2 2 4.3 3.4 1 1.4 1.5 3 1.5 5h-5.8z"/></svg> diff --git a/src/ui/public/vercel.svg b/src/ui/public/vercel.svg deleted file mode 100644 index 52151572..00000000 --- a/src/ui/public/vercel.svg +++ /dev/null @@ -1 +0,0 @@ -<svg fill="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1155 1000"><path d="m577.3 0 577.4 1000H0z" fill="#fff"/></svg> diff --git a/src/ui/public/window.svg b/src/ui/public/window.svg deleted file mode 100644 index d05e7a1b..00000000 --- a/src/ui/public/window.svg +++ /dev/null @@ -1 +0,0 @@ -<svg fill="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><path fill-rule="evenodd" clip-rule="evenodd" d="M1.5 2.5h13v10a1 1 0 0 1-1 1h-11a1 1 0 0 1-1-1zM0 1h16v11.5a2.5 2.5 0 0 1-2.5 2.5h-11A2.5 2.5 0 0 1 0 12.5zm3.75 4.5a.75.75 0 1 0 0-1.5.75.75 0 0 0 0 1.5M7 4.75a.75.75 0 1 1-1.5 0 .75.75 0 0 1 1.5 0m1.75.75a.75.75 0 1 0 0-1.5.75.75 0 0 0 0 1.5" fill="#666"/></svg> diff --git a/src/ui/tsconfig.json b/src/ui/tsconfig.json index 25ccf9e8..172c959e 100644 --- a/src/ui/tsconfig.json +++ b/src/ui/tsconfig.json @@ -1,10 +1,15 @@ { "extends": "../../tsconfig.base.json", "compilerOptions": { - /* only runtime types here */ - "types": ["node"], "allowJs": false, "isolatedModules": true }, - "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts", "app"] + "include": [ + "next-env.d.ts", + "**/*.ts", + "**/*.tsx", + ".next/types/**/*.ts", + "app", + "./app/types/images.d.ts" + ] } diff --git a/src/ui/types/declaration.d.ts b/src/ui/types/declaration.d.ts new file mode 100644 index 00000000..6a070e24 --- /dev/null +++ b/src/ui/types/declaration.d.ts @@ -0,0 +1,11 @@ +import { Benchmarks } from './src/lib/store/slices/benchmarks/benchmarks.interfaces'; +import { RunInfo } from './src/lib/store/slices/runInfo/runInfo.interfaces'; +import { WorkloadDetails } from './src/lib/store/slices/workloadDetails/workloadDetails.interfaces'; + +declare global { + interface Window { + run_info?: RunInfo; + workload_details?: WorkloadDetails; + benchmarks?: Benchmarks; + } +} diff --git a/tests/ui/__mocks__/@nivo/bar.tsx b/tests/ui/__mocks__/@nivo/bar.tsx new file mode 100644 index 00000000..a99b6dec --- /dev/null +++ b/tests/ui/__mocks__/@nivo/bar.tsx @@ -0,0 +1,2 @@ +export const ResponsiveBar = () => null; +export const BarCustomLayerProps = () => null; diff --git a/tests/ui/__mocks__/@nivo/core.tsx b/tests/ui/__mocks__/@nivo/core.tsx new file mode 100644 index 00000000..66e2da8a --- /dev/null +++ b/tests/ui/__mocks__/@nivo/core.tsx @@ -0,0 +1 @@ +export const Point = () => null; diff --git a/tests/ui/__mocks__/@nivo/line.tsx b/tests/ui/__mocks__/@nivo/line.tsx new file mode 100644 index 00000000..bf985ef3 --- /dev/null +++ b/tests/ui/__mocks__/@nivo/line.tsx @@ -0,0 +1 @@ +export const ResponsiveLine = () => null; diff --git a/tests/ui/integration/page.test.tsx b/tests/ui/integration/page.test.tsx index caa2aab4..3e765a94 100644 --- a/tests/ui/integration/page.test.tsx +++ b/tests/ui/integration/page.test.tsx @@ -1,10 +1,41 @@ -import { render } from '@testing-library/react'; +import { render, waitFor } from '@testing-library/react'; import Home from '@/app/page'; +import mockBenchmarks from '../unit/mocks/mockBenchmarks'; + +const jsonResponse = (data: unknown, status = 200) => + Promise.resolve( + new Response(JSON.stringify(data), { + status, + headers: { 'Content-Type': 'application/json' }, + }) + ); + +const route = (input: RequestInfo) => { + const url = typeof input === 'string' ? input : input.url; + + if (url.endsWith('/run-info')) return jsonResponse({}); + if (url.endsWith('/workload-details')) return jsonResponse({}); + if (url.endsWith('/benchmarks')) + return jsonResponse({ + benchmarks: mockBenchmarks, + }); + + /* fall-through → 404 */ + return { ok: false, status: 404, json: () => Promise.resolve({}) }; +}; + +beforeEach(() => { + jest.resetAllMocks(); + (global.fetch as jest.Mock).mockImplementation(route); +}); + describe('Home Page', () => { - it('renders the homepage content', () => { + it('renders the homepage content', async () => { const { getByText } = render(<Home />); - expect(getByText('GuideLLM')).toBeInTheDocument(); + await waitFor(() => { + expect(getByText('GuideLLM')).toBeInTheDocument(); + }); }); }); diff --git a/tests/ui/test.helper.tsx b/tests/ui/test.helper.tsx new file mode 100644 index 00000000..7d93b4e7 --- /dev/null +++ b/tests/ui/test.helper.tsx @@ -0,0 +1,17 @@ +import { ThemeProvider } from '@mui/material/styles'; +import React, { ReactNode } from 'react'; + +import { muiThemeV3Dark } from '@/app/theme'; +import { ReduxProvider } from '@/lib/store/provider'; + +interface TestProvidersProps { + children: ReactNode; +} + +export const MockedWrapper = ({ children }: TestProvidersProps) => { + return ( + <ReduxProvider> + <ThemeProvider theme={muiThemeV3Dark}>{children}</ThemeProvider> + </ReduxProvider> + ); +}; diff --git a/tests/ui/unit/components/Charts/DashedLine/helpers.test.ts b/tests/ui/unit/components/Charts/DashedLine/helpers.test.ts new file mode 100644 index 00000000..e8a75732 --- /dev/null +++ b/tests/ui/unit/components/Charts/DashedLine/helpers.test.ts @@ -0,0 +1,86 @@ +import { + roundNearestNice, + roundUpNice, + spacedLogValues, +} from '@/lib/components/Charts/DashedLine/helpers'; + +describe('roundNearestNice', () => { + it('rounds to a nearby nice number', () => { + expect([10, 12]).toContain(roundNearestNice(11)); + expect([25, 30]).toContain(roundNearestNice(27)); + expect([50]).toContain(roundNearestNice(49)); + expect([75, 80, 85]).toContain(roundNearestNice(81)); + expect([800]).toContain(roundNearestNice(810)); + expect([1300, 1400, 1500]).toContain(roundNearestNice(1342)); + }); + it("doesn't round some nice numbers", () => { + expect(roundNearestNice(15)).toBe(15); + expect(roundNearestNice(20)).toBe(20); + expect(roundNearestNice(30)).toBe(30); + expect(roundNearestNice(40)).toBe(40); + expect(roundNearestNice(75)).toBe(75); + expect(roundNearestNice(100)).toBe(100); + expect(roundNearestNice(150)).toBe(150); + expect(roundNearestNice(200)).toBe(200); + expect(roundNearestNice(400)).toBe(400); + expect(roundNearestNice(1000)).toBe(1000); + expect(roundNearestNice(1200)).toBe(1200); + }); +}); + +describe('roundUpNice', () => { + it('rounds up to a nearby nice number', () => { + expect([12, 15]).toContain(roundUpNice(11)); + expect([30]).toContain(roundUpNice(27)); + expect([50]).toContain(roundUpNice(49)); + expect([80, 85, 90]).toContain(roundUpNice(79)); + expect([85, 90]).toContain(roundUpNice(81)); + expect([850, 900, 1000]).toContain(roundUpNice(810)); + expect([1350, 1400, 1500]).toContain(roundUpNice(1342)); + }); + it("doesn't round some nice numbers", () => { + expect(roundUpNice(15)).toBe(15); + expect(roundUpNice(20)).toBe(20); + expect(roundUpNice(30)).toBe(30); + expect(roundUpNice(40)).toBe(40); + expect(roundUpNice(75)).toBe(75); + expect(roundUpNice(100)).toBe(100); + expect(roundUpNice(150)).toBe(150); + expect(roundUpNice(200)).toBe(200); + expect(roundUpNice(400)).toBe(400); + expect(roundUpNice(1000)).toBe(1000); + expect(roundUpNice(1200)).toBe(1200); + }); +}); + +describe('spacedLogValues', () => { + const checkValuesRoughlyLogSpaced = (values: number[]) => { + const valuesRatios = []; + for (let i = 1; i < values.length; i++) { + valuesRatios.push(values[i] / values[i - 1]); + } + const valuesRatiosAvg = valuesRatios.reduce((a, b) => a + b) / valuesRatios.length; + valuesRatios.forEach((ratio) => { + expect(ratio).toBeCloseTo(valuesRatiosAvg, -0.5); + }); + }; + + it('generates an array of roughly log-scale spaced values', () => { + expect(spacedLogValues(1, 1000, 4)).toEqual([1, 10, 100, 1000]); + checkValuesRoughlyLogSpaced(spacedLogValues(1, 1324, 4)); + checkValuesRoughlyLogSpaced(spacedLogValues(123, 12324, 6)); + checkValuesRoughlyLogSpaced(spacedLogValues(1, 122, 6)); + checkValuesRoughlyLogSpaced(spacedLogValues(1, 122, 9)); + }); + it('generates an array of nice round numbers', () => { + for (const value of spacedLogValues(1, 1000, 4)) { + expect([roundUpNice(value), roundNearestNice(value)]).toContain(value); + } + for (const value of spacedLogValues(1, 1324, 4)) { + expect([roundUpNice(value), roundNearestNice(value)]).toContain(value); + } + for (const value of spacedLogValues(1, 132, 7)) { + expect([roundUpNice(value), roundNearestNice(value)]).toContain(value); + } + }); +}); diff --git a/tests/ui/unit/layout.test.tsx b/tests/ui/unit/layout.test.tsx deleted file mode 100644 index f2a85d28..00000000 --- a/tests/ui/unit/layout.test.tsx +++ /dev/null @@ -1,15 +0,0 @@ -import { render } from '@testing-library/react'; - -import RootLayout from '@/app/layout'; - -describe('RootLayout', () => { - it('renders children inside the layout', () => { - const { getByText } = render( - <RootLayout> - <p>Test Child</p> - </RootLayout> - ); - - expect(getByText('Test Child')).toBeInTheDocument(); - }); -}); diff --git a/tests/ui/unit/mocks/mockBenchmarks.ts b/tests/ui/unit/mocks/mockBenchmarks.ts new file mode 100644 index 00000000..29b9232e --- /dev/null +++ b/tests/ui/unit/mocks/mockBenchmarks.ts @@ -0,0 +1,232 @@ +export default [ + { + requestsPerSecond: 0.6668550387660497, + tpot: { + statistics: { + total: 80, + mean: 23.00635663936911, + median: 22.959455611213805, + min: 22.880917503720237, + max: 24.14080301920573, + std: 0.18918760384209338, + }, + percentiles: [ + { + percentile: 'p50', + value: 22.959455611213805, + }, + { + percentile: 'p90', + value: 23.01789086962503, + }, + { + percentile: 'p95', + value: 23.30297423947242, + }, + { + percentile: 'p99', + value: 24.14080301920573, + }, + ], + }, + ttft: { + statistics: { + total: 80, + mean: 49.64659512042999, + median: 49.23129081726074, + min: 44.538259506225586, + max: 55.47308921813965, + std: 1.7735485090634995, + }, + percentiles: [ + { + percentile: 'p50', + value: 49.23129081726074, + }, + { + percentile: 'p90', + value: 50.16160011291504, + }, + { + percentile: 'p95', + value: 54.918766021728516, + }, + { + percentile: 'p99', + value: 55.47308921813965, + }, + ], + }, + throughput: { + statistics: { + total: 210, + mean: 42.58702991319684, + median: 43.536023084668, + min: 0.0, + max: 43.68247620237872, + std: 4.559764488536857, + }, + percentiles: [ + { + percentile: 'p50', + value: 43.536023084668, + }, + { + percentile: 'p90', + value: 43.62613633999709, + }, + { + percentile: 'p95', + value: 43.64020767654067, + }, + { + percentile: 'p99', + value: 43.68202126662431, + }, + ], + }, + timePerRequest: { + statistics: { + total: 80, + mean: 1496.706646680832, + median: 1496.1087703704834, + min: 1490.584135055542, + max: 1505.8784484863281, + std: 3.4553340533022667, + }, + percentiles: [ + { + percentile: 'p50', + value: 1496.1087703704834, + }, + { + percentile: 'p90', + value: 1500.9305477142334, + }, + { + percentile: 'p95', + value: 1505.3200721740723, + }, + { + percentile: 'p99', + value: 1505.8784484863281, + }, + ], + }, + }, + { + requestsPerSecond: 28.075330129628725, + tpot: { + statistics: { + total: 3416, + mean: 126.08707076148656, + median: 125.30853256346687, + min: 23.034303907364134, + max: 138.08223756693178, + std: 3.508992115582193, + }, + percentiles: [ + { + percentile: 'p50', + value: 125.30853256346687, + }, + { + percentile: 'p90', + value: 129.21135009281218, + }, + { + percentile: 'p95', + value: 129.52291770059554, + }, + { + percentile: 'p99', + value: 132.21229490686636, + }, + ], + }, + ttft: { + statistics: { + total: 3416, + mean: 8585.486161415694, + median: 8965.316534042358, + min: 110.53991317749023, + max: 12575.379610061646, + std: 1929.5632525234505, + }, + percentiles: [ + { + percentile: 'p50', + value: 8965.316534042358, + }, + { + percentile: 'p90', + value: 9231.79316520691, + }, + { + percentile: 'p95', + value: 9485.00108718872, + }, + { + percentile: 'p99', + value: 12096.465587615967, + }, + ], + }, + throughput: { + statistics: { + total: 15981, + mean: 1795.4403743554367, + median: 670.1236619268253, + min: 0.0, + max: 838860.8, + std: 5196.545581836957, + }, + percentiles: [ + { + percentile: 'p50', + value: 670.1236619268253, + }, + { + percentile: 'p90', + value: 4068.1901066925316, + }, + { + percentile: 'p95', + value: 6374.322188449848, + }, + { + percentile: 'p99', + value: 16194.223938223939, + }, + ], + }, + timePerRequest: { + statistics: { + total: 3416, + mean: 16526.811318389147, + median: 17058.441638946533, + min: 1711.3444805145264, + max: 20646.55351638794, + std: 2054.9553770234484, + }, + percentiles: [ + { + percentile: 'p50', + value: 17058.441638946533, + }, + { + percentile: 'p90', + value: 17143.84412765503, + }, + { + percentile: 'p95', + value: 17248.060703277588, + }, + { + percentile: 'p99', + value: 20116.52660369873, + }, + ], + }, + }, +]; diff --git a/tests/ui/unit/store/slices/slo.test.tsx b/tests/ui/unit/store/slices/slo.test.tsx new file mode 100644 index 00000000..254d2818 --- /dev/null +++ b/tests/ui/unit/store/slices/slo.test.tsx @@ -0,0 +1,19 @@ +import { initialState } from '@/lib/store/slices/slo/slo.constants'; +import { SloState } from '@/lib/store/slices/slo/slo.interfaces'; +import sloReducer, { setSloData } from '@/lib/store/slices/slo/slo.slice'; + +test('should handle initial state', () => { + expect(sloReducer(undefined, { type: '' })).toEqual(initialState); +}); + +test('should set slo data', () => { + const slo = { + enforcedPercentile: 'p50', + } as Partial<SloState>; + + const fullSlos = { + ...initialState, + ...slo, + }; + expect(sloReducer(undefined, setSloData(slo))).toEqual(fullSlos); +}); diff --git a/tests/ui/unit/utils/interpolation.test.ts b/tests/ui/unit/utils/interpolation.test.ts new file mode 100644 index 00000000..d0173548 --- /dev/null +++ b/tests/ui/unit/utils/interpolation.test.ts @@ -0,0 +1,50 @@ +import { createMonotoneSpline } from '@/lib/utils/interpolation'; + +test('should reproduce points on a straight line', () => { + const xs = [0, 1, 2, 3]; + const ys = [0, 2, 4, 6]; + const interpolate = createMonotoneSpline(xs, ys); + + [0, 0.5, 1.5, 3].forEach((x) => { + expect(interpolate(x)).toBeCloseTo(2 * x, 1e-6); + }); +}); + +test('should return constant data on flat line', () => { + const xs = [0, 1, 2, 3]; + const ys = [5, 5, 5, 5]; + const interpolate = createMonotoneSpline(xs, ys); + + [0, 1.5, 2].forEach((x) => { + expect(interpolate(x)).toBeCloseTo(5, 1e-6); + }); +}); + +test('should hit each point precisely', () => { + const xs = [0, 2, 5]; + const ys = [1, 4, 2]; + const interpolate = createMonotoneSpline(xs, ys); + + xs.forEach((x, i) => { + expect(interpolate(x)).toBeCloseTo(ys[i], 1e-6); + }); +}); + +test('no local extremas added', () => { + // generate wavy line + const xs = Array.from(Array(50)).map((_, i) => (i + 1) / 10); + const ys = xs.map((x) => 1 + Math.sin((3 * Math.PI * x) / 10)); + // check that each interpolated point is between its two bounding points + const interpolate = createMonotoneSpline(xs, ys); + for (let i = xs[0]; i < xs[xs.length - 1]; i += 0.01) { + const upperIndex = xs.findIndex((x) => x >= i); + if (upperIndex === 0) { + expect(interpolate(i)).toBeCloseTo(ys[0]); + continue; + } + const lowerY = ys[upperIndex - 1]; + const upperY = ys[upperIndex]; + expect(interpolate(i)).toBeLessThanOrEqual(Math.max(lowerY, upperY)); + expect(interpolate(i)).toBeGreaterThanOrEqual(Math.min(lowerY, upperY)); + } +}); diff --git a/tsconfig.base.json b/tsconfig.base.json index 0169c3ba..409e7aba 100644 --- a/tsconfig.base.json +++ b/tsconfig.base.json @@ -1,15 +1,12 @@ { "$schema": "https://json.schemastore.org/tsconfig", "compilerOptions": { - /* --- language / emit --- */ "target": "ES2022", "lib": ["dom", "dom.iterable", "esnext"], "module": "esnext", "jsx": "preserve", "incremental": true, "noEmit": true, - - /* --- module resolution --- */ "moduleResolution": "node", "baseUrl": ".", "paths": { @@ -20,8 +17,6 @@ "esModuleInterop": true, "allowSyntheticDefaultImports": true, "plugins": [{ "name": "next" }], - - /* --- correctness / style --- */ "strict": true, "skipLibCheck": true, "forceConsistentCasingInFileNames": true, diff --git a/tsconfig.json b/tsconfig.json index 40711aeb..222ab61a 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,22 +1,13 @@ { "extends": "./tsconfig.base.json", - /* Extra options that apply only to Node-side files */ "compilerOptions": { - "types": ["node"], // Buffer, process, __dirname, etc. + "types": ["node"], "allowJs": false, "isolatedModules": true }, - /* Narrow include so we don’t compile the entire repo twice */ - "include": [ - "*.ts", - "*.cts", - "*.mts", // a couple of root-level scripts - "scripts/**/*", - "types/**/*" - ], + "include": ["*.ts", "*.cts", "*.mts", "scripts/**/*", "types/**/*"], - /* Don’t touch the Next.js source or tests from here */ "exclude": ["src/ui", "tests", "node_modules"] }