feat: Add Caddy platform service provider with core functionality and integration
This commit is contained in:
@@ -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