Skip to content

feat: implement empty state for tables #232

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 15 commits into from
Jan 30, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
116 changes: 114 additions & 2 deletions src/features/alerts/components/__tests__/table-alerts.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -36,14 +36,18 @@ test("renders token usage cell correctly", async () => {
}),
);

const { getByRole, getByTestId } = render(<TableAlerts />);
const { getByRole, getByTestId, queryByText } = render(<TableAlerts />);

await waitFor(() => {
expect(
within(screen.getByTestId("alerts-table")).getAllByRole("row"),
).toHaveLength(2);
});

await waitFor(() => {
expect(queryByText(/loading alerts/i)).not.toBeInTheDocument();
});

expect(getByTestId("icon-arrow-up")).toBeVisible();
expect(getByTestId("icon-arrow-down")).toBeVisible();

Expand All @@ -63,13 +67,121 @@ test("renders N/A when token usage is missing", async () => {
}),
);

const { getByText } = render(<TableAlerts />);
const { getByText, queryByText } = render(<TableAlerts />);

await waitFor(() => {
expect(
within(screen.getByTestId("alerts-table")).getAllByRole("row"),
).toHaveLength(2);
});

await waitFor(() => {
expect(queryByText(/loading alerts/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(<TableAlerts />);

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(<TableAlerts />);

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(<TableAlerts />);

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");
});
98 changes: 94 additions & 4 deletions src/features/alerts/components/table-alerts.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,10 @@ import {
Badge,
Button,
ResizableTableContainer,
Link,
LinkButton,
IllustrationDragAndDrop,
IllustrationPackage,
} from "@stacklok/ui-kit";
import { AlertConversation, QuestionType } from "@/api/generated";
import {
Expand All @@ -20,10 +24,11 @@ import { useAlertSearch } from "@/hooks/useAlertSearch";
import { useNavigate } from "react-router-dom";
import { useClientSidePagination } from "@/hooks/useClientSidePagination";
import { TableAlertTokenUsage } from "./table-alert-token-usage";
import { Key01, PackageX } from "@untitled-ui/icons-react";
import { Key01, LinkExternal02, PackageX } from "@untitled-ui/icons-react";
import { useListWorkspaces } from "@/features/workspace/hooks/use-list-workspaces";
import { SearchFieldAlerts } from "./search-field-alerts";
import { useQueryGetWorkspaceAlertTable } from "../hooks/use-query-get-workspace-alerts-table";
import { SwitchMaliciousAlertsFilter } from "./switch-malicious-alerts-filter";
import { useQueryGetWorkspaceAlertTable } from "../hooks/use-query-get-workspace-alerts-table";

const getTitle = (alert: AlertConversation) => {
const prompt = alert.conversation;
Expand Down Expand Up @@ -74,10 +79,87 @@ function IssueDetectedCellContent({ alert }: { alert: AlertConversation }) {
}
}

function EmptyState({
hasMultipleWorkspaces,
}: {
hasMultipleWorkspaces: boolean;
}) {
if (hasMultipleWorkspaces) {
return (
<div className="w-full flex flex-col items-center py-9 gap-2 px-4">
<IllustrationPackage className="size-36" />
<p className="font-bold text-4xl text-gray-900">No alerts found</p>
<p className="text-secondary text-xl">
Alerts will show up here when you use this workspace in your IDE
</p>
<LinkButton
href="https://docs.codegate.ai/features/workspaces"
target="_blank"
className="mt-4"
>
Learn about Workspaces
<LinkExternal02 />
</LinkButton>
</div>
);
}

return (
<div className="w-full flex flex-col items-center py-9 gap-2 px-4">
<IllustrationDragAndDrop className="size-36" />
<p className="font-bold text-4xl text-gray-900">
Connect CodeGate to your IDE
</p>
<p className="text-secondary text-xl">
Learn how to get set up using{" "}
<Link
href="https://docs.codegate.ai/quickstart-continue"
target="_blank"
className="no-underline"
>
Continue
</Link>
,{" "}
<Link
target="_blank"
href="https://docs.codegate.ai/quickstart"
className="no-underline"
>
Copilot
</Link>
, or{" "}
<Link
target="_blank"
href="https://docs.codegate.ai/how-to/use-with-aider"
className="no-underline"
>
Aider
</Link>
.
</p>
<LinkButton
href="https://docs.codegate.ai/"
target="_blank"
className="mt-4"
>
CodeGate Documentation
<LinkExternal02 />
</LinkButton>
</div>
);
}

export function TableAlerts() {
const { page, nextPage, prevPage } = useAlertSearch();
const navigate = useNavigate();
const { data: filteredAlerts = [] } = useQueryGetWorkspaceAlertTable();
const { data: filteredAlerts = [], isLoading: isLoadingAlerts } =
useQueryGetWorkspaceAlertTable();
const {
data: { workspaces } = { workspaces: [] },
isLoading: isLoadingWorkspaces,
} = useListWorkspaces();

const isLoading = isLoadingAlerts || isLoadingWorkspaces;

const { dataView, hasNextPage, hasPreviousPage } = useClientSidePagination(
filteredAlerts,
Expand Down Expand Up @@ -114,7 +196,15 @@ export function TableAlerts() {
<Column width={200}>Token usage</Column>
</Row>
</TableHeader>
<TableBody>
<TableBody
renderEmptyState={() =>
isLoading ? (
<div>Loading alerts</div>
) : (
<EmptyState hasMultipleWorkspaces={workspaces.length > 1} />
)
}
>
{dataView.map((alert) => {
return (
<Row
Expand Down
24 changes: 24 additions & 0 deletions src/routes/__tests__/route-dashboard.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -170,6 +170,12 @@ describe("Dashboard", () => {
).toBeGreaterThan(1);
});

await waitFor(() => {
expect(
screen.queryByText("Connect CodeGate to your IDE"),
).not.toBeInTheDocument();
});

const firstRow = within(screen.getByTestId("alerts-table")).getAllByRole(
"row",
)[1] as HTMLElement;
Expand All @@ -193,6 +199,10 @@ describe("Dashboard", () => {
).toBeGreaterThan(1);
});

await waitFor(() => {
expect(screen.queryByText(/loading alerts/i)).not.toBeInTheDocument();
});

expect(
screen.getByRole("gridcell", {
name: /blocked malicious package/i,
Expand All @@ -219,6 +229,10 @@ describe("Dashboard", () => {
).toBeGreaterThan(1);
});

await waitFor(() => {
expect(screen.queryByText(/loading alerts/i)).not.toBeInTheDocument();
});

expect(screen.getByTestId(/alerts-count/i)).toHaveTextContent("2");
expect(
screen.getAllByRole("gridcell", {
Expand Down Expand Up @@ -262,6 +276,10 @@ describe("Dashboard", () => {
).toBeGreaterThan(1);
});

await waitFor(() => {
expect(screen.queryByText(/loading alerts/i)).not.toBeInTheDocument();
});

expect(screen.getByTestId(/alerts-count/i)).toHaveTextContent("2");
expect(
screen.getAllByRole("gridcell", {
Expand Down Expand Up @@ -289,6 +307,12 @@ describe("Dashboard", () => {
).toBeGreaterThan(1);
});

await waitFor(() => {
expect(
screen.queryByText("Connect CodeGate to your IDE"),
).not.toBeInTheDocument();
});

const firstRow = within(screen.getByTestId("alerts-table")).getAllByRole(
"row",
)[1] as HTMLElement;
Expand Down
Loading