containers update + docker file build
Some checks failed
Build and Push Docker Image / build-and-push (push) Has been cancelled

This commit is contained in:
Gal Podlipnik 2025-06-09 02:30:54 +02:00
parent dac64ac41a
commit 7dd003e502
37 changed files with 966 additions and 728 deletions

View File

@ -0,0 +1,29 @@
name: Build and Push Docker Image
on:
push:
branches: [master]
jobs:
build-and-push:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Login to Harbor
uses: docker/login-action@v3
with:
registry: harbor.galpodlipnik.com
username: ${{ secrets.HARBOR_USERNAME }}
password: ${{ secrets.HARBOR_PASSWORD }}
- name: Build and push
uses: docker/build-push-action@v5
with:
context: .
push: true
tags: harbor.galpodlipnik.com/project/docker-inspector:latest

View File

@ -1,19 +0,0 @@
name: Gitea Actions Demo
run-name: ${{ gitea.actor }} is testing out Gitea Actions 🚀
on: [push]
jobs:
Explore-Gitea-Actions:
runs-on: ubuntu-latest
steps:
- run: echo "🎉 The job was automatically triggered by a ${{ gitea.event_name }} event."
- run: echo "🐧 This job is now running on a ${{ runner.os }} server hosted by Gitea!"
- run: echo "🔎 The name of your branch is ${{ gitea.ref }} and your repository is ${{ gitea.repository }}."
- name: Check out repository code
uses: actions/checkout@v4
- run: echo "💡 The ${{ gitea.repository }} repository has been cloned to the runner."
- run: echo "🖥️ The workflow is now ready to test your code on the runner."
- name: List files in the repository
run: |
ls ${{ gitea.workspace }}
- run: echo "🍏 This job's status is ${{ job.status }}."

55
Dockerfile Normal file
View File

@ -0,0 +1,55 @@
# Build frontend
FROM node:20-alpine AS frontend-builder
WORKDIR /app/frontend
COPY frontend/package*.json ./
RUN npm ci --legacy-peer-deps
COPY frontend/ ./
RUN npm run build
# Build backend
FROM node:20-alpine AS backend-builder
WORKDIR /app/backend
COPY backend/package*.json ./
RUN npm ci
COPY backend/ ./
RUN npm run build
# Final image
FROM node:20-alpine
RUN apk add --no-cache tini
WORKDIR /app
# Create a non-root user
RUN addgroup -S appgroup && adduser -S appuser -G appgroup
# Copy built backend
COPY --from=backend-builder /app/backend/dist ./dist
COPY --from=backend-builder /app/backend/package*.json ./
# Copy built frontend
COPY --from=frontend-builder /app/frontend/dist/frontend ./public
# Install production dependencies only
RUN npm ci --omit=dev
# Create a startup script to serve both frontend and backend
COPY --chmod=755 <<EOF /app/start.sh
#!/bin/sh
# Make environment variables available to frontend
env | grep DOCKER_ > /app/public/env.js
echo "window.env = {" >> /app/public/env.js
env | grep DOCKER_ | sed 's/\(.*\)=\(.*\)/ \1: "\2",/' >> /app/public/env.js
echo "};" >> /app/public/env.js
# Start the server
node dist/index.js
EOF
# Set ownership
RUN chown -R appuser:appgroup /app
USER appuser
EXPOSE 3000
ENTRYPOINT ["/sbin/tini", "--"]
CMD ["/app/start.sh"]

View File

@ -0,0 +1,17 @@
import { Injectable } from '@angular/core';
declare global {
interface Window {
env: Record<string, string>;
}
}
@Injectable({
providedIn: 'root',
})
export class EnvService {
getEnv(key: string): string | undefined {
return window.env?.[key];
}
}

View File

