Prepare v0.1.0 release dependencies
This commit is contained in:
24
webui/src/components/EffectivePolicyBlock.tsx
Normal file
24
webui/src/components/EffectivePolicyBlock.tsx
Normal file
@@ -0,0 +1,24 @@
|
||||
import type { ReactNode } from "react";
|
||||
import PolicySourcePath, { type PolicySourcePathItem } from "./PolicySourcePath";
|
||||
|
||||
export type EffectivePolicyBlockProps = {
|
||||
title: string;
|
||||
sourcePath?: PolicySourcePathItem[];
|
||||
children: ReactNode;
|
||||
className?: string;
|
||||
gridClassName?: string;
|
||||
};
|
||||
|
||||
export function EffectivePolicyValue({ label, value }: { label: string; value: ReactNode }) {
|
||||
return <div><span>{label}</span><strong>{value}</strong></div>;
|
||||
}
|
||||
|
||||
export default function EffectivePolicyBlock({ title, sourcePath, children, className = "", gridClassName = "" }: EffectivePolicyBlockProps) {
|
||||
return (
|
||||
<section className={`policy-effective-block ${className}`.trim()}>
|
||||
<h3>{title}</h3>
|
||||
{sourcePath && <PolicySourcePath items={sourcePath} />}
|
||||
<div className={gridClassName || "policy-effective-grid"}>{children}</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
64
webui/src/components/PasswordField.tsx
Normal file
64
webui/src/components/PasswordField.tsx
Normal file
@@ -0,0 +1,64 @@
|
||||
import { useId, useState, type InputHTMLAttributes } from "react";
|
||||
import { Eye, EyeOff } from "lucide-react";
|
||||
|
||||
export type PasswordFieldProps = Omit<InputHTMLAttributes<HTMLInputElement>, "type" | "value" | "onChange"> & {
|
||||
value: string;
|
||||
onValueChange: (value: string) => void;
|
||||
saved?: boolean;
|
||||
savedPlaceholder?: string;
|
||||
revealLabel?: string;
|
||||
hideLabel?: string;
|
||||
inputClassName?: string;
|
||||
};
|
||||
|
||||
export default function PasswordField({
|
||||
value,
|
||||
onValueChange,
|
||||
saved = false,
|
||||
savedPlaceholder = "••••••••",
|
||||
revealLabel = "Show password",
|
||||
hideLabel = "Hide password",
|
||||
placeholder,
|
||||
disabled = false,
|
||||
className = "",
|
||||
inputClassName = "",
|
||||
id,
|
||||
...inputProps
|
||||
}: PasswordFieldProps) {
|
||||
const generatedId = useId();
|
||||
const inputId = id ?? generatedId;
|
||||
const [visible, setVisible] = useState(false);
|
||||
const hasTypedPassword = value.length > 0;
|
||||
const showSavedPlaceholder = saved && !hasTypedPassword;
|
||||
const canReveal = hasTypedPassword && !disabled;
|
||||
const inputType = visible && canReveal ? "text" : "password";
|
||||
|
||||
return (
|
||||
<div className={`password-field ${canReveal ? "has-toggle" : ""} ${showSavedPlaceholder ? "is-saved-empty" : ""} ${className}`.trim()}>
|
||||
<input
|
||||
{...inputProps}
|
||||
id={inputId}
|
||||
className={inputClassName}
|
||||
type={inputType}
|
||||
value={value}
|
||||
disabled={disabled}
|
||||
placeholder={showSavedPlaceholder ? savedPlaceholder : placeholder}
|
||||
onChange={(event) => {
|
||||
onValueChange(event.target.value);
|
||||
if (!event.target.value) setVisible(false);
|
||||
}}
|
||||
/>
|
||||
{canReveal && (
|
||||
<button
|
||||
type="button"
|
||||
className="password-field-toggle"
|
||||
aria-label={visible ? hideLabel : revealLabel}
|
||||
title={visible ? hideLabel : revealLabel}
|
||||
onClick={() => setVisible((current) => !current)}
|
||||
>
|
||||
{visible ? <EyeOff size={17} aria-hidden="true" /> : <Eye size={17} aria-hidden="true" />}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
11
webui/src/components/PolicyLockedHint.tsx
Normal file
11
webui/src/components/PolicyLockedHint.tsx
Normal file
@@ -0,0 +1,11 @@
|
||||
import type { ReactNode } from "react";
|
||||
|
||||
export type PolicyLockedHintProps = {
|
||||
children: ReactNode;
|
||||
className?: string;
|
||||
};
|
||||
|
||||
export default function PolicyLockedHint({ children, className = "" }: PolicyLockedHintProps) {
|
||||
if (!children) return null;
|
||||
return <p className={`muted small-note policy-locked-hint ${className}`.trim()}>{children}</p>;
|
||||
}
|
||||
66
webui/src/components/PolicySourcePath.tsx
Normal file
66
webui/src/components/PolicySourcePath.tsx
Normal file
@@ -0,0 +1,66 @@
|
||||
export type PolicySourcePathItem = string | {
|
||||
scope_type?: string;
|
||||
scope_id?: string | null;
|
||||
label: string;
|
||||
applied_fields?: string[];
|
||||
appliedFields?: string[];
|
||||
};
|
||||
|
||||
export type PolicySourcePathProps = {
|
||||
items: PolicySourcePathItem[];
|
||||
label?: string;
|
||||
className?: string;
|
||||
};
|
||||
|
||||
type NormalizedPolicySourcePathItem = {
|
||||
label: string;
|
||||
appliedFields: string[];
|
||||
};
|
||||
|
||||
export default function PolicySourcePath({ items, label = "Source path", className = "" }: PolicySourcePathProps) {
|
||||
const cleanItems = items.map(normalizeSourceItem).filter((item): item is NormalizedPolicySourcePathItem => Boolean(item?.label));
|
||||
if (cleanItems.length === 0) return null;
|
||||
|
||||
return (
|
||||
<div className={`policy-source-path ${className}`.trim()} aria-label={label}>
|
||||
<span className="policy-source-path-label">{label}</span>
|
||||
<ol>
|
||||
{cleanItems.map((item, index) => (
|
||||
<li key={`${item.label}-${index}`}>
|
||||
<span>{item.label}</span>
|
||||
{item.appliedFields.length > 0 && <small>{sourceFieldSummary(item.appliedFields)}</small>}
|
||||
</li>
|
||||
))}
|
||||
</ol>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function normalizeSourceItem(item: PolicySourcePathItem): NormalizedPolicySourcePathItem | null {
|
||||
if (typeof item === "string") {
|
||||
const label = item.trim();
|
||||
return label ? { label, appliedFields: [] } : null;
|
||||
}
|
||||
const label = item.label?.trim();
|
||||
if (!label) return null;
|
||||
const appliedFields = [...(item.applied_fields ?? item.appliedFields ?? [])].map((field) => field.trim()).filter(Boolean);
|
||||
return { label, appliedFields };
|
||||
}
|
||||
|
||||
function sourceFieldSummary(fields: string[]): string {
|
||||
const unique = Array.from(new Set(fields));
|
||||
if (unique.length === 0) return "";
|
||||
if (unique.length === 1 && unique[0] === "defaults") return "defaults";
|
||||
if (unique.length <= 3) return unique.map(sourceFieldLabel).join(", ");
|
||||
return `${unique.slice(0, 2).map(sourceFieldLabel).join(", ")} +${unique.length - 2}`;
|
||||
}
|
||||
|
||||
function sourceFieldLabel(field: string): string {
|
||||
if (field === "defaults") return "defaults";
|
||||
const clean = field
|
||||
.replace(/^whitelist[.]/, "allow ")
|
||||
.replace(/^blacklist[.]/, "block ")
|
||||
.replace(/[_.]/g, " ")
|
||||
.trim();
|
||||
return clean.charAt(0).toUpperCase() + clean.slice(1);
|
||||
}
|
||||
@@ -20,7 +20,7 @@ const TRIGGER_GAP = 10;
|
||||
|
||||
const tooltipBaseStyle: CSSProperties = {
|
||||
position: "fixed",
|
||||
zIndex: 10000,
|
||||
zIndex: 20000,
|
||||
width: "max-content",
|
||||
maxWidth: "min(320px, calc(100vw - 48px))",
|
||||
border: "1px solid var(--line-dark)",
|
||||
|
||||
@@ -2,6 +2,7 @@ import type { ReactNode } from "react";
|
||||
import Button from "../Button";
|
||||
import DismissibleAlert from "../DismissibleAlert";
|
||||
import FormField from "../FormField";
|
||||
import PasswordField from "../PasswordField";
|
||||
import ToggleSwitch from "../ToggleSwitch";
|
||||
|
||||
export type MailServerSecurity = "plain" | "tls" | "starttls" | string;
|
||||
@@ -50,10 +51,14 @@ export type MailServerSettingsPanelProps = {
|
||||
onImapEnabledChange?: (enabled: boolean) => void;
|
||||
smtpDisabled?: boolean;
|
||||
smtpCredentialDisabled?: boolean;
|
||||
smtpPasswordSaved?: boolean;
|
||||
smtpSavedPasswordPlaceholder?: string;
|
||||
smtpActionDisabled?: boolean;
|
||||
imapToggleDisabled?: boolean;
|
||||
imapServerDisabled?: boolean;
|
||||
imapCredentialDisabled?: boolean;
|
||||
imapPasswordSaved?: boolean;
|
||||
imapSavedPasswordPlaceholder?: string;
|
||||
imapActionDisabled?: boolean;
|
||||
smtpTitle?: ReactNode;
|
||||
imapTitle?: ReactNode;
|
||||
@@ -98,10 +103,14 @@ export default function MailServerSettingsPanel({
|
||||
onImapEnabledChange,
|
||||
smtpDisabled = false,
|
||||
smtpCredentialDisabled = smtpDisabled,
|
||||
smtpPasswordSaved = false,
|
||||
smtpSavedPasswordPlaceholder = "••••••••",
|
||||
smtpActionDisabled = smtpDisabled,
|
||||
imapToggleDisabled = false,
|
||||
imapServerDisabled = false,
|
||||
imapCredentialDisabled = imapServerDisabled,
|
||||
imapPasswordSaved = false,
|
||||
imapSavedPasswordPlaceholder = "••••••••",
|
||||
imapActionDisabled = imapServerDisabled,
|
||||
smtpTitle = "SMTP login",
|
||||
imapTitle = "IMAP sent-folder append",
|
||||
@@ -149,7 +158,16 @@ export default function MailServerSettingsPanel({
|
||||
<FormField label="Host"><input value={stringValue(smtp.host)} disabled={smtpFieldsDisabled} onChange={(event) => onSmtpChange({ host: event.target.value })} /></FormField>
|
||||
<FormField label="Port"><input type="number" min={1} max={65535} value={stringValue(smtp.port)} disabled={smtpFieldsDisabled} onChange={(event) => onSmtpChange({ port: event.target.value })} /></FormField>
|
||||
<FormField label="Username"><input value={stringValue(smtp.username)} disabled={smtpCredentialFieldsDisabled} onChange={(event) => onSmtpChange({ username: event.target.value })} /></FormField>
|
||||
<FormField label="Password"><input type="password" value={stringValue(smtp.password)} disabled={smtpCredentialFieldsDisabled} onChange={(event) => onSmtpChange({ password: event.target.value })} /></FormField>
|
||||
<FormField label="Password">
|
||||
<PasswordField
|
||||
value={stringValue(smtp.password)}
|
||||
disabled={smtpCredentialFieldsDisabled}
|
||||
saved={smtpPasswordSaved}
|
||||
savedPlaceholder={smtpSavedPasswordPlaceholder}
|
||||
autoComplete="new-password"
|
||||
onValueChange={(password) => onSmtpChange({ password })}
|
||||
/>
|
||||
</FormField>
|
||||
<FormField label="Security"><SecuritySelect value={stringValue(smtp.security, "starttls")} disabled={smtpFieldsDisabled} onChange={(security) => onSmtpChange({ security })} /></FormField>
|
||||
<FormField label="Timeout seconds"><input type="number" min={1} value={stringValue(smtp.timeout_seconds)} disabled={smtpFieldsDisabled} onChange={(event) => onSmtpChange({ timeout_seconds: event.target.value })} /></FormField>
|
||||
</div>
|
||||
@@ -174,7 +192,16 @@ export default function MailServerSettingsPanel({
|
||||
<FormField label="Host"><input value={stringValue(imap.host)} disabled={imapFieldsDisabled} onChange={(event) => onImapChange({ host: event.target.value })} /></FormField>
|
||||
<FormField label="Port"><input type="number" min={1} max={65535} value={stringValue(imap.port)} disabled={imapFieldsDisabled} onChange={(event) => onImapChange({ port: event.target.value })} /></FormField>
|
||||
<FormField label="Username"><input value={stringValue(imap.username)} disabled={imapCredentialFieldsDisabled} onChange={(event) => onImapChange({ username: event.target.value })} /></FormField>
|
||||
<FormField label="Password"><input type="password" value={stringValue(imap.password)} disabled={imapCredentialFieldsDisabled} onChange={(event) => onImapChange({ password: event.target.value })} /></FormField>
|
||||
<FormField label="Password">
|
||||
<PasswordField
|
||||
value={stringValue(imap.password)}
|
||||
disabled={imapCredentialFieldsDisabled}
|
||||
saved={imapPasswordSaved}
|
||||
savedPlaceholder={imapSavedPasswordPlaceholder}
|
||||
autoComplete="new-password"
|
||||
onValueChange={(password) => onImapChange({ password })}
|
||||
/>
|
||||
</FormField>
|
||||
<FormField label="Security"><SecuritySelect value={stringValue(imap.security, "tls")} disabled={imapFieldsDisabled} onChange={(security) => onImapChange({ security })} /></FormField>
|
||||
<FormField label="Detected/saved sent folder"><input value={stringValue(imap.sent_folder, "auto")} disabled={imapFieldsDisabled} onChange={(event) => onImapChange({ sent_folder: event.target.value })} /></FormField>
|
||||
<FormField label="Timeout seconds"><input type="number" min={1} value={stringValue(imap.timeout_seconds)} disabled={imapFieldsDisabled} onChange={(event) => onImapChange({ timeout_seconds: event.target.value })} /></FormField>
|
||||
|
||||
Reference in New Issue
Block a user