Files
Magent/frontend/app/admin/SettingField.tsx
T
Assclaw f852e7c941
Magent CI/CD / verify (push) Failing after 9m34s
Magent CI/CD / deploy-beta (push) Skipped
chore: standardize security and quality foundations
2026-09-17 20:03:47 +12:00

163 lines
5.7 KiB
TypeScript

"use client";
export type AdminSetting = { key: string; value: string | null; isSet: boolean; source: string; sensitive: boolean };
type Option = { value: string; label: string };
type Props = {
setting: AdminSetting;
label: string;
value: string;
help?: string;
placeholder?: string;
boolean?: boolean;
numeric?: boolean;
multiline?: boolean;
options?: Option[];
optionsUnavailable?: boolean;
onChange: (value: string) => void;
};
const SELECTS: Record<string, Option[]> = {
log_level: ["DEBUG", "INFO", "WARNING", "ERROR"].map((value) => ({ value, label: value })),
issue_confirmation_contact_attempts: Array.from({ length: 11 }, (_, index) => ({
value: String(index),
label: index === 0 ? "None — close when fixed" : String(index),
})),
issue_confirmation_interval_unit: ["days", "weeks", "months"].map((value) => ({
value,
label: value[0].toUpperCase() + value.slice(1),
})),
artwork_cache_mode: [
{ value: "remote", label: "Load from the internet" },
{ value: "cache", label: "Store locally" },
],
site_banner_tone: ["info", "warning", "error", "maintenance"].map((value) => ({
value,
label: value[0].toUpperCase() + value.slice(1),
})),
magent_notify_push_provider: ["ntfy", "gotify", "pushover", "webhook", "telegram", "discord"].map((value) => ({
value,
label: value,
})),
requests_data_source: [
{ value: "always_js", label: "Read directly from Seerr" },
{ value: "prefer_cache", label: "Use saved requests" },
],
};
const COLOR_DEFAULTS: Record<string, string> = {
site_banner_background_color: "#332814",
site_banner_border_color: "#a27b32",
};
export default function SettingField(props: Props) {
const { setting, label, value, help, placeholder, onChange } = props;
const id = `setting-${setting.key}`;
const options =
props.options ??
SELECTS[setting.key] ??
(setting.key === "log_http_client_level" || setting.key === "log_background_sync_level"
? SELECTS.log_level
: undefined);
const selectedOptions =
options && value && !options.some((option) => option.value === value)
? [{ value, label: `Current selection (${value})` }, ...options]
: options;
const isTime = setting.key === "requests_full_sync_time" || setting.key === "requests_cleanup_time";
const zeroAllowed = setting.key === "log_file_backup_count";
const minimum = zeroAllowed ? 0 : 1;
const maximum =
setting.key === "issue_confirmation_interval_value" ? 365 : setting.key.endsWith("_port") ? 65535 : undefined;
const colorDefault = COLOR_DEFAULTS[setting.key];
const pickerValue = /^#[0-9a-f]{6}$/i.test(value) ? value : colorDefault;
const aria = { id, name: setting.key, "aria-describedby": help ? `${id}-help` : undefined };
if (props.boolean) {
return (
<div className="setting-field setting-switch">
<div>
<label htmlFor={id}>{label}</label>
{help && <p id={`${id}-help`}>{help}</p>}
</div>
<input
{...aria}
type="checkbox"
role="switch"
aria-checked={value.toLowerCase() === "true"}
checked={value.toLowerCase() === "true"}
onChange={(event) => onChange(String(event.target.checked))}
/>
</div>
);
}
return (
<div className={`setting-field ${props.multiline ? "field-span-full" : ""}`}>
<label htmlFor={id}>
{label}
{setting.sensitive && setting.isSet && <small>Saved</small>}
</label>
{props.optionsUnavailable ? (
<select {...aria} disabled value={value}>
<option value={value}>Save the connection, then reload available options</option>
</select>
) : colorDefault ? (
<div className="setting-color-control">
<input
id={`${id}-picker`}
type="color"
aria-label={`Choose ${label.toLowerCase()}`}
value={pickerValue}
onChange={(event) => onChange(event.target.value)}
/>
<input
{...aria}
type="text"
value={value}
pattern="#[0-9A-Fa-f]{6}"
placeholder={colorDefault}
autoComplete="off"
spellCheck={false}
onChange={(event) => onChange(event.target.value)}
/>
{value ? (
<button type="button" className="ghost-button" onClick={() => onChange("")}>
Use tone default
</button>
) : null}
</div>
) : selectedOptions ? (
<select {...aria} value={value} onChange={(event) => onChange(event.target.value)}>
{!value && <option value="">Choose an option</option>}
{selectedOptions.map((option) => (
<option key={option.value} value={option.value}>
{option.label}
</option>
))}
</select>
) : props.multiline ? (
<textarea
{...aria}
rows={setting.key.includes("_pem") ? 6 : 3}
value={value}
placeholder={placeholder}
onChange={(event) => onChange(event.target.value)}
/>
) : (
<input
{...aria}
type={setting.sensitive ? "password" : props.numeric ? "number" : isTime ? "time" : "text"}
value={value}
min={props.numeric ? minimum : undefined}
max={props.numeric ? maximum : undefined}
step={props.numeric ? 1 : undefined}
autoComplete={setting.sensitive ? "new-password" : "off"}
spellCheck={false}
placeholder={setting.sensitive && setting.isSet ? "Leave blank to keep the saved value" : placeholder}
onChange={(event) => onChange(event.target.value)}
/>
)}
{help && <p id={`${id}-help`}>{help}</p>}
</div>
);
}