@ -2,15 +2,18 @@ import { Component, inject } from '@angular/core';
import { MatButtonModule } from '@angular/material/button';
import { MatIcon } from '@angular/material/icon';
import { MatToolbarModule } from '@angular/material/toolbar';
import { Router } from '@angular/router';
import { ThemeToggleService } from '@frontend/shared/theme-toggle';
@Component({
selector: 'frontend-navbar',
imports: [MatToolbarModule, MatIcon, MatButtonModule],
template: `
<mat-toolbar>
<mat-toolbar class="navbar">
<div (click)="goToHome()">
<mat-icon>view_carousel</mat-icon>
<span>Docker Containers</span>
</div>
<span class="spacer"></span>
<button mat-icon-button (click)="switchTheme()">
<mat-icon>{{ isDarkTheme() ? 'light_mode' : 'dark_mode' }}</mat-icon>
@ -22,16 +25,26 @@ import { ThemeToggleService } from '@frontend/shared/theme-toggle';
.spacer {
flex: 1 1 auto;
}
.navbar {
background-color: var(--mat-accent-500);
color: var(--mat-accent-contrast-500);
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.3);
}
`,
],
})
export class NavbarComponent {
private readonly themeToggleService = inject(ThemeToggleService);
private readonly router = inject(Router);
protected isDarkTheme = this.themeToggleService.isDark;
switchTheme() {
this.themeToggleService.toggleTheme();
}
}
goToHome() {
this.router.navigate(['/']);
}
}

View File

