257 lines
13 KiB
TypeScript
257 lines
13 KiB
TypeScript
import { useEffect, useState } from "react";
|
|
import type { ApiSettings } from "@govoplan/core-webui";
|
|
import { Button } from "@govoplan/core-webui";
|
|
import { Card } from "@govoplan/core-webui";
|
|
import { FormField } from "@govoplan/core-webui";
|
|
import { ToggleSwitch } from "@govoplan/core-webui";
|
|
import { fetchSystemSettingsDelta, updateSystemSettings, type LanguagePackage, type PrivacyRetentionLimitPermissions, type PrivacyRetentionPolicy, type SystemSettingsDeltaSections, type SystemSettingsItem } from "../../api/admin";
|
|
import { AdminPageLayout, adminErrorMessage, useDeltaWatermarks, useUnsavedDraftGuard } from "@govoplan/core-webui";
|
|
|
|
const DELTA_KEY = "admin:system-settings";
|
|
|
|
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 fallback: SystemSettingsItem = {
|
|
default_locale: "en",
|
|
allow_tenant_custom_groups: true,
|
|
allow_tenant_custom_roles: true,
|
|
allow_tenant_api_keys: true,
|
|
privacy_retention_policy: defaultPrivacyPolicy,
|
|
maintenance_mode: { enabled: false, message: "" },
|
|
available_languages: [
|
|
{ code: "en", label: "English", native_label: "English" },
|
|
{ code: "de", label: "German", native_label: "Deutsch" }
|
|
],
|
|
enabled_language_codes: ["en", "de"],
|
|
settings: {}
|
|
};
|
|
|
|
export default function SystemSettingsPanel({ settings, canWrite, canAccessMaintenance }: {settings: ApiSettings;canWrite: boolean;canAccessMaintenance: boolean;}) {
|
|
const [draft, setDraft] = useState<SystemSettingsItem>(fallback);
|
|
const [savedDraft, setSavedDraft] = useState<SystemSettingsItem>(fallback);
|
|
const [loading, setLoading] = useState(true);
|
|
const [busy, setBusy] = useState(false);
|
|
const [error, setError] = useState("");
|
|
const [success, setSuccess] = useState("");
|
|
const [packageDraft, setPackageDraft] = useState({ code: "", label: "", nativeLabel: "" });
|
|
const { getDeltaWatermark, setDeltaWatermark, resetDeltaWatermark } = useDeltaWatermarks();
|
|
const defaultLocaleOptions = localeOptions(draft.default_locale, draft.enabled_language_codes);
|
|
const dirty = systemSettingsDraftKey(draft) !== systemSettingsDraftKey(savedDraft);
|
|
|
|
useUnsavedDraftGuard({
|
|
dirty,
|
|
onSave: save,
|
|
onDiscard: () => {
|
|
setDraft(savedDraft);
|
|
setPackageDraft({ code: "", label: "", nativeLabel: "" });
|
|
}
|
|
});
|
|
|
|
async function load() {
|
|
setLoading(true);
|
|
setError("");
|
|
try {
|
|
const wasDirty = systemSettingsDraftKey(draft) !== systemSettingsDraftKey(savedDraft);
|
|
const response = await fetchSystemSettingsDelta(settings, { since: getDeltaWatermark(DELTA_KEY) });
|
|
setDeltaWatermark(DELTA_KEY, response.watermark);
|
|
if (response.full && response.item) {
|
|
setSavedDraft(response.item);
|
|
if (!wasDirty) setDraft(response.item);
|
|
} else if (response.changed_sections.length) {
|
|
setSavedDraft((current) => applySystemSettingsSections(current, response.sections));
|
|
if (!wasDirty) setDraft((current) => applySystemSettingsSections(current, response.sections));
|
|
}
|
|
}
|
|
catch (err) {setError(adminErrorMessage(err));} finally
|
|
{setLoading(false);}
|
|
}
|
|
|
|
useEffect(() => {
|
|
resetDeltaWatermark(DELTA_KEY);
|
|
void load();
|
|
}, [settings.accessToken, settings.apiBaseUrl, resetDeltaWatermark]);
|
|
|
|
async function save(): Promise<boolean> {
|
|
setBusy(true);
|
|
setError("");
|
|
try {
|
|
const saved = await updateSystemSettings(settings, {
|
|
default_locale: draft.default_locale,
|
|
allow_tenant_custom_groups: draft.allow_tenant_custom_groups,
|
|
allow_tenant_custom_roles: draft.allow_tenant_custom_roles,
|
|
allow_tenant_api_keys: draft.allow_tenant_api_keys,
|
|
maintenance_mode: draft.maintenance_mode,
|
|
available_languages: draft.available_languages,
|
|
enabled_language_codes: draft.enabled_language_codes
|
|
});
|
|
setDraft(saved);
|
|
setSavedDraft(saved);
|
|
resetDeltaWatermark(DELTA_KEY);
|
|
setSuccess("i18n:govoplan-admin.system_settings_saved.dac4f17b");
|
|
return true;
|
|
} catch (err) {setError(adminErrorMessage(err));return false;} finally
|
|
{setBusy(false);}
|
|
}
|
|
|
|
function toggleLanguage(code: string, checked: boolean) {
|
|
const enabled = new Set(draft.enabled_language_codes);
|
|
if (checked) enabled.add(code);
|
|
else enabled.delete(code);
|
|
const nextEnabled = draft.available_languages.map((item) => item.code).filter((item) => enabled.has(item));
|
|
const defaultLocale = nextEnabled.includes(draft.default_locale) ? draft.default_locale : (nextEnabled[0] ?? draft.default_locale);
|
|
setDraft({ ...draft, enabled_language_codes: nextEnabled, default_locale: defaultLocale });
|
|
}
|
|
|
|
function installLanguagePackage() {
|
|
const code = normalizeLanguageCode(packageDraft.code);
|
|
if (!code || !packageDraft.label.trim()) return;
|
|
const nextPackage: LanguagePackage = {
|
|
code,
|
|
label: packageDraft.label.trim(),
|
|
native_label: packageDraft.nativeLabel.trim() || packageDraft.label.trim()
|
|
};
|
|
const existing = new Set(draft.available_languages.map((item) => item.code));
|
|
const available = existing.has(code)
|
|
? draft.available_languages.map((item) => item.code === code ? nextPackage : item)
|
|
: [...draft.available_languages, nextPackage];
|
|
const enabled = draft.enabled_language_codes.includes(code) ? draft.enabled_language_codes : [...draft.enabled_language_codes, code];
|
|
setDraft({ ...draft, available_languages: available, enabled_language_codes: enabled, default_locale: draft.default_locale || code });
|
|
setPackageDraft({ code: "", label: "", nativeLabel: "" });
|
|
}
|
|
|
|
return (
|
|
<AdminPageLayout
|
|
title="i18n:govoplan-admin.system_general_settings.7cd662ed"
|
|
description="i18n:govoplan-admin.instance_wide_defaults_and_tenant_governance_cap.096a4f97"
|
|
loading={loading}
|
|
error={error}
|
|
success={success}
|
|
actions={<><Button onClick={() => void load()} disabled={loading}>i18n:govoplan-admin.reload.cce71553</Button><Button variant="primary" onClick={() => void save()} disabled={!canWrite || busy}>{busy ? "i18n:govoplan-admin.saving.56a2285c" : "i18n:govoplan-admin.save_settings.913aba9f"}</Button></>}>
|
|
|
|
<div className="admin-settings-form">
|
|
<Card title="i18n:govoplan-admin.defaults_for_newly_created_tenants.9d0f4ccd">
|
|
<FormField label="i18n:govoplan-admin.default_locale.b99d021f">
|
|
<select value={draft.default_locale} onChange={(event) => setDraft({ ...draft, default_locale: event.target.value })} disabled={!canWrite || busy || defaultLocaleOptions.length === 0}>
|
|
{defaultLocaleOptions.map((code) => {
|
|
const language = draft.available_languages.find((item) => item.code === code);
|
|
return <option key={code} value={code}>{languageOptionLabel(language ?? { code, label: code.toUpperCase() })}</option>;
|
|
})}
|
|
</select>
|
|
</FormField>
|
|
</Card>
|
|
<Card title="i18n:govoplan-admin.language_packages">
|
|
<div className="settings-list">
|
|
{draft.available_languages.map((item) => (
|
|
<label className="admin-inline-check" key={item.code}>
|
|
<input
|
|
type="checkbox"
|
|
checked={draft.enabled_language_codes.includes(item.code)}
|
|
disabled={!canWrite || busy || item.code === draft.default_locale}
|
|
onChange={(event) => toggleLanguage(item.code, event.target.checked)} />
|
|
<span><strong>{item.code.toUpperCase()}</strong> {languageOptionLabel(item)}</span>
|
|
</label>
|
|
))}
|
|
</div>
|
|
<div className="form-grid">
|
|
<FormField label="i18n:govoplan-admin.language_code">
|
|
<input value={packageDraft.code} disabled={!canWrite || busy} placeholder="fr" onChange={(event) => setPackageDraft({ ...packageDraft, code: event.target.value })} />
|
|
</FormField>
|
|
<FormField label="i18n:govoplan-admin.language_name">
|
|
<input value={packageDraft.label} disabled={!canWrite || busy} placeholder="i18n:govoplan-admin.language_name_placeholder" onChange={(event) => setPackageDraft({ ...packageDraft, label: event.target.value })} />
|
|
</FormField>
|
|
<FormField label="i18n:govoplan-admin.native_language_name">
|
|
<input value={packageDraft.nativeLabel} disabled={!canWrite || busy} placeholder="i18n:govoplan-admin.native_language_name_placeholder" onChange={(event) => setPackageDraft({ ...packageDraft, nativeLabel: event.target.value })} />
|
|
</FormField>
|
|
<div className="button-row compact-actions">
|
|
<Button onClick={installLanguagePackage} disabled={!canWrite || busy || !normalizeLanguageCode(packageDraft.code) || !packageDraft.label.trim()}>i18n:govoplan-admin.install_language_package</Button>
|
|
</div>
|
|
</div>
|
|
<p className="muted small-note">i18n:govoplan-admin.language_packages_help</p>
|
|
</Card>
|
|
<Card title="i18n:govoplan-admin.tenant_administration_capabilities.5d265972">
|
|
<div className="settings-list">
|
|
<ToggleSwitch checked={draft.allow_tenant_custom_groups} onChange={(checked) => setDraft({ ...draft, allow_tenant_custom_groups: checked })} label="i18n:govoplan-admin.allow_tenant_defined_groups_by_default.32099d5a" />
|
|
<ToggleSwitch checked={draft.allow_tenant_custom_roles} onChange={(checked) => setDraft({ ...draft, allow_tenant_custom_roles: checked })} label="i18n:govoplan-admin.allow_tenant_defined_roles_by_default.b1f79ee9" />
|
|
<ToggleSwitch checked={draft.allow_tenant_api_keys} onChange={(checked) => setDraft({ ...draft, allow_tenant_api_keys: checked })} label="i18n:govoplan-admin.allow_tenant_api_keys_by_default.58a6cf17" />
|
|
</div>
|
|
<p className="muted small-note">i18n:govoplan-admin.these_settings_are_enforced_by_the_backend_centr.8112ed6f</p>
|
|
</Card>
|
|
<Card title="i18n:govoplan-admin.maintenance_mode.98cca5c6">
|
|
<div className="settings-list">
|
|
<ToggleSwitch
|
|
checked={draft.maintenance_mode.enabled}
|
|
disabled={!canWrite || !canAccessMaintenance}
|
|
onChange={(checked) => setDraft({ ...draft, maintenance_mode: { ...draft.maintenance_mode, enabled: checked } })}
|
|
label="i18n:govoplan-admin.restrict_authenticated_api_access_to_maintenance.2bc47195" />
|
|
|
|
</div>
|
|
<FormField label="i18n:govoplan-admin.maintenance_message.ca62571f">
|
|
<textarea
|
|
rows={3}
|
|
value={draft.maintenance_mode.message ?? ""}
|
|
disabled={!canWrite}
|
|
onChange={(event) => setDraft({ ...draft, maintenance_mode: { ...draft.maintenance_mode, message: event.target.value } })} />
|
|
|
|
</FormField>
|
|
<p className="muted small-note">i18n:govoplan-admin.changing_the_maintenance_mode_flag_requires_syst.4492050f</p>
|
|
</Card>
|
|
</div>
|
|
</AdminPageLayout>);
|
|
|
|
}
|
|
|
|
function normalizeLanguageCode(value: string): string {
|
|
return value.trim().toLowerCase().replace(/_/g, "-").split(/[^a-z0-9]+/).filter(Boolean).join("-");
|
|
}
|
|
|
|
function localeOptions(current: string, enabled: string[]): string[] {
|
|
return [...new Set([current, ...enabled].filter((item) => item && item.trim()))];
|
|
}
|
|
|
|
function languageOptionLabel(language: LanguagePackage): string {
|
|
return `${language.code.toUpperCase()} - ${language.native_label || language.label}`;
|
|
}
|
|
|
|
function systemSettingsDraftKey(item: SystemSettingsItem): string {
|
|
return JSON.stringify({
|
|
default_locale: item.default_locale,
|
|
allow_tenant_custom_groups: item.allow_tenant_custom_groups,
|
|
allow_tenant_custom_roles: item.allow_tenant_custom_roles,
|
|
allow_tenant_api_keys: item.allow_tenant_api_keys,
|
|
maintenance_mode: item.maintenance_mode,
|
|
available_languages: item.available_languages,
|
|
enabled_language_codes: item.enabled_language_codes
|
|
});
|
|
}
|
|
|
|
function applySystemSettingsSections(item: SystemSettingsItem, sections: SystemSettingsDeltaSections): SystemSettingsItem {
|
|
return {
|
|
...item,
|
|
...(sections.defaults ?? {}),
|
|
...(sections.tenant_capabilities ?? {}),
|
|
...(sections.languages ?? {}),
|
|
...(sections.privacy_retention_policy ? { privacy_retention_policy: sections.privacy_retention_policy } : {}),
|
|
...(sections.maintenance_mode ? { maintenance_mode: sections.maintenance_mode } : {}),
|
|
...(sections.settings ? { settings: sections.settings } : {})
|
|
};
|
|
}
|