browsertrix/frontend/src/features/archived-items/archived-item-list.ts
2024-06-12 23:59:56 -07:00

472 lines
14 KiB
TypeScript

import { localized, msg, str } from "@lit/localize";
import type { SlCheckbox, SlHideEvent } from "@shoelace-style/shoelace";
import { css, html, nothing, type TemplateResult } from "lit";
import {
customElement,
property,
query,
queryAssignedElements,
state,
} from "lit/decorators.js";
import { ifDefined } from "lit/directives/if-defined.js";
import { CrawlStatus } from "./crawl-status";
import { TailwindElement } from "@/classes/TailwindElement";
import { NavigateController } from "@/controllers/navigate";
import { ReviewStatus, type ArchivedItem } from "@/types/crawler";
import { renderName } from "@/utils/crawler";
import {
formatISODateString,
formatNumber,
getLocale,
} from "@/utils/localization";
export type CheckboxChangeEventDetail = {
checked: boolean;
};
/**
* @slot actionCell - Action cell
* @fires btrix-checkbox-change
*/
@localized()
@customElement("btrix-archived-item-list-item")
export class ArchivedItemListItem extends TailwindElement {
static styles = css`
:host {
display: contents;
}
btrix-table-row {
border-top: var(--btrix-border-top, 0);
border-radius: var(--btrix-border-radius-top, 0)
var(--btrix-border-radius-to, 0) var(--btrix-border-radius-bottom, 0)
var(--btrix-border-radius-bottom, 0);
height: 2.5rem;
}
sl-progress-ring {
/* Setting size to var(--font-size-base) breaks in chrome,
have cell contents inherit size from cell instead */
--size: 1em;
--track-width: 1px;
--indicator-width: 2px;
}
`;
@property({ type: Object, attribute: false })
item?: ArchivedItem;
@property({ type: Boolean })
checkbox = false;
@property({ type: Boolean })
checked = false;
@property({ type: Boolean })
showStatus = false;
@property({ type: Number })
index = 0;
@property({ type: String })
href?: string;
@query("sl-checkbox")
readonly checkboxEl?: SlCheckbox;
@query(".rowLink")
private readonly rowLink?: HTMLAnchorElement;
private readonly navigate = new NavigateController(this);
render() {
if (!this.item) return;
const checkboxId = `${this.item.id}-checkbox`;
const rowName = renderName(this.item);
const isUpload = this.item.type === "upload";
const crawlStatus = CrawlStatus.getContent(this.item.state, this.item.type);
let typeLabel = msg("Crawl");
let typeIcon = "gear-wide-connected";
if (isUpload) {
typeLabel = msg("Upload");
typeIcon = "upload";
}
const notApplicable = html`<sl-tooltip
hoist
content=${msg("Not applicable")}
>
<sl-icon
name="slash"
class="text-base text-neutral-400"
label=${msg("Not applicable")}
></sl-icon>
</sl-tooltip>`;
const none = html`<sl-tooltip hoist content=${msg("None")}>
<sl-icon
name="slash"
class="text-base text-neutral-400"
label=${msg("None")}
></sl-icon>
</sl-tooltip>`;
const { activeQAStats, lastQAState, lastQAStarted, qaRunCount } = this.item;
const activeProgress = activeQAStats?.found
? Math.round((100 * activeQAStats.done) / activeQAStats.found)
: 0;
const qaStatus = CrawlStatus.getContent(lastQAState || undefined);
return html`
<btrix-table-row
class=${this.href || this.checkbox
? "cursor-pointer select-none transition-colors hover:bg-neutral-50 focus-within:bg-neutral-50"
: ""}
>
${this.checkbox
? html`
<btrix-table-cell class="pr-0">
<sl-checkbox
id=${checkboxId}
class="flex"
?checked=${this.checked}
@sl-change=${(e: CustomEvent) => {
this.dispatchEvent(
new CustomEvent<CheckboxChangeEventDetail>(
"btrix-checkbox-change",
{
detail: {
checked: (e.currentTarget as SlCheckbox).checked,
},
},
),
);
}}
></sl-checkbox>
</btrix-table-cell>
`
: nothing}
<btrix-table-cell class="pr-0 text-base">
${this.showStatus
? html`
<btrix-crawl-status
state=${this.item.state}
hideLabel
?isUpload=${isUpload}
></btrix-crawl-status>
`
: html`
<sl-tooltip
content=${msg(str`${typeLabel}: ${crawlStatus.label}`)}
@sl-hide=${(e: SlHideEvent) => e.stopPropagation()}
@sl-after-hide=${(e: SlHideEvent) => e.stopPropagation()}
hoist
>
<sl-icon
class="text-inherit"
style="color: ${crawlStatus.cssColor}"
name=${typeIcon}
label=${typeLabel}
></sl-icon>
</sl-tooltip>
`}
<sl-tooltip
hoist
content=${activeQAStats
? msg(
str`QA Analysis: ${qaStatus.label} (${activeProgress}% finished)`,
)
: msg(
str`QA Analysis: ${isUpload ? "Not Applicable" : qaStatus.label || msg("None")}`,
)}
>
${activeQAStats
? html`
<sl-progress-ring
value="${activeProgress}"
style="color: ${qaStatus.cssColor};"
></sl-progress-ring>
`
: html`
<sl-icon
class="text-inherit"
style="color: ${qaStatus.cssColor}"
name=${isUpload ? "slash" : "microscope"}
library=${isUpload ? "default" : "app"}
></sl-icon>
`}
</sl-tooltip>
</btrix-table-cell>
<btrix-table-cell
rowClickTarget=${ifDefined(
this.href ? "a" : this.checkbox ? "label" : undefined,
)}
>
${this.href
? html`<a
class="rowLink overflow-hidden"
href=${this.href}
@click=${this.navigate.link}
>
${rowName}
</a>`
: this.checkbox
? html`<label
for=${checkboxId}
@click=${() => {
// We need to simulate click anyway, since external label click
// won't work with the shoelace checkbox
this.checkboxEl?.click();
}}
>
${rowName}
</label>`
: html`<div>${rowName}</div>`}
</btrix-table-cell>
<btrix-table-cell class="tabular-nums">
<sl-tooltip
content=${msg(str`By ${this.item.userName}`)}
@click=${this.onTooltipClick}
hoist
>
<sl-format-date
lang=${getLocale()}
class="truncate"
date=${`${this.item.finished}Z`}
month="2-digit"
day="2-digit"
year="2-digit"
></sl-format-date>
</sl-tooltip>
</btrix-table-cell>
<btrix-table-cell class="tabular-nums">
<sl-tooltip
hoist
content=${formatNumber(this.item.fileSize || 0, {
style: "unit",
unit: "byte",
unitDisplay: "long",
})}
@click=${this.onTooltipClick}
>
<sl-format-bytes
class="truncate"
value=${this.item.fileSize || 0}
display="narrow"
></sl-format-bytes>
</sl-tooltip>
</btrix-table-cell>
<btrix-table-cell class="tabular-nums">
${isUpload
? notApplicable
: html`<sl-tooltip
hoist
@click=${this.onTooltipClick}
content=${msg(
str`${formatNumber(
this.item.stats?.done ? +this.item.stats.done : 0,
)} crawled, ${formatNumber(this.item.stats?.found ? +this.item.stats.found : 0)} found`,
)}
>
<div class="min-w-4">
${formatNumber(
this.item.stats?.done ? +this.item.stats.done : 0,
{
notation: "compact",
},
)}
</div>
</sl-tooltip>`}
</btrix-table-cell>
<btrix-table-cell class="tabular-nums">
${isUpload
? notApplicable
: lastQAStarted && qaRunCount
? html`
<sl-tooltip
hoist
content=${msg(
str`Last run started on ${formatISODateString(lastQAStarted)}`,
)}
>
<div class="min-w-4">
${formatNumber(qaRunCount, {
notation: "compact",
})}
</div>
</sl-tooltip>
`
: none}
</btrix-table-cell>
<btrix-table-cell>
${isUpload
? notApplicable
: html`<sl-tooltip
hoist
@click=${this.onTooltipClick}
content=${this.item.reviewStatus
? msg(
str`Rated ${this.item.reviewStatus} / ${ReviewStatus.Excellent}`,
)
: msg("No QA review submitted")}
>
<btrix-qa-review-status
.status=${this.item.reviewStatus}
></btrix-qa-review-status>
</sl-tooltip>`}
</btrix-table-cell>
<slot name="actionCell"></slot>
</btrix-table-row>
`;
}
// FIXME Tooltips are enabled by styling them in in table.stylesheet.css
// to have a z-index higher than the anchor link overlay.
// Should probably fix this in table-cell or table-row instead
private onTooltipClick() {
this.rowLink?.click();
}
}
/**
* @example Usage:
* ```ts
* <btrix-archived-item-list>
* <btrix-archived-item-list-item .item=${item}
* ></btrix-archived-item-list-item>
* </btrix-archived-item-list>
* ```
*
* @slot checkboxCell
* @slot actionCell
*/
@localized()
@customElement("btrix-archived-item-list")
export class ArchivedItemList extends TailwindElement {
static styles = css`
btrix-table {
--btrix-cell-gap: var(--sl-spacing-x-small);
--btrix-cell-padding-left: var(--sl-spacing-small);
--btrix-cell-padding-right: var(--sl-spacing-small);
}
btrix-table-body ::slotted(*:nth-of-type(n + 2)) {
--btrix-border-top: 1px solid var(--sl-panel-border-color);
}
btrix-table-body ::slotted(*:first-of-type) {
--btrix-border-radius-top: var(--sl-border-radius-medium);
}
btrix-table-body ::slotted(*:last-of-type) {
--btrix-border-radius-bottom: var(--sl-border-radius-medium);
}
`;
@property({ type: String })
listType: ArchivedItem["type"] | null = null;
@queryAssignedElements({ selector: "btrix-archived-item-list-item" })
public items!: ArchivedItemListItem[];
@state()
private hasCheckboxCell = false;
@state()
private hasActionCell = false;
render() {
const headerCols: { cssCol: string; cell: TemplateResult<1> | symbol }[] = [
{
cssCol: "min-content",
cell: html`<btrix-table-header-cell>
${msg("Status")}
</btrix-table-header-cell>`,
},
{
cssCol: "[clickable-start] 50ch",
cell: html`<btrix-table-header-cell>
${msg("Name")}
</btrix-table-header-cell>`,
},
{
cssCol: "1fr",
cell: html`<btrix-table-header-cell>
${msg("Date Created")}
</btrix-table-header-cell>`,
},
{
cssCol: "1fr",
cell: html`<btrix-table-header-cell>
${msg("Size")}
</btrix-table-header-cell>`,
},
{
cssCol: "1fr",
cell: html`<btrix-table-header-cell>
${msg("Pages Crawled")}
</btrix-table-header-cell>`,
},
{
cssCol: "1fr",
cell: html`<btrix-table-header-cell>
${msg("QA Analysis Runs")}
</btrix-table-header-cell>`,
},
{
cssCol: "1fr",
cell: html`<btrix-table-header-cell>
${msg("QA Rating")}
</btrix-table-header-cell>`,
},
];
if (this.hasCheckboxCell) {
headerCols.unshift({
cssCol: "min-content",
cell: nothing, // renders into slot
});
}
if (this.hasActionCell) {
headerCols.push({
cssCol: "[clickable-end] min-content",
cell: nothing, // renders into slot
});
}
return html`
<style>
btrix-table {
grid-template-columns: ${headerCols
.map(({ cssCol }) => cssCol)
.join(" ")};
}
</style>
<div class="overflow-auto">
<btrix-table>
<btrix-table-head class="mb-2">
<slot
name="checkboxCell"
@slotchange=${(e: Event) =>
(this.hasCheckboxCell =
(e.target as HTMLSlotElement).assignedElements().length > 0)}
></slot>
${headerCols.map(({ cell }) => cell)}
<slot
name="actionCell"
@slotchange=${(e: Event) =>
(this.hasActionCell =
(e.target as HTMLSlotElement).assignedElements().length > 0)}
></slot>
</btrix-table-head>
<btrix-table-body class="rounded border">
<slot></slot>
</btrix-table-body>
</btrix-table>
</div>
`;
}
}