diff --git a/eslint.config.js b/eslint.config.js index a031d0a6..ce89c0fb 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -72,17 +72,18 @@ const restrictedSyntax = { } export default tseslint.config( - { ignores: ['dist'] }, + { ignores: ['dist', 'coverage'] }, { extends: [ js.configs.recommended, ...tseslint.configs.recommended, ...tailwindPlugin.configs['flat/recommended'], ], - files: ['**/*.{ts,tsx}'], languageOptions: { parserOptions: { - projectService: true, + projectService: { + allowDefaultProject: ['*.js', '*.mjs'], + }, tsconfigRootDir: import.meta.dirname, ecmaFeatures: { jsx: true, @@ -109,6 +110,8 @@ export default tseslint.config( config: './tailwind.config.ts', }, }, + }, + { rules: { ...reactHooks.configs.recommended.rules, 'react-refresh/only-export-components': [ @@ -238,5 +241,12 @@ export default tseslint.config( }, ], }, + }, + { + files: ['src/api/generated/**/*'], + rules: { + '@typescript-eslint/no-explicit-any': 'off', + '@typescript-eslint/ban-ts-comment': 'off', + }, } ) diff --git a/package-lock.json b/package-lock.json index f65496de..f3c3d108 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,19 +10,13 @@ "dependencies": { "@dnd-kit/core": "^6.3.1", "@dnd-kit/sortable": "^10.0.0", + "@dnd-kit/utilities": "^3.2.2", "@hey-api/client-fetch": "^0.7.1", - "@jsonforms/core": "^3.5.1", - "@jsonforms/react": "^3.5.1", - "@jsonforms/vanilla-renderers": "^3.5.1", "@monaco-editor/react": "^4.6.0", - "@radix-ui/react-dialog": "^1.1.4", - "@radix-ui/react-separator": "^1.1.0", - "@radix-ui/react-slot": "^1.1.0", "@stacklok/ui-kit": "^1.0.1-9", "@tanstack/react-query": "^5.64.1", "@tanstack/react-query-devtools": "^5.66.0", "@types/lodash": "^4.17.15", - "@types/prismjs": "^1.26.5", "@types/react-syntax-highlighter": "^15.5.13", "@untitled-ui/icons-react": "^0.1.4", "clsx": "^2.1.1", @@ -30,7 +24,6 @@ "fuse.js": "^7.0.0", "highlight.js": "^11.11.1", "lodash": "^4.17.21", - "prismjs": "^1.29.0", "react": "19.0.0", "react-dom": "19.0.0", "react-markdown": "^9.0.1", @@ -1733,62 +1726,6 @@ "dev": true, "license": "MIT" }, - "node_modules/@jsonforms/core": { - "version": "3.5.1", - "resolved": "https://registry.npmjs.org/@jsonforms/core/-/core-3.5.1.tgz", - "integrity": "sha512-Jrq/UcfvKsAprLJ+9TMFa8pKsfdyv3dAw85XstSNRcjDT19LreBlhVqIvTvtgZidg8Iet3yqy5xlNnB+XyrvrQ==", - "dependencies": { - "@types/json-schema": "^7.0.3", - "ajv": "^8.6.1", - "ajv-formats": "^2.1.0", - "lodash": "^4.17.21" - } - }, - "node_modules/@jsonforms/core/node_modules/ajv": { - "version": "8.17.1", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", - "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", - "dependencies": { - "fast-deep-equal": "^3.1.3", - "fast-uri": "^3.0.1", - "json-schema-traverse": "^1.0.0", - "require-from-string": "^2.0.2" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" - } - }, - "node_modules/@jsonforms/core/node_modules/json-schema-traverse": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", - "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==" - }, - "node_modules/@jsonforms/react": { - "version": "3.5.1", - "resolved": "https://registry.npmjs.org/@jsonforms/react/-/react-3.5.1.tgz", - "integrity": "sha512-fQwCpzyNcf0FruYhc46dK6GfCcX09HkRX2PGYir7dllQPRI1axHd6t98To/h+48/L2PkFdRMGMCcIsoTXNC1qg==", - "dependencies": { - "lodash": "^4.17.21" - }, - "peerDependencies": { - "@jsonforms/core": "3.5.1", - "react": "^16.12.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" - } - }, - "node_modules/@jsonforms/vanilla-renderers": { - "version": "3.5.1", - "resolved": "https://registry.npmjs.org/@jsonforms/vanilla-renderers/-/vanilla-renderers-3.5.1.tgz", - "integrity": "sha512-lqb678VFZuns6E60SjxgtRo8Cx1E5MdloPEz9HSSZ2JRzotjXUXiUr/93b/9XPlgQFJ5DMJl5gEyV1VYC2BcwQ==", - "dependencies": { - "lodash": "^4.17.21" - }, - "peerDependencies": { - "@jsonforms/core": "3.5.1", - "@jsonforms/react": "3.5.1", - "react": "^16.12.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" - } - }, "node_modules/@monaco-editor/loader": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/@monaco-editor/loader/-/loader-1.4.0.tgz", @@ -1920,341 +1857,6 @@ "dev": true, "license": "MIT" }, - "node_modules/@radix-ui/primitive": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.1.tgz", - "integrity": "sha512-SJ31y+Q/zAyShtXJc8x83i9TYdbAfHZ++tUZnvjJJqFjzsdUnKsxPL6IEtBlxKkU7yzer//GQtZSV4GbldL3YA==", - "license": "MIT" - }, - "node_modules/@radix-ui/react-compose-refs": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.1.tgz", - "integrity": "sha512-Y9VzoRDSJtgFMUCoiZBDVo084VQ5hfpXxVE+NgkdNsjiDBByiImMZKKhxMwCbdHvhlENG6a833CbFkOQvTricw==", - "license": "MIT", - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-context": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.1.1.tgz", - "integrity": "sha512-UASk9zi+crv9WteK/NU4PLvOoL3OuE6BWVKNF6hPRBtYBDXQ2u5iu3O59zUlJiTVvkyuycnqrztsHVJwcK9K+Q==", - "license": "MIT", - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-dialog": { - "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.5", - "@radix-ui/react-focus-guards": "1.1.1", - "@radix-ui/react-focus-scope": "1.1.2", - "@radix-ui/react-id": "1.1.0", - "@radix-ui/react-portal": "1.1.4", - "@radix-ui/react-presence": "1.1.2", - "@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.2.4", - "react-remove-scroll": "^2.6.3" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-dismissable-layer": { - "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.2", - "@radix-ui/react-use-callback-ref": "1.1.0", - "@radix-ui/react-use-escape-keydown": "1.1.0" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-focus-guards": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-guards/-/react-focus-guards-1.1.1.tgz", - "integrity": "sha512-pSIwfrT1a6sIoDASCSpFwOasEwKTZWDw/iBdtnqKO7v6FeOzYJ7U53cPzYFVR3geGGXgVHaH+CdngrrAzqUGxg==", - "license": "MIT", - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-focus-scope": { - "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.2", - "@radix-ui/react-use-callback-ref": "1.1.0" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-id": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@radix-ui/react-id/-/react-id-1.1.0.tgz", - "integrity": "sha512-EJUrI8yYh7WOjNOqpoJaf1jlFIH2LvtgAl+YcFqNCa+4hj64ZXmPkAKOFs/ukjz3byN6bdb/AVUqHkI8/uWWMA==", - "license": "MIT", - "dependencies": { - "@radix-ui/react-use-layout-effect": "1.1.0" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-portal": { - "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.2", - "@radix-ui/react-use-layout-effect": "1.1.0" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-presence": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@radix-ui/react-presence/-/react-presence-1.1.2.tgz", - "integrity": "sha512-18TFr80t5EVgL9x1SwF/YGtfG+l0BS0PRAlCWBDoBEiDQjeKgnNZRVJp/oVBl24sr3Gbfwc/Qpj4OcWTQMsAEg==", - "license": "MIT", - "dependencies": { - "@radix-ui/react-compose-refs": "1.1.1", - "@radix-ui/react-use-layout-effect": "1.1.0" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-primitive": { - "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.2" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-separator": { - "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.2" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-slot": { - "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" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-use-callback-ref": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.1.0.tgz", - "integrity": "sha512-CasTfvsy+frcFkbXtSJ2Zu9JHpN8TYKxkgJGWbjiZhFivxaeW7rMeZt7QELGVLaYVfFMsKHjb7Ak0nMEe+2Vfw==", - "license": "MIT", - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-use-controllable-state": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@radix-ui/react-use-controllable-state/-/react-use-controllable-state-1.1.0.tgz", - "integrity": "sha512-MtfMVJiSr2NjzS0Aa90NPTnvTSg6C/JLCV7ma0W6+OMV78vd8OyRpID+Ng9LxzsPbLeuBnWBA1Nq30AtBIDChw==", - "license": "MIT", - "dependencies": { - "@radix-ui/react-use-callback-ref": "1.1.0" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-use-escape-keydown": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@radix-ui/react-use-escape-keydown/-/react-use-escape-keydown-1.1.0.tgz", - "integrity": "sha512-L7vwWlR1kTTQ3oh7g1O0CBF3YCyyTj8NmhLR+phShpyA50HCfBFKVJTpshm9PzLiKmehsrQzTYTpX9HvmC9rhw==", - "license": "MIT", - "dependencies": { - "@radix-ui/react-use-callback-ref": "1.1.0" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-use-layout-effect": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.1.0.tgz", - "integrity": "sha512-+FPE0rOdziWSrH9athwI1R0HDVbWlEhd+FR+aSDk4uWGmSJ9Z54sdZVDQPZAinJhJXwfT+qnj969mCsT2gfm5w==", - "license": "MIT", - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, "node_modules/@react-aria/breadcrumbs": { "version": "3.5.19", "resolved": "https://registry.npmjs.org/@react-aria/breadcrumbs/-/breadcrumbs-3.5.19.tgz", @@ -4712,6 +4314,7 @@ "version": "7.0.15", "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "dev": true, "license": "MIT" }, "node_modules/@types/json5": { @@ -4752,12 +4355,6 @@ "undici-types": "~6.20.0" } }, - "node_modules/@types/prismjs": { - "version": "1.26.5", - "resolved": "https://registry.npmjs.org/@types/prismjs/-/prismjs-1.26.5.tgz", - "integrity": "sha512-AUZTa7hQ2KY5L7AmtSiqxlhWxb4ina0yd8hNbl4TWuqnv/pFP0nDMb3YrfSBf4hJVGLh2YEIBfKaBW/9UEl6IQ==", - "license": "MIT" - }, "node_modules/@types/react": { "version": "19.0.2", "resolved": "https://registry.npmjs.org/@types/react/-/react-19.0.2.tgz", @@ -5433,42 +5030,6 @@ "url": "https://github.com/sponsors/epoberezkin" } }, - "node_modules/ajv-formats": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-2.1.1.tgz", - "integrity": "sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==", - "dependencies": { - "ajv": "^8.0.0" - }, - "peerDependencies": { - "ajv": "^8.0.0" - }, - "peerDependenciesMeta": { - "ajv": { - "optional": true - } - } - }, - "node_modules/ajv-formats/node_modules/ajv": { - "version": "8.17.1", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", - "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", - "dependencies": { - "fast-deep-equal": "^3.1.3", - "fast-uri": "^3.0.1", - "json-schema-traverse": "^1.0.0", - "require-from-string": "^2.0.2" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" - } - }, - "node_modules/ajv-formats/node_modules/json-schema-traverse": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", - "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==" - }, "node_modules/ansi-escapes": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-7.0.0.tgz", @@ -5544,18 +5105,6 @@ "dev": true, "license": "Python-2.0" }, - "node_modules/aria-hidden": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/aria-hidden/-/aria-hidden-1.2.4.tgz", - "integrity": "sha512-y+CcFFwelSXpLZk/7fMB2mUbGtX9lKycf1MWJ7CaTIERyitVlyQx6C+sxcROU2BAJ24OiZyK+8wj2i8AlBoS3A==", - "license": "MIT", - "dependencies": { - "tslib": "^2.0.0" - }, - "engines": { - "node": ">=10" - } - }, "node_modules/aria-query": { "version": "5.3.0", "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.0.tgz", @@ -6699,12 +6248,6 @@ "dev": true, "license": "MIT" }, - "node_modules/detect-node-es": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/detect-node-es/-/detect-node-es-1.1.0.tgz", - "integrity": "sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ==", - "license": "MIT" - }, "node_modules/devlop": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/devlop/-/devlop-1.1.0.tgz", @@ -7492,6 +7035,7 @@ "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true, "license": "MIT" }, "node_modules/fast-glob": { @@ -7536,21 +7080,6 @@ "dev": true, "license": "MIT" }, - "node_modules/fast-uri": { - "version": "3.0.6", - "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.0.6.tgz", - "integrity": "sha512-Atfo14OibSv5wAp4VWNsFYE1AchQRTv9cBGWET4pZWHzYshFSS9NQI6I57rdKn9croWVMbYFbLhJ+yJvmZIIHw==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/fastify" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/fastify" - } - ] - }, "node_modules/fastq": { "version": "1.18.0", "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.18.0.tgz", @@ -7899,15 +7428,6 @@ "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", - "integrity": "sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q==", - "license": "MIT", - "engines": { - "node": ">=6" - } - }, "node_modules/get-proto": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", @@ -12083,53 +11603,6 @@ "react": ">=18" } }, - "node_modules/react-remove-scroll": { - "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.3", - "tslib": "^2.1.0", - "use-callback-ref": "^1.3.3", - "use-sidecar": "^1.1.3" - }, - "engines": { - "node": ">=10" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/react-remove-scroll-bar": { - "version": "2.3.8", - "resolved": "https://registry.npmjs.org/react-remove-scroll-bar/-/react-remove-scroll-bar-2.3.8.tgz", - "integrity": "sha512-9r+yi9+mgU33AKcj6IbT9oRCO78WriSj6t/cF8DWBZJ9aOGPOTEDvdUDz1FwKim7QXWwmHqtdHnRJfhAxEG46Q==", - "license": "MIT", - "dependencies": { - "react-style-singleton": "^2.2.2", - "tslib": "^2.0.0" - }, - "engines": { - "node": ">=10" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, "node_modules/react-router": { "version": "7.1.1", "resolved": "https://registry.npmjs.org/react-router/-/react-router-7.1.1.tgz", @@ -12206,28 +11679,6 @@ "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" } }, - "node_modules/react-style-singleton": { - "version": "2.2.3", - "resolved": "https://registry.npmjs.org/react-style-singleton/-/react-style-singleton-2.2.3.tgz", - "integrity": "sha512-b6jSvxvVnyptAiLjbkWLE/lOnR4lfTtDAl+eUC7RZy+QQWc6wRzIV2CE6xBuMmDxc2qIihtDCZD5NPOFl7fRBQ==", - "license": "MIT", - "dependencies": { - "get-nonce": "^1.0.0", - "tslib": "^2.0.0" - }, - "engines": { - "node": ">=10" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, "node_modules/react-syntax-highlighter": { "version": "15.6.1", "resolved": "https://registry.npmjs.org/react-syntax-highlighter/-/react-syntax-highlighter-15.6.1.tgz", @@ -12546,14 +11997,6 @@ "node": ">=0.10.0" } }, - "node_modules/require-from-string": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", - "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/requires-port": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz", @@ -14298,49 +13741,6 @@ "requires-port": "^1.0.0" } }, - "node_modules/use-callback-ref": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/use-callback-ref/-/use-callback-ref-1.3.3.tgz", - "integrity": "sha512-jQL3lRnocaFtu3V00JToYz/4QkNWswxijDaCVNZRiRTO3HQDLsdu1ZtmIUvV4yPp+rvWm5j0y0TG/S61cuijTg==", - "license": "MIT", - "dependencies": { - "tslib": "^2.0.0" - }, - "engines": { - "node": ">=10" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/use-sidecar": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/use-sidecar/-/use-sidecar-1.1.3.tgz", - "integrity": "sha512-Fedw0aZvkhynoPYlA5WXrMCAMm+nSWdZt6lzJQ7Ok8S6Q+VsHmHpRWndVRJ8Be0ZbkfPc5LRYH+5XrzXcEeLRQ==", - "license": "MIT", - "dependencies": { - "detect-node-es": "^1.1.0", - "tslib": "^2.0.0" - }, - "engines": { - "node": ">=10" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, "node_modules/use-sync-external-store": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.4.0.tgz", diff --git a/package.json b/package.json index d14f5706..1657977f 100644 --- a/package.json +++ b/package.json @@ -23,19 +23,13 @@ "dependencies": { "@dnd-kit/core": "^6.3.1", "@dnd-kit/sortable": "^10.0.0", + "@dnd-kit/utilities": "^3.2.2", "@hey-api/client-fetch": "^0.7.1", - "@jsonforms/core": "^3.5.1", - "@jsonforms/react": "^3.5.1", - "@jsonforms/vanilla-renderers": "^3.5.1", "@monaco-editor/react": "^4.6.0", - "@radix-ui/react-dialog": "^1.1.4", - "@radix-ui/react-separator": "^1.1.0", - "@radix-ui/react-slot": "^1.1.0", "@stacklok/ui-kit": "^1.0.1-9", "@tanstack/react-query": "^5.64.1", "@tanstack/react-query-devtools": "^5.66.0", "@types/lodash": "^4.17.15", - "@types/prismjs": "^1.26.5", "@types/react-syntax-highlighter": "^15.5.13", "@untitled-ui/icons-react": "^0.1.4", "clsx": "^2.1.1", @@ -43,7 +37,6 @@ "fuse.js": "^7.0.0", "highlight.js": "^11.11.1", "lodash": "^4.17.21", - "prismjs": "^1.29.0", "react": "19.0.0", "react-dom": "19.0.0", "react-markdown": "^9.0.1", diff --git a/src/api/generated/@tanstack/react-query.gen.ts b/src/api/generated/@tanstack/react-query.gen.ts index cf1307ef..125f7497 100644 --- a/src/api/generated/@tanstack/react-query.gen.ts +++ b/src/api/generated/@tanstack/react-query.gen.ts @@ -1,7 +1,12 @@ // This file is auto-generated by @hey-api/openapi-ts import type { OptionsLegacyParser } from '@hey-api/client-fetch' -import { queryOptions, type UseMutationOptions } from '@tanstack/react-query' +import { + queryOptions, + type UseMutationOptions, + infiniteQueryOptions, + type InfiniteData, +} from '@tanstack/react-query' import { client, healthCheckHealthGet, @@ -23,7 +28,9 @@ import { v1RecoverWorkspace, v1HardDeleteWorkspace, v1GetWorkspaceAlerts, + v1GetWorkspaceAlertsSummary, v1GetWorkspaceMessages, + v1GetMessagesByPromptId, v1GetWorkspaceCustomInstructions, v1SetWorkspaceCustomInstructions, v1DeleteWorkspaceCustomInstructions, @@ -33,6 +40,11 @@ import { v1StreamSse, v1VersionCheck, v1GetWorkspaceTokenUsage, + v1ListPersonas, + v1CreatePersona, + v1GetPersona, + v1UpdatePersona, + v1DeletePersona, } from '../sdk.gen' import type { V1ListProviderEndpointsData, @@ -69,7 +81,11 @@ import type { V1HardDeleteWorkspaceError, V1HardDeleteWorkspaceResponse, V1GetWorkspaceAlertsData, + V1GetWorkspaceAlertsSummaryData, V1GetWorkspaceMessagesData, + V1GetWorkspaceMessagesError, + V1GetWorkspaceMessagesResponse, + V1GetMessagesByPromptIdData, V1GetWorkspaceCustomInstructionsData, V1SetWorkspaceCustomInstructionsData, V1SetWorkspaceCustomInstructionsError, @@ -83,6 +99,16 @@ import type { V1SetWorkspaceMuxesResponse, V1ListWorkspacesByProviderData, V1GetWorkspaceTokenUsageData, + V1CreatePersonaData, + V1CreatePersonaError, + V1CreatePersonaResponse, + V1GetPersonaData, + V1UpdatePersonaData, + V1UpdatePersonaError, + V1UpdatePersonaResponse, + V1DeletePersonaData, + V1DeletePersonaError, + V1DeletePersonaResponse, } from '../types.gen' type QueryKey = [ @@ -588,6 +614,27 @@ export const v1GetWorkspaceAlertsOptions = ( }) } +export const v1GetWorkspaceAlertsSummaryQueryKey = ( + options: OptionsLegacyParser +) => [createQueryKey('v1GetWorkspaceAlertsSummary', options)] + +export const v1GetWorkspaceAlertsSummaryOptions = ( + options: OptionsLegacyParser +) => { + return queryOptions({ + queryFn: async ({ queryKey, signal }) => { + const { data } = await v1GetWorkspaceAlertsSummary({ + ...options, + ...queryKey[0], + signal, + throwOnError: true, + }) + return data + }, + queryKey: v1GetWorkspaceAlertsSummaryQueryKey(options), + }) +} + export const v1GetWorkspaceMessagesQueryKey = ( options: OptionsLegacyParser ) => [createQueryKey('v1GetWorkspaceMessages', options)] @@ -609,6 +656,113 @@ export const v1GetWorkspaceMessagesOptions = ( }) } +const createInfiniteParams = < + K extends Pick< + QueryKey[0], + 'body' | 'headers' | 'path' | 'query' + >, +>( + queryKey: QueryKey, + page: K +) => { + const params = queryKey[0] + if (page.body) { + params.body = { + ...(queryKey[0].body as any), + ...(page.body as any), + } + } + if (page.headers) { + params.headers = { + ...queryKey[0].headers, + ...page.headers, + } + } + if (page.path) { + params.path = { + ...queryKey[0].path, + ...page.path, + } + } + if (page.query) { + params.query = { + ...queryKey[0].query, + ...page.query, + } + } + return params as unknown as typeof page +} + +export const v1GetWorkspaceMessagesInfiniteQueryKey = ( + options: OptionsLegacyParser +): QueryKey> => [ + createQueryKey('v1GetWorkspaceMessages', options, true), +] + +export const v1GetWorkspaceMessagesInfiniteOptions = ( + options: OptionsLegacyParser +) => { + return infiniteQueryOptions< + V1GetWorkspaceMessagesResponse, + V1GetWorkspaceMessagesError, + InfiniteData, + QueryKey>, + | number + | Pick< + QueryKey>[0], + 'body' | 'headers' | 'path' | 'query' + > + >( + // @ts-ignore + { + queryFn: async ({ pageParam, queryKey, signal }) => { + // @ts-ignore + const page: Pick< + QueryKey>[0], + 'body' | 'headers' | 'path' | 'query' + > = + typeof pageParam === 'object' + ? pageParam + : { + query: { + page: pageParam, + }, + } + const params = createInfiniteParams(queryKey, page) + const { data } = await v1GetWorkspaceMessages({ + ...options, + ...params, + signal, + throwOnError: true, + }) + return data + }, + queryKey: v1GetWorkspaceMessagesInfiniteQueryKey(options), + } + ) +} + +export const v1GetMessagesByPromptIdQueryKey = ( + options: OptionsLegacyParser +) => [createQueryKey('v1GetMessagesByPromptId', options)] + +export const v1GetMessagesByPromptIdOptions = ( + options: OptionsLegacyParser +) => { + return queryOptions({ + queryFn: async ({ queryKey, signal }) => { + const { data } = await v1GetMessagesByPromptId({ + ...options, + ...queryKey[0], + signal, + throwOnError: true, + }) + return data + }, + queryKey: v1GetMessagesByPromptIdQueryKey(options), + }) +} + export const v1GetWorkspaceCustomInstructionsQueryKey = ( options: OptionsLegacyParser ) => [createQueryKey('v1GetWorkspaceCustomInstructions', options)] @@ -792,3 +946,124 @@ export const v1GetWorkspaceTokenUsageOptions = ( queryKey: v1GetWorkspaceTokenUsageQueryKey(options), }) } + +export const v1ListPersonasQueryKey = (options?: OptionsLegacyParser) => [ + createQueryKey('v1ListPersonas', options), +] + +export const v1ListPersonasOptions = (options?: OptionsLegacyParser) => { + return queryOptions({ + queryFn: async ({ queryKey, signal }) => { + const { data } = await v1ListPersonas({ + ...options, + ...queryKey[0], + signal, + throwOnError: true, + }) + return data + }, + queryKey: v1ListPersonasQueryKey(options), + }) +} + +export const v1CreatePersonaQueryKey = ( + options: OptionsLegacyParser +) => [createQueryKey('v1CreatePersona', options)] + +export const v1CreatePersonaOptions = ( + options: OptionsLegacyParser +) => { + return queryOptions({ + queryFn: async ({ queryKey, signal }) => { + const { data } = await v1CreatePersona({ + ...options, + ...queryKey[0], + signal, + throwOnError: true, + }) + return data + }, + queryKey: v1CreatePersonaQueryKey(options), + }) +} + +export const v1CreatePersonaMutation = ( + options?: Partial> +) => { + const mutationOptions: UseMutationOptions< + V1CreatePersonaResponse, + V1CreatePersonaError, + OptionsLegacyParser + > = { + mutationFn: async (localOptions) => { + const { data } = await v1CreatePersona({ + ...options, + ...localOptions, + throwOnError: true, + }) + return data + }, + } + return mutationOptions +} + +export const v1GetPersonaQueryKey = ( + options: OptionsLegacyParser +) => [createQueryKey('v1GetPersona', options)] + +export const v1GetPersonaOptions = ( + options: OptionsLegacyParser +) => { + return queryOptions({ + queryFn: async ({ queryKey, signal }) => { + const { data } = await v1GetPersona({ + ...options, + ...queryKey[0], + signal, + throwOnError: true, + }) + return data + }, + queryKey: v1GetPersonaQueryKey(options), + }) +} + +export const v1UpdatePersonaMutation = ( + options?: Partial> +) => { + const mutationOptions: UseMutationOptions< + V1UpdatePersonaResponse, + V1UpdatePersonaError, + OptionsLegacyParser + > = { + mutationFn: async (localOptions) => { + const { data } = await v1UpdatePersona({ + ...options, + ...localOptions, + throwOnError: true, + }) + return data + }, + } + return mutationOptions +} + +export const v1DeletePersonaMutation = ( + options?: Partial> +) => { + const mutationOptions: UseMutationOptions< + V1DeletePersonaResponse, + V1DeletePersonaError, + OptionsLegacyParser + > = { + mutationFn: async (localOptions) => { + const { data } = await v1DeletePersona({ + ...options, + ...localOptions, + throwOnError: true, + }) + return data + }, + } + return mutationOptions +} diff --git a/src/api/generated/sdk.gen.ts b/src/api/generated/sdk.gen.ts index 4b8998a7..4f4f0553 100644 --- a/src/api/generated/sdk.gen.ts +++ b/src/api/generated/sdk.gen.ts @@ -58,9 +58,15 @@ import type { V1GetWorkspaceAlertsData, V1GetWorkspaceAlertsError, V1GetWorkspaceAlertsResponse, + V1GetWorkspaceAlertsSummaryData, + V1GetWorkspaceAlertsSummaryError, + V1GetWorkspaceAlertsSummaryResponse, V1GetWorkspaceMessagesData, V1GetWorkspaceMessagesError, V1GetWorkspaceMessagesResponse, + V1GetMessagesByPromptIdData, + V1GetMessagesByPromptIdError, + V1GetMessagesByPromptIdResponse, V1GetWorkspaceCustomInstructionsData, V1GetWorkspaceCustomInstructionsError, V1GetWorkspaceCustomInstructionsResponse, @@ -86,6 +92,20 @@ import type { V1GetWorkspaceTokenUsageData, V1GetWorkspaceTokenUsageError, V1GetWorkspaceTokenUsageResponse, + V1ListPersonasError, + V1ListPersonasResponse, + V1CreatePersonaData, + V1CreatePersonaError, + V1CreatePersonaResponse, + V1GetPersonaData, + V1GetPersonaError, + V1GetPersonaResponse, + V1UpdatePersonaData, + V1UpdatePersonaError, + V1UpdatePersonaResponse, + V1DeletePersonaData, + V1DeletePersonaError, + V1DeletePersonaResponse, } from './types.gen' export const client = createClient(createConfig()) @@ -417,6 +437,25 @@ export const v1GetWorkspaceAlerts = ( }) } +/** + * Get Workspace Alerts Summary + * Get alert summary for a workspace. + */ +export const v1GetWorkspaceAlertsSummary = < + ThrowOnError extends boolean = false, +>( + options: OptionsLegacyParser +) => { + return (options?.client ?? client).get< + V1GetWorkspaceAlertsSummaryResponse, + V1GetWorkspaceAlertsSummaryError, + ThrowOnError + >({ + ...options, + url: '/api/v1/workspaces/{workspace_name}/alerts-summary', + }) +} + /** * Get Workspace Messages * Get messages for a workspace. @@ -434,6 +473,23 @@ export const v1GetWorkspaceMessages = ( }) } +/** + * Get Messages By Prompt Id + * Get messages for a workspace. + */ +export const v1GetMessagesByPromptId = ( + options: OptionsLegacyParser +) => { + return (options?.client ?? client).get< + V1GetMessagesByPromptIdResponse, + V1GetMessagesByPromptIdError, + ThrowOnError + >({ + ...options, + url: '/api/v1/workspaces/{workspace_name}/messages/{prompt_id}', + }) +} + /** * Get Workspace Custom Instructions * Get the custom instructions of a workspace. @@ -603,3 +659,88 @@ export const v1GetWorkspaceTokenUsage = ( url: '/api/v1/workspaces/{workspace_name}/token-usage', }) } + +/** + * List Personas + * List all personas. + */ +export const v1ListPersonas = ( + options?: OptionsLegacyParser +) => { + return (options?.client ?? client).get< + V1ListPersonasResponse, + V1ListPersonasError, + ThrowOnError + >({ + ...options, + url: '/api/v1/personas', + }) +} + +/** + * Create Persona + * Create a new persona. + */ +export const v1CreatePersona = ( + options: OptionsLegacyParser +) => { + return (options?.client ?? client).post< + V1CreatePersonaResponse, + V1CreatePersonaError, + ThrowOnError + >({ + ...options, + url: '/api/v1/personas', + }) +} + +/** + * Get Persona + * Get a persona by name. + */ +export const v1GetPersona = ( + options: OptionsLegacyParser +) => { + return (options?.client ?? client).get< + V1GetPersonaResponse, + V1GetPersonaError, + ThrowOnError + >({ + ...options, + url: '/api/v1/personas/{persona_name}', + }) +} + +/** + * Update Persona + * Update an existing persona. + */ +export const v1UpdatePersona = ( + options: OptionsLegacyParser +) => { + return (options?.client ?? client).put< + V1UpdatePersonaResponse, + V1UpdatePersonaError, + ThrowOnError + >({ + ...options, + url: '/api/v1/personas/{persona_name}', + }) +} + +/** + * Delete Persona + * Delete a persona. + */ +export const v1DeletePersona = ( + options: OptionsLegacyParser +) => { + return (options?.client ?? client).delete< + V1DeletePersonaResponse, + V1DeletePersonaError, + ThrowOnError + >({ + ...options, + url: '/api/v1/personas/{persona_name}', + }) +} diff --git a/src/api/generated/types.gen.ts b/src/api/generated/types.gen.ts index 896e4bc3..8f09345f 100644 --- a/src/api/generated/types.gen.ts +++ b/src/api/generated/types.gen.ts @@ -64,6 +64,22 @@ export enum AlertSeverity { CRITICAL = 'critical', } +/** + * Represents a set of summary alerts + */ +export type AlertSummary = { + malicious_packages: number + pii: number + secrets: number + total_alerts: number +} + +export enum AlertTriggerType { + CODEGATE_PII = 'codegate-pii', + CODEGATE_CONTEXT_RETRIEVER = 'codegate-context-retriever', + CODEGATE_SECRETS = 'codegate-secrets', +} + /** * Represents a chat message. */ @@ -106,7 +122,20 @@ export type Conversation = { chat_id: string conversation_timestamp: string token_usage_agg: TokenUsageAggregate | null - alerts?: Array + alerts?: Array | null +} + +/** + * Represents a conversation summary. + */ +export type ConversationSummary = { + chat_id: string + prompt: ChatMessage + alerts_summary: AlertSummary + token_usage_agg: TokenUsageAggregate | null + provider: string | null + type: QuestionType + conversation_timestamp: string } export type CustomInstructions = { @@ -178,6 +207,38 @@ export type MuxRule = { matcher?: string | null } +export type PaginatedMessagesResponse = { + data: Array + limit: number + offset: number + total: number +} + +/** + * Represents a persona object. + */ +export type Persona = { + id: string + name: string + description: string +} + +/** + * Model for creating a new Persona. + */ +export type PersonaRequest = { + name: string + description: string +} + +/** + * Model for updating a Persona. + */ +export type PersonaUpdateRequest = { + new_name: string + new_description: string +} + /** * Represents the different types of auth we support for providers. */ @@ -448,16 +509,43 @@ export type V1GetWorkspaceAlertsResponse = Array export type V1GetWorkspaceAlertsError = HTTPValidationError +export type V1GetWorkspaceAlertsSummaryData = { + path: { + workspace_name: string + } +} + +export type V1GetWorkspaceAlertsSummaryResponse = AlertSummary + +export type V1GetWorkspaceAlertsSummaryError = HTTPValidationError + export type V1GetWorkspaceMessagesData = { path: { workspace_name: string } + query?: { + filter_by_alert_trigger_types?: Array | null + filter_by_ids?: Array | null + page?: number + page_size?: number + } } -export type V1GetWorkspaceMessagesResponse = Array +export type V1GetWorkspaceMessagesResponse = PaginatedMessagesResponse export type V1GetWorkspaceMessagesError = HTTPValidationError +export type V1GetMessagesByPromptIdData = { + path: { + prompt_id: string + workspace_name: string + } +} + +export type V1GetMessagesByPromptIdResponse = Conversation + +export type V1GetMessagesByPromptIdError = HTTPValidationError + export type V1GetWorkspaceCustomInstructionsData = { path: { workspace_name: string @@ -537,3 +625,46 @@ export type V1GetWorkspaceTokenUsageData = { export type V1GetWorkspaceTokenUsageResponse = TokenUsageAggregate export type V1GetWorkspaceTokenUsageError = HTTPValidationError + +export type V1ListPersonasResponse = Array + +export type V1ListPersonasError = unknown + +export type V1CreatePersonaData = { + body: PersonaRequest +} + +export type V1CreatePersonaResponse = Persona + +export type V1CreatePersonaError = HTTPValidationError + +export type V1GetPersonaData = { + path: { + persona_name: string + } +} + +export type V1GetPersonaResponse = Persona + +export type V1GetPersonaError = HTTPValidationError + +export type V1UpdatePersonaData = { + body: PersonaUpdateRequest + path: { + persona_name: string + } +} + +export type V1UpdatePersonaResponse = Persona + +export type V1UpdatePersonaError = HTTPValidationError + +export type V1DeletePersonaData = { + path: { + persona_name: string + } +} + +export type V1DeletePersonaResponse = void + +export type V1DeletePersonaError = HTTPValidationError diff --git a/src/api/openapi.json b/src/api/openapi.json index c636d394..9cd9c62a 100644 --- a/src/api/openapi.json +++ b/src/api/openapi.json @@ -716,6 +716,47 @@ } } }, + "/api/v1/workspaces/{workspace_name}/alerts-summary": { + "get": { + "tags": ["CodeGate API", "Workspaces"], + "summary": "Get Workspace Alerts Summary", + "description": "Get alert summary for a workspace.", + "operationId": "v1_get_workspace_alerts_summary", + "parameters": [ + { + "name": "workspace_name", + "in": "path", + "required": true, + "schema": { + "type": "string", + "title": "Workspace Name" + } + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AlertSummary" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, "/api/v1/workspaces/{workspace_name}/messages": { "get": { "tags": ["CodeGate API", "Workspaces"], @@ -731,6 +772,67 @@ "type": "string", "title": "Workspace Name" } + }, + { + "name": "page", + "in": "query", + "required": false, + "schema": { + "type": "integer", + "minimum": 1, + "default": 1, + "title": "Page" + } + }, + { + "name": "page_size", + "in": "query", + "required": false, + "schema": { + "type": "integer", + "maximum": 100, + "minimum": 1, + "default": 50, + "title": "Page Size" + } + }, + { + "name": "filter_by_ids", + "in": "query", + "required": false, + "schema": { + "anyOf": [ + { + "type": "array", + "items": { + "type": "string" + } + }, + { + "type": "null" + } + ], + "title": "Filter By Ids" + } + }, + { + "name": "filter_by_alert_trigger_types", + "in": "query", + "required": false, + "schema": { + "anyOf": [ + { + "type": "array", + "items": { + "$ref": "#/components/schemas/AlertTriggerType" + } + }, + { + "type": "null" + } + ], + "title": "Filter By Alert Trigger Types" + } } ], "responses": { @@ -739,11 +841,57 @@ "content": { "application/json": { "schema": { - "type": "array", - "items": { - "$ref": "#/components/schemas/Conversation" - }, - "title": "Response V1 Get Workspace Messages" + "$ref": "#/components/schemas/PaginatedMessagesResponse" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/api/v1/workspaces/{workspace_name}/messages/{prompt_id}": { + "get": { + "tags": ["CodeGate API", "Workspaces"], + "summary": "Get Messages By Prompt Id", + "description": "Get messages for a workspace.", + "operationId": "v1_get_messages_by_prompt_id", + "parameters": [ + { + "name": "workspace_name", + "in": "path", + "required": true, + "schema": { + "type": "string", + "title": "Workspace Name" + } + }, + { + "name": "prompt_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "title": "Prompt Id" + } + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Conversation" } } } @@ -1086,6 +1234,190 @@ } } } + }, + "/api/v1/personas": { + "get": { + "tags": ["CodeGate API", "Personas"], + "summary": "List Personas", + "description": "List all personas.", + "operationId": "v1_list_personas", + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "items": { + "$ref": "#/components/schemas/Persona" + }, + "type": "array", + "title": "Response V1 List Personas" + } + } + } + } + } + }, + "post": { + "tags": ["CodeGate API", "Personas"], + "summary": "Create Persona", + "description": "Create a new persona.", + "operationId": "v1_create_persona", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PersonaRequest" + } + } + }, + "required": true + }, + "responses": { + "201": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Persona" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/api/v1/personas/{persona_name}": { + "get": { + "tags": ["CodeGate API", "Personas"], + "summary": "Get Persona", + "description": "Get a persona by name.", + "operationId": "v1_get_persona", + "parameters": [ + { + "name": "persona_name", + "in": "path", + "required": true, + "schema": { + "type": "string", + "title": "Persona Name" + } + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Persona" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + }, + "put": { + "tags": ["CodeGate API", "Personas"], + "summary": "Update Persona", + "description": "Update an existing persona.", + "operationId": "v1_update_persona", + "parameters": [ + { + "name": "persona_name", + "in": "path", + "required": true, + "schema": { + "type": "string", + "title": "Persona Name" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PersonaUpdateRequest" + } + } + } + }, + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Persona" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + }, + "delete": { + "tags": ["CodeGate API", "Personas"], + "summary": "Delete Persona", + "description": "Delete a persona.", + "operationId": "v1_delete_persona", + "parameters": [ + { + "name": "persona_name", + "in": "path", + "required": true, + "schema": { + "type": "string", + "title": "Persona Name" + } + } + ], + "responses": { + "204": { + "description": "Successful Response" + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } } }, "components": { @@ -1303,6 +1635,39 @@ "enum": ["info", "critical"], "title": "AlertSeverity" }, + "AlertSummary": { + "properties": { + "malicious_packages": { + "type": "integer", + "title": "Malicious Packages" + }, + "pii": { + "type": "integer", + "title": "Pii" + }, + "secrets": { + "type": "integer", + "title": "Secrets" + }, + "total_alerts": { + "type": "integer", + "title": "Total Alerts" + } + }, + "type": "object", + "required": ["malicious_packages", "pii", "secrets", "total_alerts"], + "title": "AlertSummary", + "description": "Represents a set of summary alerts" + }, + "AlertTriggerType": { + "type": "string", + "enum": [ + "codegate-pii", + "codegate-context-retriever", + "codegate-secrets" + ], + "title": "AlertTriggerType" + }, "ChatMessage": { "properties": { "message": { @@ -1442,10 +1807,17 @@ ] }, "alerts": { - "items": { - "$ref": "#/components/schemas/Alert" - }, - "type": "array", + "anyOf": [ + { + "items": { + "$ref": "#/components/schemas/Alert" + }, + "type": "array" + }, + { + "type": "null" + } + ], "title": "Alerts", "default": [] } @@ -1462,6 +1834,61 @@ "title": "Conversation", "description": "Represents a conversation." }, + "ConversationSummary": { + "properties": { + "chat_id": { + "type": "string", + "title": "Chat Id" + }, + "prompt": { + "$ref": "#/components/schemas/ChatMessage" + }, + "alerts_summary": { + "$ref": "#/components/schemas/AlertSummary" + }, + "token_usage_agg": { + "anyOf": [ + { + "$ref": "#/components/schemas/TokenUsageAggregate" + }, + { + "type": "null" + } + ] + }, + "provider": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Provider" + }, + "type": { + "$ref": "#/components/schemas/QuestionType" + }, + "conversation_timestamp": { + "type": "string", + "format": "date-time", + "title": "Conversation Timestamp" + } + }, + "type": "object", + "required": [ + "chat_id", + "prompt", + "alerts_summary", + "token_usage_agg", + "provider", + "type", + "conversation_timestamp" + ], + "title": "ConversationSummary", + "description": "Represents a conversation summary." + }, "CustomInstructions": { "properties": { "prompt": { @@ -1628,6 +2055,84 @@ "title": "MuxRule", "description": "Represents a mux rule for a provider." }, + "PaginatedMessagesResponse": { + "properties": { + "data": { + "items": { + "$ref": "#/components/schemas/ConversationSummary" + }, + "type": "array", + "title": "Data" + }, + "limit": { + "type": "integer", + "title": "Limit" + }, + "offset": { + "type": "integer", + "title": "Offset" + }, + "total": { + "type": "integer", + "title": "Total" + } + }, + "type": "object", + "required": ["data", "limit", "offset", "total"], + "title": "PaginatedMessagesResponse" + }, + "Persona": { + "properties": { + "id": { + "type": "string", + "title": "Id" + }, + "name": { + "type": "string", + "title": "Name" + }, + "description": { + "type": "string", + "title": "Description" + } + }, + "type": "object", + "required": ["id", "name", "description"], + "title": "Persona", + "description": "Represents a persona object." + }, + "PersonaRequest": { + "properties": { + "name": { + "type": "string", + "title": "Name" + }, + "description": { + "type": "string", + "title": "Description" + } + }, + "type": "object", + "required": ["name", "description"], + "title": "PersonaRequest", + "description": "Model for creating a new Persona." + }, + "PersonaUpdateRequest": { + "properties": { + "new_name": { + "type": "string", + "title": "New Name" + }, + "new_description": { + "type": "string", + "title": "New Description" + } + }, + "type": "object", + "required": ["new_name", "new_description"], + "title": "PersonaUpdateRequest", + "description": "Model for updating a Persona." + }, "ProviderAuthType": { "type": "string", "enum": ["none", "passthrough", "api_key"], diff --git a/src/components/SortableArea.tsx b/src/components/SortableArea.tsx index a9596183..9fce3f08 100644 --- a/src/components/SortableArea.tsx +++ b/src/components/SortableArea.tsx @@ -34,8 +34,15 @@ function ItemWrapper({ id: UniqueIdentifier hasDragDisabled: boolean }) { - const { attributes, listeners, setNodeRef, transform, transition } = - useSortable({ id }) + const { + attributes, + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore - type declaration appears to be incorrect + listeners, + setNodeRef, + transform, + transition, + } = useSortable({ id }) const style = { transform: CSS.Transform.toString(transform), transition, diff --git a/src/features/dashboard-alerts/components/__tests__/alerts-summary-malicious-pkg.test.tsx b/src/features/dashboard-alerts/components/__tests__/alerts-summary-malicious-pkg.test.tsx index bbb4f40f..d1f70956 100644 --- a/src/features/dashboard-alerts/components/__tests__/alerts-summary-malicious-pkg.test.tsx +++ b/src/features/dashboard-alerts/components/__tests__/alerts-summary-malicious-pkg.test.tsx @@ -5,13 +5,22 @@ import { render, waitFor } from '@/lib/test-utils' import { AlertsSummaryMaliciousPkg } from '../alerts-summary-malicious-pkg' import { mswEndpoint } from '@/test/msw-endpoint' -import { mockAlert } from '@/mocks/msw/mockers/alert.mock' +import { AlertSummary } from '@/api/generated' test('shows correct count when there is a malicious alert', async () => { server.use( - http.get(mswEndpoint('/api/v1/workspaces/:workspace_name/alerts'), () => { - return HttpResponse.json([mockAlert({ type: 'malicious' })]) - }) + http.get( + mswEndpoint('/api/v1/workspaces/:workspace_name/alerts-summary'), + () => { + const response: AlertSummary = { + malicious_packages: 1, + pii: 0, + secrets: 0, + total_alerts: 1, + } + return HttpResponse.json(response) + } + ) ) const { getByTestId } = render() @@ -23,9 +32,18 @@ test('shows correct count when there is a malicious alert', async () => { test('shows correct count when there is no malicious alert', async () => { server.use( - http.get(mswEndpoint('/api/v1/workspaces/:workspace_name/alerts'), () => { - return HttpResponse.json([mockAlert({ type: 'secret' })]) - }) + http.get( + mswEndpoint('/api/v1/workspaces/:workspace_name/alerts-summary'), + () => { + const response: AlertSummary = { + malicious_packages: 0, + pii: 0, + secrets: 0, + total_alerts: 0, + } + return HttpResponse.json(response) + } + ) ) const { getByTestId } = render() diff --git a/src/features/dashboard-alerts/components/__tests__/alerts-summary-pii.test.tsx b/src/features/dashboard-alerts/components/__tests__/alerts-summary-pii.test.tsx new file mode 100644 index 00000000..7c242dcf --- /dev/null +++ b/src/features/dashboard-alerts/components/__tests__/alerts-summary-pii.test.tsx @@ -0,0 +1,54 @@ +import { server } from '@/mocks/msw/node' +import { test } from 'vitest' +import { http, HttpResponse } from 'msw' +import { render, waitFor } from '@/lib/test-utils' + +import { mswEndpoint } from '@/test/msw-endpoint' +import { AlertSummary } from '@/api/generated' +import { AlertsSummaryPii } from '../alerts-summary-pii' + +test('shows correct count when there is a pii alert', async () => { + server.use( + http.get( + mswEndpoint('/api/v1/workspaces/:workspace_name/alerts-summary'), + () => { + const response: AlertSummary = { + malicious_packages: 0, + pii: 1, + secrets: 0, + total_alerts: 1, + } + return HttpResponse.json(response) + } + ) + ) + + const { getByTestId } = render() + + await waitFor(() => { + expect(getByTestId('pii-count')).toHaveTextContent('1') + }) +}) + +test('shows correct count when there is no pii alert', async () => { + server.use( + http.get( + mswEndpoint('/api/v1/workspaces/:workspace_name/alerts-summary'), + () => { + const response: AlertSummary = { + malicious_packages: 0, + pii: 0, + secrets: 0, + total_alerts: 0, + } + return HttpResponse.json(response) + } + ) + ) + + const { getByTestId } = render() + + await waitFor(() => { + expect(getByTestId('pii-count')).toHaveTextContent('0') + }) +}) diff --git a/src/features/dashboard-alerts/components/__tests__/alerts-summary-secrets.test.tsx b/src/features/dashboard-alerts/components/__tests__/alerts-summary-secrets.test.tsx index ccc05bce..bdafc5c8 100644 --- a/src/features/dashboard-alerts/components/__tests__/alerts-summary-secrets.test.tsx +++ b/src/features/dashboard-alerts/components/__tests__/alerts-summary-secrets.test.tsx @@ -5,13 +5,22 @@ import { render, waitFor } from '@/lib/test-utils' import { AlertsSummaryMaliciousSecrets } from '../alerts-summary-secrets' import { mswEndpoint } from '@/test/msw-endpoint' -import { mockAlert } from '@/mocks/msw/mockers/alert.mock' +import { AlertSummary } from '@/api/generated' test('shows correct count when there is a secret alert', async () => { server.use( - http.get(mswEndpoint('/api/v1/workspaces/:workspace_name/alerts'), () => { - return HttpResponse.json([mockAlert({ type: 'secret' })]) - }) + http.get( + mswEndpoint('/api/v1/workspaces/:workspace_name/alerts-summary'), + () => { + const response: AlertSummary = { + malicious_packages: 0, + pii: 0, + secrets: 1, + total_alerts: 1, + } + return HttpResponse.json(response) + } + ) ) const { getByTestId } = render() @@ -23,9 +32,18 @@ test('shows correct count when there is a secret alert', async () => { test('shows correct count when there is no malicious alert', async () => { server.use( - http.get(mswEndpoint('/api/v1/workspaces/:workspace_name/alerts'), () => { - return HttpResponse.json([mockAlert({ type: 'malicious' })]) - }) + http.get( + mswEndpoint('/api/v1/workspaces/:workspace_name/alerts-summary'), + () => { + const response: AlertSummary = { + malicious_packages: 0, + pii: 0, + secrets: 0, + total_alerts: 0, + } + return HttpResponse.json(response) + } + ) ) const { getByTestId } = render() diff --git a/src/features/dashboard-alerts/components/alerts-summary-malicious-pkg.tsx b/src/features/dashboard-alerts/components/alerts-summary-malicious-pkg.tsx index 95c29bff..beb4be3a 100644 --- a/src/features/dashboard-alerts/components/alerts-summary-malicious-pkg.tsx +++ b/src/features/dashboard-alerts/components/alerts-summary-malicious-pkg.tsx @@ -1,17 +1,21 @@ import { PackageX } from '@untitled-ui/icons-react' -import { useQueryGetWorkspaceAlertsMaliciousPkg } from '../hooks/use-query-get-workspace-alerts-malicious-pkg' import { AlertsSummary } from './alerts-summary' +import { useQueryGetWorkspaceAlertsSummary } from '@/hooks/use-query-get-workspace-alerts-summary' export function AlertsSummaryMaliciousPkg() { - const { data = [], isPending } = useQueryGetWorkspaceAlertsMaliciousPkg() + const { data: alertsSummary, isPending } = useQueryGetWorkspaceAlertsSummary() return ( ) diff --git a/src/features/dashboard-alerts/components/alerts-summary-pii.tsx b/src/features/dashboard-alerts/components/alerts-summary-pii.tsx new file mode 100644 index 00000000..ae7fdc92 --- /dev/null +++ b/src/features/dashboard-alerts/components/alerts-summary-pii.tsx @@ -0,0 +1,21 @@ +import { User01 } from '@untitled-ui/icons-react' +import { AlertsSummary } from './alerts-summary' +import { useQueryGetWorkspaceAlertsSummary } from '@/hooks/use-query-get-workspace-alerts-summary' + +export function AlertsSummaryPii() { + const { data: alertsSummary, isPending } = useQueryGetWorkspaceAlertsSummary() + + return ( + + ) +} diff --git a/src/features/dashboard-alerts/components/alerts-summary-secrets.tsx b/src/features/dashboard-alerts/components/alerts-summary-secrets.tsx index 207af1f9..b3ffebbb 100644 --- a/src/features/dashboard-alerts/components/alerts-summary-secrets.tsx +++ b/src/features/dashboard-alerts/components/alerts-summary-secrets.tsx @@ -1,15 +1,21 @@ import { Key01 } from '@untitled-ui/icons-react' import { AlertsSummary } from './alerts-summary' -import { useQueryGetWorkspaceAlertSecrets } from '../hooks/use-query-get-workspace-alerts-secrets' +import { useQueryGetWorkspaceAlertsSummary } from '@/hooks/use-query-get-workspace-alerts-summary' export function AlertsSummaryMaliciousSecrets() { - const { data = [], isPending } = useQueryGetWorkspaceAlertSecrets() + const { data: alertsSummary, isPending } = useQueryGetWorkspaceAlertsSummary() return ( ) } diff --git a/src/features/dashboard-alerts/components/alerts-summary.tsx b/src/features/dashboard-alerts/components/alerts-summary.tsx index bfb16e57..3f096cce 100644 --- a/src/features/dashboard-alerts/components/alerts-summary.tsx +++ b/src/features/dashboard-alerts/components/alerts-summary.tsx @@ -12,8 +12,8 @@ function AlertsSummaryStatistic({ Icon: (props: React.SVGProps) => React.JSX.Element }) { return ( -
- +
+ {formatNumberCompact(count)}
) @@ -40,7 +40,7 @@ export function AlertsSummary({
) : ( -
+
{statistics.map((props) => ( ))} diff --git a/src/features/dashboard-alerts/hooks/use-query-get-workspace-alerts-malicious-pkg.ts b/src/features/dashboard-alerts/hooks/use-query-get-workspace-alerts-malicious-pkg.ts deleted file mode 100644 index 76a2df16..00000000 --- a/src/features/dashboard-alerts/hooks/use-query-get-workspace-alerts-malicious-pkg.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { V1GetWorkspaceAlertsResponse } from '@/api/generated' -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' - -// 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: V1GetWorkspaceAlertsResponse) { - return multiFilter(data, [isAlertCritical, isAlertMalicious]) -} - -export function useQueryGetWorkspaceAlertsMaliciousPkg() { - return useQueryGetWorkspaceAlerts({ - select, - }) -} diff --git a/src/features/dashboard-alerts/hooks/use-query-get-workspace-alerts-secrets.ts b/src/features/dashboard-alerts/hooks/use-query-get-workspace-alerts-secrets.ts deleted file mode 100644 index 0570dcd9..00000000 --- a/src/features/dashboard-alerts/hooks/use-query-get-workspace-alerts-secrets.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { V1GetWorkspaceAlertsResponse } from '@/api/generated' -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' - -// 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: V1GetWorkspaceAlertsResponse) { - return multiFilter(data, [isAlertCritical, isAlertSecret]) -} - -export function useQueryGetWorkspaceAlertSecrets() { - return useQueryGetWorkspaceAlerts({ - select, - }) -} diff --git a/src/features/dashboard-alerts/hooks/use-query-get-workspace-alerts.ts b/src/features/dashboard-alerts/hooks/use-query-get-workspace-alerts.ts deleted file mode 100644 index c734cf25..00000000 --- a/src/features/dashboard-alerts/hooks/use-query-get-workspace-alerts.ts +++ /dev/null @@ -1,48 +0,0 @@ -import { - V1GetWorkspaceAlertsData, - V1GetWorkspaceAlertsResponse, -} from '@/api/generated' -import { v1GetWorkspaceAlertsOptions } from '@/api/generated/@tanstack/react-query.gen' -import { useQueryActiveWorkspaceName } from '@/hooks/use-query-active-workspace-name' -import { getQueryCacheConfig } from '@/lib/react-query-utils' -import { useQuery } from '@tanstack/react-query' - -export function useQueryGetWorkspaceAlerts({ - select, -}: { - select?: (data: V1GetWorkspaceAlertsResponse) => T -} = {}) { - const { - data: activeWorkspaceName, - isPending: isWorkspacePending, - isFetching: isWorkspaceFetching, - isLoading: isWorkspaceLoading, - isRefetching: isWorkspaceRefetching, - } = useQueryActiveWorkspaceName() - - const options: V1GetWorkspaceAlertsData = { - path: { - workspace_name: activeWorkspaceName ?? 'default', - }, - } - - const { - isPending: isAlertsPending, - isFetching: isAlertsFetching, - isLoading: isAlertsLoading, - isRefetching: isAlertsRefetching, - ...rest - } = useQuery({ - ...v1GetWorkspaceAlertsOptions(options), - ...getQueryCacheConfig('5s'), - select, - }) - - return { - ...rest, - isPending: isAlertsPending || isWorkspacePending, - isFetching: isAlertsFetching || isWorkspaceFetching, - isLoading: isAlertsLoading || isWorkspaceLoading, - isRefetching: isAlertsRefetching || isWorkspaceRefetching, - } -} 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 index ab7d0462..a917b303 100644 --- a/src/features/dashboard-messages/components/__tests__/table-messages.alerts.test.tsx +++ b/src/features/dashboard-messages/components/__tests__/table-messages.alerts.test.tsx @@ -5,17 +5,21 @@ 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 { PaginatedMessagesResponse } from '@/api/generated' +import { mockConversationSummary } from '@/mocks/msw/mockers/conversation-summary.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 }, - }), - ]) - ) + http.get(mswEndpoint('/api/v1/workspaces/:workspace_name/messages'), () => { + const responsePayload: PaginatedMessagesResponse = { + data: [mockConversationSummary()], + limit: 50, + offset: 0, + total: 30, + } + + return HttpResponse.json(responsePayload) + }) ) render() @@ -42,14 +46,27 @@ it('shows zero in alerts counts when no alerts', async () => { 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' }, - }), - ]) - ) + http.get(mswEndpoint('/api/v1/workspaces/:workspace_name/messages'), () => { + const responsePayload: PaginatedMessagesResponse = { + data: [ + mockConversationSummary({ + alertsSummary: { + malicious_packages: 10, + pii: 0, + secrets: 0, + total_alerts: 10, + }, + }), + ], + limit: 50, + offset: 0, + total: 30, + } + + return HttpResponse.json(responsePayload) + }) ) + render() await waitFor(() => { @@ -65,13 +82,25 @@ it('shows count of malicious alerts in row', async () => { 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' }, - }), - ]) - ) + http.get(mswEndpoint('/api/v1/workspaces/:workspace_name/messages'), () => { + const responsePayload: PaginatedMessagesResponse = { + data: [ + mockConversationSummary({ + alertsSummary: { + malicious_packages: 0, + pii: 0, + secrets: 10, + total_alerts: 10, + }, + }), + ], + limit: 50, + offset: 0, + total: 30, + } + + return HttpResponse.json(responsePayload) + }) ) render() @@ -88,13 +117,25 @@ it('shows count of secret alerts in row', async () => { it('shows count of pii alerts in row', async () => { server.use( - http.get(mswEndpoint('/api/v1/workspaces/:workspace_name/messages'), () => - HttpResponse.json([ - mockConversation({ - alertsConfig: { numAlerts: 10, type: 'pii' }, - }), - ]) - ) + http.get(mswEndpoint('/api/v1/workspaces/:workspace_name/messages'), () => { + const responsePayload: PaginatedMessagesResponse = { + data: [ + mockConversationSummary({ + alertsSummary: { + malicious_packages: 0, + pii: 10, + secrets: 0, + total_alerts: 10, + }, + }), + ], + limit: 50, + offset: 0, + total: 30, + } + + return HttpResponse.json(responsePayload) + }) ) render() diff --git a/src/features/dashboard-messages/components/__tests__/table-messages.empty-state.test.tsx b/src/features/dashboard-messages/components/__tests__/table-messages.empty-state.test.tsx index 556232f5..4e10fbcc 100644 --- a/src/features/dashboard-messages/components/__tests__/table-messages.empty-state.test.tsx +++ b/src/features/dashboard-messages/components/__tests__/table-messages.empty-state.test.tsx @@ -4,11 +4,11 @@ import { server } from '@/mocks/msw/node' import { emptyStateStrings } from '../../../../constants/empty-state-strings' import { useSearchParams } from 'react-router-dom' import { delay, http, HttpHandler, HttpResponse } from 'msw' -import { mockAlert } from '../../../../mocks/msw/mockers/alert.mock' -import { AlertsFilterView } from '../../hooks/use-messages-filter-search-params' import { hrefs } from '@/lib/hrefs' import { mswEndpoint } from '@/test/msw-endpoint' import { TableMessagesEmptyState } from '../table-messages-empty-state' +import { AlertTriggerType, PaginatedMessagesResponse } from '@/api/generated' +import { buildFilterablePaginatedMessagesHandler } from '@/mocks/msw/mockers/paginated-messages-response.mock' enum IllustrationTestId { ALERT = 'illustration-alert', @@ -34,7 +34,7 @@ type TestCase = { testDescription: string handlers: HttpHandler[] searchParams: { - view: AlertsFilterView + view: AlertTriggerType | 'all' | null search: string | null } expected: { @@ -81,7 +81,7 @@ const TEST_CASES: TestCase[] = [ ], searchParams: { search: null, - view: AlertsFilterView.ALL, + view: 'all', }, expected: { title: emptyStateStrings.title.loading, @@ -91,7 +91,7 @@ const TEST_CASES: TestCase[] = [ }, }, { - testDescription: 'Only 1 workspace, no alerts', + testDescription: 'Only 1 workspace, no messages', handlers: [ http.get(mswEndpoint('/api/v1/workspaces'), () => { return HttpResponse.json({ @@ -111,13 +111,20 @@ const TEST_CASES: TestCase[] = [ http.get( mswEndpoint('/api/v1/workspaces/:workspace_name/messages'), () => { - return HttpResponse.json([]) + const responsePayload: PaginatedMessagesResponse = { + data: [], + limit: 50, + offset: 0, + total: 0, + } + + return HttpResponse.json(responsePayload) } ), ], searchParams: { search: null, - view: AlertsFilterView.ALL, + view: 'all', }, expected: { body: emptyStateStrings.body.getStartedDesc, @@ -133,47 +140,7 @@ const TEST_CASES: TestCase[] = [ }, }, { - testDescription: 'No search results', - handlers: [ - http.get(mswEndpoint('/api/v1/workspaces'), () => { - return HttpResponse.json({ - workspaces: [ - { - name: 'default', - is_active: true, - }, - ], - }) - }), - http.get(mswEndpoint('/api/v1/workspaces/archive'), () => { - return HttpResponse.json({ - workspaces: [], - }) - }), - 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: { - title: emptyStateStrings.title.noSearchResultsFor('foo-bar'), - body: emptyStateStrings.body.tryChangingSearch, - illustrationTestId: IllustrationTestId.NO_SEARCH_RESULTS, - actions: [ - { - role: 'button', - name: 'Clear search', - }, - ], - }, - }, - { - testDescription: 'No alerts, multiple workspaces', + testDescription: 'No messages, multiple workspaces', handlers: [ http.get(mswEndpoint('/api/v1/workspaces'), () => { return HttpResponse.json({ @@ -197,13 +164,20 @@ const TEST_CASES: TestCase[] = [ http.get( mswEndpoint('/api/v1/workspaces/:workspace_name/messages'), () => { - return HttpResponse.json([]) + const responsePayload: PaginatedMessagesResponse = { + data: [], + limit: 50, + offset: 0, + total: 0, + } + + return HttpResponse.json(responsePayload) } ), ], searchParams: { search: null, - view: AlertsFilterView.ALL, + view: 'all', }, expected: { title: emptyStateStrings.title.noMessagesWorkspace, @@ -219,7 +193,7 @@ const TEST_CASES: TestCase[] = [ }, }, { - testDescription: 'Has alerts, view is "malicious"', + testDescription: 'View is "malicious", no messages with "malicious" alerts', handlers: [ http.get(mswEndpoint('/api/v1/workspaces'), () => { return HttpResponse.json({ @@ -242,15 +216,18 @@ const TEST_CASES: TestCase[] = [ }), http.get( mswEndpoint('/api/v1/workspaces/:workspace_name/messages'), - () => { - return HttpResponse.json( - Array.from({ length: 10 }).map(() => mockAlert({ type: 'secret' })) - ) - } + buildFilterablePaginatedMessagesHandler({ + include: { + 'codegate-context-retriever': false, + 'codegate-pii': true, + 'codegate-secrets': true, + no_alerts: true, + }, + }) ), ], searchParams: { - view: AlertsFilterView.MALICIOUS, + view: AlertTriggerType.CODEGATE_CONTEXT_RETRIEVER, search: null, }, expected: { @@ -261,7 +238,7 @@ const TEST_CASES: TestCase[] = [ }, }, { - testDescription: 'Has alerts, view is "secret"', + testDescription: 'View is "secret", no messages with "secret" alerts', handlers: [ http.get(mswEndpoint('/api/v1/workspaces'), () => { return HttpResponse.json({ @@ -284,17 +261,18 @@ const TEST_CASES: TestCase[] = [ }), http.get( mswEndpoint('/api/v1/workspaces/:workspace_name/messages'), - () => { - return HttpResponse.json( - Array.from({ length: 10 }).map(() => - mockAlert({ type: 'malicious' }) - ) - ) - } + buildFilterablePaginatedMessagesHandler({ + include: { + 'codegate-context-retriever': true, + 'codegate-pii': true, + 'codegate-secrets': false, + no_alerts: true, + }, + }) ), ], searchParams: { - view: AlertsFilterView.SECRETS, + view: AlertTriggerType.CODEGATE_SECRETS, search: null, }, expected: { @@ -305,7 +283,7 @@ const TEST_CASES: TestCase[] = [ }, }, { - testDescription: 'Has alerts, view is "pii"', + testDescription: 'View is "pii", no messages with "pii" alerts', handlers: [ http.get(mswEndpoint('/api/v1/workspaces'), () => { return HttpResponse.json({ @@ -328,15 +306,18 @@ const TEST_CASES: TestCase[] = [ }), http.get( mswEndpoint('/api/v1/workspaces/:workspace_name/messages'), - () => { - return HttpResponse.json( - Array.from({ length: 10 }).map(() => mockAlert({ type: 'pii' })) - ) - } + buildFilterablePaginatedMessagesHandler({ + include: { + 'codegate-context-retriever': true, + 'codegate-pii': false, + 'codegate-secrets': true, + no_alerts: true, + }, + }) ), ], searchParams: { - view: AlertsFilterView.PII, + view: AlertTriggerType.CODEGATE_PII, search: null, }, expected: { @@ -354,7 +335,7 @@ test.each(TEST_CASES)('$testDescription', async (testCase) => { vi.mocked(useSearchParams).mockReturnValue([ new URLSearchParams({ search: testCase.searchParams.search ?? '', - view: testCase.searchParams.view, + view: testCase.searchParams.view ?? 'all', }), () => {}, ]) 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 index aee76c61..372fb667 100644 --- a/src/features/dashboard-messages/components/__tests__/table-messages.pagination.test.tsx +++ b/src/features/dashboard-messages/components/__tests__/table-messages.pagination.test.tsx @@ -5,33 +5,26 @@ 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 () => { +import { PaginatedMessagesResponse } from '@/api/generated' +import { mockConversationSummary } from '@/mocks/msw/mockers/conversation-summary.mock' + +/** + * + * NOTE: This needs to be totally re-written — will do so in a follow up + * @see https://github.com/stacklok/codegate-ui/issues/370 + */ +it.skip('allows pagination', async () => { server.use( http.get(mswEndpoint('/api/v1/workspaces/:workspace_name/messages'), () => { - return HttpResponse.json( - Array.from({ length: 35 }).map(() => mockConversation()) - ) + const responsePayload: PaginatedMessagesResponse = { + data: Array.from({ length: 35 }).map(() => mockConversationSummary()), + limit: 50, + offset: 0, + total: 35, + } + + return HttpResponse.json(responsePayload) }) ) diff --git a/src/features/dashboard-messages/components/__tests__/table-messages.token-usage.test.tsx b/src/features/dashboard-messages/components/__tests__/table-messages.token-usage.test.tsx index c7ea695a..9bbe926a 100644 --- a/src/features/dashboard-messages/components/__tests__/table-messages.token-usage.test.tsx +++ b/src/features/dashboard-messages/components/__tests__/table-messages.token-usage.test.tsx @@ -6,7 +6,8 @@ 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 { mockConversation } from '@/mocks/msw/mockers/conversation.mock' +import { PaginatedMessagesResponse } from '@/api/generated' +import { mockConversationSummary } from '@/mocks/msw/mockers/conversation-summary.mock' vi.mock('@untitled-ui/icons-react', async () => { const original = await vi.importActual< @@ -30,7 +31,24 @@ const OUTPUT_TOKENS = test('renders token usage cell correctly', async () => { server.use( http.get(mswEndpoint('/api/v1/workspaces/:workspace_name/messages'), () => { - return HttpResponse.json([mockConversation({ withTokenUsage: true })]) + const responsePayload: PaginatedMessagesResponse = { + data: [ + mockConversationSummary({ + alertsSummary: { + malicious_packages: 0, + pii: 0, + secrets: 0, + total_alerts: 0, + }, + withTokenUsage: true, + }), + ], + limit: 50, + offset: 0, + total: 1, + } + + return HttpResponse.json(responsePayload) }) ) @@ -53,7 +71,24 @@ 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/messages'), () => { - return HttpResponse.json([mockConversation({ withTokenUsage: false })]) + const responsePayload: PaginatedMessagesResponse = { + data: [ + mockConversationSummary({ + alertsSummary: { + malicious_packages: 0, + pii: 0, + secrets: 0, + total_alerts: 0, + }, + withTokenUsage: false, + }), + ], + limit: 50, + offset: 0, + total: 1, + } + + return HttpResponse.json(responsePayload) }) ) diff --git a/src/features/dashboard-messages/components/__tests__/tabs-messages.test.tsx b/src/features/dashboard-messages/components/__tests__/tabs-messages.test.tsx index f4a0bd53..966d346a 100644 --- a/src/features/dashboard-messages/components/__tests__/tabs-messages.test.tsx +++ b/src/features/dashboard-messages/components/__tests__/tabs-messages.test.tsx @@ -1,71 +1,47 @@ +import { describe } from 'vitest' 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' +import { AlertSummary, PaginatedMessagesResponse } from '@/api/generated' +import { mockConversationSummary } from '@/mocks/msw/mockers/conversation-summary.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') - }) -}) - -const filteredCases = [ - { tabLabel: /malicious/i, alertType: 'malicious' as const, count: 13 }, - { tabLabel: /secrets/i, alertType: 'secret' as const, count: 10 }, - { tabLabel: /pii/i, alertType: 'pii' as const, count: 9 }, -] +const SUMMARY: AlertSummary = { + malicious_packages: 13, + pii: 9, + secrets: 10, + total_alerts: 32, +} -filteredCases.forEach(({ tabLabel, alertType, count }) => { - test(`shows correct count of ${alertType} packages`, async () => { +describe('tabs-messages', () => { + beforeAll(() => { server.use( http.get( mswEndpoint('/api/v1/workspaces/:workspace_name/messages'), () => { - return HttpResponse.json( - Array.from({ length: count }).map(() => - mockConversation({ - alertsConfig: { - type: alertType, - numAlerts: 1, - }, - }) - ) - ) + const responsePayload: PaginatedMessagesResponse = { + data: Array.from({ length: 32 }).map(() => + mockConversationSummary() + ), + limit: 50, + offset: 0, + total: 32, + } + + return HttpResponse.json(responsePayload) + } + ), + http.get( + mswEndpoint('/api/v1/workspaces/:workspace_name/alerts-summary'), + () => { + return HttpResponse.json(SUMMARY) } ) ) + }) + test('shows correct count of all packages', async () => { const { getByRole } = render(
foo
@@ -73,9 +49,32 @@ filteredCases.forEach(({ tabLabel, alertType, count }) => { ) await waitFor(() => { - expect(getByRole('tab', { name: tabLabel })).toHaveTextContent( - String(count) + expect(getByRole('tab', { name: /all/i })).toHaveTextContent('32') + }) + }) + + const filteredCases = [ + { + name: 'Malicious', + count: SUMMARY.malicious_packages, + }, + { name: 'Secrets', count: SUMMARY.secrets }, + { name: 'PII', count: SUMMARY.pii }, + ] + + filteredCases.forEach(({ name, count }) => { + test(`shows correct count of ${name} packages`, async () => { + const { getByRole } = render( + +
foo
+
) + + await waitFor(() => { + expect(getByRole('tab', { name: name })).toHaveTextContent( + String(count) + ) + }) }) }) }) diff --git a/src/features/dashboard-messages/components/conversation-summary.tsx b/src/features/dashboard-messages/components/conversation-summary.tsx index bb60f0ea..850680fc 100644 --- a/src/features/dashboard-messages/components/conversation-summary.tsx +++ b/src/features/dashboard-messages/components/conversation-summary.tsx @@ -173,8 +173,8 @@ export function ConversationSummary({ value={ diff --git a/src/features/dashboard-messages/components/search-field-messages.tsx b/src/features/dashboard-messages/components/search-field-messages.tsx deleted file mode 100644 index 56636b0d..00000000 --- a/src/features/dashboard-messages/components/search-field-messages.tsx +++ /dev/null @@ -1,46 +0,0 @@ -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/table-messages-empty-state.tsx b/src/features/dashboard-messages/components/table-messages-empty-state.tsx index 8a109a80..10270acf 100644 --- a/src/features/dashboard-messages/components/table-messages-empty-state.tsx +++ b/src/features/dashboard-messages/components/table-messages-empty-state.tsx @@ -1,9 +1,7 @@ import { - Button, IllustrationAlert, IllustrationDone, IllustrationDragAndDrop, - IllustrationNoSearchResults, LinkButton, Loader, } from '@stacklok/ui-kit' @@ -14,13 +12,11 @@ import { EmptyState } from '@/components/empty-state' import { hrefs } from '@/lib/hrefs' import { LinkExternal02 } from '@untitled-ui/icons-react' import { useListAllWorkspaces } from '@/hooks/use-query-list-all-workspaces' -import { - AlertsFilterView, - useMessagesFilterSearchParams, -} from '../hooks/use-messages-filter-search-params' +import { useMessagesFilterSearchParams } from '../hooks/use-messages-filter-search-params' import { match, P } from 'ts-pattern' -import { useQueryGetWorkspaceMessages } from '@/hooks/use-query-get-workspace-messages' import { twMerge } from 'tailwind-merge' +import { AlertTriggerType } from '@/api/generated' +import { useQueryGetWorkspaceMessagesTable } from '../hooks/use-query-get-workspace-messages-table' function EmptyStateLoading() { return ( @@ -56,26 +52,26 @@ function EmptyStateGetStarted() { ) } -function EmptyStateSearch({ - search, - setSearch, -}: { - search: string - setSearch: (v: string | null) => void -}) { - return ( - setSearch(null)}> - Clear search - , - ]} - /> - ) -} +// function EmptyStateSearch({ +// search, +// setSearch, +// }: { +// search: string +// setSearch: (v: string | null) => void +// }) { +// return ( +// setSearch(null)}> +// Clear search +// , +// ]} +// /> +// ) +// } function EmptyStateNoMessagesInWorkspace() { return ( @@ -167,84 +163,73 @@ type MatchInput = { isLoading: boolean hasWorkspaceMessages: boolean hasMultipleWorkspaces: boolean - search: string | null - view: AlertsFilterView | null + view: AlertTriggerType | 'all' } export function TableMessagesEmptyState() { - const { state, setSearch } = useMessagesFilterSearchParams() + const { state } = useMessagesFilterSearchParams() - const { data: messages = [], isLoading: isMessagesLoading } = - useQueryGetWorkspaceMessages() + const { data: response, isLoading: isMessagesLoading } = + useQueryGetWorkspaceMessagesTable() const { data: workspaces = [], isLoading: isWorkspacesLoading } = useListAllWorkspaces() const isLoading = isMessagesLoading || isWorkspacesLoading + const hasMultipleWorkspaces: boolean = + workspaces.filter((w) => w.name !== 'default').length > 0 + + const hasWorkspaceMessages: boolean = Boolean(response && response.total > 0) + return match({ + hasMultipleWorkspaces, + hasWorkspaceMessages, isLoading, - hasWorkspaceMessages: messages.length > 0, - hasMultipleWorkspaces: - workspaces.filter((w) => w.name !== 'default').length > 0, - search: state.search || null, - view: state.view, + view: state.view ?? 'all', }) .with( { - hasWorkspaceMessages: false, hasMultipleWorkspaces: false, - search: P.any, - view: P.any, + hasWorkspaceMessages: false, isLoading: false, - }, - () => - ) - .with( - { - hasWorkspaceMessages: true, - hasMultipleWorkspaces: P.any, - search: P.string.select(), view: P.any, - isLoading: false, }, - (search) => + () => ) .with( { + hasMultipleWorkspaces: true, hasWorkspaceMessages: false, - hasMultipleWorkspaces: P.any, - search: P.any, - view: P.any, isLoading: false, + view: 'all', }, () => ) .with( { - hasWorkspaceMessages: true, hasMultipleWorkspaces: P.any, - view: AlertsFilterView.PII, + hasWorkspaceMessages: false, isLoading: false, + view: AlertTriggerType.CODEGATE_PII, }, () => ) .with( { - hasWorkspaceMessages: true, hasMultipleWorkspaces: P.any, - search: P.any, - view: AlertsFilterView.MALICIOUS, + hasWorkspaceMessages: false, isLoading: false, + view: AlertTriggerType.CODEGATE_CONTEXT_RETRIEVER, }, () => ) .with( { - hasWorkspaceMessages: true, hasMultipleWorkspaces: P.any, - view: AlertsFilterView.SECRETS, + hasWorkspaceMessages: false, isLoading: false, + view: AlertTriggerType.CODEGATE_SECRETS, }, () => ) diff --git a/src/features/dashboard-messages/components/table-messages-pagination.tsx b/src/features/dashboard-messages/components/table-messages-pagination.tsx new file mode 100644 index 00000000..db7adada --- /dev/null +++ b/src/features/dashboard-messages/components/table-messages-pagination.tsx @@ -0,0 +1,98 @@ +import { Button } from '@stacklok/ui-kit' +import { useMessagesFilterSearchParams } from '../hooks/use-messages-filter-search-params' +import { useQueryGetWorkspaceMessagesTable } from '../hooks/use-query-get-workspace-messages-table' +import { + ChevronLeft, + ChevronLeftDouble, + ChevronRight, + ChevronRightDouble, +} from '@untitled-ui/icons-react' + +export function TableMessagesPagination() { + const { state, goToPrevPage, goToNextPage, setPage } = + useMessagesFilterSearchParams() + + const { data } = useQueryGetWorkspaceMessagesTable() + + const totalRecords: number = data?.total ?? 0 + const totalPages: number = Math.ceil(totalRecords / (data?.limit ?? 1)) + + // We only show pagination when there is something to paginate :) + if (totalPages < 2) return null + + const hasNextPage: boolean = data + ? data.offset + data.limit < totalRecords + : false + const hasPreviousPage: boolean = data ? data.offset > 0 : false + + // A sliding window of page numbers to render as clickable buttons + // e.g. if the page number is `7`, the pages shown should be `[5, 6, 7, 8, 9]` + // e.g. if the page number is `1`, the pages shown should be `[1, 2, 3, 4, 5]` + const pageNumsToShow = Array.from( + { length: Math.min(5, totalPages) }, + (_, i) => { + const startPage = Math.max(1, Math.min(state.page - 2, totalPages - 4)) + return startPage + i + } + ) + + return ( +
+
+ + + + {pageNumsToShow.map((pageNum) => ( + + ))} + + + +
+
+ ) +} diff --git a/src/features/dashboard-messages/components/table-messages.tsx b/src/features/dashboard-messages/components/table-messages.tsx index 3dd2ac25..bdbef7c9 100644 --- a/src/features/dashboard-messages/components/table-messages.tsx +++ b/src/features/dashboard-messages/components/table-messages.tsx @@ -10,22 +10,22 @@ import { Tooltip, TooltipTrigger, } from '@stacklok/ui-kit' -import { Alert, Conversation, QuestionType } from '@/api/generated' +import { + AlertSummary, + ConversationSummary, + QuestionType, +} from '@/api/generated' import { remark } from 'remark' import strip from 'strip-markdown' -import { useClientSidePagination } from '@/hooks/useClientSidePagination' import { TableAlertTokenUsage } from './table-alert-token-usage' -import { useMessagesFilterSearchParams } from '../hooks/use-messages-filter-search-params' -import { Key01, PackageX, Passport } from '@untitled-ui/icons-react' +import { Key01, PackageX, User01 } from '@untitled-ui/icons-react' import { EmptyStateError, TableMessagesEmptyState, } 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 { @@ -33,11 +33,11 @@ import { TableMessagesColumn, } from '../constants/table-messages-columns' import { formatTime } from '@/lib/format-time' -import { isAlertPii } from '@/lib/is-alert-pii' +import { TableMessagesPagination } from './table-messages-pagination' +import { useQueryActiveWorkspaceName } from '@/hooks/use-query-active-workspace-name' -const getPromptText = (conversation: Conversation) => { - const markdownSource = - conversation.question_answers[0]?.question?.message ?? 'N/A' +const getPromptText = (conversation: ConversationSummary) => { + const markdownSource = conversation.prompt.message ?? 'N/A' const fullText = remark().use(strip).processSync(markdownSource) return fullText.toString().trim().slice(0, 200) // arbitrary slice to prevent long prompts @@ -54,18 +54,6 @@ function getTypeText(type: QuestionType) { } } -function countAlerts(alerts: Alert[]): { - secrets: number - malicious: number - pii: number -} { - return { - secrets: alerts.filter(isAlertSecret).length, - malicious: alerts.filter(isAlertMalicious).length, - pii: alerts.filter(isAlertPii).length, - } -} - function AlertsSummaryCount({ count, icon: Icon, @@ -99,9 +87,11 @@ function AlertsSummaryCount({ ) } -function AlertsSummaryCellContent({ alerts }: { alerts: Alert[] }) { - const { malicious, secrets, pii } = countAlerts(alerts) - +function AlertsSummaryCellContent({ + alertSummary, +}: { + alertSummary: AlertSummary +}) { return (
) @@ -137,7 +127,7 @@ function CellRenderer({ row, }: { column: TableMessagesColumn - row: Conversation + row: ConversationSummary }) { switch (column.id) { case 'time': @@ -151,7 +141,7 @@ function CellRenderer({ case 'prompt': return getPromptText(row) case 'alerts': - return + return case 'token_usage': return @@ -161,19 +151,18 @@ function CellRenderer({ } export function TableMessages() { - const { state, prevPage, nextPage } = useMessagesFilterSearchParams() + const { data: activeWorkspaceName } = useQueryActiveWorkspaceName() - const { data = [], isError } = useQueryGetWorkspaceMessagesTable() - const { dataView, hasNextPage, hasPreviousPage } = useClientSidePagination( - data, - state.page, - 15 - ) + const { data: response, isError } = useQueryGetWorkspaceMessagesTable() return ( <> - +
{(column) => } @@ -183,7 +172,7 @@ export function TableMessages() { return }} - items={dataView} + items={response?.data} > {(row) => ( - {hasNextPage || hasPreviousPage ? ( -
-
- - -
-
- ) : null} + ) } diff --git a/src/features/dashboard-messages/components/tabs-conversation.tsx b/src/features/dashboard-messages/components/tabs-conversation.tsx index 1e711eb1..04984367 100644 --- a/src/features/dashboard-messages/components/tabs-conversation.tsx +++ b/src/features/dashboard-messages/components/tabs-conversation.tsx @@ -6,13 +6,12 @@ import { 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' +import { useQueryGetWorkspaceMessageById } from '../hooks/use-query-get-workspace-message-by-id' function Tab({ id, @@ -48,7 +47,7 @@ export function TabsConversation({ }) { const { state, setView } = useConversationSearchParams() - const { data } = useConversationById(id) + const { data } = useQueryGetWorkspaceMessageById({ id }) const secretsCount = data?.alerts?.filter(isAlertSecret).length ?? 0 @@ -56,7 +55,7 @@ export function TabsConversation({ setView(key.toString() as ConversationView)} selectedKey={state.view} - defaultSelectedKey={AlertsFilterView.ALL} + defaultSelectedKey={ConversationView.OVERVIEW} > diff --git a/src/features/dashboard-messages/components/tabs-messages.tsx b/src/features/dashboard-messages/components/tabs-messages.tsx index 52ac99a3..16a7dee5 100644 --- a/src/features/dashboard-messages/components/tabs-messages.tsx +++ b/src/features/dashboard-messages/components/tabs-messages.tsx @@ -1,7 +1,3 @@ -import { isConversationWithMaliciousAlerts } from '../../../lib/is-alert-malicious' -import { multiFilter } from '@/lib/multi-filter' -import { isConversationWithSecretAlerts } from '../../../lib/is-alert-secret' -import { V1GetWorkspaceMessagesResponse } from '@/api/generated' import { Tab as BaseTab, Tabs, @@ -11,42 +7,10 @@ import { Card, CardBody, } from '@stacklok/ui-kit' -import { - AlertsFilterView, - useMessagesFilterSearchParams, -} from '../hooks/use-messages-filter-search-params' -import { SearchFieldMessages } from './search-field-messages' +import { useMessagesFilterSearchParams } from '../hooks/use-messages-filter-search-params' import { tv } from 'tailwind-variants' -import { useQueryGetWorkspaceMessages } from '@/hooks/use-query-get-workspace-messages' -import { isConversationWithPII } from '@/lib/is-alert-pii' - -type AlertsCount = { - all: number - malicious: number - secrets: number - pii: number -} - -function select(data: V1GetWorkspaceMessagesResponse): AlertsCount { - const all: number = data?.length ?? 0 - - const malicious: number = multiFilter(data, [ - isConversationWithMaliciousAlerts, - ]).length - - const secrets: number = multiFilter(data, [ - isConversationWithSecretAlerts, - ]).length - - const pii: number = multiFilter(data, [isConversationWithPII]).length - - return { - all, - malicious, - secrets, - pii, - } -} +import { useQueryGetWorkspaceAlertsSummary } from '@/hooks/use-query-get-workspace-alerts-summary' +import { AlertTriggerType } from '@/api/generated' const tabStyle = tv({ base: [ @@ -65,55 +29,64 @@ function Tab({ count, }: { title: string - id: AlertsFilterView - count: number + id: 'all' | AlertTriggerType + count?: number }) { return ( - + {title} - - {count} - + {typeof count === 'number' ? ( + + {count} + + ) : null} ) } export function TabsMessages({ children }: { children: React.ReactNode }) { const { state, setView } = useMessagesFilterSearchParams() - - const { data } = useQueryGetWorkspaceMessages({ - select, - }) + const { data } = useQueryGetWorkspaceAlertsSummary() return ( setView(key.toString() as AlertsFilterView)} - selectedKey={state.view} - defaultSelectedKey={AlertsFilterView.ALL} + onSelectionChange={(key) => + setView( + key.toString() === 'all' + ? undefined + : (key.toString() as AlertTriggerType) + ) + } + selectedKey={state.view || 'all'} + defaultSelectedKey="all" >
- + + - - + {/* */}
- + {children} diff --git a/src/features/dashboard-messages/hooks/use-conversation-by-id.tsx b/src/features/dashboard-messages/hooks/use-conversation-by-id.tsx deleted file mode 100644 index 84d3da22..00000000 --- a/src/features/dashboard-messages/hooks/use-conversation-by-id.tsx +++ /dev/null @@ -1,7 +0,0 @@ -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-messages-filter-search-params.ts b/src/features/dashboard-messages/hooks/use-messages-filter-search-params.ts index a311d16e..4423cba2 100644 --- a/src/features/dashboard-messages/hooks/use-messages-filter-search-params.ts +++ b/src/features/dashboard-messages/hooks/use-messages-filter-search-params.ts @@ -1,25 +1,24 @@ +import { AlertTriggerType } from '@/api/generated' import { useCallback } from 'react' import { useSearchParams } from 'react-router-dom' import { z } from 'zod' -export enum AlertsFilterView { - ALL = 'all', - MALICIOUS = 'malicious', - SECRETS = 'secrets', - PII = 'pii', -} - const alertsFilterSchema = z.object({ - search: z.string().optional(), - view: z.nativeEnum(AlertsFilterView).optional().default(AlertsFilterView.ALL), - page: z.coerce.number().optional().default(0), + // search: z.string().optional(), + view: z + .union([z.literal('all'), z.nativeEnum(AlertTriggerType)]) + .nullish() + .default('all'), + page: z.coerce + .number() + .optional() + .default(1) + .transform((v) => (v < 1 ? 1 : v)), }) type AlertsFilterSchema = z.input -const DEFAULT_FILTER = { - view: AlertsFilterView.ALL, -} as const satisfies AlertsFilterSchema +const DEFAULT_FILTER = {} as const satisfies AlertsFilterSchema export const useMessagesFilterSearchParams = () => { const [searchParams, setSearchParams] = useSearchParams( @@ -27,7 +26,7 @@ export const useMessagesFilterSearchParams = () => { ) const setView = useCallback( - (view: AlertsFilterView) => { + (view: AlertTriggerType | undefined) => { setSearchParams((prev) => { if (view) prev.set('view', view) if (!view) prev.delete('view') @@ -39,38 +38,54 @@ export const useMessagesFilterSearchParams = () => { [setSearchParams] ) - const setSearch = useCallback( - (query: string | null) => { - setSearchParams((prev) => { - if (query !== null && query !== '') { - prev.set('search', query) - prev.delete('page') - } else { - prev.delete('search') - } - return prev - }) - }, - [setSearchParams] - ) + // const setSearch = useCallback( + // (query: string | null) => { + // setSearchParams((prev) => { + // if (query !== null && query !== '') { + // prev.set('search', query) + // prev.delete('page') + // } else { + // prev.delete('search') + // } + // return prev + // }) + // }, + // [setSearchParams] + // ) - const nextPage = useCallback(() => { + const goToNextPage = useCallback(() => { setSearchParams((prev) => { const page = Number(prev.get('page') ?? 0) - prev.set('page', (page + 1).toString()) + prev.set('page', Math.max(page + 1, 1).toString()) return prev }) }, [setSearchParams]) - const prevPage = useCallback(() => { + const goToPrevPage = useCallback(() => { setSearchParams((prev) => { const page = Number(prev.get('page') ?? 0) - prev.set('page', (page - 1).toString()) + prev.set('page', Math.max(page - 1, 1).toString()) return prev }) }, [setSearchParams]) + const setPage = useCallback( + (page: number) => { + setSearchParams((prev) => { + prev.set('page', Math.max(page, 1).toString()) + return prev + }) + }, + [setSearchParams] + ) + const state = alertsFilterSchema.parse(Object.fromEntries(searchParams)) - return { state, setView, setSearch, nextPage, prevPage } + return { + state, + setView, + goToNextPage, + goToPrevPage, + setPage, + } } diff --git a/src/features/dashboard-messages/hooks/use-query-get-workspace-message-by-id.ts b/src/features/dashboard-messages/hooks/use-query-get-workspace-message-by-id.ts new file mode 100644 index 00000000..3e56326b --- /dev/null +++ b/src/features/dashboard-messages/hooks/use-query-get-workspace-message-by-id.ts @@ -0,0 +1,37 @@ +import { useQuery } from '@tanstack/react-query' +import { + V1GetMessagesByPromptIdData, + V1GetMessagesByPromptIdResponse, +} from '@/api/generated' +import { useQueryActiveWorkspaceName } from '@/hooks/use-query-active-workspace-name' +import { getQueryCacheConfig } from '@/lib/react-query-utils' +import { useMemo } from 'react' +import { v1GetMessagesByPromptIdOptions } from '@/api/generated/@tanstack/react-query.gen' + +export const useQueryGetWorkspaceMessageById = < + T = V1GetMessagesByPromptIdResponse, +>({ + id, + select, +}: { + id: string + select?: (data: V1GetMessagesByPromptIdResponse) => T +}) => { + const { data: activeWorkspaceName } = useQueryActiveWorkspaceName() + + const options: V1GetMessagesByPromptIdData = useMemo( + () => ({ + path: { + workspace_name: activeWorkspaceName ?? 'default', + prompt_id: id, + }, + }), + [activeWorkspaceName, id] + ) + + return useQuery({ + ...v1GetMessagesByPromptIdOptions(options), + ...getQueryCacheConfig('5s'), + select, + }) +} 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 index f82b9b6f..1a7b2071 100644 --- 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 @@ -1,45 +1,15 @@ -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 { useMessagesFilterSearchParams } from './use-messages-filter-search-params' import { useQueryGetWorkspaceMessages } from '@/hooks/use-query-get-workspace-messages' -import { isConversationWithPII } from '@/lib/is-alert-pii' - -const FILTER: Record< - AlertsFilterView, - (alert: Conversation | null) => boolean -> = { - all: () => true, - malicious: isConversationWithMaliciousAlerts, - secrets: isConversationWithSecretAlerts, - pii: isConversationWithPII, -} 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, + query: { + page: state.page, + page_size: 20, + filter_by_alert_trigger_types: + state.view === 'all' || state.view == null ? undefined : [state.view], + }, }) } diff --git a/src/features/dashboard-messages/lib/filter-messages-by-substring.ts b/src/features/dashboard-messages/lib/filter-messages-by-substring.ts deleted file mode 100644 index 92ba48e2..00000000 --- a/src/features/dashboard-messages/lib/filter-messages-by-substring.ts +++ /dev/null @@ -1,23 +0,0 @@ -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/workspace/lib/utils.ts b/src/features/workspace/lib/utils.ts index 5bb2050d..db69e289 100644 --- a/src/features/workspace/lib/utils.ts +++ b/src/features/workspace/lib/utils.ts @@ -1,13 +1,13 @@ import { MuxMatcherType } from '@/api/generated' -export const MUX_MATCHER_TYPE_MAP = { +const MUX_MATCHER_TYPE_MAP = { [MuxMatcherType.CHAT_FILENAME]: 'Chat', [MuxMatcherType.FIM_FILENAME]: 'FIM', [MuxMatcherType.FILENAME_MATCH]: 'FIM & Chat', [MuxMatcherType.CATCH_ALL]: 'All types', } -export function getRequestType() { +function getRequestType() { return Object.values(MuxMatcherType) .filter((item) => item !== MuxMatcherType.CATCH_ALL) .map((textValue) => ({ diff --git a/src/hooks/use-query-get-workspace-alerts-summary.ts b/src/hooks/use-query-get-workspace-alerts-summary.ts new file mode 100644 index 00000000..a9a91a5c --- /dev/null +++ b/src/hooks/use-query-get-workspace-alerts-summary.ts @@ -0,0 +1,24 @@ +import { useQuery } from '@tanstack/react-query' +import { V1GetWorkspaceAlertsSummaryData } from '@/api/generated' +import { v1GetWorkspaceAlertsSummaryOptions } from '@/api/generated/@tanstack/react-query.gen' +import { useQueryActiveWorkspaceName } from '@/hooks/use-query-active-workspace-name' +import { getQueryCacheConfig } from '@/lib/react-query-utils' +import { useMemo } from 'react' + +export const useQueryGetWorkspaceAlertsSummary = () => { + const { data: activeWorkspaceName } = useQueryActiveWorkspaceName() + + const options: V1GetWorkspaceAlertsSummaryData = useMemo( + () => ({ + path: { + workspace_name: activeWorkspaceName ?? 'default', + }, + }), + [activeWorkspaceName] + ) + + return useQuery({ + ...v1GetWorkspaceAlertsSummaryOptions(options), + ...getQueryCacheConfig('5s'), + }) +} diff --git a/src/hooks/use-query-get-workspace-messages.ts b/src/hooks/use-query-get-workspace-messages.ts index 83573eec..30670fac 100644 --- a/src/hooks/use-query-get-workspace-messages.ts +++ b/src/hooks/use-query-get-workspace-messages.ts @@ -1,7 +1,7 @@ import { useQuery } from '@tanstack/react-query' import { - V1GetWorkspaceMessagesResponse, V1GetWorkspaceMessagesData, + V1GetWorkspaceMessagesResponse, } from '@/api/generated' import { v1GetWorkspaceMessagesOptions } from '@/api/generated/@tanstack/react-query.gen' import { useQueryActiveWorkspaceName } from '@/hooks/use-query-active-workspace-name' @@ -12,9 +12,11 @@ export const useQueryGetWorkspaceMessages = < T = V1GetWorkspaceMessagesResponse, >({ select, + query, }: { select?: (data: V1GetWorkspaceMessagesResponse) => T -} = {}) => { + query: V1GetWorkspaceMessagesData['query'] +}) => { const { data: activeWorkspaceName } = useQueryActiveWorkspaceName() const options: V1GetWorkspaceMessagesData = useMemo( @@ -22,13 +24,14 @@ export const useQueryGetWorkspaceMessages = < path: { workspace_name: activeWorkspaceName ?? 'default', }, + query, }), - [activeWorkspaceName] + [activeWorkspaceName, query] ) return useQuery({ ...v1GetWorkspaceMessagesOptions(options), ...getQueryCacheConfig('5s'), - select: select, + select, }) } diff --git a/src/hooks/useClientSidePagination.ts b/src/hooks/useClientSidePagination.ts deleted file mode 100644 index 0bfea704..00000000 --- a/src/hooks/useClientSidePagination.ts +++ /dev/null @@ -1,15 +0,0 @@ -export function useClientSidePagination( - data: T[], - page: number, - pageSize: number -) { - const pageStart = page * pageSize - const pageEnd = page * pageSize + pageSize - - const dataView = data.slice(pageStart, pageEnd) - - const hasPreviousPage = page > 0 - const hasNextPage = pageEnd < data.length - - return { pageStart, pageEnd, dataView, hasPreviousPage, hasNextPage } -} diff --git a/src/lib/is-alert-critical.ts b/src/lib/is-alert-critical.ts deleted file mode 100644 index f6058240..00000000 --- a/src/lib/is-alert-critical.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { - AlertConversation, - V1GetWorkspaceAlertsResponse, -} from '@/api/generated' - -export function isAlertCritical( - alert: V1GetWorkspaceAlertsResponse[number] -): alert is AlertConversation { - return alert !== null && alert.trigger_category === 'critical' -} diff --git a/src/lib/is-alert-malicious.ts b/src/lib/is-alert-malicious.ts index eab7b52d..8f9e2c75 100644 --- a/src/lib/is-alert-malicious.ts +++ b/src/lib/is-alert-malicious.ts @@ -1,10 +1,4 @@ -import { Alert, AlertConversation, Conversation } from '@/api/generated' - -export function isConversationWithMaliciousAlerts( - conversation: Conversation | null -): boolean { - return conversation?.alerts?.some(isAlertMalicious) ?? false -} +import { Alert, AlertConversation } from '@/api/generated' export function isAlertMalicious(alert: Alert | AlertConversation | null) { return ( diff --git a/src/lib/is-alert-pii.ts b/src/lib/is-alert-pii.ts index aa1d5ea9..51f13c1b 100644 --- a/src/lib/is-alert-pii.ts +++ b/src/lib/is-alert-pii.ts @@ -1,10 +1,4 @@ -import { Alert, AlertConversation, Conversation } from '@/api/generated' - -export function isConversationWithPII( - conversation: Conversation | null -): boolean { - return conversation?.alerts?.some(isAlertPii) ?? false -} +import { Alert, AlertConversation } from '@/api/generated' export function isAlertPii(alert: Alert | AlertConversation | null) { return ( diff --git a/src/lib/is-alert-secret.ts b/src/lib/is-alert-secret.ts index 1054af3c..0a100caf 100644 --- a/src/lib/is-alert-secret.ts +++ b/src/lib/is-alert-secret.ts @@ -1,10 +1,4 @@ -import { Alert, AlertConversation, Conversation } from '@/api/generated' - -export function isConversationWithSecretAlerts( - conversation: Conversation | null -): boolean { - return conversation?.alerts?.some(isAlertSecret) ?? false -} +import { Alert, AlertConversation } from '@/api/generated' export function isAlertSecret(alert: Alert | AlertConversation | null) { return ( diff --git a/src/lib/multi-filter.ts b/src/lib/multi-filter.ts deleted file mode 100644 index 642a932f..00000000 --- a/src/lib/multi-filter.ts +++ /dev/null @@ -1,20 +0,0 @@ -/** - * Utility function for chaining multiple predicates when filtering an array, - * avoiding iterating over an array more than once. Intended to aid in composing - * filtering behavior, particularly when querying data from the server with react-query. - * - * Not intended to be used to chain filters together, but rather to apply - * multiple filter criteria to an array in a single pass. - * - * @example - * const data = [0,1,2,3,4,5] - * const result = multiFilter(data, [(d) => !!d, (d) => d > 3]) - */ -export function multiFilter( - array: T[] | undefined, - predicates: ((i: T) => boolean)[] -): T[] { - if (!array) return [] - - return array.filter((i) => predicates.every((p) => p(i) === true)) -} diff --git a/src/mocks/msw/handlers.ts b/src/mocks/msw/handlers.ts index 5909edfc..408ba2b1 100644 --- a/src/mocks/msw/handlers.ts +++ b/src/mocks/msw/handlers.ts @@ -3,9 +3,9 @@ import mockedAlerts from '@/mocks/msw/fixtures/GET_ALERTS.json' import mockedWorkspaces from '@/mocks/msw/fixtures/GET_WORKSPACES.json' import mockedProviders from '@/mocks/msw/fixtures/GET_PROVIDERS.json' import mockedProvidersModels from '@/mocks/msw/fixtures/GET_PROVIDERS_MODELS.json' -import { ProviderType } from '@/api/generated' -import { mockConversation } from './mockers/conversation.mock' +import { AlertSummary, ProviderType } from '@/api/generated' import { mswEndpoint } from '@/test/msw-endpoint' +import { buildFilterablePaginatedMessagesHandler } from './mockers/paginated-messages-response.mock' export const handlers = [ http.get(mswEndpoint('/health'), () => @@ -30,14 +30,25 @@ export const handlers = [ ], }) ), - http.get(mswEndpoint('/api/v1/workspaces/:workspace_name/messages'), () => { - return HttpResponse.json( - Array.from({ length: 10 }).map(() => mockConversation()) - ) - }), + http.get( + mswEndpoint('/api/v1/workspaces/:workspace_name/messages'), + buildFilterablePaginatedMessagesHandler() + ), http.get(mswEndpoint('/api/v1/workspaces/:workspace_name/alerts'), () => { return HttpResponse.json(mockedAlerts) }), + http.get( + mswEndpoint('/api/v1/workspaces/:workspace_name/alerts-summary'), + () => { + const response: AlertSummary = { + malicious_packages: 13, + pii: 9, + secrets: 10, + total_alerts: 32, + } + return HttpResponse.json(response) + } + ), http.get(mswEndpoint('/api/v1/workspaces'), () => { return HttpResponse.json(mockedWorkspaces) }), diff --git a/src/mocks/msw/mockers/conversation-summary.mock.ts b/src/mocks/msw/mockers/conversation-summary.mock.ts new file mode 100644 index 00000000..3f4f471b --- /dev/null +++ b/src/mocks/msw/mockers/conversation-summary.mock.ts @@ -0,0 +1,42 @@ +import { + AlertSummary, + ConversationSummary, + QuestionType, +} from '@/api/generated' +import { faker } from '@faker-js/faker' +import { TOKEN_USAGE_AGG } from './token-usage.mock' + +export function mockConversationSummary( + { + type = QuestionType.CHAT, + withTokenUsage = true, + alertsSummary, + }: { + type?: QuestionType + withTokenUsage?: boolean + alertsSummary: AlertSummary + } = { + alertsSummary: { + malicious_packages: 0, + pii: 0, + secrets: 0, + total_alerts: 0, + }, + } +): ConversationSummary { + const timestamp = faker.date.recent().toISOString() + + return { + provider: 'vllm', + alerts_summary: alertsSummary, + prompt: { + message: faker.lorem.words(5), + message_id: faker.string.uuid(), + timestamp, + }, + token_usage_agg: withTokenUsage ? TOKEN_USAGE_AGG : null, + type, + chat_id: faker.string.uuid(), // NOTE: This isn't a UUID in the API + conversation_timestamp: timestamp, + } +} diff --git a/src/mocks/msw/mockers/conversation.mock.ts b/src/mocks/msw/mockers/conversation.mock.ts index 88879b08..972eebcd 100644 --- a/src/mocks/msw/mockers/conversation.mock.ts +++ b/src/mocks/msw/mockers/conversation.mock.ts @@ -14,7 +14,7 @@ export function mockConversation({ numAlerts?: number type?: 'secret' | 'malicious' | 'any' | 'pii' } -} = {}) { +} = {}): Conversation { const timestamp = faker.date.recent().toISOString() return { @@ -42,7 +42,7 @@ export function mockConversation({ mockAlert({ type: alertsConfig?.type == null || alertsConfig.type === 'any' - ? faker.helpers.arrayElement(['secret', 'malicious']) + ? faker.helpers.arrayElement(['secret', 'malicious', 'pii']) : alertsConfig.type, }) ), @@ -50,5 +50,5 @@ export function mockConversation({ type, chat_id: faker.string.uuid(), // NOTE: This isn't a UUID in the API conversation_timestamp: timestamp, - } as const satisfies Conversation + } } diff --git a/src/mocks/msw/mockers/paginated-messages-response.mock.ts b/src/mocks/msw/mockers/paginated-messages-response.mock.ts new file mode 100644 index 00000000..a974e4f6 --- /dev/null +++ b/src/mocks/msw/mockers/paginated-messages-response.mock.ts @@ -0,0 +1,111 @@ +import { + PaginatedMessagesResponse, + AlertTriggerType, + ConversationSummary, +} from '@/api/generated' +import { mockConversationSummary } from './conversation-summary.mock' +import { HttpResponse, HttpResponseResolver } from 'msw' + +export const buildFilterablePaginatedMessagesHandler = ( + { + include, + }: { + include: { + no_alerts?: boolean + [AlertTriggerType.CODEGATE_CONTEXT_RETRIEVER]?: boolean + [AlertTriggerType.CODEGATE_PII]?: boolean + [AlertTriggerType.CODEGATE_SECRETS]?: boolean + } + } = { + include: { + no_alerts: true, + [AlertTriggerType.CODEGATE_CONTEXT_RETRIEVER]: true, + [AlertTriggerType.CODEGATE_PII]: true, + [AlertTriggerType.CODEGATE_SECRETS]: true, + }, + } +): HttpResponseResolver => { + const noAlerts = Array.from({ length: 10 }).map(() => + mockConversationSummary({ + alertsSummary: { + secrets: 0, + total_alerts: 0, + malicious_packages: 0, + pii: 0, + }, + }) + ) + const secrets = Array.from({ length: 10 }).map(() => + mockConversationSummary({ + alertsSummary: { + secrets: 10, + total_alerts: 10, + malicious_packages: 0, + pii: 0, + }, + }) + ) + const malicious = Array.from({ length: 10 }).map(() => + mockConversationSummary({ + alertsSummary: { + secrets: 0, + total_alerts: 10, + malicious_packages: 10, + pii: 0, + }, + }) + ) + const pii = Array.from({ length: 10 }).map(() => + mockConversationSummary({ + alertsSummary: { + secrets: 0, + total_alerts: 10, + malicious_packages: 0, + pii: 10, + }, + }) + ) + + const results: ConversationSummary[] = [] + + if (include.no_alerts) { + results.push(...noAlerts) + } + if (include[AlertTriggerType.CODEGATE_SECRETS]) { + results.push(...secrets) + } + if (include[AlertTriggerType.CODEGATE_CONTEXT_RETRIEVER]) { + results.push(...malicious) + } + if (include[AlertTriggerType.CODEGATE_PII]) { + results.push(...pii) + } + + return ({ request }) => { + const url = new URL(request.url) + const trigger_type = url.searchParams.get( + 'filter_by_alert_trigger_types' + ) as AlertTriggerType + + const filteredResults = [...results].filter((r) => { + if (trigger_type === AlertTriggerType.CODEGATE_SECRETS) { + return r.alerts_summary.secrets > 0 + } + if (trigger_type === AlertTriggerType.CODEGATE_CONTEXT_RETRIEVER) { + return r.alerts_summary.malicious_packages > 0 + } + if (trigger_type === AlertTriggerType.CODEGATE_PII) { + return r.alerts_summary.pii > 0 + } + }) + + const response: PaginatedMessagesResponse = { + data: filteredResults, + limit: 50, + offset: 0, + total: filteredResults.length, + } + + return HttpResponse.json(response) + } +} diff --git a/src/routes/__tests__/route-chat.test.tsx b/src/routes/__tests__/route-chat.test.tsx index 04413655..e85536d8 100644 --- a/src/routes/__tests__/route-chat.test.tsx +++ b/src/routes/__tests__/route-chat.test.tsx @@ -10,8 +10,7 @@ import { getConversationTitle } from '@/features/dashboard-messages/lib/get-conv 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' +import { mockAlert } from '@/mocks/msw/mockers/alert.mock' vi.mock('@stacklok/ui-kit', async (importOriginal) => { return { @@ -33,8 +32,9 @@ it('renders breadcrumbs', async () => { const conversation = mockConversation() server.use( - http.get(mswEndpoint('/api/v1/workspaces/:workspace_name/messages'), () => - HttpResponse.json([conversation]) + http.get( + mswEndpoint('/api/v1/workspaces/:workspace_name/messages/:prompt_id'), + () => HttpResponse.json(conversation) ) ) @@ -59,8 +59,9 @@ it('renders title', async () => { const conversation = mockConversation() server.use( - http.get(mswEndpoint('/api/v1/workspaces/:workspace_name/messages'), () => - HttpResponse.json([conversation]) + http.get( + mswEndpoint('/api/v1/workspaces/:workspace_name/messages/:prompt_id'), + () => HttpResponse.json(conversation) ) ) @@ -86,12 +87,16 @@ it('renders title', async () => { 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 + conversation.alerts = [ + ...Array.from({ length: 5 }).map(() => mockAlert({ type: 'malicious' })), + ...Array.from({ length: 5 }).map(() => mockAlert({ type: 'secret' })), + ...Array.from({ length: 5 }).map(() => mockAlert({ type: 'pii' })), + ] server.use( - http.get(mswEndpoint('/api/v1/workspaces/:workspace_name/messages'), () => - HttpResponse.json([conversation]) + http.get( + mswEndpoint('/api/v1/workspaces/:workspace_name/messages/:prompt_id'), + () => HttpResponse.json(conversation) ) ) @@ -120,22 +125,21 @@ it('renders conversation summary correctly', async () => { expect(getByText(conversation.chat_id)).toBeVisible() - expect( - getByText(`${maliciousCount} malicious packages detected`) - ).toBeVisible() - - expect(getByText(`${secretsCount} secrets detected`)).toBeVisible() + expect(getByText(`5 malicious packages detected`)).toBeVisible() + expect(getByText(`5 secrets detected`)).toBeVisible() + expect(getByText(`5 PII 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 + 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]) + http.get( + mswEndpoint('/api/v1/workspaces/:workspace_name/messages/:prompt_id'), + () => HttpResponse.json(conversation) ) ) @@ -150,8 +154,8 @@ it('renders chat correctly', async () => { const { getByText } = within( screen.getByLabelText('Conversation transcript') ) - expect(getByText(question)).toBeVisible() - expect(getByText(answer)).toBeVisible() + expect(getByText(question as string)).toBeVisible() + expect(getByText(answer as string)).toBeVisible() }) }) @@ -159,8 +163,9 @@ it('renders tabs', async () => { const conversation = mockConversation() server.use( - http.get(mswEndpoint('/api/v1/workspaces/:workspace_name/messages'), () => - HttpResponse.json([conversation]) + http.get( + mswEndpoint('/api/v1/workspaces/:workspace_name/messages/:prompt_id'), + () => HttpResponse.json(conversation) ) ) @@ -181,8 +186,9 @@ it('can navigate using tabs', async () => { const conversation = mockConversation() server.use( - http.get(mswEndpoint('/api/v1/workspaces/:workspace_name/messages'), () => - HttpResponse.json([conversation]) + http.get( + mswEndpoint('/api/v1/workspaces/:workspace_name/messages/:prompt_id'), + () => HttpResponse.json(conversation) ) ) diff --git a/src/routes/__tests__/route-dashboard.test.tsx b/src/routes/__tests__/route-dashboard.test.tsx index 06b65527..06656c13 100644 --- a/src/routes/__tests__/route-dashboard.test.tsx +++ b/src/routes/__tests__/route-dashboard.test.tsx @@ -2,14 +2,8 @@ import { render } from '@/lib/test-utils' import { screen, waitFor, within } from '@testing-library/react' import { expect, it } from 'vitest' -import { server } from '@/mocks/msw/node' -import { HttpResponse, http } from 'msw' import userEvent from '@testing-library/user-event' import { RouteDashboard } from '../route-dashboard' -import { mswEndpoint } from '@/test/msw-endpoint' - -import { mockConversation } from '@/mocks/msw/mockers/conversation.mock' -import { faker } from '@faker-js/faker' it('should mount alert summaries', async () => { render() @@ -38,31 +32,12 @@ it('should render messages table', async () => { }) 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() 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') - await userEvent.click( screen.getByRole('tab', { name: /secrets/i, @@ -81,35 +56,16 @@ it('shows only conversations with secrets when you click on the secrets tab', as }) }) -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' }, - }) - ), - ]) - }) - ) +it('shows only conversations with pii when you click on the secrets tab', async () => { render() await waitFor(() => { expect(screen.queryByText(/loading.../i)).not.toBeInTheDocument() }) - expect(screen.getByTestId(/tab-all-count/i)).toHaveTextContent('20') - expect(screen.getByTestId(/tab-malicious-count/i)).toHaveTextContent('10') - await userEvent.click( screen.getByRole('tab', { - name: /malicious/i, + name: /pii/i, }) ) @@ -117,7 +73,7 @@ it('shows only conversations with malicious when you click on the malicious tab' await waitFor(() => { const secretsCountButtons = within(tbody).getAllByRole('button', { - name: /malicious packages count/, + name: /personally identifiable information \(PII\) count/, }) as HTMLElement[] secretsCountButtons.forEach((e) => { expect(e).toHaveTextContent('10') @@ -125,69 +81,27 @@ it('shows only conversations with malicious when you click on the malicious tab' }) }) -it('should render searchbox', async () => { - render() - - expect( - screen.getByRole('searchbox', { - name: /search messages/i, - }) - ).toBeVisible() -}) - -it('can filter using searchbox', async () => { - const STRING_TO_FILTER_BY = 'foo-bar-my-awesome-string.com' - - // 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() - - 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, - ]) - }) - ) +it('shows only conversations with malicious when you click on the malicious tab', async () => { render() await waitFor(() => { expect(screen.queryByText(/loading.../i)).not.toBeInTheDocument() }) - expect(screen.queryByText(STRING_TO_FILTER_BY)).not.toBeInTheDocument() - - await userEvent.type( - screen.getByRole('searchbox', { name: /search messages/i }), - STRING_TO_FILTER_BY + await userEvent.click( + screen.getByRole('tab', { + name: /malicious/i, + }) ) - expect( - within(screen.getByRole('grid')).queryByText(STRING_TO_FILTER_BY) - ).toBeVisible() -}) - -it('should sort messages by date desc', async () => { - render() + const tbody = screen.getAllByRole('rowgroup')[1] as HTMLElement await waitFor(() => { - expect(screen.queryByText(/loading.../i)).not.toBeInTheDocument() + const secretsCountButtons = within(tbody).getAllByRole('button', { + name: /malicious packages count/, + }) as HTMLElement[] + secretsCountButtons.forEach((e) => { + expect(e).toHaveTextContent('10') + }) }) - - 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(oldest > newest).toBe(false) }) diff --git a/src/routes/route-chat.tsx b/src/routes/route-chat.tsx index b6489af7..9885e512 100644 --- a/src/routes/route-chat.tsx +++ b/src/routes/route-chat.tsx @@ -12,10 +12,10 @@ import { TabsConversation } from '@/features/dashboard-messages/components/tabs- 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' +import { useQueryGetWorkspaceMessageById } from '@/features/dashboard-messages/hooks/use-query-get-workspace-message-by-id' function ConversationContent({ view, @@ -47,7 +47,9 @@ export function RouteChat() { const { id } = useParams<'id'>() const { state } = useConversationSearchParams() - const { data: conversation, isLoading } = useConversationById(id ?? '') + const { data: conversation, isLoading } = useQueryGetWorkspaceMessageById({ + id: id ?? '', + }) const title = conversation === undefined || diff --git a/src/routes/route-dashboard.tsx b/src/routes/route-dashboard.tsx index 26ef7a76..63287005 100644 --- a/src/routes/route-dashboard.tsx +++ b/src/routes/route-dashboard.tsx @@ -4,13 +4,15 @@ import { AlertsSummaryWorkspaceTokenUsage } from '@/features/dashboard-alerts/co 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' +import { AlertsSummaryPii } from '@/features/dashboard-alerts/components/alerts-summary-pii' export function RouteDashboard() { return ( -
+
+