Prepare v0.1.0 release
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import { MailServerSettingsPanel, type MailServerConnectionTestResult, type MailServerFolderLookupResult, type MailServerImapSettings, type MailServerSmtpSettings } from "@govoplan/core-webui";
|
||||
import { EffectivePolicyBlock, EffectivePolicyValue, MailServerSettingsPanel, PolicyLockedHint, type MailServerConnectionTestResult, type MailServerFolderLookupResult, type MailServerImapSettings, type MailServerSmtpSettings, type PolicySourcePathItem } from "@govoplan/core-webui";
|
||||
import { Pencil, Plus, Trash2 } from "lucide-react";
|
||||
import type { ApiSettings } from "../../types";
|
||||
import {
|
||||
@@ -329,6 +329,8 @@ export function MailProfilePolicyEditor({
|
||||
}: MailProfilePolicyEditorProps) {
|
||||
const [policy, setPolicy] = useState<MailProfilePolicy>(blankPolicy);
|
||||
const [effectivePolicy, setEffectivePolicy] = useState<MailProfilePolicy | null>(null);
|
||||
const [parentPolicy, setParentPolicy] = useState<MailProfilePolicy | null>(null);
|
||||
const [effectivePolicySources, setEffectivePolicySources] = useState<PolicySourcePathItem[]>([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [busy, setBusy] = useState(false);
|
||||
const [error, setError] = useState("");
|
||||
@@ -345,6 +347,8 @@ export function MailProfilePolicyEditor({
|
||||
if (!scopeReady) {
|
||||
setPolicy(blankPolicy);
|
||||
setEffectivePolicy(null);
|
||||
setParentPolicy(null);
|
||||
setEffectivePolicySources([]);
|
||||
return;
|
||||
}
|
||||
setLoading(true);
|
||||
@@ -352,9 +356,13 @@ export function MailProfilePolicyEditor({
|
||||
const response = await getMailProfilePolicy(settings, scopeType, scopeId, campaignId);
|
||||
setPolicy(normalizePolicy(response.policy));
|
||||
setEffectivePolicy(response.effective_policy ? normalizePolicy(response.effective_policy) : null);
|
||||
setParentPolicy(response.parent_policy ? normalizePolicy(response.parent_policy) : null);
|
||||
setEffectivePolicySources(response.effective_policy_sources ?? []);
|
||||
} catch (err) {
|
||||
setPolicy(blankPolicy);
|
||||
setEffectivePolicy(null);
|
||||
setParentPolicy(null);
|
||||
setEffectivePolicySources([]);
|
||||
setError(errorMessage(err));
|
||||
} finally {
|
||||
setLoading(false);
|
||||
@@ -370,6 +378,8 @@ export function MailProfilePolicyEditor({
|
||||
const response = await updateMailProfilePolicy(settings, scopeType, normalizePolicy(policy), scopeId);
|
||||
setPolicy(normalizePolicy(response.policy));
|
||||
setEffectivePolicy(response.effective_policy ? normalizePolicy(response.effective_policy) : null);
|
||||
setParentPolicy(response.parent_policy ? normalizePolicy(response.parent_policy) : null);
|
||||
setEffectivePolicySources(response.effective_policy_sources ?? []);
|
||||
setSuccess("Mail profile policy saved.");
|
||||
await onSaved?.();
|
||||
} catch (err) {
|
||||
@@ -385,6 +395,18 @@ export function MailProfilePolicyEditor({
|
||||
);
|
||||
const selectedProfileIds = new Set(policy.allowed_profile_ids ?? []);
|
||||
const disabled = locked || busy || loading || !canWrite || !scopeReady;
|
||||
const parentAllowedProfileIds = parentPolicy?.allowed_profile_ids?.length ? new Set(parentPolicy.allowed_profile_ids) : null;
|
||||
const parentBlocksUserProfiles = parentPolicy?.allow_user_profiles === false;
|
||||
const parentBlocksGroupProfiles = parentPolicy?.allow_group_profiles === false;
|
||||
const parentBlocksCampaignProfiles = parentPolicy?.allow_campaign_profiles === false;
|
||||
const smtpCredentialLocked = parentPolicy?.smtp_credentials?.allow_override === false;
|
||||
const imapCredentialLocked = parentPolicy?.imap_credentials?.allow_override === false;
|
||||
const blockedProfileDefinitions = [
|
||||
parentBlocksUserProfiles ? "user" : "",
|
||||
parentBlocksGroupProfiles ? "group" : "",
|
||||
parentBlocksCampaignProfiles ? "campaign-local settings" : ""
|
||||
].filter(Boolean).join(", ");
|
||||
const lockedCredentialKinds = [smtpCredentialLocked ? "SMTP" : "", imapCredentialLocked ? "IMAP" : ""].filter(Boolean).join(" and ");
|
||||
|
||||
function patchPolicy(patch: Partial<MailProfilePolicy>) {
|
||||
setPolicy((current) => normalizePolicy({ ...current, ...patch }));
|
||||
@@ -438,31 +460,34 @@ export function MailProfilePolicyEditor({
|
||||
<div className="mail-profile-checkbox-grid">
|
||||
{candidateProfiles.map((profile) => (
|
||||
<label className="mail-profile-checkbox" key={profile.id}>
|
||||
<input type="checkbox" checked={selectedProfileIds.has(profile.id)} disabled={disabled} onChange={(event) => toggleAllowedProfile(profile.id, event.target.checked)} />
|
||||
<input type="checkbox" checked={selectedProfileIds.has(profile.id)} disabled={disabled || Boolean(parentAllowedProfileIds && !parentAllowedProfileIds.has(profile.id) && !selectedProfileIds.has(profile.id))} onChange={(event) => toggleAllowedProfile(profile.id, event.target.checked)} />
|
||||
<span><strong>{profile.name}</strong><small>{scopeLabel(profile)} · {transportLabel(profile.smtp)}</small></span>
|
||||
</label>
|
||||
))}
|
||||
{candidateProfiles.length === 0 && <p className="muted small-note">No profiles are visible for this policy scope.</p>}
|
||||
</div>
|
||||
{parentAllowedProfileIds && <p className="muted small-note">An ancestor allow-list limits selectable profiles to {parentAllowedProfileIds.size} profile(s).</p>}
|
||||
</section>
|
||||
|
||||
<section className="mail-policy-section">
|
||||
<h3>Lower-level profile definitions</h3>
|
||||
<h3>Lower-level mail definitions</h3>
|
||||
<div className="admin-form-grid three-columns mail-policy-flags">
|
||||
<PolicyFlagSelect label="User profiles" value={booleanToFlag(policy.allow_user_profiles)} disabled={disabled} onChange={(value) => setFlag("allow_user_profiles", value)} />
|
||||
<PolicyFlagSelect label="Group profiles" value={booleanToFlag(policy.allow_group_profiles)} disabled={disabled} onChange={(value) => setFlag("allow_group_profiles", value)} />
|
||||
<PolicyFlagSelect label="Campaign profiles" value={booleanToFlag(policy.allow_campaign_profiles)} disabled={disabled} onChange={(value) => setFlag("allow_campaign_profiles", value)} />
|
||||
<PolicyFlagSelect label="User profiles" value={booleanToFlag(policy.allow_user_profiles)} disabled={disabled} allowDisabled={parentBlocksUserProfiles} onChange={(value) => setFlag("allow_user_profiles", value)} />
|
||||
<PolicyFlagSelect label="Group profiles" value={booleanToFlag(policy.allow_group_profiles)} disabled={disabled} allowDisabled={parentBlocksGroupProfiles} onChange={(value) => setFlag("allow_group_profiles", value)} />
|
||||
<PolicyFlagSelect label="Campaign-local settings" value={booleanToFlag(policy.allow_campaign_profiles)} disabled={disabled} allowDisabled={parentBlocksCampaignProfiles} onChange={(value) => setFlag("allow_campaign_profiles", value)} />
|
||||
</div>
|
||||
{blockedProfileDefinitions && <PolicyLockedHint>Explicit allow is unavailable for {blockedProfileDefinitions} because an ancestor policy blocks those definitions.</PolicyLockedHint>}
|
||||
</section>
|
||||
|
||||
<section className="mail-policy-section">
|
||||
<h3>Credential inheritance</h3>
|
||||
<div className="admin-form-grid two-columns mail-policy-flags">
|
||||
<CredentialInheritanceSelect label="SMTP credentials" value={credentialInheritanceToValue(policy.smtp_credentials?.inherit)} disabled={disabled} onChange={(value) => setCredentialPolicy("smtp", { inherit: valueToCredentialInheritance(value) })} />
|
||||
<CredentialOverrideSelect label="SMTP override" value={credentialOverrideToValue(policy.smtp_credentials?.allow_override)} disabled={disabled} onChange={(value) => setCredentialPolicy("smtp", { allow_override: valueToCredentialOverride(value) })} />
|
||||
<CredentialInheritanceSelect label="IMAP credentials" value={credentialInheritanceToValue(policy.imap_credentials?.inherit)} disabled={disabled} onChange={(value) => setCredentialPolicy("imap", { inherit: valueToCredentialInheritance(value) })} />
|
||||
<CredentialOverrideSelect label="IMAP override" value={credentialOverrideToValue(policy.imap_credentials?.allow_override)} disabled={disabled} onChange={(value) => setCredentialPolicy("imap", { allow_override: valueToCredentialOverride(value) })} />
|
||||
<CredentialInheritanceSelect label="SMTP credentials" value={credentialInheritanceToValue(policy.smtp_credentials?.inherit)} disabled={disabled || smtpCredentialLocked} onChange={(value) => setCredentialPolicy("smtp", { inherit: valueToCredentialInheritance(value) })} />
|
||||
<CredentialOverrideSelect label="SMTP override" value={credentialOverrideToValue(policy.smtp_credentials?.allow_override)} disabled={disabled || smtpCredentialLocked} onChange={(value) => setCredentialPolicy("smtp", { allow_override: valueToCredentialOverride(value) })} />
|
||||
<CredentialInheritanceSelect label="IMAP credentials" value={credentialInheritanceToValue(policy.imap_credentials?.inherit)} disabled={disabled || imapCredentialLocked} onChange={(value) => setCredentialPolicy("imap", { inherit: valueToCredentialInheritance(value) })} />
|
||||
<CredentialOverrideSelect label="IMAP override" value={credentialOverrideToValue(policy.imap_credentials?.allow_override)} disabled={disabled || imapCredentialLocked} onChange={(value) => setCredentialPolicy("imap", { allow_override: valueToCredentialOverride(value) })} />
|
||||
</div>
|
||||
{lockedCredentialKinds && <PolicyLockedHint>{lockedCredentialKinds} credential inheritance is locked by an ancestor policy.</PolicyLockedHint>}
|
||||
</section>
|
||||
|
||||
<section className="mail-policy-section">
|
||||
@@ -480,17 +505,19 @@ export function MailProfilePolicyEditor({
|
||||
</section>
|
||||
|
||||
{effectivePolicy && (
|
||||
<section className="mail-policy-section mail-policy-effective">
|
||||
<h3>Effective campaign policy</h3>
|
||||
<div className="mail-policy-effective-grid">
|
||||
<div><span>Profiles</span><strong>{effectiveProfileLabel(effectivePolicy.allowed_profile_ids)}</strong></div>
|
||||
<div><span>User profiles</span><strong>{effectivePolicy.allow_user_profiles ? "Allowed" : "Blocked"}</strong></div>
|
||||
<div><span>Group profiles</span><strong>{effectivePolicy.allow_group_profiles ? "Allowed" : "Blocked"}</strong></div>
|
||||
<div><span>Campaign profiles</span><strong>{effectivePolicy.allow_campaign_profiles ? "Allowed" : "Blocked"}</strong></div>
|
||||
<div><span>SMTP credentials</span><strong>{credentialPolicyLabel(effectivePolicy.smtp_credentials)}</strong></div>
|
||||
<div><span>IMAP credentials</span><strong>{credentialPolicyLabel(effectivePolicy.imap_credentials)}</strong></div>
|
||||
</div>
|
||||
</section>
|
||||
<EffectivePolicyBlock
|
||||
title="Effective mail policy"
|
||||
sourcePath={effectivePolicySources.length > 0 ? effectivePolicySources : mailPolicySourcePath(scopeType)}
|
||||
className="mail-policy-section mail-policy-effective"
|
||||
gridClassName="mail-policy-effective-grid"
|
||||
>
|
||||
<EffectivePolicyValue label="Profiles" value={effectiveProfileLabel(effectivePolicy.allowed_profile_ids)} />
|
||||
<EffectivePolicyValue label="User profiles" value={effectivePolicy.allow_user_profiles ? "Allowed" : "Blocked"} />
|
||||
<EffectivePolicyValue label="Group profiles" value={effectivePolicy.allow_group_profiles ? "Allowed" : "Blocked"} />
|
||||
<EffectivePolicyValue label="Campaign-local" value={effectivePolicy.allow_campaign_profiles ? "Allowed" : "Blocked"} />
|
||||
<EffectivePolicyValue label="SMTP credentials" value={credentialPolicyLabel(effectivePolicy.smtp_credentials)} />
|
||||
<EffectivePolicyValue label="IMAP credentials" value={credentialPolicyLabel(effectivePolicy.imap_credentials)} />
|
||||
</EffectivePolicyBlock>
|
||||
)}
|
||||
</div>
|
||||
</Card>
|
||||
@@ -626,9 +653,11 @@ function ProfileForm({
|
||||
onImapEnabledChange={(imapEnabled) => setDraft({ ...draft, imapEnabled })}
|
||||
smtpDisabled={disabled}
|
||||
smtpCredentialDisabled={credentialDisabled}
|
||||
smtpPasswordSaved={Boolean(existingProfile?.smtp_password_configured)}
|
||||
imapToggleDisabled={imapToggleDisabled}
|
||||
imapServerDisabled={disabled || !draft.imapEnabled}
|
||||
imapCredentialDisabled={credentialDisabled || !draft.imapEnabled}
|
||||
imapPasswordSaved={Boolean(existingProfile?.imap_password_configured)}
|
||||
imapActionDisabled={disabled || !draft.imapEnabled}
|
||||
smtpTitle="SMTP"
|
||||
imapTitle="IMAP"
|
||||
@@ -648,12 +677,12 @@ function ProfileForm({
|
||||
);
|
||||
}
|
||||
|
||||
function PolicyFlagSelect({ label, value, disabled, onChange }: { label: string; value: PolicyFlagValue; disabled: boolean; onChange: (value: PolicyFlagValue) => void }) {
|
||||
function PolicyFlagSelect({ label, value, disabled, allowDisabled = false, onChange }: { label: string; value: PolicyFlagValue; disabled: boolean; allowDisabled?: boolean; onChange: (value: PolicyFlagValue) => void }) {
|
||||
return (
|
||||
<FormField label={label}>
|
||||
<select value={value} disabled={disabled} onChange={(event) => onChange(event.target.value as PolicyFlagValue)}>
|
||||
<option value="inherit">Inherit</option>
|
||||
<option value="allow">Explicit allow</option>
|
||||
<option value="allow" disabled={allowDisabled}>Explicit allow</option>
|
||||
<option value="deny">Deny</option>
|
||||
</select>
|
||||
</FormField>
|
||||
@@ -916,6 +945,14 @@ function effectiveProfileLabel(value: string[] | null | undefined): string {
|
||||
return `${value.length} profile(s)`;
|
||||
}
|
||||
|
||||
function mailPolicySourcePath(scopeType: MailProfileScope): string[] {
|
||||
if (scopeType === "system") return ["System"];
|
||||
if (scopeType === "tenant") return ["System", "Tenant"];
|
||||
if (scopeType === "user") return ["System", "Tenant", "User"];
|
||||
if (scopeType === "group") return ["System", "Tenant", "Group"];
|
||||
return ["System", "Tenant", "Owner policy", "Campaign"];
|
||||
}
|
||||
|
||||
function transportLabel(transport: MailSmtpTestPayload | MailImapTestPayload | null | undefined): string {
|
||||
if (!transport) return "Not configured";
|
||||
const host = transport.host || "No host";
|
||||
|
||||
Reference in New Issue
Block a user