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:
parent
2bfbeab55f
commit
8708c24a74
@ -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,
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -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>
|
||||
`}
|
||||
|
@ -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;
|
||||
|
@ -5,7 +5,8 @@ type CrawlState =
|
||||
| "failed"
|
||||
| "partial_complete"
|
||||
| "timed_out"
|
||||
| "stopping";
|
||||
| "stopping"
|
||||
| "canceled";
|
||||
|
||||
export type Crawl = {
|
||||
id: string;
|
||||
|
Loading…
Reference in New Issue
Block a user