first commit
This commit is contained in:
commit
a656d4ce07
28
backend/.gitignore
vendored
Normal file
28
backend/.gitignore
vendored
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
# dev
|
||||||
|
.yarn/
|
||||||
|
!.yarn/releases
|
||||||
|
.vscode/*
|
||||||
|
!.vscode/launch.json
|
||||||
|
!.vscode/*.code-snippets
|
||||||
|
.idea/workspace.xml
|
||||||
|
.idea/usage.statistics.xml
|
||||||
|
.idea/shelf
|
||||||
|
|
||||||
|
# deps
|
||||||
|
node_modules/
|
||||||
|
|
||||||
|
# env
|
||||||
|
.env
|
||||||
|
.env.production
|
||||||
|
|
||||||
|
# logs
|
||||||
|
logs/
|
||||||
|
*.log
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
pnpm-debug.log*
|
||||||
|
lerna-debug.log*
|
||||||
|
|
||||||
|
# misc
|
||||||
|
.DS_Store
|
||||||
20
backend/Dockerfile
Normal file
20
backend/Dockerfile
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
FROM node:22-alpine
|
||||||
|
|
||||||
|
RUN addgroup -S appgroup && adduser -S appuser -G appgroup
|
||||||
|
|
||||||
|
WORKDIR /usr/src/app
|
||||||
|
|
||||||
|
COPY package.json ./
|
||||||
|
COPY package-lock.json ./
|
||||||
|
|
||||||
|
RUN npm install --production
|
||||||
|
|
||||||
|
COPY src ./src
|
||||||
|
|
||||||
|
RUN chown -R appuser:appgroup /usr/src/app
|
||||||
|
|
||||||
|
USER appuser
|
||||||
|
|
||||||
|
EXPOSE 3000
|
||||||
|
|
||||||
|
CMD ["node", "src/index.js"]
|
||||||
8
backend/README.md
Normal file
8
backend/README.md
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
```
|
||||||
|
npm install
|
||||||
|
npm run dev
|
||||||
|
```
|
||||||
|
|
||||||
|
```
|
||||||
|
open http://localhost:3000
|
||||||
|
```
|
||||||
1369
backend/package-lock.json
generated
Normal file
1369
backend/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
21
backend/package.json
Normal file
21
backend/package.json
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
{
|
||||||
|
"name": "backend",
|
||||||
|
"type": "module",
|
||||||
|
"scripts": {
|
||||||
|
"dev": "tsx watch src/index.ts",
|
||||||
|
"build": "tsc",
|
||||||
|
"start": "node dist/index.js"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@hono/node-server": "^1.14.3",
|
||||||
|
"@hono/node-ws": "^1.1.5",
|
||||||
|
"dockerode": "^4.0.6",
|
||||||
|
"hono": "^4.7.11"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/dockerode": "^3.3.39",
|
||||||
|
"@types/node": "^20.11.17",
|
||||||
|
"tsx": "^4.7.1",
|
||||||
|
"typescript": "^5.8.3"
|
||||||
|
}
|
||||||
|
}
|
||||||
5
backend/src/docker/client.ts
Normal file
5
backend/src/docker/client.ts
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
import Docker from 'dockerode';
|
||||||
|
|
||||||
|
export function createDockerClient() {
|
||||||
|
return new Docker();
|
||||||
|
}
|
||||||
144
backend/src/docker/containers.ts
Normal file
144
backend/src/docker/containers.ts
Normal file
@ -0,0 +1,144 @@
|
|||||||
|
import { createDockerClient } from './client.js';
|
||||||
|
import type { DockerContainer, Networks } from './types.js';
|
||||||
|
|
||||||
|
const docker = createDockerClient();
|
||||||
|
|
||||||
|
export async function listContainers(): Promise<DockerContainer[]> {
|
||||||
|
try {
|
||||||
|
const containers = await docker.listContainers({ all: true });
|
||||||
|
return containers.map((c: any) => ({
|
||||||
|
id: c.Id,
|
||||||
|
names: c.Names,
|
||||||
|
image: c.Image,
|
||||||
|
state: c.State,
|
||||||
|
status: c.Status,
|
||||||
|
created: new Date(c.Created * 1000).toISOString(),
|
||||||
|
ports: c.Ports.map((p: any) => ({
|
||||||
|
privatePort: p.PrivatePort,
|
||||||
|
publicPort: p.PublicPort,
|
||||||
|
type: p.Type,
|
||||||
|
ip: p.IP,
|
||||||
|
})),
|
||||||
|
}));
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error listing containers:', error);
|
||||||
|
throw new Error('Failed to list Docker containers');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getContainerStats(containerId: string) {
|
||||||
|
const container = docker.getContainer(containerId);
|
||||||
|
const stats = await container.stats({ stream: false });
|
||||||
|
const prevCpu = stats.precpu_stats.cpu_usage.total_usage;
|
||||||
|
const prevSys = stats.precpu_stats.system_cpu_usage;
|
||||||
|
const currCpu = stats.cpu_stats.cpu_usage.total_usage;
|
||||||
|
const currSys = stats.cpu_stats.system_cpu_usage;
|
||||||
|
const cpuDelta = currCpu - prevCpu;
|
||||||
|
const sysDelta = currSys - prevSys;
|
||||||
|
const cpuPercent =
|
||||||
|
sysDelta > 0
|
||||||
|
? (cpuDelta / sysDelta) * stats.cpu_stats.cpu_usage.percpu_usage.length * 100.0
|
||||||
|
: 0.0;
|
||||||
|
const memUsage = stats.memory_stats.usage;
|
||||||
|
const memLimit = stats.memory_stats.limit;
|
||||||
|
const memPercent = memLimit > 0 ? (memUsage / memLimit) * 100.0 : 0.0;
|
||||||
|
let netRx = 0,
|
||||||
|
netTx = 0;
|
||||||
|
if (stats.networks) {
|
||||||
|
const networks: Networks = stats.networks;
|
||||||
|
for (const iface of Object.values(networks)) {
|
||||||
|
netRx += iface.rx_bytes || 0;
|
||||||
|
netTx += iface.tx_bytes || 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let blkRead = 0,
|
||||||
|
blkWrite = 0;
|
||||||
|
if (
|
||||||
|
stats.blkio_stats &&
|
||||||
|
Array.isArray(stats.blkio_stats.io_service_bytes_recursive)
|
||||||
|
) {
|
||||||
|
for (const entry of stats.blkio_stats.io_service_bytes_recursive) {
|
||||||
|
if (entry.op === 'Read') blkRead += entry.value;
|
||||||
|
if (entry.op === 'Write') blkWrite += entry.value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
id: containerId,
|
||||||
|
cpuPercent,
|
||||||
|
memUsage,
|
||||||
|
memLimit,
|
||||||
|
memPercent,
|
||||||
|
netRx,
|
||||||
|
netTx,
|
||||||
|
blkRead,
|
||||||
|
blkWrite,
|
||||||
|
timestamp: Date.now(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function streamContainerStats(
|
||||||
|
containerId: string,
|
||||||
|
callback: (stats: any) => void
|
||||||
|
): Promise<() => void> {
|
||||||
|
const container = docker.getContainer(containerId);
|
||||||
|
try {
|
||||||
|
const stream = (await container.stats({ stream: true })) as import('stream').Readable;
|
||||||
|
stream.on('data', (chunk: Buffer) => {
|
||||||
|
const stats = JSON.parse(chunk.toString());
|
||||||
|
const prevCpu = stats.precpu_stats.cpu_usage.total_usage;
|
||||||
|
const prevSys = stats.precpu_stats.system_cpu_usage;
|
||||||
|
const currCpu = stats.cpu_stats.cpu_usage.total_usage;
|
||||||
|
const currSys = stats.cpu_stats.system_cpu_usage;
|
||||||
|
const cpuDelta = currCpu - prevCpu;
|
||||||
|
const sysDelta = currSys - prevSys;
|
||||||
|
const cpuPercent =
|
||||||
|
sysDelta > 0
|
||||||
|
? (cpuDelta / sysDelta) * stats.cpu_stats.cpu_usage.percpu_usage.length * 100.0
|
||||||
|
: 0.0;
|
||||||
|
const memUsage = stats.memory_stats.usage;
|
||||||
|
const memLimit = stats.memory_stats.limit;
|
||||||
|
const memPercent = memLimit > 0 ? (memUsage / memLimit) * 100.0 : 0.0;
|
||||||
|
let netRx = 0,
|
||||||
|
netTx = 0;
|
||||||
|
if (stats.networks) {
|
||||||
|
const networks: Networks = stats.networks;
|
||||||
|
for (const iface of Object.values(networks)) {
|
||||||
|
netRx += iface.rx_bytes || 0;
|
||||||
|
netTx += iface.tx_bytes || 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let blkRead = 0,
|
||||||
|
blkWrite = 0;
|
||||||
|
if (
|
||||||
|
stats.blkio_stats &&
|
||||||
|
Array.isArray(stats.blkio_stats.io_service_bytes_recursive)
|
||||||
|
) {
|
||||||
|
for (const entry of stats.blkio_stats.io_service_bytes_recursive) {
|
||||||
|
if (entry.op === 'Read') blkRead += entry.value;
|
||||||
|
if (entry.op === 'Write') blkWrite += entry.value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
callback({
|
||||||
|
id: containerId,
|
||||||
|
cpuPercent,
|
||||||
|
memUsage,
|
||||||
|
memLimit,
|
||||||
|
memPercent,
|
||||||
|
netRx,
|
||||||
|
netTx,
|
||||||
|
blkRead,
|
||||||
|
blkWrite,
|
||||||
|
timestamp: Date.now(),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
stream.on('error', (err: Error) => {
|
||||||
|
console.error(`Error streaming stats for container ${containerId}:`, err);
|
||||||
|
});
|
||||||
|
return () => {
|
||||||
|
stream.destroy();
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`Error getting stats stream for container ${containerId}:`, error);
|
||||||
|
throw new Error(`Failed to get stats stream for container ${containerId}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
33
backend/src/docker/types.ts
Normal file
33
backend/src/docker/types.ts
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
// Docker types and interfaces
|
||||||
|
|
||||||
|
export type DockerContainer = {
|
||||||
|
id: string;
|
||||||
|
names: string[];
|
||||||
|
image: string;
|
||||||
|
state: string;
|
||||||
|
status: string;
|
||||||
|
created: string;
|
||||||
|
ports: DockerContainerPort[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export type DockerContainerPort = {
|
||||||
|
privatePort: number;
|
||||||
|
publicPort: number | null;
|
||||||
|
type: string;
|
||||||
|
ip: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type NetworkInterface = {
|
||||||
|
rx_bytes: number;
|
||||||
|
rx_packets: number;
|
||||||
|
rx_errors: number;
|
||||||
|
rx_dropped: number;
|
||||||
|
tx_bytes: number;
|
||||||
|
tx_packets: number;
|
||||||
|
tx_errors: number;
|
||||||
|
tx_dropped: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type Networks = {
|
||||||
|
[interfaceName: string]: NetworkInterface;
|
||||||
|
}
|
||||||
112
backend/src/index.ts
Normal file
112
backend/src/index.ts
Normal file
@ -0,0 +1,112 @@
|
|||||||
|
import { serve } from '@hono/node-server'
|
||||||
|
import { Hono } from 'hono'
|
||||||
|
import { createNodeWebSocket } from '@hono/node-ws'
|
||||||
|
import {
|
||||||
|
registerContainerClient,
|
||||||
|
registerStatsClient,
|
||||||
|
registerCombinedClient,
|
||||||
|
registerSingleContainerClient,
|
||||||
|
removeContainerClient,
|
||||||
|
removeStatsClient,
|
||||||
|
removeCombinedClient,
|
||||||
|
removeSingleContainerClient,
|
||||||
|
stopAllStreams
|
||||||
|
} from './ws/manager.js';
|
||||||
|
|
||||||
|
const app = new Hono();
|
||||||
|
|
||||||
|
const { injectWebSocket, upgradeWebSocket } = createNodeWebSocket({ app });
|
||||||
|
|
||||||
|
app.get('/', (c) => c.text('WS Docker Stats server (ESM) is running.'))
|
||||||
|
|
||||||
|
|
||||||
|
// Containers-only endpoint
|
||||||
|
app.get('/ws/containers', upgradeWebSocket(c => ({
|
||||||
|
onOpen(_, ws) {
|
||||||
|
console.log('WebSocket containers connection opened');
|
||||||
|
registerContainerClient(ws);
|
||||||
|
},
|
||||||
|
onClose(_, ws) {
|
||||||
|
console.log('❌ WS containers client disconnected');
|
||||||
|
removeContainerClient(ws);
|
||||||
|
},
|
||||||
|
onError(err) {
|
||||||
|
console.error('❌ WS containers error:', err);
|
||||||
|
}
|
||||||
|
})))
|
||||||
|
|
||||||
|
// Stats-only endpoint
|
||||||
|
app.get('/ws/stats', upgradeWebSocket(c => ({
|
||||||
|
onOpen(_, ws) {
|
||||||
|
console.log('WebSocket stats connection opened');
|
||||||
|
registerStatsClient(ws);
|
||||||
|
},
|
||||||
|
onClose(_, ws) {
|
||||||
|
console.log('❌ WS stats client disconnected');
|
||||||
|
removeStatsClient(ws);
|
||||||
|
},
|
||||||
|
onError(err) {
|
||||||
|
console.error('❌ WS stats error:', err);
|
||||||
|
}
|
||||||
|
})))
|
||||||
|
|
||||||
|
// Combined endpoint (explicit)
|
||||||
|
app.get('/ws/combined', upgradeWebSocket(c => ({
|
||||||
|
onOpen(_, ws) {
|
||||||
|
console.log('WebSocket combined connection opened');
|
||||||
|
registerCombinedClient(ws);
|
||||||
|
},
|
||||||
|
onClose(_, ws) {
|
||||||
|
console.log('❌ WS combined client disconnected');
|
||||||
|
removeCombinedClient(ws);
|
||||||
|
},
|
||||||
|
onError(err) {
|
||||||
|
console.error('❌ WS combined error:', err);
|
||||||
|
}
|
||||||
|
})))
|
||||||
|
|
||||||
|
// Single container endpoint
|
||||||
|
app.get('/ws/container', upgradeWebSocket(c => {
|
||||||
|
const containerId = c.req.query('id');
|
||||||
|
if (!containerId) {
|
||||||
|
throw new Error('Container ID is required');
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
onOpen(_, ws) {
|
||||||
|
console.log(`WebSocket connection opened for container ${containerId}`);
|
||||||
|
registerSingleContainerClient(ws, containerId);
|
||||||
|
},
|
||||||
|
onClose(_, ws) {
|
||||||
|
console.log(`❌ WS client for container ${containerId} disconnected`);
|
||||||
|
removeSingleContainerClient(ws, containerId);
|
||||||
|
},
|
||||||
|
onError(err) {
|
||||||
|
console.error(`❌ WS error for container ${containerId}:`, err);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}))
|
||||||
|
|
||||||
|
const server = serve({
|
||||||
|
fetch: app.fetch,
|
||||||
|
port: 3000
|
||||||
|
}, (info) => {
|
||||||
|
console.log(`Server is running on http://localhost:${info.port}`)
|
||||||
|
})
|
||||||
|
|
||||||
|
injectWebSocket(server);
|
||||||
|
|
||||||
|
process.on('SIGTERM', () => cleanupAndExit());
|
||||||
|
process.on('SIGINT', () => cleanupAndExit());
|
||||||
|
|
||||||
|
function cleanupAndExit() {
|
||||||
|
console.log('Shutting down gracefully...');
|
||||||
|
stopAllStreams();
|
||||||
|
server.close(() => {
|
||||||
|
console.log('Server closed');
|
||||||
|
process.exit(0);
|
||||||
|
});
|
||||||
|
setTimeout(() => {
|
||||||
|
console.log('Forcing exit after timeout');
|
||||||
|
process.exit(1);
|
||||||
|
}, 5000);
|
||||||
|
}
|
||||||
55
backend/src/types.ts
Normal file
55
backend/src/types.ts
Normal file
@ -0,0 +1,55 @@
|
|||||||
|
export type DockerContainer = {
|
||||||
|
id: string;
|
||||||
|
names: string[];
|
||||||
|
image: string;
|
||||||
|
state: string;
|
||||||
|
status: string;
|
||||||
|
created: string;
|
||||||
|
ports: DockerContainerPort[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export type DockerContainerPort = {
|
||||||
|
privatePort: number;
|
||||||
|
publicPort: number | null;
|
||||||
|
type: string;
|
||||||
|
ip: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type NetworkInterface = {
|
||||||
|
rx_bytes: number;
|
||||||
|
rx_packets: number;
|
||||||
|
rx_errors: number;
|
||||||
|
rx_dropped: number;
|
||||||
|
tx_bytes: number;
|
||||||
|
tx_packets: number;
|
||||||
|
tx_errors: number;
|
||||||
|
tx_dropped: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type Networks = {
|
||||||
|
[interfaceName: string]: NetworkInterface;
|
||||||
|
}
|
||||||
|
|
||||||
|
// New types for WebSocket responses
|
||||||
|
export type ContainerInfo = {
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type CombinedContainerData = ContainerInfo & ContainerStats;
|
||||||
16
backend/src/utils/client.ts
Normal file
16
backend/src/utils/client.ts
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
import Docker from 'dockerode';
|
||||||
|
|
||||||
|
function createDockerClient() {
|
||||||
|
const useSocket = process.env.DOCKER_USE_SOCKET === 'true';
|
||||||
|
|
||||||
|
if (useSocket) {
|
||||||
|
const socketPath = process.env.DOCKER_SOCKET_PATH || '/var/run/docker.sock';
|
||||||
|
return new Docker({ socketPath });
|
||||||
|
} else {
|
||||||
|
const host = process.env.DOCKER_HOST || '127.0.0.1';
|
||||||
|
const port = parseInt(process.env.DOCKER_PORT || '2375', 10);
|
||||||
|
return new Docker({ host, port });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default createDockerClient;
|
||||||
166
backend/src/ws/manager.ts
Normal file
166
backend/src/ws/manager.ts
Normal file
@ -0,0 +1,166 @@
|
|||||||
|
import type { WSContext } from 'hono/ws';
|
||||||
|
import { listContainers, streamContainerStats } from '../docker/containers.js';
|
||||||
|
import { sendContainerInfo, sendStats, sendCombined, sendToSingleContainer } from './sender.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
|
||||||
|
export function registerContainerClient(ws: WSContext<WebSocket>) {
|
||||||
|
const wasEmpty = allClientsEmpty();
|
||||||
|
containerClients.add(ws);
|
||||||
|
if (wasEmpty) setupStreams();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function registerStatsClient(ws: WSContext<WebSocket>) {
|
||||||
|
const wasEmpty = allClientsEmpty();
|
||||||
|
statsClients.add(ws);
|
||||||
|
if (wasEmpty) setupStreams();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function registerCombinedClient(ws: WSContext<WebSocket>) {
|
||||||
|
const wasEmpty = allClientsEmpty();
|
||||||
|
combinedClients.add(ws);
|
||||||
|
if (wasEmpty) setupStreams();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function registerSingleContainerClient(ws: WSContext<WebSocket>, containerId: string) {
|
||||||
|
if (!singleContainerClients.has(containerId)) {
|
||||||
|
singleContainerClients.set(containerId, new Set());
|
||||||
|
}
|
||||||
|
const clients = singleContainerClients.get(containerId)!;
|
||||||
|
clients.add(ws);
|
||||||
|
if (clients.size === 1) {
|
||||||
|
setupStreamForContainer(containerId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Client removal functions
|
||||||
|
export function removeContainerClient(ws: WSContext<WebSocket>) {
|
||||||
|
containerClients.delete(ws);
|
||||||
|
checkAndCleanup();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function removeStatsClient(ws: WSContext<WebSocket>) {
|
||||||
|
statsClients.delete(ws);
|
||||||
|
checkAndCleanup();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function removeCombinedClient(ws: WSContext<WebSocket>) {
|
||||||
|
combinedClients.delete(ws);
|
||||||
|
checkAndCleanup();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function removeSingleContainerClient(ws: WSContext<WebSocket>, containerId: string) {
|
||||||
|
const clients = singleContainerClients.get(containerId);
|
||||||
|
if (clients) {
|
||||||
|
clients.delete(ws);
|
||||||
|
if (clients.size === 0) {
|
||||||
|
singleContainerClients.delete(containerId);
|
||||||
|
stopStreamForContainer(containerId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function allClientsEmpty() {
|
||||||
|
return containerClients.size === 0 &&
|
||||||
|
statsClients.size === 0 &&
|
||||||
|
combinedClients.size === 0 &&
|
||||||
|
singleContainerClients.size === 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
function checkAndCleanup() {
|
||||||
|
if (allClientsEmpty()) {
|
||||||
|
stopAllStreams();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function setupStreams() {
|
||||||
|
if (allClientsEmpty()) return;
|
||||||
|
try {
|
||||||
|
const containers = await listContainers();
|
||||||
|
for (const container of containers) {
|
||||||
|
if (activeStreams.has(container.id)) continue;
|
||||||
|
setupStreamForContainer(container.id, containers);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error setting up streams:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function setupStreamForContainer(containerId: string, allContainers?: any[]) {
|
||||||
|
if (activeStreams.has(containerId)) return;
|
||||||
|
try {
|
||||||
|
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 stopStream = await streamContainerStats(containerId, async (stats) => {
|
||||||
|
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 = {
|
||||||
|
id: containerData.id,
|
||||||
|
name: containerData.names?.[0]?.replace('/', '') ?? containerData.id.slice(0, 12),
|
||||||
|
image: containerData.image,
|
||||||
|
state: containerData.state,
|
||||||
|
status: containerData.status,
|
||||||
|
};
|
||||||
|
if (containerClients.size > 0) {
|
||||||
|
sendContainerInfo(containerClients, containerInfo);
|
||||||
|
}
|
||||||
|
if (statsClients.size > 0) {
|
||||||
|
sendStats(statsClients, stats);
|
||||||
|
}
|
||||||
|
if (combinedClients.size > 0) {
|
||||||
|
sendCombined(combinedClients, containerInfo, stats);
|
||||||
|
}
|
||||||
|
const singleClients = singleContainerClients.get(containerId);
|
||||||
|
if (singleClients && singleClients.size > 0) {
|
||||||
|
sendToSingleContainer(singleContainerClients, containerId, containerInfo, stats);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
activeStreams.set(containerId, stopStream);
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`Failed to setup stream for container ${containerId}:`, error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function stopStreamForContainer(containerId: string) {
|
||||||
|
const stopStream = activeStreams.get(containerId);
|
||||||
|
if (stopStream) {
|
||||||
|
stopStream();
|
||||||
|
activeStreams.delete(containerId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function stopAllStreams() {
|
||||||
|
for (const stopStream of activeStreams.values()) {
|
||||||
|
stopStream();
|
||||||
|
}
|
||||||
|
activeStreams.clear();
|
||||||
|
}
|
||||||
43
backend/src/ws/sender.ts
Normal file
43
backend/src/ws/sender.ts
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
// Functions for sending data to WebSocket clients
|
||||||
|
|
||||||
|
import type { WSContext } from 'hono/ws';
|
||||||
|
|
||||||
|
export function sendContainerInfo(containerClients: Set<WSContext<WebSocket>>, containerInfo: any) {
|
||||||
|
const payload = JSON.stringify({ containers: [containerInfo] });
|
||||||
|
sendToClientSet(containerClients, payload);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function sendStats(statsClients: Set<WSContext<WebSocket>>, stats: any) {
|
||||||
|
const payload = JSON.stringify({ stats: [stats] });
|
||||||
|
sendToClientSet(statsClients, payload);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function sendCombined(combinedClients: Set<WSContext<WebSocket>>, containerInfo: any, stats: any) {
|
||||||
|
const payload = JSON.stringify({
|
||||||
|
containers: [containerInfo],
|
||||||
|
stats: [stats]
|
||||||
|
});
|
||||||
|
sendToClientSet(combinedClients, payload);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function sendToSingleContainer(singleContainerClients: Map<string, Set<WSContext<WebSocket>>>, containerId: string, containerInfo: any, stats: any) {
|
||||||
|
const combinedData = {
|
||||||
|
...containerInfo,
|
||||||
|
...stats
|
||||||
|
};
|
||||||
|
const payload = JSON.stringify({ container: combinedData });
|
||||||
|
const clients = singleContainerClients.get(containerId);
|
||||||
|
if (clients) {
|
||||||
|
sendToClientSet(clients, payload);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function sendToClientSet(clients: Set<WSContext<WebSocket>>, payload: string) {
|
||||||
|
for (const ws of clients) {
|
||||||
|
if (ws.readyState === 1) {
|
||||||
|
ws.send(payload);
|
||||||
|
} else {
|
||||||
|
clients.delete(ws);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
24
backend/src/ws/types.ts
Normal file
24
backend/src/ws/types.ts
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
// WebSocket types and interfaces
|
||||||
|
|
||||||
|
export type ContainerInfo = {
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type CombinedContainerData = ContainerInfo & ContainerStats;
|
||||||
16
backend/tsconfig.json
Normal file
16
backend/tsconfig.json
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "ESNext",
|
||||||
|
"module": "NodeNext",
|
||||||
|
"strict": true,
|
||||||
|
"verbatimModuleSyntax": true,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"types": [
|
||||||
|
"node"
|
||||||
|
],
|
||||||
|
"jsx": "react-jsx",
|
||||||
|
"jsxImportSource": "hono/jsx",
|
||||||
|
"outDir": "./dist"
|
||||||
|
},
|
||||||
|
"exclude": ["node_modules"]
|
||||||
|
}
|
||||||
13
frontend/.editorconfig
Normal file
13
frontend/.editorconfig
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
# Editor configuration, see http://editorconfig.org
|
||||||
|
root = true
|
||||||
|
|
||||||
|
[*]
|
||||||
|
charset = utf-8
|
||||||
|
indent_style = space
|
||||||
|
indent_size = 2
|
||||||
|
insert_final_newline = true
|
||||||
|
trim_trailing_whitespace = true
|
||||||
|
|
||||||
|
[*.md]
|
||||||
|
max_line_length = off
|
||||||
|
trim_trailing_whitespace = false
|
||||||
46
frontend/.gitignore
vendored
Normal file
46
frontend/.gitignore
vendored
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
# See https://docs.github.com/en/get-started/getting-started-with-git/ignoring-files for more about ignoring files.
|
||||||
|
|
||||||
|
# compiled output
|
||||||
|
dist
|
||||||
|
tmp
|
||||||
|
out-tsc
|
||||||
|
|
||||||
|
# dependencies
|
||||||
|
node_modules
|
||||||
|
|
||||||
|
# IDEs and editors
|
||||||
|
/.idea
|
||||||
|
.project
|
||||||
|
.classpath
|
||||||
|
.c9/
|
||||||
|
*.launch
|
||||||
|
.settings/
|
||||||
|
*.sublime-workspace
|
||||||
|
|
||||||
|
# IDE - VSCode
|
||||||
|
.vscode/*
|
||||||
|
!.vscode/settings.json
|
||||||
|
!.vscode/tasks.json
|
||||||
|
!.vscode/launch.json
|
||||||
|
!.vscode/extensions.json
|
||||||
|
|
||||||
|
# misc
|
||||||
|
/.sass-cache
|
||||||
|
/connect.lock
|
||||||
|
/coverage
|
||||||
|
/libpeerconnection.log
|
||||||
|
npm-debug.log
|
||||||
|
yarn-error.log
|
||||||
|
testem.log
|
||||||
|
/typings
|
||||||
|
|
||||||
|
# System Files
|
||||||
|
.DS_Store
|
||||||
|
Thumbs.db
|
||||||
|
|
||||||
|
.nx/cache
|
||||||
|
.nx/workspace-data
|
||||||
|
.cursor/rules/nx-rules.mdc
|
||||||
|
.github/instructions/nx.instructions.md
|
||||||
|
|
||||||
|
.angular
|
||||||
6
frontend/.prettierignore
Normal file
6
frontend/.prettierignore
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
# Add files here to ignore them from prettier formatting
|
||||||
|
/dist
|
||||||
|
/coverage
|
||||||
|
/.nx/cache
|
||||||
|
/.nx/workspace-data
|
||||||
|
.angular
|
||||||
7
frontend/.prettierrc
Normal file
7
frontend/.prettierrc
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
{
|
||||||
|
"singleQuote": true,
|
||||||
|
"trailingComma": "es5",
|
||||||
|
"useTabs": true,
|
||||||
|
"tabWidth": 2,
|
||||||
|
"endOfLine": "crlf"
|
||||||
|
}
|
||||||
8
frontend/.vscode/extensions.json
vendored
Normal file
8
frontend/.vscode/extensions.json
vendored
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
{
|
||||||
|
"recommendations": [
|
||||||
|
"nrwl.angular-console",
|
||||||
|
"esbenp.prettier-vscode",
|
||||||
|
"dbaeumer.vscode-eslint",
|
||||||
|
"firsttris.vscode-jest-runner"
|
||||||
|
]
|
||||||
|
}
|
||||||
36
frontend/.vscode/settings.json
vendored
Normal file
36
frontend/.vscode/settings.json
vendored
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
{
|
||||||
|
"files.eol": "\n",
|
||||||
|
"editor.tabSize": 2,
|
||||||
|
"editor.codeActionsOnSave": {
|
||||||
|
"source.fixAll.eslint": "explicit",
|
||||||
|
"source.organizeImports": "explicit",
|
||||||
|
"source.sortMembers": "explicit"
|
||||||
|
},
|
||||||
|
"editor.formatOnSave": true,
|
||||||
|
"[typescript]": {
|
||||||
|
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
||||||
|
},
|
||||||
|
"[html]": {
|
||||||
|
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
||||||
|
},
|
||||||
|
"[javascript]": {
|
||||||
|
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
||||||
|
},
|
||||||
|
"[javascriptreact]": {
|
||||||
|
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
||||||
|
},
|
||||||
|
"[typescriptreact]": {
|
||||||
|
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
||||||
|
},
|
||||||
|
"eslint.options": {
|
||||||
|
"extensions": [".js", ".ts", ".html"]
|
||||||
|
},
|
||||||
|
"eslint.validate": ["javascript", "typescript", "html"],
|
||||||
|
"angular.enable-strict-mode-prompt": false,
|
||||||
|
"search.exclude": {
|
||||||
|
"": true,
|
||||||
|
"**/node_modules": true
|
||||||
|
},
|
||||||
|
"search.useIgnoreFiles": false,
|
||||||
|
"nxConsole.generateAiAgentRules": true
|
||||||
|
}
|
||||||
82
frontend/README.md
Normal file
82
frontend/README.md
Normal file
@ -0,0 +1,82 @@
|
|||||||
|
# Frontend
|
||||||
|
|
||||||
|
<a alt="Nx logo" href="https://nx.dev" target="_blank" rel="noreferrer"><img src="https://raw.githubusercontent.com/nrwl/nx/master/images/nx-logo.png" width="45"></a>
|
||||||
|
|
||||||
|
✨ Your new, shiny [Nx workspace](https://nx.dev) is almost ready ✨.
|
||||||
|
|
||||||
|
[Learn more about this workspace setup and its capabilities](https://nx.dev/getting-started/tutorials/angular-standalone-tutorial?utm_source=nx_project&utm_medium=readme&utm_campaign=nx_projects) or run `npx nx graph` to visually explore what was created. Now, let's get you up to speed!
|
||||||
|
|
||||||
|
## Finish your remote caching setup
|
||||||
|
|
||||||
|
[Click here to finish setting up your workspace!](https://cloud.nx.app/connect/1cJ2WvnztZ)
|
||||||
|
|
||||||
|
|
||||||
|
## Run tasks
|
||||||
|
|
||||||
|
To run the dev server for your app, use:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
npx nx serve frontend
|
||||||
|
```
|
||||||
|
|
||||||
|
To create a production bundle:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
npx nx build frontend
|
||||||
|
```
|
||||||
|
|
||||||
|
To see all available targets to run for a project, run:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
npx nx show project frontend
|
||||||
|
```
|
||||||
|
|
||||||
|
These targets are either [inferred automatically](https://nx.dev/concepts/inferred-tasks?utm_source=nx_project&utm_medium=readme&utm_campaign=nx_projects) or defined in the `project.json` or `package.json` files.
|
||||||
|
|
||||||
|
[More about running tasks in the docs »](https://nx.dev/features/run-tasks?utm_source=nx_project&utm_medium=readme&utm_campaign=nx_projects)
|
||||||
|
|
||||||
|
## Add new projects
|
||||||
|
|
||||||
|
While you could add new projects to your workspace manually, you might want to leverage [Nx plugins](https://nx.dev/concepts/nx-plugins?utm_source=nx_project&utm_medium=readme&utm_campaign=nx_projects) and their [code generation](https://nx.dev/features/generate-code?utm_source=nx_project&utm_medium=readme&utm_campaign=nx_projects) feature.
|
||||||
|
|
||||||
|
Use the plugin's generator to create new projects.
|
||||||
|
|
||||||
|
To generate a new application, use:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
npx nx g @nx/angular:app demo
|
||||||
|
```
|
||||||
|
|
||||||
|
To generate a new library, use:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
npx nx g @nx/angular:lib mylib
|
||||||
|
```
|
||||||
|
|
||||||
|
You can use `npx nx list` to get a list of installed plugins. Then, run `npx nx list <plugin-name>` to learn about more specific capabilities of a particular plugin. Alternatively, [install Nx Console](https://nx.dev/getting-started/editor-setup?utm_source=nx_project&utm_medium=readme&utm_campaign=nx_projects) to browse plugins and generators in your IDE.
|
||||||
|
|
||||||
|
[Learn more about Nx plugins »](https://nx.dev/concepts/nx-plugins?utm_source=nx_project&utm_medium=readme&utm_campaign=nx_projects) | [Browse the plugin registry »](https://nx.dev/plugin-registry?utm_source=nx_project&utm_medium=readme&utm_campaign=nx_projects)
|
||||||
|
|
||||||
|
|
||||||
|
[Learn more about Nx on CI](https://nx.dev/ci/intro/ci-with-nx#ready-get-started-with-your-provider?utm_source=nx_project&utm_medium=readme&utm_campaign=nx_projects)
|
||||||
|
|
||||||
|
## Install Nx Console
|
||||||
|
|
||||||
|
Nx Console is an editor extension that enriches your developer experience. It lets you run tasks, generate code, and improves code autocompletion in your IDE. It is available for VSCode and IntelliJ.
|
||||||
|
|
||||||
|
[Install Nx Console »](https://nx.dev/getting-started/editor-setup?utm_source=nx_project&utm_medium=readme&utm_campaign=nx_projects)
|
||||||
|
|
||||||
|
## Useful links
|
||||||
|
|
||||||
|
Learn more:
|
||||||
|
|
||||||
|
- [Learn more about this workspace setup](https://nx.dev/getting-started/tutorials/angular-standalone-tutorial?utm_source=nx_project&utm_medium=readme&utm_campaign=nx_projects)
|
||||||
|
- [Learn about Nx on CI](https://nx.dev/ci/intro/ci-with-nx?utm_source=nx_project&utm_medium=readme&utm_campaign=nx_projects)
|
||||||
|
- [Releasing Packages with Nx release](https://nx.dev/features/manage-releases?utm_source=nx_project&utm_medium=readme&utm_campaign=nx_projects)
|
||||||
|
- [What are Nx plugins?](https://nx.dev/concepts/nx-plugins?utm_source=nx_project&utm_medium=readme&utm_campaign=nx_projects)
|
||||||
|
|
||||||
|
And join the Nx community:
|
||||||
|
- [Discord](https://go.nx.dev/community)
|
||||||
|
- [Follow us on X](https://twitter.com/nxdevtools) or [LinkedIn](https://www.linkedin.com/company/nrwl)
|
||||||
|
- [Our Youtube channel](https://www.youtube.com/@nxdevtools)
|
||||||
|
- [Our blog](https://nx.dev/blog?utm_source=nx_project&utm_medium=readme&utm_campaign=nx_projects)
|
||||||
42
frontend/eslint.base.config.mjs
Normal file
42
frontend/eslint.base.config.mjs
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
import nx from '@nx/eslint-plugin';
|
||||||
|
|
||||||
|
export default [
|
||||||
|
...nx.configs['flat/base'],
|
||||||
|
...nx.configs['flat/typescript'],
|
||||||
|
...nx.configs['flat/javascript'],
|
||||||
|
{
|
||||||
|
ignores: ['**/dist'],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
files: ['**/*.ts', '**/*.tsx', '**/*.js', '**/*.jsx'],
|
||||||
|
rules: {
|
||||||
|
'@nx/enforce-module-boundaries': [
|
||||||
|
'error',
|
||||||
|
{
|
||||||
|
enforceBuildableLibDependency: true,
|
||||||
|
allow: ['^.*/eslint(\\.base)?\\.config\\.[cm]?js$'],
|
||||||
|
depConstraints: [
|
||||||
|
{
|
||||||
|
sourceTag: '*',
|
||||||
|
onlyDependOnLibsWithTags: ['*'],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
files: [
|
||||||
|
'**/*.ts',
|
||||||
|
'**/*.tsx',
|
||||||
|
'**/*.cts',
|
||||||
|
'**/*.mts',
|
||||||
|
'**/*.js',
|
||||||
|
'**/*.jsx',
|
||||||
|
'**/*.cjs',
|
||||||
|
'**/*.mjs',
|
||||||
|
],
|
||||||
|
// Override or add rules here
|
||||||
|
rules: {},
|
||||||
|
},
|
||||||
|
];
|
||||||
71
frontend/eslint.config.mjs
Normal file
71
frontend/eslint.config.mjs
Normal file
@ -0,0 +1,71 @@
|
|||||||
|
import baseConfig from './eslint.base.config.mjs';
|
||||||
|
import { FlatCompat } from '@eslint/eslintrc';
|
||||||
|
import js from '@eslint/js';
|
||||||
|
import nxEslintPlugin from '@nx/eslint-plugin';
|
||||||
|
import { dirname } from 'path';
|
||||||
|
import { fileURLToPath } from 'url';
|
||||||
|
const compat = new FlatCompat({
|
||||||
|
baseDirectory: dirname(fileURLToPath(import.meta.url)),
|
||||||
|
recommendedConfig: js.configs.recommended,
|
||||||
|
});
|
||||||
|
|
||||||
|
export default [
|
||||||
|
...baseConfig,
|
||||||
|
{
|
||||||
|
ignores: ['**/dist'],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
files: ['**/*.ts', '**/*.tsx', '**/*.js', '**/*.jsx'],
|
||||||
|
rules: {
|
||||||
|
'@angular-eslint/no-input-rename': 'off',
|
||||||
|
'@nx/enforce-module-boundaries': [
|
||||||
|
'error',
|
||||||
|
{
|
||||||
|
enforceBuildableLibDependency: true,
|
||||||
|
allow: [],
|
||||||
|
depConstraints: [
|
||||||
|
{
|
||||||
|
sourceTag: '*',
|
||||||
|
onlyDependOnLibsWithTags: ['*'],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
...compat
|
||||||
|
.config({
|
||||||
|
extends: ['plugin:@nx/typescript'],
|
||||||
|
})
|
||||||
|
.map((config) => ({
|
||||||
|
...config,
|
||||||
|
files: ['**/*.ts', '**/*.tsx', '**/*.cts', '**/*.mts'],
|
||||||
|
rules: {
|
||||||
|
...config.rules,
|
||||||
|
},
|
||||||
|
})),
|
||||||
|
...compat
|
||||||
|
.config({
|
||||||
|
extends: ['plugin:@nx/javascript'],
|
||||||
|
})
|
||||||
|
.map((config) => ({
|
||||||
|
...config,
|
||||||
|
files: ['**/*.js', '**/*.jsx', '**/*.cjs', '**/*.mjs'],
|
||||||
|
rules: {
|
||||||
|
...config.rules,
|
||||||
|
},
|
||||||
|
})),
|
||||||
|
...compat
|
||||||
|
.config({
|
||||||
|
env: {
|
||||||
|
jest: true,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
.map((config) => ({
|
||||||
|
...config,
|
||||||
|
files: ['**/*.spec.ts', '**/*.spec.tsx', '**/*.spec.js', '**/*.spec.jsx'],
|
||||||
|
rules: {
|
||||||
|
...config.rules,
|
||||||
|
},
|
||||||
|
})),
|
||||||
|
];
|
||||||
6
frontend/jest.config.ts
Normal file
6
frontend/jest.config.ts
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
import type { Config } from 'jest';
|
||||||
|
import { getJestProjectsAsync } from '@nx/jest';
|
||||||
|
|
||||||
|
export default async (): Promise<Config> => ({
|
||||||
|
projects: await getJestProjectsAsync(),
|
||||||
|
});
|
||||||
3
frontend/jest.preset.js
Normal file
3
frontend/jest.preset.js
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
const nxPreset = require('@nx/jest/preset').default;
|
||||||
|
|
||||||
|
module.exports = { ...nxPreset };
|
||||||
7
frontend/libs/shared/stats-ws/README.md
Normal file
7
frontend/libs/shared/stats-ws/README.md
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
# stats-ws
|
||||||
|
|
||||||
|
This library was generated with [Nx](https://nx.dev).
|
||||||
|
|
||||||
|
## Running unit tests
|
||||||
|
|
||||||
|
Run `nx test stats-ws` to execute the unit tests.
|
||||||
34
frontend/libs/shared/stats-ws/eslint.config.mjs
Normal file
34
frontend/libs/shared/stats-ws/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: 'frontend',
|
||||||
|
style: 'camelCase',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
'@angular-eslint/component-selector': [
|
||||||
|
'error',
|
||||||
|
{
|
||||||
|
type: 'element',
|
||||||
|
prefix: 'frontend',
|
||||||
|
style: 'kebab-case',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
files: ['**/*.html'],
|
||||||
|
// Override or add rules here
|
||||||
|
rules: {},
|
||||||
|
},
|
||||||
|
];
|
||||||
21
frontend/libs/shared/stats-ws/jest.config.ts
Normal file
21
frontend/libs/shared/stats-ws/jest.config.ts
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
export default {
|
||||||
|
displayName: 'stats-ws',
|
||||||
|
preset: '../../../jest.preset.js',
|
||||||
|
setupFilesAfterEnv: ['<rootDir>/src/test-setup.ts'],
|
||||||
|
coverageDirectory: '../../../coverage/libs/shared/stats-ws',
|
||||||
|
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/stats-ws/project.json
Normal file
20
frontend/libs/shared/stats-ws/project.json
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
{
|
||||||
|
"name": "stats-ws",
|
||||||
|
"$schema": "../../../node_modules/nx/schemas/project-schema.json",
|
||||||
|
"sourceRoot": "libs/shared/stats-ws/src",
|
||||||
|
"prefix": "frontend",
|
||||||
|
"projectType": "library",
|
||||||
|
"tags": [],
|
||||||
|
"targets": {
|
||||||
|
"test": {
|
||||||
|
"executor": "@nx/jest:jest",
|
||||||
|
"outputs": ["{workspaceRoot}/coverage/{projectRoot}"],
|
||||||
|
"options": {
|
||||||
|
"jestConfig": "libs/shared/stats-ws/jest.config.ts"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"lint": {
|
||||||
|
"executor": "@nx/eslint:lint"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
2
frontend/libs/shared/stats-ws/src/index.ts
Normal file
2
frontend/libs/shared/stats-ws/src/index.ts
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
export * from './lib/containers';
|
||||||
|
export * from './lib/types';
|
||||||
36
frontend/libs/shared/stats-ws/src/lib/containers.ts
Normal file
36
frontend/libs/shared/stats-ws/src/lib/containers.ts
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
import { computed, inject, Injectable, signal } from "@angular/core";
|
||||||
|
import { HttpClient } from "@angular/common/http";
|
||||||
|
import { ContainerInfo, PayloadContainers } from "./types";
|
||||||
|
|
||||||
|
@Injectable({
|
||||||
|
providedIn: "root",
|
||||||
|
})
|
||||||
|
export class StatsWsService {
|
||||||
|
private _containers = signal<ContainerInfo[]>([]);
|
||||||
|
|
||||||
|
private socket?: WebSocket;
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
this.initWebSocket();
|
||||||
|
}
|
||||||
|
|
||||||
|
readonly containers = computed(() => this._containers());
|
||||||
|
|
||||||
|
private initWebSocket() {
|
||||||
|
this.socket = new WebSocket("ws://localhost:3000/ws/containers")
|
||||||
|
|
||||||
|
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.socket.onclose = () => {
|
||||||
|
console.warn("WebSocket connection closed. Attempting to reconnect...");
|
||||||
|
setTimeout(() => this.initWebSocket(), 2000);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
};
|
||||||
24
frontend/libs/shared/stats-ws/src/lib/types.ts
Normal file
24
frontend/libs/shared/stats-ws/src/lib/types.ts
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
export type ContainerInfo = {
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
export type PayloadContainers = {
|
||||||
|
containers: ContainerInfo[];
|
||||||
|
}
|
||||||
28
frontend/libs/shared/stats-ws/tsconfig.json
Normal file
28
frontend/libs/shared/stats-ws/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/stats-ws/tsconfig.lib.json
Normal file
17
frontend/libs/shared/stats-ws/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/stats-ws/tsconfig.spec.json
Normal file
16
frontend/libs/shared/stats-ws/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"
|
||||||
|
]
|
||||||
|
}
|
||||||
64
frontend/nx.json
Normal file
64
frontend/nx.json
Normal file
@ -0,0 +1,64 @@
|
|||||||
|
{
|
||||||
|
"$schema": "./node_modules/nx/schemas/nx-schema.json",
|
||||||
|
"defaultBase": "master",
|
||||||
|
"namedInputs": {
|
||||||
|
"default": ["{projectRoot}/**/*", "sharedGlobals"],
|
||||||
|
"production": [
|
||||||
|
"default",
|
||||||
|
"!{projectRoot}/.eslintrc.json",
|
||||||
|
"!{projectRoot}/eslint.config.mjs",
|
||||||
|
"!{projectRoot}/**/?(*.)+(spec|test).[jt]s?(x)?(.snap)",
|
||||||
|
"!{projectRoot}/tsconfig.spec.json",
|
||||||
|
"!{projectRoot}/jest.config.[jt]s",
|
||||||
|
"!{projectRoot}/src/test-setup.[jt]s",
|
||||||
|
"!{projectRoot}/test-setup.[jt]s"
|
||||||
|
],
|
||||||
|
"sharedGlobals": []
|
||||||
|
},
|
||||||
|
"nxCloudId": "683c630209e9a67b1f601ce8",
|
||||||
|
"targetDefaults": {
|
||||||
|
"@angular-devkit/build-angular:application": {
|
||||||
|
"cache": true,
|
||||||
|
"dependsOn": ["^build"],
|
||||||
|
"inputs": ["production", "^production"]
|
||||||
|
},
|
||||||
|
"@nx/eslint:lint": {
|
||||||
|
"cache": true,
|
||||||
|
"inputs": [
|
||||||
|
"default",
|
||||||
|
"{workspaceRoot}/.eslintrc.json",
|
||||||
|
"{workspaceRoot}/.eslintignore",
|
||||||
|
"{workspaceRoot}/eslint.config.mjs"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"@nx/jest:jest": {
|
||||||
|
"cache": true,
|
||||||
|
"inputs": ["default", "^production", "{workspaceRoot}/jest.preset.js"],
|
||||||
|
"options": {
|
||||||
|
"passWithNoTests": true
|
||||||
|
},
|
||||||
|
"configurations": {
|
||||||
|
"ci": {
|
||||||
|
"ci": true,
|
||||||
|
"codeCoverage": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"generators": {
|
||||||
|
"@nx/angular:application": {
|
||||||
|
"e2eTestRunner": "none",
|
||||||
|
"linter": "eslint",
|
||||||
|
"style": "scss",
|
||||||
|
"unitTestRunner": "none"
|
||||||
|
},
|
||||||
|
"@nx/angular:library": {
|
||||||
|
"linter": "eslint",
|
||||||
|
"unitTestRunner": "jest"
|
||||||
|
},
|
||||||
|
"@nx/angular:component": {
|
||||||
|
"style": "css"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"defaultProject": "frontend"
|
||||||
|
}
|
||||||
26114
frontend/package-lock.json
generated
Normal file
26114
frontend/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
61
frontend/package.json
Normal file
61
frontend/package.json
Normal file
@ -0,0 +1,61 @@
|
|||||||
|
{
|
||||||
|
"name": "@frontend/source",
|
||||||
|
"version": "0.0.0",
|
||||||
|
"license": "MIT",
|
||||||
|
"scripts": {
|
||||||
|
"start": "nx serve",
|
||||||
|
"build": "nx build",
|
||||||
|
"test": "nx test"
|
||||||
|
},
|
||||||
|
"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"
|
||||||
|
},
|
||||||
|
"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",
|
||||||
|
"@nx/angular": "21.1.2",
|
||||||
|
"@nx/eslint": "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",
|
||||||
|
"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"
|
||||||
|
},
|
||||||
|
"nx": {
|
||||||
|
"includedScripts": []
|
||||||
|
}
|
||||||
|
}
|
||||||
88
frontend/project.json
Normal file
88
frontend/project.json
Normal file
@ -0,0 +1,88 @@
|
|||||||
|
{
|
||||||
|
"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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
BIN
frontend/public/favicon.ico
Normal file
BIN
frontend/public/favicon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 15 KiB |
30
frontend/src/app/app.component.ts
Normal file
30
frontend/src/app/app.component.ts
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
import { Component, 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>
|
||||||
|
|
||||||
|
@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;
|
||||||
|
}
|
||||||
12
frontend/src/app/app.config.ts
Normal file
12
frontend/src/app/app.config.ts
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
import { ApplicationConfig, provideZoneChangeDetection } from '@angular/core';
|
||||||
|
import { provideRouter } from '@angular/router';
|
||||||
|
import { appRoutes } from './app.routes';
|
||||||
|
import { provideHttpClient } from '@angular/common/http';
|
||||||
|
|
||||||
|
export const appConfig: ApplicationConfig = {
|
||||||
|
providers: [
|
||||||
|
provideZoneChangeDetection({ eventCoalescing: true }),
|
||||||
|
provideHttpClient(),
|
||||||
|
provideRouter(appRoutes),
|
||||||
|
],
|
||||||
|
};
|
||||||
3
frontend/src/app/app.routes.ts
Normal file
3
frontend/src/app/app.routes.ts
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
import { Route } from '@angular/router';
|
||||||
|
|
||||||
|
export const appRoutes: Route[] = [];
|
||||||
13
frontend/src/index.html
Normal file
13
frontend/src/index.html
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8" />
|
||||||
|
<title>frontend</title>
|
||||||
|
<base href="/" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
|
<link rel="icon" type="image/x-icon" href="favicon.ico" />
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<app-root></app-root>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
7
frontend/src/main.ts
Normal file
7
frontend/src/main.ts
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
import { bootstrapApplication } from '@angular/platform-browser';
|
||||||
|
import { appConfig } from './app/app.config';
|
||||||
|
import { AppComponent } from './app/app.component';
|
||||||
|
|
||||||
|
bootstrapApplication(AppComponent, appConfig).catch((err) =>
|
||||||
|
console.error(err)
|
||||||
|
);
|
||||||
1
frontend/src/styles.scss
Normal file
1
frontend/src/styles.scss
Normal file
@ -0,0 +1 @@
|
|||||||
|
/* You can add global styles to this file, and also import other style files */
|
||||||
10
frontend/tsconfig.app.json
Normal file
10
frontend/tsconfig.app.json
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
{
|
||||||
|
"extends": "./tsconfig.json",
|
||||||
|
"compilerOptions": {
|
||||||
|
"outDir": "./dist/out-tsc",
|
||||||
|
"types": []
|
||||||
|
},
|
||||||
|
"files": ["src/main.ts"],
|
||||||
|
"include": ["src/**/*.d.ts"],
|
||||||
|
"exclude": ["jest.config.ts", "src/**/*.test.ts", "src/**/*.spec.ts"]
|
||||||
|
}
|
||||||
22
frontend/tsconfig.base.json
Normal file
22
frontend/tsconfig.base.json
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
{
|
||||||
|
"compileOnSave": false,
|
||||||
|
"compilerOptions": {
|
||||||
|
"rootDir": ".",
|
||||||
|
"sourceMap": true,
|
||||||
|
"declaration": false,
|
||||||
|
"moduleResolution": "node",
|
||||||
|
"emitDecoratorMetadata": true,
|
||||||
|
"experimentalDecorators": true,
|
||||||
|
"importHelpers": true,
|
||||||
|
"target": "es2022",
|
||||||
|
"module": "esnext",
|
||||||
|
"lib": ["es2020", "dom"],
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"skipDefaultLibCheck": true,
|
||||||
|
"baseUrl": ".",
|
||||||
|
"paths": {
|
||||||
|
"@frontend/shared/stats-ws": ["libs/shared/stats-ws/src/index.ts"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"exclude": ["node_modules", "tmp"]
|
||||||
|
}
|
||||||
6
frontend/tsconfig.editor.json
Normal file
6
frontend/tsconfig.editor.json
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
{
|
||||||
|
"extends": "./tsconfig.json",
|
||||||
|
"include": ["src/**/*.ts"],
|
||||||
|
"compilerOptions": {},
|
||||||
|
"exclude": ["jest.config.ts", "src/**/*.test.ts", "src/**/*.spec.ts"]
|
||||||
|
}
|
||||||
28
frontend/tsconfig.json
Normal file
28
frontend/tsconfig.json
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"forceConsistentCasingInFileNames": true,
|
||||||
|
"strict": true,
|
||||||
|
"noImplicitOverride": true,
|
||||||
|
"noPropertyAccessFromIndexSignature": true,
|
||||||
|
"noImplicitReturns": true,
|
||||||
|
"noFallthroughCasesInSwitch": true
|
||||||
|
},
|
||||||
|
"files": [],
|
||||||
|
"include": [],
|
||||||
|
"references": [
|
||||||
|
{
|
||||||
|
"path": "./tsconfig.editor.json"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "./tsconfig.app.json"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"angularCompilerOptions": {
|
||||||
|
"enableI18nLegacyMessageIdFormat": false,
|
||||||
|
"strictInjectionParameters": true,
|
||||||
|
"strictInputAccessModifiers": true,
|
||||||
|
"strictTemplates": true
|
||||||
|
},
|
||||||
|
"extends": "./tsconfig.base.json"
|
||||||
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user