UNPKG

@llms-sdk/representation-ui

Version:

Multi-view representation-driven development UI prototype

468 lines (407 loc) 12.3 kB
import { LitElement, html, css } from 'lit'; import { customElement } from 'lit/decorators.js'; import { SignalElement } from '../../utils/signals-lit.js'; import { sampleSystem } from '../../data/sample-system.js'; import { selectedElement, selectElement, crossViewHighlights } from '../../state/multi-view-state.js'; import { Service } from '../../data/types.js'; /** * DeploymentView - Service architecture and system topology * * Features: * - Interactive service topology diagram * - Health status indicators * - Deployment target visualization * - Service dependency mapping * - Cross-view highlighting */ @customElement('deployment-view') export class DeploymentView extends SignalElement { override connectedCallback() { super.connectedCallback(); this.watchSignals(selectedElement, crossViewHighlights); } static styles = css` :host { display: block; padding: 1rem; height: 100%; overflow: auto; } .deployment-container { display: flex; flex-direction: column; height: 100%; gap: 1rem; } .topology-diagram { flex: 1; border: 1px solid var(--sl-color-neutral-200); border-radius: 8px; padding: 1rem; background: var(--sl-color-neutral-50); position: relative; min-height: 250px; } .deployment-targets { display: flex; gap: 2rem; justify-content: space-around; height: 100%; align-items: flex-start; padding-top: 2rem; } .deployment-target { display: flex; flex-direction: column; align-items: center; gap: 1rem; min-width: 150px; } .target-header { padding: 0.5rem 1rem; background: var(--sl-color-blue-100); border: 2px solid var(--sl-color-blue-300); border-radius: 8px; font-weight: 600; color: var(--sl-color-blue-800); text-align: center; } .target-services { display: flex; flex-direction: column; gap: 0.75rem; align-items: center; } .service-node { width: 120px; padding: 0.75rem; border: 2px solid var(--sl-color-neutral-300); border-radius: 8px; background: white; cursor: pointer; transition: all 0.2s ease; text-align: center; } .service-node:hover { border-color: var(--sl-color-primary-500); box-shadow: 0 4px 12px rgba(0,0,0,0.1); transform: translateY(-2px); } .service-node.selected { border-color: var(--sl-color-primary-600); background: var(--sl-color-primary-50); box-shadow: 0 4px 16px rgba(59, 130, 246, 0.2); } .service-node.highlighted { border-color: var(--sl-color-warning-500); background: var(--sl-color-warning-50); animation: pulse 1.5s ease-in-out infinite; } .service-name { font-weight: 600; font-size: 0.875rem; color: var(--sl-color-neutral-800); margin-bottom: 0.5rem; } .service-status { display: flex; align-items: center; justify-content: center; gap: 0.25rem; font-size: 0.75rem; } .status-indicator { width: 8px; height: 8px; border-radius: 50%; } .status-healthy { background: var(--sl-color-success-500); } .status-warning { background: var(--sl-color-warning-500); } .status-error { background: var(--sl-color-danger-500); } .service-details { background: white; border: 1px solid var(--sl-color-neutral-200); border-radius: 8px; padding: 1rem; max-height: 200px; overflow-y: auto; } .details-title { font-weight: 600; margin-bottom: 0.75rem; color: var(--sl-color-neutral-800); display: flex; align-items: center; gap: 0.5rem; } .details-grid { display: grid; grid-template-columns: auto 1fr; gap: 0.5rem 1rem; align-items: start; } .details-label { font-weight: 500; color: var(--sl-color-neutral-700); } .details-value { color: var(--sl-color-neutral-600); } .endpoint-list { list-style: none; padding: 0; margin: 0; } .endpoint-item { padding: 0.25rem 0; font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace; font-size: 0.8rem; color: var(--sl-color-neutral-700); background: var(--sl-color-neutral-50); padding: 0.25rem 0.5rem; border-radius: 4px; margin-bottom: 0.25rem; } .dependency-tags { display: flex; flex-wrap: wrap; gap: 0.5rem; } .dependency-tag { padding: 0.25rem 0.5rem; background: var(--sl-color-blue-100); color: var(--sl-color-blue-800); border-radius: 0.25rem; font-size: 0.75rem; font-weight: 500; } .health-summary { display: flex; justify-content: space-between; align-items: center; padding: 0.75rem; background: var(--sl-color-neutral-100); border-radius: 8px; margin-bottom: 1rem; } .health-stats { display: flex; gap: 1rem; } .health-stat { display: flex; align-items: center; gap: 0.5rem; font-size: 0.875rem; } .stat-count { font-weight: 600; } .external-services { position: absolute; top: 1rem; right: 1rem; background: var(--sl-color-orange-100); border: 2px solid var(--sl-color-orange-300); border-radius: 8px; padding: 0.5rem; font-size: 0.75rem; font-weight: 600; color: var(--sl-color-orange-800); } .view-empty { display: flex; flex-direction: column; align-items: center; justify-content: center; height: 200px; color: var(--sl-color-neutral-500); text-align: center; } .empty-icon { font-size: 3rem; margin-bottom: 1rem; } `; private handleServiceClick(service: Service) { if (selectedElement.value?.id === service.id) { selectElement(null); } else { selectElement(service); } } private isHighlighted(service: Service): boolean { const highlights = crossViewHighlights.value; if (!highlights || typeof highlights !== 'object') { return false; } return highlights['deployment']?.includes(service.id) || false; } private getServicesGroupedByTarget() { const services = sampleSystem.services; const grouped: Record<string, Service[]> = {}; services.forEach(service => { const target = service.deploymentTarget; if (!grouped[target]) { grouped[target] = []; } grouped[target].push(service); }); return grouped; } private getHealthCounts() { const services = sampleSystem.services; return { healthy: services.filter(s => s.healthStatus === 'healthy').length, warning: services.filter(s => s.healthStatus === 'warning').length, error: services.filter(s => s.healthStatus === 'error').length, total: services.length }; } private renderService(service: Service) { const isSelected = selectedElement.value?.id === service.id; const isHighlighted = this.isHighlighted(service); return html` <div class="service-node ${isSelected ? 'selected' : ''} ${isHighlighted ? 'highlighted' : ''}" @click=${() => this.handleServiceClick(service)} > <div class="service-name">${service.name}</div> <div class="service-status"> <div class="status-indicator status-${service.healthStatus}"></div> ${service.healthStatus} </div> </div> `; } private renderDeploymentTarget(target: string, services: Service[]) { if (target === 'external') { return html` <div class="external-services"> External Services ${services.map(service => html` <div style="margin-top: 0.5rem;"> ${this.renderService(service)} </div> `)} </div> `; } return html` <div class="deployment-target"> <div class="target-header">${target}</div> <div class="target-services"> ${services.map(service => this.renderService(service))} </div> </div> `; } private renderServiceDetails() { const selected = selectedElement.value; if (!selected || selected.type !== 'service') { return html` <div class="service-details"> <div class="details-title">Service Details</div> <div style="color: var(--sl-color-neutral-500); font-style: italic;"> Select a service to view details </div> </div> `; } const service = selected as Service; return html` <div class="service-details"> <div class="details-title"> <div class="status-indicator status-${service.healthStatus}"></div> ${service.name} </div> <div class="details-grid"> <div class="details-label">Status:</div> <div class="details-value"> <span style="text-transform: capitalize;">${service.healthStatus}</span> </div> <div class="details-label">Deployment:</div> <div class="details-value">${service.deploymentTarget}</div> <div class="details-label">Description:</div> <div class="details-value">${service.description || 'No description available'}</div> <div class="details-label">Endpoints:</div> <div class="details-value"> <ul class="endpoint-list"> ${service.endpoints.map(endpoint => html` <li class="endpoint-item">${endpoint}</li> `)} </ul> </div> ${service.dependencies.length > 0 ? html` <div class="details-label">Dependencies:</div> <div class="details-value"> <div class="dependency-tags"> ${service.dependencies.map(dep => html` <span class="dependency-tag">${dep}</span> `)} </div> </div> ` : ''} </div> </div> `; } render() { const services = sampleSystem.services; if (services.length === 0) { return html` <div class="view-empty"> <div class="empty-icon">🚀</div> <div>No services defined</div> <div style="margin-top: 0.5rem; font-size: 0.875rem;"> Service deployment topology will appear here </div> </div> `; } const groupedServices = this.getServicesGroupedByTarget(); const healthCounts = this.getHealthCounts(); return html` <div class="deployment-container"> <div class="health-summary"> <div style="font-weight: 600; color: var(--sl-color-neutral-800);"> System Health Overview </div> <div class="health-stats"> <div class="health-stat"> <div class="status-indicator status-healthy"></div> <span class="stat-count">${healthCounts.healthy}</span> Healthy </div> <div class="health-stat"> <div class="status-indicator status-warning"></div> <span class="stat-count">${healthCounts.warning}</span> Warning </div> <div class="health-stat"> <div class="status-indicator status-error"></div> <span class="stat-count">${healthCounts.error}</span> Error </div> </div> </div> <div class="topology-diagram"> <div class="deployment-targets"> ${Object.entries(groupedServices) .filter(([target]) => target !== 'external') .map(([target, services]) => this.renderDeploymentTarget(target, services)) } </div> ${groupedServices['external'] ? this.renderDeploymentTarget('external', groupedServices['external']) : '' } </div> ${this.renderServiceDetails()} </div> `; } }