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.
This commit is contained in:
268
ts_web/elements/ops-view-config.ts
Normal file
268
ts_web/elements/ops-view-config.ts
Normal file
@ -0,0 +1,268 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user