Files
dcrouter/ts_web/elements/ops-view-config.ts
Juergen Kunz f1fb4c8495 feat: Add operations view components for logs, overview, security, and stats
- Implemented `ops-view-logs` for displaying and filtering logs with streaming capabilities.
- Created `ops-view-overview` to show server, email, DNS statistics, and charts.
- Developed `ops-view-security` for monitoring security metrics, blocked IPs, and authentication attempts.
- Added `ops-view-stats` to present comprehensive statistics on server, email, DNS, and security metrics.
- Introduced shared styles and components including `ops-sectionheading` for consistent UI.
2025-06-08 12:03:17 +00:00

268 lines
6.9 KiB
TypeScript

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,
} from '@design.estate/dees-element';
@customElement('ops-view-config')
export class OpsViewConfig extends DeesElement {
@state()
private configState: appstate.IConfigState = {
config: null,
isLoading: false,
error: null,
};
@state()
private editingSection: string | null = null;
@state()
private editedConfig: any = 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: white;
border: 1px solid #e9ecef;
border-radius: 8px;
margin-bottom: 24px;
overflow: hidden;
}
.sectionHeader {
background: #f8f9fa;
padding: 16px 24px;
border-bottom: 1px solid #e9ecef;
display: flex;
justify-content: space-between;
align-items: center;
}
.sectionTitle {
font-size: 18px;
font-weight: 600;
color: #333;
}
.sectionContent {
padding: 24px;
}
.configField {
margin-bottom: 20px;
}
.fieldLabel {
font-size: 14px;
font-weight: 600;
color: #666;
margin-bottom: 8px;
display: block;
}
.fieldValue {
font-family: 'Consolas', 'Monaco', monospace;
font-size: 14px;
color: #333;
background: #f8f9fa;
padding: 8px 12px;
border-radius: 4px;
border: 1px solid #e9ecef;
}
.configEditor {
width: 100%;
min-height: 200px;
font-family: 'Consolas', 'Monaco', monospace;
font-size: 14px;
padding: 12px;
border: 1px solid #e9ecef;
border-radius: 4px;
background: #f8f9fa;
resize: vertical;
}
.buttonGroup {
display: flex;
gap: 8px;
margin-top: 16px;
}
.warning {
background: #fff3cd;
border: 1px solid #ffeaa7;
border-radius: 4px;
padding: 12px;
margin-bottom: 16px;
color: #856404;
display: flex;
align-items: center;
gap: 8px;
}
.errorMessage {
background: #fee;
border: 1px solid #fcc;
border-radius: 4px;
padding: 16px;
color: #c00;
margin: 16px 0;
}
.loadingMessage {
text-align: center;
padding: 40px;
color: #666;
}
`,
];
public render() {
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="warning">
<dees-icon name="warning"></dees-icon>
<span>Changes to configuration will take effect immediately. Please be careful when editing production settings.</span>
</div>
${this.renderConfigSection('server', 'Server Configuration', this.configState.config.server)}
${this.renderConfigSection('email', 'Email Configuration', this.configState.config.email)}
${this.renderConfigSection('dns', 'DNS Configuration', this.configState.config.dns)}
${this.renderConfigSection('security', 'Security Configuration', this.configState.config.security)}
${this.renderConfigSection('storage', 'Storage Configuration', this.configState.config.storage)}
` : html`
<div class="errorMessage">No configuration loaded</div>
`}
`;
}
private renderConfigSection(key: string, title: string, config: any) {
const isEditing = this.editingSection === key;
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>
</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)}
`}
</div>
</div>
`;
}
private renderConfigFields(config: any, prefix = '') {
if (!config || typeof config !== 'object') {
return html`<div class="fieldValue">${config}</div>`;
}
return Object.entries(config).map(([key, value]) => {
const fieldName = prefix ? `${prefix}.${key}` : key;
if (typeof value === 'object' && !Array.isArray(value)) {
return html`
<div class="configField">
<label class="fieldLabel">${fieldName}</label>
${this.renderConfigFields(value, fieldName)}
</div>
`;
}
return html`
<div class="configField">
<label class="fieldLabel">${fieldName}</label>
<div class="fieldValue">
${Array.isArray(value) ? value.join(', ') : value}
</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);
}
}
}