2025-11-24 19:52:35 +00:00
|
|
|
import { Component, inject, signal, OnInit, OnDestroy, ViewChild, ElementRef, effect } from '@angular/core';
|
2025-11-24 01:31:15 +00:00
|
|
|
import { ActivatedRoute, Router, RouterLink } from '@angular/router';
|
2025-11-24 19:52:35 +00:00
|
|
|
import { FormsModule } from '@angular/forms';
|
|
|
|
|
import { ApiService } from '../../core/services/api.service';
|
2025-11-24 01:31:15 +00:00
|
|
|
import { ToastService } from '../../core/services/toast.service';
|
2025-11-24 19:52:35 +00:00
|
|
|
import { LogStreamService } from '../../core/services/log-stream.service';
|
|
|
|
|
import { IService, IServiceUpdate } from '../../core/types/api.types';
|
|
|
|
|
import {
|
|
|
|
|
CardComponent,
|
|
|
|
|
CardHeaderComponent,
|
|
|
|
|
CardTitleComponent,
|
|
|
|
|
CardDescriptionComponent,
|
|
|
|
|
CardContentComponent,
|
|
|
|
|
CardFooterComponent,
|
|
|
|
|
} from '../../ui/card/card.component';
|
|
|
|
|
import { ButtonComponent } from '../../ui/button/button.component';
|
|
|
|
|
import { BadgeComponent } from '../../ui/badge/badge.component';
|
|
|
|
|
import { InputComponent } from '../../ui/input/input.component';
|
|
|
|
|
import { LabelComponent } from '../../ui/label/label.component';
|
|
|
|
|
import { SkeletonComponent } from '../../ui/skeleton/skeleton.component';
|
|
|
|
|
import { SeparatorComponent } from '../../ui/separator/separator.component';
|
|
|
|
|
import {
|
|
|
|
|
DialogComponent,
|
|
|
|
|
DialogHeaderComponent,
|
|
|
|
|
DialogTitleComponent,
|
|
|
|
|
DialogDescriptionComponent,
|
|
|
|
|
DialogFooterComponent,
|
|
|
|
|
} from '../../ui/dialog/dialog.component';
|
2025-11-18 00:03:24 +00:00
|
|
|
|
|
|
|
|
@Component({
|
|
|
|
|
selector: 'app-service-detail',
|
|
|
|
|
standalone: true,
|
2025-11-24 19:52:35 +00:00
|
|
|
imports: [
|
|
|
|
|
FormsModule,
|
|
|
|
|
RouterLink,
|
|
|
|
|
CardComponent,
|
|
|
|
|
CardHeaderComponent,
|
|
|
|
|
CardTitleComponent,
|
|
|
|
|
CardDescriptionComponent,
|
|
|
|
|
CardContentComponent,
|
|
|
|
|
CardFooterComponent,
|
|
|
|
|
ButtonComponent,
|
|
|
|
|
BadgeComponent,
|
|
|
|
|
InputComponent,
|
|
|
|
|
LabelComponent,
|
|
|
|
|
SkeletonComponent,
|
|
|
|
|
SeparatorComponent,
|
|
|
|
|
DialogComponent,
|
|
|
|
|
DialogHeaderComponent,
|
|
|
|
|
DialogTitleComponent,
|
|
|
|
|
DialogDescriptionComponent,
|
|
|
|
|
DialogFooterComponent,
|
|
|
|
|
],
|
2025-11-18 00:03:24 +00:00
|
|
|
template: `
|
2025-11-24 19:52:35 +00:00
|
|
|
<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()!.name }}</h1>
|
|
|
|
|
<ui-badge [variant]="getStatusVariant(service()!.status)">{{ service()!.status }}</ui-badge>
|
2025-11-18 00:03:24 +00:00
|
|
|
</div>
|
2025-11-24 19:52:35 +00:00
|
|
|
}
|
|
|
|
|
</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" />
|
|
|
|
|
</ui-card-header>
|
|
|
|
|
<ui-card-content class="space-y-4">
|
|
|
|
|
@for (_ of [1,2,3,4]; track $index) {
|
|
|
|
|
<ui-skeleton class="h-4 w-full" />
|
2025-11-24 01:31:15 +00:00
|
|
|
}
|
2025-11-24 19:52:35 +00:00
|
|
|
</ui-card-content>
|
|
|
|
|
</ui-card>
|
|
|
|
|
</div>
|
|
|
|
|
} @else if (service()) {
|
|
|
|
|
<div class="grid gap-6 md:grid-cols-2">
|
|
|
|
|
<!-- Service Details -->
|
|
|
|
|
<ui-card>
|
|
|
|
|
<ui-card-header class="flex flex-row items-center justify-between">
|
|
|
|
|
<ui-card-title>Service Details</ui-card-title>
|
|
|
|
|
@if (!editMode()) {
|
|
|
|
|
<button uiButton variant="outline" size="sm" (click)="editMode.set(true)">Edit</button>
|
2025-11-24 01:31:15 +00:00
|
|
|
}
|
2025-11-24 19:52:35 +00:00
|
|
|
</ui-card-header>
|
|
|
|
|
<ui-card-content>
|
|
|
|
|
@if (editMode()) {
|
|
|
|
|
<form (ngSubmit)="saveChanges()" class="space-y-4">
|
|
|
|
|
<div class="space-y-2">
|
|
|
|
|
<label uiLabel>Image</label>
|
|
|
|
|
<input uiInput [(ngModel)]="editForm.image" name="image" />
|
|
|
|
|
</div>
|
|
|
|
|
<div class="space-y-2">
|
|
|
|
|
<label uiLabel>Port</label>
|
|
|
|
|
<input uiInput type="number" [(ngModel)]="editForm.port" name="port" />
|
|
|
|
|
</div>
|
|
|
|
|
<div class="space-y-2">
|
|
|
|
|
<label uiLabel>Domain</label>
|
|
|
|
|
<input uiInput [(ngModel)]="editForm.domain" name="domain" />
|
|
|
|
|
</div>
|
|
|
|
|
<div class="flex gap-2">
|
|
|
|
|
<button uiButton type="submit" [disabled]="actionLoading()">Save</button>
|
|
|
|
|
<button uiButton variant="outline" type="button" (click)="cancelEdit()">Cancel</button>
|
|
|
|
|
</div>
|
|
|
|
|
</form>
|
|
|
|
|
} @else {
|
|
|
|
|
<dl class="space-y-4">
|
2025-11-24 01:31:15 +00:00
|
|
|
<div>
|
2025-11-24 19:52:35 +00:00
|
|
|
<dt class="text-sm font-medium text-muted-foreground">Image</dt>
|
|
|
|
|
<dd class="text-sm">{{ service()!.image }}</dd>
|
2025-11-24 01:31:15 +00:00
|
|
|
</div>
|
|
|
|
|
<div>
|
2025-11-24 19:52:35 +00:00
|
|
|
<dt class="text-sm font-medium text-muted-foreground">Port</dt>
|
|
|
|
|
<dd class="text-sm">{{ service()!.port }}</dd>
|
2025-11-24 01:31:15 +00:00
|
|
|
</div>
|
|
|
|
|
<div>
|
2025-11-24 19:52:35 +00:00
|
|
|
<dt class="text-sm font-medium text-muted-foreground">Domain</dt>
|
|
|
|
|
<dd class="text-sm">
|
|
|
|
|
@if (service()!.domain) {
|
|
|
|
|
<a [href]="'https://' + service()!.domain" target="_blank" class="text-primary hover:underline">
|
|
|
|
|
{{ service()!.domain }}
|
|
|
|
|
</a>
|
|
|
|
|
} @else {
|
|
|
|
|
<span class="text-muted-foreground">Not configured</span>
|
|
|
|
|
}
|
2025-11-24 01:31:15 +00:00
|
|
|
</dd>
|
|
|
|
|
</div>
|
2025-11-24 19:52:35 +00:00
|
|
|
@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>
|
2025-11-24 01:31:15 +00:00
|
|
|
</div>
|
|
|
|
|
}
|
2025-11-24 19:52:35 +00:00
|
|
|
<div>
|
|
|
|
|
<dt class="text-sm font-medium text-muted-foreground">Created</dt>
|
|
|
|
|
<dd class="text-sm">{{ formatDate(service()!.createdAt) }}</dd>
|
|
|
|
|
</div>
|
|
|
|
|
<div>
|
|
|
|
|
<dt class="text-sm font-medium text-muted-foreground">Updated</dt>
|
|
|
|
|
<dd class="text-sm">{{ formatDate(service()!.updatedAt) }}</dd>
|
|
|
|
|
</div>
|
2025-11-24 01:31:15 +00:00
|
|
|
</dl>
|
2025-11-24 19:52:35 +00:00
|
|
|
}
|
|
|
|
|
</ui-card-content>
|
|
|
|
|
</ui-card>
|
|
|
|
|
|
|
|
|
|
<!-- Actions -->
|
|
|
|
|
<ui-card>
|
|
|
|
|
<ui-card-header class="flex flex-col space-y-1.5">
|
|
|
|
|
<ui-card-title>Actions</ui-card-title>
|
|
|
|
|
<ui-card-description>Manage service state</ui-card-description>
|
|
|
|
|
</ui-card-header>
|
|
|
|
|
<ui-card-content class="space-y-4">
|
|
|
|
|
<div class="flex flex-wrap gap-2">
|
|
|
|
|
@if (service()!.status === 'stopped' || 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>
|
|
|
|
|
<button uiButton variant="outline" (click)="restartService()" [disabled]="actionLoading()">
|
|
|
|
|
Restart Service
|
|
|
|
|
</button>
|
|
|
|
|
}
|
|
|
|
|
<button uiButton variant="destructive" (click)="deleteDialogOpen.set(true)">
|
|
|
|
|
Delete Service
|
|
|
|
|
</button>
|
2025-11-18 00:03:24 +00:00
|
|
|
</div>
|
2025-11-24 19:52:35 +00:00
|
|
|
</ui-card-content>
|
|
|
|
|
</ui-card>
|
|
|
|
|
|
|
|
|
|
<!-- Environment Variables -->
|
|
|
|
|
@if (service()!.envVars && getEnvKeys(service()!.envVars).length > 0) {
|
|
|
|
|
<ui-card>
|
|
|
|
|
<ui-card-header class="flex flex-col space-y-1.5">
|
|
|
|
|
<ui-card-title>Environment Variables</ui-card-title>
|
|
|
|
|
</ui-card-header>
|
|
|
|
|
<ui-card-content>
|
|
|
|
|
<div class="space-y-2">
|
|
|
|
|
@for (key of getEnvKeys(service()!.envVars); track key) {
|
|
|
|
|
<div class="flex gap-2 text-sm">
|
|
|
|
|
<span class="font-mono font-medium">{{ key }}:</span>
|
|
|
|
|
<span class="font-mono text-muted-foreground">{{ service()!.envVars[key] }}</span>
|
2025-11-24 01:31:15 +00:00
|
|
|
</div>
|
|
|
|
|
}
|
|
|
|
|
</div>
|
2025-11-24 19:52:35 +00:00
|
|
|
</ui-card-content>
|
|
|
|
|
</ui-card>
|
|
|
|
|
}
|
2025-11-24 01:31:15 +00:00
|
|
|
|
2025-11-24 19:52:35 +00:00
|
|
|
<!-- Onebox Registry Info -->
|
|
|
|
|
@if (service()!.useOneboxRegistry) {
|
|
|
|
|
<ui-card>
|
|
|
|
|
<ui-card-header class="flex flex-col space-y-1.5">
|
|
|
|
|
<ui-card-title>Onebox Registry</ui-card-title>
|
|
|
|
|
<ui-card-description>Push images directly to this service</ui-card-description>
|
|
|
|
|
</ui-card-header>
|
|
|
|
|
<ui-card-content class="space-y-4">
|
|
|
|
|
<div>
|
|
|
|
|
<dt class="text-sm font-medium text-muted-foreground">Repository</dt>
|
|
|
|
|
<dd class="text-sm font-mono">{{ service()!.registryRepository }}</dd>
|
|
|
|
|
</div>
|
|
|
|
|
<div>
|
|
|
|
|
<dt class="text-sm font-medium text-muted-foreground">Tag</dt>
|
|
|
|
|
<dd class="text-sm">{{ service()!.registryImageTag || 'latest' }}</dd>
|
|
|
|
|
</div>
|
|
|
|
|
@if (service()!.registryToken) {
|
|
|
|
|
<div>
|
|
|
|
|
<dt class="text-sm font-medium text-muted-foreground">Push Token</dt>
|
|
|
|
|
<dd class="flex items-center gap-2">
|
|
|
|
|
<input
|
|
|
|
|
uiInput
|
|
|
|
|
type="password"
|
|
|
|
|
[value]="service()!.registryToken"
|
|
|
|
|
readonly
|
|
|
|
|
class="font-mono text-xs"
|
|
|
|
|
/>
|
|
|
|
|
<button uiButton variant="outline" size="sm" (click)="copyToken()">Copy</button>
|
|
|
|
|
</dd>
|
2025-11-24 01:31:15 +00:00
|
|
|
</div>
|
|
|
|
|
}
|
2025-11-24 19:52:35 +00:00
|
|
|
<div>
|
|
|
|
|
<dt class="text-sm font-medium text-muted-foreground">Auto-update on push</dt>
|
|
|
|
|
<dd class="text-sm">{{ service()!.autoUpdateOnPush ? 'Enabled' : 'Disabled' }}</dd>
|
2025-11-24 01:31:15 +00:00
|
|
|
</div>
|
2025-11-24 19:52:35 +00:00
|
|
|
</ui-card-content>
|
|
|
|
|
</ui-card>
|
2025-11-18 00:03:24 +00:00
|
|
|
}
|
|
|
|
|
</div>
|
|
|
|
|
|
2025-11-24 19:52:35 +00:00
|
|
|
<!-- Logs Section -->
|
|
|
|
|
<ui-card>
|
|
|
|
|
<ui-card-header class="flex flex-row items-center justify-between">
|
|
|
|
|
<div class="flex flex-col space-y-1.5">
|
|
|
|
|
<ui-card-title>Logs</ui-card-title>
|
|
|
|
|
<ui-card-description>
|
|
|
|
|
@if (logStream.isStreaming()) {
|
|
|
|
|
<span class="flex items-center gap-2">
|
|
|
|
|
<span class="relative flex h-2 w-2">
|
|
|
|
|
<span class="animate-ping absolute inline-flex h-full w-full rounded-full bg-green-400 opacity-75"></span>
|
|
|
|
|
<span class="relative inline-flex rounded-full h-2 w-2 bg-green-500"></span>
|
|
|
|
|
</span>
|
|
|
|
|
Live streaming
|
|
|
|
|
</span>
|
|
|
|
|
} @else {
|
|
|
|
|
Container logs
|
|
|
|
|
}
|
|
|
|
|
</ui-card-description>
|
2025-11-24 01:31:15 +00:00
|
|
|
</div>
|
2025-11-24 19:52:35 +00:00
|
|
|
<div class="flex items-center gap-2">
|
|
|
|
|
@if (logStream.isStreaming()) {
|
|
|
|
|
<button uiButton variant="outline" size="sm" (click)="stopLogStream()">
|
|
|
|
|
<svg class="h-4 w-4 mr-1" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
|
|
|
|
|
<path stroke-linecap="round" stroke-linejoin="round" d="M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
|
|
|
|
|
<path stroke-linecap="round" stroke-linejoin="round" d="M9 10a1 1 0 011-1h4a1 1 0 011 1v4a1 1 0 01-1 1h-4a1 1 0 01-1-1v-4z" />
|
2025-11-24 01:31:15 +00:00
|
|
|
</svg>
|
2025-11-24 19:52:35 +00:00
|
|
|
Stop
|
2025-11-24 01:31:15 +00:00
|
|
|
</button>
|
2025-11-24 19:52:35 +00:00
|
|
|
} @else {
|
|
|
|
|
<button uiButton variant="outline" size="sm" (click)="startLogStream()" [disabled]="service()?.status !== 'running'">
|
|
|
|
|
<svg class="h-4 w-4 mr-1" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
|
|
|
|
|
<path stroke-linecap="round" stroke-linejoin="round" d="M14.752 11.168l-3.197-2.132A1 1 0 0010 9.87v4.263a1 1 0 001.555.832l3.197-2.132a1 1 0 000-1.664z" />
|
|
|
|
|
<path stroke-linecap="round" stroke-linejoin="round" d="M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
|
|
|
|
|
</svg>
|
|
|
|
|
Stream
|
|
|
|
|
</button>
|
|
|
|
|
}
|
|
|
|
|
<button uiButton variant="ghost" size="sm" (click)="clearLogs()" title="Clear logs">
|
|
|
|
|
<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="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16" />
|
|
|
|
|
</svg>
|
|
|
|
|
</button>
|
|
|
|
|
<label class="flex items-center gap-1 text-xs text-muted-foreground cursor-pointer">
|
|
|
|
|
<input type="checkbox" [(ngModel)]="autoScroll" class="rounded border-input" />
|
|
|
|
|
Auto-scroll
|
|
|
|
|
</label>
|
2025-11-18 00:03:24 +00:00
|
|
|
</div>
|
2025-11-24 19:52:35 +00:00
|
|
|
</ui-card-header>
|
|
|
|
|
<ui-card-content>
|
|
|
|
|
<div
|
|
|
|
|
#logContainer
|
|
|
|
|
class="bg-zinc-950 text-zinc-100 rounded-md p-4 h-96 overflow-auto font-mono text-xs"
|
|
|
|
|
>
|
|
|
|
|
@if (logStream.state().error) {
|
|
|
|
|
<p class="text-red-400">Error: {{ logStream.state().error }}</p>
|
|
|
|
|
} @else if (logStream.logs().length > 0) {
|
|
|
|
|
@for (line of logStream.logs(); track $index) {
|
|
|
|
|
<div class="whitespace-pre-wrap hover:bg-zinc-800/50">{{ line }}</div>
|
2025-11-24 01:31:15 +00:00
|
|
|
}
|
2025-11-24 19:52:35 +00:00
|
|
|
} @else if (logStream.isStreaming()) {
|
|
|
|
|
<p class="text-zinc-500">Waiting for logs...</p>
|
|
|
|
|
} @else {
|
|
|
|
|
<p class="text-zinc-500">Click "Stream" to start live log streaming</p>
|
|
|
|
|
}
|
|
|
|
|
</div>
|
|
|
|
|
</ui-card-content>
|
|
|
|
|
</ui-card>
|
2025-11-18 00:03:24 +00:00
|
|
|
}
|
|
|
|
|
</div>
|
2025-11-24 19:52:35 +00:00
|
|
|
|
|
|
|
|
<!-- Delete Confirmation Dialog -->
|
|
|
|
|
<ui-dialog [open]="deleteDialogOpen()" (openChange)="deleteDialogOpen.set($event)">
|
|
|
|
|
<ui-dialog-header>
|
|
|
|
|
<ui-dialog-title>Delete Service</ui-dialog-title>
|
|
|
|
|
<ui-dialog-description>
|
|
|
|
|
Are you sure you want to delete "{{ service()?.name }}"? This action cannot be undone.
|
|
|
|
|
</ui-dialog-description>
|
|
|
|
|
</ui-dialog-header>
|
|
|
|
|
<ui-dialog-footer>
|
|
|
|
|
<button uiButton variant="outline" (click)="deleteDialogOpen.set(false)">Cancel</button>
|
|
|
|
|
<button uiButton variant="destructive" (click)="deleteService()" [disabled]="actionLoading()">
|
|
|
|
|
Delete
|
|
|
|
|
</button>
|
|
|
|
|
</ui-dialog-footer>
|
|
|
|
|
</ui-dialog>
|
2025-11-18 00:03:24 +00:00
|
|
|
`,
|
|
|
|
|
})
|
2025-11-24 01:31:15 +00:00
|
|
|
export class ServiceDetailComponent implements OnInit, OnDestroy {
|
2025-11-18 00:03:24 +00:00
|
|
|
private route = inject(ActivatedRoute);
|
|
|
|
|
private router = inject(Router);
|
2025-11-24 19:52:35 +00:00
|
|
|
private api = inject(ApiService);
|
|
|
|
|
private toast = inject(ToastService);
|
|
|
|
|
logStream = inject(LogStreamService);
|
|
|
|
|
|
|
|
|
|
@ViewChild('logContainer') logContainer!: ElementRef<HTMLDivElement>;
|
|
|
|
|
|
|
|
|
|
service = signal<IService | null>(null);
|
|
|
|
|
loading = signal(false);
|
|
|
|
|
actionLoading = signal(false);
|
|
|
|
|
editMode = signal(false);
|
|
|
|
|
deleteDialogOpen = signal(false);
|
|
|
|
|
autoScroll = true;
|
|
|
|
|
|
|
|
|
|
editForm: IServiceUpdate = {};
|
|
|
|
|
|
|
|
|
|
constructor() {
|
|
|
|
|
// Auto-scroll when new logs arrive
|
|
|
|
|
effect(() => {
|
|
|
|
|
const logs = this.logStream.logs();
|
|
|
|
|
if (logs.length > 0 && this.autoScroll && this.logContainer?.nativeElement) {
|
|
|
|
|
setTimeout(() => {
|
|
|
|
|
const container = this.logContainer.nativeElement;
|
|
|
|
|
container.scrollTop = container.scrollHeight;
|
|
|
|
|
}, 0);
|
|
|
|
|
}
|
2025-11-18 00:03:24 +00:00
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
2025-11-24 19:52:35 +00:00
|
|
|
ngOnInit(): void {
|
|
|
|
|
const name = this.route.snapshot.paramMap.get('name');
|
|
|
|
|
if (name) {
|
|
|
|
|
this.loadService(name);
|
2025-11-24 01:31:15 +00:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-11-24 19:52:35 +00:00
|
|
|
ngOnDestroy(): void {
|
|
|
|
|
this.logStream.disconnect();
|
2025-11-24 01:31:15 +00:00
|
|
|
}
|
|
|
|
|
|
2025-11-24 19:52:35 +00:00
|
|
|
async loadService(name: string): Promise<void> {
|
|
|
|
|
this.loading.set(true);
|
|
|
|
|
try {
|
|
|
|
|
const response = await this.api.getService(name);
|
|
|
|
|
if (response.success && response.data) {
|
|
|
|
|
this.service.set(response.data);
|
|
|
|
|
this.editForm = {
|
|
|
|
|
image: response.data.image,
|
|
|
|
|
port: response.data.port,
|
|
|
|
|
domain: response.data.domain,
|
|
|
|
|
};
|
|
|
|
|
} else {
|
|
|
|
|
this.toast.error(response.error || 'Service not found');
|
|
|
|
|
this.router.navigate(['/services']);
|
2025-11-24 01:31:15 +00:00
|
|
|
}
|
2025-11-24 19:52:35 +00:00
|
|
|
} catch {
|
|
|
|
|
this.toast.error('Failed to load service');
|
|
|
|
|
} finally {
|
|
|
|
|
this.loading.set(false);
|
2025-11-24 01:31:15 +00:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-11-24 19:52:35 +00:00
|
|
|
startLogStream(): void {
|
|
|
|
|
const name = this.service()?.name;
|
|
|
|
|
if (name) {
|
|
|
|
|
this.logStream.connect(name);
|
2025-11-24 01:31:15 +00:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-11-24 19:52:35 +00:00
|
|
|
stopLogStream(): void {
|
|
|
|
|
this.logStream.disconnect();
|
2025-11-24 01:31:15 +00:00
|
|
|
}
|
|
|
|
|
|
2025-11-24 19:52:35 +00:00
|
|
|
clearLogs(): void {
|
|
|
|
|
this.logStream.clearLogs();
|
|
|
|
|
}
|
2025-11-24 01:31:15 +00:00
|
|
|
|
2025-11-24 19:52:35 +00:00
|
|
|
getStatusVariant(status: string): 'success' | 'destructive' | 'warning' | 'secondary' {
|
|
|
|
|
switch (status) {
|
|
|
|
|
case 'running': return 'success';
|
|
|
|
|
case 'stopped': return 'secondary';
|
|
|
|
|
case 'failed': return 'destructive';
|
|
|
|
|
case 'starting':
|
|
|
|
|
case 'stopping': return 'warning';
|
|
|
|
|
default: return 'secondary';
|
2025-11-24 01:31:15 +00:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-11-24 19:52:35 +00:00
|
|
|
formatDate(timestamp: number): string {
|
|
|
|
|
return new Date(timestamp).toLocaleString();
|
2025-11-24 01:31:15 +00:00
|
|
|
}
|
|
|
|
|
|
2025-11-24 19:52:35 +00:00
|
|
|
getEnvKeys(envVars: Record<string, string>): string[] {
|
|
|
|
|
return Object.keys(envVars);
|
2025-11-24 01:31:15 +00:00
|
|
|
}
|
|
|
|
|
|
2025-11-24 19:52:35 +00:00
|
|
|
cancelEdit(): void {
|
|
|
|
|
this.editMode.set(false);
|
|
|
|
|
if (this.service()) {
|
|
|
|
|
this.editForm = {
|
|
|
|
|
image: this.service()!.image,
|
|
|
|
|
port: this.service()!.port,
|
|
|
|
|
domain: this.service()!.domain,
|
|
|
|
|
};
|
2025-11-24 01:31:15 +00:00
|
|
|
}
|
2025-11-18 00:03:24 +00:00
|
|
|
}
|
|
|
|
|
|
2025-11-24 19:52:35 +00:00
|
|
|
async saveChanges(): Promise<void> {
|
|
|
|
|
const name = this.service()?.name;
|
|
|
|
|
if (!name) return;
|
|
|
|
|
|
|
|
|
|
this.actionLoading.set(true);
|
|
|
|
|
try {
|
|
|
|
|
const response = await this.api.updateService(name, this.editForm);
|
|
|
|
|
if (response.success) {
|
|
|
|
|
this.toast.success('Service updated');
|
|
|
|
|
this.editMode.set(false);
|
|
|
|
|
this.loadService(name);
|
|
|
|
|
} else {
|
|
|
|
|
this.toast.error(response.error || 'Failed to update service');
|
|
|
|
|
}
|
|
|
|
|
} catch {
|
|
|
|
|
this.toast.error('Failed to update service');
|
|
|
|
|
} finally {
|
|
|
|
|
this.actionLoading.set(false);
|
|
|
|
|
}
|
2025-11-18 00:03:24 +00:00
|
|
|
}
|
|
|
|
|
|
2025-11-24 19:52:35 +00:00
|
|
|
async startService(): Promise<void> {
|
|
|
|
|
const name = this.service()?.name;
|
|
|
|
|
if (!name) return;
|
|
|
|
|
|
|
|
|
|
this.actionLoading.set(true);
|
|
|
|
|
try {
|
|
|
|
|
const response = await this.api.startService(name);
|
|
|
|
|
if (response.success) {
|
|
|
|
|
this.toast.success('Service started');
|
|
|
|
|
this.loadService(name);
|
|
|
|
|
} else {
|
|
|
|
|
this.toast.error(response.error || 'Failed to start service');
|
|
|
|
|
}
|
|
|
|
|
} catch {
|
|
|
|
|
this.toast.error('Failed to start service');
|
|
|
|
|
} finally {
|
|
|
|
|
this.actionLoading.set(false);
|
|
|
|
|
}
|
2025-11-18 00:03:24 +00:00
|
|
|
}
|
|
|
|
|
|
2025-11-24 19:52:35 +00:00
|
|
|
async stopService(): Promise<void> {
|
|
|
|
|
const name = this.service()?.name;
|
|
|
|
|
if (!name) return;
|
|
|
|
|
|
|
|
|
|
this.actionLoading.set(true);
|
|
|
|
|
try {
|
|
|
|
|
const response = await this.api.stopService(name);
|
|
|
|
|
if (response.success) {
|
|
|
|
|
this.toast.success('Service stopped');
|
|
|
|
|
this.loadService(name);
|
|
|
|
|
} else {
|
|
|
|
|
this.toast.error(response.error || 'Failed to stop service');
|
|
|
|
|
}
|
|
|
|
|
} catch {
|
|
|
|
|
this.toast.error('Failed to stop service');
|
|
|
|
|
} finally {
|
|
|
|
|
this.actionLoading.set(false);
|
2025-11-18 00:03:24 +00:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-11-24 19:52:35 +00:00
|
|
|
async restartService(): Promise<void> {
|
|
|
|
|
const name = this.service()?.name;
|
|
|
|
|
if (!name) return;
|
|
|
|
|
|
|
|
|
|
this.actionLoading.set(true);
|
|
|
|
|
try {
|
|
|
|
|
const response = await this.api.restartService(name);
|
|
|
|
|
if (response.success) {
|
|
|
|
|
this.toast.success('Service restarted');
|
|
|
|
|
this.loadService(name);
|
|
|
|
|
} else {
|
|
|
|
|
this.toast.error(response.error || 'Failed to restart service');
|
|
|
|
|
}
|
|
|
|
|
} catch {
|
|
|
|
|
this.toast.error('Failed to restart service');
|
|
|
|
|
} finally {
|
|
|
|
|
this.actionLoading.set(false);
|
|
|
|
|
}
|
2025-11-18 00:03:24 +00:00
|
|
|
}
|
2025-11-24 01:31:15 +00:00
|
|
|
|
2025-11-24 19:52:35 +00:00
|
|
|
async deleteService(): Promise<void> {
|
|
|
|
|
const name = this.service()?.name;
|
|
|
|
|
if (!name) return;
|
2025-11-24 01:31:15 +00:00
|
|
|
|
2025-11-24 19:52:35 +00:00
|
|
|
this.actionLoading.set(true);
|
|
|
|
|
try {
|
|
|
|
|
const response = await this.api.deleteService(name);
|
|
|
|
|
if (response.success) {
|
|
|
|
|
this.toast.success('Service deleted');
|
|
|
|
|
this.router.navigate(['/services']);
|
|
|
|
|
} else {
|
|
|
|
|
this.toast.error(response.error || 'Failed to delete service');
|
|
|
|
|
}
|
|
|
|
|
} catch {
|
|
|
|
|
this.toast.error('Failed to delete service');
|
|
|
|
|
} finally {
|
|
|
|
|
this.actionLoading.set(false);
|
|
|
|
|
this.deleteDialogOpen.set(false);
|
|
|
|
|
}
|
2025-11-24 01:31:15 +00:00
|
|
|
}
|
|
|
|
|
|
2025-11-24 19:52:35 +00:00
|
|
|
copyToken(): void {
|
|
|
|
|
const token = this.service()?.registryToken;
|
|
|
|
|
if (token) {
|
|
|
|
|
navigator.clipboard.writeText(token);
|
|
|
|
|
this.toast.success('Token copied to clipboard');
|
2025-11-24 01:31:15 +00:00
|
|
|
}
|
|
|
|
|
}
|
2025-11-18 00:03:24 +00:00
|
|
|
}
|