Skip to content

Commit e39dbc3

Browse files
authored
feat: implement empty state for tables (#232)
* implement empty state for tables * fixes * test "fixes" * add rudimentary loading state for alerts table * add setup links to empty table state * make all links open in new tab * eye candy for empty table state * more eye candy * nitpick * visual changes * add different empty state when user has multiple workspaces * fix illustration
1 parent 3041074 commit e39dbc3

File tree

3 files changed

+232
-6
lines changed

3 files changed

+232
-6
lines changed

src/features/alerts/components/__tests__/table-alerts.test.tsx

+114-2
Original file line numberDiff line numberDiff line change
@@ -36,14 +36,18 @@ test("renders token usage cell correctly", async () => {
3636
}),
3737
);
3838

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

4141
await waitFor(() => {
4242
expect(
4343
within(screen.getByTestId("alerts-table")).getAllByRole("row"),
4444
).toHaveLength(2);
4545
});
4646

47+
await waitFor(() => {
48+
expect(queryByText(/loading alerts/i)).not.toBeInTheDocument();
49+
});
50+
4751
expect(getByTestId("icon-arrow-up")).toBeVisible();
4852
expect(getByTestId("icon-arrow-down")).toBeVisible();
4953

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

66-
const { getByText } = render(<TableAlerts />);
70+
const { getByText, queryByText } = render(<TableAlerts />);
6771

6872
await waitFor(() => {
6973
expect(
7074
within(screen.getByTestId("alerts-table")).getAllByRole("row"),
7175
).toHaveLength(2);
7276
});
7377

78+
await waitFor(() => {
79+
expect(queryByText(/loading alerts/i)).not.toBeInTheDocument();
80+
});
81+
7482
expect(getByText("N/A")).toBeVisible();
7583
});
84+
85+
test("renders empty state when the API returns no alerts - user has not created multipe workspaces", async () => {
86+
server.use(
87+
http.get("*/workspaces/:name/alerts", () => {
88+
return HttpResponse.json([]);
89+
}),
90+
http.get("*/workspaces", () => {
91+
return HttpResponse.json({
92+
workspaces: [
93+
{
94+
name: "my-awesome-workspace",
95+
is_active: true,
96+
last_updated: new Date(Date.now()).toISOString(),
97+
},
98+
],
99+
});
100+
}),
101+
);
102+
103+
const { getByText, queryByText, getByRole } = render(<TableAlerts />);
104+
105+
await waitFor(() => {
106+
expect(queryByText(/loading alerts/i)).not.toBeInTheDocument();
107+
});
108+
109+
expect(getByText("Connect CodeGate to your IDE")).toBeVisible();
110+
expect(getByText(/learn how to get set up using/i)).toBeVisible();
111+
112+
expect(getByRole("link", { name: /continue/i })).toHaveAttribute(
113+
"href",
114+
"https://docs.codegate.ai/quickstart-continue",
115+
);
116+
expect(getByRole("link", { name: /continue/i })).toHaveAttribute(
117+
"target",
118+
"_blank",
119+
);
120+
expect(getByRole("link", { name: /copilot/i })).toHaveAttribute(
121+
"href",
122+
"https://docs.codegate.ai/quickstart",
123+
);
124+
expect(getByRole("link", { name: /copilot/i })).toHaveAttribute(
125+
"target",
126+
"_blank",
127+
);
128+
expect(getByRole("link", { name: /aider/i })).toHaveAttribute(
129+
"href",
130+
"https://docs.codegate.ai/how-to/use-with-aider",
131+
);
132+
expect(getByRole("link", { name: /aider/i })).toHaveAttribute(
133+
"target",
134+
"_blank",
135+
);
136+
137+
expect(
138+
getByRole("link", { name: /codegate documentation/i }),
139+
).toHaveAttribute("href", "https://docs.codegate.ai/");
140+
expect(
141+
getByRole("link", { name: /codegate documentation/i }),
142+
).toHaveAttribute("target", "_blank");
143+
});
144+
145+
test("does not render table empty state when the API responds with alerts", async () => {
146+
server.use(
147+
http.get("*/workspaces/:name/alerts", () => {
148+
return HttpResponse.json([
149+
makeMockAlert({ token_usage: false, type: "malicious" }),
150+
]);
151+
}),
152+
);
153+
154+
const { queryByText } = render(<TableAlerts />);
155+
156+
await waitFor(() => {
157+
expect(queryByText("Connect CodeGate to your IDE")).not.toBeInTheDocument();
158+
});
159+
});
160+
161+
test("renders empty state when the API returns no alerts - user has multiple workspaces", async () => {
162+
server.use(
163+
http.get("*/workspaces/:name/alerts", () => {
164+
return HttpResponse.json([]);
165+
}),
166+
);
167+
168+
const { getByText, queryByText, getByRole } = render(<TableAlerts />);
169+
170+
await waitFor(() => {
171+
expect(queryByText(/loading alerts/i)).not.toBeInTheDocument();
172+
});
173+
174+
expect(getByText(/no alerts found/i)).toBeVisible();
175+
expect(
176+
getByText(
177+
/alerts will show up here when you use this workspace in your IDE/i,
178+
),
179+
).toBeVisible();
180+
181+
expect(
182+
getByRole("link", { name: /learn about workspaces/i }),
183+
).toHaveAttribute("href", "https://docs.codegate.ai/features/workspaces");
184+
expect(
185+
getByRole("link", { name: /learn about workspaces/i }),
186+
).toHaveAttribute("target", "_blank");
187+
});

