|
|
|
|
@@ -9,6 +9,7 @@ import {
|
|
|
|
|
state,
|
|
|
|
|
css,
|
|
|
|
|
cssManager,
|
|
|
|
|
type TemplateResult,
|
|
|
|
|
} from '@design.estate/dees-element';
|
|
|
|
|
|
|
|
|
|
@customElement('ops-view-config')
|
|
|
|
|
@@ -20,12 +21,6 @@ export class OpsViewConfig extends DeesElement {
|
|
|
|
|
error: null,
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
@state()
|
|
|
|
|
accessor editingSection: string | null = null;
|
|
|
|
|
|
|
|
|
|
@state()
|
|
|
|
|
accessor editedConfig: any = null;
|
|
|
|
|
|
|
|
|
|
constructor() {
|
|
|
|
|
super();
|
|
|
|
|
const subscription = appstate.configStatePart
|
|
|
|
|
@@ -61,6 +56,14 @@ export class OpsViewConfig extends DeesElement {
|
|
|
|
|
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 {
|
|
|
|
|
@@ -71,12 +74,18 @@ export class OpsViewConfig extends DeesElement {
|
|
|
|
|
margin-bottom: 20px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.configField:last-child {
|
|
|
|
|
margin-bottom: 0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.fieldLabel {
|
|
|
|
|
font-size: 14px;
|
|
|
|
|
font-size: 13px;
|
|
|
|
|
font-weight: 600;
|
|
|
|
|
color: ${cssManager.bdTheme('#666', '#999')};
|
|
|
|
|
margin-bottom: 8px;
|
|
|
|
|
display: block;
|
|
|
|
|
text-transform: uppercase;
|
|
|
|
|
letter-spacing: 0.5px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.fieldValue {
|
|
|
|
|
@@ -84,41 +93,77 @@ export class OpsViewConfig extends DeesElement {
|
|
|
|
|
font-size: 14px;
|
|
|
|
|
color: ${cssManager.bdTheme('#333', '#ccc')};
|
|
|
|
|
background: ${cssManager.bdTheme('#f8f9fa', '#1a1a1a')};
|
|
|
|
|
padding: 8px 12px;
|
|
|
|
|
border-radius: 4px;
|
|
|
|
|
padding: 10px 14px;
|
|
|
|
|
border-radius: 6px;
|
|
|
|
|
border: 1px solid ${cssManager.bdTheme('#e9ecef', '#333')};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.configEditor {
|
|
|
|
|
width: 100%;
|
|
|
|
|
min-height: 200px;
|
|
|
|
|
font-family: 'Consolas', 'Monaco', monospace;
|
|
|
|
|
font-size: 14px;
|
|
|
|
|
padding: 12px;
|
|
|
|
|
border: 1px solid ${cssManager.bdTheme('#e9ecef', '#333')};
|
|
|
|
|
border-radius: 4px;
|
|
|
|
|
background: ${cssManager.bdTheme('#f8f9fa', '#1a1a1a')};
|
|
|
|
|
resize: vertical;
|
|
|
|
|
.fieldValue.empty {
|
|
|
|
|
color: ${cssManager.bdTheme('#999', '#666')};
|
|
|
|
|
font-style: italic;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.buttonGroup {
|
|
|
|
|
display: flex;
|
|
|
|
|
gap: 8px;
|
|
|
|
|
margin-top: 16px;
|
|
|
|
|
.nestedFields {
|
|
|
|
|
margin-left: 16px;
|
|
|
|
|
padding-left: 16px;
|
|
|
|
|
border-left: 2px solid ${cssManager.bdTheme('#e9ecef', '#333')};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.warning {
|
|
|
|
|
background: ${cssManager.bdTheme('#fff3cd', '#4a4a1a')};
|
|
|
|
|
border: 1px solid ${cssManager.bdTheme('#ffeaa7', '#666633')};
|
|
|
|
|
border-radius: 4px;
|
|
|
|
|
padding: 12px;
|
|
|
|
|
margin-bottom: 16px;
|
|
|
|
|
color: ${cssManager.bdTheme('#856404', '#ffcc66')};
|
|
|
|
|
display: flex;
|
|
|
|
|
/* 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')};
|
|
|
|
|
@@ -133,13 +178,30 @@ export class OpsViewConfig extends DeesElement {
|
|
|
|
|
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`
|
|
|
|
|
<ops-sectionheading>Configuration</ops-sectionheading>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
${this.configState.isLoading ? html`
|
|
|
|
|
<div class="loadingMessage">
|
|
|
|
|
<dees-spinner></dees-spinner>
|
|
|
|
|
@@ -150,118 +212,175 @@ export class OpsViewConfig extends DeesElement {
|
|
|
|
|
Error loading configuration: ${this.configState.error}
|
|
|
|
|
</div>
|
|
|
|
|
` : this.configState.config ? html`
|
|
|
|
|
<div class="warning">
|
|
|
|
|
<dees-icon name="warning"></dees-icon>
|
|
|
|
|
<span>Changes to configuration will take effect immediately. Please be careful when editing production settings.</span>
|
|
|
|
|
<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 Configuration', this.configState.config?.email)}
|
|
|
|
|
${this.renderConfigSection('dns', 'DNS Configuration', this.configState.config?.dns)}
|
|
|
|
|
${this.renderConfigSection('proxy', 'Proxy Configuration', this.configState.config?.proxy)}
|
|
|
|
|
${this.renderConfigSection('security', 'Security Configuration', this.configState.config?.security)}
|
|
|
|
|
${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, config: any) {
|
|
|
|
|
const isEditing = this.editingSection === key;
|
|
|
|
|
|
|
|
|
|
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">${title}</h3>
|
|
|
|
|
<div>
|
|
|
|
|
${isEditing ? html`
|
|
|
|
|
<dees-button
|
|
|
|
|
@click=${() => this.saveConfig(key)}
|
|
|
|
|
type="highlighted"
|
|
|
|
|
>
|
|
|
|
|
Save
|
|
|
|
|
</dees-button>
|
|
|
|
|
<dees-button
|
|
|
|
|
@click=${() => this.cancelEdit()}
|
|
|
|
|
>
|
|
|
|
|
Cancel
|
|
|
|
|
</dees-button>
|
|
|
|
|
` : html`
|
|
|
|
|
<dees-button
|
|
|
|
|
@click=${() => this.startEdit(key, config)}
|
|
|
|
|
>
|
|
|
|
|
Edit
|
|
|
|
|
</dees-button>
|
|
|
|
|
`}
|
|
|
|
|
</div>
|
|
|
|
|
<h3 class="sectionTitle">
|
|
|
|
|
<dees-icon icon="${icon}"></dees-icon>
|
|
|
|
|
${title}
|
|
|
|
|
</h3>
|
|
|
|
|
${this.renderStatusBadge(isEnabled)}
|
|
|
|
|
</div>
|
|
|
|
|
<div class="sectionContent">
|
|
|
|
|
${isEditing ? html`
|
|
|
|
|
<textarea
|
|
|
|
|
class="configEditor"
|
|
|
|
|
@input=${(e) => this.editedConfig = e.target.value}
|
|
|
|
|
.value=${JSON.stringify(config, null, 2)}
|
|
|
|
|
></textarea>
|
|
|
|
|
` : html`
|
|
|
|
|
${this.renderConfigFields(config)}
|
|
|
|
|
${config ? this.renderConfigFields(config) : html`
|
|
|
|
|
<div class="fieldValue empty">Not configured</div>
|
|
|
|
|
`}
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
`;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private renderConfigFields(config: any, prefix = '') {
|
|
|
|
|
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">${config}</div>`;
|
|
|
|
|
return html`<div class="fieldValue">${this.formatValue(config)}</div>`;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return Object.entries(config).map(([key, value]) => {
|
|
|
|
|
const fieldName = prefix ? `${prefix}.${key}` : key;
|
|
|
|
|
|
|
|
|
|
if (typeof value === 'object' && !Array.isArray(value)) {
|
|
|
|
|
const displayName = this.formatFieldName(key);
|
|
|
|
|
|
|
|
|
|
// Handle boolean values with badges
|
|
|
|
|
if (typeof value === 'boolean') {
|
|
|
|
|
return html`
|
|
|
|
|
<div class="configField">
|
|
|
|
|
<label class="fieldLabel">${fieldName}</label>
|
|
|
|
|
${this.renderConfigFields(value, fieldName)}
|
|
|
|
|
<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)}
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
`;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Handle primitive values
|
|
|
|
|
return html`
|
|
|
|
|
<div class="configField">
|
|
|
|
|
<label class="fieldLabel">${fieldName}</label>
|
|
|
|
|
<div class="fieldValue">
|
|
|
|
|
${Array.isArray(value) ? value.join(', ') : value}
|
|
|
|
|
</div>
|
|
|
|
|
<label class="fieldLabel">${displayName}</label>
|
|
|
|
|
<div class="fieldValue">${this.formatValue(value, key)}</div>
|
|
|
|
|
</div>
|
|
|
|
|
`;
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private startEdit(section: string, config: any) {
|
|
|
|
|
this.editingSection = section;
|
|
|
|
|
this.editedConfig = JSON.stringify(config, null, 2);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private cancelEdit() {
|
|
|
|
|
this.editingSection = null;
|
|
|
|
|
this.editedConfig = null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private async saveConfig(section: string) {
|
|
|
|
|
try {
|
|
|
|
|
const parsedConfig = JSON.parse(this.editedConfig);
|
|
|
|
|
|
|
|
|
|
await appstate.configStatePart.dispatchAction(appstate.updateConfigurationAction, {
|
|
|
|
|
section,
|
|
|
|
|
config: parsedConfig,
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
this.editingSection = null;
|
|
|
|
|
this.editedConfig = null;
|
|
|
|
|
|
|
|
|
|
// Configuration updated successfully
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.error(`Error updating configuration:`, error);
|
|
|
|
|
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>
|
|
|
|
|
`;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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'],
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
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`<span class="empty">Not set</span>`;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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>`;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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];
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|