From 0e83de26979742ed266248b02b1d5d7be05a9ebf Mon Sep 17 00:00:00 2001 From: Teffen Ellis <592134+GirlBossRush@users.noreply.github.com> Date: Mon, 7 Apr 2025 20:37:03 +0200 Subject: [PATCH] web: Tidy temporal utilities. (#13755) --- .../admin-overview/cards/RecentEventsCard.ts | 4 +- web/src/admin/blueprints/BlueprintListPage.ts | 4 +- .../enterprise/EnterpriseLicenseListPage.ts | 4 +- web/src/admin/events/EventListPage.ts | 4 +- web/src/admin/events/EventViewPage.ts | 4 +- web/src/admin/groups/MemberSelectModal.ts | 4 +- web/src/admin/groups/RelatedUserList.ts | 4 +- web/src/admin/outposts/OutpostHealth.ts | 4 +- web/src/admin/outposts/OutpostHealthSimple.ts | 4 +- .../policies/reputation/ReputationListPage.ts | 4 +- .../admin/stages/invitation/InvitationForm.ts | 3 +- .../admin/system-tasks/SystemTaskListPage.ts | 6 +- web/src/admin/tokens/TokenForm.ts | 3 +- web/src/admin/tokens/TokenListPage.ts | 4 +- web/src/admin/users/ServiceAccountForm.ts | 2 +- web/src/admin/users/UserDevicesTable.ts | 8 +- web/src/admin/users/UserListPage.ts | 4 +- web/src/admin/users/UserViewPage.ts | 6 +- web/src/common/temporal.ts | 132 ++++++++++++++++++ web/src/common/utils.ts | 54 ------- web/src/components/events/ObjectChangelog.ts | 4 +- web/src/components/events/UserEvents.ts | 4 +- web/src/elements/charts/Chart.ts | 4 +- web/src/elements/events/LogViewer.ts | 4 +- web/src/elements/forms/Form.ts | 3 +- web/src/elements/notifications/APIDrawer.ts | 4 +- .../notifications/NotificationDrawer.ts | 4 +- web/src/elements/oauth/UserAccessTokenList.ts | 4 +- .../elements/oauth/UserRefreshTokenList.ts | 4 +- web/src/elements/sync/SyncStatusCard.ts | 4 +- web/src/elements/user/SessionList.ts | 6 +- web/src/elements/user/UserConsentList.ts | 4 +- web/src/elements/user/UserReputationList.ts | 4 +- .../user/user-settings/mfa/MFADevicesPage.ts | 6 +- .../user-settings/tokens/UserTokenForm.ts | 2 +- .../user-settings/tokens/UserTokenList.ts | 4 +- 36 files changed, 204 insertions(+), 123 deletions(-) create mode 100644 web/src/common/temporal.ts diff --git a/web/src/admin/admin-overview/cards/RecentEventsCard.ts b/web/src/admin/admin-overview/cards/RecentEventsCard.ts index b5cd658d62..50978de655 100644 --- a/web/src/admin/admin-overview/cards/RecentEventsCard.ts +++ b/web/src/admin/admin-overview/cards/RecentEventsCard.ts @@ -2,7 +2,7 @@ import { EventGeo, EventUser } from "@goauthentik/admin/events/utils"; import { DEFAULT_CONFIG } from "@goauthentik/common/api/config"; import { EventWithContext } from "@goauthentik/common/events"; import { actionToLabel } from "@goauthentik/common/labels"; -import { getRelativeTime } from "@goauthentik/common/utils"; +import { formatElapsedTime } from "@goauthentik/common/temporal"; import "@goauthentik/components/ak-event-info"; import "@goauthentik/elements/Tabs"; import "@goauthentik/elements/buttons/Dropdown"; @@ -74,7 +74,7 @@ export class RecentEventsCard extends Table { html`
${actionToLabel(item.action)}
${item.app}`, EventUser(item), - html`
${getRelativeTime(item.created)}
+ html`
${formatElapsedTime(item.created)}
${item.created.toLocaleString()}`, html`
${item.clientIp || msg("-")}
${EventGeo(item)}`, diff --git a/web/src/admin/blueprints/BlueprintListPage.ts b/web/src/admin/blueprints/BlueprintListPage.ts index 6317fb32cb..ff8d1f9b94 100644 --- a/web/src/admin/blueprints/BlueprintListPage.ts +++ b/web/src/admin/blueprints/BlueprintListPage.ts @@ -2,7 +2,7 @@ import "@goauthentik/admin/blueprints/BlueprintForm"; import "@goauthentik/admin/rbac/ObjectPermissionModal"; import { DEFAULT_CONFIG } from "@goauthentik/common/api/config"; import { EVENT_REFRESH } from "@goauthentik/common/constants"; -import { getRelativeTime } from "@goauthentik/common/utils"; +import { formatElapsedTime } from "@goauthentik/common/temporal"; import "@goauthentik/components/ak-status-label"; import "@goauthentik/elements/buttons/ActionButton"; import "@goauthentik/elements/buttons/SpinnerButton"; @@ -141,7 +141,7 @@ export class BlueprintListPage extends TablePage { html`
${item.name}
${description ? html`${description}` : html``}`, html`${BlueprintStatus(item)}`, - html`
${getRelativeTime(item.lastApplied)}
+ html`
${formatElapsedTime(item.lastApplied)}
${item.lastApplied.toLocaleString()}`, html``, html` diff --git a/web/src/admin/enterprise/EnterpriseLicenseListPage.ts b/web/src/admin/enterprise/EnterpriseLicenseListPage.ts index 67960ae320..1d326c6b22 100644 --- a/web/src/admin/enterprise/EnterpriseLicenseListPage.ts +++ b/web/src/admin/enterprise/EnterpriseLicenseListPage.ts @@ -2,7 +2,7 @@ import "@goauthentik/admin/enterprise/EnterpriseLicenseForm"; import "@goauthentik/admin/enterprise/EnterpriseStatusCard"; import "@goauthentik/admin/rbac/ObjectPermissionModal"; import { DEFAULT_CONFIG } from "@goauthentik/common/api/config"; -import { getRelativeTime } from "@goauthentik/common/utils"; +import { formatElapsedTime } from "@goauthentik/common/temporal"; import { PFColor } from "@goauthentik/elements/Label"; import "@goauthentik/elements/Spinner"; import "@goauthentik/elements/buttons/SpinnerButton"; @@ -186,7 +186,7 @@ export class EnterpriseLicenseListPage extends TablePage { > ${this.summary && this.summary?.status !== LicenseSummaryStatusEnum.Unlicensed - ? html`
${getRelativeTime(this.summary.latestValid)}
+ ? html`
${formatElapsedTime(this.summary.latestValid)}
${this.summary.latestValid.toLocaleString()}` : "-"} diff --git a/web/src/admin/events/EventListPage.ts b/web/src/admin/events/EventListPage.ts index e62b5767aa..300be05c69 100644 --- a/web/src/admin/events/EventListPage.ts +++ b/web/src/admin/events/EventListPage.ts @@ -3,7 +3,7 @@ import { EventGeo, EventUser } from "@goauthentik/admin/events/utils"; import { DEFAULT_CONFIG } from "@goauthentik/common/api/config"; import { EventWithContext } from "@goauthentik/common/events"; import { actionToLabel } from "@goauthentik/common/labels"; -import { getRelativeTime } from "@goauthentik/common/utils"; +import { formatElapsedTime } from "@goauthentik/common/temporal"; import "@goauthentik/components/ak-event-info"; import { PaginatedResponse } from "@goauthentik/elements/table/Table"; import { TableColumn } from "@goauthentik/elements/table/Table"; @@ -78,7 +78,7 @@ export class EventListPage extends TablePage { html`
${actionToLabel(item.action)}
${item.app}`, EventUser(item), - html`
${getRelativeTime(item.created)}
+ html`
${formatElapsedTime(item.created)}
${item.created.toLocaleString()}`, html`
${item.clientIp || msg("-")}
${EventGeo(item)}`, diff --git a/web/src/admin/events/EventViewPage.ts b/web/src/admin/events/EventViewPage.ts index 9c8d4fda9d..ca24b4cfc5 100644 --- a/web/src/admin/events/EventViewPage.ts +++ b/web/src/admin/events/EventViewPage.ts @@ -2,7 +2,7 @@ import { EventGeo, EventUser } from "@goauthentik/admin/events/utils"; import { DEFAULT_CONFIG } from "@goauthentik/common/api/config"; import { EventWithContext } from "@goauthentik/common/events"; import { actionToLabel } from "@goauthentik/common/labels"; -import { getRelativeTime } from "@goauthentik/common/utils"; +import { formatElapsedTime } from "@goauthentik/common/temporal"; import "@goauthentik/components/ak-event-info"; import { AKElement } from "@goauthentik/elements/Base"; import "@goauthentik/elements/PageHeader"; @@ -104,7 +104,7 @@ export class EventViewPage extends AKElement {
-
${getRelativeTime(this.event.created)}
+
${formatElapsedTime(this.event.created)}
${this.event.created.toLocaleString()}
diff --git a/web/src/admin/groups/MemberSelectModal.ts b/web/src/admin/groups/MemberSelectModal.ts index 2843eb6405..cf925b74e9 100644 --- a/web/src/admin/groups/MemberSelectModal.ts +++ b/web/src/admin/groups/MemberSelectModal.ts @@ -1,5 +1,5 @@ import { DEFAULT_CONFIG } from "@goauthentik/common/api/config"; -import { getRelativeTime } from "@goauthentik/common/utils"; +import { formatElapsedTime } from "@goauthentik/common/temporal"; import "@goauthentik/components/ak-status-label"; import "@goauthentik/elements/buttons/SpinnerButton"; import { PaginatedResponse } from "@goauthentik/elements/table/Table"; @@ -105,7 +105,7 @@ export class MemberSelectTable extends TableModal { ${item.name}`, html` `, html`${item.lastLogin - ? html`
${getRelativeTime(item.lastLogin)}
+ ? html`
${formatElapsedTime(item.lastLogin)}
${item.lastLogin.toLocaleString()}` : msg("-")}`, ]; diff --git a/web/src/admin/groups/RelatedUserList.ts b/web/src/admin/groups/RelatedUserList.ts index 65158e10c9..3942c7a98c 100644 --- a/web/src/admin/groups/RelatedUserList.ts +++ b/web/src/admin/groups/RelatedUserList.ts @@ -8,8 +8,8 @@ import { DEFAULT_CONFIG } from "@goauthentik/common/api/config"; import { PFSize } from "@goauthentik/common/enums.js"; import { parseAPIResponseError, pluckErrorDetail } from "@goauthentik/common/errors/network"; import { MessageLevel } from "@goauthentik/common/messages"; +import { formatElapsedTime } from "@goauthentik/common/temporal"; import { me } from "@goauthentik/common/users"; -import { getRelativeTime } from "@goauthentik/common/utils"; import "@goauthentik/components/ak-status-label"; import { WithBrandConfig } from "@goauthentik/elements/Interface/brandProvider"; import { @@ -194,7 +194,7 @@ export class RelatedUserList extends WithBrandConfig(WithCapabilitiesConfig(Tabl `, html``, html`${item.lastLogin - ? html`
${getRelativeTime(item.lastLogin)}
+ ? html`
${formatElapsedTime(item.lastLogin)}
${item.lastLogin.toLocaleString()}` : msg("-")}`, html` diff --git a/web/src/admin/outposts/OutpostHealth.ts b/web/src/admin/outposts/OutpostHealth.ts index a8c63fc283..f460b0e91e 100644 --- a/web/src/admin/outposts/OutpostHealth.ts +++ b/web/src/admin/outposts/OutpostHealth.ts @@ -1,4 +1,4 @@ -import { getRelativeTime } from "@goauthentik/common/utils"; +import { formatElapsedTime } from "@goauthentik/common/temporal"; import { AKElement } from "@goauthentik/elements/Base"; import { PFColor } from "@goauthentik/elements/Label"; import "@goauthentik/elements/Spinner"; @@ -51,7 +51,7 @@ export class OutpostHealthElement extends AKElement {
${msg( - str`${getRelativeTime(this.outpostHealth.lastSeen)} (${this.outpostHealth.lastSeen?.toLocaleTimeString()})`, + str`${formatElapsedTime(this.outpostHealth.lastSeen)} (${this.outpostHealth.lastSeen?.toLocaleTimeString()})`, )}
diff --git a/web/src/admin/outposts/OutpostHealthSimple.ts b/web/src/admin/outposts/OutpostHealthSimple.ts index 51f0061d8a..ed5ae5548a 100644 --- a/web/src/admin/outposts/OutpostHealthSimple.ts +++ b/web/src/admin/outposts/OutpostHealthSimple.ts @@ -1,6 +1,6 @@ import { DEFAULT_CONFIG } from "@goauthentik/common/api/config"; import { EVENT_REFRESH } from "@goauthentik/common/constants"; -import { getRelativeTime } from "@goauthentik/common/utils"; +import { formatElapsedTime } from "@goauthentik/common/temporal"; import { AKElement } from "@goauthentik/elements/Base"; import { PFColor } from "@goauthentik/elements/Label"; import "@goauthentik/elements/Spinner"; @@ -69,7 +69,7 @@ export class OutpostHealthSimpleElement extends AKElement { const lastSeen = this.outpostHealths[0].lastSeen; return html` ${msg( - str`Last seen: ${getRelativeTime(lastSeen)} (${lastSeen.toLocaleTimeString()})`, + str`Last seen: ${formatElapsedTime(lastSeen)} (${lastSeen.toLocaleTimeString()})`, )}`; } diff --git a/web/src/admin/policies/reputation/ReputationListPage.ts b/web/src/admin/policies/reputation/ReputationListPage.ts index 769bcfb5dd..62028cb4b8 100644 --- a/web/src/admin/policies/reputation/ReputationListPage.ts +++ b/web/src/admin/policies/reputation/ReputationListPage.ts @@ -1,6 +1,6 @@ import "@goauthentik/admin/rbac/ObjectPermissionModal"; import { DEFAULT_CONFIG } from "@goauthentik/common/api/config"; -import { getRelativeTime } from "@goauthentik/common/utils"; +import { formatElapsedTime } from "@goauthentik/common/temporal"; import "@goauthentik/elements/buttons/ModalButton"; import "@goauthentik/elements/buttons/SpinnerButton"; import "@goauthentik/elements/forms/DeleteBulkForm"; @@ -89,7 +89,7 @@ export class ReputationListPage extends TablePage { : html``} ${item.ip}`, html`${item.score}`, - html`
${getRelativeTime(item.updated)}
+ html`
${formatElapsedTime(item.updated)}
${item.updated.toLocaleString()}`, html` { item.expires || new Date() ).toLocaleString()} > - ${getRelativeTime(item.expires || new Date())} + ${formatElapsedTime(item.expires || new Date())} ` : msg("-")} @@ -128,7 +128,7 @@ export class SystemTaskListPage extends TablePage { return [ html`
${item.name}${item.uid ? `:${item.uid}` : ""}
`, html`${item.description}`, - html`
${getRelativeTime(item.finishTimestamp)}
+ html`
${formatElapsedTime(item.finishTimestamp)}
${item.finishTimestamp.toLocaleString()}`, this.taskStatus(item), html` { html`${item.userObj?.username}`, html``, html`${item.expires && item.expiring - ? html`
${getRelativeTime(item.expires)}
+ ? html`
${formatElapsedTime(item.expires)}
${item.expires.toLocaleString()}` : msg("-")}`, html`${intentToLabel(item.intent ?? IntentEnum.Api)}`, diff --git a/web/src/admin/users/ServiceAccountForm.ts b/web/src/admin/users/ServiceAccountForm.ts index cabbfa2b0d..3da467f87d 100644 --- a/web/src/admin/users/ServiceAccountForm.ts +++ b/web/src/admin/users/ServiceAccountForm.ts @@ -1,5 +1,5 @@ import { DEFAULT_CONFIG } from "@goauthentik/common/api/config"; -import { dateTimeLocal } from "@goauthentik/common/utils"; +import { dateTimeLocal } from "@goauthentik/common/temporal"; import { Form } from "@goauthentik/elements/forms/Form"; import "@goauthentik/elements/forms/HorizontalFormElement"; import { ModalForm } from "@goauthentik/elements/forms/ModalForm"; diff --git a/web/src/admin/users/UserDevicesTable.ts b/web/src/admin/users/UserDevicesTable.ts index 8ab7bb5c22..fe21970101 100644 --- a/web/src/admin/users/UserDevicesTable.ts +++ b/web/src/admin/users/UserDevicesTable.ts @@ -1,7 +1,7 @@ import { DEFAULT_CONFIG } from "@goauthentik/common/api/config"; import { deviceTypeName } from "@goauthentik/common/labels"; import { SentryIgnoredError } from "@goauthentik/common/sentry"; -import { getRelativeTime } from "@goauthentik/common/utils"; +import { formatElapsedTime } from "@goauthentik/common/temporal"; import "@goauthentik/elements/forms/DeleteBulkForm"; import { PaginatedResponse } from "@goauthentik/elements/table/Table"; import { Table, TableColumn } from "@goauthentik/elements/table/Table"; @@ -108,15 +108,15 @@ export class UserDeviceTable extends Table { ${item.extraDescription ? ` - ${item.extraDescription}` : ""}`, html`${item.confirmed ? msg("Yes") : msg("No")}`, html`${item.created.getTime() > 0 - ? html`
${getRelativeTime(item.created)}
+ ? html`
${formatElapsedTime(item.created)}
${item.created.toLocaleString()}` : html`-`}`, html`${item.lastUpdated - ? html`
${getRelativeTime(item.lastUpdated)}
+ ? html`
${formatElapsedTime(item.lastUpdated)}
${item.lastUpdated.toLocaleString()}` : html`-`}`, html`${item.lastUsed - ? html`
${getRelativeTime(item.lastUsed)}
+ ? html`
${formatElapsedTime(item.lastUsed)}
${item.lastUsed.toLocaleString()}` : html`-`}`, ]; diff --git a/web/src/admin/users/UserListPage.ts b/web/src/admin/users/UserListPage.ts index 596bf94862..a0c8b7da6f 100644 --- a/web/src/admin/users/UserListPage.ts +++ b/web/src/admin/users/UserListPage.ts @@ -10,9 +10,9 @@ import { PFSize } from "@goauthentik/common/enums.js"; import { parseAPIResponseError } from "@goauthentik/common/errors/network"; import { userTypeToLabel } from "@goauthentik/common/labels"; import { MessageLevel } from "@goauthentik/common/messages"; +import { formatElapsedTime } from "@goauthentik/common/temporal"; import { DefaultUIConfig, uiConfig } from "@goauthentik/common/ui/config"; import { me } from "@goauthentik/common/users"; -import { getRelativeTime } from "@goauthentik/common/utils"; import "@goauthentik/components/ak-status-label"; import { rootInterface } from "@goauthentik/elements/Base"; import { WithBrandConfig } from "@goauthentik/elements/Interface/brandProvider"; @@ -244,7 +244,7 @@ export class UserListPage extends WithBrandConfig(WithCapabilitiesConfig(TablePa `, html``, html`${item.lastLogin - ? html`
${getRelativeTime(item.lastLogin)}
+ ? html`
${formatElapsedTime(item.lastLogin)}
${item.lastLogin.toLocaleString()}` : msg("-")}`, html`${userTypeToLabel(item.type)}`, diff --git a/web/src/admin/users/UserViewPage.ts b/web/src/admin/users/UserViewPage.ts index 02890c0c65..eb11589dc7 100644 --- a/web/src/admin/users/UserViewPage.ts +++ b/web/src/admin/users/UserViewPage.ts @@ -15,8 +15,8 @@ import { DEFAULT_CONFIG } from "@goauthentik/common/api/config"; import { EVENT_REFRESH } from "@goauthentik/common/constants"; import { PFSize } from "@goauthentik/common/enums.js"; import { userTypeToLabel } from "@goauthentik/common/labels"; +import { formatElapsedTime } from "@goauthentik/common/temporal"; import { me } from "@goauthentik/common/users"; -import { getRelativeTime } from "@goauthentik/common/utils"; import "@goauthentik/components/DescriptionList"; import { type DescriptionPair, @@ -155,11 +155,11 @@ export class UserViewPage extends WithCapabilitiesConfig(AKElement) { [msg("Name"), user.name], [msg("Email"), user.email || "-"], [msg("Last login"), user.lastLogin - ? html`
${getRelativeTime(user.lastLogin)}
+ ? html`
${formatElapsedTime(user.lastLogin)}
${user.lastLogin.toLocaleString()}` : html`${msg("-")}`], [msg("Last password change"), user.passwordChangeDate - ? html`
${getRelativeTime(user.passwordChangeDate)}
+ ? html`
${formatElapsedTime(user.passwordChangeDate)}
${user.passwordChangeDate.toLocaleString()}` : html`${msg("-")}`], [msg("Active"), html``], diff --git a/web/src/common/temporal.ts b/web/src/common/temporal.ts new file mode 100644 index 0000000000..60767e8717 --- /dev/null +++ b/web/src/common/temporal.ts @@ -0,0 +1,132 @@ +/** + * @file Temporal utilitie for working with dates and times. + */ + +/** + * Duration in milliseconds for time units used by the `Intl.RelativeTimeFormat` API. + */ +export const Duration = { + /** + * The number of milliseconds in a year. + */ + year: 1000 * 60 * 60 * 24 * 365, + /** + * The number of milliseconds in a month. + */ + month: (24 * 60 * 60 * 1000 * 365) / 12, + /** + * The number of milliseconds in a day. + */ + day: 1000 * 60 * 60 * 24, + /** + * The number of milliseconds in an hour. + */ + hour: 1000 * 60 * 60, + /** + * The number of milliseconds in a minute. + */ + minute: 1000 * 60, + /** + * The number of milliseconds in a second. + */ + second: 1000, +} as const satisfies Partial>; + +export type DurationUnit = keyof typeof Duration; + +/** + * The order of time units used by the `Intl.RelativeTimeFormat` API. + */ +const DurationGranularity = [ + "year", + "month", + "day", + "hour", + "minute", + "second", +] as const satisfies DurationUnit[]; + +/** + * Given two dates, return a human-readable string describing the time elapsed between them. + */ +export function formatElapsedTime(d1: Date, d2: Date = new Date()): string { + const elapsed = d1.getTime() - d2.getTime(); + const rtf = new Intl.RelativeTimeFormat("default", { numeric: "auto" }); + + for (const unit of DurationGranularity) { + const duration = Duration[unit]; + + if (Math.abs(elapsed) > duration || unit === "second") { + let rounded = Math.round(elapsed / duration); + + if (!isFinite(rounded)) { + rounded = 0; + } + + return rtf.format(rounded, unit); + } + } + return rtf.format(Math.round(elapsed / 1000), "second"); +} + +/** + * Convert a Date object to a string in the format required by the datetime-local input field. + * + * ```js + * html` + * ``` + * + * @param input - The Date object to convert. + * @returns A string in the format "YYYY-MM-DDTHH:MM" (e.g., "2023-10-01T12:00"). + * @see https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input/datetime-local + * + * @remarks + * + * So for some reason, the datetime-local input field requires ISO Datetime as value. + * + * But the standard`date.toISOString()` returns everything with seconds and milliseconds, + * which the input field doesn't like (on chrome, on firefox its fine) + * + * On chrome, setting .valueAsNumber works, but that causes an error on firefox, so go figure. + * + * Additionally, `toISOString` always returns the date without timezone, + * which we would like to include for better usability + */ +export function dateTimeLocal(input: Date): string { + const tzOffset = new Date().getTimezoneOffset() * 60_000; //offset in milliseconds + const localISOTime = new Date(input.getTime() - tzOffset).toISOString().slice(0, -1); + + const [datePart, timePart] = localISOTime.split(":"); + + return [datePart, timePart].join(":"); +} + +/** + * Convert a Date object to UTC. + * + * @remarks + * + * Sigh...so our API is UTC/can take TZ info in the ISO format as it should. + * + * datetime-local fields (which is almost the only date-time input we use) + * can return its value as a UTC timestamp...however the generated API client + * _requires_ a Date object, only to then convert it to an ISO string anyways + * JS Dates don't include timezone info in the ISO string, so that just sends + * the local time as UTC...which is wrong. + * + * Instead we have to do this, convert the given date to a UTC timestamp, + * then subtract the timezone offset to create an "invalid" date (correct time&date) + * but it still "thinks" it's in local TZ. + */ +export function dateToUTC(input: Date): Date { + const timestamp = input.getTime(); + const offset = -1 * (new Date().getTimezoneOffset() * 60_000); + + return new Date(timestamp - offset); +} diff --git a/web/src/common/utils.ts b/web/src/common/utils.ts index ccf30f6ab3..b4fc8d28ff 100644 --- a/web/src/common/utils.ts +++ b/web/src/common/utils.ts @@ -103,35 +103,6 @@ export function randomString(len: number, charset: string): string { return chars.join(""); } -export function dateTimeLocal(date: Date): string { - // So for some reason, the datetime-local input field requires ISO Datetime as value - // But the standard javascript date.toISOString() returns everything with seconds and - // milliseconds, which the input field doesn't like (on chrome, on firefox its fine) - // On chrome, setting .valueAsNumber works, but that causes an error on firefox, so go - // figure. - // Additionally, toISOString always returns the date without timezone, which we would like - // to include for better usability - const tzOffset = new Date().getTimezoneOffset() * 60000; //offset in milliseconds - const localISOTime = new Date(date.getTime() - tzOffset).toISOString().slice(0, -1); - const parts = localISOTime.split(":"); - return `${parts[0]}:${parts[1]}`; -} - -export function dateToUTC(date: Date): Date { - // Sigh...so our API is UTC/can take TZ info in the ISO format as it should. - // datetime-local fields (which is almost the only date-time input we use) - // can return its value as a UTC timestamp...however the generated API client - // _requires_ a Date object, only to then convert it to an ISO string anyways - // JS Dates don't include timezone info in the ISO string, so that just sends - // the local time as UTC...which is wrong - // Instead we have to do this, convert the given date to a UTC timestamp, - // then subtract the timezone offset to create an "invalid" date (correct time&date) - // but it still "thinks" it's in local TZ - const timestamp = date.getTime(); - const offset = -1 * (new Date().getTimezoneOffset() * 60000); - return new Date(timestamp - offset); -} - // Lit is extremely well-typed with regard to CSS, and Storybook's `build` does not currently have a // coherent way of importing CSS-as-text into CSSStyleSheet. It works well when Storybook is running // in `dev,` but in `build` it fails. Storied components will have to map their textual CSS imports @@ -156,28 +127,3 @@ export function adaptCSS(sheet: AdaptableStylesheet[]): CSSStyleSheet[]; export function adaptCSS(sheet: AdaptableStylesheet | AdaptableStylesheet[]): AdaptedStylesheets { return Array.isArray(sheet) ? sheet.map(_adaptCSS) : _adaptCSS(sheet); } - -export function getRelativeTime(d1: Date, d2: Date = new Date()): string { - const elapsed = d1.getTime() - d2.getTime(); - const rtf = new Intl.RelativeTimeFormat("default", { numeric: "auto" }); - - const _timeUnits: [Intl.RelativeTimeFormatUnit, number][] = [ - ["year", 1000 * 60 * 60 * 24 * 365], - ["month", (24 * 60 * 60 * 1000 * 365) / 12], - ["day", 1000 * 60 * 60 * 24], - ["hour", 1000 * 60 * 60], - ["minute", 1000 * 60], - ["second", 1000], - ]; - - for (const [key, value] of _timeUnits) { - if (Math.abs(elapsed) > value || key === "second") { - let rounded = Math.round(elapsed / value); - if (!isFinite(rounded)) { - rounded = 0; - } - return rtf.format(rounded, key); - } - } - return rtf.format(Math.round(elapsed / 1000), "second"); -} diff --git a/web/src/components/events/ObjectChangelog.ts b/web/src/components/events/ObjectChangelog.ts index e45f3fe66c..34c03462fa 100644 --- a/web/src/components/events/ObjectChangelog.ts +++ b/web/src/components/events/ObjectChangelog.ts @@ -2,7 +2,7 @@ import { EventGeo, EventUser } from "@goauthentik/admin/events/utils"; import { DEFAULT_CONFIG } from "@goauthentik/common/api/config"; import { EventWithContext } from "@goauthentik/common/events"; import { actionToLabel } from "@goauthentik/common/labels"; -import { getRelativeTime } from "@goauthentik/common/utils"; +import { formatElapsedTime } from "@goauthentik/common/temporal"; import "@goauthentik/components/ak-event-info"; import "@goauthentik/elements/Tabs"; import "@goauthentik/elements/buttons/Dropdown"; @@ -73,7 +73,7 @@ export class ObjectChangelog extends Table { return [ html`${actionToLabel(item.action)}`, EventUser(item), - html`
${getRelativeTime(item.created)}
+ html`
${formatElapsedTime(item.created)}
${item.created.toLocaleString()}`, html`
${item.clientIp || msg("-")}
diff --git a/web/src/components/events/UserEvents.ts b/web/src/components/events/UserEvents.ts index 197fcede29..27c369ada1 100644 --- a/web/src/components/events/UserEvents.ts +++ b/web/src/components/events/UserEvents.ts @@ -2,7 +2,7 @@ import { EventUser } from "@goauthentik/admin/events/utils"; import { DEFAULT_CONFIG } from "@goauthentik/common/api/config"; import { EventWithContext } from "@goauthentik/common/events"; import { actionToLabel } from "@goauthentik/common/labels"; -import { getRelativeTime } from "@goauthentik/common/utils"; +import { formatElapsedTime } from "@goauthentik/common/temporal"; import "@goauthentik/components/ak-event-info"; import "@goauthentik/elements/Tabs"; import "@goauthentik/elements/buttons/Dropdown"; @@ -47,7 +47,7 @@ export class UserEvents extends Table { return [ html`${actionToLabel(item.action)}`, EventUser(item), - html`
${getRelativeTime(item.created)}
+ html`
${formatElapsedTime(item.created)}
${item.created.toLocaleString()}`, html`${item.clientIp || msg("-")}`, ]; diff --git a/web/src/elements/charts/Chart.ts b/web/src/elements/charts/Chart.ts index 5119648f80..a0dc56a5b5 100644 --- a/web/src/elements/charts/Chart.ts +++ b/web/src/elements/charts/Chart.ts @@ -4,7 +4,7 @@ import { parseAPIResponseError, pluckErrorDetail, } from "@goauthentik/common/errors/network"; -import { getRelativeTime } from "@goauthentik/common/utils"; +import { formatElapsedTime } from "@goauthentik/common/temporal"; import { AKElement } from "@goauthentik/elements/Base"; import "@goauthentik/elements/EmptyState"; import { @@ -176,7 +176,7 @@ export abstract class AKChart extends AKElement { timeTickCallback(tickValue: string | number, index: number, ticks: Tick[]): string { const valueStamp = ticks[index]; - return getRelativeTime(new Date(valueStamp.value)); + return formatElapsedTime(new Date(valueStamp.value)); } getOptions(): ChartOptions { diff --git a/web/src/elements/events/LogViewer.ts b/web/src/elements/events/LogViewer.ts index 4d9b60a6b7..4a0958a5d1 100644 --- a/web/src/elements/events/LogViewer.ts +++ b/web/src/elements/events/LogViewer.ts @@ -1,4 +1,4 @@ -import { getRelativeTime } from "@goauthentik/common/utils"; +import { formatElapsedTime } from "@goauthentik/common/temporal"; import "@goauthentik/components/ak-status-label"; import "@goauthentik/elements/EmptyState"; import { PaginatedResponse, Table, TableColumn } from "@goauthentik/elements/table/Table"; @@ -102,7 +102,7 @@ export class LogViewer extends Table { row(item: LogEvent): TemplateResult[] { return [ - html`${getRelativeTime(item.timestamp)}`, + html`${formatElapsedTime(item.timestamp)}`, html`${item.path}
- ${getRelativeTime(new Date(item.time))} + ${formatElapsedTime(new Date(item.time))}
`; } diff --git a/web/src/elements/notifications/NotificationDrawer.ts b/web/src/elements/notifications/NotificationDrawer.ts index 871565cc46..cfa58d819b 100644 --- a/web/src/elements/notifications/NotificationDrawer.ts +++ b/web/src/elements/notifications/NotificationDrawer.ts @@ -3,8 +3,8 @@ import { EVENT_NOTIFICATION_DRAWER_TOGGLE, EVENT_REFRESH } from "@goauthentik/co import { globalAK } from "@goauthentik/common/global"; import { actionToLabel } from "@goauthentik/common/labels"; import { MessageLevel } from "@goauthentik/common/messages"; +import { formatElapsedTime } from "@goauthentik/common/temporal"; import { me } from "@goauthentik/common/users"; -import { getRelativeTime } from "@goauthentik/common/utils"; import { AKElement } from "@goauthentik/elements/Base"; import "@goauthentik/elements/EmptyState"; import { showMessage } from "@goauthentik/elements/messages/MessageContainer"; @@ -134,7 +134,7 @@ export class NotificationDrawer extends AKElement {

${item.body}

- ${getRelativeTime(item.created!)} + ${formatElapsedTime(item.created!)} `; diff --git a/web/src/elements/oauth/UserAccessTokenList.ts b/web/src/elements/oauth/UserAccessTokenList.ts index a78ef7c56e..3e0d37b7f8 100644 --- a/web/src/elements/oauth/UserAccessTokenList.ts +++ b/web/src/elements/oauth/UserAccessTokenList.ts @@ -1,5 +1,5 @@ import { DEFAULT_CONFIG } from "@goauthentik/common/api/config"; -import { getRelativeTime } from "@goauthentik/common/utils"; +import { formatElapsedTime } from "@goauthentik/common/temporal"; import "@goauthentik/components/ak-status-label"; import "@goauthentik/elements/chips/Chip"; import "@goauthentik/elements/chips/ChipGroup"; @@ -92,7 +92,7 @@ export class UserOAuthAccessTokenList extends Table { bad-label=${msg("Yes")} >
`, html`${item.expires - ? html`
${getRelativeTime(item.expires)}
+ ? html`
${formatElapsedTime(item.expires)}
${item.expires.toLocaleString()}` : msg("-")}`, html` diff --git a/web/src/elements/oauth/UserRefreshTokenList.ts b/web/src/elements/oauth/UserRefreshTokenList.ts index fc9b3234fe..3ba1d1cca4 100644 --- a/web/src/elements/oauth/UserRefreshTokenList.ts +++ b/web/src/elements/oauth/UserRefreshTokenList.ts @@ -1,5 +1,5 @@ import { DEFAULT_CONFIG } from "@goauthentik/common/api/config"; -import { getRelativeTime } from "@goauthentik/common/utils"; +import { formatElapsedTime } from "@goauthentik/common/temporal"; import "@goauthentik/components/ak-status-label"; import "@goauthentik/elements/chips/Chip"; import "@goauthentik/elements/chips/ChipGroup"; @@ -93,7 +93,7 @@ export class UserOAuthRefreshTokenList extends Table { bad-label=${msg("Yes")} >`, html`${item.expires - ? html`
${getRelativeTime(item.expires)}
+ ? html`
${formatElapsedTime(item.expires)}
${item.expires.toLocaleString()}` : msg("-")}`, html` diff --git a/web/src/elements/sync/SyncStatusCard.ts b/web/src/elements/sync/SyncStatusCard.ts index fb75d9995a..5a0b5e2f60 100644 --- a/web/src/elements/sync/SyncStatusCard.ts +++ b/web/src/elements/sync/SyncStatusCard.ts @@ -1,5 +1,5 @@ import { EVENT_REFRESH } from "@goauthentik/common/constants"; -import { getRelativeTime } from "@goauthentik/common/utils"; +import { formatElapsedTime } from "@goauthentik/common/temporal"; import "@goauthentik/components/ak-status-label"; import { AKElement } from "@goauthentik/elements/Base"; import "@goauthentik/elements/EmptyState"; @@ -71,7 +71,7 @@ export class SyncStatusTable extends Table { good-label=${msg("Finished successfully")} bad-label=${msg("Finished with errors")} >`, - html`
${getRelativeTime(item.finishTimestamp)}
+ html`
${formatElapsedTime(item.finishTimestamp)}
${item.finishTimestamp.toLocaleString()}`, ]; } diff --git a/web/src/elements/user/SessionList.ts b/web/src/elements/user/SessionList.ts index 3e43976643..93137c4032 100644 --- a/web/src/elements/user/SessionList.ts +++ b/web/src/elements/user/SessionList.ts @@ -1,5 +1,5 @@ import { DEFAULT_CONFIG } from "@goauthentik/common/api/config"; -import { getRelativeTime } from "@goauthentik/common/utils"; +import { formatElapsedTime } from "@goauthentik/common/temporal"; import "@goauthentik/elements/forms/DeleteBulkForm"; import { PaginatedResponse } from "@goauthentik/elements/table/Table"; import { Table, TableColumn } from "@goauthentik/elements/table/Table"; @@ -73,9 +73,9 @@ export class AuthenticatedSessionList extends Table { ${item.lastIp} ${item.userAgent.userAgent?.family}, ${item.userAgent.os?.family}`, - html`
${getRelativeTime(item.lastUsed)}
+ html`
${formatElapsedTime(item.lastUsed)}
${item.lastUsed?.toLocaleString()}`, - html`
${getRelativeTime(item.expires || new Date())}
+ html`
${formatElapsedTime(item.expires || new Date())}
${item.expires?.toLocaleString()}`, ]; } diff --git a/web/src/elements/user/UserConsentList.ts b/web/src/elements/user/UserConsentList.ts index fa7dacafa9..a17fb32fed 100644 --- a/web/src/elements/user/UserConsentList.ts +++ b/web/src/elements/user/UserConsentList.ts @@ -1,5 +1,5 @@ import { DEFAULT_CONFIG } from "@goauthentik/common/api/config"; -import { getRelativeTime } from "@goauthentik/common/utils"; +import { formatElapsedTime } from "@goauthentik/common/temporal"; import "@goauthentik/elements/chips/Chip"; import "@goauthentik/elements/chips/ChipGroup"; import "@goauthentik/elements/forms/DeleteBulkForm"; @@ -62,7 +62,7 @@ export class UserConsentList extends Table { return [ html`${item.application.name}`, html`${item.expires && item.expiring - ? html`
${getRelativeTime(item.expires)}
+ ? html`
${formatElapsedTime(item.expires)}
${item.expires.toLocaleString()}` : msg("-")}`, html`${item.permissions diff --git a/web/src/elements/user/UserReputationList.ts b/web/src/elements/user/UserReputationList.ts index 05144a0caf..48d70bcc71 100644 --- a/web/src/elements/user/UserReputationList.ts +++ b/web/src/elements/user/UserReputationList.ts @@ -1,5 +1,5 @@ import { DEFAULT_CONFIG } from "@goauthentik/common/api/config"; -import { getRelativeTime } from "@goauthentik/common/utils"; +import { formatElapsedTime } from "@goauthentik/common/temporal"; import "@goauthentik/elements/forms/DeleteBulkForm"; import { PaginatedResponse } from "@goauthentik/elements/table/Table"; import { Table, TableColumn } from "@goauthentik/elements/table/Table"; @@ -73,7 +73,7 @@ export class UserReputationList extends Table { : html``} ${item.ip}`, html`${item.score}`, - html`
${getRelativeTime(item.updated)}
+ html`
${formatElapsedTime(item.updated)}
${item.updated.toLocaleString()}`, ]; } diff --git a/web/src/user/user-settings/mfa/MFADevicesPage.ts b/web/src/user/user-settings/mfa/MFADevicesPage.ts index 3a19cc0309..7cb8ee2f6f 100644 --- a/web/src/user/user-settings/mfa/MFADevicesPage.ts +++ b/web/src/user/user-settings/mfa/MFADevicesPage.ts @@ -2,7 +2,7 @@ import { AndNext, DEFAULT_CONFIG } from "@goauthentik/common/api/config"; import { globalAK } from "@goauthentik/common/global"; import { deviceTypeName } from "@goauthentik/common/labels"; import { SentryIgnoredError } from "@goauthentik/common/sentry"; -import { getRelativeTime } from "@goauthentik/common/utils"; +import { formatElapsedTime } from "@goauthentik/common/temporal"; import "@goauthentik/elements/buttons/Dropdown"; import "@goauthentik/elements/buttons/ModalButton"; import "@goauthentik/elements/buttons/TokenCopyButton"; @@ -133,11 +133,11 @@ export class MFADevicesPage extends Table { html`${deviceTypeName(item)} ${item.extraDescription ? ` - ${item.extraDescription}` : ""}`, html`${item.created.getTime() > 0 - ? html`
${getRelativeTime(item.created)}
+ ? html`
${formatElapsedTime(item.created)}
${item.created.toLocaleString()}` : html`-`}`, html`${item.lastUsed - ? html`
${getRelativeTime(item.lastUsed)}
+ ? html`
${formatElapsedTime(item.lastUsed)}
${item.lastUsed.toLocaleString()}` : html`-`}`, html` diff --git a/web/src/user/user-settings/tokens/UserTokenForm.ts b/web/src/user/user-settings/tokens/UserTokenForm.ts index e1c4797226..f7f0c37798 100644 --- a/web/src/user/user-settings/tokens/UserTokenForm.ts +++ b/web/src/user/user-settings/tokens/UserTokenForm.ts @@ -1,5 +1,5 @@ import { DEFAULT_CONFIG } from "@goauthentik/common/api/config"; -import { dateTimeLocal } from "@goauthentik/common/utils"; +import { dateTimeLocal } from "@goauthentik/common/temporal"; import "@goauthentik/elements/forms/HorizontalFormElement"; import { ModelForm } from "@goauthentik/elements/forms/ModelForm"; diff --git a/web/src/user/user-settings/tokens/UserTokenList.ts b/web/src/user/user-settings/tokens/UserTokenList.ts index 10a0b0609e..90b2b49941 100644 --- a/web/src/user/user-settings/tokens/UserTokenList.ts +++ b/web/src/user/user-settings/tokens/UserTokenList.ts @@ -1,7 +1,7 @@ import { DEFAULT_CONFIG } from "@goauthentik/common/api/config"; import { intentToLabel } from "@goauthentik/common/labels"; +import { formatElapsedTime } from "@goauthentik/common/temporal"; import { me } from "@goauthentik/common/users"; -import { getRelativeTime } from "@goauthentik/common/utils"; import "@goauthentik/components/ak-status-label"; import "@goauthentik/elements/buttons/Dropdown"; import "@goauthentik/elements/buttons/ModalButton"; @@ -110,7 +110,7 @@ export class UserTokenList extends Table { position="top" .content=${item.expires?.toLocaleString()} > - ${getRelativeTime(item.expires!)} + ${formatElapsedTime(item.expires!)} ` : msg("-")}