import * as plugins from '../plugins.js'; import * as shared from './shared/index.js'; import * as appstate from '../appstate.js'; import { DeesElement, customElement, html, state, css, cssManager, type TemplateResult, } from '@design.estate/dees-element'; @customElement('ops-view-config') export class OpsViewConfig extends DeesElement { @state() accessor configState: appstate.IConfigState = { config: null, isLoading: false, error: null, }; constructor() { super(); const subscription = appstate.configStatePart .select((stateArg) => stateArg) .subscribe((configState) => { this.configState = configState; }); this.rxSubscriptions.push(subscription); } public static styles = [ cssManager.defaultStyles, shared.viewHostCss, css` .configSection { background: ${cssManager.bdTheme('#fff', '#222')}; border: 1px solid ${cssManager.bdTheme('#e9ecef', '#333')}; border-radius: 8px; margin-bottom: 24px; overflow: hidden; } .sectionHeader { background: ${cssManager.bdTheme('#f8f9fa', '#1a1a1a')}; padding: 16px 24px; border-bottom: 1px solid ${cssManager.bdTheme('#e9ecef', '#333')}; display: flex; justify-content: space-between; align-items: center; } .sectionTitle { font-size: 18px; font-weight: 600; color: ${cssManager.bdTheme('#333', '#ccc')}; display: flex; align-items: center; gap: 12px; } .sectionTitle dees-icon { font-size: 20px; color: ${cssManager.bdTheme('#666', '#888')}; } .sectionContent { padding: 24px; } .configField { margin-bottom: 20px; } .configField:last-child { margin-bottom: 0; } .fieldLabel { font-size: 13px; font-weight: 600; color: ${cssManager.bdTheme('#666', '#999')}; margin-bottom: 8px; display: block; text-transform: uppercase; letter-spacing: 0.5px; } .fieldValue { font-family: 'Consolas', 'Monaco', monospace; font-size: 14px; color: ${cssManager.bdTheme('#333', '#ccc')}; background: ${cssManager.bdTheme('#f8f9fa', '#1a1a1a')}; padding: 10px 14px; border-radius: 6px; border: 1px solid ${cssManager.bdTheme('#e9ecef', '#333')}; } .fieldValue.empty { color: ${cssManager.bdTheme('#999', '#666')}; font-style: italic; } .nestedFields { margin-left: 16px; padding-left: 16px; border-left: 2px solid ${cssManager.bdTheme('#e9ecef', '#333')}; } /* Status badge styles */ .statusBadge { display: inline-flex; align-items: center; gap: 6px; padding: 4px 12px; border-radius: 20px; font-size: 13px; font-weight: 600; } .statusBadge.enabled { background: ${cssManager.bdTheme('#d4edda', '#1a3d1a')}; color: ${cssManager.bdTheme('#155724', '#66cc66')}; } .statusBadge.disabled { background: ${cssManager.bdTheme('#f8d7da', '#3d1a1a')}; color: ${cssManager.bdTheme('#721c24', '#cc6666')}; } .statusBadge dees-icon { font-size: 14px; } /* Array/list display */ .arrayItems { display: flex; flex-wrap: wrap; gap: 8px; } .arrayItem { display: inline-flex; align-items: center; background: ${cssManager.bdTheme('#e7f3ff', '#1a2a3d')}; color: ${cssManager.bdTheme('#0066cc', '#66aaff')}; padding: 4px 12px; border-radius: 16px; font-size: 13px; font-family: 'Consolas', 'Monaco', monospace; } .arrayCount { font-size: 12px; color: ${cssManager.bdTheme('#999', '#666')}; margin-bottom: 8px; } /* Numeric value formatting */ .numericValue { font-weight: 600; color: ${cssManager.bdTheme('#0066cc', '#66aaff')}; } .errorMessage { background: ${cssManager.bdTheme('#fee', '#4a1f1f')}; border: 1px solid ${cssManager.bdTheme('#fcc', '#6a2f2f')}; border-radius: 4px; padding: 16px; color: ${cssManager.bdTheme('#c00', '#ff6666')}; margin: 16px 0; } .loadingMessage { text-align: center; padding: 40px; color: ${cssManager.bdTheme('#666', '#999')}; } .infoNote { background: ${cssManager.bdTheme('#e7f3ff', '#1a2a3d')}; border: 1px solid ${cssManager.bdTheme('#b3d7ff', '#2a4a6d')}; border-radius: 8px; padding: 16px; margin-bottom: 24px; color: ${cssManager.bdTheme('#004085', '#88ccff')}; display: flex; align-items: center; gap: 12px; } .infoNote dees-icon { font-size: 20px; flex-shrink: 0; } `, ]; public render() { return html` Configuration ${this.configState.isLoading ? html`

Loading configuration...

` : this.configState.error ? html`
Error loading configuration: ${this.configState.error}
` : this.configState.config ? html`
This view displays the current running configuration. DcRouter is configured through code or remote management.
${this.renderConfigSection('email', 'Email', 'lucide:mail', this.configState.config?.email)} ${this.renderConfigSection('dns', 'DNS', 'lucide:globe', this.configState.config?.dns)} ${this.renderConfigSection('proxy', 'Proxy', 'lucide:network', this.configState.config?.proxy)} ${this.renderConfigSection('security', 'Security', 'lucide:shield', this.configState.config?.security)} ` : html`
No configuration loaded
`} `; } private renderConfigSection(key: string, title: string, icon: string, config: any) { const isEnabled = config?.enabled ?? false; return html`

${title}

${this.renderStatusBadge(isEnabled)}
${config ? this.renderConfigFields(config) : html`
Not configured
`}
`; } private renderStatusBadge(enabled: boolean): TemplateResult { return enabled ? html`Enabled` : html`Disabled`; } private renderConfigFields(config: any, prefix = ''): TemplateResult | TemplateResult[] { if (!config || typeof config !== 'object') { return html`
${this.formatValue(config)}
`; } return Object.entries(config).map(([key, value]) => { const fieldName = prefix ? `${prefix}.${key}` : key; const displayName = this.formatFieldName(key); // Handle boolean values with badges if (typeof value === 'boolean') { return html`
${this.renderStatusBadge(value)}
`; } // Handle arrays if (Array.isArray(value)) { return html`
${this.renderArrayValue(value, key)}
`; } // Handle nested objects if (typeof value === 'object' && value !== null) { return html`
${this.renderConfigFields(value, fieldName)}
`; } // Handle primitive values return html`
${this.formatValue(value, key)}
`; }); } private renderArrayValue(arr: any[], fieldKey: string): TemplateResult { if (arr.length === 0) { return html`
None configured
`; } // Determine if we should show as pills/tags const showAsPills = arr.every(item => typeof item === 'string' || typeof item === 'number'); if (showAsPills) { const itemLabel = this.getArrayItemLabel(fieldKey, arr.length); return html`
${arr.length} ${itemLabel}
${arr.map(item => html`${item}`)}
`; } // For complex arrays, show as JSON return html`
${arr.length} items configured
`; } private getArrayItemLabel(fieldKey: string, count: number): string { const labels: Record = { ports: ['port', 'ports'], domains: ['domain', 'domains'], nameservers: ['nameserver', 'nameservers'], blockList: ['IP', 'IPs'], }; const label = labels[fieldKey] || ['item', 'items']; return count === 1 ? label[0] : label[1]; } private formatFieldName(key: string): string { // Convert camelCase to readable format return key .replace(/([A-Z])/g, ' $1') .replace(/^./, str => str.toUpperCase()) .trim(); } private formatValue(value: any, fieldKey?: string): string | TemplateResult { if (value === null || value === undefined) { return html`Not set`; } if (typeof value === 'number') { // Format bytes if (fieldKey?.toLowerCase().includes('size') || fieldKey?.toLowerCase().includes('bytes')) { return html`${this.formatBytes(value)}`; } // Format time values if (fieldKey?.toLowerCase().includes('ttl') || fieldKey?.toLowerCase().includes('timeout')) { return html`${value} seconds`; } // Format port numbers if (fieldKey?.toLowerCase().includes('port')) { return html`${value}`; } // Format counts with separators return html`${value.toLocaleString()}`; } return String(value); } private formatBytes(bytes: number): string { if (bytes === 0) return '0 B'; const k = 1024; const sizes = ['B', 'KB', 'MB', 'GB', 'TB']; const i = Math.floor(Math.log(bytes) / Math.log(k)); return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i]; } }