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() { 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 ( return (
<div className="page"> <div className="page">
<div className="page-header"> <div className="page-header">
@ -621,72 +652,56 @@ function Settings() {
<span className="subtitle">System configuration · changes apply without restart</span> <span className="subtitle">System configuration · changes apply without restart</span>
</div> </div>
<div className="page-body"> <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"> <nav className="settings-nav">
{[ {[
{ id: "storage", label: "S3 / Object storage", icon: "hdd" }, { id: 'storage', label: 'S3 / Object storage', icon: 'hdd' },
{ id: "gpu", label: "GPU / Transcoding", icon: "gpu" }, { id: 'gpu', label: 'GPU / Transcoding', icon: 'gpu' },
{ id: "sdi", label: "SDI capture", icon: "video" }, { id: 'sdi', label: 'SDI capture', icon: 'video' },
{ id: "ampp", label: "AMPP integration", icon: "link" }, { id: 'ampp', label: 'AMPP integration', icon: 'link' },
{ id: "branding", label: "Branding", icon: "image" },
{ id: "logs", label: "Logs & telemetry", icon: "list" },
].map((s, i) => ( ].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} <Icon name={s.icon} size={14} />{s.label}
</a> </a>
))} ))}
</nav> </nav>
<div style={{ display: "flex", flexDirection: "column", gap: 16 }}> <div style={{ display: 'flex', flexDirection: 'column', gap: 16 }}>
<SettingsCard <SettingsCard
icon="hdd" icon="hdd"
title="S3 / Object Storage" title="S3 / Object Storage"
sub="S3-compatible bucket for media asset 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 /> {s3Loading ? (
<div style={{ display: "grid", gridTemplateColumns: "1fr 1fr", gap: 12 }}> <div style={{ color: 'var(--text-3)', fontSize: 12.5 }}>Loading</div>
<Field label="Region" value="us-east-1" mono /> ) : (<>
<Field label="Bucket" value="dragonmam" mono /> <SField label="Endpoint URL">
</div> <input className="field-input mono" value={s3.s3_endpoint} onChange={e => setS3(p => ({...p, s3_endpoint: e.target.value}))} placeholder="https://s3.example.com" />
<Field label="Access key ID" value="boLH2fUE7D6tzmgHvLb…" mono /> </SField>
<Field label="Secret access key" value="••••••••••••••••" mono right={<button className="btn ghost sm">Show</button>} /> <div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 12 }}>
<div style={{ display: "flex", gap: 6, marginTop: 8 }}> <SField label="Region">
<button className="btn primary sm">Save &amp; apply</button> <input className="field-input mono" value={s3.s3_region} onChange={e => setS3(p => ({...p, s3_region: e.target.value}))} placeholder="us-east-1" />
<button className="btn ghost sm">Test connection</button> </SField>
</div> <SField label="Bucket">
</SettingsCard> <input className="field-input mono" value={s3.s3_bucket} onChange={e => setS3(p => ({...p, s3_bucket: e.target.value}))} placeholder="my-bucket" />
<SettingsCard </SField>
icon="gpu" </div>
title="GPU / Transcoding" <SField label="Access key ID">
sub="NVIDIA NVENC acceleration for proxy generation and transcoding jobs" <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" />
tag={<span className="badge accent">GPUs available</span>} </SField>
> <SField label="Secret access key">
<label className="checkbox-row"> <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'} />
<input type="checkbox" defaultChecked /> Enable GPU-accelerated transcoding </SField>
</label> {s3Msg && (
<div style={{ display: "grid", gridTemplateColumns: "1fr 1fr", gap: 12 }}> <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)' }}>
<Field label="Encoder" value="H.264 (h264_nvenc)" select /> {s3Msg.text}
<Field label="Quality preset" value="p4 — medium (default)" select /> </div>
</div> )}
<div style={{ display: "grid", gridTemplateColumns: "1fr 1fr", gap: 12 }}> <div style={{ display: 'flex', gap: 6, marginTop: 8 }}>
<Field label="Proxy bitrate" value="2 Mbps" mono /> <button className="btn primary sm" onClick={saveS3} disabled={s3Saving}>{s3Saving ? 'Saving…' : 'Save & apply'}</button>
<Field label="Transcoding node" value="Auto (first online with GPU)" select /> <button className="btn ghost sm" onClick={testS3} disabled={s3Testing}>{s3Testing ? 'Testing…' : 'Test connection'}</button>
</div> </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>
</SettingsCard> </SettingsCard>
</div> </div>
</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 }) { function SettingsCard({ icon, title, sub, tag, children }) {
return ( return (
<div className="settings-card"> <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 }); Object.assign(window, { Users, Tokens, Containers, Cluster, Settings });