feat: integrate toast notifications in settings and layout components
- Added ToastService for managing toast notifications. - Replaced alert in settings component with toast notifications for success and error messages. - Included ToastComponent in layout for displaying notifications. - Created loading spinner component for better user experience. - Implemented domain detail component with detailed views for certificates, requirements, and services. - Added functionality to manage and display SSL certificates and their statuses. - Introduced a registry manager class for handling Docker registry operations.
This commit is contained in:
356
ui/src/app/features/domains/domain-detail.component.ts
Normal file
356
ui/src/app/features/domains/domain-detail.component.ts
Normal file
@@ -0,0 +1,356 @@
|
||||
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}`;
|
||||
}
|
||||
}
|
||||
@@ -1,86 +1,216 @@
|
||||
import { Component, OnInit, inject, signal } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { RouterLink } from '@angular/router';
|
||||
import { ApiService } from '../../core/services/api.service';
|
||||
import { ToastService } from '../../core/services/toast.service';
|
||||
|
||||
interface DomainView {
|
||||
domain: {
|
||||
id: number;
|
||||
domain: string;
|
||||
dnsProvider: 'cloudflare' | 'manual' | null;
|
||||
isObsolete: boolean;
|
||||
defaultWildcard: boolean;
|
||||
};
|
||||
serviceCount: number;
|
||||
certificateStatus: 'valid' | 'expiring-soon' | 'expired' | 'pending' | 'none';
|
||||
daysRemaining: number | null;
|
||||
certificates: any[];
|
||||
requirements: any[];
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: 'app-ssl',
|
||||
selector: 'app-domains',
|
||||
standalone: true,
|
||||
imports: [CommonModule],
|
||||
imports: [CommonModule, RouterLink],
|
||||
template: `
|
||||
<div class="px-4 sm:px-0">
|
||||
<h1 class="text-3xl font-bold text-gray-900 mb-8">SSL Certificates</h1>
|
||||
<div class="flex justify-between items-center mb-8">
|
||||
<h1 class="text-3xl font-bold text-gray-900">Domains</h1>
|
||||
<button
|
||||
(click)="syncDomains()"
|
||||
[disabled]="syncing()"
|
||||
class="btn btn-primary"
|
||||
>
|
||||
{{ syncing() ? 'Syncing...' : 'Sync Cloudflare' }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@if (certificates().length > 0) {
|
||||
@if (loading()) {
|
||||
<div class="card text-center py-12">
|
||||
<p class="text-gray-500">Loading domains...</p>
|
||||
</div>
|
||||
} @else if (domains().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">Issuer</th>
|
||||
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">Provider</th>
|
||||
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">Services</th>
|
||||
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">Certificate</th>
|
||||
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">Expiry</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 (cert of certificates(); track cert.domain) {
|
||||
<tr>
|
||||
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-900">{{ cert.domain }}</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-900">{{ cert.issuer }}</td>
|
||||
@for (domainView of domains(); track domainView.domain.id) {
|
||||
<tr [class.opacity-50]="domainView.domain.isObsolete">
|
||||
<td class="px-6 py-4 whitespace-nowrap">
|
||||
<div class="text-sm font-medium text-gray-900">{{ domainView.domain.domain }}</div>
|
||||
@if (domainView.domain.isObsolete) {
|
||||
<span class="text-xs text-red-600">Obsolete</span>
|
||||
}
|
||||
</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap">
|
||||
@if (domainView.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 (domainView.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
|
||||
</span>
|
||||
} @else {
|
||||
<span class="text-sm text-gray-400">None</span>
|
||||
}
|
||||
</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-900">
|
||||
{{ domainView.serviceCount }}
|
||||
</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap">
|
||||
@switch (domainView.certificateStatus) {
|
||||
@case ('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>
|
||||
}
|
||||
@case ('expiring-soon') {
|
||||
<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>
|
||||
}
|
||||
@case ('expired') {
|
||||
<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
|
||||
</span>
|
||||
}
|
||||
@case ('pending') {
|
||||
<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-blue-100 text-blue-800">
|
||||
Pending
|
||||
</span>
|
||||
}
|
||||
@default {
|
||||
<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-gray-100 text-gray-800">
|
||||
None
|
||||
</span>
|
||||
}
|
||||
}
|
||||
</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap text-sm">
|
||||
<span [ngClass]="isExpiringSoon(cert.expiryDate) ? 'text-red-600' : 'text-gray-500'">
|
||||
{{ formatDate(cert.expiryDate) }}
|
||||
</span>
|
||||
@if (domainView.daysRemaining !== null) {
|
||||
<span [ngClass]="domainView.daysRemaining <= 30 ? 'text-red-600 font-medium' : 'text-gray-500'">
|
||||
{{ domainView.daysRemaining }} days
|
||||
</span>
|
||||
} @else {
|
||||
<span class="text-gray-400">—</span>
|
||||
}
|
||||
</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap text-right text-sm">
|
||||
<button (click)="renewCertificate(cert)" class="text-primary-600 hover:text-primary-900">Renew</button>
|
||||
<a
|
||||
[routerLink]="['/domains', domainView.domain.domain]"
|
||||
class="text-primary-600 hover:text-primary-900"
|
||||
>
|
||||
View Details
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<!-- Summary Stats -->
|
||||
<div class="mt-6 grid grid-cols-1 gap-5 sm:grid-cols-4">
|
||||
<div class="card">
|
||||
<dt class="text-sm font-medium text-gray-500 truncate">Total Domains</dt>
|
||||
<dd class="mt-1 text-3xl font-semibold text-gray-900">{{ domains().length }}</dd>
|
||||
</div>
|
||||
<div class="card">
|
||||
<dt class="text-sm font-medium text-gray-500 truncate">Valid Certificates</dt>
|
||||
<dd class="mt-1 text-3xl font-semibold text-green-600">{{ getStatusCount('valid') }}</dd>
|
||||
</div>
|
||||
<div class="card">
|
||||
<dt class="text-sm font-medium text-gray-500 truncate">Expiring Soon</dt>
|
||||
<dd class="mt-1 text-3xl font-semibold text-yellow-600">{{ getStatusCount('expiring-soon') }}</dd>
|
||||
</div>
|
||||
<div class="card">
|
||||
<dt class="text-sm font-medium text-gray-500 truncate">Expired/Pending</dt>
|
||||
<dd class="mt-1 text-3xl font-semibold text-red-600">{{ getStatusCount('expired') + getStatusCount('pending') }}</dd>
|
||||
</div>
|
||||
</div>
|
||||
} @else {
|
||||
<div class="card text-center py-12">
|
||||
<p class="text-gray-500">No SSL certificates</p>
|
||||
<p class="text-sm text-gray-400 mt-2">Certificates are obtained automatically when deploying services with domains</p>
|
||||
<p class="text-gray-500">No domains found</p>
|
||||
<p class="text-sm text-gray-400 mt-2">
|
||||
Sync your Cloudflare zones or manually add domains to get started
|
||||
</p>
|
||||
<button
|
||||
(click)="syncDomains()"
|
||||
class="mt-4 btn btn-primary"
|
||||
>
|
||||
Sync Cloudflare Domains
|
||||
</button>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
`,
|
||||
})
|
||||
export class SslComponent implements OnInit {
|
||||
export class DomainsComponent implements OnInit {
|
||||
private apiService = inject(ApiService);
|
||||
certificates = signal<any[]>([]);
|
||||
private toastService = inject(ToastService);
|
||||
|
||||
domains = signal<DomainView[]>([]);
|
||||
loading = signal(true);
|
||||
syncing = signal(false);
|
||||
|
||||
ngOnInit(): void {
|
||||
this.loadCertificates();
|
||||
this.loadDomains();
|
||||
}
|
||||
|
||||
loadCertificates(): void {
|
||||
this.apiService.getSslCertificates().subscribe({
|
||||
loadDomains(): void {
|
||||
this.loading.set(true);
|
||||
this.apiService.getDomains().subscribe({
|
||||
next: (response) => {
|
||||
if (response.success && response.data) {
|
||||
this.certificates.set(response.data);
|
||||
this.domains.set(response.data);
|
||||
}
|
||||
this.loading.set(false);
|
||||
},
|
||||
error: () => {
|
||||
this.loading.set(false);
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
renewCertificate(cert: any): void {
|
||||
this.apiService.renewSslCertificate(cert.domain).subscribe({
|
||||
next: () => {
|
||||
alert('Certificate renewal initiated');
|
||||
this.loadCertificates();
|
||||
syncDomains(): void {
|
||||
this.syncing.set(true);
|
||||
this.apiService.syncCloudflareDomains().subscribe({
|
||||
next: (response) => {
|
||||
if (response.success) {
|
||||
this.toastService.success('Cloudflare domains synced successfully');
|
||||
this.loadDomains();
|
||||
}
|
||||
this.syncing.set(false);
|
||||
},
|
||||
error: (error) => {
|
||||
this.toastService.error('Failed to sync Cloudflare domains: ' + (error.error?.error || error.message));
|
||||
this.syncing.set(false);
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
formatDate(timestamp: number): string {
|
||||
return new Date(timestamp).toLocaleDateString();
|
||||
}
|
||||
|
||||
isExpiringSoon(timestamp: number): boolean {
|
||||
const thirtyDays = 30 * 24 * 60 * 60 * 1000;
|
||||
return timestamp - Date.now() < thirtyDays;
|
||||
getStatusCount(status: string): number {
|
||||
return this.domains().filter(d => d.certificateStatus === status).length;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user