diff --git a/index.js b/index.js index ad175ed..f3630c2 100644 --- a/index.js +++ b/index.js @@ -3196,10 +3196,490 @@ function displayJsonData(jsonData, type) { quotesOnKeys: false, quotesOnValues: false }; - + // Show JSON data in SweetAlert popup outputAlert(jsonData, type); } +window.displayJsonData = displayJsonData; + +// ========== RICH GUI DISPLAY FUNCTIONS FOR INFRA DATA ========== + +// Returns a colored status badge HTML span +function getInfraStatusBadge(status) { + const s = (status || '').toLowerCase(); + let color = '#6c757d', icon = 'โšช'; + if (s === 'running') { color = '#28a745'; icon = '๐ŸŸข'; } + else if (s === 'failed' || s.includes('fail')) { color = '#dc3545'; icon = '๐Ÿ”ด'; } + else if (s === 'preparing' || s === 'prepared' || s.includes('provision')) { color = '#ffc107'; icon = '๐ŸŸก'; } + else if (s === 'terminated' || s === 'deleted') { color = '#6c757d'; icon = 'โšซ'; } + return `${icon} ${window.escapeHtml ? window.escapeHtml(status || 'Unknown') : (status || 'Unknown')}`; +} + +// Stores SSH commands indexed by position so onclick can reference them safely +window._guiSshCommands = []; +window.copyGuiSshCommand = function(idx) { + const cmd = window._guiSshCommands[idx] || ''; + navigator.clipboard.writeText(cmd).then(() => { + const el = document.getElementById('gui-ssh-cmd-' + idx); + if (el) { const orig = el.style.background; el.style.background = '#155724'; setTimeout(() => el.style.background = orig, 1500); } + }).catch(err => console.error('copy failed:', err)); +}; + +// Builds the node-by-nodeGroup summary HTML block (shared by status + dynamic result views) +function buildInfraNodeSummaryHtml(data) { + const esc = window.escapeHtml || (s => String(s)); + const nodes = data.node || []; + + // Group nodes by nodeGroupId + const groups = {}; + nodes.forEach(nd => { + const gid = nd.nodeGroupId || 'default'; + if (!groups[gid]) groups[gid] = []; + groups[gid].push(nd); + }); + const groupCount = Object.keys(groups).length; + + // Unique providers + const providers = [...new Set(nodes.map(nd => + nd.connectionConfig?.providerName || nd.connectionName?.split('-')[0] || null + ).filter(Boolean))]; + + // Node status counts + const sc = data.statusCount || {}; + const runningCount = sc.countRunning ?? nodes.filter(nd => (nd.status || '').toLowerCase() === 'running').length; + const failedCount = sc.countFailed ?? nodes.filter(nd => (nd.status || '').toLowerCase().includes('fail')).length; + const totalCount = sc.countTotal ?? nodes.length; + + // Estimated hourly cost + let totalCost = 0, hasCost = false; + nodes.forEach(nd => { if (nd.spec?.costPerHour > 0) { totalCost += nd.spec.costPerHour; hasCost = true; } }); + + // newNodeList set for quick lookup + const newNodeSet = new Set(data.newNodeList || []); + + // โ”€โ”€ Summary cards โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + const nodeColor = failedCount > 0 ? '#dc3545' : (runningCount === totalCount && totalCount > 0 ? '#28a745' : '#ffc107'); + const hasActiveAction = data.targetAction && data.targetAction !== 'None' && data.targetAction !== ''; + + let html = `
`; + html += ` +
+
${runningCount}/${totalCount}
+
Running Nodes
+
+
+
${groupCount}
+
NodeGroups
+
+
+
${providers.length ? providers.map(p => esc(p)).join('
') : '-'}
+
Providers
+
+
+
${hasCost ? '$' + totalCost.toFixed(4) : '-'}
+
Est. Cost/hr
+
+
+ ${getInfraStatusBadge(data.status)} + ${hasActiveAction ? `
โณ ${esc(data.targetAction)}${data.targetStatus ? ' โ†’ ' + esc(data.targetStatus) : ''}
` : ''} +
Status
+
`; + html += `
`; + + // โ”€โ”€ Status distribution pills โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + if (totalCount > 0 && (sc.countFailed > 0 || sc.countCreating > 0 || sc.countSuspended > 0 || sc.countTerminated > 0 || sc.countUndefined > 0)) { + const pills = [ + { label: 'Running', count: sc.countRunning || 0, color: '#28a745' }, + { label: 'Creating', count: sc.countCreating || 0, color: '#17a2b8' }, + { label: 'Failed', count: sc.countFailed || 0, color: '#dc3545' }, + { label: 'Suspended', count: sc.countSuspended || 0, color: '#6c757d' }, + { label: 'Terminated', count: sc.countTerminated || 0, color: '#495057' }, + { label: 'Undefined', count: sc.countUndefined || 0, color: '#856404' }, + ].filter(p => p.count > 0); + html += `
`; + pills.forEach(p => { html += `${p.label}: ${p.count}`; }); + html += `
`; + } + + // โ”€โ”€ creationErrors block โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + const ce = data.creationErrors; + if (ce && (ce.failedNodeCount > 0 || (ce.nodeCreationErrors || []).length > 0 || (ce.nodeObjectCreationErrors || []).length > 0)) { + const allErrors = [...(ce.nodeObjectCreationErrors || []), ...(ce.nodeCreationErrors || [])]; + html += ` +
+
+ โŒ Creation Errors โ€” ${ce.failedNodeCount || allErrors.length} failed / ${ce.totalNodeCount || totalCount} total + ${ce.failureHandlingStrategy ? `(strategy: ${esc(ce.failureHandlingStrategy)})` : ''} +
+ ${allErrors.length > 0 ? ` + + + + + + + ${allErrors.map(e => ` + + + + + + `).join('')} + +
NodePhaseErrorTime
${esc(e.nodeName || '-')}${esc(e.phase || '-')}${esc(e.error || '-')}${esc((e.timestamp || '').split('T')[0] || e.timestamp || '-')}
` : ''} +
`; + } + + // โ”€โ”€ Infra-level systemMessage โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + const sysMessages = Array.isArray(data.systemMessage) ? data.systemMessage.filter(Boolean) : (data.systemMessage ? [data.systemMessage] : []); + if (sysMessages.length > 0) { + html += `
+ โš ๏ธ ${sysMessages.map(m => esc(m)).join('
')} +
`; + } + + // โ”€โ”€ Infra labels โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + const labels = data.label && typeof data.label === 'object' ? Object.entries(data.label).filter(([k]) => k) : []; + if (labels.length > 0) { + html += `
`; + labels.forEach(([k, v]) => { + html += v + ? `${esc(k)}: ${esc(v)}` + : `${esc(k)}`; + }); + html += `
`; + } + + // โ”€โ”€ Per-NodeGroup sections โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + const COL_COUNT = 6; // node-varying columns: Node ID, Status, Public IP, Private IP, CSP Resource ID, Created + Object.entries(groups).forEach(([gid, gnodes]) => { + const first = gnodes[0] || {}; + + // โ”€โ”€ Group-level common values (derived from first node) โ”€โ”€โ”€โ”€โ”€โ”€ + const provider = first.connectionConfig?.providerName || first.connectionName?.split('-')[0] || ''; + const platform = (provider || '').toLowerCase(); + const providerColor = cspGenericColors[platform] || cspGenericColors[platform.split('-')[0]] || '#6c757d'; + const connName = first.connectionName || ''; + const region = first.region?.region || first.connectionConfig?.regionZoneInfo?.assignedRegion || ''; + const zone = first.region?.zone || first.connectionConfig?.regionZoneInfo?.assignedZone || ''; + const sshUser = first.nodeUserName || ''; + const sshPort = first.sshPort || 22; + + // Check if all nodes share same sshKeyId + const allSshKeys = [...new Set(gnodes.map(nd => nd.sshKeyId).filter(Boolean))]; + const uniformSshKey = allSshKeys.length === 1 ? allSshKeys[0].split('+').pop() || allSshKeys[0] : ''; + + // Spec summary (group-level) + let specHtml = ''; + if (first.spec?.vCPU || first.spec?.memoryGiB) { + const parts = []; + if (first.spec.vCPU) parts.push(`${first.spec.vCPU} vCPU`); + if (first.spec.memoryGiB) parts.push(`${first.spec.memoryGiB} GiB`); + if (first.spec.acceleratorType && first.spec.acceleratorType !== 'none') + parts.push(`+${esc(first.spec.acceleratorType.toUpperCase())}(${first.spec.acceleratorCount || 1})`); + specHtml = parts.join(' / '); + if (first.cspSpecName) specHtml += ` (${esc(first.cspSpecName)})`; + if (first.spec.costPerHour > 0) specHtml += ` ยท $${first.spec.costPerHour}/h`; + } else if (first.specId) { + const seg = first.specId.split('+').pop() || first.specId; + specHtml = `${esc(seg)}`; + if (first.cspSpecName) specHtml += ` (${esc(first.cspSpecName)})`; + } + + // OS summary (group-level) + let osText = ''; + if (first.image?.osDistribution) osText = first.image.osDistribution; + else if (first.image?.osType) osText = first.image.osType; + else if (first.imageId) osText = first.imageId.split('+').pop() || first.imageId; + + // Disk summary (group-level) + const diskType = first.rootDiskType && first.rootDiskType !== 'default' ? first.rootDiskType : ''; + const diskSize = first.rootDiskSize > 0 ? `${first.rootDiskSize}GB` : ''; + const diskText = (diskType || diskSize) ? `${diskType}${diskType && diskSize ? ' ' : ''}${diskSize}` : ''; + + // Build CSP badge + const providerBadge = provider + ? `${esc(provider.toUpperCase())}` + : ''; + + // Build info chips for the group header details row + const chips = []; + if (provider) chips.push(`โ˜๏ธ ${providerBadge}`); + if (connName) chips.push(`๐Ÿ”Œ ${esc(connName)}`); + if (region) chips.push(`๐Ÿ“ ${esc(region)}${zone ? ` / ${esc(zone)}` : ''}`); + if (sshUser) chips.push(`๐Ÿ‘ค ${esc(sshUser)}:${sshPort !== 22 ? `${sshPort}` : sshPort}${uniformSshKey ? ` ๐Ÿ”‘${esc(uniformSshKey)}` : ''}`); + if (specHtml) chips.push(`โš™๏ธ ${specHtml}`); + if (osText) chips.push(`๐Ÿ’ฟ ${esc(osText)}`); + if (diskText) chips.push(`๐Ÿ’พ ${esc(diskText)}`); + + html += ` +
+
+
+ ๐Ÿ“ฆ NodeGroup: ${esc(gid)} + ${gnodes.length} node(s) +
+
+ ${chips.join('')} +
+
+
+ + + + + + + + + + `; + + gnodes.forEach((nd, i) => { + const bg = i % 2 === 0 ? '#0d1b2a' : '#0a1520'; + const isNew = newNodeSet.has(nd.id); + + // Status cell โ€” include monAgentStatus + networkAgentStatus + targetAction + const monIcon = nd.monAgentStatus === 'installed' ? '๐Ÿ“กโœ“' : nd.monAgentStatus === 'failed' ? '๐Ÿ“กโœ—' : ''; + const netIcon = nd.networkAgentStatus === 'installed' ? '๐Ÿ”—โœ“' : nd.networkAgentStatus === 'failed' ? '๐Ÿ”—โœ—' : ''; + const agentIcons = [monIcon, netIcon].filter(Boolean).join(' '); + const statusCell = getInfraStatusBadge(nd.status) + + (agentIcons ? `${agentIcons}` : '') + + (nd.targetAction && nd.targetAction !== 'None' ? `
โณ${esc(nd.targetAction)}
` : ''); + + // CSP resource ID + const cspId = nd.cspResourceId || nd.cspResourceName || '-'; + const cspIdShort = cspId.length > 28 ? cspId.slice(0, 26) + 'โ€ฆ' : cspId; + + // Created date + const created = nd.createdTime ? esc(nd.createdTime.split(' ')[0]) : '-'; + + // Row border highlight for new nodes + const rowStyle = isNew ? `background:${bg};outline:1px solid #28a745;` : `background:${bg};`; + + // SSH key per-node (shown only if nodes have different keys) + const nodeKeyId = nd.sshKeyId ? nd.sshKeyId.split('+').pop() || nd.sshKeyId : ''; + const nodeIdCell = isNew + ? `NEW ${esc(nd.id || '-')}` + : esc(nd.id || '-'); + const nodeIdSuffix = (!uniformSshKey && nodeKeyId) + ? `
๐Ÿ”‘${esc(nodeKeyId)}
` : ''; + + html += ` + + + + + + + + `; + + // Sub-row: node labels (show all labels; keys with empty values shown as plain tags) + const ndLabels = nd.label && typeof nd.label === 'object' ? Object.entries(nd.label).filter(([k]) => k) : []; + if (ndLabels.length > 0) { + const pillsHtml = ndLabels.map(([k, v]) => + v ? `${esc(k)}: ${esc(v)}` + : `${esc(k)}` + ).join(''); + html += ``; + } + + // Sub-row: network / security details (vNetId, subnetId, securityGroupIds) + const netDetails = []; + if (nd.vNetId) netDetails.push(`vNet: ${esc(nd.vNetId)}`); + if (nd.subnetId) netDetails.push(`Subnet: ${esc(nd.subnetId)}`); + if (nd.securityGroupIds?.length) netDetails.push(`SG: ${nd.securityGroupIds.map(esc).join(', ')}`); + if (netDetails.length > 0) { + html += ``; + } + + // Sub-row: systemMessage (especially important for failed nodes) + if (nd.systemMessage) { + const isFail = (nd.status || '').toLowerCase().includes('fail'); + const msgColor = isFail ? '#f88' : '#ffc107'; + const msgBg = isFail ? '#2a1010' : '#1a1500'; + html += ``; + } + }); + html += `
Node IDStatusPublic IPPrivate IPCSP Resource IDCreated
${nodeIdCell}${nodeIdSuffix}${statusCell}${esc(nd.publicIP || '-')}${esc(nd.privateIP || '-')}${esc(cspIdShort)}${created}
๐Ÿท๏ธ ${pillsHtml}
๐ŸŒ ${netDetails.join('  ยท  ')}
+ ${isFail ? 'โŒ' : 'โ„น๏ธ'} ${esc(nd.systemMessage)} +
`; + }); + + return html; +} + +// Rich GUI for Infra status view (replaces raw JSON in statusInfra) +function displayInfraStatusGui(data) { + window._currentJsonOutput = data; + window._currentJsonString = JSON.stringify(data, null, 2); + const esc = window.escapeHtml || (s => String(s)); + const infraId = data.id || data.name || 'Infra'; + + // Meta tags row (description, CLADNet, etc.) + const metaItems = []; + if (data.description) metaItems.push(`๐Ÿ“ ${esc(data.description)}`); + if (data.configureCloudAdaptiveNetwork === 'yes') metaItems.push(`๐Ÿ”— CLADNet: enabled`); + const metaHtml = metaItems.length + ? `

${metaItems.join('  ยท  ')}

` + : ''; + + Swal.fire({ + title: `๐Ÿ“Š Infra Status: ${esc(infraId)}`, + html: ` +
+ ${metaHtml} + ${buildInfraNodeSummaryHtml(data)} +
+ + +
+
`, + background: '#0e1746', + color: '#e0e0e0', + showConfirmButton: false, + showCancelButton: true, + cancelButtonText: 'โœ• Close', + width: '90%', + }); +} + +// Rich GUI for Access Info view (replaces raw JSON in getAccessInfo) +function displayAccessInfoGui(data, infraId) { + window._currentJsonOutput = data; + window._currentJsonString = JSON.stringify(data, null, 2); + const esc = window.escapeHtml || (s => String(s)); + window._guiSshCommands = []; + const groups = data.InfraNodeGroupAccessInfo || []; + let cmdIdx = 0; + + let html = `
`; + html += ` +
+ + +
`; + + if (groups.length === 0) { + html += `

No access information available.

`; + } + + groups.forEach(group => { + const gid = group.NodeGroupId || 'default'; + const nodeList = group.NodeAccessInfo || []; + html += ` +
+
+ ๐Ÿ“ฆ NodeGroup: ${esc(gid)} (${nodeList.length} node(s)) +
+ + + + + + + + + + `; + nodeList.forEach((nd, i) => { + const bg = i % 2 === 0 ? '#0d1b2a' : '#0a1520'; + const user = nd.nodeUserName || 'ubuntu'; + const port = nd.sshPort || 22; + let sshCmd = '-'; + if (nd.publicIP) { + sshCmd = `ssh -i ${nd.nodeId || 'key'}.pem -p ${port} ${user}@${nd.publicIP}`; + } else if (nd.bastionPublicIp) { + sshCmd = `ssh -J ${user}@${nd.bastionPublicIp}:${nd.bastionSshPort || 22} -i key.pem -p ${port} ${user}@${nd.privateIP}`; + } + const thisIdx = cmdIdx++; + window._guiSshCommands.push(sshCmd); + html += ` + + + + + + + + `; + }); + html += `
Node IDPublic IPPrivate IPUserPortSSH Command (click to copy)
${esc(nd.nodeId || '-')}${esc(nd.publicIP || '-')}${esc(nd.privateIP || '-')}${esc(user)}${esc(String(port))} + + ${esc(sshCmd)} + +
`; + }); + html += `
`; + + Swal.fire({ + title: `๐Ÿ”‘ Access Info${infraId ? ': ' + esc(infraId) : ''}`, + html: html, + background: '#0e1746', + color: '#e0e0e0', + showConfirmButton: false, + showCancelButton: true, + cancelButtonText: 'โœ• Close', + width: '88%', + }); +} + +// Rich GUI for infraDynamic provisioning result (replaces raw JSON in proceedWithInfraCreation) +function displayInfraDynamicResultGui(data) { + window._currentJsonOutput = data; + window._currentJsonString = JSON.stringify(data, null, 2); + const esc = window.escapeHtml || (s => String(s)); + const infraId = data.id || data.name || 'Infra'; + const nodes = data.node || []; + const runningCount = nodes.filter(nd => (nd.status || '').toLowerCase() === 'running').length; + + Swal.fire({ + title: `โœ… Infra Created: ${esc(infraId)}`, + html: ` +
+
+

๐ŸŽ‰ Infrastructure provisioning completed!

+

+ ${runningCount} / ${nodes.length} node(s) running + ${data.description ? ' ยท ' + esc(data.description) : ''} +

+
+ ${buildInfraNodeSummaryHtml(data)} +
+ + +
+
`, + background: '#0e1746', + color: '#e0e0e0', + showConfirmButton: false, + showCancelButton: true, + cancelButtonText: 'โœ• Close', + width: '90%', + }); +} // Handle Infra without Nodes (preparing, prepared, empty states) function handleInfraWithoutNodes(infraItem) { @@ -5035,7 +5515,7 @@ function proceedWithInfraCreation(createInfraReq, url, username, password) { console.log('Failed to activate control tab:', error); } - displayJsonData(res.data, typeInfo); + displayInfraDynamicResultGui(res.data); handleAxiosResponse(res); updateInfraList(); @@ -11008,7 +11488,7 @@ function statusInfra() { }) .then((res) => { console.log("[Status Infra]"); - displayJsonData(res.data, typeInfo); + displayInfraStatusGui(res.data); }) .catch(function (error) { if (error.response) { @@ -17769,7 +18249,7 @@ function getAccessInfo() { }, }).then((res) => { console.log(res); // for debug - displayJsonData(res.data, typeInfo); + displayAccessInfoGui(res.data, infraid); }); } window.getAccessInfo = getAccessInfo;