907 lines
27 KiB
Go
907 lines
27 KiB
Go
package main
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"html"
|
|
"strings"
|
|
|
|
v2 "edp.buildth.ing/DevFW-CICD/edge-connect-client/v2/sdk/edgeconnect/v2"
|
|
mcpuiserver "github.com/MCP-UI-Org/mcp-ui/sdks/go/server"
|
|
)
|
|
|
|
// createAppListUI generates an interactive UI for listing apps
|
|
func createAppListUI(apps []v2.App, region string, protocol mcpuiserver.ProtocolType) (*mcpuiserver.UIResource, error) {
|
|
htmlContent := fmt.Sprintf(`
|
|
<!DOCTYPE html>
|
|
<html>
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>Edge Connect Applications</title>
|
|
<style>
|
|
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
body {
|
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
|
|
background: linear-gradient(135deg, #667eea 0%%, #764ba2 100%%);
|
|
min-height: 100vh;
|
|
padding: 20px;
|
|
}
|
|
.container {
|
|
max-width: 1400px;
|
|
margin: 0 auto;
|
|
}
|
|
.header {
|
|
background: white;
|
|
border-radius: 12px;
|
|
padding: 24px;
|
|
margin-bottom: 24px;
|
|
box-shadow: 0 4px 6px rgba(0,0,0,0.1);
|
|
}
|
|
.header h1 {
|
|
color: #2d3748;
|
|
font-size: 28px;
|
|
margin-bottom: 8px;
|
|
}
|
|
.header .region {
|
|
display: inline-block;
|
|
background: #667eea;
|
|
color: white;
|
|
padding: 4px 12px;
|
|
border-radius: 6px;
|
|
font-size: 14px;
|
|
font-weight: 500;
|
|
}
|
|
.stats {
|
|
display: grid;
|
|
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
|
gap: 16px;
|
|
margin-bottom: 24px;
|
|
}
|
|
.stat-card {
|
|
background: white;
|
|
border-radius: 12px;
|
|
padding: 20px;
|
|
box-shadow: 0 4px 6px rgba(0,0,0,0.1);
|
|
}
|
|
.stat-card .label {
|
|
color: #718096;
|
|
font-size: 14px;
|
|
margin-bottom: 8px;
|
|
}
|
|
.stat-card .value {
|
|
color: #2d3748;
|
|
font-size: 32px;
|
|
font-weight: bold;
|
|
}
|
|
.apps-grid {
|
|
display: grid;
|
|
grid-template-columns: repeat(auto-fill, minmax(380px, 1fr));
|
|
gap: 20px;
|
|
}
|
|
.app-card {
|
|
background: white;
|
|
border-radius: 12px;
|
|
padding: 24px;
|
|
box-shadow: 0 4px 6px rgba(0,0,0,0.1);
|
|
transition: transform 0.2s, box-shadow 0.2s;
|
|
cursor: pointer;
|
|
}
|
|
.app-card:hover {
|
|
transform: translateY(-4px);
|
|
box-shadow: 0 8px 12px rgba(0,0,0,0.15);
|
|
}
|
|
.app-card .app-name {
|
|
font-size: 20px;
|
|
font-weight: bold;
|
|
color: #2d3748;
|
|
margin-bottom: 8px;
|
|
}
|
|
.app-card .app-org {
|
|
color: #718096;
|
|
font-size: 14px;
|
|
margin-bottom: 4px;
|
|
}
|
|
.app-card .app-version {
|
|
display: inline-block;
|
|
background: #e2e8f0;
|
|
color: #4a5568;
|
|
padding: 4px 10px;
|
|
border-radius: 6px;
|
|
font-size: 13px;
|
|
font-weight: 500;
|
|
margin-bottom: 16px;
|
|
}
|
|
.app-card .detail-row {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
padding: 8px 0;
|
|
border-bottom: 1px solid #e2e8f0;
|
|
font-size: 14px;
|
|
}
|
|
.app-card .detail-row:last-child {
|
|
border-bottom: none;
|
|
}
|
|
.app-card .detail-label {
|
|
color: #718096;
|
|
font-weight: 500;
|
|
}
|
|
.app-card .detail-value {
|
|
color: #2d3748;
|
|
font-weight: 400;
|
|
}
|
|
.badge {
|
|
display: inline-block;
|
|
padding: 4px 8px;
|
|
border-radius: 4px;
|
|
font-size: 12px;
|
|
font-weight: 600;
|
|
}
|
|
.badge.docker {
|
|
background: #bee3f8;
|
|
color: #2c5282;
|
|
}
|
|
.badge.kubernetes {
|
|
background: #c6f6d5;
|
|
color: #22543d;
|
|
}
|
|
.badge.serverless {
|
|
background: #fef5e7;
|
|
color: #975a16;
|
|
}
|
|
.actions {
|
|
display: flex;
|
|
gap: 8px;
|
|
margin-top: 16px;
|
|
}
|
|
.btn {
|
|
flex: 1;
|
|
padding: 10px;
|
|
border: none;
|
|
border-radius: 6px;
|
|
font-size: 14px;
|
|
font-weight: 500;
|
|
cursor: pointer;
|
|
transition: opacity 0.2s;
|
|
}
|
|
.btn:hover {
|
|
opacity: 0.8;
|
|
}
|
|
.btn-primary {
|
|
background: #667eea;
|
|
color: white;
|
|
}
|
|
.btn-danger {
|
|
background: #f56565;
|
|
color: white;
|
|
}
|
|
.empty-state {
|
|
background: white;
|
|
border-radius: 12px;
|
|
padding: 60px 20px;
|
|
text-align: center;
|
|
box-shadow: 0 4px 6px rgba(0,0,0,0.1);
|
|
}
|
|
.empty-state h2 {
|
|
color: #2d3748;
|
|
font-size: 24px;
|
|
margin-bottom: 12px;
|
|
}
|
|
.empty-state p {
|
|
color: #718096;
|
|
font-size: 16px;
|
|
}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<script>
|
|
function sendMessage(type, payload) {
|
|
const messageId = 'msg-' + Date.now();
|
|
console.log('Sending:', type, payload);
|
|
window.parent.postMessage({ type, messageId, payload }, '*');
|
|
}
|
|
|
|
document.addEventListener('DOMContentLoaded', function() {
|
|
// Attach event listeners to all view buttons
|
|
document.querySelectorAll('.btn-view-app').forEach(function(btn) {
|
|
btn.addEventListener('click', function() {
|
|
const card = this.closest('.app-card');
|
|
const org = card.dataset.org;
|
|
const name = card.dataset.name;
|
|
const version = card.dataset.version;
|
|
|
|
console.log('View app clicked:', org, name, version);
|
|
sendMessage('tool', {
|
|
toolName: 'show_app',
|
|
params: {
|
|
organization: org,
|
|
name: name,
|
|
version: version
|
|
}
|
|
});
|
|
});
|
|
});
|
|
|
|
// Attach event listeners to all delete buttons
|
|
document.querySelectorAll('.btn-delete-app').forEach(function(btn) {
|
|
btn.addEventListener('click', function() {
|
|
const card = this.closest('.app-card');
|
|
const org = card.dataset.org;
|
|
const name = card.dataset.name;
|
|
const version = card.dataset.version;
|
|
|
|
console.log('Delete app clicked:', org, name, version);
|
|
sendMessage('tool', {
|
|
toolName: 'delete_app',
|
|
params: {
|
|
organization: org,
|
|
name: name,
|
|
version: version
|
|
}
|
|
});
|
|
});
|
|
});
|
|
});
|
|
</script>
|
|
<div class="container">
|
|
<div class="header">
|
|
<h1>Edge Connect Applications</h1>
|
|
<span class="region">Region: %s</span>
|
|
</div>
|
|
<div class="stats">
|
|
<div class="stat-card">
|
|
<div class="label">Total Applications</div>
|
|
<div class="value">%d</div>
|
|
</div>
|
|
<div class="stat-card">
|
|
<div class="label">Docker Apps</div>
|
|
<div class="value">%d</div>
|
|
</div>
|
|
<div class="stat-card">
|
|
<div class="label">Kubernetes Apps</div>
|
|
<div class="value">%d</div>
|
|
</div>
|
|
<div class="stat-card">
|
|
<div class="label">Serverless Enabled</div>
|
|
<div class="value">%d</div>
|
|
</div>
|
|
</div>
|
|
`, region, len(apps), countDeploymentType(apps, "docker"), countDeploymentType(apps, "kubernetes"), countServerlessApps(apps))
|
|
|
|
if len(apps) == 0 {
|
|
htmlContent += `
|
|
<div class="empty-state">
|
|
<h2>No Applications Found</h2>
|
|
<p>Start by creating your first Edge Connect application</p>
|
|
</div>
|
|
`
|
|
} else {
|
|
htmlContent += ` <div class="apps-grid">`
|
|
for _, app := range apps {
|
|
htmlContent += generateAppCard(app)
|
|
}
|
|
htmlContent += ` </div>`
|
|
}
|
|
|
|
htmlContent += `
|
|
</div>
|
|
</body>
|
|
</html>
|
|
`
|
|
|
|
resource, err := mcpuiserver.CreateUIResource(
|
|
"ui://apps-dashboard",
|
|
&mcpuiserver.RawHTMLPayload{
|
|
Type: mcpuiserver.ContentTypeRawHTML,
|
|
HTMLString: htmlContent,
|
|
},
|
|
mcpuiserver.EncodingText,
|
|
mcpuiserver.WithProtocol(protocol),
|
|
mcpuiserver.WithUIMetadata(map[string]any{
|
|
"preferred-frame-size": []string{"800px", "600px"},
|
|
}),
|
|
)
|
|
|
|
return resource, err
|
|
}
|
|
|
|
// generateAppCard creates HTML for a single app card
|
|
func generateAppCard(app v2.App) string {
|
|
deploymentBadge := fmt.Sprintf(`<span class="badge docker">%s</span>`, strings.ToUpper(app.Deployment))
|
|
if strings.ToLower(app.Deployment) == "kubernetes" {
|
|
deploymentBadge = fmt.Sprintf(`<span class="badge kubernetes">%s</span>`, strings.ToUpper(app.Deployment))
|
|
}
|
|
|
|
serverlessBadge := ""
|
|
if app.AllowServerless {
|
|
serverlessBadge = ` <span class="badge serverless">SERVERLESS</span>`
|
|
}
|
|
|
|
imagePath := html.EscapeString(app.ImagePath)
|
|
if len(imagePath) > 50 {
|
|
imagePath = imagePath[:47] + "..."
|
|
}
|
|
|
|
return fmt.Sprintf(`
|
|
<div class="app-card" data-org="%s" data-name="%s" data-version="%s">
|
|
<div class="app-name">%s</div>
|
|
<div class="app-org">%s</div>
|
|
<div class="app-version">v%s</div>
|
|
<div class="detail-row">
|
|
<span class="detail-label">Deployment</span>
|
|
<span class="detail-value">%s%s</span>
|
|
</div>
|
|
<div class="detail-row">
|
|
<span class="detail-label">Image</span>
|
|
<span class="detail-value" title="%s">%s</span>
|
|
</div>
|
|
<div class="detail-row">
|
|
<span class="detail-label">Ports</span>
|
|
<span class="detail-value">%s</span>
|
|
</div>
|
|
<div class="actions">
|
|
<button class="btn btn-primary btn-view-app">View Details</button>
|
|
<button class="btn btn-danger btn-delete-app">Delete</button>
|
|
</div>
|
|
</div>
|
|
`,
|
|
html.EscapeString(app.Key.Organization),
|
|
html.EscapeString(app.Key.Name),
|
|
html.EscapeString(app.Key.Version),
|
|
html.EscapeString(app.Key.Name),
|
|
html.EscapeString(app.Key.Organization),
|
|
html.EscapeString(app.Key.Version),
|
|
deploymentBadge,
|
|
serverlessBadge,
|
|
html.EscapeString(app.ImagePath),
|
|
imagePath,
|
|
getAccessPorts(app.AccessPorts),
|
|
)
|
|
}
|
|
|
|
// createAppDetailUI generates a detailed view for a single app
|
|
func createAppDetailUI(app v2.App, region string, protocol mcpuiserver.ProtocolType) (*mcpuiserver.UIResource, error) {
|
|
appJSON, _ := json.MarshalIndent(app, "", " ")
|
|
|
|
htmlContent := fmt.Sprintf(`
|
|
<!DOCTYPE html>
|
|
<html>
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>%s - App Details</title>
|
|
<style>
|
|
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
body {
|
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
|
|
background: linear-gradient(135deg, #667eea 0%%, #764ba2 100%%);
|
|
min-height: 100vh;
|
|
padding: 20px;
|
|
}
|
|
.container {
|
|
max-width: 1000px;
|
|
margin: 0 auto;
|
|
}
|
|
.card {
|
|
background: white;
|
|
border-radius: 12px;
|
|
padding: 24px;
|
|
margin-bottom: 20px;
|
|
box-shadow: 0 4px 6px rgba(0,0,0,0.1);
|
|
}
|
|
.header {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
margin-bottom: 20px;
|
|
}
|
|
.header h1 {
|
|
color: #2d3748;
|
|
font-size: 28px;
|
|
}
|
|
.back-btn {
|
|
background: #e2e8f0;
|
|
color: #2d3748;
|
|
padding: 10px 20px;
|
|
border: none;
|
|
border-radius: 6px;
|
|
font-size: 14px;
|
|
font-weight: 500;
|
|
cursor: pointer;
|
|
text-decoration: none;
|
|
}
|
|
.detail-grid {
|
|
display: grid;
|
|
grid-template-columns: 200px 1fr;
|
|
gap: 16px 24px;
|
|
}
|
|
.detail-label {
|
|
color: #718096;
|
|
font-weight: 600;
|
|
font-size: 14px;
|
|
}
|
|
.detail-value {
|
|
color: #2d3748;
|
|
font-size: 14px;
|
|
word-break: break-all;
|
|
}
|
|
.json-viewer {
|
|
background: #2d3748;
|
|
color: #68d391;
|
|
padding: 16px;
|
|
border-radius: 8px;
|
|
font-family: 'Courier New', monospace;
|
|
font-size: 13px;
|
|
overflow-x: auto;
|
|
white-space: pre-wrap;
|
|
}
|
|
.badge {
|
|
display: inline-block;
|
|
padding: 4px 12px;
|
|
border-radius: 6px;
|
|
font-size: 13px;
|
|
font-weight: 600;
|
|
}
|
|
.badge.success {
|
|
background: #c6f6d5;
|
|
color: #22543d;
|
|
}
|
|
.badge.info {
|
|
background: #bee3f8;
|
|
color: #2c5282;
|
|
}
|
|
.badge.warning {
|
|
background: #fef5e7;
|
|
color: #975a16;
|
|
}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<div class="container">
|
|
<div class="card">
|
|
<div class="header">
|
|
<h1>%s</h1>
|
|
<a href="#" class="back-btn" onclick="history.back()">← Back to Apps</a>
|
|
</div>
|
|
<div class="detail-grid">
|
|
<div class="detail-label">Organization</div>
|
|
<div class="detail-value">%s</div>
|
|
<div class="detail-label">Name</div>
|
|
<div class="detail-value">%s</div>
|
|
<div class="detail-label">Version</div>
|
|
<div class="detail-value">%s</div>
|
|
<div class="detail-label">Region</div>
|
|
<div class="detail-value">%s</div>
|
|
<div class="detail-label">Deployment</div>
|
|
<div class="detail-value"><span class="badge info">%s</span></div>
|
|
<div class="detail-label">Image Type</div>
|
|
<div class="detail-value">%s</div>
|
|
<div class="detail-label">Image Path</div>
|
|
<div class="detail-value">%s</div>
|
|
<div class="detail-label">Access Ports</div>
|
|
<div class="detail-value">%s</div>
|
|
<div class="detail-label">Serverless</div>
|
|
<div class="detail-value"><span class="badge %s">%s</span></div>
|
|
</div>
|
|
</div>
|
|
<div class="card">
|
|
<h2 style="margin-bottom: 16px; color: #2d3748;">Raw JSON</h2>
|
|
<div class="json-viewer">%s</div>
|
|
</div>
|
|
</div>
|
|
</body>
|
|
</html>
|
|
`,
|
|
html.EscapeString(app.Key.Name),
|
|
html.EscapeString(app.Key.Name),
|
|
html.EscapeString(app.Key.Organization),
|
|
html.EscapeString(app.Key.Name),
|
|
html.EscapeString(app.Key.Version),
|
|
html.EscapeString(region),
|
|
html.EscapeString(strings.ToUpper(app.Deployment)),
|
|
html.EscapeString(app.ImageType),
|
|
html.EscapeString(app.ImagePath),
|
|
getAccessPorts(app.AccessPorts),
|
|
getServerlessBadgeClass(app.AllowServerless),
|
|
getServerlessStatus(app.AllowServerless),
|
|
html.EscapeString(string(appJSON)),
|
|
)
|
|
|
|
resource, err := mcpuiserver.CreateUIResource(
|
|
fmt.Sprintf("ui://app-detail/%s/%s/%s", app.Key.Organization, app.Key.Name, app.Key.Version),
|
|
&mcpuiserver.RawHTMLPayload{
|
|
Type: mcpuiserver.ContentTypeRawHTML,
|
|
HTMLString: htmlContent,
|
|
},
|
|
mcpuiserver.EncodingText,
|
|
mcpuiserver.WithProtocol(protocol),
|
|
mcpuiserver.WithUIMetadata(map[string]any{
|
|
"preferred-frame-size": []string{"800px", "600px"},
|
|
}),
|
|
)
|
|
|
|
return resource, err
|
|
}
|
|
|
|
// createAppInstanceListUI generates an interactive UI for listing app instances
|
|
func createAppInstanceListUI(instances []v2.AppInstance, region string, protocol mcpuiserver.ProtocolType) (*mcpuiserver.UIResource, error) {
|
|
htmlContent := fmt.Sprintf(`
|
|
<!DOCTYPE html>
|
|
<html>
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>Edge Connect App Instances</title>
|
|
<style>
|
|
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
body {
|
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
|
|
background: linear-gradient(135deg, #4f46e5 0%%, #7c3aed 100%%);
|
|
min-height: 100vh;
|
|
padding: 20px;
|
|
}
|
|
.container {
|
|
max-width: 1400px;
|
|
margin: 0 auto;
|
|
}
|
|
.header {
|
|
background: white;
|
|
border-radius: 12px;
|
|
padding: 24px;
|
|
margin-bottom: 24px;
|
|
box-shadow: 0 4px 6px rgba(0,0,0,0.1);
|
|
}
|
|
.header h1 {
|
|
color: #2d3748;
|
|
font-size: 28px;
|
|
margin-bottom: 8px;
|
|
}
|
|
.header .region {
|
|
display: inline-block;
|
|
background: #4f46e5;
|
|
color: white;
|
|
padding: 4px 12px;
|
|
border-radius: 6px;
|
|
font-size: 14px;
|
|
font-weight: 500;
|
|
}
|
|
.stats {
|
|
display: grid;
|
|
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
|
gap: 16px;
|
|
margin-bottom: 24px;
|
|
}
|
|
.stat-card {
|
|
background: white;
|
|
border-radius: 12px;
|
|
padding: 20px;
|
|
box-shadow: 0 4px 6px rgba(0,0,0,0.1);
|
|
}
|
|
.stat-card .label {
|
|
color: #718096;
|
|
font-size: 14px;
|
|
margin-bottom: 8px;
|
|
}
|
|
.stat-card .value {
|
|
color: #2d3748;
|
|
font-size: 32px;
|
|
font-weight: bold;
|
|
}
|
|
.instances-table {
|
|
background: white;
|
|
border-radius: 12px;
|
|
overflow: hidden;
|
|
box-shadow: 0 4px 6px rgba(0,0,0,0.1);
|
|
}
|
|
table {
|
|
width: 100%%;
|
|
border-collapse: collapse;
|
|
}
|
|
thead {
|
|
background: #f7fafc;
|
|
}
|
|
th {
|
|
padding: 16px;
|
|
text-align: left;
|
|
color: #2d3748;
|
|
font-weight: 600;
|
|
font-size: 13px;
|
|
text-transform: uppercase;
|
|
letter-spacing: 0.5px;
|
|
}
|
|
td {
|
|
padding: 16px;
|
|
border-top: 1px solid #e2e8f0;
|
|
color: #4a5568;
|
|
font-size: 14px;
|
|
}
|
|
tr:hover {
|
|
background: #f7fafc;
|
|
}
|
|
.badge {
|
|
display: inline-block;
|
|
padding: 4px 10px;
|
|
border-radius: 12px;
|
|
font-size: 12px;
|
|
font-weight: 600;
|
|
}
|
|
.badge.running {
|
|
background: #c6f6d5;
|
|
color: #22543d;
|
|
}
|
|
.badge.stopped {
|
|
background: #fed7d7;
|
|
color: #742a2a;
|
|
}
|
|
.badge.unknown {
|
|
background: #e2e8f0;
|
|
color: #4a5568;
|
|
}
|
|
.btn-group {
|
|
display: flex;
|
|
gap: 8px;
|
|
}
|
|
.btn {
|
|
padding: 6px 12px;
|
|
border: none;
|
|
border-radius: 6px;
|
|
font-size: 13px;
|
|
font-weight: 500;
|
|
cursor: pointer;
|
|
transition: opacity 0.2s;
|
|
}
|
|
.btn:hover {
|
|
opacity: 0.8;
|
|
}
|
|
.btn-sm {
|
|
background: #4f46e5;
|
|
color: white;
|
|
}
|
|
.btn-danger {
|
|
background: #f56565;
|
|
color: white;
|
|
}
|
|
.empty-state {
|
|
background: white;
|
|
border-radius: 12px;
|
|
padding: 60px 20px;
|
|
text-align: center;
|
|
box-shadow: 0 4px 6px rgba(0,0,0,0.1);
|
|
}
|
|
.empty-state h2 {
|
|
color: #2d3748;
|
|
font-size: 24px;
|
|
margin-bottom: 12px;
|
|
}
|
|
.empty-state p {
|
|
color: #718096;
|
|
font-size: 16px;
|
|
}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<script>
|
|
function sendMessage(type, payload) {
|
|
const messageId = 'msg-' + Date.now();
|
|
console.log('Sending:', type, payload);
|
|
window.parent.postMessage({ type, messageId, payload }, '*');
|
|
}
|
|
|
|
document.addEventListener('DOMContentLoaded', function() {
|
|
// Attach event listeners to all view buttons
|
|
document.querySelectorAll('.btn-view-instance').forEach(function(btn) {
|
|
btn.addEventListener('click', function() {
|
|
const row = this.closest('tr');
|
|
const org = row.dataset.org;
|
|
const name = row.dataset.name;
|
|
const cloudletOrg = row.dataset.cloudletOrg;
|
|
const cloudletName = row.dataset.cloudletName;
|
|
|
|
console.log('View instance clicked:', org, name, cloudletOrg, cloudletName);
|
|
sendMessage('tool', {
|
|
toolName: 'show_app_instance',
|
|
params: {
|
|
organization: org,
|
|
instance_name: name,
|
|
cloudlet_org: cloudletOrg,
|
|
cloudlet_name: cloudletName
|
|
}
|
|
});
|
|
});
|
|
});
|
|
|
|
// Attach event listeners to all delete buttons
|
|
document.querySelectorAll('.btn-delete-instance').forEach(function(btn) {
|
|
btn.addEventListener('click', function() {
|
|
const row = this.closest('tr');
|
|
const org = row.dataset.org;
|
|
const name = row.dataset.name;
|
|
const cloudletOrg = row.dataset.cloudletOrg;
|
|
const cloudletName = row.dataset.cloudletName;
|
|
|
|
console.log('Delete instance clicked:', org, name, cloudletOrg, cloudletName);
|
|
sendMessage('tool', {
|
|
toolName: 'delete_app_instance',
|
|
params: {
|
|
organization: org,
|
|
instance_name: name,
|
|
cloudlet_org: cloudletOrg,
|
|
cloudlet_name: cloudletName
|
|
}
|
|
});
|
|
});
|
|
});
|
|
});
|
|
</script>
|
|
<div class="container">
|
|
<div class="header">
|
|
<h1>Application Instances</h1>
|
|
<span class="region">Region: %s</span>
|
|
</div>
|
|
<div class="stats">
|
|
<div class="stat-card">
|
|
<div class="label">Total Instances</div>
|
|
<div class="value">%d</div>
|
|
</div>
|
|
<div class="stat-card">
|
|
<div class="label">Running</div>
|
|
<div class="value">%d</div>
|
|
</div>
|
|
<div class="stat-card">
|
|
<div class="label">Stopped</div>
|
|
<div class="value">%d</div>
|
|
</div>
|
|
</div>
|
|
`, region, len(instances), countPowerState(instances, "PowerOn"), countPowerState(instances, "PowerOff"))
|
|
|
|
if len(instances) == 0 {
|
|
htmlContent += `
|
|
<div class="empty-state">
|
|
<h2>No App Instances Found</h2>
|
|
<p>Deploy your first application instance to get started</p>
|
|
</div>
|
|
`
|
|
} else {
|
|
htmlContent += `
|
|
<div class="instances-table">
|
|
<table>
|
|
<thead>
|
|
<tr>
|
|
<th>Instance Name</th>
|
|
<th>Organization</th>
|
|
<th>Cloudlet</th>
|
|
<th>Application</th>
|
|
<th>Status</th>
|
|
<th>Flavor</th>
|
|
<th>Actions</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
`
|
|
for _, inst := range instances {
|
|
htmlContent += generateInstanceRow(inst)
|
|
}
|
|
htmlContent += `
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
`
|
|
}
|
|
|
|
htmlContent += `
|
|
</div>
|
|
</body>
|
|
</html>
|
|
`
|
|
|
|
resource, err := mcpuiserver.CreateUIResource(
|
|
"ui://app-instances-dashboard",
|
|
&mcpuiserver.RawHTMLPayload{
|
|
Type: mcpuiserver.ContentTypeRawHTML,
|
|
HTMLString: htmlContent,
|
|
},
|
|
mcpuiserver.EncodingText,
|
|
mcpuiserver.WithProtocol(protocol),
|
|
mcpuiserver.WithUIMetadata(map[string]any{
|
|
"preferred-frame-size": []string{"800px", "600px"},
|
|
}),
|
|
)
|
|
|
|
return resource, err
|
|
}
|
|
|
|
// generateInstanceRow creates HTML for a single instance table row
|
|
func generateInstanceRow(inst v2.AppInstance) string {
|
|
var statusBadge string
|
|
switch inst.PowerState {
|
|
case "PowerOn":
|
|
statusBadge = `<span class="badge running">RUNNING</span>`
|
|
case "PowerOff":
|
|
statusBadge = `<span class="badge stopped">STOPPED</span>`
|
|
default:
|
|
statusBadge = `<span class="badge unknown">UNKNOWN</span>`
|
|
}
|
|
|
|
return fmt.Sprintf(`
|
|
<tr data-org="%s" data-name="%s" data-cloudlet-org="%s" data-cloudlet-name="%s">
|
|
<td><strong>%s</strong></td>
|
|
<td>%s</td>
|
|
<td>%s/%s</td>
|
|
<td>%s:%s</td>
|
|
<td>%s</td>
|
|
<td>%s</td>
|
|
<td>
|
|
<div class="btn-group">
|
|
<button class="btn btn-sm btn-view-instance">View</button>
|
|
<button class="btn btn-danger btn-delete-instance">Delete</button>
|
|
</div>
|
|
</td>
|
|
</tr>
|
|
`,
|
|
html.EscapeString(inst.Key.Organization),
|
|
html.EscapeString(inst.Key.Name),
|
|
html.EscapeString(inst.Key.CloudletKey.Organization),
|
|
html.EscapeString(inst.Key.CloudletKey.Name),
|
|
html.EscapeString(inst.Key.Name),
|
|
html.EscapeString(inst.Key.Organization),
|
|
html.EscapeString(inst.Key.CloudletKey.Organization),
|
|
html.EscapeString(inst.Key.CloudletKey.Name),
|
|
html.EscapeString(inst.AppKey.Name),
|
|
html.EscapeString(inst.AppKey.Version),
|
|
statusBadge,
|
|
html.EscapeString(inst.Flavor.Name),
|
|
)
|
|
}
|
|
|
|
// Helper functions
|
|
|
|
func countDeploymentType(apps []v2.App, deploymentType string) int {
|
|
count := 0
|
|
for _, app := range apps {
|
|
if strings.EqualFold(app.Deployment, deploymentType) {
|
|
count++
|
|
}
|
|
}
|
|
return count
|
|
}
|
|
|
|
func countServerlessApps(apps []v2.App) int {
|
|
count := 0
|
|
for _, app := range apps {
|
|
if app.AllowServerless {
|
|
count++
|
|
}
|
|
}
|
|
return count
|
|
}
|
|
|
|
func countPowerState(instances []v2.AppInstance, state string) int {
|
|
count := 0
|
|
for _, inst := range instances {
|
|
if inst.PowerState == state {
|
|
count++
|
|
}
|
|
}
|
|
return count
|
|
}
|
|
|
|
func getAccessPorts(ports string) string {
|
|
if ports == "" {
|
|
return "None"
|
|
}
|
|
return ports
|
|
}
|
|
|
|
func getServerlessBadgeClass(allowServerless bool) string {
|
|
if allowServerless {
|
|
return "success"
|
|
}
|
|
return "warning"
|
|
}
|
|
|
|
func getServerlessStatus(allowServerless bool) string {
|
|
if allowServerless {
|
|
return "ENABLED"
|
|
}
|
|
return "DISABLED"
|
|
}
|