From 4161f37ec95d6e32e9a2f4f1e4820d198d313708 Mon Sep 17 00:00:00 2001 From: alex-mcgovern Date: Thu, 6 Feb 2025 14:38:04 +0000 Subject: [PATCH 01/19] revert: openapi-ts upgrade --- package-lock.json | 18 +++++++++--------- package.json | 2 +- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/package-lock.json b/package-lock.json index 6f178141..d94c609d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -40,7 +40,7 @@ "devDependencies": { "@eslint/js": "^9.15.0", "@faker-js/faker": "^9.4.0", - "@hey-api/openapi-ts": "^0.64.1", + "@hey-api/openapi-ts": "^0.61.2", "@tailwindcss/typography": "^0.5.16", "@testing-library/dom": "^10.4.0", "@testing-library/jest-dom": "^6.6.3", @@ -1062,9 +1062,9 @@ } }, "node_modules/@hey-api/json-schema-ref-parser": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@hey-api/json-schema-ref-parser/-/json-schema-ref-parser-1.0.2.tgz", - "integrity": "sha512-F6LSkttZcT/XiX3ydeDqTY3uRN3BLJMwyMTk4kg/ichZlKUp3+3Odv0WokSmXGSoZGTW/N66FROMYAm5NPdJlA==", + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@hey-api/json-schema-ref-parser/-/json-schema-ref-parser-1.0.1.tgz", + "integrity": "sha512-dBt0A7op9kf4BcK++x6HBYDmvCvnJUZEGe5QytghPFHnMXPyKwDKomwL/v5e9ERk6E0e1GzL/e/y6pWUso9zrQ==", "dev": true, "license": "MIT", "dependencies": { @@ -1080,13 +1080,13 @@ } }, "node_modules/@hey-api/openapi-ts": { - "version": "0.64.1", - "resolved": "https://registry.npmjs.org/@hey-api/openapi-ts/-/openapi-ts-0.64.1.tgz", - "integrity": "sha512-d9FWyJKjyH4KeCjOoR5x8+zKER7h7EIRzk3Xo5a2ShAxNoq9HlAArLbWgJFnH7QypQ+f3q29mTp/z2Zw0XkP9g==", + "version": "0.61.3", + "resolved": "https://registry.npmjs.org/@hey-api/openapi-ts/-/openapi-ts-0.61.3.tgz", + "integrity": "sha512-Ls9MBRa5+vg7UHw6fIcfdgcCyZ9vKtRw63nWxwF9zjJIPlzVOZO6xKuzGmDc6o0Pb6XCdTz6lPV5hcV0R4b/ag==", "dev": true, "license": "MIT", "dependencies": { - "@hey-api/json-schema-ref-parser": "1.0.2", + "@hey-api/json-schema-ref-parser": "1.0.1", "c12": "2.0.1", "commander": "13.0.0", "handlebars": "4.7.8" @@ -1095,7 +1095,7 @@ "openapi-ts": "bin/index.cjs" }, "engines": { - "node": "^18.18.0 || ^20.9.0 || >=22.11.0" + "node": "^18.20.5 || ^20.11.1 || >=22.11.0" }, "funding": { "url": "https://github.com/sponsors/hey-api" diff --git a/package.json b/package.json index 5ec8c102..753c5796 100644 --- a/package.json +++ b/package.json @@ -52,7 +52,7 @@ "devDependencies": { "@eslint/js": "^9.15.0", "@faker-js/faker": "^9.4.0", - "@hey-api/openapi-ts": "^0.64.1", + "@hey-api/openapi-ts": "^0.61.2", "@tailwindcss/typography": "^0.5.16", "@testing-library/dom": "^10.4.0", "@testing-library/jest-dom": "^6.6.3", From 253750b13acccd90fbe981fe88d050605c2f1398 Mon Sep 17 00:00:00 2001 From: alex-mcgovern Date: Thu, 6 Feb 2025 16:38:12 +0000 Subject: [PATCH 02/19] feat: show all alerts in dashboard table --- src/api/generated/types.gen.ts | 19 ++ src/api/openapi.json | 322 +++++++----------- .../alerts/components/table-alerts.tsx | 174 ++++++---- .../alerts/components/tabs-alerts.tsx | 21 +- ...uery-get-workspace-alerts-malicious-pkg.ts | 4 +- .../use-query-get-workspace-alerts-secrets.ts | 4 +- .../use-query-get-workspace-alerts-table.ts | 32 +- .../lib/__tests__/is-alert-malicious.test.ts | 10 +- .../lib/__tests__/is-alert-secret.test.ts | 10 +- .../alerts/lib/does-alert-include-search.ts | 37 ++ src/features/alerts/lib/is-alert-malicious.ts | 21 +- src/features/alerts/lib/is-alert-secret.ts | 24 +- src/hooks/use-query-get-workspace-messages.ts | 17 +- src/lib/utils.ts | 14 +- 14 files changed, 379 insertions(+), 330 deletions(-) diff --git a/src/api/generated/types.gen.ts b/src/api/generated/types.gen.ts index 2a202061..f09bf3e5 100644 --- a/src/api/generated/types.gen.ts +++ b/src/api/generated/types.gen.ts @@ -23,6 +23,24 @@ export type AddProviderEndpointRequest = { api_key?: string | null; }; +/** + * Represents an alert. + */ +export type Alert = { + id: string; + prompt_id: string; + code_snippet: CodeSnippet | null; + trigger_string: + | string + | { + [key: string]: unknown; + } + | null; + trigger_type: string; + trigger_category: string | null; + timestamp: string; +}; + /** * Represents an alert with it's respective conversation. */ @@ -75,6 +93,7 @@ export type Conversation = { chat_id: string; conversation_timestamp: string; token_usage_agg: TokenUsageAggregate | null; + alerts?: Array; }; export type CreateOrRenameWorkspaceRequest = { diff --git a/src/api/openapi.json b/src/api/openapi.json index 6536af0f..7ff5ba98 100644 --- a/src/api/openapi.json +++ b/src/api/openapi.json @@ -8,9 +8,7 @@ "paths": { "/health": { "get": { - "tags": [ - "System" - ], + "tags": ["System"], "summary": "Health Check", "operationId": "health_check_health_get", "responses": { @@ -27,10 +25,7 @@ }, "/api/v1/provider-endpoints": { "get": { - "tags": [ - "CodeGate API", - "Providers" - ], + "tags": ["CodeGate API", "Providers"], "summary": "List Provider Endpoints", "description": "List all provider endpoints.", "operationId": "v1_list_provider_endpoints", @@ -80,10 +75,7 @@ } }, "post": { - "tags": [ - "CodeGate API", - "Providers" - ], + "tags": ["CodeGate API", "Providers"], "summary": "Add Provider Endpoint", "description": "Add a provider endpoint.", "operationId": "v1_add_provider_endpoint", @@ -123,10 +115,7 @@ }, "/api/v1/provider-endpoints/models": { "get": { - "tags": [ - "CodeGate API", - "Providers" - ], + "tags": ["CodeGate API", "Providers"], "summary": "List All Models For All Providers", "description": "List all models for all providers.", "operationId": "v1_list_all_models_for_all_providers", @@ -150,10 +139,7 @@ }, "/api/v1/provider-endpoints/{provider_id}/models": { "get": { - "tags": [ - "CodeGate API", - "Providers" - ], + "tags": ["CodeGate API", "Providers"], "summary": "List Models By Provider", "description": "List models by provider.", "operationId": "v1_list_models_by_provider", @@ -199,10 +185,7 @@ }, "/api/v1/provider-endpoints/{provider_id}": { "get": { - "tags": [ - "CodeGate API", - "Providers" - ], + "tags": ["CodeGate API", "Providers"], "summary": "Get Provider Endpoint", "description": "Get a provider endpoint by ID.", "operationId": "v1_get_provider_endpoint", @@ -242,10 +225,7 @@ } }, "put": { - "tags": [ - "CodeGate API", - "Providers" - ], + "tags": ["CodeGate API", "Providers"], "summary": "Update Provider Endpoint", "description": "Update a provider endpoint by ID.", "operationId": "v1_update_provider_endpoint", @@ -295,10 +275,7 @@ } }, "delete": { - "tags": [ - "CodeGate API", - "Providers" - ], + "tags": ["CodeGate API", "Providers"], "summary": "Delete Provider Endpoint", "description": "Delete a provider endpoint by id.", "operationId": "v1_delete_provider_endpoint", @@ -338,10 +315,7 @@ }, "/api/v1/provider-endpoints/{provider_id}/auth-material": { "put": { - "tags": [ - "CodeGate API", - "Providers" - ], + "tags": ["CodeGate API", "Providers"], "summary": "Configure Auth Material", "description": "Configure auth material for a provider.", "operationId": "v1_configure_auth_material", @@ -386,10 +360,7 @@ }, "/api/v1/workspaces": { "get": { - "tags": [ - "CodeGate API", - "Workspaces" - ], + "tags": ["CodeGate API", "Workspaces"], "summary": "List Workspaces", "description": "List all workspaces.", "operationId": "v1_list_workspaces", @@ -407,10 +378,7 @@ } }, "post": { - "tags": [ - "CodeGate API", - "Workspaces" - ], + "tags": ["CodeGate API", "Workspaces"], "summary": "Create Workspace", "description": "Create a new workspace.", "operationId": "v1_create_workspace", @@ -450,10 +418,7 @@ }, "/api/v1/workspaces/active": { "get": { - "tags": [ - "CodeGate API", - "Workspaces" - ], + "tags": ["CodeGate API", "Workspaces"], "summary": "List Active Workspaces", "description": "List all active workspaces.\n\nIn it's current form, this function will only return one workspace. That is,\nthe globally active workspace.", "operationId": "v1_list_active_workspaces", @@ -471,10 +436,7 @@ } }, "post": { - "tags": [ - "CodeGate API", - "Workspaces" - ], + "tags": ["CodeGate API", "Workspaces"], "summary": "Activate Workspace", "description": "Activate a workspace by name.", "operationId": "v1_activate_workspace", @@ -523,10 +485,7 @@ }, "/api/v1/workspaces/{workspace_name}": { "delete": { - "tags": [ - "CodeGate API", - "Workspaces" - ], + "tags": ["CodeGate API", "Workspaces"], "summary": "Delete Workspace", "description": "Delete a workspace by name.", "operationId": "v1_delete_workspace", @@ -565,10 +524,7 @@ }, "/api/v1/workspaces/archive": { "get": { - "tags": [ - "CodeGate API", - "Workspaces" - ], + "tags": ["CodeGate API", "Workspaces"], "summary": "List Archived Workspaces", "description": "List all archived workspaces.", "operationId": "v1_list_archived_workspaces", @@ -588,10 +544,7 @@ }, "/api/v1/workspaces/archive/{workspace_name}/recover": { "post": { - "tags": [ - "CodeGate API", - "Workspaces" - ], + "tags": ["CodeGate API", "Workspaces"], "summary": "Recover Workspace", "description": "Recover an archived workspace by name.", "operationId": "v1_recover_workspace", @@ -625,10 +578,7 @@ }, "/api/v1/workspaces/archive/{workspace_name}": { "delete": { - "tags": [ - "CodeGate API", - "Workspaces" - ], + "tags": ["CodeGate API", "Workspaces"], "summary": "Hard Delete Workspace", "description": "Hard delete an archived workspace by name.", "operationId": "v1_hard_delete_workspace", @@ -667,10 +617,7 @@ }, "/api/v1/workspaces/{workspace_name}/alerts": { "get": { - "tags": [ - "CodeGate API", - "Workspaces" - ], + "tags": ["CodeGate API", "Workspaces"], "summary": "Get Workspace Alerts", "description": "Get alerts for a workspace.", "operationId": "v1_get_workspace_alerts", @@ -722,10 +669,7 @@ }, "/api/v1/workspaces/{workspace_name}/messages": { "get": { - "tags": [ - "CodeGate API", - "Workspaces" - ], + "tags": ["CodeGate API", "Workspaces"], "summary": "Get Workspace Messages", "description": "Get messages for a workspace.", "operationId": "v1_get_workspace_messages", @@ -770,10 +714,7 @@ }, "/api/v1/workspaces/{workspace_name}/custom-instructions": { "get": { - "tags": [ - "CodeGate API", - "Workspaces" - ], + "tags": ["CodeGate API", "Workspaces"], "summary": "Get Workspace Custom Instructions", "description": "Get the custom instructions of a workspace.", "operationId": "v1_get_workspace_custom_instructions", @@ -812,10 +753,7 @@ } }, "put": { - "tags": [ - "CodeGate API", - "Workspaces" - ], + "tags": ["CodeGate API", "Workspaces"], "summary": "Set Workspace Custom Instructions", "operationId": "v1_set_workspace_custom_instructions", "parameters": [ @@ -856,10 +794,7 @@ } }, "delete": { - "tags": [ - "CodeGate API", - "Workspaces" - ], + "tags": ["CodeGate API", "Workspaces"], "summary": "Delete Workspace Custom Instructions", "operationId": "v1_delete_workspace_custom_instructions", "parameters": [ @@ -892,11 +827,7 @@ }, "/api/v1/workspaces/{workspace_name}/muxes": { "get": { - "tags": [ - "CodeGate API", - "Workspaces", - "Muxes" - ], + "tags": ["CodeGate API", "Workspaces", "Muxes"], "summary": "Get Workspace Muxes", "description": "Get the mux rules of a workspace.\n\nThe list is ordered in order of priority. That is, the first rule in the list\nhas the highest priority.", "operationId": "v1_get_workspace_muxes", @@ -939,11 +870,7 @@ } }, "put": { - "tags": [ - "CodeGate API", - "Workspaces", - "Muxes" - ], + "tags": ["CodeGate API", "Workspaces", "Muxes"], "summary": "Set Workspace Muxes", "description": "Set the mux rules of a workspace.", "operationId": "v1_set_workspace_muxes", @@ -991,10 +918,7 @@ }, "/api/v1/alerts_notification": { "get": { - "tags": [ - "CodeGate API", - "Dashboard" - ], + "tags": ["CodeGate API", "Dashboard"], "summary": "Stream Sse", "description": "Send alerts event", "operationId": "v1_stream_sse", @@ -1012,10 +936,7 @@ }, "/api/v1/version": { "get": { - "tags": [ - "CodeGate API", - "Dashboard" - ], + "tags": ["CodeGate API", "Dashboard"], "summary": "Version Check", "operationId": "v1_version_check", "responses": { @@ -1032,11 +953,7 @@ }, "/api/v1/workspaces/{workspace_name}/token-usage": { "get": { - "tags": [ - "CodeGate API", - "Workspaces", - "Token Usage" - ], + "tags": ["CodeGate API", "Workspaces", "Token Usage"], "summary": "Get Workspace Token Usage", "description": "Get the token usage of a workspace.", "operationId": "v1_get_workspace_token_usage", @@ -1086,9 +1003,7 @@ } }, "type": "object", - "required": [ - "name" - ], + "required": ["name"], "title": "ActivateWorkspaceRequest" }, "ActiveWorkspace": { @@ -1106,11 +1021,7 @@ } }, "type": "object", - "required": [ - "name", - "is_active", - "last_updated" - ], + "required": ["name", "is_active", "last_updated"], "title": "ActiveWorkspace" }, "AddProviderEndpointRequest": { @@ -1168,13 +1079,78 @@ } }, "type": "object", - "required": [ - "name", - "provider_type" - ], + "required": ["name", "provider_type"], "title": "AddProviderEndpointRequest", "description": "Represents a request to add a provider endpoint." }, + "Alert": { + "properties": { + "id": { + "type": "string", + "title": "Id" + }, + "prompt_id": { + "type": "string", + "title": "Prompt Id" + }, + "code_snippet": { + "anyOf": [ + { + "$ref": "#/components/schemas/CodeSnippet" + }, + { + "type": "null" + } + ] + }, + "trigger_string": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "object" + }, + { + "type": "null" + } + ], + "title": "Trigger String" + }, + "trigger_type": { + "type": "string", + "title": "Trigger Type" + }, + "trigger_category": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Trigger Category" + }, + "timestamp": { + "type": "string", + "format": "date-time", + "title": "Timestamp" + } + }, + "type": "object", + "required": [ + "id", + "prompt_id", + "code_snippet", + "trigger_string", + "trigger_type", + "trigger_category", + "timestamp" + ], + "title": "Alert", + "description": "Represents an alert." + }, "AlertConversation": { "properties": { "conversation": { @@ -1259,11 +1235,7 @@ } }, "type": "object", - "required": [ - "message", - "timestamp", - "message_id" - ], + "required": ["message", "timestamp", "message_id"], "title": "ChatMessage", "description": "Represents a chat message." }, @@ -1304,11 +1276,7 @@ } }, "type": "object", - "required": [ - "code", - "language", - "filepath" - ], + "required": ["code", "language", "filepath"], "title": "CodeSnippet" }, "ConfigureAuthMaterial": { @@ -1329,9 +1297,7 @@ } }, "type": "object", - "required": [ - "auth_type" - ], + "required": ["auth_type"], "title": "ConfigureAuthMaterial", "description": "Represents a request to configure auth material for a provider." }, @@ -1376,6 +1342,14 @@ "type": "null" } ] + }, + "alerts": { + "items": { + "$ref": "#/components/schemas/Alert" + }, + "type": "array", + "title": "Alerts", + "default": [] } }, "type": "object", @@ -1409,9 +1383,7 @@ } }, "type": "object", - "required": [ - "name" - ], + "required": ["name"], "title": "CreateOrRenameWorkspaceRequest" }, "CustomInstructions": { @@ -1422,9 +1394,7 @@ } }, "type": "object", - "required": [ - "prompt" - ], + "required": ["prompt"], "title": "CustomInstructions" }, "HTTPValidationError": { @@ -1451,9 +1421,7 @@ } }, "type": "object", - "required": [ - "workspaces" - ], + "required": ["workspaces"], "title": "ListActiveWorkspacesResponse" }, "ListWorkspacesResponse": { @@ -1467,9 +1435,7 @@ } }, "type": "object", - "required": [ - "workspaces" - ], + "required": ["workspaces"], "title": "ListWorkspacesResponse" }, "ModelByProvider": { @@ -1488,19 +1454,13 @@ } }, "type": "object", - "required": [ - "name", - "provider_id", - "provider_name" - ], + "required": ["name", "provider_id", "provider_name"], "title": "ModelByProvider", "description": "Represents a model supported by a provider.\n\nNote that these are auto-discovered by the provider." }, "MuxMatcherType": { "type": "string", - "enum": [ - "catch_all" - ], + "enum": ["catch_all"], "title": "MuxMatcherType", "description": "Represents the different types of matchers we support." }, @@ -1530,21 +1490,13 @@ } }, "type": "object", - "required": [ - "provider_id", - "model", - "matcher_type" - ], + "required": ["provider_id", "model", "matcher_type"], "title": "MuxRule", "description": "Represents a mux rule for a provider." }, "ProviderAuthType": { "type": "string", - "enum": [ - "none", - "passthrough", - "api_key" - ], + "enum": ["none", "passthrough", "api_key"], "title": "ProviderAuthType", "description": "Represents the different types of auth we support for providers." }, @@ -1592,10 +1544,7 @@ } }, "type": "object", - "required": [ - "name", - "provider_type" - ], + "required": ["name", "provider_type"], "title": "ProviderEndpoint", "description": "Represents a provider's endpoint configuration. This\nallows us to persist the configuration for each provider,\nso we can use this for muxing messages." }, @@ -1607,7 +1556,8 @@ "vllm", "ollama", "lm_studio", - "llamacpp" + "llamacpp", + "openai" ], "title": "ProviderType", "description": "Represents the different types of providers we support." @@ -1629,19 +1579,13 @@ } }, "type": "object", - "required": [ - "question", - "answer" - ], + "required": ["question", "answer"], "title": "QuestionAnswer", "description": "Represents a question and answer pair." }, "QuestionType": { "type": "string", - "enum": [ - "chat", - "fim" - ], + "enum": ["chat", "fim"], "title": "QuestionType" }, "TokenUsage": { @@ -1685,10 +1629,7 @@ } }, "type": "object", - "required": [ - "tokens_by_model", - "token_usage" - ], + "required": ["tokens_by_model", "token_usage"], "title": "TokenUsageAggregate", "description": "Represents the tokens used. Includes the information of the tokens used by model.\n`used_tokens` are the total tokens used in the `tokens_by_model` list." }, @@ -1706,11 +1647,7 @@ } }, "type": "object", - "required": [ - "provider_type", - "model", - "token_usage" - ], + "required": ["provider_type", "model", "token_usage"], "title": "TokenUsageByModel", "description": "Represents the tokens used by a model." }, @@ -1740,11 +1677,7 @@ } }, "type": "object", - "required": [ - "loc", - "msg", - "type" - ], + "required": ["loc", "msg", "type"], "title": "ValidationError" }, "Workspace": { @@ -1759,10 +1692,7 @@ } }, "type": "object", - "required": [ - "name", - "is_active" - ], + "required": ["name", "is_active"], "title": "Workspace" } } diff --git a/src/features/alerts/components/table-alerts.tsx b/src/features/alerts/components/table-alerts.tsx index df58e066..cc2f32f4 100644 --- a/src/features/alerts/components/table-alerts.tsx +++ b/src/features/alerts/components/table-alerts.tsx @@ -8,40 +8,32 @@ import { TableHeader, Button, ResizableTableContainer, + Tooltip, + TooltipTrigger, } from "@stacklok/ui-kit"; -import { AlertConversation, QuestionType } from "@/api/generated"; -import { - sanitizeQuestionPrompt, - parsingPromptText, - getIssueDetectedType, -} from "@/lib/utils"; +import { Alert, Conversation, QuestionType } from "@/api/generated"; + import { useClientSidePagination } from "@/hooks/useClientSidePagination"; import { TableAlertTokenUsage } from "./table-alert-token-usage"; -import { useQueryGetWorkspaceAlertTable } from "../hooks/use-query-get-workspace-alerts-table"; import { useAlertsFilterSearchParams } from "../hooks/use-alerts-filter-search-params"; import { Key01, PackageX } from "@untitled-ui/icons-react"; import { TableAlertsEmptyState } from "./table-alerts-empty-state"; import { ComponentProps } from "react"; import { hrefs } from "@/lib/hrefs"; - -const getTitle = (alert: AlertConversation) => { - const prompt = alert.conversation; - const title = parsingPromptText( - sanitizeQuestionPrompt({ - question: prompt.question_answers?.[0]?.question.message ?? "", - answer: prompt.question_answers?.[0]?.answer?.message ?? "", - }), - prompt.conversation_timestamp, - ); - - return title; +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-alerts-table"; + +const getPromptText = (conversation: Conversation) => { + return (conversation.question_answers[0]?.question?.message ?? "N/A") + .trim() + .slice(0, 200); // arbitrary slice to prevent long prompts }; -function TypeCellContent({ alert }: { alert: AlertConversation }) { - const conversationType = alert.conversation.type; - - switch (conversationType) { +function getTypeText(type: QuestionType) { + switch (type) { case QuestionType.CHAT: return "Chat"; case QuestionType.FIM: @@ -51,30 +43,74 @@ function TypeCellContent({ alert }: { alert: AlertConversation }) { } } -function IssueDetectedCellContent({ alert }: { alert: AlertConversation }) { - const issueDetected = getIssueDetectedType(alert); +function countAlerts(alerts: Alert[]): { + secrets: number; + malicious: number; +} { + return { + secrets: alerts.filter(isAlertSecret).length, + malicious: alerts.filter(isAlertMalicious).length, + }; +} - switch (issueDetected) { - case "leaked_secret": - return ( - <> - - Blocked secret exposure - - ); - case "malicious_package": - return ( - <> - - Blocked malicious package - - ); - default: - return ""; - } +function AlertsSummaryCount({ + count, + icon: Icon, + strings, +}: { + count: number; + icon: (props: React.SVGProps) => React.JSX.Element; + strings: { + singular: string; + plural: string; + }; +}) { + const tooltipText = `${count} ${count === 1 ? strings.singular : strings.plural} detected`; + + return ( + + + {tooltipText} + + ); +} + +function AlertsSummaryCellContent({ alerts }: { alerts: Alert[] }) { + const { malicious, secrets } = countAlerts(alerts); + + return ( +
+ + +
+ ); } -type ColumnId = "time" | "type" | "event" | "issue_detected" | "token_usage"; +type ColumnId = "time" | "type" | "prompt" | "alerts" | "token_usage"; type Column = { id: ColumnId } & Omit, "id">; @@ -83,21 +119,23 @@ const COLUMNS: Column[] = [ id: "time", isRowHeader: true, children: "Time", - width: 200, + minWidth: 170, + maxWidth: 180, }, { id: "type", children: "Type", - width: 150, + minWidth: 170, + maxWidth: 190, }, { - id: "event", - children: "Event", + id: "prompt", + children: "Prompt", }, { - id: "issue_detected", - children: "Issue detected", - width: 325, + id: "alerts", + children: "Alerts", + width: 110, }, { id: "token_usage", @@ -106,34 +144,24 @@ const COLUMNS: Column[] = [ }, ]; -function CellRenderer({ - column, - row, -}: { - column: Column; - row: AlertConversation; -}) { +function CellRenderer({ column, row }: { column: Column; row: Conversation }) { switch (column.id) { case "time": return ( - {formatDistanceToNow(new Date(row.timestamp), { + {formatDistanceToNow(new Date(row.conversation_timestamp), { addSuffix: true, })} ); case "type": - return ; - case "event": - return getTitle(row); - case "issue_detected": - return ( -
- -
- ); + return getTypeText(row.type); + case "prompt": + return getPromptText(row); + case "alerts": + return ; case "token_usage": - return ; + return ; default: return column.id satisfies never; @@ -143,7 +171,7 @@ function CellRenderer({ export function TableAlerts() { const { state, prevPage, nextPage } = useAlertsFilterSearchParams(); - const { data = [] } = useQueryGetWorkspaceAlertTable(); + const { data = [] } = useQueryGetWorkspaceMessagesTable(); const { dataView, hasNextPage, hasPreviousPage } = useClientSidePagination( data, @@ -165,12 +193,12 @@ export function TableAlerts() { {(row) => ( {(column) => ( diff --git a/src/features/alerts/components/tabs-alerts.tsx b/src/features/alerts/components/tabs-alerts.tsx index f5bf0523..a2ae5b2e 100644 --- a/src/features/alerts/components/tabs-alerts.tsx +++ b/src/features/alerts/components/tabs-alerts.tsx @@ -1,9 +1,7 @@ -import { useQueryGetWorkspaceAlerts } from "../hooks/use-query-get-workspace-alerts"; -import { isAlertMalicious } from "../lib/is-alert-malicious"; +import { isConversationWithMaliciousAlerts } from "../lib/is-alert-malicious"; import { multiFilter } from "@/lib/multi-filter"; -import { isAlertCritical } from "../lib/is-alert-critical"; -import { isAlertSecret } from "../lib/is-alert-secret"; -import { V1GetWorkspaceAlertsResponse } from "@/api/generated"; +import { isConversationWithSecretAlerts } from "../lib/is-alert-secret"; +import { V1GetWorkspaceMessagesResponse } from "@/api/generated"; import { Tab as BaseTab, Tabs, @@ -19,6 +17,7 @@ import { } from "../hooks/use-alerts-filter-search-params"; import { SearchFieldAlerts } from "./search-field-alerts"; import { tv } from "tailwind-variants"; +import { useQueryGetWorkspaceMessages } from "@/hooks/use-query-get-workspace-messages"; type AlertsCount = { all: number; @@ -26,17 +25,15 @@ type AlertsCount = { secrets: number; }; -function select(data: V1GetWorkspaceAlertsResponse | undefined): AlertsCount { - const all: number = multiFilter(data, [isAlertCritical]).length; +function select(data: V1GetWorkspaceMessagesResponse): AlertsCount { + const all: number = data?.length ?? 0; const malicious: number = multiFilter(data, [ - isAlertCritical, - isAlertMalicious, + isConversationWithMaliciousAlerts, ]).length; const secrets: number = multiFilter(data, [ - isAlertCritical, - isAlertSecret, + isConversationWithSecretAlerts, ]).length; return { @@ -81,7 +78,7 @@ function Tab({ export function TabsAlerts({ children }: { children: React.ReactNode }) { const { state, setView } = useAlertsFilterSearchParams(); - const { data } = useQueryGetWorkspaceAlerts({ + const { data } = useQueryGetWorkspaceMessages({ select, }); diff --git a/src/features/alerts/hooks/use-query-get-workspace-alerts-malicious-pkg.ts b/src/features/alerts/hooks/use-query-get-workspace-alerts-malicious-pkg.ts index d3c4e2ff..9baff9c7 100644 --- a/src/features/alerts/hooks/use-query-get-workspace-alerts-malicious-pkg.ts +++ b/src/features/alerts/hooks/use-query-get-workspace-alerts-malicious-pkg.ts @@ -1,5 +1,5 @@ import { V1GetWorkspaceAlertsResponse } from "@/api/generated"; -import { isAlertMalicious } from "../lib/is-alert-malicious"; +import { isAlertConversationMalicious } 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"; @@ -7,7 +7,7 @@ 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]); + return multiFilter(data, [isAlertCritical, isAlertConversationMalicious]); } export function useQueryGetWorkspaceAlertsMaliciousPkg() { diff --git a/src/features/alerts/hooks/use-query-get-workspace-alerts-secrets.ts b/src/features/alerts/hooks/use-query-get-workspace-alerts-secrets.ts index 5e617173..e01937b2 100644 --- a/src/features/alerts/hooks/use-query-get-workspace-alerts-secrets.ts +++ b/src/features/alerts/hooks/use-query-get-workspace-alerts-secrets.ts @@ -1,5 +1,5 @@ import { V1GetWorkspaceAlertsResponse } from "@/api/generated"; -import { isAlertSecret } from "../lib/is-alert-secret"; +import { isAlertConversationSecret } 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"; @@ -7,7 +7,7 @@ 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]); + return multiFilter(data, [isAlertCritical, isAlertConversationSecret]); } export function useQueryGetWorkspaceAlertSecrets() { diff --git a/src/features/alerts/hooks/use-query-get-workspace-alerts-table.ts b/src/features/alerts/hooks/use-query-get-workspace-alerts-table.ts index 42094205..9d34e26d 100644 --- a/src/features/alerts/hooks/use-query-get-workspace-alerts-table.ts +++ b/src/features/alerts/hooks/use-query-get-workspace-alerts-table.ts @@ -1,43 +1,39 @@ -import { V1GetWorkspaceAlertsResponse } from "@/api/generated"; -import { useQueryGetWorkspaceAlerts } from "./use-query-get-workspace-alerts"; +import { Conversation } from "@/api/generated"; import { useCallback } from "react"; import { AlertsFilterView, useAlertsFilterSearchParams, } from "./use-alerts-filter-search-params"; import { multiFilter } from "@/lib/multi-filter"; -import { isAlertCritical } from "../lib/is-alert-critical"; -import { isAlertMalicious } from "../lib/is-alert-malicious"; -import { isAlertSecret } from "../lib/is-alert-secret"; -import { doesAlertIncludeSearch } from "../lib/does-alert-include-search"; -import { isAlertConversation } from "../lib/is-alert-conversation"; +import { isConversationWithMaliciousAlerts } from "../lib/is-alert-malicious"; +import { isConversationWithSecretAlerts } from "../lib/is-alert-secret"; +import { doesConversationIncludeSearch } from "../lib/does-alert-include-search"; +import { useQueryGetWorkspaceMessages } from "@/hooks/use-query-get-workspace-messages"; const FILTER: Record< AlertsFilterView, - (alert: V1GetWorkspaceAlertsResponse[number]) => boolean + (alert: Conversation | null) => boolean > = { all: () => true, - malicious: isAlertMalicious, - secrets: isAlertSecret, + malicious: isConversationWithMaliciousAlerts, + secrets: isConversationWithSecretAlerts, }; -export function useQueryGetWorkspaceAlertTable() { +export function useQueryGetWorkspaceMessagesTable() { const { state } = useAlertsFilterSearchParams(); // NOTE: This needs to be a stable function reference to enable memo-isation // of the select operation on each React re-render. const select = useCallback( - (data: V1GetWorkspaceAlertsResponse) => { - return multiFilter(data, [ - isAlertCritical, - isAlertConversation, - FILTER[state.view], - ]).filter((alert) => doesAlertIncludeSearch(alert, state.search ?? null)); + (data: Conversation[]) => { + return multiFilter(data, [FILTER[state.view]]).filter((conversation) => + doesConversationIncludeSearch(conversation, state.search ?? null), + ); }, [state.search, state.view], ); - return useQueryGetWorkspaceAlerts({ + return useQueryGetWorkspaceMessages({ select, }); } diff --git a/src/features/alerts/lib/__tests__/is-alert-malicious.test.ts b/src/features/alerts/lib/__tests__/is-alert-malicious.test.ts index b41a9505..89f076cf 100644 --- a/src/features/alerts/lib/__tests__/is-alert-malicious.test.ts +++ b/src/features/alerts/lib/__tests__/is-alert-malicious.test.ts @@ -1,11 +1,15 @@ import { test, expect } from "vitest"; -import { isAlertMalicious } from "../is-alert-malicious"; +import { isAlertConversationMalicious } from "../is-alert-malicious"; import { makeMockAlert } from "../../mocks/alert.mock"; test("matches malicious alert", () => { - expect(isAlertMalicious(makeMockAlert({ type: "malicious" }))).toBe(true); + expect( + isAlertConversationMalicious(makeMockAlert({ type: "malicious" })), + ).toBe(true); }); test("doesn't match secret", () => { - expect(isAlertMalicious(makeMockAlert({ type: "secret" }))).toBe(false); + expect(isAlertConversationMalicious(makeMockAlert({ type: "secret" }))).toBe( + false, + ); }); diff --git a/src/features/alerts/lib/__tests__/is-alert-secret.test.ts b/src/features/alerts/lib/__tests__/is-alert-secret.test.ts index 88d32bfa..a6d3e08c 100644 --- a/src/features/alerts/lib/__tests__/is-alert-secret.test.ts +++ b/src/features/alerts/lib/__tests__/is-alert-secret.test.ts @@ -1,11 +1,15 @@ import { test, expect } from "vitest"; -import { isAlertSecret } from "../is-alert-secret"; +import { isAlertConversationSecret } from "../is-alert-secret"; import { makeMockAlert } from "../../mocks/alert.mock"; test("matches secret alert", () => { - expect(isAlertSecret(makeMockAlert({ type: "secret" }))).toBe(true); + expect(isAlertConversationSecret(makeMockAlert({ type: "secret" }))).toBe( + true, + ); }); test("doesn't match malicious", () => { - expect(isAlertSecret(makeMockAlert({ type: "malicious" }))).toBe(false); + expect(isAlertConversationSecret(makeMockAlert({ type: "malicious" }))).toBe( + false, + ); }); diff --git a/src/features/alerts/lib/does-alert-include-search.ts b/src/features/alerts/lib/does-alert-include-search.ts index 8b91b89b..9ef55c75 100644 --- a/src/features/alerts/lib/does-alert-include-search.ts +++ b/src/features/alerts/lib/does-alert-include-search.ts @@ -1,7 +1,9 @@ import { AlertConversation, + Conversation, V1GetWorkspaceAlertsResponse, } from "@/api/generated"; +import { isAlertMalicious } from "./is-alert-malicious"; export function doesAlertIncludeSearch( alert: V1GetWorkspaceAlertsResponse[number], @@ -36,3 +38,38 @@ export function doesAlertIncludeSearch( malicious_pkg_type, ].some((i) => i?.toLowerCase().includes(searchQuery)); } + +export function doesConversationIncludeSearch( + conversation: Conversation, + searchQuery: string | null, +): boolean { + if (conversation == null) return false; + if (searchQuery === null) return true; + + const trigger_types: string[] = + conversation.alerts?.map((a) => a.trigger_type) ?? []; + + const trigger_strings: string[] = + conversation.alerts + ?.map((a) => + typeof a.trigger_string === "string" ? a.trigger_string : "", + ) + .filter(Boolean) ?? []; + + const malicious_pkg_names: string[] = + conversation.alerts?.map((a) => + isAlertMalicious(a) ? (a.trigger_string as { name: string }).name : "", + ) ?? []; + + const malicious_pkg_types: string[] = + conversation.alerts?.map((a) => + isAlertMalicious(a) ? (a.trigger_string as { type: string }).type : "", + ) ?? []; + + return [ + ...trigger_types, + ...trigger_strings, + ...malicious_pkg_names, + ...malicious_pkg_types, + ].some((i) => i?.toLowerCase().includes(searchQuery)); +} diff --git a/src/features/alerts/lib/is-alert-malicious.ts b/src/features/alerts/lib/is-alert-malicious.ts index 2c9e10ed..d647149e 100644 --- a/src/features/alerts/lib/is-alert-malicious.ts +++ b/src/features/alerts/lib/is-alert-malicious.ts @@ -1,6 +1,7 @@ -import { AlertConversation } from "@/api/generated"; +import { Alert, AlertConversation, Conversation } from "@/api/generated"; -export function isAlertMalicious( +/** @deprecated */ +export function isAlertConversationMalicious( alert: AlertConversation | null, ): alert is AlertConversation { return ( @@ -11,3 +12,19 @@ export function isAlertMalicious( alert.trigger_string.status === "malicious" ); } + +export function isConversationWithMaliciousAlerts( + conversation: Conversation | null, +): boolean { + return conversation?.alerts?.some(isAlertMalicious) ?? false; +} + +export function isAlertMalicious(alert: Alert) { + return ( + alert?.trigger_category === "critical" && + alert.trigger_string !== null && + typeof alert.trigger_string === "object" && + "status" in alert.trigger_string && + alert.trigger_string.status === "malicious" + ); +} diff --git a/src/features/alerts/lib/is-alert-secret.ts b/src/features/alerts/lib/is-alert-secret.ts index 9e7fd87a..54c3aeb2 100644 --- a/src/features/alerts/lib/is-alert-secret.ts +++ b/src/features/alerts/lib/is-alert-secret.ts @@ -1,6 +1,26 @@ -import { V1GetWorkspaceAlertsResponse } from "@/api/generated"; +import { + Alert, + Conversation, + V1GetWorkspaceAlertsResponse, +} from "@/api/generated"; -export function isAlertSecret(alert: V1GetWorkspaceAlertsResponse[number]) { +/** @deprecated */ +export function isAlertConversationSecret( + alert: V1GetWorkspaceAlertsResponse[number], +) { + return ( + alert?.trigger_category === "critical" && + alert.trigger_type === "codegate-secrets" + ); +} + +export function isConversationWithSecretAlerts( + conversation: Conversation | null, +): boolean { + return conversation?.alerts?.some(isAlertSecret) ?? false; +} + +export function isAlertSecret(alert: Alert) { return ( alert?.trigger_category === "critical" && alert.trigger_type === "codegate-secrets" diff --git a/src/hooks/use-query-get-workspace-messages.ts b/src/hooks/use-query-get-workspace-messages.ts index fbb7e2be..d0c5fb05 100644 --- a/src/hooks/use-query-get-workspace-messages.ts +++ b/src/hooks/use-query-get-workspace-messages.ts @@ -1,6 +1,5 @@ import { useQuery } from "@tanstack/react-query"; import { - Conversation, V1GetWorkspaceMessagesResponse, V1GetWorkspaceMessagesData, } from "@/api/generated"; @@ -8,15 +7,13 @@ import { v1GetWorkspaceMessagesOptions } from "@/api/generated/@tanstack/react-q import { useActiveWorkspaceName } from "@/features/workspace/hooks/use-active-workspace-name"; import { getQueryCacheConfig } from "@/lib/react-query-utils"; -// NOTE: This needs to be a stable function reference to enable memo-isation of -// the select operation on each React re-render. -function select(data: V1GetWorkspaceMessagesResponse): Conversation[] { - return data.filter((prompt) => - prompt.question_answers?.every((item) => item.answer && item.question), - ); -} - -export const useQueryGetWorkspaceMessages = () => { +export const useQueryGetWorkspaceMessages = < + T = V1GetWorkspaceMessagesResponse, +>({ + select, +}: { + select?: (data: V1GetWorkspaceMessagesResponse) => T; +} = {}) => { const { data: activeWorkspaceName } = useActiveWorkspaceName(); const options: V1GetWorkspaceMessagesData = { diff --git a/src/lib/utils.ts b/src/lib/utils.ts index 976388a4..d669f48e 100644 --- a/src/lib/utils.ts +++ b/src/lib/utils.ts @@ -1,6 +1,6 @@ import { AlertConversation, Conversation } from "@/api/generated/types.gen"; -import { isAlertSecret } from "@/features/alerts/lib/is-alert-secret"; -import { isAlertMalicious } from "@/features/alerts/lib/is-alert-malicious"; +import { isAlertConversationSecret } from "@/features/alerts/lib/is-alert-secret"; +import { isAlertConversationMalicious } from "@/features/alerts/lib/is-alert-malicious"; import { format, isToday, isYesterday } from "date-fns"; const ONE_DAY_MS = 24 * 60 * 60 * 1000; @@ -73,7 +73,7 @@ export function groupPromptsByRelativeDate(prompts: Conversation[]) { const promptsSorted = prompts.sort( (a, b) => new Date(b.conversation_timestamp).getTime() - - new Date(a.conversation_timestamp).getTime() + new Date(a.conversation_timestamp).getTime(), ); const grouped = promptsSorted.reduce( @@ -90,7 +90,7 @@ export function groupPromptsByRelativeDate(prompts: Conversation[]) { (groups[group] ?? []).push(prompt); return groups; }, - {} as Record + {} as Record, ); return grouped; @@ -125,10 +125,10 @@ export function sanitizeQuestionPrompt({ } export function getIssueDetectedType( - alert: AlertConversation + alert: AlertConversation, ): "malicious_package" | "leaked_secret" | null { - if (isAlertMalicious(alert)) return "malicious_package"; - if (isAlertSecret(alert)) return "leaked_secret"; + if (isAlertConversationMalicious(alert)) return "malicious_package"; + if (isAlertConversationSecret(alert)) return "leaked_secret"; return null; } From 95dc68422e81b8fbadc271e162050095e0d8b253 Mon Sep 17 00:00:00 2001 From: alex-mcgovern Date: Mon, 10 Feb 2025 10:55:52 +0000 Subject: [PATCH 03/19] refactor: continue replace alerts with messages --- eslint.config.js | 127 +- index.html | 2 +- knip.json | 7 + knip.ts | 10 - package-lock.json | 2604 +++++++++++++---- package.json | 8 +- src/App.test.tsx | 27 +- src/App.tsx | 29 +- src/components/AlertDetail.tsx | 8 +- src/components/Error.tsx | 2 +- src/components/PromptList.tsx | 51 - src/components/Sidebar.tsx | 49 - src/components/__tests__/PromptList.test.tsx | 93 - src/components/empty-state.tsx | 2 +- src/components/ui/sheet.tsx | 139 - src/components/ui/sidebar.tsx | 552 ---- .../components/__tests__/tabs-alerts.test.tsx | 72 - .../alerts/components/search-field-alerts.tsx | 32 - .../lib/__tests__/is-alert-malicious.test.ts | 15 - .../lib/__tests__/is-alert-secret.test.ts | 15 - .../alerts/lib/does-alert-include-search.ts | 75 - .../alerts/lib/is-alert-conversation.ts | 14 - src/features/alerts/mocks/alert.mock.ts | 91 - .../alerts-summary-malicious-pkg.test.tsx | 15 +- .../__tests__/alerts-summary-secrets.test.tsx | 15 +- ...rts-summary-workspace-token-usage.test.tsx | 25 +- .../alerts-summary-malicious-pkg.tsx | 3 +- .../components/alerts-summary-secrets.tsx | 0 .../alerts-summary-workspace-token-usage.tsx | 0 .../components/alerts-summary.tsx | 0 ...uery-get-workspace-alerts-malicious-pkg.ts | 4 +- .../use-query-get-workspace-alerts-secrets.ts | 4 +- .../hooks/use-query-get-workspace-alerts.ts | 0 .../use-query-get-workspace-token-usage.ts | 0 .../__tests__/table-messages.alerts.test.tsx | 82 + .../table-messages.empty-state.test.tsx} | 106 +- .../table-messages.pagination.test.tsx | 69 + .../__tests__/table-messages.test.tsx | 18 + .../__tests__/table-messages.token-usage.tsx} | 29 +- .../__tests__/tabs-messages.test.tsx | 95 + .../components/search-field-messages.tsx | 46 + .../components/table-alert-token-usage.tsx | 0 .../table-messages-empty-state.tsx} | 34 +- .../components/table-messages.tsx} | 70 +- .../components/tabs-messages.tsx} | 12 +- .../components/token-usage-by-providers.tsx | 0 .../constants/strings.ts | 14 +- .../constants/table-messages-columns.ts | 39 + .../use-messages-filter-search-params.ts} | 8 +- ...use-query-get-workspace-messages-table.ts} | 20 +- .../lib/__tests__/is-alert-malicious.test.ts | 11 + .../lib/__tests__/is-alert-secret.test.ts | 11 + .../lib/filter-messages-by-substring.ts | 23 + .../lib/is-alert-critical.ts | 0 .../lib/is-alert-malicious.ts | 0 .../lib/is-alert-secret.ts | 0 .../__tests__/header-status-menu.test.tsx | 51 +- src/features/header/components/header.tsx | 13 +- .../__tests__/archive-workspace.test.tsx | 21 +- .../workspace-custom-instructions.test.tsx | 23 +- .../__tests__/workspace-name.test.tsx | 15 +- .../hooks/use-preferred-preferred-model.ts | 8 +- src/hooks/use-confirm.tsx | 17 - src/hooks/use-mobile.tsx | 19 - src/index.css | 7 +- src/lib/test-utils.tsx | 10 +- src/lib/utils.ts | 17 +- src/main.tsx | 26 +- src/mocks/msw/fixtures/GET_MESSAGES.json | 306 -- src/mocks/msw/handlers.ts | 139 +- src/mocks/msw/mockers/alert.mock.ts | 65 + src/mocks/msw/mockers/conversation.mock.ts | 54 + .../msw/mockers}/token-usage.mock.ts | 0 src/routes/__tests__/route-dashboard.test.tsx | 496 +--- src/routes/route-chat.tsx | 75 +- src/routes/route-dashboard.tsx | 16 +- src/test/msw-endpoint.ts | 25 + src/vite-env.d.ts | 1 + tsconfig.app.json | 9 +- 79 files changed, 3311 insertions(+), 2879 deletions(-) create mode 100644 knip.json delete mode 100644 knip.ts delete mode 100644 src/components/PromptList.tsx delete mode 100644 src/components/Sidebar.tsx delete mode 100644 src/components/__tests__/PromptList.test.tsx delete mode 100644 src/components/ui/sheet.tsx delete mode 100644 src/components/ui/sidebar.tsx delete mode 100644 src/features/alerts/components/__tests__/tabs-alerts.test.tsx delete mode 100644 src/features/alerts/components/search-field-alerts.tsx delete mode 100644 src/features/alerts/lib/__tests__/is-alert-malicious.test.ts delete mode 100644 src/features/alerts/lib/__tests__/is-alert-secret.test.ts delete mode 100644 src/features/alerts/lib/does-alert-include-search.ts delete mode 100644 src/features/alerts/lib/is-alert-conversation.ts delete mode 100644 src/features/alerts/mocks/alert.mock.ts rename src/features/{alerts => dashboard-alerts}/components/__tests__/alerts-summary-malicious-pkg.test.tsx (64%) rename src/features/{alerts => dashboard-alerts}/components/__tests__/alerts-summary-secrets.test.tsx (64%) rename src/features/{alerts => dashboard-alerts}/components/__tests__/alerts-summary-workspace-token-usage.test.tsx (71%) rename src/features/{alerts => dashboard-alerts}/components/alerts-summary-malicious-pkg.tsx (99%) rename src/features/{alerts => dashboard-alerts}/components/alerts-summary-secrets.tsx (100%) rename src/features/{alerts => dashboard-alerts}/components/alerts-summary-workspace-token-usage.tsx (100%) rename src/features/{alerts => dashboard-alerts}/components/alerts-summary.tsx (100%) rename src/features/{alerts => dashboard-alerts}/hooks/use-query-get-workspace-alerts-malicious-pkg.ts (76%) rename src/features/{alerts => dashboard-alerts}/hooks/use-query-get-workspace-alerts-secrets.ts (76%) rename src/features/{alerts => dashboard-alerts}/hooks/use-query-get-workspace-alerts.ts (100%) rename src/features/{alerts => dashboard-alerts}/hooks/use-query-get-workspace-token-usage.ts (100%) create mode 100644 src/features/dashboard-messages/components/__tests__/table-messages.alerts.test.tsx rename src/features/{alerts/components/__tests__/table-alerts.empty-state.test.tsx => dashboard-messages/components/__tests__/table-messages.empty-state.test.tsx} (75%) create mode 100644 src/features/dashboard-messages/components/__tests__/table-messages.pagination.test.tsx create mode 100644 src/features/dashboard-messages/components/__tests__/table-messages.test.tsx rename src/features/{alerts/components/__tests__/table-alerts.test.tsx => dashboard-messages/components/__tests__/table-messages.token-usage.tsx} (66%) create mode 100644 src/features/dashboard-messages/components/__tests__/tabs-messages.test.tsx create mode 100644 src/features/dashboard-messages/components/search-field-messages.tsx rename src/features/{alerts => dashboard-messages}/components/table-alert-token-usage.tsx (100%) rename src/features/{alerts/components/table-alerts-empty-state.tsx => dashboard-messages/components/table-messages-empty-state.tsx} (85%) rename src/features/{alerts/components/table-alerts.tsx => dashboard-messages/components/table-messages.tsx} (79%) rename src/features/{alerts/components/tabs-alerts.tsx => dashboard-messages/components/tabs-messages.tsx} (88%) rename src/features/{alerts => dashboard-messages}/components/token-usage-by-providers.tsx (100%) rename src/features/{alerts => dashboard-messages}/constants/strings.ts (74%) create mode 100644 src/features/dashboard-messages/constants/table-messages-columns.ts rename src/features/{alerts/hooks/use-alerts-filter-search-params.ts => dashboard-messages/hooks/use-messages-filter-search-params.ts} (92%) rename src/features/{alerts/hooks/use-query-get-workspace-alerts-table.ts => dashboard-messages/hooks/use-query-get-workspace-messages-table.ts} (64%) create mode 100644 src/features/dashboard-messages/lib/__tests__/is-alert-malicious.test.ts create mode 100644 src/features/dashboard-messages/lib/__tests__/is-alert-secret.test.ts create mode 100644 src/features/dashboard-messages/lib/filter-messages-by-substring.ts rename src/features/{alerts => dashboard-messages}/lib/is-alert-critical.ts (100%) rename src/features/{alerts => dashboard-messages}/lib/is-alert-malicious.ts (100%) rename src/features/{alerts => dashboard-messages}/lib/is-alert-secret.ts (100%) delete mode 100644 src/hooks/use-mobile.tsx delete mode 100644 src/mocks/msw/fixtures/GET_MESSAGES.json create mode 100644 src/mocks/msw/mockers/alert.mock.ts create mode 100644 src/mocks/msw/mockers/conversation.mock.ts rename src/{features/alerts/mocks => mocks/msw/mockers}/token-usage.mock.ts (100%) create mode 100644 src/test/msw-endpoint.ts diff --git a/eslint.config.js b/eslint.config.js index 2af2f2e0..6640e919 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -1,9 +1,66 @@ import js from "@eslint/js"; import globals from "globals"; +import importPlugin from "eslint-plugin-import"; import reactHooks from "eslint-plugin-react-hooks"; import reactRefresh from "eslint-plugin-react-refresh"; import tseslint from "typescript-eslint"; import tailwindPlugin from "eslint-plugin-tailwindcss"; +import path from "path"; +import fs from "fs"; + +const FEATURES_DIR = "./src/features"; + +/** + * Traverse the features directory and return an array of restricted paths for + * use in the `import/no-restricted-paths` rule. + * + * @example + * ```js + * [ + * { + * except: [ './dependencies' ], + * from: './src/features', + * target: './src/features/dependencies' + * }, + * { + * except: [ './versions' ], + * from: './src/features', + * target: './src/features/versions' + * }, + * { + * except: [ './vulnerabilities' ], + * from: './src/features', + * target: './src/features/vulnerabilities' + * } + * ] + * ``` + */ +const getRestrictedPathsForFeatureDir = () => { + const featureDirPath = path.resolve(FEATURES_DIR); + /** + * @type {Array<{except: `./${string}`[], from: './src/features', target: string}>} + */ + const restrictedPaths = []; + + try { + const featureDirs = fs.readdirSync(featureDirPath); + + featureDirs.forEach((featureDir) => { + const subPath = path.join(featureDirPath, featureDir); + if (fs.lstatSync(subPath).isDirectory()) { + restrictedPaths.push({ + except: [`./${featureDir}`], + from: FEATURES_DIR, + target: path.join(FEATURES_DIR, featureDir), + }); + } + }); + } catch (error) { + console.error("Error reading features directory:", error); + } + + return restrictedPaths; +}; const restrictedSyntax = { reactQuery: { @@ -24,15 +81,29 @@ export default tseslint.config( ], files: ["**/*.{ts,tsx}"], languageOptions: { - ecmaVersion: 2020, - globals: globals.browser, + parserOptions: { + projectService: true, + tsconfigRootDir: import.meta.dirname, + ecmaFeatures: { + jsx: true, + }, + }, + globals: { + ...globals.browser, + ...globals.node, + }, }, plugins: { "react-hooks": reactHooks, "react-refresh": reactRefresh, + import: importPlugin, }, - settings: { + "import/resolver": { + typescript: true, + node: true, + }, + tailwindcss: { callees: ["tv", "twMerge"], config: "./tailwind.config.ts", @@ -105,6 +176,20 @@ export default tseslint.config( message: "Do not directly call `invalidateQueries`. Instead, use the `invalidateQueries` helper function.", }, + { + selector: [ + "CallExpression[callee.object.name='http'][callee.property.name='all'] > Literal:first-child", + "CallExpression[callee.object.name='http'][callee.property.name='head'] > Literal:first-child", + "CallExpression[callee.object.name='http'][callee.property.name='get'] > Literal:first-child", + "CallExpression[callee.object.name='http'][callee.property.name='post'] > Literal:first-child", + "CallExpression[callee.object.name='http'][callee.property.name='put'] > Literal:first-child", + "CallExpression[callee.object.name='http'][callee.property.name='delete'] > Literal:first-child", + "CallExpression[callee.object.name='http'][callee.property.name='patch'] > Literal:first-child", + "CallExpression[callee.object.name='http'][callee.property.name='options'] > Literal:first-child", + ].join(", "), + message: + "Do not pass a string as the first argument to methods on Mock Service Worker's `http`. Use the `mswEndpoint` helper function instead, which provides type-safe routes based on the OpenAPI spec and the API base URL.", + }, ], "no-restricted-imports": [ "error", @@ -118,6 +203,40 @@ export default tseslint.config( ], }, ], + "import/no-restricted-paths": [ + "error", + { + zones: [ + // disables cross-feature imports: + // eg. src/features/dashboard-alerts should not import from src/features/dashboard-messages, etc. + ...getRestrictedPathsForFeatureDir(), + + // enforce unidirectional codebase: + // e.g. src/routes can import from src/features but not the other way around + { + from: "./src/routes", + target: "./src/features", + }, + + // enforce unidirectional codebase: + // e.g src/features and src/routes can import from these shared modules but not the other way around + { + from: ["./src/features", "./src/routes"], + target: [ + "./src/components", + "./src/constants", + "./src/hooks", + "./src/i18n", + "./src/lib", + "./src/mocks", + "./src/trusty-api", + "./src/types", + "./src/utils", + ], + }, + ], + }, + ], }, - }, + } ); diff --git a/index.html b/index.html index 249c9553..1ba893ea 100644 --- a/index.html +++ b/index.html @@ -1,4 +1,4 @@ - + diff --git a/knip.json b/knip.json new file mode 100644 index 00000000..087830c2 --- /dev/null +++ b/knip.json @@ -0,0 +1,7 @@ +{ + "$schema": "https://unpkg.com/knip@5/schema.json", + "entry": ["src/main.tsx"], + "ignore": ["src/api/generated/**/*"], + "ignoreDependencies": ["husky"], + "project": ["src/**/*.{js,jsx,ts,tsx}"] +} diff --git a/knip.ts b/knip.ts deleted file mode 100644 index 72ef7672..00000000 --- a/knip.ts +++ /dev/null @@ -1,10 +0,0 @@ -import type { KnipConfig } from "knip"; - -const config: KnipConfig = { - entry: ["src/main.tsx"], - ignore: ["src/api/generated/**/*"], - ignoreDependencies: ["husky"], - project: ["src/**/*.{js,jsx,ts,tsx}"], -}; - -export default config; diff --git a/package-lock.json b/package-lock.json index 5260ba41..973fe4b8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,20 +10,15 @@ "dependencies": { "@hey-api/client-fetch": "^0.7.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-1", "@tanstack/react-query": "^5.64.1", "@tanstack/react-query-devtools": "^5.66.0", - "@types/prismjs": "^1.26.5", "@types/react-syntax-highlighter": "^15.5.13", "@untitled-ui/icons-react": "^0.1.4", "clsx": "^2.1.1", "date-fns": "^4.1.0", "fuse.js": "^7.0.0", "highlight.js": "^11.11.1", - "prismjs": "^1.29.0", "react": "19.0.0", "react-dom": "19.0.0", "react-markdown": "^9.0.1", @@ -49,12 +44,15 @@ "@types/node": "^22.10.1", "@types/react": "^19.0.2", "@types/react-dom": "^19.0.2", + "@typescript-eslint/parser": "^8.23.0", "@vitejs/plugin-react-swc": "^3.5.0", "@vitest/coverage-istanbul": "^2.1.8", "@vitest/expect": "^3.0.4", "@vitest/ui": "^2.1.4", "autoprefixer": "^10.4.20", "eslint": "^9.18.0", + "eslint-import-resolver-typescript": "^3.7.0", + "eslint-plugin-import": "^2.31.0", "eslint-plugin-react-hooks": "^5.1.0", "eslint-plugin-react-refresh": "^0.4.16", "eslint-plugin-tailwindcss": "3.17.5", @@ -1590,6 +1588,16 @@ "node": ">= 8" } }, + "node_modules/@nolyfill/is-core-module": { + "version": "1.0.39", + "resolved": "https://registry.npmjs.org/@nolyfill/is-core-module/-/is-core-module-1.0.39.tgz", + "integrity": "sha512-nn5ozdjYQpUCZlWGuxcJY/KpxkWQs4DcbMCmKojjyrYDEAGy4Ce19NN4v5MduafTwJlbKc99UA8YhSVqq9yPZA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.4.0" + } + }, "node_modules/@open-draft/deferred-promise": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/@open-draft/deferred-promise/-/deferred-promise-2.2.0.tgz", @@ -1632,341 +1640,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.4", - "resolved": "https://registry.npmjs.org/@radix-ui/react-dialog/-/react-dialog-1.1.4.tgz", - "integrity": "sha512-Ur7EV1IwQGCyaAuyDRiOLA5JIUZxELJljF+MbM/2NC0BYwfuRrbpS30BiQBJrVruscgUkieKkqXYDOoByaxIoA==", - "license": "MIT", - "dependencies": { - "@radix-ui/primitive": "1.1.1", - "@radix-ui/react-compose-refs": "1.1.1", - "@radix-ui/react-context": "1.1.1", - "@radix-ui/react-dismissable-layer": "1.1.3", - "@radix-ui/react-focus-guards": "1.1.1", - "@radix-ui/react-focus-scope": "1.1.1", - "@radix-ui/react-id": "1.1.0", - "@radix-ui/react-portal": "1.1.3", - "@radix-ui/react-presence": "1.1.2", - "@radix-ui/react-primitive": "2.0.1", - "@radix-ui/react-slot": "1.1.1", - "@radix-ui/react-use-controllable-state": "1.1.0", - "aria-hidden": "^1.1.1", - "react-remove-scroll": "^2.6.1" - }, - "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.3", - "resolved": "https://registry.npmjs.org/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.1.3.tgz", - "integrity": "sha512-onrWn/72lQoEucDmJnr8uczSNTujT0vJnA/X5+3AkChVPowr8n1yvIKIabhWyMQeMvvmdpsvcyDqx3X1LEXCPg==", - "license": "MIT", - "dependencies": { - "@radix-ui/primitive": "1.1.1", - "@radix-ui/react-compose-refs": "1.1.1", - "@radix-ui/react-primitive": "2.0.1", - "@radix-ui/react-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.1", - "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-scope/-/react-focus-scope-1.1.1.tgz", - "integrity": "sha512-01omzJAYRxXdG2/he/+xy+c8a8gCydoQ1yOxnWNcRhrrBW5W+RQJ22EK1SaO8tb3WoUsuEw7mJjBozPzihDFjA==", - "license": "MIT", - "dependencies": { - "@radix-ui/react-compose-refs": "1.1.1", - "@radix-ui/react-primitive": "2.0.1", - "@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.3", - "resolved": "https://registry.npmjs.org/@radix-ui/react-portal/-/react-portal-1.1.3.tgz", - "integrity": "sha512-NciRqhXnGojhT93RPyDaMPfLH3ZSl4jjIFbZQ1b/vxvZEdHsBZ49wP9w8L3HzUQwep01LcWtkUvm0OVB5JAHTw==", - "license": "MIT", - "dependencies": { - "@radix-ui/react-primitive": "2.0.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-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.1", - "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.0.1.tgz", - "integrity": "sha512-sHCWTtxwNn3L3fH8qAfnF3WbUZycW93SM1j3NFDzXBiz8D6F5UTTy8G1+WFEaiCdvCVRJWj6N2R4Xq6HdiHmDg==", - "license": "MIT", - "dependencies": { - "@radix-ui/react-slot": "1.1.1" - }, - "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.1", - "resolved": "https://registry.npmjs.org/@radix-ui/react-separator/-/react-separator-1.1.1.tgz", - "integrity": "sha512-RRiNRSrD8iUiXriq/Y5n4/3iE8HzqgLHsusUSg5jVpU2+3tqcUFPJXHDymwEypunc2sWxDUS3UC+rkZRlHedsw==", - "license": "MIT", - "dependencies": { - "@radix-ui/react-primitive": "2.0.1" - }, - "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.1", - "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.1.1.tgz", - "integrity": "sha512-RApLLOcINYJA+dMVbOju7MYv1Mb2EBp2nH4HdDzXTSyaR5optlm6Otrz1euW3HbdOR8UmmFK06TD+A9frYWv+g==", - "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", @@ -3903,6 +3576,13 @@ "win32" ] }, + "node_modules/@rtsao/scc": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@rtsao/scc/-/scc-1.1.0.tgz", + "integrity": "sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g==", + "dev": true, + "license": "MIT" + }, "node_modules/@snyk/github-codeowners": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@snyk/github-codeowners/-/github-codeowners-1.1.0.tgz", @@ -4418,6 +4098,13 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/json5": { + "version": "0.0.29", + "resolved": "https://registry.npmjs.org/@types/json5/-/json5-0.0.29.tgz", + "integrity": "sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/mdast": { "version": "4.0.4", "resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-4.0.4.tgz", @@ -4443,12 +4130,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", @@ -4528,16 +4209,16 @@ } }, "node_modules/@typescript-eslint/parser": { - "version": "8.19.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.19.0.tgz", - "integrity": "sha512-6M8taKyOETY1TKHp0x8ndycipTVgmp4xtg5QpEZzXxDhNvvHOJi5rLRkLr8SK3jTgD5l4fTlvBiRdfsuWydxBw==", + "version": "8.23.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.23.0.tgz", + "integrity": "sha512-h2lUByouOXFAlMec2mILeELUbME5SZRN/7R9Cw2RD2lRQQY08MWMM+PmVVKKJNK1aIwqTo9t/0CvOxwPbRIE2Q==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/scope-manager": "8.19.0", - "@typescript-eslint/types": "8.19.0", - "@typescript-eslint/typescript-estree": "8.19.0", - "@typescript-eslint/visitor-keys": "8.19.0", + "@typescript-eslint/scope-manager": "8.23.0", + "@typescript-eslint/types": "8.23.0", + "@typescript-eslint/typescript-estree": "8.23.0", + "@typescript-eslint/visitor-keys": "8.23.0", "debug": "^4.3.4" }, "engines": { @@ -4552,15 +4233,15 @@ "typescript": ">=4.8.4 <5.8.0" } }, - "node_modules/@typescript-eslint/scope-manager": { - "version": "8.19.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.19.0.tgz", - "integrity": "sha512-hkoJiKQS3GQ13TSMEiuNmSCvhz7ujyqD1x3ShbaETATHrck+9RaDdUbt+osXaUuns9OFwrDTTrjtwsU8gJyyRA==", + "node_modules/@typescript-eslint/parser/node_modules/@typescript-eslint/scope-manager": { + "version": "8.23.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.23.0.tgz", + "integrity": "sha512-OGqo7+dXHqI7Hfm+WqkZjKjsiRtFUQHPdGMXzk5mYXhJUedO7e/Y7i8AK3MyLMgZR93TX4bIzYrfyVjLC+0VSw==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.19.0", - "@typescript-eslint/visitor-keys": "8.19.0" + "@typescript-eslint/types": "8.23.0", + "@typescript-eslint/visitor-keys": "8.23.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -4570,44 +4251,173 @@ "url": "https://opencollective.com/typescript-eslint" } }, - "node_modules/@typescript-eslint/type-utils": { - "version": "8.19.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.19.0.tgz", - "integrity": "sha512-TZs0I0OSbd5Aza4qAMpp1cdCYVnER94IziudE3JU328YUHgWu9gwiwhag+fuLeJ2LkWLXI+F/182TbG+JaBdTg==", + "node_modules/@typescript-eslint/parser/node_modules/@typescript-eslint/types": { + "version": "8.23.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.23.0.tgz", + "integrity": "sha512-1sK4ILJbCmZOTt9k4vkoulT6/y5CHJ1qUYxqpF1K/DBAd8+ZUL4LlSCxOssuH5m4rUaaN0uS0HlVPvd45zjduQ==", "dev": true, "license": "MIT", - "dependencies": { - "@typescript-eslint/typescript-estree": "8.19.0", - "@typescript-eslint/utils": "8.19.0", - "debug": "^4.3.4", - "ts-api-utils": "^1.3.0" - }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "eslint": "^8.57.0 || ^9.0.0", - "typescript": ">=4.8.4 <5.8.0" } }, - "node_modules/@typescript-eslint/types": { - "version": "8.19.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.19.0.tgz", - "integrity": "sha512-8XQ4Ss7G9WX8oaYvD4OOLCjIQYgRQxO+qCiR2V2s2GxI9AUpo7riNwo6jDhKtTcaJjT8PY54j2Yb33kWtSJsmA==", + "node_modules/@typescript-eslint/parser/node_modules/@typescript-eslint/typescript-estree": { + "version": "8.23.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.23.0.tgz", + "integrity": "sha512-LcqzfipsB8RTvH8FX24W4UUFk1bl+0yTOf9ZA08XngFwMg4Kj8A+9hwz8Cr/ZS4KwHrmo9PJiLZkOt49vPnuvQ==", "dev": true, "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.23.0", + "@typescript-eslint/visitor-keys": "8.23.0", + "debug": "^4.3.4", + "fast-glob": "^3.3.2", + "is-glob": "^4.0.3", + "minimatch": "^9.0.4", + "semver": "^7.6.0", + "ts-api-utils": "^2.0.1" + }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/typescript-eslint" - } - }, + }, + "peerDependencies": { + "typescript": ">=4.8.4 <5.8.0" + } + }, + "node_modules/@typescript-eslint/parser/node_modules/@typescript-eslint/visitor-keys": { + "version": "8.23.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.23.0.tgz", + "integrity": "sha512-oWWhcWDLwDfu++BGTZcmXWqpwtkwb5o7fxUIGksMQQDSdPW9prsSnfIOZMlsj4vBOSrcnjIUZMiIjODgGosFhQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.23.0", + "eslint-visitor-keys": "^4.2.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/parser/node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/@typescript-eslint/parser/node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@typescript-eslint/parser/node_modules/semver": { + "version": "7.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.1.tgz", + "integrity": "sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@typescript-eslint/parser/node_modules/ts-api-utils": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.0.1.tgz", + "integrity": "sha512-dnlgjFSVetynI8nzgJ+qF62efpglpWRk8isUEWZGWlJYySCTD6aKvbUDu+zbPeDakk3bg5H4XpitHukgfL1m9w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.12" + }, + "peerDependencies": { + "typescript": ">=4.8.4" + } + }, + "node_modules/@typescript-eslint/scope-manager": { + "version": "8.19.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.19.0.tgz", + "integrity": "sha512-hkoJiKQS3GQ13TSMEiuNmSCvhz7ujyqD1x3ShbaETATHrck+9RaDdUbt+osXaUuns9OFwrDTTrjtwsU8gJyyRA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.19.0", + "@typescript-eslint/visitor-keys": "8.19.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/type-utils": { + "version": "8.19.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.19.0.tgz", + "integrity": "sha512-TZs0I0OSbd5Aza4qAMpp1cdCYVnER94IziudE3JU328YUHgWu9gwiwhag+fuLeJ2LkWLXI+F/182TbG+JaBdTg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/typescript-estree": "8.19.0", + "@typescript-eslint/utils": "8.19.0", + "debug": "^4.3.4", + "ts-api-utils": "^1.3.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <5.8.0" + } + }, + "node_modules/@typescript-eslint/types": { + "version": "8.19.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.19.0.tgz", + "integrity": "sha512-8XQ4Ss7G9WX8oaYvD4OOLCjIQYgRQxO+qCiR2V2s2GxI9AUpo7riNwo6jDhKtTcaJjT8PY54j2Yb33kWtSJsmA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, "node_modules/@typescript-eslint/typescript-estree": { "version": "8.19.0", "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.19.0.tgz", @@ -5162,18 +4972,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", @@ -5184,6 +4982,125 @@ "dequal": "^2.0.3" } }, + "node_modules/array-buffer-byte-length": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/array-buffer-byte-length/-/array-buffer-byte-length-1.0.2.tgz", + "integrity": "sha512-LHE+8BuR7RYGDKvnrmcuSq3tDcKv9OFEXQt/HpbZhY7V6h0zlUXutnAD82GiFx9rdieCMjkvtcsPqBwgUl1Iiw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "is-array-buffer": "^3.0.5" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array-includes": { + "version": "3.1.8", + "resolved": "https://registry.npmjs.org/array-includes/-/array-includes-3.1.8.tgz", + "integrity": "sha512-itaWrbYbqpGXkGhZPGUulwnhVf5Hpy1xiCFsGqyIGglbBxmG5vSjxQen3/WGOjPpNEv1RtBLKxbmVXm8HpJStQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.2", + "es-object-atoms": "^1.0.0", + "get-intrinsic": "^1.2.4", + "is-string": "^1.0.7" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array.prototype.findlastindex": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/array.prototype.findlastindex/-/array.prototype.findlastindex-1.2.5.tgz", + "integrity": "sha512-zfETvRFA8o7EiNn++N5f/kaCw221hrpGsDmcpndVupkPzEc1Wuf3VgC0qby1BbHs7f5DVYjgtEU2LLh5bqeGfQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.2", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0", + "es-shim-unscopables": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array.prototype.flat": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/array.prototype.flat/-/array.prototype.flat-1.3.3.tgz", + "integrity": "sha512-rwG/ja1neyLqCuGZ5YYrznA62D4mZXg0i1cIskIUKSiqF3Cje9/wXAls9B9s1Wa2fomMsIv8czB8jZcPmxCXFg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.5", + "es-shim-unscopables": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array.prototype.flatmap": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/array.prototype.flatmap/-/array.prototype.flatmap-1.3.3.tgz", + "integrity": "sha512-Y7Wt51eKJSyi80hFrJCePGGNo5ktJCslFuboqJsbf57CCPcm5zztluPlc4/aD8sWsKvlwatezpV4U1efk8kpjg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.5", + "es-shim-unscopables": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/arraybuffer.prototype.slice": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/arraybuffer.prototype.slice/-/arraybuffer.prototype.slice-1.0.4.tgz", + "integrity": "sha512-BNoCY6SXXPQ7gF2opIP4GBE+Xw7U+pHMYKuzjgCN3GwiaIR09UUeKfheyIry77QtrCBlC0KK0q5/TER/tYh3PQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "array-buffer-byte-length": "^1.0.1", + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.5", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "is-array-buffer": "^3.0.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/assertion-error": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", @@ -5194,6 +5111,16 @@ "node": ">=12" } }, + "node_modules/async-function": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/async-function/-/async-function-1.0.0.tgz", + "integrity": "sha512-hsU18Ae8CDTR6Kgu9DYf0EbCr/a5iGL0rytQDobUcdpYOKokk8LEjVphnXkDkgpi0wYVsqrXuP0bZxJaTqdgoA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, "node_modules/asynckit": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", @@ -5239,6 +5166,22 @@ "postcss": "^8.1.0" } }, + "node_modules/available-typed-arrays": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz", + "integrity": "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "possible-typed-array-names": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/bail": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/bail/-/bail-2.0.2.tgz", @@ -5402,6 +5345,56 @@ "node": ">=8" } }, + "node_modules/call-bind": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.8.tgz", + "integrity": "sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.0", + "es-define-property": "^1.0.0", + "get-intrinsic": "^1.2.4", + "set-function-length": "^1.2.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.1.tgz", + "integrity": "sha512-BhYE+WDaywFg2TBWYNXAE+8B1ATnThNBqXHP5nQu0jWJdVvY2hvkpyB3qOmtmDePiS5/BDQ8wASEWGMWRG148g==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.3.tgz", + "integrity": "sha512-YTd+6wGlNlPxSuri7Y6X8tY2dmm12UMH66RpKMhiX6rsk5wXXnYgbUcOt8kiS31/AjfoTOvCsE+w8nZQLQnzHA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "get-intrinsic": "^1.2.6" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/callsites": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", @@ -5922,15 +5915,69 @@ "node": ">=18" } }, - "node_modules/date-fns": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-4.1.0.tgz", - "integrity": "sha512-Ukq0owbQXxa/U3EGtsdVBkR1w7KOQ5gIBqdH2hkvknzZPYvBxb/aa6E8L7tmjFtkwZBu3UXBbjIgPo/Ez4xaNg==", + "node_modules/data-view-buffer": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/data-view-buffer/-/data-view-buffer-1.0.2.tgz", + "integrity": "sha512-EmKO5V3OLXh1rtK2wgXRansaK1/mtVdTUEiEI0W8RkvgT05kfxaH29PliLnpLP73yYO6142Q72QNa8Wx/A5CqQ==", + "dev": true, "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/kossnocorp" - } + "dependencies": { + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "is-data-view": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/data-view-byte-length": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/data-view-byte-length/-/data-view-byte-length-1.0.2.tgz", + "integrity": "sha512-tuhGbE6CfTM9+5ANGf+oQb72Ky/0+s3xKUpHvShfiz2RxMFgFPjsXuRLBVMtvMs15awe45SRb83D6wH4ew6wlQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "is-data-view": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/inspect-js" + } + }, + "node_modules/data-view-byte-offset": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/data-view-byte-offset/-/data-view-byte-offset-1.0.1.tgz", + "integrity": "sha512-BS8PfmtDGnrgYdOonGZQdLZslWIeCGFP9tpan0hi1Co2Zr2NKADsvGYA8XxuG/4UWgJ6Cjtv+YJnB6MM69QGlQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "is-data-view": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/date-fns": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-4.1.0.tgz", + "integrity": "sha512-Ukq0owbQXxa/U3EGtsdVBkR1w7KOQ5gIBqdH2hkvknzZPYvBxb/aa6E8L7tmjFtkwZBu3UXBbjIgPo/Ez4xaNg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/kossnocorp" + } }, "node_modules/debug": { "version": "4.4.0", @@ -5999,6 +6046,42 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/define-data-property": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", + "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "gopd": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/define-properties": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.1.tgz", + "integrity": "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-data-property": "^1.0.1", + "has-property-descriptors": "^1.0.0", + "object-keys": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/defu": { "version": "6.1.4", "resolved": "https://registry.npmjs.org/defu/-/defu-6.1.4.tgz", @@ -6032,12 +6115,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", @@ -6063,6 +6140,19 @@ "integrity": "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==", "license": "MIT" }, + "node_modules/doctrine": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz", + "integrity": "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "esutils": "^2.0.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/dom-accessibility-api": { "version": "0.5.16", "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz", @@ -6083,6 +6173,21 @@ "url": "https://dotenvx.com" } }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/eastasianwidth": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", @@ -6166,6 +6271,92 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/es-abstract": { + "version": "1.23.9", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.23.9.tgz", + "integrity": "sha512-py07lI0wjxAC/DcfK1S6G7iANonniZwTISvdPzk9hzeH0IZIshbuuFxLIU96OyF89Yb9hiqWn8M/bY83KY5vzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "array-buffer-byte-length": "^1.0.2", + "arraybuffer.prototype.slice": "^1.0.4", + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "data-view-buffer": "^1.0.2", + "data-view-byte-length": "^1.0.2", + "data-view-byte-offset": "^1.0.1", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0", + "es-set-tostringtag": "^2.1.0", + "es-to-primitive": "^1.3.0", + "function.prototype.name": "^1.1.8", + "get-intrinsic": "^1.2.7", + "get-proto": "^1.0.0", + "get-symbol-description": "^1.1.0", + "globalthis": "^1.0.4", + "gopd": "^1.2.0", + "has-property-descriptors": "^1.0.2", + "has-proto": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "internal-slot": "^1.1.0", + "is-array-buffer": "^3.0.5", + "is-callable": "^1.2.7", + "is-data-view": "^1.0.2", + "is-regex": "^1.2.1", + "is-shared-array-buffer": "^1.0.4", + "is-string": "^1.1.1", + "is-typed-array": "^1.1.15", + "is-weakref": "^1.1.0", + "math-intrinsics": "^1.1.0", + "object-inspect": "^1.13.3", + "object-keys": "^1.1.1", + "object.assign": "^4.1.7", + "own-keys": "^1.0.1", + "regexp.prototype.flags": "^1.5.3", + "safe-array-concat": "^1.1.3", + "safe-push-apply": "^1.0.0", + "safe-regex-test": "^1.1.0", + "set-proto": "^1.0.0", + "string.prototype.trim": "^1.2.10", + "string.prototype.trimend": "^1.0.9", + "string.prototype.trimstart": "^1.0.8", + "typed-array-buffer": "^1.0.3", + "typed-array-byte-length": "^1.0.3", + "typed-array-byte-offset": "^1.0.4", + "typed-array-length": "^1.0.7", + "unbox-primitive": "^1.1.0", + "which-typed-array": "^1.1.18" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, "node_modules/es-module-lexer": { "version": "1.6.0", "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.6.0.tgz", @@ -6173,6 +6364,63 @@ "dev": true, "license": "MIT" }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-shim-unscopables": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/es-shim-unscopables/-/es-shim-unscopables-1.0.2.tgz", + "integrity": "sha512-J3yBRXCzDu4ULnQwxyToo/OjdMx6akgVC7K6few0a7F/0wLtmKKN7I73AH5T2836UuXRqN7Qg+IIUw/+YJksRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "hasown": "^2.0.0" + } + }, + "node_modules/es-to-primitive": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.3.0.tgz", + "integrity": "sha512-w+5mJ3GuFL+NjVtJlvydShqE1eN3h3PbI7/5LAsYJP/2qtuMXjfL2LpHSRqo4b4eSF5K/DH1JXKUAHSB2UW50g==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-callable": "^1.2.7", + "is-date-object": "^1.0.5", + "is-symbol": "^1.0.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/esbuild": { "version": "0.24.2", "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.24.2.tgz", @@ -6296,6 +6544,136 @@ } } }, + "node_modules/eslint-import-resolver-node": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.9.tgz", + "integrity": "sha512-WFj2isz22JahUv+B788TlO3N6zL3nNJGU8CcZbPZvVEkBPaJdCV4vy5wyghty5ROFbCRnm132v8BScu5/1BQ8g==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^3.2.7", + "is-core-module": "^2.13.0", + "resolve": "^1.22.4" + } + }, + "node_modules/eslint-import-resolver-node/node_modules/debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.1" + } + }, + "node_modules/eslint-import-resolver-typescript": { + "version": "3.7.0", + "resolved": "https://registry.npmjs.org/eslint-import-resolver-typescript/-/eslint-import-resolver-typescript-3.7.0.tgz", + "integrity": "sha512-Vrwyi8HHxY97K5ebydMtffsWAn1SCR9eol49eCd5fJS4O1WV7PaAjbcjmbfJJSMz/t4Mal212Uz/fQZrOB8mow==", + "dev": true, + "license": "ISC", + "dependencies": { + "@nolyfill/is-core-module": "1.0.39", + "debug": "^4.3.7", + "enhanced-resolve": "^5.15.0", + "fast-glob": "^3.3.2", + "get-tsconfig": "^4.7.5", + "is-bun-module": "^1.0.2", + "is-glob": "^4.0.3", + "stable-hash": "^0.0.4" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/unts/projects/eslint-import-resolver-ts" + }, + "peerDependencies": { + "eslint": "*", + "eslint-plugin-import": "*", + "eslint-plugin-import-x": "*" + }, + "peerDependenciesMeta": { + "eslint-plugin-import": { + "optional": true + }, + "eslint-plugin-import-x": { + "optional": true + } + } + }, + "node_modules/eslint-module-utils": { + "version": "2.12.0", + "resolved": "https://registry.npmjs.org/eslint-module-utils/-/eslint-module-utils-2.12.0.tgz", + "integrity": "sha512-wALZ0HFoytlyh/1+4wuZ9FJCD/leWHQzzrxJ8+rebyReSLk7LApMyd3WJaLVoN+D5+WIdJyDK1c6JnE65V4Zyg==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^3.2.7" + }, + "engines": { + "node": ">=4" + }, + "peerDependenciesMeta": { + "eslint": { + "optional": true + } + } + }, + "node_modules/eslint-module-utils/node_modules/debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.1" + } + }, + "node_modules/eslint-plugin-import": { + "version": "2.31.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.31.0.tgz", + "integrity": "sha512-ixmkI62Rbc2/w8Vfxyh1jQRTdRTF52VxwRVHl/ykPAmqG+Nb7/kNn+byLP0LxPgI7zWA16Jt82SybJInmMia3A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@rtsao/scc": "^1.1.0", + "array-includes": "^3.1.8", + "array.prototype.findlastindex": "^1.2.5", + "array.prototype.flat": "^1.3.2", + "array.prototype.flatmap": "^1.3.2", + "debug": "^3.2.7", + "doctrine": "^2.1.0", + "eslint-import-resolver-node": "^0.3.9", + "eslint-module-utils": "^2.12.0", + "hasown": "^2.0.2", + "is-core-module": "^2.15.1", + "is-glob": "^4.0.3", + "minimatch": "^3.1.2", + "object.fromentries": "^2.0.8", + "object.groupby": "^1.0.3", + "object.values": "^1.2.0", + "semver": "^6.3.1", + "string.prototype.trimend": "^1.0.8", + "tsconfig-paths": "^3.15.0" + }, + "engines": { + "node": ">=4" + }, + "peerDependencies": { + "eslint": "^2 || ^3 || ^4 || ^5 || ^6 || ^7.2.0 || ^8 || ^9" + } + }, + "node_modules/eslint-plugin-import/node_modules/debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.1" + } + }, "node_modules/eslint-plugin-react-hooks": { "version": "5.1.0", "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-5.1.0.tgz", @@ -6636,6 +7014,22 @@ "dev": true, "license": "ISC" }, + "node_modules/for-each": { + "version": "0.3.4", + "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.4.tgz", + "integrity": "sha512-kKaIINnFpzW6ffJNDjjyjrk21BkDx38c0xa/klsT8VzLCaMEefv4ZTacrcVR4DmgTeBra++jMDAfS/tS799YDw==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-callable": "^1.2.7" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/foreground-child": { "version": "3.3.0", "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.0.tgz", @@ -6745,6 +7139,37 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/function.prototype.name": { + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/function.prototype.name/-/function.prototype.name-1.1.8.tgz", + "integrity": "sha512-e5iwyodOHhbMr/yNrc7fDYG4qlbIvI5gajyzPnb5TCwyhjApznQh1BMFou9b30SevY43gCJKXycoCBjMbsuW0Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "define-properties": "^1.2.1", + "functions-have-names": "^1.2.3", + "hasown": "^2.0.2", + "is-callable": "^1.2.7" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/functions-have-names": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/functions-have-names/-/functions-have-names-1.2.3.tgz", + "integrity": "sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/fuse.js": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/fuse.js/-/fuse.js-7.0.0.tgz", @@ -6786,13 +7211,43 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/get-nonce": { + "node_modules/get-intrinsic": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.7.tgz", + "integrity": "sha512-VW6Pxhsrk0KAOqs3WEd0klDiF/+V7gQOpAvY1jVU/LHmaD/kQO4523aiJuikX/QAKYiW6x8Jh+RJej1almdtCA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0", + "function-bind": "^1.1.2", + "get-proto": "^1.0.0", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { "version": "1.0.1", - "resolved": "https://registry.npmjs.org/get-nonce/-/get-nonce-1.0.1.tgz", - "integrity": "sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q==", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "dev": true, "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, "engines": { - "node": ">=6" + "node": ">= 0.4" } }, "node_modules/get-stream": { @@ -6808,7 +7263,38 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/giget": { + "node_modules/get-symbol-description": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/get-symbol-description/-/get-symbol-description-1.1.0.tgz", + "integrity": "sha512-w9UMqWwJxHNOvoNzSJ2oPF5wvYcvP7jUvYzhp67yEhTi17ZDBBC1z9pTdGuzjD+EFIqLSYRweZjqfiPzQ06Ebg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-tsconfig": { + "version": "4.10.0", + "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.10.0.tgz", + "integrity": "sha512-kGzZ3LWWQcGIAmg6iWvXn0ei6WDtV26wzHRMwDSzmAbcXrTEXxHy6IehI6/4eT6VRKyMP1eF1VqwrVUmE/LR7A==", + "dev": true, + "license": "MIT", + "dependencies": { + "resolve-pkg-maps": "^1.0.0" + }, + "funding": { + "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" + } + }, + "node_modules/giget": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/giget/-/giget-1.2.3.tgz", "integrity": "sha512-8EHPljDvs7qKykr6uw8b+lqLiUc/vUg+KVTI0uND4s63TdsZM2Xus3mflvF0DDG9SiM4RlCkFGL+7aAjRmV7KA==", @@ -6897,6 +7383,23 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/globalthis": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/globalthis/-/globalthis-1.0.4.tgz", + "integrity": "sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-properties": "^1.2.1", + "gopd": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/globrex": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/globrex/-/globrex-0.1.2.tgz", @@ -6904,6 +7407,19 @@ "dev": true, "license": "MIT" }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/graceful-fs": { "version": "4.2.11", "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", @@ -6950,6 +7466,19 @@ "uglify-js": "^3.1.4" } }, + "node_modules/has-bigints": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.1.0.tgz", + "integrity": "sha512-R3pbpkcIqv2Pm3dUwgjclDRVmWpTJW2DcMzcIhEXEx1oh/CEMObMm3KLmRJOdvhM7o4uQBnwr8pzRK2sJWIqfg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/has-flag": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", @@ -6960,6 +7489,64 @@ "node": ">=8" } }, + "node_modules/has-property-descriptors": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", + "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-define-property": "^1.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-proto": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.2.0.tgz", + "integrity": "sha512-KIL7eQPfHQRC8+XluaIw7BHUwwqL19bQn4hzNgdr+1wXoU0KKj6rufu47lhY7KbJR2C6T6+PfyN0Ea7wkSS+qQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/hasown": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", @@ -7259,6 +7846,21 @@ "integrity": "sha512-0aO8FkhNZlj/ZIbNi7Lxxr12obT7cL1moPfE4tg1LkX7LlLfC6DeX4l2ZEud1ukP9jNQyNnfzQVqwbwmAATY4Q==", "license": "MIT" }, + "node_modules/internal-slot": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.1.0.tgz", + "integrity": "sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "hasown": "^2.0.2", + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/intl-messageformat": { "version": "10.7.11", "resolved": "https://registry.npmjs.org/intl-messageformat/-/intl-messageformat-10.7.11.tgz", @@ -7295,6 +7897,60 @@ "url": "https://github.com/sponsors/wooorm" } }, + "node_modules/is-array-buffer": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.5.tgz", + "integrity": "sha512-DDfANUiiG2wC1qawP66qlTugJeL5HyzMpfr8lLK+jMQirGzNod0B12cFB/9q838Ru27sBwfw78/rdoU7RERz6A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "get-intrinsic": "^1.2.6" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-async-function": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-async-function/-/is-async-function-2.1.1.tgz", + "integrity": "sha512-9dgM/cZBnNvjzaMYHVoxxfPj2QXt22Ev7SuuPrs+xav0ukGB0S6d4ydZdEiM48kLx5kDV+QBPrpVnFyefL8kkQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "async-function": "^1.0.0", + "call-bound": "^1.0.3", + "get-proto": "^1.0.1", + "has-tostringtag": "^1.0.2", + "safe-regex-test": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-bigint": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-bigint/-/is-bigint-1.1.0.tgz", + "integrity": "sha512-n4ZT37wG78iz03xPRKJrHTdZbe3IicyucEtdRsV5yglwc3GyUfbAfpSeD0FJ41NbUNSt5wbhqfp1fS+BgnvDFQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-bigints": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/is-binary-path": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", @@ -7307,6 +7963,59 @@ "node": ">=8" } }, + "node_modules/is-boolean-object": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.2.2.tgz", + "integrity": "sha512-wa56o2/ElJMYqjCjGkXri7it5FbebW5usLw/nPmCMs5DeZ7eziSYZhSmPRn0txqeW4LnAmQQU7FgqLpsEFKM4A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-bun-module": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/is-bun-module/-/is-bun-module-1.3.0.tgz", + "integrity": "sha512-DgXeu5UWI0IsMQundYb5UAOzm6G2eVnarJ0byP6Tm55iZNKceD59LNPA2L4VvsScTtHcw0yEkVwSf7PC+QoLSA==", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^7.6.3" + } + }, + "node_modules/is-bun-module/node_modules/semver": { + "version": "7.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.1.tgz", + "integrity": "sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/is-callable": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz", + "integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/is-core-module": { "version": "2.16.1", "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", @@ -7322,6 +8031,41 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-data-view": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-data-view/-/is-data-view-1.0.2.tgz", + "integrity": "sha512-RKtWF8pGmS87i2D6gqQu/l7EYRlVdfzemCJN/P3UOs//x1QE7mfhvzHIApBTRf7axvT6DMGwSwBXYCT0nfB9xw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "get-intrinsic": "^1.2.6", + "is-typed-array": "^1.1.13" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-date-object": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.1.0.tgz", + "integrity": "sha512-PwwhEakHVKTdRNVOw+/Gyh0+MzlCl4R6qKvkhuvLtPMggI1WAHt9sOwZxQLSGpUaDnrdyDsomoRgNnCfKNSXXg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/is-decimal": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/is-decimal/-/is-decimal-2.0.1.tgz", @@ -7341,6 +8085,22 @@ "node": ">=0.10.0" } }, + "node_modules/is-finalizationregistry": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-finalizationregistry/-/is-finalizationregistry-1.1.1.tgz", + "integrity": "sha512-1pC6N8qWJbWoPtEjgcL2xyhQOP491EQjeUo3qTKcmV8YSDDJrOepfG8pcC7h/QgnQHYSv0mJ3Z/ZWxmatVrysg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/is-fullwidth-code-point": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-4.0.0.tgz", @@ -7354,6 +8114,25 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/is-generator-function": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.1.0.tgz", + "integrity": "sha512-nPUB5km40q9e8UfN/Zc24eLlzdSf9OfKByBw9CIdw4H1giPMeA0OIJvbchsCu4npfI2QcMVBsGEBHKZ7wLTWmQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "get-proto": "^1.0.0", + "has-tostringtag": "^1.0.2", + "safe-regex-test": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/is-glob": { "version": "4.0.3", "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", @@ -7376,6 +8155,19 @@ "url": "https://github.com/sponsors/wooorm" } }, + "node_modules/is-map": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/is-map/-/is-map-2.0.3.tgz", + "integrity": "sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/is-node-process": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/is-node-process/-/is-node-process-1.2.0.tgz", @@ -7392,6 +8184,23 @@ "node": ">=0.12.0" } }, + "node_modules/is-number-object": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-number-object/-/is-number-object-1.1.1.tgz", + "integrity": "sha512-lZhclumE1G6VYD8VHe35wFaIif+CTy5SJIi5+3y4psDgWu4wPDoBhF8NxUOinEc7pHgiTsT6MaBb92rKhhD+Xw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/is-plain-obj": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-4.1.0.tgz", @@ -7411,6 +8220,54 @@ "dev": true, "license": "MIT" }, + "node_modules/is-regex": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.2.1.tgz", + "integrity": "sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "gopd": "^1.2.0", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-set": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/is-set/-/is-set-2.0.3.tgz", + "integrity": "sha512-iPAjerrse27/ygGLxw+EBR9agv9Y6uLeYVJMu+QNCoouJ1/1ri0mGrcWpfCqFZuzzx3WjtwxG098X+n4OuRkPg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-shared-array-buffer": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.4.tgz", + "integrity": "sha512-ISWac8drv4ZGfwKl5slpHG9OwPNty4jOWPRIhBpxOoD+hqITiwuipOQ2bNthAzwA3B4fIjO4Nln74N0S9byq8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/is-stream": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-3.0.0.tgz", @@ -7424,6 +8281,110 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/is-string": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.1.1.tgz", + "integrity": "sha512-BtEeSsoaQjlSPBemMQIrY1MY0uM6vnS1g5fmufYOtnxLGUZM2178PKbhsk7Ffv58IX+ZtcvoGwccYsh0PglkAA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-symbol": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.1.1.tgz", + "integrity": "sha512-9gGx6GTtCQM73BgmHQXfDmLtfjjTUDSyoxTCbp5WtoixAhfgsDirWIcVQ/IHpvI5Vgd5i/J5F7B9cN/WlVbC/w==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "has-symbols": "^1.1.0", + "safe-regex-test": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-typed-array": { + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.15.tgz", + "integrity": "sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "which-typed-array": "^1.1.16" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-weakmap": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/is-weakmap/-/is-weakmap-2.0.2.tgz", + "integrity": "sha512-K5pXYOm9wqY1RgjpL3YTkF39tni1XajUIkawTLUo9EZEVUFga5gSQJF8nNS7ZwJQ02y+1YCNYcMh+HIf1ZqE+w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-weakref": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-weakref/-/is-weakref-1.1.1.tgz", + "integrity": "sha512-6i9mGWSlqzNMEqpCp93KwRS1uUOodk2OJ6b+sq7ZPDSy2WuI5NFIxp/254TytR8ftefexkWn5xNiHUNpPOfSew==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-weakset": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/is-weakset/-/is-weakset-2.0.4.tgz", + "integrity": "sha512-mfcwb6IzQyOKTs84CQMrOwW4gQcaTOAWJ0zzJCl2WSPDrWk/OzDaImWFH3djXhb24g4eudZfLRozAvPGw4d9hQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "get-intrinsic": "^1.2.6" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/isarray": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", + "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==", + "dev": true, + "license": "MIT" + }, "node_modules/isexe": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", @@ -8111,6 +9072,16 @@ "url": "https://github.com/sponsors/wooorm" } }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, "node_modules/mdast-util-find-and-replace": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/mdast-util-find-and-replace/-/mdast-util-find-and-replace-3.0.2.tgz", @@ -9358,6 +10329,103 @@ "node": ">= 6" } }, + "node_modules/object-inspect": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object-keys": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", + "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/object.assign": { + "version": "4.1.7", + "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.7.tgz", + "integrity": "sha512-nK28WOo+QIjBkDduTINE4JkF/UJJKyf2EJxvJKfblDpyg0Q+pkOHNTL0Qwy6NP6FhE/EnzV73BxxqcJaXY9anw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0", + "has-symbols": "^1.1.0", + "object-keys": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object.fromentries": { + "version": "2.0.8", + "resolved": "https://registry.npmjs.org/object.fromentries/-/object.fromentries-2.0.8.tgz", + "integrity": "sha512-k6E21FzySsSK5a21KRADBd/NGneRegFO5pLHfdQLpRDETUNJueLXs3WCzyQ3tFRDYgbq3KHGXfTbi2bs8WQ6rQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.2", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object.groupby": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/object.groupby/-/object.groupby-1.0.3.tgz", + "integrity": "sha512-+Lhy3TQTuzXI5hevh8sBGqbmurHbbIjAi0Z4S63nthVLmLxfbj4T54a4CfZrXIrt9iP4mVAPYMo/v99taj3wjQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/object.values": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/object.values/-/object.values-1.2.1.tgz", + "integrity": "sha512-gXah6aZrcUxjWg2zR2MwouP2eHlCBzdV4pygudehaKXSGW4v2AsRQUK+lwwXhii6KFZcunEnmSUoYp5CXibxtA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/ohash": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/ohash/-/ohash-1.1.4.tgz", @@ -9406,6 +10474,24 @@ "dev": true, "license": "MIT" }, + "node_modules/own-keys": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/own-keys/-/own-keys-1.0.1.tgz", + "integrity": "sha512-qFOyK5PjiWZd+QQIh+1jhdb9LpxTF0qs7Pm8o5QHYZ0M3vKqSqzsZaEB6oWlxZ+q2sJBMI/Ktgd2N5ZwQoRHfg==", + "dev": true, + "license": "MIT", + "dependencies": { + "get-intrinsic": "^1.2.6", + "object-keys": "^1.1.1", + "safe-push-apply": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/p-limit": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", @@ -9663,6 +10749,16 @@ "pathe": "^1.1.2" } }, + "node_modules/possible-typed-array-names": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz", + "integrity": "sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, "node_modules/postcss": { "version": "8.4.49", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.49.tgz", @@ -10128,53 +11224,6 @@ "react": ">=18" } }, - "node_modules/react-remove-scroll": { - "version": "2.6.2", - "resolved": "https://registry.npmjs.org/react-remove-scroll/-/react-remove-scroll-2.6.2.tgz", - "integrity": "sha512-KmONPx5fnlXYJQqC62Q+lwIeAk64ws/cUw6omIumRzMRPqgnYqhSSti99nbj0Ry13bv7dF+BKn7NB+OqkdZGTw==", - "license": "MIT", - "dependencies": { - "react-remove-scroll-bar": "^2.3.7", - "react-style-singleton": "^2.2.1", - "tslib": "^2.1.0", - "use-callback-ref": "^1.3.3", - "use-sidecar": "^1.1.2" - }, - "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", @@ -10251,28 +11300,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", @@ -10334,6 +11361,29 @@ "node": ">=8" } }, + "node_modules/reflect.getprototypeof": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.10.tgz", + "integrity": "sha512-00o4I+DVrefhv+nX0ulyi3biSHCPDe+yLv5o/p6d/UVlirijB8E16FtfwSAi4g3tcqrQ4lRAqQSoFEZJehYEcw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.9", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0", + "get-intrinsic": "^1.2.7", + "get-proto": "^1.0.1", + "which-builtin-type": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/refractor": { "version": "3.6.0", "resolved": "https://registry.npmjs.org/refractor/-/refractor-3.6.0.tgz", @@ -10456,6 +11506,27 @@ "integrity": "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==", "license": "MIT" }, + "node_modules/regexp.prototype.flags": { + "version": "1.5.4", + "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.4.tgz", + "integrity": "sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-errors": "^1.3.0", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "set-function-name": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/remark-gfm": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/remark-gfm/-/remark-gfm-4.0.0.tgz", @@ -10569,6 +11640,16 @@ "node": ">=4" } }, + "node_modules/resolve-pkg-maps": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz", + "integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" + } + }, "node_modules/restore-cursor": { "version": "5.1.0", "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-5.1.0.tgz", @@ -10702,6 +11783,61 @@ "queue-microtask": "^1.2.2" } }, + "node_modules/safe-array-concat": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/safe-array-concat/-/safe-array-concat-1.1.3.tgz", + "integrity": "sha512-AURm5f0jYEOydBj7VQlVvDrjeFgthDdEF5H1dP+6mNpoXOMo1quQqJ4wvJDyRZ9+pO3kGWoOdmV08cSv2aJV6Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.2", + "get-intrinsic": "^1.2.6", + "has-symbols": "^1.1.0", + "isarray": "^2.0.5" + }, + "engines": { + "node": ">=0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/safe-push-apply": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/safe-push-apply/-/safe-push-apply-1.0.0.tgz", + "integrity": "sha512-iKE9w/Z7xCzUMIZqdBsp6pEQvwuEebH4vdpjcDWnyzaI6yl6O9FHvVpmGelvEHNsoY6wGblkxR6Zty/h00WiSA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "isarray": "^2.0.5" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/safe-regex-test": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.1.0.tgz", + "integrity": "sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "is-regex": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/safer-buffer": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", @@ -10738,30 +11874,155 @@ "semver": "bin/semver.js" } }, - "node_modules/set-cookie-parser": { - "version": "2.7.1", - "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.1.tgz", - "integrity": "sha512-IOc8uWeOZgnb3ptbCURJWNjWUPcO3ZnTTdzsurqERrP6nPyv+paC55vJM0LpOlT2ne+Ix+9+CRG1MNLlyZ4GjQ==" + "node_modules/set-cookie-parser": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.1.tgz", + "integrity": "sha512-IOc8uWeOZgnb3ptbCURJWNjWUPcO3ZnTTdzsurqERrP6nPyv+paC55vJM0LpOlT2ne+Ix+9+CRG1MNLlyZ4GjQ==" + }, + "node_modules/set-function-length": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", + "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.4", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/set-function-name": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/set-function-name/-/set-function-name-2.0.2.tgz", + "integrity": "sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", + "functions-have-names": "^1.2.3", + "has-property-descriptors": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/set-proto": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/set-proto/-/set-proto-1.0.0.tgz", + "integrity": "sha512-RJRdvCo6IAnPdsvP/7m6bsQqNnn1FCBX5ZNtFL98MmFF/4xAIJTIg1YbHW5DC2W5SKZanrC6i4HsJqlajw/dZw==", + "dev": true, + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/side-channel": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", + "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3", + "side-channel-list": "^1.0.0", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", + "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } }, - "node_modules/shebang-command": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", - "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "dev": true, "license": "MIT", "dependencies": { - "shebang-regex": "^3.0.0" + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" }, "engines": { - "node": ">=8" + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/shebang-regex": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", - "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "dev": true, "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, "engines": { - "node": ">=8" + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, "node_modules/siginfo": { @@ -10880,6 +12141,13 @@ "url": "https://github.com/sponsors/wooorm" } }, + "node_modules/stable-hash": { + "version": "0.0.4", + "resolved": "https://registry.npmjs.org/stable-hash/-/stable-hash-0.0.4.tgz", + "integrity": "sha512-LjdcbuBeLcdETCrPn9i8AYAZ1eCtu4ECAWtP7UleOiZ9LzVxRzzUZEoZ8zB24nhkQnDWyET0I+3sWokSDS3E7g==", + "dev": true, + "license": "MIT" + }, "node_modules/stackback": { "version": "0.0.2", "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", @@ -10996,6 +12264,65 @@ "node": ">=8" } }, + "node_modules/string.prototype.trim": { + "version": "1.2.10", + "resolved": "https://registry.npmjs.org/string.prototype.trim/-/string.prototype.trim-1.2.10.tgz", + "integrity": "sha512-Rs66F0P/1kedk5lyYyH9uBzuiI/kNRmwJAR9quK6VOtIpZ2G+hMZd+HQbbv25MgCA6gEffoMZYxlTod4WcdrKA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.2", + "define-data-property": "^1.1.4", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.5", + "es-object-atoms": "^1.0.0", + "has-property-descriptors": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/string.prototype.trimend": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.9.tgz", + "integrity": "sha512-G7Ok5C6E/j4SGfyLCloXTrngQIQU3PWtXGst3yM7Bea9FRURf1S42ZHlZZtsNque2FN2PoUhfZXYLNWwEr4dLQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.2", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/string.prototype.trimstart": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.8.tgz", + "integrity": "sha512-UXSH262CSZY1tfu3G3Secr6uGLCFVPMhIqHjlgCUtCCcgihYc/xKs9djMTMUOb2j1mVSeU8EU6NWc/iQKU6Gfg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/stringify-entities": { "version": "4.0.4", "resolved": "https://registry.npmjs.org/stringify-entities/-/stringify-entities-4.0.4.tgz", @@ -11047,6 +12374,16 @@ "node": ">=8" } }, + "node_modules/strip-bom": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", + "integrity": "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, "node_modules/strip-final-newline": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-3.0.0.tgz", @@ -11589,6 +12926,32 @@ } } }, + "node_modules/tsconfig-paths": { + "version": "3.15.0", + "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-3.15.0.tgz", + "integrity": "sha512-2Ac2RgzDe/cn48GvOe3M+o82pEFewD3UPbyoUHHdKasHwJKjds4fLXWf/Ux5kATBKN20oaFGu+jbElp1pos0mg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/json5": "^0.0.29", + "json5": "^1.0.2", + "minimist": "^1.2.6", + "strip-bom": "^3.0.0" + } + }, + "node_modules/tsconfig-paths/node_modules/json5": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.2.tgz", + "integrity": "sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==", + "dev": true, + "license": "MIT", + "dependencies": { + "minimist": "^1.2.0" + }, + "bin": { + "json5": "lib/cli.js" + } + }, "node_modules/tslib": { "version": "2.8.1", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", @@ -11626,6 +12989,84 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/typed-array-buffer": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.3.tgz", + "integrity": "sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "is-typed-array": "^1.1.14" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/typed-array-byte-length": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/typed-array-byte-length/-/typed-array-byte-length-1.0.3.tgz", + "integrity": "sha512-BaXgOuIxz8n8pIq3e7Atg/7s+DpiYrxn4vdot3w9KbnBhcRQq6o3xemQdIfynqSeXeDrF32x+WvfzmOjPiY9lg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "for-each": "^0.3.3", + "gopd": "^1.2.0", + "has-proto": "^1.2.0", + "is-typed-array": "^1.1.14" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/typed-array-byte-offset": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/typed-array-byte-offset/-/typed-array-byte-offset-1.0.4.tgz", + "integrity": "sha512-bTlAFB/FBYMcuX81gbL4OcpH5PmlFHqlCCpAl8AlEzMz5k53oNDvN8p1PNOWLEmI2x4orp3raOFB51tv9X+MFQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.8", + "for-each": "^0.3.3", + "gopd": "^1.2.0", + "has-proto": "^1.2.0", + "is-typed-array": "^1.1.15", + "reflect.getprototypeof": "^1.0.9" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/typed-array-length": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/typed-array-length/-/typed-array-length-1.0.7.tgz", + "integrity": "sha512-3KS2b+kL7fsuk/eJZ7EQdnEmQoaho/r6KUef7hxvltNA5DR8NAUM+8wJMbJyZ4G9/7i3v5zPBIMN5aybAh2/Jg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "for-each": "^0.3.3", + "gopd": "^1.0.1", + "is-typed-array": "^1.1.13", + "possible-typed-array-names": "^1.0.0", + "reflect.getprototypeof": "^1.0.6" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/typescript": { "version": "5.7.3", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.7.3.tgz", @@ -11663,6 +13104,31 @@ "typescript": ">=4.8.4 <5.8.0" } }, + "node_modules/typescript-eslint/node_modules/@typescript-eslint/parser": { + "version": "8.19.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.19.0.tgz", + "integrity": "sha512-6M8taKyOETY1TKHp0x8ndycipTVgmp4xtg5QpEZzXxDhNvvHOJi5rLRkLr8SK3jTgD5l4fTlvBiRdfsuWydxBw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/scope-manager": "8.19.0", + "@typescript-eslint/types": "8.19.0", + "@typescript-eslint/typescript-estree": "8.19.0", + "@typescript-eslint/visitor-keys": "8.19.0", + "debug": "^4.3.4" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <5.8.0" + } + }, "node_modules/ufo": { "version": "1.5.4", "resolved": "https://registry.npmjs.org/ufo/-/ufo-1.5.4.tgz", @@ -11684,6 +13150,25 @@ "node": ">=0.8.0" } }, + "node_modules/unbox-primitive": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.1.0.tgz", + "integrity": "sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "has-bigints": "^1.0.2", + "has-symbols": "^1.1.0", + "which-boxed-primitive": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/undici-types": { "version": "6.20.0", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.20.0.tgz", @@ -11840,49 +13325,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", @@ -12276,6 +13718,94 @@ "node": ">= 8" } }, + "node_modules/which-boxed-primitive": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/which-boxed-primitive/-/which-boxed-primitive-1.1.1.tgz", + "integrity": "sha512-TbX3mj8n0odCBFVlY8AxkqcHASw3L60jIuF8jFP78az3C2YhmGvqbHBpAjTRH2/xqYunrJ9g1jSyjCjpoWzIAA==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-bigint": "^1.1.0", + "is-boolean-object": "^1.2.1", + "is-number-object": "^1.1.1", + "is-string": "^1.1.1", + "is-symbol": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/which-builtin-type": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/which-builtin-type/-/which-builtin-type-1.2.1.tgz", + "integrity": "sha512-6iBczoX+kDQ7a3+YJBnh3T+KZRxM/iYNPXicqk66/Qfm1b93iu+yOImkg0zHbj5LNOcNv1TEADiZ0xa34B4q6Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "function.prototype.name": "^1.1.6", + "has-tostringtag": "^1.0.2", + "is-async-function": "^2.0.0", + "is-date-object": "^1.1.0", + "is-finalizationregistry": "^1.1.0", + "is-generator-function": "^1.0.10", + "is-regex": "^1.2.1", + "is-weakref": "^1.0.2", + "isarray": "^2.0.5", + "which-boxed-primitive": "^1.1.0", + "which-collection": "^1.0.2", + "which-typed-array": "^1.1.16" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/which-collection": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/which-collection/-/which-collection-1.0.2.tgz", + "integrity": "sha512-K4jVyjnBdgvc86Y6BkaLZEN933SwYOuBFkdmBu9ZfkcAbdVbpITnDmjvZ/aQjRXQrv5EPkTnD1s39GiiqbngCw==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-map": "^2.0.3", + "is-set": "^2.0.3", + "is-weakmap": "^2.0.2", + "is-weakset": "^2.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/which-typed-array": { + "version": "1.1.18", + "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.18.tgz", + "integrity": "sha512-qEcY+KJYlWyLH9vNbsr6/5j59AXk5ni5aakf8ldzBvGde6Iz4sxZGkJyWSAueTG7QhOvNRYb1lDdFmL5Td0QKA==", + "dev": true, + "license": "MIT", + "dependencies": { + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "for-each": "^0.3.3", + "gopd": "^1.2.0", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/why-is-node-running": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz", diff --git a/package.json b/package.json index 22be71a1..5b099c92 100644 --- a/package.json +++ b/package.json @@ -22,20 +22,15 @@ "dependencies": { "@hey-api/client-fetch": "^0.7.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-1", "@tanstack/react-query": "^5.64.1", "@tanstack/react-query-devtools": "^5.66.0", - "@types/prismjs": "^1.26.5", "@types/react-syntax-highlighter": "^15.5.13", "@untitled-ui/icons-react": "^0.1.4", "clsx": "^2.1.1", "date-fns": "^4.1.0", "fuse.js": "^7.0.0", "highlight.js": "^11.11.1", - "prismjs": "^1.29.0", "react": "19.0.0", "react-dom": "19.0.0", "react-markdown": "^9.0.1", @@ -61,12 +56,15 @@ "@types/node": "^22.10.1", "@types/react": "^19.0.2", "@types/react-dom": "^19.0.2", + "@typescript-eslint/parser": "^8.23.0", "@vitejs/plugin-react-swc": "^3.5.0", "@vitest/coverage-istanbul": "^2.1.8", "@vitest/expect": "^3.0.4", "@vitest/ui": "^2.1.4", "autoprefixer": "^10.4.20", "eslint": "^9.18.0", + "eslint-import-resolver-typescript": "^3.7.0", + "eslint-plugin-import": "^2.31.0", "eslint-plugin-react-hooks": "^5.1.0", "eslint-plugin-react-refresh": "^0.4.16", "eslint-plugin-tailwindcss": "3.17.5", diff --git a/src/App.test.tsx b/src/App.test.tsx index 4f1c8079..9ce99bd2 100644 --- a/src/App.test.tsx +++ b/src/App.test.tsx @@ -7,7 +7,6 @@ import userEvent from "@testing-library/user-event"; describe("App", () => { it("should render header", async () => { render(); - expect(screen.getByText(/toggle sidebar/i)).toBeVisible(); expect(screen.getByText("Settings")).toBeVisible(); expect(screen.getByText("Help")).toBeVisible(); expect(screen.getByRole("banner", { name: "App header" })).toBeVisible(); @@ -17,17 +16,17 @@ describe("App", () => { expect( screen.getByRole("menuitem", { name: /providers/i, - }), + }) ).toBeVisible(); expect( screen.getByRole("menuitem", { name: /certificate security/i, - }), + }) ).toBeVisible(); expect( screen.getByRole("menuitem", { name: /download/i, - }), + }) ).toBeVisible(); await userEvent.click(screen.getByText("Settings")); @@ -36,19 +35,19 @@ describe("App", () => { expect( screen.getByRole("menuitem", { name: /use with continue/i, - }), + }) ).toBeVisible(); expect( screen.getByRole("menuitem", { name: /use with copilot/i, - }), + }) ).toBeVisible(); expect( screen.getByRole("menuitem", { name: /documentation/i, - }), + }) ).toBeVisible(); const discordMenuItem = screen.getByRole("menuitem", { @@ -57,7 +56,7 @@ describe("App", () => { expect(discordMenuItem).toBeVisible(); expect(discordMenuItem).toHaveAttribute( "href", - "https://discord.gg/stacklok", + "https://discord.gg/stacklok" ); const githubMenuItem = screen.getByRole("menuitem", { @@ -66,7 +65,7 @@ describe("App", () => { expect(githubMenuItem).toBeVisible(); expect(githubMenuItem).toHaveAttribute( "href", - "https://github.com/stacklok/codegate", + "https://github.com/stacklok/codegate" ); const youtubeMenuItem = screen.getByRole("menuitem", { @@ -75,13 +74,13 @@ describe("App", () => { expect(youtubeMenuItem).toBeVisible(); expect(youtubeMenuItem).toHaveAttribute( "href", - "https://www.youtube.com/@Stacklok", + "https://www.youtube.com/@Stacklok" ); await userEvent.click(screen.getByText("Help")); await waitFor(() => - expect(screen.getByRole("link", { name: /codeGate/i })).toBeVisible(), + expect(screen.getByRole("link", { name: /codeGate/i })).toBeVisible() ); }); @@ -89,7 +88,7 @@ describe("App", () => { render(); await waitFor(() => - expect(screen.getByRole("link", { name: "CodeGate" })).toBeVisible(), + expect(screen.getByRole("link", { name: "CodeGate" })).toBeVisible() ); const workspaceSelectionButton = screen.getByRole("button", { @@ -103,8 +102,8 @@ describe("App", () => { expect( screen.getByRole("option", { name: /anotherworkspae/i, - }), - ).toBeVisible(), + }) + ).toBeVisible() ); }); }); diff --git a/src/App.tsx b/src/App.tsx index 254fa0d8..99ce893a 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,31 +1,16 @@ import { Header } from "./features/header/components/header"; -import { PromptList } from "./components/PromptList"; -import { useQueryGetWorkspaceMessages } from "./hooks/use-query-get-workspace-messages"; -import { Sidebar } from "./components/Sidebar"; import { useSse } from "./hooks/useSse"; import Page from "./Page"; -function App() { - const { data: prompts, isLoading } = useQueryGetWorkspaceMessages(); +export default function App() { useSse(); return ( -
- - - -
-
- -
- -
-
-
+ <> +
+
+ +
+ ); } - -export default App; diff --git a/src/components/AlertDetail.tsx b/src/components/AlertDetail.tsx index 10de3054..f41a58b1 100644 --- a/src/components/AlertDetail.tsx +++ b/src/components/AlertDetail.tsx @@ -1,6 +1,6 @@ -import { AlertConversation } from "@/api/generated"; -import { isAlertMalicious } from "@/features/alerts/lib/is-alert-malicious"; -import { isAlertSecret } from "@/features/alerts/lib/is-alert-secret"; +import { Alert } from "@/api/generated"; +import { isAlertMalicious } from "@/features/dashboard-messages/lib/is-alert-malicious"; +import { isAlertSecret } from "@/features/dashboard-messages/lib/is-alert-secret"; import { Markdown } from "./Markdown"; type MaliciousPkgType = { @@ -10,7 +10,7 @@ type MaliciousPkgType = { description: string; }; -export function AlertDetail({ alert }: { alert: AlertConversation }) { +export function AlertDetail({ alert }: { alert: Alert }) { if (alert.trigger_string === null) return "N/A"; if (isAlertSecret(alert) && typeof alert.trigger_string === "string") { return ( diff --git a/src/components/Error.tsx b/src/components/Error.tsx index 71a7ad5a..3df97ba0 100644 --- a/src/components/Error.tsx +++ b/src/components/Error.tsx @@ -6,7 +6,7 @@ export function Error() { return (
-
+
diff --git a/src/components/PromptList.tsx b/src/components/PromptList.tsx deleted file mode 100644 index 2e989ded..00000000 --- a/src/components/PromptList.tsx +++ /dev/null @@ -1,51 +0,0 @@ -import { Link } from "react-router-dom"; -import { - parsingPromptText, - groupPromptsByRelativeDate, - sanitizeQuestionPrompt, -} from "@/lib/utils"; -import { useCurrentPromptStore } from "@/hooks/useCurrentPromptStore"; -import clsx from "clsx"; -import { Conversation } from "@/api/generated"; - -export function PromptList({ prompts }: { prompts: Conversation[] }) { - const { currentPromptId, setCurrentPromptId } = useCurrentPromptStore(); - - const groupedPrompts = groupPromptsByRelativeDate(prompts); - - return ( -
- {Object.entries(groupedPrompts).map(([group, prompts]) => ( -
-

- {group} -

-
    - {prompts.map((prompt) => ( -
  • - setCurrentPromptId(prompt.chat_id)} - to={`/prompt/${prompt.chat_id}`} - className={clsx( - `text-secondary text-sm truncate hover:text-gray-500`, - { "font-bold": currentPromptId === prompt.chat_id }, - )} - > - {parsingPromptText( - sanitizeQuestionPrompt({ - question: - prompt.question_answers?.[0]?.question.message ?? "", - answer: - prompt.question_answers?.[0]?.answer?.message ?? "", - }), - prompt.conversation_timestamp, - )} - -
  • - ))} -
-
- ))} -
- ); -} diff --git a/src/components/Sidebar.tsx b/src/components/Sidebar.tsx deleted file mode 100644 index bd9bc002..00000000 --- a/src/components/Sidebar.tsx +++ /dev/null @@ -1,49 +0,0 @@ -import { - Sidebar as SidebarUI, - SidebarContent, - SidebarFooter, - SidebarGroup, - SidebarHeader, - SidebarMenu, - SidebarMenuItem, - SidebarMenuSkeleton, -} from "@/components/ui/sidebar"; - -export function Sidebar({ - children, - loading, -}: { - loading: boolean; - children: React.ReactNode; -}) { - if (loading) { - return ( - - - Prompt History - - - - {Array.from({ length: 20 }).map((_, index) => ( - - - - ))} - - - - - ); - } - - return ( - - - Prompt History - - - {children} - - - ); -} diff --git a/src/components/__tests__/PromptList.test.tsx b/src/components/__tests__/PromptList.test.tsx deleted file mode 100644 index 26982578..00000000 --- a/src/components/__tests__/PromptList.test.tsx +++ /dev/null @@ -1,93 +0,0 @@ -import { describe, expect, it } from "vitest"; -import { screen } from "@testing-library/react"; -import { PromptList } from "../PromptList"; -import mockedPrompts from "@/mocks/msw/fixtures/GET_MESSAGES.json"; -import { render } from "@/lib/test-utils"; -import { Conversation } from "@/api/generated"; - -const conversationTimestamp = "2025-01-02T14:19:58.024100Z"; -const prompt = mockedPrompts[0] as Conversation; - -const testCases: [string, { message: string; expected: RegExp | string }][] = [ - [ - "codegate cmd", - { - message: "codegate workspace -h", - expected: /codegate workspace -h/i, - }, - ], - [ - "render code with path", - { - message: "// Path: src/lib/utils.ts", - expected: /Prompt on filepath: src\/lib\/utils.ts/i, - }, - ], - [ - "render code with file path", - { - message: " ```tsx // filepath: /tests/my-test.tsx import", - expected: /Prompt on file\/\/ filepath: \/tests\/my-test.tsx/i, - }, - ], - [ - "render snippet", - { - message: - 'Compare this snippet from src/test.ts: // import { fakePkg } from "fake-pkg";', - expected: /Prompt from snippet compare this snippet from src\/test.ts:/i, - }, - ], - [ - "render default", - { - message: - "I know that this local proxy can forward requests to api.foo.com.\n\napi.foo.com will validate whether the connection si trusted using a certificate authority added on the local machine, specifically whether they allow SSL and x.509 basic policy.\n\nI need to be able to validate the proxys ability to make requests to api.foo.com. I only have access to code that can run in the browser. I can infer this based on a successful request. Be creative.", - expected: - "I know that this local proxy can forward requests to api.foo.com. api.foo.com will validate whether the connection si trusted using a certificate authority added on the local machine, specifically whether they allow SSL and x.509 basic policy. I need to be able to validate the proxys ability to make requests to api.foo.com. I only have access to code that can run in the browser. I can infer this based on a successful request. Be creative.", - }, - ], -]; - -describe("PromptList", () => { - it("render prompt", () => { - render(); - expect( - screen.getByRole("link", { - name: /server\.py do you see any security issue\?/i, - }), - ).toBeVisible(); - }); - - it.each(testCases)("%s", (_title: string, { message, expected }) => { - render( - , - ); - - expect( - screen.getByRole("link", { - name: expected, - }), - ).toBeVisible(); - }); -}); diff --git a/src/components/empty-state.tsx b/src/components/empty-state.tsx index 3fb1d3f8..6f2616c6 100644 --- a/src/components/empty-state.tsx +++ b/src/components/empty-state.tsx @@ -12,7 +12,7 @@ const actionsStyle = tv({ }, }); -export function Actions({ actions }: { actions: [ReactNode, ReactNode?] }) { +function Actions({ actions }: { actions: [ReactNode, ReactNode?] }) { return (
{actions}
); diff --git a/src/components/ui/sheet.tsx b/src/components/ui/sheet.tsx deleted file mode 100644 index 35ee8813..00000000 --- a/src/components/ui/sheet.tsx +++ /dev/null @@ -1,139 +0,0 @@ -"use client"; - -import * as React from "react"; -import * as SheetPrimitive from "@radix-ui/react-dialog"; - -import { tv } from "tailwind-variants"; -import { twMerge } from "tailwind-merge"; -import { XClose } from "@untitled-ui/icons-react"; - -const Sheet = SheetPrimitive.Root; - -const SheetTrigger = SheetPrimitive.Trigger; - -const SheetClose = SheetPrimitive.Close; - -const SheetPortal = SheetPrimitive.Portal; - -const SheetOverlay = React.forwardRef< - React.ElementRef, - React.ComponentPropsWithoutRef ->(({ className, ...props }, ref) => ( - -)); -SheetOverlay.displayName = SheetPrimitive.Overlay.displayName; - -const sheetVariants = tv({ - base: "fixed z-50 gap-4 bg-base p-6 shadow-lg transition ease-in-out data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:duration-300 data-[state=open]:duration-500", - variants: { - side: { - top: "inset-x-0 top-0 border-b data-[state=closed]:slide-out-to-top data-[state=open]:slide-in-from-top", - bottom: - "inset-x-0 bottom-0 border-t data-[state=closed]:slide-out-to-bottom data-[state=open]:slide-in-from-bottom", - left: "inset-y-0 left-0 h-full w-3/4 border-r data-[state=closed]:slide-out-to-left data-[state=open]:slide-in-from-left sm:max-w-sm", - right: - "inset-y-0 right-0 h-full w-3/4 border-l data-[state=closed]:slide-out-to-right data-[state=open]:slide-in-from-right sm:max-w-sm", - }, - }, - defaultVariants: { - side: "right", - }, -}); - -interface SheetContentProps - extends React.ComponentPropsWithoutRef { - side: "top" | "left" | "right" | "bottom"; -} - -const SheetContent = React.forwardRef< - React.ElementRef, - SheetContentProps ->(({ side = "right", className, children, ...props }, ref) => ( - - - - {children} - - - Close - - - -)); -SheetContent.displayName = SheetPrimitive.Content.displayName; - -const SheetHeader = ({ - className, - ...props -}: React.HTMLAttributes) => ( -
-); -SheetHeader.displayName = "SheetHeader"; - -const SheetFooter = ({ - className, - ...props -}: React.HTMLAttributes) => ( -
-); -SheetFooter.displayName = "SheetFooter"; - -const SheetTitle = React.forwardRef< - React.ElementRef, - React.ComponentPropsWithoutRef ->(({ className, ...props }, ref) => ( - -)); -SheetTitle.displayName = SheetPrimitive.Title.displayName; - -const SheetDescription = React.forwardRef< - React.ElementRef, - React.ComponentPropsWithoutRef ->(({ className, ...props }, ref) => ( - -)); -SheetDescription.displayName = SheetPrimitive.Description.displayName; - -export { - Sheet, - SheetPortal, - SheetOverlay, - SheetTrigger, - SheetClose, - SheetContent, - SheetHeader, - SheetFooter, - SheetTitle, - SheetDescription, -}; diff --git a/src/components/ui/sidebar.tsx b/src/components/ui/sidebar.tsx deleted file mode 100644 index 582b2a36..00000000 --- a/src/components/ui/sidebar.tsx +++ /dev/null @@ -1,552 +0,0 @@ -import * as React from "react"; -import { Slot } from "@radix-ui/react-slot"; - -import { useIsMobile } from "@/hooks/use-mobile"; -import { Sheet, SheetContent } from "@/components/ui/sheet"; -import { Tooltip, TooltipTrigger, Button, Skeleton } from "@stacklok/ui-kit"; -import { twMerge } from "tailwind-merge"; -import { tv } from "tailwind-variants"; -import { LayoutAlt02 } from "@untitled-ui/icons-react"; - -const SIDEBAR_COOKIE_NAME = "sidebar:state"; -const SIDEBAR_COOKIE_MAX_AGE = 60 * 60 * 24 * 7; -const SIDEBAR_WIDTH = "16rem"; -const SIDEBAR_WIDTH_MOBILE = "18rem"; -const SIDEBAR_WIDTH_ICON = "3rem"; -const SIDEBAR_KEYBOARD_SHORTCUT = "b"; - -type SidebarContext = { - state: "expanded" | "collapsed"; - open: boolean; - setOpen: (open: boolean) => void; - openMobile: boolean; - setOpenMobile: (open: boolean) => void; - isMobile: boolean; - toggleSidebar: () => void; -}; - -const SidebarContext = React.createContext(null); - -function useSidebar() { - const context = React.useContext(SidebarContext); - if (!context) { - throw new Error("useSidebar must be used within a SidebarProvider."); - } - - return context; -} - -const SidebarProvider = React.forwardRef< - HTMLDivElement, - React.ComponentProps<"div"> & { - defaultOpen?: boolean; - open?: boolean; - onOpenChange?: (open: boolean) => void; - } ->( - ( - { - defaultOpen = true, - open: openProp, - onOpenChange: setOpenProp, - className, - style, - children, - ...props - }, - ref, - ) => { - const isMobile = useIsMobile(); - const [openMobile, setOpenMobile] = React.useState(false); - - // This is the internal state of the sidebar. - // We use openProp and setOpenProp for control from outside the component. - const [_open, _setOpen] = React.useState(defaultOpen); - const open = openProp ?? _open; - const setOpen = React.useCallback( - (value: boolean | ((value: boolean) => boolean)) => { - const openState = typeof value === "function" ? value(open) : value; - if (setOpenProp) { - setOpenProp(openState); - } else { - _setOpen(openState); - } - - // This sets the cookie to keep the sidebar state. - document.cookie = `${SIDEBAR_COOKIE_NAME}=${openState}; path=/; max-age=${SIDEBAR_COOKIE_MAX_AGE}`; - }, - [setOpenProp, open], - ); - - // Helper to toggle the sidebar. - const toggleSidebar = React.useCallback(() => { - return isMobile - ? setOpenMobile((open) => !open) - : setOpen((open) => !open); - }, [isMobile, setOpen, setOpenMobile]); - - // Adds a keyboard shortcut to toggle the sidebar. - React.useEffect(() => { - const handleKeyDown = (event: KeyboardEvent) => { - if ( - event.key === SIDEBAR_KEYBOARD_SHORTCUT && - (event.metaKey || event.ctrlKey) - ) { - event.preventDefault(); - toggleSidebar(); - } - }; - - window.addEventListener("keydown", handleKeyDown); - return () => window.removeEventListener("keydown", handleKeyDown); - }, [toggleSidebar]); - - // We add a state so that we can do data-state="expanded" or "collapsed". - // This makes it easier to style the sidebar with Tailwind classes. - const state = open ? "expanded" : "collapsed"; - - const contextValue = React.useMemo( - () => ({ - state, - open, - setOpen, - isMobile, - openMobile, - setOpenMobile, - toggleSidebar, - }), - [ - state, - open, - setOpen, - isMobile, - openMobile, - setOpenMobile, - toggleSidebar, - ], - ); - - return ( - -
- {children} -
-
- ); - }, -); -SidebarProvider.displayName = "SidebarProvider"; - -const Sidebar = React.forwardRef< - HTMLDivElement, - React.ComponentProps<"div"> & { - side?: "left" | "right"; - variant?: "sidebar" | "floating" | "inset"; - collapsible?: "offcanvas" | "icon" | "none"; - } ->( - ( - { - side = "left", - variant = "sidebar", - collapsible = "offcanvas", - className, - children, - ...props - }, - ref, - ) => { - const { isMobile, state, openMobile, setOpenMobile } = useSidebar(); - - if (collapsible === "none") { - return ( -
- {children} -
- ); - } - - if (isMobile) { - return ( - - -
{children}
-
-
- ); - } - - return ( -
- {/* This is what handles the sidebar gap on desktop */} -
- -
- ); - }, -); -Sidebar.displayName = "Sidebar"; - -const SidebarTrigger = React.forwardRef< - React.ElementRef, - React.ComponentProps ->(({ onPress, ...props }, ref) => { - const { toggleSidebar } = useSidebar(); - - return ( - - ); -}); -SidebarTrigger.displayName = "SidebarTrigger"; - -const SidebarHeader = React.forwardRef< - HTMLDivElement, - React.ComponentProps<"div"> ->(({ className, ...props }, ref) => { - return ( -
- ); -}); -SidebarHeader.displayName = "SidebarHeader"; - -const SidebarFooter = React.forwardRef< - HTMLDivElement, - React.ComponentProps<"div"> ->(({ className, ...props }, ref) => { - return ( -
- ); -}); -SidebarFooter.displayName = "SidebarFooter"; - -const SidebarContent = React.forwardRef< - HTMLDivElement, - React.ComponentProps<"div"> ->(({ className, ...props }, ref) => { - return ( -
- ); -}); -SidebarContent.displayName = "SidebarContent"; - -const SidebarGroup = React.forwardRef< - HTMLDivElement, - React.ComponentProps<"div"> ->(({ className, ...props }, ref) => { - return ( -
- ); -}); -SidebarGroup.displayName = "SidebarGroup"; - -const SidebarMenu = React.forwardRef< - HTMLUListElement, - React.ComponentProps<"ul"> ->(({ className, ...props }, ref) => ( -
    -)); -SidebarMenu.displayName = "SidebarMenu"; - -const SidebarMenuItem = React.forwardRef< - HTMLLIElement, - React.ComponentProps<"li"> ->(({ className, ...props }, ref) => ( -
  • -)); -SidebarMenuItem.displayName = "SidebarMenuItem"; - -const sidebarMenuButtonVariants = tv({ - base: "peer/menu-button flex w-full items-center gap-2 overflow-hidden rounded-md p-2 text-left text-sm outline-none ring-brand-600 transition-[width,height,padding] hover:bg-brand-25 hover:text-secondary focus-visible:ring-2 active:bg-gray-25 active:text-secondary disabled:pointer-events-none disabled:opacity-50 group-has-[[data-sidebar=menu-action]]/menu-item:pr-8 aria-disabled:pointer-events-none aria-disabled:opacity-50 data-[active=true]:bg-gray-25 data-[active=true]:font-medium data-[active=true]:text-secondary data-[state=open]:hover:bg-brand-25 data-[state=open]:hover:text-secondary group-data-[collapsible=icon]:!size-8 group-data-[collapsible=icon]:!p-2 [&>span:last-child]:truncate [&>svg]:size-4 [&>svg]:shrink-0", - variants: { - variant: { - default: "hover:bg-brand-25 hover:text-secondary", - outline: - "bg-base shadow-[0_0_0_1px_hsl(var(--sidebar-border))] hover:bg-brand-25 hover:text-secondary hover:shadow-[0_0_0_1px_hsl(var(--sidebar-accent))]", - }, - size: { - default: "h-8 text-sm", - sm: "h-7 text-sm", - lg: "h-12 text-sm group-data-[collapsible=icon]:!p-0", - }, - }, - defaultVariants: { - variant: "default", - size: "default", - }, -}); - -const SidebarMenuButton = React.forwardRef< - HTMLButtonElement, - React.ComponentProps<"button"> & { - asChild?: boolean; - isActive?: boolean; - tooltip?: string; - } & { - variant: "default" | "outline"; - size: "default" | "sm" | "lg"; - } ->( - ( - { - asChild = false, - isActive = false, - variant = "default", - size = "default", - tooltip, - className, - ...props - }, - ref, - ) => { - const Comp = asChild ? Slot : "button"; - const { isMobile, state } = useSidebar(); - - const button = ( - - ); - - if (!tooltip) { - return button; - } - - return ( - - {button} - {tooltip} - - ); - }, -); -SidebarMenuButton.displayName = "SidebarMenuButton"; - -const SidebarMenuAction = React.forwardRef< - HTMLButtonElement, - React.ComponentProps<"button"> & { - asChild?: boolean; - showOnHover?: boolean; - } ->(({ className, asChild = false, showOnHover = false, ...props }, ref) => { - const Comp = asChild ? Slot : "button"; - - return ( - svg]:size-4 [&>svg]:shrink-0", - // Increases the hit area of the button on mobile. - "after:absolute after:-inset-2 after:md:hidden", - "peer-data-[size=sm]/menu-button:top-1", - "peer-data-[size=default]/menu-button:top-1.5", - "peer-data-[size=lg]/menu-button:top-2.5", - "group-data-[collapsible=icon]:hidden", - showOnHover && - "group-focus-within/menu-item:opacity-100 group-hover/menu-item:opacity-100 data-[state=open]:opacity-100 md:opacity-0", - className, - )} - {...props} - /> - ); -}); -SidebarMenuAction.displayName = "SidebarMenuAction"; - -const SidebarMenuBadge = React.forwardRef< - HTMLDivElement, - React.ComponentProps<"div"> ->(({ className, ...props }, ref) => ( -
    -)); -SidebarMenuBadge.displayName = "SidebarMenuBadge"; - -const SidebarMenuSkeleton = React.forwardRef< - HTMLDivElement, - React.ComponentProps<"div"> & { - showIcon?: boolean; - } ->(({ className, showIcon = false, ...props }, ref) => { - // Random width between 50 to 90%. - const width = React.useMemo(() => { - return `${Math.floor(Math.random() * 40) + 50}%`; - }, []); - - return ( -
    - {showIcon && ( - - )} - -
    - ); -}); -SidebarMenuSkeleton.displayName = "SidebarMenuSkeleton"; - -export { - Sidebar, - SidebarContent, - SidebarFooter, - SidebarGroup, - SidebarHeader, - SidebarMenu, - SidebarMenuAction, - SidebarMenuBadge, - SidebarMenuButton, - SidebarMenuItem, - SidebarMenuSkeleton, - SidebarProvider, - SidebarTrigger, - useSidebar, -}; diff --git a/src/features/alerts/components/__tests__/tabs-alerts.test.tsx b/src/features/alerts/components/__tests__/tabs-alerts.test.tsx deleted file mode 100644 index 913647d4..00000000 --- a/src/features/alerts/components/__tests__/tabs-alerts.test.tsx +++ /dev/null @@ -1,72 +0,0 @@ -import { server } from "@/mocks/msw/node"; -import { http, HttpResponse } from "msw"; -import { makeMockAlert } from "../../mocks/alert.mock"; -import { render, waitFor } from "@/lib/test-utils"; -import { TabsAlerts } from "../tabs-alerts"; - -test("shows correct count of all packages", async () => { - server.use( - http.get("*/workspaces/:name/alerts", () => { - return HttpResponse.json([ - ...Array.from({ length: 13 }).map(() => - makeMockAlert({ type: "secret" }), - ), - ...Array.from({ length: 13 }).map(() => - makeMockAlert({ type: "malicious" }), - ), - ]); - }), - ); - - const { getByRole } = render( - -
    foo
    -
    , - ); - - await waitFor(() => { - expect(getByRole("tab", { name: /all/i })).toHaveTextContent("26"); - }); -}); - -test("shows correct count of malicious packages", async () => { - server.use( - http.get("*/workspaces/:name/alerts", () => { - return HttpResponse.json( - Array.from({ length: 13 }).map(() => - makeMockAlert({ type: "malicious" }), - ), - ); - }), - ); - - const { getByRole } = render( - -
    foo
    -
    , - ); - - await waitFor(() => { - expect(getByRole("tab", { name: /malicious/i })).toHaveTextContent("13"); - }); -}); - -test("shows correct count of secret packages", async () => { - server.use( - http.get("*/workspaces/:name/alerts", () => { - return HttpResponse.json( - Array.from({ length: 13 }).map(() => makeMockAlert({ type: "secret" })), - ); - }), - ); - - const { getByRole } = render( - -
    foo
    -
    , - ); - - await waitFor(() => { - expect(getByRole("tab", { name: /secrets/i })).toHaveTextContent("13"); - }); -}); diff --git a/src/features/alerts/components/search-field-alerts.tsx b/src/features/alerts/components/search-field-alerts.tsx deleted file mode 100644 index 7a1a7a7b..00000000 --- a/src/features/alerts/components/search-field-alerts.tsx +++ /dev/null @@ -1,32 +0,0 @@ -import { - FieldGroup, - Input, - SearchField, - SearchFieldClearButton, -} from "@stacklok/ui-kit"; -import { useAlertsFilterSearchParams } from "../hooks/use-alerts-filter-search-params"; -import { SearchMd } from "@untitled-ui/icons-react"; - -export function SearchFieldAlerts({ className }: { className?: string }) { - const { setSearch, state } = useAlertsFilterSearchParams(); - - return ( - setSearch(value.toLowerCase().trim())} - className={className} - > - - } - /> - - - - ); -} diff --git a/src/features/alerts/lib/__tests__/is-alert-malicious.test.ts b/src/features/alerts/lib/__tests__/is-alert-malicious.test.ts deleted file mode 100644 index 89f076cf..00000000 --- a/src/features/alerts/lib/__tests__/is-alert-malicious.test.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { test, expect } from "vitest"; -import { isAlertConversationMalicious } from "../is-alert-malicious"; -import { makeMockAlert } from "../../mocks/alert.mock"; - -test("matches malicious alert", () => { - expect( - isAlertConversationMalicious(makeMockAlert({ type: "malicious" })), - ).toBe(true); -}); - -test("doesn't match secret", () => { - expect(isAlertConversationMalicious(makeMockAlert({ type: "secret" }))).toBe( - false, - ); -}); diff --git a/src/features/alerts/lib/__tests__/is-alert-secret.test.ts b/src/features/alerts/lib/__tests__/is-alert-secret.test.ts deleted file mode 100644 index a6d3e08c..00000000 --- a/src/features/alerts/lib/__tests__/is-alert-secret.test.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { test, expect } from "vitest"; -import { isAlertConversationSecret } from "../is-alert-secret"; -import { makeMockAlert } from "../../mocks/alert.mock"; - -test("matches secret alert", () => { - expect(isAlertConversationSecret(makeMockAlert({ type: "secret" }))).toBe( - true, - ); -}); - -test("doesn't match malicious", () => { - expect(isAlertConversationSecret(makeMockAlert({ type: "malicious" }))).toBe( - false, - ); -}); diff --git a/src/features/alerts/lib/does-alert-include-search.ts b/src/features/alerts/lib/does-alert-include-search.ts deleted file mode 100644 index 9ef55c75..00000000 --- a/src/features/alerts/lib/does-alert-include-search.ts +++ /dev/null @@ -1,75 +0,0 @@ -import { - AlertConversation, - Conversation, - V1GetWorkspaceAlertsResponse, -} from "@/api/generated"; -import { isAlertMalicious } from "./is-alert-malicious"; - -export function doesAlertIncludeSearch( - alert: V1GetWorkspaceAlertsResponse[number], - searchQuery: string | null, -): alert is AlertConversation { - if (alert == null) return false; - if (searchQuery === null) return true; - - const trigger_type: string = alert.trigger_type; - const trigger_string: string | null = - typeof alert.trigger_string === "string" ? alert.trigger_string : null; - - let malicious_pkg_name: string | null = null; - let malicious_pkg_type: string | null = null; - - if ( - alert.trigger_string !== null && - typeof alert.trigger_string === "object" && - "name" in alert.trigger_string && - typeof alert.trigger_string.name === "string" && - "type" in alert.trigger_string && - typeof alert.trigger_string.type === "string" - ) { - malicious_pkg_name = alert.trigger_string.name; - malicious_pkg_type = alert.trigger_string.type; - } - - return [ - trigger_type, - trigger_string, - malicious_pkg_name, - malicious_pkg_type, - ].some((i) => i?.toLowerCase().includes(searchQuery)); -} - -export function doesConversationIncludeSearch( - conversation: Conversation, - searchQuery: string | null, -): boolean { - if (conversation == null) return false; - if (searchQuery === null) return true; - - const trigger_types: string[] = - conversation.alerts?.map((a) => a.trigger_type) ?? []; - - const trigger_strings: string[] = - conversation.alerts - ?.map((a) => - typeof a.trigger_string === "string" ? a.trigger_string : "", - ) - .filter(Boolean) ?? []; - - const malicious_pkg_names: string[] = - conversation.alerts?.map((a) => - isAlertMalicious(a) ? (a.trigger_string as { name: string }).name : "", - ) ?? []; - - const malicious_pkg_types: string[] = - conversation.alerts?.map((a) => - isAlertMalicious(a) ? (a.trigger_string as { type: string }).type : "", - ) ?? []; - - return [ - ...trigger_types, - ...trigger_strings, - ...malicious_pkg_names, - ...malicious_pkg_types, - ].some((i) => i?.toLowerCase().includes(searchQuery)); -} diff --git a/src/features/alerts/lib/is-alert-conversation.ts b/src/features/alerts/lib/is-alert-conversation.ts deleted file mode 100644 index fdcf23cc..00000000 --- a/src/features/alerts/lib/is-alert-conversation.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { - AlertConversation, - V1GetWorkspaceAlertsResponse, -} from "@/api/generated"; - -export function isAlertConversation( - alert: V1GetWorkspaceAlertsResponse[number], -): alert is AlertConversation { - return Boolean( - alert?.conversation.question_answers.every( - (item) => item.answer && item.question, - ), - ); -} diff --git a/src/features/alerts/mocks/alert.mock.ts b/src/features/alerts/mocks/alert.mock.ts deleted file mode 100644 index 945b6aef..00000000 --- a/src/features/alerts/mocks/alert.mock.ts +++ /dev/null @@ -1,91 +0,0 @@ -import { - AlertConversation, - QuestionType, - TokenUsageAggregate, -} from "@/api/generated"; -import { faker } from "@faker-js/faker"; -import { TOKEN_USAGE_AGG } from "./token-usage.mock"; - -export const ALERT_SECRET_FIELDS = { - trigger_string: "foo", - trigger_type: "codegate-secrets", -} satisfies Pick; - -export const ALERT_MALICIOUS_FIELDS = { - trigger_string: { - name: "invokehttp", - type: "pypi", - status: "malicious", - description: "Python HTTP for Humans.", - }, - trigger_type: "codegate-context-retriever", -} satisfies Pick; - -const getBaseAlert = ({ - timestamp, - token_usage_agg, -}: { - timestamp: string; - token_usage_agg: TokenUsageAggregate | null; -}): Omit => ({ - conversation: { - question_answers: [ - { - question: { - message: "foo", - timestamp: timestamp, - message_id: faker.string.uuid(), - }, - answer: { - message: "bar", - timestamp: timestamp, - message_id: faker.string.uuid(), - }, - }, - ], - provider: "anthropic", - type: QuestionType.CHAT, - chat_id: faker.string.uuid(), - conversation_timestamp: timestamp, - token_usage_agg, - }, - alert_id: faker.string.uuid(), - code_snippet: null, - trigger_category: "critical", - timestamp: timestamp, -}); - -export const makeMockAlert = ({ - token_usage = false, - type, -}: { - token_usage?: boolean; - type: "secret" | "malicious"; -}): AlertConversation => { - const timestamp = faker.date.recent().toUTCString(); - - const base: Omit = - getBaseAlert({ - timestamp, - token_usage_agg: token_usage ? TOKEN_USAGE_AGG : null, - }); - - switch (type) { - case "malicious": { - const result: AlertConversation = { - ...base, - ...ALERT_MALICIOUS_FIELDS, - }; - - return result; - } - case "secret": { - const result: AlertConversation = { - ...base, - ...ALERT_SECRET_FIELDS, - }; - - return result; - } - } -}; diff --git a/src/features/alerts/components/__tests__/alerts-summary-malicious-pkg.test.tsx b/src/features/dashboard-alerts/components/__tests__/alerts-summary-malicious-pkg.test.tsx similarity index 64% rename from src/features/alerts/components/__tests__/alerts-summary-malicious-pkg.test.tsx rename to src/features/dashboard-alerts/components/__tests__/alerts-summary-malicious-pkg.test.tsx index ed1d728a..9fb6f799 100644 --- a/src/features/alerts/components/__tests__/alerts-summary-malicious-pkg.test.tsx +++ b/src/features/dashboard-alerts/components/__tests__/alerts-summary-malicious-pkg.test.tsx @@ -3,13 +3,14 @@ import { test } from "vitest"; import { http, HttpResponse } from "msw"; import { render, waitFor } from "@/lib/test-utils"; import { AlertsSummaryMaliciousPkg } from "../alerts-summary-malicious-pkg"; -import { makeMockAlert } from "../../mocks/alert.mock"; +import { mockAlert } from "../../../../mocks/msw/mockers/alert.mock"; +import { mswEndpoint } from "@/test/msw-endpoint"; test("shows correct count when there is a malicious alert", async () => { server.use( - http.get("*/api/v1/workspaces/:name/alerts", () => { - return HttpResponse.json([makeMockAlert({ type: "malicious" })]); - }), + http.get(mswEndpoint("/api/v1/workspaces/:workspace_name/alerts"), () => { + return HttpResponse.json([mockAlert({ type: "malicious" })]); + }) ); const { getByTestId } = render(); @@ -21,9 +22,9 @@ 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("*/api/v1/workspaces/:name/alerts", () => { - return HttpResponse.json([makeMockAlert({ type: "secret" })]); - }), + http.get(mswEndpoint("/api/v1/workspaces/:workspace_name/alerts"), () => { + return HttpResponse.json([mockAlert({ type: "secret" })]); + }) ); const { getByTestId } = render(); diff --git a/src/features/alerts/components/__tests__/alerts-summary-secrets.test.tsx b/src/features/dashboard-alerts/components/__tests__/alerts-summary-secrets.test.tsx similarity index 64% rename from src/features/alerts/components/__tests__/alerts-summary-secrets.test.tsx rename to src/features/dashboard-alerts/components/__tests__/alerts-summary-secrets.test.tsx index 5a99655b..e05751ae 100644 --- a/src/features/alerts/components/__tests__/alerts-summary-secrets.test.tsx +++ b/src/features/dashboard-alerts/components/__tests__/alerts-summary-secrets.test.tsx @@ -4,13 +4,14 @@ import { http, HttpResponse } from "msw"; import { render, waitFor } from "@/lib/test-utils"; import { AlertsSummaryMaliciousSecrets } from "../alerts-summary-secrets"; -import { makeMockAlert } from "../../mocks/alert.mock"; +import { mockAlert } from "../../../../mocks/msw/mockers/alert.mock"; +import { mswEndpoint } from "@/test/msw-endpoint"; test("shows correct count when there is a secret alert", async () => { server.use( - http.get("*/api/v1/workspaces/:name/alerts", () => { - return HttpResponse.json([makeMockAlert({ type: "secret" })]); - }), + http.get(mswEndpoint("/api/v1/workspaces/:workspace_name/alerts"), () => { + return HttpResponse.json([mockAlert({ type: "secret" })]); + }) ); const { getByTestId } = render(); @@ -22,9 +23,9 @@ 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("*/api/v1/workspaces/:name/alerts", () => { - return HttpResponse.json([makeMockAlert({ type: "malicious" })]); - }), + http.get(mswEndpoint("/api/v1/workspaces/:workspace_name/alerts"), () => { + return HttpResponse.json([mockAlert({ type: "malicious" })]); + }) ); const { getByTestId } = render(); diff --git a/src/features/alerts/components/__tests__/alerts-summary-workspace-token-usage.test.tsx b/src/features/dashboard-alerts/components/__tests__/alerts-summary-workspace-token-usage.test.tsx similarity index 71% rename from src/features/alerts/components/__tests__/alerts-summary-workspace-token-usage.test.tsx rename to src/features/dashboard-alerts/components/__tests__/alerts-summary-workspace-token-usage.test.tsx index 56466a27..d308740d 100644 --- a/src/features/alerts/components/__tests__/alerts-summary-workspace-token-usage.test.tsx +++ b/src/features/dashboard-alerts/components/__tests__/alerts-summary-workspace-token-usage.test.tsx @@ -4,33 +4,40 @@ import { http, HttpResponse } from "msw"; import { render, waitFor } from "@/lib/test-utils"; import { AlertsSummaryWorkspaceTokenUsage } from "../alerts-summary-workspace-token-usage"; -import { TOKEN_USAGE_AGG } from "../../mocks/token-usage.mock"; +import { TOKEN_USAGE_AGG } from "../../../../mocks/msw/mockers/token-usage.mock"; import { formatNumberCompact } from "@/lib/format-number"; +import { mswEndpoint } from "@/test/msw-endpoint"; test("shows correct count when there is token usage", async () => { server.use( - http.get("*/api/v1/workspaces/:name/token-usage", () => { - return HttpResponse.json(TOKEN_USAGE_AGG); - }), + http.get( + mswEndpoint("/api/v1/workspaces/:workspace_name/token-usage"), + () => { + return HttpResponse.json(TOKEN_USAGE_AGG); + } + ) ); const { getByTestId } = render(); await waitFor(() => { expect(getByTestId("usage-input-tokens")).toHaveTextContent( - formatNumberCompact(TOKEN_USAGE_AGG.token_usage.input_tokens), + formatNumberCompact(TOKEN_USAGE_AGG.token_usage.input_tokens) ); expect(getByTestId("usage-output-tokens")).toHaveTextContent( - formatNumberCompact(TOKEN_USAGE_AGG.token_usage.output_tokens), + formatNumberCompact(TOKEN_USAGE_AGG.token_usage.output_tokens) ); }); }); test("shows correct count when there is no token usage", async () => { server.use( - http.get("*/api/v1/workspaces/:name/token-usage", () => { - return HttpResponse.json({}); - }), + http.get( + mswEndpoint("/api/v1/workspaces/:workspace_name/token-usage"), + () => { + return HttpResponse.json({}); + } + ) ); const { getByTestId } = render(); diff --git a/src/features/alerts/components/alerts-summary-malicious-pkg.tsx b/src/features/dashboard-alerts/components/alerts-summary-malicious-pkg.tsx similarity index 99% rename from src/features/alerts/components/alerts-summary-malicious-pkg.tsx rename to src/features/dashboard-alerts/components/alerts-summary-malicious-pkg.tsx index f497c399..a1bd424b 100644 --- a/src/features/alerts/components/alerts-summary-malicious-pkg.tsx +++ b/src/features/dashboard-alerts/components/alerts-summary-malicious-pkg.tsx @@ -1,6 +1,7 @@ import { PackageX } from "@untitled-ui/icons-react"; -import { AlertsSummary } from "./alerts-summary"; + import { useQueryGetWorkspaceAlertsMaliciousPkg } from "../hooks/use-query-get-workspace-alerts-malicious-pkg"; +import { AlertsSummary } from "./alerts-summary"; export function AlertsSummaryMaliciousPkg() { const { data = [], isPending } = useQueryGetWorkspaceAlertsMaliciousPkg(); diff --git a/src/features/alerts/components/alerts-summary-secrets.tsx b/src/features/dashboard-alerts/components/alerts-summary-secrets.tsx similarity index 100% rename from src/features/alerts/components/alerts-summary-secrets.tsx rename to src/features/dashboard-alerts/components/alerts-summary-secrets.tsx diff --git a/src/features/alerts/components/alerts-summary-workspace-token-usage.tsx b/src/features/dashboard-alerts/components/alerts-summary-workspace-token-usage.tsx similarity index 100% rename from src/features/alerts/components/alerts-summary-workspace-token-usage.tsx rename to src/features/dashboard-alerts/components/alerts-summary-workspace-token-usage.tsx diff --git a/src/features/alerts/components/alerts-summary.tsx b/src/features/dashboard-alerts/components/alerts-summary.tsx similarity index 100% rename from src/features/alerts/components/alerts-summary.tsx rename to src/features/dashboard-alerts/components/alerts-summary.tsx diff --git a/src/features/alerts/hooks/use-query-get-workspace-alerts-malicious-pkg.ts b/src/features/dashboard-alerts/hooks/use-query-get-workspace-alerts-malicious-pkg.ts similarity index 76% rename from src/features/alerts/hooks/use-query-get-workspace-alerts-malicious-pkg.ts rename to src/features/dashboard-alerts/hooks/use-query-get-workspace-alerts-malicious-pkg.ts index 9baff9c7..097a5b56 100644 --- a/src/features/alerts/hooks/use-query-get-workspace-alerts-malicious-pkg.ts +++ b/src/features/dashboard-alerts/hooks/use-query-get-workspace-alerts-malicious-pkg.ts @@ -1,8 +1,8 @@ import { V1GetWorkspaceAlertsResponse } from "@/api/generated"; -import { isAlertConversationMalicious } from "../lib/is-alert-malicious"; +import { isAlertConversationMalicious } from "../../dashboard-messages/lib/is-alert-malicious"; import { useQueryGetWorkspaceAlerts } from "./use-query-get-workspace-alerts"; import { multiFilter } from "@/lib/multi-filter"; -import { isAlertCritical } from "../lib/is-alert-critical"; +import { isAlertCritical } from "../../dashboard-messages/lib/is-alert-critical"; // NOTE: This needs to be a stable function reference to enable memo-isation of // the select operation on each React re-render. diff --git a/src/features/alerts/hooks/use-query-get-workspace-alerts-secrets.ts b/src/features/dashboard-alerts/hooks/use-query-get-workspace-alerts-secrets.ts similarity index 76% rename from src/features/alerts/hooks/use-query-get-workspace-alerts-secrets.ts rename to src/features/dashboard-alerts/hooks/use-query-get-workspace-alerts-secrets.ts index e01937b2..88f21325 100644 --- a/src/features/alerts/hooks/use-query-get-workspace-alerts-secrets.ts +++ b/src/features/dashboard-alerts/hooks/use-query-get-workspace-alerts-secrets.ts @@ -1,8 +1,8 @@ import { V1GetWorkspaceAlertsResponse } from "@/api/generated"; -import { isAlertConversationSecret } from "../lib/is-alert-secret"; +import { isAlertConversationSecret } from "../../dashboard-messages/lib/is-alert-secret"; import { useQueryGetWorkspaceAlerts } from "./use-query-get-workspace-alerts"; import { multiFilter } from "@/lib/multi-filter"; -import { isAlertCritical } from "../lib/is-alert-critical"; +import { isAlertCritical } from "../../dashboard-messages/lib/is-alert-critical"; // NOTE: This needs to be a stable function reference to enable memo-isation of // the select operation on each React re-render diff --git a/src/features/alerts/hooks/use-query-get-workspace-alerts.ts b/src/features/dashboard-alerts/hooks/use-query-get-workspace-alerts.ts similarity index 100% rename from src/features/alerts/hooks/use-query-get-workspace-alerts.ts rename to src/features/dashboard-alerts/hooks/use-query-get-workspace-alerts.ts diff --git a/src/features/alerts/hooks/use-query-get-workspace-token-usage.ts b/src/features/dashboard-alerts/hooks/use-query-get-workspace-token-usage.ts similarity index 100% rename from src/features/alerts/hooks/use-query-get-workspace-token-usage.ts rename to src/features/dashboard-alerts/hooks/use-query-get-workspace-token-usage.ts diff --git a/src/features/dashboard-messages/components/__tests__/table-messages.alerts.test.tsx b/src/features/dashboard-messages/components/__tests__/table-messages.alerts.test.tsx new file mode 100644 index 00000000..54fd00ce --- /dev/null +++ b/src/features/dashboard-messages/components/__tests__/table-messages.alerts.test.tsx @@ -0,0 +1,82 @@ +import {} from "vitest"; +import { TableMessages } from "../table-messages"; +import { render, screen, waitFor } from "@/lib/test-utils"; +import { server } from "@/mocks/msw/node"; +import { http, HttpResponse } from "msw"; + +import { mswEndpoint } from "@/test/msw-endpoint"; +import { mockConversation } from "@/mocks/msw/mockers/conversation.mock"; + +it("shows zero in alerts counts when no alerts", async () => { + server.use( + http.get(mswEndpoint("/api/v1/workspaces/:workspace_name/messages"), () => + HttpResponse.json([ + mockConversation({ + alertsConfig: { numAlerts: 0 }, + }), + ]) + ) + ); + render(); + + await waitFor(() => { + expect(screen.queryByText(/loading.../i)).not.toBeInTheDocument(); + }); + + expect( + screen.getByRole("button", { + name: /malicious packages count/i, + }) + ).toHaveTextContent("0"); + expect( + screen.getByRole("button", { + name: /secrets count/i, + }) + ).toHaveTextContent("0"); +}); + +it("shows count of malicious alerts in row", async () => { + server.use( + http.get(mswEndpoint("/api/v1/workspaces/:workspace_name/messages"), () => + HttpResponse.json([ + mockConversation({ + alertsConfig: { numAlerts: 10, type: "malicious" }, + }), + ]) + ) + ); + render(); + + await waitFor(() => { + expect(screen.queryByText(/loading.../i)).not.toBeInTheDocument(); + }); + + expect( + screen.getByRole("button", { + name: /malicious packages count/i, + }) + ).toHaveTextContent("10"); +}); + +it("shows count of secret alerts in row", async () => { + server.use( + http.get(mswEndpoint("/api/v1/workspaces/:workspace_name/messages"), () => + HttpResponse.json([ + mockConversation({ + alertsConfig: { numAlerts: 10, type: "secret" }, + }), + ]) + ) + ); + render(); + + await waitFor(() => { + expect(screen.queryByText(/loading.../i)).not.toBeInTheDocument(); + }); + + expect( + screen.getByRole("button", { + name: /secrets count/i, + }) + ).toHaveTextContent("10"); +}); diff --git a/src/features/alerts/components/__tests__/table-alerts.empty-state.test.tsx b/src/features/dashboard-messages/components/__tests__/table-messages.empty-state.test.tsx similarity index 75% rename from src/features/alerts/components/__tests__/table-alerts.empty-state.test.tsx rename to src/features/dashboard-messages/components/__tests__/table-messages.empty-state.test.tsx index c6ce31f4..c48c30a1 100644 --- a/src/features/alerts/components/__tests__/table-alerts.empty-state.test.tsx +++ b/src/features/dashboard-messages/components/__tests__/table-messages.empty-state.test.tsx @@ -4,10 +4,11 @@ import { server } from "@/mocks/msw/node"; import { emptyStateStrings } from "../../constants/strings"; import { useSearchParams } from "react-router-dom"; import { delay, http, HttpHandler, HttpResponse } from "msw"; -import { makeMockAlert } from "../../mocks/alert.mock"; -import { AlertsFilterView } from "../../hooks/use-alerts-filter-search-params"; -import { TableAlerts } from "../table-alerts"; +import { mockAlert } from "../../../../mocks/msw/mockers/alert.mock"; +import { AlertsFilterView } from "../../hooks/use-messages-filter-search-params"; +import { TableMessages } from "../table-messages"; import { hrefs } from "@/lib/hrefs"; +import { mswEndpoint } from "@/test/msw-endpoint"; enum IllustrationTestId { ALERT = "illustration-alert", @@ -47,7 +48,7 @@ type TestCase = { vi.mock("react-router-dom", async () => { const original = await vi.importActual( - "react-router-dom", + "react-router-dom" ); return { ...original, @@ -58,7 +59,7 @@ vi.mock("react-router-dom", async () => { vi.mock("@stacklok/ui-kit", async () => { const original = await vi.importActual( - "@stacklok/ui-kit", + "@stacklok/ui-kit" ); return { ...original, @@ -78,7 +79,7 @@ const TEST_CASES: TestCase[] = [ { testDescription: "Loading state", handlers: [ - http.get("*/api/v1/workspaces", () => { + http.get(mswEndpoint("/api/v1/workspaces"), () => { delay("infinite"); }), ], @@ -96,7 +97,7 @@ const TEST_CASES: TestCase[] = [ { testDescription: "Only 1 workspace, no alerts", handlers: [ - http.get("*/api/v1/workspaces", () => { + http.get(mswEndpoint("/api/v1/workspaces"), () => { return HttpResponse.json({ workspaces: [ { @@ -106,14 +107,17 @@ const TEST_CASES: TestCase[] = [ ], }); }), - http.get("*/api/v1/workspaces/archive", () => { + http.get(mswEndpoint("/api/v1/workspaces/archive"), () => { return HttpResponse.json({ workspaces: [], }); }), - http.get("*/api/v1/workspaces/:name/alerts", () => { - return HttpResponse.json([]); - }), + http.get( + mswEndpoint("/api/v1/workspaces/:workspace_name/messages"), + () => { + return HttpResponse.json([]); + } + ), ], searchParams: { search: null, @@ -135,7 +139,7 @@ const TEST_CASES: TestCase[] = [ { testDescription: "No search results", handlers: [ - http.get("*/api/v1/workspaces", () => { + http.get(mswEndpoint("/api/v1/workspaces"), () => { return HttpResponse.json({ workspaces: [ { @@ -145,18 +149,19 @@ const TEST_CASES: TestCase[] = [ ], }); }), - http.get("*/api/v1/workspaces/archive", () => { + http.get(mswEndpoint("/api/v1/workspaces/archive"), () => { return HttpResponse.json({ workspaces: [], }); }), - http.get("*/api/v1/workspaces/:name/alerts", () => { - return HttpResponse.json( - Array.from({ length: 10 }, () => - makeMockAlert({ type: "malicious" }), - ), - ); - }), + http.get( + mswEndpoint("/api/v1/workspaces/:workspace_name/messages"), + () => { + return HttpResponse.json( + Array.from({ length: 10 }, () => mockAlert({ type: "malicious" })) + ); + } + ), ], searchParams: { search: "foo-bar", view: AlertsFilterView.ALL }, expected: { @@ -174,7 +179,7 @@ const TEST_CASES: TestCase[] = [ { testDescription: "No alerts, multiple workspaces", handlers: [ - http.get("*/api/v1/workspaces", () => { + http.get(mswEndpoint("/api/v1/workspaces"), () => { return HttpResponse.json({ workspaces: [ { @@ -188,22 +193,25 @@ const TEST_CASES: TestCase[] = [ ], }); }), - http.get("*/api/v1/workspaces/archive", () => { + http.get(mswEndpoint("/api/v1/workspaces/archive"), () => { return HttpResponse.json({ workspaces: [], }); }), - http.get("*/api/v1/workspaces/:name/alerts", () => { - return HttpResponse.json([]); - }), + http.get( + mswEndpoint("/api/v1/workspaces/:workspace_name/messages"), + () => { + return HttpResponse.json([]); + } + ), ], searchParams: { search: null, view: AlertsFilterView.ALL, }, expected: { - title: emptyStateStrings.title.noAlertsFoundWorkspace, - body: emptyStateStrings.body.alertsWillShowUpWhenWorkspace, + title: emptyStateStrings.title.noMessagesWorkspace, + body: emptyStateStrings.body.messagesWillShowUpWhenWorkspace, illustrationTestId: IllustrationTestId.DONE, actions: [ { @@ -217,7 +225,7 @@ const TEST_CASES: TestCase[] = [ { testDescription: 'Has alerts, view is "malicious"', handlers: [ - http.get("*/api/v1/workspaces", () => { + http.get(mswEndpoint("/api/v1/workspaces"), () => { return HttpResponse.json({ workspaces: [ { @@ -231,18 +239,19 @@ const TEST_CASES: TestCase[] = [ ], }); }), - http.get("*/api/v1/workspaces/archive", () => { + http.get(mswEndpoint("/api/v1/workspaces/archive"), () => { return HttpResponse.json({ workspaces: [], }); }), - http.get("*/api/v1/workspaces/:name/alerts", () => { - return HttpResponse.json( - Array.from({ length: 10 }).map(() => - makeMockAlert({ type: "secret" }), - ), - ); - }), + http.get( + mswEndpoint("/api/v1/workspaces/:workspace_name/messages"), + () => { + return HttpResponse.json( + Array.from({ length: 10 }).map(() => mockAlert({ type: "secret" })) + ); + } + ), ], searchParams: { view: AlertsFilterView.MALICIOUS, @@ -258,7 +267,7 @@ const TEST_CASES: TestCase[] = [ { testDescription: 'Has alerts, view is "secret"', handlers: [ - http.get("*/api/v1/workspaces", () => { + http.get(mswEndpoint("/api/v1/workspaces"), () => { return HttpResponse.json({ workspaces: [ { @@ -272,18 +281,21 @@ const TEST_CASES: TestCase[] = [ ], }); }), - http.get("*/api/v1/workspaces/archive", () => { + http.get(mswEndpoint("/api/v1/workspaces/archive"), () => { return HttpResponse.json({ workspaces: [], }); }), - http.get("*/api/v1/workspaces/:name/alerts", () => { - return HttpResponse.json( - Array.from({ length: 10 }).map(() => - makeMockAlert({ type: "malicious" }), - ), - ); - }), + http.get( + mswEndpoint("/api/v1/workspaces/:workspace_name/messages"), + () => { + return HttpResponse.json( + Array.from({ length: 10 }).map(() => + mockAlert({ type: "malicious" }) + ) + ); + } + ), ], searchParams: { view: AlertsFilterView.SECRETS, @@ -309,11 +321,11 @@ test.each(TEST_CASES)("$testDescription", async (testCase) => { () => {}, ]); - const { getByText, getByRole, getByTestId } = render(); + const { getByText, getByRole, getByTestId } = render(); await waitFor(() => { expect( - getByRole("heading", { level: 4, name: testCase.expected.title }), + getByRole("heading", { level: 4, name: testCase.expected.title }) ).toBeVisible(); expect(getByText(testCase.expected.body)).toBeVisible(); expect(getByTestId(testCase.expected.illustrationTestId)).toBeVisible(); diff --git a/src/features/dashboard-messages/components/__tests__/table-messages.pagination.test.tsx b/src/features/dashboard-messages/components/__tests__/table-messages.pagination.test.tsx new file mode 100644 index 00000000..837b5bd3 --- /dev/null +++ b/src/features/dashboard-messages/components/__tests__/table-messages.pagination.test.tsx @@ -0,0 +1,69 @@ +import {} from "vitest"; +import { TableMessages } from "../table-messages"; +import { render, screen, waitFor, within } from "@/lib/test-utils"; +import { server } from "@/mocks/msw/node"; +import { http, HttpResponse } from "msw"; + +import { mswEndpoint } from "@/test/msw-endpoint"; +import { mockConversation } from "@/mocks/msw/mockers/conversation.mock"; +import userEvent from "@testing-library/user-event"; + +it("only displays a limited number of items in the table", async () => { + server.use( + http.get(mswEndpoint("/api/v1/workspaces/:workspace_name/messages"), () => { + return HttpResponse.json( + Array.from({ length: 30 }).map(() => mockConversation()) + ); + }) + ); + + render(); + + await waitFor(() => { + expect( + within(screen.getByTestId("messages-table")).getAllByRole("row") + ).toHaveLength(16); + }); +}); + +it("allows pagination", async () => { + server.use( + http.get(mswEndpoint("/api/v1/workspaces/:workspace_name/messages"), () => { + return HttpResponse.json( + Array.from({ length: 35 }).map(() => mockConversation()) + ); + }) + ); + + render(); + + await waitFor( + async () => { + await userEvent.click(screen.getByRole("button", { name: /next/i })); + + expect( + within(screen.getByTestId("messages-table")).getAllByRole("row").length + ).toBeLessThan(16); + }, + { timeout: 5000 } + ); + + // on the last page, we cannot go further + expect(screen.getByRole("button", { name: /next/i })).toBeDisabled(); + + await userEvent.click(screen.getByRole("button", { name: /previous/i })); + expect(screen.getByRole("button", { name: /previous/i })).toBeEnabled(); + expect(screen.getByRole("button", { name: /next/i })).toBeEnabled(); + + await waitFor(async () => { + await userEvent.click(screen.getByRole("button", { name: /previous/i })); + + // once we reach the first page, we cannot paginate backwards anymore + expect(screen.getByRole("button", { name: /previous/i })).toBeDisabled(); + expect(screen.getByRole("button", { name: /next/i })).toBeEnabled(); + + expect( + within(screen.getByTestId("messages-table")).getAllByRole("row").length + ).toEqual(16); + }); +}); diff --git a/src/features/dashboard-messages/components/__tests__/table-messages.test.tsx b/src/features/dashboard-messages/components/__tests__/table-messages.test.tsx new file mode 100644 index 00000000..2bbd1d80 --- /dev/null +++ b/src/features/dashboard-messages/components/__tests__/table-messages.test.tsx @@ -0,0 +1,18 @@ +import { it, expect } from "vitest"; + +import { render, screen, waitFor } from "@/lib/test-utils"; +import { TABLE_MESSAGES_COLUMNS } from "../../constants/table-messages-columns"; + +import { TableMessages } from "../table-messages"; + +it.each(TABLE_MESSAGES_COLUMNS)("contains $children header", async (column) => { + render(); + + await waitFor(() => { + expect( + screen.getByRole("columnheader", { + name: column.children as string, + }) + ).toBeVisible(); + }); +}); diff --git a/src/features/alerts/components/__tests__/table-alerts.test.tsx b/src/features/dashboard-messages/components/__tests__/table-messages.token-usage.tsx similarity index 66% rename from src/features/alerts/components/__tests__/table-alerts.test.tsx rename to src/features/dashboard-messages/components/__tests__/table-messages.token-usage.tsx index 905f31af..2e7e30fa 100644 --- a/src/features/alerts/components/__tests__/table-alerts.test.tsx +++ b/src/features/dashboard-messages/components/__tests__/table-messages.token-usage.tsx @@ -1,11 +1,12 @@ import {} from "vitest"; -import { TableAlerts } from "../table-alerts"; +import { TableMessages } from "../table-messages"; import { render, waitFor } from "@/lib/test-utils"; import { server } from "@/mocks/msw/node"; import { http, HttpResponse } from "msw"; -import { makeMockAlert } from "../../mocks/alert.mock"; -import { TOKEN_USAGE_AGG } from "../../mocks/token-usage.mock"; +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"; vi.mock("@untitled-ui/icons-react", async () => { const original = await vi.importActual< @@ -28,14 +29,12 @@ const OUTPUT_TOKENS = test("renders token usage cell correctly", async () => { server.use( - http.get("*/workspaces/:name/alerts", () => { - return HttpResponse.json([ - makeMockAlert({ token_usage: true, type: "malicious" }), - ]); - }), + http.get(mswEndpoint("/api/v1/workspaces/:workspace_name/messages"), () => { + return HttpResponse.json([mockConversation({ withTokenUsage: true })]); + }) ); - const { getByRole, getByTestId, queryByText } = render(); + const { getByRole, getByTestId, queryByText } = render(); await waitFor(() => { expect(queryByText(/loading.../i)).not.toBeInTheDocument(); @@ -47,20 +46,18 @@ test("renders token usage cell correctly", async () => { expect( getByRole("gridcell", { name: `${formatNumberCompact(INPUT_TOKENS)} ${formatNumberCompact(OUTPUT_TOKENS)}`, - }), + }) ).toBeVisible(); }); test("renders N/A when token usage is missing", async () => { server.use( - http.get("*/workspaces/:name/alerts", () => { - return HttpResponse.json([ - makeMockAlert({ token_usage: false, type: "malicious" }), - ]); - }), + http.get(mswEndpoint("/api/v1/workspaces/:workspace_name/messages"), () => { + return HttpResponse.json([mockConversation({ withTokenUsage: false })]); + }) ); - const { getByText, queryByText } = render(); + const { getByText, queryByText } = render(); await waitFor(() => { expect(queryByText(/loading.../i)).not.toBeInTheDocument(); diff --git a/src/features/dashboard-messages/components/__tests__/tabs-messages.test.tsx b/src/features/dashboard-messages/components/__tests__/tabs-messages.test.tsx new file mode 100644 index 00000000..b78be57f --- /dev/null +++ b/src/features/dashboard-messages/components/__tests__/tabs-messages.test.tsx @@ -0,0 +1,95 @@ +import { server } from "@/mocks/msw/node"; +import { http, HttpResponse } from "msw"; +import { render, waitFor } from "@/lib/test-utils"; +import { TabsMessages } from "../tabs-messages"; +import { mswEndpoint } from "@/test/msw-endpoint"; +import { mockConversation } from "@/mocks/msw/mockers/conversation.mock"; + +test("shows correct count of all packages", async () => { + server.use( + http.get(mswEndpoint("/api/v1/workspaces/:workspace_name/messages"), () => { + return HttpResponse.json([ + ...Array.from({ length: 13 }).map(() => + mockConversation({ + alertsConfig: { + type: "secret", + numAlerts: 1, + }, + }) + ), + ...Array.from({ length: 13 }).map(() => + mockConversation({ + alertsConfig: { + type: "malicious", + numAlerts: 1, + }, + }) + ), + ]); + }) + ); + + const { getByRole } = render( + +
    foo
    +
    + ); + + await waitFor(() => { + expect(getByRole("tab", { name: /all/i })).toHaveTextContent("26"); + }); +}); + +test("shows correct count of malicious packages", async () => { + server.use( + http.get(mswEndpoint("/api/v1/workspaces/:workspace_name/messages"), () => { + return HttpResponse.json( + Array.from({ length: 13 }).map(() => + mockConversation({ + alertsConfig: { + type: "malicious", + numAlerts: 1, + }, + }) + ) + ); + }) + ); + + const { getByRole } = render( + +
    foo
    +
    + ); + + await waitFor(() => { + expect(getByRole("tab", { name: /malicious/i })).toHaveTextContent("13"); + }); +}); + +test("shows correct count of secret packages", async () => { + server.use( + http.get(mswEndpoint("/api/v1/workspaces/:workspace_name/messages"), () => { + return HttpResponse.json( + Array.from({ length: 13 }).map(() => + mockConversation({ + alertsConfig: { + type: "secret", + numAlerts: 1, + }, + }) + ) + ); + }) + ); + + const { getByRole } = render( + +
    foo
    +
    + ); + + await waitFor(() => { + expect(getByRole("tab", { name: /secrets/i })).toHaveTextContent("13"); + }); +}); diff --git a/src/features/dashboard-messages/components/search-field-messages.tsx b/src/features/dashboard-messages/components/search-field-messages.tsx new file mode 100644 index 00000000..b311bc50 --- /dev/null +++ b/src/features/dashboard-messages/components/search-field-messages.tsx @@ -0,0 +1,46 @@ +import { + FieldGroup, + Input, + Kbd, + SearchField, + SearchFieldClearButton, +} from "@stacklok/ui-kit"; +import { useMessagesFilterSearchParams } from "../hooks/use-messages-filter-search-params"; +import { SearchMd } from "@untitled-ui/icons-react"; +import { useKbdShortcuts } from "@/hooks/use-kbd-shortcuts"; +import { useRef } from "react"; + +export function SearchFieldMessages({ className }: { className?: string }) { + const { setSearch, state } = useMessagesFilterSearchParams(); + const ref = useRef(null); + useKbdShortcuts([ + [ + "/", + () => { + ref.current?.focus(); + }, + ], + ]); + + return ( + setSearch(value)} + className={className} + > + + } + /> + + / + + + ); +} diff --git a/src/features/alerts/components/table-alert-token-usage.tsx b/src/features/dashboard-messages/components/table-alert-token-usage.tsx similarity index 100% rename from src/features/alerts/components/table-alert-token-usage.tsx rename to src/features/dashboard-messages/components/table-alert-token-usage.tsx diff --git a/src/features/alerts/components/table-alerts-empty-state.tsx b/src/features/dashboard-messages/components/table-messages-empty-state.tsx similarity index 85% rename from src/features/alerts/components/table-alerts-empty-state.tsx rename to src/features/dashboard-messages/components/table-messages-empty-state.tsx index b8bf7e41..a126f080 100644 --- a/src/features/alerts/components/table-alerts-empty-state.tsx +++ b/src/features/dashboard-messages/components/table-messages-empty-state.tsx @@ -16,10 +16,10 @@ import { LinkExternal02 } from "@untitled-ui/icons-react"; import { useListAllWorkspaces } from "@/features/workspace/hooks/use-query-list-all-workspaces"; import { AlertsFilterView, - useAlertsFilterSearchParams, -} from "../hooks/use-alerts-filter-search-params"; -import { useQueryGetWorkspaceAlerts } from "../hooks/use-query-get-workspace-alerts"; + useMessagesFilterSearchParams, +} from "../hooks/use-messages-filter-search-params"; import { match, P } from "ts-pattern"; +import { useQueryGetWorkspaceMessages } from "@/hooks/use-query-get-workspace-messages"; function EmptyStateLoading() { return ( @@ -74,11 +74,11 @@ function EmptyStateSearch({ ); } -function EmptyStateNoAlertsInWorkspace() { +function EmptyStateNoMessagesInWorkspace() { return ( ({ isLoading, - hasWorkspaceAlerts: alerts.length > 0, + hasWorkspaceAlerts: messages.length > 0, hasMultipleWorkspaces: workspaces.filter((w) => w.name !== "default").length > 0, search: state.search || null, @@ -184,7 +184,7 @@ export function TableAlertsEmptyState() { search: P._, view: P._, }, - () => , + () => ) .with( { @@ -193,7 +193,7 @@ export function TableAlertsEmptyState() { search: P._, view: P._, }, - () => , + () => ) .with( { @@ -202,7 +202,7 @@ export function TableAlertsEmptyState() { search: P.string.select(), view: P._, }, - (search) => , + (search) => ) .with( { @@ -211,7 +211,7 @@ export function TableAlertsEmptyState() { search: P._, view: P.any, }, - () => , + () => ) .with( { @@ -220,7 +220,7 @@ export function TableAlertsEmptyState() { search: P._, view: AlertsFilterView.MALICIOUS, }, - () => , + () => ) .with( { @@ -228,7 +228,7 @@ export function TableAlertsEmptyState() { hasMultipleWorkspaces: P.any, view: AlertsFilterView.SECRETS, }, - () => , + () => ) .otherwise(() => ); } diff --git a/src/features/alerts/components/table-alerts.tsx b/src/features/dashboard-messages/components/table-messages.tsx similarity index 79% rename from src/features/alerts/components/table-alerts.tsx rename to src/features/dashboard-messages/components/table-messages.tsx index cc2f32f4..b6f2c890 100644 --- a/src/features/alerts/components/table-alerts.tsx +++ b/src/features/dashboard-messages/components/table-messages.tsx @@ -16,15 +16,18 @@ import { Alert, Conversation, QuestionType } from "@/api/generated"; import { useClientSidePagination } from "@/hooks/useClientSidePagination"; import { TableAlertTokenUsage } from "./table-alert-token-usage"; -import { useAlertsFilterSearchParams } from "../hooks/use-alerts-filter-search-params"; +import { useMessagesFilterSearchParams } from "../hooks/use-messages-filter-search-params"; import { Key01, PackageX } from "@untitled-ui/icons-react"; -import { TableAlertsEmptyState } from "./table-alerts-empty-state"; -import { ComponentProps } from "react"; +import { TableAlertsEmptyState } from "./table-messages-empty-state"; import { hrefs } from "@/lib/hrefs"; import { isAlertMalicious } from "../lib/is-alert-malicious"; import { isAlertSecret } from "../lib/is-alert-secret"; import { twMerge } from "tailwind-merge"; -import { useQueryGetWorkspaceMessagesTable } from "../hooks/use-query-get-workspace-alerts-table"; +import { useQueryGetWorkspaceMessagesTable } from "../hooks/use-query-get-workspace-messages-table"; +import { + TABLE_MESSAGES_COLUMNS, + TableMessagesColumn, +} from "../constants/table-messages-columns"; const getPromptText = (conversation: Conversation) => { return (conversation.question_answers[0]?.question?.message ?? "N/A") @@ -70,11 +73,12 @@ function AlertsSummaryCount({ return (
    - ) + ), )} -
- + + ); } diff --git a/src/routes/route-dashboard.tsx b/src/routes/route-dashboard.tsx index 94e8064a..fb291c9c 100644 --- a/src/routes/route-dashboard.tsx +++ b/src/routes/route-dashboard.tsx @@ -3,10 +3,11 @@ import { AlertsSummaryMaliciousPkg } from "@/features/dashboard-alerts/component import { AlertsSummaryWorkspaceTokenUsage } from "@/features/dashboard-alerts/components/alerts-summary-workspace-token-usage"; import { AlertsSummaryMaliciousSecrets } from "@/features/dashboard-alerts/components/alerts-summary-secrets"; import { TabsMessages } from "@/features/dashboard-messages/components/tabs-messages"; +import { PageContainer } from "@/components/page-container"; export function RouteDashboard() { return ( -
+
@@ -16,6 +17,6 @@ export function RouteDashboard() { -
+
); } diff --git a/src/routes/route-providers.tsx b/src/routes/route-providers.tsx index 7ea1aeea..6767f4e8 100644 --- a/src/routes/route-providers.tsx +++ b/src/routes/route-providers.tsx @@ -11,10 +11,11 @@ import { twMerge } from "tailwind-merge"; import { PlusSquare } from "@untitled-ui/icons-react"; import { TableProviders } from "@/features/providers/components/table-providers"; import { Outlet } from "react-router-dom"; +import { PageContainer } from "@/components/page-container"; export function RouteProvider({ className }: { className?: string }) { return ( - <> + Providers @@ -26,12 +27,12 @@ export function RouteProvider({ className }: { className?: string }) { - + - + ); } diff --git a/src/routes/route-workspace-creation.tsx b/src/routes/route-workspace-creation.tsx index 1229bcfc..0fcb5a8d 100644 --- a/src/routes/route-workspace-creation.tsx +++ b/src/routes/route-workspace-creation.tsx @@ -1,11 +1,12 @@ import { BreadcrumbHome } from "@/components/BreadcrumbHome"; +import { PageContainer } from "@/components/page-container"; import { WorkspaceCreation } from "@/features/workspace/components/workspace-creation"; import { WorkspaceHeading } from "@/features/workspace/components/workspace-heading"; import { Breadcrumbs, Breadcrumb } from "@stacklok/ui-kit"; export function RouteWorkspaceCreation() { return ( - <> + Manage Workspaces @@ -14,6 +15,6 @@ export function RouteWorkspaceCreation() { - + ); } diff --git a/src/routes/route-workspace.tsx b/src/routes/route-workspace.tsx index b42ddd24..21a50770 100644 --- a/src/routes/route-workspace.tsx +++ b/src/routes/route-workspace.tsx @@ -9,6 +9,7 @@ import { useArchivedWorkspaces } from "@/features/workspace/hooks/use-archived-w import { useRestoreWorkspaceButton } from "@/features/workspace/hooks/use-restore-workspace-button"; import { WorkspaceCustomInstructions } from "@/features/workspace/components/workspace-custom-instructions"; import { WorkspacePreferredModel } from "@/features/workspace/components/workspace-preferred-model"; +import { PageContainer } from "@/components/page-container"; function WorkspaceArchivedBanner({ name }: { name: string }) { const restoreButtonProps = useRestoreWorkspaceButton({ workspaceName: name }); @@ -37,7 +38,7 @@ export function RouteWorkspace() { }); return ( - <> + Manage Workspaces @@ -64,6 +65,6 @@ export function RouteWorkspace() { className="mb-4" /> - + ); } diff --git a/src/routes/route-workspaces.tsx b/src/routes/route-workspaces.tsx index 7ca21277..a60e6ef9 100644 --- a/src/routes/route-workspaces.tsx +++ b/src/routes/route-workspaces.tsx @@ -13,6 +13,7 @@ import { useKbdShortcuts } from "@/hooks/use-kbd-shortcuts"; import { useNavigate } from "react-router-dom"; import { hrefs } from "@/lib/hrefs"; import { PlusSquare } from "@untitled-ui/icons-react"; +import { PageContainer } from "@/components/page-container"; export function RouteWorkspaces() { const navigate = useNavigate(); @@ -20,7 +21,7 @@ export function RouteWorkspaces() { useKbdShortcuts([["c", () => navigate(hrefs.workspaces.create)]]); return ( - <> + Manage Workspaces @@ -39,6 +40,6 @@ export function RouteWorkspaces() { - + ); } From 7a15dc8dcaf36f2ce62b97b2035cb398f54851f2 Mon Sep 17 00:00:00 2001 From: alex-mcgovern Date: Mon, 10 Feb 2025 15:46:57 +0000 Subject: [PATCH 06/19] deduplicate alerts & render secrets in conversation summary --- src/components/heading.tsx | 22 ++++++++ src/components/page-container.tsx | 2 +- .../hooks/use-query-get-workspace-alerts.ts | 49 ++++++++++++++--- .../conversation-secrets-detected.tsx | 17 +++--- .../components/table-messages-empty-state.tsx | 13 ++++- .../components/workspace-heading.tsx | 17 ------ src/hooks/use-query-get-workspace-messages.ts | 52 ++++++++++++++++--- src/lib/dedupe-by-keys.ts | 43 +++++++++++++++ src/routes/route-chat.tsx | 16 +++--- src/routes/route-providers.tsx | 7 ++- src/routes/route-workspace-creation.tsx | 4 +- src/routes/route-workspace.tsx | 4 +- src/routes/route-workspaces.tsx | 6 +-- 13 files changed, 195 insertions(+), 57 deletions(-) create mode 100644 src/components/heading.tsx delete mode 100644 src/features/workspace/components/workspace-heading.tsx create mode 100644 src/lib/dedupe-by-keys.ts diff --git a/src/components/heading.tsx b/src/components/heading.tsx new file mode 100644 index 00000000..5b489d80 --- /dev/null +++ b/src/components/heading.tsx @@ -0,0 +1,22 @@ +import { Heading as UIKitHeading } from "@stacklok/ui-kit"; +import React, { ComponentProps } from "react"; + +export function PageHeading({ + title, + children, + level, +}: { + level: ComponentProps["level"]; + title: string; + children?: React.ReactNode; +}) { + return ( + + {title} + {children} + + ); +} diff --git a/src/components/page-container.tsx b/src/components/page-container.tsx index f11f78cc..18821485 100644 --- a/src/components/page-container.tsx +++ b/src/components/page-container.tsx @@ -2,7 +2,7 @@ import { ReactNode } from "react"; export function PageContainer({ children }: { children: ReactNode }) { return ( -
+
{children}
); 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 index fe980f6e..cb8141d4 100644 --- a/src/features/dashboard-alerts/hooks/use-query-get-workspace-alerts.ts +++ b/src/features/dashboard-alerts/hooks/use-query-get-workspace-alerts.ts @@ -1,11 +1,15 @@ import { + v1GetWorkspaceAlerts, V1GetWorkspaceAlertsData, V1GetWorkspaceAlertsResponse, } from "@/api/generated"; -import { v1GetWorkspaceAlertsOptions } from "@/api/generated/@tanstack/react-query.gen"; +import { v1GetWorkspaceAlertsQueryKey } from "@/api/generated/@tanstack/react-query.gen"; import { useQueryActiveWorkspaceName } from "@/hooks/use-query-active-workspace-name"; +import { dedupeByKeys } from "@/lib/dedupe-by-keys"; +import { isAlertCritical } from "@/lib/is-alert-critical"; import { getQueryCacheConfig } from "@/lib/react-query-utils"; import { useQuery } from "@tanstack/react-query"; +import { useCallback, useMemo } from "react"; export function useQueryGetWorkspaceAlerts({ select, @@ -20,11 +24,41 @@ export function useQueryGetWorkspaceAlerts({ isRefetching: isWorkspaceRefetching, } = useQueryActiveWorkspaceName(); - const options: V1GetWorkspaceAlertsData = { - path: { - workspace_name: activeWorkspaceName ?? "default", + const options: V1GetWorkspaceAlertsData = useMemo( + () => ({ + path: { + workspace_name: activeWorkspaceName ?? "default", + }, + }), + [activeWorkspaceName], + ); + + // Intentionally hand-composing the queryFn, as we have to iterate over every + // alert and remove any duplicates, which is an expensive operation. We should + // at least cache the result of this. + const queryFn = useCallback( + async ({ + queryKey, + signal, + }: { + queryKey: ReturnType; + signal: AbortSignal; + }) => { + const { data } = await v1GetWorkspaceAlerts({ + ...options, + ...queryKey[0], + signal, + throwOnError: true, + }); + + // Ugly de-duplication hack + return dedupeByKeys(data.filter(isAlertCritical), [ + "alert_id", + "code_snippet", + ]); }, - }; + [options], + ); const { isPending: isAlertsPending, @@ -33,7 +67,10 @@ export function useQueryGetWorkspaceAlerts({ isRefetching: isAlertsRefetching, ...rest } = useQuery({ - ...v1GetWorkspaceAlertsOptions(options), + // eslint-disable-next-line no-restricted-syntax + queryFn, + // eslint-disable-next-line no-restricted-syntax + queryKey: v1GetWorkspaceAlertsQueryKey(options), ...getQueryCacheConfig("5s"), select, }); diff --git a/src/features/dashboard-messages/components/conversation-secrets-detected.tsx b/src/features/dashboard-messages/components/conversation-secrets-detected.tsx index 7cc7f72c..47e5a679 100644 --- a/src/features/dashboard-messages/components/conversation-secrets-detected.tsx +++ b/src/features/dashboard-messages/components/conversation-secrets-detected.tsx @@ -17,8 +17,8 @@ function ConversationSecretsListItem({ }) { return (
  • - {title} - {value} + {title} + {value}
  • ); } @@ -31,16 +31,19 @@ export function ConversationSecretsDetected({ }: { alerts: (Omit & { trigger_string: string })[]; }) { + console.debug("👉 alerts:", alerts); return ( - {alerts.map((v) => { - console.debug("👉 v:", v); - const { key, redactedValue } = parseUnstructuredSecretsData(v) || {}; + {alerts.map((a) => { + const { key, redactedValue } = parseUnstructuredSecretsData(a) || {}; + + if (!key) return null; + return ( ); })} 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 efd2d5a7..0de9092b 100644 --- a/src/features/dashboard-messages/components/table-messages-empty-state.tsx +++ b/src/features/dashboard-messages/components/table-messages-empty-state.tsx @@ -168,6 +168,15 @@ export function TableAlertsEmptyState() { const isLoading = isMessagesLoading || isWorkspacesLoading; + console.log("foo", { + isLoading, + hasWorkspaceMessages: messages.length > 0, + hasMultipleWorkspaces: + workspaces.filter((w) => w.name !== "default").length > 0, + search: state.search || null, + view: state.view, + }); + return match({ isLoading, hasWorkspaceMessages: messages.length > 0, @@ -207,7 +216,7 @@ export function TableAlertsEmptyState() { .with( { hasWorkspaceMessages: false, - hasMultipleWorkspaces: true, + hasMultipleWorkspaces: P.any, search: P._, view: P.any, }, @@ -216,7 +225,7 @@ export function TableAlertsEmptyState() { .with( { hasWorkspaceMessages: true, - hasMultipleWorkspaces: true, + hasMultipleWorkspaces: P.any, search: P._, view: AlertsFilterView.MALICIOUS, }, diff --git a/src/features/workspace/components/workspace-heading.tsx b/src/features/workspace/components/workspace-heading.tsx deleted file mode 100644 index 880f9959..00000000 --- a/src/features/workspace/components/workspace-heading.tsx +++ /dev/null @@ -1,17 +0,0 @@ -import { Heading } from "@stacklok/ui-kit"; -import React from "react"; - -export function WorkspaceHeading({ - title, - children, -}: { - title: string; - children?: React.ReactNode; -}) { - return ( - - {title} - {children} - - ); -} diff --git a/src/hooks/use-query-get-workspace-messages.ts b/src/hooks/use-query-get-workspace-messages.ts index a8f543c6..a1f0f9bc 100644 --- a/src/hooks/use-query-get-workspace-messages.ts +++ b/src/hooks/use-query-get-workspace-messages.ts @@ -2,10 +2,13 @@ import { useQuery } from "@tanstack/react-query"; import { V1GetWorkspaceMessagesResponse, V1GetWorkspaceMessagesData, + v1GetWorkspaceMessages, } from "@/api/generated"; -import { v1GetWorkspaceMessagesOptions } from "@/api/generated/@tanstack/react-query.gen"; +import { v1GetWorkspaceMessagesQueryKey } from "@/api/generated/@tanstack/react-query.gen"; import { useQueryActiveWorkspaceName } from "@/hooks/use-query-active-workspace-name"; import { getQueryCacheConfig } from "@/lib/react-query-utils"; +import { useCallback, useMemo } from "react"; +import { dedupeByKeys } from "@/lib/dedupe-by-keys"; export const useQueryGetWorkspaceMessages = < T = V1GetWorkspaceMessagesResponse, @@ -16,14 +19,51 @@ export const useQueryGetWorkspaceMessages = < } = {}) => { const { data: activeWorkspaceName } = useQueryActiveWorkspaceName(); - const options: V1GetWorkspaceMessagesData = { - path: { - workspace_name: activeWorkspaceName ?? "default", + const options: V1GetWorkspaceMessagesData = useMemo( + () => ({ + path: { + workspace_name: activeWorkspaceName ?? "default", + }, + }), + [activeWorkspaceName], + ); + + // Intentionally hand-composing the queryFn, as we have to iterate over every + // alert and remove any duplicates, which is an expensive operation. We should + // at least cache the result of this. + const queryFn = useCallback( + async ({ + queryKey, + signal, + }: { + queryKey: ReturnType; + signal: AbortSignal; + }) => { + const { data } = await v1GetWorkspaceMessages({ + ...options, + ...queryKey[0], + signal, + throwOnError: true, + }); + + // Ugly de-duplication hack + return data.map((conversation) => + conversation.alerts && conversation.alerts?.length > 0 + ? { + ...conversation, + alerts: dedupeByKeys(conversation.alerts, ["id", "code_snippet"]), + } + : conversation, + ); }, - }; + [options], + ); return useQuery({ - ...v1GetWorkspaceMessagesOptions(options), + // eslint-disable-next-line no-restricted-syntax + queryFn, + // eslint-disable-next-line no-restricted-syntax + queryKey: v1GetWorkspaceMessagesQueryKey(options), ...getQueryCacheConfig("5s"), select: select, }); diff --git a/src/lib/dedupe-by-keys.ts b/src/lib/dedupe-by-keys.ts new file mode 100644 index 00000000..951e3c5d --- /dev/null +++ b/src/lib/dedupe-by-keys.ts @@ -0,0 +1,43 @@ +// eslint-disable-next-line @typescript-eslint/no-explicit-any +const deepEquals = (a: any, b: any): boolean => { + if (a === b) return true; + + if ( + typeof a !== "object" || + typeof b !== "object" || + a === null || + b === null + ) { + return false; + } + + const keysA = Object.keys(a); + const keysB = Object.keys(b); + + if (keysA.length !== keysB.length) return false; + + for (const key of keysA) { + if (!keysB.includes(key)) return false; + if (!deepEquals(a[key], b[key])) return false; + } + + return true; +}; + +export const dedupeByKeys = >( + items: (T | null | undefined)[], + keys: (keyof T)[], +) => + items.filter(Boolean).reduce((acc, curr) => { + const alreadySeen = acc.some((item) => + keys.some((key) => { + if (typeof item[key] === "string") return item[key] === curr?.[key]; + if (typeof item[key] === "object") + return deepEquals(item[key], curr?.[key]); + }), + ); + if (!alreadySeen && curr) { + acc.push(curr); + } + return acc; + }, [] as T[]); diff --git a/src/routes/route-chat.tsx b/src/routes/route-chat.tsx index a549f7df..b5df6ce0 100644 --- a/src/routes/route-chat.tsx +++ b/src/routes/route-chat.tsx @@ -12,16 +12,18 @@ import { Breadcrumb, Breadcrumbs, Heading } from "@stacklok/ui-kit"; import { BreadcrumbHome } from "@/components/BreadcrumbHome"; import { ConversationSummary } from "@/features/dashboard-messages/components/conversation-summary"; import { PageContainer } from "@/components/page-container"; -// import { isAlertSecret } from "@/lib/is-alert-secret"; -// import { ConversationSecretsDetected } from "@/features/dashboard-messages/components/conversation-secrets-detected"; +import { PageHeading } from "@/components/heading"; +import { isAlertSecret } from "@/lib/is-alert-secret"; +import { ConversationSecretsDetected } from "@/features/dashboard-messages/components/conversation-secrets-detected"; export function RouteChat() { const { id } = useParams(); const { data: conversation } = useQueryGetWorkspaceMessages({ select: (data) => data.find((m) => m.chat_id === id), }); + console.debug("👉 conversation:", conversation); - // const secrets = conversation?.alerts?.filter(isAlertSecret); + const secrets = conversation?.alerts?.filter(isAlertSecret); const title = conversation === undefined || @@ -41,7 +43,7 @@ export function RouteChat() { {title} - Conversation +
    0 ? ( + {secrets && secrets.length > 0 ? (
    Secrets protected ({secrets.length}) @@ -76,7 +78,7 @@ export function RouteChat() {
    - ) : null} */} + ) : null} {/* {alertDetail && ( diff --git a/src/routes/route-providers.tsx b/src/routes/route-providers.tsx index 6767f4e8..3f0459cd 100644 --- a/src/routes/route-providers.tsx +++ b/src/routes/route-providers.tsx @@ -2,7 +2,6 @@ import { BreadcrumbHome } from "@/components/BreadcrumbHome"; import { Breadcrumbs, Breadcrumb, - Heading, Card, LinkButton, CardBody, @@ -12,6 +11,7 @@ import { PlusSquare } from "@untitled-ui/icons-react"; import { TableProviders } from "@/features/providers/components/table-providers"; import { Outlet } from "react-router-dom"; import { PageContainer } from "@/components/page-container"; +import { PageHeading } from "@/components/heading"; export function RouteProvider({ className }: { className?: string }) { return ( @@ -20,12 +20,11 @@ export function RouteProvider({ className }: { className?: string }) { Providers - - Providers + Add Provider - + diff --git a/src/routes/route-workspace-creation.tsx b/src/routes/route-workspace-creation.tsx index 0fcb5a8d..ad897e5b 100644 --- a/src/routes/route-workspace-creation.tsx +++ b/src/routes/route-workspace-creation.tsx @@ -1,7 +1,7 @@ import { BreadcrumbHome } from "@/components/BreadcrumbHome"; import { PageContainer } from "@/components/page-container"; import { WorkspaceCreation } from "@/features/workspace/components/workspace-creation"; -import { WorkspaceHeading } from "@/features/workspace/components/workspace-heading"; +import { PageHeading } from "@/components/heading"; import { Breadcrumbs, Breadcrumb } from "@stacklok/ui-kit"; export function RouteWorkspaceCreation() { @@ -13,7 +13,7 @@ export function RouteWorkspaceCreation() { Create Workspace - + ); diff --git a/src/routes/route-workspace.tsx b/src/routes/route-workspace.tsx index 21a50770..b7f90dc3 100644 --- a/src/routes/route-workspace.tsx +++ b/src/routes/route-workspace.tsx @@ -1,7 +1,7 @@ import { BreadcrumbHome } from "@/components/BreadcrumbHome"; import { ArchiveWorkspace } from "@/features/workspace/components/archive-workspace"; -import { WorkspaceHeading } from "@/features/workspace/components/workspace-heading"; +import { PageHeading } from "@/components/heading"; import { WorkspaceName } from "@/features/workspace/components/workspace-name"; import { Alert, Breadcrumb, Breadcrumbs } from "@stacklok/ui-kit"; import { useParams } from "react-router-dom"; @@ -45,7 +45,7 @@ export function RouteWorkspace() { Workspace Settings - + {isArchived ? : null} diff --git a/src/routes/route-workspaces.tsx b/src/routes/route-workspaces.tsx index a60e6ef9..57f77bca 100644 --- a/src/routes/route-workspaces.tsx +++ b/src/routes/route-workspaces.tsx @@ -1,4 +1,4 @@ -import { WorkspaceHeading } from "@/features/workspace/components/workspace-heading"; +import { PageHeading } from "@/components/heading"; import { BreadcrumbHome } from "@/components/BreadcrumbHome"; import { Breadcrumb, @@ -27,7 +27,7 @@ export function RouteWorkspaces() { Manage Workspaces - + Create @@ -37,7 +37,7 @@ export function RouteWorkspaces() { C - + From 20158bc5504587deffc45a4ee03d696b39feefa6 Mon Sep 17 00:00:00 2001 From: alex-mcgovern Date: Mon, 10 Feb 2025 16:27:49 +0000 Subject: [PATCH 07/19] fix: dial back de-duplication logic --- .../hooks/use-query-get-workspace-alerts.ts | 5 +---- .../components/conversation-secrets-detected.tsx | 3 +-- .../components/table-messages-empty-state.tsx | 9 --------- src/hooks/use-query-get-workspace-messages.ts | 2 +- src/routes/route-chat.tsx | 1 - 5 files changed, 3 insertions(+), 17 deletions(-) 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 index cb8141d4..3ae9eb72 100644 --- a/src/features/dashboard-alerts/hooks/use-query-get-workspace-alerts.ts +++ b/src/features/dashboard-alerts/hooks/use-query-get-workspace-alerts.ts @@ -52,10 +52,7 @@ export function useQueryGetWorkspaceAlerts({ }); // Ugly de-duplication hack - return dedupeByKeys(data.filter(isAlertCritical), [ - "alert_id", - "code_snippet", - ]); + return dedupeByKeys(data.filter(isAlertCritical), ["alert_id"]); }, [options], ); diff --git a/src/features/dashboard-messages/components/conversation-secrets-detected.tsx b/src/features/dashboard-messages/components/conversation-secrets-detected.tsx index 47e5a679..ad68ef0a 100644 --- a/src/features/dashboard-messages/components/conversation-secrets-detected.tsx +++ b/src/features/dashboard-messages/components/conversation-secrets-detected.tsx @@ -16,7 +16,7 @@ function ConversationSecretsListItem({ value: ReactNode; }) { return ( -
  • +
  • {title} {value}
  • @@ -31,7 +31,6 @@ export function ConversationSecretsDetected({ }: { alerts: (Omit & { trigger_string: string })[]; }) { - console.debug("👉 alerts:", alerts); return ( {alerts.map((a) => { 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 0de9092b..9f97b2c7 100644 --- a/src/features/dashboard-messages/components/table-messages-empty-state.tsx +++ b/src/features/dashboard-messages/components/table-messages-empty-state.tsx @@ -168,15 +168,6 @@ export function TableAlertsEmptyState() { const isLoading = isMessagesLoading || isWorkspacesLoading; - console.log("foo", { - isLoading, - hasWorkspaceMessages: messages.length > 0, - hasMultipleWorkspaces: - workspaces.filter((w) => w.name !== "default").length > 0, - search: state.search || null, - view: state.view, - }); - return match({ isLoading, hasWorkspaceMessages: messages.length > 0, diff --git a/src/hooks/use-query-get-workspace-messages.ts b/src/hooks/use-query-get-workspace-messages.ts index a1f0f9bc..e5150c51 100644 --- a/src/hooks/use-query-get-workspace-messages.ts +++ b/src/hooks/use-query-get-workspace-messages.ts @@ -51,7 +51,7 @@ export const useQueryGetWorkspaceMessages = < conversation.alerts && conversation.alerts?.length > 0 ? { ...conversation, - alerts: dedupeByKeys(conversation.alerts, ["id", "code_snippet"]), + alerts: dedupeByKeys(conversation.alerts, ["id"]), } : conversation, ); diff --git a/src/routes/route-chat.tsx b/src/routes/route-chat.tsx index b5df6ce0..3b7cdb06 100644 --- a/src/routes/route-chat.tsx +++ b/src/routes/route-chat.tsx @@ -21,7 +21,6 @@ export function RouteChat() { const { data: conversation } = useQueryGetWorkspaceMessages({ select: (data) => data.find((m) => m.chat_id === id), }); - console.debug("👉 conversation:", conversation); const secrets = conversation?.alerts?.filter(isAlertSecret); From 6eebb19f5a525026b22a325ea245588c8a4f0ca8 Mon Sep 17 00:00:00 2001 From: alex-mcgovern Date: Tue, 11 Feb 2025 08:25:05 +0000 Subject: [PATCH 08/19] chore: util function for MSW endpoint --- src/test/msw-endpoint.ts | 25 +++++++++++++++++++++++++ src/vite-env.d.ts | 1 + 2 files changed, 26 insertions(+) create mode 100644 src/test/msw-endpoint.ts diff --git a/src/test/msw-endpoint.ts b/src/test/msw-endpoint.ts new file mode 100644 index 00000000..4f6e94a4 --- /dev/null +++ b/src/test/msw-endpoint.ts @@ -0,0 +1,25 @@ +import type json from "../api/openapi.json"; + +/** + * OpenAPI spec uses curly braces to denote path parameters + * @example + * ``` + * /api/v1/provider-endpoints/{provider_id}/models + * ``` + * + * MSW expects a colon prefix for path parameters + * @example + * ``` + * /api/v1/provider-endpoints/:provider_id/models + * ``` + */ +type ReplacePathParams = + T extends `${infer Start}{${infer Param}}${infer End}` + ? `${Start}:${Param}${ReplacePathParams}` + : T; + +type Endpoint = ReplacePathParams; + +export function mswEndpoint(endpoint: Endpoint) { + return new URL(endpoint, import.meta.env.VITE_BASE_API_URL).toString(); +} diff --git a/src/vite-env.d.ts b/src/vite-env.d.ts index 32f4a144..2aa07e67 100644 --- a/src/vite-env.d.ts +++ b/src/vite-env.d.ts @@ -1,4 +1,5 @@ /// interface ImportBaseApiEnv { readonly BASE_API_URL: string; + readonly VITE_BASE_API_URL: string; } From b789f3c1f581c86d68d5fa75127399f1d641442b Mon Sep 17 00:00:00 2001 From: alex-mcgovern Date: Tue, 11 Feb 2025 08:27:45 +0000 Subject: [PATCH 09/19] chore: apply msw endpoint to handlers --- src/mocks/msw/handlers.ts | 139 ++++++++++++++++++++------------------ 1 file changed, 75 insertions(+), 64 deletions(-) diff --git a/src/mocks/msw/handlers.ts b/src/mocks/msw/handlers.ts index 2a4974cb..a852dc8f 100644 --- a/src/mocks/msw/handlers.ts +++ b/src/mocks/msw/handlers.ts @@ -1,22 +1,25 @@ import { http, HttpResponse } from "msw"; -import mockedPrompts from "@/mocks/msw/fixtures/GET_MESSAGES.json"; 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 { mswEndpoint } from "@/test/msw-endpoint"; export const handlers = [ - http.get("*/health", () => + http.get(mswEndpoint("/health"), () => HttpResponse.json({ current_version: "foo", latest_version: "bar", is_latest: false, error: null, - }), + }) ), - http.get("*/api/v1/version", () => HttpResponse.json({ status: "healthy" })), - http.get("*/api/v1/workspaces/active", () => + http.get(mswEndpoint("/api/v1/version"), () => + HttpResponse.json({ status: "healthy" }) + ), + http.get(mswEndpoint("/api/v1/workspaces/active"), () => HttpResponse.json({ workspaces: [ { @@ -25,18 +28,20 @@ export const handlers = [ last_updated: new Date(Date.now()).toISOString(), }, ], - }), + }) ), - http.get("*/api/v1/workspaces/:name/messages", () => { - return HttpResponse.json(mockedPrompts); + http.get(mswEndpoint("/api/v1/workspaces/:workspace_name/messages"), () => { + return HttpResponse.json( + Array.from({ length: 10 }).map(() => mockConversation()) + ); }), - http.get("*/api/v1/workspaces/:name/alerts", () => { + http.get(mswEndpoint("/api/v1/workspaces/:workspace_name/alerts"), () => { return HttpResponse.json(mockedAlerts); }), - http.get("*/api/v1/workspaces", () => { + http.get(mswEndpoint("/api/v1/workspaces"), () => { return HttpResponse.json(mockedWorkspaces); }), - http.get("*/api/v1/workspaces/archive", () => { + http.get(mswEndpoint("/api/v1/workspaces/archive"), () => { return HttpResponse.json({ workspaces: [ { @@ -46,55 +51,61 @@ export const handlers = [ ], }); }), - http.post("*/api/v1/workspaces", () => { + http.post(mswEndpoint("/api/v1/workspaces"), () => { return HttpResponse.json(mockedWorkspaces); }), http.post( - "*/api/v1/workspaces/active", - () => new HttpResponse(null, { status: 204 }), + mswEndpoint("/api/v1/workspaces/active"), + () => new HttpResponse(null, { status: 204 }) ), http.post( - "*/api/v1/workspaces/archive/:workspace_name/recover", - () => new HttpResponse(null, { status: 204 }), + mswEndpoint("/api/v1/workspaces/archive/:workspace_name/recover"), + () => new HttpResponse(null, { status: 204 }) ), http.delete( - "*/api/v1/workspaces/:name", - () => new HttpResponse(null, { status: 204 }), + mswEndpoint("/api/v1/workspaces/:workspace_name"), + () => new HttpResponse(null, { status: 204 }) ), http.delete( - "*/api/v1/workspaces/archive/:name", - () => new HttpResponse(null, { status: 204 }), - ), - http.get("*/api/v1/workspaces/:name/custom-instructions", () => { - return HttpResponse.json({ prompt: "foo" }); - }), - http.get("*/api/v1/workspaces/:name/token-usage", () => { - return HttpResponse.json({ - tokens_by_model: { - "claude-3-5-sonnet-latest": { - provider_type: ProviderType.ANTHROPIC, - model: "claude-3-5-sonnet-latest", - token_usage: { - input_tokens: 1183, - output_tokens: 433, - input_cost: 0.003549, - output_cost: 0.006495, + mswEndpoint("/api/v1/workspaces/archive/:workspace_name"), + () => new HttpResponse(null, { status: 204 }) + ), + http.get( + mswEndpoint("/api/v1/workspaces/:workspace_name/custom-instructions"), + () => { + return HttpResponse.json({ prompt: "foo" }); + } + ), + http.get( + mswEndpoint("/api/v1/workspaces/:workspace_name/token-usage"), + () => { + return HttpResponse.json({ + tokens_by_model: { + "claude-3-5-sonnet-latest": { + provider_type: ProviderType.ANTHROPIC, + model: "claude-3-5-sonnet-latest", + token_usage: { + input_tokens: 1183, + output_tokens: 433, + input_cost: 0.003549, + output_cost: 0.006495, + }, }, }, - }, - token_usage: { - input_tokens: 1183, - output_tokens: 433, - input_cost: 0.003549, - output_cost: 0.006495, - }, - }); - }), + token_usage: { + input_tokens: 1183, + output_tokens: 433, + input_cost: 0.003549, + output_cost: 0.006495, + }, + }); + } + ), http.put( - "*/api/v1/workspaces/:name/custom-instructions", - () => new HttpResponse(null, { status: 204 }), + mswEndpoint("/api/v1/workspaces/:workspace_name/custom-instructions"), + () => new HttpResponse(null, { status: 204 }) ), - http.get("*/api/v1/workspaces/:workspace_name/muxes", () => + http.get(mswEndpoint("/api/v1/workspaces/:workspace_name/muxes"), () => HttpResponse.json([ { provider_id: "openai", @@ -107,34 +118,34 @@ export const handlers = [ model: "davinci", matcher_type: "catch_all", }, - ]), + ]) ), http.put( - "*/api/v1/workspaces/:workspace_name/muxes", - () => new HttpResponse(null, { status: 204 }), + mswEndpoint("/api/v1/workspaces/:workspace_name/muxes"), + () => new HttpResponse(null, { status: 204 }) ), - http.get("*/api/v1/provider-endpoints/:provider_name/models", () => - HttpResponse.json(mockedProvidersModels), + http.get(mswEndpoint("/api/v1/provider-endpoints/:provider_id/models"), () => + HttpResponse.json(mockedProvidersModels) ), - http.get("*/api/v1/provider-endpoints/models", () => - HttpResponse.json(mockedProvidersModels), + http.get(mswEndpoint("/api/v1/provider-endpoints/models"), () => + HttpResponse.json(mockedProvidersModels) ), - http.get("*/api/v1/provider-endpoints/:provider_id", () => - HttpResponse.json(mockedProviders[0]), + http.get(mswEndpoint("/api/v1/provider-endpoints/:provider_id"), () => + HttpResponse.json(mockedProviders[0]) ), - http.get("*/api/v1/provider-endpoints", () => - HttpResponse.json(mockedProviders), + http.get(mswEndpoint("/api/v1/provider-endpoints"), () => + HttpResponse.json(mockedProviders) ), http.post( - "*/api/v1/provider-endpoints", - () => new HttpResponse(null, { status: 204 }), + mswEndpoint("/api/v1/provider-endpoints"), + () => new HttpResponse(null, { status: 204 }) ), http.put( - "*/api/v1/provider-endpoints", - () => new HttpResponse(null, { status: 204 }), + mswEndpoint("/api/v1/provider-endpoints"), + () => new HttpResponse(null, { status: 204 }) ), http.delete( - "*/api/v1/provider-endpoints", - () => new HttpResponse(null, { status: 204 }), + mswEndpoint("/api/v1/provider-endpoints"), + () => new HttpResponse(null, { status: 204 }) ), ]; From 02069c03456c319c14ad6bc76eeb79c8bc97d71b Mon Sep 17 00:00:00 2001 From: alex-mcgovern Date: Tue, 11 Feb 2025 08:29:15 +0000 Subject: [PATCH 10/19] chore: mockers for API responses --- src/mocks/msw/mockers/alert.mock.ts | 65 ++++++++++++++++++++++ src/mocks/msw/mockers/conversation.mock.ts | 54 ++++++++++++++++++ src/mocks/msw/mockers/token-usage.mock.ts | 22 ++++++++ 3 files changed, 141 insertions(+) create mode 100644 src/mocks/msw/mockers/alert.mock.ts create mode 100644 src/mocks/msw/mockers/conversation.mock.ts create mode 100644 src/mocks/msw/mockers/token-usage.mock.ts diff --git a/src/mocks/msw/mockers/alert.mock.ts b/src/mocks/msw/mockers/alert.mock.ts new file mode 100644 index 00000000..def3f337 --- /dev/null +++ b/src/mocks/msw/mockers/alert.mock.ts @@ -0,0 +1,65 @@ +import { Alert, TokenUsageAggregate } from "@/api/generated"; +import { faker } from "@faker-js/faker"; +import { TOKEN_USAGE_AGG } from "./token-usage.mock"; + +const ALERT_SECRET_FIELDS = { + trigger_string: "foo", + trigger_type: "codegate-secrets", +} satisfies Pick; + +const ALERT_MALICIOUS_FIELDS = { + trigger_string: { + name: "invokehttp", + type: "pypi", + status: "malicious", + description: "Python HTTP for Humans.", + }, + trigger_type: "codegate-context-retriever", +} satisfies Pick; + +const getBaseAlert = ({ + timestamp, +}: { + timestamp: string; + token_usage_agg: TokenUsageAggregate | null; +}): Omit => ({ + id: faker.string.uuid(), + prompt_id: faker.string.uuid(), + code_snippet: null, + trigger_category: "critical", + timestamp: timestamp, +}); + +export const mockAlert = ({ + token_usage = false, + type, +}: { + token_usage?: boolean; + type: "secret" | "malicious"; +}): Alert => { + const timestamp = faker.date.recent().toISOString(); + + const base: Omit = getBaseAlert({ + timestamp, + token_usage_agg: token_usage ? TOKEN_USAGE_AGG : null, + }); + + switch (type) { + case "malicious": { + const result: Alert = { + ...base, + ...ALERT_MALICIOUS_FIELDS, + }; + + return result; + } + case "secret": { + const result: Alert = { + ...base, + ...ALERT_SECRET_FIELDS, + }; + + return result; + } + } +}; diff --git a/src/mocks/msw/mockers/conversation.mock.ts b/src/mocks/msw/mockers/conversation.mock.ts new file mode 100644 index 00000000..e0461e68 --- /dev/null +++ b/src/mocks/msw/mockers/conversation.mock.ts @@ -0,0 +1,54 @@ +import { Conversation, QuestionType } from "@/api/generated"; +import { faker } from "@faker-js/faker"; +import { TOKEN_USAGE_AGG } from "./token-usage.mock"; +import { mockAlert } from "./alert.mock"; + +export function mockConversation({ + type = QuestionType.CHAT, + withTokenUsage = true, + alertsConfig = {}, +}: { + type?: QuestionType; + withTokenUsage?: boolean; + alertsConfig?: { + numAlerts?: number; + type?: "secret" | "malicious" | "any"; + }; +} = {}) { + const timestamp = faker.date.recent().toISOString(); + + return { + question_answers: [ + { + question: { + message: faker.lorem.sentence(), + timestamp: timestamp, + message_id: faker.string.uuid(), + }, + answer: { + message: faker.lorem.sentence(), + timestamp: timestamp, + message_id: faker.string.uuid(), + }, + }, + ], + provider: "vllm", + alerts: Array.from({ + length: + typeof alertsConfig?.numAlerts === "number" + ? alertsConfig?.numAlerts + : faker.number.int({ min: 0, max: 5 }), + }).map(() => + mockAlert({ + type: + alertsConfig?.type == null || alertsConfig.type === "any" + ? faker.helpers.arrayElement(["secret", "malicious"]) + : alertsConfig.type, + }), + ), + 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, + } as const satisfies Conversation; +} diff --git a/src/mocks/msw/mockers/token-usage.mock.ts b/src/mocks/msw/mockers/token-usage.mock.ts new file mode 100644 index 00000000..52cc00cc --- /dev/null +++ b/src/mocks/msw/mockers/token-usage.mock.ts @@ -0,0 +1,22 @@ +import { ProviderType, TokenUsageAggregate } from "@/api/generated"; + +export const TOKEN_USAGE_AGG = { + tokens_by_model: { + "claude-3-5-sonnet-latest": { + provider_type: ProviderType.ANTHROPIC, + model: "claude-3-5-sonnet-latest", + token_usage: { + input_tokens: 1183, + output_tokens: 433, + input_cost: 0.003549, + output_cost: 0.006495, + }, + }, + }, + token_usage: { + input_tokens: 1183, + output_tokens: 433, + input_cost: 0.003549, + output_cost: 0.006495, + }, +} as const satisfies TokenUsageAggregate; From c2b3ac38e79e56bb469f5b92a69472d5913be87f Mon Sep 17 00:00:00 2001 From: alex-mcgovern Date: Tue, 11 Feb 2025 08:30:57 +0000 Subject: [PATCH 11/19] chore: add eslint rule to enforce safe msw usage --- eslint.config.js | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/eslint.config.js b/eslint.config.js index 2af2f2e0..3dca070a 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -105,6 +105,20 @@ export default tseslint.config( message: "Do not directly call `invalidateQueries`. Instead, use the `invalidateQueries` helper function.", }, + { + selector: [ + "CallExpression[callee.object.name='http'][callee.property.name='all'] > Literal:first-child", + "CallExpression[callee.object.name='http'][callee.property.name='head'] > Literal:first-child", + "CallExpression[callee.object.name='http'][callee.property.name='get'] > Literal:first-child", + "CallExpression[callee.object.name='http'][callee.property.name='post'] > Literal:first-child", + "CallExpression[callee.object.name='http'][callee.property.name='put'] > Literal:first-child", + "CallExpression[callee.object.name='http'][callee.property.name='delete'] > Literal:first-child", + "CallExpression[callee.object.name='http'][callee.property.name='patch'] > Literal:first-child", + "CallExpression[callee.object.name='http'][callee.property.name='options'] > Literal:first-child", + ].join(", "), + message: + "Do not pass a string as the first argument to methods on Mock Service Worker's `http`. Use the `mswEndpoint` helper function instead, which provides type-safe routes based on the OpenAPI spec and the API base URL.", + }, ], "no-restricted-imports": [ "error", From 567916a30815ddb2cc08e06c1aa8a25db527182a Mon Sep 17 00:00:00 2001 From: alex-mcgovern Date: Tue, 11 Feb 2025 08:55:58 +0000 Subject: [PATCH 12/19] chore: apply new MSW approach across codebase --- .../alerts-summary-malicious-pkg.test.tsx | 12 ++- .../__tests__/alerts-summary-secrets.test.tsx | 11 ++- ...rts-summary-workspace-token-usage.test.tsx | 22 +++-- .../table-alerts.empty-state.test.tsx | 46 +++++----- .../__tests__/table-alerts.test.tsx | 24 +++-- .../components/__tests__/tabs-alerts.test.tsx | 22 ++--- .../lib/__tests__/is-alert-malicious.test.ts | 6 +- .../lib/__tests__/is-alert-secret.test.ts | 6 +- src/features/alerts/lib/is-alert-malicious.ts | 4 +- src/features/alerts/lib/is-alert-secret.ts | 4 +- src/features/alerts/mocks/alert.mock.ts | 91 ------------------- src/features/alerts/mocks/token-usage.mock.ts | 22 ----- .../__tests__/header-status-menu.test.tsx | 37 +++++--- .../__tests__/archive-workspace.test.tsx | 3 +- .../workspace-custom-instructions.test.tsx | 19 ++-- .../__tests__/workspace-name.test.tsx | 3 +- src/mocks/msw/mockers/alert.mock.ts | 7 +- src/routes/__tests__/route-dashboard.test.tsx | 5 +- 18 files changed, 135 insertions(+), 209 deletions(-) delete mode 100644 src/features/alerts/mocks/alert.mock.ts delete mode 100644 src/features/alerts/mocks/token-usage.mock.ts diff --git a/src/features/alerts/components/__tests__/alerts-summary-malicious-pkg.test.tsx b/src/features/alerts/components/__tests__/alerts-summary-malicious-pkg.test.tsx index ed1d728a..44a4c7b1 100644 --- a/src/features/alerts/components/__tests__/alerts-summary-malicious-pkg.test.tsx +++ b/src/features/alerts/components/__tests__/alerts-summary-malicious-pkg.test.tsx @@ -3,12 +3,14 @@ import { test } from "vitest"; import { http, HttpResponse } from "msw"; import { render, waitFor } from "@/lib/test-utils"; import { AlertsSummaryMaliciousPkg } from "../alerts-summary-malicious-pkg"; -import { makeMockAlert } from "../../mocks/alert.mock"; + +import { mswEndpoint } from "@/test/msw-endpoint"; +import { mockAlert } from "@/mocks/msw/mockers/alert.mock"; test("shows correct count when there is a malicious alert", async () => { server.use( - http.get("*/api/v1/workspaces/:name/alerts", () => { - return HttpResponse.json([makeMockAlert({ type: "malicious" })]); + http.get(mswEndpoint("/api/v1/workspaces/:workspace_name/alerts"), () => { + return HttpResponse.json([mockAlert({ type: "malicious" })]); }), ); @@ -21,8 +23,8 @@ 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("*/api/v1/workspaces/:name/alerts", () => { - return HttpResponse.json([makeMockAlert({ type: "secret" })]); + http.get(mswEndpoint("/api/v1/workspaces/:workspace_name/alerts"), () => { + return HttpResponse.json([mockAlert({ type: "secret" })]); }), ); diff --git a/src/features/alerts/components/__tests__/alerts-summary-secrets.test.tsx b/src/features/alerts/components/__tests__/alerts-summary-secrets.test.tsx index 5a99655b..9e3c2484 100644 --- a/src/features/alerts/components/__tests__/alerts-summary-secrets.test.tsx +++ b/src/features/alerts/components/__tests__/alerts-summary-secrets.test.tsx @@ -4,12 +4,13 @@ import { http, HttpResponse } from "msw"; import { render, waitFor } from "@/lib/test-utils"; import { AlertsSummaryMaliciousSecrets } from "../alerts-summary-secrets"; -import { makeMockAlert } from "../../mocks/alert.mock"; +import { mswEndpoint } from "@/test/msw-endpoint"; +import { mockAlert } from "@/mocks/msw/mockers/alert.mock"; test("shows correct count when there is a secret alert", async () => { server.use( - http.get("*/api/v1/workspaces/:name/alerts", () => { - return HttpResponse.json([makeMockAlert({ type: "secret" })]); + http.get(mswEndpoint("/api/v1/workspaces/:workspace_name/alerts"), () => { + return HttpResponse.json([mockAlert({ type: "secret" })]); }), ); @@ -22,8 +23,8 @@ 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("*/api/v1/workspaces/:name/alerts", () => { - return HttpResponse.json([makeMockAlert({ type: "malicious" })]); + http.get(mswEndpoint("/api/v1/workspaces/:workspace_name/alerts"), () => { + return HttpResponse.json([mockAlert({ type: "malicious" })]); }), ); diff --git a/src/features/alerts/components/__tests__/alerts-summary-workspace-token-usage.test.tsx b/src/features/alerts/components/__tests__/alerts-summary-workspace-token-usage.test.tsx index 56466a27..02138190 100644 --- a/src/features/alerts/components/__tests__/alerts-summary-workspace-token-usage.test.tsx +++ b/src/features/alerts/components/__tests__/alerts-summary-workspace-token-usage.test.tsx @@ -4,14 +4,19 @@ import { http, HttpResponse } from "msw"; import { render, waitFor } from "@/lib/test-utils"; import { AlertsSummaryWorkspaceTokenUsage } from "../alerts-summary-workspace-token-usage"; -import { TOKEN_USAGE_AGG } from "../../mocks/token-usage.mock"; + import { formatNumberCompact } from "@/lib/format-number"; +import { mswEndpoint } from "@/test/msw-endpoint"; +import { TOKEN_USAGE_AGG } from "@/mocks/msw/mockers/token-usage.mock"; test("shows correct count when there is token usage", async () => { server.use( - http.get("*/api/v1/workspaces/:name/token-usage", () => { - return HttpResponse.json(TOKEN_USAGE_AGG); - }), + http.get( + mswEndpoint("/api/v1/workspaces/:workspace_name/token-usage"), + () => { + return HttpResponse.json(TOKEN_USAGE_AGG); + }, + ), ); const { getByTestId } = render(); @@ -28,9 +33,12 @@ test("shows correct count when there is token usage", async () => { test("shows correct count when there is no token usage", async () => { server.use( - http.get("*/api/v1/workspaces/:name/token-usage", () => { - return HttpResponse.json({}); - }), + http.get( + mswEndpoint("/api/v1/workspaces/:workspace_name/token-usage"), + () => { + return HttpResponse.json({}); + }, + ), ); const { getByTestId } = render(); diff --git a/src/features/alerts/components/__tests__/table-alerts.empty-state.test.tsx b/src/features/alerts/components/__tests__/table-alerts.empty-state.test.tsx index c6ce31f4..1db06c0a 100644 --- a/src/features/alerts/components/__tests__/table-alerts.empty-state.test.tsx +++ b/src/features/alerts/components/__tests__/table-alerts.empty-state.test.tsx @@ -4,10 +4,12 @@ import { server } from "@/mocks/msw/node"; import { emptyStateStrings } from "../../constants/strings"; import { useSearchParams } from "react-router-dom"; import { delay, http, HttpHandler, HttpResponse } from "msw"; -import { makeMockAlert } from "../../mocks/alert.mock"; + import { AlertsFilterView } from "../../hooks/use-alerts-filter-search-params"; import { TableAlerts } from "../table-alerts"; import { hrefs } from "@/lib/hrefs"; +import { mswEndpoint } from "@/test/msw-endpoint"; +import { mockAlert } from "@/mocks/msw/mockers/alert.mock"; enum IllustrationTestId { ALERT = "illustration-alert", @@ -78,7 +80,7 @@ const TEST_CASES: TestCase[] = [ { testDescription: "Loading state", handlers: [ - http.get("*/api/v1/workspaces", () => { + http.get(mswEndpoint("/api/v1/workspaces"), () => { delay("infinite"); }), ], @@ -96,7 +98,7 @@ const TEST_CASES: TestCase[] = [ { testDescription: "Only 1 workspace, no alerts", handlers: [ - http.get("*/api/v1/workspaces", () => { + http.get(mswEndpoint("/api/v1/workspaces"), () => { return HttpResponse.json({ workspaces: [ { @@ -106,12 +108,12 @@ const TEST_CASES: TestCase[] = [ ], }); }), - http.get("*/api/v1/workspaces/archive", () => { + http.get(mswEndpoint("/api/v1/workspaces/archive"), () => { return HttpResponse.json({ workspaces: [], }); }), - http.get("*/api/v1/workspaces/:name/alerts", () => { + http.get(mswEndpoint("/api/v1/workspaces/:workspace_name/alerts"), () => { return HttpResponse.json([]); }), ], @@ -135,7 +137,7 @@ const TEST_CASES: TestCase[] = [ { testDescription: "No search results", handlers: [ - http.get("*/api/v1/workspaces", () => { + http.get(mswEndpoint("/api/v1/workspaces"), () => { return HttpResponse.json({ workspaces: [ { @@ -145,16 +147,14 @@ const TEST_CASES: TestCase[] = [ ], }); }), - http.get("*/api/v1/workspaces/archive", () => { + http.get(mswEndpoint("/api/v1/workspaces/archive"), () => { return HttpResponse.json({ workspaces: [], }); }), - http.get("*/api/v1/workspaces/:name/alerts", () => { + http.get(mswEndpoint("/api/v1/workspaces/:workspace_name/alerts"), () => { return HttpResponse.json( - Array.from({ length: 10 }, () => - makeMockAlert({ type: "malicious" }), - ), + Array.from({ length: 10 }, () => mockAlert({ type: "malicious" })), ); }), ], @@ -174,7 +174,7 @@ const TEST_CASES: TestCase[] = [ { testDescription: "No alerts, multiple workspaces", handlers: [ - http.get("*/api/v1/workspaces", () => { + http.get(mswEndpoint("/api/v1/workspaces"), () => { return HttpResponse.json({ workspaces: [ { @@ -188,12 +188,12 @@ const TEST_CASES: TestCase[] = [ ], }); }), - http.get("*/api/v1/workspaces/archive", () => { + http.get(mswEndpoint("/api/v1/workspaces/archive"), () => { return HttpResponse.json({ workspaces: [], }); }), - http.get("*/api/v1/workspaces/:name/alerts", () => { + http.get(mswEndpoint("/api/v1/workspaces/:workspace_name/alerts"), () => { return HttpResponse.json([]); }), ], @@ -217,7 +217,7 @@ const TEST_CASES: TestCase[] = [ { testDescription: 'Has alerts, view is "malicious"', handlers: [ - http.get("*/api/v1/workspaces", () => { + http.get(mswEndpoint("/api/v1/workspaces"), () => { return HttpResponse.json({ workspaces: [ { @@ -231,16 +231,14 @@ const TEST_CASES: TestCase[] = [ ], }); }), - http.get("*/api/v1/workspaces/archive", () => { + http.get(mswEndpoint("/api/v1/workspaces/archive"), () => { return HttpResponse.json({ workspaces: [], }); }), - http.get("*/api/v1/workspaces/:name/alerts", () => { + http.get(mswEndpoint("/api/v1/workspaces/:workspace_name/alerts"), () => { return HttpResponse.json( - Array.from({ length: 10 }).map(() => - makeMockAlert({ type: "secret" }), - ), + Array.from({ length: 10 }).map(() => mockAlert({ type: "secret" })), ); }), ], @@ -258,7 +256,7 @@ const TEST_CASES: TestCase[] = [ { testDescription: 'Has alerts, view is "secret"', handlers: [ - http.get("*/api/v1/workspaces", () => { + http.get(mswEndpoint("/api/v1/workspaces"), () => { return HttpResponse.json({ workspaces: [ { @@ -272,15 +270,15 @@ const TEST_CASES: TestCase[] = [ ], }); }), - http.get("*/api/v1/workspaces/archive", () => { + http.get(mswEndpoint("/api/v1/workspaces/archive"), () => { return HttpResponse.json({ workspaces: [], }); }), - http.get("*/api/v1/workspaces/:name/alerts", () => { + http.get(mswEndpoint("/api/v1/workspaces/:workspace_name/alerts"), () => { return HttpResponse.json( Array.from({ length: 10 }).map(() => - makeMockAlert({ type: "malicious" }), + mockAlert({ type: "malicious" }), ), ); }), diff --git a/src/features/alerts/components/__tests__/table-alerts.test.tsx b/src/features/alerts/components/__tests__/table-alerts.test.tsx index 905f31af..404932dc 100644 --- a/src/features/alerts/components/__tests__/table-alerts.test.tsx +++ b/src/features/alerts/components/__tests__/table-alerts.test.tsx @@ -3,9 +3,11 @@ import { TableAlerts } from "../table-alerts"; import { render, waitFor } from "@/lib/test-utils"; import { server } from "@/mocks/msw/node"; import { http, HttpResponse } from "msw"; -import { makeMockAlert } from "../../mocks/alert.mock"; -import { TOKEN_USAGE_AGG } from "../../mocks/token-usage.mock"; import { formatNumberCompact } from "@/lib/format-number"; +import { mswEndpoint } from "@/test/msw-endpoint"; +import { mockAlert } from "@/mocks/msw/mockers/alert.mock"; +import { TOKEN_USAGE_AGG } from "@/mocks/msw/mockers/token-usage.mock"; +import { mockConversation } from "@/mocks/msw/mockers/conversation.mock"; vi.mock("@untitled-ui/icons-react", async () => { const original = await vi.importActual< @@ -28,9 +30,14 @@ const OUTPUT_TOKENS = test("renders token usage cell correctly", async () => { server.use( - http.get("*/workspaces/:name/alerts", () => { + http.get(mswEndpoint("/api/v1/workspaces/:workspace_name/alerts"), () => { return HttpResponse.json([ - makeMockAlert({ token_usage: true, type: "malicious" }), + { + ...mockAlert({ type: "malicious" }), + conversation: mockConversation({ + withTokenUsage: true, + }), + }, ]); }), ); @@ -53,9 +60,14 @@ test("renders token usage cell correctly", async () => { test("renders N/A when token usage is missing", async () => { server.use( - http.get("*/workspaces/:name/alerts", () => { + http.get(mswEndpoint("/api/v1/workspaces/:workspace_name/alerts"), () => { return HttpResponse.json([ - makeMockAlert({ token_usage: false, type: "malicious" }), + { + ...mockAlert({ type: "malicious" }), + conversation: mockConversation({ + withTokenUsage: false, + }), + }, ]); }), ); diff --git a/src/features/alerts/components/__tests__/tabs-alerts.test.tsx b/src/features/alerts/components/__tests__/tabs-alerts.test.tsx index 913647d4..da4cd1d6 100644 --- a/src/features/alerts/components/__tests__/tabs-alerts.test.tsx +++ b/src/features/alerts/components/__tests__/tabs-alerts.test.tsx @@ -1,18 +1,18 @@ import { server } from "@/mocks/msw/node"; import { http, HttpResponse } from "msw"; -import { makeMockAlert } from "../../mocks/alert.mock"; + import { render, waitFor } from "@/lib/test-utils"; import { TabsAlerts } from "../tabs-alerts"; +import { mswEndpoint } from "@/test/msw-endpoint"; +import { mockAlert } from "@/mocks/msw/mockers/alert.mock"; test("shows correct count of all packages", async () => { server.use( - http.get("*/workspaces/:name/alerts", () => { + http.get(mswEndpoint("/api/v1/workspaces/:workspace_name/alerts"), () => { return HttpResponse.json([ + ...Array.from({ length: 13 }).map(() => mockAlert({ type: "secret" })), ...Array.from({ length: 13 }).map(() => - makeMockAlert({ type: "secret" }), - ), - ...Array.from({ length: 13 }).map(() => - makeMockAlert({ type: "malicious" }), + mockAlert({ type: "malicious" }), ), ]); }), @@ -31,11 +31,9 @@ test("shows correct count of all packages", async () => { test("shows correct count of malicious packages", async () => { server.use( - http.get("*/workspaces/:name/alerts", () => { + http.get(mswEndpoint("/api/v1/workspaces/:workspace_name/alerts"), () => { return HttpResponse.json( - Array.from({ length: 13 }).map(() => - makeMockAlert({ type: "malicious" }), - ), + Array.from({ length: 13 }).map(() => mockAlert({ type: "malicious" })), ); }), ); @@ -53,9 +51,9 @@ test("shows correct count of malicious packages", async () => { test("shows correct count of secret packages", async () => { server.use( - http.get("*/workspaces/:name/alerts", () => { + http.get(mswEndpoint("/api/v1/workspaces/:workspace_name/alerts"), () => { return HttpResponse.json( - Array.from({ length: 13 }).map(() => makeMockAlert({ type: "secret" })), + Array.from({ length: 13 }).map(() => mockAlert({ type: "secret" })), ); }), ); diff --git a/src/features/alerts/lib/__tests__/is-alert-malicious.test.ts b/src/features/alerts/lib/__tests__/is-alert-malicious.test.ts index b41a9505..dfcd8b3f 100644 --- a/src/features/alerts/lib/__tests__/is-alert-malicious.test.ts +++ b/src/features/alerts/lib/__tests__/is-alert-malicious.test.ts @@ -1,11 +1,11 @@ import { test, expect } from "vitest"; import { isAlertMalicious } from "../is-alert-malicious"; -import { makeMockAlert } from "../../mocks/alert.mock"; +import { mockAlert } from "@/mocks/msw/mockers/alert.mock"; test("matches malicious alert", () => { - expect(isAlertMalicious(makeMockAlert({ type: "malicious" }))).toBe(true); + expect(isAlertMalicious(mockAlert({ type: "malicious" }))).toBe(true); }); test("doesn't match secret", () => { - expect(isAlertMalicious(makeMockAlert({ type: "secret" }))).toBe(false); + expect(isAlertMalicious(mockAlert({ type: "secret" }))).toBe(false); }); diff --git a/src/features/alerts/lib/__tests__/is-alert-secret.test.ts b/src/features/alerts/lib/__tests__/is-alert-secret.test.ts index 88d32bfa..16f70071 100644 --- a/src/features/alerts/lib/__tests__/is-alert-secret.test.ts +++ b/src/features/alerts/lib/__tests__/is-alert-secret.test.ts @@ -1,11 +1,11 @@ import { test, expect } from "vitest"; import { isAlertSecret } from "../is-alert-secret"; -import { makeMockAlert } from "../../mocks/alert.mock"; +import { mockAlert } from "@/mocks/msw/mockers/alert.mock"; test("matches secret alert", () => { - expect(isAlertSecret(makeMockAlert({ type: "secret" }))).toBe(true); + expect(isAlertSecret(mockAlert({ type: "secret" }))).toBe(true); }); test("doesn't match malicious", () => { - expect(isAlertSecret(makeMockAlert({ type: "malicious" }))).toBe(false); + expect(isAlertSecret(mockAlert({ type: "malicious" }))).toBe(false); }); diff --git a/src/features/alerts/lib/is-alert-malicious.ts b/src/features/alerts/lib/is-alert-malicious.ts index 2c9e10ed..457c36e4 100644 --- a/src/features/alerts/lib/is-alert-malicious.ts +++ b/src/features/alerts/lib/is-alert-malicious.ts @@ -1,7 +1,7 @@ -import { AlertConversation } from "@/api/generated"; +import { Alert, AlertConversation } from "@/api/generated"; export function isAlertMalicious( - alert: AlertConversation | null, + alert: Alert | AlertConversation | null, ): alert is AlertConversation { return ( alert?.trigger_category === "critical" && diff --git a/src/features/alerts/lib/is-alert-secret.ts b/src/features/alerts/lib/is-alert-secret.ts index 9e7fd87a..7acc6612 100644 --- a/src/features/alerts/lib/is-alert-secret.ts +++ b/src/features/alerts/lib/is-alert-secret.ts @@ -1,6 +1,6 @@ -import { V1GetWorkspaceAlertsResponse } from "@/api/generated"; +import { Alert, AlertConversation } from "@/api/generated"; -export function isAlertSecret(alert: V1GetWorkspaceAlertsResponse[number]) { +export function isAlertSecret(alert: Alert | AlertConversation | null) { return ( alert?.trigger_category === "critical" && alert.trigger_type === "codegate-secrets" diff --git a/src/features/alerts/mocks/alert.mock.ts b/src/features/alerts/mocks/alert.mock.ts deleted file mode 100644 index 945b6aef..00000000 --- a/src/features/alerts/mocks/alert.mock.ts +++ /dev/null @@ -1,91 +0,0 @@ -import { - AlertConversation, - QuestionType, - TokenUsageAggregate, -} from "@/api/generated"; -import { faker } from "@faker-js/faker"; -import { TOKEN_USAGE_AGG } from "./token-usage.mock"; - -export const ALERT_SECRET_FIELDS = { - trigger_string: "foo", - trigger_type: "codegate-secrets", -} satisfies Pick; - -export const ALERT_MALICIOUS_FIELDS = { - trigger_string: { - name: "invokehttp", - type: "pypi", - status: "malicious", - description: "Python HTTP for Humans.", - }, - trigger_type: "codegate-context-retriever", -} satisfies Pick; - -const getBaseAlert = ({ - timestamp, - token_usage_agg, -}: { - timestamp: string; - token_usage_agg: TokenUsageAggregate | null; -}): Omit => ({ - conversation: { - question_answers: [ - { - question: { - message: "foo", - timestamp: timestamp, - message_id: faker.string.uuid(), - }, - answer: { - message: "bar", - timestamp: timestamp, - message_id: faker.string.uuid(), - }, - }, - ], - provider: "anthropic", - type: QuestionType.CHAT, - chat_id: faker.string.uuid(), - conversation_timestamp: timestamp, - token_usage_agg, - }, - alert_id: faker.string.uuid(), - code_snippet: null, - trigger_category: "critical", - timestamp: timestamp, -}); - -export const makeMockAlert = ({ - token_usage = false, - type, -}: { - token_usage?: boolean; - type: "secret" | "malicious"; -}): AlertConversation => { - const timestamp = faker.date.recent().toUTCString(); - - const base: Omit = - getBaseAlert({ - timestamp, - token_usage_agg: token_usage ? TOKEN_USAGE_AGG : null, - }); - - switch (type) { - case "malicious": { - const result: AlertConversation = { - ...base, - ...ALERT_MALICIOUS_FIELDS, - }; - - return result; - } - case "secret": { - const result: AlertConversation = { - ...base, - ...ALERT_SECRET_FIELDS, - }; - - return result; - } - } -}; diff --git a/src/features/alerts/mocks/token-usage.mock.ts b/src/features/alerts/mocks/token-usage.mock.ts deleted file mode 100644 index 52cc00cc..00000000 --- a/src/features/alerts/mocks/token-usage.mock.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { ProviderType, TokenUsageAggregate } from "@/api/generated"; - -export const TOKEN_USAGE_AGG = { - tokens_by_model: { - "claude-3-5-sonnet-latest": { - provider_type: ProviderType.ANTHROPIC, - model: "claude-3-5-sonnet-latest", - token_usage: { - input_tokens: 1183, - output_tokens: 433, - input_cost: 0.003549, - output_cost: 0.006495, - }, - }, - }, - token_usage: { - input_tokens: 1183, - output_tokens: 433, - input_cost: 0.003549, - output_cost: 0.006495, - }, -} as const satisfies TokenUsageAggregate; diff --git a/src/features/header/components/__tests__/header-status-menu.test.tsx b/src/features/header/components/__tests__/header-status-menu.test.tsx index 154109e0..a957694c 100644 --- a/src/features/header/components/__tests__/header-status-menu.test.tsx +++ b/src/features/header/components/__tests__/header-status-menu.test.tsx @@ -5,13 +5,16 @@ import { expect } from "vitest"; import { render, waitFor } from "@/lib/test-utils"; import { HeaderStatusMenu } from "../header-status-menu"; import userEvent from "@testing-library/user-event"; +import { mswEndpoint } from "@/test/msw-endpoint"; const renderComponent = () => render(); describe("CardCodegateStatus", () => { test("renders 'healthy' state", async () => { server.use( - http.get("*/health", () => HttpResponse.json({ status: "healthy" })), + http.get(mswEndpoint("/health"), () => + HttpResponse.json({ status: "healthy" }), + ), ); const { getByRole } = renderComponent(); @@ -22,7 +25,11 @@ describe("CardCodegateStatus", () => { }); test("renders 'unhealthy' state", async () => { - server.use(http.get("*/health", () => HttpResponse.json({ status: null }))); + server.use( + http.get(mswEndpoint("/health"), () => + HttpResponse.json({ status: null }), + ), + ); const { getByRole } = renderComponent(); @@ -32,7 +39,7 @@ describe("CardCodegateStatus", () => { }); test("renders 'error' state when health check request fails", async () => { - server.use(http.get("*/health", () => HttpResponse.error())); + server.use(http.get(mswEndpoint("/health"), () => HttpResponse.error())); const { getByRole } = renderComponent(); @@ -43,8 +50,10 @@ describe("CardCodegateStatus", () => { test("renders 'error' state when version check request fails", async () => { server.use( - http.get("*/health", () => HttpResponse.json({ status: "healthy" })), - http.get("*/api/v1/version", () => HttpResponse.error()), + http.get(mswEndpoint("/health"), () => + HttpResponse.json({ status: "healthy" }), + ), + http.get(mswEndpoint("/api/v1/version"), () => HttpResponse.error()), ); const { getByRole } = renderComponent(); @@ -56,8 +65,10 @@ describe("CardCodegateStatus", () => { test("renders 'up to date' state", async () => { server.use( - http.get("*/health", () => HttpResponse.json({ status: "healthy" })), - http.get("*/api/v1/version", () => + http.get(mswEndpoint("/health"), () => + HttpResponse.json({ status: "healthy" }), + ), + http.get(mswEndpoint("/api/v1/version"), () => HttpResponse.json({ current_version: "foo", latest_version: "foo", @@ -83,8 +94,10 @@ describe("CardCodegateStatus", () => { test("renders 'update available' state", async () => { server.use( - http.get("*/health", () => HttpResponse.json({ status: "healthy" })), - http.get("*/api/v1/version", () => + http.get(mswEndpoint("/health"), () => + HttpResponse.json({ status: "healthy" }), + ), + http.get(mswEndpoint("/api/v1/version"), () => HttpResponse.json({ current_version: "foo", latest_version: "bar", @@ -115,8 +128,10 @@ describe("CardCodegateStatus", () => { test("renders 'version check error' state", async () => { server.use( - http.get("*/health", () => HttpResponse.json({ status: "healthy" })), - http.get("*/api/v1/version", () => + http.get(mswEndpoint("/health"), () => + HttpResponse.json({ status: "healthy" }), + ), + http.get(mswEndpoint("/api/v1/version"), () => HttpResponse.json({ current_version: "foo", latest_version: "bar", diff --git a/src/features/workspace/components/__tests__/archive-workspace.test.tsx b/src/features/workspace/components/__tests__/archive-workspace.test.tsx index f27d6183..9a6b32de 100644 --- a/src/features/workspace/components/__tests__/archive-workspace.test.tsx +++ b/src/features/workspace/components/__tests__/archive-workspace.test.tsx @@ -4,6 +4,7 @@ import userEvent from "@testing-library/user-event"; import { waitFor } from "@testing-library/react"; import { server } from "@/mocks/msw/node"; import { http, HttpResponse } from "msw"; +import { mswEndpoint } from "@/test/msw-endpoint"; test("has correct buttons when not archived", async () => { const { getByRole, queryByRole } = render( @@ -66,7 +67,7 @@ test("can permanently delete archived workspace", async () => { test("can't archive active workspace", async () => { server.use( - http.get("*/api/v1/workspaces/active", () => + http.get(mswEndpoint("/api/v1/workspaces/active"), () => HttpResponse.json({ workspaces: [ { diff --git a/src/features/workspace/components/__tests__/workspace-custom-instructions.test.tsx b/src/features/workspace/components/__tests__/workspace-custom-instructions.test.tsx index cf63e5fb..b9d83c1f 100644 --- a/src/features/workspace/components/__tests__/workspace-custom-instructions.test.tsx +++ b/src/features/workspace/components/__tests__/workspace-custom-instructions.test.tsx @@ -5,6 +5,7 @@ import userEvent from "@testing-library/user-event"; import { server } from "@/mocks/msw/node"; import { http, HttpResponse } from "msw"; import { WorkspaceCustomInstructions } from "../workspace-custom-instructions"; +import { mswEndpoint } from "@/test/msw-endpoint"; vi.mock("@monaco-editor/react", () => { const FakeEditor = vi.fn((props) => { @@ -26,9 +27,12 @@ const renderComponent = () => test("can update custom instructions", async () => { server.use( - http.get("*/api/v1/workspaces/:name/custom-instructions", () => { - return HttpResponse.json({ prompt: "initial prompt from server" }); - }), + http.get( + mswEndpoint("/api/v1/workspaces/:workspace_name/custom-instructions"), + () => { + return HttpResponse.json({ prompt: "initial prompt from server" }); + }, + ), ); const { getByRole, getByText } = renderComponent(); @@ -45,9 +49,12 @@ test("can update custom instructions", async () => { expect(input).toHaveTextContent("new prompt from test"); server.use( - http.get("*/api/v1/workspaces/:name/custom-instructions", () => { - return HttpResponse.json({ prompt: "new prompt from test" }); - }), + http.get( + mswEndpoint("/api/v1/workspaces/:workspace_name/custom-instructions"), + () => { + return HttpResponse.json({ prompt: "new prompt from test" }); + }, + ), ); await userEvent.click(getByRole("button", { name: /Save/i })); diff --git a/src/features/workspace/components/__tests__/workspace-name.test.tsx b/src/features/workspace/components/__tests__/workspace-name.test.tsx index d92b25b4..84bfc93f 100644 --- a/src/features/workspace/components/__tests__/workspace-name.test.tsx +++ b/src/features/workspace/components/__tests__/workspace-name.test.tsx @@ -4,6 +4,7 @@ import { render, waitFor } from "@/lib/test-utils"; import userEvent from "@testing-library/user-event"; import { server } from "@/mocks/msw/node"; import { http, HttpResponse } from "msw"; +import { mswEndpoint } from "@/test/msw-endpoint"; test("can rename workspace", async () => { const { getByRole, getByText } = render( @@ -34,7 +35,7 @@ test("can't rename archived workspace", async () => { test("can't rename active workspace", async () => { server.use( - http.get("*/api/v1/workspaces/active", () => + http.get(mswEndpoint("/api/v1/workspaces/active"), () => HttpResponse.json({ workspaces: [ { diff --git a/src/mocks/msw/mockers/alert.mock.ts b/src/mocks/msw/mockers/alert.mock.ts index def3f337..7c96ca03 100644 --- a/src/mocks/msw/mockers/alert.mock.ts +++ b/src/mocks/msw/mockers/alert.mock.ts @@ -1,6 +1,5 @@ -import { Alert, TokenUsageAggregate } from "@/api/generated"; +import { Alert } from "@/api/generated"; import { faker } from "@faker-js/faker"; -import { TOKEN_USAGE_AGG } from "./token-usage.mock"; const ALERT_SECRET_FIELDS = { trigger_string: "foo", @@ -21,7 +20,6 @@ const getBaseAlert = ({ timestamp, }: { timestamp: string; - token_usage_agg: TokenUsageAggregate | null; }): Omit => ({ id: faker.string.uuid(), prompt_id: faker.string.uuid(), @@ -31,17 +29,14 @@ const getBaseAlert = ({ }); export const mockAlert = ({ - token_usage = false, type, }: { - token_usage?: boolean; type: "secret" | "malicious"; }): Alert => { const timestamp = faker.date.recent().toISOString(); const base: Omit = getBaseAlert({ timestamp, - token_usage_agg: token_usage ? TOKEN_USAGE_AGG : null, }); switch (type) { diff --git a/src/routes/__tests__/route-dashboard.test.tsx b/src/routes/__tests__/route-dashboard.test.tsx index 0bb5097c..6d422dd4 100644 --- a/src/routes/__tests__/route-dashboard.test.tsx +++ b/src/routes/__tests__/route-dashboard.test.tsx @@ -8,6 +8,7 @@ import { HttpResponse, http } from "msw"; import mockedAlerts from "@/mocks/msw/fixtures/GET_ALERTS.json"; import userEvent from "@testing-library/user-event"; import { RouteDashboard } from "../route-dashboard"; +import { mswEndpoint } from "@/test/msw-endpoint"; const fakeConversionation1 = { conversation: { @@ -79,7 +80,7 @@ const fakeConversionation2 = { function mockAlertsWithMaliciousPkg() { server.use( - http.get("*/workspaces/:name/alerts", () => { + http.get(mswEndpoint("/api/v1/workspaces/:workspace_name/alerts"), () => { return HttpResponse.json([fakeConversionation1, fakeConversionation2]); }), ); @@ -87,7 +88,7 @@ function mockAlertsWithMaliciousPkg() { function mockManyAlerts() { server.use( - http.get("*/workspaces/:name/alerts", () => { + http.get(mswEndpoint("/api/v1/workspaces/:workspace_name/alerts"), () => { return HttpResponse.json( [ ...mockedAlerts, From 39b9102bd53de335df38deb0ad7097099ae29e7f Mon Sep 17 00:00:00 2001 From: alex-mcgovern Date: Tue, 11 Feb 2025 09:19:49 +0000 Subject: [PATCH 13/19] fix: remove de-duplication logic --- .../hooks/use-query-get-workspace-alerts.ts | 36 ++-------------- src/hooks/use-query-get-workspace-messages.ts | 42 ++----------------- 2 files changed, 6 insertions(+), 72 deletions(-) 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 index 3ae9eb72..8ebc3446 100644 --- a/src/features/dashboard-alerts/hooks/use-query-get-workspace-alerts.ts +++ b/src/features/dashboard-alerts/hooks/use-query-get-workspace-alerts.ts @@ -1,15 +1,12 @@ import { - v1GetWorkspaceAlerts, V1GetWorkspaceAlertsData, V1GetWorkspaceAlertsResponse, } from "@/api/generated"; -import { v1GetWorkspaceAlertsQueryKey } from "@/api/generated/@tanstack/react-query.gen"; +import { v1GetWorkspaceAlertsOptions } from "@/api/generated/@tanstack/react-query.gen"; import { useQueryActiveWorkspaceName } from "@/hooks/use-query-active-workspace-name"; -import { dedupeByKeys } from "@/lib/dedupe-by-keys"; -import { isAlertCritical } from "@/lib/is-alert-critical"; import { getQueryCacheConfig } from "@/lib/react-query-utils"; import { useQuery } from "@tanstack/react-query"; -import { useCallback, useMemo } from "react"; +import { useMemo } from "react"; export function useQueryGetWorkspaceAlerts({ select, @@ -33,30 +30,6 @@ export function useQueryGetWorkspaceAlerts({ [activeWorkspaceName], ); - // Intentionally hand-composing the queryFn, as we have to iterate over every - // alert and remove any duplicates, which is an expensive operation. We should - // at least cache the result of this. - const queryFn = useCallback( - async ({ - queryKey, - signal, - }: { - queryKey: ReturnType; - signal: AbortSignal; - }) => { - const { data } = await v1GetWorkspaceAlerts({ - ...options, - ...queryKey[0], - signal, - throwOnError: true, - }); - - // Ugly de-duplication hack - return dedupeByKeys(data.filter(isAlertCritical), ["alert_id"]); - }, - [options], - ); - const { isPending: isAlertsPending, isFetching: isAlertsFetching, @@ -64,10 +37,7 @@ export function useQueryGetWorkspaceAlerts({ isRefetching: isAlertsRefetching, ...rest } = useQuery({ - // eslint-disable-next-line no-restricted-syntax - queryFn, - // eslint-disable-next-line no-restricted-syntax - queryKey: v1GetWorkspaceAlertsQueryKey(options), + ...v1GetWorkspaceAlertsOptions(options), ...getQueryCacheConfig("5s"), select, }); diff --git a/src/hooks/use-query-get-workspace-messages.ts b/src/hooks/use-query-get-workspace-messages.ts index e5150c51..2e7de014 100644 --- a/src/hooks/use-query-get-workspace-messages.ts +++ b/src/hooks/use-query-get-workspace-messages.ts @@ -2,13 +2,11 @@ import { useQuery } from "@tanstack/react-query"; import { V1GetWorkspaceMessagesResponse, V1GetWorkspaceMessagesData, - v1GetWorkspaceMessages, } from "@/api/generated"; -import { v1GetWorkspaceMessagesQueryKey } from "@/api/generated/@tanstack/react-query.gen"; +import { v1GetWorkspaceMessagesOptions } from "@/api/generated/@tanstack/react-query.gen"; import { useQueryActiveWorkspaceName } from "@/hooks/use-query-active-workspace-name"; import { getQueryCacheConfig } from "@/lib/react-query-utils"; -import { useCallback, useMemo } from "react"; -import { dedupeByKeys } from "@/lib/dedupe-by-keys"; +import { useMemo } from "react"; export const useQueryGetWorkspaceMessages = < T = V1GetWorkspaceMessagesResponse, @@ -28,42 +26,8 @@ export const useQueryGetWorkspaceMessages = < [activeWorkspaceName], ); - // Intentionally hand-composing the queryFn, as we have to iterate over every - // alert and remove any duplicates, which is an expensive operation. We should - // at least cache the result of this. - const queryFn = useCallback( - async ({ - queryKey, - signal, - }: { - queryKey: ReturnType; - signal: AbortSignal; - }) => { - const { data } = await v1GetWorkspaceMessages({ - ...options, - ...queryKey[0], - signal, - throwOnError: true, - }); - - // Ugly de-duplication hack - return data.map((conversation) => - conversation.alerts && conversation.alerts?.length > 0 - ? { - ...conversation, - alerts: dedupeByKeys(conversation.alerts, ["id"]), - } - : conversation, - ); - }, - [options], - ); - return useQuery({ - // eslint-disable-next-line no-restricted-syntax - queryFn, - // eslint-disable-next-line no-restricted-syntax - queryKey: v1GetWorkspaceMessagesQueryKey(options), + ...v1GetWorkspaceMessagesOptions(options), ...getQueryCacheConfig("5s"), select: select, }); From 22fe4a404b682a2b635ada258597ba68138bde26 Mon Sep 17 00:00:00 2001 From: alex-mcgovern Date: Tue, 11 Feb 2025 12:32:19 +0000 Subject: [PATCH 14/19] tidy up conversation detail with tabs, add tests --- package.json | 3 +- src/components/AlertDetail.tsx | 55 ----- src/components/Error.tsx | 62 ++--- src/components/heading.tsx | 4 +- ...sx => table-messages.token-usage.test.tsx} | 6 +- .../conversation-secrets-detected.tsx | 35 ++- .../section-conversation-secrets.tsx | 40 ++++ .../section-conversation-transcript.tsx | 72 ++++++ .../components/table-messages.tsx | 2 +- .../components/tabs-conversation.tsx | 73 ++++++ .../constants/table-messages-columns.ts | 7 +- .../hooks/use-conversation-by-id.tsx | 2 +- .../hooks/use-conversation-search-params.ts | 46 ++++ .../parse-unstructured-secrets-data.test.ts | 187 --------------- .../lib/get-conversation-title.ts | 17 ++ .../lib/get-provider-string.ts | 2 +- .../lib/parse-unstructured-secrets-data.ts | 18 -- .../workspace/components/table-workspaces.tsx | 6 +- src/hooks/useCurrentPromptStore.ts | 11 - src/lib/currency.ts | 9 +- src/lib/dedupe-by-keys.ts | 43 ---- src/lib/is-alert-malicious.ts | 13 -- src/lib/test-utils.tsx | 7 +- src/lib/utils.ts | 58 +---- src/routes/__tests__/route-chat.test.tsx | 221 +++++++++++++----- src/routes/__tests__/route-workspace.test.tsx | 2 +- src/routes/route-chat.tsx | 152 +++++------- 27 files changed, 539 insertions(+), 614 deletions(-) delete mode 100644 src/components/AlertDetail.tsx rename src/features/dashboard-messages/components/__tests__/{table-messages.token-usage.tsx => table-messages.token-usage.test.tsx} (98%) create mode 100644 src/features/dashboard-messages/components/section-conversation-secrets.tsx create mode 100644 src/features/dashboard-messages/components/section-conversation-transcript.tsx create mode 100644 src/features/dashboard-messages/components/tabs-conversation.tsx create mode 100644 src/features/dashboard-messages/hooks/use-conversation-search-params.ts delete mode 100644 src/features/dashboard-messages/lib/__tests__/parse-unstructured-secrets-data.test.ts create mode 100644 src/features/dashboard-messages/lib/get-conversation-title.ts delete mode 100644 src/features/dashboard-messages/lib/parse-unstructured-secrets-data.ts delete mode 100644 src/hooks/useCurrentPromptStore.ts delete mode 100644 src/lib/dedupe-by-keys.ts diff --git a/package.json b/package.json index 45f9e2fe..51db9fe4 100644 --- a/package.json +++ b/package.json @@ -41,8 +41,7 @@ "tailwind-variants": "^0.3.0", "tailwindcss-animate": "^1.0.7", "ts-pattern": "^5.6.2", - "zod": "^3.24.1", - "zustand": "^5.0.3" + "zod": "^3.24.1" }, "devDependencies": { "@eslint/js": "^9.15.0", diff --git a/src/components/AlertDetail.tsx b/src/components/AlertDetail.tsx deleted file mode 100644 index 92b29ab4..00000000 --- a/src/components/AlertDetail.tsx +++ /dev/null @@ -1,55 +0,0 @@ -import { Alert } from "@/api/generated"; -import { isAlertMalicious } from "@/lib/is-alert-malicious"; -import { isAlertSecret } from "@/lib/is-alert-secret"; -import { Markdown } from "./Markdown"; - -type MaliciousPkgType = { - name: string; - type: string; - status: string; - description: string; -}; - -export function AlertDetail({ alert }: { alert: Alert }) { - if (alert.trigger_string === null) return "N/A"; - if (isAlertSecret(alert) && typeof alert.trigger_string === "string") { - return ( -
    - {alert.trigger_string} -
    - ); - } - - if (isAlertMalicious(alert) && typeof alert.trigger_string === "object") { - const maliciousPkg = alert.trigger_string as MaliciousPkgType; - - return ( -
    - -   - - {maliciousPkg.type}/{maliciousPkg.name} - - {maliciousPkg.status && ( - <> -
    - {maliciousPkg.status} - - )} - {maliciousPkg.description && ( - <> -
    - {" "} - {maliciousPkg.description} - - )} -
    - ); - } - return null; -} diff --git a/src/components/Error.tsx b/src/components/Error.tsx index eae26eed..8e1561f7 100644 --- a/src/components/Error.tsx +++ b/src/components/Error.tsx @@ -3,40 +3,46 @@ import { AlertCircle } from "@untitled-ui/icons-react"; import { Header } from "../features/header/components/header"; import { Card } from "@stacklok/ui-kit"; +export function ErrorFallbackContent() { + return ( +
    + + +
    + An error occurred +
    +
    + If this issue persists, please reach out to us on{" "} + + Discord + {" "} + or open a new{" "} + + Github issue + +
    +
    +
    + ); +} + export function Error() { return (
    -
    - - -
    - An error occurred -
    -
    - If this issue persists, please reach out to us on{" "} - - Discord - {" "} - or open a new{" "} - - Github issue - -
    -
    -
    +
    ); } diff --git a/src/components/heading.tsx b/src/components/heading.tsx index 5b489d80..da655a68 100644 --- a/src/components/heading.tsx +++ b/src/components/heading.tsx @@ -7,13 +7,13 @@ export function PageHeading({ level, }: { level: ComponentProps["level"]; - title: string; + title: React.ReactNode; children?: React.ReactNode; }) { return ( {title} {children} diff --git a/src/features/dashboard-messages/components/__tests__/table-messages.token-usage.tsx b/src/features/dashboard-messages/components/__tests__/table-messages.token-usage.test.tsx similarity index 98% rename from src/features/dashboard-messages/components/__tests__/table-messages.token-usage.tsx rename to src/features/dashboard-messages/components/__tests__/table-messages.token-usage.test.tsx index 2e7e30fa..2b949ba3 100644 --- a/src/features/dashboard-messages/components/__tests__/table-messages.token-usage.tsx +++ b/src/features/dashboard-messages/components/__tests__/table-messages.token-usage.test.tsx @@ -31,7 +31,7 @@ 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 { getByRole, getByTestId, queryByText } = render(); @@ -46,7 +46,7 @@ test("renders token usage cell correctly", async () => { expect( getByRole("gridcell", { name: `${formatNumberCompact(INPUT_TOKENS)} ${formatNumberCompact(OUTPUT_TOKENS)}`, - }) + }), ).toBeVisible(); }); @@ -54,7 +54,7 @@ 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 { getByText, queryByText } = render(); diff --git a/src/features/dashboard-messages/components/conversation-secrets-detected.tsx b/src/features/dashboard-messages/components/conversation-secrets-detected.tsx index 833cb515..c82b11bf 100644 --- a/src/features/dashboard-messages/components/conversation-secrets-detected.tsx +++ b/src/features/dashboard-messages/components/conversation-secrets-detected.tsx @@ -1,28 +1,23 @@ import { Alert } from "@/api/generated"; -import { parseUnstructuredSecretsData } from "../lib/parse-unstructured-secrets-data"; import { ReactNode } from "react"; +import Markdown from "react-markdown"; function ConversationSecretsList({ children }: { children: ReactNode }) { - return ( -
      {children}
    - ); + return
      {children}
    ; } -function ConversationSecretsListItem({ - title, - value, -}: { - title: ReactNode; - value: ReactNode; -}) { +function ConversationSecretsListItem({ children }: { children: ReactNode }) { return ( -
  • - {title} - {value} +
  • + {children}
  • ); } +function formatTriggerString(string: string): string { + return string.replace(/REDACTED<[^>]*?>/g, "**REDACTED**"); +} + // NOTE: The secrets detection backend code appears to be returning fairly // unstructured data with a lot of false positives. This is not actually // referenced in the frontend yet. @@ -30,16 +25,12 @@ export function ConversationSecretsDetected({ alerts }: { alerts: Alert[] }) { return ( {alerts.map((a) => { - const { key, redactedValue } = parseUnstructuredSecretsData(a) || {}; - - if (!key) return null; + if (typeof a.trigger_string !== "string") return null; return ( - + + {formatTriggerString(a.trigger_string)} + ); })} diff --git a/src/features/dashboard-messages/components/section-conversation-secrets.tsx b/src/features/dashboard-messages/components/section-conversation-secrets.tsx new file mode 100644 index 00000000..576a3ed3 --- /dev/null +++ b/src/features/dashboard-messages/components/section-conversation-secrets.tsx @@ -0,0 +1,40 @@ +import { IllustrationDone } from "@stacklok/ui-kit"; +import { isAlertSecret } from "@/lib/is-alert-secret"; +import { ConversationSecretsDetected } from "./conversation-secrets-detected"; +import { EmptyState } from "@/components/empty-state"; +import { emptyStateStrings } from "../constants/strings"; +import { Conversation } from "@/api/generated"; + +export function SectionConversationSecrets({ + conversation, +}: { + conversation: Conversation; +}) { + const secrets = conversation.alerts?.filter(isAlertSecret) ?? []; + + if (secrets.length === 0) + return ( + + ); + + return ( +
    +

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

    +

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

    + + +
    + ); +} diff --git a/src/features/dashboard-messages/components/section-conversation-transcript.tsx b/src/features/dashboard-messages/components/section-conversation-transcript.tsx new file mode 100644 index 00000000..b4da83d4 --- /dev/null +++ b/src/features/dashboard-messages/components/section-conversation-transcript.tsx @@ -0,0 +1,72 @@ +import { Markdown } from "@/components/Markdown"; +import { + ChatBubble, + ChatBubbleAvatar, + ChatBubbleMessage, +} from "@/components/ui/chat/chat-bubble"; +import { ChatMessageList } from "@/components/ui/chat/chat-message-list"; +import { sanitizeQuestionPrompt } from "@/lib/utils"; +import { Heading } from "@stacklok/ui-kit"; +import { ConversationSummary } from "./conversation-summary"; +import { Conversation } from "@/api/generated"; + +export function SectionConversationTranscript({ + conversation, +}: { + conversation: Conversation; +}) { + return ( + <> +
    + + Conversation summary + + + +
    +
    + + Conversation transcript + + + + {(conversation?.question_answers ?? []).map( + ({ question, answer }, index) => ( +
    + + + + + {sanitizeQuestionPrompt({ + question: question?.message ?? "", + answer: answer?.message ?? "", + })} + + + + + + + {answer?.message ?? ""} + + +
    + ), + )} +
    +
    + + ); +} diff --git a/src/features/dashboard-messages/components/table-messages.tsx b/src/features/dashboard-messages/components/table-messages.tsx index cb8249c0..9f4b017f 100644 --- a/src/features/dashboard-messages/components/table-messages.tsx +++ b/src/features/dashboard-messages/components/table-messages.tsx @@ -40,7 +40,7 @@ function getTypeText(type: QuestionType) { case QuestionType.CHAT: return "Chat"; case QuestionType.FIM: - return "Code Suggestion"; + return "Fill in the middle (FIM)"; default: return "Unknown"; } diff --git a/src/features/dashboard-messages/components/tabs-conversation.tsx b/src/features/dashboard-messages/components/tabs-conversation.tsx new file mode 100644 index 00000000..24106591 --- /dev/null +++ b/src/features/dashboard-messages/components/tabs-conversation.tsx @@ -0,0 +1,73 @@ +import { isAlertSecret } from "../../../lib/is-alert-secret"; +import { + Tab as BaseTab, + Tabs, + TabList, + TabPanel, + Badge, +} from "@stacklok/ui-kit"; +import { AlertsFilterView } from "../hooks/use-messages-filter-search-params"; + +import { + ConversationView, + useConversationSearchParams, +} from "../hooks/use-conversation-search-params"; +import { useConversationById } from "../hooks/use-conversation-by-id"; + +function Tab({ + id, + title, + count, +}: { + title: string; + id: ConversationView; + count?: number; +}) { + return ( + + {title} + {count ? ( + + {count} + + ) : null} + + ); +} + +export function TabsConversation({ + children, + id, +}: { + id: string; + children: React.ReactNode; +}) { + const { state, setView } = useConversationSearchParams(); + + const { data } = useConversationById(id); + + const secretsCount = data?.alerts?.filter(isAlertSecret).length ?? 0; + + return ( + setView(key.toString() as ConversationView)} + selectedKey={state.view} + defaultSelectedKey={AlertsFilterView.ALL} + > + + + + + + {children} + + ); +} diff --git a/src/features/dashboard-messages/constants/table-messages-columns.ts b/src/features/dashboard-messages/constants/table-messages-columns.ts index f851fbb6..5b002071 100644 --- a/src/features/dashboard-messages/constants/table-messages-columns.ts +++ b/src/features/dashboard-messages/constants/table-messages-columns.ts @@ -19,7 +19,7 @@ export const TABLE_MESSAGES_COLUMNS: TableMessagesColumn[] = [ id: "type", children: "Type", minWidth: 170, - maxWidth: 190, + maxWidth: 200, }, { id: "prompt", @@ -28,8 +28,9 @@ export const TABLE_MESSAGES_COLUMNS: TableMessagesColumn[] = [ { id: "alerts", children: "Alerts", - alignment: "center", - width: 110, + alignment: "start", + minWidth: 110, + maxWidth: 160, }, { id: "token_usage", diff --git a/src/features/dashboard-messages/hooks/use-conversation-by-id.tsx b/src/features/dashboard-messages/hooks/use-conversation-by-id.tsx index 44bae983..b63549f4 100644 --- a/src/features/dashboard-messages/hooks/use-conversation-by-id.tsx +++ b/src/features/dashboard-messages/hooks/use-conversation-by-id.tsx @@ -2,6 +2,6 @@ import { useQueryGetWorkspaceMessages } from "@/hooks/use-query-get-workspace-me export function useConversationById(id: string) { return useQueryGetWorkspaceMessages({ - select: (d) => d.find((c) => c.chat_id === id), + select: (d) => d.find((c) => c.chat_id === id) ?? undefined, }); } diff --git a/src/features/dashboard-messages/hooks/use-conversation-search-params.ts b/src/features/dashboard-messages/hooks/use-conversation-search-params.ts new file mode 100644 index 00000000..7194dd9d --- /dev/null +++ b/src/features/dashboard-messages/hooks/use-conversation-search-params.ts @@ -0,0 +1,46 @@ +import { useCallback } from "react"; +import { useSearchParams } from "react-router-dom"; +import { z } from "zod"; + +export enum ConversationView { + OVERVIEW = "overview", + SECRETS = "secrets", +} + +const conversationParamsSchema = z.object({ + view: z + .nativeEnum(ConversationView) + .optional() + .default(ConversationView.OVERVIEW), +}); + +type ConversationParamsSchema = z.input; + +const DEFAULT_FILTER = { + view: ConversationView.OVERVIEW, +} as const satisfies ConversationParamsSchema; + +export const useConversationSearchParams = () => { + const [searchParams, setSearchParams] = useSearchParams( + new URLSearchParams(DEFAULT_FILTER), + ); + + const setView = useCallback( + (view: ConversationView) => { + setSearchParams((prev) => { + if (view) prev.set("view", view); + if (!view) prev.delete("view"); + + prev.delete("page"); + return prev; + }); + }, + [setSearchParams], + ); + + const state = conversationParamsSchema.parse( + Object.fromEntries(searchParams), + ); + + return { state, setView }; +}; diff --git a/src/features/dashboard-messages/lib/__tests__/parse-unstructured-secrets-data.test.ts b/src/features/dashboard-messages/lib/__tests__/parse-unstructured-secrets-data.test.ts deleted file mode 100644 index ff3e038f..00000000 --- a/src/features/dashboard-messages/lib/__tests__/parse-unstructured-secrets-data.test.ts +++ /dev/null @@ -1,187 +0,0 @@ -import { it } from "vitest"; -import { parseUnstructuredSecretsData } from "../parse-unstructured-secrets-data"; - -const SECRETS_ALERTS_DATA = [ - { - id: "e03ae066-7720-4926-be65-cb7ca39f5bd8", - prompt_id: "9fafdd84-0ea4-4bb7-8a94-92a24a1db22b", - code_snippet: { - code: '; filepath: /Users/alexmcgovern/src/codegate-demonstration/conf.ini\nGITHUB_TOKEN="ghp_aBcDeFgHiJkLmNoPqRsTuVwXyZ0123456789"\nAWS_ACCESS_KEY_ID="AKIAIOSFODNN7EXAMPLE"\nAWS_SECRET_ACCESS_KEY="wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY"\n', - language: null, - filepath: null, - libraries: [], - file_extension: null, - }, - trigger_string: - '**Secret Detected** 🔒\n- Service: GitHub\n- Type: Access Token\n- Key: GITHUB_TOKEN\n- Line Number: 2\n- Context:\n```\n; filepath: /Users/alexmcgovern/src/codegate-demonstration/conf.ini\nGITHUB_TOKEN="REDACTED<$Zd/EcxDvBqoiS5sXYD+M9HiqMe24rESHlpt+rwqft0laQydrVmDTKSDMaBegRMJt70sIhLlWgQtWMQQfRgv3QvtFUY5rB0tJUMaxkUOg5g==>"\nAWS_ACCESS_KEY_ID="REDACTED<$HhZp9SPhgSXfOP5g5B7n7914LfVti/lxQylEcmEJvZmOb17ivsrWq6VjucGJHTgvi8YJx5yEy066s1U=>"\nAWS_SECRET_ACCESS_KEY="REDACTED<$twn1O8vAberMoWTwkCBQa8IC43aqNafNGacxiTNgRSHwQWOJr9yKeoWpty4yZhoxfBWNI3kw0+Kc1R1/KxINhO7SNMUZ07B4fzKHDbq8Kw==>"\n\n```', - trigger_type: "codegate-secrets", - trigger_category: "critical", - timestamp: "2025-02-10T13:04:47.522168Z", - }, - { - id: "65b2be5c-e031-4f2d-8e6c-ff3f3d7a8f1e", - prompt_id: "9fafdd84-0ea4-4bb7-8a94-92a24a1db22b", - code_snippet: { - code: '; filepath: /Users/alexmcgovern/src/codegate-demonstration/conf.ini\nGITHUB_TOKEN="ghp_aBcDeFgHiJkLmNoPqRsTuVwXyZ0123456789"\nAWS_ACCESS_KEY_ID="AKIAIOSFODNN7EXAMPLE"\nAWS_SECRET_ACCESS_KEY="wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY"\n', - language: null, - filepath: null, - libraries: [], - file_extension: null, - }, - trigger_string: - '**Secret Detected** 🔒\n- Service: Amazon\n- Type: Access Key\n- Key: AWS_ACCESS_KEY_ID\n- Line Number: 3\n- Context:\n```\n; filepath: /Users/alexmcgovern/src/codegate-demonstration/conf.ini\nGITHUB_TOKEN="REDACTED<$Zd/EcxDvBqoiS5sXYD+M9HiqMe24rESHlpt+rwqft0laQydrVmDTKSDMaBegRMJt70sIhLlWgQtWMQQfRgv3QvtFUY5rB0tJUMaxkUOg5g==>"\nAWS_ACCESS_KEY_ID="REDACTED<$HhZp9SPhgSXfOP5g5B7n7914LfVti/lxQylEcmEJvZmOb17ivsrWq6VjucGJHTgvi8YJx5yEy066s1U=>"\nAWS_SECRET_ACCESS_KEY="REDACTED<$twn1O8vAberMoWTwkCBQa8IC43aqNafNGacxiTNgRSHwQWOJr9yKeoWpty4yZhoxfBWNI3kw0+Kc1R1/KxINhO7SNMUZ07B4fzKHDbq8Kw==>"\n\n```', - trigger_type: "codegate-secrets", - trigger_category: "critical", - timestamp: "2025-02-10T13:04:47.522146Z", - }, - { - id: "8d48ff1e-6feb-447f-82c5-21ad5d89ab53", - prompt_id: "9fafdd84-0ea4-4bb7-8a94-92a24a1db22b", - code_snippet: { - code: '; filepath: /Users/alexmcgovern/src/codegate-demonstration/conf.ini\nGITHUB_TOKEN="ghp_aBcDeFgHiJkLmNoPqRsTuVwXyZ0123456789"\nAWS_ACCESS_KEY_ID="AKIAIOSFODNN7EXAMPLE"\nAWS_SECRET_ACCESS_KEY="wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY"\n', - language: null, - filepath: null, - libraries: [], - file_extension: null, - }, - trigger_string: - '**Secret Detected** 🔒\n- Service: High Entropy\n- Type: Potential Secret\n- Key: AWS_SECRET_ACCESS_KEY\n- Line Number: 4\n- Context:\n```\nGITHUB_TOKEN="REDACTED<$Zd/EcxDvBqoiS5sXYD+M9HiqMe24rESHlpt+rwqft0laQydrVmDTKSDMaBegRMJt70sIhLlWgQtWMQQfRgv3QvtFUY5rB0tJUMaxkUOg5g==>"\nAWS_ACCESS_KEY_ID="REDACTED<$HhZp9SPhgSXfOP5g5B7n7914LfVti/lxQylEcmEJvZmOb17ivsrWq6VjucGJHTgvi8YJx5yEy066s1U=>"\nAWS_SECRET_ACCESS_KEY="REDACTED<$twn1O8vAberMoWTwkCBQa8IC43aqNafNGacxiTNgRSHwQWOJr9yKeoWpty4yZhoxfBWNI3kw0+Kc1R1/KxINhO7SNMUZ07B4fzKHDbq8Kw==>"\n\n```', - trigger_type: "codegate-secrets", - trigger_category: "critical", - timestamp: "2025-02-10T13:04:47.522127Z", - }, - { - id: "5faea6cb-1fe7-4d2c-8bd1-f33a361cb4f5", - prompt_id: "9fafdd84-0ea4-4bb7-8a94-92a24a1db22b", - code_snippet: null, - trigger_string: - '**Secret Detected** 🔒\n- Service: GitHub\n- Type: Access Token\n- Key: GITHUB_TOKEN\n- Line Number: 5\n- Context:\n```\nExcerpt from conf.ini, lines 1 to 4:\n```ini\nGITHUB_TOKEN="REDACTED<$p0nBrp3QSPMt7DypsFgSRdYfrbA3546N2a370UpV15EMHoo5XFeABiFYMt5R2IkoTGDB/FOFlrr3jVo1H9CJ0evRJHPbBhC1hXaQPjWk1A==>"\n\n```', - trigger_type: "codegate-secrets", - trigger_category: "critical", - timestamp: "2025-02-10T13:04:44.086559Z", - }, - { - id: "2f29042b-fbdb-4722-96f5-597a73faf72e", - prompt_id: "9fafdd84-0ea4-4bb7-8a94-92a24a1db22b", - code_snippet: { - code: 'AWS_ACCESS_KEY_ID="AKIAIOSFODNN7EXAMPLE"\nAWS_SECRET_ACCESS_KEY="wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY"\n', - language: null, - filepath: 'GITHUB_TOKEN="ghp_aBcDeFgHiJkLmNoPqRsTuVwXyZ0123456789"', - libraries: [], - file_extension: "", - }, - trigger_string: - '**Secret Detected** 🔒\n- Service: Amazon\n- Type: Access Key\n- Key: AWS_ACCESS_KEY_ID\n- Line Number: 1\n- Context:\n```\nAWS_ACCESS_KEY_ID="REDACTED<$A9kjOiTR6L8YXNq8DXM/e7yv6omTWpglmBmIZOOrXNhPvmE9e6c6GOChWB1v6wRpboDO07Z45nuSMu4=>"\nAWS_SECRET_ACCESS_KEY="REDACTED<$ZC/eGkm6541ixJ48573XHvpBOumat4xhRyV8ROZwL3fgS2CHhi73g+kzYEkb/bkWS+tJajjICWm/1kXjQ6b0suL7PbcwnbJHKtkYcujWxw==>"\n\n```', - trigger_type: "codegate-secrets", - trigger_category: "critical", - timestamp: "2025-02-10T13:04:44.086190Z", - }, - { - id: "12e79095-192d-442a-9e7b-4e5f46f6e0dc", - prompt_id: "9fafdd84-0ea4-4bb7-8a94-92a24a1db22b", - code_snippet: { - code: 'AWS_ACCESS_KEY_ID="AKIAIOSFODNN7EXAMPLE"\nAWS_SECRET_ACCESS_KEY="wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY"\n', - language: null, - filepath: 'GITHUB_TOKEN="ghp_aBcDeFgHiJkLmNoPqRsTuVwXyZ0123456789"', - libraries: [], - file_extension: "", - }, - trigger_string: - '**Secret Detected** 🔒\n- Service: High Entropy\n- Type: Potential Secret\n- Key: AWS_SECRET_ACCESS_KEY\n- Line Number: 2\n- Context:\n```\nAWS_ACCESS_KEY_ID="REDACTED<$A9kjOiTR6L8YXNq8DXM/e7yv6omTWpglmBmIZOOrXNhPvmE9e6c6GOChWB1v6wRpboDO07Z45nuSMu4=>"\nAWS_SECRET_ACCESS_KEY="REDACTED<$ZC/eGkm6541ixJ48573XHvpBOumat4xhRyV8ROZwL3fgS2CHhi73g+kzYEkb/bkWS+tJajjICWm/1kXjQ6b0suL7PbcwnbJHKtkYcujWxw==>"\n\n```', - trigger_type: "codegate-secrets", - trigger_category: "critical", - timestamp: "2025-02-10T13:04:44.086178Z", - }, - { - id: "95df388c-54a6-4e3e-8f70-5ace465c9609", - prompt_id: "9fafdd84-0ea4-4bb7-8a94-92a24a1db22b", - code_snippet: null, - trigger_string: - '**Secret Detected** 🔒\n- Service: GitHub\n- Type: Access Token\n- Key: GITHUB_TOKEN\n- Line Number: 5\n- Context:\n```\nExcerpt from conf.ini, lines 1 to 4:\n```ini\nGITHUB_TOKEN="REDACTED<$iPKajC6vbjifexEIptk0LcH0bOdsPaGcTKT5S5QmjzmnWFWfT3Ptmfq8c61oHV8D23qQVcLYRL+dK3Py/5tCc1qU2tyz/cfCMxPnbdhbJQ==>"\n\n```', - trigger_type: "codegate-secrets", - trigger_category: "critical", - timestamp: "2025-02-10T13:04:44.078467Z", - }, - { - id: "4a0111a4-0a82-4cc7-98e0-3c4009456310", - prompt_id: "9fafdd84-0ea4-4bb7-8a94-92a24a1db22b", - code_snippet: { - code: 'AWS_ACCESS_KEY_ID="AKIAIOSFODNN7EXAMPLE"\nAWS_SECRET_ACCESS_KEY="wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY"\n', - language: null, - filepath: 'GITHUB_TOKEN="ghp_aBcDeFgHiJkLmNoPqRsTuVwXyZ0123456789"', - libraries: [], - file_extension: "", - }, - trigger_string: - '**Secret Detected** 🔒\n- Service: Amazon\n- Type: Access Key\n- Key: AWS_ACCESS_KEY_ID\n- Line Number: 1\n- Context:\n```\nAWS_ACCESS_KEY_ID="REDACTED<$DH3fMpVAb+fHZKz9hu437l6rN9VF1BXciCpk+q5OGGYTuXVWY1uCj44q7FsRF9jkNSNi/1bfUmjjwJg=>"\nAWS_SECRET_ACCESS_KEY="REDACTED<$iZF8W1J0R3ErViM/MyR6djFFWwZel1RO/doE8cw2ghEzu745MvYloawzN/FzXPPD2gRllBMR1Zi8cvdqDutHEKyMN1xXPCBoqAkyH6h2Cw==>"\n\n```', - trigger_type: "codegate-secrets", - trigger_category: "critical", - timestamp: "2025-02-10T13:04:44.077959Z", - }, - { - id: "03a66ff4-ef9c-49d0-9481-b5569ddc7cbf", - prompt_id: "9fafdd84-0ea4-4bb7-8a94-92a24a1db22b", - code_snippet: { - code: 'AWS_ACCESS_KEY_ID="AKIAIOSFODNN7EXAMPLE"\nAWS_SECRET_ACCESS_KEY="wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY"\n', - language: null, - filepath: 'GITHUB_TOKEN="ghp_aBcDeFgHiJkLmNoPqRsTuVwXyZ0123456789"', - libraries: [], - file_extension: "", - }, - trigger_string: - '**Secret Detected** 🔒\n- Service: High Entropy\n- Type: Potential Secret\n- Key: AWS_SECRET_ACCESS_KEY\n- Line Number: 2\n- Context:\n```\nAWS_ACCESS_KEY_ID="REDACTED<$DH3fMpVAb+fHZKz9hu437l6rN9VF1BXciCpk+q5OGGYTuXVWY1uCj44q7FsRF9jkNSNi/1bfUmjjwJg=>"\nAWS_SECRET_ACCESS_KEY="REDACTED<$iZF8W1J0R3ErViM/MyR6djFFWwZel1RO/doE8cw2ghEzu745MvYloawzN/FzXPPD2gRllBMR1Zi8cvdqDutHEKyMN1xXPCBoqAkyH6h2Cw==>"\n\n```', - trigger_type: "codegate-secrets", - trigger_category: "critical", - timestamp: "2025-02-10T13:04:44.077941Z", - }, -]; - -it("should parse the unstructured secret alerts data correctly", () => { - expect(SECRETS_ALERTS_DATA.map(parseUnstructuredSecretsData)).toStrictEqual([ - { - key: "GITHUB_TOKEN", - redactedValue: - 'GITHUB_TOKEN="REDACTED<$Zd/EcxDvBqoiS5sXYD+M9HiqMe24rESHlpt+rwqft0laQydrVmDTKSDMaBegRMJt70sIhLlWgQtWMQQfRgv3QvtFUY5rB0tJUMaxkUOg5g==>"', - }, - { - key: "AWS_ACCESS_KEY_ID", - redactedValue: - 'AWS_ACCESS_KEY_ID="REDACTED<$HhZp9SPhgSXfOP5g5B7n7914LfVti/lxQylEcmEJvZmOb17ivsrWq6VjucGJHTgvi8YJx5yEy066s1U=>"', - }, - { - key: "AWS_SECRET_ACCESS_KEY", - redactedValue: - 'AWS_SECRET_ACCESS_KEY="REDACTED<$twn1O8vAberMoWTwkCBQa8IC43aqNafNGacxiTNgRSHwQWOJr9yKeoWpty4yZhoxfBWNI3kw0+Kc1R1/KxINhO7SNMUZ07B4fzKHDbq8Kw==>"', - }, - { - key: "GITHUB_TOKEN", - redactedValue: - 'GITHUB_TOKEN="REDACTED<$p0nBrp3QSPMt7DypsFgSRdYfrbA3546N2a370UpV15EMHoo5XFeABiFYMt5R2IkoTGDB/FOFlrr3jVo1H9CJ0evRJHPbBhC1hXaQPjWk1A==>"', - }, - { - key: "AWS_ACCESS_KEY_ID", - redactedValue: - 'AWS_ACCESS_KEY_ID="REDACTED<$A9kjOiTR6L8YXNq8DXM/e7yv6omTWpglmBmIZOOrXNhPvmE9e6c6GOChWB1v6wRpboDO07Z45nuSMu4=>"', - }, - { - key: "AWS_SECRET_ACCESS_KEY", - redactedValue: - 'AWS_SECRET_ACCESS_KEY="REDACTED<$ZC/eGkm6541ixJ48573XHvpBOumat4xhRyV8ROZwL3fgS2CHhi73g+kzYEkb/bkWS+tJajjICWm/1kXjQ6b0suL7PbcwnbJHKtkYcujWxw==>"', - }, - { - key: "GITHUB_TOKEN", - redactedValue: - 'GITHUB_TOKEN="REDACTED<$iPKajC6vbjifexEIptk0LcH0bOdsPaGcTKT5S5QmjzmnWFWfT3Ptmfq8c61oHV8D23qQVcLYRL+dK3Py/5tCc1qU2tyz/cfCMxPnbdhbJQ==>"', - }, - { - key: "AWS_ACCESS_KEY_ID", - redactedValue: - 'AWS_ACCESS_KEY_ID="REDACTED<$DH3fMpVAb+fHZKz9hu437l6rN9VF1BXciCpk+q5OGGYTuXVWY1uCj44q7FsRF9jkNSNi/1bfUmjjwJg=>"', - }, - { - key: "AWS_SECRET_ACCESS_KEY", - redactedValue: - 'AWS_SECRET_ACCESS_KEY="REDACTED<$iZF8W1J0R3ErViM/MyR6djFFWwZel1RO/doE8cw2ghEzu745MvYloawzN/FzXPPD2gRllBMR1Zi8cvdqDutHEKyMN1xXPCBoqAkyH6h2Cw==>"', - }, - ]); -}); diff --git a/src/features/dashboard-messages/lib/get-conversation-title.ts b/src/features/dashboard-messages/lib/get-conversation-title.ts new file mode 100644 index 00000000..948d890b --- /dev/null +++ b/src/features/dashboard-messages/lib/get-conversation-title.ts @@ -0,0 +1,17 @@ +import { Conversation, QuestionType } from "@/api/generated"; +import { getProviderString } from "./get-provider-string"; + +function getTypeString(type: Conversation["type"]) { + switch (type) { + case QuestionType.CHAT: + return "Chat"; + case QuestionType.FIM: + return "Fill in the middle (FIM)"; + default: + return type; + } +} + +export function getConversationTitle(conversation: Conversation) { + return `${getTypeString(conversation.type)} with ${getProviderString(conversation.provider)}`; +} diff --git a/src/features/dashboard-messages/lib/get-provider-string.ts b/src/features/dashboard-messages/lib/get-provider-string.ts index 9560eec2..e05360a1 100644 --- a/src/features/dashboard-messages/lib/get-provider-string.ts +++ b/src/features/dashboard-messages/lib/get-provider-string.ts @@ -3,7 +3,7 @@ export function getProviderString(provider: string | null): string { case "copilot": return "Github Copilot"; case null: - return "N/A"; + return "Unknown provider"; default: return provider; } diff --git a/src/features/dashboard-messages/lib/parse-unstructured-secrets-data.ts b/src/features/dashboard-messages/lib/parse-unstructured-secrets-data.ts deleted file mode 100644 index d441220b..00000000 --- a/src/features/dashboard-messages/lib/parse-unstructured-secrets-data.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { Alert } from "@/api/generated"; - -export function parseUnstructuredSecretsData(alert: Alert): { - key: string | null; - redactedValue: string | null; -} { - const keyMatch = (alert.trigger_string as string)?.match(/- Key: (\w+)/); - - const key = keyMatch?.[1] ?? null; - - const redactedValueMatch = key - ? (alert.trigger_string as string)?.match(new RegExp(`${key}="[^"]*?"`)) - : null; - - const redactedValue = redactedValueMatch?.[0] ?? null; - - return { key, redactedValue }; -} diff --git a/src/features/workspace/components/table-workspaces.tsx b/src/features/workspace/components/table-workspaces.tsx index 42654b18..c4a1fac2 100644 --- a/src/features/workspace/components/table-workspaces.tsx +++ b/src/features/workspace/components/table-workspaces.tsx @@ -26,7 +26,7 @@ function CellName({ }) { if (isArchived) return ( - + {name}    @@ -37,7 +37,7 @@ function CellName({ if (isActive) return ( - + {name}    @@ -76,7 +76,7 @@ export function TableWorkspaces() { isActive={workspace.is_active} isArchived={workspace.isArchived} /> - + void; -}; - -export const useCurrentPromptStore = create((set) => ({ - currentPromptId: "", - setCurrentPromptId: (id: string) => set({ currentPromptId: id }), -})); diff --git a/src/lib/currency.ts b/src/lib/currency.ts index 70a83d24..accacdb3 100644 --- a/src/lib/currency.ts +++ b/src/lib/currency.ts @@ -7,7 +7,7 @@ type FormatCurrencyOptions = { to_minor?: boolean; }; -export const getCurrencyFormatOptions = (currency: Currency) => { +const getCurrencyFormatOptions = (currency: Currency) => { return new Intl.NumberFormat(undefined, { currency: currency, currencyDisplay: "code", @@ -70,10 +70,3 @@ export function convertCurrencyFromMinor( ): number { return amount / 10 ** getDigits(currency); } - -export function formatCurrencyWithTrailingZeroes( - number: number, - currency: Currency, -) { - return number.toFixed(getDigits(currency)); -} diff --git a/src/lib/dedupe-by-keys.ts b/src/lib/dedupe-by-keys.ts deleted file mode 100644 index 951e3c5d..00000000 --- a/src/lib/dedupe-by-keys.ts +++ /dev/null @@ -1,43 +0,0 @@ -// eslint-disable-next-line @typescript-eslint/no-explicit-any -const deepEquals = (a: any, b: any): boolean => { - if (a === b) return true; - - if ( - typeof a !== "object" || - typeof b !== "object" || - a === null || - b === null - ) { - return false; - } - - const keysA = Object.keys(a); - const keysB = Object.keys(b); - - if (keysA.length !== keysB.length) return false; - - for (const key of keysA) { - if (!keysB.includes(key)) return false; - if (!deepEquals(a[key], b[key])) return false; - } - - return true; -}; - -export const dedupeByKeys = >( - items: (T | null | undefined)[], - keys: (keyof T)[], -) => - items.filter(Boolean).reduce((acc, curr) => { - const alreadySeen = acc.some((item) => - keys.some((key) => { - if (typeof item[key] === "string") return item[key] === curr?.[key]; - if (typeof item[key] === "object") - return deepEquals(item[key], curr?.[key]); - }), - ); - if (!alreadySeen && curr) { - acc.push(curr); - } - return acc; - }, [] as T[]); diff --git a/src/lib/is-alert-malicious.ts b/src/lib/is-alert-malicious.ts index 000e3919..4a458dba 100644 --- a/src/lib/is-alert-malicious.ts +++ b/src/lib/is-alert-malicious.ts @@ -1,18 +1,5 @@ import { Alert, AlertConversation, Conversation } from "@/api/generated"; -/** @deprecated */ -export function isAlertConversationMalicious( - alert: AlertConversation | null, -): alert is AlertConversation { - return ( - alert?.trigger_category === "critical" && - alert.trigger_string !== null && - typeof alert.trigger_string === "object" && - "status" in alert.trigger_string && - alert.trigger_string.status === "malicious" - ); -} - export function isConversationWithMaliciousAlerts( conversation: Conversation | null, ): boolean { diff --git a/src/lib/test-utils.tsx b/src/lib/test-utils.tsx index d99f3436..cf3e1394 100644 --- a/src/lib/test-utils.tsx +++ b/src/lib/test-utils.tsx @@ -27,6 +27,9 @@ export const TestQueryClientProvider = ({ new QueryClient({ defaultOptions: { queries: { + refetchOnMount: true, + refetchOnReconnect: true, + refetchOnWindowFocus: true, gcTime: 0, staleTime: 0, }, @@ -41,7 +44,7 @@ export const TestQueryClientProvider = ({ const renderWithProviders = ( children: React.ReactNode, - options?: Omit & RoutConfig + options?: Omit & RoutConfig, ) => render( @@ -57,7 +60,7 @@ const renderWithProviders = ( - + , ); export * from "@testing-library/react"; diff --git a/src/lib/utils.ts b/src/lib/utils.ts index eea7cf75..64df408c 100644 --- a/src/lib/utils.ts +++ b/src/lib/utils.ts @@ -1,10 +1,5 @@ -import { Conversation } from "@/api/generated/types.gen"; -import { format, isToday, isYesterday } from "date-fns"; +import { format } from "date-fns"; -const ONE_DAY_MS = 24 * 60 * 60 * 1000; -const SEVEN_DAYS_MS = 7 * ONE_DAY_MS; -const TEEN_DAYS_MS = 14 * ONE_DAY_MS; -const THTY_DAYS_MS = 30 * ONE_DAY_MS; const FILEPATH_REGEX = /(?:---FILEPATH|Path:|\/\/\s*filepath:)\s*([^\s]+)/g; const COMPARE_CODE_REGEX = /Compare this snippet[^:]*:/g; @@ -48,52 +43,6 @@ export function parsingPromptText(message: string, timestamp: string) { } } -function getGroup(differenceInMs: number, promptDate: Date): string { - if (isToday(promptDate)) { - return "Today"; - } - if (isYesterday(promptDate)) { - return "Yesterday"; - } - if (differenceInMs <= SEVEN_DAYS_MS) { - return "Previous 7 days"; - } - if (differenceInMs <= TEEN_DAYS_MS) { - return "Previous 14 days"; - } - if (differenceInMs <= THTY_DAYS_MS) { - return "Previous 30 days"; - } - return "Beyond 30 days"; -} - -export function groupPromptsByRelativeDate(prompts: Conversation[]) { - const promptsSorted = prompts.sort( - (a, b) => - new Date(b.conversation_timestamp).getTime() - - new Date(a.conversation_timestamp).getTime() - ); - - const grouped = promptsSorted.reduce( - (groups, prompt) => { - const promptDate = new Date(prompt.conversation_timestamp); - const now = new Date(); - const differenceInMs = now.getTime() - promptDate.getTime(); - const group = getGroup(differenceInMs, promptDate); - - if (!groups[group]) { - groups[group] = []; - } - - (groups[group] ?? []).push(prompt); - return groups; - }, - {} as Record - ); - - return grouped; -} - export function sanitizeQuestionPrompt({ question, answer, @@ -121,8 +70,3 @@ export function sanitizeQuestionPrompt({ return question; } } - -export function capitalize(text: string) { - const [first, ...rest] = text; - return first ? first.toUpperCase() + rest.join("") : text; -} diff --git a/src/routes/__tests__/route-chat.test.tsx b/src/routes/__tests__/route-chat.test.tsx index 60eb927a..fbf94ebe 100644 --- a/src/routes/__tests__/route-chat.test.tsx +++ b/src/routes/__tests__/route-chat.test.tsx @@ -1,7 +1,14 @@ import { render } from "@/lib/test-utils"; -import { screen, within } from "@testing-library/react"; -import { describe, expect, it } from "vitest"; +import { screen, waitFor, within } from "@testing-library/react"; +import { expect, it, vi } from "vitest"; import { RouteChat } from "../route-chat"; +import { server } from "@/mocks/msw/node"; +import { http, HttpResponse } from "msw"; +import { mswEndpoint } from "@/test/msw-endpoint"; +import { mockConversation } from "@/mocks/msw/mockers/conversation.mock"; +import { getConversationTitle } from "@/features/dashboard-messages/lib/get-conversation-title"; +import { formatTime } from "@/lib/format-time"; +import userEvent from "@testing-library/user-event"; vi.mock("@stacklok/ui-kit", async (importOriginal) => { return { @@ -19,69 +26,167 @@ vi.mock("@/hooks/useCurrentPromptStore", () => ({ })), })); -// TODO: Replace this with MSW -vi.mock("@/hooks/use-query-get-workspace-messages", () => ({ - useQueryGetWorkspaceMessages: vi.fn(() => ({ - data: [ - { - question_answers: [ - { - question: { - message: - '\n\n```tsx src/App.tsx (23-29)\n const apiKey = "REDACTED<$lCzMsqxUWElmZqd/qX4llR3pqsrBJVxkt6SB6WXXKKhYG37ZnscxPawjfZmgPVoSpcSR975hOIE2s/6iVr2JETpVdwdaG4hAYlzJ4tteJg==>";\n\n\n```\nDo you see any security issue?.', - timestamp: "2025-01-02T13:42:48.721165Z", - message_id: "031cf7a3-1ce6-435d-bbee-0f939b30cc20", - }, - answer: { - message: - '**CodeGate Security Analysis**\n\nThe provided code snippet contains a string that is likely an encoded value, specifically a Base64 encoded string. Here is a detailed analysis:\n\n### Security Vulnerabilities and Potential Risks\n\n1. **Sensitive Data Handling**:\n - The string starts with "REDACTED", indicating that it contains sensitive information that has been masked or obfuscated. However, the actual value is still present and could be potentially revealing if decoded.\n - Using "REDACTED" is a good practice for hiding sensitive information in logs, but it\'s important to ensure that the actual sensitive data is not exposed.\n\n2. **Decoding Risks**:\n - If the string is Base64 encoded, decoding it could reveal sensitive information such as API keys, tokens, or other credentials.\n - Decoding and handling such sensitive data in client-side code (like `App.tsx`) can expose it to potential attackers if the data is transmitted or logged.\n\n3. **Potential for Injection**:\n - If the decoded string is used in any form of user input or external communication, there is a risk of injection attacks (e.g., SQL injection, cross-site scripting, etc.).\n\n### Best Practices for Secure Implementation\n\n1. **Avoid Hardcoding Sensitive Data**:\n - Never hardcode sensitive data such as API keys, tokens, or passwords directly in your source code. Use environment variables or secure vaults to manage such data.\n\n2. **Proper Encoding and Decoding**:\n - Ensure that encoding and decoding operations are performed securely and that the decoded data is handled with care to prevent unauthorized access or usage.\n\n3. **Data Validation and Sanitization**:\n - Always validate and sanitize any data that is used in your application. This includes checking for malicious content or unexpected formats.\n\n4. **Secure Logging**:\n - Avoid logging sensitive data directly. If logging is necessary, ensure that sensitive information is either sanitized or replaced with placeholders.\n\n### Recommendations for Security Improvements\n\n1. **Use Environment Variables**:\n - Replace the hardcoded string with an environment variable. For example:\n ```typescript\n const test = process.env.REACT_APP_REDACTED_TOKEN;\n ```\n - Ensure that environment variables are securely managed and not exposed in version control systems.\n\n2. **Implement Access Controls**:\n - Restrict access to sensitive data to only those parts of your application that require it. Use role-based access controls (RBAC) to manage permissions.\n\n3. **Regular Security Audits**:\n - Conduct regular security audits and code reviews to identify and address potential security vulnerabilities.\n\n4. **Stay Updated with Security Standards**:\n - Follow security best practices and guidelines such as those provided by OWASP (Open Web Application Security Project) and other relevant organizations.\n\n### References to Relevant Security Standards or Guidelines\n\n- **OWASP**: [OWASP Security Guidelines](https://owasp.org/)\n- **Secure Coding Practices**: [Secure Coding Guidelines](https://www.securecoding.org/)\n- **Environment Variables**: [12factor.net - Config](https://12factor.net/config)\n\nBy following these best practices and recommendations, you can help ensure that your application handles sensitive data securely and mitigates potential security risks.', - timestamp: "2025-01-02T13:42:49.361714Z", - message_id: "a542408b-d0bc-4f6f-8b71-64c93c1e388a", - }, - }, - ], - provider: "vllm", - type: "chat", - chat_id: "chatcmpl-7d87679de7ed41639eb91d8ebbaa6f72", - conversation_timestamp: "2025-01-02T13:42:48.721165Z", - }, - ], - })), -})); +it("renders breadcrumbs", async () => { + const conversation = mockConversation(); -describe("Chat", () => { - it("render secret issue chat", () => { - render(, { - routeConfig: { - initialEntries: ["/prompt/chatcmpl-7d87679de7ed41639eb91d8ebbaa6f72"], - }, - pathConfig: "/prompt/:id", - }); + server.use( + http.get(mswEndpoint("/api/v1/workspaces/:workspace_name/messages"), () => + HttpResponse.json([conversation]), + ), + ); - expect(screen.getByText(/REDACTED, { + routeConfig: { + initialEntries: [`/prompt/${conversation.chat_id}`], + }, + pathConfig: "/prompt/:id", + }); + await waitFor(() => { const breadcrumbs = screen.getByRole("list", { name: "Breadcrumbs" }); expect(breadcrumbs).toBeVisible(); + expect( within(breadcrumbs).getByRole("link", { name: "Dashboard" }), ).toHaveAttribute("href", "/"); - expect( - within(breadcrumbs).getByText(/do you see any security issue/i), - ).toBeVisible(); + }); +}); + +it("renders title", async () => { + const conversation = mockConversation(); + + server.use( + http.get(mswEndpoint("/api/v1/workspaces/:workspace_name/messages"), () => + HttpResponse.json([conversation]), + ), + ); + + render(, { + routeConfig: { + initialEntries: [`/prompt/${conversation.chat_id}`], + }, + pathConfig: "/prompt/:id", + }); + + await waitFor(() => { + const heading = screen.getByRole("heading", { + level: 1, + }); + + expect(heading).toHaveTextContent(getConversationTitle(conversation)); + expect(heading).toHaveTextContent( + formatTime(new Date(conversation.conversation_timestamp)), + ); + }); +}); + +it("renders chat correctly", async () => { + const conversation = mockConversation(); + + const question = conversation.question_answers[0].question.message; + const answer = conversation.question_answers[0].answer.message; + + server.use( + http.get(mswEndpoint("/api/v1/workspaces/:workspace_name/messages"), () => + HttpResponse.json([conversation]), + ), + ); + + render(, { + routeConfig: { + initialEntries: [`/prompt/${conversation.chat_id}`], + }, + pathConfig: "/prompt/:id", + }); + + await waitFor(() => { + const { getByText } = within( + screen.getByLabelText("Conversation transcript"), + ); + expect(getByText(question)).toBeVisible(); + expect(getByText(answer)).toBeVisible(); + }); +}); + +it("renders tabs", async () => { + const conversation = mockConversation(); + + server.use( + http.get(mswEndpoint("/api/v1/workspaces/:workspace_name/messages"), () => + HttpResponse.json([conversation]), + ), + ); + + render(, { + routeConfig: { + initialEntries: [`/prompt/${conversation.chat_id}`], + }, + pathConfig: "/prompt/:id", + }); + + await waitFor(() => { + expect(screen.getByRole("tab", { name: /overview/i })).toBeVisible(); + expect(screen.getByRole("tab", { name: /secrets/i })).toBeVisible(); + }); +}); + +it("can navigate using tabs", async () => { + const conversation = mockConversation(); + + server.use( + http.get(mswEndpoint("/api/v1/workspaces/:workspace_name/messages"), () => + HttpResponse.json([conversation]), + ), + ); + + render(, { + routeConfig: { + initialEntries: [ + { + pathname: `/prompt/${conversation.chat_id}`, + }, + ], + }, + pathConfig: "/prompt/:id", + }); + + await waitFor(() => { + expect(screen.getByRole("tab", { name: /overview/i })).toBeVisible(); + expect(screen.getByRole("tab", { name: /secrets/i })).toBeVisible(); + + expect(screen.getByRole("tab", { name: /overview/i })).toHaveAttribute( + "data-selected", + "true", + ); + expect(screen.getByRole("tab", { name: /secrets/i })).not.toHaveAttribute( + "data-selected", + "true", + ); + }); + + await userEvent.click(screen.getByRole("tab", { name: /secrets/i })); + + await waitFor(() => { + expect(screen.getByRole("tab", { name: /overview/i })).not.toHaveAttribute( + "data-selected", + "true", + ); + expect(screen.getByRole("tab", { name: /secrets/i })).toHaveAttribute( + "data-selected", + "true", + ); + }); + + await userEvent.click(screen.getByRole("tab", { name: /overview/i })); + + await waitFor(() => { + expect(screen.getByRole("tab", { name: /overview/i })).toHaveAttribute( + "data-selected", + "true", + ); + expect(screen.getByRole("tab", { name: /secrets/i })).not.toHaveAttribute( + "data-selected", + "true", + ); }); }); diff --git a/src/routes/__tests__/route-workspace.test.tsx b/src/routes/__tests__/route-workspace.test.tsx index 8b2ea6ea..4171a163 100644 --- a/src/routes/__tests__/route-workspace.test.tsx +++ b/src/routes/__tests__/route-workspace.test.tsx @@ -42,7 +42,7 @@ test("renders title", () => { const { getByRole } = renderComponent(); expect( - getByRole("heading", { name: "Workspace settings", level: 4 }), + getByRole("heading", { name: "Workspace settings", level: 1 }), ).toBeVisible(); }); diff --git a/src/routes/route-chat.tsx b/src/routes/route-chat.tsx index 3b7cdb06..01043d62 100644 --- a/src/routes/route-chat.tsx +++ b/src/routes/route-chat.tsx @@ -1,28 +1,53 @@ import { useParams } from "react-router-dom"; -import { useQueryGetWorkspaceMessages } from "@/hooks/use-query-get-workspace-messages"; import { parsingPromptText, sanitizeQuestionPrompt } from "@/lib/utils"; -import { ChatMessageList } from "@/components/ui/chat/chat-message-list"; -import { - ChatBubble, - ChatBubbleAvatar, - ChatBubbleMessage, -} from "@/components/ui/chat/chat-bubble"; -import { Markdown } from "@/components/Markdown"; -import { Breadcrumb, Breadcrumbs, Heading } from "@stacklok/ui-kit"; +import { Breadcrumb, Breadcrumbs, Loader } from "@stacklok/ui-kit"; import { BreadcrumbHome } from "@/components/BreadcrumbHome"; -import { ConversationSummary } from "@/features/dashboard-messages/components/conversation-summary"; import { PageContainer } from "@/components/page-container"; import { PageHeading } from "@/components/heading"; -import { isAlertSecret } from "@/lib/is-alert-secret"; -import { ConversationSecretsDetected } from "@/features/dashboard-messages/components/conversation-secrets-detected"; +import { + ConversationView, + useConversationSearchParams, +} from "@/features/dashboard-messages/hooks/use-conversation-search-params"; +import { TabsConversation } from "@/features/dashboard-messages/components/tabs-conversation"; +import { SectionConversationTranscript } from "@/features/dashboard-messages/components/section-conversation-transcript"; +import { SectionConversationSecrets } from "@/features/dashboard-messages/components/section-conversation-secrets"; +import { ErrorFallbackContent } from "@/components/Error"; +import { useConversationById } from "@/features/dashboard-messages/hooks/use-conversation-by-id"; +import { getConversationTitle } from "@/features/dashboard-messages/lib/get-conversation-title"; +import { formatTime } from "@/lib/format-time"; +import { Conversation } from "@/api/generated"; + +function ConversationContent({ + view, + conversation, +}: { + view: ConversationView; + conversation: Conversation; +}) { + switch (view) { + case ConversationView.OVERVIEW: + return ; + case ConversationView.SECRETS: + return ; + } +} + +function TitleContent({ conversation }: { conversation: Conversation }) { + return ( +
    + {getConversationTitle(conversation)} + + {formatTime(new Date(conversation.conversation_timestamp))} + +
    + ); +} export function RouteChat() { - const { id } = useParams(); - const { data: conversation } = useQueryGetWorkspaceMessages({ - select: (data) => data.find((m) => m.chat_id === id), - }); + const { id } = useParams<"id">(); + const { state } = useConversationSearchParams(); - const secrets = conversation?.alerts?.filter(isAlertSecret); + const { data: conversation, isLoading } = useConversationById(id ?? ""); const title = conversation === undefined || @@ -36,91 +61,28 @@ export function RouteChat() { conversation.conversation_timestamp, ); + if (isLoading) + return ( +
    + +
    + ); + if (!id || !conversation) return ; + return ( {title} - -
    - - Conversation summary - - - {conversation ? ( - - ) : null} -
    - - {/* - * NOTE: The secrets detection backend code appears to be returning fairly - * unstructured data with a lot of false positives. This is not actually - * referenced in the frontend yet. - */} - {secrets && secrets.length > 0 ? ( -
    - - Secrets protected ({secrets.length}) - - -

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

    - - -
    - ) : null} - - {/* {alertDetail && ( - - - - - - )} */} - -
    - - Conversation transcript - + } + /> - - {(conversation?.question_answers ?? []).map( - ({ question, answer }, index) => ( -
    - - - - - {sanitizeQuestionPrompt({ - question: question?.message ?? "", - answer: answer?.message ?? "", - })} - - - - - - - {answer?.message ?? ""} - - -
    - ), - )} -
    -
    + + +
    ); } From fc934b567357b441a1be0ae8c13d487f007f11ee Mon Sep 17 00:00:00 2001 From: alex-mcgovern Date: Tue, 11 Feb 2025 12:45:15 +0000 Subject: [PATCH 15/19] visual improvements to conversation view --- package-lock.json | 531 +++++++++++++++++- .../components/conversation-summary.tsx | 31 +- .../section-conversation-transcript.tsx | 10 +- .../components/table-messages.tsx | 12 +- src/lib/format-time.ts | 24 +- 5 files changed, 560 insertions(+), 48 deletions(-) diff --git a/package-lock.json b/package-lock.json index a070aa80..be862adc 100644 --- a/package-lock.json +++ b/package-lock.json @@ -13,15 +13,23 @@ "@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", + "@sinclair/typebox": "^0.34.16", "@stacklok/ui-kit": "^1.0.1-1", "@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", "date-fns": "^4.1.0", "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", @@ -32,8 +40,7 @@ "tailwind-variants": "^0.3.0", "tailwindcss-animate": "^1.0.7", "ts-pattern": "^5.6.2", - "zod": "^3.24.1", - "zustand": "^5.0.3" + "zod": "^3.24.1" }, "devDependencies": { "@eslint/js": "^9.15.0", @@ -1699,6 +1706,341 @@ "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", @@ -3642,6 +3984,12 @@ "dev": true, "license": "MIT" }, + "node_modules/@sinclair/typebox": { + "version": "0.34.16", + "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.34.16.tgz", + "integrity": "sha512-rIljj8VPYAfn26ANY+5pCNVBPiv6hSufuKGe46y65cJZpvx8vHvPXlU0Q/Le4OGtlNaL8Jg2FuhtvQX18lSIqA==", + "license": "MIT" + }, "node_modules/@snyk/github-codeowners": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@snyk/github-codeowners/-/github-codeowners-1.1.0.tgz", @@ -4163,6 +4511,12 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/lodash": { + "version": "4.17.15", + "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.17.15.tgz", + "integrity": "sha512-w/P33JFeySuhN6JLkysYUK2gEmy9kHHFN7E8ro0tkfmlDOgxBDzWEZ/J8cWA+fHqFevpswDTFZnDx+R9lbL6xw==", + "license": "MIT" + }, "node_modules/@types/mdast": { "version": "4.0.4", "resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-4.0.4.tgz", @@ -4188,6 +4542,12 @@ "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", @@ -5066,6 +5426,18 @@ "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", @@ -6209,6 +6581,12 @@ "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", @@ -7344,6 +7722,15 @@ "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", @@ -11332,6 +11719,53 @@ "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", @@ -11408,6 +11842,28 @@ "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", @@ -13441,6 +13897,49 @@ "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", @@ -14263,34 +14762,6 @@ "zod": "^3.18.0" } }, - "node_modules/zustand": { - "version": "5.0.3", - "resolved": "https://registry.npmjs.org/zustand/-/zustand-5.0.3.tgz", - "integrity": "sha512-14fwWQtU3pH4dE0dOpdMiWjddcH+QzKIgk1cl8epwSE7yag43k/AD/m4L6+K7DytAOr9gGBe3/EXj9g7cdostg==", - "engines": { - "node": ">=12.20.0" - }, - "peerDependencies": { - "@types/react": ">=18.0.0", - "immer": ">=9.0.6", - "react": ">=18.0.0", - "use-sync-external-store": ">=1.2.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "immer": { - "optional": true - }, - "react": { - "optional": true - }, - "use-sync-external-store": { - "optional": true - } - } - }, "node_modules/zwitch": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/zwitch/-/zwitch-2.0.4.tgz", diff --git a/src/features/dashboard-messages/components/conversation-summary.tsx b/src/features/dashboard-messages/components/conversation-summary.tsx index fc3bee09..7d7c8928 100644 --- a/src/features/dashboard-messages/components/conversation-summary.tsx +++ b/src/features/dashboard-messages/components/conversation-summary.tsx @@ -7,6 +7,13 @@ import { countConversationAlerts } from "../lib/count-conversation-alerts"; import { twMerge } from "tailwind-merge"; import { TokenUsageIcon } from "./token-usage-icon"; import { formatNumberCompact } from "@/lib/format-number"; +import { + Clock, + Hash01, + Key01, + PackageX, + Server05, +} from "@untitled-ui/icons-react"; function TokenUsage({ tokens, @@ -51,13 +58,18 @@ function AlertsSummaryCount({ count }: { count: number }) { function ConversationSummaryListItem({ title, value, + icon: Icon, }: { title: ReactNode; value: ReactNode; + icon: (props: React.SVGProps) => React.JSX.Element; }) { return (
  • - {title} + + + {title} + {value}
  • ); @@ -82,19 +94,28 @@ export function ConversationSummary({
    + -
    {conversation.token_usage_agg ? ( ) : null} } /> } /> diff --git a/src/features/dashboard-messages/components/section-conversation-transcript.tsx b/src/features/dashboard-messages/components/section-conversation-transcript.tsx index b4da83d4..8a56c41b 100644 --- a/src/features/dashboard-messages/components/section-conversation-transcript.tsx +++ b/src/features/dashboard-messages/components/section-conversation-transcript.tsx @@ -21,10 +21,7 @@ export function SectionConversationTranscript({ className="py-4 border-b-gray-200 border-b" aria-label="Conversation summary" > - + Conversation summary @@ -34,10 +31,7 @@ export function SectionConversationTranscript({ className="py-4 border-b-gray-200 border-b" aria-label="Conversation transcript" > - + Conversation transcript diff --git a/src/features/dashboard-messages/components/table-messages.tsx b/src/features/dashboard-messages/components/table-messages.tsx index 9f4b017f..6d5e63d4 100644 --- a/src/features/dashboard-messages/components/table-messages.tsx +++ b/src/features/dashboard-messages/components/table-messages.tsx @@ -188,10 +188,18 @@ export function TableMessages() { {hasNextPage || hasPreviousPage ? (
    - -
    diff --git a/src/lib/format-time.ts b/src/lib/format-time.ts index ab0f641e..c6c3882d 100644 --- a/src/lib/format-time.ts +++ b/src/lib/format-time.ts @@ -1,7 +1,23 @@ import { formatDistanceToNow } from "date-fns"; -export const formatTime = (date: Date) => { - return formatDistanceToNow(date, { - addSuffix: true, - }); +type Format = "relative" | "absolute"; + +export const formatTime = ( + date: Date, + options: { + format: Format; + } = { + format: "relative", + }, +) => { + switch (options.format) { + case "absolute": + return date.toLocaleString(); + case "relative": + return formatDistanceToNow(date, { + addSuffix: true, + }); + default: + return options.format satisfies never; + } }; From 2ea3fe5bb4b521a9575d6f4991aec1b3335c74d4 Mon Sep 17 00:00:00 2001 From: alex-mcgovern Date: Tue, 11 Feb 2025 13:02:45 +0000 Subject: [PATCH 16/19] fix: tests --- src/routes/__tests__/route-chat.test.tsx | 45 +++++++++++++++++++ src/routes/__tests__/route-workspace.test.tsx | 4 ++ 2 files changed, 49 insertions(+) diff --git a/src/routes/__tests__/route-chat.test.tsx b/src/routes/__tests__/route-chat.test.tsx index fbf94ebe..236fe4b4 100644 --- a/src/routes/__tests__/route-chat.test.tsx +++ b/src/routes/__tests__/route-chat.test.tsx @@ -9,6 +9,9 @@ import { mockConversation } from "@/mocks/msw/mockers/conversation.mock"; import { getConversationTitle } from "@/features/dashboard-messages/lib/get-conversation-title"; import { formatTime } from "@/lib/format-time"; import userEvent from "@testing-library/user-event"; +import { getProviderString } from "@/features/dashboard-messages/lib/get-provider-string"; +import { isAlertMalicious } from "@/lib/is-alert-malicious"; +import { isAlertSecret } from "@/lib/is-alert-secret"; vi.mock("@stacklok/ui-kit", async (importOriginal) => { return { @@ -80,6 +83,48 @@ 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; + + server.use( + http.get(mswEndpoint("/api/v1/workspaces/:workspace_name/messages"), () => + HttpResponse.json([conversation]), + ), + ); + + render(, { + routeConfig: { + initialEntries: [`/prompt/${conversation.chat_id}`], + }, + pathConfig: "/prompt/:id", + }); + + await waitFor(() => { + expect(screen.getByLabelText("Conversation summary")).toBeVisible(); + }); + + const { getByText } = within(screen.getByLabelText("Conversation summary")); + + expect(getByText(getProviderString(conversation.provider))).toBeVisible(); + + expect( + getByText( + formatTime(new Date(conversation.conversation_timestamp), { + format: "absolute", + }), + ), + ).toBeVisible(); + + expect(getByText(conversation.chat_id)).toBeVisible(); + + expect(getByText(`${maliciousCount} detected`)).toBeVisible(); + + expect(getByText(`${secretsCount} detected`)).toBeVisible(); +}); + it("renders chat correctly", async () => { const conversation = mockConversation(); diff --git a/src/routes/__tests__/route-workspace.test.tsx b/src/routes/__tests__/route-workspace.test.tsx index 9d277c40..5209a5f8 100644 --- a/src/routes/__tests__/route-workspace.test.tsx +++ b/src/routes/__tests__/route-workspace.test.tsx @@ -94,6 +94,10 @@ test("rename workspace", async () => { const saveBtn = within(getByTestId("workspace-name")).getByRole("button", { name: /save/i, }); + + await waitFor(() => { + expect(saveBtn).toBeEnabled(); + }); await userEvent.click(saveBtn); await waitFor(() => expect(mockNavigate).toHaveBeenCalledTimes(1)); expect(mockNavigate).toHaveBeenCalledWith("/workspace/foo_renamed"); From a38d0dc90408102c6e0b1cdbcf1495e7662e3d0c Mon Sep 17 00:00:00 2001 From: alex-mcgovern Date: Tue, 11 Feb 2025 13:18:54 +0000 Subject: [PATCH 17/19] chore: tidy ups --- .../hooks/use-query-get-workspace-alerts.ts | 14 +++++--------- .../hooks/use-conversation-by-id.tsx | 2 +- .../hooks/use-conversation-search-params.ts | 1 - 3 files changed, 6 insertions(+), 11 deletions(-) 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 index 8ebc3446..fe980f6e 100644 --- a/src/features/dashboard-alerts/hooks/use-query-get-workspace-alerts.ts +++ b/src/features/dashboard-alerts/hooks/use-query-get-workspace-alerts.ts @@ -6,7 +6,6 @@ import { v1GetWorkspaceAlertsOptions } from "@/api/generated/@tanstack/react-que import { useQueryActiveWorkspaceName } from "@/hooks/use-query-active-workspace-name"; import { getQueryCacheConfig } from "@/lib/react-query-utils"; import { useQuery } from "@tanstack/react-query"; -import { useMemo } from "react"; export function useQueryGetWorkspaceAlerts({ select, @@ -21,14 +20,11 @@ export function useQueryGetWorkspaceAlerts({ isRefetching: isWorkspaceRefetching, } = useQueryActiveWorkspaceName(); - const options: V1GetWorkspaceAlertsData = useMemo( - () => ({ - path: { - workspace_name: activeWorkspaceName ?? "default", - }, - }), - [activeWorkspaceName], - ); + const options: V1GetWorkspaceAlertsData = { + path: { + workspace_name: activeWorkspaceName ?? "default", + }, + }; const { isPending: isAlertsPending, diff --git a/src/features/dashboard-messages/hooks/use-conversation-by-id.tsx b/src/features/dashboard-messages/hooks/use-conversation-by-id.tsx index b63549f4..44bae983 100644 --- a/src/features/dashboard-messages/hooks/use-conversation-by-id.tsx +++ b/src/features/dashboard-messages/hooks/use-conversation-by-id.tsx @@ -2,6 +2,6 @@ import { useQueryGetWorkspaceMessages } from "@/hooks/use-query-get-workspace-me export function useConversationById(id: string) { return useQueryGetWorkspaceMessages({ - select: (d) => d.find((c) => c.chat_id === id) ?? undefined, + select: (d) => d.find((c) => c.chat_id === id), }); } diff --git a/src/features/dashboard-messages/hooks/use-conversation-search-params.ts b/src/features/dashboard-messages/hooks/use-conversation-search-params.ts index 7194dd9d..ec9f36cd 100644 --- a/src/features/dashboard-messages/hooks/use-conversation-search-params.ts +++ b/src/features/dashboard-messages/hooks/use-conversation-search-params.ts @@ -31,7 +31,6 @@ export const useConversationSearchParams = () => { if (view) prev.set("view", view); if (!view) prev.delete("view"); - prev.delete("page"); return prev; }); }, From d1165b30c9abdc805e362729b9137530a00b753c Mon Sep 17 00:00:00 2001 From: alex-mcgovern Date: Tue, 11 Feb 2025 13:23:27 +0000 Subject: [PATCH 18/19] fix flaky test --- .../components/conversation-summary.tsx | 35 ++++++++++++++++--- src/mocks/msw/mockers/conversation.mock.ts | 4 +-- src/routes/__tests__/route-chat.test.tsx | 6 ++-- 3 files changed, 37 insertions(+), 8 deletions(-) diff --git a/src/features/dashboard-messages/components/conversation-summary.tsx b/src/features/dashboard-messages/components/conversation-summary.tsx index 7d7c8928..7189618d 100644 --- a/src/features/dashboard-messages/components/conversation-summary.tsx +++ b/src/features/dashboard-messages/components/conversation-summary.tsx @@ -45,8 +45,19 @@ function TokenUsageRow({ ); } -function AlertsSummaryCount({ count }: { count: number }) { - const text = `${count} detected`; +function AlertsSummaryCount({ + count, + type, +}: { + count: number; + type: { + singular: "malicious package" | "secret"; + plural: "malicious packages" | "secrets"; + }; +}) { + const typeText = count === 1 ? type.singular : type.plural; + + const text = `${count} ${typeText} detected`; return ( 0 ? "text-secondary" : "text-disabled")}> @@ -132,12 +143,28 @@ export function ConversationSummary({ } + value={ + + } /> } + value={ + + } />
    diff --git a/src/mocks/msw/mockers/conversation.mock.ts b/src/mocks/msw/mockers/conversation.mock.ts index c2bd62bf..9a0f16ca 100644 --- a/src/mocks/msw/mockers/conversation.mock.ts +++ b/src/mocks/msw/mockers/conversation.mock.ts @@ -37,14 +37,14 @@ export function mockConversation({ length: typeof alertsConfig?.numAlerts === "number" ? alertsConfig?.numAlerts - : faker.number.int({ min: 0, max: 5 }), + : faker.number.int({ min: 2, max: 5 }), }).map(() => mockAlert({ type: alertsConfig?.type == null || alertsConfig.type === "any" ? faker.helpers.arrayElement(["secret", "malicious"]) : alertsConfig.type, - }) + }), ), token_usage_agg: withTokenUsage ? TOKEN_USAGE_AGG : null, type, diff --git a/src/routes/__tests__/route-chat.test.tsx b/src/routes/__tests__/route-chat.test.tsx index 236fe4b4..9af830ef 100644 --- a/src/routes/__tests__/route-chat.test.tsx +++ b/src/routes/__tests__/route-chat.test.tsx @@ -120,9 +120,11 @@ it("renders conversation summary correctly", async () => { expect(getByText(conversation.chat_id)).toBeVisible(); - expect(getByText(`${maliciousCount} detected`)).toBeVisible(); + expect( + getByText(`${maliciousCount} malicious packages detected`), + ).toBeVisible(); - expect(getByText(`${secretsCount} detected`)).toBeVisible(); + expect(getByText(`${secretsCount} secrets detected`)).toBeVisible(); }); it("renders chat correctly", async () => { From c2a966b2486a0ab5da816bd40b352d3329eabc45 Mon Sep 17 00:00:00 2001 From: alex-mcgovern Date: Tue, 11 Feb 2025 13:28:55 +0000 Subject: [PATCH 19/19] fix vitest & istanbul mismatch causing flakiness --- package-lock.json | 276 ++++++++++++---------------------------------- package.json | 8 +- 2 files changed, 77 insertions(+), 207 deletions(-) diff --git a/package-lock.json b/package-lock.json index be862adc..169b8e0c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -56,9 +56,9 @@ "@types/react-dom": "^19.0.2", "@typescript-eslint/parser": "^8.23.0", "@vitejs/plugin-react-swc": "^3.5.0", - "@vitest/coverage-istanbul": "^2.1.8", - "@vitest/expect": "^3.0.4", - "@vitest/ui": "^2.1.4", + "@vitest/coverage-istanbul": "^3.0.5", + "@vitest/expect": "^3.0.5", + "@vitest/ui": "^3.0.5", "autoprefixer": "^10.4.20", "eslint": "^9.18.0", "eslint-import-resolver-typescript": "^3.7.0", @@ -79,7 +79,7 @@ "typescript-eslint": "^8.15.0", "vite": "^6.0.1", "vite-tsconfig-paths": "^5.1.4", - "vitest": "^3.0.4", + "vitest": "^3.0.5", "vitest-fail-on-console": "^0.7.1" }, "optionalDependencies": { @@ -4979,14 +4979,14 @@ } }, "node_modules/@vitest/coverage-istanbul": { - "version": "2.1.8", - "resolved": "https://registry.npmjs.org/@vitest/coverage-istanbul/-/coverage-istanbul-2.1.8.tgz", - "integrity": "sha512-cSaCd8KcWWvgDwEJSXm0NEWZ1YTiJzjicKHy+zOEbUm0gjbbkz+qJf1p8q71uBzSlS7vdnZA8wRLeiwVE3fFTA==", + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@vitest/coverage-istanbul/-/coverage-istanbul-3.0.5.tgz", + "integrity": "sha512-yTcIwrpLHOyPP28PXXLRv1NzzKCrqDnmT7oVypTa1Q24P6OwGT4Wi6dXNEaJg33vmrPpoe81f31kwB5MtfM+ow==", "dev": true, "license": "MIT", "dependencies": { "@istanbuljs/schema": "^0.1.3", - "debug": "^4.3.7", + "debug": "^4.4.0", "istanbul-lib-coverage": "^3.2.2", "istanbul-lib-instrument": "^6.0.3", "istanbul-lib-report": "^3.0.1", @@ -4994,24 +4994,24 @@ "istanbul-reports": "^3.1.7", "magicast": "^0.3.5", "test-exclude": "^7.0.1", - "tinyrainbow": "^1.2.0" + "tinyrainbow": "^2.0.0" }, "funding": { "url": "https://opencollective.com/vitest" }, "peerDependencies": { - "vitest": "2.1.8" + "vitest": "3.0.5" } }, "node_modules/@vitest/expect": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-3.0.4.tgz", - "integrity": "sha512-Nm5kJmYw6P2BxhJPkO3eKKhGYKRsnqJqf+r0yOGRKpEP+bSCBDsjXgiu1/5QFrnPMEgzfC38ZEjvCFgaNBC0Eg==", + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-3.0.5.tgz", + "integrity": "sha512-nNIOqupgZ4v5jWuQx2DSlHLEs7Q4Oh/7AYwNyE+k0UQzG7tSmjPXShUikn1mpNGzYEN2jJbTvLejwShMitovBA==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/spy": "3.0.4", - "@vitest/utils": "3.0.4", + "@vitest/spy": "3.0.5", + "@vitest/utils": "3.0.5", "chai": "^5.1.2", "tinyrainbow": "^2.0.0" }, @@ -5019,52 +5019,14 @@ "url": "https://opencollective.com/vitest" } }, - "node_modules/@vitest/expect/node_modules/@vitest/pretty-format": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-3.0.4.tgz", - "integrity": "sha512-ts0fba+dEhK2aC9PFuZ9LTpULHpY/nd6jhAQ5IMU7Gaj7crPCTdCFfgvXxruRBLFS+MLraicCuFXxISEq8C93g==", - "dev": true, - "license": "MIT", - "dependencies": { - "tinyrainbow": "^2.0.0" - }, - "funding": { - "url": "https://opencollective.com/vitest" - } - }, - "node_modules/@vitest/expect/node_modules/@vitest/utils": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-3.0.4.tgz", - "integrity": "sha512-8BqC1ksYsHtbWH+DfpOAKrFw3jl3Uf9J7yeFh85Pz52IWuh1hBBtyfEbRNNZNjl8H8A5yMLH9/t+k7HIKzQcZQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@vitest/pretty-format": "3.0.4", - "loupe": "^3.1.2", - "tinyrainbow": "^2.0.0" - }, - "funding": { - "url": "https://opencollective.com/vitest" - } - }, - "node_modules/@vitest/expect/node_modules/tinyrainbow": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-2.0.0.tgz", - "integrity": "sha512-op4nsTR47R6p0vMUUoYl/a+ljLFVtlfaXkLQmqfLR1qHma1h/ysYk4hEXZ880bf2CYgTskvTa/e196Vd5dDQXw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=14.0.0" - } - }, "node_modules/@vitest/mocker": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-3.0.4.tgz", - "integrity": "sha512-gEef35vKafJlfQbnyOXZ0Gcr9IBUsMTyTLXsEQwuyYAerpHqvXhzdBnDFuHLpFqth3F7b6BaFr4qV/Cs1ULx5A==", + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-3.0.5.tgz", + "integrity": "sha512-CLPNBFBIE7x6aEGbIjaQAX03ZZlBMaWwAjBdMkIf/cAn6xzLTiM3zYqO/WAbieEjsAZir6tO71mzeHZoodThvw==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/spy": "3.0.4", + "@vitest/spy": "3.0.5", "estree-walker": "^3.0.3", "magic-string": "^0.30.17" }, @@ -5085,60 +5047,32 @@ } }, "node_modules/@vitest/pretty-format": { - "version": "2.1.8", - "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-2.1.8.tgz", - "integrity": "sha512-9HiSZ9zpqNLKlbIDRWOnAWqgcA7xu+8YxXSekhr0Ykab7PAYFkhkwoqVArPOtJhPmYeE2YHgKZlj3CP36z2AJQ==", + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-3.0.5.tgz", + "integrity": "sha512-CjUtdmpOcm4RVtB+up8r2vVDLR16Mgm/bYdkGFe3Yj/scRfCpbSi2W/BDSDcFK7ohw8UXvjMbOp9H4fByd/cOA==", "dev": true, "license": "MIT", "dependencies": { - "tinyrainbow": "^1.2.0" + "tinyrainbow": "^2.0.0" }, "funding": { "url": "https://opencollective.com/vitest" } }, "node_modules/@vitest/runner": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-3.0.4.tgz", - "integrity": "sha512-dKHzTQ7n9sExAcWH/0sh1elVgwc7OJ2lMOBrAm73J7AH6Pf9T12Zh3lNE1TETZaqrWFXtLlx3NVrLRb5hCK+iw==", + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-3.0.5.tgz", + "integrity": "sha512-BAiZFityFexZQi2yN4OX3OkJC6scwRo8EhRB0Z5HIGGgd2q+Nq29LgHU/+ovCtd0fOfXj5ZI6pwdlUmC5bpi8A==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/utils": "3.0.4", + "@vitest/utils": "3.0.5", "pathe": "^2.0.2" }, "funding": { "url": "https://opencollective.com/vitest" } }, - "node_modules/@vitest/runner/node_modules/@vitest/pretty-format": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-3.0.4.tgz", - "integrity": "sha512-ts0fba+dEhK2aC9PFuZ9LTpULHpY/nd6jhAQ5IMU7Gaj7crPCTdCFfgvXxruRBLFS+MLraicCuFXxISEq8C93g==", - "dev": true, - "license": "MIT", - "dependencies": { - "tinyrainbow": "^2.0.0" - }, - "funding": { - "url": "https://opencollective.com/vitest" - } - }, - "node_modules/@vitest/runner/node_modules/@vitest/utils": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-3.0.4.tgz", - "integrity": "sha512-8BqC1ksYsHtbWH+DfpOAKrFw3jl3Uf9J7yeFh85Pz52IWuh1hBBtyfEbRNNZNjl8H8A5yMLH9/t+k7HIKzQcZQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@vitest/pretty-format": "3.0.4", - "loupe": "^3.1.2", - "tinyrainbow": "^2.0.0" - }, - "funding": { - "url": "https://opencollective.com/vitest" - } - }, "node_modules/@vitest/runner/node_modules/pathe": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.2.tgz", @@ -5146,24 +5080,14 @@ "dev": true, "license": "MIT" }, - "node_modules/@vitest/runner/node_modules/tinyrainbow": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-2.0.0.tgz", - "integrity": "sha512-op4nsTR47R6p0vMUUoYl/a+ljLFVtlfaXkLQmqfLR1qHma1h/ysYk4hEXZ880bf2CYgTskvTa/e196Vd5dDQXw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=14.0.0" - } - }, "node_modules/@vitest/snapshot": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-3.0.4.tgz", - "integrity": "sha512-+p5knMLwIk7lTQkM3NonZ9zBewzVp9EVkVpvNta0/PlFWpiqLaRcF4+33L1it3uRUCh0BGLOaXPPGEjNKfWb4w==", + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-3.0.5.tgz", + "integrity": "sha512-GJPZYcd7v8QNUJ7vRvLDmRwl+a1fGg4T/54lZXe+UOGy47F9yUfE18hRCtXL5aHN/AONu29NGzIXSVFh9K0feA==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/pretty-format": "3.0.4", + "@vitest/pretty-format": "3.0.5", "magic-string": "^0.30.17", "pathe": "^2.0.2" }, @@ -5171,19 +5095,6 @@ "url": "https://opencollective.com/vitest" } }, - "node_modules/@vitest/snapshot/node_modules/@vitest/pretty-format": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-3.0.4.tgz", - "integrity": "sha512-ts0fba+dEhK2aC9PFuZ9LTpULHpY/nd6jhAQ5IMU7Gaj7crPCTdCFfgvXxruRBLFS+MLraicCuFXxISEq8C93g==", - "dev": true, - "license": "MIT", - "dependencies": { - "tinyrainbow": "^2.0.0" - }, - "funding": { - "url": "https://opencollective.com/vitest" - } - }, "node_modules/@vitest/snapshot/node_modules/pathe": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.2.tgz", @@ -5191,20 +5102,10 @@ "dev": true, "license": "MIT" }, - "node_modules/@vitest/snapshot/node_modules/tinyrainbow": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-2.0.0.tgz", - "integrity": "sha512-op4nsTR47R6p0vMUUoYl/a+ljLFVtlfaXkLQmqfLR1qHma1h/ysYk4hEXZ880bf2CYgTskvTa/e196Vd5dDQXw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=14.0.0" - } - }, "node_modules/@vitest/spy": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-3.0.4.tgz", - "integrity": "sha512-sXIMF0oauYyUy2hN49VFTYodzEAu744MmGcPR3ZBsPM20G+1/cSW/n1U+3Yu/zHxX2bIDe1oJASOkml+osTU6Q==", + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-3.0.5.tgz", + "integrity": "sha512-5fOzHj0WbUNqPK6blI/8VzZdkBlQLnT25knX0r4dbZI9qoZDf3qAdjoMmDcLG5A83W6oUUFJgUd0EYBc2P5xqg==", "dev": true, "license": "MIT", "dependencies": { @@ -5215,37 +5116,44 @@ } }, "node_modules/@vitest/ui": { - "version": "2.1.8", - "resolved": "https://registry.npmjs.org/@vitest/ui/-/ui-2.1.8.tgz", - "integrity": "sha512-5zPJ1fs0ixSVSs5+5V2XJjXLmNzjugHRyV11RqxYVR+oMcogZ9qTuSfKW+OcTV0JeFNznI83BNylzH6SSNJ1+w==", + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@vitest/ui/-/ui-3.0.5.tgz", + "integrity": "sha512-gw2noso6WI+2PeMVCZFntdATS6xl9qhQcbhkPQ9sOmx/Xn0f4Bx4KDSbD90jpJPF0l5wOzSoGCmKyVR3W612mg==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/utils": "2.1.8", + "@vitest/utils": "3.0.5", "fflate": "^0.8.2", - "flatted": "^3.3.1", - "pathe": "^1.1.2", + "flatted": "^3.3.2", + "pathe": "^2.0.2", "sirv": "^3.0.0", "tinyglobby": "^0.2.10", - "tinyrainbow": "^1.2.0" + "tinyrainbow": "^2.0.0" }, "funding": { "url": "https://opencollective.com/vitest" }, "peerDependencies": { - "vitest": "2.1.8" + "vitest": "3.0.5" } }, + "node_modules/@vitest/ui/node_modules/pathe": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.2.tgz", + "integrity": "sha512-15Ztpk+nov8DR524R4BF7uEuzESgzUEAV4Ah7CUMNGXdE5ELuvxElxGXndBl32vMSsWa1jpNf22Z+Er3sKwq+w==", + "dev": true, + "license": "MIT" + }, "node_modules/@vitest/utils": { - "version": "2.1.8", - "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-2.1.8.tgz", - "integrity": "sha512-dwSoui6djdwbfFmIgbIjX2ZhIoG7Ex/+xpxyiEgIGzjliY8xGkcpITKTlp6B4MgtGkF2ilvm97cPM96XZaAgcA==", + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-3.0.5.tgz", + "integrity": "sha512-N9AX0NUoUtVwKwy21JtwzaqR5L5R5A99GAbrHfCCXK1lp593i/3AZAXhSP43wRQuxYsflrdzEfXZFo1reR1Nkg==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/pretty-format": "2.1.8", + "@vitest/pretty-format": "3.0.5", "loupe": "^3.1.2", - "tinyrainbow": "^1.2.0" + "tinyrainbow": "^2.0.0" }, "funding": { "url": "https://opencollective.com/vitest" @@ -13342,9 +13250,9 @@ } }, "node_modules/tinyrainbow": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-1.2.0.tgz", - "integrity": "sha512-weEDEq7Z5eTHPDh4xjX789+fHfF+P8boiFB+0vbWzpbnbsEr/GRaohi/uMKxg8RZMXnl1ItAi/IUHWMsjDV7kQ==", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-2.0.0.tgz", + "integrity": "sha512-op4nsTR47R6p0vMUUoYl/a+ljLFVtlfaXkLQmqfLR1qHma1h/ysYk4hEXZ880bf2CYgTskvTa/e196Vd5dDQXw==", "dev": true, "license": "MIT", "engines": { @@ -14056,9 +13964,9 @@ } }, "node_modules/vite-node": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-3.0.4.tgz", - "integrity": "sha512-7JZKEzcYV2Nx3u6rlvN8qdo3QV7Fxyt6hx+CCKz9fbWxdX5IvUOmTWEAxMrWxaiSf7CKGLJQ5rFu8prb/jBjOA==", + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-3.0.5.tgz", + "integrity": "sha512-02JEJl7SbtwSDJdYS537nU6l+ktdvcREfLksk/NDAqtdKWGqHl+joXzEubHROmS3E6pip+Xgu2tFezMu75jH7A==", "dev": true, "license": "MIT", "dependencies": { @@ -14106,19 +14014,19 @@ } }, "node_modules/vitest": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/vitest/-/vitest-3.0.4.tgz", - "integrity": "sha512-6XG8oTKy2gnJIFTHP6LD7ExFeNLxiTkK3CfMvT7IfR8IN+BYICCf0lXUQmX7i7JoxUP8QmeP4mTnWXgflu4yjw==", + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-3.0.5.tgz", + "integrity": "sha512-4dof+HvqONw9bvsYxtkfUp2uHsTN9bV2CZIi1pWgoFpL1Lld8LA1ka9q/ONSsoScAKG7NVGf2stJTI7XRkXb2Q==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/expect": "3.0.4", - "@vitest/mocker": "3.0.4", - "@vitest/pretty-format": "^3.0.4", - "@vitest/runner": "3.0.4", - "@vitest/snapshot": "3.0.4", - "@vitest/spy": "3.0.4", - "@vitest/utils": "3.0.4", + "@vitest/expect": "3.0.5", + "@vitest/mocker": "3.0.5", + "@vitest/pretty-format": "^3.0.5", + "@vitest/runner": "3.0.5", + "@vitest/snapshot": "3.0.5", + "@vitest/spy": "3.0.5", + "@vitest/utils": "3.0.5", "chai": "^5.1.2", "debug": "^4.4.0", "expect-type": "^1.1.0", @@ -14130,7 +14038,7 @@ "tinypool": "^1.0.2", "tinyrainbow": "^2.0.0", "vite": "^5.0.0 || ^6.0.0", - "vite-node": "3.0.4", + "vite-node": "3.0.5", "why-is-node-running": "^2.3.0" }, "bin": { @@ -14146,8 +14054,8 @@ "@edge-runtime/vm": "*", "@types/debug": "^4.1.12", "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", - "@vitest/browser": "3.0.4", - "@vitest/ui": "3.0.4", + "@vitest/browser": "3.0.5", + "@vitest/ui": "3.0.5", "happy-dom": "*", "jsdom": "*" }, @@ -14202,34 +14110,6 @@ "url": "https://github.com/chalk/chalk?sponsor=1" } }, - "node_modules/vitest/node_modules/@vitest/pretty-format": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-3.0.4.tgz", - "integrity": "sha512-ts0fba+dEhK2aC9PFuZ9LTpULHpY/nd6jhAQ5IMU7Gaj7crPCTdCFfgvXxruRBLFS+MLraicCuFXxISEq8C93g==", - "dev": true, - "license": "MIT", - "dependencies": { - "tinyrainbow": "^2.0.0" - }, - "funding": { - "url": "https://opencollective.com/vitest" - } - }, - "node_modules/vitest/node_modules/@vitest/utils": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-3.0.4.tgz", - "integrity": "sha512-8BqC1ksYsHtbWH+DfpOAKrFw3jl3Uf9J7yeFh85Pz52IWuh1hBBtyfEbRNNZNjl8H8A5yMLH9/t+k7HIKzQcZQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@vitest/pretty-format": "3.0.4", - "loupe": "^3.1.2", - "tinyrainbow": "^2.0.0" - }, - "funding": { - "url": "https://opencollective.com/vitest" - } - }, "node_modules/vitest/node_modules/pathe": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.2.tgz", @@ -14237,16 +14117,6 @@ "dev": true, "license": "MIT" }, - "node_modules/vitest/node_modules/tinyrainbow": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-2.0.0.tgz", - "integrity": "sha512-op4nsTR47R6p0vMUUoYl/a+ljLFVtlfaXkLQmqfLR1qHma1h/ysYk4hEXZ880bf2CYgTskvTa/e196Vd5dDQXw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=14.0.0" - } - }, "node_modules/w3c-xmlserializer": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-5.0.0.tgz", diff --git a/package.json b/package.json index 3aa00936..955084d8 100644 --- a/package.json +++ b/package.json @@ -68,9 +68,9 @@ "@types/react-dom": "^19.0.2", "@typescript-eslint/parser": "^8.23.0", "@vitejs/plugin-react-swc": "^3.5.0", - "@vitest/coverage-istanbul": "^2.1.8", - "@vitest/expect": "^3.0.4", - "@vitest/ui": "^2.1.4", + "@vitest/coverage-istanbul": "^3.0.5", + "@vitest/expect": "^3.0.5", + "@vitest/ui": "^3.0.5", "autoprefixer": "^10.4.20", "eslint": "^9.18.0", "eslint-import-resolver-typescript": "^3.7.0", @@ -91,7 +91,7 @@ "typescript-eslint": "^8.15.0", "vite": "^6.0.1", "vite-tsconfig-paths": "^5.1.4", - "vitest": "^3.0.4", + "vitest": "^3.0.5", "vitest-fail-on-console": "^0.7.1" }, "overrides": {