Files
onebox/ui/src/app/features/domains/domain-detail.component.ts

357 lines
16 KiB
TypeScript
Raw Normal View History

import { Component, OnInit, inject, signal } from '@angular/core';
import { CommonModule } from '@angular/common';
import { ActivatedRoute, RouterLink } from '@angular/router';
import { ApiService } from '../../core/services/api.service';
interface DomainDetail {
domain: {
id: number;
domain: string;
dnsProvider: 'cloudflare' | 'manual' | null;
cloudflareZoneId?: string;
isObsolete: boolean;
defaultWildcard: boolean;
createdAt: number;
updatedAt: number;
};
certificates: Array<{
id: number;
certDomain: string;
isWildcard: boolean;
expiryDate: number;
issuer: string;
isValid: boolean;
createdAt: number;
}>;
requirements: Array<{
id: number;
serviceId: number;
subdomain: string;
status: 'pending' | 'active' | 'renewing';
certificateId?: number;
}>;
services: Array<{
id: number;
name: string;
domain: string;
status: string;
}>;
}
@Component({
selector: 'app-domain-detail',
standalone: true,
imports: [CommonModule, RouterLink],
template: `
<div class="px-4 sm:px-0">
<!-- Header -->
<div class="mb-8">
<div class="flex items-center mb-4">
<a
routerLink="/domains"
class="text-primary-600 hover:text-primary-900 mr-4"
>
Back to Domains
</a>
</div>
@if (loading()) {
<div class="card text-center py-12">
<p class="text-gray-500">Loading domain details...</p>
</div>
} @else if (domainDetail()) {
<div>
<div class="flex justify-between items-start">
<div>
<h1 class="text-3xl font-bold text-gray-900">
{{ domainDetail()!.domain.domain }}
</h1>
<div class="mt-2 flex items-center gap-3">
@if (domainDetail()!.domain.dnsProvider === 'cloudflare') {
<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-blue-100 text-blue-800">
Cloudflare
</span>
} @else if (domainDetail()!.domain.dnsProvider === 'manual') {
<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-gray-100 text-gray-800">
Manual DNS
</span>
}
@if (domainDetail()!.domain.defaultWildcard) {
<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-purple-100 text-purple-800">
Wildcard Enabled
</span>
}
@if (domainDetail()!.domain.isObsolete) {
<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-red-100 text-red-800">
Obsolete
</span>
}
</div>
</div>
</div>
<!-- Stats Grid -->
<div class="mt-6 grid grid-cols-1 gap-5 sm:grid-cols-3">
<div class="card">
<dt class="text-sm font-medium text-gray-500 truncate">Certificates</dt>
<dd class="mt-1 text-3xl font-semibold text-gray-900">
{{ domainDetail()!.certificates.length }}
</dd>
</div>
<div class="card">
<dt class="text-sm font-medium text-gray-500 truncate">Requirements</dt>
<dd class="mt-1 text-3xl font-semibold text-gray-900">
{{ domainDetail()!.requirements.length }}
</dd>
</div>
<div class="card">
<dt class="text-sm font-medium text-gray-500 truncate">Services</dt>
<dd class="mt-1 text-3xl font-semibold text-gray-900">
{{ domainDetail()!.services.length }}
</dd>
</div>
</div>
<!-- Certificates Section -->
<div class="mt-8">
<h2 class="text-xl font-bold text-gray-900 mb-4">SSL Certificates</h2>
@if (domainDetail()!.certificates.length > 0) {
<div class="card overflow-hidden p-0">
<table class="min-w-full divide-y divide-gray-200">
<thead class="bg-gray-50">
<tr>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">Domain</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">Type</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">Status</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">Expires</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">Issuer</th>
</tr>
</thead>
<tbody class="bg-white divide-y divide-gray-200">
@for (cert of domainDetail()!.certificates; track cert.id) {
<tr>
<td class="px-6 py-4 whitespace-nowrap text-sm font-medium text-gray-900">
{{ cert.certDomain }}
</td>
<td class="px-6 py-4 whitespace-nowrap">
@if (cert.isWildcard) {
<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-purple-100 text-purple-800">
Wildcard
</span>
} @else {
<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-gray-100 text-gray-800">
Standard
</span>
}
</td>
<td class="px-6 py-4 whitespace-nowrap">
@if (getCertStatus(cert) === 'valid') {
<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-green-100 text-green-800">
Valid
</span>
} @else if (getCertStatus(cert) === 'expiring') {
<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-yellow-100 text-yellow-800">
Expiring Soon
</span>
} @else {
<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-red-100 text-red-800">
Expired/Invalid
</span>
}
</td>
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-900">
{{ formatDate(cert.expiryDate) }}
<span class="text-gray-500">({{ getDaysRemaining(cert.expiryDate) }} days)</span>
</td>
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
{{ cert.issuer }}
</td>
</tr>
}
</tbody>
</table>
</div>
} @else {
<div class="card text-center py-8">
<p class="text-gray-500">No certificates for this domain</p>
</div>
}
</div>
<!-- Certificate Requirements Section -->
<div class="mt-8">
<h2 class="text-xl font-bold text-gray-900 mb-4">Certificate Requirements</h2>
@if (domainDetail()!.requirements.length > 0) {
<div class="card overflow-hidden p-0">
<table class="min-w-full divide-y divide-gray-200">
<thead class="bg-gray-50">
<tr>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">Service</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">Subdomain</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">Status</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">Certificate ID</th>
</tr>
</thead>
<tbody class="bg-white divide-y divide-gray-200">
@for (req of domainDetail()!.requirements; track req.id) {
<tr>
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-900">
{{ getServiceName(req.serviceId) }}
</td>
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
{{ req.subdomain || '(root)' }}
</td>
<td class="px-6 py-4 whitespace-nowrap">
@if (req.status === 'active') {
<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-green-100 text-green-800">
Active
</span>
} @else if (req.status === 'pending') {
<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-yellow-100 text-yellow-800">
Pending
</span>
} @else if (req.status === 'renewing') {
<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-blue-100 text-blue-800">
Renewing
</span>
}
</td>
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
{{ req.certificateId || '—' }}
</td>
</tr>
}
</tbody>
</table>
</div>
} @else {
<div class="card text-center py-8">
<p class="text-gray-500">No certificate requirements</p>
</div>
}
</div>
<!-- Services Section -->
<div class="mt-8">
<h2 class="text-xl font-bold text-gray-900 mb-4">Services Using This Domain</h2>
@if (domainDetail()!.services.length > 0) {
<div class="card overflow-hidden p-0">
<table class="min-w-full divide-y divide-gray-200">
<thead class="bg-gray-50">
<tr>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">Name</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">Domain</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">Status</th>
<th class="px-6 py-3 text-right text-xs font-medium text-gray-500 uppercase">Actions</th>
</tr>
</thead>
<tbody class="bg-white divide-y divide-gray-200">
@for (service of domainDetail()!.services; track service.id) {
<tr>
<td class="px-6 py-4 whitespace-nowrap text-sm font-medium text-gray-900">
{{ service.name }}
</td>
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
{{ service.domain }}
</td>
<td class="px-6 py-4 whitespace-nowrap">
@if (service.status === 'running') {
<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-green-100 text-green-800">
Running
</span>
} @else if (service.status === 'stopped') {
<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-gray-100 text-gray-800">
Stopped
</span>
} @else {
<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-yellow-100 text-yellow-800">
{{ service.status }}
</span>
}
</td>
<td class="px-6 py-4 whitespace-nowrap text-right text-sm">
<a
[routerLink]="['/services', service.name]"
class="text-primary-600 hover:text-primary-900"
>
View Service
</a>
</td>
</tr>
}
</tbody>
</table>
</div>
} @else {
<div class="card text-center py-8">
<p class="text-gray-500">No services using this domain</p>
</div>
}
</div>
</div>
} @else {
<div class="card text-center py-12">
<p class="text-gray-500">Domain not found</p>
</div>
}
</div>
</div>
`,
})
export class DomainDetailComponent implements OnInit {
private route = inject(ActivatedRoute);
private apiService = inject(ApiService);
domainDetail = signal<DomainDetail | null>(null);
loading = signal(true);
ngOnInit(): void {
const domain = this.route.snapshot.paramMap.get('domain');
if (domain) {
this.loadDomainDetail(domain);
}
}
loadDomainDetail(domain: string): void {
this.loading.set(true);
this.apiService.getDomainDetail(domain).subscribe({
next: (response) => {
if (response.success && response.data) {
this.domainDetail.set(response.data);
}
this.loading.set(false);
},
error: () => {
this.loading.set(false);
},
});
}
formatDate(timestamp: number): string {
return new Date(timestamp).toLocaleDateString('en-US', {
year: 'numeric',
month: 'short',
day: 'numeric',
});
}
getDaysRemaining(expiryDate: number): number {
const now = Date.now();
const diff = expiryDate - now;
return Math.floor(diff / (24 * 60 * 60 * 1000));
}
getCertStatus(cert: any): 'valid' | 'expiring' | 'invalid' {
if (!cert.isValid) return 'invalid';
const daysRemaining = this.getDaysRemaining(cert.expiryDate);
if (daysRemaining < 0) return 'invalid';
if (daysRemaining <= 30) return 'expiring';
return 'valid';
}
getServiceName(serviceId: number): string {
const service = this.domainDetail()?.services.find((s) => s.id === serviceId);
return service?.name || `Service #${serviceId}`;
}
}