269 lines
14 KiB
React
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>
|
|
);
|
|
}
|