Move execution time formatting into its own util (#1386)
Refactors and rewrites the humanize time functions used on the dashboard, and swaps out these new functions in a couple of places. Examples of these functions' behaviours can be found in the tests for them. <img width="375" alt="Screenshot 2023-11-16 at 8 07 14 PM" src="https://github.com/webrecorder/browsertrix-cloud/assets/5727389/775b3a49-1061-4002-8c34-961777423542"> <img width="267" alt="Screenshot 2023-11-16 at 8 07 45 PM" src="https://github.com/webrecorder/browsertrix-cloud/assets/5727389/1d22aec0-4b88-4a9a-b1d7-f6612d287769"> <img width="224" alt="Screenshot 2023-11-16 at 8 21 13 PM" src="https://github.com/webrecorder/browsertrix-cloud/assets/5727389/7d895938-ea02-4ffa-9f82-8526725f36c5"> Also fixes inconsistent tooltip text alignment on the dashboard :)
This commit is contained in:
parent
0638e5dad8
commit
d64def00c2
@ -4,7 +4,6 @@ import { when } from "lit/directives/when.js";
|
||||
import { ifDefined } from "lit/directives/if-defined.js";
|
||||
import { classMap } from "lit/directives/class-map.js";
|
||||
import { msg, localized, str } from "@lit/localize";
|
||||
import humanizeDuration from "pretty-ms";
|
||||
|
||||
import type { PageChangeEvent } from "../../components/pagination";
|
||||
import { RelativeDuration } from "../../components/relative-duration";
|
||||
@ -14,6 +13,7 @@ import { isActive } from "../../utils/crawler";
|
||||
import { CopyButton } from "../../components/copy-button";
|
||||
import type { Crawl, Seed } from "./types";
|
||||
import { APIPaginatedList } from "../../types/api";
|
||||
import { humanizeExecutionSeconds } from "../../utils/executionTimeFormatter";
|
||||
|
||||
const SECTIONS = [
|
||||
"overview",
|
||||
@ -644,7 +644,7 @@ export class CrawlDetail extends LiteElement {
|
||||
<btrix-desc-list-item label=${msg("Execution Time")}>
|
||||
${this.crawl!.finished
|
||||
? html`<span
|
||||
>${humanizeDuration(
|
||||
>${humanizeExecutionSeconds(
|
||||
this.crawl!.crawlExecSeconds * 1000
|
||||
)}</span
|
||||
>`
|
||||
|
@ -4,13 +4,15 @@ import { when } from "lit/directives/when.js";
|
||||
import { ifDefined } from "lit/directives/if-defined.js";
|
||||
import { msg, localized, str } from "@lit/localize";
|
||||
import type { SlSelectEvent } from "@shoelace-style/shoelace";
|
||||
import humanizeMilliseconds from "pretty-ms";
|
||||
|
||||
import LiteElement, { html } from "../../utils/LiteElement";
|
||||
import type { AuthState } from "../../utils/AuthService";
|
||||
import type { OrgData } from "../../utils/orgs";
|
||||
import type { SelectNewDialogEvent } from "./index";
|
||||
import { getLocale } from "../../utils/localization";
|
||||
import {
|
||||
humanizeExecutionSeconds,
|
||||
humanizeSeconds,
|
||||
} from "../../utils/executionTimeFormatter";
|
||||
|
||||
type Metrics = {
|
||||
storageUsedBytes: number;
|
||||
@ -66,30 +68,6 @@ export class Dashboard extends LiteElement {
|
||||
}
|
||||
}
|
||||
|
||||
private humanizeExecutionSeconds = (seconds: number) => {
|
||||
const minutes = Math.ceil(seconds / 60);
|
||||
|
||||
const locale = getLocale();
|
||||
const compactFormatter = new Intl.NumberFormat(locale, {
|
||||
notation: "compact",
|
||||
style: "unit",
|
||||
unit: "minute",
|
||||
unitDisplay: "long",
|
||||
});
|
||||
|
||||
const fullFormatter = new Intl.NumberFormat(locale, {
|
||||
style: "unit",
|
||||
unit: "minute",
|
||||
unitDisplay: "long",
|
||||
maximumFractionDigits: 0,
|
||||
});
|
||||
|
||||
return html`<span title="${fullFormatter.format(minutes)}">
|
||||
${compactFormatter.format(minutes)}</span
|
||||
>
|
||||
(${humanizeMilliseconds(seconds * 1000)})`;
|
||||
};
|
||||
|
||||
render() {
|
||||
const hasQuota = Boolean(this.metrics?.storageQuotaBytes);
|
||||
const quotaReached =
|
||||
@ -347,7 +325,7 @@ export class Dashboard extends LiteElement {
|
||||
)
|
||||
)}
|
||||
<div slot="available" class="flex-1">
|
||||
<sl-tooltip>
|
||||
<sl-tooltip class="text-center">
|
||||
<div slot="content">
|
||||
<div>${msg("Available")}</div>
|
||||
<div class="text-xs opacity-80">
|
||||
@ -415,7 +393,7 @@ export class Dashboard extends LiteElement {
|
||||
<div class="text-center">
|
||||
<div>${label}</div>
|
||||
<div class="text-xs opacity-80">
|
||||
${humanizeMilliseconds(value * 1000)} |
|
||||
${humanizeExecutionSeconds(value)} |
|
||||
${this.renderPercentage(value / quotaSeconds)}
|
||||
</div>
|
||||
</div>
|
||||
@ -438,9 +416,7 @@ export class Dashboard extends LiteElement {
|
||||
hasQuota
|
||||
? html`
|
||||
<span class="inline-flex items-center">
|
||||
${this.humanizeExecutionSeconds(
|
||||
quotaSeconds - usageSeconds
|
||||
)}
|
||||
${humanizeExecutionSeconds(quotaSeconds - usageSeconds)}
|
||||
${msg("Available")}
|
||||
</span>
|
||||
`
|
||||
@ -464,14 +440,11 @@ export class Dashboard extends LiteElement {
|
||||
)
|
||||
)}
|
||||
<div slot="available" class="flex-1">
|
||||
<sl-tooltip>
|
||||
<sl-tooltip class="text-center">
|
||||
<div slot="content">
|
||||
<div>${msg("Monthly Execution Time Available")}</div>
|
||||
<div class="text-xs opacity-80">
|
||||
${this.humanizeExecutionSeconds(
|
||||
quotaSeconds - usageSeconds
|
||||
)}
|
||||
|
|
||||
${humanizeExecutionSeconds(quotaSeconds - usageSeconds)} |
|
||||
${this.renderPercentage(
|
||||
(quotaSeconds - usageSeconds) / quotaSeconds
|
||||
)}
|
||||
@ -481,10 +454,10 @@ export class Dashboard extends LiteElement {
|
||||
</sl-tooltip>
|
||||
</div>
|
||||
<span slot="valueLabel">
|
||||
${this.humanizeExecutionSeconds(usageSeconds)}
|
||||
${humanizeExecutionSeconds(usageSeconds, "short")}
|
||||
</span>
|
||||
<span slot="maxLabel">
|
||||
${this.humanizeExecutionSeconds(quotaSeconds)}
|
||||
${humanizeExecutionSeconds(quotaSeconds, "short")}
|
||||
</span>
|
||||
</btrix-meter>
|
||||
</div>
|
||||
@ -603,8 +576,8 @@ export class Dashboard extends LiteElement {
|
||||
>
|
||||
</sl-format-date>
|
||||
`,
|
||||
value ? this.humanizeExecutionSeconds(value) : "--",
|
||||
humanizeMilliseconds(crawlTime * 1000 || 0),
|
||||
value ? humanizeExecutionSeconds(value) : "--",
|
||||
humanizeSeconds(crawlTime || 0),
|
||||
];
|
||||
});
|
||||
return html`
|
||||
|
76
frontend/src/utils/executionTimeFormatter.test.ts
Normal file
76
frontend/src/utils/executionTimeFormatter.test.ts
Normal file
@ -0,0 +1,76 @@
|
||||
import {
|
||||
humanizeSeconds,
|
||||
humanizeExecutionSeconds,
|
||||
} from "./executionTimeFormatter";
|
||||
import { expect, fixture } from "@open-wc/testing";
|
||||
|
||||
describe("formatHours", () => {
|
||||
it("returns a time in hours and minutes when given a time over an hour", () => {
|
||||
expect(humanizeSeconds(12_345, "en-US")).to.equal("3h 26m");
|
||||
});
|
||||
it("returns 1m when given a time under a minute", () => {
|
||||
expect(humanizeSeconds(24, "en-US")).to.equal("1m");
|
||||
});
|
||||
it("returns 0m and seconds when given a time under a minute with seconds on", () => {
|
||||
expect(humanizeSeconds(24, "en-US", true)).to.equal("0m 24s");
|
||||
});
|
||||
it("returns minutes when given a time under an hour", () => {
|
||||
expect(humanizeSeconds(1_234, "en-US")).to.equal("21m");
|
||||
});
|
||||
it("returns just hours when given a time exactly in hours", () => {
|
||||
expect(humanizeSeconds(3_600, "en-US")).to.equal("1h");
|
||||
expect(humanizeSeconds(44_442_000, "en-US")).to.equal("12,345h");
|
||||
});
|
||||
it("handles different locales correctly", () => {
|
||||
expect(humanizeSeconds(44_442_000_000, "en-IN")).to.equal("1,23,45,000h");
|
||||
expect(humanizeSeconds(44_442_000_000, "pt-BR")).to.equal("12.345.000 h");
|
||||
expect(humanizeSeconds(44_442_000_000, "de-DE")).to.equal(
|
||||
"12.345.000 Std."
|
||||
);
|
||||
expect(humanizeSeconds(44_442_000_000, "ar-EG")).to.equal("١٢٬٣٤٥٬٠٠٠ س");
|
||||
});
|
||||
it("formats zero time as expected", () => {
|
||||
expect(humanizeSeconds(0, "en-US")).to.equal("0m");
|
||||
});
|
||||
it("formats zero time as expected", () => {
|
||||
expect(humanizeSeconds(0, "en-US", true)).to.equal("0s");
|
||||
});
|
||||
it("formats negative time as expected", () => {
|
||||
expect(() => humanizeSeconds(-100, "en-US")).to.throw(
|
||||
"humanizeSeconds in unimplemented for negative times"
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("humanizeExecutionSeconds", () => {
|
||||
it("formats a given time in billable minutes", async () => {
|
||||
const parentNode = document.createElement("div");
|
||||
const el = await fixture(humanizeExecutionSeconds(1_234_567_890), {
|
||||
parentNode,
|
||||
});
|
||||
expect(el.getAttribute("title")).to.equal("20,576,132 minutes");
|
||||
expect(el.textContent?.trim()).to.equal("21M minutes");
|
||||
expect(parentNode.innerText).to.equal("21M minutes\u00a0(342,935h 32m)");
|
||||
});
|
||||
|
||||
it("shows a short version when set", async () => {
|
||||
const parentNode = document.createElement("div");
|
||||
const el = await fixture(humanizeExecutionSeconds(1_234_567_890, "short"), {
|
||||
parentNode,
|
||||
});
|
||||
expect(el.getAttribute("title")).to.equal(
|
||||
"20,576,132 minutes\u00a0(342,935h 32m)"
|
||||
);
|
||||
expect(el.textContent?.trim()).to.equal("21M minutes");
|
||||
expect(parentNode.innerText).to.equal("21M minutes");
|
||||
});
|
||||
it("skips the details when given a time less than an hour that is exactly in minutes", async () => {
|
||||
const parentNode = document.createElement("div");
|
||||
const el = await fixture(humanizeExecutionSeconds(3_540), {
|
||||
parentNode,
|
||||
});
|
||||
expect(el.getAttribute("title")).to.equal("59 minutes");
|
||||
expect(el.textContent?.trim()).to.equal("59 minutes");
|
||||
expect(parentNode.innerText).to.equal("59 minutes");
|
||||
});
|
||||
});
|
109
frontend/src/utils/executionTimeFormatter.ts
Normal file
109
frontend/src/utils/executionTimeFormatter.ts
Normal file
@ -0,0 +1,109 @@
|
||||
import { html, nothing } from "lit";
|
||||
import { getLocale } from "./localization";
|
||||
|
||||
/**
|
||||
* Returns either `nothing`, or hours-minutes-seconds wrapped in parens.
|
||||
* Biases towards minutes:
|
||||
* - When the time is exactly on an hour boundary, just shows hours
|
||||
* - e.g. `3h`
|
||||
* - When the time isn't on an hour boundary but is on a minute broundary, just shows hours (if applicable) and minutes
|
||||
* - e.g. `3h 2m` or `32m`
|
||||
* - When the time is less than a minute, shows minutes and seconds
|
||||
* - e.g. `0m 43s`
|
||||
*/
|
||||
export function humanizeSeconds(
|
||||
seconds: number,
|
||||
locale?: string,
|
||||
displaySeconds = false
|
||||
) {
|
||||
if (seconds < 0) {
|
||||
throw new Error("humanizeSeconds in unimplemented for negative times");
|
||||
}
|
||||
const hours = Math.floor(seconds / 3600);
|
||||
seconds -= hours * 3600;
|
||||
// If displaying seconds, round minutes down, otherwise round up
|
||||
const minutes = displaySeconds
|
||||
? Math.floor(seconds / 60)
|
||||
: Math.ceil(seconds / 60);
|
||||
seconds -= minutes * 60;
|
||||
|
||||
const hourFormatter = new Intl.NumberFormat(locale, {
|
||||
style: "unit",
|
||||
unit: "hour",
|
||||
unitDisplay: "narrow",
|
||||
});
|
||||
|
||||
const minuteFormatter = new Intl.NumberFormat(locale, {
|
||||
style: "unit",
|
||||
unit: "minute",
|
||||
unitDisplay: "narrow",
|
||||
});
|
||||
|
||||
const secondFormatter = new Intl.NumberFormat(locale, {
|
||||
style: "unit",
|
||||
unit: "second",
|
||||
unitDisplay: "narrow",
|
||||
});
|
||||
|
||||
return [
|
||||
hours !== 0 && hourFormatter.format(hours),
|
||||
(minutes !== 0 || seconds !== 0 || (!displaySeconds && hours === 0)) &&
|
||||
minuteFormatter.format(minutes),
|
||||
displaySeconds &&
|
||||
(seconds !== 0 || (hours === 0 && minutes === 0)) &&
|
||||
secondFormatter.format(seconds),
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join(" ");
|
||||
}
|
||||
|
||||
/**
|
||||
* Formats execution seconds, either just as minutes (when `style` is `"short"`), or as minutes and hours-minutes-seconds (when `style` is undefined or `"full"`)
|
||||
* @example humanizeExecutionSeconds(1_234_567_890)
|
||||
* // <span title="20,576,132 minutes">21M minutes</span> (342,935h 31m 30s)
|
||||
*
|
||||
* @example humanizeExecutionSeconds(1_234_567_890, "short")
|
||||
* // <span title="20,576,132 minutes (342,935h 31m 30s)">21M minutes</span>
|
||||
*/
|
||||
export const humanizeExecutionSeconds = (
|
||||
seconds: number,
|
||||
style: "short" | "full" = "full",
|
||||
displaySeconds = false
|
||||
) => {
|
||||
const locale = getLocale();
|
||||
const minutes = Math.ceil(seconds / 60);
|
||||
|
||||
const compactMinuteFormatter = new Intl.NumberFormat(locale, {
|
||||
notation: "compact",
|
||||
style: "unit",
|
||||
unit: "minute",
|
||||
unitDisplay: "long",
|
||||
});
|
||||
|
||||
const longMinuteFormatter = new Intl.NumberFormat(locale, {
|
||||
style: "unit",
|
||||
unit: "minute",
|
||||
unitDisplay: "long",
|
||||
maximumFractionDigits: 0,
|
||||
});
|
||||
|
||||
const details = humanizeSeconds(seconds, locale, displaySeconds);
|
||||
|
||||
// if the time is less than an hour and lines up exactly on the minute, don't render the details.
|
||||
const formattedDetails =
|
||||
minutes === Math.floor(seconds / 60) && seconds < 3600
|
||||
? nothing
|
||||
: `\u00a0(${details})`;
|
||||
|
||||
switch (style) {
|
||||
case "full":
|
||||
return html`<span title="${longMinuteFormatter.format(minutes)}">
|
||||
${compactMinuteFormatter.format(minutes)}</span
|
||||
>${formattedDetails}`;
|
||||
case "short":
|
||||
return html`<span
|
||||
title="${longMinuteFormatter.format(minutes)}${formattedDetails}"
|
||||
>${compactMinuteFormatter.format(minutes)}</span
|
||||
>`;
|
||||
}
|
||||
};
|
Loading…
Reference in New Issue
Block a user