From 8708c24a746761af0fcce502a653159ada3f8fa6 Mon Sep 17 00:00:00 2001 From: sua yoo Date: Wed, 5 Oct 2022 18:12:31 -0700 Subject: [PATCH] Improve crawl elapsed time UX (#323) Smoother elapsed crawl timer: - Crawls list: show seconds increment up to 2 minutes, then show minutes only - Crawls detail: show seconds increment up to one day --- frontend/src/components/relative-duration.ts | 65 ++++++++++++++---- frontend/src/pages/archive/crawl-detail.ts | 2 + frontend/src/pages/archive/crawls-list.ts | 72 ++++++++++++++++---- frontend/src/pages/archive/types.ts | 3 +- 4 files changed, 115 insertions(+), 27 deletions(-) diff --git a/frontend/src/components/relative-duration.ts b/frontend/src/components/relative-duration.ts index b9589626..336ec775 100644 --- a/frontend/src/components/relative-duration.ts +++ b/frontend/src/components/relative-duration.ts @@ -1,50 +1,89 @@ import { LitElement } from "lit"; import { property, state } from "lit/decorators.js"; +import { msg, localized, str } from "@lit/localize"; import humanizeDuration from "pretty-ms"; +export type HumanizeOptions = { + compact?: boolean; + verbose?: boolean; + unitCount?: number; +}; + /** * Show time passed from date in human-friendly format - * Updates every 5 seconds * * Usage example: * ```ts * * ``` */ +@localized() export class RelativeDuration extends LitElement { @property({ type: String }) value?: string; // `new Date` compatible date format - @state() - private now = Date.now(); + @property({ type: Number }) + tickSeconds?: number; // Enables ticks every specified seconds - // For long polling: + @property({ type: Number }) + endTime?: number = Date.now(); + + @property({ type: Boolean }) + compact? = false; + + @property({ type: Boolean }) + verbose? = false; + + @property({ type: Number }) + unitCount?: number; + + @state() private timerId?: number; - static humanize(duration: number) { + static humanize(duration: number, options: HumanizeOptions = {}) { return humanizeDuration(duration, { secondsDecimalDigits: 0, + ...options, }); } connectedCallback(): void { super.connectedCallback(); - - this.timerId = window.setInterval(() => this.updateValue(), 1000 * 5); } disconnectedCallback(): void { - window.clearInterval(this.timerId); + window.clearTimeout(this.timerId); super.disconnectedCallback(); } + protected updated(changedProperties: Map) { + if (changedProperties.has("tickSeconds")) { + window.clearTimeout(this.timerId); + } + + if (changedProperties.has("endTime") && this.tickSeconds) { + this.tick(this.tickSeconds * 1000); + } + } + + private tick(timeoutMs: number) { + window.clearTimeout(this.timerId); + + this.timerId = window.setTimeout(() => { + this.endTime = Date.now(); + }, timeoutMs); + } + render() { if (!this.value) return ""; - return RelativeDuration.humanize(this.now - new Date(this.value).valueOf()); - } - - private updateValue() { - this.now = Date.now(); + return RelativeDuration.humanize( + (this.endTime || Date.now()) - new Date(this.value).valueOf(), + { + compact: this.compact, + verbose: this.verbose, + unitCount: this.unitCount, + } + ); } } diff --git a/frontend/src/pages/archive/crawl-detail.ts b/frontend/src/pages/archive/crawl-detail.ts index 9825ec2b..fcbb49dc 100644 --- a/frontend/src/pages/archive/crawl-detail.ts +++ b/frontend/src/pages/archive/crawl-detail.ts @@ -446,6 +446,8 @@ export class CrawlDetail extends LiteElement { `} diff --git a/frontend/src/pages/archive/crawls-list.ts b/frontend/src/pages/archive/crawls-list.ts index 6fc921f7..de8fba34 100644 --- a/frontend/src/pages/archive/crawls-list.ts +++ b/frontend/src/pages/archive/crawls-list.ts @@ -1,4 +1,5 @@ import { state, property } from "lit/decorators.js"; +import { ifDefined } from "lit/directives/if-defined.js"; import { msg, localized, str } from "@lit/localize"; import debounce from "lodash/fp/debounce"; import flow from "lodash/fp/flow"; @@ -137,7 +138,9 @@ export class CrawlsList extends LiteElement { ? this.renderCrawlList() : html`
-

${msg("No crawls yet.")}

+

+ ${msg("No crawls yet.")} +

`} @@ -177,7 +180,9 @@ export class CrawlsList extends LiteElement {
-
${msg("Sort By")}
+
+ ${msg("Sort By")} +
@@ -376,7 +380,7 @@ export class CrawlsList extends LiteElement { : crawl.state === "complete" ? "text-emerald-500" : isActive(crawl) - ? "text-purple-500" + ? "text-purple-500 motion-safe:animate-pulse" : "text-zinc-300"}" style="font-size: 10px; vertical-align: 2px" > @@ -391,16 +395,20 @@ export class CrawlsList extends LiteElement { > ${crawl.state.replace(/_/g, " ")}
-
+
${crawl.finished ? html` ` - : html``} + : ""} + ${!crawl.finished + ? html` + ${crawl.state === "canceled" ? msg("Unknown") : ""} + ${isActive(crawl) ? this.renderActiveDuration(crawl) : ""} + ` + : ""}
@@ -414,17 +422,20 @@ export class CrawlsList extends LiteElement { lang=${/* TODO localize: */ "en"} > - + (${crawl.fileCount === 1 ? msg(str`${crawl.fileCount} file`) : msg(str`${crawl.fileCount} files`)}) -
+
${msg( str`in ${RelativeDuration.humanize( new Date(`${crawl.finished}Z`).valueOf() - - new Date(`${crawl.started}Z`).valueOf() + new Date(`${crawl.started}Z`).valueOf(), + { compact: true } )}` )}
@@ -438,7 +449,9 @@ export class CrawlsList extends LiteElement { / ${this.numberFormatter.format(+crawl.stats.found)}
-
+
${msg("pages crawled")}
` @@ -453,7 +466,9 @@ export class CrawlsList extends LiteElement { >${msg("Manual Start")}
-
+
${msg(str`by ${crawl.userName || crawl.userid}`)}
` @@ -470,6 +485,37 @@ export class CrawlsList extends LiteElement { `; }; + private renderActiveDuration(crawl: Crawl) { + const endTime = this.lastFetched || Date.now(); + const duration = endTime - new Date(`${crawl.started}Z`).valueOf(); + let unitCount: number; + let tickSeconds: number | undefined = undefined; + + // Show second unit if showing seconds or greater than 1 hr + const showSeconds = duration < 60 * 2 * 1000; + if (showSeconds || duration > 60 * 60 * 1000) { + unitCount = 2; + } else { + unitCount = 1; + } + // Tick if seconds are showing + if (showSeconds) { + tickSeconds = 1; + } else { + tickSeconds = undefined; + } + + return html` + + `; + } + private onSearchInput = debounce(200)((e: any) => { this.filterBy = e.target.value; }) as any; diff --git a/frontend/src/pages/archive/types.ts b/frontend/src/pages/archive/types.ts index f3890e75..43921db5 100644 --- a/frontend/src/pages/archive/types.ts +++ b/frontend/src/pages/archive/types.ts @@ -5,7 +5,8 @@ type CrawlState = | "failed" | "partial_complete" | "timed_out" - | "stopping"; + | "stopping" + | "canceled"; export type Crawl = { id: string;