containers page wip
All checks were successful
Gitea Actions Demo / Explore-Gitea-Actions (push) Successful in 3s

This commit is contained in:
Gal Podlipnik 2025-06-07 02:25:53 +02:00
parent 50056356f3
commit 4a2a9eba30
No known key found for this signature in database
30 changed files with 914 additions and 384 deletions

View File

@ -0,0 +1,7 @@
# navbar
This library was generated with [Nx](https://nx.dev).
## Running unit tests
Run `nx test navbar` 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: 'navbar',
preset: '../../../jest.preset.js',
setupFilesAfterEnv: ['<rootDir>/src/test-setup.ts'],
coverageDirectory: '../../../coverage/libs/shared/navbar',
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": "navbar",
"$schema": "../../../node_modules/nx/schemas/project-schema.json",
"sourceRoot": "libs/shared/navbar/src",
"prefix": "lib",
"projectType": "library",
"tags": [],
"targets": {
"test": {
"executor": "@nx/jest:jest",
"outputs": ["{workspaceRoot}/coverage/{projectRoot}"],
"options": {
"jestConfig": "libs/shared/navbar/jest.config.ts"
}
},
"lint": {
"executor": "@nx/eslint:lint"
}
}
}

View File

@ -0,0 +1,2 @@
export * from './lib/navbar-shell.component';
export * from './lib/navbar.component';

View File

@ -0,0 +1,13 @@
import { Component } from '@angular/core';
import { NavbarComponent } from './navbar.component';
@Component({
selector: 'frontend-navbar-shell',
imports: [NavbarComponent],
template: `
<frontend-navbar />
<ng-content />
`,
})
export class NavbarShellComponent {}

View File

@ -0,0 +1,37 @@
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 { ThemeToggleService } from '@frontend/shared/theme-toggle';
@Component({
selector: 'frontend-navbar',
imports: [MatToolbarModule, MatIcon, MatButtonModule],
template: `
<mat-toolbar>
<mat-icon>view_carousel</mat-icon>
<span>Docker Containers</span>
<span class="spacer"></span>
<button mat-icon-button (click)="switchTheme()">
<mat-icon>{{ isDarkTheme() ? 'light_mode' : 'dark_mode' }}</mat-icon>
</button>
</mat-toolbar>
`,
styles: [
`
.spacer {
flex: 1 1 auto;
}
`,
],
})
export class NavbarComponent {
private readonly themeToggleService = inject(ThemeToggleService);
protected isDarkTheme = this.themeToggleService.isDark;
switchTheme() {
this.themeToggleService.toggleTheme();
}
}

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

@ -1,2 +1,2 @@
export * from './lib/containers';
export * from './lib/types'; export * from './lib/types';
export * from './lib/ws.service';

View File

@ -0,0 +1,7 @@
# containers
This library was generated with [Nx](https://nx.dev).
## Running unit tests
Run `nx test containers` 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: 'containers',
preset: '../../../jest.preset.js',
setupFilesAfterEnv: ['<rootDir>/src/test-setup.ts'],
coverageDirectory: '../../../coverage/libs/web/containers',
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": "containers",
"$schema": "../../../node_modules/nx/schemas/project-schema.json",
"sourceRoot": "libs/web/containers/src",
"prefix": "lib",
"projectType": "library",
"tags": [],
"targets": {
"test": {
"executor": "@nx/jest:jest",
"outputs": ["{workspaceRoot}/coverage/{projectRoot}"],
"options": {
"jestConfig": "libs/web/containers/jest.config.ts"
}
},
"lint": {
"executor": "@nx/eslint:lint"
}
}
}

View File

@ -0,0 +1 @@
export * from './lib/containers-shell.component';

View File

@ -0,0 +1,30 @@
import { Component } from '@angular/core';
import { MatIconModule } from '@angular/material/icon';
import { MatToolbarModule } from '@angular/material/toolbar';
import { ContainersComponent } from './containers.component';
@Component({
selector: 'frontend-containers-shell',
imports: [ContainersComponent, MatToolbarModule, MatIconModule],
template: `
<div class="content">
<p class="description">View and manage your Docker containers here.</p>
<frontend-containers />
</div>
`,
styles: [
`
.content {
padding: 24px;
flex: 1;
}
.description {
color: var(--sys-on-surface-variant);
margin-bottom: 24px;
font-size: 16px;
}
`,
],
})
export class ContainersComponentShell {}

View File

@ -0,0 +1,174 @@
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';
@Component({
selector: 'frontend-containers',
imports: [
MatCardModule,
MatIconModule,
MatChipsModule,
MatButtonModule,
NgClass,
],
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>
}
</div>
`,
styles: [
`
.content-container {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
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);
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';
}
}
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 '';
}
}

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

File diff suppressed because it is too large Load Diff

View File

