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 * as sharedStyles 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, }) accessor currentIncidents: IIncidentDetails[] = []; @property({ type: Array, }) accessor pastIncidents: IIncidentDetails[] = []; @property({ type: Boolean, }) accessor whitelabel = false; @property({ type: Boolean, }) accessor loading = false; @property({ type: Number, }) accessor daysToShow = 90; @property({ type: Array, }) accessor subscribedIncidentIds: string[] = []; @property({ type: Object, state: true, }) private accessor expandedIncidents: Set = new Set(); @property({ type: Object, state: true, }) private accessor subscribedIncidents: Set = new Set(); constructor() { super(); } async connectedCallback() { await super.connectedCallback(); // Initialize subscribed incidents from the property if (this.subscribedIncidentIds.length > 0) { this.subscribedIncidents = new Set(this.subscribedIncidentIds); } } updated(changedProperties: Map) { super.updated(changedProperties); if (changedProperties.has('subscribedIncidentIds')) { this.subscribedIncidents = new Set(this.subscribedIncidentIds); } } public static styles = [ plugins.domtools.elementBasic.staticStyles, sharedStyles.commonStyles, css` :host { display: block; background: transparent; font-family: ${unsafeCSS(sharedStyles.fonts.base)}; color: ${sharedStyles.colors.text.primary}; } .container { max-width: 1200px; margin: 0 auto; padding: 0 ${unsafeCSS(sharedStyles.spacing.lg)} ${unsafeCSS(sharedStyles.spacing.lg)} ${unsafeCSS(sharedStyles.spacing.lg)}; } .noIncidentBox { background: ${sharedStyles.colors.background.card}; padding: ${unsafeCSS(sharedStyles.spacing.xl)}; margin-bottom: ${unsafeCSS(sharedStyles.spacing.lg)}; border-radius: ${unsafeCSS(sharedStyles.borderRadius.md)}; border: 1px solid ${sharedStyles.colors.border.default}; text-align: center; color: ${sharedStyles.colors.text.secondary}; box-shadow: ${unsafeCSS(sharedStyles.shadows.sm)}; } .incident-card { background: ${sharedStyles.colors.background.card}; border-radius: ${unsafeCSS(sharedStyles.borderRadius.md)}; margin-bottom: ${unsafeCSS(sharedStyles.spacing.lg)}; overflow: hidden; box-shadow: ${unsafeCSS(sharedStyles.shadows.sm)}; border: 1px solid ${sharedStyles.colors.border.default}; transition: all 0.2s ease; } .incident-card.expanded { box-shadow: ${unsafeCSS(sharedStyles.shadows.md)}; } .incident-header { padding: ${unsafeCSS(sharedStyles.spacing.lg)} ${unsafeCSS(sharedStyles.spacing.lg)}; border-left: 4px solid; display: flex; align-items: start; justify-content: space-between; gap: ${unsafeCSS(sharedStyles.spacing.md)}; cursor: pointer; transition: background-color 0.2s ease; } .incident-header:hover { background: ${cssManager.bdTheme('rgba(0, 0, 0, 0.02)', 'rgba(255, 255, 255, 0.02)')}; } .incident-header.critical { border-left-color: ${sharedStyles.colors.status.major}; } .incident-header.major { border-left-color: ${sharedStyles.colors.status.partial}; } .incident-header.minor { border-left-color: ${sharedStyles.colors.status.degraded}; } .incident-header.maintenance { border-left-color: ${sharedStyles.colors.status.maintenance}; } .incident-title { font-size: 18px; font-weight: 600; margin: 0; line-height: 1.3; } .incident-meta { display: flex; gap: ${unsafeCSS(sharedStyles.spacing.lg)}; margin-top: ${unsafeCSS(sharedStyles.spacing.sm)}; font-size: 13px; color: ${sharedStyles.colors.text.secondary}; flex-wrap: wrap; } .incident-status { display: inline-flex; align-items: center; gap: ${unsafeCSS(sharedStyles.spacing.xs)}; padding: ${unsafeCSS(sharedStyles.spacing.xs)} ${unsafeCSS(sharedStyles.spacing.md)}; border-radius: ${unsafeCSS(sharedStyles.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(sharedStyles.spacing.lg)} ${unsafeCSS(sharedStyles.spacing.lg)} ${unsafeCSS(sharedStyles.spacing.lg)}; } .incident-impact { margin: ${unsafeCSS(sharedStyles.spacing.md)} 0; padding: ${unsafeCSS(sharedStyles.spacing.md)}; background: ${sharedStyles.colors.background.secondary}; border-radius: ${unsafeCSS(sharedStyles.borderRadius.base)}; font-size: 14px; line-height: 1.6; } .affected-services { margin-top: ${unsafeCSS(sharedStyles.spacing.md)}; } .affected-services-title { font-size: 13px; font-weight: 600; margin-bottom: ${unsafeCSS(sharedStyles.spacing.sm)}; color: ${sharedStyles.colors.text.primary}; } .service-tag { display: inline-block; padding: ${unsafeCSS(sharedStyles.spacing.xs)} ${unsafeCSS(sharedStyles.spacing.sm)}; margin: 2px; background: ${sharedStyles.colors.background.muted}; border-radius: ${unsafeCSS(sharedStyles.borderRadius.sm)}; font-size: 12px; color: ${sharedStyles.colors.text.secondary}; } .incident-updates { margin-top: ${unsafeCSS(sharedStyles.spacing.lg)}; border-top: 1px solid ${sharedStyles.colors.border.default}; padding-top: ${unsafeCSS(sharedStyles.spacing.lg)}; } .update-item { position: relative; padding-left: ${unsafeCSS(sharedStyles.spacing.lg)}; margin-bottom: ${unsafeCSS(sharedStyles.spacing.md)}; } .update-item::before { content: ''; position: absolute; left: 0; top: 6px; width: 8px; height: 8px; border-radius: ${unsafeCSS(sharedStyles.borderRadius.full)}; background: ${sharedStyles.colors.border.muted}; } .update-time { font-size: 12px; color: ${sharedStyles.colors.text.secondary}; margin-bottom: ${unsafeCSS(sharedStyles.spacing.xs)}; font-family: ${unsafeCSS(sharedStyles.fonts.mono)}; } .update-message { font-size: 14px; line-height: 1.6; color: ${sharedStyles.colors.text.primary}; } .update-author { font-size: 12px; color: ${sharedStyles.colors.text.secondary}; margin-top: ${unsafeCSS(sharedStyles.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(sharedStyles.borderRadius.md)}; margin-bottom: ${unsafeCSS(sharedStyles.spacing.lg)}; } @keyframes loading { 0% { background-position: 200% 0; } 100% { background-position: -200% 0; } } .show-more { text-align: center; margin-top: ${unsafeCSS(sharedStyles.spacing.lg)}; } .show-more-button { display: inline-flex; align-items: center; justify-content: center; padding: ${unsafeCSS(sharedStyles.spacing.sm)} ${unsafeCSS(sharedStyles.spacing.lg)}; background: transparent; border: 1px solid ${sharedStyles.colors.border.default}; border-radius: ${unsafeCSS(sharedStyles.borderRadius.base)}; cursor: pointer; font-size: 14px; font-weight: 500; transition: all 0.2s ease; color: ${sharedStyles.colors.text.primary}; font-family: ${unsafeCSS(sharedStyles.fonts.base)}; } .show-more-button:hover { background: ${sharedStyles.colors.background.secondary}; border-color: ${sharedStyles.colors.border.muted}; transform: translateY(-1px); } .show-more-button:active { transform: translateY(0); } .incident-actions { display: flex; gap: ${unsafeCSS(sharedStyles.spacing.md)}; align-items: center; margin-top: ${unsafeCSS(sharedStyles.spacing.lg)}; padding-top: ${unsafeCSS(sharedStyles.spacing.lg)}; border-top: 1px solid ${sharedStyles.colors.border.default}; } .subscribe-button { display: inline-flex; align-items: center; gap: ${unsafeCSS(sharedStyles.spacing.xs)}; padding: ${unsafeCSS(sharedStyles.spacing.xs)} ${unsafeCSS(sharedStyles.spacing.md)}; background: transparent; border: 1px solid ${sharedStyles.colors.border.default}; border-radius: ${unsafeCSS(sharedStyles.borderRadius.base)}; cursor: pointer; font-size: 13px; font-weight: 400; transition: all 0.2s ease; color: ${sharedStyles.colors.text.primary}; font-family: ${unsafeCSS(sharedStyles.fonts.base)}; } .subscribe-button:hover { background: ${sharedStyles.colors.background.secondary}; border-color: ${sharedStyles.colors.border.muted}; } .subscribe-button.subscribed { background: ${cssManager.bdTheme('#f0fdf4', '#064e3b')}; border-color: ${cssManager.bdTheme('#86efac', '#047857')}; color: ${cssManager.bdTheme('#047857', '#86efac')}; } .subscribe-button.subscribed:hover { background: ${cssManager.bdTheme('#dcfce7', '#065f46')}; } .collapsed-hint { font-size: 12px; color: ${sharedStyles.colors.text.secondary}; text-align: center; margin-top: ${unsafeCSS(sharedStyles.spacing.md)}; opacity: 0.8; } @media (max-width: 640px) { .container { padding: 0 ${unsafeCSS(sharedStyles.spacing.md)} ${unsafeCSS(sharedStyles.spacing.md)} ${unsafeCSS(sharedStyles.spacing.md)}; } .incident-header { padding: ${unsafeCSS(sharedStyles.spacing.md)}; } .incident-meta { flex-direction: column; gap: ${unsafeCSS(sharedStyles.spacing.xs)}; } } `, ]; public render(): TemplateResult { return html`
Current Incidents ${this.loading ? html`
` : this.currentIncidents.length ? html` ${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.toggleIncident(incident.id)}>

${incident.title}

Started: ${this.formatDate(incident.startTime)} Duration: ${duration} ${incident.endTime ? html` Ended: ${this.formatDate(incident.endTime)} ` : ''}
${!this.expandedIncidents.has(incident.id) ? html`
${incident.impact ? html` ${incident.impact} ` : ''} ${incident.updates.length} update${incident.updates.length !== 1 ? 's' : ''}
` : ''}
${this.getStatusIcon(latestUpdate.status)} ${latestUpdate.status.replace(/_/g, ' ')}
${this.expandedIncidents.has(incident.id) ? html`
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}
` : ''}
${isCurrent ? html` Get notified when this incident is updated or resolved ` : ''}
` : ''}
`; } private renderUpdate(update: any): TemplateResult { return html`
${this.formatDate(update.timestamp)}
${update.message}
${update.author ? html`
— ${update.author}
` : ''}
`; } private getStatusIcon(status: string): TemplateResult { return html``; } 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 toggleIncident(incidentId: string) { const newExpanded = new Set(this.expandedIncidents); if (newExpanded.has(incidentId)) { newExpanded.delete(incidentId); } else { newExpanded.add(incidentId); } this.expandedIncidents = newExpanded; } 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'); } private isSubscribedToIncident(incidentId: string): boolean { return this.subscribedIncidents.has(incidentId); } private handleIncidentSubscribe(incident: IIncidentDetails) { const newSubscribed = new Set(this.subscribedIncidents); if (newSubscribed.has(incident.id)) { newSubscribed.delete(incident.id); this.dispatchEvent(new CustomEvent('incidentUnsubscribe', { detail: { incident, incidentId: incident.id }, bubbles: true, composed: true })); } else { newSubscribed.add(incident.id); this.dispatchEvent(new CustomEvent('incidentSubscribe', { detail: { incident, incidentId: incident.id, incidentTitle: incident.title, affectedServices: incident.affectedServices }, bubbles: true, composed: true })); } this.subscribedIncidents = newSubscribed; } public dispatchReportNewIncident() { this.dispatchEvent(new CustomEvent('reportNewIncident', { bubbles: true, composed: true })); } public dispatchStatusSubscribe() { this.dispatchEvent(new CustomEvent('statusSubscribe', { bubbles: true, composed: true })); } }