diff --git a/src/client/package-lock.json b/src/client/package-lock.json
index 3faf1d3..561d02f 100644
--- a/src/client/package-lock.json
+++ b/src/client/package-lock.json
@@ -17,6 +17,7 @@
"@angular/platform-browser-dynamic": "^17.0.0",
"@angular/router": "^17.0.0",
"@fortawesome/fontawesome-free": "^6.5.1",
+ "chart.js": "^4.4.1",
"primeicons": "^6.0.1",
"primeng": "^17.3.1",
"rxjs": "~7.8.0",
@@ -2756,6 +2757,11 @@
"@jridgewell/sourcemap-codec": "^1.4.14"
}
},
+ "node_modules/@kurkle/color": {
+ "version": "0.3.2",
+ "resolved": "https://registry.npmjs.org/@kurkle/color/-/color-0.3.2.tgz",
+ "integrity": "sha512-fuscdXJ9G1qb7W8VdHi+IwRqij3lBkosAm4ydQtEmbY58OzHXqQhvlxqEkoz0yssNVn38bcpRWgA9PP+OGoisw=="
+ },
"node_modules/@leichtgewicht/ip-codec": {
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/@leichtgewicht/ip-codec/-/ip-codec-2.0.4.tgz",
@@ -4533,6 +4539,17 @@
"integrity": "sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==",
"dev": true
},
+ "node_modules/chart.js": {
+ "version": "4.4.1",
+ "resolved": "https://registry.npmjs.org/chart.js/-/chart.js-4.4.1.tgz",
+ "integrity": "sha512-C74QN1bxwV1v2PEujhmKjOZ7iUM4w6BWs23Md/6aOZZSlwMzeCIDGuZay++rBgChYru7/+QFeoQW0fQoP534Dg==",
+ "dependencies": {
+ "@kurkle/color": "^0.3.0"
+ },
+ "engines": {
+ "pnpm": ">=7"
+ }
+ },
"node_modules/chokidar": {
"version": "3.5.3",
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz",
diff --git a/src/client/package.json b/src/client/package.json
index 018f66d..594c0ca 100644
--- a/src/client/package.json
+++ b/src/client/package.json
@@ -19,6 +19,7 @@
"@angular/platform-browser-dynamic": "^17.0.0",
"@angular/router": "^17.0.0",
"@fortawesome/fontawesome-free": "^6.5.1",
+ "chart.js": "^4.4.1",
"primeicons": "^6.0.1",
"primeng": "^17.3.1",
"rxjs": "~7.8.0",
diff --git a/src/client/src/app/app-routing.module.ts b/src/client/src/app/app-routing.module.ts
index 13d7a55..b46cb2b 100644
--- a/src/client/src/app/app-routing.module.ts
+++ b/src/client/src/app/app-routing.module.ts
@@ -1,12 +1,18 @@
import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
-import { ContainersComponent } from '@containers/containers.component'
+import { ContainersComponent } from './containers/containers.component'
+import { DashboardComponent } from './dashboard/dashboard.component';
-const routes: Routes = [{
- path: 'containers',
- component: ContainersComponent
-}];
+const routes: Routes = [
+ {
+ path: '',
+ component: DashboardComponent
+ },
+ {
+ path: 'containers',
+ component: ContainersComponent
+ }];
@NgModule({
imports: [RouterModule.forRoot(routes)],
diff --git a/src/client/src/app/app.component.html b/src/client/src/app/app.component.html
index 0bf7bfb..eef963f 100644
--- a/src/client/src/app/app.component.html
+++ b/src/client/src/app/app.component.html
@@ -24,7 +24,7 @@
Admiral
size="small"
[rounded]="true"
(onClick)="changeTheme()"
- [icon]="isDark ? 'fa-regular fa-moon' : 'fa-regular fa-lightbulb'"
+ [icon]="!isDark ? 'fa-regular fa-moon' : 'fa-regular fa-lightbulb'"
[outlined]="true"
>
diff --git a/src/client/src/app/app.component.ts b/src/client/src/app/app.component.ts
index 67fbbdf..b62fb20 100644
--- a/src/client/src/app/app.component.ts
+++ b/src/client/src/app/app.component.ts
@@ -2,7 +2,6 @@ import { Component } from '@angular/core';
import { MenuItem, PrimeNGConfig } from 'primeng/api';
import { ThemeService } from '@services/theme.service';
-import { TreeNode } from 'primeng/api';
@Component({
selector: 'app-root',
@@ -31,6 +30,11 @@ export class AppComponent {
this.primengConfig.ripple = true;
this.items = [
+ {
+ label: 'Home',
+ icon: 'fa-solid fa-house-chimney',
+ routerLink: '/'
+ },
{
label: 'Containers',
icon: 'fa-solid fa-box-open',
diff --git a/src/client/src/app/app.module.ts b/src/client/src/app/app.module.ts
index 0b72e11..0927c40 100644
--- a/src/client/src/app/app.module.ts
+++ b/src/client/src/app/app.module.ts
@@ -17,20 +17,25 @@ import { MessageService } from 'primeng/api';
import { TableModule } from 'primeng/table';
import { AvatarModule } from 'primeng/avatar'
import { ButtonModule } from 'primeng/button';
+import { PanelModule } from 'primeng/panel';
+import { ToolbarModule } from 'primeng/toolbar'
+import { TooltipModule } from 'primeng/tooltip';
import { DividerModule } from 'primeng/divider';
import { ListboxModule } from 'primeng/listbox';
import { SidebarModule } from 'primeng/sidebar';
import { DropdownModule } from 'primeng/dropdown';
+import { FieldsetModule } from 'primeng/fieldset'
import { PanelMenuModule } from 'primeng/panelmenu';
import { TieredMenuModule } from 'primeng/tieredmenu';
-import { ToolbarModule } from 'primeng/toolbar'
// Components
import { ContainersComponent } from '@containers/containers.component';
+import { DashboardComponent } from './dashboard/dashboard.component';
@NgModule({
declarations: [
AppComponent,
- ContainersComponent
+ ContainersComponent,
+ DashboardComponent
],
imports: [
BrowserModule,
@@ -49,10 +54,13 @@ import { ContainersComponent } from '@containers/containers.component';
MenuModule,
BadgeModule,
TieredMenuModule,
+ PanelModule,
DividerModule,
PanelMenuModule,
DropdownModule,
- ToolbarModule
+ ToolbarModule,
+ FieldsetModule,
+ TooltipModule,
],
providers: [MessageService],
bootstrap: [AppComponent]
diff --git a/src/client/src/app/dashboard/dashboard.component.html b/src/client/src/app/dashboard/dashboard.component.html
new file mode 100644
index 0000000..9611885
--- /dev/null
+++ b/src/client/src/app/dashboard/dashboard.component.html
@@ -0,0 +1,182 @@
+
+
+ Dashboard
+
+
+
+
+
+
+
+
+
+ Docker Related
+
+
+ Docker API Version
+
+
+
+
+ Platform Version
+
+
+
+
+ Go Version
+
+
+
+
+ Root Dir
+
+
+
+
+
+
+ Containers
+
+
+
+
+ Total
+
+
+
+
+
+ Running
+
+
+
+
+
+ Paused
+
+
+
+
+
+ Stopped
+
+
+
+
+
+
+
+
+
+
+ Operating System Info
+
+
+ Name
+
+
+
+
+ OS Type
+
+ Version
+
+
+
+
+ Others
+
+
+
+ Total Memory
+
+ CPU count
+
+
+
+
+ Kernel Version
+
+
+
+
+
+
+
+ Running on {{ dashboardInfo.docker_info.platform_name }}
+
diff --git a/src/client/src/app/dashboard/dashboard.component.scss b/src/client/src/app/dashboard/dashboard.component.scss
new file mode 100644
index 0000000..e69de29
diff --git a/src/client/src/app/dashboard/dashboard.component.spec.ts b/src/client/src/app/dashboard/dashboard.component.spec.ts
new file mode 100644
index 0000000..0571781
--- /dev/null
+++ b/src/client/src/app/dashboard/dashboard.component.spec.ts
@@ -0,0 +1,23 @@
+import { ComponentFixture, TestBed } from '@angular/core/testing';
+
+import { DashboardComponent } from './dashboard.component';
+
+describe('DashboardComponent', () => {
+ let component: DashboardComponent;
+ let fixture: ComponentFixture;
+
+ beforeEach(async () => {
+ await TestBed.configureTestingModule({
+ declarations: [DashboardComponent]
+ })
+ .compileComponents();
+
+ fixture = TestBed.createComponent(DashboardComponent);
+ component = fixture.componentInstance;
+ fixture.detectChanges();
+ });
+
+ it('should create', () => {
+ expect(component).toBeTruthy();
+ });
+});
diff --git a/src/client/src/app/dashboard/dashboard.component.ts b/src/client/src/app/dashboard/dashboard.component.ts
new file mode 100644
index 0000000..941f826
--- /dev/null
+++ b/src/client/src/app/dashboard/dashboard.component.ts
@@ -0,0 +1,21 @@
+import { Component, OnInit } from '@angular/core';
+
+import { DashboardService } from '@services/dashboard.service'
+import { DashboardInfo } from '@models/dashboard'
+
+@Component({
+ selector: 'app-dashboard',
+ templateUrl: './dashboard.component.html',
+ styleUrl: './dashboard.component.scss'
+})
+export class DashboardComponent implements OnInit {
+ dashboardInfo!: DashboardInfo;
+
+ constructor(private dashboardService: DashboardService) { }
+
+ ngOnInit(): void {
+ this.dashboardService.getDashboardInfo().subscribe((data: DashboardInfo) => {
+ this.dashboardInfo = data;
+ })
+ }
+}
diff --git a/src/client/src/app/models/dashboard.ts b/src/client/src/app/models/dashboard.ts
new file mode 100644
index 0000000..9f18193
--- /dev/null
+++ b/src/client/src/app/models/dashboard.ts
@@ -0,0 +1,33 @@
+interface DockerInfo {
+ api_version: string
+ go_version: string
+ platform_name: string
+ platform_version: string
+}
+interface OsInfo {
+ type: string
+ version: string
+ name: string
+}
+
+interface ContainersInfo {
+ total: number
+ paused: number
+ running: number
+ stopped: number
+}
+
+interface SystemWideInfo {
+ os: OsInfo
+ containers: ContainersInfo
+ root_dir: string
+ kernel_version: string
+ images: number
+ cpu_count: number
+ total_memory: string
+}
+
+export interface DashboardInfo {
+ docker_info: DockerInfo
+ system_wide_info: SystemWideInfo
+}
\ No newline at end of file
diff --git a/src/client/src/app/services/dashboard.service.spec.ts b/src/client/src/app/services/dashboard.service.spec.ts
new file mode 100644
index 0000000..79e72a6
--- /dev/null
+++ b/src/client/src/app/services/dashboard.service.spec.ts
@@ -0,0 +1,16 @@
+import { TestBed } from '@angular/core/testing';
+
+import { DashboardService } from './dashboard.service';
+
+describe('DashboardService', () => {
+ let service: DashboardService;
+
+ beforeEach(() => {
+ TestBed.configureTestingModule({});
+ service = TestBed.inject(DashboardService);
+ });
+
+ it('should be created', () => {
+ expect(service).toBeTruthy();
+ });
+});
diff --git a/src/client/src/app/services/dashboard.service.ts b/src/client/src/app/services/dashboard.service.ts
new file mode 100644
index 0000000..9fb18f7
--- /dev/null
+++ b/src/client/src/app/services/dashboard.service.ts
@@ -0,0 +1,21 @@
+import { Injectable } from '@angular/core';
+import { HttpClient } from '@angular/common/http';
+
+import { DashboardInfo } from '@models/dashboard'
+@Injectable({
+ providedIn: 'root'
+})
+export class DashboardService {
+
+ private api: string = 'http://127.0.0.1:2120/api';
+
+ constructor(private http: HttpClient) { }
+
+
+ getDashboardInfo() {
+ return this.http.get(`${this.api}/stats/dashboard`);
+ }
+}
+
+
+
diff --git a/src/client/src/styles.scss b/src/client/src/styles.scss
index 6329ea5..de07bc2 100644
--- a/src/client/src/styles.scss
+++ b/src/client/src/styles.scss
@@ -344,8 +344,160 @@ body {
.container {
max-width: 85% !important;
margin: auto;
+ display: grid;
}
.text-center {
text-align: center;
}
+
+.grid {
+ margin: 1em;
+ display: grid;
+ grid-template-columns: repeat(12, 1fr);
+ grid-gap: 2px;
+ counter-reset: div;
+}
+
+.col-2 {
+ grid-column: auto/span 2;
+}
+
+.col-3 {
+ grid-column: auto/span 3;
+}
+
+.col-6 {
+ grid-column: auto/span 6;
+}
+
+.col-8 {
+ grid-column: auto/span 8;
+}
+
+.row-2 {
+ grid-row: auto/span 2;
+}
+
+:root {
+ --blue-50: #f5f9ff;
+ --blue-100: #d0e1fd;
+ --blue-200: #abc9fb;
+ --blue-300: #85b2f9;
+ --blue-400: #609af8;
+ --blue-500: #3b82f6;
+ --blue-600: #326fd1;
+ --blue-700: #295bac;
+ --blue-800: #204887;
+ --blue-900: #183462;
+ --green-50: #f4fcf7;
+ --green-100: #caf1d8;
+ --green-200: #a0e6ba;
+ --green-300: #76db9b;
+ --green-400: #4cd07d;
+ --green-500: #22c55e;
+ --green-600: #1da750;
+ --green-700: #188a42;
+ --green-800: #136c34;
+ --green-900: #0e4f26;
+ --yellow-50: #fefbf3;
+ --yellow-100: #faedc4;
+ --yellow-200: #f6de95;
+ --yellow-300: #f2d066;
+ --yellow-400: #eec137;
+ --yellow-500: #eab308;
+ --yellow-600: #c79807;
+ --yellow-700: #a47d06;
+ --yellow-800: #816204;
+ --yellow-900: #5e4803;
+ --cyan-50: #f3fbfd;
+ --cyan-100: #c3edf5;
+ --cyan-200: #94e0ed;
+ --cyan-300: #65d2e4;
+ --cyan-400: #35c4dc;
+ --cyan-500: #06b6d4;
+ --cyan-600: #059bb4;
+ --cyan-700: #047f94;
+ --cyan-800: #036475;
+ --cyan-900: #024955;
+ --pink-50: #fef6fa;
+ --pink-100: #fad3e7;
+ --pink-200: #f7b0d3;
+ --pink-300: #f38ec0;
+ --pink-400: #f06bac;
+ --pink-500: #ec4899;
+ --pink-600: #c93d82;
+ --pink-700: #a5326b;
+ --pink-800: #822854;
+ --pink-900: #5e1d3d;
+ --indigo-50: #f7f7fe;
+ --indigo-100: #dadafc;
+ --indigo-200: #bcbdf9;
+ --indigo-300: #9ea0f6;
+ --indigo-400: #8183f4;
+ --indigo-500: #6366f1;
+ --indigo-600: #5457cd;
+ --indigo-700: #4547a9;
+ --indigo-800: #363885;
+ --indigo-900: #282960;
+ --teal-50: #f3fbfb;
+ --teal-100: #c7eeea;
+ --teal-200: #9ae0d9;
+ --teal-300: #6dd3c8;
+ --teal-400: #41c5b7;
+ --teal-500: #14b8a6;
+ --teal-600: #119c8d;
+ --teal-700: #0e8174;
+ --teal-800: #0b655b;
+ --teal-900: #084a42;
+ --orange-50: #fff8f3;
+ --orange-100: #feddc7;
+ --orange-200: #fcc39b;
+ --orange-300: #fba86f;
+ --orange-400: #fa8e42;
+ --orange-500: #f97316;
+ --orange-600: #d46213;
+ --orange-700: #ae510f;
+ --orange-800: #893f0c;
+ --orange-900: #642e09;
+ --bluegray-50: #f7f8f9;
+ --bluegray-100: #dadee3;
+ --bluegray-200: #bcc3cd;
+ --bluegray-300: #9fa9b7;
+ --bluegray-400: #818ea1;
+ --bluegray-500: #64748b;
+ --bluegray-600: #556376;
+ --bluegray-700: #465161;
+ --bluegray-800: #37404c;
+ --bluegray-900: #282e38;
+ --purple-50: #fbf7ff;
+ --purple-100: #ead6fd;
+ --purple-200: #dab6fc;
+ --purple-300: #c996fa;
+ --purple-400: #b975f9;
+ --purple-500: #a855f7;
+ --purple-600: #8f48d2;
+ --purple-700: #763cad;
+ --purple-800: #5c2f88;
+ --purple-900: #432263;
+ --red-50: #fff5f5;
+ --red-100: #ffd0ce;
+ --red-200: #ffaca7;
+ --red-300: #ff8780;
+ --red-400: #ff6259;
+ --red-500: #ff3d32;
+ --red-600: #d9342b;
+ --red-700: #b32b23;
+ --red-800: #8c221c;
+ --red-900: #661814;
+ --primary-50: #f7fbff;
+ --primary-100: #d9e9fe;
+ --primary-200: #bbd8fd;
+ --primary-300: #9cc7fc;
+ --primary-400: #7eb6fb;
+ --primary-500: #60a5fa;
+ --primary-600: #528cd5;
+ --primary-700: #4374af;
+ --primary-800: #355b8a;
+ --primary-900: #264264;
+}
diff --git a/src/server/models/container.py b/src/server/models/container.py
index 9f690b5..acf177c 100644
--- a/src/server/models/container.py
+++ b/src/server/models/container.py
@@ -26,6 +26,7 @@ class ContainerRunOptions(BaseModel):
class ContainerResponse(BaseModel):
id: str
name: str
+ ports: dict
status: ContainerStatusEnum
labels: dict[str, t.Any]
image: DockerImageResponse
diff --git a/src/server/utils/helpers.py b/src/server/utils/helpers.py
index ce761d8..a781b7c 100644
--- a/src/server/utils/helpers.py
+++ b/src/server/utils/helpers.py
@@ -152,6 +152,7 @@ def build_dict(container: Container) -> dict:
short_id=container.short_id,
labels=container.labels,
image=image_as_dict(container.image),
+ ports=container.ports,
)
# for multiple instances