/**
* Display list of workflows
*
* Usage example:
* ```ts
*
*
*
*
*
*
* ```
*/
import { localized, msg, str } from "@lit/localize";
import { css, html, LitElement, type TemplateResult } from "lit";
import {
customElement,
property,
query,
queryAssignedElements,
} from "lit/decorators.js";
import { BtrixElement } from "@/classes/BtrixElement";
import type { OverflowDropdown } from "@/components/ui/overflow-dropdown";
import { WorkflowTab } from "@/routes";
import { noData } from "@/strings/ui";
import type { ListWorkflow } from "@/types/crawler";
import { humanizeSchedule } from "@/utils/cron";
import { srOnly, truncate } from "@/utils/css";
import { pluralOf } from "@/utils/pluralize";
// postcss-lit-disable-next-line
const mediumBreakpointCss = css`30rem`;
// postcss-lit-disable-next-line
const largeBreakpointCss = css`60rem`;
// postcss-lit-disable-next-line
const rowCss = css`
.row {
display: grid;
grid-template-columns: 1fr;
}
@media only screen and (min-width: ${mediumBreakpointCss}) {
.row {
grid-template-columns: repeat(2, 1fr);
}
}
@media only screen and (min-width: ${largeBreakpointCss}) {
.row {
grid-template-columns: 1fr 17rem 10rem 11rem 3rem;
}
}
.col {
grid-column: span 1 / span 1;
}
`;
const columnCss = css`
.col:not(.action) {
padding-left: var(--sl-spacing-small);
padding-right: var(--sl-spacing-small);
}
.col:first-child {
padding-left: var(--sl-spacing-medium);
}
`;
// Shared custom variables
const hostVars = css`
:host {
--row-offset: var(--sl-spacing-x-small);
}
`;
const shortDate = (date: string) => html`
`;
const longDate = (date: string) => html`
`;
const notSpecified = html`${noData}`;
@customElement("btrix-workflow-list-item")
@localized()
export class WorkflowListItem extends BtrixElement {
static styles = [
truncate,
rowCss,
columnCss,
hostVars,
css`
a {
all: unset;
}
.item {
cursor: pointer;
transition-property: background-color, box-shadow, margin;
transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
transition-duration: 150ms;
overflow: hidden;
}
.item:hover,
.item:focus,
.item:focus-within {
background-color: var(--sl-color-neutral-50);
}
.item:hover {
background-color: var(--sl-color-neutral-50);
margin-left: calc(-1 * var(--row-offset));
margin-right: calc(-1 * var(--row-offset));
}
.item:hover .col:nth-child(n + 2) {
margin-left: calc(-1 * var(--row-offset));
}
.item:hover .col.action {
margin-left: calc(-2 * var(--row-offset));
}
.row {
border: 1px solid var(--sl-panel-border-color);
border-radius: var(--sl-border-radius-medium);
box-shadow: var(--sl-shadow-x-small);
}
.row:hover {
box-shadow: var(--sl-shadow-small);
}
.col {
padding-top: var(--sl-spacing-small);
padding-bottom: var(--sl-spacing-small);
transition-property: margin;
transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
transition-duration: 150ms;
overflow: hidden;
}
.detail {
color: var(--sl-color-neutral-700);
font-size: var(--sl-font-size-medium);
text-overflow: ellipsis;
height: 1.5rem;
}
.desc {
color: var(--sl-color-neutral-500);
font-size: var(--sl-font-size-x-small);
font-family: var(--font-monostyle-family);
font-variation-settings: var(--font-monostyle-variation);
height: 1rem;
}
.notSpecified {
color: var(--sl-color-neutral-400);
}
.url {
display: flex;
}
.url .primaryUrl {
flex: 0 1 auto;
}
.url .additionalUrls {
flex: none;
margin-left: var(--sl-spacing-2x-small);
}
.primaryUrl {
word-break: break-all;
}
.additionalUrls {
color: var(--sl-color-neutral-500);
}
.currCrawlSize {
color: var(--success);
}
.duration {
margin-left: calc(1rem + var(--sl-spacing-x-small));
}
.userName {
font-family: var(--font-monostyle-family);
font-variation-settings: var(--font-monostyle-variation);
}
.action {
display: flex;
align-items: center;
justify-content: center;
}
@media only screen and (min-width: ${largeBreakpointCss}) {
.action {
border-left: 1px solid var(--sl-panel-border-color);
}
}
`,
];
@property({ type: Object })
workflow?: ListWorkflow;
@query(".row")
row!: HTMLElement;
@query("btrix-overflow-dropdown")
dropdownMenu!: OverflowDropdown;
render() {
return html`
{
if (e.target === this.dropdownMenu) {
return;
}
e.preventDefault();
await this.updateComplete;
const href = `/orgs/${this.orgSlugState}/workflows/${this.workflow?.id}/${this.workflow?.lastCrawlState === "failed" ? WorkflowTab.Logs : WorkflowTab.LatestCrawl}`;
this.navigate.to(href);
}}
>
${this.safeRender(this.renderName)}
${this.safeRender((workflow) => {
if (workflow.schedule) {
return humanizeSchedule(workflow.schedule, {
length: "short",
});
}
if (workflow.lastStartedByName) {
return msg(str`Manual run by ${workflow.lastStartedByName}`);
}
return msg("---");
})}
${this.safeRender(this.renderLatestCrawl)}
${this.safeRender((workflow) => {
if (
workflow.isCrawlRunning &&
workflow.totalSize &&
workflow.lastCrawlSize
) {
return html`${this.localize.bytes(+workflow.totalSize, {
unitDisplay: "narrow",
})}
+
${this.localize.bytes(workflow.lastCrawlSize, {
unitDisplay: "narrow",
})}
`;
}
if (workflow.totalSize && workflow.lastCrawlSize) {
return this.localize.bytes(+workflow.totalSize, {
unitDisplay: "narrow",
});
}
if (workflow.isCrawlRunning && workflow.lastCrawlSize) {
return html`
${this.localize.bytes(workflow.lastCrawlSize, {
unitDisplay: "narrow",
})}
`;
}
if (workflow.totalSize) {
return this.localize.bytes(+workflow.totalSize, {
unitDisplay: "narrow",
});
}
return notSpecified;
})}
${this.safeRender(
(workflow) =>
`${this.localize.number(workflow.crawlCount, { notation: "compact" })} ${pluralOf("crawls", workflow.crawlCount)}`,
)}
${this.safeRender(this.renderModifiedBy)}
{
// Prevent navigation to detail view
e.preventDefault();
e.stopPropagation();
}}
>
`;
}
private readonly renderLatestCrawl = (workflow: ListWorkflow) => {
let tooltipContent: TemplateResult | null = null;
const status = html`
`;
const renderDuration = () => {
const compactIn = (dur: number) => {
const compactDuration = this.localize.humanizeDuration(dur, {
compact: true,
});
return `${msg("in")} ${compactDuration}`;
};
const verboseIn = (dur: number) => {
const verboseDuration = this.localize.humanizeDuration(dur, {
verbose: true,
unitCount: 2,
});
return `${msg("in")} ${verboseDuration}`;
};
const compactFor = (dur: number) => {
const compactDuration = this.localize.humanizeDuration(dur, {
compact: true,
});
return `${msg("for")} ${compactDuration}`;
};
const verboseFor = (dur: number) => {
const verboseDuration = this.localize.humanizeDuration(dur, {
verbose: true,
unitCount: 2,
});
return msg(str`for ${verboseDuration}`, {
desc: "`verboseDuration` example: '2 hours, 15 seconds'",
});
};
if (workflow.lastCrawlTime && workflow.lastCrawlStartTime) {
const diff =
new Date(workflow.lastCrawlTime).valueOf() -
new Date(workflow.lastCrawlStartTime).valueOf();
tooltipContent = html`
${msg("Finished")} ${longDate(workflow.lastCrawlTime)}
${verboseIn(diff)}
`;
return html`${shortDate(workflow.lastCrawlTime)} ${compactIn(diff)}`;
}
if (workflow.lastCrawlStartTime) {
const latestDate =
workflow.lastCrawlShouldPause && workflow.lastCrawlPausedAt
? new Date(workflow.lastCrawlPausedAt)
: new Date();
const diff =
latestDate.valueOf() -
new Date(workflow.lastCrawlStartTime).valueOf();
if (diff < 1000) {
return "";
}
if (
workflow.lastCrawlState === "paused" &&
workflow.lastCrawlPausedAt
) {
const pausedDiff =
new Date().valueOf() -
new Date(workflow.lastCrawlPausedAt).valueOf();
tooltipContent = html`
${msg("Crawl paused on")} ${longDate(workflow.lastCrawlPausedAt)}
`;
return html`
${shortDate(workflow.lastCrawlPausedAt)} ${compactFor(pausedDiff)}
`;
}
tooltipContent = html`
${msg("Running")} ${verboseFor(diff)} ${msg("since")}
${longDate(workflow.lastCrawlStartTime)}
`;
return html`${msg("Running")} ${compactFor(diff)}`;
}
return notSpecified;
};
const duration = renderDuration();
return html`
${tooltipContent}
`;
};
private readonly renderModifiedBy = (workflow: ListWorkflow) => {
const date = longDate(workflow.modified);
return html`
${workflow.modifiedByName}
${shortDate(workflow.modified)}
${workflow.modified === workflow.created
? msg("Created by")
: msg("Edited by")}
${workflow.modifiedByName}
${msg(html`on ${date}`, {
desc: "`date` example: 'January 1st, 2025 at 05:00 PM EST'",
})}
`;
};
private safeRender(
render: (workflow: ListWorkflow) => string | TemplateResult<1>,
) {
if (!this.workflow) {
return html``;
}
return render(this.workflow);
}
// TODO consolidate collections/workflow name
private readonly renderName = (workflow: ListWorkflow) => {
if (workflow.name)
return html`${workflow.name}`;
if (!workflow.firstSeed)
return html`${workflow.id}`;
const remainder = workflow.seedCount - 1;
let nameSuffix: string | TemplateResult<1> = "";
if (remainder) {
nameSuffix = html`+${this.localize.number(remainder, { notation: "compact" })}
${pluralOf("URLs", remainder)}`;
}
return html`
${workflow.firstSeed}${nameSuffix}
`;
};
}
@customElement("btrix-workflow-list")
@localized()
export class WorkflowList extends LitElement {
static styles = [
srOnly,
rowCss,
columnCss,
hostVars,
css`
.listHeader,
.list {
margin-left: var(--row-offset);
margin-right: var(--row-offset);
}
.listHeader {
line-height: 1;
}
.row {
display: none;
font-size: var(--sl-font-size-x-small);
color: var(--sl-color-neutral-600);
}
.col {
padding-top: var(--sl-spacing-x-small);
padding-bottom: var(--sl-spacing-x-small);
}
@media only screen and (min-width: ${largeBreakpointCss}) {
.row {
display: grid;
}
}
::slotted(btrix-workflow-list-item) {
display: block;
}
::slotted(btrix-workflow-list-item:not(:last-of-type)) {
margin-bottom: var(--sl-spacing-x-small);
}
`,
];
@queryAssignedElements({ selector: "btrix-workflow-list-item" })
listItems!: HTMLElement[];
render() {
return html`
`;
}
private handleSlotchange() {
this.listItems.map((el) => {
if (!el.attributes.getNamedItem("role")) {
el.setAttribute("role", "listitem");
}
});
}
}