feat: Implement dees-statsgrid in DCRouter UI for enhanced stats visualization

- Added new readme.statsgrid.md outlining the implementation plan for dees-statsgrid component.
- Replaced custom stats cards in ops-view-overview.ts and ops-view-network.ts with dees-statsgrid for better visualization.
- Introduced consistent color scheme for success, warning, error, and info states.
- Enhanced interactive features including click actions, context menus, and real-time updates.
- Developed ops-view-emails.ts for email management with features like composing, searching, and viewing emails.
- Integrated mock data generation for emails and network requests to facilitate testing.
- Added responsive design elements and improved UI consistency across components.
This commit is contained in:
Juergen Kunz
2025-06-12 08:04:30 +00:00
parent 0eb4963247
commit facae93e4b
9 changed files with 1543 additions and 499 deletions

View File

@ -0,0 +1,697 @@
import { DeesElement, property, html, customElement, type TemplateResult, css, state, cssManager } from '@design.estate/dees-element';
import * as appstate from '../appstate.js';
declare global {
interface HTMLElementTagNameMap {
'ops-view-emails': OpsViewEmails;
}
}
interface IEmail {
id: string;
from: string;
to: string[];
cc?: string[];
bcc?: string[];
subject: string;
body: string;
html?: string;
attachments?: Array<{
filename: string;
size: number;
contentType: string;
}>;
date: number;
read: boolean;
folder: 'inbox' | 'sent' | 'draft' | 'trash';
flags?: string[];
messageId?: string;
inReplyTo?: string;
}
@customElement('ops-view-emails')
export class OpsViewEmails extends DeesElement {
@state()
private selectedFolder: 'inbox' | 'sent' | 'draft' | 'trash' = 'inbox';
@state()
private emails: IEmail[] = [];
@state()
private selectedEmail: IEmail | null = null;
@state()
private showCompose = false;
@state()
private isLoading = false;
@state()
private searchTerm = '';
constructor() {
super();
this.loadEmails();
}
public static styles = [
cssManager.defaultStyles,
css`
:host {
display: block;
height: 100%;
padding: 24px;
}
.emailContainer {
display: grid;
grid-template-columns: 250px 1fr;
gap: 24px;
height: calc(100vh - 200px);
}
.sidebar {
background: white;
border: 1px solid #e9ecef;
border-radius: 8px;
padding: 16px;
display: flex;
flex-direction: column;
gap: 16px;
}
.folderList {
display: flex;
flex-direction: column;
gap: 8px;
}
.folderItem {
display: flex;
align-items: center;
gap: 12px;
padding: 12px 16px;
border-radius: 6px;
cursor: pointer;
transition: all 0.2s;
position: relative;
}
.folderItem:hover {
background: #f5f5f5;
}
.folderItem.selected {
background: #e3f2fd;
color: #1976d2;
}
.folderIcon {
font-size: 18px;
}
.folderLabel {
flex: 1;
font-weight: 500;
}
.folderCount {
background: #e0e0e0;
color: #666;
padding: 2px 8px;
border-radius: 12px;
font-size: 12px;
font-weight: 600;
}
.folderItem.selected .folderCount {
background: #1976d2;
color: white;
}
.mainContent {
background: white;
border: 1px solid #e9ecef;
border-radius: 8px;
overflow: hidden;
display: flex;
flex-direction: column;
}
.emailToolbar {
display: flex;
align-items: center;
gap: 16px;
padding: 16px;
border-bottom: 1px solid #e9ecef;
background: #fafafa;
}
.searchBox {
flex: 1;
max-width: 400px;
}
.emailList {
flex: 1;
overflow-y: auto;
}
.emailPreview {
background: white;
border: 1px solid #e9ecef;
border-radius: 8px;
overflow: hidden;
}
.emailHeader {
padding: 20px;
border-bottom: 1px solid #e9ecef;
background: #fafafa;
}
.emailSubject {
font-size: 20px;
font-weight: 600;
margin-bottom: 12px;
}
.emailMeta {
display: flex;
gap: 16px;
font-size: 14px;
color: #666;
}
.emailBody {
padding: 20px;
max-height: 500px;
overflow-y: auto;
}
.emailActions {
display: flex;
gap: 8px;
padding: 16px;
border-top: 1px solid #e9ecef;
background: #fafafa;
}
.composeModal {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.5);
display: flex;
align-items: center;
justify-content: center;
z-index: 1000;
}
.composeContent {
background: white;
border-radius: 8px;
width: 90%;
max-width: 800px;
max-height: 90vh;
overflow: hidden;
display: flex;
flex-direction: column;
}
.composeHeader {
padding: 20px;
border-bottom: 1px solid #e9ecef;
display: flex;
align-items: center;
justify-content: space-between;
}
.composeTitle {
font-size: 20px;
font-weight: 600;
}
.composeForm {
flex: 1;
overflow-y: auto;
padding: 20px;
}
.composeActions {
padding: 20px;
border-top: 1px solid #e9ecef;
display: flex;
gap: 12px;
justify-content: flex-end;
}
.emptyState {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: 100%;
color: #999;
}
.emptyIcon {
font-size: 64px;
margin-bottom: 16px;
opacity: 0.3;
}
.emptyText {
font-size: 18px;
}
`,
];
public render() {
return html`
<ops-sectionheading>Emails</ops-sectionheading>
<div class="emailContainer">
<!-- Sidebar -->
<div class="sidebar">
<dees-button @click=${() => this.showCompose = true} type="highlighted">
<dees-icon name="penToSquare" slot="iconSlot"></dees-icon>
Compose
</dees-button>
<div class="folderList">
${this.renderFolderItem('inbox', 'inbox', 'Inbox', this.getEmailCount('inbox'))}
${this.renderFolderItem('sent', 'paperPlane', 'Sent', this.getEmailCount('sent'))}
${this.renderFolderItem('draft', 'file', 'Drafts', this.getEmailCount('draft'))}
${this.renderFolderItem('trash', 'trash', 'Trash', this.getEmailCount('trash'))}
</div>
</div>
<!-- Main Content -->
<div class="mainContent">
<!-- Toolbar -->
<div class="emailToolbar">
<dees-input-text
class="searchBox"
placeholder="Search emails..."
.value=${this.searchTerm}
@input=${(e: Event) => this.searchTerm = (e.target as any).value}
>
<dees-icon name="magnifyingGlass" slot="iconSlot"></dees-icon>
</dees-input-text>
<dees-button @click=${() => this.refreshEmails()}>
${this.isLoading ? html`<dees-spinner size="small"></dees-spinner>` : html`<dees-icon name="arrowsRotate"></dees-icon>`}
</dees-button>
<dees-button @click=${() => this.markAllAsRead()}>
<dees-icon name="envelopeOpen"></dees-icon>
Mark all read
</dees-button>
</div>
<!-- Email List or Preview -->
${this.selectedEmail ? this.renderEmailPreview() : this.renderEmailList()}
</div>
</div>
<!-- Compose Modal -->
${this.showCompose ? this.renderComposeModal() : ''}
`;
}
private renderFolderItem(folder: string, icon: string, label: string, count: number) {
return html`
<div
class="folderItem ${this.selectedFolder === folder ? 'selected' : ''}"
@click=${() => this.selectFolder(folder as any)}
>
<dees-icon class="folderIcon" name="${icon}"></dees-icon>
<span class="folderLabel">${label}</span>
${count > 0 ? html`<span class="folderCount">${count}</span>` : ''}
</div>
`;
}
private renderEmailList() {
const filteredEmails = this.getFilteredEmails();
if (filteredEmails.length === 0) {
return html`
<div class="emptyState">
<dees-icon class="emptyIcon" name="envelope"></dees-icon>
<div class="emptyText">No emails in ${this.selectedFolder}</div>
</div>
`;
}
return html`
<div class="emailList">
<dees-table
.data=${filteredEmails}
.displayFunction=${(email: IEmail) => ({
'Status': html`<dees-icon name="${email.read ? 'envelopeOpen' : 'envelope'}" style="color: ${email.read ? '#999' : '#1976d2'}"></dees-icon>`,
From: email.from,
Subject: html`<strong style="${!email.read ? 'font-weight: 600' : ''}">${email.subject}</strong>`,
Date: this.formatDate(email.date),
'Attach': html`
${email.attachments?.length ? html`<dees-icon name="paperclip" style="color: #666"></dees-icon>` : ''}
`,
})}
.dataActions=${[
{
name: 'Read',
iconName: 'eye',
type: ['doubleClick', 'inRow'],
actionFunc: async (actionData) => {
this.selectedEmail = actionData.item;
if (!actionData.item.read) {
this.markAsRead(actionData.item.id);
}
}
},
{
name: 'Reply',
iconName: 'reply',
type: ['contextmenu'],
actionFunc: async (actionData) => {
this.replyToEmail(actionData.item);
}
},
{
name: 'Forward',
iconName: 'share',
type: ['contextmenu'],
actionFunc: async (actionData) => {
this.forwardEmail(actionData.item);
}
},
{
name: 'Delete',
iconName: 'trash',
type: ['contextmenu'],
actionFunc: async (actionData) => {
this.deleteEmail(actionData.item.id);
}
}
]}
.selectionMode=${'single'}
></dees-table>
</div>
`;
}
private renderEmailPreview() {
if (!this.selectedEmail) return '';
return html`
<div class="emailPreview">
<div class="emailHeader">
<div class="emailSubject">${this.selectedEmail.subject}</div>
<div class="emailMeta">
<span><strong>From:</strong> ${this.selectedEmail.from}</span>
<span><strong>To:</strong> ${this.selectedEmail.to.join(', ')}</span>
<span><strong>Date:</strong> ${this.formatDate(this.selectedEmail.date)}</span>
</div>
</div>
<div class="emailBody">
${this.selectedEmail.html ?
html`<div .innerHTML=${this.selectedEmail.html}></div>` :
html`<pre style="white-space: pre-wrap; font-family: inherit;">${this.selectedEmail.body}</pre>`
}
</div>
<div class="emailActions">
<dees-button @click=${() => this.selectedEmail = null}>
<dees-icon name="arrowLeft"></dees-icon>
Back
</dees-button>
<dees-button @click=${() => this.replyToEmail(this.selectedEmail!)}>
<dees-icon name="reply"></dees-icon>
Reply
</dees-button>
<dees-button @click=${() => this.replyAllToEmail(this.selectedEmail!)}>
<dees-icon name="replyAll"></dees-icon>
Reply All
</dees-button>
<dees-button @click=${() => this.forwardEmail(this.selectedEmail!)}>
<dees-icon name="share"></dees-icon>
Forward
</dees-button>
<dees-button @click=${() => this.deleteEmail(this.selectedEmail!.id)} type="danger">
<dees-icon name="trash"></dees-icon>
Delete
</dees-button>
</div>
</div>
`;
}
private renderComposeModal() {
return html`
<div class="composeModal" @click=${(e: Event) => {
if (e.target === e.currentTarget) this.showCompose = false;
}}>
<div class="composeContent">
<div class="composeHeader">
<div class="composeTitle">New Email</div>
<dees-button @click=${() => this.showCompose = false} type="ghost">
<dees-icon name="xmark"></dees-icon>
</dees-button>
</div>
<div class="composeForm">
<dees-form @formData=${(e: CustomEvent) => this.sendEmail(e.detail)}>
<dees-input-tags
key="to"
label="To"
placeholder="Enter recipient email addresses..."
required
></dees-input-tags>
<dees-input-tags
key="cc"
label="CC"
placeholder="Enter CC recipients..."
></dees-input-tags>
<dees-input-tags
key="bcc"
label="BCC"
placeholder="Enter BCC recipients..."
></dees-input-tags>
<dees-input-text
key="subject"
label="Subject"
placeholder="Enter email subject..."
required
></dees-input-text>
<dees-editor
key="body"
label="Message"
.mode=${'markdown'}
.height=${300}
required
></dees-editor>
<dees-input-fileupload
key="attachments"
label="Attachments"
multiple
></dees-input-fileupload>
<div class="composeActions">
<dees-button @click=${() => this.showCompose = false} type="secondary">
Cancel
</dees-button>
<dees-form-submit>
<dees-icon name="paperPlane"></dees-icon>
Send Email
</dees-form-submit>
</div>
</dees-form>
</div>
</div>
</div>
`;
}
private getFilteredEmails(): IEmail[] {
let emails = this.emails.filter(e => e.folder === this.selectedFolder);
if (this.searchTerm) {
const search = this.searchTerm.toLowerCase();
emails = emails.filter(e =>
e.subject.toLowerCase().includes(search) ||
e.from.toLowerCase().includes(search) ||
e.body.toLowerCase().includes(search)
);
}
return emails.sort((a, b) => b.date - a.date);
}
private getEmailCount(folder: string): number {
return this.emails.filter(e => e.folder === folder && !e.read).length;
}
private selectFolder(folder: 'inbox' | 'sent' | 'draft' | 'trash') {
this.selectedFolder = folder;
this.selectedEmail = null;
}
private formatDate(timestamp: number): string {
const date = new Date(timestamp);
const now = new Date();
const diff = now.getTime() - date.getTime();
const hours = diff / (1000 * 60 * 60);
if (hours < 24) {
return date.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
} else if (hours < 168) { // 7 days
return date.toLocaleDateString([], { weekday: 'short' });
} else {
return date.toLocaleDateString([], { month: 'short', day: 'numeric' });
}
}
private async loadEmails() {
// TODO: Load real emails from server
// For now, generate mock data
this.generateMockEmails();
}
private async refreshEmails() {
this.isLoading = true;
await this.loadEmails();
this.isLoading = false;
}
private async sendEmail(formData: any) {
try {
// TODO: Implement actual email sending
console.log('Sending email:', formData);
// Add to sent folder
const newEmail: IEmail = {
id: `email-${Date.now()}`,
from: 'me@dcrouter.local',
to: formData.to,
cc: formData.cc || [],
bcc: formData.bcc || [],
subject: formData.subject,
body: formData.body,
date: Date.now(),
read: true,
folder: 'sent',
};
this.emails = [...this.emails, newEmail];
this.showCompose = false;
// TODO: Implement toast notification when DeesToast.show is available
console.log('Email sent successfully');
} catch (error: any) {
// TODO: Implement toast notification when DeesToast.show is available
console.error('Failed to send email', error);
}
}
private async markAsRead(emailId: string) {
const email = this.emails.find(e => e.id === emailId);
if (email) {
email.read = true;
this.emails = [...this.emails];
}
}
private async markAllAsRead() {
this.emails = this.emails.map(e =>
e.folder === this.selectedFolder ? { ...e, read: true } : e
);
}
private async deleteEmail(emailId: string) {
const email = this.emails.find(e => e.id === emailId);
if (email) {
if (email.folder === 'trash') {
// Permanently delete
this.emails = this.emails.filter(e => e.id !== emailId);
} else {
// Move to trash
email.folder = 'trash';
this.emails = [...this.emails];
}
if (this.selectedEmail?.id === emailId) {
this.selectedEmail = null;
}
}
}
private async replyToEmail(email: IEmail) {
// TODO: Open compose with reply context
this.showCompose = true;
}
private async replyAllToEmail(email: IEmail) {
// TODO: Open compose with reply all context
this.showCompose = true;
}
private async forwardEmail(email: IEmail) {
// TODO: Open compose with forward context
this.showCompose = true;
}
private generateMockEmails() {
const subjects = [
'Server Alert: High CPU Usage',
'Daily Report - Network Activity',
'Security Update Required',
'New User Registration',
'Backup Completed Successfully',
'DNS Query Spike Detected',
'SSL Certificate Renewal Notice',
'Monthly Usage Summary',
];
const senders = [
'monitoring@dcrouter.local',
'alerts@system.local',
'admin@company.com',
'noreply@service.com',
'support@vendor.com',
];
const bodies = [
'This is an automated alert regarding your server status.',
'Please review the attached report for detailed information.',
'Action required: Update your security settings.',
'Your daily summary is ready for review.',
'All systems are operating normally.',
];
this.emails = Array.from({ length: 50 }, (_, i) => ({
id: `email-${i}`,
from: senders[Math.floor(Math.random() * senders.length)],
to: ['admin@dcrouter.local'],
subject: subjects[Math.floor(Math.random() * subjects.length)],
body: bodies[Math.floor(Math.random() * bodies.length)],
date: Date.now() - (i * 3600000), // 1 hour apart
read: Math.random() > 0.3,
folder: i < 40 ? 'inbox' : i < 45 ? 'sent' : 'trash',
attachments: Math.random() > 0.8 ? [{
filename: 'report.pdf',
size: 1024 * 1024,
contentType: 'application/pdf',
}] : undefined,
}));
}
}