Skip to content

Commit

Permalink
feat: Create validation set table (#4806)
Browse files Browse the repository at this point in the history
  • Loading branch information
steverydz authored Aug 16, 2024
1 parent 06c382c commit 4a12977
Show file tree
Hide file tree
Showing 10 changed files with 372 additions and 16 deletions.
4 changes: 2 additions & 2 deletions requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,9 @@ canonicalwebteam.discourse==5.6.1
canonicalwebteam.blog==6.4.2
canonicalwebteam.search==1.3.0
canonicalwebteam.image-template==1.3.1
canonicalwebteam.store-api==4.14.0
canonicalwebteam.store-api==5.0.0
canonicalwebteam.launchpad==0.9.0
canonicalwebteam.store-base==0.5.0
canonicalwebteam.store-base==1.3.0
django-openid-auth==0.17
Flask-OpenID==1.3.0
Flask-WTF==1.2.1
Expand Down
12 changes: 12 additions & 0 deletions static/js/validation-sets/hooks/__tests__/useValidationSet.test.ts
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 useValidationSet from "../useValidationSet";

describe("useValidationSet", () => {
test("Calls useQuery", () => {
jest.spyOn(ReactQuery, "useQuery").mockImplementation(jest.fn());
renderHook(() => useValidationSet("test-id"));
expect(ReactQuery.useQuery).toHaveBeenCalled();
});
});
3 changes: 2 additions & 1 deletion static/js/validation-sets/hooks/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import useValidationSets from "./useValidationSets";
import useValidationSet from "./useValidationSet";

export { useValidationSets };
export { useValidationSets, useValidationSet };
24 changes: 24 additions & 0 deletions static/js/validation-sets/hooks/useValidationSet.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { useQuery } from "react-query";

function useValidationSet(validationSetId: string | undefined) {
return useQuery({
queryKey: ["validationSet"],
queryFn: async () => {
const response = await fetch(`/api/validation-sets/${validationSetId}`);

if (!response.ok) {
throw new Error("Unable to fetch validation set");
}

const validationSetData = await response.json();

if (!validationSetData.success) {
throw new Error(validationSetData.message);
}

return validationSetData.data;
},
});
}

export default useValidationSet;
161 changes: 160 additions & 1 deletion static/js/validation-sets/pages/ValidationSet/ValidationSet.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,166 @@
import { useParams, useSearchParams, Link } from "react-router-dom";
import {
Icon,
Notification,
MainTable,
Row,
Col,
} from "@canonical/react-components";

import { useValidationSet } from "../../hooks";

import type { ValidationSet, Snap } from "../../types";

function ValidationSet(): JSX.Element {
const { validationSetId } = useParams();
const [searchParams, setSearchParams] = useSearchParams();
const { status, data: validationSetSequences } =
useValidationSet(validationSetId);

const getSelectedSquence = () => {
const sequenceQuery = searchParams.get("sequence");

if (sequenceQuery) {
return parseInt(sequenceQuery) - 1;
}

return validationSetSequences.length - 1;
};

return (
<>
<h1>Validation set page</h1>
<h1 className="p-heading--4">
<Link to="/validation-sets">My validation sets</Link> /{" "}
{validationSetId}
</h1>

{status === "success" && validationSetSequences.length > 0 && (
<Row>
<Col size={3}>
<div style={{ display: "flex" }}>
<label
htmlFor="sequence-selector"
style={{ display: "inline-block", marginRight: "1rem" }}
>
Sequence
</label>
<select
name="sequence-selector"
id="sequence-selector"
style={{ minWidth: "64px", width: "64px" }}
defaultValue={
searchParams.get("sequence") || validationSetSequences.length
}
onChange={(e) => {
setSearchParams({ sequence: e.target.value });
}}
>
{validationSetSequences.map(
(validateSetSequence: ValidationSet) => (
<option
key={validateSetSequence.timestamp}
value={validateSetSequence.sequence}
>
{validateSetSequence.sequence}
</option>
)
)}
</select>
</div>
</Col>
</Row>
)}

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

{status === "success" && validationSetSequences.length === 0 && (
<Notification severity="information">
There are no snaps in this validation set to display
</Notification>
)}

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

{status === "success" && validationSetSequences.length > 0 && (
<MainTable
sortable
headers={[
{
content: "Snap name",
sortKey: "name",
},
{
content: "ID",
sortKey: "id",
className: "u-align--right",
},
{
content: "Revision",
sortKey: "revision",
className: "u-align--right",
},
{
content: "Presence",
sortKey: "presence",
},
{
content: "Publisher",
sortKey: "publisher",
},
{
content: "Release date",
sortKey: "releaseDate",
className: "u-align--right",
},
]}
rows={validationSetSequences[getSelectedSquence()].snaps.map(
(snap: Snap) => {
return {
columns: [
{
content: <a href={`/${snap.name}`}>{snap.name}</a>,
},
{
content: snap.id,
className: "u-align--right u-truncate",
},
{
content: snap.revision || "-",
className: "u-align--right",
},
{
content: snap.presence || "-",
},
{
content: "-",
},
{
content: "-",
className: "u-align--right",
},
],
sortData: {
name: "",
id: "",
revision: "",
presence: "",
publisher: "",
releaseDate: "",
},
};
}
)}
/>
)}
</>
);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
import { BrowserRouter, useSearchParams } from "react-router-dom";
import { QueryClient, QueryClientProvider, useQuery } from "react-query";
import { render, screen, fireEvent, waitFor } from "@testing-library/react";
import "@testing-library/jest-dom";

import ValidationSet from "../ValidationSet";

const queryClient = new QueryClient();

const renderComponent = () => {
return render(
<QueryClientProvider client={queryClient}>
<BrowserRouter>
<ValidationSet />
</BrowserRouter>
</QueryClientProvider>
);
};

const mockValidationSet = [
{
name: "validation-set-1",
revision: 1,
sequence: 1,
snaps: [
{
id: "test-snap-1-id",
name: "test-snap-1",
},
{
id: "test-snap-2-id",
name: "only-in-sequence-1",
},
],
timestamp: "2024-08-13T09:49:18Z",
},
{
name: "validation-set-1",
revision: 1,
sequence: 2,
snaps: [
{
id: "test-snap-1-id",
name: "test-snap-1",
},
{
id: "test-snap-2-id",
name: "only-in-sequence-2",
},
],
timestamp: "2024-08-14T09:49:18Z",
},
{
name: "validation-set-1",
revision: 1,
sequence: 3,
snaps: [
{
id: "test-snap-1-id",
name: "test-snap-1",
},
{
id: "test-snap-2-id",
name: "test-snap-2",
},
{
id: "test-snap-3-id",
name: "only-in-sequence-3",
},
],
timestamp: "2024-08-15T09: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("ValidationSet", () => {
test("shows loading state when fetching validation set", () => {
// @ts-ignore
useQuery.mockReturnValue({ status: "loading", data: undefined });
// @ts-ignore
useSearchParams.mockReturnValue([new URLSearchParams()]);
renderComponent();
expect(
screen.getByText(/Fetching validation set snaps/)
).toBeInTheDocument();
});

test("shows message if no validation set", () => {
// @ts-ignore
useQuery.mockReturnValue({ status: "success", data: [] });
// @ts-ignore
useSearchParams.mockReturnValue([new URLSearchParams()]);
renderComponent();
expect(
screen.getByText(/There are no snaps in this validation set to display/)
).toBeInTheDocument();
});

test("shows message when there is an error fetching validation set", () => {
// @ts-ignore
useQuery.mockReturnValue({ status: "error", data: undefined });
// @ts-ignore
useSearchParams.mockReturnValue([new URLSearchParams()]);
renderComponent();
expect(
screen.getByText(/Unable to load validation set snaps/)
).toBeInTheDocument();
});

test("displays validation set snaps", () => {
// @ts-ignore
useQuery.mockReturnValue({ status: "success", data: mockValidationSet });
// @ts-ignore
useSearchParams.mockReturnValue([new URLSearchParams()]);
renderComponent();
expect(
screen.getByText(mockValidationSet[0].snaps[0].name)
).toBeInTheDocument();
});

test("sequence selector defaults to latest sequence", () => {
// @ts-ignore
useQuery.mockReturnValue({ status: "success", data: mockValidationSet });
// @ts-ignore
useSearchParams.mockReturnValue([new URLSearchParams()]);
renderComponent();
expect(screen.getByLabelText("Sequence")).toHaveValue("3");
expect(screen.getByText(/only-in-sequence-3/)).toBeInTheDocument();
});

test("sequence selector uses query string value", () => {
// @ts-ignore
useQuery.mockReturnValue({ status: "success", data: mockValidationSet });
// @ts-ignore
useSearchParams.mockReturnValue([new URLSearchParams({ sequence: "2" })]);
renderComponent();
expect(screen.getByLabelText("Sequence")).toHaveValue("2");
expect(screen.getByText(/only-in-sequence-2/)).toBeInTheDocument();
});
});
3 changes: 2 additions & 1 deletion static/js/validation-sets/pages/ValidationSet/index.ts
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
export { default } from "./ValidationSet";
import ValidationSet from "./ValidationSet";
export default ValidationSet;
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,7 @@ import {

import { useValidationSets } from "../../hooks";

type ValidationSet = {
name: string;
revision?: number;
sequence?: number;
snaps: { id: string; name: string }[];
timestamp: string;
};
import type { ValidationSet } from "../../types";

function ValidationSets(): JSX.Element {
const { status, data: validationSets } = useValidationSets();
Expand Down
Loading

0 comments on commit 4a12977

Please sign in to comment.