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