Files
onebox/ts_web/elements/ob-view-dashboard.ts
T

278 lines
9.2 KiB
TypeScript
Raw Normal View History

import * as plugins from '../plugins.js';
import * as shared from './shared/index.js';
import * as appstate from '../appstate.js';
import { appRouter } from '../router.js';
import {
DeesElement,
customElement,
html,
state,
css,
cssManager,
type TemplateResult,
} from '@design.estate/dees-element';
const byteUnits = ['B', 'KB', 'MB', 'GB', 'TB'];
function getByteUnitIndex(bytes: number): number {
if (!bytes || bytes === 0) return 0;
return Math.min(Math.floor(Math.log(bytes) / Math.log(1024)), byteUnits.length - 1);
}
function formatBytes(bytes: number, forcedUnitIndex?: number): string {
if ((!bytes || bytes === 0) && forcedUnitIndex === undefined) return '0 B';
const unitIndex = forcedUnitIndex ?? getByteUnitIndex(bytes);
const value = bytes / Math.pow(1024, unitIndex);
return `${value.toFixed(1)} ${byteUnits[unitIndex]}`;
}
@customElement('ob-view-dashboard')
export class ObViewDashboard extends DeesElement {
@state()
accessor systemState: appstate.ISystemState = { status: null };
@state()
accessor servicesState: appstate.IServicesState = {
services: [],
currentService: null,
currentServiceLogs: [],
currentServiceStats: null,
platformServices: [],
currentPlatformService: null,
currentPlatformServiceStats: null,
currentPlatformServiceLogs: [],
};
@state()
accessor networkState: appstate.INetworkState = {
targets: [],
stats: null,
trafficStats: null,
dnsRecords: [],
domains: [],
gatewayDomains: [],
gatewayDnsRecords: [],
certificates: [],
};
constructor() {
super();
const systemSub = appstate.systemStatePart
.select((s) => s)
.subscribe((newState) => {
this.systemState = newState;
});
this.rxSubscriptions.push(systemSub);
const servicesSub = appstate.servicesStatePart
.select((s) => s)
.subscribe((newState) => {
this.servicesState = newState;
});
this.rxSubscriptions.push(servicesSub);
const networkSub = appstate.networkStatePart
.select((s) => s)
.subscribe((newState) => {
this.networkState = newState;
});
this.rxSubscriptions.push(networkSub);
}
public static styles = [
cssManager.defaultStyles,
shared.viewHostCss,
css`
.dashboard {
display: flex;
flex-direction: column;
gap: 24px;
}
.section {
display: flex;
flex-direction: column;
}
.section-title {
font-size: 18px;
font-weight: 600;
color: ${cssManager.bdTheme('#18181b', '#fafafa')};
margin: 0 0 12px;
}
.services-grid {
display: grid;
grid-template-columns: 1fr;
gap: 16px;
align-items: stretch;
}
.services-grid > * {
height: 100%;
}
@media (min-width: 768px) {
.services-grid {
grid-template-columns: 1fr 1fr;
}
}
`,
];
async connectedCallback() {
super.connectedCallback();
await Promise.all([
appstate.systemStatePart.dispatchAction(appstate.fetchSystemStatusAction, null),
appstate.servicesStatePart.dispatchAction(appstate.fetchServicesAction, null),
appstate.servicesStatePart.dispatchAction(appstate.fetchPlatformServicesAction, null),
appstate.networkStatePart.dispatchAction(appstate.fetchNetworkStatsAction, null),
appstate.networkStatePart.dispatchAction(appstate.fetchTrafficStatsAction, null),
appstate.networkStatePart.dispatchAction(appstate.fetchCertificatesAction, null),
]);
}
public render(): TemplateResult {
const status = this.systemState.status;
const services = this.servicesState.services;
const platformServices = this.servicesState.platformServices;
const networkStats = this.networkState.stats;
const trafficStats = this.networkState.trafficStats;
const certificates = this.networkState.certificates;
const statusCounts = trafficStats?.statusCounts || {};
const runningServices = services.filter((s) => s.status === 'running').length;
const stoppedServices = services.filter((s) => s.status === 'stopped').length;
const memoryUnitIndex = getByteUnitIndex(
status?.docker?.memoryTotal || status?.docker?.memoryUsage || 0,
);
const validCerts = certificates.filter((c) => c.isValid).length;
const expiringCerts = certificates.filter(
(c) => c.isValid && c.expiresAt && c.expiresAt - Date.now() < 30 * 24 * 60 * 60 * 1000,
).length;
const expiredCerts = certificates.filter((c) => !c.isValid).length;
const dashboardData = {
cluster: {
totalServices: services.length,
running: runningServices,
stopped: stoppedServices,
dockerStatus: status?.docker?.running ? 'running' as const : 'stopped' as const,
},
resourceUsage: {
cpu: status?.docker?.cpuUsage || 0,
memoryUsed: formatBytes(status?.docker?.memoryUsage || 0, memoryUnitIndex),
memoryTotal: formatBytes(status?.docker?.memoryTotal || 0, memoryUnitIndex),
networkIn: formatBytes(status?.docker?.networkIn || 0),
networkOut: formatBytes(status?.docker?.networkOut || 0),
topConsumers: [],
},
platformServices: platformServices
.filter((ps) => ps.status === 'running' || ps.status === 'starting' || ps.status === 'stopping' || ps.isCore)
.map((ps) => ({
name: ps.displayName,
status: ps.status === 'running' ? 'Running' : ps.status === 'starting' ? 'Starting...' : ps.status === 'stopping' ? 'Stopping...' : 'Stopped',
running: ps.status === 'running',
})),
traffic: {
requests: trafficStats?.requestCount || 0,
errors: trafficStats?.errorCount || 0,
errorPercent: trafficStats?.errorRate || 0,
avgResponse: trafficStats?.avgResponseTime || 0,
reqPerMin: trafficStats?.requestsPerMinute || 0,
status2xx: statusCounts['2xx'] || 0,
status3xx: statusCounts['3xx'] || 0,
status4xx: statusCounts['4xx'] || 0,
status5xx: statusCounts['5xx'] || 0,
},
proxy: {
httpPort: String(networkStats?.proxy?.httpPort || 80),
httpsPort: String(networkStats?.proxy?.httpsPort || 443),
httpActive: networkStats?.proxy?.running || false,
httpsActive: networkStats?.proxy?.running || false,
routeCount: String(networkStats?.proxy?.routes || 0),
},
certificates: {
valid: validCerts,
expiring: expiringCerts,
expired: expiredCerts,
},
dnsConfigured: status?.dns?.configured || false,
acmeConfigured: status?.ssl?.configured || false,
quickActions: [
{ label: 'Deploy Service', icon: 'lucide:Plus', primary: true },
{ label: 'Add Domain', icon: 'lucide:Globe' },
{ label: 'View Logs', icon: 'lucide:FileText' },
],
};
return html`
<ob-sectionheading>Dashboard</ob-sectionheading>
<div class="dashboard">
<section class="section">
<h2 class="section-title">Cluster Overview</h2>
<sz-status-grid-cluster .stats=${dashboardData.cluster}></sz-status-grid-cluster>
</section>
<section class="section">
<h2 class="section-title">Services & Resources</h2>
<div class="services-grid">
<sz-resource-usage-card .data=${dashboardData.resourceUsage}></sz-resource-usage-card>
<sz-platform-services-card
.services=${dashboardData.platformServices}
@service-click=${(e: CustomEvent) => this.handlePlatformServiceClick(e)}
></sz-platform-services-card>
</div>
</section>
<section class="section">
<h2 class="section-title">Network & Traffic</h2>
<sz-status-grid-network
.traffic=${dashboardData.traffic}
.proxy=${dashboardData.proxy}
.certificates=${dashboardData.certificates}
></sz-status-grid-network>
</section>
<section class="section">
<h2 class="section-title">Infrastructure</h2>
<sz-status-grid-infra
?dnsConfigured=${dashboardData.dnsConfigured}
?acmeConfigured=${dashboardData.acmeConfigured}
.actions=${dashboardData.quickActions}
@action-click=${(e: CustomEvent) => this.handleQuickAction(e)}
></sz-status-grid-infra>
</section>
</div>
`;
}
private handleQuickAction(e: CustomEvent) {
const action = e.detail?.action || e.detail?.label;
if (action === 'Deploy Service') {
appRouter.navigateToView('services');
} else if (action === 'Add Domain') {
appRouter.navigateToView('network');
}
}
private handlePlatformServiceClick(e: CustomEvent) {
// Find the platform service type from the click event
const name = e.detail?.name;
const ps = this.servicesState.platformServices.find(
(p) => p.displayName === name,
);
if (ps) {
// Navigate to services tab — the ObViewServices component will pick up the type
// Store the selected platform type so the services view can open it
appstate.servicesStatePart.setState({
...appstate.servicesStatePart.getState(),
currentPlatformService: ps,
});
appRouter.navigateToView('services');
}
}
}