Add elastic-recorder-dashboard.jsx

This commit is contained in:
Zac Gaetano 2026-03-31 15:29:51 -04:00
parent 62528fcaf1
commit 525aa97adb

View file

@ -0,0 +1,327 @@
import React, { useState, useCallback } from 'react';
import { RefreshCw, Settings, Save, AlertCircle, CheckCircle } from 'lucide-react';
export default function ElasticRecorderDashboard() {
const [apiKey, setApiKey] = useState('');
const [baseUrl, setBaseUrl] = useState('https://us-east-1.gvampp.com');
const [channels, setChannels] = useState([]);
const [loading, setLoading] = useState(false);
const [authenticated, setAuthenticated] = useState(false);
const [error, setError] = useState('');
const [success, setSuccess] = useState('');
const [editingId, setEditingId] = useState(null);
const [editForm, setEditForm] = useState({});
// Get auth token
const getToken = useCallback(async () => {
try {
const [username, password] = atob(apiKey).split(':');
const response = await fetch(`${baseUrl}/identity/connect/token`, {
method: 'POST',
headers: {
'Authorization': `Basic ${apiKey}`,
'Content-Type': 'application/x-www-form-urlencoded',
},
body: new URLSearchParams({
grant_type: 'client_credentials',
scope: 'platform'
})
});
if (!response.ok) {
throw new Error(`Authentication failed: ${response.statusText}`);
}
const data = await response.json();
return data.access_token;
} catch (err) {
throw new Error(`Token error: ${err.message}`);
}
}, [apiKey, baseUrl]);
// Fetch channels
const fetchChannels = useCallback(async () => {
setLoading(true);
setError('');
try {
const token = await getToken();
const response = await fetch(`${baseUrl}/api/store/channel/v1/channels`, {
headers: {
'Authorization': `Bearer ${token}`
}
});
if (!response.ok) {
throw new Error(`Failed to fetch channels: ${response.statusText}`);
}
const data = await response.json();
setChannels(data);
setAuthenticated(true);
setSuccess(`Loaded ${data.length} channels`);
setTimeout(() => setSuccess(''), 3000);
} catch (err) {
setError(err.message);
setAuthenticated(false);
} finally {
setLoading(false);
}
}, [getToken, baseUrl]);
// Handle login
const handleLogin = (e) => {
e.preventDefault();
if (apiKey && baseUrl) {
fetchChannels();
}
};
// Start editing a channel
const startEdit = (channel) => {
setEditingId(channel['channel:id']);
setEditForm({
'source:text': channel['source:text'] || '',
'destinationId:int': channel['destinationId:int'] || '',
'name:text': channel['name:text'] || ''
});
};
// Cancel edit
const cancelEdit = () => {
setEditingId(null);
setEditForm({});
};
// Save changes
const saveChanges = async () => {
setLoading(true);
setError('');
try {
const token = await getToken();
const updateData = {
'source:text': editForm['source:text'],
'destinationId:int': editForm['destinationId:int'] ? parseInt(editForm['destinationId:int']) : null
};
const response = await fetch(`${baseUrl}/api/store/channel/v1/channels/${editingId}`, {
method: 'PATCH',
headers: {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json'
},
body: JSON.stringify(updateData)
});
if (!response.ok) {
throw new Error(`Failed to update channel: ${response.statusText}`);
}
setSuccess('Channel updated successfully');
setEditingId(null);
setTimeout(() => fetchChannels(), 500);
} catch (err) {
setError(err.message);
} finally {
setLoading(false);
}
};
// Login form
if (!authenticated) {
return (
<div className="min-h-screen bg-gradient-to-br from-slate-900 to-slate-800 flex items-center justify-center p-4">
<div className="bg-slate-800 rounded-lg shadow-xl p-8 w-full max-w-md border border-slate-700">
<div className="mb-6">
<h1 className="text-2xl font-bold text-white mb-2">Elastic Recorder Control</h1>
<p className="text-slate-400">Manage recording settings and folder assignments</p>
</div>
<form onSubmit={handleLogin} className="space-y-4">
<div>
<label className="block text-sm font-medium text-slate-300 mb-2">API Key</label>
<textarea
value={apiKey}
onChange={(e) => setApiKey(e.target.value)}
placeholder="Paste your base64-encoded API key"
className="w-full px-3 py-2 bg-slate-700 border border-slate-600 rounded text-white placeholder-slate-400 text-sm font-mono h-24"
/>
</div>
<div>
<label className="block text-sm font-medium text-slate-300 mb-2">Platform URL</label>
<input
type="text"
value={baseUrl}
onChange={(e) => setBaseUrl(e.target.value)}
className="w-full px-3 py-2 bg-slate-700 border border-slate-600 rounded text-white placeholder-slate-400"
/>
</div>
{error && (
<div className="flex gap-2 p-3 bg-red-900/20 border border-red-600 rounded text-red-400 text-sm">
<AlertCircle className="w-4 h-4 flex-shrink-0 mt-0.5" />
<span>{error}</span>
</div>
)}
<button
type="submit"
disabled={loading || !apiKey}
className="w-full py-2 bg-blue-600 hover:bg-blue-700 disabled:bg-slate-600 text-white font-medium rounded transition"
>
{loading ? 'Connecting...' : 'Connect'}
</button>
</form>
</div>
</div>
);
}
// Main dashboard
return (
<div className="min-h-screen bg-slate-900 text-white p-6">
<div className="max-w-6xl mx-auto">
{/* Header */}
<div className="flex items-center justify-between mb-8">
<div>
<h1 className="text-3xl font-bold mb-2">Elastic Recorder Control</h1>
<p className="text-slate-400">Manage {channels.length} recorders</p>
</div>
<button
onClick={fetchChannels}
disabled={loading}
className="flex items-center gap-2 px-4 py-2 bg-slate-700 hover:bg-slate-600 rounded transition"
>
<RefreshCw className={`w-4 h-4 ${loading ? 'animate-spin' : ''}`} />
Refresh
</button>
</div>
{/* Alerts */}
{error && (
<div className="mb-4 p-4 bg-red-900/20 border border-red-600 rounded flex gap-3">
<AlertCircle className="w-5 h-5 text-red-400 flex-shrink-0" />
<div className="text-red-400">{error}</div>
</div>
)}
{success && (
<div className="mb-4 p-4 bg-green-900/20 border border-green-600 rounded flex gap-3">
<CheckCircle className="w-5 h-5 text-green-400 flex-shrink-0" />
<div className="text-green-400">{success}</div>
</div>
)}
{/* Channels Grid */}
<div className="grid grid-cols-1 lg:grid-cols-2 gap-4">
{channels.map((channel) => {
const isEditing = editingId === channel['channel:id'];
return (
<div
key={channel['channel:id']}
className="bg-slate-800 border border-slate-700 rounded-lg p-6 hover:border-slate-600 transition"
>
{/* Channel Header */}
<div className="flex items-start justify-between mb-4">
<div>
<h2 className="text-lg font-semibold mb-1">{channel['name:text']}</h2>
<p className="text-slate-400 text-sm font-mono">{channel['channel:id'].substring(0, 20)}...</p>
</div>
{channel['elasticRecorder'] && (
<div className="px-3 py-1 bg-blue-900/30 border border-blue-700 rounded text-xs text-blue-300 font-medium">
Elastic Recorder
</div>
)}
</div>
{/* Display Mode */}
{!isEditing ? (
<>
<div className="space-y-3 mb-4">
<div>
<label className="text-xs font-medium text-slate-400 uppercase">Recording Store</label>
<p className="text-white mt-1">{channel['source:text'] || '—'}</p>
</div>
<div>
<label className="text-xs font-medium text-slate-400 uppercase">Framelight Folder</label>
<p className="text-white mt-1">{channel['destinationId:int'] || '—'}</p>
</div>
{channel['elasticRecorder'] && (
<div>
<label className="text-xs font-medium text-slate-400 uppercase">Workload ID</label>
<p className="text-white mt-1 font-mono text-sm">{channel['elasticRecorder']['workloadId:text']?.substring(0, 16)}...</p>
</div>
)}
</div>
<button
onClick={() => startEdit(channel)}
className="w-full flex items-center justify-center gap-2 px-4 py-2 bg-slate-700 hover:bg-slate-600 rounded transition"
>
<Settings className="w-4 h-4" />
Edit Settings
</button>
</>
) : (
/* Edit Mode */
<>
<div className="space-y-3 mb-4">
<div>
<label className="text-xs font-medium text-slate-400 uppercase mb-1 block">Recording Store</label>
<input
type="text"
value={editForm['source:text']}
onChange={(e) => setEditForm({...editForm, 'source:text': e.target.value})}
placeholder="e.g., /archive/store1"
className="w-full px-3 py-2 bg-slate-700 border border-slate-600 rounded text-white placeholder-slate-400 text-sm"
/>
</div>
<div>
<label className="text-xs font-medium text-slate-400 uppercase mb-1 block">Framelight Folder ID</label>
<input
type="text"
value={editForm['destinationId:int']}
onChange={(e) => setEditForm({...editForm, 'destinationId:int': e.target.value})}
placeholder="Folder ID or path"
className="w-full px-3 py-2 bg-slate-700 border border-slate-600 rounded text-white placeholder-slate-400 text-sm"
/>
</div>
</div>
<div className="flex gap-2">
<button
onClick={saveChanges}
disabled={loading}
className="flex-1 flex items-center justify-center gap-2 px-4 py-2 bg-green-600 hover:bg-green-700 disabled:bg-slate-600 text-white rounded transition"
>
<Save className="w-4 h-4" />
Save
</button>
<button
onClick={cancelEdit}
disabled={loading}
className="flex-1 px-4 py-2 bg-slate-700 hover:bg-slate-600 disabled:bg-slate-600 text-white rounded transition"
>
Cancel
</button>
</div>
</>
)}
</div>
);
})}
</div>
{channels.length === 0 && (
<div className="text-center py-12 text-slate-400">
<p>No channels found. Try refreshing or check your connection.</p>
</div>
)}
</div>
</div>
);
}