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`