← Back to team overview

sts-sponsors team mailing list archive

[Merge] ~maas-committers/maas-site-manager/+git/maas-site-manager-frontend:react-router-dom-MAASENG-1399 into ~maas-committers/maas-site-manager/+git/maas-site-manager-frontend:main

 

Peter Makowski has proposed merging ~maas-committers/maas-site-manager/+git/maas-site-manager-frontend:react-router-dom-MAASENG-1399 into ~maas-committers/maas-site-manager/+git/maas-site-manager-frontend:main.

Requested reviews:
  MAAS Committers (maas-committers)

For more details, see:
https://code.launchpad.net/~maas-committers/maas-site-manager/+git/maas-site-manager-frontend/+merge/437798

Add react router (react-router-dom) and setup base routes for the app
-- 
Your team MAAS Committers is requested to review the proposed merge of ~maas-committers/maas-site-manager/+git/maas-site-manager-frontend:react-router-dom-MAASENG-1399 into ~maas-committers/maas-site-manager/+git/maas-site-manager-frontend:main.
diff --git a/package.json b/package.json
index 1e65cf2..48cab3d 100644
--- a/package.json
+++ b/package.json
@@ -14,6 +14,7 @@
     "react": "18.2.0",
     "react-dom": "18.2.0",
     "react-query": "3.39.3",
