Compare commits
28 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 8e3ec4e671 | |||
| 474751add8 | |||
| 32bc42e355 | |||
| 37b9f4e895 | |||
| 883648579e | |||
| 183a1d0658 | |||
| 213323073f | |||
| ed9728dd4a | |||
| 891eb04d11 | |||
| f9604263e3 | |||
| df4dd3f539 | |||
| bff7ec6640 | |||
| bef2cdf2ce | |||
| a20d46f561 | |||
| bcddd4dbee | |||
| 5fcf910eab | |||
| 0f0764564b | |||
| 0ad46c1ed5 | |||
| f6bf598481 | |||
| f2ed469dbd | |||
| f7d3709dac | |||
| 313e736f29 | |||
| d12147716d | |||
| 363cf32325 | |||
| 7e704483d0 | |||
| ee54e62eab | |||
| 3eeb971188 | |||
| d2243d2376 |
1
.gitignore
vendored
1
.gitignore
vendored
@@ -3,7 +3,6 @@
|
||||
# artifacts
|
||||
coverage/
|
||||
public/
|
||||
pages/
|
||||
|
||||
# installs
|
||||
node_modules/
|
||||
|
||||
132
.gitlab-ci.yml
132
.gitlab-ci.yml
@@ -1,132 +0,0 @@
|
||||
# gitzone ci_default_private
|
||||
image: registry.gitlab.com/hosttoday/ht-docker-node:npmci
|
||||
|
||||
cache:
|
||||
paths:
|
||||
- .npmci_cache/
|
||||
key: '$CI_BUILD_STAGE'
|
||||
|
||||
stages:
|
||||
- security
|
||||
- test
|
||||
- release
|
||||
- metadata
|
||||
|
||||
before_script:
|
||||
- pnpm install -g pnpm
|
||||
- pnpm install -g @shipzone/npmci
|
||||
- npmci npm prepare
|
||||
|
||||
# ====================
|
||||
# security stage
|
||||
# ====================
|
||||
# ====================
|
||||
# security stage
|
||||
# ====================
|
||||
auditProductionDependencies:
|
||||
image: registry.gitlab.com/hosttoday/ht-docker-node:npmci
|
||||
stage: security
|
||||
script:
|
||||
- npmci command npm config set registry https://registry.npmjs.org
|
||||
- npmci command pnpm audit --audit-level=high --prod
|
||||
tags:
|
||||
- lossless
|
||||
- docker
|
||||
|
||||
auditDevDependencies:
|
||||
image: registry.gitlab.com/hosttoday/ht-docker-node:npmci
|
||||
stage: security
|
||||
script:
|
||||
- npmci command npm config set registry https://registry.npmjs.org
|
||||
- npmci command pnpm audit --audit-level=high --dev
|
||||
tags:
|
||||
- lossless
|
||||
- docker
|
||||
allow_failure: true
|
||||
|
||||
# ====================
|
||||
# test stage
|
||||
# ====================
|
||||
|
||||
testStable:
|
||||
stage: test
|
||||
script:
|
||||
- npmci node install stable
|
||||
- npmci npm install
|
||||
- npmci npm test
|
||||
coverage: /\d+.?\d+?\%\s*coverage/
|
||||
tags:
|
||||
- lossless
|
||||
- docker
|
||||
- notpriv
|
||||
|
||||
testBuild:
|
||||
stage: test
|
||||
script:
|
||||
- npmci node install stable
|
||||
- npmci npm install
|
||||
- npmci command npm run build
|
||||
coverage: /\d+.?\d+?\%\s*coverage/
|
||||
tags:
|
||||
- lossless
|
||||
- docker
|
||||
- notpriv
|
||||
|
||||
release:
|
||||
stage: release
|
||||
script:
|
||||
- npmci node install stable
|
||||
- npmci npm publish
|
||||
only:
|
||||
- tags
|
||||
tags:
|
||||
- lossless
|
||||
- docker
|
||||
- notpriv
|
||||
|
||||
# ====================
|
||||
# metadata stage
|
||||
# ====================
|
||||
codequality:
|
||||
stage: metadata
|
||||
allow_failure: true
|
||||
only:
|
||||
- tags
|
||||
script:
|
||||
- npmci command npm install -g typescript
|
||||
- npmci npm prepare
|
||||
- npmci npm install
|
||||
tags:
|
||||
- lossless
|
||||
- docker
|
||||
- priv
|
||||
|
||||
trigger:
|
||||
stage: metadata
|
||||
script:
|
||||
- npmci trigger
|
||||
only:
|
||||
- tags
|
||||
tags:
|
||||
- lossless
|
||||
- docker
|
||||
- notpriv
|
||||
|
||||
pages:
|
||||
stage: metadata
|
||||
script:
|
||||
- npmci node install lts
|
||||
- npmci command npm install -g @gitzone/tsdoc
|
||||
- npmci npm install
|
||||
- npmci command tsdoc
|
||||
tags:
|
||||
- lossless
|
||||
- docker
|
||||
- notpriv
|
||||
only:
|
||||
- tags
|
||||
artifacts:
|
||||
expire_in: 1 week
|
||||
paths:
|
||||
- public
|
||||
allow_failure: true
|
||||
95
changelog.md
Normal file
95
changelog.md
Normal file
@@ -0,0 +1,95 @@
|
||||
# Changelog
|
||||
|
||||
## 2025-12-23 - 1.3.1 - fix(statuspage)
|
||||
update timeline connector and dot positioning in statuspage incidents component
|
||||
|
||||
- Remove global .timeline::before vertical connector and replace with per-item .update-item:not(:last-child)::after to draw connectors between dots
|
||||
- Use sharedStyles.colors.border.default for the connector background instead of a theme gradient
|
||||
- Adjust connector and dot offsets for desktop and mobile to align with dot size and border, improving visual alignment
|
||||
- Add clarifying comments about dot dimensions and center calculations
|
||||
|
||||
## 2025-12-23 - 1.3.0 - feat(statuspage)
|
||||
use dynamic status-based accent colors and computed card statuses; update stat card markup and incident/response displays
|
||||
|
||||
- Replace hardcoded stat-card type selectors with status-based classes (.operational, .degraded, .partial_outage, .major_outage, .maintenance) so accent colors are applied centrally.
|
||||
- Introduce getUptimeCardStatus, getResponseCardStatus, and getIncidentCardStatus helper methods to compute and apply per-card status classes based on uptime, response time, and affected services.
|
||||
- Update stat card markup to use computed status classes instead of dedicated uptime/response/incident class names.
|
||||
- Change incident summary text to show 'All services ok.' when no services are affected, otherwise display 'X of Y services affected'.
|
||||
- Minor layout tweak: adjust timeline connector left offset from 5px to 7px for improved alignment.
|
||||
|
||||
## 2025-12-23 - 1.2.0 - feat(statuspage-ui)
|
||||
improve styling and animations across status page components
|
||||
|
||||
- Preload Geist Sans variable font in html/index.html to improve font rendering.
|
||||
- Replace many cssManager.bdTheme usages with sharedStyles color tokens, durations and easings for consistent theming.
|
||||
- Introduce animations, transitions and motion utilities (fadeIn, fadeInUp, card/pill fades, shimmer, pulse) for cards, pills, bars, tooltips and skeletons to enhance perceived performance.
|
||||
- Enhance interactive states: hover/active/focus for buttons, social links, action buttons, and asset/incident cards; add subtle transforms, shadows and icon animations.
|
||||
- Add status-specific visuals: status gradients, status glows, pulsing/animated status indicators and left accent bars for statusbar and stat cards.
|
||||
- Improve incidents and timeline UI: staggered entrance animations, active-incident pulse, update timeline with icons and delays; tooltip and tooltip content improvements; responsive tweaks across header, footer, stats and month views.
|
||||
|
||||
## 2025-12-23 - 1.1.0 - feat(statuspage)
|
||||
refactor shared styles and modernize components for consistent theming, spacing and APIs
|
||||
|
||||
- Centralized styles: switch to a single sharedStyles module and extend design tokens (colors, spacing, shadows, borderRadius, easings, durations, accent colors).
|
||||
- Component modernization: replace many destructured style imports with sharedStyles and convert numerous @property fields to 'accessor' for updated component APIs.
|
||||
- Visual/layout updates: adjust typography, spacing scale, responsive rules and polish header/statusbar/footer/stats/incident components (improved gaps, paddings, status pills, loading skeletons, and button styles).
|
||||
- Dependency bumps: updated @design.estate and @git.zone packages as well as @push.rocks dev deps and @types/node.
|
||||
- Build/config changes: tsconfig.json flags removed (experimentalDecorators, useDefineForClassFields) and npmextra.json updated with registry/release config and new package keys.
|
||||
- Docs and demos: expanded README content and added demo/test files (test/test.ts, test-shadcn-spacing.html) to showcase spacing and layout changes.
|
||||
|
||||
## 2024-06-27 - 1.0.74 - fix(core)
|
||||
Updated font loading strategy in index.html for improved performance
|
||||
|
||||
- Replaced multiple font loading links with a single assetbroker link.
|
||||
- Removed redundant preconnect and stylesheet links for fonts.
|
||||
|
||||
## 2024-06-26 - 1.0.73 - fix(documentation)
|
||||
Update project description and keywords in package.json and npmextra.json. Refactored documentation in readme.md.
|
||||
|
||||
- Updated project description and keywords within package.json and npmextra.json to ensure alignment.
|
||||
- Enhanced readme.md with detailed setup, installation and usage instructions including examples for importing and using components.
|
||||
|
||||
## 2024-06-26 - 1.0.72 - fix(core)
|
||||
Fixed incorrect import paths and updated configurations for package publication.
|
||||
|
||||
- Fixed import paths for various dependencies in TypeScript files.
|
||||
- Updated npm package name and scope in package.json.
|
||||
- Deleted .gitlab-ci.yml file.
|
||||
- Adjusted tsconfig.json for ESModule compatibility.
|
||||
- Adjusted npmextra.json for correct repository and license information.
|
||||
- Updated documentation links in readme.md.
|
||||
|
||||
## 2023-01-05 - 1.0.69 to 1.0.71 - core
|
||||
Multiple updates.
|
||||
|
||||
- fix(core): update
|
||||
|
||||
## 2022-03-24 - 1.0.67 to 1.0.69 - core
|
||||
Multiple updates.
|
||||
|
||||
- fix(core): update
|
||||
|
||||
## 2021-09-27 - 1.0.60 to 1.0.66 - core
|
||||
Multiple updates.
|
||||
|
||||
- fix(core): update
|
||||
|
||||
## 2021-09-24 - 1.0.59 to 1.0.60 - core
|
||||
Fixes and updates.
|
||||
|
||||
- fix(core): update
|
||||
|
||||
## 2021-09-23 - 1.0.58 to 1.0.59 - core
|
||||
Fixes and updates.
|
||||
|
||||
- fix(core): update
|
||||
|
||||
## 2020-11-29 - 1.0.57 to 1.0.58 - core
|
||||
Fixes and updates.
|
||||
|
||||
- fix(core): update
|
||||
|
||||
## 2020-09-19 - 1.0.55 to 1.0.57 - core
|
||||
Fixes and updates.
|
||||
|
||||
- fix(core): update
|
||||
228
example.statuspage.html
Normal file
228
example.statuspage.html
Normal file
@@ -0,0 +1,228 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Status Page Example</title>
|
||||
<script type="module" src="./dist_bundle/bundle.js"></script>
|
||||
<style>
|
||||
body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
font-family: 'Geist Sans', -apple-system, BlinkMacSystemFont, sans-serif;
|
||||
background: #fafafa;
|
||||
}
|
||||
@media (prefers-color-scheme: dark) {
|
||||
body {
|
||||
background: #09090b;
|
||||
}
|
||||
}
|
||||
.page-container {
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
main {
|
||||
flex: 1;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="page-container">
|
||||
<!-- Header -->
|
||||
<upl-statuspage-header
|
||||
title="System Status"
|
||||
brandName="Example Corp"
|
||||
description="Real-time status of our services"
|
||||
></upl-statuspage-header>
|
||||
|
||||
<!-- Status Bar -->
|
||||
<upl-statuspage-statusbar></upl-statuspage-statusbar>
|
||||
|
||||
<!-- Stats Grid - NEW COMPONENT -->
|
||||
<upl-statuspage-statsgrid
|
||||
uptime="99.95"
|
||||
avgResponseTime="125"
|
||||
totalIncidents="0"
|
||||
affectedServices="0"
|
||||
totalServices="12"
|
||||
currentStatus="operational"
|
||||
timePeriod="90 days"
|
||||
></upl-statuspage-statsgrid>
|
||||
|
||||
<main>
|
||||
<!-- Assets Selector -->
|
||||
<upl-statuspage-assetsselector></upl-statuspage-assetsselector>
|
||||
|
||||
<!-- Status Details (48 hours) -->
|
||||
<upl-statuspage-statusdetails
|
||||
serviceName="API Gateway"
|
||||
hoursToShow="48"
|
||||
></upl-statuspage-statusdetails>
|
||||
|
||||
<!-- Status Month -->
|
||||
<upl-statuspage-statusmonth
|
||||
serviceName="API Gateway"
|
||||
monthsToShow="3"
|
||||
></upl-statuspage-statusmonth>
|
||||
|
||||
<!-- Incidents -->
|
||||
<upl-statuspage-incidents
|
||||
daysToShow="90"
|
||||
></upl-statuspage-incidents>
|
||||
</main>
|
||||
|
||||
<!-- Footer -->
|
||||
<upl-statuspage-footer
|
||||
showPoweredBy="true"
|
||||
showApiLink="true"
|
||||
></upl-statuspage-footer>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// Example: Initialize with demo data
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
// Status bar data
|
||||
const statusBar = document.querySelector('upl-statuspage-statusbar');
|
||||
if (statusBar) {
|
||||
statusBar.overallStatus = {
|
||||
status: 'operational',
|
||||
message: 'All systems operational',
|
||||
lastUpdated: Date.now(),
|
||||
affectedServices: 0,
|
||||
totalServices: 12
|
||||
};
|
||||
}
|
||||
|
||||
// Assets selector data
|
||||
const assetsSelector = document.querySelector('upl-statuspage-assetsselector');
|
||||
if (assetsSelector) {
|
||||
assetsSelector.services = [
|
||||
{
|
||||
id: 'api-gateway',
|
||||
displayName: 'API Gateway',
|
||||
category: 'Core Services',
|
||||
description: 'Main API endpoint',
|
||||
currentStatus: 'operational',
|
||||
selected: true
|
||||
},
|
||||
{
|
||||
id: 'web-app',
|
||||
displayName: 'Web Application',
|
||||
category: 'Frontend',
|
||||
description: 'Customer-facing web app',
|
||||
currentStatus: 'operational',
|
||||
selected: true
|
||||
},
|
||||
{
|
||||
id: 'database',
|
||||
displayName: 'Database Cluster',
|
||||
category: 'Core Services',
|
||||
description: 'Primary data storage',
|
||||
currentStatus: 'operational',
|
||||
selected: true
|
||||
},
|
||||
{
|
||||
id: 'cdn',
|
||||
displayName: 'CDN',
|
||||
category: 'Infrastructure',
|
||||
description: 'Content delivery network',
|
||||
currentStatus: 'operational',
|
||||
selected: false
|
||||
}
|
||||
];
|
||||
}
|
||||
|
||||
// Status details data (48 hours)
|
||||
const statusDetails = document.querySelector('upl-statuspage-statusdetails');
|
||||
if (statusDetails) {
|
||||
const now = Date.now();
|
||||
statusDetails.historyData = Array.from({ length: 48 }, (_, i) => {
|
||||
const date = new Date();
|
||||
date.setMinutes(0, 0, 0);
|
||||
date.setHours(date.getHours() - (47 - i));
|
||||
return {
|
||||
timestamp: date.getTime(),
|
||||
status: Math.random() > 0.95 ? 'degraded' : 'operational',
|
||||
responseTime: 50 + Math.random() * 50,
|
||||
errorRate: 0
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
// Monthly data
|
||||
const statusMonth = document.querySelector('upl-statuspage-statusmonth');
|
||||
if (statusMonth) {
|
||||
const generateMonthData = (monthOffset) => {
|
||||
const date = new Date();
|
||||
date.setMonth(date.getMonth() - monthOffset);
|
||||
const year = date.getFullYear();
|
||||
const month = date.getMonth();
|
||||
const daysInMonth = new Date(year, month + 1, 0).getDate();
|
||||
|
||||
return {
|
||||
month: `${year}-${(month + 1).toString().padStart(2, '0')}`,
|
||||
days: Array.from({ length: daysInMonth }, (_, i) => ({
|
||||
date: `${year}-${(month + 1).toString().padStart(2, '0')}-${(i + 1).toString().padStart(2, '0')}`,
|
||||
uptime: Math.random() > 0.05 ? 100 : 95 + Math.random() * 5,
|
||||
status: Math.random() > 0.05 ? 'operational' : 'degraded',
|
||||
incidents: Math.random() > 0.05 ? 0 : 1,
|
||||
totalDowntime: 0
|
||||
})),
|
||||
overallUptime: 99.5 + Math.random() * 0.5,
|
||||
totalIncidents: Math.floor(Math.random() * 3)
|
||||
};
|
||||
};
|
||||
|
||||
statusMonth.monthlyData = [
|
||||
generateMonthData(0),
|
||||
generateMonthData(1),
|
||||
generateMonthData(2)
|
||||
];
|
||||
}
|
||||
|
||||
// Incidents data
|
||||
const incidents = document.querySelector('upl-statuspage-incidents');
|
||||
if (incidents) {
|
||||
incidents.currentIncidents = [];
|
||||
incidents.pastIncidents = [
|
||||
{
|
||||
id: 'inc-001',
|
||||
title: 'Database connection pool exhaustion',
|
||||
severity: 'minor',
|
||||
startTime: Date.now() - 7 * 24 * 60 * 60 * 1000,
|
||||
endTime: Date.now() - 7 * 24 * 60 * 60 * 1000 + 2 * 60 * 60 * 1000,
|
||||
impact: 'Slower response times for some API endpoints',
|
||||
affectedServices: ['API Gateway', 'Database Cluster'],
|
||||
updates: [
|
||||
{
|
||||
id: 'update-1',
|
||||
timestamp: Date.now() - 7 * 24 * 60 * 60 * 1000,
|
||||
status: 'investigating',
|
||||
message: 'We are investigating reports of slow API responses',
|
||||
author: 'Engineering Team'
|
||||
},
|
||||
{
|
||||
id: 'update-2',
|
||||
timestamp: Date.now() - 7 * 24 * 60 * 60 * 1000 + 30 * 60 * 1000,
|
||||
status: 'identified',
|
||||
message: 'The issue has been identified as database connection pool exhaustion',
|
||||
author: 'Engineering Team'
|
||||
},
|
||||
{
|
||||
id: 'update-3',
|
||||
timestamp: Date.now() - 7 * 24 * 60 * 60 * 1000 + 2 * 60 * 60 * 1000,
|
||||
status: 'resolved',
|
||||
message: 'Connection pool settings have been adjusted and performance is back to normal',
|
||||
author: 'Engineering Team'
|
||||
}
|
||||
],
|
||||
rootCause: 'Insufficient connection pool size for peak traffic',
|
||||
resolution: 'Increased connection pool size and implemented better connection management'
|
||||
}
|
||||
];
|
||||
}
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -10,16 +10,10 @@
|
||||
/>
|
||||
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
|
||||
|
||||
<link rel="preconnect" href="https://rsms.me/">
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link rel="stylesheet" href="https://rsms.me/inter/inter.css">
|
||||
<link
|
||||
crossorigin="anonymous"
|
||||
href="https://fonts.googleapis.com/icon?family=Material+Icons"
|
||||
rel="stylesheet"
|
||||
/>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Courier+Prime:ital,wght@0,400;0,700;1,400;1,700&display=swap" rel="stylesheet">
|
||||
<!--Lets load standard fonts-->
|
||||
<link rel="preconnect" href="https://assetbroker.lossless.one/" crossorigin>
|
||||
<link rel="preload" href="https://assetbroker.lossless.one/fonts/geist-sans/geistvf.woff2" as="font" type="font/woff2" crossorigin>
|
||||
<link rel="stylesheet" href="https://assetbroker.lossless.one/fonts/fonts.css">
|
||||
|
||||
<style>
|
||||
body {
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
// dees tools
|
||||
import * as deesWccTools from '@designestate/dees-wcctools';
|
||||
import * as deesDomTools from '@designestate/dees-domtools';
|
||||
import * as deesWccTools from '@design.estate/dees-wcctools';
|
||||
import * as deesDomTools from '@design.estate/dees-domtools';
|
||||
// Import demotools to register dees-demowrapper
|
||||
import '@design.estate/dees-wcctools/demotools';
|
||||
|
||||
// elements and pages
|
||||
import * as elements from '../ts_web/elements/index.js';
|
||||
|
||||
@@ -1,19 +1,39 @@
|
||||
{
|
||||
"gitzone": {
|
||||
"@git.zone/cli": {
|
||||
"projectType": "wcc",
|
||||
"module": {
|
||||
"githost": "gitlab.com",
|
||||
"gitscope": "uptimelink/private",
|
||||
"gitrepo": "catalog",
|
||||
"description": "a catalog with webcomponents for uptimelink dashboard",
|
||||
"npmPackagename": "@uptimelink_private/catalog",
|
||||
"license": "UNLICENSED",
|
||||
"projectDomain": "uptime.link"
|
||||
"githost": "code.foss.global",
|
||||
"gitscope": "uptime.link",
|
||||
"gitrepo": "statuspage",
|
||||
"description": "A catalog of web components for the UptimeLink dashboard.",
|
||||
"npmPackagename": "@uptime.link/statuspage",
|
||||
"license": "MIT",
|
||||
"projectDomain": "uptime.link",
|
||||
"keywords": [
|
||||
"web components",
|
||||
"uptimelink",
|
||||
"dashboard",
|
||||
"status monitoring",
|
||||
"typescript",
|
||||
"incidents",
|
||||
"status",
|
||||
"performance",
|
||||
"uptime",
|
||||
"frontend",
|
||||
"UI",
|
||||
"catalog"
|
||||
]
|
||||
},
|
||||
"release": {
|
||||
"registries": [
|
||||
"https://verdaccio.lossless.digital",
|
||||
"https://registry.npmjs.org"
|
||||
],
|
||||
"accessLevel": "public"
|
||||
}
|
||||
},
|
||||
"npmci": {
|
||||
"@ship.zone/szci": {
|
||||
"npmGlobalTools": [],
|
||||
"npmAccessLevel": "private",
|
||||
"npmRegistryUrl": "verdaccio.lossless.one"
|
||||
}
|
||||
}
|
||||
45
package.json
45
package.json
@@ -1,34 +1,33 @@
|
||||
{
|
||||
"name": "@uptimelink_private/catalog",
|
||||
"version": "1.0.71",
|
||||
"name": "@uptime.link/statuspage",
|
||||
"version": "1.3.1",
|
||||
"private": false,
|
||||
"description": "a catalog with webcomponents for uptimelink dashboard",
|
||||
"description": "A catalog of web components for the UptimeLink dashboard.",
|
||||
"main": "dist_ts_web/index.js",
|
||||
"typings": "dist_ts_web/index.d.ts",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"test": "npm run build",
|
||||
"build": "tsbuild element --allowimplicitany && tsbundle element --production",
|
||||
"build": "tsbuild tsfolders --allowimplicitany && tsbundle element --production",
|
||||
"watch": "tswatch element",
|
||||
"buildDocs": "tsdoc"
|
||||
},
|
||||
"author": "Lossless GmbH",
|
||||
"license": "UNLICENSED",
|
||||
"dependencies": {
|
||||
"@designestate/dees-domtools": "^2.0.1",
|
||||
"@designestate/dees-element": "^2.0.4",
|
||||
"@designestate/dees-wcctools": "^1.0.73",
|
||||
"@losslessone_private/loint-pubapi": "^1.0.10",
|
||||
"@uptimelink/interfaces": "^1.0.10"
|
||||
"@design.estate/dees-domtools": "^2.3.6",
|
||||
"@design.estate/dees-element": "^2.1.3",
|
||||
"@design.estate/dees-wcctools": "^3.2.0",
|
||||
"@uptime.link/interfaces": "^2.0.21"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@gitzone/tsbuild": "^2.1.61",
|
||||
"@gitzone/tsbundle": "^2.0.7",
|
||||
"@gitzone/tsrun": "^1.2.39",
|
||||
"@gitzone/tswatch": "^2.0.5",
|
||||
"@pushrocks/projectinfo": "^5.0.1",
|
||||
"@pushrocks/smartenv": "^5.0.0",
|
||||
"@types/node": "^18.11.18"
|
||||
"@git.zone/tsbuild": "^4.0.2",
|
||||
"@git.zone/tsbundle": "^2.6.3",
|
||||
"@git.zone/tsrun": "^2.0.1",
|
||||
"@git.zone/tswatch": "^2.3.13",
|
||||
"@push.rocks/projectinfo": "^5.0.2",
|
||||
"@push.rocks/smartenv": "^6.0.0",
|
||||
"@types/node": "^25.0.3"
|
||||
},
|
||||
"files": [
|
||||
"ts/**/*",
|
||||
@@ -44,5 +43,19 @@
|
||||
],
|
||||
"browserslist": [
|
||||
"last 1 Chrome versions"
|
||||
],
|
||||
"keywords": [
|
||||
"web components",
|
||||
"uptimelink",
|
||||
"dashboard",
|
||||
"status monitoring",
|
||||
"typescript",
|
||||
"incidents",
|
||||
"status",
|
||||
"performance",
|
||||
"uptime",
|
||||
"frontend",
|
||||
"UI",
|
||||
"catalog"
|
||||
]
|
||||
}
|
||||
|
||||
8570
pnpm-lock.yaml
generated
8570
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
292
readme.hints.md
Normal file
292
readme.hints.md
Normal file
@@ -0,0 +1,292 @@
|
||||
# Project Hints and Analysis
|
||||
|
||||
## Project Overview
|
||||
The @uptime.link/statuspage (v1.0.74) is a web components catalog specifically designed for building status pages for UptimeLink - an uptime monitoring platform. This catalog provides pre-built, customizable UI components that can be assembled to create complete status pages.
|
||||
|
||||
## Core Purpose
|
||||
- **Primary Function**: Provide a comprehensive set of web components for building status monitoring dashboards
|
||||
- **Target Audience**: Developers building status pages for services using UptimeLink monitoring
|
||||
- **Key Features**: Real-time status display, incident management, historical data visualization
|
||||
|
||||
## Architecture
|
||||
- **Package Name**: @uptime.link/statuspage (v1.0.74)
|
||||
- **Project Type**: Web Component Catalog (wcc)
|
||||
- **Module Type**: ESM (ECMAScript Modules)
|
||||
- **Distribution**: Private npm package on verdaccio.lossless.one registry
|
||||
- **License**: UNLICENSED (proprietary to Lossless GmbH)
|
||||
|
||||
## Technology Stack
|
||||
- **Framework**: @design.estate/dees-element - A web components framework with:
|
||||
- TypeScript decorators for component registration
|
||||
- Built-in CSS-in-JS with theme support
|
||||
- Shadow DOM encapsulation
|
||||
- Property binding system
|
||||
- **DOM Utilities**: @design.estate/dees-domtools for DOM manipulation
|
||||
- **Interfaces**: @uptime.link/interfaces for shared data structures
|
||||
- **Build Tools**:
|
||||
- tsbuild: TypeScript compilation with --allowimplicitany flag
|
||||
- tsbundle: Creates production bundles for web components
|
||||
- tswatch: Development file watching
|
||||
- TypeScript target: ES2022 with NodeNext module resolution
|
||||
|
||||
## Component Library
|
||||
The catalog provides 7 main components + 1 internal component:
|
||||
|
||||
### Main Components:
|
||||
1. **upl-statuspage-header**:
|
||||
- Page header with customizable title
|
||||
- Action buttons for "Report Incident" and "Subscribe to Updates"
|
||||
- Emits custom events: 'reportNewIncident' and 'statusSubscribe'
|
||||
|
||||
2. **upl-statuspage-statusbar**:
|
||||
- Main status indicator showing overall system health
|
||||
- Visual status representation (likely green/yellow/red indicators)
|
||||
|
||||
3. **upl-statuspage-assetsselector**:
|
||||
- Component for selecting/filtering which assets to view
|
||||
- Useful for multi-service status pages
|
||||
|
||||
4. **upl-statuspage-statusdetails**:
|
||||
- Detailed status information display
|
||||
- Shows granular status data for selected services
|
||||
|
||||
5. **upl-statuspage-statusmonth**:
|
||||
- Monthly calendar view of status history
|
||||
- Visual representation of uptime/downtime over time
|
||||
|
||||
6. **upl-statuspage-incidents**:
|
||||
- Incident management display
|
||||
- Properties: currentIncidences and pastIncidences (arrays of IIncident)
|
||||
- Supports whitelabel mode
|
||||
- Shows active and historical incidents
|
||||
|
||||
7. **upl-statuspage-footer**:
|
||||
- Page footer with legal information link
|
||||
- Customizable legal URL
|
||||
|
||||
### Internal Components:
|
||||
- **uplinternal-miniheading**: Internal component for consistent heading styling
|
||||
|
||||
## Data Flow & Integration
|
||||
- Components receive data through properties (using @property decorator)
|
||||
- Incident data follows the IIncident interface from @uptime.link/interfaces
|
||||
- Components are designed to work standalone or together
|
||||
- Event-driven communication between components
|
||||
|
||||
## Styling & Theming
|
||||
- CSS-in-JS approach using cssManager
|
||||
- Built-in light/dark theme support via bdTheme() helper
|
||||
- Font: Inter (loaded via assetbroker)
|
||||
- Responsive design with max-width constraints (900px)
|
||||
- Background colors: Light (#eeeeeb) / Dark (#222222)
|
||||
- Text colors: Light (#333333) / Dark (#ffffff)
|
||||
|
||||
## Build Output Structure
|
||||
- Source: ts_web/ directory
|
||||
- Compiled output: dist_ts_web/ (ES modules with TypeScript definitions)
|
||||
- Bundle output: dist_bundle/ (production-ready bundle with source maps)
|
||||
- Development server: dist_watch/ with index.html for testing
|
||||
|
||||
## Usage Pattern
|
||||
1. Import components from the package
|
||||
2. Create elements using document.createElement()
|
||||
3. Set properties programmatically
|
||||
4. Append to DOM
|
||||
5. Handle custom events for user interactions
|
||||
|
||||
## Recent Updates (from changelog)
|
||||
- v1.0.74: Improved font loading strategy using single assetbroker link
|
||||
- v1.0.73: Enhanced documentation and aligned project descriptions
|
||||
- v1.0.72: Fixed import paths and updated package configurations
|
||||
|
||||
## Development Workflow
|
||||
- `pnpm build`: Compile TypeScript and create production bundle
|
||||
- `pnpm watch`: Start development server with hot reload
|
||||
- `pnpm test`: Currently just runs build (no actual tests implemented)
|
||||
- Demo page available at html/index.html using page1 template
|
||||
|
||||
## Key Observations
|
||||
1. The project follows a consistent pattern for all components
|
||||
2. Each component is self-contained with its own styling
|
||||
3. Theme support is built-in for all components
|
||||
4. The project is part of a larger UptimeLink ecosystem
|
||||
5. Components are designed for composition into complete status pages
|
||||
6. No test files are currently implemented despite test infrastructure being set up
|
||||
|
||||
## Production Readiness Analysis (v1.0.74)
|
||||
|
||||
### Current State
|
||||
The components are essentially **UI shells** - they have styling and structure but lack actual functionality. They display static/hardcoded content with no real data integration.
|
||||
|
||||
### Major Missing Functionality
|
||||
|
||||
#### 1. Data Integration
|
||||
- **No API client or data fetching logic** - components can't retrieve real status data
|
||||
- **No authentication/authorization** - no secure API communication
|
||||
- **No real-time updates** - no WebSocket/SSE implementation
|
||||
- **Static content only** - statusbar always shows "Everything is working normally!"
|
||||
- **Empty data properties** - currentIncidences/pastIncidences arrays are never populated
|
||||
|
||||
#### 2. Component Implementation Gaps
|
||||
- **upl-statuspage-assetsselector**: Only shows "Hello!" - missing entire asset selection UI
|
||||
- **upl-statuspage-statusbar**: Hardcoded green status - no dynamic status calculation
|
||||
- **upl-statuspage-statusdetails**: Shows 48 static green bars - no actual hourly data
|
||||
- **upl-statuspage-statusmonth**: Shows 150 static green days - no real uptime data
|
||||
- **upl-statuspage-incidents**: Only shows "No incidents" - missing incident card rendering
|
||||
- **upl-statuspage-footer**: Placeholder "Hi there" - missing actual footer content
|
||||
|
||||
#### 3. Error Handling & States
|
||||
- No loading indicators during data fetch
|
||||
- No error states for failed requests
|
||||
- No offline detection or handling
|
||||
- No retry mechanisms
|
||||
- No skeleton screens
|
||||
|
||||
#### 4. Accessibility Issues
|
||||
- No ARIA labels on interactive elements
|
||||
- No keyboard navigation support
|
||||
- No focus management
|
||||
- No screen reader announcements
|
||||
- Missing semantic HTML (divs instead of buttons/nav)
|
||||
- No skip navigation links
|
||||
|
||||
#### 5. Responsive Design Issues
|
||||
- Fixed 900px max-width with no proper mobile breakpoints
|
||||
- Grid layouts won't adapt to small screens
|
||||
- No touch-friendly tap targets
|
||||
- Font sizes not responsive
|
||||
|
||||
#### 6. Internationalization
|
||||
- All text hardcoded in English
|
||||
- No i18n framework or translation system
|
||||
- No locale-aware date/time formatting
|
||||
- No RTL language support
|
||||
|
||||
#### 7. Missing Infrastructure
|
||||
- No configuration system for API endpoints
|
||||
- No analytics integration
|
||||
- No performance monitoring
|
||||
- No PWA capabilities
|
||||
- No export functionality
|
||||
- No proper TypeScript interfaces for data models
|
||||
- **No tests whatsoever** despite test infrastructure
|
||||
|
||||
### Production Requirements Summary
|
||||
To make these components production-ready requires implementing:
|
||||
1. Complete data layer with API client
|
||||
2. State management system
|
||||
3. All missing UI functionality
|
||||
4. Comprehensive error handling
|
||||
5. Full accessibility compliance
|
||||
6. Proper responsive design
|
||||
7. Internationalization support
|
||||
8. Authentication/authorization
|
||||
9. Real-time update capabilities
|
||||
10. Comprehensive test suite
|
||||
|
||||
## Recent Updates (Post v1.0.74)
|
||||
|
||||
### Component Order (Top to Bottom)
|
||||
1. `upl-statuspage-header` - Navigation header
|
||||
2. `upl-statuspage-statusbar` - Overall system status
|
||||
3. `upl-statuspage-statsgrid` - Key metrics grid (uptime, response time, incidents)
|
||||
4. `upl-statuspage-assetsselector` - Service selection
|
||||
5. `upl-statuspage-statusdetails` - 48-hour status visualization
|
||||
6. `upl-statuspage-statusmonth` - Monthly calendar view
|
||||
7. `upl-statuspage-incidents` - Current and past incidents
|
||||
8. `upl-statuspage-footer` - Page footer
|
||||
|
||||
### Components Made Production-Ready
|
||||
All components have been significantly enhanced with the following improvements:
|
||||
|
||||
1. **upl-statuspage-header**
|
||||
- Added properties: showReportButton, showSubscribeButton, brandColor, logoUrl, customStyles, loading
|
||||
- Supports custom branding with dynamic colors
|
||||
- Loading state with skeleton animation
|
||||
- Configurable button visibility
|
||||
|
||||
2. **upl-statuspage-statusbar**
|
||||
- Already production-ready with full functionality
|
||||
- Supports all status states (operational, degraded, partial_outage, major_outage, maintenance)
|
||||
- Loading state and expandable behavior
|
||||
|
||||
3. **upl-statuspage-assetsselector**
|
||||
- Complete implementation with service selection grid
|
||||
- Full filtering capabilities (text, category, selected-only)
|
||||
- Select all/none functionality
|
||||
- Real-time status updates
|
||||
- Event emissions for selection changes
|
||||
- Loading states and empty states
|
||||
|
||||
4. **upl-statuspage-statusdetails**
|
||||
- Hourly status bars with tooltips
|
||||
- Skeleton loading states
|
||||
- Real-time data updates
|
||||
- Important: Expects hourly-aligned timestamps in data
|
||||
|
||||
5. **upl-statuspage-statusmonth**
|
||||
- Calendar grid display with status colors
|
||||
- Weekday labels and proper month alignment
|
||||
- Hover tooltips with detailed information
|
||||
- Day click events
|
||||
|
||||
6. **upl-statuspage-incidents**
|
||||
- Full incident management with current/past incidents
|
||||
- Multiple incident statuses (investigating, identified, monitoring, resolved, postmortem)
|
||||
- Incident updates timeline
|
||||
- Affected services display
|
||||
- Root cause and resolution information
|
||||
|
||||
7. **upl-statuspage-footer** (Completely rebuilt)
|
||||
- Comprehensive footer implementation with all expected properties
|
||||
- Social media links with SVG icons (Twitter, GitHub, LinkedIn, Facebook, YouTube, Instagram, Slack, Discord)
|
||||
- Subscribe/Report issue functionality
|
||||
- Language selector and theme toggle
|
||||
- Whitelabel support
|
||||
- Custom branding options
|
||||
- Loading and error states
|
||||
- RSS feed and API status links
|
||||
- Last updated timestamp with relative formatting
|
||||
|
||||
8. **upl-statuspage-statsgrid** (NEW)
|
||||
- Displays key performance metrics in a responsive grid
|
||||
- Shows current status with color indicator
|
||||
- Uptime percentage with configurable time period
|
||||
- Average response time with performance trend indicators
|
||||
- Total incidents count with affected services
|
||||
- Loading state with skeleton animation
|
||||
- Responsive design that stacks on mobile
|
||||
- Used stats data that was previously in statusdetails component
|
||||
|
||||
### Demo Architecture
|
||||
- All demos have been updated to use dees-demowrapper with runAfterRender callbacks
|
||||
- Properties are set dynamically on elements within runAfterRender
|
||||
- Multiple demo sections show different use cases and states
|
||||
- Event logging demonstrates interactivity
|
||||
- Demos can be instrumented with multiple wrappers for different scenarios
|
||||
|
||||
### Interfaces Implemented
|
||||
Created comprehensive TypeScript interfaces in ts_web/interfaces/index.ts:
|
||||
- IServiceStatus - Service monitoring data
|
||||
- IOverallStatus - Overall system status
|
||||
- IIncidentUpdate - Incident update entries
|
||||
- IIncidentDetails - Full incident information
|
||||
- IMonthlyUptime - Monthly uptime calendar data
|
||||
- IStatusHistoryPoint - Hourly status data points
|
||||
- IStatusPageConfig - Configuration options
|
||||
|
||||
### Remaining Tasks
|
||||
- Integration with actual UptimeLink API
|
||||
- WebSocket/SSE for real-time updates
|
||||
- Authentication/authorization implementation
|
||||
- Accessibility improvements (ARIA labels, keyboard navigation)
|
||||
- More comprehensive responsive design
|
||||
- Internationalization system
|
||||
- Unit and integration tests
|
||||
|
||||
### Important Fix Applied
|
||||
The `dees-demowrapper` component was not functioning because it wasn't being imported. Fixed by adding:
|
||||
```typescript
|
||||
import '@design.estate/dees-wcctools/demotools';
|
||||
```
|
||||
to `html/index.ts`. This registers the `dees-demowrapper` custom element which properly executes the `runAfterRender` callbacks in demos.
|
||||
402
readme.md
402
readme.md
@@ -1,32 +1,382 @@
|
||||
# @uptimelink/private/catalog
|
||||
a catalog with webcomponents for uptimelink dashboard
|
||||
# @uptime.link/statuspage
|
||||
|
||||
## Availabililty and Links
|
||||
* [npmjs.org (npm package)](https://www.npmjs.com/package/@uptimelink_private/catalog)
|
||||
* [gitlab.com (source)](https://gitlab.com/uptimelink/private/catalog)
|
||||
* [github.com (source mirror)](https://github.com/uptimelink/private/catalog)
|
||||
* [docs (typedoc)](https://uptimelink/private.gitlab.io/catalog/)
|
||||
🚀 **A powerful collection of web components for building stunning status pages** — because your users deserve to know what's happening, and you deserve to look good while telling them.
|
||||
|
||||
## Status for master
|
||||
Built with [Lit](https://lit.dev/) and TypeScript, these components are designed to be composable, customizable, and production-ready out of the box.
|
||||
|
||||
Status Category | Status Badge
|
||||
-- | --
|
||||
GitLab Pipelines | [](https://lossless.cloud)
|
||||
GitLab Pipline Test Coverage | [](https://lossless.cloud)
|
||||
npm | [](https://lossless.cloud)
|
||||
Snyk | [](https://lossless.cloud)
|
||||
TypeScript Support | [](https://lossless.cloud)
|
||||
node Support | [](https://nodejs.org/dist/latest-v10.x/docs/api/)
|
||||
Code Style | [](https://lossless.cloud)
|
||||
PackagePhobia (total standalone install weight) | [](https://lossless.cloud)
|
||||
PackagePhobia (package size on registry) | [](https://lossless.cloud)
|
||||
BundlePhobia (total size when bundled) | [](https://lossless.cloud)
|
||||
## Issue Reporting and Security
|
||||
|
||||
## Usage
|
||||
For reporting bugs, issues, or security vulnerabilities, please visit [community.foss.global/](https://community.foss.global/). This is the central community hub for all issue reporting. Developers who sign and comply with our contribution agreement and go through identification can also get a [code.foss.global/](https://code.foss.global/) account to submit Pull Requests directly.
|
||||
|
||||
Use TypeScript for best in class intellisense
|
||||
For further information read the linked docs at the top of this readme.
|
||||
---
|
||||
|
||||
## Legal
|
||||
> UNLICENSED licensed | **©** [Task Venture Capital GmbH](https://task.vc)
|
||||
| By using this npm module you agree to our [privacy policy](https://lossless.gmbH/privacy)
|
||||
## ✨ Features
|
||||
|
||||
- 🎨 **Themeable** — Automatic light/dark mode support with CSS custom properties
|
||||
- 📱 **Responsive** — Looks great on everything from mobile to ultrawide
|
||||
- 🔌 **Composable** — Use components individually or build complete status pages
|
||||
- 🎯 **Type-Safe** — Full TypeScript support with exported interfaces
|
||||
- ⚡ **Performant** — Lit-based components with minimal overhead
|
||||
- 🎭 **Pre-built Pages** — Demo, all-green, outage, and maintenance page templates included
|
||||
|
||||
---
|
||||
|
||||
## 📦 Installation
|
||||
|
||||
```bash
|
||||
npm install @uptime.link/statuspage
|
||||
```
|
||||
|
||||
Or with pnpm:
|
||||
|
||||
```bash
|
||||
pnpm add @uptime.link/statuspage
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🚀 Quick Start
|
||||
|
||||
Import the components and start building:
|
||||
|
||||
```typescript
|
||||
import '@uptime.link/statuspage';
|
||||
```
|
||||
|
||||
Then use them in your HTML:
|
||||
|
||||
```html
|
||||
<upl-statuspage-header></upl-statuspage-header>
|
||||
<upl-statuspage-statusbar></upl-statuspage-statusbar>
|
||||
<upl-statuspage-incidents></upl-statuspage-incidents>
|
||||
<upl-statuspage-footer></upl-statuspage-footer>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🧩 Components
|
||||
|
||||
### `<upl-statuspage-header>`
|
||||
|
||||
The main navigation header with branding and action buttons.
|
||||
|
||||
| Property | Type | Default | Description |
|
||||
|----------|------|---------|-------------|
|
||||
| `pageTitle` | `string` | `'Status'` | Title displayed in the header |
|
||||
| `logoUrl` | `string` | `''` | URL to your logo image |
|
||||
| `showReportButton` | `boolean` | `false` | Show "Report Incident" button |
|
||||
| `showSubscribeButton` | `boolean` | `false` | Show "Subscribe" button |
|
||||
|
||||
**Events:**
|
||||
- `reportNewIncident` — Fired when report button is clicked
|
||||
- `statusSubscribe` — Fired when subscribe button is clicked
|
||||
|
||||
---
|
||||
|
||||
### `<upl-statuspage-pagetitle>`
|
||||
|
||||
A hero section with title and subtitle.
|
||||
|
||||
| Property | Type | Default | Description |
|
||||
|----------|------|---------|-------------|
|
||||
| `pageTitle` | `string` | `'System Status'` | Main heading |
|
||||
| `pageSubtitle` | `string` | `''` | Optional description text |
|
||||
| `centered` | `boolean` | `false` | Center-align the content |
|
||||
|
||||
---
|
||||
|
||||
### `<upl-statuspage-statusbar>`
|
||||
|
||||
The overall status indicator — the heart of any status page.
|
||||
|
||||
| Property | Type | Default | Description |
|
||||
|----------|------|---------|-------------|
|
||||
| `overallStatus` | `IOverallStatus` | — | Current system status object |
|
||||
| `loading` | `boolean` | `false` | Show loading skeleton |
|
||||
|
||||
The status object supports: `operational`, `degraded`, `partial_outage`, `major_outage`, `maintenance`
|
||||
|
||||
---
|
||||
|
||||
### `<upl-statuspage-statsgrid>`
|
||||
|
||||
Key metrics at a glance — uptime, response time, incidents.
|
||||
|
||||
| Property | Type | Default | Description |
|
||||
|----------|------|---------|-------------|
|
||||
| `currentStatus` | `string` | `'operational'` | Current status indicator |
|
||||
| `uptime` | `number` | `100` | Uptime percentage |
|
||||
| `avgResponseTime` | `number` | `0` | Average response time (ms) |
|
||||
| `totalIncidents` | `number` | `0` | Incident count |
|
||||
| `affectedServices` | `number` | `0` | Currently affected services |
|
||||
| `totalServices` | `number` | `0` | Total monitored services |
|
||||
| `timePeriod` | `string` | `'30 days'` | Time range label |
|
||||
| `loading` | `boolean` | `false` | Show loading skeleton |
|
||||
|
||||
---
|
||||
|
||||
### `<upl-statuspage-assetsselector>`
|
||||
|
||||
Interactive service selector with filtering and search.
|
||||
|
||||
| Property | Type | Default | Description |
|
||||
|----------|------|---------|-------------|
|
||||
| `services` | `IServiceStatus[]` | `[]` | Array of service objects |
|
||||
| `filterText` | `string` | `''` | Search filter text |
|
||||
| `filterCategory` | `string` | `'all'` | Category filter |
|
||||
| `showOnlySelected` | `boolean` | `false` | Show only selected services |
|
||||
| `loading` | `boolean` | `false` | Show loading state |
|
||||
| `expanded` | `boolean` | `false` | Expand the selector panel |
|
||||
|
||||
**Events:**
|
||||
- `selectionChanged` — Fired with `{ selectedServices: string[] }` when selection changes
|
||||
|
||||
---
|
||||
|
||||
### `<upl-statuspage-statusdetails>`
|
||||
|
||||
Hourly status timeline visualization.
|
||||
|
||||
| Property | Type | Default | Description |
|
||||
|----------|------|---------|-------------|
|
||||
| `dataPoints` | `IStatusHistoryPoint[]` | `[]` | Hourly status data |
|
||||
| `historyData` | `IStatusHistoryPoint[]` | `[]` | Alternative data property |
|
||||
| `serviceId` | `string` | `''` | Service identifier |
|
||||
| `serviceName` | `string` | `'Service'` | Display name |
|
||||
| `hoursToShow` | `number` | `48` | Number of hours to display |
|
||||
| `loading` | `boolean` | `false` | Show loading skeleton |
|
||||
|
||||
**Events:**
|
||||
- `barClick` — Fired with `{ timestamp, status, responseTime, serviceId }` on bar click
|
||||
|
||||
---
|
||||
|
||||
### `<upl-statuspage-statusmonth>`
|
||||
|
||||
Calendar-style monthly uptime visualization.
|
||||
|
||||
| Property | Type | Default | Description |
|
||||
|----------|------|---------|-------------|
|
||||
| `monthlyData` | `IMonthlyUptime[]` | `[]` | Monthly uptime data |
|
||||
| `serviceId` | `string` | `'all'` | Service identifier |
|
||||
| `serviceName` | `string` | `'All Services'` | Display name |
|
||||
| `loading` | `boolean` | `false` | Show loading skeleton |
|
||||
|
||||
**Events:**
|
||||
- `dayClick` — Fired with day details when a day cell is clicked
|
||||
|
||||
---
|
||||
|
||||
### `<upl-statuspage-incidents>`
|
||||
|
||||
Incident feed with expandable details and status updates.
|
||||
|
||||
| Property | Type | Default | Description |
|
||||
|----------|------|---------|-------------|
|
||||
| `currentIncidents` | `IIncidentDetails[]` | `[]` | Active incidents |
|
||||
| `pastIncidents` | `IIncidentDetails[]` | `[]` | Resolved incidents |
|
||||
| `maxPastIncidents` | `number` | `10` | Max past incidents to show |
|
||||
| `loading` | `boolean` | `false` | Show loading skeleton |
|
||||
| `enableSubscription` | `boolean` | `false` | Allow incident subscriptions |
|
||||
| `subscribedIncidentIds` | `string[]` | `[]` | Pre-subscribed incident IDs |
|
||||
|
||||
**Events:**
|
||||
- `subscribeToIncident` — Fired with incident ID on subscription toggle
|
||||
|
||||
---
|
||||
|
||||
### `<upl-statuspage-footer>`
|
||||
|
||||
Full-featured footer with links, social icons, and subscription.
|
||||
|
||||
| Property | Type | Default | Description |
|
||||
|----------|------|---------|-------------|
|
||||
| `companyName` | `string` | `''` | Company name |
|
||||
| `legalUrl` | `string` | `''` | Terms/legal page URL |
|
||||
| `supportEmail` | `string` | `''` | Support email address |
|
||||
| `statusPageUrl` | `string` | `''` | Status page URL |
|
||||
| `lastUpdated` | `number` | — | Last update timestamp |
|
||||
| `currentYear` | `number` | `2024` | Copyright year |
|
||||
| `socialLinks` | `ISocialLink[]` | `[]` | Social media links |
|
||||
| `rssFeedUrl` | `string` | `''` | RSS feed URL |
|
||||
| `apiStatusUrl` | `string` | `''` | Status API URL |
|
||||
| `enableSubscribe` | `boolean` | `false` | Show subscribe button |
|
||||
| `subscriberCount` | `number` | `0` | Display subscriber count |
|
||||
| `additionalLinks` | `IFooterLink[]` | `[]` | Extra footer links |
|
||||
|
||||
**Events:**
|
||||
- `subscribeClick` — Fired when subscribe button is clicked
|
||||
|
||||
---
|
||||
|
||||
## 📐 Interfaces
|
||||
|
||||
All TypeScript interfaces are exported for type safety:
|
||||
|
||||
```typescript
|
||||
import type {
|
||||
IServiceStatus,
|
||||
IStatusHistoryPoint,
|
||||
IIncidentDetails,
|
||||
IIncidentUpdate,
|
||||
IMonthlyUptime,
|
||||
IUptimeDay,
|
||||
IOverallStatus,
|
||||
IStatusPageConfig,
|
||||
ISubscription
|
||||
} from '@uptime.link/statuspage';
|
||||
```
|
||||
|
||||
### Key Interfaces
|
||||
|
||||
```typescript
|
||||
interface IServiceStatus {
|
||||
id: string;
|
||||
name: string;
|
||||
displayName: string;
|
||||
description?: string;
|
||||
currentStatus: 'operational' | 'degraded' | 'partial_outage' | 'major_outage' | 'maintenance';
|
||||
lastChecked: number;
|
||||
uptime30d: number;
|
||||
uptime90d: number;
|
||||
responseTime: number;
|
||||
category?: string;
|
||||
selected?: boolean;
|
||||
}
|
||||
|
||||
interface IIncidentDetails {
|
||||
id: string;
|
||||
title: string;
|
||||
status: 'investigating' | 'identified' | 'monitoring' | 'resolved' | 'postmortem';
|
||||
severity: 'critical' | 'major' | 'minor' | 'maintenance';
|
||||
affectedServices: string[];
|
||||
startTime: number;
|
||||
endTime?: number;
|
||||
updates: IIncidentUpdate[];
|
||||
impact: string;
|
||||
rootCause?: string;
|
||||
resolution?: string;
|
||||
}
|
||||
|
||||
interface IOverallStatus {
|
||||
status: 'operational' | 'degraded' | 'partial_outage' | 'major_outage' | 'maintenance';
|
||||
message: string;
|
||||
lastUpdated: number;
|
||||
affectedServices: number;
|
||||
totalServices: number;
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🎬 Complete Example
|
||||
|
||||
Build a full status page in minutes:
|
||||
|
||||
```typescript
|
||||
import '@uptime.link/statuspage';
|
||||
import type { IServiceStatus, IOverallStatus, IIncidentDetails } from '@uptime.link/statuspage';
|
||||
|
||||
// Get your components
|
||||
const header = document.querySelector('upl-statuspage-header');
|
||||
const statusBar = document.querySelector('upl-statuspage-statusbar');
|
||||
const statsGrid = document.querySelector('upl-statuspage-statsgrid');
|
||||
const incidents = document.querySelector('upl-statuspage-incidents');
|
||||
const footer = document.querySelector('upl-statuspage-footer');
|
||||
|
||||
// Configure the header
|
||||
header.pageTitle = 'Acme Cloud';
|
||||
header.logoUrl = '/logo.svg';
|
||||
header.showSubscribeButton = true;
|
||||
|
||||
// Set overall status
|
||||
statusBar.overallStatus = {
|
||||
status: 'operational',
|
||||
message: 'All systems operational',
|
||||
lastUpdated: Date.now(),
|
||||
affectedServices: 0,
|
||||
totalServices: 12
|
||||
};
|
||||
|
||||
// Configure stats
|
||||
statsGrid.uptime = 99.98;
|
||||
statsGrid.avgResponseTime = 45;
|
||||
statsGrid.totalServices = 12;
|
||||
|
||||
// Add incidents
|
||||
incidents.currentIncidents = []; // No current incidents
|
||||
incidents.pastIncidents = [
|
||||
{
|
||||
id: 'inc-001',
|
||||
title: 'Scheduled Maintenance Complete',
|
||||
status: 'resolved',
|
||||
severity: 'maintenance',
|
||||
impact: 'Database maintenance window',
|
||||
affectedServices: ['Database'],
|
||||
startTime: Date.now() - 86400000,
|
||||
endTime: Date.now() - 82800000,
|
||||
updates: [
|
||||
{
|
||||
id: 'upd-1',
|
||||
timestamp: Date.now() - 82800000,
|
||||
status: 'resolved',
|
||||
message: 'Maintenance completed successfully'
|
||||
}
|
||||
]
|
||||
}
|
||||
];
|
||||
|
||||
// Configure footer
|
||||
footer.companyName = 'Acme Cloud';
|
||||
footer.supportEmail = 'support@acme.cloud';
|
||||
footer.enableSubscribe = true;
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📄 Pre-built Page Templates
|
||||
|
||||
The package includes ready-to-use page templates:
|
||||
|
||||
```typescript
|
||||
import {
|
||||
statuspageDemo, // Full demo with sample data
|
||||
statuspageAllgreen, // All systems operational
|
||||
statuspageOutage, // Major outage scenario
|
||||
statuspageMaintenance // Scheduled maintenance
|
||||
} from '@uptime.link/statuspage/pages';
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🎨 Theming
|
||||
|
||||
Components automatically adapt to light/dark mode using `cssManager.bdTheme()`. The design follows a modern, minimal aesthetic with:
|
||||
|
||||
- Clean typography with `-apple-system, BlinkMacSystemFont, 'Segoe UI'` font stack
|
||||
- Subtle shadows and borders
|
||||
- Semantic status colors (green, yellow, orange, red, blue)
|
||||
- Smooth transitions and hover states
|
||||
|
||||
---
|
||||
|
||||
## License and Legal Information
|
||||
|
||||
This repository contains open-source code licensed under the MIT License. A copy of the license can be found in the [LICENSE](./LICENSE) file.
|
||||
|
||||
**Please note:** The MIT License does not grant permission to use the trade names, trademarks, service marks, or product names of the project, except as required for reasonable and customary use in describing the origin of the work and reproducing the content of the NOTICE file.
|
||||
|
||||
### Trademarks
|
||||
|
||||
This project is owned and maintained by Task Venture Capital GmbH. The names and logos associated with Task Venture Capital GmbH and any related products or services are trademarks of Task Venture Capital GmbH or third parties, and are not included within the scope of the MIT license granted herein.
|
||||
|
||||
Use of these trademarks must comply with Task Venture Capital GmbH's Trademark Guidelines or the guidelines of the respective third-party owners, and any usage must be approved in writing. Third-party trademarks used herein are the property of their respective owners and used only in a descriptive manner, e.g. for an implementation of an API or similar.
|
||||
|
||||
### Company Information
|
||||
|
||||
Task Venture Capital GmbH
|
||||
Registered at District Court Bremen HRB 35230 HB, Germany
|
||||
|
||||
For any legal inquiries or further information, please contact us via email at hello@task.vc.
|
||||
|
||||
By using this repository, you acknowledge that you have read this section, agree to comply with its terms, and understand that the licensing of the code does not imply endorsement by Task Venture Capital GmbH of any derivative works.
|
||||
|
||||
261
readme.plan.md
Normal file
261
readme.plan.md
Normal file
@@ -0,0 +1,261 @@
|
||||
# Production-Ready Elements Implementation Plan
|
||||
|
||||
## First: Reread CLAUDE.md guidelines
|
||||
|
||||
## Overview
|
||||
Transform the @uptime.link/statuspage components from UI shells into fully functional, production-ready web components with real data integration, proper error handling, accessibility, and comprehensive testing.
|
||||
|
||||
## Phase 1: Core Infrastructure (Foundation)
|
||||
|
||||
### 1.1 Data Layer & API Client
|
||||
- [ ] Create `ts_web/services/api.client.ts` for API communication
|
||||
- [ ] Implement authentication/token management
|
||||
- [ ] Add request/response interceptors for error handling
|
||||
- [ ] Create retry logic with exponential backoff
|
||||
- [ ] Add request caching mechanism
|
||||
|
||||
### 1.2 TypeScript Interfaces & Models
|
||||
- [ ] Create `ts_web/interfaces/` directory
|
||||
- [ ] Define comprehensive interfaces for:
|
||||
- Service status data
|
||||
- Incident details with severity levels
|
||||
- Asset/service definitions
|
||||
- API responses
|
||||
- Configuration options
|
||||
- User preferences
|
||||
|
||||
### 1.3 State Management
|
||||
- [ ] Create `ts_web/services/state.manager.ts`
|
||||
- [ ] Implement observable state pattern
|
||||
- [ ] Add state persistence (localStorage)
|
||||
- [ ] Create state update notifications
|
||||
|
||||
### 1.4 Real-time Updates
|
||||
- [ ] Create `ts_web/services/realtime.service.ts`
|
||||
- [ ] Implement WebSocket connection management
|
||||
- [ ] Add fallback to Server-Sent Events (SSE)
|
||||
- [ ] Create reconnection logic
|
||||
- [ ] Add heartbeat/ping mechanism
|
||||
|
||||
### 1.5 Configuration System
|
||||
- [ ] Create `ts_web/config/default.config.ts`
|
||||
- [ ] Add environment-based configuration
|
||||
- [ ] Implement config validation
|
||||
- [ ] Add runtime config updates
|
||||
|
||||
## Phase 2: Component Implementation
|
||||
|
||||
### 2.1 upl-statuspage-header
|
||||
- [ ] Add loading state during actions
|
||||
- [ ] Implement proper event handling with data
|
||||
- [ ] Add keyboard shortcuts (Alt+R for report, Alt+S for subscribe)
|
||||
- [ ] Add ARIA labels and roles
|
||||
- [ ] Implement focus management
|
||||
|
||||
### 2.2 upl-statuspage-statusbar
|
||||
- [ ] Connect to real status data
|
||||
- [ ] Implement dynamic status calculation
|
||||
- [ ] Add status levels (operational, degraded, partial outage, major outage)
|
||||
- [ ] Color coding (green, yellow, orange, red)
|
||||
- [ ] Add animated transitions between states
|
||||
- [ ] Implement click to expand details
|
||||
- [ ] Add ARIA live region for status changes
|
||||
|
||||
### 2.3 upl-statuspage-assetsselector
|
||||
- [ ] Implement complete asset listing UI
|
||||
- [ ] Add search/filter functionality
|
||||
- [ ] Create checkbox/toggle selection
|
||||
- [ ] Add select all/none buttons
|
||||
- [ ] Implement category grouping
|
||||
- [ ] Add asset status indicators
|
||||
- [ ] Emit selection change events
|
||||
- [ ] Add keyboard navigation (arrow keys)
|
||||
|
||||
### 2.4 upl-statuspage-statusdetails
|
||||
- [ ] Connect to real hourly status data
|
||||
- [ ] Implement dynamic color coding
|
||||
- [ ] Add hover tooltips with exact times
|
||||
- [ ] Create time zone support
|
||||
- [ ] Add zoom in/out functionality
|
||||
- [ ] Implement data aggregation options
|
||||
- [ ] Add export to CSV/JSON
|
||||
- [ ] Make responsive for mobile
|
||||
|
||||
### 2.5 upl-statuspage-statusmonth
|
||||
- [ ] Connect to real daily uptime data
|
||||
- [ ] Add month/year navigation
|
||||
- [ ] Implement uptime percentage calculation
|
||||
- [ ] Color coding by uptime percentage
|
||||
- [ ] Add detailed day tooltips
|
||||
- [ ] Create calendar grid with proper labels
|
||||
- [ ] Add click to drill down
|
||||
- [ ] Implement date range selection
|
||||
|
||||
### 2.6 upl-statuspage-incidents
|
||||
- [ ] Create incident card component
|
||||
- [ ] Implement incident rendering from data
|
||||
- [ ] Add severity indicators (critical, major, minor)
|
||||
- [ ] Create incident timeline
|
||||
- [ ] Add affected services display
|
||||
- [ ] Implement status updates (investigating, identified, monitoring, resolved)
|
||||
- [ ] Add time calculations (duration, time to resolution)
|
||||
- [ ] Create incident filtering/search
|
||||
- [ ] Add pagination for historical incidents
|
||||
|
||||
### 2.7 upl-statuspage-footer
|
||||
- [ ] Implement configurable footer content
|
||||
- [ ] Add RSS feed link
|
||||
- [ ] Create API status endpoint link
|
||||
- [ ] Add social media links
|
||||
- [ ] Implement "Report Incident" modal
|
||||
- [ ] Create "Subscribe" functionality
|
||||
- [ ] Add language selector
|
||||
- [ ] Include last update timestamp
|
||||
|
||||
## Phase 3: User Experience Enhancements
|
||||
|
||||
### 3.1 Loading States
|
||||
- [ ] Create skeleton screens for each component
|
||||
- [ ] Add loading spinners/indicators
|
||||
- [ ] Implement progressive loading
|
||||
- [ ] Add loading progress for large datasets
|
||||
|
||||
### 3.2 Error Handling
|
||||
- [ ] Create error boundary components
|
||||
- [ ] Design error state UI for each component
|
||||
- [ ] Add retry buttons
|
||||
- [ ] Implement offline detection
|
||||
- [ ] Create fallback content
|
||||
- [ ] Add error logging/reporting
|
||||
|
||||
### 3.3 Accessibility (WCAG 2.1 AA)
|
||||
- [ ] Add comprehensive ARIA labels
|
||||
- [ ] Implement keyboard navigation
|
||||
- [ ] Create skip navigation links
|
||||
- [ ] Add focus indicators
|
||||
- [ ] Implement screen reader announcements
|
||||
- [ ] Ensure color contrast compliance
|
||||
- [ ] Add reduced motion support
|
||||
|
||||
### 3.4 Responsive Design
|
||||
- [ ] Create mobile breakpoints
|
||||
- [ ] Implement touch-friendly interactions
|
||||
- [ ] Add swipe gestures for navigation
|
||||
- [ ] Create responsive typography
|
||||
- [ ] Optimize layouts for tablets
|
||||
- [ ] Add horizontal scroll prevention
|
||||
|
||||
### 3.5 Internationalization
|
||||
- [ ] Create i18n service
|
||||
- [ ] Add translation files (en, de, es, fr, ja)
|
||||
- [ ] Implement locale detection
|
||||
- [ ] Add date/time formatting
|
||||
- [ ] Create number formatting
|
||||
- [ ] Add RTL support
|
||||
- [ ] Implement pluralization rules
|
||||
|
||||
## Phase 4: Advanced Features
|
||||
|
||||
### 4.1 Performance Optimization
|
||||
- [ ] Implement virtual scrolling for long lists
|
||||
- [ ] Add lazy loading for historical data
|
||||
- [ ] Create data pagination
|
||||
- [ ] Implement request debouncing
|
||||
- [ ] Add response caching
|
||||
- [ ] Optimize re-renders
|
||||
|
||||
### 4.2 Analytics & Monitoring
|
||||
- [ ] Add page view tracking
|
||||
- [ ] Implement user interaction tracking
|
||||
- [ ] Create performance metrics
|
||||
- [ ] Add error tracking
|
||||
- [ ] Implement custom event tracking
|
||||
|
||||
### 4.3 PWA Capabilities
|
||||
- [ ] Create service worker
|
||||
- [ ] Implement offline support
|
||||
- [ ] Add push notifications
|
||||
- [ ] Create app manifest
|
||||
- [ ] Enable installation prompt
|
||||
|
||||
### 4.4 Export & Reporting
|
||||
- [ ] Add PDF export for status reports
|
||||
- [ ] Create CSV export for data
|
||||
- [ ] Implement scheduled reports
|
||||
- [ ] Add print stylesheets
|
||||
- [ ] Create shareable status links
|
||||
|
||||
## Phase 5: Testing & Documentation
|
||||
|
||||
### 5.1 Unit Tests
|
||||
- [ ] Set up testing framework (@git.zone/tstest)
|
||||
- [ ] Create tests for all services
|
||||
- [ ] Test component logic
|
||||
- [ ] Add API client tests
|
||||
- [ ] Test state management
|
||||
- [ ] Create test utilities
|
||||
|
||||
### 5.2 Integration Tests
|
||||
- [ ] Test component interactions
|
||||
- [ ] Test data flow
|
||||
- [ ] Test error scenarios
|
||||
- [ ] Test real-time updates
|
||||
- [ ] Test offline behavior
|
||||
|
||||
### 5.3 E2E Tests
|
||||
- [ ] Set up Playwright
|
||||
- [ ] Test user workflows
|
||||
- [ ] Test accessibility
|
||||
- [ ] Test responsive behavior
|
||||
- [ ] Test cross-browser compatibility
|
||||
|
||||
### 5.4 Documentation
|
||||
- [ ] Create component API documentation
|
||||
- [ ] Add usage examples
|
||||
- [ ] Create integration guide
|
||||
- [ ] Add configuration documentation
|
||||
- [ ] Create troubleshooting guide
|
||||
|
||||
## Implementation Order
|
||||
|
||||
1. **Week 1-2**: Core Infrastructure (Phase 1)
|
||||
2. **Week 3-4**: Basic Component Functionality (Phase 2.1-2.3)
|
||||
3. **Week 5-6**: Advanced Components (Phase 2.4-2.7)
|
||||
4. **Week 7**: User Experience (Phase 3)
|
||||
5. **Week 8**: Advanced Features (Phase 4)
|
||||
6. **Week 9-10**: Testing & Documentation (Phase 5)
|
||||
|
||||
## Success Criteria
|
||||
|
||||
- All components display real data from API
|
||||
- Full accessibility compliance (WCAG 2.1 AA)
|
||||
- 90%+ test coverage
|
||||
- Sub-3 second initial load time
|
||||
- Works offline with cached data
|
||||
- Supports 5+ languages
|
||||
- Mobile-responsive design
|
||||
- Real-time updates working
|
||||
- Comprehensive error handling
|
||||
- Production-ready documentation
|
||||
|
||||
## Dependencies to Add
|
||||
|
||||
```json
|
||||
{
|
||||
"dependencies": {
|
||||
"@push.rocks/smartrequest": "*",
|
||||
"@push.rocks/smartwebsocket": "*",
|
||||
"@push.rocks/smartstate": "*",
|
||||
"@push.rocks/smarti18n": "*",
|
||||
"@push.rocks/smarttime": "*"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@git.zone/tstest": "*",
|
||||
"@playwright/test": "*"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
Would you like me to proceed with implementing this plan? I recommend starting with Phase 1 (Core Infrastructure) as it provides the foundation for all other functionality.
|
||||
84
test-demo-no-banners.html
Normal file
84
test-demo-no-banners.html
Normal file
@@ -0,0 +1,84 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Demo Pages Without Banners</title>
|
||||
<script type="module" src="./dist_bundle/bundle.js"></script>
|
||||
<style>
|
||||
body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
font-family: 'Geist Sans', -apple-system, BlinkMacSystemFont, sans-serif;
|
||||
background: #fafafa;
|
||||
}
|
||||
@media (prefers-color-scheme: dark) {
|
||||
body {
|
||||
background: #09090b;
|
||||
}
|
||||
}
|
||||
.page-selector {
|
||||
position: fixed;
|
||||
top: 20px;
|
||||
right: 20px;
|
||||
z-index: 100;
|
||||
background: white;
|
||||
padding: 10px;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
|
||||
}
|
||||
@media (prefers-color-scheme: dark) {
|
||||
.page-selector {
|
||||
background: #18181b;
|
||||
box-shadow: 0 2px 8px rgba(0,0,0,0.3);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="page-selector">
|
||||
<label for="demoSelect">Select Demo:</label>
|
||||
<select id="demoSelect">
|
||||
<option value="demo">Main Demo (Degraded)</option>
|
||||
<option value="allgreen">All Green</option>
|
||||
<option value="outage">Major Outage</option>
|
||||
<option value="maintenance">Maintenance</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div id="demoContainer"></div>
|
||||
|
||||
<script type="module">
|
||||
import { statuspageDemo } from './dist_ts_web/pages/statuspage-demo.js';
|
||||
import { statuspageAllgreen } from './dist_ts_web/pages/statuspage-allgreen.js';
|
||||
import { statuspageOutage } from './dist_ts_web/pages/statuspage-outage.js';
|
||||
import { statuspageMaintenance } from './dist_ts_web/pages/statuspage-maintenance.js';
|
||||
|
||||
const demoContainer = document.getElementById('demoContainer');
|
||||
const demoSelect = document.getElementById('demoSelect');
|
||||
|
||||
const demos = {
|
||||
demo: statuspageDemo,
|
||||
allgreen: statuspageAllgreen,
|
||||
outage: statuspageOutage,
|
||||
maintenance: statuspageMaintenance
|
||||
};
|
||||
|
||||
function loadDemo(demoName) {
|
||||
const demoFunc = demos[demoName];
|
||||
if (demoFunc) {
|
||||
demoContainer.innerHTML = '';
|
||||
const template = demoFunc();
|
||||
demoContainer.innerHTML = template.strings.join('');
|
||||
}
|
||||
}
|
||||
|
||||
demoSelect.addEventListener('change', (e) => {
|
||||
loadDemo(e.target.value);
|
||||
});
|
||||
|
||||
// Load initial demo
|
||||
loadDemo('demo');
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
132
test-footer-shadcn.html
Normal file
132
test-footer-shadcn.html
Normal file
@@ -0,0 +1,132 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Footer Component - shadcn Style</title>
|
||||
<script type="module" src="./dist_bundle/bundle.js"></script>
|
||||
<style>
|
||||
body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
font-family: 'Geist Sans', -apple-system, BlinkMacSystemFont, sans-serif;
|
||||
background: #fafafa;
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
@media (prefers-color-scheme: dark) {
|
||||
body {
|
||||
background: #09090b;
|
||||
}
|
||||
}
|
||||
main {
|
||||
flex: 1;
|
||||
padding: 40px;
|
||||
text-align: center;
|
||||
color: #6b7280;
|
||||
}
|
||||
@media (prefers-color-scheme: dark) {
|
||||
main {
|
||||
color: #a1a1aa;
|
||||
}
|
||||
}
|
||||
.demo-section {
|
||||
margin-top: auto;
|
||||
}
|
||||
h1 {
|
||||
margin-bottom: 20px;
|
||||
color: #0a0a0a;
|
||||
}
|
||||
@media (prefers-color-scheme: dark) {
|
||||
h1 {
|
||||
color: #fafafa;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<main>
|
||||
<h1>Footer Component with Full shadcn Styling</h1>
|
||||
<p>The footer below has been fully updated to embrace shadcn design principles:</p>
|
||||
<ul style="text-align: left; max-width: 600px; margin: 20px auto;">
|
||||
<li>Minimal design with subtle borders</li>
|
||||
<li>Consistent spacing and typography</li>
|
||||
<li>No transform effects, only color transitions</li>
|
||||
<li>Flat design with no gradients</li>
|
||||
<li>Proper muted colors for secondary text</li>
|
||||
<li>Clean hover states</li>
|
||||
<li>Simplified social icons in square containers</li>
|
||||
<li>Native-looking form controls</li>
|
||||
</ul>
|
||||
</main>
|
||||
|
||||
<div class="demo-section">
|
||||
<upl-statuspage-footer
|
||||
id="footer1"
|
||||
company-name="Example Corp"
|
||||
legal-url="https://example.com/legal"
|
||||
support-email="support@example.com"
|
||||
status-page-url="https://status.example.com"
|
||||
show-powered-by="true"
|
||||
enable-subscribe="true"
|
||||
show-api-link="true"
|
||||
rss-feed-url="https://status.example.com/rss"
|
||||
api-status-url="https://api.example.com/status"
|
||||
enable-language-selector="true"
|
||||
enable-theme-toggle="true"
|
||||
subscriber-count="1234"
|
||||
></upl-statuspage-footer>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
const footer = document.getElementById('footer1');
|
||||
|
||||
// Set social links
|
||||
footer.socialLinks = [
|
||||
{ platform: 'twitter', url: 'https://twitter.com/example' },
|
||||
{ platform: 'github', url: 'https://github.com/example' },
|
||||
{ platform: 'linkedin', url: 'https://linkedin.com/company/example' },
|
||||
{ platform: 'youtube', url: 'https://youtube.com/example' }
|
||||
];
|
||||
|
||||
// Set additional links
|
||||
footer.additionalLinks = [
|
||||
{ label: 'API Documentation', url: 'https://docs.example.com' },
|
||||
{ label: 'Privacy Policy', url: 'https://example.com/privacy' },
|
||||
{ label: 'Terms of Service', url: 'https://example.com/terms' }
|
||||
];
|
||||
|
||||
// Set supported languages
|
||||
footer.supportedLanguages = [
|
||||
{ code: 'en', name: 'English' },
|
||||
{ code: 'de', name: 'Deutsch' },
|
||||
{ code: 'fr', name: 'Français' },
|
||||
{ code: 'es', name: 'Español' }
|
||||
];
|
||||
|
||||
footer.currentLanguage = 'en';
|
||||
footer.lastUpdated = Date.now();
|
||||
footer.currentYear = new Date().getFullYear();
|
||||
|
||||
// Event listeners
|
||||
footer.addEventListener('subscribeClick', () => {
|
||||
alert('Subscribe clicked - would open subscription form');
|
||||
});
|
||||
|
||||
footer.addEventListener('reportIssueClick', () => {
|
||||
alert('Report issue clicked - would open issue form');
|
||||
});
|
||||
|
||||
footer.addEventListener('languageChange', (e) => {
|
||||
console.log('Language changed to:', e.detail.language);
|
||||
});
|
||||
|
||||
footer.addEventListener('themeToggle', () => {
|
||||
console.log('Theme toggle clicked');
|
||||
});
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
105
test-footer-subscribe.html
Normal file
105
test-footer-subscribe.html
Normal file
@@ -0,0 +1,105 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Footer Subscribe Button Test</title>
|
||||
<script type="module" src="./dist_bundle/bundle.js"></script>
|
||||
<style>
|
||||
body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
font-family: 'Geist Sans', -apple-system, BlinkMacSystemFont, sans-serif;
|
||||
background: #fafafa;
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
@media (prefers-color-scheme: dark) {
|
||||
body {
|
||||
background: #09090b;
|
||||
}
|
||||
}
|
||||
main {
|
||||
flex: 1;
|
||||
padding: 40px;
|
||||
text-align: center;
|
||||
}
|
||||
h1 {
|
||||
margin-bottom: 20px;
|
||||
color: #0a0a0a;
|
||||
}
|
||||
@media (prefers-color-scheme: dark) {
|
||||
h1 {
|
||||
color: #fafafa;
|
||||
}
|
||||
}
|
||||
.demo-section {
|
||||
margin-top: auto;
|
||||
}
|
||||
.comparison {
|
||||
display: flex;
|
||||
gap: 40px;
|
||||
justify-content: center;
|
||||
margin: 40px 0;
|
||||
}
|
||||
.example {
|
||||
text-align: center;
|
||||
}
|
||||
.example h3 {
|
||||
margin-bottom: 20px;
|
||||
color: #666;
|
||||
}
|
||||
@media (prefers-color-scheme: dark) {
|
||||
.example h3 {
|
||||
color: #a1a1aa;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<main>
|
||||
<h1>Footer Subscribe Button - Fixed Layout</h1>
|
||||
<p>The subscriber count is now displayed below the button instead of inside it:</p>
|
||||
|
||||
<div class="comparison">
|
||||
<div class="example">
|
||||
<h3>Without Subscribers</h3>
|
||||
<p>Button appears alone when subscriber count is 0</p>
|
||||
</div>
|
||||
<div class="example">
|
||||
<h3>With Subscribers</h3>
|
||||
<p>Count appears below the button as secondary text</p>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<div class="demo-section">
|
||||
<upl-statuspage-footer
|
||||
id="footer1"
|
||||
company-name="Example Corp"
|
||||
enable-subscribe="true"
|
||||
subscriber-count="1234"
|
||||
></upl-statuspage-footer>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
const footer = document.getElementById('footer1');
|
||||
|
||||
footer.addEventListener('subscribeClick', () => {
|
||||
alert('Subscribe button clicked! The subscriber count is displayed separately below the button.');
|
||||
});
|
||||
|
||||
// Demonstrate different subscriber counts
|
||||
let counts = [0, 42, 1234, 50000];
|
||||
let currentIndex = 0;
|
||||
|
||||
setInterval(() => {
|
||||
currentIndex = (currentIndex + 1) % counts.length;
|
||||
footer.subscriberCount = counts[currentIndex];
|
||||
}, 3000);
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
286
test-incident-subscriptions.html
Normal file
286
test-incident-subscriptions.html
Normal file
@@ -0,0 +1,286 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Incident Subscription Test</title>
|
||||
<script type="module" src="./dist_bundle/bundle.js"></script>
|
||||
<style>
|
||||
body {
|
||||
margin: 0;
|
||||
padding: 20px;
|
||||
font-family: 'Geist Sans', -apple-system, BlinkMacSystemFont, sans-serif;
|
||||
background: #fafafa;
|
||||
}
|
||||
@media (prefers-color-scheme: dark) {
|
||||
body {
|
||||
background: #09090b;
|
||||
}
|
||||
}
|
||||
.container {
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
h1 {
|
||||
margin-bottom: 20px;
|
||||
color: #0a0a0a;
|
||||
}
|
||||
@media (prefers-color-scheme: dark) {
|
||||
h1 {
|
||||
color: #fafafa;
|
||||
}
|
||||
}
|
||||
.info-panel {
|
||||
background: #f0f9ff;
|
||||
border: 1px solid #bae6fd;
|
||||
border-radius: 8px;
|
||||
padding: 20px;
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
@media (prefers-color-scheme: dark) {
|
||||
.info-panel {
|
||||
background: #0c4a6e;
|
||||
border-color: #0284c7;
|
||||
}
|
||||
}
|
||||
.subscription-log {
|
||||
background: #f3f4f6;
|
||||
border: 1px solid #e5e7eb;
|
||||
border-radius: 8px;
|
||||
padding: 20px;
|
||||
margin-top: 30px;
|
||||
font-family: monospace;
|
||||
font-size: 13px;
|
||||
max-height: 200px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
@media (prefers-color-scheme: dark) {
|
||||
.subscription-log {
|
||||
background: #1f2937;
|
||||
border-color: #374151;
|
||||
color: #e5e7eb;
|
||||
}
|
||||
}
|
||||
.notification {
|
||||
position: fixed;
|
||||
top: 20px;
|
||||
right: 20px;
|
||||
padding: 16px 24px;
|
||||
background: #10b981;
|
||||
color: white;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 4px 12px rgba(0,0,0,0.15);
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
animation: slideIn 0.3s ease;
|
||||
z-index: 1000;
|
||||
}
|
||||
@keyframes slideIn {
|
||||
from {
|
||||
transform: translateX(100%);
|
||||
opacity: 0;
|
||||
}
|
||||
to {
|
||||
transform: translateX(0);
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
.notification.unsubscribe {
|
||||
background: #6b7280;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<h1>Incident Subscription Feature Demo</h1>
|
||||
|
||||
<div class="info-panel">
|
||||
<h3>How Incident Subscriptions Work:</h3>
|
||||
<ul>
|
||||
<li>Each incident can be individually subscribed to</li>
|
||||
<li>Click on an incident to expand it and see the "Subscribe to updates" button</li>
|
||||
<li>Subscribed incidents show a checkmark and "Subscribed to updates" text</li>
|
||||
<li>Click again to unsubscribe</li>
|
||||
<li>The component emits events for subscribe/unsubscribe actions</li>
|
||||
<li>You can pre-set subscribed incidents using the <code>subscribedIncidentIds</code> property</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<upl-statuspage-incidents id="incidents"></upl-statuspage-incidents>
|
||||
|
||||
<div class="subscription-log" id="log">
|
||||
<strong>Event Log:</strong><br>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
const incidents = document.getElementById('incidents');
|
||||
const log = document.getElementById('log');
|
||||
|
||||
// Helper function to add log entries
|
||||
function addLog(message) {
|
||||
const timestamp = new Date().toLocaleTimeString();
|
||||
log.innerHTML += `[${timestamp}] ${message}<br>`;
|
||||
log.scrollTop = log.scrollHeight;
|
||||
}
|
||||
|
||||
// Helper function to show notifications
|
||||
function showNotification(message, type = 'subscribe') {
|
||||
const notification = document.createElement('div');
|
||||
notification.className = `notification ${type === 'unsubscribe' ? 'unsubscribe' : ''}`;
|
||||
notification.textContent = message;
|
||||
document.body.appendChild(notification);
|
||||
|
||||
setTimeout(() => {
|
||||
notification.style.opacity = '0';
|
||||
notification.style.transform = 'translateX(100%)';
|
||||
setTimeout(() => notification.remove(), 300);
|
||||
}, 3000);
|
||||
}
|
||||
|
||||
// Set up some test incidents
|
||||
const currentIncidents = [
|
||||
{
|
||||
id: 'inc-001',
|
||||
title: 'Database Connection Pool Exhaustion',
|
||||
status: 'monitoring',
|
||||
severity: 'major',
|
||||
affectedServices: ['API Gateway', 'User Service', 'Order Service'],
|
||||
startTime: Date.now() - 2 * 60 * 60 * 1000,
|
||||
impact: 'API response times are elevated. Some requests may timeout.',
|
||||
updates: [
|
||||
{
|
||||
id: 'upd-1',
|
||||
timestamp: Date.now() - 2 * 60 * 60 * 1000,
|
||||
status: 'investigating',
|
||||
message: 'We are investigating reports of slow API responses',
|
||||
author: 'Operations Team'
|
||||
},
|
||||
{
|
||||
id: 'upd-2',
|
||||
timestamp: Date.now() - 90 * 60 * 1000,
|
||||
status: 'identified',
|
||||
message: 'Database connection pool is at capacity due to increased traffic',
|
||||
author: 'Database Team'
|
||||
},
|
||||
{
|
||||
id: 'upd-3',
|
||||
timestamp: Date.now() - 30 * 60 * 1000,
|
||||
status: 'monitoring',
|
||||
message: 'Connection pool size has been increased. Monitoring for stability.',
|
||||
author: 'Operations Team'
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
id: 'inc-002',
|
||||
title: 'CDN Configuration Update',
|
||||
status: 'identified',
|
||||
severity: 'minor',
|
||||
affectedServices: ['CDN', 'Static Assets'],
|
||||
startTime: Date.now() - 45 * 60 * 1000,
|
||||
impact: 'Some static assets may load slowly in certain regions',
|
||||
updates: [
|
||||
{
|
||||
id: 'upd-4',
|
||||
timestamp: Date.now() - 45 * 60 * 1000,
|
||||
status: 'investigating',
|
||||
message: 'Investigating reports of slow asset loading',
|
||||
author: 'Network Team'
|
||||
},
|
||||
{
|
||||
id: 'upd-5',
|
||||
timestamp: Date.now() - 20 * 60 * 1000,
|
||||
status: 'identified',
|
||||
message: 'CDN configuration needs optimization for new edge locations',
|
||||
author: 'Network Team'
|
||||
}
|
||||
]
|
||||
}
|
||||
];
|
||||
|
||||
const pastIncidents = [
|
||||
{
|
||||
id: 'inc-003',
|
||||
title: 'Authentication Service Outage',
|
||||
status: 'resolved',
|
||||
severity: 'critical',
|
||||
affectedServices: ['Authentication', 'All Services'],
|
||||
startTime: Date.now() - 7 * 24 * 60 * 60 * 1000,
|
||||
endTime: Date.now() - 7 * 24 * 60 * 60 * 1000 + 2 * 60 * 60 * 1000,
|
||||
impact: 'Users were unable to log in',
|
||||
rootCause: 'Certificate expiration in auth service',
|
||||
resolution: 'Certificate renewed and monitoring alerts added',
|
||||
updates: [
|
||||
{
|
||||
id: 'upd-6',
|
||||
timestamp: Date.now() - 7 * 24 * 60 * 60 * 1000,
|
||||
status: 'investigating',
|
||||
message: 'Multiple reports of login failures',
|
||||
author: 'On-call Engineer'
|
||||
},
|
||||
{
|
||||
id: 'upd-7',
|
||||
timestamp: Date.now() - 7 * 24 * 60 * 60 * 1000 + 30 * 60 * 1000,
|
||||
status: 'identified',
|
||||
message: 'SSL certificate has expired',
|
||||
author: 'Security Team'
|
||||
},
|
||||
{
|
||||
id: 'upd-8',
|
||||
timestamp: Date.now() - 7 * 24 * 60 * 60 * 1000 + 2 * 60 * 60 * 1000,
|
||||
status: 'resolved',
|
||||
message: 'Certificate renewed and service restored',
|
||||
author: 'Security Team'
|
||||
}
|
||||
]
|
||||
}
|
||||
];
|
||||
|
||||
// Set the incidents
|
||||
incidents.currentIncidents = currentIncidents;
|
||||
incidents.pastIncidents = pastIncidents;
|
||||
|
||||
// Pre-subscribe to the first incident
|
||||
incidents.subscribedIncidentIds = ['inc-001'];
|
||||
addLog('Pre-subscribed to incident: "Database Connection Pool Exhaustion"');
|
||||
|
||||
// Handle subscription events
|
||||
incidents.addEventListener('incidentSubscribe', (event) => {
|
||||
const { incidentId, incidentTitle, affectedServices } = event.detail;
|
||||
addLog(`SUBSCRIBED to incident: "${incidentTitle}" (ID: ${incidentId})`);
|
||||
addLog(` Affected services: ${affectedServices.join(', ')}`);
|
||||
showNotification(`✓ Subscribed to: ${incidentTitle}`, 'subscribe');
|
||||
|
||||
// In a real application, you would send this to your backend
|
||||
console.log('Subscribe API call would be made here:', {
|
||||
incidentId,
|
||||
userEmail: 'user@example.com',
|
||||
notificationPreferences: ['email', 'sms']
|
||||
});
|
||||
});
|
||||
|
||||
incidents.addEventListener('incidentUnsubscribe', (event) => {
|
||||
const { incident } = event.detail;
|
||||
addLog(`UNSUBSCRIBED from incident: "${incident.title}" (ID: ${incident.id})`);
|
||||
showNotification(`Unsubscribed from: ${incident.title}`, 'unsubscribe');
|
||||
|
||||
// In a real application, you would send this to your backend
|
||||
console.log('Unsubscribe API call would be made here:', {
|
||||
incidentId: incident.id,
|
||||
userEmail: 'user@example.com'
|
||||
});
|
||||
});
|
||||
|
||||
// Simulate receiving a new update for a subscribed incident
|
||||
setTimeout(() => {
|
||||
if (incidents.subscribedIncidents && incidents.subscribedIncidents.has('inc-001')) {
|
||||
addLog('📬 NEW UPDATE for subscribed incident "Database Connection Pool Exhaustion"');
|
||||
showNotification('New update: Connection pool stable, incident will be resolved soon');
|
||||
}
|
||||
}, 5000);
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
175
test-shadcn-spacing.html
Normal file
175
test-shadcn-spacing.html
Normal file
@@ -0,0 +1,175 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>shadcn-Aligned Spacing Test</title>
|
||||
<script type="module" src="./dist_bundle/bundle.js"></script>
|
||||
<style>
|
||||
body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
font-family: 'Geist Sans', -apple-system, BlinkMacSystemFont, sans-serif;
|
||||
background: #fafafa;
|
||||
}
|
||||
@media (prefers-color-scheme: dark) {
|
||||
body {
|
||||
background: #09090b;
|
||||
}
|
||||
}
|
||||
.demo-wrapper {
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 24px; /* Using consistent spacing between components */
|
||||
}
|
||||
.spacing-info {
|
||||
position: fixed;
|
||||
top: 20px;
|
||||
right: 20px;
|
||||
background: rgba(255, 255, 255, 0.9);
|
||||
backdrop-filter: blur(10px);
|
||||
border: 1px solid #e5e7eb;
|
||||
border-radius: 8px;
|
||||
padding: 16px;
|
||||
font-size: 13px;
|
||||
max-width: 300px;
|
||||
z-index: 1000;
|
||||
}
|
||||
@media (prefers-color-scheme: dark) {
|
||||
.spacing-info {
|
||||
background: rgba(18, 18, 18, 0.9);
|
||||
border-color: #27272a;
|
||||
}
|
||||
}
|
||||
.spacing-info h3 {
|
||||
margin: 0 0 12px 0;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
}
|
||||
.spacing-info ul {
|
||||
margin: 0;
|
||||
padding-left: 20px;
|
||||
line-height: 1.6;
|
||||
}
|
||||
.spacing-info code {
|
||||
background: #f3f4f6;
|
||||
padding: 2px 4px;
|
||||
border-radius: 3px;
|
||||
font-size: 12px;
|
||||
}
|
||||
@media (prefers-color-scheme: dark) {
|
||||
.spacing-info code {
|
||||
background: #27272a;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="demo-wrapper">
|
||||
<upl-statuspage-header
|
||||
page-title="CloudFlow"
|
||||
show-report-button="true"
|
||||
show-subscribe-button="true"
|
||||
></upl-statuspage-header>
|
||||
|
||||
<upl-statuspage-pagetitle
|
||||
page-title="Service Status"
|
||||
page-subtitle="Current operational status of CloudFlow Infrastructure services"
|
||||
></upl-statuspage-pagetitle>
|
||||
|
||||
<upl-statuspage-statusbar id="statusbar"></upl-statuspage-statusbar>
|
||||
|
||||
<upl-statuspage-statsgrid
|
||||
current-status="operational"
|
||||
uptime="99.95"
|
||||
avg-response-time="125"
|
||||
total-incidents="2"
|
||||
></upl-statuspage-statsgrid>
|
||||
|
||||
<upl-statuspage-assetsselector id="assets"></upl-statuspage-assetsselector>
|
||||
|
||||
<upl-statuspage-statusdetails id="details"></upl-statuspage-statusdetails>
|
||||
|
||||
<upl-statuspage-statusmonth id="month"></upl-statuspage-statusmonth>
|
||||
|
||||
<upl-statuspage-incidents id="incidents"></upl-statuspage-incidents>
|
||||
|
||||
<upl-statuspage-footer
|
||||
company-name="CloudFlow Infrastructure"
|
||||
enable-subscribe="true"
|
||||
subscriber-count="1234"
|
||||
></upl-statuspage-footer>
|
||||
</div>
|
||||
|
||||
<div class="spacing-info">
|
||||
<h3>shadcn Spacing Improvements</h3>
|
||||
<ul>
|
||||
<li>Page title padding reduced from <code>48px</code> to <code>24px</code></li>
|
||||
<li>Mini-heading padding reduced from <code>16px</code> to <code>8px</code></li>
|
||||
<li>All arbitrary values replaced with spacing scale</li>
|
||||
<li>Consistent <code>24px</code> gap between components</li>
|
||||
<li>More compact, professional layout</li>
|
||||
<li>Follows shadcn's minimal spacing approach</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
const statusbar = document.getElementById('statusbar');
|
||||
const assets = document.getElementById('assets');
|
||||
const details = document.getElementById('details');
|
||||
const month = document.getElementById('month');
|
||||
const incidents = document.getElementById('incidents');
|
||||
|
||||
// Configure status bar
|
||||
statusbar.overallStatus = {
|
||||
status: 'operational',
|
||||
message: 'All systems operational',
|
||||
lastUpdated: Date.now()
|
||||
};
|
||||
|
||||
// Configure services
|
||||
const services = [
|
||||
{
|
||||
id: 'api',
|
||||
name: 'API Gateway',
|
||||
displayName: 'API Gateway',
|
||||
currentStatus: 'operational',
|
||||
uptime30d: 99.99,
|
||||
category: 'Core Services',
|
||||
selected: true
|
||||
},
|
||||
{
|
||||
id: 'database',
|
||||
name: 'Database Cluster',
|
||||
displayName: 'Database Cluster',
|
||||
currentStatus: 'operational',
|
||||
uptime30d: 99.95,
|
||||
category: 'Core Services',
|
||||
selected: true
|
||||
}
|
||||
];
|
||||
|
||||
assets.services = services;
|
||||
details.services = services;
|
||||
|
||||
// Configure monthly data
|
||||
month.monthlyData = [{
|
||||
month: '2024-01',
|
||||
days: Array(31).fill(null).map((_, i) => ({
|
||||
date: `2024-01-${String(i + 1).padStart(2, '0')}`,
|
||||
uptime: 100,
|
||||
incidents: 0
|
||||
})),
|
||||
overallUptime: 100,
|
||||
totalIncidents: 0
|
||||
}];
|
||||
|
||||
// Configure incidents
|
||||
incidents.currentIncidents = [];
|
||||
incidents.pastIncidents = [];
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
107
test-statsgrid.html
Normal file
107
test-statsgrid.html
Normal file
@@ -0,0 +1,107 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Stats Grid Test</title>
|
||||
<script type="module" src="./dist_bundle/bundle.js"></script>
|
||||
<style>
|
||||
body {
|
||||
margin: 0;
|
||||
padding: 20px;
|
||||
font-family: 'Geist Sans', -apple-system, BlinkMacSystemFont, sans-serif;
|
||||
background: #fafafa;
|
||||
}
|
||||
@media (prefers-color-scheme: dark) {
|
||||
body {
|
||||
background: #09090b;
|
||||
}
|
||||
}
|
||||
.test-section {
|
||||
margin-bottom: 40px;
|
||||
}
|
||||
h2 {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h1>Stats Grid Component Test</h1>
|
||||
|
||||
<div class="test-section">
|
||||
<h2>Normal Operation</h2>
|
||||
<upl-statuspage-statsgrid
|
||||
id="grid1"
|
||||
current-status="operational"
|
||||
uptime="99.95"
|
||||
avg-response-time="125"
|
||||
total-incidents="0"
|
||||
affected-services="0"
|
||||
total-services="12"
|
||||
time-period="90 days"
|
||||
></upl-statuspage-statsgrid>
|
||||
</div>
|
||||
|
||||
<div class="test-section">
|
||||
<h2>Degraded Performance</h2>
|
||||
<upl-statuspage-statsgrid
|
||||
id="grid2"
|
||||
current-status="degraded"
|
||||
uptime="98.50"
|
||||
avg-response-time="450"
|
||||
total-incidents="3"
|
||||
affected-services="2"
|
||||
total-services="12"
|
||||
time-period="30 days"
|
||||
></upl-statuspage-statsgrid>
|
||||
</div>
|
||||
|
||||
<div class="test-section">
|
||||
<h2>Major Outage</h2>
|
||||
<upl-statuspage-statsgrid
|
||||
id="grid3"
|
||||
current-status="major_outage"
|
||||
uptime="95.20"
|
||||
avg-response-time="1250"
|
||||
total-incidents="8"
|
||||
affected-services="7"
|
||||
total-services="12"
|
||||
time-period="7 days"
|
||||
></upl-statuspage-statsgrid>
|
||||
</div>
|
||||
|
||||
<div class="test-section">
|
||||
<h2>Loading State</h2>
|
||||
<upl-statuspage-statsgrid
|
||||
id="grid4"
|
||||
loading="true"
|
||||
></upl-statuspage-statsgrid>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// Test dynamic updates
|
||||
setTimeout(() => {
|
||||
const grid1 = document.getElementById('grid1');
|
||||
console.log('Updating grid1 to show degraded status...');
|
||||
grid1.currentStatus = 'degraded';
|
||||
grid1.avgResponseTime = 350;
|
||||
grid1.totalIncidents = 1;
|
||||
grid1.affectedServices = 1;
|
||||
}, 3000);
|
||||
|
||||
// Test loading state toggle
|
||||
setTimeout(() => {
|
||||
const grid4 = document.getElementById('grid4');
|
||||
console.log('Loading complete, showing data...');
|
||||
grid4.loading = false;
|
||||
grid4.currentStatus = 'operational';
|
||||
grid4.uptime = 99.99;
|
||||
grid4.avgResponseTime = 85;
|
||||
grid4.totalIncidents = 0;
|
||||
grid4.affectedServices = 0;
|
||||
grid4.totalServices = 15;
|
||||
grid4.timePeriod = '24 hours';
|
||||
}, 5000);
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
1
test/test.ts
Normal file
1
test/test.ts
Normal file
@@ -0,0 +1 @@
|
||||
console.log('hello');
|
||||
@@ -1,8 +1,8 @@
|
||||
/**
|
||||
* autocreated commitinfo by @pushrocks/commitinfo
|
||||
* autocreated commitinfo by @push.rocks/commitinfo
|
||||
*/
|
||||
export const commitinfo = {
|
||||
name: '@uptimelink_private/catalog',
|
||||
version: '1.0.71',
|
||||
description: 'a catalog with webcomponents for uptimelink dashboard'
|
||||
name: '@uptime.link/statuspage',
|
||||
version: '1.3.1',
|
||||
description: 'A catalog of web components for the UptimeLink dashboard.'
|
||||
}
|
||||
|
||||
@@ -1,7 +1,13 @@
|
||||
// Export components
|
||||
export * from './upl-statuspage-assetsselector.js';
|
||||
export * from './upl-statuspage-footer.js';
|
||||
export * from './upl-statuspage-header.js';
|
||||
export * from './upl-statuspage-incidents.js';
|
||||
export * from './upl-statuspage-pagetitle.js';
|
||||
export * from './upl-statuspage-statusbar.js';
|
||||
export * from './upl-statuspage-statusdetails.js';
|
||||
export * from './upl-statuspage-statusmonth.js';
|
||||
export * from './upl-statuspage-statsgrid.js';
|
||||
|
||||
// Export interfaces
|
||||
export * from '../interfaces/index.js';
|
||||
|
||||
@@ -1,26 +1,33 @@
|
||||
import { customElement, DeesElement, html, TemplateResult } from '@designestate/dees-element';
|
||||
|
||||
import * as domtools from '@designestate/dees-domtools';
|
||||
import { customElement, DeesElement, html, type TemplateResult, css, cssManager, unsafeCSS } from '@design.estate/dees-element';
|
||||
import * as domtools from '@design.estate/dees-domtools';
|
||||
import * as sharedStyles 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(sharedStyles.fonts.base)};
|
||||
}
|
||||
|
||||
h5 {
|
||||
display: block;
|
||||
max-width: 1200px;
|
||||
margin: 0px auto;
|
||||
padding: 0px 0px ${unsafeCSS(sharedStyles.spacing.sm)} 0px;
|
||||
color: ${sharedStyles.colors.text.secondary};
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
letter-spacing: 0.05em;
|
||||
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>
|
||||
`;
|
||||
}
|
||||
|
||||
607
ts_web/elements/upl-statuspage-assetsselector.demo.ts
Normal file
607
ts_web/elements/upl-statuspage-assetsselector.demo.ts
Normal 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>
|
||||
`;
|
||||
@@ -3,13 +3,17 @@ import {
|
||||
property,
|
||||
html,
|
||||
customElement,
|
||||
TemplateResult,
|
||||
type TemplateResult,
|
||||
cssManager,
|
||||
css,
|
||||
} from '@designestate/dees-element';
|
||||
import * as domtools from '@designestate/dees-domtools';
|
||||
unsafeCSS,
|
||||
} from '@design.estate/dees-element';
|
||||
import * as domtools from '@design.estate/dees-domtools';
|
||||
import type { IServiceStatus } from '../interfaces/index.js';
|
||||
import * as sharedStyles 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,25 @@ 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 })
|
||||
accessor services: IServiceStatus[] = [];
|
||||
|
||||
@property({ type: String })
|
||||
accessor filterText: string = '';
|
||||
|
||||
@property({ type: String })
|
||||
accessor filterCategory: string = 'all';
|
||||
|
||||
@property({ type: Boolean })
|
||||
accessor showOnlySelected: boolean = false;
|
||||
|
||||
@property({ type: Boolean })
|
||||
accessor loading: boolean = false;
|
||||
|
||||
@property({ type: Boolean })
|
||||
accessor expanded: boolean = false;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
@@ -29,35 +49,597 @@ export class UplStatuspageAssetsselector extends DeesElement {
|
||||
|
||||
public static styles = [
|
||||
cssManager.defaultStyles,
|
||||
sharedStyles.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(sharedStyles.fonts.base)};
|
||||
color: ${sharedStyles.colors.text.primary};
|
||||
}
|
||||
|
||||
.mainbox {
|
||||
margin: auto;
|
||||
max-width: 900px;
|
||||
.container {
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
padding: 0 ${unsafeCSS(sharedStyles.spacing.lg)} ${unsafeCSS(sharedStyles.spacing.lg)} ${unsafeCSS(sharedStyles.spacing.lg)};
|
||||
}
|
||||
|
||||
.controls {
|
||||
display: flex;
|
||||
gap: ${unsafeCSS(sharedStyles.spacing.sm)};
|
||||
margin-bottom: ${unsafeCSS(sharedStyles.spacing.md)};
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.search-input {
|
||||
flex: 1;
|
||||
min-width: 200px;
|
||||
padding: ${unsafeCSS(sharedStyles.spacing.xs)} ${unsafeCSS(sharedStyles.spacing.sm)};
|
||||
border: 1px solid ${sharedStyles.colors.border.default};
|
||||
border-radius: ${unsafeCSS(sharedStyles.borderRadius.base)};
|
||||
background: ${sharedStyles.colors.background.card};
|
||||
color: ${sharedStyles.colors.text.primary};
|
||||
font-size: 13px;
|
||||
font-family: ${unsafeCSS(sharedStyles.fonts.base)};
|
||||
transition: all ${unsafeCSS(sharedStyles.durations.fast)} ${unsafeCSS(sharedStyles.easings.default)};
|
||||
height: 32px;
|
||||
}
|
||||
|
||||
.search-input:focus {
|
||||
outline: none;
|
||||
border-color: ${sharedStyles.colors.text.primary};
|
||||
box-shadow: 0 0 0 2px ${cssManager.bdTheme('rgba(0, 0, 0, 0.05)', 'rgba(255, 255, 255, 0.05)')};
|
||||
}
|
||||
|
||||
.search-input::placeholder {
|
||||
color: ${sharedStyles.colors.text.muted};
|
||||
}
|
||||
|
||||
.filter-button {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: ${unsafeCSS(sharedStyles.spacing.xs)} ${unsafeCSS(sharedStyles.spacing.sm)};
|
||||
border: 1px solid ${sharedStyles.colors.border.default};
|
||||
border-radius: ${unsafeCSS(sharedStyles.borderRadius.base)};
|
||||
background: ${sharedStyles.colors.background.card};
|
||||
color: ${sharedStyles.colors.text.secondary};
|
||||
cursor: pointer;
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
font-family: ${unsafeCSS(sharedStyles.fonts.base)};
|
||||
transition: all ${unsafeCSS(sharedStyles.durations.fast)} ${unsafeCSS(sharedStyles.easings.default)};
|
||||
height: 32px;
|
||||
}
|
||||
|
||||
.filter-button:hover {
|
||||
border-color: ${sharedStyles.colors.border.muted};
|
||||
color: ${sharedStyles.colors.text.primary};
|
||||
box-shadow: ${unsafeCSS(sharedStyles.shadows.sm)};
|
||||
}
|
||||
|
||||
.filter-button.active {
|
||||
background: ${sharedStyles.colors.text.primary};
|
||||
color: ${sharedStyles.colors.background.primary};
|
||||
border-color: ${sharedStyles.colors.text.primary};
|
||||
}
|
||||
|
||||
.selected-services {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: ${unsafeCSS(sharedStyles.spacing.sm)};
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.service-pill {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: ${unsafeCSS(sharedStyles.spacing.xs)};
|
||||
padding: 6px ${unsafeCSS(sharedStyles.spacing.md)};
|
||||
background: ${sharedStyles.colors.background.card};
|
||||
border: 1px solid ${sharedStyles.colors.border.default};
|
||||
border-radius: ${unsafeCSS(sharedStyles.borderRadius.full)};
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
transition: all ${unsafeCSS(sharedStyles.durations.fast)} ${unsafeCSS(sharedStyles.easings.default)};
|
||||
animation: pillFadeIn 0.3s ${unsafeCSS(sharedStyles.easings.default)} both;
|
||||
}
|
||||
|
||||
.service-pill:nth-child(1) { animation-delay: 0ms; }
|
||||
.service-pill:nth-child(2) { animation-delay: 30ms; }
|
||||
.service-pill:nth-child(3) { animation-delay: 60ms; }
|
||||
.service-pill:nth-child(4) { animation-delay: 90ms; }
|
||||
.service-pill:nth-child(5) { animation-delay: 120ms; }
|
||||
|
||||
@keyframes pillFadeIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: scale(0.9);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: scale(1);
|
||||
}
|
||||
}
|
||||
|
||||
.service-pill:hover {
|
||||
border-color: ${sharedStyles.colors.border.muted};
|
||||
box-shadow: ${unsafeCSS(sharedStyles.shadows.sm)};
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.service-pill .status-dot {
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
border-radius: 50%;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.service-pill .status-dot.operational {
|
||||
box-shadow: 0 0 0 2px ${cssManager.bdTheme('rgba(34, 197, 94, 0.2)', 'rgba(34, 197, 94, 0.3)')};
|
||||
}
|
||||
|
||||
.manage-button {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: ${unsafeCSS(sharedStyles.spacing.xs)};
|
||||
padding: 6px ${unsafeCSS(sharedStyles.spacing.md)};
|
||||
background: ${sharedStyles.colors.background.card};
|
||||
border: 1px solid ${sharedStyles.colors.border.default};
|
||||
border-radius: ${unsafeCSS(sharedStyles.borderRadius.base)};
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
transition: all ${unsafeCSS(sharedStyles.durations.fast)} ${unsafeCSS(sharedStyles.easings.default)};
|
||||
color: ${sharedStyles.colors.text.secondary};
|
||||
font-family: ${unsafeCSS(sharedStyles.fonts.base)};
|
||||
}
|
||||
|
||||
.manage-button:hover {
|
||||
border-color: ${sharedStyles.colors.border.muted};
|
||||
color: ${sharedStyles.colors.text.primary};
|
||||
box-shadow: ${unsafeCSS(sharedStyles.shadows.sm)};
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.manage-button:active {
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
.expandable-section {
|
||||
margin-top: ${unsafeCSS(sharedStyles.spacing.lg)};
|
||||
overflow: hidden;
|
||||
animation: expandIn 0.3s ${unsafeCSS(sharedStyles.easings.default)};
|
||||
}
|
||||
|
||||
@keyframes expandIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(-8px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
.expandable-content {
|
||||
padding: ${unsafeCSS(sharedStyles.spacing.lg)};
|
||||
background: ${sharedStyles.colors.background.secondary};
|
||||
border: 1px solid ${sharedStyles.colors.border.default};
|
||||
border-radius: ${unsafeCSS(sharedStyles.borderRadius.lg)};
|
||||
}
|
||||
|
||||
.assets-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
|
||||
gap: ${unsafeCSS(sharedStyles.spacing.sm)};
|
||||
margin-top: ${unsafeCSS(sharedStyles.spacing.md)};
|
||||
}
|
||||
|
||||
.asset-card {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: ${unsafeCSS(sharedStyles.spacing.md)};
|
||||
background: ${sharedStyles.colors.background.card};
|
||||
border-radius: ${unsafeCSS(sharedStyles.borderRadius.base)};
|
||||
cursor: pointer;
|
||||
transition: all ${unsafeCSS(sharedStyles.durations.fast)} ${unsafeCSS(sharedStyles.easings.default)};
|
||||
border: 1px solid ${sharedStyles.colors.border.default};
|
||||
gap: ${unsafeCSS(sharedStyles.spacing.sm)};
|
||||
animation: cardFadeIn 0.3s ${unsafeCSS(sharedStyles.easings.default)} both;
|
||||
}
|
||||
|
||||
@keyframes cardFadeIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(8px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
.asset-card:hover {
|
||||
border-color: ${sharedStyles.colors.border.muted};
|
||||
box-shadow: ${unsafeCSS(sharedStyles.shadows.sm)};
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
.asset-card.selected {
|
||||
border-color: ${sharedStyles.colors.text.primary};
|
||||
background: ${sharedStyles.colors.background.secondary};
|
||||
}
|
||||
|
||||
.asset-card.selected:hover {
|
||||
box-shadow: ${unsafeCSS(sharedStyles.shadows.md)};
|
||||
}
|
||||
|
||||
.asset-checkbox {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
cursor: pointer;
|
||||
accent-color: ${sharedStyles.colors.text.primary};
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.asset-info {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.asset-name {
|
||||
font-weight: 600;
|
||||
font-size: 14px;
|
||||
margin-bottom: ${unsafeCSS(sharedStyles.spacing.xs)};
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.asset-description {
|
||||
font-size: 13px;
|
||||
color: ${sharedStyles.colors.text.secondary};
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.asset-status {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: ${unsafeCSS(sharedStyles.spacing.xs)};
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.status-indicator {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
border-radius: ${unsafeCSS(sharedStyles.borderRadius.full)};
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.status-indicator.operational, .status-dot.operational {
|
||||
background: ${sharedStyles.colors.status.operational};
|
||||
}
|
||||
.status-indicator.degraded, .status-dot.degraded {
|
||||
background: ${sharedStyles.colors.status.degraded};
|
||||
}
|
||||
.status-indicator.partial_outage, .status-dot.partial_outage {
|
||||
background: ${sharedStyles.colors.status.partial};
|
||||
}
|
||||
.status-indicator.major_outage, .status-dot.major_outage {
|
||||
background: ${sharedStyles.colors.status.major};
|
||||
}
|
||||
.status-indicator.maintenance, .status-dot.maintenance {
|
||||
background: ${sharedStyles.colors.status.maintenance};
|
||||
}
|
||||
|
||||
.status-text {
|
||||
font-size: 12px;
|
||||
text-transform: capitalize;
|
||||
color: ${sharedStyles.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(sharedStyles.spacing.xl)};
|
||||
color: ${cssManager.bdTheme('#9ca3af', '#71717a')};
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.summary {
|
||||
text-align: right;
|
||||
font-size: 12px;
|
||||
margin-top: ${unsafeCSS(sharedStyles.spacing.md)};
|
||||
color: ${cssManager.bdTheme('#9ca3af', '#71717a')};
|
||||
}
|
||||
|
||||
.no-services {
|
||||
padding: ${unsafeCSS(sharedStyles.spacing.xl)};
|
||||
text-align: center;
|
||||
color: ${cssManager.bdTheme('#9ca3af', '#71717a')};
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
@media (max-width: 640px) {
|
||||
.container {
|
||||
padding: 0 ${unsafeCSS(sharedStyles.spacing.md)} ${unsafeCSS(sharedStyles.spacing.md)} ${unsafeCSS(sharedStyles.spacing.md)};
|
||||
}
|
||||
|
||||
.controls {
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
.search-input {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.selected-services {
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
.service-pill {
|
||||
width: auto;
|
||||
}
|
||||
|
||||
.expandable-content {
|
||||
padding: ${unsafeCSS(sharedStyles.spacing.md)};
|
||||
}
|
||||
|
||||
.assets-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.asset-card {
|
||||
padding: ${unsafeCSS(sharedStyles.spacing.sm)};
|
||||
}
|
||||
}
|
||||
`,
|
||||
]
|
||||
|
||||
public render(): TemplateResult {
|
||||
const selectedServices = this.services.filter(s => s.selected);
|
||||
const selectedCount = selectedServices.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="selected-services">
|
||||
${selectedCount === 0 ? html`
|
||||
<span style="color: ${cssManager.bdTheme('#9ca3af', '#71717a')}; font-size: 13px;">
|
||||
No services selected
|
||||
</span>
|
||||
` : selectedCount > 5 && !this.expanded ? html`
|
||||
${selectedServices.slice(0, 4).map(service => html`
|
||||
<div class="service-pill">
|
||||
<span class="status-dot ${service.currentStatus}"></span>
|
||||
<span>${service.displayName}</span>
|
||||
</div>
|
||||
`)}
|
||||
<span style="color: ${cssManager.bdTheme('#6b7280', '#a1a1aa')}; font-size: 12px;">
|
||||
+${selectedCount - 4} more
|
||||
</span>
|
||||
` : selectedServices.map(service => html`
|
||||
<div class="service-pill">
|
||||
<span class="status-dot ${service.currentStatus}"></span>
|
||||
<span>${service.displayName}</span>
|
||||
</div>
|
||||
`)}
|
||||
|
||||
<button
|
||||
class="manage-button"
|
||||
@click=${() => { this.expanded = !this.expanded; }}
|
||||
>
|
||||
${this.expanded ? 'Close' : 'Manage Services'}
|
||||
<svg
|
||||
width="10"
|
||||
height="6"
|
||||
viewBox="0 0 10 6"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
style="transform: rotate(${this.expanded ? '180deg' : '0'}); transition: transform 0.2s;"
|
||||
>
|
||||
<path
|
||||
d="M1 1L5 5L9 1"
|
||||
stroke="currentColor"
|
||||
stroke-width="1.5"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
${this.expanded ? html`
|
||||
<div class="expandable-section">
|
||||
<div class="expandable-content">
|
||||
<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>
|
||||
` : this.renderServiceGrid()}
|
||||
</div>
|
||||
|
||||
<div class="summary">
|
||||
${selectedCount} of ${this.services.length} services selected
|
||||
</div>
|
||||
</div>
|
||||
</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
|
||||
}));
|
||||
}
|
||||
|
||||
private renderServiceGrid(): TemplateResult {
|
||||
const filteredServices = this.getFilteredServices();
|
||||
|
||||
if (filteredServices.length === 0) {
|
||||
return html`<div class="no-results">No services found matching your criteria</div>`;
|
||||
}
|
||||
|
||||
return html`${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>
|
||||
`)}`;
|
||||
}
|
||||
}
|
||||
|
||||
744
ts_web/elements/upl-statuspage-footer.demo.ts
Normal file
744
ts_web/elements/upl-statuspage-footer.demo.ts
Normal 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>
|
||||
`;
|
||||
@@ -1,5 +1,7 @@
|
||||
import { DeesElement, property, html, customElement, TemplateResult, css, cssManager } from '@designestate/dees-element';
|
||||
import * as domtools from '@designestate/dees-domtools';
|
||||
import { DeesElement, property, html, customElement, type TemplateResult, css, cssManager, unsafeCSS } from '@design.estate/dees-element';
|
||||
import * as domtools from '@design.estate/dees-domtools';
|
||||
import * as sharedStyles 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 })
|
||||
accessor companyName: string = '';
|
||||
|
||||
@property({
|
||||
type: Boolean
|
||||
})
|
||||
public whitelabel = false;
|
||||
@property({ type: String })
|
||||
accessor legalUrl: string = '';
|
||||
|
||||
@property({ type: String })
|
||||
accessor supportEmail: string = '';
|
||||
|
||||
@property({ type: String })
|
||||
accessor statusPageUrl: string = '';
|
||||
|
||||
@property({ type: Boolean })
|
||||
accessor whitelabel: boolean = false;
|
||||
|
||||
@property({ type: Number })
|
||||
accessor lastUpdated: number | null = null;
|
||||
|
||||
@property({ type: Number })
|
||||
accessor currentYear: number = new Date().getFullYear();
|
||||
|
||||
@property({ type: Array })
|
||||
accessor socialLinks: Array<{ platform: string; url: string }> = [];
|
||||
|
||||
@property({ type: Array })
|
||||
accessor additionalLinks: Array<{ label: string; url: string }> = [];
|
||||
|
||||
@property({ type: String })
|
||||
accessor rssFeedUrl: string = '';
|
||||
|
||||
@property({ type: String })
|
||||
accessor apiStatusUrl: string = '';
|
||||
|
||||
@property({ type: Boolean })
|
||||
accessor loading: boolean = false;
|
||||
|
||||
@property({ type: String })
|
||||
accessor errorMessage: string | null = null;
|
||||
|
||||
@property({ type: Boolean })
|
||||
accessor offline: boolean = false;
|
||||
|
||||
@property({ type: String })
|
||||
accessor latestStatusUpdate: string = '';
|
||||
|
||||
@property({ type: Boolean })
|
||||
accessor enableSubscribe: boolean = false;
|
||||
|
||||
@property({ type: Boolean })
|
||||
accessor enableReportIssue: boolean = false;
|
||||
|
||||
@property({ type: Boolean })
|
||||
accessor enableLanguageSelector: boolean = false;
|
||||
|
||||
@property({ type: Boolean })
|
||||
accessor enableThemeToggle: boolean = false;
|
||||
|
||||
@property({ type: Array })
|
||||
accessor languageOptions: Array<{ code: string; label: string }> = [];
|
||||
|
||||
@property({ type: String })
|
||||
accessor currentLanguage: string = 'en';
|
||||
|
||||
@property({ type: String })
|
||||
accessor currentTheme: string = 'light';
|
||||
|
||||
@property({ type: Number })
|
||||
accessor subscriberCount: number = 0;
|
||||
|
||||
@property({ type: Object })
|
||||
accessor customBranding: { primaryColor?: string; logoUrl?: string; footerText?: string } | null = null;
|
||||
|
||||
|
||||
constructor() {
|
||||
@@ -30,43 +94,611 @@ export class UplStatuspageFooter extends DeesElement {
|
||||
|
||||
public static styles = [
|
||||
domtools.elementBasic.staticStyles,
|
||||
sharedStyles.commonStyles,
|
||||
css`
|
||||
:host {
|
||||
display: block;
|
||||
background: ${cssManager.bdTheme('#ffffff', '#000000')};
|
||||
font-family: Inter;
|
||||
color: ${cssManager.bdTheme('#333333', '#ffffff')};
|
||||
display: block;
|
||||
background: ${sharedStyles.colors.background.primary};
|
||||
font-family: ${unsafeCSS(sharedStyles.fonts.base)};
|
||||
color: ${sharedStyles.colors.text.primary};
|
||||
font-size: 14px;
|
||||
border-top: 1px solid ${sharedStyles.colors.border.default};
|
||||
}
|
||||
|
||||
.container {
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
padding: ${unsafeCSS(sharedStyles.spacing['2xl'])} ${unsafeCSS(sharedStyles.spacing.lg)};
|
||||
}
|
||||
|
||||
.footer-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: ${unsafeCSS(sharedStyles.spacing.xl)};
|
||||
}
|
||||
|
||||
.footer-main {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: start;
|
||||
gap: ${unsafeCSS(sharedStyles.spacing['2xl'])};
|
||||
}
|
||||
|
||||
.company-info {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: ${unsafeCSS(sharedStyles.spacing.lg)};
|
||||
}
|
||||
|
||||
.company-name {
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
color: ${sharedStyles.colors.text.primary};
|
||||
letter-spacing: -0.01em;
|
||||
}
|
||||
|
||||
.company-links {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: ${unsafeCSS(sharedStyles.spacing.lg)};
|
||||
}
|
||||
|
||||
.footer-link {
|
||||
color: ${sharedStyles.colors.text.secondary};
|
||||
text-decoration: none;
|
||||
transition: all ${unsafeCSS(sharedStyles.durations.fast)} ${unsafeCSS(sharedStyles.easings.default)};
|
||||
font-size: 13px;
|
||||
font-weight: 400;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.footer-link::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
bottom: -2px;
|
||||
left: 0;
|
||||
width: 0;
|
||||
height: 1px;
|
||||
background: ${sharedStyles.colors.text.primary};
|
||||
transition: width ${unsafeCSS(sharedStyles.durations.fast)} ${unsafeCSS(sharedStyles.easings.default)};
|
||||
}
|
||||
|
||||
.footer-link:hover {
|
||||
color: ${sharedStyles.colors.text.primary};
|
||||
}
|
||||
|
||||
.footer-link:hover::after {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.footer-actions {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: ${unsafeCSS(sharedStyles.spacing.md)};
|
||||
}
|
||||
|
||||
.action-button {
|
||||
padding: 8px 16px;
|
||||
height: 36px;
|
||||
border: 1px solid ${sharedStyles.colors.border.default};
|
||||
background: ${sharedStyles.colors.background.card};
|
||||
border-radius: ${unsafeCSS(sharedStyles.borderRadius.base)};
|
||||
cursor: pointer;
|
||||
text-align: center;
|
||||
transition: all ${unsafeCSS(sharedStyles.durations.fast)} ${unsafeCSS(sharedStyles.easings.default)};
|
||||
white-space: nowrap;
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
color: ${sharedStyles.colors.text.primary};
|
||||
font-family: ${unsafeCSS(sharedStyles.fonts.base)};
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.action-button:hover {
|
||||
background: ${sharedStyles.colors.background.secondary};
|
||||
border-color: ${sharedStyles.colors.border.muted};
|
||||
transform: translateY(-1px);
|
||||
box-shadow: ${unsafeCSS(sharedStyles.shadows.sm)};
|
||||
}
|
||||
|
||||
.action-button:active {
|
||||
transform: translateY(0);
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.footer-bottom {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding-top: ${unsafeCSS(sharedStyles.spacing.lg)};
|
||||
margin-top: ${unsafeCSS(sharedStyles.spacing.lg)};
|
||||
border-top: 1px solid ${cssManager.bdTheme('#e5e7eb', '#27272a')};
|
||||
}
|
||||
|
||||
.footer-meta {
|
||||
display: flex;
|
||||
gap: ${unsafeCSS(sharedStyles.spacing.lg)};
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.social-links {
|
||||
display: flex;
|
||||
gap: ${unsafeCSS(sharedStyles.spacing.sm)};
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.social-link {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 34px;
|
||||
height: 34px;
|
||||
border-radius: ${unsafeCSS(sharedStyles.borderRadius.base)};
|
||||
transition: all ${unsafeCSS(sharedStyles.durations.fast)} ${unsafeCSS(sharedStyles.easings.default)};
|
||||
color: ${sharedStyles.colors.text.muted};
|
||||
border: 1px solid transparent;
|
||||
}
|
||||
|
||||
.social-link:hover {
|
||||
color: ${sharedStyles.colors.text.primary};
|
||||
background: ${sharedStyles.colors.background.secondary};
|
||||
border-color: ${sharedStyles.colors.border.default};
|
||||
transform: translateY(-2px);
|
||||
box-shadow: ${unsafeCSS(sharedStyles.shadows.sm)};
|
||||
}
|
||||
|
||||
.social-link:active {
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
.social-link svg {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
fill: currentColor;
|
||||
transition: transform ${unsafeCSS(sharedStyles.durations.fast)} ${unsafeCSS(sharedStyles.easings.default)};
|
||||
}
|
||||
|
||||
.social-link:hover svg {
|
||||
transform: scale(1.1);
|
||||
}
|
||||
|
||||
.copyright {
|
||||
font-size: 13px;
|
||||
color: ${cssManager.bdTheme('#6b7280', '#a1a1aa')};
|
||||
}
|
||||
|
||||
.last-updated {
|
||||
font-size: 12px;
|
||||
color: ${cssManager.bdTheme('#9ca3af', '#71717a')};
|
||||
}
|
||||
|
||||
.powered-by {
|
||||
font-size: 12px;
|
||||
color: ${sharedStyles.colors.text.muted};
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.powered-by a {
|
||||
color: ${sharedStyles.colors.text.secondary};
|
||||
text-decoration: none;
|
||||
transition: all ${unsafeCSS(sharedStyles.durations.fast)} ${unsafeCSS(sharedStyles.easings.default)};
|
||||
font-weight: 500;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.powered-by a::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
bottom: -1px;
|
||||
left: 0;
|
||||
width: 0;
|
||||
height: 1px;
|
||||
background: ${sharedStyles.colors.text.primary};
|
||||
transition: width ${unsafeCSS(sharedStyles.durations.fast)} ${unsafeCSS(sharedStyles.easings.default)};
|
||||
}
|
||||
|
||||
.powered-by a:hover {
|
||||
color: ${sharedStyles.colors.text.primary};
|
||||
}
|
||||
|
||||
.powered-by a:hover::after {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.status-update {
|
||||
padding: 12px 16px;
|
||||
background: ${cssManager.bdTheme('#f9fafb', '#18181b')};
|
||||
border: 1px solid ${cssManager.bdTheme('#e5e7eb', '#27272a')};
|
||||
border-radius: 6px;
|
||||
font-size: 13px;
|
||||
margin-bottom: ${unsafeCSS(sharedStyles.spacing.lg)};
|
||||
line-height: 1.5;
|
||||
color: ${cssManager.bdTheme('#4b5563', '#d1d5db')};
|
||||
}
|
||||
|
||||
.language-selector {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.language-selector select {
|
||||
padding: ${unsafeCSS(sharedStyles.spacing.xs)} ${unsafeCSS(sharedStyles.spacing.sm)};
|
||||
border: 1px solid ${sharedStyles.colors.border.default};
|
||||
background: ${sharedStyles.colors.background.primary};
|
||||
color: ${sharedStyles.colors.text.primary};
|
||||
border-radius: ${unsafeCSS(sharedStyles.borderRadius.base)};
|
||||
font-size: 14px;
|
||||
cursor: pointer;
|
||||
font-family: ${unsafeCSS(sharedStyles.fonts.base)};
|
||||
}
|
||||
|
||||
.theme-toggle {
|
||||
cursor: pointer;
|
||||
padding: ${unsafeCSS(sharedStyles.spacing.xs)} ${unsafeCSS(sharedStyles.spacing.sm)};
|
||||
border: 1px solid ${sharedStyles.colors.border.default};
|
||||
background: transparent;
|
||||
border-radius: ${unsafeCSS(sharedStyles.borderRadius.base)};
|
||||
font-size: 14px;
|
||||
transition: all 0.2s ease;
|
||||
color: ${sharedStyles.colors.text.primary};
|
||||
font-family: ${unsafeCSS(sharedStyles.fonts.base)};
|
||||
}
|
||||
|
||||
.theme-toggle:hover {
|
||||
background: ${sharedStyles.colors.background.secondary};
|
||||
border-color: ${sharedStyles.colors.border.muted};
|
||||
}
|
||||
|
||||
.subscribe-wrapper {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: ${unsafeCSS(sharedStyles.spacing.xs)};
|
||||
}
|
||||
|
||||
.subscriber-count {
|
||||
font-size: 12px;
|
||||
color: ${cssManager.bdTheme('#6b7280', '#a1a1aa')};
|
||||
}
|
||||
|
||||
.error-message {
|
||||
padding: ${unsafeCSS(sharedStyles.spacing.md)};
|
||||
background: ${cssManager.bdTheme('#fee9e9', '#7f1d1d')};
|
||||
color: ${cssManager.bdTheme('#dc2626', '#fca5a5')};
|
||||
border-radius: ${unsafeCSS(sharedStyles.borderRadius.base)};
|
||||
margin-bottom: ${unsafeCSS(sharedStyles.spacing.lg)};
|
||||
font-size: 14px;
|
||||
border: 1px solid ${cssManager.bdTheme('#fecaca', '#991b1b')};
|
||||
}
|
||||
|
||||
.offline-indicator {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: ${unsafeCSS(sharedStyles.spacing.xs)};
|
||||
padding: ${unsafeCSS(sharedStyles.spacing.xs)} ${unsafeCSS(sharedStyles.spacing.md)};
|
||||
background: ${sharedStyles.colors.status.degraded};
|
||||
color: white;
|
||||
border-radius: ${unsafeCSS(sharedStyles.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(sharedStyles.borderRadius.md)};
|
||||
}
|
||||
|
||||
@keyframes loading {
|
||||
0% { background-position: 200% 0; }
|
||||
100% { background-position: -200% 0; }
|
||||
}
|
||||
|
||||
.additional-links {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: ${unsafeCSS(sharedStyles.spacing.md)};
|
||||
margin-top: ${unsafeCSS(sharedStyles.spacing.md)};
|
||||
}
|
||||
|
||||
.additional-link {
|
||||
font-size: 13px;
|
||||
color: ${cssManager.bdTheme('#6b7280', '#a1a1aa')};
|
||||
text-decoration: none;
|
||||
transition: color 0.15s ease;
|
||||
}
|
||||
|
||||
.additional-link:hover {
|
||||
color: ${sharedStyles.colors.text.primary};
|
||||
}
|
||||
|
||||
@media (max-width: 640px) {
|
||||
.container {
|
||||
padding: ${unsafeCSS(sharedStyles.spacing.lg)} ${unsafeCSS(sharedStyles.spacing.md)};
|
||||
}
|
||||
|
||||
.mainbox {
|
||||
max-width: 900px;
|
||||
margin: auto;
|
||||
padding-top: 20px;
|
||||
padding-bottom: 20px;
|
||||
.footer-main {
|
||||
flex-direction: column;
|
||||
gap: ${unsafeCSS(sharedStyles.spacing.lg)};
|
||||
}
|
||||
|
||||
|
||||
.footer-bottom {
|
||||
flex-direction: column;
|
||||
gap: ${unsafeCSS(sharedStyles.spacing.lg)};
|
||||
align-items: start;
|
||||
}
|
||||
|
||||
.footer-meta {
|
||||
flex-direction: column;
|
||||
align-items: start;
|
||||
gap: ${unsafeCSS(sharedStyles.spacing.md)};
|
||||
}
|
||||
|
||||
.company-links {
|
||||
flex-direction: column;
|
||||
gap: ${unsafeCSS(sharedStyles.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">
|
||||
<svg width="12" height="12" viewBox="0 0 12 12" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M6 2V6M6 10H6.01M11 6C11 8.76142 8.76142 11 6 11C3.23858 11 1 8.76142 1 6C1 3.23858 3.23858 1 6 1C8.76142 1 11 3.23858 11 6Z" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
You are currently offline
|
||||
</div>
|
||||
` : ''}
|
||||
|
||||
${this.latestStatusUpdate ? html`
|
||||
<div class="status-update">
|
||||
Latest Update: ${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`
|
||||
<div class="subscribe-wrapper">
|
||||
<button class="action-button" @click=${this.handleSubscribeClick}>
|
||||
Subscribe to Updates
|
||||
</button>
|
||||
${this.subscriberCount > 0 ? html`
|
||||
<div class="subscriber-count">${this.subscriberCount.toLocaleString()} subscribers</div>
|
||||
` : ''}
|
||||
</div>
|
||||
` : ''}
|
||||
|
||||
${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();
|
||||
}
|
||||
}
|
||||
241
ts_web/elements/upl-statuspage-header.demo.ts
Normal file
241
ts_web/elements/upl-statuspage-header.demo.ts
Normal 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>
|
||||
`;
|
||||
@@ -1,5 +1,7 @@
|
||||
import { DeesElement, property, html, customElement, TemplateResult, css, cssManager } from '@designestate/dees-element';
|
||||
import * as domtools from '@designestate/dees-domtools';
|
||||
import { DeesElement, property, html, customElement, type TemplateResult, css, cssManager, unsafeCSS } from '@design.estate/dees-element';
|
||||
import * as domtools from '@design.estate/dees-domtools';
|
||||
import * as sharedStyles from '../styles/shared.styles.js';
|
||||
import { demoFunc } from './upl-statuspage-header.demo.js';
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
@@ -10,13 +12,23 @@ 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()
|
||||
public pageTitle: string = "Statuspage Title";
|
||||
@property({ type: String })
|
||||
accessor pageTitle: string = "Statuspage Title";
|
||||
|
||||
@property({ type: Boolean })
|
||||
accessor showReportButton: boolean = true;
|
||||
|
||||
@property({ type: Boolean })
|
||||
accessor showSubscribeButton: boolean = true;
|
||||
|
||||
@property({ type: String })
|
||||
accessor logoUrl: string = '';
|
||||
|
||||
@property({ type: Boolean })
|
||||
accessor loading: boolean = false;
|
||||
|
||||
|
||||
constructor() {
|
||||
@@ -27,72 +39,297 @@ export class UplStatuspageHeader extends DeesElement {
|
||||
domtools.elementBasic.staticStyles,
|
||||
css`
|
||||
:host {
|
||||
display: block;
|
||||
background: ${cssManager.bdTheme('#eeeeeb', '#222222')};
|
||||
font-family: Inter;
|
||||
color: ${cssManager.bdTheme('#333333', '#ffffff')};
|
||||
display: block;
|
||||
background: ${cssManager.bdTheme(
|
||||
'rgba(255, 255, 255, 0.85)',
|
||||
'rgba(9, 9, 11, 0.85)'
|
||||
)};
|
||||
font-family: ${unsafeCSS(sharedStyles.fonts.base)};
|
||||
color: ${sharedStyles.colors.text.primary};
|
||||
border-bottom: 1px solid ${cssManager.bdTheme(
|
||||
'rgba(0, 0, 0, 0.06)',
|
||||
'rgba(255, 255, 255, 0.06)'
|
||||
)};
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 40;
|
||||
backdrop-filter: blur(16px) saturate(180%);
|
||||
-webkit-backdrop-filter: blur(16px) saturate(180%);
|
||||
}
|
||||
|
||||
.header-container {
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
padding: 0 ${unsafeCSS(sharedStyles.spacing.lg)};
|
||||
}
|
||||
|
||||
.header-nav {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
height: 64px;
|
||||
}
|
||||
|
||||
.header-left {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: ${unsafeCSS(sharedStyles.spacing.md)};
|
||||
}
|
||||
|
||||
.header-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: ${unsafeCSS(sharedStyles.spacing.sm)};
|
||||
}
|
||||
|
||||
.actionButton {
|
||||
font-family: ${unsafeCSS(sharedStyles.fonts.base)};
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
padding: 0 14px;
|
||||
border-radius: ${unsafeCSS(sharedStyles.borderRadius.base)};
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
transition: all ${unsafeCSS(sharedStyles.durations.fast)} ${unsafeCSS(sharedStyles.easings.default)};
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 6px;
|
||||
height: 36px;
|
||||
background: transparent;
|
||||
border: 1px solid ${sharedStyles.colors.border.default};
|
||||
color: ${sharedStyles.colors.text.primary};
|
||||
letter-spacing: -0.01em;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.actionButton::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background: ${cssManager.bdTheme(
|
||||
'linear-gradient(135deg, rgba(0, 0, 0, 0.02) 0%, transparent 100%)',
|
||||
'linear-gradient(135deg, rgba(255, 255, 255, 0.03) 0%, transparent 100%)'
|
||||
)};
|
||||
opacity: 0;
|
||||
transition: opacity ${unsafeCSS(sharedStyles.durations.fast)} ${unsafeCSS(sharedStyles.easings.default)};
|
||||
}
|
||||
|
||||
.actionButton:hover {
|
||||
background: ${sharedStyles.colors.background.secondary};
|
||||
border-color: ${sharedStyles.colors.border.muted};
|
||||
box-shadow: ${unsafeCSS(sharedStyles.shadows.sm)};
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.actionButton:hover::before {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.actionButton:active {
|
||||
transform: translateY(0) scale(0.98);
|
||||
transition-duration: ${unsafeCSS(sharedStyles.durations.fast)};
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.actionButton:focus-visible {
|
||||
outline: 2px solid ${sharedStyles.colors.accent.focus};
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
/* Button icon styles */
|
||||
.actionButton svg {
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
flex-shrink: 0;
|
||||
transition: transform ${unsafeCSS(sharedStyles.durations.fast)} ${unsafeCSS(sharedStyles.easings.default)};
|
||||
}
|
||||
|
||||
.actionButton:hover svg {
|
||||
transform: scale(1.1);
|
||||
}
|
||||
|
||||
.site-title {
|
||||
font-size: 17px;
|
||||
font-weight: 600;
|
||||
letter-spacing: -0.02em;
|
||||
color: ${sharedStyles.colors.text.primary};
|
||||
transition: color ${unsafeCSS(sharedStyles.durations.fast)} ${unsafeCSS(sharedStyles.easings.default)};
|
||||
}
|
||||
|
||||
.site-title:hover {
|
||||
color: ${sharedStyles.colors.text.secondary};
|
||||
}
|
||||
|
||||
.logo {
|
||||
height: 28px;
|
||||
width: auto;
|
||||
filter: ${cssManager.bdTheme('none', 'brightness(0) invert(1)')};
|
||||
transition: all ${unsafeCSS(sharedStyles.durations.normal)} ${unsafeCSS(sharedStyles.easings.default)};
|
||||
}
|
||||
|
||||
.logo:hover {
|
||||
opacity: 0.8;
|
||||
transform: scale(1.02);
|
||||
}
|
||||
|
||||
.page-info {
|
||||
padding: ${unsafeCSS(sharedStyles.spacing.lg)} 0 ${unsafeCSS(sharedStyles.spacing.xl)} 0;
|
||||
}
|
||||
|
||||
.page-title {
|
||||
font-size: 48px;
|
||||
font-weight: 700;
|
||||
letter-spacing: -0.03em;
|
||||
line-height: 1.1;
|
||||
color: ${sharedStyles.colors.text.primary};
|
||||
margin: 0 0 16px 0;
|
||||
}
|
||||
|
||||
.page-subtitle {
|
||||
font-size: 18px;
|
||||
color: ${sharedStyles.colors.text.secondary};
|
||||
margin: 0;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
/* Primary button variant */
|
||||
.actionButton.primary {
|
||||
background: ${sharedStyles.colors.accent.primary};
|
||||
color: ${sharedStyles.colors.background.primary};
|
||||
border-color: transparent;
|
||||
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.actionButton.primary::before {
|
||||
background: linear-gradient(135deg, rgba(255, 255, 255, 0.15) 0%, transparent 100%);
|
||||
}
|
||||
|
||||
.actionButton.primary:hover {
|
||||
background: ${sharedStyles.colors.accent.hover};
|
||||
box-shadow: ${unsafeCSS(sharedStyles.shadows.md)};
|
||||
}
|
||||
|
||||
.loading-skeleton {
|
||||
height: 64px;
|
||||
background: ${sharedStyles.colors.background.secondary};
|
||||
border-bottom: 1px solid ${sharedStyles.colors.border.default};
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.loading-skeleton::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background: ${cssManager.bdTheme(
|
||||
'linear-gradient(90deg, transparent 0%, rgba(0,0,0,0.04) 50%, transparent 100%)',
|
||||
'linear-gradient(90deg, transparent 0%, rgba(255,255,255,0.04) 50%, transparent 100%)'
|
||||
)};
|
||||
animation: shimmer 1.5s ${unsafeCSS(sharedStyles.easings.default)} infinite;
|
||||
}
|
||||
|
||||
@keyframes shimmer {
|
||||
0% { transform: translateX(-100%); }
|
||||
100% { transform: translateX(200%); }
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.header-container {
|
||||
padding: 0 ${unsafeCSS(sharedStyles.spacing.md)};
|
||||
}
|
||||
|
||||
.mainbox {
|
||||
margin: auto;
|
||||
max-width: 900px;
|
||||
.header-nav {
|
||||
height: 56px;
|
||||
}
|
||||
|
||||
.mainbox .actions {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
padding: 20px 0px 40px 0px;
|
||||
.header-left {
|
||||
gap: ${unsafeCSS(sharedStyles.spacing.sm)};
|
||||
}
|
||||
|
||||
.mainbox .actions .actionButton {
|
||||
background: ${cssManager.bdTheme('#00000000', '#ffffff00')};
|
||||
|
||||
.site-title {
|
||||
font-size: 15px;
|
||||
}
|
||||
|
||||
.logo {
|
||||
height: 24px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 640px) {
|
||||
.actionButton {
|
||||
font-size: 12px;
|
||||
border: 1px solid ${cssManager.bdTheme('#333', '#CCC')};
|
||||
padding: 6px 10px 7px 10px;
|
||||
margin-left: 10px;
|
||||
border-radius: 3px;
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
padding: 0 10px;
|
||||
height: 32px;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.mainbox .actions .actionButton:hover {
|
||||
background: ${cssManager.bdTheme('#333333', '#efefef')};
|
||||
border: 1px solid ${cssManager.bdTheme('#333333', '#efefef')};
|
||||
color: ${cssManager.bdTheme('#fff', '#333333')};
|
||||
.actionButton svg {
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
}
|
||||
|
||||
h1 {
|
||||
margin: 0px;
|
||||
text-align: center;
|
||||
font-weight: 600;
|
||||
font-size: 35px;
|
||||
.actionButton .button-text {
|
||||
display: none;
|
||||
}
|
||||
|
||||
h2 {
|
||||
margin: 0px;
|
||||
margin-top: 10px;
|
||||
text-align: center;
|
||||
font-weight: 600;
|
||||
font-size: 18px;
|
||||
.page-title {
|
||||
font-size: 32px;
|
||||
}
|
||||
|
||||
.page-subtitle {
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.header-actions {
|
||||
gap: 6px;
|
||||
}
|
||||
}
|
||||
`
|
||||
]
|
||||
|
||||
public render(): TemplateResult {
|
||||
if (this.loading) {
|
||||
return html`
|
||||
<div class="loading-skeleton"></div>
|
||||
`;
|
||||
}
|
||||
|
||||
return html`
|
||||
${domtools.elementBasic.styles}
|
||||
<style>
|
||||
|
||||
</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>
|
||||
<header>
|
||||
<div class="header-container">
|
||||
<nav class="header-nav">
|
||||
<div class="header-left">
|
||||
${this.logoUrl ? html`
|
||||
<img src="${this.logoUrl}" alt="${this.pageTitle}" class="logo">
|
||||
` : ''}
|
||||
<h1 class="site-title">${this.pageTitle}</h1>
|
||||
</div>
|
||||
<div class="header-actions">
|
||||
${this.showReportButton ? html`
|
||||
<button class="actionButton" @click=${this.dispatchReportNewIncident}>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M7.86 2h8.28L22 7.86v8.28L16.14 22H7.86L2 16.14V7.86L7.86 2z"></path>
|
||||
<line x1="12" y1="8" x2="12" y2="12"></line>
|
||||
<line x1="12" y1="16" x2="12.01" y2="16"></line>
|
||||
</svg>
|
||||
<span class="button-text">Report Issue</span>
|
||||
</button>
|
||||
` : ''}
|
||||
${this.showSubscribeButton ? html`
|
||||
<button class="actionButton primary" @click=${this.dispatchStatusSubscribe}>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M6 8a6 6 0 0 1 12 0c0 7 3 9 3 9H3s3-2 3-9"></path>
|
||||
<path d="M10.3 21a1.94 1.94 0 0 0 3.4 0"></path>
|
||||
</svg>
|
||||
<span class="button-text">Subscribe</span>
|
||||
</button>
|
||||
` : ''}
|
||||
</div>
|
||||
</nav>
|
||||
</div>
|
||||
<h1>${this.pageTitle}</h1>
|
||||
<h2>STATUS BOARD</h2>
|
||||
</div>
|
||||
</header>
|
||||
`;
|
||||
}
|
||||
|
||||
|
||||
1216
ts_web/elements/upl-statuspage-incidents.demo.ts
Normal file
1216
ts_web/elements/upl-statuspage-incidents.demo.ts
Normal file
File diff suppressed because it is too large
Load Diff
@@ -4,10 +4,15 @@ import {
|
||||
property,
|
||||
html,
|
||||
customElement,
|
||||
TemplateResult,
|
||||
type TemplateResult,
|
||||
css,
|
||||
cssManager,
|
||||
} from '@designestate/dees-element';
|
||||
unsafeCSS,
|
||||
} from '@design.estate/dees-element';
|
||||
import type { IIncidentDetails } from '../interfaces/index.js';
|
||||
import * as sharedStyles from '../styles/shared.styles.js';
|
||||
import './internal/uplinternal-miniheading.js';
|
||||
import { demoFunc } from './upl-statuspage-incidents.demo.js';
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
@@ -18,73 +23,881 @@ 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[] = [];
|
||||
accessor currentIncidents: IIncidentDetails[] = [];
|
||||
|
||||
@property({
|
||||
type: Array,
|
||||
})
|
||||
public pastIncidences: plugins.uplInterfaces.data.IIncident[] = [];
|
||||
accessor pastIncidents: IIncidentDetails[] = [];
|
||||
|
||||
@property({
|
||||
type: Boolean,
|
||||
})
|
||||
public whitelabel = false;
|
||||
accessor whitelabel = false;
|
||||
|
||||
@property({
|
||||
type: Boolean,
|
||||
})
|
||||
accessor loading = false;
|
||||
|
||||
@property({
|
||||
type: Number,
|
||||
})
|
||||
accessor daysToShow = 90;
|
||||
|
||||
@property({
|
||||
type: Array,
|
||||
})
|
||||
accessor subscribedIncidentIds: string[] = [];
|
||||
|
||||
@property({
|
||||
type: Object,
|
||||
state: true,
|
||||
})
|
||||
private accessor expandedIncidents: Set<string> = new Set();
|
||||
|
||||
@property({
|
||||
type: Object,
|
||||
state: true,
|
||||
})
|
||||
private accessor subscribedIncidents: Set<string> = new Set();
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
}
|
||||
|
||||
async connectedCallback() {
|
||||
await super.connectedCallback();
|
||||
// Initialize subscribed incidents from the property
|
||||
if (this.subscribedIncidentIds.length > 0) {
|
||||
this.subscribedIncidents = new Set(this.subscribedIncidentIds);
|
||||
}
|
||||
}
|
||||
|
||||
updated(changedProperties: Map<string, any>) {
|
||||
super.updated(changedProperties);
|
||||
if (changedProperties.has('subscribedIncidentIds')) {
|
||||
this.subscribedIncidents = new Set(this.subscribedIncidentIds);
|
||||
}
|
||||
}
|
||||
|
||||
public static styles = [
|
||||
plugins.domtools.elementBasic.staticStyles,
|
||||
sharedStyles.commonStyles,
|
||||
css`
|
||||
:host {
|
||||
display: block;
|
||||
background: ${cssManager.bdTheme('#eeeeeb', '#222222')};
|
||||
font-family: Inter;
|
||||
color: ${cssManager.bdTheme('#333333', '#ffffff')};
|
||||
background: transparent;
|
||||
font-family: ${unsafeCSS(sharedStyles.fonts.base)};
|
||||
color: ${sharedStyles.colors.text.primary};
|
||||
}
|
||||
|
||||
.mainbox {
|
||||
max-width: 900px;
|
||||
margin: auto;
|
||||
.container {
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
padding: 0 ${unsafeCSS(sharedStyles.spacing.lg)} ${unsafeCSS(sharedStyles.spacing.lg)} ${unsafeCSS(sharedStyles.spacing.lg)};
|
||||
}
|
||||
|
||||
.noIncidentBox {
|
||||
background: ${cssManager.bdTheme('#ffffff', '#333333')};;
|
||||
padding: 10px;
|
||||
margin-bottom: 15px;
|
||||
border-radius: 3px;
|
||||
background: ${sharedStyles.colors.background.card};
|
||||
padding: ${unsafeCSS(sharedStyles.spacing.xl)};
|
||||
margin-bottom: ${unsafeCSS(sharedStyles.spacing.lg)};
|
||||
border-radius: ${unsafeCSS(sharedStyles.borderRadius.lg)};
|
||||
border: 1px solid ${sharedStyles.colors.border.default};
|
||||
text-align: center;
|
||||
color: ${sharedStyles.colors.text.secondary};
|
||||
box-shadow: ${unsafeCSS(sharedStyles.shadows.sm)};
|
||||
animation: fadeInUp 0.4s ${unsafeCSS(sharedStyles.easings.default)} both;
|
||||
}
|
||||
|
||||
/* Staggered entrance animations */
|
||||
.incident-card {
|
||||
background: ${sharedStyles.colors.background.card};
|
||||
border-radius: ${unsafeCSS(sharedStyles.borderRadius.lg)};
|
||||
margin-bottom: ${unsafeCSS(sharedStyles.spacing.lg)};
|
||||
overflow: hidden;
|
||||
box-shadow: ${unsafeCSS(sharedStyles.shadows.sm)};
|
||||
border: 1px solid ${sharedStyles.colors.border.default};
|
||||
transition: all ${unsafeCSS(sharedStyles.durations.normal)} ${unsafeCSS(sharedStyles.easings.default)};
|
||||
animation: fadeInUp 0.4s ${unsafeCSS(sharedStyles.easings.default)} both;
|
||||
}
|
||||
|
||||
.incident-card:nth-child(1) { animation-delay: 0ms; }
|
||||
.incident-card:nth-child(2) { animation-delay: 50ms; }
|
||||
.incident-card:nth-child(3) { animation-delay: 100ms; }
|
||||
.incident-card:nth-child(4) { animation-delay: 150ms; }
|
||||
.incident-card:nth-child(5) { animation-delay: 200ms; }
|
||||
|
||||
.incident-card:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: ${unsafeCSS(sharedStyles.shadows.md)};
|
||||
border-color: ${sharedStyles.colors.border.muted};
|
||||
}
|
||||
|
||||
.incident-card.expanded {
|
||||
box-shadow: ${unsafeCSS(sharedStyles.shadows.lg)};
|
||||
}
|
||||
|
||||
/* Active incident pulse effect */
|
||||
.incident-card.active-incident {
|
||||
animation: fadeInUp 0.4s ${unsafeCSS(sharedStyles.easings.default)} both,
|
||||
incident-pulse 3s ease-in-out infinite;
|
||||
}
|
||||
|
||||
@keyframes fadeInUp {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(16px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes incident-pulse {
|
||||
0%, 100% {
|
||||
box-shadow: ${unsafeCSS(sharedStyles.shadows.sm)};
|
||||
}
|
||||
50% {
|
||||
box-shadow: ${unsafeCSS(sharedStyles.shadows.md)},
|
||||
0 0 0 2px ${cssManager.bdTheme('rgba(239, 68, 68, 0.15)', 'rgba(248, 113, 113, 0.2)')};
|
||||
}
|
||||
}
|
||||
|
||||
.incident-header {
|
||||
padding: ${unsafeCSS(sharedStyles.spacing.lg)} ${unsafeCSS(sharedStyles.spacing.xl)};
|
||||
border-left: 4px solid;
|
||||
display: flex;
|
||||
align-items: start;
|
||||
justify-content: space-between;
|
||||
gap: ${unsafeCSS(sharedStyles.spacing.md)};
|
||||
cursor: pointer;
|
||||
transition: background-color ${unsafeCSS(sharedStyles.durations.fast)} ${unsafeCSS(sharedStyles.easings.default)};
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.incident-header:hover {
|
||||
background: ${cssManager.bdTheme('rgba(0, 0, 0, 0.02)', 'rgba(255, 255, 255, 0.02)')};
|
||||
}
|
||||
|
||||
.incident-header.critical {
|
||||
border-left-color: ${sharedStyles.colors.status.major};
|
||||
}
|
||||
|
||||
.incident-header.major {
|
||||
border-left-color: ${sharedStyles.colors.status.partial};
|
||||
}
|
||||
|
||||
.incident-header.minor {
|
||||
border-left-color: ${sharedStyles.colors.status.degraded};
|
||||
}
|
||||
|
||||
.incident-header.maintenance {
|
||||
border-left-color: ${sharedStyles.colors.status.maintenance};
|
||||
}
|
||||
|
||||
.incident-title {
|
||||
font-size: 17px;
|
||||
font-weight: 600;
|
||||
margin: 0;
|
||||
line-height: 1.4;
|
||||
letter-spacing: -0.01em;
|
||||
}
|
||||
|
||||
.incident-meta {
|
||||
display: flex;
|
||||
gap: ${unsafeCSS(sharedStyles.spacing.lg)};
|
||||
margin-top: ${unsafeCSS(sharedStyles.spacing.sm)};
|
||||
font-size: 13px;
|
||||
color: ${sharedStyles.colors.text.secondary};
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.incident-meta span {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.incident-status {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
padding: 6px 12px;
|
||||
border-radius: ${unsafeCSS(sharedStyles.borderRadius.full)};
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.04em;
|
||||
flex-shrink: 0;
|
||||
transition: all ${unsafeCSS(sharedStyles.durations.fast)} ${unsafeCSS(sharedStyles.easings.default)};
|
||||
}
|
||||
|
||||
.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')};
|
||||
}
|
||||
|
||||
/* Pulse for investigating status */
|
||||
.incident-status.investigating .status-dot {
|
||||
animation: status-pulse 1.5s ease-in-out infinite;
|
||||
}
|
||||
|
||||
@keyframes status-pulse {
|
||||
0%, 100% { opacity: 1; transform: scale(1); }
|
||||
50% { opacity: 0.6; transform: scale(1.2); }
|
||||
}
|
||||
|
||||
.incident-body {
|
||||
padding: 0 ${unsafeCSS(sharedStyles.spacing.xl)} ${unsafeCSS(sharedStyles.spacing.xl)} ${unsafeCSS(sharedStyles.spacing.xl)};
|
||||
animation: slideDown 0.3s ${unsafeCSS(sharedStyles.easings.default)};
|
||||
}
|
||||
|
||||
@keyframes slideDown {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(-8px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
.incident-impact {
|
||||
margin: ${unsafeCSS(sharedStyles.spacing.md)} 0;
|
||||
padding: ${unsafeCSS(sharedStyles.spacing.md)} ${unsafeCSS(sharedStyles.spacing.lg)};
|
||||
background: ${sharedStyles.colors.background.secondary};
|
||||
border-radius: ${unsafeCSS(sharedStyles.borderRadius.base)};
|
||||
font-size: 14px;
|
||||
line-height: 1.6;
|
||||
border-left: 3px solid ${sharedStyles.colors.border.muted};
|
||||
}
|
||||
|
||||
.affected-services {
|
||||
margin-top: ${unsafeCSS(sharedStyles.spacing.lg)};
|
||||
}
|
||||
|
||||
.affected-services-title {
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
margin-bottom: ${unsafeCSS(sharedStyles.spacing.sm)};
|
||||
color: ${sharedStyles.colors.text.secondary};
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.04em;
|
||||
}
|
||||
|
||||
.service-tag {
|
||||
display: inline-block;
|
||||
padding: 4px 10px;
|
||||
margin: 2px;
|
||||
background: ${sharedStyles.colors.background.muted};
|
||||
border-radius: ${unsafeCSS(sharedStyles.borderRadius.sm)};
|
||||
font-size: 12px;
|
||||
color: ${sharedStyles.colors.text.secondary};
|
||||
transition: all ${unsafeCSS(sharedStyles.durations.fast)} ${unsafeCSS(sharedStyles.easings.default)};
|
||||
}
|
||||
|
||||
.service-tag:hover {
|
||||
background: ${sharedStyles.colors.background.secondary};
|
||||
color: ${sharedStyles.colors.text.primary};
|
||||
}
|
||||
|
||||
/* Timeline visualization for updates */
|
||||
.incident-updates {
|
||||
margin-top: ${unsafeCSS(sharedStyles.spacing.xl)};
|
||||
border-top: 1px solid ${sharedStyles.colors.border.default};
|
||||
padding-top: ${unsafeCSS(sharedStyles.spacing.lg)};
|
||||
}
|
||||
|
||||
.updates-title {
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.04em;
|
||||
margin: 0 0 ${unsafeCSS(sharedStyles.spacing.lg)} 0;
|
||||
color: ${sharedStyles.colors.text.secondary};
|
||||
}
|
||||
|
||||
.timeline {
|
||||
position: relative;
|
||||
padding-left: 24px;
|
||||
}
|
||||
|
||||
.update-item {
|
||||
position: relative;
|
||||
padding-left: ${unsafeCSS(sharedStyles.spacing.lg)};
|
||||
padding-bottom: ${unsafeCSS(sharedStyles.spacing.lg)};
|
||||
animation: fadeInUp 0.3s ${unsafeCSS(sharedStyles.easings.default)} both;
|
||||
}
|
||||
|
||||
.update-item:last-child {
|
||||
padding-bottom: 0;
|
||||
}
|
||||
|
||||
/* Vertical connector line from each dot to the next */
|
||||
/* Dot: left -22px, width 12px + border 2px*2 = 16px total, center at -14px */
|
||||
.update-item:not(:last-child)::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
left: -15px;
|
||||
top: 18px;
|
||||
bottom: 0;
|
||||
width: 2px;
|
||||
background: ${sharedStyles.colors.border.default};
|
||||
}
|
||||
|
||||
/* Timeline dot */
|
||||
.update-item::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
left: -22px;
|
||||
top: 4px;
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
border-radius: 50%;
|
||||
background: ${sharedStyles.colors.background.card};
|
||||
border: 2px solid ${sharedStyles.colors.border.muted};
|
||||
z-index: 1;
|
||||
transition: all ${unsafeCSS(sharedStyles.durations.fast)} ${unsafeCSS(sharedStyles.easings.default)};
|
||||
}
|
||||
|
||||
.update-item:first-child::before {
|
||||
border-color: ${sharedStyles.colors.status.operational};
|
||||
background: ${sharedStyles.colors.status.operational};
|
||||
box-shadow: 0 0 0 3px ${cssManager.bdTheme('rgba(22, 163, 74, 0.15)', 'rgba(34, 197, 94, 0.2)')};
|
||||
}
|
||||
|
||||
.update-item:hover::before {
|
||||
transform: scale(1.2);
|
||||
border-color: ${sharedStyles.colors.text.secondary};
|
||||
}
|
||||
|
||||
.update-time {
|
||||
font-size: 11px;
|
||||
color: ${sharedStyles.colors.text.muted};
|
||||
margin-bottom: 4px;
|
||||
font-family: ${unsafeCSS(sharedStyles.fonts.mono)};
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.update-status-badge {
|
||||
display: inline-flex;
|
||||
padding: 2px 6px;
|
||||
border-radius: 4px;
|
||||
font-size: 10px;
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.02em;
|
||||
background: ${sharedStyles.colors.background.muted};
|
||||
color: ${sharedStyles.colors.text.secondary};
|
||||
}
|
||||
|
||||
.update-message {
|
||||
font-size: 14px;
|
||||
line-height: 1.6;
|
||||
color: ${sharedStyles.colors.text.primary};
|
||||
}
|
||||
|
||||
.update-author {
|
||||
font-size: 12px;
|
||||
color: ${sharedStyles.colors.text.muted};
|
||||
margin-top: 4px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.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: shimmer 1.5s infinite;
|
||||
border-radius: ${unsafeCSS(sharedStyles.borderRadius.lg)};
|
||||
margin-bottom: ${unsafeCSS(sharedStyles.spacing.lg)};
|
||||
}
|
||||
|
||||
@keyframes shimmer {
|
||||
0% { background-position: 200% 0; }
|
||||
100% { background-position: -200% 0; }
|
||||
}
|
||||
|
||||
.show-more {
|
||||
text-align: center;
|
||||
margin-top: ${unsafeCSS(sharedStyles.spacing.xl)};
|
||||
}
|
||||
|
||||
.show-more-button {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 8px;
|
||||
padding: 10px 20px;
|
||||
background: transparent;
|
||||
border: 1px solid ${sharedStyles.colors.border.default};
|
||||
border-radius: ${unsafeCSS(sharedStyles.borderRadius.base)};
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
transition: all ${unsafeCSS(sharedStyles.durations.fast)} ${unsafeCSS(sharedStyles.easings.default)};
|
||||
color: ${sharedStyles.colors.text.primary};
|
||||
font-family: ${unsafeCSS(sharedStyles.fonts.base)};
|
||||
}
|
||||
|
||||
.show-more-button:hover {
|
||||
background: ${sharedStyles.colors.background.secondary};
|
||||
border-color: ${sharedStyles.colors.border.muted};
|
||||
transform: translateY(-2px);
|
||||
box-shadow: ${unsafeCSS(sharedStyles.shadows.sm)};
|
||||
}
|
||||
|
||||
.show-more-button:active {
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
.incident-actions {
|
||||
display: flex;
|
||||
gap: ${unsafeCSS(sharedStyles.spacing.md)};
|
||||
align-items: center;
|
||||
margin-top: ${unsafeCSS(sharedStyles.spacing.lg)};
|
||||
padding-top: ${unsafeCSS(sharedStyles.spacing.lg)};
|
||||
border-top: 1px solid ${sharedStyles.colors.border.default};
|
||||
}
|
||||
|
||||
.subscribe-button {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
padding: 8px 14px;
|
||||
background: transparent;
|
||||
border: 1px solid ${sharedStyles.colors.border.default};
|
||||
border-radius: ${unsafeCSS(sharedStyles.borderRadius.base)};
|
||||
cursor: pointer;
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
transition: all ${unsafeCSS(sharedStyles.durations.fast)} ${unsafeCSS(sharedStyles.easings.default)};
|
||||
color: ${sharedStyles.colors.text.primary};
|
||||
font-family: ${unsafeCSS(sharedStyles.fonts.base)};
|
||||
}
|
||||
|
||||
.subscribe-button:hover {
|
||||
background: ${sharedStyles.colors.background.secondary};
|
||||
border-color: ${sharedStyles.colors.border.muted};
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.subscribe-button.subscribed {
|
||||
background: ${cssManager.bdTheme('#f0fdf4', '#064e3b')};
|
||||
border-color: ${cssManager.bdTheme('#86efac', '#047857')};
|
||||
color: ${cssManager.bdTheme('#047857', '#86efac')};
|
||||
}
|
||||
|
||||
.subscribe-button.subscribed:hover {
|
||||
background: ${cssManager.bdTheme('#dcfce7', '#065f46')};
|
||||
}
|
||||
|
||||
.collapsed-hint {
|
||||
font-size: 12px;
|
||||
color: ${sharedStyles.colors.text.secondary};
|
||||
text-align: center;
|
||||
margin-top: ${unsafeCSS(sharedStyles.spacing.md)};
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
/* Expand icon animation */
|
||||
.expand-icon {
|
||||
transition: transform ${unsafeCSS(sharedStyles.durations.normal)} ${unsafeCSS(sharedStyles.easings.default)};
|
||||
}
|
||||
|
||||
.expand-icon.rotated {
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
|
||||
@media (max-width: 640px) {
|
||||
.container {
|
||||
padding: 0 ${unsafeCSS(sharedStyles.spacing.md)} ${unsafeCSS(sharedStyles.spacing.md)} ${unsafeCSS(sharedStyles.spacing.md)};
|
||||
}
|
||||
|
||||
.incident-header {
|
||||
padding: ${unsafeCSS(sharedStyles.spacing.md)};
|
||||
}
|
||||
|
||||
.incident-body {
|
||||
padding: 0 ${unsafeCSS(sharedStyles.spacing.md)} ${unsafeCSS(sharedStyles.spacing.md)} ${unsafeCSS(sharedStyles.spacing.md)};
|
||||
}
|
||||
|
||||
.incident-meta {
|
||||
flex-direction: column;
|
||||
gap: ${unsafeCSS(sharedStyles.spacing.xs)};
|
||||
}
|
||||
|
||||
.timeline {
|
||||
padding-left: 20px;
|
||||
}
|
||||
|
||||
.update-item::before {
|
||||
left: -18px;
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
}
|
||||
|
||||
/* Mobile dot: left -18px, width 10px + border 2px*2 = 14px, center at -11px */
|
||||
.update-item:not(:last-child)::after {
|
||||
left: -12px;
|
||||
top: 16px;
|
||||
}
|
||||
}
|
||||
`,
|
||||
];
|
||||
|
||||
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 ? html`
|
||||
${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);
|
||||
|
||||
const isActive = isCurrent && latestUpdate?.status !== 'resolved';
|
||||
|
||||
return html`
|
||||
<div class="incident-card ${this.expandedIncidents.has(incident.id) ? 'expanded' : ''} ${isActive ? 'active-incident' : ''}">
|
||||
<div class="incident-header ${incident.severity}" @click=${() => this.toggleIncident(incident.id)}>
|
||||
<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>
|
||||
${!this.expandedIncidents.has(incident.id) ? html`
|
||||
<div style="
|
||||
margin-top: ${unsafeCSS(sharedStyles.spacing.sm)};
|
||||
font-size: 13px;
|
||||
color: ${sharedStyles.colors.text.secondary};
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: ${unsafeCSS(sharedStyles.spacing.md)};
|
||||
">
|
||||
${incident.impact ? html`
|
||||
<span style="
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
max-width: 500px;
|
||||
">${incident.impact}</span>
|
||||
` : ''}
|
||||
<span style="
|
||||
font-size: 12px;
|
||||
color: ${cssManager.bdTheme('#9ca3af', '#71717a')};
|
||||
">
|
||||
${incident.updates.length} update${incident.updates.length !== 1 ? 's' : ''}
|
||||
</span>
|
||||
</div>
|
||||
` : ''}
|
||||
</div>
|
||||
<div style="display: flex; align-items: center; gap: ${unsafeCSS(sharedStyles.spacing.md)};">
|
||||
<div class="incident-status ${latestUpdate.status}">
|
||||
${this.getStatusIcon(latestUpdate.status)}
|
||||
${latestUpdate.status.replace(/_/g, ' ')}
|
||||
</div>
|
||||
<div class="expand-icon" style="
|
||||
font-size: 10px;
|
||||
color: ${cssManager.bdTheme('#6b7280', '#a1a1aa')};
|
||||
transition: transform 0.2s ease;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
border-radius: 4px;
|
||||
background: ${cssManager.bdTheme('#f3f4f6', '#27272a')};
|
||||
${this.expandedIncidents.has(incident.id) ? 'transform: rotate(180deg);' : ''}
|
||||
">
|
||||
▼
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
${this.expandedIncidents.has(incident.id) ? html`
|
||||
<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 class="updates-title">Updates</h4>
|
||||
<div class="timeline">
|
||||
${incident.updates.slice(-3).reverse().map((update, index) => this.renderUpdate(update, index))}
|
||||
</div>
|
||||
</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 class="incident-actions">
|
||||
<button
|
||||
class="subscribe-button ${this.isSubscribedToIncident(incident.id) ? 'subscribed' : ''}"
|
||||
@click=${(e: Event) => {
|
||||
e.stopPropagation();
|
||||
this.handleIncidentSubscribe(incident);
|
||||
}}
|
||||
>
|
||||
${this.isSubscribedToIncident(incident.id) ? html`
|
||||
<svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M11.6667 3.5L5.25 9.91667L2.33334 7" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
Subscribed to updates
|
||||
` : html`
|
||||
<svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M10.5 5.25V8.75C10.5 9.34674 10.2629 9.91903 9.84099 10.341C9.41903 10.7629 8.84674 11 8.25 11L3.75 11C3.15326 11 2.58097 10.7629 2.15901 10.341C1.73705 9.91903 1.5 9.34674 1.5 8.75V4.25C1.5 3.65326 1.73705 3.08097 2.15901 2.65901C2.58097 2.23705 3.15326 2 3.75 2H7.25" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M9 1.5H12.5M12.5 1.5V5M12.5 1.5L6 8" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
Subscribe to updates
|
||||
`}
|
||||
</button>
|
||||
${isCurrent ? html`
|
||||
<span style="
|
||||
font-size: 12px;
|
||||
color: ${cssManager.bdTheme('#6b7280', '#a1a1aa')};
|
||||
">Get notified when this incident is updated or resolved</span>
|
||||
` : ''}
|
||||
</div>
|
||||
</div>
|
||||
` : ''}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
private renderUpdate(update: any, index: number = 0): TemplateResult {
|
||||
return html`
|
||||
<div class="update-item" style="animation-delay: ${index * 50}ms">
|
||||
<div class="update-time">
|
||||
${this.formatDate(update.timestamp)}
|
||||
${update.status ? html`<span class="update-status-badge">${update.status}</span>` : ''}
|
||||
</div>
|
||||
<div class="update-message">${update.message}</div>
|
||||
${update.author ? html`
|
||||
<div class="update-author">
|
||||
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2"></path>
|
||||
<circle cx="12" cy="7" r="4"></circle>
|
||||
</svg>
|
||||
${update.author}
|
||||
</div>
|
||||
` : ''}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
private getStatusIcon(status: string): TemplateResult {
|
||||
return html`<span class="status-dot" style="
|
||||
display: inline-block;
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
border-radius: 50%;
|
||||
background: ${status === 'resolved' ? sharedStyles.colors.status.operational :
|
||||
status === 'monitoring' ? sharedStyles.colors.status.maintenance :
|
||||
status === 'identified' ? sharedStyles.colors.status.degraded :
|
||||
sharedStyles.colors.status.partial};
|
||||
"></span>`;
|
||||
}
|
||||
|
||||
private formatDate(timestamp: number): string {
|
||||
const date = new Date(timestamp);
|
||||
const now = Date.now();
|
||||
const diff = now - timestamp;
|
||||
|
||||
// Less than 1 hour ago
|
||||
if (diff < 60 * 60 * 1000) {
|
||||
const minutes = Math.floor(diff / (60 * 1000));
|
||||
return `${minutes} minute${minutes !== 1 ? 's' : ''} ago`;
|
||||
}
|
||||
|
||||
// Less than 24 hours ago
|
||||
if (diff < 24 * 60 * 60 * 1000) {
|
||||
const hours = Math.floor(diff / (60 * 60 * 1000));
|
||||
return `${hours} hour${hours !== 1 ? 's' : ''} ago`;
|
||||
}
|
||||
|
||||
// Less than 7 days ago
|
||||
if (diff < 7 * 24 * 60 * 60 * 1000) {
|
||||
const days = Math.floor(diff / (24 * 60 * 60 * 1000));
|
||||
return `${days} day${days !== 1 ? 's' : ''} ago`;
|
||||
}
|
||||
|
||||
// Default to full date
|
||||
return date.toLocaleDateString('en-US', {
|
||||
month: 'short',
|
||||
day: 'numeric',
|
||||
year: date.getFullYear() !== new Date().getFullYear() ? 'numeric' : undefined
|
||||
});
|
||||
}
|
||||
|
||||
private formatDuration(milliseconds: number): string {
|
||||
const minutes = Math.floor(milliseconds / (60 * 1000));
|
||||
const hours = Math.floor(minutes / 60);
|
||||
const days = Math.floor(hours / 24);
|
||||
|
||||
if (days > 0) {
|
||||
return `${days}d ${hours % 24}h`;
|
||||
} else if (hours > 0) {
|
||||
return `${hours}h ${minutes % 60}m`;
|
||||
} else {
|
||||
return `${minutes}m`;
|
||||
}
|
||||
}
|
||||
|
||||
private toggleIncident(incidentId: string) {
|
||||
const newExpanded = new Set(this.expandedIncidents);
|
||||
if (newExpanded.has(incidentId)) {
|
||||
newExpanded.delete(incidentId);
|
||||
} else {
|
||||
newExpanded.add(incidentId);
|
||||
}
|
||||
this.expandedIncidents = newExpanded;
|
||||
}
|
||||
|
||||
private handleIncidentClick(incident: IIncidentDetails) {
|
||||
this.dispatchEvent(new CustomEvent('incidentClick', {
|
||||
detail: { incident },
|
||||
bubbles: true,
|
||||
composed: true
|
||||
}));
|
||||
}
|
||||
|
||||
private handleShowMore() {
|
||||
// This would typically load more incidents or navigate to a full list
|
||||
console.log('Show more incidents');
|
||||
}
|
||||
|
||||
private isSubscribedToIncident(incidentId: string): boolean {
|
||||
return this.subscribedIncidents.has(incidentId);
|
||||
}
|
||||
|
||||
private handleIncidentSubscribe(incident: IIncidentDetails) {
|
||||
const newSubscribed = new Set(this.subscribedIncidents);
|
||||
if (newSubscribed.has(incident.id)) {
|
||||
newSubscribed.delete(incident.id);
|
||||
this.dispatchEvent(new CustomEvent('incidentUnsubscribe', {
|
||||
detail: {
|
||||
incident,
|
||||
incidentId: incident.id
|
||||
},
|
||||
bubbles: true,
|
||||
composed: true
|
||||
}));
|
||||
} else {
|
||||
newSubscribed.add(incident.id);
|
||||
this.dispatchEvent(new CustomEvent('incidentSubscribe', {
|
||||
detail: {
|
||||
incident,
|
||||
incidentId: incident.id,
|
||||
incidentTitle: incident.title,
|
||||
affectedServices: incident.affectedServices
|
||||
},
|
||||
bubbles: true,
|
||||
composed: true
|
||||
}));
|
||||
}
|
||||
this.subscribedIncidents = newSubscribed;
|
||||
}
|
||||
|
||||
public dispatchReportNewIncident() {
|
||||
this.dispatchEvent(new CustomEvent('reportNewIncident', {}));
|
||||
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
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
25
ts_web/elements/upl-statuspage-pagetitle.demo.ts
Normal file
25
ts_web/elements/upl-statuspage-pagetitle.demo.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
import { html } from '@design.estate/dees-element';
|
||||
|
||||
export const demoFunc = () => html`
|
||||
<style>
|
||||
.demo-container {
|
||||
background: #fafafa;
|
||||
padding: 40px 0;
|
||||
min-height: 400px;
|
||||
}
|
||||
</style>
|
||||
<div class="demo-container">
|
||||
<upl-statuspage-pagetitle
|
||||
.pageTitle=${'System Status'}
|
||||
.pageSubtitle=${'Real-time operational status and incident reports for all services'}
|
||||
></upl-statuspage-pagetitle>
|
||||
|
||||
<br>
|
||||
|
||||
<upl-statuspage-pagetitle
|
||||
.pageTitle=${'API Documentation'}
|
||||
.pageSubtitle=${'Comprehensive guides and references for integrating with our platform'}
|
||||
.centered=${true}
|
||||
></upl-statuspage-pagetitle>
|
||||
</div>
|
||||
`;
|
||||
89
ts_web/elements/upl-statuspage-pagetitle.ts
Normal file
89
ts_web/elements/upl-statuspage-pagetitle.ts
Normal file
@@ -0,0 +1,89 @@
|
||||
import { DeesElement, property, html, customElement, type TemplateResult, css, cssManager, unsafeCSS } from '@design.estate/dees-element';
|
||||
import * as domtools from '@design.estate/dees-domtools';
|
||||
import * as sharedStyles from '../styles/shared.styles.js';
|
||||
import { demoFunc } from './upl-statuspage-pagetitle.demo.js';
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
'upl-statuspage-pagetitle': UplStatuspagePagetitle;
|
||||
}
|
||||
}
|
||||
|
||||
@customElement('upl-statuspage-pagetitle')
|
||||
export class UplStatuspagePagetitle extends DeesElement {
|
||||
public static demo = demoFunc;
|
||||
|
||||
@property({ type: String })
|
||||
accessor pageTitle: string = 'System Status';
|
||||
|
||||
@property({ type: String })
|
||||
accessor pageSubtitle: string = '';
|
||||
|
||||
@property({ type: Boolean })
|
||||
accessor centered: boolean = false;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
}
|
||||
|
||||
public static styles = [
|
||||
domtools.elementBasic.staticStyles,
|
||||
css`
|
||||
:host {
|
||||
display: block;
|
||||
font-family: ${unsafeCSS(sharedStyles.fonts.base)};
|
||||
}
|
||||
|
||||
.title-container {
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
padding: ${unsafeCSS(sharedStyles.spacing.lg)};
|
||||
}
|
||||
|
||||
.title-container.centered {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 48px;
|
||||
font-weight: 700;
|
||||
letter-spacing: -0.03em;
|
||||
line-height: 1.1;
|
||||
color: ${sharedStyles.colors.text.primary};
|
||||
margin: 0 0 ${unsafeCSS(sharedStyles.spacing.md)} 0;
|
||||
}
|
||||
|
||||
p {
|
||||
font-size: 18px;
|
||||
color: ${sharedStyles.colors.text.secondary};
|
||||
margin: 0;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
@media (max-width: 640px) {
|
||||
.title-container {
|
||||
padding: ${unsafeCSS(sharedStyles.spacing.lg)} ${unsafeCSS(sharedStyles.spacing.md)};
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 32px;
|
||||
}
|
||||
|
||||
p {
|
||||
font-size: 16px;
|
||||
}
|
||||
}
|
||||
`
|
||||
]
|
||||
|
||||
public render(): TemplateResult {
|
||||
return html`
|
||||
<div class="title-container ${this.centered ? 'centered' : ''}">
|
||||
<h1>${this.pageTitle}</h1>
|
||||
${this.pageSubtitle ? html`
|
||||
<p>${this.pageSubtitle}</p>
|
||||
` : ''}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
}
|
||||
315
ts_web/elements/upl-statuspage-statsgrid.demo.ts
Normal file
315
ts_web/elements/upl-statuspage-statsgrid.demo.ts
Normal file
@@ -0,0 +1,315 @@
|
||||
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;
|
||||
}
|
||||
.demo-button.active {
|
||||
background: #2196F3;
|
||||
color: white;
|
||||
border-color: #2196F3;
|
||||
}
|
||||
</style>
|
||||
|
||||
<div class="demo-container">
|
||||
<!-- Normal Operation -->
|
||||
<div class="demo-section">
|
||||
<div class="demo-title">Normal Operation</div>
|
||||
<dees-demowrapper
|
||||
.runAfterRender=${async (wrapperElement: any) => {
|
||||
const statsGrid = wrapperElement.querySelector('upl-statuspage-statsgrid') as any;
|
||||
|
||||
// Set initial values
|
||||
statsGrid.currentStatus = 'operational';
|
||||
statsGrid.uptime = 99.95;
|
||||
statsGrid.avgResponseTime = 125;
|
||||
statsGrid.totalIncidents = 0;
|
||||
statsGrid.affectedServices = 0;
|
||||
statsGrid.totalServices = 12;
|
||||
statsGrid.timePeriod = '90 days';
|
||||
}}
|
||||
>
|
||||
<upl-statuspage-statsgrid></upl-statuspage-statsgrid>
|
||||
</dees-demowrapper>
|
||||
</div>
|
||||
|
||||
<!-- Degraded Performance -->
|
||||
<div class="demo-section">
|
||||
<div class="demo-title">Degraded Performance</div>
|
||||
<dees-demowrapper
|
||||
.runAfterRender=${async (wrapperElement: any) => {
|
||||
const statsGrid = wrapperElement.querySelector('upl-statuspage-statsgrid') as any;
|
||||
|
||||
statsGrid.currentStatus = 'degraded';
|
||||
statsGrid.uptime = 98.50;
|
||||
statsGrid.avgResponseTime = 450;
|
||||
statsGrid.totalIncidents = 3;
|
||||
statsGrid.affectedServices = 2;
|
||||
statsGrid.totalServices = 12;
|
||||
statsGrid.timePeriod = '30 days';
|
||||
}}
|
||||
>
|
||||
<upl-statuspage-statsgrid></upl-statuspage-statsgrid>
|
||||
</dees-demowrapper>
|
||||
</div>
|
||||
|
||||
<!-- Major Outage -->
|
||||
<div class="demo-section">
|
||||
<div class="demo-title">Major Outage</div>
|
||||
<dees-demowrapper
|
||||
.runAfterRender=${async (wrapperElement: any) => {
|
||||
const statsGrid = wrapperElement.querySelector('upl-statuspage-statsgrid') as any;
|
||||
|
||||
statsGrid.currentStatus = 'major_outage';
|
||||
statsGrid.uptime = 95.20;
|
||||
statsGrid.avgResponseTime = 1250;
|
||||
statsGrid.totalIncidents = 8;
|
||||
statsGrid.affectedServices = 7;
|
||||
statsGrid.totalServices = 12;
|
||||
statsGrid.timePeriod = '7 days';
|
||||
}}
|
||||
>
|
||||
<upl-statuspage-statsgrid></upl-statuspage-statsgrid>
|
||||
</dees-demowrapper>
|
||||
</div>
|
||||
|
||||
<!-- Interactive Demo -->
|
||||
<div class="demo-section">
|
||||
<div class="demo-title">Interactive Demo</div>
|
||||
<dees-demowrapper
|
||||
.runAfterRender=${async (wrapperElement: any) => {
|
||||
const statsGrid = wrapperElement.querySelector('upl-statuspage-statsgrid') as any;
|
||||
|
||||
// Initial state
|
||||
statsGrid.currentStatus = 'operational';
|
||||
statsGrid.uptime = 99.99;
|
||||
statsGrid.avgResponseTime = 85;
|
||||
statsGrid.totalIncidents = 0;
|
||||
statsGrid.affectedServices = 0;
|
||||
statsGrid.totalServices = 15;
|
||||
statsGrid.timePeriod = '90 days';
|
||||
|
||||
// Create controls
|
||||
const controls = document.createElement('div');
|
||||
controls.className = 'demo-controls';
|
||||
|
||||
// Status buttons
|
||||
const statuses = ['operational', 'degraded', 'partial_outage', 'major_outage', 'maintenance'];
|
||||
statuses.forEach((status, index) => {
|
||||
const button = document.createElement('button');
|
||||
button.className = 'demo-button' + (index === 0 ? ' active' : '');
|
||||
button.textContent = status.replace(/_/g, ' ').replace(/\b\w/g, l => l.toUpperCase());
|
||||
button.onclick = () => {
|
||||
controls.querySelectorAll('.demo-button').forEach(btn => btn.classList.remove('active'));
|
||||
button.classList.add('active');
|
||||
|
||||
statsGrid.currentStatus = status;
|
||||
|
||||
// Adjust other values based on status
|
||||
switch (status) {
|
||||
case 'operational':
|
||||
statsGrid.uptime = 99.99;
|
||||
statsGrid.avgResponseTime = 85;
|
||||
statsGrid.totalIncidents = 0;
|
||||
statsGrid.affectedServices = 0;
|
||||
break;
|
||||
case 'degraded':
|
||||
statsGrid.uptime = 98.50;
|
||||
statsGrid.avgResponseTime = 350;
|
||||
statsGrid.totalIncidents = 2;
|
||||
statsGrid.affectedServices = 1;
|
||||
break;
|
||||
case 'partial_outage':
|
||||
statsGrid.uptime = 97.00;
|
||||
statsGrid.avgResponseTime = 750;
|
||||
statsGrid.totalIncidents = 5;
|
||||
statsGrid.affectedServices = 3;
|
||||
break;
|
||||
case 'major_outage':
|
||||
statsGrid.uptime = 94.50;
|
||||
statsGrid.avgResponseTime = 1500;
|
||||
statsGrid.totalIncidents = 10;
|
||||
statsGrid.affectedServices = 8;
|
||||
break;
|
||||
case 'maintenance':
|
||||
statsGrid.uptime = 99.00;
|
||||
statsGrid.avgResponseTime = 150;
|
||||
statsGrid.totalIncidents = 1;
|
||||
statsGrid.affectedServices = 2;
|
||||
break;
|
||||
}
|
||||
};
|
||||
controls.appendChild(button);
|
||||
});
|
||||
|
||||
wrapperElement.appendChild(controls);
|
||||
|
||||
// Add time period selector
|
||||
const timePeriodControls = document.createElement('div');
|
||||
timePeriodControls.className = 'demo-controls';
|
||||
timePeriodControls.style.marginTop = '10px';
|
||||
|
||||
const periods = ['24 hours', '7 days', '30 days', '90 days'];
|
||||
periods.forEach((period, index) => {
|
||||
const button = document.createElement('button');
|
||||
button.className = 'demo-button' + (index === 3 ? ' active' : '');
|
||||
button.textContent = period;
|
||||
button.onclick = () => {
|
||||
timePeriodControls.querySelectorAll('.demo-button').forEach(btn => btn.classList.remove('active'));
|
||||
button.classList.add('active');
|
||||
statsGrid.timePeriod = period;
|
||||
};
|
||||
timePeriodControls.appendChild(button);
|
||||
});
|
||||
|
||||
wrapperElement.appendChild(timePeriodControls);
|
||||
}}
|
||||
>
|
||||
<upl-statuspage-statsgrid></upl-statuspage-statsgrid>
|
||||
</dees-demowrapper>
|
||||
</div>
|
||||
|
||||
<!-- Loading State -->
|
||||
<div class="demo-section">
|
||||
<div class="demo-title">Loading State</div>
|
||||
<dees-demowrapper
|
||||
.runAfterRender=${async (wrapperElement: any) => {
|
||||
const statsGrid = wrapperElement.querySelector('upl-statuspage-statsgrid') as any;
|
||||
statsGrid.loading = true;
|
||||
|
||||
// Create toggle button
|
||||
const controls = document.createElement('div');
|
||||
controls.className = 'demo-controls';
|
||||
|
||||
const toggleButton = document.createElement('button');
|
||||
toggleButton.className = 'demo-button';
|
||||
toggleButton.textContent = 'Toggle Loading';
|
||||
toggleButton.onclick = () => {
|
||||
statsGrid.loading = !statsGrid.loading;
|
||||
if (!statsGrid.loading) {
|
||||
statsGrid.currentStatus = 'operational';
|
||||
statsGrid.uptime = 99.95;
|
||||
statsGrid.avgResponseTime = 125;
|
||||
statsGrid.totalIncidents = 0;
|
||||
statsGrid.affectedServices = 0;
|
||||
statsGrid.totalServices = 12;
|
||||
}
|
||||
};
|
||||
controls.appendChild(toggleButton);
|
||||
|
||||
wrapperElement.appendChild(controls);
|
||||
}}
|
||||
>
|
||||
<upl-statuspage-statsgrid></upl-statuspage-statsgrid>
|
||||
</dees-demowrapper>
|
||||
</div>
|
||||
|
||||
<!-- Real-time Updates -->
|
||||
<div class="demo-section">
|
||||
<div class="demo-title">Real-time Updates</div>
|
||||
<dees-demowrapper
|
||||
.runAfterRender=${async (wrapperElement: any) => {
|
||||
const statsGrid = wrapperElement.querySelector('upl-statuspage-statsgrid') as any;
|
||||
|
||||
// Initial values
|
||||
statsGrid.currentStatus = 'operational';
|
||||
statsGrid.uptime = 99.95;
|
||||
statsGrid.avgResponseTime = 100;
|
||||
statsGrid.totalIncidents = 0;
|
||||
statsGrid.affectedServices = 0;
|
||||
statsGrid.totalServices = 10;
|
||||
statsGrid.timePeriod = '24 hours';
|
||||
|
||||
// Simulate real-time updates
|
||||
let interval = setInterval(() => {
|
||||
// Slight variations in response time
|
||||
statsGrid.avgResponseTime = Math.floor(80 + Math.random() * 40);
|
||||
|
||||
// Occasionally change status
|
||||
if (Math.random() < 0.1) {
|
||||
const statuses = ['operational', 'degraded'];
|
||||
statsGrid.currentStatus = statuses[Math.floor(Math.random() * statuses.length)];
|
||||
|
||||
if (statsGrid.currentStatus === 'degraded') {
|
||||
statsGrid.avgResponseTime = Math.floor(300 + Math.random() * 200);
|
||||
statsGrid.totalIncidents = Math.min(statsGrid.totalIncidents + 1, 5);
|
||||
statsGrid.affectedServices = Math.min(Math.floor(Math.random() * 3) + 1, statsGrid.totalServices);
|
||||
statsGrid.uptime = Math.max(99.0, statsGrid.uptime - 0.05);
|
||||
} else {
|
||||
statsGrid.affectedServices = 0;
|
||||
}
|
||||
}
|
||||
}, 2000);
|
||||
|
||||
// Add control button
|
||||
const controls = document.createElement('div');
|
||||
controls.className = 'demo-controls';
|
||||
|
||||
const toggleButton = document.createElement('button');
|
||||
toggleButton.className = 'demo-button active';
|
||||
toggleButton.textContent = 'Stop Updates';
|
||||
toggleButton.onclick = () => {
|
||||
if (interval) {
|
||||
clearInterval(interval);
|
||||
interval = null;
|
||||
toggleButton.textContent = 'Start Updates';
|
||||
toggleButton.classList.remove('active');
|
||||
} else {
|
||||
interval = setInterval(() => {
|
||||
statsGrid.avgResponseTime = Math.floor(80 + Math.random() * 40);
|
||||
if (Math.random() < 0.1) {
|
||||
const statuses = ['operational', 'degraded'];
|
||||
statsGrid.currentStatus = statuses[Math.floor(Math.random() * statuses.length)];
|
||||
}
|
||||
}, 2000);
|
||||
toggleButton.textContent = 'Stop Updates';
|
||||
toggleButton.classList.add('active');
|
||||
}
|
||||
};
|
||||
controls.appendChild(toggleButton);
|
||||
|
||||
wrapperElement.appendChild(controls);
|
||||
|
||||
// Cleanup on unmount
|
||||
wrapperElement.addEventListener('remove', () => {
|
||||
if (interval) clearInterval(interval);
|
||||
});
|
||||
}}
|
||||
>
|
||||
<upl-statuspage-statsgrid></upl-statuspage-statsgrid>
|
||||
</dees-demowrapper>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
486
ts_web/elements/upl-statuspage-statsgrid.ts
Normal file
486
ts_web/elements/upl-statuspage-statsgrid.ts
Normal file
@@ -0,0 +1,486 @@
|
||||
import {
|
||||
DeesElement,
|
||||
property,
|
||||
html,
|
||||
customElement,
|
||||
type TemplateResult,
|
||||
css,
|
||||
cssManager,
|
||||
unsafeCSS,
|
||||
} from '@design.estate/dees-element';
|
||||
import * as domtools from '@design.estate/dees-domtools';
|
||||
import * as sharedStyles from '../styles/shared.styles.js';
|
||||
|
||||
import './internal/uplinternal-miniheading.js';
|
||||
import { demoFunc } from './upl-statuspage-statsgrid.demo.js';
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
'upl-statuspage-statsgrid': UplStatuspageStatsgrid;
|
||||
}
|
||||
}
|
||||
|
||||
@customElement('upl-statuspage-statsgrid')
|
||||
export class UplStatuspageStatsgrid extends DeesElement {
|
||||
public static demo = demoFunc;
|
||||
|
||||
@property({ type: Number })
|
||||
accessor uptime: number = 99.99;
|
||||
|
||||
@property({ type: Number })
|
||||
accessor avgResponseTime: number = 125;
|
||||
|
||||
@property({ type: Number })
|
||||
accessor totalIncidents: number = 0;
|
||||
|
||||
@property({ type: Number })
|
||||
accessor affectedServices: number = 0;
|
||||
|
||||
@property({ type: Number })
|
||||
accessor totalServices: number = 0;
|
||||
|
||||
@property({ type: String })
|
||||
accessor currentStatus: string = 'operational';
|
||||
|
||||
@property({ type: Boolean })
|
||||
accessor loading: boolean = false;
|
||||
|
||||
@property({ type: String })
|
||||
accessor timePeriod: string = '90 days';
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
}
|
||||
|
||||
public static styles = [
|
||||
domtools.elementBasic.staticStyles,
|
||||
sharedStyles.commonStyles,
|
||||
css`
|
||||
:host {
|
||||
display: block;
|
||||
background: transparent;
|
||||
font-family: ${unsafeCSS(sharedStyles.fonts.base)};
|
||||
color: ${sharedStyles.colors.text.primary};
|
||||
}
|
||||
|
||||
.container {
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
padding: 0 ${unsafeCSS(sharedStyles.spacing.lg)} ${unsafeCSS(sharedStyles.spacing.lg)} ${unsafeCSS(sharedStyles.spacing.lg)};
|
||||
}
|
||||
|
||||
.stats-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
||||
gap: ${unsafeCSS(sharedStyles.spacing.md)};
|
||||
}
|
||||
|
||||
.stat-card {
|
||||
background: ${sharedStyles.colors.background.card};
|
||||
border: 1px solid ${sharedStyles.colors.border.default};
|
||||
border-radius: ${unsafeCSS(sharedStyles.borderRadius.lg)};
|
||||
padding: ${unsafeCSS(sharedStyles.spacing.lg)};
|
||||
transition: all ${unsafeCSS(sharedStyles.durations.normal)} ${unsafeCSS(sharedStyles.easings.default)};
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
animation: fadeInUp 0.4s ${unsafeCSS(sharedStyles.easings.default)} both;
|
||||
}
|
||||
|
||||
.stat-card:nth-child(1) { animation-delay: 0ms; }
|
||||
.stat-card:nth-child(2) { animation-delay: 50ms; }
|
||||
.stat-card:nth-child(3) { animation-delay: 100ms; }
|
||||
.stat-card:nth-child(4) { animation-delay: 150ms; }
|
||||
|
||||
@keyframes fadeInUp {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(12px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
/* Status-colored top accent */
|
||||
.stat-card::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: 3px;
|
||||
background: ${sharedStyles.colors.border.muted};
|
||||
transition: all ${unsafeCSS(sharedStyles.durations.fast)} ${unsafeCSS(sharedStyles.easings.default)};
|
||||
}
|
||||
|
||||
/* Dynamic status-based accent colors for all stat cards */
|
||||
.stat-card.operational::before {
|
||||
background: ${sharedStyles.colors.status.operational};
|
||||
}
|
||||
|
||||
.stat-card.degraded::before {
|
||||
background: ${sharedStyles.colors.status.degraded};
|
||||
}
|
||||
|
||||
.stat-card.partial_outage::before {
|
||||
background: ${sharedStyles.colors.status.partial};
|
||||
}
|
||||
|
||||
.stat-card.major_outage::before {
|
||||
background: ${sharedStyles.colors.status.major};
|
||||
}
|
||||
|
||||
.stat-card.maintenance::before {
|
||||
background: ${sharedStyles.colors.status.maintenance};
|
||||
}
|
||||
|
||||
.stat-card:hover {
|
||||
border-color: ${sharedStyles.colors.border.muted};
|
||||
box-shadow: ${unsafeCSS(sharedStyles.shadows.md)};
|
||||
transform: translateY(-3px);
|
||||
}
|
||||
|
||||
.stat-card:hover::before {
|
||||
height: 4px;
|
||||
}
|
||||
|
||||
.stat-label {
|
||||
font-size: 11px;
|
||||
color: ${sharedStyles.colors.text.muted};
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.06em;
|
||||
font-weight: 600;
|
||||
margin-bottom: ${unsafeCSS(sharedStyles.spacing.sm)};
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.stat-label svg {
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
.stat-value {
|
||||
font-size: 28px;
|
||||
font-weight: 700;
|
||||
color: ${sharedStyles.colors.text.primary};
|
||||
font-variant-numeric: tabular-nums;
|
||||
line-height: 1.2;
|
||||
letter-spacing: -0.02em;
|
||||
transition: transform ${unsafeCSS(sharedStyles.durations.fast)} ${unsafeCSS(sharedStyles.easings.default)};
|
||||
}
|
||||
|
||||
.stat-card:hover .stat-value {
|
||||
transform: scale(1.02);
|
||||
}
|
||||
|
||||
.stat-unit {
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
color: ${sharedStyles.colors.text.secondary};
|
||||
margin-left: 2px;
|
||||
}
|
||||
|
||||
.stat-change {
|
||||
font-size: 12px;
|
||||
margin-top: ${unsafeCSS(sharedStyles.spacing.sm)};
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
padding: 4px 8px;
|
||||
border-radius: ${unsafeCSS(sharedStyles.borderRadius.sm)};
|
||||
width: fit-content;
|
||||
}
|
||||
|
||||
.stat-change.positive {
|
||||
color: ${sharedStyles.colors.status.operational};
|
||||
background: ${cssManager.bdTheme('rgba(22, 163, 74, 0.08)', 'rgba(34, 197, 94, 0.12)')};
|
||||
}
|
||||
|
||||
.stat-change.negative {
|
||||
color: ${sharedStyles.colors.status.partial};
|
||||
background: ${cssManager.bdTheme('rgba(239, 68, 68, 0.08)', 'rgba(248, 113, 113, 0.12)')};
|
||||
}
|
||||
|
||||
.stat-change.neutral {
|
||||
color: ${sharedStyles.colors.text.muted};
|
||||
background: ${sharedStyles.colors.background.muted};
|
||||
}
|
||||
|
||||
/* Status text color variations */
|
||||
.stat-value.operational {
|
||||
color: ${sharedStyles.colors.status.operational};
|
||||
}
|
||||
|
||||
.stat-value.degraded {
|
||||
color: ${sharedStyles.colors.status.degraded};
|
||||
}
|
||||
|
||||
.stat-value.partial_outage {
|
||||
color: ${sharedStyles.colors.status.partial};
|
||||
}
|
||||
|
||||
.stat-value.major_outage {
|
||||
color: ${sharedStyles.colors.status.major};
|
||||
}
|
||||
|
||||
.stat-value.maintenance {
|
||||
color: ${sharedStyles.colors.status.maintenance};
|
||||
}
|
||||
|
||||
.loading-skeleton {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
||||
gap: ${unsafeCSS(sharedStyles.spacing.md)};
|
||||
}
|
||||
|
||||
.skeleton-card {
|
||||
background: ${sharedStyles.colors.background.card};
|
||||
border: 1px solid ${sharedStyles.colors.border.default};
|
||||
border-radius: ${unsafeCSS(sharedStyles.borderRadius.lg)};
|
||||
padding: ${unsafeCSS(sharedStyles.spacing.lg)};
|
||||
height: 110px;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.skeleton-card::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: 3px;
|
||||
background: ${sharedStyles.colors.background.muted};
|
||||
}
|
||||
|
||||
.skeleton-label {
|
||||
height: 12px;
|
||||
width: 80px;
|
||||
background: ${sharedStyles.colors.background.muted};
|
||||
border-radius: 4px;
|
||||
margin-bottom: ${unsafeCSS(sharedStyles.spacing.sm)};
|
||||
animation: shimmer 1.5s infinite;
|
||||
}
|
||||
|
||||
.skeleton-value {
|
||||
height: 32px;
|
||||
width: 100px;
|
||||
background: ${sharedStyles.colors.background.muted};
|
||||
border-radius: 6px;
|
||||
animation: shimmer 1.5s infinite;
|
||||
animation-delay: 0.1s;
|
||||
}
|
||||
|
||||
.skeleton-change {
|
||||
height: 20px;
|
||||
width: 70px;
|
||||
background: ${sharedStyles.colors.background.muted};
|
||||
border-radius: 4px;
|
||||
margin-top: ${unsafeCSS(sharedStyles.spacing.sm)};
|
||||
animation: shimmer 1.5s infinite;
|
||||
animation-delay: 0.2s;
|
||||
}
|
||||
|
||||
@keyframes shimmer {
|
||||
0% { opacity: 0.5; }
|
||||
50% { opacity: 1; }
|
||||
100% { opacity: 0.5; }
|
||||
}
|
||||
|
||||
.status-indicator {
|
||||
display: inline-block;
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
border-radius: 50%;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.status-indicator.operational {
|
||||
background: ${sharedStyles.colors.status.operational};
|
||||
box-shadow: 0 0 0 2px ${cssManager.bdTheme('rgba(22, 163, 74, 0.2)', 'rgba(34, 197, 94, 0.25)')};
|
||||
animation: statusPulse 2s ease-in-out infinite;
|
||||
}
|
||||
|
||||
@keyframes statusPulse {
|
||||
0%, 100% { box-shadow: 0 0 0 2px ${cssManager.bdTheme('rgba(22, 163, 74, 0.2)', 'rgba(34, 197, 94, 0.25)')}; }
|
||||
50% { box-shadow: 0 0 0 4px ${cssManager.bdTheme('rgba(22, 163, 74, 0.1)', 'rgba(34, 197, 94, 0.15)')}; }
|
||||
}
|
||||
|
||||
.status-indicator.degraded {
|
||||
background: ${sharedStyles.colors.status.degraded};
|
||||
box-shadow: 0 0 0 2px ${cssManager.bdTheme('rgba(217, 119, 6, 0.2)', 'rgba(251, 191, 36, 0.25)')};
|
||||
}
|
||||
|
||||
.status-indicator.partial_outage {
|
||||
background: ${sharedStyles.colors.status.partial};
|
||||
box-shadow: 0 0 0 2px ${cssManager.bdTheme('rgba(239, 68, 68, 0.2)', 'rgba(248, 113, 113, 0.25)')};
|
||||
}
|
||||
|
||||
.status-indicator.major_outage {
|
||||
background: ${sharedStyles.colors.status.major};
|
||||
box-shadow: 0 0 0 2px ${cssManager.bdTheme('rgba(185, 28, 28, 0.2)', 'rgba(239, 68, 68, 0.25)')};
|
||||
animation: majorPulse 1s ease-in-out infinite;
|
||||
}
|
||||
|
||||
@keyframes majorPulse {
|
||||
0%, 100% { opacity: 1; }
|
||||
50% { opacity: 0.6; }
|
||||
}
|
||||
|
||||
.status-indicator.maintenance {
|
||||
background: ${sharedStyles.colors.status.maintenance};
|
||||
box-shadow: 0 0 0 2px ${cssManager.bdTheme('rgba(37, 99, 235, 0.2)', 'rgba(96, 165, 250, 0.25)')};
|
||||
}
|
||||
|
||||
@media (max-width: 640px) {
|
||||
.container {
|
||||
padding: 0 ${unsafeCSS(sharedStyles.spacing.md)} ${unsafeCSS(sharedStyles.spacing.md)} ${unsafeCSS(sharedStyles.spacing.md)};
|
||||
}
|
||||
|
||||
.stats-grid {
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
gap: ${unsafeCSS(sharedStyles.spacing.sm)};
|
||||
}
|
||||
|
||||
.stat-card {
|
||||
padding: ${unsafeCSS(sharedStyles.spacing.md)};
|
||||
}
|
||||
|
||||
.stat-value {
|
||||
font-size: 22px;
|
||||
}
|
||||
|
||||
.stat-label {
|
||||
font-size: 10px;
|
||||
}
|
||||
|
||||
.stat-label svg {
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
}
|
||||
}
|
||||
`,
|
||||
];
|
||||
|
||||
public render(): TemplateResult {
|
||||
return html`
|
||||
<div class="container">
|
||||
${this.loading ? html`
|
||||
<div class="loading-skeleton">
|
||||
${Array(4).fill(0).map(() => html`
|
||||
<div class="skeleton-card">
|
||||
<div class="skeleton-label"></div>
|
||||
<div class="skeleton-value"></div>
|
||||
</div>
|
||||
`)}
|
||||
</div>
|
||||
` : html`
|
||||
<div class="stats-grid">
|
||||
<div class="stat-card ${this.currentStatus}">
|
||||
<div class="stat-label">
|
||||
<span class="status-indicator ${this.currentStatus}"></span>
|
||||
Current Status
|
||||
</div>
|
||||
<div class="stat-value ${this.currentStatus}">
|
||||
${this.formatStatus(this.currentStatus)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="stat-card ${this.getUptimeCardStatus()}">
|
||||
<div class="stat-label">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<polyline points="22 12 18 12 15 21 9 3 6 12 2 12"></polyline>
|
||||
</svg>
|
||||
Uptime
|
||||
</div>
|
||||
<div class="stat-value">
|
||||
${this.uptime.toFixed(2)}<span class="stat-unit">%</span>
|
||||
</div>
|
||||
<div class="stat-change neutral">
|
||||
Last ${this.timePeriod}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="stat-card ${this.getResponseCardStatus()}">
|
||||
<div class="stat-label">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<circle cx="12" cy="12" r="10"></circle>
|
||||
<polyline points="12 6 12 12 16 14"></polyline>
|
||||
</svg>
|
||||
Avg Response
|
||||
</div>
|
||||
<div class="stat-value">
|
||||
${this.avgResponseTime}<span class="stat-unit">ms</span>
|
||||
</div>
|
||||
${this.renderResponseChange()}
|
||||
</div>
|
||||
|
||||
<div class="stat-card ${this.getIncidentCardStatus()}">
|
||||
<div class="stat-label">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M10.29 3.86L1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z"></path>
|
||||
<line x1="12" y1="9" x2="12" y2="13"></line>
|
||||
<line x1="12" y1="17" x2="12.01" y2="17"></line>
|
||||
</svg>
|
||||
Incidents
|
||||
</div>
|
||||
<div class="stat-value">
|
||||
${this.totalIncidents}
|
||||
</div>
|
||||
<div class="stat-change ${this.affectedServices === 0 ? 'positive' : 'negative'}">
|
||||
${this.affectedServices === 0 ? 'All services ok.' : `${this.affectedServices} of ${this.totalServices} services affected`}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
private formatStatus(status: string): string {
|
||||
const statusMap: Record<string, string> = {
|
||||
operational: 'Operational',
|
||||
degraded: 'Degraded',
|
||||
partial_outage: 'Partial Outage',
|
||||
major_outage: 'Major Outage',
|
||||
maintenance: 'Maintenance',
|
||||
};
|
||||
return statusMap[status] || 'Unknown';
|
||||
}
|
||||
|
||||
private renderResponseChange(): TemplateResult {
|
||||
// This could be enhanced with actual trend data
|
||||
const trend = this.avgResponseTime < 200 ? 'positive' : this.avgResponseTime > 500 ? 'negative' : 'neutral';
|
||||
const icon = trend === 'positive' ? '↓' : trend === 'negative' ? '↑' : '→';
|
||||
|
||||
return html`
|
||||
<div class="stat-change ${trend}">
|
||||
<span>${icon}</span>
|
||||
<span>${trend === 'positive' ? 'Fast' : trend === 'negative' ? 'Slow' : 'Normal'}</span>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
private getUptimeCardStatus(): string {
|
||||
if (this.uptime >= 99.9) return 'operational';
|
||||
if (this.uptime >= 99.0) return 'degraded';
|
||||
return 'partial_outage';
|
||||
}
|
||||
|
||||
private getResponseCardStatus(): string {
|
||||
if (this.avgResponseTime < 200) return 'operational';
|
||||
if (this.avgResponseTime < 500) return 'degraded';
|
||||
return 'partial_outage';
|
||||
}
|
||||
|
||||
private getIncidentCardStatus(): string {
|
||||
if (this.affectedServices === 0) return 'operational';
|
||||
if (this.currentStatus === 'major_outage') return 'major_outage';
|
||||
if (this.currentStatus === 'partial_outage') return 'partial_outage';
|
||||
return 'degraded';
|
||||
}
|
||||
}
|
||||
393
ts_web/elements/upl-statuspage-statusbar.demo.ts
Normal file
393
ts_web/elements/upl-statuspage-statusbar.demo.ts
Normal 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>
|
||||
`;
|
||||
@@ -1,5 +1,8 @@
|
||||
import { DeesElement, property, html, customElement, TemplateResult, cssManager, css } from '@designestate/dees-element';
|
||||
import * as domtools from '@designestate/dees-domtools';
|
||||
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 * as sharedStyles 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 })
|
||||
accessor overallStatus: IOverallStatus = {
|
||||
status: 'operational',
|
||||
message: 'All Systems Operational',
|
||||
lastUpdated: Date.now(),
|
||||
affectedServices: 0,
|
||||
totalServices: 0
|
||||
};
|
||||
|
||||
@property({ type: Boolean })
|
||||
accessor loading: boolean = false;
|
||||
|
||||
@property({ type: Boolean })
|
||||
accessor expandable: boolean = true;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
@@ -21,30 +37,326 @@ export class UplStatuspageStatusbar extends DeesElement {
|
||||
cssManager.defaultStyles,
|
||||
css`
|
||||
:host {
|
||||
padding: 20px 0px 15px 0px;
|
||||
display: block;
|
||||
background: ${cssManager.bdTheme('#eeeeeb', '#222222')};;
|
||||
font-family: Inter;
|
||||
color: #fff;
|
||||
padding: 0;
|
||||
display: block;
|
||||
background: transparent;
|
||||
font-family: ${unsafeCSS(sharedStyles.fonts.base)};
|
||||
}
|
||||
|
||||
.statusbar-container {
|
||||
margin: auto;
|
||||
max-width: 1200px;
|
||||
padding: ${unsafeCSS(sharedStyles.spacing.lg)};
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.statusbar-inner {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
min-height: 72px;
|
||||
padding: ${unsafeCSS(sharedStyles.spacing.lg)} ${unsafeCSS(sharedStyles.spacing.xl)};
|
||||
border-radius: ${unsafeCSS(sharedStyles.borderRadius.xl)};
|
||||
cursor: default;
|
||||
transition: all ${unsafeCSS(sharedStyles.durations.slow)} ${unsafeCSS(sharedStyles.easings.default)};
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
font-weight: 500;
|
||||
font-size: 15px;
|
||||
letter-spacing: -0.01em;
|
||||
border: 1px solid ${sharedStyles.colors.border.default};
|
||||
box-shadow: ${unsafeCSS(sharedStyles.shadows.sm)};
|
||||
}
|
||||
|
||||
/* Gradient background overlay */
|
||||
.statusbar-inner::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
opacity: 1;
|
||||
transition: opacity ${unsafeCSS(sharedStyles.durations.slow)} ${unsafeCSS(sharedStyles.easings.default)};
|
||||
z-index: 0;
|
||||
}
|
||||
|
||||
/* Left accent border */
|
||||
.statusbar-inner::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
width: 4px;
|
||||
transition: background ${unsafeCSS(sharedStyles.durations.normal)} ${unsafeCSS(sharedStyles.easings.default)};
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
/* Operational - green gradient */
|
||||
.statusbar-inner.operational {
|
||||
background: ${sharedStyles.colors.background.card};
|
||||
}
|
||||
.statusbar-inner.operational::before {
|
||||
background: ${sharedStyles.statusGradients.operational};
|
||||
}
|
||||
.statusbar-inner.operational::after {
|
||||
background: ${sharedStyles.colors.status.operational};
|
||||
}
|
||||
|
||||
/* Degraded - yellow/amber gradient */
|
||||
.statusbar-inner.degraded {
|
||||
background: ${sharedStyles.colors.background.card};
|
||||
}
|
||||
.statusbar-inner.degraded::before {
|
||||
background: ${sharedStyles.statusGradients.degraded};
|
||||
}
|
||||
.statusbar-inner.degraded::after {
|
||||
background: ${sharedStyles.colors.status.degraded};
|
||||
}
|
||||
|
||||
/* Partial outage - orange/red gradient */
|
||||
.statusbar-inner.partial_outage {
|
||||
background: ${sharedStyles.colors.background.card};
|
||||
}
|
||||
.statusbar-inner.partial_outage::before {
|
||||
background: ${sharedStyles.statusGradients.partial};
|
||||
}
|
||||
.statusbar-inner.partial_outage::after {
|
||||
background: ${sharedStyles.colors.status.partial};
|
||||
}
|
||||
|
||||
/* Major outage - red gradient */
|
||||
.statusbar-inner.major_outage {
|
||||
background: ${sharedStyles.colors.background.card};
|
||||
}
|
||||
.statusbar-inner.major_outage::before {
|
||||
background: ${sharedStyles.statusGradients.major};
|
||||
}
|
||||
.statusbar-inner.major_outage::after {
|
||||
background: ${sharedStyles.colors.status.major};
|
||||
}
|
||||
|
||||
/* Maintenance - blue gradient */
|
||||
.statusbar-inner.maintenance {
|
||||
background: ${sharedStyles.colors.background.card};
|
||||
}
|
||||
.statusbar-inner.maintenance::before {
|
||||
background: ${sharedStyles.statusGradients.maintenance};
|
||||
}
|
||||
.statusbar-inner.maintenance::after {
|
||||
background: ${sharedStyles.colors.status.maintenance};
|
||||
}
|
||||
|
||||
.statusbar-inner:hover {
|
||||
border-color: ${sharedStyles.colors.border.muted};
|
||||
box-shadow: ${unsafeCSS(sharedStyles.shadows.md)};
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.statusbar-inner:hover::before {
|
||||
opacity: 1.2;
|
||||
}
|
||||
|
||||
.status-indicator {
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
border-radius: 50%;
|
||||
flex-shrink: 0;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.status-indicator::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
inset: -3px;
|
||||
border-radius: 50%;
|
||||
opacity: 0.2;
|
||||
}
|
||||
|
||||
.statusbar-inner.operational .status-indicator {
|
||||
background: ${sharedStyles.colors.status.operational};
|
||||
box-shadow: 0 0 0 3px ${cssManager.bdTheme('rgba(22, 163, 74, 0.15)', 'rgba(34, 197, 94, 0.2)')};
|
||||
}
|
||||
|
||||
.statusbar-inner.operational .status-indicator::after {
|
||||
background: ${sharedStyles.colors.status.operational};
|
||||
animation: pulse-ring 2s ease-out infinite;
|
||||
}
|
||||
|
||||
.statusbar-inner.degraded .status-indicator {
|
||||
background: ${sharedStyles.colors.status.degraded};
|
||||
box-shadow: 0 0 0 3px ${cssManager.bdTheme('rgba(217, 119, 6, 0.15)', 'rgba(251, 191, 36, 0.2)')};
|
||||
}
|
||||
|
||||
.statusbar-inner.partial_outage .status-indicator {
|
||||
background: ${sharedStyles.colors.status.partial};
|
||||
box-shadow: 0 0 0 3px ${cssManager.bdTheme('rgba(220, 38, 38, 0.15)', 'rgba(248, 113, 113, 0.2)')};
|
||||
}
|
||||
|
||||
.statusbar-inner.major_outage .status-indicator {
|
||||
background: ${sharedStyles.colors.status.major};
|
||||
box-shadow: 0 0 0 3px ${cssManager.bdTheme('rgba(185, 28, 28, 0.15)', 'rgba(239, 68, 68, 0.2)')};
|
||||
animation: pulse-indicator 1s ease-in-out infinite;
|
||||
}
|
||||
|
||||
.statusbar-inner.maintenance .status-indicator {
|
||||
background: ${sharedStyles.colors.status.maintenance};
|
||||
box-shadow: 0 0 0 3px ${cssManager.bdTheme('rgba(37, 99, 235, 0.15)', 'rgba(96, 165, 250, 0.2)')};
|
||||
}
|
||||
|
||||
@keyframes pulse-ring {
|
||||
0% { transform: scale(1); opacity: 0.2; }
|
||||
50% { transform: scale(1.5); opacity: 0; }
|
||||
100% { transform: scale(1); opacity: 0; }
|
||||
}
|
||||
|
||||
@keyframes pulse-indicator {
|
||||
0%, 100% { opacity: 1; }
|
||||
50% { opacity: 0.6; }
|
||||
}
|
||||
|
||||
.status-content {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: ${unsafeCSS(sharedStyles.spacing.md)};
|
||||
flex: 1;
|
||||
padding-left: ${unsafeCSS(sharedStyles.spacing.sm)};
|
||||
}
|
||||
|
||||
.status-main {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: ${unsafeCSS(sharedStyles.spacing.sm)};
|
||||
color: ${sharedStyles.colors.text.primary};
|
||||
}
|
||||
|
||||
.status-message {
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.status-details {
|
||||
font-size: 13px;
|
||||
font-weight: 400;
|
||||
color: ${sharedStyles.colors.text.secondary};
|
||||
}
|
||||
|
||||
.loading-skeleton {
|
||||
background: ${sharedStyles.colors.background.card};
|
||||
border: 1px solid ${sharedStyles.colors.border.default};
|
||||
border-radius: ${unsafeCSS(sharedStyles.borderRadius.lg)};
|
||||
height: 64px;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.loading-skeleton::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: ${cssManager.bdTheme(
|
||||
'linear-gradient(90deg, transparent 0%, rgba(0,0,0,0.04) 50%, transparent 100%)',
|
||||
'linear-gradient(90deg, transparent 0%, rgba(255,255,255,0.04) 50%, transparent 100%)'
|
||||
)};
|
||||
animation: loading 1.5s ${unsafeCSS(sharedStyles.easings.default)} infinite;
|
||||
}
|
||||
|
||||
@keyframes loading {
|
||||
0% { transform: translateX(-100%); }
|
||||
100% { transform: translateX(200%); }
|
||||
}
|
||||
|
||||
.last-updated {
|
||||
font-size: 12px;
|
||||
color: ${sharedStyles.colors.text.muted};
|
||||
white-space: nowrap;
|
||||
padding: 4px 10px;
|
||||
background: ${sharedStyles.colors.background.muted};
|
||||
border-radius: ${unsafeCSS(sharedStyles.borderRadius.full)};
|
||||
transition: all ${unsafeCSS(sharedStyles.durations.fast)} ${unsafeCSS(sharedStyles.easings.default)};
|
||||
}
|
||||
|
||||
.statusbar-inner:hover .last-updated {
|
||||
background: ${cssManager.bdTheme('#e4e4e7', '#3f3f46')};
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.statusbar-container {
|
||||
padding: ${unsafeCSS(sharedStyles.spacing.md)};
|
||||
}
|
||||
|
||||
.mainbox {
|
||||
margin: auto;
|
||||
max-width: 900px;
|
||||
text-align: center;
|
||||
background: #19572E;
|
||||
line-height: 50px;
|
||||
border-radius: 3px;
|
||||
.statusbar-inner {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: ${unsafeCSS(sharedStyles.spacing.sm)};
|
||||
min-height: auto;
|
||||
padding: ${unsafeCSS(sharedStyles.spacing.md)};
|
||||
}
|
||||
|
||||
.status-main {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.last-updated {
|
||||
align-self: flex-start;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 640px) {
|
||||
.statusbar-inner {
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.status-indicator {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
}
|
||||
|
||||
.last-updated {
|
||||
font-size: 11px;
|
||||
}
|
||||
}
|
||||
`,
|
||||
]
|
||||
|
||||
public render(): TemplateResult {
|
||||
const formatLastUpdated = () => {
|
||||
const date = new Date(this.overallStatus.lastUpdated);
|
||||
const now = new Date();
|
||||
const diff = now.getTime() - date.getTime();
|
||||
const minutes = Math.floor(diff / 60000);
|
||||
|
||||
if (minutes < 1) return 'Just now';
|
||||
if (minutes < 60) return `${minutes}m ago`;
|
||||
const hours = Math.floor(minutes / 60);
|
||||
if (hours < 24) return `${hours}h ago`;
|
||||
return date.toLocaleDateString();
|
||||
};
|
||||
|
||||
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}">
|
||||
<div class="status-content">
|
||||
<div class="status-indicator"></div>
|
||||
<div class="status-main">
|
||||
<span>${this.overallStatus.message}</span>
|
||||
${this.overallStatus.affectedServices > 0 ? html`
|
||||
<span class="status-details">
|
||||
· ${this.overallStatus.affectedServices} of ${this.overallStatus.totalServices} services affected
|
||||
</span>
|
||||
` : ''}
|
||||
</div>
|
||||
</div>
|
||||
<div class="last-updated">
|
||||
${formatLastUpdated()}
|
||||
</div>
|
||||
</div>
|
||||
`}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
754
ts_web/elements/upl-statuspage-statusdetails.demo.ts
Normal file
754
ts_web/elements/upl-statuspage-statusdetails.demo.ts
Normal 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>
|
||||
`;
|
||||
@@ -4,12 +4,16 @@ import {
|
||||
property,
|
||||
html,
|
||||
customElement,
|
||||
TemplateResult,
|
||||
type TemplateResult,
|
||||
css,
|
||||
cssManager,
|
||||
} from '@designestate/dees-element';
|
||||
unsafeCSS,
|
||||
} from '@design.estate/dees-element';
|
||||
import type { IStatusHistoryPoint } from '../interfaces/index.js';
|
||||
import * as sharedStyles 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,25 @@ 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 })
|
||||
accessor historyData: IStatusHistoryPoint[] = [];
|
||||
|
||||
@property({ type: Array })
|
||||
accessor dataPoints: IStatusHistoryPoint[] = [];
|
||||
|
||||
@property({ type: String })
|
||||
accessor serviceId: string = '';
|
||||
|
||||
@property({ type: String })
|
||||
accessor serviceName: string = 'Service';
|
||||
|
||||
@property({ type: Boolean })
|
||||
accessor loading: boolean = false;
|
||||
|
||||
@property({ type: Number })
|
||||
accessor hoursToShow: number = 48;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
@@ -27,69 +49,331 @@ export class UplStatuspageStatusdetails extends DeesElement {
|
||||
|
||||
public static styles = [
|
||||
plugins.domtools.elementBasic.staticStyles,
|
||||
sharedStyles.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(sharedStyles.fonts.base)};
|
||||
color: ${sharedStyles.colors.text.primary};
|
||||
}
|
||||
|
||||
.container {
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
padding: 0 ${unsafeCSS(sharedStyles.spacing.lg)} ${unsafeCSS(sharedStyles.spacing.lg)} ${unsafeCSS(sharedStyles.spacing.lg)};
|
||||
}
|
||||
|
||||
.graph-wrapper {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.graph-container {
|
||||
position: relative;
|
||||
animation: fadeIn 0.3s ${unsafeCSS(sharedStyles.easings.default)};
|
||||
}
|
||||
|
||||
@keyframes fadeIn {
|
||||
from { opacity: 0; }
|
||||
to { opacity: 1; }
|
||||
}
|
||||
|
||||
.mainbox {
|
||||
margin: auto;
|
||||
max-width: 900px;
|
||||
text-align: right;
|
||||
background: ${cssManager.bdTheme('#ffffff', '#333333')};;
|
||||
line-height: 50px;
|
||||
border-radius: 3px;
|
||||
background: transparent;
|
||||
border: none;
|
||||
border-radius: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.mainbox .barContainer {
|
||||
position: relative;
|
||||
display: flex;
|
||||
padding: 6px;
|
||||
gap: 2px;
|
||||
padding: ${unsafeCSS(sharedStyles.spacing.sm)};
|
||||
background: ${cssManager.bdTheme('#fafafa', '#0a0a0a')};
|
||||
border: 1px solid ${cssManager.bdTheme('#e5e7eb', '#27272a')};
|
||||
border-radius: ${unsafeCSS(sharedStyles.borderRadius.base)};
|
||||
overflow: hidden;
|
||||
height: 40px;
|
||||
}
|
||||
|
||||
.mainbox .barContainer .bar {
|
||||
margin: 4px;
|
||||
width: 11px;
|
||||
flex: 1;
|
||||
height: 100%;
|
||||
cursor: pointer;
|
||||
transition: all ${unsafeCSS(sharedStyles.durations.fast)} ${unsafeCSS(sharedStyles.easings.default)};
|
||||
position: relative;
|
||||
border-radius: 3px;
|
||||
height: 40px;
|
||||
background: #2deb51;
|
||||
animation: barGrow 0.4s ${unsafeCSS(sharedStyles.easings.default)} both;
|
||||
animation-delay: calc(var(--bar-index, 0) * 8ms);
|
||||
transform-origin: bottom;
|
||||
}
|
||||
.timeIndicator {
|
||||
|
||||
@keyframes barGrow {
|
||||
from {
|
||||
transform: scaleY(0);
|
||||
opacity: 0;
|
||||
}
|
||||
to {
|
||||
transform: scaleY(1);
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.mainbox .barContainer .bar:hover {
|
||||
transform: scaleY(1.15);
|
||||
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.15);
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.mainbox .barContainer .bar.operational {
|
||||
background: ${cssManager.bdTheme('#22c55e', '#22c55e')};
|
||||
}
|
||||
|
||||
.mainbox .barContainer .bar.degraded {
|
||||
background: ${cssManager.bdTheme('#fbbf24', '#fbbf24')};
|
||||
}
|
||||
|
||||
.mainbox .barContainer .bar.partial_outage {
|
||||
background: ${cssManager.bdTheme('#f87171', '#f87171')};
|
||||
}
|
||||
|
||||
.mainbox .barContainer .bar.major_outage {
|
||||
background: ${cssManager.bdTheme('#ef4444', '#ef4444')};
|
||||
}
|
||||
|
||||
.mainbox .barContainer .bar.maintenance {
|
||||
background: ${cssManager.bdTheme('#60a5fa', '#60a5fa')};
|
||||
}
|
||||
|
||||
.mainbox .barContainer .bar.no-data {
|
||||
background: ${cssManager.bdTheme('#e5e7eb', '#27272a')};
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
|
||||
.time-labels {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
padding: 0;
|
||||
margin-top: ${unsafeCSS(sharedStyles.spacing.xs)};
|
||||
font-size: 10px;
|
||||
color: ${cssManager.bdTheme('#9ca3af', '#71717a')};
|
||||
font-family: ${unsafeCSS(sharedStyles.fonts.base)};
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
.tooltip {
|
||||
position: absolute;
|
||||
width: 11px;
|
||||
height: 11px;
|
||||
background: #FF9800;
|
||||
top: 56px;
|
||||
left: 400px;
|
||||
transform: rotate(45deg);
|
||||
background: ${cssManager.bdTheme('#0a0a0a', '#fafafa')};
|
||||
color: ${cssManager.bdTheme('#fafafa', '#0a0a0a')};
|
||||
padding: 8px 12px;
|
||||
border-radius: ${unsafeCSS(sharedStyles.borderRadius.base)};
|
||||
font-size: 11px;
|
||||
pointer-events: none;
|
||||
opacity: 0;
|
||||
transition: opacity ${unsafeCSS(sharedStyles.durations.fast)} ${unsafeCSS(sharedStyles.easings.default)},
|
||||
transform ${unsafeCSS(sharedStyles.durations.fast)} ${unsafeCSS(sharedStyles.easings.default)};
|
||||
z-index: 50;
|
||||
white-space: nowrap;
|
||||
box-shadow: ${unsafeCSS(sharedStyles.shadows.lg)};
|
||||
line-height: 1.5;
|
||||
transform: translateY(4px);
|
||||
}
|
||||
|
||||
.tooltip.visible {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
.tooltip-time {
|
||||
font-weight: 600;
|
||||
display: block;
|
||||
margin-bottom: 2px;
|
||||
}
|
||||
|
||||
.tooltip-stat {
|
||||
font-size: 10px;
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
.loading-skeleton {
|
||||
display: flex;
|
||||
gap: 1px;
|
||||
height: 24px;
|
||||
}
|
||||
|
||||
.loading-skeleton .skeleton-bar {
|
||||
flex: 1;
|
||||
height: 100%;
|
||||
background: ${cssManager.bdTheme('#f3f4f6', '#27272a')};
|
||||
animation: pulse 2s infinite;
|
||||
}
|
||||
|
||||
@keyframes pulse {
|
||||
0%, 100% { opacity: 1; }
|
||||
50% { opacity: 0.5; }
|
||||
}
|
||||
|
||||
@keyframes loading {
|
||||
0% { background-position: 200% 0; }
|
||||
100% { background-position: -200% 0; }
|
||||
}
|
||||
|
||||
|
||||
@media (max-width: 640px) {
|
||||
.container {
|
||||
padding: 0 ${unsafeCSS(sharedStyles.spacing.md)} ${unsafeCSS(sharedStyles.spacing.md)} ${unsafeCSS(sharedStyles.spacing.md)};
|
||||
}
|
||||
|
||||
.graph-container {
|
||||
padding: ${unsafeCSS(sharedStyles.spacing.sm)};
|
||||
}
|
||||
|
||||
.mainbox .barContainer {
|
||||
height: 32px;
|
||||
padding: ${unsafeCSS(sharedStyles.spacing.xs)};
|
||||
}
|
||||
|
||||
.time-labels {
|
||||
font-size: 9px;
|
||||
}
|
||||
|
||||
.stats-row {
|
||||
font-size: 11px;
|
||||
flex-direction: column;
|
||||
gap: ${unsafeCSS(sharedStyles.spacing.sm)};
|
||||
align-items: flex-start;
|
||||
}
|
||||
}
|
||||
`,
|
||||
];
|
||||
|
||||
public render(): TemplateResult {
|
||||
return html`
|
||||
<style></style>
|
||||
<uplinternal-miniheading>Yesterday & Today</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 class="container">
|
||||
<uplinternal-miniheading>${this.serviceName} - Last ${this.hoursToShow} Hours</uplinternal-miniheading>
|
||||
<div class="mainbox">
|
||||
${this.loading ? html`
|
||||
<div class="graph-container">
|
||||
<div class="barContainer" style="background: ${cssManager.bdTheme('#fafafa', '#0a0a0a')}; border: 1px solid ${cssManager.bdTheme('#f3f4f6', '#1f1f1f')}; border-radius: ${sharedStyles.borderRadius.base}; padding: ${sharedStyles.spacing.sm}; height: 40px;">
|
||||
<div class="loading-skeleton">
|
||||
${Array(this.hoursToShow).fill(0).map(() => html`<div class="skeleton-bar"></div>`)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
` : html`
|
||||
<div class="graph-container">
|
||||
<div class="barContainer" @mouseleave=${this.hideTooltip}>
|
||||
${this.renderBars()}
|
||||
</div>
|
||||
</div>
|
||||
<div class="time-labels">
|
||||
<span>${this.getTimeLabel(this.hoursToShow - 1)}</span>
|
||||
<span>${this.getTimeLabel(Math.floor(this.hoursToShow * 3 / 4))}</span>
|
||||
<span>${this.getTimeLabel(Math.floor(this.hoursToShow / 2))}</span>
|
||||
<span>${this.getTimeLabel(Math.floor(this.hoursToShow / 4))}</span>
|
||||
<span>now</span>
|
||||
</div>
|
||||
`}
|
||||
</div>
|
||||
<div class="tooltip" id="tooltip"></div>
|
||||
</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}"
|
||||
style="--bar-index: ${i}"
|
||||
@mouseenter=${(e: MouseEvent) => this.showTooltip(e, timestamp, status, responseTime)}
|
||||
@click=${() => this.handleBarClick(timestamp, status, responseTime)}
|
||||
></div>
|
||||
`);
|
||||
}
|
||||
|
||||
return bars;
|
||||
}
|
||||
|
||||
private getData(): IStatusHistoryPoint[] {
|
||||
return this.dataPoints?.length > 0 ? this.dataPoints : this.historyData || [];
|
||||
}
|
||||
|
||||
private findDataPointForTime(timestamp: number): IStatusHistoryPoint | undefined {
|
||||
const data = this.getData();
|
||||
if (!data || data.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 data.find(point => {
|
||||
const pointDate = new Date(point.timestamp);
|
||||
return pointDate.toDateString() === targetDate &&
|
||||
pointDate.getHours() === targetHour;
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
private getTimeLabel(hoursAgo: number): string {
|
||||
const date = new Date(Date.now() - (hoursAgo * 60 * 60 * 1000));
|
||||
if (hoursAgo >= 24) {
|
||||
return `${date.getMonth() + 1}/${date.getDate()}`;
|
||||
}
|
||||
return `${date.getHours()}h`;
|
||||
}
|
||||
|
||||
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 class="tooltip-time">${timeStr}</div>
|
||||
<div class="tooltip-stat">Status: ${statusStr}</div>
|
||||
${responseTime > 0 ? `<div class="tooltip-stat">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 - 10}px`;
|
||||
tooltip.style.transform = 'translate(-50%, -100%)';
|
||||
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
|
||||
}));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
876
ts_web/elements/upl-statuspage-statusmonth.demo.ts
Normal file
876
ts_web/elements/upl-statuspage-statusmonth.demo.ts
Normal 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>
|
||||
`;
|
||||
@@ -3,13 +3,17 @@ import {
|
||||
property,
|
||||
html,
|
||||
customElement,
|
||||
TemplateResult,
|
||||
type TemplateResult,
|
||||
css,
|
||||
cssManager
|
||||
} from '@designestate/dees-element';
|
||||
import * as domtools from '@designestate/dees-domtools';
|
||||
cssManager,
|
||||
unsafeCSS
|
||||
} from '@design.estate/dees-element';
|
||||
import * as domtools from '@design.estate/dees-domtools';
|
||||
import type { IMonthlyUptime } from '../interfaces/index.js';
|
||||
import * as sharedStyles 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 })
|
||||
accessor monthlyData: IMonthlyUptime[] = [];
|
||||
|
||||
@property({ type: String })
|
||||
accessor serviceId: string = '';
|
||||
|
||||
@property({ type: String })
|
||||
accessor serviceName: string = 'Service';
|
||||
|
||||
@property({ type: Boolean })
|
||||
accessor loading: boolean = false;
|
||||
|
||||
@property({ type: Boolean })
|
||||
accessor showTooltip: boolean = true;
|
||||
|
||||
@property({ type: Number })
|
||||
accessor monthsToShow: number = 5;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
@@ -27,104 +49,587 @@ export class UplStatuspageStatusmonth extends DeesElement {
|
||||
|
||||
public static styles = [
|
||||
domtools.elementBasic.staticStyles,
|
||||
sharedStyles.commonStyles,
|
||||
css`
|
||||
:host {
|
||||
position: relative;
|
||||
padding: 0px 0px 15px 0px;
|
||||
display: block;
|
||||
background: ${cssManager.bdTheme('#eeeeeb', '#222222')};;
|
||||
font-family: Inter;
|
||||
color: #fff;
|
||||
position: relative;
|
||||
display: block;
|
||||
background: transparent;
|
||||
font-family: ${unsafeCSS(sharedStyles.fonts.base)};
|
||||
color: ${sharedStyles.colors.text.primary};
|
||||
}
|
||||
|
||||
.container {
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
padding: 0 ${unsafeCSS(sharedStyles.spacing.lg)} ${unsafeCSS(sharedStyles.spacing.lg)} ${unsafeCSS(sharedStyles.spacing.lg)};
|
||||
}
|
||||
|
||||
.mainbox {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(260px, 1fr));
|
||||
gap: ${unsafeCSS(sharedStyles.spacing.lg)};
|
||||
}
|
||||
|
||||
/* Month card with entrance animation */
|
||||
.statusMonth {
|
||||
background: ${sharedStyles.colors.background.card};
|
||||
padding: ${unsafeCSS(sharedStyles.spacing.lg)};
|
||||
border-radius: ${unsafeCSS(sharedStyles.borderRadius.lg)};
|
||||
border: 1px solid ${sharedStyles.colors.border.default};
|
||||
position: relative;
|
||||
transition: all ${unsafeCSS(sharedStyles.durations.normal)} ${unsafeCSS(sharedStyles.easings.default)};
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-height: 280px;
|
||||
box-shadow: ${unsafeCSS(sharedStyles.shadows.sm)};
|
||||
animation: fadeInUp 0.5s ${unsafeCSS(sharedStyles.easings.default)} both;
|
||||
}
|
||||
|
||||
.statusMonth:nth-child(1) { animation-delay: 0ms; }
|
||||
.statusMonth:nth-child(2) { animation-delay: 100ms; }
|
||||
.statusMonth:nth-child(3) { animation-delay: 200ms; }
|
||||
.statusMonth:nth-child(4) { animation-delay: 300ms; }
|
||||
.statusMonth:nth-child(5) { animation-delay: 400ms; }
|
||||
|
||||
@keyframes fadeInUp {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(16px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
.statusMonth:hover {
|
||||
border-color: ${sharedStyles.colors.border.muted};
|
||||
box-shadow: ${unsafeCSS(sharedStyles.shadows.md)};
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
.month-header {
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
margin-bottom: ${unsafeCSS(sharedStyles.spacing.md)};
|
||||
color: ${sharedStyles.colors.text.primary};
|
||||
letter-spacing: 0.04em;
|
||||
text-transform: uppercase;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.month-header .current-badge {
|
||||
font-size: 9px;
|
||||
padding: 2px 6px;
|
||||
background: ${sharedStyles.colors.status.operational};
|
||||
color: white;
|
||||
border-radius: ${unsafeCSS(sharedStyles.borderRadius.full)};
|
||||
font-weight: 500;
|
||||
letter-spacing: 0.02em;
|
||||
}
|
||||
|
||||
.days-container {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
margin-bottom: ${unsafeCSS(sharedStyles.spacing.lg)};
|
||||
}
|
||||
|
||||
.days-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(7, 1fr);
|
||||
gap: 4px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.weekday-label {
|
||||
font-size: 9px;
|
||||
text-align: center;
|
||||
color: ${sharedStyles.colors.text.muted};
|
||||
font-weight: 600;
|
||||
height: 20px;
|
||||
line-height: 20px;
|
||||
margin-bottom: ${unsafeCSS(sharedStyles.spacing.xs)};
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.02em;
|
||||
}
|
||||
|
||||
/* Calendar day cell */
|
||||
.statusDay {
|
||||
aspect-ratio: 1;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
transition: all ${unsafeCSS(sharedStyles.durations.fast)} ${unsafeCSS(sharedStyles.easings.default)};
|
||||
position: relative;
|
||||
animation: dayFadeIn 0.3s ${unsafeCSS(sharedStyles.easings.default)} both;
|
||||
animation-delay: calc(var(--day-index, 0) * 15ms);
|
||||
}
|
||||
|
||||
@keyframes dayFadeIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: scale(0.8);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: scale(1);
|
||||
}
|
||||
}
|
||||
|
||||
.statusDay:hover:not(.empty) {
|
||||
transform: scale(1.2);
|
||||
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.15);
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
/* Current day highlight */
|
||||
.statusDay.today {
|
||||
box-shadow: 0 0 0 2px ${sharedStyles.colors.text.primary};
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.statusDay.today:hover {
|
||||
box-shadow: 0 0 0 2px ${sharedStyles.colors.text.primary}, 0 4px 8px rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
|
||||
/* Status colors with intensity variations based on uptime */
|
||||
.statusDay.operational {
|
||||
background: ${sharedStyles.colors.status.operational};
|
||||
}
|
||||
|
||||
.statusDay.operational.uptime-high {
|
||||
background: ${cssManager.bdTheme('#22c55e', '#22c55e')};
|
||||
}
|
||||
|
||||
.statusDay.operational.uptime-mid {
|
||||
background: ${cssManager.bdTheme('#4ade80', '#4ade80')};
|
||||
}
|
||||
|
||||
.statusDay.degraded {
|
||||
background: ${sharedStyles.colors.status.degraded};
|
||||
}
|
||||
|
||||
.statusDay.partial_outage {
|
||||
background: ${sharedStyles.colors.status.partial};
|
||||
}
|
||||
|
||||
.statusDay.major_outage {
|
||||
background: ${sharedStyles.colors.status.major};
|
||||
animation: dayFadeIn 0.3s ${unsafeCSS(sharedStyles.easings.default)} both,
|
||||
majorOutagePulse 2s ease-in-out infinite;
|
||||
}
|
||||
|
||||
@keyframes majorOutagePulse {
|
||||
0%, 100% { opacity: 1; }
|
||||
50% { opacity: 0.8; }
|
||||
}
|
||||
|
||||
.statusDay.maintenance {
|
||||
background: ${sharedStyles.colors.status.maintenance};
|
||||
}
|
||||
|
||||
.statusDay.no-data {
|
||||
background: ${sharedStyles.colors.background.muted};
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.statusDay.empty {
|
||||
background: transparent;
|
||||
cursor: default;
|
||||
pointer-events: none;
|
||||
animation: none;
|
||||
}
|
||||
|
||||
/* Incident count indicator */
|
||||
.incident-count {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
font-size: 8px;
|
||||
font-weight: 700;
|
||||
color: white;
|
||||
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.3);
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
/* Overall uptime footer */
|
||||
.overall-uptime {
|
||||
font-size: 12px;
|
||||
margin-top: auto;
|
||||
padding-top: ${unsafeCSS(sharedStyles.spacing.md)};
|
||||
color: ${sharedStyles.colors.text.secondary};
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
border-top: 1px solid ${sharedStyles.colors.border.default};
|
||||
}
|
||||
|
||||
.uptime-stat {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.uptime-value {
|
||||
font-weight: 600;
|
||||
color: ${sharedStyles.colors.text.primary};
|
||||
font-variant-numeric: tabular-nums;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.uptime-value.good {
|
||||
color: ${sharedStyles.colors.status.operational};
|
||||
}
|
||||
|
||||
.uptime-value.warning {
|
||||
color: ${sharedStyles.colors.status.degraded};
|
||||
}
|
||||
|
||||
.uptime-value.bad {
|
||||
color: ${sharedStyles.colors.status.partial};
|
||||
}
|
||||
|
||||
/* Uptime bar visualization */
|
||||
.uptime-bar {
|
||||
height: 4px;
|
||||
background: ${sharedStyles.colors.background.muted};
|
||||
border-radius: 2px;
|
||||
overflow: hidden;
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
.uptime-bar-fill {
|
||||
height: 100%;
|
||||
border-radius: 2px;
|
||||
transition: width ${unsafeCSS(sharedStyles.durations.slow)} ${unsafeCSS(sharedStyles.easings.default)};
|
||||
}
|
||||
|
||||
.uptime-bar-fill.good {
|
||||
background: ${sharedStyles.colors.status.operational};
|
||||
}
|
||||
|
||||
.uptime-bar-fill.warning {
|
||||
background: ${sharedStyles.colors.status.degraded};
|
||||
}
|
||||
|
||||
.uptime-bar-fill.bad {
|
||||
background: ${sharedStyles.colors.status.partial};
|
||||
}
|
||||
|
||||
/* Loading skeleton */
|
||||
.loading-skeleton {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: ${unsafeCSS(sharedStyles.spacing.sm)};
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.skeleton-header {
|
||||
height: 20px;
|
||||
width: 80px;
|
||||
background: ${sharedStyles.colors.background.muted};
|
||||
border-radius: 4px;
|
||||
animation: shimmer 1.5s infinite;
|
||||
}
|
||||
|
||||
.skeleton-grid {
|
||||
flex: 1;
|
||||
display: grid;
|
||||
grid-template-columns: repeat(7, 1fr);
|
||||
gap: 3px;
|
||||
}
|
||||
|
||||
.skeleton-day {
|
||||
background: ${sharedStyles.colors.background.muted};
|
||||
border-radius: 3px;
|
||||
animation: shimmer 1.5s infinite;
|
||||
animation-delay: calc(var(--index) * 30ms);
|
||||
}
|
||||
|
||||
@keyframes shimmer {
|
||||
0% { opacity: 0.5; }
|
||||
50% { opacity: 1; }
|
||||
100% { opacity: 0.5; }
|
||||
}
|
||||
|
||||
/* Tooltip */
|
||||
.tooltip {
|
||||
position: absolute;
|
||||
background: ${cssManager.bdTheme('#0a0a0a', '#fafafa')};
|
||||
color: ${cssManager.bdTheme('#fafafa', '#0a0a0a')};
|
||||
padding: 10px 14px;
|
||||
border-radius: ${unsafeCSS(sharedStyles.borderRadius.base)};
|
||||
font-size: 12px;
|
||||
pointer-events: none;
|
||||
opacity: 0;
|
||||
transition: opacity ${unsafeCSS(sharedStyles.durations.fast)} ${unsafeCSS(sharedStyles.easings.default)},
|
||||
transform ${unsafeCSS(sharedStyles.durations.fast)} ${unsafeCSS(sharedStyles.easings.default)};
|
||||
z-index: 50;
|
||||
white-space: nowrap;
|
||||
box-shadow: ${unsafeCSS(sharedStyles.shadows.lg)};
|
||||
line-height: 1.5;
|
||||
transform: translateX(-50%) translateY(4px);
|
||||
}
|
||||
|
||||
.tooltip.visible {
|
||||
opacity: 1;
|
||||
transform: translateX(-50%) translateY(0);
|
||||
}
|
||||
|
||||
.tooltip-date {
|
||||
font-weight: 600;
|
||||
margin-bottom: 6px;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.tooltip-stat {
|
||||
font-size: 11px;
|
||||
opacity: 0.85;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.tooltip-stat + .tooltip-stat {
|
||||
margin-top: 2px;
|
||||
}
|
||||
|
||||
.tooltip-uptime-bar {
|
||||
height: 3px;
|
||||
width: 60px;
|
||||
background: rgba(128, 128, 128, 0.3);
|
||||
border-radius: 2px;
|
||||
overflow: hidden;
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
.tooltip-uptime-fill {
|
||||
height: 100%;
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
.no-data-message {
|
||||
grid-column: 1 / -1;
|
||||
text-align: center;
|
||||
padding: ${unsafeCSS(sharedStyles.spacing['2xl'])};
|
||||
color: ${sharedStyles.colors.text.secondary};
|
||||
animation: fadeInUp 0.4s ${unsafeCSS(sharedStyles.easings.default)} both;
|
||||
}
|
||||
|
||||
@media (max-width: 640px) {
|
||||
.container {
|
||||
padding: 0 ${unsafeCSS(sharedStyles.spacing.md)} ${unsafeCSS(sharedStyles.spacing.md)} ${unsafeCSS(sharedStyles.spacing.md)};
|
||||
}
|
||||
|
||||
.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: 1fr;
|
||||
gap: ${unsafeCSS(sharedStyles.spacing.md)};
|
||||
}
|
||||
|
||||
.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;
|
||||
padding: ${unsafeCSS(sharedStyles.spacing.md)};
|
||||
min-height: 260px;
|
||||
}
|
||||
|
||||
.statusMonth .statusDay {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
background: #2deb51;
|
||||
border-radius: 3px;
|
||||
.days-grid {
|
||||
gap: 3px;
|
||||
}
|
||||
|
||||
.statusDay:hover:not(.empty) {
|
||||
transform: scale(1.1);
|
||||
}
|
||||
|
||||
.loading-skeleton {
|
||||
height: 180px;
|
||||
padding: ${unsafeCSS(sharedStyles.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((_, index) => html`
|
||||
<div class="statusMonth">
|
||||
<div class="loading-skeleton">
|
||||
<div class="skeleton-header"></div>
|
||||
<div class="days-container">
|
||||
<div class="skeleton-grid">
|
||||
${Array(42).fill(0).map((_, i) => html`
|
||||
<div class="skeleton-day" style="--index: ${i}"></div>
|
||||
`)}
|
||||
</div>
|
||||
</div>
|
||||
<div style="height: 48px; border-top: 1px solid ${cssManager.bdTheme('#f3f4f6', '#1f1f1f')}; margin-top: auto; padding-top: 16px;"></div>
|
||||
</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();
|
||||
const now = new Date();
|
||||
const isCurrentMonth = monthDate.getMonth() === now.getMonth() && monthDate.getFullYear() === now.getFullYear();
|
||||
const uptimeClass = this.getUptimeClass(monthData.overallUptime);
|
||||
|
||||
return html`
|
||||
<div class="statusMonth" @mouseleave=${this.hideTooltip}>
|
||||
<div class="month-header">
|
||||
${monthName}
|
||||
${isCurrentMonth ? html`<span class="current-badge">Current</span>` : ''}
|
||||
</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="days-container">
|
||||
<div class="days-grid">
|
||||
${this.renderWeekdayLabels()}
|
||||
${this.renderEmptyDays(firstDayOfWeek)}
|
||||
${monthData.days.map((day, index) => this.renderDay(day, index))}
|
||||
${this.renderTrailingEmptyDays(firstDayOfWeek + monthData.days.length)}
|
||||
</div>
|
||||
</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">
|
||||
<div class="uptime-stat">
|
||||
<span>Uptime</span>
|
||||
<span class="uptime-value ${uptimeClass}">${monthData.overallUptime.toFixed(2)}%</span>
|
||||
</div>
|
||||
<div class="uptime-bar">
|
||||
<div class="uptime-bar-fill ${uptimeClass}" style="width: ${monthData.overallUptime}%"></div>
|
||||
</div>
|
||||
${monthData.totalIncidents > 0 ? html`
|
||||
<div class="uptime-stat">
|
||||
<span>Incidents</span>
|
||||
<span class="uptime-value">${monthData.totalIncidents}</span>
|
||||
</div>
|
||||
` : ''}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
private getUptimeClass(uptime: number): string {
|
||||
if (uptime >= 99.9) return 'good';
|
||||
if (uptime >= 99) return 'warning';
|
||||
return 'bad';
|
||||
}
|
||||
|
||||
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 renderTrailingEmptyDays(totalCells: number): TemplateResult[] {
|
||||
const remainder = totalCells % 7;
|
||||
const trailingCount = remainder === 0 ? 0 : 7 - remainder;
|
||||
return Array(trailingCount).fill(0).map(() => html`<div class="statusDay empty"></div>`);
|
||||
}
|
||||
|
||||
private renderDay(day: any, index: number = 0): TemplateResult {
|
||||
const status = day.status || 'no-data';
|
||||
const date = new Date(day.date);
|
||||
const now = new Date();
|
||||
const isToday = date.toDateString() === now.toDateString();
|
||||
const uptimeIntensity = this.getUptimeIntensity(day.uptime);
|
||||
|
||||
return html`
|
||||
<div
|
||||
class="statusDay ${status} ${isToday ? 'today' : ''} ${status === 'operational' ? uptimeIntensity : ''}"
|
||||
style="--day-index: ${index}"
|
||||
@mouseenter=${(e: MouseEvent) => this.showTooltip && this.showDayTooltip(e, day)}
|
||||
@click=${() => this.handleDayClick(day)}
|
||||
>
|
||||
${(status === 'major_outage' || status === 'partial_outage') && day.incidents > 0 ? html`
|
||||
<span class="incident-count">${day.incidents}</span>
|
||||
` : ''}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
private getUptimeIntensity(uptime: number): string {
|
||||
if (uptime >= 99.9) return 'uptime-high';
|
||||
if (uptime >= 99) return 'uptime-mid';
|
||||
return '';
|
||||
}
|
||||
|
||||
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: 'short',
|
||||
month: 'short',
|
||||
day: 'numeric'
|
||||
});
|
||||
|
||||
let statusText = day.status.replace(/_/g, ' ');
|
||||
statusText = statusText.charAt(0).toUpperCase() + statusText.slice(1);
|
||||
|
||||
const uptimeColor = day.uptime >= 99.9 ? '#22c55e' :
|
||||
day.uptime >= 99 ? '#fbbf24' : '#f87171';
|
||||
|
||||
tooltip.innerHTML = `
|
||||
<div class="tooltip-date">${dateStr}</div>
|
||||
<div class="tooltip-stat">
|
||||
<span style="display: inline-block; width: 6px; height: 6px; border-radius: 50%; background: ${uptimeColor};"></span>
|
||||
${statusText}
|
||||
</div>
|
||||
<div class="tooltip-stat">Uptime: <strong>${day.uptime.toFixed(2)}%</strong></div>
|
||||
${day.incidents > 0 ? `<div class="tooltip-stat">Incidents: <strong>${day.incidents}</strong></div>` : ''}
|
||||
${day.totalDowntime > 0 ? `<div class="tooltip-stat">Downtime: <strong>${day.totalDowntime}m</strong></div>` : ''}
|
||||
<div class="tooltip-uptime-bar">
|
||||
<div class="tooltip-uptime-fill" style="width: ${day.uptime}%; background: ${uptimeColor};"></div>
|
||||
</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 - 10}px`;
|
||||
tooltip.style.transform = 'translateX(-50%) translateY(-100%)';
|
||||
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
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
95
ts_web/interfaces/index.ts
Normal file
95
ts_web/interfaces/index.ts
Normal file
@@ -0,0 +1,95 @@
|
||||
export interface IServiceStatus {
|
||||
id: string;
|
||||
name: string;
|
||||
displayName: string;
|
||||
description?: string;
|
||||
currentStatus: 'operational' | 'degraded' | 'partial_outage' | 'major_outage' | 'maintenance';
|
||||
lastChecked: number; // timestamp
|
||||
uptime30d: number; // percentage
|
||||
uptime90d: number; // percentage
|
||||
responseTime: number; // milliseconds
|
||||
category?: string;
|
||||
dependencies?: string[];
|
||||
selected?: boolean;
|
||||
}
|
||||
|
||||
export interface IStatusHistoryPoint {
|
||||
timestamp: number;
|
||||
status: 'operational' | 'degraded' | 'partial_outage' | 'major_outage' | 'maintenance';
|
||||
responseTime?: number;
|
||||
errorRate?: number;
|
||||
}
|
||||
|
||||
export interface IIncidentUpdate {
|
||||
id: string;
|
||||
timestamp: number;
|
||||
status: 'investigating' | 'identified' | 'monitoring' | 'resolved' | 'postmortem';
|
||||
message: string;
|
||||
author?: string;
|
||||
}
|
||||
|
||||
export interface IIncidentDetails {
|
||||
id: string;
|
||||
title: string;
|
||||
status: 'investigating' | 'identified' | 'monitoring' | 'resolved' | 'postmortem';
|
||||
severity: 'critical' | 'major' | 'minor' | 'maintenance';
|
||||
affectedServices: string[];
|
||||
startTime: number;
|
||||
endTime?: number;
|
||||
updates: IIncidentUpdate[];
|
||||
impact: string;
|
||||
rootCause?: string;
|
||||
resolution?: string;
|
||||
}
|
||||
|
||||
export interface IUptimeDay {
|
||||
date: string; // YYYY-MM-DD
|
||||
uptime: number; // percentage
|
||||
incidents: number;
|
||||
totalDowntime: number; // minutes
|
||||
status: 'operational' | 'degraded' | 'partial_outage' | 'major_outage';
|
||||
}
|
||||
|
||||
export interface IMonthlyUptime {
|
||||
month: string; // YYYY-MM
|
||||
days: IUptimeDay[];
|
||||
overallUptime: number; // percentage
|
||||
totalIncidents: number;
|
||||
}
|
||||
|
||||
export interface IOverallStatus {
|
||||
status: 'operational' | 'degraded' | 'partial_outage' | 'major_outage' | 'maintenance';
|
||||
message: string;
|
||||
lastUpdated: number;
|
||||
affectedServices: number;
|
||||
totalServices: number;
|
||||
}
|
||||
|
||||
export interface IStatusPageConfig {
|
||||
apiEndpoint?: string;
|
||||
refreshInterval?: number; // milliseconds
|
||||
timeZone?: string;
|
||||
dateFormat?: string;
|
||||
enableWebSocket?: boolean;
|
||||
enableNotifications?: boolean;
|
||||
theme?: 'light' | 'dark' | 'auto';
|
||||
language?: string;
|
||||
showHistoricalDays?: number;
|
||||
whitelabel?: boolean;
|
||||
companyName?: string;
|
||||
companyLogo?: string;
|
||||
supportEmail?: string;
|
||||
statusPageUrl?: string;
|
||||
legalUrl?: string;
|
||||
}
|
||||
|
||||
export interface ISubscription {
|
||||
email?: string;
|
||||
phone?: string;
|
||||
webhook?: string;
|
||||
services: string[];
|
||||
severityFilter: ('critical' | 'major' | 'minor' | 'maintenance')[];
|
||||
}
|
||||
|
||||
// Re-export the incident interface from @uptime.link/interfaces if needed
|
||||
// Note: The IIncident interface is imported in the incidents component directly from plugins
|
||||
4
ts_web/pages/index.ts
Normal file
4
ts_web/pages/index.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
export * from './statuspage-demo.js';
|
||||
export * from './statuspage-allgreen.js';
|
||||
export * from './statuspage-outage.js';
|
||||
export * from './statuspage-maintenance.js';
|
||||
412
ts_web/pages/statuspage-allgreen.ts
Normal file
412
ts_web/pages/statuspage-allgreen.ts
Normal file
@@ -0,0 +1,412 @@
|
||||
import { html, cssManager } from "@design.estate/dees-element";
|
||||
import type { IServiceStatus, IOverallStatus, IIncidentDetails, IStatusHistoryPoint, IMonthlyUptime } from '../interfaces/index.js';
|
||||
|
||||
export const statuspageAllgreen = () => html`
|
||||
<style>
|
||||
.demo-page-wrapper {
|
||||
min-height: 100vh;
|
||||
background: ${cssManager.bdTheme('#fafafa', '#0a0a0a')};
|
||||
}
|
||||
.demo-page-wrapper > dees-demowrapper {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 24px;
|
||||
}
|
||||
</style>
|
||||
|
||||
<div class="demo-page-wrapper">
|
||||
|
||||
<dees-demowrapper
|
||||
.runAfterRender=${async (wrapperElement: any) => {
|
||||
const header = wrapperElement.querySelector('upl-statuspage-header') as any;
|
||||
const statusBar = wrapperElement.querySelector('upl-statuspage-statusbar') as any;
|
||||
const statsGrid = wrapperElement.querySelector('upl-statuspage-statsgrid') as any;
|
||||
const assetsSelector = wrapperElement.querySelector('upl-statuspage-assetsselector') as any;
|
||||
const statusDetails = wrapperElement.querySelector('upl-statuspage-statusdetails') as any;
|
||||
const statusMonth = wrapperElement.querySelector('upl-statuspage-statusmonth') as any;
|
||||
const incidents = wrapperElement.querySelector('upl-statuspage-incidents') as any;
|
||||
const footer = wrapperElement.querySelector('upl-statuspage-footer') as any;
|
||||
|
||||
// Configure Header
|
||||
header.pageTitle = 'TechVault';
|
||||
header.showReportButton = true;
|
||||
header.showSubscribeButton = true;
|
||||
header.logoUrl = 'https://via.placeholder.com/150x50/4CAF50/ffffff?text=TV';
|
||||
|
||||
// Configure Overall Status - All Green
|
||||
statusBar.overallStatus = {
|
||||
status: 'operational',
|
||||
message: 'All Systems Operational',
|
||||
lastUpdated: Date.now(),
|
||||
affectedServices: 0,
|
||||
totalServices: 12
|
||||
};
|
||||
|
||||
// Configure Services - All Operational
|
||||
const services: IServiceStatus[] = [
|
||||
{
|
||||
id: 'web-app',
|
||||
name: 'web-app',
|
||||
displayName: 'Web Application',
|
||||
description: 'Main customer-facing application',
|
||||
currentStatus: 'operational',
|
||||
lastChecked: Date.now(),
|
||||
uptime30d: 99.99,
|
||||
uptime90d: 99.98,
|
||||
responseTime: 32,
|
||||
category: 'Frontend',
|
||||
selected: true
|
||||
},
|
||||
{
|
||||
id: 'mobile-app',
|
||||
name: 'mobile-app',
|
||||
displayName: 'Mobile Applications',
|
||||
description: 'iOS and Android apps',
|
||||
currentStatus: 'operational',
|
||||
lastChecked: Date.now(),
|
||||
uptime30d: 100,
|
||||
uptime90d: 99.99,
|
||||
responseTime: 45,
|
||||
category: 'Frontend',
|
||||
selected: true
|
||||
},
|
||||
{
|
||||
id: 'api',
|
||||
name: 'api',
|
||||
displayName: 'API Services',
|
||||
description: 'RESTful and GraphQL APIs',
|
||||
currentStatus: 'operational',
|
||||
lastChecked: Date.now(),
|
||||
uptime30d: 99.97,
|
||||
uptime90d: 99.96,
|
||||
responseTime: 28,
|
||||
category: 'Backend',
|
||||
selected: true
|
||||
},
|
||||
{
|
||||
id: 'auth',
|
||||
name: 'auth',
|
||||
displayName: 'Authentication',
|
||||
description: 'OAuth and SSO services',
|
||||
currentStatus: 'operational',
|
||||
lastChecked: Date.now(),
|
||||
uptime30d: 100,
|
||||
uptime90d: 99.99,
|
||||
responseTime: 22,
|
||||
category: 'Backend',
|
||||
selected: true
|
||||
},
|
||||
{
|
||||
id: 'database',
|
||||
name: 'database',
|
||||
displayName: 'Database Cluster',
|
||||
description: 'Primary data storage',
|
||||
currentStatus: 'operational',
|
||||
lastChecked: Date.now(),
|
||||
uptime30d: 100,
|
||||
uptime90d: 99.99,
|
||||
responseTime: 15,
|
||||
category: 'Infrastructure',
|
||||
selected: true
|
||||
},
|
||||
{
|
||||
id: 'cache',
|
||||
name: 'cache',
|
||||
displayName: 'Cache Layer',
|
||||
description: 'Redis cache clusters',
|
||||
currentStatus: 'operational',
|
||||
lastChecked: Date.now(),
|
||||
uptime30d: 100,
|
||||
uptime90d: 100,
|
||||
responseTime: 3,
|
||||
category: 'Infrastructure',
|
||||
selected: false
|
||||
},
|
||||
{
|
||||
id: 'search',
|
||||
name: 'search',
|
||||
displayName: 'Search Service',
|
||||
description: 'Elasticsearch clusters',
|
||||
currentStatus: 'operational',
|
||||
lastChecked: Date.now(),
|
||||
uptime30d: 99.98,
|
||||
uptime90d: 99.97,
|
||||
responseTime: 42,
|
||||
category: 'Infrastructure',
|
||||
selected: true
|
||||
},
|
||||
{
|
||||
id: 'cdn',
|
||||
name: 'cdn',
|
||||
displayName: 'CDN',
|
||||
description: 'Global content delivery',
|
||||
currentStatus: 'operational',
|
||||
lastChecked: Date.now(),
|
||||
uptime30d: 100,
|
||||
uptime90d: 100,
|
||||
responseTime: 8,
|
||||
category: 'Network',
|
||||
selected: true
|
||||
},
|
||||
{
|
||||
id: 'email',
|
||||
name: 'email',
|
||||
displayName: 'Email Service',
|
||||
description: 'Transactional emails',
|
||||
currentStatus: 'operational',
|
||||
lastChecked: Date.now(),
|
||||
uptime30d: 99.95,
|
||||
uptime90d: 99.94,
|
||||
responseTime: 125,
|
||||
category: 'Communication',
|
||||
selected: false
|
||||
},
|
||||
{
|
||||
id: 'sms',
|
||||
name: 'sms',
|
||||
displayName: 'SMS Service',
|
||||
description: 'SMS notifications',
|
||||
currentStatus: 'operational',
|
||||
lastChecked: Date.now(),
|
||||
uptime30d: 99.92,
|
||||
uptime90d: 99.90,
|
||||
responseTime: 180,
|
||||
category: 'Communication',
|
||||
selected: false
|
||||
},
|
||||
{
|
||||
id: 'payments',
|
||||
name: 'payments',
|
||||
displayName: 'Payment Processing',
|
||||
description: 'Payment gateway integration',
|
||||
currentStatus: 'operational',
|
||||
lastChecked: Date.now(),
|
||||
uptime30d: 100,
|
||||
uptime90d: 99.99,
|
||||
responseTime: 95,
|
||||
category: 'Services',
|
||||
selected: true
|
||||
},
|
||||
{
|
||||
id: 'analytics',
|
||||
name: 'analytics',
|
||||
displayName: 'Analytics Engine',
|
||||
description: 'Real-time analytics',
|
||||
currentStatus: 'operational',
|
||||
lastChecked: Date.now(),
|
||||
uptime30d: 99.96,
|
||||
uptime90d: 99.95,
|
||||
responseTime: 55,
|
||||
category: 'Services',
|
||||
selected: true
|
||||
}
|
||||
];
|
||||
|
||||
assetsSelector.services = services;
|
||||
|
||||
// Configure Stats Grid - All green metrics
|
||||
statsGrid.currentStatus = 'operational';
|
||||
statsGrid.uptime = 99.98;
|
||||
statsGrid.avgResponseTime = Math.round(services.reduce((sum, s) => sum + (s.responseTime || 0), 0) / services.length);
|
||||
statsGrid.totalIncidents = 0;
|
||||
statsGrid.affectedServices = 0;
|
||||
statsGrid.totalServices = services.length;
|
||||
statsGrid.timePeriod = '30 days';
|
||||
|
||||
// Configure Status Details - All operational for 48 hours
|
||||
const generateStatusDetails = (): IStatusHistoryPoint[] => {
|
||||
const details: IStatusHistoryPoint[] = [];
|
||||
const now = new Date();
|
||||
|
||||
for (let i = 47; i >= 0; i--) {
|
||||
const date = new Date(now);
|
||||
date.setMinutes(0, 0, 0);
|
||||
date.setHours(date.getHours() - i);
|
||||
|
||||
details.push({
|
||||
timestamp: date.getTime(),
|
||||
status: 'operational',
|
||||
responseTime: 20 + Math.random() * 10 // Very consistent response times
|
||||
});
|
||||
}
|
||||
|
||||
return details;
|
||||
};
|
||||
|
||||
statusDetails.dataPoints = generateStatusDetails();
|
||||
statusDetails.serviceId = 'api';
|
||||
statusDetails.serviceName = 'API Services';
|
||||
|
||||
// Configure Monthly Status - Near perfect uptime
|
||||
const generateMonthlyData = (): IMonthlyUptime[] => {
|
||||
const months: IMonthlyUptime[] = [];
|
||||
const now = new Date();
|
||||
|
||||
for (let m = 4; m >= 0; m--) {
|
||||
const monthDate = new Date(now.getFullYear(), now.getMonth() - m, 1);
|
||||
const daysInMonth = new Date(monthDate.getFullYear(), monthDate.getMonth() + 1, 0).getDate();
|
||||
const monthKey = monthDate.toISOString().slice(0, 7);
|
||||
|
||||
const days = [];
|
||||
let totalUptime = 0;
|
||||
let incidents = 0;
|
||||
|
||||
for (let d = 1; d <= daysInMonth; d++) {
|
||||
const dayDate = new Date(monthDate.getFullYear(), monthDate.getMonth(), d);
|
||||
const isFuture = dayDate > now;
|
||||
|
||||
if (isFuture) continue;
|
||||
|
||||
// Almost all days are perfect
|
||||
let status: 'operational' | 'degraded' | 'partial_outage' | 'major_outage' = 'operational';
|
||||
let uptime = 100;
|
||||
let dayIncidents = 0;
|
||||
|
||||
// Very rare minor issues
|
||||
if (Math.random() > 0.98) {
|
||||
status = 'degraded';
|
||||
uptime = 99.5 + Math.random() * 0.5;
|
||||
dayIncidents = 1;
|
||||
}
|
||||
|
||||
days.push({
|
||||
date: dayDate.toISOString().slice(0, 10),
|
||||
status,
|
||||
uptime,
|
||||
incidents: dayIncidents,
|
||||
totalDowntime: Math.round((100 - uptime) * 14.4)
|
||||
});
|
||||
|
||||
totalUptime += uptime;
|
||||
incidents += dayIncidents;
|
||||
}
|
||||
|
||||
months.push({
|
||||
month: monthKey,
|
||||
days,
|
||||
overallUptime: totalUptime / days.length,
|
||||
totalIncidents: incidents
|
||||
});
|
||||
}
|
||||
|
||||
return months;
|
||||
};
|
||||
|
||||
statusMonth.monthlyData = generateMonthlyData();
|
||||
statusMonth.serviceId = 'all-services';
|
||||
statusMonth.serviceName = 'All Services';
|
||||
|
||||
// Configure Incidents - None current, few past
|
||||
const currentIncidents: IIncidentDetails[] = [];
|
||||
|
||||
const pastIncidents: IIncidentDetails[] = [
|
||||
{
|
||||
id: 'inc-2024-001',
|
||||
title: 'Brief API Response Time Increase',
|
||||
impact: 'API responses were slightly slower than usual',
|
||||
severity: 'minor',
|
||||
status: 'resolved',
|
||||
startTime: Date.now() - 45 * 24 * 60 * 60 * 1000,
|
||||
endTime: Date.now() - 45 * 24 * 60 * 60 * 1000 + 15 * 60 * 1000,
|
||||
affectedServices: ['API Services'],
|
||||
rootCause: 'Temporary increase in traffic due to a viral marketing campaign.',
|
||||
resolution: 'Auto-scaling handled the load increase. No manual intervention required.',
|
||||
updates: [
|
||||
{
|
||||
id: 'update-1',
|
||||
timestamp: Date.now() - 45 * 24 * 60 * 60 * 1000,
|
||||
status: 'investigating',
|
||||
message: 'Monitoring increased API response times.',
|
||||
author: 'Automated Monitoring'
|
||||
},
|
||||
{
|
||||
id: 'update-2',
|
||||
timestamp: Date.now() - 45 * 24 * 60 * 60 * 1000 + 5 * 60 * 1000,
|
||||
status: 'identified',
|
||||
message: 'Traffic spike identified. Auto-scaling in progress.',
|
||||
author: 'DevOps Team'
|
||||
},
|
||||
{
|
||||
id: 'update-3',
|
||||
timestamp: Date.now() - 45 * 24 * 60 * 60 * 1000 + 15 * 60 * 1000,
|
||||
status: 'resolved',
|
||||
message: 'Response times back to normal. Incident resolved.',
|
||||
author: 'Automated Monitoring'
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
id: 'inc-2023-089',
|
||||
title: 'Scheduled Database Maintenance',
|
||||
impact: 'Database was in read-only mode for 30 minutes',
|
||||
severity: 'minor',
|
||||
status: 'resolved',
|
||||
startTime: Date.now() - 90 * 24 * 60 * 60 * 1000,
|
||||
endTime: Date.now() - 90 * 24 * 60 * 60 * 1000 + 30 * 60 * 1000,
|
||||
affectedServices: ['Database Cluster'],
|
||||
rootCause: 'Scheduled maintenance for security patches.',
|
||||
resolution: 'Maintenance completed successfully as planned.',
|
||||
updates: [
|
||||
{
|
||||
id: 'update-4',
|
||||
timestamp: Date.now() - 97 * 24 * 60 * 60 * 1000,
|
||||
status: 'investigating',
|
||||
message: 'Scheduled maintenance announced.',
|
||||
author: 'Database Team'
|
||||
},
|
||||
{
|
||||
id: 'update-5',
|
||||
timestamp: Date.now() - 90 * 24 * 60 * 60 * 1000,
|
||||
status: 'monitoring',
|
||||
message: 'Maintenance started. Database in read-only mode.',
|
||||
author: 'Database Team'
|
||||
},
|
||||
{
|
||||
id: 'update-6',
|
||||
timestamp: Date.now() - 90 * 24 * 60 * 60 * 1000 + 30 * 60 * 1000,
|
||||
status: 'resolved',
|
||||
message: 'Maintenance completed. All systems operational.',
|
||||
author: 'Database Team'
|
||||
}
|
||||
]
|
||||
}
|
||||
];
|
||||
|
||||
incidents.currentIncidents = currentIncidents;
|
||||
incidents.pastIncidents = pastIncidents;
|
||||
|
||||
// Configure Footer
|
||||
footer.companyName = 'TechVault Services';
|
||||
footer.legalUrl = 'https://techvault.example.com/legal';
|
||||
footer.supportEmail = 'support@techvault.example.com';
|
||||
footer.statusPageUrl = 'https://status.techvault.example.com';
|
||||
footer.lastUpdated = Date.now();
|
||||
footer.currentYear = new Date().getFullYear();
|
||||
footer.socialLinks = [
|
||||
{ platform: 'twitter', url: 'https://twitter.com/techvault' },
|
||||
{ platform: 'github', url: 'https://github.com/techvault' },
|
||||
{ platform: 'linkedin', url: 'https://linkedin.com/company/techvault' },
|
||||
{ platform: 'youtube', url: 'https://youtube.com/techvault' }
|
||||
];
|
||||
footer.rssFeedUrl = 'https://status.techvault.example.com/rss';
|
||||
footer.apiStatusUrl = 'https://api.techvault.example.com/v1/status';
|
||||
footer.enableSubscribe = true;
|
||||
footer.subscriberCount = 5421;
|
||||
footer.additionalLinks = [
|
||||
{ label: 'API Documentation', url: 'https://docs.techvault.example.com' },
|
||||
{ label: 'System Architecture', url: 'https://techvault.example.com/architecture' },
|
||||
{ label: 'Security', url: 'https://techvault.example.com/security' }
|
||||
];
|
||||
}}
|
||||
>
|
||||
<upl-statuspage-header></upl-statuspage-header>
|
||||
<upl-statuspage-statusbar></upl-statuspage-statusbar>
|
||||
<upl-statuspage-statsgrid></upl-statuspage-statsgrid>
|
||||
<upl-statuspage-assetsselector></upl-statuspage-assetsselector>
|
||||
<upl-statuspage-statusdetails></upl-statuspage-statusdetails>
|
||||
<upl-statuspage-statusmonth></upl-statuspage-statusmonth>
|
||||
<upl-statuspage-incidents></upl-statuspage-incidents>
|
||||
<upl-statuspage-footer></upl-statuspage-footer>
|
||||
</dees-demowrapper>
|
||||
</div>
|
||||
`;
|
||||
653
ts_web/pages/statuspage-demo.ts
Normal file
653
ts_web/pages/statuspage-demo.ts
Normal file
@@ -0,0 +1,653 @@
|
||||
import { html, cssManager } from "@design.estate/dees-element";
|
||||
import type { IServiceStatus, IOverallStatus, IIncidentDetails, IStatusHistoryPoint, IMonthlyUptime } from '../interfaces/index.js';
|
||||
|
||||
export const statuspageDemo = () => html`
|
||||
<style>
|
||||
.demo-page-wrapper {
|
||||
min-height: 100vh;
|
||||
background: ${cssManager.bdTheme('#fafafa', '#0a0a0a')};
|
||||
}
|
||||
.demo-page-wrapper > dees-demowrapper {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 24px;
|
||||
}
|
||||
</style>
|
||||
|
||||
<div class="demo-page-wrapper">
|
||||
|
||||
<dees-demowrapper
|
||||
.runAfterRender=${async (wrapperElement: any) => {
|
||||
const header = wrapperElement.querySelector('upl-statuspage-header') as any;
|
||||
const pageTitle = wrapperElement.querySelector('upl-statuspage-pagetitle') as any;
|
||||
const statusBar = wrapperElement.querySelector('upl-statuspage-statusbar') as any;
|
||||
const statsGrid = wrapperElement.querySelector('upl-statuspage-statsgrid') as any;
|
||||
const assetsSelector = wrapperElement.querySelector('upl-statuspage-assetsselector') as any;
|
||||
const statusDetails = wrapperElement.querySelector('upl-statuspage-statusdetails') as any;
|
||||
const statusMonth = wrapperElement.querySelector('upl-statuspage-statusmonth') as any;
|
||||
const incidents = wrapperElement.querySelector('upl-statuspage-incidents') as any;
|
||||
const footer = wrapperElement.querySelector('upl-statuspage-footer') as any;
|
||||
|
||||
// Configure Header
|
||||
header.pageTitle = 'CloudFlow';
|
||||
header.showReportButton = true;
|
||||
header.showSubscribeButton = true;
|
||||
header.logoUrl = 'https://via.placeholder.com/150x50/2196F3/ffffff?text=CF';
|
||||
|
||||
// Configure Page Title
|
||||
pageTitle.pageTitle = 'Service Status';
|
||||
pageTitle.pageSubtitle = 'Current operational status of CloudFlow Infrastructure services';
|
||||
|
||||
// Configure Overall Status
|
||||
statusBar.overallStatus = {
|
||||
status: 'degraded',
|
||||
message: 'Minor service degradation in EU-West region',
|
||||
lastUpdated: Date.now(),
|
||||
affectedServices: 3,
|
||||
totalServices: 18
|
||||
};
|
||||
|
||||
// Configure Services
|
||||
const services: IServiceStatus[] = [
|
||||
// Core 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: 'Core Services',
|
||||
selected: true
|
||||
},
|
||||
{
|
||||
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: 32,
|
||||
category: 'Core Services',
|
||||
selected: true
|
||||
},
|
||||
{
|
||||
id: 'user-dashboard',
|
||||
name: 'user-dashboard',
|
||||
displayName: 'User Dashboard',
|
||||
description: 'Web application dashboard',
|
||||
currentStatus: 'operational',
|
||||
lastChecked: Date.now(),
|
||||
uptime30d: 99.99,
|
||||
uptime90d: 99.97,
|
||||
responseTime: 128,
|
||||
category: 'Web Services',
|
||||
selected: true
|
||||
},
|
||||
// Regional Services - US
|
||||
{
|
||||
id: 'us-east-compute',
|
||||
name: 'us-east-compute',
|
||||
displayName: 'US-East Compute',
|
||||
description: 'Virtual machine instances',
|
||||
currentStatus: 'operational',
|
||||
lastChecked: Date.now(),
|
||||
uptime30d: 99.94,
|
||||
uptime90d: 99.91,
|
||||
responseTime: 22,
|
||||
category: 'US-East',
|
||||
selected: true
|
||||
},
|
||||
{
|
||||
id: 'us-east-storage',
|
||||
name: 'us-east-storage',
|
||||
displayName: 'US-East Storage',
|
||||
description: 'Object storage service',
|
||||
currentStatus: 'operational',
|
||||
lastChecked: Date.now(),
|
||||
uptime30d: 100,
|
||||
uptime90d: 99.99,
|
||||
responseTime: 18,
|
||||
category: 'US-East',
|
||||
selected: true
|
||||
},
|
||||
{
|
||||
id: 'us-east-database',
|
||||
name: 'us-east-database',
|
||||
displayName: 'US-East Database',
|
||||
description: 'Managed database clusters',
|
||||
currentStatus: 'operational',
|
||||
lastChecked: Date.now(),
|
||||
uptime30d: 99.97,
|
||||
uptime90d: 99.95,
|
||||
responseTime: 14,
|
||||
category: 'US-East',
|
||||
selected: false
|
||||
},
|
||||
// Regional Services - EU (Degraded)
|
||||
{
|
||||
id: 'eu-west-compute',
|
||||
name: 'eu-west-compute',
|
||||
displayName: 'EU-West Compute',
|
||||
description: 'Virtual machine instances',
|
||||
currentStatus: 'degraded',
|
||||
lastChecked: Date.now(),
|
||||
uptime30d: 98.2,
|
||||
uptime90d: 99.1,
|
||||
responseTime: 145,
|
||||
category: 'EU-West',
|
||||
selected: true
|
||||
},
|
||||
{
|
||||
id: 'eu-west-storage',
|
||||
name: 'eu-west-storage',
|
||||
displayName: 'EU-West Storage',
|
||||
description: 'Object storage service',
|
||||
currentStatus: 'degraded',
|
||||
lastChecked: Date.now(),
|
||||
uptime30d: 98.5,
|
||||
uptime90d: 99.3,
|
||||
responseTime: 220,
|
||||
category: 'EU-West',
|
||||
selected: true
|
||||
},
|
||||
{
|
||||
id: 'eu-west-database',
|
||||
name: 'eu-west-database',
|
||||
displayName: 'EU-West Database',
|
||||
description: 'Managed database clusters',
|
||||
currentStatus: 'partial_outage',
|
||||
lastChecked: Date.now(),
|
||||
uptime30d: 97.8,
|
||||
uptime90d: 98.9,
|
||||
responseTime: 450,
|
||||
category: 'EU-West',
|
||||
selected: true
|
||||
},
|
||||
// Regional Services - Asia
|
||||
{
|
||||
id: 'ap-south-compute',
|
||||
name: 'ap-south-compute',
|
||||
displayName: 'Asia-Pacific Compute',
|
||||
description: 'Virtual machine instances',
|
||||
currentStatus: 'operational',
|
||||
lastChecked: Date.now(),
|
||||
uptime30d: 99.91,
|
||||
uptime90d: 99.88,
|
||||
responseTime: 38,
|
||||
category: 'Asia-Pacific',
|
||||
selected: false
|
||||
},
|
||||
{
|
||||
id: 'ap-south-storage',
|
||||
name: 'ap-south-storage',
|
||||
displayName: 'Asia-Pacific Storage',
|
||||
description: 'Object storage service',
|
||||
currentStatus: 'operational',
|
||||
lastChecked: Date.now(),
|
||||
uptime30d: 99.95,
|
||||
uptime90d: 99.93,
|
||||
responseTime: 42,
|
||||
category: 'Asia-Pacific',
|
||||
selected: false
|
||||
},
|
||||
// Supporting Services
|
||||
{
|
||||
id: 'cdn',
|
||||
name: 'cdn',
|
||||
displayName: 'Content Delivery Network',
|
||||
description: 'Global CDN for static assets',
|
||||
currentStatus: 'operational',
|
||||
lastChecked: Date.now(),
|
||||
uptime30d: 100,
|
||||
uptime90d: 99.99,
|
||||
responseTime: 12,
|
||||
category: 'Network',
|
||||
selected: true
|
||||
},
|
||||
{
|
||||
id: 'dns',
|
||||
name: 'dns',
|
||||
displayName: 'DNS Service',
|
||||
description: 'Managed DNS resolution',
|
||||
currentStatus: 'operational',
|
||||
lastChecked: Date.now(),
|
||||
uptime30d: 100,
|
||||
uptime90d: 100,
|
||||
responseTime: 8,
|
||||
category: 'Network',
|
||||
selected: false
|
||||
},
|
||||
{
|
||||
id: 'monitoring',
|
||||
name: 'monitoring',
|
||||
displayName: 'Monitoring Service',
|
||||
description: 'Infrastructure monitoring',
|
||||
currentStatus: 'operational',
|
||||
lastChecked: Date.now(),
|
||||
uptime30d: 99.92,
|
||||
uptime90d: 99.90,
|
||||
responseTime: 65,
|
||||
category: 'Operations',
|
||||
selected: true
|
||||
},
|
||||
{
|
||||
id: 'logging',
|
||||
name: 'logging',
|
||||
displayName: 'Logging Service',
|
||||
description: 'Centralized log management',
|
||||
currentStatus: 'operational',
|
||||
lastChecked: Date.now(),
|
||||
uptime30d: 99.88,
|
||||
uptime90d: 99.85,
|
||||
responseTime: 72,
|
||||
category: 'Operations',
|
||||
selected: false
|
||||
},
|
||||
{
|
||||
id: 'backup-service',
|
||||
name: 'backup-service',
|
||||
displayName: 'Backup Service',
|
||||
description: 'Automated backup system',
|
||||
currentStatus: 'operational',
|
||||
lastChecked: Date.now(),
|
||||
uptime30d: 100,
|
||||
uptime90d: 99.98,
|
||||
responseTime: 95,
|
||||
category: 'Operations',
|
||||
selected: false
|
||||
},
|
||||
{
|
||||
id: 'email-service',
|
||||
name: 'email-service',
|
||||
displayName: 'Email Delivery',
|
||||
description: 'Transactional email service',
|
||||
currentStatus: 'operational',
|
||||
lastChecked: Date.now(),
|
||||
uptime30d: 99.94,
|
||||
uptime90d: 99.91,
|
||||
responseTime: 125,
|
||||
category: 'Communication',
|
||||
selected: true
|
||||
},
|
||||
{
|
||||
id: 'sms-service',
|
||||
name: 'sms-service',
|
||||
displayName: 'SMS Gateway',
|
||||
description: 'SMS notification service',
|
||||
currentStatus: 'operational',
|
||||
lastChecked: Date.now(),
|
||||
uptime30d: 99.89,
|
||||
uptime90d: 99.87,
|
||||
responseTime: 180,
|
||||
category: 'Communication',
|
||||
selected: false
|
||||
}
|
||||
];
|
||||
|
||||
assetsSelector.services = services;
|
||||
|
||||
// Configure Status Details (48 hours of data)
|
||||
const generateStatusDetails = (): IStatusHistoryPoint[] => {
|
||||
const details: IStatusHistoryPoint[] = [];
|
||||
const now = new Date();
|
||||
|
||||
for (let i = 47; i >= 0; i--) {
|
||||
const date = new Date(now);
|
||||
date.setMinutes(0, 0, 0);
|
||||
date.setHours(date.getHours() - i);
|
||||
|
||||
let status: 'operational' | 'degraded' | 'outage' = 'operational';
|
||||
let value = 100;
|
||||
|
||||
// Simulate the EU-West issues from 6-3 hours ago
|
||||
if (i >= 3 && i <= 6) {
|
||||
status = 'degraded';
|
||||
value = 85 + Math.random() * 10;
|
||||
} else if (i > 6 && i <= 12) {
|
||||
// Some minor issues earlier
|
||||
if (Math.random() > 0.8) {
|
||||
status = 'degraded';
|
||||
value = 90 + Math.random() * 10;
|
||||
}
|
||||
}
|
||||
|
||||
details.push({
|
||||
timestamp: date.getTime(),
|
||||
status,
|
||||
responseTime: status === 'operational' ? 20 + Math.random() * 30 : 50 + Math.random() * 100
|
||||
});
|
||||
}
|
||||
|
||||
return details;
|
||||
};
|
||||
|
||||
statusDetails.dataPoints = generateStatusDetails();
|
||||
statusDetails.serviceId = 'eu-west-database';
|
||||
statusDetails.serviceName = 'EU-West Database';
|
||||
|
||||
// Configure Monthly Status
|
||||
const generateMonthlyData = (): IMonthlyUptime[] => {
|
||||
const months: IMonthlyUptime[] = [];
|
||||
const now = new Date();
|
||||
|
||||
for (let m = 4; m >= 0; m--) {
|
||||
const monthDate = new Date(now.getFullYear(), now.getMonth() - m, 1);
|
||||
const daysInMonth = new Date(monthDate.getFullYear(), monthDate.getMonth() + 1, 0).getDate();
|
||||
const monthKey = monthDate.toISOString().slice(0, 7);
|
||||
|
||||
const days = [];
|
||||
let totalUptime = 0;
|
||||
let incidents = 0;
|
||||
|
||||
for (let d = 1; d <= daysInMonth; d++) {
|
||||
const dayDate = new Date(monthDate.getFullYear(), monthDate.getMonth(), d);
|
||||
const isToday = dayDate.toDateString() === now.toDateString();
|
||||
const isFuture = dayDate > now;
|
||||
|
||||
if (isFuture) continue;
|
||||
|
||||
let status: 'operational' | 'degraded' | 'partial_outage' | 'major_outage' = 'operational';
|
||||
let uptime = 100;
|
||||
let dayIncidents = 0;
|
||||
|
||||
// Add some random incidents
|
||||
if (!isToday && Math.random() > 0.92) {
|
||||
status = 'degraded';
|
||||
uptime = 95 + Math.random() * 4;
|
||||
dayIncidents = 1;
|
||||
} else if (!isToday && Math.random() > 0.98) {
|
||||
status = 'partial_outage';
|
||||
uptime = 80 + Math.random() * 15;
|
||||
dayIncidents = 2;
|
||||
}
|
||||
|
||||
// Today's incident
|
||||
if (isToday) {
|
||||
status = 'degraded';
|
||||
uptime = 96.5;
|
||||
dayIncidents = 1;
|
||||
}
|
||||
|
||||
days.push({
|
||||
date: dayDate.toISOString().slice(0, 10),
|
||||
status,
|
||||
uptime,
|
||||
incidents: dayIncidents,
|
||||
totalDowntime: Math.round((100 - uptime) * 14.4) // Convert to minutes (1440 minutes per day)
|
||||
});
|
||||
|
||||
totalUptime += uptime;
|
||||
incidents += dayIncidents;
|
||||
}
|
||||
|
||||
months.push({
|
||||
month: monthKey,
|
||||
days,
|
||||
overallUptime: totalUptime / days.length,
|
||||
totalIncidents: incidents
|
||||
});
|
||||
}
|
||||
|
||||
return months;
|
||||
};
|
||||
|
||||
statusMonth.monthlyData = generateMonthlyData();
|
||||
statusMonth.serviceId = 'all-services';
|
||||
statusMonth.serviceName = 'All Services';
|
||||
|
||||
// Configure Incidents
|
||||
const currentIncidents: IIncidentDetails[] = [
|
||||
{
|
||||
id: 'inc-2024-001',
|
||||
title: 'EU-West Database Performance Degradation',
|
||||
impact: 'Users in EU-West region may experience slower database queries and occasional timeouts',
|
||||
severity: 'major',
|
||||
status: 'investigating',
|
||||
startTime: Date.now() - 3 * 60 * 60 * 1000, // 3 hours ago
|
||||
affectedServices: ['EU-West Database', 'EU-West Compute', 'EU-West Storage'],
|
||||
updates: [
|
||||
{
|
||||
id: 'update-1',
|
||||
timestamp: Date.now() - 3 * 60 * 60 * 1000,
|
||||
status: 'investigating',
|
||||
message: 'We are investigating reports of database performance issues in the EU-West region.',
|
||||
author: 'CloudFlow Operations'
|
||||
},
|
||||
{
|
||||
id: 'update-2',
|
||||
timestamp: Date.now() - 2.5 * 60 * 60 * 1000,
|
||||
status: 'identified',
|
||||
message: 'We have identified unusual load patterns on our EU-West database cluster. Our team is working on redistributing the load.',
|
||||
author: 'Database Team'
|
||||
},
|
||||
{
|
||||
id: 'update-3',
|
||||
timestamp: Date.now() - 1 * 60 * 60 * 1000,
|
||||
status: 'monitoring',
|
||||
message: 'Load balancing adjustments have been applied. We are monitoring the situation closely. Performance is gradually improving.',
|
||||
author: 'CloudFlow Operations'
|
||||
}
|
||||
]
|
||||
}
|
||||
];
|
||||
|
||||
const pastIncidents: IIncidentDetails[] = [
|
||||
{
|
||||
id: 'inc-2023-098',
|
||||
title: 'Global Authentication Service Outage',
|
||||
impact: 'Users were unable to log in to their accounts',
|
||||
severity: 'critical',
|
||||
status: 'resolved',
|
||||
startTime: Date.now() - 7 * 24 * 60 * 60 * 1000, // 7 days ago
|
||||
endTime: Date.now() - 7 * 24 * 60 * 60 * 1000 + 2 * 60 * 60 * 1000, // 2 hour duration
|
||||
affectedServices: ['Authentication Service', 'User Dashboard', 'API Gateway'],
|
||||
rootCause: 'A configuration change in our authentication service caused a cascading failure in the token validation system.',
|
||||
resolution: 'The configuration was rolled back and additional safeguards were implemented to prevent similar issues.',
|
||||
updates: [
|
||||
{
|
||||
id: 'update-4',
|
||||
timestamp: Date.now() - 7 * 24 * 60 * 60 * 1000,
|
||||
status: 'investigating',
|
||||
message: 'Multiple reports of login failures across all regions.',
|
||||
author: 'CloudFlow Operations'
|
||||
},
|
||||
{
|
||||
id: 'update-5',
|
||||
timestamp: Date.now() - 7 * 24 * 60 * 60 * 1000 + 30 * 60 * 1000,
|
||||
status: 'identified',
|
||||
message: 'Root cause identified as misconfiguration in auth service. Rolling back changes.',
|
||||
author: 'Security Team'
|
||||
},
|
||||
{
|
||||
id: 'update-6',
|
||||
timestamp: Date.now() - 7 * 24 * 60 * 60 * 1000 + 90 * 60 * 1000,
|
||||
status: 'monitoring',
|
||||
message: 'Service restored. Monitoring for any lingering issues.',
|
||||
author: 'CloudFlow Operations'
|
||||
},
|
||||
{
|
||||
id: 'update-7',
|
||||
timestamp: Date.now() - 7 * 24 * 60 * 60 * 1000 + 2 * 60 * 60 * 1000,
|
||||
status: 'resolved',
|
||||
message: 'All systems operating normally. Incident resolved.',
|
||||
author: 'CloudFlow Operations'
|
||||
},
|
||||
{
|
||||
id: 'update-8',
|
||||
timestamp: Date.now() - 6 * 24 * 60 * 60 * 1000,
|
||||
status: 'postmortem',
|
||||
message: 'Postmortem completed. Action items identified and assigned to prevent recurrence.',
|
||||
author: 'Engineering Lead'
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
id: 'inc-2023-095',
|
||||
title: 'US-East Storage Service Maintenance',
|
||||
impact: 'Reduced redundancy for stored objects during maintenance window',
|
||||
severity: 'minor',
|
||||
status: 'resolved',
|
||||
startTime: Date.now() - 14 * 24 * 60 * 60 * 1000,
|
||||
endTime: Date.now() - 14 * 24 * 60 * 60 * 1000 + 4 * 60 * 60 * 1000,
|
||||
affectedServices: ['US-East Storage'],
|
||||
rootCause: 'Scheduled maintenance for storage infrastructure upgrade.',
|
||||
resolution: 'Maintenance completed successfully. All systems operating at full redundancy.',
|
||||
updates: [
|
||||
{
|
||||
id: 'update-9',
|
||||
timestamp: Date.now() - 21 * 24 * 60 * 60 * 1000,
|
||||
status: 'investigating',
|
||||
message: 'Scheduled maintenance announced for US-East Storage service.',
|
||||
author: 'CloudFlow Operations'
|
||||
},
|
||||
{
|
||||
id: 'update-10',
|
||||
timestamp: Date.now() - 14 * 24 * 60 * 60 * 1000,
|
||||
status: 'monitoring',
|
||||
message: 'Maintenance window has begun. No customer impact expected.',
|
||||
author: 'Storage Team'
|
||||
},
|
||||
{
|
||||
id: 'update-11',
|
||||
timestamp: Date.now() - 14 * 24 * 60 * 60 * 1000 + 4 * 60 * 60 * 1000,
|
||||
status: 'resolved',
|
||||
message: 'Maintenance completed successfully.',
|
||||
author: 'Storage Team'
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
id: 'inc-2023-089',
|
||||
title: 'API Gateway Rate Limiting Issues',
|
||||
impact: 'Some API requests were incorrectly rate limited',
|
||||
severity: 'major',
|
||||
status: 'resolved',
|
||||
startTime: Date.now() - 30 * 24 * 60 * 60 * 1000,
|
||||
endTime: Date.now() - 30 * 24 * 60 * 60 * 1000 + 45 * 60 * 1000,
|
||||
affectedServices: ['API Gateway'],
|
||||
rootCause: 'A bug in the rate limiting algorithm caused legitimate requests to be blocked.',
|
||||
resolution: 'Hotfix deployed to correct the rate limiting logic.',
|
||||
updates: [
|
||||
{
|
||||
id: 'update-12',
|
||||
timestamp: Date.now() - 30 * 24 * 60 * 60 * 1000,
|
||||
status: 'investigating',
|
||||
message: 'Reports of API requests being blocked despite being within rate limits.',
|
||||
author: 'API Team'
|
||||
},
|
||||
{
|
||||
id: 'update-13',
|
||||
timestamp: Date.now() - 30 * 24 * 60 * 60 * 1000 + 20 * 60 * 1000,
|
||||
status: 'identified',
|
||||
message: 'Bug identified in rate limiting code. Preparing hotfix.',
|
||||
author: 'API Team'
|
||||
},
|
||||
{
|
||||
id: 'update-14',
|
||||
timestamp: Date.now() - 30 * 24 * 60 * 60 * 1000 + 45 * 60 * 1000,
|
||||
status: 'resolved',
|
||||
message: 'Hotfix deployed. Rate limiting now functioning correctly.',
|
||||
author: 'API Team'
|
||||
}
|
||||
]
|
||||
}
|
||||
];
|
||||
|
||||
incidents.currentIncidents = currentIncidents;
|
||||
incidents.pastIncidents = pastIncidents;
|
||||
|
||||
// Configure Stats Grid
|
||||
const operationalCount = services.filter(s => s.currentStatus === 'operational').length;
|
||||
const totalIncidents = currentIncidents.length + 3; // Current + recent past
|
||||
const avgResponseTime = services.reduce((sum, s) => sum + (s.responseTime || 0), 0) / services.length;
|
||||
const avgUptime = services.reduce((sum, s) => sum + (s.uptime30d || 0), 0) / services.length;
|
||||
|
||||
statsGrid.currentStatus = 'degraded';
|
||||
statsGrid.uptime = avgUptime;
|
||||
statsGrid.avgResponseTime = Math.round(avgResponseTime);
|
||||
statsGrid.totalIncidents = totalIncidents;
|
||||
statsGrid.affectedServices = services.filter(s => s.currentStatus !== 'operational').length;
|
||||
statsGrid.totalServices = services.length;
|
||||
statsGrid.timePeriod = '30 days';
|
||||
|
||||
// Configure Footer
|
||||
footer.companyName = 'CloudFlow Infrastructure';
|
||||
footer.legalUrl = 'https://cloudflow.example.com/legal';
|
||||
footer.supportEmail = 'support@cloudflow.example.com';
|
||||
footer.statusPageUrl = 'https://status.cloudflow.example.com';
|
||||
footer.lastUpdated = Date.now();
|
||||
footer.currentYear = new Date().getFullYear();
|
||||
footer.socialLinks = [
|
||||
{ platform: 'twitter', url: 'https://twitter.com/cloudflow' },
|
||||
{ platform: 'github', url: 'https://github.com/cloudflow' },
|
||||
{ platform: 'linkedin', url: 'https://linkedin.com/company/cloudflow' }
|
||||
];
|
||||
footer.rssFeedUrl = 'https://status.cloudflow.example.com/rss';
|
||||
footer.apiStatusUrl = 'https://api.cloudflow.example.com/v1/status';
|
||||
footer.enableSubscribe = true;
|
||||
footer.subscriberCount = 2847;
|
||||
footer.additionalLinks = [
|
||||
{ label: 'API Documentation', url: 'https://docs.cloudflow.example.com' },
|
||||
{ label: 'Service SLA', url: 'https://cloudflow.example.com/sla' },
|
||||
{ label: 'Privacy Policy', url: 'https://cloudflow.example.com/privacy' }
|
||||
];
|
||||
|
||||
// Add event listeners for interactivity
|
||||
header.addEventListener('reportNewIncident', () => {
|
||||
alert('Report Incident: This would open a form to report a new incident.');
|
||||
});
|
||||
|
||||
header.addEventListener('statusSubscribe', () => {
|
||||
alert('Subscribe: This would open a subscription form for status updates.');
|
||||
});
|
||||
|
||||
footer.addEventListener('subscribeClick', () => {
|
||||
alert('Subscribe to updates: Enter your email for status notifications.');
|
||||
});
|
||||
|
||||
assetsSelector.addEventListener('selectionChanged', (event: CustomEvent) => {
|
||||
console.log('Selected services changed:', event.detail.selectedServices);
|
||||
// In a real app, this would update the other components to show only selected services
|
||||
});
|
||||
|
||||
statusMonth.addEventListener('dayClick', (event: CustomEvent) => {
|
||||
console.log('Day clicked:', event.detail);
|
||||
alert(`Day details: ${event.detail.date}\nUptime: ${event.detail.uptime}%\nIncidents: ${event.detail.incidents}`);
|
||||
});
|
||||
|
||||
// Simulate real-time updates
|
||||
setInterval(() => {
|
||||
// Update last checked times
|
||||
services.forEach(service => {
|
||||
service.lastChecked = Date.now();
|
||||
// Randomly change response times
|
||||
service.responseTime = service.responseTime * (0.9 + Math.random() * 0.2);
|
||||
});
|
||||
assetsSelector.requestUpdate();
|
||||
|
||||
// Update overall status last updated
|
||||
statusBar.overallStatus = { ...statusBar.overallStatus, lastUpdated: Date.now() };
|
||||
|
||||
// Update footer last updated
|
||||
footer.lastUpdated = Date.now();
|
||||
}, 30000); // Every 30 seconds
|
||||
}}
|
||||
>
|
||||
<upl-statuspage-header></upl-statuspage-header>
|
||||
<upl-statuspage-pagetitle></upl-statuspage-pagetitle>
|
||||
<upl-statuspage-statusbar></upl-statuspage-statusbar>
|
||||
<upl-statuspage-statsgrid></upl-statuspage-statsgrid>
|
||||
<upl-statuspage-assetsselector></upl-statuspage-assetsselector>
|
||||
<upl-statuspage-statusdetails></upl-statuspage-statusdetails>
|
||||
<upl-statuspage-statusmonth></upl-statuspage-statusmonth>
|
||||
<upl-statuspage-incidents></upl-statuspage-incidents>
|
||||
<upl-statuspage-footer></upl-statuspage-footer>
|
||||
</dees-demowrapper>
|
||||
</div>
|
||||
`;
|
||||
570
ts_web/pages/statuspage-maintenance.ts
Normal file
570
ts_web/pages/statuspage-maintenance.ts
Normal file
@@ -0,0 +1,570 @@
|
||||
import { html, cssManager } from "@design.estate/dees-element";
|
||||
import type { IServiceStatus, IOverallStatus, IIncidentDetails, IStatusHistoryPoint, IMonthlyUptime } from '../interfaces/index.js';
|
||||
|
||||
export const statuspageMaintenance = () => html`
|
||||
<style>
|
||||
.demo-page-wrapper {
|
||||
min-height: 100vh;
|
||||
background: ${cssManager.bdTheme('#fafafa', '#0a0a0a')};
|
||||
}
|
||||
.demo-page-wrapper > dees-demowrapper {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 24px;
|
||||
}
|
||||
</style>
|
||||
|
||||
<div class="demo-page-wrapper">
|
||||
|
||||
<dees-demowrapper
|
||||
.runAfterRender=${async (wrapperElement: any) => {
|
||||
const header = wrapperElement.querySelector('upl-statuspage-header') as any;
|
||||
const statusBar = wrapperElement.querySelector('upl-statuspage-statusbar') as any;
|
||||
const statsGrid = wrapperElement.querySelector('upl-statuspage-statsgrid') as any;
|
||||
const assetsSelector = wrapperElement.querySelector('upl-statuspage-assetsselector') as any;
|
||||
const statusDetails = wrapperElement.querySelector('upl-statuspage-statusdetails') as any;
|
||||
const statusMonth = wrapperElement.querySelector('upl-statuspage-statusmonth') as any;
|
||||
const incidents = wrapperElement.querySelector('upl-statuspage-incidents') as any;
|
||||
const footer = wrapperElement.querySelector('upl-statuspage-footer') as any;
|
||||
|
||||
// Configure Header
|
||||
header.pageTitle = 'SecureVault';
|
||||
header.showReportButton = true;
|
||||
header.showSubscribeButton = true;
|
||||
header.logoUrl = 'https://via.placeholder.com/150x50/2196F3/ffffff?text=SV';
|
||||
|
||||
// Configure Overall Status - Maintenance
|
||||
statusBar.overallStatus = {
|
||||
status: 'maintenance',
|
||||
message: 'Scheduled maintenance in progress - Expected completion: 2:00 AM UTC',
|
||||
lastUpdated: Date.now(),
|
||||
affectedServices: 5,
|
||||
totalServices: 14
|
||||
};
|
||||
|
||||
// Configure Services - Mix of maintenance and operational
|
||||
const services: IServiceStatus[] = [
|
||||
// Core Services - Some under maintenance
|
||||
{
|
||||
id: 'web-app',
|
||||
name: 'web-app',
|
||||
displayName: 'Web Application',
|
||||
description: 'Customer portal',
|
||||
currentStatus: 'operational',
|
||||
lastChecked: Date.now(),
|
||||
uptime30d: 99.92,
|
||||
uptime90d: 99.89,
|
||||
responseTime: 45,
|
||||
category: 'Frontend',
|
||||
selected: true
|
||||
},
|
||||
{
|
||||
id: 'mobile-api',
|
||||
name: 'mobile-api',
|
||||
displayName: 'Mobile API',
|
||||
description: 'Mobile app backend',
|
||||
currentStatus: 'operational',
|
||||
lastChecked: Date.now(),
|
||||
uptime30d: 99.88,
|
||||
uptime90d: 99.85,
|
||||
responseTime: 62,
|
||||
category: 'Frontend',
|
||||
selected: true
|
||||
},
|
||||
{
|
||||
id: 'auth-service',
|
||||
name: 'auth-service',
|
||||
displayName: 'Authentication Service',
|
||||
description: 'User authentication',
|
||||
currentStatus: 'maintenance',
|
||||
lastChecked: Date.now(),
|
||||
uptime30d: 99.5,
|
||||
uptime90d: 99.7,
|
||||
responseTime: 0,
|
||||
category: 'Core Services',
|
||||
selected: true
|
||||
},
|
||||
{
|
||||
id: 'api-gateway',
|
||||
name: 'api-gateway',
|
||||
displayName: 'API Gateway',
|
||||
description: 'Main API endpoint',
|
||||
currentStatus: 'degraded',
|
||||
lastChecked: Date.now(),
|
||||
uptime30d: 99.2,
|
||||
uptime90d: 99.4,
|
||||
responseTime: 125,
|
||||
category: 'Core Services',
|
||||
selected: true
|
||||
},
|
||||
// Database Services
|
||||
{
|
||||
id: 'primary-db',
|
||||
name: 'primary-db',
|
||||
displayName: 'Primary Database',
|
||||
description: 'Main database cluster',
|
||||
currentStatus: 'maintenance',
|
||||
lastChecked: Date.now(),
|
||||
uptime30d: 99.6,
|
||||
uptime90d: 99.7,
|
||||
responseTime: 0,
|
||||
category: 'Data Storage',
|
||||
selected: true
|
||||
},
|
||||
{
|
||||
id: 'replica-db',
|
||||
name: 'replica-db',
|
||||
displayName: 'Database Replicas',
|
||||
description: 'Read replicas (read-only mode)',
|
||||
currentStatus: 'degraded',
|
||||
lastChecked: Date.now(),
|
||||
uptime30d: 99.7,
|
||||
uptime90d: 99.8,
|
||||
responseTime: 85,
|
||||
category: 'Data Storage',
|
||||
selected: true
|
||||
},
|
||||
{
|
||||
id: 'cache',
|
||||
name: 'cache',
|
||||
displayName: 'Cache Service',
|
||||
description: 'Redis cache layer',
|
||||
currentStatus: 'operational',
|
||||
lastChecked: Date.now(),
|
||||
uptime30d: 99.95,
|
||||
uptime90d: 99.93,
|
||||
responseTime: 8,
|
||||
category: 'Data Storage',
|
||||
selected: false
|
||||
},
|
||||
// Backup and Storage
|
||||
{
|
||||
id: 'backup-service',
|
||||
name: 'backup-service',
|
||||
displayName: 'Backup Service',
|
||||
description: 'Automated backups',
|
||||
currentStatus: 'maintenance',
|
||||
lastChecked: Date.now(),
|
||||
uptime30d: 99.8,
|
||||
uptime90d: 99.85,
|
||||
responseTime: 0,
|
||||
category: 'Operations',
|
||||
selected: true
|
||||
},
|
||||
{
|
||||
id: 'file-storage',
|
||||
name: 'file-storage',
|
||||
displayName: 'File Storage',
|
||||
description: 'Object storage service',
|
||||
currentStatus: 'operational',
|
||||
lastChecked: Date.now(),
|
||||
uptime30d: 99.99,
|
||||
uptime90d: 99.98,
|
||||
responseTime: 42,
|
||||
category: 'Operations',
|
||||
selected: false
|
||||
},
|
||||
// Monitoring and Support
|
||||
{
|
||||
id: 'monitoring',
|
||||
name: 'monitoring',
|
||||
displayName: 'Monitoring System',
|
||||
description: 'System monitoring',
|
||||
currentStatus: 'operational',
|
||||
lastChecked: Date.now(),
|
||||
uptime30d: 99.9,
|
||||
uptime90d: 99.88,
|
||||
responseTime: 32,
|
||||
category: 'Support',
|
||||
selected: true
|
||||
},
|
||||
{
|
||||
id: 'logging',
|
||||
name: 'logging',
|
||||
displayName: 'Logging Service',
|
||||
description: 'Centralized logs',
|
||||
currentStatus: 'maintenance',
|
||||
lastChecked: Date.now(),
|
||||
uptime30d: 99.7,
|
||||
uptime90d: 99.75,
|
||||
responseTime: 0,
|
||||
category: 'Support',
|
||||
selected: false
|
||||
},
|
||||
{
|
||||
id: 'alerting',
|
||||
name: 'alerting',
|
||||
displayName: 'Alerting System',
|
||||
description: 'Alert notifications',
|
||||
currentStatus: 'operational',
|
||||
lastChecked: Date.now(),
|
||||
uptime30d: 99.85,
|
||||
uptime90d: 99.82,
|
||||
responseTime: 28,
|
||||
category: 'Support',
|
||||
selected: false
|
||||
},
|
||||
// Communication Services
|
||||
{
|
||||
id: 'email',
|
||||
name: 'email',
|
||||
displayName: 'Email Service',
|
||||
description: 'Transactional emails',
|
||||
currentStatus: 'operational',
|
||||
lastChecked: Date.now(),
|
||||
uptime30d: 99.6,
|
||||
uptime90d: 99.55,
|
||||
responseTime: 142,
|
||||
category: 'Communication',
|
||||
selected: true
|
||||
},
|
||||
{
|
||||
id: 'webhooks',
|
||||
name: 'webhooks',
|
||||
displayName: 'Webhook Delivery',
|
||||
description: 'Webhook notifications',
|
||||
currentStatus: 'maintenance',
|
||||
lastChecked: Date.now(),
|
||||
uptime30d: 99.4,
|
||||
uptime90d: 99.45,
|
||||
responseTime: 0,
|
||||
category: 'Communication',
|
||||
selected: false
|
||||
}
|
||||
];
|
||||
|
||||
assetsSelector.services = services;
|
||||
|
||||
// Configure Stats Grid - Maintenance mode metrics
|
||||
const operationalCount = services.filter(s => s.currentStatus === 'operational').length;
|
||||
const avgResponseTime = services.reduce((sum, s) => sum + (s.responseTime || 0), 0) / services.length;
|
||||
const avgUptime = services.reduce((sum, s) => sum + (s.uptime30d || 0), 0) / services.length;
|
||||
|
||||
statsGrid.currentStatus = 'maintenance';
|
||||
statsGrid.uptime = avgUptime;
|
||||
statsGrid.avgResponseTime = Math.round(avgResponseTime);
|
||||
statsGrid.totalIncidents = 1; // Just the maintenance
|
||||
statsGrid.affectedServices = services.filter(s => s.currentStatus === 'maintenance').length;
|
||||
statsGrid.totalServices = services.length;
|
||||
statsGrid.timePeriod = '30 days';
|
||||
|
||||
// Configure Status Details - Showing maintenance period
|
||||
const generateStatusDetails = (): IStatusHistoryPoint[] => {
|
||||
const details: IStatusHistoryPoint[] = [];
|
||||
const now = new Date();
|
||||
const maintenanceStarted = 2; // 2 hours ago
|
||||
|
||||
for (let i = 47; i >= 0; i--) {
|
||||
const date = new Date(now);
|
||||
date.setMinutes(0, 0, 0);
|
||||
date.setHours(date.getHours() - i);
|
||||
|
||||
let status: 'operational' | 'degraded' | 'outage' = 'operational';
|
||||
let value = 100;
|
||||
let responseTime = 25;
|
||||
|
||||
// Maintenance window
|
||||
if (i <= maintenanceStarted) {
|
||||
status = 'degraded';
|
||||
value = 60;
|
||||
responseTime = 0;
|
||||
} else if (i <= maintenanceStarted + 2) {
|
||||
// Pre-maintenance prep
|
||||
status = 'degraded';
|
||||
value = 85;
|
||||
responseTime = 80;
|
||||
}
|
||||
|
||||
details.push({
|
||||
timestamp: date.getTime(),
|
||||
status,
|
||||
responseTime
|
||||
});
|
||||
}
|
||||
|
||||
return details;
|
||||
};
|
||||
|
||||
statusDetails.dataPoints = generateStatusDetails();
|
||||
statusDetails.serviceId = 'primary-db';
|
||||
statusDetails.serviceName = 'Primary Database';
|
||||
|
||||
// Configure Monthly Status - Good uptime with scheduled maintenance windows
|
||||
const generateMonthlyData = (): IMonthlyUptime[] => {
|
||||
const months: IMonthlyUptime[] = [];
|
||||
const now = new Date();
|
||||
|
||||
for (let m = 4; m >= 0; m--) {
|
||||
const monthDate = new Date(now.getFullYear(), now.getMonth() - m, 1);
|
||||
const daysInMonth = new Date(monthDate.getFullYear(), monthDate.getMonth() + 1, 0).getDate();
|
||||
const monthKey = monthDate.toISOString().slice(0, 7);
|
||||
|
||||
const days = [];
|
||||
let totalUptime = 0;
|
||||
let incidents = 0;
|
||||
|
||||
for (let d = 1; d <= daysInMonth; d++) {
|
||||
const dayDate = new Date(monthDate.getFullYear(), monthDate.getMonth(), d);
|
||||
const isToday = dayDate.toDateString() === now.toDateString();
|
||||
const isFuture = dayDate > now;
|
||||
const isFirstSunday = dayDate.getDay() === 0 && d <= 7;
|
||||
|
||||
if (isFuture) continue;
|
||||
|
||||
let status: 'operational' | 'degraded' | 'partial_outage' | 'major_outage' = 'operational';
|
||||
let uptime = 100;
|
||||
let dayIncidents = 0;
|
||||
|
||||
// Today - maintenance
|
||||
if (isToday) {
|
||||
status = 'degraded';
|
||||
uptime = 92; // 2 hours of maintenance = 8% downtime
|
||||
dayIncidents = 0; // Maintenance is not an incident
|
||||
} else if (isFirstSunday) {
|
||||
// Monthly maintenance on first Sunday
|
||||
status = 'degraded';
|
||||
uptime = 96; // 1 hour maintenance
|
||||
dayIncidents = 0;
|
||||
} else if (Math.random() > 0.95) {
|
||||
// Occasional issues
|
||||
status = 'degraded';
|
||||
uptime = 98 + Math.random() * 2;
|
||||
dayIncidents = 1;
|
||||
}
|
||||
|
||||
days.push({
|
||||
date: dayDate.toISOString().slice(0, 10),
|
||||
status,
|
||||
uptime,
|
||||
incidents: dayIncidents,
|
||||
totalDowntime: Math.round((100 - uptime) * 14.4)
|
||||
});
|
||||
|
||||
totalUptime += uptime;
|
||||
incidents += dayIncidents;
|
||||
}
|
||||
|
||||
months.push({
|
||||
month: monthKey,
|
||||
days,
|
||||
overallUptime: totalUptime / days.length,
|
||||
totalIncidents: incidents
|
||||
});
|
||||
}
|
||||
|
||||
return months;
|
||||
};
|
||||
|
||||
statusMonth.monthlyData = generateMonthlyData();
|
||||
statusMonth.serviceId = 'all-services';
|
||||
statusMonth.serviceName = 'All Services';
|
||||
|
||||
// Configure Incidents - Current maintenance
|
||||
const currentIncidents: IIncidentDetails[] = [
|
||||
{
|
||||
id: 'maint-2024-001',
|
||||
title: 'Scheduled Database Maintenance',
|
||||
impact: 'Database will be in read-only mode. Some features may be temporarily unavailable.',
|
||||
severity: 'minor',
|
||||
status: 'monitoring',
|
||||
startTime: Date.now() - 2 * 60 * 60 * 1000,
|
||||
affectedServices: ['Primary Database', 'Authentication Service', 'Backup Service', 'Logging Service', 'Webhook Delivery'],
|
||||
updates: [
|
||||
{
|
||||
id: 'update-1',
|
||||
timestamp: Date.now() - 7 * 24 * 60 * 60 * 1000,
|
||||
status: 'investigating',
|
||||
message: 'Scheduled maintenance has been announced for database upgrades and security patches.',
|
||||
author: 'Operations Team'
|
||||
},
|
||||
{
|
||||
id: 'update-2',
|
||||
timestamp: Date.now() - 24 * 60 * 60 * 1000,
|
||||
status: 'investigating',
|
||||
message: 'Reminder: Database maintenance scheduled for tomorrow at 12:00 AM UTC.',
|
||||
author: 'Operations Team'
|
||||
},
|
||||
{
|
||||
id: 'update-3',
|
||||
timestamp: Date.now() - 2 * 60 * 60 * 1000,
|
||||
status: 'monitoring',
|
||||
message: 'Maintenance window has begun. Database is now in read-only mode.',
|
||||
author: 'Database Team'
|
||||
},
|
||||
{
|
||||
id: 'update-4',
|
||||
timestamp: Date.now() - 1.5 * 60 * 60 * 1000,
|
||||
status: 'monitoring',
|
||||
message: 'Security patches applied successfully. Beginning database schema updates.',
|
||||
author: 'Database Team'
|
||||
},
|
||||
{
|
||||
id: 'update-5',
|
||||
timestamp: Date.now() - 1 * 60 * 60 * 1000,
|
||||
status: 'monitoring',
|
||||
message: 'Schema updates 50% complete. Everything proceeding as planned.',
|
||||
author: 'Database Team'
|
||||
},
|
||||
{
|
||||
id: 'update-6',
|
||||
timestamp: Date.now() - 30 * 60 * 1000,
|
||||
status: 'monitoring',
|
||||
message: 'Final testing phase. Services will begin coming back online shortly.',
|
||||
author: 'Operations Team'
|
||||
}
|
||||
]
|
||||
}
|
||||
];
|
||||
|
||||
const pastIncidents: IIncidentDetails[] = [
|
||||
{
|
||||
id: 'maint-2024-prev-001',
|
||||
title: 'Previous Monthly Maintenance',
|
||||
impact: 'Services were in maintenance mode for 1 hour',
|
||||
severity: 'minor',
|
||||
status: 'resolved',
|
||||
startTime: Date.now() - 30 * 24 * 60 * 60 * 1000,
|
||||
endTime: Date.now() - 30 * 24 * 60 * 60 * 1000 + 60 * 60 * 1000,
|
||||
affectedServices: ['Primary Database', 'Backup Service'],
|
||||
rootCause: 'Scheduled monthly maintenance for security updates.',
|
||||
resolution: 'Maintenance completed successfully with all updates applied.',
|
||||
updates: [
|
||||
{
|
||||
id: 'update-7',
|
||||
timestamp: Date.now() - 30 * 24 * 60 * 60 * 1000,
|
||||
status: 'monitoring',
|
||||
message: 'Monthly maintenance started.',
|
||||
author: 'Operations Team'
|
||||
},
|
||||
{
|
||||
id: 'update-8',
|
||||
timestamp: Date.now() - 30 * 24 * 60 * 60 * 1000 + 60 * 60 * 1000,
|
||||
status: 'resolved',
|
||||
message: 'Maintenance completed successfully.',
|
||||
author: 'Operations Team'
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
id: 'inc-2024-001',
|
||||
title: 'API Gateway Performance Issues',
|
||||
impact: 'Slow API responses for some users',
|
||||
severity: 'minor',
|
||||
status: 'resolved',
|
||||
startTime: Date.now() - 15 * 24 * 60 * 60 * 1000,
|
||||
endTime: Date.now() - 15 * 24 * 60 * 60 * 1000 + 45 * 60 * 1000,
|
||||
affectedServices: ['API Gateway'],
|
||||
rootCause: 'Memory leak in API Gateway service.',
|
||||
resolution: 'Service restarted and patch applied.',
|
||||
updates: [
|
||||
{
|
||||
id: 'update-9',
|
||||
timestamp: Date.now() - 15 * 24 * 60 * 60 * 1000,
|
||||
status: 'investigating',
|
||||
message: 'Investigating reports of slow API responses.',
|
||||
author: 'API Team'
|
||||
},
|
||||
{
|
||||
id: 'update-10',
|
||||
timestamp: Date.now() - 15 * 24 * 60 * 60 * 1000 + 30 * 60 * 1000,
|
||||
status: 'identified',
|
||||
message: 'Memory leak identified. Preparing fix.',
|
||||
author: 'API Team'
|
||||
},
|
||||
{
|
||||
id: 'update-11',
|
||||
timestamp: Date.now() - 15 * 24 * 60 * 60 * 1000 + 45 * 60 * 1000,
|
||||
status: 'resolved',
|
||||
message: 'Fix applied. Performance back to normal.',
|
||||
author: 'API Team'
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
id: 'maint-2023-012',
|
||||
title: 'Year-End Infrastructure Upgrade',
|
||||
impact: 'Planned downtime for major infrastructure improvements',
|
||||
severity: 'major',
|
||||
status: 'resolved',
|
||||
startTime: Date.now() - 60 * 24 * 60 * 60 * 1000,
|
||||
endTime: Date.now() - 60 * 24 * 60 * 60 * 1000 + 4 * 60 * 60 * 1000,
|
||||
affectedServices: ['All Services'],
|
||||
rootCause: 'Annual infrastructure upgrade and capacity expansion.',
|
||||
resolution: 'All upgrades completed successfully. System capacity increased by 50%.',
|
||||
updates: [
|
||||
{
|
||||
id: 'update-12',
|
||||
timestamp: Date.now() - 67 * 24 * 60 * 60 * 1000,
|
||||
status: 'investigating',
|
||||
message: 'Year-end maintenance scheduled for next week.',
|
||||
author: 'CTO'
|
||||
},
|
||||
{
|
||||
id: 'update-13',
|
||||
timestamp: Date.now() - 60 * 24 * 60 * 60 * 1000,
|
||||
status: 'monitoring',
|
||||
message: 'Maintenance window started. All services going offline.',
|
||||
author: 'Operations Team'
|
||||
},
|
||||
{
|
||||
id: 'update-14',
|
||||
timestamp: Date.now() - 60 * 24 * 60 * 60 * 1000 + 2 * 60 * 60 * 1000,
|
||||
status: 'monitoring',
|
||||
message: 'Hardware upgrades complete. Software updates in progress.',
|
||||
author: 'Infrastructure Team'
|
||||
},
|
||||
{
|
||||
id: 'update-15',
|
||||
timestamp: Date.now() - 60 * 24 * 60 * 60 * 1000 + 4 * 60 * 60 * 1000,
|
||||
status: 'resolved',
|
||||
message: 'All systems back online. Performance improvements confirmed.',
|
||||
author: 'Operations Team'
|
||||
},
|
||||
{
|
||||
id: 'update-16',
|
||||
timestamp: Date.now() - 59 * 24 * 60 * 60 * 1000,
|
||||
status: 'postmortem',
|
||||
message: 'Maintenance report published. 50% capacity increase achieved.',
|
||||
author: 'Engineering Team'
|
||||
}
|
||||
]
|
||||
}
|
||||
];
|
||||
|
||||
incidents.currentIncidents = currentIncidents;
|
||||
incidents.pastIncidents = pastIncidents;
|
||||
|
||||
// Configure Footer
|
||||
footer.companyName = 'SecureVault Infrastructure';
|
||||
footer.legalUrl = 'https://securevault.example.com/legal';
|
||||
footer.supportEmail = 'support@securevault.example.com';
|
||||
footer.statusPageUrl = 'https://status.securevault.example.com';
|
||||
footer.lastUpdated = Date.now();
|
||||
footer.currentYear = new Date().getFullYear();
|
||||
footer.socialLinks = [
|
||||
{ platform: 'twitter', url: 'https://twitter.com/securevault' },
|
||||
{ platform: 'linkedin', url: 'https://linkedin.com/company/securevault' }
|
||||
];
|
||||
footer.rssFeedUrl = 'https://status.securevault.example.com/rss';
|
||||
footer.apiStatusUrl = 'https://api.securevault.example.com/v1/status';
|
||||
footer.enableSubscribe = true;
|
||||
footer.subscriberCount = 3892;
|
||||
footer.additionalLinks = [
|
||||
{ label: 'Maintenance Schedule', url: 'https://securevault.example.com/maintenance' },
|
||||
{ label: 'API Documentation', url: 'https://docs.securevault.example.com' },
|
||||
{ label: 'Support Portal', url: 'https://support.securevault.example.com' }
|
||||
];
|
||||
footer.latestStatusUpdate = 'Database maintenance 50% complete - On track for 2:00 AM UTC completion';
|
||||
}}
|
||||
>
|
||||
<upl-statuspage-header></upl-statuspage-header>
|
||||
<upl-statuspage-statusbar></upl-statuspage-statusbar>
|
||||
<upl-statuspage-statsgrid></upl-statuspage-statsgrid>
|
||||
<upl-statuspage-assetsselector></upl-statuspage-assetsselector>
|
||||
<upl-statuspage-statusdetails></upl-statuspage-statusdetails>
|
||||
<upl-statuspage-statusmonth></upl-statuspage-statusmonth>
|
||||
<upl-statuspage-incidents></upl-statuspage-incidents>
|
||||
<upl-statuspage-footer></upl-statuspage-footer>
|
||||
</dees-demowrapper>
|
||||
</div>
|
||||
`;
|
||||
568
ts_web/pages/statuspage-outage.ts
Normal file
568
ts_web/pages/statuspage-outage.ts
Normal file
@@ -0,0 +1,568 @@
|
||||
import { html, cssManager } from "@design.estate/dees-element";
|
||||
import type { IServiceStatus, IOverallStatus, IIncidentDetails, IStatusHistoryPoint, IMonthlyUptime } from '../interfaces/index.js';
|
||||
|
||||
export const statuspageOutage = () => html`
|
||||
<style>
|
||||
.demo-page-wrapper {
|
||||
min-height: 100vh;
|
||||
background: ${cssManager.bdTheme('#fafafa', '#0a0a0a')};
|
||||
}
|
||||
.demo-page-wrapper > dees-demowrapper {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 24px;
|
||||
}
|
||||
</style>
|
||||
|
||||
<div class="demo-page-wrapper">
|
||||
|
||||
<dees-demowrapper
|
||||
.runAfterRender=${async (wrapperElement: any) => {
|
||||
const header = wrapperElement.querySelector('upl-statuspage-header') as any;
|
||||
const statusBar = wrapperElement.querySelector('upl-statuspage-statusbar') as any;
|
||||
const statsGrid = wrapperElement.querySelector('upl-statuspage-statsgrid') as any;
|
||||
const assetsSelector = wrapperElement.querySelector('upl-statuspage-assetsselector') as any;
|
||||
const statusDetails = wrapperElement.querySelector('upl-statuspage-statusdetails') as any;
|
||||
const statusMonth = wrapperElement.querySelector('upl-statuspage-statusmonth') as any;
|
||||
const incidents = wrapperElement.querySelector('upl-statuspage-incidents') as any;
|
||||
const footer = wrapperElement.querySelector('upl-statuspage-footer') as any;
|
||||
|
||||
// Configure Header
|
||||
header.pageTitle = 'DataStream';
|
||||
header.showReportButton = true;
|
||||
header.showSubscribeButton = true;
|
||||
header.logoUrl = 'https://via.placeholder.com/150x50/F44336/ffffff?text=DS';
|
||||
|
||||
// Configure Overall Status - Major Outage
|
||||
statusBar.overallStatus = {
|
||||
status: 'major_outage',
|
||||
message: 'Major service disruption affecting multiple regions',
|
||||
lastUpdated: Date.now(),
|
||||
affectedServices: 8,
|
||||
totalServices: 15
|
||||
};
|
||||
|
||||
// Configure Services - Multiple Outages
|
||||
const services: IServiceStatus[] = [
|
||||
// Core Services - Major Outage
|
||||
{
|
||||
id: 'api-gateway',
|
||||
name: 'api-gateway',
|
||||
displayName: 'API Gateway',
|
||||
description: 'Main API endpoint',
|
||||
currentStatus: 'major_outage',
|
||||
lastChecked: Date.now(),
|
||||
uptime30d: 85.2,
|
||||
uptime90d: 92.5,
|
||||
responseTime: 2500,
|
||||
category: 'Core Infrastructure',
|
||||
selected: true
|
||||
},
|
||||
{
|
||||
id: 'auth-service',
|
||||
name: 'auth-service',
|
||||
displayName: 'Authentication',
|
||||
description: 'Login and authorization',
|
||||
currentStatus: 'major_outage',
|
||||
lastChecked: Date.now(),
|
||||
uptime30d: 84.8,
|
||||
uptime90d: 91.2,
|
||||
responseTime: 3200,
|
||||
category: 'Core Infrastructure',
|
||||
selected: true
|
||||
},
|
||||
{
|
||||
id: 'web-portal',
|
||||
name: 'web-portal',
|
||||
displayName: 'Web Portal',
|
||||
description: 'Customer web interface',
|
||||
currentStatus: 'partial_outage',
|
||||
lastChecked: Date.now(),
|
||||
uptime30d: 88.5,
|
||||
uptime90d: 94.2,
|
||||
responseTime: 1800,
|
||||
category: 'Frontend',
|
||||
selected: true
|
||||
},
|
||||
// Database Services - Critical
|
||||
{
|
||||
id: 'primary-db',
|
||||
name: 'primary-db',
|
||||
displayName: 'Primary Database',
|
||||
description: 'Main data storage',
|
||||
currentStatus: 'major_outage',
|
||||
lastChecked: Date.now(),
|
||||
uptime30d: 82.1,
|
||||
uptime90d: 90.5,
|
||||
responseTime: 5000,
|
||||
category: 'Data Layer',
|
||||
selected: true
|
||||
},
|
||||
{
|
||||
id: 'replica-db',
|
||||
name: 'replica-db',
|
||||
displayName: 'Database Replicas',
|
||||
description: 'Read replicas',
|
||||
currentStatus: 'partial_outage',
|
||||
lastChecked: Date.now(),
|
||||
uptime30d: 86.7,
|
||||
uptime90d: 93.1,
|
||||
responseTime: 2200,
|
||||
category: 'Data Layer',
|
||||
selected: true
|
||||
},
|
||||
{
|
||||
id: 'cache-layer',
|
||||
name: 'cache-layer',
|
||||
displayName: 'Cache Layer',
|
||||
description: 'Redis cache',
|
||||
currentStatus: 'degraded',
|
||||
lastChecked: Date.now(),
|
||||
uptime30d: 92.3,
|
||||
uptime90d: 95.8,
|
||||
responseTime: 450,
|
||||
category: 'Data Layer',
|
||||
selected: true
|
||||
},
|
||||
// Regional Services
|
||||
{
|
||||
id: 'us-east',
|
||||
name: 'us-east',
|
||||
displayName: 'US East Region',
|
||||
description: 'Primary US datacenter',
|
||||
currentStatus: 'major_outage',
|
||||
lastChecked: Date.now(),
|
||||
uptime30d: 81.5,
|
||||
uptime90d: 89.2,
|
||||
responseTime: 4500,
|
||||
category: 'Regional',
|
||||
selected: true
|
||||
},
|
||||
{
|
||||
id: 'us-west',
|
||||
name: 'us-west',
|
||||
displayName: 'US West Region',
|
||||
description: 'Secondary US datacenter',
|
||||
currentStatus: 'degraded',
|
||||
lastChecked: Date.now(),
|
||||
uptime30d: 94.2,
|
||||
uptime90d: 96.8,
|
||||
responseTime: 180,
|
||||
category: 'Regional',
|
||||
selected: true
|
||||
},
|
||||
{
|
||||
id: 'eu-central',
|
||||
name: 'eu-central',
|
||||
displayName: 'EU Central Region',
|
||||
description: 'European datacenter',
|
||||
currentStatus: 'major_outage',
|
||||
lastChecked: Date.now(),
|
||||
uptime30d: 83.7,
|
||||
uptime90d: 91.2,
|
||||
responseTime: 3800,
|
||||
category: 'Regional',
|
||||
selected: true
|
||||
},
|
||||
// Some operational services
|
||||
{
|
||||
id: 'status-page',
|
||||
name: 'status-page',
|
||||
displayName: 'Status Page',
|
||||
description: 'This status page',
|
||||
currentStatus: 'operational',
|
||||
lastChecked: Date.now(),
|
||||
uptime30d: 99.9,
|
||||
uptime90d: 99.8,
|
||||
responseTime: 45,
|
||||
category: 'Support',
|
||||
selected: false
|
||||
},
|
||||
{
|
||||
id: 'support-portal',
|
||||
name: 'support-portal',
|
||||
displayName: 'Support Portal',
|
||||
description: 'Customer support system',
|
||||
currentStatus: 'operational',
|
||||
lastChecked: Date.now(),
|
||||
uptime30d: 98.5,
|
||||
uptime90d: 99.1,
|
||||
responseTime: 120,
|
||||
category: 'Support',
|
||||
selected: false
|
||||
},
|
||||
// Degraded services
|
||||
{
|
||||
id: 'email-service',
|
||||
name: 'email-service',
|
||||
displayName: 'Email Notifications',
|
||||
description: 'Automated emails',
|
||||
currentStatus: 'degraded',
|
||||
lastChecked: Date.now(),
|
||||
uptime30d: 91.2,
|
||||
uptime90d: 94.5,
|
||||
responseTime: 850,
|
||||
category: 'Communication',
|
||||
selected: true
|
||||
},
|
||||
{
|
||||
id: 'analytics',
|
||||
name: 'analytics',
|
||||
displayName: 'Analytics Platform',
|
||||
description: 'Usage analytics',
|
||||
currentStatus: 'partial_outage',
|
||||
lastChecked: Date.now(),
|
||||
uptime30d: 87.3,
|
||||
uptime90d: 92.8,
|
||||
responseTime: 1200,
|
||||
category: 'Services',
|
||||
selected: false
|
||||
},
|
||||
{
|
||||
id: 'backup-system',
|
||||
name: 'backup-system',
|
||||
displayName: 'Backup System',
|
||||
description: 'Data backups',
|
||||
currentStatus: 'degraded',
|
||||
lastChecked: Date.now(),
|
||||
uptime30d: 95.2,
|
||||
uptime90d: 97.1,
|
||||
responseTime: 320,
|
||||
category: 'Services',
|
||||
selected: false
|
||||
},
|
||||
{
|
||||
id: 'monitoring',
|
||||
name: 'monitoring',
|
||||
displayName: 'Monitoring System',
|
||||
description: 'Infrastructure monitoring',
|
||||
currentStatus: 'operational',
|
||||
lastChecked: Date.now(),
|
||||
uptime30d: 98.7,
|
||||
uptime90d: 99.2,
|
||||
responseTime: 65,
|
||||
category: 'Services',
|
||||
selected: true
|
||||
}
|
||||
];
|
||||
|
||||
assetsSelector.services = services;
|
||||
|
||||
// Configure Stats Grid - Major outage metrics
|
||||
const operationalCount = services.filter(s => s.currentStatus === 'operational').length;
|
||||
const avgResponseTime = services.reduce((sum, s) => sum + (s.responseTime || 0), 0) / services.length;
|
||||
const avgUptime = services.reduce((sum, s) => sum + (s.uptime30d || 0), 0) / services.length;
|
||||
|
||||
statsGrid.currentStatus = 'major_outage';
|
||||
statsGrid.uptime = avgUptime;
|
||||
statsGrid.avgResponseTime = Math.round(avgResponseTime);
|
||||
statsGrid.totalIncidents = 15; // High number of incidents
|
||||
statsGrid.affectedServices = services.filter(s => s.currentStatus !== 'operational').length;
|
||||
statsGrid.totalServices = services.length;
|
||||
statsGrid.timePeriod = '30 days';
|
||||
|
||||
// Configure Status Details - Showing the outage timeline
|
||||
const generateStatusDetails = (): IStatusHistoryPoint[] => {
|
||||
const details: IStatusHistoryPoint[] = [];
|
||||
const now = new Date();
|
||||
|
||||
for (let i = 47; i >= 0; i--) {
|
||||
const date = new Date(now);
|
||||
date.setMinutes(0, 0, 0);
|
||||
date.setHours(date.getHours() - i);
|
||||
|
||||
let status: 'operational' | 'degraded' | 'major_outage' = 'operational';
|
||||
let value = 100;
|
||||
let responseTime = 30;
|
||||
|
||||
// Outage started 8 hours ago
|
||||
if (i <= 8) {
|
||||
status = 'major_outage';
|
||||
value = 15 + Math.random() * 20;
|
||||
responseTime = 2000 + Math.random() * 3000;
|
||||
} else if (i <= 12) {
|
||||
// Degradation before the outage
|
||||
status = 'degraded';
|
||||
value = 70 + Math.random() * 20;
|
||||
responseTime = 200 + Math.random() * 800;
|
||||
} else if (i <= 24) {
|
||||
// Some issues yesterday
|
||||
if (Math.random() > 0.7) {
|
||||
status = 'degraded';
|
||||
value = 85 + Math.random() * 10;
|
||||
responseTime = 100 + Math.random() * 200;
|
||||
}
|
||||
}
|
||||
|
||||
details.push({
|
||||
timestamp: date.getTime(),
|
||||
status,
|
||||
responseTime
|
||||
});
|
||||
}
|
||||
|
||||
return details;
|
||||
};
|
||||
|
||||
statusDetails.dataPoints = generateStatusDetails();
|
||||
statusDetails.serviceId = 'primary-db';
|
||||
statusDetails.serviceName = 'Primary Database';
|
||||
|
||||
// Configure Monthly Status - Poor performance
|
||||
const generateMonthlyData = (): IMonthlyUptime[] => {
|
||||
const months: IMonthlyUptime[] = [];
|
||||
const now = new Date();
|
||||
|
||||
for (let m = 4; m >= 0; m--) {
|
||||
const monthDate = new Date(now.getFullYear(), now.getMonth() - m, 1);
|
||||
const daysInMonth = new Date(monthDate.getFullYear(), monthDate.getMonth() + 1, 0).getDate();
|
||||
const monthKey = monthDate.toISOString().slice(0, 7);
|
||||
|
||||
const days = [];
|
||||
let totalUptime = 0;
|
||||
let incidents = 0;
|
||||
|
||||
for (let d = 1; d <= daysInMonth; d++) {
|
||||
const dayDate = new Date(monthDate.getFullYear(), monthDate.getMonth(), d);
|
||||
const isToday = dayDate.toDateString() === now.toDateString();
|
||||
const isFuture = dayDate > now;
|
||||
|
||||
if (isFuture) continue;
|
||||
|
||||
let status: 'operational' | 'degraded' | 'partial_outage' | 'major_outage' = 'operational';
|
||||
let uptime = 100;
|
||||
let dayIncidents = 0;
|
||||
|
||||
// Today - major outage
|
||||
if (isToday) {
|
||||
status = 'major_outage';
|
||||
uptime = 45;
|
||||
dayIncidents = 3;
|
||||
} else if (Math.random() > 0.7) {
|
||||
// Frequent issues
|
||||
if (Math.random() > 0.5) {
|
||||
status = 'partial_outage';
|
||||
uptime = 75 + Math.random() * 15;
|
||||
dayIncidents = 2;
|
||||
} else {
|
||||
status = 'degraded';
|
||||
uptime = 85 + Math.random() * 10;
|
||||
dayIncidents = 1;
|
||||
}
|
||||
}
|
||||
|
||||
days.push({
|
||||
date: dayDate.toISOString().slice(0, 10),
|
||||
status,
|
||||
uptime,
|
||||
incidents: dayIncidents,
|
||||
totalDowntime: Math.round((100 - uptime) * 14.4)
|
||||
});
|
||||
|
||||
totalUptime += uptime;
|
||||
incidents += dayIncidents;
|
||||
}
|
||||
|
||||
months.push({
|
||||
month: monthKey,
|
||||
days,
|
||||
overallUptime: totalUptime / days.length,
|
||||
totalIncidents: incidents
|
||||
});
|
||||
}
|
||||
|
||||
return months;
|
||||
};
|
||||
|
||||
statusMonth.monthlyData = generateMonthlyData();
|
||||
statusMonth.serviceId = 'all-services';
|
||||
statusMonth.serviceName = 'All Services';
|
||||
|
||||
// Configure Incidents - Multiple current incidents
|
||||
const currentIncidents: IIncidentDetails[] = [
|
||||
{
|
||||
id: 'inc-2024-crit-001',
|
||||
title: 'Complete Database Failure in Multiple Regions',
|
||||
impact: 'Users unable to access any services. Complete system outage.',
|
||||
severity: 'critical',
|
||||
status: 'investigating',
|
||||
startTime: Date.now() - 8 * 60 * 60 * 1000,
|
||||
affectedServices: ['Primary Database', 'Database Replicas', 'API Gateway', 'Authentication', 'US East Region', 'EU Central Region'],
|
||||
updates: [
|
||||
{
|
||||
id: 'update-1',
|
||||
timestamp: Date.now() - 8 * 60 * 60 * 1000,
|
||||
status: 'investigating',
|
||||
message: 'Multiple alerts triggered. Complete service failure detected across regions.',
|
||||
author: 'Automated Monitoring'
|
||||
},
|
||||
{
|
||||
id: 'update-2',
|
||||
timestamp: Date.now() - 7.5 * 60 * 60 * 1000,
|
||||
status: 'investigating',
|
||||
message: 'All hands on deck. Engineering teams mobilized. Initial investigation points to cascading database failure.',
|
||||
author: 'Incident Commander'
|
||||
},
|
||||
{
|
||||
id: 'update-3',
|
||||
timestamp: Date.now() - 6 * 60 * 60 * 1000,
|
||||
status: 'identified',
|
||||
message: 'Root cause identified: Corrupted replication logs causing database cluster failures. Working on recovery.',
|
||||
author: 'Database Team'
|
||||
},
|
||||
{
|
||||
id: 'update-4',
|
||||
timestamp: Date.now() - 4 * 60 * 60 * 1000,
|
||||
status: 'monitoring',
|
||||
message: 'Attempting to restore from backups. This is a complex operation and will take time.',
|
||||
author: 'Infrastructure Team'
|
||||
},
|
||||
{
|
||||
id: 'update-5',
|
||||
timestamp: Date.now() - 2 * 60 * 60 * 1000,
|
||||
status: 'monitoring',
|
||||
message: 'Partial recovery in progress. Some read-only operations may become available soon.',
|
||||
author: 'Database Team'
|
||||
},
|
||||
{
|
||||
id: 'update-6',
|
||||
timestamp: Date.now() - 30 * 60 * 1000,
|
||||
status: 'monitoring',
|
||||
message: 'Critical update: Recovery is taking longer than expected. We are exploring all options including failover to disaster recovery site.',
|
||||
author: 'CTO'
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
id: 'inc-2024-maj-002',
|
||||
title: 'API Gateway Overload',
|
||||
impact: 'API requests failing or timing out',
|
||||
severity: 'major',
|
||||
status: 'monitoring',
|
||||
startTime: Date.now() - 6 * 60 * 60 * 1000,
|
||||
affectedServices: ['API Gateway', 'Web Portal'],
|
||||
updates: [
|
||||
{
|
||||
id: 'update-7',
|
||||
timestamp: Date.now() - 6 * 60 * 60 * 1000,
|
||||
status: 'investigating',
|
||||
message: 'API Gateway experiencing extreme load due to retry storms from database failures.',
|
||||
author: 'API Team'
|
||||
},
|
||||
{
|
||||
id: 'update-8',
|
||||
timestamp: Date.now() - 5 * 60 * 60 * 1000,
|
||||
status: 'identified',
|
||||
message: 'Implementing rate limiting and circuit breakers to prevent cascade failures.',
|
||||
author: 'API Team'
|
||||
},
|
||||
{
|
||||
id: 'update-9',
|
||||
timestamp: Date.now() - 3 * 60 * 60 * 1000,
|
||||
status: 'monitoring',
|
||||
message: 'Rate limiting in place. Load is stabilizing but service remains degraded.',
|
||||
author: 'API Team'
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
id: 'inc-2024-maj-003',
|
||||
title: 'Email Service Delays',
|
||||
impact: 'Notification emails delayed by up to 2 hours',
|
||||
severity: 'major',
|
||||
status: 'monitoring',
|
||||
startTime: Date.now() - 5 * 60 * 60 * 1000,
|
||||
affectedServices: ['Email Notifications'],
|
||||
updates: [
|
||||
{
|
||||
id: 'update-10',
|
||||
timestamp: Date.now() - 5 * 60 * 60 * 1000,
|
||||
status: 'investigating',
|
||||
message: 'Email queue backing up due to system outages.',
|
||||
author: 'Communications Team'
|
||||
},
|
||||
{
|
||||
id: 'update-11',
|
||||
timestamp: Date.now() - 3 * 60 * 60 * 1000,
|
||||
status: 'monitoring',
|
||||
message: 'Processing backlog slowly. Prioritizing critical notifications.',
|
||||
author: 'Communications Team'
|
||||
}
|
||||
]
|
||||
}
|
||||
];
|
||||
|
||||
const pastIncidents: IIncidentDetails[] = [
|
||||
{
|
||||
id: 'inc-2024-prev-001',
|
||||
title: 'Previous Major Outage',
|
||||
impact: 'Services unavailable for 4 hours',
|
||||
severity: 'critical',
|
||||
status: 'resolved',
|
||||
startTime: Date.now() - 30 * 24 * 60 * 60 * 1000,
|
||||
endTime: Date.now() - 30 * 24 * 60 * 60 * 1000 + 4 * 60 * 60 * 1000,
|
||||
affectedServices: ['All Services'],
|
||||
rootCause: 'Power failure at primary datacenter with UPS failure.',
|
||||
resolution: 'Power restored. UPS systems replaced and tested.',
|
||||
updates: [
|
||||
{
|
||||
id: 'update-12',
|
||||
timestamp: Date.now() - 30 * 24 * 60 * 60 * 1000,
|
||||
status: 'investigating',
|
||||
message: 'Complete datacenter power loss.',
|
||||
author: 'Operations'
|
||||
},
|
||||
{
|
||||
id: 'update-13',
|
||||
timestamp: Date.now() - 30 * 24 * 60 * 60 * 1000 + 4 * 60 * 60 * 1000,
|
||||
status: 'resolved',
|
||||
message: 'Power restored. Services coming back online.',
|
||||
author: 'Operations'
|
||||
},
|
||||
{
|
||||
id: 'update-14',
|
||||
timestamp: Date.now() - 29 * 24 * 60 * 60 * 1000,
|
||||
status: 'postmortem',
|
||||
message: 'Postmortem published. Multiple redundancy improvements implemented.',
|
||||
author: 'Engineering'
|
||||
}
|
||||
]
|
||||
}
|
||||
];
|
||||
|
||||
incidents.currentIncidents = currentIncidents;
|
||||
incidents.pastIncidents = pastIncidents;
|
||||
|
||||
// Configure Footer
|
||||
footer.companyName = 'DataStream Platform';
|
||||
footer.legalUrl = 'https://datastream.example.com/legal';
|
||||
footer.supportEmail = 'emergency@datastream.example.com';
|
||||
footer.statusPageUrl = 'https://status.datastream.example.com';
|
||||
footer.lastUpdated = Date.now();
|
||||
footer.currentYear = new Date().getFullYear();
|
||||
footer.socialLinks = [
|
||||
{ platform: 'twitter', url: 'https://twitter.com/datastream' }
|
||||
];
|
||||
footer.rssFeedUrl = 'https://status.datastream.example.com/rss';
|
||||
footer.apiStatusUrl = 'https://api.datastream.example.com/v1/status';
|
||||
footer.enableSubscribe = true;
|
||||
footer.enableReportIssue = true;
|
||||
footer.subscriberCount = 15234;
|
||||
footer.errorMessage = 'Critical: Multiple services experiencing major outages';
|
||||
footer.additionalLinks = [
|
||||
{ label: 'Emergency Support', url: 'tel:+1-800-HELP-NOW' },
|
||||
{ label: 'Incident Updates', url: 'https://datastream.example.com/incidents' }
|
||||
];
|
||||
}}
|
||||
>
|
||||
<upl-statuspage-header></upl-statuspage-header>
|
||||
<upl-statuspage-statusbar></upl-statuspage-statusbar>
|
||||
<upl-statuspage-statsgrid></upl-statuspage-statsgrid>
|
||||
<upl-statuspage-assetsselector></upl-statuspage-assetsselector>
|
||||
<upl-statuspage-statusdetails></upl-statuspage-statusdetails>
|
||||
<upl-statuspage-statusmonth></upl-statuspage-statusmonth>
|
||||
<upl-statuspage-incidents></upl-statuspage-incidents>
|
||||
<upl-statuspage-footer></upl-statuspage-footer>
|
||||
</dees-demowrapper>
|
||||
</div>
|
||||
`;
|
||||
@@ -1,5 +1,5 @@
|
||||
import * as domtools from '@designestate/dees-domtools';
|
||||
import * as uplInterfaces from '@uptimelink/interfaces';
|
||||
import * as domtools from '@design.estate/dees-domtools';
|
||||
import * as uplInterfaces from '@uptime.link/interfaces';
|
||||
|
||||
export {
|
||||
domtools,
|
||||
|
||||
531
ts_web/styles/shared.styles.ts
Normal file
531
ts_web/styles/shared.styles.ts
Normal file
@@ -0,0 +1,531 @@
|
||||
import { css, cssManager, unsafeCSS } from '@design.estate/dees-element';
|
||||
|
||||
export const fonts = {
|
||||
base: `'Geist Sans', -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen", "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue", sans-serif`,
|
||||
mono: `'Geist Mono', ui-monospace, SFMono-Regular, "SF Mono", Consolas, "Liberation Mono", Menlo, monospace`
|
||||
};
|
||||
|
||||
export const colors = {
|
||||
// Background colors
|
||||
background: {
|
||||
primary: cssManager.bdTheme('#ffffff', '#09090b'),
|
||||
secondary: cssManager.bdTheme('#fafafa', '#18181b'),
|
||||
muted: cssManager.bdTheme('#f4f4f5', '#27272a'),
|
||||
card: cssManager.bdTheme('#ffffff', '#0f0f12'),
|
||||
elevated: cssManager.bdTheme('#ffffff', '#1a1a1e')
|
||||
},
|
||||
|
||||
// Border colors
|
||||
border: {
|
||||
default: cssManager.bdTheme('#e4e4e7', '#27272a'),
|
||||
muted: cssManager.bdTheme('#f4f4f5', '#3f3f46'),
|
||||
subtle: cssManager.bdTheme('#f0f0f2', '#1f1f23')
|
||||
},
|
||||
|
||||
// Text colors
|
||||
text: {
|
||||
primary: cssManager.bdTheme('#09090b', '#fafafa'),
|
||||
secondary: cssManager.bdTheme('#71717a', '#a1a1aa'),
|
||||
muted: cssManager.bdTheme('#a1a1aa', '#71717a')
|
||||
},
|
||||
|
||||
// Status colors - vibrant and accessible
|
||||
status: {
|
||||
operational: cssManager.bdTheme('#16a34a', '#22c55e'),
|
||||
degraded: cssManager.bdTheme('#d97706', '#fbbf24'),
|
||||
partial: cssManager.bdTheme('#dc2626', '#f87171'),
|
||||
major: cssManager.bdTheme('#b91c1c', '#ef4444'),
|
||||
maintenance: cssManager.bdTheme('#2563eb', '#60a5fa')
|
||||
},
|
||||
|
||||
// Accent colors for interactive elements
|
||||
accent: {
|
||||
primary: cssManager.bdTheme('#09090b', '#fafafa'),
|
||||
hover: cssManager.bdTheme('#18181b', '#e4e4e7'),
|
||||
focus: cssManager.bdTheme('#3b82f6', '#60a5fa')
|
||||
}
|
||||
};
|
||||
|
||||
export const shadows = {
|
||||
xs: '0 1px 2px 0 rgba(0, 0, 0, 0.03)',
|
||||
sm: '0 1px 3px 0 rgba(0, 0, 0, 0.06), 0 1px 2px -1px rgba(0, 0, 0, 0.06)',
|
||||
base: '0 4px 6px -1px rgba(0, 0, 0, 0.07), 0 2px 4px -2px rgba(0, 0, 0, 0.05)',
|
||||
md: '0 6px 12px -2px rgba(0, 0, 0, 0.08), 0 3px 7px -3px rgba(0, 0, 0, 0.05)',
|
||||
lg: '0 12px 24px -4px rgba(0, 0, 0, 0.1), 0 6px 12px -6px rgba(0, 0, 0, 0.05)',
|
||||
xl: '0 24px 48px -12px rgba(0, 0, 0, 0.12), 0 12px 24px -12px rgba(0, 0, 0, 0.05)',
|
||||
inner: 'inset 0 2px 4px 0 rgba(0, 0, 0, 0.04)',
|
||||
glow: '0 0 20px -5px rgba(34, 197, 94, 0.3)'
|
||||
};
|
||||
|
||||
export const borderRadius = {
|
||||
xs: '3px',
|
||||
sm: '4px',
|
||||
base: '6px',
|
||||
md: '8px',
|
||||
lg: '12px',
|
||||
xl: '16px',
|
||||
'2xl': '24px',
|
||||
full: '9999px'
|
||||
};
|
||||
|
||||
export const spacing = {
|
||||
xs: '4px',
|
||||
sm: '8px',
|
||||
md: '16px',
|
||||
lg: '24px',
|
||||
xl: '32px',
|
||||
'2xl': '48px',
|
||||
'3xl': '64px',
|
||||
'4xl': '96px'
|
||||
};
|
||||
|
||||
// Animation easings
|
||||
export const easings = {
|
||||
default: 'cubic-bezier(0.4, 0, 0.2, 1)',
|
||||
smooth: 'cubic-bezier(0.4, 0, 0.6, 1)',
|
||||
bounce: 'cubic-bezier(0.68, -0.55, 0.265, 1.55)',
|
||||
snappy: 'cubic-bezier(0.2, 0, 0, 1)',
|
||||
spring: 'cubic-bezier(0.175, 0.885, 0.32, 1.275)'
|
||||
};
|
||||
|
||||
// Durations
|
||||
export const durations = {
|
||||
instant: '50ms',
|
||||
fast: '100ms',
|
||||
normal: '200ms',
|
||||
slow: '300ms',
|
||||
slower: '500ms',
|
||||
slowest: '800ms'
|
||||
};
|
||||
|
||||
// Status gradients for backgrounds
|
||||
export const statusGradients = {
|
||||
operational: cssManager.bdTheme(
|
||||
'linear-gradient(135deg, rgba(22, 163, 74, 0.08) 0%, rgba(22, 163, 74, 0.02) 100%)',
|
||||
'linear-gradient(135deg, rgba(34, 197, 94, 0.12) 0%, rgba(34, 197, 94, 0.03) 100%)'
|
||||
),
|
||||
degraded: cssManager.bdTheme(
|
||||
'linear-gradient(135deg, rgba(217, 119, 6, 0.08) 0%, rgba(217, 119, 6, 0.02) 100%)',
|
||||
'linear-gradient(135deg, rgba(251, 191, 36, 0.12) 0%, rgba(251, 191, 36, 0.03) 100%)'
|
||||
),
|
||||
partial: cssManager.bdTheme(
|
||||
'linear-gradient(135deg, rgba(220, 38, 38, 0.08) 0%, rgba(220, 38, 38, 0.02) 100%)',
|
||||
'linear-gradient(135deg, rgba(248, 113, 113, 0.12) 0%, rgba(248, 113, 113, 0.03) 100%)'
|
||||
),
|
||||
major: cssManager.bdTheme(
|
||||
'linear-gradient(135deg, rgba(185, 28, 28, 0.10) 0%, rgba(185, 28, 28, 0.03) 100%)',
|
||||
'linear-gradient(135deg, rgba(239, 68, 68, 0.15) 0%, rgba(239, 68, 68, 0.04) 100%)'
|
||||
),
|
||||
maintenance: cssManager.bdTheme(
|
||||
'linear-gradient(135deg, rgba(37, 99, 235, 0.08) 0%, rgba(37, 99, 235, 0.02) 100%)',
|
||||
'linear-gradient(135deg, rgba(96, 165, 250, 0.12) 0%, rgba(96, 165, 250, 0.03) 100%)'
|
||||
)
|
||||
};
|
||||
|
||||
// Glassmorphism styles
|
||||
export const glass = {
|
||||
light: 'background: rgba(255, 255, 255, 0.7); backdrop-filter: blur(12px); -webkit-backdrop-filter: blur(12px);',
|
||||
dark: 'background: rgba(9, 9, 11, 0.8); backdrop-filter: blur(12px); -webkit-backdrop-filter: blur(12px);',
|
||||
subtle: cssManager.bdTheme(
|
||||
'background: rgba(255, 255, 255, 0.5); backdrop-filter: blur(8px);',
|
||||
'background: rgba(9, 9, 11, 0.6); backdrop-filter: blur(8px);'
|
||||
)
|
||||
};
|
||||
|
||||
// Status glow shadows
|
||||
export const statusGlows = {
|
||||
operational: '0 0 20px -5px rgba(34, 197, 94, 0.4)',
|
||||
degraded: '0 0 20px -5px rgba(251, 191, 36, 0.4)',
|
||||
partial: '0 0 20px -5px rgba(248, 113, 113, 0.4)',
|
||||
major: '0 0 20px -5px rgba(239, 68, 68, 0.5)',
|
||||
maintenance: '0 0 20px -5px rgba(96, 165, 250, 0.4)'
|
||||
};
|
||||
|
||||
export const commonStyles = css`
|
||||
/* Button styles */
|
||||
.button {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-family: ${unsafeCSS(fonts.base)};
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
height: 36px;
|
||||
padding: 0 14px;
|
||||
border-radius: ${unsafeCSS(borderRadius.base)};
|
||||
border: 1px solid ${colors.border.default};
|
||||
background: ${colors.background.primary};
|
||||
color: ${colors.text.primary};
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
transition: all ${unsafeCSS(durations.normal)} ${unsafeCSS(easings.default)};
|
||||
gap: 6px;
|
||||
white-space: nowrap;
|
||||
letter-spacing: -0.01em;
|
||||
}
|
||||
|
||||
.button:hover {
|
||||
background: ${colors.background.secondary};
|
||||
border-color: ${cssManager.bdTheme('#d4d4d8', '#3f3f46')};
|
||||
box-shadow: ${unsafeCSS(shadows.xs)};
|
||||
}
|
||||
|
||||
.button:active {
|
||||
transform: scale(0.98);
|
||||
transition-duration: ${unsafeCSS(durations.fast)};
|
||||
}
|
||||
|
||||
.button:focus-visible {
|
||||
outline: 2px solid ${colors.accent.focus};
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
.button.primary {
|
||||
background: ${colors.accent.primary};
|
||||
color: ${colors.background.primary};
|
||||
border-color: transparent;
|
||||
}
|
||||
|
||||
.button.primary:hover {
|
||||
background: ${colors.accent.hover};
|
||||
box-shadow: ${unsafeCSS(shadows.sm)};
|
||||
}
|
||||
|
||||
.button.ghost {
|
||||
background: transparent;
|
||||
border-color: transparent;
|
||||
}
|
||||
|
||||
.button.ghost:hover {
|
||||
background: ${colors.background.muted};
|
||||
}
|
||||
|
||||
.button.sm {
|
||||
height: 32px;
|
||||
padding: 0 12px;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.button.lg {
|
||||
height: 44px;
|
||||
padding: 0 20px;
|
||||
font-size: 15px;
|
||||
}
|
||||
|
||||
/* Card styles */
|
||||
.card {
|
||||
background: ${colors.background.card};
|
||||
border: 1px solid ${colors.border.default};
|
||||
border-radius: ${unsafeCSS(borderRadius.lg)};
|
||||
padding: ${unsafeCSS(spacing.lg)};
|
||||
box-shadow: ${unsafeCSS(shadows.sm)};
|
||||
transition: all ${unsafeCSS(durations.normal)} ${unsafeCSS(easings.default)};
|
||||
}
|
||||
|
||||
.card:hover {
|
||||
border-color: ${colors.border.muted};
|
||||
box-shadow: ${unsafeCSS(shadows.base)};
|
||||
}
|
||||
|
||||
.card.interactive {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.card.interactive:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: ${unsafeCSS(shadows.md)};
|
||||
}
|
||||
|
||||
/* Loading skeleton */
|
||||
.skeleton {
|
||||
background: ${cssManager.bdTheme(
|
||||
'linear-gradient(90deg, #f4f4f5 0%, #e4e4e7 50%, #f4f4f5 100%)',
|
||||
'linear-gradient(90deg, #18181b 0%, #27272a 50%, #18181b 100%)'
|
||||
)};
|
||||
background-size: 200% 100%;
|
||||
animation: skeleton-loading 1.5s ease-in-out infinite;
|
||||
border-radius: ${unsafeCSS(borderRadius.base)};
|
||||
}
|
||||
|
||||
@keyframes skeleton-loading {
|
||||
0% { background-position: 200% 0; }
|
||||
100% { background-position: -200% 0; }
|
||||
}
|
||||
|
||||
/* Pulse animation for status indicators */
|
||||
@keyframes pulse {
|
||||
0%, 100% { opacity: 1; }
|
||||
50% { opacity: 0.5; }
|
||||
}
|
||||
|
||||
/* Pulse ring animation for active status */
|
||||
@keyframes pulse-ring {
|
||||
0% {
|
||||
transform: scale(1);
|
||||
opacity: 0.8;
|
||||
}
|
||||
100% {
|
||||
transform: scale(2.5);
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
|
||||
/* Shimmer effect for loading states */
|
||||
@keyframes shimmer {
|
||||
0% {
|
||||
background-position: -200% 0;
|
||||
}
|
||||
100% {
|
||||
background-position: 200% 0;
|
||||
}
|
||||
}
|
||||
|
||||
/* Fade in animation */
|
||||
@keyframes fadeIn {
|
||||
from { opacity: 0; transform: translateY(4px); }
|
||||
to { opacity: 1; transform: translateY(0); }
|
||||
}
|
||||
|
||||
.fade-in {
|
||||
animation: fadeIn ${unsafeCSS(durations.slow)} ${unsafeCSS(easings.default)} forwards;
|
||||
}
|
||||
|
||||
/* Fade in up animation */
|
||||
@keyframes fadeInUp {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(16px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
.fade-in-up {
|
||||
animation: fadeInUp ${unsafeCSS(durations.slower)} ${unsafeCSS(easings.default)} forwards;
|
||||
}
|
||||
|
||||
/* Scale in animation */
|
||||
@keyframes scaleIn {
|
||||
from { opacity: 0; transform: scale(0.95); }
|
||||
to { opacity: 1; transform: scale(1); }
|
||||
}
|
||||
|
||||
.scale-in {
|
||||
animation: scaleIn ${unsafeCSS(durations.slow)} ${unsafeCSS(easings.bounce)} forwards;
|
||||
}
|
||||
|
||||
/* Slide down animation for expanding content */
|
||||
@keyframes slideDown {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(-8px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
.slide-down {
|
||||
animation: slideDown ${unsafeCSS(durations.slow)} ${unsafeCSS(easings.default)} forwards;
|
||||
}
|
||||
|
||||
/* Stagger animation delay utilities */
|
||||
.stagger-1 { animation-delay: 50ms; }
|
||||
.stagger-2 { animation-delay: 100ms; }
|
||||
.stagger-3 { animation-delay: 150ms; }
|
||||
.stagger-4 { animation-delay: 200ms; }
|
||||
.stagger-5 { animation-delay: 250ms; }
|
||||
|
||||
/* Status indicator with pulse ring */
|
||||
.status-dot-animated {
|
||||
position: relative;
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
.status-dot-animated::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
border-radius: 50%;
|
||||
background: inherit;
|
||||
animation: pulse-ring 2s ${unsafeCSS(easings.default)} infinite;
|
||||
}
|
||||
|
||||
.status-dot-animated.operational::before {
|
||||
background: ${colors.status.operational};
|
||||
}
|
||||
|
||||
.status-dot-animated.degraded::before,
|
||||
.status-dot-animated.partial_outage::before,
|
||||
.status-dot-animated.major_outage::before {
|
||||
animation: pulse-ring 1.5s ${unsafeCSS(easings.default)} infinite;
|
||||
}
|
||||
|
||||
/* Hover lift effect */
|
||||
.hover-lift {
|
||||
transition: transform ${unsafeCSS(durations.normal)} ${unsafeCSS(easings.default)},
|
||||
box-shadow ${unsafeCSS(durations.normal)} ${unsafeCSS(easings.default)};
|
||||
}
|
||||
|
||||
.hover-lift:hover {
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
/* Icon animation */
|
||||
.icon-spin {
|
||||
animation: spin 1s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
from { transform: rotate(0deg); }
|
||||
to { transform: rotate(360deg); }
|
||||
}
|
||||
|
||||
/* Bounce animation for attention */
|
||||
@keyframes bounce {
|
||||
0%, 100% { transform: translateY(0); }
|
||||
50% { transform: translateY(-4px); }
|
||||
}
|
||||
|
||||
.bounce {
|
||||
animation: bounce 1s ${unsafeCSS(easings.bounce)} infinite;
|
||||
}
|
||||
|
||||
/* Container styles */
|
||||
.container {
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
padding: 0 ${unsafeCSS(spacing.lg)};
|
||||
}
|
||||
|
||||
/* Status pill */
|
||||
.status-pill {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
padding: 4px 10px;
|
||||
border-radius: ${unsafeCSS(borderRadius.full)};
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
letter-spacing: 0.01em;
|
||||
}
|
||||
|
||||
.status-pill .status-dot {
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
border-radius: 50%;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.status-pill.operational {
|
||||
background: ${cssManager.bdTheme('rgba(22, 163, 74, 0.1)', 'rgba(34, 197, 94, 0.15)')};
|
||||
color: ${cssManager.bdTheme('#15803d', '#4ade80')};
|
||||
}
|
||||
|
||||
.status-pill.operational .status-dot {
|
||||
background: ${colors.status.operational};
|
||||
}
|
||||
|
||||
.status-pill.degraded {
|
||||
background: ${cssManager.bdTheme('rgba(217, 119, 6, 0.1)', 'rgba(251, 191, 36, 0.15)')};
|
||||
color: ${cssManager.bdTheme('#b45309', '#fcd34d')};
|
||||
}
|
||||
|
||||
.status-pill.degraded .status-dot {
|
||||
background: ${colors.status.degraded};
|
||||
}
|
||||
|
||||
.status-pill.partial_outage,
|
||||
.status-pill.major_outage {
|
||||
background: ${cssManager.bdTheme('rgba(220, 38, 38, 0.1)', 'rgba(248, 113, 113, 0.15)')};
|
||||
color: ${cssManager.bdTheme('#b91c1c', '#fca5a5')};
|
||||
}
|
||||
|
||||
.status-pill.partial_outage .status-dot,
|
||||
.status-pill.major_outage .status-dot {
|
||||
background: ${colors.status.major};
|
||||
}
|
||||
|
||||
.status-pill.maintenance {
|
||||
background: ${cssManager.bdTheme('rgba(37, 99, 235, 0.1)', 'rgba(96, 165, 250, 0.15)')};
|
||||
color: ${cssManager.bdTheme('#1d4ed8', '#93c5fd')};
|
||||
}
|
||||
|
||||
.status-pill.maintenance .status-dot {
|
||||
background: ${colors.status.maintenance};
|
||||
}
|
||||
|
||||
/* Responsive utilities */
|
||||
@media (max-width: 1024px) {
|
||||
.container {
|
||||
padding: 0 ${unsafeCSS(spacing.md)};
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 640px) {
|
||||
.container {
|
||||
padding: 0 ${unsafeCSS(spacing.md)};
|
||||
}
|
||||
|
||||
.button {
|
||||
height: 40px;
|
||||
padding: 0 16px;
|
||||
}
|
||||
|
||||
.button.sm {
|
||||
height: 36px;
|
||||
}
|
||||
}
|
||||
|
||||
/* Visually hidden (for accessibility) */
|
||||
.sr-only {
|
||||
position: absolute;
|
||||
width: 1px;
|
||||
height: 1px;
|
||||
padding: 0;
|
||||
margin: -1px;
|
||||
overflow: hidden;
|
||||
clip: rect(0, 0, 0, 0);
|
||||
white-space: nowrap;
|
||||
border: 0;
|
||||
}
|
||||
`;
|
||||
|
||||
export const getStatusColor = (status: string) => {
|
||||
switch (status) {
|
||||
case 'operational':
|
||||
return colors.status.operational;
|
||||
case 'degraded':
|
||||
return colors.status.degraded;
|
||||
case 'partial_outage':
|
||||
return colors.status.partial;
|
||||
case 'major_outage':
|
||||
return colors.status.major;
|
||||
case 'maintenance':
|
||||
return colors.status.maintenance;
|
||||
default:
|
||||
return colors.text.secondary;
|
||||
}
|
||||
};
|
||||
|
||||
export const getStatusIcon = (status: string) => {
|
||||
switch (status) {
|
||||
case 'operational':
|
||||
return '✓';
|
||||
case 'degraded':
|
||||
return '!';
|
||||
case 'partial_outage':
|
||||
return '⚠';
|
||||
case 'major_outage':
|
||||
return '✕';
|
||||
case 'maintenance':
|
||||
return '🔧';
|
||||
default:
|
||||
return '?';
|
||||
}
|
||||
};
|
||||
@@ -1,8 +1,12 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"experimentalDecorators": true,
|
||||
"target": "ES2022",
|
||||
"module": "ES2022",
|
||||
"moduleResolution": "nodenext"
|
||||
}
|
||||
"module": "NodeNext",
|
||||
"moduleResolution": "NodeNext",
|
||||
"esModuleInterop": true,
|
||||
"verbatimModuleSyntax": true
|
||||
},
|
||||
"exclude": [
|
||||
"dist_*/**/*.d.ts"
|
||||
]
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user