624 lines
29 KiB
TypeScript
624 lines
29 KiB
TypeScript
import { i18nMessage } from "../../i18n/LanguageContext";
|
|
import { useEffect, useMemo, useState, type ReactNode } from "react";
|
|
import type { ApiSettings } from "../../types";
|
|
import {
|
|
getPrivacyRetentionPolicy,
|
|
updatePrivacyRetentionPolicy,
|
|
type PrivacyRetentionLimitPermissionPatch,
|
|
type PrivacyRetentionLimitPermissions,
|
|
type PrivacyRetentionPolicy,
|
|
type PrivacyRetentionPolicyFieldKey,
|
|
type PrivacyRetentionPolicyPatch,
|
|
type PrivacyRetentionPolicyScope } from
|
|
"../../api/admin";
|
|
import Button from "../../components/Button";
|
|
import Card from "../../components/Card";
|
|
import DismissibleAlert from "../../components/DismissibleAlert";
|
|
import LoadingFrame from "../../components/LoadingFrame";
|
|
import { PolicyRow, PolicySection, PolicyTable } from "../../components/PolicyTable";
|
|
import type { PolicySourcePathItem } from "../../components/PolicySourcePath";
|
|
import FormField from "../../components/FormField";
|
|
import ToggleSwitch from "../../components/ToggleSwitch";
|
|
import { useUnsavedDraftGuard } from "../../components/UnsavedChangesGuard";
|
|
import {
|
|
privacyRetentionLocalAllowsLowerLevel,
|
|
privacyRetentionParentAllowsField } from
|
|
"./policyLogic";
|
|
|
|
export type RetentionPolicyTargetOption = {
|
|
id: string;
|
|
label: string;
|
|
secondary?: string | null;
|
|
};
|
|
|
|
type RetentionPolicyScopeManagerProps = {
|
|
settings: ApiSettings;
|
|
scopeType: PrivacyRetentionPolicyScope;
|
|
scopeId?: string | null;
|
|
targetOptions?: RetentionPolicyTargetOption[];
|
|
targetLabel?: string;
|
|
title?: string;
|
|
description?: string;
|
|
canWrite: boolean;
|
|
locked?: boolean;
|
|
};
|
|
|
|
type RetentionPolicyEditorProps = {
|
|
settings: ApiSettings;
|
|
scopeType: PrivacyRetentionPolicyScope;
|
|
scopeId?: string | null;
|
|
title?: string;
|
|
description?: string;
|
|
canWrite: boolean;
|
|
locked?: boolean;
|
|
};
|
|
|
|
type DayKey =
|
|
"raw_campaign_json_retention_days" |
|
|
"generated_eml_retention_days" |
|
|
"stored_report_detail_retention_days" |
|
|
"mock_mailbox_retention_days" |
|
|
"audit_detail_retention_days";
|
|
|
|
type FieldDefinition = {
|
|
key: PrivacyRetentionPolicyFieldKey;
|
|
label: string;
|
|
kind: "raw-json" | "days" | "audit";
|
|
systemOnly?: boolean;
|
|
};
|
|
|
|
type RawJsonValue = "inherit" | "keep" | "disable";
|
|
type AuditDetailValue = "inherit" | PrivacyRetentionPolicy["audit_detail_level"];
|
|
|
|
const auditDetailOrder: Record<PrivacyRetentionPolicy["audit_detail_level"], number> = { full: 0, redacted: 1, minimal: 2 };
|
|
|
|
const defaultAllowLowerLevelLimits: PrivacyRetentionLimitPermissions = {
|
|
store_raw_campaign_json: true,
|
|
raw_campaign_json_retention_days: true,
|
|
generated_eml_retention_days: true,
|
|
stored_report_detail_retention_days: true,
|
|
mock_mailbox_retention_days: true,
|
|
audit_detail_retention_days: true,
|
|
audit_detail_level: true
|
|
};
|
|
|
|
const defaultPrivacyPolicy: PrivacyRetentionPolicy = {
|
|
store_raw_campaign_json: true,
|
|
raw_campaign_json_retention_days: null,
|
|
generated_eml_retention_days: null,
|
|
stored_report_detail_retention_days: null,
|
|
mock_mailbox_retention_days: null,
|
|
audit_detail_retention_days: null,
|
|
audit_detail_level: "full",
|
|
allow_lower_level_limits: defaultAllowLowerLevelLimits
|
|
};
|
|
|
|
const fieldDefinitions: FieldDefinition[] = [
|
|
{ key: "store_raw_campaign_json", label: "i18n:govoplan-core.raw_campaign_json.4ca2265b", kind: "raw-json" },
|
|
{ key: "raw_campaign_json_retention_days", label: "i18n:govoplan-core.raw_campaign_json_days.48805a59", kind: "days" },
|
|
{ key: "generated_eml_retention_days", label: "i18n:govoplan-core.generated_eml_days.bcb273d9", kind: "days" },
|
|
{ key: "stored_report_detail_retention_days", label: "i18n:govoplan-core.stored_report_detail_days.296f6dab", kind: "days" },
|
|
{ key: "mock_mailbox_retention_days", label: "i18n:govoplan-core.mock_mailbox_days.a0cf3209", kind: "days", systemOnly: true },
|
|
{ key: "audit_detail_retention_days", label: "i18n:govoplan-core.audit_detail_days.21c84dd1", kind: "days" },
|
|
{ key: "audit_detail_level", label: "i18n:govoplan-core.audit_detail_level.b0565260", kind: "audit" }];
|
|
|
|
|
|
export function RetentionPolicyScopeManager({
|
|
settings,
|
|
scopeType,
|
|
scopeId = null,
|
|
targetOptions = [],
|
|
targetLabel = "i18n:govoplan-core.target.61ad50a9",
|
|
title = "i18n:govoplan-core.retention_policy.962fb418",
|
|
description,
|
|
canWrite,
|
|
locked = false
|
|
}: RetentionPolicyScopeManagerProps) {
|
|
const [selectedTargetId, setSelectedTargetId] = useState(scopeId || targetOptions[0]?.id || "");
|
|
const requiresTarget = scopeType === "user" || scopeType === "group" || scopeType === "campaign";
|
|
const targetSelectionRequired = requiresTarget && !scopeId;
|
|
const hasSelectableTarget = targetOptions.length > 0;
|
|
const activeScopeId = scopeId || selectedTargetId || null;
|
|
const defaultDescription = scopeType === "system" ?
|
|
"i18n:govoplan-core.system_retention_defaults_and_the_fields_lower_l.90a9e923" :
|
|
"i18n:govoplan-core.local_retention_limits_for_this_scope_values_inh.6114dade";
|
|
const targetEmptyText = i18nMessage("i18n:govoplan-core.no_value_available", { value0: targetPluralLabel(scopeType, targetLabel) });
|
|
|
|
useEffect(() => {
|
|
if (scopeId) {
|
|
setSelectedTargetId(scopeId);
|
|
return;
|
|
}
|
|
if (targetOptions.length > 0 && !targetOptions.some((option) => option.id === selectedTargetId)) {
|
|
setSelectedTargetId(targetOptions[0].id);
|
|
return;
|
|
}
|
|
if (targetOptions.length === 0 && selectedTargetId) setSelectedTargetId("");
|
|
}, [scopeId, selectedTargetId, targetOptions]);
|
|
|
|
return (
|
|
<div className="retention-policy-manager">
|
|
{targetSelectionRequired &&
|
|
<Card title={i18nMessage("i18n:govoplan-core.value_scope", { value0: targetLabel })}>
|
|
<div className="retention-policy-target-row">
|
|
<FormField label={targetLabel}>
|
|
<select value={selectedTargetId} disabled={!hasSelectableTarget} onChange={(event) => setSelectedTargetId(event.target.value)}>
|
|
{!hasSelectableTarget && <option value="">{targetEmptyText}</option>}
|
|
{targetOptions.map((option) => <option key={option.id} value={option.id}>{option.secondary ? i18nMessage("i18n:govoplan-core.value_value.c189e8bc", { value0: option.label, value1: option.secondary }) : option.label}</option>)}
|
|
</select>
|
|
</FormField>
|
|
</div>
|
|
</Card>
|
|
}
|
|
{(!requiresTarget || Boolean(activeScopeId)) &&
|
|
<RetentionPolicyEditor
|
|
settings={settings}
|
|
scopeType={scopeType}
|
|
scopeId={activeScopeId}
|
|
title={title}
|
|
description={description ?? defaultDescription}
|
|
canWrite={canWrite}
|
|
locked={locked} />
|
|
|
|
}
|
|
</div>);
|
|
|
|
}
|
|
|
|
function targetPluralLabel(scopeType: PrivacyRetentionPolicyScope, fallback: string): string {
|
|
if (scopeType === "user") return "i18n:govoplan-core.users";
|
|
if (scopeType === "group") return "i18n:govoplan-core.groups";
|
|
if (scopeType === "campaign") return "i18n:govoplan-core.campaigns.01a23a28";
|
|
return fallback;
|
|
}
|
|
|
|
export function RetentionPolicyEditor({
|
|
settings,
|
|
scopeType,
|
|
scopeId = null,
|
|
title = "i18n:govoplan-core.retention_policy.962fb418",
|
|
description,
|
|
canWrite,
|
|
locked = false
|
|
}: RetentionPolicyEditorProps) {
|
|
const [policy, setPolicy] = useState<PrivacyRetentionPolicyPatch>({});
|
|
const [effectivePolicy, setEffectivePolicy] = useState<PrivacyRetentionPolicy | null>(null);
|
|
const [parentPolicy, setParentPolicy] = useState<PrivacyRetentionPolicy | null>(null);
|
|
const [effectivePolicySources, setEffectivePolicySources] = useState<PolicySourcePathItem[]>([]);
|
|
const [savedPolicyKey, setSavedPolicyKey] = useState("");
|
|
const [loading, setLoading] = useState(false);
|
|
const [busy, setBusy] = useState(false);
|
|
const [error, setError] = useState("");
|
|
const [success, setSuccess] = useState("");
|
|
|
|
const isSystem = scopeType === "system";
|
|
const requiresTarget = scopeType === "user" || scopeType === "group" || scopeType === "campaign";
|
|
const scopeReady = !requiresTarget || Boolean(scopeId);
|
|
const disabled = locked || loading || busy || !canWrite || !scopeReady;
|
|
const showAllowColumn = scopeType !== "campaign";
|
|
const showEffectiveColumn = !isSystem && scopeReady;
|
|
const activeParentPolicy = parentPolicy ? normalizeFullPolicy(parentPolicy) : null;
|
|
const activeFullPolicy = normalizeFullPolicy({ ...defaultPrivacyPolicy, ...policy, allow_lower_level_limits: { ...defaultAllowLowerLevelLimits, ...(policy.allow_lower_level_limits ?? {}) } });
|
|
const visibleFields = useMemo(() => fieldDefinitions.filter((field) => isSystem || !field.systemOnly), [isSystem]);
|
|
const policyDirty = scopeReady && Boolean(savedPolicyKey) && policyDraftKey(policy, isSystem) !== savedPolicyKey;
|
|
|
|
useUnsavedDraftGuard({
|
|
dirty: policyDirty,
|
|
onSave: savePolicy,
|
|
onDiscard: () => {
|
|
if (!savedPolicyKey) return;
|
|
setPolicy(JSON.parse(savedPolicyKey) as PrivacyRetentionPolicyPatch);
|
|
}
|
|
});
|
|
|
|
useEffect(() => {void loadPolicy();}, [settings.accessToken, settings.apiBaseUrl, settings.apiKey, scopeType, scopeId]);
|
|
|
|
async function loadPolicy() {
|
|
setError("");
|
|
setSuccess("");
|
|
if (!scopeReady) {
|
|
setPolicy({});
|
|
setEffectivePolicy(null);
|
|
setParentPolicy(null);
|
|
setEffectivePolicySources([]);
|
|
return;
|
|
}
|
|
setLoading(true);
|
|
try {
|
|
const response = await getPrivacyRetentionPolicy(settings, scopeType, scopeId);
|
|
const loadedPolicy = isSystem ? normalizeFullPolicy(response.policy) : normalizePatch(response.policy);
|
|
setPolicy(loadedPolicy);
|
|
setSavedPolicyKey(policyDraftKey(loadedPolicy, isSystem));
|
|
setEffectivePolicy(normalizeFullPolicy(response.effective_policy));
|
|
setParentPolicy(response.parent_policy ? normalizeFullPolicy(response.parent_policy) : null);
|
|
setEffectivePolicySources(response.effective_policy_sources ?? []);
|
|
} catch (err) {
|
|
setPolicy({});
|
|
setSavedPolicyKey("");
|
|
setEffectivePolicy(null);
|
|
setParentPolicy(null);
|
|
setEffectivePolicySources([]);
|
|
setError(errorMessage(err));
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
}
|
|
|
|
async function savePolicy(): Promise<boolean> {
|
|
if (!scopeReady) return false;
|
|
setBusy(true);
|
|
setError("");
|
|
setSuccess("");
|
|
try {
|
|
const payload = isSystem ? normalizeFullPolicy(policy) : normalizePatchForSave(policy, activeParentPolicy, scopeType);
|
|
const response = await updatePrivacyRetentionPolicy(settings, scopeType, payload, scopeId);
|
|
const savedPolicy = isSystem ? normalizeFullPolicy(response.policy) : normalizePatch(response.policy);
|
|
setPolicy(savedPolicy);
|
|
setSavedPolicyKey(policyDraftKey(savedPolicy, isSystem));
|
|
setEffectivePolicy(normalizeFullPolicy(response.effective_policy));
|
|
setParentPolicy(response.parent_policy ? normalizeFullPolicy(response.parent_policy) : null);
|
|
setEffectivePolicySources(response.effective_policy_sources ?? []);
|
|
setSuccess("i18n:govoplan-core.retention_policy_saved.eb577758");
|
|
return true;
|
|
} catch (err) {
|
|
setError(errorMessage(err));
|
|
return false;
|
|
} finally {
|
|
setBusy(false);
|
|
}
|
|
}
|
|
|
|
function patchPolicy(patch: PrivacyRetentionPolicyPatch) {
|
|
const next = { ...policy, ...patch };
|
|
if (policy.allow_lower_level_limits || patch.allow_lower_level_limits) {
|
|
next.allow_lower_level_limits = { ...(policy.allow_lower_level_limits ?? {}), ...(patch.allow_lower_level_limits ?? {}) };
|
|
}
|
|
setPolicy(isSystem ? normalizeFullPolicy(next) : normalizePatch(next));
|
|
}
|
|
|
|
function setRawJson(value: RawJsonValue | "store" | "disabled") {
|
|
if (isSystem) {
|
|
patchPolicy({ store_raw_campaign_json: value === "store" });
|
|
return;
|
|
}
|
|
patchPolicy({ store_raw_campaign_json: value === "inherit" ? undefined : value === "keep" });
|
|
}
|
|
|
|
function setRetentionDays(key: DayKey, value: string) {
|
|
const trimmed = value.trim();
|
|
patchPolicy({ [key]: trimmed === "" ? isSystem ? null : undefined : Math.max(0, Number(trimmed) || 0) });
|
|
}
|
|
|
|
function setAuditDetail(value: AuditDetailValue) {
|
|
patchPolicy({ audit_detail_level: value === "inherit" ? undefined : value });
|
|
}
|
|
|
|
function setAllowLowerLevelLimit(key: PrivacyRetentionPolicyFieldKey, allowed: boolean) {
|
|
patchPolicy({ allow_lower_level_limits: { [key]: allowed } });
|
|
}
|
|
|
|
function parentAllows(key: PrivacyRetentionPolicyFieldKey): boolean {
|
|
return privacyRetentionParentAllowsField(activeParentPolicy, key);
|
|
}
|
|
|
|
function localAllowsLower(key: PrivacyRetentionPolicyFieldKey): boolean {
|
|
return privacyRetentionLocalAllowsLowerLevel(isSystem ? activeFullPolicy : policy, activeParentPolicy, key, isSystem);
|
|
}
|
|
|
|
const localPolicySummary = useMemo(() => isSystem ? "i18n:govoplan-core.system_baseline.219ce334" : localPolicyCount(policy), [isSystem, policy]);
|
|
const defaultDescription = isSystem ?
|
|
"i18n:govoplan-core.set_concrete_system_retention_values_blank_day_f.98b9a627" :
|
|
"i18n:govoplan-core.blank_values_inherit_numeric_values_may_only_sho.1a96ce70";
|
|
|
|
return (
|
|
<Card
|
|
title={title}
|
|
actions={
|
|
<div className="button-row compact-actions">
|
|
<Button onClick={() => void loadPolicy()} disabled={loading || !scopeReady}>{loading ? "i18n:govoplan-core.loading.33ce4174" : "i18n:govoplan-core.reload.cce71553"}</Button>
|
|
<Button variant="primary" onClick={() => void savePolicy()} disabled={disabled}>{busy ? "i18n:govoplan-core.saving.56a2285c" : "i18n:govoplan-core.save_policy.77d67ce3"}</Button>
|
|
</div>
|
|
}>
|
|
|
|
<LoadingFrame loading={loading} label="i18n:govoplan-core.loading_retention_policy.dcd30fb6">
|
|
<div className="retention-policy-editor">
|
|
<p className="muted small-note retention-policy-description">{description ?? defaultDescription}</p>
|
|
{!isSystem && <p className="muted small-note retention-policy-description">i18n:govoplan-core.mock_mailbox_retention_is_currently_managed_at_s.8c6a8ec7</p>}
|
|
{error && <DismissibleAlert tone="danger" resetKey={error} floating>{error}</DismissibleAlert>}
|
|
{success && <DismissibleAlert tone="success" resetKey={success} floating>{success}</DismissibleAlert>}
|
|
|
|
<PolicySection
|
|
className="retention-policy-section"
|
|
title={isSystem ? "i18n:govoplan-core.system_policy.3e1aac4b" : "i18n:govoplan-core.local_policy.942f8665"}
|
|
summary={<span className="muted small-note">{localPolicySummary}</span>}>
|
|
<PolicyTable
|
|
className="retention-policy-table"
|
|
rowClassName="retention-policy-row"
|
|
headerClassName="retention-policy-row-header"
|
|
fieldLabel="i18n:govoplan-core.field.c326a466"
|
|
settingLabel={isSystem ? "i18n:govoplan-core.value.8dce170d" : "i18n:govoplan-core.local_setting.967607a9"}
|
|
effectiveLabel="i18n:govoplan-core.effective_policy.feedb950"
|
|
lowerLevelLabel="i18n:govoplan-core.lower_levels.940821ee"
|
|
showEffectiveColumn={showEffectiveColumn}
|
|
showAllowColumn={showAllowColumn}>
|
|
{visibleFields.map((field) => {
|
|
const fieldLocked = !parentAllows(field.key);
|
|
const fieldDisabled = disabled || fieldLocked;
|
|
return (
|
|
<PolicyRow
|
|
key={field.key}
|
|
className="retention-policy-row"
|
|
labelClassName="retention-policy-field-label"
|
|
effectiveCellClassName="retention-policy-effective-cell"
|
|
effectiveClassName="retention-policy-effective-value"
|
|
label={field.label}
|
|
note={
|
|
<>
|
|
{fieldLocked && <small>i18n:govoplan-core.locked_by_parent_policy.66f587c1</small>}
|
|
{field.systemOnly && <small>i18n:govoplan-core.system_level_cleanup_scope.2bce57b5</small>}
|
|
</>
|
|
}
|
|
control={renderFieldControl(field, activeFullPolicy, policy, isSystem, fieldDisabled, setRawJson, setRetentionDays, setAuditDetail, activeParentPolicy)}
|
|
effective={showEffectiveColumn ? retentionFieldValue(field, effectivePolicy, loading) : undefined}
|
|
effectiveHelp={showEffectiveColumn ? retentionPolicyPathHelp(field, effectivePolicySources, scopeType) : undefined}
|
|
allowControl={showAllowColumn ?
|
|
<ToggleSwitch
|
|
checked={localAllowsLower(field.key)}
|
|
disabled={disabled || fieldLocked}
|
|
onChange={(checked) => setAllowLowerLevelLimit(field.key, checked)}
|
|
label="i18n:govoplan-core.allow_override.ffa6e9a0" /> :
|
|
undefined} />);
|
|
|
|
})}
|
|
</PolicyTable>
|
|
</PolicySection>
|
|
|
|
</div>
|
|
</LoadingFrame>
|
|
</Card>);
|
|
|
|
}
|
|
|
|
function policyDraftKey(policy: PrivacyRetentionPolicyPatch, isSystem: boolean): string {
|
|
return JSON.stringify(isSystem ? normalizeFullPolicy(policy) : normalizePatch(policy));
|
|
}
|
|
|
|
type PolicySourceItem = {
|
|
label: string;
|
|
policy?: Record<string, unknown> | null;
|
|
};
|
|
|
|
function retentionPolicyPathHelp(field: FieldDefinition, sources: PolicySourcePathItem[], scopeType: PrivacyRetentionPolicyScope): ReactNode {
|
|
const items = normalizePolicySourceItems(sources.length > 0 ? sources : retentionPolicySourcePath(scopeType));
|
|
return <PolicyPathHelp lines={retentionPolicyPathLines(field, items)} />;
|
|
}
|
|
|
|
function PolicyPathHelp({ lines }: {lines: string[];}) {
|
|
return (
|
|
<span className="policy-path-help">
|
|
{lines.map((line, index) => <span className="policy-path-help-line" key={`${line}-${index}`}>{line}</span>)}
|
|
</span>);
|
|
|
|
}
|
|
|
|
function retentionPolicyPathLines(field: FieldDefinition, items: PolicySourceItem[]): string[] {
|
|
if (items.length === 0) return ["i18n:govoplan-core.system_default.1f06f3ed"];
|
|
const lines: string[] = [];
|
|
for (const [index, item] of items.entries()) {
|
|
const sourcePolicy = asRecord(item.policy);
|
|
const value = retentionSourceValue(field, sourcePolicy, index === 0);
|
|
const locked = asRecord(sourcePolicy.allow_lower_level_limits)[field.key] === false;
|
|
lines.push(`${policyPathPrefix(index)}${item.label}: ${locked ? `${value} without override` : value}`);
|
|
if (locked) break;
|
|
}
|
|
return lines;
|
|
}
|
|
|
|
function retentionSourceValue(field: FieldDefinition, sourcePolicy: Record<string, unknown>, isSystem: boolean): string {
|
|
if (Object.keys(sourcePolicy).length === 0) return isSystem ? "i18n:govoplan-core.default.808d7dca" : "i18n:govoplan-core.inherit.18f99833";
|
|
if (!isSystem && !(field.key in sourcePolicy)) return "i18n:govoplan-core.inherit.18f99833";
|
|
if (field.kind === "raw-json") {
|
|
const value = sourcePolicy.store_raw_campaign_json;
|
|
return value === false ? "i18n:govoplan-core.disabled.f4f4473d" : value === true ? "i18n:govoplan-core.stored.b2b53ee4" : "i18n:govoplan-core.inherit.18f99833";
|
|
}
|
|
if (field.kind === "audit") {
|
|
const value = sourcePolicy.audit_detail_level;
|
|
return value === "full" || value === "redacted" || value === "minimal" ? auditDetailLabel(value) : "i18n:govoplan-core.inherit.18f99833";
|
|
}
|
|
const value = sourcePolicy[field.key];
|
|
return typeof value === "number" ? daysLabel(value) : value === null && isSystem ? daysLabel(null) : "i18n:govoplan-core.inherit.18f99833";
|
|
}
|
|
|
|
function normalizePolicySourceItems(items: PolicySourcePathItem[]): PolicySourceItem[] {
|
|
return items.map((item) => {
|
|
if (typeof item === "string") return { label: item };
|
|
return { label: item.label, policy: item.policy };
|
|
}).filter((item) => item.label.trim());
|
|
}
|
|
|
|
function asRecord(value: unknown): Record<string, unknown> {
|
|
return value && typeof value === "object" && !Array.isArray(value) ? value as Record<string, unknown> : {};
|
|
}
|
|
|
|
function policyPathPrefix(index: number): string {
|
|
return index === 0 ? "" : `${" ".repeat(index - 1)}> `;
|
|
}
|
|
|
|
function retentionFieldValue(field: FieldDefinition, policy: PrivacyRetentionPolicy | null, loading: boolean): string {
|
|
if (!policy) return loading ? "i18n:govoplan-core.loading.b04ba49f" : "i18n:govoplan-core.unavailable.2c9c1f79";
|
|
if (field.kind === "raw-json") return policy.store_raw_campaign_json ? "i18n:govoplan-core.stored.b2b53ee4" : "i18n:govoplan-core.disabled.f4f4473d";
|
|
if (field.kind === "audit") return auditDetailLabel(policy.audit_detail_level);
|
|
return daysLabel(policy[field.key as DayKey]);
|
|
}
|
|
|
|
function renderFieldControl(
|
|
field: FieldDefinition,
|
|
fullPolicy: PrivacyRetentionPolicy,
|
|
patchPolicy: PrivacyRetentionPolicyPatch,
|
|
isSystem: boolean,
|
|
disabled: boolean,
|
|
setRawJson: (value: RawJsonValue | "store" | "disabled") => void,
|
|
setRetentionDays: (key: DayKey, value: string) => void,
|
|
setAuditDetail: (value: AuditDetailValue) => void,
|
|
parentPolicy: PrivacyRetentionPolicy | null)
|
|
{
|
|
if (field.kind === "raw-json") {
|
|
if (isSystem) {
|
|
return <RawJsonSystemSelect value={fullPolicy.store_raw_campaign_json ? "store" : "disabled"} disabled={disabled} onChange={setRawJson} />;
|
|
}
|
|
return <RawJsonScopedSelect value={rawJsonValue(patchPolicy.store_raw_campaign_json)} parentStoresRawJson={parentPolicy?.store_raw_campaign_json ?? true} disabled={disabled} onChange={setRawJson} />;
|
|
}
|
|
if (field.kind === "audit") {
|
|
return <AuditDetailSelect value={isSystem ? fullPolicy.audit_detail_level : auditDetailValue(patchPolicy.audit_detail_level)} includeInherit={!isSystem} parentValue={isSystem ? null : parentPolicy?.audit_detail_level ?? null} disabled={disabled} onChange={setAuditDetail} />;
|
|
}
|
|
const parentDayLimit = !isSystem && parentPolicy ? parentPolicy[field.key as DayKey] : null;
|
|
return (
|
|
<RetentionDaysField
|
|
value={isSystem ? fullPolicy[field.key as DayKey] : patchPolicy[field.key as DayKey]}
|
|
disabled={disabled}
|
|
placeholder={isSystem ? "i18n:govoplan-core.unlimited.b8bef37b" : "i18n:govoplan-core.inherit.18f99833"}
|
|
max={parentDayLimit}
|
|
onChange={(value) => setRetentionDays(field.key as DayKey, value)} />);
|
|
|
|
|
|
}
|
|
|
|
function RawJsonSystemSelect({ value, disabled, onChange }: {value: "store" | "disabled";disabled: boolean;onChange: (value: "store" | "disabled") => void;}) {
|
|
return (
|
|
<select value={value} disabled={disabled} onChange={(event) => onChange(event.target.value as "store" | "disabled")}>
|
|
<option value="store">i18n:govoplan-core.store.0d8a7046</option>
|
|
<option value="disabled">i18n:govoplan-core.disable_storage.07c44d8b</option>
|
|
</select>);
|
|
|
|
}
|
|
|
|
function RawJsonScopedSelect({ value, parentStoresRawJson, disabled, onChange }: {value: RawJsonValue;parentStoresRawJson: boolean;disabled: boolean;onChange: (value: RawJsonValue) => void;}) {
|
|
return (
|
|
<select value={value} disabled={disabled} onChange={(event) => onChange(event.target.value as RawJsonValue)}>
|
|
<option value="inherit">i18n:govoplan-core.inherit.18f99833</option>
|
|
<option value="keep" disabled={!parentStoresRawJson}>i18n:govoplan-core.store_if_parent_allows.36dcb07b</option>
|
|
<option value="disable">i18n:govoplan-core.disable_storage.07c44d8b</option>
|
|
</select>);
|
|
|
|
}
|
|
|
|
function RetentionDaysField({ value, disabled, placeholder, max, onChange }: {value?: number | null;disabled: boolean;placeholder: string;max?: number | null;onChange: (value: string) => void;}) {
|
|
function handleChange(nextValue: string) {
|
|
const trimmed = nextValue.trim();
|
|
if (trimmed === "" || max === null || max === undefined) {
|
|
onChange(nextValue);
|
|
return;
|
|
}
|
|
const parsed = Number(trimmed);
|
|
onChange(Number.isFinite(parsed) && parsed > max ? String(max) : nextValue);
|
|
}
|
|
|
|
return <input type="number" min={0} max={max ?? undefined} value={value ?? ""} disabled={disabled} placeholder={placeholder} onChange={(event) => handleChange(event.target.value)} />;
|
|
}
|
|
|
|
function AuditDetailSelect({ value, includeInherit, parentValue, disabled, onChange }: {value: AuditDetailValue;includeInherit: boolean;parentValue?: PrivacyRetentionPolicy["audit_detail_level"] | null;disabled: boolean;onChange: (value: AuditDetailValue) => void;}) {
|
|
const minimumOrder = parentValue ? auditDetailOrder[parentValue] : 0;
|
|
function optionDisabled(option: PrivacyRetentionPolicy["audit_detail_level"]): boolean {
|
|
return auditDetailOrder[option] < minimumOrder;
|
|
}
|
|
|
|
return (
|
|
<select value={value} disabled={disabled} onChange={(event) => onChange(event.target.value as AuditDetailValue)}>
|
|
{includeInherit && <option value="inherit">i18n:govoplan-core.inherit.18f99833</option>}
|
|
<option value="full" disabled={optionDisabled("full")}>i18n:govoplan-core.full.10b28a8c</option>
|
|
<option value="redacted" disabled={optionDisabled("redacted")}>i18n:govoplan-core.redacted.8780785a</option>
|
|
<option value="minimal" disabled={optionDisabled("minimal")}>i18n:govoplan-core.minimal.a711cca9</option>
|
|
</select>);
|
|
|
|
}
|
|
|
|
function rawJsonValue(value: boolean | undefined): RawJsonValue {
|
|
if (value === undefined) return "inherit";
|
|
return value ? "keep" : "disable";
|
|
}
|
|
|
|
function auditDetailValue(value: PrivacyRetentionPolicyPatch["audit_detail_level"]): AuditDetailValue {
|
|
return value ?? "inherit";
|
|
}
|
|
|
|
function daysLabel(value?: number | null): string {
|
|
return value === null || value === undefined ? "i18n:govoplan-core.unlimited.b8bef37b" : `${value} day${value === 1 ? "" : "s"}`;
|
|
}
|
|
|
|
function auditDetailLabel(value: PrivacyRetentionPolicy["audit_detail_level"]): string {
|
|
if (value === "full") return "i18n:govoplan-core.full.10b28a8c";
|
|
if (value === "redacted") return "i18n:govoplan-core.redacted.8780785a";
|
|
return "i18n:govoplan-core.minimal.a711cca9";
|
|
}
|
|
|
|
function localPolicyCount(policy: PrivacyRetentionPolicyPatch): string {
|
|
let count = 0;
|
|
for (const field of fieldDefinitions) {
|
|
if (field.systemOnly) continue;
|
|
if (policy[field.key as keyof PrivacyRetentionPolicyPatch] !== undefined && policy[field.key as keyof PrivacyRetentionPolicyPatch] !== null) count += 1;
|
|
}
|
|
const allowCount = Object.keys(policy.allow_lower_level_limits ?? {}).length;
|
|
count += allowCount;
|
|
return count === 0 ? "i18n:govoplan-core.fully_inherited.5e48132a" : `${count} local override${count === 1 ? "" : "s"}`;
|
|
}
|
|
|
|
function retentionPolicySourcePath(scopeType: PrivacyRetentionPolicyScope): string[] {
|
|
if (scopeType === "system") return ["i18n:govoplan-core.system.bc0792d8"];
|
|
if (scopeType === "tenant") return ["i18n:govoplan-core.system.bc0792d8", "i18n:govoplan-core.tenant.3ca93c78"];
|
|
if (scopeType === "user") return ["i18n:govoplan-core.system.bc0792d8", "i18n:govoplan-core.tenant.3ca93c78", "i18n:govoplan-core.user.9f8a2389"];
|
|
if (scopeType === "group") return ["i18n:govoplan-core.system.bc0792d8", "i18n:govoplan-core.tenant.3ca93c78", "i18n:govoplan-core.group.171a0606"];
|
|
return ["i18n:govoplan-core.system.bc0792d8", "i18n:govoplan-core.tenant.3ca93c78", "i18n:govoplan-core.owner_policy.1e8df143", "i18n:govoplan-core.campaign.69390e16"];
|
|
}
|
|
|
|
function normalizeAllowLimits(value: PrivacyRetentionLimitPermissionPatch | PrivacyRetentionLimitPermissions | null | undefined): PrivacyRetentionLimitPermissions {
|
|
return { ...defaultAllowLowerLevelLimits, ...(value ?? {}) };
|
|
}
|
|
|
|
function normalizeFullPolicy(policy: PrivacyRetentionPolicyPatch | Partial<PrivacyRetentionPolicy> | null | undefined): PrivacyRetentionPolicy {
|
|
return {
|
|
...defaultPrivacyPolicy,
|
|
...(policy ?? {}),
|
|
raw_campaign_json_retention_days: policy?.raw_campaign_json_retention_days ?? null,
|
|
generated_eml_retention_days: policy?.generated_eml_retention_days ?? null,
|
|
stored_report_detail_retention_days: policy?.stored_report_detail_retention_days ?? null,
|
|
mock_mailbox_retention_days: policy?.mock_mailbox_retention_days ?? null,
|
|
audit_detail_retention_days: policy?.audit_detail_retention_days ?? null,
|
|
allow_lower_level_limits: normalizeAllowLimits(policy?.allow_lower_level_limits)
|
|
};
|
|
}
|
|
|
|
function normalizePatch(policy: PrivacyRetentionPolicyPatch | null | undefined): PrivacyRetentionPolicyPatch {
|
|
const normalized: PrivacyRetentionPolicyPatch = {};
|
|
if (!policy) return normalized;
|
|
if (policy.store_raw_campaign_json !== undefined) normalized.store_raw_campaign_json = policy.store_raw_campaign_json;
|
|
for (const field of fieldDefinitions) {
|
|
if (field.kind !== "days" || field.systemOnly) continue;
|
|
const value = policy[field.key as DayKey];
|
|
if (value !== undefined && value !== null) normalized[field.key as DayKey] = Math.max(0, Number(value) || 0);
|
|
}
|
|
if (policy.audit_detail_level) normalized.audit_detail_level = policy.audit_detail_level;
|
|
if (policy.allow_lower_level_limits) normalized.allow_lower_level_limits = { ...policy.allow_lower_level_limits };
|
|
return normalized;
|
|
}
|
|
|
|
function normalizePatchForSave(policy: PrivacyRetentionPolicyPatch, parentPolicy: PrivacyRetentionPolicy | null, scopeType: PrivacyRetentionPolicyScope): PrivacyRetentionPolicyPatch {
|
|
const normalized = normalizePatch(policy);
|
|
const allowPatch = { ...(normalized.allow_lower_level_limits ?? {}) };
|
|
for (const field of fieldDefinitions) {
|
|
if (field.systemOnly || scopeType === "campaign") delete allowPatch[field.key];
|
|
if (field.systemOnly) delete normalized[field.key as keyof PrivacyRetentionPolicyPatch];
|
|
if (parentPolicy && parentPolicy.allow_lower_level_limits[field.key] === false) {
|
|
delete normalized[field.key as keyof PrivacyRetentionPolicyPatch];
|
|
delete allowPatch[field.key];
|
|
}
|
|
}
|
|
if (Object.keys(allowPatch).length > 0) normalized.allow_lower_level_limits = allowPatch;else
|
|
delete normalized.allow_lower_level_limits;
|
|
return normalized;
|
|
}
|
|
|
|
function errorMessage(err: unknown): string {
|
|
if (err instanceof Error) return err.message;
|
|
return "i18n:govoplan-core.request_failed.9fcda32c";
|
|
}
|