From d08a41fb56053331cc6f6ffe2757b6f2e241f33a Mon Sep 17 00:00:00 2001 From: Albrecht Degering Date: Sat, 11 Jul 2026 02:34:55 +0200 Subject: [PATCH] Release v0.1.7 --- package.json | 4 +- pyproject.toml | 4 +- src/govoplan_access/backend/manifest.py | 2 +- webui/package.json | 4 +- webui/src/api/admin.ts | 125 ------------ webui/src/features/admin/AdminAuditPanel.tsx | 181 ------------------ webui/src/features/admin/AdminPage.tsx | 36 +--- .../features/admin/RetentionPoliciesPanel.tsx | 161 ---------------- 8 files changed, 10 insertions(+), 507 deletions(-) delete mode 100644 webui/src/features/admin/AdminAuditPanel.tsx delete mode 100644 webui/src/features/admin/RetentionPoliciesPanel.tsx diff --git a/package.json b/package.json index e6132f2..7c7f03b 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@govoplan/access-webui", - "version": "0.1.6", + "version": "0.1.7", "private": true, "type": "module", "main": "webui/src/index.ts", @@ -18,7 +18,7 @@ "LICENSE" ], "peerDependencies": { - "@govoplan/core-webui": "^0.1.6", + "@govoplan/core-webui": "^0.1.7", "lucide-react": "^1.23.0", "react": "^19.0.0", "react-dom": "^19.0.0", diff --git a/pyproject.toml b/pyproject.toml index 1b078ce..ee3cfcb 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,14 +4,14 @@ build-backend = "setuptools.build_meta" [project] name = "govoplan-access" -version = "0.1.6" +version = "0.1.7" description = "GovOPlaN access platform module with identity, auth, RBAC, and scope primitives." readme = "README.md" requires-python = ">=3.12" license = { file = "LICENSE" } authors = [{ name = "GovOPlaN" }] dependencies = [ - "govoplan-core>=0.1.6", + "govoplan-core>=0.1.7", "SQLAlchemy>=2,<3", ] diff --git a/src/govoplan_access/backend/manifest.py b/src/govoplan_access/backend/manifest.py index 5118760..aecaf20 100644 --- a/src/govoplan_access/backend/manifest.py +++ b/src/govoplan_access/backend/manifest.py @@ -600,7 +600,7 @@ def _route_factory(context: ModuleContext): manifest = ModuleManifest( id="access", name="Access", - version="0.1.6", + version="0.1.7", optional_dependencies=("identity", "organizations", "tenancy", "idm"), permissions=ACCESS_PERMISSIONS, role_templates=ACCESS_ROLE_TEMPLATES, diff --git a/webui/package.json b/webui/package.json index 819f16e..4ba70cf 100644 --- a/webui/package.json +++ b/webui/package.json @@ -1,6 +1,6 @@ { "name": "@govoplan/access-webui", - "version": "0.1.6", + "version": "0.1.7", "private": true, "type": "module", "main": "src/index.ts", @@ -13,7 +13,7 @@ } }, "peerDependencies": { - "@govoplan/core-webui": "^0.1.6", + "@govoplan/core-webui": "^0.1.7", "lucide-react": "^1.23.0", "react": "^19.0.0", "react-dom": "^19.0.0", diff --git a/webui/src/api/admin.ts b/webui/src/api/admin.ts index ade7ed3..87c847e 100644 --- a/webui/src/api/admin.ts +++ b/webui/src/api/admin.ts @@ -205,7 +205,6 @@ export type PrivacyRetentionPolicyFieldKey = | "audit_detail_level"; export type PrivacyRetentionLimitPermissions = Record; -export type PrivacyRetentionLimitPermissionPatch = Partial; export type PrivacyRetentionPolicy = { store_raw_campaign_json: boolean; @@ -218,29 +217,6 @@ export type PrivacyRetentionPolicy = { allow_lower_level_limits: PrivacyRetentionLimitPermissions; }; -export type PrivacyRetentionPolicyPatch = Partial> & { - allow_lower_level_limits?: PrivacyRetentionLimitPermissionPatch; -}; -export type PrivacyRetentionPolicyScope = "system" | "tenant" | "user" | "group" | "campaign"; - -export type PolicySourceStep = { - scope_type: string; - scope_id?: string | null; - label: string; - applied_fields?: string[]; - policy?: PrivacyRetentionPolicyPatch | PrivacyRetentionPolicy | null; -}; - -export type PrivacyRetentionPolicyScopeResponse = { - scope_type: PrivacyRetentionPolicyScope; - scope_id?: string | null; - policy: PrivacyRetentionPolicyPatch; - effective_policy: PrivacyRetentionPolicy; - parent_policy?: PrivacyRetentionPolicy | null; - effective_policy_sources?: PolicySourceStep[]; - parent_policy_sources?: PolicySourceStep[]; -}; - export type SystemSettingsItem = { default_locale: string; allow_tenant_custom_groups: boolean; @@ -276,16 +252,6 @@ export type TenantSettingsDeltaSections = Partial<{ settings: Pick["settings"]; }>; -export type RetentionRunResponse = { - result: { - dry_run: boolean; - policy: PrivacyRetentionPolicy; - cutoffs: Record; - effective_policy_scope?: string; - counts: Record>; - }; -}; - export type GovernanceAssignment = { tenant_id: string; mode: "available" | "required"; @@ -329,18 +295,6 @@ export type ExternalFunctionRoleMappingItem = { updated_at: string; }; -export type AuditAdminItem = { - id: string; - scope: "tenant" | "system"; - tenant_id?: string | null; - actor_email?: string | null; - action: string; - object_type?: string | null; - object_id?: string | null; - details: Record; - created_at: string; -}; - type DeltaResponseFields = { deleted: DeltaDeletedItem[]; watermark?: string | null; @@ -361,15 +315,6 @@ export type TenantSettingsDeltaResponse = { sections: TenantSettingsDeltaSections; changed_sections: string[]; } & DeltaResponseFields; -export type AuditAdminDeltaResponse = { - items: AuditAdminItem[]; - total: number; - page: number; - page_size: number; - pages: number; - cursor?: string | null; - next_cursor?: string | null; -} & DeltaResponseFields; function deltaSuffix(options: { since?: string | null; limit?: number } = {}): string { const params = new URLSearchParams(); @@ -671,58 +616,6 @@ export function revokeApiKey(settings: ApiSettings, keyId: string): Promise>; -}; - -export async function fetchAdminAudit(settings: ApiSettings, options: AuditQueryOptions = {}): Promise<{ items: AuditAdminItem[]; total: number; page: number; page_size: number; pages: number; cursor?: string | null; next_cursor?: string | null }> { - const params = new URLSearchParams(); - if (options.tenantId) params.set("tenant_id", options.tenantId); - if (options.allTenants) params.set("all_tenants", "true"); - if (options.scope) params.set("scope", options.scope); - if (options.limit) params.set("limit", String(options.limit)); - if (options.offset) params.set("offset", String(options.offset)); - if (options.page) params.set("page", String(options.page)); - if (options.pageSize) params.set("page_size", String(options.pageSize)); - if (options.cursor) params.set("cursor", options.cursor); - if (options.sortBy) params.set("sort_by", options.sortBy); - if (options.sortDirection) params.set("sort_direction", options.sortDirection); - for (const [column, value] of Object.entries(options.filters ?? {})) { - if (value?.trim()) params.set(`filter_${column}`, value); - } - const suffix = params.toString() ? `?${params.toString()}` : ""; - return apiFetch(settings, `/api/v1/admin/audit${suffix}`); -} - -export async function fetchAdminAuditDelta(settings: ApiSettings, options: AuditQueryOptions & { since?: string | null } = {}): Promise { - const params = new URLSearchParams(); - if (options.tenantId) params.set("tenant_id", options.tenantId); - if (options.allTenants) params.set("all_tenants", "true"); - if (options.scope) params.set("scope", options.scope); - if (options.limit) params.set("limit", String(options.limit)); - if (options.pageSize) params.set("page_size", String(options.pageSize)); - if (options.cursor) params.set("cursor", options.cursor); - if (options.sortBy) params.set("sort_by", options.sortBy); - if (options.sortDirection) params.set("sort_direction", options.sortDirection); - if (options.since) params.set("since", options.since); - for (const [column, value] of Object.entries(options.filters ?? {})) { - if (value?.trim()) params.set(`filter_${column}`, value); - } - const suffix = params.toString() ? `?${params.toString()}` : ""; - return apiFetch(settings, `/api/v1/admin/audit/delta${suffix}`); -} - - export function createSystemAccount(settings: ApiSettings, payload: { email: string; display_name?: string | null; @@ -759,24 +652,6 @@ export function updateSystemSettings(settings: ApiSettings, payload: SystemSetti return apiFetch(settings, "/api/v1/admin/system/settings", { method: "PATCH", body: JSON.stringify(payload) }); } -export function getPrivacyRetentionPolicy(settings: ApiSettings, scope: PrivacyRetentionPolicyScope, scopeId?: string | null): Promise { - const params = new URLSearchParams(); - if (scopeId) params.set("scope_id", scopeId); - const suffix = params.toString() ? `?${params.toString()}` : ""; - return apiFetch(settings, `/api/v1/admin/privacy-retention/policies/${encodeURIComponent(scope)}${suffix}`); -} - -export function updatePrivacyRetentionPolicy(settings: ApiSettings, scope: PrivacyRetentionPolicyScope, policy: PrivacyRetentionPolicyPatch, scopeId?: string | null): Promise { - const params = new URLSearchParams(); - if (scopeId) params.set("scope_id", scopeId); - const suffix = params.toString() ? `?${params.toString()}` : ""; - return apiFetch(settings, `/api/v1/admin/privacy-retention/policies/${encodeURIComponent(scope)}${suffix}`, { method: "PUT", body: JSON.stringify({ policy }) }); -} - -export function runRetentionPolicy(settings: ApiSettings, dryRun = true): Promise { - return apiFetch(settings, "/api/v1/admin/system/retention/run", { method: "POST", body: JSON.stringify({ dry_run: dryRun }) }); -} - export async function fetchGovernanceTemplates(settings: ApiSettings, kind?: "group" | "role"): Promise { const suffix = kind ? `?kind=${encodeURIComponent(kind)}` : ""; const response = await apiFetch<{ templates: GovernanceTemplateItem[] }>(settings, `/api/v1/admin/system/governance-templates${suffix}`); diff --git a/webui/src/features/admin/AdminAuditPanel.tsx b/webui/src/features/admin/AdminAuditPanel.tsx deleted file mode 100644 index c27c078..0000000 --- a/webui/src/features/admin/AdminAuditPanel.tsx +++ /dev/null @@ -1,181 +0,0 @@ -import { useCallback, useEffect, useMemo, useRef, useState } from "react"; -import { Search } from "lucide-react"; -import type { ApiSettings, AuthInfo } from "@govoplan/core-webui"; -import { fetchAdminAudit, fetchAdminAuditDelta, type AuditAdminItem } from "../../api/admin"; -import { Button } from "@govoplan/core-webui"; -import { DataGrid, type DataGridColumn, type DataGridQueryState } from "@govoplan/core-webui"; -import { Dialog } from "@govoplan/core-webui"; -import { AdminIconButton, AdminPageLayout, adminErrorMessage, formatAdminDateTime as formatDateTime, i18nMessage, mergeDeltaRows, useDeltaWatermarks } from "@govoplan/core-webui"; - -type AuditSortBy = "time" | "actor" | "action" | "object" | "tenant"; - -const DEFAULT_QUERY: DataGridQueryState = { - sort: { columnId: "time", direction: "desc" }, - filters: {} -}; - -export default function AdminAuditPanel({ - settings, - auth, - systemMode = false - - - - -}: {settings: ApiSettings;auth: AuthInfo;systemMode?: boolean;}) { - const [items, setItems] = useState([]); - const itemsRef = useRef([]); - const pageItemsRef = useRef>({}); - const pageCursorsRef = useRef>({ 1: null }); - const { getDeltaWatermark, setDeltaWatermark, resetDeltaWatermark } = useDeltaWatermarks(); - const [total, setTotal] = useState(0); - const [page, setPage] = useState(1); - const [pageSize, setPageSize] = useState(10); - const [query, setQuery] = useState(DEFAULT_QUERY); - const [selected, setSelected] = useState(null); - const [loading, setLoading] = useState(true); - const [error, setError] = useState(""); - const [reloadToken, setReloadToken] = useState(0); - const tenantId = (auth.active_tenant ?? auth.tenant).id; - - const load = useCallback(async () => { - setLoading(true); - setError(""); - try { - const sortColumn = query.sort?.columnId; - const sortBy = sortColumn && ["time", "actor", "action", "object", "tenant"].includes(sortColumn) ? - sortColumn as AuditSortBy : - "time"; - const sortDirection = query.sort?.direction ?? "desc"; - const filters = query.filters; - const pageCursor = page === 1 ? null : pageCursorsRef.current[page]; - const deltaMode = page === 1 || pageCursor !== undefined; - const requestOptions = { - scope: systemMode ? "system" : "tenant", - page, - pageSize, - cursor: pageCursor, - sortBy, - sortDirection, - filters - }; - const deltaKey = `access:audit:${systemMode ? "system" : "tenant"}:${tenantId}:${pageSize}:${page}:${pageCursor ?? "root"}:${JSON.stringify({ sortBy, sortDirection, filters })}`; - const response = deltaMode - ? await fetchAdminAuditDelta(settings, { ...requestOptions, since: getDeltaWatermark(deltaKey) }) - : await fetchAdminAudit(settings, requestOptions); - const baseItems = pageItemsRef.current[deltaKey] ?? []; - const nextItems = "full" in response && !response.full - ? mergeDeltaRows(baseItems, response.items, response.deleted, (item) => item.id, { sort: compareAuditEvents(sortBy, sortDirection) }).slice(0, pageSize) - : response.items; - pageItemsRef.current[deltaKey] = nextItems; - itemsRef.current = nextItems; - setItems(nextItems); - setTotal(response.total); - if (!deltaMode && response.page !== page) setPage(response.page); - if (response.cursor !== undefined) pageCursorsRef.current[page] = response.cursor ?? null; - if (response.next_cursor !== undefined) { - if (response.next_cursor) pageCursorsRef.current[page + 1] = response.next_cursor; - else delete pageCursorsRef.current[page + 1]; - } - if ("full" in response && !response.full && page === 1 && (response.items.length > 0 || response.deleted.length > 0)) { - pageCursorsRef.current = { 1: null }; - } - if ("watermark" in response) setDeltaWatermark(deltaKey, response.watermark); - if (!deltaMode) resetDeltaWatermark(deltaKey); - } catch (err) { - setError(adminErrorMessage(err)); - } finally { - setLoading(false); - } - }, [settings.accessToken, settings.apiBaseUrl, systemMode, tenantId, page, pageSize, query, reloadToken, getDeltaWatermark, setDeltaWatermark, resetDeltaWatermark]); - - useEffect(() => { - itemsRef.current = []; - pageItemsRef.current = {}; - pageCursorsRef.current = { 1: null }; - resetDeltaWatermark(); - }, [settings.accessToken, settings.apiBaseUrl, systemMode, tenantId, pageSize, query, resetDeltaWatermark]); - - useEffect(() => {void load();}, [load]); - - const handleQueryChange = useCallback((next: DataGridQueryState) => { - setQuery((current) => { - if (JSON.stringify(current) === JSON.stringify(next)) return current; - setPage(1); - return next; - }); - }, []); - - const columns = useMemo[]>(() => [ - { id: "time", header: "i18n:govoplan-access.time.6c82e6dd", width: 190, minWidth: 150, maxWidth: 260, resizable: true, sticky: "start", sortable: true, filterable: true, filterType: "date", value: (row) => row.created_at, render: (row) => formatDateTime(row.created_at) }, - { id: "actor", header: "i18n:govoplan-access.actor.cbd19b5c", width: 220, minWidth: 170, maxWidth: 360, resizable: true, sortable: true, filterable: true, value: (row) => row.actor_email || "i18n:govoplan-access.system.bc0792d8" }, - { id: "action", header: "i18n:govoplan-access.action.97c89a4d", width: 250, minWidth: 170, maxWidth: 420, resizable: true, sortable: true, filterable: true, value: (row) => row.action }, - { id: "object", header: "i18n:govoplan-access.object.2883f191", width: 300, minWidth: 180, maxWidth: 640, resizable: true, fill: true, sortable: true, filterable: true, value: (row) => `${row.object_type || "—"} ${row.object_id || ""}`.trim() }, - ...(systemMode ? [{ id: "tenant", header: "i18n:govoplan-access.tenant_context.b401a2ad", width: 190, minWidth: 150, maxWidth: 300, resizable: true, sortable: true, filterable: true, value: (row: AuditAdminItem) => row.tenant_id || "—" }] : []), - { id: "actions", header: "i18n:govoplan-access.actions.c3cd636a", width: 70, sticky: "end", resizable: false, align: "right", render: (row) =>
} onClick={() => setSelected(row)} />
}], - [systemMode]); - - const firstShown = total === 0 ? 0 : (page - 1) * pageSize + 1; - const lastShown = Math.min(total, page * pageSize); - - return ( - <> - setReloadToken((value) => value + 1)} disabled={loading}>i18n:govoplan-access.reload.cce71553}> - -
- row.id} - emptyText="i18n:govoplan-access.no_administrative_audit_records_found.8d128767" - className="admin-audit-grid" - initialSort={{ columnId: "time", direction: "desc" }} - pagination={{ - mode: "server", - page, - pageSize, - totalRows: total, - pageSizeOptions: [10, 25, 50, 100, 250], - disabled: loading, - onPageChange: setPage, - onPageSizeChange: (next) => {setPageSize(next);setPage(1);} - }} - onQueryChange={handleQueryChange} /> - -
-
- setSelected(null)} className="admin-dialog admin-dialog-wide" footer={}> - {selected && <>
i18n:govoplan-access.scope.4651a34e
{selected.scope}
i18n:govoplan-access.action.97c89a4d
{selected.action}
i18n:govoplan-access.actor.cbd19b5c
{selected.actor_email || "i18n:govoplan-access.system.bc0792d8"}
i18n:govoplan-access.object.2883f191
{selected.object_type || "—"} {selected.object_id || ""}
i18n:govoplan-access.tenant_context.b401a2ad
{selected.tenant_id || "—"}
i18n:govoplan-access.time.6c82e6dd
{formatDateTime(selected.created_at)}
{JSON.stringify(selected.details, null, 2)}
} -
- ); - -} - -function compareAuditEvents(sortBy: AuditSortBy, sortDirection: "asc" | "desc"): (left: AuditAdminItem, right: AuditAdminItem) => number { - return (left, right) => { - const primary = compareAuditValues(auditSortValue(left, sortBy), auditSortValue(right, sortBy)); - const directed = sortDirection === "asc" ? primary : -primary; - return directed || right.id.localeCompare(left.id); - }; -} - -function auditSortValue(item: AuditAdminItem, sortBy: AuditSortBy): string | number { - if (sortBy === "time") return new Date(item.created_at).getTime(); - if (sortBy === "actor") return item.actor_email || "System"; - if (sortBy === "action") return item.action; - if (sortBy === "object") return `${item.object_type || ""} ${item.object_id || ""}`; - return item.tenant_id || ""; -} - -function compareAuditValues(left: string | number, right: string | number): number { - if (typeof left === "number" && typeof right === "number") return left - right; - return String(left).localeCompare(String(right)); -} diff --git a/webui/src/features/admin/AdminPage.tsx b/webui/src/features/admin/AdminPage.tsx index 1155280..07e9e57 100644 --- a/webui/src/features/admin/AdminPage.tsx +++ b/webui/src/features/admin/AdminPage.tsx @@ -22,11 +22,9 @@ import GroupsPanel from "./GroupsPanel"; import RolesPanel from "./RolesPanel"; import ExternalFunctionRoleMappingsPanel from "./ExternalFunctionRoleMappingsPanel"; import ApiKeysPanel from "./ApiKeysPanel"; -import AdminAuditPanel from "./AdminAuditPanel"; import FileConnectorsPanel from "./FileConnectorsPanel"; import MailProfilesPanel from "./MailProfilesPanel"; -import RetentionPoliciesPanel from "./RetentionPoliciesPanel"; -import { usePlatformModuleInstalled, usePlatformUiCapabilities, usePlatformUiCapability } from "@govoplan/core-webui"; +import { usePlatformUiCapabilities, usePlatformUiCapability } from "@govoplan/core-webui"; type AdminSection = string; type OrderedAdminNavItem = { id: AdminSection; label: string; order: number }; @@ -37,7 +35,6 @@ const handledAdminSectionIds = new Set([ "system-configuration-changes", "system-configuration-packages", "system-modules", - "system-audit", "system-tenants", "system-roles", "system-role-templates", @@ -45,7 +42,6 @@ const handledAdminSectionIds = new Set([ "system-users", "system-file-connectors", "system-mail-servers", - "system-retention", "tenant-settings", "tenant-roles", "tenant-function-role-mappings", @@ -54,14 +50,10 @@ const handledAdminSectionIds = new Set([ "tenant-file-connectors", "tenant-mail-servers", "tenant-api-keys", - "tenant-retention", - "tenant-audit", "tenant-group-file-connectors", "tenant-group-mail-servers", - "tenant-group-retention", "tenant-user-file-connectors", - "tenant-user-mail-servers", - "tenant-user-retention" + "tenant-user-mail-servers" ]); export default function AdminPage({ @@ -79,8 +71,6 @@ export default function AdminPage({ const adminSectionCapabilities = usePlatformUiCapabilities("admin.sections"); const mailProfilesAvailable = Boolean(mailProfilesUi); const fileConnectorsAvailable = Boolean(fileConnectorsUi); - const auditAvailable = usePlatformModuleInstalled("audit"); - const policyAvailable = usePlatformModuleInstalled("policy"); const contributedSections = useMemo( () => adminSectionCapabilities @@ -102,13 +92,11 @@ export default function AdminPage({ if (canUseContributedSection(auth, section)) sections.add(section.id); } if (hasScope(auth, "system:settings:read")) { - if (policyAvailable) sections.add("system-retention"); if (mailProfilesAvailable) sections.add("system-mail-servers"); } if (hasScope(auth, "system:tenants:read")) sections.add("system-tenants"); if (hasAnyScope(auth, ["system:accounts:read", "system:access:read"])) sections.add("system-users"); if (hasAnyScope(auth, ["system:roles:read", "system:access:read"])) sections.add("system-roles"); - if (auditAvailable && hasScope(auth, "system:audit:read")) sections.add("system-audit"); if (hasScope(auth, "admin:users:read")) sections.add("tenant-users"); if (hasScope(auth, "admin:groups:read")) sections.add("tenant-groups"); if (hasScope(auth, "admin:roles:read")) sections.add("tenant-roles"); @@ -123,15 +111,9 @@ export default function AdminPage({ if (hasScope(auth, "admin:users:read")) sections.add("tenant-user-file-connectors"); if (hasScope(auth, "admin:groups:read")) sections.add("tenant-group-file-connectors"); } - if (policyAvailable && hasScope(auth, "admin:policies:read")) { - sections.add("tenant-retention"); - if (hasScope(auth, "admin:users:read")) sections.add("tenant-user-retention"); - if (hasScope(auth, "admin:groups:read")) sections.add("tenant-group-retention"); - } if (hasScope(auth, "admin:settings:read")) sections.add("tenant-settings"); - if (auditAvailable && hasScope(auth, "audit:read")) sections.add("tenant-audit"); return sections; - }, [auth, auditAvailable, contributedSections, fileConnectorsAvailable, mailProfilesAvailable, organizationFunctionPicker, policyAvailable]); + }, [auth, contributedSections, fileConnectorsAvailable, mailProfilesAvailable, organizationFunctionPicker]); const [searchParams, setSearchParams] = useSearchParams(); const requestedSection = searchParams.get("section") as AdminSection | null; const fallbackSection = available.has("overview") ? "overview" : (Array.from(available)[0] ?? "overview"); @@ -176,7 +158,6 @@ export default function AdminPage({ visibleNavItem(available, "system-configuration-packages", "i18n:govoplan-access.packages.0a999012", 20), visibleNavItem(available, "system-settings", "i18n:govoplan-access.maintenance.94de303b", 30), visibleNavItem(available, "system-configuration-changes", "i18n:govoplan-access.changes.8aa57de6", 40), - visibleNavItem(available, "system-audit", "i18n:govoplan-access.audit.fa1703dd", 90), ...contributedNavItems(contributedSections, available, "ADMINISTRATION", handledAdminSectionIds) ]) ) @@ -192,7 +173,6 @@ export default function AdminPage({ visibleNavItem(available, "system-users", "i18n:govoplan-access.users.57f2b181", 50), visibleNavItem(available, "system-file-connectors", "i18n:govoplan-access.file_connections.1e362326", 60), visibleNavItem(available, "system-mail-servers", "i18n:govoplan-access.mail_servers.d627326a", 70), - visibleNavItem(available, "system-retention", "i18n:govoplan-access.retention.c7199d9e", 80), ...contributedNavItems(contributedSections, available, "GLOBAL", handledAdminSectionIds), ...contributedNavItems(contributedSections, available, "SYSTEM", handledAdminSectionIds) ]) @@ -209,9 +189,7 @@ export default function AdminPage({ visibleNavItem(available, "tenant-file-connectors", "i18n:govoplan-access.file_connections.1e362326", 50), visibleNavItem(available, "tenant-mail-servers", "i18n:govoplan-access.mail_servers.d627326a", 60), visibleNavItem(available, "tenant-api-keys", "i18n:govoplan-access.api_keys.94fcf3c2", 70), - visibleNavItem(available, "tenant-retention", "i18n:govoplan-access.retention.c7199d9e", 80), visibleNavItem(available, "tenant-settings", "i18n:govoplan-access.general.9239ee2c", 90), - visibleNavItem(available, "tenant-audit", "i18n:govoplan-access.audit.fa1703dd", 100), ...contributedNavItems(contributedSections, available, "TENANT", handledAdminSectionIds) ]) ) @@ -222,7 +200,6 @@ export default function AdminPage({ sortNavItems([ visibleNavItem(available, "tenant-group-file-connectors", "i18n:govoplan-access.file_connections.1e362326", 10), visibleNavItem(available, "tenant-group-mail-servers", "i18n:govoplan-access.mail_servers.d627326a", 20), - visibleNavItem(available, "tenant-group-retention", "i18n:govoplan-access.retention.c7199d9e", 30), ...contributedNavItems(contributedSections, available, "GROUP", handledAdminSectionIds) ]) ) @@ -233,7 +210,6 @@ export default function AdminPage({ sortNavItems([ visibleNavItem(available, "tenant-user-file-connectors", "i18n:govoplan-access.file_connections.1e362326", 10), visibleNavItem(available, "tenant-user-mail-servers", "i18n:govoplan-access.mail_servers.d627326a", 20), - visibleNavItem(available, "tenant-user-retention", "i18n:govoplan-access.retention.c7199d9e", 30), ...contributedNavItems(contributedSections, available, "USER", handledAdminSectionIds) ]) ) @@ -248,7 +224,6 @@ export default function AdminPage({
{contributedSection && contributedSection.render(contributionContext)} - {!contributedSection && active === "system-retention" && } {!contributedSection && active === "system-mail-servers" && ( )} @@ -267,7 +242,6 @@ export default function AdminPage({ /> )} {!contributedSection && active === "system-roles" && } - {!contributedSection && active === "system-audit" && } {!contributedSection && active === "tenant-users" && } {!contributedSection && active === "tenant-groups" && } {!contributedSection && active === "tenant-roles" && } @@ -278,11 +252,7 @@ export default function AdminPage({ {!contributedSection && active === "tenant-group-mail-servers" && } {!contributedSection && active === "tenant-user-file-connectors" && } {!contributedSection && active === "tenant-group-file-connectors" && } - {!contributedSection && active === "tenant-retention" && } - {!contributedSection && active === "tenant-user-retention" && } - {!contributedSection && active === "tenant-group-retention" && } {!contributedSection && active === "tenant-settings" && } - {!contributedSection && active === "tenant-audit" && }
diff --git a/webui/src/features/admin/RetentionPoliciesPanel.tsx b/webui/src/features/admin/RetentionPoliciesPanel.tsx deleted file mode 100644 index 3e05b0c..0000000 --- a/webui/src/features/admin/RetentionPoliciesPanel.tsx +++ /dev/null @@ -1,161 +0,0 @@ -import { useEffect, useRef, useState } from "react"; -import type { ApiSettings } from "@govoplan/core-webui"; -import { fetchGroupsDelta, fetchUsersDelta, runRetentionPolicy, type GroupSummary, type PrivacyRetentionPolicyScope, type RetentionRunResponse, type UserAdminItem } from "../../api/admin"; -import { Button } from "@govoplan/core-webui"; -import { Card } from "@govoplan/core-webui"; -import { ConfirmDialog } from "@govoplan/core-webui"; -import { RetentionPolicyScopeManager, type RetentionPolicyTargetOption } from "@govoplan/core-webui"; -import { AdminPageLayout, adminErrorMessage, useDeltaWatermarks } from "@govoplan/core-webui"; -import { loadDeltaRows } from "./utils/deltaRows"; - -type Props = { - settings: ApiSettings; - scopeType: Extract; - canWrite: boolean; -}; - -const copy: Record = { - system: { - title: "i18n:govoplan-access.system_retention.4191e7f7", - description: "i18n:govoplan-access.instance_wide_privacy_retention_policy_and_lower.646ce224", - policyTitle: "i18n:govoplan-access.system_retention_policy.7027f6ba", - policyDescription: "i18n:govoplan-access.set_concrete_system_retention_values_the_allow_o.02e1fd13" - }, - tenant: { - title: "i18n:govoplan-access.tenant_retention.95b35db0", - description: "i18n:govoplan-access.tenant_level_privacy_and_retention_limits_for_th.a6d4108c", - policyTitle: "i18n:govoplan-access.tenant_retention_policy.f10893d7", - policyDescription: "i18n:govoplan-access.tenant_limits_may_only_narrow_the_system_policy_.de974a77" - }, - user: { - title: "i18n:govoplan-access.user_retention.f0966bbf", - description: "i18n:govoplan-access.user_scoped_retention_limits_for_campaigns_owned.cbc9268b", - targetLabel: "i18n:govoplan-access.user.9f8a2389", - policyTitle: "i18n:govoplan-access.user_retention_policy.2776f485", - policyDescription: "i18n:govoplan-access.user_limits_may_only_narrow_inherited_system_and.b194c7c0" - }, - group: { - title: "i18n:govoplan-access.group_retention.57cdcdaa", - description: "i18n:govoplan-access.group_scoped_retention_limits_for_group_owned_ca.e8e25e04", - targetLabel: "i18n:govoplan-access.group.171a0606", - policyTitle: "i18n:govoplan-access.group_retention_policy.ad941c0b", - policyDescription: "i18n:govoplan-access.group_limits_may_only_narrow_inherited_system_an.32649b6f" - } -}; - -export default function RetentionPoliciesPanel({ settings, scopeType, canWrite }: Props) { - const [targets, setTargets] = useState([]); - const usersRef = useRef([]); - const groupsRef = useRef([]); - const { getDeltaWatermark, setDeltaWatermark, resetDeltaWatermark } = useDeltaWatermarks(); - const [loadingTargets, setLoadingTargets] = useState(scopeType === "user" || scopeType === "group"); - const [targetError, setTargetError] = useState(""); - const [busy, setBusy] = useState(false); - const [success, setSuccess] = useState(""); - const [runError, setRunError] = useState(""); - const [confirmRetentionRun, setConfirmRetentionRun] = useState(false); - const [retentionResult, setRetentionResult] = useState(null); - - useEffect(() => { - usersRef.current = []; - groupsRef.current = []; - resetDeltaWatermark(); - void loadTargets(); - }, [settings.accessToken, settings.apiBaseUrl, settings.apiKey, scopeType, resetDeltaWatermark]); - - async function loadTargets() { - if (scopeType !== "user" && scopeType !== "group") { - setTargets([]); - setLoadingTargets(false); - setTargetError(""); - return; - } - setLoadingTargets(true); - setTargetError(""); - try { - if (scopeType === "user") { - const users = await loadDeltaRows(usersRef.current, "access:retention-users", getDeltaWatermark, setDeltaWatermark, (since) => fetchUsersDelta(settings, { since }), (response) => response.users, (user) => user.id, "access_user", sortUsers); - usersRef.current = users; - setTargets(users.map((user) => ({ - id: user.id, - label: user.display_name || user.email, - secondary: user.display_name ? user.email : null - }))); - } else { - const groups = await loadDeltaRows(groupsRef.current, "access:retention-groups", getDeltaWatermark, setDeltaWatermark, (since) => fetchGroupsDelta(settings, { since }), (response) => response.groups, (group) => group.id, "access_group", sortGroups); - groupsRef.current = groups; - setTargets(groups.map((group) => ({ - id: group.id, - label: group.name, - secondary: group.slug - }))); - } - } catch (err) { - setTargets([]); - setTargetError(adminErrorMessage(err)); - } finally { - setLoadingTargets(false); - } - } - - async function runRetention(dryRun: boolean) { - setBusy(true); - setRunError(""); - setSuccess(""); - try { - const response = await runRetentionPolicy(settings, dryRun); - setRetentionResult(response); - setSuccess(dryRun ? "i18n:govoplan-access.retention_dry_run_completed.91895aee" : "i18n:govoplan-access.retention_policy_applied.7fa4e050"); - setConfirmRetentionRun(false); - } catch (err) {setRunError(adminErrorMessage(err));} finally - {setBusy(false);} - } - - const labels = copy[scopeType]; - - return ( - <> - - - - {scopeType === "system" && -
- -

i18n:govoplan-access.run_the_saved_effective_retention_policy_against.cd39a54c

-
- - -
- {retentionResult &&
{JSON.stringify(retentionResult.result, null, 2)}
} -
-
- } -
- setConfirmRetentionRun(false)} - onConfirm={() => void runRetention(false)} /> - - ); - -} - -function sortUsers(left: UserAdminItem, right: UserAdminItem): number { - return left.email.localeCompare(right.email); -} - -function sortGroups(left: GroupSummary, right: GroupSummary): number { - return left.name.localeCompare(right.name) || left.slug.localeCompare(right.slug); -}