From be92d93f3f267965649e0570c0c192eca31b4262 Mon Sep 17 00:00:00 2001 From: Juergen Kunz Date: Wed, 26 Nov 2025 18:34:29 +0000 Subject: [PATCH] feat(ui): Sync UI tab state with URL and update routes/links --- changelog.md | 9 ++++ ts/00_commitinfo_data.ts | 2 +- ui/src/app/app.routes.ts | 46 +++++++++++++------ .../domains/domain-detail.component.ts | 2 +- .../app/features/network/network.component.ts | 16 ++++++- .../registries/registries.component.ts | 24 ++++++++-- .../platform-service-detail.component.ts | 11 +++-- .../services/service-detail.component.ts | 8 +++- .../services/services-list.component.ts | 30 +++++++++--- 9 files changed, 117 insertions(+), 31 deletions(-) diff --git a/changelog.md b/changelog.md index a562007..95c8804 100644 --- a/changelog.md +++ b/changelog.md @@ -1,5 +1,14 @@ # Changelog +## 2025-11-26 - 1.2.0 - feat(ui) +Sync UI tab state with URL and update routes/links + +- Add VSCode workspace recommendations, launch and tasks configs for the UI (ui/.vscode/*) +- Update Angular routes to support tab URL segments and default redirects for services, network and registries +- Change service detail route to use explicit 'detail/:name' path and update links accordingly +- Make ServicesList, Registries and Network components read tab from route params and navigate on tab changes; add ngOnDestroy to unsubscribe +- Update Domain detail template link to point to the new services detail route + ## 2025-11-26 - 1.1.0 - feat(platform-services) Add platform service log streaming, improve health checks and provisioning robustness diff --git a/ts/00_commitinfo_data.ts b/ts/00_commitinfo_data.ts index c61e402..0005f0b 100644 --- a/ts/00_commitinfo_data.ts +++ b/ts/00_commitinfo_data.ts @@ -3,6 +3,6 @@ */ export const commitinfo = { name: '@serve.zone/onebox', - version: '1.1.0', + version: '1.2.0', description: 'Self-hosted container platform with automatic SSL and DNS - a mini Heroku for single servers' } diff --git a/ui/src/app/app.routes.ts b/ui/src/app/app.routes.ts index d9ec738..380acaa 100644 --- a/ui/src/app/app.routes.ts +++ b/ui/src/app/app.routes.ts @@ -32,10 +32,8 @@ export const routes: Routes = [ children: [ { path: '', - loadComponent: () => - import('./features/services/services-list.component').then( - (m) => m.ServicesListComponent - ), + redirectTo: 'user', + pathMatch: 'full', }, { path: 'create', @@ -52,12 +50,19 @@ export const routes: Routes = [ ), }, { - path: ':name', + path: 'detail/:name', loadComponent: () => import('./features/services/service-detail.component').then( (m) => m.ServiceDetailComponent ), }, + { + path: ':tab', + loadComponent: () => + import('./features/services/services-list.component').then( + (m) => m.ServicesListComponent + ), + }, ], }, { @@ -65,10 +70,8 @@ export const routes: Routes = [ children: [ { path: '', - loadComponent: () => - import('./features/network/network.component').then( - (m) => m.NetworkComponent - ), + redirectTo: 'proxy', + pathMatch: 'full', }, { path: 'domains/:domain', @@ -77,14 +80,31 @@ export const routes: Routes = [ (m) => m.DomainDetailComponent ), }, + { + path: ':tab', + loadComponent: () => + import('./features/network/network.component').then( + (m) => m.NetworkComponent + ), + }, ], }, { path: 'registries', - loadComponent: () => - import('./features/registries/registries.component').then( - (m) => m.RegistriesComponent - ), + children: [ + { + path: '', + redirectTo: 'onebox', + pathMatch: 'full', + }, + { + path: ':tab', + loadComponent: () => + import('./features/registries/registries.component').then( + (m) => m.RegistriesComponent + ), + }, + ], }, { path: 'tokens', diff --git a/ui/src/app/features/domains/domain-detail.component.ts b/ui/src/app/features/domains/domain-detail.component.ts index 6824234..6205185 100644 --- a/ui/src/app/features/domains/domain-detail.component.ts +++ b/ui/src/app/features/domains/domain-detail.component.ts @@ -191,7 +191,7 @@ import { SkeletonComponent } from '../../ui/skeleton/skeleton.component'; - + diff --git a/ui/src/app/features/network/network.component.ts b/ui/src/app/features/network/network.component.ts index 5fd8ee5..4720c02 100644 --- a/ui/src/app/features/network/network.component.ts +++ b/ui/src/app/features/network/network.component.ts @@ -1,4 +1,6 @@ import { Component, inject, signal, OnInit, OnDestroy, ViewChild, ElementRef, effect } from '@angular/core'; +import { ActivatedRoute, Router } from '@angular/router'; +import { Subscription } from 'rxjs'; import { ApiService } from '../../core/services/api.service'; import { ToastService } from '../../core/services/toast.service'; import { NetworkLogStreamService } from '../../core/services/network-log-stream.service'; @@ -294,6 +296,9 @@ type TNetworkTab = 'proxy' | 'dns' | 'domains'; export class NetworkComponent implements OnInit, OnDestroy { private api = inject(ApiService); private toast = inject(ToastService); + private route = inject(ActivatedRoute); + private router = inject(Router); + private routeSub?: Subscription; networkLogStream = inject(NetworkLogStreamService); @ViewChild('logContainer') logContainer!: ElementRef; @@ -321,15 +326,24 @@ export class NetworkComponent implements OnInit, OnDestroy { } ngOnInit(): void { + // Subscribe to route params to sync tab state with URL + this.routeSub = this.route.paramMap.subscribe((params) => { + const tab = params.get('tab') as TNetworkTab; + if (tab && ['proxy', 'dns', 'domains'].includes(tab)) { + this.activeTab.set(tab); + } + }); + this.loadData(); } ngOnDestroy(): void { + this.routeSub?.unsubscribe(); this.networkLogStream.disconnect(); } setTab(tab: TNetworkTab): void { - this.activeTab.set(tab); + this.router.navigate(['/network', tab]); } async loadData(): Promise { diff --git a/ui/src/app/features/registries/registries.component.ts b/ui/src/app/features/registries/registries.component.ts index a4f3992..1e519e1 100644 --- a/ui/src/app/features/registries/registries.component.ts +++ b/ui/src/app/features/registries/registries.component.ts @@ -1,6 +1,7 @@ -import { Component, inject, signal, OnInit } from '@angular/core'; +import { Component, inject, signal, OnInit, OnDestroy } from '@angular/core'; import { FormsModule } from '@angular/forms'; -import { RouterLink } from '@angular/router'; +import { RouterLink, ActivatedRoute, Router } from '@angular/router'; +import { Subscription } from 'rxjs'; import { ApiService } from '../../core/services/api.service'; import { ToastService } from '../../core/services/toast.service'; import { IRegistry, IRegistryCreate } from '../../core/types/api.types'; @@ -242,9 +243,12 @@ type TRegistriesTab = 'onebox' | 'external'; `, }) -export class RegistriesComponent implements OnInit { +export class RegistriesComponent implements OnInit, OnDestroy { private api = inject(ApiService); private toast = inject(ToastService); + private route = inject(ActivatedRoute); + private router = inject(Router); + private routeSub?: Subscription; activeTab = signal('onebox'); registries = signal([]); @@ -256,13 +260,25 @@ export class RegistriesComponent implements OnInit { form: IRegistryCreate = { url: '', username: '', password: '' }; setTab(tab: TRegistriesTab): void { - this.activeTab.set(tab); + this.router.navigate(['/registries', tab]); } ngOnInit(): void { + // Subscribe to route params to sync tab state with URL + this.routeSub = this.route.paramMap.subscribe((params) => { + const tab = params.get('tab') as TRegistriesTab; + if (tab && ['onebox', 'external'].includes(tab)) { + this.activeTab.set(tab); + } + }); + this.loadRegistries(); } + ngOnDestroy(): void { + this.routeSub?.unsubscribe(); + } + async loadRegistries(): Promise { this.loading.set(true); try { diff --git a/ui/src/app/features/services/platform-service-detail.component.ts b/ui/src/app/features/services/platform-service-detail.component.ts index c100ea2..ac09a08 100644 --- a/ui/src/app/features/services/platform-service-detail.component.ts +++ b/ui/src/app/features/services/platform-service-detail.component.ts @@ -1,5 +1,6 @@ import { Component, inject, signal, OnInit, OnDestroy, effect, ViewChild, ElementRef } from '@angular/core'; -import { ActivatedRoute, Router, RouterLink } from '@angular/router'; +import { Location } from '@angular/common'; +import { ActivatedRoute, Router } from '@angular/router'; import { FormsModule } from '@angular/forms'; import { ApiService } from '../../core/services/api.service'; import { ToastService } from '../../core/services/toast.service'; @@ -22,7 +23,6 @@ import { SkeletonComponent } from '../../ui/skeleton/skeleton.component'; selector: 'app-platform-service-detail', standalone: true, imports: [ - RouterLink, FormsModule, CardComponent, CardHeaderComponent, @@ -38,7 +38,7 @@ import { SkeletonComponent } from '../../ui/skeleton/skeleton.component';
- + @@ -235,6 +235,7 @@ import { SkeletonComponent } from '../../ui/skeleton/skeleton.component'; `, }) export class PlatformServiceDetailComponent implements OnInit, OnDestroy { + private location = inject(Location); private route = inject(ActivatedRoute); private router = inject(Router); private api = inject(ApiService); @@ -281,6 +282,10 @@ export class PlatformServiceDetailComponent implements OnInit, OnDestroy { } } + goBack(): void { + this.location.back(); + } + async loadService(type: TPlatformServiceType): Promise { this.loading.set(true); try { diff --git a/ui/src/app/features/services/service-detail.component.ts b/ui/src/app/features/services/service-detail.component.ts index 79fa5b6..661c77d 100644 --- a/ui/src/app/features/services/service-detail.component.ts +++ b/ui/src/app/features/services/service-detail.component.ts @@ -1,4 +1,5 @@ import { Component, inject, signal, computed, OnInit, OnDestroy, ViewChild, ElementRef, effect } from '@angular/core'; +import { Location } from '@angular/common'; import { ActivatedRoute, Router, RouterLink } from '@angular/router'; import { FormsModule } from '@angular/forms'; import { ApiService } from '../../core/services/api.service'; @@ -58,7 +59,7 @@ import {
- + @@ -422,6 +423,7 @@ import { `, }) export class ServiceDetailComponent implements OnInit, OnDestroy { + private location = inject(Location); private route = inject(ActivatedRoute); private router = inject(Router); private api = inject(ApiService); @@ -479,6 +481,10 @@ export class ServiceDetailComponent implements OnInit, OnDestroy { } } + goBack(): void { + this.location.back(); + } + ngOnDestroy(): void { this.logStream.disconnect(); } diff --git a/ui/src/app/features/services/services-list.component.ts b/ui/src/app/features/services/services-list.component.ts index 6c0d412..7354170 100644 --- a/ui/src/app/features/services/services-list.component.ts +++ b/ui/src/app/features/services/services-list.component.ts @@ -1,5 +1,6 @@ -import { Component, inject, signal, effect, OnInit } from '@angular/core'; -import { RouterLink } from '@angular/router'; +import { Component, inject, signal, effect, OnInit, OnDestroy } from '@angular/core'; +import { RouterLink, ActivatedRoute, Router } from '@angular/router'; +import { Subscription } from 'rxjs'; import { ApiService } from '../../core/services/api.service'; import { WebSocketService } from '../../core/services/websocket.service'; import { ToastService } from '../../core/services/toast.service'; @@ -69,7 +70,7 @@ type TServicesTab = 'user' | 'system';

