import { customElement, DeesElement, type TemplateResult, html, property, css, cssManager, state, } from '@design.estate/dees-element'; import { DeesAppuiSecondarymenu, DeesIcon, DeesStatsGrid, DeesChartLog, DeesButton, type ILogEntry, } from '@design.estate/dees-catalog'; import type { ISecondaryMenuGroup, ISecondaryMenuItem } from '../../elements/interfaces/secondarymenu.js'; import { demo } from './eco-view-containers.demo.js'; // Ensure components are registered DeesAppuiSecondarymenu; DeesIcon; DeesStatsGrid; DeesChartLog; DeesButton; declare global { interface HTMLElementTagNameMap { 'eco-view-containers': EcoViewContainers; } } export interface IContainer { id: string; name: string; image: string; status: 'running' | 'stopped' | 'paused' | 'restarting' | 'exited'; state: string; created: string; ports: Array<{ hostPort: number; containerPort: number; protocol: string }>; networks: string[]; mounts: Array<{ source: string; destination: string; mode: string }>; cpuPercent: number; memoryUsage: number; memoryLimit: number; networkRx: number; networkTx: number; } export type TContainerPanel = 'overview' | 'logs' | 'stats' | 'inspect' | 'terminal'; @customElement('eco-view-containers') export class EcoViewContainers extends DeesElement { public static demo = demo; public static demoGroup = 'Views'; public static styles = [ cssManager.defaultStyles, css` :host { display: block; width: 100%; height: 100%; background: ${cssManager.bdTheme('#f5f5f7', 'hsl(240 6% 10%)')}; color: ${cssManager.bdTheme('hsl(0 0% 10%)', 'hsl(0 0% 98%)')}; font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; } .containers-container { display: flex; height: 100%; } dees-appui-secondarymenu { flex-shrink: 0; background: ${cssManager.bdTheme('#ffffff', 'hsl(240 6% 8%)')}; border-right: 1px solid ${cssManager.bdTheme('hsl(0 0% 90%)', 'hsl(240 5% 15%)')}; } .content { flex: 1; overflow-y: auto; padding: 32px 48px; display: flex; flex-direction: column; } .panel-header { margin-bottom: 32px; } .panel-title { font-size: 28px; font-weight: 600; color: ${cssManager.bdTheme('hsl(0 0% 10%)', 'hsl(0 0% 98%)')}; margin-bottom: 8px; display: flex; align-items: center; gap: 12px; } .panel-description { font-size: 14px; color: ${cssManager.bdTheme('hsl(0 0% 50%)', 'hsl(0 0% 60%)')}; } .status-badge { display: inline-flex; align-items: center; gap: 6px; padding: 4px 12px; border-radius: 12px; font-size: 12px; font-weight: 500; text-transform: uppercase; } .status-badge.running { background: hsla(142, 71%, 45%, 0.15); color: hsl(142, 71%, 45%); } .status-badge.stopped, .status-badge.exited { background: hsla(0, 0%, 50%, 0.15); color: hsl(0, 0%, 50%); } .status-badge.paused { background: hsla(45, 93%, 47%, 0.15); color: hsl(45, 93%, 47%); } .status-badge.restarting { background: hsla(217, 91%, 60%, 0.15); color: hsl(217, 91%, 60%); } .status-dot { width: 8px; height: 8px; border-radius: 50%; background: currentColor; } .stats-section { margin-bottom: 32px; } .section-title { font-size: 13px; font-weight: 600; color: ${cssManager.bdTheme('hsl(0 0% 50%)', 'hsl(0 0% 50%)')}; text-transform: uppercase; letter-spacing: 0.5px; margin-bottom: 16px; } .actions-bar { display: flex; gap: 12px; margin-bottom: 24px; } .info-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); gap: 16px; margin-bottom: 32px; } .info-card { background: ${cssManager.bdTheme('#ffffff', 'hsl(240 6% 12%)')}; border: 1px solid ${cssManager.bdTheme('hsl(0 0% 90%)', 'hsl(240 5% 18%)')}; border-radius: 12px; padding: 20px; } .info-card-title { font-size: 13px; font-weight: 600; color: ${cssManager.bdTheme('hsl(0 0% 50%)', 'hsl(0 0% 50%)')}; text-transform: uppercase; letter-spacing: 0.5px; margin-bottom: 12px; } .info-row { display: flex; justify-content: space-between; padding: 8px 0; border-bottom: 1px solid ${cssManager.bdTheme('hsl(0 0% 94%)', 'hsl(240 5% 15%)')}; } .info-row:last-child { border-bottom: none; } .info-label { font-size: 14px; color: ${cssManager.bdTheme('hsl(0 0% 50%)', 'hsl(0 0% 55%)')}; } .info-value { font-size: 14px; font-weight: 500; color: ${cssManager.bdTheme('hsl(0 0% 20%)', 'hsl(0 0% 85%)')}; font-family: ui-monospace, monospace; } .ports-list { display: flex; flex-wrap: wrap; gap: 8px; } .port-badge { background: ${cssManager.bdTheme('hsl(217 91% 95%)', 'hsl(217 91% 20%)')}; color: ${cssManager.bdTheme('hsl(217 91% 40%)', 'hsl(217 91% 70%)')}; padding: 4px 8px; border-radius: 6px; font-size: 12px; font-family: ui-monospace, monospace; } .networks-list { display: flex; flex-wrap: wrap; gap: 8px; } .network-badge { background: ${cssManager.bdTheme('hsl(262 83% 95%)', 'hsl(262 83% 20%)')}; color: ${cssManager.bdTheme('hsl(262 83% 45%)', 'hsl(262 83% 70%)')}; padding: 4px 8px; border-radius: 6px; font-size: 12px; } .logs-container { flex: 1; min-height: 400px; display: flex; flex-direction: column; } dees-chart-log { flex: 1; min-height: 0; } .empty-state { display: flex; flex-direction: column; align-items: center; justify-content: center; height: 100%; color: ${cssManager.bdTheme('hsl(0 0% 50%)', 'hsl(0 0% 50%)')}; text-align: center; padding: 48px; } .empty-state dees-icon { margin-bottom: 16px; opacity: 0.5; } .empty-state-title { font-size: 18px; font-weight: 600; margin-bottom: 8px; } .empty-state-description { font-size: 14px; max-width: 400px; } .mounts-list { display: flex; flex-direction: column; gap: 8px; } .mount-item { display: flex; align-items: center; gap: 8px; padding: 8px 12px; background: ${cssManager.bdTheme('hsl(0 0% 97%)', 'hsl(240 5% 14%)')}; border-radius: 8px; font-size: 13px; font-family: ui-monospace, monospace; } .mount-source { color: ${cssManager.bdTheme('hsl(0 0% 40%)', 'hsl(0 0% 65%)')}; } .mount-arrow { color: ${cssManager.bdTheme('hsl(0 0% 60%)', 'hsl(0 0% 45%)')}; } .mount-dest { color: ${cssManager.bdTheme('hsl(0 0% 20%)', 'hsl(0 0% 85%)')}; } .mount-mode { margin-left: auto; padding: 2px 6px; background: ${cssManager.bdTheme('hsl(0 0% 90%)', 'hsl(240 5% 20%)')}; border-radius: 4px; font-size: 11px; color: ${cssManager.bdTheme('hsl(0 0% 50%)', 'hsl(0 0% 60%)')}; } .tabs-container { display: flex; gap: 4px; margin-bottom: 24px; border-bottom: 1px solid ${cssManager.bdTheme('hsl(0 0% 90%)', 'hsl(240 5% 18%)')}; padding-bottom: -1px; } .tab-button { padding: 10px 16px; font-size: 14px; font-weight: 500; color: ${cssManager.bdTheme('hsl(0 0% 50%)', 'hsl(0 0% 55%)')}; background: transparent; border: none; border-bottom: 2px solid transparent; cursor: pointer; transition: all 0.15s ease; margin-bottom: -1px; } .tab-button:hover { color: ${cssManager.bdTheme('hsl(0 0% 30%)', 'hsl(0 0% 75%)')}; } .tab-button.active { color: ${cssManager.bdTheme('hsl(217 91% 50%)', 'hsl(217 91% 60%)')}; border-bottom-color: hsl(217 91% 60%); } `, ]; @property({ type: Array }) accessor containers: IContainer[] = []; @state() accessor selectedContainerId: string | null = null; @state() accessor activePanel: TContainerPanel = 'overview'; @state() accessor logEntries: ILogEntry[] = []; private get selectedContainer(): IContainer | null { return this.containers.find(c => c.id === this.selectedContainerId) || null; } // Helper to format bytes private formatBytes(bytes: number): string { if (bytes === 0) return '0 B'; const k = 1024; const sizes = ['B', 'KB', 'MB', 'GB', 'TB']; const i = Math.floor(Math.log(bytes) / Math.log(k)); return parseFloat((bytes / Math.pow(k, i)).toFixed(1)) + ' ' + sizes[i]; } // Helper to format container ID (short form) private formatContainerId(id: string): string { return id.substring(0, 12); } private getStatusBadgeVariant(status: IContainer['status']): 'default' | 'success' | 'warning' | 'error' { switch (status) { case 'running': return 'success'; case 'paused': return 'warning'; case 'stopped': case 'exited': return 'error'; case 'restarting': return 'default'; default: return 'default'; } } private getMenuGroups(): ISecondaryMenuGroup[] { const runningContainers = this.containers.filter(c => c.status === 'running'); const stoppedContainers = this.containers.filter(c => c.status !== 'running'); const groups: ISecondaryMenuGroup[] = []; if (runningContainers.length > 0) { groups.push({ name: 'Running', iconName: 'lucide:play', items: runningContainers.map(container => ({ key: container.id, iconName: 'lucide:container', action: () => { this.selectedContainerId = container.id; this.activePanel = 'overview'; }, badge: container.cpuPercent > 0 ? `${container.cpuPercent.toFixed(0)}%` : undefined, badgeVariant: container.cpuPercent > 80 ? 'warning' as const : 'default' as const, })), }); } if (stoppedContainers.length > 0) { groups.push({ name: 'Stopped', iconName: 'lucide:square', items: stoppedContainers.map(container => ({ key: container.id, iconName: 'lucide:container', action: () => { this.selectedContainerId = container.id; this.activePanel = 'overview'; }, })), }); } if (this.containers.length === 0) { groups.push({ name: 'Containers', iconName: 'lucide:container', items: [ { type: 'header' as const, label: 'No containers found', }, ], }); } return groups; } private getSelectedItem(): ISecondaryMenuItem | null { if (!this.selectedContainerId) return null; for (const group of this.getMenuGroups()) { for (const item of group.items) { if ('key' in item && item.key === this.selectedContainerId) { return item; } } } return null; } // Public method to add containers public setContainers(containers: IContainer[]): void { this.containers = containers; // Auto-select first container if none selected if (!this.selectedContainerId && containers.length > 0) { this.selectedContainerId = containers[0].id; } } // Public method to update log entries for the selected container public setLogs(logs: ILogEntry[]): void { this.logEntries = logs; } // Public method to add a single log entry public addLog(log: ILogEntry): void { this.logEntries = [...this.logEntries, log]; } // Events for container actions private emitContainerAction(action: 'start' | 'stop' | 'restart' | 'remove', containerId: string): void { this.dispatchEvent(new CustomEvent('container-action', { detail: { action, containerId }, bubbles: true, composed: true, })); } public render(): TemplateResult { return html`
${this.selectedContainer ? this.renderContainerDetails() : this.renderEmptyState()}
`; } private renderEmptyState(): TemplateResult { return html`
No Container Selected
Select a container from the list to view its details, logs, and statistics.
`; } private renderContainerDetails(): TemplateResult { const container = this.selectedContainer!; return html`
${container.name} ${container.status}
${container.image}
${container.status === 'running' ? html` this.emitContainerAction('stop', container.id)} > Stop this.emitContainerAction('restart', container.id)} > Restart ` : html` this.emitContainerAction('start', container.id)} > Start `} this.emitContainerAction('remove', container.id)} > Remove
${this.renderActivePanel()} `; } private renderActivePanel(): TemplateResult { switch (this.activePanel) { case 'overview': return this.renderOverviewPanel(); case 'logs': return this.renderLogsPanel(); case 'stats': return this.renderStatsPanel(); case 'inspect': return this.renderInspectPanel(); default: return this.renderOverviewPanel(); } } private renderOverviewPanel(): TemplateResult { const container = this.selectedContainer!; const memPercent = container.memoryLimit > 0 ? Math.round((container.memoryUsage / container.memoryLimit) * 100) : 0; const overviewTiles = [ { id: 'cpu', title: 'CPU Usage', value: container.cpuPercent, type: 'gauge' as const, icon: 'lucide:cpu', gaugeOptions: { min: 0, max: 100, thresholds: [ { value: 0, color: 'hsl(142 71% 45%)' }, { value: 60, color: 'hsl(45 93% 47%)' }, { value: 80, color: 'hsl(0 84% 60%)' }, ], }, }, { id: 'memory', title: 'Memory Usage', value: memPercent, type: 'gauge' as const, icon: 'lucide:memoryStick', description: `${this.formatBytes(container.memoryUsage)} / ${this.formatBytes(container.memoryLimit)}`, gaugeOptions: { min: 0, max: 100, thresholds: [ { value: 0, color: 'hsl(142 71% 45%)' }, { value: 70, color: 'hsl(45 93% 47%)' }, { value: 85, color: 'hsl(0 84% 60%)' }, ], }, }, { id: 'network-rx', title: 'Network In', value: this.formatBytes(container.networkRx), type: 'text' as const, icon: 'lucide:download', color: 'hsl(142 71% 45%)', }, { id: 'network-tx', title: 'Network Out', value: this.formatBytes(container.networkTx), type: 'text' as const, icon: 'lucide:upload', color: 'hsl(217 91% 60%)', }, ]; return html`
Container Info
Container ID ${this.formatContainerId(container.id)}
Image ${container.image}
Created ${container.created}
State ${container.state}
Ports
${container.ports.length > 0 ? html`
${container.ports.map(port => html` ${port.hostPort}:${port.containerPort}/${port.protocol} `)}
` : html`
No ports exposed
`}
Networks
${container.networks.length > 0 ? html`
${container.networks.map(network => html` ${network} `)}
` : html`
No networks attached
`}
${container.mounts.length > 0 ? html`
Volumes & Mounts
${container.mounts.map(mount => html`
${mount.source} ${mount.destination} ${mount.mode}
`)}
` : ''}
`; } private renderLogsPanel(): TemplateResult { return html`
`; } private renderStatsPanel(): TemplateResult { const container = this.selectedContainer!; const memPercent = container.memoryLimit > 0 ? Math.round((container.memoryUsage / container.memoryLimit) * 100) : 0; const statsTiles = [ { id: 'cpu-detail', title: 'CPU Usage', value: container.cpuPercent, type: 'gauge' as const, icon: 'lucide:cpu', description: 'Current CPU utilization', gaugeOptions: { min: 0, max: 100, thresholds: [ { value: 0, color: 'hsl(142 71% 45%)' }, { value: 60, color: 'hsl(45 93% 47%)' }, { value: 80, color: 'hsl(0 84% 60%)' }, ], }, }, { id: 'memory-detail', title: 'Memory Usage', value: memPercent, type: 'gauge' as const, icon: 'lucide:memoryStick', gaugeOptions: { min: 0, max: 100, thresholds: [ { value: 0, color: 'hsl(142 71% 45%)' }, { value: 70, color: 'hsl(45 93% 47%)' }, { value: 85, color: 'hsl(0 84% 60%)' }, ], }, }, { id: 'mem-used', title: 'Memory Used', value: this.formatBytes(container.memoryUsage), type: 'text' as const, icon: 'lucide:hardDrive', color: 'hsl(217 91% 60%)', }, { id: 'mem-limit', title: 'Memory Limit', value: this.formatBytes(container.memoryLimit), type: 'text' as const, icon: 'lucide:gauge', color: 'hsl(262 83% 58%)', }, { id: 'net-rx', title: 'Total Network RX', value: this.formatBytes(container.networkRx), type: 'text' as const, icon: 'lucide:download', color: 'hsl(142 71% 45%)', }, { id: 'net-tx', title: 'Total Network TX', value: this.formatBytes(container.networkTx), type: 'text' as const, icon: 'lucide:upload', color: 'hsl(217 91% 60%)', }, ]; return html`
`; } private renderInspectPanel(): TemplateResult { const container = this.selectedContainer!; return html`
Container Details
ID ${container.id}
Name ${container.name}
Image ${container.image}
Created ${container.created}
Status ${container.status}
State ${container.state}
Network Configuration
${container.ports.map(port => html`
Port Mapping ${port.hostPort}:${port.containerPort}/${port.protocol}
`)} ${container.networks.map(network => html`
Network ${network}
`)} ${container.ports.length === 0 && container.networks.length === 0 ? html`
No network configuration
` : ''}
Storage
${container.mounts.length > 0 ? container.mounts.map(mount => html`
${mount.destination} ${mount.source} (${mount.mode})
`) : html`
No volumes mounted
`}
Resource Usage
CPU ${container.cpuPercent.toFixed(2)}%
Memory ${this.formatBytes(container.memoryUsage)} / ${this.formatBytes(container.memoryLimit)}
Network RX ${this.formatBytes(container.networkRx)}
Network TX ${this.formatBytes(container.networkTx)}
`; } }