FP
This commit is contained in:
161
packages/web/src/components/AgentSettings.tsx
Normal file
161
packages/web/src/components/AgentSettings.tsx
Normal file
@@ -0,0 +1,161 @@
|
||||
import { useState } from 'react';
|
||||
import type { AgentAction, AgentView } from '@stream-control/shared';
|
||||
import { api } from '../api';
|
||||
|
||||
interface Props {
|
||||
agent: AgentView;
|
||||
onClose: () => void;
|
||||
onCommand: (id: string, action: AgentAction, params?: Record<string, unknown>) => Promise<void>;
|
||||
notify: (message: string, tone?: 'info' | 'error') => void;
|
||||
}
|
||||
|
||||
export function AgentSettings({ agent, onClose, onCommand, notify }: Props) {
|
||||
const [name, setName] = useState(agent.name);
|
||||
const [host, setHost] = useState(agent.obs.host);
|
||||
const [port, setPort] = useState(String(agent.obs.port));
|
||||
const [password, setPassword] = useState(agent.obs.password);
|
||||
const [autoConnect, setAutoConnect] = useState(agent.autoConnectObs);
|
||||
const [notes, setNotes] = useState(agent.notes ?? '');
|
||||
const [directory, setDirectory] = useState(agent.status.recordDirectory ?? '');
|
||||
const [token, setToken] = useState<string | null>(null);
|
||||
const [busy, setBusy] = useState(false);
|
||||
|
||||
async function save() {
|
||||
setBusy(true);
|
||||
try {
|
||||
await api.updateAgent(agent.id, {
|
||||
name,
|
||||
notes,
|
||||
autoConnectObs: autoConnect,
|
||||
obs: { host, port: Number(port), password },
|
||||
} as Partial<AgentView>);
|
||||
notify('Configuration enregistrée');
|
||||
onClose();
|
||||
} catch (err) {
|
||||
notify(err instanceof Error ? err.message : 'Enregistrement impossible', 'error');
|
||||
} finally {
|
||||
setBusy(false);
|
||||
}
|
||||
}
|
||||
|
||||
async function applyDirectory() {
|
||||
if (!directory.trim()) return;
|
||||
await onCommand(agent.id, 'recordDirectory.set', { directory: directory.trim() });
|
||||
}
|
||||
|
||||
async function rotate() {
|
||||
if (!confirm("Régénérer le jeton ? L'agent sera déconnecté jusqu'à sa reconfiguration.")) return;
|
||||
try {
|
||||
const result = await api.rotateToken(agent.id);
|
||||
setToken(result.token);
|
||||
} catch (err) {
|
||||
notify(err instanceof Error ? err.message : 'Rotation impossible', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
async function remove() {
|
||||
if (!confirm(`Supprimer définitivement l'agent « ${agent.name} » ?`)) return;
|
||||
try {
|
||||
await api.deleteAgent(agent.id);
|
||||
notify('Agent supprimé');
|
||||
onClose();
|
||||
} catch (err) {
|
||||
notify(err instanceof Error ? err.message : 'Suppression impossible', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="modal-backdrop" onClick={onClose}>
|
||||
<div className="modal" onClick={(event) => event.stopPropagation()}>
|
||||
<header className="modal-head">
|
||||
<h2>Configuration · {agent.name}</h2>
|
||||
<button className="icon" onClick={onClose}>
|
||||
✕
|
||||
</button>
|
||||
</header>
|
||||
|
||||
<div className="modal-body">
|
||||
<label className="field">
|
||||
<span>Nom affiché</span>
|
||||
<input value={name} onChange={(event) => setName(event.target.value)} />
|
||||
</label>
|
||||
|
||||
<div className="row">
|
||||
<label className="field grow">
|
||||
<span>Hôte obs-websocket</span>
|
||||
<input value={host} onChange={(event) => setHost(event.target.value)} />
|
||||
</label>
|
||||
<label className="field small-field">
|
||||
<span>Port</span>
|
||||
<input value={port} onChange={(event) => setPort(event.target.value)} inputMode="numeric" />
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<label className="field">
|
||||
<span>Mot de passe obs-websocket</span>
|
||||
<input
|
||||
type="password"
|
||||
value={password}
|
||||
onChange={(event) => setPassword(event.target.value)}
|
||||
placeholder="inchangé"
|
||||
/>
|
||||
</label>
|
||||
|
||||
<label className="checkbox">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={autoConnect}
|
||||
onChange={(event) => setAutoConnect(event.target.checked)}
|
||||
/>
|
||||
<span>Connecter OBS automatiquement au démarrage de l'agent</span>
|
||||
</label>
|
||||
|
||||
<label className="field">
|
||||
<span>Notes</span>
|
||||
<textarea rows={2} value={notes} onChange={(event) => setNotes(event.target.value)} />
|
||||
</label>
|
||||
|
||||
<div className="row">
|
||||
<label className="field grow">
|
||||
<span>Dossier d'enregistrement OBS</span>
|
||||
<input
|
||||
value={directory}
|
||||
onChange={(event) => setDirectory(event.target.value)}
|
||||
placeholder="D:\\records ou /srv/records"
|
||||
/>
|
||||
</label>
|
||||
<button className="ghost align-end" onClick={() => void applyDirectory()}>
|
||||
Appliquer
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="danger-zone">
|
||||
<div>
|
||||
<strong>Jeton d'agent</strong>
|
||||
<p className="muted small">
|
||||
Affiché une seule fois. À reporter dans le fichier <code>agent.config.json</code> de la VM.
|
||||
</p>
|
||||
{token && <code className="token">{token}</code>}
|
||||
</div>
|
||||
<button className="ghost" onClick={() => void rotate()}>
|
||||
Régénérer
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<footer className="modal-foot">
|
||||
<button className="danger ghost" onClick={() => void remove()}>
|
||||
Supprimer l'agent
|
||||
</button>
|
||||
<div className="spacer" />
|
||||
<button className="ghost" onClick={onClose}>
|
||||
Annuler
|
||||
</button>
|
||||
<button className="primary" disabled={busy} onClick={() => void save()}>
|
||||
Enregistrer
|
||||
</button>
|
||||
</footer>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user