diff --git a/frontend/src/features/collections/collections-grid.ts b/frontend/src/features/collections/collections-grid.ts
index 501759db..9237e88a 100644
--- a/frontend/src/features/collections/collections-grid.ts
+++ b/frontend/src/features/collections/collections-grid.ts
@@ -1,11 +1,17 @@
import { localized, msg } from "@lit/localize";
import clsx from "clsx";
import { html, nothing } from "lit";
-import { customElement, property, state } from "lit/decorators.js";
+import {
+ customElement,
+ property,
+ queryAssignedNodes,
+ state,
+} from "lit/decorators.js";
import { ifDefined } from "lit/directives/if-defined.js";
import { when } from "lit/directives/when.js";
import { CollectionThumbnail } from "./collection-thumbnail";
+import { SelectCollectionAccess } from "./select-collection-access";
import { BtrixElement } from "@/classes/BtrixElement";
import { RouteNamespace } from "@/routes";
@@ -32,6 +38,12 @@ export class CollectionsGrid extends BtrixElement {
@property({ type: String })
collectionRefreshing: string | null = null;
+ @property({ type: Boolean })
+ showVisibility = false;
+
+ @queryAssignedNodes({ slot: "pagination" })
+ pagination!: Node[];
+
render() {
const gridClassNames = tw`grid flex-1 grid-cols-1 gap-10 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4`;
@@ -89,6 +101,16 @@ export class CollectionsGrid extends BtrixElement {
${this.renderDateBadge(collection)}
+ ${this.showVisibility
+ ? html`
`
+ : nothing}
@@ -118,6 +140,12 @@ export class CollectionsGrid extends BtrixElement {
`,
)}
+
+
+
${when(
showActions,
() =>
diff --git a/frontend/src/pages/org/collection-detail.ts b/frontend/src/pages/org/collection-detail.ts
index 2367f17c..ce2c125f 100644
--- a/frontend/src/pages/org/collection-detail.ts
+++ b/frontend/src/pages/org/collection-detail.ts
@@ -257,10 +257,7 @@ export class CollectionDetail extends BtrixElement {
${!this.collection ||
Boolean(this.collection.crawlCount && !this.isRwpLoaded)
? html``
- : html``}
+ : html``}
${msg("Set Initial View")}
@@ -522,7 +519,7 @@ export class CollectionDetail extends BtrixElement {
${!this.collection ||
Boolean(this.collection.crawlCount && !this.isRwpLoaded)
? html``
- : html``}
+ : html``}
${msg("Set Initial View")}
diff --git a/frontend/src/pages/org/dashboard.ts b/frontend/src/pages/org/dashboard.ts
index 65db52dd..eac5b9da 100644
--- a/frontend/src/pages/org/dashboard.ts
+++ b/frontend/src/pages/org/dashboard.ts
@@ -1,6 +1,10 @@
import { localized, msg } from "@lit/localize";
-import { Task } from "@lit/task";
-import type { SlSelectEvent } from "@shoelace-style/shoelace";
+import { Task, TaskStatus } from "@lit/task";
+import type {
+ SlChangeEvent,
+ SlRadioGroup,
+ SlSelectEvent,
+} from "@shoelace-style/shoelace";
import { html, nothing, type PropertyValues, type TemplateResult } from "lit";
import { customElement, property, state } from "lit/decorators.js";
import { ifDefined } from "lit/directives/if-defined.js";
@@ -10,6 +14,7 @@ import queryString from "query-string";
import type { SelectNewDialogEvent } from ".";
import { BtrixElement } from "@/classes/BtrixElement";
+import { type PageChangeEvent } from "@/components/ui/pagination";
import { type CollectionSavedEvent } from "@/features/collections/collection-edit-dialog";
import { pageHeading } from "@/layouts/page";
import { pageHeader } from "@/layouts/pageHeader";
@@ -19,6 +24,8 @@ import { CollectionAccess, type Collection } from "@/types/collection";
import { SortDirection } from "@/types/utils";
import { humanizeExecutionSeconds } from "@/utils/executionTimeFormatter";
import { tw } from "@/utils/tailwind";
+import { timeoutCache } from "@/utils/timeoutCache";
+import { cached } from "@/utils/weakCache";
type Metrics = {
storageUsedBytes: number;
@@ -40,6 +47,13 @@ type Metrics = {
publicCollectionsCount: number;
};
+enum CollectionGridView {
+ All = "all",
+ Public = "public",
+}
+
+const PAGE_SIZE = 16;
+
@customElement("btrix-dashboard")
@localized()
export class Dashboard extends BtrixElement {
@@ -52,6 +66,15 @@ export class Dashboard extends BtrixElement {
@state()
collectionRefreshing: string | null = null;
+ @state()
+ collectionsView = CollectionGridView.Public;
+
+ @state()
+ collectionPage = 1;
+
+ // Used for busting cache when updating visible collection
+ cacheBust = 0;
+
private readonly colors = {
default: "neutral",
crawls: "green",
@@ -60,15 +83,31 @@ export class Dashboard extends BtrixElement {
runningTime: "blue",
};
- private readonly publicCollections = new Task(this, {
- task: async ([orgId]) => {
- if (!orgId) throw new Error("orgId required");
+ private readonly collections = new Task(this, {
+ task: cached(
+ async ([orgId, collectionsView, collectionPage]) => {
+ if (!orgId) throw new Error("orgId required");
- const collections = await this.getPublicCollections({ orgId });
- this.collectionRefreshing = null;
- return collections;
- },
- args: () => [this.orgId] as const,
+ const collections = await this.getCollections({
+ orgId,
+ access:
+ collectionsView === CollectionGridView.Public
+ ? CollectionAccess.Public
+ : undefined,
+ page: collectionPage,
+ });
+ this.collectionRefreshing = null;
+ return collections;
+ },
+ { cacheConstructor: timeoutCache(300) },
+ ),
+ args: () =>
+ [
+ this.orgId,
+ this.collectionsView,
+ this.collectionPage,
+ this.cacheBust,
+ ] as const,
});
willUpdate(changedProperties: PropertyValues & Map) {
@@ -330,17 +369,28 @@ export class Dashboard extends BtrixElement {
- ${pageHeading({
- content: msg("Public Collections"),
- })}
+
+ ${when(
+ this.appState.isCrawler,
+ () => html`
`,
)}
- {
+ this.collectionPage = 1;
+ this.collectionsView = (e.target as SlRadioGroup)
+ .value as CollectionGridView;
+ }}
>
-
-
+
+
+
+
+
+
+
-
+
{
this.collectionRefreshing = e.detail.id;
- void this.publicCollections.run([this.orgId]);
+ void this.collections.run([
+ this.orgId,
+ this.collectionsView,
+ this.collectionPage,
+ ++this.cacheBust,
+ ]);
}}
>
${this.renderNoPublicCollections()}
+ ${this.collections.value &&
+ this.collections.value.total > this.collections.value.items.length
+ ? html`
+ {
+ this.collectionPage = e.detail.page;
+ }}
+ slot="pagination"
+ >
+
+ `
+ : nothing}
+ ${this.collections.status === TaskStatus.PENDING &&
+ this.collections.value
+ ? html`
+
+
`
+ : nothing}
@@ -864,13 +963,25 @@ export class Dashboard extends BtrixElement {
}
}
- private async getPublicCollections({ orgId }: { orgId: string }) {
+ private async getCollections({
+ orgId,
+ access,
+ page,
+ }: {
+ orgId: string;
+ access?: CollectionAccess;
+ page?: number;
+ }) {
const params: APISortQuery
& {
- access: CollectionAccess;
+ access?: CollectionAccess;
+ page?: number;
+ pageSize?: number;
} = {
sortBy: "dateLatest",
sortDirection: SortDirection.Descending,
- access: CollectionAccess.Public,
+ access,
+ page,
+ pageSize: PAGE_SIZE,
};
const query = queryString.stringify(params);
@@ -878,6 +989,6 @@ export class Dashboard extends BtrixElement {
`/orgs/${orgId}/collections?${query}`,
);
- return data.items;
+ return data;
}
}
diff --git a/frontend/src/utils/timeoutCache.ts b/frontend/src/utils/timeoutCache.ts
new file mode 100644
index 00000000..4a4b2d0c
--- /dev/null
+++ b/frontend/src/utils/timeoutCache.ts
@@ -0,0 +1,39 @@
+import { type Cache } from "./weakCache";
+
+export function timeoutCache(seconds: number) {
+ return class implements Cache {
+ private readonly cache: { [key: string]: V } = Object.create(null);
+ set(key: K | string, value: V) {
+ if (typeof key !== "string") {
+ key = JSON.stringify(key);
+ }
+ this.cache[key] = value;
+ setTimeout(() => {
+ try {
+ delete this.cache[key as string];
+ } catch (_) {
+ /* empty */
+ console.debug("missing key", key);
+ }
+ }, seconds * 1000);
+ return this;
+ }
+ get(key: K | string) {
+ if (typeof key !== "string") {
+ key = JSON.stringify(key);
+ }
+ try {
+ return this.cache[key];
+ } catch (_) {
+ console.debug("missing key", key);
+ /* empty */
+ }
+ }
+ has(key: K | string) {
+ if (typeof key !== "string") {
+ key = JSON.stringify(key);
+ }
+ return Object.prototype.hasOwnProperty.call(this.cache, key);
+ }
+ };
+}
diff --git a/frontend/src/utils/weakCache.ts b/frontend/src/utils/weakCache.ts
index e2a944b8..b2429527 100644
--- a/frontend/src/utils/weakCache.ts
+++ b/frontend/src/utils/weakCache.ts
@@ -1,3 +1,14 @@
+export interface Cache {
+ set: (key: K, value: V) => this;
+ get: (key: K) => V | undefined;
+ has: (key: K) => boolean;
+}
+
+export interface CacheConstructor {
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ new (...args: any[]): Cache;
+}
+
export const WeakRefMapInnerValue = Symbol("inner value");
type WeakRefValue = T extends object
@@ -28,16 +39,16 @@ const unwrapValue = (val: WeakRef> | undefined) => {
*
* Adapted from https://developer.mozilla.org/en-US/docs/Web/JavaScript/Memory_management#weakrefs_and_finalizationregistry and https://stackoverflow.com/a/72896692
*/
-export class WeakRefMap {
+export class WeakRefMap implements Cache {
readonly cacheMap = new Map>>();
private readonly finalizer = new FinalizationRegistry((key: K) => {
this.cacheMap.delete(key);
});
- set(key: K, value: V): V {
+ set(key: K, value: V): this {
const cache = this.get(key);
if (cache) {
- if (cache === value) return value;
+ if (cache === value) return this;
if (typeof cache === "object") {
this.finalizer.unregister(cache);
}
@@ -47,14 +58,14 @@ export class WeakRefMap {
this.cacheMap.set(key, ref);
this.finalizer.register(objVal, key, objVal);
- return isWrapped(objVal) ? objVal[WeakRefMapInnerValue] : (objVal as V);
+ return this;
}
- get(key: K): V | undefined {
+ get(key: K) {
return unwrapValue(this.cacheMap.get(key));
}
- has(key: K): boolean {
+ has(key: K) {
return this.cacheMap.has(key);
}
}
@@ -67,13 +78,20 @@ export function cached<
Serializer extends (args: Args) => unknown = (args: Args) => string,
>(
fn: (...args: Args) => Result,
- serializer: Serializer = JSON.stringify as Serializer,
+ options: {
+ cacheConstructor?: CacheConstructor;
+ serializer?: Serializer;
+ } = {},
) {
+ const {
+ serializer = JSON.stringify as Serializer,
+ cacheConstructor = WeakRefMap,
+ } = options;
type Key = ReturnType;
- const cache = new WeakRefMap();
+ const cache = new cacheConstructor();
const cachedFn: {
(...args: Args): Result;
- [InnerCache]: WeakRefMap;
+ [InnerCache]: Cache;
} = (...args: Args) => {
let k;
try {
@@ -83,7 +101,13 @@ export function cached<
"Unable to serialize function arguments successfully - ensure your serializer function is able to handle the args you're passing in",
);
}
- return cache.get(k) ?? cache.set(k, fn(...args));
+ if (cache.has(k)) {
+ return cache.get(k)!;
+ } else {
+ const v = fn(...args);
+ cache.set(k, v);
+ return v;
+ }
};
cachedFn[InnerCache] = cache;
return cachedFn;