Skip to content

Commit

Permalink
feat: Add validation sets table (#4804)
Browse files Browse the repository at this point in the history
  • Loading branch information
steverydz authored Aug 15, 2024
1 parent cb46778 commit 06c382c
Show file tree
Hide file tree
Showing 6 changed files with 324 additions and 2 deletions.
Original file line number Diff line number Diff line change
@@ -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();
});
});
3 changes: 3 additions & 0 deletions static/js/validation-sets/hooks/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import useValidationSets from "./useValidationSets";

export { useValidationSets };
24 changes: 24 additions & 0 deletions static/js/validation-sets/hooks/useValidationSets.ts
Original file line number Diff line number Diff line change
@@ -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;
9 changes: 8 additions & 1 deletion static/js/validation-sets/index.tsx
Original file line number Diff line number Diff line change
@@ -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";
Expand All @@ -25,4 +26,10 @@ const router = createBrowserRouter([
const rootEl = document.getElementById("root") as HTMLElement;
const root = createRoot(rootEl);

root.render(<RouterProvider router={router} />);
const queryClient = new QueryClient();

root.render(
<QueryClientProvider client={queryClient}>
<RouterProvider router={router} />
</QueryClientProvider>
);
168 changes: 167 additions & 1 deletion static/js/validation-sets/pages/ValidationSets/ValidationSets.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<>
<h1>Validation sets page</h1>
<h1 className="p-heading--4">My validation sets</h1>

<Row>
<Col size={6}>
<div className="p-search-box">
<label className="u-off-screen" htmlFor="search">
Search validation sets
</label>
<input
required
type="search"
id="search"
name="search"
className="p-search-box__input"
placeholder="Search validation sets"
autoComplete="off"
value={searchParams.get("filter") || ""}
onChange={(e) => {
if (e.target.value) {
setSearchParams({ filter: e.target.value });
} else {
setSearchParams();
}
}}
/>
<Button
type="reset"
className="p-search-box__reset"
onClick={() => {
setSearchParams();
}}
>
<Icon name="close">Clear filter</Icon>
</Button>
<Button type="submit" className="p-search-box__button">
<Icon name="search">Search</Icon>
</Button>
</div>
</Col>
</Row>

{status === "loading" && (
<p>
<Icon name="spinner" className="u-animation--spin" />
&nbsp;&nbsp;Fetching validation sets
</p>
)}

{status === "success" && filteredValidationSets.length === 0 && (
<Notification severity="information">
There are no validation sets to display
</Notification>
)}

{status === "error" && (
<Notification severity="negative">
Unable to load validation sets
</Notification>
)}

{status === "success" && filteredValidationSets.length > 0 && (
<MainTable
sortable
headers={[
{
content: `Name (${filteredValidationSets.length})`,
sortKey: "name",
},
{
content: "Revision",
className: "u-align--right",
sortKey: "revision",
},
{
content: "Sequence",
className: "u-align--right",
sortKey: "sequence",
},
{
content: "Referenced snaps",
className: "u-align--right",
sortKey: "snaps",
},
{
content: "Last updated",
className: "u-align--right",
sortKey: "updated",
},
]}
rows={filteredValidationSets.map((validationSet: ValidationSet) => {
return {
columns: [
{
content: (
<Link to={`/validation-sets/${validationSet.name}`}>
{validationSet.name}
</Link>
),
},
{
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,
},
};
})}
/>
)}
</>
);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -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(
<QueryClientProvider client={queryClient}>
<BrowserRouter>
<ValidationSets />
</BrowserRouter>
</QueryClientProvider>
);
};

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();
});
});

0 comments on commit 06c382c

Please sign in to comment.