fix: make Settings S3 form functional — load from API, save & test
This commit is contained in:
parent
665ab5238d
commit
dc269bec00
1 changed files with 78 additions and 73 deletions
|
|
@ -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 & 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 });
|
||||
|
|
|
|||
Loading…
Reference in a new issue