From 06c382c470e844be50fa211987330bef56708f82 Mon Sep 17 00:00:00 2001 From: Steve Rydz Date: Thu, 15 Aug 2024 08:51:04 +0100 Subject: [PATCH] feat: Add validation sets table (#4804) --- .../hooks/__tests__/useValidationSets.test.ts | 12 ++ static/js/validation-sets/hooks/index.ts | 3 + .../hooks/useValidationSets.ts | 24 +++ static/js/validation-sets/index.tsx | 9 +- .../pages/ValidationSets/ValidationSets.tsx | 168 +++++++++++++++++- .../__tests__/ValidationSets.test.tsx | 110 ++++++++++++ 6 files changed, 324 insertions(+), 2 deletions(-) create mode 100644 static/js/validation-sets/hooks/__tests__/useValidationSets.test.ts create mode 100644 static/js/validation-sets/hooks/index.ts create mode 100644 static/js/validation-sets/hooks/useValidationSets.ts create mode 100644 static/js/validation-sets/pages/ValidationSets/__tests__/ValidationSets.test.tsx diff --git a/static/js/validation-sets/hooks/__tests__/useValidationSets.test.ts b/static/js/validation-sets/hooks/__tests__/useValidationSets.test.ts new file mode 100644 index 0000000000..d909691d4c --- /dev/null +++ b/static/js/validation-sets/hooks/__tests__/useValidationSets.test.ts @@ -0,0 +1,12 @@ +import * as ReactQuery from "react-query"; +import { renderHook } from "@testing-library/react"; + +import useValidationSets from "../useValidationSets"; + +describe("useValidationSets", () => { + test("Calls useQuery", () => { + jest.spyOn(ReactQuery, "useQuery").mockImplementation(jest.fn()); + renderHook(() => useValidationSets()); + expect(ReactQuery.useQuery).toHaveBeenCalled(); + }); +}); diff --git a/static/js/validation-sets/hooks/index.ts b/static/js/validation-sets/hooks/index.ts new file mode 100644 index 0000000000..feb766d627 --- /dev/null +++ b/static/js/validation-sets/hooks/index.ts @@ -0,0 +1,3 @@ +import useValidationSets from "./useValidationSets"; + +export { useValidationSets }; diff --git a/static/js/validation-sets/hooks/useValidationSets.ts b/static/js/validation-sets/hooks/useValidationSets.ts new file mode 100644 index 0000000000..965549c95a --- /dev/null +++ b/static/js/validation-sets/hooks/useValidationSets.ts @@ -0,0 +1,24 @@ +import { useQuery } from "react-query"; + +function useValidationSets() { + return useQuery({ + queryKey: ["validationSets"], + queryFn: async () => { + const response = await fetch("/api/validation-sets"); + + if (!response.ok) { + throw new Error("Unable to fetch validation sets"); + } + + const validationSetsData = await response.json(); + + if (!validationSetsData.success) { + throw new Error(validationSetsData.message); + } + + return validationSetsData.data; + }, + }); +} + +export default useValidationSets; diff --git a/static/js/validation-sets/index.tsx b/static/js/validation-sets/index.tsx index dbe5f08162..c1275a3a62 100644 --- a/static/js/validation-sets/index.tsx +++ b/static/js/validation-sets/index.tsx @@ -1,5 +1,6 @@ import { createRoot } from "react-dom/client"; import { createBrowserRouter, RouterProvider } from "react-router-dom"; +import { QueryClient, QueryClientProvider } from "react-query"; import Root from "./routes/root"; import ValidationSets from "./pages/ValidationSets"; @@ -25,4 +26,10 @@ const router = createBrowserRouter([ const rootEl = document.getElementById("root") as HTMLElement; const root = createRoot(rootEl); -root.render(); +const queryClient = new QueryClient(); + +root.render( + + + +); diff --git a/static/js/validation-sets/pages/ValidationSets/ValidationSets.tsx b/static/js/validation-sets/pages/ValidationSets/ValidationSets.tsx index cf21ef75c6..28635c70c8 100644 --- a/static/js/validation-sets/pages/ValidationSets/ValidationSets.tsx +++ b/static/js/validation-sets/pages/ValidationSets/ValidationSets.tsx @@ -1,7 +1,173 @@ +import { Link, useSearchParams } from "react-router-dom"; +import { format } from "date-fns"; +import { + Row, + Col, + MainTable, + Icon, + Notification, + Button, +} from "@canonical/react-components"; + +import { useValidationSets } from "../../hooks"; + +type ValidationSet = { + name: string; + revision?: number; + sequence?: number; + snaps: { id: string; name: string }[]; + timestamp: string; +}; + function ValidationSets(): JSX.Element { + const { status, data: validationSets } = useValidationSets(); + const [searchParams, setSearchParams] = useSearchParams(); + + const filteredValidationSets: ValidationSet[] = + validationSets && validationSets.length + ? validationSets.filter((validationSet: ValidationSet) => { + const query = searchParams.get("filter"); + + if (!query) { + return true; + } + + return validationSet.name.includes(query); + }) + : []; + return ( <> -

Validation sets page

+

My validation sets

+ + + +
+ + { + if (e.target.value) { + setSearchParams({ filter: e.target.value }); + } else { + setSearchParams(); + } + }} + /> + + +
+ +
+ + {status === "loading" && ( +

+ +   Fetching validation sets +

+ )} + + {status === "success" && filteredValidationSets.length === 0 && ( + + There are no validation sets to display + + )} + + {status === "error" && ( + + Unable to load validation sets + + )} + + {status === "success" && filteredValidationSets.length > 0 && ( + { + return { + columns: [ + { + content: ( + + {validationSet.name} + + ), + }, + { + content: validationSet.revision || "-", + className: "u-align--right", + }, + { + content: validationSet.sequence || "-", + className: "u-align--right", + }, + { + content: validationSet.snaps.length, + className: "u-align--right", + }, + { + content: format( + new Date(validationSet.timestamp), + "dd/MM/yyyy" + ), + className: "u-align--right", + }, + ], + sortData: { + name: validationSet.name, + revision: validationSet.revision, + sequence: validationSet.sequence, + snaps: validationSet.snaps.length, + updated: validationSet.timestamp, + }, + }; + })} + /> + )} ); } diff --git a/static/js/validation-sets/pages/ValidationSets/__tests__/ValidationSets.test.tsx b/static/js/validation-sets/pages/ValidationSets/__tests__/ValidationSets.test.tsx new file mode 100644 index 0000000000..614a673263 --- /dev/null +++ b/static/js/validation-sets/pages/ValidationSets/__tests__/ValidationSets.test.tsx @@ -0,0 +1,110 @@ +import { BrowserRouter, useSearchParams } from "react-router-dom"; +import { QueryClient, QueryClientProvider, useQuery } from "react-query"; +import { render, screen } from "@testing-library/react"; +import "@testing-library/jest-dom"; + +import ValidationSets from "../ValidationSets"; + +const queryClient = new QueryClient(); + +const renderComponent = () => { + return render( + + + + + + ); +}; + +const mockValidationSets = [ + { + name: "validation-set-1", + revision: 1, + sequence: 1, + snaps: [ + { + id: "test-snap-1-id", + name: "test-snap-1", + }, + ], + timestamp: "2024-08-13T09:49:18Z", + }, + { + name: "validation-set-2", + revision: 1, + sequence: 2, + snaps: [ + { + id: "test-snap-1-id", + name: "test-snap-1", + }, + ], + timestamp: "2024-08-13T09:49:18Z", + }, +]; + +jest.mock("react-query", () => ({ + ...jest.requireActual("react-query"), + useQuery: jest.fn(), +})); + +jest.mock("react-router-dom", () => ({ + ...jest.requireActual("react-router-dom"), + useSearchParams: jest.fn(), +})); + +describe("ValidationSets", () => { + test("shows loading state when fetching validation sets", () => { + // @ts-ignore + useQuery.mockReturnValue({ status: "loading", data: undefined }); + // @ts-ignore + useSearchParams.mockReturnValue([new URLSearchParams()]); + renderComponent(); + expect(screen.getByText(/Fetching validation sets/)).toBeInTheDocument(); + }); + + test("shows message if no validation sets", () => { + // @ts-ignore + useQuery.mockReturnValue({ status: "success", data: [] }); + // @ts-ignore + useSearchParams.mockReturnValue([new URLSearchParams()]); + renderComponent(); + expect( + screen.getByText(/There are no validation sets to display/) + ).toBeInTheDocument(); + }); + + test("shows message when there is an error fetching validation sets", () => { + // @ts-ignore + useQuery.mockReturnValue({ status: "error", data: undefined }); + // @ts-ignore + useSearchParams.mockReturnValue([new URLSearchParams()]); + renderComponent(); + expect( + screen.getByText(/Unable to load validation sets/) + ).toBeInTheDocument(); + }); + + test("displays validation sets", () => { + // @ts-ignore + useQuery.mockReturnValue({ status: "success", data: mockValidationSets }); + // @ts-ignore + useSearchParams.mockReturnValue([new URLSearchParams()]); + renderComponent(); + expect(screen.getByText(mockValidationSets[0].name)).toBeInTheDocument(); + expect(screen.getByText(mockValidationSets[1].name)).toBeInTheDocument(); + }); + + test("filters validation sets based on search query", () => { + // @ts-ignore + useQuery.mockReturnValue({ status: "success", data: mockValidationSets }); + // @ts-ignore + useSearchParams.mockReturnValue([new URLSearchParams({ filter: "set-2" })]); + renderComponent(); + expect(screen.getByText(mockValidationSets[1].name)).toBeInTheDocument(); + expect( + screen.queryByText(mockValidationSets[0].name) + ).not.toBeInTheDocument(); + }); +});