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