fixes and api setup
All checks were successful
Gitea Actions Demo / Explore-Gitea-Actions (push) Successful in 2s
All checks were successful
Gitea Actions Demo / Explore-Gitea-Actions (push) Successful in 2s
This commit is contained in:
parent
2381c51789
commit
459deb255d
@ -1,15 +1,24 @@
|
||||
import type { WSContext } from 'hono/ws';
|
||||
import { listContainers, streamContainerStats } from '../docker/containers.js';
|
||||
import { sendContainerInfo, sendStats, sendCombined, sendToSingleContainer } from './sender.js';
|
||||
import type { ContainerInfo, ContainerStats } from './types.js';
|
||||
import type { DockerContainer } from '../docker/types.js';
|
||||
|
||||
|
||||
// WebSocket client management and stream orchestration
|
||||
const containerClients = new Set<WSContext<WebSocket>>();
|
||||
const statsClients = new Set<WSContext<WebSocket>>();
|
||||
const combinedClients = new Set<WSContext<WebSocket>>();
|
||||
const singleContainerClients = new Map<string, Set<WSContext<WebSocket>>>();
|
||||
const activeStreams = new Map<string, () => void>();
|
||||
|
||||
// Client registration functions
|
||||
|
||||
const containerCache = new Map<string, {
|
||||
data: DockerContainer | null;
|
||||
lastUpdated: number;
|
||||
}>();
|
||||
const CACHE_TTL = 2000;
|
||||
|
||||
|
||||
export function registerContainerClient(ws: WSContext<WebSocket>) {
|
||||
const wasEmpty = allClientsEmpty();
|
||||
containerClients.add(ws);
|
||||
@ -39,7 +48,7 @@ export function registerSingleContainerClient(ws: WSContext<WebSocket>, containe
|
||||
}
|
||||
}
|
||||
|
||||
// Client removal functions
|
||||
|
||||
export function removeContainerClient(ws: WSContext<WebSocket>) {
|
||||
containerClients.delete(ws);
|
||||
checkAndCleanup();
|
||||
@ -83,53 +92,89 @@ async function setupStreams() {
|
||||
if (allClientsEmpty()) return;
|
||||
try {
|
||||
const containers = await listContainers();
|
||||
|
||||
for (const container of containers) {
|
||||
containerCache.set(container.id, {
|
||||
data: container,
|
||||
lastUpdated: Date.now()
|
||||
});
|
||||
}
|
||||
|
||||
for (const container of containers) {
|
||||
if (activeStreams.has(container.id)) continue;
|
||||
setupStreamForContainer(container.id, containers);
|
||||
setupStreamForContainer(container.id);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error setting up streams:', error);
|
||||
}
|
||||
}
|
||||
|
||||
async function setupStreamForContainer(containerId: string, allContainers?: any[]) {
|
||||
if (activeStreams.has(containerId)) return;
|
||||
async function getContainerData(containerId: string): Promise<DockerContainer | null> {
|
||||
const cached = containerCache.get(containerId);
|
||||
const now = Date.now();
|
||||
|
||||
if (cached && (now - cached.lastUpdated < CACHE_TTL)) {
|
||||
return cached.data;
|
||||
}
|
||||
|
||||
try {
|
||||
let containerData;
|
||||
if (!allContainers) {
|
||||
const containers = await listContainers();
|
||||
containerData = containers.find(c => c.id === containerId);
|
||||
} else {
|
||||
containerData = allContainers.find(c => c.id === containerId);
|
||||
const containers = await listContainers();
|
||||
const containerData = containers.find(c => c.id === containerId) || null;
|
||||
|
||||
for (const container of containers) {
|
||||
containerCache.set(container.id, {
|
||||
data: container,
|
||||
lastUpdated: now
|
||||
});
|
||||
}
|
||||
|
||||
return containerData;
|
||||
} catch (error) {
|
||||
console.error(`Error fetching container data for ${containerId}:`, error);
|
||||
|
||||
return cached?.data || null;
|
||||
}
|
||||
}
|
||||
|
||||
async function setupStreamForContainer(containerId: string) {
|
||||
if (activeStreams.has(containerId)) return;
|
||||
|
||||
try {
|
||||
const containerData = await getContainerData(containerId);
|
||||
if (!containerData) {
|
||||
console.warn(`Container ${containerId} not found`);
|
||||
return;
|
||||
}
|
||||
const stopStream = await streamContainerStats(containerId, async (stats) => {
|
||||
|
||||
const stopStream = await streamContainerStats(containerId, async (stats: ContainerStats) => {
|
||||
if (allClientsEmpty()) {
|
||||
stopAllStreams();
|
||||
return;
|
||||
}
|
||||
// Always fetch latest container info
|
||||
let containerData;
|
||||
if (!allContainers) {
|
||||
const containers = await listContainers();
|
||||
containerData = containers.find(c => c.id === containerId);
|
||||
} else {
|
||||
containerData = allContainers.find(c => c.id === containerId);
|
||||
}
|
||||
if (!containerData) {
|
||||
console.warn(`Container ${containerId} not found`);
|
||||
return;
|
||||
}
|
||||
const containerInfo = {
|
||||
|
||||
|
||||
const freshContainerData = await getContainerData(containerId);
|
||||
|
||||
const containerInfo: ContainerInfo = freshContainerData ? {
|
||||
id: freshContainerData.id,
|
||||
name: freshContainerData.names?.[0]?.replace('/', '') ?? freshContainerData.id.slice(0, 12),
|
||||
image: freshContainerData.image,
|
||||
state: freshContainerData.state,
|
||||
status: freshContainerData.status,
|
||||
} : {
|
||||
id: containerData.id,
|
||||
name: containerData.names?.[0]?.replace('/', '') ?? containerData.id.slice(0, 12),
|
||||
image: containerData.image,
|
||||
state: containerData.state,
|
||||
status: containerData.status,
|
||||
state: 'exited',
|
||||
status: 'Container stopped',
|
||||
};
|
||||
|
||||
|
||||
if (!freshContainerData && !singleContainerClients.has(containerId)) {
|
||||
stopStreamForContainer(containerId);
|
||||
return;
|
||||
}
|
||||
|
||||
if (containerClients.size > 0) {
|
||||
sendContainerInfo(containerClients, containerInfo);
|
||||
}
|
||||
@ -144,9 +189,12 @@ async function setupStreamForContainer(containerId: string, allContainers?: any[
|
||||
sendToSingleContainer(singleContainerClients, containerId, containerInfo, stats);
|
||||
}
|
||||
});
|
||||
|
||||
activeStreams.set(containerId, stopStream);
|
||||
} catch (error) {
|
||||
console.error(`Failed to setup stream for container ${containerId}:`, error);
|
||||
|
||||
containerCache.delete(containerId);
|
||||
}
|
||||
}
|
||||
|
||||
@ -155,6 +203,8 @@ function stopStreamForContainer(containerId: string) {
|
||||
if (stopStream) {
|
||||
stopStream();
|
||||
activeStreams.delete(containerId);
|
||||
|
||||
containerCache.delete(containerId);
|
||||
}
|
||||
}
|
||||
|
||||
@ -163,4 +213,5 @@ export function stopAllStreams() {
|
||||
stopStream();
|
||||
}
|
||||
activeStreams.clear();
|
||||
containerCache.clear();
|
||||
}
|
||||
|
||||
@ -1,18 +1,21 @@
|
||||
// Functions for sending data to WebSocket clients
|
||||
|
||||
import type { WSContext } from 'hono/ws';
|
||||
import type { ContainerInfo, ContainerStats } from './types.js';
|
||||
|
||||
export function sendContainerInfo(containerClients: Set<WSContext<WebSocket>>, containerInfo: any) {
|
||||
export function sendContainerInfo(containerClients: Set<WSContext<WebSocket>>, containerInfo: ContainerInfo) {
|
||||
const payload = JSON.stringify({ containers: [containerInfo] });
|
||||
sendToClientSet(containerClients, payload);
|
||||
}
|
||||
|
||||
export function sendStats(statsClients: Set<WSContext<WebSocket>>, stats: any) {
|
||||
export function sendStats(statsClients: Set<WSContext<WebSocket>>, stats: ContainerStats) {
|
||||
const payload = JSON.stringify({ stats: [stats] });
|
||||
sendToClientSet(statsClients, payload);
|
||||
}
|
||||
|
||||
export function sendCombined(combinedClients: Set<WSContext<WebSocket>>, containerInfo: any, stats: any) {
|
||||
export function sendCombined(
|
||||
combinedClients: Set<WSContext<WebSocket>>,
|
||||
containerInfo: ContainerInfo,
|
||||
stats: ContainerStats
|
||||
) {
|
||||
const payload = JSON.stringify({
|
||||
containers: [containerInfo],
|
||||
stats: [stats]
|
||||
@ -20,7 +23,12 @@ export function sendCombined(combinedClients: Set<WSContext<WebSocket>>, contain
|
||||
sendToClientSet(combinedClients, payload);
|
||||
}
|
||||
|
||||
export function sendToSingleContainer(singleContainerClients: Map<string, Set<WSContext<WebSocket>>>, containerId: string, containerInfo: any, stats: any) {
|
||||
export function sendToSingleContainer(
|
||||
singleContainerClients: Map<string, Set<WSContext<WebSocket>>>,
|
||||
containerId: string,
|
||||
containerInfo: ContainerInfo,
|
||||
stats: ContainerStats
|
||||
) {
|
||||
const combinedData = {
|
||||
...containerInfo,
|
||||
...stats
|
||||
@ -35,7 +43,12 @@ export function sendToSingleContainer(singleContainerClients: Map<string, Set<WS
|
||||
export function sendToClientSet(clients: Set<WSContext<WebSocket>>, payload: string) {
|
||||
for (const ws of clients) {
|
||||
if (ws.readyState === 1) {
|
||||
ws.send(payload);
|
||||
try {
|
||||
ws.send(payload);
|
||||
} catch (err) {
|
||||
console.error('Error sending to client:', err);
|
||||
clients.delete(ws);
|
||||
}
|
||||
} else {
|
||||
clients.delete(ws);
|
||||
}
|
||||
|
||||
7
frontend/libs/shared/environment/README.md
Normal file
7
frontend/libs/shared/environment/README.md
Normal file
@ -0,0 +1,7 @@
|
||||
# environment
|
||||
|
||||
This library was generated with [Nx](https://nx.dev).
|
||||
|
||||
## Running unit tests
|
||||
|
||||
Run `nx test environment` to execute the unit tests.
|
||||
34
frontend/libs/shared/environment/eslint.config.mjs
Normal file
34
frontend/libs/shared/environment/eslint.config.mjs
Normal file
@ -0,0 +1,34 @@
|
||||
import nx from '@nx/eslint-plugin';
|
||||
import baseConfig from '../../../eslint.base.config.mjs';
|
||||
|
||||
export default [
|
||||
...baseConfig,
|
||||
...nx.configs['flat/angular'],
|
||||
...nx.configs['flat/angular-template'],
|
||||
{
|
||||
files: ['**/*.ts'],
|
||||
rules: {
|
||||
'@angular-eslint/directive-selector': [
|
||||
'error',
|
||||
{
|
||||
type: 'attribute',
|
||||
prefix: 'lib',
|
||||
style: 'camelCase',
|
||||
},
|
||||
],
|
||||
'@angular-eslint/component-selector': [
|
||||
'error',
|
||||
{
|
||||
type: 'element',
|
||||
prefix: 'lib',
|
||||
style: 'kebab-case',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
files: ['**/*.html'],
|
||||
// Override or add rules here
|
||||
rules: {},
|
||||
},
|
||||
];
|
||||
21
frontend/libs/shared/environment/jest.config.ts
Normal file
21
frontend/libs/shared/environment/jest.config.ts
Normal file
@ -0,0 +1,21 @@
|
||||
export default {
|
||||
displayName: 'environment',
|
||||
preset: '../../../jest.preset.js',
|
||||
setupFilesAfterEnv: ['<rootDir>/src/test-setup.ts'],
|
||||
coverageDirectory: '../../../coverage/libs/shared/environment',
|
||||
transform: {
|
||||
'^.+\\.(ts|mjs|js|html)$': [
|
||||
'jest-preset-angular',
|
||||
{
|
||||
tsconfig: '<rootDir>/tsconfig.spec.json',
|
||||
stringifyContentPathRegex: '\\.(html|svg)$',
|
||||
},
|
||||
],
|
||||
},
|
||||
transformIgnorePatterns: ['node_modules/(?!.*\\.mjs$)'],
|
||||
snapshotSerializers: [
|
||||
'jest-preset-angular/build/serializers/no-ng-attributes',
|
||||
'jest-preset-angular/build/serializers/ng-snapshot',
|
||||
'jest-preset-angular/build/serializers/html-comment',
|
||||
],
|
||||
};
|
||||
20
frontend/libs/shared/environment/project.json
Normal file
20
frontend/libs/shared/environment/project.json
Normal file
@ -0,0 +1,20 @@
|
||||
{
|
||||
"name": "environment",
|
||||
"$schema": "../../../node_modules/nx/schemas/project-schema.json",
|
||||
"sourceRoot": "libs/shared/environment/src",
|
||||
"prefix": "lib",
|
||||
"projectType": "library",
|
||||
"tags": [],
|
||||
"targets": {
|
||||
"test": {
|
||||
"executor": "@nx/jest:jest",
|
||||
"outputs": ["{workspaceRoot}/coverage/{projectRoot}"],
|
||||
"options": {
|
||||
"jestConfig": "libs/shared/environment/jest.config.ts"
|
||||
}
|
||||
},
|
||||
"lint": {
|
||||
"executor": "@nx/eslint:lint"
|
||||
}
|
||||
}
|
||||
}
|
||||
2
frontend/libs/shared/environment/src/index.ts
Normal file
2
frontend/libs/shared/environment/src/index.ts
Normal file
@ -0,0 +1,2 @@
|
||||
export * from './lib/types';
|
||||
export * from './lib/token';
|
||||
4
frontend/libs/shared/environment/src/lib/token.ts
Normal file
4
frontend/libs/shared/environment/src/lib/token.ts
Normal file
@ -0,0 +1,4 @@
|
||||
import { InjectionToken } from '@angular/core';
|
||||
import { Environment } from './types';
|
||||
|
||||
export const ENVIRONMENT = new InjectionToken<Environment>('environment');
|
||||
16
frontend/libs/shared/environment/src/lib/types.ts
Normal file
16
frontend/libs/shared/environment/src/lib/types.ts
Normal file
@ -0,0 +1,16 @@
|
||||
export type Environment = {
|
||||
wsBaseUrl: string;
|
||||
production: boolean;
|
||||
apis: APIS;
|
||||
}
|
||||
|
||||
export const API_KEYS = [
|
||||
'containers',
|
||||
'container',
|
||||
'stats',
|
||||
'combined'
|
||||
] as const;
|
||||
|
||||
export type ApiKey = (typeof API_KEYS)[number];
|
||||
|
||||
export type APIS = Record<ApiKey, string>;
|
||||
6
frontend/libs/shared/environment/src/test-setup.ts
Normal file
6
frontend/libs/shared/environment/src/test-setup.ts
Normal file
@ -0,0 +1,6 @@
|
||||
import { setupZoneTestEnv } from 'jest-preset-angular/setup-env/zone';
|
||||
|
||||
setupZoneTestEnv({
|
||||
errorOnUnknownElements: true,
|
||||
errorOnUnknownProperties: true,
|
||||
});
|
||||
28
frontend/libs/shared/environment/tsconfig.json
Normal file
28
frontend/libs/shared/environment/tsconfig.json
Normal file
@ -0,0 +1,28 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "es2022",
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"strict": true,
|
||||
"noImplicitOverride": true,
|
||||
"noPropertyAccessFromIndexSignature": true,
|
||||
"noImplicitReturns": true,
|
||||
"noFallthroughCasesInSwitch": true
|
||||
},
|
||||
"files": [],
|
||||
"include": [],
|
||||
"references": [
|
||||
{
|
||||
"path": "./tsconfig.lib.json"
|
||||
},
|
||||
{
|
||||
"path": "./tsconfig.spec.json"
|
||||
}
|
||||
],
|
||||
"extends": "../../../tsconfig.base.json",
|
||||
"angularCompilerOptions": {
|
||||
"enableI18nLegacyMessageIdFormat": false,
|
||||
"strictInjectionParameters": true,
|
||||
"strictInputAccessModifiers": true,
|
||||
"strictTemplates": true
|
||||
}
|
||||
}
|
||||
17
frontend/libs/shared/environment/tsconfig.lib.json
Normal file
17
frontend/libs/shared/environment/tsconfig.lib.json
Normal file
@ -0,0 +1,17 @@
|
||||
{
|
||||
"extends": "./tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "../../../dist/out-tsc",
|
||||
"declaration": true,
|
||||
"declarationMap": true,
|
||||
"inlineSources": true,
|
||||
"types": []
|
||||
},
|
||||
"exclude": [
|
||||
"src/**/*.spec.ts",
|
||||
"src/test-setup.ts",
|
||||
"jest.config.ts",
|
||||
"src/**/*.test.ts"
|
||||
],
|
||||
"include": ["src/**/*.ts"]
|
||||
}
|
||||
16
frontend/libs/shared/environment/tsconfig.spec.json
Normal file
16
frontend/libs/shared/environment/tsconfig.spec.json
Normal file
@ -0,0 +1,16 @@
|
||||
{
|
||||
"extends": "./tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "../../../dist/out-tsc",
|
||||
"module": "commonjs",
|
||||
"target": "es2016",
|
||||
"types": ["jest", "node"]
|
||||
},
|
||||
"files": ["src/test-setup.ts"],
|
||||
"include": [
|
||||
"jest.config.ts",
|
||||
"src/**/*.test.ts",
|
||||
"src/**/*.spec.ts",
|
||||
"src/**/*.d.ts"
|
||||
]
|
||||
}
|
||||
@ -1,36 +1,161 @@
|
||||
import { computed, inject, Injectable, signal } from "@angular/core";
|
||||
import { HttpClient } from "@angular/common/http";
|
||||
import { ContainerInfo, PayloadContainers } from "./types";
|
||||
import { effect, inject, Injectable, signal } from '@angular/core';
|
||||
import { ApiKey, ENVIRONMENT } from '@frontend/shared/environment';
|
||||
import {
|
||||
ContainerInfo,
|
||||
ContainerStats,
|
||||
PayloadContainerDetail,
|
||||
PayloadContainers,
|
||||
PayloadStats,
|
||||
} from './types';
|
||||
|
||||
@Injectable({
|
||||
providedIn: "root",
|
||||
providedIn: 'root',
|
||||
})
|
||||
export class StatsWsService {
|
||||
private _containers = signal<ContainerInfo[]>([]);
|
||||
readonly containers = signal<ContainerInfo[]>([]);
|
||||
readonly containerStats = signal<Record<string, ContainerStats>>({});
|
||||
readonly containerDetail = signal<PayloadContainerDetail | null>(null);
|
||||
private environment = inject(ENVIRONMENT);
|
||||
private connections: Partial<Record<ApiKey, WebSocket>> = {};
|
||||
|
||||
private socket?: WebSocket;
|
||||
constructor() {
|
||||
this.connectToEndpoint('containers');
|
||||
|
||||
constructor() {
|
||||
this.initWebSocket();
|
||||
}
|
||||
effect(() => {
|
||||
return () => this.closeAllConnections();
|
||||
});
|
||||
}
|
||||
|
||||
readonly containers = computed(() => this._containers());
|
||||
initContainers(): void {
|
||||
this.connectToEndpoint('containers');
|
||||
}
|
||||
|
||||
private initWebSocket() {
|
||||
this.socket = new WebSocket("ws://localhost:3000/ws/containers")
|
||||
initStats(): void {
|
||||
this.connectToEndpoint('stats');
|
||||
|
||||
this.socket.onmessage = (event) => {
|
||||
try {
|
||||
const parsed = JSON.parse(event.data) as PayloadContainers;
|
||||
this._containers.set(parsed.containers);
|
||||
} catch (error) {
|
||||
console.error("Error parsing WebSocket message:", error);
|
||||
}
|
||||
}
|
||||
this.connectToEndpoint('containers');
|
||||
}
|
||||
|
||||
this.socket.onclose = () => {
|
||||
console.warn("WebSocket connection closed. Attempting to reconnect...");
|
||||
setTimeout(() => this.initWebSocket(), 2000);
|
||||
}
|
||||
};
|
||||
};
|
||||
initContainerDetail(containerId: string): void {
|
||||
this.connectToEndpoint('container', { id: containerId });
|
||||
}
|
||||
|
||||
initCombined(): void {
|
||||
this.connectToEndpoint('combined');
|
||||
}
|
||||
|
||||
connectToEndpoint(
|
||||
endpoint: ApiKey,
|
||||
queryParams?: Record<string, string>
|
||||
): WebSocket | undefined {
|
||||
if (this.connections[endpoint]) {
|
||||
return this.connections[endpoint];
|
||||
}
|
||||
|
||||
try {
|
||||
let wsUrl = `${this.environment.wsBaseUrl}${this.environment.apis[endpoint]}`;
|
||||
|
||||
if (queryParams) {
|
||||
const params = new URLSearchParams();
|
||||
Object.entries(queryParams).forEach(([key, value]) => {
|
||||
params.append(key, value);
|
||||
});
|
||||
wsUrl += `?${params.toString()}`;
|
||||
}
|
||||
|
||||
const socket = new WebSocket(wsUrl);
|
||||
|
||||
socket.onmessage = (event) => this.handleMessage(event, endpoint);
|
||||
|
||||
socket.onclose = () => {
|
||||
console.warn(
|
||||
`WebSocket connection to ${endpoint} closed. Attempting to reconnect...`
|
||||
);
|
||||
delete this.connections[endpoint];
|
||||
setTimeout(() => this.connectToEndpoint(endpoint), 2000);
|
||||
};
|
||||
|
||||
socket.onerror = (error) => {
|
||||
console.error(`WebSocket error for ${endpoint}:`, error);
|
||||
};
|
||||
|
||||
this.connections[endpoint] = socket;
|
||||
return socket;
|
||||
} catch (error) {
|
||||
console.error(`Failed to connect to ${endpoint}:`, error);
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
closeConnection(endpoint: ApiKey): void {
|
||||
if (this.connections[endpoint]) {
|
||||
this.connections[endpoint]?.close();
|
||||
delete this.connections[endpoint];
|
||||
}
|
||||
}
|
||||
|
||||
closeAllConnections(): void {
|
||||
Object.keys(this.connections).forEach((key) => {
|
||||
this.closeConnection(key as ApiKey);
|
||||
});
|
||||
}
|
||||
|
||||
private handleMessage(event: MessageEvent, endpoint: ApiKey): void {
|
||||
try {
|
||||
const data = JSON.parse(event.data);
|
||||
|
||||
switch (endpoint) {
|
||||
case 'containers':
|
||||
this.handleContainersMessage(data as PayloadContainers);
|
||||
break;
|
||||
case 'stats':
|
||||
this.handleStatsMessage(data as PayloadStats);
|
||||
break;
|
||||
case 'container':
|
||||
this.handleContainerDetailMessage(data as PayloadContainerDetail);
|
||||
break;
|
||||
case 'combined':
|
||||
this.handleCombinedMessage(data);
|
||||
break;
|
||||
default:
|
||||
console.warn(`Unhandled endpoint type: ${endpoint}`);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`Error parsing WebSocket message from ${endpoint}:`, error);
|
||||
}
|
||||
}
|
||||
|
||||
private handleContainersMessage(data: PayloadContainers): void {
|
||||
this.containers.set(data.containers);
|
||||
}
|
||||
|
||||
private handleStatsMessage(data: PayloadStats): void {
|
||||
if (data.stats && Array.isArray(data.stats)) {
|
||||
data.stats.forEach((stat) => {
|
||||
this.containerStats.update((currentStats) => ({
|
||||
...currentStats,
|
||||
[stat.id]: stat,
|
||||
}));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private handleContainerDetailMessage(data: PayloadContainerDetail): void {
|
||||
this.containerDetail.set(data);
|
||||
}
|
||||
|
||||
private handleCombinedMessage(data: any): void {
|
||||
if (data.containers) {
|
||||
this.containers.set(data.containers);
|
||||
}
|
||||
|
||||
if (data.stats) {
|
||||
Object.entries(data.stats).forEach(([containerId, stats]) => {
|
||||
this.containerStats.update((current) => ({
|
||||
...current,
|
||||
[containerId]: stats as ContainerStats,
|
||||
}));
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,24 +1,33 @@
|
||||
export type ContainerInfo = {
|
||||
id: string;
|
||||
name: string;
|
||||
image: string;
|
||||
state: string;
|
||||
status: string;
|
||||
}
|
||||
id: string;
|
||||
name: string;
|
||||
image: string;
|
||||
state: string;
|
||||
status: string;
|
||||
};
|
||||
|
||||
export type ContainerStats = {
|
||||
id: string
|
||||
cpuPercent: number
|
||||
memUsage: number
|
||||
memLimit: number
|
||||
memPercent: number
|
||||
netRx: number
|
||||
netTx: number
|
||||
blkRead: number
|
||||
blkWrite: number
|
||||
timestamp: number
|
||||
}
|
||||
id: string;
|
||||
cpuPercent: number;
|
||||
memUsage: number;
|
||||
memLimit: number;
|
||||
memPercent: number;
|
||||
netRx: number;
|
||||
netTx: number;
|
||||
blkRead: number;
|
||||
blkWrite: number;
|
||||
timestamp: number;
|
||||
};
|
||||
|
||||
export type PayloadContainers = {
|
||||
containers: ContainerInfo[];
|
||||
}
|
||||
containers: ContainerInfo[];
|
||||
};
|
||||
|
||||
export type PayloadStats = {
|
||||
stats: ContainerStats[];
|
||||
};
|
||||
|
||||
export type PayloadContainerDetail = {
|
||||
container: ContainerInfo;
|
||||
stats: ContainerStats;
|
||||
};
|
||||
|
||||
3601
frontend/package-lock.json
generated
3601
frontend/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -9,51 +9,56 @@
|
||||
},
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"@angular/common": "~19.2.0",
|
||||
"@angular/compiler": "~19.2.0",
|
||||
"@angular/core": "~19.2.0",
|
||||
"@angular/forms": "~19.2.0",
|
||||
"@angular/platform-browser": "~19.2.0",
|
||||
"@angular/platform-browser-dynamic": "~19.2.0",
|
||||
"@angular/router": "~19.2.0",
|
||||
"rxjs": "~7.8.0",
|
||||
"zone.js": "~0.15.0"
|
||||
"@angular/cdk": "^20.0.1",
|
||||
"@angular/common": "20.0.0",
|
||||
"@angular/compiler": "20.0.0",
|
||||
"@angular/core": "20.0.0",
|
||||
"@angular/forms": "20.0.0",
|
||||
"@angular/material": "^20.0.1",
|
||||
"@angular/platform-browser": "20.0.0",
|
||||
"@angular/platform-browser-dynamic": "20.0.0",
|
||||
"@angular/router": "20.0.0",
|
||||
"@mmstack/form-material": "^19.2.0",
|
||||
"@mmstack/primitives": "^19.2.3",
|
||||
"@mmstack/resource": "^19.2.0",
|
||||
"rxjs": "7.8.2",
|
||||
"zone.js": "0.15.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@angular-devkit/build-angular": "~19.2.0",
|
||||
"@angular-devkit/core": "~19.2.0",
|
||||
"@angular-devkit/schematics": "~19.2.0",
|
||||
"@angular/cli": "~19.2.0",
|
||||
"@angular/compiler-cli": "~19.2.0",
|
||||
"@angular/language-service": "~19.2.0",
|
||||
"@eslint/js": "^9.8.0",
|
||||
"@angular-devkit/build-angular": "20.0.0",
|
||||
"@angular-devkit/core": "20.0.0",
|
||||
"@angular-devkit/schematics": "20.0.0",
|
||||
"@angular/cli": "20.0.0",
|
||||
"@angular/compiler-cli": "20.0.0",
|
||||
"@angular/language-service": "20.0.0",
|
||||
"@eslint/js": "9.28.0",
|
||||
"@nx/angular": "21.1.2",
|
||||
"@nx/eslint": "21.1.2",
|
||||
"@nx/eslint-plugin": "^21.1.2",
|
||||
"@nx/eslint-plugin": "21.1.2",
|
||||
"@nx/jest": "21.1.2",
|
||||
"@nx/js": "21.1.2",
|
||||
"@nx/web": "21.1.2",
|
||||
"@nx/workspace": "21.1.2",
|
||||
"@schematics/angular": "~19.2.0",
|
||||
"@swc-node/register": "~1.9.1",
|
||||
"@swc/core": "~1.5.7",
|
||||
"@swc/helpers": "~0.5.11",
|
||||
"@types/jest": "^29.5.12",
|
||||
"@types/node": "18.16.9",
|
||||
"@typescript-eslint/utils": "^8.19.0",
|
||||
"angular-eslint": "^19.2.0",
|
||||
"eslint": "^9.8.0",
|
||||
"eslint-config-prettier": "^10.0.0",
|
||||
"jest": "^29.7.0",
|
||||
"jest-environment-jsdom": "^29.7.0",
|
||||
"jest-preset-angular": "~14.4.0",
|
||||
"@schematics/angular": "20.0.0",
|
||||
"@swc-node/register": "1.10.10",
|
||||
"@swc/core": "1.11.29",
|
||||
"@swc/helpers": "0.5.17",
|
||||
"@types/jest": "29.5.14",
|
||||
"@types/node": "22.15.29",
|
||||
"@typescript-eslint/utils": "8.33.1",
|
||||
"angular-eslint": "19.7.0",
|
||||
"eslint": "9.28.0",
|
||||
"eslint-config-prettier": "10.1.5",
|
||||
"jest": "29.7.0",
|
||||
"jest-environment-jsdom": "29.7.0",
|
||||
"jest-preset-angular": "14.6.0",
|
||||
"nx": "21.1.2",
|
||||
"prettier": "^2.6.2",
|
||||
"ts-jest": "^29.1.0",
|
||||
"ts-node": "10.9.1",
|
||||
"tslib": "^2.3.0",
|
||||
"typescript": "~5.7.2",
|
||||
"typescript-eslint": "^8.19.0"
|
||||
"prettier": "3.5.3",
|
||||
"ts-jest": "29.3.4",
|
||||
"ts-node": "10.9.2",
|
||||
"tslib": "2.8.1",
|
||||
"typescript": "5.8.3",
|
||||
"typescript-eslint": "8.33.1"
|
||||
},
|
||||
"nx": {
|
||||
"includedScripts": []
|
||||
|
||||
@ -1,88 +1,89 @@
|
||||
{
|
||||
"name": "frontend",
|
||||
"$schema": "node_modules/nx/schemas/project-schema.json",
|
||||
"includedScripts": [],
|
||||
"projectType": "application",
|
||||
"prefix": "app",
|
||||
"sourceRoot": "./src",
|
||||
"tags": [],
|
||||
"targets": {
|
||||
"build": {
|
||||
"executor": "@angular-devkit/build-angular:application",
|
||||
"outputs": ["{options.outputPath}"],
|
||||
"options": {
|
||||
"outputPath": "dist/frontend",
|
||||
"index": "./src/index.html",
|
||||
"browser": "./src/main.ts",
|
||||
"polyfills": ["zone.js"],
|
||||
"tsConfig": "tsconfig.app.json",
|
||||
"inlineStyleLanguage": "scss",
|
||||
"assets": [
|
||||
{
|
||||
"glob": "**/*",
|
||||
"input": "public"
|
||||
}
|
||||
],
|
||||
"styles": ["./src/styles.scss"],
|
||||
"scripts": []
|
||||
},
|
||||
"configurations": {
|
||||
"production": {
|
||||
"budgets": [
|
||||
{
|
||||
"type": "initial",
|
||||
"maximumWarning": "500kb",
|
||||
"maximumError": "1mb"
|
||||
},
|
||||
{
|
||||
"type": "anyComponentStyle",
|
||||
"maximumWarning": "4kb",
|
||||
"maximumError": "8kb"
|
||||
}
|
||||
],
|
||||
"outputHashing": "all"
|
||||
},
|
||||
"development": {
|
||||
"optimization": false,
|
||||
"extractLicenses": false,
|
||||
"sourceMap": true
|
||||
}
|
||||
},
|
||||
"defaultConfiguration": "production"
|
||||
},
|
||||
"serve": {
|
||||
"continuous": true,
|
||||
"executor": "@angular-devkit/build-angular:dev-server",
|
||||
"configurations": {
|
||||
"production": {
|
||||
"buildTarget": "frontend:build:production"
|
||||
},
|
||||
"development": {
|
||||
"buildTarget": "frontend:build:development"
|
||||
}
|
||||
},
|
||||
"defaultConfiguration": "development"
|
||||
},
|
||||
"extract-i18n": {
|
||||
"executor": "@angular-devkit/build-angular:extract-i18n",
|
||||
"options": {
|
||||
"buildTarget": "frontend:build"
|
||||
}
|
||||
},
|
||||
"lint": {
|
||||
"executor": "@nx/eslint:lint",
|
||||
"options": {
|
||||
"lintFilePatterns": ["./src"]
|
||||
}
|
||||
},
|
||||
"serve-static": {
|
||||
"continuous": true,
|
||||
"executor": "@nx/web:file-server",
|
||||
"options": {
|
||||
"buildTarget": "frontend:build",
|
||||
"staticFilePath": "dist/frontend/browser",
|
||||
"spa": true
|
||||
}
|
||||
}
|
||||
}
|
||||
"name": "frontend",
|
||||
"$schema": "node_modules/nx/schemas/project-schema.json",
|
||||
"includedScripts": [],
|
||||
"projectType": "application",
|
||||
"prefix": "app",
|
||||
"sourceRoot": "./src",
|
||||
"tags": [],
|
||||
"targets": {
|
||||
"build": {
|
||||
"executor": "@angular-devkit/build-angular:application",
|
||||
"outputs": ["{options.outputPath}"],
|
||||
"options": {
|
||||
"outputPath": "dist/frontend",
|
||||
"index": "./src/index.html",
|
||||
"browser": "./src/main.ts",
|
||||
"polyfills": ["zone.js"],
|
||||
"tsConfig": "tsconfig.app.json",
|
||||
"inlineStyleLanguage": "scss",
|
||||
"assets": [
|
||||
{
|
||||
"glob": "**/*",
|
||||
"input": "public"
|
||||
}
|
||||
],
|
||||
"styles": ["./src/styles.scss", "./src/theme.scss"],
|
||||
"scripts": []
|
||||
},
|
||||
"configurations": {
|
||||
"production": {
|
||||
"budgets": [
|
||||
{
|
||||
"type": "initial",
|
||||
"maximumWarning": "500kb",
|
||||
"maximumError": "1mb"
|
||||
},
|
||||
{
|
||||
"type": "anyComponentStyle",
|
||||
"maximumWarning": "4kb",
|
||||
"maximumError": "8kb"
|
||||
}
|
||||
],
|
||||
"outputHashing": "all"
|
||||
},
|
||||
"development": {
|
||||
"optimization": false,
|
||||
"extractLicenses": false,
|
||||
"sourceMap": true
|
||||
}
|
||||
},
|
||||
"defaultConfiguration": "production"
|
||||
},
|
||||
"serve": {
|
||||
"continuous": true,
|
||||
"executor": "@angular-devkit/build-angular:dev-server",
|
||||
"configurations": {
|
||||
"production": {
|
||||
"buildTarget": "frontend:build:production"
|
||||
},
|
||||
"development": {
|
||||
"buildTarget": "frontend:build:development"
|
||||
}
|
||||
},
|
||||
"defaultConfiguration": "development"
|
||||
},
|
||||
"extract-i18n": {
|
||||
"executor": "@angular-devkit/build-angular:extract-i18n",
|
||||
"options": {
|
||||
"buildTarget": "frontend:build"
|
||||
}
|
||||
},
|
||||
"lint": {
|
||||
"executor": "@nx/eslint:lint",
|
||||
"options": {
|
||||
"lintFilePatterns": ["./src"]
|
||||
}
|
||||
},
|
||||
"serve-static": {
|
||||
"continuous": true,
|
||||
"executor": "@nx/web:file-server",
|
||||
"options": {
|
||||
"buildTarget": "frontend:build",
|
||||
"staticFilePath": "dist/frontend/browser",
|
||||
"spa": true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -1,30 +1,35 @@
|
||||
import { Component, inject } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { Component, OnInit, inject } from '@angular/core';
|
||||
import { RouterModule } from '@angular/router';
|
||||
import { StatsWsService } from '@frontend/shared/stats-ws';
|
||||
import { CommonModule } from '@angular/common';
|
||||
|
||||
@Component({
|
||||
standalone: true,
|
||||
imports: [RouterModule, CommonModule],
|
||||
selector: 'app-root',
|
||||
template: `
|
||||
<div>
|
||||
<h1>Docker Containers</h1>
|
||||
standalone: true,
|
||||
imports: [RouterModule, CommonModule],
|
||||
selector: 'app-root',
|
||||
template: `
|
||||
<div>
|
||||
<h1>Docker Containers</h1>
|
||||
|
||||
@for (container of containers(); track container.id) {
|
||||
<div class="container">
|
||||
<h2>{{ container.name }}</h2>
|
||||
<p>ID: {{ container.id }}</p>
|
||||
<p>Image: {{ container.image }}</p>
|
||||
<p>Status: {{ container.status }}</p>
|
||||
<p>State: {{ container.state }}</p>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
<router-outlet></router-outlet>
|
||||
`
|
||||
@for (container of containers(); track container.id) {
|
||||
<div class="container">
|
||||
<h2>{{ container.name }}</h2>
|
||||
<p>ID: {{ container.id }}</p>
|
||||
<p>Image: {{ container.image }}</p>
|
||||
<p>Status: {{ container.status }}</p>
|
||||
<p>State: {{ container.state }}</p>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
<router-outlet></router-outlet>
|
||||
`,
|
||||
})
|
||||
export class AppComponent {
|
||||
private readonly service = inject(StatsWsService);
|
||||
protected containers = this.service.containers;
|
||||
export class AppComponent implements OnInit {
|
||||
private readonly service = inject(StatsWsService);
|
||||
protected containers = this.service.containers;
|
||||
|
||||
ngOnInit() {
|
||||
this.service.initContainers();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -2,11 +2,14 @@ import { ApplicationConfig, provideZoneChangeDetection } from '@angular/core';
|
||||
import { provideRouter } from '@angular/router';
|
||||
import { appRoutes } from './app.routes';
|
||||
import { provideHttpClient } from '@angular/common/http';
|
||||
import { ENVIRONMENT } from '@frontend/shared/environment';
|
||||
import { environment } from '../environments/environment';
|
||||
|
||||
export const appConfig: ApplicationConfig = {
|
||||
providers: [
|
||||
provideZoneChangeDetection({ eventCoalescing: true }),
|
||||
provideHttpClient(),
|
||||
provideRouter(appRoutes),
|
||||
{ provide: ENVIRONMENT, useValue: environment }
|
||||
],
|
||||
};
|
||||
|
||||
13
frontend/src/environments/environment.prod.ts
Normal file
13
frontend/src/environments/environment.prod.ts
Normal file
@ -0,0 +1,13 @@
|
||||
import { Environment } from '@frontend/shared/environment';
|
||||
|
||||
export const environment: Environment = {
|
||||
// Change this to your production WebSocket server URL
|
||||
wsBaseUrl: 'wss://your-production-server.com',
|
||||
production: true,
|
||||
apis: {
|
||||
containers: '/ws/containers',
|
||||
container: '/ws/container',
|
||||
stats: '/ws/stats',
|
||||
combined: '/ws/combined'
|
||||
}
|
||||
};
|
||||
12
frontend/src/environments/environment.ts
Normal file
12
frontend/src/environments/environment.ts
Normal file
@ -0,0 +1,12 @@
|
||||
import { Environment } from '@frontend/shared/environment';
|
||||
|
||||
export const environment: Environment = {
|
||||
wsBaseUrl: 'ws://localhost:3000',
|
||||
production: false,
|
||||
apis: {
|
||||
containers: '/ws/containers',
|
||||
container: '/ws/container',
|
||||
stats: '/ws/stats',
|
||||
combined: '/ws/combined'
|
||||
}
|
||||
};
|
||||
16
frontend/src/theme.scss
Normal file
16
frontend/src/theme.scss
Normal file
@ -0,0 +1,16 @@
|
||||
@use '@angular/material' as mat;
|
||||
|
||||
:root {
|
||||
color-scheme: light dark;
|
||||
@include mat.theme(
|
||||
(
|
||||
color: (
|
||||
primary: mat.$azure-palette,
|
||||
tertiary: mat.$blue-palette,
|
||||
theme-type: color-scheme,
|
||||
),
|
||||
typography: Roboto,
|
||||
density: -3,
|
||||
)
|
||||
);
|
||||
}
|
||||
@ -15,6 +15,7 @@
|
||||
"skipDefaultLibCheck": true,
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"@frontend/shared/environment": ["libs/shared/environment/src/index.ts"],
|
||||
"@frontend/shared/stats-ws": ["libs/shared/stats-ws/src/index.ts"]
|
||||
}
|
||||
},
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user