2025-11-26 14:12:20 +00:00
import { Component , inject , signal , OnInit , effect } from '@angular/core' ;
import { ActivatedRoute , Router , RouterLink } from '@angular/router' ;
import { ApiService } from '../../core/services/api.service' ;
import { ToastService } from '../../core/services/toast.service' ;
import { WebSocketService } from '../../core/services/websocket.service' ;
import { IPlatformService , IContainerStats , TPlatformServiceType } from '../../core/types/api.types' ;
2025-11-26 16:36:01 +00:00
import { ContainerStatsComponent } from '../../shared/components/container-stats/container-stats.component' ;
2025-11-26 14:12:20 +00:00
import {
CardComponent ,
CardHeaderComponent ,
CardTitleComponent ,
CardDescriptionComponent ,
CardContentComponent ,
} from '../../ui/card/card.component' ;
import { ButtonComponent } from '../../ui/button/button.component' ;
import { BadgeComponent } from '../../ui/badge/badge.component' ;
import { SkeletonComponent } from '../../ui/skeleton/skeleton.component' ;
@Component ( {
selector : 'app-platform-service-detail' ,
standalone : true ,
imports : [
RouterLink ,
CardComponent ,
CardHeaderComponent ,
CardTitleComponent ,
CardDescriptionComponent ,
CardContentComponent ,
ButtonComponent ,
BadgeComponent ,
SkeletonComponent ,
2025-11-26 16:36:01 +00:00
ContainerStatsComponent ,
2025-11-26 14:12:20 +00:00
] ,
template : `
< div class = "space-y-6" >
<!-- Header -->
< div >
< a routerLink = "/services" class = "text-sm text-muted-foreground hover:text-foreground inline-flex items-center gap-1 mb-2" >
< svg class = "h-4 w-4" fill = "none" viewBox = "0 0 24 24" stroke = "currentColor" stroke-width = "2" >
< path stroke-linecap = "round" stroke-linejoin = "round" d = "M15 19l-7-7 7-7" / >
< / svg >
Back to Services
< / a >
@if ( loading ( ) && ! service ( ) ) {
< ui - skeleton class = "h-9 w-48" / >
} @else if ( service ( ) ) {
< div class = "flex items-center gap-4" >
< h1 class = "text-3xl font-bold tracking-tight" > { { service ( ) ! . displayName } } < / h1 >
< ui - badge [ variant ] = "getStatusVariant(service()!.status)" > { { service ( ) ! . status } } < / u i - b a d g e >
@if ( service ( ) ! . isCore ) {
< ui - badge variant = "outline" > Core Service < / u i - b a d g e >
}
< / div >
}
< / div >
@if ( loading ( ) && ! service ( ) ) {
< div class = "grid gap-6 md:grid-cols-2" >
< ui - card >
< ui - card - header class = "flex flex-col space-y-1.5" >
< ui - skeleton class = "h-6 w-32" / >
< / u i - c a r d - h e a d e r >
< ui - card - content class = "space-y-4" >
@for ( _ of [ 1 , 2 , 3 ] ; track $index ) {
< ui - skeleton class = "h-4 w-full" / >
}
< / u i - c a r d - c o n t e n t >
< / u i - c a r d >
< / div >
} @else if ( service ( ) ) {
< div class = "grid gap-6 md:grid-cols-2" >
<!-- Service Details -->
< ui - card >
< ui - card - header class = "flex flex-col space-y-1.5" >
< ui - card - title > Service Details < / u i - c a r d - t i t l e >
< ui - card - description > Platform service information < / u i - c a r d - d e s c r i p t i o n >
< / u i - c a r d - h e a d e r >
< ui - card - content >
< dl class = "space-y-4" >
< div >
< dt class = "text-sm font-medium text-muted-foreground" > Type < / dt >
< dd class = "text-sm" > { { service ( ) ! . type } } < / dd >
< / div >
< div >
< dt class = "text-sm font-medium text-muted-foreground" > Resource Types < / dt >
< dd class = "text-sm" >
< div class = "flex flex-wrap gap-1 mt-1" >
@for ( type of service ( ) ! . resourceTypes ; track type ) {
< ui - badge variant = "outline" > { { type } } < / u i - b a d g e >
}
< / div >
< / dd >
< / div >
@if ( service ( ) ! . containerId ) {
< div >
< dt class = "text-sm font-medium text-muted-foreground" > Container ID < / dt >
< dd class = "text-sm font-mono" > { { service ( ) ! . containerId ? . slice ( 0 , 12 ) } } < / dd >
< / div >
}
@if ( service ( ) ! . createdAt ) {
< div >
< dt class = "text-sm font-medium text-muted-foreground" > Created < / dt >
< dd class = "text-sm" > { { formatDate ( service ( ) ! . createdAt ! ) } } < / dd >
< / div >
}
< / dl >
< / u i - c a r d - c o n t e n t >
< / u i - c a r d >
<!-- Actions -->
< ui - card >
< ui - card - header class = "flex flex-col space-y-1.5" >
< ui - card - title > Actions < / u i - c a r d - t i t l e >
< ui - card - description > Manage platform service state < / u i - c a r d - d e s c r i p t i o n >
< / u i - c a r d - h e a d e r >
< ui - card - content class = "space-y-4" >
@if ( service ( ) ! . isCore ) {
< p class = "text-sm text-muted-foreground" >
This is a core service managed by Onebox . It cannot be stopped manually .
< / p >
} @else {
< div class = "flex flex-wrap gap-2" >
@if ( service ( ) ! . status === 'stopped' || service ( ) ! . status === 'not-deployed' || service ( ) ! . status === 'failed' ) {
< button uiButton ( click ) = "startService()" [ disabled ] = "actionLoading()" >
@if ( actionLoading ( ) ) {
< svg class = "animate-spin h-4 w-4 mr-2" fill = "none" viewBox = "0 0 24 24" >
< circle class = "opacity-25" cx = "12" cy = "12" r = "10" stroke = "currentColor" stroke-width = "4" > < / circle >
< path class = "opacity-75" fill = "currentColor" d = "M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z" > < / path >
< / svg >
}
Start Service
< / button >
}
@if ( service ( ) ! . status === 'running' ) {
< button uiButton variant = "outline" ( click ) = "stopService()" [ disabled ] = "actionLoading()" >
Stop Service
< / button >
}
< / div >
}
< / u i - c a r d - c o n t e n t >
< / u i - c a r d >
<!-- Resource Stats (only shown when service is running) -->
2025-11-26 16:36:01 +00:00
@if ( service ( ) ! . status === 'running' ) {
< app - container - stats [ stats ] = "stats()" [ showLiveIndicator ] = "true" / >
2025-11-26 14:12:20 +00:00
}
<!-- Service Description -->
< ui - card >
< ui - card - header class = "flex flex-col space-y-1.5" >
< ui - card - title > About { { service ( ) ! . displayName } } < / u i - c a r d - t i t l e >
< / u i - c a r d - h e a d e r >
< ui - card - content >
< p class = "text-sm text-muted-foreground" > { { getServiceDescription ( service ( ) ! . type ) } } < / p >
< / u i - c a r d - c o n t e n t >
< / u i - c a r d >
< / div >
}
< / div >
` ,
} )
export class PlatformServiceDetailComponent implements OnInit {
private route = inject ( ActivatedRoute ) ;
private router = inject ( Router ) ;
private api = inject ( ApiService ) ;
private toast = inject ( ToastService ) ;
private ws = inject ( WebSocketService ) ;
service = signal < IPlatformService | null > ( null ) ;
stats = signal < IContainerStats | null > ( null ) ;
loading = signal ( false ) ;
actionLoading = signal ( false ) ;
private statsInterval : any ;
constructor ( ) {
// Listen for WebSocket stats updates for platform services
effect ( ( ) = > {
const update = this . ws . statsUpdate ( ) ;
const currentService = this . service ( ) ;
// Platform services use "onebox-{type}" as service name in WebSocket
if ( update && currentService && update . serviceName === ` onebox- ${ currentService . type } ` ) {
this . stats . set ( update . stats ) ;
}
} ) ;
}
ngOnInit ( ) : void {
const type = this . route . snapshot . paramMap . get ( 'type' ) as TPlatformServiceType ;
if ( type ) {
this . loadService ( type ) ;
}
}
async loadService ( type : TPlatformServiceType ) : Promise < void > {
this . loading . set ( true ) ;
try {
const response = await this . api . getPlatformService ( type ) ;
if ( response . success && response . data ) {
this . service . set ( response . data ) ;
// Load stats if service is running
if ( response . data . status === 'running' ) {
this . loadStats ( type ) ;
// Start polling stats every 5 seconds
this . startStatsPolling ( type ) ;
}
} else {
this . toast . error ( response . error || 'Platform service not found' ) ;
this . router . navigate ( [ '/services' ] ) ;
}
} catch {
this . toast . error ( 'Failed to load platform service' ) ;
} finally {
this . loading . set ( false ) ;
}
}
async loadStats ( type : TPlatformServiceType ) : Promise < void > {
try {
const response = await this . api . getPlatformServiceStats ( type ) ;
if ( response . success && response . data ) {
this . stats . set ( response . data ) ;
}
} catch {
// Silent fail - stats are optional
}
}
startStatsPolling ( type : TPlatformServiceType ) : void {
// Clear existing interval if any
if ( this . statsInterval ) {
clearInterval ( this . statsInterval ) ;
}
// Poll every 5 seconds
this . statsInterval = setInterval ( ( ) = > {
if ( this . service ( ) ? . status === 'running' ) {
this . loadStats ( type ) ;
}
} , 5000 ) ;
}
getStatusVariant ( status : string ) : 'success' | 'destructive' | 'warning' | 'secondary' {
switch ( status ) {
case 'running' : return 'success' ;
case 'stopped' :
case 'not-deployed' : return 'secondary' ;
case 'failed' : return 'destructive' ;
case 'starting' :
case 'stopping' : return 'warning' ;
default : return 'secondary' ;
}
}
formatDate ( timestamp : number ) : string {
return new Date ( timestamp ) . toLocaleString ( ) ;
}
getServiceDescription ( type : TPlatformServiceType ) : string {
const descriptions : Record < TPlatformServiceType , string > = {
mongodb : 'MongoDB is a document-oriented NoSQL database used for high volume data storage. It stores data in flexible, JSON-like documents.' ,
minio : 'MinIO is a high-performance, S3-compatible object storage service. Use it to store unstructured data like photos, videos, log files, and backups.' ,
redis : 'Redis is an in-memory data structure store, used as a distributed cache, message broker, and key-value database.' ,
postgresql : 'PostgreSQL is a powerful, open-source object-relational database system with over 35 years of active development.' ,
rabbitmq : 'RabbitMQ is a message broker that enables applications to communicate with each other using messages through queues.' ,
caddy : 'Caddy is a powerful, enterprise-ready, open-source web server with automatic HTTPS. It serves as the reverse proxy for Onebox.' ,
} ;
return descriptions [ type ] || 'A platform service managed by Onebox.' ;
}
async startService ( ) : Promise < void > {
const type = this . service ( ) ? . type ;
if ( ! type ) return ;
this . actionLoading . set ( true ) ;
try {
const response = await this . api . startPlatformService ( type ) ;
if ( response . success ) {
this . toast . success ( 'Platform service started' ) ;
this . loadService ( type ) ;
} else {
this . toast . error ( response . error || 'Failed to start platform service' ) ;
}
} catch {
this . toast . error ( 'Failed to start platform service' ) ;
} finally {
this . actionLoading . set ( false ) ;
}
}
async stopService ( ) : Promise < void > {
const type = this . service ( ) ? . type ;
if ( ! type ) return ;
this . actionLoading . set ( true ) ;
try {
const response = await this . api . stopPlatformService ( type ) ;
if ( response . success ) {
this . toast . success ( 'Platform service stopped' ) ;
// Clear stats and stop polling
this . stats . set ( null ) ;
if ( this . statsInterval ) {
clearInterval ( this . statsInterval ) ;
this . statsInterval = null ;
}
this . loadService ( type ) ;
} else {
this . toast . error ( response . error || 'Failed to stop platform service' ) ;
}
} catch {
this . toast . error ( 'Failed to stop platform service' ) ;
} finally {
this . actionLoading . set ( false ) ;
}
}
}