Manage your deployed and system services

@if (activeTab() === 'user') { - +
@@ -124,7 +125,7 @@ type TServicesTab = 'user' | 'system'; @for (service of services(); track service.name) { - + {{ service.name }} @@ -299,10 +300,13 @@ type TServicesTab = 'user' | 'system'; `, }) -export class ServicesListComponent implements OnInit { +export class ServicesListComponent implements OnInit, OnDestroy { private api = inject(ApiService); private ws = inject(WebSocketService); private toast = inject(ToastService); + private route = inject(ActivatedRoute); + private router = inject(Router); + private routeSub?: Subscription; // Tab state activeTab = signal('user'); @@ -331,12 +335,24 @@ export class ServicesListComponent implements OnInit { } ngOnInit(): void { + // Subscribe to route params to sync tab state with URL + this.routeSub = this.route.paramMap.subscribe((params) => { + const tab = params.get('tab') as TServicesTab; + if (tab && ['user', 'system'].includes(tab)) { + this.activeTab.set(tab); + } + }); + this.loadServices(); this.loadPlatformServices(); } + ngOnDestroy(): void { + this.routeSub?.unsubscribe(); + } + setTab(tab: TServicesTab): void { - this.activeTab.set(tab); + this.router.navigate(['/services', tab]); } async loadServices(): Promise {