feat: amélioration UX interface admin audio

- Admin : regroupement des 3 dropdowns cartes son dans une seule section
- Admin : suppression du mode édition pour noms de canaux (directement éditables)
- Admin : unification des boutons de sauvegarde en bas de chaque section
- Admin : routing par hash URL pour persistance des onglets (#groups, #audio, etc.)
- AudioRoutingMatrix : bouton sauvegarde déplacé en bas de la matrice
- AudioRoutingMatrix : dropdowns de gain en nuance de bleu (cohérence visuelle)
This commit is contained in:
2026-06-01 23:04:57 +02:00
parent 58bc91b966
commit 77bc36b765
3 changed files with 145 additions and 125 deletions
+52 -41
View File
@@ -5,7 +5,13 @@ import AudioRoutingMatrix from './components/AudioRoutingMatrix';
const API_URL = import.meta.env.VITE_API_URL || '/api'; const API_URL = import.meta.env.VITE_API_URL || '/api';
function Admin() { function Admin() {
const [activeTab, setActiveTab] = useState('groups'); // Lire l'onglet depuis l'URL hash (ex: #audio) ou utiliser 'groups' par défaut
const getInitialTab = () => {
const hash = window.location.hash.slice(1); // Enlever le #
return ['groups', 'audio', 'users', 'stats', 'logs'].includes(hash) ? hash : 'groups';
};
const [activeTab, setActiveTab] = useState(getInitialTab());
const [groups, setGroups] = useState([]); const [groups, setGroups] = useState([]);
const [users, setUsers] = useState([]); const [users, setUsers] = useState([]);
const [stats, setStats] = useState(null); const [stats, setStats] = useState(null);
@@ -23,8 +29,6 @@ function Admin() {
// Channel names (Phase 2.5) // Channel names (Phase 2.5)
const [channelNames, setChannelNames] = useState({ inputs: {}, outputs: {} }); const [channelNames, setChannelNames] = useState({ inputs: {}, outputs: {} });
const editingChannelNamesRef = useRef(false);
const [, forceUpdate] = useState({});
// Gestion formulaire nouveau groupe // Gestion formulaire nouveau groupe
const [showGroupForm, setShowGroupForm] = useState(false); const [showGroupForm, setShowGroupForm] = useState(false);
@@ -34,6 +38,19 @@ function Admin() {
audioBitrate: 96 audioBitrate: 96
}); });
// Synchroniser l'onglet avec l'URL hash
useEffect(() => {
const handleHashChange = () => {
const hash = window.location.hash.slice(1);
if (['groups', 'audio', 'users', 'stats', 'logs'].includes(hash)) {
setActiveTab(hash);
}
};
window.addEventListener('hashchange', handleHashChange);
return () => window.removeEventListener('hashchange', handleHashChange);
}, []);
// Rafraîchissement automatique // Rafraîchissement automatique
useEffect(() => { useEffect(() => {
loadData(); loadData();
@@ -105,11 +122,7 @@ function Admin() {
const device = currentData.device || { inputChannels: 8, outputChannels: 8 }; const device = currentData.device || { inputChannels: 8, outputChannels: 8 };
setCurrentDevice(device); setCurrentDevice(device);
// Ne pas écraser les noms de canaux pendant l'édition
if (!editingChannelNamesRef.current) {
setChannelNames(channelNamesData.channelNames || { inputs: {}, outputs: {} }); setChannelNames(channelNamesData.channelNames || { inputs: {}, outputs: {} });
}
// Ne réinitialiser les sélections que lors du chargement initial (pas en train d'éditer) // Ne réinitialiser les sélections que lors du chargement initial (pas en train d'éditer)
if (!isEditingAudioRef.current) { if (!isEditingAudioRef.current) {
@@ -221,8 +234,6 @@ function Admin() {
if (res.ok) { if (res.ok) {
alert('Noms de canaux sauvegardés avec succès!'); alert('Noms de canaux sauvegardés avec succès!');
editingChannelNamesRef.current = false;
forceUpdate({});
await loadAudioDevices(); await loadAudioDevices();
} else { } else {
const error = await res.json(); const error = await res.json();
@@ -317,31 +328,31 @@ function Admin() {
<nav className="admin-tabs"> <nav className="admin-tabs">
<button <button
className={activeTab === 'groups' ? 'active' : ''} className={activeTab === 'groups' ? 'active' : ''}
onClick={() => setActiveTab('groups')} onClick={() => { window.location.hash = 'groups'; setActiveTab('groups'); }}
> >
Groupes Groupes
</button> </button>
<button <button
className={activeTab === 'audio' ? 'active' : ''} className={activeTab === 'audio' ? 'active' : ''}
onClick={() => setActiveTab('audio')} onClick={() => { window.location.hash = 'audio'; setActiveTab('audio'); }}
> >
Audio Audio
</button> </button>
<button <button
className={activeTab === 'users' ? 'active' : ''} className={activeTab === 'users' ? 'active' : ''}
onClick={() => setActiveTab('users')} onClick={() => { window.location.hash = 'users'; setActiveTab('users'); }}
> >
Utilisateurs ({users.length}) Utilisateurs ({users.length})
</button> </button>
<button <button
className={activeTab === 'stats' ? 'active' : ''} className={activeTab === 'stats' ? 'active' : ''}
onClick={() => setActiveTab('stats')} onClick={() => { window.location.hash = 'stats'; setActiveTab('stats'); }}
> >
Statistiques Statistiques
</button> </button>
<button <button
className={activeTab === 'logs' ? 'active' : ''} className={activeTab === 'logs' ? 'active' : ''}
onClick={() => setActiveTab('logs')} onClick={() => { window.location.hash = 'logs'; setActiveTab('logs'); }}
> >
Logs Logs
</button> </button>
@@ -444,7 +455,13 @@ function Admin() {
<div className="audio-config-container"> <div className="audio-config-container">
<div className="audio-section"> <div className="audio-section">
<h3>Carte son d'entrée (Input)</h3> <h3>Configuration des cartes son</h3>
<div style={{display: 'grid', gap: 'var(--spacing-lg)', marginTop: 'var(--spacing-md)'}}>
<div>
<label style={{display: 'block', marginBottom: 'var(--spacing-xs)', fontSize: '0.9rem', fontWeight: 600, color: 'var(--color-text-secondary)'}}>
Carte son d'entrée (Input)
</label>
<select <select
value={selectedInputDevice ?? ''} value={selectedInputDevice ?? ''}
onChange={(e) => { onChange={(e) => {
@@ -469,8 +486,10 @@ function Admin() {
)} )}
</div> </div>
<div className="audio-section"> <div>
<h3>Carte son de sortie (Output)</h3> <label style={{display: 'block', marginBottom: 'var(--spacing-xs)', fontSize: '0.9rem', fontWeight: 600, color: 'var(--color-text-secondary)'}}>
Carte son de sortie (Output)
</label>
<select <select
value={selectedOutputDevice ?? ''} value={selectedOutputDevice ?? ''}
onChange={(e) => { onChange={(e) => {
@@ -495,8 +514,10 @@ function Admin() {
)} )}
</div> </div>
<div className="audio-section"> <div>
<h3>Sample Rate</h3> <label style={{display: 'block', marginBottom: 'var(--spacing-xs)', fontSize: '0.9rem', fontWeight: 600, color: 'var(--color-text-secondary)'}}>
Sample Rate
</label>
<select <select
value={selectedSampleRate} value={selectedSampleRate}
onChange={(e) => { onChange={(e) => {
@@ -510,33 +531,19 @@ function Admin() {
<option value={96000}>96000 Hz (High quality)</option> <option value={96000}>96000 Hz (High quality)</option>
</select> </select>
</div> </div>
</div>
<div className="audio-actions"> <div className="audio-actions">
<button onClick={handleSaveAudioDevice} className="btn-primary"> <button onClick={handleSaveAudioDevice} className="btn-primary">
Sauvegarder la configuration Sauvegarder la configuration audio
</button> </button>
</div> </div>
</div>
<div className="audio-section"> <div className="audio-section">
<div style={{display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 'var(--spacing-md)'}}>
<h3>Nommage des canaux physiques</h3> <h3>Nommage des canaux physiques</h3>
{!editingChannelNamesRef.current ? (
<button onClick={() => { editingChannelNamesRef.current = true; forceUpdate({}); }} className="btn-secondary">
Modifier les noms
</button>
) : (
<div style={{display: 'flex', gap: 'var(--spacing-sm)'}}>
<button onClick={handleSaveChannelNames} className="btn-primary">
Sauvegarder
</button>
<button onClick={() => { editingChannelNamesRef.current = false; forceUpdate({}); loadAudioDevices(); }} className="btn-secondary">
Annuler
</button>
</div>
)}
</div>
<div style={{display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 'var(--spacing-xl)'}}> <div style={{display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 'var(--spacing-xl)', marginTop: 'var(--spacing-md)'}}>
<div> <div>
<h4 style={{marginBottom: 'var(--spacing-md)', color: 'var(--color-text-secondary)'}}> <h4 style={{marginBottom: 'var(--spacing-md)', color: 'var(--color-text-secondary)'}}>
Entrées (Inputs) - {currentDevice.inputChannels || 0} canaux disponibles Entrées (Inputs) - {currentDevice.inputChannels || 0} canaux disponibles
@@ -550,10 +557,9 @@ function Admin() {
value={channelNames.inputs?.[i] || ''} value={channelNames.inputs?.[i] || ''}
onChange={(e) => updateChannelName('inputs', i, e.target.value)} onChange={(e) => updateChannelName('inputs', i, e.target.value)}
placeholder={`Input ${i}`} placeholder={`Input ${i}`}
disabled={!editingChannelNamesRef.current}
style={{ style={{
padding: 'var(--spacing-sm)', padding: 'var(--spacing-sm)',
background: editingChannelNamesRef.current ? 'var(--color-bg)' : 'var(--color-surface-hover)', background: 'var(--color-bg)',
border: '1px solid var(--color-border)', border: '1px solid var(--color-border)',
borderRadius: '6px', borderRadius: '6px',
color: 'var(--color-text)', color: 'var(--color-text)',
@@ -578,10 +584,9 @@ function Admin() {
value={channelNames.outputs?.[i] || ''} value={channelNames.outputs?.[i] || ''}
onChange={(e) => updateChannelName('outputs', i, e.target.value)} onChange={(e) => updateChannelName('outputs', i, e.target.value)}
placeholder={`Output ${i}`} placeholder={`Output ${i}`}
disabled={!editingChannelNamesRef.current}
style={{ style={{
padding: 'var(--spacing-sm)', padding: 'var(--spacing-sm)',
background: editingChannelNamesRef.current ? 'var(--color-bg)' : 'var(--color-surface-hover)', background: 'var(--color-bg)',
border: '1px solid var(--color-border)', border: '1px solid var(--color-border)',
borderRadius: '6px', borderRadius: '6px',
color: 'var(--color-text)', color: 'var(--color-text)',
@@ -593,6 +598,12 @@ function Admin() {
</div> </div>
</div> </div>
</div> </div>
<div className="audio-actions">
<button onClick={handleSaveChannelNames} className="btn-primary">
Sauvegarder les noms des canaux
</button>
</div>
</div> </div>
<AudioRoutingMatrix groups={groups} channelNames={channelNames} /> <AudioRoutingMatrix groups={groups} channelNames={channelNames} />
+13 -5
View File
@@ -6,6 +6,13 @@
margin-top: var(--spacing-lg); margin-top: var(--spacing-lg);
} }
.routing-actions {
margin-top: var(--spacing-xl);
display: flex;
justify-content: flex-start;
gap: var(--spacing-sm);
}
.routing-matrix-header { .routing-matrix-header {
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
@@ -169,9 +176,9 @@
width: 100%; width: 100%;
padding: 4px 8px; padding: 4px 8px;
font-size: 0.75rem; font-size: 0.75rem;
background: rgba(255, 255, 255, 0.9); background: rgba(59, 130, 246, 0.2);
color: var(--color-text); color: #ffffff;
border: 1px solid rgba(255, 255, 255, 0.3); border: 1px solid rgba(255, 255, 255, 1);
border-radius: 4px; border-radius: 4px;
cursor: pointer; cursor: pointer;
font-weight: 600; font-weight: 600;
@@ -180,8 +187,9 @@
.gain-select:focus { .gain-select:focus {
outline: none; outline: none;
border-color: rgba(255, 255, 255, 0.6); background: rgba(59, 130, 246, 0.3);
box-shadow: 0 0 0 2px rgba(255, 255, 255, 0.2); border-color: rgba(255, 255, 255, 1);
box-shadow: 0 0 0 2px rgba(59, 130, 246, 0.2);
} }
@media (max-width: 1024px) { @media (max-width: 1024px) {
+6 -5
View File
@@ -194,7 +194,6 @@ function AudioRoutingMatrix({ groups, channelNames }) {
{wsConnected ? '● Live' : '○ Offline'} {wsConnected ? '● Live' : '○ Offline'}
</span> </span>
</div> </div>
<div style={{ display: 'flex', gap: '1rem', alignItems: 'center' }}>
<label style={{ display: 'flex', alignItems: 'center', gap: '0.5rem', cursor: 'pointer' }}> <label style={{ display: 'flex', alignItems: 'center', gap: '0.5rem', cursor: 'pointer' }}>
<input <input
type="checkbox" type="checkbox"
@@ -203,10 +202,6 @@ function AudioRoutingMatrix({ groups, channelNames }) {
/> />
<span>Afficher uniquement les canaux nommés</span> <span>Afficher uniquement les canaux nommés</span>
</label> </label>
<button onClick={saveRouting} className="btn-primary">
Sauvegarder le routing
</button>
</div>
</div> </div>
<div className="routing-section"> <div className="routing-section">
@@ -341,6 +336,12 @@ function AudioRoutingMatrix({ groups, channelNames }) {
))} ))}
</div> </div>
</div> </div>
<div className="routing-actions">
<button onClick={saveRouting} className="btn-primary">
Sauvegarder le routing audio
</button>
</div>
</div> </div>
); );
} }