import * as plugins from '../../plugins.js'; import { DeesElement, property, html, customElement, type TemplateResult, css, cssManager, unsafeCSS, state, } from '@design.estate/dees-element'; import * as sharedStyles from '../../styles/shared.styles.js'; import type { IServiceStatus, IIncidentDetails, IOverallStatus } from '../../interfaces/index.js'; import type { IStatsTile } from '@design.estate/dees-catalog'; import { demoFunc } from './upladmin-dashboard.demo.js'; declare global { interface HTMLElementTagNameMap { 'upladmin-dashboard': UpladminDashboard; } } type TStatusType = 'operational' | 'degraded' | 'partial_outage' | 'major_outage' | 'maintenance'; @customElement('upladmin-dashboard') export class UpladminDashboard extends DeesElement { public static demo = demoFunc; @property({ type: Array }) accessor monitors: IServiceStatus[] = []; @property({ type: Array }) accessor incidents: IIncidentDetails[] = []; @property({ type: Object }) accessor overallStatus: IOverallStatus | null = null; @property({ type: Boolean }) accessor loading: boolean = false; public static styles = [ plugins.domtools.elementBasic.staticStyles, sharedStyles.commonStyles, css` :host { display: block; font-family: ${unsafeCSS(sharedStyles.fonts.base)}; } .dashboard { display: grid; gap: ${unsafeCSS(sharedStyles.spacing.lg)}; } /* Overall Status Banner */ .status-banner { display: flex; align-items: center; gap: ${unsafeCSS(sharedStyles.spacing.md)}; padding: ${unsafeCSS(sharedStyles.spacing.lg)}; border-radius: ${unsafeCSS(sharedStyles.borderRadius.lg)}; border: 1px solid; } .status-banner.operational { background: ${cssManager.bdTheme('rgba(34, 197, 94, 0.1)', 'rgba(34, 197, 94, 0.15)')}; border-color: ${sharedStyles.colors.status.operational}; } .status-banner.degraded { background: ${cssManager.bdTheme('rgba(234, 179, 8, 0.1)', 'rgba(234, 179, 8, 0.15)')}; border-color: ${sharedStyles.colors.status.degraded}; } .status-banner.partial_outage { background: ${cssManager.bdTheme('rgba(249, 115, 22, 0.1)', 'rgba(249, 115, 22, 0.15)')}; border-color: ${sharedStyles.colors.status.partialOutage}; } .status-banner.major_outage { background: ${cssManager.bdTheme('rgba(239, 68, 68, 0.1)', 'rgba(239, 68, 68, 0.15)')}; border-color: ${sharedStyles.colors.status.majorOutage}; } .status-banner.maintenance { background: ${cssManager.bdTheme('rgba(59, 130, 246, 0.1)', 'rgba(59, 130, 246, 0.15)')}; border-color: ${sharedStyles.colors.status.maintenance}; } .status-indicator { width: 48px; height: 48px; border-radius: 50%; display: flex; align-items: center; justify-content: center; flex-shrink: 0; color: white; } .status-indicator dees-icon { --icon-size: 24px; } .status-indicator.operational { background: ${sharedStyles.colors.status.operational}; } .status-indicator.degraded { background: ${sharedStyles.colors.status.degraded}; } .status-indicator.partial_outage { background: ${sharedStyles.colors.status.partialOutage}; } .status-indicator.major_outage { background: ${sharedStyles.colors.status.majorOutage}; } .status-indicator.maintenance { background: ${sharedStyles.colors.status.maintenance}; } .status-content { flex: 1; } .status-title { font-size: 18px; font-weight: 600; color: ${sharedStyles.colors.text.primary}; margin-bottom: 4px; } .status-message { font-size: 14px; color: ${sharedStyles.colors.text.secondary}; } .status-meta { font-size: 12px; color: ${sharedStyles.colors.text.muted}; margin-top: 4px; } /* Stats Grid Container */ .stats-container { margin: 0; } dees-statsgrid { --tile-padding: 20px; --value-font-size: 28px; } /* Content Grid */ .content-grid { display: grid; grid-template-columns: 1fr 1fr; gap: ${unsafeCSS(sharedStyles.spacing.lg)}; } @media (max-width: 900px) { .content-grid { grid-template-columns: 1fr; } } /* Section Card */ .section-card { background: ${sharedStyles.colors.background.secondary}; border: 1px solid ${sharedStyles.colors.border.default}; border-radius: ${unsafeCSS(sharedStyles.borderRadius.lg)}; overflow: hidden; } .section-header { display: flex; align-items: center; justify-content: space-between; padding: ${unsafeCSS(sharedStyles.spacing.md)} ${unsafeCSS(sharedStyles.spacing.lg)}; border-bottom: 1px solid ${sharedStyles.colors.border.default}; } .section-title { font-size: 15px; font-weight: 600; color: ${sharedStyles.colors.text.primary}; } .section-action { display: inline-flex; align-items: center; gap: 4px; font-size: 13px; font-weight: 500; color: ${sharedStyles.colors.accent.primary}; background: none; border: none; cursor: pointer; transition: opacity ${unsafeCSS(sharedStyles.durations.fast)} ${unsafeCSS(sharedStyles.easings.default)}; } .section-action:hover { opacity: 0.8; } .section-action dees-icon { --icon-size: 14px; } .section-body { padding: ${unsafeCSS(sharedStyles.spacing.md)}; } /* Status By Category */ .category-list { display: flex; flex-direction: column; gap: ${unsafeCSS(sharedStyles.spacing.sm)}; } .category-item { display: flex; align-items: center; gap: ${unsafeCSS(sharedStyles.spacing.md)}; padding: ${unsafeCSS(sharedStyles.spacing.sm)} ${unsafeCSS(sharedStyles.spacing.md)}; background: ${sharedStyles.colors.background.primary}; border-radius: ${unsafeCSS(sharedStyles.borderRadius.base)}; } .category-name { flex: 1; font-size: 14px; font-weight: 500; color: ${sharedStyles.colors.text.primary}; } .category-stats { display: flex; align-items: center; gap: ${unsafeCSS(sharedStyles.spacing.sm)}; } .category-count { font-size: 13px; color: ${sharedStyles.colors.text.muted}; } .category-bar { width: 80px; height: 6px; background: ${sharedStyles.colors.background.muted}; border-radius: 3px; overflow: hidden; } .category-bar-fill { height: 100%; background: ${sharedStyles.colors.status.operational}; border-radius: 3px; transition: width ${unsafeCSS(sharedStyles.durations.normal)} ${unsafeCSS(sharedStyles.easings.default)}; } /* Active Incidents */ .incident-list { display: flex; flex-direction: column; gap: ${unsafeCSS(sharedStyles.spacing.sm)}; } .incident-item { display: flex; align-items: flex-start; gap: ${unsafeCSS(sharedStyles.spacing.md)}; padding: ${unsafeCSS(sharedStyles.spacing.md)}; background: ${sharedStyles.colors.background.primary}; border-radius: ${unsafeCSS(sharedStyles.borderRadius.base)}; border-left: 3px solid; cursor: pointer; transition: background ${unsafeCSS(sharedStyles.durations.fast)} ${unsafeCSS(sharedStyles.easings.default)}; } .incident-item:hover { background: ${sharedStyles.colors.background.muted}; } .incident-item.critical { border-left-color: ${sharedStyles.colors.status.majorOutage}; } .incident-item.major { border-left-color: ${sharedStyles.colors.status.partialOutage}; } .incident-item.minor { border-left-color: ${sharedStyles.colors.status.degraded}; } .incident-item.maintenance { border-left-color: ${sharedStyles.colors.status.maintenance}; } .incident-content { flex: 1; min-width: 0; } .incident-title { font-size: 14px; font-weight: 500; color: ${sharedStyles.colors.text.primary}; margin-bottom: 4px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } .incident-meta { display: flex; align-items: center; gap: ${unsafeCSS(sharedStyles.spacing.sm)}; font-size: 12px; color: ${sharedStyles.colors.text.muted}; } .incident-status { display: inline-flex; align-items: center; gap: 4px; padding: 2px 8px; font-size: 10px; font-weight: 600; text-transform: uppercase; border-radius: 9999px; background: ${sharedStyles.colors.background.muted}; color: ${sharedStyles.colors.text.secondary}; } /* Quick Actions */ .quick-actions { display: grid; grid-template-columns: repeat(2, 1fr); gap: ${unsafeCSS(sharedStyles.spacing.sm)}; } .quick-action { display: flex; flex-direction: column; align-items: center; gap: 8px; padding: ${unsafeCSS(sharedStyles.spacing.lg)}; background: ${sharedStyles.colors.background.primary}; border: 1px solid ${sharedStyles.colors.border.default}; border-radius: ${unsafeCSS(sharedStyles.borderRadius.base)}; cursor: pointer; transition: all ${unsafeCSS(sharedStyles.durations.fast)} ${unsafeCSS(sharedStyles.easings.default)}; color: ${sharedStyles.colors.text.secondary}; } .quick-action:hover { background: ${sharedStyles.colors.background.muted}; border-color: ${sharedStyles.colors.border.strong}; color: ${sharedStyles.colors.text.primary}; } .quick-action-icon { display: flex; align-items: center; justify-content: center; } .quick-action-icon dees-icon { --icon-size: 24px; } .quick-action-label { font-size: 13px; font-weight: 500; color: ${sharedStyles.colors.text.primary}; text-align: center; } /* Empty State */ .empty-state { display: flex; flex-direction: column; align-items: center; justify-content: center; padding: ${unsafeCSS(sharedStyles.spacing.xl)}; text-align: center; color: ${sharedStyles.colors.text.muted}; } .empty-icon { margin-bottom: ${unsafeCSS(sharedStyles.spacing.sm)}; opacity: 0.5; } .empty-icon dees-icon { --icon-size: 32px; } .empty-text { font-size: 14px; color: ${sharedStyles.colors.text.muted}; } ` ]; private get statsTiles(): IStatsTile[] { const activeIncidents = this.incidents.filter(i => !['resolved', 'postmortem'].includes(i.status)); const operationalCount = this.monitors.filter(m => m.currentStatus === 'operational').length; const degradedCount = this.monitors.filter(m => m.currentStatus === 'degraded').length; const outageCount = this.monitors.filter(m => ['partial_outage', 'major_outage'].includes(m.currentStatus)).length; const avgUptime = this.monitors.length > 0 ? this.monitors.reduce((sum, m) => sum + m.uptime30d, 0) / this.monitors.length : 100; const uptimeColor = avgUptime >= 99.9 ? sharedStyles.colors.status.operational.cssText : avgUptime >= 99 ? sharedStyles.colors.status.degraded.cssText : sharedStyles.colors.status.majorOutage.cssText; return [ { id: 'uptime', title: 'Average Uptime (30d)', value: avgUptime, unit: '%', type: 'percentage', color: uptimeColor, icon: 'lucide:barChart3', description: avgUptime >= 99.9 ? 'Excellent' : avgUptime >= 99 ? 'Good' : 'Needs attention', }, { id: 'operational', title: 'Operational Services', value: operationalCount, type: 'number', icon: 'lucide:checkCircle', color: sharedStyles.colors.status.operational.cssText, }, { id: 'issues', title: 'Services with Issues', value: degradedCount + outageCount, type: 'number', icon: 'lucide:alertTriangle', color: (degradedCount + outageCount) > 0 ? sharedStyles.colors.status.degraded.cssText : undefined, }, { id: 'incidents', title: 'Active Incidents', value: activeIncidents.length, type: 'number', icon: 'lucide:alertCircle', color: activeIncidents.length > 0 ? sharedStyles.colors.status.majorOutage.cssText : undefined, }, ]; } public render(): TemplateResult { const activeIncidents = this.incidents.filter(i => !['resolved', 'postmortem'].includes(i.status)); return html`
${this.renderStatusBanner()}
Active Incidents
${activeIncidents.length > 0 ? html`
${activeIncidents.slice(0, 5).map(incident => this.renderIncidentItem(incident))}
` : html`
No active incidents
`}
Status by Category
${this.renderCategoryStatus()}
Quick Actions
`; } private renderStatusBanner(): TemplateResult { const status = this.overallStatus || this.calculateOverallStatus(); const statusIcons: Record = { operational: 'lucide:check', degraded: 'lucide:alertTriangle', partial_outage: 'lucide:zap', major_outage: 'lucide:x', maintenance: 'lucide:wrench', }; const statusTitles: Record = { operational: 'All Systems Operational', degraded: 'Degraded Performance', partial_outage: 'Partial System Outage', major_outage: 'Major System Outage', maintenance: 'Scheduled Maintenance', }; return html`
${statusTitles[status.status]}
${status.message}
Last updated: ${new Date(status.lastUpdated).toLocaleString()}
`; } private renderIncidentItem(incident: IIncidentDetails): TemplateResult { const formatTime = (timestamp: number) => { const now = Date.now(); const diff = now - timestamp; const hours = Math.floor(diff / (1000 * 60 * 60)); if (hours < 1) return `${Math.floor(diff / (1000 * 60))}m ago`; if (hours < 24) return `${hours}h ago`; return `${Math.floor(hours / 24)}d ago`; }; return html`
${incident.title}
${incident.status} ${formatTime(incident.startTime)} ${incident.affectedServices.length} services
`; } private renderCategoryStatus(): TemplateResult { const categories = [...new Set(this.monitors.map(m => m.category || 'Uncategorized'))]; if (categories.length === 0) { return html`
No monitors configured
`; } return html`
${categories.map(category => { const categoryMonitors = this.monitors.filter(m => (m.category || 'Uncategorized') === category); const operational = categoryMonitors.filter(m => m.currentStatus === 'operational').length; const percentage = (operational / categoryMonitors.length) * 100; return html`
${category}
${operational}/${categoryMonitors.length}
`; })}
`; } private calculateOverallStatus(): IOverallStatus { const hasOutage = this.monitors.some(m => ['partial_outage', 'major_outage'].includes(m.currentStatus)); const hasDegraded = this.monitors.some(m => m.currentStatus === 'degraded'); const hasMaintenance = this.monitors.some(m => m.currentStatus === 'maintenance'); const affectedCount = this.monitors.filter(m => m.currentStatus !== 'operational').length; let status: TStatusType = 'operational'; let message = 'All systems are operating normally.'; if (hasOutage) { status = this.monitors.some(m => m.currentStatus === 'major_outage') ? 'major_outage' : 'partial_outage'; message = `${affectedCount} services are experiencing issues.`; } else if (hasDegraded) { status = 'degraded'; message = `${affectedCount} services are experiencing degraded performance.`; } else if (hasMaintenance) { status = 'maintenance'; message = `${affectedCount} services are under maintenance.`; } return { status, message, lastUpdated: Date.now(), affectedServices: affectedCount, totalServices: this.monitors.length, }; } private handleViewAllIncidents() { this.dispatchEvent(new CustomEvent('navigateIncidents', { bubbles: true, composed: true })); } private handleViewAllMonitors() { this.dispatchEvent(new CustomEvent('navigateMonitors', { bubbles: true, composed: true })); } private handleIncidentClick(incident: IIncidentDetails) { this.dispatchEvent(new CustomEvent('incidentSelect', { detail: { incident }, bubbles: true, composed: true })); } private handleNewIncident() { this.dispatchEvent(new CustomEvent('createIncident', { bubbles: true, composed: true })); } private handleNewMonitor() { this.dispatchEvent(new CustomEvent('createMonitor', { bubbles: true, composed: true })); } private handleScheduleMaintenance() { this.dispatchEvent(new CustomEvent('scheduleMaintenance', { bubbles: true, composed: true })); } private handleViewConfig() { this.dispatchEvent(new CustomEvent('navigateConfig', { bubbles: true, composed: true })); } }