Compare commits
6 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 2ecd05eef2 | |||
| 88381e1fc7 | |||
| f74c8e5410 | |||
| abeb8ecda3 | |||
| b5dc6204ce | |||
| b800740d5d |
22
changelog.md
22
changelog.md
@@ -1,5 +1,27 @@
|
|||||||
# Changelog
|
# 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
|
||||||
|
|
||||||
|
- Add memoryUsageHistory state and update it in setMetrics to track memory usage trend
|
||||||
|
- Introduce process state: processTotal, processRunning, processSleeping, processBlocked, and topProcesses
|
||||||
|
- Extend setProcesses signature to accept blocked and a typed process list and store values in state
|
||||||
|
- Use reactive state values in UI tiles (memory trend, process tiles) instead of hardcoded placeholders
|
||||||
|
- Replace 'Threads' tile with 'Blocked' tile and update its icon and color
|
||||||
|
- Render top processes from state and show a 'Loading...' fallback when the list is empty
|
||||||
|
|
||||||
|
## 2026-01-12 - 3.38.0 - feat(eco-view-system)
|
||||||
|
add extended system metrics and display formatted total network usage in eco system view
|
||||||
|
|
||||||
|
- Added numerous new observable properties for richer system telemetry: cpuPhysicalCores, cpuSpeedMax, memoryAvailable, memoryCached, memoryBuffers, swapTotal, swapUsed, diskTotal, diskUsed, diskFree, networkRxSec, networkTxSec, networkRxTotal, networkTxTotal, distro, and coreLoads.
|
||||||
|
- Reworked network/usage UI: removed several temporary network/latency cards and replaced them with Total Downloaded/Uploaded cards that use formatBytes(...) and show totals since boot.
|
||||||
|
- Bumped dependency @design.estate/dees-catalog from ^3.34.1 to ^3.35.0.
|
||||||
|
|
||||||
## 2026-01-12 - 3.37.0 - feat(elements)
|
## 2026-01-12 - 3.37.0 - feat(elements)
|
||||||
add eco-provider-frame and dataprovider interfaces; improve virtual keyboard interactions; add demos, exports and bump dev dependencies
|
add eco-provider-frame and dataprovider interfaces; improve virtual keyboard interactions; add demos, exports and bump dev dependencies
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@ecobridge.xyz/catalog",
|
"name": "@ecobridge.xyz/catalog",
|
||||||
"version": "3.37.0",
|
"version": "3.39.1",
|
||||||
"private": false,
|
"private": false,
|
||||||
"description": "A comprehensive library that provides dynamic web components for building sophisticated and modern web applications using JavaScript and TypeScript.",
|
"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",
|
"main": "dist_ts_web/index.js",
|
||||||
@@ -15,7 +15,7 @@
|
|||||||
"author": "Lossless GmbH",
|
"author": "Lossless GmbH",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@design.estate/dees-catalog": "^3.34.1",
|
"@design.estate/dees-catalog": "^3.36.0",
|
||||||
"@design.estate/dees-domtools": "^2.3.7",
|
"@design.estate/dees-domtools": "^2.3.7",
|
||||||
"@design.estate/dees-element": "^2.1.5",
|
"@design.estate/dees-element": "^2.1.5",
|
||||||
"@push.rocks/smartpromise": "^4.2.3",
|
"@push.rocks/smartpromise": "^4.2.3",
|
||||||
|
|||||||
12
pnpm-lock.yaml
generated
12
pnpm-lock.yaml
generated
@@ -9,8 +9,8 @@ importers:
|
|||||||
.:
|
.:
|
||||||
dependencies:
|
dependencies:
|
||||||
'@design.estate/dees-catalog':
|
'@design.estate/dees-catalog':
|
||||||
specifier: ^3.34.1
|
specifier: ^3.36.0
|
||||||
version: 3.34.1(@tiptap/pm@2.27.2)
|
version: 3.36.0(@tiptap/pm@2.27.2)
|
||||||
'@design.estate/dees-domtools':
|
'@design.estate/dees-domtools':
|
||||||
specifier: ^2.3.7
|
specifier: ^2.3.7
|
||||||
version: 2.3.7
|
version: 2.3.7
|
||||||
@@ -398,8 +398,8 @@ packages:
|
|||||||
'@configvault.io/interfaces@1.0.17':
|
'@configvault.io/interfaces@1.0.17':
|
||||||
resolution: {integrity: sha512-bEcCUR2VBDJsTin8HQh8Uw/mlYl2v8A3jMIaQ+MTB9Hrqd6CZL2dL7iJdWyFl/3EIX+LDxWFR+Oq7liIq7w+1Q==}
|
resolution: {integrity: sha512-bEcCUR2VBDJsTin8HQh8Uw/mlYl2v8A3jMIaQ+MTB9Hrqd6CZL2dL7iJdWyFl/3EIX+LDxWFR+Oq7liIq7w+1Q==}
|
||||||
|
|
||||||
'@design.estate/dees-catalog@3.34.1':
|
'@design.estate/dees-catalog@3.36.0':
|
||||||
resolution: {integrity: sha512-jm/ZPoLgBHicIlrGPTiCynztFEemJGHweiz+GQK9FppFfjCCC47AOHI4ZjIhrNibfyPXGfqfFWr7DI0zCOl1gw==}
|
resolution: {integrity: sha512-0buDgj1dL48zN0T669+RjpIvCe5vCH0PTBmIqomeWDSuOO5HpaOmtFlZSkmj0QZ5xYxJ6KkRv8XUi/NN9ogl1Q==}
|
||||||
|
|
||||||
'@design.estate/dees-comms@1.0.30':
|
'@design.estate/dees-comms@1.0.30':
|
||||||
resolution: {integrity: sha512-KchMlklJfKAjQiJiR0xmofXtQ27VgZtBIxcMwPE9d+h3jJRv+lPZxzBQVOM0eyM0uS44S5vJMZ11IeV4uDXSHg==}
|
resolution: {integrity: sha512-KchMlklJfKAjQiJiR0xmofXtQ27VgZtBIxcMwPE9d+h3jJRv+lPZxzBQVOM0eyM0uS44S5vJMZ11IeV4uDXSHg==}
|
||||||
@@ -4309,7 +4309,7 @@ snapshots:
|
|||||||
'@api.global/typedrequest-interfaces': 3.0.19
|
'@api.global/typedrequest-interfaces': 3.0.19
|
||||||
'@api.global/typedsocket': 4.1.0(@push.rocks/smartserve@1.4.0)
|
'@api.global/typedsocket': 4.1.0(@push.rocks/smartserve@1.4.0)
|
||||||
'@cloudflare/workers-types': 4.20251211.0
|
'@cloudflare/workers-types': 4.20251211.0
|
||||||
'@design.estate/dees-catalog': 3.34.1(@tiptap/pm@2.27.2)
|
'@design.estate/dees-catalog': 3.36.0(@tiptap/pm@2.27.2)
|
||||||
'@design.estate/dees-comms': 1.0.30
|
'@design.estate/dees-comms': 1.0.30
|
||||||
'@push.rocks/lik': 6.2.2
|
'@push.rocks/lik': 6.2.2
|
||||||
'@push.rocks/smartdelay': 3.0.5
|
'@push.rocks/smartdelay': 3.0.5
|
||||||
@@ -5317,7 +5317,7 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
'@api.global/typedrequest-interfaces': 3.0.19
|
'@api.global/typedrequest-interfaces': 3.0.19
|
||||||
|
|
||||||
'@design.estate/dees-catalog@3.34.1(@tiptap/pm@2.27.2)':
|
'@design.estate/dees-catalog@3.36.0(@tiptap/pm@2.27.2)':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@design.estate/dees-domtools': 2.3.7
|
'@design.estate/dees-domtools': 2.3.7
|
||||||
'@design.estate/dees-element': 2.1.5
|
'@design.estate/dees-element': 2.1.5
|
||||||
|
|||||||
@@ -3,6 +3,6 @@
|
|||||||
*/
|
*/
|
||||||
export const commitinfo = {
|
export const commitinfo = {
|
||||||
name: '@ecobridge.xyz/catalog',
|
name: '@ecobridge.xyz/catalog',
|
||||||
version: '3.37.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.'
|
description: 'A comprehensive library that provides dynamic web components for building sophisticated and modern web applications using JavaScript and TypeScript.'
|
||||||
}
|
}
|
||||||
|
|||||||
934
ts_web/views/eco-view-containers/eco-view-containers.ts
Normal file
934
ts_web/views/eco-view-containers/eco-view-containers.ts
Normal 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>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -168,12 +168,18 @@ export class EcoViewSystem extends DeesElement {
|
|||||||
@property({ type: Number })
|
@property({ type: Number })
|
||||||
accessor cpuCores = 0;
|
accessor cpuCores = 0;
|
||||||
|
|
||||||
|
@property({ type: Number })
|
||||||
|
accessor cpuPhysicalCores = 0;
|
||||||
|
|
||||||
@property({ type: String })
|
@property({ type: String })
|
||||||
accessor cpuModel = 'Unknown';
|
accessor cpuModel = 'Unknown';
|
||||||
|
|
||||||
@property({ type: Number })
|
@property({ type: Number })
|
||||||
accessor cpuSpeed = 0;
|
accessor cpuSpeed = 0;
|
||||||
|
|
||||||
|
@property({ type: Number })
|
||||||
|
accessor cpuSpeedMax = 0;
|
||||||
|
|
||||||
@property({ type: Number })
|
@property({ type: Number })
|
||||||
accessor memoryTotal = 0;
|
accessor memoryTotal = 0;
|
||||||
|
|
||||||
@@ -183,41 +189,155 @@ export class EcoViewSystem extends DeesElement {
|
|||||||
@property({ type: Number })
|
@property({ type: Number })
|
||||||
accessor memoryFree = 0;
|
accessor memoryFree = 0;
|
||||||
|
|
||||||
|
@property({ type: Number })
|
||||||
|
accessor memoryAvailable = 0;
|
||||||
|
|
||||||
|
@property({ type: Number })
|
||||||
|
accessor memoryCached = 0;
|
||||||
|
|
||||||
|
@property({ type: Number })
|
||||||
|
accessor memoryBuffers = 0;
|
||||||
|
|
||||||
|
@property({ type: Number })
|
||||||
|
accessor swapTotal = 0;
|
||||||
|
|
||||||
|
@property({ type: Number })
|
||||||
|
accessor swapUsed = 0;
|
||||||
|
|
||||||
|
@property({ type: Number })
|
||||||
|
accessor diskTotal = 0;
|
||||||
|
|
||||||
|
@property({ type: Number })
|
||||||
|
accessor diskUsed = 0;
|
||||||
|
|
||||||
|
@property({ type: Number })
|
||||||
|
accessor diskFree = 0;
|
||||||
|
|
||||||
|
@property({ type: Number })
|
||||||
|
accessor networkRxSec = 0;
|
||||||
|
|
||||||
|
@property({ type: Number })
|
||||||
|
accessor networkTxSec = 0;
|
||||||
|
|
||||||
|
@property({ type: Number })
|
||||||
|
accessor networkRxTotal = 0;
|
||||||
|
|
||||||
|
@property({ type: Number })
|
||||||
|
accessor networkTxTotal = 0;
|
||||||
|
|
||||||
@property({ type: String })
|
@property({ type: String })
|
||||||
accessor hostname = 'Unknown';
|
accessor hostname = 'Unknown';
|
||||||
|
|
||||||
@property({ type: String })
|
@property({ type: String })
|
||||||
accessor platform = 'Unknown';
|
accessor platform = 'Unknown';
|
||||||
|
|
||||||
|
@property({ type: String })
|
||||||
|
accessor distro = '';
|
||||||
|
|
||||||
@property({ type: Array })
|
@property({ type: Array })
|
||||||
accessor loadAvg: number[] = [0, 0, 0];
|
accessor loadAvg: number[] = [0, 0, 0];
|
||||||
|
|
||||||
@state()
|
@property({ type: Array })
|
||||||
accessor networkIn = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0];
|
accessor coreLoads: number[] = [];
|
||||||
|
|
||||||
@state()
|
@state()
|
||||||
accessor networkOut = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0];
|
accessor networkInHistory: number[] = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0];
|
||||||
|
|
||||||
|
@state()
|
||||||
|
accessor networkOutHistory: number[] = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0];
|
||||||
|
|
||||||
|
@state()
|
||||||
|
accessor memoryUsageHistory: number[] = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0];
|
||||||
|
|
||||||
|
// Process data
|
||||||
|
@state()
|
||||||
|
accessor processTotal = 0;
|
||||||
|
|
||||||
|
@state()
|
||||||
|
accessor processRunning = 0;
|
||||||
|
|
||||||
|
@state()
|
||||||
|
accessor processSleeping = 0;
|
||||||
|
|
||||||
|
@state()
|
||||||
|
accessor processBlocked = 0;
|
||||||
|
|
||||||
|
@state()
|
||||||
|
accessor topProcesses: Array<{ name: string; pid: number; cpu: number; memory: number; state: string }> = [];
|
||||||
|
|
||||||
// Public method to update metrics from backend
|
// Public method to update metrics from backend
|
||||||
public setMetrics(metrics: {
|
public setMetrics(metrics: {
|
||||||
cpu: { usage: number; cores: number; model: string; speed: number; loadAvg: number[] };
|
cpu: { usage: number; cores: number; physicalCores?: number; model: string; speed: number; speedMax?: number; loadAvg: number[]; coreLoads?: number[] };
|
||||||
memory: { total: number; used: number; free: number; usagePercent: number };
|
memory: { total: number; used: number; free: number; available?: number; usagePercent: number; swapTotal?: number; swapUsed?: number; cached?: number; buffers?: number };
|
||||||
system: { platform: string; hostname: string; uptimeFormatted: string };
|
disk?: { total: number; used: number; free: number; usagePercent: number };
|
||||||
|
network?: { rxSec: number; txSec: number; rxTotal: number; txTotal: number };
|
||||||
|
system: { platform: string; distro?: string; hostname: string; uptimeFormatted: string };
|
||||||
}): void {
|
}): void {
|
||||||
|
// CPU metrics
|
||||||
this.cpuUsage = metrics.cpu.usage;
|
this.cpuUsage = metrics.cpu.usage;
|
||||||
this.cpuCores = metrics.cpu.cores;
|
this.cpuCores = metrics.cpu.cores;
|
||||||
|
this.cpuPhysicalCores = metrics.cpu.physicalCores || metrics.cpu.cores;
|
||||||
this.cpuModel = metrics.cpu.model;
|
this.cpuModel = metrics.cpu.model;
|
||||||
this.cpuSpeed = metrics.cpu.speed;
|
this.cpuSpeed = metrics.cpu.speed;
|
||||||
|
this.cpuSpeedMax = metrics.cpu.speedMax || metrics.cpu.speed;
|
||||||
this.loadAvg = metrics.cpu.loadAvg;
|
this.loadAvg = metrics.cpu.loadAvg;
|
||||||
|
this.coreLoads = metrics.cpu.coreLoads || [];
|
||||||
|
|
||||||
|
// Memory metrics
|
||||||
this.memoryUsage = metrics.memory.usagePercent;
|
this.memoryUsage = metrics.memory.usagePercent;
|
||||||
this.memoryTotal = metrics.memory.total;
|
this.memoryTotal = metrics.memory.total;
|
||||||
this.memoryUsed = metrics.memory.used;
|
this.memoryUsed = metrics.memory.used;
|
||||||
this.memoryFree = metrics.memory.free;
|
this.memoryFree = metrics.memory.free;
|
||||||
|
this.memoryAvailable = metrics.memory.available || metrics.memory.free;
|
||||||
|
this.memoryCached = metrics.memory.cached || 0;
|
||||||
|
this.memoryBuffers = metrics.memory.buffers || 0;
|
||||||
|
this.swapTotal = metrics.memory.swapTotal || 0;
|
||||||
|
this.swapUsed = metrics.memory.swapUsed || 0;
|
||||||
|
|
||||||
|
// Update memory usage history for trend chart
|
||||||
|
this.memoryUsageHistory = [...this.memoryUsageHistory.slice(1), metrics.memory.usagePercent];
|
||||||
|
|
||||||
|
// Disk metrics
|
||||||
|
if (metrics.disk) {
|
||||||
|
this.diskUsage = metrics.disk.usagePercent;
|
||||||
|
this.diskTotal = metrics.disk.total;
|
||||||
|
this.diskUsed = metrics.disk.used;
|
||||||
|
this.diskFree = metrics.disk.free;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Network metrics
|
||||||
|
if (metrics.network) {
|
||||||
|
this.networkRxSec = metrics.network.rxSec;
|
||||||
|
this.networkTxSec = metrics.network.txSec;
|
||||||
|
this.networkRxTotal = metrics.network.rxTotal;
|
||||||
|
this.networkTxTotal = metrics.network.txTotal;
|
||||||
|
|
||||||
|
// Update history for trend charts
|
||||||
|
this.networkInHistory = [...this.networkInHistory.slice(1), metrics.network.rxSec];
|
||||||
|
this.networkOutHistory = [...this.networkOutHistory.slice(1), metrics.network.txSec];
|
||||||
|
}
|
||||||
|
|
||||||
|
// System metrics
|
||||||
this.hostname = metrics.system.hostname;
|
this.hostname = metrics.system.hostname;
|
||||||
this.platform = metrics.system.platform;
|
this.platform = metrics.system.platform;
|
||||||
|
this.distro = metrics.system.distro || '';
|
||||||
this.uptime = metrics.system.uptimeFormatted;
|
this.uptime = metrics.system.uptimeFormatted;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Method to set CPU temperature separately (may not always be available)
|
||||||
|
public setTemperature(temp: { main: number; cores?: number[]; max?: number }): void {
|
||||||
|
this.cpuTemp = temp.main || 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Method to set processes data
|
||||||
|
public setProcesses(data: { total: number; running: number; sleeping: number; blocked?: number; list: Array<{ name: string; pid: number; cpu: number; memory: number; state: string }> }): void {
|
||||||
|
this.processTotal = data.total;
|
||||||
|
this.processRunning = data.running;
|
||||||
|
this.processSleeping = data.sleeping;
|
||||||
|
this.processBlocked = data.blocked || 0;
|
||||||
|
this.topProcesses = data.list || [];
|
||||||
|
}
|
||||||
|
|
||||||
// Helper to format bytes
|
// Helper to format bytes
|
||||||
private formatBytes(bytes: number): string {
|
private formatBytes(bytes: number): string {
|
||||||
if (bytes === 0) return '0 B';
|
if (bytes === 0) return '0 B';
|
||||||
@@ -331,6 +451,15 @@ export class EcoViewSystem extends DeesElement {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Helper to format bytes per second as speed
|
||||||
|
private formatSpeed(bytesPerSec: number): string {
|
||||||
|
if (bytesPerSec === 0) return '0 B/s';
|
||||||
|
const k = 1024;
|
||||||
|
const sizes = ['B/s', 'KB/s', 'MB/s', 'GB/s'];
|
||||||
|
const i = Math.floor(Math.log(bytesPerSec) / Math.log(k));
|
||||||
|
return parseFloat((bytesPerSec / Math.pow(k, i)).toFixed(1)) + ' ' + sizes[i];
|
||||||
|
}
|
||||||
|
|
||||||
private renderOverviewPanel(): TemplateResult {
|
private renderOverviewPanel(): TemplateResult {
|
||||||
const overviewTiles = [
|
const overviewTiles = [
|
||||||
{
|
{
|
||||||
@@ -339,6 +468,7 @@ export class EcoViewSystem extends DeesElement {
|
|||||||
value: this.cpuUsage,
|
value: this.cpuUsage,
|
||||||
type: 'gauge' as const,
|
type: 'gauge' as const,
|
||||||
icon: 'lucide:cpu',
|
icon: 'lucide:cpu',
|
||||||
|
description: this.cpuModel !== 'Unknown' ? `${this.cpuPhysicalCores} cores @ ${this.cpuSpeed} GHz` : undefined,
|
||||||
gaugeOptions: {
|
gaugeOptions: {
|
||||||
min: 0,
|
min: 0,
|
||||||
max: 100,
|
max: 100,
|
||||||
@@ -372,6 +502,7 @@ export class EcoViewSystem extends DeesElement {
|
|||||||
value: this.diskUsage,
|
value: this.diskUsage,
|
||||||
type: 'gauge' as const,
|
type: 'gauge' as const,
|
||||||
icon: 'lucide:hardDrive',
|
icon: 'lucide:hardDrive',
|
||||||
|
description: this.diskTotal ? `${this.formatBytes(this.diskUsed)} of ${this.formatBytes(this.diskTotal)}` : undefined,
|
||||||
gaugeOptions: {
|
gaugeOptions: {
|
||||||
min: 0,
|
min: 0,
|
||||||
max: 100,
|
max: 100,
|
||||||
@@ -403,22 +534,22 @@ export class EcoViewSystem extends DeesElement {
|
|||||||
{
|
{
|
||||||
id: 'network-in',
|
id: 'network-in',
|
||||||
title: 'Network In',
|
title: 'Network In',
|
||||||
value: '85',
|
value: this.formatSpeed(this.networkRxSec),
|
||||||
unit: 'Mbps',
|
|
||||||
type: 'trend' as const,
|
type: 'trend' as const,
|
||||||
icon: 'lucide:download',
|
icon: 'lucide:download',
|
||||||
trendData: this.networkIn,
|
trendData: this.networkInHistory,
|
||||||
color: 'hsl(142 71% 45%)',
|
color: 'hsl(142 71% 45%)',
|
||||||
|
description: `Total: ${this.formatBytes(this.networkRxTotal)}`,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'network-out',
|
id: 'network-out',
|
||||||
title: 'Network Out',
|
title: 'Network Out',
|
||||||
value: '65',
|
value: this.formatSpeed(this.networkTxSec),
|
||||||
unit: 'Mbps',
|
|
||||||
type: 'trend' as const,
|
type: 'trend' as const,
|
||||||
icon: 'lucide:upload',
|
icon: 'lucide:upload',
|
||||||
trendData: this.networkOut,
|
trendData: this.networkOutHistory,
|
||||||
color: 'hsl(217 91% 60%)',
|
color: 'hsl(217 91% 60%)',
|
||||||
|
description: `Total: ${this.formatBytes(this.networkTxTotal)}`,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'uptime',
|
id: 'uptime',
|
||||||
@@ -435,7 +566,7 @@ export class EcoViewSystem extends DeesElement {
|
|||||||
value: this.hostname,
|
value: this.hostname,
|
||||||
type: 'text' as const,
|
type: 'text' as const,
|
||||||
icon: 'lucide:server',
|
icon: 'lucide:server',
|
||||||
description: `${this.platform} - ${this.cpuCores} cores`,
|
description: this.distro ? `${this.distro} - ${this.cpuCores} threads` : `${this.platform} - ${this.cpuCores} threads`,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
@@ -456,6 +587,13 @@ export class EcoViewSystem extends DeesElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private renderCpuPanel(): TemplateResult {
|
private renderCpuPanel(): TemplateResult {
|
||||||
|
// Generate cores data for cpuCores tile type
|
||||||
|
const coresData = this.coreLoads.map((load, index) => ({
|
||||||
|
id: index,
|
||||||
|
usage: load,
|
||||||
|
label: `Core ${index}`,
|
||||||
|
}));
|
||||||
|
|
||||||
const cpuTiles = [
|
const cpuTiles = [
|
||||||
{
|
{
|
||||||
id: 'cpu-total',
|
id: 'cpu-total',
|
||||||
@@ -463,6 +601,7 @@ export class EcoViewSystem extends DeesElement {
|
|||||||
value: this.cpuUsage,
|
value: this.cpuUsage,
|
||||||
type: 'gauge' as const,
|
type: 'gauge' as const,
|
||||||
icon: 'lucide:cpu',
|
icon: 'lucide:cpu',
|
||||||
|
description: `${this.cpuCores} threads on ${this.cpuPhysicalCores} cores`,
|
||||||
gaugeOptions: {
|
gaugeOptions: {
|
||||||
min: 0,
|
min: 0,
|
||||||
max: 100,
|
max: 100,
|
||||||
@@ -474,73 +613,21 @@ export class EcoViewSystem extends DeesElement {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'core-0',
|
id: 'cpu-cores',
|
||||||
title: 'Core 0',
|
title: 'Core Usage',
|
||||||
value: 38,
|
value: this.cpuUsage,
|
||||||
type: 'gauge' as const,
|
type: 'cpuCores' as const,
|
||||||
gaugeOptions: {
|
coresData: coresData,
|
||||||
min: 0,
|
columnSpan: 2,
|
||||||
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: 'core-1',
|
|
||||||
title: 'Core 1',
|
|
||||||
value: 52,
|
|
||||||
type: 'gauge' as const,
|
|
||||||
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: 'core-2',
|
|
||||||
title: 'Core 2',
|
|
||||||
value: 45,
|
|
||||||
type: 'gauge' as const,
|
|
||||||
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: 'core-3',
|
|
||||||
title: 'Core 3',
|
|
||||||
value: 33,
|
|
||||||
type: 'gauge' as const,
|
|
||||||
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: 'load-avg',
|
id: 'load-avg',
|
||||||
title: 'Load Average',
|
title: 'Load Average',
|
||||||
value: '2.45',
|
value: this.loadAvg[0]?.toFixed(2) || '0',
|
||||||
type: 'trend' as const,
|
type: 'trend' as const,
|
||||||
icon: 'lucide:activity',
|
icon: 'lucide:activity',
|
||||||
trendData: [1.8, 2.1, 2.4, 2.2, 2.5, 2.3, 2.6, 2.4, 2.45],
|
trendData: this.loadAvg,
|
||||||
description: '1m: 2.45, 5m: 2.32, 15m: 2.18',
|
description: `1m: ${this.loadAvg[0]?.toFixed(2)}, 5m: ${this.loadAvg[1]?.toFixed(2)}, 15m: ${this.loadAvg[2]?.toFixed(2)}`,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'cpu-temp',
|
id: 'cpu-temp',
|
||||||
@@ -563,18 +650,18 @@ export class EcoViewSystem extends DeesElement {
|
|||||||
{
|
{
|
||||||
id: 'freq',
|
id: 'freq',
|
||||||
title: 'Clock Speed',
|
title: 'Clock Speed',
|
||||||
value: '3.2',
|
value: this.cpuSpeed.toFixed(1),
|
||||||
unit: 'GHz',
|
unit: 'GHz',
|
||||||
type: 'number' as const,
|
type: 'number' as const,
|
||||||
icon: 'lucide:gauge',
|
icon: 'lucide:gauge',
|
||||||
description: 'Max: 4.2 GHz',
|
description: this.cpuSpeedMax ? `Max: ${this.cpuSpeedMax.toFixed(1)} GHz` : undefined,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
return html`
|
return html`
|
||||||
<div class="panel-header">
|
<div class="panel-header">
|
||||||
<div class="panel-title">CPU</div>
|
<div class="panel-title">CPU</div>
|
||||||
<div class="panel-description">Processor usage and performance</div>
|
<div class="panel-description">${this.cpuModel}</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="stats-section">
|
<div class="stats-section">
|
||||||
@@ -588,6 +675,8 @@ export class EcoViewSystem extends DeesElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private renderMemoryPanel(): TemplateResult {
|
private renderMemoryPanel(): TemplateResult {
|
||||||
|
const swapUsagePercent = this.swapTotal > 0 ? Math.round((this.swapUsed / this.swapTotal) * 100) : 0;
|
||||||
|
|
||||||
const memoryTiles = [
|
const memoryTiles = [
|
||||||
{
|
{
|
||||||
id: 'ram-usage',
|
id: 'ram-usage',
|
||||||
@@ -604,12 +693,12 @@ export class EcoViewSystem extends DeesElement {
|
|||||||
{ value: 85, color: 'hsl(0 84% 60%)' },
|
{ value: 85, color: 'hsl(0 84% 60%)' },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
description: '10.7 GB of 16 GB',
|
description: `${this.formatBytes(this.memoryUsed)} of ${this.formatBytes(this.memoryTotal)}`,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'swap-usage',
|
id: 'swap-usage',
|
||||||
title: 'Swap Usage',
|
title: 'Swap Usage',
|
||||||
value: 12,
|
value: swapUsagePercent,
|
||||||
type: 'gauge' as const,
|
type: 'gauge' as const,
|
||||||
icon: 'lucide:hardDrive',
|
icon: 'lucide:hardDrive',
|
||||||
gaugeOptions: {
|
gaugeOptions: {
|
||||||
@@ -621,41 +710,38 @@ export class EcoViewSystem extends DeesElement {
|
|||||||
{ value: 75, color: 'hsl(0 84% 60%)' },
|
{ value: 75, color: 'hsl(0 84% 60%)' },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
description: '0.5 GB of 4 GB',
|
description: this.swapTotal > 0 ? `${this.formatBytes(this.swapUsed)} of ${this.formatBytes(this.swapTotal)}` : 'No swap',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'mem-trend',
|
id: 'mem-trend',
|
||||||
title: 'Memory History',
|
title: 'Memory History',
|
||||||
value: '67%',
|
value: `${this.memoryUsage}%`,
|
||||||
type: 'trend' as const,
|
type: 'trend' as const,
|
||||||
icon: 'lucide:trendingUp',
|
icon: 'lucide:trendingUp',
|
||||||
trendData: [58, 62, 65, 63, 68, 72, 70, 65, 67],
|
trendData: this.memoryUsageHistory,
|
||||||
description: 'Last hour',
|
description: 'Recent usage',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'cached',
|
id: 'cached',
|
||||||
title: 'Cached',
|
title: 'Cached',
|
||||||
value: '3.2',
|
value: this.formatBytes(this.memoryCached),
|
||||||
unit: 'GB',
|
type: 'text' as const,
|
||||||
type: 'number' as const,
|
|
||||||
icon: 'lucide:database',
|
icon: 'lucide:database',
|
||||||
color: 'hsl(217 91% 60%)',
|
color: 'hsl(217 91% 60%)',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'buffers',
|
id: 'buffers',
|
||||||
title: 'Buffers',
|
title: 'Buffers',
|
||||||
value: '512',
|
value: this.formatBytes(this.memoryBuffers),
|
||||||
unit: 'MB',
|
type: 'text' as const,
|
||||||
type: 'number' as const,
|
|
||||||
icon: 'lucide:layers',
|
icon: 'lucide:layers',
|
||||||
color: 'hsl(262 83% 58%)',
|
color: 'hsl(262 83% 58%)',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'available',
|
id: 'available',
|
||||||
title: 'Available',
|
title: 'Available',
|
||||||
value: '5.3',
|
value: this.formatBytes(this.memoryAvailable),
|
||||||
unit: 'GB',
|
type: 'text' as const,
|
||||||
type: 'number' as const,
|
|
||||||
icon: 'lucide:checkCircle',
|
icon: 'lucide:checkCircle',
|
||||||
color: 'hsl(142 71% 45%)',
|
color: 'hsl(142 71% 45%)',
|
||||||
},
|
},
|
||||||
@@ -680,58 +766,39 @@ export class EcoViewSystem extends DeesElement {
|
|||||||
private renderStoragePanel(): TemplateResult {
|
private renderStoragePanel(): TemplateResult {
|
||||||
const storageTiles = [
|
const storageTiles = [
|
||||||
{
|
{
|
||||||
id: 'disk-main',
|
id: 'disk-total',
|
||||||
title: 'System Drive',
|
title: 'Total Storage',
|
||||||
value: this.diskUsage,
|
value: this.diskUsage,
|
||||||
type: 'percentage' as const,
|
type: 'percentage' as const,
|
||||||
icon: 'lucide:hardDrive',
|
icon: 'lucide:hardDrive',
|
||||||
description: '275 GB of 512 GB used',
|
description: `${this.formatBytes(this.diskUsed)} of ${this.formatBytes(this.diskTotal)} used`,
|
||||||
color: 'hsl(217 91% 60%)',
|
color: 'hsl(217 91% 60%)',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'disk-data',
|
id: 'disk-free',
|
||||||
title: 'Data Drive',
|
title: 'Free Space',
|
||||||
value: 38,
|
value: this.formatBytes(this.diskFree),
|
||||||
type: 'percentage' as const,
|
type: 'text' as const,
|
||||||
icon: 'lucide:hardDrive',
|
icon: 'lucide:hardDrive',
|
||||||
description: '380 GB of 1 TB used',
|
description: 'Available storage',
|
||||||
color: 'hsl(142 71% 45%)',
|
color: 'hsl(142 71% 45%)',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'read-speed',
|
id: 'disk-used',
|
||||||
title: 'Read Speed',
|
title: 'Used Space',
|
||||||
value: '245',
|
value: this.formatBytes(this.diskUsed),
|
||||||
unit: 'MB/s',
|
type: 'text' as const,
|
||||||
type: 'trend' as const,
|
icon: 'lucide:database',
|
||||||
icon: 'lucide:download',
|
description: 'Currently in use',
|
||||||
trendData: [180, 220, 195, 280, 245, 210, 265, 230, 245],
|
color: 'hsl(45 93% 47%)',
|
||||||
color: 'hsl(142 71% 45%)',
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'write-speed',
|
id: 'disk-total-size',
|
||||||
title: 'Write Speed',
|
title: 'Total Capacity',
|
||||||
value: '128',
|
value: this.formatBytes(this.diskTotal),
|
||||||
unit: 'MB/s',
|
type: 'text' as const,
|
||||||
type: 'trend' as const,
|
icon: 'lucide:server',
|
||||||
icon: 'lucide:upload',
|
description: 'All filesystems',
|
||||||
trendData: [95, 110, 85, 145, 120, 105, 138, 115, 128],
|
|
||||||
color: 'hsl(217 91% 60%)',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'iops-read',
|
|
||||||
title: 'Read IOPS',
|
|
||||||
value: '12.4k',
|
|
||||||
type: 'number' as const,
|
|
||||||
icon: 'lucide:gauge',
|
|
||||||
description: 'Operations/sec',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'iops-write',
|
|
||||||
title: 'Write IOPS',
|
|
||||||
value: '8.2k',
|
|
||||||
type: 'number' as const,
|
|
||||||
icon: 'lucide:gauge',
|
|
||||||
description: 'Operations/sec',
|
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
@@ -755,83 +822,38 @@ export class EcoViewSystem extends DeesElement {
|
|||||||
const networkTiles = [
|
const networkTiles = [
|
||||||
{
|
{
|
||||||
id: 'download',
|
id: 'download',
|
||||||
title: 'Download',
|
title: 'Download Speed',
|
||||||
value: '85.2',
|
value: this.formatSpeed(this.networkRxSec),
|
||||||
unit: 'Mbps',
|
|
||||||
type: 'trend' as const,
|
type: 'trend' as const,
|
||||||
icon: 'lucide:download',
|
icon: 'lucide:download',
|
||||||
trendData: this.networkIn,
|
trendData: this.networkInHistory,
|
||||||
color: 'hsl(142 71% 45%)',
|
color: 'hsl(142 71% 45%)',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'upload',
|
id: 'upload',
|
||||||
title: 'Upload',
|
title: 'Upload Speed',
|
||||||
value: '64.8',
|
value: this.formatSpeed(this.networkTxSec),
|
||||||
unit: 'Mbps',
|
|
||||||
type: 'trend' as const,
|
type: 'trend' as const,
|
||||||
icon: 'lucide:upload',
|
icon: 'lucide:upload',
|
||||||
trendData: this.networkOut,
|
trendData: this.networkOutHistory,
|
||||||
color: 'hsl(217 91% 60%)',
|
color: 'hsl(217 91% 60%)',
|
||||||
},
|
},
|
||||||
{
|
|
||||||
id: 'latency',
|
|
||||||
title: 'Latency',
|
|
||||||
value: 12,
|
|
||||||
unit: 'ms',
|
|
||||||
type: 'gauge' as const,
|
|
||||||
icon: 'lucide:activity',
|
|
||||||
gaugeOptions: {
|
|
||||||
min: 0,
|
|
||||||
max: 100,
|
|
||||||
thresholds: [
|
|
||||||
{ value: 0, color: 'hsl(142 71% 45%)' },
|
|
||||||
{ value: 30, color: 'hsl(45 93% 47%)' },
|
|
||||||
{ value: 60, color: 'hsl(0 84% 60%)' },
|
|
||||||
],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'packets-in',
|
|
||||||
title: 'Packets In',
|
|
||||||
value: '1.2M',
|
|
||||||
type: 'number' as const,
|
|
||||||
icon: 'lucide:arrowDownCircle',
|
|
||||||
description: 'Per second',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'packets-out',
|
|
||||||
title: 'Packets Out',
|
|
||||||
value: '892k',
|
|
||||||
type: 'number' as const,
|
|
||||||
icon: 'lucide:arrowUpCircle',
|
|
||||||
description: 'Per second',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'connections',
|
|
||||||
title: 'Active Connections',
|
|
||||||
value: 48,
|
|
||||||
type: 'number' as const,
|
|
||||||
icon: 'lucide:link',
|
|
||||||
description: '12 established, 36 waiting',
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
id: 'total-down',
|
id: 'total-down',
|
||||||
title: 'Total Downloaded',
|
title: 'Total Downloaded',
|
||||||
value: '24.5',
|
value: this.formatBytes(this.networkRxTotal),
|
||||||
unit: 'GB',
|
type: 'text' as const,
|
||||||
type: 'number' as const,
|
icon: 'lucide:arrowDownCircle',
|
||||||
icon: 'lucide:database',
|
description: 'Since boot',
|
||||||
description: 'This session',
|
|
||||||
color: 'hsl(142 71% 45%)',
|
color: 'hsl(142 71% 45%)',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'total-up',
|
id: 'total-up',
|
||||||
title: 'Total Uploaded',
|
title: 'Total Uploaded',
|
||||||
value: '8.2',
|
value: this.formatBytes(this.networkTxTotal),
|
||||||
unit: 'GB',
|
type: 'text' as const,
|
||||||
type: 'number' as const,
|
icon: 'lucide:arrowUpCircle',
|
||||||
icon: 'lucide:database',
|
description: 'Since boot',
|
||||||
description: 'This session',
|
|
||||||
color: 'hsl(217 91% 60%)',
|
color: 'hsl(217 91% 60%)',
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
@@ -857,14 +879,14 @@ export class EcoViewSystem extends DeesElement {
|
|||||||
{
|
{
|
||||||
id: 'total-processes',
|
id: 'total-processes',
|
||||||
title: 'Total Processes',
|
title: 'Total Processes',
|
||||||
value: 247,
|
value: this.processTotal,
|
||||||
type: 'number' as const,
|
type: 'number' as const,
|
||||||
icon: 'lucide:layers',
|
icon: 'lucide:layers',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'running',
|
id: 'running',
|
||||||
title: 'Running',
|
title: 'Running',
|
||||||
value: 12,
|
value: this.processRunning,
|
||||||
type: 'number' as const,
|
type: 'number' as const,
|
||||||
icon: 'lucide:play',
|
icon: 'lucide:play',
|
||||||
color: 'hsl(142 71% 45%)',
|
color: 'hsl(142 71% 45%)',
|
||||||
@@ -872,30 +894,21 @@ export class EcoViewSystem extends DeesElement {
|
|||||||
{
|
{
|
||||||
id: 'sleeping',
|
id: 'sleeping',
|
||||||
title: 'Sleeping',
|
title: 'Sleeping',
|
||||||
value: 235,
|
value: this.processSleeping,
|
||||||
type: 'number' as const,
|
type: 'number' as const,
|
||||||
icon: 'lucide:moon',
|
icon: 'lucide:moon',
|
||||||
color: 'hsl(217 91% 60%)',
|
color: 'hsl(217 91% 60%)',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'threads',
|
id: 'blocked',
|
||||||
title: 'Threads',
|
title: 'Blocked',
|
||||||
value: 1842,
|
value: this.processBlocked,
|
||||||
type: 'number' as const,
|
type: 'number' as const,
|
||||||
icon: 'lucide:gitBranch',
|
icon: 'lucide:pauseCircle',
|
||||||
|
color: 'hsl(0 84% 60%)',
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
const topProcesses = [
|
|
||||||
{ name: 'node', pid: 1234, cpu: 12.5, memory: 8.2 },
|
|
||||||
{ name: 'chrome', pid: 2345, cpu: 8.3, memory: 15.4 },
|
|
||||||
{ name: 'code', pid: 3456, cpu: 5.2, memory: 12.1 },
|
|
||||||
{ name: 'docker', pid: 4567, cpu: 4.8, memory: 6.8 },
|
|
||||||
{ name: 'postgres', pid: 5678, cpu: 3.2, memory: 4.5 },
|
|
||||||
{ name: 'nginx', pid: 6789, cpu: 1.5, memory: 2.1 },
|
|
||||||
{ name: 'redis', pid: 7890, cpu: 0.8, memory: 1.8 },
|
|
||||||
];
|
|
||||||
|
|
||||||
return html`
|
return html`
|
||||||
<div class="panel-header">
|
<div class="panel-header">
|
||||||
<div class="panel-title">Processes</div>
|
<div class="panel-title">Processes</div>
|
||||||
@@ -919,14 +932,14 @@ export class EcoViewSystem extends DeesElement {
|
|||||||
<span>CPU %</span>
|
<span>CPU %</span>
|
||||||
<span>Memory %</span>
|
<span>Memory %</span>
|
||||||
</div>
|
</div>
|
||||||
${topProcesses.map(proc => html`
|
${this.topProcesses.length > 0 ? this.topProcesses.map(proc => html`
|
||||||
<div class="process-row">
|
<div class="process-row">
|
||||||
<span class="process-name">${proc.name}</span>
|
<span class="process-name">${proc.name}</span>
|
||||||
<span class="process-value">${proc.pid}</span>
|
<span class="process-value">${proc.pid}</span>
|
||||||
<span class="process-value ${proc.cpu > 10 ? 'high' : ''}">${proc.cpu}%</span>
|
<span class="process-value ${proc.cpu > 10 ? 'high' : ''}">${proc.cpu}%</span>
|
||||||
<span class="process-value ${proc.memory > 10 ? 'high' : ''}">${proc.memory}%</span>
|
<span class="process-value ${proc.memory > 10 ? 'high' : ''}">${proc.memory}%</span>
|
||||||
</div>
|
</div>
|
||||||
`)}
|
`) : html`<div class="process-row"><span class="process-name">Loading...</span></div>`}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
|
|||||||
Reference in New Issue
Block a user