@ -32,13 +32,13 @@
"@angular/compiler-cli": "20.0.0", "@angular/compiler-cli": "20.0.0",
"@angular/language-service": "20.0.0", "@angular/language-service": "20.0.0",
"@eslint/js": "9.28.0", "@eslint/js": "9.28.0",
"@nx/angular": "21.1.2", "@nx/angular": "21.1.3",
"@nx/eslint": "21.1.2", "@nx/eslint": "21.1.3",
"@nx/eslint-plugin": "21.1.2", "@nx/eslint-plugin": "21.1.3",
"@nx/jest": "21.1.2", "@nx/jest": "21.1.3",
"@nx/js": "21.1.2", "@nx/js": "21.1.3",
"@nx/web": "21.1.2", "@nx/web": "21.1.3",
"@nx/workspace": "21.1.2", "@nx/workspace": "21.1.3",
"@schematics/angular": "20.0.0", "@schematics/angular": "20.0.0",
"@swc-node/register": "1.10.10", "@swc-node/register": "1.10.10",
"@swc/core": "1.11.29", "@swc/core": "1.11.29",
@ -52,7 +52,7 @@
"jest": "29.7.0", "jest": "29.7.0",
"jest-environment-jsdom": "29.7.0", "jest-environment-jsdom": "29.7.0",
"jest-preset-angular": "14.6.0", "jest-preset-angular": "14.6.0",
"nx": "21.1.2", "nx": "21.1.3",
"prettier": "3.5.3", "prettier": "3.5.3",
"ts-jest": "29.3.4", "ts-jest": "29.3.4",
"ts-node": "10.9.2", "ts-node": "10.9.2",

View File

@ -1,74 +1,15 @@
import { CommonModule } from '@angular/common'; import { Component } from '@angular/core';
import { Component, OnInit, inject } from '@angular/core'; import { RouterOutlet } from '@angular/router';
import { MatButtonModule } from '@angular/material/button'; import { NavbarShellComponent } from '@frontend/shared/navbar';
import { MatCardModule } from '@angular/material/card';
import { MatIconModule } from '@angular/material/icon';
import { MatToolbarModule } from '@angular/material/toolbar';
import { RouterModule } from '@angular/router';
import { StatsWsService } from '@frontend/shared/stats-ws';
import { ThemeToggleService } from '@frontend/shared/theme-toggle';
@Component({ @Component({
standalone: true, standalone: true,
imports: [ imports: [NavbarShellComponent, RouterOutlet],
RouterModule,
MatButtonModule,
MatCardModule,
MatIconModule,
MatToolbarModule,
CommonModule,
],
selector: 'app-root', selector: 'app-root',
template: ` template: `
<mat-toolbar> <frontend-navbar-shell>
<span>Docker Containers</span> <router-outlet />
<span class="spacer"></span> </frontend-navbar-shell>
<button mat-icon-button (click)="switchTheme()">
<mat-icon>{{ isDarkTheme() ? 'light_mode' : 'dark_mode' }}</mat-icon>
</button>
</mat-toolbar>
<div class="content-container">
@for (container of containers(); track container.id) {
<mat-card class="container">
<mat-card-header>
<mat-card-title>{{ container.name }}</mat-card-title>
</mat-card-header>
<mat-card-content>
<p>ID: {{ container.id }}</p>
<p>Image: {{ container.image }}</p>
<p>Status: {{ container.status }}</p>
<p>State: {{ container.state }}</p>
</mat-card-content>
</mat-card>
}
</div>
<router-outlet></router-outlet>
`, `,
styles: [
`
.spacer {
flex: 1 1 auto;
}
.content-container {
padding: 16px;
}
`,
],
}) })
export class AppComponent implements OnInit { export class AppComponent {}
private readonly service = inject(StatsWsService);
private readonly themeToggleService = inject(ThemeToggleService);
protected containers = this.service.containers;
// Add this to check current theme for the template
protected isDarkTheme = () => this.themeToggleService.isDark();
switchTheme() {
this.themeToggleService.toggleTheme();
}
ngOnInit() {
this.service.initContainers();
}
}

View File

@ -1,3 +1,13 @@
import { Route } from '@angular/router'; import { Route } from '@angular/router';
export const appRoutes: Route[] = []; export const appRoutes: Route[] = [
{
path: 'containers',
loadComponent: () =>
import('@frontend/web/container').then((m) => m.ContainersComponentShell),
},
{
path: '**',
redirectTo: 'containers',
},
];

View File

@ -1,16 +1,35 @@
@import url('https://fonts.googleapis.com/icon?family=Material+Icons');
html, html,
body { body {
height: 100%; height: 100%;
max-width: 100vw; max-width: 100vw;
overflow-x: hidden;
} }
body { body {
margin: 0; margin: 0;
font-family: Roboto, 'Helvetica Neue', sans-serif; font-family: Roboto, 'Helvetica Neue', sans-serif;
background-color: #fafafa; background-color: #fafafa;
transition: background-color 0.3s ease; color: rgba(0, 0, 0, 0.87);
transition:
background-color 0.3s ease,
color 0.3s ease;
} }
body.dark-theme { body.dark-theme {
background-color: #303030; background-color: #303030;
color: rgba(255, 255, 255, 0.87);
}
// Add this for the mat-card styling
.container {
margin-bottom: 16px;
transition:
background-color 0.3s ease,
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

@ -16,8 +16,12 @@
"baseUrl": ".", "baseUrl": ".",
"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/stats-ws": ["libs/shared/stats-ws/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/shared/theme-toggle": [
"libs/shared/theme-toggle/src/index.ts"
],
"@frontend/web/container": ["libs/web/containers/src/index.ts"]
} }
}, },
"exclude": ["node_modules", "tmp"] "exclude": ["node_modules", "tmp"]