Add info bar to Collection detail view (#1036)

- Adds Collection info bar to detail view
- Update "Web Captures" -> "Archived Items"
- Updates Collection list columns to match
- Refactors `btrix-desc-list` and usage in `workflow-details` to reuse horizontal info bar component
This commit is contained in:
sua yoo 2023-08-03 16:58:56 -07:00 committed by GitHub
parent af09d56ef6
commit 62d3399223
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 166 additions and 80 deletions

View File

@ -16,9 +16,14 @@
*/
import { LitElement, html, css } from "lit";
import { property } from "lit/decorators.js";
import { classMap } from "lit/directives/class-map.js";
export class DescListItem extends LitElement {
static styles = css`
:host {
display: contents;
}
dt {
color: var(--sl-color-neutral-500);
font-size: var(--sl-font-size-x-small);
@ -28,22 +33,30 @@ export class DescListItem extends LitElement {
dd {
margin: 0;
padding: 0;
padding: 0 0 var(--sl-spacing-2x-small);
color: var(--sl-color-neutral-700);
font-size: var(--sl-font-size-medium);
font-family: var(--font-monostyle-family);
font-variation-settings: var(--font-monostyle-variation);
line-height: 1rem;
}
.item {
display: flex;
justify-content: var(--justify-item, initial);
border-right: var(--border-right, 0px);
}
`;
@property({ type: String })
label: string = "";
render() {
return html`<div>
<dt>${this.label}</dt>
<dd><slot></slot></dd>
return html`<div class="item">
<div class="content">
<dt>${this.label}</dt>
<dd><slot></slot></dd>
</div>
</div>`;
}
}
@ -52,13 +65,38 @@ export class DescList extends LitElement {
static styles = css`
dl {
display: grid;
grid-template-columns: 100%;
grid-gap: 1rem;
margin: 0;
}
.vertical {
grid-template-columns: 100%;
gap: 1rem;
}
.horizontal {
--justify-item: center;
--border-right: 1px solid var(--sl-panel-border-color);
grid-auto-flow: column;
}
/* Although this only applies to .horizontal, apply to any last child
since we can't do complex selectors with ::slotted */
::slotted(*:last-of-type) {
--border-right: 0px;
}
`;
@property({ type: Boolean })
horizontal = false;
render() {
return html`<dl><slot></slot></dl>`;
return html`<dl
class=${classMap({
vertical: !this.horizontal,
horizontal: this.horizontal,
})}
>
<slot></slot>
</dl>`;
}
}

View File

@ -4,6 +4,7 @@ import { choose } from "lit/directives/choose.js";
import { when } from "lit/directives/when.js";
import { guard } from "lit/directives/guard.js";
import queryString from "query-string";
import type { TemplateResult } from "lit";
import type { AuthState } from "../../utils/AuthService";
import LiteElement, { html } from "../../utils/LiteElement";
@ -18,7 +19,7 @@ import type { PageChangeEvent } from "../../components/pagination";
const ABORT_REASON_THROTTLE = "throttled";
const DESCRIPTION_MAX_HEIGHT_PX = 200;
const TABS = ["replay", "web-captures"] as const;
const TABS = ["replay", "items"] as const;
export type Tab = (typeof TABS)[number];
@localized()
@ -42,7 +43,7 @@ export class CollectionDetail extends LiteElement {
private collection?: Collection;
@state()
private webCaptures?: APIPaginatedList;
private archivedItems?: APIPaginatedList;
@state()
private openDialogName?: "delete";
@ -51,16 +52,21 @@ export class CollectionDetail extends LiteElement {
private isDescriptionExpanded = false;
// Use to cancel requests
private getWebCapturesController: AbortController | null = null;
private getArchivedItemsController: AbortController | null = null;
// TODO localize
private numberFormatter = new Intl.NumberFormat(undefined, {
notation: "compact",
});
private readonly tabLabels: Record<Tab, { icon: any; text: string }> = {
replay: {
icon: { name: "link-replay", library: "app" },
text: msg("Replay"),
},
"web-captures": {
items: {
icon: { name: "list-ul", library: "default" },
text: msg("Web Captures"),
text: msg("Archived Items"),
},
};
@ -70,7 +76,7 @@ export class CollectionDetail extends LiteElement {
this.fetchCollection();
}
if (changedProperties.has("collectionId")) {
this.fetchWebCaptures();
this.fetchArchivedItems();
}
}
@ -82,7 +88,7 @@ export class CollectionDetail extends LiteElement {
render() {
return html`${this.renderHeader()}
<header class="md:flex items-center gap-2 pb-3 mb-3 border-b">
<header class="md:flex items-center gap-2 pb-3">
<h1
class="flex-1 min-w-0 text-xl font-semibold leading-7 truncate mb-2 md:mb-0"
>
@ -91,13 +97,14 @@ export class CollectionDetail extends LiteElement {
</h1>
${when(this.isCrawler, this.renderActions)}
</header>
<div class="border rounded-lg py-2 mb-3">${this.renderInfoBar()}</div>
<div class="mb-3">${this.renderTabs()}</div>
${choose(
this.resourceTab,
[
["replay", this.renderOverview],
["web-captures", this.renderWebCaptures],
["items", this.renderArchivedItems],
],
() => html`<btrix-not-found></btrix-not-found>`
@ -215,6 +222,56 @@ export class CollectionDetail extends LiteElement {
`;
};
private renderInfoBar() {
return html`
<btrix-desc-list horizontal>
${this.renderDetailItem(msg("Archived Items"), (col) =>
col.crawlCount === 1
? msg("1 item")
: msg(str`${this.numberFormatter.format(col.crawlCount)} items`)
)}
${this.renderDetailItem(
msg("Total Size"),
(col) => html`<sl-format-bytes
value=${col.totalSize || 0}
display="narrow"
></sl-format-bytes>`
)}
${this.renderDetailItem(msg("Total Pages"), (col) =>
col.pageCount === 1
? msg("1 page")
: msg(str`${this.numberFormatter.format(col.pageCount)} pages`)
)}
${this.renderDetailItem(
msg("Last Updated"),
(col) => html`<sl-format-date
date=${`${col.modified}Z`}
month="2-digit"
day="2-digit"
year="2-digit"
hour="2-digit"
minute="2-digit"
></sl-format-date>`
)}
</btrix-desc-list>
`;
}
private renderDetailItem(
label: string | TemplateResult,
renderContent: (collection: Collection) => any
) {
return html`
<btrix-desc-list-item label=${label}>
${when(
this.collection,
() => renderContent(this.collection!),
() => html`<sl-skeleton class="w-full"></sl-skeleton>`
)}
</btrix-desc-list-item>
`;
}
private renderDescription() {
return html`
<section>
@ -289,15 +346,17 @@ export class CollectionDetail extends LiteElement {
<div class="my-7">${this.renderDescription()}</div>
`;
private renderWebCaptures = () => html`<section>
private renderArchivedItems = () => html`<section>
${when(
this.webCaptures,
this.archivedItems,
() => {
const { items, page, total, pageSize } = this.webCaptures!;
const { items, page, total, pageSize } = this.archivedItems!;
const hasItems = items.length;
return html`
<section>
${hasItems ? this.renderWebCaptureList() : this.renderEmptyState()}
${hasItems
? this.renderArchivedItemsList()
: this.renderEmptyState()}
</section>
${when(
hasItems || page > 1,
@ -308,7 +367,7 @@ export class CollectionDetail extends LiteElement {
totalCount=${total}
size=${pageSize}
@page-change=${async (e: PageChangeEvent) => {
await this.fetchWebCaptures({
await this.fetchArchivedItems({
page: e.detail.page,
});
@ -330,20 +389,20 @@ export class CollectionDetail extends LiteElement {
)}
</section>`;
private renderWebCaptureList() {
if (!this.webCaptures) return;
private renderArchivedItemsList() {
if (!this.archivedItems) return;
return html`
<btrix-crawl-list
baseUrl=${`/orgs/${this.orgId}/collections/view/${this.collectionId}/artifact`}
>
${this.webCaptures.items.map(this.renderWebCaptureItem)}
${this.archivedItems.items.map(this.renderArchivedItem)}
</btrix-crawl-list>
`;
}
private renderEmptyState() {
if (this.webCaptures?.page && this.webCaptures?.page > 1) {
if (this.archivedItems?.page && this.archivedItems?.page > 1) {
return html`
<div class="border-t border-b py-5">
<p class="text-center text-neutral-500">
@ -362,7 +421,7 @@ export class CollectionDetail extends LiteElement {
`;
}
private renderWebCaptureItem = (wc: Crawl | Upload) =>
private renderArchivedItem = (wc: Crawl | Upload) =>
html`
<btrix-crawl-list-item .crawl=${wc}>
<div slot="menuTrigger" role="none"></div>
@ -478,10 +537,10 @@ export class CollectionDetail extends LiteElement {
/**
* Fetch web captures and update internal state
*/
private async fetchWebCaptures(params?: APIPaginationQuery): Promise<void> {
this.cancelInProgressGetWebCaptures();
private async fetchArchivedItems(params?: APIPaginationQuery): Promise<void> {
this.cancelInProgressGetArchivedItems();
try {
this.webCaptures = await this.getWebCaptures();
this.archivedItems = await this.getArchivedItems();
} catch (e: any) {
if (e === ABORT_REASON_THROTTLE) {
console.debug("Fetch web captures aborted to throttle");
@ -495,14 +554,14 @@ export class CollectionDetail extends LiteElement {
}
}
private cancelInProgressGetWebCaptures() {
if (this.getWebCapturesController) {
this.getWebCapturesController.abort(ABORT_REASON_THROTTLE);
this.getWebCapturesController = null;
private cancelInProgressGetArchivedItems() {
if (this.getArchivedItemsController) {
this.getArchivedItemsController.abort(ABORT_REASON_THROTTLE);
this.getArchivedItemsController = null;
}
}
private async getWebCaptures(
private async getArchivedItems(
params?: Partial<{
state: CrawlState[];
}> &

View File

@ -369,14 +369,14 @@ export class CollectionsList extends LiteElement {
return html`
<header class="py-2 text-neutral-600 leading-none">
<div
class="hidden md:grid md:grid-cols-[repeat(2,1fr)_16ch_repeat(3,10ch)_2.5rem] gap-3"
class="hidden md:grid md:grid-cols-[repeat(2,1fr)_repeat(3,12ch)_16ch_2.5rem] gap-3"
>
<div class="col-span-1 text-xs pl-3">${msg("Collection Name")}</div>
<div class="col-span-1 text-xs">${msg("Top 3 Tags")}</div>
<div class="col-span-1 text-xs">${msg("Last Updated")}</div>
<div class="col-span-1 text-xs">${msg("Size")}</div>
<div class="col-span-1 text-xs">${msg("Web Captures")}</div>
<div class="col-span-2 text-xs">${msg("Total Pages")}</div>
<div class="col-span-1 text-xs">${msg("Archived Items")}</div>
<div class="col-span-1 text-xs">${msg("Total Size")}</div>
<div class="col-span-1 text-xs">${msg("Total Pages")}</div>
<div class="col-span-2 text-xs">${msg("Last Updated")}</div>
</div>
</header>
<ul class="contents">
@ -446,7 +446,7 @@ export class CollectionsList extends LiteElement {
html`<li class="mb-2 last:mb-0">
<div class="block border rounded leading-none">
<div
class="relative p-3 md:p-0 grid grid-cols-1 md:grid-cols-[repeat(2,1fr)_16ch_repeat(3,10ch)_2.5rem] gap-3 lg:h-10 items-center"
class="relative p-3 md:p-0 grid grid-cols-1 md:grid-cols-[repeat(2,1fr)_repeat(3,12ch)_16ch_2.5rem] gap-3 lg:h-10 items-center"
>
<div class="col-span-1 md:pl-3 truncate font-semibold">
<a
@ -465,15 +465,12 @@ export class CollectionsList extends LiteElement {
html`<btrix-tag class="mr-1" size="small">${tag}</btrix-tag>`
)}
</div>
<div class="col-span-1 text-xs text-neutral-500 font-monostyle">
<sl-format-date
date=${`${col.modified}Z`}
month="2-digit"
day="2-digit"
year="2-digit"
hour="2-digit"
minute="2-digit"
></sl-format-date>
<div
class="col-span-1 truncate text-xs text-neutral-500 font-monostyle"
>
${col.crawlCount === 1
? msg("1 item")
: msg(str`${this.numberFormatter.format(col.crawlCount)} items`)}
</div>
<div
class="col-span-1 truncate text-xs text-neutral-500 font-monostyle"
@ -483,15 +480,6 @@ export class CollectionsList extends LiteElement {
display="narrow"
></sl-format-bytes>
</div>
<div
class="col-span-1 truncate text-xs text-neutral-500 font-monostyle"
>
${col.crawlCount === 1
? msg("1 capture")
: msg(
str`${this.numberFormatter.format(col.crawlCount)} captures`
)}
</div>
<div
class="col-span-1 truncate text-xs text-neutral-500 font-monostyle"
>
@ -499,6 +487,16 @@ export class CollectionsList extends LiteElement {
? msg("1 page")
: msg(str`${this.numberFormatter.format(col.pageCount)} pages`)}
</div>
<div class="col-span-1 text-xs text-neutral-500 font-monostyle">
<sl-format-date
date=${`${col.modified}Z`}
month="2-digit"
day="2-digit"
year="2-digit"
hour="2-digit"
minute="2-digit"
></sl-format-date>
</div>
<div
class="actionsCol absolute top-0 right-0 md:relative col-span-1 flex items-center justify-center"
>

View File

@ -306,7 +306,7 @@ export class WorkflowDetail extends LiteElement {
</div>
</header>
<section class="col-span-1 border rounded-lg py-2 h-14">
<section class="col-span-1 border rounded-lg py-2">
${this.renderDetails()}
</section>
@ -683,7 +683,7 @@ export class WorkflowDetail extends LiteElement {
private renderDetails() {
return html`
<dl class="h-14 px-3 md:px-0 md:flex justify-evenly">
<btrix-desc-list horizontal>
${this.renderDetailItem(
msg("Status"),
() => html`
@ -711,26 +711,20 @@ export class WorkflowDetail extends LiteElement {
`
: html`<span class="text-neutral-400">${msg("No Schedule")}</span>`
)}
${this.renderDetailItem(
msg("Created By"),
() =>
msg(
str`${
this.workflow!.createdByName
} on ${this.dateFormatter.format(
new Date(`${this.workflow!.created}Z`)
)}`
),
true
${this.renderDetailItem(msg("Created By"), () =>
msg(
str`${this.workflow!.createdByName} on ${this.dateFormatter.format(
new Date(`${this.workflow!.created}Z`)
)}`
)
)}
</dl>
</btrix-desc-list>
`;
}
private renderDetailItem(
label: string | TemplateResult,
renderContent: () => any,
isLast = false
renderContent: () => any
) {
return html`
<btrix-desc-list-item label=${label}>
@ -740,7 +734,6 @@ export class WorkflowDetail extends LiteElement {
() => html`<sl-skeleton class="w-full"></sl-skeleton>`
)}
</btrix-desc-list-item>
${when(!isLast, () => html`<hr class="flex-0 border-l w-0 h-10" />`)}
`;
}
@ -878,7 +871,7 @@ export class WorkflowDetail extends LiteElement {
const skeleton = html`<sl-skeleton class="w-full"></sl-skeleton>`;
return html`
<dl class="px-3 md:px-0 md:flex justify-evenly">
<btrix-desc-list horizontal>
${this.renderDetailItem(msg("Pages Crawled"), () =>
this.lastCrawlStats
? msg(
@ -906,12 +899,10 @@ export class WorkflowDetail extends LiteElement {
></sl-format-bytes>`
: skeleton
)}
${this.renderDetailItem(
msg("Crawler Instances"),
() => (this.workflow ? this.workflow.scale : skeleton),
true
${this.renderDetailItem(msg("Crawler Instances"), () =>
this.workflow ? this.workflow.scale : skeleton
)}
</dl>
</btrix-desc-list>
`;
};