diff --git a/changelog.md b/changelog.md index 2cd6b9e..5ad6241 100644 --- a/changelog.md +++ b/changelog.md @@ -1,5 +1,10 @@ # Changelog +## 2026-01-12 - 3.39.1 - fix(deps) +bump @design.estate/dees-catalog to ^3.36.0 + +- Updated dependency @design.estate/dees-catalog from ^3.35.0 to ^3.36.0 in package.json + ## 2026-01-12 - 3.39.0 - feat(eco-view-system) add memory usage history, process metrics, and top processes display with loading fallback diff --git a/package.json b/package.json index 0cf1b0f..ebdb33b 100644 --- a/package.json +++ b/package.json @@ -15,7 +15,7 @@ "author": "Lossless GmbH", "license": "MIT", "dependencies": { - "@design.estate/dees-catalog": "^3.35.0", + "@design.estate/dees-catalog": "^3.36.0", "@design.estate/dees-domtools": "^2.3.7", "@design.estate/dees-element": "^2.1.5", "@push.rocks/smartpromise": "^4.2.3", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 41dc04b..8324816 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -9,8 +9,8 @@ importers: .: dependencies: '@design.estate/dees-catalog': - specifier: ^3.35.0 - version: 3.35.0(@tiptap/pm@2.27.2) + specifier: ^3.36.0 + version: 3.36.0(@tiptap/pm@2.27.2) '@design.estate/dees-domtools': specifier: ^2.3.7 version: 2.3.7 @@ -398,8 +398,8 @@ packages: '@configvault.io/interfaces@1.0.17': resolution: {integrity: sha512-bEcCUR2VBDJsTin8HQh8Uw/mlYl2v8A3jMIaQ+MTB9Hrqd6CZL2dL7iJdWyFl/3EIX+LDxWFR+Oq7liIq7w+1Q==} - '@design.estate/dees-catalog@3.35.0': - resolution: {integrity: sha512-6K5ddjpZOh8JVmxr/XkBGOARGLDIXKWPeyo+NeGrmNMG5HOFSdGj2RnDqK9FkH0zxG1u1hG9T/EdIMmcEKgIvw==} + '@design.estate/dees-catalog@3.36.0': + resolution: {integrity: sha512-0buDgj1dL48zN0T669+RjpIvCe5vCH0PTBmIqomeWDSuOO5HpaOmtFlZSkmj0QZ5xYxJ6KkRv8XUi/NN9ogl1Q==} '@design.estate/dees-comms@1.0.30': resolution: {integrity: sha512-KchMlklJfKAjQiJiR0xmofXtQ27VgZtBIxcMwPE9d+h3jJRv+lPZxzBQVOM0eyM0uS44S5vJMZ11IeV4uDXSHg==} @@ -4309,7 +4309,7 @@ snapshots: '@api.global/typedrequest-interfaces': 3.0.19 '@api.global/typedsocket': 4.1.0(@push.rocks/smartserve@1.4.0) '@cloudflare/workers-types': 4.20251211.0 - '@design.estate/dees-catalog': 3.35.0(@tiptap/pm@2.27.2) + '@design.estate/dees-catalog': 3.36.0(@tiptap/pm@2.27.2) '@design.estate/dees-comms': 1.0.30 '@push.rocks/lik': 6.2.2 '@push.rocks/smartdelay': 3.0.5 @@ -5317,7 +5317,7 @@ snapshots: dependencies: '@api.global/typedrequest-interfaces': 3.0.19 - '@design.estate/dees-catalog@3.35.0(@tiptap/pm@2.27.2)': + '@design.estate/dees-catalog@3.36.0(@tiptap/pm@2.27.2)': dependencies: '@design.estate/dees-domtools': 2.3.7 '@design.estate/dees-element': 2.1.5 diff --git a/ts_web/00_commitinfo_data.ts b/ts_web/00_commitinfo_data.ts index a39ac9d..0761568 100644 --- a/ts_web/00_commitinfo_data.ts +++ b/ts_web/00_commitinfo_data.ts @@ -3,6 +3,6 @@ */ export const commitinfo = { name: '@ecobridge.xyz/catalog', - version: '3.39.0', + version: '3.39.1', description: 'A comprehensive library that provides dynamic web components for building sophisticated and modern web applications using JavaScript and TypeScript.' } diff --git a/ts_web/views/eco-view-containers/eco-view-containers.ts b/ts_web/views/eco-view-containers/eco-view-containers.ts new file mode 100644 index 0000000..150e8b3 --- /dev/null +++ b/ts_web/views/eco-view-containers/eco-view-containers.ts @@ -0,0 +1,934 @@ +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)} +
+
+
+ `; + } +}