← Back to team overview

sts-sponsors team mailing list archive

[Merge] ~petermakowski/maas-site-manager:feat-heartbeat-status-icons-MAASENG-1469 into maas-site-manager:main

 

Peter Makowski has proposed merging ~petermakowski/maas-site-manager:feat-heartbeat-status-icons-MAASENG-1469 into maas-site-manager:main.

Commit message:
feat(sites): heartbeat status icons MAASENG-1469


Requested reviews:
  MAAS Committers (maas-committers)

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

QA Steps:
1. Go to sites page
2. Verify that each site has a correct connection status text along with a status icon
-- 
Your team MAAS Committers is requested to review the proposed merge of ~petermakowski/maas-site-manager:feat-heartbeat-status-icons-MAASENG-1469 into maas-site-manager:main.
diff --git a/frontend/src/App.scss b/frontend/src/App.scss
index ff53c94..9923a4c 100644
--- a/frontend/src/App.scss
+++ b/frontend/src/App.scss
@@ -5,6 +5,12 @@
 @include vf-p-grid;
 @include vf-l-application;
 
+@import "patterns_icons"; // include common styles shared by all icons
+@include vf-p-icons-common;
+@include vf-p-icon-status-failed-small;
+@include vf-p-icon-status-waiting-small;
+@include vf-p-icon-status-succeeded-small;
+@include vf-p-icons;
 @import "./utils";
 @import "./patterns_icons";
 @import "./patterns_typography";
diff --git a/frontend/src/_utils.scss b/frontend/src/_utils.scss
index 8190eef..0ff7a85 100644
--- a/frontend/src/_utils.scss
+++ b/frontend/src/_utils.scss
@@ -10,3 +10,6 @@
 .u-flex--column {
   flex-direction: column !important;
 }
