sts-sponsors team mailing list archive
-
sts-sponsors team
-
Mailing list archive
-
Message #07653
[Merge] ~petermakowski/maas-site-manager:connection-column-MAASENG-1557 into maas-site-manager:main
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;
};