Show only running crawls in superadmin view (#1015)
- Show separate crawls list for admin view, fixes #1010
This commit is contained in:
		
							parent
							
								
									6506965d98
								
							
						
					
					
						commit
						7069b33646
					
				| @ -1,13 +1,41 @@ | ||||
| import { state, property } from "lit/decorators.js"; | ||||
| import { when } from "lit/directives/when.js"; | ||||
| import { msg, localized, str } from "@lit/localize"; | ||||
| import type { SlSelect } from "@shoelace-style/shoelace"; | ||||
| import queryString from "query-string"; | ||||
| 
 | ||||
| import type { PageChangeEvent } from "../components/pagination"; | ||||
| import { CrawlStatus } from "../components/crawl-status"; | ||||
| import type { AuthState } from "../utils/AuthService"; | ||||
| import LiteElement, { html } from "../utils/LiteElement"; | ||||
| import { needLogin } from "../utils/auth"; | ||||
| import type { Crawl } from "../types/crawler"; | ||||
| import { ROUTES } from "../routes"; | ||||
| import { activeCrawlStates, inactiveCrawlStates } from "../utils/crawler"; | ||||
| import type { Crawl, CrawlState } from "../types/crawler"; | ||||
| import type { APIPaginationQuery, APIPaginatedList } from "../types/api"; | ||||
| import "./org/workflow-detail"; | ||||
| import "./org/crawls-list"; | ||||
| import { PropertyValueMap } from "lit"; | ||||
| 
 | ||||
| type SortField = "started" | "firstSeed" | "fileSize"; | ||||
| type SortDirection = "asc" | "desc"; | ||||
| const sortableFields: Record< | ||||
|   SortField, | ||||
|   { label: string; defaultDirection?: SortDirection } | ||||
| > = { | ||||
|   started: { | ||||
|     label: msg("Date Started"), | ||||
|     defaultDirection: "desc", | ||||
|   }, | ||||
|   firstSeed: { | ||||
|     label: msg("Crawl Start URL"), | ||||
|     defaultDirection: "desc", | ||||
|   }, | ||||
|   fileSize: { | ||||
|     label: msg("File Size"), | ||||
|     defaultDirection: "desc", | ||||
|   }, | ||||
| }; | ||||
| const ABORT_REASON_THROTTLE = "throttled"; | ||||
| 
 | ||||
| @needLogin | ||||
| @localized() | ||||
| @ -21,17 +49,49 @@ export class Crawls extends LiteElement { | ||||
|   @state() | ||||
|   private crawl?: Crawl; | ||||
| 
 | ||||
|   willUpdate(changedProperties: Map<string, any>) { | ||||
|   @state() | ||||
|   private crawls?: APIPaginatedList; | ||||
| 
 | ||||
|   @state() | ||||
|   private orderBy: { | ||||
|     field: SortField; | ||||
|     direction: SortDirection; | ||||
|   } = { | ||||
|     field: "started", | ||||
|     direction: sortableFields["started"].defaultDirection!, | ||||
|   }; | ||||
| 
 | ||||
|   @state() | ||||
|   private filterBy: Partial<Record<keyof Crawl, any>> = { | ||||
|     state: activeCrawlStates, | ||||
|   }; | ||||
| 
 | ||||
|   // Use to cancel requests
 | ||||
|   private getCrawlsController: AbortController | null = null; | ||||
| 
 | ||||
|   protected willUpdate(changedProperties: Map<string, any>) { | ||||
|     if (changedProperties.has("crawlId") && this.crawlId) { | ||||
|       this.fetchWorkflowId(); | ||||
|     } else { | ||||
|       if ( | ||||
|         changedProperties.has("filterBy") || | ||||
|         changedProperties.has("orderBy") | ||||
|       ) { | ||||
|         this.fetchCrawls(); | ||||
|       } | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   disconnectedCallback(): void { | ||||
|     this.cancelInProgressGetCrawls(); | ||||
|     super.disconnectedCallback(); | ||||
|   } | ||||
| 
 | ||||
|   render() { | ||||
|     return html` <div
 | ||||
|       class="w-full max-w-screen-lg mx-auto px-3 py-4 box-border" | ||||
|     > | ||||
|       ${this.crawlId ? this.renderDetail() : this.renderList()} | ||||
|       ${this.crawlId ? this.renderDetail() : this.renderCrawls()} | ||||
|     </div>`;
 | ||||
|   } | ||||
| 
 | ||||
| @ -49,18 +109,194 @@ export class Crawls extends LiteElement { | ||||
|     `;
 | ||||
|   } | ||||
| 
 | ||||
|   private renderList() { | ||||
|     return html`<btrix-crawls-list
 | ||||
|       .authState=${this.authState} | ||||
|       crawlsBaseUrl=${ROUTES.crawls} | ||||
|       crawlsAPIBaseUrl="/orgs/all/crawls" | ||||
|       artifactType="crawl" | ||||
|       isCrawler | ||||
|       isAdminView | ||||
|       shouldFetch | ||||
|     ></btrix-crawls-list>`; | ||||
|   private renderCrawls() { | ||||
|     return html` | ||||
|       <main> | ||||
|         <header class="contents"> | ||||
|           <div class="flex justify-between w-full pb-4 mb-3 border-b"> | ||||
|             <h1 class="text-xl font-semibold h-8"> | ||||
|               ${msg("All Running Crawls")} | ||||
|             </h1> | ||||
|           </div> | ||||
|           <div | ||||
|             class="sticky z-10 mb-3 top-2 p-4 bg-neutral-50 border rounded-lg" | ||||
|           > | ||||
|             ${this.renderControls()} | ||||
|           </div> | ||||
|         </header> | ||||
| 
 | ||||
|         ${when( | ||||
|           this.crawls, | ||||
|           () => { | ||||
|             const { items, page, total, pageSize } = this.crawls!; | ||||
|             const hasCrawlItems = items.length; | ||||
|             return html` | ||||
|               <section> | ||||
|                 ${hasCrawlItems | ||||
|                   ? this.renderCrawlList() | ||||
|                   : this.renderEmptyState()} | ||||
|               </section> | ||||
|               ${when( | ||||
|                 hasCrawlItems || page > 1, | ||||
|                 () => html` | ||||
|                   <footer class="mt-6 flex justify-center"> | ||||
|                     <btrix-pagination | ||||
|                       page=${page} | ||||
|                       totalCount=${total} | ||||
|                       size=${pageSize} | ||||
|                       @page-change=${async (e: PageChangeEvent) => { | ||||
|                         await this.fetchCrawls({ | ||||
|                           page: e.detail.page, | ||||
|                         }); | ||||
| 
 | ||||
|                         // Scroll to top of list
 | ||||
|                         // TODO once deep-linking is implemented, scroll to top of pushstate
 | ||||
|                         this.scrollIntoView({ behavior: "smooth" }); | ||||
|                       }} | ||||
|                     ></btrix-pagination> | ||||
|                   </footer> | ||||
|                 ` | ||||
|               )} | ||||
|             `;
 | ||||
|           }, | ||||
|           () => html` | ||||
|             <div class="w-full flex items-center justify-center my-12 text-2xl"> | ||||
|               <sl-spinner></sl-spinner> | ||||
|             </div> | ||||
|           ` | ||||
|         )} | ||||
|       </main> | ||||
|     `;
 | ||||
|   } | ||||
| 
 | ||||
|   private renderControls() { | ||||
|     const viewPlaceholder = msg("Any Active Status"); | ||||
|     const viewOptions = activeCrawlStates; | ||||
|     return html` | ||||
|       <div class="flex gap-2 items-center justify-end"> | ||||
|         <div class="flex items-center"> | ||||
|           <div class="text-neutral-500 mx-2">${msg("View:")}</div> | ||||
|           <sl-select | ||||
|             id="stateSelect" | ||||
|             class="flex-1 md:w-[14.5rem]" | ||||
|             size="small" | ||||
|             pill | ||||
|             multiple | ||||
|             max-options-visible="1" | ||||
|             placeholder=${viewPlaceholder} | ||||
|             @sl-change=${async (e: CustomEvent) => { | ||||
|               const value = (e.target as SlSelect).value as CrawlState[]; | ||||
|               await this.updateComplete; | ||||
|               this.filterBy = { | ||||
|                 ...this.filterBy, | ||||
|                 state: value, | ||||
|               }; | ||||
|             }} | ||||
|           > | ||||
|             ${viewOptions.map(this.renderStatusMenuItem)} | ||||
|           </sl-select> | ||||
|         </div> | ||||
| 
 | ||||
|         <div class="flex items-center"> | ||||
|           <div class="whitespace-nowrap text-neutral-500 mx-2"> | ||||
|             ${msg("Sort by:")} | ||||
|           </div> | ||||
|           <div class="grow flex">${this.renderSortControl()}</div> | ||||
|         </div> | ||||
|       </div> | ||||
|     `;
 | ||||
|   } | ||||
| 
 | ||||
|   private renderSortControl() { | ||||
|     const options = Object.entries(sortableFields).map( | ||||
|       ([value, { label }]) => html` | ||||
|         <sl-option value=${value}>${label}</sl-option> | ||||
|       ` | ||||
|     ); | ||||
|     return html` | ||||
|       <sl-select | ||||
|         class="flex-1 md:w-[10rem]" | ||||
|         size="small" | ||||
|         pill | ||||
|         value=${this.orderBy.field} | ||||
|         @sl-change=${(e: Event) => { | ||||
|           const field = (e.target as HTMLSelectElement).value as SortField; | ||||
|           this.orderBy = { | ||||
|             field: field, | ||||
|             direction: | ||||
|               sortableFields[field].defaultDirection || this.orderBy.direction, | ||||
|           }; | ||||
|         }} | ||||
|       > | ||||
|         ${options} | ||||
|       </sl-select> | ||||
|       <sl-icon-button | ||||
|         name="arrow-down-up" | ||||
|         label=${msg("Reverse sort")} | ||||
|         @click=${() => { | ||||
|           this.orderBy = { | ||||
|             ...this.orderBy, | ||||
|             direction: this.orderBy.direction === "asc" ? "desc" : "asc", | ||||
|           }; | ||||
|         }} | ||||
|       ></sl-icon-button> | ||||
|     `;
 | ||||
|   } | ||||
| 
 | ||||
|   private renderStatusMenuItem = (state: CrawlState) => { | ||||
|     const { icon, label } = CrawlStatus.getContent(state); | ||||
| 
 | ||||
|     return html`<sl-option value=${state}>${icon}${label}</sl-option>`; | ||||
|   }; | ||||
| 
 | ||||
|   private renderCrawlList() { | ||||
|     if (!this.crawls) return; | ||||
| 
 | ||||
|     return html` | ||||
|       <btrix-crawl-list baseUrl=${"/crawls/crawl"} artifactType="crawl"> | ||||
|         ${this.crawls.items.map(this.renderCrawlItem)} | ||||
|       </btrix-crawl-list> | ||||
|     `;
 | ||||
|   } | ||||
| 
 | ||||
|   private renderEmptyState() { | ||||
|     if (this.crawls?.page && this.crawls?.page > 1) { | ||||
|       return html` | ||||
|         <div class="border-t border-b py-5"> | ||||
|           <p class="text-center text-neutral-500"> | ||||
|             ${msg("Could not find page.")} | ||||
|           </p> | ||||
|         </div> | ||||
|       `;
 | ||||
|     } | ||||
| 
 | ||||
|     return html` | ||||
|       <div class="border-t border-b py-5"> | ||||
|         <p class="text-center text-neutral-500"> | ||||
|           ${msg("No matching crawls found.")} | ||||
|         </p> | ||||
|       </div> | ||||
|     `;
 | ||||
|   } | ||||
| 
 | ||||
|   private renderCrawlItem = (crawl: Crawl) => | ||||
|     html` | ||||
|       <btrix-crawl-list-item .crawl=${crawl}> | ||||
|         <sl-menu slot="menu"> | ||||
|           <sl-menu-item | ||||
|             @click=${() => | ||||
|               this.navTo( | ||||
|                 `/orgs/${crawl.oid}/artifacts/${ | ||||
|                   crawl.type === "upload" ? "upload" : "crawl" | ||||
|                 }/${crawl.id}` | ||||
|               )} | ||||
|           > | ||||
|             ${msg("View Crawl Details")} | ||||
|           </sl-menu-item> | ||||
|         </sl-menu> | ||||
|       </btrix-crawl-list-item> | ||||
|     `;
 | ||||
| 
 | ||||
|   private async fetchWorkflowId() { | ||||
|     try { | ||||
|       this.crawl = await this.getCrawl(); | ||||
| @ -69,6 +305,63 @@ export class Crawls extends LiteElement { | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   /** | ||||
|    * Fetch crawls and update internal state | ||||
|    */ | ||||
|   private async fetchCrawls(params?: APIPaginationQuery): Promise<void> { | ||||
|     this.cancelInProgressGetCrawls(); | ||||
|     try { | ||||
|       this.crawls = await this.getCrawls(params); | ||||
|     } catch (e: any) { | ||||
|       if (e === ABORT_REASON_THROTTLE) { | ||||
|         console.debug("Fetch crawls aborted to throttle"); | ||||
|       } else { | ||||
|         this.notify({ | ||||
|           message: msg("Sorry, couldn't retrieve crawls at this time."), | ||||
|           variant: "danger", | ||||
|           icon: "exclamation-octagon", | ||||
|         }); | ||||
|       } | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   private cancelInProgressGetCrawls() { | ||||
|     if (this.getCrawlsController) { | ||||
|       this.getCrawlsController.abort(ABORT_REASON_THROTTLE); | ||||
|       this.getCrawlsController = null; | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   private async getCrawls( | ||||
|     queryParams?: APIPaginationQuery & { state?: CrawlState[] } | ||||
|   ): Promise<APIPaginatedList> { | ||||
|     const query = queryString.stringify( | ||||
|       { | ||||
|         ...this.filterBy, | ||||
|         ...queryParams, | ||||
|         page: queryParams?.page || this.crawls?.page || 1, | ||||
|         pageSize: queryParams?.pageSize || this.crawls?.pageSize || 100, | ||||
|         sortBy: this.orderBy.field, | ||||
|         sortDirection: this.orderBy.direction === "desc" ? -1 : 1, | ||||
|       }, | ||||
|       { | ||||
|         arrayFormat: "comma", | ||||
|       } | ||||
|     ); | ||||
| 
 | ||||
|     this.getCrawlsController = new AbortController(); | ||||
|     const data = await this.apiFetch( | ||||
|       `/orgs/all/crawls?${query}`, | ||||
|       this.authState!, | ||||
|       { | ||||
|         signal: this.getCrawlsController.signal, | ||||
|       } | ||||
|     ); | ||||
|     this.getCrawlsController = null; | ||||
| 
 | ||||
|     return data; | ||||
|   } | ||||
| 
 | ||||
|   private async getCrawl(): Promise<Crawl> { | ||||
|     const data: Crawl = await this.apiFetch( | ||||
|       `/orgs/all/crawls/${this.crawlId}/replay.json`, | ||||
|  | ||||
| @ -91,11 +91,6 @@ export class CrawlsList extends LiteElement { | ||||
|   @property({ type: Boolean }) | ||||
|   isCrawler!: boolean; | ||||
| 
 | ||||
|   // TODO better handling of using same crawls-list
 | ||||
|   // component between superadmin view and regular view
 | ||||
|   @property({ type: Boolean }) | ||||
|   isAdminView = false; | ||||
| 
 | ||||
|   // e.g. `/org/${this.orgId}/crawls`
 | ||||
|   @property({ type: String }) | ||||
|   crawlsBaseUrl!: string; | ||||
| @ -178,12 +173,6 @@ export class CrawlsList extends LiteElement { | ||||
|   } | ||||
| 
 | ||||
|   protected willUpdate(changedProperties: Map<string, any>) { | ||||
|     if (changedProperties.has("isAdminView") && this.isAdminView === true) { | ||||
|       this.orderBy = { | ||||
|         field: "started", | ||||
|         direction: sortableFields["started"].defaultDirection!, | ||||
|       }; | ||||
|     } | ||||
|     if ( | ||||
|       changedProperties.has("shouldFetch") || | ||||
|       changedProperties.get("crawlsBaseUrl") || | ||||
| @ -226,10 +215,7 @@ export class CrawlsList extends LiteElement { | ||||
|       changedProperties.has("crawlsBaseUrl") || | ||||
|       changedProperties.has("crawlsAPIBaseUrl") | ||||
|     ) { | ||||
|       // TODO add back when API supports `orgs/all/crawlconfigs`
 | ||||
|       if (!this.isAdminView) { | ||||
|         this.fetchConfigSearchValues(); | ||||
|       } | ||||
|       this.fetchConfigSearchValues(); | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
| @ -244,30 +230,21 @@ export class CrawlsList extends LiteElement { | ||||
|       label: string; | ||||
|       icon?: string; | ||||
|     }[] = [ | ||||
|       { | ||||
|         artifactType: null, | ||||
|         label: msg("All"), | ||||
|       }, | ||||
|       { | ||||
|         artifactType: "crawl", | ||||
|         icon: "gear-wide-connected", | ||||
|         label: msg("Crawls"), | ||||
|       }, | ||||
|     ]; | ||||
| 
 | ||||
|     if (this.isAdminView) { | ||||
|       listTypes.unshift({ | ||||
|         artifactType: "crawl", | ||||
|         icon: "gear-wide", | ||||
|         label: msg("Running Crawls"), | ||||
|       }); | ||||
|     } else { | ||||
|       listTypes.unshift({ | ||||
|         artifactType: null, | ||||
|         label: msg("All"), | ||||
|       }); | ||||
|       listTypes.push({ | ||||
|       { | ||||
|         artifactType: "upload", | ||||
|         icon: "upload", | ||||
|         label: msg("Uploads"), | ||||
|       }); | ||||
|     } | ||||
|       }, | ||||
|     ]; | ||||
| 
 | ||||
|     return html` | ||||
|       <main> | ||||
| @ -376,25 +353,18 @@ export class CrawlsList extends LiteElement { | ||||
|   } | ||||
| 
 | ||||
|   private renderControls() { | ||||
|     let viewPlaceholder = ""; | ||||
|     let viewOptions = []; | ||||
|     if (this.isAdminView) { | ||||
|       viewPlaceholder = msg("All Active Crawls"); | ||||
|       viewOptions = activeCrawlStates; | ||||
|     } else { | ||||
|       viewOptions = finishedCrawlStates; | ||||
|       if (this.artifactType === "upload") { | ||||
|         viewPlaceholder = msg("All Uploaded"); | ||||
|       } else { | ||||
|         viewPlaceholder = msg("All Finished"); | ||||
|       } | ||||
|     } | ||||
|     const viewPlaceholder = | ||||
|       this.artifactType === "upload" | ||||
|         ? msg("All Uploaded") | ||||
|         : msg("All Finished"); | ||||
|     const viewOptions = finishedCrawlStates; | ||||
| 
 | ||||
|     return html` | ||||
|       <div | ||||
|         class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-[minmax(0,100%)_fit-content(100%)_fit-content(100%)] gap-x-2 gap-y-2 items-center" | ||||
|       > | ||||
|         <div class="col-span-1 md:col-span-2 lg:col-span-1"> | ||||
|           ${when(!this.isAdminView, () => this.renderSearch())} | ||||
|           ${this.renderSearch()} | ||||
|         </div> | ||||
|         <div class="flex items-center"> | ||||
|           <div class="text-neutral-500 mx-2">${msg("View:")}</div> | ||||
| @ -586,7 +556,7 @@ export class CrawlsList extends LiteElement { | ||||
| 
 | ||||
|     return html` | ||||
|       <btrix-crawl-list | ||||
|         baseUrl=${this.isAdminView ? "/crawls/crawl" : ""} | ||||
|         baseUrl="" | ||||
|         artifactType=${ifDefined(this.artifactType || undefined)} | ||||
|       > | ||||
|         ${this.crawls.items.map(this.renderCrawlItem)} | ||||
| @ -781,12 +751,6 @@ export class CrawlsList extends LiteElement { | ||||
|             state: this.filterBy.state || finishedCrawlStates, | ||||
|           }); | ||||
|           break; | ||||
|         // case "crawl":
 | ||||
|         //   crawls = await this.getCrawls({
 | ||||
|         //     ...params,
 | ||||
|         //     state: this.filterBy.state || activeCrawlStates,
 | ||||
|         //   });
 | ||||
|         //   break;
 | ||||
|         case "upload": | ||||
|           crawls = await this.getUploads(params); | ||||
|           break; | ||||
|  | ||||
		Loading…
	
		Reference in New Issue
	
	Block a user