diff --git a/eslint.config.js b/eslint.config.js index 3dca070a..6640e919 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -1,9 +1,66 @@ import js from "@eslint/js"; import globals from "globals"; +import importPlugin from "eslint-plugin-import"; import reactHooks from "eslint-plugin-react-hooks"; import reactRefresh from "eslint-plugin-react-refresh"; import tseslint from "typescript-eslint"; import tailwindPlugin from "eslint-plugin-tailwindcss"; +import path from "path"; +import fs from "fs"; + +const FEATURES_DIR = "./src/features"; + +/** + * Traverse the features directory and return an array of restricted paths for + * use in the `import/no-restricted-paths` rule. + * + * @example + * ```js + * [ + * { + * except: [ './dependencies' ], + * from: './src/features', + * target: './src/features/dependencies' + * }, + * { + * except: [ './versions' ], + * from: './src/features', + * target: './src/features/versions' + * }, + * { + * except: [ './vulnerabilities' ], + * from: './src/features', + * target: './src/features/vulnerabilities' + * } + * ] + * ``` + */ +const getRestrictedPathsForFeatureDir = () => { + const featureDirPath = path.resolve(FEATURES_DIR); + /** + * @type {Array<{except: `./${string}`[], from: './src/features', target: string}>} + */ + const restrictedPaths = []; + + try { + const featureDirs = fs.readdirSync(featureDirPath); + + featureDirs.forEach((featureDir) => { + const subPath = path.join(featureDirPath, featureDir); + if (fs.lstatSync(subPath).isDirectory()) { + restrictedPaths.push({ + except: [`./${featureDir}`], + from: FEATURES_DIR, + target: path.join(FEATURES_DIR, featureDir), + }); + } + }); + } catch (error) { + console.error("Error reading features directory:", error); + } + + return restrictedPaths; +}; const restrictedSyntax = { reactQuery: { @@ -24,15 +81,29 @@ export default tseslint.config( ], files: ["**/*.{ts,tsx}"], languageOptions: { - ecmaVersion: 2020, - globals: globals.browser, + parserOptions: { + projectService: true, + tsconfigRootDir: import.meta.dirname, + ecmaFeatures: { + jsx: true, + }, + }, + globals: { + ...globals.browser, + ...globals.node, + }, }, plugins: { "react-hooks": reactHooks, "react-refresh": reactRefresh, + import: importPlugin, }, - settings: { + "import/resolver": { + typescript: true, + node: true, + }, + tailwindcss: { callees: ["tv", "twMerge"], config: "./tailwind.config.ts", @@ -132,6 +203,40 @@ export default tseslint.config( ], }, ], + "import/no-restricted-paths": [ + "error", + { + zones: [ + // disables cross-feature imports: + // eg. src/features/dashboard-alerts should not import from src/features/dashboard-messages, etc. + ...getRestrictedPathsForFeatureDir(), + + // enforce unidirectional codebase: + // e.g. src/routes can import from src/features but not the other way around + { + from: "./src/routes", + target: "./src/features", + }, + + // enforce unidirectional codebase: + // e.g src/features and src/routes can import from these shared modules but not the other way around + { + from: ["./src/features", "./src/routes"], + target: [ + "./src/components", + "./src/constants", + "./src/hooks", + "./src/i18n", + "./src/lib", + "./src/mocks", + "./src/trusty-api", + "./src/types", + "./src/utils", + ], + }, + ], + }, + ], }, - }, + } ); diff --git a/index.html b/index.html index 249c9553..1ba893ea 100644 --- a/index.html +++ b/index.html @@ -1,4 +1,4 @@ - + diff --git a/knip.json b/knip.json new file mode 100644 index 00000000..087830c2 --- /dev/null +++ b/knip.json @@ -0,0 +1,7 @@ +{ + "$schema": "https://unpkg.com/knip@5/schema.json", + "entry": ["src/main.tsx"], + "ignore": ["src/api/generated/**/*"], + "ignoreDependencies": ["husky"], + "project": ["src/**/*.{js,jsx,ts,tsx}"] +} diff --git a/knip.ts b/knip.ts deleted file mode 100644 index 72ef7672..00000000 --- a/knip.ts +++ /dev/null @@ -1,10 +0,0 @@ -import type { KnipConfig } from "knip"; - -const config: KnipConfig = { - entry: ["src/main.tsx"], - ignore: ["src/api/generated/**/*"], - ignoreDependencies: ["husky"], - project: ["src/**/*.{js,jsx,ts,tsx}"], -}; - -export default config; diff --git a/package-lock.json b/package-lock.json index 805f9f08..169b8e0c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -40,8 +40,7 @@ "tailwind-variants": "^0.3.0", "tailwindcss-animate": "^1.0.7", "ts-pattern": "^5.6.2", - "zod": "^3.24.1", - "zustand": "^5.0.3" + "zod": "^3.24.1" }, "devDependencies": { "@eslint/js": "^9.15.0", @@ -55,12 +54,15 @@ "@types/node": "^22.10.1", "@types/react": "^19.0.2", "@types/react-dom": "^19.0.2", + "@typescript-eslint/parser": "^8.23.0", "@vitejs/plugin-react-swc": "^3.5.0", - "@vitest/coverage-istanbul": "^2.1.8", - "@vitest/expect": "^3.0.4", - "@vitest/ui": "^2.1.4", + "@vitest/coverage-istanbul": "^3.0.5", + "@vitest/expect": "^3.0.5", + "@vitest/ui": "^3.0.5", "autoprefixer": "^10.4.20", "eslint": "^9.18.0", + "eslint-import-resolver-typescript": "^3.7.0", + "eslint-plugin-import": "^2.31.0", "eslint-plugin-react-hooks": "^5.1.0", "eslint-plugin-react-refresh": "^0.4.16", "eslint-plugin-tailwindcss": "3.17.5", @@ -77,7 +79,7 @@ "typescript-eslint": "^8.15.0", "vite": "^6.0.1", "vite-tsconfig-paths": "^5.1.4", - "vitest": "^3.0.4", + "vitest": "^3.0.5", "vitest-fail-on-console": "^0.7.1" }, "optionalDependencies": { @@ -1652,6 +1654,16 @@ "node": ">= 8" } }, + "node_modules/@nolyfill/is-core-module": { + "version": "1.0.39", + "resolved": "https://registry.npmjs.org/@nolyfill/is-core-module/-/is-core-module-1.0.39.tgz", + "integrity": "sha512-nn5ozdjYQpUCZlWGuxcJY/KpxkWQs4DcbMCmKojjyrYDEAGy4Ce19NN4v5MduafTwJlbKc99UA8YhSVqq9yPZA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.4.0" + } + }, "node_modules/@open-draft/deferred-promise": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/@open-draft/deferred-promise/-/deferred-promise-2.2.0.tgz", @@ -1731,25 +1743,25 @@ } }, "node_modules/@radix-ui/react-dialog": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/@radix-ui/react-dialog/-/react-dialog-1.1.4.tgz", - "integrity": "sha512-Ur7EV1IwQGCyaAuyDRiOLA5JIUZxELJljF+MbM/2NC0BYwfuRrbpS30BiQBJrVruscgUkieKkqXYDOoByaxIoA==", + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/@radix-ui/react-dialog/-/react-dialog-1.1.6.tgz", + "integrity": "sha512-/IVhJV5AceX620DUJ4uYVMymzsipdKBzo3edo+omeskCKGm9FRHM0ebIdbPnlQVJqyuHbuBltQUOG2mOTq2IYw==", "license": "MIT", "dependencies": { "@radix-ui/primitive": "1.1.1", "@radix-ui/react-compose-refs": "1.1.1", "@radix-ui/react-context": "1.1.1", - "@radix-ui/react-dismissable-layer": "1.1.3", + "@radix-ui/react-dismissable-layer": "1.1.5", "@radix-ui/react-focus-guards": "1.1.1", - "@radix-ui/react-focus-scope": "1.1.1", + "@radix-ui/react-focus-scope": "1.1.2", "@radix-ui/react-id": "1.1.0", - "@radix-ui/react-portal": "1.1.3", + "@radix-ui/react-portal": "1.1.4", "@radix-ui/react-presence": "1.1.2", - "@radix-ui/react-primitive": "2.0.1", - "@radix-ui/react-slot": "1.1.1", + "@radix-ui/react-primitive": "2.0.2", + "@radix-ui/react-slot": "1.1.2", "@radix-ui/react-use-controllable-state": "1.1.0", - "aria-hidden": "^1.1.1", - "react-remove-scroll": "^2.6.1" + "aria-hidden": "^1.2.4", + "react-remove-scroll": "^2.6.3" }, "peerDependencies": { "@types/react": "*", @@ -1767,14 +1779,14 @@ } }, "node_modules/@radix-ui/react-dismissable-layer": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.1.3.tgz", - "integrity": "sha512-onrWn/72lQoEucDmJnr8uczSNTujT0vJnA/X5+3AkChVPowr8n1yvIKIabhWyMQeMvvmdpsvcyDqx3X1LEXCPg==", + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.1.5.tgz", + "integrity": "sha512-E4TywXY6UsXNRhFrECa5HAvE5/4BFcGyfTyK36gP+pAW1ed7UTK4vKwdr53gAJYwqbfCWC6ATvJa3J3R/9+Qrg==", "license": "MIT", "dependencies": { "@radix-ui/primitive": "1.1.1", "@radix-ui/react-compose-refs": "1.1.1", - "@radix-ui/react-primitive": "2.0.1", + "@radix-ui/react-primitive": "2.0.2", "@radix-ui/react-use-callback-ref": "1.1.0", "@radix-ui/react-use-escape-keydown": "1.1.0" }, @@ -1809,13 +1821,13 @@ } }, "node_modules/@radix-ui/react-focus-scope": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-scope/-/react-focus-scope-1.1.1.tgz", - "integrity": "sha512-01omzJAYRxXdG2/he/+xy+c8a8gCydoQ1yOxnWNcRhrrBW5W+RQJ22EK1SaO8tb3WoUsuEw7mJjBozPzihDFjA==", + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-scope/-/react-focus-scope-1.1.2.tgz", + "integrity": "sha512-zxwE80FCU7lcXUGWkdt6XpTTCKPitG1XKOwViTxHVKIJhZl9MvIl2dVHeZENCWD9+EdWv05wlaEkRXUykU27RA==", "license": "MIT", "dependencies": { "@radix-ui/react-compose-refs": "1.1.1", - "@radix-ui/react-primitive": "2.0.1", + "@radix-ui/react-primitive": "2.0.2", "@radix-ui/react-use-callback-ref": "1.1.0" }, "peerDependencies": { @@ -1852,12 +1864,12 @@ } }, "node_modules/@radix-ui/react-portal": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/@radix-ui/react-portal/-/react-portal-1.1.3.tgz", - "integrity": "sha512-NciRqhXnGojhT93RPyDaMPfLH3ZSl4jjIFbZQ1b/vxvZEdHsBZ49wP9w8L3HzUQwep01LcWtkUvm0OVB5JAHTw==", + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@radix-ui/react-portal/-/react-portal-1.1.4.tgz", + "integrity": "sha512-sn2O9k1rPFYVyKd5LAJfo96JlSGVFpa1fS6UuBJfrZadudiw5tAmru+n1x7aMRQ84qDM71Zh1+SzK5QwU0tJfA==", "license": "MIT", "dependencies": { - "@radix-ui/react-primitive": "2.0.1", + "@radix-ui/react-primitive": "2.0.2", "@radix-ui/react-use-layout-effect": "1.1.0" }, "peerDependencies": { @@ -1900,12 +1912,12 @@ } }, "node_modules/@radix-ui/react-primitive": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.0.1.tgz", - "integrity": "sha512-sHCWTtxwNn3L3fH8qAfnF3WbUZycW93SM1j3NFDzXBiz8D6F5UTTy8G1+WFEaiCdvCVRJWj6N2R4Xq6HdiHmDg==", + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.0.2.tgz", + "integrity": "sha512-Ec/0d38EIuvDF+GZjcMU/Ze6MxntVJYO/fRlCPhCaVUyPY9WTalHJw54tp9sXeJo3tlShWpy41vQRgLRGOuz+w==", "license": "MIT", "dependencies": { - "@radix-ui/react-slot": "1.1.1" + "@radix-ui/react-slot": "1.1.2" }, "peerDependencies": { "@types/react": "*", @@ -1923,12 +1935,12 @@ } }, "node_modules/@radix-ui/react-separator": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@radix-ui/react-separator/-/react-separator-1.1.1.tgz", - "integrity": "sha512-RRiNRSrD8iUiXriq/Y5n4/3iE8HzqgLHsusUSg5jVpU2+3tqcUFPJXHDymwEypunc2sWxDUS3UC+rkZRlHedsw==", + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-separator/-/react-separator-1.1.2.tgz", + "integrity": "sha512-oZfHcaAp2Y6KFBX6I5P1u7CQoy4lheCGiYj+pGFrHy8E/VNRb5E39TkTr3JrV520csPBTZjkuKFdEsjS5EUNKQ==", "license": "MIT", "dependencies": { - "@radix-ui/react-primitive": "2.0.1" + "@radix-ui/react-primitive": "2.0.2" }, "peerDependencies": { "@types/react": "*", @@ -1946,9 +1958,9 @@ } }, "node_modules/@radix-ui/react-slot": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.1.1.tgz", - "integrity": "sha512-RApLLOcINYJA+dMVbOju7MYv1Mb2EBp2nH4HdDzXTSyaR5optlm6Otrz1euW3HbdOR8UmmFK06TD+A9frYWv+g==", + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.1.2.tgz", + "integrity": "sha512-YAKxaiGsSQJ38VzKH86/BPRC4rh+b1Jpa+JneA5LRE7skmLPNAyeG8kPJj/oo4STLvlrs8vkf/iYyc3A5stYCQ==", "license": "MIT", "dependencies": { "@radix-ui/react-compose-refs": "1.1.1" @@ -3965,10 +3977,18 @@ "win32" ] }, + "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/@sinclair/typebox": { "version": "0.34.16", "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.34.16.tgz", - "integrity": "sha512-rIljj8VPYAfn26ANY+5pCNVBPiv6hSufuKGe46y65cJZpvx8vHvPXlU0Q/Le4OGtlNaL8Jg2FuhtvQX18lSIqA==" + "integrity": "sha512-rIljj8VPYAfn26ANY+5pCNVBPiv6hSufuKGe46y65cJZpvx8vHvPXlU0Q/Le4OGtlNaL8Jg2FuhtvQX18lSIqA==", + "license": "MIT" }, "node_modules/@snyk/github-codeowners": { "version": "1.1.0", @@ -4484,10 +4504,18 @@ "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", "license": "MIT" }, + "node_modules/@types/json5": { + "version": "0.0.29", + "resolved": "https://registry.npmjs.org/@types/json5/-/json5-0.0.29.tgz", + "integrity": "sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/lodash": { "version": "4.17.15", "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.17.15.tgz", - "integrity": "sha512-w/P33JFeySuhN6JLkysYUK2gEmy9kHHFN7E8ro0tkfmlDOgxBDzWEZ/J8cWA+fHqFevpswDTFZnDx+R9lbL6xw==" + "integrity": "sha512-w/P33JFeySuhN6JLkysYUK2gEmy9kHHFN7E8ro0tkfmlDOgxBDzWEZ/J8cWA+fHqFevpswDTFZnDx+R9lbL6xw==", + "license": "MIT" }, "node_modules/@types/mdast": { "version": "4.0.4", @@ -4599,16 +4627,16 @@ } }, "node_modules/@typescript-eslint/parser": { - "version": "8.19.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.19.0.tgz", - "integrity": "sha512-6M8taKyOETY1TKHp0x8ndycipTVgmp4xtg5QpEZzXxDhNvvHOJi5rLRkLr8SK3jTgD5l4fTlvBiRdfsuWydxBw==", + "version": "8.23.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.23.0.tgz", + "integrity": "sha512-h2lUByouOXFAlMec2mILeELUbME5SZRN/7R9Cw2RD2lRQQY08MWMM+PmVVKKJNK1aIwqTo9t/0CvOxwPbRIE2Q==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/scope-manager": "8.19.0", - "@typescript-eslint/types": "8.19.0", - "@typescript-eslint/typescript-estree": "8.19.0", - "@typescript-eslint/visitor-keys": "8.19.0", + "@typescript-eslint/scope-manager": "8.23.0", + "@typescript-eslint/types": "8.23.0", + "@typescript-eslint/typescript-estree": "8.23.0", + "@typescript-eslint/visitor-keys": "8.23.0", "debug": "^4.3.4" }, "engines": { @@ -4623,6 +4651,135 @@ "typescript": ">=4.8.4 <5.8.0" } }, + "node_modules/@typescript-eslint/parser/node_modules/@typescript-eslint/scope-manager": { + "version": "8.23.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.23.0.tgz", + "integrity": "sha512-OGqo7+dXHqI7Hfm+WqkZjKjsiRtFUQHPdGMXzk5mYXhJUedO7e/Y7i8AK3MyLMgZR93TX4bIzYrfyVjLC+0VSw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.23.0", + "@typescript-eslint/visitor-keys": "8.23.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/parser/node_modules/@typescript-eslint/types": { + "version": "8.23.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.23.0.tgz", + "integrity": "sha512-1sK4ILJbCmZOTt9k4vkoulT6/y5CHJ1qUYxqpF1K/DBAd8+ZUL4LlSCxOssuH5m4rUaaN0uS0HlVPvd45zjduQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/parser/node_modules/@typescript-eslint/typescript-estree": { + "version": "8.23.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.23.0.tgz", + "integrity": "sha512-LcqzfipsB8RTvH8FX24W4UUFk1bl+0yTOf9ZA08XngFwMg4Kj8A+9hwz8Cr/ZS4KwHrmo9PJiLZkOt49vPnuvQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.23.0", + "@typescript-eslint/visitor-keys": "8.23.0", + "debug": "^4.3.4", + "fast-glob": "^3.3.2", + "is-glob": "^4.0.3", + "minimatch": "^9.0.4", + "semver": "^7.6.0", + "ts-api-utils": "^2.0.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <5.8.0" + } + }, + "node_modules/@typescript-eslint/parser/node_modules/@typescript-eslint/visitor-keys": { + "version": "8.23.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.23.0.tgz", + "integrity": "sha512-oWWhcWDLwDfu++BGTZcmXWqpwtkwb5o7fxUIGksMQQDSdPW9prsSnfIOZMlsj4vBOSrcnjIUZMiIjODgGosFhQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.23.0", + "eslint-visitor-keys": "^4.2.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/parser/node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/@typescript-eslint/parser/node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@typescript-eslint/parser/node_modules/semver": { + "version": "7.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.1.tgz", + "integrity": "sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@typescript-eslint/parser/node_modules/ts-api-utils": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.0.1.tgz", + "integrity": "sha512-dnlgjFSVetynI8nzgJ+qF62efpglpWRk8isUEWZGWlJYySCTD6aKvbUDu+zbPeDakk3bg5H4XpitHukgfL1m9w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.12" + }, + "peerDependencies": { + "typescript": ">=4.8.4" + } + }, "node_modules/@typescript-eslint/scope-manager": { "version": "8.19.0", "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.19.0.tgz", @@ -4822,14 +4979,14 @@ } }, "node_modules/@vitest/coverage-istanbul": { - "version": "2.1.8", - "resolved": "https://registry.npmjs.org/@vitest/coverage-istanbul/-/coverage-istanbul-2.1.8.tgz", - "integrity": "sha512-cSaCd8KcWWvgDwEJSXm0NEWZ1YTiJzjicKHy+zOEbUm0gjbbkz+qJf1p8q71uBzSlS7vdnZA8wRLeiwVE3fFTA==", + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@vitest/coverage-istanbul/-/coverage-istanbul-3.0.5.tgz", + "integrity": "sha512-yTcIwrpLHOyPP28PXXLRv1NzzKCrqDnmT7oVypTa1Q24P6OwGT4Wi6dXNEaJg33vmrPpoe81f31kwB5MtfM+ow==", "dev": true, "license": "MIT", "dependencies": { "@istanbuljs/schema": "^0.1.3", - "debug": "^4.3.7", + "debug": "^4.4.0", "istanbul-lib-coverage": "^3.2.2", "istanbul-lib-instrument": "^6.0.3", "istanbul-lib-report": "^3.0.1", @@ -4837,24 +4994,24 @@ "istanbul-reports": "^3.1.7", "magicast": "^0.3.5", "test-exclude": "^7.0.1", - "tinyrainbow": "^1.2.0" + "tinyrainbow": "^2.0.0" }, "funding": { "url": "https://opencollective.com/vitest" }, "peerDependencies": { - "vitest": "2.1.8" + "vitest": "3.0.5" } }, "node_modules/@vitest/expect": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-3.0.4.tgz", - "integrity": "sha512-Nm5kJmYw6P2BxhJPkO3eKKhGYKRsnqJqf+r0yOGRKpEP+bSCBDsjXgiu1/5QFrnPMEgzfC38ZEjvCFgaNBC0Eg==", + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-3.0.5.tgz", + "integrity": "sha512-nNIOqupgZ4v5jWuQx2DSlHLEs7Q4Oh/7AYwNyE+k0UQzG7tSmjPXShUikn1mpNGzYEN2jJbTvLejwShMitovBA==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/spy": "3.0.4", - "@vitest/utils": "3.0.4", + "@vitest/spy": "3.0.5", + "@vitest/utils": "3.0.5", "chai": "^5.1.2", "tinyrainbow": "^2.0.0" }, @@ -4862,52 +5019,14 @@ "url": "https://opencollective.com/vitest" } }, - "node_modules/@vitest/expect/node_modules/@vitest/pretty-format": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-3.0.4.tgz", - "integrity": "sha512-ts0fba+dEhK2aC9PFuZ9LTpULHpY/nd6jhAQ5IMU7Gaj7crPCTdCFfgvXxruRBLFS+MLraicCuFXxISEq8C93g==", - "dev": true, - "license": "MIT", - "dependencies": { - "tinyrainbow": "^2.0.0" - }, - "funding": { - "url": "https://opencollective.com/vitest" - } - }, - "node_modules/@vitest/expect/node_modules/@vitest/utils": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-3.0.4.tgz", - "integrity": "sha512-8BqC1ksYsHtbWH+DfpOAKrFw3jl3Uf9J7yeFh85Pz52IWuh1hBBtyfEbRNNZNjl8H8A5yMLH9/t+k7HIKzQcZQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@vitest/pretty-format": "3.0.4", - "loupe": "^3.1.2", - "tinyrainbow": "^2.0.0" - }, - "funding": { - "url": "https://opencollective.com/vitest" - } - }, - "node_modules/@vitest/expect/node_modules/tinyrainbow": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-2.0.0.tgz", - "integrity": "sha512-op4nsTR47R6p0vMUUoYl/a+ljLFVtlfaXkLQmqfLR1qHma1h/ysYk4hEXZ880bf2CYgTskvTa/e196Vd5dDQXw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=14.0.0" - } - }, "node_modules/@vitest/mocker": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-3.0.4.tgz", - "integrity": "sha512-gEef35vKafJlfQbnyOXZ0Gcr9IBUsMTyTLXsEQwuyYAerpHqvXhzdBnDFuHLpFqth3F7b6BaFr4qV/Cs1ULx5A==", + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-3.0.5.tgz", + "integrity": "sha512-CLPNBFBIE7x6aEGbIjaQAX03ZZlBMaWwAjBdMkIf/cAn6xzLTiM3zYqO/WAbieEjsAZir6tO71mzeHZoodThvw==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/spy": "3.0.4", + "@vitest/spy": "3.0.5", "estree-walker": "^3.0.3", "magic-string": "^0.30.17" }, @@ -4928,60 +5047,32 @@ } }, "node_modules/@vitest/pretty-format": { - "version": "2.1.8", - "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-2.1.8.tgz", - "integrity": "sha512-9HiSZ9zpqNLKlbIDRWOnAWqgcA7xu+8YxXSekhr0Ykab7PAYFkhkwoqVArPOtJhPmYeE2YHgKZlj3CP36z2AJQ==", + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-3.0.5.tgz", + "integrity": "sha512-CjUtdmpOcm4RVtB+up8r2vVDLR16Mgm/bYdkGFe3Yj/scRfCpbSi2W/BDSDcFK7ohw8UXvjMbOp9H4fByd/cOA==", "dev": true, "license": "MIT", "dependencies": { - "tinyrainbow": "^1.2.0" + "tinyrainbow": "^2.0.0" }, "funding": { "url": "https://opencollective.com/vitest" } }, "node_modules/@vitest/runner": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-3.0.4.tgz", - "integrity": "sha512-dKHzTQ7n9sExAcWH/0sh1elVgwc7OJ2lMOBrAm73J7AH6Pf9T12Zh3lNE1TETZaqrWFXtLlx3NVrLRb5hCK+iw==", + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-3.0.5.tgz", + "integrity": "sha512-BAiZFityFexZQi2yN4OX3OkJC6scwRo8EhRB0Z5HIGGgd2q+Nq29LgHU/+ovCtd0fOfXj5ZI6pwdlUmC5bpi8A==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/utils": "3.0.4", + "@vitest/utils": "3.0.5", "pathe": "^2.0.2" }, "funding": { "url": "https://opencollective.com/vitest" } }, - "node_modules/@vitest/runner/node_modules/@vitest/pretty-format": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-3.0.4.tgz", - "integrity": "sha512-ts0fba+dEhK2aC9PFuZ9LTpULHpY/nd6jhAQ5IMU7Gaj7crPCTdCFfgvXxruRBLFS+MLraicCuFXxISEq8C93g==", - "dev": true, - "license": "MIT", - "dependencies": { - "tinyrainbow": "^2.0.0" - }, - "funding": { - "url": "https://opencollective.com/vitest" - } - }, - "node_modules/@vitest/runner/node_modules/@vitest/utils": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-3.0.4.tgz", - "integrity": "sha512-8BqC1ksYsHtbWH+DfpOAKrFw3jl3Uf9J7yeFh85Pz52IWuh1hBBtyfEbRNNZNjl8H8A5yMLH9/t+k7HIKzQcZQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@vitest/pretty-format": "3.0.4", - "loupe": "^3.1.2", - "tinyrainbow": "^2.0.0" - }, - "funding": { - "url": "https://opencollective.com/vitest" - } - }, "node_modules/@vitest/runner/node_modules/pathe": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.2.tgz", @@ -4989,24 +5080,14 @@ "dev": true, "license": "MIT" }, - "node_modules/@vitest/runner/node_modules/tinyrainbow": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-2.0.0.tgz", - "integrity": "sha512-op4nsTR47R6p0vMUUoYl/a+ljLFVtlfaXkLQmqfLR1qHma1h/ysYk4hEXZ880bf2CYgTskvTa/e196Vd5dDQXw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=14.0.0" - } - }, "node_modules/@vitest/snapshot": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-3.0.4.tgz", - "integrity": "sha512-+p5knMLwIk7lTQkM3NonZ9zBewzVp9EVkVpvNta0/PlFWpiqLaRcF4+33L1it3uRUCh0BGLOaXPPGEjNKfWb4w==", + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-3.0.5.tgz", + "integrity": "sha512-GJPZYcd7v8QNUJ7vRvLDmRwl+a1fGg4T/54lZXe+UOGy47F9yUfE18hRCtXL5aHN/AONu29NGzIXSVFh9K0feA==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/pretty-format": "3.0.4", + "@vitest/pretty-format": "3.0.5", "magic-string": "^0.30.17", "pathe": "^2.0.2" }, @@ -5014,19 +5095,6 @@ "url": "https://opencollective.com/vitest" } }, - "node_modules/@vitest/snapshot/node_modules/@vitest/pretty-format": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-3.0.4.tgz", - "integrity": "sha512-ts0fba+dEhK2aC9PFuZ9LTpULHpY/nd6jhAQ5IMU7Gaj7crPCTdCFfgvXxruRBLFS+MLraicCuFXxISEq8C93g==", - "dev": true, - "license": "MIT", - "dependencies": { - "tinyrainbow": "^2.0.0" - }, - "funding": { - "url": "https://opencollective.com/vitest" - } - }, "node_modules/@vitest/snapshot/node_modules/pathe": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.2.tgz", @@ -5034,20 +5102,10 @@ "dev": true, "license": "MIT" }, - "node_modules/@vitest/snapshot/node_modules/tinyrainbow": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-2.0.0.tgz", - "integrity": "sha512-op4nsTR47R6p0vMUUoYl/a+ljLFVtlfaXkLQmqfLR1qHma1h/ysYk4hEXZ880bf2CYgTskvTa/e196Vd5dDQXw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=14.0.0" - } - }, "node_modules/@vitest/spy": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-3.0.4.tgz", - "integrity": "sha512-sXIMF0oauYyUy2hN49VFTYodzEAu744MmGcPR3ZBsPM20G+1/cSW/n1U+3Yu/zHxX2bIDe1oJASOkml+osTU6Q==", + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-3.0.5.tgz", + "integrity": "sha512-5fOzHj0WbUNqPK6blI/8VzZdkBlQLnT25knX0r4dbZI9qoZDf3qAdjoMmDcLG5A83W6oUUFJgUd0EYBc2P5xqg==", "dev": true, "license": "MIT", "dependencies": { @@ -5058,37 +5116,44 @@ } }, "node_modules/@vitest/ui": { - "version": "2.1.8", - "resolved": "https://registry.npmjs.org/@vitest/ui/-/ui-2.1.8.tgz", - "integrity": "sha512-5zPJ1fs0ixSVSs5+5V2XJjXLmNzjugHRyV11RqxYVR+oMcogZ9qTuSfKW+OcTV0JeFNznI83BNylzH6SSNJ1+w==", + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@vitest/ui/-/ui-3.0.5.tgz", + "integrity": "sha512-gw2noso6WI+2PeMVCZFntdATS6xl9qhQcbhkPQ9sOmx/Xn0f4Bx4KDSbD90jpJPF0l5wOzSoGCmKyVR3W612mg==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/utils": "2.1.8", + "@vitest/utils": "3.0.5", "fflate": "^0.8.2", - "flatted": "^3.3.1", - "pathe": "^1.1.2", + "flatted": "^3.3.2", + "pathe": "^2.0.2", "sirv": "^3.0.0", "tinyglobby": "^0.2.10", - "tinyrainbow": "^1.2.0" + "tinyrainbow": "^2.0.0" }, "funding": { "url": "https://opencollective.com/vitest" }, "peerDependencies": { - "vitest": "2.1.8" + "vitest": "3.0.5" } }, + "node_modules/@vitest/ui/node_modules/pathe": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.2.tgz", + "integrity": "sha512-15Ztpk+nov8DR524R4BF7uEuzESgzUEAV4Ah7CUMNGXdE5ELuvxElxGXndBl32vMSsWa1jpNf22Z+Er3sKwq+w==", + "dev": true, + "license": "MIT" + }, "node_modules/@vitest/utils": { - "version": "2.1.8", - "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-2.1.8.tgz", - "integrity": "sha512-dwSoui6djdwbfFmIgbIjX2ZhIoG7Ex/+xpxyiEgIGzjliY8xGkcpITKTlp6B4MgtGkF2ilvm97cPM96XZaAgcA==", + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-3.0.5.tgz", + "integrity": "sha512-N9AX0NUoUtVwKwy21JtwzaqR5L5R5A99GAbrHfCCXK1lp593i/3AZAXhSP43wRQuxYsflrdzEfXZFo1reR1Nkg==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/pretty-format": "2.1.8", + "@vitest/pretty-format": "3.0.5", "loupe": "^3.1.2", - "tinyrainbow": "^1.2.0" + "tinyrainbow": "^2.0.0" }, "funding": { "url": "https://opencollective.com/vitest" @@ -5291,24 +5356,153 @@ "dequal": "^2.0.3" } }, - "node_modules/assertion-error": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", - "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", + "node_modules/array-buffer-byte-length": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/array-buffer-byte-length/-/array-buffer-byte-length-1.0.2.tgz", + "integrity": "sha512-LHE+8BuR7RYGDKvnrmcuSq3tDcKv9OFEXQt/HpbZhY7V6h0zlUXutnAD82GiFx9rdieCMjkvtcsPqBwgUl1Iiw==", "dev": true, "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "is-array-buffer": "^3.0.5" + }, "engines": { - "node": ">=12" + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/asynckit": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", - "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "node_modules/array-includes": { + "version": "3.1.8", + "resolved": "https://registry.npmjs.org/array-includes/-/array-includes-3.1.8.tgz", + "integrity": "sha512-itaWrbYbqpGXkGhZPGUulwnhVf5Hpy1xiCFsGqyIGglbBxmG5vSjxQen3/WGOjPpNEv1RtBLKxbmVXm8HpJStQ==", "dev": true, - "license": "MIT" - }, - "node_modules/autoprefixer": { + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.2", + "es-object-atoms": "^1.0.0", + "get-intrinsic": "^1.2.4", + "is-string": "^1.0.7" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array.prototype.findlastindex": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/array.prototype.findlastindex/-/array.prototype.findlastindex-1.2.5.tgz", + "integrity": "sha512-zfETvRFA8o7EiNn++N5f/kaCw221hrpGsDmcpndVupkPzEc1Wuf3VgC0qby1BbHs7f5DVYjgtEU2LLh5bqeGfQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.2", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0", + "es-shim-unscopables": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array.prototype.flat": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/array.prototype.flat/-/array.prototype.flat-1.3.3.tgz", + "integrity": "sha512-rwG/ja1neyLqCuGZ5YYrznA62D4mZXg0i1cIskIUKSiqF3Cje9/wXAls9B9s1Wa2fomMsIv8czB8jZcPmxCXFg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.5", + "es-shim-unscopables": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array.prototype.flatmap": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/array.prototype.flatmap/-/array.prototype.flatmap-1.3.3.tgz", + "integrity": "sha512-Y7Wt51eKJSyi80hFrJCePGGNo5ktJCslFuboqJsbf57CCPcm5zztluPlc4/aD8sWsKvlwatezpV4U1efk8kpjg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.5", + "es-shim-unscopables": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/arraybuffer.prototype.slice": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/arraybuffer.prototype.slice/-/arraybuffer.prototype.slice-1.0.4.tgz", + "integrity": "sha512-BNoCY6SXXPQ7gF2opIP4GBE+Xw7U+pHMYKuzjgCN3GwiaIR09UUeKfheyIry77QtrCBlC0KK0q5/TER/tYh3PQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "array-buffer-byte-length": "^1.0.1", + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.5", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "is-array-buffer": "^3.0.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/assertion-error": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", + "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + } + }, + "node_modules/async-function": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/async-function/-/async-function-1.0.0.tgz", + "integrity": "sha512-hsU18Ae8CDTR6Kgu9DYf0EbCr/a5iGL0rytQDobUcdpYOKokk8LEjVphnXkDkgpi0wYVsqrXuP0bZxJaTqdgoA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/autoprefixer": { "version": "10.4.20", "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.20.tgz", "integrity": "sha512-XY25y5xSv/wEoqzDyXXME4AFfkZI0P23z6Fs3YgymDnKJkCGOnkL0iTxCa85UTqaSgfcqyf3UA6+c7wUvx/16g==", @@ -5346,6 +5540,22 @@ "postcss": "^8.1.0" } }, + "node_modules/available-typed-arrays": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz", + "integrity": "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "possible-typed-array-names": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/bail": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/bail/-/bail-2.0.2.tgz", @@ -5509,6 +5719,56 @@ "node": ">=8" } }, + "node_modules/call-bind": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.8.tgz", + "integrity": "sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.0", + "es-define-property": "^1.0.0", + "get-intrinsic": "^1.2.4", + "set-function-length": "^1.2.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.1.tgz", + "integrity": "sha512-BhYE+WDaywFg2TBWYNXAE+8B1ATnThNBqXHP5nQu0jWJdVvY2hvkpyB3qOmtmDePiS5/BDQ8wASEWGMWRG148g==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.3.tgz", + "integrity": "sha512-YTd+6wGlNlPxSuri7Y6X8tY2dmm12UMH66RpKMhiX6rsk5wXXnYgbUcOt8kiS31/AjfoTOvCsE+w8nZQLQnzHA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "get-intrinsic": "^1.2.6" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/callsites": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", @@ -6029,6 +6289,60 @@ "node": ">=18" } }, + "node_modules/data-view-buffer": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/data-view-buffer/-/data-view-buffer-1.0.2.tgz", + "integrity": "sha512-EmKO5V3OLXh1rtK2wgXRansaK1/mtVdTUEiEI0W8RkvgT05kfxaH29PliLnpLP73yYO6142Q72QNa8Wx/A5CqQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "is-data-view": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/data-view-byte-length": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/data-view-byte-length/-/data-view-byte-length-1.0.2.tgz", + "integrity": "sha512-tuhGbE6CfTM9+5ANGf+oQb72Ky/0+s3xKUpHvShfiz2RxMFgFPjsXuRLBVMtvMs15awe45SRb83D6wH4ew6wlQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "is-data-view": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/inspect-js" + } + }, + "node_modules/data-view-byte-offset": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/data-view-byte-offset/-/data-view-byte-offset-1.0.1.tgz", + "integrity": "sha512-BS8PfmtDGnrgYdOonGZQdLZslWIeCGFP9tpan0hi1Co2Zr2NKADsvGYA8XxuG/4UWgJ6Cjtv+YJnB6MM69QGlQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "is-data-view": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/date-fns": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-4.1.0.tgz", @@ -6106,6 +6420,42 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/define-data-property": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", + "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "gopd": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/define-properties": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.1.tgz", + "integrity": "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-data-property": "^1.0.1", + "has-property-descriptors": "^1.0.0", + "object-keys": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/defu": { "version": "6.1.4", "resolved": "https://registry.npmjs.org/defu/-/defu-6.1.4.tgz", @@ -6170,6 +6520,19 @@ "integrity": "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==", "license": "MIT" }, + "node_modules/doctrine": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz", + "integrity": "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "esutils": "^2.0.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/dom-accessibility-api": { "version": "0.5.16", "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz", @@ -6190,6 +6553,21 @@ "url": "https://dotenvx.com" } }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/eastasianwidth": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", @@ -6273,6 +6651,92 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/es-abstract": { + "version": "1.23.9", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.23.9.tgz", + "integrity": "sha512-py07lI0wjxAC/DcfK1S6G7iANonniZwTISvdPzk9hzeH0IZIshbuuFxLIU96OyF89Yb9hiqWn8M/bY83KY5vzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "array-buffer-byte-length": "^1.0.2", + "arraybuffer.prototype.slice": "^1.0.4", + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "data-view-buffer": "^1.0.2", + "data-view-byte-length": "^1.0.2", + "data-view-byte-offset": "^1.0.1", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0", + "es-set-tostringtag": "^2.1.0", + "es-to-primitive": "^1.3.0", + "function.prototype.name": "^1.1.8", + "get-intrinsic": "^1.2.7", + "get-proto": "^1.0.0", + "get-symbol-description": "^1.1.0", + "globalthis": "^1.0.4", + "gopd": "^1.2.0", + "has-property-descriptors": "^1.0.2", + "has-proto": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "internal-slot": "^1.1.0", + "is-array-buffer": "^3.0.5", + "is-callable": "^1.2.7", + "is-data-view": "^1.0.2", + "is-regex": "^1.2.1", + "is-shared-array-buffer": "^1.0.4", + "is-string": "^1.1.1", + "is-typed-array": "^1.1.15", + "is-weakref": "^1.1.0", + "math-intrinsics": "^1.1.0", + "object-inspect": "^1.13.3", + "object-keys": "^1.1.1", + "object.assign": "^4.1.7", + "own-keys": "^1.0.1", + "regexp.prototype.flags": "^1.5.3", + "safe-array-concat": "^1.1.3", + "safe-push-apply": "^1.0.0", + "safe-regex-test": "^1.1.0", + "set-proto": "^1.0.0", + "string.prototype.trim": "^1.2.10", + "string.prototype.trimend": "^1.0.9", + "string.prototype.trimstart": "^1.0.8", + "typed-array-buffer": "^1.0.3", + "typed-array-byte-length": "^1.0.3", + "typed-array-byte-offset": "^1.0.4", + "typed-array-length": "^1.0.7", + "unbox-primitive": "^1.1.0", + "which-typed-array": "^1.1.18" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, "node_modules/es-module-lexer": { "version": "1.6.0", "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.6.0.tgz", @@ -6280,30 +6744,87 @@ "dev": true, "license": "MIT" }, - "node_modules/esbuild": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.24.2.tgz", - "integrity": "sha512-+9egpBW8I3CD5XPe0n6BfT5fxLzxrlDzqydF3aviG+9ni1lDC/OvMHcxqEFV0+LANZG5R1bFMWfUrjVsdwxJvA==", + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", "dev": true, - "hasInstallScript": true, "license": "MIT", - "bin": { - "esbuild": "bin/esbuild" + "dependencies": { + "es-errors": "^1.3.0" }, "engines": { - "node": ">=18" + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" }, - "optionalDependencies": { - "@esbuild/aix-ppc64": "0.24.2", - "@esbuild/android-arm": "0.24.2", - "@esbuild/android-arm64": "0.24.2", - "@esbuild/android-x64": "0.24.2", - "@esbuild/darwin-arm64": "0.24.2", - "@esbuild/darwin-x64": "0.24.2", - "@esbuild/freebsd-arm64": "0.24.2", - "@esbuild/freebsd-x64": "0.24.2", - "@esbuild/linux-arm": "0.24.2", - "@esbuild/linux-arm64": "0.24.2", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-shim-unscopables": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/es-shim-unscopables/-/es-shim-unscopables-1.0.2.tgz", + "integrity": "sha512-J3yBRXCzDu4ULnQwxyToo/OjdMx6akgVC7K6few0a7F/0wLtmKKN7I73AH5T2836UuXRqN7Qg+IIUw/+YJksRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "hasown": "^2.0.0" + } + }, + "node_modules/es-to-primitive": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.3.0.tgz", + "integrity": "sha512-w+5mJ3GuFL+NjVtJlvydShqE1eN3h3PbI7/5LAsYJP/2qtuMXjfL2LpHSRqo4b4eSF5K/DH1JXKUAHSB2UW50g==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-callable": "^1.2.7", + "is-date-object": "^1.0.5", + "is-symbol": "^1.0.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/esbuild": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.24.2.tgz", + "integrity": "sha512-+9egpBW8I3CD5XPe0n6BfT5fxLzxrlDzqydF3aviG+9ni1lDC/OvMHcxqEFV0+LANZG5R1bFMWfUrjVsdwxJvA==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.24.2", + "@esbuild/android-arm": "0.24.2", + "@esbuild/android-arm64": "0.24.2", + "@esbuild/android-x64": "0.24.2", + "@esbuild/darwin-arm64": "0.24.2", + "@esbuild/darwin-x64": "0.24.2", + "@esbuild/freebsd-arm64": "0.24.2", + "@esbuild/freebsd-x64": "0.24.2", + "@esbuild/linux-arm": "0.24.2", + "@esbuild/linux-arm64": "0.24.2", "@esbuild/linux-ia32": "0.24.2", "@esbuild/linux-loong64": "0.24.2", "@esbuild/linux-mips64el": "0.24.2", @@ -6403,6 +6924,136 @@ } } }, + "node_modules/eslint-import-resolver-node": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.9.tgz", + "integrity": "sha512-WFj2isz22JahUv+B788TlO3N6zL3nNJGU8CcZbPZvVEkBPaJdCV4vy5wyghty5ROFbCRnm132v8BScu5/1BQ8g==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^3.2.7", + "is-core-module": "^2.13.0", + "resolve": "^1.22.4" + } + }, + "node_modules/eslint-import-resolver-node/node_modules/debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.1" + } + }, + "node_modules/eslint-import-resolver-typescript": { + "version": "3.7.0", + "resolved": "https://registry.npmjs.org/eslint-import-resolver-typescript/-/eslint-import-resolver-typescript-3.7.0.tgz", + "integrity": "sha512-Vrwyi8HHxY97K5ebydMtffsWAn1SCR9eol49eCd5fJS4O1WV7PaAjbcjmbfJJSMz/t4Mal212Uz/fQZrOB8mow==", + "dev": true, + "license": "ISC", + "dependencies": { + "@nolyfill/is-core-module": "1.0.39", + "debug": "^4.3.7", + "enhanced-resolve": "^5.15.0", + "fast-glob": "^3.3.2", + "get-tsconfig": "^4.7.5", + "is-bun-module": "^1.0.2", + "is-glob": "^4.0.3", + "stable-hash": "^0.0.4" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/unts/projects/eslint-import-resolver-ts" + }, + "peerDependencies": { + "eslint": "*", + "eslint-plugin-import": "*", + "eslint-plugin-import-x": "*" + }, + "peerDependenciesMeta": { + "eslint-plugin-import": { + "optional": true + }, + "eslint-plugin-import-x": { + "optional": true + } + } + }, + "node_modules/eslint-module-utils": { + "version": "2.12.0", + "resolved": "https://registry.npmjs.org/eslint-module-utils/-/eslint-module-utils-2.12.0.tgz", + "integrity": "sha512-wALZ0HFoytlyh/1+4wuZ9FJCD/leWHQzzrxJ8+rebyReSLk7LApMyd3WJaLVoN+D5+WIdJyDK1c6JnE65V4Zyg==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^3.2.7" + }, + "engines": { + "node": ">=4" + }, + "peerDependenciesMeta": { + "eslint": { + "optional": true + } + } + }, + "node_modules/eslint-module-utils/node_modules/debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.1" + } + }, + "node_modules/eslint-plugin-import": { + "version": "2.31.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.31.0.tgz", + "integrity": "sha512-ixmkI62Rbc2/w8Vfxyh1jQRTdRTF52VxwRVHl/ykPAmqG+Nb7/kNn+byLP0LxPgI7zWA16Jt82SybJInmMia3A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@rtsao/scc": "^1.1.0", + "array-includes": "^3.1.8", + "array.prototype.findlastindex": "^1.2.5", + "array.prototype.flat": "^1.3.2", + "array.prototype.flatmap": "^1.3.2", + "debug": "^3.2.7", + "doctrine": "^2.1.0", + "eslint-import-resolver-node": "^0.3.9", + "eslint-module-utils": "^2.12.0", + "hasown": "^2.0.2", + "is-core-module": "^2.15.1", + "is-glob": "^4.0.3", + "minimatch": "^3.1.2", + "object.fromentries": "^2.0.8", + "object.groupby": "^1.0.3", + "object.values": "^1.2.0", + "semver": "^6.3.1", + "string.prototype.trimend": "^1.0.8", + "tsconfig-paths": "^3.15.0" + }, + "engines": { + "node": ">=4" + }, + "peerDependencies": { + "eslint": "^2 || ^3 || ^4 || ^5 || ^6 || ^7.2.0 || ^8 || ^9" + } + }, + "node_modules/eslint-plugin-import/node_modules/debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.1" + } + }, "node_modules/eslint-plugin-react-hooks": { "version": "5.1.0", "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-5.1.0.tgz", @@ -6757,6 +7408,22 @@ "dev": true, "license": "ISC" }, + "node_modules/for-each": { + "version": "0.3.4", + "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.4.tgz", + "integrity": "sha512-kKaIINnFpzW6ffJNDjjyjrk21BkDx38c0xa/klsT8VzLCaMEefv4ZTacrcVR4DmgTeBra++jMDAfS/tS799YDw==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-callable": "^1.2.7" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/foreground-child": { "version": "3.3.0", "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.0.tgz", @@ -6866,6 +7533,37 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/function.prototype.name": { + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/function.prototype.name/-/function.prototype.name-1.1.8.tgz", + "integrity": "sha512-e5iwyodOHhbMr/yNrc7fDYG4qlbIvI5gajyzPnb5TCwyhjApznQh1BMFou9b30SevY43gCJKXycoCBjMbsuW0Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "define-properties": "^1.2.1", + "functions-have-names": "^1.2.3", + "hasown": "^2.0.2", + "is-callable": "^1.2.7" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/functions-have-names": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/functions-have-names/-/functions-have-names-1.2.3.tgz", + "integrity": "sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/fuse.js": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/fuse.js/-/fuse.js-7.0.0.tgz", @@ -6907,6 +7605,31 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/get-intrinsic": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.7.tgz", + "integrity": "sha512-VW6Pxhsrk0KAOqs3WEd0klDiF/+V7gQOpAvY1jVU/LHmaD/kQO4523aiJuikX/QAKYiW6x8Jh+RJej1almdtCA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0", + "function-bind": "^1.1.2", + "get-proto": "^1.0.0", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/get-nonce": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/get-nonce/-/get-nonce-1.0.1.tgz", @@ -6916,6 +7639,20 @@ "node": ">=6" } }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/get-stream": { "version": "8.0.1", "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-8.0.1.tgz", @@ -6929,6 +7666,37 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/get-symbol-description": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/get-symbol-description/-/get-symbol-description-1.1.0.tgz", + "integrity": "sha512-w9UMqWwJxHNOvoNzSJ2oPF5wvYcvP7jUvYzhp67yEhTi17ZDBBC1z9pTdGuzjD+EFIqLSYRweZjqfiPzQ06Ebg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-tsconfig": { + "version": "4.10.0", + "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.10.0.tgz", + "integrity": "sha512-kGzZ3LWWQcGIAmg6iWvXn0ei6WDtV26wzHRMwDSzmAbcXrTEXxHy6IehI6/4eT6VRKyMP1eF1VqwrVUmE/LR7A==", + "dev": true, + "license": "MIT", + "dependencies": { + "resolve-pkg-maps": "^1.0.0" + }, + "funding": { + "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" + } + }, "node_modules/giget": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/giget/-/giget-1.2.3.tgz", @@ -7018,6 +7786,23 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/globalthis": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/globalthis/-/globalthis-1.0.4.tgz", + "integrity": "sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-properties": "^1.2.1", + "gopd": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/globrex": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/globrex/-/globrex-0.1.2.tgz", @@ -7025,6 +7810,19 @@ "dev": true, "license": "MIT" }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/graceful-fs": { "version": "4.2.11", "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", @@ -7071,6 +7869,19 @@ "uglify-js": "^3.1.4" } }, + "node_modules/has-bigints": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.1.0.tgz", + "integrity": "sha512-R3pbpkcIqv2Pm3dUwgjclDRVmWpTJW2DcMzcIhEXEx1oh/CEMObMm3KLmRJOdvhM7o4uQBnwr8pzRK2sJWIqfg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/has-flag": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", @@ -7081,6 +7892,64 @@ "node": ">=8" } }, + "node_modules/has-property-descriptors": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", + "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-define-property": "^1.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-proto": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.2.0.tgz", + "integrity": "sha512-KIL7eQPfHQRC8+XluaIw7BHUwwqL19bQn4hzNgdr+1wXoU0KKj6rufu47lhY7KbJR2C6T6+PfyN0Ea7wkSS+qQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/hasown": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", @@ -7380,6 +8249,21 @@ "integrity": "sha512-0aO8FkhNZlj/ZIbNi7Lxxr12obT7cL1moPfE4tg1LkX7LlLfC6DeX4l2ZEud1ukP9jNQyNnfzQVqwbwmAATY4Q==", "license": "MIT" }, + "node_modules/internal-slot": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.1.0.tgz", + "integrity": "sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "hasown": "^2.0.2", + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/intl-messageformat": { "version": "10.7.11", "resolved": "https://registry.npmjs.org/intl-messageformat/-/intl-messageformat-10.7.11.tgz", @@ -7416,6 +8300,60 @@ "url": "https://github.com/sponsors/wooorm" } }, + "node_modules/is-array-buffer": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.5.tgz", + "integrity": "sha512-DDfANUiiG2wC1qawP66qlTugJeL5HyzMpfr8lLK+jMQirGzNod0B12cFB/9q838Ru27sBwfw78/rdoU7RERz6A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "get-intrinsic": "^1.2.6" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-async-function": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-async-function/-/is-async-function-2.1.1.tgz", + "integrity": "sha512-9dgM/cZBnNvjzaMYHVoxxfPj2QXt22Ev7SuuPrs+xav0ukGB0S6d4ydZdEiM48kLx5kDV+QBPrpVnFyefL8kkQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "async-function": "^1.0.0", + "call-bound": "^1.0.3", + "get-proto": "^1.0.1", + "has-tostringtag": "^1.0.2", + "safe-regex-test": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-bigint": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-bigint/-/is-bigint-1.1.0.tgz", + "integrity": "sha512-n4ZT37wG78iz03xPRKJrHTdZbe3IicyucEtdRsV5yglwc3GyUfbAfpSeD0FJ41NbUNSt5wbhqfp1fS+BgnvDFQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-bigints": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/is-binary-path": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", @@ -7428,6 +8366,59 @@ "node": ">=8" } }, + "node_modules/is-boolean-object": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.2.2.tgz", + "integrity": "sha512-wa56o2/ElJMYqjCjGkXri7it5FbebW5usLw/nPmCMs5DeZ7eziSYZhSmPRn0txqeW4LnAmQQU7FgqLpsEFKM4A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-bun-module": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/is-bun-module/-/is-bun-module-1.3.0.tgz", + "integrity": "sha512-DgXeu5UWI0IsMQundYb5UAOzm6G2eVnarJ0byP6Tm55iZNKceD59LNPA2L4VvsScTtHcw0yEkVwSf7PC+QoLSA==", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^7.6.3" + } + }, + "node_modules/is-bun-module/node_modules/semver": { + "version": "7.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.1.tgz", + "integrity": "sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA==", + "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", + "integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/is-core-module": { "version": "2.16.1", "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", @@ -7443,6 +8434,41 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-data-view": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-data-view/-/is-data-view-1.0.2.tgz", + "integrity": "sha512-RKtWF8pGmS87i2D6gqQu/l7EYRlVdfzemCJN/P3UOs//x1QE7mfhvzHIApBTRf7axvT6DMGwSwBXYCT0nfB9xw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "get-intrinsic": "^1.2.6", + "is-typed-array": "^1.1.13" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-date-object": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.1.0.tgz", + "integrity": "sha512-PwwhEakHVKTdRNVOw+/Gyh0+MzlCl4R6qKvkhuvLtPMggI1WAHt9sOwZxQLSGpUaDnrdyDsomoRgNnCfKNSXXg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/is-decimal": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/is-decimal/-/is-decimal-2.0.1.tgz", @@ -7462,6 +8488,22 @@ "node": ">=0.10.0" } }, + "node_modules/is-finalizationregistry": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-finalizationregistry/-/is-finalizationregistry-1.1.1.tgz", + "integrity": "sha512-1pC6N8qWJbWoPtEjgcL2xyhQOP491EQjeUo3qTKcmV8YSDDJrOepfG8pcC7h/QgnQHYSv0mJ3Z/ZWxmatVrysg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/is-fullwidth-code-point": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-4.0.0.tgz", @@ -7475,6 +8517,25 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/is-generator-function": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.1.0.tgz", + "integrity": "sha512-nPUB5km40q9e8UfN/Zc24eLlzdSf9OfKByBw9CIdw4H1giPMeA0OIJvbchsCu4npfI2QcMVBsGEBHKZ7wLTWmQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "get-proto": "^1.0.0", + "has-tostringtag": "^1.0.2", + "safe-regex-test": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/is-glob": { "version": "4.0.3", "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", @@ -7497,6 +8558,19 @@ "url": "https://github.com/sponsors/wooorm" } }, + "node_modules/is-map": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/is-map/-/is-map-2.0.3.tgz", + "integrity": "sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/is-node-process": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/is-node-process/-/is-node-process-1.2.0.tgz", @@ -7513,38 +8587,207 @@ "node": ">=0.12.0" } }, + "node_modules/is-number-object": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-number-object/-/is-number-object-1.1.1.tgz", + "integrity": "sha512-lZhclumE1G6VYD8VHe35wFaIif+CTy5SJIi5+3y4psDgWu4wPDoBhF8NxUOinEc7pHgiTsT6MaBb92rKhhD+Xw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/is-plain-obj": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-4.1.0.tgz", "integrity": "sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==", "license": "MIT", "engines": { - "node": ">=12" + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-potential-custom-element-name": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz", + "integrity": "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/is-regex": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.2.1.tgz", + "integrity": "sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "gopd": "^1.2.0", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-set": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/is-set/-/is-set-2.0.3.tgz", + "integrity": "sha512-iPAjerrse27/ygGLxw+EBR9agv9Y6uLeYVJMu+QNCoouJ1/1ri0mGrcWpfCqFZuzzx3WjtwxG098X+n4OuRkPg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-shared-array-buffer": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.4.tgz", + "integrity": "sha512-ISWac8drv4ZGfwKl5slpHG9OwPNty4jOWPRIhBpxOoD+hqITiwuipOQ2bNthAzwA3B4fIjO4Nln74N0S9byq8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-stream": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-3.0.0.tgz", + "integrity": "sha512-LnQR4bZ9IADDRSkvpqMGvt/tEJWclzklNgSw48V5EAaAeDd6qGvN8ei6k5p0tvxSR171VmGyHuTiAOfxAbr8kA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-string": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.1.1.tgz", + "integrity": "sha512-BtEeSsoaQjlSPBemMQIrY1MY0uM6vnS1g5fmufYOtnxLGUZM2178PKbhsk7Ffv58IX+ZtcvoGwccYsh0PglkAA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-symbol": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.1.1.tgz", + "integrity": "sha512-9gGx6GTtCQM73BgmHQXfDmLtfjjTUDSyoxTCbp5WtoixAhfgsDirWIcVQ/IHpvI5Vgd5i/J5F7B9cN/WlVbC/w==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "has-symbols": "^1.1.0", + "safe-regex-test": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-typed-array": { + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.15.tgz", + "integrity": "sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "which-typed-array": "^1.1.16" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-weakmap": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/is-weakmap/-/is-weakmap-2.0.2.tgz", + "integrity": "sha512-K5pXYOm9wqY1RgjpL3YTkF39tni1XajUIkawTLUo9EZEVUFga5gSQJF8nNS7ZwJQ02y+1YCNYcMh+HIf1ZqE+w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" }, "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/is-potential-custom-element-name": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz", - "integrity": "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==", + "node_modules/is-weakref": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-weakref/-/is-weakref-1.1.1.tgz", + "integrity": "sha512-6i9mGWSlqzNMEqpCp93KwRS1uUOodk2OJ6b+sq7ZPDSy2WuI5NFIxp/254TytR8ftefexkWn5xNiHUNpPOfSew==", "dev": true, - "license": "MIT" + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } }, - "node_modules/is-stream": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-3.0.0.tgz", - "integrity": "sha512-LnQR4bZ9IADDRSkvpqMGvt/tEJWclzklNgSw48V5EAaAeDd6qGvN8ei6k5p0tvxSR171VmGyHuTiAOfxAbr8kA==", + "node_modules/is-weakset": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/is-weakset/-/is-weakset-2.0.4.tgz", + "integrity": "sha512-mfcwb6IzQyOKTs84CQMrOwW4gQcaTOAWJ0zzJCl2WSPDrWk/OzDaImWFH3djXhb24g4eudZfLRozAvPGw4d9hQ==", "dev": true, "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "get-intrinsic": "^1.2.6" + }, "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + "node": ">= 0.4" }, "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/isarray": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", + "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==", + "dev": true, + "license": "MIT" + }, "node_modules/isexe": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", @@ -8232,6 +9475,16 @@ "url": "https://github.com/sponsors/wooorm" } }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, "node_modules/mdast-util-find-and-replace": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/mdast-util-find-and-replace/-/mdast-util-find-and-replace-3.0.2.tgz", @@ -9479,6 +10732,103 @@ "node": ">= 6" } }, + "node_modules/object-inspect": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object-keys": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", + "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/object.assign": { + "version": "4.1.7", + "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.7.tgz", + "integrity": "sha512-nK28WOo+QIjBkDduTINE4JkF/UJJKyf2EJxvJKfblDpyg0Q+pkOHNTL0Qwy6NP6FhE/EnzV73BxxqcJaXY9anw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0", + "has-symbols": "^1.1.0", + "object-keys": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object.fromentries": { + "version": "2.0.8", + "resolved": "https://registry.npmjs.org/object.fromentries/-/object.fromentries-2.0.8.tgz", + "integrity": "sha512-k6E21FzySsSK5a21KRADBd/NGneRegFO5pLHfdQLpRDETUNJueLXs3WCzyQ3tFRDYgbq3KHGXfTbi2bs8WQ6rQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.2", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object.groupby": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/object.groupby/-/object.groupby-1.0.3.tgz", + "integrity": "sha512-+Lhy3TQTuzXI5hevh8sBGqbmurHbbIjAi0Z4S63nthVLmLxfbj4T54a4CfZrXIrt9iP4mVAPYMo/v99taj3wjQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/object.values": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/object.values/-/object.values-1.2.1.tgz", + "integrity": "sha512-gXah6aZrcUxjWg2zR2MwouP2eHlCBzdV4pygudehaKXSGW4v2AsRQUK+lwwXhii6KFZcunEnmSUoYp5CXibxtA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/ohash": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/ohash/-/ohash-1.1.4.tgz", @@ -9527,6 +10877,24 @@ "dev": true, "license": "MIT" }, + "node_modules/own-keys": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/own-keys/-/own-keys-1.0.1.tgz", + "integrity": "sha512-qFOyK5PjiWZd+QQIh+1jhdb9LpxTF0qs7Pm8o5QHYZ0M3vKqSqzsZaEB6oWlxZ+q2sJBMI/Ktgd2N5ZwQoRHfg==", + "dev": true, + "license": "MIT", + "dependencies": { + "get-intrinsic": "^1.2.6", + "object-keys": "^1.1.1", + "safe-push-apply": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/p-limit": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", @@ -9784,6 +11152,16 @@ "pathe": "^1.1.2" } }, + "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", + "integrity": "sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, "node_modules/postcss": { "version": "8.4.49", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.49.tgz", @@ -10250,16 +11628,16 @@ } }, "node_modules/react-remove-scroll": { - "version": "2.6.2", - "resolved": "https://registry.npmjs.org/react-remove-scroll/-/react-remove-scroll-2.6.2.tgz", - "integrity": "sha512-KmONPx5fnlXYJQqC62Q+lwIeAk64ws/cUw6omIumRzMRPqgnYqhSSti99nbj0Ry13bv7dF+BKn7NB+OqkdZGTw==", + "version": "2.6.3", + "resolved": "https://registry.npmjs.org/react-remove-scroll/-/react-remove-scroll-2.6.3.tgz", + "integrity": "sha512-pnAi91oOk8g8ABQKGF5/M9qxmmOPxaAnopyTHYfqYEwJhyFrbbBtHuSgtKEoH0jpcxx5o3hXqH1mNd9/Oi+8iQ==", "license": "MIT", "dependencies": { "react-remove-scroll-bar": "^2.3.7", - "react-style-singleton": "^2.2.1", + "react-style-singleton": "^2.2.3", "tslib": "^2.1.0", "use-callback-ref": "^1.3.3", - "use-sidecar": "^1.1.2" + "use-sidecar": "^1.1.3" }, "engines": { "node": ">=10" @@ -10455,6 +11833,29 @@ "node": ">=8" } }, + "node_modules/reflect.getprototypeof": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.10.tgz", + "integrity": "sha512-00o4I+DVrefhv+nX0ulyi3biSHCPDe+yLv5o/p6d/UVlirijB8E16FtfwSAi4g3tcqrQ4lRAqQSoFEZJehYEcw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.9", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0", + "get-intrinsic": "^1.2.7", + "get-proto": "^1.0.1", + "which-builtin-type": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/refractor": { "version": "3.6.0", "resolved": "https://registry.npmjs.org/refractor/-/refractor-3.6.0.tgz", @@ -10577,6 +11978,27 @@ "integrity": "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==", "license": "MIT" }, + "node_modules/regexp.prototype.flags": { + "version": "1.5.4", + "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.4.tgz", + "integrity": "sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-errors": "^1.3.0", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "set-function-name": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/remark-gfm": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/remark-gfm/-/remark-gfm-4.0.0.tgz", @@ -10698,6 +12120,16 @@ "node": ">=4" } }, + "node_modules/resolve-pkg-maps": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz", + "integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" + } + }, "node_modules/restore-cursor": { "version": "5.1.0", "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-5.1.0.tgz", @@ -10831,6 +12263,61 @@ "queue-microtask": "^1.2.2" } }, + "node_modules/safe-array-concat": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/safe-array-concat/-/safe-array-concat-1.1.3.tgz", + "integrity": "sha512-AURm5f0jYEOydBj7VQlVvDrjeFgthDdEF5H1dP+6mNpoXOMo1quQqJ4wvJDyRZ9+pO3kGWoOdmV08cSv2aJV6Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.2", + "get-intrinsic": "^1.2.6", + "has-symbols": "^1.1.0", + "isarray": "^2.0.5" + }, + "engines": { + "node": ">=0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/safe-push-apply": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/safe-push-apply/-/safe-push-apply-1.0.0.tgz", + "integrity": "sha512-iKE9w/Z7xCzUMIZqdBsp6pEQvwuEebH4vdpjcDWnyzaI6yl6O9FHvVpmGelvEHNsoY6wGblkxR6Zty/h00WiSA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "isarray": "^2.0.5" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/safe-regex-test": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.1.0.tgz", + "integrity": "sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "is-regex": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/safer-buffer": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", @@ -10872,6 +12359,55 @@ "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.1.tgz", "integrity": "sha512-IOc8uWeOZgnb3ptbCURJWNjWUPcO3ZnTTdzsurqERrP6nPyv+paC55vJM0LpOlT2ne+Ix+9+CRG1MNLlyZ4GjQ==" }, + "node_modules/set-function-length": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", + "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.4", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/set-function-name": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/set-function-name/-/set-function-name-2.0.2.tgz", + "integrity": "sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", + "functions-have-names": "^1.2.3", + "has-property-descriptors": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/set-proto": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/set-proto/-/set-proto-1.0.0.tgz", + "integrity": "sha512-RJRdvCo6IAnPdsvP/7m6bsQqNnn1FCBX5ZNtFL98MmFF/4xAIJTIg1YbHW5DC2W5SKZanrC6i4HsJqlajw/dZw==", + "dev": true, + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/shebang-command": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", @@ -10884,13 +12420,89 @@ "node": ">=8" } }, - "node_modules/shebang-regex": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", - "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/side-channel": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", + "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3", + "side-channel-list": "^1.0.0", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", + "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "dev": true, "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, "engines": { - "node": ">=8" + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, "node_modules/siginfo": { @@ -11009,6 +12621,13 @@ "url": "https://github.com/sponsors/wooorm" } }, + "node_modules/stable-hash": { + "version": "0.0.4", + "resolved": "https://registry.npmjs.org/stable-hash/-/stable-hash-0.0.4.tgz", + "integrity": "sha512-LjdcbuBeLcdETCrPn9i8AYAZ1eCtu4ECAWtP7UleOiZ9LzVxRzzUZEoZ8zB24nhkQnDWyET0I+3sWokSDS3E7g==", + "dev": true, + "license": "MIT" + }, "node_modules/stackback": { "version": "0.0.2", "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", @@ -11125,6 +12744,65 @@ "node": ">=8" } }, + "node_modules/string.prototype.trim": { + "version": "1.2.10", + "resolved": "https://registry.npmjs.org/string.prototype.trim/-/string.prototype.trim-1.2.10.tgz", + "integrity": "sha512-Rs66F0P/1kedk5lyYyH9uBzuiI/kNRmwJAR9quK6VOtIpZ2G+hMZd+HQbbv25MgCA6gEffoMZYxlTod4WcdrKA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.2", + "define-data-property": "^1.1.4", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.5", + "es-object-atoms": "^1.0.0", + "has-property-descriptors": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/string.prototype.trimend": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.9.tgz", + "integrity": "sha512-G7Ok5C6E/j4SGfyLCloXTrngQIQU3PWtXGst3yM7Bea9FRURf1S42ZHlZZtsNque2FN2PoUhfZXYLNWwEr4dLQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.2", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/string.prototype.trimstart": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.8.tgz", + "integrity": "sha512-UXSH262CSZY1tfu3G3Secr6uGLCFVPMhIqHjlgCUtCCcgihYc/xKs9djMTMUOb2j1mVSeU8EU6NWc/iQKU6Gfg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/stringify-entities": { "version": "4.0.4", "resolved": "https://registry.npmjs.org/stringify-entities/-/stringify-entities-4.0.4.tgz", @@ -11176,6 +12854,16 @@ "node": ">=8" } }, + "node_modules/strip-bom": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", + "integrity": "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, "node_modules/strip-final-newline": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-3.0.0.tgz", @@ -11562,9 +13250,9 @@ } }, "node_modules/tinyrainbow": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-1.2.0.tgz", - "integrity": "sha512-weEDEq7Z5eTHPDh4xjX789+fHfF+P8boiFB+0vbWzpbnbsEr/GRaohi/uMKxg8RZMXnl1ItAi/IUHWMsjDV7kQ==", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-2.0.0.tgz", + "integrity": "sha512-op4nsTR47R6p0vMUUoYl/a+ljLFVtlfaXkLQmqfLR1qHma1h/ysYk4hEXZ880bf2CYgTskvTa/e196Vd5dDQXw==", "dev": true, "license": "MIT", "engines": { @@ -11718,6 +13406,32 @@ } } }, + "node_modules/tsconfig-paths": { + "version": "3.15.0", + "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-3.15.0.tgz", + "integrity": "sha512-2Ac2RgzDe/cn48GvOe3M+o82pEFewD3UPbyoUHHdKasHwJKjds4fLXWf/Ux5kATBKN20oaFGu+jbElp1pos0mg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/json5": "^0.0.29", + "json5": "^1.0.2", + "minimist": "^1.2.6", + "strip-bom": "^3.0.0" + } + }, + "node_modules/tsconfig-paths/node_modules/json5": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.2.tgz", + "integrity": "sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==", + "dev": true, + "license": "MIT", + "dependencies": { + "minimist": "^1.2.0" + }, + "bin": { + "json5": "lib/cli.js" + } + }, "node_modules/tslib": { "version": "2.8.1", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", @@ -11755,6 +13469,84 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/typed-array-buffer": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.3.tgz", + "integrity": "sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "is-typed-array": "^1.1.14" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/typed-array-byte-length": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/typed-array-byte-length/-/typed-array-byte-length-1.0.3.tgz", + "integrity": "sha512-BaXgOuIxz8n8pIq3e7Atg/7s+DpiYrxn4vdot3w9KbnBhcRQq6o3xemQdIfynqSeXeDrF32x+WvfzmOjPiY9lg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "for-each": "^0.3.3", + "gopd": "^1.2.0", + "has-proto": "^1.2.0", + "is-typed-array": "^1.1.14" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/typed-array-byte-offset": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/typed-array-byte-offset/-/typed-array-byte-offset-1.0.4.tgz", + "integrity": "sha512-bTlAFB/FBYMcuX81gbL4OcpH5PmlFHqlCCpAl8AlEzMz5k53oNDvN8p1PNOWLEmI2x4orp3raOFB51tv9X+MFQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.8", + "for-each": "^0.3.3", + "gopd": "^1.2.0", + "has-proto": "^1.2.0", + "is-typed-array": "^1.1.15", + "reflect.getprototypeof": "^1.0.9" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/typed-array-length": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/typed-array-length/-/typed-array-length-1.0.7.tgz", + "integrity": "sha512-3KS2b+kL7fsuk/eJZ7EQdnEmQoaho/r6KUef7hxvltNA5DR8NAUM+8wJMbJyZ4G9/7i3v5zPBIMN5aybAh2/Jg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "for-each": "^0.3.3", + "gopd": "^1.0.1", + "is-typed-array": "^1.1.13", + "possible-typed-array-names": "^1.0.0", + "reflect.getprototypeof": "^1.0.6" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/typescript": { "version": "5.7.3", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.7.3.tgz", @@ -11792,6 +13584,31 @@ "typescript": ">=4.8.4 <5.8.0" } }, + "node_modules/typescript-eslint/node_modules/@typescript-eslint/parser": { + "version": "8.19.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.19.0.tgz", + "integrity": "sha512-6M8taKyOETY1TKHp0x8ndycipTVgmp4xtg5QpEZzXxDhNvvHOJi5rLRkLr8SK3jTgD5l4fTlvBiRdfsuWydxBw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/scope-manager": "8.19.0", + "@typescript-eslint/types": "8.19.0", + "@typescript-eslint/typescript-estree": "8.19.0", + "@typescript-eslint/visitor-keys": "8.19.0", + "debug": "^4.3.4" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <5.8.0" + } + }, "node_modules/ufo": { "version": "1.5.4", "resolved": "https://registry.npmjs.org/ufo/-/ufo-1.5.4.tgz", @@ -11813,6 +13630,25 @@ "node": ">=0.8.0" } }, + "node_modules/unbox-primitive": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.1.0.tgz", + "integrity": "sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "has-bigints": "^1.0.2", + "has-symbols": "^1.1.0", + "which-boxed-primitive": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/undici-types": { "version": "6.20.0", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.20.0.tgz", @@ -12128,9 +13964,9 @@ } }, "node_modules/vite-node": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-3.0.4.tgz", - "integrity": "sha512-7JZKEzcYV2Nx3u6rlvN8qdo3QV7Fxyt6hx+CCKz9fbWxdX5IvUOmTWEAxMrWxaiSf7CKGLJQ5rFu8prb/jBjOA==", + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-3.0.5.tgz", + "integrity": "sha512-02JEJl7SbtwSDJdYS537nU6l+ktdvcREfLksk/NDAqtdKWGqHl+joXzEubHROmS3E6pip+Xgu2tFezMu75jH7A==", "dev": true, "license": "MIT", "dependencies": { @@ -12178,19 +14014,19 @@ } }, "node_modules/vitest": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/vitest/-/vitest-3.0.4.tgz", - "integrity": "sha512-6XG8oTKy2gnJIFTHP6LD7ExFeNLxiTkK3CfMvT7IfR8IN+BYICCf0lXUQmX7i7JoxUP8QmeP4mTnWXgflu4yjw==", + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-3.0.5.tgz", + "integrity": "sha512-4dof+HvqONw9bvsYxtkfUp2uHsTN9bV2CZIi1pWgoFpL1Lld8LA1ka9q/ONSsoScAKG7NVGf2stJTI7XRkXb2Q==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/expect": "3.0.4", - "@vitest/mocker": "3.0.4", - "@vitest/pretty-format": "^3.0.4", - "@vitest/runner": "3.0.4", - "@vitest/snapshot": "3.0.4", - "@vitest/spy": "3.0.4", - "@vitest/utils": "3.0.4", + "@vitest/expect": "3.0.5", + "@vitest/mocker": "3.0.5", + "@vitest/pretty-format": "^3.0.5", + "@vitest/runner": "3.0.5", + "@vitest/snapshot": "3.0.5", + "@vitest/spy": "3.0.5", + "@vitest/utils": "3.0.5", "chai": "^5.1.2", "debug": "^4.4.0", "expect-type": "^1.1.0", @@ -12202,7 +14038,7 @@ "tinypool": "^1.0.2", "tinyrainbow": "^2.0.0", "vite": "^5.0.0 || ^6.0.0", - "vite-node": "3.0.4", + "vite-node": "3.0.5", "why-is-node-running": "^2.3.0" }, "bin": { @@ -12218,8 +14054,8 @@ "@edge-runtime/vm": "*", "@types/debug": "^4.1.12", "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", - "@vitest/browser": "3.0.4", - "@vitest/ui": "3.0.4", + "@vitest/browser": "3.0.5", + "@vitest/ui": "3.0.5", "happy-dom": "*", "jsdom": "*" }, @@ -12274,34 +14110,6 @@ "url": "https://github.com/chalk/chalk?sponsor=1" } }, - "node_modules/vitest/node_modules/@vitest/pretty-format": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-3.0.4.tgz", - "integrity": "sha512-ts0fba+dEhK2aC9PFuZ9LTpULHpY/nd6jhAQ5IMU7Gaj7crPCTdCFfgvXxruRBLFS+MLraicCuFXxISEq8C93g==", - "dev": true, - "license": "MIT", - "dependencies": { - "tinyrainbow": "^2.0.0" - }, - "funding": { - "url": "https://opencollective.com/vitest" - } - }, - "node_modules/vitest/node_modules/@vitest/utils": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-3.0.4.tgz", - "integrity": "sha512-8BqC1ksYsHtbWH+DfpOAKrFw3jl3Uf9J7yeFh85Pz52IWuh1hBBtyfEbRNNZNjl8H8A5yMLH9/t+k7HIKzQcZQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@vitest/pretty-format": "3.0.4", - "loupe": "^3.1.2", - "tinyrainbow": "^2.0.0" - }, - "funding": { - "url": "https://opencollective.com/vitest" - } - }, "node_modules/vitest/node_modules/pathe": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.2.tgz", @@ -12309,16 +14117,6 @@ "dev": true, "license": "MIT" }, - "node_modules/vitest/node_modules/tinyrainbow": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-2.0.0.tgz", - "integrity": "sha512-op4nsTR47R6p0vMUUoYl/a+ljLFVtlfaXkLQmqfLR1qHma1h/ysYk4hEXZ880bf2CYgTskvTa/e196Vd5dDQXw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=14.0.0" - } - }, "node_modules/w3c-xmlserializer": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-5.0.0.tgz", @@ -12405,6 +14203,94 @@ "node": ">= 8" } }, + "node_modules/which-boxed-primitive": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/which-boxed-primitive/-/which-boxed-primitive-1.1.1.tgz", + "integrity": "sha512-TbX3mj8n0odCBFVlY8AxkqcHASw3L60jIuF8jFP78az3C2YhmGvqbHBpAjTRH2/xqYunrJ9g1jSyjCjpoWzIAA==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-bigint": "^1.1.0", + "is-boolean-object": "^1.2.1", + "is-number-object": "^1.1.1", + "is-string": "^1.1.1", + "is-symbol": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/which-builtin-type": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/which-builtin-type/-/which-builtin-type-1.2.1.tgz", + "integrity": "sha512-6iBczoX+kDQ7a3+YJBnh3T+KZRxM/iYNPXicqk66/Qfm1b93iu+yOImkg0zHbj5LNOcNv1TEADiZ0xa34B4q6Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "function.prototype.name": "^1.1.6", + "has-tostringtag": "^1.0.2", + "is-async-function": "^2.0.0", + "is-date-object": "^1.1.0", + "is-finalizationregistry": "^1.1.0", + "is-generator-function": "^1.0.10", + "is-regex": "^1.2.1", + "is-weakref": "^1.0.2", + "isarray": "^2.0.5", + "which-boxed-primitive": "^1.1.0", + "which-collection": "^1.0.2", + "which-typed-array": "^1.1.16" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/which-collection": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/which-collection/-/which-collection-1.0.2.tgz", + "integrity": "sha512-K4jVyjnBdgvc86Y6BkaLZEN933SwYOuBFkdmBu9ZfkcAbdVbpITnDmjvZ/aQjRXQrv5EPkTnD1s39GiiqbngCw==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-map": "^2.0.3", + "is-set": "^2.0.3", + "is-weakmap": "^2.0.2", + "is-weakset": "^2.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/which-typed-array": { + "version": "1.1.18", + "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.18.tgz", + "integrity": "sha512-qEcY+KJYlWyLH9vNbsr6/5j59AXk5ni5aakf8ldzBvGde6Iz4sxZGkJyWSAueTG7QhOvNRYb1lDdFmL5Td0QKA==", + "dev": true, + "license": "MIT", + "dependencies": { + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "for-each": "^0.3.3", + "gopd": "^1.2.0", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/why-is-node-running": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz", @@ -12746,34 +14632,6 @@ "zod": "^3.18.0" } }, - "node_modules/zustand": { - "version": "5.0.3", - "resolved": "https://registry.npmjs.org/zustand/-/zustand-5.0.3.tgz", - "integrity": "sha512-14fwWQtU3pH4dE0dOpdMiWjddcH+QzKIgk1cl8epwSE7yag43k/AD/m4L6+K7DytAOr9gGBe3/EXj9g7cdostg==", - "engines": { - "node": ">=12.20.0" - }, - "peerDependencies": { - "@types/react": ">=18.0.0", - "immer": ">=9.0.6", - "react": ">=18.0.0", - "use-sync-external-store": ">=1.2.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "immer": { - "optional": true - }, - "react": { - "optional": true - }, - "use-sync-external-store": { - "optional": true - } - } - }, "node_modules/zwitch": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/zwitch/-/zwitch-2.0.4.tgz", diff --git a/package.json b/package.json index e03aeb43..955084d8 100644 --- a/package.json +++ b/package.json @@ -52,8 +52,7 @@ "tailwind-variants": "^0.3.0", "tailwindcss-animate": "^1.0.7", "ts-pattern": "^5.6.2", - "zod": "^3.24.1", - "zustand": "^5.0.3" + "zod": "^3.24.1" }, "devDependencies": { "@eslint/js": "^9.15.0", @@ -67,12 +66,15 @@ "@types/node": "^22.10.1", "@types/react": "^19.0.2", "@types/react-dom": "^19.0.2", + "@typescript-eslint/parser": "^8.23.0", "@vitejs/plugin-react-swc": "^3.5.0", - "@vitest/coverage-istanbul": "^2.1.8", - "@vitest/expect": "^3.0.4", - "@vitest/ui": "^2.1.4", + "@vitest/coverage-istanbul": "^3.0.5", + "@vitest/expect": "^3.0.5", + "@vitest/ui": "^3.0.5", "autoprefixer": "^10.4.20", "eslint": "^9.18.0", + "eslint-import-resolver-typescript": "^3.7.0", + "eslint-plugin-import": "^2.31.0", "eslint-plugin-react-hooks": "^5.1.0", "eslint-plugin-react-refresh": "^0.4.16", "eslint-plugin-tailwindcss": "3.17.5", @@ -89,7 +91,7 @@ "typescript-eslint": "^8.15.0", "vite": "^6.0.1", "vite-tsconfig-paths": "^5.1.4", - "vitest": "^3.0.4", + "vitest": "^3.0.5", "vitest-fail-on-console": "^0.7.1" }, "overrides": { diff --git a/src/App.test.tsx b/src/App.test.tsx index a224397b..1cf341d5 100644 --- a/src/App.test.tsx +++ b/src/App.test.tsx @@ -7,7 +7,6 @@ import userEvent from "@testing-library/user-event"; describe("App", () => { it("should render header", async () => { render(); - expect(screen.getByText(/toggle sidebar/i)).toBeVisible(); expect(screen.getByText("Settings")).toBeVisible(); expect(screen.getByText("Help")).toBeVisible(); expect(screen.getByRole("banner", { name: "App header" })).toBeVisible(); diff --git a/src/App.tsx b/src/App.tsx index 254fa0d8..2badc373 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,31 +1,16 @@ import { Header } from "./features/header/components/header"; -import { PromptList } from "./components/PromptList"; -import { useQueryGetWorkspaceMessages } from "./hooks/use-query-get-workspace-messages"; -import { Sidebar } from "./components/Sidebar"; import { useSse } from "./hooks/useSse"; import Page from "./Page"; -function App() { - const { data: prompts, isLoading } = useQueryGetWorkspaceMessages(); +export default function App() { useSse(); return ( -
- - - -
-
- -
- -
-
-
+ <> +
+
+ +
+ ); } - -export default App; diff --git a/src/api/openapi.json b/src/api/openapi.json index 1969d932..e587d68e 100644 --- a/src/api/openapi.json +++ b/src/api/openapi.json @@ -8,9 +8,7 @@ "paths": { "/health": { "get": { - "tags": [ - "System" - ], + "tags": ["System"], "summary": "Health Check", "operationId": "health_check_health_get", "responses": { @@ -27,10 +25,7 @@ }, "/api/v1/provider-endpoints": { "get": { - "tags": [ - "CodeGate API", - "Providers" - ], + "tags": ["CodeGate API", "Providers"], "summary": "List Provider Endpoints", "description": "List all provider endpoints.", "operationId": "v1_list_provider_endpoints", @@ -80,10 +75,7 @@ } }, "post": { - "tags": [ - "CodeGate API", - "Providers" - ], + "tags": ["CodeGate API", "Providers"], "summary": "Add Provider Endpoint", "description": "Add a provider endpoint.", "operationId": "v1_add_provider_endpoint", @@ -123,10 +115,7 @@ }, "/api/v1/provider-endpoints/models": { "get": { - "tags": [ - "CodeGate API", - "Providers" - ], + "tags": ["CodeGate API", "Providers"], "summary": "List All Models For All Providers", "description": "List all models for all providers.", "operationId": "v1_list_all_models_for_all_providers", @@ -150,10 +139,7 @@ }, "/api/v1/provider-endpoints/{provider_id}/models": { "get": { - "tags": [ - "CodeGate API", - "Providers" - ], + "tags": ["CodeGate API", "Providers"], "summary": "List Models By Provider", "description": "List models by provider.", "operationId": "v1_list_models_by_provider", @@ -199,10 +185,7 @@ }, "/api/v1/provider-endpoints/{provider_id}": { "get": { - "tags": [ - "CodeGate API", - "Providers" - ], + "tags": ["CodeGate API", "Providers"], "summary": "Get Provider Endpoint", "description": "Get a provider endpoint by ID.", "operationId": "v1_get_provider_endpoint", @@ -242,10 +225,7 @@ } }, "put": { - "tags": [ - "CodeGate API", - "Providers" - ], + "tags": ["CodeGate API", "Providers"], "summary": "Update Provider Endpoint", "description": "Update a provider endpoint by ID.", "operationId": "v1_update_provider_endpoint", @@ -295,10 +275,7 @@ } }, "delete": { - "tags": [ - "CodeGate API", - "Providers" - ], + "tags": ["CodeGate API", "Providers"], "summary": "Delete Provider Endpoint", "description": "Delete a provider endpoint by id.", "operationId": "v1_delete_provider_endpoint", @@ -338,10 +315,7 @@ }, "/api/v1/provider-endpoints/{provider_id}/auth-material": { "put": { - "tags": [ - "CodeGate API", - "Providers" - ], + "tags": ["CodeGate API", "Providers"], "summary": "Configure Auth Material", "description": "Configure auth material for a provider.", "operationId": "v1_configure_auth_material", @@ -386,10 +360,7 @@ }, "/api/v1/workspaces": { "get": { - "tags": [ - "CodeGate API", - "Workspaces" - ], + "tags": ["CodeGate API", "Workspaces"], "summary": "List Workspaces", "description": "List all workspaces.", "operationId": "v1_list_workspaces", @@ -407,10 +378,7 @@ } }, "post": { - "tags": [ - "CodeGate API", - "Workspaces" - ], + "tags": ["CodeGate API", "Workspaces"], "summary": "Create Workspace", "description": "Create a new workspace.", "operationId": "v1_create_workspace", @@ -450,10 +418,7 @@ }, "/api/v1/workspaces/active": { "get": { - "tags": [ - "CodeGate API", - "Workspaces" - ], + "tags": ["CodeGate API", "Workspaces"], "summary": "List Active Workspaces", "description": "List all active workspaces.\n\nIn it's current form, this function will only return one workspace. That is,\nthe globally active workspace.", "operationId": "v1_list_active_workspaces", @@ -471,10 +436,7 @@ } }, "post": { - "tags": [ - "CodeGate API", - "Workspaces" - ], + "tags": ["CodeGate API", "Workspaces"], "summary": "Activate Workspace", "description": "Activate a workspace by name.", "operationId": "v1_activate_workspace", @@ -523,10 +485,7 @@ }, "/api/v1/workspaces/{workspace_name}": { "delete": { - "tags": [ - "CodeGate API", - "Workspaces" - ], + "tags": ["CodeGate API", "Workspaces"], "summary": "Delete Workspace", "description": "Delete a workspace by name.", "operationId": "v1_delete_workspace", @@ -565,10 +524,7 @@ }, "/api/v1/workspaces/archive": { "get": { - "tags": [ - "CodeGate API", - "Workspaces" - ], + "tags": ["CodeGate API", "Workspaces"], "summary": "List Archived Workspaces", "description": "List all archived workspaces.", "operationId": "v1_list_archived_workspaces", @@ -588,10 +544,7 @@ }, "/api/v1/workspaces/archive/{workspace_name}/recover": { "post": { - "tags": [ - "CodeGate API", - "Workspaces" - ], + "tags": ["CodeGate API", "Workspaces"], "summary": "Recover Workspace", "description": "Recover an archived workspace by name.", "operationId": "v1_recover_workspace", @@ -625,10 +578,7 @@ }, "/api/v1/workspaces/archive/{workspace_name}": { "delete": { - "tags": [ - "CodeGate API", - "Workspaces" - ], + "tags": ["CodeGate API", "Workspaces"], "summary": "Hard Delete Workspace", "description": "Hard delete an archived workspace by name.", "operationId": "v1_hard_delete_workspace", @@ -667,10 +617,7 @@ }, "/api/v1/workspaces/{workspace_name}/alerts": { "get": { - "tags": [ - "CodeGate API", - "Workspaces" - ], + "tags": ["CodeGate API", "Workspaces"], "summary": "Get Workspace Alerts", "description": "Get alerts for a workspace.", "operationId": "v1_get_workspace_alerts", @@ -722,10 +669,7 @@ }, "/api/v1/workspaces/{workspace_name}/messages": { "get": { - "tags": [ - "CodeGate API", - "Workspaces" - ], + "tags": ["CodeGate API", "Workspaces"], "summary": "Get Workspace Messages", "description": "Get messages for a workspace.", "operationId": "v1_get_workspace_messages", @@ -770,10 +714,7 @@ }, "/api/v1/workspaces/{workspace_name}/custom-instructions": { "get": { - "tags": [ - "CodeGate API", - "Workspaces" - ], + "tags": ["CodeGate API", "Workspaces"], "summary": "Get Workspace Custom Instructions", "description": "Get the custom instructions of a workspace.", "operationId": "v1_get_workspace_custom_instructions", @@ -812,10 +753,7 @@ } }, "put": { - "tags": [ - "CodeGate API", - "Workspaces" - ], + "tags": ["CodeGate API", "Workspaces"], "summary": "Set Workspace Custom Instructions", "operationId": "v1_set_workspace_custom_instructions", "parameters": [ @@ -856,10 +794,7 @@ } }, "delete": { - "tags": [ - "CodeGate API", - "Workspaces" - ], + "tags": ["CodeGate API", "Workspaces"], "summary": "Delete Workspace Custom Instructions", "operationId": "v1_delete_workspace_custom_instructions", "parameters": [ @@ -892,11 +827,7 @@ }, "/api/v1/workspaces/{workspace_name}/muxes": { "get": { - "tags": [ - "CodeGate API", - "Workspaces", - "Muxes" - ], + "tags": ["CodeGate API", "Workspaces", "Muxes"], "summary": "Get Workspace Muxes", "description": "Get the mux rules of a workspace.\n\nThe list is ordered in order of priority. That is, the first rule in the list\nhas the highest priority.", "operationId": "v1_get_workspace_muxes", @@ -939,11 +870,7 @@ } }, "put": { - "tags": [ - "CodeGate API", - "Workspaces", - "Muxes" - ], + "tags": ["CodeGate API", "Workspaces", "Muxes"], "summary": "Set Workspace Muxes", "description": "Set the mux rules of a workspace.", "operationId": "v1_set_workspace_muxes", @@ -991,10 +918,7 @@ }, "/api/v1/alerts_notification": { "get": { - "tags": [ - "CodeGate API", - "Dashboard" - ], + "tags": ["CodeGate API", "Dashboard"], "summary": "Stream Sse", "description": "Send alerts event", "operationId": "v1_stream_sse", @@ -1012,10 +936,7 @@ }, "/api/v1/version": { "get": { - "tags": [ - "CodeGate API", - "Dashboard" - ], + "tags": ["CodeGate API", "Dashboard"], "summary": "Version Check", "operationId": "v1_version_check", "responses": { @@ -1032,11 +953,7 @@ }, "/api/v1/workspaces/{workspace_name}/token-usage": { "get": { - "tags": [ - "CodeGate API", - "Workspaces", - "Token Usage" - ], + "tags": ["CodeGate API", "Workspaces", "Token Usage"], "summary": "Get Workspace Token Usage", "description": "Get the token usage of a workspace.", "operationId": "v1_get_workspace_token_usage", @@ -1086,9 +1003,7 @@ } }, "type": "object", - "required": [ - "name" - ], + "required": ["name"], "title": "ActivateWorkspaceRequest" }, "ActiveWorkspace": { @@ -1106,11 +1021,7 @@ } }, "type": "object", - "required": [ - "name", - "is_active", - "last_updated" - ], + "required": ["name", "is_active", "last_updated"], "title": "ActiveWorkspace" }, "AddProviderEndpointRequest": { @@ -1161,10 +1072,7 @@ } }, "type": "object", - "required": [ - "name", - "provider_type" - ], + "required": ["name", "provider_type"], "title": "AddProviderEndpointRequest", "description": "Represents a request to add a provider endpoint." }, @@ -1320,11 +1228,7 @@ } }, "type": "object", - "required": [ - "message", - "timestamp", - "message_id" - ], + "required": ["message", "timestamp", "message_id"], "title": "ChatMessage", "description": "Represents a chat message." }, @@ -1377,11 +1281,7 @@ } }, "type": "object", - "required": [ - "code", - "language", - "filepath" - ], + "required": ["code", "language", "filepath"], "title": "CodeSnippet", "description": "Represents a code snippet with its programming language.\n\nArgs:\n language: The programming language identifier (e.g., 'python', 'javascript')\n code: The actual code content" }, @@ -1403,9 +1303,7 @@ } }, "type": "object", - "required": [ - "auth_type" - ], + "required": ["auth_type"], "title": "ConfigureAuthMaterial", "description": "Represents a request to configure auth material for a provider." }, @@ -1491,9 +1389,7 @@ } }, "type": "object", - "required": [ - "name" - ], + "required": ["name"], "title": "CreateOrRenameWorkspaceRequest" }, "CustomInstructions": { @@ -1504,9 +1400,7 @@ } }, "type": "object", - "required": [ - "prompt" - ], + "required": ["prompt"], "title": "CustomInstructions" }, "HTTPValidationError": { @@ -1533,9 +1427,7 @@ } }, "type": "object", - "required": [ - "workspaces" - ], + "required": ["workspaces"], "title": "ListActiveWorkspacesResponse" }, "ListWorkspacesResponse": { @@ -1549,9 +1441,7 @@ } }, "type": "object", - "required": [ - "workspaces" - ], + "required": ["workspaces"], "title": "ListWorkspacesResponse" }, "ModelByProvider": { @@ -1570,19 +1460,13 @@ } }, "type": "object", - "required": [ - "name", - "provider_id", - "provider_name" - ], + "required": ["name", "provider_id", "provider_name"], "title": "ModelByProvider", "description": "Represents a model supported by a provider.\n\nNote that these are auto-discovered by the provider." }, "MuxMatcherType": { "type": "string", - "enum": [ - "catch_all" - ], + "enum": ["catch_all"], "title": "MuxMatcherType", "description": "Represents the different types of matchers we support." }, @@ -1612,21 +1496,13 @@ } }, "type": "object", - "required": [ - "provider_id", - "model", - "matcher_type" - ], + "required": ["provider_id", "model", "matcher_type"], "title": "MuxRule", "description": "Represents a mux rule for a provider." }, "ProviderAuthType": { "type": "string", - "enum": [ - "none", - "passthrough", - "api_key" - ], + "enum": ["none", "passthrough", "api_key"], "title": "ProviderAuthType", "description": "Represents the different types of auth we support for providers." }, @@ -1667,10 +1543,7 @@ } }, "type": "object", - "required": [ - "name", - "provider_type" - ], + "required": ["name", "provider_type"], "title": "ProviderEndpoint", "description": "Represents a provider's endpoint configuration. This\nallows us to persist the configuration for each provider,\nso we can use this for muxing messages." }, @@ -1705,19 +1578,13 @@ } }, "type": "object", - "required": [ - "question", - "answer" - ], + "required": ["question", "answer"], "title": "QuestionAnswer", "description": "Represents a question and answer pair." }, "QuestionType": { "type": "string", - "enum": [ - "chat", - "fim" - ], + "enum": ["chat", "fim"], "title": "QuestionType" }, "TokenUsage": { @@ -1761,10 +1628,7 @@ } }, "type": "object", - "required": [ - "tokens_by_model", - "token_usage" - ], + "required": ["tokens_by_model", "token_usage"], "title": "TokenUsageAggregate", "description": "Represents the tokens used. Includes the information of the tokens used by model.\n`used_tokens` are the total tokens used in the `tokens_by_model` list." }, @@ -1782,11 +1646,7 @@ } }, "type": "object", - "required": [ - "provider_type", - "model", - "token_usage" - ], + "required": ["provider_type", "model", "token_usage"], "title": "TokenUsageByModel", "description": "Represents the tokens used by a model." }, @@ -1816,11 +1676,7 @@ } }, "type": "object", - "required": [ - "loc", - "msg", - "type" - ], + "required": ["loc", "msg", "type"], "title": "ValidationError" }, "Workspace": { @@ -1835,10 +1691,7 @@ } }, "type": "object", - "required": [ - "name", - "is_active" - ], + "required": ["name", "is_active"], "title": "Workspace" } } diff --git a/src/components/AlertDetail.tsx b/src/components/AlertDetail.tsx deleted file mode 100644 index 10de3054..00000000 --- a/src/components/AlertDetail.tsx +++ /dev/null @@ -1,55 +0,0 @@ -import { AlertConversation } from "@/api/generated"; -import { isAlertMalicious } from "@/features/alerts/lib/is-alert-malicious"; -import { isAlertSecret } from "@/features/alerts/lib/is-alert-secret"; -import { Markdown } from "./Markdown"; - -type MaliciousPkgType = { - name: string; - type: string; - status: string; - description: string; -}; - -export function AlertDetail({ alert }: { alert: AlertConversation }) { - if (alert.trigger_string === null) return "N/A"; - if (isAlertSecret(alert) && typeof alert.trigger_string === "string") { - return ( -
- {alert.trigger_string} -
- ); - } - - if (isAlertMalicious(alert) && typeof alert.trigger_string === "object") { - const maliciousPkg = alert.trigger_string as MaliciousPkgType; - - return ( -
- -   - - {maliciousPkg.type}/{maliciousPkg.name} - - {maliciousPkg.status && ( - <> -
- {maliciousPkg.status} - - )} - {maliciousPkg.description && ( - <> -
- {" "} - {maliciousPkg.description} - - )} -
- ); - } - return null; -} diff --git a/src/components/Error.tsx b/src/components/Error.tsx index 71a7ad5a..8e1561f7 100644 --- a/src/components/Error.tsx +++ b/src/components/Error.tsx @@ -1,41 +1,48 @@ import { AlertCircle } from "@untitled-ui/icons-react"; +// eslint-disable-next-line import/no-restricted-paths import { Header } from "../features/header/components/header"; import { Card } from "@stacklok/ui-kit"; +export function ErrorFallbackContent() { + return ( +
+ + +
+ An error occurred +
+
+ If this issue persists, please reach out to us on{" "} + + Discord + {" "} + or open a new{" "} + + Github issue + +
+
+
+ ); +} + export function Error() { return (
-
-
-
- - -
- An error occurred -
-
- If this issue persists, please reach out to us on{" "} - - Discord - {" "} - or open a new{" "} - - Github issue - -
-
+
+
); } diff --git a/src/components/PromptList.tsx b/src/components/PromptList.tsx deleted file mode 100644 index 2e989ded..00000000 --- a/src/components/PromptList.tsx +++ /dev/null @@ -1,51 +0,0 @@ -import { Link } from "react-router-dom"; -import { - parsingPromptText, - groupPromptsByRelativeDate, - sanitizeQuestionPrompt, -} from "@/lib/utils"; -import { useCurrentPromptStore } from "@/hooks/useCurrentPromptStore"; -import clsx from "clsx"; -import { Conversation } from "@/api/generated"; - -export function PromptList({ prompts }: { prompts: Conversation[] }) { - const { currentPromptId, setCurrentPromptId } = useCurrentPromptStore(); - - const groupedPrompts = groupPromptsByRelativeDate(prompts); - - return ( -
- {Object.entries(groupedPrompts).map(([group, prompts]) => ( -
-

- {group} -

-
    - {prompts.map((prompt) => ( -
  • - setCurrentPromptId(prompt.chat_id)} - to={`/prompt/${prompt.chat_id}`} - className={clsx( - `text-secondary text-sm truncate hover:text-gray-500`, - { "font-bold": currentPromptId === prompt.chat_id }, - )} - > - {parsingPromptText( - sanitizeQuestionPrompt({ - question: - prompt.question_answers?.[0]?.question.message ?? "", - answer: - prompt.question_answers?.[0]?.answer?.message ?? "", - }), - prompt.conversation_timestamp, - )} - -
  • - ))} -
-
- ))} -
- ); -} diff --git a/src/components/Sidebar.tsx b/src/components/Sidebar.tsx deleted file mode 100644 index bd9bc002..00000000 --- a/src/components/Sidebar.tsx +++ /dev/null @@ -1,49 +0,0 @@ -import { - Sidebar as SidebarUI, - SidebarContent, - SidebarFooter, - SidebarGroup, - SidebarHeader, - SidebarMenu, - SidebarMenuItem, - SidebarMenuSkeleton, -} from "@/components/ui/sidebar"; - -export function Sidebar({ - children, - loading, -}: { - loading: boolean; - children: React.ReactNode; -}) { - if (loading) { - return ( - - - Prompt History - - - - {Array.from({ length: 20 }).map((_, index) => ( - - - - ))} - - - - - ); - } - - return ( - - - Prompt History - - - {children} - - - ); -} diff --git a/src/components/__tests__/PromptList.test.tsx b/src/components/__tests__/PromptList.test.tsx deleted file mode 100644 index 26982578..00000000 --- a/src/components/__tests__/PromptList.test.tsx +++ /dev/null @@ -1,93 +0,0 @@ -import { describe, expect, it } from "vitest"; -import { screen } from "@testing-library/react"; -import { PromptList } from "../PromptList"; -import mockedPrompts from "@/mocks/msw/fixtures/GET_MESSAGES.json"; -import { render } from "@/lib/test-utils"; -import { Conversation } from "@/api/generated"; - -const conversationTimestamp = "2025-01-02T14:19:58.024100Z"; -const prompt = mockedPrompts[0] as Conversation; - -const testCases: [string, { message: string; expected: RegExp | string }][] = [ - [ - "codegate cmd", - { - message: "codegate workspace -h", - expected: /codegate workspace -h/i, - }, - ], - [ - "render code with path", - { - message: "// Path: src/lib/utils.ts", - expected: /Prompt on filepath: src\/lib\/utils.ts/i, - }, - ], - [ - "render code with file path", - { - message: " ```tsx // filepath: /tests/my-test.tsx import", - expected: /Prompt on file\/\/ filepath: \/tests\/my-test.tsx/i, - }, - ], - [ - "render snippet", - { - message: - 'Compare this snippet from src/test.ts: // import { fakePkg } from "fake-pkg";', - expected: /Prompt from snippet compare this snippet from src\/test.ts:/i, - }, - ], - [ - "render default", - { - message: - "I know that this local proxy can forward requests to api.foo.com.\n\napi.foo.com will validate whether the connection si trusted using a certificate authority added on the local machine, specifically whether they allow SSL and x.509 basic policy.\n\nI need to be able to validate the proxys ability to make requests to api.foo.com. I only have access to code that can run in the browser. I can infer this based on a successful request. Be creative.", - expected: - "I know that this local proxy can forward requests to api.foo.com. api.foo.com will validate whether the connection si trusted using a certificate authority added on the local machine, specifically whether they allow SSL and x.509 basic policy. I need to be able to validate the proxys ability to make requests to api.foo.com. I only have access to code that can run in the browser. I can infer this based on a successful request. Be creative.", - }, - ], -]; - -describe("PromptList", () => { - it("render prompt", () => { - render(); - expect( - screen.getByRole("link", { - name: /server\.py do you see any security issue\?/i, - }), - ).toBeVisible(); - }); - - it.each(testCases)("%s", (_title: string, { message, expected }) => { - render( - , - ); - - expect( - screen.getByRole("link", { - name: expected, - }), - ).toBeVisible(); - }); -}); diff --git a/src/components/empty-state.tsx b/src/components/empty-state.tsx index 3fb1d3f8..6f2616c6 100644 --- a/src/components/empty-state.tsx +++ b/src/components/empty-state.tsx @@ -12,7 +12,7 @@ const actionsStyle = tv({ }, }); -export function Actions({ actions }: { actions: [ReactNode, ReactNode?] }) { +function Actions({ actions }: { actions: [ReactNode, ReactNode?] }) { return (
{actions}
); diff --git a/src/components/heading.tsx b/src/components/heading.tsx new file mode 100644 index 00000000..da655a68 --- /dev/null +++ b/src/components/heading.tsx @@ -0,0 +1,22 @@ +import { Heading as UIKitHeading } from "@stacklok/ui-kit"; +import React, { ComponentProps } from "react"; + +export function PageHeading({ + title, + children, + level, +}: { + level: ComponentProps["level"]; + title: React.ReactNode; + children?: React.ReactNode; +}) { + return ( + + {title} + {children} + + ); +} diff --git a/src/components/page-container.tsx b/src/components/page-container.tsx new file mode 100644 index 00000000..18821485 --- /dev/null +++ b/src/components/page-container.tsx @@ -0,0 +1,9 @@ +import { ReactNode } from "react"; + +export function PageContainer({ children }: { children: ReactNode }) { + return ( +
+ {children} +
+ ); +} diff --git a/src/components/react-query-provider.tsx b/src/components/react-query-provider.tsx index f4a7f3c7..da61b49d 100644 --- a/src/components/react-query-provider.tsx +++ b/src/components/react-query-provider.tsx @@ -43,7 +43,7 @@ export function QueryClientProvider({ children }: { children: ReactNode }) { defaultOptions: { queries: { ...getQueryCacheConfig("no-cache"), - refetchOnMount: true, + refetchOnMount: false, // additional instances of a query shouldn't trigger background refetch refetchOnReconnect: true, refetchOnWindowFocus: true, }, diff --git a/src/components/ui/chat/chat-bubble.tsx b/src/components/ui/chat/chat-bubble.tsx index 73cfa0fd..597da570 100644 --- a/src/components/ui/chat/chat-bubble.tsx +++ b/src/components/ui/chat/chat-bubble.tsx @@ -6,7 +6,7 @@ import { twMerge } from "tailwind-merge"; // ChatBubble const chatBubbleVariant = tv({ - base: "flex gap-2 max-w-[60%] items-end relative group", + base: "flex gap-2 max-w-[60%] items-end relative group text-sm", variants: { variant: { received: "self-start", diff --git a/src/components/ui/sheet.tsx b/src/components/ui/sheet.tsx deleted file mode 100644 index 35ee8813..00000000 --- a/src/components/ui/sheet.tsx +++ /dev/null @@ -1,139 +0,0 @@ -"use client"; - -import * as React from "react"; -import * as SheetPrimitive from "@radix-ui/react-dialog"; - -import { tv } from "tailwind-variants"; -import { twMerge } from "tailwind-merge"; -import { XClose } from "@untitled-ui/icons-react"; - -const Sheet = SheetPrimitive.Root; - -const SheetTrigger = SheetPrimitive.Trigger; - -const SheetClose = SheetPrimitive.Close; - -const SheetPortal = SheetPrimitive.Portal; - -const SheetOverlay = React.forwardRef< - React.ElementRef, - React.ComponentPropsWithoutRef ->(({ className, ...props }, ref) => ( - -)); -SheetOverlay.displayName = SheetPrimitive.Overlay.displayName; - -const sheetVariants = tv({ - base: "fixed z-50 gap-4 bg-base p-6 shadow-lg transition ease-in-out data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:duration-300 data-[state=open]:duration-500", - variants: { - side: { - top: "inset-x-0 top-0 border-b data-[state=closed]:slide-out-to-top data-[state=open]:slide-in-from-top", - bottom: - "inset-x-0 bottom-0 border-t data-[state=closed]:slide-out-to-bottom data-[state=open]:slide-in-from-bottom", - left: "inset-y-0 left-0 h-full w-3/4 border-r data-[state=closed]:slide-out-to-left data-[state=open]:slide-in-from-left sm:max-w-sm", - right: - "inset-y-0 right-0 h-full w-3/4 border-l data-[state=closed]:slide-out-to-right data-[state=open]:slide-in-from-right sm:max-w-sm", - }, - }, - defaultVariants: { - side: "right", - }, -}); - -interface SheetContentProps - extends React.ComponentPropsWithoutRef { - side: "top" | "left" | "right" | "bottom"; -} - -const SheetContent = React.forwardRef< - React.ElementRef, - SheetContentProps ->(({ side = "right", className, children, ...props }, ref) => ( - - - - {children} - - - Close - - - -)); -SheetContent.displayName = SheetPrimitive.Content.displayName; - -const SheetHeader = ({ - className, - ...props -}: React.HTMLAttributes) => ( -
-); -SheetHeader.displayName = "SheetHeader"; - -const SheetFooter = ({ - className, - ...props -}: React.HTMLAttributes) => ( -
-); -SheetFooter.displayName = "SheetFooter"; - -const SheetTitle = React.forwardRef< - React.ElementRef, - React.ComponentPropsWithoutRef ->(({ className, ...props }, ref) => ( - -)); -SheetTitle.displayName = SheetPrimitive.Title.displayName; - -const SheetDescription = React.forwardRef< - React.ElementRef, - React.ComponentPropsWithoutRef ->(({ className, ...props }, ref) => ( - -)); -SheetDescription.displayName = SheetPrimitive.Description.displayName; - -export { - Sheet, - SheetPortal, - SheetOverlay, - SheetTrigger, - SheetClose, - SheetContent, - SheetHeader, - SheetFooter, - SheetTitle, - SheetDescription, -}; diff --git a/src/components/ui/sidebar.tsx b/src/components/ui/sidebar.tsx deleted file mode 100644 index 582b2a36..00000000 --- a/src/components/ui/sidebar.tsx +++ /dev/null @@ -1,552 +0,0 @@ -import * as React from "react"; -import { Slot } from "@radix-ui/react-slot"; - -import { useIsMobile } from "@/hooks/use-mobile"; -import { Sheet, SheetContent } from "@/components/ui/sheet"; -import { Tooltip, TooltipTrigger, Button, Skeleton } from "@stacklok/ui-kit"; -import { twMerge } from "tailwind-merge"; -import { tv } from "tailwind-variants"; -import { LayoutAlt02 } from "@untitled-ui/icons-react"; - -const SIDEBAR_COOKIE_NAME = "sidebar:state"; -const SIDEBAR_COOKIE_MAX_AGE = 60 * 60 * 24 * 7; -const SIDEBAR_WIDTH = "16rem"; -const SIDEBAR_WIDTH_MOBILE = "18rem"; -const SIDEBAR_WIDTH_ICON = "3rem"; -const SIDEBAR_KEYBOARD_SHORTCUT = "b"; - -type SidebarContext = { - state: "expanded" | "collapsed"; - open: boolean; - setOpen: (open: boolean) => void; - openMobile: boolean; - setOpenMobile: (open: boolean) => void; - isMobile: boolean; - toggleSidebar: () => void; -}; - -const SidebarContext = React.createContext(null); - -function useSidebar() { - const context = React.useContext(SidebarContext); - if (!context) { - throw new Error("useSidebar must be used within a SidebarProvider."); - } - - return context; -} - -const SidebarProvider = React.forwardRef< - HTMLDivElement, - React.ComponentProps<"div"> & { - defaultOpen?: boolean; - open?: boolean; - onOpenChange?: (open: boolean) => void; - } ->( - ( - { - defaultOpen = true, - open: openProp, - onOpenChange: setOpenProp, - className, - style, - children, - ...props - }, - ref, - ) => { - const isMobile = useIsMobile(); - const [openMobile, setOpenMobile] = React.useState(false); - - // This is the internal state of the sidebar. - // We use openProp and setOpenProp for control from outside the component. - const [_open, _setOpen] = React.useState(defaultOpen); - const open = openProp ?? _open; - const setOpen = React.useCallback( - (value: boolean | ((value: boolean) => boolean)) => { - const openState = typeof value === "function" ? value(open) : value; - if (setOpenProp) { - setOpenProp(openState); - } else { - _setOpen(openState); - } - - // This sets the cookie to keep the sidebar state. - document.cookie = `${SIDEBAR_COOKIE_NAME}=${openState}; path=/; max-age=${SIDEBAR_COOKIE_MAX_AGE}`; - }, - [setOpenProp, open], - ); - - // Helper to toggle the sidebar. - const toggleSidebar = React.useCallback(() => { - return isMobile - ? setOpenMobile((open) => !open) - : setOpen((open) => !open); - }, [isMobile, setOpen, setOpenMobile]); - - // Adds a keyboard shortcut to toggle the sidebar. - React.useEffect(() => { - const handleKeyDown = (event: KeyboardEvent) => { - if ( - event.key === SIDEBAR_KEYBOARD_SHORTCUT && - (event.metaKey || event.ctrlKey) - ) { - event.preventDefault(); - toggleSidebar(); - } - }; - - window.addEventListener("keydown", handleKeyDown); - return () => window.removeEventListener("keydown", handleKeyDown); - }, [toggleSidebar]); - - // We add a state so that we can do data-state="expanded" or "collapsed". - // This makes it easier to style the sidebar with Tailwind classes. - const state = open ? "expanded" : "collapsed"; - - const contextValue = React.useMemo( - () => ({ - state, - open, - setOpen, - isMobile, - openMobile, - setOpenMobile, - toggleSidebar, - }), - [ - state, - open, - setOpen, - isMobile, - openMobile, - setOpenMobile, - toggleSidebar, - ], - ); - - return ( - -
- {children} -
-
- ); - }, -); -SidebarProvider.displayName = "SidebarProvider"; - -const Sidebar = React.forwardRef< - HTMLDivElement, - React.ComponentProps<"div"> & { - side?: "left" | "right"; - variant?: "sidebar" | "floating" | "inset"; - collapsible?: "offcanvas" | "icon" | "none"; - } ->( - ( - { - side = "left", - variant = "sidebar", - collapsible = "offcanvas", - className, - children, - ...props - }, - ref, - ) => { - const { isMobile, state, openMobile, setOpenMobile } = useSidebar(); - - if (collapsible === "none") { - return ( -
- {children} -
- ); - } - - if (isMobile) { - return ( - - -
{children}
-
-
- ); - } - - return ( -
- {/* This is what handles the sidebar gap on desktop */} -
- -
- ); - }, -); -Sidebar.displayName = "Sidebar"; - -const SidebarTrigger = React.forwardRef< - React.ElementRef, - React.ComponentProps ->(({ onPress, ...props }, ref) => { - const { toggleSidebar } = useSidebar(); - - return ( - - ); -}); -SidebarTrigger.displayName = "SidebarTrigger"; - -const SidebarHeader = React.forwardRef< - HTMLDivElement, - React.ComponentProps<"div"> ->(({ className, ...props }, ref) => { - return ( -
- ); -}); -SidebarHeader.displayName = "SidebarHeader"; - -const SidebarFooter = React.forwardRef< - HTMLDivElement, - React.ComponentProps<"div"> ->(({ className, ...props }, ref) => { - return ( -
- ); -}); -SidebarFooter.displayName = "SidebarFooter"; - -const SidebarContent = React.forwardRef< - HTMLDivElement, - React.ComponentProps<"div"> ->(({ className, ...props }, ref) => { - return ( -
- ); -}); -SidebarContent.displayName = "SidebarContent"; - -const SidebarGroup = React.forwardRef< - HTMLDivElement, - React.ComponentProps<"div"> ->(({ className, ...props }, ref) => { - return ( -
- ); -}); -SidebarGroup.displayName = "SidebarGroup"; - -const SidebarMenu = React.forwardRef< - HTMLUListElement, - React.ComponentProps<"ul"> ->(({ className, ...props }, ref) => ( -
    -)); -SidebarMenu.displayName = "SidebarMenu"; - -const SidebarMenuItem = React.forwardRef< - HTMLLIElement, - React.ComponentProps<"li"> ->(({ className, ...props }, ref) => ( -
  • -)); -SidebarMenuItem.displayName = "SidebarMenuItem"; - -const sidebarMenuButtonVariants = tv({ - base: "peer/menu-button flex w-full items-center gap-2 overflow-hidden rounded-md p-2 text-left text-sm outline-none ring-brand-600 transition-[width,height,padding] hover:bg-brand-25 hover:text-secondary focus-visible:ring-2 active:bg-gray-25 active:text-secondary disabled:pointer-events-none disabled:opacity-50 group-has-[[data-sidebar=menu-action]]/menu-item:pr-8 aria-disabled:pointer-events-none aria-disabled:opacity-50 data-[active=true]:bg-gray-25 data-[active=true]:font-medium data-[active=true]:text-secondary data-[state=open]:hover:bg-brand-25 data-[state=open]:hover:text-secondary group-data-[collapsible=icon]:!size-8 group-data-[collapsible=icon]:!p-2 [&>span:last-child]:truncate [&>svg]:size-4 [&>svg]:shrink-0", - variants: { - variant: { - default: "hover:bg-brand-25 hover:text-secondary", - outline: - "bg-base shadow-[0_0_0_1px_hsl(var(--sidebar-border))] hover:bg-brand-25 hover:text-secondary hover:shadow-[0_0_0_1px_hsl(var(--sidebar-accent))]", - }, - size: { - default: "h-8 text-sm", - sm: "h-7 text-sm", - lg: "h-12 text-sm group-data-[collapsible=icon]:!p-0", - }, - }, - defaultVariants: { - variant: "default", - size: "default", - }, -}); - -const SidebarMenuButton = React.forwardRef< - HTMLButtonElement, - React.ComponentProps<"button"> & { - asChild?: boolean; - isActive?: boolean; - tooltip?: string; - } & { - variant: "default" | "outline"; - size: "default" | "sm" | "lg"; - } ->( - ( - { - asChild = false, - isActive = false, - variant = "default", - size = "default", - tooltip, - className, - ...props - }, - ref, - ) => { - const Comp = asChild ? Slot : "button"; - const { isMobile, state } = useSidebar(); - - const button = ( - - ); - - if (!tooltip) { - return button; - } - - return ( - - {button} - {tooltip} - - ); - }, -); -SidebarMenuButton.displayName = "SidebarMenuButton"; - -const SidebarMenuAction = React.forwardRef< - HTMLButtonElement, - React.ComponentProps<"button"> & { - asChild?: boolean; - showOnHover?: boolean; - } ->(({ className, asChild = false, showOnHover = false, ...props }, ref) => { - const Comp = asChild ? Slot : "button"; - - return ( - svg]:size-4 [&>svg]:shrink-0", - // Increases the hit area of the button on mobile. - "after:absolute after:-inset-2 after:md:hidden", - "peer-data-[size=sm]/menu-button:top-1", - "peer-data-[size=default]/menu-button:top-1.5", - "peer-data-[size=lg]/menu-button:top-2.5", - "group-data-[collapsible=icon]:hidden", - showOnHover && - "group-focus-within/menu-item:opacity-100 group-hover/menu-item:opacity-100 data-[state=open]:opacity-100 md:opacity-0", - className, - )} - {...props} - /> - ); -}); -SidebarMenuAction.displayName = "SidebarMenuAction"; - -const SidebarMenuBadge = React.forwardRef< - HTMLDivElement, - React.ComponentProps<"div"> ->(({ className, ...props }, ref) => ( -
    -)); -SidebarMenuBadge.displayName = "SidebarMenuBadge"; - -const SidebarMenuSkeleton = React.forwardRef< - HTMLDivElement, - React.ComponentProps<"div"> & { - showIcon?: boolean; - } ->(({ className, showIcon = false, ...props }, ref) => { - // Random width between 50 to 90%. - const width = React.useMemo(() => { - return `${Math.floor(Math.random() * 40) + 50}%`; - }, []); - - return ( -
    - {showIcon && ( - - )} - -
    - ); -}); -SidebarMenuSkeleton.displayName = "SidebarMenuSkeleton"; - -export { - Sidebar, - SidebarContent, - SidebarFooter, - SidebarGroup, - SidebarHeader, - SidebarMenu, - SidebarMenuAction, - SidebarMenuBadge, - SidebarMenuButton, - SidebarMenuItem, - SidebarMenuSkeleton, - SidebarProvider, - SidebarTrigger, - useSidebar, -}; diff --git a/src/features/alerts/components/__tests__/tabs-alerts.test.tsx b/src/features/alerts/components/__tests__/tabs-alerts.test.tsx deleted file mode 100644 index da4cd1d6..00000000 --- a/src/features/alerts/components/__tests__/tabs-alerts.test.tsx +++ /dev/null @@ -1,70 +0,0 @@ -import { server } from "@/mocks/msw/node"; -import { http, HttpResponse } from "msw"; - -import { render, waitFor } from "@/lib/test-utils"; -import { TabsAlerts } from "../tabs-alerts"; -import { mswEndpoint } from "@/test/msw-endpoint"; -import { mockAlert } from "@/mocks/msw/mockers/alert.mock"; - -test("shows correct count of all packages", async () => { - server.use( - http.get(mswEndpoint("/api/v1/workspaces/:workspace_name/alerts"), () => { - return HttpResponse.json([ - ...Array.from({ length: 13 }).map(() => mockAlert({ type: "secret" })), - ...Array.from({ length: 13 }).map(() => - mockAlert({ type: "malicious" }), - ), - ]); - }), - ); - - const { getByRole } = render( - -
    foo
    -
    , - ); - - await waitFor(() => { - expect(getByRole("tab", { name: /all/i })).toHaveTextContent("26"); - }); -}); - -test("shows correct count of malicious packages", async () => { - server.use( - http.get(mswEndpoint("/api/v1/workspaces/:workspace_name/alerts"), () => { - return HttpResponse.json( - Array.from({ length: 13 }).map(() => mockAlert({ type: "malicious" })), - ); - }), - ); - - const { getByRole } = render( - -
    foo
    -
    , - ); - - await waitFor(() => { - expect(getByRole("tab", { name: /malicious/i })).toHaveTextContent("13"); - }); -}); - -test("shows correct count of secret packages", async () => { - server.use( - http.get(mswEndpoint("/api/v1/workspaces/:workspace_name/alerts"), () => { - return HttpResponse.json( - Array.from({ length: 13 }).map(() => mockAlert({ type: "secret" })), - ); - }), - ); - - const { getByRole } = render( - -
    foo
    -
    , - ); - - await waitFor(() => { - expect(getByRole("tab", { name: /secrets/i })).toHaveTextContent("13"); - }); -}); diff --git a/src/features/alerts/components/search-field-alerts.tsx b/src/features/alerts/components/search-field-alerts.tsx deleted file mode 100644 index 7a1a7a7b..00000000 --- a/src/features/alerts/components/search-field-alerts.tsx +++ /dev/null @@ -1,32 +0,0 @@ -import { - FieldGroup, - Input, - SearchField, - SearchFieldClearButton, -} from "@stacklok/ui-kit"; -import { useAlertsFilterSearchParams } from "../hooks/use-alerts-filter-search-params"; -import { SearchMd } from "@untitled-ui/icons-react"; - -export function SearchFieldAlerts({ className }: { className?: string }) { - const { setSearch, state } = useAlertsFilterSearchParams(); - - return ( - setSearch(value.toLowerCase().trim())} - className={className} - > - - } - /> - - - - ); -} diff --git a/src/features/alerts/components/table-alerts.tsx b/src/features/alerts/components/table-alerts.tsx deleted file mode 100644 index df58e066..00000000 --- a/src/features/alerts/components/table-alerts.tsx +++ /dev/null @@ -1,200 +0,0 @@ -import { formatDistanceToNow } from "date-fns"; -import { - Cell, - Column, - Row, - Table, - TableBody, - TableHeader, - Button, - ResizableTableContainer, -} from "@stacklok/ui-kit"; -import { AlertConversation, QuestionType } from "@/api/generated"; -import { - sanitizeQuestionPrompt, - parsingPromptText, - getIssueDetectedType, -} from "@/lib/utils"; -import { useClientSidePagination } from "@/hooks/useClientSidePagination"; -import { TableAlertTokenUsage } from "./table-alert-token-usage"; - -import { useQueryGetWorkspaceAlertTable } from "../hooks/use-query-get-workspace-alerts-table"; -import { useAlertsFilterSearchParams } from "../hooks/use-alerts-filter-search-params"; -import { Key01, PackageX } from "@untitled-ui/icons-react"; -import { TableAlertsEmptyState } from "./table-alerts-empty-state"; -import { ComponentProps } from "react"; -import { hrefs } from "@/lib/hrefs"; - -const getTitle = (alert: AlertConversation) => { - const prompt = alert.conversation; - const title = parsingPromptText( - sanitizeQuestionPrompt({ - question: prompt.question_answers?.[0]?.question.message ?? "", - answer: prompt.question_answers?.[0]?.answer?.message ?? "", - }), - prompt.conversation_timestamp, - ); - - return title; -}; - -function TypeCellContent({ alert }: { alert: AlertConversation }) { - const conversationType = alert.conversation.type; - - switch (conversationType) { - case QuestionType.CHAT: - return "Chat"; - case QuestionType.FIM: - return "Code Suggestion"; - default: - return "Unknown"; - } -} - -function IssueDetectedCellContent({ alert }: { alert: AlertConversation }) { - const issueDetected = getIssueDetectedType(alert); - - switch (issueDetected) { - case "leaked_secret": - return ( - <> - - Blocked secret exposure - - ); - case "malicious_package": - return ( - <> - - Blocked malicious package - - ); - default: - return ""; - } -} - -type ColumnId = "time" | "type" | "event" | "issue_detected" | "token_usage"; - -type Column = { id: ColumnId } & Omit, "id">; - -const COLUMNS: Column[] = [ - { - id: "time", - isRowHeader: true, - children: "Time", - width: 200, - }, - { - id: "type", - children: "Type", - width: 150, - }, - { - id: "event", - children: "Event", - }, - { - id: "issue_detected", - children: "Issue detected", - width: 325, - }, - { - id: "token_usage", - children: "Token usage", - width: 200, - }, -]; - -function CellRenderer({ - column, - row, -}: { - column: Column; - row: AlertConversation; -}) { - switch (column.id) { - case "time": - return ( - - {formatDistanceToNow(new Date(row.timestamp), { - addSuffix: true, - })} - - ); - case "type": - return ; - case "event": - return getTitle(row); - case "issue_detected": - return ( -
    - -
    - ); - case "token_usage": - return ; - - default: - return column.id satisfies never; - } -} - -export function TableAlerts() { - const { state, prevPage, nextPage } = useAlertsFilterSearchParams(); - - const { data = [] } = useQueryGetWorkspaceAlertTable(); - - const { dataView, hasNextPage, hasPreviousPage } = useClientSidePagination( - data, - state.page, - 15, - ); - - return ( - <> - - - - {(column) => } - - } - items={dataView} - > - {(row) => ( - - {(column) => ( - - - - )} - - )} - -
    -
    - - {hasNextPage || hasPreviousPage ? ( -
    -
    - - -
    -
    - ) : null} - - ); -} diff --git a/src/features/alerts/hooks/use-query-get-workspace-alerts-table.ts b/src/features/alerts/hooks/use-query-get-workspace-alerts-table.ts deleted file mode 100644 index 42094205..00000000 --- a/src/features/alerts/hooks/use-query-get-workspace-alerts-table.ts +++ /dev/null @@ -1,43 +0,0 @@ -import { V1GetWorkspaceAlertsResponse } from "@/api/generated"; -import { useQueryGetWorkspaceAlerts } from "./use-query-get-workspace-alerts"; -import { useCallback } from "react"; -import { - AlertsFilterView, - useAlertsFilterSearchParams, -} from "./use-alerts-filter-search-params"; -import { multiFilter } from "@/lib/multi-filter"; -import { isAlertCritical } from "../lib/is-alert-critical"; -import { isAlertMalicious } from "../lib/is-alert-malicious"; -import { isAlertSecret } from "../lib/is-alert-secret"; -import { doesAlertIncludeSearch } from "../lib/does-alert-include-search"; -import { isAlertConversation } from "../lib/is-alert-conversation"; - -const FILTER: Record< - AlertsFilterView, - (alert: V1GetWorkspaceAlertsResponse[number]) => boolean -> = { - all: () => true, - malicious: isAlertMalicious, - secrets: isAlertSecret, -}; - -export function useQueryGetWorkspaceAlertTable() { - const { state } = useAlertsFilterSearchParams(); - - // NOTE: This needs to be a stable function reference to enable memo-isation - // of the select operation on each React re-render. - const select = useCallback( - (data: V1GetWorkspaceAlertsResponse) => { - return multiFilter(data, [ - isAlertCritical, - isAlertConversation, - FILTER[state.view], - ]).filter((alert) => doesAlertIncludeSearch(alert, state.search ?? null)); - }, - [state.search, state.view], - ); - - return useQueryGetWorkspaceAlerts({ - select, - }); -} diff --git a/src/features/alerts/lib/does-alert-include-search.ts b/src/features/alerts/lib/does-alert-include-search.ts deleted file mode 100644 index 8b91b89b..00000000 --- a/src/features/alerts/lib/does-alert-include-search.ts +++ /dev/null @@ -1,38 +0,0 @@ -import { - AlertConversation, - V1GetWorkspaceAlertsResponse, -} from "@/api/generated"; - -export function doesAlertIncludeSearch( - alert: V1GetWorkspaceAlertsResponse[number], - searchQuery: string | null, -): alert is AlertConversation { - if (alert == null) return false; - if (searchQuery === null) return true; - - const trigger_type: string = alert.trigger_type; - const trigger_string: string | null = - typeof alert.trigger_string === "string" ? alert.trigger_string : null; - - let malicious_pkg_name: string | null = null; - let malicious_pkg_type: string | null = null; - - if ( - alert.trigger_string !== null && - typeof alert.trigger_string === "object" && - "name" in alert.trigger_string && - typeof alert.trigger_string.name === "string" && - "type" in alert.trigger_string && - typeof alert.trigger_string.type === "string" - ) { - malicious_pkg_name = alert.trigger_string.name; - malicious_pkg_type = alert.trigger_string.type; - } - - return [ - trigger_type, - trigger_string, - malicious_pkg_name, - malicious_pkg_type, - ].some((i) => i?.toLowerCase().includes(searchQuery)); -} diff --git a/src/features/alerts/lib/is-alert-conversation.ts b/src/features/alerts/lib/is-alert-conversation.ts deleted file mode 100644 index fdcf23cc..00000000 --- a/src/features/alerts/lib/is-alert-conversation.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { - AlertConversation, - V1GetWorkspaceAlertsResponse, -} from "@/api/generated"; - -export function isAlertConversation( - alert: V1GetWorkspaceAlertsResponse[number], -): alert is AlertConversation { - return Boolean( - alert?.conversation.question_answers.every( - (item) => item.answer && item.question, - ), - ); -} diff --git a/src/features/alerts/lib/is-alert-malicious.ts b/src/features/alerts/lib/is-alert-malicious.ts deleted file mode 100644 index 457c36e4..00000000 --- a/src/features/alerts/lib/is-alert-malicious.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { Alert, AlertConversation } from "@/api/generated"; - -export function isAlertMalicious( - alert: Alert | AlertConversation | null, -): alert is AlertConversation { - return ( - alert?.trigger_category === "critical" && - alert.trigger_string !== null && - typeof alert.trigger_string === "object" && - "status" in alert.trigger_string && - alert.trigger_string.status === "malicious" - ); -} diff --git a/src/features/alerts/lib/is-alert-secret.ts b/src/features/alerts/lib/is-alert-secret.ts deleted file mode 100644 index 7acc6612..00000000 --- a/src/features/alerts/lib/is-alert-secret.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { Alert, AlertConversation } from "@/api/generated"; - -export function isAlertSecret(alert: Alert | AlertConversation | null) { - return ( - alert?.trigger_category === "critical" && - alert.trigger_type === "codegate-secrets" - ); -} diff --git a/src/features/alerts/components/__tests__/alerts-summary-malicious-pkg.test.tsx b/src/features/dashboard-alerts/components/__tests__/alerts-summary-malicious-pkg.test.tsx similarity index 100% rename from src/features/alerts/components/__tests__/alerts-summary-malicious-pkg.test.tsx rename to src/features/dashboard-alerts/components/__tests__/alerts-summary-malicious-pkg.test.tsx diff --git a/src/features/alerts/components/__tests__/alerts-summary-secrets.test.tsx b/src/features/dashboard-alerts/components/__tests__/alerts-summary-secrets.test.tsx similarity index 100% rename from src/features/alerts/components/__tests__/alerts-summary-secrets.test.tsx rename to src/features/dashboard-alerts/components/__tests__/alerts-summary-secrets.test.tsx diff --git a/src/features/alerts/components/__tests__/alerts-summary-workspace-token-usage.test.tsx b/src/features/dashboard-alerts/components/__tests__/alerts-summary-workspace-token-usage.test.tsx similarity index 100% rename from src/features/alerts/components/__tests__/alerts-summary-workspace-token-usage.test.tsx rename to src/features/dashboard-alerts/components/__tests__/alerts-summary-workspace-token-usage.test.tsx diff --git a/src/features/alerts/components/alerts-summary-malicious-pkg.tsx b/src/features/dashboard-alerts/components/alerts-summary-malicious-pkg.tsx similarity index 99% rename from src/features/alerts/components/alerts-summary-malicious-pkg.tsx rename to src/features/dashboard-alerts/components/alerts-summary-malicious-pkg.tsx index f497c399..a1bd424b 100644 --- a/src/features/alerts/components/alerts-summary-malicious-pkg.tsx +++ b/src/features/dashboard-alerts/components/alerts-summary-malicious-pkg.tsx @@ -1,6 +1,7 @@ import { PackageX } from "@untitled-ui/icons-react"; -import { AlertsSummary } from "./alerts-summary"; + import { useQueryGetWorkspaceAlertsMaliciousPkg } from "../hooks/use-query-get-workspace-alerts-malicious-pkg"; +import { AlertsSummary } from "./alerts-summary"; export function AlertsSummaryMaliciousPkg() { const { data = [], isPending } = useQueryGetWorkspaceAlertsMaliciousPkg(); diff --git a/src/features/alerts/components/alerts-summary-secrets.tsx b/src/features/dashboard-alerts/components/alerts-summary-secrets.tsx similarity index 100% rename from src/features/alerts/components/alerts-summary-secrets.tsx rename to src/features/dashboard-alerts/components/alerts-summary-secrets.tsx diff --git a/src/features/alerts/components/alerts-summary-workspace-token-usage.tsx b/src/features/dashboard-alerts/components/alerts-summary-workspace-token-usage.tsx similarity index 100% rename from src/features/alerts/components/alerts-summary-workspace-token-usage.tsx rename to src/features/dashboard-alerts/components/alerts-summary-workspace-token-usage.tsx diff --git a/src/features/alerts/components/alerts-summary.tsx b/src/features/dashboard-alerts/components/alerts-summary.tsx similarity index 100% rename from src/features/alerts/components/alerts-summary.tsx rename to src/features/dashboard-alerts/components/alerts-summary.tsx diff --git a/src/features/alerts/hooks/use-query-get-workspace-alerts-malicious-pkg.ts b/src/features/dashboard-alerts/hooks/use-query-get-workspace-alerts-malicious-pkg.ts similarity index 80% rename from src/features/alerts/hooks/use-query-get-workspace-alerts-malicious-pkg.ts rename to src/features/dashboard-alerts/hooks/use-query-get-workspace-alerts-malicious-pkg.ts index d3c4e2ff..722cd42a 100644 --- a/src/features/alerts/hooks/use-query-get-workspace-alerts-malicious-pkg.ts +++ b/src/features/dashboard-alerts/hooks/use-query-get-workspace-alerts-malicious-pkg.ts @@ -1,8 +1,8 @@ import { V1GetWorkspaceAlertsResponse } from "@/api/generated"; -import { isAlertMalicious } from "../lib/is-alert-malicious"; +import { isAlertMalicious } from "../../../lib/is-alert-malicious"; import { useQueryGetWorkspaceAlerts } from "./use-query-get-workspace-alerts"; import { multiFilter } from "@/lib/multi-filter"; -import { isAlertCritical } from "../lib/is-alert-critical"; +import { isAlertCritical } from "../../../lib/is-alert-critical"; // NOTE: This needs to be a stable function reference to enable memo-isation of // the select operation on each React re-render. diff --git a/src/features/alerts/hooks/use-query-get-workspace-alerts-secrets.ts b/src/features/dashboard-alerts/hooks/use-query-get-workspace-alerts-secrets.ts similarity index 81% rename from src/features/alerts/hooks/use-query-get-workspace-alerts-secrets.ts rename to src/features/dashboard-alerts/hooks/use-query-get-workspace-alerts-secrets.ts index 5e617173..9311bd8d 100644 --- a/src/features/alerts/hooks/use-query-get-workspace-alerts-secrets.ts +++ b/src/features/dashboard-alerts/hooks/use-query-get-workspace-alerts-secrets.ts @@ -1,8 +1,8 @@ import { V1GetWorkspaceAlertsResponse } from "@/api/generated"; -import { isAlertSecret } from "../lib/is-alert-secret"; +import { isAlertSecret } from "../../../lib/is-alert-secret"; import { useQueryGetWorkspaceAlerts } from "./use-query-get-workspace-alerts"; import { multiFilter } from "@/lib/multi-filter"; -import { isAlertCritical } from "../lib/is-alert-critical"; +import { isAlertCritical } from "../../../lib/is-alert-critical"; // NOTE: This needs to be a stable function reference to enable memo-isation of // the select operation on each React re-render diff --git a/src/features/alerts/hooks/use-query-get-workspace-alerts.ts b/src/features/dashboard-alerts/hooks/use-query-get-workspace-alerts.ts similarity index 91% rename from src/features/alerts/hooks/use-query-get-workspace-alerts.ts rename to src/features/dashboard-alerts/hooks/use-query-get-workspace-alerts.ts index 13cdca5c..fe980f6e 100644 --- a/src/features/alerts/hooks/use-query-get-workspace-alerts.ts +++ b/src/features/dashboard-alerts/hooks/use-query-get-workspace-alerts.ts @@ -3,7 +3,7 @@ import { V1GetWorkspaceAlertsResponse, } from "@/api/generated"; import { v1GetWorkspaceAlertsOptions } from "@/api/generated/@tanstack/react-query.gen"; -import { useActiveWorkspaceName } from "@/features/workspace/hooks/use-active-workspace-name"; +import { useQueryActiveWorkspaceName } from "@/hooks/use-query-active-workspace-name"; import { getQueryCacheConfig } from "@/lib/react-query-utils"; import { useQuery } from "@tanstack/react-query"; @@ -18,7 +18,7 @@ export function useQueryGetWorkspaceAlerts({ isFetching: isWorkspaceFetching, isLoading: isWorkspaceLoading, isRefetching: isWorkspaceRefetching, - } = useActiveWorkspaceName(); + } = useQueryActiveWorkspaceName(); const options: V1GetWorkspaceAlertsData = { path: { diff --git a/src/features/alerts/hooks/use-query-get-workspace-token-usage.ts b/src/features/dashboard-alerts/hooks/use-query-get-workspace-token-usage.ts similarity index 79% rename from src/features/alerts/hooks/use-query-get-workspace-token-usage.ts rename to src/features/dashboard-alerts/hooks/use-query-get-workspace-token-usage.ts index a06e8abd..37384acb 100644 --- a/src/features/alerts/hooks/use-query-get-workspace-token-usage.ts +++ b/src/features/dashboard-alerts/hooks/use-query-get-workspace-token-usage.ts @@ -3,7 +3,7 @@ import { V1GetWorkspaceTokenUsageResponse, } from "@/api/generated"; import { v1GetWorkspaceTokenUsageOptions } from "@/api/generated/@tanstack/react-query.gen"; -import { useActiveWorkspaceName } from "@/features/workspace/hooks/use-active-workspace-name"; +import { useQueryActiveWorkspaceName } from "@/hooks/use-query-active-workspace-name"; import { useQuery } from "@tanstack/react-query"; export function useQueryGetWorkspaceTokenUsage< @@ -13,7 +13,7 @@ export function useQueryGetWorkspaceTokenUsage< }: { select?: (data: V1GetWorkspaceTokenUsageResponse) => T; } = {}) { - const { data: activeWorkspaceName } = useActiveWorkspaceName(); + const { data: activeWorkspaceName } = useQueryActiveWorkspaceName(); const options: V1GetWorkspaceTokenUsageData = { path: { diff --git a/src/features/dashboard-messages/components/__tests__/table-messages.alerts.test.tsx b/src/features/dashboard-messages/components/__tests__/table-messages.alerts.test.tsx new file mode 100644 index 00000000..54fd00ce --- /dev/null +++ b/src/features/dashboard-messages/components/__tests__/table-messages.alerts.test.tsx @@ -0,0 +1,82 @@ +import {} from "vitest"; +import { TableMessages } from "../table-messages"; +import { render, screen, waitFor } from "@/lib/test-utils"; +import { server } from "@/mocks/msw/node"; +import { http, HttpResponse } from "msw"; + +import { mswEndpoint } from "@/test/msw-endpoint"; +import { mockConversation } from "@/mocks/msw/mockers/conversation.mock"; + +it("shows zero in alerts counts when no alerts", async () => { + server.use( + http.get(mswEndpoint("/api/v1/workspaces/:workspace_name/messages"), () => + HttpResponse.json([ + mockConversation({ + alertsConfig: { numAlerts: 0 }, + }), + ]) + ) + ); + render(); + + await waitFor(() => { + expect(screen.queryByText(/loading.../i)).not.toBeInTheDocument(); + }); + + expect( + screen.getByRole("button", { + name: /malicious packages count/i, + }) + ).toHaveTextContent("0"); + expect( + screen.getByRole("button", { + name: /secrets count/i, + }) + ).toHaveTextContent("0"); +}); + +it("shows count of malicious alerts in row", async () => { + server.use( + http.get(mswEndpoint("/api/v1/workspaces/:workspace_name/messages"), () => + HttpResponse.json([ + mockConversation({ + alertsConfig: { numAlerts: 10, type: "malicious" }, + }), + ]) + ) + ); + render(); + + await waitFor(() => { + expect(screen.queryByText(/loading.../i)).not.toBeInTheDocument(); + }); + + expect( + screen.getByRole("button", { + name: /malicious packages count/i, + }) + ).toHaveTextContent("10"); +}); + +it("shows count of secret alerts in row", async () => { + server.use( + http.get(mswEndpoint("/api/v1/workspaces/:workspace_name/messages"), () => + HttpResponse.json([ + mockConversation({ + alertsConfig: { numAlerts: 10, type: "secret" }, + }), + ]) + ) + ); + render(); + + await waitFor(() => { + expect(screen.queryByText(/loading.../i)).not.toBeInTheDocument(); + }); + + expect( + screen.getByRole("button", { + name: /secrets count/i, + }) + ).toHaveTextContent("10"); +}); diff --git a/src/features/alerts/components/__tests__/table-alerts.empty-state.test.tsx b/src/features/dashboard-messages/components/__tests__/table-messages.empty-state.test.tsx similarity index 83% rename from src/features/alerts/components/__tests__/table-alerts.empty-state.test.tsx rename to src/features/dashboard-messages/components/__tests__/table-messages.empty-state.test.tsx index 1db06c0a..c48c30a1 100644 --- a/src/features/alerts/components/__tests__/table-alerts.empty-state.test.tsx +++ b/src/features/dashboard-messages/components/__tests__/table-messages.empty-state.test.tsx @@ -4,12 +4,11 @@ import { server } from "@/mocks/msw/node"; import { emptyStateStrings } from "../../constants/strings"; import { useSearchParams } from "react-router-dom"; import { delay, http, HttpHandler, HttpResponse } from "msw"; - -import { AlertsFilterView } from "../../hooks/use-alerts-filter-search-params"; -import { TableAlerts } from "../table-alerts"; +import { mockAlert } from "../../../../mocks/msw/mockers/alert.mock"; +import { AlertsFilterView } from "../../hooks/use-messages-filter-search-params"; +import { TableMessages } from "../table-messages"; import { hrefs } from "@/lib/hrefs"; import { mswEndpoint } from "@/test/msw-endpoint"; -import { mockAlert } from "@/mocks/msw/mockers/alert.mock"; enum IllustrationTestId { ALERT = "illustration-alert", @@ -49,7 +48,7 @@ type TestCase = { vi.mock("react-router-dom", async () => { const original = await vi.importActual( - "react-router-dom", + "react-router-dom" ); return { ...original, @@ -60,7 +59,7 @@ vi.mock("react-router-dom", async () => { vi.mock("@stacklok/ui-kit", async () => { const original = await vi.importActual( - "@stacklok/ui-kit", + "@stacklok/ui-kit" ); return { ...original, @@ -113,9 +112,12 @@ const TEST_CASES: TestCase[] = [ workspaces: [], }); }), - http.get(mswEndpoint("/api/v1/workspaces/:workspace_name/alerts"), () => { - return HttpResponse.json([]); - }), + http.get( + mswEndpoint("/api/v1/workspaces/:workspace_name/messages"), + () => { + return HttpResponse.json([]); + } + ), ], searchParams: { search: null, @@ -152,11 +154,14 @@ const TEST_CASES: TestCase[] = [ workspaces: [], }); }), - http.get(mswEndpoint("/api/v1/workspaces/:workspace_name/alerts"), () => { - return HttpResponse.json( - Array.from({ length: 10 }, () => mockAlert({ type: "malicious" })), - ); - }), + http.get( + mswEndpoint("/api/v1/workspaces/:workspace_name/messages"), + () => { + return HttpResponse.json( + Array.from({ length: 10 }, () => mockAlert({ type: "malicious" })) + ); + } + ), ], searchParams: { search: "foo-bar", view: AlertsFilterView.ALL }, expected: { @@ -193,17 +198,20 @@ const TEST_CASES: TestCase[] = [ workspaces: [], }); }), - http.get(mswEndpoint("/api/v1/workspaces/:workspace_name/alerts"), () => { - return HttpResponse.json([]); - }), + http.get( + mswEndpoint("/api/v1/workspaces/:workspace_name/messages"), + () => { + return HttpResponse.json([]); + } + ), ], searchParams: { search: null, view: AlertsFilterView.ALL, }, expected: { - title: emptyStateStrings.title.noAlertsFoundWorkspace, - body: emptyStateStrings.body.alertsWillShowUpWhenWorkspace, + title: emptyStateStrings.title.noMessagesWorkspace, + body: emptyStateStrings.body.messagesWillShowUpWhenWorkspace, illustrationTestId: IllustrationTestId.DONE, actions: [ { @@ -236,11 +244,14 @@ const TEST_CASES: TestCase[] = [ workspaces: [], }); }), - http.get(mswEndpoint("/api/v1/workspaces/:workspace_name/alerts"), () => { - return HttpResponse.json( - Array.from({ length: 10 }).map(() => mockAlert({ type: "secret" })), - ); - }), + http.get( + mswEndpoint("/api/v1/workspaces/:workspace_name/messages"), + () => { + return HttpResponse.json( + Array.from({ length: 10 }).map(() => mockAlert({ type: "secret" })) + ); + } + ), ], searchParams: { view: AlertsFilterView.MALICIOUS, @@ -275,13 +286,16 @@ const TEST_CASES: TestCase[] = [ workspaces: [], }); }), - http.get(mswEndpoint("/api/v1/workspaces/:workspace_name/alerts"), () => { - return HttpResponse.json( - Array.from({ length: 10 }).map(() => - mockAlert({ type: "malicious" }), - ), - ); - }), + http.get( + mswEndpoint("/api/v1/workspaces/:workspace_name/messages"), + () => { + return HttpResponse.json( + Array.from({ length: 10 }).map(() => + mockAlert({ type: "malicious" }) + ) + ); + } + ), ], searchParams: { view: AlertsFilterView.SECRETS, @@ -307,11 +321,11 @@ test.each(TEST_CASES)("$testDescription", async (testCase) => { () => {}, ]); - const { getByText, getByRole, getByTestId } = render(); + const { getByText, getByRole, getByTestId } = render(); await waitFor(() => { expect( - getByRole("heading", { level: 4, name: testCase.expected.title }), + getByRole("heading", { level: 4, name: testCase.expected.title }) ).toBeVisible(); expect(getByText(testCase.expected.body)).toBeVisible(); expect(getByTestId(testCase.expected.illustrationTestId)).toBeVisible(); diff --git a/src/features/dashboard-messages/components/__tests__/table-messages.pagination.test.tsx b/src/features/dashboard-messages/components/__tests__/table-messages.pagination.test.tsx new file mode 100644 index 00000000..837b5bd3 --- /dev/null +++ b/src/features/dashboard-messages/components/__tests__/table-messages.pagination.test.tsx @@ -0,0 +1,69 @@ +import {} from "vitest"; +import { TableMessages } from "../table-messages"; +import { render, screen, waitFor, within } from "@/lib/test-utils"; +import { server } from "@/mocks/msw/node"; +import { http, HttpResponse } from "msw"; + +import { mswEndpoint } from "@/test/msw-endpoint"; +import { mockConversation } from "@/mocks/msw/mockers/conversation.mock"; +import userEvent from "@testing-library/user-event"; + +it("only displays a limited number of items in the table", async () => { + server.use( + http.get(mswEndpoint("/api/v1/workspaces/:workspace_name/messages"), () => { + return HttpResponse.json( + Array.from({ length: 30 }).map(() => mockConversation()) + ); + }) + ); + + render(); + + await waitFor(() => { + expect( + within(screen.getByTestId("messages-table")).getAllByRole("row") + ).toHaveLength(16); + }); +}); + +it("allows pagination", async () => { + server.use( + http.get(mswEndpoint("/api/v1/workspaces/:workspace_name/messages"), () => { + return HttpResponse.json( + Array.from({ length: 35 }).map(() => mockConversation()) + ); + }) + ); + + render(); + + await waitFor( + async () => { + await userEvent.click(screen.getByRole("button", { name: /next/i })); + + expect( + within(screen.getByTestId("messages-table")).getAllByRole("row").length + ).toBeLessThan(16); + }, + { timeout: 5000 } + ); + + // on the last page, we cannot go further + expect(screen.getByRole("button", { name: /next/i })).toBeDisabled(); + + await userEvent.click(screen.getByRole("button", { name: /previous/i })); + expect(screen.getByRole("button", { name: /previous/i })).toBeEnabled(); + expect(screen.getByRole("button", { name: /next/i })).toBeEnabled(); + + await waitFor(async () => { + await userEvent.click(screen.getByRole("button", { name: /previous/i })); + + // once we reach the first page, we cannot paginate backwards anymore + expect(screen.getByRole("button", { name: /previous/i })).toBeDisabled(); + expect(screen.getByRole("button", { name: /next/i })).toBeEnabled(); + + expect( + within(screen.getByTestId("messages-table")).getAllByRole("row").length + ).toEqual(16); + }); +}); diff --git a/src/features/dashboard-messages/components/__tests__/table-messages.test.tsx b/src/features/dashboard-messages/components/__tests__/table-messages.test.tsx new file mode 100644 index 00000000..2bbd1d80 --- /dev/null +++ b/src/features/dashboard-messages/components/__tests__/table-messages.test.tsx @@ -0,0 +1,18 @@ +import { it, expect } from "vitest"; + +import { render, screen, waitFor } from "@/lib/test-utils"; +import { TABLE_MESSAGES_COLUMNS } from "../../constants/table-messages-columns"; + +import { TableMessages } from "../table-messages"; + +it.each(TABLE_MESSAGES_COLUMNS)("contains $children header", async (column) => { + render(); + + await waitFor(() => { + expect( + screen.getByRole("columnheader", { + name: column.children as string, + }) + ).toBeVisible(); + }); +}); diff --git a/src/features/alerts/components/__tests__/table-alerts.test.tsx b/src/features/dashboard-messages/components/__tests__/table-messages.token-usage.test.tsx similarity index 64% rename from src/features/alerts/components/__tests__/table-alerts.test.tsx rename to src/features/dashboard-messages/components/__tests__/table-messages.token-usage.test.tsx index 404932dc..2b949ba3 100644 --- a/src/features/alerts/components/__tests__/table-alerts.test.tsx +++ b/src/features/dashboard-messages/components/__tests__/table-messages.token-usage.test.tsx @@ -1,12 +1,11 @@ import {} from "vitest"; -import { TableAlerts } from "../table-alerts"; +import { TableMessages } from "../table-messages"; import { render, waitFor } from "@/lib/test-utils"; import { server } from "@/mocks/msw/node"; import { http, HttpResponse } from "msw"; +import { TOKEN_USAGE_AGG } from "../../../../mocks/msw/mockers/token-usage.mock"; import { formatNumberCompact } from "@/lib/format-number"; import { mswEndpoint } from "@/test/msw-endpoint"; -import { mockAlert } from "@/mocks/msw/mockers/alert.mock"; -import { TOKEN_USAGE_AGG } from "@/mocks/msw/mockers/token-usage.mock"; import { mockConversation } from "@/mocks/msw/mockers/conversation.mock"; vi.mock("@untitled-ui/icons-react", async () => { @@ -30,19 +29,12 @@ const OUTPUT_TOKENS = test("renders token usage cell correctly", async () => { server.use( - http.get(mswEndpoint("/api/v1/workspaces/:workspace_name/alerts"), () => { - return HttpResponse.json([ - { - ...mockAlert({ type: "malicious" }), - conversation: mockConversation({ - withTokenUsage: true, - }), - }, - ]); + http.get(mswEndpoint("/api/v1/workspaces/:workspace_name/messages"), () => { + return HttpResponse.json([mockConversation({ withTokenUsage: true })]); }), ); - const { getByRole, getByTestId, queryByText } = render(); + const { getByRole, getByTestId, queryByText } = render(); await waitFor(() => { expect(queryByText(/loading.../i)).not.toBeInTheDocument(); @@ -60,19 +52,12 @@ test("renders token usage cell correctly", async () => { test("renders N/A when token usage is missing", async () => { server.use( - http.get(mswEndpoint("/api/v1/workspaces/:workspace_name/alerts"), () => { - return HttpResponse.json([ - { - ...mockAlert({ type: "malicious" }), - conversation: mockConversation({ - withTokenUsage: false, - }), - }, - ]); + http.get(mswEndpoint("/api/v1/workspaces/:workspace_name/messages"), () => { + return HttpResponse.json([mockConversation({ withTokenUsage: false })]); }), ); - const { getByText, queryByText } = render(); + const { getByText, queryByText } = render(); await waitFor(() => { expect(queryByText(/loading.../i)).not.toBeInTheDocument(); diff --git a/src/features/dashboard-messages/components/__tests__/tabs-messages.test.tsx b/src/features/dashboard-messages/components/__tests__/tabs-messages.test.tsx new file mode 100644 index 00000000..b78be57f --- /dev/null +++ b/src/features/dashboard-messages/components/__tests__/tabs-messages.test.tsx @@ -0,0 +1,95 @@ +import { server } from "@/mocks/msw/node"; +import { http, HttpResponse } from "msw"; +import { render, waitFor } from "@/lib/test-utils"; +import { TabsMessages } from "../tabs-messages"; +import { mswEndpoint } from "@/test/msw-endpoint"; +import { mockConversation } from "@/mocks/msw/mockers/conversation.mock"; + +test("shows correct count of all packages", async () => { + server.use( + http.get(mswEndpoint("/api/v1/workspaces/:workspace_name/messages"), () => { + return HttpResponse.json([ + ...Array.from({ length: 13 }).map(() => + mockConversation({ + alertsConfig: { + type: "secret", + numAlerts: 1, + }, + }) + ), + ...Array.from({ length: 13 }).map(() => + mockConversation({ + alertsConfig: { + type: "malicious", + numAlerts: 1, + }, + }) + ), + ]); + }) + ); + + const { getByRole } = render( + +
    foo
    +
    + ); + + await waitFor(() => { + expect(getByRole("tab", { name: /all/i })).toHaveTextContent("26"); + }); +}); + +test("shows correct count of malicious packages", async () => { + server.use( + http.get(mswEndpoint("/api/v1/workspaces/:workspace_name/messages"), () => { + return HttpResponse.json( + Array.from({ length: 13 }).map(() => + mockConversation({ + alertsConfig: { + type: "malicious", + numAlerts: 1, + }, + }) + ) + ); + }) + ); + + const { getByRole } = render( + +
    foo
    +
    + ); + + await waitFor(() => { + expect(getByRole("tab", { name: /malicious/i })).toHaveTextContent("13"); + }); +}); + +test("shows correct count of secret packages", async () => { + server.use( + http.get(mswEndpoint("/api/v1/workspaces/:workspace_name/messages"), () => { + return HttpResponse.json( + Array.from({ length: 13 }).map(() => + mockConversation({ + alertsConfig: { + type: "secret", + numAlerts: 1, + }, + }) + ) + ); + }) + ); + + const { getByRole } = render( + +
    foo
    +
    + ); + + await waitFor(() => { + expect(getByRole("tab", { name: /secrets/i })).toHaveTextContent("13"); + }); +}); diff --git a/src/features/dashboard-messages/components/conversation-secrets-detected.tsx b/src/features/dashboard-messages/components/conversation-secrets-detected.tsx new file mode 100644 index 00000000..c82b11bf --- /dev/null +++ b/src/features/dashboard-messages/components/conversation-secrets-detected.tsx @@ -0,0 +1,38 @@ +import { Alert } from "@/api/generated"; +import { ReactNode } from "react"; +import Markdown from "react-markdown"; + +function ConversationSecretsList({ children }: { children: ReactNode }) { + return
      {children}
    ; +} + +function ConversationSecretsListItem({ children }: { children: ReactNode }) { + return ( +
  • + {children} +
  • + ); +} + +function formatTriggerString(string: string): string { + return string.replace(/REDACTED<[^>]*?>/g, "**REDACTED**"); +} + +// NOTE: The secrets detection backend code appears to be returning fairly +// unstructured data with a lot of false positives. This is not actually +// referenced in the frontend yet. +export function ConversationSecretsDetected({ alerts }: { alerts: Alert[] }) { + return ( + + {alerts.map((a) => { + if (typeof a.trigger_string !== "string") return null; + + return ( + + {formatTriggerString(a.trigger_string)} + + ); + })} + + ); +} diff --git a/src/features/dashboard-messages/components/conversation-summary.tsx b/src/features/dashboard-messages/components/conversation-summary.tsx new file mode 100644 index 00000000..7189618d --- /dev/null +++ b/src/features/dashboard-messages/components/conversation-summary.tsx @@ -0,0 +1,172 @@ +import { Conversation } from "@/api/generated"; +import { ReactNode } from "react"; +import { getProviderString } from "../lib/get-provider-string"; +import { formatTime } from "@/lib/format-time"; +import { countConversationAlerts } from "../lib/count-conversation-alerts"; + +import { twMerge } from "tailwind-merge"; +import { TokenUsageIcon } from "./token-usage-icon"; +import { formatNumberCompact } from "@/lib/format-number"; +import { + Clock, + Hash01, + Key01, + PackageX, + Server05, +} from "@untitled-ui/icons-react"; + +function TokenUsage({ + tokens, + type, +}: { + type: "input" | "output"; + tokens: number; +}) { + return ( +
    + +
    {formatNumberCompact(tokens)}
    +
    + ); +} + +function TokenUsageRow({ + input_tokens, + output_tokens, +}: { + input_tokens: number; + output_tokens: number; +}) { + return ( +
    + + +
    + ); +} + +function AlertsSummaryCount({ + count, + type, +}: { + count: number; + type: { + singular: "malicious package" | "secret"; + plural: "malicious packages" | "secrets"; + }; +}) { + const typeText = count === 1 ? type.singular : type.plural; + + const text = `${count} ${typeText} detected`; + + return ( + 0 ? "text-secondary" : "text-disabled")}> + {text} + + ); +} + +function ConversationSummaryListItem({ + title, + value, + icon: Icon, +}: { + title: ReactNode; + value: ReactNode; + icon: (props: React.SVGProps) => React.JSX.Element; +}) { + return ( +
  • + + + {title} + + {value} +
  • + ); +} + +function ConversationSummaryList({ children }: { children: ReactNode }) { + return ( +
      {children}
    + ); +} + +export function ConversationSummary({ + conversation, +}: { + conversation: Conversation; +}) { + const { malicious, secrets } = conversation.alerts + ? countConversationAlerts(conversation.alerts) + : { malicious: 0, secrets: 0 }; + + return ( +
    + + + + + +
    + + {conversation.token_usage_agg ? ( + + } + /> + ) : null} + + } + /> + + } + /> + +
    + ); +} diff --git a/src/features/dashboard-messages/components/search-field-messages.tsx b/src/features/dashboard-messages/components/search-field-messages.tsx new file mode 100644 index 00000000..b311bc50 --- /dev/null +++ b/src/features/dashboard-messages/components/search-field-messages.tsx @@ -0,0 +1,46 @@ +import { + FieldGroup, + Input, + Kbd, + SearchField, + SearchFieldClearButton, +} from "@stacklok/ui-kit"; +import { useMessagesFilterSearchParams } from "../hooks/use-messages-filter-search-params"; +import { SearchMd } from "@untitled-ui/icons-react"; +import { useKbdShortcuts } from "@/hooks/use-kbd-shortcuts"; +import { useRef } from "react"; + +export function SearchFieldMessages({ className }: { className?: string }) { + const { setSearch, state } = useMessagesFilterSearchParams(); + const ref = useRef(null); + useKbdShortcuts([ + [ + "/", + () => { + ref.current?.focus(); + }, + ], + ]); + + return ( + setSearch(value)} + className={className} + > + + } + /> + + / + + + ); +} diff --git a/src/features/dashboard-messages/components/section-conversation-secrets.tsx b/src/features/dashboard-messages/components/section-conversation-secrets.tsx new file mode 100644 index 00000000..576a3ed3 --- /dev/null +++ b/src/features/dashboard-messages/components/section-conversation-secrets.tsx @@ -0,0 +1,40 @@ +import { IllustrationDone } from "@stacklok/ui-kit"; +import { isAlertSecret } from "@/lib/is-alert-secret"; +import { ConversationSecretsDetected } from "./conversation-secrets-detected"; +import { EmptyState } from "@/components/empty-state"; +import { emptyStateStrings } from "../constants/strings"; +import { Conversation } from "@/api/generated"; + +export function SectionConversationSecrets({ + conversation, +}: { + conversation: Conversation; +}) { + const secrets = conversation.alerts?.filter(isAlertSecret) ?? []; + + if (secrets.length === 0) + return ( + + ); + + return ( +
    +

    + CodeGate helps you protect sensitive information from being accidentally + exposed to AI models and third-party AI provider systems by redacting + detected secrets from your prompts using encryption. +

    +

    + The following secrets were detected in plain-text in the input provided + to the LLM. +

    + + +
    + ); +} diff --git a/src/features/dashboard-messages/components/section-conversation-transcript.tsx b/src/features/dashboard-messages/components/section-conversation-transcript.tsx new file mode 100644 index 00000000..8a56c41b --- /dev/null +++ b/src/features/dashboard-messages/components/section-conversation-transcript.tsx @@ -0,0 +1,66 @@ +import { Markdown } from "@/components/Markdown"; +import { + ChatBubble, + ChatBubbleAvatar, + ChatBubbleMessage, +} from "@/components/ui/chat/chat-bubble"; +import { ChatMessageList } from "@/components/ui/chat/chat-message-list"; +import { sanitizeQuestionPrompt } from "@/lib/utils"; +import { Heading } from "@stacklok/ui-kit"; +import { ConversationSummary } from "./conversation-summary"; +import { Conversation } from "@/api/generated"; + +export function SectionConversationTranscript({ + conversation, +}: { + conversation: Conversation; +}) { + return ( + <> +
    + + Conversation summary + + + +
    +
    + + Conversation transcript + + + + {(conversation?.question_answers ?? []).map( + ({ question, answer }, index) => ( +
    + + + + + {sanitizeQuestionPrompt({ + question: question?.message ?? "", + answer: answer?.message ?? "", + })} + + + + + + + {answer?.message ?? ""} + + +
    + ), + )} +
    +
    + + ); +} diff --git a/src/features/alerts/components/table-alert-token-usage.tsx b/src/features/dashboard-messages/components/table-alert-token-usage.tsx similarity index 100% rename from src/features/alerts/components/table-alert-token-usage.tsx rename to src/features/dashboard-messages/components/table-alert-token-usage.tsx diff --git a/src/features/alerts/components/table-alerts-empty-state.tsx b/src/features/dashboard-messages/components/table-messages-empty-state.tsx similarity index 81% rename from src/features/alerts/components/table-alerts-empty-state.tsx rename to src/features/dashboard-messages/components/table-messages-empty-state.tsx index b8bf7e41..9f97b2c7 100644 --- a/src/features/alerts/components/table-alerts-empty-state.tsx +++ b/src/features/dashboard-messages/components/table-messages-empty-state.tsx @@ -13,13 +13,13 @@ import { emptyStateStrings } from "../constants/strings"; import { EmptyState } from "@/components/empty-state"; import { hrefs } from "@/lib/hrefs"; import { LinkExternal02 } from "@untitled-ui/icons-react"; -import { useListAllWorkspaces } from "@/features/workspace/hooks/use-query-list-all-workspaces"; +import { useListAllWorkspaces } from "@/hooks/use-query-list-all-workspaces"; import { AlertsFilterView, - useAlertsFilterSearchParams, -} from "../hooks/use-alerts-filter-search-params"; -import { useQueryGetWorkspaceAlerts } from "../hooks/use-query-get-workspace-alerts"; + useMessagesFilterSearchParams, +} from "../hooks/use-messages-filter-search-params"; import { match, P } from "ts-pattern"; +import { useQueryGetWorkspaceMessages } from "@/hooks/use-query-get-workspace-messages"; function EmptyStateLoading() { return ( @@ -74,11 +74,11 @@ function EmptyStateSearch({ ); } -function EmptyStateNoAlertsInWorkspace() { +function EmptyStateNoMessagesInWorkspace() { return ( ({ isLoading, - hasWorkspaceAlerts: alerts.length > 0, + hasWorkspaceMessages: messages.length > 0, hasMultipleWorkspaces: workspaces.filter((w) => w.name !== "default").length > 0, search: state.search || null, @@ -179,7 +179,7 @@ export function TableAlertsEmptyState() { .with( { isLoading: true, - hasWorkspaceAlerts: P._, + hasWorkspaceMessages: P._, hasMultipleWorkspaces: P._, search: P._, view: P._, @@ -188,7 +188,7 @@ export function TableAlertsEmptyState() { ) .with( { - hasWorkspaceAlerts: false, + hasWorkspaceMessages: false, hasMultipleWorkspaces: false, search: P._, view: P._, @@ -197,7 +197,7 @@ export function TableAlertsEmptyState() { ) .with( { - hasWorkspaceAlerts: true, + hasWorkspaceMessages: true, hasMultipleWorkspaces: P.any, search: P.string.select(), view: P._, @@ -206,17 +206,17 @@ export function TableAlertsEmptyState() { ) .with( { - hasWorkspaceAlerts: false, - hasMultipleWorkspaces: true, + hasWorkspaceMessages: false, + hasMultipleWorkspaces: P.any, search: P._, view: P.any, }, - () => , + () => , ) .with( { - hasWorkspaceAlerts: true, - hasMultipleWorkspaces: true, + hasWorkspaceMessages: true, + hasMultipleWorkspaces: P.any, search: P._, view: AlertsFilterView.MALICIOUS, }, @@ -224,7 +224,7 @@ export function TableAlertsEmptyState() { ) .with( { - hasWorkspaceAlerts: true, + hasWorkspaceMessages: true, hasMultipleWorkspaces: P.any, view: AlertsFilterView.SECRETS, }, diff --git a/src/features/dashboard-messages/components/table-messages.tsx b/src/features/dashboard-messages/components/table-messages.tsx new file mode 100644 index 00000000..6d5e63d4 --- /dev/null +++ b/src/features/dashboard-messages/components/table-messages.tsx @@ -0,0 +1,210 @@ +import { + Cell, + Column, + Row, + Table, + TableBody, + TableHeader, + Button, + ResizableTableContainer, + Tooltip, + TooltipTrigger, +} from "@stacklok/ui-kit"; +import { Alert, Conversation, QuestionType } from "@/api/generated"; + +import { useClientSidePagination } from "@/hooks/useClientSidePagination"; +import { TableAlertTokenUsage } from "./table-alert-token-usage"; + +import { useMessagesFilterSearchParams } from "../hooks/use-messages-filter-search-params"; +import { Key01, PackageX } from "@untitled-ui/icons-react"; +import { TableAlertsEmptyState } from "./table-messages-empty-state"; +import { hrefs } from "@/lib/hrefs"; +import { isAlertMalicious } from "../../../lib/is-alert-malicious"; +import { isAlertSecret } from "../../../lib/is-alert-secret"; +import { twMerge } from "tailwind-merge"; +import { useQueryGetWorkspaceMessagesTable } from "../hooks/use-query-get-workspace-messages-table"; +import { + TABLE_MESSAGES_COLUMNS, + TableMessagesColumn, +} from "../constants/table-messages-columns"; +import { formatTime } from "@/lib/format-time"; + +const getPromptText = (conversation: Conversation) => { + return (conversation.question_answers[0]?.question?.message ?? "N/A") + .trim() + .slice(0, 200); // arbitrary slice to prevent long prompts +}; + +function getTypeText(type: QuestionType) { + switch (type) { + case QuestionType.CHAT: + return "Chat"; + case QuestionType.FIM: + return "Fill in the middle (FIM)"; + default: + return "Unknown"; + } +} + +function countAlerts(alerts: Alert[]): { + secrets: number; + malicious: number; +} { + return { + secrets: alerts.filter(isAlertSecret).length, + malicious: alerts.filter(isAlertMalicious).length, + }; +} + +function AlertsSummaryCount({ + count, + icon: Icon, + strings, +}: { + count: number; + icon: (props: React.SVGProps) => React.JSX.Element; + strings: { + singular: string; + plural: string; + }; +}) { + const tooltipText = `${count} ${count === 1 ? strings.singular : strings.plural} detected`; + + return ( + + + {tooltipText} + + ); +} + +function AlertsSummaryCellContent({ alerts }: { alerts: Alert[] }) { + const { malicious, secrets } = countAlerts(alerts); + + return ( +
    + + +
    + ); +} + +function CellRenderer({ + column, + row, +}: { + column: TableMessagesColumn; + row: Conversation; +}) { + switch (column.id) { + case "time": + return ( + + {formatTime(new Date(row.conversation_timestamp))} + + ); + case "type": + return getTypeText(row.type); + case "prompt": + return getPromptText(row); + case "alerts": + return ; + case "token_usage": + return ; + + default: + return column.id satisfies never; + } +} + +export function TableMessages() { + const { state, prevPage, nextPage } = useMessagesFilterSearchParams(); + + const { data = [] } = useQueryGetWorkspaceMessagesTable(); + const { dataView, hasNextPage, hasPreviousPage } = useClientSidePagination( + data, + state.page, + 15, + ); + + return ( + <> + + + + {(column) => } + + } + items={dataView} + > + {(row) => ( + + {(column) => ( + + + + )} + + )} + +
    +
    + + {hasNextPage || hasPreviousPage ? ( +
    +
    + + +
    +
    + ) : null} + + ); +} diff --git a/src/features/dashboard-messages/components/tabs-conversation.tsx b/src/features/dashboard-messages/components/tabs-conversation.tsx new file mode 100644 index 00000000..24106591 --- /dev/null +++ b/src/features/dashboard-messages/components/tabs-conversation.tsx @@ -0,0 +1,73 @@ +import { isAlertSecret } from "../../../lib/is-alert-secret"; +import { + Tab as BaseTab, + Tabs, + TabList, + TabPanel, + Badge, +} from "@stacklok/ui-kit"; +import { AlertsFilterView } from "../hooks/use-messages-filter-search-params"; + +import { + ConversationView, + useConversationSearchParams, +} from "../hooks/use-conversation-search-params"; +import { useConversationById } from "../hooks/use-conversation-by-id"; + +function Tab({ + id, + title, + count, +}: { + title: string; + id: ConversationView; + count?: number; +}) { + return ( + + {title} + {count ? ( + + {count} + + ) : null} + + ); +} + +export function TabsConversation({ + children, + id, +}: { + id: string; + children: React.ReactNode; +}) { + const { state, setView } = useConversationSearchParams(); + + const { data } = useConversationById(id); + + const secretsCount = data?.alerts?.filter(isAlertSecret).length ?? 0; + + return ( + setView(key.toString() as ConversationView)} + selectedKey={state.view} + defaultSelectedKey={AlertsFilterView.ALL} + > + + + + + + {children} + + ); +} diff --git a/src/features/alerts/components/tabs-alerts.tsx b/src/features/dashboard-messages/components/tabs-messages.tsx similarity index 69% rename from src/features/alerts/components/tabs-alerts.tsx rename to src/features/dashboard-messages/components/tabs-messages.tsx index f5bf0523..38263aa6 100644 --- a/src/features/alerts/components/tabs-alerts.tsx +++ b/src/features/dashboard-messages/components/tabs-messages.tsx @@ -1,9 +1,7 @@ -import { useQueryGetWorkspaceAlerts } from "../hooks/use-query-get-workspace-alerts"; -import { isAlertMalicious } from "../lib/is-alert-malicious"; +import { isConversationWithMaliciousAlerts } from "../../../lib/is-alert-malicious"; import { multiFilter } from "@/lib/multi-filter"; -import { isAlertCritical } from "../lib/is-alert-critical"; -import { isAlertSecret } from "../lib/is-alert-secret"; -import { V1GetWorkspaceAlertsResponse } from "@/api/generated"; +import { isConversationWithSecretAlerts } from "../../../lib/is-alert-secret"; +import { V1GetWorkspaceMessagesResponse } from "@/api/generated"; import { Tab as BaseTab, Tabs, @@ -15,10 +13,11 @@ import { } from "@stacklok/ui-kit"; import { AlertsFilterView, - useAlertsFilterSearchParams, -} from "../hooks/use-alerts-filter-search-params"; -import { SearchFieldAlerts } from "./search-field-alerts"; + useMessagesFilterSearchParams, +} from "../hooks/use-messages-filter-search-params"; +import { SearchFieldMessages } from "./search-field-messages"; import { tv } from "tailwind-variants"; +import { useQueryGetWorkspaceMessages } from "@/hooks/use-query-get-workspace-messages"; type AlertsCount = { all: number; @@ -26,17 +25,15 @@ type AlertsCount = { secrets: number; }; -function select(data: V1GetWorkspaceAlertsResponse | undefined): AlertsCount { - const all: number = multiFilter(data, [isAlertCritical]).length; +function select(data: V1GetWorkspaceMessagesResponse): AlertsCount { + const all: number = data?.length ?? 0; const malicious: number = multiFilter(data, [ - isAlertCritical, - isAlertMalicious, + isConversationWithMaliciousAlerts, ]).length; const secrets: number = multiFilter(data, [ - isAlertCritical, - isAlertSecret, + isConversationWithSecretAlerts, ]).length; return { @@ -78,10 +75,10 @@ function Tab({ ); } -export function TabsAlerts({ children }: { children: React.ReactNode }) { - const { state, setView } = useAlertsFilterSearchParams(); +export function TabsMessages({ children }: { children: React.ReactNode }) { + const { state, setView } = useMessagesFilterSearchParams(); - const { data } = useQueryGetWorkspaceAlerts({ + const { data } = useQueryGetWorkspaceMessages({ select, }); @@ -106,7 +103,7 @@ export function TabsAlerts({ children }: { children: React.ReactNode }) { /> - +
    diff --git a/src/features/alerts/components/token-usage-by-providers.tsx b/src/features/dashboard-messages/components/token-usage-by-providers.tsx similarity index 81% rename from src/features/alerts/components/token-usage-by-providers.tsx rename to src/features/dashboard-messages/components/token-usage-by-providers.tsx index 8c291c80..f136a56a 100644 --- a/src/features/alerts/components/token-usage-by-providers.tsx +++ b/src/features/dashboard-messages/components/token-usage-by-providers.tsx @@ -1,8 +1,7 @@ import { TokenUsage, TokenUsageAggregate } from "@/api/generated"; import { formatCurrency } from "@/lib/currency"; import { formatNumberCompact } from "@/lib/format-number"; - -import { ArrowDown, ArrowUp } from "@untitled-ui/icons-react"; +import { TokenUsageIcon } from "./token-usage-icon"; function validateUsage(usage: TokenUsage | null): usage is { input_tokens: number; @@ -18,23 +17,6 @@ function validateUsage(usage: TokenUsage | null): usage is { ); } -function UsageIcon({ - iconType: iconType, - ...props -}: { - iconType: "input" | "output"; - className?: string; -}) { - switch (iconType) { - case "input": - return ; - case "output": - return ; - default: - iconType satisfies never; - } -} - function UsageRow({ cost, tokens, @@ -46,7 +28,7 @@ function UsageRow({ }) { return (
  • - +
    {formatNumberCompact(tokens)}
    diff --git a/src/features/dashboard-messages/components/token-usage-icon.tsx b/src/features/dashboard-messages/components/token-usage-icon.tsx new file mode 100644 index 00000000..dd46e952 --- /dev/null +++ b/src/features/dashboard-messages/components/token-usage-icon.tsx @@ -0,0 +1,18 @@ +import { ArrowUp, ArrowDown } from "@untitled-ui/icons-react"; + +export function TokenUsageIcon({ + iconType: iconType, + ...props +}: { + iconType: "input" | "output"; + className?: string; +}) { + switch (iconType) { + case "input": + return ; + case "output": + return ; + default: + iconType satisfies never; + } +} diff --git a/src/features/alerts/constants/strings.ts b/src/features/dashboard-messages/constants/strings.ts similarity index 74% rename from src/features/alerts/constants/strings.ts rename to src/features/dashboard-messages/constants/strings.ts index 119e1ae5..dc329f51 100644 --- a/src/features/alerts/constants/strings.ts +++ b/src/features/dashboard-messages/constants/strings.ts @@ -2,8 +2,8 @@ export const emptyStateStrings = { title: { loading: "Loading...", getStarted: "Get started with CodeGate", - noAlertsFound: "No alerts found", - noAlertsFoundWorkspace: "This workspace hasn't triggered any alerts", + noMessages: "No messages found", + noMessagesWorkspace: "This workspace hasn't recorded any messages yet.", anErrorOccurred: "An error occurred", noLeakedSecretsDetected: "No leaked secrets detected", noMaliciousPackagesDetected: "No malicious packages detected", @@ -11,15 +11,15 @@ export const emptyStateStrings = { !x ? "No search results" : `No search results for "${x}"`, }, body: { - loading: "Checking for the latest alerts.", + loading: "Checking for the latest messages.", errorDesc: "Please try refreshing the page. If this issue persists, please let us know on Discord, or open a a new Github Issue", getStartedDesc: "Learn how to get started with CodeGate in your IDE.", tryChangingSearch: "Try changing your search query or clearing the search.", - alertsWillShowUpWhenWorkspace: - "Alerts will show up here when they are detected for this workspace.", - alertsDesc: - "Alerts are issues that CodeGate has detected and mitigated in your interactions with the LLM.", + messagesWillShowUpWhenWorkspace: + "Messages will show up here when they are detected for this workspace.", + messagesDesc: + "Messages are issues that CodeGate has detected and mitigated in your interactions with the LLM.", secretsDesc: "CodeGate helps you protect sensitive information from being accidentally exposed to AI models and third-party AI provider systems by redacting detected secrets from your prompts using encryption.", maliciousDesc: diff --git a/src/features/dashboard-messages/constants/table-messages-columns.ts b/src/features/dashboard-messages/constants/table-messages-columns.ts new file mode 100644 index 00000000..5b002071 --- /dev/null +++ b/src/features/dashboard-messages/constants/table-messages-columns.ts @@ -0,0 +1,40 @@ +import { Column } from "@stacklok/ui-kit"; +import { ComponentProps } from "react"; + +type ColumnId = "time" | "type" | "prompt" | "alerts" | "token_usage"; + +export type TableMessagesColumn = { id: ColumnId; children: string } & Omit< + ComponentProps, + "id" | "children" +>; + +export const TABLE_MESSAGES_COLUMNS: TableMessagesColumn[] = [ + { + id: "time", + isRowHeader: true, + children: "Time", + width: 160, + }, + { + id: "type", + children: "Type", + minWidth: 170, + maxWidth: 200, + }, + { + id: "prompt", + children: "Prompt", + }, + { + id: "alerts", + children: "Alerts", + alignment: "start", + minWidth: 110, + maxWidth: 160, + }, + { + id: "token_usage", + children: "Token usage", + width: 200, + }, +]; diff --git a/src/features/dashboard-messages/hooks/use-conversation-by-id.tsx b/src/features/dashboard-messages/hooks/use-conversation-by-id.tsx new file mode 100644 index 00000000..44bae983 --- /dev/null +++ b/src/features/dashboard-messages/hooks/use-conversation-by-id.tsx @@ -0,0 +1,7 @@ +import { useQueryGetWorkspaceMessages } from "@/hooks/use-query-get-workspace-messages"; + +export function useConversationById(id: string) { + return useQueryGetWorkspaceMessages({ + select: (d) => d.find((c) => c.chat_id === id), + }); +} diff --git a/src/features/dashboard-messages/hooks/use-conversation-search-params.ts b/src/features/dashboard-messages/hooks/use-conversation-search-params.ts new file mode 100644 index 00000000..ec9f36cd --- /dev/null +++ b/src/features/dashboard-messages/hooks/use-conversation-search-params.ts @@ -0,0 +1,45 @@ +import { useCallback } from "react"; +import { useSearchParams } from "react-router-dom"; +import { z } from "zod"; + +export enum ConversationView { + OVERVIEW = "overview", + SECRETS = "secrets", +} + +const conversationParamsSchema = z.object({ + view: z + .nativeEnum(ConversationView) + .optional() + .default(ConversationView.OVERVIEW), +}); + +type ConversationParamsSchema = z.input; + +const DEFAULT_FILTER = { + view: ConversationView.OVERVIEW, +} as const satisfies ConversationParamsSchema; + +export const useConversationSearchParams = () => { + const [searchParams, setSearchParams] = useSearchParams( + new URLSearchParams(DEFAULT_FILTER), + ); + + const setView = useCallback( + (view: ConversationView) => { + setSearchParams((prev) => { + if (view) prev.set("view", view); + if (!view) prev.delete("view"); + + return prev; + }); + }, + [setSearchParams], + ); + + const state = conversationParamsSchema.parse( + Object.fromEntries(searchParams), + ); + + return { state, setView }; +}; diff --git a/src/features/alerts/hooks/use-alerts-filter-search-params.ts b/src/features/dashboard-messages/hooks/use-messages-filter-search-params.ts similarity index 92% rename from src/features/alerts/hooks/use-alerts-filter-search-params.ts rename to src/features/dashboard-messages/hooks/use-messages-filter-search-params.ts index a6e700dc..f626833e 100644 --- a/src/features/alerts/hooks/use-alerts-filter-search-params.ts +++ b/src/features/dashboard-messages/hooks/use-messages-filter-search-params.ts @@ -20,9 +20,9 @@ const DEFAULT_FILTER = { view: AlertsFilterView.ALL, } as const satisfies AlertsFilterSchema; -export const useAlertsFilterSearchParams = () => { +export const useMessagesFilterSearchParams = () => { const [searchParams, setSearchParams] = useSearchParams( - new URLSearchParams(DEFAULT_FILTER), + new URLSearchParams(DEFAULT_FILTER) ); const setView = useCallback( @@ -35,7 +35,7 @@ export const useAlertsFilterSearchParams = () => { return prev; }); }, - [setSearchParams], + [setSearchParams] ); const setSearch = useCallback( @@ -46,7 +46,7 @@ export const useAlertsFilterSearchParams = () => { return prev; }); }, - [setSearchParams], + [setSearchParams] ); const nextPage = useCallback(() => { diff --git a/src/features/dashboard-messages/hooks/use-query-get-workspace-messages-table.ts b/src/features/dashboard-messages/hooks/use-query-get-workspace-messages-table.ts new file mode 100644 index 00000000..af0b7fd2 --- /dev/null +++ b/src/features/dashboard-messages/hooks/use-query-get-workspace-messages-table.ts @@ -0,0 +1,43 @@ +import { Conversation } from "@/api/generated"; +import { useCallback } from "react"; +import { + AlertsFilterView, + useMessagesFilterSearchParams, +} from "./use-messages-filter-search-params"; +import { multiFilter } from "@/lib/multi-filter"; +import { isConversationWithMaliciousAlerts } from "../../../lib/is-alert-malicious"; +import { isConversationWithSecretAlerts } from "../../../lib/is-alert-secret"; +import { filterMessagesBySubstring } from "../lib/filter-messages-by-substring"; +import { useQueryGetWorkspaceMessages } from "@/hooks/use-query-get-workspace-messages"; + +const FILTER: Record< + AlertsFilterView, + (alert: Conversation | null) => boolean +> = { + all: () => true, + malicious: isConversationWithMaliciousAlerts, + secrets: isConversationWithSecretAlerts, +}; + +export function useQueryGetWorkspaceMessagesTable() { + const { state } = useMessagesFilterSearchParams(); + + // NOTE: This needs to be a stable function reference to enable memo-isation + // of the select operation on each React re-render. + const select = useCallback( + (data: Conversation[]) => { + return multiFilter(data, [FILTER[state.view]]) + .filter((conversation) => + filterMessagesBySubstring(conversation, state.search ?? null), + ) + .sort((a, b) => + b.conversation_timestamp > a.conversation_timestamp ? 1 : -1, + ); + }, + [state.search, state.view], + ); + + return useQueryGetWorkspaceMessages({ + select, + }); +} diff --git a/src/features/alerts/lib/__tests__/is-alert-malicious.test.ts b/src/features/dashboard-messages/lib/__tests__/is-alert-malicious.test.ts similarity index 65% rename from src/features/alerts/lib/__tests__/is-alert-malicious.test.ts rename to src/features/dashboard-messages/lib/__tests__/is-alert-malicious.test.ts index dfcd8b3f..f49fd762 100644 --- a/src/features/alerts/lib/__tests__/is-alert-malicious.test.ts +++ b/src/features/dashboard-messages/lib/__tests__/is-alert-malicious.test.ts @@ -1,6 +1,6 @@ import { test, expect } from "vitest"; -import { isAlertMalicious } from "../is-alert-malicious"; -import { mockAlert } from "@/mocks/msw/mockers/alert.mock"; +import { isAlertMalicious } from "../../../../lib/is-alert-malicious"; +import { mockAlert } from "../../../../mocks/msw/mockers/alert.mock"; test("matches malicious alert", () => { expect(isAlertMalicious(mockAlert({ type: "malicious" }))).toBe(true); diff --git a/src/features/alerts/lib/__tests__/is-alert-secret.test.ts b/src/features/dashboard-messages/lib/__tests__/is-alert-secret.test.ts similarity index 66% rename from src/features/alerts/lib/__tests__/is-alert-secret.test.ts rename to src/features/dashboard-messages/lib/__tests__/is-alert-secret.test.ts index 16f70071..84eba1c9 100644 --- a/src/features/alerts/lib/__tests__/is-alert-secret.test.ts +++ b/src/features/dashboard-messages/lib/__tests__/is-alert-secret.test.ts @@ -1,6 +1,6 @@ import { test, expect } from "vitest"; -import { isAlertSecret } from "../is-alert-secret"; -import { mockAlert } from "@/mocks/msw/mockers/alert.mock"; +import { isAlertSecret } from "../../../../lib/is-alert-secret"; +import { mockAlert } from "../../../../mocks/msw/mockers/alert.mock"; test("matches secret alert", () => { expect(isAlertSecret(mockAlert({ type: "secret" }))).toBe(true); diff --git a/src/features/dashboard-messages/lib/count-conversation-alerts.ts b/src/features/dashboard-messages/lib/count-conversation-alerts.ts new file mode 100644 index 00000000..7cdba530 --- /dev/null +++ b/src/features/dashboard-messages/lib/count-conversation-alerts.ts @@ -0,0 +1,13 @@ +import { Alert } from "@/api/generated"; +import { isAlertMalicious } from "@/lib/is-alert-malicious"; +import { isAlertSecret } from "@/lib/is-alert-secret"; + +export function countConversationAlerts(alerts: Alert[]): { + secrets: number; + malicious: number; +} { + return { + secrets: alerts.filter(isAlertSecret).length, + malicious: alerts.filter(isAlertMalicious).length, + }; +} diff --git a/src/features/dashboard-messages/lib/filter-messages-by-substring.ts b/src/features/dashboard-messages/lib/filter-messages-by-substring.ts new file mode 100644 index 00000000..1bf7f6ae --- /dev/null +++ b/src/features/dashboard-messages/lib/filter-messages-by-substring.ts @@ -0,0 +1,23 @@ +import { Conversation } from "@/api/generated"; + +export function filterMessagesBySubstring( + conversation: Conversation, + substring: string | null +): boolean { + if (conversation == null) return false; + if (substring === null) return true; + + // NOTE: This is a naive implementation that is expensive for large datasets. + const messages = conversation.question_answers.reduce( + (acc, curr) => { + if (curr.question) acc.push(curr.question.message); + if (curr.answer) acc.push(curr.answer.message); + return acc; + }, + [] as string[] + ); + + return [...messages].some((i) => + i?.toLowerCase().includes(substring.toLowerCase()) + ); +} diff --git a/src/features/dashboard-messages/lib/get-conversation-title.ts b/src/features/dashboard-messages/lib/get-conversation-title.ts new file mode 100644 index 00000000..948d890b --- /dev/null +++ b/src/features/dashboard-messages/lib/get-conversation-title.ts @@ -0,0 +1,17 @@ +import { Conversation, QuestionType } from "@/api/generated"; +import { getProviderString } from "./get-provider-string"; + +function getTypeString(type: Conversation["type"]) { + switch (type) { + case QuestionType.CHAT: + return "Chat"; + case QuestionType.FIM: + return "Fill in the middle (FIM)"; + default: + return type; + } +} + +export function getConversationTitle(conversation: Conversation) { + return `${getTypeString(conversation.type)} with ${getProviderString(conversation.provider)}`; +} diff --git a/src/features/dashboard-messages/lib/get-provider-string.ts b/src/features/dashboard-messages/lib/get-provider-string.ts new file mode 100644 index 00000000..e05360a1 --- /dev/null +++ b/src/features/dashboard-messages/lib/get-provider-string.ts @@ -0,0 +1,10 @@ +export function getProviderString(provider: string | null): string { + switch (provider) { + case "copilot": + return "Github Copilot"; + case null: + return "Unknown provider"; + default: + return provider; + } +} diff --git a/src/features/header/components/__tests__/header-status-menu.test.tsx b/src/features/header/components/__tests__/header-status-menu.test.tsx index a957694c..81e12524 100644 --- a/src/features/header/components/__tests__/header-status-menu.test.tsx +++ b/src/features/header/components/__tests__/header-status-menu.test.tsx @@ -13,8 +13,8 @@ describe("CardCodegateStatus", () => { test("renders 'healthy' state", async () => { server.use( http.get(mswEndpoint("/health"), () => - HttpResponse.json({ status: "healthy" }), - ), + HttpResponse.json({ status: "healthy" }) + ) ); const { getByRole } = renderComponent(); @@ -27,8 +27,8 @@ describe("CardCodegateStatus", () => { test("renders 'unhealthy' state", async () => { server.use( http.get(mswEndpoint("/health"), () => - HttpResponse.json({ status: null }), - ), + HttpResponse.json({ status: null }) + ) ); const { getByRole } = renderComponent(); @@ -51,9 +51,9 @@ describe("CardCodegateStatus", () => { test("renders 'error' state when version check request fails", async () => { server.use( http.get(mswEndpoint("/health"), () => - HttpResponse.json({ status: "healthy" }), + HttpResponse.json({ status: "healthy" }) ), - http.get(mswEndpoint("/api/v1/version"), () => HttpResponse.error()), + http.get(mswEndpoint("/api/v1/version"), () => HttpResponse.error()) ); const { getByRole } = renderComponent(); @@ -66,7 +66,7 @@ describe("CardCodegateStatus", () => { test("renders 'up to date' state", async () => { server.use( http.get(mswEndpoint("/health"), () => - HttpResponse.json({ status: "healthy" }), + HttpResponse.json({ status: "healthy" }) ), http.get(mswEndpoint("/api/v1/version"), () => HttpResponse.json({ @@ -74,8 +74,8 @@ describe("CardCodegateStatus", () => { latest_version: "foo", is_latest: true, error: null, - }), - ), + }) + ) ); const { getByRole, getByText } = renderComponent(); @@ -95,7 +95,7 @@ describe("CardCodegateStatus", () => { test("renders 'update available' state", async () => { server.use( http.get(mswEndpoint("/health"), () => - HttpResponse.json({ status: "healthy" }), + HttpResponse.json({ status: "healthy" }) ), http.get(mswEndpoint("/api/v1/version"), () => HttpResponse.json({ @@ -103,8 +103,8 @@ describe("CardCodegateStatus", () => { latest_version: "bar", is_latest: false, error: null, - }), - ), + }) + ) ); const { getByRole } = renderComponent(); @@ -121,7 +121,7 @@ describe("CardCodegateStatus", () => { expect(role).toBeVisible(); expect(role).toHaveAttribute( "href", - "https://docs.codegate.ai/how-to/install#upgrade-codegate", + "https://docs.codegate.ai/how-to/install#upgrade-codegate" ); }); }); @@ -129,7 +129,7 @@ describe("CardCodegateStatus", () => { test("renders 'version check error' state", async () => { server.use( http.get(mswEndpoint("/health"), () => - HttpResponse.json({ status: "healthy" }), + HttpResponse.json({ status: "healthy" }) ), http.get(mswEndpoint("/api/v1/version"), () => HttpResponse.json({ @@ -137,8 +137,8 @@ describe("CardCodegateStatus", () => { latest_version: "bar", is_latest: false, error: "foo", - }), - ), + }) + ) ); const { getByRole, getByText } = renderComponent(); diff --git a/src/features/workspace/components/workspaces-selection.tsx b/src/features/header/components/header-active-workspace-selector.tsx similarity index 90% rename from src/features/workspace/components/workspaces-selection.tsx rename to src/features/header/components/header-active-workspace-selector.tsx index b142452c..ea204d6e 100644 --- a/src/features/workspace/components/workspaces-selection.tsx +++ b/src/features/header/components/header-active-workspace-selector.tsx @@ -1,4 +1,4 @@ -import { useListWorkspaces } from "@/features/workspace/hooks/use-list-workspaces"; +import { useQueryListWorkspaces } from "@/hooks/use-query-list-workspaces"; import { Button, DialogTrigger, @@ -12,21 +12,21 @@ import { } from "@stacklok/ui-kit"; import { useQueryClient } from "@tanstack/react-query"; import { useState } from "react"; -import { useMutationActivateWorkspace } from "../hooks/use-mutation-activate-workspace"; +import { useMutationActivateWorkspace } from "../../../hooks/use-mutation-activate-workspace"; import clsx from "clsx"; -import { useActiveWorkspaceName } from "../hooks/use-active-workspace-name"; +import { useQueryActiveWorkspaceName } from "../../../hooks/use-query-active-workspace-name"; import { hrefs } from "@/lib/hrefs"; import { twMerge } from "tailwind-merge"; import ChevronDown from "@untitled-ui/icons-react/build/cjs/ChevronDown"; import { SearchMd, Settings01 } from "@untitled-ui/icons-react"; -export function WorkspacesSelection() { +export function HeaderActiveWorkspaceSelector() { const queryClient = useQueryClient(); - const { data: workspacesResponse } = useListWorkspaces(); + const { data: workspacesResponse } = useQueryListWorkspaces(); const { mutateAsync: activateWorkspace } = useMutationActivateWorkspace(); - const { data: activeWorkspaceName } = useActiveWorkspaceName(); + const { data: activeWorkspaceName } = useQueryActiveWorkspaceName(); const [isOpen, setIsOpen] = useState(false); const [searchWorkspace, setSearchWorkspace] = useState(""); diff --git a/src/features/header/components/header.tsx b/src/features/header/components/header.tsx index 19eaf836..6ae077d1 100644 --- a/src/features/header/components/header.tsx +++ b/src/features/header/components/header.tsx @@ -1,8 +1,8 @@ import { Link } from "react-router-dom"; -import { SidebarTrigger } from "../../../components/ui/sidebar"; + import { DropdownMenu } from "../../../components/HoverPopover"; import { Separator, ButtonDarkMode } from "@stacklok/ui-kit"; -import { WorkspacesSelection } from "@/features/workspace/components/workspaces-selection"; +import { HeaderActiveWorkspaceSelector } from "@/features/header/components/header-active-workspace-selector"; import { HELP_MENU_ITEMS } from "../constants/help-menu-items"; import { HeaderStatusMenu } from "./header-status-menu"; import { SETTINGS_MENU_ITEMS } from "../constants/settings-menu-items"; @@ -17,25 +17,18 @@ function HomeLink() { ); } -export function Header({ hasError }: { hasError?: boolean }) { +export function Header() { return (
    - {!hasError && ( - <> - - - - )} - - +
    diff --git a/src/features/providers/components/table-providers.tsx b/src/features/providers/components/table-providers.tsx index 8b8a886a..6f4de724 100644 --- a/src/features/providers/components/table-providers.tsx +++ b/src/features/providers/components/table-providers.tsx @@ -33,12 +33,19 @@ const COLUMNS: Column[] = [ id: "provider", isRowHeader: true, children: "Name & Description", - width: "40%", + minWidth: 450, + maxWidth: 520, }, - { id: "type", children: "Provider", width: "10%", className: "capitalize" }, - { id: "endpoint", children: "Endpoint", width: "20%", minWidth: 250 }, - { id: "auth", children: "Authentication", width: "20%" }, - { id: "configuration", alignment: "end", width: "10%", children: "" }, + { + id: "type", + children: "Provider", + minWidth: 110, + maxWidth: 130, + className: "capitalize", + }, + { id: "endpoint", children: "Endpoint", minWidth: 250 }, + { id: "auth", children: "Authentication", minWidth: 140 }, + { id: "configuration", alignment: "end", minWidth: 40, children: "" }, ]; function CellRenderer({ diff --git a/src/features/workspace/components/__tests__/archive-workspace.test.tsx b/src/features/workspace/components/__tests__/archive-workspace.test.tsx index 9a6b32de..e0fde0c3 100644 --- a/src/features/workspace/components/__tests__/archive-workspace.test.tsx +++ b/src/features/workspace/components/__tests__/archive-workspace.test.tsx @@ -8,7 +8,7 @@ import { mswEndpoint } from "@/test/msw-endpoint"; test("has correct buttons when not archived", async () => { const { getByRole, queryByRole } = render( - , + ); expect(getByRole("button", { name: /archive/i })).toBeVisible(); @@ -17,7 +17,7 @@ test("has correct buttons when not archived", async () => { test("has correct buttons when archived", async () => { const { getByRole } = render( - , + ); expect(getByRole("button", { name: /restore/i })).toBeVisible(); expect(getByRole("button", { name: /permanently delete/i })).toBeVisible(); @@ -25,7 +25,7 @@ test("has correct buttons when archived", async () => { test("can archive workspace", async () => { const { getByText, getByRole } = render( - , + ); await userEvent.click(getByRole("button", { name: /archive/i })); @@ -37,7 +37,7 @@ test("can archive workspace", async () => { test("can restore archived workspace", async () => { const { getByText, getByRole } = render( - , + ); await userEvent.click(getByRole("button", { name: /restore/i })); @@ -49,7 +49,7 @@ test("can restore archived workspace", async () => { test("can permanently delete archived workspace", async () => { const { getByText, getByRole } = render( - , + ); await userEvent.click(getByRole("button", { name: /permanently delete/i })); @@ -76,11 +76,11 @@ test("can't archive active workspace", async () => { last_updated: new Date(Date.now()).toISOString(), }, ], - }), - ), + }) + ) ); const { getByRole } = render( - , + ); await waitFor(() => { @@ -91,7 +91,7 @@ test("can't archive active workspace", async () => { test("can't archive default workspace", async () => { const { getByRole } = render( - , + ); await waitFor(() => { diff --git a/src/features/workspace/components/__tests__/workspace-custom-instructions.test.tsx b/src/features/workspace/components/__tests__/workspace-custom-instructions.test.tsx index b9d83c1f..01ebbecc 100644 --- a/src/features/workspace/components/__tests__/workspace-custom-instructions.test.tsx +++ b/src/features/workspace/components/__tests__/workspace-custom-instructions.test.tsx @@ -22,7 +22,7 @@ vi.mock("@monaco-editor/react", () => { const renderComponent = () => render( - , + ); test("can update custom instructions", async () => { @@ -31,8 +31,8 @@ test("can update custom instructions", async () => { mswEndpoint("/api/v1/workspaces/:workspace_name/custom-instructions"), () => { return HttpResponse.json({ prompt: "initial prompt from server" }); - }, - ), + } + ) ); const { getByRole, getByText } = renderComponent(); @@ -53,15 +53,15 @@ test("can update custom instructions", async () => { mswEndpoint("/api/v1/workspaces/:workspace_name/custom-instructions"), () => { return HttpResponse.json({ prompt: "new prompt from test" }); - }, - ), + } + ) ); await userEvent.click(getByRole("button", { name: /Save/i })); await waitFor(() => { expect( - getByText(/successfully updated custom instructions/i), + getByText(/successfully updated custom instructions/i) ).toBeVisible(); }); diff --git a/src/features/workspace/components/__tests__/workspace-name.test.tsx b/src/features/workspace/components/__tests__/workspace-name.test.tsx index 84bfc93f..27e32f93 100644 --- a/src/features/workspace/components/__tests__/workspace-name.test.tsx +++ b/src/features/workspace/components/__tests__/workspace-name.test.tsx @@ -8,7 +8,7 @@ import { mswEndpoint } from "@/test/msw-endpoint"; test("can rename workspace", async () => { const { getByRole, getByText } = render( - , + ); const input = getByRole("textbox", { name: /workspace name/i }); @@ -26,7 +26,7 @@ test("can rename workspace", async () => { test("can't rename archived workspace", async () => { const { getByRole } = render( - , + ); expect(getByRole("textbox", { name: /workspace name/i })).toBeDisabled(); @@ -44,11 +44,11 @@ test("can't rename active workspace", async () => { last_updated: new Date(Date.now()).toISOString(), }, ], - }), - ), + }) + ) ); const { getByRole } = render( - , + ); expect(getByRole("textbox", { name: /workspace name/i })).toBeDisabled(); @@ -57,7 +57,7 @@ test("can't rename active workspace", async () => { test("can't rename default workspace", async () => { const { getByRole } = render( - , + ); expect(getByRole("textbox", { name: /workspace name/i })).toBeDisabled(); diff --git a/src/features/workspace/components/archive-workspace.tsx b/src/features/workspace/components/archive-workspace.tsx index aedb2fbf..a3a2045a 100644 --- a/src/features/workspace/components/archive-workspace.tsx +++ b/src/features/workspace/components/archive-workspace.tsx @@ -13,7 +13,7 @@ import { useArchiveWorkspaceButton } from "../hooks/use-archive-workspace-button import { useConfirmHardDeleteWorkspace } from "../hooks/use-confirm-hard-delete-workspace"; import { useNavigate } from "react-router-dom"; import { hrefs } from "@/lib/hrefs"; -import { useActiveWorkspaceName } from "../hooks/use-active-workspace-name"; +import { useQueryActiveWorkspaceName } from "../../../hooks/use-query-active-workspace-name"; function getContextualText({ activeWorkspaceName, @@ -34,7 +34,7 @@ function getContextualText({ // NOTE: You can't show a tooltip on a disabled button // React Aria's recommended approach is https://spectrum.adobe.com/page/contextual-help/ function ContextualHelp({ workspaceName }: { workspaceName: string }) { - const { data: activeWorkspaceName } = useActiveWorkspaceName(); + const { data: activeWorkspaceName } = useQueryActiveWorkspaceName(); if (!activeWorkspaceName) return null; const text = getContextualText({ activeWorkspaceName, workspaceName }); diff --git a/src/features/workspace/components/table-actions-workspaces.tsx b/src/features/workspace/components/table-actions-workspaces.tsx index f36f46da..b651bd78 100644 --- a/src/features/workspace/components/table-actions-workspaces.tsx +++ b/src/features/workspace/components/table-actions-workspaces.tsx @@ -10,7 +10,7 @@ import { import { useMutationArchiveWorkspace } from "@/features/workspace/hooks/use-mutation-archive-workspace"; import { useMutationRestoreWorkspace } from "../hooks/use-mutation-restore-workspace"; import { useMutationHardDeleteWorkspace } from "../hooks/use-mutation-hard-delete-workspace"; -import { useMutationActivateWorkspace } from "../hooks/use-mutation-activate-workspace"; +import { useMutationActivateWorkspace } from "../../../hooks/use-mutation-activate-workspace"; import { useConfirmHardDeleteWorkspace } from "../hooks/use-confirm-hard-delete-workspace"; import { hrefs } from "@/lib/hrefs"; import { diff --git a/src/features/workspace/components/table-workspaces.tsx b/src/features/workspace/components/table-workspaces.tsx index 46187e8c..c4a1fac2 100644 --- a/src/features/workspace/components/table-workspaces.tsx +++ b/src/features/workspace/components/table-workspaces.tsx @@ -1,5 +1,7 @@ import { Badge, + Card, + CardBody, Cell, Column, Row, @@ -8,8 +10,8 @@ import { TableHeader, } from "@stacklok/ui-kit"; -import { useListAllWorkspaces } from "../hooks/use-query-list-all-workspaces"; -import { useActiveWorkspaceName } from "../hooks/use-active-workspace-name"; +import { useListAllWorkspaces } from "../../../hooks/use-query-list-all-workspaces"; +import { useQueryActiveWorkspaceName } from "../../../hooks/use-query-active-workspace-name"; import { TableActionsWorkspaces } from "./table-actions-workspaces"; import { hrefs } from "@/lib/hrefs"; @@ -24,7 +26,7 @@ function CellName({ }) { if (isArchived) return ( - + {name}    @@ -35,7 +37,7 @@ function CellName({ if (isActive) return ( - + {name}    @@ -49,35 +51,42 @@ function CellName({ export function TableWorkspaces() { const { data: workspaces } = useListAllWorkspaces(); - const { data: activeWorkspaceName } = useActiveWorkspaceName(); + const { data: activeWorkspaceName } = useQueryActiveWorkspaceName(); return ( - - - - - Name - - - - - - {workspaces.map((workspace) => ( - - - - - + + +
    + + + + Name + + + - ))} - -
    + + {workspaces.map((workspace) => ( + + + + + + + ))} + + + + ); } diff --git a/src/features/workspace/components/workspace-heading.tsx b/src/features/workspace/components/workspace-heading.tsx deleted file mode 100644 index 8daaf93a..00000000 --- a/src/features/workspace/components/workspace-heading.tsx +++ /dev/null @@ -1,17 +0,0 @@ -import { Heading } from "@stacklok/ui-kit"; -import React from "react"; - -export function WorkspaceHeading({ - title, - children, -}: { - title: React.ReactNode; - children?: React.ReactNode; -}) { - return ( - - {title} - {children} - - ); -} diff --git a/src/features/workspace/components/workspace-preferred-model.tsx b/src/features/workspace/components/workspace-preferred-model.tsx index 60ba4331..d0dda4d4 100644 --- a/src/features/workspace/components/workspace-preferred-model.tsx +++ b/src/features/workspace/components/workspace-preferred-model.tsx @@ -15,7 +15,7 @@ import { MuxMatcherType } from "@/api/generated"; import { FormEvent } from "react"; import { usePreferredModelWorkspace } from "../hooks/use-preferred-preferred-model"; import { Select, SelectButton } from "@stacklok/ui-kit"; -import { useModelsData } from "@/hooks/use-models-data"; +import { useQueryListAllModelsForAllProviders } from "@/hooks/use-query-list-all-models-for-all-providers"; function MissingProviderBanner() { return ( @@ -42,7 +42,7 @@ export function WorkspacePreferredModel({ const { preferredModel, setPreferredModel, isPending } = usePreferredModelWorkspace(workspaceName); const { mutateAsync } = useMutationPreferredModelWorkspace(); - const { data: providerModels = [] } = useModelsData(); + const { data: providerModels = [] } = useQueryListAllModelsForAllProviders(); const { model, provider_id } = preferredModel; const isModelsEmpty = !isPending && providerModels.length === 0; diff --git a/src/features/workspace/hooks/use-archive-workspace-button.tsx b/src/features/workspace/hooks/use-archive-workspace-button.tsx index e6782ed4..42085b3a 100644 --- a/src/features/workspace/hooks/use-archive-workspace-button.tsx +++ b/src/features/workspace/hooks/use-archive-workspace-button.tsx @@ -1,14 +1,14 @@ import { Button } from "@stacklok/ui-kit"; import { ComponentProps } from "react"; import { useMutationArchiveWorkspace } from "@/features/workspace/hooks/use-mutation-archive-workspace"; -import { useActiveWorkspaceName } from "./use-active-workspace-name"; +import { useQueryActiveWorkspaceName } from "../../../hooks/use-query-active-workspace-name"; export function useArchiveWorkspaceButton({ workspaceName, }: { workspaceName: string; }): ComponentProps { - const { data: activeWorkspaceName } = useActiveWorkspaceName(); + const { data: activeWorkspaceName } = useQueryActiveWorkspaceName(); const { mutateAsync, isPending } = useMutationArchiveWorkspace(); return { diff --git a/src/features/workspace/hooks/use-mutation-archive-workspace.ts b/src/features/workspace/hooks/use-mutation-archive-workspace.ts index 7dc0267e..954211f6 100644 --- a/src/features/workspace/hooks/use-mutation-archive-workspace.ts +++ b/src/features/workspace/hooks/use-mutation-archive-workspace.ts @@ -10,12 +10,12 @@ import { V1ListArchivedWorkspacesResponse, V1ListWorkspacesResponse, } from "@/api/generated"; -import { useActiveWorkspaceName } from "./use-active-workspace-name"; +import { useQueryActiveWorkspaceName } from "../../../hooks/use-query-active-workspace-name"; export function useMutationArchiveWorkspace() { const queryClient = useQueryClient(); const invalidate = useInvalidateWorkspaceQueries(); - const { data: activeWorkspaceName } = useActiveWorkspaceName(); + const { data: activeWorkspaceName } = useQueryActiveWorkspaceName(); return useToastMutation({ ...v1DeleteWorkspaceMutation(), diff --git a/src/features/workspace/hooks/use-preferred-preferred-model.ts b/src/features/workspace/hooks/use-preferred-preferred-model.ts index 78af9e21..4d917084 100644 --- a/src/features/workspace/hooks/use-preferred-preferred-model.ts +++ b/src/features/workspace/hooks/use-preferred-preferred-model.ts @@ -3,12 +3,12 @@ import { v1GetWorkspaceMuxesOptions } from "@/api/generated/@tanstack/react-quer import { useQuery } from "@tanstack/react-query"; import { useEffect, useMemo, useState } from "react"; +type ModelRule = Omit & {}; + const DEFAULT_STATE = { provider_id: "", model: "", -}; - -export type ModelRule = Omit & {}; +} as const satisfies ModelRule; const usePreferredModel = (options: { path: { @@ -28,7 +28,7 @@ export const usePreferredModelWorkspace = (workspaceName: string) => { () => ({ path: { workspace_name: workspaceName }, }), - [workspaceName], + [workspaceName] ); const { data, isPending } = usePreferredModel(options); diff --git a/src/hooks/use-confirm.tsx b/src/hooks/use-confirm.tsx index dc1305cc..207637a0 100644 --- a/src/hooks/use-confirm.tsx +++ b/src/hooks/use-confirm.tsx @@ -1,25 +1,8 @@ "use client"; import { ConfirmContext } from "@/context/confirm-context"; -import type { ReactNode } from "react"; import { useContext } from "react"; -type Buttons = { - yes: ReactNode; - no: ReactNode; -}; - -type Config = { - buttons: Buttons; - title?: ReactNode; - isDestructive?: boolean; -}; - -export type ConfirmFunction = ( - message: ReactNode, - config: Config, -) => Promise; - export const useConfirm = () => { const context = useContext(ConfirmContext); if (!context) { diff --git a/src/hooks/use-mobile.tsx b/src/hooks/use-mobile.tsx deleted file mode 100644 index 3799a13d..00000000 --- a/src/hooks/use-mobile.tsx +++ /dev/null @@ -1,19 +0,0 @@ -import * as React from "react" - -const MOBILE_BREAKPOINT = 1024 - -export function useIsMobile() { - const [isMobile, setIsMobile] = React.useState(undefined) - - React.useEffect(() => { - const mql = window.matchMedia(`(max-width: ${MOBILE_BREAKPOINT - 1}px)`) - const onChange = () => { - setIsMobile(window.innerWidth < MOBILE_BREAKPOINT) - } - mql.addEventListener("change", onChange) - setIsMobile(window.innerWidth < MOBILE_BREAKPOINT) - return () => mql.removeEventListener("change", onChange) - }, []) - - return !!isMobile -} diff --git a/src/features/workspace/hooks/use-mutation-activate-workspace.ts b/src/hooks/use-mutation-activate-workspace.ts similarity index 100% rename from src/features/workspace/hooks/use-mutation-activate-workspace.ts rename to src/hooks/use-mutation-activate-workspace.ts diff --git a/src/features/workspace/hooks/use-active-workspace-name.ts b/src/hooks/use-query-active-workspace-name.ts similarity index 66% rename from src/features/workspace/hooks/use-active-workspace-name.ts rename to src/hooks/use-query-active-workspace-name.ts index 85eddfd4..21e78885 100644 --- a/src/features/workspace/hooks/use-active-workspace-name.ts +++ b/src/hooks/use-query-active-workspace-name.ts @@ -1,5 +1,5 @@ import { ListActiveWorkspacesResponse } from "@/api/generated"; -import { useActiveWorkspaces } from "./use-active-workspaces"; +import { useQueryListActiveWorkspaces } from "./use-query-list-active-workspaces"; // NOTE: This needs to be a stable function reference to enable memo-isation of // the select operation on each React re-render. @@ -7,8 +7,8 @@ function select(data: ListActiveWorkspacesResponse | undefined): string | null { return data?.workspaces?.[0]?.name ?? null; } -export function useActiveWorkspaceName() { - return useActiveWorkspaces({ +export function useQueryActiveWorkspaceName() { + return useQueryListActiveWorkspaces({ select, }); } diff --git a/src/hooks/use-query-get-workspace-messages.ts b/src/hooks/use-query-get-workspace-messages.ts index fbb7e2be..2e7de014 100644 --- a/src/hooks/use-query-get-workspace-messages.ts +++ b/src/hooks/use-query-get-workspace-messages.ts @@ -1,29 +1,30 @@ import { useQuery } from "@tanstack/react-query"; import { - Conversation, V1GetWorkspaceMessagesResponse, V1GetWorkspaceMessagesData, } from "@/api/generated"; import { v1GetWorkspaceMessagesOptions } from "@/api/generated/@tanstack/react-query.gen"; -import { useActiveWorkspaceName } from "@/features/workspace/hooks/use-active-workspace-name"; +import { useQueryActiveWorkspaceName } from "@/hooks/use-query-active-workspace-name"; import { getQueryCacheConfig } from "@/lib/react-query-utils"; +import { useMemo } from "react"; -// NOTE: This needs to be a stable function reference to enable memo-isation of -// the select operation on each React re-render. -function select(data: V1GetWorkspaceMessagesResponse): Conversation[] { - return data.filter((prompt) => - prompt.question_answers?.every((item) => item.answer && item.question), - ); -} - -export const useQueryGetWorkspaceMessages = () => { - const { data: activeWorkspaceName } = useActiveWorkspaceName(); +export const useQueryGetWorkspaceMessages = < + T = V1GetWorkspaceMessagesResponse, +>({ + select, +}: { + select?: (data: V1GetWorkspaceMessagesResponse) => T; +} = {}) => { + const { data: activeWorkspaceName } = useQueryActiveWorkspaceName(); - const options: V1GetWorkspaceMessagesData = { - path: { - workspace_name: activeWorkspaceName ?? "default", - }, - }; + const options: V1GetWorkspaceMessagesData = useMemo( + () => ({ + path: { + workspace_name: activeWorkspaceName ?? "default", + }, + }), + [activeWorkspaceName], + ); return useQuery({ ...v1GetWorkspaceMessagesOptions(options), diff --git a/src/features/workspace/hooks/use-active-workspaces.ts b/src/hooks/use-query-list-active-workspaces.ts similarity index 84% rename from src/features/workspace/hooks/use-active-workspaces.ts rename to src/hooks/use-query-list-active-workspaces.ts index 9996d3f5..4aa87c64 100644 --- a/src/features/workspace/hooks/use-active-workspaces.ts +++ b/src/hooks/use-query-list-active-workspaces.ts @@ -2,7 +2,7 @@ import { ListActiveWorkspacesResponse } from "@/api/generated"; import { v1ListActiveWorkspacesOptions } from "@/api/generated/@tanstack/react-query.gen"; import { useQuery } from "@tanstack/react-query"; -export function useActiveWorkspaces({ +export function useQueryListActiveWorkspaces({ select, }: { select?: (data?: ListActiveWorkspacesResponse) => T; diff --git a/src/hooks/use-models-data.ts b/src/hooks/use-query-list-all-models-for-all-providers.ts similarity index 79% rename from src/hooks/use-models-data.ts rename to src/hooks/use-query-list-all-models-for-all-providers.ts index b9fc280b..5b731708 100644 --- a/src/hooks/use-models-data.ts +++ b/src/hooks/use-query-list-all-models-for-all-providers.ts @@ -1,7 +1,7 @@ import { useQuery } from "@tanstack/react-query"; import { v1ListAllModelsForAllProvidersOptions } from "@/api/generated/@tanstack/react-query.gen"; -export const useModelsData = () => { +export const useQueryListAllModelsForAllProviders = () => { return useQuery({ ...v1ListAllModelsForAllProvidersOptions(), }); diff --git a/src/features/workspace/hooks/use-query-list-all-workspaces.ts b/src/hooks/use-query-list-all-workspaces.ts similarity index 100% rename from src/features/workspace/hooks/use-query-list-all-workspaces.ts rename to src/hooks/use-query-list-all-workspaces.ts diff --git a/src/features/workspace/hooks/use-list-workspaces.ts b/src/hooks/use-query-list-workspaces.ts similarity index 85% rename from src/features/workspace/hooks/use-list-workspaces.ts rename to src/hooks/use-query-list-workspaces.ts index 03afcf01..5c78c0b8 100644 --- a/src/features/workspace/hooks/use-list-workspaces.ts +++ b/src/hooks/use-query-list-workspaces.ts @@ -2,7 +2,7 @@ import { useQuery } from "@tanstack/react-query"; import { v1ListWorkspacesOptions } from "@/api/generated/@tanstack/react-query.gen"; import { V1ListWorkspacesResponse } from "@/api/generated"; -export function useListWorkspaces({ +export function useQueryListWorkspaces({ select, }: { select?: (data: V1ListWorkspacesResponse) => T; diff --git a/src/hooks/useCurrentPromptStore.ts b/src/hooks/useCurrentPromptStore.ts deleted file mode 100644 index 28b7004f..00000000 --- a/src/hooks/useCurrentPromptStore.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { create } from "zustand"; - -type CurrentPromptState = { - currentPromptId: string; - setCurrentPromptId: (id: string) => void; -}; - -export const useCurrentPromptStore = create((set) => ({ - currentPromptId: "", - setCurrentPromptId: (id: string) => set({ currentPromptId: id }), -})); diff --git a/src/index.css b/src/index.css index 2ba4db1c..20998261 100644 --- a/src/index.css +++ b/src/index.css @@ -4,9 +4,11 @@ html, body { - height: 100%; - margin: 0; - overflow: hidden; +} + +html { + scrollbar-gutter: stable; + overflow-x: hidden; } :root { diff --git a/src/lib/currency.ts b/src/lib/currency.ts index 70a83d24..accacdb3 100644 --- a/src/lib/currency.ts +++ b/src/lib/currency.ts @@ -7,7 +7,7 @@ type FormatCurrencyOptions = { to_minor?: boolean; }; -export const getCurrencyFormatOptions = (currency: Currency) => { +const getCurrencyFormatOptions = (currency: Currency) => { return new Intl.NumberFormat(undefined, { currency: currency, currencyDisplay: "code", @@ -70,10 +70,3 @@ export function convertCurrencyFromMinor( ): number { return amount / 10 ** getDigits(currency); } - -export function formatCurrencyWithTrailingZeroes( - number: number, - currency: Currency, -) { - return number.toFixed(getDigits(currency)); -} diff --git a/src/lib/format-time.ts b/src/lib/format-time.ts new file mode 100644 index 00000000..c6c3882d --- /dev/null +++ b/src/lib/format-time.ts @@ -0,0 +1,23 @@ +import { formatDistanceToNow } from "date-fns"; + +type Format = "relative" | "absolute"; + +export const formatTime = ( + date: Date, + options: { + format: Format; + } = { + format: "relative", + }, +) => { + switch (options.format) { + case "absolute": + return date.toLocaleString(); + case "relative": + return formatDistanceToNow(date, { + addSuffix: true, + }); + default: + return options.format satisfies never; + } +}; diff --git a/src/features/alerts/lib/is-alert-critical.ts b/src/lib/is-alert-critical.ts similarity index 100% rename from src/features/alerts/lib/is-alert-critical.ts rename to src/lib/is-alert-critical.ts diff --git a/src/lib/is-alert-malicious.ts b/src/lib/is-alert-malicious.ts new file mode 100644 index 00000000..4a458dba --- /dev/null +++ b/src/lib/is-alert-malicious.ts @@ -0,0 +1,17 @@ +import { Alert, AlertConversation, Conversation } from "@/api/generated"; + +export function isConversationWithMaliciousAlerts( + conversation: Conversation | null, +): boolean { + return conversation?.alerts?.some(isAlertMalicious) ?? false; +} + +export function isAlertMalicious(alert: Alert | AlertConversation | null) { + return ( + alert?.trigger_category === "critical" && + alert.trigger_string !== null && + typeof alert.trigger_string === "object" && + "status" in alert.trigger_string && + alert.trigger_string.status === "malicious" + ); +} diff --git a/src/lib/is-alert-secret.ts b/src/lib/is-alert-secret.ts new file mode 100644 index 00000000..36d825d6 --- /dev/null +++ b/src/lib/is-alert-secret.ts @@ -0,0 +1,14 @@ +import { Alert, AlertConversation, Conversation } from "@/api/generated"; + +export function isConversationWithSecretAlerts( + conversation: Conversation | null, +): boolean { + return conversation?.alerts?.some(isAlertSecret) ?? false; +} + +export function isAlertSecret(alert: Alert | AlertConversation | null) { + return ( + alert?.trigger_category === "critical" && + alert.trigger_type === "codegate-secrets" + ); +} diff --git a/src/lib/test-utils.tsx b/src/lib/test-utils.tsx index 8dc22683..cf3e1394 100644 --- a/src/lib/test-utils.tsx +++ b/src/lib/test-utils.tsx @@ -1,4 +1,3 @@ -import { SidebarProvider } from "@/components/ui/sidebar"; import { ConfirmProvider } from "@/context/confirm-context"; import { DarkModeProvider, Toaster } from "@stacklok/ui-kit"; import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; @@ -28,6 +27,9 @@ export const TestQueryClientProvider = ({ new QueryClient({ defaultOptions: { queries: { + refetchOnMount: true, + refetchOnReconnect: true, + refetchOnWindowFocus: true, gcTime: 0, staleTime: 0, }, @@ -52,10 +54,7 @@ const renderWithProviders = ( - {children}} - /> + diff --git a/src/lib/utils.ts b/src/lib/utils.ts index 976388a4..64df408c 100644 --- a/src/lib/utils.ts +++ b/src/lib/utils.ts @@ -1,12 +1,5 @@ -import { AlertConversation, Conversation } from "@/api/generated/types.gen"; -import { isAlertSecret } from "@/features/alerts/lib/is-alert-secret"; -import { isAlertMalicious } from "@/features/alerts/lib/is-alert-malicious"; -import { format, isToday, isYesterday } from "date-fns"; +import { format } from "date-fns"; -const ONE_DAY_MS = 24 * 60 * 60 * 1000; -const SEVEN_DAYS_MS = 7 * ONE_DAY_MS; -const TEEN_DAYS_MS = 14 * ONE_DAY_MS; -const THTY_DAYS_MS = 30 * ONE_DAY_MS; const FILEPATH_REGEX = /(?:---FILEPATH|Path:|\/\/\s*filepath:)\s*([^\s]+)/g; const COMPARE_CODE_REGEX = /Compare this snippet[^:]*:/g; @@ -50,52 +43,6 @@ export function parsingPromptText(message: string, timestamp: string) { } } -function getGroup(differenceInMs: number, promptDate: Date): string { - if (isToday(promptDate)) { - return "Today"; - } - if (isYesterday(promptDate)) { - return "Yesterday"; - } - if (differenceInMs <= SEVEN_DAYS_MS) { - return "Previous 7 days"; - } - if (differenceInMs <= TEEN_DAYS_MS) { - return "Previous 14 days"; - } - if (differenceInMs <= THTY_DAYS_MS) { - return "Previous 30 days"; - } - return "Beyond 30 days"; -} - -export function groupPromptsByRelativeDate(prompts: Conversation[]) { - const promptsSorted = prompts.sort( - (a, b) => - new Date(b.conversation_timestamp).getTime() - - new Date(a.conversation_timestamp).getTime() - ); - - const grouped = promptsSorted.reduce( - (groups, prompt) => { - const promptDate = new Date(prompt.conversation_timestamp); - const now = new Date(); - const differenceInMs = now.getTime() - promptDate.getTime(); - const group = getGroup(differenceInMs, promptDate); - - if (!groups[group]) { - groups[group] = []; - } - - (groups[group] ?? []).push(prompt); - return groups; - }, - {} as Record - ); - - return grouped; -} - export function sanitizeQuestionPrompt({ question, answer, @@ -123,17 +70,3 @@ export function sanitizeQuestionPrompt({ return question; } } - -export function getIssueDetectedType( - alert: AlertConversation -): "malicious_package" | "leaked_secret" | null { - if (isAlertMalicious(alert)) return "malicious_package"; - if (isAlertSecret(alert)) return "leaked_secret"; - - return null; -} - -export function capitalize(text: string) { - const [first, ...rest] = text; - return first ? first.toUpperCase() + rest.join("") : text; -} diff --git a/src/main.tsx b/src/main.tsx index 7398fed0..0628ebc3 100644 --- a/src/main.tsx +++ b/src/main.tsx @@ -3,7 +3,7 @@ import { createRoot } from "react-dom/client"; import "./index.css"; import "@stacklok/ui-kit/style"; import App from "./App.tsx"; -import { SidebarProvider } from "./components/ui/sidebar.tsx"; + import ErrorBoundary from "./components/ErrorBoundary.tsx"; import { Error } from "./components/Error.tsx"; import { DarkModeProvider, Toaster } from "@stacklok/ui-kit"; @@ -24,20 +24,18 @@ createRoot(document.getElementById("root")!).render( - - - }> - - - - - - - - - + + }> + + + + + + + + - , + ); diff --git a/src/mocks/msw/fixtures/GET_MESSAGES.json b/src/mocks/msw/fixtures/GET_MESSAGES.json deleted file mode 100644 index 8e8b1158..00000000 --- a/src/mocks/msw/fixtures/GET_MESSAGES.json +++ /dev/null @@ -1,306 +0,0 @@ -[ - { - "question_answers": [ - { - "question": { - "message": "```/Users/giuseppe/workspace/codegate/src/codegate/server.py\nimport traceback\n\nimport structlog\nfrom fastapi import APIRouter, FastAPI\nfrom fastapi.middleware.cors import CORSMiddleware\nfrom fastapi.responses import JSONResponse\nfrom starlette.middleware.errors import ServerErrorMiddleware\n\nfrom codegate import __description__, __version__\nfrom codegate.dashboard.dashboard import dashboard_router\nfrom codegate.pipeline.factory import PipelineFactory\nfrom codegate.providers.anthropic.provider import AnthropicProvider\nfrom codegate.providers.llamacpp.provider import LlamaCppProvider\nfrom codegate.providers.ollama.provider import OllamaProvider\nfrom codegate.providers.openai.provider import OpenAIProvider\nfrom codegate.providers.registry import ProviderRegistry\nfrom codegate.providers.vllm.provider import VLLMProvider\n\nlogger = structlog.get_logger(\"codegate\")\n\n\nasync def custom_error_handler(request, exc: Exception):\n \"\"\"This is a Middleware to handle exceptions and log them.\"\"\"\n # Capture the stack trace\n extracted_traceback = traceback.extract_tb(exc.__traceback__)\n # Log only the last 3 items of the stack trace. 3 is an arbitrary number.\n logger.error(traceback.print_list(extracted_traceback[-3:]))\n return JSONResponse({\"error\": str(exc)}, status_code=500)\n\n\ndef init_app(pipeline_factory: PipelineFactory) -> FastAPI:\n \"\"\"Create the FastAPI application.\"\"\"\n app = FastAPI(\n title=\"CodeGate\",\n description=__description__,\n version=__version__,\n )\n app.add_middleware(\n CORSMiddleware,\n allow_origins=[\"*\"],\n allow_credentials=True,\n allow_methods=[\"*\"],\n allow_headers=[\"*\"],\n )\n # Apply error handling middleware\n app.add_middleware(ServerErrorMiddleware, handler=custom_error_handler)\n\n # Create provider registry\n registry = ProviderRegistry(app)\n\n # Register all known providers\n registry.add_provider(\n \"openai\",\n OpenAIProvider(\n pipeline_processor=pipeline_factory.create_input_pipeline(),\n fim_pipeline_processor=pipeline_factory.create_fim_pipeline(),\n output_pipeline_processor=pipeline_factory.create_output_pipeline(),\n fim_output_pipeline_processor=pipeline_factory.create_fim_output_pipeline(),\n ),\n )\n registry.add_provider(\n \"anthropic\",\n AnthropicProvider(\n pipeline_processor=pipeline_factory.create_input_pipeline(),\n fim_pipeline_processor=pipeline_factory.create_fim_pipeline(),\n output_pipeline_processor=pipeline_factory.create_output_pipeline(),\n fim_output_pipeline_processor=pipeline_factory.create_fim_output_pipeline(),\n ),\n )\n registry.add_provider(\n \"llamacpp\",\n LlamaCppProvider(\n pipeline_processor=pipeline_factory.create_input_pipeline(),\n fim_pipeline_processor=pipeline_factory.create_fim_pipeline(),\n output_pipeline_processor=pipeline_factory.create_output_pipeline(),\n fim_output_pipeline_processor=pipeline_factory.create_fim_output_pipeline(),\n ),\n )\n registry.add_provider(\n \"vllm\",\n VLLMProvider(\n pipeline_processor=pipeline_factory.create_input_pipeline(),\n fim_pipeline_processor=pipeline_factory.create_fim_pipeline(),\n output_pipeline_processor=pipeline_factory.create_output_pipeline(),\n fim_output_pipeline_processor=pipeline_factory.create_fim_output_pipeline(),\n ),\n )\n registry.add_provider(\n \"ollama\",\n OllamaProvider(\n pipeline_processor=pipeline_factory.create_input_pipeline(),\n fim_pipeline_processor=pipeline_factory.create_fim_pipeline(),\n output_pipeline_processor=pipeline_factory.create_output_pipeline(),\n fim_output_pipeline_processor=pipeline_factory.create_fim_output_pipeline(),\n ),\n )\n\n # Create and add system routes\n system_router = APIRouter(tags=[\"System\"])\n\n @system_router.get(\"/health\")\n async def health_check():\n return {\"status\": \"healthy\"}\n\n app.include_router(system_router)\n app.include_router(dashboard_router)\n\n return app\n\n```\n\nserver.py do you see any security issue?", - "timestamp": "2025-01-03T09:09:33.743797Z", - "message_id": "a1549789-b610-4999-9907-f8f010c2f3b7" - }, - "answer": { - "message": "Yes, there are several security issues in the provided `server.py` code. Here are the main ones:\n\n1. **CORS Configuration**:\n ```python\n app.add_middleware(\n CORSMiddleware,\n allow_origins=[\"*\"],\n allow_credentials=True,\n allow_methods=[\"*\"],\n allow_headers=[\"*\"],\n )\n ```\n - Allowing all origins (`allow_origins=[\"*\"]`) and methods (`allow_methods=[\"*\"]`) can expose your server to Cross-Site Request Forgery (CSRF) attacks and other security vulnerabilities. It's recommended to restrict these settings to only the origins and methods that your application needs to support.\n\n2. **Error Handling Middleware**:\n ```python\n async def custom_error_handler(request, exc: Exception):\n \"\"\"This is a Middleware to handle exceptions and log them.\"\"\"\n # Capture the stack trace\n extracted_traceback = traceback.extract_tb(exc.__traceback__)\n # Log only the last 3 items of the stack trace. 3 is an arbitrary number.\n logger.error(traceback.print_list(extracted_traceback[-3:]))\n return JSONResponse({\"error\": str(exc)}, status_code=500)\n ```\n - Exposing detailed error messages in the response can be dangerous, as it can provide attackers with information that can be used to exploit vulnerabilities. It's better to log the error details and return a generic error message to the client.\n\n3. **Handling Exceptions**:\n - The `custom_error_handler` function logs the traceback and returns the exception message directly to the client. This can expose sensitive information about the server's internal state. It's better to log the error and return a generic message.\n\n4. **Provider Configuration**:\n - The providers are configured with the same pipeline processors. This might not be a security issue in itself, but it's important to ensure that the pipeline processors are secure and correctly configured.\n\n5. **Logging**:\n - The `logger.error` function is used to log the error traceback. Ensure that the logging configuration is set up correctly to prevent logging sensitive information to insecure destinations.\n\nHere is a revised version of the code with some of these issues addressed:\n\n```python\nimport traceback\n\nimport structlog\nfrom fastapi import APIRouter, FastAPI\nfrom fastapi.middleware.cors import CORSMiddleware\nfrom fastapi.responses import JSONResponse\nfrom starlette.middleware.errors import ServerErrorMiddleware\n\nfrom codegate import __description__, __version__\nfrom codegate.dashboard.dashboard import dashboard_router\nfrom codegate.pipeline.factory import PipelineFactory\nfrom codegate.providers.anthropic.provider import AnthropicProvider\nfrom codegate.providers.llamacpp.provider import LlamaCppProvider\nfrom codegate.providers.ollama.provider import OllamaProvider\nfrom codegate.providers.openai.provider import OpenAIProvider\nfrom codegate.providers.registry import ProviderRegistry\nfrom codegate.providers.vllm.provider import VLLMProvider\n\nlogger = structlog.get_logger(\"codegate\")\n\n\nasync def custom_error_handler(request, exc: Exception):\n \"\"\"This is a Middleware to handle exceptions and log them.\"\"\"\n # Capture the stack trace\n extracted_traceback = traceback.extract_tb(exc.__traceback__)\n # Log only the last 3 items of the stack trace. 3 is an arbitrary number.\n logger.error(traceback.print_list(extracted_traceback[-3:]))\n return JSONResponse({\"error\": \"An internal server error occurred\"}, status_code=500)\n\n\ndef init_app(pipeline_factory: PipelineFactory) -> FastAPI:\n \"\"\"Create the FastAPI application.\"\"\"\n app = FastAPI(\n title=\"CodeGate\",\n description=__description__,\n version=__version__,\n )\n app.add_middleware(\n CORSMiddleware,\n allow_origins=[\"https://example.com\"], # Restrict to specific origins\n allow_credentials=True,\n allow_methods=[\"GET\", \"POST\"], # Restrict to specific methods\n allow_headers=[\"*\"],\n )\n # Apply error handling middleware\n app.add_middleware(ServerErrorMiddleware, handler=custom_error_handler)\n\n # Create provider registry\n registry = ProviderRegistry(app)\n\n # Register all known providers\n registry.add_provider(\n \"openai\",\n OpenAIProvider(\n pipeline_processor=pipeline_factory.create_input_pipeline(),\n fim_pipeline_processor=pipeline_factory.create_fim_pipeline(),\n output_pipeline_processor=pipeline_factory.create_output_pipeline(),\n fim_output_pipeline_processor=pipeline_factory.create_fim_output_pipeline(),\n ),\n )\n registry.add_provider(\n \"anthropic\",\n AnthropicProvider(\n pipeline_processor=pipeline_factory.create_input_pipeline(),\n fim_pipeline_processor=pipeline_factory.create_fim_pipeline(),\n output_pipeline_processor=pipeline_factory.create_output_pipeline(),\n fim_output_pipeline_processor=pipeline_factory.create_fim_output_pipeline(),\n ),\n )\n registry.add_provider(\n \"llamacpp\",\n LlamaCppProvider(\n pipeline_processor=pipeline_factory.create_input_pipeline(),\n fim_pipeline_processor=pipeline_factory.create_fim_pipeline(),\n output_pipeline_processor=pipeline_factory.create_output_pipeline(),\n fim_output_pipeline_processor=pipeline_factory.create_fim_output_pipeline(),\n ),\n )\n registry.add_provider(\n \"vllm\",\n VLLMProvider(\n pipeline_processor=pipeline_factory.create_input_pipeline(),\n fim_pipeline_processor=pipeline_factory.create_fim_pipeline(),\n output_pipeline_processor=pipeline_factory.create_output_pipeline(),\n fim_output_pipeline_processor=pipeline_factory.create_fim_output_pipeline(),\n ),\n )\n registry.add_provider(\n \"ollama\",\n OllamaProvider(\n pipeline_processor=pipeline_factory.create_input_pipeline(),\n fim_pipeline_processor=pipeline_factory.create_fim_pipeline(),\n output_pipeline_processor=pipeline_factory.create_output_pipeline(),\n fim_output_pipeline_processor=pipeline_factory.create_fim_output_pipeline(),\n ),\n )\n\n # Create and add system routes\n system_router = APIRouter(tags=[\"System\"])\n\n @system_router.get(\"/health\")\n async def health_check():\n return {\"status\": \"healthy\"}\n\n app.include_router(system_router)\n app.include_router(dashboard_router)\n\n return app\n```\n\nIn this revised version, the CORS middleware is restricted to specific origins and methods, and the error handler returns a generic error message to the client.", - "timestamp": "2025-01-03T09:09:34.359269Z", - "message_id": "68e7c9bc-9afa-4240-8eba-621eaae4a2a0" - } - } - ], - "provider": "vllm", - "type": "chat", - "chat_id": "chatcmpl-eebad2bd68434c7c947d10754a204ea4", - "conversation_timestamp": "2025-01-03T09:09:33.743797Z" - }, - { - "question_answers": [ - { - "question": { - "message": "\n\n```tsx src/App.tsx (23-29)\n const { prompts, loading, fetchPrompts } = usePromptsStore();\n useSse();\n\n const test = \"REDACTED<$lCzMsqxUWElmZqd/qX4llR3pqsrBJVxkt6SB6WXXKKhYG37ZnscxPawjfZmgPVoSpcSR975hOIE2s/6iVr2JETpVdwdaG4hAYlzJ4tteJg==>\";\n const breadcrumb = useBreadcrumb();\n\n\n```\nPlease explain the cause of this error and how to solve it: 'test' is declared but its value is never read.", - "timestamp": "2025-01-02T13:42:48.721165Z", - "message_id": "031cf7a3-1ce6-435d-bbee-0f939b30cc20" - }, - "answer": { - "message": "The error message \"'test' is declared but its value is never read\" indicates that the variable `test` is defined in your code, but it is never used anywhere. This can lead to confusion for other developers reading the code and might indicate that the intended functionality has not been implemented.\n\n### Cause of the Error\n\n1. **Unused Variable**: The variable `test` is declared but not used in the rest of the code within the `App` component. This can be a result of incomplete implementation or a placeholder that was meant to be used later but was forgotten.\n\n### How to Solve It\n\nYou have a few options to resolve this issue:\n\n1. **Remove the Unused Variable**:\n - If the variable is not needed, you can safely remove it to clean up the code and eliminate the warning.\n\n ```tsx\n const { prompts, loading, fetchPrompts } = usePromptsStore();\n useSse();\n\n const breadcrumb = useBreadcrumb();\n\n useEffect(() => {\n fetchPrompts();\n }, [fetchPrompts]);\n ```\n\n2. **Use the Variable**:\n - If the variable is intended to be used, ensure that it is utilized somewhere in the component. For example, you could log it for debugging purposes or use it in some part of the application logic.\n\n ```tsx\n const { prompts, loading, fetchPrompts } = usePromptsStore();\n useSse();\n\n const test = \"REDACTED<$lCzMsqxUWElmZqd/qX4llR3pqsrBJVxkt6SB6WXXKKhYG37ZnscxPawjfZmgPVoSpcSR975hOIE2s/6iVr2JETpVdwdaG4hAYlzJ4tteJg==>\";\n const breadcrumb = useBreadcrumb();\n\n useEffect(() => {\n console.log(test); // Example usage\n fetchPrompts();\n }, [fetchPrompts]);\n ```\n\n3. **Suppress the Warning (Not Recommended)**:\n - If you are sure that the variable is intentionally unused and do not want to remove it, you can suppress the warning. However, this is generally not recommended as it can lead to oversight of real issues.\n\n ```tsx\n // @ts-ignore\n const test = \"REDACTED<$lCzMsqxUWElmZqd/qX4llR3pqsrBJVxkt6SB6WXXKKhYG37ZnscxPawjfZmgPVoSpcSR975hOIE2s/6iVr2JETpVdwdaG4hAYlzJ4tteJg==>\";\n const breadcrumb = useBreadcrumb();\n ```\n\n### Best Practices\n\n- **Keep Code Clean**: Regularly clean up unused variables to maintain a clean and understandable codebase.\n- **Use Linters**: Utilize tools like ESLint to automatically detect and flag unused variables, helping you maintain code quality.\n\nBy addressing this issue, you can improve the maintainability and readability of your code.", - "timestamp": "2025-01-02T13:42:49.361714Z", - "message_id": "a542408b-d0bc-4f6f-8b71-64c93c1e388a" - } - } - ], - "provider": "vllm", - "type": "chat", - "chat_id": "chatcmpl-7d87679de7ed41639eb91d8ebbaa6f72", - "conversation_timestamp": "2025-01-02T13:42:48.721165Z" - }, - { - "question_answers": [ - { - "question": { - "message": "```/Users/giuseppe/workspace/codegate-ui/src/App.tsx\nimport { Header } from \"./components/Header\";\nimport { PromptList } from \"./components/PromptList\";\nimport { useEffect } from \"react\";\nimport { Dashboard } from \"./components/Dashboard\";\nimport { Routes, Route, Link } from \"react-router-dom\";\nimport { Chat } from \"./components/Chat\";\nimport { usePromptsStore } from \"./hooks/usePromptsStore\";\nimport { Sidebar } from \"./components/Sidebar\";\nimport { useSse } from \"./hooks/useSse\";\nimport { Help } from \"./components/Help\";\nimport { Certificates } from \"./components/Certificates\";\nimport { CertificateSecurity } from \"./components/CertificateSecurity\";\nimport {\n Breadcrumb,\n BreadcrumbList,\n BreadcrumbItem,\n BreadcrumbSeparator,\n BreadcrumbPage,\n} from \"./components/ui/breadcrumb\";\nimport { useBreadcrumb } from \"./hooks/useBreadcrumb\";\n\nfunction App() {\n const { prompts, loading, fetchPrompts } = usePromptsStore();\n useSse();\n\n const test = \"REDACTED<$4g3iWc/V3YD6pAXIeA43M197ostjpL3XZuDbJuxTN+jG1Mcjv6cMFcC73SsIDzECoWSuZqSYGkrWe5vQqbheaJbDlocBrPZMkgQWkcP8gw==>\";\n const breadcrumb = useBreadcrumb();\n\n useEffect(() => {\n fetchPrompts();\n }, [fetchPrompts]);\n\n return (\n
    \n \n \n \n
    \n
    \n\n
    \n \n \n \n Dashboard\n \n {breadcrumb && (\n <>\n \n \n \n {breadcrumb}\n \n \n \n )}\n \n \n
    \n\n
    \n \n } />\n } />\n } />\n } />\n }\n />\n \n
    \n
    \n
    \n );\n}\n\nexport default App;\n\n```\n\nApp.tsx could you review?", - "timestamp": "2025-01-02T13:40:18.343146Z", - "message_id": "33b28648-c3a8-4f52-a110-e59a19dc6fab" - }, - "answer": { - "message": "**CodeGate Security Analysis**\n\n### Security Vulnerabilities and Potential Risks\n\n1. **Unsanitized User Input and Cross-Site Scripting (XSS)**\n - The `breadcrumb` variable, which is derived from user interaction, is directly rendered within the `` component. If the `breadcrumb` contains user-controlled data, it could be vulnerable to XSS attacks if not properly sanitized.\n\n2. **Potential Sensitive Data Exposure**\n - The `test` variable contains a string that begins with \"REDACTED\", indicating it may contain sensitive information. Ensure that such strings are not exposed or logged in a manner that could lead to data breaches.\n\n3. **Lack of Authentication and Authorization**\n - The code does not include any authentication or authorization mechanisms. This could allow unauthorized users to access sensitive components like `/certificates` and `/certificates/security`.\n\n4. **Security of Sensitive Components**\n - Components like `CertificateSecurity` and `Certificates` might handle sensitive information. Ensure that these components implement proper security measures to protect the data they handle.\n\n### Best Practices for Secure Implementation\n\n1. **Sanitize and Encode User Input**\n - Always sanitize and encode user inputs to prevent XSS attacks. Use libraries like `DOMPurify` to sanitize HTML content.\n\n2. **Implement Authentication and Authorization**\n - Integrate a robust authentication system (e.g., OAuth, JWT) to ensure that only authorized users can access sensitive routes and components.\n\n3. **Protect Sensitive Data**\n - Ensure that any sensitive data, such as certificates and security-related information, is encrypted both at rest and in transit. Use HTTPS for all communications.\n\n4. **Regular Security Audits and Testing**\n - Conduct regular security audits and penetration testing to identify and fix vulnerabilities in the application.\n\n### Recommendations for Security Improvements\n\n1. **Sanitize Breadcrumb Data**\n - Modify the `BreadcrumbPage` component to sanitize the `breadcrumb` data before rendering it. For example:\n ```jsx\n \n {DOMPurify.sanitize(breadcrumb)}\n \n ```\n\n2. **Implement Authentication**\n - Use a library like `react-router-dom` with authentication middleware to protect routes. For example:\n ```jsx\n \n \n \n }\n />\n ```\n\n3. **Secure Components**\n - Ensure that components like `CertificateSecurity` and `Certificates` implement proper security measures, such as input validation, encryption, and access controls.\n\n4. **Review and Secure Dependencies**\n - Regularly review and update dependencies to ensure they are secure and free from known vulnerabilities. Use tools like `npm audit` to identify and fix vulnerabilities.\n\n### References to Relevant Security Standards or Guidelines\n\n- **OWASP Top Ten**: A list of the most critical security risks to web applications, including XSS and injection flaws.\n- **OWASP Security Testing Guide**: Provides detailed guidance on how to test for various security vulnerabilities.\n- **NIST Cybersecurity Framework**: Offers a comprehensive set of guidelines for managing and reducing cybersecurity risks.\n\nBy addressing these security considerations, you can significantly enhance the security posture of your application.", - "timestamp": "2025-01-02T13:40:18.881023Z", - "message_id": "82b89210-811a-478d-9229-e45bd3d60913" - } - } - ], - "provider": "vllm", - "type": "chat", - "chat_id": "chatcmpl-92720293078e459e9d65be5298a9dae6", - "conversation_timestamp": "2025-01-02T13:40:18.343146Z" - }, - { - "question_answers": [ - { - "question": { - "message": "hello", - "timestamp": "2025-01-02T13:39:39.656516Z", - "message_id": "1273fc1a-43e7-42c3-82b0-0057858d9b98" - }, - "answer": { - "message": "Hello! How can I assist you today?", - "timestamp": "2025-01-02T13:39:40.159594Z", - "message_id": "24aa2eec-f5bc-4275-8e30-a8f68d3f3002" - } - } - ], - "provider": "vllm", - "type": "chat", - "chat_id": "chatcmpl-40c88006b69940dab2d454175ff4c533", - "conversation_timestamp": "2025-01-02T13:39:39.656516Z" - }, - { - "question_answers": [ - { - "question": { - "message": "<|fim_prefix|>// diff --git a/src/App.tsx b/src/App.tsx\n// index fafa5bf..46b5fbd 100644\n// --- a/src/App.tsx\n// +++ b/src/App.tsx\n// @@ -22,6 +22,8 @@ import { useBreadcrumb } from \"./hooks/useBreadcrumb\";\n// function App() {\n// const { prompts, loading, fetchPrompts } = usePromptsStore();\n// useSse();\n// +\n// + cosnt test = REDACTED<$u0UEa5CGkrHrKv9oWKNC/aUuzW/D1d0ULifi+LvgIaGefP/xzkPr73YSZpciPs1YUAfjgcx35E3X81EwaKw4C68LTZuOXBBHEzZokLNZbg==>\n// const breadcrumb = useBreadcrumb();\n// \n// useEffect(() => {\n// src/App.tsx\nimport { useEffect } from \"react\";\nimport { Dashboard } from \"./components/Dashboard\";\nimport { Routes, Route, Link } from \"react-router-dom\";\nimport { Chat } from \"./components/Chat\";\nimport { usePromptsStore } from \"./hooks/usePromptsStore\";\nimport { Sidebar } from \"./components/Sidebar\";\nimport { useSse } from \"./hooks/useSse\";\nimport { Help } from \"./components/Help\";\nimport { Certificates } from \"./components/Certificates\";\nimport { CertificateSecurity } from \"./components/CertificateSecurity\";\nimport {\n Breadcrumb,\n BreadcrumbList,\n BreadcrumbItem,\n BreadcrumbSeparator,\n BreadcrumbPage,\n} from \"./components/ui/breadcrumb\";\nimport { useBreadcrumb } from \"./hooks/useBreadcrumb\";\n\nfunction App() {\n const { prompts, loading, fetchPrompts } = usePromptsStore();\n useSse();\n\n cosnt test = 'REDACTED<$m2fVY6C1zlk0swockzeQM/vDxL0h6im+Ber6vzYFFplvdR7dncAecjH18/Wptj0h3lQu7xCKxCFfCXxn7qe0clMZisPnRvohJOS/+W6P/m0++N/asPFkZnhJ3YPS>'\n const breadcrumb = useBreadcrumb();\n\n useEffect(() => {\n fetchPrompts();\n }, [fetchPrompts]);\n\n return (\n
    \n \n \n \n
    \n
    \n\n
    \n \n \n \n Dashboard\n \n {breadcrumb && (\n <>\n \n <|fim_middle|>", - "timestamp": "2025-01-02T13:40:06.097504Z", - "message_id": "ccf1c87a-4846-4d81-b69f-f882f1971cf7" - }, - "answer": null - } - ], - "provider": "vllm", - "type": "fim", - "chat_id": "ccf1c87a-4846-4d81-b69f-f882f1971cf7", - "conversation_timestamp": "2025-01-02T13:40:06.097504Z" - }, - { - "question_answers": [ - { - "question": { - "message": "<|im_start|>user\n```tsx\nimport { Header } from \"./components/Header\";\nimport { PromptList } from \"./components/PromptList\";\nimport { useEffect } from \"react\";\nimport { Dashboard } from \"./components/Dashboard\";\nimport { Routes, Route, Link } from \"react-router-dom\";\nimport { Chat } from \"./components/Chat\";\nimport { usePromptsStore } from \"./hooks/usePromptsStore\";\nimport { Sidebar } from \"./components/Sidebar\";\nimport { useSse } from \"./hooks/useSse\";\nimport { Help } from \"./components/Help\";\nimport { Certificates } from \"./components/Certificates\";\nimport { CertificateSecurity } from \"./components/CertificateSecurity\";\nimport {\n Breadcrumb,\n BreadcrumbList,\n BreadcrumbItem,\n BreadcrumbSeparator,\n BreadcrumbPage,\n} from \"./components/ui/breadcrumb\";\nimport { useBreadcrumb } from \"./hooks/useBreadcrumb\";\n\nfunction App() {\n const { prompts, loading, fetchPrompts } = usePromptsStore();\n useSse();\n\n const test = \"REDACTED<$lAJek6QoLZgSKq3z4ybqMVFOCtiHQtP8hbBzWeQysFQoGwwNrEghIPnFCsXY985bpTjx4ZIiBzx11E1yM3ybbUzoxG/W4TrUPzM4aHAjIg==>\";\n const breadcrumb = useBreadcrumb();\n\n useEffect(() => {\n fetchPrompts();\n }, [fetchPrompts]);\n\n return (\n
    \n \n \n \n
    \n
    \n\n
    \n \n \n \n Dashboard\n \n {breadcrumb && (\n <>\n \n \n \n {breadcrumb}\n \n \n \n )}\n \n \n
    \n\n
    \n \n } />\n } />\n } />\n } />\n }\n />\n \n
    \n
    \n
    \n );\n}\n\nexport default App;\n\n\n```\n\nPlease rewrite the entire code block above in order to satisfy the following request: \"The following code was suggested as an edit:\n```\n// @ts-ignore\nconst test = \"REDACTED<$gGYIqfbrVCHST8Xn/yfGtFDfQQ3Kn6mQ9t1+YrUCKYxNEiRYb4u9BQmxZkOQE5I+9UZRasD0/bY3m00v4OSo+PI6Zzn97RSVNJgM47nUlQ==>\";\nconst breadcrumb = useBreadcrumb();\n```\nPlease apply it to the previous code.\". You should rewrite the entire code block without leaving placeholders, even if the code is the same as before.<|im_end|>\n<|im_start|>assistant\nSure! Here's the entire rewritten code block:\n```tsx\n", - "timestamp": "2025-01-02T14:19:58.024100Z", - "message_id": "b97fbe59-0e34-4b98-8f2f-41332ebc059a" - }, - "answer": null - } - ], - "provider": "vllm", - "type": "fim", - "chat_id": "b97fbe59-0e34-4b98-8f2f-41332ebc059a", - "conversation_timestamp": "2025-01-02T14:19:58.024100Z" - }, - { - "question_answers": [ - { - "question": { - "message": "<|fim_prefix|>// diff --git a/package.json b/package.json\n// index 3b62d87..4774c65 100644\n// --- a/package.json\n// +++ b/package.json\n// @@ -21,7 +21,6 @@\n// \"@radix-ui/react-switch\": \"^1.1.1\",\n// \"@radix-ui/react-tooltip\": \"^1.1.6\",\n// \"@types/prismjs\": \"^1.26.5\",\n// - \"@types/react-copy-to-clipboard\": \"^5.0.7\",\n// \"@types/react-syntax-highlighter\": \"^15.5.13\",\n// \"class-variance-authority\": \"^0.7.1\",\n// \"clsx\": \"^2.1.1\",\n// @@ -29,7 +28,6 @@\n// \"lucide-react\": \"^0.462.0\",\n// \"prismjs\": \"^1.29.0\",\n// \"react\": \"^18.3.1\",\n// - \"react-copy-to-clipboard\": \"^5.1.0\",\n// \"react-dom\": \"^18.3.1\",\n// \"react-markdown\": \"^9.0.1\",\n// \"react-router-dom\": \"^7.0.2\",\n// @@ -61,4 +59,4 @@\n// \"typescript-eslint\": \"^8.15.0\",\n// \"vite\": \"^6.0.1\"\n// }\n// -}\n// +}\n// \\ No newline at end of file\n// diff --git a/src/components/CopyToClipboard.tsx b/src/components/CopyToClipboard.tsx\n// new file mode 100644\n// index 0000000..682af0c\n// --- /dev/null\n// +++ b/src/components/CopyToClipboard.tsx\n// @@ -0,0 +1,31 @@\n// +import clsx from \"clsx\";\n// +import { ClipboardCopy } from \"lucide-react\";\n// +\n// +export function CopyToClipboard({\n// + text,\n// + className,\n// +}: {\n// + className?: string;\n// + text: string;\n// +}) {\n// + const copyToClipboard = async () => {\n// + try {\n// + await navigator.clipboard.writeText(text);\n// + } catch {}\n// + };\n// +\n// + return (\n// + \n// + \n// + \n// + );\n// +}\n// Path: Untitled.txt\n// navigator.clipboard.writeText\n// components/CopyToClipboard.tsx\nimport clsx from \"clsx\";\nimport { ClipboardCopy } from \"lucide-react\";\n\nexport function CopyToClipboard({\n text,\n className,\n}: {\n className?: string;\n text: string;\n}) {\n const copyToClipboard = async () => {\n try {\n await navigator.clipboard.writeText(text);\n } catch {}\n };\n\n return (\n hover:bg-gray-700/70 \n transition-opacity duration-200 \n opacity-0 group-hover:opacity-100`,\n className\n )}\n >\n \n \n );\n}\n<|fim_middle|>", - "timestamp": "2025-01-02T15:00:12.114817Z", - "message_id": "27318899-9e9b-4dfa-9e61-f482dcd24e54" - }, - "answer": null - } - ], - "provider": "vllm", - "type": "fim", - "chat_id": "27318899-9e9b-4dfa-9e61-f482dcd24e54", - "conversation_timestamp": "2025-01-02T15:00:12.114817Z" - }, - { - "question_answers": [ - { - "question": { - "message": "<|fim_prefix|>\n// components/Markdown.tsx\nimport remarkGfm from \"remark-gfm\";\nimport ReactMarkdown from \"react-markdown\";\nimport { Prism as SyntaxHighlighter } from \"react-syntax-highlighter\";\nimport { oneDark } from \"react-syntax-highlighter/dist/esm/styles/prism\";\nimport { cn } from \"@/lib/utils\";\nimport { CopyToClipboard } from \"./CopyToClipboard\";\n\ninterface Props {\n children: string;\n className?: string;\n}\n\nconst customStyle = {\n ...oneDark,\n 'pre[class*=\"language-\"]': {\n ...oneDark['pre[class*=\"language-\"]'],\n whiteSpace: \"pre-wrap\",\n background: \"#re<|fim_suffix|>\",\n padding: \"1.5rem\",\n borderRadius: \"0.5rem\",\n margin: \"1.5rem 0\",\n fontSize: \"10px\",\n width: \"80%\", // Ensure the block takes full width\n position: \"relative\",\n },\n};\n\nexport function Markdown({ children, className = \"\" }: Props) {\n return (\n <|fim_middle|>", - "timestamp": "2025-01-02T17:01:41.579128Z", - "message_id": "4ea5d1df-c724-49f7-a132-d19e092b52c8" - }, - "answer": null - } - ], - "provider": "vllm", - "type": "fim", - "chat_id": "4ea5d1df-c724-49f7-a132-d19e092b52c8", - "conversation_timestamp": "2025-01-02T17:01:41.579128Z" - }, - { - "question_answers": [ - { - "question": { - "message": "<|fim_prefix|>// Path: Untitled.txt\n// 1a1b26\n// components/Markdown.tsx\n // whiteSpace: \"pre-wrap\",\n // background: \"#1a1b26\",\n // padding: \"1.5rem\",\n // borderRadius: \"0.5rem\",\n // margin: \"1.5rem 0\",\n // fontSize: \"10px\",\n // width: \"80%\", // Ensure the block takes full width\n // position: \"relative\",\n // },\n};\n\nexport function Markdown({ children, className = \"\" }: Props) {\n return (\n \n 1a1b26\",\n // fontSize: \"10x\",\n // whiteSpace: \"pre-wrap\",\n // padding: \"1.5rem\",\n // borderRadius: \"0.5rem\",\n // margin: \"1.5rem 0\",\n // position: \"relative\", // Critical for clipboard positioning\n // width: \"100%\", // Ensure full width of parent container\n // boxSizing: \"border-box\", // Prevent padding overflow\n },\n }}\n language={match[1]}\n PreTag=\"div\"\n className=\"rounded-lg overflow-hidden shadow-lg text-sm\"\n showLineNumbers={false}\n wrapLines\n {...props}\n ><|fim_middle|>", - "timestamp": "2025-01-02T17:08:08.428889Z", - "message_id": "203fe119-0e58-43b5-a863-73f30f73d272" - }, - "answer": null - } - ], - "provider": "vllm", - "type": "fim", - "chat_id": "203fe119-0e58-43b5-a863-73f30f73d272", - "conversation_timestamp": "2025-01-02T17:08:08.428889Z" - }, - { - "question_answers": [ - { - "question": { - "message": "<|fim_prefix|>\n// components/Markdown.tsx\n}\n\nconst customStyle = {\n ...oneDark,\n 'pre[class*=\"language-\"]': {\n ...oneDark['pre[class*=\"language-\"]'],\n whiteSpace: \"pre-wrap\",\n // background: \"#1a1b26\",\n padding: \"1.5rem\",\n borderRadius: \"0.5rem\",\n margin: \"1.5rem 0\",\n fontSize: \"10px\",\n width: \"80%\", // Ensure the block takes full width\n position: \"relative\",\n },\n};\n\nexport function Markdown({ children, className = \"\" }: Props) {\n return (\n \n \"\n showLineNumbers={false}\n wrapLines\n {...props}\n >\n {String(children).replace(/\\n$/, \"\")}\n \n \n
    \n ) : (\n ", - "timestamp": "2025-01-02T17:20:32.921469Z", - "message_id": "d81ca662-84ae-4114-9041-6d640c547bb9" - }, - "answer": null - } - ], - "provider": "vllm", - "type": "fim", - "chat_id": "d81ca662-84ae-4114-9041-6d640c547bb9", - "conversation_timestamp": "2025-01-02T17:20:32.921469Z" - }, - { - "question_answers": [ - { - "question": { - "message": "<|fim_prefix|>// Path: Untitled.txt\n// showLineNumbers\n// components/Markdown.tsx\n background: \"none\",\n padding: \"0\",\n borderRadius: \"0\",\n fontSize: \"inherit\",\n color: \"inherit\",\n display: \"inline\",\n },\n 'pre[class*=\"language-\"]': {\n ...oneDark['pre[class*=\"language-\"]'],\n whiteSpace: \"pre-wrap\",\n background: \"#1a1b26\",\n padding: \"1.5rem\",\n borderRadius: \"0.5rem\",\n margin: \"1.5rem 0\",\n fontSize: \"10px\",\n width: \"80%\",\n position: \"relative\",\n },\n};\nexport function Markdown({ children, className = \"\" }: Props) {\n return (\n \n ={{\n ...customStyle,\n 'pre[class*=\"language-\"]': {\n ...oneDark['pre[class*=\"language-\"]'],\n background: \"#1a1b26\",\n padding: \"1.5rem\",\n borderRadius: \"0.5rem\",\n margin: \"1.5rem 0\",\n width: \"100%\",\n position: \"relative\",\n boxSizing: \"border-box\",\n },\n }}\n language={match[1]}\n PreTag=\"div\"\n className=\"rounded-lg overflow-hidden shadow-lg text-sm\"\n wrapLines\n {...props}\n >\n {String(children).replace(/\\n$/, \"\")}\n <|fim_middle|>", - "timestamp": "2025-01-02T17:25:40.052416Z", - "message_id": "bb2ab808-1ce7-4937-9d79-0d773dcb7111" - }, - "answer": null - } - ], - "provider": "vllm", - "type": "fim", - "chat_id": "bb2ab808-1ce7-4937-9d79-0d773dcb7111", - "conversation_timestamp": "2025-01-02T17:25:40.052416Z" - }, - { - "question_answers": [ - { - "question": { - "message": "<|fim_prefix|>// diff --git a/package.json b/package.json\n// index 42ed9d9..6b1ddde 100644\n// --- a/package.json\n// +++ b/package.json\n// @@ -25,6 +25,7 @@\n// \"class-variance-authority\": \"^0.7.1\",\n// \"clsx\": \"^2.1.1\",\n// \"date-fns\": \"^4.1.0\",\n// + \"highlight.js\": \"^11.11.1\",\n// \"lucide-react\": \"^0.462.0\",\n// \"prismjs\": \"^1.29.0\",\n// \"react\": \"19.0.0\",\n// components/Markdown.tsx\nimport hljs from \"highlight.js\";\n\ninterface Props {\n children: string;\n className?: string;\n}\nconsole.log({ oneDark });\nconst customStyle = {\n ...oneDark,\n 'code[class*=\"language-\"]': {\n ...oneDark['code[class*=\"language-\"]'],\n background: \"none\",\n },\n 'pre[class*=\"language-\"]': {\n ...oneDark['pre[class*=\"language-\"]'],\n whiteSpace: \"pre-wrap\",\n background: \"#1a1b26\",\n padding: \"1.5rem\",\n borderRadius: \"0.5rem\",\n margin: \"1.5rem 0\",\n fontSize: \"10px\",\n width: \"80%\",\n position: \"relative\",\n },\n};\nexport function Markdown({ children, className = \"\" }: Props) {\n \n return (\n ).language;\n console.log({ detectedLanguage });\n const match = /language-(\\w+)/.exec(className || \"\");\n const inline = !match;\n return !inline ? (\n
    \n ", - "timestamp": "2025-01-03T10:22:01.721847Z", - "message_id": "9b522dca-a54e-4356-9692-d5be66f051cc" - }, - "answer": null - } - ], - "provider": "vllm", - "type": "fim", - "chat_id": "9b522dca-a54e-4356-9692-d5be66f051cc", - "conversation_timestamp": "2025-01-03T10:22:01.721847Z" - }, - { - "question_answers": [ - { - "question": { - "message": "<|fim_prefix|>// diff --git a/package.json b/package.json\n// index 42ed9d9..6b1ddde 100644\n// --- a/package.json\n// +++ b/package.json\n// @@ -25,6 +25,7 @@\n// \"class-variance-authority\": \"^0.7.1\",\n// \"clsx\": \"^2.1.1\",\n// \"date-fns\": \"^4.1.0\",\n// + \"highlight.js\": \"^11.11.1\",\n// \"lucide-react\": \"^0.462.0\",\n// \"prismjs\": \"^1.29.0\",\n// \"react\": \"19.0.0\",\n// Path: Untitled.txt\n// a11yDark\n// components/Markdown.tsx\nconst customStyle = {\n ...oneDark,\n 'code[class*=\"language-\"]': {\n ...oneDark['code[class*=\"language-\"]'],\n background: \"none\",\n },\n 'pre[class*=\"language-\"]': {\n ...oneDark['pre[class*=\"language-\"]'],\n whiteSpace: \"pre-wrap\",\n background: \"#1a1b26\",\n padding: \"1.5rem\",\n borderRadius: \"0.5rem\",\n margin: \"1.5rem 0\",\n fontSize: \"10px\",\n width: \"80%\",\n position: \"relative\",\n },\n};\nexport function Markdown({ children, className = \"\" }: Props) {\n return (\n \n return (\n <>\n \n {String(children).replace(/\\n$/, \"\")}<|fim_middle|>", - "timestamp": "2025-01-03T10:59:20.862404Z", - "message_id": "b21cb89e-f5f0-4cbe-95a6-7407bf59199d" - }, - "answer": null - } - ], - "provider": "vllm", - "type": "fim", - "chat_id": "b21cb89e-f5f0-4cbe-95a6-7407bf59199d", - "conversation_timestamp": "2025-01-03T10:59:20.862404Z" - }, - { - "question_answers": [ - { - "question": { - "message": "<|fim_prefix|>// diff --git a/package.json b/package.json\n// index 42ed9d9..6b1ddde 100644\n// --- a/package.json\n// +++ b/package.json\n// @@ -25,6 +25,7 @@\n// \"class-variance-authority\": \"^0.7.1\",\n// \"clsx\": \"^2.1.1\",\n// \"date-fns\": \"^4.1.0\",\n// + \"highlight.js\": \"^11.11.1\",\n// \"lucide-react\": \"^0.462.0\",\n// \"prismjs\": \"^1.29.0\",\n// \"react\": \"19.0.0\",\n// components/Markdown.tsx\n children: string;\n className?: string;\n}\nconsole.log({ oneDark });\nconst customStyle = {\n ...oneDark,\n 'code[class*=\"language-\"]': {\n ...oneDark['code[class*=\"language-\"]'],\n background: \"none\",\n },\n 'pre[class*=\"language-\"]': {\n ...oneDark['pre[class*=\"language-\"]'],\n whiteSpace: \"pre-wrap\",\n background: \"#1a1b26\",\n padding: \"1.5rem\",\n borderRadius: \"0.5rem\",\n margin: \"1.5rem 0\",\n fontSize: \"10px\",\n width: \"80%\",\n position: \"relative\",\n },\n};\nexport function Markdown({ children, className = \"\" }: Props) {\n return (\n >\n \n {String(children).replace(/\\n$/, \"\")}\n <|fim_middle|>", - "timestamp": "2025-01-03T11:05:21.749423Z", - "message_id": "a5bd2d58-4339-4bbd-a5b0-8f65ff758b46" - }, - "answer": null - } - ], - "provider": "vllm", - "type": "fim", - "chat_id": "a5bd2d58-4339-4bbd-a5b0-8f65ff758b46", - "conversation_timestamp": "2025-01-03T11:05:21.749423Z" - }, - { - "question_answers": [ - { - "question": { - "message": "<|fim_prefix|>// diff --git a/package.json b/package.json\n// index 42ed9d9..6b1ddde 100644\n// --- a/package.json\n// +++ b/package.json\n// @@ -25,6 +25,7 @@\n// \"class-variance-authority\": \"^0.7.1\",\n// \"clsx\": \"^2.1.1\",\n// \"date-fns\": \"^4.1.0\",\n// + \"highlight.js\": \"^11.11.1\",\n// \"lucide-react\": \"^0.462.0\",\n// \"prismjs\": \"^1.29.0\",\n// \"react\": \"19.0.0\",\n// diff --git a/src/components/ui/chat/chat-bubble.tsx b/src/components/ui/chat/chat-bubble.tsx\n// index b9d20ec..1be8760 100644\n// --- a/src/components/ui/chat/chat-bubble.tsx\n// +++ b/src/components/ui/chat/chat-bubble.tsx\n// @@ -107,7 +107,7 @@ const ChatBubbleMessage = React.forwardRef<\n// const customStyle = {\n ...oneDark,\n 'code[class*=\"language-\"]': {\n ...oneDark['code[class*=\"language-\"]'],\n background: \"none\",\n },\n 'pre[class*=\"language-\"]': {\n ...oneDark['pre[class*=\"language-\"]'],\n whiteSpace: \"pre-wrap\",\n background: \"#1a1b26\",\n padding: \"1.5rem\",\n borderRadius: \"0.5rem\",\n margin: \"1.5rem 0\",\n fontSize: \"10px\",\n width: \"80%\",\n position: \"relative\",\n },\n};\nexport function Markdown({ children, className = \"\" }: Props) {\n return (\n ", - "timestamp": "2025-01-03T11:32:56.031157Z", - "message_id": "63dee5cb-444e-484c-8259-83be0ea073ac" - }, - "answer": null - } - ], - "provider": "vllm", - "type": "fim", - "chat_id": "63dee5cb-444e-484c-8259-83be0ea073ac", - "conversation_timestamp": "2025-01-03T11:32:56.031157Z" - }, - { - "question_answers": [ - { - "question": { - "message": "<|fim_prefix|>// diff --git a/package.json b/package.json\n// index 42ed9d9..6b1ddde 100644\n// --- a/package.json\n// +++ b/package.json\n// @@ -25,6 +25,7 @@\n// \"class-variance-authority\": \"^0.7.1\",\n// \"clsx\": \"^2.1.1\",\n// \"date-fns\": \"^4.1.0\",\n// + \"highlight.js\": \"^11.11.1\",\n// \"lucide-react\": \"^0.462.0\",\n// \"prismjs\": \"^1.29.0\",\n// \"react\": \"19.0.0\",\n// diff --git a/src/components/ui/chat/chat-bubble.tsx b/src/components/ui/chat/chat-bubble.tsx\n// index b9d20ec..1be8760 100644\n// --- a/src/components/ui/chat/chat-bubble.tsx\n// +++ b/src/components/ui/chat/chat-bubble.tsx\n// @@ -107,7 +107,7 @@ const ChatBubbleMessage = React.forwardRef<\n// \n} from \"react-syntax-highlighter\";\nimport { oneDark } from \"react-syntax-highlighter/dist/esm/styles/prism\";\nimport { cn } from \"@/lib/utils\";\nimport { CopyToClipboard } from \"./CopyToClipboard\";\nimport hljs from \"highlight.js\";\n\nconst LANGUAGES_SUBSET_DETECTION = [\n \"bash\",\n \"c\",\n \"cpp\",\n \"csharp\",\n \"css\",\n \"elixir\",\n \"go\",\n \"groovy\",\n \"haskell\",\n \"html\",\n \"java\",\n \"javascript\",\n \"json\",\n \"kotlin\",\n \"lua\",\n \"markdown\",\n \"perl\",\n \"php\",<|fim_middle|>", - "timestamp": "2025-01-03T11:56:42.129289Z", - "message_id": "cd2f0343-5b65-4848-a694-79acb3e4de7b" - }, - "answer": null - } - ], - "provider": "vllm", - "type": "fim", - "chat_id": "cd2f0343-5b65-4848-a694-79acb3e4de7b", - "conversation_timestamp": "2025-01-03T11:56:42.129289Z" - }, - { - "question_answers": [ - { - "question": { - "message": "<|fim_prefix|>// diff --git a/package.json b/package.json\n// index 42ed9d9..6b1ddde 100644\n// --- a/package.json\n// +++ b/package.json\n// @@ -25,6 +25,7 @@\n// \"class-variance-authority\": \"^0.7.1\",\n// \"clsx\": \"^2.1.1\",\n// \"date-fns\": \"^4.1.0\",\n// + \"highlight.js\": \"^11.11.1\",\n// \"lucide-react\": \"^0.462.0\",\n// \"prismjs\": \"^1.29.0\",\n// \"react\": \"19.0.0\",\n// diff --git a/src/components/ui/chat/chat-bubble.tsx b/src/components/ui/chat/chat-bubble.tsx\n// index b9d20ec..1be8760 100644\n// --- a/src/components/ui/chat/chat-bubble.tsx\n// +++ b/src/components/ui/chat/chat-bubble.tsx\n// @@ -107,7 +107,7 @@ const ChatBubbleMessage = React.forwardRef<\n// /prism\";\nimport { cn } from \"@/lib/utils\";\nimport { CopyToClipboard } from \"./CopyToClipboard\";\n// import hljs from \"highlight.js\";\n\nconst LANGUAGES_SUBSET_DETECTION = [\n \"bash\",\n \"c\",\n \"cpp\",\n \"csharp\",\n \"css\",\n \"elixir\",\n \"go\",\n \"groovy\",\n \"haskell\",\n \"html\",\n \"java\",\n \"javascript\",\n \"json\",\n \"kotlin\",\n \"lua\",\n \"markdown\",\n \"perl\",\n \"php\",\n \"python\",\n \"ruby\",\n \"rust\",\n \"scala\",\n \"sql\",\n \"swift\",\n \"typescript\",<|fim_middle|>", - "timestamp": "2025-01-03T12:10:18.944843Z", - "message_id": "52bf2933-2a34-4d78-8368-9fccb26da0a2" - }, - "answer": null - } - ], - "provider": "vllm", - "type": "fim", - "chat_id": "52bf2933-2a34-4d78-8368-9fccb26da0a2", - "conversation_timestamp": "2025-01-03T12:10:18.944843Z" - }, - { - "question_answers": [ - { - "question": { - "message": "<|fim_prefix|>// diff --git a/package.json b/package.json\n// index 42ed9d9..6b1ddde 100644\n// --- a/package.json\n// +++ b/package.json\n// @@ -25,6 +25,7 @@\n// \"class-variance-authority\": \"^0.7.1\",\n// \"clsx\": \"^2.1.1\",\n// \"date-fns\": \"^4.1.0\",\n// + \"highlight.js\": \"^11.11.1\",\n// \"lucide-react\": \"^0.462.0\",\n// \"prismjs\": \"^1.29.0\",\n// \"react\": \"19.0.0\",\n// diff --git a/src/components/ui/chat/chat-bubble.tsx b/src/components/ui/chat/chat-bubble.tsx\n// index b9d20ec..1be8760 100644\n// --- a/src/components/ui/chat/chat-bubble.tsx\n// +++ b/src/components/ui/chat/chat-bubble.tsx\n// @@ -107,7 +107,7 @@ const ChatBubbleMessage = React.forwardRef<\n// \n return (\n
    \n ", - "timestamp": "2025-01-03T14:47:23.644901Z", - "message_id": "b57a9d41-c022-4e72-9b80-e96eed86a6ba" - }, - "answer": null - } - ], - "provider": "vllm", - "type": "fim", - "chat_id": "b57a9d41-c022-4e72-9b80-e96eed86a6ba", - "conversation_timestamp": "2025-01-03T14:47:23.644901Z" - } -] \ No newline at end of file diff --git a/src/mocks/msw/mockers/conversation.mock.ts b/src/mocks/msw/mockers/conversation.mock.ts index e0461e68..9a0f16ca 100644 --- a/src/mocks/msw/mockers/conversation.mock.ts +++ b/src/mocks/msw/mockers/conversation.mock.ts @@ -37,7 +37,7 @@ export function mockConversation({ length: typeof alertsConfig?.numAlerts === "number" ? alertsConfig?.numAlerts - : faker.number.int({ min: 0, max: 5 }), + : faker.number.int({ min: 2, max: 5 }), }).map(() => mockAlert({ type: diff --git a/src/routes/__tests__/route-chat.test.tsx b/src/routes/__tests__/route-chat.test.tsx index 60eb927a..9af830ef 100644 --- a/src/routes/__tests__/route-chat.test.tsx +++ b/src/routes/__tests__/route-chat.test.tsx @@ -1,7 +1,17 @@ import { render } from "@/lib/test-utils"; -import { screen, within } from "@testing-library/react"; -import { describe, expect, it } from "vitest"; +import { screen, waitFor, within } from "@testing-library/react"; +import { expect, it, vi } from "vitest"; import { RouteChat } from "../route-chat"; +import { server } from "@/mocks/msw/node"; +import { http, HttpResponse } from "msw"; +import { mswEndpoint } from "@/test/msw-endpoint"; +import { mockConversation } from "@/mocks/msw/mockers/conversation.mock"; +import { getConversationTitle } from "@/features/dashboard-messages/lib/get-conversation-title"; +import { formatTime } from "@/lib/format-time"; +import userEvent from "@testing-library/user-event"; +import { getProviderString } from "@/features/dashboard-messages/lib/get-provider-string"; +import { isAlertMalicious } from "@/lib/is-alert-malicious"; +import { isAlertSecret } from "@/lib/is-alert-secret"; vi.mock("@stacklok/ui-kit", async (importOriginal) => { return { @@ -19,69 +29,211 @@ vi.mock("@/hooks/useCurrentPromptStore", () => ({ })), })); -// TODO: Replace this with MSW -vi.mock("@/hooks/use-query-get-workspace-messages", () => ({ - useQueryGetWorkspaceMessages: vi.fn(() => ({ - data: [ - { - question_answers: [ - { - question: { - message: - '\n\n```tsx src/App.tsx (23-29)\n const apiKey = "REDACTED<$lCzMsqxUWElmZqd/qX4llR3pqsrBJVxkt6SB6WXXKKhYG37ZnscxPawjfZmgPVoSpcSR975hOIE2s/6iVr2JETpVdwdaG4hAYlzJ4tteJg==>";\n\n\n```\nDo you see any security issue?.', - timestamp: "2025-01-02T13:42:48.721165Z", - message_id: "031cf7a3-1ce6-435d-bbee-0f939b30cc20", - }, - answer: { - message: - '**CodeGate Security Analysis**\n\nThe provided code snippet contains a string that is likely an encoded value, specifically a Base64 encoded string. Here is a detailed analysis:\n\n### Security Vulnerabilities and Potential Risks\n\n1. **Sensitive Data Handling**:\n - The string starts with "REDACTED", indicating that it contains sensitive information that has been masked or obfuscated. However, the actual value is still present and could be potentially revealing if decoded.\n - Using "REDACTED" is a good practice for hiding sensitive information in logs, but it\'s important to ensure that the actual sensitive data is not exposed.\n\n2. **Decoding Risks**:\n - If the string is Base64 encoded, decoding it could reveal sensitive information such as API keys, tokens, or other credentials.\n - Decoding and handling such sensitive data in client-side code (like `App.tsx`) can expose it to potential attackers if the data is transmitted or logged.\n\n3. **Potential for Injection**:\n - If the decoded string is used in any form of user input or external communication, there is a risk of injection attacks (e.g., SQL injection, cross-site scripting, etc.).\n\n### Best Practices for Secure Implementation\n\n1. **Avoid Hardcoding Sensitive Data**:\n - Never hardcode sensitive data such as API keys, tokens, or passwords directly in your source code. Use environment variables or secure vaults to manage such data.\n\n2. **Proper Encoding and Decoding**:\n - Ensure that encoding and decoding operations are performed securely and that the decoded data is handled with care to prevent unauthorized access or usage.\n\n3. **Data Validation and Sanitization**:\n - Always validate and sanitize any data that is used in your application. This includes checking for malicious content or unexpected formats.\n\n4. **Secure Logging**:\n - Avoid logging sensitive data directly. If logging is necessary, ensure that sensitive information is either sanitized or replaced with placeholders.\n\n### Recommendations for Security Improvements\n\n1. **Use Environment Variables**:\n - Replace the hardcoded string with an environment variable. For example:\n ```typescript\n const test = process.env.REACT_APP_REDACTED_TOKEN;\n ```\n - Ensure that environment variables are securely managed and not exposed in version control systems.\n\n2. **Implement Access Controls**:\n - Restrict access to sensitive data to only those parts of your application that require it. Use role-based access controls (RBAC) to manage permissions.\n\n3. **Regular Security Audits**:\n - Conduct regular security audits and code reviews to identify and address potential security vulnerabilities.\n\n4. **Stay Updated with Security Standards**:\n - Follow security best practices and guidelines such as those provided by OWASP (Open Web Application Security Project) and other relevant organizations.\n\n### References to Relevant Security Standards or Guidelines\n\n- **OWASP**: [OWASP Security Guidelines](https://owasp.org/)\n- **Secure Coding Practices**: [Secure Coding Guidelines](https://www.securecoding.org/)\n- **Environment Variables**: [12factor.net - Config](https://12factor.net/config)\n\nBy following these best practices and recommendations, you can help ensure that your application handles sensitive data securely and mitigates potential security risks.', - timestamp: "2025-01-02T13:42:49.361714Z", - message_id: "a542408b-d0bc-4f6f-8b71-64c93c1e388a", - }, - }, - ], - provider: "vllm", - type: "chat", - chat_id: "chatcmpl-7d87679de7ed41639eb91d8ebbaa6f72", - conversation_timestamp: "2025-01-02T13:42:48.721165Z", - }, - ], - })), -})); +it("renders breadcrumbs", async () => { + const conversation = mockConversation(); -describe("Chat", () => { - it("render secret issue chat", () => { - render(, { - routeConfig: { - initialEntries: ["/prompt/chatcmpl-7d87679de7ed41639eb91d8ebbaa6f72"], - }, - pathConfig: "/prompt/:id", - }); + server.use( + http.get(mswEndpoint("/api/v1/workspaces/:workspace_name/messages"), () => + HttpResponse.json([conversation]), + ), + ); - expect(screen.getByText(/REDACTED, { + routeConfig: { + initialEntries: [`/prompt/${conversation.chat_id}`], + }, + pathConfig: "/prompt/:id", + }); + await waitFor(() => { const breadcrumbs = screen.getByRole("list", { name: "Breadcrumbs" }); expect(breadcrumbs).toBeVisible(); + expect( within(breadcrumbs).getByRole("link", { name: "Dashboard" }), ).toHaveAttribute("href", "/"); - expect( - within(breadcrumbs).getByText(/do you see any security issue/i), - ).toBeVisible(); + }); +}); + +it("renders title", async () => { + const conversation = mockConversation(); + + server.use( + http.get(mswEndpoint("/api/v1/workspaces/:workspace_name/messages"), () => + HttpResponse.json([conversation]), + ), + ); + + render(, { + routeConfig: { + initialEntries: [`/prompt/${conversation.chat_id}`], + }, + pathConfig: "/prompt/:id", + }); + + await waitFor(() => { + const heading = screen.getByRole("heading", { + level: 1, + }); + + expect(heading).toHaveTextContent(getConversationTitle(conversation)); + expect(heading).toHaveTextContent( + formatTime(new Date(conversation.conversation_timestamp)), + ); + }); +}); + +it("renders conversation summary correctly", async () => { + const conversation = mockConversation({ alertsConfig: { numAlerts: 10 } }); + + const maliciousCount = conversation.alerts.filter(isAlertMalicious).length; + const secretsCount = conversation.alerts.filter(isAlertSecret).length; + + server.use( + http.get(mswEndpoint("/api/v1/workspaces/:workspace_name/messages"), () => + HttpResponse.json([conversation]), + ), + ); + + render(, { + routeConfig: { + initialEntries: [`/prompt/${conversation.chat_id}`], + }, + pathConfig: "/prompt/:id", + }); + + await waitFor(() => { + expect(screen.getByLabelText("Conversation summary")).toBeVisible(); + }); + + const { getByText } = within(screen.getByLabelText("Conversation summary")); + + expect(getByText(getProviderString(conversation.provider))).toBeVisible(); + + expect( + getByText( + formatTime(new Date(conversation.conversation_timestamp), { + format: "absolute", + }), + ), + ).toBeVisible(); + + expect(getByText(conversation.chat_id)).toBeVisible(); + + expect( + getByText(`${maliciousCount} malicious packages detected`), + ).toBeVisible(); + + expect(getByText(`${secretsCount} secrets detected`)).toBeVisible(); +}); + +it("renders chat correctly", async () => { + const conversation = mockConversation(); + + const question = conversation.question_answers[0].question.message; + const answer = conversation.question_answers[0].answer.message; + + server.use( + http.get(mswEndpoint("/api/v1/workspaces/:workspace_name/messages"), () => + HttpResponse.json([conversation]), + ), + ); + + render(, { + routeConfig: { + initialEntries: [`/prompt/${conversation.chat_id}`], + }, + pathConfig: "/prompt/:id", + }); + + await waitFor(() => { + const { getByText } = within( + screen.getByLabelText("Conversation transcript"), + ); + expect(getByText(question)).toBeVisible(); + expect(getByText(answer)).toBeVisible(); + }); +}); + +it("renders tabs", async () => { + const conversation = mockConversation(); + + server.use( + http.get(mswEndpoint("/api/v1/workspaces/:workspace_name/messages"), () => + HttpResponse.json([conversation]), + ), + ); + + render(, { + routeConfig: { + initialEntries: [`/prompt/${conversation.chat_id}`], + }, + pathConfig: "/prompt/:id", + }); + + await waitFor(() => { + expect(screen.getByRole("tab", { name: /overview/i })).toBeVisible(); + expect(screen.getByRole("tab", { name: /secrets/i })).toBeVisible(); + }); +}); + +it("can navigate using tabs", async () => { + const conversation = mockConversation(); + + server.use( + http.get(mswEndpoint("/api/v1/workspaces/:workspace_name/messages"), () => + HttpResponse.json([conversation]), + ), + ); + + render(, { + routeConfig: { + initialEntries: [ + { + pathname: `/prompt/${conversation.chat_id}`, + }, + ], + }, + pathConfig: "/prompt/:id", + }); + + await waitFor(() => { + expect(screen.getByRole("tab", { name: /overview/i })).toBeVisible(); + expect(screen.getByRole("tab", { name: /secrets/i })).toBeVisible(); + + expect(screen.getByRole("tab", { name: /overview/i })).toHaveAttribute( + "data-selected", + "true", + ); + expect(screen.getByRole("tab", { name: /secrets/i })).not.toHaveAttribute( + "data-selected", + "true", + ); + }); + + await userEvent.click(screen.getByRole("tab", { name: /secrets/i })); + + await waitFor(() => { + expect(screen.getByRole("tab", { name: /overview/i })).not.toHaveAttribute( + "data-selected", + "true", + ); + expect(screen.getByRole("tab", { name: /secrets/i })).toHaveAttribute( + "data-selected", + "true", + ); + }); + + await userEvent.click(screen.getByRole("tab", { name: /overview/i })); + + await waitFor(() => { + expect(screen.getByRole("tab", { name: /overview/i })).toHaveAttribute( + "data-selected", + "true", + ); + expect(screen.getByRole("tab", { name: /secrets/i })).not.toHaveAttribute( + "data-selected", + "true", + ); }); }); diff --git a/src/routes/__tests__/route-dashboard.test.tsx b/src/routes/__tests__/route-dashboard.test.tsx index 6d422dd4..965866e9 100644 --- a/src/routes/__tests__/route-dashboard.test.tsx +++ b/src/routes/__tests__/route-dashboard.test.tsx @@ -1,401 +1,193 @@ import { render } from "@/lib/test-utils"; import { screen, waitFor, within } from "@testing-library/react"; -import { describe, expect, it } from "vitest"; -import { faker } from "@faker-js/faker"; +import { expect, it } from "vitest"; import { server } from "@/mocks/msw/node"; import { HttpResponse, http } from "msw"; -import mockedAlerts from "@/mocks/msw/fixtures/GET_ALERTS.json"; import userEvent from "@testing-library/user-event"; import { RouteDashboard } from "../route-dashboard"; import { mswEndpoint } from "@/test/msw-endpoint"; -const fakeConversionation1 = { - conversation: { - question_answers: [ - { - question: { - message: - "\n\n```tsx src/App.tsx (23-29)\n const { prompts, loading, fetchPrompts } = usePromptsStore();\n useSse();\n\n const test = \"REDACTED<$lCzMsqxUWElmZqd/qX4llR3pqsrBJVxkt6SB6WXXKKhYG37ZnscxPawjfZmgPVoSpcSR975hOIE2s/6iVr2JETpVdwdaG4hAYlzJ4tteJg==>\";\n const breadcrumb = useBreadcrumb();\n\n\n```\nPlease explain the cause of this error and how to solve it: 'test' is declared but its value is never read.", - timestamp: "2025-01-02T13:42:48.617799Z", - message_id: "c8a6afd1-98d7-43c4-97ff-7b183b8f6a5f", - }, - answer: { - message: - 'The error message "\'test\' is declared but its value is never read" indicates that the variable `test` is defined in your code, but it is never used anywhere. This can lead to confusion for other developers reading the code and might indicate that the intended functionality has not been implemented.\n\n### Cause of the Error\n\n1. **Unused Variable**: The variable `test` is declared but not used in the rest of the code within the `App` component. This can be a result of incomplete implementation or a placeholder that was meant to be used later but was forgotten.\n\n### How to Solve It\n\nYou have a few options to resolve this issue:\n\n1. **Remove the Unused Variable**:\n - If the variable is not needed, you can safely remove it to clean up the code and eliminate the warning.\n\n ```tsx\n const { prompts, loading, fetchPrompts } = usePromptsStore();\n useSse();\n\n const breadcrumb = useBreadcrumb();\n\n useEffect(() => {\n fetchPrompts();\n }, [fetchPrompts]);\n ```\n\n2. **Use the Variable**:\n - If the variable is intended to be used, ensure that it is utilized somewhere in the component. For example, you could log it for debugging purposes or use it in some part of the application logic.\n\n ```tsx\n const { prompts, loading, fetchPrompts } = usePromptsStore();\n useSse();\n\n const test = "REDACTED<$lCzMsqxUWElmZqd/qX4llR3pqsrBJVxkt6SB6WXXKKhYG37ZnscxPawjfZmgPVoSpcSR975hOIE2s/6iVr2JETpVdwdaG4hAYlzJ4tteJg==>";\n const breadcrumb = useBreadcrumb();\n\n useEffect(() => {\n console.log(test); // Example usage\n fetchPrompts();\n }, [fetchPrompts]);\n ```\n\n3. **Suppress the Warning (Not Recommended)**:\n - If you are sure that the variable is intentionally unused and do not want to remove it, you can suppress the warning. However, this is generally not recommended as it can lead to oversight of real issues.\n\n ```tsx\n // @ts-ignore\n const test = "REDACTED<$lCzMsqxUWElmZqd/qX4llR3pqsrBJVxkt6SB6WXXKKhYG37ZnscxPawjfZmgPVoSpcSR975hOIE2s/6iVr2JETpVdwdaG4hAYlzJ4tteJg==>";\n const breadcrumb = useBreadcrumb();\n ```\n\n### Best Practices\n\n- **Keep Code Clean**: Regularly clean up unused variables to maintain a clean and understandable codebase.\n- **Use Linters**: Utilize tools like ESLint to automatically detect and flag unused variables, helping you maintain code quality.\n\nBy addressing this issue, you can improve the maintainability and readability of your code.', - timestamp: "2025-01-02T13:42:49.361714Z", - message_id: "a542408b-d0bc-4f6f-8b71-64c93c1e388a", - }, - }, - ], - provider: "vllm", - type: "chat", - chat_id: "chatcmpl-7d87679de7ed41639eb91d8ebbaa6f72", - conversation_timestamp: "2025-01-02T13:42:48.617799Z", - }, - alert_id: "c8a6afd1-98d7-43c4-97ff-7b183b8f6a5f", - code_snippet: null, - trigger_string: - 'GitHub - Access Token:\n useSse();\n\n const test = "ghp_token";\n const breadcrumb = useBreadcrumb();\n\n', - trigger_type: "codegate-secrets", - trigger_category: "critical", - timestamp: "2025-01-02T13:42:48.617799Z", -}; - -const fakeConversionation2 = { - conversation: { - question_answers: [ - { - question: { - message: - '\n\n```py testing_file.py (1-15)\nimport invokehttp\nimport fastapi\nfrom fastapi import FastAPI, Request, Response, HTTPException\nimport numpy\n\n\n@app.route("/providers", methods=["GET"])\ndef get_providers():\n return jsonify([provider.to_dict() for provider in registry.providers])\n\nGITHUB_TOKEN="ghp_token"\n\nif __name__ == "__main__":\n app.run()\n\n```\nare there malicious?', - timestamp: "2024-12-09T17:50:04.418017Z", - message_id: "fa53ba0d-4fb0-4770-a563-88e504af621f", - }, - answer: { - message: - 'Warning: CodeGate detected one or more potentially malicious or archived packages.\n- Pkg 1: [trustypkg.dev/pypi/invokehttp](https://www.trustypkg.dev/pypi/invokehttp)\n\nYes, there is a security concern in your code. The `invokehttp` package is known to be malicious and should not be used in your project. Here\'s how to make your code more secure:\n\n1. Remove the malicious package and replace it with safe alternatives:\n - Instead of `invokehttp`, you can use well-maintained packages like `requests` or `httpx` for HTTP requests\n \nHere\'s the corrected version:\n\n```python testing_file.py\nimport requests # or httpx\nimport fastapi\nfrom fastapi import FastAPI, Request, Response, HTTPException\nimport numpy\n\n\n@app.route("/providers", methods=["GET"])\ndef get_providers():\n return jsonify([provider.to_dict() for provider in registry.providers])\n\nGITHUB_TOKEN="REDACTED<$SoFD847FAe6yVl6NWtk/p5+X8sQW5eU2NO0gHwbcCH4JtuEeS3Mi1IZ0BTM3oU4028c7+Vn9kkCvHQQvp8YnevjxIbZftv0xtK1iH7BgRqrAFTyh1A==>"\n\nif __name__ == "__main__":\n app.run()\n```\n\nAdditional security recommendations:\n1. Make sure to manage your dependencies using a requirements.txt or Pipfile\n2. Regularly update your dependencies to get security patches\n3. Consider using a virtual environment for your project\n4. Use package security scanning tools like `safety` or `bandit` to check for known vulnerabilities in your dependencies', - timestamp: "2024-12-09T17:50:20.846946Z", - message_id: "91b84560-2d36-4aab-807a-c6f4612dd5fd", - }, - }, - ], - provider: "anthropic", - type: "chat", - chat_id: "chatcmpl-0f3c7eb8-4306-452b-a415-b2de8867e734", - conversation_timestamp: "2024-12-09T17:50:04.418017Z", - }, - alert_id: "fa53ba0d-4fb0-4770-a563-88e504af621f", - code_snippet: null, - trigger_string: { - name: "invokehttp", - type: "pypi", - status: "malicious", - description: "Python HTTP for Humans.", - }, - trigger_type: "codegate-context-retriever", - trigger_category: "critical", - timestamp: "2024-12-09T17:50:04.418017Z", -}; - -function mockAlertsWithMaliciousPkg() { - server.use( - http.get(mswEndpoint("/api/v1/workspaces/:workspace_name/alerts"), () => { - return HttpResponse.json([fakeConversionation1, fakeConversionation2]); - }), - ); -} +import { mockConversation } from "@/mocks/msw/mockers/conversation.mock"; +import { faker } from "@faker-js/faker"; -function mockManyAlerts() { - server.use( - http.get(mswEndpoint("/api/v1/workspaces/:workspace_name/alerts"), () => { - return HttpResponse.json( - [ - ...mockedAlerts, - ...mockedAlerts, - ...mockedAlerts, - ...mockedAlerts, - ...mockedAlerts, - ].map((alert) => ({ ...alert, alert_id: faker.string.uuid() })), - ); - }), - ); -} +it("should mount alert summaries", async () => { + render(); -describe("Dashboard", () => { - it("should mount alert summaries", async () => { - render(); + expect( + screen.getByRole("heading", { name: /workspace token usage/i }), + ).toBeVisible(); - expect( - screen.getByRole("heading", { name: /workspace token usage/i }), - ).toBeVisible(); + expect( + screen.getByRole("heading", { name: /secrets redacted/i }), + ).toBeVisible(); - expect( - screen.getByRole("heading", { name: /secrets redacted/i }), - ).toBeVisible(); + expect( + screen.getByRole("heading", { name: /malicious packages/i }), + ).toBeVisible(); +}); - expect( - screen.getByRole("heading", { name: /malicious packages/i }), - ).toBeVisible(); - }); +it("should render messages table", async () => { + render(); - it("should render alerts table", async () => { - render(); - - expect( - screen.getByRole("grid", { - name: /alerts table/i, - }), - ).toBeVisible(); - expect(screen.getByTestId(/tab-all-count/i)).toHaveTextContent("0"); - - expect( - screen.getByRole("columnheader", { - name: /type/i, - }), - ).toBeVisible(); - expect( - screen.getByRole("columnheader", { - name: /event/i, - }), - ).toBeVisible(); - - expect( - screen.getByRole("columnheader", { - name: /time/i, - }), - ).toBeVisible(); - - expect( - screen.getByRole("columnheader", { - name: /issue detected/i, - }), - ).toBeVisible(); - - expect( - screen.getByRole("columnheader", { - name: /token usage/i, - }), - ).toBeVisible(); - - expect(screen.getByRole("searchbox")).toBeVisible(); - - await waitFor(() => { - expect( - within(screen.getByTestId("alerts-table")).getAllByRole("row").length, - ).toBeGreaterThan(1); - }); + expect( + screen.getByRole("grid", { + name: /alerts table/i, + }), + ).toBeVisible(); +}); - await waitFor(() => { - expect( - screen.queryByText("Connect CodeGate to your IDE"), - ).not.toBeInTheDocument(); - }); +it("shows only conversations with secrets when you click on the secrets tab", async () => { + server.use( + http.get(mswEndpoint("/api/v1/workspaces/:workspace_name/messages"), () => { + return HttpResponse.json([ + ...Array.from({ length: 10 }).map(() => + mockConversation({ + alertsConfig: { numAlerts: 10, type: "malicious" }, + }), + ), + ...Array.from({ length: 10 }).map(() => + mockConversation({ + alertsConfig: { numAlerts: 10, type: "secret" }, + }), + ), + ]); + }), + ); + render(); - const firstRow = within(screen.getByTestId("alerts-table")).getAllByRole( - "row", - )[1] as HTMLElement; - - expect(within(firstRow).getByText(/chat/i)).toBeVisible(); - expect(within(firstRow).getByText(/[0-9]+.*ago/i)).toBeVisible(); - expect( - screen.getAllByRole("gridcell", { - name: /blocked secret exposure/i, - }).length, - ).toBeGreaterThanOrEqual(1); + await waitFor(() => { + expect(screen.queryByText(/loading.../i)).not.toBeInTheDocument(); }); - it("should render malicious pkg table column", async () => { - mockAlertsWithMaliciousPkg(); - render(); - - await waitFor(() => { - expect(screen.queryByText(/loading.../i)).not.toBeInTheDocument(); - }); + expect(screen.getByTestId(/tab-all-count/i)).toHaveTextContent("20"); + expect(screen.getByTestId(/tab-secrets-count/i)).toHaveTextContent("10"); - expect( - screen.getByRole("gridcell", { - name: /blocked malicious package/i, - }), - ).toBeVisible(); - }); + await userEvent.click( + screen.getByRole("tab", { + name: /secrets/i, + }), + ); - it("renders event column", async () => { - mockAlertsWithMaliciousPkg(); - render(); + const tbody = screen.getAllByRole("rowgroup")[1] as HTMLElement; - await waitFor(() => { - expect(screen.getByText(/are there malicious/i)).toBeVisible(); + await waitFor(() => { + const secretsCountButtons = within(tbody).getAllByRole("button", { + name: /secrets count/, + }) as HTMLElement[]; + secretsCountButtons.forEach((e) => { + expect(e).toHaveTextContent("10"); }); }); +}); - it("should filter by malicious pkg", async () => { - mockAlertsWithMaliciousPkg(); - render(); - - await waitFor(() => { - expect(screen.queryByText(/loading.../i)).not.toBeInTheDocument(); - }); - - expect(screen.getByTestId(/tab-all-count/i)).toHaveTextContent("2"); - - expect( - screen.getAllByRole("gridcell", { - name: /chat/i, - }).length, - ).toBeGreaterThanOrEqual(1); - - userEvent.click( - screen.getByRole("tab", { - name: /malicious/i, - }), - ); - - await waitFor(() => { - expect( - screen.queryAllByRole("gridcell", { - name: /blocked secret exposure/i, - }).length, - ).toBe(0); - }); +it("shows only conversations with malicious when you click on the malicious tab", async () => { + server.use( + http.get(mswEndpoint("/api/v1/workspaces/:workspace_name/messages"), () => { + return HttpResponse.json([ + ...Array.from({ length: 10 }).map(() => + mockConversation({ + alertsConfig: { numAlerts: 10, type: "malicious" }, + }), + ), + ...Array.from({ length: 10 }).map(() => + mockConversation({ + alertsConfig: { numAlerts: 10, type: "secret" }, + }), + ), + ]); + }), + ); + render(); - userEvent.click( - screen.getByRole("tab", { - name: /all/i, - }), - ); - - await waitFor(() => { - expect( - screen.queryAllByRole("gridcell", { - name: /blocked secret exposure/i, - }).length, - ).toBe(1); - }); + await waitFor(() => { + expect(screen.queryByText(/loading.../i)).not.toBeInTheDocument(); }); - it("should filter by secrets", async () => { - mockAlertsWithMaliciousPkg(); - render(); + expect(screen.getByTestId(/tab-all-count/i)).toHaveTextContent("20"); + expect(screen.getByTestId(/tab-malicious-count/i)).toHaveTextContent("10"); - await waitFor(() => { - expect(screen.queryByText(/loading.../i)).not.toBeInTheDocument(); - }); + await userEvent.click( + screen.getByRole("tab", { + name: /malicious/i, + }), + ); - expect(screen.getByTestId(/tab-all-count/i)).toHaveTextContent("2"); - expect( - screen.getAllByRole("gridcell", { - name: /chat/i, - }).length, - ).toBeGreaterThanOrEqual(1); - - userEvent.click( - screen.getByRole("tab", { - name: /secrets/i, - }), - ); - - await waitFor(() => { - expect( - screen.queryAllByRole("gridcell", { - name: /blocked malicious package/i, - }).length, - ).toBe(0); - }); + const tbody = screen.getAllByRole("rowgroup")[1] as HTMLElement; - userEvent.click( - screen.getByRole("tab", { - name: /all/i, - }), - ); - - await waitFor(() => { - expect( - screen.queryAllByRole("gridcell", { - name: /blocked malicious package/i, - }).length, - ).toBe(1); + await waitFor(() => { + const secretsCountButtons = within(tbody).getAllByRole("button", { + name: /malicious packages count/, + }) as HTMLElement[]; + secretsCountButtons.forEach((e) => { + expect(e).toHaveTextContent("10"); }); }); +}); - it("should search by secrets alert", async () => { - mockAlertsWithMaliciousPkg(); - render(); +it("should render searchbox", async () => { + render(); - await waitFor(() => { - expect(screen.queryByText(/loading.../i)).not.toBeInTheDocument(); - }); + expect( + screen.getByRole("searchbox", { + name: /search messages/i, + }), + ).toBeVisible(); +}); - expect(screen.getByTestId(/tab-all-count/i)).toHaveTextContent("2"); +it("can filter using searchbox", async () => { + const STRING_TO_FILTER_BY = "foo-bar-my-awesome-string.com"; - expect( - screen.getAllByRole("gridcell", { - name: /chat/i, - }).length, - ).toBeGreaterThanOrEqual(1); + // mock a conversation to filter to + // - replace the message with our search string + // - timestamp very far in the past, so it is sorted to end of list + const CONVERSATION_TO_FILTER_BY = mockConversation(); + (CONVERSATION_TO_FILTER_BY.question_answers[0].question.message as string) = + STRING_TO_FILTER_BY; + (CONVERSATION_TO_FILTER_BY.conversation_timestamp as string) = faker.date + .past({ years: 1 }) + .toISOString(); - await userEvent.type(screen.getByRole("searchbox"), "codegate-secrets"); + server.use( + http.get(mswEndpoint("/api/v1/workspaces/:workspace_name/messages"), () => { + return HttpResponse.json([ + ...Array.from({ length: 15 }).map(() => mockConversation()), // at least 1 page worth of data + CONVERSATION_TO_FILTER_BY, + ]); + }), + ); + render(); - waitFor(() => - expect(screen.getByTestId(/tab-all-count/i)).toHaveTextContent("1"), - ); - const row = within(screen.getByTestId("alerts-table")).getAllByRole( - "row", - )[1] as HTMLElement; - expect(within(row).getByText(/chat/i)).toBeVisible(); + await waitFor(() => { + expect(screen.queryByText(/loading.../i)).not.toBeInTheDocument(); }); - it("should sort alerts by date desc", async () => { - render(); - - await waitFor(() => { - expect( - within(screen.getByTestId("alerts-table")).getAllByRole("row").length, - ).toBeGreaterThan(1); - }); - - await waitFor(() => { - expect( - screen.queryByText("Connect CodeGate to your IDE"), - ).not.toBeInTheDocument(); - }); - - const firstRow = within(screen.getByTestId("alerts-table")).getAllByRole( - "row", - )[1] as HTMLElement; - const secondRow = within(screen.getByTestId("alerts-table")).getAllByRole( - "row", - )[2] as HTMLElement; + expect(screen.queryByText(STRING_TO_FILTER_BY)).not.toBeInTheDocument(); - expect(within(firstRow).getByText(/[0-9]+.*ago/i)).toBeVisible(); - expect(within(secondRow).getByText(/[0-9]+.*ago/i)).toBeVisible(); - }); + await userEvent.type( + screen.getByRole("searchbox", { name: /search messages/i }), + STRING_TO_FILTER_BY, + ); - it("only displays a limited number of items in the table", async () => { - mockManyAlerts(); + expect( + within(screen.getByRole("grid")).queryByText(STRING_TO_FILTER_BY), + ).toBeVisible(); +}); - render(); +it("should sort messages by date desc", async () => { + render(); - await waitFor(() => { - expect( - within(screen.getByTestId("alerts-table")).getAllByRole("row"), - ).toHaveLength(16); - }); + await waitFor(() => { + expect(screen.queryByText(/loading.../i)).not.toBeInTheDocument(); }); - it("allows pagination", async () => { - mockManyAlerts(); - - render(); - - await waitFor( - async () => { - await userEvent.click(screen.getByRole("button", { name: /next/i })); + const tbody = screen.getAllByRole("rowgroup")[1] as HTMLElement; + const newest = ( + within(tbody).getAllByRole("row")[1] as HTMLElement + ).getAttribute("data-timestamp") as string; + const oldest = ( + within(tbody).getAllByRole("row")[2] as HTMLElement + ).getAttribute("data-timestamp") as string; - expect( - within(screen.getByTestId("alerts-table")).getAllByRole("row").length, - ).toBeLessThan(16); - }, - { timeout: 5000 }, - ); - - // on the last page, we cannot go further - expect(screen.getByRole("button", { name: /next/i })).toBeDisabled(); - - await userEvent.click(screen.getByRole("button", { name: /previous/i })); - expect(screen.getByRole("button", { name: /previous/i })).toBeEnabled(); - expect(screen.getByRole("button", { name: /next/i })).toBeEnabled(); - - await waitFor(async () => { - await userEvent.click(screen.getByRole("button", { name: /previous/i })); - - // once we reach the first page, we cannot paginate backwards anymore - expect(screen.getByRole("button", { name: /previous/i })).toBeDisabled(); - expect(screen.getByRole("button", { name: /next/i })).toBeEnabled(); - - expect( - within(screen.getByTestId("alerts-table")).getAllByRole("row").length, - ).toEqual(16); - }); - }); + expect(oldest > newest).toBe(false); }); diff --git a/src/routes/__tests__/route-workspace.test.tsx b/src/routes/__tests__/route-workspace.test.tsx index 30e80b97..5209a5f8 100644 --- a/src/routes/__tests__/route-workspace.test.tsx +++ b/src/routes/__tests__/route-workspace.test.tsx @@ -48,7 +48,7 @@ test("renders title", () => { const { getByRole } = renderComponent(); expect( - getByRole("heading", { name: "Workspace settings for foo", level: 4 }), + getByRole("heading", { name: "Workspace settings for foo", level: 1 }), ).toBeVisible(); }); @@ -94,6 +94,10 @@ test("rename workspace", async () => { const saveBtn = within(getByTestId("workspace-name")).getByRole("button", { name: /save/i, }); + + await waitFor(() => { + expect(saveBtn).toBeEnabled(); + }); await userEvent.click(saveBtn); await waitFor(() => expect(mockNavigate).toHaveBeenCalledTimes(1)); expect(mockNavigate).toHaveBeenCalledWith("/workspace/foo_renamed"); diff --git a/src/routes/route-chat.tsx b/src/routes/route-chat.tsx index 6cda4069..01043d62 100644 --- a/src/routes/route-chat.tsx +++ b/src/routes/route-chat.tsx @@ -1,83 +1,88 @@ import { useParams } from "react-router-dom"; -import { useQueryGetWorkspaceMessages } from "@/hooks/use-query-get-workspace-messages"; import { parsingPromptText, sanitizeQuestionPrompt } from "@/lib/utils"; -import { ChatMessageList } from "@/components/ui/chat/chat-message-list"; -import { - ChatBubble, - ChatBubbleAvatar, - ChatBubbleMessage, -} from "@/components/ui/chat/chat-bubble"; -import { Markdown } from "@/components/Markdown"; -import { Breadcrumb, Breadcrumbs, Card, CardBody } from "@stacklok/ui-kit"; +import { Breadcrumb, Breadcrumbs, Loader } from "@stacklok/ui-kit"; import { BreadcrumbHome } from "@/components/BreadcrumbHome"; -import { useQueryGetWorkspaceAlertTable } from "@/features/alerts/hooks/use-query-get-workspace-alerts-table"; -import { AlertDetail } from "@/components/AlertDetail"; +import { PageContainer } from "@/components/page-container"; +import { PageHeading } from "@/components/heading"; +import { + ConversationView, + useConversationSearchParams, +} from "@/features/dashboard-messages/hooks/use-conversation-search-params"; +import { TabsConversation } from "@/features/dashboard-messages/components/tabs-conversation"; +import { SectionConversationTranscript } from "@/features/dashboard-messages/components/section-conversation-transcript"; +import { SectionConversationSecrets } from "@/features/dashboard-messages/components/section-conversation-secrets"; +import { ErrorFallbackContent } from "@/components/Error"; +import { useConversationById } from "@/features/dashboard-messages/hooks/use-conversation-by-id"; +import { getConversationTitle } from "@/features/dashboard-messages/lib/get-conversation-title"; +import { formatTime } from "@/lib/format-time"; +import { Conversation } from "@/api/generated"; + +function ConversationContent({ + view, + conversation, +}: { + view: ConversationView; + conversation: Conversation; +}) { + switch (view) { + case ConversationView.OVERVIEW: + return ; + case ConversationView.SECRETS: + return ; + } +} + +function TitleContent({ conversation }: { conversation: Conversation }) { + return ( +
    + {getConversationTitle(conversation)} + + {formatTime(new Date(conversation.conversation_timestamp))} + +
    + ); +} export function RouteChat() { - const { id } = useParams(); - const { data = [] } = useQueryGetWorkspaceAlertTable(); - const { data: prompts } = useQueryGetWorkspaceMessages(); - const chat = prompts?.find((prompt) => prompt.chat_id === id); + const { id } = useParams<"id">(); + const { state } = useConversationSearchParams(); + + const { data: conversation, isLoading } = useConversationById(id ?? ""); const title = - chat === undefined || - chat.question_answers?.[0]?.question?.message === undefined + conversation === undefined || + conversation.question_answers?.[0]?.question?.message === undefined ? `Prompt ${id}` : parsingPromptText( sanitizeQuestionPrompt({ - question: chat.question_answers?.[0].question.message, - answer: chat.question_answers?.[0]?.answer?.message ?? "", + question: conversation.question_answers?.[0].question.message, + answer: conversation.question_answers?.[0]?.answer?.message ?? "", }), - chat.conversation_timestamp, + conversation.conversation_timestamp, ); - // we have an issue on BE, we received duplicated alerts - const alertDetail = data.filter((alert) => - alert.conversation.question_answers.some( - (item) => item.question.message_id === id, - ), - )[0]; + if (isLoading) + return ( +
    + +
    + ); + if (!id || !conversation) return ; return ( - <> + {title} + } + /> -
    - {alertDetail && ( - - - - - - )} - - - {(chat?.question_answers ?? []).map(({ question, answer }, index) => ( -
    - - - - - {sanitizeQuestionPrompt({ - question: question?.message ?? "", - answer: answer?.message ?? "", - })} - - - - - - - {answer?.message ?? ""} - - -
    - ))} -
    -
    - + + + +
    ); } diff --git a/src/routes/route-dashboard.tsx b/src/routes/route-dashboard.tsx index 2625e332..fb291c9c 100644 --- a/src/routes/route-dashboard.tsx +++ b/src/routes/route-dashboard.tsx @@ -1,21 +1,22 @@ -import { TableAlerts } from "@/features/alerts/components/table-alerts"; -import { AlertsSummaryMaliciousPkg } from "@/features/alerts/components/alerts-summary-malicious-pkg"; -import { AlertsSummaryWorkspaceTokenUsage } from "@/features/alerts/components/alerts-summary-workspace-token-usage"; -import { AlertsSummaryMaliciousSecrets } from "@/features/alerts/components/alerts-summary-secrets"; -import { TabsAlerts } from "@/features/alerts/components/tabs-alerts"; +import { TableMessages } from "@/features/dashboard-messages/components/table-messages"; +import { AlertsSummaryMaliciousPkg } from "@/features/dashboard-alerts/components/alerts-summary-malicious-pkg"; +import { AlertsSummaryWorkspaceTokenUsage } from "@/features/dashboard-alerts/components/alerts-summary-workspace-token-usage"; +import { AlertsSummaryMaliciousSecrets } from "@/features/dashboard-alerts/components/alerts-summary-secrets"; +import { TabsMessages } from "@/features/dashboard-messages/components/tabs-messages"; +import { PageContainer } from "@/components/page-container"; export function RouteDashboard() { return ( -
    +
    - - - -
    + + + + ); } diff --git a/src/routes/route-providers.tsx b/src/routes/route-providers.tsx index 7ea1aeea..3f0459cd 100644 --- a/src/routes/route-providers.tsx +++ b/src/routes/route-providers.tsx @@ -2,7 +2,6 @@ import { BreadcrumbHome } from "@/components/BreadcrumbHome"; import { Breadcrumbs, Breadcrumb, - Heading, Card, LinkButton, CardBody, @@ -11,27 +10,28 @@ import { twMerge } from "tailwind-merge"; import { PlusSquare } from "@untitled-ui/icons-react"; import { TableProviders } from "@/features/providers/components/table-providers"; import { Outlet } from "react-router-dom"; +import { PageContainer } from "@/components/page-container"; +import { PageHeading } from "@/components/heading"; export function RouteProvider({ className }: { className?: string }) { return ( - <> + Providers - - Providers + Add Provider - + - + - + ); } diff --git a/src/routes/route-workspace-creation.tsx b/src/routes/route-workspace-creation.tsx index 1229bcfc..ad897e5b 100644 --- a/src/routes/route-workspace-creation.tsx +++ b/src/routes/route-workspace-creation.tsx @@ -1,19 +1,20 @@ import { BreadcrumbHome } from "@/components/BreadcrumbHome"; +import { PageContainer } from "@/components/page-container"; import { WorkspaceCreation } from "@/features/workspace/components/workspace-creation"; -import { WorkspaceHeading } from "@/features/workspace/components/workspace-heading"; +import { PageHeading } from "@/components/heading"; import { Breadcrumbs, Breadcrumb } from "@stacklok/ui-kit"; export function RouteWorkspaceCreation() { return ( - <> + Manage Workspaces Create Workspace - + - + ); } diff --git a/src/routes/route-workspace.tsx b/src/routes/route-workspace.tsx index 76e9bf20..fea687e0 100644 --- a/src/routes/route-workspace.tsx +++ b/src/routes/route-workspace.tsx @@ -1,7 +1,7 @@ import { BreadcrumbHome } from "@/components/BreadcrumbHome"; import { ArchiveWorkspace } from "@/features/workspace/components/archive-workspace"; -import { WorkspaceHeading } from "@/features/workspace/components/workspace-heading"; +import { PageHeading } from "@/components/heading"; import { WorkspaceName } from "@/features/workspace/components/workspace-name"; import { Alert, Breadcrumb, Breadcrumbs } from "@stacklok/ui-kit"; import { useParams } from "react-router-dom"; @@ -9,6 +9,7 @@ import { useArchivedWorkspaces } from "@/features/workspace/hooks/use-archived-w import { useRestoreWorkspaceButton } from "@/features/workspace/hooks/use-restore-workspace-button"; import { WorkspaceCustomInstructions } from "@/features/workspace/components/workspace-custom-instructions"; import { WorkspacePreferredModel } from "@/features/workspace/components/workspace-preferred-model"; +import { PageContainer } from "@/components/page-container"; function WorkspaceArchivedBanner({ name }: { name: string }) { const restoreButtonProps = useRestoreWorkspaceButton({ workspaceName: name }); @@ -37,20 +38,14 @@ export function RouteWorkspace() { }); return ( - <> + Manage Workspaces Workspace Settings - - Workspace settings for {name} -
    - } - /> + {isArchived ? : null} @@ -70,6 +65,6 @@ export function RouteWorkspace() { className="mb-4" /> - + ); } diff --git a/src/routes/route-workspaces.tsx b/src/routes/route-workspaces.tsx index 7ca21277..57f77bca 100644 --- a/src/routes/route-workspaces.tsx +++ b/src/routes/route-workspaces.tsx @@ -1,4 +1,4 @@ -import { WorkspaceHeading } from "@/features/workspace/components/workspace-heading"; +import { PageHeading } from "@/components/heading"; import { BreadcrumbHome } from "@/components/BreadcrumbHome"; import { Breadcrumb, @@ -13,6 +13,7 @@ import { useKbdShortcuts } from "@/hooks/use-kbd-shortcuts"; import { useNavigate } from "react-router-dom"; import { hrefs } from "@/lib/hrefs"; import { PlusSquare } from "@untitled-ui/icons-react"; +import { PageContainer } from "@/components/page-container"; export function RouteWorkspaces() { const navigate = useNavigate(); @@ -20,13 +21,13 @@ export function RouteWorkspaces() { useKbdShortcuts([["c", () => navigate(hrefs.workspaces.create)]]); return ( - <> + Manage Workspaces - + Create @@ -36,9 +37,9 @@ export function RouteWorkspaces() { C - + - + ); } diff --git a/tsconfig.app.json b/tsconfig.app.json index 3385eef8..0c42ab7d 100644 --- a/tsconfig.app.json +++ b/tsconfig.app.json @@ -23,6 +23,13 @@ }, "types": ["vitest/globals", "@testing-library/jest-dom/vitest"] }, - "include": ["src/**/*.ts", "src/**/*.tsx"], + "include": [ + "src/**/*", + "knip.ts", + "openapi-ts.config.ts", + "tailwind.config.ts", + "vitest.config.ts", + "vitest.setup.ts" + ], "exclude": ["node_modules"] }