290 lines
11 KiB
TypeScript
290 lines
11 KiB
TypeScript
import { Component, inject, signal, OnInit } from '@angular/core';
|
|
import { ActivatedRoute, RouterLink } from '@angular/router';
|
|
import { ApiService } from '../../core/services/api.service';
|
|
import { ToastService } from '../../core/services/toast.service';
|
|
import { IDomainDetail, IService } from '../../core/types/api.types';
|
|
import {
|
|
CardComponent,
|
|
CardHeaderComponent,
|
|
CardTitleComponent,
|
|
CardDescriptionComponent,
|
|
CardContentComponent,
|
|
} from '../../ui/card/card.component';
|
|
import { ButtonComponent } from '../../ui/button/button.component';
|
|
import { BadgeComponent } from '../../ui/badge/badge.component';
|
|
import {
|
|
TableComponent,
|
|
TableHeaderComponent,
|
|
TableBodyComponent,
|
|
TableRowComponent,
|
|
TableHeadComponent,
|
|
TableCellComponent,
|
|
} from '../../ui/table/table.component';
|
|
import { SkeletonComponent } from '../../ui/skeleton/skeleton.component';
|
|
|
|
@Component({
|
|
selector: 'app-domain-detail',
|
|
standalone: true,
|
|
imports: [
|
|
RouterLink,
|
|
CardComponent,
|
|
CardHeaderComponent,
|
|
CardTitleComponent,
|
|
CardDescriptionComponent,
|
|
CardContentComponent,
|
|
ButtonComponent,
|
|
BadgeComponent,
|
|
TableComponent,
|
|
TableHeaderComponent,
|
|
TableBodyComponent,
|
|
TableRowComponent,
|
|
TableHeadComponent,
|
|
TableCellComponent,
|
|
SkeletonComponent,
|
|
],
|
|
template: `
|
|
<div class="space-y-6">
|
|
<div>
|
|
<a routerLink="/domains" 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 Domains
|
|
</a>
|
|
|
|
@if (loading() && !domain()) {
|
|
<ui-skeleton class="h-9 w-64" />
|
|
} @else if (domain()) {
|
|
<div class="flex items-center gap-4">
|
|
<h1 class="text-3xl font-bold tracking-tight">{{ domain()!.domain.domain }}</h1>
|
|
<ui-badge [variant]="domain()!.domain.dnsProvider === 'cloudflare' ? 'default' : 'secondary'">
|
|
{{ domain()!.domain.dnsProvider || 'Manual' }}
|
|
</ui-badge>
|
|
@if (domain()!.domain.defaultWildcard) {
|
|
<ui-badge variant="outline">Wildcard</ui-badge>
|
|
}
|
|
@if (domain()!.domain.isObsolete) {
|
|
<ui-badge variant="destructive">Obsolete</ui-badge>
|
|
}
|
|
</div>
|
|
}
|
|
</div>
|
|
|
|
@if (domain()) {
|
|
<!-- Stats -->
|
|
<div class="grid gap-4 md:grid-cols-3">
|
|
<ui-card>
|
|
<ui-card-header class="flex flex-row items-center justify-between space-y-0 pb-2">
|
|
<ui-card-title class="text-sm font-medium">Certificates</ui-card-title>
|
|
<svg class="h-4 w-4 text-muted-foreground" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
|
|
<path stroke-linecap="round" stroke-linejoin="round" d="M9 12l2 2 4-4m5.618-4.016A11.955 11.955 0 0112 2.944a11.955 11.955 0 01-8.618 3.04A12.02 12.02 0 003 9c0 5.591 3.824 10.29 9 11.622 5.176-1.332 9-6.03 9-11.622 0-1.042-.133-2.052-.382-3.016z" />
|
|
</svg>
|
|
</ui-card-header>
|
|
<ui-card-content>
|
|
<div class="text-2xl font-bold">{{ domain()!.certificates.length }}</div>
|
|
</ui-card-content>
|
|
</ui-card>
|
|
<ui-card>
|
|
<ui-card-header class="flex flex-row items-center justify-between space-y-0 pb-2">
|
|
<ui-card-title class="text-sm font-medium">Requirements</ui-card-title>
|
|
<svg class="h-4 w-4 text-muted-foreground" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
|
|
<path stroke-linecap="round" stroke-linejoin="round" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2m-6 9l2 2 4-4" />
|
|
</svg>
|
|
</ui-card-header>
|
|
<ui-card-content>
|
|
<div class="text-2xl font-bold">{{ domain()!.requirements.length }}</div>
|
|
</ui-card-content>
|
|
</ui-card>
|
|
<ui-card>
|
|
<ui-card-header class="flex flex-row items-center justify-between space-y-0 pb-2">
|
|
<ui-card-title class="text-sm font-medium">Services</ui-card-title>
|
|
<svg class="h-4 w-4 text-muted-foreground" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
|
|
<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>
|
|
</ui-card-header>
|
|
<ui-card-content>
|
|
<div class="text-2xl font-bold">{{ domain()!.serviceCount }}</div>
|
|
</ui-card-content>
|
|
</ui-card>
|
|
</div>
|
|
|
|
<!-- Certificates -->
|
|
<ui-card>
|
|
<ui-card-header class="flex flex-col space-y-1.5">
|
|
<ui-card-title>SSL Certificates</ui-card-title>
|
|
<ui-card-description>Active certificates for this domain</ui-card-description>
|
|
</ui-card-header>
|
|
<ui-card-content class="p-0">
|
|
@if (domain()!.certificates.length === 0) {
|
|
<div class="p-6 text-center text-muted-foreground">No certificates</div>
|
|
} @else {
|
|
<ui-table>
|
|
<ui-table-header>
|
|
<ui-table-row>
|
|
<ui-table-head>Domain</ui-table-head>
|
|
<ui-table-head>Type</ui-table-head>
|
|
<ui-table-head>Status</ui-table-head>
|
|
<ui-table-head>Expires</ui-table-head>
|
|
<ui-table-head>Issuer</ui-table-head>
|
|
<ui-table-head class="text-right">Actions</ui-table-head>
|
|
</ui-table-row>
|
|
</ui-table-header>
|
|
<ui-table-body>
|
|
@for (cert of domain()!.certificates; track cert.id) {
|
|
<ui-table-row>
|
|
<ui-table-cell class="font-medium">{{ cert.certDomain }}</ui-table-cell>
|
|
<ui-table-cell>
|
|
<ui-badge variant="outline">{{ cert.isWildcard ? 'Wildcard' : 'Standard' }}</ui-badge>
|
|
</ui-table-cell>
|
|
<ui-table-cell>
|
|
<ui-badge [variant]="getCertStatusVariant(cert)">
|
|
{{ getCertStatus(cert) }}
|
|
</ui-badge>
|
|
</ui-table-cell>
|
|
<ui-table-cell>
|
|
{{ formatDate(cert.expiryDate) }}
|
|
<span class="text-xs text-muted-foreground ml-1">
|
|
({{ getDaysRemaining(cert.expiryDate) }} days)
|
|
</span>
|
|
</ui-table-cell>
|
|
<ui-table-cell>{{ cert.issuer }}</ui-table-cell>
|
|
<ui-table-cell class="text-right">
|
|
<button uiButton variant="outline" size="sm" (click)="renewCertificate(cert.certDomain)">
|
|
Renew
|
|
</button>
|
|
</ui-table-cell>
|
|
</ui-table-row>
|
|
}
|
|
</ui-table-body>
|
|
</ui-table>
|
|
}
|
|
</ui-card-content>
|
|
</ui-card>
|
|
|
|
<!-- Services using this domain -->
|
|
<ui-card>
|
|
<ui-card-header class="flex flex-col space-y-1.5">
|
|
<ui-card-title>Services</ui-card-title>
|
|
<ui-card-description>Services using this domain</ui-card-description>
|
|
</ui-card-header>
|
|
<ui-card-content class="p-0">
|
|
@if (services().length === 0) {
|
|
<div class="p-6 text-center text-muted-foreground">No services using this domain</div>
|
|
} @else {
|
|
<ui-table>
|
|
<ui-table-header>
|
|
<ui-table-row>
|
|
<ui-table-head>Service</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 (svc of services(); track svc.name) {
|
|
<ui-table-row>
|
|
<ui-table-cell class="font-medium">{{ svc.name }}</ui-table-cell>
|
|
<ui-table-cell>{{ svc.domain }}</ui-table-cell>
|
|
<ui-table-cell>
|
|
<ui-badge [variant]="svc.status === 'running' ? 'success' : 'secondary'">
|
|
{{ svc.status }}
|
|
</ui-badge>
|
|
</ui-table-cell>
|
|
<ui-table-cell class="text-right">
|
|
<a [routerLink]="['/services', svc.name]">
|
|
<button uiButton variant="outline" size="sm">View</button>
|
|
</a>
|
|
</ui-table-cell>
|
|
</ui-table-row>
|
|
}
|
|
</ui-table-body>
|
|
</ui-table>
|
|
}
|
|
</ui-card-content>
|
|
</ui-card>
|
|
}
|
|
</div>
|
|
`,
|
|
})
|
|
export class DomainDetailComponent implements OnInit {
|
|
private route = inject(ActivatedRoute);
|
|
private api = inject(ApiService);
|
|
private toast = inject(ToastService);
|
|
|
|
domain = signal<IDomainDetail | null>(null);
|
|
services = signal<IService[]>([]);
|
|
loading = signal(false);
|
|
|
|
ngOnInit(): void {
|
|
const domainName = this.route.snapshot.paramMap.get('domain');
|
|
if (domainName) {
|
|
this.loadDomain(domainName);
|
|
this.loadServices(domainName);
|
|
}
|
|
}
|
|
|
|
async loadDomain(name: string): Promise<void> {
|
|
this.loading.set(true);
|
|
try {
|
|
const response = await this.api.getDomainDetail(name);
|
|
if (response.success && response.data) {
|
|
this.domain.set(response.data);
|
|
}
|
|
} catch {
|
|
this.toast.error('Failed to load domain');
|
|
} finally {
|
|
this.loading.set(false);
|
|
}
|
|
}
|
|
|
|
async loadServices(domainName: string): Promise<void> {
|
|
try {
|
|
const response = await this.api.getServices();
|
|
if (response.success && response.data) {
|
|
this.services.set(response.data.filter(s => s.domain?.includes(domainName)));
|
|
}
|
|
} catch {
|
|
// Silent fail
|
|
}
|
|
}
|
|
|
|
formatDate(timestamp: number): string {
|
|
return new Date(timestamp).toLocaleDateString();
|
|
}
|
|
|
|
getDaysRemaining(timestamp: number): number {
|
|
const now = Date.now();
|
|
return Math.floor((timestamp - now) / (1000 * 60 * 60 * 24));
|
|
}
|
|
|
|
getCertStatus(cert: any): string {
|
|
if (!cert.isValid) return 'Invalid';
|
|
const days = this.getDaysRemaining(cert.expiryDate);
|
|
if (days < 0) return 'Expired';
|
|
if (days <= 30) return 'Expiring';
|
|
return 'Valid';
|
|
}
|
|
|
|
getCertStatusVariant(cert: any): 'success' | 'warning' | 'destructive' {
|
|
const status = this.getCertStatus(cert);
|
|
switch (status) {
|
|
case 'Valid': return 'success';
|
|
case 'Expiring': return 'warning';
|
|
default: return 'destructive';
|
|
}
|
|
}
|
|
|
|
async renewCertificate(domain: string): Promise<void> {
|
|
try {
|
|
const response = await this.api.renewCertificate(domain);
|
|
if (response.success) {
|
|
this.toast.success('Certificate renewal initiated');
|
|
} else {
|
|
this.toast.error(response.error || 'Failed to renew certificate');
|
|
}
|
|
} catch {
|
|
this.toast.error('Failed to renew certificate');
|
|
}
|
|
}
|
|
}
|