From 0d23b45dac573615558b717477f5359686842e32 Mon Sep 17 00:00:00 2001 From: sua yoo Date: Fri, 5 May 2023 13:50:45 -0700 Subject: [PATCH] Crawl workflow detail page improvements (#823) Resolves #817 - Adds relevant action buttons to each Workflow detail tab header - Adds "Delete" action menu item to crawls in Crawls tab - Prevent automatically switching to "Watch" tab after running crawl from detail page - Removes "Stop" confirmation prompt and only shows "Cancel" confirmation prompt if there are one or more pages crawled - Replaces "Cancel" confirmation prompt with web component dialog (partially addresses Switch to in-page dialogue boxes #619) - Fixes hash routing to fix going back with browser back button --- frontend/src/components/crawl-list.ts | 20 +- frontend/src/components/workflow-list.ts | 4 +- frontend/src/pages/org/workflow-detail.ts | 268 +++++++++++++++++----- frontend/src/pages/org/workflows-list.ts | 72 +++--- 4 files changed, 260 insertions(+), 104 deletions(-) diff --git a/frontend/src/components/crawl-list.ts b/frontend/src/components/crawl-list.ts index 3f810807..574f3ffc 100644 --- a/frontend/src/components/crawl-list.ts +++ b/frontend/src/components/crawl-list.ts @@ -20,8 +20,9 @@ import { } from "lit/decorators.js"; import { when } from "lit/directives/when.js"; import { msg, localized, str } from "@lit/localize"; -import type { SlIconButton, SlMenu } from "@shoelace-style/shoelace"; +import type { SlMenu } from "@shoelace-style/shoelace"; +import type { Button } from "./button"; import { RelativeDuration } from "./relative-duration"; import type { Crawl } from "../types/crawler"; import { srOnly, truncate, dropdown } from "../utils/css"; @@ -190,7 +191,7 @@ export class CrawlListItem extends LitElement { dropdown!: HTMLElement; @query(".dropdownTrigger") - dropdownTrigger!: SlIconButton; + dropdownTrigger!: Button; @queryAssignedElements({ selector: "sl-menu", slot: "menu" }) private menuArr!: Array; @@ -336,10 +337,10 @@ export class CrawlListItem extends LitElement {
- { // Prevent anchor link default behavior e.preventDefault(); @@ -361,7 +362,9 @@ export class CrawlListItem extends LitElement { } this.dropdownIsOpen = false; }} - > + > + +
`; @@ -443,6 +446,11 @@ export class CrawlList extends LitElement { columnCss, hostVars, css` + .listHeader, .list { + margin-left: var(--row-offset); + margin-right: var(--row-offset); + } + .listHeader { line-height: 1; } @@ -451,8 +459,6 @@ export class CrawlList extends LitElement { border: 1px solid var(--sl-panel-border-color); border-radius: var(--sl-border-radius-medium); overflow: hidden; - margin-left: var(--row-offset); - margin-right: var(--row-offset); } .row { diff --git a/frontend/src/components/workflow-list.ts b/frontend/src/components/workflow-list.ts index 6136fce6..37adf168 100644 --- a/frontend/src/components/workflow-list.ts +++ b/frontend/src/components/workflow-list.ts @@ -253,7 +253,9 @@ export class WorkflowListItem extends LitElement { return html` { e.preventDefault(); await this.updateComplete; diff --git a/frontend/src/pages/org/workflow-detail.ts b/frontend/src/pages/org/workflow-detail.ts index 3e6dd408..80d2b721 100644 --- a/frontend/src/pages/org/workflow-detail.ts +++ b/frontend/src/pages/org/workflow-detail.ts @@ -25,7 +25,7 @@ import { DASHBOARD_ROUTE } from "../../routes"; const SECTIONS = ["artifacts", "watch", "settings"] as const; type Tab = (typeof SECTIONS)[number]; - +const DEFAULT_SECTION: Tab = "artifacts"; const POLL_INTERVAL_SECONDS = 10; /** @@ -75,6 +75,12 @@ export class WorkflowDetail extends LiteElement { @state() private isDialogVisible: boolean = false; + @state() + private isAttemptCancel: boolean = false; + + @state() + private isCancelingOrStoppingCrawl: boolean = false; + @state() private filterBy: Partial> = {}; @@ -106,10 +112,7 @@ export class WorkflowDetail extends LiteElement { connectedCallback(): void { // Set initial active section and dialog based on URL #hash value - const hash = window.location.hash.slice(1); - if (SECTIONS.includes(hash as any)) { - this.activePanel = hash as Tab; - } + this.getActivePanelFromHash(); if ( this.openDialogName && @@ -118,11 +121,13 @@ export class WorkflowDetail extends LiteElement { this.isDialogVisible = true; } super.connectedCallback(); + window.addEventListener("hashchange", this.getActivePanelFromHash); } disconnectedCallback(): void { this.stopPoll(); super.disconnectedCallback(); + window.removeEventListener("hashchange", this.getActivePanelFromHash); } willUpdate(changedProperties: Map) { @@ -143,41 +148,51 @@ export class WorkflowDetail extends LiteElement { }); } - if (this.activePanel !== window.location.hash.slice(1)) { - window.location.hash = `#${this.activePanel}`; - } - if (this.activePanel === "artifacts") { this.fetchCrawls(); } } } + updated(changedProperties: Map) { + const prevWorkflow = changedProperties.get("workflow"); + if ( + prevWorkflow?.currCrawlId && + !this.workflow?.currCrawlId && + this.activePanel === "watch" + ) { + this.goToTab(DEFAULT_SECTION, { replace: true }); + } + } + + private getActivePanelFromHash = () => { + const hashValue = window.location.hash.slice(1); + if (SECTIONS.includes(hashValue as any)) { + this.activePanel = hashValue as Tab; + } else { + this.goToTab(DEFAULT_SECTION, { replace: true }); + } + }; + + private goToTab(tab: Tab, { replace = false } = {}) { + const path = `${window.location.href.split("#")[0]}#${tab}`; + if (replace) { + window.history.replaceState(null, "", path); + } else { + window.history.pushState(null, "", path); + } + this.activePanel = tab; + } + private async fetchWorkflow() { this.stopPoll(); this.isLoading = true; try { this.workflow = await this.getWorkflow(); - let activePanel = this.activePanel; - - if (!this.activePanel) { - if (this.workflow.currCrawlId) { - activePanel = "watch"; - } else { - activePanel = "artifacts"; - } + if (this.workflow.currCrawlId) { + this.fetchCurrentCrawl(); } - - if (activePanel === "watch") { - if (this.workflow.currCrawlId) { - this.fetchCurrentCrawl(); - } else { - activePanel = "artifacts"; - } - } - - this.activePanel = activePanel; } catch (e: any) { this.notify({ message: @@ -249,6 +264,31 @@ export class WorkflowDetail extends LiteElement { ` )} + + (this.isAttemptCancel = false)} + > + ${msg( + "Canceling will discard all pages crawled. Are you sure you want to discard them?" + )} +
+ (this.isAttemptCancel = false)} + >Keep Crawling + { + await this.cancel(); + this.isAttemptCancel = false; + }} + >Cancel & Discard Crawl +
+
`; } @@ -313,26 +353,71 @@ export class WorkflowDetail extends LiteElement { if (!this.activePanel) return; if (this.activePanel === "artifacts") { return html`

- ${this.workflow?.crawlCount === 1 - ? msg(str`${this.workflow?.crawlCount} Crawl`) - : msg(str`${this.workflow?.crawlCount} Crawls`)} -

`; + ${this.workflow?.crawlCount === 1 + ? msg(str`${this.workflow?.crawlCount} Crawl`) + : msg(str`${this.workflow?.crawlCount} Crawls`)} + + this.runNow()} + ?disabled=${this.workflow?.currCrawlId} + > + + ${msg("Run")} + `; } - - if (this.activePanel === "watch") { + if (this.activePanel === "settings") { return html`

${this.tabLabels[this.activePanel]}

{ - this.openDialogName = "scale"; - this.isDialogVisible = true; - }} + @click=${() => + this.navTo( + `/orgs/${this.workflow?.oid}/workflows/crawl/${this.workflow?.id}?edit` + )} > - - ${msg("Crawler Instances")} + + ${msg("Edit")} `; } + if (this.activePanel === "watch") { + return html`

${this.tabLabels[this.activePanel]}

+ + { + this.openDialogName = "scale"; + this.isDialogVisible = true; + }} + > + + ${msg("Edit Instances")} + + this.stop()} + ?disabled=${!this.workflow?.currCrawlId || + this.isCancelingOrStoppingCrawl || + this.workflow?.currCrawlState === "stopping"} + > + + ${msg("Stop")} + + this.requestCancelCrawl()} + ?disabled=${!this.workflow?.currCrawlId || + this.isCancelingOrStoppingCrawl} + > + + ${msg("Cancel")} + + `; + } return html`

${this.tabLabels[this.activePanel]}

`; } @@ -346,7 +431,6 @@ export class WorkflowDetail extends LiteElement { class="block font-medium rounded-sm mb-2 mr-2 p-2 transition-all ${isActive ? "text-blue-600 bg-blue-50 shadow-sm" : "text-neutral-600 hover:bg-neutral-50"}" - @click=${() => (this.activePanel = tabName)} aria-selected=${isActive} > ${this.tabLabels[tabName]} @@ -384,7 +468,7 @@ export class WorkflowDetail extends LiteElement { const workflow = this.workflow; return html` - + ${msg("Actions")} @@ -396,14 +480,16 @@ export class WorkflowDetail extends LiteElement { () => html` this.stop()} - ?disabled=${workflow.currCrawlState === "stopping"} + ?disabled=${workflow.currCrawlState === "stopping" || + this.isCancelingOrStoppingCrawl} > ${msg("Stop Crawl")} this.cancel()} + ?disabled=${this.isCancelingOrStoppingCrawl} + @click=${() => this.requestCancelCrawl()} > ${msg("Cancel & Discard Crawl")} @@ -624,8 +710,15 @@ export class WorkflowDetail extends LiteElement { hour="2-digit" minute="2-digit" > - -
+ + this.deleteCrawl(crawl)} + > + + ${msg("Delete Crawl")} + + ` ), @@ -887,6 +980,15 @@ export class WorkflowDetail extends LiteElement { this.fetchWorkflow(); } + private requestCancelCrawl() { + const pagesDone = this.currentCrawl?.stats?.done; + if (pagesDone && +pagesDone > 0) { + this.isAttemptCancel = true; + } else { + this.cancel(); + } + } + private async scale(value: Crawl["scale"]) { if (!this.workflow?.currCrawlId) return; this.isSubmittingUpdate = true; @@ -1081,7 +1183,10 @@ export class WorkflowDetail extends LiteElement { private async cancel() { if (!this.workflow?.currCrawlId) return; - if (window.confirm(msg("Are you sure you want to cancel the crawl?"))) { + + this.isCancelingOrStoppingCrawl = true; + + try { const data = await this.apiFetch( `/orgs/${this.orgId}/crawls/${this.workflow.currCrawlId}/cancel`, this.authState!, @@ -1092,18 +1197,25 @@ export class WorkflowDetail extends LiteElement { if (data.success === true) { this.fetchWorkflow(); } else { - this.notify({ - message: msg("Something went wrong, couldn't cancel crawl."), - variant: "danger", - icon: "exclamation-octagon", - }); + throw data; } + } catch { + this.notify({ + message: msg("Something went wrong, couldn't cancel crawl."), + variant: "danger", + icon: "exclamation-octagon", + }); } + + this.isCancelingOrStoppingCrawl = false; } private async stop() { if (!this.workflow?.currCrawlId) return; - if (window.confirm(msg("Are you sure you want to stop the crawl?"))) { + + this.isCancelingOrStoppingCrawl = true; + + try { const data = await this.apiFetch( `/orgs/${this.orgId}/crawls/${this.workflow.currCrawlId}/stop`, this.authState!, @@ -1114,13 +1226,17 @@ export class WorkflowDetail extends LiteElement { if (data.success === true) { this.fetchWorkflow(); } else { - this.notify({ - message: msg("Something went wrong, couldn't stop crawl."), - variant: "danger", - icon: "exclamation-octagon", - }); + throw data; } + } catch { + this.notify({ + message: msg("Something went wrong, couldn't stop crawl."), + variant: "danger", + icon: "exclamation-octagon", + }); } + + this.isCancelingOrStoppingCrawl = false; } private async runNow(): Promise { @@ -1132,7 +1248,6 @@ export class WorkflowDetail extends LiteElement { method: "POST", } ); - this.activePanel = "watch"; this.fetchWorkflow(); this.notify({ @@ -1141,8 +1256,8 @@ export class WorkflowDetail extends LiteElement {
Watch crawl` ), @@ -1158,6 +1273,37 @@ export class WorkflowDetail extends LiteElement { }); } } + + private async deleteCrawl(crawl: Crawl) { + try { + const data = await this.apiFetch( + `/orgs/${crawl.oid}/crawls/delete`, + this.authState!, + { + method: "POST", + body: JSON.stringify({ + crawl_ids: [crawl.id], + }), + } + ); + + this.crawls = this.crawls!.filter((c) => c.id !== crawl.id); + this.notify({ + message: msg(`Successfully deleted crawl`), + variant: "success", + icon: "check2-circle", + }); + this.fetchCrawls(); + } catch (e: any) { + this.notify({ + message: + (e.isApiError && e.message) || + msg("Sorry, couldn't run crawl at this time."), + variant: "danger", + icon: "exclamation-octagon", + }); + } + } } customElements.define("btrix-workflow-detail", WorkflowDetail); diff --git a/frontend/src/pages/org/workflows-list.ts b/frontend/src/pages/org/workflows-list.ts index ea68e5cf..643358d3 100644 --- a/frontend/src/pages/org/workflows-list.ts +++ b/frontend/src/pages/org/workflows-list.ts @@ -227,40 +227,40 @@ export class WorkflowsList extends LiteElement {
- ${msg("Sort by:")} -
- { - const field = e.detail.item.value as SortField; - this.orderBy = { - field: field, - direction: - sortableFields[field].defaultDirection || - this.orderBy.direction, - }; - }} - > - ${Object.entries(sortableFields).map( - ([value, { label }]) => html` - ${label} - ` - )} - - { - this.orderBy = { - ...this.orderBy, - direction: this.orderBy.direction === "asc" ? "desc" : "asc", - }; - }} - > + ${msg("Sort by:")}
+ { + const field = e.detail.item.value as SortField; + this.orderBy = { + field: field, + direction: + sortableFields[field].defaultDirection || + this.orderBy.direction, + }; + }} + > + ${Object.entries(sortableFields).map( + ([value, { label }]) => html` + ${label} + ` + )} + + { + this.orderBy = { + ...this.orderBy, + direction: this.orderBy.direction === "asc" ? "desc" : "asc", + }; + }} + > +
@@ -298,7 +298,9 @@ export class WorkflowsList extends LiteElement {