forked from baron/baron-sso
- Implemented server-side search, infinite scrolling, and list virtualization for Tenants, Users, and Audit Logs. - Backend: Enhanced Repository, Service, and Handler layers to support 'search' and 'cursor' parameters. - Frontend: Integrated @tanstack/react-virtual and useInfiniteQuery for high-performance rendering. - Quality: Updated all unit tests and E2E tests to match the new asynchronous server-side search architecture. - i18n: Synced all translation keys and cleaned up unused resources.
132 lines
3.3 KiB
TypeScript
132 lines
3.3 KiB
TypeScript
import type { TenantSummary } from "../../../lib/adminApi";
|
|
import { buildTenantFullTree, type TenantNode } from "../../../lib/tenantTree";
|
|
|
|
export type TenantViewMode = "tree" | "table";
|
|
export type TenantViewRow = TenantNode & { depth: number };
|
|
|
|
export function tenantMatchesListSearch(
|
|
tenant: Pick<TenantSummary, "id" | "name" | "slug" | "type">,
|
|
search: string,
|
|
) {
|
|
const normalizedSearch = search.trim().toLowerCase();
|
|
if (!normalizedSearch) return true;
|
|
|
|
return [tenant.name, tenant.slug, tenant.id, tenant.type]
|
|
.filter(Boolean)
|
|
.some((value) => value.toLowerCase().includes(normalizedSearch));
|
|
}
|
|
|
|
function collectTenantTreeRows(
|
|
nodes: TenantNode[],
|
|
depth: number,
|
|
rows: TenantViewRow[],
|
|
) {
|
|
for (const node of nodes) {
|
|
rows.push({ ...node, depth });
|
|
collectTenantTreeRows(node.children, depth + 1, rows);
|
|
}
|
|
}
|
|
|
|
function collectTenantDescendantIds(
|
|
tenantId: string,
|
|
tenants: TenantSummary[],
|
|
) {
|
|
const childrenByParent = new Map<string, TenantSummary[]>();
|
|
for (const tenant of tenants) {
|
|
if (!tenant.parentId) continue;
|
|
const children = childrenByParent.get(tenant.parentId) ?? [];
|
|
children.push(tenant);
|
|
childrenByParent.set(tenant.parentId, children);
|
|
}
|
|
|
|
const ids: string[] = [];
|
|
const visitedIds = new Set<string>();
|
|
const visit = (parentId: string) => {
|
|
for (const child of childrenByParent.get(parentId) ?? []) {
|
|
if (visitedIds.has(child.id)) continue;
|
|
visitedIds.add(child.id);
|
|
ids.push(child.id);
|
|
visit(child.id);
|
|
}
|
|
};
|
|
visit(tenantId);
|
|
return ids;
|
|
}
|
|
|
|
export function filterTenantsByScope(
|
|
tenants: TenantSummary[],
|
|
scopeTenantId: string,
|
|
) {
|
|
if (!scopeTenantId) return tenants;
|
|
const descendantIds = new Set(
|
|
collectTenantDescendantIds(scopeTenantId, tenants),
|
|
);
|
|
return tenants.filter((tenant) => descendantIds.has(tenant.id));
|
|
}
|
|
|
|
export function getTenantViewRows(
|
|
tenants: TenantSummary[],
|
|
viewMode: TenantViewMode,
|
|
scopeTenantId = "",
|
|
isSearchActive = false,
|
|
): TenantViewRow[] {
|
|
const { subTree } = buildTenantFullTree(
|
|
tenants,
|
|
scopeTenantId || undefined,
|
|
isSearchActive,
|
|
);
|
|
const treeRows: TenantViewRow[] = [];
|
|
collectTenantTreeRows(subTree, 0, treeRows);
|
|
|
|
if (viewMode === "tree") {
|
|
return treeRows;
|
|
}
|
|
|
|
const rowsById = new Map(treeRows.map((row) => [row.id, row]));
|
|
const flatSource = scopeTenantId
|
|
? filterTenantsByScope(tenants, scopeTenantId)
|
|
: tenants;
|
|
|
|
return flatSource.map((tenant) => ({
|
|
...(rowsById.get(tenant.id) ?? {
|
|
...tenant,
|
|
children: [],
|
|
recursiveMemberCount: Number(tenant.memberCount) || 0,
|
|
}),
|
|
depth: 0,
|
|
}));
|
|
}
|
|
|
|
export function resolveTenantSelectionIds({
|
|
currentIds,
|
|
tenant,
|
|
checked,
|
|
tenants,
|
|
deletableTenants,
|
|
}: {
|
|
currentIds: string[];
|
|
tenant: TenantSummary;
|
|
checked: boolean;
|
|
tenants: TenantSummary[];
|
|
deletableTenants: TenantSummary[];
|
|
}) {
|
|
const allowedIds = new Set(deletableTenants.map((item) => item.id));
|
|
const targetIds = [
|
|
tenant.id,
|
|
...collectTenantDescendantIds(tenant.id, tenants),
|
|
].filter((id) => allowedIds.has(id));
|
|
const next = new Set(currentIds.filter((id) => allowedIds.has(id)));
|
|
|
|
if (checked) {
|
|
for (const id of targetIds) {
|
|
next.add(id);
|
|
}
|
|
} else {
|
|
for (const id of targetIds) {
|
|
next.delete(id);
|
|
}
|
|
}
|
|
|
|
return Array.from(next);
|
|
}
|