This commit is contained in:
2025-11-18 00:03:24 +00:00
parent 246a6073e0
commit 8f538ab9c0
50 changed files with 12836 additions and 531 deletions

View File

@@ -0,0 +1,209 @@
import { Component, OnInit, inject, signal } from '@angular/core';
import { CommonModule } from '@angular/common';
import { ActivatedRoute, Router } from '@angular/router';
import { ApiService, Service } from '../../core/services/api.service';
@Component({
selector: 'app-service-detail',
standalone: true,
imports: [CommonModule],
template: `
<div class="px-4 sm:px-0">
@if (loading()) {
<div class="text-center py-12">
<div class="inline-block animate-spin rounded-full h-8 w-8 border-b-2 border-primary-600"></div>
</div>
} @else if (service()) {
<div class="mb-8">
<div class="flex items-center justify-between">
<h1 class="text-3xl font-bold text-gray-900">{{ service()!.name }}</h1>
<span [ngClass]="{
'badge-success': service()!.status === 'running',
'badge-danger': service()!.status === 'stopped' || service()!.status === 'failed',
'badge-warning': service()!.status === 'starting' || service()!.status === 'stopping'
}" class="badge text-lg">
{{ service()!.status }}
</span>
</div>
</div>
<!-- Details Card -->
<div class="card mb-6">
<h2 class="text-lg font-medium text-gray-900 mb-4">Service Details</h2>
<dl class="grid grid-cols-1 gap-x-4 gap-y-4 sm:grid-cols-2">
<div>
<dt class="text-sm font-medium text-gray-500">Image</dt>
<dd class="mt-1 text-sm text-gray-900">{{ service()!.image }}</dd>
</div>
<div>
<dt class="text-sm font-medium text-gray-500">Port</dt>
<dd class="mt-1 text-sm text-gray-900">{{ service()!.port }}</dd>
</div>
@if (service()!.domain) {
<div>
<dt class="text-sm font-medium text-gray-500">Domain</dt>
<dd class="mt-1 text-sm text-gray-900">
<a [href]="'https://' + service()!.domain" target="_blank" class="text-primary-600 hover:text-primary-900">
{{ service()!.domain }}
</a>
</dd>
</div>
}
@if (service()!.containerID) {
<div>
<dt class="text-sm font-medium text-gray-500">Container ID</dt>
<dd class="mt-1 text-sm text-gray-900 font-mono">{{ service()!.containerID?.substring(0, 12) }}</dd>
</div>
}
<div>
<dt class="text-sm font-medium text-gray-500">Created</dt>
<dd class="mt-1 text-sm text-gray-900">{{ formatDate(service()!.createdAt) }}</dd>
</div>
<div>
<dt class="text-sm font-medium text-gray-500">Updated</dt>
<dd class="mt-1 text-sm text-gray-900">{{ formatDate(service()!.updatedAt) }}</dd>
</div>
</dl>
<!-- Environment Variables -->
@if (Object.keys(service()!.envVars).length > 0) {
<div class="mt-6">
<h3 class="text-sm font-medium text-gray-500 mb-2">Environment Variables</h3>
<div class="bg-gray-50 rounded-md p-4">
@for (entry of Object.entries(service()!.envVars); track entry[0]) {
<div class="flex justify-between py-1">
<span class="text-sm font-mono text-gray-700">{{ entry[0] }}</span>
<span class="text-sm font-mono text-gray-900">{{ entry[1] }}</span>
</div>
}
</div>
</div>
}
</div>
<!-- Actions -->
<div class="card mb-6">
<h2 class="text-lg font-medium text-gray-900 mb-4">Actions</h2>
<div class="flex space-x-4">
@if (service()!.status === 'stopped') {
<button (click)="startService()" class="btn btn-success">Start</button>
}
@if (service()!.status === 'running') {
<button (click)="stopService()" class="btn btn-secondary">Stop</button>
<button (click)="restartService()" class="btn btn-primary">Restart</button>
}
<button (click)="deleteService()" class="btn btn-danger">Delete</button>
</div>
</div>
<!-- Logs -->
<div class="card">
<div class="flex items-center justify-between mb-4">
<h2 class="text-lg font-medium text-gray-900">Logs</h2>
<button (click)="refreshLogs()" class="btn btn-secondary text-sm">Refresh</button>
</div>
@if (loadingLogs()) {
<div class="text-center py-8">
<div class="inline-block animate-spin rounded-full h-6 w-6 border-b-2 border-primary-600"></div>
</div>
} @else {
<div class="bg-gray-900 rounded-md p-4 overflow-x-auto">
<pre class="text-xs text-gray-100 font-mono">{{ logs() || 'No logs available' }}</pre>
</div>
}
</div>
}
</div>
`,
})
export class ServiceDetailComponent implements OnInit {
private apiService = inject(ApiService);
private route = inject(ActivatedRoute);
private router = inject(Router);
service = signal<Service | null>(null);
logs = signal('');
loading = signal(true);
loadingLogs = signal(false);
Object = Object;
ngOnInit(): void {
const name = this.route.snapshot.paramMap.get('name')!;
this.loadService(name);
this.loadLogs(name);
}
loadService(name: string): void {
this.loading.set(true);
this.apiService.getService(name).subscribe({
next: (response) => {
if (response.success && response.data) {
this.service.set(response.data);
}
this.loading.set(false);
},
error: () => {
this.loading.set(false);
this.router.navigate(['/services']);
},
});
}
loadLogs(name: string): void {
this.loadingLogs.set(true);
this.apiService.getServiceLogs(name).subscribe({
next: (response) => {
if (response.success && response.data) {
this.logs.set(response.data);
}
this.loadingLogs.set(false);
},
error: () => {
this.loadingLogs.set(false);
},
});
}
refreshLogs(): void {
this.loadLogs(this.service()!.name);
}
startService(): void {
this.apiService.startService(this.service()!.name).subscribe({
next: () => {
this.loadService(this.service()!.name);
},
});
}
stopService(): void {
this.apiService.stopService(this.service()!.name).subscribe({
next: () => {
this.loadService(this.service()!.name);
},
});
}
restartService(): void {
this.apiService.restartService(this.service()!.name).subscribe({
next: () => {
this.loadService(this.service()!.name);
},
});
}
deleteService(): void {
if (confirm(`Are you sure you want to delete ${this.service()!.name}?`)) {
this.apiService.deleteService(this.service()!.name).subscribe({
next: () => {
this.router.navigate(['/services']);
},
});
}
}
formatDate(timestamp: number): string {
return new Date(timestamp).toLocaleString();
}
}