import * as plugins from '../plugins.js'; import { DeesElement, property, html, customElement, type TemplateResult, css, cssManager, unsafeCSS, } from '@design.estate/dees-element'; import type { IIncidentDetails } from '../interfaces/index.js'; import { fonts, colors, shadows, borderRadius, spacing, commonStyles } from '../styles/shared.styles.js'; import './internal/uplinternal-miniheading.js'; import { demoFunc } from './upl-statuspage-incidents.demo.js'; declare global { interface HTMLElementTagNameMap { 'upl-statuspage-incidents': UplStatuspageIncidents; } } @customElement('upl-statuspage-incidents') export class UplStatuspageIncidents extends DeesElement { // STATIC public static demo = demoFunc; // INSTANCE @property({ type: Array, }) public currentIncidents: IIncidentDetails[] = []; @property({ type: Array, }) public pastIncidents: IIncidentDetails[] = []; @property({ type: Boolean, }) public whitelabel = false; @property({ type: Boolean, }) public loading = false; @property({ type: Number, }) public daysToShow = 90; constructor() { super(); } public static styles = [ plugins.domtools.elementBasic.staticStyles, commonStyles, css` :host { display: block; background: transparent; font-family: ${unsafeCSS(fonts.base)}; color: ${colors.text.primary}; } .container { max-width: 1200px; margin: 0 auto; padding: 0 ${unsafeCSS(spacing.lg)} ${unsafeCSS(spacing.lg)} ${unsafeCSS(spacing.lg)}; } .noIncidentBox { background: ${colors.background.card}; padding: ${unsafeCSS(spacing.xl)}; margin-bottom: ${unsafeCSS(spacing.lg)}; border-radius: ${unsafeCSS(borderRadius.md)}; border: 1px solid ${colors.border.default}; text-align: center; color: ${colors.text.secondary}; box-shadow: ${unsafeCSS(shadows.sm)}; } .incident-card { background: ${colors.background.card}; border-radius: ${unsafeCSS(borderRadius.md)}; margin-bottom: ${unsafeCSS(spacing.lg)}; overflow: hidden; box-shadow: ${unsafeCSS(shadows.sm)}; border: 1px solid ${colors.border.default}; transition: all 0.2s ease; cursor: pointer; } .incident-card:hover { box-shadow: ${unsafeCSS(shadows.md)}; transform: translateY(-2px); } .incident-header { padding: ${unsafeCSS(spacing.lg)} ${unsafeCSS(spacing.lg)}; border-left: 4px solid; display: flex; align-items: start; justify-content: space-between; gap: ${unsafeCSS(spacing.md)}; } .incident-header.critical { border-left-color: ${colors.status.major}; } .incident-header.major { border-left-color: ${colors.status.partial}; } .incident-header.minor { border-left-color: ${colors.status.degraded}; } .incident-header.maintenance { border-left-color: ${colors.status.maintenance}; } .incident-title { font-size: 18px; font-weight: 600; margin: 0; line-height: 1.3; } .incident-meta { display: flex; gap: ${unsafeCSS(spacing.lg)}; margin-top: ${unsafeCSS(spacing.sm)}; font-size: 13px; color: ${colors.text.secondary}; flex-wrap: wrap; } .incident-status { display: inline-flex; align-items: center; gap: ${unsafeCSS(spacing.xs)}; padding: ${unsafeCSS(spacing.xs)} ${unsafeCSS(spacing.md)}; border-radius: ${unsafeCSS(borderRadius.full)}; font-size: 12px; font-weight: 600; text-transform: uppercase; letter-spacing: 0.02em; flex-shrink: 0; } .incident-status.investigating { background: ${cssManager.bdTheme('#fef3c7', '#78350f')}; color: ${cssManager.bdTheme('#92400e', '#fbbf24')}; } .incident-status.identified { background: ${cssManager.bdTheme('#e9d5ff', '#581c87')}; color: ${cssManager.bdTheme('#6b21a8', '#d8b4fe')}; } .incident-status.monitoring { background: ${cssManager.bdTheme('#dbeafe', '#1e3a8a')}; color: ${cssManager.bdTheme('#1e40af', '#93c5fd')}; } .incident-status.resolved { background: ${cssManager.bdTheme('#d1fae5', '#064e3b')}; color: ${cssManager.bdTheme('#047857', '#6ee7b7')}; } .incident-status.postmortem { background: ${cssManager.bdTheme('#e5e7eb', '#374151')}; color: ${cssManager.bdTheme('#4b5563', '#d1d5db')}; } .incident-body { padding: 0 ${unsafeCSS(spacing.lg)} ${unsafeCSS(spacing.lg)} ${unsafeCSS(spacing.lg)}; } .incident-impact { margin: ${unsafeCSS(spacing.md)} 0; padding: ${unsafeCSS(spacing.md)}; background: ${colors.background.secondary}; border-radius: ${unsafeCSS(borderRadius.base)}; font-size: 14px; line-height: 1.6; } .affected-services { margin-top: ${unsafeCSS(spacing.md)}; } .affected-services-title { font-size: 13px; font-weight: 600; margin-bottom: ${unsafeCSS(spacing.sm)}; color: ${colors.text.primary}; } .service-tag { display: inline-block; padding: ${unsafeCSS(spacing.xs)} ${unsafeCSS(spacing.sm)}; margin: 2px; background: ${colors.background.muted}; border-radius: ${unsafeCSS(borderRadius.sm)}; font-size: 12px; color: ${colors.text.secondary}; } .incident-updates { margin-top: ${unsafeCSS(spacing.lg)}; border-top: 1px solid ${colors.border.default}; padding-top: ${unsafeCSS(spacing.lg)}; } .update-item { position: relative; padding-left: ${unsafeCSS(spacing.lg)}; margin-bottom: ${unsafeCSS(spacing.md)}; } .update-item::before { content: ''; position: absolute; left: 0; top: 6px; width: 8px; height: 8px; border-radius: ${unsafeCSS(borderRadius.full)}; background: ${colors.border.muted}; } .update-time { font-size: 12px; color: ${colors.text.secondary}; margin-bottom: ${unsafeCSS(spacing.xs)}; font-family: ${unsafeCSS(fonts.mono)}; } .update-message { font-size: 14px; line-height: 1.6; color: ${colors.text.primary}; } .update-author { font-size: 12px; color: ${colors.text.secondary}; margin-top: ${unsafeCSS(spacing.xs)}; font-style: italic; } .loading-skeleton { height: 140px; background: ${cssManager.bdTheme( 'linear-gradient(90deg, #f3f4f6 25%, #e5e7eb 50%, #f3f4f6 75%)', 'linear-gradient(90deg, #1f1f1f 25%, #262626 50%, #1f1f1f 75%)' )}; background-size: 200% 100%; animation: loading 1.5s infinite; border-radius: ${unsafeCSS(borderRadius.md)}; margin-bottom: ${unsafeCSS(spacing.lg)}; } @keyframes loading { 0% { background-position: 200% 0; } 100% { background-position: -200% 0; } } .show-more { text-align: center; margin-top: ${unsafeCSS(spacing.lg)}; } .show-more-button { display: inline-flex; align-items: center; justify-content: center; padding: ${unsafeCSS(spacing.sm)} ${unsafeCSS(spacing.lg)}; background: transparent; border: 1px solid ${colors.border.default}; border-radius: ${unsafeCSS(borderRadius.base)}; cursor: pointer; font-size: 14px; font-weight: 500; transition: all 0.2s ease; color: ${colors.text.primary}; font-family: ${unsafeCSS(fonts.base)}; } .show-more-button:hover { background: ${colors.background.secondary}; border-color: ${colors.border.muted}; transform: translateY(-1px); } .show-more-button:active { transform: translateY(0); } @media (max-width: 640px) { .container { padding: 0 ${unsafeCSS(spacing.md)} ${unsafeCSS(spacing.md)} ${unsafeCSS(spacing.md)}; } .incident-header { flex-direction: column; align-items: start; gap: ${unsafeCSS(spacing.sm)}; } .incident-meta { flex-direction: column; gap: ${unsafeCSS(spacing.xs)}; } } `, ]; public render(): TemplateResult { return html`
Current Incidents ${this.loading ? html`
` : this.currentIncidents.length ? this.currentIncidents.map(incident => this.renderIncident(incident, true)) : html`
No incidents ongoing.
` } Past Incidents ${this.loading ? html`
` : this.pastIncidents.length ? this.pastIncidents.slice(0, 5).map(incident => this.renderIncident(incident, false)) : html`
No past incidents in the last ${this.daysToShow} days.
` } ${this.pastIncidents.length > 5 && !this.loading ? html`
` : ''}
`; } private renderIncident(incident: IIncidentDetails, isCurrent: boolean): TemplateResult { const latestUpdate = incident.updates[incident.updates.length - 1]; const duration = incident.endTime ? this.formatDuration(incident.endTime - incident.startTime) : this.formatDuration(Date.now() - incident.startTime); return html`
this.handleIncidentClick(incident)}>

