Init working UI
This commit is contained in:
425
src/components/BotsManagement.tsx
Normal file
425
src/components/BotsManagement.tsx
Normal file
@@ -0,0 +1,425 @@
|
||||
"use client";
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import Link from 'next/link';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { Card, CardHeader, CardTitle, CardContent } from '@/components/ui/card';
|
||||
import { Label } from '@/components/ui/label';
|
||||
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table';
|
||||
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter } from '@/components/ui/dialog';
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
|
||||
import { Checkbox } from '@/components/ui/checkbox';
|
||||
import { API_BASE_URL } from '@/constants/api';
|
||||
import { DiscordId, System, ErrorResponse } from '@/types'; // Import types
|
||||
|
||||
interface BotsManagementProps {
|
||||
token: string;
|
||||
}
|
||||
|
||||
const BotsManagement: React.FC<BotsManagementProps> = ({ token }) => {
|
||||
const [bots, setBots] = useState<string[]>([]);
|
||||
const [discordIds, setDiscordIds] = useState<DiscordId[]>([]);
|
||||
const [systems, setSystems] = useState<System[]>([]);
|
||||
const [loading, setLoading] = useState<boolean>(true);
|
||||
const [error, setError] = useState<string>('');
|
||||
|
||||
const [isAddIdDialogOpen, setIsAddIdDialogOpen] = useState<boolean>(false);
|
||||
const [newIdData, setNewIdData] = useState<Omit<DiscordId, '_id' | 'guild_ids'> & { guild_ids: string }>({
|
||||
discord_id: '',
|
||||
name: '',
|
||||
token: '',
|
||||
active: false,
|
||||
guild_ids: '',
|
||||
});
|
||||
const [editingId, setEditingId] = useState<DiscordId | null>(null);
|
||||
|
||||
const [isAssignDismissDialogOpen, setIsAssignDismissDialogOpen] = useState<boolean>(false);
|
||||
const [selectedBotClientId, setSelectedBotClientId] = useState<string>('');
|
||||
const [selectedSystemId, setSelectedSystemId] = useState<string>('');
|
||||
const [assignDismissAction, setAssignDismissAction] = useState<'assign' | 'dismiss'>('assign');
|
||||
|
||||
const fetchData = async (): Promise<void> => {
|
||||
setLoading(true);
|
||||
setError('');
|
||||
let accumulatedError = '';
|
||||
|
||||
try {
|
||||
const [nodesRes, discordIdsRes, systemsRes] = await Promise.all([
|
||||
fetch(`${API_BASE_URL}/nodes/`, { headers: { Authorization: `Bearer ${token}` } }),
|
||||
fetch(`${API_BASE_URL}/bots/tokens/`, { headers: { Authorization: `Bearer ${token}` } }),
|
||||
fetch(`${API_BASE_URL}/systems/`, { headers: { Authorization: `Bearer ${token}` } }),
|
||||
]);
|
||||
|
||||
if (nodesRes.ok) {
|
||||
const botsData: any = await nodesRes.json();
|
||||
setBots(botsData);
|
||||
} else {
|
||||
let errorMsg = `Failed to fetch bots (${nodesRes.status})`;
|
||||
try {
|
||||
const errorData: ErrorResponse = await nodesRes.json();
|
||||
errorMsg += `: ${errorData.message || (typeof errorData.detail === 'string' ? errorData.detail : JSON.stringify(errorData.detail)) || nodesRes.statusText}`;
|
||||
} catch (e) { errorMsg += `: ${nodesRes.statusText}`; }
|
||||
accumulatedError += errorMsg + '\n';
|
||||
}
|
||||
|
||||
if (discordIdsRes.ok) {
|
||||
const discordIdsData: DiscordId[] = await discordIdsRes.json();
|
||||
setDiscordIds(discordIdsData);
|
||||
} else {
|
||||
let errorMsg = `Failed to fetch Discord IDs (${discordIdsRes.status})`;
|
||||
try {
|
||||
const errorData: ErrorResponse = await discordIdsRes.json();
|
||||
errorMsg += `: ${errorData.message || (typeof errorData.detail === 'string' ? errorData.detail : JSON.stringify(errorData.detail)) || discordIdsRes.statusText}`;
|
||||
} catch (e) { errorMsg += `: ${discordIdsRes.statusText}`; }
|
||||
accumulatedError += errorMsg + '\n';
|
||||
}
|
||||
|
||||
if (systemsRes.ok) {
|
||||
const systemsData: System[] = await systemsRes.json();
|
||||
setSystems(systemsData);
|
||||
} else {
|
||||
let errorMsg = `Failed to fetch systems (${systemsRes.status})`;
|
||||
try {
|
||||
const errorData: ErrorResponse = await systemsRes.json();
|
||||
errorMsg += `: ${errorData.message || (typeof errorData.detail === 'string' ? errorData.detail : JSON.stringify(errorData.detail)) || systemsRes.statusText}`;
|
||||
} catch (e) { errorMsg += `: ${systemsRes.statusText}`; }
|
||||
accumulatedError += errorMsg + '\n';
|
||||
}
|
||||
|
||||
if (accumulatedError) {
|
||||
setError(accumulatedError.trim());
|
||||
}
|
||||
|
||||
} catch (err: any) {
|
||||
setError('Failed to fetch data. Check server connection or console.');
|
||||
console.error(err);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (token) {
|
||||
fetchData();
|
||||
}
|
||||
}, [token]);
|
||||
|
||||
|
||||
const handleAddId = async (): Promise<void> => {
|
||||
setError('');
|
||||
try {
|
||||
const payload: Omit<DiscordId, '_id'> = {
|
||||
...newIdData,
|
||||
guild_ids: newIdData.guild_ids.split(',').map(id => id.trim()).filter(id => id),
|
||||
};
|
||||
const method = editingId ? 'PUT' : 'POST';
|
||||
const url = editingId ? `${API_BASE_URL}/discord_ids/${editingId._id}` : `${API_BASE_URL}/discord_ids/`;
|
||||
|
||||
const response = await fetch(url, {
|
||||
method,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: `Bearer ${token}`,
|
||||
},
|
||||
body: JSON.stringify(payload),
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
fetchData();
|
||||
setIsAddIdDialogOpen(false);
|
||||
setNewIdData({ discord_id: '', name: '', token: '', active: false, guild_ids: '' });
|
||||
setEditingId(null);
|
||||
} else {
|
||||
let errorMsg = `Failed to ${editingId ? 'update' : 'add'} Discord ID (${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(`Network error during ${editingId ? 'update' : 'add'} Discord ID.`);
|
||||
console.error(err);
|
||||
}
|
||||
};
|
||||
|
||||
const handleDeleteId = async (id: string): Promise<void> => {
|
||||
if (!window.confirm('Are you sure you want to delete this Discord ID?')) return;
|
||||
setError('');
|
||||
try {
|
||||
const response = await fetch(`${API_BASE_URL}/discord_ids/${id}`, {
|
||||
method: 'DELETE',
|
||||
headers: { Authorization: `Bearer ${token}` },
|
||||
});
|
||||
if (response.ok) {
|
||||
fetchData();
|
||||
} else {
|
||||
let errorMsg = `Failed to delete Discord ID (${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('Network error during delete Discord ID.');
|
||||
console.error(err);
|
||||
}
|
||||
};
|
||||
|
||||
const handleAssignDismiss = async (): Promise<void> => {
|
||||
setError('');
|
||||
if (!selectedBotClientId || !selectedSystemId) {
|
||||
setError("Bot Client ID and System must be selected.");
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const endpoint = assignDismissAction === 'assign' ? 'assign' : 'dismiss';
|
||||
const response = await fetch(`${API_BASE_URL}/systems/${selectedSystemId}/${endpoint}`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: `Bearer ${token}`,
|
||||
},
|
||||
body: JSON.stringify({ client_id: selectedBotClientId }),
|
||||
});
|
||||
if (response.ok) {
|
||||
fetchData();
|
||||
setIsAssignDismissDialogOpen(false);
|
||||
setSelectedBotClientId('');
|
||||
setSelectedSystemId('');
|
||||
} else {
|
||||
let errorMsg = `Failed to ${assignDismissAction} bot (${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(`Network error during ${assignDismissAction} bot.`);
|
||||
console.error(err);
|
||||
}
|
||||
};
|
||||
|
||||
if (loading) return <p>Loading bots and Discord IDs...</p>;
|
||||
if (error && !loading) return <p className="text-red-500 whitespace-pre-line">{error}</p>;
|
||||
|
||||
|
||||
return (
|
||||
<Card className="w-full">
|
||||
<CardHeader>
|
||||
<CardTitle>Bots Management</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-6">
|
||||
{error && <p className="text-red-500 whitespace-pre-line">{error}</p>}
|
||||
<div>
|
||||
<h3 className="text-lg font-semibold mb-2">Online Bots (Clients)</h3>
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead>Client ID</TableHead>
|
||||
<TableHead>Assigned Discord ID (Name)</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{bots.length > 0 ? (
|
||||
bots.map((botClientId) => {
|
||||
const assignedSystem = systems.find(sys => sys.avail_on_nodes.includes(botClientId));
|
||||
const assignedDiscordBot = discordIds.find(did => did.name === assignedSystem?.name);
|
||||
|
||||
return (
|
||||
<TableRow key={botClientId}>
|
||||
<TableCell className="font-medium">
|
||||
<Link href={`/nodes/${botClientId}`} className="text-blue-600 hover:underline">
|
||||
{botClientId}
|
||||
</Link>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
{assignedSystem ? `${assignedSystem.name} (System)` : "N/A or requires different mapping"}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
);
|
||||
})
|
||||
) : (
|
||||
<TableRow>
|
||||
<TableCell colSpan={2} className="text-center">No bots currently online or reported by /bots.</TableCell>
|
||||
</TableRow>
|
||||
)}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h3 className="text-lg font-semibold mb-2">Manage Discord IDs</h3>
|
||||
<Button onClick={() => { setEditingId(null); setNewIdData({ discord_id: '', name: '', token: '', active: false, guild_ids: '' }); setIsAddIdDialogOpen(true);}} className="mb-4">Add New Discord ID</Button>
|
||||
<Button onClick={() => setIsAssignDismissDialogOpen(true)} className="mb-4 ml-2">Assign/Dismiss Bot System</Button>
|
||||
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead>Name</TableHead>
|
||||
<TableHead>Discord ID</TableHead>
|
||||
<TableHead>Token (Partial)</TableHead>
|
||||
<TableHead>Active</TableHead>
|
||||
<TableHead>Guilds</TableHead>
|
||||
<TableHead>Actions</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{discordIds.length > 0 ? (
|
||||
discordIds.map((dId) => (
|
||||
<TableRow key={dId._id}>
|
||||
<TableCell className="font-medium">{dId.name}</TableCell>
|
||||
<TableCell>{dId.discord_id}</TableCell>
|
||||
<TableCell className="truncate max-w-xs">{dId.token ? dId.token.substring(0, 8) + '...' : 'N/A'}</TableCell>
|
||||
<TableCell>{dId.active ? 'Yes' : 'No'}</TableCell>
|
||||
<TableCell>{dId.guild_ids.join(', ')}</TableCell>
|
||||
<TableCell>
|
||||
<Button variant="outline" size="sm" className="mr-2"
|
||||
onClick={() => {
|
||||
setEditingId(dId);
|
||||
setNewIdData({
|
||||
discord_id: dId.discord_id,
|
||||
name: dId.name,
|
||||
token: dId.token,
|
||||
active: dId.active,
|
||||
guild_ids: dId.guild_ids.join(', '),
|
||||
});
|
||||
setIsAddIdDialogOpen(true);
|
||||
}}>
|
||||
Edit
|
||||
</Button>
|
||||
<Button variant="destructive" size="sm" onClick={() => handleDeleteId(dId._id)}>
|
||||
Delete
|
||||
</Button>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))
|
||||
) : (
|
||||
<TableRow>
|
||||
<TableCell colSpan={6} className="text-center">No Discord IDs found.</TableCell>
|
||||
</TableRow>
|
||||
)}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
|
||||
<Dialog open={isAddIdDialogOpen} onOpenChange={setIsAddIdDialogOpen}>
|
||||
<DialogContent className="sm:max-w-[425px]">
|
||||
<DialogHeader>
|
||||
<DialogTitle>{editingId ? 'Edit Discord ID' : 'Add New Discord ID'}</DialogTitle>
|
||||
</DialogHeader>
|
||||
<div className="grid gap-4 py-4">
|
||||
<div className="grid grid-cols-4 items-center gap-4">
|
||||
<Label htmlFor="name" className="text-right">Name</Label>
|
||||
<Input
|
||||
id="name"
|
||||
value={newIdData.name}
|
||||
onChange={(e) => setNewIdData({ ...newIdData, name: e.target.value })}
|
||||
className="col-span-3"
|
||||
/>
|
||||
</div>
|
||||
<div className="grid grid-cols-4 items-center gap-4">
|
||||
<Label htmlFor="discord_id" className="text-right">Discord ID</Label>
|
||||
<Input
|
||||
id="discord_id"
|
||||
value={newIdData.discord_id}
|
||||
onChange={(e) => setNewIdData({ ...newIdData, discord_id: e.target.value })}
|
||||
className="col-span-3"
|
||||
/>
|
||||
</div>
|
||||
<div className="grid grid-cols-4 items-center gap-4">
|
||||
<Label htmlFor="token" className="text-right">Token</Label>
|
||||
<Input
|
||||
id="token"
|
||||
value={newIdData.token}
|
||||
onChange={(e) => setNewIdData({ ...newIdData, token: e.target.value })}
|
||||
className="col-span-3"
|
||||
placeholder={editingId ? "Token hidden for security, re-enter to change" : ""}
|
||||
/>
|
||||
</div>
|
||||
<div className="grid grid-cols-4 items-center gap-4">
|
||||
<Label htmlFor="guild_ids" className="text-right">Guild IDs (comma-separated)</Label>
|
||||
<Input
|
||||
id="guild_ids"
|
||||
value={newIdData.guild_ids}
|
||||
onChange={(e) => setNewIdData({ ...newIdData, guild_ids: e.target.value })}
|
||||
className="col-span-3"
|
||||
/>
|
||||
</div>
|
||||
<div className="grid grid-cols-4 items-center gap-4">
|
||||
<Label htmlFor="active" className="text-right">Active</Label>
|
||||
<Checkbox
|
||||
id="active"
|
||||
checked={newIdData.active}
|
||||
onCheckedChange={(checked) => setNewIdData({ ...newIdData, active: checked === true })}
|
||||
className="col-span-3"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<DialogFooter>
|
||||
<Button onClick={handleAddId}>{editingId ? 'Save Changes' : 'Add Discord ID'}</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
<Dialog open={isAssignDismissDialogOpen} onOpenChange={setIsAssignDismissDialogOpen}>
|
||||
<DialogContent className="sm:max-w-[425px]">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Assign/Dismiss Bot to System</DialogTitle>
|
||||
</DialogHeader>
|
||||
<div className="grid gap-4 py-4">
|
||||
<div className="grid grid-cols-4 items-center gap-4">
|
||||
<Label htmlFor="bot-client-id" className="text-right">Bot Client ID</Label>
|
||||
<Select onValueChange={setSelectedBotClientId} value={selectedBotClientId}>
|
||||
<SelectTrigger className="col-span-3">
|
||||
<SelectValue placeholder="Select a bot client ID" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{bots.map((botId) => (
|
||||
<SelectItem key={botId} value={botId}>{botId}</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<div className="grid grid-cols-4 items-center gap-4">
|
||||
<Label htmlFor="system-id" className="text-right">System</Label>
|
||||
<Select onValueChange={setSelectedSystemId} value={selectedSystemId}>
|
||||
<SelectTrigger className="col-span-3">
|
||||
<SelectValue placeholder="Select a system" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{systems.map((system) => (
|
||||
<SelectItem key={system._id} value={system._id}>{system.name} ({system._id})</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<div className="grid grid-cols-4 items-center gap-4">
|
||||
<Label htmlFor="action" className="text-right">Action</Label>
|
||||
<Select
|
||||
onValueChange={(value: string) => setAssignDismissAction(value as 'assign' | 'dismiss')}
|
||||
value={assignDismissAction}
|
||||
>
|
||||
<SelectTrigger className="col-span-3">
|
||||
<SelectValue placeholder="Select action" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="assign">Assign</SelectItem>
|
||||
<SelectItem value="dismiss">Dismiss</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
<DialogFooter>
|
||||
<Button onClick={handleAssignDismiss} disabled={!selectedBotClientId || !selectedSystemId}>Perform Action</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
|
||||
export default BotsManagement;
|
||||
Reference in New Issue
Block a user