Files
catalog/ts_web/elements/sz-domain-detail-view.ts

769 lines
24 KiB
TypeScript

import {
DeesElement,
customElement,
html,
css,
cssManager,
property,
type TemplateResult,
} from '@design.estate/dees-element';
declare global {
interface HTMLElementTagNameMap {
'sz-domain-detail-view': SzDomainDetailView;
}
}
export interface IDomainDetail {
id: string;
name: string;
status: 'active' | 'pending' | 'error';
verified: boolean;
createdAt: string;
proxyRoutes?: string[];
}
export interface ICertificateDetail {
id: string;
domain: string;
issuer: string;
validFrom: string;
validUntil: string;
daysRemaining: number;
status: 'valid' | 'expiring' | 'expired';
autoRenew: boolean;
chain?: string[];
}
export interface IDnsRecordDetail {
id: string;
type: 'A' | 'AAAA' | 'CNAME' | 'MX' | 'TXT' | 'NS' | 'SRV';
name: string;
value: string;
ttl: number;
priority?: number;
}
@customElement('sz-domain-detail-view')
export class SzDomainDetailView extends DeesElement {
public static demo = () => html`
<div style="padding: 24px; max-width: 1000px;">
<sz-domain-detail-view
.domain=${{
id: '1',
name: 'example.com',
status: 'active',
verified: true,
createdAt: '2024-01-10',
proxyRoutes: ['/api/*', '/app/*'],
}}
.certificate=${{
id: '1',
domain: 'example.com',
issuer: "Let's Encrypt",
validFrom: '2024-01-10',
validUntil: '2024-04-10',
daysRemaining: 45,
status: 'valid',
autoRenew: true,
chain: ['R3', 'ISRG Root X1'],
}}
.dnsRecords=${[
{ id: '1', type: 'A', name: '@', value: '192.168.1.100', ttl: 3600 },
{ id: '2', type: 'CNAME', name: 'www', value: 'example.com', ttl: 3600 },
{ id: '3', type: 'MX', name: '@', value: 'mail.example.com', ttl: 3600, priority: 10 },
{ id: '4', type: 'TXT', name: '@', value: 'v=spf1 include:_spf.example.com ~all', ttl: 3600 },
]}
></sz-domain-detail-view>
</div>
`;
public static demoGroups = ['Network'];
@property({ type: Object })
public accessor domain: IDomainDetail | null = null;
@property({ type: Object })
public accessor certificate: ICertificateDetail | null = null;
@property({ type: Array })
public accessor dnsRecords: IDnsRecordDetail[] = [];
@property({ type: Boolean })
public accessor actionLoading: boolean = false;
public static styles = [
cssManager.defaultStyles,
css`
:host {
display: block;
}
.header {
display: flex;
justify-content: space-between;
align-items: flex-start;
margin-bottom: 24px;
}
.header-info {
display: flex;
flex-direction: column;
gap: 8px;
}
.domain-name {
font-size: 24px;
font-weight: 700;
color: ${cssManager.bdTheme('#18181b', '#fafafa')};
display: flex;
align-items: center;
gap: 12px;
}
.status-badge {
display: inline-flex;
align-items: center;
gap: 6px;
padding: 4px 10px;
border-radius: 9999px;
font-size: 12px;
font-weight: 500;
}
.status-badge.active {
background: ${cssManager.bdTheme('#dcfce7', 'rgba(34, 197, 94, 0.2)')};
color: ${cssManager.bdTheme('#16a34a', '#22c55e')};
}
.status-badge.pending {
background: ${cssManager.bdTheme('#fef3c7', 'rgba(245, 158, 11, 0.2)')};
color: ${cssManager.bdTheme('#d97706', '#f59e0b')};
}
.status-badge.error {
background: ${cssManager.bdTheme('#fee2e2', 'rgba(239, 68, 68, 0.2)')};
color: ${cssManager.bdTheme('#dc2626', '#ef4444')};
}
.status-dot {
width: 6px;
height: 6px;
border-radius: 50%;
background: currentColor;
}
.domain-meta {
display: flex;
align-items: center;
gap: 16px;
font-size: 14px;
color: ${cssManager.bdTheme('#71717a', '#a1a1aa')};
}
.verified-badge {
display: inline-flex;
align-items: center;
gap: 4px;
color: ${cssManager.bdTheme('#16a34a', '#22c55e')};
}
.verified-badge svg {
width: 16px;
height: 16px;
}
.header-actions {
display: flex;
gap: 8px;
}
.action-button {
display: inline-flex;
align-items: center;
gap: 6px;
padding: 8px 14px;
background: ${cssManager.bdTheme('#ffffff', '#09090b')};
border: 1px solid ${cssManager.bdTheme('#e4e4e7', '#27272a')};
border-radius: 6px;
font-size: 13px;
font-weight: 500;
color: ${cssManager.bdTheme('#18181b', '#fafafa')};
cursor: pointer;
transition: all 200ms ease;
}
.action-button:hover:not(:disabled) {
background: ${cssManager.bdTheme('#f4f4f5', '#18181b')};
}
.action-button:disabled {
opacity: 0.6;
cursor: not-allowed;
}
.action-button svg {
width: 14px;
height: 14px;
}
.action-button.danger {
color: ${cssManager.bdTheme('#dc2626', '#ef4444')};
border-color: ${cssManager.bdTheme('#fee2e2', 'rgba(239, 68, 68, 0.3)')};
}
.action-button.danger:hover:not(:disabled) {
background: ${cssManager.bdTheme('#fee2e2', 'rgba(239, 68, 68, 0.2)')};
}
.grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 16px;
margin-bottom: 16px;
}
@media (max-width: 768px) {
.grid {
grid-template-columns: 1fr;
}
}
.section {
background: ${cssManager.bdTheme('#ffffff', '#09090b')};
border: 1px solid ${cssManager.bdTheme('#e4e4e7', '#27272a')};
border-radius: 8px;
overflow: hidden;
}
.section.full-width {
grid-column: 1 / -1;
}
.section-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 14px 16px;
border-bottom: 1px solid ${cssManager.bdTheme('#e4e4e7', '#27272a')};
background: ${cssManager.bdTheme('#f4f4f5', '#18181b')};
}
.section-title {
font-size: 14px;
font-weight: 600;
color: ${cssManager.bdTheme('#18181b', '#fafafa')};
display: flex;
align-items: center;
gap: 8px;
}
.section-title svg {
width: 16px;
height: 16px;
color: ${cssManager.bdTheme('#71717a', '#a1a1aa')};
}
.section-action {
padding: 6px 10px;
background: transparent;
border: 1px solid ${cssManager.bdTheme('#e4e4e7', '#27272a')};
border-radius: 4px;
font-size: 12px;
color: ${cssManager.bdTheme('#71717a', '#a1a1aa')};
cursor: pointer;
transition: all 200ms ease;
}
.section-action:hover {
background: ${cssManager.bdTheme('#ffffff', '#09090b')};
color: ${cssManager.bdTheme('#18181b', '#fafafa')};
}
.section-content {
padding: 16px;
}
.cert-status {
display: flex;
align-items: center;
gap: 12px;
padding: 16px;
background: ${cssManager.bdTheme('#f0fdf4', 'rgba(34, 197, 94, 0.1)')};
border-radius: 8px;
margin-bottom: 16px;
}
.cert-status.expiring {
background: ${cssManager.bdTheme('#fffbeb', 'rgba(245, 158, 11, 0.1)')};
}
.cert-status.expired {
background: ${cssManager.bdTheme('#fef2f2', 'rgba(239, 68, 68, 0.1)')};
}
.cert-icon {
width: 40px;
height: 40px;
border-radius: 8px;
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
}
.cert-icon.valid {
background: ${cssManager.bdTheme('#dcfce7', 'rgba(34, 197, 94, 0.2)')};
color: ${cssManager.bdTheme('#16a34a', '#22c55e')};
}
.cert-icon.expiring {
background: ${cssManager.bdTheme('#fef3c7', 'rgba(245, 158, 11, 0.2)')};
color: ${cssManager.bdTheme('#d97706', '#f59e0b')};
}
.cert-icon.expired {
background: ${cssManager.bdTheme('#fee2e2', 'rgba(239, 68, 68, 0.2)')};
color: ${cssManager.bdTheme('#dc2626', '#ef4444')};
}
.cert-icon svg {
width: 20px;
height: 20px;
}
.cert-info {
flex: 1;
}
.cert-title {
font-size: 14px;
font-weight: 600;
color: ${cssManager.bdTheme('#18181b', '#fafafa')};
}
.cert-subtitle {
font-size: 13px;
color: ${cssManager.bdTheme('#71717a', '#a1a1aa')};
margin-top: 2px;
}
.cert-days {
font-size: 24px;
font-weight: 700;
text-align: center;
}
.cert-days.valid {
color: ${cssManager.bdTheme('#16a34a', '#22c55e')};
}
.cert-days.expiring {
color: ${cssManager.bdTheme('#d97706', '#f59e0b')};
}
.cert-days.expired {
color: ${cssManager.bdTheme('#dc2626', '#ef4444')};
}
.cert-days-label {
font-size: 11px;
color: ${cssManager.bdTheme('#71717a', '#a1a1aa')};
text-align: center;
margin-top: 2px;
}
.info-row {
display: flex;
justify-content: space-between;
align-items: center;
padding: 10px 0;
border-bottom: 1px solid ${cssManager.bdTheme('#f4f4f5', '#27272a')};
}
.info-row:last-child {
border-bottom: none;
}
.info-label {
font-size: 13px;
color: ${cssManager.bdTheme('#71717a', '#a1a1aa')};
}
.info-value {
font-size: 13px;
font-weight: 500;
color: ${cssManager.bdTheme('#18181b', '#fafafa')};
}
.info-value.enabled {
color: ${cssManager.bdTheme('#16a34a', '#22c55e')};
}
.chain-list {
display: flex;
flex-wrap: wrap;
gap: 8px;
}
.chain-badge {
padding: 4px 8px;
background: ${cssManager.bdTheme('#f4f4f5', '#27272a')};
border-radius: 4px;
font-size: 12px;
color: ${cssManager.bdTheme('#71717a', '#a1a1aa')};
}
.dns-table {
width: 100%;
}
.dns-header {
display: grid;
grid-template-columns: 80px 1fr 2fr 80px 60px;
gap: 12px;
padding: 10px 0;
font-size: 12px;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.05em;
color: ${cssManager.bdTheme('#71717a', '#a1a1aa')};
border-bottom: 1px solid ${cssManager.bdTheme('#e4e4e7', '#27272a')};
}
.dns-row {
display: grid;
grid-template-columns: 80px 1fr 2fr 80px 60px;
gap: 12px;
padding: 12px 0;
font-size: 13px;
color: ${cssManager.bdTheme('#18181b', '#fafafa')};
border-bottom: 1px solid ${cssManager.bdTheme('#f4f4f5', '#27272a')};
align-items: center;
}
.dns-row:last-child {
border-bottom: none;
}
.dns-row:hover {
background: ${cssManager.bdTheme('#f4f4f5', '#18181b')};
margin: 0 -16px;
padding-left: 16px;
padding-right: 16px;
}
.dns-type {
padding: 2px 8px;
background: ${cssManager.bdTheme('#dbeafe', 'rgba(59, 130, 246, 0.2)')};
color: ${cssManager.bdTheme('#2563eb', '#60a5fa')};
border-radius: 4px;
font-size: 11px;
font-weight: 600;
text-align: center;
}
.dns-name {
font-family: monospace;
}
.dns-value {
font-family: monospace;
color: ${cssManager.bdTheme('#71717a', '#a1a1aa')};
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.dns-ttl {
color: ${cssManager.bdTheme('#71717a', '#a1a1aa')};
}
.dns-actions {
display: flex;
gap: 4px;
}
.icon-button {
padding: 4px;
background: transparent;
border: none;
color: ${cssManager.bdTheme('#71717a', '#a1a1aa')};
cursor: pointer;
border-radius: 4px;
transition: all 200ms ease;
}
.icon-button:hover {
background: ${cssManager.bdTheme('#e4e4e7', '#27272a')};
color: ${cssManager.bdTheme('#18181b', '#fafafa')};
}
.icon-button.danger:hover {
background: ${cssManager.bdTheme('#fee2e2', 'rgba(239, 68, 68, 0.2)')};
color: ${cssManager.bdTheme('#dc2626', '#ef4444')};
}
.icon-button svg {
width: 14px;
height: 14px;
}
.routes-list {
display: flex;
flex-wrap: wrap;
gap: 8px;
}
.route-badge {
padding: 6px 10px;
background: ${cssManager.bdTheme('#f4f4f5', '#27272a')};
border-radius: 4px;
font-size: 13px;
font-family: monospace;
color: ${cssManager.bdTheme('#18181b', '#fafafa')};
}
.empty-state {
text-align: center;
padding: 24px;
color: ${cssManager.bdTheme('#71717a', '#a1a1aa')};
font-size: 14px;
}
`,
];
public render(): TemplateResult {
if (!this.domain) {
return html`<div class="empty-state">No domain selected</div>`;
}
return html`
<div class="header">
<div class="header-info">
<div class="domain-name">
${this.domain.name}
<span class="status-badge ${this.domain.status}">
<span class="status-dot"></span>
${this.domain.status.charAt(0).toUpperCase() + this.domain.status.slice(1)}
</span>
</div>
<div class="domain-meta">
${this.domain.verified ? html`
<span class="verified-badge">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M22 11.08V12a10 10 0 1 1-5.93-9.14"></path>
<polyline points="22 4 12 14.01 9 11.01"></polyline>
</svg>
Verified
</span>
` : html`<span>Not verified</span>`}
<span>Added ${this.domain.createdAt}</span>
</div>
</div>
<div class="header-actions">
${!this.domain.verified ? html`
<button class="action-button" ?disabled=${this.actionLoading} @click=${() => this.handleVerify()}>
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<polyline points="20 6 9 17 4 12"></polyline>
</svg>
Verify Domain
</button>
` : ''}
<button class="action-button danger" ?disabled=${this.actionLoading} @click=${() => this.handleDelete()}>
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<polyline points="3 6 5 6 21 6"></polyline>
<path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"></path>
</svg>
Delete
</button>
</div>
</div>
<div class="grid">
<!-- Certificate Section -->
<div class="section">
<div class="section-header">
<div class="section-title">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<rect x="3" y="11" width="18" height="11" rx="2" ry="2"></rect>
<path d="M7 11V7a5 5 0 0 1 10 0v4"></path>
</svg>
SSL Certificate
</div>
${this.certificate ? html`
<button class="section-action" @click=${() => this.handleRenewCertificate()}>Renew</button>
` : ''}
</div>
<div class="section-content">
${this.certificate ? html`
<div class="cert-status ${this.certificate.status}">
<div class="cert-icon ${this.certificate.status}">
${this.certificate.status === 'valid' ? html`
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M22 11.08V12a10 10 0 1 1-5.93-9.14"></path>
<polyline points="22 4 12 14.01 9 11.01"></polyline>
</svg>
` : this.certificate.status === 'expiring' ? html`
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<circle cx="12" cy="12" r="10"></circle>
<line x1="12" y1="8" x2="12" y2="12"></line>
<line x1="12" y1="16" x2="12.01" y2="16"></line>
</svg>
` : html`
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<circle cx="12" cy="12" r="10"></circle>
<line x1="15" y1="9" x2="9" y2="15"></line>
<line x1="9" y1="9" x2="15" y2="15"></line>
</svg>
`}
</div>
<div class="cert-info">
<div class="cert-title">${this.certificate.status === 'valid' ? 'Certificate Valid' : this.certificate.status === 'expiring' ? 'Certificate Expiring Soon' : 'Certificate Expired'}</div>
<div class="cert-subtitle">Issued by ${this.certificate.issuer}</div>
</div>
<div>
<div class="cert-days ${this.certificate.status}">${Math.abs(this.certificate.daysRemaining)}</div>
<div class="cert-days-label">${this.certificate.daysRemaining >= 0 ? 'days left' : 'days ago'}</div>
</div>
</div>
<div class="info-row">
<span class="info-label">Valid From</span>
<span class="info-value">${this.certificate.validFrom}</span>
</div>
<div class="info-row">
<span class="info-label">Valid Until</span>
<span class="info-value">${this.certificate.validUntil}</span>
</div>
<div class="info-row">
<span class="info-label">Auto-Renew</span>
<span class="info-value ${this.certificate.autoRenew ? 'enabled' : ''}">${this.certificate.autoRenew ? 'Enabled' : 'Disabled'}</span>
</div>
${this.certificate.chain && this.certificate.chain.length > 0 ? html`
<div class="info-row">
<span class="info-label">Certificate Chain</span>
<div class="chain-list">
${this.certificate.chain.map(cert => html`<span class="chain-badge">${cert}</span>`)}
</div>
</div>
` : ''}
` : html`
<div class="empty-state">No certificate configured</div>
`}
</div>
</div>
<!-- Proxy Routes Section -->
<div class="section">
<div class="section-header">
<div class="section-title">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<polyline points="16 3 21 3 21 8"></polyline>
<line x1="4" y1="20" x2="21" y2="3"></line>
<polyline points="21 16 21 21 16 21"></polyline>
<line x1="15" y1="15" x2="21" y2="21"></line>
<line x1="4" y1="4" x2="9" y2="9"></line>
</svg>
Proxy Routes
</div>
</div>
<div class="section-content">
${this.domain.proxyRoutes && this.domain.proxyRoutes.length > 0 ? html`
<div class="routes-list">
${this.domain.proxyRoutes.map(route => html`
<span class="route-badge">${route}</span>
`)}
</div>
` : html`
<div class="empty-state">No proxy routes configured</div>
`}
</div>
</div>
<!-- DNS Records Section -->
<div class="section full-width">
<div class="section-header">
<div class="section-title">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<circle cx="12" cy="12" r="10"></circle>
<line x1="2" y1="12" x2="22" y2="12"></line>
<path d="M12 2a15.3 15.3 0 0 1 4 10 15.3 15.3 0 0 1-4 10 15.3 15.3 0 0 1-4-10 15.3 15.3 0 0 1 4-10z"></path>
</svg>
DNS Records
</div>
<button class="section-action" @click=${() => this.handleAddDnsRecord()}>
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="margin-right: 4px;">
<line x1="12" y1="5" x2="12" y2="19"></line>
<line x1="5" y1="12" x2="19" y2="12"></line>
</svg>
Add Record
</button>
</div>
<div class="section-content">
${this.dnsRecords.length > 0 ? html`
<div class="dns-table">
<div class="dns-header">
<span>Type</span>
<span>Name</span>
<span>Value</span>
<span>TTL</span>
<span></span>
</div>
${this.dnsRecords.map(record => html`
<div class="dns-row">
<span class="dns-type">${record.type}</span>
<span class="dns-name">${record.name}</span>
<span class="dns-value" title="${record.value}">${record.priority ? `${record.priority} ` : ''}${record.value}</span>
<span class="dns-ttl">${record.ttl}s</span>
<span class="dns-actions">
<button class="icon-button" title="Edit" @click=${() => this.handleEditDnsRecord(record)}>
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"></path>
<path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z"></path>
</svg>
</button>
<button class="icon-button danger" title="Delete" @click=${() => this.handleDeleteDnsRecord(record)}>
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<polyline points="3 6 5 6 21 6"></polyline>
<path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"></path>
</svg>
</button>
</span>
</div>
`)}
</div>
` : html`
<div class="empty-state">No DNS records configured</div>
`}
</div>
</div>
</div>
`;
}
private handleVerify() {
this.dispatchEvent(new CustomEvent('verify-domain', { detail: this.domain, bubbles: true, composed: true }));
}
private handleDelete() {
this.dispatchEvent(new CustomEvent('delete-domain', { detail: this.domain, bubbles: true, composed: true }));
}
private handleRenewCertificate() {
this.dispatchEvent(new CustomEvent('renew-certificate', { detail: this.certificate, bubbles: true, composed: true }));
}
private handleAddDnsRecord() {
this.dispatchEvent(new CustomEvent('add-dns-record', { detail: this.domain, bubbles: true, composed: true }));
}
private handleEditDnsRecord(record: IDnsRecordDetail) {
this.dispatchEvent(new CustomEvent('edit-dns-record', { detail: record, bubbles: true, composed: true }));
}
private handleDeleteDnsRecord(record: IDnsRecordDetail) {
this.dispatchEvent(new CustomEvent('delete-dns-record', { detail: record, bubbles: true, composed: true }));
}
}