This commit is contained in:
2025-06-29 19:55:58 +00:00
parent d12147716d
commit 313e736f29
23 changed files with 11550 additions and 1587 deletions

View File

@@ -1,3 +1,4 @@
// Export components
export * from './upl-statuspage-assetsselector.js';
export * from './upl-statuspage-footer.js';
export * from './upl-statuspage-header.js';
@@ -5,3 +6,6 @@ export * from './upl-statuspage-incidents.js';
export * from './upl-statuspage-statusbar.js';
export * from './upl-statuspage-statusdetails.js';
export * from './upl-statuspage-statusmonth.js';
// Export interfaces
export * from '../interfaces/index.js';

View File

@@ -1,26 +1,33 @@
import { customElement, DeesElement, html, type TemplateResult } from '@design.estate/dees-element';
import { customElement, DeesElement, html, type TemplateResult, css, cssManager, unsafeCSS } from '@design.estate/dees-element';
import * as domtools from '@design.estate/dees-domtools';
import { fonts, colors, spacing } from '../../styles/shared.styles.js';
@customElement('uplinternal-miniheading')
export class UplinternalMiniheading extends DeesElement {
public static styles = [
domtools.elementBasic.staticStyles,
css`
:host {
display: block;
font-family: ${unsafeCSS(fonts.base)};
}
h5 {
display: block;
max-width: 1200px;
margin: 0px auto;
padding: 0px 0px ${unsafeCSS(spacing.md)} 0px;
color: ${colors.text.secondary};
font-size: 14px;
font-weight: 600;
letter-spacing: 0.025em;
text-transform: uppercase;
}
`
];
public render(): TemplateResult {
return html`
${domtools.elementBasic.styles}
<style>
:host {
display: block;
font-family: Inter;
}
h5 {
display: block;
max-width: 900px;
margin: 0px auto;
padding: 0px 0px 10px 0px;
color: #707070;
}
</style>
<h5>${this.textContent}</h5>
`;
}

View File

@@ -0,0 +1,607 @@
import { html } from '@design.estate/dees-element';
import type { IServiceStatus } from '../interfaces/index.js';
export const demoFunc = () => html`
<style>
.demo-container {
display: flex;
flex-direction: column;
gap: 20px;
}
.demo-section {
border: 1px solid #ddd;
border-radius: 8px;
padding: 20px;
background: #f5f5f5;
}
.demo-title {
font-size: 14px;
font-weight: 600;
margin-bottom: 16px;
color: #333;
}
.demo-controls {
display: flex;
gap: 10px;
margin-top: 16px;
flex-wrap: wrap;
}
.demo-button {
padding: 6px 12px;
border: 1px solid #ddd;
background: white;
border-radius: 4px;
cursor: pointer;
font-size: 13px;
}
.demo-button:hover {
background: #f0f0f0;
}
.demo-info {
margin-top: 12px;
padding: 12px;
background: white;
border-radius: 4px;
font-size: 13px;
}
.event-log {
margin-top: 12px;
padding: 12px;
background: #f9f9f9;
border-radius: 4px;
font-size: 12px;
max-height: 150px;
overflow-y: auto;
font-family: monospace;
}
</style>
<div class="demo-container">
<!-- Full Featured Demo -->
<div class="demo-section">
<div class="demo-title">Full Featured Service Selector</div>
<dees-demowrapper
.runAfterRender=${async (wrapperElement: any) => {
const assetsSelector = wrapperElement.querySelector('upl-statuspage-assetsselector') as any;
// Comprehensive demo data
const demoServices: IServiceStatus[] = [
// Infrastructure
{
id: 'api-gateway',
name: 'api-gateway',
displayName: 'API Gateway',
description: 'Main API endpoint for all services',
currentStatus: 'operational',
lastChecked: Date.now(),
uptime30d: 99.95,
uptime90d: 99.92,
responseTime: 45,
category: 'Infrastructure',
selected: true
},
{
id: 'web-server',
name: 'web-server',
displayName: 'Web Server',
description: 'Frontend web application server',
currentStatus: 'operational',
lastChecked: Date.now(),
uptime30d: 99.99,
uptime90d: 99.97,
responseTime: 28,
category: 'Infrastructure',
selected: true
},
{
id: 'load-balancer',
name: 'load-balancer',
displayName: 'Load Balancer',
description: 'Traffic distribution system',
currentStatus: 'operational',
lastChecked: Date.now(),
uptime30d: 100,
uptime90d: 99.99,
responseTime: 5,
category: 'Infrastructure',
selected: false
},
{
id: 'cdn',
name: 'cdn',
displayName: 'CDN',
description: 'Content delivery network',
currentStatus: 'operational',
lastChecked: Date.now(),
uptime30d: 100,
uptime90d: 99.99,
responseTime: 12,
category: 'Infrastructure',
selected: false
},
// Data Services
{
id: 'database',
name: 'database',
displayName: 'Database Cluster',
description: 'Primary database cluster with replicas',
currentStatus: 'degraded',
lastChecked: Date.now(),
uptime30d: 98.5,
uptime90d: 99.1,
responseTime: 120,
category: 'Data',
selected: true
},
{
id: 'redis-cache',
name: 'redis-cache',
displayName: 'Redis Cache',
description: 'In-memory data caching',
currentStatus: 'operational',
lastChecked: Date.now(),
uptime30d: 99.98,
uptime90d: 99.96,
responseTime: 5,
category: 'Data',
selected: true
},
{
id: 'elasticsearch',
name: 'elasticsearch',
displayName: 'Search Engine',
description: 'Full-text search service',
currentStatus: 'partial_outage',
lastChecked: Date.now(),
uptime30d: 95.2,
uptime90d: 97.8,
responseTime: 180,
category: 'Data',
selected: false
},
{
id: 'backup-service',
name: 'backup-service',
displayName: 'Backup Service',
description: 'Automated backup and recovery',
currentStatus: 'operational',
lastChecked: Date.now(),
uptime30d: 100,
uptime90d: 99.99,
responseTime: 95,
category: 'Data',
selected: true
},
// Application Services
{
id: 'auth-service',
name: 'auth-service',
displayName: 'Authentication Service',
description: 'User authentication and authorization',
currentStatus: 'operational',
lastChecked: Date.now(),
uptime30d: 99.98,
uptime90d: 99.95,
responseTime: 65,
category: 'Services',
selected: true
},
{
id: 'payment-gateway',
name: 'payment-gateway',
displayName: 'Payment Gateway',
description: 'Payment processing service',
currentStatus: 'maintenance',
lastChecked: Date.now(),
uptime30d: 97.5,
uptime90d: 98.8,
responseTime: 250,
category: 'Services',
selected: false
},
{
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: 150,
category: 'Services',
selected: true
},
{
id: 'notification-service',
name: 'notification-service',
displayName: 'Notification Service',
description: 'Push notifications and alerts',
currentStatus: 'operational',
lastChecked: Date.now(),
uptime30d: 99.7,
uptime90d: 99.8,
responseTime: 88,
category: 'Services',
selected: false
},
{
id: 'analytics',
name: 'analytics',
displayName: 'Analytics Engine',
description: 'Real-time analytics processing',
currentStatus: 'major_outage',
lastChecked: Date.now(),
uptime30d: 89.5,
uptime90d: 94.2,
responseTime: 450,
category: 'Services',
selected: false
},
// Monitoring
{
id: 'monitoring',
name: 'monitoring',
displayName: 'Monitoring System',
description: 'System health and metrics monitoring',
currentStatus: 'operational',
lastChecked: Date.now(),
uptime30d: 99.95,
uptime90d: 99.93,
responseTime: 78,
category: 'Monitoring',
selected: true
},
{
id: 'logging',
name: 'logging',
displayName: 'Logging Service',
description: 'Centralized log management',
currentStatus: 'operational',
lastChecked: Date.now(),
uptime30d: 99.9,
uptime90d: 99.88,
responseTime: 92,
category: 'Monitoring',
selected: false
}
];
// Set initial data
assetsSelector.services = demoServices;
// Demo loading state
assetsSelector.loading = true;
setTimeout(() => {
assetsSelector.loading = false;
}, 1000);
// Create event log
const eventLog = document.createElement('div');
eventLog.className = 'event-log';
eventLog.innerHTML = '<strong>Event Log:</strong><br>';
wrapperElement.appendChild(eventLog);
const logEvent = (message: string) => {
const time = new Date().toLocaleTimeString();
eventLog.innerHTML += `[${time}] ${message}<br>`;
eventLog.scrollTop = eventLog.scrollHeight;
};
// Listen for selection changes
assetsSelector.addEventListener('selectionChanged', (event: CustomEvent) => {
const selected = event.detail.selectedServices.length;
const total = demoServices.length;
logEvent(`Selection changed: ${selected}/${total} services selected`);
});
// Simulate status updates
setInterval(() => {
const randomService = demoServices[Math.floor(Math.random() * demoServices.length)];
const statuses: Array<IServiceStatus['currentStatus']> = ['operational', 'degraded', 'partial_outage', 'major_outage', 'maintenance'];
const newStatus = statuses[Math.floor(Math.random() * statuses.length)];
if (randomService.currentStatus !== newStatus) {
const oldStatus = randomService.currentStatus;
randomService.currentStatus = newStatus;
randomService.lastChecked = Date.now();
assetsSelector.requestUpdate();
logEvent(`${randomService.displayName}: ${oldStatus} ${newStatus}`);
}
}, 5000);
}}
>
<upl-statuspage-assetsselector></upl-statuspage-assetsselector>
</dees-demowrapper>
</div>
<!-- Empty State -->
<div class="demo-section">
<div class="demo-title">Empty State</div>
<dees-demowrapper
.runAfterRender=${async (wrapperElement: any) => {
const assetsSelector = wrapperElement.querySelector('upl-statuspage-assetsselector') as any;
// No services
assetsSelector.services = [];
const controls = document.createElement('div');
controls.className = 'demo-controls';
controls.innerHTML = `
<button class="demo-button" id="addServices">Add Services</button>
`;
wrapperElement.appendChild(controls);
controls.querySelector('#addServices')?.addEventListener('click', () => {
assetsSelector.services = [
{
id: 'new-service-1',
name: 'new-service-1',
displayName: 'New Service 1',
description: 'Just added',
currentStatus: 'operational',
lastChecked: Date.now(),
uptime30d: 100,
uptime90d: 100,
responseTime: 50,
selected: true
},
{
id: 'new-service-2',
name: 'new-service-2',
displayName: 'New Service 2',
description: 'Just added',
currentStatus: 'operational',
lastChecked: Date.now(),
uptime30d: 100,
uptime90d: 100,
responseTime: 60,
selected: false
}
];
});
}}
>
<upl-statuspage-assetsselector></upl-statuspage-assetsselector>
</dees-demowrapper>
</div>
<!-- Filtering Scenarios -->
<div class="demo-section">
<div class="demo-title">Advanced Filtering Demo</div>
<dees-demowrapper
.runAfterRender=${async (wrapperElement: any) => {
const assetsSelector = wrapperElement.querySelector('upl-statuspage-assetsselector') as any;
// Generate many services for filtering
const generateServices = (): IServiceStatus[] => {
const services: IServiceStatus[] = [];
const regions = ['us-east', 'us-west', 'eu-central', 'ap-south'];
const types = ['api', 'web', 'db', 'cache', 'queue'];
const statuses: Array<IServiceStatus['currentStatus']> = ['operational', 'degraded', 'partial_outage'];
regions.forEach(region => {
types.forEach(type => {
const id = `${region}-${type}`;
services.push({
id,
name: id,
displayName: `${region.toUpperCase()} ${type.toUpperCase()}`,
description: `${type} service in ${region} region`,
currentStatus: statuses[Math.floor(Math.random() * statuses.length)],
lastChecked: Date.now(),
uptime30d: 95 + Math.random() * 5,
uptime90d: 94 + Math.random() * 6,
responseTime: 20 + Math.random() * 200,
category: region,
selected: Math.random() > 0.5
});
});
});
return services;
};
assetsSelector.services = generateServices();
// Demo different filter scenarios
const scenarios = [
{
name: 'Show All',
action: () => {
assetsSelector.filterText = '';
assetsSelector.filterCategory = 'all';
assetsSelector.showOnlySelected = false;
}
},
{
name: 'Filter by Text: "api"',
action: () => {
assetsSelector.filterText = 'api';
assetsSelector.filterCategory = 'all';
assetsSelector.showOnlySelected = false;
}
},
{
name: 'Filter by Region: EU',
action: () => {
assetsSelector.filterText = '';
assetsSelector.filterCategory = 'eu-central';
assetsSelector.showOnlySelected = false;
}
},
{
name: 'Show Only Selected',
action: () => {
assetsSelector.filterText = '';
assetsSelector.filterCategory = 'all';
assetsSelector.showOnlySelected = true;
}
},
{
name: 'Complex: "db" in US regions',
action: () => {
assetsSelector.filterText = 'db';
assetsSelector.filterCategory = 'us-east';
assetsSelector.showOnlySelected = false;
}
}
];
const controls = document.createElement('div');
controls.className = 'demo-controls';
scenarios.forEach(scenario => {
const button = document.createElement('button');
button.className = 'demo-button';
button.textContent = scenario.name;
button.onclick = scenario.action;
controls.appendChild(button);
});
wrapperElement.appendChild(controls);
const info = document.createElement('div');
info.className = 'demo-info';
wrapperElement.appendChild(info);
// Update info on changes
const updateInfo = () => {
const filtered = assetsSelector.getFilteredServices();
const selected = assetsSelector.services.filter((s: any) => s.selected).length;
info.innerHTML = `
<strong>Filter Status:</strong><br>
Total Services: ${assetsSelector.services.length}<br>
Visible Services: ${filtered.length}<br>
Selected Services: ${selected}<br>
Active Filters: ${assetsSelector.filterText ? 'Text="' + assetsSelector.filterText + '" ' : ''}${assetsSelector.filterCategory !== 'all' ? 'Category=' + assetsSelector.filterCategory + ' ' : ''}${assetsSelector.showOnlySelected ? 'Selected Only' : ''}
`;
};
// Watch for changes
assetsSelector.addEventListener('selectionChanged', updateInfo);
setInterval(updateInfo, 500);
updateInfo();
}}
>
<upl-statuspage-assetsselector></upl-statuspage-assetsselector>
</dees-demowrapper>
</div>
<!-- Performance Test -->
<div class="demo-section">
<div class="demo-title">Performance Test - Many Services</div>
<dees-demowrapper
.runAfterRender=${async (wrapperElement: any) => {
const assetsSelector = wrapperElement.querySelector('upl-statuspage-assetsselector') as any;
const controls = document.createElement('div');
controls.className = 'demo-controls';
controls.innerHTML = `
<button class="demo-button" id="load50">Load 50 Services</button>
<button class="demo-button" id="load100">Load 100 Services</button>
<button class="demo-button" id="load200">Load 200 Services</button>
<button class="demo-button" id="clear">Clear All</button>
`;
wrapperElement.appendChild(controls);
const loadServices = (count: number) => {
const services: IServiceStatus[] = [];
const statuses: Array<IServiceStatus['currentStatus']> = ['operational', 'degraded', 'partial_outage', 'major_outage', 'maintenance'];
for (let i = 0; i < count; i++) {
services.push({
id: `service-${i}`,
name: `service-${i}`,
displayName: `Service ${i}`,
description: `Auto-generated service number ${i}`,
currentStatus: statuses[Math.floor(Math.random() * statuses.length)],
lastChecked: Date.now() - Math.random() * 3600000,
uptime30d: 85 + Math.random() * 15,
uptime90d: 80 + Math.random() * 20,
responseTime: 10 + Math.random() * 500,
category: `Category ${Math.floor(i / 10)}`,
selected: Math.random() > 0.7
});
}
assetsSelector.loading = true;
setTimeout(() => {
assetsSelector.services = services;
assetsSelector.loading = false;
}, 500);
};
controls.querySelector('#load50')?.addEventListener('click', () => loadServices(50));
controls.querySelector('#load100')?.addEventListener('click', () => loadServices(100));
controls.querySelector('#load200')?.addEventListener('click', () => loadServices(200));
controls.querySelector('#clear')?.addEventListener('click', () => {
assetsSelector.services = [];
});
// Start with 50 services
loadServices(50);
}}
>
<upl-statuspage-assetsselector></upl-statuspage-assetsselector>
</dees-demowrapper>
</div>
<!-- Loading States -->
<div class="demo-section">
<div class="demo-title">Loading and Error States</div>
<dees-demowrapper
.runAfterRender=${async (wrapperElement: any) => {
const assetsSelector = wrapperElement.querySelector('upl-statuspage-assetsselector') as any;
// Start with loading
assetsSelector.loading = true;
const controls = document.createElement('div');
controls.className = 'demo-controls';
controls.innerHTML = `
<button class="demo-button" id="toggleLoading">Toggle Loading</button>
<button class="demo-button" id="simulateError">Simulate Error</button>
<button class="demo-button" id="loadSuccess">Load Successfully</button>
`;
wrapperElement.appendChild(controls);
controls.querySelector('#toggleLoading')?.addEventListener('click', () => {
assetsSelector.loading = !assetsSelector.loading;
});
controls.querySelector('#simulateError')?.addEventListener('click', () => {
assetsSelector.loading = true;
setTimeout(() => {
assetsSelector.loading = false;
assetsSelector.services = [];
// You could add an error message property to the component
assetsSelector.errorMessage = 'Failed to load services';
}, 1500);
});
controls.querySelector('#loadSuccess')?.addEventListener('click', () => {
assetsSelector.loading = true;
setTimeout(() => {
assetsSelector.loading = false;
assetsSelector.services = [
{
id: 'loaded-1',
name: 'loaded-1',
displayName: 'Successfully Loaded Service',
description: 'This service was loaded after simulated delay',
currentStatus: 'operational',
lastChecked: Date.now(),
uptime30d: 99.9,
uptime90d: 99.8,
responseTime: 45,
selected: true
}
];
}, 1000);
});
}}
>
<upl-statuspage-assetsselector></upl-statuspage-assetsselector>
</dees-demowrapper>
</div>
</div>
`;

View File

@@ -6,10 +6,14 @@ import {
type TemplateResult,
cssManager,
css,
unsafeCSS,
} from '@design.estate/dees-element';
import * as domtools from '@design.estate/dees-domtools';
import type { IServiceStatus } from '../interfaces/index.js';
import { fonts, colors, shadows, borderRadius, spacing, commonStyles, getStatusColor } from '../styles/shared.styles.js';
import './internal/uplinternal-miniheading.js';
import { demoFunc } from './upl-statuspage-assetsselector.demo.js';
declare global {
interface HTMLElementTagNameMap {
@@ -19,9 +23,22 @@ declare global {
@customElement('upl-statuspage-assetsselector')
export class UplStatuspageAssetsselector extends DeesElement {
public static demo = () => html`
<upl-statuspage-assetsselector></upl-statuspage-assetsselector>
`;
public static demo = demoFunc;
@property({ type: Array })
public services: IServiceStatus[] = [];
@property({ type: String })
public filterText: string = '';
@property({ type: String })
public filterCategory: string = 'all';
@property({ type: Boolean })
public showOnlySelected: boolean = false;
@property({ type: Boolean })
public loading: boolean = false;
constructor() {
super();
@@ -29,35 +46,398 @@ export class UplStatuspageAssetsselector extends DeesElement {
public static styles = [
cssManager.defaultStyles,
commonStyles,
css`
:host {
padding: 0px 0px 15px 0px;
display: block;
background: ${cssManager.bdTheme('#eeeeeb', '#222222')};
font-family: Inter;
color: #fff;
background: transparent;
font-family: ${unsafeCSS(fonts.base)};
color: ${colors.text.primary};
}
.mainbox {
margin: auto;
max-width: 900px;
.container {
max-width: 1200px;
margin: 0 auto;
padding: 0 ${unsafeCSS(spacing.lg)} ${unsafeCSS(spacing.lg)} ${unsafeCSS(spacing.lg)};
}
.controls {
display: flex;
gap: ${unsafeCSS(spacing.sm)};
margin-bottom: ${unsafeCSS(spacing.lg)};
flex-wrap: wrap;
align-items: center;
}
.search-input {
flex: 1;
min-width: 240px;
padding: ${unsafeCSS(spacing.sm)} ${unsafeCSS(spacing.md)};
border: 1px solid ${colors.border.default};
border-radius: ${unsafeCSS(borderRadius.base)};
background: ${colors.background.primary};
color: ${colors.text.primary};
font-size: 14px;
font-family: ${unsafeCSS(fonts.base)};
transition: all 0.2s ease;
}
.search-input:focus {
outline: none;
border-color: ${colors.text.primary};
box-shadow: 0 0 0 3px ${cssManager.bdTheme('rgba(0, 0, 0, 0.05)', 'rgba(255, 255, 255, 0.05)')};
}
.search-input::placeholder {
color: ${colors.text.muted};
}
.filter-button {
display: inline-flex;
align-items: center;
justify-content: center;
padding: ${unsafeCSS(spacing.sm)} ${unsafeCSS(spacing.md)};
border: 1px solid ${colors.border.default};
border-radius: ${unsafeCSS(borderRadius.base)};
background: transparent;
color: ${colors.text.primary};
cursor: pointer;
font-size: 14px;
font-weight: 500;
font-family: ${unsafeCSS(fonts.base)};
transition: all 0.2s ease;
height: 36px;
}
.filter-button:hover {
background: ${colors.background.secondary};
border-color: ${colors.border.muted};
transform: translateY(-1px);
}
.filter-button:active {
transform: translateY(0);
}
.filter-button.active {
background: ${colors.text.primary};
color: ${colors.background.primary};
border-color: ${colors.text.primary};
}
.filter-button.active:hover {
background: ${cssManager.bdTheme('#262626', '#f4f4f5')};
border-color: ${cssManager.bdTheme('#262626', '#f4f4f5')};
}
.assets-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(320px, 1fr));
gap: ${unsafeCSS(spacing.md)};
background: ${colors.background.card};
padding: ${unsafeCSS(spacing.lg)};
border-radius: ${unsafeCSS(borderRadius.md)};
border: 1px solid ${colors.border.default};
min-height: 200px;
box-shadow: ${unsafeCSS(shadows.sm)};
}
.asset-card {
display: flex;
align-items: center;
padding: ${unsafeCSS(spacing.md)};
background: ${colors.background.secondary};
border-radius: ${unsafeCSS(borderRadius.base)};
cursor: pointer;
transition: all 0.2s ease;
border: 1px solid ${colors.border.default};
gap: ${unsafeCSS(spacing.md)};
}
.asset-card:hover {
transform: translateY(-2px);
box-shadow: ${unsafeCSS(shadows.md)};
border-color: ${colors.border.muted};
}
.asset-card.selected {
border-color: ${colors.text.primary};
background: ${colors.background.muted};
box-shadow: 0 0 0 1px ${colors.text.primary};
}
.asset-checkbox {
width: 16px;
height: 16px;
cursor: pointer;
accent-color: ${colors.text.primary};
flex-shrink: 0;
}
.asset-info {
flex: 1;
min-width: 0;
}
.asset-name {
font-weight: 600;
font-size: 14px;
margin-bottom: ${unsafeCSS(spacing.xs)};
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.asset-description {
font-size: 13px;
color: ${colors.text.secondary};
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.asset-status {
display: flex;
align-items: center;
gap: ${unsafeCSS(spacing.xs)};
flex-shrink: 0;
}
.status-indicator {
width: 8px;
height: 8px;
border-radius: ${unsafeCSS(borderRadius.full)};
}
.status-indicator.operational { background: ${colors.status.operational}; }
.status-indicator.degraded { background: ${colors.status.degraded}; }
.status-indicator.partial_outage { background: ${colors.status.partial}; }
.status-indicator.major_outage { background: ${colors.status.major}; }
.status-indicator.maintenance { background: ${colors.status.maintenance}; }
.status-text {
font-size: 12px;
text-transform: capitalize;
color: ${colors.text.secondary};
}
.loading-message,
.no-results {
grid-column: 1 / -1;
text-align: center;
height: 50px;
border-radius: 3px;
background: ${cssManager.bdTheme('#ffffff', '#333333')};;
padding: ${unsafeCSS(spacing['2xl'])};
color: ${colors.text.secondary};
}
.summary {
text-align: right;
font-size: 13px;
margin-top: ${unsafeCSS(spacing.md)};
color: ${colors.text.secondary};
}
@media (max-width: 640px) {
.container {
padding: 0 ${unsafeCSS(spacing.md)} ${unsafeCSS(spacing.md)} ${unsafeCSS(spacing.md)};
}
.controls {
flex-direction: column;
align-items: stretch;
}
.search-input {
width: 100%;
}
.assets-grid {
grid-template-columns: 1fr;
padding: ${unsafeCSS(spacing.md)};
}
.asset-card {
padding: ${unsafeCSS(spacing.sm)};
}
}
`,
]
public render(): TemplateResult {
const filteredServices = this.getFilteredServices();
const selectedCount = this.services.filter(s => s.selected).length;
const categories = this.getUniqueCategories();
return html`
<style>
<div class="container">
<uplinternal-miniheading>Monitored Assets</uplinternal-miniheading>
</style>
<uplinternal-miniheading>Monitored Assets</uplinternal-miniheading>
<div class="mainbox">
Hello!
<div class="controls">
<input
type="text"
class="search-input"
placeholder="Search services..."
.value=${this.filterText}
@input=${(e: Event) => {
this.filterText = (e.target as HTMLInputElement).value;
}}
/>
<button
class="filter-button ${this.filterCategory === 'all' ? 'active' : ''}"
@click=${() => { this.filterCategory = 'all'; }}
>
All
</button>
${categories.map(category => html`
<button
class="filter-button ${this.filterCategory === category ? 'active' : ''}"
@click=${() => { this.filterCategory = category; }}
>
${category}
</button>
`)}
<button
class="filter-button ${this.showOnlySelected ? 'active' : ''}"
@click=${() => { this.showOnlySelected = !this.showOnlySelected; }}
>
${this.showOnlySelected ? 'Show All' : 'Selected Only'}
</button>
<button
class="filter-button"
@click=${() => this.selectAll()}
>
Select All
</button>
<button
class="filter-button"
@click=${() => this.selectNone()}
>
Select None
</button>
</div>
<div class="assets-grid">
${this.loading ? html`
<div class="loading-message">Loading services...</div>
` : filteredServices.length === 0 ? html`
<div class="no-results">No services found matching your criteria</div>
` : filteredServices.map(service => html`
<div
class="asset-card ${service.selected ? 'selected' : ''}"
@click=${() => this.toggleService(service.id)}
>
<input
type="checkbox"
class="asset-checkbox"
.checked=${service.selected}
@click=${(e: Event) => e.stopPropagation()}
@change=${(e: Event) => {
e.stopPropagation();
this.toggleService(service.id);
}}
/>
<div class="asset-info">
<div class="asset-name">${service.displayName}</div>
${service.description ? html`
<div class="asset-description">${service.description}</div>
` : ''}
</div>
<div class="asset-status">
<div class="status-indicator ${service.currentStatus}"></div>
<div class="status-text">${service.currentStatus.replace(/_/g, ' ')}</div>
</div>
</div>
`)}
</div>
<div class="summary">
${selectedCount} of ${this.services.length} services selected
</div>
</div>
`;
}
private getFilteredServices(): IServiceStatus[] {
return this.services.filter(service => {
// Apply text filter
if (this.filterText && !service.displayName.toLowerCase().includes(this.filterText.toLowerCase()) &&
(!service.description || !service.description.toLowerCase().includes(this.filterText.toLowerCase()))) {
return false;
}
// Apply category filter
if (this.filterCategory !== 'all' && service.category !== this.filterCategory) {
return false;
}
// Apply selected filter
if (this.showOnlySelected && !service.selected) {
return false;
}
return true;
});
}
private getUniqueCategories(): string[] {
const categories = new Set<string>();
this.services.forEach(service => {
if (service.category) {
categories.add(service.category);
}
});
return Array.from(categories).sort();
}
private toggleService(serviceId: string) {
const service = this.services.find(s => s.id === serviceId);
if (service) {
service.selected = !service.selected;
this.requestUpdate();
this.dispatchEvent(new CustomEvent('selectionChanged', {
detail: {
serviceId,
selected: service.selected,
selectedServices: this.services.filter(s => s.selected).map(s => s.id)
},
bubbles: true,
composed: true
}));
}
}
private selectAll() {
const filteredServices = this.getFilteredServices();
filteredServices.forEach(service => {
service.selected = true;
});
this.requestUpdate();
this.emitSelectionUpdate();
}
private selectNone() {
const filteredServices = this.getFilteredServices();
filteredServices.forEach(service => {
service.selected = false;
});
this.requestUpdate();
this.emitSelectionUpdate();
}
private emitSelectionUpdate() {
this.dispatchEvent(new CustomEvent('selectionChanged', {
detail: {
selectedServices: this.services.filter(s => s.selected).map(s => s.id)
},
bubbles: true,
composed: true
}));
}
}

View File

@@ -0,0 +1,744 @@
import { html } from '@design.estate/dees-element';
import type { IStatusPageConfig } from '../interfaces/index.js';
export const demoFunc = () => html`
<style>
.demo-container {
display: flex;
flex-direction: column;
gap: 20px;
}
.demo-section {
border: 1px solid #ddd;
border-radius: 8px;
padding: 20px;
background: #f5f5f5;
}
.demo-title {
font-size: 14px;
font-weight: 600;
margin-bottom: 16px;
color: #333;
}
.demo-controls {
display: flex;
gap: 10px;
margin-top: 16px;
flex-wrap: wrap;
}
.demo-button {
padding: 6px 12px;
border: 1px solid #ddd;
background: white;
border-radius: 4px;
cursor: pointer;
font-size: 13px;
}
.demo-button:hover {
background: #f0f0f0;
}
.demo-button.active {
background: #2196F3;
color: white;
border-color: #2196F3;
}
.demo-info {
margin-top: 12px;
padding: 12px;
background: white;
border-radius: 4px;
font-size: 13px;
line-height: 1.6;
}
.event-log {
margin-top: 12px;
padding: 12px;
background: #f9f9f9;
border-radius: 4px;
font-size: 12px;
max-height: 150px;
overflow-y: auto;
font-family: monospace;
}
.config-display {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 12px;
margin-top: 12px;
}
.config-item {
background: white;
padding: 12px;
border-radius: 4px;
font-size: 12px;
}
.config-label {
font-weight: 600;
color: #666;
margin-bottom: 4px;
}
.config-value {
word-break: break-word;
}
</style>
<div class="demo-container">
<!-- Different Configuration Scenarios -->
<div class="demo-section">
<div class="demo-title">Different Footer Configurations</div>
<dees-demowrapper
.runAfterRender=${async (wrapperElement: any) => {
const footer = wrapperElement.querySelector('upl-statuspage-footer') as any;
// Configuration presets
const configPresets = {
minimal: {
name: 'Minimal',
config: {
companyName: 'SimpleStatus',
whitelabel: true,
lastUpdated: Date.now()
}
},
standard: {
name: 'Standard',
config: {
companyName: 'TechCorp Solutions',
legalUrl: 'https://example.com/legal',
supportEmail: 'support@techcorp.com',
statusPageUrl: 'https://status.techcorp.com',
whitelabel: false,
lastUpdated: Date.now(),
currentYear: new Date().getFullYear()
}
},
fullFeatured: {
name: 'Full Featured',
config: {
companyName: 'Enterprise Cloud Platform',
legalUrl: 'https://enterprise.com/legal',
supportEmail: 'support@enterprise.com',
statusPageUrl: 'https://status.enterprise.com',
whitelabel: false,
socialLinks: [
{ platform: 'twitter', url: 'https://twitter.com/enterprise' },
{ platform: 'github', url: 'https://github.com/enterprise' },
{ platform: 'linkedin', url: 'https://linkedin.com/company/enterprise' },
{ platform: 'facebook', url: 'https://facebook.com/enterprise' },
{ platform: 'youtube', url: 'https://youtube.com/enterprise' }
],
rssFeedUrl: 'https://status.enterprise.com/rss',
apiStatusUrl: 'https://api.enterprise.com/v1/status',
lastUpdated: Date.now(),
currentYear: new Date().getFullYear(),
language: 'en',
additionalLinks: [
{ label: 'API Docs', url: 'https://docs.enterprise.com' },
{ label: 'Service SLA', url: 'https://enterprise.com/sla' },
{ label: 'Security', url: 'https://enterprise.com/security' }
]
}
},
international: {
name: 'International',
config: {
companyName: 'Global Services GmbH',
legalUrl: 'https://global.eu/legal',
supportEmail: 'support@global.eu',
statusPageUrl: 'https://status.global.eu',
whitelabel: false,
language: 'de',
currentYear: new Date().getFullYear(),
lastUpdated: Date.now(),
languageOptions: [
{ code: 'en', label: 'English' },
{ code: 'de', label: 'Deutsch' },
{ code: 'fr', label: 'Français' },
{ code: 'es', label: 'Español' },
{ code: 'ja', label: '日本語' }
],
socialLinks: [
{ platform: 'twitter', url: 'https://twitter.com/global_eu' },
{ platform: 'linkedin', url: 'https://linkedin.com/company/global-eu' }
]
}
},
whitelabel: {
name: 'Whitelabel',
config: {
companyName: 'Custom Brand Status',
whitelabel: true,
customBranding: {
primaryColor: '#FF5722',
logoUrl: 'https://example.com/custom-logo.png',
footerText: 'Powered by Custom Infrastructure'
},
lastUpdated: Date.now(),
currentYear: new Date().getFullYear()
}
}
};
// Initial setup
let currentPreset = 'standard';
const applyPreset = (preset: any) => {
Object.keys(preset.config).forEach(key => {
footer[key] = preset.config[key];
});
updateConfigDisplay(preset.config);
};
applyPreset(configPresets[currentPreset]);
// Create controls
const controls = document.createElement('div');
controls.className = 'demo-controls';
Object.entries(configPresets).forEach(([key, preset]) => {
const button = document.createElement('button');
button.className = 'demo-button' + (key === currentPreset ? ' active' : '');
button.textContent = preset.name;
button.onclick = () => {
controls.querySelectorAll('.demo-button').forEach(btn => btn.classList.remove('active'));
button.classList.add('active');
currentPreset = key;
footer.loading = true;
setTimeout(() => {
applyPreset(preset);
footer.loading = false;
}, 500);
};
controls.appendChild(button);
});
wrapperElement.appendChild(controls);
// Add configuration display
const configDisplay = document.createElement('div');
configDisplay.className = 'config-display';
wrapperElement.appendChild(configDisplay);
const updateConfigDisplay = (config: any) => {
configDisplay.innerHTML = Object.entries(config)
.filter(([key]) => key !== 'socialLinks' && key !== 'additionalLinks' && key !== 'languageOptions')
.map(([key, value]) => `
<div class="config-item">
<div class="config-label">${key}</div>
<div class="config-value">${value}</div>
</div>
`).join('');
};
// Handle events
footer.addEventListener('footerLinkClick', (event: CustomEvent) => {
console.log('Footer link clicked:', event.detail);
alert(`Link clicked: ${event.detail.type} - ${event.detail.url}`);
});
footer.addEventListener('subscribeClick', () => {
alert('Subscribe feature would open here');
});
footer.addEventListener('reportIncidentClick', () => {
alert('Report incident form would open here');
});
footer.addEventListener('languageChange', (event: CustomEvent) => {
alert(`Language changed to: ${event.detail.language}`);
});
}}
>
<upl-statuspage-footer></upl-statuspage-footer>
</dees-demowrapper>
</div>
<!-- Loading and Error States -->
<div class="demo-section">
<div class="demo-title">Loading and Error States</div>
<dees-demowrapper
.runAfterRender=${async (wrapperElement: any) => {
const footer = wrapperElement.querySelector('upl-statuspage-footer') as any;
// Start with loading
footer.loading = true;
footer.companyName = 'LoadingCorp';
const controls = document.createElement('div');
controls.className = 'demo-controls';
controls.innerHTML = `
<button class="demo-button" id="toggleLoading">Toggle Loading</button>
<button class="demo-button" id="loadSuccess">Load Successfully</button>
<button class="demo-button" id="simulateError">Simulate Error</button>
<button class="demo-button" id="simulateOffline">Simulate Offline</button>
<button class="demo-button" id="brokenLinks">Broken Links</button>
`;
wrapperElement.appendChild(controls);
controls.querySelector('#toggleLoading')?.addEventListener('click', () => {
footer.loading = !footer.loading;
});
controls.querySelector('#loadSuccess')?.addEventListener('click', () => {
footer.loading = true;
setTimeout(() => {
footer.companyName = 'Successfully Loaded Inc';
footer.legalUrl = 'https://example.com/legal';
footer.supportEmail = 'support@loaded.com';
footer.statusPageUrl = 'https://status.loaded.com';
footer.lastUpdated = Date.now();
footer.socialLinks = [
{ platform: 'twitter', url: 'https://twitter.com/loaded' },
{ platform: 'github', url: 'https://github.com/loaded' }
];
footer.loading = false;
footer.errorMessage = null;
}, 1000);
});
controls.querySelector('#simulateError')?.addEventListener('click', () => {
footer.loading = true;
setTimeout(() => {
footer.loading = false;
footer.errorMessage = 'Failed to load footer configuration';
footer.companyName = 'Error Loading';
footer.socialLinks = [];
}, 1500);
});
controls.querySelector('#simulateOffline')?.addEventListener('click', () => {
footer.offline = true;
footer.errorMessage = 'You are currently offline';
footer.lastUpdated = null;
});
controls.querySelector('#brokenLinks')?.addEventListener('click', () => {
footer.companyName = 'Broken Links Demo';
footer.legalUrl = 'https://broken.invalid/legal';
footer.supportEmail = 'invalid-email';
footer.socialLinks = [
{ platform: 'twitter', url: '' },
{ platform: 'github', url: 'not-a-url' }
];
footer.rssFeedUrl = 'https://broken.invalid/rss';
footer.apiStatusUrl = null;
});
// Add info display
const info = document.createElement('div');
info.className = 'demo-info';
info.innerHTML = 'Test different loading states and error scenarios using the controls above.';
wrapperElement.appendChild(info);
}}
>
<upl-statuspage-footer></upl-statuspage-footer>
</dees-demowrapper>
</div>
<!-- Dynamic Updates and Real-time Features -->
<div class="demo-section">
<div class="demo-title">Dynamic Updates and Real-time Features</div>
<dees-demowrapper
.runAfterRender=${async (wrapperElement: any) => {
const footer = wrapperElement.querySelector('upl-statuspage-footer') as any;
// Initial configuration
footer.companyName = 'RealTime Systems';
footer.legalUrl = 'https://realtime.com/legal';
footer.supportEmail = 'support@realtime.com';
footer.statusPageUrl = 'https://status.realtime.com';
footer.lastUpdated = Date.now();
footer.currentYear = new Date().getFullYear();
// Dynamic social links
const allSocialPlatforms = [
{ platform: 'twitter', url: 'https://twitter.com/realtime' },
{ platform: 'github', url: 'https://github.com/realtime' },
{ platform: 'linkedin', url: 'https://linkedin.com/company/realtime' },
{ platform: 'facebook', url: 'https://facebook.com/realtime' },
{ platform: 'youtube', url: 'https://youtube.com/realtime' },
{ platform: 'instagram', url: 'https://instagram.com/realtime' },
{ platform: 'slack', url: 'https://realtime.slack.com' },
{ platform: 'discord', url: 'https://discord.gg/realtime' }
];
footer.socialLinks = allSocialPlatforms.slice(0, 3);
// Real-time status feed
footer.rssFeedUrl = 'https://status.realtime.com/rss';
footer.apiStatusUrl = 'https://api.realtime.com/v1/status';
// Status feed simulation
const statusUpdates = [
'All systems operational',
'Investigating API latency',
'Maintenance scheduled for tonight',
'Performance improvements deployed',
'New datacenter online',
'Security patch applied'
];
let updateIndex = 0;
footer.latestStatusUpdate = statusUpdates[0];
// Auto-update last updated time
const updateInterval = setInterval(() => {
footer.lastUpdated = Date.now();
}, 5000);
// Rotate status updates
const statusInterval = setInterval(() => {
updateIndex = (updateIndex + 1) % statusUpdates.length;
footer.latestStatusUpdate = statusUpdates[updateIndex];
logEvent(`Status update: ${statusUpdates[updateIndex]}`);
}, 8000);
// Create controls
const controls = document.createElement('div');
controls.className = 'demo-controls';
controls.innerHTML = `
<button class="demo-button" id="addSocial">Add Social Link</button>
<button class="demo-button" id="removeSocial">Remove Social Link</button>
<button class="demo-button" id="updateStatus">Force Status Update</button>
<button class="demo-button" id="changeYear">Change Year</button>
`;
wrapperElement.appendChild(controls);
controls.querySelector('#addSocial')?.addEventListener('click', () => {
if (footer.socialLinks.length < allSocialPlatforms.length) {
footer.socialLinks = [...footer.socialLinks, allSocialPlatforms[footer.socialLinks.length]];
logEvent(`Added ${allSocialPlatforms[footer.socialLinks.length - 1].platform} link`);
}
});
controls.querySelector('#removeSocial')?.addEventListener('click', () => {
if (footer.socialLinks.length > 0) {
const removed = footer.socialLinks[footer.socialLinks.length - 1];
footer.socialLinks = footer.socialLinks.slice(0, -1);
logEvent(`Removed ${removed.platform} link`);
}
});
controls.querySelector('#updateStatus')?.addEventListener('click', () => {
const customStatus = prompt('Enter custom status update:');
if (customStatus) {
footer.latestStatusUpdate = customStatus;
footer.lastUpdated = Date.now();
logEvent(`Custom status: ${customStatus}`);
}
});
controls.querySelector('#changeYear')?.addEventListener('click', () => {
footer.currentYear = footer.currentYear + 1;
logEvent(`Year changed to ${footer.currentYear}`);
});
// Event log
const eventLog = document.createElement('div');
eventLog.className = 'event-log';
eventLog.innerHTML = '<strong>Event Log:</strong><br>';
wrapperElement.appendChild(eventLog);
const logEvent = (message: string) => {
const time = new Date().toLocaleTimeString();
eventLog.innerHTML += `[${time}] ${message}<br>`;
eventLog.scrollTop = eventLog.scrollHeight;
};
logEvent('Real-time updates started');
// Cleanup
wrapperElement.addEventListener('remove', () => {
clearInterval(updateInterval);
clearInterval(statusInterval);
});
}}
>
<upl-statuspage-footer></upl-statuspage-footer>
</dees-demowrapper>
</div>
<!-- Interactive Features -->
<div class="demo-section">
<div class="demo-title">Interactive Features and Actions</div>
<dees-demowrapper
.runAfterRender=${async (wrapperElement: any) => {
const footer = wrapperElement.querySelector('upl-statuspage-footer') as any;
// Setup interactive footer
footer.companyName = 'Interactive Corp';
footer.legalUrl = 'https://interactive.com/legal';
footer.supportEmail = 'help@interactive.com';
footer.statusPageUrl = 'https://status.interactive.com';
footer.whitelabel = false;
footer.lastUpdated = Date.now();
footer.currentYear = new Date().getFullYear();
// Interactive features
footer.enableSubscribe = true;
footer.enableReportIssue = true;
footer.enableLanguageSelector = true;
footer.enableThemeToggle = true;
footer.languageOptions = [
{ code: 'en', label: 'English' },
{ code: 'es', label: 'Español' },
{ code: 'fr', label: 'Français' },
{ code: 'de', label: 'Deutsch' },
{ code: 'ja', label: '日本語' },
{ code: 'zh', label: '中文' }
];
footer.socialLinks = [
{ platform: 'twitter', url: 'https://twitter.com/interactive' },
{ platform: 'github', url: 'https://github.com/interactive' },
{ platform: 'discord', url: 'https://discord.gg/interactive' }
];
footer.additionalLinks = [
{ label: 'API Documentation', url: 'https://docs.interactive.com' },
{ label: 'Service Level Agreement', url: 'https://interactive.com/sla' },
{ label: 'Privacy Policy', url: 'https://interactive.com/privacy' },
{ label: 'Terms of Service', url: 'https://interactive.com/terms' }
];
// Subscribe functionality
let subscriberCount = 1234;
footer.subscriberCount = subscriberCount;
footer.addEventListener('subscribeClick', (event: CustomEvent) => {
const email = prompt('Enter your email to subscribe:');
if (email && email.includes('@')) {
subscriberCount++;
footer.subscriberCount = subscriberCount;
logAction(`New subscriber: ${email} (Total: ${subscriberCount})`);
alert(`Successfully subscribed! You are subscriber #${subscriberCount}`);
}
});
// Report issue functionality
footer.addEventListener('reportIncidentClick', (event: CustomEvent) => {
const issue = prompt('Describe the issue you are experiencing:');
if (issue) {
const ticketId = `INC-${Date.now().toString().slice(-6)}`;
logAction(`Issue reported: ${ticketId} - ${issue.substring(0, 50)}...`);
alert(`Thank you! Your issue has been logged.\nTicket ID: ${ticketId}\nWe will investigate and update you at the provided email.`);
}
});
// Language change
footer.addEventListener('languageChange', (event: CustomEvent) => {
const newLang = event.detail.language;
footer.currentLanguage = newLang;
logAction(`Language changed to: ${newLang}`);
// Simulate translation
const translations = {
en: 'Interactive Corp',
es: 'Corporación Interactiva',
fr: 'Corp Interactif',
de: 'Interaktive GmbH',
ja: 'インタラクティブ株式会社',
zh: '互动公司'
};
footer.companyName = translations[newLang] || translations.en;
});
// Theme toggle
footer.addEventListener('themeToggle', (event: CustomEvent) => {
const theme = event.detail.theme;
logAction(`Theme changed to: ${theme}`);
footer.currentTheme = theme;
});
// Click tracking
footer.addEventListener('footerLinkClick', (event: CustomEvent) => {
logAction(`Link clicked: ${event.detail.type} - ${event.detail.label || event.detail.url}`);
});
// Action log
const actionLog = document.createElement('div');
actionLog.className = 'event-log';
actionLog.innerHTML = '<strong>User Actions:</strong><br>';
wrapperElement.appendChild(actionLog);
const logAction = (message: string) => {
const time = new Date().toLocaleTimeString();
actionLog.innerHTML += `[${time}] ${message}<br>`;
actionLog.scrollTop = actionLog.scrollHeight;
};
logAction('Interactive footer ready');
// Add info
const info = document.createElement('div');
info.className = 'demo-info';
info.innerHTML = 'Try clicking on various footer elements to see the interactive features in action.';
wrapperElement.appendChild(info);
}}
>
<upl-statuspage-footer></upl-statuspage-footer>
</dees-demowrapper>
</div>
<!-- Edge Cases -->
<div class="demo-section">
<div class="demo-title">Edge Cases and Special Scenarios</div>
<dees-demowrapper
.runAfterRender=${async (wrapperElement: any) => {
const footer = wrapperElement.querySelector('upl-statuspage-footer') as any;
const edgeCases = {
empty: {
name: 'Empty/Minimal',
config: {
companyName: '',
whitelabel: true,
lastUpdated: null
}
},
veryLong: {
name: 'Very Long Content',
config: {
companyName: 'International Mega Corporation with an Extremely Long Company Name That Tests Layout Limits Inc.',
legalUrl: 'https://very-long-domain-name-that-might-break-layouts.international-corporation.com/legal/terms-and-conditions/privacy-policy/cookie-policy',
supportEmail: 'customer.support.team@very-long-domain-name.international-corporation.com',
socialLinks: Array.from({ length: 15 }, (_, i) => ({
platform: ['twitter', 'github', 'linkedin', 'facebook', 'youtube'][i % 5],
url: `https://social-${i}.com/long-username-handle-that-tests-limits`
})),
additionalLinks: Array.from({ length: 10 }, (_, i) => ({
label: `Very Long Link Label That Might Cause Layout Issues #${i + 1}`,
url: `https://example.com/very/long/path/structure/that/goes/on/and/on/page-${i}`
}))
}
},
unicode: {
name: 'Unicode/International',
config: {
companyName: '🌍 全球服务 • グローバル • العالمية • Глобальный 🌏',
legalUrl: 'https://unicode.test/法律',
supportEmail: 'support@日本.jp',
currentYear: new Date().getFullYear(),
socialLinks: [
{ platform: 'twitter', url: 'https://twitter.com/🌐' },
{ platform: 'github', url: 'https://github.com/世界' }
],
additionalLinks: [
{ label: '📋 Terms & Conditions', url: '#' },
{ label: '🔒 Privacy Policy', url: '#' },
{ label: '🛡️ Security', url: '#' }
]
}
},
brokenData: {
name: 'Broken/Invalid Data',
config: {
companyName: null,
legalUrl: 'not-a-valid-url',
supportEmail: 'not-an-email',
currentYear: 'not-a-year',
lastUpdated: 'invalid-timestamp',
socialLinks: [
{ platform: null, url: null },
{ platform: 'unknown-platform', url: '' },
{ url: 'https://missing-platform.com' },
{ platform: 'twitter' }
],
rssFeedUrl: '',
apiStatusUrl: undefined
}
},
maxData: {
name: 'Maximum Data',
config: {
companyName: 'Maximum Configuration Demo',
legalUrl: 'https://max.demo/legal',
supportEmail: 'all@max.demo',
statusPageUrl: 'https://status.max.demo',
whitelabel: false,
currentYear: new Date().getFullYear(),
lastUpdated: Date.now(),
language: 'en',
theme: 'dark',
socialLinks: Array.from({ length: 20 }, (_, i) => ({
platform: 'generic',
url: `https://social${i}.com`
})),
additionalLinks: Array.from({ length: 15 }, (_, i) => ({
label: `Link ${i + 1}`,
url: `#link${i + 1}`
})),
rssFeedUrl: 'https://status.max.demo/rss',
apiStatusUrl: 'https://api.max.demo/status',
subscriberCount: 999999,
enableSubscribe: true,
enableReportIssue: true,
enableLanguageSelector: true,
enableThemeToggle: true,
languageOptions: Array.from({ length: 50 }, (_, i) => ({
code: `lang${i}`,
label: `Language ${i}`
}))
}
}
};
// Initial setup
let currentCase = 'empty';
const applyCase = (edgeCase: any) => {
// Clear all properties first
Object.keys(footer).forEach(key => {
if (typeof footer[key] !== 'function') {
footer[key] = undefined;
}
});
// Apply new config
Object.keys(edgeCase.config).forEach(key => {
footer[key] = edgeCase.config[key];
});
};
applyCase(edgeCases[currentCase]);
// Create controls
const controls = document.createElement('div');
controls.className = 'demo-controls';
Object.entries(edgeCases).forEach(([key, edgeCase]) => {
const button = document.createElement('button');
button.className = 'demo-button' + (key === currentCase ? ' active' : '');
button.textContent = edgeCase.name;
button.onclick = () => {
controls.querySelectorAll('.demo-button').forEach(btn => btn.classList.remove('active'));
button.classList.add('active');
currentCase = key;
applyCase(edgeCase);
};
controls.appendChild(button);
});
wrapperElement.appendChild(controls);
// Add description
const info = document.createElement('div');
info.className = 'demo-info';
info.innerHTML = `
<strong>Edge Case Descriptions:</strong><br>
<strong>Empty:</strong> Minimal configuration with missing data<br>
<strong>Very Long:</strong> Tests layout with extremely long content<br>
<strong>Unicode:</strong> International characters and emojis<br>
<strong>Broken Data:</strong> Invalid or malformed configuration<br>
<strong>Maximum Data:</strong> All features with maximum content
`;
wrapperElement.appendChild(info);
}}
>
<upl-statuspage-footer></upl-statuspage-footer>
</dees-demowrapper>
</div>
</div>
`;

View File

@@ -1,5 +1,7 @@
import { DeesElement, property, html, customElement, type TemplateResult, css, cssManager } from '@design.estate/dees-element';
import { DeesElement, property, html, customElement, type TemplateResult, css, cssManager, unsafeCSS } from '@design.estate/dees-element';
import * as domtools from '@design.estate/dees-domtools';
import { fonts, colors, shadows, borderRadius, spacing, commonStyles } from '../styles/shared.styles.js';
import { demoFunc } from './upl-statuspage-footer.demo.js';
declare global {
interface HTMLElementTagNameMap {
@@ -10,18 +12,80 @@ declare global {
@customElement('upl-statuspage-footer')
export class UplStatuspageFooter extends DeesElement {
// STATIC
public static demo = () => html`
<upl-statuspage-footer></upl-statuspage-footer>
`;
public static demo = demoFunc;
// INSTANCE
@property()
public legalInfo: string = "https://lossless.gmbh";
@property({ type: String })
public companyName: string = '';
@property({
type: Boolean
})
public whitelabel = false;
@property({ type: String })
public legalUrl: string = '';
@property({ type: String })
public supportEmail: string = '';
@property({ type: String })
public statusPageUrl: string = '';
@property({ type: Boolean })
public whitelabel: boolean = false;
@property({ type: Number })
public lastUpdated: number | null = null;
@property({ type: Number })
public currentYear: number = new Date().getFullYear();
@property({ type: Array })
public socialLinks: Array<{ platform: string; url: string }> = [];
@property({ type: Array })
public additionalLinks: Array<{ label: string; url: string }> = [];
@property({ type: String })
public rssFeedUrl: string = '';
@property({ type: String })
public apiStatusUrl: string = '';
@property({ type: Boolean })
public loading: boolean = false;
@property({ type: String })
public errorMessage: string | null = null;
@property({ type: Boolean })
public offline: boolean = false;
@property({ type: String })
public latestStatusUpdate: string = '';
@property({ type: Boolean })
public enableSubscribe: boolean = false;
@property({ type: Boolean })
public enableReportIssue: boolean = false;
@property({ type: Boolean })
public enableLanguageSelector: boolean = false;
@property({ type: Boolean })
public enableThemeToggle: boolean = false;
@property({ type: Array })
public languageOptions: Array<{ code: string; label: string }> = [];
@property({ type: String })
public currentLanguage: string = 'en';
@property({ type: String })
public currentTheme: string = 'light';
@property({ type: Number })
public subscriberCount: number = 0;
@property({ type: Object })
public customBranding: { primaryColor?: string; logoUrl?: string; footerText?: string } | null = null;
constructor() {
@@ -30,43 +94,545 @@ export class UplStatuspageFooter extends DeesElement {
public static styles = [
domtools.elementBasic.staticStyles,
commonStyles,
css`
:host {
display: block;
background: ${cssManager.bdTheme('#ffffff', '#000000')};
font-family: Inter;
color: ${cssManager.bdTheme('#333333', '#ffffff')};
}
display: block;
background: ${colors.background.primary};
font-family: ${unsafeCSS(fonts.base)};
color: ${colors.text.primary};
font-size: 14px;
border-top: 1px solid ${colors.border.default};
}
.mainbox {
max-width: 900px;
margin: auto;
padding-top: 20px;
padding-bottom: 20px;
.container {
max-width: 1200px;
margin: 0 auto;
padding: ${unsafeCSS(spacing['2xl'])} ${unsafeCSS(spacing.lg)};
}
.footer-content {
display: flex;
flex-direction: column;
gap: ${unsafeCSS(spacing.xl)};
}
.footer-main {
display: grid;
grid-template-columns: 1fr auto;
gap: ${unsafeCSS(spacing['2xl'])};
align-items: start;
}
.company-info {
display: flex;
flex-direction: column;
gap: ${unsafeCSS(spacing.lg)};
}
.company-name {
font-size: 20px;
font-weight: 600;
color: ${colors.text.primary};
}
.company-links {
display: flex;
flex-wrap: wrap;
gap: ${unsafeCSS(spacing.lg)};
}
.footer-link {
color: ${colors.text.secondary};
text-decoration: none;
transition: color 0.2s ease;
font-size: 14px;
}
.footer-link:hover {
color: ${colors.text.primary};
text-decoration: underline;
}
.footer-actions {
display: flex;
flex-direction: column;
gap: ${unsafeCSS(spacing.md)};
}
.action-button {
padding: ${unsafeCSS(spacing.sm)} ${unsafeCSS(spacing.md)};
border: 1px solid ${colors.border.default};
background: transparent;
border-radius: ${unsafeCSS(borderRadius.base)};
cursor: pointer;
text-align: center;
transition: all 0.2s ease;
white-space: nowrap;
font-size: 14px;
font-weight: 500;
color: ${colors.text.primary};
font-family: ${unsafeCSS(fonts.base)};
}
.action-button:hover {
background: ${colors.background.secondary};
border-color: ${colors.border.muted};
transform: translateY(-1px);
}
.action-button:active {
transform: translateY(0);
}
.footer-bottom {
display: flex;
justify-content: space-between;
align-items: center;
padding-top: ${unsafeCSS(spacing.xl)};
margin-top: ${unsafeCSS(spacing.xl)};
border-top: 1px solid ${colors.border.default};
}
.footer-meta {
display: flex;
gap: ${unsafeCSS(spacing.lg)};
align-items: center;
flex-wrap: wrap;
}
.social-links {
display: flex;
gap: ${unsafeCSS(spacing.md)};
align-items: center;
}
.social-link {
display: inline-block;
width: 20px;
height: 20px;
opacity: 0.7;
transition: all 0.2s ease;
color: ${colors.text.secondary};
}
.social-link:hover {
opacity: 1;
color: ${colors.text.primary};
transform: translateY(-1px);
}
.social-link svg {
width: 100%;
height: 100%;
fill: currentColor;
}
.copyright {
font-size: 14px;
color: ${colors.text.secondary};
}
.last-updated {
font-size: 13px;
color: ${colors.text.muted};
}
.powered-by {
font-size: 13px;
color: ${colors.text.muted};
text-align: right;
}
.powered-by a {
color: ${colors.text.secondary};
text-decoration: none;
transition: color 0.2s ease;
}
.powered-by a:hover {
color: ${colors.text.primary};
text-decoration: underline;
}
.status-update {
padding: ${unsafeCSS(spacing.md)} ${unsafeCSS(spacing.lg)};
background: ${colors.background.muted};
border-radius: ${unsafeCSS(borderRadius.base)};
font-size: 14px;
margin-bottom: ${unsafeCSS(spacing.lg)};
line-height: 1.6;
}
.language-selector {
position: relative;
}
.language-selector select {
padding: ${unsafeCSS(spacing.xs)} ${unsafeCSS(spacing.sm)};
border: 1px solid ${colors.border.default};
background: ${colors.background.primary};
color: ${colors.text.primary};
border-radius: ${unsafeCSS(borderRadius.base)};
font-size: 14px;
cursor: pointer;
font-family: ${unsafeCSS(fonts.base)};
}
.theme-toggle {
cursor: pointer;
padding: ${unsafeCSS(spacing.xs)} ${unsafeCSS(spacing.sm)};
border: 1px solid ${colors.border.default};
background: transparent;
border-radius: ${unsafeCSS(borderRadius.base)};
font-size: 14px;
transition: all 0.2s ease;
color: ${colors.text.primary};
font-family: ${unsafeCSS(fonts.base)};
}
.theme-toggle:hover {
background: ${colors.background.secondary};
border-color: ${colors.border.muted};
}
.subscriber-count {
font-size: 12px;
color: ${colors.text.muted};
margin-top: ${unsafeCSS(spacing.xs)};
}
.error-message {
padding: ${unsafeCSS(spacing.md)};
background: ${cssManager.bdTheme('#fee9e9', '#7f1d1d')};
color: ${cssManager.bdTheme('#dc2626', '#fca5a5')};
border-radius: ${unsafeCSS(borderRadius.base)};
margin-bottom: ${unsafeCSS(spacing.lg)};
font-size: 14px;
border: 1px solid ${cssManager.bdTheme('#fecaca', '#991b1b')};
}
.offline-indicator {
display: inline-flex;
align-items: center;
gap: ${unsafeCSS(spacing.xs)};
padding: ${unsafeCSS(spacing.xs)} ${unsafeCSS(spacing.md)};
background: ${colors.status.degraded};
color: white;
border-radius: ${unsafeCSS(borderRadius.full)};
font-size: 13px;
font-weight: 500;
}
.loading-skeleton {
height: 200px;
background: ${cssManager.bdTheme(
'linear-gradient(90deg, #f3f4f6 25%, #e5e7eb 50%, #f3f4f6 75%)',
'linear-gradient(90deg, #1f1f1f 25%, #262626 50%, #1f1f1f 75%)'
)};
background-size: 200% 100%;
animation: loading 1.5s infinite;
border-radius: ${unsafeCSS(borderRadius.md)};
}
@keyframes loading {
0% { background-position: 200% 0; }
100% { background-position: -200% 0; }
}
.additional-links {
display: flex;
flex-wrap: wrap;
gap: ${unsafeCSS(spacing.md)};
margin-top: ${unsafeCSS(spacing.md)};
}
.additional-link {
font-size: 14px;
color: ${colors.text.secondary};
text-decoration: none;
transition: color 0.2s ease;
}
.additional-link:hover {
color: ${colors.text.primary};
text-decoration: underline;
}
@media (max-width: 640px) {
.container {
padding: ${unsafeCSS(spacing.xl)} ${unsafeCSS(spacing.md)};
}
.footer-main {
grid-template-columns: 1fr;
gap: ${unsafeCSS(spacing.xl)};
}
.footer-bottom {
flex-direction: column;
gap: ${unsafeCSS(spacing.lg)};
align-items: start;
}
.footer-meta {
flex-direction: column;
align-items: start;
gap: ${unsafeCSS(spacing.md)};
}
.company-links {
flex-direction: column;
gap: ${unsafeCSS(spacing.sm)};
}
.powered-by {
text-align: left;
}
}
`
]
public render(): TemplateResult {
if (this.loading) {
return html`
<div class="container">
<div class="loading-skeleton"></div>
</div>
`;
}
return html`
${domtools.elementBasic.styles}
<style></style>
<div class="mainbox">
Hi there
<div class="container">
<div class="footer-content">
${this.errorMessage ? html`
<div class="error-message">${this.errorMessage}</div>
` : ''}
${this.offline ? html`
<div class="offline-indicator">
<span>⚠</span> You are currently offline
</div>
` : ''}
${this.latestStatusUpdate ? html`
<div class="status-update">
<strong>Latest Update:</strong> ${this.latestStatusUpdate}
</div>
` : ''}
<div class="footer-main">
<div class="company-info">
${this.companyName ? html`
<div class="company-name">${this.companyName}</div>
` : ''}
<div class="company-links">
${this.legalUrl && this.isValidUrl(this.legalUrl) ? html`
<a href="${this.legalUrl}" class="footer-link" @click=${(e: Event) => this.handleLinkClick(e, 'legal', this.legalUrl)}>Legal</a>
` : ''}
${this.supportEmail && this.isValidEmail(this.supportEmail) ? html`
<a href="mailto:${this.supportEmail}" class="footer-link" @click=${(e: Event) => this.handleLinkClick(e, 'support', this.supportEmail)}>Support</a>
` : ''}
${this.statusPageUrl && this.isValidUrl(this.statusPageUrl) ? html`
<a href="${this.statusPageUrl}" class="footer-link" @click=${(e: Event) => this.handleLinkClick(e, 'status', this.statusPageUrl)}>Status Page</a>
` : ''}
${this.rssFeedUrl && this.isValidUrl(this.rssFeedUrl) ? html`
<a href="${this.rssFeedUrl}" class="footer-link" @click=${(e: Event) => this.handleLinkClick(e, 'rss', this.rssFeedUrl)}>RSS Feed</a>
` : ''}
${this.apiStatusUrl && this.isValidUrl(this.apiStatusUrl) ? html`
<a href="${this.apiStatusUrl}" class="footer-link" @click=${(e: Event) => this.handleLinkClick(e, 'api', this.apiStatusUrl)}>API Status</a>
` : ''}
</div>
${this.additionalLinks && this.additionalLinks.length > 0 ? html`
<div class="additional-links">
${this.additionalLinks.map(link => html`
${link.label && link.url && this.isValidUrl(link.url) ? html`
<a href="${link.url}" class="additional-link" @click=${(e: Event) => this.handleLinkClick(e, 'additional', link.url, link.label)}>
${link.label}
</a>
` : ''}
`)}
</div>
` : ''}
</div>
<div class="footer-actions">
${this.enableSubscribe ? html`
<button class="action-button" @click=${this.handleSubscribeClick}>
Subscribe to Updates
${this.subscriberCount > 0 ? html`
<div class="subscriber-count">${this.subscriberCount.toLocaleString()} subscribers</div>
` : ''}
</button>
` : ''}
${this.enableReportIssue ? html`
<button class="action-button" @click=${this.handleReportIncidentClick}>
Report an Issue
</button>
` : ''}
${this.enableLanguageSelector && this.languageOptions.length > 0 ? html`
<div class="language-selector">
<select @change=${this.handleLanguageChange} .value=${this.currentLanguage}>
${this.languageOptions.map(option => html`
<option value="${option.code}" ?selected=${option.code === this.currentLanguage}>
${option.label}
</option>
`)}
</select>
</div>
` : ''}
${this.enableThemeToggle ? html`
<button class="theme-toggle" @click=${this.handleThemeToggle}>
${this.currentTheme === 'dark' ? '☀️' : '🌙'} ${this.currentTheme === 'dark' ? 'Light' : 'Dark'} Mode
</button>
` : ''}
</div>
</div>
<div class="footer-bottom">
<div class="footer-meta">
<div class="copyright">
© ${this.currentYear} ${this.companyName || 'Status Page'}
</div>
${this.lastUpdated ? html`
<div class="last-updated">
Last updated: ${this.formatLastUpdated()}
</div>
` : ''}
${this.socialLinks && this.socialLinks.length > 0 ? html`
<div class="social-links">
${this.socialLinks.map(social => html`
${social.platform && social.url && this.isValidUrl(social.url) ? html`
<a href="${social.url}" class="social-link" title="${social.platform}" @click=${(e: Event) => this.handleLinkClick(e, 'social', social.url, social.platform)}>
${this.getSocialIcon(social.platform)}
</a>
` : ''}
`)}
</div>
` : ''}
</div>
${!this.whitelabel ? html`
<div class="powered-by">
${this.customBranding?.footerText || html`
Powered by <a href="https://uptime.link" target="_blank" @click=${(e: Event) => this.handleLinkClick(e, 'powered-by', 'https://uptime.link')}>uptime.link</a>
`}
</div>
` : ''}
</div>
</div>
</div>
`;
}
private isValidUrl(url: string): boolean {
if (!url) return false;
try {
new URL(url);
return true;
} catch {
return url.startsWith('#') || url.startsWith('/');
}
}
private isValidEmail(email: string): boolean {
if (!email) return false;
return email.includes('@');
}
private formatLastUpdated(): string {
if (!this.lastUpdated) return 'Never';
const date = new Date(this.lastUpdated);
const now = Date.now();
const diff = now - this.lastUpdated;
if (diff < 60000) {
return 'Just now';
} else if (diff < 3600000) {
const minutes = Math.floor(diff / 60000);
return `${minutes} minute${minutes !== 1 ? 's' : ''} ago`;
} else if (diff < 86400000) {
const hours = Math.floor(diff / 3600000);
return `${hours} hour${hours !== 1 ? 's' : ''} ago`;
} else {
return date.toLocaleDateString();
}
}
private getSocialIcon(platform: string): TemplateResult {
const icons: Record<string, TemplateResult> = {
twitter: html`<svg viewBox="0 0 24 24"><path d="M23.953 4.57a10 10 0 01-2.825.775 4.958 4.958 0 002.163-2.723c-.951.555-2.005.959-3.127 1.184a4.92 4.92 0 00-8.384 4.482C7.69 8.095 4.067 6.13 1.64 3.162a4.822 4.822 0 00-.666 2.475c0 1.71.87 3.213 2.188 4.096a4.904 4.904 0 01-2.228-.616v.06a4.923 4.923 0 003.946 4.827 4.996 4.996 0 01-2.212.085 4.936 4.936 0 004.604 3.417 9.867 9.867 0 01-6.102 2.105c-.39 0-.779-.023-1.17-.067a13.995 13.995 0 007.557 2.209c9.053 0 13.998-7.496 13.998-13.985 0-.21 0-.42-.015-.63A9.935 9.935 0 0024 4.59z"/></svg>`,
github: html`<svg viewBox="0 0 24 24"><path d="M12 .297c-6.63 0-12 5.373-12 12 0 5.303 3.438 9.8 8.205 11.385.6.113.82-.258.82-.577 0-.285-.01-1.04-.015-2.04-3.338.724-4.042-1.61-4.042-1.61C4.422 18.07 3.633 17.7 3.633 17.7c-1.087-.744.084-.729.084-.729 1.205.084 1.838 1.236 1.838 1.236 1.07 1.835 2.809 1.305 3.495.998.108-.776.417-1.305.76-1.605-2.665-.3-5.466-1.332-5.466-5.93 0-1.31.465-2.38 1.235-3.22-.135-.303-.54-1.523.105-3.176 0 0 1.005-.322 3.3 1.23.96-.267 1.98-.399 3-.405 1.02.006 2.04.138 3 .405 2.28-1.552 3.285-1.23 3.285-1.23.645 1.653.24 2.873.12 3.176.765.84 1.23 1.91 1.23 3.22 0 4.61-2.805 5.625-5.475 5.92.42.36.81 1.096.81 2.22 0 1.606-.015 2.896-.015 3.286 0 .315.21.69.825.57C20.565 22.092 24 17.592 24 12.297c0-6.627-5.373-12-12-12"/></svg>`,
linkedin: html`<svg viewBox="0 0 24 24"><path d="M20.447 20.452h-3.554v-5.569c0-1.328-.027-3.037-1.852-3.037-1.853 0-2.136 1.445-2.136 2.939v5.667H9.351V9h3.414v1.561h.046c.477-.9 1.637-1.85 3.37-1.85 3.601 0 4.267 2.37 4.267 5.455v6.286zM5.337 7.433c-1.144 0-2.063-.926-2.063-2.065 0-1.138.92-2.063 2.063-2.063 1.14 0 2.064.925 2.064 2.063 0 1.139-.925 2.065-2.064 2.065zm1.782 13.019H3.555V9h3.564v11.452zM22.225 0H1.771C.792 0 0 .774 0 1.729v20.542C0 23.227.792 24 1.771 24h20.451C23.2 24 24 23.227 24 22.271V1.729C24 .774 23.2 0 22.222 0h.003z"/></svg>`,
facebook: html`<svg viewBox="0 0 24 24"><path d="M24 12.073c0-6.627-5.373-12-12-12s-12 5.373-12 12c0 5.99 4.388 10.954 10.125 11.854v-8.385H7.078v-3.47h3.047V9.43c0-3.007 1.792-4.669 4.533-4.669 1.312 0 2.686.235 2.686.235v2.953H15.83c-1.491 0-1.956.925-1.956 1.874v2.25h3.328l-.532 3.47h-2.796v8.385C19.612 23.027 24 18.062 24 12.073z"/></svg>`,
youtube: html`<svg viewBox="0 0 24 24"><path d="M23.498 6.186a3.016 3.016 0 0 0-2.122-2.136C19.505 3.545 12 3.545 12 3.545s-7.505 0-9.377.505A3.017 3.017 0 0 0 .502 6.186C0 8.07 0 12 0 12s0 3.93.502 5.814a3.016 3.016 0 0 0 2.122 2.136c1.871.505 9.376.505 9.376.505s7.505 0 9.377-.505a3.015 3.015 0 0 0 2.122-2.136C24 15.93 24 12 24 12s0-3.93-.502-5.814zM9.545 15.568V8.432L15.818 12l-6.273 3.568z"/></svg>`,
instagram: html`<svg viewBox="0 0 24 24"><path d="M12 2.163c3.204 0 3.584.012 4.85.07 3.252.148 4.771 1.691 4.919 4.919.058 1.265.069 1.645.069 4.849 0 3.205-.012 3.584-.069 4.849-.149 3.225-1.664 4.771-4.919 4.919-1.266.058-1.644.07-4.85.07-3.204 0-3.584-.012-4.849-.07-3.26-.149-4.771-1.699-4.919-4.92-.058-1.265-.07-1.644-.07-4.849 0-3.204.013-3.583.07-4.849.149-3.227 1.664-4.771 4.919-4.919 1.266-.057 1.645-.069 4.849-.069zm0-2.163c-3.259 0-3.667.014-4.947.072-4.358.2-6.78 2.618-6.98 6.98-.059 1.281-.073 1.689-.073 4.948 0 3.259.014 3.668.072 4.948.2 4.358 2.618 6.78 6.98 6.98 1.281.058 1.689.072 4.948.072 3.259 0 3.668-.014 4.948-.072 4.354-.2 6.782-2.618 6.979-6.98.059-1.28.073-1.689.073-4.948 0-3.259-.014-3.667-.072-4.947-.196-4.354-2.617-6.78-6.979-6.98-1.281-.059-1.69-.073-4.949-.073zM5.838 12a6.162 6.162 0 1 1 12.324 0 6.162 6.162 0 0 1-12.324 0zM12 16a4 4 0 1 1 0-8 4 4 0 0 1 0 8zm4.965-10.405a1.44 1.44 0 1 1 2.881.001 1.44 1.44 0 0 1-2.881-.001z"/></svg>`,
slack: html`<svg viewBox="0 0 24 24"><path d="M5.042 15.165a2.528 2.528 0 0 1-2.52 2.523A2.528 2.528 0 0 1 0 15.165a2.527 2.527 0 0 1 2.522-2.52h2.52v2.52zM6.313 15.165a2.527 2.527 0 0 1 2.521-2.52 2.527 2.527 0 0 1 2.521 2.52v6.313A2.528 2.528 0 0 1 8.834 24a2.528 2.528 0 0 1-2.521-2.522v-6.313zM8.834 5.042a2.528 2.528 0 0 1-2.521-2.52A2.528 2.528 0 0 1 8.834 0a2.528 2.528 0 0 1 2.521 2.522v2.52H8.834zM8.834 6.313a2.528 2.528 0 0 1 2.521 2.521 2.528 2.528 0 0 1-2.521 2.521H2.522A2.528 2.528 0 0 1 0 8.834a2.528 2.528 0 0 1 2.522-2.521h6.312zM18.956 8.834a2.528 2.528 0 0 1 2.522-2.521A2.528 2.528 0 0 1 24 8.834a2.528 2.528 0 0 1-2.522 2.521h-2.522V8.834zM17.688 8.834a2.528 2.528 0 0 1-2.523 2.521 2.527 2.527 0 0 1-2.52-2.521V2.522A2.527 2.527 0 0 1 15.165 0a2.528 2.528 0 0 1 2.523 2.522v6.312zM15.165 18.956a2.528 2.528 0 0 1 2.523 2.522A2.528 2.528 0 0 1 15.165 24a2.527 2.527 0 0 1-2.52-2.522v-2.522h2.52zM15.165 17.688a2.527 2.527 0 0 1-2.52-2.523 2.526 2.526 0 0 1 2.52-2.52h6.313A2.527 2.527 0 0 1 24 15.165a2.528 2.528 0 0 1-2.522 2.523h-6.313z"/></svg>`,
discord: html`<svg viewBox="0 0 24 24"><path d="M20.317 4.3698a19.7913 19.7913 0 00-4.8851-1.5152.0741.0741 0 00-.0785.0371c-.211.3753-.4447.8648-.6083 1.2495-1.8447-.2762-3.68-.2762-5.4868 0-.1636-.3933-.4058-.8742-.6177-1.2495a.077.077 0 00-.0785-.037 19.7363 19.7363 0 00-4.8852 1.515.0699.0699 0 00-.0321.0277C.5334 9.0458-.319 13.5799.0992 18.0578a.0824.0824 0 00.0312.0561c2.0528 1.5076 4.0413 2.4228 5.9929 3.0294a.0777.0777 0 00.0842-.0276c.4616-.6304.8731-1.2952 1.226-1.9942a.076.076 0 00-.0416-.1057c-.6528-.2476-1.2743-.5495-1.8722-.8923a.077.077 0 01-.0076-.1277c.1258-.0943.2517-.1923.3718-.2914a.0743.0743 0 01.0776-.0105c3.9278 1.7933 8.18 1.7933 12.0614 0a.0739.0739 0 01.0785.0095c.1202.099.246.1981.3728.2924a.077.077 0 01-.0066.1276 12.2986 12.2986 0 01-1.873.8914.0766.0766 0 00-.0407.1067c.3604.698.7719 1.3628 1.225 1.9932a.076.076 0 00.0842.0286c1.961-.6067 3.9495-1.5219 6.0023-3.0294a.077.077 0 00.0313-.0552c.5004-5.177-.8382-9.6739-3.5485-13.6604a.061.061 0 00-.0312-.0286zM8.02 15.3312c-1.1825 0-2.1569-1.0857-2.1569-2.419 0-1.3332.9555-2.4189 2.157-2.4189 1.2108 0 2.1757 1.0952 2.1568 2.419 0 1.3332-.9555 2.4189-2.1569 2.4189zm7.9748 0c-1.1825 0-2.1569-1.0857-2.1569-2.419 0-1.3332.9554-2.4189 2.1569-2.4189 1.2108 0 2.1757 1.0952 2.1568 2.419 0 1.3332-.946 2.4189-2.1568 2.4189Z"/></svg>`,
generic: html`<svg viewBox="0 0 24 24"><path d="M13.46,12L19,17.54V19H17.54L12,13.46L6.46,19H5V17.54L10.54,12L5,6.46V5H6.46L12,10.54L17.54,5H19V6.46L13.46,12Z"/></svg>`
};
return icons[platform.toLowerCase()] || icons.generic;
}
private handleLinkClick(event: Event, type: string, url: string, label?: string) {
this.dispatchEvent(new CustomEvent('footerLinkClick', {
detail: { type, url, label },
bubbles: true,
composed: true
}));
}
private handleSubscribeClick() {
this.dispatchEvent(new CustomEvent('subscribeClick', {
bubbles: true,
composed: true
}));
}
private handleReportIncidentClick() {
this.dispatchEvent(new CustomEvent('reportIncidentClick', {
bubbles: true,
composed: true
}));
}
private handleLanguageChange(event: Event) {
const select = event.target as HTMLSelectElement;
const language = select.value;
this.currentLanguage = language;
this.dispatchEvent(new CustomEvent('languageChange', {
detail: { language },
bubbles: true,
composed: true
}));
}
private handleThemeToggle() {
const newTheme = this.currentTheme === 'dark' ? 'light' : 'dark';
this.currentTheme = newTheme;
this.dispatchEvent(new CustomEvent('themeToggle', {
detail: { theme: newTheme },
bubbles: true,
composed: true
}));
}
public dispatchReportNewIncident() {
this.dispatchEvent(new CustomEvent('reportNewIncident', {
}))
this.handleReportIncidentClick();
}
public dispatchStatusSubscribe() {
this.dispatchEvent(new CustomEvent('statusSubscribe', {
}))
this.handleSubscribeClick();
}
}

View File

@@ -0,0 +1,241 @@
import { html } from '@design.estate/dees-element';
export const demoFunc = () => html`
<style>
.demo-container {
display: flex;
flex-direction: column;
gap: 20px;
}
.demo-section {
border: 1px solid #ddd;
border-radius: 8px;
padding: 20px;
background: #f5f5f5;
}
.demo-title {
font-size: 14px;
font-weight: 600;
margin-bottom: 16px;
color: #333;
}
.demo-controls {
display: flex;
gap: 10px;
margin-top: 16px;
flex-wrap: wrap;
}
.demo-button {
padding: 6px 12px;
border: 1px solid #ddd;
background: white;
border-radius: 4px;
cursor: pointer;
font-size: 13px;
}
.demo-button:hover {
background: #f0f0f0;
}
</style>
<div class="demo-container">
<!-- Basic Header -->
<div class="demo-section">
<div class="demo-title">Basic Header with Dynamic Title</div>
<dees-demowrapper
.runAfterRender=${async (wrapperElement: any) => {
const header = wrapperElement.querySelector('upl-statuspage-header') as any;
// Demo different titles
const titles = [
'MyService Status Page',
'Production Environment Status',
'API Health Dashboard',
'Global Infrastructure Status',
'🚀 Rocket Systems Monitor',
'Multi-Region Service Status'
];
let titleIndex = 0;
header.pageTitle = titles[titleIndex];
// Add event listeners
header.addEventListener('reportNewIncident', (event: CustomEvent) => {
console.log('Report incident clicked');
alert('Report Incident form would open here');
});
header.addEventListener('statusSubscribe', (event: CustomEvent) => {
console.log('Subscribe clicked');
alert('Subscribe modal would open here');
});
// Cycle through titles
setInterval(() => {
titleIndex = (titleIndex + 1) % titles.length;
header.pageTitle = titles[titleIndex];
}, 2000);
}}
>
<upl-statuspage-header></upl-statuspage-header>
</dees-demowrapper>
</div>
<!-- Header with Hidden Buttons -->
<div class="demo-section">
<div class="demo-title">Header with Configurable Buttons</div>
<dees-demowrapper
.runAfterRender=${async (wrapperElement: any) => {
const header = wrapperElement.querySelector('upl-statuspage-header') as any;
header.pageTitle = 'Configurable Button States';
// Add properties to control button visibility
header.showReportButton = true;
header.showSubscribeButton = true;
const controls = document.createElement('div');
controls.className = 'demo-controls';
controls.innerHTML = `
<button class="demo-button" id="toggleReport">Toggle Report Button</button>
<button class="demo-button" id="toggleSubscribe">Toggle Subscribe Button</button>
<button class="demo-button" id="toggleBoth">Hide Both</button>
<button class="demo-button" id="showBoth">Show Both</button>
`;
wrapperElement.appendChild(controls);
controls.querySelector('#toggleReport')?.addEventListener('click', () => {
header.showReportButton = !header.showReportButton;
});
controls.querySelector('#toggleSubscribe')?.addEventListener('click', () => {
header.showSubscribeButton = !header.showSubscribeButton;
});
controls.querySelector('#toggleBoth')?.addEventListener('click', () => {
header.showReportButton = false;
header.showSubscribeButton = false;
});
controls.querySelector('#showBoth')?.addEventListener('click', () => {
header.showReportButton = true;
header.showSubscribeButton = true;
});
}}
>
<upl-statuspage-header></upl-statuspage-header>
</dees-demowrapper>
</div>
<!-- Header with Custom Styling -->
<div class="demo-section">
<div class="demo-title">Header with Custom Branding</div>
<dees-demowrapper
.runAfterRender=${async (wrapperElement: any) => {
const header = wrapperElement.querySelector('upl-statuspage-header') as any;
header.pageTitle = 'Enterprise Cloud Platform';
// Custom branding properties
header.brandColor = '#1976D2';
header.logoUrl = 'https://via.placeholder.com/120x40/1976D2/ffffff?text=LOGO';
header.customStyles = true;
// Simulate different brand states
const brands = [
{ title: 'Enterprise Cloud Platform', color: '#1976D2', logo: 'ENTERPRISE' },
{ title: 'StartUp SaaS Monitor', color: '#00BCD4', logo: 'STARTUP' },
{ title: 'Government Services Status', color: '#4CAF50', logo: 'GOV' },
{ title: 'Financial Systems Health', color: '#673AB7', logo: 'FINTECH' }
];
let brandIndex = 0;
setInterval(() => {
brandIndex = (brandIndex + 1) % brands.length;
const brand = brands[brandIndex];
header.pageTitle = brand.title;
header.brandColor = brand.color;
header.logoUrl = `https://via.placeholder.com/120x40/${brand.color.slice(1)}/ffffff?text=${brand.logo}`;
}, 3000);
}}
>
<upl-statuspage-header></upl-statuspage-header>
</dees-demowrapper>
</div>
<!-- Header with Loading State -->
<div class="demo-section">
<div class="demo-title">Header with Loading States</div>
<dees-demowrapper
.runAfterRender=${async (wrapperElement: any) => {
const header = wrapperElement.querySelector('upl-statuspage-header') as any;
header.pageTitle = 'Loading State Demo';
header.loading = true;
// Simulate loading completion
setTimeout(() => {
header.loading = false;
header.pageTitle = 'Status Page Loaded';
}, 2000);
// Add loading toggle
const controls = document.createElement('div');
controls.className = 'demo-controls';
controls.innerHTML = `
<button class="demo-button" id="toggleLoading">Toggle Loading State</button>
`;
wrapperElement.appendChild(controls);
controls.querySelector('#toggleLoading')?.addEventListener('click', () => {
header.loading = !header.loading;
if (header.loading) {
header.pageTitle = 'Loading...';
setTimeout(() => {
header.loading = false;
header.pageTitle = 'Status Page Ready';
}, 2000);
}
});
}}
>
<upl-statuspage-header></upl-statuspage-header>
</dees-demowrapper>
</div>
<!-- Header with Event Counter -->
<div class="demo-section">
<div class="demo-title">Header with Event Tracking</div>
<dees-demowrapper
.runAfterRender=${async (wrapperElement: any) => {
const header = wrapperElement.querySelector('upl-statuspage-header') as any;
header.pageTitle = 'Event Tracking Demo';
let reportCount = 0;
let subscribeCount = 0;
// Create counter display
const counterDisplay = document.createElement('div');
counterDisplay.style.marginTop = '16px';
counterDisplay.style.fontSize = '14px';
counterDisplay.innerHTML = `
<div>Report Clicks: <strong id="reportCount">0</strong></div>
<div>Subscribe Clicks: <strong id="subscribeCount">0</strong></div>
`;
wrapperElement.appendChild(counterDisplay);
header.addEventListener('reportNewIncident', () => {
reportCount++;
counterDisplay.querySelector('#reportCount').textContent = reportCount.toString();
console.log(`Report incident clicked ${reportCount} times`);
});
header.addEventListener('statusSubscribe', () => {
subscribeCount++;
counterDisplay.querySelector('#subscribeCount').textContent = subscribeCount.toString();
console.log(`Subscribe clicked ${subscribeCount} times`);
});
}}
>
<upl-statuspage-header></upl-statuspage-header>
</dees-demowrapper>
</div>
</div>
`;

View File

@@ -1,5 +1,7 @@
import { DeesElement, property, html, customElement, type TemplateResult, css, cssManager } from '@design.estate/dees-element';
import { DeesElement, property, html, customElement, type TemplateResult, css, cssManager, unsafeCSS } from '@design.estate/dees-element';
import * as domtools from '@design.estate/dees-domtools';
import { fonts } from '../styles/shared.styles.js';
import { demoFunc } from './upl-statuspage-header.demo.js';
declare global {
interface HTMLElementTagNameMap {
@@ -10,14 +12,30 @@ declare global {
@customElement('upl-statuspage-header')
export class UplStatuspageHeader extends DeesElement {
// STATIC
public static demo = () => html`
<upl-statuspage-header></upl-statuspage-header>
`;
public static demo = demoFunc;
// INSTANCE
@property()
@property({ type: String })
public pageTitle: string = "Statuspage Title";
@property({ type: Boolean })
public showReportButton: boolean = true;
@property({ type: Boolean })
public showSubscribeButton: boolean = true;
@property({ type: String })
public brandColor: string = '';
@property({ type: String })
public logoUrl: string = '';
@property({ type: Boolean })
public customStyles: boolean = false;
@property({ type: Boolean })
public loading: boolean = false;
constructor() {
super();
@@ -28,70 +46,162 @@ export class UplStatuspageHeader extends DeesElement {
css`
:host {
display: block;
background: ${cssManager.bdTheme('#eeeeeb', '#222222')};
font-family: Inter;
color: ${cssManager.bdTheme('#333333', '#ffffff')};
background: ${cssManager.bdTheme('#ffffff', '#0a0a0a')};
font-family: ${unsafeCSS(fonts.base)};
color: ${cssManager.bdTheme('#0a0a0a', '#fafafa')};
border-bottom: 1px solid ${cssManager.bdTheme('#e5e7eb', '#262626')};
}
.mainbox {
margin: auto;
max-width: 900px;
max-width: 1200px;
padding: 0 24px;
}
.mainbox .actions {
display: flex;
justify-content: flex-end;
padding: 20px 0px 40px 0px;
gap: 8px;
padding: 24px 0;
}
.mainbox .actions .actionButton {
background: ${cssManager.bdTheme('#00000000', '#ffffff00')};
font-size: 12px;
border: 1px solid ${cssManager.bdTheme('#333', '#CCC')};
padding: 6px 10px 7px 10px;
margin-left: 10px;
border-radius: 3px;
background: transparent;
font-size: 14px;
font-weight: 500;
border: 1px solid ${cssManager.bdTheme('#e5e7eb', '#262626')};
padding: 8px 16px;
border-radius: 6px;
cursor: pointer;
user-select: none;
transition: all 0.2s ease;
display: inline-flex;
align-items: center;
justify-content: center;
min-height: 36px;
color: ${cssManager.bdTheme('#0a0a0a', '#fafafa')};
}
.mainbox .actions .actionButton:hover {
background: ${cssManager.bdTheme('#333333', '#efefef')};
border: 1px solid ${cssManager.bdTheme('#333333', '#efefef')};
color: ${cssManager.bdTheme('#fff', '#333333')};
background: ${cssManager.bdTheme('#f9fafb', '#262626')};
border-color: ${cssManager.bdTheme('#d1d5db', '#404040')};
transform: translateY(-1px);
}
.mainbox .actions .actionButton:active {
transform: translateY(0);
}
.header-content {
padding: 48px 0 64px 0;
text-align: center;
}
h1 {
margin: 0px;
text-align: center;
font-weight: 600;
font-size: 35px;
margin: 0;
font-size: 48px;
font-weight: 700;
letter-spacing: -0.02em;
line-height: 1.1;
color: ${cssManager.bdTheme('#0a0a0a', '#fafafa')};
}
h2 {
margin: 0px;
margin-top: 10px;
text-align: center;
font-weight: 600;
font-size: 18px;
.subtitle {
margin: 16px 0 0 0;
font-size: 16px;
font-weight: 400;
color: ${cssManager.bdTheme('#6b7280', '#a1a1aa')};
letter-spacing: 0.02em;
text-transform: uppercase;
}
.logo {
display: block;
margin: 0 auto 32px;
max-width: 180px;
height: auto;
filter: ${cssManager.bdTheme('none', 'brightness(0) invert(1)')};
}
.loading-skeleton {
height: 200px;
background: ${cssManager.bdTheme(
'linear-gradient(90deg, #f3f4f6 25%, #e5e7eb 50%, #f3f4f6 75%)',
'linear-gradient(90deg, #1f1f1f 25%, #262626 50%, #1f1f1f 75%)'
)};
background-size: 200% 100%;
animation: loading 1.5s infinite;
border-radius: 6px;
margin: 24px 0;
}
@keyframes loading {
0% { background-position: 200% 0; }
100% { background-position: -200% 0; }
}
/* Primary button variant */
.actionButton.primary {
background: ${cssManager.bdTheme('#0a0a0a', '#fafafa')};
color: ${cssManager.bdTheme('#ffffff', '#0a0a0a')};
border-color: ${cssManager.bdTheme('#0a0a0a', '#fafafa')};
}
.actionButton.primary:hover {
background: ${cssManager.bdTheme('#262626', '#f4f4f5')};
border-color: ${cssManager.bdTheme('#262626', '#f4f4f5')};
}
`
]
public render(): TemplateResult {
if (this.loading) {
return html`
<div class="mainbox">
<div class="loading-skeleton"></div>
</div>
`;
}
return html`
${domtools.elementBasic.styles}
<style>
${this.customStyles && this.brandColor ? `
.mainbox .actions .actionButton {
border-color: ${this.brandColor};
color: ${this.brandColor};
}
.mainbox .actions .actionButton:hover {
background: ${this.brandColor}10;
border-color: ${this.brandColor};
}
.mainbox .actions .actionButton.primary {
background: ${this.brandColor};
border-color: ${this.brandColor};
color: white;
}
.mainbox .actions .actionButton.primary:hover {
background: ${this.brandColor}dd;
border-color: ${this.brandColor}dd;
}
` : ''}
</style>
<div class="mainbox">
<div class="actions">
<div class="actionButton" @click=${this.dispatchReportNewIncident}>report new incident</div>
<div class="actionButton" @click=${this.dispatchStatusSubscribe}>subscribe</div>
${this.showReportButton ? html`
<div class="actionButton" @click=${this.dispatchReportNewIncident}>Report Issue</div>
` : ''}
${this.showSubscribeButton ? html`
<div class="actionButton primary" @click=${this.dispatchStatusSubscribe}>Subscribe</div>
` : ''}
</div>
<div class="header-content">
${this.logoUrl ? html`
<img src="${this.logoUrl}" alt="Logo" class="logo">
` : ''}
<h1>${this.pageTitle}</h1>
<div class="subtitle">System Status</div>
</div>
<h1>${this.pageTitle}</h1>
<h2>STATUS BOARD</h2>
</div>
`;
}

File diff suppressed because it is too large Load Diff

View File

@@ -7,7 +7,12 @@ import {
type TemplateResult,
css,
cssManager,
unsafeCSS,
} from '@design.estate/dees-element';
import type { IIncidentDetails } from '../interfaces/index.js';
import { fonts, colors, shadows, borderRadius, spacing, commonStyles } from '../styles/shared.styles.js';
import './internal/uplinternal-miniheading.js';
import { demoFunc } from './upl-statuspage-incidents.demo.js';
declare global {
interface HTMLElementTagNameMap {
@@ -18,73 +23,490 @@ declare global {
@customElement('upl-statuspage-incidents')
export class UplStatuspageIncidents extends DeesElement {
// STATIC
public static demo = () => html` <upl-statuspage-incidents></upl-statuspage-incidents> `;
public static demo = demoFunc;
// INSTANCE
@property({
type: Array,
})
public currentIncidences: plugins.uplInterfaces.data.IIncident[] = [];
public currentIncidents: IIncidentDetails[] = [];
@property({
type: Array,
})
public pastIncidences: plugins.uplInterfaces.data.IIncident[] = [];
public pastIncidents: IIncidentDetails[] = [];
@property({
type: Boolean,
})
public whitelabel = false;
@property({
type: Boolean,
})
public loading = false;
@property({
type: Number,
})
public daysToShow = 90;
constructor() {
super();
}
public static styles = [
plugins.domtools.elementBasic.staticStyles,
commonStyles,
css`
:host {
display: block;
background: ${cssManager.bdTheme('#eeeeeb', '#222222')};
font-family: Inter;
color: ${cssManager.bdTheme('#333333', '#ffffff')};
background: transparent;
font-family: ${unsafeCSS(fonts.base)};
color: ${colors.text.primary};
}
.mainbox {
max-width: 900px;
margin: auto;
.container {
max-width: 1200px;
margin: 0 auto;
padding: 0 ${unsafeCSS(spacing.lg)} ${unsafeCSS(spacing.lg)} ${unsafeCSS(spacing.lg)};
}
.noIncidentBox {
background: ${cssManager.bdTheme('#ffffff', '#333333')};;
padding: 10px;
margin-bottom: 15px;
border-radius: 3px;
background: ${colors.background.card};
padding: ${unsafeCSS(spacing.xl)};
margin-bottom: ${unsafeCSS(spacing.lg)};
border-radius: ${unsafeCSS(borderRadius.md)};
border: 1px solid ${colors.border.default};
text-align: center;
color: ${colors.text.secondary};
box-shadow: ${unsafeCSS(shadows.sm)};
}
.incident-card {
background: ${colors.background.card};
border-radius: ${unsafeCSS(borderRadius.md)};
margin-bottom: ${unsafeCSS(spacing.lg)};
overflow: hidden;
box-shadow: ${unsafeCSS(shadows.sm)};
border: 1px solid ${colors.border.default};
transition: all 0.2s ease;
cursor: pointer;
}
.incident-card:hover {
box-shadow: ${unsafeCSS(shadows.md)};
transform: translateY(-2px);
}
.incident-header {
padding: ${unsafeCSS(spacing.lg)} ${unsafeCSS(spacing.lg)};
border-left: 4px solid;
display: flex;
align-items: start;
justify-content: space-between;
gap: ${unsafeCSS(spacing.md)};
}
.incident-header.critical {
border-left-color: ${colors.status.major};
}
.incident-header.major {
border-left-color: ${colors.status.partial};
}
.incident-header.minor {
border-left-color: ${colors.status.degraded};
}
.incident-header.maintenance {
border-left-color: ${colors.status.maintenance};
}
.incident-title {
font-size: 18px;
font-weight: 600;
margin: 0;
line-height: 1.3;
}
.incident-meta {
display: flex;
gap: ${unsafeCSS(spacing.lg)};
margin-top: ${unsafeCSS(spacing.sm)};
font-size: 13px;
color: ${colors.text.secondary};
flex-wrap: wrap;
}
.incident-status {
display: inline-flex;
align-items: center;
gap: ${unsafeCSS(spacing.xs)};
padding: ${unsafeCSS(spacing.xs)} ${unsafeCSS(spacing.md)};
border-radius: ${unsafeCSS(borderRadius.full)};
font-size: 12px;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.02em;
flex-shrink: 0;
}
.incident-status.investigating {
background: ${cssManager.bdTheme('#fef3c7', '#78350f')};
color: ${cssManager.bdTheme('#92400e', '#fbbf24')};
}
.incident-status.identified {
background: ${cssManager.bdTheme('#e9d5ff', '#581c87')};
color: ${cssManager.bdTheme('#6b21a8', '#d8b4fe')};
}
.incident-status.monitoring {
background: ${cssManager.bdTheme('#dbeafe', '#1e3a8a')};
color: ${cssManager.bdTheme('#1e40af', '#93c5fd')};
}
.incident-status.resolved {
background: ${cssManager.bdTheme('#d1fae5', '#064e3b')};
color: ${cssManager.bdTheme('#047857', '#6ee7b7')};
}
.incident-status.postmortem {
background: ${cssManager.bdTheme('#e5e7eb', '#374151')};
color: ${cssManager.bdTheme('#4b5563', '#d1d5db')};
}
.incident-body {
padding: 0 ${unsafeCSS(spacing.lg)} ${unsafeCSS(spacing.lg)} ${unsafeCSS(spacing.lg)};
}
.incident-impact {
margin: ${unsafeCSS(spacing.md)} 0;
padding: ${unsafeCSS(spacing.md)};
background: ${colors.background.secondary};
border-radius: ${unsafeCSS(borderRadius.base)};
font-size: 14px;
line-height: 1.6;
}
.affected-services {
margin-top: ${unsafeCSS(spacing.md)};
}
.affected-services-title {
font-size: 13px;
font-weight: 600;
margin-bottom: ${unsafeCSS(spacing.sm)};
color: ${colors.text.primary};
}
.service-tag {
display: inline-block;
padding: ${unsafeCSS(spacing.xs)} ${unsafeCSS(spacing.sm)};
margin: 2px;
background: ${colors.background.muted};
border-radius: ${unsafeCSS(borderRadius.sm)};
font-size: 12px;
color: ${colors.text.secondary};
}
.incident-updates {
margin-top: ${unsafeCSS(spacing.lg)};
border-top: 1px solid ${colors.border.default};
padding-top: ${unsafeCSS(spacing.lg)};
}
.update-item {
position: relative;
padding-left: ${unsafeCSS(spacing.lg)};
margin-bottom: ${unsafeCSS(spacing.md)};
}
.update-item::before {
content: '';
position: absolute;
left: 0;
top: 6px;
width: 8px;
height: 8px;
border-radius: ${unsafeCSS(borderRadius.full)};
background: ${colors.border.muted};
}
.update-time {
font-size: 12px;
color: ${colors.text.secondary};
margin-bottom: ${unsafeCSS(spacing.xs)};
font-family: ${unsafeCSS(fonts.mono)};
}
.update-message {
font-size: 14px;
line-height: 1.6;
color: ${colors.text.primary};
}
.update-author {
font-size: 12px;
color: ${colors.text.secondary};
margin-top: ${unsafeCSS(spacing.xs)};
font-style: italic;
}
.loading-skeleton {
height: 140px;
background: ${cssManager.bdTheme(
'linear-gradient(90deg, #f3f4f6 25%, #e5e7eb 50%, #f3f4f6 75%)',
'linear-gradient(90deg, #1f1f1f 25%, #262626 50%, #1f1f1f 75%)'
)};
background-size: 200% 100%;
animation: loading 1.5s infinite;
border-radius: ${unsafeCSS(borderRadius.md)};
margin-bottom: ${unsafeCSS(spacing.lg)};
}
@keyframes loading {
0% { background-position: 200% 0; }
100% { background-position: -200% 0; }
}
.show-more {
text-align: center;
margin-top: ${unsafeCSS(spacing.lg)};
}
.show-more-button {
display: inline-flex;
align-items: center;
justify-content: center;
padding: ${unsafeCSS(spacing.sm)} ${unsafeCSS(spacing.lg)};
background: transparent;
border: 1px solid ${colors.border.default};
border-radius: ${unsafeCSS(borderRadius.base)};
cursor: pointer;
font-size: 14px;
font-weight: 500;
transition: all 0.2s ease;
color: ${colors.text.primary};
font-family: ${unsafeCSS(fonts.base)};
}
.show-more-button:hover {
background: ${colors.background.secondary};
border-color: ${colors.border.muted};
transform: translateY(-1px);
}
.show-more-button:active {
transform: translateY(0);
}
@media (max-width: 640px) {
.container {
padding: 0 ${unsafeCSS(spacing.md)} ${unsafeCSS(spacing.md)} ${unsafeCSS(spacing.md)};
}
.incident-header {
flex-direction: column;
align-items: start;
gap: ${unsafeCSS(spacing.sm)};
}
.incident-meta {
flex-direction: column;
gap: ${unsafeCSS(spacing.xs)};
}
}
`,
];
public render(): TemplateResult {
return html`
<style></style>
<div class="mainbox">
<uplinternal-miniheading> Current Incidents </uplinternal-miniheading>
${this.currentIncidences.length
? html``
: html` <div class="noIncidentBox">No incidents ongoing.</div> `}
<uplinternal-miniheading> Past Incidents </uplinternal-miniheading>
${this.pastIncidences.length
? html``
: html` <div class="noIncidentBox">No past incidents in the last 90 days.</div> `}
<div class="container">
<uplinternal-miniheading>Current Incidents</uplinternal-miniheading>
${this.loading ? html`
<div class="loading-skeleton"></div>
` : this.currentIncidents.length ?
this.currentIncidents.map(incident => this.renderIncident(incident, true)) :
html`<div class="noIncidentBox">No incidents ongoing.</div>`
}
<uplinternal-miniheading>Past Incidents</uplinternal-miniheading>
${this.loading ? html`
<div class="loading-skeleton"></div>
<div class="loading-skeleton"></div>
` : this.pastIncidents.length ?
this.pastIncidents.slice(0, 5).map(incident => this.renderIncident(incident, false)) :
html`<div class="noIncidentBox">No past incidents in the last ${this.daysToShow} days.</div>`
}
${this.pastIncidents.length > 5 && !this.loading ? html`
<div class="show-more">
<button class="show-more-button" @click=${this.handleShowMore}>
Show ${this.pastIncidents.length - 5} more incidents
</button>
</div>
` : ''}
</div>
`;
}
private renderIncident(incident: IIncidentDetails, isCurrent: boolean): TemplateResult {
const latestUpdate = incident.updates[incident.updates.length - 1];
const duration = incident.endTime ?
this.formatDuration(incident.endTime - incident.startTime) :
this.formatDuration(Date.now() - incident.startTime);
return html`
<div class="incident-card" @click=${() => this.handleIncidentClick(incident)}>
<div class="incident-header ${incident.severity}">
<div>
<h3 class="incident-title">${incident.title}</h3>
<div class="incident-meta">
<span>Started: ${this.formatDate(incident.startTime)}</span>
<span>Duration: ${duration}</span>
${incident.endTime ? html`
<span>Ended: ${this.formatDate(incident.endTime)}</span>
` : ''}
</div>
</div>
<div class="incident-status ${latestUpdate.status}">
${this.getStatusIcon(latestUpdate.status)}
${latestUpdate.status.replace(/_/g, ' ')}
</div>
</div>
<div class="incident-body">
<div class="incident-impact">
<strong>Impact:</strong> ${incident.impact}
</div>
${incident.affectedServices.length > 0 ? html`
<div class="affected-services">
<div class="affected-services-title">Affected Services:</div>
${incident.affectedServices.map(service => html`
<span class="service-tag">${service}</span>
`)}
</div>
` : ''}
${incident.updates.length > 0 ? html`
<div class="incident-updates">
<h4 style="font-size: 14px; margin: 0 0 12px 0;">Updates</h4>
${incident.updates.slice(-3).reverse().map(update => this.renderUpdate(update))}
</div>
` : ''}
${incident.rootCause && isCurrent === false ? html`
<div class="incident-impact" style="margin-top: 12px;">
<strong>Root Cause:</strong> ${incident.rootCause}
</div>
` : ''}
${incident.resolution && isCurrent === false ? html`
<div class="incident-impact" style="margin-top: 12px;">
<strong>Resolution:</strong> ${incident.resolution}
</div>
` : ''}
</div>
</div>
`;
}
private renderUpdate(update: any): TemplateResult {
return html`
<div class="update-item">
<div class="update-time">${this.formatDate(update.timestamp)}</div>
<div class="update-message">${update.message}</div>
${update.author ? html`
<div class="update-author">— ${update.author}</div>
` : ''}
</div>
`;
}
private getStatusIcon(status: string): string {
const icons: Record<string, string> = {
investigating: '🔍',
identified: '🎯',
monitoring: '👁️',
resolved: '✅',
postmortem: '📋'
};
return icons[status] || '•';
}
private formatDate(timestamp: number): string {
const date = new Date(timestamp);
const now = Date.now();
const diff = now - timestamp;
// Less than 1 hour ago
if (diff < 60 * 60 * 1000) {
const minutes = Math.floor(diff / (60 * 1000));
return `${minutes} minute${minutes !== 1 ? 's' : ''} ago`;
}
// Less than 24 hours ago
if (diff < 24 * 60 * 60 * 1000) {
const hours = Math.floor(diff / (60 * 60 * 1000));
return `${hours} hour${hours !== 1 ? 's' : ''} ago`;
}
// Less than 7 days ago
if (diff < 7 * 24 * 60 * 60 * 1000) {
const days = Math.floor(diff / (24 * 60 * 60 * 1000));
return `${days} day${days !== 1 ? 's' : ''} ago`;
}
// Default to full date
return date.toLocaleDateString('en-US', {
month: 'short',
day: 'numeric',
year: date.getFullYear() !== new Date().getFullYear() ? 'numeric' : undefined
});
}
private formatDuration(milliseconds: number): string {
const minutes = Math.floor(milliseconds / (60 * 1000));
const hours = Math.floor(minutes / 60);
const days = Math.floor(hours / 24);
if (days > 0) {
return `${days}d ${hours % 24}h`;
} else if (hours > 0) {
return `${hours}h ${minutes % 60}m`;
} else {
return `${minutes}m`;
}
}
private handleIncidentClick(incident: IIncidentDetails) {
this.dispatchEvent(new CustomEvent('incidentClick', {
detail: { incident },
bubbles: true,
composed: true
}));
}
private handleShowMore() {
// This would typically load more incidents or navigate to a full list
console.log('Show more incidents');
}
public dispatchReportNewIncident() {
this.dispatchEvent(new CustomEvent('reportNewIncident', {}));
this.dispatchEvent(new CustomEvent('reportNewIncident', {
bubbles: true,
composed: true
}));
}
public dispatchStatusSubscribe() {
this.dispatchEvent(new CustomEvent('statusSubscribe', {}));
this.dispatchEvent(new CustomEvent('statusSubscribe', {
bubbles: true,
composed: true
}));
}
}

View File

@@ -0,0 +1,393 @@
import { html } from '@design.estate/dees-element';
import type { IOverallStatus } from '../interfaces/index.js';
export const demoFunc = () => html`
<style>
.demo-container {
display: flex;
flex-direction: column;
gap: 20px;
}
.demo-section {
border: 1px solid #ddd;
border-radius: 8px;
padding: 20px;
background: #f5f5f5;
}
.demo-title {
font-size: 14px;
font-weight: 600;
margin-bottom: 16px;
color: #333;
}
.demo-controls {
display: flex;
gap: 10px;
margin-top: 16px;
flex-wrap: wrap;
}
.demo-button {
padding: 6px 12px;
border: 1px solid #ddd;
background: white;
border-radius: 4px;
cursor: pointer;
font-size: 13px;
}
.demo-button:hover {
background: #f0f0f0;
}
.status-info {
margin-top: 12px;
padding: 12px;
background: white;
border-radius: 4px;
font-size: 13px;
line-height: 1.6;
}
</style>
<div class="demo-container">
<!-- Cycling Through All States -->
<div class="demo-section">
<div class="demo-title">Automatic Status Cycling</div>
<dees-demowrapper
.runAfterRender=${async (wrapperElement: any) => {
const statusBar = wrapperElement.querySelector('upl-statuspage-statusbar') as any;
const statusStates: IOverallStatus[] = [
{
status: 'operational',
message: 'All Systems Operational',
lastUpdated: Date.now(),
affectedServices: 0,
totalServices: 12
},
{
status: 'degraded',
message: 'Minor Service Degradation',
lastUpdated: Date.now(),
affectedServices: 2,
totalServices: 12
},
{
status: 'partial_outage',
message: 'Partial System Outage',
lastUpdated: Date.now(),
affectedServices: 4,
totalServices: 12
},
{
status: 'major_outage',
message: 'Major Service Disruption',
lastUpdated: Date.now(),
affectedServices: 8,
totalServices: 12
},
{
status: 'maintenance',
message: 'Scheduled Maintenance in Progress',
lastUpdated: Date.now(),
affectedServices: 3,
totalServices: 12
}
];
let statusIndex = 0;
// Initial loading demo
statusBar.loading = true;
setTimeout(() => {
statusBar.loading = false;
statusBar.overallStatus = statusStates[0];
}, 1500);
// Cycle through states
setInterval(() => {
statusIndex = (statusIndex + 1) % statusStates.length;
statusBar.overallStatus = statusStates[statusIndex];
statusBar.overallStatus = { ...statusBar.overallStatus, lastUpdated: Date.now() };
}, 3000);
// Handle clicks
statusBar.addEventListener('statusClick', (event: CustomEvent) => {
console.log('Status bar clicked:', event.detail);
alert(`Status Details:\n\nStatus: ${event.detail.status.status}\nMessage: ${event.detail.status.message}\nAffected Services: ${event.detail.status.affectedServices}`);
});
}}
>
<upl-statuspage-statusbar></upl-statuspage-statusbar>
</dees-demowrapper>
</div>
<!-- Manual Status Control -->
<div class="demo-section">
<div class="demo-title">Manual Status Control</div>
<dees-demowrapper
.runAfterRender=${async (wrapperElement: any) => {
const statusBar = wrapperElement.querySelector('upl-statuspage-statusbar') as any;
// Initial state
statusBar.overallStatus = {
status: 'operational',
message: 'All Systems Operational',
lastUpdated: Date.now(),
affectedServices: 0,
totalServices: 15
};
// Create controls
const controls = document.createElement('div');
controls.className = 'demo-controls';
controls.innerHTML = `
<button class="demo-button" data-status="operational">Operational</button>
<button class="demo-button" data-status="degraded">Degraded</button>
<button class="demo-button" data-status="partial_outage">Partial Outage</button>
<button class="demo-button" data-status="major_outage">Major Outage</button>
<button class="demo-button" data-status="maintenance">Maintenance</button>
`;
wrapperElement.appendChild(controls);
// Status messages
const statusMessages = {
operational: 'All Systems Operational',
degraded: 'Performance Issues Detected',
partial_outage: 'Some Services Unavailable',
major_outage: 'Critical System Failure',
maintenance: 'Planned Maintenance Window'
};
const affectedCounts = {
operational: 0,
degraded: 3,
partial_outage: 7,
major_outage: 12,
maintenance: 5
};
// Handle button clicks
controls.querySelectorAll('.demo-button').forEach(button => {
button.addEventListener('click', () => {
const status = button.getAttribute('data-status') as keyof typeof statusMessages;
statusBar.overallStatus = {
status: status as any,
message: statusMessages[status],
lastUpdated: Date.now(),
affectedServices: affectedCounts[status],
totalServices: 15
};
});
});
}}
>
<upl-statuspage-statusbar></upl-statuspage-statusbar>
</dees-demowrapper>
</div>
<!-- Loading States -->
<div class="demo-section">
<div class="demo-title">Loading and Refresh States</div>
<dees-demowrapper
.runAfterRender=${async (wrapperElement: any) => {
const statusBar = wrapperElement.querySelector('upl-statuspage-statusbar') as any;
// Initial loading
statusBar.loading = true;
const controls = document.createElement('div');
controls.className = 'demo-controls';
controls.innerHTML = `
<button class="demo-button" id="toggleLoading">Toggle Loading</button>
<button class="demo-button" id="refresh">Refresh Status</button>
<button class="demo-button" id="simulateError">Simulate Error</button>
`;
wrapperElement.appendChild(controls);
// Set initial status after loading
setTimeout(() => {
statusBar.loading = false;
statusBar.overallStatus = {
status: 'operational',
message: 'All Systems Operational',
lastUpdated: Date.now(),
affectedServices: 0,
totalServices: 10
};
}, 2000);
// Toggle loading
controls.querySelector('#toggleLoading')?.addEventListener('click', () => {
statusBar.loading = !statusBar.loading;
});
// Refresh simulation
controls.querySelector('#refresh')?.addEventListener('click', () => {
statusBar.loading = true;
setTimeout(() => {
statusBar.loading = false;
// Simulate random status after refresh
const statuses = ['operational', 'degraded', 'partial_outage'];
const randomStatus = statuses[Math.floor(Math.random() * statuses.length)];
statusBar.overallStatus = {
status: randomStatus as any,
message: 'Status refreshed at ' + new Date().toLocaleTimeString(),
lastUpdated: Date.now(),
affectedServices: randomStatus === 'operational' ? 0 : Math.floor(Math.random() * 5) + 1,
totalServices: 10
};
}, 1000);
});
// Error simulation
controls.querySelector('#simulateError')?.addEventListener('click', () => {
statusBar.loading = true;
setTimeout(() => {
statusBar.loading = false;
statusBar.overallStatus = {
status: 'major_outage',
message: 'Unable to fetch status - Connection Error',
lastUpdated: Date.now(),
affectedServices: -1, // Unknown
totalServices: -1
};
}, 1500);
});
}}
>
<upl-statuspage-statusbar></upl-statuspage-statusbar>
</dees-demowrapper>
</div>
<!-- Edge Cases -->
<div class="demo-section">
<div class="demo-title">Edge Cases and Special States</div>
<dees-demowrapper
.runAfterRender=${async (wrapperElement: any) => {
const statusBar = wrapperElement.querySelector('upl-statuspage-statusbar') as any;
const edgeCases = [
{
label: 'No Services',
status: {
status: 'operational',
message: 'No services to monitor',
lastUpdated: Date.now(),
affectedServices: 0,
totalServices: 0
}
},
{
label: 'All Services Down',
status: {
status: 'major_outage',
message: 'Complete System Failure',
lastUpdated: Date.now(),
affectedServices: 25,
totalServices: 25
}
},
{
label: 'Very Long Message',
status: {
status: 'degraded',
message: 'Multiple services experiencing degraded performance due to increased load from seasonal traffic surge',
lastUpdated: Date.now(),
affectedServices: 7,
totalServices: 20
}
},
{
label: 'Old Timestamp',
status: {
status: 'operational',
message: 'Status data may be stale',
lastUpdated: Date.now() - 24 * 60 * 60 * 1000, // 24 hours ago
affectedServices: 0,
totalServices: 10
}
},
{
label: 'Future Maintenance',
status: {
status: 'maintenance',
message: 'Scheduled maintenance starting in 2 hours',
lastUpdated: Date.now(),
affectedServices: 0,
totalServices: 15
}
}
];
let currentCase = 0;
statusBar.overallStatus = edgeCases[0].status;
// Create info display
const info = document.createElement('div');
info.className = 'status-info';
info.innerHTML = `<strong>Current Case:</strong> ${edgeCases[0].label}`;
wrapperElement.appendChild(info);
// Create controls
const controls = document.createElement('div');
controls.className = 'demo-controls';
controls.innerHTML = `
<button class="demo-button" id="prevCase"> Previous Case</button>
<button class="demo-button" id="nextCase">Next Case </button>
`;
wrapperElement.appendChild(controls);
const updateCase = (index: number) => {
currentCase = index;
statusBar.overallStatus = edgeCases[currentCase].status;
info.innerHTML = `<strong>Current Case:</strong> ${edgeCases[currentCase].label}`;
};
controls.querySelector('#prevCase')?.addEventListener('click', () => {
const newIndex = (currentCase - 1 + edgeCases.length) % edgeCases.length;
updateCase(newIndex);
});
controls.querySelector('#nextCase')?.addEventListener('click', () => {
const newIndex = (currentCase + 1) % edgeCases.length;
updateCase(newIndex);
});
}}
>
<upl-statuspage-statusbar></upl-statuspage-statusbar>
</dees-demowrapper>
</div>
<!-- Non-Expandable Status Bar -->
<div class="demo-section">
<div class="demo-title">Non-Expandable Status Bar</div>
<dees-demowrapper
.runAfterRender=${async (wrapperElement: any) => {
const statusBar = wrapperElement.querySelector('upl-statuspage-statusbar') as any;
// Disable expandable behavior
statusBar.expandable = false;
statusBar.overallStatus = {
status: 'operational',
message: 'This status bar cannot be clicked',
lastUpdated: Date.now(),
affectedServices: 0,
totalServices: 8
};
// This event won't fire since expandable is false
statusBar.addEventListener('statusClick', (event: CustomEvent) => {
console.log('This should not fire');
});
const info = document.createElement('div');
info.className = 'status-info';
info.innerHTML = 'Try clicking the status bar - it should not respond to clicks when expandable=false';
wrapperElement.appendChild(info);
}}
>
<upl-statuspage-statusbar></upl-statuspage-statusbar>
</dees-demowrapper>
</div>
</div>
`;

View File

@@ -1,5 +1,8 @@
import { DeesElement, property, html, customElement, type TemplateResult, cssManager, css } from '@design.estate/dees-element';
import { DeesElement, property, html, customElement, type TemplateResult, cssManager, css, unsafeCSS } from '@design.estate/dees-element';
import * as domtools from '@design.estate/dees-domtools';
import type { IOverallStatus } from '../interfaces/index.js';
import { fonts } from '../styles/shared.styles.js';
import { demoFunc } from './upl-statuspage-statusbar.demo.js';
declare global {
interface HTMLElementTagNameMap {
@@ -9,9 +12,22 @@ declare global {
@customElement('upl-statuspage-statusbar')
export class UplStatuspageStatusbar extends DeesElement {
public static demo = () => html`
<upl-statuspage-statusbar></upl-statuspage-statusbar>
`;
public static demo = demoFunc;
@property({ type: Object })
public overallStatus: IOverallStatus = {
status: 'operational',
message: 'All Systems Operational',
lastUpdated: Date.now(),
affectedServices: 0,
totalServices: 0
};
@property({ type: Boolean })
public loading: boolean = false;
@property({ type: Boolean })
public expandable: boolean = true;
constructor() {
super();
@@ -21,30 +37,200 @@ export class UplStatuspageStatusbar extends DeesElement {
cssManager.defaultStyles,
css`
:host {
padding: 20px 0px 15px 0px;
padding: 0;
display: block;
background: ${cssManager.bdTheme('#eeeeeb', '#222222')};;
font-family: Inter;
color: #fff;
background: transparent;
font-family: ${unsafeCSS(fonts.base)};
}
.mainbox {
.statusbar-container {
margin: auto;
max-width: 900px;
text-align: center;
background: #19572E;
line-height: 50px;
border-radius: 3px;
max-width: 1200px;
padding: 0 24px 24px 24px;
position: relative;
}
.statusbar-inner {
display: flex;
align-items: center;
justify-content: center;
min-height: 64px;
padding: 16px 24px;
border-radius: 8px;
cursor: pointer;
transition: all 0.2s ease;
position: relative;
overflow: hidden;
font-weight: 500;
font-size: 16px;
border: 1px solid transparent;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1), 0 1px 2px rgba(0, 0, 0, 0.06);
}
.statusbar-inner:hover {
transform: translateY(-1px);
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1), 0 2px 4px rgba(0, 0, 0, 0.06);
}
.statusbar-inner:active {
transform: translateY(0);
}
.statusbar-inner.operational {
background: ${cssManager.bdTheme('#10b981', '#064e3b')};
border-color: ${cssManager.bdTheme('#10b981', '#064e3b')};
color: white;
}
.statusbar-inner.degraded {
background: ${cssManager.bdTheme('#f59e0b', '#78350f')};
border-color: ${cssManager.bdTheme('#f59e0b', '#78350f')};
color: white;
}
.statusbar-inner.partial_outage {
background: ${cssManager.bdTheme('#ef4444', '#7f1d1d')};
border-color: ${cssManager.bdTheme('#ef4444', '#7f1d1d')};
color: white;
}
.statusbar-inner.major_outage {
background: ${cssManager.bdTheme('#dc2626', '#450a0a')};
border-color: ${cssManager.bdTheme('#dc2626', '#450a0a')};
color: white;
}
.statusbar-inner.maintenance {
background: ${cssManager.bdTheme('#3b82f6', '#1e3a8a')};
border-color: ${cssManager.bdTheme('#3b82f6', '#1e3a8a')};
color: white;
}
.status-content {
display: flex;
flex-direction: column;
align-items: center;
gap: 4px;
}
.status-main {
display: flex;
align-items: center;
gap: 8px;
}
.status-icon {
display: inline-flex;
align-items: center;
justify-content: center;
width: 20px;
height: 20px;
font-size: 14px;
}
.loading-skeleton {
background: ${cssManager.bdTheme(
'linear-gradient(90deg, #f3f4f6 25%, #e5e7eb 50%, #f3f4f6 75%)',
'linear-gradient(90deg, #1f1f1f 25%, #262626 50%, #1f1f1f 75%)'
)};
background-size: 200% 100%;
animation: loading 1.5s infinite;
height: 64px;
border-radius: 8px;
}
@keyframes loading {
0% {
background-position: 200% 0;
}
100% {
background-position: -200% 0;
}
}
.status-details {
font-size: 14px;
opacity: 0.9;
}
.last-updated {
font-size: 13px;
text-align: right;
margin-top: 12px;
color: ${cssManager.bdTheme('#6b7280', '#9ca3af')};
}
@media (max-width: 640px) {
.statusbar-inner {
font-size: 14px;
padding: 12px 16px;
min-height: 56px;
}
.status-icon {
width: 18px;
height: 18px;
font-size: 12px;
}
}
`,
]
public render(): TemplateResult {
const getStatusIcon = () => {
switch (this.overallStatus.status) {
case 'operational':
return '✓';
case 'degraded':
return '!';
case 'partial_outage':
return '⚠';
case 'major_outage':
return '✕';
case 'maintenance':
return '🔧';
default:
return '';
}
};
const formatLastUpdated = () => {
const date = new Date(this.overallStatus.lastUpdated);
return date.toLocaleString();
};
const handleClick = () => {
if (this.expandable) {
this.dispatchEvent(new CustomEvent('statusClick', {
detail: { status: this.overallStatus },
bubbles: true,
composed: true
}));
}
};
return html`
<style>
</style>
<div class="mainbox">
Everything is working normally!
<div class="statusbar-container">
${this.loading ? html`
<div class="loading-skeleton"></div>
` : html`
<div class="statusbar-inner ${this.overallStatus.status}" @click=${handleClick}>
<div class="status-content">
<div class="status-main">
<span class="status-icon">${getStatusIcon()}</span>
<span>${this.overallStatus.message}</span>
</div>
${this.overallStatus.affectedServices > 0 ? html`
<div class="status-details">
${this.overallStatus.affectedServices} of ${this.overallStatus.totalServices} services affected
</div>
` : ''}
</div>
</div>
<div class="last-updated">
Last updated: ${formatLastUpdated()}
</div>
`}
</div>
`;
}

View File

@@ -0,0 +1,754 @@
import { html } from '@design.estate/dees-element';
import type { IStatusHistoryPoint } from '../interfaces/index.js';
export const demoFunc = () => html`
<style>
.demo-container {
display: flex;
flex-direction: column;
gap: 20px;
}
.demo-section {
border: 1px solid #ddd;
border-radius: 8px;
padding: 20px;
background: #f5f5f5;
}
.demo-title {
font-size: 14px;
font-weight: 600;
margin-bottom: 16px;
color: #333;
}
.demo-controls {
display: flex;
gap: 10px;
margin-top: 16px;
flex-wrap: wrap;
}
.demo-button {
padding: 6px 12px;
border: 1px solid #ddd;
background: white;
border-radius: 4px;
cursor: pointer;
font-size: 13px;
}
.demo-button:hover {
background: #f0f0f0;
}
.demo-button.active {
background: #2196F3;
color: white;
border-color: #2196F3;
}
.demo-info {
margin-top: 12px;
padding: 12px;
background: white;
border-radius: 4px;
font-size: 13px;
}
.stats-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
gap: 12px;
margin-top: 12px;
}
.stat-box {
background: white;
padding: 12px;
border-radius: 4px;
text-align: center;
}
.stat-value {
font-size: 20px;
font-weight: 600;
color: #2196F3;
}
.stat-label {
font-size: 12px;
color: #666;
margin-top: 4px;
}
</style>
<div class="demo-container">
<!-- Time Range Demo -->
<div class="demo-section">
<div class="demo-title">Different Time Ranges</div>
<dees-demowrapper
.runAfterRender=${async (wrapperElement: any) => {
const statusDetails = wrapperElement.querySelector('upl-statuspage-statusdetails') as any;
// Generate data for different time ranges
const generateDataForRange = (hours: number, pattern: 'stable' | 'degrading' | 'improving' | 'volatile' = 'stable'): IStatusHistoryPoint[] => {
const now = Date.now();
const data: IStatusHistoryPoint[] = [];
// For proper display, we need hourly data points that align with actual hours
for (let i = hours - 1; i >= 0; i--) {
// Create timestamp at the start of each hour
const date = new Date();
date.setMinutes(0, 0, 0);
date.setHours(date.getHours() - i);
const timestamp = date.getTime();
let status: IStatusHistoryPoint['status'] = 'operational';
let responseTime = 50 + Math.random() * 50;
let errorRate = 0;
switch (pattern) {
case 'degrading':
// Getting worse over time
const degradation = (hours - i) / hours;
if (degradation > 0.7) {
status = 'major_outage';
responseTime = 800 + Math.random() * 200;
errorRate = 0.3 + Math.random() * 0.2;
} else if (degradation > 0.5) {
status = 'partial_outage';
responseTime = 500 + Math.random() * 200;
errorRate = 0.1 + Math.random() * 0.1;
} else if (degradation > 0.3) {
status = 'degraded';
responseTime = 200 + Math.random() * 100;
errorRate = 0.02 + Math.random() * 0.03;
}
break;
case 'improving':
// Getting better over time
const improvement = i / hours;
if (improvement < 0.3) {
status = 'major_outage';
responseTime = 800 + Math.random() * 200;
errorRate = 0.3 + Math.random() * 0.2;
} else if (improvement < 0.5) {
status = 'partial_outage';
responseTime = 500 + Math.random() * 200;
errorRate = 0.1 + Math.random() * 0.1;
} else if (improvement < 0.7) {
status = 'degraded';
responseTime = 200 + Math.random() * 100;
errorRate = 0.02 + Math.random() * 0.03;
}
break;
case 'volatile':
// Random ups and downs
const rand = Math.random();
if (rand < 0.05) {
status = 'major_outage';
responseTime = 800 + Math.random() * 200;
errorRate = 0.3 + Math.random() * 0.2;
} else if (rand < 0.1) {
status = 'partial_outage';
responseTime = 500 + Math.random() * 200;
errorRate = 0.1 + Math.random() * 0.1;
} else if (rand < 0.2) {
status = 'degraded';
responseTime = 200 + Math.random() * 100;
errorRate = 0.02 + Math.random() * 0.03;
} else if (rand < 0.25) {
status = 'maintenance';
responseTime = 100 + Math.random() * 50;
errorRate = 0;
}
break;
default:
// Stable with occasional hiccups
if (Math.random() < 0.02) {
status = 'degraded';
responseTime = 200 + Math.random() * 100;
errorRate = 0.01 + Math.random() * 0.02;
}
}
data.push({
timestamp,
status,
responseTime,
errorRate
});
}
return data;
};
// Initial setup
statusDetails.serviceId = 'api-gateway';
statusDetails.serviceName = 'API Gateway';
statusDetails.historyData = generateDataForRange(24);
// Create controls
const controls = document.createElement('div');
controls.className = 'demo-controls';
const timeRanges = [
{ hours: 24, label: '24 Hours' },
{ hours: 168, label: '7 Days' },
{ hours: 720, label: '30 Days' },
{ hours: 2160, label: '90 Days' }
];
timeRanges.forEach((range, index) => {
const button = document.createElement('button');
button.className = 'demo-button' + (index === 0 ? ' active' : '');
button.textContent = range.label;
button.onclick = () => {
// Update active button
controls.querySelectorAll('.demo-button').forEach(btn => btn.classList.remove('active'));
button.classList.add('active');
// Load new data with loading state
statusDetails.loading = true;
setTimeout(() => {
statusDetails.historyData = generateDataForRange(range.hours, 'volatile');
statusDetails.loading = false;
updateStats();
}, 500);
};
controls.appendChild(button);
});
wrapperElement.appendChild(controls);
// Add statistics display
const statsDiv = document.createElement('div');
statsDiv.className = 'stats-grid';
wrapperElement.appendChild(statsDiv);
const updateStats = () => {
const data = statusDetails.historyData || [];
const operational = data.filter(d => d.status === 'operational').length;
const avgResponseTime = data.reduce((sum, d) => sum + (d.responseTime || 0), 0) / data.length;
const uptime = (operational / data.length) * 100;
const incidents = data.filter(d => d.status !== 'operational' && d.status !== 'maintenance').length;
statsDiv.innerHTML = `
<div class="stat-box">
<div class="stat-value">${uptime.toFixed(2)}%</div>
<div class="stat-label">Uptime</div>
</div>
<div class="stat-box">
<div class="stat-value">${avgResponseTime.toFixed(0)}ms</div>
<div class="stat-label">Avg Response Time</div>
</div>
<div class="stat-box">
<div class="stat-value">${incidents}</div>
<div class="stat-label">Incidents</div>
</div>
<div class="stat-box">
<div class="stat-value">${data.length}</div>
<div class="stat-label">Data Points</div>
</div>
`;
};
updateStats();
// Handle bar clicks
statusDetails.addEventListener('barClick', (event: CustomEvent) => {
const { timestamp, status, responseTime, errorRate } = event.detail;
const date = new Date(timestamp);
alert(`Details for ${date.toLocaleString()}:\n\nStatus: ${status}\nResponse Time: ${responseTime.toFixed(0)}ms\nError Rate: ${(errorRate * 100).toFixed(2)}%`);
});
}}
>
<upl-statuspage-statusdetails></upl-statuspage-statusdetails>
</dees-demowrapper>
</div>
<!-- Data Pattern Scenarios -->
<div class="demo-section">
<div class="demo-title">Different Data Patterns</div>
<dees-demowrapper
.runAfterRender=${async (wrapperElement: any) => {
const statusDetails = wrapperElement.querySelector('upl-statuspage-statusdetails') as any;
// Pattern generators
const patterns = {
stable: () => {
const data: IStatusHistoryPoint[] = [];
for (let i = 47; i >= 0; i--) {
const date = new Date();
date.setMinutes(0, 0, 0);
date.setHours(date.getHours() - i);
data.push({
timestamp: date.getTime(),
status: 'operational',
responseTime: 40 + Math.random() * 20,
errorRate: 0
});
}
return data;
},
degrading: () => {
const now = Date.now();
const data: IStatusHistoryPoint[] = [];
for (let i = 47; i >= 0; i--) {
const degradation = (47 - i) / 47;
let status: IStatusHistoryPoint['status'] = 'operational';
let responseTime = 50;
let errorRate = 0;
if (degradation > 0.8) {
status = 'major_outage';
responseTime = 800 + Math.random() * 200;
errorRate = 0.4;
} else if (degradation > 0.6) {
status = 'partial_outage';
responseTime = 500 + Math.random() * 100;
errorRate = 0.2;
} else if (degradation > 0.4) {
status = 'degraded';
responseTime = 200 + Math.random() * 100;
errorRate = 0.05;
} else {
responseTime = 50 + degradation * 100;
}
data.push({
timestamp: now - (i * 60 * 60 * 1000),
status,
responseTime,
errorRate
});
}
return data;
},
recovering: () => {
const now = Date.now();
const data: IStatusHistoryPoint[] = [];
for (let i = 47; i >= 0; i--) {
const recovery = i / 47;
let status: IStatusHistoryPoint['status'] = 'operational';
let responseTime = 50;
let errorRate = 0;
if (recovery < 0.2) {
status = 'operational';
responseTime = 50 + Math.random() * 20;
} else if (recovery < 0.4) {
status = 'degraded';
responseTime = 150 + Math.random() * 50;
errorRate = 0.02;
} else if (recovery < 0.7) {
status = 'partial_outage';
responseTime = 400 + Math.random() * 100;
errorRate = 0.15;
} else {
status = 'major_outage';
responseTime = 800 + Math.random() * 200;
errorRate = 0.35;
}
data.push({
timestamp: now - (i * 60 * 60 * 1000),
status,
responseTime,
errorRate
});
}
return data;
},
periodic: () => {
const now = Date.now();
const data: IStatusHistoryPoint[] = [];
for (let i = 47; i >= 0; i--) {
// Issues every 12 hours
const hourOfDay = i % 24;
let status: IStatusHistoryPoint['status'] = 'operational';
let responseTime = 50 + Math.random() * 30;
let errorRate = 0;
if (hourOfDay >= 9 && hourOfDay <= 11) {
// Morning peak
status = 'degraded';
responseTime = 200 + Math.random() * 100;
errorRate = 0.05;
} else if (hourOfDay >= 18 && hourOfDay <= 20) {
// Evening peak
status = 'degraded';
responseTime = 250 + Math.random() * 150;
errorRate = 0.08;
}
data.push({
timestamp: now - (i * 60 * 60 * 1000),
status,
responseTime,
errorRate
});
}
return data;
},
maintenance: () => {
const now = Date.now();
const data: IStatusHistoryPoint[] = [];
for (let i = 47; i >= 0; i--) {
let status: IStatusHistoryPoint['status'] = 'operational';
let responseTime = 50 + Math.random() * 30;
let errorRate = 0;
// Maintenance window from hour 20-24
if (i >= 20 && i <= 24) {
status = 'maintenance';
responseTime = 0;
errorRate = 0;
}
data.push({
timestamp: now - (i * 60 * 60 * 1000),
status,
responseTime,
errorRate
});
}
return data;
}
};
// Initial setup
statusDetails.serviceId = 'web-server';
statusDetails.serviceName = 'Web Server';
statusDetails.historyData = patterns.stable();
// Create controls
const controls = document.createElement('div');
controls.className = 'demo-controls';
Object.entries(patterns).forEach(([name, generator]) => {
const button = document.createElement('button');
button.className = 'demo-button' + (name === 'stable' ? ' active' : '');
button.textContent = name.charAt(0).toUpperCase() + name.slice(1);
button.onclick = () => {
controls.querySelectorAll('.demo-button').forEach(btn => btn.classList.remove('active'));
button.classList.add('active');
statusDetails.loading = true;
setTimeout(() => {
statusDetails.historyData = generator();
statusDetails.loading = false;
updateInfo(name);
}, 300);
};
controls.appendChild(button);
});
wrapperElement.appendChild(controls);
// Add info display
const info = document.createElement('div');
info.className = 'demo-info';
wrapperElement.appendChild(info);
const updateInfo = (pattern: string) => {
const descriptions = {
stable: 'Service running smoothly with consistent performance',
degrading: 'Service health deteriorating over time',
recovering: 'Service recovering from a major outage',
periodic: 'Regular performance issues during peak hours (9-11 AM and 6-8 PM)',
maintenance: 'Scheduled maintenance window (hours 20-24)'
};
info.innerHTML = `<strong>Pattern:</strong> ${descriptions[pattern as keyof typeof descriptions] || pattern}`;
};
updateInfo('stable');
}}
>
<upl-statuspage-statusdetails></upl-statuspage-statusdetails>
</dees-demowrapper>
</div>
<!-- Interactive Real-time Updates -->
<div class="demo-section">
<div class="demo-title">Real-time Updates with Manual Control</div>
<dees-demowrapper
.runAfterRender=${async (wrapperElement: any) => {
const statusDetails = wrapperElement.querySelector('upl-statuspage-statusdetails') as any;
// Initialize with recent data
const now = Date.now();
const initialData: IStatusHistoryPoint[] = [];
for (let i = 23; i >= 0; i--) {
initialData.push({
timestamp: now - (i * 60 * 60 * 1000),
status: 'operational',
responseTime: 50 + Math.random() * 30,
errorRate: 0
});
}
statusDetails.serviceId = 'real-time-api';
statusDetails.serviceName = 'Real-time API';
statusDetails.historyData = initialData;
statusDetails.timeRange = '24h';
// Create controls
const controls = document.createElement('div');
controls.className = 'demo-controls';
controls.innerHTML = `
<button class="demo-button" id="addHealthy">Add Healthy Point</button>
<button class="demo-button" id="addDegraded">Add Degraded Point</button>
<button class="demo-button" id="addOutage">Add Outage Point</button>
<button class="demo-button" id="simulateSpike">Simulate Traffic Spike</button>
<button class="demo-button" id="clearData">Clear All Data</button>
`;
wrapperElement.appendChild(controls);
const addDataPoint = (status: IStatusHistoryPoint['status'], responseTime: number, errorRate: number = 0) => {
const data = [...(statusDetails.historyData || [])];
if (data.length >= 24) {
data.shift(); // Keep only 24 points
}
data.push({
timestamp: Date.now(),
status,
responseTime,
errorRate
});
statusDetails.historyData = data;
};
controls.querySelector('#addHealthy')?.addEventListener('click', () => {
addDataPoint('operational', 50 + Math.random() * 30);
});
controls.querySelector('#addDegraded')?.addEventListener('click', () => {
addDataPoint('degraded', 200 + Math.random() * 100, 0.05);
});
controls.querySelector('#addOutage')?.addEventListener('click', () => {
addDataPoint('major_outage', 800 + Math.random() * 200, 0.5);
});
controls.querySelector('#simulateSpike')?.addEventListener('click', () => {
// Add several degraded points
for (let i = 0; i < 3; i++) {
setTimeout(() => {
addDataPoint('degraded', 300 + Math.random() * 200, 0.1 + Math.random() * 0.1);
}, i * 1000);
}
});
controls.querySelector('#clearData')?.addEventListener('click', () => {
statusDetails.historyData = [];
});
// Auto-update every 5 seconds
let autoUpdate = setInterval(() => {
const rand = Math.random();
if (rand < 0.8) {
addDataPoint('operational', 40 + Math.random() * 40);
} else if (rand < 0.95) {
addDataPoint('degraded', 150 + Math.random() * 100, 0.02);
} else {
addDataPoint('partial_outage', 400 + Math.random() * 200, 0.15);
}
}, 5000);
// Add toggle for auto-updates
const autoToggle = document.createElement('button');
autoToggle.className = 'demo-button active';
autoToggle.textContent = 'Auto-update: ON';
autoToggle.style.marginLeft = '10px';
autoToggle.onclick = () => {
if (autoUpdate) {
clearInterval(autoUpdate);
autoUpdate = null;
autoToggle.textContent = 'Auto-update: OFF';
autoToggle.classList.remove('active');
} else {
autoUpdate = setInterval(() => {
const rand = Math.random();
if (rand < 0.8) {
addDataPoint('operational', 40 + Math.random() * 40);
} else if (rand < 0.95) {
addDataPoint('degraded', 150 + Math.random() * 100, 0.02);
} else {
addDataPoint('partial_outage', 400 + Math.random() * 200, 0.15);
}
}, 5000);
autoToggle.textContent = 'Auto-update: ON';
autoToggle.classList.add('active');
}
};
controls.appendChild(autoToggle);
// Cleanup on unmount
wrapperElement.addEventListener('remove', () => {
if (autoUpdate) clearInterval(autoUpdate);
});
}}
>
<upl-statuspage-statusdetails></upl-statuspage-statusdetails>
</dees-demowrapper>
</div>
<!-- Edge Cases -->
<div class="demo-section">
<div class="demo-title">Edge Cases and Special Scenarios</div>
<dees-demowrapper
.runAfterRender=${async (wrapperElement: any) => {
const statusDetails = wrapperElement.querySelector('upl-statuspage-statusdetails') as any;
const scenarios = {
noData: {
name: 'No Data Available',
data: []
},
singlePoint: {
name: 'Single Data Point',
data: [{
timestamp: Date.now(),
status: 'operational' as const,
responseTime: 75,
errorRate: 0
}]
},
allDown: {
name: 'Complete Outage',
data: Array.from({ length: 48 }, (_, i) => ({
timestamp: Date.now() - (i * 60 * 60 * 1000),
status: 'major_outage' as const,
responseTime: 0,
errorRate: 1
}))
},
highLatency: {
name: 'High Latency Issues',
data: Array.from({ length: 48 }, (_, i) => ({
timestamp: Date.now() - (i * 60 * 60 * 1000),
status: 'operational' as const,
responseTime: 2000 + Math.random() * 1000,
errorRate: 0
}))
},
mixedStatuses: {
name: 'All Status Types',
data: Array.from({ length: 50 }, (_, i) => {
const statuses: IStatusHistoryPoint['status'][] = ['operational', 'degraded', 'partial_outage', 'major_outage', 'maintenance'];
const status = statuses[i % statuses.length];
return {
timestamp: Date.now() - (i * 60 * 60 * 1000),
status,
responseTime: status === 'operational' ? 50 : status === 'maintenance' ? 0 : 200 + Math.random() * 600,
errorRate: status === 'operational' || status === 'maintenance' ? 0 : 0.1 + Math.random() * 0.4
};
})
}
};
// Initial scenario
let currentScenario = 'noData';
statusDetails.serviceId = 'edge-case-service';
statusDetails.serviceName = 'Edge Case Service';
statusDetails.historyData = scenarios[currentScenario].data;
// Create scenario buttons
const controls = document.createElement('div');
controls.className = 'demo-controls';
Object.entries(scenarios).forEach(([key, scenario]) => {
const button = document.createElement('button');
button.className = 'demo-button' + (key === currentScenario ? ' active' : '');
button.textContent = scenario.name;
button.onclick = () => {
controls.querySelectorAll('.demo-button').forEach(btn => btn.classList.remove('active'));
button.classList.add('active');
currentScenario = key;
statusDetails.loading = true;
setTimeout(() => {
statusDetails.historyData = scenario.data;
statusDetails.loading = false;
}, 300);
};
controls.appendChild(button);
});
wrapperElement.appendChild(controls);
}}
>
<upl-statuspage-statusdetails></upl-statuspage-statusdetails>
</dees-demowrapper>
</div>
<!-- Loading and Error States -->
<div class="demo-section">
<div class="demo-title">Loading and Error Handling</div>
<dees-demowrapper
.runAfterRender=${async (wrapperElement: any) => {
const statusDetails = wrapperElement.querySelector('upl-statuspage-statusdetails') as any;
// Start with loading
statusDetails.loading = true;
statusDetails.serviceId = 'loading-demo';
statusDetails.serviceName = 'Loading Demo Service';
const controls = document.createElement('div');
controls.className = 'demo-controls';
controls.innerHTML = `
<button class="demo-button" id="toggleLoading">Toggle Loading</button>
<button class="demo-button" id="loadSuccess">Load Successfully</button>
<button class="demo-button" id="loadError">Simulate Error</button>
<button class="demo-button" id="loadSlowly">Load Slowly (3s)</button>
`;
wrapperElement.appendChild(controls);
controls.querySelector('#toggleLoading')?.addEventListener('click', () => {
statusDetails.loading = !statusDetails.loading;
});
controls.querySelector('#loadSuccess')?.addEventListener('click', () => {
statusDetails.loading = true;
setTimeout(() => {
const now = Date.now();
statusDetails.historyData = Array.from({ length: 24 }, (_, i) => ({
timestamp: now - (i * 60 * 60 * 1000),
status: Math.random() > 0.9 ? 'degraded' : 'operational',
responseTime: 50 + Math.random() * 50,
errorRate: 0
}));
statusDetails.loading = false;
}, 500);
});
controls.querySelector('#loadError')?.addEventListener('click', () => {
statusDetails.loading = true;
setTimeout(() => {
statusDetails.loading = false;
statusDetails.historyData = [];
statusDetails.errorMessage = 'Failed to load status data: Connection timeout';
}, 1500);
});
controls.querySelector('#loadSlowly')?.addEventListener('click', () => {
statusDetails.loading = true;
setTimeout(() => {
const now = Date.now();
statusDetails.historyData = Array.from({ length: 48 }, (_, i) => ({
timestamp: now - (i * 60 * 60 * 1000),
status: 'operational',
responseTime: 45 + Math.random() * 30,
errorRate: 0
}));
statusDetails.loading = false;
}, 3000);
});
}}
>
<upl-statuspage-statusdetails></upl-statuspage-statusdetails>
</dees-demowrapper>
</div>
</div>
`;

View File

@@ -7,9 +7,13 @@ import {
type TemplateResult,
css,
cssManager,
unsafeCSS,
} from '@design.estate/dees-element';
import type { IStatusHistoryPoint } from '../interfaces/index.js';
import { fonts, colors, shadows, borderRadius, spacing, commonStyles, getStatusColor } from '../styles/shared.styles.js';
import './internal/uplinternal-miniheading.js';
import { demoFunc } from './upl-statuspage-statusdetails.demo.js';
declare global {
interface HTMLElementTagNameMap {
@@ -19,7 +23,22 @@ declare global {
@customElement('upl-statuspage-statusdetails')
export class UplStatuspageStatusdetails extends DeesElement {
public static demo = () => html` <upl-statuspage-statusdetails></upl-statuspage-statusdetails> `;
public static demo = demoFunc;
@property({ type: Array })
public historyData: IStatusHistoryPoint[] = [];
@property({ type: String })
public serviceId: string = '';
@property({ type: String })
public serviceName: string = 'Service';
@property({ type: Boolean })
public loading: boolean = false;
@property({ type: Number })
public hoursToShow: number = 48;
constructor() {
super();
@@ -27,69 +46,342 @@ export class UplStatuspageStatusdetails extends DeesElement {
public static styles = [
plugins.domtools.elementBasic.staticStyles,
commonStyles,
css`
:host {
position: relative;
padding: 0px 0px 15px 0px;
display: block;
background: ${cssManager.bdTheme('#eeeeeb', '#222222')};;
font-family: Inter;
color: #fff;
background: transparent;
font-family: ${unsafeCSS(fonts.base)};
color: ${colors.text.primary};
}
.container {
max-width: 1200px;
margin: 0 auto;
padding: 0 ${unsafeCSS(spacing.lg)} ${unsafeCSS(spacing.lg)} ${unsafeCSS(spacing.lg)};
}
.mainbox {
margin: auto;
max-width: 900px;
text-align: right;
background: ${cssManager.bdTheme('#ffffff', '#333333')};;
line-height: 50px;
border-radius: 3px;
background: ${colors.background.card};
border: 1px solid ${colors.border.default};
border-radius: ${unsafeCSS(borderRadius.md)};
padding: ${unsafeCSS(spacing.md)};
box-shadow: ${unsafeCSS(shadows.sm)};
}
.mainbox .barContainer {
position: relative;
display: flex;
padding: 6px;
gap: 2px;
padding: ${unsafeCSS(spacing.sm)};
background: ${colors.background.secondary};
border-radius: ${unsafeCSS(borderRadius.base)};
overflow: hidden;
}
.mainbox .barContainer .bar {
margin: 4px;
width: 11px;
border-radius: 3px;
height: 40px;
background: #2deb51;
flex: 1;
height: 48px;
border-radius: ${unsafeCSS(borderRadius.sm)};
cursor: pointer;
transition: all 0.2s ease;
position: relative;
opacity: 0.9;
}
.mainbox .barContainer .bar:hover {
opacity: 1;
transform: scaleY(1.05);
}
.mainbox .barContainer .bar.operational {
background: ${colors.status.operational};
}
.mainbox .barContainer .bar.degraded {
background: ${colors.status.degraded};
}
.mainbox .barContainer .bar.partial_outage {
background: ${colors.status.partial};
}
.mainbox .barContainer .bar.major_outage {
background: ${colors.status.major};
}
.mainbox .barContainer .bar.maintenance {
background: ${colors.status.maintenance};
}
.mainbox .barContainer .bar.no-data {
background: ${colors.border.muted};
opacity: 0.3;
}
.timeIndicator {
position: absolute;
width: 11px;
height: 11px;
background: #FF9800;
top: 56px;
left: 400px;
transform: rotate(45deg);
width: 2px;
height: calc(100% - ${unsafeCSS(spacing.md)});
background: ${colors.text.primary};
top: ${unsafeCSS(spacing.sm)};
transition: left 0.3s;
opacity: 0.5;
pointer-events: none;
}
.time-labels {
display: flex;
justify-content: space-between;
padding: ${unsafeCSS(spacing.sm)} ${unsafeCSS(spacing.sm)} 0;
font-size: 12px;
color: ${colors.text.secondary};
font-family: ${unsafeCSS(fonts.mono)};
}
.tooltip {
position: absolute;
background: ${cssManager.bdTheme('#1a1a1a', '#ffffff')};
color: ${cssManager.bdTheme('#ffffff', '#1a1a1a')};
padding: ${unsafeCSS(spacing.sm)} ${unsafeCSS(spacing.md)};
border-radius: ${unsafeCSS(borderRadius.base)};
font-size: 13px;
pointer-events: none;
opacity: 0;
transition: opacity 0.2s;
z-index: 10;
white-space: nowrap;
box-shadow: ${unsafeCSS(shadows.lg)};
border: 1px solid ${colors.border.default};
}
.tooltip.visible {
opacity: 0.95;
}
.tooltip strong {
font-weight: 600;
display: block;
margin-bottom: ${unsafeCSS(spacing.xs)};
color: ${cssManager.bdTheme('#ffffff', '#1a1a1a')};
}
.tooltip div {
line-height: 1.4;
}
.loading-skeleton {
display: flex;
padding: ${unsafeCSS(spacing.sm)};
gap: 2px;
background: ${colors.background.secondary};
border-radius: ${unsafeCSS(borderRadius.base)};
}
.loading-skeleton .skeleton-bar {
flex: 1;
height: 48px;
background: ${cssManager.bdTheme(
'linear-gradient(90deg, #f3f4f6 25%, #e5e7eb 50%, #f3f4f6 75%)',
'linear-gradient(90deg, #1f1f1f 25%, #262626 50%, #1f1f1f 75%)'
)};
background-size: 200% 100%;
animation: loading 1.5s infinite;
border-radius: ${unsafeCSS(borderRadius.sm)};
}
@keyframes loading {
0% { background-position: 200% 0; }
100% { background-position: -200% 0; }
}
.status-legend {
display: flex;
gap: ${unsafeCSS(spacing.lg)};
justify-content: center;
margin-top: ${unsafeCSS(spacing.lg)};
font-size: 13px;
flex-wrap: wrap;
}
.legend-item {
display: flex;
align-items: center;
gap: ${unsafeCSS(spacing.xs)};
}
.legend-color {
width: 12px;
height: 12px;
border-radius: ${unsafeCSS(borderRadius.sm)};
}
@media (max-width: 640px) {
.container {
padding: 0 ${unsafeCSS(spacing.md)} ${unsafeCSS(spacing.md)} ${unsafeCSS(spacing.md)};
}
.mainbox {
padding: ${unsafeCSS(spacing.sm)};
}
.time-labels {
font-size: 11px;
}
.status-legend {
gap: ${unsafeCSS(spacing.md)};
font-size: 12px;
}
}
`,
];
public render(): TemplateResult {
const now = Date.now();
const currentHour = new Date().getHours();
const timeIndicatorPosition = this.calculateTimeIndicatorPosition();
return html`
<style></style>
<uplinternal-miniheading>Yesterday & Today</uplinternal-miniheading>
<uplinternal-miniheading>${this.serviceName} - Last ${this.hoursToShow} Hours</uplinternal-miniheading>
<div class="mainbox">
<div class="barContainer">
${(() => {
let counter = 0;
const returnArray: TemplateResult[] = [];
while (counter < 48) {
counter++;
returnArray.push(html` <div class="bar"></div> `);
}
return returnArray;
})()}
<div class="timeIndicator"></div>
</div>
${this.loading ? html`
<div class="loading-skeleton">
${Array(this.hoursToShow).fill(0).map(() => html`<div class="skeleton-bar"></div>`)}
</div>
` : html`
<div class="barContainer" @mouseleave=${this.hideTooltip}>
${this.renderBars()}
<div class="timeIndicator" style="left: ${timeIndicatorPosition}px"></div>
</div>
<div class="time-labels">
<span>${this.getTimeLabel(0)}</span>
<span>${this.getTimeLabel(Math.floor(this.hoursToShow / 4))}</span>
<span>${this.getTimeLabel(Math.floor(this.hoursToShow / 2))}</span>
<span>${this.getTimeLabel(Math.floor(this.hoursToShow * 3 / 4))}</span>
<span>Now</span>
</div>
`}
</div>
${!this.loading ? html`
<div class="status-legend">
<div class="legend-item">
<div class="legend-color" style="background: #2deb51"></div>
<span>Operational</span>
</div>
<div class="legend-item">
<div class="legend-color" style="background: #FF9800"></div>
<span>Degraded</span>
</div>
<div class="legend-item">
<div class="legend-color" style="background: #FF6F00"></div>
<span>Partial Outage</span>
</div>
<div class="legend-item">
<div class="legend-color" style="background: #F44336"></div>
<span>Major Outage</span>
</div>
<div class="legend-item">
<div class="legend-color" style="background: #2196F3"></div>
<span>Maintenance</span>
</div>
</div>
` : ''}
<div class="tooltip" id="tooltip"></div>
`;
}
private renderBars(): TemplateResult[] {
const bars: TemplateResult[] = [];
const now = Date.now();
for (let i = 0; i < this.hoursToShow; i++) {
const hourIndex = this.hoursToShow - 1 - i;
const timestamp = now - (hourIndex * 60 * 60 * 1000);
const dataPoint = this.findDataPointForTime(timestamp);
const status = dataPoint?.status || 'no-data';
const responseTime = dataPoint?.responseTime || 0;
bars.push(html`
<div
class="bar ${status}"
@mouseenter=${(e: MouseEvent) => this.showTooltip(e, timestamp, status, responseTime)}
@click=${() => this.handleBarClick(timestamp, status, responseTime)}
></div>
`);
}
return bars;
}
private findDataPointForTime(timestamp: number): IStatusHistoryPoint | undefined {
if (!this.historyData || this.historyData.length === 0) return undefined;
// Find the closest data point within the same hour
const targetHour = new Date(timestamp).getHours();
const targetDate = new Date(timestamp).toDateString();
return this.historyData.find(point => {
const pointDate = new Date(point.timestamp);
return pointDate.toDateString() === targetDate &&
pointDate.getHours() === targetHour;
});
}
private calculateTimeIndicatorPosition(): number {
const containerWidth = 888; // Approximate width minus padding
const barWidth = 19; // Width + margin
const totalBars = this.hoursToShow;
const currentMinutes = new Date().getMinutes();
const currentPosition = (totalBars - 1 + currentMinutes / 60) * barWidth + 6;
return Math.min(currentPosition, containerWidth - 11);
}
private getTimeLabel(hoursAgo: number): string {
const date = new Date(Date.now() - (hoursAgo * 60 * 60 * 1000));
if (hoursAgo >= 24) {
return `${date.getMonth() + 1}/${date.getDate()} ${date.getHours()}:00`;
}
return `${date.getHours()}:00`;
}
private showTooltip(event: MouseEvent, timestamp: number, status: string, responseTime: number) {
const tooltip = this.shadowRoot?.getElementById('tooltip') as HTMLElement;
if (!tooltip) return;
const date = new Date(timestamp);
const timeStr = date.toLocaleString();
const statusStr = status.replace(/_/g, ' ').replace('no-data', 'No Data');
tooltip.innerHTML = `
<div><strong>${timeStr}</strong></div>
<div>Status: ${statusStr}</div>
${responseTime > 0 ? `<div>Response Time: ${responseTime.toFixed(0)}ms</div>` : ''}
`;
const rect = (event.target as HTMLElement).getBoundingClientRect();
const containerRect = this.getBoundingClientRect();
tooltip.style.left = `${rect.left - containerRect.left + rect.width / 2}px`;
tooltip.style.top = `${rect.top - containerRect.top - 60}px`;
tooltip.style.transform = 'translateX(-50%)';
tooltip.classList.add('visible');
}
private hideTooltip() {
const tooltip = this.shadowRoot?.getElementById('tooltip') as HTMLElement;
if (tooltip) {
tooltip.classList.remove('visible');
}
}
private handleBarClick(timestamp: number, status: string, responseTime: number) {
this.dispatchEvent(new CustomEvent('barClick', {
detail: { timestamp, status, responseTime, serviceId: this.serviceId },
bubbles: true,
composed: true
}));
}
}

View File

@@ -0,0 +1,876 @@
import { html } from '@design.estate/dees-element';
import type { IMonthlyUptime, IUptimeDay } from '../interfaces/index.js';
export const demoFunc = () => html`
<style>
.demo-container {
display: flex;
flex-direction: column;
gap: 20px;
}
.demo-section {
border: 1px solid #ddd;
border-radius: 8px;
padding: 20px;
background: #f5f5f5;
}
.demo-title {
font-size: 14px;
font-weight: 600;
margin-bottom: 16px;
color: #333;
}
.demo-controls {
display: flex;
gap: 10px;
margin-top: 16px;
flex-wrap: wrap;
}
.demo-button {
padding: 6px 12px;
border: 1px solid #ddd;
background: white;
border-radius: 4px;
cursor: pointer;
font-size: 13px;
}
.demo-button:hover {
background: #f0f0f0;
}
.demo-button.active {
background: #2196F3;
color: white;
border-color: #2196F3;
}
.demo-info {
margin-top: 12px;
padding: 12px;
background: white;
border-radius: 4px;
font-size: 13px;
line-height: 1.6;
}
.stats-display {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(120px, 1fr));
gap: 12px;
margin-top: 12px;
}
.stat-card {
background: white;
padding: 12px;
border-radius: 4px;
text-align: center;
}
.stat-value {
font-size: 18px;
font-weight: 600;
color: #2196F3;
}
.stat-label {
font-size: 11px;
color: #666;
margin-top: 4px;
}
.month-nav {
display: flex;
justify-content: space-between;
align-items: center;
margin-top: 12px;
}
</style>
<div class="demo-container">
<!-- Different Month Patterns -->
<div class="demo-section">
<div class="demo-title">Different Month Patterns</div>
<dees-demowrapper
.runAfterRender=${async (wrapperElement: any) => {
const statusMonth = wrapperElement.querySelector('upl-statuspage-statusmonth') as any;
// Pattern generators
const generateMonthPattern = (monthCount: number, pattern: 'perfect' | 'problematic' | 'improving' | 'degrading' | 'seasonal'): IMonthlyUptime[] => {
const months: IMonthlyUptime[] = [];
const now = new Date();
for (let monthOffset = monthCount - 1; monthOffset >= 0; monthOffset--) {
const monthDate = new Date(now.getFullYear(), now.getMonth() - monthOffset, 1);
const year = monthDate.getFullYear();
const month = monthDate.getMonth();
const monthKey = `${year}-${String(month + 1).padStart(2, '0')}`;
const daysInMonth = new Date(year, month + 1, 0).getDate();
const days: IUptimeDay[] = [];
let totalIncidents = 0;
let totalUptimeMinutes = 0;
for (let day = 1; day <= daysInMonth; day++) {
let uptime = 100;
let incidents = 0;
let downtime = 0;
let status: IUptimeDay['status'] = 'operational';
switch (pattern) {
case 'perfect':
// Near perfect uptime
if (Math.random() < 0.02) {
uptime = 99.9 + Math.random() * 0.099;
status = 'degraded';
}
break;
case 'problematic':
// Frequent issues
const problemRand = Math.random();
if (problemRand < 0.1) {
uptime = 70 + Math.random() * 20;
incidents = 2 + Math.floor(Math.random() * 3);
status = 'major_outage';
} else if (problemRand < 0.25) {
uptime = 90 + Math.random() * 8;
incidents = 1 + Math.floor(Math.random() * 2);
status = 'partial_outage';
} else if (problemRand < 0.4) {
uptime = 98 + Math.random() * 1.5;
incidents = 1;
status = 'degraded';
}
break;
case 'improving':
// Getting better over time
const improvementFactor = (monthCount - monthOffset) / monthCount;
const improveRand = Math.random();
if (improveRand < 0.3 * (1 - improvementFactor)) {
uptime = 85 + Math.random() * 10 + (improvementFactor * 10);
incidents = Math.max(0, 3 - Math.floor(improvementFactor * 3));
status = improvementFactor > 0.7 ? 'degraded' : 'partial_outage';
}
break;
case 'degrading':
// Getting worse over time
const degradationFactor = monthOffset / monthCount;
const degradeRand = Math.random();
if (degradeRand < 0.3 * (1 - degradationFactor)) {
uptime = 85 + Math.random() * 10 + (degradationFactor * 10);
incidents = Math.max(0, 3 - Math.floor(degradationFactor * 3));
status = degradationFactor > 0.7 ? 'degraded' : 'major_outage';
}
break;
case 'seasonal':
// Worse during certain months (simulating high traffic periods)
const monthNum = month;
if (monthNum === 11 || monthNum === 0) { // December, January
if (Math.random() < 0.3) {
uptime = 92 + Math.random() * 6;
incidents = 1 + Math.floor(Math.random() * 2);
status = 'degraded';
}
} else if (monthNum === 6 || monthNum === 7) { // July, August
if (Math.random() < 0.2) {
uptime = 94 + Math.random() * 5;
incidents = 1;
status = 'degraded';
}
}
break;
}
downtime = Math.floor((100 - uptime) * 14.4);
totalIncidents += incidents;
totalUptimeMinutes += uptime * 14.4;
days.push({
date: `${year}-${String(month + 1).padStart(2, '0')}-${String(day).padStart(2, '0')}`,
uptime,
incidents,
totalDowntime: downtime,
status
});
}
const overallUptime = totalUptimeMinutes / (daysInMonth * 1440) * 100;
months.push({
month: monthKey,
days,
overallUptime,
totalIncidents
});
}
return months;
};
// Initial setup
statusMonth.serviceId = 'production-api';
statusMonth.serviceName = 'Production API';
statusMonth.monthlyData = generateMonthPattern(6, 'perfect');
// Create pattern controls
const controls = document.createElement('div');
controls.className = 'demo-controls';
const patterns = [
{ key: 'perfect', label: 'Perfect Uptime' },
{ key: 'problematic', label: 'Problematic' },
{ key: 'improving', label: 'Improving Trend' },
{ key: 'degrading', label: 'Degrading Trend' },
{ key: 'seasonal', label: 'Seasonal Pattern' }
];
patterns.forEach((pattern, index) => {
const button = document.createElement('button');
button.className = 'demo-button' + (index === 0 ? ' active' : '');
button.textContent = pattern.label;
button.onclick = () => {
controls.querySelectorAll('.demo-button').forEach(btn => btn.classList.remove('active'));
button.classList.add('active');
statusMonth.loading = true;
setTimeout(() => {
statusMonth.monthlyData = generateMonthPattern(6, pattern.key as any);
statusMonth.loading = false;
updateStats();
}, 500);
};
controls.appendChild(button);
});
wrapperElement.appendChild(controls);
// Add statistics display
const statsDiv = document.createElement('div');
statsDiv.className = 'stats-display';
wrapperElement.appendChild(statsDiv);
const updateStats = () => {
const data = statusMonth.monthlyData || [];
const avgUptime = data.reduce((sum, month) => sum + month.overallUptime, 0) / data.length;
const totalIncidents = data.reduce((sum, month) => sum + month.totalIncidents, 0);
const worstMonth = data.reduce((worst, month) =>
month.overallUptime < worst.overallUptime ? month : worst, data[0]);
statsDiv.innerHTML = `
<div class="stat-card">
<div class="stat-value">${avgUptime.toFixed(3)}%</div>
<div class="stat-label">Avg Uptime</div>
</div>
<div class="stat-card">
<div class="stat-value">${totalIncidents}</div>
<div class="stat-label">Total Incidents</div>
</div>
<div class="stat-card">
<div class="stat-value">${data.length}</div>
<div class="stat-label">Months</div>
</div>
<div class="stat-card">
<div class="stat-value">${worstMonth ? worstMonth.overallUptime.toFixed(2) : '100'}%</div>
<div class="stat-label">Worst Month</div>
</div>
`;
};
updateStats();
// Handle day clicks
statusMonth.addEventListener('dayClick', (event: CustomEvent) => {
const { date, uptime, incidents, status, totalDowntime } = event.detail;
alert(`Day Details for ${date}:\n\nUptime: ${uptime.toFixed(3)}%\nIncidents: ${incidents}\nStatus: ${status}\nDowntime: ${totalDowntime} minutes`);
});
}}
>
<upl-statuspage-statusmonth></upl-statuspage-statusmonth>
</dees-demowrapper>
</div>
<!-- Different Time Spans -->
<div class="demo-section">
<div class="demo-title">Different Time Spans</div>
<dees-demowrapper
.runAfterRender=${async (wrapperElement: any) => {
const statusMonth = wrapperElement.querySelector('upl-statuspage-statusmonth') as any;
// Generate data for different time spans
const generateTimeSpanData = (months: number): IMonthlyUptime[] => {
const data: IMonthlyUptime[] = [];
const now = new Date();
for (let monthOffset = months - 1; monthOffset >= 0; monthOffset--) {
const monthDate = new Date(now.getFullYear(), now.getMonth() - monthOffset, 1);
const year = monthDate.getFullYear();
const month = monthDate.getMonth();
const monthKey = `${year}-${String(month + 1).padStart(2, '0')}`;
const daysInMonth = new Date(year, month + 1, 0).getDate();
const days: IUptimeDay[] = [];
let totalIncidents = 0;
let totalUptimeMinutes = 0;
for (let day = 1; day <= daysInMonth; day++) {
// Create realistic patterns
let uptime = 99.9 + Math.random() * 0.099;
let incidents = 0;
let status: IUptimeDay['status'] = 'operational';
if (Math.random() < 0.05) {
uptime = 95 + Math.random() * 4.9;
incidents = 1;
status = 'degraded';
} else if (Math.random() < 0.01) {
uptime = 85 + Math.random() * 10;
incidents = 2;
status = 'partial_outage';
}
const downtime = Math.floor((100 - uptime) * 14.4);
totalIncidents += incidents;
totalUptimeMinutes += uptime * 14.4;
days.push({
date: `${year}-${String(month + 1).padStart(2, '0')}-${String(day).padStart(2, '0')}`,
uptime,
incidents,
totalDowntime: downtime,
status
});
}
const overallUptime = totalUptimeMinutes / (daysInMonth * 1440) * 100;
data.push({
month: monthKey,
days,
overallUptime,
totalIncidents
});
}
return data;
};
// Initial setup
statusMonth.serviceId = 'multi-region-lb';
statusMonth.serviceName = 'Multi-Region Load Balancer';
statusMonth.monthlyData = generateTimeSpanData(3);
// Create time span controls
const controls = document.createElement('div');
controls.className = 'demo-controls';
const timeSpans = [
{ months: 3, label: 'Last 3 Months' },
{ months: 6, label: 'Last 6 Months' },
{ months: 12, label: 'Last 12 Months' },
{ months: 24, label: 'Last 24 Months' }
];
timeSpans.forEach((span, index) => {
const button = document.createElement('button');
button.className = 'demo-button' + (index === 0 ? ' active' : '');
button.textContent = span.label;
button.onclick = () => {
controls.querySelectorAll('.demo-button').forEach(btn => btn.classList.remove('active'));
button.classList.add('active');
statusMonth.loading = true;
setTimeout(() => {
statusMonth.monthlyData = generateTimeSpanData(span.months);
statusMonth.loading = false;
}, 500);
};
controls.appendChild(button);
});
wrapperElement.appendChild(controls);
// Add info display
const info = document.createElement('div');
info.className = 'demo-info';
info.innerHTML = 'Click on different time spans to see historical uptime data. The component automatically adjusts the display based on the number of months.';
wrapperElement.appendChild(info);
}}
>
<upl-statuspage-statusmonth></upl-statuspage-statusmonth>
</dees-demowrapper>
</div>
<!-- Current Month Real-time Updates -->
<div class="demo-section">
<div class="demo-title">Current Month with Real-time Updates</div>
<dees-demowrapper
.runAfterRender=${async (wrapperElement: any) => {
const statusMonth = wrapperElement.querySelector('upl-statuspage-statusmonth') as any;
// Generate current month data
const generateCurrentMonthData = (): IMonthlyUptime[] => {
const now = new Date();
const year = now.getFullYear();
const month = now.getMonth();
const today = now.getDate();
const monthKey = `${year}-${String(month + 1).padStart(2, '0')}`;
const daysInMonth = new Date(year, month + 1, 0).getDate();
const days: IUptimeDay[] = [];
let totalIncidents = 0;
let totalUptimeMinutes = 0;
// Generate data only up to today
for (let day = 1; day <= today; day++) {
let uptime = 99.9 + Math.random() * 0.099;
let incidents = 0;
let status: IUptimeDay['status'] = 'operational';
// Today might have ongoing issues
if (day === today) {
if (Math.random() < 0.3) {
uptime = 95 + Math.random() * 4;
incidents = 1;
status = 'degraded';
}
} else if (Math.random() < 0.05) {
uptime = 97 + Math.random() * 2.9;
incidents = 1;
status = 'degraded';
}
const downtime = Math.floor((100 - uptime) * 14.4);
totalIncidents += incidents;
totalUptimeMinutes += uptime * 14.4;
days.push({
date: `${year}-${String(month + 1).padStart(2, '0')}-${String(day).padStart(2, '0')}`,
uptime,
incidents,
totalDowntime: downtime,
status
});
}
// Fill remaining days with placeholder
for (let day = today + 1; day <= daysInMonth; day++) {
days.push({
date: `${year}-${String(month + 1).padStart(2, '0')}-${String(day).padStart(2, '0')}`,
uptime: 0,
incidents: 0,
totalDowntime: 0,
status: 'operational'
});
}
const overallUptime = today > 0 ? totalUptimeMinutes / (today * 1440) * 100 : 100;
return [{
month: monthKey,
days,
overallUptime,
totalIncidents
}];
};
// Initial setup
statusMonth.serviceId = 'realtime-monitor';
statusMonth.serviceName = 'Real-time Monitoring Service';
statusMonth.monthlyData = generateCurrentMonthData();
statusMonth.showCurrentDay = true;
// Update today's status periodically
const updateInterval = setInterval(() => {
const data = statusMonth.monthlyData;
if (data && data.length > 0) {
const currentMonth = data[0];
const today = new Date().getDate() - 1;
if (currentMonth.days[today]) {
// Simulate status changes
const rand = Math.random();
if (rand < 0.1) {
currentMonth.days[today].uptime = 95 + Math.random() * 4.9;
currentMonth.days[today].incidents = (currentMonth.days[today].incidents || 0) + 1;
currentMonth.days[today].status = 'degraded';
currentMonth.days[today].totalDowntime = Math.floor((100 - currentMonth.days[today].uptime) * 14.4);
// Recalculate overall uptime
let totalUptime = 0;
let validDays = 0;
currentMonth.days.forEach((day, index) => {
if (index <= today && day.uptime > 0) {
totalUptime += day.uptime;
validDays++;
}
});
currentMonth.overallUptime = validDays > 0 ? totalUptime / validDays : 100;
currentMonth.totalIncidents = currentMonth.days.reduce((sum, day) => sum + (day.incidents || 0), 0);
statusMonth.requestUpdate();
logUpdate('Status degraded - Uptime: ' + currentMonth.days[today].uptime.toFixed(2) + '%');
} else if (rand < 0.05 && currentMonth.days[today].status !== 'operational') {
// Recover from issues
currentMonth.days[today].uptime = 99.9 + Math.random() * 0.099;
currentMonth.days[today].status = 'operational';
currentMonth.days[today].totalDowntime = Math.floor((100 - currentMonth.days[today].uptime) * 14.4);
statusMonth.requestUpdate();
logUpdate('Service recovered to operational status');
}
}
}
}, 3000);
// Create controls
const controls = document.createElement('div');
controls.className = 'demo-controls';
controls.innerHTML = '<button class="demo-button" id="simulateOutage">Simulate Outage</button>' +
'<button class="demo-button" id="simulateRecovery">Simulate Recovery</button>' +
'<button class="demo-button" id="refreshData">Refresh Data</button>';
wrapperElement.appendChild(controls);
controls.querySelector('#simulateOutage')?.addEventListener('click', () => {
const data = statusMonth.monthlyData;
if (data && data.length > 0) {
const today = new Date().getDate() - 1;
data[0].days[today].uptime = 85 + Math.random() * 10;
data[0].days[today].incidents = (data[0].days[today].incidents || 0) + 1;
data[0].days[today].status = 'major_outage';
data[0].days[today].totalDowntime = Math.floor((100 - data[0].days[today].uptime) * 14.4);
statusMonth.requestUpdate();
logUpdate('Major outage simulated');
}
});
controls.querySelector('#simulateRecovery')?.addEventListener('click', () => {
const data = statusMonth.monthlyData;
if (data && data.length > 0) {
const today = new Date().getDate() - 1;
data[0].days[today].uptime = 99.95;
data[0].days[today].status = 'operational';
data[0].days[today].totalDowntime = Math.floor((100 - data[0].days[today].uptime) * 14.4);
statusMonth.requestUpdate();
logUpdate('Service recovered');
}
});
controls.querySelector('#refreshData')?.addEventListener('click', () => {
statusMonth.monthlyData = generateCurrentMonthData();
logUpdate('Data refreshed');
});
// Add update log
const logDiv = document.createElement('div');
logDiv.className = 'demo-info';
logDiv.style.maxHeight = '100px';
logDiv.style.overflowY = 'auto';
logDiv.innerHTML = '<strong>Update Log:</strong><br>';
wrapperElement.appendChild(logDiv);
const logUpdate = (message: string) => {
const time = new Date().toLocaleTimeString();
logDiv.innerHTML += '[' + time + '] ' + message + '<br>';
logDiv.scrollTop = logDiv.scrollHeight;
};
// Cleanup
wrapperElement.addEventListener('remove', () => {
clearInterval(updateInterval);
});
}}
>
<upl-statuspage-statusmonth></upl-statuspage-statusmonth>
</dees-demowrapper>
</div>
<!-- Edge Cases -->
<div class="demo-section">
<div class="demo-title">Edge Cases and Special Scenarios</div>
<dees-demowrapper
.runAfterRender=${async (wrapperElement: any) => {
const statusMonth = wrapperElement.querySelector('upl-statuspage-statusmonth') as any;
const scenarios = {
noData: {
name: 'No Data',
data: []
},
singleMonth: {
name: 'Single Month',
data: [{
month: '2024-01',
days: Array.from({ length: 31 }, (_, i) => ({
date: `2024-01-${String(i + 1).padStart(2, '0')}`,
uptime: 99.9 + Math.random() * 0.099,
incidents: 0,
totalDowntime: 0,
status: 'operational' as const
})),
overallUptime: 99.95,
totalIncidents: 0
}]
},
allDown: {
name: 'Complete Outage Month',
data: [{
month: '2024-01',
days: Array.from({ length: 31 }, (_, i) => ({
date: `2024-01-${String(i + 1).padStart(2, '0')}`,
uptime: 0,
incidents: 5,
totalDowntime: 1440,
status: 'major_outage' as const
})),
overallUptime: 0,
totalIncidents: 155
}]
},
maintenanceMonth: {
name: 'Maintenance Heavy Month',
data: [{
month: '2024-01',
days: Array.from({ length: 31 }, (_, i) => {
// Maintenance every weekend
const dayOfWeek = new Date(2024, 0, i + 1).getDay();
if (dayOfWeek === 0 || dayOfWeek === 6) {
return {
date: `2024-01-${String(i + 1).padStart(2, '0')}`,
uptime: 95,
incidents: 0,
totalDowntime: 72,
status: 'maintenance' as const
};
}
return {
date: `2024-01-${String(i + 1).padStart(2, '0')}`,
uptime: 99.95,
incidents: 0,
totalDowntime: 0.7,
status: 'operational' as const
};
}),
overallUptime: 98.2,
totalIncidents: 0
}]
},
mixedYear: {
name: 'Full Year Mixed',
data: Array.from({ length: 12 }, (_, monthIndex) => {
const year = 2023;
const month = monthIndex;
const daysInMonth = new Date(year, month + 1, 0).getDate();
// Different pattern each quarter
let monthPattern = 'operational';
if (monthIndex < 3) monthPattern = 'degraded';
else if (monthIndex < 6) monthPattern = 'improving';
else if (monthIndex < 9) monthPattern = 'stable';
else monthPattern = 'volatile';
const days = Array.from({ length: daysInMonth }, (_, dayIndex) => {
let uptime = 99.9;
let status: IUptimeDay['status'] = 'operational';
let incidents = 0;
if (monthPattern === 'degraded' && Math.random() < 0.3) {
uptime = 85 + Math.random() * 10;
status = 'degraded';
incidents = 1;
} else if (monthPattern === 'volatile' && Math.random() < 0.2) {
uptime = 90 + Math.random() * 9;
status = Math.random() < 0.5 ? 'partial_outage' : 'degraded';
incidents = Math.floor(Math.random() * 3) + 1;
}
return {
date: `${year}-${String(month + 1).padStart(2, '0')}-${String(dayIndex + 1).padStart(2, '0')}`,
uptime,
incidents,
totalDowntime: Math.floor((100 - uptime) * 14.4),
status
};
});
const totalIncidents = days.reduce((sum, day) => sum + day.incidents, 0);
const overallUptime = days.reduce((sum, day) => sum + day.uptime, 0) / days.length;
return {
month: `${year}-${String(month + 1).padStart(2, '0')}`,
days,
overallUptime,
totalIncidents
};
})
}
};
// Initial setup
let currentScenario = 'singleMonth';
statusMonth.serviceId = 'edge-case-service';
statusMonth.serviceName = 'Edge Case Service';
statusMonth.monthlyData = scenarios[currentScenario].data;
// Create scenario controls
const controls = document.createElement('div');
controls.className = 'demo-controls';
Object.entries(scenarios).forEach(([key, scenario]) => {
const button = document.createElement('button');
button.className = 'demo-button' + (key === currentScenario ? ' active' : '');
button.textContent = scenario.name;
button.onclick = () => {
controls.querySelectorAll('.demo-button').forEach(btn => btn.classList.remove('active'));
button.classList.add('active');
currentScenario = key;
statusMonth.loading = true;
setTimeout(() => {
statusMonth.monthlyData = scenario.data;
statusMonth.loading = false;
}, 300);
};
controls.appendChild(button);
});
wrapperElement.appendChild(controls);
}}
>
<upl-statuspage-statusmonth></upl-statuspage-statusmonth>
</dees-demowrapper>
</div>
<!-- Loading and Navigation States -->
<div class="demo-section">
<div class="demo-title">Loading and Navigation Features</div>
<dees-demowrapper
.runAfterRender=${async (wrapperElement: any) => {
const statusMonth = wrapperElement.querySelector('upl-statuspage-statusmonth') as any;
// Start with loading
statusMonth.loading = true;
statusMonth.serviceId = 'navigation-demo';
statusMonth.serviceName = 'Navigation Demo Service';
const controls = document.createElement('div');
controls.className = 'demo-controls';
controls.innerHTML = '<button class="demo-button" id="toggleLoading">Toggle Loading</button>' +
'<button class="demo-button" id="loadSuccess">Load Successfully</button>' +
'<button class="demo-button" id="loadError">Simulate Error</button>' +
'<button class="demo-button" id="toggleTooltip">Toggle Tooltip</button>';
wrapperElement.appendChild(controls);
controls.querySelector('#toggleLoading')?.addEventListener('click', () => {
statusMonth.loading = !statusMonth.loading;
});
controls.querySelector('#loadSuccess')?.addEventListener('click', () => {
statusMonth.loading = true;
setTimeout(() => {
const months = 6;
const data: IMonthlyUptime[] = [];
const now = new Date();
for (let i = months - 1; i >= 0; i--) {
const monthDate = new Date(now.getFullYear(), now.getMonth() - i, 1);
const year = monthDate.getFullYear();
const month = monthDate.getMonth();
const daysInMonth = new Date(year, month + 1, 0).getDate();
data.push({
month: `${year}-${String(month + 1).padStart(2, '0')}`,
days: Array.from({ length: daysInMonth }, (_, d) => ({
date: `${year}-${String(month + 1).padStart(2, '0')}-${String(d + 1).padStart(2, '0')}`,
uptime: 99 + Math.random(),
incidents: Math.random() < 0.05 ? 1 : 0,
totalDowntime: Math.random() < 0.05 ? Math.floor(Math.random() * 60) : 0,
status: Math.random() < 0.05 ? 'degraded' : 'operational'
})),
overallUptime: 99.5 + Math.random() * 0.4,
totalIncidents: Math.floor(Math.random() * 5)
});
}
statusMonth.monthlyData = data;
statusMonth.loading = false;
}, 1000);
});
controls.querySelector('#loadError')?.addEventListener('click', () => {
statusMonth.loading = true;
setTimeout(() => {
statusMonth.loading = false;
statusMonth.monthlyData = [];
statusMonth.errorMessage = 'Failed to load monthly uptime data';
}, 1500);
});
controls.querySelector('#toggleTooltip')?.addEventListener('click', () => {
statusMonth.showTooltip = !statusMonth.showTooltip;
const btn = controls.querySelector('#toggleTooltip');
if (btn) btn.textContent = 'Toggle Tooltip (' + (statusMonth.showTooltip ? 'ON' : 'OFF') + ')';
});
// Month navigation
const navDiv = document.createElement('div');
navDiv.className = 'month-nav';
navDiv.innerHTML = '<button class="demo-button" id="prevMonth">← Previous</button>' +
'<span id="currentMonth">Loading...</span>' +
'<button class="demo-button" id="nextMonth">Next →</button>';
wrapperElement.appendChild(navDiv);
let currentMonthIndex = 0;
const updateNavigation = () => {
const data = statusMonth.monthlyData || [];
if (data.length > 0 && currentMonthIndex < data.length) {
const month = data[currentMonthIndex];
const currentMonthEl = navDiv.querySelector('#currentMonth');
if (currentMonthEl) currentMonthEl.textContent = month.month;
const prevBtn = navDiv.querySelector('#prevMonth') as HTMLButtonElement;
const nextBtn = navDiv.querySelector('#nextMonth') as HTMLButtonElement;
if (prevBtn) prevBtn.disabled = currentMonthIndex === 0;
if (nextBtn) nextBtn.disabled = currentMonthIndex === data.length - 1;
}
};
navDiv.querySelector('#prevMonth')?.addEventListener('click', () => {
if (currentMonthIndex > 0) {
currentMonthIndex--;
updateNavigation();
// Highlight the month somehow
statusMonth.highlightMonth = statusMonth.monthlyData[currentMonthIndex].month;
}
});
navDiv.querySelector('#nextMonth')?.addEventListener('click', () => {
if (currentMonthIndex < (statusMonth.monthlyData?.length || 0) - 1) {
currentMonthIndex++;
updateNavigation();
statusMonth.highlightMonth = statusMonth.monthlyData[currentMonthIndex].month;
}
});
// Initial load
setTimeout(() => {
const data = Array.from({ length: 3 }, (_, i) => ({
month: `2024-${String(i + 1).padStart(2, '0')}`,
days: Array.from({ length: 31 }, (_, d) => ({
date: `2024-${String(i + 1).padStart(2, '0')}-${String(d + 1).padStart(2, '0')}`,
uptime: 99.5 + Math.random() * 0.5,
incidents: 0,
totalDowntime: 0,
status: 'operational' as const
})),
overallUptime: 99.7 + Math.random() * 0.3,
totalIncidents: Math.floor(Math.random() * 3)
}));
statusMonth.monthlyData = data;
statusMonth.loading = false;
statusMonth.showTooltip = true;
updateNavigation();
}, 1000);
}}
>
<upl-statuspage-statusmonth></upl-statuspage-statusmonth>
</dees-demowrapper>
</div>
</div>
`;

View File

@@ -5,11 +5,15 @@ import {
customElement,
type TemplateResult,
css,
cssManager
cssManager,
unsafeCSS
} from '@design.estate/dees-element';
import * as domtools from '@design.estate/dees-domtools';
import type { IMonthlyUptime } from '../interfaces/index.js';
import { fonts, colors, shadows, borderRadius, spacing, commonStyles } from '../styles/shared.styles.js';
import './internal/uplinternal-miniheading.js';
import { demoFunc } from './upl-statuspage-statusmonth.demo.js';
declare global {
interface HTMLElementTagNameMap {
@@ -19,7 +23,25 @@ declare global {
@customElement('upl-statuspage-statusmonth')
export class UplStatuspageStatusmonth extends DeesElement {
public static demo = () => html` <upl-statuspage-statusmonth></upl-statuspage-statusmonth> `;
public static demo = demoFunc;
@property({ type: Array })
public monthlyData: IMonthlyUptime[] = [];
@property({ type: String })
public serviceId: string = '';
@property({ type: String })
public serviceName: string = 'Service';
@property({ type: Boolean })
public loading: boolean = false;
@property({ type: Boolean })
public showTooltip: boolean = true;
@property({ type: Number })
public monthsToShow: number = 5;
constructor() {
super();
@@ -27,104 +49,310 @@ export class UplStatuspageStatusmonth extends DeesElement {
public static styles = [
domtools.elementBasic.staticStyles,
commonStyles,
css`
:host {
position: relative;
padding: 0px 0px 15px 0px;
display: block;
background: ${cssManager.bdTheme('#eeeeeb', '#222222')};;
font-family: Inter;
color: #fff;
background: transparent;
font-family: ${unsafeCSS(fonts.base)};
color: ${colors.text.primary};
}
.container {
max-width: 1200px;
margin: 0 auto;
padding: 0 ${unsafeCSS(spacing.lg)} ${unsafeCSS(spacing.lg)} ${unsafeCSS(spacing.lg)};
}
.mainbox {
margin: auto;
max-width: 900px;
display: grid;
grid-template-columns: repeat(5, calc(100% / 5 - 80px / 5));
grid-column-gap: 20px;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: ${unsafeCSS(spacing.lg)};
}
.statusMonth {
background: ${cssManager.bdTheme('#ffffff', '#333333')};;
min-height: 20px;
display: grid;
padding: 10px;
grid-template-columns: repeat(6, auto);
grid-gap: 9px;
border-radius: 3px;
background: ${colors.background.card};
padding: ${unsafeCSS(spacing.lg)};
border-radius: ${unsafeCSS(borderRadius.md)};
border: 1px solid ${colors.border.default};
box-shadow: ${unsafeCSS(shadows.sm)};
position: relative;
}
.statusMonth .statusDay {
width: 16px;
height: 16px;
background: #2deb51;
border-radius: 3px;
.month-header {
font-size: 14px;
font-weight: 600;
margin-bottom: ${unsafeCSS(spacing.md)};
text-align: center;
color: ${colors.text.primary};
letter-spacing: 0.02em;
}
.days-grid {
display: grid;
grid-template-columns: repeat(7, 1fr);
gap: 3px;
}
.weekday-label {
font-size: 12px;
text-align: center;
color: ${colors.text.secondary};
font-weight: 500;
height: 20px;
line-height: 20px;
margin-bottom: ${unsafeCSS(spacing.xs)};
}
.statusDay {
aspect-ratio: 1;
border-radius: ${unsafeCSS(borderRadius.sm)};
cursor: pointer;
transition: all 0.2s ease;
position: relative;
}
.statusDay:hover {
transform: scale(1.15);
box-shadow: ${unsafeCSS(shadows.md)};
z-index: 1;
}
.statusDay.operational {
background: ${colors.status.operational};
}
.statusDay.degraded {
background: ${colors.status.degraded};
}
.statusDay.partial_outage {
background: ${colors.status.partial};
}
.statusDay.major_outage {
background: ${colors.status.major};
}
.statusDay.no-data {
background: ${colors.border.muted};
opacity: 0.3;
}
.statusDay.empty {
background: transparent;
cursor: default;
}
.statusDay.empty:hover {
transform: none;
box-shadow: none;
}
.overall-uptime {
text-align: center;
font-size: 13px;
margin-top: ${unsafeCSS(spacing.md)};
color: ${colors.text.secondary};
line-height: 1.4;
}
.loading-skeleton {
height: 280px;
background: ${cssManager.bdTheme(
'linear-gradient(90deg, #f3f4f6 25%, #e5e7eb 50%, #f3f4f6 75%)',
'linear-gradient(90deg, #1f1f1f 25%, #262626 50%, #1f1f1f 75%)'
)};
background-size: 200% 100%;
animation: loading 1.5s infinite;
border-radius: ${unsafeCSS(borderRadius.md)};
}
@keyframes loading {
0% { background-position: 200% 0; }
100% { background-position: -200% 0; }
}
.tooltip {
position: absolute;
background: ${cssManager.bdTheme('#1a1a1a', '#ffffff')};
color: ${cssManager.bdTheme('#ffffff', '#1a1a1a')};
padding: ${unsafeCSS(spacing.sm)} ${unsafeCSS(spacing.md)};
border-radius: ${unsafeCSS(borderRadius.base)};
font-size: 12px;
pointer-events: none;
opacity: 0;
transition: opacity 0.2s;
z-index: 10;
white-space: nowrap;
box-shadow: ${unsafeCSS(shadows.lg)};
border: 1px solid ${colors.border.default};
}
.tooltip.visible {
opacity: 0.95;
}
.tooltip strong {
font-weight: 600;
display: block;
margin-bottom: ${unsafeCSS(spacing.xs)};
color: ${cssManager.bdTheme('#ffffff', '#1a1a1a')};
}
.no-data-message {
grid-column: 1 / -1;
text-align: center;
padding: ${unsafeCSS(spacing['2xl'])};
color: ${colors.text.secondary};
}
@media (max-width: 640px) {
.container {
padding: 0 ${unsafeCSS(spacing.md)} ${unsafeCSS(spacing.md)} ${unsafeCSS(spacing.md)};
}
.mainbox {
grid-template-columns: 1fr;
gap: ${unsafeCSS(spacing.md)};
}
.statusMonth {
padding: ${unsafeCSS(spacing.md)};
}
}
`
]
public render(): TemplateResult {
const totalDays = this.monthlyData.reduce((sum, month) => sum + month.days.length, 0);
return html`
<style></style>
<uplinternal-miniheading>Last 150 days</uplinternal-miniheading>
<div class="mainbox">
<div class="statusMonth">
${(() => {
let counter = 0;
const returnArray: TemplateResult[] = [];
while (counter < 30) {
counter++;
returnArray.push(html` <div class="statusDay"></div> `);
}
return returnArray;
})()}
<div class="container">
<uplinternal-miniheading>${this.serviceName} - Last ${totalDays} Days</uplinternal-miniheading>
<div class="mainbox">
${this.loading ? html`
${Array(this.monthsToShow).fill(0).map(() => html`
<div class="statusMonth">
<div class="loading-skeleton"></div>
</div>
`)}
` : this.monthlyData.length === 0 ? html`
<div class="no-data-message">No uptime data available</div>
` : this.monthlyData.map(month => this.renderMonth(month))}
</div>
<div class="statusMonth">
${(() => {
let counter = 0;
const returnArray: TemplateResult[] = [];
while (counter < 30) {
counter++;
returnArray.push(html` <div class="statusDay"></div> `);
}
return returnArray;
})()}
${this.showTooltip ? html`<div class="tooltip" id="tooltip"></div>` : ''}
</div>
`;
}
private renderMonth(monthData: IMonthlyUptime): TemplateResult {
const monthDate = new Date(monthData.month + '-01');
const monthName = monthDate.toLocaleDateString('en-US', { month: 'short', year: 'numeric' });
const firstDayOfWeek = new Date(monthDate.getFullYear(), monthDate.getMonth(), 1).getDay();
return html`
<div class="statusMonth" @mouseleave=${this.hideTooltip}>
<div class="month-header">${monthName}</div>
<div class="days-grid">
${this.renderWeekdayLabels()}
${this.renderEmptyDays(firstDayOfWeek)}
${monthData.days.map(day => this.renderDay(day))}
</div>
<div class="statusMonth">
${(() => {
let counter = 0;
const returnArray: TemplateResult[] = [];
while (counter < 30) {
counter++;
returnArray.push(html` <div class="statusDay"></div> `);
}
return returnArray;
})()}
</div>
<div class="statusMonth">
${(() => {
let counter = 0;
const returnArray: TemplateResult[] = [];
while (counter < 30) {
counter++;
returnArray.push(html` <div class="statusDay"></div> `);
}
return returnArray;
})()}
</div>
<div class="statusMonth">
${(() => {
let counter = 0;
const returnArray: TemplateResult[] = [];
while (counter < 30) {
counter++;
returnArray.push(html` <div class="statusDay"></div> `);
}
return returnArray;
})()}
<div class="overall-uptime">
${monthData.overallUptime.toFixed(2)}% uptime
${monthData.totalIncidents > 0 ? html`<br/>${monthData.totalIncidents} incidents` : ''}
</div>
</div>
`;
}
private renderWeekdayLabels(): TemplateResult[] {
const weekdays = ['S', 'M', 'T', 'W', 'T', 'F', 'S'];
return weekdays.map(day => html`<div class="weekday-label">${day}</div>`);
}
private renderEmptyDays(count: number): TemplateResult[] {
return Array(count).fill(0).map(() => html`<div class="statusDay empty"></div>`);
}
private renderDay(day: any): TemplateResult {
const status = day.status || 'no-data';
const date = new Date(day.date);
const dayNumber = date.getDate();
return html`
<div
class="statusDay ${status}"
@mouseenter=${(e: MouseEvent) => this.showTooltip && this.showDayTooltip(e, day)}
@click=${() => this.handleDayClick(day)}
>
${status === 'major_outage' || status === 'partial_outage' ? html`
<div style="
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
font-size: 8px;
font-weight: bold;
color: white;
">${day.incidents}</div>
` : ''}
</div>
`;
}
private showDayTooltip(event: MouseEvent, day: any) {
const tooltip = this.shadowRoot?.getElementById('tooltip') as HTMLElement;
if (!tooltip) return;
const date = new Date(day.date);
const dateStr = date.toLocaleDateString('en-US', {
weekday: 'long',
year: 'numeric',
month: 'long',
day: 'numeric'
});
let statusText = day.status.replace(/_/g, ' ');
statusText = statusText.charAt(0).toUpperCase() + statusText.slice(1);
tooltip.innerHTML = `
<div><strong>${dateStr}</strong></div>
<div>Status: ${statusText}</div>
<div>Uptime: ${day.uptime.toFixed(2)}%</div>
${day.incidents > 0 ? `<div>Incidents: ${day.incidents}</div>` : ''}
${day.totalDowntime > 0 ? `<div>Downtime: ${day.totalDowntime} minutes</div>` : ''}
`;
const rect = (event.target as HTMLElement).getBoundingClientRect();
const containerRect = this.getBoundingClientRect();
tooltip.style.left = `${rect.left - containerRect.left + rect.width / 2}px`;
tooltip.style.top = `${rect.top - containerRect.top - 80}px`;
tooltip.style.transform = 'translateX(-50%)';
tooltip.classList.add('visible');
}
private hideTooltip() {
const tooltip = this.shadowRoot?.getElementById('tooltip') as HTMLElement;
if (tooltip) {
tooltip.classList.remove('visible');
}
}
private handleDayClick(day: any) {
this.dispatchEvent(new CustomEvent('dayClick', {
detail: {
date: day.date,
uptime: day.uptime,
incidents: day.incidents,
status: day.status,
serviceId: this.serviceId
},
bubbles: true,
composed: true
}));
}
}