Skip to content

Commit 51fd254

Browse files
authored
feat(workspace): add dropdown (#123)
* feat(workspace): add workspace dropdown selection * fix: invalidate cache should be called only for alerts event * fix: sse api path * fix: close dialog * fix: mock workspaces api
1 parent 0d9d752 commit 51fd254

File tree

6 files changed

+115
-6
lines changed

6 files changed

+115
-6
lines changed

src/App.test.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,7 @@ describe("App", () => {
6161
render(<App />);
6262
await waitFor(() =>
6363
expect(
64-
screen.getByRole("link", { name: /codeGate dashboard/i }),
64+
screen.getByRole("link", { name: "CodeGate Dashboard" }),
6565
).toBeVisible(),
6666
);
6767
expect(screen.getByRole("link", { name: "Dashboard" })).toBeVisible();

src/components/Header.tsx

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,28 +2,31 @@ import { Link } from "react-router-dom";
22
import { SidebarTrigger } from "./ui/sidebar";
33
import { HoverPopover } from "./HoverPopover";
44
import { Separator, ButtonDarkMode } from "@stacklok/ui-kit";
5+
import { WorkspacesSelection } from "@/features/workspace/components/workspaces-selection";
56

67
export function Header({ hasError }: { hasError?: boolean }) {
78
return (
89
<header
910
aria-label="App header"
1011
className="shrink-0 h-16 px-3 items-center flex w-full bg-gray-25 border-b-gray-200 border-b"
1112
>
12-
<div className="flex items-center flex-1">
13+
<div className="flex items-center gap-2 flex-1">
1314
{!hasError && (
1415
<>
1516
<SidebarTrigger />
16-
<Separator orientation="vertical" className="h-8 mx-2" />
17+
<Separator orientation="vertical" className="h-8" />
1718
</>
1819
)}
1920

20-
<nav className="mr-1 flex ml-2">
21+
<nav className="flex ml-2">
2122
<Link to="/">
2223
<h1 className="text-2xl text-primary font-title w-max flex font-semibold">
2324
CodeGate Dashboard
2425
</h1>
2526
</Link>
2627
</nav>
28+
<Separator orientation="vertical" className="h-8 ml-4" />
29+
<WorkspacesSelection />
2730
</div>
2831
<div className="flex items-center gap-4 mr-16">
2932
<HoverPopover title="Certificates">
Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
import { useWorkspacesData } from "@/hooks/useWorkspacesData";
2+
import {
3+
Button,
4+
DialogTrigger,
5+
Input,
6+
ListBox,
7+
ListBoxItem,
8+
Popover,
9+
SearchField,
10+
Separator,
11+
} from "@stacklok/ui-kit";
12+
import { useQueryClient } from "@tanstack/react-query";
13+
import clsx from "clsx";
14+
import { ChevronDown, Search, Settings } from "lucide-react";
15+
import { useState } from "react";
16+
import { Link } from "react-router-dom";
17+
18+
export function WorkspacesSelection() {
19+
const queryClient = useQueryClient();
20+
const { data } = useWorkspacesData();
21+
const [isOpen, setIsOpen] = useState(false);
22+
const [searchWorkspace, setSearchWorkspace] = useState("");
23+
const workspaces = data?.workspaces ?? [];
24+
const filteredWorkspaces = workspaces.filter((workspace) =>
25+
workspace.name.toLowerCase().includes(searchWorkspace.toLowerCase()),
26+
);
27+
const activeWorkspace = workspaces.find((workspace) => workspace.is_active);
28+
29+
const handleWorkspaceClick = () => {
30+
queryClient.invalidateQueries({ refetchType: "all" });
31+
setIsOpen(false);
32+
};
33+
34+
return (
35+
<DialogTrigger isOpen={isOpen} onOpenChange={(test) => setIsOpen(test)}>
36+
<Button variant="tertiary" className="flex cursor-pointer">
37+
Workspace {activeWorkspace?.name ?? "default"}
38+
<ChevronDown />
39+
</Button>
40+
41+
<Popover className="w-1/4 p-4" placement="bottom left">
42+
<div>
43+
<div>
44+
<SearchField
45+
onChange={setSearchWorkspace}
46+
autoFocus
47+
aria-label="search"
48+
>
49+
<Input icon={<Search />} />
50+
</SearchField>
51+
</div>
52+
53+
<ListBox
54+
className="pb-2 pt-3"
55+
aria-label="Workspaces"
56+
items={filteredWorkspaces}
57+
selectedKeys={activeWorkspace?.name ?? []}
58+
renderEmptyState={() => (
59+
<p className="text-center">No workspaces found</p>
60+
)}
61+
>
62+
{(item) => (
63+
<ListBoxItem
64+
id={item.name}
65+
onAction={() => handleWorkspaceClick()}
66+
className={clsx(
67+
"cursor-pointer py-2 m-1 text-base hover:bg-gray-300",
68+
{
69+
"bg-gray-900 text-white hover:text-secondary":
70+
item.is_active,
71+
},
72+
)}
73+
key={item.name}
74+
>
75+
{item.name}
76+
</ListBoxItem>
77+
)}
78+
</ListBox>
79+
<Separator className="" />
80+
<Link
81+
to="/workspaces"
82+
onClick={() => setIsOpen(false)}
83+
className="text-secondary pt-3 px-2 gap-2 flex"
84+
>
85+
<Settings />
86+
Manage Workspaces
87+
</Link>
88+
</div>
89+
</Popover>
90+
</DialogTrigger>
91+
);
92+
}

src/hooks/useSse.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,12 +12,12 @@ export function useSse() {
1212

1313
useEffect(() => {
1414
const eventSource = new EventSource(
15-
`${BASE_URL}/dashboard/alerts_notification`,
15+
`${BASE_URL}/api/v1/dashboard/alerts_notification`,
1616
);
1717

1818
eventSource.onmessage = function (event) {
19-
queryClient.invalidateQueries({ refetchType: "all" });
2019
if (event.data.toLowerCase().includes("new alert detected")) {
20+
queryClient.invalidateQueries({ refetchType: "all" });
2121
sendNotification("CodeGate Dashboard", {
2222
body: "New Alert detected!",
2323
});
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
[
2+
{
3+
"name": "workspace-1",
4+
"is_active": true
5+
},
6+
{
7+
"name": "workspace-2",
8+
"is_active": false
9+
}
10+
]

src/mocks/msw/handlers.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { http, HttpResponse } from "msw";
22
import mockedPrompts from "@/mocks/msw/fixtures/GET_MESSAGES.json";
33
import mockedAlerts from "@/mocks/msw/fixtures/GET_ALERTS.json";
4+
import mockedWorkspaces from "@/mocks/msw/fixtures/GET_WORKSPACES.json";
45

56
export const handlers = [
67
http.get("*/health", () =>
@@ -20,4 +21,7 @@ export const handlers = [
2021
http.get("*/dashboard/alerts", () => {
2122
return HttpResponse.json(mockedAlerts);
2223
}),
24+
http.get("*/workspaces", () => {
25+
return HttpResponse.json(mockedWorkspaces);
26+
}),
2327
];

0 commit comments

Comments
 (0)