- Removed DNS and Domains entries from the layout navigation. - Added DnsContentComponent for managing DNS records with Cloudflare. - Added DomainsContentComponent for managing domains and SSL certificates. - Introduced TabsComponent and TabComponent for tab navigation. - Updated index.ts to export new TabsComponent and TabComponent.
238 lines
9.6 KiB
TypeScript
238 lines
9.6 KiB
TypeScript
import { Component, inject, signal, OnInit } from '@angular/core';
|
|
import { RouterLink } from '@angular/router';
|
|
import { ApiService } from '../../core/services/api.service';
|
|
import { ToastService } from '../../core/services/toast.service';
|
|
import { IDomainDetail } from '../../core/types/api.types';
|
|
import {
|
|
CardComponent,
|
|
CardHeaderComponent,
|
|
CardTitleComponent,
|
|
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-domains-content',
|
|
standalone: true,
|
|
imports: [
|
|
RouterLink,
|
|
CardComponent,
|
|
CardHeaderComponent,
|
|
CardTitleComponent,
|
|
CardContentComponent,
|
|
ButtonComponent,
|
|
BadgeComponent,
|
|
TableComponent,
|
|
TableHeaderComponent,
|
|
TableBodyComponent,
|
|
TableRowComponent,
|
|
TableHeadComponent,
|
|
TableCellComponent,
|
|
SkeletonComponent,
|
|
],
|
|
template: `
|
|
<div class="space-y-6">
|
|
<div class="flex items-center justify-between">
|
|
<p class="text-muted-foreground">Manage domains and SSL certificates</p>
|
|
<button uiButton (click)="syncDomains()" [disabled]="syncing()">
|
|
@if (syncing()) {
|
|
<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>
|
|
Syncing...
|
|
} @else {
|
|
Sync Cloudflare
|
|
}
|
|
</button>
|
|
</div>
|
|
|
|
<!-- Stats -->
|
|
<div class="grid gap-4 md:grid-cols-4">
|
|
<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">Total Domains</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="M21 12a9 9 0 01-9 9m9-9a9 9 0 00-9-9m9 9H3m9 9a9 9 0 01-9-9m9 9c1.657 0 3-4.03 3-9s-1.343-9-3-9m0 18c-1.657 0-3-4.03-3-9s1.343-9 3-9m-9 9a9 9 0 019-9" />
|
|
</svg>
|
|
</ui-card-header>
|
|
<ui-card-content>
|
|
<div class="text-2xl font-bold">{{ domains().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">Valid Certificates</ui-card-title>
|
|
<svg class="h-4 w-4 text-success" 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 text-success">{{ countByStatus('valid') }}</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">Expiring Soon</ui-card-title>
|
|
<svg class="h-4 w-4 text-warning" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
|
|
<path stroke-linecap="round" stroke-linejoin="round" d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z" />
|
|
</svg>
|
|
</ui-card-header>
|
|
<ui-card-content>
|
|
<div class="text-2xl font-bold text-warning">{{ countByStatus('expiring-soon') }}</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">Expired/Pending</ui-card-title>
|
|
<svg class="h-4 w-4 text-destructive" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
|
|
<path stroke-linecap="round" stroke-linejoin="round" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z" />
|
|
</svg>
|
|
</ui-card-header>
|
|
<ui-card-content>
|
|
<div class="text-2xl font-bold text-destructive">{{ countByStatus('expired') + countByStatus('pending') }}</div>
|
|
</ui-card-content>
|
|
</ui-card>
|
|
</div>
|
|
|
|
<!-- Domains Table -->
|
|
<ui-card>
|
|
<ui-card-content class="p-0">
|
|
@if (loading() && domains().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 (domains().length === 0) {
|
|
<div class="p-12 text-center">
|
|
<h3 class="text-lg font-semibold">No domains found</h3>
|
|
<p class="mt-2 text-sm text-muted-foreground">Sync domains from Cloudflare to get started.</p>
|
|
<button uiButton class="mt-4" (click)="syncDomains()">Sync Cloudflare Domains</button>
|
|
</div>
|
|
} @else {
|
|
<ui-table>
|
|
<ui-table-header>
|
|
<ui-table-row>
|
|
<ui-table-head>Domain</ui-table-head>
|
|
<ui-table-head>Provider</ui-table-head>
|
|
<ui-table-head>Services</ui-table-head>
|
|
<ui-table-head>Certificate</ui-table-head>
|
|
<ui-table-head>Expires</ui-table-head>
|
|
<ui-table-head class="text-right">Actions</ui-table-head>
|
|
</ui-table-row>
|
|
</ui-table-header>
|
|
<ui-table-body>
|
|
@for (d of domains(); track d.domain.id) {
|
|
<ui-table-row [class.opacity-50]="d.domain.isObsolete">
|
|
<ui-table-cell>
|
|
<div class="flex items-center gap-2">
|
|
<span class="font-medium">{{ d.domain.domain }}</span>
|
|
@if (d.domain.isObsolete) {
|
|
<ui-badge variant="destructive">Obsolete</ui-badge>
|
|
}
|
|
</div>
|
|
</ui-table-cell>
|
|
<ui-table-cell>
|
|
<ui-badge [variant]="d.domain.dnsProvider === 'cloudflare' ? 'default' : 'secondary'">
|
|
{{ d.domain.dnsProvider || 'None' }}
|
|
</ui-badge>
|
|
</ui-table-cell>
|
|
<ui-table-cell>{{ d.serviceCount }}</ui-table-cell>
|
|
<ui-table-cell>
|
|
<ui-badge [variant]="getCertStatusVariant(d.certificateStatus)">
|
|
{{ d.certificateStatus }}
|
|
</ui-badge>
|
|
</ui-table-cell>
|
|
<ui-table-cell>
|
|
@if (d.daysRemaining !== null) {
|
|
<span [class.text-destructive]="d.daysRemaining <= 30">
|
|
{{ d.daysRemaining }} days
|
|
</span>
|
|
} @else {
|
|
<span class="text-muted-foreground">-</span>
|
|
}
|
|
</ui-table-cell>
|
|
<ui-table-cell class="text-right">
|
|
<a [routerLink]="['/network/domains', d.domain.domain]">
|
|
<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 DomainsContentComponent implements OnInit {
|
|
private api = inject(ApiService);
|
|
private toast = inject(ToastService);
|
|
|
|
domains = signal<IDomainDetail[]>([]);
|
|
loading = signal(false);
|
|
syncing = signal(false);
|
|
|
|
ngOnInit(): void {
|
|
this.loadDomains();
|
|
}
|
|
|
|
async loadDomains(): Promise<void> {
|
|
this.loading.set(true);
|
|
try {
|
|
const response = await this.api.getDomains();
|
|
if (response.success && response.data) {
|
|
this.domains.set(response.data);
|
|
}
|
|
} catch {
|
|
this.toast.error('Failed to load domains');
|
|
} finally {
|
|
this.loading.set(false);
|
|
}
|
|
}
|
|
|
|
async syncDomains(): Promise<void> {
|
|
this.syncing.set(true);
|
|
try {
|
|
const response = await this.api.syncCloudflareDomains();
|
|
if (response.success) {
|
|
this.toast.success('Domains synced');
|
|
this.loadDomains();
|
|
} else {
|
|
this.toast.error(response.error || 'Failed to sync domains');
|
|
}
|
|
} catch {
|
|
this.toast.error('Failed to sync domains');
|
|
} finally {
|
|
this.syncing.set(false);
|
|
}
|
|
}
|
|
|
|
countByStatus(status: string): number {
|
|
return this.domains().filter(d => d.certificateStatus === status).length;
|
|
}
|
|
|
|
getCertStatusVariant(status: string): 'success' | 'warning' | 'destructive' | 'secondary' {
|
|
switch (status) {
|
|
case 'valid': return 'success';
|
|
case 'expiring-soon': return 'warning';
|
|
case 'expired': return 'destructive';
|
|
case 'pending': return 'secondary';
|
|
default: return 'secondary';
|
|
}
|
|
}
|
|
}
|