Files
drb-frontend/src/components/IndividualClientPage.tsx
2025-05-25 23:20:48 -04:00

221 lines
8.3 KiB
TypeScript

// components/IndividualClientPage.tsx
"use client";
import React, { useState, useEffect } from 'react';
import { Button } from '@/components/ui/button';
import { Card, CardHeader, CardTitle, CardContent } from '@/components/ui/card';
import { API_BASE_URL } from '@/constants/api';
import { ErrorResponse, NodeStatusResponse, System } from '@/types';
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter } from '@/components/ui/dialog';
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
import { Label } from '@/components/ui/label';
interface IndividualClientPageProps {
clientId: string; // Now received as a prop
token: string;
}
const IndividualClientPage: React.FC<IndividualClientPageProps> = ({ clientId, token }) => {
const [message, setMessage] = useState<string>('');
const [error, setError] = useState<string>('');
const [loading, setLoading] = useState<boolean>(false);
const [currentClientDiscordStatus, setCurrentClientDiscordStatus] = useState<string>('Unknown');
const [currentClientOp25Status, setCurrentClientOp25Status] = useState<string>('Unknown');
// OP25 Set Config Popup States
const [selectedSystem, setSelectedSystem] = useState<string | null>(null);
const [isSetConfigDialogOpen, setIsSetConfigDialogOpen] = useState<boolean>(false);
const [availableSystems, setAvailableSystems] = useState<System[]>([]);
const fetchClientStatus = async () => {
try {
setLoading(true);
const response = await fetch(`${API_BASE_URL}/nodes/${clientId}/status`, {
headers: { Authorization: `Bearer ${token}` },
});
if (response.ok) {
const data: NodeStatusResponse = await response.json();
console.log(data)
setCurrentClientDiscordStatus(data.status.discord_status);
setCurrentClientOp25Status(data.status.op25_status)
} else {
setCurrentClientDiscordStatus('Failed to fetch status');
setCurrentClientOp25Status('Failed to fetch status');
}
} catch (err) {
setCurrentClientDiscordStatus('Error fetching status');
setCurrentClientOp25Status('Error fetching status');
} finally {
setLoading(false);
}
};
const fetchSystems = async (): Promise<void> => {
setError('');
try {
const response = await fetch(`${API_BASE_URL}/systems/`, {
headers: { Authorization: `Bearer ${token}` },
});
if (response.ok) {
const data: System[] = await response.json();
// Filter systems to those available on the current clientId
const filteredSystems = data.filter(system => system.avail_on_nodes.includes(clientId));
setAvailableSystems(filteredSystems);
} else {
let errorMsg = `Failed to fetch systems (${response.status})`;
try {
const errorData: ErrorResponse = await response.json();
errorMsg += `: ${errorData.message || (typeof errorData.detail === 'string' ? errorData.detail : JSON.stringify(errorData.detail)) || response.statusText}`;
} catch (e) { errorMsg += `: ${response.statusText}`; }
setError(errorMsg);
}
} catch (err: any) {
setError('Failed to fetch systems. Check server connection or console.');
console.error(err);
}
};
useEffect(() => {
fetchClientStatus();
fetchSystems(); // Fetch systems when the component mounts or clientId/token changes
}, [clientId, token]);
const handleAction = async (action: 'join' | 'leave' | 'op25_start' | 'op25_stop' | 'op25_set'): Promise<void> => {
setMessage('');
setError('');
setLoading(true);
try {
const httpOptions: RequestInit = {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${token}`,
}
}
if (action === "op25_set") {
if (!selectedSystem) {
setError("Please select a system to set configuration.");
setLoading(false);
return;
}
httpOptions.body = JSON.stringify({
"system_id": selectedSystem
});
}
const response = await fetch(`${API_BASE_URL}/nodes/${clientId}/${action}`, httpOptions);
const data = await response.json();
if (response.ok) {
setMessage(`Client "${clientId}" successfully ${action === 'op25_set' ? 'set config for system ' + selectedSystem : action + 'ed'}.`);
fetchClientStatus();
setIsSetConfigDialogOpen(false); // Close dialog on success
} else {
const errorData = data as ErrorResponse;
setError(`Failed to ${action} client "${clientId}": ${errorData.message || (typeof errorData.detail === 'string' ? errorData.detail : JSON.stringify(errorData.detail)) || response.statusText}`);
}
} catch (err: any) {
setError(`Network error during ${action} client: ${err.message}`);
console.error(err);
} finally {
setLoading(false);
}
};
return (
<Card className="w-full max-w-md mx-auto mt-8">
<CardHeader>
<CardTitle>Manage Client: {clientId}</CardTitle>
</CardHeader>
<CardContent className="space-y-4">
<p>Current Discord Status: {currentClientDiscordStatus}</p>
<div className="flex space-x-2">
<Button
onClick={() => handleAction('join')}
disabled={loading}
className="flex-1"
>
{loading && message.includes('join') ? 'Joining...' : 'Join Client'}
</Button>
<Button
onClick={() => handleAction('leave')}
disabled={loading}
variant="outline"
className="flex-1"
>
{loading && message.includes('leave') ? 'Leaving...' : 'Leave Client'}
</Button>
</div>
<p>Current OP25 Status: {currentClientOp25Status}</p>
<div className="flex space-x-2">
<Button
onClick={() => handleAction('op25_start')}
disabled={loading}
className="flex-1"
>
{loading && message.includes('start') ? 'Starting...' : 'Start OP25'}
</Button>
<Button
onClick={() => setIsSetConfigDialogOpen(true)} // Open dialog for set config
disabled={loading}
className="flex-1"
>
Set OP25 Config
</Button>
<Button
onClick={() => handleAction('op25_stop')}
disabled={loading}
variant="outline"
className="flex-1"
>
{loading && message.includes('stop') ? 'Stopping...' : 'Stop OP25'}
</Button>
</div>
{message && <p className="text-green-500 text-sm">{message}</p>}
{error && <p className="text-red-500 text-sm">{error}</p>}
{loading && <p className="text-blue-500 text-sm">Processing request...</p>}
</CardContent>
{/* Set Config Dialog */}
<Dialog open={isSetConfigDialogOpen} onOpenChange={setIsSetConfigDialogOpen}>
<DialogContent>
<DialogHeader>
<DialogTitle>Set OP25 System Configuration</DialogTitle>
</DialogHeader>
<div className="grid gap-4 py-4">
<div className="grid grid-cols-4 items-center gap-4">
<Label htmlFor="system-select" className="text-right">Select System</Label>
<Select onValueChange={setSelectedSystem} value={selectedSystem || ''}>
<SelectTrigger className="col-span-3">
<SelectValue placeholder="Choose a system" />
</SelectTrigger>
<SelectContent>
{availableSystems.map((system) => (
<SelectItem key={system._id} value={system._id}>
{system.name} ({system._id})
</SelectItem>
))}
</SelectContent>
</Select>
</div>
</div>
<DialogFooter>
<Button onClick={() => handleAction('op25_set')} disabled={!selectedSystem || loading}>
{loading ? 'Setting...' : 'Confirm Set Config'}
</Button>
<Button variant="outline" onClick={() => setIsSetConfigDialogOpen(false)}>
Cancel
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
</Card>
);
};
export default IndividualClientPage;