something
This commit is contained in:
@@ -1,7 +1,9 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import { admin } from '../utils/api';
|
||||
import { useState, useEffect, useRef } from 'react';
|
||||
import { admin, jobs, normalizeArrayResponse } from '../utils/api';
|
||||
import { wsManager } from '../utils/websocket';
|
||||
import UserJobs from './UserJobs';
|
||||
import PasswordChange from './PasswordChange';
|
||||
import LoadingSpinner from './LoadingSpinner';
|
||||
|
||||
export default function AdminPanel() {
|
||||
const [activeSection, setActiveSection] = useState('api-keys');
|
||||
@@ -16,16 +18,110 @@ export default function AdminPanel() {
|
||||
const [selectedUser, setSelectedUser] = useState(null);
|
||||
const [registrationEnabled, setRegistrationEnabled] = useState(true);
|
||||
const [passwordChangeUser, setPasswordChangeUser] = useState(null);
|
||||
const listenerIdRef = useRef(null); // Listener ID for shared WebSocket
|
||||
const subscribedChannelsRef = useRef(new Set()); // Track confirmed subscribed channels
|
||||
const pendingSubscriptionsRef = useRef(new Set()); // Track pending subscriptions (waiting for confirmation)
|
||||
|
||||
// Connect to shared WebSocket on mount
|
||||
useEffect(() => {
|
||||
listenerIdRef.current = wsManager.subscribe('adminpanel', {
|
||||
open: () => {
|
||||
console.log('AdminPanel: Shared WebSocket connected');
|
||||
// Subscribe to runners if already viewing runners section
|
||||
if (activeSection === 'runners') {
|
||||
subscribeToRunners();
|
||||
}
|
||||
},
|
||||
message: (data) => {
|
||||
// Handle subscription responses
|
||||
if (data.type === 'subscribed' && data.channel) {
|
||||
pendingSubscriptionsRef.current.delete(data.channel);
|
||||
subscribedChannelsRef.current.add(data.channel);
|
||||
console.log('Successfully subscribed to channel:', data.channel);
|
||||
} else if (data.type === 'subscription_error' && data.channel) {
|
||||
pendingSubscriptionsRef.current.delete(data.channel);
|
||||
subscribedChannelsRef.current.delete(data.channel);
|
||||
console.error('Subscription failed for channel:', data.channel, data.error);
|
||||
}
|
||||
|
||||
// Handle runners channel messages
|
||||
if (data.channel === 'runners' && data.type === 'runner_status') {
|
||||
// Update runner in list
|
||||
setRunners(prev => {
|
||||
const index = prev.findIndex(r => r.id === data.runner_id);
|
||||
if (index >= 0 && data.data) {
|
||||
const updated = [...prev];
|
||||
updated[index] = { ...updated[index], ...data.data };
|
||||
return updated;
|
||||
}
|
||||
return prev;
|
||||
});
|
||||
}
|
||||
},
|
||||
error: (error) => {
|
||||
console.error('AdminPanel: Shared WebSocket error:', error);
|
||||
},
|
||||
close: (event) => {
|
||||
console.log('AdminPanel: Shared WebSocket closed:', event);
|
||||
subscribedChannelsRef.current.clear();
|
||||
pendingSubscriptionsRef.current.clear();
|
||||
}
|
||||
});
|
||||
|
||||
// Ensure connection is established
|
||||
wsManager.connect();
|
||||
|
||||
return () => {
|
||||
// Unsubscribe from all channels before unmounting
|
||||
unsubscribeFromRunners();
|
||||
if (listenerIdRef.current) {
|
||||
wsManager.unsubscribe(listenerIdRef.current);
|
||||
listenerIdRef.current = null;
|
||||
}
|
||||
};
|
||||
}, []);
|
||||
|
||||
const subscribeToRunners = () => {
|
||||
const channel = 'runners';
|
||||
if (wsManager.getReadyState() !== WebSocket.OPEN) {
|
||||
return;
|
||||
}
|
||||
// Don't subscribe if already subscribed or pending
|
||||
if (subscribedChannelsRef.current.has(channel) || pendingSubscriptionsRef.current.has(channel)) {
|
||||
return;
|
||||
}
|
||||
wsManager.send({ type: 'subscribe', channel });
|
||||
pendingSubscriptionsRef.current.add(channel);
|
||||
console.log('Subscribing to runners channel');
|
||||
};
|
||||
|
||||
const unsubscribeFromRunners = () => {
|
||||
const channel = 'runners';
|
||||
if (wsManager.getReadyState() !== WebSocket.OPEN) {
|
||||
return;
|
||||
}
|
||||
if (!subscribedChannelsRef.current.has(channel)) {
|
||||
return; // Not subscribed
|
||||
}
|
||||
wsManager.send({ type: 'unsubscribe', channel });
|
||||
subscribedChannelsRef.current.delete(channel);
|
||||
pendingSubscriptionsRef.current.delete(channel);
|
||||
console.log('Unsubscribed from runners channel');
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (activeSection === 'api-keys') {
|
||||
loadAPIKeys();
|
||||
unsubscribeFromRunners();
|
||||
} else if (activeSection === 'runners') {
|
||||
loadRunners();
|
||||
subscribeToRunners();
|
||||
} else if (activeSection === 'users') {
|
||||
loadUsers();
|
||||
unsubscribeFromRunners();
|
||||
} else if (activeSection === 'settings') {
|
||||
loadSettings();
|
||||
unsubscribeFromRunners();
|
||||
}
|
||||
}, [activeSection]);
|
||||
|
||||
@@ -33,7 +129,7 @@ export default function AdminPanel() {
|
||||
setLoading(true);
|
||||
try {
|
||||
const data = await admin.listAPIKeys();
|
||||
setApiKeys(Array.isArray(data) ? data : []);
|
||||
setApiKeys(normalizeArrayResponse(data));
|
||||
} catch (error) {
|
||||
console.error('Failed to load API keys:', error);
|
||||
setApiKeys([]);
|
||||
@@ -47,7 +143,7 @@ export default function AdminPanel() {
|
||||
setLoading(true);
|
||||
try {
|
||||
const data = await admin.listRunners();
|
||||
setRunners(Array.isArray(data) ? data : []);
|
||||
setRunners(normalizeArrayResponse(data));
|
||||
} catch (error) {
|
||||
console.error('Failed to load runners:', error);
|
||||
setRunners([]);
|
||||
@@ -61,7 +157,7 @@ export default function AdminPanel() {
|
||||
setLoading(true);
|
||||
try {
|
||||
const data = await admin.listUsers();
|
||||
setUsers(Array.isArray(data) ? data : []);
|
||||
setUsers(normalizeArrayResponse(data));
|
||||
} catch (error) {
|
||||
console.error('Failed to load users:', error);
|
||||
setUsers([]);
|
||||
@@ -121,29 +217,22 @@ export default function AdminPanel() {
|
||||
}
|
||||
};
|
||||
|
||||
const revokeAPIKey = async (keyId) => {
|
||||
if (!confirm('Are you sure you want to revoke this API key? Revoked keys cannot be used for new runner registrations.')) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
await admin.revokeAPIKey(keyId);
|
||||
await loadAPIKeys();
|
||||
} catch (error) {
|
||||
console.error('Failed to revoke API key:', error);
|
||||
alert('Failed to revoke API key');
|
||||
}
|
||||
};
|
||||
const [deletingKeyId, setDeletingKeyId] = useState(null);
|
||||
const [deletingRunnerId, setDeletingRunnerId] = useState(null);
|
||||
|
||||
const deleteAPIKey = async (keyId) => {
|
||||
if (!confirm('Are you sure you want to permanently delete this API key? This action cannot be undone.')) {
|
||||
const revokeAPIKey = async (keyId) => {
|
||||
if (!confirm('Are you sure you want to delete this API key? This action cannot be undone.')) {
|
||||
return;
|
||||
}
|
||||
setDeletingKeyId(keyId);
|
||||
try {
|
||||
await admin.deleteAPIKey(keyId);
|
||||
await loadAPIKeys();
|
||||
} catch (error) {
|
||||
console.error('Failed to delete API key:', error);
|
||||
alert('Failed to delete API key');
|
||||
} finally {
|
||||
setDeletingKeyId(null);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -152,12 +241,15 @@ export default function AdminPanel() {
|
||||
if (!confirm('Are you sure you want to delete this runner?')) {
|
||||
return;
|
||||
}
|
||||
setDeletingRunnerId(runnerId);
|
||||
try {
|
||||
await admin.deleteRunner(runnerId);
|
||||
await loadRunners();
|
||||
} catch (error) {
|
||||
console.error('Failed to delete runner:', error);
|
||||
alert('Failed to delete runner');
|
||||
} finally {
|
||||
setDeletingRunnerId(null);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -313,9 +405,7 @@ export default function AdminPanel() {
|
||||
<div className="bg-gray-800 rounded-lg shadow-md p-6 border border-gray-700">
|
||||
<h2 className="text-xl font-semibold mb-4 text-gray-100">API Keys</h2>
|
||||
{loading ? (
|
||||
<div className="flex justify-center py-8">
|
||||
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-orange-500"></div>
|
||||
</div>
|
||||
<LoadingSpinner size="sm" className="py-8" />
|
||||
) : !apiKeys || apiKeys.length === 0 ? (
|
||||
<p className="text-gray-400 text-center py-8">No API keys generated yet.</p>
|
||||
) : (
|
||||
@@ -384,21 +474,13 @@ export default function AdminPanel() {
|
||||
{new Date(key.created_at).toLocaleString()}
|
||||
</td>
|
||||
<td className="px-6 py-4 whitespace-nowrap text-sm space-x-2">
|
||||
{key.is_active && !expired && (
|
||||
<button
|
||||
onClick={() => revokeAPIKey(key.id)}
|
||||
className="text-yellow-400 hover:text-yellow-300 font-medium"
|
||||
title="Revoke API key"
|
||||
>
|
||||
Revoke
|
||||
</button>
|
||||
)}
|
||||
<button
|
||||
onClick={() => deleteAPIKey(key.id)}
|
||||
className="text-red-400 hover:text-red-300 font-medium"
|
||||
title="Permanently delete API key"
|
||||
onClick={() => revokeAPIKey(key.id)}
|
||||
disabled={deletingKeyId === key.id}
|
||||
className="text-red-400 hover:text-red-300 font-medium disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
title="Delete API key"
|
||||
>
|
||||
Delete
|
||||
{deletingKeyId === key.id ? 'Deleting...' : 'Delete'}
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
@@ -416,9 +498,7 @@ export default function AdminPanel() {
|
||||
<div className="bg-gray-800 rounded-lg shadow-md p-6 border border-gray-700">
|
||||
<h2 className="text-xl font-semibold mb-4 text-gray-100">Runner Management</h2>
|
||||
{loading ? (
|
||||
<div className="flex justify-center py-8">
|
||||
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-orange-500"></div>
|
||||
</div>
|
||||
<LoadingSpinner size="sm" className="py-8" />
|
||||
) : !runners || runners.length === 0 ? (
|
||||
<p className="text-gray-400 text-center py-8">No runners registered.</p>
|
||||
) : (
|
||||
@@ -506,9 +586,10 @@ export default function AdminPanel() {
|
||||
<td className="px-6 py-4 whitespace-nowrap text-sm">
|
||||
<button
|
||||
onClick={() => deleteRunner(runner.id)}
|
||||
className="text-red-400 hover:text-red-300 font-medium"
|
||||
disabled={deletingRunnerId === runner.id}
|
||||
className="text-red-400 hover:text-red-300 font-medium disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
>
|
||||
Delete
|
||||
{deletingRunnerId === runner.id ? 'Deleting...' : 'Delete'}
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
@@ -558,9 +639,7 @@ export default function AdminPanel() {
|
||||
<div className="bg-gray-800 rounded-lg shadow-md p-6 border border-gray-700">
|
||||
<h2 className="text-xl font-semibold mb-4 text-gray-100">User Management</h2>
|
||||
{loading ? (
|
||||
<div className="flex justify-center py-8">
|
||||
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-orange-500"></div>
|
||||
</div>
|
||||
<LoadingSpinner size="sm" className="py-8" />
|
||||
) : !users || users.length === 0 ? (
|
||||
<p className="text-gray-400 text-center py-8">No users found.</p>
|
||||
) : (
|
||||
|
||||
Reference in New Issue
Block a user