[TSM.ID].[11031972] PXE : Platform X Ecosystem I [118 Module -LIVE-]
This commit is contained in:
@@ -0,0 +1,246 @@
|
||||
// [TSM.ID].[11031972] -- All Rights Reserved. Proprietary & Confidential.
|
||||
// Module Manager — Supreme Admin CRUD for XCU Module Registry
|
||||
// Dynamic: Add, Deactivate, Search modules. Data from xcu_iam PostgreSQL.
|
||||
|
||||
import { useState } from 'react';
|
||||
import { useModuleRegistry } from '../context/ModuleRegistryContext';
|
||||
import DangerModal from './DangerModal';
|
||||
|
||||
export default function ModuleManager() {
|
||||
const { registry, allModules, totalModules, loading, source, addModule, deleteModule, refreshRegistry } = useModuleRegistry();
|
||||
|
||||
const [showAddForm, setShowAddForm] = useState(false);
|
||||
const [searchQuery, setSearchQuery] = useState('');
|
||||
const [deleteTarget, setDeleteTarget] = useState(null);
|
||||
|
||||
// Add module form state
|
||||
const [newId, setNewId] = useState('');
|
||||
const [newName, setNewName] = useState('');
|
||||
const [newGroupId, setNewGroupId] = useState('group1');
|
||||
const [newGroupName, setNewGroupName] = useState('');
|
||||
const [newSortOrder, setNewSortOrder] = useState(100);
|
||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||
|
||||
// Derive available groups
|
||||
const groups = Object.entries(registry).map(([key, val]) => ({
|
||||
id: key,
|
||||
name: val.name
|
||||
}));
|
||||
|
||||
const handleAddModule = async () => {
|
||||
if (!newId || !newName) return;
|
||||
setIsSubmitting(true);
|
||||
|
||||
const groupName = newGroupName || (groups.find(g => g.id === newGroupId)?.name || newGroupId);
|
||||
const success = await addModule({
|
||||
id: newId,
|
||||
name: newName,
|
||||
group_id: newGroupId,
|
||||
group_name: groupName,
|
||||
sort_order: Number(newSortOrder)
|
||||
});
|
||||
|
||||
if (success) {
|
||||
setNewId('');
|
||||
setNewName('');
|
||||
setNewSortOrder(totalModules + 1);
|
||||
setNewGroupName('');
|
||||
setShowAddForm(false);
|
||||
}
|
||||
setIsSubmitting(false);
|
||||
};
|
||||
|
||||
const handleDeleteConfirm = async () => {
|
||||
if (!deleteTarget) return;
|
||||
await deleteModule(deleteTarget);
|
||||
setDeleteTarget(null);
|
||||
};
|
||||
|
||||
// Filter modules
|
||||
const q = searchQuery.toLowerCase();
|
||||
const filteredGroups = Object.entries(registry).map(([key, group]) => ({
|
||||
key,
|
||||
name: group.name,
|
||||
modules: (group.modules || []).filter(m =>
|
||||
!q || m.id.toLowerCase().includes(q) || m.name.toLowerCase().includes(q)
|
||||
)
|
||||
})).filter(g => g.modules.length > 0);
|
||||
|
||||
const filteredCount = filteredGroups.reduce((sum, g) => sum + g.modules.length, 0);
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div style={{display: 'flex', alignItems: 'center', justifyContent: 'center', height: '300px', color: 'var(--accent-cyan)', fontFamily: 'monospace'}}>
|
||||
LOADING SOVEREIGN MODULE REGISTRY...
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div style={{animation: 'fadeIn 0.5s ease-out'}}>
|
||||
<DangerModal
|
||||
isOpen={!!deleteTarget}
|
||||
title="DEACTIVATE MODULE"
|
||||
message={`Anda akan menonaktifkan modul ${deleteTarget}. Modul tidak dihapus permanen, tapi tidak akan muncul di registry aktif. Lanjut?`}
|
||||
onConfirm={handleDeleteConfirm}
|
||||
onCancel={() => setDeleteTarget(null)}
|
||||
confirmText="DEACTIVATE MODULE"
|
||||
/>
|
||||
|
||||
{/* Header */}
|
||||
<div style={{display: 'flex', justifyContent: 'space-between', alignItems: 'flex-start', marginBottom: '30px', gap: '20px', flexWrap: 'wrap'}}>
|
||||
<div>
|
||||
<h2 style={{color: 'var(--text-main)', fontFamily: 'monospace', letterSpacing: '2px', marginBottom: '10px'}}>
|
||||
SOVEREIGN MODULE REGISTRY
|
||||
</h2>
|
||||
<p style={{color: 'var(--text-muted)', fontSize: '0.9rem'}}>
|
||||
Dynamic Module CRUD — Source: <span style={{color: source === 'API' ? 'var(--accent-green)' : 'var(--accent-yellow)', fontWeight: 'bold'}}>{source === 'API' ? 'xcu_iam PostgreSQL' : 'Static Fallback'}</span>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div style={{display: 'flex', gap: '10px', alignItems: 'center'}}>
|
||||
{/* Stats */}
|
||||
<div style={{
|
||||
background: 'rgba(0, 243, 255, 0.1)', border: '1px solid var(--accent-cyan)',
|
||||
padding: '10px 20px', borderRadius: '8px', fontFamily: 'monospace', textAlign: 'center'
|
||||
}}>
|
||||
<div style={{color: 'var(--accent-cyan)', fontSize: '1.8rem', fontWeight: 'bold'}}>{totalModules}</div>
|
||||
<div style={{color: 'var(--text-muted)', fontSize: '0.7rem', letterSpacing: '1px'}}>ACTIVE MODULES</div>
|
||||
</div>
|
||||
<div style={{
|
||||
background: 'rgba(168, 85, 247, 0.1)', border: '1px solid var(--accent-purple)',
|
||||
padding: '10px 20px', borderRadius: '8px', fontFamily: 'monospace', textAlign: 'center'
|
||||
}}>
|
||||
<div style={{color: 'var(--accent-purple)', fontSize: '1.8rem', fontWeight: 'bold'}}>{Object.keys(registry).length}</div>
|
||||
<div style={{color: 'var(--text-muted)', fontSize: '0.7rem', letterSpacing: '1px'}}>GROUPS</div>
|
||||
</div>
|
||||
|
||||
<button onClick={refreshRegistry} style={{
|
||||
padding: '10px 20px', background: 'transparent', border: '1px solid var(--accent-green)',
|
||||
color: 'var(--accent-green)', borderRadius: '8px', cursor: 'pointer', fontFamily: 'monospace'
|
||||
}}>↻ REFRESH</button>
|
||||
|
||||
<button onClick={() => { setShowAddForm(!showAddForm); setNewSortOrder(totalModules + 1); setNewId(`m${totalModules + 1}`); }} style={{
|
||||
padding: '10px 20px', background: showAddForm ? 'var(--accent-cyan)' : 'transparent',
|
||||
border: '1px solid var(--accent-cyan)', borderRadius: '8px', cursor: 'pointer', fontFamily: 'monospace',
|
||||
color: showAddForm ? '#000' : 'var(--accent-cyan)', fontWeight: 'bold'
|
||||
}}>+ ADD MODULE</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Add Module Form */}
|
||||
{showAddForm && (
|
||||
<div className="glass-panel" style={{
|
||||
padding: '25px', marginBottom: '30px', borderLeft: '4px solid var(--accent-cyan)',
|
||||
animation: 'fadeIn 0.3s ease-out'
|
||||
}}>
|
||||
<h3 style={{color: 'var(--accent-cyan)', marginBottom: '20px', fontFamily: 'monospace'}}>FORGE NEW MODULE</h3>
|
||||
<div style={{display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(200px, 1fr))', gap: '15px'}}>
|
||||
<div>
|
||||
<label style={{color: 'var(--text-muted)', fontSize: '0.75rem', display: 'block', marginBottom: '5px'}}>MODULE ID</label>
|
||||
<input value={newId} onChange={e => setNewId(e.target.value)} placeholder="m100"
|
||||
style={{width: '100%', padding: '10px', background: 'var(--hover-bg)', border: '1px solid var(--table-border)', color: 'var(--text-main)', borderRadius: '6px', fontFamily: 'monospace'}} />
|
||||
</div>
|
||||
<div>
|
||||
<label style={{color: 'var(--text-muted)', fontSize: '0.75rem', display: 'block', marginBottom: '5px'}}>MODULE NAME</label>
|
||||
<input value={newName} onChange={e => setNewName(e.target.value)} placeholder="Quantum Flux Capacitor"
|
||||
style={{width: '100%', padding: '10px', background: 'var(--hover-bg)', border: '1px solid var(--table-border)', color: 'var(--text-main)', borderRadius: '6px', fontFamily: 'monospace'}} />
|
||||
</div>
|
||||
<div>
|
||||
<label style={{color: 'var(--text-muted)', fontSize: '0.75rem', display: 'block', marginBottom: '5px'}}>GROUP</label>
|
||||
<select value={newGroupId} onChange={e => setNewGroupId(e.target.value)}
|
||||
style={{width: '100%', padding: '10px', background: 'var(--hover-bg)', border: '1px solid var(--table-border)', color: 'var(--text-main)', borderRadius: '6px', fontFamily: 'monospace'}}>
|
||||
{groups.map(g => <option key={g.id} value={g.id}>{g.name}</option>)}
|
||||
<option value="new">+ NEW GROUP</option>
|
||||
</select>
|
||||
</div>
|
||||
{newGroupId === 'new' && (
|
||||
<div>
|
||||
<label style={{color: 'var(--text-muted)', fontSize: '0.75rem', display: 'block', marginBottom: '5px'}}>NEW GROUP NAME</label>
|
||||
<input value={newGroupName} onChange={e => setNewGroupName(e.target.value)} placeholder="KELOMPOK IX: ..."
|
||||
style={{width: '100%', padding: '10px', background: 'var(--hover-bg)', border: '1px solid var(--table-border)', color: 'var(--text-main)', borderRadius: '6px', fontFamily: 'monospace'}} />
|
||||
</div>
|
||||
)}
|
||||
<div>
|
||||
<label style={{color: 'var(--text-muted)', fontSize: '0.75rem', display: 'block', marginBottom: '5px'}}>SORT ORDER</label>
|
||||
<input type="number" value={newSortOrder} onChange={e => setNewSortOrder(e.target.value)}
|
||||
style={{width: '100%', padding: '10px', background: 'var(--hover-bg)', border: '1px solid var(--table-border)', color: 'var(--text-main)', borderRadius: '6px', fontFamily: 'monospace'}} />
|
||||
</div>
|
||||
</div>
|
||||
<div style={{marginTop: '20px', display: 'flex', gap: '10px'}}>
|
||||
<button onClick={handleAddModule} disabled={isSubmitting || !newId || !newName} style={{
|
||||
padding: '10px 30px', background: 'var(--accent-cyan)', color: '#000', border: 'none',
|
||||
borderRadius: '6px', cursor: 'pointer', fontFamily: 'monospace', fontWeight: 'bold',
|
||||
opacity: (!newId || !newName || isSubmitting) ? 0.5 : 1
|
||||
}}>{isSubmitting ? 'FORGING...' : 'FORGE MODULE'}</button>
|
||||
<button onClick={() => setShowAddForm(false)} style={{
|
||||
padding: '10px 30px', background: 'transparent', color: 'var(--text-muted)',
|
||||
border: '1px solid var(--table-border)', borderRadius: '6px', cursor: 'pointer', fontFamily: 'monospace'
|
||||
}}>CANCEL</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Search */}
|
||||
<div style={{marginBottom: '20px', position: 'relative'}}>
|
||||
<input
|
||||
value={searchQuery} onChange={e => setSearchQuery(e.target.value)}
|
||||
placeholder="Search modules by ID or name..."
|
||||
style={{
|
||||
width: '100%', padding: '12px 20px', background: 'var(--hover-bg)',
|
||||
border: '1px solid var(--table-border)', color: 'var(--text-main)',
|
||||
borderRadius: '8px', fontFamily: 'monospace', fontSize: '0.9rem', outline: 'none'
|
||||
}}
|
||||
/>
|
||||
{searchQuery && (
|
||||
<span style={{position: 'absolute', right: '15px', top: '12px', color: 'var(--text-muted)', fontFamily: 'monospace', fontSize: '0.8rem'}}>
|
||||
{filteredCount} / {totalModules}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Module Grid */}
|
||||
<div style={{display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(450px, 1fr))', gap: '20px'}}>
|
||||
{filteredGroups.map(group => {
|
||||
const groupColors = {
|
||||
group1: '#00f3ff', group2: '#ffea00', group3: '#ff003c', group4: '#a855f7',
|
||||
group5: '#10b981', group6: '#3b82f6', group7: '#f97316', group8: '#ef4444'
|
||||
};
|
||||
const color = groupColors[group.key] || '#00f3ff';
|
||||
|
||||
return (
|
||||
<div key={group.key} className="glass-panel" style={{
|
||||
padding: '20px', borderTop: `3px solid ${color}`,
|
||||
background: 'var(--panel-bg)', borderRadius: '10px'
|
||||
}}>
|
||||
<h3 style={{color, fontSize: '0.85rem', marginBottom: '15px', fontFamily: 'monospace', letterSpacing: '1px',
|
||||
borderBottom: '1px solid var(--table-border)', paddingBottom: '10px', display: 'flex', justifyContent: 'space-between'}}>
|
||||
<span>{group.name}</span>
|
||||
<span style={{color: 'var(--text-muted)'}}>[{group.modules.length}]</span>
|
||||
</h3>
|
||||
<div style={{display: 'flex', flexDirection: 'column', gap: '6px', maxHeight: '400px', overflowY: 'auto', paddingRight: '5px'}} className="custom-scrollbar">
|
||||
{group.modules.map(mod => (
|
||||
<div key={mod.id} style={{
|
||||
display: 'flex', justifyContent: 'space-between', alignItems: 'center',
|
||||
padding: '8px 12px', background: 'var(--hover-bg)', borderRadius: '6px',
|
||||
border: '1px solid var(--table-border)', transition: 'all 0.2s'
|
||||
}}>
|
||||
<div>
|
||||
<span style={{color: 'var(--text-main)', fontSize: '0.85rem'}}>{mod.name}</span>
|
||||
<span style={{color: 'var(--text-muted)', fontSize: '0.7rem', marginLeft: '8px', fontFamily: 'monospace'}}>{mod.id}</span>
|
||||
</div>
|
||||
<button onClick={() => setDeleteTarget(mod.id)} title="Deactivate module" style={{
|
||||
background: 'transparent', border: '1px solid rgba(255,0,60,0.3)', color: 'var(--accent-red)',
|
||||
padding: '4px 10px', borderRadius: '4px', cursor: 'pointer', fontFamily: 'monospace', fontSize: '0.7rem',
|
||||
transition: 'all 0.2s'
|
||||
}}>✕</button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user