BREAKING CHANGE(opsserver): Return structured configuration (IConfigData) from opsserver and update UI to render detailed config sections
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
import * as plugins from '../plugins.js';
|
||||
import * as shared from './shared/index.js';
|
||||
import * as appstate from '../appstate.js';
|
||||
import { appRouter } from '../router.js';
|
||||
|
||||
import {
|
||||
DeesElement,
|
||||
@@ -12,6 +13,8 @@ import {
|
||||
type TemplateResult,
|
||||
} from '@design.estate/dees-element';
|
||||
|
||||
import type { IConfigField, IConfigSectionAction } from '@serve.zone/catalog';
|
||||
|
||||
@customElement('ops-view-config')
|
||||
export class OpsViewConfig extends DeesElement {
|
||||
@state()
|
||||
@@ -35,165 +38,19 @@ export class OpsViewConfig extends DeesElement {
|
||||
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')};
|
||||
color: ${cssManager.bdTheme('#71717a', '#a1a1aa')};
|
||||
}
|
||||
|
||||
.infoNote {
|
||||
background: ${cssManager.bdTheme('#e7f3ff', '#1a2a3d')};
|
||||
border: 1px solid ${cssManager.bdTheme('#b3d7ff', '#2a4a6d')};
|
||||
.errorMessage {
|
||||
background: ${cssManager.bdTheme('#fee2e2', 'rgba(239,68,68,0.1)')};
|
||||
border: 1px solid ${cssManager.bdTheme('#fecaca', 'rgba(239,68,68,0.3)')};
|
||||
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;
|
||||
color: ${cssManager.bdTheme('#dc2626', '#ef4444')};
|
||||
margin: 16px 0;
|
||||
}
|
||||
`,
|
||||
];
|
||||
@@ -202,185 +59,266 @@ export class OpsViewConfig extends DeesElement {
|
||||
return html`
|
||||
<ops-sectionheading>Configuration</ops-sectionheading>
|
||||
|
||||
${this.configState.isLoading ? html`
|
||||
<div class="loadingMessage">
|
||||
<dees-spinner></dees-spinner>
|
||||
<p>Loading configuration...</p>
|
||||
</div>
|
||||
` : this.configState.error ? html`
|
||||
<div class="errorMessage">
|
||||
Error loading configuration: ${this.configState.error}
|
||||
</div>
|
||||
` : this.configState.config ? html`
|
||||
<div class="infoNote">
|
||||
<dees-icon icon="lucide:info"></dees-icon>
|
||||
<span>This view displays the current running configuration. DcRouter is configured through code or remote management.</span>
|
||||
</div>
|
||||
|
||||
${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`
|
||||
<div class="errorMessage">No configuration loaded</div>
|
||||
`}
|
||||
`;
|
||||
}
|
||||
|
||||
private renderConfigSection(key: string, title: string, icon: string, config: any) {
|
||||
const isEnabled = config?.enabled ?? false;
|
||||
|
||||
return html`
|
||||
<div class="configSection">
|
||||
<div class="sectionHeader">
|
||||
<h3 class="sectionTitle">
|
||||
<dees-icon icon="${icon}"></dees-icon>
|
||||
${title}
|
||||
</h3>
|
||||
${this.renderStatusBadge(isEnabled)}
|
||||
</div>
|
||||
<div class="sectionContent">
|
||||
${config ? this.renderConfigFields(config) : html`
|
||||
<div class="fieldValue empty">Not configured</div>
|
||||
`}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
private renderStatusBadge(enabled: boolean): TemplateResult {
|
||||
return enabled
|
||||
? html`<span class="statusBadge enabled"><dees-icon icon="lucide:check"></dees-icon>Enabled</span>`
|
||||
: html`<span class="statusBadge disabled"><dees-icon icon="lucide:x"></dees-icon>Disabled</span>`;
|
||||
}
|
||||
|
||||
private renderConfigFields(config: any, prefix = ''): TemplateResult | TemplateResult[] {
|
||||
if (!config || typeof config !== 'object') {
|
||||
return html`<div class="fieldValue">${this.formatValue(config)}</div>`;
|
||||
}
|
||||
|
||||
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`
|
||||
<div class="configField">
|
||||
<label class="fieldLabel">${displayName}</label>
|
||||
${this.renderStatusBadge(value)}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
// Handle arrays
|
||||
if (Array.isArray(value)) {
|
||||
return html`
|
||||
<div class="configField">
|
||||
<label class="fieldLabel">${displayName}</label>
|
||||
${this.renderArrayValue(value, key)}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
// Handle nested objects
|
||||
if (typeof value === 'object' && value !== null) {
|
||||
return html`
|
||||
<div class="configField">
|
||||
<label class="fieldLabel">${displayName}</label>
|
||||
<div class="nestedFields">
|
||||
${this.renderConfigFields(value, fieldName)}
|
||||
${this.configState.isLoading
|
||||
? html`
|
||||
<div class="loadingMessage">
|
||||
<dees-spinner></dees-spinner>
|
||||
<p>Loading configuration...</p>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
// Handle primitive values
|
||||
return html`
|
||||
<div class="configField">
|
||||
<label class="fieldLabel">${displayName}</label>
|
||||
<div class="fieldValue">${this.formatValue(value, key)}</div>
|
||||
</div>
|
||||
`;
|
||||
});
|
||||
}
|
||||
|
||||
private renderArrayValue(arr: any[], fieldKey: string): TemplateResult {
|
||||
if (arr.length === 0) {
|
||||
return html`<div class="fieldValue empty">None configured</div>`;
|
||||
}
|
||||
|
||||
// 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`
|
||||
<div class="arrayCount">${arr.length} ${itemLabel}</div>
|
||||
<div class="arrayItems">
|
||||
${arr.map(item => html`<span class="arrayItem">${item}</span>`)}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
// For complex arrays, show as JSON
|
||||
return html`
|
||||
<div class="fieldValue">
|
||||
${arr.length} items configured
|
||||
</div>
|
||||
`
|
||||
: this.configState.error
|
||||
? html`
|
||||
<div class="errorMessage">
|
||||
Error loading configuration: ${this.configState.error}
|
||||
</div>
|
||||
`
|
||||
: this.configState.config
|
||||
? this.renderConfig()
|
||||
: html`<div class="errorMessage">No configuration loaded</div>`}
|
||||
`;
|
||||
}
|
||||
|
||||
private getArrayItemLabel(fieldKey: string, count: number): string {
|
||||
const labels: Record<string, [string, string]> = {
|
||||
ports: ['port', 'ports'],
|
||||
domains: ['domain', 'domains'],
|
||||
nameservers: ['nameserver', 'nameservers'],
|
||||
blockList: ['IP', 'IPs'],
|
||||
};
|
||||
private renderConfig(): TemplateResult {
|
||||
const cfg = this.configState.config!;
|
||||
|
||||
const label = labels[fieldKey] || ['item', 'items'];
|
||||
return count === 1 ? label[0] : label[1];
|
||||
return html`
|
||||
<sz-config-overview
|
||||
infoText="This view displays the current running configuration. DcRouter is configured through code or remote management."
|
||||
@navigate=${(e: CustomEvent) => {
|
||||
if (e.detail?.view) {
|
||||
appRouter.navigateToView(e.detail.view);
|
||||
}
|
||||
}}
|
||||
>
|
||||
${this.renderSystemSection(cfg.system)}
|
||||
${this.renderSmartProxySection(cfg.smartProxy)}
|
||||
${this.renderEmailSection(cfg.email)}
|
||||
${this.renderDnsSection(cfg.dns)}
|
||||
${this.renderTlsSection(cfg.tls)}
|
||||
${this.renderCacheSection(cfg.cache)}
|
||||
${this.renderRadiusSection(cfg.radius)}
|
||||
${this.renderRemoteIngressSection(cfg.remoteIngress)}
|
||||
</sz-config-overview>
|
||||
`;
|
||||
}
|
||||
|
||||
private formatFieldName(key: string): string {
|
||||
// Convert camelCase to readable format
|
||||
return key
|
||||
.replace(/([A-Z])/g, ' $1')
|
||||
.replace(/^./, str => str.toUpperCase())
|
||||
.trim();
|
||||
private renderSystemSection(sys: appstate.IConfigState['config']['system']): TemplateResult {
|
||||
const fields: IConfigField[] = [
|
||||
{ key: 'Base Directory', value: sys.baseDir },
|
||||
{ key: 'Data Directory', value: sys.dataDir },
|
||||
{ key: 'Public IP', value: sys.publicIp },
|
||||
{ key: 'Proxy IPs', value: sys.proxyIps.length > 0 ? sys.proxyIps : null, type: 'pills' },
|
||||
{ key: 'Uptime', value: this.formatUptime(sys.uptime) },
|
||||
{ key: 'Storage Backend', value: sys.storageBackend, type: 'badge' },
|
||||
{ key: 'Storage Path', value: sys.storagePath },
|
||||
];
|
||||
|
||||
return html`
|
||||
<sz-config-section
|
||||
title="System"
|
||||
subtitle="Base paths and infrastructure"
|
||||
icon="lucide:server"
|
||||
status="enabled"
|
||||
.fields=${fields}
|
||||
></sz-config-section>
|
||||
`;
|
||||
}
|
||||
|
||||
private formatValue(value: any, fieldKey?: string): string | TemplateResult {
|
||||
if (value === null || value === undefined) {
|
||||
return html`<span class="empty">Not set</span>`;
|
||||
private renderSmartProxySection(proxy: appstate.IConfigState['config']['smartProxy']): TemplateResult {
|
||||
const fields: IConfigField[] = [
|
||||
{ key: 'Route Count', value: proxy.routeCount },
|
||||
];
|
||||
|
||||
if (proxy.acme) {
|
||||
fields.push(
|
||||
{ key: 'ACME Enabled', value: proxy.acme.enabled, type: 'boolean' },
|
||||
{ key: 'Account Email', value: proxy.acme.accountEmail || null },
|
||||
{ key: 'Use Production', value: proxy.acme.useProduction, type: 'boolean' },
|
||||
{ key: 'Auto Renew', value: proxy.acme.autoRenew, type: 'boolean' },
|
||||
{ key: 'Renew Threshold', value: `${proxy.acme.renewThresholdDays} days` },
|
||||
);
|
||||
}
|
||||
|
||||
if (typeof value === 'number') {
|
||||
// Format bytes
|
||||
if (fieldKey?.toLowerCase().includes('size') || fieldKey?.toLowerCase().includes('bytes')) {
|
||||
return html`<span class="numericValue">${this.formatBytes(value)}</span>`;
|
||||
}
|
||||
// Format time values
|
||||
if (fieldKey?.toLowerCase().includes('ttl') || fieldKey?.toLowerCase().includes('timeout')) {
|
||||
return html`<span class="numericValue">${value} seconds</span>`;
|
||||
}
|
||||
// Format port numbers
|
||||
if (fieldKey?.toLowerCase().includes('port')) {
|
||||
return html`<span class="numericValue">${value}</span>`;
|
||||
}
|
||||
// Format counts with separators
|
||||
return html`<span class="numericValue">${value.toLocaleString()}</span>`;
|
||||
}
|
||||
const actions: IConfigSectionAction[] = [
|
||||
{ label: 'View Routes', icon: 'lucide:arrow-right', event: 'navigate', detail: { view: 'routes' } },
|
||||
];
|
||||
|
||||
return String(value);
|
||||
return html`
|
||||
<sz-config-section
|
||||
title="SmartProxy"
|
||||
subtitle="HTTP/HTTPS and TCP/SNI reverse proxy"
|
||||
icon="lucide:network"
|
||||
.status=${proxy.enabled ? 'enabled' : 'disabled'}
|
||||
.fields=${fields}
|
||||
.actions=${actions}
|
||||
></sz-config-section>
|
||||
`;
|
||||
}
|
||||
|
||||
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];
|
||||
private renderEmailSection(email: appstate.IConfigState['config']['email']): TemplateResult {
|
||||
const fields: IConfigField[] = [
|
||||
{ key: 'Ports', value: email.ports.length > 0 ? email.ports.map(String) : null, type: 'pills' },
|
||||
{ key: 'Hostname', value: email.hostname },
|
||||
{ key: 'Domains', value: email.domains.length > 0 ? email.domains : null, type: 'pills' },
|
||||
{ key: 'Email Routes', value: email.emailRouteCount },
|
||||
{ key: 'Received Emails Path', value: email.receivedEmailsPath },
|
||||
];
|
||||
|
||||
if (email.portMapping) {
|
||||
const mappingStr = Object.entries(email.portMapping)
|
||||
.map(([ext, int]) => `${ext} → ${int}`)
|
||||
.join(', ');
|
||||
fields.splice(1, 0, { key: 'Port Mapping', value: mappingStr, type: 'code' });
|
||||
}
|
||||
|
||||
const actions: IConfigSectionAction[] = [
|
||||
{ label: 'View Emails', icon: 'lucide:arrow-right', event: 'navigate', detail: { view: 'emails' } },
|
||||
];
|
||||
|
||||
return html`
|
||||
<sz-config-section
|
||||
title="Email Server"
|
||||
subtitle="SMTP email handling with smartmta"
|
||||
icon="lucide:mail"
|
||||
.status=${email.enabled ? 'enabled' : 'disabled'}
|
||||
.fields=${fields}
|
||||
.actions=${actions}
|
||||
></sz-config-section>
|
||||
`;
|
||||
}
|
||||
|
||||
private renderDnsSection(dns: appstate.IConfigState['config']['dns']): TemplateResult {
|
||||
const fields: IConfigField[] = [
|
||||
{ key: 'Port', value: dns.port },
|
||||
{ key: 'NS Domains', value: dns.nsDomains.length > 0 ? dns.nsDomains : null, type: 'pills' },
|
||||
{ key: 'Scopes', value: dns.scopes.length > 0 ? dns.scopes : null, type: 'pills' },
|
||||
{ key: 'Record Count', value: dns.recordCount },
|
||||
{ key: 'DNS Challenge', value: dns.dnsChallenge, type: 'boolean' },
|
||||
];
|
||||
|
||||
return html`
|
||||
<sz-config-section
|
||||
title="DNS Server"
|
||||
subtitle="Authoritative DNS with smartdns"
|
||||
icon="lucide:globe"
|
||||
.status=${dns.enabled ? 'enabled' : 'disabled'}
|
||||
.fields=${fields}
|
||||
></sz-config-section>
|
||||
`;
|
||||
}
|
||||
|
||||
private renderTlsSection(tls: appstate.IConfigState['config']['tls']): TemplateResult {
|
||||
const fields: IConfigField[] = [
|
||||
{ key: 'Contact Email', value: tls.contactEmail },
|
||||
{ key: 'Domain', value: tls.domain },
|
||||
{ key: 'Source', value: tls.source, type: 'badge' },
|
||||
{ key: 'Certificate Path', value: tls.certPath },
|
||||
{ key: 'Key Path', value: tls.keyPath },
|
||||
];
|
||||
|
||||
const status = tls.source === 'none' ? 'not-configured' : 'enabled';
|
||||
const actions: IConfigSectionAction[] = [
|
||||
{ label: 'View Certificates', icon: 'lucide:arrow-right', event: 'navigate', detail: { view: 'certificates' } },
|
||||
];
|
||||
|
||||
return html`
|
||||
<sz-config-section
|
||||
title="TLS / Certificates"
|
||||
subtitle="Certificate management and ACME"
|
||||
icon="lucide:shield-check"
|
||||
.status=${status as any}
|
||||
.fields=${fields}
|
||||
.actions=${actions}
|
||||
></sz-config-section>
|
||||
`;
|
||||
}
|
||||
|
||||
private renderCacheSection(cache: appstate.IConfigState['config']['cache']): TemplateResult {
|
||||
const fields: IConfigField[] = [
|
||||
{ key: 'Storage Path', value: cache.storagePath },
|
||||
{ key: 'DB Name', value: cache.dbName },
|
||||
{ key: 'Default TTL', value: `${cache.defaultTTLDays} days` },
|
||||
{ key: 'Cleanup Interval', value: `${cache.cleanupIntervalHours} hours` },
|
||||
];
|
||||
|
||||
if (cache.ttlConfig && Object.keys(cache.ttlConfig).length > 0) {
|
||||
for (const [key, val] of Object.entries(cache.ttlConfig)) {
|
||||
fields.push({ key: `TTL: ${key}`, value: `${val} days` });
|
||||
}
|
||||
}
|
||||
|
||||
return html`
|
||||
<sz-config-section
|
||||
title="Cache Database"
|
||||
subtitle="Persistent caching with smartdata"
|
||||
icon="lucide:database"
|
||||
.status=${cache.enabled ? 'enabled' : 'disabled'}
|
||||
.fields=${fields}
|
||||
></sz-config-section>
|
||||
`;
|
||||
}
|
||||
|
||||
private renderRadiusSection(radius: appstate.IConfigState['config']['radius']): TemplateResult {
|
||||
const fields: IConfigField[] = [
|
||||
{ key: 'Auth Port', value: radius.authPort },
|
||||
{ key: 'Accounting Port', value: radius.acctPort },
|
||||
{ key: 'Bind Address', value: radius.bindAddress },
|
||||
{ key: 'Client Count', value: radius.clientCount },
|
||||
];
|
||||
|
||||
if (radius.vlanDefaultVlan !== null) {
|
||||
fields.push(
|
||||
{ key: 'Default VLAN', value: radius.vlanDefaultVlan },
|
||||
{ key: 'Allow Unknown MACs', value: radius.vlanAllowUnknownMacs, type: 'boolean' },
|
||||
{ key: 'VLAN Mappings', value: radius.vlanMappingCount },
|
||||
);
|
||||
}
|
||||
|
||||
const status = radius.enabled ? 'enabled' : 'not-configured';
|
||||
|
||||
return html`
|
||||
<sz-config-section
|
||||
title="RADIUS Server"
|
||||
subtitle="Network authentication and VLAN assignment"
|
||||
icon="lucide:wifi"
|
||||
.status=${status as any}
|
||||
.fields=${fields}
|
||||
></sz-config-section>
|
||||
`;
|
||||
}
|
||||
|
||||
private renderRemoteIngressSection(ri: appstate.IConfigState['config']['remoteIngress']): TemplateResult {
|
||||
const fields: IConfigField[] = [
|
||||
{ key: 'Tunnel Port', value: ri.tunnelPort },
|
||||
{ key: 'Hub Domain', value: ri.hubDomain },
|
||||
{ key: 'TLS Configured', value: ri.tlsConfigured, type: 'boolean' },
|
||||
];
|
||||
|
||||
const actions: IConfigSectionAction[] = [
|
||||
{ label: 'View Remote Ingress', icon: 'lucide:arrow-right', event: 'navigate', detail: { view: 'remoteingress' } },
|
||||
];
|
||||
|
||||
return html`
|
||||
<sz-config-section
|
||||
title="Remote Ingress"
|
||||
subtitle="Edge tunnel nodes"
|
||||
icon="lucide:cloud"
|
||||
.status=${ri.enabled ? 'enabled' : 'disabled'}
|
||||
.fields=${fields}
|
||||
.actions=${actions}
|
||||
></sz-config-section>
|
||||
`;
|
||||
}
|
||||
|
||||
private formatUptime(seconds: number): string {
|
||||
const days = Math.floor(seconds / 86400);
|
||||
const hours = Math.floor((seconds % 86400) / 3600);
|
||||
const mins = Math.floor((seconds % 3600) / 60);
|
||||
|
||||
const parts: string[] = [];
|
||||
if (days > 0) parts.push(`${days}d`);
|
||||
if (hours > 0) parts.push(`${hours}h`);
|
||||
parts.push(`${mins}m`);
|
||||
return parts.join(' ');
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user