// [TSM.ID].[11031972] -- All Rights Reserved. Proprietary & Confidential. import { useState, useEffect } from 'react'; import { useAuth } from '../context/AuthContext'; import { useCommercial } from '../context/CommercialContext'; import DangerModal from './DangerModal'; import { useModuleRegistry } from '../context/ModuleRegistryContext'; function useDebounce(value, delay) { const [debouncedValue, setDebouncedValue] = useState(value); useEffect(() => { const handler = setTimeout(() => { setDebouncedValue(value); }, delay); return () => { clearTimeout(handler); }; }, [value, delay]); return debouncedValue; } export default function TenantArchitect() { const { tiers } = useCommercial(); const { registry } = useModuleRegistry(); const API_BASE = import.meta.env.VITE_XCU_API_URL || ''; // Load Tenants dari API Gateway (xcu_iam PostgreSQL) const [tenants, setTenants] = useState([]); const [isLoading, setIsLoading] = useState(true); const [isProvisioning, setIsProvisioning] = useState(false); const [apiError, setApiError] = useState(null); const { cryptographicKey } = useAuth(); // State for Form (Create/Edit) const [editingId, setEditingId] = useState(null); const [newTenantName, setNewTenantName] = useState(''); const [customTierName, setCustomTierName] = useState(''); const [selectedModules, setSelectedModules] = useState({}); const [showProvisionForm, setShowProvisionForm] = useState(false); const handleAutoFill = (tierId) => { const selectedTier = tiers.find(t => t.id === tierId); if (!selectedTier) return; // Convert array of modules to object { m1: true, m2: true } const newSelection = {}; selectedTier.modules.forEach(mod => { newSelection[mod] = true; }); setSelectedModules(newSelection); }; // Omni-Dimensional Search States const [moduleSearchQuery, setModuleSearchQuery] = useState(''); const debouncedModuleQuery = useDebounce(moduleSearchQuery, 250); const [tenantSearchQuery, setTenantSearchQuery] = useState(''); const debouncedTenantQuery = useDebounce(tenantSearchQuery, 250); // Fetch tenants dari API Gateway const fetchTenants = async () => { try { const res = await fetch(`${API_BASE}/xcu-api/v1/tenants`); if (!res.ok) throw new Error(`HTTP ${res.status}`); const data = await res.json(); // Transform API data to component format const mapped = data.map(t => ({ id: t.tenant_key || t.id, name: t.tenant_name || t.name, status: t.status || 'PROVISIONED', baseTierId: t.base_tier_id || t.baseTierId || null, customModules: t.custom_modules || t.customModules || [], customTierName: t.custom_tier_name || t.customTierName || '', created: t.created_at ? t.created_at.split('T')[0] : (t.created || new Date().toISOString().split('T')[0]) })); setTenants(mapped); setApiError(null); console.log(`[XCU TENANT] Loaded ${mapped.length} tenants from API`); } catch (err) { console.warn('[XCU TENANT] API offline, using fallback:', err.message); setApiError(err.message); // Fallback: seed JUMPA.ID jika API offline if (tenants.length === 0) { setTenants([{ id: 'AEGIS-TENANT-80AA3EEBE8-X', name: 'JUMPA.ID ENTERPRISE', status: 'PROVISIONED', baseTierId: 'tier_jumpa_omni', customModules: [], customTierName: '', created: '2026-05-08' }]); } } finally { setIsLoading(false); } }; useEffect(() => { fetchTenants(); }, []); // Fungsi Pembantu: Dapatkan Modul Asli untuk Tenant (Auto-Sync dengan Blueprint) const resolveTenantModules = (tenant) => { if (tenant.baseTierId) { const tier = tiers.find(t => t.id === tenant.baseTierId); if (tier) return tier.modules; } return tenant.customModules || []; }; const resolveTierBadge = (tenant) => { const activeModules = resolveTenantModules(tenant); if (!activeModules || activeModules.length === 0) return { name: 'BLIND (0 MODUL)', color: 'var(--accent-red)' }; if (tenant.baseTierId) { const tierIndex = tiers.findIndex(t => t.id === tenant.baseTierId); if (tierIndex !== -1) { let themeColor = 'var(--accent-cyan)'; if (tierIndex === 1) themeColor = 'var(--accent-yellow)'; if (tierIndex === 2) themeColor = 'var(--accent-red)'; if (tierIndex === 3) themeColor = 'var(--accent-green)'; if (tierIndex >= 4) themeColor = 'var(--text-main)'; return { name: tiers[tierIndex].name, color: themeColor }; } } const finalName = tenant.customTierName ? `CUSTOM FORGE: ${tenant.customTierName}` : 'CUSTOM FORGE'; return { name: finalName, color: 'var(--accent-purple)' }; }; const toggleModule = (moduleId) => { setSelectedModules(prev => ({ ...prev, [moduleId]: !prev[moduleId] })); }; const handleEditClick = (tenant) => { setEditingId(tenant.id); setNewTenantName(tenant.name); setCustomTierName(tenant.customTierName || ''); setShowProvisionForm(true); // Konversi array modul kembali ke object untuk state const modState = {}; tenant.modules.forEach(m => modState[m] = true); setSelectedModules(modState); // Auto-scroll ke atas window.scrollTo({ top: 0, behavior: 'smooth' }); }; const [selectedTenants, setSelectedTenants] = useState([]); const [deleteModal, setDeleteModal] = useState({ isOpen: false, tenantIds: [] }); const handleDeleteClick = (tenantId) => { setDeleteModal({ isOpen: true, tenantIds: [tenantId] }); }; const handleBulkDeleteClick = () => { setDeleteModal({ isOpen: true, tenantIds: selectedTenants }); }; const toggleSelectTenant = (id) => { setSelectedTenants(prev => prev.includes(id) ? prev.filter(tid => tid !== id) : [...prev, id]); }; const confirmDelete = async () => { if (deleteModal.tenantIds.length > 0) { // Delete via API for each tenant for (const tenantId of deleteModal.tenantIds) { try { await fetch(`${API_BASE}/xcu-api/v1/tenants/${encodeURIComponent(tenantId)}`, { method: 'DELETE' }); } catch (err) { console.error('[XCU TENANT] Delete error:', err.message); } } // Refresh from API await fetchTenants(); if (deleteModal.tenantIds.includes(editingId)) { cancelEdit(); } setSelectedTenants([]); } setDeleteModal({ isOpen: false, tenantIds: [] }); }; const cancelDelete = () => { setDeleteModal({ isOpen: false, tenantIds: [] }); }; const cancelEdit = () => { setEditingId(null); setNewTenantName(''); setCustomTierName(''); setSelectedModules({}); setShowProvisionForm(false); }; const saveTenant = async () => { if (!newTenantName) return alert("Nama Tenant tidak boleh kosong."); setIsProvisioning(true); // Konversi object { id: true/false } menjadi array of allowed IDs const allowedModules = Object.keys(selectedModules).filter(id => selectedModules[id]); // Check if the allowedModules matches any Tier exactly let matchedTierId = null; const sortedAllowed = [...allowedModules].sort().join(','); for (const tier of tiers) { if ([...tier.modules].sort().join(',') === sortedAllowed) { matchedTierId = tier.id; break; } } try { if (editingId) { // UPDATE via API await fetch(`${API_BASE}/xcu-api/v1/tenants/${encodeURIComponent(editingId)}`, { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ tenant_name: newTenantName, base_tier_id: matchedTierId, custom_modules: matchedTierId ? [] : allowedModules, custom_tier_name: matchedTierId ? '' : customTierName }) }); } else { // CREATE via API const tenantKey = 'AEGIS-TENANT-' + newTenantName.replace(/[^A-Z0-9]/gi, '').toUpperCase().slice(0, 10) + '-' + Math.random().toString(16).substr(2, 4).toUpperCase(); await fetch(`${API_BASE}/xcu-api/v1/tenants`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ tenant_key: tenantKey, tenant_name: newTenantName, base_tier_id: matchedTierId, custom_modules: matchedTierId ? [] : allowedModules, custom_tier_name: matchedTierId ? '' : customTierName }) }); } // Refresh from API await fetchTenants(); } catch (err) { console.error('[XCU TENANT] Save error:', err.message); setApiError(err.message); } cancelEdit(); setIsProvisioning(false); }; const isEditMode = editingId !== null; const borderColor = isEditMode ? 'var(--accent-yellow)' : 'rgba(0, 243, 255, 0.2)'; const themeColor = isEditMode ? 'var(--accent-yellow)' : 'var(--accent-cyan)'; const filteredTenants = tenants.filter(t => { if (!debouncedTenantQuery) return true; const q = debouncedTenantQuery.toLowerCase(); if (t.name.toLowerCase().includes(q) || t.id.toLowerCase().includes(q)) return true; // Omni-Search: Cek nama modul aslinya dari Registry const activeModules = resolveTenantModules(t); return activeModules.some(modId => { for (let coreKey in registry) { const found = registry[coreKey].modules.find(m => m.id === modId); if (found && found.name.toLowerCase().includes(q)) return true; } return false; }); }); const handleSelectAll = () => { if (selectedTenants.length === filteredTenants.length && filteredTenants.length > 0) { setSelectedTenants([]); } else { setSelectedTenants(filteredTenants.map(t => t.id)); } }; const handleGenerateTether = (tenant) => { // Generate Quantum Tether (.xcu) file const tetherData = { tenantId: tenant.id, tenantName: tenant.name, baseTierId: tenant.baseTierId, customModules: tenant.customModules, endpoint: "wss://xcu ULTRA.ultramodul.xyz/quantum-relay", signature: btoa(tenant.id + "-" + Date.now()) }; const blob = new Blob([JSON.stringify(tetherData, null, 2)], { type: "application/json" }); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = `${tenant.id.toLowerCase()}.xcu`; document.body.appendChild(a); a.click(); document.body.removeChild(a); URL.revokeObjectURL(url); }; return (
Source: {apiError ? 'Static Fallback' : 'xcu_iam PostgreSQL'} | {tenants.length} Tenants
| 0} style={{cursor: 'pointer', accentColor: 'var(--accent-red)'}} /> | Cryptographic Key ID | Tenant Name | Active Modules | Override |
|---|---|---|---|---|
| Tidak ada Entitas Klien yang cocok dengan pencarian. | ||||
| toggleSelectTenant(t.id)} style={{cursor: 'pointer', accentColor: 'var(--accent-red)'}} /> | {t.id} |
{t.name}
{badge.name}
|
{activeModules && activeModules.length > 0 ? (
{activeModules.map(m => {
let modName = m;
for (let coreKey in registry) {
const found = registry[coreKey].modules.find(x => x.id === m);
if (found) modName = found.name;
}
const isMatch = debouncedTenantQuery && modName.toLowerCase().includes(debouncedTenantQuery.toLowerCase());
return (
{modName}
);
})}
) : BLIND (0 MODUL)}
|
|