initial
This commit is contained in:
1
ts_web/elements/upladmin-incident-list/index.ts
Normal file
1
ts_web/elements/upladmin-incident-list/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from './upladmin-incident-list.js';
|
||||
@@ -0,0 +1,94 @@
|
||||
import { html, css, cssManager } from '@design.estate/dees-element';
|
||||
import type { IIncidentDetails } from '../../interfaces/index.js';
|
||||
import './upladmin-incident-list.js';
|
||||
|
||||
export const demoFunc = () => html`
|
||||
<style>
|
||||
${css`
|
||||
.demo-container {
|
||||
padding: 24px;
|
||||
background: ${cssManager.bdTheme('#fafafa', '#0a0a0a')};
|
||||
min-height: 100vh;
|
||||
}
|
||||
`}
|
||||
</style>
|
||||
|
||||
<div class="demo-container">
|
||||
<upladmin-incident-list
|
||||
.incidents=${[
|
||||
{
|
||||
id: 'inc-1',
|
||||
title: 'CDN Performance Degradation',
|
||||
status: 'monitoring',
|
||||
severity: 'minor',
|
||||
affectedServices: ['cdn'],
|
||||
startTime: Date.now() - 4 * 60 * 60 * 1000,
|
||||
impact: 'Some users may experience slower page loads for static assets.',
|
||||
rootCause: 'Network congestion at edge locations',
|
||||
updates: [
|
||||
{ id: 'u1', timestamp: Date.now() - 1 * 60 * 60 * 1000, status: 'monitoring', message: 'Fix deployed, monitoring results.', author: 'Infrastructure Team' },
|
||||
{ id: 'u2', timestamp: Date.now() - 2 * 60 * 60 * 1000, status: 'identified', message: 'Root cause identified as network congestion at edge nodes.', author: 'Infrastructure Team' },
|
||||
{ id: 'u3', timestamp: Date.now() - 4 * 60 * 60 * 1000, status: 'investigating', message: 'We are investigating reports of slow content delivery.', author: 'Support Team' },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'inc-2',
|
||||
title: 'Search Cluster Partial Failure',
|
||||
status: 'investigating',
|
||||
severity: 'major',
|
||||
affectedServices: ['search', 'api'],
|
||||
startTime: Date.now() - 45 * 60 * 1000,
|
||||
impact: 'Search functionality may return incomplete results. API responses may be delayed.',
|
||||
updates: [
|
||||
{ id: 'u4', timestamp: Date.now() - 45 * 60 * 1000, status: 'investigating', message: 'We are investigating issues with the search cluster. Some nodes are not responding.', author: 'Platform Team' },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'inc-3',
|
||||
title: 'Scheduled Database Maintenance',
|
||||
status: 'investigating',
|
||||
severity: 'maintenance',
|
||||
affectedServices: ['db', 'api', 'web'],
|
||||
startTime: Date.now() - 15 * 60 * 1000,
|
||||
impact: 'Brief interruptions may occur during the maintenance window.',
|
||||
updates: [
|
||||
{ id: 'u5', timestamp: Date.now() - 15 * 60 * 1000, status: 'investigating', message: 'Starting scheduled database maintenance. Expected duration: 2 hours.', author: 'DBA Team' },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'inc-4',
|
||||
title: 'Authentication Service Outage',
|
||||
status: 'resolved',
|
||||
severity: 'critical',
|
||||
affectedServices: ['auth', 'api', 'web'],
|
||||
startTime: Date.now() - 24 * 60 * 60 * 1000,
|
||||
endTime: Date.now() - 22 * 60 * 60 * 1000,
|
||||
impact: 'Users were unable to log in or access authenticated features.',
|
||||
rootCause: 'Certificate expiration on the identity provider.',
|
||||
resolution: 'Renewed certificates and implemented automated monitoring for future expirations.',
|
||||
updates: [
|
||||
{ id: 'u6', timestamp: Date.now() - 22 * 60 * 60 * 1000, status: 'resolved', message: 'Issue has been fully resolved. All authentication services are operational.', author: 'Security Team' },
|
||||
{ id: 'u7', timestamp: Date.now() - 23 * 60 * 60 * 1000, status: 'identified', message: 'Root cause identified: expired SSL certificate on identity provider.', author: 'Security Team' },
|
||||
{ id: 'u8', timestamp: Date.now() - 24 * 60 * 60 * 1000, status: 'investigating', message: 'We are aware of authentication issues and are investigating.', author: 'On-call Engineer' },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'inc-5',
|
||||
title: 'Payment Processing Delays',
|
||||
status: 'postmortem',
|
||||
severity: 'major',
|
||||
affectedServices: ['payment', 'api'],
|
||||
startTime: Date.now() - 72 * 60 * 60 * 1000,
|
||||
endTime: Date.now() - 70 * 60 * 60 * 1000,
|
||||
impact: 'Payment transactions were delayed by up to 5 minutes.',
|
||||
rootCause: 'Third-party payment provider experienced capacity issues.',
|
||||
resolution: 'Provider resolved their capacity issues. Implemented fallback payment routing.',
|
||||
updates: [
|
||||
{ id: 'u9', timestamp: Date.now() - 48 * 60 * 60 * 1000, status: 'postmortem', message: 'Postmortem complete. Implementing additional redundancy measures.', author: 'Engineering Lead' },
|
||||
{ id: 'u10', timestamp: Date.now() - 70 * 60 * 60 * 1000, status: 'resolved', message: 'Payment processing has returned to normal.', author: 'Payments Team' },
|
||||
],
|
||||
},
|
||||
] as IIncidentDetails[]}
|
||||
></upladmin-incident-list>
|
||||
</div>
|
||||
`;
|
||||
722
ts_web/elements/upladmin-incident-list/upladmin-incident-list.ts
Normal file
722
ts_web/elements/upladmin-incident-list/upladmin-incident-list.ts
Normal file
@@ -0,0 +1,722 @@
|
||||
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 { IIncidentDetails } from '../../interfaces/index.js';
|
||||
import { demoFunc } from './upladmin-incident-list.demo.js';
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
'upladmin-incident-list': UpladminIncidentList;
|
||||
}
|
||||
}
|
||||
|
||||
type TSeverity = 'critical' | 'major' | 'minor' | 'maintenance';
|
||||
type TIncidentStatus = 'investigating' | 'identified' | 'monitoring' | 'resolved' | 'postmortem';
|
||||
type TTabFilter = 'current' | 'past' | 'all';
|
||||
|
||||
@customElement('upladmin-incident-list')
|
||||
export class UpladminIncidentList extends DeesElement {
|
||||
public static demo = demoFunc;
|
||||
|
||||
@property({ type: Array })
|
||||
accessor incidents: IIncidentDetails[] = [];
|
||||
|
||||
@property({ type: Boolean })
|
||||
accessor loading: boolean = false;
|
||||
|
||||
@state()
|
||||
accessor tabFilter: TTabFilter = 'current';
|
||||
|
||||
@state()
|
||||
accessor severityFilter: TSeverity | 'all' = 'all';
|
||||
|
||||
@state()
|
||||
accessor expandedIncidents: Set<string> = new Set();
|
||||
|
||||
private statusIcons: Record<TIncidentStatus, string> = {
|
||||
investigating: 'lucide:Search',
|
||||
identified: 'lucide:Target',
|
||||
monitoring: 'lucide:Eye',
|
||||
resolved: 'lucide:CheckCircle',
|
||||
postmortem: 'lucide:FileText',
|
||||
};
|
||||
|
||||
private statusLabels: Record<TIncidentStatus, string> = {
|
||||
investigating: 'Investigating',
|
||||
identified: 'Identified',
|
||||
monitoring: 'Monitoring',
|
||||
resolved: 'Resolved',
|
||||
postmortem: 'Postmortem',
|
||||
};
|
||||
|
||||
public static styles = [
|
||||
plugins.domtools.elementBasic.staticStyles,
|
||||
sharedStyles.commonStyles,
|
||||
css`
|
||||
:host {
|
||||
display: block;
|
||||
font-family: ${unsafeCSS(sharedStyles.fonts.base)};
|
||||
}
|
||||
|
||||
.list-container {
|
||||
background: ${sharedStyles.colors.background.secondary};
|
||||
border: 1px solid ${sharedStyles.colors.border.default};
|
||||
border-radius: ${unsafeCSS(sharedStyles.borderRadius.lg)};
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.list-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: ${unsafeCSS(sharedStyles.spacing.md)};
|
||||
padding: ${unsafeCSS(sharedStyles.spacing.md)} ${unsafeCSS(sharedStyles.spacing.lg)};
|
||||
border-bottom: 1px solid ${sharedStyles.colors.border.default};
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.tabs {
|
||||
display: flex;
|
||||
gap: 4px;
|
||||
background: ${sharedStyles.colors.background.muted};
|
||||
padding: 4px;
|
||||
border-radius: ${unsafeCSS(sharedStyles.borderRadius.base)};
|
||||
}
|
||||
|
||||
.tab {
|
||||
padding: 10px 16px;
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
color: ${sharedStyles.colors.text.secondary};
|
||||
background: transparent;
|
||||
border: none;
|
||||
border-radius: ${unsafeCSS(sharedStyles.borderRadius.sm)};
|
||||
cursor: pointer;
|
||||
transition: all ${unsafeCSS(sharedStyles.durations.fast)} ${unsafeCSS(sharedStyles.easings.default)};
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.tab:hover {
|
||||
color: ${sharedStyles.colors.text.primary};
|
||||
}
|
||||
|
||||
.tab.active {
|
||||
background: ${sharedStyles.colors.background.primary};
|
||||
color: ${sharedStyles.colors.text.primary};
|
||||
box-shadow: ${unsafeCSS(sharedStyles.shadows.sm)};
|
||||
}
|
||||
|
||||
.tab-count {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-width: 22px;
|
||||
height: 22px;
|
||||
padding: 0 6px;
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
background: ${sharedStyles.colors.background.muted};
|
||||
border-radius: 11px;
|
||||
}
|
||||
|
||||
.tab.active .tab-count {
|
||||
background: ${sharedStyles.colors.accent.primary};
|
||||
color: white;
|
||||
}
|
||||
|
||||
.list-controls {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: ${unsafeCSS(sharedStyles.spacing.sm)};
|
||||
}
|
||||
|
||||
.filter-select {
|
||||
padding: 10px 32px 10px 12px;
|
||||
font-size: 13px;
|
||||
font-family: ${unsafeCSS(sharedStyles.fonts.base)};
|
||||
color: ${sharedStyles.colors.text.primary};
|
||||
background: ${sharedStyles.colors.background.primary};
|
||||
border: 1px solid ${sharedStyles.colors.border.default};
|
||||
border-radius: ${unsafeCSS(sharedStyles.borderRadius.base)};
|
||||
cursor: pointer;
|
||||
appearance: none;
|
||||
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 12 12'%3E%3Cpath fill='%2371717a' d='M2.5 4.5L6 8l3.5-3.5'/%3E%3C/svg%3E");
|
||||
background-repeat: no-repeat;
|
||||
background-position: right 10px center;
|
||||
}
|
||||
|
||||
.incidents-list {
|
||||
padding: ${unsafeCSS(sharedStyles.spacing.sm)};
|
||||
}
|
||||
|
||||
.incident-card {
|
||||
background: ${sharedStyles.colors.background.primary};
|
||||
border: 1px solid ${sharedStyles.colors.border.default};
|
||||
border-radius: ${unsafeCSS(sharedStyles.borderRadius.base)};
|
||||
margin-bottom: ${unsafeCSS(sharedStyles.spacing.sm)};
|
||||
overflow: hidden;
|
||||
transition: box-shadow ${unsafeCSS(sharedStyles.durations.fast)} ${unsafeCSS(sharedStyles.easings.default)};
|
||||
}
|
||||
|
||||
.incident-card:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.incident-card:hover {
|
||||
box-shadow: ${unsafeCSS(sharedStyles.shadows.sm)};
|
||||
}
|
||||
|
||||
.incident-header {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: ${unsafeCSS(sharedStyles.spacing.md)};
|
||||
padding: ${unsafeCSS(sharedStyles.spacing.md)};
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.incident-severity {
|
||||
width: 4px;
|
||||
align-self: stretch;
|
||||
border-radius: 2px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.incident-severity.critical { background: ${sharedStyles.colors.status.majorOutage}; }
|
||||
.incident-severity.major { background: ${sharedStyles.colors.status.partialOutage}; }
|
||||
.incident-severity.minor { background: ${sharedStyles.colors.status.degraded}; }
|
||||
.incident-severity.maintenance { background: ${sharedStyles.colors.status.maintenance}; }
|
||||
|
||||
.incident-main {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.incident-title-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: ${unsafeCSS(sharedStyles.spacing.sm)};
|
||||
margin-bottom: 6px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.incident-title {
|
||||
font-size: 15px;
|
||||
font-weight: 600;
|
||||
color: ${sharedStyles.colors.text.primary};
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.incident-status {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
padding: 4px 10px;
|
||||
font-size: 11px;
|
||||
font-weight: 500;
|
||||
border-radius: 9999px;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.3px;
|
||||
}
|
||||
|
||||
.incident-status dees-icon {
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.incident-status.investigating {
|
||||
background: ${cssManager.bdTheme('rgba(249, 115, 22, 0.1)', 'rgba(249, 115, 22, 0.2)')};
|
||||
color: ${sharedStyles.colors.status.partialOutage};
|
||||
--icon-color: ${sharedStyles.colors.status.partialOutage};
|
||||
}
|
||||
|
||||
.incident-status.identified {
|
||||
background: ${cssManager.bdTheme('rgba(234, 179, 8, 0.1)', 'rgba(234, 179, 8, 0.2)')};
|
||||
color: ${sharedStyles.colors.status.degraded};
|
||||
--icon-color: ${sharedStyles.colors.status.degraded};
|
||||
}
|
||||
|
||||
.incident-status.monitoring {
|
||||
background: ${cssManager.bdTheme('rgba(59, 130, 246, 0.1)', 'rgba(59, 130, 246, 0.2)')};
|
||||
color: ${sharedStyles.colors.status.maintenance};
|
||||
--icon-color: ${sharedStyles.colors.status.maintenance};
|
||||
}
|
||||
|
||||
.incident-status.resolved {
|
||||
background: ${cssManager.bdTheme('rgba(34, 197, 94, 0.1)', 'rgba(34, 197, 94, 0.2)')};
|
||||
color: ${sharedStyles.colors.status.operational};
|
||||
--icon-color: ${sharedStyles.colors.status.operational};
|
||||
}
|
||||
|
||||
.incident-status.postmortem {
|
||||
background: ${cssManager.bdTheme('rgba(168, 85, 247, 0.1)', 'rgba(168, 85, 247, 0.2)')};
|
||||
color: #a855f7;
|
||||
--icon-color: #a855f7;
|
||||
}
|
||||
|
||||
.incident-meta {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: ${unsafeCSS(sharedStyles.spacing.md)};
|
||||
font-size: 12px;
|
||||
color: ${sharedStyles.colors.text.muted};
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.incident-meta-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.incident-meta-item dees-icon {
|
||||
--icon-color: ${sharedStyles.colors.text.muted};
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
.incident-actions {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.incident-expand {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
padding: 0;
|
||||
color: ${sharedStyles.colors.text.muted};
|
||||
background: transparent;
|
||||
border: none;
|
||||
border-radius: ${unsafeCSS(sharedStyles.borderRadius.sm)};
|
||||
cursor: pointer;
|
||||
transition: all ${unsafeCSS(sharedStyles.durations.fast)} ${unsafeCSS(sharedStyles.easings.default)};
|
||||
}
|
||||
|
||||
.incident-expand:hover {
|
||||
background: ${sharedStyles.colors.background.muted};
|
||||
color: ${sharedStyles.colors.text.primary};
|
||||
}
|
||||
|
||||
.incident-expand.expanded dees-icon {
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
|
||||
.incident-expand dees-icon {
|
||||
transition: transform ${unsafeCSS(sharedStyles.durations.fast)} ${unsafeCSS(sharedStyles.easings.default)};
|
||||
--icon-color: currentColor;
|
||||
}
|
||||
|
||||
.incident-details {
|
||||
padding: 0 ${unsafeCSS(sharedStyles.spacing.md)} ${unsafeCSS(sharedStyles.spacing.md)};
|
||||
padding-left: calc(${unsafeCSS(sharedStyles.spacing.md)} + 4px + ${unsafeCSS(sharedStyles.spacing.md)});
|
||||
border-top: 1px solid ${sharedStyles.colors.border.light};
|
||||
}
|
||||
|
||||
.detail-section {
|
||||
margin-top: ${unsafeCSS(sharedStyles.spacing.md)};
|
||||
}
|
||||
|
||||
.detail-label {
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
color: ${sharedStyles.colors.text.muted};
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.detail-text {
|
||||
font-size: 13px;
|
||||
color: ${sharedStyles.colors.text.primary};
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.services-list {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.service-tag {
|
||||
display: inline-block;
|
||||
padding: 4px 10px;
|
||||
font-size: 11px;
|
||||
font-weight: 500;
|
||||
background: ${sharedStyles.colors.background.muted};
|
||||
color: ${sharedStyles.colors.text.secondary};
|
||||
border-radius: ${unsafeCSS(sharedStyles.borderRadius.sm)};
|
||||
}
|
||||
|
||||
.updates-timeline {
|
||||
position: relative;
|
||||
padding-left: 24px;
|
||||
}
|
||||
|
||||
.updates-timeline::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
left: 7px;
|
||||
top: 8px;
|
||||
bottom: 8px;
|
||||
width: 2px;
|
||||
background: ${sharedStyles.colors.border.default};
|
||||
border-radius: 1px;
|
||||
}
|
||||
|
||||
.update-item {
|
||||
position: relative;
|
||||
padding-bottom: ${unsafeCSS(sharedStyles.spacing.md)};
|
||||
}
|
||||
|
||||
.update-item:last-child {
|
||||
padding-bottom: 0;
|
||||
}
|
||||
|
||||
.update-item::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
left: -17px;
|
||||
top: 6px;
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
background: ${sharedStyles.colors.background.secondary};
|
||||
border: 2px solid ${sharedStyles.colors.accent.primary};
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
.update-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: ${unsafeCSS(sharedStyles.spacing.sm)};
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
|
||||
.update-status {
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
text-transform: capitalize;
|
||||
color: ${sharedStyles.colors.text.primary};
|
||||
}
|
||||
|
||||
.update-time {
|
||||
font-size: 11px;
|
||||
color: ${sharedStyles.colors.text.muted};
|
||||
}
|
||||
|
||||
.update-message {
|
||||
font-size: 13px;
|
||||
color: ${sharedStyles.colors.text.secondary};
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.update-author {
|
||||
font-size: 11px;
|
||||
color: ${sharedStyles.colors.text.muted};
|
||||
margin-top: 6px;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.empty-state {
|
||||
padding: 64px 24px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.empty-state dees-icon {
|
||||
--icon-color: ${sharedStyles.colors.status.operational};
|
||||
opacity: 0.6;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.empty-title {
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
color: ${sharedStyles.colors.text.primary};
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.empty-text {
|
||||
font-size: 14px;
|
||||
color: ${sharedStyles.colors.text.muted};
|
||||
}
|
||||
|
||||
.loading-overlay {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: ${cssManager.bdTheme('rgba(255,255,255,0.9)', 'rgba(0,0,0,0.7)')};
|
||||
z-index: 10;
|
||||
backdrop-filter: blur(2px);
|
||||
}
|
||||
`
|
||||
];
|
||||
|
||||
public render(): TemplateResult {
|
||||
const filteredIncidents = this.getFilteredIncidents();
|
||||
const currentCount = this.incidents.filter(i => !['resolved', 'postmortem'].includes(i.status)).length;
|
||||
const pastCount = this.incidents.filter(i => ['resolved', 'postmortem'].includes(i.status)).length;
|
||||
|
||||
return html`
|
||||
<div class="list-container" style="position: relative;">
|
||||
${this.loading ? html`
|
||||
<div class="loading-overlay">
|
||||
<dees-spinner></dees-spinner>
|
||||
</div>
|
||||
` : ''}
|
||||
|
||||
<div class="list-header">
|
||||
<div class="tabs">
|
||||
<button
|
||||
class="tab ${this.tabFilter === 'current' ? 'active' : ''}"
|
||||
@click="${() => this.tabFilter = 'current'}"
|
||||
>
|
||||
Current
|
||||
<span class="tab-count">${currentCount}</span>
|
||||
</button>
|
||||
<button
|
||||
class="tab ${this.tabFilter === 'past' ? 'active' : ''}"
|
||||
@click="${() => this.tabFilter = 'past'}"
|
||||
>
|
||||
Past
|
||||
<span class="tab-count">${pastCount}</span>
|
||||
</button>
|
||||
<button
|
||||
class="tab ${this.tabFilter === 'all' ? 'active' : ''}"
|
||||
@click="${() => this.tabFilter = 'all'}"
|
||||
>
|
||||
All
|
||||
<span class="tab-count">${this.incidents.length}</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="list-controls">
|
||||
<select class="filter-select" @change="${this.handleSeverityFilter}">
|
||||
<option value="all" ?selected="${this.severityFilter === 'all'}">All Severities</option>
|
||||
<option value="critical" ?selected="${this.severityFilter === 'critical'}">Critical</option>
|
||||
<option value="major" ?selected="${this.severityFilter === 'major'}">Major</option>
|
||||
<option value="minor" ?selected="${this.severityFilter === 'minor'}">Minor</option>
|
||||
<option value="maintenance" ?selected="${this.severityFilter === 'maintenance'}">Maintenance</option>
|
||||
</select>
|
||||
|
||||
<dees-button type="highlighted" @click="${this.handleAddClick}">
|
||||
<dees-icon .icon=${'lucide:Plus'} .iconSize=${16}></dees-icon>
|
||||
New Incident
|
||||
</dees-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
${filteredIncidents.length > 0 ? html`
|
||||
<div class="incidents-list">
|
||||
${filteredIncidents.map(incident => this.renderIncidentCard(incident))}
|
||||
</div>
|
||||
` : html`
|
||||
<div class="empty-state">
|
||||
<dees-icon .icon=${'lucide:PartyPopper'} .iconSize=${48}></dees-icon>
|
||||
<div class="empty-title">
|
||||
${this.tabFilter === 'current' ? 'No active incidents' : 'No incidents found'}
|
||||
</div>
|
||||
<div class="empty-text">
|
||||
${this.tabFilter === 'current'
|
||||
? 'All systems are operating normally'
|
||||
: 'Try adjusting your filters'}
|
||||
</div>
|
||||
</div>
|
||||
`}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
private renderIncidentCard(incident: IIncidentDetails): TemplateResult {
|
||||
const isExpanded = this.expandedIncidents.has(incident.id);
|
||||
|
||||
const formatTime = (timestamp: number) => {
|
||||
const date = new Date(timestamp);
|
||||
return date.toLocaleString();
|
||||
};
|
||||
|
||||
const formatDuration = (start: number, end?: number) => {
|
||||
const duration = (end || Date.now()) - start;
|
||||
const hours = Math.floor(duration / (1000 * 60 * 60));
|
||||
const minutes = Math.floor((duration % (1000 * 60 * 60)) / (1000 * 60));
|
||||
if (hours > 0) return `${hours}h ${minutes}m`;
|
||||
return `${minutes}m`;
|
||||
};
|
||||
|
||||
return html`
|
||||
<div class="incident-card">
|
||||
<div class="incident-header" @click="${() => this.toggleExpanded(incident.id)}">
|
||||
<div class="incident-severity ${incident.severity}"></div>
|
||||
|
||||
<div class="incident-main">
|
||||
<div class="incident-title-row">
|
||||
<h3 class="incident-title">${incident.title}</h3>
|
||||
<span class="incident-status ${incident.status}">
|
||||
<dees-icon .icon=${this.statusIcons[incident.status]} .iconSize=${12}></dees-icon>
|
||||
${this.statusLabels[incident.status]}
|
||||
</span>
|
||||
</div>
|
||||
<div class="incident-meta">
|
||||
<span class="incident-meta-item">
|
||||
<dees-icon .icon=${'lucide:Calendar'} .iconSize=${12}></dees-icon>
|
||||
${formatTime(incident.startTime)}
|
||||
</span>
|
||||
<span class="incident-meta-item">
|
||||
<dees-icon .icon=${'lucide:Clock'} .iconSize=${12}></dees-icon>
|
||||
${formatDuration(incident.startTime, incident.endTime)}
|
||||
</span>
|
||||
<span class="incident-meta-item">
|
||||
<dees-icon .icon=${'lucide:Server'} .iconSize=${12}></dees-icon>
|
||||
${incident.affectedServices.length} services
|
||||
</span>
|
||||
<span class="incident-meta-item">
|
||||
<dees-icon .icon=${'lucide:MessageSquare'} .iconSize=${12}></dees-icon>
|
||||
${incident.updates.length} updates
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="incident-actions" @click="${(e: Event) => e.stopPropagation()}">
|
||||
${!['resolved', 'postmortem'].includes(incident.status) ? html`
|
||||
<dees-button type="highlighted" @click="${() => this.handleAddUpdate(incident)}">
|
||||
<dees-icon .icon=${'lucide:Plus'} .iconSize=${14}></dees-icon>
|
||||
Update
|
||||
</dees-button>
|
||||
` : ''}
|
||||
<dees-button type="discreet" @click="${() => this.handleEdit(incident)}">
|
||||
<dees-icon .icon=${'lucide:Pencil'} .iconSize=${14}></dees-icon>
|
||||
</dees-button>
|
||||
</div>
|
||||
|
||||
<button class="incident-expand ${isExpanded ? 'expanded' : ''}">
|
||||
<dees-icon .icon=${'lucide:ChevronDown'} .iconSize=${16}></dees-icon>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
${isExpanded ? html`
|
||||
<div class="incident-details">
|
||||
<div class="detail-section">
|
||||
<div class="detail-label">Impact</div>
|
||||
<div class="detail-text">${incident.impact}</div>
|
||||
</div>
|
||||
|
||||
<div class="detail-section">
|
||||
<div class="detail-label">Affected Services</div>
|
||||
<div class="services-list">
|
||||
${incident.affectedServices.map(service => html`
|
||||
<span class="service-tag">${service}</span>
|
||||
`)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
${incident.rootCause ? html`
|
||||
<div class="detail-section">
|
||||
<div class="detail-label">Root Cause</div>
|
||||
<div class="detail-text">${incident.rootCause}</div>
|
||||
</div>
|
||||
` : ''}
|
||||
|
||||
${incident.resolution ? html`
|
||||
<div class="detail-section">
|
||||
<div class="detail-label">Resolution</div>
|
||||
<div class="detail-text">${incident.resolution}</div>
|
||||
</div>
|
||||
` : ''}
|
||||
|
||||
${incident.updates.length > 0 ? html`
|
||||
<div class="detail-section">
|
||||
<div class="detail-label">Updates Timeline</div>
|
||||
<div class="updates-timeline">
|
||||
${incident.updates.slice().reverse().map(update => html`
|
||||
<div class="update-item">
|
||||
<div class="update-header">
|
||||
<span class="update-status">${update.status}</span>
|
||||
<span class="update-time">${formatTime(update.timestamp)}</span>
|
||||
</div>
|
||||
<div class="update-message">${update.message}</div>
|
||||
${update.author ? html`<div class="update-author">— ${update.author}</div>` : ''}
|
||||
</div>
|
||||
`)}
|
||||
</div>
|
||||
</div>
|
||||
` : ''}
|
||||
</div>
|
||||
` : ''}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
private getFilteredIncidents(): IIncidentDetails[] {
|
||||
let result = [...this.incidents];
|
||||
|
||||
// Tab filter
|
||||
switch (this.tabFilter) {
|
||||
case 'current':
|
||||
result = result.filter(i => !['resolved', 'postmortem'].includes(i.status));
|
||||
break;
|
||||
case 'past':
|
||||
result = result.filter(i => ['resolved', 'postmortem'].includes(i.status));
|
||||
break;
|
||||
}
|
||||
|
||||
// Severity filter
|
||||
if (this.severityFilter !== 'all') {
|
||||
result = result.filter(i => i.severity === this.severityFilter);
|
||||
}
|
||||
|
||||
// Sort by start time descending
|
||||
result.sort((a, b) => b.startTime - a.startTime);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private handleSeverityFilter(e: Event) {
|
||||
this.severityFilter = (e.target as HTMLSelectElement).value as TSeverity | 'all';
|
||||
}
|
||||
|
||||
private toggleExpanded(incidentId: string) {
|
||||
const newSet = new Set(this.expandedIncidents);
|
||||
if (newSet.has(incidentId)) {
|
||||
newSet.delete(incidentId);
|
||||
} else {
|
||||
newSet.add(incidentId);
|
||||
}
|
||||
this.expandedIncidents = newSet;
|
||||
}
|
||||
|
||||
private handleAddClick() {
|
||||
this.dispatchEvent(new CustomEvent('incidentAdd', {
|
||||
bubbles: true,
|
||||
composed: true
|
||||
}));
|
||||
}
|
||||
|
||||
private handleEdit(incident: IIncidentDetails) {
|
||||
this.dispatchEvent(new CustomEvent('incidentEdit', {
|
||||
detail: { incident },
|
||||
bubbles: true,
|
||||
composed: true
|
||||
}));
|
||||
}
|
||||
|
||||
private handleAddUpdate(incident: IIncidentDetails) {
|
||||
this.dispatchEvent(new CustomEvent('incidentAddUpdate', {
|
||||
detail: { incident },
|
||||
bubbles: true,
|
||||
composed: true
|
||||
}));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user