${incident.title}

Started: ${this.formatDate(incident.startTime)} Duration: ${duration} ${incident.endTime ? html` Ended: ${this.formatDate(incident.endTime)} ` : ''}
${this.getStatusIcon(latestUpdate.status)} ${latestUpdate.status.replace(/_/g, ' ')}
Impact: ${incident.impact}
${incident.affectedServices.length > 0 ? html`
Affected Services:
${incident.affectedServices.map(service => html` ${service} `)}
` : ''} ${incident.updates.length > 0 ? html`

Updates

${incident.updates.slice(-3).reverse().map(update => this.renderUpdate(update))}
` : ''} ${incident.rootCause && isCurrent === false ? html`
Root Cause: ${incident.rootCause}
` : ''} ${incident.resolution && isCurrent === false ? html`
Resolution: ${incident.resolution}
` : ''}
`; } private renderUpdate(update: any): TemplateResult { return html`
${this.formatDate(update.timestamp)}
${update.message}
${update.author ? html`
— ${update.author}
` : ''}
`; } private getStatusIcon(status: string): string { const icons: Record = { investigating: '🔍', identified: '🎯', monitoring: '👁️', resolved: '✅', postmortem: '📋' }; return icons[status] || '•'; } private formatDate(timestamp: number): string { const date = new Date(timestamp); const now = Date.now(); const diff = now - timestamp; // Less than 1 hour ago if (diff < 60 * 60 * 1000) { const minutes = Math.floor(diff / (60 * 1000)); return `${minutes} minute${minutes !== 1 ? 's' : ''} ago`; } // Less than 24 hours ago if (diff < 24 * 60 * 60 * 1000) { const hours = Math.floor(diff / (60 * 60 * 1000)); return `${hours} hour${hours !== 1 ? 's' : ''} ago`; } // Less than 7 days ago if (diff < 7 * 24 * 60 * 60 * 1000) { const days = Math.floor(diff / (24 * 60 * 60 * 1000)); return `${days} day${days !== 1 ? 's' : ''} ago`; } // Default to full date return date.toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: date.getFullYear() !== new Date().getFullYear() ? 'numeric' : undefined }); } private formatDuration(milliseconds: number): string { const minutes = Math.floor(milliseconds / (60 * 1000)); const hours = Math.floor(minutes / 60); const days = Math.floor(hours / 24); if (days > 0) { return `${days}d ${hours % 24}h`; } else if (hours > 0) { return `${hours}h ${minutes % 60}m`; } else { return `${minutes}m`; } } private handleIncidentClick(incident: IIncidentDetails) { this.dispatchEvent(new CustomEvent('incidentClick', { detail: { incident }, bubbles: true, composed: true })); } private handleShowMore() { // This would typically load more incidents or navigate to a full list console.log('Show more incidents'); } public dispatchReportNewIncident() { this.dispatchEvent(new CustomEvent('reportNewIncident', { bubbles: true, composed: true })); } public dispatchStatusSubscribe() { this.dispatchEvent(new CustomEvent('statusSubscribe', { bubbles: true, composed: true })); } }