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 { MatButtonModule } from '@angular/material/button';
import { MatIcon } from '@angular/material/icon'; import { MatIcon } from '@angular/material/icon';
import { MatToolbarModule } from '@angular/material/toolbar'; import { MatToolbarModule } from '@angular/material/toolbar';
import { Router } from '@angular/router';
import { ThemeToggleService } from '@frontend/shared/theme-toggle'; import { ThemeToggleService } from '@frontend/shared/theme-toggle';
@Component({ @Component({
selector: 'frontend-navbar', selector: 'frontend-navbar',
imports: [MatToolbarModule, MatIcon, MatButtonModule], imports: [MatToolbarModule, MatIcon, MatButtonModule],
template: ` template: `
<mat-toolbar> <mat-toolbar class="navbar">
<mat-icon>view_carousel</mat-icon> <div (click)="goToHome()">
<span>Docker Containers</span> <mat-icon>view_carousel</mat-icon>
<span>Docker Containers</span>
</div>
<span class="spacer"></span> <span class="spacer"></span>
<button mat-icon-button (click)="switchTheme()"> <button mat-icon-button (click)="switchTheme()">
<mat-icon>{{ isDarkTheme() ? 'light_mode' : 'dark_mode' }}</mat-icon> <mat-icon>{{ isDarkTheme() ? 'light_mode' : 'dark_mode' }}</mat-icon>
@ -22,16 +25,26 @@ import { ThemeToggleService } from '@frontend/shared/theme-toggle';
.spacer { .spacer {
flex: 1 1 auto; 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 { export class NavbarComponent {
private readonly themeToggleService = inject(ThemeToggleService); private readonly themeToggleService = inject(ThemeToggleService);
private readonly router = inject(Router);
protected isDarkTheme = this.themeToggleService.isDark; protected isDarkTheme = this.themeToggleService.isDark;
switchTheme() { switchTheme() {
this.themeToggleService.toggleTheme(); 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 { computed, DOCUMENT, effect, inject, Injectable } from '@angular/core';
import { stored } from '@mmstack/primitives'; import { stored } from '@mmstack/primitives';
function prefersDarkMode() {
return window.matchMedia('(prefers-color-scheme: dark)').matches;
}
@Injectable({ @Injectable({
providedIn: 'root', providedIn: 'root',
}) })
@ -10,21 +14,17 @@ export class ThemeToggleService {
inject(OverlayContainer).getContainerElement().classList; inject(OverlayContainer).getContainerElement().classList;
private readonly dockument = inject(DOCUMENT); private readonly dockument = inject(DOCUMENT);
private readonly theme = stored<'light' | 'dark'>('light', { private readonly theme = stored<'light' | 'dark'>(
key: 'user-theme', prefersDarkMode() ? 'dark' : 'light',
syncTabs: true, {
}); key: 'user-theme',
syncTabs: true,
}
);
public isDark = computed(() => this.theme() === 'dark'); public isDark = computed(() => this.theme() === 'dark');
constructor() { constructor() {
const prefersDark =
window.matchMedia &&
window.matchMedia('(prefers-color-scheme: dark)').matches;
if (prefersDark && this.theme() === 'light') {
this.theme.set('dark');
}
effect(() => effect(() =>
this.isDark() ? this.addDarkThemeClass() : this.removeDarkThemeClass() this.isDark() ? this.addDarkThemeClass() : this.removeDarkThemeClass()
); );

View File

@ -1,7 +1,7 @@
# stats-ws # ws
This library was generated with [Nx](https://nx.dev). This library was generated with [Nx](https://nx.dev).
## Running unit tests ## 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 { export default {
displayName: 'stats-ws', displayName: 'ws',
preset: '../../../jest.preset.js', preset: '../../../jest.preset.js',
setupFilesAfterEnv: ['<rootDir>/src/test-setup.ts'], setupFilesAfterEnv: ['<rootDir>/src/test-setup.ts'],
coverageDirectory: '../../../coverage/libs/shared/stats-ws', coverageDirectory: '../../../coverage/libs/shared/ws',
transform: { transform: {
'^.+\\.(ts|mjs|js|html)$': [ '^.+\\.(ts|mjs|js|html)$': [
'jest-preset-angular', 'jest-preset-angular',

View File

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

View File

@ -11,7 +11,7 @@ import {
@Injectable({ @Injectable({
providedIn: 'root', providedIn: 'root',
}) })
export class StatsWsService { export class WsService {
readonly containers = signal<ContainerInfo[]>([]); readonly containers = signal<ContainerInfo[]>([]);
readonly containerStats = signal<Record<string, ContainerStats>>({}); readonly containerStats = signal<Record<string, ContainerStats>>({});
readonly containerDetail = signal<PayloadContainerDetail | null>(null); 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], imports: [ContainersComponent, MatToolbarModule, MatIconModule],
template: ` template: `
<div class="content"> <div class="content">
<p class="description">View and manage your Docker containers here.</p>
<frontend-containers /> <frontend-containers />
</div> </div>
`, `,
@ -27,4 +26,3 @@ import { ContainersComponent } from './containers.component';
], ],
}) })
export class ContainersComponentShell {} export class ContainersComponentShell {}

View File

@ -1,62 +1,20 @@
import { NgClass } from '@angular/common';
import { Component, inject, OnInit } from '@angular/core'; import { Component, inject, OnInit } from '@angular/core';
import { MatButtonModule } from '@angular/material/button'; import { Router } from '@angular/router';
import { MatCardModule } from '@angular/material/card'; import { WsService } from '@frontend/shared/ws';
import { MatChipsModule } from '@angular/material/chips'; import { ContainerCardComponent } from './container-card.component';
import { MatIconModule } from '@angular/material/icon';
import { StatsWsService } from '@frontend/shared/stats-ws';
@Component({ @Component({
selector: 'frontend-containers', selector: 'frontend-containers',
imports: [ standalone: true,
MatCardModule, imports: [ContainerCardComponent],
MatIconModule,
MatChipsModule,
MatButtonModule,
NgClass,
],
template: ` template: `
<div class="content-container"> <div class="content-container">
@for (container of containers(); track container.id) { @for (container of containers(); track container.id) {
<mat-card class="container-card"> <frontend-container-card
<mat-card-header> [container]="container"
<mat-icon (containerClick)="onContainerClick($event)"
mat-card-avatar (actionClick)="onContainerAction($event)"
[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>
} }
</div> </div>
`, `,
@ -64,111 +22,38 @@ import { StatsWsService } from '@frontend/shared/stats-ws';
` `
.content-container { .content-container {
display: grid; display: grid;
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr)); grid-template-columns: repeat(auto-fill, minmax(320px, 1fr));
gap: 16px; gap: 24px;
margin-top: 16px; padding: 24px;
} }
.container-card { @media (max-width: 599px) {
transition: .content-container {
transform 0.2s, padding: 16px;
box-shadow 0.2s; gap: 16px;
} }
.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 { 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; protected containers = this.service.containers;
ngOnInit() { ngOnInit() {
this.service.initContainers(); this.service.initContainers();
} }
getStateIcon(state: string): string { onContainerClick(id: string): void {
switch (state) { this.router.navigate(['/container'], {
case 'running': queryParams: { id },
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 { onContainerAction(action: { id: string; state: string }): void {
return `state-${state.toLowerCase()}`; console.log(
} `Container action clicked: ${action.id}, State: ${action.state}`
);
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 '';
} }
} }

File diff suppressed because it is too large Load Diff

View File

@ -9,28 +9,29 @@
}, },
"private": true, "private": true,
"dependencies": { "dependencies": {
"@angular/cdk": "^20.0.1", "@angular/cdk": "20.0.2",
"@angular/common": "20.0.0", "@angular/common": "20.0.2",
"@angular/compiler": "20.0.0", "@angular/compiler": "20.0.2",
"@angular/core": "20.0.0", "@angular/core": "20.0.2",
"@angular/forms": "20.0.0", "@angular/forms": "20.0.2",
"@angular/material": "^20.0.1", "@angular/material": "20.0.2",
"@angular/platform-browser": "20.0.0", "@angular/platform-browser": "20.0.2",
"@angular/platform-browser-dynamic": "20.0.0", "@angular/platform-browser-dynamic": "20.0.2",
"@angular/router": "20.0.0", "@angular/router": "20.0.2",
"@mmstack/form-material": "^19.2.0", "@mmstack/form-material": "19.2.2",
"@mmstack/primitives": "^19.2.3", "@mmstack/primitives": "19.2.3",
"@mmstack/resource": "^19.2.0", "@mmstack/resource": "19.2.0",
"@mmstack/router-core": "^19.3.0",
"rxjs": "7.8.2", "rxjs": "7.8.2",
"zone.js": "0.15.1" "zone.js": "0.15.1"
}, },
"devDependencies": { "devDependencies": {
"@angular-devkit/build-angular": "20.0.0", "@angular-devkit/build-angular": "20.0.1",
"@angular-devkit/core": "20.0.0", "@angular-devkit/core": "20.0.1",
"@angular-devkit/schematics": "20.0.0", "@angular-devkit/schematics": "20.0.1",
"@angular/cli": "20.0.0", "@angular/cli": "20.0.1",
"@angular/compiler-cli": "20.0.0", "@angular/compiler-cli": "20.0.2",
"@angular/language-service": "20.0.0", "@angular/language-service": "20.0.2",
"@eslint/js": "9.28.0", "@eslint/js": "9.28.0",
"@nx/angular": "21.1.3", "@nx/angular": "21.1.3",
"@nx/eslint": "21.1.3", "@nx/eslint": "21.1.3",
@ -39,14 +40,14 @@
"@nx/js": "21.1.3", "@nx/js": "21.1.3",
"@nx/web": "21.1.3", "@nx/web": "21.1.3",
"@nx/workspace": "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-node/register": "1.10.10",
"@swc/core": "1.11.29", "@swc/core": "1.11.31",
"@swc/helpers": "0.5.17", "@swc/helpers": "0.5.17",
"@types/jest": "29.5.14", "@types/jest": "29.5.14",
"@types/node": "22.15.29", "@types/node": "22.15.30",
"@typescript-eslint/utils": "8.33.1", "@typescript-eslint/utils": "8.33.1",
"angular-eslint": "19.7.0", "angular-eslint": "20.0.0",
"eslint": "9.28.0", "eslint": "9.28.0",
"eslint-config-prettier": "10.1.5", "eslint-config-prettier": "10.1.5",
"jest": "29.7.0", "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 { provideHttpClient } from '@angular/common/http';
import { ApplicationConfig, provideZoneChangeDetection } from '@angular/core';
import { provideRouter, withPreloading } from '@angular/router';
import { ENVIRONMENT } from '@frontend/shared/environment'; import { ENVIRONMENT } from '@frontend/shared/environment';
import { PreloadStrategy } from '@mmstack/router-core';
import { environment } from '../environments/environment'; import { environment } from '../environments/environment';
import { appRoutes } from './app.routes';
export const appConfig: ApplicationConfig = { export const appConfig: ApplicationConfig = {
providers: [ providers: [
provideZoneChangeDetection({ eventCoalescing: true }), provideZoneChangeDetection({ eventCoalescing: true }),
provideHttpClient(), provideHttpClient(),
provideRouter(appRoutes), provideRouter(appRoutes, withPreloading(PreloadStrategy)),
{ provide: ENVIRONMENT, useValue: environment } { provide: ENVIRONMENT, useValue: environment },
], ],
}; };

View File

@ -4,7 +4,16 @@ export const appRoutes: Route[] = [
{ {
path: 'containers', path: 'containers',
loadComponent: () => 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: '**', 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,13 +1,14 @@
<!DOCTYPE html> <!doctype html>
<html lang="en"> <html lang="en">
<head> <head>
<meta charset="utf-8" /> <meta charset="utf-8" />
<title>frontend</title> <title>frontend</title>
<base href="/" /> <base href="/" />
<meta name="viewport" content="width=device-width, initial-scale=1" /> <meta name="viewport" content="width=device-width, initial-scale=1" />
<link rel="icon" type="image/x-icon" href="favicon.ico" /> <link rel="icon" type="image/x-icon" href="favicon.ico" />
</head> <script src="env.js"></script>
<body> </head>
<app-root></app-root> <body>
</body> <app-root></app-root>
</body>
</html> </html>

View File

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

View File

@ -17,11 +17,12 @@
"paths": { "paths": {
"@frontend/shared/environment": ["libs/shared/environment/src/index.ts"], "@frontend/shared/environment": ["libs/shared/environment/src/index.ts"],
"@frontend/shared/navbar": ["libs/shared/navbar/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": [ "@frontend/shared/theme-toggle": [
"libs/shared/theme-toggle/src/index.ts" "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"] "exclude": ["node_modules", "tmp"]