143 lines
6.1 KiB
TypeScript
143 lines
6.1 KiB
TypeScript
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)}
|
|
/>
|
|
</>
|
|
);
|
|
}
|