BREAKING CHANGE(email-ops): migrate email operations to catalog-compatible email model and simplify UI/router
This commit is contained in:
@@ -3,6 +3,6 @@
|
||||
*/
|
||||
export const commitinfo = {
|
||||
name: '@serve.zone/dcrouter',
|
||||
version: '7.4.3',
|
||||
version: '8.0.0',
|
||||
description: 'A multifaceted routing service handling mail and SMS delivery functions.'
|
||||
}
|
||||
|
||||
@@ -67,14 +67,7 @@ export interface ICertificateState {
|
||||
}
|
||||
|
||||
export interface IEmailOpsState {
|
||||
currentView: 'queued' | 'sent' | 'failed' | 'received' | 'security';
|
||||
queuedEmails: interfaces.requests.IEmailQueueItem[];
|
||||
sentEmails: interfaces.requests.IEmailQueueItem[];
|
||||
failedEmails: interfaces.requests.IEmailQueueItem[];
|
||||
securityIncidents: interfaces.requests.ISecurityIncident[];
|
||||
bounceRecords: interfaces.requests.IBounceRecord[];
|
||||
suppressionList: string[];
|
||||
selectedEmailId: string | null;
|
||||
emails: interfaces.requests.IEmail[];
|
||||
isLoading: boolean;
|
||||
error: string | null;
|
||||
lastUpdated: number;
|
||||
@@ -165,14 +158,7 @@ export const networkStatePart = await appState.getStatePart<INetworkState>(
|
||||
export const emailOpsStatePart = await appState.getStatePart<IEmailOpsState>(
|
||||
'emailOps',
|
||||
{
|
||||
currentView: 'queued',
|
||||
queuedEmails: [],
|
||||
sentEmails: [],
|
||||
failedEmails: [],
|
||||
securityIncidents: [],
|
||||
bounceRecords: [],
|
||||
suppressionList: [],
|
||||
selectedEmailId: null,
|
||||
emails: [],
|
||||
isLoading: false,
|
||||
error: null,
|
||||
lastUpdated: 0,
|
||||
@@ -492,35 +478,22 @@ export const fetchNetworkStatsAction = networkStatePart.createAction(async (stat
|
||||
// Email Operations Actions
|
||||
// ============================================================================
|
||||
|
||||
// Set Email Ops View Action
|
||||
export const setEmailOpsViewAction = emailOpsStatePart.createAction<IEmailOpsState['currentView']>(
|
||||
async (statePartArg, view) => {
|
||||
return {
|
||||
...statePartArg.getState(),
|
||||
currentView: view,
|
||||
};
|
||||
}
|
||||
);
|
||||
|
||||
// Fetch Queued Emails Action
|
||||
export const fetchQueuedEmailsAction = emailOpsStatePart.createAction(async (statePartArg) => {
|
||||
// Fetch All Emails Action
|
||||
export const fetchAllEmailsAction = emailOpsStatePart.createAction(async (statePartArg) => {
|
||||
const context = getActionContext();
|
||||
const currentState = statePartArg.getState();
|
||||
|
||||
try {
|
||||
const request = new plugins.domtools.plugins.typedrequest.TypedRequest<
|
||||
interfaces.requests.IReq_GetQueuedEmails
|
||||
>('/typedrequest', 'getQueuedEmails');
|
||||
interfaces.requests.IReq_GetAllEmails
|
||||
>('/typedrequest', 'getAllEmails');
|
||||
|
||||
const response = await request.fire({
|
||||
identity: context.identity,
|
||||
status: 'pending',
|
||||
limit: 100,
|
||||
});
|
||||
|
||||
return {
|
||||
...currentState,
|
||||
queuedEmails: response.items,
|
||||
emails: response.emails,
|
||||
isLoading: false,
|
||||
error: null,
|
||||
lastUpdated: Date.now(),
|
||||
@@ -529,197 +502,11 @@ export const fetchQueuedEmailsAction = emailOpsStatePart.createAction(async (sta
|
||||
return {
|
||||
...currentState,
|
||||
isLoading: false,
|
||||
error: error instanceof Error ? error.message : 'Failed to fetch queued emails',
|
||||
error: error instanceof Error ? error.message : 'Failed to fetch emails',
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
// Fetch Sent Emails Action
|
||||
export const fetchSentEmailsAction = emailOpsStatePart.createAction(async (statePartArg) => {
|
||||
const context = getActionContext();
|
||||
const currentState = statePartArg.getState();
|
||||
|
||||
try {
|
||||
const request = new plugins.domtools.plugins.typedrequest.TypedRequest<
|
||||
interfaces.requests.IReq_GetSentEmails
|
||||
>('/typedrequest', 'getSentEmails');
|
||||
|
||||
const response = await request.fire({
|
||||
identity: context.identity,
|
||||
limit: 100,
|
||||
});
|
||||
|
||||
return {
|
||||
...currentState,
|
||||
sentEmails: response.items,
|
||||
isLoading: false,
|
||||
error: null,
|
||||
lastUpdated: Date.now(),
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
...currentState,
|
||||
isLoading: false,
|
||||
error: error instanceof Error ? error.message : 'Failed to fetch sent emails',
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
// Fetch Failed Emails Action
|
||||
export const fetchFailedEmailsAction = emailOpsStatePart.createAction(async (statePartArg) => {
|
||||
const context = getActionContext();
|
||||
const currentState = statePartArg.getState();
|
||||
|
||||
try {
|
||||
const request = new plugins.domtools.plugins.typedrequest.TypedRequest<
|
||||
interfaces.requests.IReq_GetFailedEmails
|
||||
>('/typedrequest', 'getFailedEmails');
|
||||
|
||||
const response = await request.fire({
|
||||
identity: context.identity,
|
||||
limit: 100,
|
||||
});
|
||||
|
||||
return {
|
||||
...currentState,
|
||||
failedEmails: response.items,
|
||||
isLoading: false,
|
||||
error: null,
|
||||
lastUpdated: Date.now(),
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
...currentState,
|
||||
isLoading: false,
|
||||
error: error instanceof Error ? error.message : 'Failed to fetch failed emails',
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
// Fetch Security Incidents Action
|
||||
export const fetchSecurityIncidentsAction = emailOpsStatePart.createAction(async (statePartArg) => {
|
||||
const context = getActionContext();
|
||||
const currentState = statePartArg.getState();
|
||||
|
||||
try {
|
||||
const request = new plugins.domtools.plugins.typedrequest.TypedRequest<
|
||||
interfaces.requests.IReq_GetSecurityIncidents
|
||||
>('/typedrequest', 'getSecurityIncidents');
|
||||
|
||||
const response = await request.fire({
|
||||
identity: context.identity,
|
||||
limit: 100,
|
||||
});
|
||||
|
||||
return {
|
||||
...currentState,
|
||||
securityIncidents: response.incidents,
|
||||
isLoading: false,
|
||||
error: null,
|
||||
lastUpdated: Date.now(),
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
...currentState,
|
||||
isLoading: false,
|
||||
error: error instanceof Error ? error.message : 'Failed to fetch security incidents',
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
// Fetch Bounce Records Action
|
||||
export const fetchBounceRecordsAction = emailOpsStatePart.createAction(async (statePartArg) => {
|
||||
const context = getActionContext();
|
||||
const currentState = statePartArg.getState();
|
||||
|
||||
try {
|
||||
const request = new plugins.domtools.plugins.typedrequest.TypedRequest<
|
||||
interfaces.requests.IReq_GetBounceRecords
|
||||
>('/typedrequest', 'getBounceRecords');
|
||||
|
||||
const response = await request.fire({
|
||||
identity: context.identity,
|
||||
limit: 100,
|
||||
});
|
||||
|
||||
return {
|
||||
...currentState,
|
||||
bounceRecords: response.records,
|
||||
suppressionList: response.suppressionList,
|
||||
isLoading: false,
|
||||
error: null,
|
||||
lastUpdated: Date.now(),
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
...currentState,
|
||||
isLoading: false,
|
||||
error: error instanceof Error ? error.message : 'Failed to fetch bounce records',
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
// Resend Failed Email Action
|
||||
export const resendEmailAction = emailOpsStatePart.createAction<string>(async (statePartArg, emailId) => {
|
||||
const context = getActionContext();
|
||||
const currentState = statePartArg.getState();
|
||||
|
||||
try {
|
||||
const request = new plugins.domtools.plugins.typedrequest.TypedRequest<
|
||||
interfaces.requests.IReq_ResendEmail
|
||||
>('/typedrequest', 'resendEmail');
|
||||
|
||||
const response = await request.fire({
|
||||
identity: context.identity,
|
||||
emailId,
|
||||
});
|
||||
|
||||
if (response.success) {
|
||||
// Refresh failed emails list
|
||||
await emailOpsStatePart.dispatchAction(fetchFailedEmailsAction, null);
|
||||
await emailOpsStatePart.dispatchAction(fetchQueuedEmailsAction, null);
|
||||
}
|
||||
|
||||
return statePartArg.getState();
|
||||
} catch (error) {
|
||||
return {
|
||||
...currentState,
|
||||
error: error instanceof Error ? error.message : 'Failed to resend email',
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
// Remove from Suppression List Action
|
||||
export const removeFromSuppressionListAction = emailOpsStatePart.createAction<string>(
|
||||
async (statePartArg, email) => {
|
||||
const context = getActionContext();
|
||||
const currentState = statePartArg.getState();
|
||||
|
||||
try {
|
||||
const request = new plugins.domtools.plugins.typedrequest.TypedRequest<
|
||||
interfaces.requests.IReq_RemoveFromSuppressionList
|
||||
>('/typedrequest', 'removeFromSuppressionList');
|
||||
|
||||
const response = await request.fire({
|
||||
identity: context.identity,
|
||||
email,
|
||||
});
|
||||
|
||||
if (response.success) {
|
||||
// Refresh bounce records
|
||||
await emailOpsStatePart.dispatchAction(fetchBounceRecordsAction, null);
|
||||
}
|
||||
|
||||
return statePartArg.getState();
|
||||
} catch (error) {
|
||||
return {
|
||||
...currentState,
|
||||
error: error instanceof Error ? error.message : 'Failed to remove from suppression list',
|
||||
};
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
// ============================================================================
|
||||
// Certificate Actions
|
||||
// ============================================================================
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { DeesElement, property, html, customElement, type TemplateResult, css, state, cssManager } from '@design.estate/dees-element';
|
||||
import * as plugins from '../plugins.js';
|
||||
import * as appstate from '../appstate.js';
|
||||
import * as shared from './shared/index.js';
|
||||
import * as interfaces from '../../dist_ts_interfaces/index.js';
|
||||
import { appRouter } from '../router.js';
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
@@ -10,67 +10,30 @@ declare global {
|
||||
}
|
||||
}
|
||||
|
||||
type TEmailFolder = 'queued' | 'sent' | 'failed' | 'received' | 'security';
|
||||
|
||||
@customElement('ops-view-emails')
|
||||
export class OpsViewEmails extends DeesElement {
|
||||
@state()
|
||||
accessor selectedFolder: TEmailFolder = 'queued';
|
||||
accessor emails: interfaces.requests.IEmail[] = [];
|
||||
|
||||
@state()
|
||||
accessor queuedEmails: interfaces.requests.IEmailQueueItem[] = [];
|
||||
accessor selectedEmail: interfaces.requests.IEmailDetail | null = null;
|
||||
|
||||
@state()
|
||||
accessor sentEmails: interfaces.requests.IEmailQueueItem[] = [];
|
||||
|
||||
@state()
|
||||
accessor failedEmails: interfaces.requests.IEmailQueueItem[] = [];
|
||||
|
||||
@state()
|
||||
accessor securityIncidents: interfaces.requests.ISecurityIncident[] = [];
|
||||
|
||||
@state()
|
||||
accessor selectedEmail: interfaces.requests.IEmailQueueItem | null = null;
|
||||
|
||||
@state()
|
||||
accessor selectedIncident: interfaces.requests.ISecurityIncident | null = null;
|
||||
|
||||
@state()
|
||||
accessor showCompose = false;
|
||||
accessor currentView: 'list' | 'detail' = 'list';
|
||||
|
||||
@state()
|
||||
accessor isLoading = false;
|
||||
|
||||
@state()
|
||||
accessor searchTerm = '';
|
||||
|
||||
@state()
|
||||
accessor emailDomains: string[] = [];
|
||||
|
||||
private stateSubscription: any;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this.loadData();
|
||||
this.loadEmailDomains();
|
||||
}
|
||||
|
||||
async connectedCallback() {
|
||||
await super.connectedCallback();
|
||||
// Subscribe to state changes
|
||||
this.stateSubscription = appstate.emailOpsStatePart.state.subscribe((state) => {
|
||||
this.queuedEmails = state.queuedEmails;
|
||||
this.sentEmails = state.sentEmails;
|
||||
this.failedEmails = state.failedEmails;
|
||||
this.securityIncidents = state.securityIncidents;
|
||||
this.emails = state.emails;
|
||||
this.isLoading = state.isLoading;
|
||||
|
||||
// Sync folder from state (e.g., when URL changes)
|
||||
if (state.currentView !== this.selectedFolder) {
|
||||
this.selectedFolder = state.currentView as TEmailFolder;
|
||||
this.loadFolderData(state.currentView as TEmailFolder);
|
||||
}
|
||||
});
|
||||
// Initial fetch
|
||||
await appstate.emailOpsStatePart.dispatchAction(appstate.fetchAllEmailsAction, null);
|
||||
}
|
||||
|
||||
async disconnectedCallback() {
|
||||
@@ -89,730 +52,58 @@ export class OpsViewEmails extends DeesElement {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.emailLayout {
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
.viewContainer {
|
||||
height: 100%;
|
||||
min-height: 600px;
|
||||
}
|
||||
|
||||
.sidebar {
|
||||
flex-shrink: 0;
|
||||
width: 280px;
|
||||
}
|
||||
|
||||
.mainArea {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.emailToolbar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.searchBox {
|
||||
flex: 1;
|
||||
min-width: 200px;
|
||||
max-width: 400px;
|
||||
}
|
||||
|
||||
.emailList {
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.emailPreview {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background: ${cssManager.bdTheme('#fff', '#222')};
|
||||
border: 1px solid ${cssManager.bdTheme('#e9ecef', '#333')};
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.emailHeader {
|
||||
padding: 24px;
|
||||
border-bottom: 1px solid ${cssManager.bdTheme('#e9ecef', '#333')};
|
||||
}
|
||||
|
||||
.emailSubject {
|
||||
font-size: 24px;
|
||||
font-weight: 600;
|
||||
margin-bottom: 16px;
|
||||
color: ${cssManager.bdTheme('#333', '#ccc')};
|
||||
}
|
||||
|
||||
.emailMeta {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
font-size: 14px;
|
||||
color: ${cssManager.bdTheme('#666', '#999')};
|
||||
}
|
||||
|
||||
.emailMetaRow {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.emailMetaLabel {
|
||||
font-weight: 600;
|
||||
min-width: 80px;
|
||||
}
|
||||
|
||||
.emailBody {
|
||||
flex: 1;
|
||||
padding: 24px;
|
||||
overflow-y: auto;
|
||||
font-size: 15px;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.emailActions {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
padding: 16px 24px;
|
||||
border-top: 1px solid ${cssManager.bdTheme('#e9ecef', '#333')};
|
||||
background: ${cssManager.bdTheme('#fafafa', '#1a1a1a')};
|
||||
}
|
||||
|
||||
.emptyState {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 400px;
|
||||
color: ${cssManager.bdTheme('#999', '#666')};
|
||||
}
|
||||
|
||||
.emptyIcon {
|
||||
font-size: 64px;
|
||||
margin-bottom: 16px;
|
||||
opacity: 0.3;
|
||||
}
|
||||
|
||||
.emptyText {
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
.status-pending {
|
||||
color: ${cssManager.bdTheme('#f59e0b', '#fbbf24')};
|
||||
}
|
||||
|
||||
.status-processing {
|
||||
color: ${cssManager.bdTheme('#3b82f6', '#60a5fa')};
|
||||
}
|
||||
|
||||
.status-delivered {
|
||||
color: ${cssManager.bdTheme('#10b981', '#34d399')};
|
||||
}
|
||||
|
||||
.status-failed {
|
||||
color: ${cssManager.bdTheme('#ef4444', '#f87171')};
|
||||
}
|
||||
|
||||
.status-deferred {
|
||||
color: ${cssManager.bdTheme('#f97316', '#fb923c')};
|
||||
}
|
||||
|
||||
.severity-info {
|
||||
color: ${cssManager.bdTheme('#3b82f6', '#60a5fa')};
|
||||
}
|
||||
|
||||
.severity-warn {
|
||||
color: ${cssManager.bdTheme('#f59e0b', '#fbbf24')};
|
||||
}
|
||||
|
||||
.severity-error {
|
||||
color: ${cssManager.bdTheme('#ef4444', '#f87171')};
|
||||
}
|
||||
|
||||
.severity-critical {
|
||||
color: ${cssManager.bdTheme('#dc2626', '#ef4444')};
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.incidentDetails {
|
||||
padding: 24px;
|
||||
background: ${cssManager.bdTheme('#fff', '#222')};
|
||||
border: 1px solid ${cssManager.bdTheme('#e9ecef', '#333')};
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.incidentHeader {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.incidentTitle {
|
||||
font-size: 20px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.incidentMeta {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
|
||||
gap: 12px;
|
||||
margin-top: 16px;
|
||||
}
|
||||
|
||||
.incidentField {
|
||||
padding: 12px;
|
||||
background: ${cssManager.bdTheme('#f8f9fa', '#1a1a1a')};
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
.incidentFieldLabel {
|
||||
font-size: 12px;
|
||||
color: ${cssManager.bdTheme('#666', '#999')};
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.incidentFieldValue {
|
||||
font-size: 14px;
|
||||
word-break: break-all;
|
||||
}
|
||||
`,
|
||||
];
|
||||
|
||||
public render() {
|
||||
if (this.selectedEmail) {
|
||||
return this.renderEmailDetail();
|
||||
}
|
||||
|
||||
if (this.selectedIncident) {
|
||||
return this.renderIncidentDetail();
|
||||
}
|
||||
|
||||
return html`
|
||||
<ops-sectionheading>Email Operations</ops-sectionheading>
|
||||
|
||||
<!-- Toolbar -->
|
||||
<div class="emailToolbar" style="margin-bottom: 16px;">
|
||||
<dees-button @click=${() => this.openComposeModal()} type="highlighted">
|
||||
<dees-icon icon="lucide:penLine" slot="iconSlot"></dees-icon>
|
||||
Compose
|
||||
</dees-button>
|
||||
|
||||
<dees-input-text
|
||||
class="searchBox"
|
||||
placeholder="Search..."
|
||||
.value=${this.searchTerm}
|
||||
@input=${(e: Event) => this.searchTerm = (e.target as any).value}
|
||||
>
|
||||
<dees-icon icon="lucide:search" slot="iconSlot"></dees-icon>
|
||||
</dees-input-text>
|
||||
|
||||
<dees-button @click=${() => this.refreshData()}>
|
||||
${this.isLoading ? html`<dees-spinner slot="iconSlot" size="small"></dees-spinner>` : html`<dees-icon slot="iconSlot" icon="lucide:refreshCw"></dees-icon>`}
|
||||
Refresh
|
||||
</dees-button>
|
||||
|
||||
<div style="margin-left: auto; display: flex; gap: 8px;">
|
||||
<dees-button-group>
|
||||
<dees-button
|
||||
@click=${() => this.selectFolder('queued')}
|
||||
.type=${this.selectedFolder === 'queued' ? 'highlighted' : 'normal'}
|
||||
>
|
||||
Queued ${this.queuedEmails.length > 0 ? `(${this.queuedEmails.length})` : ''}
|
||||
</dees-button>
|
||||
<dees-button
|
||||
@click=${() => this.selectFolder('sent')}
|
||||
.type=${this.selectedFolder === 'sent' ? 'highlighted' : 'normal'}
|
||||
>
|
||||
Sent
|
||||
</dees-button>
|
||||
<dees-button
|
||||
@click=${() => this.selectFolder('failed')}
|
||||
.type=${this.selectedFolder === 'failed' ? 'highlighted' : 'normal'}
|
||||
>
|
||||
Failed ${this.failedEmails.length > 0 ? `(${this.failedEmails.length})` : ''}
|
||||
</dees-button>
|
||||
<dees-button
|
||||
@click=${() => this.selectFolder('security')}
|
||||
.type=${this.selectedFolder === 'security' ? 'highlighted' : 'normal'}
|
||||
>
|
||||
Security ${this.securityIncidents.length > 0 ? `(${this.securityIncidents.length})` : ''}
|
||||
</dees-button>
|
||||
</dees-button-group>
|
||||
</div>
|
||||
<div class="viewContainer">
|
||||
${this.currentView === 'detail' && this.selectedEmail
|
||||
? html`
|
||||
<sz-mta-detail-view
|
||||
.email=${this.selectedEmail}
|
||||
@back=${this.handleBack}
|
||||
></sz-mta-detail-view>
|
||||
`
|
||||
: html`
|
||||
<sz-mta-list-view
|
||||
.emails=${this.emails}
|
||||
@email-click=${this.handleEmailClick}
|
||||
></sz-mta-list-view>
|
||||
`
|
||||
}
|
||||
</div>
|
||||
|
||||
${this.renderContent()}
|
||||
`;
|
||||
}
|
||||
|
||||
private renderContent() {
|
||||
switch (this.selectedFolder) {
|
||||
case 'queued':
|
||||
return this.renderEmailTable(this.queuedEmails, 'Queued Emails', 'Emails waiting to be delivered');
|
||||
case 'sent':
|
||||
return this.renderEmailTable(this.sentEmails, 'Sent Emails', 'Successfully delivered emails');
|
||||
case 'failed':
|
||||
return this.renderEmailTable(this.failedEmails, 'Failed Emails', 'Emails that failed to deliver', true);
|
||||
case 'security':
|
||||
return this.renderSecurityIncidents();
|
||||
default:
|
||||
return this.renderEmptyState('Select a folder');
|
||||
}
|
||||
}
|
||||
private async handleEmailClick(e: CustomEvent<interfaces.requests.IEmail>) {
|
||||
const emailSummary = e.detail;
|
||||
try {
|
||||
const context = appstate.loginStatePart.getState();
|
||||
const request = new plugins.domtools.plugins.typedrequest.TypedRequest<
|
||||
interfaces.requests.IReq_GetEmailDetail
|
||||
>('/typedrequest', 'getEmailDetail');
|
||||
|
||||
private renderEmailTable(
|
||||
emails: interfaces.requests.IEmailQueueItem[],
|
||||
heading1: string,
|
||||
heading2: string,
|
||||
showResend = false
|
||||
) {
|
||||
const filteredEmails = this.filterEmails(emails);
|
||||
|
||||
if (filteredEmails.length === 0) {
|
||||
return this.renderEmptyState(`No emails in ${this.selectedFolder}`);
|
||||
}
|
||||
|
||||
const actions = [
|
||||
{
|
||||
name: 'View Details',
|
||||
iconName: 'lucide:eye',
|
||||
type: ['doubleClick', 'inRow'] as any,
|
||||
actionFunc: async (actionData: any) => {
|
||||
this.selectedEmail = actionData.item;
|
||||
}
|
||||
}
|
||||
];
|
||||
|
||||
if (showResend) {
|
||||
actions.push({
|
||||
name: 'Resend',
|
||||
iconName: 'lucide:send',
|
||||
type: ['inRow'] as any,
|
||||
actionFunc: async (actionData: any) => {
|
||||
await this.resendEmail(actionData.item.id);
|
||||
}
|
||||
const response = await request.fire({
|
||||
identity: context.identity,
|
||||
emailId: emailSummary.id,
|
||||
});
|
||||
}
|
||||
|
||||
return html`
|
||||
<dees-table
|
||||
.data=${filteredEmails}
|
||||
.displayFunction=${(email: interfaces.requests.IEmailQueueItem) => ({
|
||||
'Status': html`<span class="status-${email.status}">${email.status}</span>`,
|
||||
'From': email.from || 'N/A',
|
||||
'To': email.to?.join(', ') || 'N/A',
|
||||
'Subject': email.subject || 'No subject',
|
||||
'Attempts': email.attempts,
|
||||
'Created': this.formatDate(email.createdAt),
|
||||
})}
|
||||
.dataActions=${actions}
|
||||
.selectionMode=${'single'}
|
||||
heading1=${heading1}
|
||||
heading2=${`${filteredEmails.length} emails - ${heading2}`}
|
||||
></dees-table>
|
||||
`;
|
||||
}
|
||||
|
||||
private renderSecurityIncidents() {
|
||||
const incidents = this.securityIncidents;
|
||||
|
||||
if (incidents.length === 0) {
|
||||
return this.renderEmptyState('No security incidents');
|
||||
}
|
||||
|
||||
return html`
|
||||
<dees-table
|
||||
.data=${incidents}
|
||||
.displayFunction=${(incident: interfaces.requests.ISecurityIncident) => ({
|
||||
'Severity': html`<span class="severity-${incident.level}">${incident.level.toUpperCase()}</span>`,
|
||||
'Type': incident.type,
|
||||
'Message': incident.message,
|
||||
'IP': incident.ipAddress || 'N/A',
|
||||
'Domain': incident.domain || 'N/A',
|
||||
'Time': this.formatDate(incident.timestamp),
|
||||
})}
|
||||
.dataActions=${[
|
||||
{
|
||||
name: 'View Details',
|
||||
iconName: 'lucide:eye',
|
||||
type: ['doubleClick', 'inRow'],
|
||||
actionFunc: async (actionData: any) => {
|
||||
this.selectedIncident = actionData.item;
|
||||
}
|
||||
}
|
||||
]}
|
||||
.selectionMode=${'single'}
|
||||
heading1="Security Incidents"
|
||||
heading2=${`${incidents.length} incidents`}
|
||||
></dees-table>
|
||||
`;
|
||||
}
|
||||
|
||||
private renderEmailDetail() {
|
||||
if (!this.selectedEmail) return '';
|
||||
|
||||
return html`
|
||||
<ops-sectionheading>Email Details</ops-sectionheading>
|
||||
<div class="emailLayout">
|
||||
<div class="sidebar">
|
||||
<dees-windowbox>
|
||||
<dees-button @click=${() => this.selectedEmail = null} type="secondary" style="width: 100%;">
|
||||
<dees-icon icon="lucide:arrowLeft" slot="iconSlot"></dees-icon>
|
||||
Back to List
|
||||
</dees-button>
|
||||
</dees-windowbox>
|
||||
</div>
|
||||
<div class="mainArea">
|
||||
<div class="emailPreview">
|
||||
<div class="emailHeader">
|
||||
<div class="emailSubject">${this.selectedEmail.subject || 'No subject'}</div>
|
||||
<div class="emailMeta">
|
||||
<div class="emailMetaRow">
|
||||
<span class="emailMetaLabel">Status:</span>
|
||||
<span class="status-${this.selectedEmail.status}">${this.selectedEmail.status}</span>
|
||||
</div>
|
||||
<div class="emailMetaRow">
|
||||
<span class="emailMetaLabel">From:</span>
|
||||
<span>${this.selectedEmail.from || 'N/A'}</span>
|
||||
</div>
|
||||
<div class="emailMetaRow">
|
||||
<span class="emailMetaLabel">To:</span>
|
||||
<span>${this.selectedEmail.to?.join(', ') || 'N/A'}</span>
|
||||
</div>
|
||||
<div class="emailMetaRow">
|
||||
<span class="emailMetaLabel">Mode:</span>
|
||||
<span>${this.selectedEmail.processingMode}</span>
|
||||
</div>
|
||||
<div class="emailMetaRow">
|
||||
<span class="emailMetaLabel">Attempts:</span>
|
||||
<span>${this.selectedEmail.attempts}</span>
|
||||
</div>
|
||||
<div class="emailMetaRow">
|
||||
<span class="emailMetaLabel">Created:</span>
|
||||
<span>${new Date(this.selectedEmail.createdAt).toLocaleString()}</span>
|
||||
</div>
|
||||
${this.selectedEmail.deliveredAt ? html`
|
||||
<div class="emailMetaRow">
|
||||
<span class="emailMetaLabel">Delivered:</span>
|
||||
<span>${new Date(this.selectedEmail.deliveredAt).toLocaleString()}</span>
|
||||
</div>
|
||||
` : ''}
|
||||
${this.selectedEmail.lastError ? html`
|
||||
<div class="emailMetaRow">
|
||||
<span class="emailMetaLabel">Last Error:</span>
|
||||
<span style="color: #ef4444;">${this.selectedEmail.lastError}</span>
|
||||
</div>
|
||||
` : ''}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="emailActions">
|
||||
${this.selectedEmail.status === 'failed' ? html`
|
||||
<dees-button @click=${() => this.resendEmail(this.selectedEmail!.id)} type="highlighted">
|
||||
<dees-icon icon="lucide:send" slot="iconSlot"></dees-icon>
|
||||
Resend
|
||||
</dees-button>
|
||||
` : ''}
|
||||
<dees-button @click=${() => this.selectedEmail = null}>
|
||||
<dees-icon icon="lucide:x" slot="iconSlot"></dees-icon>
|
||||
Close
|
||||
</dees-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
private renderIncidentDetail() {
|
||||
if (!this.selectedIncident) return '';
|
||||
|
||||
const incident = this.selectedIncident;
|
||||
|
||||
return html`
|
||||
<ops-sectionheading>Security Incident Details</ops-sectionheading>
|
||||
<div style="margin-bottom: 16px;">
|
||||
<dees-button @click=${() => this.selectedIncident = null} type="secondary">
|
||||
<dees-icon icon="lucide:arrowLeft" slot="iconSlot"></dees-icon>
|
||||
Back to List
|
||||
</dees-button>
|
||||
</div>
|
||||
<div class="incidentDetails">
|
||||
<div class="incidentHeader">
|
||||
<div>
|
||||
<div class="incidentTitle">${incident.message}</div>
|
||||
<div style="margin-top: 8px; color: #666;">
|
||||
${new Date(incident.timestamp).toLocaleString()}
|
||||
</div>
|
||||
</div>
|
||||
<span class="severity-${incident.level}" style="font-size: 16px; padding: 4px 12px; background: rgba(0,0,0,0.1); border-radius: 4px;">
|
||||
${incident.level.toUpperCase()}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="incidentMeta">
|
||||
<div class="incidentField">
|
||||
<div class="incidentFieldLabel">Type</div>
|
||||
<div class="incidentFieldValue">${incident.type}</div>
|
||||
</div>
|
||||
${incident.ipAddress ? html`
|
||||
<div class="incidentField">
|
||||
<div class="incidentFieldLabel">IP Address</div>
|
||||
<div class="incidentFieldValue">${incident.ipAddress}</div>
|
||||
</div>
|
||||
` : ''}
|
||||
${incident.domain ? html`
|
||||
<div class="incidentField">
|
||||
<div class="incidentFieldLabel">Domain</div>
|
||||
<div class="incidentFieldValue">${incident.domain}</div>
|
||||
</div>
|
||||
` : ''}
|
||||
${incident.emailId ? html`
|
||||
<div class="incidentField">
|
||||
<div class="incidentFieldLabel">Email ID</div>
|
||||
<div class="incidentFieldValue">${incident.emailId}</div>
|
||||
</div>
|
||||
` : ''}
|
||||
${incident.userId ? html`
|
||||
<div class="incidentField">
|
||||
<div class="incidentFieldLabel">User ID</div>
|
||||
<div class="incidentFieldValue">${incident.userId}</div>
|
||||
</div>
|
||||
` : ''}
|
||||
${incident.action ? html`
|
||||
<div class="incidentField">
|
||||
<div class="incidentFieldLabel">Action</div>
|
||||
<div class="incidentFieldValue">${incident.action}</div>
|
||||
</div>
|
||||
` : ''}
|
||||
${incident.result ? html`
|
||||
<div class="incidentField">
|
||||
<div class="incidentFieldLabel">Result</div>
|
||||
<div class="incidentFieldValue">${incident.result}</div>
|
||||
</div>
|
||||
` : ''}
|
||||
${incident.success !== undefined ? html`
|
||||
<div class="incidentField">
|
||||
<div class="incidentFieldLabel">Success</div>
|
||||
<div class="incidentFieldValue">${incident.success ? 'Yes' : 'No'}</div>
|
||||
</div>
|
||||
` : ''}
|
||||
</div>
|
||||
|
||||
${incident.details ? html`
|
||||
<div style="margin-top: 24px;">
|
||||
<div class="incidentFieldLabel" style="margin-bottom: 8px;">Details</div>
|
||||
<pre style="background: #1a1a1a; color: #e5e5e5; padding: 16px; border-radius: 6px; overflow-x: auto; font-size: 13px;">
|
||||
${JSON.stringify(incident.details, null, 2)}
|
||||
</pre>
|
||||
</div>
|
||||
` : ''}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
private renderEmptyState(message: string) {
|
||||
return html`
|
||||
<div class="emptyState">
|
||||
<dees-icon class="emptyIcon" icon="lucide:inbox"></dees-icon>
|
||||
<div class="emptyText">${message}</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
private async openComposeModal() {
|
||||
const { DeesModal } = await import('@design.estate/dees-catalog');
|
||||
|
||||
// Ensure domains are loaded before opening modal
|
||||
if (this.emailDomains.length === 0) {
|
||||
await this.loadEmailDomains();
|
||||
}
|
||||
|
||||
await DeesModal.createAndShow({
|
||||
heading: 'New Email',
|
||||
width: 'large',
|
||||
content: html`
|
||||
<div>
|
||||
<dees-form @formData=${async (e: CustomEvent) => {
|
||||
await this.sendEmail(e.detail);
|
||||
const modals = document.querySelectorAll('dees-modal');
|
||||
modals.forEach(m => (m as any).destroy?.());
|
||||
}}>
|
||||
<div style="display: flex; gap: 8px; align-items: flex-end;">
|
||||
<dees-input-text
|
||||
key="fromUsername"
|
||||
label="From"
|
||||
placeholder="username"
|
||||
.value=${'admin'}
|
||||
required
|
||||
style="flex: 1;"
|
||||
></dees-input-text>
|
||||
<span style="padding-bottom: 12px; font-size: 18px; color: #666;">@</span>
|
||||
<dees-input-dropdown
|
||||
key="fromDomain"
|
||||
label=" "
|
||||
.options=${this.emailDomains.length > 0
|
||||
? this.emailDomains.map(domain => ({ key: domain, value: domain }))
|
||||
: [{ key: 'dcrouter.local', value: 'dcrouter.local' }]}
|
||||
.selectedKey=${this.emailDomains[0] || 'dcrouter.local'}
|
||||
required
|
||||
style="flex: 1;"
|
||||
></dees-input-dropdown>
|
||||
</div>
|
||||
|
||||
<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-text
|
||||
key="subject"
|
||||
label="Subject"
|
||||
placeholder="Enter email subject..."
|
||||
required
|
||||
></dees-input-text>
|
||||
|
||||
<dees-input-wysiwyg
|
||||
key="body"
|
||||
label="Message"
|
||||
outputFormat="html"
|
||||
></dees-input-wysiwyg>
|
||||
</dees-form>
|
||||
</div>
|
||||
`,
|
||||
menuOptions: [
|
||||
{
|
||||
name: 'Send',
|
||||
iconName: 'lucide:send',
|
||||
action: async (modalArg) => {
|
||||
const form = modalArg.shadowRoot?.querySelector('dees-form') as any;
|
||||
form?.submit();
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'Cancel',
|
||||
iconName: 'lucide:x',
|
||||
action: async (modalArg) => await modalArg.destroy()
|
||||
}
|
||||
]
|
||||
});
|
||||
}
|
||||
|
||||
private filterEmails(emails: interfaces.requests.IEmailQueueItem[]): interfaces.requests.IEmailQueueItem[] {
|
||||
if (!this.searchTerm) {
|
||||
return emails;
|
||||
}
|
||||
|
||||
const search = this.searchTerm.toLowerCase();
|
||||
return emails.filter(e =>
|
||||
(e.subject?.toLowerCase().includes(search)) ||
|
||||
(e.from?.toLowerCase().includes(search)) ||
|
||||
(e.to?.some(t => t.toLowerCase().includes(search)))
|
||||
);
|
||||
}
|
||||
|
||||
private selectFolder(folder: TEmailFolder) {
|
||||
// Use router for navigation to update URL
|
||||
appRouter.navigateToEmailFolder(folder);
|
||||
// Clear selections
|
||||
this.selectedEmail = null;
|
||||
this.selectedIncident = 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', hour: '2-digit', minute: '2-digit' });
|
||||
} else {
|
||||
return date.toLocaleDateString([], { month: 'short', day: 'numeric' });
|
||||
}
|
||||
}
|
||||
|
||||
private async loadData() {
|
||||
this.isLoading = true;
|
||||
await this.loadFolderData(this.selectedFolder);
|
||||
this.isLoading = false;
|
||||
}
|
||||
|
||||
private async loadFolderData(folder: TEmailFolder) {
|
||||
switch (folder) {
|
||||
case 'queued':
|
||||
await appstate.emailOpsStatePart.dispatchAction(appstate.fetchQueuedEmailsAction, null);
|
||||
break;
|
||||
case 'sent':
|
||||
await appstate.emailOpsStatePart.dispatchAction(appstate.fetchSentEmailsAction, null);
|
||||
break;
|
||||
case 'failed':
|
||||
await appstate.emailOpsStatePart.dispatchAction(appstate.fetchFailedEmailsAction, null);
|
||||
break;
|
||||
case 'security':
|
||||
await appstate.emailOpsStatePart.dispatchAction(appstate.fetchSecurityIncidentsAction, null);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private async loadEmailDomains() {
|
||||
try {
|
||||
await appstate.configStatePart.dispatchAction(appstate.fetchConfigurationAction, null);
|
||||
const config = appstate.configStatePart.getState().config;
|
||||
|
||||
if (config?.email?.domains && Array.isArray(config.email.domains) && config.email.domains.length > 0) {
|
||||
this.emailDomains = config.email.domains;
|
||||
} else {
|
||||
this.emailDomains = ['dcrouter.local'];
|
||||
if (response.email) {
|
||||
this.selectedEmail = response.email;
|
||||
this.currentView = 'detail';
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to load email domains:', error);
|
||||
this.emailDomains = ['dcrouter.local'];
|
||||
console.error('Failed to fetch email detail:', error);
|
||||
}
|
||||
}
|
||||
|
||||
private async refreshData() {
|
||||
this.isLoading = true;
|
||||
await this.loadFolderData(this.selectedFolder);
|
||||
this.isLoading = false;
|
||||
}
|
||||
|
||||
private async sendEmail(formData: any) {
|
||||
try {
|
||||
console.log('Sending email:', formData);
|
||||
// TODO: Implement actual email sending via API
|
||||
// For now, just log the data
|
||||
const fromEmail = `${formData.fromUsername || 'admin'}@${formData.fromDomain || this.emailDomains[0] || 'dcrouter.local'}`;
|
||||
console.log('From:', fromEmail);
|
||||
console.log('To:', formData.to);
|
||||
console.log('Subject:', formData.subject);
|
||||
} catch (error: any) {
|
||||
console.error('Failed to send email', error);
|
||||
}
|
||||
}
|
||||
|
||||
private async resendEmail(emailId: string) {
|
||||
try {
|
||||
await appstate.emailOpsStatePart.dispatchAction(appstate.resendEmailAction, emailId);
|
||||
this.selectedEmail = null;
|
||||
} catch (error) {
|
||||
console.error('Failed to resend email:', error);
|
||||
}
|
||||
private handleBack() {
|
||||
this.selectedEmail = null;
|
||||
this.currentView = 'list';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import * as plugins from '../plugins.js';
|
||||
import * as shared from './shared/index.js';
|
||||
import * as appstate from '../appstate.js';
|
||||
|
||||
@@ -20,15 +19,6 @@ export class OpsViewLogs extends DeesElement {
|
||||
filters: {},
|
||||
};
|
||||
|
||||
@state()
|
||||
accessor filterLevel: string | undefined;
|
||||
|
||||
@state()
|
||||
accessor filterCategory: string | undefined;
|
||||
|
||||
@state()
|
||||
accessor filterLimit: number = 100;
|
||||
|
||||
private lastPushedCount = 0;
|
||||
|
||||
constructor() {
|
||||
@@ -44,63 +34,13 @@ export class OpsViewLogs extends DeesElement {
|
||||
public static styles = [
|
||||
cssManager.defaultStyles,
|
||||
shared.viewHostCss,
|
||||
css`
|
||||
.controls {
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
margin-bottom: 24px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.filterGroup {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
`,
|
||||
css``,
|
||||
];
|
||||
|
||||
public render() {
|
||||
return html`
|
||||
<ops-sectionheading>Logs</ops-sectionheading>
|
||||
|
||||
<div class="controls">
|
||||
<div class="filterGroup">
|
||||
<dees-button
|
||||
@click=${() => this.fetchLogs()}
|
||||
>
|
||||
Refresh Logs
|
||||
</dees-button>
|
||||
</div>
|
||||
|
||||
<div class="filterGroup">
|
||||
<label>Level:</label>
|
||||
<dees-input-dropdown
|
||||
.options=${['all', 'debug', 'info', 'warn', 'error']}
|
||||
.selectedOption=${'all'}
|
||||
@selectedOption=${(e: any) => this.updateFilter('level', e.detail)}
|
||||
></dees-input-dropdown>
|
||||
</div>
|
||||
|
||||
<div class="filterGroup">
|
||||
<label>Category:</label>
|
||||
<dees-input-dropdown
|
||||
.options=${['all', 'smtp', 'dns', 'security', 'system', 'email']}
|
||||
.selectedOption=${'all'}
|
||||
@selectedOption=${(e: any) => this.updateFilter('category', e.detail)}
|
||||
></dees-input-dropdown>
|
||||
</div>
|
||||
|
||||
<div class="filterGroup">
|
||||
<label>Limit:</label>
|
||||
<dees-input-dropdown
|
||||
.options=${['50', '100', '200', '500']}
|
||||
.selectedOption=${'100'}
|
||||
@selectedOption=${(e: any) => this.updateFilter('limit', e.detail)}
|
||||
></dees-input-dropdown>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<dees-chart-log
|
||||
.label=${'Application Logs'}
|
||||
.autoScroll=${true}
|
||||
@@ -115,7 +55,7 @@ export class OpsViewLogs extends DeesElement {
|
||||
this.lastPushedCount = 0;
|
||||
// Only fetch if state is empty (streaming will handle new entries)
|
||||
if (this.logState.recentLogs.length === 0) {
|
||||
this.fetchLogs();
|
||||
await appstate.logStatePart.dispatchAction(appstate.fetchRecentLogsAction, { limit: 100 });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -166,29 +106,4 @@ export class OpsViewLogs extends DeesElement {
|
||||
}));
|
||||
}
|
||||
|
||||
private async fetchLogs() {
|
||||
await appstate.logStatePart.dispatchAction(appstate.fetchRecentLogsAction, {
|
||||
limit: this.filterLimit,
|
||||
level: this.filterLevel as 'debug' | 'info' | 'warn' | 'error' | undefined,
|
||||
category: this.filterCategory as 'smtp' | 'dns' | 'security' | 'system' | 'email' | undefined,
|
||||
});
|
||||
}
|
||||
|
||||
private updateFilter(type: string, value: string) {
|
||||
const resolved = value === 'all' ? undefined : value;
|
||||
|
||||
switch (type) {
|
||||
case 'level':
|
||||
this.filterLevel = resolved;
|
||||
break;
|
||||
case 'category':
|
||||
this.filterCategory = resolved;
|
||||
break;
|
||||
case 'limit':
|
||||
this.filterLimit = resolved ? parseInt(resolved, 10) : 100;
|
||||
break;
|
||||
}
|
||||
|
||||
this.fetchLogs();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,12 +2,16 @@
|
||||
import * as deesElement from '@design.estate/dees-element';
|
||||
import * as deesCatalog from '@design.estate/dees-catalog';
|
||||
|
||||
// @serve.zone scope
|
||||
import * as szCatalog from '@serve.zone/catalog';
|
||||
|
||||
// TypedSocket for real-time push communication
|
||||
import * as typedsocket from '@api.global/typedsocket';
|
||||
|
||||
export {
|
||||
deesElement,
|
||||
deesCatalog,
|
||||
szCatalog,
|
||||
typedsocket,
|
||||
}
|
||||
|
||||
|
||||
@@ -4,10 +4,8 @@ import * as appstate from './appstate.js';
|
||||
const SmartRouter = plugins.domtools.plugins.smartrouter.SmartRouter;
|
||||
|
||||
export const validViews = ['overview', 'network', 'emails', 'logs', 'configuration', 'security', 'certificates', 'remoteingress'] as const;
|
||||
export const validEmailFolders = ['queued', 'sent', 'failed', 'security'] as const;
|
||||
|
||||
export type TValidView = typeof validViews[number];
|
||||
export type TValidEmailFolder = typeof validEmailFolders[number];
|
||||
|
||||
class AppRouter {
|
||||
private router: InstanceType<typeof SmartRouter>;
|
||||
@@ -27,31 +25,10 @@ class AppRouter {
|
||||
}
|
||||
|
||||
private setupRoutes(): void {
|
||||
// Main views
|
||||
for (const view of validViews) {
|
||||
if (view === 'emails') {
|
||||
// Email root - default to queued
|
||||
this.router.on('/emails', async () => {
|
||||
this.updateViewState('emails');
|
||||
this.updateEmailFolder('queued');
|
||||
});
|
||||
|
||||
// Email with folder parameter
|
||||
this.router.on('/emails/:folder', async (routeInfo) => {
|
||||
const folder = routeInfo.params.folder as string;
|
||||
if (validEmailFolders.includes(folder as TValidEmailFolder)) {
|
||||
this.updateViewState('emails');
|
||||
this.updateEmailFolder(folder as TValidEmailFolder);
|
||||
} else {
|
||||
// Invalid folder, redirect to queued
|
||||
this.navigateTo('/emails/queued');
|
||||
}
|
||||
});
|
||||
} else {
|
||||
this.router.on(`/${view}`, async () => {
|
||||
this.updateViewState(view);
|
||||
});
|
||||
}
|
||||
this.router.on(`/${view}`, async () => {
|
||||
this.updateViewState(view);
|
||||
});
|
||||
}
|
||||
|
||||
// Root redirect
|
||||
@@ -61,60 +38,32 @@ class AppRouter {
|
||||
}
|
||||
|
||||
private setupStateSync(): void {
|
||||
// Sync URL when state changes programmatically (not from router)
|
||||
appstate.uiStatePart.state.subscribe((uiState) => {
|
||||
if (this.suppressStateUpdate) return;
|
||||
|
||||
const currentPath = window.location.pathname;
|
||||
const expectedPath = this.getExpectedPath(uiState.activeView);
|
||||
const expectedPath = `/${uiState.activeView}`;
|
||||
|
||||
// Only update URL if it doesn't match current state
|
||||
if (!currentPath.startsWith(expectedPath)) {
|
||||
if (currentPath !== expectedPath) {
|
||||
this.suppressStateUpdate = true;
|
||||
if (uiState.activeView === 'emails') {
|
||||
const emailState = appstate.emailOpsStatePart.getState();
|
||||
this.router.pushUrl(`/emails/${emailState.currentView}`);
|
||||
} else {
|
||||
this.router.pushUrl(`/${uiState.activeView}`);
|
||||
}
|
||||
this.router.pushUrl(expectedPath);
|
||||
this.suppressStateUpdate = false;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private getExpectedPath(view: string): string {
|
||||
if (view === 'emails') {
|
||||
return '/emails';
|
||||
}
|
||||
return `/${view}`;
|
||||
}
|
||||
|
||||
private handleInitialRoute(): void {
|
||||
const path = window.location.pathname;
|
||||
|
||||
if (!path || path === '/') {
|
||||
// Redirect root to overview
|
||||
this.router.pushUrl('/overview');
|
||||
} else {
|
||||
// Parse current path and update state
|
||||
const segments = path.split('/').filter(Boolean);
|
||||
const view = segments[0];
|
||||
|
||||
if (validViews.includes(view as TValidView)) {
|
||||
this.updateViewState(view as TValidView);
|
||||
|
||||
if (view === 'emails' && segments[1]) {
|
||||
const folder = segments[1];
|
||||
if (validEmailFolders.includes(folder as TValidEmailFolder)) {
|
||||
this.updateEmailFolder(folder as TValidEmailFolder);
|
||||
} else {
|
||||
this.updateEmailFolder('queued');
|
||||
}
|
||||
} else if (view === 'emails') {
|
||||
this.updateEmailFolder('queued');
|
||||
}
|
||||
} else {
|
||||
// Invalid view, redirect to overview
|
||||
this.router.pushUrl('/overview');
|
||||
}
|
||||
}
|
||||
@@ -132,18 +81,6 @@ class AppRouter {
|
||||
this.suppressStateUpdate = false;
|
||||
}
|
||||
|
||||
private updateEmailFolder(folder: TValidEmailFolder): void {
|
||||
this.suppressStateUpdate = true;
|
||||
const currentState = appstate.emailOpsStatePart.getState();
|
||||
if (currentState.currentView !== folder) {
|
||||
appstate.emailOpsStatePart.setState({
|
||||
...currentState,
|
||||
currentView: folder as appstate.IEmailOpsState['currentView'],
|
||||
});
|
||||
}
|
||||
this.suppressStateUpdate = false;
|
||||
}
|
||||
|
||||
public navigateTo(path: string): void {
|
||||
this.router.pushUrl(path);
|
||||
}
|
||||
@@ -156,22 +93,10 @@ class AppRouter {
|
||||
}
|
||||
}
|
||||
|
||||
public navigateToEmailFolder(folder: string): void {
|
||||
if (validEmailFolders.includes(folder as TValidEmailFolder)) {
|
||||
this.navigateTo(`/emails/${folder}`);
|
||||
} else {
|
||||
this.navigateTo('/emails/queued');
|
||||
}
|
||||
}
|
||||
|
||||
public getCurrentView(): string {
|
||||
return appstate.uiStatePart.getState().activeView;
|
||||
}
|
||||
|
||||
public getCurrentEmailFolder(): string {
|
||||
return appstate.emailOpsStatePart.getState().currentView;
|
||||
}
|
||||
|
||||
public destroy(): void {
|
||||
this.router.destroy();
|
||||
this.initialized = false;
|
||||
|
||||
Reference in New Issue
Block a user