diff --git a/package-lock.json b/package-lock.json index 51a703b3..b19088a9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -15,6 +15,7 @@ "@radix-ui/react-slot": "^1.1.0", "@stacklok/ui-kit": "^1.0.1-1", "@tanstack/react-query": "^5.64.1", + "@tanstack/react-query-devtools": "^5.66.0", "@types/prismjs": "^1.26.5", "@types/react-syntax-highlighter": "^15.5.13", "@untitled-ui/icons-react": "^0.1.4", @@ -4219,6 +4220,16 @@ "url": "https://github.com/sponsors/tannerlinsley" } }, + "node_modules/@tanstack/query-devtools": { + "version": "5.65.0", + "resolved": "https://registry.npmjs.org/@tanstack/query-devtools/-/query-devtools-5.65.0.tgz", + "integrity": "sha512-g5y7zc07U9D3esMdqUfTEVu9kMHoIaVBsD0+M3LPdAdD710RpTcLiNvJY1JkYXqkq9+NV+CQoemVNpQPBXVsJg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + } + }, "node_modules/@tanstack/react-query": { "version": "5.64.1", "resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.64.1.tgz", @@ -4234,6 +4245,23 @@ "react": "^18 || ^19" } }, + "node_modules/@tanstack/react-query-devtools": { + "version": "5.66.0", + "resolved": "https://registry.npmjs.org/@tanstack/react-query-devtools/-/react-query-devtools-5.66.0.tgz", + "integrity": "sha512-uB57wA2YZaQ2fPcFW0E9O1zAGDGSbRKRx84uMk/86VyU9jWVxvJ3Uzp+zNm+nZJYsuekCIo2opTdgNuvM3cKgA==", + "license": "MIT", + "dependencies": { + "@tanstack/query-devtools": "5.65.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + }, + "peerDependencies": { + "@tanstack/react-query": "^5.66.0", + "react": "^18 || ^19" + } + }, "node_modules/@testing-library/dom": { "version": "10.4.0", "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-10.4.0.tgz", diff --git a/package.json b/package.json index 8f7347b8..7e1b6f16 100644 --- a/package.json +++ b/package.json @@ -27,6 +27,7 @@ "@radix-ui/react-slot": "^1.1.0", "@stacklok/ui-kit": "^1.0.1-1", "@tanstack/react-query": "^5.64.1", + "@tanstack/react-query-devtools": "^5.66.0", "@types/prismjs": "^1.26.5", "@types/react-syntax-highlighter": "^15.5.13", "@untitled-ui/icons-react": "^0.1.4", diff --git a/src/hooks/__tests__/useBrowserNotification.test.ts b/src/hooks/__tests__/useBrowserNotification.test.ts deleted file mode 100644 index e8899c41..00000000 --- a/src/hooks/__tests__/useBrowserNotification.test.ts +++ /dev/null @@ -1,57 +0,0 @@ -import { renderHook } from "@testing-library/react"; -import { vi } from "vitest"; -import { useBrowserNotification } from "../useBrowserNotification"; - -describe("useBrowserNotification", () => { - let mockRequestPermission: ( - deprecatedCallback?: NotificationPermissionCallback | undefined, - ) => Promise; - let mockNotification: ReturnType; - - beforeAll(() => { - mockRequestPermission = vi.fn(); - mockNotification = vi.fn(); - - global.Notification = vi.fn( - (title: string, options?: NotificationOptions) => { - mockNotification(title, options); - return {}; - }, - ) as unknown as typeof Notification; - - Object.defineProperty(global.Notification, "permission", { - value: "default", - writable: true, - }); - global.Notification.requestPermission = mockRequestPermission; - }); - - it("should request the permission for notification", () => { - renderHook(() => useBrowserNotification()); - expect(mockRequestPermission).toHaveBeenCalled(); - }); - - it("should send a notification if permission is granted", () => { - Object.defineProperty(global.Notification, "permission", { - value: "granted", - }); - - const { result } = renderHook(() => useBrowserNotification()); - result.current.sendNotification("title", { body: "body" }); - - expect(mockNotification).toHaveBeenCalledWith("title", { - body: "body", - }); - }); - - it("should not send a notification if permission is denied", () => { - Object.defineProperty(global.Notification, "permission", { - value: "denied", - }); - - const { result } = renderHook(() => useBrowserNotification()); - result.current.sendNotification("title", { body: "body" }); - - expect(mockNotification).not.toHaveBeenCalled(); - }); -}); diff --git a/src/hooks/__tests__/useSee.test.ts b/src/hooks/__tests__/useSee.test.ts index 9c0bdf4c..1fc34d03 100644 --- a/src/hooks/__tests__/useSee.test.ts +++ b/src/hooks/__tests__/useSee.test.ts @@ -7,12 +7,19 @@ vi.mock("react-router-dom", () => ({ useLocation: vi.fn(() => ({ pathname: "/" })), })); -const mockSendNotification = vi.fn(); -vi.mock("../useBrowserNotification", () => ({ - useBrowserNotification: vi.fn(() => { - return { sendNotification: mockSendNotification }; - }), -})); +const mockInvalidate = vi.fn(); + +vi.mock("@tanstack/react-query", async () => { + const original = await vi.importActual< + typeof import("@tanstack/react-query") + >("@tanstack/react-query"); + return { + ...original, + useQueryClient: () => ({ + invalidateQueries: mockInvalidate, + }), + }; +}); class MockEventSource { static instances: MockEventSource[] = []; @@ -63,7 +70,7 @@ describe("useSse", () => { global.EventSource = originalEventSource; }); - it("should send notification if new alert is detected", () => { + it("should invalidate queries if new alert is detected", () => { renderHook(() => useSse(), { wrapper: TestQueryClientProvider }); expect(MockEventSource.instances.length).toBe(1); @@ -76,29 +83,6 @@ describe("useSse", () => { MockEventSource.triggerMessage("new alert detected"); }); - expect(mockSendNotification).toHaveBeenCalledWith("CodeGate Dashboard", { - body: "New Alert detected!", - }); - - act(() => { - vi.advanceTimersByTime(2000); - }); - - expect(global.location.reload).toHaveBeenCalled(); - }); - - it("should send notification if new alert is detected", () => { - renderHook(() => useSse(), { wrapper: TestQueryClientProvider }); - - act(() => { - MockEventSource.triggerMessage("other message"); - }); - - expect(mockSendNotification).not.toHaveBeenCalledWith( - "CodeGate Dashboard", - { - body: "New Alert detected!", - }, - ); + expect(mockInvalidate).toHaveBeenCalled(); }); }); diff --git a/src/hooks/useBrowserNotification.ts b/src/hooks/useBrowserNotification.ts deleted file mode 100644 index 3b607a3d..00000000 --- a/src/hooks/useBrowserNotification.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { useEffect } from "react"; - -export function useBrowserNotification() { - const requestPermission = () => { - if ("Notification" in window) { - Notification.requestPermission(); - } - }; - - const sendNotification = (title: string, options: NotificationOptions) => { - if ("Notification" in window && Notification.permission === "granted") { - new Notification(title, options); - } - }; - - useEffect(() => { - requestPermission(); - }, []); - - return { sendNotification }; -} diff --git a/src/hooks/useSse.ts b/src/hooks/useSse.ts index 6bd783b9..32ad8e0e 100644 --- a/src/hooks/useSse.ts +++ b/src/hooks/useSse.ts @@ -1,13 +1,16 @@ import { useEffect } from "react"; -import { useBrowserNotification } from "./useBrowserNotification"; import { useLocation } from "react-router-dom"; -import { useQueryClient } from "@tanstack/react-query"; +import { Query, useQueryClient } from "@tanstack/react-query"; +import { OpenApiTsReactQueryKey } from "@/types/openapi-ts"; +import { + v1GetWorkspaceAlertsQueryKey, + v1GetWorkspaceMessagesQueryKey, +} from "@/api/generated/@tanstack/react-query.gen"; const BASE_URL = import.meta.env.VITE_BASE_API_URL; export function useSse() { const location = useLocation(); - const { sendNotification } = useBrowserNotification(); const queryClient = useQueryClient(); useEffect(() => { @@ -17,19 +20,25 @@ export function useSse() { eventSource.onmessage = function (event) { if (event.data.toLowerCase().includes("new alert detected")) { - queryClient.invalidateQueries({ refetchType: "all" }); - sendNotification("CodeGate Dashboard", { - body: "New Alert detected!", + queryClient.invalidateQueries({ + refetchType: "all", + predicate: ( + query: Query, + ) => + query.queryKey[0]._id === + v1GetWorkspaceAlertsQueryKey({ + path: { workspace_name: "default" }, // NOTE: Just supplying "default" to satisfy the type-checker, because we are just using the `_id`, this invalidates for any workspace + })[0]?._id || + query.queryKey[0]._id === + v1GetWorkspaceMessagesQueryKey({ + path: { workspace_name: "default" }, // NOTE: Just supplying "default" to satisfy the type-checker, because we are just using the `_id`, this invalidates for any workspace + })[0]?._id, }); - - setTimeout(() => { - window.location.reload(); - }, 2000); } }; return () => { eventSource.close(); }; - }, [location.pathname, queryClient, sendNotification]); + }, [location.pathname, queryClient]); } diff --git a/src/main.tsx b/src/main.tsx index bd08502d..4a874faf 100644 --- a/src/main.tsx +++ b/src/main.tsx @@ -12,6 +12,7 @@ import { QueryClientProvider } from "./components/react-query-provider.tsx"; import { BrowserRouter } from "react-router-dom"; import { UiKitClientSideRoutingProvider } from "./lib/ui-kit-client-side-routing.tsx"; import { ConfirmProvider } from "./context/confirm-context.tsx"; +import { ReactQueryDevtools } from "@tanstack/react-query-devtools"; // Initialize the API client client.setConfig({ @@ -31,6 +32,7 @@ createRoot(document.getElementById("root")!).render( + diff --git a/src/types/openapi-ts.ts b/src/types/openapi-ts.ts new file mode 100644 index 00000000..1f95fc74 --- /dev/null +++ b/src/types/openapi-ts.ts @@ -0,0 +1,11 @@ +import { OptionsLegacyParser } from "@hey-api/client-fetch"; + +export type OpenApiTsReactQueryKey = [ + Pick< + OptionsLegacyParser, + "baseUrl" | "body" | "headers" | "path" | "query" + > & { + _id: string; + _infinite?: boolean; + }, +];