containers update + docker file build
Some checks failed
Build and Push Docker Image / build-and-push (push) Has been cancelled
Some checks failed
Build and Push Docker Image / build-and-push (push) Has been cancelled
This commit is contained in:
parent
dac64ac41a
commit
7dd003e502
29
.gitea/workflows/build-push.yaml
Normal file
29
.gitea/workflows/build-push.yaml
Normal 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
|
||||
@ -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
55
Dockerfile
Normal 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"]
|
||||
17
frontend/libs/shared/environment/src/lib/env.service.ts
Normal file
17
frontend/libs/shared/environment/src/lib/env.service.ts
Normal 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];
|
||||
}
|
||||
}
|
||||
|
||||
@ -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-icon>view_carousel</mat-icon>
|
||||
<span>Docker Containers</span>
|
||||
<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(['/']);
|
||||
}
|
||||
}
|
||||
|
||||
@ -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', {
|
||||
key: 'user-theme',
|
||||
syncTabs: true,
|
||||
});
|
||||
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()
|
||||
);
|
||||
|
||||
@ -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.
|
||||
@ -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',
|
||||
@ -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": {
|
||||
@ -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);
|
||||
7
frontend/libs/web/container/README.md
Normal file
7
frontend/libs/web/container/README.md
Normal 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.
|
||||
34
frontend/libs/web/container/eslint.config.mjs
Normal file
34
frontend/libs/web/container/eslint.config.mjs
Normal file
@ -0,0 +1,34 @@
|
||||
import nx from '@nx/eslint-plugin';
|
||||
import baseConfig from '../../../eslint.base.config.mjs';
|
||||
|
||||
export default [
|
||||
...baseConfig,
|
||||
...nx.configs['flat/angular'],
|
||||
...nx.configs['flat/angular-template'],
|
||||
{
|
||||
files: ['**/*.ts'],
|
||||
rules: {
|
||||
'@angular-eslint/directive-selector': [
|
||||
'error',
|
||||
{
|
||||
type: 'attribute',
|
||||
prefix: 'lib',
|
||||
style: 'camelCase',
|
||||
},
|
||||
],
|
||||
'@angular-eslint/component-selector': [
|
||||
'error',
|
||||
{
|
||||
type: 'element',
|
||||
prefix: 'lib',
|
||||
style: 'kebab-case',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
files: ['**/*.html'],
|
||||
// Override or add rules here
|
||||
rules: {},
|
||||
},
|
||||
];
|
||||
21
frontend/libs/web/container/jest.config.ts
Normal file
21
frontend/libs/web/container/jest.config.ts
Normal 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',
|
||||
],
|
||||
};
|
||||
20
frontend/libs/web/container/project.json
Normal file
20
frontend/libs/web/container/project.json
Normal 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"
|
||||
}
|
||||
}
|
||||
}
|
||||
1
frontend/libs/web/container/src/index.ts
Normal file
1
frontend/libs/web/container/src/index.ts
Normal file
@ -0,0 +1 @@
|
||||
export * from './lib/container-details.component';
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
6
frontend/libs/web/container/src/test-setup.ts
Normal file
6
frontend/libs/web/container/src/test-setup.ts
Normal file
@ -0,0 +1,6 @@
|
||||
import { setupZoneTestEnv } from 'jest-preset-angular/setup-env/zone';
|
||||
|
||||
setupZoneTestEnv({
|
||||
errorOnUnknownElements: true,
|
||||
errorOnUnknownProperties: true,
|
||||
});
|
||||
28
frontend/libs/web/container/tsconfig.json
Normal file
28
frontend/libs/web/container/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/web/container/tsconfig.lib.json
Normal file
17
frontend/libs/web/container/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/web/container/tsconfig.spec.json
Normal file
16
frontend/libs/web/container/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"
|
||||
]
|
||||
}
|
||||
211
frontend/libs/web/containers/src/lib/container-card.component.ts
Normal file
211
frontend/libs/web/containers/src/lib/container-card.component.ts
Normal 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 });
|
||||
}
|
||||
}
|
||||
|
||||
@ -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 {}
|
||||
|
||||
|
||||
@ -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));
|
||||
gap: 16px;
|
||||
margin-top: 16px;
|
||||
grid-template-columns: repeat(auto-fill, minmax(320px, 1fr));
|
||||
gap: 24px;
|
||||
padding: 24px;
|
||||
}
|
||||
|
||||
.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;
|
||||
@media (max-width: 599px) {
|
||||
.content-container {
|
||||
padding: 16px;
|
||||
gap: 16px;
|
||||
}
|
||||
}
|
||||
`,
|
||||
],
|
||||
})
|
||||
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}`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
840
frontend/package-lock.json
generated
840
frontend/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -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",
|
||||
|
||||
@ -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 }
|
||||
],
|
||||
providers: [
|
||||
provideZoneChangeDetection({ eventCoalescing: true }),
|
||||
provideHttpClient(),
|
||||
provideRouter(appRoutes, withPreloading(PreloadStrategy)),
|
||||
{ provide: ENVIRONMENT, useValue: environment },
|
||||
],
|
||||
};
|
||||
|
||||
@ -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: '**',
|
||||
|
||||
@ -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'
|
||||
}
|
||||
};
|
||||
@ -1,13 +1,14 @@
|
||||
<!DOCTYPE html>
|
||||
<!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>
|
||||
<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" />
|
||||
<script src="env.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<app-root></app-root>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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"]
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user