221 lines
8.3 KiB
TypeScript
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; |