2 Commits

Author SHA1 Message Date
2ecd05eef2 v3.39.1
Some checks failed
Default (tags) / security (push) Failing after 1s
Default (tags) / test (push) Failing after 1s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2026-01-12 23:48:47 +00:00
88381e1fc7 fix(deps): bump @design.estate/dees-catalog to ^3.36.0 2026-01-12 23:48:47 +00:00
5 changed files with 948 additions and 9 deletions

View File

@@ -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

View File

@@ -1,6 +1,6 @@
{
"name": "@ecobridge.xyz/catalog",
"version": "3.39.0",
"version": "3.39.1",
"private": false,
"description": "A comprehensive library that provides dynamic web components for building sophisticated and modern web applications using JavaScript and TypeScript.",
"main": "dist_ts_web/index.js",
@@ -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",

12
pnpm-lock.yaml generated
View File

@@ -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

View File

@@ -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.'
}

View File

@@ -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`
<div class="containers-container">
<dees-appui-secondarymenu
.heading=${'Containers'}
.groups=${this.getMenuGroups()}
.selectedItem=${this.getSelectedItem()}
></dees-appui-secondarymenu>
<div class="content">
${this.selectedContainer ? this.renderContainerDetails() : this.renderEmptyState()}
</div>
</div>
`;
}
private renderEmptyState(): TemplateResult {
return html`
<div class="empty-state">
<dees-icon .icon=${'lucide:container'} .iconSize=${64}></dees-icon>
<div class="empty-state-title">No Container Selected</div>
<div class="empty-state-description">
Select a container from the list to view its details, logs, and statistics.
</div>
</div>
`;
}
private renderContainerDetails(): TemplateResult {
const container = this.selectedContainer!;
return html`
<div class="panel-header">
<div class="panel-title">
${container.name}
<span class="status-badge ${container.status}">
<span class="status-dot"></span>
${container.status}
</span>
</div>
<div class="panel-description">${container.image}</div>
</div>
<div class="actions-bar">
${container.status === 'running' ? html`
<dees-button
.type=${'secondary'}
@click=${() => this.emitContainerAction('stop', container.id)}
>
<dees-icon .icon=${'lucide:square'} .iconSize=${14}></dees-icon>
Stop
</dees-button>
<dees-button
.type=${'secondary'}
@click=${() => this.emitContainerAction('restart', container.id)}
>
<dees-icon .icon=${'lucide:refreshCw'} .iconSize=${14}></dees-icon>
Restart
</dees-button>
` : html`
<dees-button
.type=${'secondary'}
@click=${() => this.emitContainerAction('start', container.id)}
>
<dees-icon .icon=${'lucide:play'} .iconSize=${14}></dees-icon>
Start
</dees-button>
`}
<dees-button
.type=${'secondary'}
.status=${'error'}
@click=${() => this.emitContainerAction('remove', container.id)}
>
<dees-icon .icon=${'lucide:trash2'} .iconSize=${14}></dees-icon>
Remove
</dees-button>
</div>
<div class="tabs-container">
<button
class="tab-button ${this.activePanel === 'overview' ? 'active' : ''}"
@click=${() => this.activePanel = 'overview'}
>Overview</button>
<button
class="tab-button ${this.activePanel === 'logs' ? 'active' : ''}"
@click=${() => this.activePanel = 'logs'}
>Logs</button>
<button
class="tab-button ${this.activePanel === 'stats' ? 'active' : ''}"
@click=${() => this.activePanel = 'stats'}
>Stats</button>
<button
class="tab-button ${this.activePanel === 'inspect' ? 'active' : ''}"
@click=${() => this.activePanel = 'inspect'}
>Inspect</button>
</div>
${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`
<div class="stats-section">
<dees-statsgrid
.tiles=${overviewTiles}
.minTileWidth=${200}
.gap=${16}
></dees-statsgrid>
</div>
<div class="info-grid">
<div class="info-card">
<div class="info-card-title">Container Info</div>
<div class="info-row">
<span class="info-label">Container ID</span>
<span class="info-value">${this.formatContainerId(container.id)}</span>
</div>
<div class="info-row">
<span class="info-label">Image</span>
<span class="info-value">${container.image}</span>
</div>
<div class="info-row">
<span class="info-label">Created</span>
<span class="info-value">${container.created}</span>
</div>
<div class="info-row">
<span class="info-label">State</span>
<span class="info-value">${container.state}</span>
</div>
</div>
<div class="info-card">
<div class="info-card-title">Ports</div>
${container.ports.length > 0 ? html`
<div class="ports-list">
${container.ports.map(port => html`
<span class="port-badge">
${port.hostPort}:${port.containerPort}/${port.protocol}
</span>
`)}
</div>
` : html`
<div class="info-row">
<span class="info-label">No ports exposed</span>
</div>
`}
</div>
<div class="info-card">
<div class="info-card-title">Networks</div>
${container.networks.length > 0 ? html`
<div class="networks-list">
${container.networks.map(network => html`
<span class="network-badge">${network}</span>
`)}
</div>
` : html`
<div class="info-row">
<span class="info-label">No networks attached</span>
</div>
`}
</div>
${container.mounts.length > 0 ? html`
<div class="info-card">
<div class="info-card-title">Volumes & Mounts</div>
<div class="mounts-list">
${container.mounts.map(mount => html`
<div class="mount-item">
<span class="mount-source">${mount.source}</span>
<span class="mount-arrow">→</span>
<span class="mount-dest">${mount.destination}</span>
<span class="mount-mode">${mount.mode}</span>
</div>
`)}
</div>
</div>
` : ''}
</div>
`;
}
private renderLogsPanel(): TemplateResult {
return html`
<div class="logs-container">
<dees-chart-log
.label=${'Container Logs'}
.mode=${'structured'}
.logEntries=${this.logEntries}
.autoScroll=${true}
.showMetrics=${true}
.maxEntries=${1000}
></dees-chart-log>
</div>
`;
}
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`
<div class="stats-section">
<dees-statsgrid
.tiles=${statsTiles}
.minTileWidth=${200}
.gap=${16}
></dees-statsgrid>
</div>
`;
}
private renderInspectPanel(): TemplateResult {
const container = this.selectedContainer!;
return html`
<div class="info-grid">
<div class="info-card">
<div class="info-card-title">Container Details</div>
<div class="info-row">
<span class="info-label">ID</span>
<span class="info-value">${container.id}</span>
</div>
<div class="info-row">
<span class="info-label">Name</span>
<span class="info-value">${container.name}</span>
</div>
<div class="info-row">
<span class="info-label">Image</span>
<span class="info-value">${container.image}</span>
</div>
<div class="info-row">
<span class="info-label">Created</span>
<span class="info-value">${container.created}</span>
</div>
<div class="info-row">
<span class="info-label">Status</span>
<span class="info-value">${container.status}</span>
</div>
<div class="info-row">
<span class="info-label">State</span>
<span class="info-value">${container.state}</span>
</div>
</div>
<div class="info-card">
<div class="info-card-title">Network Configuration</div>
${container.ports.map(port => html`
<div class="info-row">
<span class="info-label">Port Mapping</span>
<span class="info-value">${port.hostPort}:${port.containerPort}/${port.protocol}</span>
</div>
`)}
${container.networks.map(network => html`
<div class="info-row">
<span class="info-label">Network</span>
<span class="info-value">${network}</span>
</div>
`)}
${container.ports.length === 0 && container.networks.length === 0 ? html`
<div class="info-row">
<span class="info-label">No network configuration</span>
</div>
` : ''}
</div>
<div class="info-card">
<div class="info-card-title">Storage</div>
${container.mounts.length > 0 ? container.mounts.map(mount => html`
<div class="info-row">
<span class="info-label">${mount.destination}</span>
<span class="info-value">${mount.source} (${mount.mode})</span>
</div>
`) : html`
<div class="info-row">
<span class="info-label">No volumes mounted</span>
</div>
`}
</div>
<div class="info-card">
<div class="info-card-title">Resource Usage</div>
<div class="info-row">
<span class="info-label">CPU</span>
<span class="info-value">${container.cpuPercent.toFixed(2)}%</span>
</div>
<div class="info-row">
<span class="info-label">Memory</span>
<span class="info-value">${this.formatBytes(container.memoryUsage)} / ${this.formatBytes(container.memoryLimit)}</span>
</div>
<div class="info-row">
<span class="info-label">Network RX</span>
<span class="info-value">${this.formatBytes(container.networkRx)}</span>
</div>
<div class="info-row">
<span class="info-label">Network TX</span>
<span class="info-value">${this.formatBytes(container.networkTx)}</span>
</div>
</div>
</div>
`;
}
}