diff --git a/frontend/src/assets/icons/file-earmark-scan.svg b/frontend/src/assets/icons/file-earmark-scan.svg new file mode 100644 index 0000000000..ca3dc8cc7e --- /dev/null +++ b/frontend/src/assets/icons/file-earmark-scan.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/frontend/src/assets/icons/file-earmark-scan2.svg b/frontend/src/assets/icons/file-earmark-scan2.svg new file mode 100644 index 0000000000..4fa3f2d470 --- /dev/null +++ b/frontend/src/assets/icons/file-earmark-scan2.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/frontend/src/assets/icons/file-earmark-scan3.svg b/frontend/src/assets/icons/file-earmark-scan3.svg new file mode 100644 index 0000000000..a2988adca8 --- /dev/null +++ b/frontend/src/assets/icons/file-earmark-scan3.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/frontend/src/components/ui/badge.ts b/frontend/src/components/ui/badge.ts index 992601f5de..80060f82cb 100644 --- a/frontend/src/components/ui/badge.ts +++ b/frontend/src/components/ui/badge.ts @@ -11,6 +11,7 @@ export type BadgeVariant = | "danger" | "neutral" | "primary" + | "lime" | "cyan" | "blue" | "violet" @@ -73,6 +74,7 @@ export class Badge extends TailwindElement { neutral: tw`bg-neutral-100 text-neutral-600 ring-neutral-300`, "high-contrast": tw`bg-neutral-0 text-neutral-700 ring-neutral-600`, primary: tw`bg-white text-primary ring-primary`, + lime: tw`bg-lime-50 text-lime-600 ring-lime-600`, cyan: tw`bg-cyan-50 text-cyan-600 ring-cyan-600`, blue: tw`bg-blue-50 text-blue-600 ring-blue-600`, text: tw`text-blue-500 ring-blue-600`, @@ -82,12 +84,13 @@ export class Badge extends TailwindElement { }[this.variant], ] : { - success: tw`bg-success-500 text-neutral-0`, + success: tw`bg-success-600 text-neutral-0`, warning: tw`bg-warning-600 text-neutral-0`, danger: tw`bg-danger-500 text-neutral-0`, neutral: tw`bg-neutral-100 text-neutral-600`, "high-contrast": tw`bg-neutral-600 text-neutral-0`, primary: tw`bg-primary text-neutral-0`, + lime: tw`bg-lime-50 text-lime-600`, cyan: tw`bg-cyan-50 text-cyan-600`, blue: tw`bg-blue-50 text-blue-600`, violet: tw`bg-violet-50 text-violet-600`, diff --git a/frontend/src/components/ui/link.ts b/frontend/src/components/ui/link.ts index 73f1c46c45..6ad1869dff 100644 --- a/frontend/src/components/ui/link.ts +++ b/frontend/src/components/ui/link.ts @@ -5,6 +5,9 @@ import { ifDefined } from "lit/directives/if-defined.js"; import { BtrixElement } from "@/classes/BtrixElement"; +/** + * @cssPart base + */ @customElement("btrix-link") export class Link extends BtrixElement { @property({ type: String }) @@ -17,7 +20,7 @@ export class Link extends BtrixElement { rel?: HTMLAnchorElement["rel"]; @property({ type: String }) - variant: "primary" | "neutral" = "neutral"; + variant: "primary" | "warning" | "neutral" = "neutral"; @property({ type: Boolean }) hideIcon = false; @@ -31,6 +34,7 @@ export class Link extends BtrixElement { "group inline-flex items-center gap-1 transition-colors duration-fast", { primary: "text-primary-500 hover:text-primary-600", + warning: "text-warning-700 hover:text-warning-800", neutral: "text-blue-500 hover:text-blue-600", }[this.variant], )} @@ -40,6 +44,7 @@ export class Link extends BtrixElement { @click=${this.target === "_blank" || this.href.startsWith("http") ? () => {} : this.navigate.link} + part="base" > ${this.hideIcon diff --git a/frontend/src/features/archived-items/archived-item-list/archived-item-list-item.ts b/frontend/src/features/archived-items/archived-item-list/archived-item-list-item.ts index b8140d993e..1f4c653b1c 100644 --- a/frontend/src/features/archived-items/archived-item-list/archived-item-list-item.ts +++ b/frontend/src/features/archived-items/archived-item-list/archived-item-list-item.ts @@ -1,5 +1,6 @@ import { localized, msg, str } from "@lit/localize"; import type { SlCheckbox, SlHideEvent } from "@shoelace-style/shoelace"; +import clsx from "clsx"; import { css, html, nothing } from "lit"; import { customElement, property, query } from "lit/decorators.js"; import { ifDefined } from "lit/directives/if-defined.js"; @@ -9,8 +10,9 @@ import type { ArchivedItemCheckedEvent } from "./types"; import { BtrixElement } from "@/classes/BtrixElement"; import { CrawlStatus } from "@/features/archived-items/crawl-status"; import { ReviewStatus, type ArchivedItem, type Crawl } from "@/types/crawler"; -import { renderName } from "@/utils/crawler"; +import { isCrawl, renderName } from "@/utils/crawler"; import localize from "@/utils/localize"; +import { tw } from "@/utils/tailwind"; /** * @slot actionCell - Action cell @@ -82,14 +84,14 @@ export class ArchivedItemListItem extends BtrixElement { content=${msg("Not applicable")} > `; const none = html` @@ -103,6 +105,9 @@ export class ArchivedItemListItem extends BtrixElement { const qaStatus = CrawlStatus.getContent({ state: lastQAState || undefined, }); + const dedupeDependent = + isCrawl(this.item) && + (this.item.requiredByCrawls.length || this.item.requiresCrawls.length); return html` `} - ${activeQAStats - ? html` - - ` - : html` - - `} + ${isUpload ? notApplicable - : lastQAStarted && qaRunCount - ? html` - -
- ${this.localize.number(qaRunCount, { - notation: "compact", - })} -
-
- ` - : none} + : activeQAStats + ? html` + + ` + : lastQAStarted && qaRunCount + ? html` + +
+ ${this.localize.number(qaRunCount, { + notation: "compact", + })} +
+
+ ` + : none}
${isUpload diff --git a/frontend/src/features/archived-items/templates/dedupe-files-notice.ts b/frontend/src/features/archived-items/templates/dedupe-files-notice.ts new file mode 100644 index 0000000000..979d815b2d --- /dev/null +++ b/frontend/src/features/archived-items/templates/dedupe-files-notice.ts @@ -0,0 +1,41 @@ +import { msg } from "@lit/localize"; +import { html } from "lit"; +import { when } from "lit/directives/when.js"; + +export function dedupeFilesNotice({ href }: { href?: string } = {}) { + return html` +
+ + + + ${msg("This crawl is dependent on other crawls.")} + + +
+
+

+ ${msg( + "Files may contain incomplete or missing content due to deduplication.", + )} +

+ ${when( + href, + (href) => + html`

+ ${msg( + "Download the complete and deduplicated files in the collection.", + )} +

+ ${msg("Go to Collection")}`, + )} +
+
`; +} diff --git a/frontend/src/features/archived-items/templates/dedupe-qa-notice.ts b/frontend/src/features/archived-items/templates/dedupe-qa-notice.ts new file mode 100644 index 0000000000..7511387fb1 --- /dev/null +++ b/frontend/src/features/archived-items/templates/dedupe-qa-notice.ts @@ -0,0 +1,25 @@ +import { msg } from "@lit/localize"; +import { html } from "lit"; + +export function dedupeQANotice() { + return html` +
+ + + + ${msg("This crawl is dependent on other crawls.")} + + +
+
+

+ ${msg( + "Quality assurance tools are not currently supported for deduplication dependent crawls.", + )} +

+
+
`; +} diff --git a/frontend/src/features/archived-items/templates/dedupe-replay-notice.ts b/frontend/src/features/archived-items/templates/dedupe-replay-notice.ts new file mode 100644 index 0000000000..a8bdb8a4ea --- /dev/null +++ b/frontend/src/features/archived-items/templates/dedupe-replay-notice.ts @@ -0,0 +1,41 @@ +import { msg } from "@lit/localize"; +import { html } from "lit"; +import { when } from "lit/directives/when.js"; + +export function dedupeReplayNotice({ href }: { href?: string } = {}) { + return html` +
+ + + + ${msg("This crawl is dependent on other crawls.")} + + +
+
+

+ ${msg( + "Replay for this crawl may contain incomplete or missing pages due to its dependency of the deduplication source.", + )} +

+ ${when( + href, + (href) => + html`

+ ${msg( + "Replay the collection to view the complete and deduplicated crawl.", + )} +

+ ${msg("Go to Collection")}`, + )} +
+
`; +} diff --git a/frontend/src/features/browser-profiles/templates/badges.ts b/frontend/src/features/browser-profiles/templates/badges.ts index 5fdea74f6f..6721056655 100644 --- a/frontend/src/features/browser-profiles/templates/badges.ts +++ b/frontend/src/features/browser-profiles/templates/badges.ts @@ -10,7 +10,7 @@ export const usageBadge = (inUse: boolean) => ? msg("In Use by Crawl Workflow") : msg("Not In Use by Crawl Workflow")} > - + + + + ${text} + + `; + } +} diff --git a/frontend/src/features/collections/dedupe-source-badge.ts b/frontend/src/features/collections/dedupe-source-badge.ts new file mode 100644 index 0000000000..4bcf002c6e --- /dev/null +++ b/frontend/src/features/collections/dedupe-source-badge.ts @@ -0,0 +1,31 @@ +import { localized, msg } from "@lit/localize"; +import { css, html } from "lit"; +import { customElement } from "lit/decorators.js"; + +import { TailwindElement } from "@/classes/TailwindElement"; + +@customElement("btrix-dedupe-source-badge") +@localized() +export class DedupeSourceBadge extends TailwindElement { + static styles = css` + :host { + display: contents; + } + `; + + render() { + return html` + + + ${msg("Dedupe")} + + `; + } +} diff --git a/frontend/src/features/collections/index.ts b/frontend/src/features/collections/index.ts index 2fd3fbe5fe..30f4f15c73 100644 --- a/frontend/src/features/collections/index.ts +++ b/frontend/src/features/collections/index.ts @@ -7,6 +7,8 @@ import("./collection-edit-dialog"); import("./collection-create-dialog"); import("./collection-initial-view-dialog"); import("./collection-workflow-list"); +import("./dedupe-badge"); +import("./dedupe-source-badge"); import("./linked-collections"); import("./select-collection-access"); import("./select-collection-page"); diff --git a/frontend/src/features/collections/linked-collections/linked-collections-list-item.ts b/frontend/src/features/collections/linked-collections/linked-collections-list-item.ts index 6a2d039426..aaddc28181 100644 --- a/frontend/src/features/collections/linked-collections/linked-collections-list-item.ts +++ b/frontend/src/features/collections/linked-collections/linked-collections-list-item.ts @@ -45,21 +45,16 @@ export class LinkedCollectionsListItem extends TailwindElement { >
${item.name}
${dedupeEnabled - ? html` - ${msg("Dedupe Source")} - ` + ? html`` : nothing} `, ]; if (actual) { content.push( - html`
- ${item.crawlCount} - ${pluralOf("items", item.crawlCount)} -
`, + html`${item.crawlCount} ${pluralOf("items", item.crawlCount)}`, ); } diff --git a/frontend/src/pages/org/archived-item-detail/archived-item-detail.ts b/frontend/src/pages/org/archived-item-detail/archived-item-detail.ts index c2713a4a3d..d03054af44 100644 --- a/frontend/src/pages/org/archived-item-detail/archived-item-detail.ts +++ b/frontend/src/pages/org/archived-item-detail/archived-item-detail.ts @@ -11,7 +11,15 @@ import { BtrixElement } from "@/classes/BtrixElement"; import { type Dialog } from "@/components/ui/dialog"; import { ClipboardController } from "@/controllers/clipboard"; import type { CrawlMetadataEditor } from "@/features/archived-items/item-metadata-editor"; -import { pageBack, pageNav, type Breadcrumb } from "@/layouts/pageHeader"; +import { dedupeFilesNotice } from "@/features/archived-items/templates/dedupe-files-notice"; +import { dedupeQANotice } from "@/features/archived-items/templates/dedupe-qa-notice"; +import { dedupeReplayNotice } from "@/features/archived-items/templates/dedupe-replay-notice"; +import { + pageBack, + pageHeader, + pageNav, + type Breadcrumb, +} from "@/layouts/pageHeader"; import { OrgTab, WorkflowTab } from "@/routes"; import type { APIPaginatedList } from "@/types/api"; import type { @@ -306,6 +314,9 @@ export class ArchivedItemDetail extends BtrixElement { render() { const authToken = this.authState?.headers.Authorization.split(" ")[1]; const isSuccess = this.item && isSuccessfullyFinished(this.item); + const dedupeDependent = + this.item && isCrawl(this.item) && this.item.requiresCrawls.length; + let sectionContent: string | TemplateResult<1> = ""; switch (this.activeTab) { @@ -319,20 +330,22 @@ export class ArchivedItemDetail extends BtrixElement { html`${this.tabLabels.qa} `, )}
- ${when(this.qaRuns, this.renderQAHeader)} + ${when(!dedupeDependent && this.qaRuns, this.renderQAHeader)}
`, - html` - void this.fetchQARuns()} - > - `, + dedupeDependent + ? dedupeQANotice() + : html` + void this.fetchQARuns()} + > + `, ); break; } @@ -340,7 +353,6 @@ export class ArchivedItemDetail extends BtrixElement { sectionContent = this.renderPanel( this.tabLabels.replay, this.renderReplay(), - [tw`overflow-hidden rounded-lg border`], ); break; case "files": @@ -411,7 +423,9 @@ export class ArchivedItemDetail extends BtrixElement { break; default: sectionContent = html` -
+
${this.renderPanel(msg("Overview"), this.renderOverview(), [ tw`rounded-lg border p-4`, @@ -673,20 +687,52 @@ export class ArchivedItemDetail extends BtrixElement { } private renderHeader() { - return html` -
- -
- ${this.isCrawler - ? this.item - ? this.renderMenu() - : html`` - : nothing} -
-
- `; + const badgesSkeleton = () => + html``; + + const badges = (item: ArchivedItem) => { + return html`
+ ${isCrawl(item) + ? html` + + ${msg("Crawl")} + + + + + ${item.reviewStatus + ? msg("Reviewed") + : msg("No Review")} + ` + : html` + + ${msg("Uploaded")}`} +
`; + }; + return pageHeader({ + title: this.item ? renderName(this.item) : undefined, + secondary: when(this.item, badges, badgesSkeleton), + actions: this.isCrawler + ? this.item + ? this.renderMenu() + : html`` + : undefined, + }); } private renderMenu() { @@ -831,6 +877,16 @@ export class ArchivedItemDetail extends BtrixElement { } private renderReplay() { + const dedupeDependent = + this.item && isCrawl(this.item) && this.item.requiresCrawls.length; + + return html` + ${dedupeDependent ? dedupeReplayNotice() : nothing} +
${this.renderRWP()}
+ `; + } + + private renderRWP() { if (!this.item) return; const replaySource = `/api/orgs/${this.item.oid}/${ this.item.type === "upload" ? "uploads" : "crawls" @@ -1049,7 +1105,11 @@ export class ArchivedItemDetail extends BtrixElement { } private renderFiles() { + const dedupeDependent = + this.item && isCrawl(this.item) && this.item.requiresCrawls.length; + return html` + ${this.hasFiles && dedupeDependent ? dedupeFilesNotice() : nothing} ${this.hasFiles ? html`
    diff --git a/frontend/src/pages/org/workflow-detail.ts b/frontend/src/pages/org/workflow-detail.ts index aae3defed9..113cb8f906 100644 --- a/frontend/src/pages/org/workflow-detail.ts +++ b/frontend/src/pages/org/workflow-detail.ts @@ -20,6 +20,7 @@ import type { Alert } from "@/components/ui/alert"; import { parsePage, type PageChangeEvent } from "@/components/ui/pagination"; import { ClipboardController } from "@/controllers/clipboard"; import { CrawlStatus } from "@/features/archived-items/crawl-status"; +import { dedupeReplayNotice } from "@/features/archived-items/templates/dedupe-replay-notice"; import { ExclusionEditor } from "@/features/crawl-workflows/exclusion-editor"; import { ShareableNotice } from "@/features/crawl-workflows/templates/shareable-notice"; import { @@ -29,7 +30,7 @@ import { import type { BtrixChangeCrawlStateFilterEvent } from "@/features/crawls/crawl-state-filter"; import { pageError } from "@/layouts/pageError"; import { pageNav, type Breadcrumb } from "@/layouts/pageHeader"; -import { WorkflowTab } from "@/routes"; +import { CommonTab, OrgTab, WorkflowTab } from "@/routes"; import { deleteConfirmation, noData, notApplicable } from "@/strings/ui"; import type { APIPaginatedList, APIPaginationQuery } from "@/types/api"; import { type CrawlState } from "@/types/crawlState"; @@ -683,7 +684,12 @@ export class WorkflowDetail extends BtrixElement {
    -

    ${this.tabLabels[tab]}

    +
    +

    ${this.tabLabels[tab]}

    + ${tab === WorkflowTab.LatestCrawl + ? this.renderDedupeBadge() + : nothing} +
    ${this.renderPanelAction()}
    @@ -1387,6 +1393,17 @@ export class WorkflowDetail extends BtrixElement { `; }; + private readonly renderDedupeBadge = () => { + const latestCrawl = this.latestCrawlTask.value; + + if (!latestCrawl) return; + + return html``; + }; + private readonly renderPausedNotice = ( { truncate } = { truncate: false }, ) => { @@ -1715,6 +1732,16 @@ export class WorkflowDetail extends BtrixElement { } return html` + ${when(this.latestCrawlTask.value, (crawl) => + crawl.requiresCrawls.length + ? dedupeReplayNotice({ + href: this.workflow?.dedupeCollId + ? `${this.navigate.orgBasePath}/${OrgTab.Collections}/${CommonTab.View}/${this.workflow.dedupeCollId}` + : undefined, + }) + : nothing, + )} +
    ${guard([this.lastCrawlId], () => when(this.latestCrawlTask.value, this.renderReplay), diff --git a/frontend/src/stories/components/Badge.stories.ts b/frontend/src/stories/components/Badge.stories.ts index 3ba5efefbc..af9367e819 100644 --- a/frontend/src/stories/components/Badge.stories.ts +++ b/frontend/src/stories/components/Badge.stories.ts @@ -6,6 +6,8 @@ import { renderComponent, type RenderProps } from "./Badge"; import "@/features/crawls/crawler-channel-badge"; import "@/features/crawls/proxy-badge"; +import "@/features/collections/dedupe-badge"; +import "@/features/collections/dedupe-source-badge"; const meta = { title: "Components/Badge", @@ -32,6 +34,7 @@ const variants = [ "danger", "neutral", "primary", + "lime", "cyan", "blue", "violet", @@ -129,5 +132,10 @@ export const FeatureBadges: Story = { + + `, }; diff --git a/frontend/src/stories/components/DedupeBadge.stories.ts b/frontend/src/stories/components/DedupeBadge.stories.ts new file mode 100644 index 0000000000..67cc75f8e7 --- /dev/null +++ b/frontend/src/stories/components/DedupeBadge.stories.ts @@ -0,0 +1,35 @@ +import type { Meta, StoryObj } from "@storybook/web-components"; + +import { renderComponent, type RenderProps } from "./DedupeBadge"; + +const meta = { + title: "Features/Dedupe Badge", + component: "btrix-dedupe-badge", + tags: ["autodocs"], + decorators: [], + render: renderComponent, + argTypes: {}, + args: {}, +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +export const Dependents: Story = { + args: { + dependents: ["crawl1", "crawl2"], + }, +}; + +export const Dependencies: Story = { + args: { + dependencies: ["crawl1"], + }, +}; + +export const Both: Story = { + args: { + dependents: ["crawl1", "crawl2"], + dependencies: ["crawl1"], + }, +}; diff --git a/frontend/src/stories/components/DedupeBadge.ts b/frontend/src/stories/components/DedupeBadge.ts new file mode 100644 index 0000000000..9eee109f16 --- /dev/null +++ b/frontend/src/stories/components/DedupeBadge.ts @@ -0,0 +1,14 @@ +import { html } from "lit"; + +import type { DedupeBadge } from "@/features/collections/dedupe-badge"; + +import "@/features/collections/dedupe-badge"; + +export type RenderProps = DedupeBadge; + +export const renderComponent = (props: Partial) => { + return html``; +}; diff --git a/frontend/src/types/crawlState.ts b/frontend/src/types/crawlState.ts index f32e06c1ae..20bc18f0df 100644 --- a/frontend/src/types/crawlState.ts +++ b/frontend/src/types/crawlState.ts @@ -11,6 +11,7 @@ export const WAITING_NOT_PAUSED_STATES = [ "starting", "waiting_capacity", "waiting_org_limit", + "waiting_dedupe_index", ] as const; // Match backend TYPE_PAUSED_STATES in models.py diff --git a/frontend/src/types/crawler.ts b/frontend/src/types/crawler.ts index f24ade1241..f78901a14a 100644 --- a/frontend/src/types/crawler.ts +++ b/frontend/src/types/crawler.ts @@ -206,6 +206,8 @@ export type Crawl = ArchivedItemBase & browserWindows: number; shouldPause: boolean | null; resources?: (StorageFile & { numReplicas: number })[]; + requiresCrawls: string[]; + requiredByCrawls: string[]; }; export type Upload = ArchivedItemBase & {