diff --git a/backend/btrixcloud/colls.py b/backend/btrixcloud/colls.py
index 94b6dc03a3..79dbe14e5a 100644
--- a/backend/btrixcloud/colls.py
+++ b/backend/btrixcloud/colls.py
@@ -537,6 +537,7 @@ async def list_collections(
sort_direction: int = 1,
name: Optional[str] = None,
name_prefix: Optional[str] = None,
+ has_dedupe_index: Optional[bool] = None,
access: Optional[str] = None,
headers: Optional[dict] = None,
):
@@ -554,6 +555,9 @@ async def list_collections(
regex_pattern = f"^{name_prefix}"
match_query["name"] = {"$regex": regex_pattern, "$options": "i"}
+ if has_dedupe_index is not None:
+ match_query["hasDedupeIndex"] = has_dedupe_index
+
if public_colls_out:
match_query["access"] = CollAccessType.PUBLIC
elif access:
@@ -1121,6 +1125,7 @@ async def list_collection_all(
sortDirection: int = 1,
name: Optional[str] = None,
namePrefix: Optional[str] = None,
+ hasDedupeIndex: Optional[bool] = None,
access: Optional[str] = None,
):
# pylint: disable=duplicate-code
@@ -1132,6 +1137,7 @@ async def list_collection_all(
sort_direction=sortDirection,
name=name,
name_prefix=namePrefix,
+ has_dedupe_index=hasDedupeIndex,
access=access,
headers=dict(request.headers),
)
diff --git a/frontend/src/assets/icons/database-dedupe-fill.svg b/frontend/src/assets/icons/database-dedupe-fill.svg
new file mode 100644
index 0000000000..9fb5f4ce9a
--- /dev/null
+++ b/frontend/src/assets/icons/database-dedupe-fill.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/frontend/src/assets/icons/database-dedupe.svg b/frontend/src/assets/icons/database-dedupe.svg
new file mode 100644
index 0000000000..929d56559c
--- /dev/null
+++ b/frontend/src/assets/icons/database-dedupe.svg
@@ -0,0 +1,4 @@
+
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..408f744403
--- /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/data-grid/data-grid.ts b/frontend/src/components/ui/data-grid/data-grid.ts
index 636232aa66..767a5f1107 100644
--- a/frontend/src/components/ui/data-grid/data-grid.ts
+++ b/frontend/src/components/ui/data-grid/data-grid.ts
@@ -149,9 +149,14 @@ export class DataGrid<
return html`
-
+ ${this.formControlLabel
+ ? html``
+ : nothing}
= {
+ page: parsePage(new URLSearchParams(location.search).get("page")),
+ pageSize: INITIAL_PAGE_SIZE,
+ };
+
+ protected willUpdate(changedProperties: PropertyValues): void {
+ // Reset pagination when tab is hidden
+ if (changedProperties.has("visible") && !this.visible) {
+ this.pagination = {
+ ...this.pagination,
+ page: 1,
+ };
+ }
+ }
+
+ private readonly sources = new Task(this, {
+ task: async ([pagination], { signal }) => {
+ return this.getCollections({ ...pagination }, signal);
+ },
+ args: () => [this.pagination] as const,
+ });
+
+ render() {
+ return html` ${this.sources.render({
+ initial: loadingPanel,
+ pending: () =>
+ this.sources.value
+ ? this.renderTable(this.sources.value)
+ : loadingPanel(),
+ complete: this.renderTable,
+ })}`;
+ }
+
+ private readonly renderTable = (
+ collections: APIPaginatedList
,
+ ) => {
+ return html`
+
+
+ ${msg("Source Type")}
+
+ ${msg("Name")}
+
+ ${msg("Archived Items")}
+
+
+ ${msg("Index Entries")}
+
+
+ ${msg("Index Size")}
+
+
+ ${msg("Purgeable Entries")}
+
+
+ ${msg("Actions")}
+
+
+
+ ${collections.items.map(
+ (item) => html`
+
+
+ ${msg("Collection")}
+
+ ${item.name}
+ ${this.localize.number(item.crawlCount)}
+ ${noData}
+ ${noData}
+ ${noData}
+
+
+
+
+
+
+
+
+
+
+
+
+
+ `,
+ )}
+
+
+ ${when(
+ collections.total > collections.pageSize,
+ () => html`
+
+ `,
+ )} `;
+ };
+
+ private async getCollections(
+ params: APIPaginationQuery,
+ signal: AbortSignal,
+ ) {
+ const query = queryString.stringify({
+ pageSize: 2,
+ ...params,
+ hasDedupeIndex: true,
+ });
+ return this.api.fetch>(
+ `/orgs/${this.orgId}/collections?${query}`,
+ {
+ signal,
+ },
+ );
+ }
+}
diff --git a/frontend/src/pages/org/settings/settings.ts b/frontend/src/pages/org/settings/settings.ts
index 896f06f331..3f353dc630 100644
--- a/frontend/src/pages/org/settings/settings.ts
+++ b/frontend/src/pages/org/settings/settings.ts
@@ -26,10 +26,16 @@ import { tw } from "@/utils/tailwind";
import "./components/general";
import "./components/billing";
import "./components/crawling-defaults";
+import "./components/deduplication";
const styles = unsafeCSS(stylesheet);
-type Tab = "information" | "members" | "billing" | "crawling-defaults";
+type Tab =
+ | "information"
+ | "members"
+ | "billing"
+ | "crawling-defaults"
+ | "deduplication";
type User = {
email: string;
role: AccessCode;
@@ -92,6 +98,7 @@ export class OrgSettings extends BtrixElement {
members: msg("Members"),
billing: msg("Billing"),
"crawling-defaults": msg("Crawling Defaults"),
+ deduplication: msg("Deduplication"),
};
}
@@ -158,6 +165,7 @@ export class OrgSettings extends BtrixElement {
this.renderTab("billing", "settings/billing"),
)}
${this.renderTab("crawling-defaults", "settings/crawling-defaults")}
+ ${this.renderTab("deduplication", "settings/deduplication")}
${this.renderPanelHeader({ title: msg("General") })}
@@ -194,7 +202,7 @@ export class OrgSettings extends BtrixElement {
${this.renderPanelHeader({
- title: msg("Crawling Defaults"),
+ title: this.tabLabels["crawling-defaults"],
actions: html`
+
+ ${this.renderPanelHeader({ title: msg("Deduplication Sources") })}
+
+
`;
}
@@ -247,6 +259,11 @@ export class OrgSettings extends BtrixElement {
"crawling-defaults",
() => html``,
],
+ [
+ "deduplication",
+ () =>
+ html``,
+ ],
])}
${this.tabLabels[name]}
diff --git a/frontend/src/pages/org/settings/templates/loading-panel.ts b/frontend/src/pages/org/settings/templates/loading-panel.ts
new file mode 100644
index 0000000000..2ba16032ba
--- /dev/null
+++ b/frontend/src/pages/org/settings/templates/loading-panel.ts
@@ -0,0 +1,7 @@
+import { html } from "lit";
+
+export const loadingPanel = () => {
+ return html`
+
+
`;
+};
diff --git a/frontend/src/theme.stylesheet.css b/frontend/src/theme.stylesheet.css
index 2ad070739a..6efffed63b 100644
--- a/frontend/src/theme.stylesheet.css
+++ b/frontend/src/theme.stylesheet.css
@@ -241,6 +241,10 @@
@apply part-[base]:text-success part-[base]:hover:bg-success-50 part-[base]:hover:text-success-700 part-[base]:focus-visible:bg-success-50;
}
+ .menu-item-warning {
+ @apply part-[base]:text-warning part-[base]:hover:bg-warning-50 part-[base]:hover:text-warning-700 part-[base]:focus-visible:bg-warning-50;
+ }
+
.menu-item-danger {
@apply part-[base]:text-danger part-[base]:hover:bg-danger-50 part-[base]:hover:text-danger-700 part-[base]:focus-visible:bg-danger-50;
}