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");