initial
This commit is contained in:
1
ts_web/elements/upladmin-dashboard/index.ts
Normal file
1
ts_web/elements/upladmin-dashboard/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from './upladmin-dashboard.js';
|
||||
@@ -0,0 +1,57 @@
|
||||
import { html, css, cssManager } from '@design.estate/dees-element';
|
||||
import type { IServiceStatus, IIncidentDetails } from '../../interfaces/index.js';
|
||||
import './upladmin-dashboard.js';
|
||||
|
||||
export const demoFunc = () => html`
|
||||
<style>
|
||||
${css`
|
||||
.demo-container {
|
||||
padding: 24px;
|
||||
background: ${cssManager.bdTheme('#f4f4f5', '#09090b')};
|
||||
min-height: 100vh;
|
||||
}
|
||||
`}
|
||||
</style>
|
||||
|
||||
<div class="demo-container">
|
||||
<upladmin-dashboard
|
||||
.monitors=${[
|
||||
{ id: 'api', name: 'api', displayName: 'API Server', currentStatus: 'operational', lastChecked: Date.now(), uptime30d: 99.98, uptime90d: 99.95, responseTime: 45, category: 'Core' },
|
||||
{ id: 'web', name: 'web', displayName: 'Web App', currentStatus: 'operational', lastChecked: Date.now(), uptime30d: 99.99, uptime90d: 99.97, responseTime: 120, category: 'Core' },
|
||||
{ id: 'db', name: 'db', displayName: 'Database', currentStatus: 'operational', lastChecked: Date.now(), uptime30d: 99.999, uptime90d: 99.998, responseTime: 5, category: 'Infrastructure' },
|
||||
{ id: 'cdn', name: 'cdn', displayName: 'CDN', currentStatus: 'degraded', lastChecked: Date.now(), uptime30d: 99.5, uptime90d: 99.8, responseTime: 200, category: 'Infrastructure' },
|
||||
{ id: 'cache', name: 'cache', displayName: 'Redis Cache', currentStatus: 'operational', lastChecked: Date.now(), uptime30d: 99.99, uptime90d: 99.98, responseTime: 2, category: 'Infrastructure' },
|
||||
{ id: 'email', name: 'email', displayName: 'Email Service', currentStatus: 'operational', lastChecked: Date.now(), uptime30d: 99.9, uptime90d: 99.85, responseTime: 500, category: 'External' },
|
||||
{ id: 'payment', name: 'payment', displayName: 'Payment Gateway', currentStatus: 'maintenance', lastChecked: Date.now(), uptime30d: 99.95, uptime90d: 99.9, responseTime: 350, category: 'External' },
|
||||
{ id: 'search', name: 'search', displayName: 'Search Engine', currentStatus: 'partial_outage', lastChecked: Date.now(), uptime30d: 98.5, uptime90d: 99.2, responseTime: 150, category: 'Core' },
|
||||
] as IServiceStatus[]}
|
||||
.activeIncidents=${[
|
||||
{
|
||||
id: 'inc-1',
|
||||
title: 'CDN Performance Degradation',
|
||||
status: 'identified',
|
||||
severity: 'minor',
|
||||
affectedServices: ['cdn'],
|
||||
startTime: Date.now() - 2 * 60 * 60 * 1000,
|
||||
impact: 'Some users may experience slower page loads',
|
||||
updates: [
|
||||
{ id: 'u1', timestamp: Date.now() - 1 * 60 * 60 * 1000, status: 'identified', message: 'Root cause identified as network congestion' },
|
||||
{ id: 'u2', timestamp: Date.now() - 2 * 60 * 60 * 1000, status: 'investigating', message: 'We are investigating reports of slow content delivery' },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'inc-2',
|
||||
title: 'Search Cluster Partial Failure',
|
||||
status: 'investigating',
|
||||
severity: 'major',
|
||||
affectedServices: ['search'],
|
||||
startTime: Date.now() - 30 * 60 * 1000,
|
||||
impact: 'Search functionality may return incomplete results',
|
||||
updates: [
|
||||
{ id: 'u3', timestamp: Date.now() - 30 * 60 * 1000, status: 'investigating', message: 'We are investigating issues with the search cluster' },
|
||||
],
|
||||
},
|
||||
] as IIncidentDetails[]}
|
||||
></upladmin-dashboard>
|
||||
</div>
|
||||
`;
|
||||
681
ts_web/elements/upladmin-dashboard/upladmin-dashboard.ts
Normal file
681
ts_web/elements/upladmin-dashboard/upladmin-dashboard.ts
Normal file
@@ -0,0 +1,681 @@
|
||||
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`
|
||||
<div class="dashboard">
|
||||
<!-- Overall Status Banner -->
|
||||
${this.renderStatusBanner()}
|
||||
|
||||
<!-- Stats Grid using dees-statsgrid -->
|
||||
<div class="stats-container">
|
||||
<dees-statsgrid
|
||||
.tiles=${this.statsTiles}
|
||||
.minTileWidth=${200}
|
||||
.gap=${16}
|
||||
></dees-statsgrid>
|
||||
</div>
|
||||
|
||||
<!-- Content Grid -->
|
||||
<div class="content-grid">
|
||||
<!-- Active Incidents -->
|
||||
<div class="section-card">
|
||||
<div class="section-header">
|
||||
<span class="section-title">Active Incidents</span>
|
||||
<button class="section-action" @click="${this.handleViewAllIncidents}">
|
||||
View All <dees-icon .icon=${'lucide:arrowRight'}></dees-icon>
|
||||
</button>
|
||||
</div>
|
||||
<div class="section-body">
|
||||
${activeIncidents.length > 0 ? html`
|
||||
<div class="incident-list">
|
||||
${activeIncidents.slice(0, 5).map(incident => this.renderIncidentItem(incident))}
|
||||
</div>
|
||||
` : html`
|
||||
<div class="empty-state">
|
||||
<div class="empty-icon"><dees-icon .icon=${'lucide:partyPopper'}></dees-icon></div>
|
||||
<div class="empty-text">No active incidents</div>
|
||||
</div>
|
||||
`}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Status by Category -->
|
||||
<div class="section-card">
|
||||
<div class="section-header">
|
||||
<span class="section-title">Status by Category</span>
|
||||
<button class="section-action" @click="${this.handleViewAllMonitors}">
|
||||
View All <dees-icon .icon=${'lucide:arrowRight'}></dees-icon>
|
||||
</button>
|
||||
</div>
|
||||
<div class="section-body">
|
||||
${this.renderCategoryStatus()}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Quick Actions -->
|
||||
<div class="section-card">
|
||||
<div class="section-header">
|
||||
<span class="section-title">Quick Actions</span>
|
||||
</div>
|
||||
<div class="section-body">
|
||||
<div class="quick-actions">
|
||||
<button class="quick-action" @click="${this.handleNewIncident}">
|
||||
<span class="quick-action-icon"><dees-icon .icon=${'lucide:alertCircle'}></dees-icon></span>
|
||||
<span class="quick-action-label">Report Incident</span>
|
||||
</button>
|
||||
<button class="quick-action" @click="${this.handleNewMonitor}">
|
||||
<span class="quick-action-icon"><dees-icon .icon=${'lucide:radio'}></dees-icon></span>
|
||||
<span class="quick-action-label">Add Monitor</span>
|
||||
</button>
|
||||
<button class="quick-action" @click="${this.handleScheduleMaintenance}">
|
||||
<span class="quick-action-icon"><dees-icon .icon=${'lucide:wrench'}></dees-icon></span>
|
||||
<span class="quick-action-label">Schedule Maintenance</span>
|
||||
</button>
|
||||
<button class="quick-action" @click="${this.handleViewConfig}">
|
||||
<span class="quick-action-icon"><dees-icon .icon=${'lucide:settings'}></dees-icon></span>
|
||||
<span class="quick-action-label">Configuration</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
private renderStatusBanner(): TemplateResult {
|
||||
const status = this.overallStatus || this.calculateOverallStatus();
|
||||
const statusIcons: Record<TStatusType, string> = {
|
||||
operational: 'lucide:check',
|
||||
degraded: 'lucide:alertTriangle',
|
||||
partial_outage: 'lucide:zap',
|
||||
major_outage: 'lucide:x',
|
||||
maintenance: 'lucide:wrench',
|
||||
};
|
||||
|
||||
const statusTitles: Record<TStatusType, string> = {
|
||||
operational: 'All Systems Operational',
|
||||
degraded: 'Degraded Performance',
|
||||
partial_outage: 'Partial System Outage',
|
||||
major_outage: 'Major System Outage',
|
||||
maintenance: 'Scheduled Maintenance',
|
||||
};
|
||||
|
||||
return html`
|
||||
<div class="status-banner ${status.status}">
|
||||
<div class="status-indicator ${status.status}">
|
||||
<dees-icon .icon=${statusIcons[status.status]}></dees-icon>
|
||||
</div>
|
||||
<div class="status-content">
|
||||
<div class="status-title">${statusTitles[status.status]}</div>
|
||||
<div class="status-message">${status.message}</div>
|
||||
<div class="status-meta">
|
||||
Last updated: ${new Date(status.lastUpdated).toLocaleString()}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
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`
|
||||
<div class="incident-item ${incident.severity}" @click="${() => this.handleIncidentClick(incident)}">
|
||||
<div class="incident-content">
|
||||
<div class="incident-title">${incident.title}</div>
|
||||
<div class="incident-meta">
|
||||
<span class="incident-status">${incident.status}</span>
|
||||
<span>•</span>
|
||||
<span>${formatTime(incident.startTime)}</span>
|
||||
<span>•</span>
|
||||
<span>${incident.affectedServices.length} services</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
private renderCategoryStatus(): TemplateResult {
|
||||
const categories = [...new Set(this.monitors.map(m => m.category || 'Uncategorized'))];
|
||||
|
||||
if (categories.length === 0) {
|
||||
return html`
|
||||
<div class="empty-state">
|
||||
<div class="empty-icon"><dees-icon .icon=${'lucide:barChart3'}></dees-icon></div>
|
||||
<div class="empty-text">No monitors configured</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
return html`
|
||||
<div class="category-list">
|
||||
${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`
|
||||
<div class="category-item">
|
||||
<span class="category-name">${category}</span>
|
||||
<div class="category-stats">
|
||||
<span class="category-count">${operational}/${categoryMonitors.length}</span>
|
||||
<div class="category-bar">
|
||||
<div class="category-bar-fill" style="width: ${percentage}%"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
})}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
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 }));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user