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 type { WSContext } from 'hono/ws';
|
||||||
import { listContainers, streamContainerStats } from '../docker/containers.js';
|
import { listContainers, streamContainerStats } from '../docker/containers.js';
|
||||||
import { sendContainerInfo, sendStats, sendCombined, sendToSingleContainer } from './sender.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 containerClients = new Set<WSContext<WebSocket>>();
|
||||||
const statsClients = new Set<WSContext<WebSocket>>();
|
const statsClients = new Set<WSContext<WebSocket>>();
|
||||||
const combinedClients = new Set<WSContext<WebSocket>>();
|
const combinedClients = new Set<WSContext<WebSocket>>();
|
||||||
const singleContainerClients = new Map<string, Set<WSContext<WebSocket>>>();
|
const singleContainerClients = new Map<string, Set<WSContext<WebSocket>>>();
|
||||||
const activeStreams = new Map<string, () => void>();
|
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>) {
|
export function registerContainerClient(ws: WSContext<WebSocket>) {
|
||||||
const wasEmpty = allClientsEmpty();
|
const wasEmpty = allClientsEmpty();
|
||||||
containerClients.add(ws);
|
containerClients.add(ws);
|
||||||
@ -39,7 +48,7 @@ export function registerSingleContainerClient(ws: WSContext<WebSocket>, containe
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Client removal functions
|
|
||||||
export function removeContainerClient(ws: WSContext<WebSocket>) {
|
export function removeContainerClient(ws: WSContext<WebSocket>) {
|
||||||
containerClients.delete(ws);
|
containerClients.delete(ws);
|
||||||
checkAndCleanup();
|
checkAndCleanup();
|
||||||
@ -83,53 +92,89 @@ async function setupStreams() {
|
|||||||
if (allClientsEmpty()) return;
|
if (allClientsEmpty()) return;
|
||||||
try {
|
try {
|
||||||
const containers = await listContainers();
|
const containers = await listContainers();
|
||||||
|
|
||||||
|
for (const container of containers) {
|
||||||
|
containerCache.set(container.id, {
|
||||||
|
data: container,
|
||||||
|
lastUpdated: Date.now()
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
for (const container of containers) {
|
for (const container of containers) {
|
||||||
if (activeStreams.has(container.id)) continue;
|
if (activeStreams.has(container.id)) continue;
|
||||||
setupStreamForContainer(container.id, containers);
|
setupStreamForContainer(container.id);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error setting up streams:', error);
|
console.error('Error setting up streams:', error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function setupStreamForContainer(containerId: string, allContainers?: any[]) {
|
async function getContainerData(containerId: string): Promise<DockerContainer | null> {
|
||||||
if (activeStreams.has(containerId)) return;
|
const cached = containerCache.get(containerId);
|
||||||
try {
|
const now = Date.now();
|
||||||
let containerData;
|
|
||||||
if (!allContainers) {
|
if (cached && (now - cached.lastUpdated < CACHE_TTL)) {
|
||||||
const containers = await listContainers();
|
return cached.data;
|
||||||
containerData = containers.find(c => c.id === containerId);
|
|
||||||
} else {
|
|
||||||
containerData = allContainers.find(c => c.id === containerId);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
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) {
|
if (!containerData) {
|
||||||
console.warn(`Container ${containerId} not found`);
|
console.warn(`Container ${containerId} not found`);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const stopStream = await streamContainerStats(containerId, async (stats) => {
|
|
||||||
|
const stopStream = await streamContainerStats(containerId, async (stats: ContainerStats) => {
|
||||||
if (allClientsEmpty()) {
|
if (allClientsEmpty()) {
|
||||||
stopAllStreams();
|
stopAllStreams();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
// Always fetch latest container info
|
|
||||||
let containerData;
|
|
||||||
if (!allContainers) {
|
const freshContainerData = await getContainerData(containerId);
|
||||||
const containers = await listContainers();
|
|
||||||
containerData = containers.find(c => c.id === containerId);
|
const containerInfo: ContainerInfo = freshContainerData ? {
|
||||||
} else {
|
id: freshContainerData.id,
|
||||||
containerData = allContainers.find(c => c.id === containerId);
|
name: freshContainerData.names?.[0]?.replace('/', '') ?? freshContainerData.id.slice(0, 12),
|
||||||
}
|
image: freshContainerData.image,
|
||||||
if (!containerData) {
|
state: freshContainerData.state,
|
||||||
console.warn(`Container ${containerId} not found`);
|
status: freshContainerData.status,
|
||||||
return;
|
} : {
|
||||||
}
|
|
||||||
const containerInfo = {
|
|
||||||
id: containerData.id,
|
id: containerData.id,
|
||||||
name: containerData.names?.[0]?.replace('/', '') ?? containerData.id.slice(0, 12),
|
name: containerData.names?.[0]?.replace('/', '') ?? containerData.id.slice(0, 12),
|
||||||
image: containerData.image,
|
image: containerData.image,
|
||||||
state: containerData.state,
|
state: 'exited',
|
||||||
status: containerData.status,
|
status: 'Container stopped',
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
if (!freshContainerData && !singleContainerClients.has(containerId)) {
|
||||||
|
stopStreamForContainer(containerId);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (containerClients.size > 0) {
|
if (containerClients.size > 0) {
|
||||||
sendContainerInfo(containerClients, containerInfo);
|
sendContainerInfo(containerClients, containerInfo);
|
||||||
}
|
}
|
||||||
@ -144,9 +189,12 @@ async function setupStreamForContainer(containerId: string, allContainers?: any[
|
|||||||
sendToSingleContainer(singleContainerClients, containerId, containerInfo, stats);
|
sendToSingleContainer(singleContainerClients, containerId, containerInfo, stats);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
activeStreams.set(containerId, stopStream);
|
activeStreams.set(containerId, stopStream);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(`Failed to setup stream for container ${containerId}:`, error);
|
console.error(`Failed to setup stream for container ${containerId}:`, error);
|
||||||
|
|
||||||
|
containerCache.delete(containerId);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -155,6 +203,8 @@ function stopStreamForContainer(containerId: string) {
|
|||||||
if (stopStream) {
|
if (stopStream) {
|
||||||
stopStream();
|
stopStream();
|
||||||
activeStreams.delete(containerId);
|
activeStreams.delete(containerId);
|
||||||
|
|
||||||
|
containerCache.delete(containerId);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -163,4 +213,5 @@ export function stopAllStreams() {
|
|||||||
stopStream();
|
stopStream();
|
||||||
}
|
}
|
||||||
activeStreams.clear();
|
activeStreams.clear();
|
||||||
|
containerCache.clear();
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,18 +1,21 @@
|
|||||||
// Functions for sending data to WebSocket clients
|
|
||||||
|
|
||||||
import type { WSContext } from 'hono/ws';
|
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] });
|
const payload = JSON.stringify({ containers: [containerInfo] });
|
||||||
sendToClientSet(containerClients, payload);
|
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] });
|
const payload = JSON.stringify({ stats: [stats] });
|
||||||
sendToClientSet(statsClients, payload);
|
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({
|
const payload = JSON.stringify({
|
||||||
containers: [containerInfo],
|
containers: [containerInfo],
|
||||||
stats: [stats]
|
stats: [stats]
|
||||||
@ -20,7 +23,12 @@ export function sendCombined(combinedClients: Set<WSContext<WebSocket>>, contain
|
|||||||
sendToClientSet(combinedClients, payload);
|
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 = {
|
const combinedData = {
|
||||||
...containerInfo,
|
...containerInfo,
|
||||||
...stats
|
...stats
|
||||||
@ -35,7 +43,12 @@ export function sendToSingleContainer(singleContainerClients: Map<string, Set<WS
|
|||||||
export function sendToClientSet(clients: Set<WSContext<WebSocket>>, payload: string) {
|
export function sendToClientSet(clients: Set<WSContext<WebSocket>>, payload: string) {
|
||||||
for (const ws of clients) {
|
for (const ws of clients) {
|
||||||
if (ws.readyState === 1) {
|
if (ws.readyState === 1) {
|
||||||
|
try {
|
||||||
ws.send(payload);
|
ws.send(payload);
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Error sending to client:', err);
|
||||||
|
clients.delete(ws);
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
clients.delete(ws);
|
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 { effect, inject, Injectable, signal } from '@angular/core';
|
||||||
import { HttpClient } from "@angular/common/http";
|
import { ApiKey, ENVIRONMENT } from '@frontend/shared/environment';
|
||||||
import { ContainerInfo, PayloadContainers } from "./types";
|
import {
|
||||||
|
ContainerInfo,
|
||||||
|
ContainerStats,
|
||||||
|
PayloadContainerDetail,
|
||||||
|
PayloadContainers,
|
||||||
|
PayloadStats,
|
||||||
|
} from './types';
|
||||||
|
|
||||||
@Injectable({
|
@Injectable({
|
||||||
providedIn: "root",
|
providedIn: 'root',
|
||||||
})
|
})
|
||||||
export class StatsWsService {
|
export class StatsWsService {
|
||||||
private _containers = signal<ContainerInfo[]>([]);
|
readonly containers = signal<ContainerInfo[]>([]);
|
||||||
|
readonly containerStats = signal<Record<string, ContainerStats>>({});
|
||||||
private socket?: WebSocket;
|
readonly containerDetail = signal<PayloadContainerDetail | null>(null);
|
||||||
|
private environment = inject(ENVIRONMENT);
|
||||||
|
private connections: Partial<Record<ApiKey, WebSocket>> = {};
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
this.initWebSocket();
|
this.connectToEndpoint('containers');
|
||||||
|
|
||||||
|
effect(() => {
|
||||||
|
return () => this.closeAllConnections();
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
readonly containers = computed(() => this._containers());
|
initContainers(): void {
|
||||||
|
this.connectToEndpoint('containers');
|
||||||
|
}
|
||||||
|
|
||||||
private initWebSocket() {
|
initStats(): void {
|
||||||
this.socket = new WebSocket("ws://localhost:3000/ws/containers")
|
this.connectToEndpoint('stats');
|
||||||
|
|
||||||
|
this.connectToEndpoint('containers');
|
||||||
|
}
|
||||||
|
|
||||||
|
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];
|
||||||
|
}
|
||||||
|
|
||||||
this.socket.onmessage = (event) => {
|
|
||||||
try {
|
try {
|
||||||
const parsed = JSON.parse(event.data) as PayloadContainers;
|
let wsUrl = `${this.environment.wsBaseUrl}${this.environment.apis[endpoint]}`;
|
||||||
this._containers.set(parsed.containers);
|
|
||||||
|
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) {
|
} catch (error) {
|
||||||
console.error("Error parsing WebSocket message:", error);
|
console.error(`Failed to connect to ${endpoint}:`, error);
|
||||||
|
return undefined;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
this.socket.onclose = () => {
|
closeConnection(endpoint: ApiKey): void {
|
||||||
console.warn("WebSocket connection closed. Attempting to reconnect...");
|
if (this.connections[endpoint]) {
|
||||||
setTimeout(() => this.initWebSocket(), 2000);
|
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,
|
||||||
|
}));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@ -4,21 +4,30 @@ export type ContainerInfo = {
|
|||||||
image: string;
|
image: string;
|
||||||
state: string;
|
state: string;
|
||||||
status: string;
|
status: string;
|
||||||
}
|
};
|
||||||
|
|
||||||
export type ContainerStats = {
|
export type ContainerStats = {
|
||||||
id: string
|
id: string;
|
||||||
cpuPercent: number
|
cpuPercent: number;
|
||||||
memUsage: number
|
memUsage: number;
|
||||||
memLimit: number
|
memLimit: number;
|
||||||
memPercent: number
|
memPercent: number;
|
||||||
netRx: number
|
netRx: number;
|
||||||
netTx: number
|
netTx: number;
|
||||||
blkRead: number
|
blkRead: number;
|
||||||
blkWrite: number
|
blkWrite: number;
|
||||||
timestamp: number
|
timestamp: number;
|
||||||
}
|
};
|
||||||
|
|
||||||
export type PayloadContainers = {
|
export type PayloadContainers = {
|
||||||
containers: ContainerInfo[];
|
containers: ContainerInfo[];
|
||||||
}
|
};
|
||||||
|
|
||||||
|
export type PayloadStats = {
|
||||||
|
stats: ContainerStats[];
|
||||||
|
};
|
||||||
|
|
||||||
|
export type PayloadContainerDetail = {
|
||||||
|
container: ContainerInfo;
|
||||||
|
stats: ContainerStats;
|
||||||
|
};
|
||||||
|
|||||||
3595
frontend/package-lock.json
generated
3595
frontend/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -9,51 +9,56 @@
|
|||||||
},
|
},
|
||||||
"private": true,
|
"private": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@angular/common": "~19.2.0",
|
"@angular/cdk": "^20.0.1",
|
||||||
"@angular/compiler": "~19.2.0",
|
"@angular/common": "20.0.0",
|
||||||
"@angular/core": "~19.2.0",
|
"@angular/compiler": "20.0.0",
|
||||||
"@angular/forms": "~19.2.0",
|
"@angular/core": "20.0.0",
|
||||||
"@angular/platform-browser": "~19.2.0",
|
"@angular/forms": "20.0.0",
|
||||||
"@angular/platform-browser-dynamic": "~19.2.0",
|
"@angular/material": "^20.0.1",
|
||||||
"@angular/router": "~19.2.0",
|
"@angular/platform-browser": "20.0.0",
|
||||||
"rxjs": "~7.8.0",
|
"@angular/platform-browser-dynamic": "20.0.0",
|
||||||
"zone.js": "~0.15.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": {
|
"devDependencies": {
|
||||||
"@angular-devkit/build-angular": "~19.2.0",
|
"@angular-devkit/build-angular": "20.0.0",
|
||||||
"@angular-devkit/core": "~19.2.0",
|
"@angular-devkit/core": "20.0.0",
|
||||||
"@angular-devkit/schematics": "~19.2.0",
|
"@angular-devkit/schematics": "20.0.0",
|
||||||
"@angular/cli": "~19.2.0",
|
"@angular/cli": "20.0.0",
|
||||||
"@angular/compiler-cli": "~19.2.0",
|
"@angular/compiler-cli": "20.0.0",
|
||||||
"@angular/language-service": "~19.2.0",
|
"@angular/language-service": "20.0.0",
|
||||||
"@eslint/js": "^9.8.0",
|
"@eslint/js": "9.28.0",
|
||||||
"@nx/angular": "21.1.2",
|
"@nx/angular": "21.1.2",
|
||||||
"@nx/eslint": "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/jest": "21.1.2",
|
||||||
"@nx/js": "21.1.2",
|
"@nx/js": "21.1.2",
|
||||||
"@nx/web": "21.1.2",
|
"@nx/web": "21.1.2",
|
||||||
"@nx/workspace": "21.1.2",
|
"@nx/workspace": "21.1.2",
|
||||||
"@schematics/angular": "~19.2.0",
|
"@schematics/angular": "20.0.0",
|
||||||
"@swc-node/register": "~1.9.1",
|
"@swc-node/register": "1.10.10",
|
||||||
"@swc/core": "~1.5.7",
|
"@swc/core": "1.11.29",
|
||||||
"@swc/helpers": "~0.5.11",
|
"@swc/helpers": "0.5.17",
|
||||||
"@types/jest": "^29.5.12",
|
"@types/jest": "29.5.14",
|
||||||
"@types/node": "18.16.9",
|
"@types/node": "22.15.29",
|
||||||
"@typescript-eslint/utils": "^8.19.0",
|
"@typescript-eslint/utils": "8.33.1",
|
||||||
"angular-eslint": "^19.2.0",
|
"angular-eslint": "19.7.0",
|
||||||
"eslint": "^9.8.0",
|
"eslint": "9.28.0",
|
||||||
"eslint-config-prettier": "^10.0.0",
|
"eslint-config-prettier": "10.1.5",
|
||||||
"jest": "^29.7.0",
|
"jest": "29.7.0",
|
||||||
"jest-environment-jsdom": "^29.7.0",
|
"jest-environment-jsdom": "29.7.0",
|
||||||
"jest-preset-angular": "~14.4.0",
|
"jest-preset-angular": "14.6.0",
|
||||||
"nx": "21.1.2",
|
"nx": "21.1.2",
|
||||||
"prettier": "^2.6.2",
|
"prettier": "3.5.3",
|
||||||
"ts-jest": "^29.1.0",
|
"ts-jest": "29.3.4",
|
||||||
"ts-node": "10.9.1",
|
"ts-node": "10.9.2",
|
||||||
"tslib": "^2.3.0",
|
"tslib": "2.8.1",
|
||||||
"typescript": "~5.7.2",
|
"typescript": "5.8.3",
|
||||||
"typescript-eslint": "^8.19.0"
|
"typescript-eslint": "8.33.1"
|
||||||
},
|
},
|
||||||
"nx": {
|
"nx": {
|
||||||
"includedScripts": []
|
"includedScripts": []
|
||||||
|
|||||||
@ -23,7 +23,7 @@
|
|||||||
"input": "public"
|
"input": "public"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"styles": ["./src/styles.scss"],
|
"styles": ["./src/styles.scss", "./src/theme.scss"],
|
||||||
"scripts": []
|
"scripts": []
|
||||||
},
|
},
|
||||||
"configurations": {
|
"configurations": {
|
||||||
@ -86,3 +86,4 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
import { Component, inject } from '@angular/core';
|
import { CommonModule } from '@angular/common';
|
||||||
|
import { Component, OnInit, inject } from '@angular/core';
|
||||||
import { RouterModule } from '@angular/router';
|
import { RouterModule } from '@angular/router';
|
||||||
import { StatsWsService } from '@frontend/shared/stats-ws';
|
import { StatsWsService } from '@frontend/shared/stats-ws';
|
||||||
import { CommonModule } from '@angular/common';
|
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
standalone: true,
|
standalone: true,
|
||||||
@ -22,9 +22,14 @@ import { CommonModule } from '@angular/common';
|
|||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
<router-outlet></router-outlet>
|
<router-outlet></router-outlet>
|
||||||
`
|
`,
|
||||||
})
|
})
|
||||||
export class AppComponent {
|
export class AppComponent implements OnInit {
|
||||||
private readonly service = inject(StatsWsService);
|
private readonly service = inject(StatsWsService);
|
||||||
protected containers = this.service.containers;
|
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 { provideRouter } from '@angular/router';
|
||||||
import { appRoutes } from './app.routes';
|
import { appRoutes } from './app.routes';
|
||||||
import { provideHttpClient } from '@angular/common/http';
|
import { provideHttpClient } from '@angular/common/http';
|
||||||
|
import { ENVIRONMENT } from '@frontend/shared/environment';
|
||||||
|
import { environment } from '../environments/environment';
|
||||||
|
|
||||||
export const appConfig: ApplicationConfig = {
|
export const appConfig: ApplicationConfig = {
|
||||||
providers: [
|
providers: [
|
||||||
provideZoneChangeDetection({ eventCoalescing: true }),
|
provideZoneChangeDetection({ eventCoalescing: true }),
|
||||||
provideHttpClient(),
|
provideHttpClient(),
|
||||||
provideRouter(appRoutes),
|
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,
|
"skipDefaultLibCheck": true,
|
||||||
"baseUrl": ".",
|
"baseUrl": ".",
|
||||||
"paths": {
|
"paths": {
|
||||||
|
"@frontend/shared/environment": ["libs/shared/environment/src/index.ts"],
|
||||||
"@frontend/shared/stats-ws": ["libs/shared/stats-ws/src/index.ts"]
|
"@frontend/shared/stats-ws": ["libs/shared/stats-ws/src/index.ts"]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user