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
This commit is contained in:
sua yoo 2022-10-05 18:12:31 -07:00 committed by GitHub
parent 2bfbeab55f
commit 8708c24a74
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 115 additions and 27 deletions

View File

@ -1,50 +1,89 @@
import { LitElement } from "lit"; import { LitElement } from "lit";
import { property, state } from "lit/decorators.js"; import { property, state } from "lit/decorators.js";
import { msg, localized, str } from "@lit/localize";
import humanizeDuration from "pretty-ms"; import humanizeDuration from "pretty-ms";
export type HumanizeOptions = {
compact?: boolean;
verbose?: boolean;
unitCount?: number;
};
/** /**
* Show time passed from date in human-friendly format * Show time passed from date in human-friendly format
* Updates every 5 seconds
* *
* Usage example: * Usage example:
* ```ts * ```ts
* <btrix-relative-duration value=${value}></btrix-relative-duration> * <btrix-relative-duration value=${value}></btrix-relative-duration>
* ``` * ```
*/ */
@localized()
export class RelativeDuration extends LitElement { export class RelativeDuration extends LitElement {
@property({ type: String }) @property({ type: String })
value?: string; // `new Date` compatible date format value?: string; // `new Date` compatible date format
@state() @property({ type: Number })
private now = Date.now(); 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; private timerId?: number;
static humanize(duration: number) { static humanize(duration: number, options: HumanizeOptions = {}) {
return humanizeDuration(duration, { return humanizeDuration(duration, {
secondsDecimalDigits: 0, secondsDecimalDigits: 0,
...options,
}); });
} }
connectedCallback(): void { connectedCallback(): void {
super.connectedCallback(); super.connectedCallback();
this.timerId = window.setInterval(() => this.updateValue(), 1000 * 5);
} }
disconnectedCallback(): void { disconnectedCallback(): void {
window.clearInterval(this.timerId); window.clearTimeout(this.timerId);
super.disconnectedCallback(); super.disconnectedCallback();
} }
protected updated(changedProperties: Map<string | number | symbol, unknown>) {
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() { render() {
if (!this.value) return ""; if (!this.value) return "";
return RelativeDuration.humanize(this.now - new Date(this.value).valueOf()); return RelativeDuration.humanize(
} (this.endTime || Date.now()) - new Date(this.value).valueOf(),
{
private updateValue() { compact: this.compact,
this.now = Date.now(); verbose: this.verbose,
unitCount: this.unitCount,
}
);
} }
} }

View File

@ -446,6 +446,8 @@ export class CrawlDetail extends LiteElement {
<span class="text-purple-600"> <span class="text-purple-600">
<btrix-relative-duration <btrix-relative-duration
value=${`${this.crawl.started}Z`} value=${`${this.crawl.started}Z`}
unitCount="3"
tickSeconds="1"
></btrix-relative-duration> ></btrix-relative-duration>
</span> </span>
`} `}

View File

@ -1,4 +1,5 @@
import { state, property } from "lit/decorators.js"; import { state, property } from "lit/decorators.js";
import { ifDefined } from "lit/directives/if-defined.js";
import { msg, localized, str } from "@lit/localize"; import { msg, localized, str } from "@lit/localize";
import debounce from "lodash/fp/debounce"; import debounce from "lodash/fp/debounce";
import flow from "lodash/fp/flow"; import flow from "lodash/fp/flow";
@ -137,7 +138,9 @@ export class CrawlsList extends LiteElement {
? this.renderCrawlList() ? this.renderCrawlList()
: html` : html`
<div class="border-t border-b py-5"> <div class="border-t border-b py-5">
<p class="text-center text-0-500">${msg("No crawls yet.")}</p> <p class="text-center text-neutral-500">
${msg("No crawls yet.")}
</p>
</div> </div>
`} `}
</section> </section>
@ -177,7 +180,9 @@ export class CrawlsList extends LiteElement {
</sl-input> </sl-input>
</div> </div>
<div class="col-span-12 md:col-span-1 flex items-center justify-end"> <div class="col-span-12 md:col-span-1 flex items-center justify-end">
<div class="whitespace-nowrap text-0-500 mr-2">${msg("Sort By")}</div> <div class="whitespace-nowrap text-neutral-500 mr-2">
${msg("Sort By")}
</div>
<sl-dropdown <sl-dropdown
placement="bottom-end" placement="bottom-end"
distance="4" distance="4"
@ -252,7 +257,6 @@ export class CrawlsList extends LiteElement {
<a <a
href=${`${this.crawlsBaseUrl}/crawl/${crawl.id}`} href=${`${this.crawlsBaseUrl}/crawl/${crawl.id}`}
class="grid grid-cols-12 gap-4 p-4 leading-none hover:bg-zinc-50 hover:text-primary transition-colors" class="grid grid-cols-12 gap-4 p-4 leading-none hover:bg-zinc-50 hover:text-primary transition-colors"
title=${crawl.configName}
@click=${this.navLink} @click=${this.navLink}
> >
<div class="col-span-11 md:col-span-5"> <div class="col-span-11 md:col-span-5">
@ -376,7 +380,7 @@ export class CrawlsList extends LiteElement {
: crawl.state === "complete" : crawl.state === "complete"
? "text-emerald-500" ? "text-emerald-500"
: isActive(crawl) : isActive(crawl)
? "text-purple-500" ? "text-purple-500 motion-safe:animate-pulse"
: "text-zinc-300"}" : "text-zinc-300"}"
style="font-size: 10px; vertical-align: 2px" style="font-size: 10px; vertical-align: 2px"
> >
@ -391,16 +395,20 @@ export class CrawlsList extends LiteElement {
> >
${crawl.state.replace(/_/g, " ")} ${crawl.state.replace(/_/g, " ")}
</div> </div>
<div class="text-0-500 text-sm whitespace-nowrap truncate"> <div class="text-neutral-500 text-sm whitespace-nowrap truncate">
${crawl.finished ${crawl.finished
? html` ? html`
<sl-relative-time <sl-relative-time
date=${`${crawl.finished}Z` /** Z for UTC */} date=${`${crawl.finished}Z` /** Z for UTC */}
></sl-relative-time> ></sl-relative-time>
` `
: html`<btrix-relative-duration : ""}
value=${`${crawl.started}Z`} ${!crawl.finished
></btrix-relative-duration>`} ? html`
${crawl.state === "canceled" ? msg("Unknown") : ""}
${isActive(crawl) ? this.renderActiveDuration(crawl) : ""}
`
: ""}
</div> </div>
</div> </div>
</div> </div>
@ -414,17 +422,20 @@ export class CrawlsList extends LiteElement {
lang=${/* TODO localize: */ "en"} lang=${/* TODO localize: */ "en"}
></sl-format-bytes> ></sl-format-bytes>
</span> </span>
<span class="text-0-500"> <span class="text-neutral-500">
(${crawl.fileCount === 1 (${crawl.fileCount === 1
? msg(str`${crawl.fileCount} file`) ? msg(str`${crawl.fileCount} file`)
: msg(str`${crawl.fileCount} files`)}) : msg(str`${crawl.fileCount} files`)})
</span> </span>
</div> </div>
<div class="text-0-500 text-sm whitespace-nowrap truncate"> <div
class="text-neutral-500 text-sm whitespace-nowrap truncate"
>
${msg( ${msg(
str`in ${RelativeDuration.humanize( str`in ${RelativeDuration.humanize(
new Date(`${crawl.finished}Z`).valueOf() - new Date(`${crawl.finished}Z`).valueOf() -
new Date(`${crawl.started}Z`).valueOf() new Date(`${crawl.started}Z`).valueOf(),
{ compact: true }
)}` )}`
)} )}
</div> </div>
@ -438,7 +449,9 @@ export class CrawlsList extends LiteElement {
<span class="text-0-400">/</span> <span class="text-0-400">/</span>
${this.numberFormatter.format(+crawl.stats.found)} ${this.numberFormatter.format(+crawl.stats.found)}
</div> </div>
<div class="text-0-500 text-sm whitespace-nowrap truncate"> <div
class="text-neutral-500 text-sm whitespace-nowrap truncate"
>
${msg("pages crawled")} ${msg("pages crawled")}
</div> </div>
` `
@ -453,7 +466,9 @@ export class CrawlsList extends LiteElement {
>${msg("Manual Start")}</span >${msg("Manual Start")}</span
> >
</div> </div>
<div class="ml-1 text-0-500 text-sm whitespace-nowrap truncate"> <div
class="ml-1 text-neutral-500 text-sm whitespace-nowrap truncate"
>
${msg(str`by ${crawl.userName || crawl.userid}`)} ${msg(str`by ${crawl.userName || crawl.userid}`)}
</div> </div>
` `
@ -470,6 +485,37 @@ export class CrawlsList extends LiteElement {
</li>`; </li>`;
}; };
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`
<btrix-relative-duration
class="text-purple-500"
value=${`${crawl.started}Z`}
endTime=${this.lastFetched || Date.now()}
unitCount=${unitCount}
tickSeconds=${ifDefined(tickSeconds)}
></btrix-relative-duration>
`;
}
private onSearchInput = debounce(200)((e: any) => { private onSearchInput = debounce(200)((e: any) => {
this.filterBy = e.target.value; this.filterBy = e.target.value;
}) as any; }) as any;

View File

@ -5,7 +5,8 @@ type CrawlState =
| "failed" | "failed"
| "partial_complete" | "partial_complete"
| "timed_out" | "timed_out"
| "stopping"; | "stopping"
| "canceled";
export type Crawl = { export type Crawl = {
id: string; id: string;