@ -2,6 +2,10 @@ import { OverlayContainer } from '@angular/cdk/overlay';
import { computed, DOCUMENT, effect, inject, Injectable } from '@angular/core';
import { stored } from '@mmstack/primitives';
function prefersDarkMode() {
return window.matchMedia('(prefers-color-scheme: dark)').matches;
}
@Injectable({
providedIn: 'root',
})
@ -10,21 +14,17 @@ export class ThemeToggleService {
inject(OverlayContainer).getContainerElement().classList;
private readonly dockument = inject(DOCUMENT);
private readonly theme = stored<'light' | 'dark'>('light', {
private readonly theme = stored<'light' | 'dark'>(
prefersDarkMode() ? 'dark' : 'light',
{
key: 'user-theme',
syncTabs: true,
});
}
);
public isDark = computed(() => this.theme() === 'dark');
constructor() {
const prefersDark =
window.matchMedia &&
window.matchMedia('(prefers-color-scheme: dark)').matches;
if (prefersDark && this.theme() === 'light') {
this.theme.set('dark');
}
effect(() =>
this.isDark() ? this.addDarkThemeClass() : this.removeDarkThemeClass()
);

View File

@ -1,7 +1,7 @@
# stats-ws
# ws
This library was generated with [Nx](https://nx.dev).
## Running unit tests
Run `nx test stats-ws` to execute the unit tests.
Run `nx test ws` to execute the unit tests.

View File

@ -1,8 +1,8 @@
export default {
displayName: 'stats-ws',
displayName: 'ws',
preset: '../../../jest.preset.js',
setupFilesAfterEnv: ['<rootDir>/src/test-setup.ts'],
coverageDirectory: '../../../coverage/libs/shared/stats-ws',
coverageDirectory: '../../../coverage/libs/shared/ws',
transform: {
'^.+\\.(ts|mjs|js|html)$': [
'jest-preset-angular',

View File

@ -1,7 +1,7 @@
{
"name": "stats-ws",
"name": "ws",
"$schema": "../../../node_modules/nx/schemas/project-schema.json",
"sourceRoot": "libs/shared/stats-ws/src",
"sourceRoot": "libs/shared/ws/src",
"prefix": "frontend",
"projectType": "library",
"tags": [],
@ -10,7 +10,7 @@
"executor": "@nx/jest:jest",
"outputs": ["{workspaceRoot}/coverage/{projectRoot}"],
"options": {
"jestConfig": "libs/shared/stats-ws/jest.config.ts"
"jestConfig": "libs/shared/ws/jest.config.ts"
}
},
"lint": {

View File

@ -11,7 +11,7 @@ import {
@Injectable({
providedIn: 'root',
})
export class StatsWsService {
export class WsService {
readonly containers = signal<ContainerInfo[]>([]);
readonly containerStats = signal<Record<string, ContainerStats>>({});
readonly containerDetail = signal<PayloadContainerDetail | null>(null);

View File

@ -0,0 +1,7 @@
# container
This library was generated with [Nx](https://nx.dev).
## Running unit tests
Run `nx test container` to execute the unit tests.

View 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: {},
},
];

View File

@ -0,0 +1,21 @@
export default {
displayName: 'container',
preset: '../../../jest.preset.js',
setupFilesAfterEnv: ['<rootDir>/src/test-setup.ts'],
coverageDirectory: '../../../coverage/libs/web/container',
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',
],
};

View File

@ -0,0 +1,20 @@
{
"name": "container",
"$schema": "../../../node_modules/nx/schemas/project-schema.json",
"sourceRoot": "libs/web/container/src",
"prefix": "lib",
"projectType": "library",
"tags": [],
"targets": {
"test": {
"executor": "@nx/jest:jest",
"outputs": ["{workspaceRoot}/coverage/{projectRoot}"],
"options": {
"jestConfig": "libs/web/container/jest.config.ts"
}
},
"lint": {
"executor": "@nx/eslint:lint"
}
}
}

View File

@ -0,0 +1 @@
export * from './lib/container-details.component';

View File

@ -0,0 +1,22 @@
import { Component, inject, OnInit } from '@angular/core';
import { WsService } from '@frontend/shared/ws';
import { queryParam } from '@mmstack/router-core';
@Component({
selector: 'frontend-container-details',
imports: [],
template: `Container ID: {{ containerId() }}`,
styles: [``],
})
export class ContainerDetailsComponent implements OnInit {
private readonly service = inject(WsService);
protected readonly containerId = queryParam('id');
ngOnInit() {
const id = this.containerId();
if (id) {
this.service.initContainerDetail(id);
}
}
}

View File

@ -0,0 +1,6 @@
import { setupZoneTestEnv } from 'jest-preset-angular/setup-env/zone';
setupZoneTestEnv({
errorOnUnknownElements: true,
errorOnUnknownProperties: true,
});

View 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
}
}

View 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"]
}

View 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"
]
}

View File

@ -0,0 +1,211 @@
import { NgClass } from '@angular/common';
import { Component, EventEmitter, Input, Output } from '@angular/core';
import { MatButtonModule } from '@angular/material/button';
import { MatCardModule } from '@angular/material/card';
import { MatChipsModule } from '@angular/material/chips';
import { MatDividerModule } from '@angular/material/divider';
import { MatIconModule } from '@angular/material/icon';
import { MatTooltipModule } from '@angular/material/tooltip';
@Component({
selector: 'frontend-container-card',
standalone: true,
imports: [
MatCardModule,
MatIconModule,
MatChipsModule,
MatButtonModule,
MatDividerModule,
MatTooltipModule,
NgClass,
],
template: `
<mat-card
class="container-card"
(click)="containerClick.emit(container.id)"
>
<mat-card-header>
<div mat-card-avatar class="avatar-container">
<mat-icon
class="status-icon"
[ngClass]="getStateClass(container.state)"
[matTooltip]="container.state"
>
{{ getStateIcon(container.state) }}
</mat-icon>
</div>
<mat-card-title class="mat-headline-6">{{
container.name
}}</mat-card-title>
<mat-card-subtitle class="mat-subtitle-2">{{
container.image
}}</mat-card-subtitle>
</mat-card-header>
<mat-card-content class="card-content">
<div class="info-row">
<span class="label">ID:</span>
<span class="value monospace">{{
container.id.substring(0, 12)
}}</span>
</div>
<div class="info-row">
<span class="label">Status:</span>
<mat-chip [ngClass]="getStatusClass(container.status)">
{{ container.status }}
</mat-chip>
</div>
</mat-card-content>
<mat-divider></mat-divider>
<mat-card-actions>
<button
mat-stroked-button
[disabled]="true"
[color]="container.state === 'running' ? 'warn' : 'primary'"
class="action-button"
(click)="onActionClick($event, container.id, container.state)"
>
<mat-icon>{{
container.state === 'running' ? 'stop' : 'play_arrow'
}}</mat-icon>
{{ container.state === 'running' ? 'STOP' : 'START' }}
</button>
</mat-card-actions>
</mat-card>
`,
styles: [
`
.container-card {
border-radius: 8px;
transition: transform 0.2s;
overflow: hidden;
cursor: pointer;
}
.container-card:hover {
transform: translateY(-2px);
box-shadow: 0 5px 10px rgba(0, 0, 0, 0.1);
}
.avatar-container {
display: flex;
align-items: center;
justify-content: center;
background-color: rgba(0, 0, 0, 0.04);
border-radius: 50%;
}
.status-icon {
font-size: 24px;
height: 24px;
width: 24px;
}
.card-content {
padding: 16px 16px 8px;
}
.info-row {
display: flex;
margin-bottom: 12px;
align-items: center;
}
.label {
font-weight: 500;
color: var(--mat-text-secondary-color);
width: 70px;
}
.value {
font-size: 14px;
}
.monospace {
font-family: 'Roboto Mono', monospace;
}
.state-running {
color: var(--mat-green-500);
}
.state-exited,
.state-stopped {
color: var(--mat-red-500);
}
.state-created,
.state-paused {
color: var(--mat-amber-500);
}
.status-chip-running {
background-color: var(--mat-green-50) !important;
color: var(--mat-green-700) !important;
}
.status-chip-exited,
.status-chip-stopped {
background-color: var(--mat-red-50) !important;
color: var(--mat-red-700) !important;
}
.status-chip-created,
.status-chip-paused {
background-color: var(--mat-amber-50) !important;
color: var(--mat-amber-700) !important;
}
mat-card-actions {
padding: 16px;
display: flex;
justify-content: flex-end;
gap: 8px;
}
.action-button {
min-width: 100px;
}
@media (max-width: 599px) {
mat-card-actions {
flex-direction: column;
align-items: stretch;
}
.action-button {
width: 100%;
margin: 0 !important;
}
}
`,
],
})
export class ContainerCardComponent {
@Input() container: any;
@Output() containerClick = new EventEmitter<string>();
@Output() actionClick = new EventEmitter<{ id: string; state: string }>();
getStateIcon(state: string): string {
switch (state) {
case 'running':
return 'play_circle';
case 'exited':
return 'stop_circle';
case 'created':
return 'fiber_new';
case 'paused':
return 'pause_circle';
default:
return 'help_circle';
}
}
getStateClass(state: string): string {
return `state-${state.toLowerCase()}`;
}
getStatusClass(status: string): string {
if (status.includes('Up')) return 'status-chip-running';
if (status.includes('Exited')) return 'status-chip-exited';
if (status.includes('Created')) return 'status-chip-created';
if (status.includes('Paused')) return 'status-chip-paused';
return '';
}
onActionClick(event: Event, id: string, state: string) {
event.stopPropagation();
this.actionClick.emit({ id, state });
}
}

View File

@ -8,7 +8,6 @@ import { ContainersComponent } from './containers.component';
imports: [ContainersComponent, MatToolbarModule, MatIconModule],
template: `
<div class="content">
<p class="description">View and manage your Docker containers here.</p>
<frontend-containers />
</div>
`,
@ -27,4 +26,3 @@ import { ContainersComponent } from './containers.component';
],
})
export class ContainersComponentShell {}

