fix: make Settings S3 form functional — load from API, save & test

This commit is contained in:
Zac Gaetano 2026-05-22 12:08:10 -04:00
parent 665ab5238d
commit dc269bec00

View file

@ -614,6 +614,37 @@ function DetailRow({ k, v, mono }) {
}
function Settings() {
const [s3, setS3] = React.useState({ s3_endpoint: '', s3_bucket: '', s3_access_key: '', s3_secret_key: '', s3_region: 'us-east-1' });
const [s3Loading, setS3Loading] = React.useState(true);
const [s3Saving, setS3Saving] = React.useState(false);
const [s3Testing, setS3Testing] = React.useState(false);
const [s3Msg, setS3Msg] = React.useState(null);
const [secretExists, setSecretExists] = React.useState(false);
React.useEffect(() => {
window.ZAMPP_API.fetch('/settings/s3')
.then(data => {
setS3({ s3_endpoint: data.s3_endpoint || '', s3_bucket: data.s3_bucket || '', s3_access_key: data.s3_access_key || '', s3_secret_key: '', s3_region: data.s3_region || 'us-east-1' });
setSecretExists(!!data.s3_secret_key_exists);
setS3Loading(false);
})
.catch(() => setS3Loading(false));
}, []);
const saveS3 = () => {
setS3Saving(true); setS3Msg(null);
window.ZAMPP_API.fetch('/settings/s3', { method: 'PUT', body: JSON.stringify(s3) })
.then(() => { setS3Saving(false); setS3Msg({ ok: true, text: 'Saved and applied.' }); if (s3.s3_secret_key) setSecretExists(true); })
.catch(e => { setS3Saving(false); setS3Msg({ ok: false, text: e.message }); });
};
const testS3 = () => {
setS3Testing(true); setS3Msg(null);
window.ZAMPP_API.fetch('/settings/s3/test', { method: 'POST', body: JSON.stringify(s3) })
.then(r => { setS3Testing(false); setS3Msg({ ok: r.ok !== false, text: r.message || 'Connection OK' }); })
.catch(e => { setS3Testing(false); setS3Msg({ ok: false, text: e.message }); });
};
return (
<div className="page">
<div className="page-header">
@ -621,72 +652,56 @@ function Settings() {
<span className="subtitle">System configuration · changes apply without restart</span>
</div>
<div className="page-body">
<div style={{ display: "grid", gridTemplateColumns: "200px 1fr", gap: 24, alignItems: "start" }}>
<div style={{ display: 'grid', gridTemplateColumns: '200px 1fr', gap: 24, alignItems: 'start' }}>
<nav className="settings-nav">
{[
{ id: "storage", label: "S3 / Object storage", icon: "hdd" },
{ id: "gpu", label: "GPU / Transcoding", icon: "gpu" },
{ id: "sdi", label: "SDI capture", icon: "video" },
{ id: "ampp", label: "AMPP integration", icon: "link" },
{ id: "branding", label: "Branding", icon: "image" },
{ id: "logs", label: "Logs & telemetry", icon: "list" },
{ id: 'storage', label: 'S3 / Object storage', icon: 'hdd' },
{ id: 'gpu', label: 'GPU / Transcoding', icon: 'gpu' },
{ id: 'sdi', label: 'SDI capture', icon: 'video' },
{ id: 'ampp', label: 'AMPP integration', icon: 'link' },
].map((s, i) => (
<a key={s.id} className={`settings-nav-item ${i === 0 ? "active" : ""}`}>
<a key={s.id} className={`settings-nav-item ${i === 0 ? 'active' : ''}`}>
<Icon name={s.icon} size={14} />{s.label}
</a>
))}
</nav>
<div style={{ display: "flex", flexDirection: "column", gap: 16 }}>
<div style={{ display: 'flex', flexDirection: 'column', gap: 16 }}>
<SettingsCard
icon="hdd"
title="S3 / Object Storage"
sub="S3-compatible bucket for media asset storage"
tag={<span className="badge success">connected</span>}
tag={secretExists ? <span className="badge success">connected</span> : <span className="badge warning">not configured</span>}
>
<Field label="Endpoint URL" value="https://broadcastmgmt.wilddragon.net" mono />
<div style={{ display: "grid", gridTemplateColumns: "1fr 1fr", gap: 12 }}>
<Field label="Region" value="us-east-1" mono />
<Field label="Bucket" value="dragonmam" mono />
</div>
<Field label="Access key ID" value="boLH2fUE7D6tzmgHvLb…" mono />
<Field label="Secret access key" value="••••••••••••••••" mono right={<button className="btn ghost sm">Show</button>} />
<div style={{ display: "flex", gap: 6, marginTop: 8 }}>
<button className="btn primary sm">Save &amp; apply</button>
<button className="btn ghost sm">Test connection</button>
</div>
</SettingsCard>
<SettingsCard
icon="gpu"
title="GPU / Transcoding"
sub="NVIDIA NVENC acceleration for proxy generation and transcoding jobs"
tag={<span className="badge accent">GPUs available</span>}
>
<label className="checkbox-row">
<input type="checkbox" defaultChecked /> Enable GPU-accelerated transcoding
</label>
<div style={{ display: "grid", gridTemplateColumns: "1fr 1fr", gap: 12 }}>
<Field label="Encoder" value="H.264 (h264_nvenc)" select />
<Field label="Quality preset" value="p4 — medium (default)" select />
</div>
<div style={{ display: "grid", gridTemplateColumns: "1fr 1fr", gap: 12 }}>
<Field label="Proxy bitrate" value="2 Mbps" mono />
<Field label="Transcoding node" value="Auto (first online with GPU)" select />
</div>
<div style={{ display: "flex", gap: 6, marginTop: 8 }}>
<button className="btn primary sm">Save GPU settings</button>
</div>
</SettingsCard>
<SettingsCard
icon="link"
title="AMPP Integration"
sub="Grass Valley AMPP platform connectivity for asset sync"
tag={<span className="badge warning">setup needed</span>}
>
<Field label="AMPP base URL" value="https://ampp.example.com" mono />
<Field label="Tenant ID" value="" placeholder="e.g. wild-dragon-prod" />
<div style={{ display: "flex", gap: 6, marginTop: 8 }}>
<button className="btn primary sm">Connect</button>
</div>
{s3Loading ? (
<div style={{ color: 'var(--text-3)', fontSize: 12.5 }}>Loading</div>
) : (<>
<SField label="Endpoint URL">
<input className="field-input mono" value={s3.s3_endpoint} onChange={e => setS3(p => ({...p, s3_endpoint: e.target.value}))} placeholder="https://s3.example.com" />
</SField>
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 12 }}>
<SField label="Region">
<input className="field-input mono" value={s3.s3_region} onChange={e => setS3(p => ({...p, s3_region: e.target.value}))} placeholder="us-east-1" />
</SField>
<SField label="Bucket">
<input className="field-input mono" value={s3.s3_bucket} onChange={e => setS3(p => ({...p, s3_bucket: e.target.value}))} placeholder="my-bucket" />
</SField>
</div>
<SField label="Access key ID">
<input className="field-input mono" value={s3.s3_access_key} onChange={e => setS3(p => ({...p, s3_access_key: e.target.value}))} placeholder="Access key ID" />
</SField>
<SField label="Secret access key">
<input className="field-input mono" type="password" value={s3.s3_secret_key} onChange={e => setS3(p => ({...p, s3_secret_key: e.target.value}))} placeholder={secretExists ? '(saved — type to replace)' : 'Secret key'} />
</SField>
{s3Msg && (
<div style={{ fontSize: 12, padding: '6px 10px', borderRadius: 5, border: '1px solid', background: s3Msg.ok ? 'var(--success-soft)' : 'var(--danger-soft)', borderColor: s3Msg.ok ? 'var(--success)' : 'var(--danger)', color: s3Msg.ok ? 'var(--success)' : 'var(--danger)' }}>
{s3Msg.text}
</div>
)}
<div style={{ display: 'flex', gap: 6, marginTop: 8 }}>
<button className="btn primary sm" onClick={saveS3} disabled={s3Saving}>{s3Saving ? 'Saving…' : 'Save & apply'}</button>
<button className="btn ghost sm" onClick={testS3} disabled={s3Testing}>{s3Testing ? 'Testing…' : 'Test connection'}</button>
</div>
</>)}
</SettingsCard>
</div>
</div>
@ -695,6 +710,15 @@ function Settings() {
);
}
function SField({ label, children }) {
return (
<div className="field">
<label className="field-label">{label}</label>
{children}
</div>
);
}
function SettingsCard({ icon, title, sub, tag, children }) {
return (
<div className="settings-card">
@ -711,23 +735,4 @@ function SettingsCard({ icon, title, sub, tag, children }) {
);
}
function Field({ label, value, mono, select, placeholder, right }) {
return (
<div className="field">
<label className="field-label">{label}</label>
<div className="field-input-wrap">
{select ? (
<div className="field-input select">
<span className={mono ? "mono" : ""}>{value || placeholder}</span>
<Icon name="chevronDown" size={12} style={{ color: "var(--text-3)" }} />
</div>
) : (
<input className={`field-input ${mono ? "mono" : ""}`} defaultValue={value} placeholder={placeholder} />
)}
{right}
</div>
</div>
);
}
Object.assign(window, { Users, Tokens, Containers, Cluster, Settings });