+.u-capitalize {
+  text-transform: capitalize !important;
+}
diff --git a/frontend/src/api/types.ts b/frontend/src/api/types.ts
index 690882e..8166a68 100644
--- a/frontend/src/api/types.ts
+++ b/frontend/src/api/types.ts
@@ -2,7 +2,7 @@ export type Site = {
   identifier: string;
   name: string;
   url: string; // <full URL including protocol>,
-  connection: "stable" | "unstable" | "stale" | "lost";
+  connection: "stable" | "lost" | "unknown";
   last_seen: string; // <ISO 8601 date>,
   address: {
     countrycode: string; // <alpha2 country code>,
diff --git a/frontend/src/components/SitesList/components/ConnectionInfo.test.tsx b/frontend/src/components/SitesList/components/ConnectionInfo.test.tsx
new file mode 100644
index 0000000..88e355c
--- /dev/null
+++ b/frontend/src/components/SitesList/components/ConnectionInfo.test.tsx
@@ -0,0 +1,13 @@
+import ConnectionInfo, { connectionIcons, connectionLabels } from "./ConnectionInfo";
+
+import { connections } from "@/mocks/factories";
+import { render, screen } from "@/test-utils";
+
+connections.forEach((connection) => {
+  it(`displays correct connection status icon and label for ${connection} connection`, () => {
+    const { container } = render(<ConnectionInfo connection={connection} />);
+    expect(screen.getByText(connectionLabels[connection])).toBeInTheDocument();
+    // eslint-disable-next-line testing-library/no-container
+    expect(container.querySelector(".status-icon")).toHaveClass(connectionIcons[connection]);
+  });
+});
diff --git a/frontend/src/components/SitesList/components/ConnectionInfo.tsx b/frontend/src/components/SitesList/components/ConnectionInfo.tsx
new file mode 100644
index 0000000..17c9d5b
--- /dev/null
+++ b/frontend/src/components/SitesList/components/ConnectionInfo.tsx
@@ -0,0 +1,28 @@
+import classNames from "classnames";
+import get from "lodash/get";
+
+import type { Site } from "@/api/types";
+
+// Stable, Lost and "Waiting for first" are the only possible values
+export const connectionIcons: Record<Site["connection"], string> = {
+  stable: "is-stable",
+  lost: "is-lost",
+  unknown: "is-unknown",
+} as const;
+export const connectionLabels: Record<Site["connection"], string> = {
+  stable: "Stable",
+  lost: "Lost",
+  unknown: "Waiting for first",
+} as const;
+
+type ConnectionInfoProps = { connection: Site["connection"]; lastSeen?: Site["last_seen"] };
+
+const ConnectionInfo = ({ connection, lastSeen }: ConnectionInfoProps) => (
+  <>
+    <div className={classNames("connection__text", "status-icon", get(connectionIcons, connection))}>
+      {get(connectionLabels, connection)}
+    </div>
+    <div className="connection__text u-text--muted">{lastSeen}</div>
+  </>
+);
+export default ConnectionInfo;
diff --git a/frontend/src/components/SitesList/components/SitesTable.scss b/frontend/src/components/SitesList/components/SitesTable.scss
index 37b435f..da82d6e 100644
--- a/frontend/src/components/SitesList/components/SitesTable.scss
+++ b/frontend/src/components/SitesList/components/SitesTable.scss
@@ -2,4 +2,35 @@
   thead th:first-child {
     width: 3rem;
   }
+  th.connection {
+    padding-left: 1.5rem;
+  }
+  td.connection {
+    padding-left: 0;
+    .connection__text {
+      padding-left: 1.5rem;
+    }
+  }
+  .status-icon {
+    display: inline-block;
+    position: relative;
+    padding-left: 1.5rem;
+  }
+  .status-icon::before {
+    content: "\00B7";
+    font-size: 5rem;
+    position: absolute;
+    left: 0;
+    top: -6px;
+    color: transparent;
+  }
+  .status-icon.is-lost::before {
+    color: #c7162b;
+  }
+  .status-icon.is-stable::before {
+    color: #0e8420;
+  }
+  .status-icon.is-unknown::before {
+    color: #cdcdcd;
+  }
 }
diff --git a/frontend/src/components/SitesList/components/SitesTable.tsx b/frontend/src/components/SitesList/components/SitesTable.tsx
index f32ca38..b80b413 100644
--- a/frontend/src/components/SitesList/components/SitesTable.tsx
+++ b/frontend/src/components/SitesList/components/SitesTable.tsx
@@ -6,6 +6,7 @@ import type { ColumnDef, Column } from "@tanstack/react-table";
 import pick from "lodash/fp/pick";
 import useLocalStorageState from "use-local-storage-state";
 
+import ConnectionInfo from "./ConnectionInfo";
 import SitesTableControls from "./SitesTableControls";
 
 import type { SitesQueryResult } from "@/api/types";
@@ -87,16 +88,14 @@ const SitesTable = ({
         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>
+            <div className="connection__text">connection</div>
+            <div className="connection__text u-text--muted">last seen</div>
           </>
         ),
+        cell: ({ getValue }) => {
+          const { connection, last_seen } = getValue();
+          return connection ? <ConnectionInfo connection={connection} lastSeen={last_seen} /> : null;
+        },
       },
       {
         id: "address",
@@ -201,7 +200,7 @@ const SitesTable = ({
             <tr key={headerGroup.id}>
               {headerGroup.headers.map((header) => {
                 return (
-                  <th colSpan={header.colSpan} key={header.id}>
+                  <th className={`${header.column.id}`} colSpan={header.colSpan} key={header.id}>
                     {header.isPlaceholder ? null : flexRender(header.column.columnDef.header, header.getContext())}
                     {header.column.getCanResize() && (
                       <div
@@ -224,7 +223,11 @@ const SitesTable = ({
               return (
                 <tr key={row.id}>
                   {row.getVisibleCells().map((cell) => {
-                    return <td key={cell.id}>{flexRender(cell.column.columnDef.cell, cell.getContext())}</td>;
+                    return (
+                      <td className={`${cell.column.id}`} key={cell.id}>
+                        {flexRender(cell.column.columnDef.cell, cell.getContext())}
+                      </td>
+                    );
                   })}
                 </tr>
               );
diff --git a/frontend/src/mocks/factories.ts b/frontend/src/mocks/factories.ts
index afb3c09..d1dccba 100644
--- a/frontend/src/mocks/factories.ts
+++ b/frontend/src/mocks/factories.ts
@@ -4,7 +4,7 @@ import { uniqueNamesGenerator, adjectives, colors, animals } from "unique-names-
 
 import type { Site, Token } from "@/api/types";
 
-const connections: Site["connection"][] = ["stable", "lost", "stale", "unstable"];
+export const connections: Site["connection"][] = ["stable", "lost", "unknown"];
 
 export const siteFactory = Factory.define<Site>(({ sequence }) => {
   const chance = new Chance(`maas-${sequence}`);

Follow ups