View File

@ -1,62 +1,20 @@
import { NgClass } from '@angular/common';
import { Component, inject, OnInit } from '@angular/core';
import { MatButtonModule } from '@angular/material/button';
import { MatCardModule } from '@angular/material/card';
import { MatChipsModule } from '@angular/material/chips';
import { MatIconModule } from '@angular/material/icon';
import { StatsWsService } from '@frontend/shared/stats-ws';
import { Router } from '@angular/router';
import { WsService } from '@frontend/shared/ws';
import { ContainerCardComponent } from './container-card.component';
@Component({
selector: 'frontend-containers',
imports: [
MatCardModule,
MatIconModule,
MatChipsModule,
MatButtonModule,
NgClass,
],
standalone: true,
imports: [ContainerCardComponent],
template: `
<div class="content-container">
@for (container of containers(); track container.id) {
<mat-card class="container-card">
<mat-card-header>
<mat-icon
mat-card-avatar
[ngClass]="getStateClass(container.state)"
>
{{ getStateIcon(container.state) }}
</mat-icon>
<mat-card-title>{{ container.name }}</mat-card-title>
<mat-card-subtitle>{{ container.image }}</mat-card-subtitle>
</mat-card-header>
<mat-card-content>
<div class="info-row">
<span class="label">ID:</span>
<span class="value">{{ container.id.substring(0, 12) }}</span>
</div>
<div class="info-row">
<span class="label">Status:</span>
<mat-chip [ngClass]="getStatusClass(container.status)">
{{ container.status }}
</mat-chip>
</div>
</mat-card-content>
<mat-card-actions>
<button mat-button color="primary">
<mat-icon>info</mat-icon> DETAILS
</button>
<button
mat-button
[disabled]="true"
[color]="container.state === 'running' ? 'warn' : 'primary'"
>
<mat-icon>{{
container.state === 'running' ? 'stop' : 'play_arrow'
}}</mat-icon>
{{ container.state === 'running' ? 'STOP' : 'START' }}
</button>
</mat-card-actions>
</mat-card>
<frontend-container-card
[container]="container"
(containerClick)="onContainerClick($event)"
(actionClick)="onContainerAction($event)"
/>
}
</div>
`,
@ -64,111 +22,38 @@ import { StatsWsService } from '@frontend/shared/stats-ws';
`
.content-container {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
grid-template-columns: repeat(auto-fill, minmax(320px, 1fr));
gap: 24px;
padding: 24px;
}
@media (max-width: 599px) {
.content-container {
padding: 16px;
gap: 16px;
margin-top: 16px;
}
.container-card {
transition:
transform 0.2s,
box-shadow 0.2s;
}
.container-card:hover {
transform: translateY(-4px);
box-shadow: 0 6px 10px rgba(0, 0, 0, 0.1);
}
.info-row {
display: flex;
margin-bottom: 8px;
align-items: center;
}
.label {
font-weight: 500;
color: var(--sys-on-surface-variant);
width: 70px;
}
.value {
font-family: monospace;
font-size: 14px;
}
.state-running {
color: #4caf50;
}
.state-exited,
.state-stopped {
color: #f44336;
}
.state-created,
.state-paused {
color: #ff9800;
}
.status-chip-running {
background-color: rgba(76, 175, 80, 0.15) !important;
color: #4caf50 !important;
}
.status-chip-exited,
.status-chip-stopped {
background-color: rgba(244, 67, 54, 0.15) !important;
color: #f44336 !important;
}
.status-chip-created,
.status-chip-paused {
background-color: rgba(255, 152, 0, 0.15) !important;
color: #ff9800 !important;
}
mat-card-actions {
padding: 8px 16px 16px;
display: flex;
justify-content: flex-end;
}
`,
],
})
export class ContainersComponent implements OnInit {
private readonly service = inject(StatsWsService);
private readonly service = inject(WsService);
private readonly router = inject(Router);
protected containers = this.service.containers;
ngOnInit() {
this.service.initContainers();
}
getStateIcon(state: string): string {
switch (state) {
case 'running':
return 'play_circle';
case 'exited':
return 'stop_circle';
case 'created':
return 'fiber_new';
case 'paused':
return 'pause_circle';
default:
return 'help_circle';
}
onContainerClick(id: string): void {
this.router.navigate(['/container'], {
queryParams: { id },
});
}
getStateClass(state: string): string {
return `state-${state.toLowerCase()}`;
}
getStatusClass(status: string): string {
if (status.includes('Up')) return 'status-chip-running';
if (status.includes('Exited')) return 'status-chip-exited';
if (status.includes('Created')) return 'status-chip-created';
if (status.includes('Paused')) return 'status-chip-paused';
return '';
onContainerAction(action: { id: string; state: string }): void {
console.log(
`Container action clicked: ${action.id}, State: ${action.state}`
);
}
}

File diff suppressed because it is too large Load Diff

View File

@ -9,28 +9,29 @@
},
"private": true,
"dependencies": {
"@angular/cdk": "^20.0.1",
"@angular/common": "20.0.0",
"@angular/compiler": "20.0.0",
"@angular/core": "20.0.0",
"@angular/forms": "20.0.0",
"@angular/material": "^20.0.1",
"@angular/platform-browser": "20.0.0",
"@angular/platform-browser-dynamic": "20.0.0",
"@angular/router": "20.0.0",
"@mmstack/form-material": "^19.2.0",
"@mmstack/primitives": "^19.2.3",
"@mmstack/resource": "^19.2.0",
"@angular/cdk": "20.0.2",
"@angular/common": "20.0.2",
"@angular/compiler": "20.0.2",
"@angular/core": "20.0.2",
"@angular/forms": "20.0.2",
"@angular/material": "20.0.2",
"@angular/platform-browser": "20.0.2",
"@angular/platform-browser-dynamic": "20.0.2",
"@angular/router": "20.0.2",
"@mmstack/form-material": "19.2.2",
"@mmstack/primitives": "19.2.3",
"@mmstack/resource": "19.2.0",
"@mmstack/router-core": "^19.3.0",
"rxjs": "7.8.2",
"zone.js": "0.15.1"
},
"devDependencies": {
"@angular-devkit/build-angular": "20.0.0",
"@angular-devkit/core": "20.0.0",
"@angular-devkit/schematics": "20.0.0",
"@angular/cli": "20.0.0",
"@angular/compiler-cli": "20.0.0",
"@angular/language-service": "20.0.0",
"@angular-devkit/build-angular": "20.0.1",
"@angular-devkit/core": "20.0.1",
"@angular-devkit/schematics": "20.0.1",
"@angular/cli": "20.0.1",
"@angular/compiler-cli": "20.0.2",
"@angular/language-service": "20.0.2",
"@eslint/js": "9.28.0",
"@nx/angular": "21.1.3",
"@nx/eslint": "21.1.3",
@ -39,14 +40,14 @@
"@nx/js": "21.1.3",
"@nx/web": "21.1.3",
"@nx/workspace": "21.1.3",
"@schematics/angular": "20.0.0",
"@schematics/angular": "20.0.1",
"@swc-node/register": "1.10.10",
"@swc/core": "1.11.29",
"@swc/core": "1.11.31",
"@swc/helpers": "0.5.17",
"@types/jest": "29.5.14",
"@types/node": "22.15.29",
"@types/node": "22.15.30",
"@typescript-eslint/utils": "8.33.1",
"angular-eslint": "19.7.0",
"angular-eslint": "20.0.0",
"eslint": "9.28.0",
"eslint-config-prettier": "10.1.5",
"jest": "29.7.0",

View File

@ -1,15 +1,16 @@
import { ApplicationConfig, provideZoneChangeDetection } from '@angular/core';
import { provideRouter } from '@angular/router';
import { appRoutes } from './app.routes';
import { provideHttpClient } from '@angular/common/http';
import { ApplicationConfig, provideZoneChangeDetection } from '@angular/core';
import { provideRouter, withPreloading } from '@angular/router';
import { ENVIRONMENT } from '@frontend/shared/environment';
import { PreloadStrategy } from '@mmstack/router-core';
import { environment } from '../environments/environment';
import { appRoutes } from './app.routes';
export const appConfig: ApplicationConfig = {
providers: [
provideZoneChangeDetection({ eventCoalescing: true }),
provideHttpClient(),
provideRouter(appRoutes),
{ provide: ENVIRONMENT, useValue: environment }
provideRouter(appRoutes, withPreloading(PreloadStrategy)),
{ provide: ENVIRONMENT, useValue: environment },
],
};

View File

@ -4,7 +4,16 @@ export const appRoutes: Route[] = [
{
path: 'containers',
loadComponent: () =>
import('@frontend/web/container').then((m) => m.ContainersComponentShell),
import('@frontend/web/containers').then(
(m) => m.ContainersComponentShell
),
},
{
path: 'container',
loadComponent: () =>
import('@frontend/web/container').then(
(m) => m.ContainerDetailsComponent
),
},
{
path: '**',

View File

@ -1,13 +0,0 @@
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'
}
};

View File

@ -1,4 +1,4 @@
<!DOCTYPE html>
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
@ -6,6 +6,7 @@
<base href="/" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<link rel="icon" type="image/x-icon" href="favicon.ico" />
<script src="env.js"></script>
</head>
<body>
<app-root></app-root>

View File

@ -7,7 +7,6 @@ body {
}
body {
margin: 0;
font-family: Roboto, 'Helvetica Neue', sans-serif;
background-color: #fafafa;
color: rgba(0, 0, 0, 0.87);
transition:
@ -20,7 +19,6 @@ body.dark-theme {
color: rgba(255, 255, 255, 0.87);
}
// Add this for the mat-card styling
.container {
margin-bottom: 16px;
transition:
@ -28,7 +26,6 @@ body.dark-theme {
color 0.3s ease;
}
// Ensure mat-card content changes color appropriately
body.dark-theme .mat-mdc-card {
--mdc-elevated-card-container-color: #424242;
color: rgba(255, 255, 255, 0.87);

View File

@ -17,11 +17,12 @@
"paths": {
"@frontend/shared/environment": ["libs/shared/environment/src/index.ts"],
"@frontend/shared/navbar": ["libs/shared/navbar/src/index.ts"],
"@frontend/shared/stats-ws": ["libs/shared/stats-ws/src/index.ts"],
"@frontend/shared/theme-toggle": [
"libs/shared/theme-toggle/src/index.ts"
],
"@frontend/web/container": ["libs/web/containers/src/index.ts"]
"@frontend/shared/ws": ["libs/shared/ws/src/index.ts"],
"@frontend/web/container": ["libs/web/container/src/index.ts"],
"@frontend/web/containers": ["libs/web/containers/src/index.ts"]
}
},
"exclude": ["node_modules", "tmp"]