edge-connect-mcp/ui.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"
}