Compare commits
6 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 5c1fa514b1 | |||
| 3c46becf6c | |||
| fca927cd34 | |||
| 23e5d93183 | |||
| 2ecd05eef2 | |||
| 88381e1fc7 |
19
changelog.md
19
changelog.md
@@ -1,5 +1,24 @@
|
||||
# Changelog
|
||||
|
||||
## 2026-01-16 - 3.40.1 - fix(deps)
|
||||
bump @design.estate/dees-catalog to ^3.37.0 and @types/node to ^25.0.9
|
||||
|
||||
- Upgrade @design.estate/dees-catalog from ^3.36.0 to ^3.37.0
|
||||
- Upgrade @types/node from ^25.0.7 to ^25.0.9
|
||||
|
||||
## 2026-01-13 - 3.40.0 - feat(eco-view-containers)
|
||||
add eco-view-containers demo and export; update remove button to destructive; bump devDependencies
|
||||
|
||||
- Add ts_web/views/eco-view-containers/eco-view-containers.demo.ts with sample container and log data and a demo template
|
||||
- Export eco-view-containers from ts_web/views/eco-view-containers/index.ts and add export to ts_web/views/index.ts
|
||||
- Change remove button in eco-view-containers to use .type='destructive' (removed .status='error')
|
||||
- Bump devDependencies: @git.zone/tsbuild ^4.1.0 -> ^4.1.2 and @types/node ^25.0.6 -> ^25.0.7
|
||||
|
||||
## 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
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@ecobridge.xyz/catalog",
|
||||
"version": "3.39.0",
|
||||
"version": "3.40.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.37.0",
|
||||
"@design.estate/dees-domtools": "^2.3.7",
|
||||
"@design.estate/dees-element": "^2.1.5",
|
||||
"@push.rocks/smartpromise": "^4.2.3",
|
||||
@@ -23,12 +23,12 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@design.estate/dees-wcctools": "^3.7.1",
|
||||
"@git.zone/tsbuild": "^4.1.0",
|
||||
"@git.zone/tsbuild": "^4.1.2",
|
||||
"@git.zone/tsbundle": "^2.8.1",
|
||||
"@git.zone/tstest": "^3.1.4",
|
||||
"@git.zone/tswatch": "^2.3.13",
|
||||
"@push.rocks/projectinfo": "^5.0.2",
|
||||
"@types/node": "^25.0.6"
|
||||
"@types/node": "^25.0.9"
|
||||
},
|
||||
"files": [
|
||||
"ts/**/*",
|
||||
|
||||
398
pnpm-lock.yaml
generated
398
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@@ -3,6 +3,6 @@
|
||||
*/
|
||||
export const commitinfo = {
|
||||
name: '@ecobridge.xyz/catalog',
|
||||
version: '3.39.0',
|
||||
version: '3.40.1',
|
||||
description: 'A comprehensive library that provides dynamic web components for building sophisticated and modern web applications using JavaScript and TypeScript.'
|
||||
}
|
||||
|
||||
149
ts_web/views/eco-view-containers/eco-view-containers.demo.ts
Normal file
149
ts_web/views/eco-view-containers/eco-view-containers.demo.ts
Normal file
@@ -0,0 +1,149 @@
|
||||
import { html } from '@design.estate/dees-element';
|
||||
import type { IContainer } from './eco-view-containers.js';
|
||||
import type { ILogEntry } from '@design.estate/dees-catalog';
|
||||
|
||||
const sampleContainers: IContainer[] = [
|
||||
{
|
||||
id: 'a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6q7r8s9t0u1v2w3x4y5z6',
|
||||
name: 'nginx-proxy',
|
||||
image: 'nginx:alpine',
|
||||
status: 'running',
|
||||
state: 'Up 3 days',
|
||||
created: '2025-01-09T14:30:00Z',
|
||||
ports: [
|
||||
{ hostPort: 80, containerPort: 80, protocol: 'tcp' },
|
||||
{ hostPort: 443, containerPort: 443, protocol: 'tcp' },
|
||||
],
|
||||
networks: ['bridge', 'web-network'],
|
||||
mounts: [
|
||||
{ source: '/etc/nginx/conf.d', destination: '/etc/nginx/conf.d', mode: 'ro' },
|
||||
{ source: '/var/log/nginx', destination: '/var/log/nginx', mode: 'rw' },
|
||||
],
|
||||
cpuPercent: 2.5,
|
||||
memoryUsage: 52428800, // 50 MB
|
||||
memoryLimit: 536870912, // 512 MB
|
||||
networkRx: 1073741824, // 1 GB
|
||||
networkTx: 536870912, // 512 MB
|
||||
},
|
||||
{
|
||||
id: 'b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6q7r8s9t0u1v2w3x4y5z6a1',
|
||||
name: 'postgres-db',
|
||||
image: 'postgres:15',
|
||||
status: 'running',
|
||||
state: 'Up 5 days',
|
||||
created: '2025-01-07T10:00:00Z',
|
||||
ports: [
|
||||
{ hostPort: 5432, containerPort: 5432, protocol: 'tcp' },
|
||||
],
|
||||
networks: ['db-network'],
|
||||
mounts: [
|
||||
{ source: '/var/lib/postgresql/data', destination: '/var/lib/postgresql/data', mode: 'rw' },
|
||||
],
|
||||
cpuPercent: 8.3,
|
||||
memoryUsage: 268435456, // 256 MB
|
||||
memoryLimit: 1073741824, // 1 GB
|
||||
networkRx: 2147483648, // 2 GB
|
||||
networkTx: 1073741824, // 1 GB
|
||||
},
|
||||
{
|
||||
id: 'c3d4e5f6g7h8i9j0k1l2m3n4o5p6q7r8s9t0u1v2w3x4y5z6a1b2',
|
||||
name: 'redis-cache',
|
||||
image: 'redis:7-alpine',
|
||||
status: 'running',
|
||||
state: 'Up 2 hours',
|
||||
created: '2025-01-12T08:00:00Z',
|
||||
ports: [
|
||||
{ hostPort: 6379, containerPort: 6379, protocol: 'tcp' },
|
||||
],
|
||||
networks: ['cache-network', 'web-network'],
|
||||
mounts: [],
|
||||
cpuPercent: 0.5,
|
||||
memoryUsage: 16777216, // 16 MB
|
||||
memoryLimit: 134217728, // 128 MB
|
||||
networkRx: 52428800, // 50 MB
|
||||
networkTx: 26214400, // 25 MB
|
||||
},
|
||||
{
|
||||
id: 'd4e5f6g7h8i9j0k1l2m3n4o5p6q7r8s9t0u1v2w3x4y5z6a1b2c3',
|
||||
name: 'node-api',
|
||||
image: 'node:20-alpine',
|
||||
status: 'running',
|
||||
state: 'Up 1 day',
|
||||
created: '2025-01-11T12:00:00Z',
|
||||
ports: [
|
||||
{ hostPort: 3000, containerPort: 3000, protocol: 'tcp' },
|
||||
],
|
||||
networks: ['web-network', 'db-network'],
|
||||
mounts: [
|
||||
{ source: '/app', destination: '/app', mode: 'rw' },
|
||||
{ source: '/app/node_modules', destination: '/app/node_modules', mode: 'rw' },
|
||||
],
|
||||
cpuPercent: 45.2,
|
||||
memoryUsage: 524288000, // 500 MB
|
||||
memoryLimit: 1073741824, // 1 GB
|
||||
networkRx: 104857600, // 100 MB
|
||||
networkTx: 52428800, // 50 MB
|
||||
},
|
||||
{
|
||||
id: 'e5f6g7h8i9j0k1l2m3n4o5p6q7r8s9t0u1v2w3x4y5z6a1b2c3d4',
|
||||
name: 'mongodb-old',
|
||||
image: 'mongo:4.4',
|
||||
status: 'stopped',
|
||||
state: 'Exited (0) 2 days ago',
|
||||
created: '2024-12-20T10:00:00Z',
|
||||
ports: [],
|
||||
networks: ['db-network'],
|
||||
mounts: [
|
||||
{ source: '/var/lib/mongodb', destination: '/data/db', mode: 'rw' },
|
||||
],
|
||||
cpuPercent: 0,
|
||||
memoryUsage: 0,
|
||||
memoryLimit: 2147483648, // 2 GB
|
||||
networkRx: 0,
|
||||
networkTx: 0,
|
||||
},
|
||||
];
|
||||
|
||||
const sampleLogs: ILogEntry[] = [
|
||||
{ timestamp: '2025-01-12T10:00:00Z', level: 'info', message: 'Container started successfully', source: 'docker' },
|
||||
{ timestamp: '2025-01-12T10:00:01Z', level: 'info', message: 'Listening on port 80', source: 'nginx' },
|
||||
{ timestamp: '2025-01-12T10:00:01Z', level: 'info', message: 'Listening on port 443', source: 'nginx' },
|
||||
{ timestamp: '2025-01-12T10:00:05Z', level: 'debug', message: 'Worker process started (PID: 1234)', source: 'nginx' },
|
||||
{ timestamp: '2025-01-12T10:00:10Z', level: 'info', message: 'Configuration loaded from /etc/nginx/nginx.conf', source: 'nginx' },
|
||||
{ timestamp: '2025-01-12T10:01:00Z', level: 'info', message: 'GET /api/health 200 5ms', source: 'nginx' },
|
||||
{ timestamp: '2025-01-12T10:01:15Z', level: 'info', message: 'GET /api/users 200 45ms', source: 'nginx' },
|
||||
{ timestamp: '2025-01-12T10:01:30Z', level: 'warn', message: 'Upstream server temporarily unavailable', source: 'nginx' },
|
||||
{ timestamp: '2025-01-12T10:01:31Z', level: 'info', message: 'Retrying upstream connection...', source: 'nginx' },
|
||||
{ timestamp: '2025-01-12T10:01:32Z', level: 'success', message: 'Upstream connection restored', source: 'nginx' },
|
||||
{ timestamp: '2025-01-12T10:02:00Z', level: 'info', message: 'POST /api/login 200 120ms', source: 'nginx' },
|
||||
{ timestamp: '2025-01-12T10:02:30Z', level: 'info', message: 'GET /api/products 200 80ms', source: 'nginx' },
|
||||
{ timestamp: '2025-01-12T10:03:00Z', level: 'error', message: 'Connection refused from 192.168.1.100', source: 'nginx' },
|
||||
{ timestamp: '2025-01-12T10:03:05Z', level: 'warn', message: 'Rate limit exceeded for IP 192.168.1.100', source: 'nginx' },
|
||||
{ timestamp: '2025-01-12T10:03:30Z', level: 'info', message: 'GET /api/health 200 3ms', source: 'nginx' },
|
||||
{ timestamp: '2025-01-12T10:04:00Z', level: 'debug', message: 'Cache hit for /static/main.js', source: 'nginx' },
|
||||
{ timestamp: '2025-01-12T10:04:30Z', level: 'info', message: 'SSL certificate valid for 89 days', source: 'nginx' },
|
||||
{ timestamp: '2025-01-12T10:05:00Z', level: 'info', message: 'GET /api/orders 200 150ms', source: 'nginx' },
|
||||
];
|
||||
|
||||
export const demo = () => html`
|
||||
<style>
|
||||
.demo-container {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
min-height: 600px;
|
||||
background: hsl(240 10% 4%);
|
||||
border-radius: 12px;
|
||||
overflow: hidden;
|
||||
}
|
||||
</style>
|
||||
<div class="demo-container">
|
||||
<eco-view-containers
|
||||
.containers=${sampleContainers}
|
||||
.logEntries=${sampleLogs}
|
||||
@container-action=${(e: CustomEvent) => {
|
||||
console.log('Container action:', e.detail);
|
||||
alert(`Action: ${e.detail.action} on container ${e.detail.containerId.substring(0, 12)}`);
|
||||
}}
|
||||
></eco-view-containers>
|
||||
</div>
|
||||
`;
|
||||
933
ts_web/views/eco-view-containers/eco-view-containers.ts
Normal file
933
ts_web/views/eco-view-containers/eco-view-containers.ts
Normal file
@@ -0,0 +1,933 @@
|
||||
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=${'destructive'}
|
||||
@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>
|
||||
`;
|
||||
}
|
||||
}
|
||||
1
ts_web/views/eco-view-containers/index.ts
Normal file
1
ts_web/views/eco-view-containers/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from './eco-view-containers.js';
|
||||
@@ -6,3 +6,4 @@ export * from './eco-view-home/index.js';
|
||||
export * from './eco-view-login/index.js';
|
||||
export * from './eco-view-scan/index.js';
|
||||
export * from './eco-view-browser/index.js';
|
||||
export * from './eco-view-containers/index.js';
|
||||
|
||||
Reference in New Issue
Block a user