Files
multiverse/xcom-ultra/xcu-command-center/src/components/BridgeManager.jsx
T

269 lines
14 KiB
React

// [TSM.ID].[11031972] -- All Rights Reserved. Proprietary & Confidential.
// Bridge Manager — ONE-WAY PUSH Control: XCU → JUMPA
// View/Add bridge mappings, trigger sync to JUMPA system_features
import { useState, useEffect, useCallback } from 'react';
import { useModuleRegistry } from '../context/ModuleRegistryContext';
const API_BASE = import.meta.env.VITE_XCU_API_URL || '';
export default function BridgeManager() {
const { allModules, getModuleName } = useModuleRegistry();
const [mappings, setMappings] = useState([]);
const [tenants, setTenants] = useState([]);
const [loading, setLoading] = useState(true);
const [syncResult, setSyncResult] = useState(null);
const [syncing, setSyncing] = useState(false);
// Add mapping form
const [showAddForm, setShowAddForm] = useState(false);
const [newModuleId, setNewModuleId] = useState('');
const [newFeatureKey, setNewFeatureKey] = useState('');
const [isAdding, setIsAdding] = useState(false);
// Search
const [searchQuery, setSearchQuery] = useState('');
const fetchMappings = useCallback(async () => {
try {
const [mapRes, tenantRes] = await Promise.all([
fetch(`${API_BASE}/xcu-api/v1/bridge/mappings`),
fetch(`${API_BASE}/xcu-api/v1/tenants`)
]);
if (mapRes.ok) setMappings(await mapRes.json());
if (tenantRes.ok) setTenants(await tenantRes.json());
} catch (err) {
console.error('[BRIDGE] Fetch error:', err.message);
} finally {
setLoading(false);
}
}, []);
useEffect(() => { fetchMappings(); }, [fetchMappings]);
// Group mappings by module
const groupedMappings = {};
mappings.forEach(m => {
if (!groupedMappings[m.xcu_module_id]) {
groupedMappings[m.xcu_module_id] = [];
}
groupedMappings[m.xcu_module_id].push(m.jumpa_feature_key);
});
const modulesWithMappings = Object.keys(groupedMappings).length;
const totalFeaturesCovered = new Set(mappings.map(m => m.jumpa_feature_key)).size;
// Sync
const handleSync = async (tenantKey) => {
setSyncing(true);
setSyncResult(null);
try {
const res = await fetch(`${API_BASE}/xcu-api/v1/bridge/sync`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ tenant_key: tenantKey })
});
const data = await res.json();
setSyncResult(data);
} catch (err) {
setSyncResult({ error: err.message });
} finally {
setSyncing(false);
}
};
// Add mapping
const handleAddMapping = async () => {
if (!newModuleId || !newFeatureKey) return;
setIsAdding(true);
try {
await fetch(`${API_BASE}/xcu-api/v1/bridge/mappings`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ xcu_module_id: newModuleId, jumpa_feature_key: newFeatureKey })
});
setNewModuleId('');
setNewFeatureKey('');
setShowAddForm(false);
await fetchMappings();
} catch (err) {
console.error(err);
} finally {
setIsAdding(false);
}
};
// Filter
const q = searchQuery.toLowerCase();
const filteredModuleIds = Object.keys(groupedMappings)
.filter(modId => !q || modId.includes(q) || getModuleName(modId).toLowerCase().includes(q) ||
groupedMappings[modId].some(fk => fk.toLowerCase().includes(q)))
.sort((a, b) => {
const numA = parseInt(a.replace('m', ''));
const numB = parseInt(b.replace('m', ''));
return numA - numB;
});
if (loading) {
return (
<div style={{display: 'flex', alignItems: 'center', justifyContent: 'center', height: '300px', color: 'var(--accent-cyan)', fontFamily: 'monospace'}}>
LOADING BRIDGE MATRIX...
</div>
);
}
return (
<div style={{animation: 'fadeIn 0.5s ease-out'}}>
{/* Header */}
<div style={{display: 'flex', justifyContent: 'space-between', alignItems: 'flex-start', marginBottom: '30px', flexWrap: 'wrap', gap: '20px'}}>
<div>
<h2 style={{color: 'var(--text-main)', fontFamily: 'monospace', letterSpacing: '2px', marginBottom: '10px'}}>
BRIDGE CONTROL ONE-WAY PUSH
</h2>
<p style={{color: 'var(--text-muted)', fontSize: '0.9rem'}}>
XCU JUMPA.ID | Mapping modul ke system_features | JUMPA <span style={{color: 'var(--accent-red)'}}>TIDAK BISA</span> balik
</p>
</div>
<div style={{display: 'flex', gap: '10px', alignItems: 'center', flexWrap: 'wrap'}}>
<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.6rem', fontWeight: 'bold'}}>{mappings.length}</div>
<div style={{color: 'var(--text-muted)', fontSize: '0.65rem', letterSpacing: '1px'}}>MAPPINGS</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.6rem', fontWeight: 'bold'}}>{modulesWithMappings}</div>
<div style={{color: 'var(--text-muted)', fontSize: '0.65rem', letterSpacing: '1px'}}>MODULES</div>
</div>
<div style={{background: 'rgba(16, 185, 129, 0.1)', border: '1px solid var(--accent-green)', padding: '10px 20px', borderRadius: '8px', fontFamily: 'monospace', textAlign: 'center'}}>
<div style={{color: 'var(--accent-green)', fontSize: '1.6rem', fontWeight: 'bold'}}>{totalFeaturesCovered}</div>
<div style={{color: 'var(--text-muted)', fontSize: '0.65rem', letterSpacing: '1px'}}>FEATURES</div>
</div>
<button onClick={() => setShowAddForm(!showAddForm)} 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 MAPPING</button>
</div>
</div>
{/* Sync Panel */}
<div className="glass-panel" style={{
padding: '20px', marginBottom: '25px', borderLeft: '4px solid var(--accent-green)',
display: 'flex', alignItems: 'center', justifyContent: 'space-between', flexWrap: 'wrap', gap: '15px'
}}>
<div>
<h3 style={{color: 'var(--accent-green)', fontFamily: 'monospace', fontSize: '0.9rem', marginBottom: '5px'}}>
ONE-WAY PUSH SYNC
</h3>
<p style={{color: 'var(--text-muted)', fontSize: '0.8rem'}}>
Push modul aktif tenant ke JUMPA system_features. Fitur yang di-bridge = GRANTED, sisanya = UPSELL.
</p>
</div>
<div style={{display: 'flex', gap: '10px', alignItems: 'center', flexWrap: 'wrap'}}>
{tenants.map(t => (
<button key={t.tenant_key} onClick={() => handleSync(t.tenant_key)} disabled={syncing} style={{
padding: '10px 20px', background: syncing ? 'rgba(16,185,129,0.2)' : 'rgba(16,185,129,0.1)',
border: '1px solid var(--accent-green)', color: 'var(--accent-green)',
borderRadius: '6px', cursor: syncing ? 'wait' : 'pointer', fontFamily: 'monospace', fontSize: '0.8rem'
}}>
{syncing ? '⟳ SYNCING...' : `⚡ SYNC → ${t.tenant_name}`}
</button>
))}
</div>
{syncResult && (
<div style={{
width: '100%', marginTop: '10px', padding: '12px', borderRadius: '6px', fontFamily: 'monospace', fontSize: '0.8rem',
background: syncResult.error ? 'rgba(255,0,60,0.1)' : 'rgba(16,185,129,0.1)',
border: `1px solid ${syncResult.error ? 'var(--accent-red)' : 'var(--accent-green)'}`,
color: syncResult.error ? 'var(--accent-red)' : 'var(--accent-green)'
}}>
{syncResult.error
? `ERROR: ${syncResult.error}`
: `✓ BRIDGE SYNC COMPLETE — ${syncResult.granted} GRANTED / ${syncResult.disabled} DISABLED`}
</div>
)}
</div>
{/* Add Mapping Form */}
{showAddForm && (
<div className="glass-panel" style={{padding: '25px', marginBottom: '25px', borderLeft: '4px solid var(--accent-cyan)', animation: 'fadeIn 0.3s ease-out'}}>
<h3 style={{color: 'var(--accent-cyan)', marginBottom: '15px', fontFamily: 'monospace'}}>NEW BRIDGE MAPPING</h3>
<div style={{display: 'flex', gap: '15px', flexWrap: 'wrap', alignItems: 'flex-end'}}>
<div style={{flex: 1, minWidth: '200px'}}>
<label style={{color: 'var(--text-muted)', fontSize: '0.75rem', display: 'block', marginBottom: '5px'}}>XCU MODULE</label>
<select value={newModuleId} onChange={e => setNewModuleId(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'}}>
<option value="">-- Select Module --</option>
{allModules.map(m => <option key={m.id} value={m.id}>{m.id} {m.name}</option>)}
</select>
</div>
<div style={{flex: 1, minWidth: '200px'}}>
<label style={{color: 'var(--text-muted)', fontSize: '0.75rem', display: 'block', marginBottom: '5px'}}>JUMPA FEATURE KEY</label>
<input value={newFeatureKey} onChange={e => setNewFeatureKey(e.target.value)} placeholder="jvc.feature.recording"
style={{width: '100%', padding: '10px', background: 'var(--hover-bg)', border: '1px solid var(--table-border)', color: 'var(--text-main)', borderRadius: '6px', fontFamily: 'monospace'}} />
</div>
<button onClick={handleAddMapping} disabled={isAdding || !newModuleId || !newFeatureKey} style={{
padding: '10px 25px', background: 'var(--accent-cyan)', color: '#000', border: 'none',
borderRadius: '6px', cursor: 'pointer', fontFamily: 'monospace', fontWeight: 'bold',
opacity: (!newModuleId || !newFeatureKey || isAdding) ? 0.5 : 1
}}>{isAdding ? 'ADDING...' : 'ADD MAPPING'}</button>
</div>
</div>
)}
{/* Search */}
<div style={{marginBottom: '20px'}}>
<input value={searchQuery} onChange={e => setSearchQuery(e.target.value)}
placeholder="Search by module ID, name, or feature key..."
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'}} />
</div>
{/* Mapping Table */}
<div className="glass-panel" style={{padding: '0', overflow: 'hidden'}}>
<table style={{width: '100%', borderCollapse: 'collapse'}}>
<thead>
<tr style={{background: 'var(--hover-bg)'}}>
<th style={{padding: '12px 15px', textAlign: 'left', color: 'var(--accent-cyan)', fontFamily: 'monospace', fontSize: '0.75rem', letterSpacing: '1px', borderBottom: '1px solid var(--table-border)'}}>XCU MODULE</th>
<th style={{padding: '12px 15px', textAlign: 'left', color: 'var(--accent-purple)', fontFamily: 'monospace', fontSize: '0.75rem', letterSpacing: '1px', borderBottom: '1px solid var(--table-border)'}}>JUMPA FEATURE KEYS</th>
<th style={{padding: '12px 15px', textAlign: 'center', color: 'var(--text-muted)', fontFamily: 'monospace', fontSize: '0.75rem', borderBottom: '1px solid var(--table-border)', width: '80px'}}>COUNT</th>
</tr>
</thead>
<tbody>
{filteredModuleIds.map((modId, i) => (
<tr key={modId} style={{borderBottom: '1px solid var(--table-border)', background: i % 2 === 0 ? 'transparent' : 'rgba(255,255,255,0.01)'}}>
<td style={{padding: '10px 15px', verticalAlign: 'top'}}>
<div style={{color: 'var(--accent-cyan)', fontFamily: 'monospace', fontWeight: 'bold', fontSize: '0.85rem'}}>{modId}</div>
<div style={{color: 'var(--text-muted)', fontSize: '0.75rem'}}>{getModuleName(modId)}</div>
</td>
<td style={{padding: '10px 15px'}}>
<div style={{display: 'flex', flexWrap: 'wrap', gap: '4px'}}>
{groupedMappings[modId].map(fk => {
const prefixColor = fk.startsWith('jvc.') ? '#a855f7' :
fk.startsWith('jc.') ? '#10b981' :
fk.startsWith('xtm.') ? '#f97316' :
fk.startsWith('iam.') ? '#ffea00' :
fk.startsWith('xcu.') ? '#00f3ff' : '#888';
return (
<span key={fk} style={{
background: `${prefixColor}15`, border: `1px solid ${prefixColor}40`,
color: prefixColor, padding: '2px 8px', borderRadius: '4px',
fontFamily: 'monospace', fontSize: '0.7rem'
}}>{fk}</span>
);
})}
</div>
</td>
<td style={{padding: '10px 15px', textAlign: 'center', color: 'var(--text-muted)', fontFamily: 'monospace'}}>
{groupedMappings[modId].length}
</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
);
}