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