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 { 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
* <btrix-relative-duration value=${value}></btrix-relative-duration>
* ```
*/
@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<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() {
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,
}
);
}
}

View File

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

View File

@ -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`
<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>
`}
</section>
@ -177,7 +180,9 @@ export class CrawlsList extends LiteElement {
</sl-input>
</div>
<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
placement="bottom-end"
distance="4"
@ -252,7 +257,6 @@ export class CrawlsList extends LiteElement {
<a
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"
title=${crawl.configName}
@click=${this.navLink}
>
<div class="col-span-11 md:col-span-5">
@ -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, " ")}
</div>
<div class="text-0-500 text-sm whitespace-nowrap truncate">
<div class="text-neutral-500 text-sm whitespace-nowrap truncate">
${crawl.finished
? html`
<sl-relative-time
date=${`${crawl.finished}Z` /** Z for UTC */}
></sl-relative-time>
`
: html`<btrix-relative-duration
value=${`${crawl.started}Z`}
></btrix-relative-duration>`}
: ""}
${!crawl.finished
? html`
${crawl.state === "canceled" ? msg("Unknown") : ""}
${isActive(crawl) ? this.renderActiveDuration(crawl) : ""}
`
: ""}
</div>
</div>
</div>
@ -414,17 +422,20 @@ export class CrawlsList extends LiteElement {
lang=${/* TODO localize: */ "en"}
></sl-format-bytes>
</span>
<span class="text-0-500">
<span class="text-neutral-500">
(${crawl.fileCount === 1
? msg(str`${crawl.fileCount} file`)
: msg(str`${crawl.fileCount} files`)})
</span>
</div>
<div class="text-0-500 text-sm whitespace-nowrap truncate">
<div
class="text-neutral-500 text-sm whitespace-nowrap truncate"
>
${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 }
)}`
)}
</div>
@ -438,7 +449,9 @@ export class CrawlsList extends LiteElement {
<span class="text-0-400">/</span>
${this.numberFormatter.format(+crawl.stats.found)}
</div>
<div class="text-0-500 text-sm whitespace-nowrap truncate">
<div
class="text-neutral-500 text-sm whitespace-nowrap truncate"
>
${msg("pages crawled")}
</div>
`
@ -453,7 +466,9 @@ export class CrawlsList extends LiteElement {
>${msg("Manual Start")}</span
>
</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}`)}
</div>
`
@ -470,6 +485,37 @@ export class CrawlsList extends LiteElement {
</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) => {
this.filterBy = e.target.value;
}) as any;

View File

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