Peter Makowski has proposed merging 
~petermakowski/maas-site-manager:connection-column-MAASENG-1557 into 
maas-site-manager:main.

Commit message:
update connection column MAASENG-1557
- fix line-height 0 text collapsing issue
-  cleanup redundant table resizer code

Requested reviews:
  MAAS Lander (maas-lander): unittests
  MAAS Committers (maas-committers)

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

https://warthogs.atlassian.net/browse/MAASENG-1557

QA Steps
Go to sites
Verify that last seen is displayed in human readable format, e.g. 1 minute ago
Verify that rows for sites which have "Waiting for first" status are greyed out 
except from the "name" column

-- 
Your team MAAS Committers is requested to review the proposed merge of 
~petermakowski/maas-site-manager:connection-column-MAASENG-1557 into 
maas-site-manager:main.
diff --git a/frontend/src/_utils.scss b/frontend/src/_utils.scss
index b666356..514f0bf 100644
--- a/frontend/src/_utils.scss
+++ b/frontend/src/_utils.scss
@@ -38,9 +38,6 @@
 .u-no-border {
   border: 0 !important;
 }
-.u-no-line-height {
-  line-height: 0 !important;
-}
 .u-padding-top--medium {
   padding-top: $spv--medium !important;
 }
diff --git a/frontend/src/api/types.ts b/frontend/src/api/types.ts
index c124177..486a255 100644
--- a/frontend/src/api/types.ts
+++ b/frontend/src/api/types.ts
@@ -16,14 +16,12 @@ export type Site = {
   id: string;
   name: string;
   url: string; // <full URL including protocol>,
-  connection: Stats["connection"];
-  last_seen: string; // <ISO 8601 date>,
   country: string; // <alpha2 country code>,
   city: string;
   zip: string;
   street: string;
   timezone: string; // IANA time zone name,
-  stats: Stats;
+  stats: Stats | null;
 };
 
 export type PaginatedQueryResult<D extends unknown> = {
diff --git a/frontend/src/components/RequestsTable/RequestsTable.tsx b/frontend/src/components/RequestsTable/RequestsTable.tsx
index b4ccfc1..5a3bd3d 100644
--- a/frontend/src/components/RequestsTable/RequestsTable.tsx
+++ b/frontend/src/components/RequestsTable/RequestsTable.tsx
@@ -32,11 +32,10 @@ const RequestsTable = ({
 
   const columns = useMemo<EnrollmentRequestsColumnDef[]>(
     () => [
-      {
+      columnHelper.accessor("name", {
         id: "select",
-        accessorKey: "name",
         header: ({ table }) => <SelectAllCheckbox table={table} />,
-        cell: ({ row, getValue }: { row: Row<EnrollmentRequest>; getValue: Getter<EnrollmentRequest["name"]> }) => {
+        cell: ({ row, getValue }) => {
           return (
             <label className="p-checkbox--inline">
               <input
@@ -53,7 +52,7 @@ const RequestsTable = ({
             </label>
           );
         },
-      },
+      }),
       columnHelper.accessor("name", {
         id: "name",
         header: () => <div>Name</div>,
diff --git a/frontend/src/components/SitesList/SitesTable/ConnectionInfo/ConnectionInfo.test.tsx b/frontend/src/components/SitesList/SitesTable/ConnectionInfo/ConnectionInfo.test.tsx
index 88e355c..3ebdf1b 100644
--- a/frontend/src/components/SitesList/SitesTable/ConnectionInfo/ConnectionInfo.test.tsx
+++ b/frontend/src/components/SitesList/SitesTable/ConnectionInfo/ConnectionInfo.test.tsx
@@ -1,8 +1,20 @@
+import * as timezoneMock from "timezone-mock";
+
 import ConnectionInfo, { connectionIcons, connectionLabels } from "./ConnectionInfo";
 
 import { connections } from "@/mocks/factories";
 import { render, screen } from "@/test-utils";
 
+beforeEach(() => {
+  vi.useFakeTimers();
+  timezoneMock.register("Etc/GMT");
+});
+
+afterEach(() => {
+  timezoneMock.unregister();
+  vi.useRealTimers();
+});
+
 connections.forEach((connection) => {
   it(`displays correct connection status icon and label for ${connection} connection`, () => {
     const { container } = render(<ConnectionInfo connection={connection} />);
@@ -11,3 +23,15 @@ connections.forEach((connection) => {
     expect(container.querySelector(".status-icon")).toHaveClass(connectionIcons[connection]);
   });
 });
+
+it("displays last seen text relative to local time correctly", () => {
+  const date = new Date("2000-01-01T12:00:00Z");
+  vi.setSystemTime(date);
+  render(<ConnectionInfo connection={connections[0]} lastSeen="2000-01-01T11:58:00Z" />);
+  expect(screen.getByText("2 minutes ago")).toBeInTheDocument();
+});
+
+it("displays 'waiting for first' text for the unknown status", () => {
+  render(<ConnectionInfo connection="unknown" />);
+  expect(screen.getByText(/waiting for first/i)).toBeInTheDocument();
+});
diff --git a/frontend/src/components/SitesList/SitesTable/ConnectionInfo/ConnectionInfo.tsx b/frontend/src/components/SitesList/SitesTable/ConnectionInfo/ConnectionInfo.tsx
index 3eeef54..764d9da 100644
--- a/frontend/src/components/SitesList/SitesTable/ConnectionInfo/ConnectionInfo.tsx
+++ b/frontend/src/components/SitesList/SitesTable/ConnectionInfo/ConnectionInfo.tsx
@@ -1,50 +1,63 @@
 import classNames from "classnames";
 import get from "lodash/get";
 
-import type { Site } from "@/api/types";
+import type { Stats } from "@/api/types";
 import docsUrls from "@/base/docsUrls";
 import ExternalLink from "@/components/ExternalLink";
 import TooltipButton from "@/components/base/TooltipButton";
+import { formatDistanceToNow } from "@/utils";
 
-export const connectionIcons: Record<Site["connection"], string> = {
+export const connectionIcons: Record<Stats["connection"], string> = {
   stable: "is-stable",
   lost: "is-lost",
   unknown: "is-unknown",
 } as const;
-export const connectionLabels: Record<Site["connection"], string> = {
+export const connectionLabels: Record<Stats["connection"], string> = {
   stable: "Stable",
   lost: "Lost",
   unknown: "Waiting for first",
 } as const;
 
-type ConnectionInfoProps = { connection: Site["connection"]; lastSeen?: Site["last_seen"] };
+type ConnectionInfoProps = { connection: Stats["connection"]; lastSeen?: Stats["last_seen"] };
 
-const ConnectionInfo = ({ connection, lastSeen }: ConnectionInfoProps) => (
-  <>
-    <TooltipButton
-      iconName=""
-      message={
-        connection === "unknown" ? (
-          "Haven't received a heartbeat from this region yet"
-        ) : connection === "stable" ? (
-          "Received a heartbeat in the expected interval of 5 minutes"
-        ) : (
-          <>
-            Haven't received a heartbeat in the expected interval of 5 minutes.
-            <br />
-            <ExternalLink to={docsUrls.troubleshooting}>
-              Check the documentation for troubleshooting steps.
-            </ExternalLink>
-          </>
-        )
-      }
-      position="btm-center"
-    >
-      <div className={classNames("connection__text", "status-icon", get(connectionIcons, connection))}>
-        {get(connectionLabels, connection)}
+const getLastSeenText = ({ connection, lastSeen }: ConnectionInfoProps) => {
+  if (!lastSeen) {
+    return null;
+  }
+  return connection === "unknown" ? `heartbeat since ${formatDistanceToNow(lastSeen)}` : formatDistanceToNow(lastSeen);
+};
+
+const ConnectionInfo = ({ connection, lastSeen }: ConnectionInfoProps) => {
+  return (
+    <>
+      <TooltipButton
+        iconName=""
+        message={
+          connection === "unknown" ? (
+            "Haven't received a heartbeat from this region yet"
+          ) : connection === "stable" ? (
+            "Received a heartbeat in the expected interval of 5 minutes"
+          ) : (
+            <>
+              Haven't received a heartbeat in the expected interval of 5 minutes.
+              <br />
+              <ExternalLink to={docsUrls.troubleshooting}>
+                Check the documentation for troubleshooting steps.
+              </ExternalLink>
+            </>
+          )
+        }
+        position="btm-center"
+      >
+        <div className={classNames("connection__text", "status-icon", get(connectionIcons, connection))}>
+          {get(connectionLabels, connection)}
+        </div>
+      </TooltipButton>
+      <div className="connection__text u-text--muted">
+        <time dateTime={lastSeen}>{getLastSeenText({ connection, lastSeen })}</time>
       </div>
-    </TooltipButton>
-    <div className="connection__text u-text--muted">{lastSeen}</div>
-  </>
-);
+    </>
+  );
+};
+
 export default ConnectionInfo;
diff --git a/frontend/src/components/SitesList/SitesTable/SitesTable.tsx b/frontend/src/components/SitesList/SitesTable/SitesTable.tsx
index 4d214ed..f27ea4a 100644
--- a/frontend/src/components/SitesList/SitesTable/SitesTable.tsx
+++ b/frontend/src/components/SitesList/SitesTable/SitesTable.tsx
@@ -2,11 +2,12 @@ import { useEffect, useMemo } from "react";
 
 import { useReactTable, flexRender, getCoreRowModel } from "@tanstack/react-table";
 import type { ColumnDef, Column, Getter, Row } from "@tanstack/react-table";
+import classNames from "classnames";
 import pick from "lodash/fp/pick";
 import useLocalStorageState from "use-local-storage-state";
 
 import AggregatedStats from "./AggregatedStatus";
-import ConnectionInfo from "./ConnectionInfo/ConnectionInfo";
+import ConnectionInfo from "./ConnectionInfo";
 import SitesTableControls from "./SitesTableControls/SitesTableControls";
 
 import type { SitesQueryResult } from "@/api/types";
@@ -123,7 +124,7 @@ const SitesTable = ({
       },
       {
         id: "time",
-        accessorFn: createAccessor("timezone"),
+        accessorFn: createAccessor(["timezone"]),
         header: () => (
           <>
             <div>local time (24hr)</div>
@@ -148,7 +149,7 @@ const SitesTable = ({
         ),
         cell: ({ getValue }) => {
           const { stats } = getValue();
-          return getAllMachines(stats);
+          return stats ? getAllMachines(stats) : null;
         },
       },
       {
@@ -191,8 +192,6 @@ const SitesTable = ({
     enableRowSelection: true,
     enableMultiRowSelection: true,
     onRowSelectionChange: setRowSelection,
-    enableColumnResizing: false,
-    columnResizeMode: "onChange",
     getCoreRowModel: getCoreRowModel(),
     debugTable: isDev,
     debugHeaders: isDev,
@@ -215,13 +214,6 @@ const SitesTable = ({
                 return (
                   <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
-                        className={`resizer ${header.column.getIsResizing() ? "isResizing" : ""}`}
-                        onMouseDown={header.getResizeHandler()}
-                        onTouchStart={header.getResizeHandler()}
-                      ></div>
-                    )}
                   </th>
                 );
               })}
@@ -236,7 +228,10 @@ const SitesTable = ({
           <tbody>
             {table.getRowModel().rows.map((row) => {
               return (
-                <tr key={row.id}>
+                <tr
+                  className={classNames({ "sites-table-row--muted": row.original.stats?.connection === "unknown" })}
+                  key={row.id}
+                >
                   {row.getVisibleCells().map((cell) => {
                     return (
                       <td className={`${cell.column.id}`} key={cell.id}>
diff --git a/frontend/src/components/SitesList/SitesTable/_SitesTable.scss b/frontend/src/components/SitesList/SitesTable/_SitesTable.scss
index 5543712..35397ae 100644
--- a/frontend/src/components/SitesList/SitesTable/_SitesTable.scss
+++ b/frontend/src/components/SitesList/SitesTable/_SitesTable.scss
@@ -1,20 +1,27 @@
+$connection-status-icon-width: 1.5rem;
+
 .sites-table {
   thead th:first-child {
     width: 3rem;
   }
+  td.connection,
   th.connection {
-    padding-left: 1.5rem;
-  }
-  td.connection {
-    padding-left: 0;
     .connection__text {
-      padding-left: 1.5rem;
+      padding-left: $connection-status-icon-width;
+    }
+  }
+  .sites-table-row--muted {
+    td:not(.name) {
+      &,
+      .tooltip-button {
+        @extend %muted-text;
+      }
     }
   }
   .status-icon {
     display: inline-block;
     position: relative;
-    padding-left: 1.5rem;
+    padding-left: $connection-status-icon-width;
   }
   .status-icon::before {
     content: "\00B7";
diff --git a/frontend/src/components/TokensList/components/TokensTable/TokensTable.tsx b/frontend/src/components/TokensList/components/TokensTable/TokensTable.tsx
index a951f66..f4c96cd 100644
--- a/frontend/src/components/TokensList/components/TokensTable/TokensTable.tsx
+++ b/frontend/src/components/TokensList/components/TokensTable/TokensTable.tsx
@@ -135,13 +135,6 @@ const TokensTable = ({
             {headerGroup.headers.map((header) => (
               <th colSpan={header.colSpan} key={header.id}>
                 {header.isPlaceholder ? null : flexRender(header.column.columnDef.header, header.getContext())}
-                {header.column.getCanResize() && (
-                  <div
-                    className={`resizer ${header.column.getIsResizing() ? "isResizing" : ""}`}
-                    onMouseDown={header.getResizeHandler()}
-                    onTouchStart={header.getResizeHandler()}
-                  ></div>
-                )}
               </th>
             ))}
           </tr>
diff --git a/frontend/src/components/base/TooltipButton/TooltipButton.tsx b/frontend/src/components/base/TooltipButton/TooltipButton.tsx
index 8d05795..97176d6 100644
--- a/frontend/src/components/base/TooltipButton/TooltipButton.tsx
+++ b/frontend/src/components/base/TooltipButton/TooltipButton.tsx
@@ -25,7 +25,7 @@ const TooltipButton = ({
       <Button
         appearance="link"
         aria-label={ariaLabel}
-        className="tooltip-button u-no-border u-no-line-height u-no-margin"
+        className="tooltip-button u-no-border u-no-padding u-no-margin u-align--left"
         hasIcon
         type="button"
         {...buttonProps}
diff --git a/frontend/src/mocks/factories.ts b/frontend/src/mocks/factories.ts
index 7121d20..c6903ea 100644
--- a/frontend/src/mocks/factories.ts
+++ b/frontend/src/mocks/factories.ts
@@ -1,28 +1,30 @@
 import Chance from "chance";
+import { sub } from "date-fns";
 import { Factory } from "fishery";
 import { uniqueNamesGenerator, adjectives, colors, animals } from "unique-names-generator";
 
-import type { AccessToken, EnrollmentRequest, PaginatedQueryResult, Site, Token } from "@/api/types";
+import type { AccessToken, EnrollmentRequest, PaginatedQueryResult, Site, Stats, Token } from "@/api/types";
 
-export const connections: Site["connection"][] = ["stable", "lost", "unknown"];
+export const connections: Stats["connection"][] = ["stable", "lost", "unknown"];
 
 export const statsFactory = Factory.define<Site["stats"]>(({ sequence }) => {
   const chance = new Chance(`maas-${sequence}`);
+  const now = new Date();
   return {
     deployed_machines: chance.integer({ min: 0, max: 500 }),
     allocated_machines: chance.integer({ min: 0, max: 500 }),
     ready_machines: chance.integer({ min: 0, max: 500 }),
     error_machines: chance.integer({ min: 0, max: 500 }),
-    last_seen: new Date(chance.date({ year: 2023 })).toISOString(),
+    last_seen: new Date(chance.date({ min: sub(now, { minutes: 15 }), max: now })).toISOString(),
     connection: connectionFactory.build(),
   };
 });
 
-export const connectionFactory = Factory.define<Site["connection"]>(({ sequence }) => {
+export const connectionFactory = Factory.define<Stats["connection"]>(({ sequence }) => {
   return uniqueNamesGenerator({
     dictionaries: [connections],
     seed: sequence,
-  }) as Site["connection"];
+  }) as Stats["connection"];
 });
 
 export const siteFactory = Factory.define<Site>(({ sequence }) => {
@@ -38,8 +40,6 @@ export const siteFactory = Factory.define<Site>(({ sequence }) => {
     id: `${sequence}`,
     name,
     url: `http://${name}.${chance.tld()}`,
-    connection: connectionFactory.build(),
-    last_seen: new Date(chance.date({ year: 2023 })).toISOString(),
     country: chance.country(), // <alpha2 country code>,
     city: chance.city(),
     zip: chance.zip(),
diff --git a/frontend/src/utils.ts b/frontend/src/utils.ts
index e5f5a0c..5e3be93 100644
--- a/frontend/src/utils.ts
+++ b/frontend/src/utils.ts
@@ -1,5 +1,5 @@
 import * as Sentry from "@sentry/browser";
-import { parseISO } from "date-fns";
+import { formatDistanceToNowStrict, parseISO } from "date-fns";
 import { getTimezoneOffset, format, utcToZonedTime } from "date-fns-tz";
 import * as countries from "i18n-iso-countries";
 import { getName } from "i18n-iso-countries";
@@ -36,6 +36,11 @@ export const customParamSerializer = (params: Record<string, string | number>, q
   );
 };
 
+export const formatDistanceToNow = (dateString: string) =>
+  formatDistanceToNowStrict(parseISO(dateString), {
+    addSuffix: true,
+  });
+
 export const getTimezoneUTCString = (timezone: string, date?: Date | number) => {
   const offset = getTimezoneOffset(timezone, date);
   const sign = offset < 0 ? "-" : "+";
@@ -70,7 +75,7 @@ export const copyToClipboard = (text: string, callback?: (text: string) => void)
     });
 };
 
-export const getAllMachines = (stats?: Stats) => {
+export const getAllMachines = (stats: Stats) => {
   if (!stats) return null;
   return stats.deployed_machines + stats.allocated_machines + stats.ready_machines + stats.error_machines;
 };
-- 
Mailing list: https://launchpad.net/~sts-sponsors
Post to     : sts-sponsors@lists.launchpad.net
Unsubscribe : https://launchpad.net/~sts-sponsors
More help   : https://help.launchpad.net/ListHelp

Reply via email to