massive changes and it works
This commit is contained in:
@@ -1,19 +1,29 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import { admin } from '../utils/api';
|
||||
import UserJobs from './UserJobs';
|
||||
import PasswordChange from './PasswordChange';
|
||||
|
||||
export default function AdminPanel() {
|
||||
const [activeSection, setActiveSection] = useState('tokens');
|
||||
const [tokens, setTokens] = useState([]);
|
||||
const [runners, setRunners] = useState([]);
|
||||
const [users, setUsers] = useState([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [newTokenExpires, setNewTokenExpires] = useState(24);
|
||||
const [newToken, setNewToken] = useState(null);
|
||||
const [selectedUser, setSelectedUser] = useState(null);
|
||||
const [registrationEnabled, setRegistrationEnabled] = useState(true);
|
||||
const [passwordChangeUser, setPasswordChangeUser] = useState(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (activeSection === 'tokens') {
|
||||
loadTokens();
|
||||
} else if (activeSection === 'runners') {
|
||||
loadRunners();
|
||||
} else if (activeSection === 'users') {
|
||||
loadUsers();
|
||||
} else if (activeSection === 'settings') {
|
||||
loadSettings();
|
||||
}
|
||||
}, [activeSection]);
|
||||
|
||||
@@ -21,9 +31,10 @@ export default function AdminPanel() {
|
||||
setLoading(true);
|
||||
try {
|
||||
const data = await admin.listTokens();
|
||||
setTokens(data);
|
||||
setTokens(Array.isArray(data) ? data : []);
|
||||
} catch (error) {
|
||||
console.error('Failed to load tokens:', error);
|
||||
setTokens([]);
|
||||
alert('Failed to load tokens');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
@@ -34,15 +45,58 @@ export default function AdminPanel() {
|
||||
setLoading(true);
|
||||
try {
|
||||
const data = await admin.listRunners();
|
||||
setRunners(data);
|
||||
setRunners(Array.isArray(data) ? data : []);
|
||||
} catch (error) {
|
||||
console.error('Failed to load runners:', error);
|
||||
setRunners([]);
|
||||
alert('Failed to load runners');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const loadUsers = async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const data = await admin.listUsers();
|
||||
setUsers(Array.isArray(data) ? data : []);
|
||||
} catch (error) {
|
||||
console.error('Failed to load users:', error);
|
||||
setUsers([]);
|
||||
alert('Failed to load users');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const loadSettings = async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const data = await admin.getRegistrationEnabled();
|
||||
setRegistrationEnabled(data.enabled);
|
||||
} catch (error) {
|
||||
console.error('Failed to load settings:', error);
|
||||
alert('Failed to load settings');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleToggleRegistration = async () => {
|
||||
const newValue = !registrationEnabled;
|
||||
setLoading(true);
|
||||
try {
|
||||
await admin.setRegistrationEnabled(newValue);
|
||||
setRegistrationEnabled(newValue);
|
||||
alert(`Registration ${newValue ? 'enabled' : 'disabled'}`);
|
||||
} catch (error) {
|
||||
console.error('Failed to update registration setting:', error);
|
||||
alert('Failed to update registration setting');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const generateToken = async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
@@ -109,36 +163,68 @@ export default function AdminPanel() {
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<div className="flex space-x-4 border-b border-gray-200">
|
||||
<div className="flex space-x-4 border-b border-gray-700">
|
||||
<button
|
||||
onClick={() => setActiveSection('tokens')}
|
||||
onClick={() => {
|
||||
setActiveSection('tokens');
|
||||
setSelectedUser(null);
|
||||
}}
|
||||
className={`py-2 px-4 border-b-2 font-medium ${
|
||||
activeSection === 'tokens'
|
||||
? 'border-purple-600 text-purple-600'
|
||||
: 'border-transparent text-gray-500 hover:text-gray-700'
|
||||
? 'border-orange-500 text-orange-500'
|
||||
: 'border-transparent text-gray-400 hover:text-gray-300'
|
||||
}`}
|
||||
>
|
||||
Registration Tokens
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setActiveSection('runners')}
|
||||
onClick={() => {
|
||||
setActiveSection('runners');
|
||||
setSelectedUser(null);
|
||||
}}
|
||||
className={`py-2 px-4 border-b-2 font-medium ${
|
||||
activeSection === 'runners'
|
||||
? 'border-purple-600 text-purple-600'
|
||||
: 'border-transparent text-gray-500 hover:text-gray-700'
|
||||
? 'border-orange-500 text-orange-500'
|
||||
: 'border-transparent text-gray-400 hover:text-gray-300'
|
||||
}`}
|
||||
>
|
||||
Runner Management
|
||||
</button>
|
||||
<button
|
||||
onClick={() => {
|
||||
setActiveSection('users');
|
||||
setSelectedUser(null);
|
||||
}}
|
||||
className={`py-2 px-4 border-b-2 font-medium ${
|
||||
activeSection === 'users'
|
||||
? 'border-orange-500 text-orange-500'
|
||||
: 'border-transparent text-gray-400 hover:text-gray-300'
|
||||
}`}
|
||||
>
|
||||
Users
|
||||
</button>
|
||||
<button
|
||||
onClick={() => {
|
||||
setActiveSection('settings');
|
||||
setSelectedUser(null);
|
||||
}}
|
||||
className={`py-2 px-4 border-b-2 font-medium ${
|
||||
activeSection === 'settings'
|
||||
? 'border-orange-500 text-orange-500'
|
||||
: 'border-transparent text-gray-400 hover:text-gray-300'
|
||||
}`}
|
||||
>
|
||||
Settings
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{activeSection === 'tokens' && (
|
||||
<div className="space-y-6">
|
||||
<div className="bg-white rounded-lg shadow-md p-6">
|
||||
<h2 className="text-xl font-semibold mb-4">Generate Registration Token</h2>
|
||||
<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">Generate Registration Token</h2>
|
||||
<div className="flex gap-4 items-end">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||||
<label className="block text-sm font-medium text-gray-300 mb-2">
|
||||
Expires in (hours)
|
||||
</label>
|
||||
<input
|
||||
@@ -147,106 +233,106 @@ export default function AdminPanel() {
|
||||
max="168"
|
||||
value={newTokenExpires}
|
||||
onChange={(e) => setNewTokenExpires(parseInt(e.target.value) || 24)}
|
||||
className="w-32 px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-purple-600 focus:border-transparent"
|
||||
className="w-32 px-3 py-2 bg-gray-900 border border-gray-600 rounded-lg text-gray-100 focus:ring-2 focus:ring-orange-500 focus:border-transparent"
|
||||
/>
|
||||
</div>
|
||||
<button
|
||||
onClick={generateToken}
|
||||
disabled={loading}
|
||||
className="px-6 py-2 bg-purple-600 text-white rounded-lg hover:bg-purple-700 disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
|
||||
className="px-6 py-2 bg-orange-600 text-white rounded-lg hover:bg-orange-500 disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
|
||||
>
|
||||
Generate Token
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{newToken && (
|
||||
<div className="mt-4 p-4 bg-green-50 border border-green-200 rounded-lg">
|
||||
<p className="text-sm font-medium text-green-800 mb-2">New Token Generated:</p>
|
||||
<div className="mt-4 p-4 bg-green-400/20 border border-green-400/50 rounded-lg">
|
||||
<p className="text-sm font-medium text-green-400 mb-2">New Token Generated:</p>
|
||||
<div className="flex items-center gap-2">
|
||||
<code className="flex-1 px-3 py-2 bg-white border border-green-300 rounded text-sm font-mono break-all">
|
||||
<code className="flex-1 px-3 py-2 bg-gray-900 border border-green-400/50 rounded text-sm font-mono break-all text-gray-100">
|
||||
{newToken}
|
||||
</code>
|
||||
<button
|
||||
onClick={() => copyToClipboard(newToken)}
|
||||
className="px-4 py-2 bg-green-600 text-white rounded hover:bg-green-700 transition-colors text-sm"
|
||||
className="px-4 py-2 bg-green-600 text-white rounded hover:bg-green-500 transition-colors text-sm"
|
||||
>
|
||||
Copy
|
||||
</button>
|
||||
</div>
|
||||
<p className="text-xs text-green-700 mt-2">
|
||||
<p className="text-xs text-green-400/80 mt-2">
|
||||
Save this token securely. It will not be shown again.
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="bg-white rounded-lg shadow-md p-6">
|
||||
<h2 className="text-xl font-semibold mb-4">Active Tokens</h2>
|
||||
<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">Active Tokens</h2>
|
||||
{loading ? (
|
||||
<div className="flex justify-center py-8">
|
||||
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-purple-600"></div>
|
||||
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-orange-500"></div>
|
||||
</div>
|
||||
) : tokens.length === 0 ? (
|
||||
<p className="text-gray-500 text-center py-8">No tokens generated yet.</p>
|
||||
) : !tokens || tokens.length === 0 ? (
|
||||
<p className="text-gray-400 text-center py-8">No tokens generated yet.</p>
|
||||
) : (
|
||||
<div className="overflow-x-auto">
|
||||
<table className="min-w-full divide-y divide-gray-200">
|
||||
<thead className="bg-gray-50">
|
||||
<table className="min-w-full divide-y divide-gray-700">
|
||||
<thead className="bg-gray-900">
|
||||
<tr>
|
||||
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||
<th className="px-6 py-3 text-left text-xs font-medium text-gray-400 uppercase tracking-wider">
|
||||
Token
|
||||
</th>
|
||||
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||
<th className="px-6 py-3 text-left text-xs font-medium text-gray-400 uppercase tracking-wider">
|
||||
Status
|
||||
</th>
|
||||
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||
<th className="px-6 py-3 text-left text-xs font-medium text-gray-400 uppercase tracking-wider">
|
||||
Expires At
|
||||
</th>
|
||||
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||
<th className="px-6 py-3 text-left text-xs font-medium text-gray-400 uppercase tracking-wider">
|
||||
Created At
|
||||
</th>
|
||||
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||
<th className="px-6 py-3 text-left text-xs font-medium text-gray-400 uppercase tracking-wider">
|
||||
Actions
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="bg-white divide-y divide-gray-200">
|
||||
<tbody className="bg-gray-800 divide-y divide-gray-700">
|
||||
{tokens.map((token) => {
|
||||
const expired = isTokenExpired(token.expires_at);
|
||||
const used = isTokenUsed(token.used);
|
||||
return (
|
||||
<tr key={token.id}>
|
||||
<td className="px-6 py-4 whitespace-nowrap">
|
||||
<code className="text-sm font-mono text-gray-900">
|
||||
<code className="text-sm font-mono text-gray-100">
|
||||
{token.token.substring(0, 16)}...
|
||||
</code>
|
||||
</td>
|
||||
<td className="px-6 py-4 whitespace-nowrap">
|
||||
{expired ? (
|
||||
<span className="px-2 py-1 text-xs font-medium rounded-full bg-red-100 text-red-800">
|
||||
<span className="px-2 py-1 text-xs font-medium rounded-full bg-red-400/20 text-red-400">
|
||||
Expired
|
||||
</span>
|
||||
) : used ? (
|
||||
<span className="px-2 py-1 text-xs font-medium rounded-full bg-yellow-100 text-yellow-800">
|
||||
<span className="px-2 py-1 text-xs font-medium rounded-full bg-yellow-400/20 text-yellow-400">
|
||||
Used
|
||||
</span>
|
||||
) : (
|
||||
<span className="px-2 py-1 text-xs font-medium rounded-full bg-green-100 text-green-800">
|
||||
<span className="px-2 py-1 text-xs font-medium rounded-full bg-green-400/20 text-green-400">
|
||||
Active
|
||||
</span>
|
||||
)}
|
||||
</td>
|
||||
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
|
||||
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-400">
|
||||
{new Date(token.expires_at).toLocaleString()}
|
||||
</td>
|
||||
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
|
||||
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-400">
|
||||
{new Date(token.created_at).toLocaleString()}
|
||||
</td>
|
||||
<td className="px-6 py-4 whitespace-nowrap text-sm">
|
||||
{!used && !expired && (
|
||||
<button
|
||||
onClick={() => revokeToken(token.id)}
|
||||
className="text-red-600 hover:text-red-800 font-medium"
|
||||
className="text-red-400 hover:text-red-300 font-medium"
|
||||
>
|
||||
Revoke
|
||||
</button>
|
||||
@@ -264,56 +350,68 @@ export default function AdminPanel() {
|
||||
)}
|
||||
|
||||
{activeSection === 'runners' && (
|
||||
<div className="bg-white rounded-lg shadow-md p-6">
|
||||
<h2 className="text-xl font-semibold mb-4">Runner Management</h2>
|
||||
<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-purple-600"></div>
|
||||
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-orange-500"></div>
|
||||
</div>
|
||||
) : runners.length === 0 ? (
|
||||
<p className="text-gray-500 text-center py-8">No runners registered.</p>
|
||||
) : !runners || runners.length === 0 ? (
|
||||
<p className="text-gray-400 text-center py-8">No runners registered.</p>
|
||||
) : (
|
||||
<div className="overflow-x-auto">
|
||||
<table className="min-w-full divide-y divide-gray-200">
|
||||
<thead className="bg-gray-50">
|
||||
<table className="min-w-full divide-y divide-gray-700">
|
||||
<thead className="bg-gray-900">
|
||||
<tr>
|
||||
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||
<th className="px-6 py-3 text-left text-xs font-medium text-gray-400 uppercase tracking-wider">
|
||||
Name
|
||||
</th>
|
||||
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||
<th className="px-6 py-3 text-left text-xs font-medium text-gray-400 uppercase tracking-wider">
|
||||
Hostname
|
||||
</th>
|
||||
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||
<th className="px-6 py-3 text-left text-xs font-medium text-gray-400 uppercase tracking-wider">
|
||||
IP Address
|
||||
</th>
|
||||
<th className="px-6 py-3 text-left text-xs font-medium text-gray-400 uppercase tracking-wider">
|
||||
Status
|
||||
</th>
|
||||
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||
<th className="px-6 py-3 text-left text-xs font-medium text-gray-400 uppercase tracking-wider">
|
||||
Verified
|
||||
</th>
|
||||
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||
<th className="px-6 py-3 text-left text-xs font-medium text-gray-400 uppercase tracking-wider">
|
||||
Priority
|
||||
</th>
|
||||
<th className="px-6 py-3 text-left text-xs font-medium text-gray-400 uppercase tracking-wider">
|
||||
Capabilities
|
||||
</th>
|
||||
<th className="px-6 py-3 text-left text-xs font-medium text-gray-400 uppercase tracking-wider">
|
||||
Last Heartbeat
|
||||
</th>
|
||||
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||
<th className="px-6 py-3 text-left text-xs font-medium text-gray-400 uppercase tracking-wider">
|
||||
Actions
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="bg-white divide-y divide-gray-200">
|
||||
<tbody className="bg-gray-800 divide-y divide-gray-700">
|
||||
{runners.map((runner) => {
|
||||
const isOnline = new Date(runner.last_heartbeat) > new Date(Date.now() - 60000);
|
||||
return (
|
||||
<tr key={runner.id}>
|
||||
<td className="px-6 py-4 whitespace-nowrap text-sm font-medium text-gray-900">
|
||||
<td className="px-6 py-4 whitespace-nowrap text-sm font-medium text-gray-100">
|
||||
{runner.name}
|
||||
</td>
|
||||
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
|
||||
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-400">
|
||||
{runner.hostname}
|
||||
</td>
|
||||
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-400">
|
||||
{runner.ip_address}
|
||||
</td>
|
||||
<td className="px-6 py-4 whitespace-nowrap">
|
||||
<span
|
||||
className={`px-2 py-1 text-xs font-medium rounded-full ${
|
||||
isOnline
|
||||
? 'bg-green-100 text-green-800'
|
||||
: 'bg-gray-100 text-gray-800'
|
||||
? 'bg-green-400/20 text-green-400'
|
||||
: 'bg-gray-500/20 text-gray-400'
|
||||
}`}
|
||||
>
|
||||
{isOnline ? 'Online' : 'Offline'}
|
||||
@@ -323,28 +421,49 @@ export default function AdminPanel() {
|
||||
<span
|
||||
className={`px-2 py-1 text-xs font-medium rounded-full ${
|
||||
runner.verified
|
||||
? 'bg-green-100 text-green-800'
|
||||
: 'bg-yellow-100 text-yellow-800'
|
||||
? 'bg-green-400/20 text-green-400'
|
||||
: 'bg-yellow-400/20 text-yellow-400'
|
||||
}`}
|
||||
>
|
||||
{runner.verified ? 'Verified' : 'Unverified'}
|
||||
</span>
|
||||
</td>
|
||||
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
|
||||
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-400">
|
||||
{runner.priority}
|
||||
</td>
|
||||
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-400">
|
||||
{runner.capabilities ? (
|
||||
(() => {
|
||||
try {
|
||||
const caps = JSON.parse(runner.capabilities);
|
||||
const enabled = Object.entries(caps)
|
||||
.filter(([_, v]) => v)
|
||||
.map(([k, _]) => k)
|
||||
.join(', ');
|
||||
return enabled || 'None';
|
||||
} catch {
|
||||
return runner.capabilities;
|
||||
}
|
||||
})()
|
||||
) : (
|
||||
'None'
|
||||
)}
|
||||
</td>
|
||||
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-400">
|
||||
{new Date(runner.last_heartbeat).toLocaleString()}
|
||||
</td>
|
||||
<td className="px-6 py-4 whitespace-nowrap text-sm space-x-2">
|
||||
{!runner.verified && (
|
||||
<button
|
||||
onClick={() => verifyRunner(runner.id)}
|
||||
className="text-blue-600 hover:text-blue-800 font-medium"
|
||||
className="text-orange-400 hover:text-orange-300 font-medium"
|
||||
>
|
||||
Verify
|
||||
</button>
|
||||
)}
|
||||
<button
|
||||
onClick={() => deleteRunner(runner.id)}
|
||||
className="text-red-600 hover:text-red-800 font-medium"
|
||||
className="text-red-400 hover:text-red-300 font-medium"
|
||||
>
|
||||
Delete
|
||||
</button>
|
||||
@@ -358,6 +477,214 @@ export default function AdminPanel() {
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{activeSection === 'change-password' && passwordChangeUser && (
|
||||
<div className="bg-gray-800 rounded-lg shadow-md p-6 border border-gray-700">
|
||||
<button
|
||||
onClick={() => {
|
||||
setPasswordChangeUser(null);
|
||||
setActiveSection('users');
|
||||
}}
|
||||
className="text-gray-400 hover:text-gray-300 mb-4 flex items-center gap-2"
|
||||
>
|
||||
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 19l-7-7 7-7" />
|
||||
</svg>
|
||||
Back to Users
|
||||
</button>
|
||||
<PasswordChange
|
||||
targetUserId={passwordChangeUser.id}
|
||||
targetUserName={passwordChangeUser.name || passwordChangeUser.email}
|
||||
onSuccess={() => {
|
||||
setPasswordChangeUser(null);
|
||||
setActiveSection('users');
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{activeSection === 'users' && (
|
||||
<div className="space-y-6">
|
||||
{selectedUser ? (
|
||||
<UserJobs
|
||||
userId={selectedUser.id}
|
||||
userName={selectedUser.name || selectedUser.email}
|
||||
onBack={() => setSelectedUser(null)}
|
||||
/>
|
||||
) : (
|
||||
<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>
|
||||
) : !users || users.length === 0 ? (
|
||||
<p className="text-gray-400 text-center py-8">No users found.</p>
|
||||
) : (
|
||||
<div className="overflow-x-auto">
|
||||
<table className="min-w-full divide-y divide-gray-700">
|
||||
<thead className="bg-gray-900">
|
||||
<tr>
|
||||
<th className="px-6 py-3 text-left text-xs font-medium text-gray-400 uppercase tracking-wider">
|
||||
Email
|
||||
</th>
|
||||
<th className="px-6 py-3 text-left text-xs font-medium text-gray-400 uppercase tracking-wider">
|
||||
Name
|
||||
</th>
|
||||
<th className="px-6 py-3 text-left text-xs font-medium text-gray-400 uppercase tracking-wider">
|
||||
Provider
|
||||
</th>
|
||||
<th className="px-6 py-3 text-left text-xs font-medium text-gray-400 uppercase tracking-wider">
|
||||
Admin
|
||||
</th>
|
||||
<th className="px-6 py-3 text-left text-xs font-medium text-gray-400 uppercase tracking-wider">
|
||||
Jobs
|
||||
</th>
|
||||
<th className="px-6 py-3 text-left text-xs font-medium text-gray-400 uppercase tracking-wider">
|
||||
Created
|
||||
</th>
|
||||
<th className="px-6 py-3 text-left text-xs font-medium text-gray-400 uppercase tracking-wider">
|
||||
Actions
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="bg-gray-800 divide-y divide-gray-700">
|
||||
{users.map((user) => (
|
||||
<tr key={user.id}>
|
||||
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-100">
|
||||
{user.email}
|
||||
</td>
|
||||
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-300">
|
||||
{user.name}
|
||||
</td>
|
||||
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-400">
|
||||
{user.oauth_provider}
|
||||
</td>
|
||||
<td className="px-6 py-4 whitespace-nowrap">
|
||||
<div className="flex items-center gap-2">
|
||||
{user.is_admin ? (
|
||||
<span className="px-2 py-1 text-xs font-medium rounded-full bg-orange-400/20 text-orange-400">
|
||||
Admin
|
||||
</span>
|
||||
) : (
|
||||
<span className="px-2 py-1 text-xs font-medium rounded-full bg-gray-500/20 text-gray-400">
|
||||
User
|
||||
</span>
|
||||
)}
|
||||
<button
|
||||
onClick={async () => {
|
||||
if (user.is_first_user && user.is_admin) {
|
||||
alert('Cannot remove admin status from the first user');
|
||||
return;
|
||||
}
|
||||
if (!confirm(`Are you sure you want to ${user.is_admin ? 'remove admin privileges from' : 'grant admin privileges to'} ${user.name || user.email}?`)) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
await admin.setUserAdminStatus(user.id, !user.is_admin);
|
||||
await loadUsers();
|
||||
alert(`Admin status ${user.is_admin ? 'removed' : 'granted'} successfully`);
|
||||
} catch (error) {
|
||||
console.error('Failed to update admin status:', error);
|
||||
const errorMsg = error.message || 'Failed to update admin status';
|
||||
if (errorMsg.includes('first user')) {
|
||||
alert('Cannot remove admin status from the first user');
|
||||
} else {
|
||||
alert(errorMsg);
|
||||
}
|
||||
}
|
||||
}}
|
||||
disabled={user.is_first_user && user.is_admin}
|
||||
className={`text-xs px-2 py-1 rounded ${
|
||||
user.is_first_user && user.is_admin
|
||||
? 'text-gray-500 bg-gray-500/10 cursor-not-allowed'
|
||||
: user.is_admin
|
||||
? 'text-red-400 hover:text-red-300 bg-red-400/10 hover:bg-red-400/20'
|
||||
: 'text-green-400 hover:text-green-300 bg-green-400/10 hover:bg-green-400/20'
|
||||
} transition-colors`}
|
||||
title={user.is_first_user && user.is_admin ? 'First user must remain admin' : user.is_admin ? 'Remove admin privileges' : 'Grant admin privileges'}
|
||||
>
|
||||
{user.is_admin ? 'Remove Admin' : 'Make Admin'}
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-400">
|
||||
{user.job_count || 0}
|
||||
</td>
|
||||
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-400">
|
||||
{new Date(user.created_at).toLocaleString()}
|
||||
</td>
|
||||
<td className="px-6 py-4 whitespace-nowrap text-sm">
|
||||
<div className="flex gap-3">
|
||||
<button
|
||||
onClick={() => setSelectedUser(user)}
|
||||
className="text-orange-400 hover:text-orange-300 font-medium"
|
||||
>
|
||||
View Jobs
|
||||
</button>
|
||||
{user.oauth_provider === 'local' && (
|
||||
<button
|
||||
onClick={() => {
|
||||
const userForPassword = { id: user.id, name: user.name || user.email };
|
||||
setPasswordChangeUser(userForPassword);
|
||||
setSelectedUser(null);
|
||||
setActiveSection('change-password');
|
||||
}}
|
||||
className="text-blue-400 hover:text-blue-300 font-medium"
|
||||
>
|
||||
Change Password
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{activeSection === 'settings' && (
|
||||
<div className="space-y-6">
|
||||
<PasswordChange />
|
||||
<div className="bg-gray-800 rounded-lg shadow-md p-6 border border-gray-700">
|
||||
<h2 className="text-xl font-semibold mb-6 text-gray-100">System Settings</h2>
|
||||
|
||||
<div className="space-y-6">
|
||||
<div className="flex items-center justify-between p-4 bg-gray-900 rounded-lg border border-gray-700">
|
||||
<div>
|
||||
<h3 className="text-lg font-medium text-gray-100 mb-1">User Registration</h3>
|
||||
<p className="text-sm text-gray-400">
|
||||
{registrationEnabled
|
||||
? 'New users can register via OAuth or local login'
|
||||
: 'Registration is disabled. Only existing users can log in.'}
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex items-center gap-4">
|
||||
<span className={`text-sm font-medium ${registrationEnabled ? 'text-green-400' : 'text-red-400'}`}>
|
||||
{registrationEnabled ? 'Enabled' : 'Disabled'}
|
||||
</span>
|
||||
<button
|
||||
onClick={handleToggleRegistration}
|
||||
disabled={loading}
|
||||
className={`px-6 py-2 rounded-lg font-medium transition-colors ${
|
||||
registrationEnabled
|
||||
? 'bg-red-600 hover:bg-red-500 text-white'
|
||||
: 'bg-green-600 hover:bg-green-500 text-white'
|
||||
} disabled:opacity-50 disabled:cursor-not-allowed`}
|
||||
>
|
||||
{registrationEnabled ? 'Disable' : 'Enable'}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user