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
|
// Platform Services endpoints
|
||||||
} else if (path === '/api/platform-services' && method === 'GET') {
|
} else if (path === '/api/platform-services' && method === 'GET') {
|
||||||
return await this.handleListPlatformServicesRequest();
|
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;
|
const type = path.split('/').pop()! as TPlatformServiceType;
|
||||||
return await this.handleGetPlatformServiceRequest(type);
|
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;
|
const type = path.split('/')[3] as TPlatformServiceType;
|
||||||
return await this.handleStartPlatformServiceRequest(type);
|
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;
|
const type = path.split('/')[3] as TPlatformServiceType;
|
||||||
return await this.handleStopPlatformServiceRequest(type);
|
return await this.handleStopPlatformServiceRequest(type);
|
||||||
} else if (path.match(/^\/api\/services\/[^/]+\/platform-resources$/) && method === 'GET') {
|
} else if (path.match(/^\/api\/services\/[^/]+\/platform-resources$/) && method === 'GET') {
|
||||||
@@ -1144,6 +1144,7 @@ export class OneboxHttpServer {
|
|||||||
redis: 6379,
|
redis: 6379,
|
||||||
postgresql: 5432,
|
postgresql: 5432,
|
||||||
rabbitmq: 5672,
|
rabbitmq: 5672,
|
||||||
|
caddy: 80,
|
||||||
};
|
};
|
||||||
return ports[type] || 0;
|
return ports[type] || 0;
|
||||||
}
|
}
|
||||||
@@ -1260,12 +1261,15 @@ export class OneboxHttpServer {
|
|||||||
// Build response with provider info
|
// Build response with provider info
|
||||||
const result = providers.map((provider) => {
|
const result = providers.map((provider) => {
|
||||||
const service = platformServices.find((s) => s.type === provider.type);
|
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 {
|
return {
|
||||||
type: provider.type,
|
type: provider.type,
|
||||||
displayName: provider.displayName,
|
displayName: provider.displayName,
|
||||||
resourceTypes: provider.resourceTypes,
|
resourceTypes: provider.resourceTypes,
|
||||||
status: service?.status || 'not-deployed',
|
status: service?.status || 'not-deployed',
|
||||||
containerId: service?.containerId,
|
containerId: service?.containerId,
|
||||||
|
isCore,
|
||||||
createdAt: service?.createdAt,
|
createdAt: service?.createdAt,
|
||||||
updatedAt: service?.updatedAt,
|
updatedAt: service?.updatedAt,
|
||||||
};
|
};
|
||||||
@@ -1362,6 +1366,15 @@ export class OneboxHttpServer {
|
|||||||
}, 404);
|
}, 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}`);
|
logger.info(`Stopping platform service: ${type}`);
|
||||||
await this.oneboxRef.platformServices.stopPlatformService(type);
|
await this.oneboxRef.platformServices.stopPlatformService(type);
|
||||||
|
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import type {
|
|||||||
import type { IPlatformServiceProvider } from './providers/base.ts';
|
import type { IPlatformServiceProvider } from './providers/base.ts';
|
||||||
import { MongoDBProvider } from './providers/mongodb.ts';
|
import { MongoDBProvider } from './providers/mongodb.ts';
|
||||||
import { MinioProvider } from './providers/minio.ts';
|
import { MinioProvider } from './providers/minio.ts';
|
||||||
|
import { CaddyProvider } from './providers/caddy.ts';
|
||||||
import { logger } from '../../logging.ts';
|
import { logger } from '../../logging.ts';
|
||||||
import { getErrorMessage } from '../../utils/error.ts';
|
import { getErrorMessage } from '../../utils/error.ts';
|
||||||
import { credentialEncryption } from '../encryption.ts';
|
import { credentialEncryption } from '../encryption.ts';
|
||||||
@@ -37,6 +38,7 @@ export class PlatformServicesManager {
|
|||||||
// Register providers
|
// Register providers
|
||||||
this.registerProvider(new MongoDBProvider(this.oneboxRef));
|
this.registerProvider(new MongoDBProvider(this.oneboxRef));
|
||||||
this.registerProvider(new MinioProvider(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`);
|
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
|
// 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 TPlatformResourceType = 'database' | 'bucket' | 'cache' | 'queue';
|
||||||
export type TPlatformServiceStatus = 'stopped' | 'starting' | 'running' | 'stopping' | 'failed';
|
export type TPlatformServiceStatus = 'stopped' | 'starting' | 'running' | 'stopping' | 'failed';
|
||||||
|
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ export interface ILoginResponse {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Platform Service Types (defined early for use in ISystemStatus)
|
// 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 TPlatformServiceStatus = 'not-deployed' | 'stopped' | 'starting' | 'running' | 'stopping' | 'failed';
|
||||||
export type TPlatformResourceType = 'database' | 'bucket' | 'cache' | 'queue';
|
export type TPlatformResourceType = 'database' | 'bucket' | 'cache' | 'queue';
|
||||||
|
|
||||||
@@ -220,6 +220,7 @@ export interface IPlatformService {
|
|||||||
resourceTypes: TPlatformResourceType[];
|
resourceTypes: TPlatformResourceType[];
|
||||||
status: TPlatformServiceStatus;
|
status: TPlatformServiceStatus;
|
||||||
containerId?: string;
|
containerId?: string;
|
||||||
|
isCore?: boolean; // true for core services like Caddy (cannot be stopped)
|
||||||
createdAt?: number;
|
createdAt?: number;
|
||||||
updatedAt?: number;
|
updatedAt?: number;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -31,6 +31,9 @@ import {
|
|||||||
DialogDescriptionComponent,
|
DialogDescriptionComponent,
|
||||||
DialogFooterComponent,
|
DialogFooterComponent,
|
||||||
} from '../../ui/dialog/dialog.component';
|
} from '../../ui/dialog/dialog.component';
|
||||||
|
import { TabsComponent, TabComponent } from '../../ui/tabs/tabs.component';
|
||||||
|
|
||||||
|
type TRegistriesTab = 'onebox' | 'external';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-registries',
|
selector: 'app-registries',
|
||||||
@@ -59,6 +62,8 @@ import {
|
|||||||
DialogTitleComponent,
|
DialogTitleComponent,
|
||||||
DialogDescriptionComponent,
|
DialogDescriptionComponent,
|
||||||
DialogFooterComponent,
|
DialogFooterComponent,
|
||||||
|
TabsComponent,
|
||||||
|
TabComponent,
|
||||||
],
|
],
|
||||||
template: `
|
template: `
|
||||||
<div class="space-y-6">
|
<div class="space-y-6">
|
||||||
@@ -67,8 +72,17 @@ import {
|
|||||||
<p class="text-muted-foreground">Manage container image registries</p>
|
<p class="text-muted-foreground">Manage container image registries</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Onebox Registry Card -->
|
<!-- Tabs -->
|
||||||
<ui-card class="border-primary/50">
|
<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">
|
<ui-card-header class="flex flex-row items-start justify-between space-y-0">
|
||||||
<div class="space-y-1">
|
<div class="space-y-1">
|
||||||
<div class="flex items-center gap-2">
|
<div class="flex items-center gap-2">
|
||||||
@@ -118,21 +132,22 @@ import {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</ui-card-content>
|
</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 -->
|
<ui-card>
|
||||||
<div class="flex items-center justify-between">
|
<ui-card-content class="p-0">
|
||||||
<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">
|
|
||||||
@if (loading() && registries().length === 0) {
|
@if (loading() && registries().length === 0) {
|
||||||
<div class="p-6 space-y-4">
|
<div class="p-6 space-y-4">
|
||||||
@for (_ of [1,2]; track $index) {
|
@for (_ of [1,2]; track $index) {
|
||||||
@@ -178,8 +193,10 @@ import {
|
|||||||
</ui-table-body>
|
</ui-table-body>
|
||||||
</ui-table>
|
</ui-table>
|
||||||
}
|
}
|
||||||
</ui-card-content>
|
</ui-card-content>
|
||||||
</ui-card>
|
</ui-card>
|
||||||
|
}
|
||||||
|
}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Add Registry Dialog -->
|
<!-- Add Registry Dialog -->
|
||||||
@@ -229,6 +246,7 @@ export class RegistriesComponent implements OnInit {
|
|||||||
private api = inject(ApiService);
|
private api = inject(ApiService);
|
||||||
private toast = inject(ToastService);
|
private toast = inject(ToastService);
|
||||||
|
|
||||||
|
activeTab = signal<TRegistriesTab>('onebox');
|
||||||
registries = signal<IRegistry[]>([]);
|
registries = signal<IRegistry[]>([]);
|
||||||
loading = signal(false);
|
loading = signal(false);
|
||||||
addDialogOpen = signal(false);
|
addDialogOpen = signal(false);
|
||||||
@@ -237,6 +255,10 @@ export class RegistriesComponent implements OnInit {
|
|||||||
|
|
||||||
form: IRegistryCreate = { url: '', username: '', password: '' };
|
form: IRegistryCreate = { url: '', username: '', password: '' };
|
||||||
|
|
||||||
|
setTab(tab: TRegistriesTab): void {
|
||||||
|
this.activeTab.set(tab);
|
||||||
|
}
|
||||||
|
|
||||||
ngOnInit(): void {
|
ngOnInit(): void {
|
||||||
this.loadRegistries();
|
this.loadRegistries();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { RouterLink } from '@angular/router';
|
|||||||
import { ApiService } from '../../core/services/api.service';
|
import { ApiService } from '../../core/services/api.service';
|
||||||
import { WebSocketService } from '../../core/services/websocket.service';
|
import { WebSocketService } from '../../core/services/websocket.service';
|
||||||
import { ToastService } from '../../core/services/toast.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 {
|
import {
|
||||||
CardComponent,
|
CardComponent,
|
||||||
CardHeaderComponent,
|
CardHeaderComponent,
|
||||||
@@ -29,6 +29,9 @@ import {
|
|||||||
DialogDescriptionComponent,
|
DialogDescriptionComponent,
|
||||||
DialogFooterComponent,
|
DialogFooterComponent,
|
||||||
} from '../../ui/dialog/dialog.component';
|
} from '../../ui/dialog/dialog.component';
|
||||||
|
import { TabsComponent, TabComponent } from '../../ui/tabs/tabs.component';
|
||||||
|
|
||||||
|
type TServicesTab = 'user' | 'system';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-services-list',
|
selector: 'app-services-list',
|
||||||
@@ -54,6 +57,8 @@ import {
|
|||||||
DialogTitleComponent,
|
DialogTitleComponent,
|
||||||
DialogDescriptionComponent,
|
DialogDescriptionComponent,
|
||||||
DialogFooterComponent,
|
DialogFooterComponent,
|
||||||
|
TabsComponent,
|
||||||
|
TabComponent,
|
||||||
],
|
],
|
||||||
template: `
|
template: `
|
||||||
<div class="space-y-6">
|
<div class="space-y-6">
|
||||||
@@ -61,122 +66,218 @@ import {
|
|||||||
<div class="flex items-center justify-between">
|
<div class="flex items-center justify-between">
|
||||||
<div>
|
<div>
|
||||||
<h1 class="text-3xl font-bold tracking-tight">Services</h1>
|
<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>
|
</div>
|
||||||
<a routerLink="/services/create">
|
@if (activeTab() === 'user') {
|
||||||
<button uiButton>
|
<a routerLink="/services/create">
|
||||||
<svg class="h-4 w-4 mr-2" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
|
<button uiButton>
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" d="M12 4v16m8-8H4" />
|
<svg class="h-4 w-4 mr-2" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
|
||||||
</svg>
|
<path stroke-linecap="round" stroke-linejoin="round" d="M12 4v16m8-8H4" />
|
||||||
Deploy Service
|
</svg>
|
||||||
</button>
|
Deploy Service
|
||||||
</a>
|
</button>
|
||||||
|
</a>
|
||||||
|
}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Services Table -->
|
<!-- Tabs -->
|
||||||
<ui-card>
|
<ui-tabs class="block">
|
||||||
<ui-card-content class="p-0">
|
<ui-tab [active]="activeTab() === 'user'" (tabClick)="setTab('user')">User Services</ui-tab>
|
||||||
@if (loading() && services().length === 0) {
|
<ui-tab [active]="activeTab() === 'system'" (tabClick)="setTab('system')">System Services</ui-tab>
|
||||||
<div class="p-6 space-y-4">
|
</ui-tabs>
|
||||||
@for (_ of [1,2,3]; track $index) {
|
|
||||||
<ui-skeleton class="h-12 w-full" />
|
<!-- 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>
|
</ui-card-content>
|
||||||
} @else if (services().length === 0) {
|
</ui-card>
|
||||||
<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">
|
@case ('system') {
|
||||||
<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" />
|
<!-- System Services Table -->
|
||||||
</svg>
|
<ui-card>
|
||||||
<h3 class="mt-4 text-lg font-semibold">No services</h3>
|
<ui-card-content class="p-0">
|
||||||
<p class="mt-2 text-sm text-muted-foreground">Get started by deploying your first service.</p>
|
@if (platformLoading() && platformServices().length === 0) {
|
||||||
<a routerLink="/services/create" class="mt-4 inline-block">
|
<div class="p-6 space-y-4">
|
||||||
<button uiButton>Deploy Service</button>
|
@for (_ of [1,2,3]; track $index) {
|
||||||
</a>
|
<ui-skeleton class="h-12 w-full" />
|
||||||
</div>
|
}
|
||||||
} @else {
|
</div>
|
||||||
<ui-table>
|
} @else if (platformServices().length === 0) {
|
||||||
<ui-table-header>
|
<div class="p-12 text-center">
|
||||||
<ui-table-row>
|
<svg class="mx-auto h-12 w-12 text-muted-foreground" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="1">
|
||||||
<ui-table-head>Name</ui-table-head>
|
<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" />
|
||||||
<ui-table-head>Image</ui-table-head>
|
</svg>
|
||||||
<ui-table-head>Domain</ui-table-head>
|
<h3 class="mt-4 text-lg font-semibold">No system services</h3>
|
||||||
<ui-table-head>Status</ui-table-head>
|
<p class="mt-2 text-sm text-muted-foreground">System services will appear here once configured.</p>
|
||||||
<ui-table-head class="text-right">Actions</ui-table-head>
|
</div>
|
||||||
</ui-table-row>
|
} @else {
|
||||||
</ui-table-header>
|
<ui-table>
|
||||||
<ui-table-body>
|
<ui-table-header>
|
||||||
@for (service of services(); track service.name) {
|
<ui-table-row>
|
||||||
<ui-table-row>
|
<ui-table-head>Service</ui-table-head>
|
||||||
<ui-table-cell>
|
<ui-table-head>Type</ui-table-head>
|
||||||
<a [routerLink]="['/services', service.name]" class="font-medium hover:underline">
|
<ui-table-head>Status</ui-table-head>
|
||||||
{{ service.name }}
|
<ui-table-head class="text-right">Actions</ui-table-head>
|
||||||
</a>
|
</ui-table-row>
|
||||||
</ui-table-cell>
|
</ui-table-header>
|
||||||
<ui-table-cell class="text-muted-foreground">{{ service.image }}</ui-table-cell>
|
<ui-table-body>
|
||||||
<ui-table-cell>
|
@for (service of platformServices(); track service.type) {
|
||||||
@if (service.domain) {
|
<ui-table-row>
|
||||||
<a [href]="'https://' + service.domain" target="_blank" class="text-primary hover:underline">
|
<ui-table-cell>
|
||||||
{{ service.domain }}
|
<div class="flex items-center gap-2">
|
||||||
</a>
|
<span class="font-medium">{{ service.displayName }}</span>
|
||||||
} @else {
|
@if (service.isCore) {
|
||||||
<span class="text-muted-foreground">-</span>
|
<ui-badge variant="outline">Core</ui-badge>
|
||||||
}
|
}
|
||||||
</ui-table-cell>
|
</div>
|
||||||
<ui-table-cell>
|
</ui-table-cell>
|
||||||
<ui-badge [variant]="getStatusVariant(service.status)">
|
<ui-table-cell class="text-muted-foreground">{{ service.type }}</ui-table-cell>
|
||||||
{{ service.status }}
|
<ui-table-cell>
|
||||||
</ui-badge>
|
<ui-badge [variant]="getPlatformStatusVariant(service.status)">
|
||||||
</ui-table-cell>
|
{{ service.status }}
|
||||||
<ui-table-cell class="text-right">
|
</ui-badge>
|
||||||
<div class="flex items-center justify-end gap-2">
|
</ui-table-cell>
|
||||||
@if (service.status === 'stopped' || service.status === 'failed') {
|
<ui-table-cell class="text-right">
|
||||||
<button
|
<div class="flex items-center justify-end gap-2">
|
||||||
uiButton
|
@if (service.isCore) {
|
||||||
variant="outline"
|
<span class="text-xs text-muted-foreground">Managed by Onebox</span>
|
||||||
size="sm"
|
} @else {
|
||||||
(click)="startService(service.name)"
|
@if (service.status === 'stopped' || service.status === 'not-deployed' || service.status === 'failed') {
|
||||||
[disabled]="actionLoading() === service.name"
|
<button
|
||||||
>
|
uiButton
|
||||||
Start
|
variant="outline"
|
||||||
</button>
|
size="sm"
|
||||||
}
|
(click)="startPlatformService(service.type)"
|
||||||
@if (service.status === 'running') {
|
[disabled]="platformActionLoading() === service.type"
|
||||||
<button
|
>
|
||||||
uiButton
|
Start
|
||||||
variant="outline"
|
</button>
|
||||||
size="sm"
|
}
|
||||||
(click)="stopService(service.name)"
|
@if (service.status === 'running') {
|
||||||
[disabled]="actionLoading() === service.name"
|
<button
|
||||||
>
|
uiButton
|
||||||
Stop
|
variant="outline"
|
||||||
</button>
|
size="sm"
|
||||||
<button
|
(click)="stopPlatformService(service.type)"
|
||||||
uiButton
|
[disabled]="platformActionLoading() === service.type"
|
||||||
variant="outline"
|
>
|
||||||
size="sm"
|
Stop
|
||||||
(click)="restartService(service.name)"
|
</button>
|
||||||
[disabled]="actionLoading() === service.name"
|
}
|
||||||
>
|
}
|
||||||
Restart
|
</div>
|
||||||
</button>
|
</ui-table-cell>
|
||||||
}
|
</ui-table-row>
|
||||||
<button
|
}
|
||||||
uiButton
|
</ui-table-body>
|
||||||
variant="destructive"
|
</ui-table>
|
||||||
size="sm"
|
}
|
||||||
(click)="confirmDelete(service)"
|
</ui-card-content>
|
||||||
>
|
</ui-card>
|
||||||
Delete
|
}
|
||||||
</button>
|
}
|
||||||
</div>
|
|
||||||
</ui-table-cell>
|
|
||||||
</ui-table-row>
|
|
||||||
}
|
|
||||||
</ui-table-body>
|
|
||||||
</ui-table>
|
|
||||||
}
|
|
||||||
</ui-card-content>
|
|
||||||
</ui-card>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Delete Confirmation Dialog -->
|
<!-- Delete Confirmation Dialog -->
|
||||||
@@ -201,12 +302,21 @@ export class ServicesListComponent implements OnInit {
|
|||||||
private ws = inject(WebSocketService);
|
private ws = inject(WebSocketService);
|
||||||
private toast = inject(ToastService);
|
private toast = inject(ToastService);
|
||||||
|
|
||||||
|
// Tab state
|
||||||
|
activeTab = signal<TServicesTab>('user');
|
||||||
|
|
||||||
|
// User services
|
||||||
services = signal<IService[]>([]);
|
services = signal<IService[]>([]);
|
||||||
loading = signal(false);
|
loading = signal(false);
|
||||||
actionLoading = signal<string | null>(null);
|
actionLoading = signal<string | null>(null);
|
||||||
deleteDialogOpen = signal(false);
|
deleteDialogOpen = signal(false);
|
||||||
serviceToDelete = signal<IService | null>(null);
|
serviceToDelete = signal<IService | null>(null);
|
||||||
|
|
||||||
|
// Platform services
|
||||||
|
platformServices = signal<IPlatformService[]>([]);
|
||||||
|
platformLoading = signal(false);
|
||||||
|
platformActionLoading = signal<TPlatformServiceType | null>(null);
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
// React to WebSocket updates
|
// React to WebSocket updates
|
||||||
effect(() => {
|
effect(() => {
|
||||||
@@ -220,6 +330,11 @@ export class ServicesListComponent implements OnInit {
|
|||||||
|
|
||||||
ngOnInit(): void {
|
ngOnInit(): void {
|
||||||
this.loadServices();
|
this.loadServices();
|
||||||
|
this.loadPlatformServices();
|
||||||
|
}
|
||||||
|
|
||||||
|
setTab(tab: TServicesTab): void {
|
||||||
|
this.activeTab.set(tab);
|
||||||
}
|
}
|
||||||
|
|
||||||
async loadServices(): Promise<void> {
|
async loadServices(): Promise<void> {
|
||||||
@@ -329,4 +444,70 @@ export class ServicesListComponent implements OnInit {
|
|||||||
this.serviceToDelete.set(null);
|
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