diff --git a/package-lock.json b/package-lock.json index 3f8f0be3..96ad32bf 100644 --- a/package-lock.json +++ b/package-lock.json @@ -32,6 +32,7 @@ "tailwind-merge": "^2.5.5", "tailwind-variants": "^0.3.0", "tailwindcss-animate": "^1.0.7", + "ts-pattern": "^5.6.2", "zod": "^3.24.1", "zustand": "^5.0.3" }, @@ -11533,6 +11534,12 @@ "integrity": "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==", "license": "Apache-2.0" }, + "node_modules/ts-pattern": { + "version": "5.6.2", + "resolved": "https://registry.npmjs.org/ts-pattern/-/ts-pattern-5.6.2.tgz", + "integrity": "sha512-d4IxJUXROL5NCa3amvMg6VQW2HVtZYmUTPfvVtO7zJWGYLJ+mry9v2OmYm+z67aniQoQ8/yFNadiEwtNS9qQiw==", + "license": "MIT" + }, "node_modules/tsconfck": { "version": "3.1.4", "resolved": "https://registry.npmjs.org/tsconfck/-/tsconfck-3.1.4.tgz", diff --git a/package.json b/package.json index 9fa218d0..d735c323 100644 --- a/package.json +++ b/package.json @@ -44,6 +44,7 @@ "tailwind-merge": "^2.5.5", "tailwind-variants": "^0.3.0", "tailwindcss-animate": "^1.0.7", + "ts-pattern": "^5.6.2", "zod": "^3.24.1", "zustand": "^5.0.3" }, diff --git a/src/components/empty-state.tsx b/src/components/empty-state.tsx new file mode 100644 index 00000000..3fb1d3f8 --- /dev/null +++ b/src/components/empty-state.tsx @@ -0,0 +1,42 @@ +import { Heading } from "@stacklok/ui-kit"; +import { JSX, ReactNode, SVGProps } from "react"; +import { tv } from "tailwind-variants"; + +const actionsStyle = tv({ + base: "mx-auto mt-8", + variants: { + actions: { + 1: "", + 2: "grid grid-cols-2 gap-2", + }, + }, +}); + +export function Actions({ actions }: { actions: [ReactNode, ReactNode?] }) { + return ( +
{actions}
+ ); +} + +export function EmptyState({ + actions, + body, + illustration: Illustration, + title, +}: { + illustration: (props: SVGProps) => JSX.Element; + title: string; + body: string; + actions: [ReactNode, ReactNode?] | null; +}) { + return ( +
+ + + {title} + +

{body}

+ {actions ? : null} +
+ ); +} 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 new file mode 100644 index 00000000..c6ce31f4 --- /dev/null +++ b/src/features/alerts/components/__tests__/table-alerts.empty-state.test.tsx @@ -0,0 +1,331 @@ +import { test } from "vitest"; +import { render, waitFor } from "@/lib/test-utils"; +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"; + +enum IllustrationTestId { + ALERT = "illustration-alert", + DONE = "illustration-done", + DRAG_AND_DROP = "illustration-drag-and-drop", + LOADER = "illustration-loader", + NO_SEARCH_RESULTS = "illustration-no-search-results", +} + +type TestCaseAction = + | { + role: "button"; + name: string; + href?: never; + } + | { + role: "link"; + name: string; + href: string; + }; + +type TestCase = { + testDescription: string; + handlers: HttpHandler[]; + searchParams: { + view: AlertsFilterView; + search: string | null; + }; + expected: { + title: string; + body: string; + illustrationTestId: IllustrationTestId; + actions: TestCaseAction[] | null; + }; +}; + +vi.mock("react-router-dom", async () => { + const original = + await vi.importActual( + "react-router-dom", + ); + return { + ...original, + useSearchParams: vi.fn(() => [new URLSearchParams({}), () => {}]), + }; +}); + +vi.mock("@stacklok/ui-kit", async () => { + const original = + await vi.importActual( + "@stacklok/ui-kit", + ); + return { + ...original, + IllustrationDone: () =>
, + IllustrationDragAndDrop: () => ( +
+ ), + IllustrationAlert: () =>
, + IllustrationNoSearchResults: () => ( +
+ ), + Loader: () =>
, + }; +}); + +const TEST_CASES: TestCase[] = [ + { + testDescription: "Loading state", + handlers: [ + http.get("*/api/v1/workspaces", () => { + delay("infinite"); + }), + ], + searchParams: { + search: null, + view: AlertsFilterView.ALL, + }, + expected: { + title: emptyStateStrings.title.loading, + body: emptyStateStrings.body.loading, + illustrationTestId: IllustrationTestId.LOADER, + actions: null, + }, + }, + { + testDescription: "Only 1 workspace, no alerts", + handlers: [ + http.get("*/api/v1/workspaces", () => { + return HttpResponse.json({ + workspaces: [ + { + name: "default", + is_active: true, + }, + ], + }); + }), + http.get("*/api/v1/workspaces/archive", () => { + return HttpResponse.json({ + workspaces: [], + }); + }), + http.get("*/api/v1/workspaces/:name/alerts", () => { + return HttpResponse.json([]); + }), + ], + searchParams: { + search: null, + view: AlertsFilterView.ALL, + }, + expected: { + body: emptyStateStrings.body.getStartedDesc, + title: emptyStateStrings.title.getStarted, + illustrationTestId: IllustrationTestId.DRAG_AND_DROP, + actions: [ + { + role: "link", + name: "CodeGate docs", + href: hrefs.external.docs.home, + }, + ], + }, + }, + { + testDescription: "No search results", + handlers: [ + http.get("*/api/v1/workspaces", () => { + return HttpResponse.json({ + workspaces: [ + { + name: "default", + is_active: true, + }, + ], + }); + }), + http.get("*/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" }), + ), + ); + }), + ], + searchParams: { search: "foo-bar", view: AlertsFilterView.ALL }, + expected: { + title: emptyStateStrings.title.noSearchResultsFor("foo-bar"), + body: emptyStateStrings.body.tryChangingSearch, + illustrationTestId: IllustrationTestId.NO_SEARCH_RESULTS, + actions: [ + { + role: "button", + name: "Clear search", + }, + ], + }, + }, + { + testDescription: "No alerts, multiple workspaces", + handlers: [ + http.get("*/api/v1/workspaces", () => { + return HttpResponse.json({ + workspaces: [ + { + name: "default", + is_active: true, + }, + { + name: "foo-bar", + is_active: false, + }, + ], + }); + }), + http.get("*/api/v1/workspaces/archive", () => { + return HttpResponse.json({ + workspaces: [], + }); + }), + http.get("*/api/v1/workspaces/:name/alerts", () => { + return HttpResponse.json([]); + }), + ], + searchParams: { + search: null, + view: AlertsFilterView.ALL, + }, + expected: { + title: emptyStateStrings.title.noAlertsFoundWorkspace, + body: emptyStateStrings.body.alertsWillShowUpWhenWorkspace, + illustrationTestId: IllustrationTestId.DONE, + actions: [ + { + role: "link", + name: "Learn about Workspaces", + href: hrefs.external.docs.workspaces, + }, + ], + }, + }, + { + testDescription: 'Has alerts, view is "malicious"', + handlers: [ + http.get("*/api/v1/workspaces", () => { + return HttpResponse.json({ + workspaces: [ + { + name: "default", + is_active: true, + }, + { + name: "foo-bar", + is_active: false, + }, + ], + }); + }), + http.get("*/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" }), + ), + ); + }), + ], + searchParams: { + view: AlertsFilterView.MALICIOUS, + search: null, + }, + expected: { + title: emptyStateStrings.title.noMaliciousPackagesDetected, + body: emptyStateStrings.body.maliciousDesc, + illustrationTestId: IllustrationTestId.DONE, + actions: null, + }, + }, + { + testDescription: 'Has alerts, view is "secret"', + handlers: [ + http.get("*/api/v1/workspaces", () => { + return HttpResponse.json({ + workspaces: [ + { + name: "default", + is_active: true, + }, + { + name: "foo-bar", + is_active: false, + }, + ], + }); + }), + http.get("*/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" }), + ), + ); + }), + ], + searchParams: { + view: AlertsFilterView.SECRETS, + search: null, + }, + expected: { + title: emptyStateStrings.title.noLeakedSecretsDetected, + body: emptyStateStrings.body.secretsDesc, + illustrationTestId: IllustrationTestId.DONE, + actions: null, + }, + }, +]; + +test.each(TEST_CASES)("$testDescription", async (testCase) => { + server.use(...testCase.handlers); + + vi.mocked(useSearchParams).mockReturnValue([ + new URLSearchParams({ + search: testCase.searchParams.search ?? "", + view: testCase.searchParams.view, + }), + () => {}, + ]); + + const { getByText, getByRole, getByTestId } = render(); + + await waitFor(() => { + expect( + getByRole("heading", { level: 4, name: testCase.expected.title }), + ).toBeVisible(); + expect(getByText(testCase.expected.body)).toBeVisible(); + expect(getByTestId(testCase.expected.illustrationTestId)).toBeVisible(); + + if (testCase.expected.actions) { + for (const action of testCase.expected.actions) { + const actionButton = getByRole(action.role, { name: action.name }); + expect(actionButton).toBeVisible(); + if (action.href) { + expect(actionButton).toHaveAttribute("href", action.href); + } + } + } + }); +}); diff --git a/src/features/alerts/components/__tests__/table-alerts.test.tsx b/src/features/alerts/components/__tests__/table-alerts.test.tsx index ccfc0221..905f31af 100644 --- a/src/features/alerts/components/__tests__/table-alerts.test.tsx +++ b/src/features/alerts/components/__tests__/table-alerts.test.tsx @@ -1,6 +1,6 @@ import {} from "vitest"; import { TableAlerts } from "../table-alerts"; -import { render, screen, waitFor, within } from "@/lib/test-utils"; +import { render, waitFor } from "@/lib/test-utils"; import { server } from "@/mocks/msw/node"; import { http, HttpResponse } from "msw"; import { makeMockAlert } from "../../mocks/alert.mock"; @@ -38,13 +38,7 @@ test("renders token usage cell correctly", async () => { const { getByRole, getByTestId, queryByText } = render(); await waitFor(() => { - expect( - within(screen.getByTestId("alerts-table")).getAllByRole("row"), - ).toHaveLength(2); - }); - - await waitFor(() => { - expect(queryByText(/loading alerts/i)).not.toBeInTheDocument(); + expect(queryByText(/loading.../i)).not.toBeInTheDocument(); }); expect(getByTestId("icon-arrow-up")).toBeVisible(); @@ -69,118 +63,8 @@ test("renders N/A when token usage is missing", async () => { const { getByText, queryByText } = render(); await waitFor(() => { - expect( - within(screen.getByTestId("alerts-table")).getAllByRole("row"), - ).toHaveLength(2); - }); - - await waitFor(() => { - expect(queryByText(/loading alerts/i)).not.toBeInTheDocument(); + expect(queryByText(/loading.../i)).not.toBeInTheDocument(); }); expect(getByText("N/A")).toBeVisible(); }); - -test("renders empty state when the API returns no alerts - user has not created multipe workspaces", async () => { - server.use( - http.get("*/workspaces/:name/alerts", () => { - return HttpResponse.json([]); - }), - http.get("*/workspaces", () => { - return HttpResponse.json({ - workspaces: [ - { - name: "my-awesome-workspace", - is_active: true, - last_updated: new Date(Date.now()).toISOString(), - }, - ], - }); - }), - ); - - const { getByText, queryByText, getByRole } = render(); - - await waitFor(() => { - expect(queryByText(/loading alerts/i)).not.toBeInTheDocument(); - }); - - expect(getByText("Connect CodeGate to your IDE")).toBeVisible(); - expect(getByText(/learn how to get set up using/i)).toBeVisible(); - - expect(getByRole("link", { name: /continue/i })).toHaveAttribute( - "href", - "https://docs.codegate.ai/quickstart-continue", - ); - expect(getByRole("link", { name: /continue/i })).toHaveAttribute( - "target", - "_blank", - ); - expect(getByRole("link", { name: /copilot/i })).toHaveAttribute( - "href", - "https://docs.codegate.ai/quickstart", - ); - expect(getByRole("link", { name: /copilot/i })).toHaveAttribute( - "target", - "_blank", - ); - expect(getByRole("link", { name: /aider/i })).toHaveAttribute( - "href", - "https://docs.codegate.ai/how-to/use-with-aider", - ); - expect(getByRole("link", { name: /aider/i })).toHaveAttribute( - "target", - "_blank", - ); - - expect( - getByRole("link", { name: /codegate documentation/i }), - ).toHaveAttribute("href", "https://docs.codegate.ai/"); - expect( - getByRole("link", { name: /codegate documentation/i }), - ).toHaveAttribute("target", "_blank"); -}); - -test("does not render table empty state when the API responds with alerts", async () => { - server.use( - http.get("*/workspaces/:name/alerts", () => { - return HttpResponse.json([ - makeMockAlert({ token_usage: false, type: "malicious" }), - ]); - }), - ); - - const { queryByText } = render(); - - await waitFor(() => { - expect(queryByText("Connect CodeGate to your IDE")).not.toBeInTheDocument(); - }); -}); - -test("renders empty state when the API returns no alerts - user has multiple workspaces", async () => { - server.use( - http.get("*/workspaces/:name/alerts", () => { - return HttpResponse.json([]); - }), - ); - - const { getByText, queryByText, getByRole } = render(); - - await waitFor(() => { - expect(queryByText(/loading alerts/i)).not.toBeInTheDocument(); - }); - - expect(getByText(/no alerts found/i)).toBeVisible(); - expect( - getByText( - /alerts will show up here when you use this workspace in your IDE/i, - ), - ).toBeVisible(); - - expect( - getByRole("link", { name: /learn about workspaces/i }), - ).toHaveAttribute("href", "https://docs.codegate.ai/features/workspaces"); - expect( - getByRole("link", { name: /learn about workspaces/i }), - ).toHaveAttribute("target", "_blank"); -}); diff --git a/src/features/alerts/components/table-alerts-empty-state.tsx b/src/features/alerts/components/table-alerts-empty-state.tsx new file mode 100644 index 00000000..b8bf7e41 --- /dev/null +++ b/src/features/alerts/components/table-alerts-empty-state.tsx @@ -0,0 +1,234 @@ +import { + Button, + IllustrationAlert, + IllustrationDone, + IllustrationDragAndDrop, + IllustrationNoSearchResults, + LinkButton, + Loader, +} from "@stacklok/ui-kit"; +import { ReactNode } from "react"; + +import { emptyStateStrings } from "../constants/strings"; +import { EmptyState } from "@/components/empty-state"; +import { hrefs } from "@/lib/hrefs"; +import { LinkExternal02 } from "@untitled-ui/icons-react"; +import { useListAllWorkspaces } from "@/features/workspace/hooks/use-query-list-all-workspaces"; +import { + AlertsFilterView, + useAlertsFilterSearchParams, +} from "../hooks/use-alerts-filter-search-params"; +import { useQueryGetWorkspaceAlerts } from "../hooks/use-query-get-workspace-alerts"; +import { match, P } from "ts-pattern"; + +function EmptyStateLoading() { + return ( + + ); +} + +function EmptyStateGetStarted() { + return ( + + CodeGate docs + + , + ]} + /> + ); +} + +function EmptyStateSearch({ + search, + setSearch, +}: { + search: string; + setSearch: (v: string | null) => void; +}) { + return ( + setSearch(null)}> + Clear search + , + ]} + /> + ); +} + +function EmptyStateNoAlertsInWorkspace() { + return ( + + Learn about Workspaces + + , + ]} + /> + ); +} + +function EmptyStateMalicious() { + return ( + + ); +} + +function EmptyStateSecrets() { + return ( + + ); +} + +function EmptyStateError() { + return ( + + Discord + + , + + Github issues + + , + ]} + /> + ); +} + +type MatchInput = { + isLoading: boolean; + hasWorkspaceAlerts: boolean; + hasMultipleWorkspaces: boolean; + search: string | null; + view: AlertsFilterView | null; +}; + +export function TableAlertsEmptyState() { + const { state, setSearch } = useAlertsFilterSearchParams(); + + const { data: alerts = [], isLoading: isAlertsLoading } = + useQueryGetWorkspaceAlerts(); + + const { data: workspaces = [], isLoading: isWorkspacesLoading } = + useListAllWorkspaces(); + + const isLoading = isAlertsLoading || isWorkspacesLoading; + + return match({ + isLoading, + hasWorkspaceAlerts: alerts.length > 0, + hasMultipleWorkspaces: + workspaces.filter((w) => w.name !== "default").length > 0, + search: state.search || null, + view: state.view, + }) + .with( + { + isLoading: true, + hasWorkspaceAlerts: P._, + hasMultipleWorkspaces: P._, + search: P._, + view: P._, + }, + () => , + ) + .with( + { + hasWorkspaceAlerts: false, + hasMultipleWorkspaces: false, + search: P._, + view: P._, + }, + () => , + ) + .with( + { + hasWorkspaceAlerts: true, + hasMultipleWorkspaces: P.any, + search: P.string.select(), + view: P._, + }, + (search) => , + ) + .with( + { + hasWorkspaceAlerts: false, + hasMultipleWorkspaces: true, + search: P._, + view: P.any, + }, + () => , + ) + .with( + { + hasWorkspaceAlerts: true, + hasMultipleWorkspaces: true, + search: P._, + view: AlertsFilterView.MALICIOUS, + }, + () => , + ) + .with( + { + hasWorkspaceAlerts: true, + hasMultipleWorkspaces: P.any, + view: AlertsFilterView.SECRETS, + }, + () => , + ) + .otherwise(() => ); +} diff --git a/src/features/alerts/components/table-alerts.tsx b/src/features/alerts/components/table-alerts.tsx index 96c3a335..df58e066 100644 --- a/src/features/alerts/components/table-alerts.tsx +++ b/src/features/alerts/components/table-alerts.tsx @@ -8,10 +8,6 @@ import { TableHeader, Button, ResizableTableContainer, - Link, - LinkButton, - IllustrationDragAndDrop, - IllustrationPackage, } from "@stacklok/ui-kit"; import { AlertConversation, QuestionType } from "@/api/generated"; import { @@ -19,14 +15,15 @@ import { parsingPromptText, getIssueDetectedType, } from "@/lib/utils"; -import { useNavigate } from "react-router-dom"; 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 { useListWorkspaces } from "@/features/workspace/hooks/use-list-workspaces"; -import { Key01, LinkExternal02, PackageX } from "@untitled-ui/icons-react"; +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; @@ -77,92 +74,79 @@ function IssueDetectedCellContent({ alert }: { alert: AlertConversation }) { } } -function EmptyState({ - hasMultipleWorkspaces, +type ColumnId = "time" | "type" | "event" | "issue_detected" | "token_usage"; + +type Column = { id: ColumnId } & Omit, "id">; + +const COLUMNS: Column[] = [ + { + id: "time", + isRowHeader: true, + children: "Time", + width: 200, + }, + { + id: "type", + children: "Type", + width: 150, + }, + { + id: "event", + children: "Event", + }, + { + id: "issue_detected", + children: "Issue detected", + width: 325, + }, + { + id: "token_usage", + children: "Token usage", + width: 200, + }, +]; + +function CellRenderer({ + column, + row, }: { - hasMultipleWorkspaces: boolean; + column: Column; + row: AlertConversation; }) { - if (hasMultipleWorkspaces) { - return ( -
- -

No alerts found

-

- Alerts will show up here when you use this workspace in your IDE -

- - Learn about Workspaces - - -
- ); - } + switch (column.id) { + case "time": + return ( + + {formatDistanceToNow(new Date(row.timestamp), { + addSuffix: true, + })} + + ); + case "type": + return ; + case "event": + return getTitle(row); + case "issue_detected": + return ( +
+ +
+ ); + case "token_usage": + return ; - return ( -
- -

- Connect CodeGate to your IDE -

-

- Learn how to get set up using{" "} - - Continue - - ,{" "} - - Copilot - - , or{" "} - - Aider - - . -

- - CodeGate Documentation - - -
- ); + default: + return column.id satisfies never; + } } export function TableAlerts() { - const navigate = useNavigate(); const { state, prevPage, nextPage } = useAlertsFilterSearchParams(); - const { data: filteredAlerts = [], isLoading: isLoadingAlerts } = - useQueryGetWorkspaceAlertTable(); - - const { - data: { workspaces } = { workspaces: [] }, - isLoading: isLoadingWorkspaces, - } = useListWorkspaces(); - - const isLoading = isLoadingAlerts || isLoadingWorkspaces; + const { data = [] } = useQueryGetWorkspaceAlertTable(); const { dataView, hasNextPage, hasPreviousPage } = useClientSidePagination( - filteredAlerts, + data, state.page, 15, ); @@ -171,71 +155,46 @@ export function TableAlerts() { <> - - - - Time - - Type - Event - Issue Detected - Token usage - + + {(column) => } - isLoading ? ( -
Loading alerts
- ) : ( - 1} /> - ) - } + renderEmptyState={() => } + items={dataView} > - {dataView.map((alert) => { - return ( - - navigate(`/prompt/${alert.conversation.chat_id}`) - } - > - - {formatDistanceToNow(new Date(alert.timestamp), { - addSuffix: true, - })} - - - - - {getTitle(alert)} - -
- -
-
- - + {(row) => ( + + {(column) => ( + + - - ); - })} + )} +
+ )}
-
-
- - + {hasNextPage || hasPreviousPage ? ( +
+
+ + +
-
+ ) : null} ); } diff --git a/src/features/alerts/constants/strings.ts b/src/features/alerts/constants/strings.ts new file mode 100644 index 00000000..119e1ae5 --- /dev/null +++ b/src/features/alerts/constants/strings.ts @@ -0,0 +1,28 @@ +export const emptyStateStrings = { + title: { + loading: "Loading...", + getStarted: "Get started with CodeGate", + noAlertsFound: "No alerts found", + noAlertsFoundWorkspace: "This workspace hasn't triggered any alerts", + anErrorOccurred: "An error occurred", + noLeakedSecretsDetected: "No leaked secrets detected", + noMaliciousPackagesDetected: "No malicious packages detected", + noSearchResultsFor: (x: string | undefined): string => + !x ? "No search results" : `No search results for "${x}"`, + }, + body: { + loading: "Checking for the latest alerts.", + errorDesc: + "Please try refreshing the page. If this issue persists, please let us know on Discord, or open a a new Github Issue", + getStartedDesc: "Learn how to get started with CodeGate in your IDE.", + tryChangingSearch: "Try changing your search query or clearing the search.", + alertsWillShowUpWhenWorkspace: + "Alerts will show up here when they are detected for this workspace.", + alertsDesc: + "Alerts are issues that CodeGate has detected and mitigated in your interactions with the LLM.", + secretsDesc: + "CodeGate helps you protect sensitive information from being accidentally exposed to AI models and third-party AI provider systems by redacting detected secrets from your prompts using encryption.", + maliciousDesc: + "CodeGate's dependency risk insight helps protect your codebase from malicious or vulnerable dependencies. It identifies potentially risky packages and suggests fixed versions or alternative packages to consider.", + }, +} as const; diff --git a/src/features/alerts/hooks/use-query-get-workspace-alerts.ts b/src/features/alerts/hooks/use-query-get-workspace-alerts.ts index 608f41c1..1a2e9f8d 100644 --- a/src/features/alerts/hooks/use-query-get-workspace-alerts.ts +++ b/src/features/alerts/hooks/use-query-get-workspace-alerts.ts @@ -11,7 +11,13 @@ export function useQueryGetWorkspaceAlerts({ }: { select?: (data: V1GetWorkspaceAlertsResponse) => T; } = {}) { - const { data: activeWorkspaceName } = useActiveWorkspaceName(); + const { + data: activeWorkspaceName, + isPending: isWorkspacePending, + isFetching: isWorkspaceFetching, + isLoading: isWorkspaceLoading, + isRefetching: isWorkspaceRefetching, + } = useActiveWorkspaceName(); const options: V1GetWorkspaceAlertsData = { path: { @@ -19,11 +25,25 @@ export function useQueryGetWorkspaceAlerts({ }, }; - return useQuery({ + const { + isPending: isAlertsPending, + isFetching: isAlertsFetching, + isLoading: isAlertsLoading, + isRefetching: isAlertsRefetching, + ...rest + } = useQuery({ ...v1GetWorkspaceAlertsOptions(options), refetchOnMount: true, refetchOnReconnect: true, refetchOnWindowFocus: true, select, }); + + return { + ...rest, + isPending: isAlertsPending || isWorkspacePending, + isFetching: isAlertsFetching || isWorkspaceFetching, + isLoading: isAlertsLoading || isWorkspaceLoading, + isRefetching: isAlertsRefetching || isWorkspaceRefetching, + }; } diff --git a/src/features/workspace/hooks/use-query-list-all-workspaces.ts b/src/features/workspace/hooks/use-query-list-all-workspaces.ts index 37a50551..875a0e6f 100644 --- a/src/features/workspace/hooks/use-query-list-all-workspaces.ts +++ b/src/features/workspace/hooks/use-query-list-all-workspaces.ts @@ -48,6 +48,7 @@ const combine = (results: UseQueryDataReturn) => { data: [...active, ...archived], isPending: results.some((r) => r.isPending), isFetching: results.some((r) => r.isFetching), + isLoading: results.some((r) => r.isLoading), isRefetching: results.some((r) => r.isRefetching), }; }; diff --git a/src/lib/hrefs.ts b/src/lib/hrefs.ts index cb3b9407..53b2d7e6 100644 --- a/src/lib/hrefs.ts +++ b/src/lib/hrefs.ts @@ -1,4 +1,13 @@ export const hrefs = { + external: { + discord: "https://discord.gg/stacklok", + github: { newIssue: "https://github.com/stacklok/codegate/issues/new" }, + docs: { + home: "https://docs.codegate.ai/", + workspaces: "https://docs.codegate.ai/features/workspaces", + }, + }, + prompt: (id: string) => `/prompt/${id}`, workspaces: { all: "/workspaces", create: "/workspace/create", diff --git a/src/routes/__tests__/route-dashboard.test.tsx b/src/routes/__tests__/route-dashboard.test.tsx index f8c2a92b..0bb5097c 100644 --- a/src/routes/__tests__/route-dashboard.test.tsx +++ b/src/routes/__tests__/route-dashboard.test.tsx @@ -189,13 +189,7 @@ describe("Dashboard", () => { render(); await waitFor(() => { - expect( - within(screen.getByTestId("alerts-table")).getAllByRole("row").length, - ).toBeGreaterThan(1); - }); - - await waitFor(() => { - expect(screen.queryByText(/loading alerts/i)).not.toBeInTheDocument(); + expect(screen.queryByText(/loading.../i)).not.toBeInTheDocument(); }); expect( @@ -219,14 +213,9 @@ describe("Dashboard", () => { render(); await waitFor(() => { - expect( - within(screen.getByTestId("alerts-table")).getAllByRole("row").length, - ).toBeGreaterThan(1); + expect(screen.queryByText(/loading.../i)).not.toBeInTheDocument(); }); - await waitFor(() => { - expect(screen.queryByText(/loading alerts/i)).not.toBeInTheDocument(); - }); expect(screen.getByTestId(/tab-all-count/i)).toHaveTextContent("2"); expect( @@ -269,14 +258,9 @@ describe("Dashboard", () => { render(); await waitFor(() => { - expect( - within(screen.getByTestId("alerts-table")).getAllByRole("row").length, - ).toBeGreaterThan(1); + expect(screen.queryByText(/loading.../i)).not.toBeInTheDocument(); }); - await waitFor(() => { - expect(screen.queryByText(/loading alerts/i)).not.toBeInTheDocument(); - }); expect(screen.getByTestId(/tab-all-count/i)).toHaveTextContent("2"); expect( screen.getAllByRole("gridcell", { @@ -318,13 +302,7 @@ describe("Dashboard", () => { render(); await waitFor(() => { - expect( - within(screen.getByTestId("alerts-table")).getAllByRole("row").length, - ).toBeGreaterThan(1); - }); - - await waitFor(() => { - expect(screen.queryByText(/loading alerts/i)).not.toBeInTheDocument(); + expect(screen.queryByText(/loading.../i)).not.toBeInTheDocument(); }); expect(screen.getByTestId(/tab-all-count/i)).toHaveTextContent("2");