+    "react-router-dom": "6.8.1",
     "vanilla-framework": "3.11.0"
   },
   "devDependencies": {
diff --git a/src/App.test.tsx b/src/App.test.tsx
index 7bb2bcf..e4c3b38 100644
--- a/src/App.test.tsx
+++ b/src/App.test.tsx
@@ -1,4 +1,4 @@
-import { render, screen } from "@testing-library/react";
+import { render, screen } from "./test-utils";
 
 import App from "./App";
 
diff --git a/src/App.tsx b/src/App.tsx
index 1f20875..36cb2b5 100644
--- a/src/App.tsx
+++ b/src/App.tsx
@@ -1,20 +1,14 @@
 import "./App.scss";
-import SitesList from "./components/SitesList";
 import { QueryClient, QueryClientProvider } from "react-query";
+import router from "./router";
+import { RouterProvider } from "react-router-dom";
 
 const queryClient = new QueryClient();
 
 function App() {
   return (
     <QueryClientProvider client={queryClient}>
-      <div className="App">
-        <div className="row">
-          <div className="col-12">
-            <h1>MAAS Site Manager</h1>
-            <SitesList />
-          </div>
-        </div>
-      </div>
+      <RouterProvider router={router} />
     </QueryClientProvider>
   );
 }
diff --git a/src/components/MainLayout/MainLayout.tsx b/src/components/MainLayout/MainLayout.tsx
new file mode 100644
index 0000000..a60b2d9
--- /dev/null
+++ b/src/components/MainLayout/MainLayout.tsx
@@ -0,0 +1,14 @@
+import { Outlet } from "react-router-dom";
+
+const MainLayout = () => (
+  <div className="App">
+    <div className="row">
+      <div className="col-12">
+        <h1>MAAS Site Manager</h1>
+        <Outlet />
+      </div>
+    </div>
+  </div>
+);
+
+export default MainLayout;
diff --git a/src/components/MainLayout/index.ts b/src/components/MainLayout/index.ts
new file mode 100644
index 0000000..ff87b06
--- /dev/null
+++ b/src/components/MainLayout/index.ts
@@ -0,0 +1 @@
+export { default } from "./MainLayout";
diff --git a/src/components/SitesList.scss b/src/components/SitesList/SitesList.scss
similarity index 100%
rename from src/components/SitesList.scss
rename to src/components/SitesList/SitesList.scss
diff --git a/src/components/SitesList/SitesList.test.tsx b/src/components/SitesList/SitesList.test.tsx
new file mode 100644
index 0000000..e23e81c
--- /dev/null
+++ b/src/components/SitesList/SitesList.test.tsx
@@ -0,0 +1,10 @@
+import SitesList from "./SitesList";
+import { render, screen } from "../../test-utils";
+
+it("renders header", () => {
+  render(<SitesList />);
+
+  expect(
+    screen.getByRole("heading", { name: /MAAS Regions/i })
+  ).toBeInTheDocument();
+});
diff --git a/src/components/SitesList.tsx b/src/components/SitesList/SitesList.tsx
similarity index 56%
rename from src/components/SitesList.tsx
rename to src/components/SitesList/SitesList.tsx
index b1ba613..9a49bd0 100644
--- a/src/components/SitesList.tsx
+++ b/src/components/SitesList/SitesList.tsx
@@ -1,70 +1,11 @@
 import { useQuery } from "react-query";
+import SiteRow from "./components/SiteRow";
 import "./SitesList.scss";
-
-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;
-  };
-};
-
-type Sites = {
-  items: Site[];
-  total: number;
-  page: number;
-  size: number;
-};
-
-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>
-  );
-};
+import { Sites } from "./types";
 
 const SitesList = () => {
-  const query = useQuery<Sites>("/sites", async function () {
-    const response = await fetch("/sites");
+  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");
     }
@@ -74,7 +15,7 @@ const SitesList = () => {
 
   return (
     <div>
-      <h2>Sites</h2>
+      <h2>{query?.data?.items?.length || ""} MAAS Regions</h2>
       <table>
         <thead>
           <tr>
@@ -102,7 +43,9 @@ const SitesList = () => {
         </thead>
         <tbody>
           {query.data
-            ? query.data.items.map((site) => <SiteRow site={site} />)
+            ? query.data.items.map((site) => (
+                <SiteRow key={site.url} site={site} />
+              ))
             : null}
         </tbody>
       </table>
diff --git a/src/components/SitesList/components/SiteRow.tsx b/src/components/SitesList/components/SiteRow.tsx
new file mode 100644
index 0000000..22ed089
--- /dev/null
+++ b/src/components/SitesList/components/SiteRow.tsx
@@ -0,0 +1,37 @@
+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/src/components/SitesList/index.ts b/src/components/SitesList/index.ts
new file mode 100644
index 0000000..75d6f99
--- /dev/null
+++ b/src/components/SitesList/index.ts
@@ -0,0 +1 @@
+export { default } from "./SitesList";
diff --git a/src/components/SitesList/types.ts b/src/components/SitesList/types.ts
new file mode 100644
index 0000000..c8495ac
--- /dev/null
+++ b/src/components/SitesList/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/src/mocks/browser.ts b/src/mocks/browser.ts
index d84c82f..0f20911 100644
--- a/src/mocks/browser.ts
+++ b/src/mocks/browser.ts
@@ -1,50 +1,10 @@
 // src/mocks/browser.js
 import { setupWorker, rest } from "msw";
-
-const getMockSite = (site = {}) => ({
-  name: "maas-example-region",
-  url: "http://maas.example.com";,
-  connection: "stable",
-  last_seen: "2020-10-20T12:00:00Z",
-  address: {
-    countrycode: "DE", // <alpha2 country code>,
-    city: "Berlin",
-    zip: "10405",
-    street: "Prenzlauer Allee 132",
-  },
-  timezone: "CET", // <three letter abbreviation>,
-  stats: {
-    machines: 300,
-    occupied_machines: 289,
-    ready_machines: 11,
-    error_machines: 0,
-  },
-  ...site,
-});
+import { sites } from "./factories";
 
 const worker = setupWorker(
-  rest.get("/sites", (req, res, ctx) => {
-    return res(
-      ctx.json({
-        items: [
-          getMockSite(),
-          getMockSite({
-            name: "maas-shanghai",
-            url: "http://maas.shanghai.com";,
-            address: {
-              countrycode: "CHN",
-              city: "Shanghai",
-              zip: "200000",
-              street: "Xinzhen Road 3",
-            },
-            timezone: "CST",
-          }),
-        ],
-        total: 42,
-        page: 1,
-        size: 20,
-      })
-    );
+  rest.get("/api/sites", (_req, res, ctx) => {
+    return res(ctx.json(sites));
   })
 );
 
diff --git a/src/mocks/factories.ts b/src/mocks/factories.ts
new file mode 100644
index 0000000..2e647e2
--- /dev/null
+++ b/src/mocks/factories.ts
@@ -0,0 +1,40 @@
+export const site = (site = {}) => ({
+  name: "maas-example-region",
+  url: "http://maas.example.com";,
+  connection: "stable",
+  last_seen: "2020-10-20T12:00:00Z",
+  address: {
+    countrycode: "DE", // <alpha2 country code>,
+    city: "Berlin",
+    zip: "10405",
+    street: "Prenzlauer Allee 132",
+  },
+  timezone: "CET", // <three letter abbreviation>,
+  stats: {
+    machines: 300,
+    occupied_machines: 289,
+    ready_machines: 11,
+    error_machines: 0,
+  },
+  ...site,
+});
+
+export const sites = {
+  items: [
+    site(),
+    site({
+      name: "maas-shanghai",
+      url: "http://maas.shanghai.com";,
+      address: {
+        countrycode: "CHN",
+        city: "Shanghai",
+        zip: "200000",
+        street: "Xinzhen Road 3",
+      },
+      timezone: "CST",
+    }),
+  ],
+  total: 42,
+  page: 1,
+  size: 20,
+};
diff --git a/src/pages/index.tsx b/src/pages/index.tsx
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/src/pages/index.tsx
diff --git a/src/pages/list.tsx b/src/pages/list.tsx
new file mode 100644
index 0000000..3c34f73
--- /dev/null
+++ b/src/pages/list.tsx
@@ -0,0 +1,5 @@
+import SitesList from "../components/SitesList";
+
+const List = () => <SitesList />;
+
+export default List;
diff --git a/src/pages/login.tsx b/src/pages/login.tsx
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/src/pages/login.tsx
diff --git a/src/pages/logout.tsx b/src/pages/logout.tsx
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/src/pages/logout.tsx
diff --git a/src/pages/requests.tsx b/src/pages/requests.tsx
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/src/pages/requests.tsx
diff --git a/src/pages/tokens.tsx b/src/pages/tokens.tsx
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/src/pages/tokens.tsx
diff --git a/src/pages/users.tsx b/src/pages/users.tsx
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/src/pages/users.tsx
diff --git a/src/router.tsx b/src/router.tsx
new file mode 100644
index 0000000..b8ec6f8
--- /dev/null
+++ b/src/router.tsx
@@ -0,0 +1,24 @@
+import {
+  createBrowserRouter,
+  createRoutesFromElements,
+  Link,
+  Outlet,
+  Route,
+} from "react-router-dom";
+import MainLayout from "./components/MainLayout";
+import SitesList from "./components/SitesList/SitesList";
+
+const router = createBrowserRouter(
+  createRoutesFromElements(
+    <Route path="/" element={<MainLayout />}>
+      <Route path="login" />
+      <Route path="logout" />
+      <Route path="sites" element={<SitesList />} />
+      <Route path="requests" />
+      <Route path="tokens" />
+      <Route path="users" />
+    </Route>
+  )
+);
+
+export default router;
diff --git a/src/test-utils.tsx b/src/test-utils.tsx
new file mode 100644
index 0000000..419ac79
--- /dev/null
+++ b/src/test-utils.tsx
@@ -0,0 +1,26 @@
+import React, { ReactElement } from "react";
+import { render, RenderOptions } from "@testing-library/react";
+
+import { QueryClient, QueryClientProvider } from "react-query";
+
+const queryClient = new QueryClient({
+  defaultOptions: {
+    queries: {
+      retry: false,
+    },
+  },
+});
+
+const Providers = ({ children }: { children: React.ReactNode }) => {
+  return (
+    <QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
+  );
+};
+
+const customRender = (
+  ui: ReactElement,
+  options?: Omit<RenderOptions, "wrapper">
+) => render(ui, { wrapper: Providers, ...options });
+
+export * from "@testing-library/react";
+export { customRender as render };
diff --git a/yarn.lock b/yarn.lock
index a799953..8ecf88b 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -229,6 +229,11 @@
   resolved "https://registry.yarnpkg.com/@open-draft/until/-/until-1.0.3.tgz#db9cc719191a62e7d9200f6e7bab21c5b848adca";
   integrity sha512-Aq58f5HiWdyDlFffbbSjAlv596h/cOnt2DO1w3DOC7OJ5EHs0hd/nycJfiu9RJbT6Yk6F1knnRRXNSpxoIVZ9Q==
 
+"@remix-run/router@1.3.2":
+  version "1.3.2"
+  resolved "https://registry.yarnpkg.com/@remix-run/router/-/router-1.3.2.tgz#58cd2bd25df2acc16c628e1b6f6150ea6c7455bc";
+  integrity sha512-t54ONhl/h75X94SWsHGQ4G/ZrCEguKSRQr7DrjTciJXW0YU1QhlwYeycvK5JgkzlxmvrK7wq1NB/PLtHxoiDcA==
+
 "@sinclair/typebox@^0.25.16":
   version "0.25.22"
   resolved "https://registry.yarnpkg.com/@sinclair/typebox/-/typebox-0.25.22.tgz#2808d895e9c2722b20a622a9c8cb332f6720eb4a";
@@ -2002,6 +2007,21 @@ react-query@3.39.3:
     broadcast-channel "^3.4.1"
     match-sorter "^6.0.2"
 
+react-router-dom@6.8.1:
+  version "6.8.1"
+  resolved "https://registry.yarnpkg.com/react-router-dom/-/react-router-dom-6.8.1.tgz#7e136b67d9866f55999e9a8482c7008e3c575ac9";
+  integrity sha512-67EXNfkQgf34P7+PSb6VlBuaacGhkKn3kpE51+P6zYSG2kiRoumXEL6e27zTa9+PGF2MNXbgIUHTVlleLbIcHQ==
+  dependencies:
+    "@remix-run/router" "1.3.2"
+    react-router "6.8.1"
+
+react-router@6.8.1:
+  version "6.8.1"
+  resolved "https://registry.yarnpkg.com/react-router/-/react-router-6.8.1.tgz#e362caf93958a747c649be1b47cd505cf28ca63e";
+  integrity sha512-Jgi8BzAJQ8MkPt8ipXnR73rnD7EmZ0HFFb7jdQU24TynGW1Ooqin2KVDN9voSC+7xhqbbCd2cjGUepb6RObnyg==
+  dependencies:
+    "@remix-run/router" "1.3.2"
+
 react@18.2.0:
   version "18.2.0"
   resolved "https://registry.yarnpkg.com/react/-/react-18.2.0.tgz#555bd98592883255fa00de14f1151a917b5d77d5";

Follow ups