Release v0.1.5
This commit is contained in:
142
webui/src/features/admin/RetentionPoliciesPanel.tsx
Normal file
142
webui/src/features/admin/RetentionPoliciesPanel.tsx
Normal file
@@ -0,0 +1,142 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import type { ApiSettings } from "@govoplan/core-webui";
|
||||
import { fetchGroups, fetchUsers, runRetentionPolicy, type PrivacyRetentionPolicyScope, type RetentionRunResponse } 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 } from "@govoplan/core-webui";
|
||||
|
||||
type Props = {
|
||||
settings: ApiSettings;
|
||||
scopeType: Extract<PrivacyRetentionPolicyScope, "system" | "tenant" | "user" | "group">;
|
||||
canWrite: 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 permissions.",
|
||||
policyTitle: "System retention policy",
|
||||
policyDescription: "Set concrete system retention values. The Allow override toggles decide which fields tenants, owners and campaigns may override."
|
||||
},
|
||||
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. The Allow override toggles decide which fields users, groups and campaigns may override."
|
||||
},
|
||||
user: {
|
||||
title: "User retention",
|
||||
description: "User-scoped retention limits for campaigns owned by users in the active tenant.",
|
||||
targetLabel: "User",
|
||||
policyTitle: "User retention policy",
|
||||
policyDescription: "User limits may only narrow inherited system and tenant policy. The Allow override toggles decide which fields user-owned campaigns may override."
|
||||
},
|
||||
group: {
|
||||
title: "Group retention",
|
||||
description: "Group-scoped retention limits for group-owned campaigns in the active tenant.",
|
||||
targetLabel: "Group",
|
||||
policyTitle: "Group retention policy",
|
||||
policyDescription: "Group limits may only narrow inherited system and tenant policy. The Allow override toggles decide which fields group-owned campaigns may override."
|
||||
}
|
||||
};
|
||||
|
||||
export default function RetentionPoliciesPanel({ settings, scopeType, canWrite }: Props) {
|
||||
const [targets, setTargets] = useState<RetentionPolicyTargetOption[]>([]);
|
||||
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(() => { void loadTargets(); }, [settings.accessToken, settings.apiBaseUrl, settings.apiKey, scopeType]);
|
||||
|
||||
async function loadTargets() {
|
||||
if (scopeType !== "user" && scopeType !== "group") {
|
||||
setTargets([]);
|
||||
setLoadingTargets(false);
|
||||
setTargetError("");
|
||||
return;
|
||||
}
|
||||
setLoadingTargets(true);
|
||||
setTargetError("");
|
||||
try {
|
||||
if (scopeType === "user") {
|
||||
const users = await fetchUsers(settings);
|
||||
setTargets(users.map((user) => ({
|
||||
id: user.id,
|
||||
label: user.display_name || user.email,
|
||||
secondary: user.display_name ? user.email : null
|
||||
})));
|
||||
} else {
|
||||
const groups = await fetchGroups(settings);
|
||||
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 stored raw JSON, generated EML, report detail, mock mailbox content and audit detail.</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. Run a dry run first if the counts have not been reviewed."
|
||||
confirmLabel="Apply retention"
|
||||
tone="danger"
|
||||
busy={busy}
|
||||
onCancel={() => setConfirmRetentionRun(false)}
|
||||
onConfirm={() => void runRetention(false)}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user