Release v0.1.7
This commit is contained in:
10
README.md
10
README.md
@@ -1,10 +1,12 @@
|
|||||||
# GovOPlaN Policy
|
# GovOPlaN Policy
|
||||||
|
|
||||||
`govoplan-policy` owns policy and retention API route contributions during the
|
`govoplan-policy` owns policy and retention API route contributions and the
|
||||||
GovOPlaN module split.
|
retention administration WebUI sections during the GovOPlaN module split.
|
||||||
|
|
||||||
The current package delegates to the legacy access administration
|
The `@govoplan/policy-webui` package contributes system, tenant, group, and
|
||||||
implementation while route ownership is separated before model migration.
|
user retention sections through the shared `admin.sections` UI capability. The
|
||||||
|
admin shell does not render retention policy panels unless this module is
|
||||||
|
installed and enabled.
|
||||||
|
|
||||||
Policy decision and provenance payloads use the shared kernel DTOs documented
|
Policy decision and provenance payloads use the shared kernel DTOs documented
|
||||||
in [docs/POLICY_DECISION_PROVENANCE.md](docs/POLICY_DECISION_PROVENANCE.md)
|
in [docs/POLICY_DECISION_PROVENANCE.md](docs/POLICY_DECISION_PROVENANCE.md)
|
||||||
|
|||||||
32
package.json
Normal file
32
package.json
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
{
|
||||||
|
"name": "@govoplan/policy-webui",
|
||||||
|
"version": "0.1.7",
|
||||||
|
"private": true,
|
||||||
|
"type": "module",
|
||||||
|
"main": "webui/src/index.ts",
|
||||||
|
"module": "webui/src/index.ts",
|
||||||
|
"types": "webui/src/index.ts",
|
||||||
|
"exports": {
|
||||||
|
".": {
|
||||||
|
"types": "./webui/src/index.ts",
|
||||||
|
"import": "./webui/src/index.ts"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"files": [
|
||||||
|
"webui/src",
|
||||||
|
"README.md",
|
||||||
|
"LICENSE"
|
||||||
|
],
|
||||||
|
"peerDependencies": {
|
||||||
|
"@govoplan/core-webui": "^0.1.7",
|
||||||
|
"lucide-react": "^1.23.0",
|
||||||
|
"react": "^19.0.0",
|
||||||
|
"react-dom": "^19.0.0",
|
||||||
|
"react-router-dom": "^7.1.1"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@govoplan/core-webui": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -4,13 +4,13 @@ build-backend = "setuptools.build_meta"
|
|||||||
|
|
||||||
[project]
|
[project]
|
||||||
name = "govoplan-policy"
|
name = "govoplan-policy"
|
||||||
version = "0.1.6"
|
version = "0.1.7"
|
||||||
description = "GovOPlaN policy platform module."
|
description = "GovOPlaN policy platform module."
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
requires-python = ">=3.12"
|
requires-python = ">=3.12"
|
||||||
authors = [{ name = "GovOPlaN" }]
|
authors = [{ name = "GovOPlaN" }]
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"govoplan-core>=0.1.6",
|
"govoplan-core>=0.1.7",
|
||||||
]
|
]
|
||||||
|
|
||||||
[tool.setuptools.packages.find]
|
[tool.setuptools.packages.find]
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ from __future__ import annotations
|
|||||||
|
|
||||||
from govoplan_core.core.access import CAPABILITY_AUTH_PERMISSION_EVALUATOR, CAPABILITY_AUTH_PRINCIPAL_RESOLVER
|
from govoplan_core.core.access import CAPABILITY_AUTH_PERMISSION_EVALUATOR, CAPABILITY_AUTH_PRINCIPAL_RESOLVER
|
||||||
from govoplan_core.core.policy import CAPABILITY_POLICY_PRIVACY_RETENTION
|
from govoplan_core.core.policy import CAPABILITY_POLICY_PRIVACY_RETENTION
|
||||||
from govoplan_core.core.modules import ModuleContext, ModuleManifest
|
from govoplan_core.core.modules import FrontendModule, ModuleContext, ModuleManifest
|
||||||
|
|
||||||
|
|
||||||
def _route_factory(context: ModuleContext):
|
def _route_factory(context: ModuleContext):
|
||||||
@@ -22,9 +22,13 @@ def _privacy_retention_service(context: ModuleContext) -> object:
|
|||||||
manifest = ModuleManifest(
|
manifest = ModuleManifest(
|
||||||
id="policy",
|
id="policy",
|
||||||
name="Policy",
|
name="Policy",
|
||||||
version="0.1.6",
|
version="0.1.7",
|
||||||
required_capabilities=(CAPABILITY_AUTH_PRINCIPAL_RESOLVER, CAPABILITY_AUTH_PERMISSION_EVALUATOR),
|
required_capabilities=(CAPABILITY_AUTH_PRINCIPAL_RESOLVER, CAPABILITY_AUTH_PERMISSION_EVALUATOR),
|
||||||
route_factory=_route_factory,
|
route_factory=_route_factory,
|
||||||
|
frontend=FrontendModule(
|
||||||
|
module_id="policy",
|
||||||
|
package_name="@govoplan/policy-webui",
|
||||||
|
),
|
||||||
capability_factories={
|
capability_factories={
|
||||||
CAPABILITY_POLICY_PRIVACY_RETENTION: _privacy_retention_service,
|
CAPABILITY_POLICY_PRIVACY_RETENTION: _privacy_retention_service,
|
||||||
},
|
},
|
||||||
|
|||||||
27
webui/package.json
Normal file
27
webui/package.json
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
{
|
||||||
|
"name": "@govoplan/policy-webui",
|
||||||
|
"version": "0.1.7",
|
||||||
|
"private": true,
|
||||||
|
"type": "module",
|
||||||
|
"main": "src/index.ts",
|
||||||
|
"module": "src/index.ts",
|
||||||
|
"types": "src/index.ts",
|
||||||
|
"exports": {
|
||||||
|
".": {
|
||||||
|
"types": "./src/index.ts",
|
||||||
|
"import": "./src/index.ts"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@govoplan/core-webui": "^0.1.7",
|
||||||
|
"lucide-react": "^1.23.0",
|
||||||
|
"react": "^19.0.0",
|
||||||
|
"react-dom": "^19.0.0",
|
||||||
|
"react-router-dom": "^7.1.1"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@govoplan/core-webui": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
56
webui/src/api/adminTargets.ts
Normal file
56
webui/src/api/adminTargets.ts
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
import { apiFetch, type ApiSettings, type DeltaDeletedItem } from "@govoplan/core-webui";
|
||||||
|
|
||||||
|
export type RoleSummary = {
|
||||||
|
id: string;
|
||||||
|
slug: string;
|
||||||
|
name: string;
|
||||||
|
permissions: string[];
|
||||||
|
};
|
||||||
|
|
||||||
|
export type GroupSummary = {
|
||||||
|
id: string;
|
||||||
|
slug: string;
|
||||||
|
name: string;
|
||||||
|
description?: string | null;
|
||||||
|
is_active: boolean;
|
||||||
|
member_count: number;
|
||||||
|
member_ids: string[];
|
||||||
|
roles: RoleSummary[];
|
||||||
|
};
|
||||||
|
|
||||||
|
export type UserAdminItem = {
|
||||||
|
id: string;
|
||||||
|
account_id: string;
|
||||||
|
tenant_id: string;
|
||||||
|
email: string;
|
||||||
|
display_name?: string | null;
|
||||||
|
is_active: boolean;
|
||||||
|
groups: GroupSummary[];
|
||||||
|
roles: RoleSummary[];
|
||||||
|
};
|
||||||
|
|
||||||
|
type DeltaResponseFields = {
|
||||||
|
deleted: DeltaDeletedItem[];
|
||||||
|
watermark?: string | null;
|
||||||
|
has_more: boolean;
|
||||||
|
full: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type UserListDeltaResponse = { users: UserAdminItem[] } & DeltaResponseFields;
|
||||||
|
export type GroupListDeltaResponse = { groups: GroupSummary[] } & DeltaResponseFields;
|
||||||
|
|
||||||
|
function deltaSuffix(options: { since?: string | null; limit?: number } = {}): string {
|
||||||
|
const params = new URLSearchParams();
|
||||||
|
if (options.since) params.set("since", options.since);
|
||||||
|
if (options.limit) params.set("limit", String(options.limit));
|
||||||
|
const suffix = params.toString();
|
||||||
|
return suffix ? `?${suffix}` : "";
|
||||||
|
}
|
||||||
|
|
||||||
|
export function fetchUsersDelta(settings: ApiSettings, options: { since?: string | null; limit?: number } = {}): Promise<UserListDeltaResponse> {
|
||||||
|
return apiFetch(settings, `/api/v1/admin/users/delta${deltaSuffix(options)}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function fetchGroupsDelta(settings: ApiSettings, options: { since?: string | null; limit?: number } = {}): Promise<GroupListDeltaResponse> {
|
||||||
|
return apiFetch(settings, `/api/v1/admin/groups/delta${deltaSuffix(options)}`);
|
||||||
|
}
|
||||||
226
webui/src/features/policy/RetentionPoliciesPanel.tsx
Normal file
226
webui/src/features/policy/RetentionPoliciesPanel.tsx
Normal file
@@ -0,0 +1,226 @@
|
|||||||
|
import { useEffect, useRef, useState } from "react";
|
||||||
|
import {
|
||||||
|
AdminPageLayout,
|
||||||
|
adminErrorMessage,
|
||||||
|
Button,
|
||||||
|
Card,
|
||||||
|
ConfirmDialog,
|
||||||
|
mergeDeltaRows,
|
||||||
|
RetentionPolicyScopeManager,
|
||||||
|
runRetentionPolicy,
|
||||||
|
useDeltaWatermarks,
|
||||||
|
type ApiSettings,
|
||||||
|
type DeltaDeletedItem,
|
||||||
|
type PrivacyRetentionPolicyScope,
|
||||||
|
type RetentionPolicyTargetOption,
|
||||||
|
type RetentionRunResponse
|
||||||
|
} from "@govoplan/core-webui";
|
||||||
|
import { fetchGroupsDelta, fetchUsersDelta, type GroupListDeltaResponse, type GroupSummary, type UserAdminItem, type UserListDeltaResponse } from "../../api/adminTargets";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
settings: ApiSettings;
|
||||||
|
scopeType: Extract<PrivacyRetentionPolicyScope, "system" | "tenant" | "user" | "group">;
|
||||||
|
canWrite: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
type DeltaResponse = {
|
||||||
|
deleted: DeltaDeletedItem[];
|
||||||
|
watermark?: string | null;
|
||||||
|
has_more: boolean;
|
||||||
|
full: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
const copy: Record<Props["scopeType"], { title: string; description: string; targetLabel?: string; policyTitle: string; policyDescription: string }> = {
|
||||||
|
system: {
|
||||||
|
title: "System retention",
|
||||||
|
description: "Instance-wide privacy retention policy and lower-level override limits.",
|
||||||
|
policyTitle: "System retention policy",
|
||||||
|
policyDescription: "Set concrete system retention values. Override switches define whether lower levels may narrow each value."
|
||||||
|
},
|
||||||
|
tenant: {
|
||||||
|
title: "Tenant retention",
|
||||||
|
description: "Tenant-level privacy and retention limits for the active tenant.",
|
||||||
|
policyTitle: "Tenant retention policy",
|
||||||
|
policyDescription: "Tenant limits may only narrow the system policy where the parent policy allows overrides."
|
||||||
|
},
|
||||||
|
user: {
|
||||||
|
title: "User retention",
|
||||||
|
description: "User-scoped retention limits for campaigns owned by a user.",
|
||||||
|
targetLabel: "User",
|
||||||
|
policyTitle: "User retention policy",
|
||||||
|
policyDescription: "User limits may only narrow inherited system and tenant policy."
|
||||||
|
},
|
||||||
|
group: {
|
||||||
|
title: "Group retention",
|
||||||
|
description: "Group-scoped retention limits for campaigns owned by a group.",
|
||||||
|
targetLabel: "Group",
|
||||||
|
policyTitle: "Group retention policy",
|
||||||
|
policyDescription: "Group limits may only narrow inherited system and tenant policy."
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function RetentionPoliciesPanel({ settings, scopeType, canWrite }: Props) {
|
||||||
|
const [targets, setTargets] = useState<RetentionPolicyTargetOption[]>([]);
|
||||||
|
const usersRef = useRef<UserAdminItem[]>([]);
|
||||||
|
const groupsRef = useRef<GroupSummary[]>([]);
|
||||||
|
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<RetentionRunResponse | null>(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<UserAdminItem, UserListDeltaResponse>(
|
||||||
|
usersRef.current,
|
||||||
|
"policy: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<GroupSummary, GroupListDeltaResponse>(
|
||||||
|
groupsRef.current,
|
||||||
|
"policy: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 ? "Retention dry run completed." : "Retention policy applied.");
|
||||||
|
setConfirmRetentionRun(false);
|
||||||
|
} catch (err) {
|
||||||
|
setRunError(adminErrorMessage(err));
|
||||||
|
} finally {
|
||||||
|
setBusy(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const labels = copy[scopeType];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<AdminPageLayout title={labels.title} description={labels.description} loading={loadingTargets} error={targetError || runError} success={success}>
|
||||||
|
<RetentionPolicyScopeManager
|
||||||
|
settings={settings}
|
||||||
|
scopeType={scopeType}
|
||||||
|
targetOptions={targets}
|
||||||
|
targetLabel={labels.targetLabel}
|
||||||
|
title={labels.policyTitle}
|
||||||
|
description={labels.policyDescription}
|
||||||
|
canWrite={canWrite}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{scopeType === "system" && (
|
||||||
|
<div className="retention-run-card">
|
||||||
|
<Card title="Retention execution">
|
||||||
|
<p className="muted small-note">Run the saved effective retention policy against retained platform data.</p>
|
||||||
|
<div className="button-row compact-actions subsection-bottom-actions">
|
||||||
|
<Button onClick={() => void runRetention(true)} disabled={!canWrite || busy}>Dry run</Button>
|
||||||
|
<Button variant="danger" onClick={() => setConfirmRetentionRun(true)} disabled={!canWrite || busy}>Apply retention</Button>
|
||||||
|
</div>
|
||||||
|
{retentionResult && <pre className="admin-json-preview">{JSON.stringify(retentionResult.result, null, 2)}</pre>}
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</AdminPageLayout>
|
||||||
|
<ConfirmDialog
|
||||||
|
open={confirmRetentionRun}
|
||||||
|
title="Apply retention policy"
|
||||||
|
message="This will redact or delete eligible retained data according to the saved policy."
|
||||||
|
confirmLabel="Apply retention"
|
||||||
|
tone="danger"
|
||||||
|
busy={busy}
|
||||||
|
onCancel={() => setConfirmRetentionRun(false)}
|
||||||
|
onConfirm={() => void runRetention(false)}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadDeltaRows<TItem, TResponse extends DeltaResponse>(
|
||||||
|
current: TItem[],
|
||||||
|
key: string,
|
||||||
|
getDeltaWatermark: (key: string) => string | null,
|
||||||
|
setDeltaWatermark: (key: string, watermark: string | null | undefined) => void,
|
||||||
|
fetchDelta: (since: string | null) => Promise<TResponse>,
|
||||||
|
rowsFromResponse: (response: TResponse) => TItem[],
|
||||||
|
getKey: (item: TItem) => string,
|
||||||
|
deletedResourceType: string,
|
||||||
|
sort?: (left: TItem, right: TItem) => number
|
||||||
|
): Promise<TItem[]> {
|
||||||
|
let nextWatermark = getDeltaWatermark(key);
|
||||||
|
let merged = current;
|
||||||
|
let hasMore = false;
|
||||||
|
do {
|
||||||
|
const response = await fetchDelta(nextWatermark);
|
||||||
|
const rows = rowsFromResponse(response);
|
||||||
|
merged = response.full ? rows : mergeDeltaRows(merged, rows, response.deleted, getKey, { deletedResourceType, sort });
|
||||||
|
nextWatermark = response.watermark ?? null;
|
||||||
|
hasMore = response.has_more;
|
||||||
|
} while (hasMore);
|
||||||
|
setDeltaWatermark(key, nextWatermark);
|
||||||
|
return merged;
|
||||||
|
}
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
5
webui/src/index.ts
Normal file
5
webui/src/index.ts
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
export { default } from "./module";
|
||||||
|
export * from "./module";
|
||||||
|
export * from "./api/adminTargets";
|
||||||
|
export { default as RetentionPoliciesPanel } from "./features/policy/RetentionPoliciesPanel";
|
||||||
|
export type { PlatformWebModule } from "@govoplan/core-webui";
|
||||||
69
webui/src/module.ts
Normal file
69
webui/src/module.ts
Normal file
@@ -0,0 +1,69 @@
|
|||||||
|
import { createElement, lazy } from "react";
|
||||||
|
import { hasScope, type AdminSectionsUiCapability, type PlatformWebModule } from "@govoplan/core-webui";
|
||||||
|
|
||||||
|
const RetentionPoliciesPanel = lazy(() => import("./features/policy/RetentionPoliciesPanel"));
|
||||||
|
|
||||||
|
const policyAdminSections: AdminSectionsUiCapability = {
|
||||||
|
sections: [
|
||||||
|
{
|
||||||
|
id: "system-retention",
|
||||||
|
label: "Retention",
|
||||||
|
group: "SYSTEM",
|
||||||
|
order: 80,
|
||||||
|
allOf: ["system:settings:read"],
|
||||||
|
render: ({ settings, auth }) => createElement(RetentionPoliciesPanel, {
|
||||||
|
settings,
|
||||||
|
scopeType: "system",
|
||||||
|
canWrite: hasScope(auth, "system:settings:write")
|
||||||
|
})
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "tenant-retention",
|
||||||
|
label: "Retention",
|
||||||
|
group: "TENANT",
|
||||||
|
order: 80,
|
||||||
|
allOf: ["admin:policies:read"],
|
||||||
|
render: ({ settings, auth }) => createElement(RetentionPoliciesPanel, {
|
||||||
|
settings,
|
||||||
|
scopeType: "tenant",
|
||||||
|
canWrite: hasScope(auth, "admin:policies:write")
|
||||||
|
})
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "tenant-group-retention",
|
||||||
|
label: "Retention",
|
||||||
|
group: "GROUP",
|
||||||
|
order: 30,
|
||||||
|
allOf: ["admin:policies:read", "admin:groups:read"],
|
||||||
|
render: ({ settings, auth }) => createElement(RetentionPoliciesPanel, {
|
||||||
|
settings,
|
||||||
|
scopeType: "group",
|
||||||
|
canWrite: hasScope(auth, "admin:policies:write")
|
||||||
|
})
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "tenant-user-retention",
|
||||||
|
label: "Retention",
|
||||||
|
group: "USER",
|
||||||
|
order: 30,
|
||||||
|
allOf: ["admin:policies:read", "admin:users:read"],
|
||||||
|
render: ({ settings, auth }) => createElement(RetentionPoliciesPanel, {
|
||||||
|
settings,
|
||||||
|
scopeType: "user",
|
||||||
|
canWrite: hasScope(auth, "admin:policies:write")
|
||||||
|
})
|
||||||
|
}
|
||||||
|
]
|
||||||
|
};
|
||||||
|
|
||||||
|
export const policyModule: PlatformWebModule = {
|
||||||
|
id: "policy",
|
||||||
|
label: "Policy",
|
||||||
|
version: "0.1.6",
|
||||||
|
dependencies: ["access", "admin"],
|
||||||
|
uiCapabilities: {
|
||||||
|
"admin.sections": policyAdminSections
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export default policyModule;
|
||||||
Reference in New Issue
Block a user