Spaces:
Running on CPU Upgrade
Running on CPU Upgrade
Update frontend/src/pages/SuperAdminPanel.jsx
Browse files
frontend/src/pages/SuperAdminPanel.jsx
CHANGED
|
@@ -2,7 +2,7 @@ import React, { useState, useEffect } from 'react';
|
|
| 2 |
import { Link } from 'react-router-dom';
|
| 3 |
import { useAuth } from '../components/AuthContext';
|
| 4 |
import { CustomModal } from '../components/CustomModal';
|
| 5 |
-
import { Building2, Users, CreditCard, Shield, Trash2, Plus, Server, Activity, Database, HardDrive, RefreshCw, BarChart2, Edit, Download, Eye } from 'lucide-react';
|
| 6 |
import toast from 'react-hot-toast';
|
| 7 |
|
| 8 |
const DEFAULT_BILLING_TIERS = [
|
|
@@ -307,6 +307,11 @@ const SuperAdminPanel = () => {
|
|
| 307 |
const [sortUserCol, setSortUserCol] = useState('Email');
|
| 308 |
const [sortUserDir, setSortUserDir] = useState('asc');
|
| 309 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 310 |
const [sortBillCol, setSortBillCol] = useState('Date');
|
| 311 |
const [sortBillDir, setSortBillDir] = useState('desc');
|
| 312 |
|
|
@@ -361,8 +366,35 @@ const SuperAdminPanel = () => {
|
|
| 361 |
}
|
| 362 |
};
|
| 363 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 364 |
const getSortedUsers = () => {
|
| 365 |
-
|
|
|
|
| 366 |
let aVal, bVal;
|
| 367 |
switch (sortUserCol) {
|
| 368 |
case 'Email': aVal = a.email || ''; bVal = b.email || ''; break;
|
|
@@ -376,6 +408,13 @@ const SuperAdminPanel = () => {
|
|
| 376 |
});
|
| 377 |
};
|
| 378 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 379 |
const handleBillSort = (column) => {
|
| 380 |
if (column === 'Invoice') return;
|
| 381 |
if (sortBillCol === column) {
|
|
@@ -810,17 +849,21 @@ const SuperAdminPanel = () => {
|
|
| 810 |
{ key: 'org_id', label: 'Organization', type: 'select', options: orgOptions }
|
| 811 |
],
|
| 812 |
onConfirm: async (values) => {
|
|
|
|
|
|
|
|
|
|
|
|
|
| 813 |
try {
|
| 814 |
-
const res = await
|
| 815 |
method: 'POST',
|
| 816 |
-
headers: { '
|
| 817 |
-
body: JSON.stringify({ email: values.email, role: values.role, org_id: values.org_id || null })
|
| 818 |
});
|
|
|
|
| 819 |
if (res.ok) {
|
| 820 |
toast.success('Member added successfully');
|
| 821 |
fetchStats();
|
| 822 |
} else {
|
| 823 |
-
const data = await res.json();
|
| 824 |
toast.error(data.message || 'Failed to add member');
|
| 825 |
}
|
| 826 |
} catch (err) {
|
|
@@ -860,9 +903,9 @@ const SuperAdminPanel = () => {
|
|
| 860 |
],
|
| 861 |
onConfirm: async (vals) => {
|
| 862 |
try {
|
| 863 |
-
const res = await
|
| 864 |
method: 'PUT',
|
| 865 |
-
headers: { '
|
| 866 |
body: JSON.stringify({ role: vals.role, org_id: vals.org_id || null })
|
| 867 |
});
|
| 868 |
if (res.ok) {
|
|
@@ -1382,14 +1425,115 @@ const SuperAdminPanel = () => {
|
|
| 1382 |
|
| 1383 |
{activeTab === 'members' && (
|
| 1384 |
<div className="flex flex-col gap-md mb-xl animate-fade-in">
|
| 1385 |
-
<div className="flex
|
| 1386 |
-
<
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1387 |
{!isSupportEngineer && (
|
| 1388 |
-
<button onClick={handleAddMember} className="flex items-center px-4 py-2 bg-primary text-white rounded-lg hover:brightness-110 transition-all font-bold text-[13.5px] border-0 cursor-pointer
|
|
|
|
|
|
|
| 1389 |
)}
|
| 1390 |
</div>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1391 |
<div className="bg-surface-container-lowest border border-outline-variant rounded-2xl overflow-hidden shadow-sm overflow-x-auto hide-scrollbar">
|
| 1392 |
-
{loading ? <div className="p-10 text-center text-on-surface-variant">Fetching users...</div> : (
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1393 |
<table className="w-full text-left text-sm border-collapse">
|
| 1394 |
<thead className="bg-surface-container text-on-surface-variant border-b border-outline-variant select-none">
|
| 1395 |
<tr>
|
|
@@ -1429,12 +1573,18 @@ const SuperAdminPanel = () => {
|
|
| 1429 |
type: 'error',
|
| 1430 |
onConfirm: async () => {
|
| 1431 |
try {
|
| 1432 |
-
await
|
| 1433 |
-
method: 'DELETE'
|
| 1434 |
-
headers: { 'Authorization': `Bearer ${localStorage.getItem('wss_token')}` }
|
| 1435 |
});
|
| 1436 |
-
|
| 1437 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1438 |
closeConfirm();
|
| 1439 |
}
|
| 1440 |
});
|
|
|
|
| 2 |
import { Link } from 'react-router-dom';
|
| 3 |
import { useAuth } from '../components/AuthContext';
|
| 4 |
import { CustomModal } from '../components/CustomModal';
|
| 5 |
+
import { Building2, Users, CreditCard, Shield, Trash2, Plus, Server, Activity, Database, HardDrive, RefreshCw, BarChart2, Edit, Download, Eye, Search, X } from 'lucide-react';
|
| 6 |
import toast from 'react-hot-toast';
|
| 7 |
|
| 8 |
const DEFAULT_BILLING_TIERS = [
|
|
|
|
| 307 |
const [sortUserCol, setSortUserCol] = useState('Email');
|
| 308 |
const [sortUserDir, setSortUserDir] = useState('asc');
|
| 309 |
|
| 310 |
+
// Filter states for Global Members
|
| 311 |
+
const [userSearch, setUserSearch] = useState('');
|
| 312 |
+
const [userRoleFilter, setUserRoleFilter] = useState('all');
|
| 313 |
+
const [userOrgFilter, setUserOrgFilter] = useState('all');
|
| 314 |
+
|
| 315 |
const [sortBillCol, setSortBillCol] = useState('Date');
|
| 316 |
const [sortBillDir, setSortBillDir] = useState('desc');
|
| 317 |
|
|
|
|
| 366 |
}
|
| 367 |
};
|
| 368 |
|
| 369 |
+
const getFilteredUsers = () => {
|
| 370 |
+
return (users || []).filter(u => {
|
| 371 |
+
if (userSearch) {
|
| 372 |
+
const q = userSearch.toLowerCase().trim();
|
| 373 |
+
const emailMatch = (u.email || '').toLowerCase().includes(q);
|
| 374 |
+
const roleMatch = (u.role || '').toLowerCase().includes(q);
|
| 375 |
+
const orgMatch = (u.org_name || '').toLowerCase().includes(q);
|
| 376 |
+
if (!emailMatch && !roleMatch && !orgMatch) return false;
|
| 377 |
+
}
|
| 378 |
+
|
| 379 |
+
if (userRoleFilter !== 'all') {
|
| 380 |
+
if ((u.role || '').toLowerCase() !== userRoleFilter.toLowerCase()) return false;
|
| 381 |
+
}
|
| 382 |
+
|
| 383 |
+
if (userOrgFilter !== 'all') {
|
| 384 |
+
if (userOrgFilter === 'no_org') {
|
| 385 |
+
if (u.org_id || (u.org_name && u.org_name !== 'No Org (Super Admin)')) return false;
|
| 386 |
+
} else {
|
| 387 |
+
if (String(u.org_id) !== String(userOrgFilter) && u.org_name !== userOrgFilter) return false;
|
| 388 |
+
}
|
| 389 |
+
}
|
| 390 |
+
|
| 391 |
+
return true;
|
| 392 |
+
});
|
| 393 |
+
};
|
| 394 |
+
|
| 395 |
const getSortedUsers = () => {
|
| 396 |
+
const filtered = getFilteredUsers();
|
| 397 |
+
return [...filtered].sort((a, b) => {
|
| 398 |
let aVal, bVal;
|
| 399 |
switch (sortUserCol) {
|
| 400 |
case 'Email': aVal = a.email || ''; bVal = b.email || ''; break;
|
|
|
|
| 408 |
});
|
| 409 |
};
|
| 410 |
|
| 411 |
+
const clearUserFilters = () => {
|
| 412 |
+
setUserSearch('');
|
| 413 |
+
setUserRoleFilter('all');
|
| 414 |
+
setUserOrgFilter('all');
|
| 415 |
+
setUserPage(1);
|
| 416 |
+
};
|
| 417 |
+
|
| 418 |
const handleBillSort = (column) => {
|
| 419 |
if (column === 'Invoice') return;
|
| 420 |
if (sortBillCol === column) {
|
|
|
|
| 849 |
{ key: 'org_id', label: 'Organization', type: 'select', options: orgOptions }
|
| 850 |
],
|
| 851 |
onConfirm: async (values) => {
|
| 852 |
+
if (!values.email || !values.email.trim()) {
|
| 853 |
+
toast.error('Please enter a valid user email');
|
| 854 |
+
return;
|
| 855 |
+
}
|
| 856 |
try {
|
| 857 |
+
const res = await authFetch('/api/auth/users', {
|
| 858 |
method: 'POST',
|
| 859 |
+
headers: { 'Content-Type': 'application/json' },
|
| 860 |
+
body: JSON.stringify({ email: values.email.trim(), role: values.role, org_id: values.org_id || null })
|
| 861 |
});
|
| 862 |
+
const data = await res.json().catch(() => ({}));
|
| 863 |
if (res.ok) {
|
| 864 |
toast.success('Member added successfully');
|
| 865 |
fetchStats();
|
| 866 |
} else {
|
|
|
|
| 867 |
toast.error(data.message || 'Failed to add member');
|
| 868 |
}
|
| 869 |
} catch (err) {
|
|
|
|
| 903 |
],
|
| 904 |
onConfirm: async (vals) => {
|
| 905 |
try {
|
| 906 |
+
const res = await authFetch(`/api/auth/users/${u.id}/role`, {
|
| 907 |
method: 'PUT',
|
| 908 |
+
headers: { 'Content-Type': 'application/json' },
|
| 909 |
body: JSON.stringify({ role: vals.role, org_id: vals.org_id || null })
|
| 910 |
});
|
| 911 |
if (res.ok) {
|
|
|
|
| 1425 |
|
| 1426 |
{activeTab === 'members' && (
|
| 1427 |
<div className="flex flex-col gap-md mb-xl animate-fade-in">
|
| 1428 |
+
<div className="flex flex-col sm:flex-row sm:items-center justify-between gap-md mb-xs">
|
| 1429 |
+
<div>
|
| 1430 |
+
<h2 className="font-headline-sm font-bold text-on-surface flex items-center text-[18px]">
|
| 1431 |
+
<Users className="w-5 h-5 text-primary mr-2" /> Global Members
|
| 1432 |
+
</h2>
|
| 1433 |
+
<p className="text-[13px] text-on-surface-variant mt-0.5">
|
| 1434 |
+
Centralized user management, role assignments, and client org mapping.
|
| 1435 |
+
</p>
|
| 1436 |
+
</div>
|
| 1437 |
{!isSupportEngineer && (
|
| 1438 |
+
<button onClick={handleAddMember} className="flex items-center px-4 py-2 bg-primary text-white rounded-lg hover:brightness-110 transition-all font-bold text-[13.5px] border-0 cursor-pointer shadow-md shadow-primary/20 shrink-0">
|
| 1439 |
+
<Plus className="w-4 h-4 mr-2" /> Add Member
|
| 1440 |
+
</button>
|
| 1441 |
)}
|
| 1442 |
</div>
|
| 1443 |
+
|
| 1444 |
+
{/* Filter & Search Bar */}
|
| 1445 |
+
<div className="bg-surface-container-lowest border border-outline-variant/70 rounded-2xl p-4 flex flex-col md:flex-row items-center justify-between gap-3 shadow-2xs">
|
| 1446 |
+
<div className="flex flex-wrap items-center gap-3 w-full flex-1">
|
| 1447 |
+
{/* Search Input */}
|
| 1448 |
+
<div className="relative w-full sm:w-72">
|
| 1449 |
+
<Search className="w-4 h-4 absolute left-3 top-1/2 -translate-y-1/2 text-on-surface-variant" />
|
| 1450 |
+
<input
|
| 1451 |
+
type="text"
|
| 1452 |
+
placeholder="Search email, org, role..."
|
| 1453 |
+
value={userSearch}
|
| 1454 |
+
onChange={(e) => {
|
| 1455 |
+
setUserSearch(e.target.value);
|
| 1456 |
+
setUserPage(1);
|
| 1457 |
+
}}
|
| 1458 |
+
className="w-full pl-9 pr-8 py-2 bg-surface border border-outline-variant/60 rounded-lg text-[13px] text-on-surface focus:outline-none focus:border-primary"
|
| 1459 |
+
/>
|
| 1460 |
+
{userSearch && (
|
| 1461 |
+
<button
|
| 1462 |
+
onClick={() => { setUserSearch(''); setUserPage(1); }}
|
| 1463 |
+
className="absolute right-2.5 top-1/2 -translate-y-1/2 text-on-surface-variant hover:text-on-surface bg-transparent border-0 cursor-pointer p-0.5"
|
| 1464 |
+
>
|
| 1465 |
+
<X className="w-3.5 h-3.5" />
|
| 1466 |
+
</button>
|
| 1467 |
+
)}
|
| 1468 |
+
</div>
|
| 1469 |
+
|
| 1470 |
+
{/* Filter by Role */}
|
| 1471 |
+
<div className="flex items-center gap-1.5 w-full sm:w-auto">
|
| 1472 |
+
<span className="text-[11px] font-bold text-on-surface-variant uppercase tracking-wider shrink-0">Role:</span>
|
| 1473 |
+
<select
|
| 1474 |
+
value={userRoleFilter}
|
| 1475 |
+
onChange={(e) => {
|
| 1476 |
+
setUserRoleFilter(e.target.value);
|
| 1477 |
+
setUserPage(1);
|
| 1478 |
+
}}
|
| 1479 |
+
className="bg-surface border border-outline-variant/60 text-on-surface text-[13px] font-medium rounded-lg px-3 py-2 focus:outline-none focus:border-primary cursor-pointer w-full sm:w-auto"
|
| 1480 |
+
>
|
| 1481 |
+
<option value="all">All Roles</option>
|
| 1482 |
+
<option value="admin">Admin</option>
|
| 1483 |
+
<option value="soc_analyst">SOC Analyst</option>
|
| 1484 |
+
<option value="org_admin">Organization Admin</option>
|
| 1485 |
+
<option value="executive_user">Executive User</option>
|
| 1486 |
+
<option value="super_admin">Super Admin</option>
|
| 1487 |
+
<option value="support_engineer">Support Engineer</option>
|
| 1488 |
+
<option value="read_only">Read Only</option>
|
| 1489 |
+
</select>
|
| 1490 |
+
</div>
|
| 1491 |
+
|
| 1492 |
+
{/* Filter by Organization */}
|
| 1493 |
+
<div className="flex items-center gap-1.5 w-full sm:w-auto">
|
| 1494 |
+
<span className="text-[11px] font-bold text-on-surface-variant uppercase tracking-wider shrink-0">Organization:</span>
|
| 1495 |
+
<select
|
| 1496 |
+
value={userOrgFilter}
|
| 1497 |
+
onChange={(e) => {
|
| 1498 |
+
setUserOrgFilter(e.target.value);
|
| 1499 |
+
setUserPage(1);
|
| 1500 |
+
}}
|
| 1501 |
+
className="bg-surface border border-outline-variant/60 text-on-surface text-[13px] font-medium rounded-lg px-3 py-2 focus:outline-none focus:border-primary cursor-pointer w-full sm:w-auto max-w-[200px] truncate"
|
| 1502 |
+
>
|
| 1503 |
+
<option value="all">All Organizations</option>
|
| 1504 |
+
<option value="no_org">No Org (Super Admin / Global)</option>
|
| 1505 |
+
{(organizations || []).map(org => (
|
| 1506 |
+
<option key={org.id} value={org.id}>{org.name}</option>
|
| 1507 |
+
))}
|
| 1508 |
+
</select>
|
| 1509 |
+
</div>
|
| 1510 |
+
|
| 1511 |
+
{/* Reset Button */}
|
| 1512 |
+
{(userSearch || userRoleFilter !== 'all' || userOrgFilter !== 'all') && (
|
| 1513 |
+
<button
|
| 1514 |
+
onClick={clearUserFilters}
|
| 1515 |
+
className="text-error hover:underline text-[12.5px] font-bold flex items-center gap-1 cursor-pointer bg-transparent border-0 px-1 ml-auto"
|
| 1516 |
+
>
|
| 1517 |
+
<X className="w-4 h-4" />
|
| 1518 |
+
Reset Filters
|
| 1519 |
+
</button>
|
| 1520 |
+
)}
|
| 1521 |
+
</div>
|
| 1522 |
+
</div>
|
| 1523 |
+
|
| 1524 |
<div className="bg-surface-container-lowest border border-outline-variant rounded-2xl overflow-hidden shadow-sm overflow-x-auto hide-scrollbar">
|
| 1525 |
+
{loading ? <div className="p-10 text-center text-on-surface-variant">Fetching users...</div> : getSortedUsers().length === 0 ? (
|
| 1526 |
+
<div className="p-12 text-center text-on-surface-variant">
|
| 1527 |
+
<Users className="w-8 h-8 text-on-surface-variant/40 mx-auto mb-2" />
|
| 1528 |
+
<p className="font-bold text-[14px] text-on-surface">No members match your filter criteria.</p>
|
| 1529 |
+
<p className="text-[12.5px] text-on-surface-variant mt-1">Try clearing your search terms or filter criteria.</p>
|
| 1530 |
+
{(userSearch || userRoleFilter !== 'all' || userOrgFilter !== 'all') && (
|
| 1531 |
+
<button onClick={clearUserFilters} className="mt-3 px-3.5 py-1.5 bg-primary/10 text-primary font-bold text-[12px] rounded-lg border border-primary/20 hover:bg-primary/20 cursor-pointer">
|
| 1532 |
+
Clear All Filters
|
| 1533 |
+
</button>
|
| 1534 |
+
)}
|
| 1535 |
+
</div>
|
| 1536 |
+
) : (
|
| 1537 |
<table className="w-full text-left text-sm border-collapse">
|
| 1538 |
<thead className="bg-surface-container text-on-surface-variant border-b border-outline-variant select-none">
|
| 1539 |
<tr>
|
|
|
|
| 1573 |
type: 'error',
|
| 1574 |
onConfirm: async () => {
|
| 1575 |
try {
|
| 1576 |
+
const res = await authFetch(`/api/auth/users/${u.id}`, {
|
| 1577 |
+
method: 'DELETE'
|
|
|
|
| 1578 |
});
|
| 1579 |
+
if (res.ok) {
|
| 1580 |
+
toast.success('Member deleted successfully');
|
| 1581 |
+
fetchStats();
|
| 1582 |
+
} else {
|
| 1583 |
+
toast.error('Failed to delete member');
|
| 1584 |
+
}
|
| 1585 |
+
} catch (err) {
|
| 1586 |
+
toast.error('Network error deleting member');
|
| 1587 |
+
}
|
| 1588 |
closeConfirm();
|
| 1589 |
}
|
| 1590 |
});
|