initial
This commit is contained in:
14
ts_web/elements/index.ts
Normal file
14
ts_web/elements/index.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
// Monitor components
|
||||
export * from './upladmin-monitor-form/index.js';
|
||||
export * from './upladmin-monitor-list/index.js';
|
||||
|
||||
// Incident components
|
||||
export * from './upladmin-incident-form/index.js';
|
||||
export * from './upladmin-incident-list/index.js';
|
||||
export * from './upladmin-incident-update/index.js';
|
||||
|
||||
// Configuration components
|
||||
export * from './upladmin-statuspage-config/index.js';
|
||||
|
||||
// Dashboard components
|
||||
export * from './upladmin-dashboard/index.js';
|
||||
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 }));
|
||||
}
|
||||
}
|
||||
1
ts_web/elements/upladmin-incident-form/index.ts
Normal file
1
ts_web/elements/upladmin-incident-form/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from './upladmin-incident-form.js';
|
||||
@@ -0,0 +1,70 @@
|
||||
import { html, css, cssManager } from '@design.estate/dees-element';
|
||||
import type { IServiceStatus } from '../../interfaces/index.js';
|
||||
import './upladmin-incident-form.js';
|
||||
|
||||
export const demoFunc = () => html`
|
||||
<style>
|
||||
${css`
|
||||
.demo-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 32px;
|
||||
padding: 24px;
|
||||
max-width: 900px;
|
||||
margin: 0 auto;
|
||||
background: ${cssManager.bdTheme('#fafafa', '#0a0a0a')};
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
.demo-section {
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.demo-section h3 {
|
||||
margin: 0 0 16px 0;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: ${cssManager.bdTheme('#71717a', '#a1a1aa')};
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.05em;
|
||||
}
|
||||
`}
|
||||
</style>
|
||||
|
||||
<div class="demo-container">
|
||||
<div class="demo-section">
|
||||
<h3>Create New Incident</h3>
|
||||
<upladmin-incident-form
|
||||
.availableServices=${[
|
||||
{ id: 'api', name: 'api', displayName: 'API Server', currentStatus: 'operational', lastChecked: Date.now(), uptime30d: 99.98, uptime90d: 99.95, responseTime: 45 },
|
||||
{ id: 'web', name: 'web', displayName: 'Web Application', currentStatus: 'operational', lastChecked: Date.now(), uptime30d: 99.99, uptime90d: 99.97, responseTime: 120 },
|
||||
{ id: 'db', name: 'db', displayName: 'Database', currentStatus: 'operational', lastChecked: Date.now(), uptime30d: 99.999, uptime90d: 99.998, responseTime: 5 },
|
||||
{ id: 'cdn', name: 'cdn', displayName: 'CDN', currentStatus: 'degraded', lastChecked: Date.now(), uptime30d: 99.5, uptime90d: 99.8, responseTime: 200 },
|
||||
{ id: 'cache', name: 'cache', displayName: 'Redis Cache', currentStatus: 'operational', lastChecked: Date.now(), uptime30d: 99.99, uptime90d: 99.98, responseTime: 2 },
|
||||
] as IServiceStatus[]}
|
||||
></upladmin-incident-form>
|
||||
</div>
|
||||
|
||||
<div class="demo-section">
|
||||
<h3>Edit Existing Incident</h3>
|
||||
<upladmin-incident-form
|
||||
.incident=${{
|
||||
id: 'inc-123',
|
||||
title: 'Database Connection Issues',
|
||||
severity: 'major',
|
||||
status: 'identified',
|
||||
affectedServices: ['db', 'api'],
|
||||
impact: 'Users may experience slow response times and occasional timeouts when accessing the application.',
|
||||
rootCause: 'Connection pool exhaustion due to a memory leak in the database driver.',
|
||||
}}
|
||||
.availableServices=${[
|
||||
{ id: 'api', name: 'api', displayName: 'API Server', currentStatus: 'degraded', lastChecked: Date.now(), uptime30d: 99.98, uptime90d: 99.95, responseTime: 45 },
|
||||
{ id: 'web', name: 'web', displayName: 'Web Application', currentStatus: 'operational', lastChecked: Date.now(), uptime30d: 99.99, uptime90d: 99.97, responseTime: 120 },
|
||||
{ id: 'db', name: 'db', displayName: 'Database', currentStatus: 'partial_outage', lastChecked: Date.now(), uptime30d: 99.999, uptime90d: 99.998, responseTime: 5 },
|
||||
{ id: 'cdn', name: 'cdn', displayName: 'CDN', currentStatus: 'operational', lastChecked: Date.now(), uptime30d: 99.5, uptime90d: 99.8, responseTime: 200 },
|
||||
{ id: 'cache', name: 'cache', displayName: 'Redis Cache', currentStatus: 'operational', lastChecked: Date.now(), uptime30d: 99.99, uptime90d: 99.98, responseTime: 2 },
|
||||
] as IServiceStatus[]}
|
||||
></upladmin-incident-form>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
585
ts_web/elements/upladmin-incident-form/upladmin-incident-form.ts
Normal file
585
ts_web/elements/upladmin-incident-form/upladmin-incident-form.ts
Normal file
@@ -0,0 +1,585 @@
|
||||
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 { IIncidentFormData, IServiceStatus } from '../../interfaces/index.js';
|
||||
import { demoFunc } from './upladmin-incident-form.demo.js';
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
'upladmin-incident-form': UpladminIncidentForm;
|
||||
}
|
||||
}
|
||||
|
||||
type TSeverity = 'critical' | 'major' | 'minor' | 'maintenance';
|
||||
type TIncidentStatus = 'investigating' | 'identified' | 'monitoring' | 'resolved' | 'postmortem';
|
||||
|
||||
@customElement('upladmin-incident-form')
|
||||
export class UpladminIncidentForm extends DeesElement {
|
||||
public static demo = demoFunc;
|
||||
|
||||
@property({ type: Object })
|
||||
accessor incident: IIncidentFormData | null = null;
|
||||
|
||||
@property({ type: Array })
|
||||
accessor availableServices: IServiceStatus[] = [];
|
||||
|
||||
@property({ type: Boolean })
|
||||
accessor loading: boolean = false;
|
||||
|
||||
@state()
|
||||
accessor formData: IIncidentFormData = {
|
||||
title: '',
|
||||
severity: 'minor',
|
||||
status: 'investigating',
|
||||
affectedServices: [],
|
||||
impact: '',
|
||||
rootCause: '',
|
||||
resolution: '',
|
||||
};
|
||||
|
||||
@state()
|
||||
accessor errors: Record<string, string> = {};
|
||||
|
||||
private severityIcons: Record<TSeverity, string> = {
|
||||
critical: 'lucide:AlertCircle',
|
||||
major: 'lucide:AlertTriangle',
|
||||
minor: 'lucide:Info',
|
||||
maintenance: 'lucide:Wrench',
|
||||
};
|
||||
|
||||
private statusIcons: Record<TIncidentStatus, string> = {
|
||||
investigating: 'lucide:Search',
|
||||
identified: 'lucide:Target',
|
||||
monitoring: 'lucide:Eye',
|
||||
resolved: 'lucide:CheckCircle',
|
||||
postmortem: 'lucide:FileText',
|
||||
};
|
||||
|
||||
public static styles = [
|
||||
plugins.domtools.elementBasic.staticStyles,
|
||||
sharedStyles.commonStyles,
|
||||
css`
|
||||
:host {
|
||||
display: block;
|
||||
font-family: ${unsafeCSS(sharedStyles.fonts.base)};
|
||||
}
|
||||
|
||||
.form-container {
|
||||
background: ${sharedStyles.colors.background.secondary};
|
||||
border: 1px solid ${sharedStyles.colors.border.default};
|
||||
border-radius: ${unsafeCSS(sharedStyles.borderRadius.lg)};
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.form-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: ${unsafeCSS(sharedStyles.spacing.md)};
|
||||
padding: ${unsafeCSS(sharedStyles.spacing.lg)};
|
||||
border-bottom: 1px solid ${sharedStyles.colors.border.default};
|
||||
background: ${sharedStyles.colors.background.muted};
|
||||
}
|
||||
|
||||
.form-header dees-icon {
|
||||
--icon-color: ${cssManager.bdTheme('#f97316', '#fb923c')};
|
||||
}
|
||||
|
||||
.form-title-wrapper {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.form-title {
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
color: ${sharedStyles.colors.text.primary};
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.form-subtitle {
|
||||
font-size: 13px;
|
||||
color: ${sharedStyles.colors.text.muted};
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
.form-body {
|
||||
display: grid;
|
||||
gap: ${unsafeCSS(sharedStyles.spacing.lg)};
|
||||
padding: ${unsafeCSS(sharedStyles.spacing.lg)};
|
||||
}
|
||||
|
||||
dees-form {
|
||||
display: contents;
|
||||
}
|
||||
|
||||
.form-row {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: ${unsafeCSS(sharedStyles.spacing.md)};
|
||||
}
|
||||
|
||||
@media (max-width: 600px) {
|
||||
.form-row {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
.form-section {
|
||||
margin-top: ${unsafeCSS(sharedStyles.spacing.md)};
|
||||
padding-top: ${unsafeCSS(sharedStyles.spacing.md)};
|
||||
border-top: 1px solid ${sharedStyles.colors.border.light};
|
||||
}
|
||||
|
||||
.section-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: ${sharedStyles.colors.text.primary};
|
||||
margin-bottom: ${unsafeCSS(sharedStyles.spacing.md)};
|
||||
}
|
||||
|
||||
.section-title dees-icon {
|
||||
--icon-color: ${sharedStyles.colors.text.muted};
|
||||
}
|
||||
|
||||
.form-actions {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: ${unsafeCSS(sharedStyles.spacing.sm)};
|
||||
padding: ${unsafeCSS(sharedStyles.spacing.md)} ${unsafeCSS(sharedStyles.spacing.lg)};
|
||||
border-top: 1px solid ${sharedStyles.colors.border.default};
|
||||
background: ${sharedStyles.colors.background.muted};
|
||||
}
|
||||
|
||||
.option-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(130px, 1fr));
|
||||
gap: ${unsafeCSS(sharedStyles.spacing.sm)};
|
||||
}
|
||||
|
||||
.option-card {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
padding: 18px 14px;
|
||||
background: ${sharedStyles.colors.background.primary};
|
||||
border: 2px solid ${sharedStyles.colors.border.default};
|
||||
border-radius: ${unsafeCSS(sharedStyles.borderRadius.base)};
|
||||
cursor: pointer;
|
||||
transition: all ${unsafeCSS(sharedStyles.durations.fast)} ${unsafeCSS(sharedStyles.easings.default)};
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.option-card:hover {
|
||||
border-color: ${sharedStyles.colors.border.strong};
|
||||
background: ${sharedStyles.colors.background.muted};
|
||||
}
|
||||
|
||||
.option-card.selected {
|
||||
border-color: ${sharedStyles.colors.accent.primary};
|
||||
background: ${cssManager.bdTheme('rgba(59, 130, 246, 0.05)', 'rgba(96, 165, 250, 0.1)')};
|
||||
}
|
||||
|
||||
.option-card input {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.option-label {
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
color: ${sharedStyles.colors.text.primary};
|
||||
}
|
||||
|
||||
.option-desc {
|
||||
font-size: 11px;
|
||||
color: ${sharedStyles.colors.text.muted};
|
||||
line-height: 1.3;
|
||||
}
|
||||
|
||||
.severity-critical dees-icon { --icon-color: ${sharedStyles.colors.status.majorOutage}; }
|
||||
.severity-major dees-icon { --icon-color: ${sharedStyles.colors.status.partialOutage}; }
|
||||
.severity-minor dees-icon { --icon-color: ${sharedStyles.colors.status.degraded}; }
|
||||
.severity-maintenance dees-icon { --icon-color: ${sharedStyles.colors.status.maintenance}; }
|
||||
|
||||
.field-label {
|
||||
display: block;
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
color: ${sharedStyles.colors.text.primary};
|
||||
margin-bottom: ${unsafeCSS(sharedStyles.spacing.sm)};
|
||||
}
|
||||
|
||||
.field-label.required::after {
|
||||
content: ' *';
|
||||
color: ${sharedStyles.colors.accent.danger};
|
||||
}
|
||||
|
||||
.services-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
|
||||
gap: ${unsafeCSS(sharedStyles.spacing.sm)};
|
||||
max-height: 220px;
|
||||
overflow-y: auto;
|
||||
padding: 4px;
|
||||
}
|
||||
|
||||
.service-checkbox {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
padding: 12px 14px;
|
||||
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)};
|
||||
}
|
||||
|
||||
.service-checkbox:hover {
|
||||
background: ${sharedStyles.colors.background.muted};
|
||||
border-color: ${sharedStyles.colors.border.strong};
|
||||
}
|
||||
|
||||
.service-checkbox.selected {
|
||||
border-color: ${sharedStyles.colors.accent.primary};
|
||||
background: ${cssManager.bdTheme('rgba(59, 130, 246, 0.05)', 'rgba(96, 165, 250, 0.1)')};
|
||||
}
|
||||
|
||||
.service-checkbox input {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
accent-color: ${sharedStyles.colors.accent.primary};
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.service-checkbox label {
|
||||
flex: 1;
|
||||
font-size: 13px;
|
||||
color: ${sharedStyles.colors.text.primary};
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.status-dot {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
border-radius: 50%;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.status-dot.operational { background: ${sharedStyles.colors.status.operational}; }
|
||||
.status-dot.degraded { background: ${sharedStyles.colors.status.degraded}; }
|
||||
.status-dot.partial_outage { background: ${sharedStyles.colors.status.partialOutage}; }
|
||||
.status-dot.major_outage { background: ${sharedStyles.colors.status.majorOutage}; }
|
||||
.status-dot.maintenance { background: ${sharedStyles.colors.status.maintenance}; }
|
||||
|
||||
.error-text {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
font-size: 12px;
|
||||
color: ${sharedStyles.colors.accent.danger};
|
||||
margin-top: ${unsafeCSS(sharedStyles.spacing.xs)};
|
||||
}
|
||||
|
||||
.error-text dees-icon {
|
||||
--icon-color: ${sharedStyles.colors.accent.danger};
|
||||
}
|
||||
|
||||
/* Style dees-input components */
|
||||
dees-input-text {
|
||||
--dees-input-background: ${sharedStyles.colors.background.primary};
|
||||
--dees-input-border-color: ${sharedStyles.colors.border.default};
|
||||
}
|
||||
`
|
||||
];
|
||||
|
||||
async connectedCallback() {
|
||||
await super.connectedCallback();
|
||||
if (this.incident) {
|
||||
this.formData = { ...this.incident };
|
||||
}
|
||||
}
|
||||
|
||||
updated(changedProperties: Map<string, unknown>) {
|
||||
if (changedProperties.has('incident') && this.incident) {
|
||||
this.formData = { ...this.incident };
|
||||
}
|
||||
}
|
||||
|
||||
public render(): TemplateResult {
|
||||
const isEdit = !!this.incident?.id;
|
||||
|
||||
const severityOptions: Array<{ value: TSeverity; label: string; desc: string }> = [
|
||||
{ value: 'critical', label: 'Critical', desc: 'Major system failure' },
|
||||
{ value: 'major', label: 'Major', desc: 'Significant impact' },
|
||||
{ value: 'minor', label: 'Minor', desc: 'Limited impact' },
|
||||
{ value: 'maintenance', label: 'Maintenance', desc: 'Planned work' },
|
||||
];
|
||||
|
||||
const statusOptions: Array<{ value: TIncidentStatus; label: string }> = [
|
||||
{ value: 'investigating', label: 'Investigating' },
|
||||
{ value: 'identified', label: 'Identified' },
|
||||
{ value: 'monitoring', label: 'Monitoring' },
|
||||
{ value: 'resolved', label: 'Resolved' },
|
||||
{ value: 'postmortem', label: 'Postmortem' },
|
||||
];
|
||||
|
||||
return html`
|
||||
<div class="form-container">
|
||||
<div class="form-header">
|
||||
<dees-icon .icon=${isEdit ? 'lucide:Pencil' : 'lucide:AlertTriangle'} .iconSize=${24}></dees-icon>
|
||||
<div class="form-title-wrapper">
|
||||
<h2 class="form-title">${isEdit ? 'Edit Incident' : 'Create Incident'}</h2>
|
||||
<p class="form-subtitle">
|
||||
${isEdit ? 'Update incident details' : 'Report a new incident or maintenance'}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-body">
|
||||
<dees-form>
|
||||
<dees-input-text
|
||||
key="title"
|
||||
label="Incident Title"
|
||||
.value="${this.formData.title}"
|
||||
placeholder="Brief description of the incident"
|
||||
required
|
||||
@changeSubject="${this.handleTitleChange}"
|
||||
></dees-input-text>
|
||||
|
||||
<div>
|
||||
<label class="field-label required">Severity</label>
|
||||
<div class="option-grid">
|
||||
${severityOptions.map(opt => html`
|
||||
<label
|
||||
class="option-card severity-${opt.value} ${this.formData.severity === opt.value ? 'selected' : ''}"
|
||||
@click="${() => this.handleSeverityChange(opt.value)}"
|
||||
>
|
||||
<input
|
||||
type="radio"
|
||||
name="severity"
|
||||
value="${opt.value}"
|
||||
?checked="${this.formData.severity === opt.value}"
|
||||
/>
|
||||
<dees-icon .icon=${this.severityIcons[opt.value]} .iconSize=${24}></dees-icon>
|
||||
<span class="option-label">${opt.label}</span>
|
||||
<span class="option-desc">${opt.desc}</span>
|
||||
</label>
|
||||
`)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="field-label required">Status</label>
|
||||
<div class="option-grid">
|
||||
${statusOptions.map(opt => html`
|
||||
<label
|
||||
class="option-card ${this.formData.status === opt.value ? 'selected' : ''}"
|
||||
@click="${() => this.handleStatusChange(opt.value)}"
|
||||
>
|
||||
<input
|
||||
type="radio"
|
||||
name="status"
|
||||
value="${opt.value}"
|
||||
?checked="${this.formData.status === opt.value}"
|
||||
/>
|
||||
<dees-icon .icon=${this.statusIcons[opt.value]} .iconSize=${24}></dees-icon>
|
||||
<span class="option-label">${opt.label}</span>
|
||||
</label>
|
||||
`)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="field-label required">Affected Services</label>
|
||||
<div class="services-grid">
|
||||
${this.availableServices.map(service => html`
|
||||
<div
|
||||
class="service-checkbox ${this.formData.affectedServices.includes(service.id) ? 'selected' : ''}"
|
||||
@click="${() => this.toggleService(service.id)}"
|
||||
>
|
||||
<input
|
||||
type="checkbox"
|
||||
id="service-${service.id}"
|
||||
?checked="${this.formData.affectedServices.includes(service.id)}"
|
||||
/>
|
||||
<span class="status-dot ${service.currentStatus}"></span>
|
||||
<label for="service-${service.id}">${service.displayName || service.name}</label>
|
||||
</div>
|
||||
`)}
|
||||
</div>
|
||||
${this.errors.affectedServices ? html`
|
||||
<div class="error-text">
|
||||
<dees-icon .icon=${'lucide:AlertCircle'} .iconSize=${12}></dees-icon>
|
||||
${this.errors.affectedServices}
|
||||
</div>
|
||||
` : ''}
|
||||
</div>
|
||||
|
||||
<dees-input-text
|
||||
key="impact"
|
||||
label="Impact Description"
|
||||
inputType="textarea"
|
||||
.value="${this.formData.impact}"
|
||||
placeholder="Describe how users are affected by this incident..."
|
||||
required
|
||||
@changeSubject="${this.handleImpactChange}"
|
||||
></dees-input-text>
|
||||
|
||||
<div class="form-section">
|
||||
<div class="section-title">
|
||||
<dees-icon .icon=${'lucide:FileSearch'} .iconSize=${16}></dees-icon>
|
||||
Resolution Details (Optional)
|
||||
</div>
|
||||
<div class="form-row">
|
||||
<dees-input-text
|
||||
key="rootCause"
|
||||
label="Root Cause"
|
||||
inputType="textarea"
|
||||
.value="${this.formData.rootCause || ''}"
|
||||
placeholder="What caused this incident..."
|
||||
@changeSubject="${this.handleRootCauseChange}"
|
||||
></dees-input-text>
|
||||
|
||||
<dees-input-text
|
||||
key="resolution"
|
||||
label="Resolution"
|
||||
inputType="textarea"
|
||||
.value="${this.formData.resolution || ''}"
|
||||
placeholder="How was this incident resolved..."
|
||||
@changeSubject="${this.handleResolutionChange}"
|
||||
></dees-input-text>
|
||||
</div>
|
||||
</div>
|
||||
</dees-form>
|
||||
</div>
|
||||
|
||||
<div class="form-actions">
|
||||
<dees-button type="discreet" @click="${this.handleCancel}" ?disabled="${this.loading}">
|
||||
Cancel
|
||||
</dees-button>
|
||||
<dees-button type="highlighted" @click="${this.handleSave}" ?disabled="${this.loading}">
|
||||
${this.loading ? html`<dees-spinner .size=${16}></dees-spinner>` : ''}
|
||||
${isEdit ? 'Update Incident' : 'Create Incident'}
|
||||
</dees-button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
private handleTitleChange(e: CustomEvent) {
|
||||
this.formData = { ...this.formData, title: e.detail };
|
||||
if (this.errors.title) {
|
||||
this.errors = { ...this.errors, title: '' };
|
||||
}
|
||||
}
|
||||
|
||||
private handleImpactChange(e: CustomEvent) {
|
||||
this.formData = { ...this.formData, impact: e.detail };
|
||||
if (this.errors.impact) {
|
||||
this.errors = { ...this.errors, impact: '' };
|
||||
}
|
||||
}
|
||||
|
||||
private handleRootCauseChange(e: CustomEvent) {
|
||||
this.formData = { ...this.formData, rootCause: e.detail };
|
||||
}
|
||||
|
||||
private handleResolutionChange(e: CustomEvent) {
|
||||
this.formData = { ...this.formData, resolution: e.detail };
|
||||
}
|
||||
|
||||
private handleSeverityChange(severity: TSeverity) {
|
||||
this.formData = { ...this.formData, severity };
|
||||
}
|
||||
|
||||
private handleStatusChange(status: TIncidentStatus) {
|
||||
this.formData = { ...this.formData, status };
|
||||
}
|
||||
|
||||
private toggleService(serviceId: string) {
|
||||
const current = this.formData.affectedServices;
|
||||
if (current.includes(serviceId)) {
|
||||
this.formData = {
|
||||
...this.formData,
|
||||
affectedServices: current.filter(id => id !== serviceId)
|
||||
};
|
||||
} else {
|
||||
this.formData = {
|
||||
...this.formData,
|
||||
affectedServices: [...current, serviceId]
|
||||
};
|
||||
}
|
||||
if (this.errors.affectedServices) {
|
||||
this.errors = { ...this.errors, affectedServices: '' };
|
||||
}
|
||||
}
|
||||
|
||||
private validate(): boolean {
|
||||
const errors: Record<string, string> = {};
|
||||
|
||||
if (!this.formData.title?.trim()) {
|
||||
errors.title = 'Title is required';
|
||||
}
|
||||
|
||||
if (this.formData.affectedServices.length === 0) {
|
||||
errors.affectedServices = 'At least one service must be selected';
|
||||
}
|
||||
|
||||
if (!this.formData.impact?.trim()) {
|
||||
errors.impact = 'Impact description is required';
|
||||
}
|
||||
|
||||
this.errors = errors;
|
||||
return Object.keys(errors).length === 0;
|
||||
}
|
||||
|
||||
private handleSave() {
|
||||
if (!this.validate()) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.dispatchEvent(new CustomEvent('incidentSave', {
|
||||
detail: { incident: { ...this.formData } },
|
||||
bubbles: true,
|
||||
composed: true
|
||||
}));
|
||||
}
|
||||
|
||||
private handleCancel() {
|
||||
this.dispatchEvent(new CustomEvent('incidentCancel', {
|
||||
bubbles: true,
|
||||
composed: true
|
||||
}));
|
||||
}
|
||||
|
||||
public reset() {
|
||||
this.formData = {
|
||||
title: '',
|
||||
severity: 'minor',
|
||||
status: 'investigating',
|
||||
affectedServices: [],
|
||||
impact: '',
|
||||
rootCause: '',
|
||||
resolution: '',
|
||||
};
|
||||
this.errors = {};
|
||||
}
|
||||
|
||||
public setIncident(incident: IIncidentFormData) {
|
||||
this.formData = { ...incident };
|
||||
this.errors = {};
|
||||
}
|
||||
}
|
||||
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
|
||||
}));
|
||||
}
|
||||
}
|
||||
1
ts_web/elements/upladmin-incident-update/index.ts
Normal file
1
ts_web/elements/upladmin-incident-update/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from './upladmin-incident-update.js';
|
||||
@@ -0,0 +1,91 @@
|
||||
import { html, css, cssManager } from '@design.estate/dees-element';
|
||||
import type { IIncidentDetails } from '../../interfaces/index.js';
|
||||
import './upladmin-incident-update.js';
|
||||
|
||||
export const demoFunc = () => html`
|
||||
<style>
|
||||
${css`
|
||||
.demo-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 32px;
|
||||
padding: 24px;
|
||||
max-width: 800px;
|
||||
margin: 0 auto;
|
||||
background: ${cssManager.bdTheme('#fafafa', '#0a0a0a')};
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
.demo-section {
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.demo-section h3 {
|
||||
margin: 0 0 16px 0;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: ${cssManager.bdTheme('#71717a', '#a1a1aa')};
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.05em;
|
||||
}
|
||||
`}
|
||||
</style>
|
||||
|
||||
<div class="demo-container">
|
||||
<div class="demo-section">
|
||||
<h3>Update Active Incident (Critical)</h3>
|
||||
<upladmin-incident-update
|
||||
.incident=${{
|
||||
id: 'inc-1',
|
||||
title: 'Database Connection Pool Exhaustion',
|
||||
status: 'investigating',
|
||||
severity: 'critical',
|
||||
affectedServices: ['db', 'api', 'web'],
|
||||
startTime: Date.now() - 30 * 60 * 1000,
|
||||
impact: 'All database-dependent services are experiencing failures.',
|
||||
updates: [
|
||||
{ id: 'u1', timestamp: Date.now() - 30 * 60 * 1000, status: 'investigating', message: 'We are investigating reports of service failures.' },
|
||||
],
|
||||
} as IIncidentDetails}
|
||||
></upladmin-incident-update>
|
||||
</div>
|
||||
|
||||
<div class="demo-section">
|
||||
<h3>Update Active Incident (Minor)</h3>
|
||||
<upladmin-incident-update
|
||||
.incident=${{
|
||||
id: 'inc-2',
|
||||
title: 'Elevated API Response Times',
|
||||
status: 'identified',
|
||||
severity: 'minor',
|
||||
affectedServices: ['api'],
|
||||
startTime: Date.now() - 2 * 60 * 60 * 1000,
|
||||
impact: 'API responses may be slower than usual.',
|
||||
updates: [
|
||||
{ id: 'u2', timestamp: Date.now() - 1 * 60 * 60 * 1000, status: 'identified', message: 'Issue identified as increased traffic from a specific client.' },
|
||||
{ id: 'u3', timestamp: Date.now() - 2 * 60 * 60 * 1000, status: 'investigating', message: 'We are investigating elevated response times.' },
|
||||
],
|
||||
} as IIncidentDetails}
|
||||
></upladmin-incident-update>
|
||||
</div>
|
||||
|
||||
<div class="demo-section">
|
||||
<h3>Update Maintenance Window</h3>
|
||||
<upladmin-incident-update
|
||||
.incident=${{
|
||||
id: 'inc-3',
|
||||
title: 'Scheduled Infrastructure Upgrade',
|
||||
status: 'monitoring',
|
||||
severity: 'maintenance',
|
||||
affectedServices: ['api', 'web', 'cdn'],
|
||||
startTime: Date.now() - 45 * 60 * 1000,
|
||||
impact: 'Brief interruptions may occur during the upgrade.',
|
||||
updates: [
|
||||
{ id: 'u4', timestamp: Date.now() - 30 * 60 * 1000, status: 'monitoring', message: 'Upgrade complete. Monitoring for any issues.' },
|
||||
{ id: 'u5', timestamp: Date.now() - 45 * 60 * 1000, status: 'investigating', message: 'Starting scheduled infrastructure upgrade.' },
|
||||
],
|
||||
} as IIncidentDetails}
|
||||
></upladmin-incident-update>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
@@ -0,0 +1,478 @@
|
||||
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 { IIncidentUpdateFormData, IIncidentDetails } from '../../interfaces/index.js';
|
||||
import { demoFunc } from './upladmin-incident-update.demo.js';
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
'upladmin-incident-update': UpladminIncidentUpdate;
|
||||
}
|
||||
}
|
||||
|
||||
type TIncidentStatus = 'investigating' | 'identified' | 'monitoring' | 'resolved' | 'postmortem';
|
||||
|
||||
@customElement('upladmin-incident-update')
|
||||
export class UpladminIncidentUpdate extends DeesElement {
|
||||
public static demo = demoFunc;
|
||||
|
||||
@property({ type: Object })
|
||||
accessor incident: IIncidentDetails | null = null;
|
||||
|
||||
@property({ type: Boolean })
|
||||
accessor loading: boolean = false;
|
||||
|
||||
@state()
|
||||
accessor formData: IIncidentUpdateFormData = {
|
||||
status: 'investigating',
|
||||
message: '',
|
||||
author: '',
|
||||
};
|
||||
|
||||
@state()
|
||||
accessor errors: Record<string, string> = {};
|
||||
|
||||
private statusIcons: Record<TIncidentStatus, string> = {
|
||||
investigating: 'lucide:Search',
|
||||
identified: 'lucide:Target',
|
||||
monitoring: 'lucide:Eye',
|
||||
resolved: 'lucide:CheckCircle',
|
||||
postmortem: 'lucide:FileText',
|
||||
};
|
||||
|
||||
public static styles = [
|
||||
plugins.domtools.elementBasic.staticStyles,
|
||||
sharedStyles.commonStyles,
|
||||
css`
|
||||
:host {
|
||||
display: block;
|
||||
font-family: ${unsafeCSS(sharedStyles.fonts.base)};
|
||||
}
|
||||
|
||||
.update-container {
|
||||
background: ${sharedStyles.colors.background.secondary};
|
||||
border: 1px solid ${sharedStyles.colors.border.default};
|
||||
border-radius: ${unsafeCSS(sharedStyles.borderRadius.lg)};
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.update-header {
|
||||
padding: ${unsafeCSS(sharedStyles.spacing.lg)};
|
||||
border-bottom: 1px solid ${sharedStyles.colors.border.default};
|
||||
background: ${sharedStyles.colors.background.muted};
|
||||
}
|
||||
|
||||
.update-title-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: ${unsafeCSS(sharedStyles.spacing.md)};
|
||||
margin-bottom: ${unsafeCSS(sharedStyles.spacing.sm)};
|
||||
}
|
||||
|
||||
.update-title-row dees-icon {
|
||||
--icon-color: ${cssManager.bdTheme('#3b82f6', '#60a5fa')};
|
||||
}
|
||||
|
||||
.update-title {
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
color: ${sharedStyles.colors.text.primary};
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.incident-info {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: ${unsafeCSS(sharedStyles.spacing.sm)};
|
||||
padding-left: 36px;
|
||||
}
|
||||
|
||||
.incident-name {
|
||||
font-size: 14px;
|
||||
color: ${sharedStyles.colors.text.secondary};
|
||||
}
|
||||
|
||||
.severity-badge {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
padding: 4px 10px;
|
||||
font-size: 11px;
|
||||
font-weight: 500;
|
||||
border-radius: 9999px;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.severity-badge dees-icon {
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.severity-badge.critical {
|
||||
background: ${cssManager.bdTheme('rgba(239, 68, 68, 0.1)', 'rgba(239, 68, 68, 0.2)')};
|
||||
color: ${sharedStyles.colors.status.majorOutage};
|
||||
--icon-color: ${sharedStyles.colors.status.majorOutage};
|
||||
}
|
||||
|
||||
.severity-badge.major {
|
||||
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};
|
||||
}
|
||||
|
||||
.severity-badge.minor {
|
||||
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};
|
||||
}
|
||||
|
||||
.severity-badge.maintenance {
|
||||
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};
|
||||
}
|
||||
|
||||
.update-body {
|
||||
display: grid;
|
||||
gap: ${unsafeCSS(sharedStyles.spacing.lg)};
|
||||
padding: ${unsafeCSS(sharedStyles.spacing.lg)};
|
||||
}
|
||||
|
||||
dees-form {
|
||||
display: contents;
|
||||
}
|
||||
|
||||
.status-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(130px, 1fr));
|
||||
gap: ${unsafeCSS(sharedStyles.spacing.sm)};
|
||||
}
|
||||
|
||||
.status-option {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
padding: 18px 14px;
|
||||
background: ${sharedStyles.colors.background.primary};
|
||||
border: 2px solid ${sharedStyles.colors.border.default};
|
||||
border-radius: ${unsafeCSS(sharedStyles.borderRadius.base)};
|
||||
cursor: pointer;
|
||||
transition: all ${unsafeCSS(sharedStyles.durations.fast)} ${unsafeCSS(sharedStyles.easings.default)};
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.status-option:hover {
|
||||
border-color: ${sharedStyles.colors.border.strong};
|
||||
background: ${sharedStyles.colors.background.muted};
|
||||
}
|
||||
|
||||
.status-option.selected {
|
||||
border-color: ${sharedStyles.colors.accent.primary};
|
||||
background: ${cssManager.bdTheme('rgba(59, 130, 246, 0.05)', 'rgba(96, 165, 250, 0.1)')};
|
||||
}
|
||||
|
||||
.status-option input {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.status-option.investigating dees-icon { --icon-color: ${sharedStyles.colors.status.partialOutage}; }
|
||||
.status-option.identified dees-icon { --icon-color: ${sharedStyles.colors.status.degraded}; }
|
||||
.status-option.monitoring dees-icon { --icon-color: ${sharedStyles.colors.status.maintenance}; }
|
||||
.status-option.resolved dees-icon { --icon-color: ${sharedStyles.colors.status.operational}; }
|
||||
.status-option.postmortem dees-icon { --icon-color: #a855f7; }
|
||||
|
||||
.status-label {
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
color: ${sharedStyles.colors.text.primary};
|
||||
}
|
||||
|
||||
.status-desc {
|
||||
font-size: 11px;
|
||||
color: ${sharedStyles.colors.text.muted};
|
||||
line-height: 1.3;
|
||||
}
|
||||
|
||||
.field-label {
|
||||
display: block;
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
color: ${sharedStyles.colors.text.primary};
|
||||
margin-bottom: ${unsafeCSS(sharedStyles.spacing.xs)};
|
||||
}
|
||||
|
||||
.field-label.required::after {
|
||||
content: ' *';
|
||||
color: ${sharedStyles.colors.accent.danger};
|
||||
}
|
||||
|
||||
.template-section {
|
||||
margin-bottom: ${unsafeCSS(sharedStyles.spacing.sm)};
|
||||
}
|
||||
|
||||
.template-label {
|
||||
font-size: 12px;
|
||||
color: ${sharedStyles.colors.text.muted};
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.template-buttons {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.template-btn {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
padding: 8px 14px;
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
font-family: ${unsafeCSS(sharedStyles.fonts.base)};
|
||||
color: ${sharedStyles.colors.text.secondary};
|
||||
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)};
|
||||
}
|
||||
|
||||
.template-btn:hover {
|
||||
background: ${sharedStyles.colors.background.muted};
|
||||
border-color: ${sharedStyles.colors.border.strong};
|
||||
color: ${sharedStyles.colors.text.primary};
|
||||
}
|
||||
|
||||
.template-btn dees-icon {
|
||||
--icon-color: currentColor;
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
.update-actions {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: ${unsafeCSS(sharedStyles.spacing.sm)};
|
||||
padding: ${unsafeCSS(sharedStyles.spacing.md)} ${unsafeCSS(sharedStyles.spacing.lg)};
|
||||
border-top: 1px solid ${sharedStyles.colors.border.default};
|
||||
background: ${sharedStyles.colors.background.muted};
|
||||
}
|
||||
|
||||
/* Style dees-input components */
|
||||
dees-input-text {
|
||||
--dees-input-background: ${sharedStyles.colors.background.primary};
|
||||
--dees-input-border-color: ${sharedStyles.colors.border.default};
|
||||
}
|
||||
`
|
||||
];
|
||||
|
||||
async connectedCallback() {
|
||||
await super.connectedCallback();
|
||||
if (this.incident) {
|
||||
this.formData = {
|
||||
...this.formData,
|
||||
status: this.incident.status,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
updated(changedProperties: Map<string, unknown>) {
|
||||
if (changedProperties.has('incident') && this.incident) {
|
||||
this.formData = {
|
||||
...this.formData,
|
||||
status: this.incident.status,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
public render(): TemplateResult {
|
||||
if (!this.incident) {
|
||||
return html`<div class="update-container">No incident selected</div>`;
|
||||
}
|
||||
|
||||
const statusOptions: Array<{ value: TIncidentStatus; label: string; desc: string }> = [
|
||||
{ value: 'investigating', label: 'Investigating', desc: 'Looking into the issue' },
|
||||
{ value: 'identified', label: 'Identified', desc: 'Root cause found' },
|
||||
{ value: 'monitoring', label: 'Monitoring', desc: 'Fix applied, watching' },
|
||||
{ value: 'resolved', label: 'Resolved', desc: 'Issue is fixed' },
|
||||
{ value: 'postmortem', label: 'Postmortem', desc: 'Analysis complete' },
|
||||
];
|
||||
|
||||
const templates: Array<{ icon: string; label: string; message: string }> = [
|
||||
{ icon: 'lucide:Search', label: 'Started investigating', message: 'We are currently investigating this issue.' },
|
||||
{ icon: 'lucide:Target', label: 'Issue identified', message: 'We have identified the root cause and are working on a fix.' },
|
||||
{ icon: 'lucide:Rocket', label: 'Fix deployed', message: 'A fix has been deployed. We are monitoring the results.' },
|
||||
{ icon: 'lucide:CheckCircle', label: 'Resolved', message: 'This incident has been resolved. All systems are operating normally.' },
|
||||
];
|
||||
|
||||
const severityIcons: Record<string, string> = {
|
||||
critical: 'lucide:AlertCircle',
|
||||
major: 'lucide:AlertTriangle',
|
||||
minor: 'lucide:Info',
|
||||
maintenance: 'lucide:Wrench',
|
||||
};
|
||||
|
||||
return html`
|
||||
<div class="update-container">
|
||||
<div class="update-header">
|
||||
<div class="update-title-row">
|
||||
<dees-icon .icon=${'lucide:MessageSquarePlus'} .iconSize=${24}></dees-icon>
|
||||
<h2 class="update-title">Post Update</h2>
|
||||
</div>
|
||||
<div class="incident-info">
|
||||
<span class="severity-badge ${this.incident.severity}">
|
||||
<dees-icon .icon=${severityIcons[this.incident.severity]} .iconSize=${12}></dees-icon>
|
||||
${this.incident.severity}
|
||||
</span>
|
||||
<span class="incident-name">${this.incident.title}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="update-body">
|
||||
<dees-form>
|
||||
<div>
|
||||
<label class="field-label required">Status</label>
|
||||
<div class="status-grid">
|
||||
${statusOptions.map(opt => html`
|
||||
<label
|
||||
class="status-option ${opt.value} ${this.formData.status === opt.value ? 'selected' : ''}"
|
||||
@click="${() => this.handleStatusChange(opt.value)}"
|
||||
>
|
||||
<input
|
||||
type="radio"
|
||||
name="status"
|
||||
value="${opt.value}"
|
||||
?checked="${this.formData.status === opt.value}"
|
||||
/>
|
||||
<dees-icon .icon=${this.statusIcons[opt.value]} .iconSize=${24}></dees-icon>
|
||||
<span class="status-label">${opt.label}</span>
|
||||
<span class="status-desc">${opt.desc}</span>
|
||||
</label>
|
||||
`)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="field-label required">Update Message</label>
|
||||
<div class="template-section">
|
||||
<div class="template-label">Quick templates:</div>
|
||||
<div class="template-buttons">
|
||||
${templates.map(tpl => html`
|
||||
<button type="button" class="template-btn" @click="${() => this.applyTemplate(tpl.message)}">
|
||||
<dees-icon .icon=${tpl.icon} .iconSize=${12}></dees-icon>
|
||||
${tpl.label}
|
||||
</button>
|
||||
`)}
|
||||
</div>
|
||||
</div>
|
||||
<dees-input-text
|
||||
key="message"
|
||||
inputType="textarea"
|
||||
.value="${this.formData.message}"
|
||||
placeholder="Provide an update on the incident status..."
|
||||
required
|
||||
@changeSubject="${this.handleMessageChange}"
|
||||
></dees-input-text>
|
||||
</div>
|
||||
|
||||
<dees-input-text
|
||||
key="author"
|
||||
label="Author (Optional)"
|
||||
.value="${this.formData.author || ''}"
|
||||
placeholder="Your name or team name"
|
||||
@changeSubject="${this.handleAuthorChange}"
|
||||
></dees-input-text>
|
||||
</dees-form>
|
||||
</div>
|
||||
|
||||
<div class="update-actions">
|
||||
<dees-button type="discreet" @click="${this.handleCancel}" ?disabled="${this.loading}">
|
||||
Cancel
|
||||
</dees-button>
|
||||
${this.formData.status === 'resolved' ? html`
|
||||
<dees-button type="highlighted" @click="${this.handlePost}" ?disabled="${this.loading}" style="--dees-button-background: ${sharedStyles.colors.status.operational}">
|
||||
${this.loading ? html`<dees-spinner .size=${16}></dees-spinner>` : html`<dees-icon .icon=${'lucide:CheckCircle'} .iconSize=${16}></dees-icon>`}
|
||||
Resolve Incident
|
||||
</dees-button>
|
||||
` : html`
|
||||
<dees-button type="highlighted" @click="${this.handlePost}" ?disabled="${this.loading}">
|
||||
${this.loading ? html`<dees-spinner .size=${16}></dees-spinner>` : html`<dees-icon .icon=${'lucide:Send'} .iconSize=${16}></dees-icon>`}
|
||||
Post Update
|
||||
</dees-button>
|
||||
`}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
private handleMessageChange(e: CustomEvent) {
|
||||
this.formData = { ...this.formData, message: e.detail };
|
||||
if (this.errors.message) {
|
||||
this.errors = { ...this.errors, message: '' };
|
||||
}
|
||||
}
|
||||
|
||||
private handleAuthorChange(e: CustomEvent) {
|
||||
this.formData = { ...this.formData, author: e.detail };
|
||||
}
|
||||
|
||||
private handleStatusChange(status: TIncidentStatus) {
|
||||
this.formData = { ...this.formData, status };
|
||||
}
|
||||
|
||||
private applyTemplate(message: string) {
|
||||
this.formData = { ...this.formData, message };
|
||||
}
|
||||
|
||||
private validate(): boolean {
|
||||
const errors: Record<string, string> = {};
|
||||
|
||||
if (!this.formData.message?.trim()) {
|
||||
errors.message = 'Update message is required';
|
||||
}
|
||||
|
||||
this.errors = errors;
|
||||
return Object.keys(errors).length === 0;
|
||||
}
|
||||
|
||||
private handlePost() {
|
||||
if (!this.validate()) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.dispatchEvent(new CustomEvent('updatePost', {
|
||||
detail: {
|
||||
incidentId: this.incident?.id,
|
||||
update: { ...this.formData }
|
||||
},
|
||||
bubbles: true,
|
||||
composed: true
|
||||
}));
|
||||
}
|
||||
|
||||
private handleCancel() {
|
||||
this.dispatchEvent(new CustomEvent('updateCancel', {
|
||||
bubbles: true,
|
||||
composed: true
|
||||
}));
|
||||
}
|
||||
|
||||
public reset() {
|
||||
this.formData = {
|
||||
status: this.incident?.status || 'investigating',
|
||||
message: '',
|
||||
author: '',
|
||||
};
|
||||
this.errors = {};
|
||||
}
|
||||
}
|
||||
1
ts_web/elements/upladmin-monitor-form/index.ts
Normal file
1
ts_web/elements/upladmin-monitor-form/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from './upladmin-monitor-form.js';
|
||||
@@ -0,0 +1,66 @@
|
||||
import { html, css, cssManager } from '@design.estate/dees-element';
|
||||
import type { IServiceStatus } from '../../interfaces/index.js';
|
||||
import './upladmin-monitor-form.js';
|
||||
|
||||
export const demoFunc = () => html`
|
||||
<style>
|
||||
${css`
|
||||
.demo-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 32px;
|
||||
padding: 24px;
|
||||
max-width: 900px;
|
||||
margin: 0 auto;
|
||||
background: ${cssManager.bdTheme('#fafafa', '#0a0a0a')};
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
.demo-section {
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.demo-section h3 {
|
||||
margin: 0 0 16px 0;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: ${cssManager.bdTheme('#71717a', '#a1a1aa')};
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.05em;
|
||||
}
|
||||
`}
|
||||
</style>
|
||||
|
||||
<div class="demo-container">
|
||||
<div class="demo-section">
|
||||
<h3>Create New Monitor</h3>
|
||||
<upladmin-monitor-form
|
||||
.availableMonitors=${[
|
||||
{ id: 'api-server', name: 'api-server', displayName: 'API Server', currentStatus: 'operational', lastChecked: Date.now(), uptime30d: 99.98, uptime90d: 99.95, responseTime: 45 },
|
||||
{ id: 'database', name: 'database', displayName: 'Database', currentStatus: 'operational', lastChecked: Date.now(), uptime30d: 99.99, uptime90d: 99.98, responseTime: 5 },
|
||||
] as IServiceStatus[]}
|
||||
.categories=${['Core Services', 'Infrastructure', 'External Services', 'Web Services']}
|
||||
></upladmin-monitor-form>
|
||||
</div>
|
||||
|
||||
<div class="demo-section">
|
||||
<h3>Edit Existing Monitor</h3>
|
||||
<upladmin-monitor-form
|
||||
.monitor=${{
|
||||
id: 'cdn',
|
||||
name: 'cdn',
|
||||
displayName: 'Content Delivery Network',
|
||||
description: 'Global CDN for static assets and media files',
|
||||
category: 'Infrastructure',
|
||||
dependencies: ['api-server'],
|
||||
currentStatus: 'degraded',
|
||||
}}
|
||||
.availableMonitors=${[
|
||||
{ id: 'api-server', name: 'api-server', displayName: 'API Server', currentStatus: 'operational', lastChecked: Date.now(), uptime30d: 99.98, uptime90d: 99.95, responseTime: 45 },
|
||||
{ id: 'database', name: 'database', displayName: 'Database', currentStatus: 'operational', lastChecked: Date.now(), uptime30d: 99.99, uptime90d: 99.98, responseTime: 5 },
|
||||
] as IServiceStatus[]}
|
||||
.categories=${['Core Services', 'Infrastructure', 'External Services', 'Web Services']}
|
||||
></upladmin-monitor-form>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
431
ts_web/elements/upladmin-monitor-form/upladmin-monitor-form.ts
Normal file
431
ts_web/elements/upladmin-monitor-form/upladmin-monitor-form.ts
Normal file
@@ -0,0 +1,431 @@
|
||||
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 { IMonitorFormData, IServiceStatus } from '../../interfaces/index.js';
|
||||
import { demoFunc } from './upladmin-monitor-form.demo.js';
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
'upladmin-monitor-form': UpladminMonitorForm;
|
||||
}
|
||||
}
|
||||
|
||||
type TStatusType = 'operational' | 'degraded' | 'partial_outage' | 'major_outage' | 'maintenance';
|
||||
|
||||
@customElement('upladmin-monitor-form')
|
||||
export class UpladminMonitorForm extends DeesElement {
|
||||
public static demo = demoFunc;
|
||||
|
||||
@property({ type: Object })
|
||||
accessor monitor: IMonitorFormData | null = null;
|
||||
|
||||
@property({ type: Array })
|
||||
accessor availableMonitors: IServiceStatus[] = [];
|
||||
|
||||
@property({ type: Array })
|
||||
accessor categories: string[] = [];
|
||||
|
||||
@property({ type: Boolean })
|
||||
accessor loading: boolean = false;
|
||||
|
||||
@state()
|
||||
accessor formData: IMonitorFormData = {
|
||||
name: '',
|
||||
displayName: '',
|
||||
description: '',
|
||||
category: '',
|
||||
dependencies: [],
|
||||
currentStatus: 'operational',
|
||||
};
|
||||
|
||||
@state()
|
||||
accessor errors: Record<string, string> = {};
|
||||
|
||||
private statusIcons: Record<TStatusType, string> = {
|
||||
operational: 'lucide:CheckCircle',
|
||||
degraded: 'lucide:AlertTriangle',
|
||||
partial_outage: 'lucide:AlertOctagon',
|
||||
major_outage: 'lucide:XCircle',
|
||||
maintenance: 'lucide:Wrench',
|
||||
};
|
||||
|
||||
public static styles = [
|
||||
plugins.domtools.elementBasic.staticStyles,
|
||||
sharedStyles.commonStyles,
|
||||
css`
|
||||
:host {
|
||||
display: block;
|
||||
font-family: ${unsafeCSS(sharedStyles.fonts.base)};
|
||||
}
|
||||
|
||||
.form-container {
|
||||
background: ${sharedStyles.colors.background.secondary};
|
||||
border: 1px solid ${sharedStyles.colors.border.default};
|
||||
border-radius: ${unsafeCSS(sharedStyles.borderRadius.lg)};
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.form-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: ${unsafeCSS(sharedStyles.spacing.md)};
|
||||
padding: ${unsafeCSS(sharedStyles.spacing.lg)};
|
||||
border-bottom: 1px solid ${sharedStyles.colors.border.default};
|
||||
background: ${sharedStyles.colors.background.muted};
|
||||
}
|
||||
|
||||
.form-header dees-icon {
|
||||
--icon-color: ${cssManager.bdTheme('#3b82f6', '#60a5fa')};
|
||||
}
|
||||
|
||||
.form-title-wrapper {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.form-title {
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
color: ${sharedStyles.colors.text.primary};
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.form-subtitle {
|
||||
font-size: 13px;
|
||||
color: ${sharedStyles.colors.text.muted};
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
.form-body {
|
||||
display: grid;
|
||||
gap: ${unsafeCSS(sharedStyles.spacing.lg)};
|
||||
padding: ${unsafeCSS(sharedStyles.spacing.lg)};
|
||||
}
|
||||
|
||||
dees-form {
|
||||
display: contents;
|
||||
}
|
||||
|
||||
.form-row {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: ${unsafeCSS(sharedStyles.spacing.md)};
|
||||
}
|
||||
|
||||
@media (max-width: 600px) {
|
||||
.form-row {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
.form-actions {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: ${unsafeCSS(sharedStyles.spacing.sm)};
|
||||
padding: ${unsafeCSS(sharedStyles.spacing.md)} ${unsafeCSS(sharedStyles.spacing.lg)};
|
||||
border-top: 1px solid ${sharedStyles.colors.border.default};
|
||||
background: ${sharedStyles.colors.background.muted};
|
||||
}
|
||||
|
||||
.status-section {
|
||||
margin-top: ${unsafeCSS(sharedStyles.spacing.sm)};
|
||||
}
|
||||
|
||||
.status-options {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(140px, 1fr));
|
||||
gap: ${unsafeCSS(sharedStyles.spacing.sm)};
|
||||
}
|
||||
|
||||
.status-option {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
padding: 14px 16px;
|
||||
background: ${sharedStyles.colors.background.primary};
|
||||
border: 2px solid ${sharedStyles.colors.border.default};
|
||||
border-radius: ${unsafeCSS(sharedStyles.borderRadius.base)};
|
||||
cursor: pointer;
|
||||
transition: all ${unsafeCSS(sharedStyles.durations.fast)} ${unsafeCSS(sharedStyles.easings.default)};
|
||||
}
|
||||
|
||||
.status-option:hover {
|
||||
border-color: ${sharedStyles.colors.border.strong};
|
||||
background: ${sharedStyles.colors.background.muted};
|
||||
}
|
||||
|
||||
.status-option.selected {
|
||||
border-color: ${sharedStyles.colors.accent.primary};
|
||||
background: ${cssManager.bdTheme('rgba(59, 130, 246, 0.05)', 'rgba(96, 165, 250, 0.1)')};
|
||||
}
|
||||
|
||||
.status-option input {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.status-option dees-icon {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.status-option.operational dees-icon { --icon-color: ${sharedStyles.colors.status.operational}; }
|
||||
.status-option.degraded dees-icon { --icon-color: ${sharedStyles.colors.status.degraded}; }
|
||||
.status-option.partial_outage dees-icon { --icon-color: ${sharedStyles.colors.status.partialOutage}; }
|
||||
.status-option.major_outage dees-icon { --icon-color: ${sharedStyles.colors.status.majorOutage}; }
|
||||
.status-option.maintenance dees-icon { --icon-color: ${sharedStyles.colors.status.maintenance}; }
|
||||
|
||||
.status-label {
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
color: ${sharedStyles.colors.text.primary};
|
||||
}
|
||||
|
||||
.field-label {
|
||||
display: block;
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
color: ${sharedStyles.colors.text.primary};
|
||||
margin-bottom: ${unsafeCSS(sharedStyles.spacing.sm)};
|
||||
}
|
||||
|
||||
.field-label.required::after {
|
||||
content: ' *';
|
||||
color: ${sharedStyles.colors.accent.danger};
|
||||
}
|
||||
|
||||
/* Style dees-input components */
|
||||
dees-input-text,
|
||||
dees-input-dropdown {
|
||||
--dees-input-background: ${sharedStyles.colors.background.primary};
|
||||
--dees-input-border-color: ${sharedStyles.colors.border.default};
|
||||
}
|
||||
`
|
||||
];
|
||||
|
||||
async connectedCallback() {
|
||||
await super.connectedCallback();
|
||||
if (this.monitor) {
|
||||
this.formData = { ...this.monitor };
|
||||
}
|
||||
}
|
||||
|
||||
updated(changedProperties: Map<string, unknown>) {
|
||||
if (changedProperties.has('monitor') && this.monitor) {
|
||||
this.formData = { ...this.monitor };
|
||||
}
|
||||
}
|
||||
|
||||
public render(): TemplateResult {
|
||||
const isEdit = !!this.monitor?.id;
|
||||
const statusOptions: Array<{ value: TStatusType; label: string }> = [
|
||||
{ value: 'operational', label: 'Operational' },
|
||||
{ value: 'degraded', label: 'Degraded' },
|
||||
{ value: 'partial_outage', label: 'Partial Outage' },
|
||||
{ value: 'major_outage', label: 'Major Outage' },
|
||||
{ value: 'maintenance', label: 'Maintenance' },
|
||||
];
|
||||
|
||||
const categoryOptions = this.categories.map(cat => ({ key: cat, option: cat, payload: null }));
|
||||
const dependencyOptions = this.availableMonitors
|
||||
.filter(m => m.id !== this.monitor?.id)
|
||||
.map(m => ({ key: m.id, option: m.displayName || m.name, payload: null }));
|
||||
|
||||
return html`
|
||||
<div class="form-container">
|
||||
<div class="form-header">
|
||||
<dees-icon .icon=${isEdit ? 'lucide:Pencil' : 'lucide:Plus'} .iconSize=${24}></dees-icon>
|
||||
<div class="form-title-wrapper">
|
||||
<h2 class="form-title">${isEdit ? 'Edit Monitor' : 'Create Monitor'}</h2>
|
||||
<p class="form-subtitle">
|
||||
${isEdit ? 'Update the monitor configuration' : 'Add a new service to monitor'}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-body">
|
||||
<dees-form>
|
||||
<div class="form-row">
|
||||
<dees-input-text
|
||||
key="name"
|
||||
label="Internal Name"
|
||||
.value="${this.formData.name}"
|
||||
placeholder="api-server"
|
||||
required
|
||||
description="Lowercase, no spaces. Used as identifier."
|
||||
@changeSubject="${this.handleNameChange}"
|
||||
></dees-input-text>
|
||||
|
||||
<dees-input-text
|
||||
key="displayName"
|
||||
label="Display Name"
|
||||
.value="${this.formData.displayName}"
|
||||
placeholder="API Server"
|
||||
required
|
||||
description="Human-readable name shown to users."
|
||||
@changeSubject="${this.handleDisplayNameChange}"
|
||||
></dees-input-text>
|
||||
</div>
|
||||
|
||||
<dees-input-text
|
||||
key="description"
|
||||
label="Description"
|
||||
inputType="textarea"
|
||||
.value="${this.formData.description || ''}"
|
||||
placeholder="Brief description of what this service does..."
|
||||
@changeSubject="${this.handleDescriptionChange}"
|
||||
></dees-input-text>
|
||||
|
||||
<div class="form-row">
|
||||
<dees-input-dropdown
|
||||
key="category"
|
||||
label="Category"
|
||||
.options="${categoryOptions}"
|
||||
.selectedOption="${this.formData.category || ''}"
|
||||
placeholder="Select category..."
|
||||
@selectedOption="${this.handleCategoryChange}"
|
||||
></dees-input-dropdown>
|
||||
|
||||
<dees-input-dropdown
|
||||
key="dependencies"
|
||||
label="Dependencies"
|
||||
.options="${dependencyOptions}"
|
||||
.selectedOptions="${this.formData.dependencies || []}"
|
||||
multiple
|
||||
description="Services this monitor depends on."
|
||||
@selectedOption="${this.handleDependenciesChange}"
|
||||
></dees-input-dropdown>
|
||||
</div>
|
||||
|
||||
<div class="status-section">
|
||||
<label class="field-label required">Current Status</label>
|
||||
<div class="status-options">
|
||||
${statusOptions.map(opt => html`
|
||||
<label
|
||||
class="status-option ${opt.value} ${this.formData.currentStatus === opt.value ? 'selected' : ''}"
|
||||
@click="${() => this.handleStatusChange(opt.value)}"
|
||||
>
|
||||
<input
|
||||
type="radio"
|
||||
name="currentStatus"
|
||||
value="${opt.value}"
|
||||
?checked="${this.formData.currentStatus === opt.value}"
|
||||
/>
|
||||
<dees-icon .icon=${this.statusIcons[opt.value]} .iconSize=${20}></dees-icon>
|
||||
<span class="status-label">${opt.label}</span>
|
||||
</label>
|
||||
`)}
|
||||
</div>
|
||||
</div>
|
||||
</dees-form>
|
||||
</div>
|
||||
|
||||
<div class="form-actions">
|
||||
<dees-button type="discreet" @click="${this.handleCancel}" ?disabled="${this.loading}">
|
||||
Cancel
|
||||
</dees-button>
|
||||
<dees-button type="highlighted" @click="${this.handleSave}" ?disabled="${this.loading}">
|
||||
${this.loading ? html`<dees-spinner .size=${16}></dees-spinner>` : ''}
|
||||
${isEdit ? 'Update Monitor' : 'Create Monitor'}
|
||||
</dees-button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
private handleNameChange(e: CustomEvent) {
|
||||
this.formData = { ...this.formData, name: e.detail };
|
||||
if (this.errors.name) {
|
||||
this.errors = { ...this.errors, name: '' };
|
||||
}
|
||||
}
|
||||
|
||||
private handleDisplayNameChange(e: CustomEvent) {
|
||||
this.formData = { ...this.formData, displayName: e.detail };
|
||||
if (this.errors.displayName) {
|
||||
this.errors = { ...this.errors, displayName: '' };
|
||||
}
|
||||
}
|
||||
|
||||
private handleDescriptionChange(e: CustomEvent) {
|
||||
this.formData = { ...this.formData, description: e.detail };
|
||||
}
|
||||
|
||||
private handleCategoryChange(e: CustomEvent) {
|
||||
this.formData = { ...this.formData, category: e.detail };
|
||||
}
|
||||
|
||||
private handleDependenciesChange(e: CustomEvent) {
|
||||
const selected = e.detail;
|
||||
if (Array.isArray(selected)) {
|
||||
this.formData = { ...this.formData, dependencies: selected };
|
||||
} else if (selected) {
|
||||
// Single selection mode, convert to array
|
||||
this.formData = { ...this.formData, dependencies: [selected] };
|
||||
}
|
||||
}
|
||||
|
||||
private handleStatusChange(status: TStatusType) {
|
||||
this.formData = { ...this.formData, currentStatus: status };
|
||||
}
|
||||
|
||||
private validate(): boolean {
|
||||
const errors: Record<string, string> = {};
|
||||
|
||||
if (!this.formData.name?.trim()) {
|
||||
errors.name = 'Name is required';
|
||||
} else if (!/^[a-z0-9-]+$/.test(this.formData.name)) {
|
||||
errors.name = 'Name must be lowercase letters, numbers, and hyphens only';
|
||||
}
|
||||
|
||||
if (!this.formData.displayName?.trim()) {
|
||||
errors.displayName = 'Display name is required';
|
||||
}
|
||||
|
||||
this.errors = errors;
|
||||
return Object.keys(errors).length === 0;
|
||||
}
|
||||
|
||||
private handleSave() {
|
||||
if (!this.validate()) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.dispatchEvent(new CustomEvent('monitorSave', {
|
||||
detail: { monitor: { ...this.formData } },
|
||||
bubbles: true,
|
||||
composed: true
|
||||
}));
|
||||
}
|
||||
|
||||
private handleCancel() {
|
||||
this.dispatchEvent(new CustomEvent('monitorCancel', {
|
||||
bubbles: true,
|
||||
composed: true
|
||||
}));
|
||||
}
|
||||
|
||||
public reset() {
|
||||
this.formData = {
|
||||
name: '',
|
||||
displayName: '',
|
||||
description: '',
|
||||
category: '',
|
||||
dependencies: [],
|
||||
currentStatus: 'operational',
|
||||
};
|
||||
this.errors = {};
|
||||
}
|
||||
|
||||
public setMonitor(monitor: IMonitorFormData) {
|
||||
this.formData = { ...monitor };
|
||||
this.errors = {};
|
||||
}
|
||||
}
|
||||
1
ts_web/elements/upladmin-monitor-list/index.ts
Normal file
1
ts_web/elements/upladmin-monitor-list/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from './upladmin-monitor-list.js';
|
||||
@@ -0,0 +1,118 @@
|
||||
import { html, css, cssManager } from '@design.estate/dees-element';
|
||||
import type { IServiceStatus } from '../../interfaces/index.js';
|
||||
import './upladmin-monitor-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-monitor-list
|
||||
.monitors=${[
|
||||
{
|
||||
id: 'api-server',
|
||||
name: 'api-server',
|
||||
displayName: 'API Server',
|
||||
description: 'Main REST API endpoint',
|
||||
currentStatus: 'operational',
|
||||
lastChecked: Date.now(),
|
||||
uptime30d: 99.98,
|
||||
uptime90d: 99.95,
|
||||
responseTime: 45,
|
||||
category: 'Core Services',
|
||||
},
|
||||
{
|
||||
id: 'web-app',
|
||||
name: 'web-app',
|
||||
displayName: 'Web Application',
|
||||
description: 'Customer-facing web application',
|
||||
currentStatus: 'operational',
|
||||
lastChecked: Date.now(),
|
||||
uptime30d: 99.99,
|
||||
uptime90d: 99.97,
|
||||
responseTime: 120,
|
||||
category: 'Core Services',
|
||||
},
|
||||
{
|
||||
id: 'database-primary',
|
||||
name: 'database-primary',
|
||||
displayName: 'Primary Database',
|
||||
description: 'PostgreSQL primary node',
|
||||
currentStatus: 'operational',
|
||||
lastChecked: Date.now(),
|
||||
uptime30d: 99.999,
|
||||
uptime90d: 99.998,
|
||||
responseTime: 5,
|
||||
category: 'Infrastructure',
|
||||
},
|
||||
{
|
||||
id: 'cdn',
|
||||
name: 'cdn',
|
||||
displayName: 'Content Delivery Network',
|
||||
description: 'Global CDN for static assets',
|
||||
currentStatus: 'degraded',
|
||||
lastChecked: Date.now(),
|
||||
uptime30d: 99.5,
|
||||
uptime90d: 99.8,
|
||||
responseTime: 200,
|
||||
category: 'Infrastructure',
|
||||
},
|
||||
{
|
||||
id: 'redis-cache',
|
||||
name: 'redis-cache',
|
||||
displayName: 'Redis Cache',
|
||||
description: 'In-memory caching layer',
|
||||
currentStatus: 'operational',
|
||||
lastChecked: Date.now(),
|
||||
uptime30d: 99.99,
|
||||
uptime90d: 99.98,
|
||||
responseTime: 2,
|
||||
category: 'Infrastructure',
|
||||
},
|
||||
{
|
||||
id: 'email-service',
|
||||
name: 'email-service',
|
||||
displayName: 'Email Service',
|
||||
description: 'Transactional email delivery',
|
||||
currentStatus: 'operational',
|
||||
lastChecked: Date.now(),
|
||||
uptime30d: 99.9,
|
||||
uptime90d: 99.85,
|
||||
responseTime: 500,
|
||||
category: 'External Services',
|
||||
},
|
||||
{
|
||||
id: 'payment-gateway',
|
||||
name: 'payment-gateway',
|
||||
displayName: 'Payment Gateway',
|
||||
description: 'Payment processing integration',
|
||||
currentStatus: 'maintenance',
|
||||
lastChecked: Date.now(),
|
||||
uptime30d: 99.95,
|
||||
uptime90d: 99.9,
|
||||
responseTime: 350,
|
||||
category: 'External Services',
|
||||
},
|
||||
{
|
||||
id: 'search-engine',
|
||||
name: 'search-engine',
|
||||
displayName: 'Search Engine',
|
||||
description: 'Elasticsearch cluster',
|
||||
currentStatus: 'partial_outage',
|
||||
lastChecked: Date.now(),
|
||||
uptime30d: 98.5,
|
||||
uptime90d: 99.2,
|
||||
responseTime: 150,
|
||||
category: 'Core Services',
|
||||
},
|
||||
] as IServiceStatus[]}
|
||||
></upladmin-monitor-list>
|
||||
</div>
|
||||
`;
|
||||
481
ts_web/elements/upladmin-monitor-list/upladmin-monitor-list.ts
Normal file
481
ts_web/elements/upladmin-monitor-list/upladmin-monitor-list.ts
Normal file
@@ -0,0 +1,481 @@
|
||||
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 } from '../../interfaces/index.js';
|
||||
import { demoFunc } from './upladmin-monitor-list.demo.js';
|
||||
import type { Column, ITableAction, DeesTable } from '@design.estate/dees-catalog';
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
'upladmin-monitor-list': UpladminMonitorList;
|
||||
}
|
||||
}
|
||||
|
||||
type TStatusType = 'operational' | 'degraded' | 'partial_outage' | 'major_outage' | 'maintenance';
|
||||
|
||||
@customElement('upladmin-monitor-list')
|
||||
export class UpladminMonitorList extends DeesElement {
|
||||
public static demo = demoFunc;
|
||||
|
||||
@property({ type: Array })
|
||||
accessor monitors: IServiceStatus[] = [];
|
||||
|
||||
@property({ type: Boolean })
|
||||
accessor loading: boolean = false;
|
||||
|
||||
@state()
|
||||
accessor statusFilter: TStatusType | 'all' = 'all';
|
||||
|
||||
@state()
|
||||
accessor categoryFilter: string = 'all';
|
||||
|
||||
private statusIcons: Record<TStatusType, string> = {
|
||||
operational: 'lucide:CheckCircle',
|
||||
degraded: 'lucide:AlertTriangle',
|
||||
partial_outage: 'lucide:AlertOctagon',
|
||||
major_outage: 'lucide:XCircle',
|
||||
maintenance: 'lucide:Wrench',
|
||||
};
|
||||
|
||||
private statusLabels: Record<TStatusType, string> = {
|
||||
operational: 'Operational',
|
||||
degraded: 'Degraded',
|
||||
partial_outage: 'Partial Outage',
|
||||
major_outage: 'Major Outage',
|
||||
maintenance: 'Maintenance',
|
||||
};
|
||||
|
||||
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;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.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;
|
||||
}
|
||||
|
||||
.list-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
color: ${sharedStyles.colors.text.primary};
|
||||
}
|
||||
|
||||
.list-title dees-icon {
|
||||
--icon-color: ${cssManager.bdTheme('#3b82f6', '#60a5fa')};
|
||||
}
|
||||
|
||||
.list-controls {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: ${unsafeCSS(sharedStyles.spacing.sm)};
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.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;
|
||||
transition: all ${unsafeCSS(sharedStyles.durations.fast)} ${unsafeCSS(sharedStyles.easings.default)};
|
||||
}
|
||||
|
||||
.filter-select:focus {
|
||||
outline: none;
|
||||
border-color: ${sharedStyles.colors.accent.primary};
|
||||
box-shadow: 0 0 0 3px ${cssManager.bdTheme('rgba(59, 130, 246, 0.1)', 'rgba(96, 165, 250, 0.15)')};
|
||||
}
|
||||
|
||||
.table-container {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
dees-table {
|
||||
--dees-table-background: transparent;
|
||||
--dees-table-border: none;
|
||||
}
|
||||
|
||||
dees-table::part(mainbox) {
|
||||
border: none;
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
.status-badge {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 6px 12px;
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
border-radius: 9999px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.status-badge dees-icon {
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.status-badge.operational {
|
||||
background: ${cssManager.bdTheme('rgba(34, 197, 94, 0.1)', 'rgba(34, 197, 94, 0.15)')};
|
||||
color: ${sharedStyles.colors.status.operational};
|
||||
--icon-color: ${sharedStyles.colors.status.operational};
|
||||
}
|
||||
|
||||
.status-badge.degraded {
|
||||
background: ${cssManager.bdTheme('rgba(234, 179, 8, 0.1)', 'rgba(234, 179, 8, 0.15)')};
|
||||
color: ${sharedStyles.colors.status.degraded};
|
||||
--icon-color: ${sharedStyles.colors.status.degraded};
|
||||
}
|
||||
|
||||
.status-badge.partial_outage {
|
||||
background: ${cssManager.bdTheme('rgba(249, 115, 22, 0.1)', 'rgba(249, 115, 22, 0.15)')};
|
||||
color: ${sharedStyles.colors.status.partialOutage};
|
||||
--icon-color: ${sharedStyles.colors.status.partialOutage};
|
||||
}
|
||||
|
||||
.status-badge.major_outage {
|
||||
background: ${cssManager.bdTheme('rgba(239, 68, 68, 0.1)', 'rgba(239, 68, 68, 0.15)')};
|
||||
color: ${sharedStyles.colors.status.majorOutage};
|
||||
--icon-color: ${sharedStyles.colors.status.majorOutage};
|
||||
}
|
||||
|
||||
.status-badge.maintenance {
|
||||
background: ${cssManager.bdTheme('rgba(59, 130, 246, 0.1)', 'rgba(59, 130, 246, 0.15)')};
|
||||
color: ${sharedStyles.colors.status.maintenance};
|
||||
--icon-color: ${sharedStyles.colors.status.maintenance};
|
||||
}
|
||||
|
||||
.monitor-info {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2px;
|
||||
}
|
||||
|
||||
.monitor-name {
|
||||
font-weight: 600;
|
||||
color: ${sharedStyles.colors.text.primary};
|
||||
}
|
||||
|
||||
.monitor-id {
|
||||
font-size: 12px;
|
||||
color: ${sharedStyles.colors.text.muted};
|
||||
font-family: ${unsafeCSS(sharedStyles.fonts.mono)};
|
||||
}
|
||||
|
||||
.uptime-value {
|
||||
font-family: ${unsafeCSS(sharedStyles.fonts.mono)};
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.uptime-value.good {
|
||||
color: ${sharedStyles.colors.status.operational};
|
||||
}
|
||||
|
||||
.uptime-value.warning {
|
||||
color: ${sharedStyles.colors.status.degraded};
|
||||
}
|
||||
|
||||
.uptime-value.bad {
|
||||
color: ${sharedStyles.colors.status.majorOutage};
|
||||
}
|
||||
|
||||
.category-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)};
|
||||
}
|
||||
|
||||
.empty-state {
|
||||
padding: 64px 24px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.empty-state dees-icon {
|
||||
--icon-color: ${sharedStyles.colors.text.muted};
|
||||
opacity: 0.4;
|
||||
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};
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.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);
|
||||
}
|
||||
|
||||
.loading-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
color: ${sharedStyles.colors.text.secondary};
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.no-category {
|
||||
color: ${sharedStyles.colors.text.muted};
|
||||
}
|
||||
`
|
||||
];
|
||||
|
||||
private get filteredMonitors(): IServiceStatus[] {
|
||||
let result = [...this.monitors];
|
||||
|
||||
// Status filter
|
||||
if (this.statusFilter !== 'all') {
|
||||
result = result.filter(m => m.currentStatus === this.statusFilter);
|
||||
}
|
||||
|
||||
// Category filter
|
||||
if (this.categoryFilter !== 'all') {
|
||||
result = result.filter(m => m.category === this.categoryFilter);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private get categories(): string[] {
|
||||
return [...new Set(this.monitors.map(m => m.category).filter(Boolean))] as string[];
|
||||
}
|
||||
|
||||
private get tableColumns(): Column<IServiceStatus>[] {
|
||||
return [
|
||||
{
|
||||
key: 'name' as keyof IServiceStatus,
|
||||
header: 'Name',
|
||||
sortable: true,
|
||||
renderer: (value, item) => html`
|
||||
<div class="monitor-info">
|
||||
<span class="monitor-name">${item.displayName || item.name}</span>
|
||||
<span class="monitor-id">${item.id}</span>
|
||||
</div>
|
||||
`,
|
||||
},
|
||||
{
|
||||
key: 'currentStatus' as keyof IServiceStatus,
|
||||
header: 'Status',
|
||||
sortable: true,
|
||||
renderer: (value, item) => html`
|
||||
<span class="status-badge ${item.currentStatus}">
|
||||
<dees-icon .icon=${this.statusIcons[item.currentStatus]} .iconSize=${14}></dees-icon>
|
||||
${this.statusLabels[item.currentStatus]}
|
||||
</span>
|
||||
`,
|
||||
},
|
||||
{
|
||||
key: 'uptime30d' as keyof IServiceStatus,
|
||||
header: 'Uptime (30d)',
|
||||
sortable: true,
|
||||
renderer: (value, item) => {
|
||||
const uptimeClass = item.uptime30d >= 99.9 ? 'good' : item.uptime30d >= 99 ? 'warning' : 'bad';
|
||||
return html`<span class="uptime-value ${uptimeClass}">${item.uptime30d.toFixed(2)}%</span>`;
|
||||
},
|
||||
},
|
||||
{
|
||||
key: 'category' as keyof IServiceStatus,
|
||||
header: 'Category',
|
||||
sortable: true,
|
||||
renderer: (value, item) => item.category
|
||||
? html`<span class="category-tag">${item.category}</span>`
|
||||
: html`<span class="no-category">—</span>`,
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
private get tableActions(): ITableAction<IServiceStatus>[] {
|
||||
return [
|
||||
{
|
||||
name: 'Edit',
|
||||
iconName: 'lucide:Pencil',
|
||||
type: ['inRow'],
|
||||
actionFunc: async ({ item }) => {
|
||||
this.handleEdit(item);
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'Delete',
|
||||
iconName: 'lucide:Trash2',
|
||||
type: ['inRow'],
|
||||
actionFunc: async ({ item }) => {
|
||||
this.handleDelete(item);
|
||||
},
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
public render(): TemplateResult {
|
||||
const filteredMonitors = this.filteredMonitors;
|
||||
|
||||
return html`
|
||||
<div class="list-container">
|
||||
${this.loading ? html`
|
||||
<div class="loading-overlay">
|
||||
<div class="loading-content">
|
||||
<dees-spinner></dees-spinner>
|
||||
<span>Loading monitors...</span>
|
||||
</div>
|
||||
</div>
|
||||
` : ''}
|
||||
|
||||
<div class="list-header">
|
||||
<span class="list-title">
|
||||
<dees-icon .icon=${'lucide:Activity'}></dees-icon>
|
||||
Monitors (${filteredMonitors.length})
|
||||
</span>
|
||||
|
||||
<div class="list-controls">
|
||||
<select class="filter-select" @change="${this.handleStatusFilter}">
|
||||
<option value="all" ?selected="${this.statusFilter === 'all'}">All Statuses</option>
|
||||
<option value="operational" ?selected="${this.statusFilter === 'operational'}">Operational</option>
|
||||
<option value="degraded" ?selected="${this.statusFilter === 'degraded'}">Degraded</option>
|
||||
<option value="partial_outage" ?selected="${this.statusFilter === 'partial_outage'}">Partial Outage</option>
|
||||
<option value="major_outage" ?selected="${this.statusFilter === 'major_outage'}">Major Outage</option>
|
||||
<option value="maintenance" ?selected="${this.statusFilter === 'maintenance'}">Maintenance</option>
|
||||
</select>
|
||||
|
||||
${this.categories.length > 0 ? html`
|
||||
<select class="filter-select" @change="${this.handleCategoryFilter}">
|
||||
<option value="all" ?selected="${this.categoryFilter === 'all'}">All Categories</option>
|
||||
${this.categories.map(cat => html`
|
||||
<option value="${cat}" ?selected="${this.categoryFilter === cat}">${cat}</option>
|
||||
`)}
|
||||
</select>
|
||||
` : ''}
|
||||
|
||||
<dees-button
|
||||
type="highlighted"
|
||||
@click="${this.handleAddClick}"
|
||||
>
|
||||
<dees-icon .icon=${'lucide:Plus'} .iconSize=${16}></dees-icon>
|
||||
Add Monitor
|
||||
</dees-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="table-container">
|
||||
${filteredMonitors.length > 0 ? html`
|
||||
<dees-table
|
||||
.heading1=${''}
|
||||
.heading2=${''}
|
||||
.data=${filteredMonitors}
|
||||
.dataName=${'monitors'}
|
||||
.columns=${this.tableColumns}
|
||||
.dataActions=${this.tableActions}
|
||||
.searchable=${true}
|
||||
.showGrid=${false}
|
||||
.showHorizontalLines=${true}
|
||||
.rowKey=${'id'}
|
||||
></dees-table>
|
||||
` : html`
|
||||
<div class="empty-state">
|
||||
<dees-icon .icon=${'lucide:BarChart2'} .iconSize=${48}></dees-icon>
|
||||
<div class="empty-title">No monitors found</div>
|
||||
<div class="empty-text">
|
||||
${this.statusFilter !== 'all' || this.categoryFilter !== 'all'
|
||||
? 'Try adjusting your filters'
|
||||
: 'Add your first monitor to start tracking service status'}
|
||||
</div>
|
||||
${this.statusFilter === 'all' && this.categoryFilter === 'all' ? html`
|
||||
<dees-button type="highlighted" @click="${this.handleAddClick}">
|
||||
<dees-icon .icon=${'lucide:Plus'} .iconSize=${16}></dees-icon>
|
||||
Add Your First Monitor
|
||||
</dees-button>
|
||||
` : ''}
|
||||
</div>
|
||||
`}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
private handleStatusFilter(e: Event) {
|
||||
this.statusFilter = (e.target as HTMLSelectElement).value as TStatusType | 'all';
|
||||
}
|
||||
|
||||
private handleCategoryFilter(e: Event) {
|
||||
this.categoryFilter = (e.target as HTMLSelectElement).value;
|
||||
}
|
||||
|
||||
private handleAddClick() {
|
||||
this.dispatchEvent(new CustomEvent('monitorAdd', {
|
||||
bubbles: true,
|
||||
composed: true
|
||||
}));
|
||||
}
|
||||
|
||||
private handleEdit(monitor: IServiceStatus) {
|
||||
this.dispatchEvent(new CustomEvent('monitorEdit', {
|
||||
detail: { monitor },
|
||||
bubbles: true,
|
||||
composed: true
|
||||
}));
|
||||
}
|
||||
|
||||
private handleDelete(monitor: IServiceStatus) {
|
||||
this.dispatchEvent(new CustomEvent('monitorDelete', {
|
||||
detail: { monitor },
|
||||
bubbles: true,
|
||||
composed: true
|
||||
}));
|
||||
}
|
||||
}
|
||||
1
ts_web/elements/upladmin-statuspage-config/index.ts
Normal file
1
ts_web/elements/upladmin-statuspage-config/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from './upladmin-statuspage-config.js';
|
||||
@@ -0,0 +1,47 @@
|
||||
import { html, css, cssManager } from '@design.estate/dees-element';
|
||||
import type { IStatusPageConfig } from '../../interfaces/index.js';
|
||||
import './upladmin-statuspage-config.js';
|
||||
|
||||
export const demoFunc = () => html`
|
||||
<style>
|
||||
${css`
|
||||
.demo-container {
|
||||
padding: 24px;
|
||||
background: ${cssManager.bdTheme('#fafafa', '#0a0a0a')};
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
.demo-title {
|
||||
margin: 0 0 24px 0;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: ${cssManager.bdTheme('#71717a', '#a1a1aa')};
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.05em;
|
||||
}
|
||||
`}
|
||||
</style>
|
||||
|
||||
<div class="demo-container">
|
||||
<h3 class="demo-title">Status Page Configuration</h3>
|
||||
<upladmin-statuspage-config
|
||||
.config=${{
|
||||
companyName: 'Acme Corporation',
|
||||
companyLogo: 'https://via.placeholder.com/200x60?text=ACME',
|
||||
supportEmail: 'support@acme.example.com',
|
||||
statusPageUrl: 'https://status.acme.example.com',
|
||||
legalUrl: 'https://acme.example.com/terms',
|
||||
apiEndpoint: 'https://api.acme.example.com/status',
|
||||
theme: 'auto',
|
||||
whitelabel: false,
|
||||
refreshInterval: 60,
|
||||
showHistoricalDays: 90,
|
||||
enableWebSocket: true,
|
||||
enableNotifications: false,
|
||||
timeZone: 'America/New_York',
|
||||
language: 'en',
|
||||
dateFormat: 'relative',
|
||||
} as IStatusPageConfig}
|
||||
></upladmin-statuspage-config>
|
||||
</div>
|
||||
`;
|
||||
@@ -0,0 +1,717 @@
|
||||
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 { IStatusPageConfig } from '../../interfaces/index.js';
|
||||
import { demoFunc } from './upladmin-statuspage-config.demo.js';
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
'upladmin-statuspage-config': UpladminStatuspageConfig;
|
||||
}
|
||||
}
|
||||
|
||||
@customElement('upladmin-statuspage-config')
|
||||
export class UpladminStatuspageConfig extends DeesElement {
|
||||
public static demo = demoFunc;
|
||||
|
||||
@property({ type: Object })
|
||||
accessor config: IStatusPageConfig = {};
|
||||
|
||||
@property({ type: Boolean })
|
||||
accessor loading: boolean = false;
|
||||
|
||||
@state()
|
||||
accessor formData: IStatusPageConfig = {};
|
||||
|
||||
@state()
|
||||
accessor activeSection: string = 'branding';
|
||||
|
||||
@state()
|
||||
accessor hasChanges: boolean = false;
|
||||
|
||||
public static styles = [
|
||||
plugins.domtools.elementBasic.staticStyles,
|
||||
sharedStyles.commonStyles,
|
||||
css`
|
||||
:host {
|
||||
display: block;
|
||||
font-family: ${unsafeCSS(sharedStyles.fonts.base)};
|
||||
}
|
||||
|
||||
.config-container {
|
||||
display: grid;
|
||||
grid-template-columns: 220px 1fr;
|
||||
gap: ${unsafeCSS(sharedStyles.spacing.lg)};
|
||||
min-height: 500px;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.config-container {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
.config-nav {
|
||||
background: ${sharedStyles.colors.background.secondary};
|
||||
border: 1px solid ${sharedStyles.colors.border.default};
|
||||
border-radius: ${unsafeCSS(sharedStyles.borderRadius.lg)};
|
||||
padding: ${unsafeCSS(sharedStyles.spacing.sm)};
|
||||
height: fit-content;
|
||||
}
|
||||
|
||||
.nav-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
width: 100%;
|
||||
padding: 14px 16px;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
font-family: ${unsafeCSS(sharedStyles.fonts.base)};
|
||||
color: ${sharedStyles.colors.text.secondary};
|
||||
background: transparent;
|
||||
border: none;
|
||||
border-radius: ${unsafeCSS(sharedStyles.borderRadius.base)};
|
||||
cursor: pointer;
|
||||
text-align: left;
|
||||
transition: all ${unsafeCSS(sharedStyles.durations.fast)} ${unsafeCSS(sharedStyles.easings.default)};
|
||||
}
|
||||
|
||||
.nav-item:hover {
|
||||
background: ${sharedStyles.colors.background.muted};
|
||||
color: ${sharedStyles.colors.text.primary};
|
||||
}
|
||||
|
||||
.nav-item.active {
|
||||
background: ${sharedStyles.colors.accent.primary};
|
||||
color: white;
|
||||
}
|
||||
|
||||
.nav-item.active dees-icon {
|
||||
--icon-color: white;
|
||||
}
|
||||
|
||||
.nav-item dees-icon {
|
||||
--icon-color: ${sharedStyles.colors.text.muted};
|
||||
transition: color ${unsafeCSS(sharedStyles.durations.fast)} ${unsafeCSS(sharedStyles.easings.default)};
|
||||
}
|
||||
|
||||
.nav-item:hover dees-icon {
|
||||
--icon-color: ${sharedStyles.colors.text.primary};
|
||||
}
|
||||
|
||||
.config-content {
|
||||
background: ${sharedStyles.colors.background.secondary};
|
||||
border: 1px solid ${sharedStyles.colors.border.default};
|
||||
border-radius: ${unsafeCSS(sharedStyles.borderRadius.lg)};
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.content-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};
|
||||
background: ${sharedStyles.colors.background.muted};
|
||||
}
|
||||
|
||||
.content-title {
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
color: ${sharedStyles.colors.text.primary};
|
||||
}
|
||||
|
||||
.content-subtitle {
|
||||
font-size: 13px;
|
||||
color: ${sharedStyles.colors.text.muted};
|
||||
margin-top: 2px;
|
||||
}
|
||||
|
||||
.save-indicator {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 6px 12px;
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
color: ${sharedStyles.colors.accent.warning};
|
||||
background: ${cssManager.bdTheme('rgba(234, 179, 8, 0.1)', 'rgba(234, 179, 8, 0.15)')};
|
||||
border-radius: ${unsafeCSS(sharedStyles.borderRadius.base)};
|
||||
}
|
||||
|
||||
.save-indicator dees-icon {
|
||||
--icon-color: ${sharedStyles.colors.accent.warning};
|
||||
}
|
||||
|
||||
.content-body {
|
||||
padding: ${unsafeCSS(sharedStyles.spacing.lg)};
|
||||
}
|
||||
|
||||
dees-form {
|
||||
display: contents;
|
||||
}
|
||||
|
||||
.form-section {
|
||||
margin-bottom: ${unsafeCSS(sharedStyles.spacing.xl)};
|
||||
}
|
||||
|
||||
.form-section:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.section-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: ${sharedStyles.colors.text.primary};
|
||||
margin-bottom: ${unsafeCSS(sharedStyles.spacing.md)};
|
||||
padding-bottom: ${unsafeCSS(sharedStyles.spacing.xs)};
|
||||
border-bottom: 1px solid ${sharedStyles.colors.border.light};
|
||||
}
|
||||
|
||||
.section-title dees-icon {
|
||||
--icon-color: ${sharedStyles.colors.text.muted};
|
||||
}
|
||||
|
||||
.form-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
|
||||
gap: ${unsafeCSS(sharedStyles.spacing.md)};
|
||||
}
|
||||
|
||||
.content-actions {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: ${unsafeCSS(sharedStyles.spacing.sm)};
|
||||
padding: ${unsafeCSS(sharedStyles.spacing.md)} ${unsafeCSS(sharedStyles.spacing.lg)};
|
||||
border-top: 1px solid ${sharedStyles.colors.border.default};
|
||||
background: ${sharedStyles.colors.background.muted};
|
||||
}
|
||||
|
||||
.theme-options {
|
||||
display: flex;
|
||||
gap: ${unsafeCSS(sharedStyles.spacing.sm)};
|
||||
}
|
||||
|
||||
.theme-option {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
padding: 18px;
|
||||
background: ${sharedStyles.colors.background.primary};
|
||||
border: 2px solid ${sharedStyles.colors.border.default};
|
||||
border-radius: ${unsafeCSS(sharedStyles.borderRadius.base)};
|
||||
cursor: pointer;
|
||||
transition: all ${unsafeCSS(sharedStyles.durations.fast)} ${unsafeCSS(sharedStyles.easings.default)};
|
||||
}
|
||||
|
||||
.theme-option:hover {
|
||||
border-color: ${sharedStyles.colors.border.strong};
|
||||
}
|
||||
|
||||
.theme-option.selected {
|
||||
border-color: ${sharedStyles.colors.accent.primary};
|
||||
background: ${cssManager.bdTheme('rgba(59, 130, 246, 0.05)', 'rgba(96, 165, 250, 0.1)')};
|
||||
}
|
||||
|
||||
.theme-option input {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.theme-preview {
|
||||
width: 56px;
|
||||
height: 36px;
|
||||
border-radius: 6px;
|
||||
border: 1px solid ${sharedStyles.colors.border.default};
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.theme-preview.light {
|
||||
background: linear-gradient(180deg, #f8fafc 0%, #e2e8f0 100%);
|
||||
}
|
||||
|
||||
.theme-preview.dark {
|
||||
background: linear-gradient(180deg, #1e293b 0%, #0f172a 100%);
|
||||
}
|
||||
|
||||
.theme-preview.auto {
|
||||
background: linear-gradient(135deg, #f8fafc 0%, #f8fafc 50%, #1e293b 50%, #1e293b 100%);
|
||||
}
|
||||
|
||||
.theme-label {
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
color: ${sharedStyles.colors.text.primary};
|
||||
}
|
||||
|
||||
.logo-preview {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: ${unsafeCSS(sharedStyles.spacing.md)};
|
||||
padding: ${unsafeCSS(sharedStyles.spacing.md)};
|
||||
background: ${sharedStyles.colors.background.primary};
|
||||
border: 1px solid ${sharedStyles.colors.border.default};
|
||||
border-radius: ${unsafeCSS(sharedStyles.borderRadius.base)};
|
||||
margin-top: ${unsafeCSS(sharedStyles.spacing.sm)};
|
||||
}
|
||||
|
||||
.logo-preview img {
|
||||
max-width: 140px;
|
||||
max-height: 48px;
|
||||
object-fit: contain;
|
||||
}
|
||||
|
||||
.logo-placeholder {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
color: ${sharedStyles.colors.text.muted};
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.logo-placeholder dees-icon {
|
||||
--icon-color: ${sharedStyles.colors.text.muted};
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.toggle-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: ${unsafeCSS(sharedStyles.spacing.md)};
|
||||
padding: ${unsafeCSS(sharedStyles.spacing.sm)} 0;
|
||||
}
|
||||
|
||||
.toggle-label {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.toggle-label-text {
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
color: ${sharedStyles.colors.text.primary};
|
||||
}
|
||||
|
||||
.toggle-label-hint {
|
||||
font-size: 12px;
|
||||
color: ${sharedStyles.colors.text.muted};
|
||||
margin-top: 2px;
|
||||
}
|
||||
|
||||
/* Style dees-input components */
|
||||
dees-input-text,
|
||||
dees-input-dropdown {
|
||||
--dees-input-background: ${sharedStyles.colors.background.primary};
|
||||
--dees-input-border-color: ${sharedStyles.colors.border.default};
|
||||
}
|
||||
`
|
||||
];
|
||||
|
||||
async connectedCallback() {
|
||||
await super.connectedCallback();
|
||||
this.formData = { ...this.config };
|
||||
}
|
||||
|
||||
updated(changedProperties: Map<string, unknown>) {
|
||||
if (changedProperties.has('config')) {
|
||||
this.formData = { ...this.config };
|
||||
this.hasChanges = false;
|
||||
}
|
||||
}
|
||||
|
||||
public render(): TemplateResult {
|
||||
const sections = [
|
||||
{ id: 'branding', icon: 'lucide:Palette', label: 'Branding', subtitle: 'Logo, company name, colors' },
|
||||
{ id: 'urls', icon: 'lucide:Link', label: 'URLs', subtitle: 'Links and endpoints' },
|
||||
{ id: 'behavior', icon: 'lucide:Settings', label: 'Behavior', subtitle: 'Refresh, notifications, history' },
|
||||
{ id: 'advanced', icon: 'lucide:Wrench', label: 'Advanced', subtitle: 'API, timezone, language' },
|
||||
];
|
||||
|
||||
return html`
|
||||
<div class="config-container">
|
||||
<nav class="config-nav">
|
||||
${sections.map(section => html`
|
||||
<button
|
||||
class="nav-item ${this.activeSection === section.id ? 'active' : ''}"
|
||||
@click="${() => this.activeSection = section.id}"
|
||||
>
|
||||
<dees-icon .icon=${section.icon} .iconSize=${18}></dees-icon>
|
||||
<span>${section.label}</span>
|
||||
</button>
|
||||
`)}
|
||||
</nav>
|
||||
|
||||
<div class="config-content">
|
||||
<div class="content-header">
|
||||
<div>
|
||||
<div class="content-title">${sections.find(s => s.id === this.activeSection)?.label}</div>
|
||||
<div class="content-subtitle">${sections.find(s => s.id === this.activeSection)?.subtitle}</div>
|
||||
</div>
|
||||
${this.hasChanges ? html`
|
||||
<div class="save-indicator">
|
||||
<dees-icon .icon=${'lucide:AlertCircle'} .iconSize=${14}></dees-icon>
|
||||
<span>Unsaved changes</span>
|
||||
</div>
|
||||
` : ''}
|
||||
</div>
|
||||
|
||||
<div class="content-body">
|
||||
<dees-form>
|
||||
${this.renderSection()}
|
||||
</dees-form>
|
||||
</div>
|
||||
|
||||
<div class="content-actions">
|
||||
<dees-button type="discreet" @click="${this.handleReset}" ?disabled="${!this.hasChanges || this.loading}">
|
||||
<dees-icon .icon=${'lucide:RotateCcw'} .iconSize=${14}></dees-icon>
|
||||
Reset
|
||||
</dees-button>
|
||||
<dees-button type="highlighted" @click="${this.handleSave}" ?disabled="${!this.hasChanges || this.loading}">
|
||||
${this.loading ? html`<dees-spinner .size=${16}></dees-spinner>` : html`<dees-icon .icon=${'lucide:Save'} .iconSize=${16}></dees-icon>`}
|
||||
Save Changes
|
||||
</dees-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
private renderSection(): TemplateResult {
|
||||
switch (this.activeSection) {
|
||||
case 'branding':
|
||||
return this.renderBrandingSection();
|
||||
case 'urls':
|
||||
return this.renderUrlsSection();
|
||||
case 'behavior':
|
||||
return this.renderBehaviorSection();
|
||||
case 'advanced':
|
||||
return this.renderAdvancedSection();
|
||||
default:
|
||||
return html``;
|
||||
}
|
||||
}
|
||||
|
||||
private renderBrandingSection(): TemplateResult {
|
||||
const themeOptions: Array<{ value: 'light' | 'dark' | 'auto'; label: string; icon: string }> = [
|
||||
{ value: 'light', label: 'Light', icon: 'lucide:Sun' },
|
||||
{ value: 'dark', label: 'Dark', icon: 'lucide:Moon' },
|
||||
{ value: 'auto', label: 'Auto', icon: 'lucide:Monitor' },
|
||||
];
|
||||
|
||||
return html`
|
||||
<div class="form-section">
|
||||
<div class="section-title">
|
||||
<dees-icon .icon=${'lucide:Building'} .iconSize=${16}></dees-icon>
|
||||
Company Information
|
||||
</div>
|
||||
<div class="form-grid">
|
||||
<dees-input-text
|
||||
key="companyName"
|
||||
label="Company Name"
|
||||
.value="${this.formData.companyName || ''}"
|
||||
placeholder="Your Company"
|
||||
description="Displayed in the header and footer"
|
||||
@changeSubject="${(e: CustomEvent) => this.handleTextChange('companyName', e.detail)}"
|
||||
></dees-input-text>
|
||||
|
||||
<dees-input-text
|
||||
key="supportEmail"
|
||||
label="Support Email"
|
||||
.value="${this.formData.supportEmail || ''}"
|
||||
placeholder="support@example.com"
|
||||
description="Contact email for users"
|
||||
@changeSubject="${(e: CustomEvent) => this.handleTextChange('supportEmail', e.detail)}"
|
||||
></dees-input-text>
|
||||
</div>
|
||||
|
||||
<dees-input-text
|
||||
key="companyLogo"
|
||||
label="Company Logo URL"
|
||||
.value="${this.formData.companyLogo || ''}"
|
||||
placeholder="https://example.com/logo.png"
|
||||
@changeSubject="${(e: CustomEvent) => this.handleTextChange('companyLogo', e.detail)}"
|
||||
></dees-input-text>
|
||||
|
||||
${this.formData.companyLogo ? html`
|
||||
<div class="logo-preview">
|
||||
<img src="${this.formData.companyLogo}" alt="Company logo" @error="${this.handleLogoError}" />
|
||||
</div>
|
||||
` : html`
|
||||
<div class="logo-preview">
|
||||
<div class="logo-placeholder">
|
||||
<dees-icon .icon=${'lucide:Image'} .iconSize=${20}></dees-icon>
|
||||
No logo configured
|
||||
</div>
|
||||
</div>
|
||||
`}
|
||||
</div>
|
||||
|
||||
<div class="form-section">
|
||||
<div class="section-title">
|
||||
<dees-icon .icon=${'lucide:Palette'} .iconSize=${16}></dees-icon>
|
||||
Theme
|
||||
</div>
|
||||
<div class="theme-options">
|
||||
${themeOptions.map(opt => html`
|
||||
<label
|
||||
class="theme-option ${this.formData.theme === opt.value ? 'selected' : ''}"
|
||||
@click="${() => this.handleThemeChange(opt.value)}"
|
||||
>
|
||||
<input type="radio" name="theme" value="${opt.value}" ?checked="${this.formData.theme === opt.value}" />
|
||||
<div class="theme-preview ${opt.value}"></div>
|
||||
<span class="theme-label">${opt.label}</span>
|
||||
</label>
|
||||
`)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-section">
|
||||
<div class="toggle-row">
|
||||
<div class="toggle-label">
|
||||
<div class="toggle-label-text">White Label Mode</div>
|
||||
<div class="toggle-label-hint">Hide 'Powered by' branding</div>
|
||||
</div>
|
||||
<dees-input-checkbox
|
||||
key="whitelabel"
|
||||
.value="${this.formData.whitelabel || false}"
|
||||
@changeSubject="${(e: CustomEvent) => this.handleBooleanChange('whitelabel', e.detail)}"
|
||||
></dees-input-checkbox>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
private renderUrlsSection(): TemplateResult {
|
||||
return html`
|
||||
<div class="form-section">
|
||||
<div class="section-title">
|
||||
<dees-icon .icon=${'lucide:Globe'} .iconSize=${16}></dees-icon>
|
||||
Status Page URLs
|
||||
</div>
|
||||
<div class="form-grid">
|
||||
<dees-input-text
|
||||
key="statusPageUrl"
|
||||
label="Status Page URL"
|
||||
.value="${this.formData.statusPageUrl || ''}"
|
||||
placeholder="https://status.example.com"
|
||||
description="Public URL of your status page"
|
||||
@changeSubject="${(e: CustomEvent) => this.handleTextChange('statusPageUrl', e.detail)}"
|
||||
></dees-input-text>
|
||||
|
||||
<dees-input-text
|
||||
key="legalUrl"
|
||||
label="Legal / Terms URL"
|
||||
.value="${this.formData.legalUrl || ''}"
|
||||
placeholder="https://example.com/terms"
|
||||
description="Link to terms of service or legal info"
|
||||
@changeSubject="${(e: CustomEvent) => this.handleTextChange('legalUrl', e.detail)}"
|
||||
></dees-input-text>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-section">
|
||||
<div class="section-title">
|
||||
<dees-icon .icon=${'lucide:Server'} .iconSize=${16}></dees-icon>
|
||||
API Configuration
|
||||
</div>
|
||||
<dees-input-text
|
||||
key="apiEndpoint"
|
||||
label="API Endpoint"
|
||||
.value="${this.formData.apiEndpoint || ''}"
|
||||
placeholder="https://api.example.com/status"
|
||||
description="Base URL for status API requests"
|
||||
@changeSubject="${(e: CustomEvent) => this.handleTextChange('apiEndpoint', e.detail)}"
|
||||
></dees-input-text>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
private renderBehaviorSection(): TemplateResult {
|
||||
return html`
|
||||
<div class="form-section">
|
||||
<div class="section-title">
|
||||
<dees-icon .icon=${'lucide:RefreshCw'} .iconSize=${16}></dees-icon>
|
||||
Auto-refresh
|
||||
</div>
|
||||
<dees-input-text
|
||||
key="refreshInterval"
|
||||
label="Refresh Interval (seconds)"
|
||||
inputType="number"
|
||||
.value="${String(this.formData.refreshInterval || 60)}"
|
||||
placeholder="60"
|
||||
description="How often to refresh status data (minimum 30 seconds)"
|
||||
@changeSubject="${(e: CustomEvent) => this.handleNumberChange('refreshInterval', e.detail)}"
|
||||
></dees-input-text>
|
||||
</div>
|
||||
|
||||
<div class="form-section">
|
||||
<div class="section-title">
|
||||
<dees-icon .icon=${'lucide:History'} .iconSize=${16}></dees-icon>
|
||||
History
|
||||
</div>
|
||||
<dees-input-text
|
||||
key="showHistoricalDays"
|
||||
label="Historical Days to Show"
|
||||
inputType="number"
|
||||
.value="${String(this.formData.showHistoricalDays || 90)}"
|
||||
placeholder="90"
|
||||
description="Number of days of history to display"
|
||||
@changeSubject="${(e: CustomEvent) => this.handleNumberChange('showHistoricalDays', e.detail)}"
|
||||
></dees-input-text>
|
||||
</div>
|
||||
|
||||
<div class="form-section">
|
||||
<div class="section-title">
|
||||
<dees-icon .icon=${'lucide:Zap'} .iconSize=${16}></dees-icon>
|
||||
Features
|
||||
</div>
|
||||
<div class="toggle-row">
|
||||
<div class="toggle-label">
|
||||
<div class="toggle-label-text">WebSocket Updates</div>
|
||||
<div class="toggle-label-hint">Enable real-time updates</div>
|
||||
</div>
|
||||
<dees-input-checkbox
|
||||
key="enableWebSocket"
|
||||
.value="${this.formData.enableWebSocket || false}"
|
||||
@changeSubject="${(e: CustomEvent) => this.handleBooleanChange('enableWebSocket', e.detail)}"
|
||||
></dees-input-checkbox>
|
||||
</div>
|
||||
|
||||
<div class="toggle-row">
|
||||
<div class="toggle-label">
|
||||
<div class="toggle-label-text">Browser Notifications</div>
|
||||
<div class="toggle-label-hint">Allow push notifications</div>
|
||||
</div>
|
||||
<dees-input-checkbox
|
||||
key="enableNotifications"
|
||||
.value="${this.formData.enableNotifications || false}"
|
||||
@changeSubject="${(e: CustomEvent) => this.handleBooleanChange('enableNotifications', e.detail)}"
|
||||
></dees-input-checkbox>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
private renderAdvancedSection(): TemplateResult {
|
||||
const timezoneOptions = [
|
||||
{ key: 'UTC', option: 'UTC', payload: null },
|
||||
{ key: 'America/New_York', option: 'Eastern Time (US)', payload: null },
|
||||
{ key: 'America/Los_Angeles', option: 'Pacific Time (US)', payload: null },
|
||||
{ key: 'Europe/London', option: 'London', payload: null },
|
||||
{ key: 'Europe/Berlin', option: 'Berlin', payload: null },
|
||||
{ key: 'Asia/Tokyo', option: 'Tokyo', payload: null },
|
||||
{ key: 'Asia/Shanghai', option: 'Shanghai', payload: null },
|
||||
];
|
||||
|
||||
const dateFormatOptions = [
|
||||
{ key: 'relative', option: 'Relative (2 hours ago)', payload: null },
|
||||
{ key: 'absolute', option: 'Absolute (Dec 23, 2024 14:30)', payload: null },
|
||||
{ key: 'iso', option: 'ISO (2024-12-23T14:30:00)', payload: null },
|
||||
];
|
||||
|
||||
const languageOptions = [
|
||||
{ key: 'en', option: 'English', payload: null },
|
||||
{ key: 'de', option: 'German', payload: null },
|
||||
{ key: 'fr', option: 'French', payload: null },
|
||||
{ key: 'es', option: 'Spanish', payload: null },
|
||||
{ key: 'ja', option: 'Japanese', payload: null },
|
||||
{ key: 'zh', option: 'Chinese', payload: null },
|
||||
];
|
||||
|
||||
return html`
|
||||
<div class="form-section">
|
||||
<div class="section-title">
|
||||
<dees-icon .icon=${'lucide:Globe2'} .iconSize=${16}></dees-icon>
|
||||
Localization
|
||||
</div>
|
||||
<div class="form-grid">
|
||||
<dees-input-dropdown
|
||||
key="timeZone"
|
||||
label="Timezone"
|
||||
.options="${timezoneOptions}"
|
||||
.selectedOption="${this.formData.timeZone || 'UTC'}"
|
||||
@selectedOption="${(e: CustomEvent) => this.handleDropdownChange('timeZone', e.detail)}"
|
||||
></dees-input-dropdown>
|
||||
|
||||
<dees-input-dropdown
|
||||
key="language"
|
||||
label="Language"
|
||||
.options="${languageOptions}"
|
||||
.selectedOption="${this.formData.language || 'en'}"
|
||||
@selectedOption="${(e: CustomEvent) => this.handleDropdownChange('language', e.detail)}"
|
||||
></dees-input-dropdown>
|
||||
|
||||
<dees-input-dropdown
|
||||
key="dateFormat"
|
||||
label="Date Format"
|
||||
.options="${dateFormatOptions}"
|
||||
.selectedOption="${this.formData.dateFormat || 'relative'}"
|
||||
@selectedOption="${(e: CustomEvent) => this.handleDropdownChange('dateFormat', e.detail)}"
|
||||
></dees-input-dropdown>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
private handleTextChange(name: string, value: string) {
|
||||
this.formData = { ...this.formData, [name]: value };
|
||||
this.hasChanges = true;
|
||||
}
|
||||
|
||||
private handleNumberChange(name: string, value: string) {
|
||||
this.formData = { ...this.formData, [name]: parseInt(value, 10) || 0 };
|
||||
this.hasChanges = true;
|
||||
}
|
||||
|
||||
private handleBooleanChange(name: string, value: boolean) {
|
||||
this.formData = { ...this.formData, [name]: value };
|
||||
this.hasChanges = true;
|
||||
}
|
||||
|
||||
private handleDropdownChange(name: string, value: string) {
|
||||
this.formData = { ...this.formData, [name]: value };
|
||||
this.hasChanges = true;
|
||||
}
|
||||
|
||||
private handleThemeChange(theme: 'light' | 'dark' | 'auto') {
|
||||
this.formData = { ...this.formData, theme };
|
||||
this.hasChanges = true;
|
||||
}
|
||||
|
||||
private handleLogoError(e: Event) {
|
||||
const img = e.target as HTMLImageElement;
|
||||
img.style.display = 'none';
|
||||
}
|
||||
|
||||
private handleSave() {
|
||||
this.dispatchEvent(new CustomEvent('configSave', {
|
||||
detail: { config: { ...this.formData } },
|
||||
bubbles: true,
|
||||
composed: true
|
||||
}));
|
||||
}
|
||||
|
||||
private handleReset() {
|
||||
this.formData = { ...this.config };
|
||||
this.hasChanges = false;
|
||||
}
|
||||
|
||||
public setConfig(config: IStatusPageConfig) {
|
||||
this.formData = { ...config };
|
||||
this.hasChanges = false;
|
||||
}
|
||||
}
|
||||
4
ts_web/index.ts
Normal file
4
ts_web/index.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
export * from './elements/index.js';
|
||||
export * from './pages/index.js';
|
||||
export * from './interfaces/index.js';
|
||||
export * from './services/index.js';
|
||||
98
ts_web/interfaces/index.ts
Normal file
98
ts_web/interfaces/index.ts
Normal file
@@ -0,0 +1,98 @@
|
||||
// Re-export interfaces from the public catalog for consistency
|
||||
export interface IServiceStatus {
|
||||
id: string;
|
||||
name: string;
|
||||
displayName: string;
|
||||
description?: string;
|
||||
currentStatus: 'operational' | 'degraded' | 'partial_outage' | 'major_outage' | 'maintenance';
|
||||
lastChecked: number;
|
||||
uptime30d: number;
|
||||
uptime90d: number;
|
||||
responseTime: number;
|
||||
category?: string;
|
||||
dependencies?: string[];
|
||||
selected?: boolean;
|
||||
}
|
||||
|
||||
export interface IStatusHistoryPoint {
|
||||
timestamp: number;
|
||||
status: 'operational' | 'degraded' | 'partial_outage' | 'major_outage' | 'maintenance';
|
||||
responseTime?: number;
|
||||
errorRate?: number;
|
||||
}
|
||||
|
||||
export interface IIncidentUpdate {
|
||||
id: string;
|
||||
timestamp: number;
|
||||
status: 'investigating' | 'identified' | 'monitoring' | 'resolved' | 'postmortem';
|
||||
message: string;
|
||||
author?: string;
|
||||
}
|
||||
|
||||
export interface IIncidentDetails {
|
||||
id: string;
|
||||
title: string;
|
||||
status: 'investigating' | 'identified' | 'monitoring' | 'resolved' | 'postmortem';
|
||||
severity: 'critical' | 'major' | 'minor' | 'maintenance';
|
||||
affectedServices: string[];
|
||||
startTime: number;
|
||||
endTime?: number;
|
||||
updates: IIncidentUpdate[];
|
||||
impact: string;
|
||||
rootCause?: string;
|
||||
resolution?: string;
|
||||
}
|
||||
|
||||
export interface IOverallStatus {
|
||||
status: 'operational' | 'degraded' | 'partial_outage' | 'major_outage' | 'maintenance';
|
||||
message: string;
|
||||
lastUpdated: number;
|
||||
affectedServices: number;
|
||||
totalServices: number;
|
||||
}
|
||||
|
||||
export interface IStatusPageConfig {
|
||||
apiEndpoint?: string;
|
||||
refreshInterval?: number;
|
||||
timeZone?: string;
|
||||
dateFormat?: string;
|
||||
enableWebSocket?: boolean;
|
||||
enableNotifications?: boolean;
|
||||
theme?: 'light' | 'dark' | 'auto';
|
||||
language?: string;
|
||||
showHistoricalDays?: number;
|
||||
whitelabel?: boolean;
|
||||
companyName?: string;
|
||||
companyLogo?: string;
|
||||
supportEmail?: string;
|
||||
statusPageUrl?: string;
|
||||
legalUrl?: string;
|
||||
}
|
||||
|
||||
// Admin-specific interfaces
|
||||
export interface IMonitorFormData {
|
||||
id?: string;
|
||||
name: string;
|
||||
displayName: string;
|
||||
description?: string;
|
||||
category?: string;
|
||||
dependencies?: string[];
|
||||
currentStatus: 'operational' | 'degraded' | 'partial_outage' | 'major_outage' | 'maintenance';
|
||||
}
|
||||
|
||||
export interface IIncidentFormData {
|
||||
id?: string;
|
||||
title: string;
|
||||
severity: 'critical' | 'major' | 'minor' | 'maintenance';
|
||||
status: 'investigating' | 'identified' | 'monitoring' | 'resolved' | 'postmortem';
|
||||
affectedServices: string[];
|
||||
impact: string;
|
||||
rootCause?: string;
|
||||
resolution?: string;
|
||||
}
|
||||
|
||||
export interface IIncidentUpdateFormData {
|
||||
status: 'investigating' | 'identified' | 'monitoring' | 'resolved' | 'postmortem';
|
||||
message: string;
|
||||
author?: string;
|
||||
}
|
||||
83
ts_web/pages/adminpage-config.ts
Normal file
83
ts_web/pages/adminpage-config.ts
Normal file
@@ -0,0 +1,83 @@
|
||||
import { html, cssManager } from "@design.estate/dees-element";
|
||||
import type { IStatusPageConfig } from '../interfaces/index.js';
|
||||
import '../elements/index.js';
|
||||
|
||||
export const adminpageConfig = () => html`
|
||||
<style>
|
||||
.demo-page-wrapper {
|
||||
min-height: 100vh;
|
||||
background: ${cssManager.bdTheme('#fafafa', '#0a0a0a')};
|
||||
padding: 24px;
|
||||
}
|
||||
</style>
|
||||
|
||||
<div class="demo-page-wrapper">
|
||||
<dees-demowrapper
|
||||
.runAfterRender=${async (wrapperElement: any) => {
|
||||
const config = wrapperElement.querySelector('upladmin-statuspage-config') as any;
|
||||
|
||||
const configData: IStatusPageConfig = {
|
||||
companyName: 'CloudFlow Inc.',
|
||||
companyLogo: '',
|
||||
supportEmail: 'support@cloudflow.io',
|
||||
statusPageUrl: 'https://status.cloudflow.io',
|
||||
legalUrl: 'https://cloudflow.io/terms',
|
||||
apiEndpoint: 'https://api.cloudflow.io/status',
|
||||
refreshInterval: 60,
|
||||
showHistoricalDays: 90,
|
||||
theme: 'auto',
|
||||
language: 'en',
|
||||
timeZone: 'UTC',
|
||||
dateFormat: 'relative',
|
||||
enableWebSocket: true,
|
||||
enableNotifications: false,
|
||||
whitelabel: false,
|
||||
};
|
||||
|
||||
config.config = configData;
|
||||
}}
|
||||
>
|
||||
<upladmin-statuspage-config></upladmin-statuspage-config>
|
||||
</dees-demowrapper>
|
||||
</div>
|
||||
`;
|
||||
|
||||
export const adminpageConfigWhitelabel = () => html`
|
||||
<style>
|
||||
.demo-page-wrapper {
|
||||
min-height: 100vh;
|
||||
background: ${cssManager.bdTheme('#fafafa', '#0a0a0a')};
|
||||
padding: 24px;
|
||||
}
|
||||
</style>
|
||||
|
||||
<div class="demo-page-wrapper">
|
||||
<dees-demowrapper
|
||||
.runAfterRender=${async (wrapperElement: any) => {
|
||||
const config = wrapperElement.querySelector('upladmin-statuspage-config') as any;
|
||||
|
||||
const configData: IStatusPageConfig = {
|
||||
companyName: 'Enterprise Corp',
|
||||
companyLogo: 'https://via.placeholder.com/200x60/1a1a2e/ffffff?text=ENTERPRISE',
|
||||
supportEmail: 'support@enterprise.com',
|
||||
statusPageUrl: 'https://status.enterprise.com',
|
||||
legalUrl: 'https://enterprise.com/legal',
|
||||
apiEndpoint: 'https://api.enterprise.com/v2/status',
|
||||
refreshInterval: 30,
|
||||
showHistoricalDays: 180,
|
||||
theme: 'dark',
|
||||
language: 'en',
|
||||
timeZone: 'America/New_York',
|
||||
dateFormat: 'absolute',
|
||||
enableWebSocket: true,
|
||||
enableNotifications: true,
|
||||
whitelabel: true,
|
||||
};
|
||||
|
||||
config.config = configData;
|
||||
}}
|
||||
>
|
||||
<upladmin-statuspage-config></upladmin-statuspage-config>
|
||||
</dees-demowrapper>
|
||||
</div>
|
||||
`;
|
||||
156
ts_web/pages/adminpage-dashboard.ts
Normal file
156
ts_web/pages/adminpage-dashboard.ts
Normal file
@@ -0,0 +1,156 @@
|
||||
import { html, cssManager } from "@design.estate/dees-element";
|
||||
import type { IServiceStatus, IIncidentDetails } from '../interfaces/index.js';
|
||||
import '../elements/index.js';
|
||||
|
||||
export const adminpageDashboard = () => html`
|
||||
<style>
|
||||
.demo-page-wrapper {
|
||||
min-height: 100vh;
|
||||
background: ${cssManager.bdTheme('#fafafa', '#0a0a0a')};
|
||||
padding: 24px;
|
||||
}
|
||||
</style>
|
||||
|
||||
<div class="demo-page-wrapper">
|
||||
<dees-demowrapper
|
||||
.runAfterRender=${async (wrapperElement: any) => {
|
||||
const dashboard = wrapperElement.querySelector('upladmin-dashboard') as any;
|
||||
|
||||
// Demo monitors
|
||||
const monitors: IServiceStatus[] = [
|
||||
{
|
||||
id: 'api-server',
|
||||
name: 'api-server',
|
||||
displayName: 'API Server',
|
||||
description: 'Main REST API endpoint',
|
||||
currentStatus: 'operational',
|
||||
lastChecked: Date.now(),
|
||||
uptime30d: 99.98,
|
||||
uptime90d: 99.95,
|
||||
responseTime: 45,
|
||||
category: 'Core Services',
|
||||
},
|
||||
{
|
||||
id: 'web-app',
|
||||
name: 'web-app',
|
||||
displayName: 'Web Application',
|
||||
description: 'Customer-facing web application',
|
||||
currentStatus: 'operational',
|
||||
lastChecked: Date.now(),
|
||||
uptime30d: 99.99,
|
||||
uptime90d: 99.97,
|
||||
responseTime: 120,
|
||||
category: 'Core Services',
|
||||
},
|
||||
{
|
||||
id: 'database-primary',
|
||||
name: 'database-primary',
|
||||
displayName: 'Primary Database',
|
||||
description: 'PostgreSQL primary node',
|
||||
currentStatus: 'operational',
|
||||
lastChecked: Date.now(),
|
||||
uptime30d: 99.999,
|
||||
uptime90d: 99.998,
|
||||
responseTime: 5,
|
||||
category: 'Infrastructure',
|
||||
},
|
||||
{
|
||||
id: 'cdn',
|
||||
name: 'cdn',
|
||||
displayName: 'Content Delivery Network',
|
||||
description: 'Global CDN for static assets',
|
||||
currentStatus: 'degraded',
|
||||
lastChecked: Date.now(),
|
||||
uptime30d: 99.5,
|
||||
uptime90d: 99.8,
|
||||
responseTime: 200,
|
||||
category: 'Infrastructure',
|
||||
},
|
||||
{
|
||||
id: 'email-service',
|
||||
name: 'email-service',
|
||||
displayName: 'Email Service',
|
||||
description: 'Transactional email delivery',
|
||||
currentStatus: 'operational',
|
||||
lastChecked: Date.now(),
|
||||
uptime30d: 99.9,
|
||||
uptime90d: 99.85,
|
||||
responseTime: 500,
|
||||
category: 'External Services',
|
||||
},
|
||||
{
|
||||
id: 'payment-gateway',
|
||||
name: 'payment-gateway',
|
||||
displayName: 'Payment Gateway',
|
||||
description: 'Payment processing integration',
|
||||
currentStatus: 'maintenance',
|
||||
lastChecked: Date.now(),
|
||||
uptime30d: 99.95,
|
||||
uptime90d: 99.9,
|
||||
responseTime: 350,
|
||||
category: 'External Services',
|
||||
},
|
||||
];
|
||||
|
||||
// Demo incidents
|
||||
const incidents: IIncidentDetails[] = [
|
||||
{
|
||||
id: 'inc-001',
|
||||
title: 'CDN Performance Degradation',
|
||||
status: 'monitoring',
|
||||
severity: 'minor',
|
||||
affectedServices: ['cdn'],
|
||||
startTime: Date.now() - 2 * 60 * 60 * 1000,
|
||||
impact: 'Some users may experience slower loading times for images and static assets.',
|
||||
updates: [
|
||||
{
|
||||
id: 'upd-001',
|
||||
timestamp: Date.now() - 2 * 60 * 60 * 1000,
|
||||
status: 'investigating',
|
||||
message: 'We are investigating reports of slow asset loading.',
|
||||
author: 'Platform Team',
|
||||
},
|
||||
{
|
||||
id: 'upd-002',
|
||||
timestamp: Date.now() - 1 * 60 * 60 * 1000,
|
||||
status: 'identified',
|
||||
message: 'We have identified the issue as a problem with one of our CDN edge nodes.',
|
||||
author: 'Platform Team',
|
||||
},
|
||||
{
|
||||
id: 'upd-003',
|
||||
timestamp: Date.now() - 30 * 60 * 1000,
|
||||
status: 'monitoring',
|
||||
message: 'Traffic has been rerouted to healthy nodes. Monitoring for stability.',
|
||||
author: 'Platform Team',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'inc-002',
|
||||
title: 'Payment Gateway Scheduled Maintenance',
|
||||
status: 'investigating',
|
||||
severity: 'maintenance',
|
||||
affectedServices: ['payment-gateway'],
|
||||
startTime: Date.now() - 30 * 60 * 1000,
|
||||
impact: 'Payment processing is temporarily unavailable during the maintenance window.',
|
||||
updates: [
|
||||
{
|
||||
id: 'upd-004',
|
||||
timestamp: Date.now() - 30 * 60 * 1000,
|
||||
status: 'investigating',
|
||||
message: 'Scheduled maintenance has begun. Expected duration: 2 hours.',
|
||||
author: 'DevOps Team',
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
dashboard.monitors = monitors;
|
||||
dashboard.incidents = incidents;
|
||||
}}
|
||||
>
|
||||
<upladmin-dashboard></upladmin-dashboard>
|
||||
</dees-demowrapper>
|
||||
</div>
|
||||
`;
|
||||
166
ts_web/pages/adminpage-incidents.ts
Normal file
166
ts_web/pages/adminpage-incidents.ts
Normal file
@@ -0,0 +1,166 @@
|
||||
import { html, cssManager } from "@design.estate/dees-element";
|
||||
import type { IServiceStatus, IIncidentDetails } from '../interfaces/index.js';
|
||||
import '../elements/index.js';
|
||||
|
||||
export const adminpageIncidents = () => html`
|
||||
<style>
|
||||
.demo-page-wrapper {
|
||||
min-height: 100vh;
|
||||
background: ${cssManager.bdTheme('#fafafa', '#0a0a0a')};
|
||||
padding: 24px;
|
||||
}
|
||||
</style>
|
||||
|
||||
<div class="demo-page-wrapper">
|
||||
<dees-demowrapper
|
||||
.runAfterRender=${async (wrapperElement: any) => {
|
||||
const incidentList = wrapperElement.querySelector('upladmin-incident-list') as any;
|
||||
|
||||
const incidents: IIncidentDetails[] = [
|
||||
{
|
||||
id: 'inc-001',
|
||||
title: 'CDN Performance Degradation',
|
||||
status: 'monitoring',
|
||||
severity: 'minor',
|
||||
affectedServices: ['cdn'],
|
||||
startTime: Date.now() - 2 * 60 * 60 * 1000,
|
||||
impact: 'Some users may experience slower loading times for images and static assets.',
|
||||
updates: [
|
||||
{ id: 'upd-001', timestamp: Date.now() - 2 * 60 * 60 * 1000, status: 'investigating', message: 'We are investigating reports of slow asset loading.', author: 'Platform Team' },
|
||||
{ id: 'upd-002', timestamp: Date.now() - 1 * 60 * 60 * 1000, status: 'identified', message: 'We have identified the issue as a problem with one of our CDN edge nodes.', author: 'Platform Team' },
|
||||
{ id: 'upd-003', timestamp: Date.now() - 30 * 60 * 1000, status: 'monitoring', message: 'Traffic has been rerouted to healthy nodes. Monitoring for stability.', author: 'Platform Team' },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'inc-002',
|
||||
title: 'Payment Gateway Scheduled Maintenance',
|
||||
status: 'investigating',
|
||||
severity: 'maintenance',
|
||||
affectedServices: ['payment-gateway'],
|
||||
startTime: Date.now() - 30 * 60 * 1000,
|
||||
impact: 'Payment processing is temporarily unavailable during the maintenance window.',
|
||||
updates: [
|
||||
{ id: 'upd-004', timestamp: Date.now() - 30 * 60 * 1000, status: 'investigating', message: 'Scheduled maintenance has begun. Expected duration: 2 hours.', author: 'DevOps Team' },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'inc-003',
|
||||
title: 'Search Engine Partial Outage',
|
||||
status: 'identified',
|
||||
severity: 'major',
|
||||
affectedServices: ['search-engine', 'api-server'],
|
||||
startTime: Date.now() - 45 * 60 * 1000,
|
||||
impact: 'Search functionality is degraded. Some queries may timeout or return incomplete results.',
|
||||
updates: [
|
||||
{ id: 'upd-005', timestamp: Date.now() - 45 * 60 * 1000, status: 'investigating', message: 'We are aware of issues with search functionality.', author: 'Engineering Team' },
|
||||
{ id: 'upd-006', timestamp: Date.now() - 20 * 60 * 1000, status: 'identified', message: 'Root cause identified: disk space exhaustion on search cluster nodes.', author: 'Engineering Team' },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'inc-004',
|
||||
title: 'API Server Outage',
|
||||
status: 'resolved',
|
||||
severity: 'critical',
|
||||
affectedServices: ['api-server', 'web-app'],
|
||||
startTime: Date.now() - 24 * 60 * 60 * 1000,
|
||||
endTime: Date.now() - 23 * 60 * 60 * 1000,
|
||||
impact: 'Complete service unavailability for all API-dependent services.',
|
||||
rootCause: 'Database connection pool exhaustion due to a query performance regression.',
|
||||
resolution: 'Rolled back recent deployment and optimized database queries.',
|
||||
updates: [
|
||||
{ id: 'upd-007', timestamp: Date.now() - 24 * 60 * 60 * 1000, status: 'investigating', message: 'We are aware of service unavailability and actively investigating.', author: 'Platform Team' },
|
||||
{ id: 'upd-008', timestamp: Date.now() - 23.5 * 60 * 60 * 1000, status: 'identified', message: 'Root cause identified as database connection pool exhaustion.', author: 'Platform Team' },
|
||||
{ id: 'upd-009', timestamp: Date.now() - 23 * 60 * 60 * 1000, status: 'resolved', message: 'Service has been restored. All systems operational.', author: 'Platform Team' },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'inc-005',
|
||||
title: 'Email Delivery Delays',
|
||||
status: 'resolved',
|
||||
severity: 'minor',
|
||||
affectedServices: ['email-service'],
|
||||
startTime: Date.now() - 48 * 60 * 60 * 1000,
|
||||
endTime: Date.now() - 46 * 60 * 60 * 1000,
|
||||
impact: 'Email notifications may be delayed by up to 30 minutes.',
|
||||
rootCause: 'Third-party email provider experiencing capacity issues.',
|
||||
resolution: 'Provider resolved their capacity issues.',
|
||||
updates: [
|
||||
{ id: 'upd-010', timestamp: Date.now() - 48 * 60 * 60 * 1000, status: 'investigating', message: 'Investigating reports of delayed email delivery.', author: 'Support Team' },
|
||||
{ id: 'upd-011', timestamp: Date.now() - 46 * 60 * 60 * 1000, status: 'resolved', message: 'Email delivery has returned to normal.', author: 'Support Team' },
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
incidentList.incidents = incidents;
|
||||
}}
|
||||
>
|
||||
<upladmin-incident-list></upladmin-incident-list>
|
||||
</dees-demowrapper>
|
||||
</div>
|
||||
`;
|
||||
|
||||
export const adminpageIncidentForm = () => html`
|
||||
<style>
|
||||
.demo-page-wrapper {
|
||||
min-height: 100vh;
|
||||
background: ${cssManager.bdTheme('#fafafa', '#0a0a0a')};
|
||||
padding: 24px;
|
||||
}
|
||||
</style>
|
||||
|
||||
<div class="demo-page-wrapper">
|
||||
<dees-demowrapper
|
||||
.runAfterRender=${async (wrapperElement: any) => {
|
||||
const incidentForm = wrapperElement.querySelector('upladmin-incident-form') as any;
|
||||
|
||||
const services: IServiceStatus[] = [
|
||||
{ id: 'api-server', name: 'api-server', displayName: 'API Server', currentStatus: 'operational', lastChecked: Date.now(), uptime30d: 99.98, uptime90d: 99.95, responseTime: 45 },
|
||||
{ id: 'web-app', name: 'web-app', displayName: 'Web Application', currentStatus: 'operational', lastChecked: Date.now(), uptime30d: 99.99, uptime90d: 99.97, responseTime: 120 },
|
||||
{ id: 'database-primary', name: 'database-primary', displayName: 'Primary Database', currentStatus: 'operational', lastChecked: Date.now(), uptime30d: 99.999, uptime90d: 99.998, responseTime: 5 },
|
||||
{ id: 'cdn', name: 'cdn', displayName: 'Content Delivery Network', currentStatus: 'degraded', lastChecked: Date.now(), uptime30d: 99.5, uptime90d: 99.8, responseTime: 200 },
|
||||
{ id: 'email-service', name: 'email-service', displayName: 'Email Service', currentStatus: 'operational', lastChecked: Date.now(), uptime30d: 99.9, uptime90d: 99.85, responseTime: 500 },
|
||||
{ id: 'payment-gateway', name: 'payment-gateway', displayName: 'Payment Gateway', currentStatus: 'maintenance', lastChecked: Date.now(), uptime30d: 99.95, uptime90d: 99.9, responseTime: 350 },
|
||||
];
|
||||
|
||||
incidentForm.availableServices = services;
|
||||
}}
|
||||
>
|
||||
<upladmin-incident-form></upladmin-incident-form>
|
||||
</dees-demowrapper>
|
||||
</div>
|
||||
`;
|
||||
|
||||
export const adminpageIncidentUpdate = () => html`
|
||||
<style>
|
||||
.demo-page-wrapper {
|
||||
min-height: 100vh;
|
||||
background: ${cssManager.bdTheme('#fafafa', '#0a0a0a')};
|
||||
padding: 24px;
|
||||
}
|
||||
</style>
|
||||
|
||||
<div class="demo-page-wrapper">
|
||||
<dees-demowrapper
|
||||
.runAfterRender=${async (wrapperElement: any) => {
|
||||
const incidentUpdate = wrapperElement.querySelector('upladmin-incident-update') as any;
|
||||
|
||||
incidentUpdate.incident = {
|
||||
id: 'inc-001',
|
||||
title: 'CDN Performance Degradation',
|
||||
status: 'monitoring',
|
||||
severity: 'minor',
|
||||
affectedServices: ['cdn'],
|
||||
startTime: Date.now() - 2 * 60 * 60 * 1000,
|
||||
impact: 'Some users may experience slower loading times for images and static assets.',
|
||||
updates: [
|
||||
{ id: 'upd-001', timestamp: Date.now() - 2 * 60 * 60 * 1000, status: 'investigating', message: 'We are investigating reports of slow asset loading.', author: 'Platform Team' },
|
||||
{ id: 'upd-002', timestamp: Date.now() - 1 * 60 * 60 * 1000, status: 'identified', message: 'We have identified the issue as a problem with one of our CDN edge nodes.', author: 'Platform Team' },
|
||||
{ id: 'upd-003', timestamp: Date.now() - 30 * 60 * 1000, status: 'monitoring', message: 'Traffic has been rerouted to healthy nodes. Monitoring for stability.', author: 'Platform Team' },
|
||||
],
|
||||
};
|
||||
}}
|
||||
>
|
||||
<upladmin-incident-update></upladmin-incident-update>
|
||||
</dees-demowrapper>
|
||||
</div>
|
||||
`;
|
||||
213
ts_web/pages/adminpage-monitors.ts
Normal file
213
ts_web/pages/adminpage-monitors.ts
Normal file
@@ -0,0 +1,213 @@
|
||||
import { html, cssManager } from "@design.estate/dees-element";
|
||||
import type { IServiceStatus } from '../interfaces/index.js';
|
||||
import '../elements/index.js';
|
||||
|
||||
export const adminpageMonitors = () => html`
|
||||
<style>
|
||||
.demo-page-wrapper {
|
||||
min-height: 100vh;
|
||||
background: ${cssManager.bdTheme('#fafafa', '#0a0a0a')};
|
||||
padding: 24px;
|
||||
}
|
||||
</style>
|
||||
|
||||
<div class="demo-page-wrapper">
|
||||
<dees-demowrapper
|
||||
.runAfterRender=${async (wrapperElement: any) => {
|
||||
const monitorList = wrapperElement.querySelector('upladmin-monitor-list') as any;
|
||||
|
||||
const monitors: IServiceStatus[] = [
|
||||
{
|
||||
id: 'api-server',
|
||||
name: 'api-server',
|
||||
displayName: 'API Server',
|
||||
description: 'Main REST API endpoint',
|
||||
currentStatus: 'operational',
|
||||
lastChecked: Date.now(),
|
||||
uptime30d: 99.98,
|
||||
uptime90d: 99.95,
|
||||
responseTime: 45,
|
||||
category: 'Core Services',
|
||||
},
|
||||
{
|
||||
id: 'web-app',
|
||||
name: 'web-app',
|
||||
displayName: 'Web Application',
|
||||
description: 'Customer-facing web application',
|
||||
currentStatus: 'operational',
|
||||
lastChecked: Date.now(),
|
||||
uptime30d: 99.99,
|
||||
uptime90d: 99.97,
|
||||
responseTime: 120,
|
||||
category: 'Core Services',
|
||||
},
|
||||
{
|
||||
id: 'database-primary',
|
||||
name: 'database-primary',
|
||||
displayName: 'Primary Database',
|
||||
description: 'PostgreSQL primary node',
|
||||
currentStatus: 'operational',
|
||||
lastChecked: Date.now(),
|
||||
uptime30d: 99.999,
|
||||
uptime90d: 99.998,
|
||||
responseTime: 5,
|
||||
category: 'Infrastructure',
|
||||
},
|
||||
{
|
||||
id: 'database-replica',
|
||||
name: 'database-replica',
|
||||
displayName: 'Database Replica',
|
||||
description: 'PostgreSQL read replica',
|
||||
currentStatus: 'operational',
|
||||
lastChecked: Date.now(),
|
||||
uptime30d: 99.99,
|
||||
uptime90d: 99.95,
|
||||
responseTime: 8,
|
||||
category: 'Infrastructure',
|
||||
},
|
||||
{
|
||||
id: 'cdn',
|
||||
name: 'cdn',
|
||||
displayName: 'Content Delivery Network',
|
||||
description: 'Global CDN for static assets',
|
||||
currentStatus: 'degraded',
|
||||
lastChecked: Date.now(),
|
||||
uptime30d: 99.5,
|
||||
uptime90d: 99.8,
|
||||
responseTime: 200,
|
||||
category: 'Infrastructure',
|
||||
},
|
||||
{
|
||||
id: 'redis-cache',
|
||||
name: 'redis-cache',
|
||||
displayName: 'Redis Cache',
|
||||
description: 'In-memory caching layer',
|
||||
currentStatus: 'operational',
|
||||
lastChecked: Date.now(),
|
||||
uptime30d: 99.99,
|
||||
uptime90d: 99.98,
|
||||
responseTime: 2,
|
||||
category: 'Infrastructure',
|
||||
},
|
||||
{
|
||||
id: 'email-service',
|
||||
name: 'email-service',
|
||||
displayName: 'Email Service',
|
||||
description: 'Transactional email delivery',
|
||||
currentStatus: 'operational',
|
||||
lastChecked: Date.now(),
|
||||
uptime30d: 99.9,
|
||||
uptime90d: 99.85,
|
||||
responseTime: 500,
|
||||
category: 'External Services',
|
||||
},
|
||||
{
|
||||
id: 'payment-gateway',
|
||||
name: 'payment-gateway',
|
||||
displayName: 'Payment Gateway',
|
||||
description: 'Payment processing integration',
|
||||
currentStatus: 'maintenance',
|
||||
lastChecked: Date.now(),
|
||||
uptime30d: 99.95,
|
||||
uptime90d: 99.9,
|
||||
responseTime: 350,
|
||||
category: 'External Services',
|
||||
},
|
||||
{
|
||||
id: 'sms-service',
|
||||
name: 'sms-service',
|
||||
displayName: 'SMS Service',
|
||||
description: 'SMS notifications and 2FA',
|
||||
currentStatus: 'operational',
|
||||
lastChecked: Date.now(),
|
||||
uptime30d: 99.8,
|
||||
uptime90d: 99.75,
|
||||
responseTime: 800,
|
||||
category: 'External Services',
|
||||
},
|
||||
{
|
||||
id: 'search-engine',
|
||||
name: 'search-engine',
|
||||
displayName: 'Search Engine',
|
||||
description: 'Elasticsearch cluster',
|
||||
currentStatus: 'partial_outage',
|
||||
lastChecked: Date.now(),
|
||||
uptime30d: 98.5,
|
||||
uptime90d: 99.2,
|
||||
responseTime: 150,
|
||||
category: 'Core Services',
|
||||
},
|
||||
];
|
||||
|
||||
monitorList.monitors = monitors;
|
||||
}}
|
||||
>
|
||||
<upladmin-monitor-list></upladmin-monitor-list>
|
||||
</dees-demowrapper>
|
||||
</div>
|
||||
`;
|
||||
|
||||
export const adminpageMonitorForm = () => html`
|
||||
<style>
|
||||
.demo-page-wrapper {
|
||||
min-height: 100vh;
|
||||
background: ${cssManager.bdTheme('#fafafa', '#0a0a0a')};
|
||||
padding: 24px;
|
||||
}
|
||||
</style>
|
||||
|
||||
<div class="demo-page-wrapper">
|
||||
<dees-demowrapper
|
||||
.runAfterRender=${async (wrapperElement: any) => {
|
||||
const monitorForm = wrapperElement.querySelector('upladmin-monitor-form') as any;
|
||||
|
||||
const availableMonitors: IServiceStatus[] = [
|
||||
{ id: 'api-server', name: 'api-server', displayName: 'API Server', currentStatus: 'operational', lastChecked: Date.now(), uptime30d: 99.98, uptime90d: 99.95, responseTime: 45 },
|
||||
{ id: 'database-primary', name: 'database-primary', displayName: 'Primary Database', currentStatus: 'operational', lastChecked: Date.now(), uptime30d: 99.999, uptime90d: 99.998, responseTime: 5 },
|
||||
];
|
||||
|
||||
monitorForm.availableMonitors = availableMonitors;
|
||||
monitorForm.categories = ['Core Services', 'Infrastructure', 'External Services', 'Web Services'];
|
||||
}}
|
||||
>
|
||||
<upladmin-monitor-form></upladmin-monitor-form>
|
||||
</dees-demowrapper>
|
||||
</div>
|
||||
`;
|
||||
|
||||
export const adminpageMonitorFormEdit = () => html`
|
||||
<style>
|
||||
.demo-page-wrapper {
|
||||
min-height: 100vh;
|
||||
background: ${cssManager.bdTheme('#fafafa', '#0a0a0a')};
|
||||
padding: 24px;
|
||||
}
|
||||
</style>
|
||||
|
||||
<div class="demo-page-wrapper">
|
||||
<dees-demowrapper
|
||||
.runAfterRender=${async (wrapperElement: any) => {
|
||||
const monitorForm = wrapperElement.querySelector('upladmin-monitor-form') as any;
|
||||
|
||||
const availableMonitors: IServiceStatus[] = [
|
||||
{ id: 'api-server', name: 'api-server', displayName: 'API Server', currentStatus: 'operational', lastChecked: Date.now(), uptime30d: 99.98, uptime90d: 99.95, responseTime: 45 },
|
||||
{ id: 'database-primary', name: 'database-primary', displayName: 'Primary Database', currentStatus: 'operational', lastChecked: Date.now(), uptime30d: 99.999, uptime90d: 99.998, responseTime: 5 },
|
||||
];
|
||||
|
||||
monitorForm.availableMonitors = availableMonitors;
|
||||
monitorForm.categories = ['Core Services', 'Infrastructure', 'External Services', 'Web Services'];
|
||||
monitorForm.monitor = {
|
||||
id: 'cdn',
|
||||
name: 'cdn',
|
||||
displayName: 'Content Delivery Network',
|
||||
description: 'Global CDN for static assets and media files',
|
||||
category: 'Infrastructure',
|
||||
dependencies: ['api-server'],
|
||||
currentStatus: 'degraded',
|
||||
};
|
||||
}}
|
||||
>
|
||||
<upladmin-monitor-form></upladmin-monitor-form>
|
||||
</dees-demowrapper>
|
||||
</div>
|
||||
`;
|
||||
5
ts_web/pages/index.ts
Normal file
5
ts_web/pages/index.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
export * from './adminpage-dashboard.js';
|
||||
export * from './adminpage-monitors.js';
|
||||
export * from './adminpage-incidents.js';
|
||||
export * from './adminpage-config.js';
|
||||
export { demoFunc as adminpageApp } from './upladmin-app/upladmin-app.demo.js';
|
||||
173
ts_web/pages/upladmin-app/upladmin-app.demo.ts
Normal file
173
ts_web/pages/upladmin-app/upladmin-app.demo.ts
Normal file
@@ -0,0 +1,173 @@
|
||||
import { html } from '@design.estate/dees-element';
|
||||
import { adminState } from '../../services/admin-state.js';
|
||||
import type { IServiceStatus, IIncidentDetails, IStatusPageConfig } from '../../interfaces/index.js';
|
||||
import './upladmin-app.js';
|
||||
|
||||
// Initialize demo data
|
||||
const initDemoData = () => {
|
||||
const now = Date.now();
|
||||
|
||||
// Demo monitors
|
||||
const monitors: IServiceStatus[] = [
|
||||
{
|
||||
id: 'api-server',
|
||||
name: 'api-server',
|
||||
displayName: 'API Server',
|
||||
description: 'Main REST API backend',
|
||||
category: 'Core Services',
|
||||
currentStatus: 'operational',
|
||||
lastChecked: now,
|
||||
uptime30d: 99.98,
|
||||
uptime90d: 99.95,
|
||||
responseTime: 45,
|
||||
dependencies: [],
|
||||
},
|
||||
{
|
||||
id: 'web-app',
|
||||
name: 'web-app',
|
||||
displayName: 'Web Application',
|
||||
description: 'Frontend web application',
|
||||
category: 'Core Services',
|
||||
currentStatus: 'operational',
|
||||
lastChecked: now,
|
||||
uptime30d: 99.95,
|
||||
uptime90d: 99.90,
|
||||
responseTime: 120,
|
||||
dependencies: ['api-server'],
|
||||
},
|
||||
{
|
||||
id: 'database',
|
||||
name: 'database',
|
||||
displayName: 'Database',
|
||||
description: 'Primary PostgreSQL database',
|
||||
category: 'Infrastructure',
|
||||
currentStatus: 'operational',
|
||||
lastChecked: now,
|
||||
uptime30d: 99.99,
|
||||
uptime90d: 99.98,
|
||||
responseTime: 5,
|
||||
dependencies: [],
|
||||
},
|
||||
{
|
||||
id: 'cdn',
|
||||
name: 'cdn',
|
||||
displayName: 'CDN',
|
||||
description: 'Content delivery network',
|
||||
category: 'Infrastructure',
|
||||
currentStatus: 'degraded',
|
||||
lastChecked: now,
|
||||
uptime30d: 99.85,
|
||||
uptime90d: 99.80,
|
||||
responseTime: 25,
|
||||
dependencies: [],
|
||||
},
|
||||
{
|
||||
id: 'email-service',
|
||||
name: 'email-service',
|
||||
displayName: 'Email Service',
|
||||
description: 'Transactional email delivery',
|
||||
category: 'External Services',
|
||||
currentStatus: 'operational',
|
||||
lastChecked: now,
|
||||
uptime30d: 99.90,
|
||||
uptime90d: 99.85,
|
||||
responseTime: 200,
|
||||
dependencies: [],
|
||||
},
|
||||
];
|
||||
|
||||
// Demo incidents
|
||||
const incidents: IIncidentDetails[] = [
|
||||
{
|
||||
id: 'incident-1',
|
||||
title: 'CDN Performance Degradation',
|
||||
impact: 'We are experiencing slower than normal response times from our CDN provider.',
|
||||
severity: 'minor',
|
||||
status: 'monitoring',
|
||||
affectedServices: ['cdn'],
|
||||
startTime: now - 2 * 60 * 60 * 1000, // 2 hours ago
|
||||
updates: [
|
||||
{
|
||||
id: 'update-1-1',
|
||||
status: 'investigating',
|
||||
message: 'We are investigating reports of slow load times.',
|
||||
timestamp: now - 2 * 60 * 60 * 1000,
|
||||
},
|
||||
{
|
||||
id: 'update-1-2',
|
||||
status: 'identified',
|
||||
message: 'The issue has been identified as a CDN edge node problem.',
|
||||
timestamp: now - 1 * 60 * 60 * 1000,
|
||||
},
|
||||
{
|
||||
id: 'update-1-3',
|
||||
status: 'monitoring',
|
||||
message: 'A fix has been deployed. We are monitoring the situation.',
|
||||
timestamp: now - 30 * 60 * 1000,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'incident-2',
|
||||
title: 'Scheduled Database Maintenance',
|
||||
impact: 'Routine database maintenance window.',
|
||||
severity: 'maintenance',
|
||||
status: 'resolved',
|
||||
affectedServices: ['database'],
|
||||
startTime: now - 24 * 60 * 60 * 1000,
|
||||
endTime: now - 23 * 60 * 60 * 1000,
|
||||
updates: [
|
||||
{
|
||||
id: 'update-2-1',
|
||||
status: 'investigating',
|
||||
message: 'Maintenance has begun.',
|
||||
timestamp: now - 24 * 60 * 60 * 1000,
|
||||
},
|
||||
{
|
||||
id: 'update-2-2',
|
||||
status: 'resolved',
|
||||
message: 'Maintenance completed successfully.',
|
||||
timestamp: now - 23 * 60 * 60 * 1000,
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
// Demo config
|
||||
const config: IStatusPageConfig = {
|
||||
theme: 'dark',
|
||||
companyName: 'uptime.link',
|
||||
companyLogo: '',
|
||||
supportEmail: 'support@uptime.link',
|
||||
showHistoricalDays: 90,
|
||||
timeZone: 'UTC',
|
||||
};
|
||||
|
||||
// Set demo data in state
|
||||
adminState.monitors = monitors;
|
||||
adminState.incidents = incidents;
|
||||
adminState.config = config;
|
||||
};
|
||||
|
||||
export const demoFunc = () => {
|
||||
// Initialize demo data
|
||||
initDemoData();
|
||||
|
||||
return html`
|
||||
<dees-demowrapper>
|
||||
<style>
|
||||
.demo-container {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
}
|
||||
</style>
|
||||
<div class="demo-container">
|
||||
<upladmin-app></upladmin-app>
|
||||
</div>
|
||||
</dees-demowrapper>
|
||||
`;
|
||||
};
|
||||
283
ts_web/pages/upladmin-app/upladmin-app.ts
Normal file
283
ts_web/pages/upladmin-app/upladmin-app.ts
Normal file
@@ -0,0 +1,283 @@
|
||||
import {
|
||||
DeesElement,
|
||||
customElement,
|
||||
html,
|
||||
css,
|
||||
cssManager,
|
||||
state,
|
||||
type TemplateResult,
|
||||
} from '@design.estate/dees-element';
|
||||
import type { DeesAppuiBase } from '@design.estate/dees-catalog';
|
||||
import { adminState } from '../../services/admin-state.js';
|
||||
import { demoFunc } from './upladmin-app.demo.js';
|
||||
|
||||
// Import components directly
|
||||
import '../../elements/upladmin-dashboard/upladmin-dashboard.js';
|
||||
import '../../elements/upladmin-monitor-list/upladmin-monitor-list.js';
|
||||
import '../../elements/upladmin-monitor-form/upladmin-monitor-form.js';
|
||||
import '../../elements/upladmin-incident-list/upladmin-incident-list.js';
|
||||
import '../../elements/upladmin-incident-form/upladmin-incident-form.js';
|
||||
import '../../elements/upladmin-incident-update/upladmin-incident-update.js';
|
||||
import '../../elements/upladmin-statuspage-config/upladmin-statuspage-config.js';
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
'upladmin-app': UpladminApp;
|
||||
}
|
||||
}
|
||||
|
||||
@customElement('upladmin-app')
|
||||
export class UpladminApp extends DeesElement {
|
||||
public static demo = demoFunc;
|
||||
|
||||
@state()
|
||||
accessor appuiBase: DeesAppuiBase | null = null;
|
||||
|
||||
public static styles = [
|
||||
cssManager.defaultStyles,
|
||||
css`
|
||||
:host {
|
||||
display: block;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
`,
|
||||
];
|
||||
|
||||
async firstUpdated() {
|
||||
await this.updateComplete;
|
||||
this.appuiBase = this.shadowRoot!.querySelector('dees-appui-base') as DeesAppuiBase;
|
||||
|
||||
if (this.appuiBase) {
|
||||
await this.appuiBase.updateComplete;
|
||||
this.configureApp();
|
||||
}
|
||||
}
|
||||
|
||||
private configureApp() {
|
||||
if (!this.appuiBase) return;
|
||||
|
||||
const appConfig = {
|
||||
branding: {
|
||||
logoIcon: 'lucide:activity',
|
||||
logoText: 'uptime.link',
|
||||
},
|
||||
|
||||
appBar: {
|
||||
menuItems: [
|
||||
{
|
||||
name: 'File',
|
||||
action: async () => {},
|
||||
submenu: [
|
||||
{
|
||||
name: 'New Monitor',
|
||||
shortcut: 'Cmd+N',
|
||||
iconName: 'plus',
|
||||
action: async () => (window.location.hash = 'monitors/create'),
|
||||
},
|
||||
{
|
||||
name: 'New Incident',
|
||||
shortcut: 'Cmd+I',
|
||||
iconName: 'alertTriangle',
|
||||
action: async () => (window.location.hash = 'incidents/create'),
|
||||
},
|
||||
{ divider: true },
|
||||
{
|
||||
name: 'Reload Data',
|
||||
shortcut: 'Cmd+R',
|
||||
iconName: 'refreshCw',
|
||||
action: async () => this.reloadData(),
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'View',
|
||||
action: async () => {},
|
||||
submenu: [
|
||||
{
|
||||
name: 'Dashboard',
|
||||
iconName: 'layoutDashboard',
|
||||
action: async () => (window.location.hash = 'dashboard'),
|
||||
},
|
||||
{
|
||||
name: 'Monitors',
|
||||
iconName: 'activity',
|
||||
action: async () => (window.location.hash = 'monitors'),
|
||||
},
|
||||
{
|
||||
name: 'Incidents',
|
||||
iconName: 'alertCircle',
|
||||
action: async () => (window.location.hash = 'incidents'),
|
||||
},
|
||||
{ divider: true },
|
||||
{
|
||||
name: 'Settings',
|
||||
iconName: 'settings',
|
||||
action: async () => (window.location.hash = 'config'),
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'Help',
|
||||
action: async () => {},
|
||||
submenu: [
|
||||
{
|
||||
name: 'Documentation',
|
||||
iconName: 'book',
|
||||
action: async () => window.open('https://uptime.link/docs', '_blank'),
|
||||
},
|
||||
{
|
||||
name: 'API Reference',
|
||||
iconName: 'code',
|
||||
action: async () => window.open('https://uptime.link/api', '_blank'),
|
||||
},
|
||||
{ divider: true },
|
||||
{
|
||||
name: 'About uptime.link',
|
||||
iconName: 'info',
|
||||
action: async () => console.log('About'),
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
breadcrumbs: 'Dashboard',
|
||||
showWindowControls: false,
|
||||
showSearch: true,
|
||||
user: {
|
||||
name: 'Admin User',
|
||||
email: 'admin@uptime.link',
|
||||
status: 'online',
|
||||
},
|
||||
profileMenuItems: [
|
||||
{
|
||||
name: 'Profile',
|
||||
iconName: 'user',
|
||||
action: async () => console.log('Profile'),
|
||||
},
|
||||
{
|
||||
name: 'Account Settings',
|
||||
iconName: 'settings',
|
||||
action: async () => (window.location.hash = 'config'),
|
||||
},
|
||||
{ divider: true },
|
||||
{
|
||||
name: 'Sign Out',
|
||||
iconName: 'logOut',
|
||||
action: async () => console.log('Sign out'),
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
views: [
|
||||
{
|
||||
id: 'dashboard',
|
||||
name: 'Dashboard',
|
||||
iconName: 'lucide:layoutDashboard',
|
||||
content: 'upladmin-dashboard',
|
||||
route: 'dashboard',
|
||||
},
|
||||
{
|
||||
id: 'monitors',
|
||||
name: 'Monitors',
|
||||
iconName: 'lucide:activity',
|
||||
content: 'upladmin-monitor-list',
|
||||
route: 'monitors',
|
||||
badge: adminState.monitors.length,
|
||||
},
|
||||
{
|
||||
id: 'monitor-form',
|
||||
name: 'Monitor',
|
||||
iconName: 'lucide:activity',
|
||||
content: 'upladmin-monitor-form',
|
||||
route: 'monitors/:id',
|
||||
cache: false,
|
||||
},
|
||||
{
|
||||
id: 'incidents',
|
||||
name: 'Incidents',
|
||||
iconName: 'lucide:alertCircle',
|
||||
content: 'upladmin-incident-list',
|
||||
route: 'incidents',
|
||||
badge: adminState.getActiveIncidents().length,
|
||||
badgeVariant: adminState.getActiveIncidents().length > 0 ? 'warning' : 'default',
|
||||
},
|
||||
{
|
||||
id: 'incident-form',
|
||||
name: 'Incident',
|
||||
iconName: 'lucide:alertCircle',
|
||||
content: 'upladmin-incident-form',
|
||||
route: 'incidents/:id',
|
||||
cache: false,
|
||||
},
|
||||
{
|
||||
id: 'incident-update',
|
||||
name: 'Post Update',
|
||||
iconName: 'lucide:messageSquarePlus',
|
||||
content: 'upladmin-incident-update',
|
||||
route: 'incidents/:id/update',
|
||||
cache: false,
|
||||
},
|
||||
{
|
||||
id: 'config',
|
||||
name: 'Settings',
|
||||
iconName: 'lucide:settings',
|
||||
content: 'upladmin-statuspage-config',
|
||||
route: 'config',
|
||||
},
|
||||
],
|
||||
|
||||
mainMenu: {
|
||||
sections: [
|
||||
{ name: 'Overview', views: ['dashboard'] },
|
||||
{ name: 'Management', views: ['monitors', 'incidents'] },
|
||||
],
|
||||
bottomItems: ['config'],
|
||||
},
|
||||
|
||||
defaultView: 'dashboard',
|
||||
|
||||
onViewChange: (viewId, view) => {
|
||||
console.log(`View changed to: ${viewId} (${view.name})`);
|
||||
},
|
||||
|
||||
onSearch: (query) => {
|
||||
console.log('Search query:', query);
|
||||
// Implement search functionality
|
||||
},
|
||||
};
|
||||
|
||||
this.appuiBase.configure(appConfig as any);
|
||||
|
||||
// Update badges when state changes
|
||||
this.setupStateSubscriptions();
|
||||
}
|
||||
|
||||
private setupStateSubscriptions() {
|
||||
if (!this.appuiBase) return;
|
||||
|
||||
const appui = this.appuiBase;
|
||||
|
||||
adminState.monitors$.subscribe((monitors) => {
|
||||
appui.setMainMenuBadge('monitors', monitors.length);
|
||||
});
|
||||
|
||||
adminState.incidents$.subscribe((incidents) => {
|
||||
const activeCount = incidents.filter(
|
||||
(i) => !['resolved', 'postmortem'].includes(i.status)
|
||||
).length;
|
||||
appui.setMainMenuBadge('incidents', activeCount);
|
||||
});
|
||||
}
|
||||
|
||||
private async reloadData() {
|
||||
console.log('Reloading data...');
|
||||
// Implement data reload
|
||||
}
|
||||
|
||||
public render(): TemplateResult {
|
||||
return html`<dees-appui-base></dees-appui-base>`;
|
||||
}
|
||||
}
|
||||
9
ts_web/plugins.ts
Normal file
9
ts_web/plugins.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
import * as domtools from '@design.estate/dees-domtools';
|
||||
import * as deesCatalog from '@design.estate/dees-catalog';
|
||||
import * as uplInterfaces from '@uptime.link/interfaces';
|
||||
|
||||
export {
|
||||
domtools,
|
||||
deesCatalog,
|
||||
uplInterfaces
|
||||
}
|
||||
172
ts_web/services/admin-state.ts
Normal file
172
ts_web/services/admin-state.ts
Normal file
@@ -0,0 +1,172 @@
|
||||
import type {
|
||||
IServiceStatus,
|
||||
IIncidentDetails,
|
||||
IStatusPageConfig,
|
||||
IMonitorFormData,
|
||||
IIncidentFormData,
|
||||
} from '../interfaces/index.js';
|
||||
|
||||
type TStateChangeListener<T> = (data: T) => void;
|
||||
|
||||
/**
|
||||
* Simple observable implementation for state changes
|
||||
*/
|
||||
class SimpleObservable<T> {
|
||||
private listeners: Set<TStateChangeListener<T>> = new Set();
|
||||
|
||||
subscribe(listener: TStateChangeListener<T>): () => void {
|
||||
this.listeners.add(listener);
|
||||
return () => this.listeners.delete(listener);
|
||||
}
|
||||
|
||||
next(value: T): void {
|
||||
for (const listener of this.listeners) {
|
||||
listener(value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Centralized state management for the admin dashboard.
|
||||
* Handles cross-view data passing and state synchronization.
|
||||
*/
|
||||
export class AdminState {
|
||||
// Observable subjects for reactive updates
|
||||
public monitors$ = new SimpleObservable<IServiceStatus[]>();
|
||||
public incidents$ = new SimpleObservable<IIncidentDetails[]>();
|
||||
public config$ = new SimpleObservable<IStatusPageConfig>();
|
||||
|
||||
// Current data
|
||||
private _monitors: IServiceStatus[] = [];
|
||||
private _incidents: IIncidentDetails[] = [];
|
||||
private _config: IStatusPageConfig | null = null;
|
||||
|
||||
// Selected items for navigation context
|
||||
private _selectedMonitor: IServiceStatus | null = null;
|
||||
private _selectedIncident: IIncidentDetails | null = null;
|
||||
|
||||
// Monitors
|
||||
get monitors(): IServiceStatus[] {
|
||||
return this._monitors;
|
||||
}
|
||||
|
||||
set monitors(value: IServiceStatus[]) {
|
||||
this._monitors = value;
|
||||
this.monitors$.next(value);
|
||||
}
|
||||
|
||||
// Incidents
|
||||
get incidents(): IIncidentDetails[] {
|
||||
return this._incidents;
|
||||
}
|
||||
|
||||
set incidents(value: IIncidentDetails[]) {
|
||||
this._incidents = value;
|
||||
this.incidents$.next(value);
|
||||
}
|
||||
|
||||
// Config
|
||||
get config(): IStatusPageConfig | null {
|
||||
return this._config;
|
||||
}
|
||||
|
||||
set config(value: IStatusPageConfig | null) {
|
||||
this._config = value;
|
||||
if (value) {
|
||||
this.config$.next(value);
|
||||
}
|
||||
}
|
||||
|
||||
// Selected monitor for edit navigation
|
||||
setSelectedMonitor(monitor: IServiceStatus | null): void {
|
||||
this._selectedMonitor = monitor;
|
||||
}
|
||||
|
||||
getSelectedMonitor(): IServiceStatus | null {
|
||||
return this._selectedMonitor;
|
||||
}
|
||||
|
||||
clearSelectedMonitor(): void {
|
||||
this._selectedMonitor = null;
|
||||
}
|
||||
|
||||
// Selected incident for edit navigation
|
||||
setSelectedIncident(incident: IIncidentDetails | null): void {
|
||||
this._selectedIncident = incident;
|
||||
}
|
||||
|
||||
getSelectedIncident(): IIncidentDetails | null {
|
||||
return this._selectedIncident;
|
||||
}
|
||||
|
||||
clearSelectedIncident(): void {
|
||||
this._selectedIncident = null;
|
||||
}
|
||||
|
||||
// Helper methods
|
||||
getCategories(): string[] {
|
||||
const categories = new Set<string>();
|
||||
for (const monitor of this._monitors) {
|
||||
if (monitor.category) {
|
||||
categories.add(monitor.category);
|
||||
}
|
||||
}
|
||||
return Array.from(categories).sort();
|
||||
}
|
||||
|
||||
getAvailableServices(): IServiceStatus[] {
|
||||
return [...this._monitors];
|
||||
}
|
||||
|
||||
getMonitorById(id: string): IServiceStatus | undefined {
|
||||
return this._monitors.find(m => m.id === id);
|
||||
}
|
||||
|
||||
getIncidentById(id: string): IIncidentDetails | undefined {
|
||||
return this._incidents.find(i => i.id === id);
|
||||
}
|
||||
|
||||
getActiveIncidents(): IIncidentDetails[] {
|
||||
return this._incidents.filter(
|
||||
i => !['resolved', 'postmortem'].includes(i.status)
|
||||
);
|
||||
}
|
||||
|
||||
getPastIncidents(): IIncidentDetails[] {
|
||||
return this._incidents.filter(
|
||||
i => ['resolved', 'postmortem'].includes(i.status)
|
||||
);
|
||||
}
|
||||
|
||||
// CRUD operations (these would typically call an API)
|
||||
addMonitor(monitor: IServiceStatus): void {
|
||||
this.monitors = [...this._monitors, monitor];
|
||||
}
|
||||
|
||||
updateMonitor(id: string, data: Partial<IMonitorFormData>): void {
|
||||
this.monitors = this._monitors.map(m =>
|
||||
m.id === id ? { ...m, ...data } : m
|
||||
);
|
||||
}
|
||||
|
||||
deleteMonitor(id: string): void {
|
||||
this.monitors = this._monitors.filter(m => m.id !== id);
|
||||
}
|
||||
|
||||
addIncident(incident: IIncidentDetails): void {
|
||||
this.incidents = [...this._incidents, incident];
|
||||
}
|
||||
|
||||
updateIncident(id: string, data: Partial<IIncidentFormData>): void {
|
||||
this.incidents = this._incidents.map(i =>
|
||||
i.id === id ? { ...i, ...data } : i
|
||||
);
|
||||
}
|
||||
|
||||
deleteIncident(id: string): void {
|
||||
this.incidents = this._incidents.filter(i => i.id !== id);
|
||||
}
|
||||
}
|
||||
|
||||
// Singleton instance
|
||||
export const adminState = new AdminState();
|
||||
1
ts_web/services/index.ts
Normal file
1
ts_web/services/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from './admin-state.js';
|
||||
677
ts_web/styles/shared.styles.ts
Normal file
677
ts_web/styles/shared.styles.ts
Normal file
@@ -0,0 +1,677 @@
|
||||
import { css, cssManager, unsafeCSS } from '@design.estate/dees-element';
|
||||
|
||||
export const fonts = {
|
||||
base: `'Geist Sans', -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen", "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue", sans-serif`,
|
||||
mono: `'Geist Mono', ui-monospace, SFMono-Regular, "SF Mono", Consolas, "Liberation Mono", Menlo, monospace`
|
||||
};
|
||||
|
||||
export const colors = {
|
||||
background: {
|
||||
primary: cssManager.bdTheme('#ffffff', '#09090b'),
|
||||
secondary: cssManager.bdTheme('#fafafa', '#18181b'),
|
||||
muted: cssManager.bdTheme('#f4f4f5', '#27272a'),
|
||||
card: cssManager.bdTheme('#ffffff', '#0f0f12'),
|
||||
elevated: cssManager.bdTheme('#ffffff', '#1a1a1e')
|
||||
},
|
||||
border: {
|
||||
default: cssManager.bdTheme('#e4e4e7', '#27272a'),
|
||||
muted: cssManager.bdTheme('#f4f4f5', '#3f3f46'),
|
||||
subtle: cssManager.bdTheme('#f0f0f2', '#1f1f23'),
|
||||
light: cssManager.bdTheme('#f4f4f5', '#27272a'),
|
||||
strong: cssManager.bdTheme('#d4d4d8', '#3f3f46')
|
||||
},
|
||||
text: {
|
||||
primary: cssManager.bdTheme('#09090b', '#fafafa'),
|
||||
secondary: cssManager.bdTheme('#71717a', '#a1a1aa'),
|
||||
muted: cssManager.bdTheme('#a1a1aa', '#71717a')
|
||||
},
|
||||
status: {
|
||||
operational: cssManager.bdTheme('#16a34a', '#22c55e'),
|
||||
degraded: cssManager.bdTheme('#d97706', '#fbbf24'),
|
||||
partial: cssManager.bdTheme('#dc2626', '#f87171'),
|
||||
major: cssManager.bdTheme('#b91c1c', '#ef4444'),
|
||||
maintenance: cssManager.bdTheme('#2563eb', '#60a5fa'),
|
||||
partialOutage: cssManager.bdTheme('#f97316', '#fb923c'),
|
||||
majorOutage: cssManager.bdTheme('#ef4444', '#f87171')
|
||||
},
|
||||
accent: {
|
||||
primary: cssManager.bdTheme('#3b82f6', '#60a5fa'),
|
||||
hover: cssManager.bdTheme('#2563eb', '#3b82f6'),
|
||||
focus: cssManager.bdTheme('#3b82f6', '#60a5fa'),
|
||||
danger: cssManager.bdTheme('#dc2626', '#ef4444'),
|
||||
dangerHover: cssManager.bdTheme('#b91c1c', '#dc2626'),
|
||||
success: cssManager.bdTheme('#16a34a', '#22c55e'),
|
||||
warning: cssManager.bdTheme('#f59e0b', '#fbbf24')
|
||||
}
|
||||
};
|
||||
|
||||
export const shadows = {
|
||||
xs: '0 1px 2px 0 rgba(0, 0, 0, 0.03)',
|
||||
sm: '0 1px 3px 0 rgba(0, 0, 0, 0.06), 0 1px 2px -1px rgba(0, 0, 0, 0.06)',
|
||||
base: '0 4px 6px -1px rgba(0, 0, 0, 0.07), 0 2px 4px -2px rgba(0, 0, 0, 0.05)',
|
||||
md: '0 6px 12px -2px rgba(0, 0, 0, 0.08), 0 3px 7px -3px rgba(0, 0, 0, 0.05)',
|
||||
lg: '0 12px 24px -4px rgba(0, 0, 0, 0.1), 0 6px 12px -6px rgba(0, 0, 0, 0.05)',
|
||||
xl: '0 24px 48px -12px rgba(0, 0, 0, 0.12), 0 12px 24px -12px rgba(0, 0, 0, 0.05)',
|
||||
inner: 'inset 0 2px 4px 0 rgba(0, 0, 0, 0.04)'
|
||||
};
|
||||
|
||||
export const borderRadius = {
|
||||
xs: '3px',
|
||||
sm: '4px',
|
||||
base: '6px',
|
||||
md: '8px',
|
||||
lg: '12px',
|
||||
xl: '16px',
|
||||
'2xl': '24px',
|
||||
full: '9999px'
|
||||
};
|
||||
|
||||
export const spacing = {
|
||||
xs: '4px',
|
||||
sm: '8px',
|
||||
md: '16px',
|
||||
lg: '24px',
|
||||
xl: '32px',
|
||||
'2xl': '48px',
|
||||
'3xl': '64px',
|
||||
'4xl': '96px'
|
||||
};
|
||||
|
||||
export const easings = {
|
||||
default: 'cubic-bezier(0.4, 0, 0.2, 1)',
|
||||
smooth: 'cubic-bezier(0.4, 0, 0.6, 1)',
|
||||
bounce: 'cubic-bezier(0.68, -0.55, 0.265, 1.55)',
|
||||
snappy: 'cubic-bezier(0.2, 0, 0, 1)',
|
||||
spring: 'cubic-bezier(0.175, 0.885, 0.32, 1.275)'
|
||||
};
|
||||
|
||||
export const durations = {
|
||||
instant: '50ms',
|
||||
fast: '100ms',
|
||||
normal: '200ms',
|
||||
slow: '300ms',
|
||||
slower: '500ms',
|
||||
slowest: '800ms'
|
||||
};
|
||||
|
||||
export const commonStyles = css`
|
||||
/* Button styles */
|
||||
.button {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-family: ${unsafeCSS(fonts.base)};
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
height: 36px;
|
||||
padding: 0 14px;
|
||||
border-radius: ${unsafeCSS(borderRadius.base)};
|
||||
border: 1px solid ${colors.border.default};
|
||||
background: ${colors.background.primary};
|
||||
color: ${colors.text.primary};
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
transition: all ${unsafeCSS(durations.normal)} ${unsafeCSS(easings.default)};
|
||||
gap: 6px;
|
||||
white-space: nowrap;
|
||||
letter-spacing: -0.01em;
|
||||
}
|
||||
|
||||
.button:hover {
|
||||
background: ${colors.background.secondary};
|
||||
border-color: ${cssManager.bdTheme('#d4d4d8', '#3f3f46')};
|
||||
box-shadow: ${unsafeCSS(shadows.xs)};
|
||||
}
|
||||
|
||||
.button:active {
|
||||
transform: scale(0.98);
|
||||
}
|
||||
|
||||
.button:focus-visible {
|
||||
outline: 2px solid ${colors.accent.focus};
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
.button.primary {
|
||||
background: ${colors.accent.primary};
|
||||
color: #ffffff;
|
||||
border-color: transparent;
|
||||
}
|
||||
|
||||
.button.primary:hover {
|
||||
background: ${colors.accent.hover};
|
||||
box-shadow: ${unsafeCSS(shadows.sm)};
|
||||
}
|
||||
|
||||
.button.danger {
|
||||
background: ${colors.accent.danger};
|
||||
color: #ffffff;
|
||||
border-color: transparent;
|
||||
}
|
||||
|
||||
.button.danger:hover {
|
||||
background: ${colors.accent.dangerHover};
|
||||
}
|
||||
|
||||
.button.ghost {
|
||||
background: transparent;
|
||||
border-color: transparent;
|
||||
}
|
||||
|
||||
.button.ghost:hover {
|
||||
background: ${colors.background.muted};
|
||||
}
|
||||
|
||||
.button.sm {
|
||||
height: 32px;
|
||||
padding: 0 12px;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.button.lg {
|
||||
height: 44px;
|
||||
padding: 0 20px;
|
||||
font-size: 15px;
|
||||
}
|
||||
|
||||
.button:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
/* Card styles */
|
||||
.card {
|
||||
background: ${colors.background.card};
|
||||
border: 1px solid ${colors.border.default};
|
||||
border-radius: ${unsafeCSS(borderRadius.lg)};
|
||||
padding: ${unsafeCSS(spacing.lg)};
|
||||
box-shadow: ${unsafeCSS(shadows.sm)};
|
||||
}
|
||||
|
||||
/* Form styles */
|
||||
.form-group {
|
||||
margin-bottom: ${unsafeCSS(spacing.md)};
|
||||
}
|
||||
|
||||
.form-label {
|
||||
display: block;
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
color: ${colors.text.primary};
|
||||
margin-bottom: ${unsafeCSS(spacing.xs)};
|
||||
}
|
||||
|
||||
.form-label.required::after {
|
||||
content: ' *';
|
||||
color: ${colors.accent.danger};
|
||||
}
|
||||
|
||||
.form-input {
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: 40px;
|
||||
padding: 0 12px;
|
||||
font-family: ${unsafeCSS(fonts.base)};
|
||||
font-size: 14px;
|
||||
color: ${colors.text.primary};
|
||||
background: ${colors.background.primary};
|
||||
border: 1px solid ${colors.border.default};
|
||||
border-radius: ${unsafeCSS(borderRadius.base)};
|
||||
transition: border-color ${unsafeCSS(durations.fast)} ${unsafeCSS(easings.default)},
|
||||
box-shadow ${unsafeCSS(durations.fast)} ${unsafeCSS(easings.default)};
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.form-input:focus {
|
||||
outline: none;
|
||||
border-color: ${colors.accent.primary};
|
||||
box-shadow: 0 0 0 3px ${cssManager.bdTheme('rgba(59, 130, 246, 0.1)', 'rgba(96, 165, 250, 0.15)')};
|
||||
}
|
||||
|
||||
.form-input::placeholder {
|
||||
color: ${colors.text.muted};
|
||||
}
|
||||
|
||||
.form-input.error {
|
||||
border-color: ${colors.accent.danger};
|
||||
}
|
||||
|
||||
.form-input.error:focus {
|
||||
box-shadow: 0 0 0 3px ${cssManager.bdTheme('rgba(220, 38, 38, 0.1)', 'rgba(248, 113, 113, 0.15)')};
|
||||
}
|
||||
|
||||
.form-textarea {
|
||||
min-height: 100px;
|
||||
padding: 12px;
|
||||
resize: vertical;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.form-select {
|
||||
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 12px center;
|
||||
padding-right: 36px;
|
||||
}
|
||||
|
||||
.form-error {
|
||||
font-size: 12px;
|
||||
color: ${colors.accent.danger};
|
||||
margin-top: ${unsafeCSS(spacing.xs)};
|
||||
}
|
||||
|
||||
.form-hint {
|
||||
font-size: 12px;
|
||||
color: ${colors.text.muted};
|
||||
margin-top: ${unsafeCSS(spacing.xs)};
|
||||
}
|
||||
|
||||
/* Checkbox and toggle */
|
||||
.form-checkbox {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
color: ${colors.text.primary};
|
||||
}
|
||||
|
||||
.form-checkbox input[type="checkbox"] {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
border: 1px solid ${colors.border.default};
|
||||
border-radius: ${unsafeCSS(borderRadius.sm)};
|
||||
background: ${colors.background.primary};
|
||||
cursor: pointer;
|
||||
appearance: none;
|
||||
transition: all ${unsafeCSS(durations.fast)} ${unsafeCSS(easings.default)};
|
||||
}
|
||||
|
||||
.form-checkbox input[type="checkbox"]:checked {
|
||||
background: ${colors.accent.primary};
|
||||
border-color: ${colors.accent.primary};
|
||||
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='white' d='M10 3L4.5 8.5L2 6'/%3E%3C/svg%3E");
|
||||
background-repeat: no-repeat;
|
||||
background-position: center;
|
||||
}
|
||||
|
||||
/* Table styles */
|
||||
.table-container {
|
||||
overflow-x: auto;
|
||||
border: 1px solid ${colors.border.default};
|
||||
border-radius: ${unsafeCSS(borderRadius.lg)};
|
||||
background: ${colors.background.card};
|
||||
}
|
||||
|
||||
.table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.table th {
|
||||
text-align: left;
|
||||
padding: 12px 16px;
|
||||
font-weight: 600;
|
||||
font-size: 12px;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.04em;
|
||||
color: ${colors.text.muted};
|
||||
background: ${colors.background.secondary};
|
||||
border-bottom: 1px solid ${colors.border.default};
|
||||
}
|
||||
|
||||
.table th.sortable {
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.table th.sortable:hover {
|
||||
color: ${colors.text.primary};
|
||||
}
|
||||
|
||||
.table td {
|
||||
padding: 12px 16px;
|
||||
border-bottom: 1px solid ${colors.border.subtle};
|
||||
color: ${colors.text.primary};
|
||||
}
|
||||
|
||||
.table tr:last-child td {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.table tr:hover td {
|
||||
background: ${colors.background.secondary};
|
||||
}
|
||||
|
||||
.table .actions {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
/* Status badge */
|
||||
.status-badge {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
padding: 4px 10px;
|
||||
border-radius: ${unsafeCSS(borderRadius.full)};
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.status-badge .dot {
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
.status-badge.operational {
|
||||
background: ${cssManager.bdTheme('rgba(22, 163, 74, 0.1)', 'rgba(34, 197, 94, 0.15)')};
|
||||
color: ${cssManager.bdTheme('#15803d', '#4ade80')};
|
||||
}
|
||||
|
||||
.status-badge.operational .dot {
|
||||
background: ${colors.status.operational};
|
||||
}
|
||||
|
||||
.status-badge.degraded {
|
||||
background: ${cssManager.bdTheme('rgba(217, 119, 6, 0.1)', 'rgba(251, 191, 36, 0.15)')};
|
||||
color: ${cssManager.bdTheme('#b45309', '#fcd34d')};
|
||||
}
|
||||
|
||||
.status-badge.degraded .dot {
|
||||
background: ${colors.status.degraded};
|
||||
}
|
||||
|
||||
.status-badge.partial_outage,
|
||||
.status-badge.major_outage {
|
||||
background: ${cssManager.bdTheme('rgba(220, 38, 38, 0.1)', 'rgba(248, 113, 113, 0.15)')};
|
||||
color: ${cssManager.bdTheme('#b91c1c', '#fca5a5')};
|
||||
}
|
||||
|
||||
.status-badge.partial_outage .dot,
|
||||
.status-badge.major_outage .dot {
|
||||
background: ${colors.status.major};
|
||||
}
|
||||
|
||||
.status-badge.maintenance {
|
||||
background: ${cssManager.bdTheme('rgba(37, 99, 235, 0.1)', 'rgba(96, 165, 250, 0.15)')};
|
||||
color: ${cssManager.bdTheme('#1d4ed8', '#93c5fd')};
|
||||
}
|
||||
|
||||
.status-badge.maintenance .dot {
|
||||
background: ${colors.status.maintenance};
|
||||
}
|
||||
|
||||
/* Severity badge */
|
||||
.severity-badge {
|
||||
display: inline-flex;
|
||||
padding: 2px 8px;
|
||||
border-radius: ${unsafeCSS(borderRadius.sm)};
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.02em;
|
||||
}
|
||||
|
||||
.severity-badge.critical {
|
||||
background: ${cssManager.bdTheme('#fef2f2', '#450a0a')};
|
||||
color: ${cssManager.bdTheme('#b91c1c', '#fca5a5')};
|
||||
}
|
||||
|
||||
.severity-badge.major {
|
||||
background: ${cssManager.bdTheme('#fff7ed', '#431407')};
|
||||
color: ${cssManager.bdTheme('#c2410c', '#fdba74')};
|
||||
}
|
||||
|
||||
.severity-badge.minor {
|
||||
background: ${cssManager.bdTheme('#fefce8', '#422006')};
|
||||
color: ${cssManager.bdTheme('#a16207', '#fde047')};
|
||||
}
|
||||
|
||||
.severity-badge.maintenance {
|
||||
background: ${cssManager.bdTheme('#eff6ff', '#172554')};
|
||||
color: ${cssManager.bdTheme('#1d4ed8', '#93c5fd')};
|
||||
}
|
||||
|
||||
/* Tabs */
|
||||
.tabs {
|
||||
display: flex;
|
||||
gap: 4px;
|
||||
border-bottom: 1px solid ${colors.border.default};
|
||||
margin-bottom: ${unsafeCSS(spacing.lg)};
|
||||
}
|
||||
|
||||
.tab {
|
||||
padding: 12px 16px;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
color: ${colors.text.secondary};
|
||||
background: transparent;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
position: relative;
|
||||
transition: color ${unsafeCSS(durations.fast)} ${unsafeCSS(easings.default)};
|
||||
}
|
||||
|
||||
.tab:hover {
|
||||
color: ${colors.text.primary};
|
||||
}
|
||||
|
||||
.tab.active {
|
||||
color: ${colors.accent.primary};
|
||||
}
|
||||
|
||||
.tab.active::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
bottom: -1px;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: 2px;
|
||||
background: ${colors.accent.primary};
|
||||
border-radius: 1px;
|
||||
}
|
||||
|
||||
/* Empty state */
|
||||
.empty-state {
|
||||
text-align: center;
|
||||
padding: ${unsafeCSS(spacing['2xl'])} ${unsafeCSS(spacing.lg)};
|
||||
color: ${colors.text.muted};
|
||||
}
|
||||
|
||||
.empty-state-icon {
|
||||
font-size: 48px;
|
||||
margin-bottom: ${unsafeCSS(spacing.md)};
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.empty-state-title {
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
color: ${colors.text.primary};
|
||||
margin-bottom: ${unsafeCSS(spacing.xs)};
|
||||
}
|
||||
|
||||
.empty-state-description {
|
||||
font-size: 14px;
|
||||
margin-bottom: ${unsafeCSS(spacing.lg)};
|
||||
}
|
||||
|
||||
/* Modal/Dialog */
|
||||
.modal-overlay {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
background: rgba(0, 0, 0, 0.5);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 1000;
|
||||
padding: ${unsafeCSS(spacing.lg)};
|
||||
}
|
||||
|
||||
.modal {
|
||||
background: ${colors.background.card};
|
||||
border-radius: ${unsafeCSS(borderRadius.xl)};
|
||||
box-shadow: ${unsafeCSS(shadows.xl)};
|
||||
max-width: 500px;
|
||||
width: 100%;
|
||||
max-height: 90vh;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.modal-header {
|
||||
padding: ${unsafeCSS(spacing.lg)};
|
||||
border-bottom: 1px solid ${colors.border.default};
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.modal-title {
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
color: ${colors.text.primary};
|
||||
}
|
||||
|
||||
.modal-close {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border: none;
|
||||
background: transparent;
|
||||
color: ${colors.text.muted};
|
||||
cursor: pointer;
|
||||
border-radius: ${unsafeCSS(borderRadius.base)};
|
||||
transition: all ${unsafeCSS(durations.fast)} ${unsafeCSS(easings.default)};
|
||||
}
|
||||
|
||||
.modal-close:hover {
|
||||
background: ${colors.background.muted};
|
||||
color: ${colors.text.primary};
|
||||
}
|
||||
|
||||
.modal-body {
|
||||
padding: ${unsafeCSS(spacing.lg)};
|
||||
overflow-y: auto;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.modal-footer {
|
||||
padding: ${unsafeCSS(spacing.lg)};
|
||||
border-top: 1px solid ${colors.border.default};
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
/* Search input */
|
||||
.search-input {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.search-input input {
|
||||
padding-left: 40px;
|
||||
}
|
||||
|
||||
.search-input .search-icon {
|
||||
position: absolute;
|
||||
left: 12px;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
color: ${colors.text.muted};
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
/* Animations */
|
||||
@keyframes fadeIn {
|
||||
from { opacity: 0; }
|
||||
to { opacity: 1; }
|
||||
}
|
||||
|
||||
@keyframes slideUp {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(16px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
.fade-in {
|
||||
animation: fadeIn ${unsafeCSS(durations.normal)} ${unsafeCSS(easings.default)};
|
||||
}
|
||||
|
||||
.slide-up {
|
||||
animation: slideUp ${unsafeCSS(durations.slow)} ${unsafeCSS(easings.default)};
|
||||
}
|
||||
`;
|
||||
|
||||
export const getStatusColor = (status: string) => {
|
||||
switch (status) {
|
||||
case 'operational':
|
||||
return colors.status.operational;
|
||||
case 'degraded':
|
||||
return colors.status.degraded;
|
||||
case 'partial_outage':
|
||||
return colors.status.partial;
|
||||
case 'major_outage':
|
||||
return colors.status.major;
|
||||
case 'maintenance':
|
||||
return colors.status.maintenance;
|
||||
default:
|
||||
return colors.text.secondary;
|
||||
}
|
||||
};
|
||||
|
||||
export const getSeverityLabel = (severity: string) => {
|
||||
switch (severity) {
|
||||
case 'critical':
|
||||
return 'Critical';
|
||||
case 'major':
|
||||
return 'Major';
|
||||
case 'minor':
|
||||
return 'Minor';
|
||||
case 'maintenance':
|
||||
return 'Maintenance';
|
||||
default:
|
||||
return severity;
|
||||
}
|
||||
};
|
||||
|
||||
export const getStatusLabel = (status: string) => {
|
||||
switch (status) {
|
||||
case 'operational':
|
||||
return 'Operational';
|
||||
case 'degraded':
|
||||
return 'Degraded';
|
||||
case 'partial_outage':
|
||||
return 'Partial Outage';
|
||||
case 'major_outage':
|
||||
return 'Major Outage';
|
||||
case 'maintenance':
|
||||
return 'Maintenance';
|
||||
case 'investigating':
|
||||
return 'Investigating';
|
||||
case 'identified':
|
||||
return 'Identified';
|
||||
case 'monitoring':
|
||||
return 'Monitoring';
|
||||
case 'resolved':
|
||||
return 'Resolved';
|
||||
case 'postmortem':
|
||||
return 'Postmortem';
|
||||
default:
|
||||
return status;
|
||||
}
|
||||
};
|
||||
Reference in New Issue
Block a user