src/features/alerts/components/table-alerts.tsx

+94-4
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,10 @@ import {
99
Badge,
1010
Button,
1111
ResizableTableContainer,
12+
Link,
13+
LinkButton,
14+
IllustrationDragAndDrop,
15+
IllustrationPackage,
1216
} from "@stacklok/ui-kit";
1317
import { AlertConversation, QuestionType } from "@/api/generated";
1418
import {
@@ -20,10 +24,11 @@ import { useAlertSearch } from "@/hooks/useAlertSearch";
2024
import { useNavigate } from "react-router-dom";
2125
import { useClientSidePagination } from "@/hooks/useClientSidePagination";
2226
import { TableAlertTokenUsage } from "./table-alert-token-usage";
23-
import { Key01, PackageX } from "@untitled-ui/icons-react";
27+
import { Key01, LinkExternal02, PackageX } from "@untitled-ui/icons-react";
28+
import { useListWorkspaces } from "@/features/workspace/hooks/use-list-workspaces";
2429
import { SearchFieldAlerts } from "./search-field-alerts";
25-
import { useQueryGetWorkspaceAlertTable } from "../hooks/use-query-get-workspace-alerts-table";
2630
import { SwitchMaliciousAlertsFilter } from "./switch-malicious-alerts-filter";
31+
import { useQueryGetWorkspaceAlertTable } from "../hooks/use-query-get-workspace-alerts-table";
2732

2833
const getTitle = (alert: AlertConversation) => {
2934
const prompt = alert.conversation;
@@ -74,10 +79,87 @@ function IssueDetectedCellContent({ alert }: { alert: AlertConversation }) {
7479
}
7580
}
7681

82+
function EmptyState({
83+
hasMultipleWorkspaces,
84+
}: {
85+
hasMultipleWorkspaces: boolean;
86+
}) {
87+
if (hasMultipleWorkspaces) {
88+
return (
89+
<div className="w-full flex flex-col items-center py-9 gap-2 px-4">
90+
<IllustrationPackage className="size-36" />
91+
<p className="font-bold text-4xl text-gray-900">No alerts found</p>
92+
<p className="text-secondary text-xl">
93+
Alerts will show up here when you use this workspace in your IDE
94+
</p>
95+
<LinkButton
96+
href="https://docs.codegate.ai/features/workspaces"
97+
target="_blank"
98+
className="mt-4"
99+
>
100+
Learn about Workspaces
101+
<LinkExternal02 />
102+
</LinkButton>
103+
</div>
104+
);
105+
}
106+
107+
return (
108+
<div className="w-full flex flex-col items-center py-9 gap-2 px-4">
109+
<IllustrationDragAndDrop className="size-36" />
110+
<p className="font-bold text-4xl text-gray-900">
111+
Connect CodeGate to your IDE
112+
</p>
113+
<p className="text-secondary text-xl">
114+
Learn how to get set up using{" "}
115+
<Link
116+
href="https://docs.codegate.ai/quickstart-continue"
117+
target="_blank"
118+
className="no-underline"
119+
>
120+
Continue
121+
</Link>
122+
,{" "}
123+
<Link
124+
target="_blank"
125+
href="https://docs.codegate.ai/quickstart"
126+
className="no-underline"
127+
>
128+
Copilot
129+
</Link>
130+
, or{" "}
131+
<Link
132+
target="_blank"
133+
href="https://docs.codegate.ai/how-to/use-with-aider"
134+
className="no-underline"
135+
>
136+
Aider
137+
</Link>
138+
.
139+
</p>
140+
<LinkButton
141+
href="https://docs.codegate.ai/"
142+
target="_blank"
143+
className="mt-4"
144+
>
145+
CodeGate Documentation
146+
<LinkExternal02 />
147+
</LinkButton>
148+
</div>
149+
);
150+
}
151+
77152
export function TableAlerts() {
78153
const { page, nextPage, prevPage } = useAlertSearch();
79154
const navigate = useNavigate();
80-
const { data: filteredAlerts = [] } = useQueryGetWorkspaceAlertTable();
155+
const { data: filteredAlerts = [], isLoading: isLoadingAlerts } =
156+
useQueryGetWorkspaceAlertTable();
157+
const {
158+
data: { workspaces } = { workspaces: [] },
159+
isLoading: isLoadingWorkspaces,
160+
} = useListWorkspaces();
161+
162+
const isLoading = isLoadingAlerts || isLoadingWorkspaces;
81163

82164
const { dataView, hasNextPage, hasPreviousPage } = useClientSidePagination(
83165
filteredAlerts,
@@ -114,7 +196,15 @@ export function TableAlerts() {
114196
<Column width={200}>Token usage</Column>
115197
</Row>
116198
</TableHeader>
117-
<TableBody>
199+
<TableBody
200+
renderEmptyState={() =>
201+
isLoading ? (
202+
<div>Loading alerts</div>
203+
) : (
204+
<EmptyState hasMultipleWorkspaces={workspaces.length > 1} />
205+
)
206+
}
207+
>
118208
{dataView.map((alert) => {
119209
return (
120210
<Row

src/routes/__tests__/route-dashboard.test.tsx

+24
Original file line numberDiff line numberDiff line change
@@ -170,6 +170,12 @@ describe("Dashboard", () => {
170170
).toBeGreaterThan(1);
171171
});
172172

173+
await waitFor(() => {
174+
expect(
175+
screen.queryByText("Connect CodeGate to your IDE"),
176+
).not.toBeInTheDocument();
177+
});
178+
173179
const firstRow = within(screen.getByTestId("alerts-table")).getAllByRole(
174180
"row",
175181
)[1] as HTMLElement;
@@ -193,6 +199,10 @@ describe("Dashboard", () => {
193199
).toBeGreaterThan(1);
194200
});
195201

202+
await waitFor(() => {
203+
expect(screen.queryByText(/loading alerts/i)).not.toBeInTheDocument();
204+
});
205+
196206
expect(
197207
screen.getByRole("gridcell", {
198208
name: /blocked malicious package/i,
@@ -219,6 +229,10 @@ describe("Dashboard", () => {
219229
).toBeGreaterThan(1);
220230
});
221231

232+
await waitFor(() => {
233+
expect(screen.queryByText(/loading alerts/i)).not.toBeInTheDocument();
234+
});
235+
222236
expect(screen.getByTestId(/alerts-count/i)).toHaveTextContent("2");
223237
expect(
224238
screen.getAllByRole("gridcell", {
@@ -262,6 +276,10 @@ describe("Dashboard", () => {
262276
).toBeGreaterThan(1);
263277
});
264278

279+
await waitFor(() => {
280+
expect(screen.queryByText(/loading alerts/i)).not.toBeInTheDocument();
281+
});
282+
265283
expect(screen.getByTestId(/alerts-count/i)).toHaveTextContent("2");
266284
expect(
267285
screen.getAllByRole("gridcell", {
@@ -289,6 +307,12 @@ describe("Dashboard", () => {
289307
).toBeGreaterThan(1);
290308
});
291309

310+
await waitFor(() => {
311+
expect(
312+
screen.queryByText("Connect CodeGate to your IDE"),
313+
).not.toBeInTheDocument();
314+
});
315+
292316
const firstRow = within(screen.getByTestId("alerts-table")).getAllByRole(
293317
"row",
294318
)[1] as HTMLElement;

0 commit comments

Comments
 (0)