sts-sponsors team mailing list archive
-
sts-sponsors team
-
Mailing list archive
-
Message #05438
[Merge] maas-site-manager:feat-table-component-MAASENG-1387 into maas-site-manager:main
Peter Makowski has proposed merging maas-site-manager:feat-table-component-MAASENG-1387 into maas-site-manager:main.
Requested reviews:
MAAS Committers (maas-committers)
For more details, see:
https://code.launchpad.net/~maas-committers/maas-site-manager/+git/site-manager/+merge/437995
--
Your team MAAS Committers is requested to review the proposed merge of maas-site-manager:feat-table-component-MAASENG-1387 into maas-site-manager:main.
diff --git a/.env b/.env
index 22cb9fd..797df3b 100644
--- a/.env
+++ b/.env
@@ -1 +1,2 @@
-UI_PORT=8405
\ No newline at end of file
+VITE_UI_PORT=8405
+VITE_API_URL=http://localhost:8000
diff --git a/frontend/package.json b/frontend/package.json
index cfa7539..c70b8d3 100644
--- a/frontend/package.json
+++ b/frontend/package.json
@@ -11,6 +11,12 @@
"preview": "vite preview"
},
"dependencies": {
+ "@canonical/react-components": "0.38.0",
+ "@tanstack/react-table": "8.7.9",
+ "axios": "1.3.4",
+ "date-fns": "2.29.3",
+ "date-fns-tz": "2.0.0",
+ "lodash": "4.17.21",
"react": "18.2.0",
"react-dom": "18.2.0",
"react-query": "3.39.3",
@@ -18,15 +24,19 @@
"vanilla-framework": "3.11.0"
},
"devDependencies": {
- "@playwright/test": "^1.31.1",
+ "@playwright/test": "1.31.1",
"@testing-library/jest-dom": "5.16.5",
"@testing-library/react": "13.4.0",
+ "@types/axios": "0.14.0",
+ "@types/lodash": "4.14.191",
"@types/react": "18.0.27",
"@types/react-dom": "18.0.10",
"@vitejs/plugin-react-swc": "3.0.0",
- "dotenv": "^16.0.3",
+ "dotenv": "16.0.3",
+ "mockdate": "3.0.5",
"msw": "1.0.1",
"sass": "1.58.1",
+ "timezone-mock": "1.3.6",
"typescript": "4.9.3",
"vite": "4.1.0",
"vitest": "0.28.5"
diff --git a/frontend/setupTests.js b/frontend/setupTests.ts
similarity index 90%
rename from frontend/setupTests.js
rename to frontend/setupTests.ts
index 45657de..d025d9d 100644
--- a/frontend/setupTests.js
+++ b/frontend/setupTests.ts
@@ -1,7 +1,10 @@
+import dotenv from "dotenv";
import { expect, afterEach } from "vitest";
import { cleanup } from "@testing-library/react";
import matchers from "@testing-library/jest-dom/matchers";
+dotenv.config({ path: "../.env" });
+
// extends Vitest's expect method with methods from react-testing-library
expect.extend(matchers);
diff --git a/frontend/src/App.scss b/frontend/src/App.scss
index 6049fbf..9a5b6a7 100644
--- a/frontend/src/App.scss
+++ b/frontend/src/App.scss
@@ -1,5 +1,6 @@
@import "node_modules/vanilla-framework";
@include vf-base;
+@include vanilla;
// @include vf-p-table-sortable;
// @include vf-p-table-expanding;
@include vf-p-grid;
diff --git a/frontend/src/api/api.ts b/frontend/src/api/api.ts
new file mode 100644
index 0000000..9b40510
--- /dev/null
+++ b/frontend/src/api/api.ts
@@ -0,0 +1,7 @@
+import axios from "axios";
+
+const api = axios.create({
+ baseURL: import.meta.env.VITE_API_URL,
+});
+
+export default api;
diff --git a/frontend/src/api/handlers.ts b/frontend/src/api/handlers.ts
new file mode 100644
index 0000000..8877ca6
--- /dev/null
+++ b/frontend/src/api/handlers.ts
@@ -0,0 +1,11 @@
+import api from "./api";
+import urls from "./urls";
+
+export const getSites = async () => {
+ try {
+ const response = await api.get(urls.sites);
+ return response.data;
+ } catch (error) {
+ console.error(error);
+ }
+};
diff --git a/frontend/src/api/index.ts b/frontend/src/api/index.ts
new file mode 100644
index 0000000..9e19686
--- /dev/null
+++ b/frontend/src/api/index.ts
@@ -0,0 +1 @@
+export { default } from "./api";
diff --git a/frontend/src/api/types.ts b/frontend/src/api/types.ts
new file mode 100644
index 0000000..c8495ac
--- /dev/null
+++ b/frontend/src/api/types.ts
@@ -0,0 +1,26 @@
+export type Site = {
+ name: string;
+ url: string; // <full URL including protocol>,
+ connection: "stable" | "stale" | "lost";
+ last_seen: string; // <ISO 8601 date>,
+ address: {
+ countrycode: string; // <alpha2 country code>,
+ city: string;
+ zip: string;
+ street: string;
+ };
+ timezone: string; // <three letter abbreviation>,
+ stats: {
+ machines: number;
+ occupied_machines: number;
+ ready_machines: number;
+ error_machines: number;
+ };
+};
+
+export type Sites = {
+ items: Site[];
+ total: number;
+ page: number;
+ size: number;
+};
diff --git a/frontend/src/api/urls.ts b/frontend/src/api/urls.ts
new file mode 100644
index 0000000..0a3217a
--- /dev/null
+++ b/frontend/src/api/urls.ts
@@ -0,0 +1,7 @@
+import { getApiUrl } from "./utils";
+
+const urls = {
+ sites: getApiUrl("/api/sites"),
+};
+
+export default urls;
diff --git a/frontend/src/api/utils.ts b/frontend/src/api/utils.ts
new file mode 100644
index 0000000..7386f0f
--- /dev/null
+++ b/frontend/src/api/utils.ts
@@ -0,0 +1,3 @@
+export const getApiUrl = (path: string) => {
+ return new URL(path, import.meta.env.VITE_API_URL).toString();
+};
diff --git a/frontend/src/components/MainLayout/MainLayout.scss b/frontend/src/components/MainLayout/MainLayout.scss
new file mode 100644
index 0000000..12ae5c8
--- /dev/null
+++ b/frontend/src/components/MainLayout/MainLayout.scss
@@ -0,0 +1,3 @@
+.l-main.is-maas-site-manager {
+ margin-top: 1.5rem;
+}
diff --git a/frontend/src/components/MainLayout/MainLayout.tsx b/frontend/src/components/MainLayout/MainLayout.tsx
index 9b55505..8218511 100644
--- a/frontend/src/components/MainLayout/MainLayout.tsx
+++ b/frontend/src/components/MainLayout/MainLayout.tsx
@@ -1,11 +1,12 @@
import { Outlet } from "react-router-dom";
+import "./MainLayout.scss";
const MainLayout = () => (
<div className="l-application">
- <main className="l-main">
+ <main className="l-main is-maas-site-manager">
<div className="row">
<div className="col-12">
- <h1>MAAS Site Manager</h1>
+ <h1 className="u-hide">MAAS Site Manager</h1>
<Outlet />
</div>
</div>
diff --git a/frontend/src/components/SitesList/SitesList.scss b/frontend/src/components/SitesList/SitesList.scss
deleted file mode 100644
index cad2f95..0000000
--- a/frontend/src/components/SitesList/SitesList.scss
+++ /dev/null
@@ -1,3 +0,0 @@
-table {
- table-layout: auto;
-}
diff --git a/frontend/src/components/SitesList/SitesList.test.tsx b/frontend/src/components/SitesList/SitesList.test.tsx
index e23e81c..75cf9c5 100644
--- a/frontend/src/components/SitesList/SitesList.test.tsx
+++ b/frontend/src/components/SitesList/SitesList.test.tsx
@@ -1,5 +1,21 @@
import SitesList from "./SitesList";
-import { render, screen } from "../../test-utils";
+import { render, screen, waitFor, within } from "../../test-utils";
+import { createMockGetServer } from "../../mocks/server";
+import { sites } from "../../mocks/factories";
+import urls from "../../api/urls";
+
+const sitesData = sites();
+const mockServer = createMockGetServer(urls.sites, sitesData);
+
+beforeAll(() => {
+ mockServer.listen();
+});
+afterEach(() => {
+ mockServer.resetHandlers();
+});
+afterAll(() => {
+ mockServer.close();
+});
it("renders header", () => {
render(<SitesList />);
@@ -8,3 +24,30 @@ it("renders header", () => {
screen.getByRole("heading", { name: /MAAS Regions/i })
).toBeInTheDocument();
});
+
+it("displays loading text", () => {
+ render(<SitesList />);
+
+ expect(screen.getByText(/loading/i)).toBeInTheDocument();
+});
+
+it("displays populated sites table", async () => {
+ const { items } = sitesData;
+ render(<SitesList />);
+
+ await waitFor(() =>
+ expect(screen.getByRole("table", { name: /sites/i })).toBeInTheDocument()
+ );
+
+ expect(screen.getAllByRole("rowgroup")).toHaveLength(2);
+ expect(
+ screen.getByRole("heading", { name: /2 MAAS Regions/i })
+ ).toBeInTheDocument();
+ const tableBody = screen.getAllByRole("rowgroup")[1];
+ expect(within(tableBody).getAllByRole("row")).toHaveLength(items.length);
+ within(tableBody)
+ .getAllByRole("row")
+ .forEach((row, i) =>
+ expect(row).toHaveTextContent(new RegExp(items[i].name, "i"))
+ );
+});
diff --git a/frontend/src/components/SitesList/SitesList.tsx b/frontend/src/components/SitesList/SitesList.tsx
index 9a49bd0..571a440 100644
--- a/frontend/src/components/SitesList/SitesList.tsx
+++ b/frontend/src/components/SitesList/SitesList.tsx
@@ -1,54 +1,18 @@
-import { useQuery } from "react-query";
-import SiteRow from "./components/SiteRow";
-import "./SitesList.scss";
-import { Sites } from "./types";
+import { useSitesQuery } from "../../hooks/api";
+import SitesTable from "./components/SitesTable";
const SitesList = () => {
- const query = useQuery<Sites>("/api/sites", async function () {
- const response = await fetch("/api/sites");
- if (!response.ok) {
- throw new Error("Network response was not ok");
- }
- const responseJson = await response.json();
- return responseJson;
- });
+ const query = useSitesQuery();
return (
<div>
- <h2>{query?.data?.items?.length || ""} MAAS Regions</h2>
- <table>
- <thead>
- <tr>
- <th>
- <div>MAAS region alias</div>
- <div>URL</div>
- </th>
- <th>
- <div>connection</div>
- <div>last seen</div>
- </th>
- <th>
- <div>country</div>
- <div>street, city, zip</div>
- </th>
- <th>
- <div>local time</div>
- <div>timezone</div>
- </th>
- <th>
- <div>total number of machines</div>
- <div>machines per aggregated status</div>
- </th>
- </tr>
- </thead>
- <tbody>
- {query.data
- ? query.data.items.map((site) => (
- <SiteRow key={site.url} site={site} />
- ))
- : null}
- </tbody>
- </table>
+ <h2 className="p-heading--4">
+ {query?.data?.items?.length || ""} MAAS Regions
+ </h2>
+ {query.isLoading && !query.isFetchedAfterMount ? "Loading..." : null}
+ {query.isFetchedAfterMount && query.data ? (
+ <SitesTable data={query.data.items} />
+ ) : null}
</div>
);
};
diff --git a/frontend/src/components/SitesList/components/SiteRow.tsx b/frontend/src/components/SitesList/components/SiteRow.tsx
deleted file mode 100644
index 22ed089..0000000
--- a/frontend/src/components/SitesList/components/SiteRow.tsx
+++ /dev/null
@@ -1,37 +0,0 @@
-import { Site } from "../types";
-
-const SiteRow = ({ site }: { site: Site }) => {
- return (
- <tr>
- <td>
- <div>{site.name}</div>
- <div>
- <a href={site.url}>{site.url}</a>
- </div>
- </td>
- <td>
- <div>{site.connection}</div>
- <div>{site.last_seen}</div>
- </td>
- <td>
- <div>{site.address.countrycode}</div>
- <div>
- {site.address.street}, {site.address.city}, {site.address.zip}
- </div>
- </td>
- <td>
- <div>11:00 (local time)</div>
- <div>{site.timezone}</div>
- </td>
- <td>
- <div>{site.stats.machines}</div>
- <div>
- Ready: {site.stats.ready_machines}, Occupied:
- {site.stats.occupied_machines}, Error: {site.stats.error_machines}
- </div>
- </td>
- </tr>
- );
-};
-
-export default SiteRow;
diff --git a/frontend/src/components/SitesList/components/SitesTable.test.tsx b/frontend/src/components/SitesList/components/SitesTable.test.tsx
new file mode 100644
index 0000000..4fd2a23
--- /dev/null
+++ b/frontend/src/components/SitesList/components/SitesTable.test.tsx
@@ -0,0 +1,49 @@
+import SitesTable from "./SitesTable";
+import { render, screen, within } from "../../../test-utils";
+import { sites, site } from "../../../mocks/factories";
+import { Site } from "../../../api/types";
+import MockDate from "mockdate";
+import timezoneMock from "timezone-mock";
+import { vi } from "vitest";
+
+beforeEach(() => {
+ vi.useFakeTimers();
+ timezoneMock.register("Etc/GMT");
+});
+
+afterEach(() => {
+ timezoneMock.unregister();
+ vi.useRealTimers();
+});
+
+it("displays an empty sites table", () => {
+ render(<SitesTable data={[]} />);
+
+ expect(screen.getByRole("table", { name: /sites/i })).toBeInTheDocument();
+});
+
+it("displays rows with details for each site", () => {
+ const items = sites().items as Site[];
+ render(<SitesTable data={items} />);
+
+ expect(screen.getByRole("table", { name: /sites/i })).toBeInTheDocument();
+
+ const tableBody = screen.getAllByRole("rowgroup")[1];
+ expect(within(tableBody).getAllByRole("row")).toHaveLength(items.length);
+ within(tableBody)
+ .getAllByRole("row")
+ .forEach((row, i) =>
+ expect(row).toHaveTextContent(new RegExp(items[i].name, "i"))
+ );
+});
+
+it("displays correct local time", () => {
+ const date = new Date("2000-01-01T12:00:00Z");
+ vi.setSystemTime(date);
+
+ const item = site({ timezone: "CET" });
+ render(<SitesTable data={[item]} />);
+
+ expect(screen.getByRole("table", { name: /sites/i })).toBeInTheDocument();
+ expect(screen.getByText(/13:00 \(local time\)/i)).toBeInTheDocument();
+});
diff --git a/frontend/src/components/SitesList/components/SitesTable.tsx b/frontend/src/components/SitesList/components/SitesTable.tsx
new file mode 100644
index 0000000..b6137e6
--- /dev/null
+++ b/frontend/src/components/SitesList/components/SitesTable.tsx
@@ -0,0 +1,232 @@
+import {
+ useReactTable,
+ flexRender,
+ ColumnDef,
+ getCoreRowModel,
+ getFilteredRowModel,
+ getPaginationRowModel,
+} from "@tanstack/react-table";
+import { Input } from "@canonical/react-components";
+import { useMemo, useState } from "react";
+import pick from "lodash/fp/pick";
+import { format } from "date-fns";
+import { utcToZonedTime } from "date-fns-tz";
+import { Site } from "../../../api/types";
+
+const createAccessor =
+ <T, K extends keyof T>(keys: K[] | K) =>
+ (row: T) =>
+ pick(keys, row);
+
+const SitesTable = ({ data }: { data: Site[] }) => {
+ const [columnVisibility, setColumnVisibility] = useState({});
+
+ const columns = useMemo<ColumnDef<Site, Partial<Site>>[]>(
+ () => [
+ {
+ id: "select",
+ header: ({ table }) => (
+ <div>
+ <Input
+ type="checkbox"
+ {...{
+ checked: table.getIsAllRowsSelected(),
+ indeterminate: table.getIsSomeRowsSelected(),
+ onChange: table.getToggleAllRowsSelectedHandler(),
+ }}
+ />
+ </div>
+ ),
+ cell: ({ row }) => (
+ <div>
+ <Input
+ type="checkbox"
+ {...{
+ checked: row.getIsSelected(),
+ disabled: !row.getCanSelect(),
+ indeterminate: row.getIsSomeSelected(),
+ onChange: row.getToggleSelectedHandler(),
+ }}
+ />
+ </div>
+ ),
+ },
+ {
+ id: "name",
+ accessorFn: createAccessor(["name", "url"]),
+ header: () => (
+ <>
+ <div>Name</div>
+ <div className="u-text--muted">URL</div>
+ </>
+ ),
+ cell: ({ getValue }) => (
+ <>
+ <div>{getValue().name}</div>
+ <div className="u-text--muted">{getValue().url}</div>
+ </>
+ ),
+ },
+ {
+ id: "connection",
+ accessorFn: createAccessor(["connection", "last_seen"]),
+ header: () => (
+ <>
+ <div>connection</div>
+ <div className="u-text--muted">last seen</div>
+ </>
+ ),
+ cell: ({ getValue }) => (
+ <>
+ <div>{getValue().connection}</div>
+ <div className="u-text--muted">{getValue().last_seen}</div>
+ </>
+ ),
+ },
+ {
+ id: "address",
+ accessorFn: createAccessor("address"),
+ header: () => (
+ <>
+ <div>country</div>
+ <div className="u-text--muted">street, city, ZIP</div>
+ </>
+ ),
+ cell: ({ getValue }) => {
+ const { address } = getValue();
+ const { countrycode, city, zip, street } = address || {};
+ return (
+ <>
+ <div>{countrycode}</div>
+ <div className="u-text--muted">
+ {street}, {city}, {zip}
+ </div>
+ </>
+ );
+ },
+ },
+ {
+ id: "time",
+ accessorFn: createAccessor("timezone"),
+ header: () => (
+ <>
+ <div>local time</div>
+ <div className="u-text--muted">timezone</div>
+ </>
+ ),
+ cell: ({ getValue }) => {
+ const { timezone } = getValue();
+ return timezone ? (
+ <>
+ <div>
+ {format(utcToZonedTime(new Date(), timezone), "HH:mm")} (local
+ time)
+ </div>
+ <div className="u-text--muted">{timezone}</div>
+ </>
+ ) : null;
+ },
+ },
+ {
+ id: "status",
+ accessorFn: createAccessor("stats"),
+ header: () => (
+ <>
+ <div>machines</div>
+ <div className="u-text--muted">aggregated status</div>
+ </>
+ ),
+ cell: ({ getValue }) => {
+ const { stats } = getValue();
+ const {
+ machines,
+ ready_machines,
+ occupied_machines,
+ error_machines,
+ } = stats || {};
+ return (
+ <>
+ <div>{machines}</div>
+ <div className="u-text--muted">
+ Ready: {ready_machines}, Occupied: {occupied_machines}, Error:{" "}
+ {error_machines}
+ </div>
+ </>
+ );
+ },
+ },
+ ],
+ []
+ );
+
+ const [rowSelection, setRowSelection] = useState({});
+
+ const table = useReactTable<Site>({
+ data: data || [],
+ columns,
+ state: {
+ rowSelection,
+ columnVisibility,
+ },
+ onColumnVisibilityChange: setColumnVisibility,
+ enableRowSelection: true,
+ onRowSelectionChange: setRowSelection,
+ enableColumnResizing: false,
+ columnResizeMode: "onChange",
+ getCoreRowModel: getCoreRowModel(),
+ getFilteredRowModel: getFilteredRowModel(),
+ getPaginationRowModel: getPaginationRowModel(),
+ debugTable: true,
+ debugHeaders: true,
+ debugColumns: true,
+ });
+
+ return (
+ <table className="u-table-layout--auto" aria-label="sites">
+ <thead>
+ {table.getHeaderGroups().map((headerGroup) => (
+ <tr key={headerGroup.id}>
+ {headerGroup.headers.map((header) => {
+ return (
+ <th key={header.id} colSpan={header.colSpan}>
+ {header.isPlaceholder
+ ? null
+ : flexRender(
+ header.column.columnDef.header,
+ header.getContext()
+ )}
+ {header.column.getCanResize() && (
+ <div
+ onMouseDown={header.getResizeHandler()}
+ onTouchStart={header.getResizeHandler()}
+ className={`resizer ${
+ header.column.getIsResizing() ? "isResizing" : ""
+ }`}
+ ></div>
+ )}
+ </th>
+ );
+ })}
+ </tr>
+ ))}
+ </thead>
+ <tbody>
+ {table.getRowModel().rows.map((row) => {
+ return (
+ <tr key={row.id}>
+ {row.getVisibleCells().map((cell) => {
+ return (
+ <td key={cell.id}>
+ {flexRender(cell.column.columnDef.cell, cell.getContext())}
+ </td>
+ );
+ })}
+ </tr>
+ );
+ })}
+ </tbody>
+ </table>
+ );
+};
+
+export default SitesTable;
diff --git a/frontend/src/components/SitesList/types.ts b/frontend/src/components/SitesList/types.ts
index c8495ac..e69de29 100644
--- a/frontend/src/components/SitesList/types.ts
+++ b/frontend/src/components/SitesList/types.ts
@@ -1,26 +0,0 @@
-export type Site = {
- name: string;
- url: string; // <full URL including protocol>,
- connection: "stable" | "stale" | "lost";
- last_seen: string; // <ISO 8601 date>,
- address: {
- countrycode: string; // <alpha2 country code>,
- city: string;
- zip: string;
- street: string;
- };
- timezone: string; // <three letter abbreviation>,
- stats: {
- machines: number;
- occupied_machines: number;
- ready_machines: number;
- error_machines: number;
- };
-};
-
-export type Sites = {
- items: Site[];
- total: number;
- page: number;
- size: number;
-};
diff --git a/frontend/src/hooks/api.test.ts b/frontend/src/hooks/api.test.ts
new file mode 100644
index 0000000..46390b6
--- /dev/null
+++ b/frontend/src/hooks/api.test.ts
@@ -0,0 +1,27 @@
+import { renderHook, waitFor } from "@testing-library/react";
+import urls from "../api/urls";
+import { sites } from "../mocks/factories";
+import { createMockGetServer } from "../mocks/server";
+import { Providers } from "../test-utils";
+import { useSitesQuery } from "./api";
+
+const sitesData = sites();
+const mockServer = createMockGetServer(urls.sites, sitesData);
+
+beforeAll(() => {
+ mockServer.listen();
+});
+afterEach(() => {
+ mockServer.resetHandlers();
+});
+afterAll(() => {
+ mockServer.close();
+});
+
+it("should return sites", async () => {
+ const { result } = renderHook(() => useSitesQuery(), { wrapper: Providers });
+
+ await waitFor(() => expect(result.current.isFetchedAfterMount).toBe(true));
+
+ expect(result.current.data!.items).toEqual(sitesData.items);
+});
diff --git a/frontend/src/hooks/api.ts b/frontend/src/hooks/api.ts
new file mode 100644
index 0000000..e70f848
--- /dev/null
+++ b/frontend/src/hooks/api.ts
@@ -0,0 +1,5 @@
+import { useQuery } from "react-query";
+import { getSites } from "../api/handlers";
+import { Sites } from "../api/types";
+
+export const useSitesQuery = () => useQuery<Sites>("/api/sites", getSites);
diff --git a/frontend/src/main.tsx b/frontend/src/main.tsx
index a8ae664..5d83917 100644
--- a/frontend/src/main.tsx
+++ b/frontend/src/main.tsx
@@ -3,7 +3,8 @@ import ReactDOM from "react-dom/client";
import App from "./App";
if (process.env.NODE_ENV === "development") {
- import("./mocks/browser");
+ const { worker } = await import("./mocks/browser");
+ worker.start();
}
ReactDOM.createRoot(document.getElementById("root") as HTMLElement).render(
diff --git a/frontend/src/mocks/browser.ts b/frontend/src/mocks/browser.ts
index 0f20911..34d0b39 100644
--- a/frontend/src/mocks/browser.ts
+++ b/frontend/src/mocks/browser.ts
@@ -1,11 +1,9 @@
-// src/mocks/browser.js
+import urls from "../api/urls";
import { setupWorker, rest } from "msw";
import { sites } from "./factories";
-const worker = setupWorker(
- rest.get("/api/sites", (_req, res, ctx) => {
- return res(ctx.json(sites));
+export const worker = setupWorker(
+ rest.get(urls.sites, (_req, res, ctx) => {
+ return res(ctx.json(sites()));
})
);
-
-worker.start();
diff --git a/frontend/src/mocks/factories.ts b/frontend/src/mocks/factories.ts
index 2e647e2..67bbc0b 100644
--- a/frontend/src/mocks/factories.ts
+++ b/frontend/src/mocks/factories.ts
@@ -1,4 +1,6 @@
-export const site = (site = {}) => ({
+import { Site } from "../api/types";
+
+export const site = (site: Partial<Site> = {}): Site => ({
name: "maas-example-region",
url: "http://maas.example.com",
connection: "stable",
@@ -19,7 +21,7 @@ export const site = (site = {}) => ({
...site,
});
-export const sites = {
+export const sites = (sites = {}) => ({
items: [
site(),
site({
@@ -37,4 +39,5 @@ export const sites = {
total: 42,
page: 1,
size: 20,
-};
+ ...sites,
+});
diff --git a/frontend/src/mocks/server.ts b/frontend/src/mocks/server.ts
new file mode 100644
index 0000000..83178d5
--- /dev/null
+++ b/frontend/src/mocks/server.ts
@@ -0,0 +1,16 @@
+// src/mocks/browser.js
+import { setupServer } from "msw/node";
+import { rest } from "msw";
+import { sites } from "./factories";
+import urls from "../api/urls";
+
+const createMockGetServer = (endpoint: string, response: object) =>
+ setupServer(
+ rest.get(endpoint, (_req, res, ctx) => {
+ return res(ctx.json(response));
+ })
+ );
+
+const mockSitesServer = createMockGetServer(urls.sites, sites());
+
+export { createMockGetServer, mockSitesServer };
diff --git a/frontend/src/test-utils.tsx b/frontend/src/test-utils.tsx
index 543ac8a..622c7ae 100644
--- a/frontend/src/test-utils.tsx
+++ b/frontend/src/test-utils.tsx
@@ -25,3 +25,4 @@ const customRender = (
export * from "@testing-library/react";
export { customRender as render };
+export { Providers };
diff --git a/frontend/vite.config.ts b/frontend/vite.config.ts
index f3fdf22..cde0616 100644
--- a/frontend/vite.config.ts
+++ b/frontend/vite.config.ts
@@ -2,10 +2,10 @@ import { defineConfig } from "vite";
import react from "@vitejs/plugin-react-swc";
import dotenv from "dotenv";
-dotenv.config();
+dotenv.config({ path: "../.env" });
// https://vitejs.dev/config/
export default defineConfig({
plugins: [react()],
- server: { port: Number(process.env.UI_PORT) },
+ server: { port: Number(process.env.VITE_UI_PORT) },
});
diff --git a/frontend/vitest.config.ts b/frontend/vitest.config.ts
index 8767b5f..2cd5a2c 100644
--- a/frontend/vitest.config.ts
+++ b/frontend/vitest.config.ts
@@ -1,4 +1,4 @@
-import { defineConfig } from "vitest/config";
+import { defineConfig, configDefaults } from "vitest/config";
import react from "@vitejs/plugin-react-swc";
export default defineConfig({
@@ -6,6 +6,7 @@ export default defineConfig({
test: {
globals: true,
environment: "jsdom",
- setupFiles: "./setupTests.js",
+ setupFiles: ["./setupTests.ts"],
+ exclude: [...configDefaults.exclude, "**/tests/**"],
},
});
diff --git a/frontend/yarn.lock b/frontend/yarn.lock
index 959ae32..18f7148 100644
--- a/frontend/yarn.lock
+++ b/frontend/yarn.lock
@@ -45,6 +45,22 @@
resolved "https://registry.yarnpkg.com/@canonical/latest-news/-/latest-news-1.4.1.tgz#dcdd445ac2268a54cf60f2f8c725b6bdeb285d71"
integrity sha512-lwrikCj0Y11X8Ln8D+elp6Ri3mYjMjsAOJtadNEFzhBrgmg8TVGYJjOdwy8TvRaxy7fHnvVajIKAYWX44Tfj6w==
+"@canonical/react-components@0.38.0":
+ version "0.38.0"
+ resolved "https://registry.yarnpkg.com/@canonical/react-components/-/react-components-0.38.0.tgz#180eb0412d62e29002a724386d08d5bea959b58b"
+ integrity sha512-0t20yrHamxCPCxlJu7ZjXMFexrfZXUSTZvVL+dSoTF8ZHTTi/NUnvgLMak8NWzVht9nOkVb6ltAA8pPSmMr8rg==
+ dependencies:
+ "@types/jest" "27.5.2"
+ "@types/node" "16.11.47"
+ "@types/react" "17.0.48"
+ "@types/react-dom" "17.0.17"
+ "@types/react-table" "7.7.12"
+ classnames "2.3.1"
+ nanoid "3.3.4"
+ prop-types "15.8.1"
+ react-table "7.8.0"
+ react-useportal "1.0.17"
+
"@esbuild/android-arm64@0.16.17":
version "0.16.17"
resolved "https://registry.yarnpkg.com/@esbuild/android-arm64/-/android-arm64-0.16.17.tgz#cf91e86df127aa3d141744edafcba0abdc577d23"
@@ -229,7 +245,7 @@
resolved "https://registry.yarnpkg.com/@open-draft/until/-/until-1.0.3.tgz#db9cc719191a62e7d9200f6e7bab21c5b848adca"
integrity sha512-Aq58f5HiWdyDlFffbbSjAlv596h/cOnt2DO1w3DOC7OJ5EHs0hd/nycJfiu9RJbT6Yk6F1knnRRXNSpxoIVZ9Q==
-"@playwright/test@^1.31.1":
+"@playwright/test@1.31.1":
version "1.31.1"
resolved "https://registry.yarnpkg.com/@playwright/test/-/test-1.31.1.tgz#39d6873dc46af135f12451d79707db7d1357455d"
integrity sha512-IsytVZ+0QLDh1Hj83XatGp/GsI1CDJWbyDaBGbainsh0p2zC7F4toUocqowmjS6sQff2NGT3D9WbDj/3K2CJiA==
@@ -315,6 +331,18 @@
"@swc/core-win32-ia32-msvc" "1.3.35"
"@swc/core-win32-x64-msvc" "1.3.35"
+"@tanstack/react-table@8.7.9":
+ version "8.7.9"
+ resolved "https://registry.yarnpkg.com/@tanstack/react-table/-/react-table-8.7.9.tgz#9efcd168fb0080a7e0bc213b5eac8b55513babf4"
+ integrity sha512-6MbbQn5AupSOkek1+6IYu+1yZNthAKTRZw9tW92Vi6++iRrD1GbI3lKTjJalf8lEEKOqapPzQPE20nywu0PjCA==
+ dependencies:
+ "@tanstack/table-core" "8.7.9"
+
+"@tanstack/table-core@8.7.9":
+ version "8.7.9"
+ resolved "https://registry.yarnpkg.com/@tanstack/table-core/-/table-core-8.7.9.tgz#0e975f8a5079972f1827a569079943d43257c42f"
+ integrity sha512-4RkayPMV1oS2SKDXfQbFoct1w5k+pvGpmX18tCXMofK/VDRdA2hhxfsQlMvsJ4oTX8b0CI4Y3GDKn5T425jBCw==
+
"@testing-library/dom@^8.5.0":
version "8.20.0"
resolved "https://registry.yarnpkg.com/@testing-library/dom/-/dom-8.20.0.tgz#914aa862cef0f5e89b98cc48e3445c4c921010f6"
@@ -358,6 +386,13 @@
resolved "https://registry.yarnpkg.com/@types/aria-query/-/aria-query-5.0.1.tgz#3286741fb8f1e1580ac28784add4c7a1d49bdfbc"
integrity sha512-XTIieEY+gvJ39ChLcB4If5zHtPxt3Syj5rgZR+e1ctpmK8NjPf0zFqsz4JpLJT0xla9GFDKjy8Cpu331nrmE1Q==
+"@types/axios@0.14.0":
+ version "0.14.0"
+ resolved "https://registry.yarnpkg.com/@types/axios/-/axios-0.14.0.tgz#ec2300fbe7d7dddd7eb9d3abf87999964cafce46"
+ integrity sha512-KqQnQbdYE54D7oa/UmYVMZKq7CO4l8DEENzOKc4aBRwxCXSlJXGz83flFx5L7AWrOQnmuN3kVsRdt+GZPPjiVQ==
+ dependencies:
+ axios "*"
+
"@types/chai-subset@^1.3.3":
version "1.3.3"
resolved "https://registry.yarnpkg.com/@types/chai-subset/-/chai-subset-1.3.3.tgz#97893814e92abd2c534de422cb377e0e0bdaac94"
@@ -409,11 +444,24 @@
expect "^29.0.0"
pretty-format "^29.0.0"
+"@types/jest@27.5.2":
+ version "27.5.2"
+ resolved "https://registry.yarnpkg.com/@types/jest/-/jest-27.5.2.tgz#ec49d29d926500ffb9fd22b84262e862049c026c"
+ integrity sha512-mpT8LJJ4CMeeahobofYWIjFo0xonRS/HfxnVEPMPFSQdGUt1uHCnoPT7Zhb+sjDU2wz0oKV0OLUR0WzrHNgfeA==
+ dependencies:
+ jest-matcher-utils "^27.0.0"
+ pretty-format "^27.0.0"
+
"@types/js-levenshtein@^1.1.1":
version "1.1.1"
resolved "https://registry.yarnpkg.com/@types/js-levenshtein/-/js-levenshtein-1.1.1.tgz#ba05426a43f9e4e30b631941e0aa17bf0c890ed5"
integrity sha512-qC4bCqYGy1y/NP7dDVr7KJarn+PbX1nSpwA7JXdu0HxT3QYjO8MJ+cntENtHFVy2dRAyBV23OZ6MxsW1AM1L8g==
+"@types/lodash@4.14.191":
+ version "4.14.191"
+ resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.191.tgz#09511e7f7cba275acd8b419ddac8da9a6a79e2fa"
+ integrity sha512-BdZ5BCCvho3EIXw6wUCXHe7rS53AIDPLE+JzwgT+OsJk53oBfbSmZZ7CX4VaRoN78N+TJpFi9QPlfIVNmJYWxQ==
+
"@types/ms@*":
version "0.7.31"
resolved "https://registry.yarnpkg.com/@types/ms/-/ms-0.7.31.tgz#31b7ca6407128a3d2bbc27fe2d21b345397f6197"
@@ -424,11 +472,23 @@
resolved "https://registry.yarnpkg.com/@types/node/-/node-18.13.0.tgz#0400d1e6ce87e9d3032c19eb6c58205b0d3f7850"
integrity sha512-gC3TazRzGoOnoKAhUx+Q0t8S9Tzs74z7m0ipwGpSqQrleP14hKxP4/JUeEQcD3W1/aIpnWl8pHowI7WokuZpXg==
+"@types/node@16.11.47":
+ version "16.11.47"
+ resolved "https://registry.yarnpkg.com/@types/node/-/node-16.11.47.tgz#efa9e3e0f72e7aa6a138055dace7437a83d9f91c"
+ integrity sha512-fpP+jk2zJ4VW66+wAMFoBJlx1bxmBKx4DUFf68UHgdGCOuyUTDlLWqsaNPJh7xhNDykyJ9eIzAygilP/4WoN8g==
+
"@types/prop-types@*":
version "15.7.5"
resolved "https://registry.yarnpkg.com/@types/prop-types/-/prop-types-15.7.5.tgz#5f19d2b85a98e9558036f6a3cacc8819420f05cf"
integrity sha512-JCB8C6SnDoQf0cNycqd/35A7MjcnK+ZTqE7judS6o7utxUCg6imJg3QK2qzHKszlTjcj2cn+NwMB2i96ubpj7w==
+"@types/react-dom@17.0.17":
+ version "17.0.17"
+ resolved "https://registry.yarnpkg.com/@types/react-dom/-/react-dom-17.0.17.tgz#2e3743277a793a96a99f1bf87614598289da68a1"
+ integrity sha512-VjnqEmqGnasQKV0CWLevqMTXBYG9GbwuE6x3VetERLh0cq2LTptFE73MrQi2S7GkKXCf2GgwItB/melLnxfnsg==
+ dependencies:
+ "@types/react" "^17"
+
"@types/react-dom@18.0.10":
version "18.0.10"
resolved "https://registry.yarnpkg.com/@types/react-dom/-/react-dom-18.0.10.tgz#3b66dec56aa0f16a6cc26da9e9ca96c35c0b4352"
@@ -443,6 +503,13 @@
dependencies:
"@types/react" "*"
+"@types/react-table@7.7.12":
+ version "7.7.12"
+ resolved "https://registry.yarnpkg.com/@types/react-table/-/react-table-7.7.12.tgz#628011d3cb695b07c678704a61f2f1d5b8e567fd"
+ integrity sha512-bRUent+NR/WwtDGwI/BqhZ8XnHghwHw0HUKeohzB5xN3K2qKWYE5w19e7GCuOkL1CXD9Gi1HFy7TIm2AvgWUHg==
+ dependencies:
+ "@types/react" "*"
+
"@types/react@*":
version "18.0.28"
resolved "https://registry.yarnpkg.com/@types/react/-/react-18.0.28.tgz#accaeb8b86f4908057ad629a26635fe641480065"
@@ -452,6 +519,15 @@
"@types/scheduler" "*"
csstype "^3.0.2"
+"@types/react@17.0.48":
+ version "17.0.48"
+ resolved "https://registry.yarnpkg.com/@types/react/-/react-17.0.48.tgz#a4532a8b91d7b27b8768b6fc0c3bccb760d15a6c"
+ integrity sha512-zJ6IYlJ8cYYxiJfUaZOQee4lh99mFihBoqkOSEGV+dFi9leROW6+PgstzQ+w3gWTnUfskALtQPGHK6dYmPj+2A==
+ dependencies:
+ "@types/prop-types" "*"
+ "@types/scheduler" "*"
+ csstype "^3.0.2"
+
"@types/react@18.0.27":
version "18.0.27"
resolved "https://registry.yarnpkg.com/@types/react/-/react-18.0.27.tgz#d9425abe187a00f8a5ec182b010d4fd9da703b71"
@@ -461,6 +537,15 @@
"@types/scheduler" "*"
csstype "^3.0.2"
+"@types/react@^17":
+ version "17.0.53"
+ resolved "https://registry.yarnpkg.com/@types/react/-/react-17.0.53.tgz#10d4d5999b8af3d6bc6a9369d7eb953da82442ab"
+ integrity sha512-1yIpQR2zdYu1Z/dc1OxC+MA6GR240u3gcnP4l6mvj/PJiVaqHsQPmWttsvHsfnhfPbU2FuGmo0wSITPygjBmsw==
+ dependencies:
+ "@types/prop-types" "*"
+ "@types/scheduler" "*"
+ csstype "^3.0.2"
+
"@types/scheduler@*":
version "0.16.2"
resolved "https://registry.yarnpkg.com/@types/scheduler/-/scheduler-0.16.2.tgz#1a62f89525723dde24ba1b01b092bf5df8ad4d39"
@@ -626,6 +711,11 @@ assertion-error@^1.1.0:
resolved "https://registry.yarnpkg.com/assertion-error/-/assertion-error-1.1.0.tgz#e60b6b0e8f301bd97e5375215bda406c85118c0b"
integrity sha512-jgsaNduz+ndvGyFt3uSuWqvy4lCnIJiovtouQN5JZHOKCS2QuhEdbcQHFhVksz2N2U9hXJo8odG7ETyWlEeuDw==
+asynckit@^0.4.0:
+ version "0.4.0"
+ resolved "https://registry.yarnpkg.com/asynckit/-/asynckit-0.4.0.tgz#c79ed97f7f34cb8f2ba1bc9790bcc366474b4b79"
+ integrity sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==
+
autoprefixer@10.4.13:
version "10.4.13"
resolved "https://registry.yarnpkg.com/autoprefixer/-/autoprefixer-10.4.13.tgz#b5136b59930209a321e9fa3dca2e7c4d223e83a8"
@@ -643,6 +733,15 @@ available-typed-arrays@^1.0.5:
resolved "https://registry.yarnpkg.com/available-typed-arrays/-/available-typed-arrays-1.0.5.tgz#92f95616501069d07d10edb2fc37d3e1c65123b7"
integrity sha512-DMD0KiN46eipeziST1LPP/STfDU0sufISXmjSgvVsoU2tqxctQeASejWcfNtxYKqETM1UxQ8sp2OrSBWpHY6sw==
+axios@*, axios@1.3.4:
+ version "1.3.4"
+ resolved "https://registry.yarnpkg.com/axios/-/axios-1.3.4.tgz#f5760cefd9cfb51fd2481acf88c05f67c4523024"
+ integrity sha512-toYm+Bsyl6VC5wSkfkbbNB6ROv7KY93PEBBL6xyDczaIHasAiv4wPqQ/c4RjoQzipxRD2W5g21cOqQulZ7rHwQ==
+ dependencies:
+ follow-redirects "^1.15.0"
+ form-data "^4.0.0"
+ proxy-from-env "^1.1.0"
+
balanced-match@^1.0.0:
version "1.0.2"
resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.2.tgz#e83e3a7e3f300b34cb9d87f615fa0cbf357690ee"
@@ -818,6 +917,11 @@ ci-info@^3.2.0:
resolved "https://registry.yarnpkg.com/ci-info/-/ci-info-3.8.0.tgz#81408265a5380c929f0bc665d62256628ce9ef91"
integrity sha512-eXTggHWSooYhq49F2opQhuHWgzucfF2YgODK4e1566GQs5BIfP30B0oenwBJHfWxAs2fyPB1s7Mg949zLf61Yw==
+classnames@2.3.1:
+ version "2.3.1"
+ resolved "https://registry.yarnpkg.com/classnames/-/classnames-2.3.1.tgz#dfcfa3891e306ec1dad105d0e88f4417b8535e8e"
+ integrity sha512-OlQdbZ7gLfGarSqxesMesDa5uz7KFbID8Kpq/SxIoNGDqY8lSYs0D+hhtBXhcdB3rcbXArFr7vlHheLk1voeNA==
+
cli-cursor@^3.1.0:
version "3.1.0"
resolved "https://registry.yarnpkg.com/cli-cursor/-/cli-cursor-3.1.0.tgz#264305a7ae490d1d03bf0c9ba7c925d1753af307"
@@ -881,6 +985,13 @@ color-name@~1.1.4:
resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.4.tgz#c2a09a87acbde69543de6f63fa3995c826c536a2"
integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==
+combined-stream@^1.0.8:
+ version "1.0.8"
+ resolved "https://registry.yarnpkg.com/combined-stream/-/combined-stream-1.0.8.tgz#c3d45a8b34fd730631a110a8a2520682b31d5a7f"
+ integrity sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==
+ dependencies:
+ delayed-stream "~1.0.0"
+
concat-map@0.0.1:
version "0.0.1"
resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b"
@@ -901,6 +1012,16 @@ csstype@^3.0.2:
resolved "https://registry.yarnpkg.com/csstype/-/csstype-3.1.1.tgz#841b532c45c758ee546a11d5bd7b7b473c8c30b9"
integrity sha512-DJR/VvkAvSZW9bTouZue2sSxDwdTN92uHjqeKVm+0dAqdfNykRzQ95tay8aXMBAAPpUiq4Qcug2L7neoRh2Egw==
+date-fns-tz@2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/date-fns-tz/-/date-fns-tz-2.0.0.tgz#1b14c386cb8bc16fc56fe333d4fc34ae1d1099d5"
+ integrity sha512-OAtcLdB9vxSXTWHdT8b398ARImVwQMyjfYGkKD2zaGpHseG2UPHbHjXELReErZFxWdSLph3c2zOaaTyHfOhERQ==
+
+date-fns@2.29.3:
+ version "2.29.3"
+ resolved "https://registry.yarnpkg.com/date-fns/-/date-fns-2.29.3.tgz#27402d2fc67eb442b511b70bbdf98e6411cd68a8"
+ integrity sha512-dDCnyH2WnnKusqvZZ6+jA1O51Ibt8ZMRNkDZdyAyK4YfbDwa/cEmuztzG5pk6hqlp9aSBPYcjOlktquahGwGeA==
+
debug@^4.3.3, debug@^4.3.4:
version "4.3.4"
resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.4.tgz#1319f6579357f2338d3337d2cdd4914bb5dcc865"
@@ -953,6 +1074,11 @@ define-properties@^1.1.3, define-properties@^1.1.4:
has-property-descriptors "^1.0.0"
object-keys "^1.1.1"
+delayed-stream@~1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/delayed-stream/-/delayed-stream-1.0.0.tgz#df3ae199acadfb7d440aaae0b29e2272b24ec619"
+ integrity sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==
+
dependency-graph@^0.11.0:
version "0.11.0"
resolved "https://registry.yarnpkg.com/dependency-graph/-/dependency-graph-0.11.0.tgz#ac0ce7ed68a54da22165a85e97a01d53f5eb2e27"
@@ -963,6 +1089,11 @@ detect-node@^2.0.4, detect-node@^2.1.0:
resolved "https://registry.yarnpkg.com/detect-node/-/detect-node-2.1.0.tgz#c9c70775a49c3d03bc2c06d9a73be550f978f8b1"
integrity sha512-T0NIuQpnTvFDATNuHN5roPwSBG83rFsuO+MXXH9/3N1eFbn4wcPjttvjMLEPWJ0RGUYgQE7cGgS3tNxbqCGM7g==
+diff-sequences@^27.5.1:
+ version "27.5.1"
+ resolved "https://registry.yarnpkg.com/diff-sequences/-/diff-sequences-27.5.1.tgz#eaecc0d327fd68c8d9672a1e64ab8dccb2ef5327"
+ integrity sha512-k1gCAXAsNgLwEL+Y8Wvl+M6oEFj5bgazfZULpS5CneoPPXRaCCW7dm+q21Ky2VEE5X+VeRDBVg1Pcvvsr4TtNQ==
+
diff-sequences@^29.4.3:
version "29.4.3"
resolved "https://registry.yarnpkg.com/diff-sequences/-/diff-sequences-29.4.3.tgz#9314bc1fabe09267ffeca9cbafc457d8499a13f2"
@@ -985,7 +1116,7 @@ dom-accessibility-api@^0.5.6, dom-accessibility-api@^0.5.9:
resolved "https://registry.yarnpkg.com/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz#5a7429e6066eb3664d911e33fb0e45de8eb08453"
integrity sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==
-dotenv@^16.0.3:
+dotenv@16.0.3:
version "16.0.3"
resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-16.0.3.tgz#115aec42bac5053db3c456db30cc243a5a836a07"
integrity sha512-7GO6HghkA5fYG9TYnNxi14/7K9f5occMlp3zXAuSxn7CKCxt9xbNWG7yF8hTCSUchlfWSe3uLmlPfigevRItzQ==
@@ -1125,6 +1256,11 @@ fill-range@^7.0.1:
dependencies:
to-regex-range "^5.0.1"
+follow-redirects@^1.15.0:
+ version "1.15.2"
+ resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.15.2.tgz#b460864144ba63f2681096f274c4e57026da2c13"
+ integrity sha512-VQLG33o04KaQ8uYi2tVNbdrWp1QWxNNea+nmIB4EVM28v0hmP17z7aG1+wAkNzVq4KeXTq3221ye5qTJP91JwA==
+
for-each@^0.3.3:
version "0.3.3"
resolved "https://registry.yarnpkg.com/for-each/-/for-each-0.3.3.tgz#69b447e88a0a5d32c3e7084f3f1710034b21376e"
@@ -1132,6 +1268,15 @@ for-each@^0.3.3:
dependencies:
is-callable "^1.1.3"
+form-data@^4.0.0:
+ version "4.0.0"
+ resolved "https://registry.yarnpkg.com/form-data/-/form-data-4.0.0.tgz#93919daeaf361ee529584b9b31664dc12c9fa452"
+ integrity sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==
+ dependencies:
+ asynckit "^0.4.0"
+ combined-stream "^1.0.8"
+ mime-types "^2.1.12"
+
fraction.js@^4.2.0:
version "4.2.0"
resolved "https://registry.yarnpkg.com/fraction.js/-/fraction.js-4.2.0.tgz#448e5109a313a3527f5a3ab2119ec4cf0e0e2950"
@@ -1536,6 +1681,16 @@ isarray@^2.0.5:
resolved "https://registry.yarnpkg.com/isarray/-/isarray-2.0.5.tgz#8af1e4c1221244cc62459faf38940d4e644a5723"
integrity sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==
+jest-diff@^27.5.1:
+ version "27.5.1"
+ resolved "https://registry.yarnpkg.com/jest-diff/-/jest-diff-27.5.1.tgz#a07f5011ac9e6643cf8a95a462b7b1ecf6680def"
+ integrity sha512-m0NvkX55LDt9T4mctTEgnZk3fmEg3NRYutvMPWM/0iPnkFj2wIeF45O1718cMSOFO1vINkqmxqD8vE37uTEbqw==
+ dependencies:
+ chalk "^4.0.0"
+ diff-sequences "^27.5.1"
+ jest-get-type "^27.5.1"
+ pretty-format "^27.5.1"
+
jest-diff@^29.4.3:
version "29.4.3"
resolved "https://registry.yarnpkg.com/jest-diff/-/jest-diff-29.4.3.tgz#42f4eb34d0bf8c0fb08b0501069b87e8e84df347"
@@ -1546,11 +1701,26 @@ jest-diff@^29.4.3:
jest-get-type "^29.4.3"
pretty-format "^29.4.3"
+jest-get-type@^27.5.1:
+ version "27.5.1"
+ resolved "https://registry.yarnpkg.com/jest-get-type/-/jest-get-type-27.5.1.tgz#3cd613c507b0f7ace013df407a1c1cd578bcb4f1"
+ integrity sha512-2KY95ksYSaK7DMBWQn6dQz3kqAf3BB64y2udeG+hv4KfSOb9qwcYQstTJc1KCbsix+wLZWZYN8t7nwX3GOBLRw==
+
jest-get-type@^29.4.3:
version "29.4.3"
resolved "https://registry.yarnpkg.com/jest-get-type/-/jest-get-type-29.4.3.tgz#1ab7a5207c995161100b5187159ca82dd48b3dd5"
integrity sha512-J5Xez4nRRMjk8emnTpWrlkyb9pfRQQanDrvWHhsR1+VUfbwxi30eVcZFlcdGInRibU4G5LwHXpI7IRHU0CY+gg==
+jest-matcher-utils@^27.0.0:
+ version "27.5.1"
+ resolved "https://registry.yarnpkg.com/jest-matcher-utils/-/jest-matcher-utils-27.5.1.tgz#9c0cdbda8245bc22d2331729d1091308b40cf8ab"
+ integrity sha512-z2uTx/T6LBaCoNWNFWwChLBKYxTMcGBRjAt+2SbP929/Fflb9aa5LGma654Rz8z9HLxsrUaYzxE9T/EFIL/PAw==
+ dependencies:
+ chalk "^4.0.0"
+ jest-diff "^27.5.1"
+ jest-get-type "^27.5.1"
+ pretty-format "^27.5.1"
+
jest-matcher-utils@^29.4.3:
version "29.4.3"
resolved "https://registry.yarnpkg.com/jest-matcher-utils/-/jest-matcher-utils-29.4.3.tgz#ea68ebc0568aebea4c4213b99f169ff786df96a0"
@@ -1627,7 +1797,7 @@ local-pkg@^0.4.2:
resolved "https://registry.yarnpkg.com/local-pkg/-/local-pkg-0.4.3.tgz#0ff361ab3ae7f1c19113d9bb97b98b905dbc4963"
integrity sha512-SFppqq5p42fe2qcZQqqEOiVRXl+WCP1MdT6k7BDEW1j++sp5fIY+/fdRQitvKgB5BrBcmrs5m/L0v2FrU5MY1g==
-lodash@^4.17.15, lodash@^4.17.21:
+lodash@4.17.21, lodash@^4.17.15, lodash@^4.17.21:
version "4.17.21"
resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c"
integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==
@@ -1640,7 +1810,7 @@ log-symbols@^4.1.0:
chalk "^4.1.0"
is-unicode-supported "^0.1.0"
-loose-envify@^1.1.0:
+loose-envify@^1.1.0, loose-envify@^1.4.0:
version "1.4.0"
resolved "https://registry.yarnpkg.com/loose-envify/-/loose-envify-1.4.0.tgz#71ee51fa7be4caec1a63839f7e682d8132d30caf"
integrity sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==
@@ -1685,6 +1855,18 @@ microseconds@0.2.0:
resolved "https://registry.yarnpkg.com/microseconds/-/microseconds-0.2.0.tgz#233b25f50c62a65d861f978a4a4f8ec18797dc39"
integrity sha512-n7DHHMjR1avBbSpsTBj6fmMGh2AGrifVV4e+WYc3Q9lO+xnSZ3NyhcBND3vzzatt05LFhoKFRxrIyklmLlUtyA==
+mime-db@1.52.0:
+ version "1.52.0"
+ resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.52.0.tgz#bbabcdc02859f4987301c856e3387ce5ec43bf70"
+ integrity sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==
+
+mime-types@^2.1.12:
+ version "2.1.35"
+ resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.35.tgz#381a871b62a734450660ae3deee44813f70d959a"
+ integrity sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==
+ dependencies:
+ mime-db "1.52.0"
+
mimic-fn@^2.1.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/mimic-fn/-/mimic-fn-2.1.0.tgz#7ed2c2ccccaf84d3ffcb7a69b57711fc2083401b"
@@ -1712,6 +1894,11 @@ mlly@^1.0.0, mlly@^1.1.0:
pkg-types "^1.0.1"
ufo "^1.0.1"
+mockdate@3.0.5:
+ version "3.0.5"
+ resolved "https://registry.yarnpkg.com/mockdate/-/mockdate-3.0.5.tgz#789be686deb3149e7df2b663d2bc4392bc3284fb"
+ integrity sha512-iniQP4rj1FhBdBYS/+eQv7j1tadJ9lJtdzgOpvsOHng/GbcDh2Fhdeq+ZRldrPYdXvCyfFUmFeEwEGXZB5I/AQ==
+
ms@2.1.2:
version "2.1.2"
resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009"
@@ -1754,7 +1941,7 @@ nano-time@1.0.0:
dependencies:
big-integer "^1.6.16"
-nanoid@^3.3.4:
+nanoid@3.3.4, nanoid@^3.3.4:
version "3.3.4"
resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.4.tgz#730b67e3cd09e2deacf03c027c81c9d9dbc5e8ab"
integrity sha512-MqBkQh/OHTS2egovRtLk45wEyNXwF+cokD+1YPf9u5VfJiRdAiRwB2froX5Co9Rh20xs4siNPm8naNotSD6RBw==
@@ -1781,6 +1968,11 @@ normalize-range@^0.1.2:
resolved "https://registry.yarnpkg.com/normalize-range/-/normalize-range-0.1.2.tgz#2d10c06bdfd312ea9777695a4d28439456b75942"
integrity sha512-bdok/XvKII3nUpklnV6P2hxtMNrCboOjAcyBuQnWEhO665FwrSNRxU+AqpsyvO6LgGYPspN+lu5CLtw4jPRKNA==
+object-assign@^4.1.1:
+ version "4.1.1"
+ resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863"
+ integrity sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==
+
object-inspect@^1.9.0:
version "1.12.3"
resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.12.3.tgz#ba62dffd67ee256c8c086dfae69e016cd1f198b9"
@@ -1972,7 +2164,7 @@ postcss@8.4.21, postcss@^8.4.21:
picocolors "^1.0.0"
source-map-js "^1.0.2"
-pretty-format@^27.0.2, pretty-format@^27.5.1:
+pretty-format@^27.0.0, pretty-format@^27.0.2, pretty-format@^27.5.1:
version "27.5.1"
resolved "https://registry.yarnpkg.com/pretty-format/-/pretty-format-27.5.1.tgz#2181879fdea51a7a5851fb39d920faa63f01d88e"
integrity sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==
@@ -1995,6 +2187,20 @@ pretty-hrtime@^1.0.3:
resolved "https://registry.yarnpkg.com/pretty-hrtime/-/pretty-hrtime-1.0.3.tgz#b7e3ea42435a4c9b2759d99e0f201eb195802ee1"
integrity sha512-66hKPCr+72mlfiSjlEB1+45IjXSqvVAIy6mocupoww4tBFE9R9IhwwUGoI4G++Tc9Aq+2rxOt0RFU6gPcrte0A==
+prop-types@15.8.1:
+ version "15.8.1"
+ resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.8.1.tgz#67d87bf1a694f48435cf332c24af10214a3140b5"
+ integrity sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==
+ dependencies:
+ loose-envify "^1.4.0"
+ object-assign "^4.1.1"
+ react-is "^16.13.1"
+
+proxy-from-env@^1.1.0:
+ version "1.1.0"
+ resolved "https://registry.yarnpkg.com/proxy-from-env/-/proxy-from-env-1.1.0.tgz#e102f16ca355424865755d2c9e8ea4f24d58c3e2"
+ integrity sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==
+
queue-microtask@^1.2.2:
version "1.2.3"
resolved "https://registry.yarnpkg.com/queue-microtask/-/queue-microtask-1.2.3.tgz#4929228bbc724dfac43e0efb058caf7b6cfb6243"
@@ -2008,6 +2214,11 @@ react-dom@18.2.0:
loose-envify "^1.1.0"
scheduler "^0.23.0"
+react-is@^16.13.1:
+ version "16.13.1"
+ resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4"
+ integrity sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==
+
react-is@^17.0.1:
version "17.0.2"
resolved "https://registry.yarnpkg.com/react-is/-/react-is-17.0.2.tgz#e691d4a8e9c789365655539ab372762b0efb54f0"
@@ -2042,6 +2253,18 @@ react-router@6.8.1:
dependencies:
"@remix-run/router" "1.3.2"
+react-table@7.8.0:
+ version "7.8.0"
+ resolved "https://registry.yarnpkg.com/react-table/-/react-table-7.8.0.tgz#07858c01c1718c09f7f1aed7034fcfd7bda907d2"
+ integrity sha512-hNaz4ygkZO4bESeFfnfOft73iBUj8K5oKi1EcSHPAibEydfsX2MyU6Z8KCr3mv3C9Kqqh71U+DhZkFvibbnPbA==
+
+react-useportal@1.0.17:
+ version "1.0.17"
+ resolved "https://registry.yarnpkg.com/react-useportal/-/react-useportal-1.0.17.tgz#dcea1de8aa6d1ebd4bb3bb08075180a0e620f718"
+ integrity sha512-IS1NC+qQtp8e9Z9f8EPpW5whU85VuaT9Vxzw20vAAfKoRB9dlsnFmZ8NeNkbjNaue7Iy64Qsafs73M7qvkMzoA==
+ dependencies:
+ use-ssr "^1.0.22"
+
react@18.2.0:
version "18.2.0"
resolved "https://registry.yarnpkg.com/react/-/react-18.2.0.tgz#555bd98592883255fa00de14f1151a917b5d77d5"
@@ -2372,6 +2595,11 @@ through@^2.3.6:
resolved "https://registry.yarnpkg.com/through/-/through-2.3.8.tgz#0dd4c9ffaabc357960b1b724115d7e0e86a2e1f5"
integrity sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==
+timezone-mock@1.3.6:
+ version "1.3.6"
+ resolved "https://registry.yarnpkg.com/timezone-mock/-/timezone-mock-1.3.6.tgz#44e4c5aeb57e6c07ae630a05c528fc4d9aab86f4"
+ integrity sha512-YcloWmZfLD9Li5m2VcobkCDNVaLMx8ohAb/97l/wYS3m+0TIEK5PFNMZZfRcusc6sFjIfxu8qcJT0CNnOdpqmg==
+
tinybench@^2.3.1:
version "2.3.1"
resolved "https://registry.yarnpkg.com/tinybench/-/tinybench-2.3.1.tgz#14f64e6b77d7ef0b1f6ab850c7a808c6760b414d"
@@ -2457,6 +2685,11 @@ update-browserslist-db@^1.0.10:
escalade "^3.1.1"
picocolors "^1.0.0"
+use-ssr@^1.0.22:
+ version "1.0.24"
+ resolved "https://registry.yarnpkg.com/use-ssr/-/use-ssr-1.0.24.tgz#213a3df58f5ab9268e6fe1a57ad0a9de91e514d1"
+ integrity sha512-0MFps7ezL57/3o0yl4CvrHLlp9z20n1rQZV/lSRz7if+TUoM6POU1XdOvEjIgjgKeIhTEye1U0khrIYWCTWw4g==
+
util-deprecate@^1.0.1:
version "1.0.2"
resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf"
Follow ups