@llms-sdk/representation-ui
Version:
Multi-view representation-driven development UI prototype
468 lines (407 loc) • 12.3 kB
text/typescript
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
*/
('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>
`;
}
}