feat: Add Caddy platform service provider with core functionality and integration
This commit is contained in:
@@ -284,13 +284,13 @@ export class OneboxHttpServer {
|
||||
// Platform Services endpoints
|
||||
} else if (path === '/api/platform-services' && method === 'GET') {
|
||||
return await this.handleListPlatformServicesRequest();
|
||||
} else if (path.match(/^\/api\/platform-services\/(mongodb|minio|redis|postgresql|rabbitmq)$/) && method === 'GET') {
|
||||
} else if (path.match(/^\/api\/platform-services\/(mongodb|minio|redis|postgresql|rabbitmq|caddy)$/) && method === 'GET') {
|
||||
const type = path.split('/').pop()! as TPlatformServiceType;
|
||||
return await this.handleGetPlatformServiceRequest(type);
|
||||
} else if (path.match(/^\/api\/platform-services\/(mongodb|minio|redis|postgresql|rabbitmq)\/start$/) && method === 'POST') {
|
||||
} else if (path.match(/^\/api\/platform-services\/(mongodb|minio|redis|postgresql|rabbitmq|caddy)\/start$/) && method === 'POST') {
|
||||
const type = path.split('/')[3] as TPlatformServiceType;
|
||||
return await this.handleStartPlatformServiceRequest(type);
|
||||
} else if (path.match(/^\/api\/platform-services\/(mongodb|minio|redis|postgresql|rabbitmq)\/stop$/) && method === 'POST') {
|
||||
} else if (path.match(/^\/api\/platform-services\/(mongodb|minio|redis|postgresql|rabbitmq|caddy)\/stop$/) && method === 'POST') {
|
||||
const type = path.split('/')[3] as TPlatformServiceType;
|
||||
return await this.handleStopPlatformServiceRequest(type);
|
||||
} else if (path.match(/^\/api\/services\/[^/]+\/platform-resources$/) && method === 'GET') {
|
||||
@@ -1144,6 +1144,7 @@ export class OneboxHttpServer {
|
||||
redis: 6379,
|
||||
postgresql: 5432,
|
||||
rabbitmq: 5672,
|
||||
caddy: 80,
|
||||
};
|
||||
return ports[type] || 0;
|
||||
}
|
||||
@@ -1260,12 +1261,15 @@ export class OneboxHttpServer {
|
||||
// Build response with provider info
|
||||
const result = providers.map((provider) => {
|
||||
const service = platformServices.find((s) => s.type === provider.type);
|
||||
// Check if provider has isCore property (like CaddyProvider)
|
||||
const isCore = 'isCore' in provider && (provider as any).isCore === true;
|
||||
return {
|
||||
type: provider.type,
|
||||
displayName: provider.displayName,
|
||||
resourceTypes: provider.resourceTypes,
|
||||
status: service?.status || 'not-deployed',
|
||||
containerId: service?.containerId,
|
||||
isCore,
|
||||
createdAt: service?.createdAt,
|
||||
updatedAt: service?.updatedAt,
|
||||
};
|
||||
@@ -1362,6 +1366,15 @@ export class OneboxHttpServer {
|
||||
}, 404);
|
||||
}
|
||||
|
||||
// Check if this is a core service that cannot be stopped
|
||||
const isCore = 'isCore' in provider && (provider as any).isCore === true;
|
||||
if (isCore) {
|
||||
return this.jsonResponse({
|
||||
success: false,
|
||||
error: `${provider.displayName} is a core service and cannot be stopped`,
|
||||
}, 400);
|
||||
}
|
||||
|
||||
logger.info(`Stopping platform service: ${type}`);
|
||||
await this.oneboxRef.platformServices.stopPlatformService(type);
|
||||
|
||||
|
||||
@@ -14,6 +14,7 @@ import type {
|
||||
import type { IPlatformServiceProvider } from './providers/base.ts';
|
||||
import { MongoDBProvider } from './providers/mongodb.ts';
|
||||
import { MinioProvider } from './providers/minio.ts';
|
||||
import { CaddyProvider } from './providers/caddy.ts';
|
||||
import { logger } from '../../logging.ts';
|
||||
import { getErrorMessage } from '../../utils/error.ts';
|
||||
import { credentialEncryption } from '../encryption.ts';
|
||||
@@ -37,6 +38,7 @@ export class PlatformServicesManager {
|
||||
// Register providers
|
||||
this.registerProvider(new MongoDBProvider(this.oneboxRef));
|
||||
this.registerProvider(new MinioProvider(this.oneboxRef));
|
||||
this.registerProvider(new CaddyProvider(this.oneboxRef));
|
||||
|
||||
logger.info(`Platform services manager initialized with ${this.providers.size} providers`);
|
||||
}
|
||||
|
||||
110
ts/classes/platform-services/providers/caddy.ts
Normal file
110
ts/classes/platform-services/providers/caddy.ts
Normal file
@@ -0,0 +1,110 @@
|
||||
/**
|
||||
* Caddy Platform Service Provider
|
||||
*
|
||||
* Caddy is a core infrastructure service that provides reverse proxy functionality.
|
||||
* Unlike other platform services:
|
||||
* - It doesn't provision resources for user services
|
||||
* - It's started automatically by Onebox and cannot be stopped by users
|
||||
* - It delegates to the existing CaddyManager for actual operations
|
||||
*/
|
||||
|
||||
import { BasePlatformServiceProvider } from './base.ts';
|
||||
import type {
|
||||
IService,
|
||||
IPlatformResource,
|
||||
IPlatformServiceConfig,
|
||||
IProvisionedResource,
|
||||
IEnvVarMapping,
|
||||
TPlatformServiceType,
|
||||
TPlatformResourceType,
|
||||
} from '../../../types.ts';
|
||||
import { logger } from '../../../logging.ts';
|
||||
import type { Onebox } from '../../onebox.ts';
|
||||
|
||||
export class CaddyProvider extends BasePlatformServiceProvider {
|
||||
readonly type: TPlatformServiceType = 'caddy';
|
||||
readonly displayName = 'Caddy Reverse Proxy';
|
||||
readonly resourceTypes: TPlatformResourceType[] = []; // Caddy doesn't provision resources
|
||||
readonly isCore = true; // Core infrastructure - cannot be stopped by users
|
||||
|
||||
constructor(oneboxRef: Onebox) {
|
||||
super(oneboxRef);
|
||||
}
|
||||
|
||||
getDefaultConfig(): IPlatformServiceConfig {
|
||||
return {
|
||||
image: 'caddy:2-alpine',
|
||||
port: 80,
|
||||
volumes: [],
|
||||
environment: {},
|
||||
};
|
||||
}
|
||||
|
||||
getEnvVarMappings(): IEnvVarMapping[] {
|
||||
// Caddy doesn't inject any env vars into user services
|
||||
return [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Deploy Caddy container - delegates to CaddyManager via reverseProxy
|
||||
*/
|
||||
async deployContainer(): Promise<string> {
|
||||
logger.info('Starting Caddy via reverse proxy manager...');
|
||||
|
||||
// Get the reverse proxy which manages Caddy
|
||||
const reverseProxy = this.oneboxRef.reverseProxy;
|
||||
|
||||
// Start reverse proxy (which starts Caddy)
|
||||
await reverseProxy.startHttp();
|
||||
|
||||
// Get Caddy status to find container ID
|
||||
const status = reverseProxy.getStatus();
|
||||
|
||||
// Update platform service record
|
||||
const platformService = this.oneboxRef.database.getPlatformServiceByType(this.type);
|
||||
if (platformService) {
|
||||
this.oneboxRef.database.updatePlatformService(platformService.id!, {
|
||||
status: 'running',
|
||||
containerId: 'onebox-caddy', // Service name for Swarm services
|
||||
});
|
||||
}
|
||||
|
||||
logger.success('Caddy platform service started');
|
||||
return 'onebox-caddy';
|
||||
}
|
||||
|
||||
/**
|
||||
* Stop Caddy container - NOT ALLOWED for core infrastructure
|
||||
*/
|
||||
async stopContainer(_containerId: string): Promise<void> {
|
||||
throw new Error('Caddy is a core infrastructure service and cannot be stopped');
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if Caddy is healthy via the reverse proxy
|
||||
*/
|
||||
async healthCheck(): Promise<boolean> {
|
||||
try {
|
||||
const reverseProxy = this.oneboxRef.reverseProxy;
|
||||
const status = reverseProxy.getStatus();
|
||||
return status.http.running;
|
||||
} catch (error) {
|
||||
logger.debug(`Caddy health check failed: ${error}`);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Caddy doesn't provision resources for user services
|
||||
*/
|
||||
async provisionResource(_userService: IService): Promise<IProvisionedResource> {
|
||||
throw new Error('Caddy does not provision resources for user services');
|
||||
}
|
||||
|
||||
/**
|
||||
* Caddy doesn't deprovision resources
|
||||
*/
|
||||
async deprovisionResource(_resource: IPlatformResource, _credentials: Record<string, string>): Promise<void> {
|
||||
throw new Error('Caddy does not manage resources for user services');
|
||||
}
|
||||
}
|
||||
@@ -73,7 +73,7 @@ export interface ITokenCreatedResponse {
|
||||
}
|
||||
|
||||
// Platform service types
|
||||
export type TPlatformServiceType = 'mongodb' | 'minio' | 'redis' | 'postgresql' | 'rabbitmq';
|
||||
export type TPlatformServiceType = 'mongodb' | 'minio' | 'redis' | 'postgresql' | 'rabbitmq' | 'caddy';
|
||||
export type TPlatformResourceType = 'database' | 'bucket' | 'cache' | 'queue';
|
||||
export type TPlatformServiceStatus = 'stopped' | 'starting' | 'running' | 'stopping' | 'failed';
|
||||
|
||||
|
||||
@@ -16,7 +16,7 @@ export interface ILoginResponse {
|
||||
}
|
||||
|
||||
// Platform Service Types (defined early for use in ISystemStatus)
|
||||
export type TPlatformServiceType = 'mongodb' | 'minio' | 'redis' | 'postgresql' | 'rabbitmq';
|
||||
export type TPlatformServiceType = 'mongodb' | 'minio' | 'redis' | 'postgresql' | 'rabbitmq' | 'caddy';
|
||||
export type TPlatformServiceStatus = 'not-deployed' | 'stopped' | 'starting' | 'running' | 'stopping' | 'failed';
|
||||
export type TPlatformResourceType = 'database' | 'bucket' | 'cache' | 'queue';
|
||||
|
||||
@@ -220,6 +220,7 @@ export interface IPlatformService {
|
||||
resourceTypes: TPlatformResourceType[];
|
||||
status: TPlatformServiceStatus;
|
||||
containerId?: string;
|
||||
isCore?: boolean; // true for core services like Caddy (cannot be stopped)
|
||||
createdAt?: number;
|
||||
updatedAt?: number;
|
||||
}
|
||||
|
||||
@@ -31,6 +31,9 @@ import {
|
||||
DialogDescriptionComponent,
|
||||
DialogFooterComponent,
|
||||
} from '../../ui/dialog/dialog.component';
|
||||
import { TabsComponent, TabComponent } from '../../ui/tabs/tabs.component';
|
||||
|
||||
type TRegistriesTab = 'onebox' | 'external';
|
||||
|
||||
@Component({
|
||||
selector: 'app-registries',
|
||||
@@ -59,6 +62,8 @@ import {
|
||||
DialogTitleComponent,
|
||||
DialogDescriptionComponent,
|
||||
DialogFooterComponent,
|
||||
TabsComponent,
|
||||
TabComponent,
|
||||
],
|
||||
template: `
|
||||
<div class="space-y-6">
|
||||
@@ -67,8 +72,17 @@ import {
|
||||
<p class="text-muted-foreground">Manage container image registries</p>
|
||||
</div>
|
||||
|
||||
<!-- Onebox Registry Card -->
|
||||
<ui-card class="border-primary/50">
|
||||
<!-- Tabs -->
|
||||
<ui-tabs class="block">
|
||||
<ui-tab [active]="activeTab() === 'onebox'" (tabClick)="setTab('onebox')">Onebox Registry</ui-tab>
|
||||
<ui-tab [active]="activeTab() === 'external'" (tabClick)="setTab('external')">External Registries</ui-tab>
|
||||
</ui-tabs>
|
||||
|
||||
<!-- Tab Content -->
|
||||
@switch (activeTab()) {
|
||||
@case ('onebox') {
|
||||
<!-- Onebox Registry Card -->
|
||||
<ui-card class="border-primary/50">
|
||||
<ui-card-header class="flex flex-row items-start justify-between space-y-0">
|
||||
<div class="space-y-1">
|
||||
<div class="flex items-center gap-2">
|
||||
@@ -118,21 +132,22 @@ import {
|
||||
</div>
|
||||
</div>
|
||||
</ui-card-content>
|
||||
</ui-card>
|
||||
</ui-card>
|
||||
}
|
||||
@case ('external') {
|
||||
<!-- External Registries Section -->
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<h2 class="text-xl font-semibold">External Registries</h2>
|
||||
<p class="text-sm text-muted-foreground">Add credentials for private Docker registries</p>
|
||||
</div>
|
||||
<button uiButton variant="outline" (click)="addDialogOpen.set(true)">
|
||||
Add Registry
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- External Registries Section -->
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<h2 class="text-xl font-semibold">External Registries</h2>
|
||||
<p class="text-sm text-muted-foreground">Add credentials for private Docker registries</p>
|
||||
</div>
|
||||
<button uiButton variant="outline" (click)="addDialogOpen.set(true)">
|
||||
Add Registry
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<ui-card>
|
||||
<ui-card-content class="p-0">
|
||||
<ui-card>
|
||||
<ui-card-content class="p-0">
|
||||
@if (loading() && registries().length === 0) {
|
||||
<div class="p-6 space-y-4">
|
||||
@for (_ of [1,2]; track $index) {
|
||||
@@ -178,8 +193,10 @@ import {
|
||||
</ui-table-body>
|
||||
</ui-table>
|
||||
}
|
||||
</ui-card-content>
|
||||
</ui-card>
|
||||
</ui-card-content>
|
||||
</ui-card>
|
||||
}
|
||||
}
|
||||
</div>
|
||||
|
||||
<!-- Add Registry Dialog -->
|
||||
@@ -229,6 +246,7 @@ export class RegistriesComponent implements OnInit {
|
||||
private api = inject(ApiService);
|
||||
private toast = inject(ToastService);
|
||||
|
||||
activeTab = signal<TRegistriesTab>('onebox');
|
||||
registries = signal<IRegistry[]>([]);
|
||||
loading = signal(false);
|
||||
addDialogOpen = signal(false);
|
||||
@@ -237,6 +255,10 @@ export class RegistriesComponent implements OnInit {
|
||||
|
||||
form: IRegistryCreate = { url: '', username: '', password: '' };
|
||||
|
||||
setTab(tab: TRegistriesTab): void {
|
||||
this.activeTab.set(tab);
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
this.loadRegistries();
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@ import { RouterLink } from '@angular/router';
|
||||
import { ApiService } from '../../core/services/api.service';
|
||||
import { WebSocketService } from '../../core/services/websocket.service';
|
||||
import { ToastService } from '../../core/services/toast.service';
|
||||
import { IService } from '../../core/types/api.types';
|
||||
import { IService, IPlatformService, TPlatformServiceType } from '../../core/types/api.types';
|
||||
import {
|
||||
CardComponent,
|
||||
CardHeaderComponent,
|
||||
@@ -29,6 +29,9 @@ import {
|
||||
DialogDescriptionComponent,
|
||||
DialogFooterComponent,
|
||||
} from '../../ui/dialog/dialog.component';
|
||||
import { TabsComponent, TabComponent } from '../../ui/tabs/tabs.component';
|
||||
|
||||
type TServicesTab = 'user' | 'system';
|
||||
|
||||
@Component({
|
||||
selector: 'app-services-list',
|
||||
@@ -54,6 +57,8 @@ import {
|
||||
DialogTitleComponent,
|
||||
DialogDescriptionComponent,
|
||||
DialogFooterComponent,
|
||||
TabsComponent,
|
||||
TabComponent,
|
||||
],
|
||||
template: `
|
||||
<div class="space-y-6">
|
||||
@@ -61,122 +66,218 @@ import {
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<h1 class="text-3xl font-bold tracking-tight">Services</h1>
|
||||
<p class="text-muted-foreground">Manage your deployed services</p>
|
||||
<p class="text-muted-foreground">Manage your deployed and system services</p>
|
||||
</div>
|
||||
<a routerLink="/services/create">
|
||||
<button uiButton>
|
||||
<svg class="h-4 w-4 mr-2" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M12 4v16m8-8H4" />
|
||||
</svg>
|
||||
Deploy Service
|
||||
</button>
|
||||
</a>
|
||||
@if (activeTab() === 'user') {
|
||||
<a routerLink="/services/create">
|
||||
<button uiButton>
|
||||
<svg class="h-4 w-4 mr-2" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M12 4v16m8-8H4" />
|
||||
</svg>
|
||||
Deploy Service
|
||||
</button>
|
||||
</a>
|
||||
}
|
||||
</div>
|
||||
|
||||
<!-- Services Table -->
|
||||
<ui-card>
|
||||
<ui-card-content class="p-0">
|
||||
@if (loading() && services().length === 0) {
|
||||
<div class="p-6 space-y-4">
|
||||
@for (_ of [1,2,3]; track $index) {
|
||||
<ui-skeleton class="h-12 w-full" />
|
||||
<!-- Tabs -->
|
||||
<ui-tabs class="block">
|
||||
<ui-tab [active]="activeTab() === 'user'" (tabClick)="setTab('user')">User Services</ui-tab>
|
||||
<ui-tab [active]="activeTab() === 'system'" (tabClick)="setTab('system')">System Services</ui-tab>
|
||||
</ui-tabs>
|
||||
|
||||
<!-- Tab Content -->
|
||||
@switch (activeTab()) {
|
||||
@case ('user') {
|
||||
<!-- User Services Table -->
|
||||
<ui-card>
|
||||
<ui-card-content class="p-0">
|
||||
@if (loading() && services().length === 0) {
|
||||
<div class="p-6 space-y-4">
|
||||
@for (_ of [1,2,3]; track $index) {
|
||||
<ui-skeleton class="h-12 w-full" />
|
||||
}
|
||||
</div>
|
||||
} @else if (services().length === 0) {
|
||||
<div class="p-12 text-center">
|
||||
<svg class="mx-auto h-12 w-12 text-muted-foreground" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="1">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M5 12h14M5 12a2 2 0 01-2-2V6a2 2 0 012-2h14a2 2 0 012 2v4a2 2 0 01-2 2M5 12a2 2 0 00-2 2v4a2 2 0 002 2h14a2 2 0 002-2v-4a2 2 0 00-2-2m-2-4h.01M17 16h.01" />
|
||||
</svg>
|
||||
<h3 class="mt-4 text-lg font-semibold">No services</h3>
|
||||
<p class="mt-2 text-sm text-muted-foreground">Get started by deploying your first service.</p>
|
||||
<a routerLink="/services/create" class="mt-4 inline-block">
|
||||
<button uiButton>Deploy Service</button>
|
||||
</a>
|
||||
</div>
|
||||
} @else {
|
||||
<ui-table>
|
||||
<ui-table-header>
|
||||
<ui-table-row>
|
||||
<ui-table-head>Name</ui-table-head>
|
||||
<ui-table-head>Image</ui-table-head>
|
||||
<ui-table-head>Domain</ui-table-head>
|
||||
<ui-table-head>Status</ui-table-head>
|
||||
<ui-table-head class="text-right">Actions</ui-table-head>
|
||||
</ui-table-row>
|
||||
</ui-table-header>
|
||||
<ui-table-body>
|
||||
@for (service of services(); track service.name) {
|
||||
<ui-table-row>
|
||||
<ui-table-cell>
|
||||
<a [routerLink]="['/services', service.name]" class="font-medium hover:underline">
|
||||
{{ service.name }}
|
||||
</a>
|
||||
</ui-table-cell>
|
||||
<ui-table-cell class="text-muted-foreground">{{ service.image }}</ui-table-cell>
|
||||
<ui-table-cell>
|
||||
@if (service.domain) {
|
||||
<a [href]="'https://' + service.domain" target="_blank" class="text-primary hover:underline">
|
||||
{{ service.domain }}
|
||||
</a>
|
||||
} @else {
|
||||
<span class="text-muted-foreground">-</span>
|
||||
}
|
||||
</ui-table-cell>
|
||||
<ui-table-cell>
|
||||
<ui-badge [variant]="getStatusVariant(service.status)">
|
||||
{{ service.status }}
|
||||
</ui-badge>
|
||||
</ui-table-cell>
|
||||
<ui-table-cell class="text-right">
|
||||
<div class="flex items-center justify-end gap-2">
|
||||
@if (service.status === 'stopped' || service.status === 'failed') {
|
||||
<button
|
||||
uiButton
|
||||
variant="outline"
|
||||
size="sm"
|
||||
(click)="startService(service.name)"
|
||||
[disabled]="actionLoading() === service.name"
|
||||
>
|
||||
Start
|
||||
</button>
|
||||
}
|
||||
@if (service.status === 'running') {
|
||||
<button
|
||||
uiButton
|
||||
variant="outline"
|
||||
size="sm"
|
||||
(click)="stopService(service.name)"
|
||||
[disabled]="actionLoading() === service.name"
|
||||
>
|
||||
Stop
|
||||
</button>
|
||||
<button
|
||||
uiButton
|
||||
variant="outline"
|
||||
size="sm"
|
||||
(click)="restartService(service.name)"
|
||||
[disabled]="actionLoading() === service.name"
|
||||
>
|
||||
Restart
|
||||
</button>
|
||||
}
|
||||
<button
|
||||
uiButton
|
||||
variant="destructive"
|
||||
size="sm"
|
||||
(click)="confirmDelete(service)"
|
||||
>
|
||||
Delete
|
||||
</button>
|
||||
</div>
|
||||
</ui-table-cell>
|
||||
</ui-table-row>
|
||||
}
|
||||
</ui-table-body>
|
||||
</ui-table>
|
||||
}
|
||||
</div>
|
||||
} @else if (services().length === 0) {
|
||||
<div class="p-12 text-center">
|
||||
<svg class="mx-auto h-12 w-12 text-muted-foreground" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="1">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M5 12h14M5 12a2 2 0 01-2-2V6a2 2 0 012-2h14a2 2 0 012 2v4a2 2 0 01-2 2M5 12a2 2 0 00-2 2v4a2 2 0 002 2h14a2 2 0 002-2v-4a2 2 0 00-2-2m-2-4h.01M17 16h.01" />
|
||||
</svg>
|
||||
<h3 class="mt-4 text-lg font-semibold">No services</h3>
|
||||
<p class="mt-2 text-sm text-muted-foreground">Get started by deploying your first service.</p>
|
||||
<a routerLink="/services/create" class="mt-4 inline-block">
|
||||
<button uiButton>Deploy Service</button>
|
||||
</a>
|
||||
</div>
|
||||
} @else {
|
||||
<ui-table>
|
||||
<ui-table-header>
|
||||
<ui-table-row>
|
||||
<ui-table-head>Name</ui-table-head>
|
||||
<ui-table-head>Image</ui-table-head>
|
||||
<ui-table-head>Domain</ui-table-head>
|
||||
<ui-table-head>Status</ui-table-head>
|
||||
<ui-table-head class="text-right">Actions</ui-table-head>
|
||||
</ui-table-row>
|
||||
</ui-table-header>
|
||||
<ui-table-body>
|
||||
@for (service of services(); track service.name) {
|
||||
<ui-table-row>
|
||||
<ui-table-cell>
|
||||
<a [routerLink]="['/services', service.name]" class="font-medium hover:underline">
|
||||
{{ service.name }}
|
||||
</a>
|
||||
</ui-table-cell>
|
||||
<ui-table-cell class="text-muted-foreground">{{ service.image }}</ui-table-cell>
|
||||
<ui-table-cell>
|
||||
@if (service.domain) {
|
||||
<a [href]="'https://' + service.domain" target="_blank" class="text-primary hover:underline">
|
||||
{{ service.domain }}
|
||||
</a>
|
||||
} @else {
|
||||
<span class="text-muted-foreground">-</span>
|
||||
}
|
||||
</ui-table-cell>
|
||||
<ui-table-cell>
|
||||
<ui-badge [variant]="getStatusVariant(service.status)">
|
||||
{{ service.status }}
|
||||
</ui-badge>
|
||||
</ui-table-cell>
|
||||
<ui-table-cell class="text-right">
|
||||
<div class="flex items-center justify-end gap-2">
|
||||
@if (service.status === 'stopped' || service.status === 'failed') {
|
||||
<button
|
||||
uiButton
|
||||
variant="outline"
|
||||
size="sm"
|
||||
(click)="startService(service.name)"
|
||||
[disabled]="actionLoading() === service.name"
|
||||
>
|
||||
Start
|
||||
</button>
|
||||
}
|
||||
@if (service.status === 'running') {
|
||||
<button
|
||||
uiButton
|
||||
variant="outline"
|
||||
size="sm"
|
||||
(click)="stopService(service.name)"
|
||||
[disabled]="actionLoading() === service.name"
|
||||
>
|
||||
Stop
|
||||
</button>
|
||||
<button
|
||||
uiButton
|
||||
variant="outline"
|
||||
size="sm"
|
||||
(click)="restartService(service.name)"
|
||||
[disabled]="actionLoading() === service.name"
|
||||
>
|
||||
Restart
|
||||
</button>
|
||||
}
|
||||
<button
|
||||
uiButton
|
||||
variant="destructive"
|
||||
size="sm"
|
||||
(click)="confirmDelete(service)"
|
||||
>
|
||||
Delete
|
||||
</button>
|
||||
</div>
|
||||
</ui-table-cell>
|
||||
</ui-table-row>
|
||||
}
|
||||
</ui-table-body>
|
||||
</ui-table>
|
||||
}
|
||||
</ui-card-content>
|
||||
</ui-card>
|
||||
</ui-card-content>
|
||||
</ui-card>
|
||||
}
|
||||
@case ('system') {
|
||||
<!-- System Services Table -->
|
||||
<ui-card>
|
||||
<ui-card-content class="p-0">
|
||||
@if (platformLoading() && platformServices().length === 0) {
|
||||
<div class="p-6 space-y-4">
|
||||
@for (_ of [1,2,3]; track $index) {
|
||||
<ui-skeleton class="h-12 w-full" />
|
||||
}
|
||||
</div>
|
||||
} @else if (platformServices().length === 0) {
|
||||
<div class="p-12 text-center">
|
||||
<svg class="mx-auto h-12 w-12 text-muted-foreground" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="1">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M5 12h14M5 12a2 2 0 01-2-2V6a2 2 0 012-2h14a2 2 0 012 2v4a2 2 0 01-2 2M5 12a2 2 0 00-2 2v4a2 2 0 002 2h14a2 2 0 002-2v-4a2 2 0 00-2-2m-2-4h.01M17 16h.01" />
|
||||
</svg>
|
||||
<h3 class="mt-4 text-lg font-semibold">No system services</h3>
|
||||
<p class="mt-2 text-sm text-muted-foreground">System services will appear here once configured.</p>
|
||||
</div>
|
||||
} @else {
|
||||
<ui-table>
|
||||
<ui-table-header>
|
||||
<ui-table-row>
|
||||
<ui-table-head>Service</ui-table-head>
|
||||
<ui-table-head>Type</ui-table-head>
|
||||
<ui-table-head>Status</ui-table-head>
|
||||
<ui-table-head class="text-right">Actions</ui-table-head>
|
||||
</ui-table-row>
|
||||
</ui-table-header>
|
||||
<ui-table-body>
|
||||
@for (service of platformServices(); track service.type) {
|
||||
<ui-table-row>
|
||||
<ui-table-cell>
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="font-medium">{{ service.displayName }}</span>
|
||||
@if (service.isCore) {
|
||||
<ui-badge variant="outline">Core</ui-badge>
|
||||
}
|
||||
</div>
|
||||
</ui-table-cell>
|
||||
<ui-table-cell class="text-muted-foreground">{{ service.type }}</ui-table-cell>
|
||||
<ui-table-cell>
|
||||
<ui-badge [variant]="getPlatformStatusVariant(service.status)">
|
||||
{{ service.status }}
|
||||
</ui-badge>
|
||||
</ui-table-cell>
|
||||
<ui-table-cell class="text-right">
|
||||
<div class="flex items-center justify-end gap-2">
|
||||
@if (service.isCore) {
|
||||
<span class="text-xs text-muted-foreground">Managed by Onebox</span>
|
||||
} @else {
|
||||
@if (service.status === 'stopped' || service.status === 'not-deployed' || service.status === 'failed') {
|
||||
<button
|
||||
uiButton
|
||||
variant="outline"
|
||||
size="sm"
|
||||
(click)="startPlatformService(service.type)"
|
||||
[disabled]="platformActionLoading() === service.type"
|
||||
>
|
||||
Start
|
||||
</button>
|
||||
}
|
||||
@if (service.status === 'running') {
|
||||
<button
|
||||
uiButton
|
||||
variant="outline"
|
||||
size="sm"
|
||||
(click)="stopPlatformService(service.type)"
|
||||
[disabled]="platformActionLoading() === service.type"
|
||||
>
|
||||
Stop
|
||||
</button>
|
||||
}
|
||||
}
|
||||
</div>
|
||||
</ui-table-cell>
|
||||
</ui-table-row>
|
||||
}
|
||||
</ui-table-body>
|
||||
</ui-table>
|
||||
}
|
||||
</ui-card-content>
|
||||
</ui-card>
|
||||
}
|
||||
}
|
||||
</div>
|
||||
|
||||
<!-- Delete Confirmation Dialog -->
|
||||
@@ -201,12 +302,21 @@ export class ServicesListComponent implements OnInit {
|
||||
private ws = inject(WebSocketService);
|
||||
private toast = inject(ToastService);
|
||||
|
||||
// Tab state
|
||||
activeTab = signal<TServicesTab>('user');
|
||||
|
||||
// User services
|
||||
services = signal<IService[]>([]);
|
||||
loading = signal(false);
|
||||
actionLoading = signal<string | null>(null);
|
||||
deleteDialogOpen = signal(false);
|
||||
serviceToDelete = signal<IService | null>(null);
|
||||
|
||||
// Platform services
|
||||
platformServices = signal<IPlatformService[]>([]);
|
||||
platformLoading = signal(false);
|
||||
platformActionLoading = signal<TPlatformServiceType | null>(null);
|
||||
|
||||
constructor() {
|
||||
// React to WebSocket updates
|
||||
effect(() => {
|
||||
@@ -220,6 +330,11 @@ export class ServicesListComponent implements OnInit {
|
||||
|
||||
ngOnInit(): void {
|
||||
this.loadServices();
|
||||
this.loadPlatformServices();
|
||||
}
|
||||
|
||||
setTab(tab: TServicesTab): void {
|
||||
this.activeTab.set(tab);
|
||||
}
|
||||
|
||||
async loadServices(): Promise<void> {
|
||||
@@ -329,4 +444,70 @@ export class ServicesListComponent implements OnInit {
|
||||
this.serviceToDelete.set(null);
|
||||
}
|
||||
}
|
||||
|
||||
// Platform Service Methods
|
||||
async loadPlatformServices(): Promise<void> {
|
||||
this.platformLoading.set(true);
|
||||
try {
|
||||
const response = await this.api.getPlatformServices();
|
||||
if (response.success && response.data) {
|
||||
this.platformServices.set(response.data);
|
||||
}
|
||||
} catch {
|
||||
this.toast.error('Failed to load system services');
|
||||
} finally {
|
||||
this.platformLoading.set(false);
|
||||
}
|
||||
}
|
||||
|
||||
getPlatformStatusVariant(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';
|
||||
}
|
||||
}
|
||||
|
||||
async startPlatformService(type: TPlatformServiceType): Promise<void> {
|
||||
this.platformActionLoading.set(type);
|
||||
try {
|
||||
const response = await this.api.startPlatformService(type);
|
||||
if (response.success) {
|
||||
this.toast.success(`Platform service "${type}" started`);
|
||||
this.loadPlatformServices();
|
||||
} else {
|
||||
this.toast.error(response.error || 'Failed to start platform service');
|
||||
}
|
||||
} catch {
|
||||
this.toast.error('Failed to start platform service');
|
||||
} finally {
|
||||
this.platformActionLoading.set(null);
|
||||
}
|
||||
}
|
||||
|
||||
async stopPlatformService(type: TPlatformServiceType): Promise<void> {
|
||||
this.platformActionLoading.set(type);
|
||||
try {
|
||||
const response = await this.api.stopPlatformService(type);
|
||||
if (response.success) {
|
||||
this.toast.success(`Platform service "${type}" stopped`);
|
||||
this.loadPlatformServices();
|
||||
} else {
|
||||
this.toast.error(response.error || 'Failed to stop platform service');
|
||||
}
|
||||
} catch {
|
||||
this.toast.error('Failed to stop platform service');
|
||||
} finally {
|
||||
this.platformActionLoading.set(null);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user