sts-sponsors team mailing list archive
-
sts-sponsors team
-
Mailing list archive
-
Message #07666
[Merge] ~petermakowski/maas-site-manager:fix-validation-onSubmit-MAASENG-1571 into maas-site-manager:main
Peter Makowski has proposed merging ~petermakowski/maas-site-manager:fix-validation-onSubmit-MAASENG-1571 into maas-site-manager:main.
Commit message:
fix validation onSubmit MAASENG-1571
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/441949
## QA Steps
Go to /sites
Select a site
Click "Remove"
Click on the text input field
Click cancel
The side panel should close
Click "Remove" again
Press "Esc" button
The side panel should close
Go to /settings/tokens
Click "Generate tokens"
Click cancel
The side panel should close
--
Your team MAAS Committers is requested to review the proposed merge of ~petermakowski/maas-site-manager:fix-validation-onSubmit-MAASENG-1571 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/RemoveRegions/RemoveRegions.test.tsx b/frontend/src/components/RemoveRegions/RemoveRegions.test.tsx
index e5414c3..5b1c6ef 100644
--- a/frontend/src/components/RemoveRegions/RemoveRegions.test.tsx
+++ b/frontend/src/components/RemoveRegions/RemoveRegions.test.tsx
@@ -19,10 +19,18 @@ it("if the correct phrase has been entered the 'Remove' button becomes enabled."
expect(screen.getByRole("button", { name: /Remove/i })).toBeEnabled();
});
-it("if the confirmation string is not correct and the user unfoxuses the input field a error state is shown.", async () => {
+it("if the confirmation string is not correct and the user unfocuses the input field a error state is shown.", async () => {
render(<RemoveRegions />);
expect(screen.getByRole("button", { name: /Remove/i })).toBeDisabled();
await userEvent.type(screen.getByRole("textbox"), "incorrect string{tab}");
expect(screen.getByText(/Confirmation string is not correct/i)).toBeInTheDocument();
expect(screen.getByRole("button", { name: /Remove/i })).toBeDisabled();
});
+
+it("does not display error message on blur if the value has not chagned", async () => {
+ render(<RemoveRegions />);
+ expect(screen.getByRole("button", { name: /Remove/i })).toBeDisabled();
+ await userEvent.type(screen.getByRole("textbox"), "{tab}");
+ expect(screen.queryByText(/Confirmation string is not correct/i)).not.toBeInTheDocument();
+ expect(screen.getByRole("button", { name: /Remove/i })).toBeDisabled();
+});
diff --git a/frontend/src/components/RemoveRegions/RemoveRegions.tsx b/frontend/src/components/RemoveRegions/RemoveRegions.tsx
index 4136519..1f6f32a 100644
--- a/frontend/src/components/RemoveRegions/RemoveRegions.tsx
+++ b/frontend/src/components/RemoveRegions/RemoveRegions.tsx
@@ -60,6 +60,7 @@ const RemoveRegions = () => {
initialValues={initialValues}
onSubmit={handleSubmit}
validate={createHandleValidate({ expectedConfirmTextValue })}
+ validateOnBlur={false}
>
{({ isSubmitting, errors, touched, isValid, dirty }) => (
<Form aria-labelledby={headingId} className="tokens-create" noValidate>
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/TokensCreate/TokensCreate.test.tsx b/frontend/src/components/TokensCreate/TokensCreate.test.tsx
index 2519f7f..4665b36 100644
--- a/frontend/src/components/TokensCreate/TokensCreate.test.tsx
+++ b/frontend/src/components/TokensCreate/TokensCreate.test.tsx
@@ -29,46 +29,54 @@ afterAll(() => {
mockServer.close();
});
-describe("TokensCreate", () => {
- it("renders the form", async () => {
- renderWithMemoryRouter(<TokensCreate />);
- expect(screen.getByRole("form", { name: /Generate new enrolment tokens/i })).toBeInTheDocument();
- });
+it("renders the form", async () => {
+ renderWithMemoryRouter(<TokensCreate />);
+ expect(screen.getByRole("form", { name: /Generate new enrolment tokens/i })).toBeInTheDocument();
+});
- it("if not all required fields have been entered the submit button is disabled", async () => {
- renderWithMemoryRouter(<TokensCreate />);
- const amount = screen.getByLabelText(/Amount of tokens to generate/i);
- const expires = screen.getByLabelText(/Expiration time/i);
- expect(screen.getByRole("button", { name: /Generate tokens/i })).toBeDisabled();
- await userEvent.type(amount, "1");
- await userEvent.type(expires, "1 month");
- expect(screen.getByRole("button", { name: /Generate tokens/i })).toBeEnabled();
- });
+it("if not all required fields have been entered the submit button is disabled", async () => {
+ renderWithMemoryRouter(<TokensCreate />);
+ const amount = screen.getByLabelText(/Amount of tokens to generate/i);
+ const expires = screen.getByLabelText(/Expiration time/i);
+ expect(screen.getByRole("button", { name: /Generate tokens/i })).toBeDisabled();
+ await userEvent.type(amount, "1");
+ await userEvent.type(expires, "1 month");
+ expect(screen.getByRole("button", { name: /Generate tokens/i })).toBeEnabled();
+});
- it("displays an error for invalid expiration value", async () => {
- renderWithMemoryRouter(<TokensCreate />);
- const expires = screen.getByLabelText(/Expiration time/i);
- await userEvent.type(expires, "2");
- await userEvent.tab();
- expect(expires).toHaveErrorMessage(
- /Time unit must be a `string` type with a value of weeks, days, hours, and\/or minutes./i,
- );
- });
+it("displays an error for invalid expiration value", async () => {
+ renderWithMemoryRouter(<TokensCreate />);
+ const expires = screen.getByLabelText(/Expiration time/i);
+ await userEvent.type(expires, "2");
+ await userEvent.tab();
+ expect(expires).toHaveErrorMessage(
+ /Time unit must be a `string` type with a value of weeks, days, hours, and\/or minutes./i,
+ );
+});
- it("can generate enrolment tokens", async () => {
- renderWithMemoryRouter(<TokensCreate />);
- const amount = screen.getByLabelText(/Amount of tokens to generate/i);
- const expires = screen.getByLabelText(/Expiration time/i);
- expect(screen.getByRole("button", { name: /Generate tokens/i })).toBeDisabled();
- // can specify the number of tokens to generate
- await userEvent.type(amount, "1");
- // can specify the token expiration time (e.g. 1 week)
- await userEvent.type(expires, "1 week");
- await userEvent.click(screen.getByRole("button", { name: /Generate tokens/i }));
- expect(tokensMutationMock).toHaveBeenCalledTimes(1);
- expect(tokensMutationMock).toHaveBeenCalledWith({
- amount: 1,
- expires: "P0Y0M7DT0H0M0S",
- });
+it("can generate enrolment tokens", async () => {
+ renderWithMemoryRouter(<TokensCreate />);
+ const amount = screen.getByLabelText(/Amount of tokens to generate/i);
+ const expires = screen.getByLabelText(/Expiration time/i);
+ expect(screen.getByRole("button", { name: /Generate tokens/i })).toBeDisabled();
+ // can specify the number of tokens to generate
+ await userEvent.type(amount, "1");
+ // can specify the token expiration time (e.g. 1 week)
+ await userEvent.type(expires, "1 week");
+ await userEvent.click(screen.getByRole("button", { name: /Generate tokens/i }));
+ expect(tokensMutationMock).toHaveBeenCalledTimes(1);
+ expect(tokensMutationMock).toHaveBeenCalledWith({
+ amount: 1,
+ expires: "P0Y0M7DT0H0M0S",
});
});
+
+it("does not display error message on blur if the value has not chagned", async () => {
+ renderWithMemoryRouter(<TokensCreate />);
+ const amount = screen.getByLabelText(/Amount of tokens to generate/i);
+ await userEvent.type(amount, "{tab}");
+ expect(amount).not.toHaveErrorMessage(/Error/i);
+ // enter a value and then delete it
+ await userEvent.type(amount, "1{backspace}");
+ expect(amount).toHaveErrorMessage(/Error/i);
+});
diff --git a/frontend/src/components/TokensCreate/TokensCreate.tsx b/frontend/src/components/TokensCreate/TokensCreate.tsx
index 2443c7b..4f8e32d 100644
--- a/frontend/src/components/TokensCreate/TokensCreate.tsx
+++ b/frontend/src/components/TokensCreate/TokensCreate.tsx
@@ -1,6 +1,7 @@
import { useId } from "react";
import { Button, Input, Label, Notification } from "@canonical/react-components";
+import type { FormikHelpers } from "formik";
import { Field, Formik, Form } from "formik";
import * as Yup from "yup";
@@ -44,7 +45,7 @@ const TokensCreate = () => {
const { setSidebar } = useAppContext();
const handleSubmit = async (
{ amount, expires }: TokensCreateFormValues,
- { setSubmitting }: { setSubmitting: (isSubmitting: boolean) => void },
+ { setSubmitting }: FormikHelpers<TokensCreateFormValues>,
) => {
await tokensMutation.mutateAsync({
amount: Number(amount),
@@ -64,7 +65,12 @@ const TokensCreate = () => {
{tokensMutation.isError && (
<Notification severity="negative">There was an error generating the token(s).</Notification>
)}
- <Formik initialValues={initialValues} onSubmit={handleSubmit} validationSchema={TokensCreateSchema}>
+ <Formik
+ initialValues={initialValues}
+ onSubmit={handleSubmit}
+ validateOnBlur={false}
+ validationSchema={TokensCreateSchema}
+ >
{({ isSubmitting, errors, touched, isValid, dirty }) => (
<Form aria-labelledby={headingId} className="tokens-create" noValidate>
<Label htmlFor={amountId}>Amount of tokens to generate</Label>
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;
};