update
This commit is contained in:
@@ -1,6 +1,8 @@
|
||||
// dees tools
|
||||
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';
|
||||
|
||||
16
package.json
16
package.json
@@ -15,19 +15,19 @@
|
||||
"author": "Lossless GmbH",
|
||||
"license": "UNLICENSED",
|
||||
"dependencies": {
|
||||
"@design.estate/dees-domtools": "^2.0.1",
|
||||
"@design.estate/dees-element": "^2.0.4",
|
||||
"@design.estate/dees-wcctools": "^1.0.73",
|
||||
"@design.estate/dees-domtools": "^2.3.3",
|
||||
"@design.estate/dees-element": "^2.0.45",
|
||||
"@design.estate/dees-wcctools": "^1.1.0",
|
||||
"@uptime.link/interfaces": "^2.0.21"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@git.zone/tsbuild": "^2.1.61",
|
||||
"@git.zone/tsbundle": "^2.0.7",
|
||||
"@git.zone/tsrun": "^1.2.39",
|
||||
"@git.zone/tswatch": "^2.0.5",
|
||||
"@git.zone/tsbuild": "^2.6.4",
|
||||
"@git.zone/tsbundle": "^2.5.1",
|
||||
"@git.zone/tsrun": "^1.3.3",
|
||||
"@git.zone/tswatch": "^2.1.2",
|
||||
"@push.rocks/projectinfo": "^5.0.1",
|
||||
"@push.rocks/smartenv": "^5.0.0",
|
||||
"@types/node": "^18.11.18"
|
||||
"@types/node": "^24.0.7"
|
||||
},
|
||||
"files": [
|
||||
"ts/**/*",
|
||||
|
||||
4955
pnpm-lock.yaml
generated
4955
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
272
readme.hints.md
272
readme.hints.md
@@ -0,0 +1,272 @@
|
||||
# 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)
|
||||
|
||||
### 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
|
||||
|
||||
### 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
|
||||
- IStatusDetail - 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.
|
||||
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.
|
||||
@@ -1,3 +1,4 @@
|
||||
// Export components
|
||||
export * from './upl-statuspage-assetsselector.js';
|
||||
export * from './upl-statuspage-footer.js';
|
||||
export * from './upl-statuspage-header.js';
|
||||
@@ -5,3 +6,6 @@ export * from './upl-statuspage-incidents.js';
|
||||
export * from './upl-statuspage-statusbar.js';
|
||||
export * from './upl-statuspage-statusdetails.js';
|
||||
export * from './upl-statuspage-statusmonth.js';
|
||||
|
||||
// Export interfaces
|
||||
export * from '../interfaces/index.js';
|
||||
|
||||
@@ -1,26 +1,33 @@
|
||||
import { customElement, DeesElement, html, type TemplateResult } from '@design.estate/dees-element';
|
||||
|
||||
import { customElement, DeesElement, html, type TemplateResult, css, cssManager, unsafeCSS } from '@design.estate/dees-element';
|
||||
import * as domtools from '@design.estate/dees-domtools';
|
||||
import { fonts, colors, spacing } from '../../styles/shared.styles.js';
|
||||
|
||||
@customElement('uplinternal-miniheading')
|
||||
export class UplinternalMiniheading extends DeesElement {
|
||||
public static styles = [
|
||||
domtools.elementBasic.staticStyles,
|
||||
css`
|
||||
:host {
|
||||
display: block;
|
||||
font-family: ${unsafeCSS(fonts.base)};
|
||||
}
|
||||
|
||||
h5 {
|
||||
display: block;
|
||||
max-width: 1200px;
|
||||
margin: 0px auto;
|
||||
padding: 0px 0px ${unsafeCSS(spacing.md)} 0px;
|
||||
color: ${colors.text.secondary};
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
letter-spacing: 0.025em;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
`
|
||||
];
|
||||
|
||||
public render(): TemplateResult {
|
||||
return html`
|
||||
${domtools.elementBasic.styles}
|
||||
<style>
|
||||
:host {
|
||||
display: block;
|
||||
font-family: Inter;
|
||||
}
|
||||
|
||||
h5 {
|
||||
display: block;
|
||||
max-width: 900px;
|
||||
margin: 0px auto;
|
||||
padding: 0px 0px 10px 0px;
|
||||
color: #707070;
|
||||
}
|
||||
</style>
|
||||
<h5>${this.textContent}</h5>
|
||||
`;
|
||||
}
|
||||
|
||||
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>
|
||||
`;
|
||||
@@ -6,10 +6,14 @@ import {
|
||||
type TemplateResult,
|
||||
cssManager,
|
||||
css,
|
||||
unsafeCSS,
|
||||
} from '@design.estate/dees-element';
|
||||
import * as domtools from '@design.estate/dees-domtools';
|
||||
import type { IServiceStatus } from '../interfaces/index.js';
|
||||
import { fonts, colors, shadows, borderRadius, spacing, commonStyles, getStatusColor } from '../styles/shared.styles.js';
|
||||
|
||||
import './internal/uplinternal-miniheading.js';
|
||||
import { demoFunc } from './upl-statuspage-assetsselector.demo.js';
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
@@ -19,9 +23,22 @@ declare global {
|
||||
|
||||
@customElement('upl-statuspage-assetsselector')
|
||||
export class UplStatuspageAssetsselector extends DeesElement {
|
||||
public static demo = () => html`
|
||||
<upl-statuspage-assetsselector></upl-statuspage-assetsselector>
|
||||
`;
|
||||
public static demo = demoFunc;
|
||||
|
||||
@property({ type: Array })
|
||||
public services: IServiceStatus[] = [];
|
||||
|
||||
@property({ type: String })
|
||||
public filterText: string = '';
|
||||
|
||||
@property({ type: String })
|
||||
public filterCategory: string = 'all';
|
||||
|
||||
@property({ type: Boolean })
|
||||
public showOnlySelected: boolean = false;
|
||||
|
||||
@property({ type: Boolean })
|
||||
public loading: boolean = false;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
@@ -29,35 +46,398 @@ export class UplStatuspageAssetsselector extends DeesElement {
|
||||
|
||||
public static styles = [
|
||||
cssManager.defaultStyles,
|
||||
commonStyles,
|
||||
css`
|
||||
:host {
|
||||
padding: 0px 0px 15px 0px;
|
||||
display: block;
|
||||
background: ${cssManager.bdTheme('#eeeeeb', '#222222')};
|
||||
font-family: Inter;
|
||||
color: #fff;
|
||||
background: transparent;
|
||||
font-family: ${unsafeCSS(fonts.base)};
|
||||
color: ${colors.text.primary};
|
||||
}
|
||||
|
||||
.mainbox {
|
||||
margin: auto;
|
||||
max-width: 900px;
|
||||
.container {
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
padding: 0 ${unsafeCSS(spacing.lg)} ${unsafeCSS(spacing.lg)} ${unsafeCSS(spacing.lg)};
|
||||
}
|
||||
|
||||
.controls {
|
||||
display: flex;
|
||||
gap: ${unsafeCSS(spacing.sm)};
|
||||
margin-bottom: ${unsafeCSS(spacing.lg)};
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.search-input {
|
||||
flex: 1;
|
||||
min-width: 240px;
|
||||
padding: ${unsafeCSS(spacing.sm)} ${unsafeCSS(spacing.md)};
|
||||
border: 1px solid ${colors.border.default};
|
||||
border-radius: ${unsafeCSS(borderRadius.base)};
|
||||
background: ${colors.background.primary};
|
||||
color: ${colors.text.primary};
|
||||
font-size: 14px;
|
||||
font-family: ${unsafeCSS(fonts.base)};
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.search-input:focus {
|
||||
outline: none;
|
||||
border-color: ${colors.text.primary};
|
||||
box-shadow: 0 0 0 3px ${cssManager.bdTheme('rgba(0, 0, 0, 0.05)', 'rgba(255, 255, 255, 0.05)')};
|
||||
}
|
||||
|
||||
.search-input::placeholder {
|
||||
color: ${colors.text.muted};
|
||||
}
|
||||
|
||||
.filter-button {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: ${unsafeCSS(spacing.sm)} ${unsafeCSS(spacing.md)};
|
||||
border: 1px solid ${colors.border.default};
|
||||
border-radius: ${unsafeCSS(borderRadius.base)};
|
||||
background: transparent;
|
||||
color: ${colors.text.primary};
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
font-family: ${unsafeCSS(fonts.base)};
|
||||
transition: all 0.2s ease;
|
||||
height: 36px;
|
||||
}
|
||||
|
||||
.filter-button:hover {
|
||||
background: ${colors.background.secondary};
|
||||
border-color: ${colors.border.muted};
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.filter-button:active {
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
.filter-button.active {
|
||||
background: ${colors.text.primary};
|
||||
color: ${colors.background.primary};
|
||||
border-color: ${colors.text.primary};
|
||||
}
|
||||
|
||||
.filter-button.active:hover {
|
||||
background: ${cssManager.bdTheme('#262626', '#f4f4f5')};
|
||||
border-color: ${cssManager.bdTheme('#262626', '#f4f4f5')};
|
||||
}
|
||||
|
||||
.assets-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(320px, 1fr));
|
||||
gap: ${unsafeCSS(spacing.md)};
|
||||
background: ${colors.background.card};
|
||||
padding: ${unsafeCSS(spacing.lg)};
|
||||
border-radius: ${unsafeCSS(borderRadius.md)};
|
||||
border: 1px solid ${colors.border.default};
|
||||
min-height: 200px;
|
||||
box-shadow: ${unsafeCSS(shadows.sm)};
|
||||
}
|
||||
|
||||
.asset-card {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: ${unsafeCSS(spacing.md)};
|
||||
background: ${colors.background.secondary};
|
||||
border-radius: ${unsafeCSS(borderRadius.base)};
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
border: 1px solid ${colors.border.default};
|
||||
gap: ${unsafeCSS(spacing.md)};
|
||||
}
|
||||
|
||||
.asset-card:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: ${unsafeCSS(shadows.md)};
|
||||
border-color: ${colors.border.muted};
|
||||
}
|
||||
|
||||
.asset-card.selected {
|
||||
border-color: ${colors.text.primary};
|
||||
background: ${colors.background.muted};
|
||||
box-shadow: 0 0 0 1px ${colors.text.primary};
|
||||
}
|
||||
|
||||
.asset-checkbox {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
cursor: pointer;
|
||||
accent-color: ${colors.text.primary};
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.asset-info {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.asset-name {
|
||||
font-weight: 600;
|
||||
font-size: 14px;
|
||||
margin-bottom: ${unsafeCSS(spacing.xs)};
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.asset-description {
|
||||
font-size: 13px;
|
||||
color: ${colors.text.secondary};
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.asset-status {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: ${unsafeCSS(spacing.xs)};
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.status-indicator {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
border-radius: ${unsafeCSS(borderRadius.full)};
|
||||
}
|
||||
|
||||
.status-indicator.operational { background: ${colors.status.operational}; }
|
||||
.status-indicator.degraded { background: ${colors.status.degraded}; }
|
||||
.status-indicator.partial_outage { background: ${colors.status.partial}; }
|
||||
.status-indicator.major_outage { background: ${colors.status.major}; }
|
||||
.status-indicator.maintenance { background: ${colors.status.maintenance}; }
|
||||
|
||||
.status-text {
|
||||
font-size: 12px;
|
||||
text-transform: capitalize;
|
||||
color: ${colors.text.secondary};
|
||||
}
|
||||
|
||||
.loading-message,
|
||||
.no-results {
|
||||
grid-column: 1 / -1;
|
||||
text-align: center;
|
||||
height: 50px;
|
||||
border-radius: 3px;
|
||||
background: ${cssManager.bdTheme('#ffffff', '#333333')};;
|
||||
padding: ${unsafeCSS(spacing['2xl'])};
|
||||
color: ${colors.text.secondary};
|
||||
}
|
||||
|
||||
.summary {
|
||||
text-align: right;
|
||||
font-size: 13px;
|
||||
margin-top: ${unsafeCSS(spacing.md)};
|
||||
color: ${colors.text.secondary};
|
||||
}
|
||||
|
||||
@media (max-width: 640px) {
|
||||
.container {
|
||||
padding: 0 ${unsafeCSS(spacing.md)} ${unsafeCSS(spacing.md)} ${unsafeCSS(spacing.md)};
|
||||
}
|
||||
|
||||
.controls {
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
.search-input {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.assets-grid {
|
||||
grid-template-columns: 1fr;
|
||||
padding: ${unsafeCSS(spacing.md)};
|
||||
}
|
||||
|
||||
.asset-card {
|
||||
padding: ${unsafeCSS(spacing.sm)};
|
||||
}
|
||||
}
|
||||
`,
|
||||
]
|
||||
|
||||
public render(): TemplateResult {
|
||||
const filteredServices = this.getFilteredServices();
|
||||
const selectedCount = this.services.filter(s => s.selected).length;
|
||||
const categories = this.getUniqueCategories();
|
||||
|
||||
return html`
|
||||
<style>
|
||||
<div class="container">
|
||||
<uplinternal-miniheading>Monitored Assets</uplinternal-miniheading>
|
||||
|
||||
</style>
|
||||
<uplinternal-miniheading>Monitored Assets</uplinternal-miniheading>
|
||||
<div class="mainbox">
|
||||
Hello!
|
||||
<div class="controls">
|
||||
<input
|
||||
type="text"
|
||||
class="search-input"
|
||||
placeholder="Search services..."
|
||||
.value=${this.filterText}
|
||||
@input=${(e: Event) => {
|
||||
this.filterText = (e.target as HTMLInputElement).value;
|
||||
}}
|
||||
/>
|
||||
|
||||
<button
|
||||
class="filter-button ${this.filterCategory === 'all' ? 'active' : ''}"
|
||||
@click=${() => { this.filterCategory = 'all'; }}
|
||||
>
|
||||
All
|
||||
</button>
|
||||
|
||||
${categories.map(category => html`
|
||||
<button
|
||||
class="filter-button ${this.filterCategory === category ? 'active' : ''}"
|
||||
@click=${() => { this.filterCategory = category; }}
|
||||
>
|
||||
${category}
|
||||
</button>
|
||||
`)}
|
||||
|
||||
<button
|
||||
class="filter-button ${this.showOnlySelected ? 'active' : ''}"
|
||||
@click=${() => { this.showOnlySelected = !this.showOnlySelected; }}
|
||||
>
|
||||
${this.showOnlySelected ? 'Show All' : 'Selected Only'}
|
||||
</button>
|
||||
|
||||
<button
|
||||
class="filter-button"
|
||||
@click=${() => this.selectAll()}
|
||||
>
|
||||
Select All
|
||||
</button>
|
||||
|
||||
<button
|
||||
class="filter-button"
|
||||
@click=${() => this.selectNone()}
|
||||
>
|
||||
Select None
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="assets-grid">
|
||||
${this.loading ? html`
|
||||
<div class="loading-message">Loading services...</div>
|
||||
` : filteredServices.length === 0 ? html`
|
||||
<div class="no-results">No services found matching your criteria</div>
|
||||
` : filteredServices.map(service => html`
|
||||
<div
|
||||
class="asset-card ${service.selected ? 'selected' : ''}"
|
||||
@click=${() => this.toggleService(service.id)}
|
||||
>
|
||||
<input
|
||||
type="checkbox"
|
||||
class="asset-checkbox"
|
||||
.checked=${service.selected}
|
||||
@click=${(e: Event) => e.stopPropagation()}
|
||||
@change=${(e: Event) => {
|
||||
e.stopPropagation();
|
||||
this.toggleService(service.id);
|
||||
}}
|
||||
/>
|
||||
|
||||
<div class="asset-info">
|
||||
<div class="asset-name">${service.displayName}</div>
|
||||
${service.description ? html`
|
||||
<div class="asset-description">${service.description}</div>
|
||||
` : ''}
|
||||
</div>
|
||||
|
||||
<div class="asset-status">
|
||||
<div class="status-indicator ${service.currentStatus}"></div>
|
||||
<div class="status-text">${service.currentStatus.replace(/_/g, ' ')}</div>
|
||||
</div>
|
||||
</div>
|
||||
`)}
|
||||
</div>
|
||||
|
||||
<div class="summary">
|
||||
${selectedCount} of ${this.services.length} services selected
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
private getFilteredServices(): IServiceStatus[] {
|
||||
return this.services.filter(service => {
|
||||
// Apply text filter
|
||||
if (this.filterText && !service.displayName.toLowerCase().includes(this.filterText.toLowerCase()) &&
|
||||
(!service.description || !service.description.toLowerCase().includes(this.filterText.toLowerCase()))) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Apply category filter
|
||||
if (this.filterCategory !== 'all' && service.category !== this.filterCategory) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Apply selected filter
|
||||
if (this.showOnlySelected && !service.selected) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
private getUniqueCategories(): string[] {
|
||||
const categories = new Set<string>();
|
||||
this.services.forEach(service => {
|
||||
if (service.category) {
|
||||
categories.add(service.category);
|
||||
}
|
||||
});
|
||||
return Array.from(categories).sort();
|
||||
}
|
||||
|
||||
private toggleService(serviceId: string) {
|
||||
const service = this.services.find(s => s.id === serviceId);
|
||||
if (service) {
|
||||
service.selected = !service.selected;
|
||||
this.requestUpdate();
|
||||
|
||||
this.dispatchEvent(new CustomEvent('selectionChanged', {
|
||||
detail: {
|
||||
serviceId,
|
||||
selected: service.selected,
|
||||
selectedServices: this.services.filter(s => s.selected).map(s => s.id)
|
||||
},
|
||||
bubbles: true,
|
||||
composed: true
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
private selectAll() {
|
||||
const filteredServices = this.getFilteredServices();
|
||||
filteredServices.forEach(service => {
|
||||
service.selected = true;
|
||||
});
|
||||
this.requestUpdate();
|
||||
this.emitSelectionUpdate();
|
||||
}
|
||||
|
||||
private selectNone() {
|
||||
const filteredServices = this.getFilteredServices();
|
||||
filteredServices.forEach(service => {
|
||||
service.selected = false;
|
||||
});
|
||||
this.requestUpdate();
|
||||
this.emitSelectionUpdate();
|
||||
}
|
||||
|
||||
private emitSelectionUpdate() {
|
||||
this.dispatchEvent(new CustomEvent('selectionChanged', {
|
||||
detail: {
|
||||
selectedServices: this.services.filter(s => s.selected).map(s => s.id)
|
||||
},
|
||||
bubbles: true,
|
||||
composed: true
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
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, type TemplateResult, css, cssManager } from '@design.estate/dees-element';
|
||||
import { DeesElement, property, html, customElement, type TemplateResult, css, cssManager, unsafeCSS } from '@design.estate/dees-element';
|
||||
import * as domtools from '@design.estate/dees-domtools';
|
||||
import { fonts, colors, shadows, borderRadius, spacing, commonStyles } from '../styles/shared.styles.js';
|
||||
import { demoFunc } from './upl-statuspage-footer.demo.js';
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
@@ -10,18 +12,80 @@ declare global {
|
||||
@customElement('upl-statuspage-footer')
|
||||
export class UplStatuspageFooter extends DeesElement {
|
||||
// STATIC
|
||||
public static demo = () => html`
|
||||
<upl-statuspage-footer></upl-statuspage-footer>
|
||||
`;
|
||||
public static demo = demoFunc;
|
||||
|
||||
// INSTANCE
|
||||
@property()
|
||||
public legalInfo: string = "https://lossless.gmbh";
|
||||
@property({ type: String })
|
||||
public companyName: string = '';
|
||||
|
||||
@property({
|
||||
type: Boolean
|
||||
})
|
||||
public whitelabel = false;
|
||||
@property({ type: String })
|
||||
public legalUrl: string = '';
|
||||
|
||||
@property({ type: String })
|
||||
public supportEmail: string = '';
|
||||
|
||||
@property({ type: String })
|
||||
public statusPageUrl: string = '';
|
||||
|
||||
@property({ type: Boolean })
|
||||
public whitelabel: boolean = false;
|
||||
|
||||
@property({ type: Number })
|
||||
public lastUpdated: number | null = null;
|
||||
|
||||
@property({ type: Number })
|
||||
public currentYear: number = new Date().getFullYear();
|
||||
|
||||
@property({ type: Array })
|
||||
public socialLinks: Array<{ platform: string; url: string }> = [];
|
||||
|
||||
@property({ type: Array })
|
||||
public additionalLinks: Array<{ label: string; url: string }> = [];
|
||||
|
||||
@property({ type: String })
|
||||
public rssFeedUrl: string = '';
|
||||
|
||||
@property({ type: String })
|
||||
public apiStatusUrl: string = '';
|
||||
|
||||
@property({ type: Boolean })
|
||||
public loading: boolean = false;
|
||||
|
||||
@property({ type: String })
|
||||
public errorMessage: string | null = null;
|
||||
|
||||
@property({ type: Boolean })
|
||||
public offline: boolean = false;
|
||||
|
||||
@property({ type: String })
|
||||
public latestStatusUpdate: string = '';
|
||||
|
||||
@property({ type: Boolean })
|
||||
public enableSubscribe: boolean = false;
|
||||
|
||||
@property({ type: Boolean })
|
||||
public enableReportIssue: boolean = false;
|
||||
|
||||
@property({ type: Boolean })
|
||||
public enableLanguageSelector: boolean = false;
|
||||
|
||||
@property({ type: Boolean })
|
||||
public enableThemeToggle: boolean = false;
|
||||
|
||||
@property({ type: Array })
|
||||
public languageOptions: Array<{ code: string; label: string }> = [];
|
||||
|
||||
@property({ type: String })
|
||||
public currentLanguage: string = 'en';
|
||||
|
||||
@property({ type: String })
|
||||
public currentTheme: string = 'light';
|
||||
|
||||
@property({ type: Number })
|
||||
public subscriberCount: number = 0;
|
||||
|
||||
@property({ type: Object })
|
||||
public customBranding: { primaryColor?: string; logoUrl?: string; footerText?: string } | null = null;
|
||||
|
||||
|
||||
constructor() {
|
||||
@@ -30,43 +94,545 @@ export class UplStatuspageFooter extends DeesElement {
|
||||
|
||||
public static styles = [
|
||||
domtools.elementBasic.staticStyles,
|
||||
commonStyles,
|
||||
css`
|
||||
:host {
|
||||
display: block;
|
||||
background: ${cssManager.bdTheme('#ffffff', '#000000')};
|
||||
font-family: Inter;
|
||||
color: ${cssManager.bdTheme('#333333', '#ffffff')};
|
||||
}
|
||||
display: block;
|
||||
background: ${colors.background.primary};
|
||||
font-family: ${unsafeCSS(fonts.base)};
|
||||
color: ${colors.text.primary};
|
||||
font-size: 14px;
|
||||
border-top: 1px solid ${colors.border.default};
|
||||
}
|
||||
|
||||
.mainbox {
|
||||
max-width: 900px;
|
||||
margin: auto;
|
||||
padding-top: 20px;
|
||||
padding-bottom: 20px;
|
||||
.container {
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
padding: ${unsafeCSS(spacing['2xl'])} ${unsafeCSS(spacing.lg)};
|
||||
}
|
||||
|
||||
.footer-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: ${unsafeCSS(spacing.xl)};
|
||||
}
|
||||
|
||||
.footer-main {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr auto;
|
||||
gap: ${unsafeCSS(spacing['2xl'])};
|
||||
align-items: start;
|
||||
}
|
||||
|
||||
.company-info {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: ${unsafeCSS(spacing.lg)};
|
||||
}
|
||||
|
||||
.company-name {
|
||||
font-size: 20px;
|
||||
font-weight: 600;
|
||||
color: ${colors.text.primary};
|
||||
}
|
||||
|
||||
.company-links {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: ${unsafeCSS(spacing.lg)};
|
||||
}
|
||||
|
||||
.footer-link {
|
||||
color: ${colors.text.secondary};
|
||||
text-decoration: none;
|
||||
transition: color 0.2s ease;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.footer-link:hover {
|
||||
color: ${colors.text.primary};
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.footer-actions {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: ${unsafeCSS(spacing.md)};
|
||||
}
|
||||
|
||||
.action-button {
|
||||
padding: ${unsafeCSS(spacing.sm)} ${unsafeCSS(spacing.md)};
|
||||
border: 1px solid ${colors.border.default};
|
||||
background: transparent;
|
||||
border-radius: ${unsafeCSS(borderRadius.base)};
|
||||
cursor: pointer;
|
||||
text-align: center;
|
||||
transition: all 0.2s ease;
|
||||
white-space: nowrap;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
color: ${colors.text.primary};
|
||||
font-family: ${unsafeCSS(fonts.base)};
|
||||
}
|
||||
|
||||
.action-button:hover {
|
||||
background: ${colors.background.secondary};
|
||||
border-color: ${colors.border.muted};
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.action-button:active {
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
.footer-bottom {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding-top: ${unsafeCSS(spacing.xl)};
|
||||
margin-top: ${unsafeCSS(spacing.xl)};
|
||||
border-top: 1px solid ${colors.border.default};
|
||||
}
|
||||
|
||||
.footer-meta {
|
||||
display: flex;
|
||||
gap: ${unsafeCSS(spacing.lg)};
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.social-links {
|
||||
display: flex;
|
||||
gap: ${unsafeCSS(spacing.md)};
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.social-link {
|
||||
display: inline-block;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
opacity: 0.7;
|
||||
transition: all 0.2s ease;
|
||||
color: ${colors.text.secondary};
|
||||
}
|
||||
|
||||
.social-link:hover {
|
||||
opacity: 1;
|
||||
color: ${colors.text.primary};
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.social-link svg {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
fill: currentColor;
|
||||
}
|
||||
|
||||
.copyright {
|
||||
font-size: 14px;
|
||||
color: ${colors.text.secondary};
|
||||
}
|
||||
|
||||
.last-updated {
|
||||
font-size: 13px;
|
||||
color: ${colors.text.muted};
|
||||
}
|
||||
|
||||
.powered-by {
|
||||
font-size: 13px;
|
||||
color: ${colors.text.muted};
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.powered-by a {
|
||||
color: ${colors.text.secondary};
|
||||
text-decoration: none;
|
||||
transition: color 0.2s ease;
|
||||
}
|
||||
|
||||
.powered-by a:hover {
|
||||
color: ${colors.text.primary};
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.status-update {
|
||||
padding: ${unsafeCSS(spacing.md)} ${unsafeCSS(spacing.lg)};
|
||||
background: ${colors.background.muted};
|
||||
border-radius: ${unsafeCSS(borderRadius.base)};
|
||||
font-size: 14px;
|
||||
margin-bottom: ${unsafeCSS(spacing.lg)};
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.language-selector {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.language-selector select {
|
||||
padding: ${unsafeCSS(spacing.xs)} ${unsafeCSS(spacing.sm)};
|
||||
border: 1px solid ${colors.border.default};
|
||||
background: ${colors.background.primary};
|
||||
color: ${colors.text.primary};
|
||||
border-radius: ${unsafeCSS(borderRadius.base)};
|
||||
font-size: 14px;
|
||||
cursor: pointer;
|
||||
font-family: ${unsafeCSS(fonts.base)};
|
||||
}
|
||||
|
||||
.theme-toggle {
|
||||
cursor: pointer;
|
||||
padding: ${unsafeCSS(spacing.xs)} ${unsafeCSS(spacing.sm)};
|
||||
border: 1px solid ${colors.border.default};
|
||||
background: transparent;
|
||||
border-radius: ${unsafeCSS(borderRadius.base)};
|
||||
font-size: 14px;
|
||||
transition: all 0.2s ease;
|
||||
color: ${colors.text.primary};
|
||||
font-family: ${unsafeCSS(fonts.base)};
|
||||
}
|
||||
|
||||
.theme-toggle:hover {
|
||||
background: ${colors.background.secondary};
|
||||
border-color: ${colors.border.muted};
|
||||
}
|
||||
|
||||
.subscriber-count {
|
||||
font-size: 12px;
|
||||
color: ${colors.text.muted};
|
||||
margin-top: ${unsafeCSS(spacing.xs)};
|
||||
}
|
||||
|
||||
.error-message {
|
||||
padding: ${unsafeCSS(spacing.md)};
|
||||
background: ${cssManager.bdTheme('#fee9e9', '#7f1d1d')};
|
||||
color: ${cssManager.bdTheme('#dc2626', '#fca5a5')};
|
||||
border-radius: ${unsafeCSS(borderRadius.base)};
|
||||
margin-bottom: ${unsafeCSS(spacing.lg)};
|
||||
font-size: 14px;
|
||||
border: 1px solid ${cssManager.bdTheme('#fecaca', '#991b1b')};
|
||||
}
|
||||
|
||||
.offline-indicator {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: ${unsafeCSS(spacing.xs)};
|
||||
padding: ${unsafeCSS(spacing.xs)} ${unsafeCSS(spacing.md)};
|
||||
background: ${colors.status.degraded};
|
||||
color: white;
|
||||
border-radius: ${unsafeCSS(borderRadius.full)};
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.loading-skeleton {
|
||||
height: 200px;
|
||||
background: ${cssManager.bdTheme(
|
||||
'linear-gradient(90deg, #f3f4f6 25%, #e5e7eb 50%, #f3f4f6 75%)',
|
||||
'linear-gradient(90deg, #1f1f1f 25%, #262626 50%, #1f1f1f 75%)'
|
||||
)};
|
||||
background-size: 200% 100%;
|
||||
animation: loading 1.5s infinite;
|
||||
border-radius: ${unsafeCSS(borderRadius.md)};
|
||||
}
|
||||
|
||||
@keyframes loading {
|
||||
0% { background-position: 200% 0; }
|
||||
100% { background-position: -200% 0; }
|
||||
}
|
||||
|
||||
.additional-links {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: ${unsafeCSS(spacing.md)};
|
||||
margin-top: ${unsafeCSS(spacing.md)};
|
||||
}
|
||||
|
||||
.additional-link {
|
||||
font-size: 14px;
|
||||
color: ${colors.text.secondary};
|
||||
text-decoration: none;
|
||||
transition: color 0.2s ease;
|
||||
}
|
||||
|
||||
.additional-link:hover {
|
||||
color: ${colors.text.primary};
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
@media (max-width: 640px) {
|
||||
.container {
|
||||
padding: ${unsafeCSS(spacing.xl)} ${unsafeCSS(spacing.md)};
|
||||
}
|
||||
|
||||
.footer-main {
|
||||
grid-template-columns: 1fr;
|
||||
gap: ${unsafeCSS(spacing.xl)};
|
||||
}
|
||||
|
||||
.footer-bottom {
|
||||
flex-direction: column;
|
||||
gap: ${unsafeCSS(spacing.lg)};
|
||||
align-items: start;
|
||||
}
|
||||
|
||||
.footer-meta {
|
||||
flex-direction: column;
|
||||
align-items: start;
|
||||
gap: ${unsafeCSS(spacing.md)};
|
||||
}
|
||||
|
||||
.company-links {
|
||||
flex-direction: column;
|
||||
gap: ${unsafeCSS(spacing.sm)};
|
||||
}
|
||||
|
||||
.powered-by {
|
||||
text-align: left;
|
||||
}
|
||||
}
|
||||
`
|
||||
]
|
||||
|
||||
public render(): TemplateResult {
|
||||
if (this.loading) {
|
||||
return html`
|
||||
<div class="container">
|
||||
<div class="loading-skeleton"></div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
return html`
|
||||
${domtools.elementBasic.styles}
|
||||
<style></style>
|
||||
<div class="mainbox">
|
||||
Hi there
|
||||
<div class="container">
|
||||
<div class="footer-content">
|
||||
${this.errorMessage ? html`
|
||||
<div class="error-message">${this.errorMessage}</div>
|
||||
` : ''}
|
||||
|
||||
${this.offline ? html`
|
||||
<div class="offline-indicator">
|
||||
<span>⚠</span> You are currently offline
|
||||
</div>
|
||||
` : ''}
|
||||
|
||||
${this.latestStatusUpdate ? html`
|
||||
<div class="status-update">
|
||||
<strong>Latest Update:</strong> ${this.latestStatusUpdate}
|
||||
</div>
|
||||
` : ''}
|
||||
|
||||
<div class="footer-main">
|
||||
<div class="company-info">
|
||||
${this.companyName ? html`
|
||||
<div class="company-name">${this.companyName}</div>
|
||||
` : ''}
|
||||
|
||||
<div class="company-links">
|
||||
${this.legalUrl && this.isValidUrl(this.legalUrl) ? html`
|
||||
<a href="${this.legalUrl}" class="footer-link" @click=${(e: Event) => this.handleLinkClick(e, 'legal', this.legalUrl)}>Legal</a>
|
||||
` : ''}
|
||||
|
||||
${this.supportEmail && this.isValidEmail(this.supportEmail) ? html`
|
||||
<a href="mailto:${this.supportEmail}" class="footer-link" @click=${(e: Event) => this.handleLinkClick(e, 'support', this.supportEmail)}>Support</a>
|
||||
` : ''}
|
||||
|
||||
${this.statusPageUrl && this.isValidUrl(this.statusPageUrl) ? html`
|
||||
<a href="${this.statusPageUrl}" class="footer-link" @click=${(e: Event) => this.handleLinkClick(e, 'status', this.statusPageUrl)}>Status Page</a>
|
||||
` : ''}
|
||||
|
||||
${this.rssFeedUrl && this.isValidUrl(this.rssFeedUrl) ? html`
|
||||
<a href="${this.rssFeedUrl}" class="footer-link" @click=${(e: Event) => this.handleLinkClick(e, 'rss', this.rssFeedUrl)}>RSS Feed</a>
|
||||
` : ''}
|
||||
|
||||
${this.apiStatusUrl && this.isValidUrl(this.apiStatusUrl) ? html`
|
||||
<a href="${this.apiStatusUrl}" class="footer-link" @click=${(e: Event) => this.handleLinkClick(e, 'api', this.apiStatusUrl)}>API Status</a>
|
||||
` : ''}
|
||||
</div>
|
||||
|
||||
${this.additionalLinks && this.additionalLinks.length > 0 ? html`
|
||||
<div class="additional-links">
|
||||
${this.additionalLinks.map(link => html`
|
||||
${link.label && link.url && this.isValidUrl(link.url) ? html`
|
||||
<a href="${link.url}" class="additional-link" @click=${(e: Event) => this.handleLinkClick(e, 'additional', link.url, link.label)}>
|
||||
${link.label}
|
||||
</a>
|
||||
` : ''}
|
||||
`)}
|
||||
</div>
|
||||
` : ''}
|
||||
</div>
|
||||
|
||||
<div class="footer-actions">
|
||||
${this.enableSubscribe ? html`
|
||||
<button class="action-button" @click=${this.handleSubscribeClick}>
|
||||
Subscribe to Updates
|
||||
${this.subscriberCount > 0 ? html`
|
||||
<div class="subscriber-count">${this.subscriberCount.toLocaleString()} subscribers</div>
|
||||
` : ''}
|
||||
</button>
|
||||
` : ''}
|
||||
|
||||
${this.enableReportIssue ? html`
|
||||
<button class="action-button" @click=${this.handleReportIncidentClick}>
|
||||
Report an Issue
|
||||
</button>
|
||||
` : ''}
|
||||
|
||||
${this.enableLanguageSelector && this.languageOptions.length > 0 ? html`
|
||||
<div class="language-selector">
|
||||
<select @change=${this.handleLanguageChange} .value=${this.currentLanguage}>
|
||||
${this.languageOptions.map(option => html`
|
||||
<option value="${option.code}" ?selected=${option.code === this.currentLanguage}>
|
||||
${option.label}
|
||||
</option>
|
||||
`)}
|
||||
</select>
|
||||
</div>
|
||||
` : ''}
|
||||
|
||||
${this.enableThemeToggle ? html`
|
||||
<button class="theme-toggle" @click=${this.handleThemeToggle}>
|
||||
${this.currentTheme === 'dark' ? '☀️' : '🌙'} ${this.currentTheme === 'dark' ? 'Light' : 'Dark'} Mode
|
||||
</button>
|
||||
` : ''}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="footer-bottom">
|
||||
<div class="footer-meta">
|
||||
<div class="copyright">
|
||||
© ${this.currentYear} ${this.companyName || 'Status Page'}
|
||||
</div>
|
||||
|
||||
${this.lastUpdated ? html`
|
||||
<div class="last-updated">
|
||||
Last updated: ${this.formatLastUpdated()}
|
||||
</div>
|
||||
` : ''}
|
||||
|
||||
${this.socialLinks && this.socialLinks.length > 0 ? html`
|
||||
<div class="social-links">
|
||||
${this.socialLinks.map(social => html`
|
||||
${social.platform && social.url && this.isValidUrl(social.url) ? html`
|
||||
<a href="${social.url}" class="social-link" title="${social.platform}" @click=${(e: Event) => this.handleLinkClick(e, 'social', social.url, social.platform)}>
|
||||
${this.getSocialIcon(social.platform)}
|
||||
</a>
|
||||
` : ''}
|
||||
`)}
|
||||
</div>
|
||||
` : ''}
|
||||
</div>
|
||||
|
||||
${!this.whitelabel ? html`
|
||||
<div class="powered-by">
|
||||
${this.customBranding?.footerText || html`
|
||||
Powered by <a href="https://uptime.link" target="_blank" @click=${(e: Event) => this.handleLinkClick(e, 'powered-by', 'https://uptime.link')}>uptime.link</a>
|
||||
`}
|
||||
</div>
|
||||
` : ''}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
private isValidUrl(url: string): boolean {
|
||||
if (!url) return false;
|
||||
try {
|
||||
new URL(url);
|
||||
return true;
|
||||
} catch {
|
||||
return url.startsWith('#') || url.startsWith('/');
|
||||
}
|
||||
}
|
||||
|
||||
private isValidEmail(email: string): boolean {
|
||||
if (!email) return false;
|
||||
return email.includes('@');
|
||||
}
|
||||
|
||||
private formatLastUpdated(): string {
|
||||
if (!this.lastUpdated) return 'Never';
|
||||
const date = new Date(this.lastUpdated);
|
||||
const now = Date.now();
|
||||
const diff = now - this.lastUpdated;
|
||||
|
||||
if (diff < 60000) {
|
||||
return 'Just now';
|
||||
} else if (diff < 3600000) {
|
||||
const minutes = Math.floor(diff / 60000);
|
||||
return `${minutes} minute${minutes !== 1 ? 's' : ''} ago`;
|
||||
} else if (diff < 86400000) {
|
||||
const hours = Math.floor(diff / 3600000);
|
||||
return `${hours} hour${hours !== 1 ? 's' : ''} ago`;
|
||||
} else {
|
||||
return date.toLocaleDateString();
|
||||
}
|
||||
}
|
||||
|
||||
private getSocialIcon(platform: string): TemplateResult {
|
||||
const icons: Record<string, TemplateResult> = {
|
||||
twitter: html`<svg viewBox="0 0 24 24"><path d="M23.953 4.57a10 10 0 01-2.825.775 4.958 4.958 0 002.163-2.723c-.951.555-2.005.959-3.127 1.184a4.92 4.92 0 00-8.384 4.482C7.69 8.095 4.067 6.13 1.64 3.162a4.822 4.822 0 00-.666 2.475c0 1.71.87 3.213 2.188 4.096a4.904 4.904 0 01-2.228-.616v.06a4.923 4.923 0 003.946 4.827 4.996 4.996 0 01-2.212.085 4.936 4.936 0 004.604 3.417 9.867 9.867 0 01-6.102 2.105c-.39 0-.779-.023-1.17-.067a13.995 13.995 0 007.557 2.209c9.053 0 13.998-7.496 13.998-13.985 0-.21 0-.42-.015-.63A9.935 9.935 0 0024 4.59z"/></svg>`,
|
||||
github: html`<svg viewBox="0 0 24 24"><path d="M12 .297c-6.63 0-12 5.373-12 12 0 5.303 3.438 9.8 8.205 11.385.6.113.82-.258.82-.577 0-.285-.01-1.04-.015-2.04-3.338.724-4.042-1.61-4.042-1.61C4.422 18.07 3.633 17.7 3.633 17.7c-1.087-.744.084-.729.084-.729 1.205.084 1.838 1.236 1.838 1.236 1.07 1.835 2.809 1.305 3.495.998.108-.776.417-1.305.76-1.605-2.665-.3-5.466-1.332-5.466-5.93 0-1.31.465-2.38 1.235-3.22-.135-.303-.54-1.523.105-3.176 0 0 1.005-.322 3.3 1.23.96-.267 1.98-.399 3-.405 1.02.006 2.04.138 3 .405 2.28-1.552 3.285-1.23 3.285-1.23.645 1.653.24 2.873.12 3.176.765.84 1.23 1.91 1.23 3.22 0 4.61-2.805 5.625-5.475 5.92.42.36.81 1.096.81 2.22 0 1.606-.015 2.896-.015 3.286 0 .315.21.69.825.57C20.565 22.092 24 17.592 24 12.297c0-6.627-5.373-12-12-12"/></svg>`,
|
||||
linkedin: html`<svg viewBox="0 0 24 24"><path d="M20.447 20.452h-3.554v-5.569c0-1.328-.027-3.037-1.852-3.037-1.853 0-2.136 1.445-2.136 2.939v5.667H9.351V9h3.414v1.561h.046c.477-.9 1.637-1.85 3.37-1.85 3.601 0 4.267 2.37 4.267 5.455v6.286zM5.337 7.433c-1.144 0-2.063-.926-2.063-2.065 0-1.138.92-2.063 2.063-2.063 1.14 0 2.064.925 2.064 2.063 0 1.139-.925 2.065-2.064 2.065zm1.782 13.019H3.555V9h3.564v11.452zM22.225 0H1.771C.792 0 0 .774 0 1.729v20.542C0 23.227.792 24 1.771 24h20.451C23.2 24 24 23.227 24 22.271V1.729C24 .774 23.2 0 22.222 0h.003z"/></svg>`,
|
||||
facebook: html`<svg viewBox="0 0 24 24"><path d="M24 12.073c0-6.627-5.373-12-12-12s-12 5.373-12 12c0 5.99 4.388 10.954 10.125 11.854v-8.385H7.078v-3.47h3.047V9.43c0-3.007 1.792-4.669 4.533-4.669 1.312 0 2.686.235 2.686.235v2.953H15.83c-1.491 0-1.956.925-1.956 1.874v2.25h3.328l-.532 3.47h-2.796v8.385C19.612 23.027 24 18.062 24 12.073z"/></svg>`,
|
||||
youtube: html`<svg viewBox="0 0 24 24"><path d="M23.498 6.186a3.016 3.016 0 0 0-2.122-2.136C19.505 3.545 12 3.545 12 3.545s-7.505 0-9.377.505A3.017 3.017 0 0 0 .502 6.186C0 8.07 0 12 0 12s0 3.93.502 5.814a3.016 3.016 0 0 0 2.122 2.136c1.871.505 9.376.505 9.376.505s7.505 0 9.377-.505a3.015 3.015 0 0 0 2.122-2.136C24 15.93 24 12 24 12s0-3.93-.502-5.814zM9.545 15.568V8.432L15.818 12l-6.273 3.568z"/></svg>`,
|
||||
instagram: html`<svg viewBox="0 0 24 24"><path d="M12 2.163c3.204 0 3.584.012 4.85.07 3.252.148 4.771 1.691 4.919 4.919.058 1.265.069 1.645.069 4.849 0 3.205-.012 3.584-.069 4.849-.149 3.225-1.664 4.771-4.919 4.919-1.266.058-1.644.07-4.85.07-3.204 0-3.584-.012-4.849-.07-3.26-.149-4.771-1.699-4.919-4.92-.058-1.265-.07-1.644-.07-4.849 0-3.204.013-3.583.07-4.849.149-3.227 1.664-4.771 4.919-4.919 1.266-.057 1.645-.069 4.849-.069zm0-2.163c-3.259 0-3.667.014-4.947.072-4.358.2-6.78 2.618-6.98 6.98-.059 1.281-.073 1.689-.073 4.948 0 3.259.014 3.668.072 4.948.2 4.358 2.618 6.78 6.98 6.98 1.281.058 1.689.072 4.948.072 3.259 0 3.668-.014 4.948-.072 4.354-.2 6.782-2.618 6.979-6.98.059-1.28.073-1.689.073-4.948 0-3.259-.014-3.667-.072-4.947-.196-4.354-2.617-6.78-6.979-6.98-1.281-.059-1.69-.073-4.949-.073zM5.838 12a6.162 6.162 0 1 1 12.324 0 6.162 6.162 0 0 1-12.324 0zM12 16a4 4 0 1 1 0-8 4 4 0 0 1 0 8zm4.965-10.405a1.44 1.44 0 1 1 2.881.001 1.44 1.44 0 0 1-2.881-.001z"/></svg>`,
|
||||
slack: html`<svg viewBox="0 0 24 24"><path d="M5.042 15.165a2.528 2.528 0 0 1-2.52 2.523A2.528 2.528 0 0 1 0 15.165a2.527 2.527 0 0 1 2.522-2.52h2.52v2.52zM6.313 15.165a2.527 2.527 0 0 1 2.521-2.52 2.527 2.527 0 0 1 2.521 2.52v6.313A2.528 2.528 0 0 1 8.834 24a2.528 2.528 0 0 1-2.521-2.522v-6.313zM8.834 5.042a2.528 2.528 0 0 1-2.521-2.52A2.528 2.528 0 0 1 8.834 0a2.528 2.528 0 0 1 2.521 2.522v2.52H8.834zM8.834 6.313a2.528 2.528 0 0 1 2.521 2.521 2.528 2.528 0 0 1-2.521 2.521H2.522A2.528 2.528 0 0 1 0 8.834a2.528 2.528 0 0 1 2.522-2.521h6.312zM18.956 8.834a2.528 2.528 0 0 1 2.522-2.521A2.528 2.528 0 0 1 24 8.834a2.528 2.528 0 0 1-2.522 2.521h-2.522V8.834zM17.688 8.834a2.528 2.528 0 0 1-2.523 2.521 2.527 2.527 0 0 1-2.52-2.521V2.522A2.527 2.527 0 0 1 15.165 0a2.528 2.528 0 0 1 2.523 2.522v6.312zM15.165 18.956a2.528 2.528 0 0 1 2.523 2.522A2.528 2.528 0 0 1 15.165 24a2.527 2.527 0 0 1-2.52-2.522v-2.522h2.52zM15.165 17.688a2.527 2.527 0 0 1-2.52-2.523 2.526 2.526 0 0 1 2.52-2.52h6.313A2.527 2.527 0 0 1 24 15.165a2.528 2.528 0 0 1-2.522 2.523h-6.313z"/></svg>`,
|
||||
discord: html`<svg viewBox="0 0 24 24"><path d="M20.317 4.3698a19.7913 19.7913 0 00-4.8851-1.5152.0741.0741 0 00-.0785.0371c-.211.3753-.4447.8648-.6083 1.2495-1.8447-.2762-3.68-.2762-5.4868 0-.1636-.3933-.4058-.8742-.6177-1.2495a.077.077 0 00-.0785-.037 19.7363 19.7363 0 00-4.8852 1.515.0699.0699 0 00-.0321.0277C.5334 9.0458-.319 13.5799.0992 18.0578a.0824.0824 0 00.0312.0561c2.0528 1.5076 4.0413 2.4228 5.9929 3.0294a.0777.0777 0 00.0842-.0276c.4616-.6304.8731-1.2952 1.226-1.9942a.076.076 0 00-.0416-.1057c-.6528-.2476-1.2743-.5495-1.8722-.8923a.077.077 0 01-.0076-.1277c.1258-.0943.2517-.1923.3718-.2914a.0743.0743 0 01.0776-.0105c3.9278 1.7933 8.18 1.7933 12.0614 0a.0739.0739 0 01.0785.0095c.1202.099.246.1981.3728.2924a.077.077 0 01-.0066.1276 12.2986 12.2986 0 01-1.873.8914.0766.0766 0 00-.0407.1067c.3604.698.7719 1.3628 1.225 1.9932a.076.076 0 00.0842.0286c1.961-.6067 3.9495-1.5219 6.0023-3.0294a.077.077 0 00.0313-.0552c.5004-5.177-.8382-9.6739-3.5485-13.6604a.061.061 0 00-.0312-.0286zM8.02 15.3312c-1.1825 0-2.1569-1.0857-2.1569-2.419 0-1.3332.9555-2.4189 2.157-2.4189 1.2108 0 2.1757 1.0952 2.1568 2.419 0 1.3332-.9555 2.4189-2.1569 2.4189zm7.9748 0c-1.1825 0-2.1569-1.0857-2.1569-2.419 0-1.3332.9554-2.4189 2.1569-2.4189 1.2108 0 2.1757 1.0952 2.1568 2.419 0 1.3332-.946 2.4189-2.1568 2.4189Z"/></svg>`,
|
||||
generic: html`<svg viewBox="0 0 24 24"><path d="M13.46,12L19,17.54V19H17.54L12,13.46L6.46,19H5V17.54L10.54,12L5,6.46V5H6.46L12,10.54L17.54,5H19V6.46L13.46,12Z"/></svg>`
|
||||
};
|
||||
return icons[platform.toLowerCase()] || icons.generic;
|
||||
}
|
||||
|
||||
private handleLinkClick(event: Event, type: string, url: string, label?: string) {
|
||||
this.dispatchEvent(new CustomEvent('footerLinkClick', {
|
||||
detail: { type, url, label },
|
||||
bubbles: true,
|
||||
composed: true
|
||||
}));
|
||||
}
|
||||
|
||||
private handleSubscribeClick() {
|
||||
this.dispatchEvent(new CustomEvent('subscribeClick', {
|
||||
bubbles: true,
|
||||
composed: true
|
||||
}));
|
||||
}
|
||||
|
||||
private handleReportIncidentClick() {
|
||||
this.dispatchEvent(new CustomEvent('reportIncidentClick', {
|
||||
bubbles: true,
|
||||
composed: true
|
||||
}));
|
||||
}
|
||||
|
||||
private handleLanguageChange(event: Event) {
|
||||
const select = event.target as HTMLSelectElement;
|
||||
const language = select.value;
|
||||
this.currentLanguage = language;
|
||||
this.dispatchEvent(new CustomEvent('languageChange', {
|
||||
detail: { language },
|
||||
bubbles: true,
|
||||
composed: true
|
||||
}));
|
||||
}
|
||||
|
||||
private handleThemeToggle() {
|
||||
const newTheme = this.currentTheme === 'dark' ? 'light' : 'dark';
|
||||
this.currentTheme = newTheme;
|
||||
this.dispatchEvent(new CustomEvent('themeToggle', {
|
||||
detail: { theme: newTheme },
|
||||
bubbles: true,
|
||||
composed: true
|
||||
}));
|
||||
}
|
||||
|
||||
public dispatchReportNewIncident() {
|
||||
this.dispatchEvent(new CustomEvent('reportNewIncident', {
|
||||
|
||||
}))
|
||||
this.handleReportIncidentClick();
|
||||
}
|
||||
|
||||
public dispatchStatusSubscribe() {
|
||||
this.dispatchEvent(new CustomEvent('statusSubscribe', {
|
||||
|
||||
}))
|
||||
this.handleSubscribeClick();
|
||||
}
|
||||
}
|
||||
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, type TemplateResult, css, cssManager } from '@design.estate/dees-element';
|
||||
import { DeesElement, property, html, customElement, type TemplateResult, css, cssManager, unsafeCSS } from '@design.estate/dees-element';
|
||||
import * as domtools from '@design.estate/dees-domtools';
|
||||
import { fonts } from '../styles/shared.styles.js';
|
||||
import { demoFunc } from './upl-statuspage-header.demo.js';
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
@@ -10,14 +12,30 @@ declare global {
|
||||
@customElement('upl-statuspage-header')
|
||||
export class UplStatuspageHeader extends DeesElement {
|
||||
// STATIC
|
||||
public static demo = () => html`
|
||||
<upl-statuspage-header></upl-statuspage-header>
|
||||
`;
|
||||
public static demo = demoFunc;
|
||||
|
||||
// INSTANCE
|
||||
@property()
|
||||
@property({ type: String })
|
||||
public pageTitle: string = "Statuspage Title";
|
||||
|
||||
@property({ type: Boolean })
|
||||
public showReportButton: boolean = true;
|
||||
|
||||
@property({ type: Boolean })
|
||||
public showSubscribeButton: boolean = true;
|
||||
|
||||
@property({ type: String })
|
||||
public brandColor: string = '';
|
||||
|
||||
@property({ type: String })
|
||||
public logoUrl: string = '';
|
||||
|
||||
@property({ type: Boolean })
|
||||
public customStyles: boolean = false;
|
||||
|
||||
@property({ type: Boolean })
|
||||
public loading: boolean = false;
|
||||
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
@@ -28,70 +46,162 @@ export class UplStatuspageHeader extends DeesElement {
|
||||
css`
|
||||
:host {
|
||||
display: block;
|
||||
background: ${cssManager.bdTheme('#eeeeeb', '#222222')};
|
||||
font-family: Inter;
|
||||
color: ${cssManager.bdTheme('#333333', '#ffffff')};
|
||||
background: ${cssManager.bdTheme('#ffffff', '#0a0a0a')};
|
||||
font-family: ${unsafeCSS(fonts.base)};
|
||||
color: ${cssManager.bdTheme('#0a0a0a', '#fafafa')};
|
||||
border-bottom: 1px solid ${cssManager.bdTheme('#e5e7eb', '#262626')};
|
||||
}
|
||||
|
||||
.mainbox {
|
||||
margin: auto;
|
||||
max-width: 900px;
|
||||
max-width: 1200px;
|
||||
padding: 0 24px;
|
||||
}
|
||||
|
||||
.mainbox .actions {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
padding: 20px 0px 40px 0px;
|
||||
gap: 8px;
|
||||
padding: 24px 0;
|
||||
}
|
||||
|
||||
.mainbox .actions .actionButton {
|
||||
background: ${cssManager.bdTheme('#00000000', '#ffffff00')};
|
||||
|
||||
font-size: 12px;
|
||||
border: 1px solid ${cssManager.bdTheme('#333', '#CCC')};
|
||||
padding: 6px 10px 7px 10px;
|
||||
margin-left: 10px;
|
||||
border-radius: 3px;
|
||||
background: transparent;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
border: 1px solid ${cssManager.bdTheme('#e5e7eb', '#262626')};
|
||||
padding: 8px 16px;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
transition: all 0.2s ease;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-height: 36px;
|
||||
color: ${cssManager.bdTheme('#0a0a0a', '#fafafa')};
|
||||
}
|
||||
|
||||
.mainbox .actions .actionButton:hover {
|
||||
background: ${cssManager.bdTheme('#333333', '#efefef')};
|
||||
border: 1px solid ${cssManager.bdTheme('#333333', '#efefef')};
|
||||
color: ${cssManager.bdTheme('#fff', '#333333')};
|
||||
background: ${cssManager.bdTheme('#f9fafb', '#262626')};
|
||||
border-color: ${cssManager.bdTheme('#d1d5db', '#404040')};
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.mainbox .actions .actionButton:active {
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
.header-content {
|
||||
padding: 48px 0 64px 0;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
h1 {
|
||||
margin: 0px;
|
||||
text-align: center;
|
||||
font-weight: 600;
|
||||
font-size: 35px;
|
||||
margin: 0;
|
||||
font-size: 48px;
|
||||
font-weight: 700;
|
||||
letter-spacing: -0.02em;
|
||||
line-height: 1.1;
|
||||
color: ${cssManager.bdTheme('#0a0a0a', '#fafafa')};
|
||||
}
|
||||
|
||||
h2 {
|
||||
margin: 0px;
|
||||
margin-top: 10px;
|
||||
text-align: center;
|
||||
font-weight: 600;
|
||||
font-size: 18px;
|
||||
.subtitle {
|
||||
margin: 16px 0 0 0;
|
||||
font-size: 16px;
|
||||
font-weight: 400;
|
||||
color: ${cssManager.bdTheme('#6b7280', '#a1a1aa')};
|
||||
letter-spacing: 0.02em;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.logo {
|
||||
display: block;
|
||||
margin: 0 auto 32px;
|
||||
max-width: 180px;
|
||||
height: auto;
|
||||
filter: ${cssManager.bdTheme('none', 'brightness(0) invert(1)')};
|
||||
}
|
||||
|
||||
.loading-skeleton {
|
||||
height: 200px;
|
||||
background: ${cssManager.bdTheme(
|
||||
'linear-gradient(90deg, #f3f4f6 25%, #e5e7eb 50%, #f3f4f6 75%)',
|
||||
'linear-gradient(90deg, #1f1f1f 25%, #262626 50%, #1f1f1f 75%)'
|
||||
)};
|
||||
background-size: 200% 100%;
|
||||
animation: loading 1.5s infinite;
|
||||
border-radius: 6px;
|
||||
margin: 24px 0;
|
||||
}
|
||||
|
||||
@keyframes loading {
|
||||
0% { background-position: 200% 0; }
|
||||
100% { background-position: -200% 0; }
|
||||
}
|
||||
|
||||
/* Primary button variant */
|
||||
.actionButton.primary {
|
||||
background: ${cssManager.bdTheme('#0a0a0a', '#fafafa')};
|
||||
color: ${cssManager.bdTheme('#ffffff', '#0a0a0a')};
|
||||
border-color: ${cssManager.bdTheme('#0a0a0a', '#fafafa')};
|
||||
}
|
||||
|
||||
.actionButton.primary:hover {
|
||||
background: ${cssManager.bdTheme('#262626', '#f4f4f5')};
|
||||
border-color: ${cssManager.bdTheme('#262626', '#f4f4f5')};
|
||||
}
|
||||
`
|
||||
]
|
||||
|
||||
public render(): TemplateResult {
|
||||
if (this.loading) {
|
||||
return html`
|
||||
<div class="mainbox">
|
||||
<div class="loading-skeleton"></div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
return html`
|
||||
${domtools.elementBasic.styles}
|
||||
<style>
|
||||
|
||||
${this.customStyles && this.brandColor ? `
|
||||
.mainbox .actions .actionButton {
|
||||
border-color: ${this.brandColor};
|
||||
color: ${this.brandColor};
|
||||
}
|
||||
.mainbox .actions .actionButton:hover {
|
||||
background: ${this.brandColor}10;
|
||||
border-color: ${this.brandColor};
|
||||
}
|
||||
.mainbox .actions .actionButton.primary {
|
||||
background: ${this.brandColor};
|
||||
border-color: ${this.brandColor};
|
||||
color: white;
|
||||
}
|
||||
.mainbox .actions .actionButton.primary:hover {
|
||||
background: ${this.brandColor}dd;
|
||||
border-color: ${this.brandColor}dd;
|
||||
}
|
||||
` : ''}
|
||||
</style>
|
||||
<div class="mainbox">
|
||||
<div class="actions">
|
||||
<div class="actionButton" @click=${this.dispatchReportNewIncident}>report new incident</div>
|
||||
<div class="actionButton" @click=${this.dispatchStatusSubscribe}>subscribe</div>
|
||||
${this.showReportButton ? html`
|
||||
<div class="actionButton" @click=${this.dispatchReportNewIncident}>Report Issue</div>
|
||||
` : ''}
|
||||
${this.showSubscribeButton ? html`
|
||||
<div class="actionButton primary" @click=${this.dispatchStatusSubscribe}>Subscribe</div>
|
||||
` : ''}
|
||||
</div>
|
||||
<div class="header-content">
|
||||
${this.logoUrl ? html`
|
||||
<img src="${this.logoUrl}" alt="Logo" class="logo">
|
||||
` : ''}
|
||||
<h1>${this.pageTitle}</h1>
|
||||
<div class="subtitle">System Status</div>
|
||||
</div>
|
||||
<h1>${this.pageTitle}</h1>
|
||||
<h2>STATUS BOARD</h2>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
1031
ts_web/elements/upl-statuspage-incidents.demo.ts
Normal file
1031
ts_web/elements/upl-statuspage-incidents.demo.ts
Normal file
File diff suppressed because it is too large
Load Diff
@@ -7,7 +7,12 @@ import {
|
||||
type TemplateResult,
|
||||
css,
|
||||
cssManager,
|
||||
unsafeCSS,
|
||||
} from '@design.estate/dees-element';
|
||||
import type { IIncidentDetails } from '../interfaces/index.js';
|
||||
import { fonts, colors, shadows, borderRadius, spacing, commonStyles } from '../styles/shared.styles.js';
|
||||
import './internal/uplinternal-miniheading.js';
|
||||
import { demoFunc } from './upl-statuspage-incidents.demo.js';
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
@@ -18,73 +23,490 @@ declare global {
|
||||
@customElement('upl-statuspage-incidents')
|
||||
export class UplStatuspageIncidents extends DeesElement {
|
||||
// STATIC
|
||||
public static demo = () => html` <upl-statuspage-incidents></upl-statuspage-incidents> `;
|
||||
public static demo = demoFunc;
|
||||
|
||||
// INSTANCE
|
||||
@property({
|
||||
type: Array,
|
||||
})
|
||||
public currentIncidences: plugins.uplInterfaces.data.IIncident[] = [];
|
||||
public currentIncidents: IIncidentDetails[] = [];
|
||||
|
||||
@property({
|
||||
type: Array,
|
||||
})
|
||||
public pastIncidences: plugins.uplInterfaces.data.IIncident[] = [];
|
||||
public pastIncidents: IIncidentDetails[] = [];
|
||||
|
||||
@property({
|
||||
type: Boolean,
|
||||
})
|
||||
public whitelabel = false;
|
||||
|
||||
@property({
|
||||
type: Boolean,
|
||||
})
|
||||
public loading = false;
|
||||
|
||||
@property({
|
||||
type: Number,
|
||||
})
|
||||
public daysToShow = 90;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
}
|
||||
|
||||
public static styles = [
|
||||
plugins.domtools.elementBasic.staticStyles,
|
||||
commonStyles,
|
||||
css`
|
||||
:host {
|
||||
display: block;
|
||||
background: ${cssManager.bdTheme('#eeeeeb', '#222222')};
|
||||
font-family: Inter;
|
||||
color: ${cssManager.bdTheme('#333333', '#ffffff')};
|
||||
background: transparent;
|
||||
font-family: ${unsafeCSS(fonts.base)};
|
||||
color: ${colors.text.primary};
|
||||
}
|
||||
|
||||
.mainbox {
|
||||
max-width: 900px;
|
||||
margin: auto;
|
||||
.container {
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
padding: 0 ${unsafeCSS(spacing.lg)} ${unsafeCSS(spacing.lg)} ${unsafeCSS(spacing.lg)};
|
||||
}
|
||||
|
||||
.noIncidentBox {
|
||||
background: ${cssManager.bdTheme('#ffffff', '#333333')};;
|
||||
padding: 10px;
|
||||
margin-bottom: 15px;
|
||||
border-radius: 3px;
|
||||
background: ${colors.background.card};
|
||||
padding: ${unsafeCSS(spacing.xl)};
|
||||
margin-bottom: ${unsafeCSS(spacing.lg)};
|
||||
border-radius: ${unsafeCSS(borderRadius.md)};
|
||||
border: 1px solid ${colors.border.default};
|
||||
text-align: center;
|
||||
color: ${colors.text.secondary};
|
||||
box-shadow: ${unsafeCSS(shadows.sm)};
|
||||
}
|
||||
|
||||
.incident-card {
|
||||
background: ${colors.background.card};
|
||||
border-radius: ${unsafeCSS(borderRadius.md)};
|
||||
margin-bottom: ${unsafeCSS(spacing.lg)};
|
||||
overflow: hidden;
|
||||
box-shadow: ${unsafeCSS(shadows.sm)};
|
||||
border: 1px solid ${colors.border.default};
|
||||
transition: all 0.2s ease;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.incident-card:hover {
|
||||
box-shadow: ${unsafeCSS(shadows.md)};
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
.incident-header {
|
||||
padding: ${unsafeCSS(spacing.lg)} ${unsafeCSS(spacing.lg)};
|
||||
border-left: 4px solid;
|
||||
display: flex;
|
||||
align-items: start;
|
||||
justify-content: space-between;
|
||||
gap: ${unsafeCSS(spacing.md)};
|
||||
}
|
||||
|
||||
.incident-header.critical {
|
||||
border-left-color: ${colors.status.major};
|
||||
}
|
||||
|
||||
.incident-header.major {
|
||||
border-left-color: ${colors.status.partial};
|
||||
}
|
||||
|
||||
.incident-header.minor {
|
||||
border-left-color: ${colors.status.degraded};
|
||||
}
|
||||
|
||||
.incident-header.maintenance {
|
||||
border-left-color: ${colors.status.maintenance};
|
||||
}
|
||||
|
||||
.incident-title {
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
margin: 0;
|
||||
line-height: 1.3;
|
||||
}
|
||||
|
||||
.incident-meta {
|
||||
display: flex;
|
||||
gap: ${unsafeCSS(spacing.lg)};
|
||||
margin-top: ${unsafeCSS(spacing.sm)};
|
||||
font-size: 13px;
|
||||
color: ${colors.text.secondary};
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.incident-status {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: ${unsafeCSS(spacing.xs)};
|
||||
padding: ${unsafeCSS(spacing.xs)} ${unsafeCSS(spacing.md)};
|
||||
border-radius: ${unsafeCSS(borderRadius.full)};
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.02em;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.incident-status.investigating {
|
||||
background: ${cssManager.bdTheme('#fef3c7', '#78350f')};
|
||||
color: ${cssManager.bdTheme('#92400e', '#fbbf24')};
|
||||
}
|
||||
|
||||
.incident-status.identified {
|
||||
background: ${cssManager.bdTheme('#e9d5ff', '#581c87')};
|
||||
color: ${cssManager.bdTheme('#6b21a8', '#d8b4fe')};
|
||||
}
|
||||
|
||||
.incident-status.monitoring {
|
||||
background: ${cssManager.bdTheme('#dbeafe', '#1e3a8a')};
|
||||
color: ${cssManager.bdTheme('#1e40af', '#93c5fd')};
|
||||
}
|
||||
|
||||
.incident-status.resolved {
|
||||
background: ${cssManager.bdTheme('#d1fae5', '#064e3b')};
|
||||
color: ${cssManager.bdTheme('#047857', '#6ee7b7')};
|
||||
}
|
||||
|
||||
.incident-status.postmortem {
|
||||
background: ${cssManager.bdTheme('#e5e7eb', '#374151')};
|
||||
color: ${cssManager.bdTheme('#4b5563', '#d1d5db')};
|
||||
}
|
||||
|
||||
.incident-body {
|
||||
padding: 0 ${unsafeCSS(spacing.lg)} ${unsafeCSS(spacing.lg)} ${unsafeCSS(spacing.lg)};
|
||||
}
|
||||
|
||||
.incident-impact {
|
||||
margin: ${unsafeCSS(spacing.md)} 0;
|
||||
padding: ${unsafeCSS(spacing.md)};
|
||||
background: ${colors.background.secondary};
|
||||
border-radius: ${unsafeCSS(borderRadius.base)};
|
||||
font-size: 14px;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.affected-services {
|
||||
margin-top: ${unsafeCSS(spacing.md)};
|
||||
}
|
||||
|
||||
.affected-services-title {
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
margin-bottom: ${unsafeCSS(spacing.sm)};
|
||||
color: ${colors.text.primary};
|
||||
}
|
||||
|
||||
.service-tag {
|
||||
display: inline-block;
|
||||
padding: ${unsafeCSS(spacing.xs)} ${unsafeCSS(spacing.sm)};
|
||||
margin: 2px;
|
||||
background: ${colors.background.muted};
|
||||
border-radius: ${unsafeCSS(borderRadius.sm)};
|
||||
font-size: 12px;
|
||||
color: ${colors.text.secondary};
|
||||
}
|
||||
|
||||
.incident-updates {
|
||||
margin-top: ${unsafeCSS(spacing.lg)};
|
||||
border-top: 1px solid ${colors.border.default};
|
||||
padding-top: ${unsafeCSS(spacing.lg)};
|
||||
}
|
||||
|
||||
.update-item {
|
||||
position: relative;
|
||||
padding-left: ${unsafeCSS(spacing.lg)};
|
||||
margin-bottom: ${unsafeCSS(spacing.md)};
|
||||
}
|
||||
|
||||
.update-item::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 6px;
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
border-radius: ${unsafeCSS(borderRadius.full)};
|
||||
background: ${colors.border.muted};
|
||||
}
|
||||
|
||||
.update-time {
|
||||
font-size: 12px;
|
||||
color: ${colors.text.secondary};
|
||||
margin-bottom: ${unsafeCSS(spacing.xs)};
|
||||
font-family: ${unsafeCSS(fonts.mono)};
|
||||
}
|
||||
|
||||
.update-message {
|
||||
font-size: 14px;
|
||||
line-height: 1.6;
|
||||
color: ${colors.text.primary};
|
||||
}
|
||||
|
||||
.update-author {
|
||||
font-size: 12px;
|
||||
color: ${colors.text.secondary};
|
||||
margin-top: ${unsafeCSS(spacing.xs)};
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.loading-skeleton {
|
||||
height: 140px;
|
||||
background: ${cssManager.bdTheme(
|
||||
'linear-gradient(90deg, #f3f4f6 25%, #e5e7eb 50%, #f3f4f6 75%)',
|
||||
'linear-gradient(90deg, #1f1f1f 25%, #262626 50%, #1f1f1f 75%)'
|
||||
)};
|
||||
background-size: 200% 100%;
|
||||
animation: loading 1.5s infinite;
|
||||
border-radius: ${unsafeCSS(borderRadius.md)};
|
||||
margin-bottom: ${unsafeCSS(spacing.lg)};
|
||||
}
|
||||
|
||||
@keyframes loading {
|
||||
0% { background-position: 200% 0; }
|
||||
100% { background-position: -200% 0; }
|
||||
}
|
||||
|
||||
.show-more {
|
||||
text-align: center;
|
||||
margin-top: ${unsafeCSS(spacing.lg)};
|
||||
}
|
||||
|
||||
.show-more-button {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: ${unsafeCSS(spacing.sm)} ${unsafeCSS(spacing.lg)};
|
||||
background: transparent;
|
||||
border: 1px solid ${colors.border.default};
|
||||
border-radius: ${unsafeCSS(borderRadius.base)};
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
transition: all 0.2s ease;
|
||||
color: ${colors.text.primary};
|
||||
font-family: ${unsafeCSS(fonts.base)};
|
||||
}
|
||||
|
||||
.show-more-button:hover {
|
||||
background: ${colors.background.secondary};
|
||||
border-color: ${colors.border.muted};
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.show-more-button:active {
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
@media (max-width: 640px) {
|
||||
.container {
|
||||
padding: 0 ${unsafeCSS(spacing.md)} ${unsafeCSS(spacing.md)} ${unsafeCSS(spacing.md)};
|
||||
}
|
||||
|
||||
.incident-header {
|
||||
flex-direction: column;
|
||||
align-items: start;
|
||||
gap: ${unsafeCSS(spacing.sm)};
|
||||
}
|
||||
|
||||
.incident-meta {
|
||||
flex-direction: column;
|
||||
gap: ${unsafeCSS(spacing.xs)};
|
||||
}
|
||||
}
|
||||
`,
|
||||
];
|
||||
|
||||
public render(): TemplateResult {
|
||||
return html`
|
||||
<style></style>
|
||||
<div class="mainbox">
|
||||
<uplinternal-miniheading> Current Incidents </uplinternal-miniheading>
|
||||
${this.currentIncidences.length
|
||||
? html``
|
||||
: html` <div class="noIncidentBox">No incidents ongoing.</div> `}
|
||||
<uplinternal-miniheading> Past Incidents </uplinternal-miniheading>
|
||||
${this.pastIncidences.length
|
||||
? html``
|
||||
: html` <div class="noIncidentBox">No past incidents in the last 90 days.</div> `}
|
||||
<div class="container">
|
||||
<uplinternal-miniheading>Current Incidents</uplinternal-miniheading>
|
||||
${this.loading ? html`
|
||||
<div class="loading-skeleton"></div>
|
||||
` : this.currentIncidents.length ?
|
||||
this.currentIncidents.map(incident => this.renderIncident(incident, true)) :
|
||||
html`<div class="noIncidentBox">No incidents ongoing.</div>`
|
||||
}
|
||||
|
||||
<uplinternal-miniheading>Past Incidents</uplinternal-miniheading>
|
||||
${this.loading ? html`
|
||||
<div class="loading-skeleton"></div>
|
||||
<div class="loading-skeleton"></div>
|
||||
` : this.pastIncidents.length ?
|
||||
this.pastIncidents.slice(0, 5).map(incident => this.renderIncident(incident, false)) :
|
||||
html`<div class="noIncidentBox">No past incidents in the last ${this.daysToShow} days.</div>`
|
||||
}
|
||||
|
||||
${this.pastIncidents.length > 5 && !this.loading ? html`
|
||||
<div class="show-more">
|
||||
<button class="show-more-button" @click=${this.handleShowMore}>
|
||||
Show ${this.pastIncidents.length - 5} more incidents
|
||||
</button>
|
||||
</div>
|
||||
` : ''}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
private renderIncident(incident: IIncidentDetails, isCurrent: boolean): TemplateResult {
|
||||
const latestUpdate = incident.updates[incident.updates.length - 1];
|
||||
const duration = incident.endTime ?
|
||||
this.formatDuration(incident.endTime - incident.startTime) :
|
||||
this.formatDuration(Date.now() - incident.startTime);
|
||||
|
||||
return html`
|
||||
<div class="incident-card" @click=${() => this.handleIncidentClick(incident)}>
|
||||
<div class="incident-header ${incident.severity}">
|
||||
<div>
|
||||
<h3 class="incident-title">${incident.title}</h3>
|
||||
<div class="incident-meta">
|
||||
<span>Started: ${this.formatDate(incident.startTime)}</span>
|
||||
<span>Duration: ${duration}</span>
|
||||
${incident.endTime ? html`
|
||||
<span>Ended: ${this.formatDate(incident.endTime)}</span>
|
||||
` : ''}
|
||||
</div>
|
||||
</div>
|
||||
<div class="incident-status ${latestUpdate.status}">
|
||||
${this.getStatusIcon(latestUpdate.status)}
|
||||
${latestUpdate.status.replace(/_/g, ' ')}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="incident-body">
|
||||
<div class="incident-impact">
|
||||
<strong>Impact:</strong> ${incident.impact}
|
||||
</div>
|
||||
|
||||
${incident.affectedServices.length > 0 ? html`
|
||||
<div class="affected-services">
|
||||
<div class="affected-services-title">Affected Services:</div>
|
||||
${incident.affectedServices.map(service => html`
|
||||
<span class="service-tag">${service}</span>
|
||||
`)}
|
||||
</div>
|
||||
` : ''}
|
||||
|
||||
${incident.updates.length > 0 ? html`
|
||||
<div class="incident-updates">
|
||||
<h4 style="font-size: 14px; margin: 0 0 12px 0;">Updates</h4>
|
||||
${incident.updates.slice(-3).reverse().map(update => this.renderUpdate(update))}
|
||||
</div>
|
||||
` : ''}
|
||||
|
||||
${incident.rootCause && isCurrent === false ? html`
|
||||
<div class="incident-impact" style="margin-top: 12px;">
|
||||
<strong>Root Cause:</strong> ${incident.rootCause}
|
||||
</div>
|
||||
` : ''}
|
||||
|
||||
${incident.resolution && isCurrent === false ? html`
|
||||
<div class="incident-impact" style="margin-top: 12px;">
|
||||
<strong>Resolution:</strong> ${incident.resolution}
|
||||
</div>
|
||||
` : ''}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
private renderUpdate(update: any): TemplateResult {
|
||||
return html`
|
||||
<div class="update-item">
|
||||
<div class="update-time">${this.formatDate(update.timestamp)}</div>
|
||||
<div class="update-message">${update.message}</div>
|
||||
${update.author ? html`
|
||||
<div class="update-author">— ${update.author}</div>
|
||||
` : ''}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
private getStatusIcon(status: string): string {
|
||||
const icons: Record<string, string> = {
|
||||
investigating: '🔍',
|
||||
identified: '🎯',
|
||||
monitoring: '👁️',
|
||||
resolved: '✅',
|
||||
postmortem: '📋'
|
||||
};
|
||||
return icons[status] || '•';
|
||||
}
|
||||
|
||||
private formatDate(timestamp: number): string {
|
||||
const date = new Date(timestamp);
|
||||
const now = Date.now();
|
||||
const diff = now - timestamp;
|
||||
|
||||
// Less than 1 hour ago
|
||||
if (diff < 60 * 60 * 1000) {
|
||||
const minutes = Math.floor(diff / (60 * 1000));
|
||||
return `${minutes} minute${minutes !== 1 ? 's' : ''} ago`;
|
||||
}
|
||||
|
||||
// Less than 24 hours ago
|
||||
if (diff < 24 * 60 * 60 * 1000) {
|
||||
const hours = Math.floor(diff / (60 * 60 * 1000));
|
||||
return `${hours} hour${hours !== 1 ? 's' : ''} ago`;
|
||||
}
|
||||
|
||||
// Less than 7 days ago
|
||||
if (diff < 7 * 24 * 60 * 60 * 1000) {
|
||||
const days = Math.floor(diff / (24 * 60 * 60 * 1000));
|
||||
return `${days} day${days !== 1 ? 's' : ''} ago`;
|
||||
}
|
||||
|
||||
// Default to full date
|
||||
return date.toLocaleDateString('en-US', {
|
||||
month: 'short',
|
||||
day: 'numeric',
|
||||
year: date.getFullYear() !== new Date().getFullYear() ? 'numeric' : undefined
|
||||
});
|
||||
}
|
||||
|
||||
private formatDuration(milliseconds: number): string {
|
||||
const minutes = Math.floor(milliseconds / (60 * 1000));
|
||||
const hours = Math.floor(minutes / 60);
|
||||
const days = Math.floor(hours / 24);
|
||||
|
||||
if (days > 0) {
|
||||
return `${days}d ${hours % 24}h`;
|
||||
} else if (hours > 0) {
|
||||
return `${hours}h ${minutes % 60}m`;
|
||||
} else {
|
||||
return `${minutes}m`;
|
||||
}
|
||||
}
|
||||
|
||||
private handleIncidentClick(incident: IIncidentDetails) {
|
||||
this.dispatchEvent(new CustomEvent('incidentClick', {
|
||||
detail: { incident },
|
||||
bubbles: true,
|
||||
composed: true
|
||||
}));
|
||||
}
|
||||
|
||||
private handleShowMore() {
|
||||
// This would typically load more incidents or navigate to a full list
|
||||
console.log('Show more incidents');
|
||||
}
|
||||
|
||||
public dispatchReportNewIncident() {
|
||||
this.dispatchEvent(new CustomEvent('reportNewIncident', {}));
|
||||
this.dispatchEvent(new CustomEvent('reportNewIncident', {
|
||||
bubbles: true,
|
||||
composed: true
|
||||
}));
|
||||
}
|
||||
|
||||
public dispatchStatusSubscribe() {
|
||||
this.dispatchEvent(new CustomEvent('statusSubscribe', {}));
|
||||
this.dispatchEvent(new CustomEvent('statusSubscribe', {
|
||||
bubbles: true,
|
||||
composed: true
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
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, type TemplateResult, cssManager, css } from '@design.estate/dees-element';
|
||||
import { DeesElement, property, html, customElement, type TemplateResult, cssManager, css, unsafeCSS } from '@design.estate/dees-element';
|
||||
import * as domtools from '@design.estate/dees-domtools';
|
||||
import type { IOverallStatus } from '../interfaces/index.js';
|
||||
import { fonts } from '../styles/shared.styles.js';
|
||||
import { demoFunc } from './upl-statuspage-statusbar.demo.js';
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
@@ -9,9 +12,22 @@ declare global {
|
||||
|
||||
@customElement('upl-statuspage-statusbar')
|
||||
export class UplStatuspageStatusbar extends DeesElement {
|
||||
public static demo = () => html`
|
||||
<upl-statuspage-statusbar></upl-statuspage-statusbar>
|
||||
`;
|
||||
public static demo = demoFunc;
|
||||
|
||||
@property({ type: Object })
|
||||
public overallStatus: IOverallStatus = {
|
||||
status: 'operational',
|
||||
message: 'All Systems Operational',
|
||||
lastUpdated: Date.now(),
|
||||
affectedServices: 0,
|
||||
totalServices: 0
|
||||
};
|
||||
|
||||
@property({ type: Boolean })
|
||||
public loading: boolean = false;
|
||||
|
||||
@property({ type: Boolean })
|
||||
public expandable: boolean = true;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
@@ -21,30 +37,200 @@ export class UplStatuspageStatusbar extends DeesElement {
|
||||
cssManager.defaultStyles,
|
||||
css`
|
||||
:host {
|
||||
padding: 20px 0px 15px 0px;
|
||||
padding: 0;
|
||||
display: block;
|
||||
background: ${cssManager.bdTheme('#eeeeeb', '#222222')};;
|
||||
font-family: Inter;
|
||||
color: #fff;
|
||||
background: transparent;
|
||||
font-family: ${unsafeCSS(fonts.base)};
|
||||
}
|
||||
|
||||
.mainbox {
|
||||
.statusbar-container {
|
||||
margin: auto;
|
||||
max-width: 900px;
|
||||
text-align: center;
|
||||
background: #19572E;
|
||||
line-height: 50px;
|
||||
border-radius: 3px;
|
||||
max-width: 1200px;
|
||||
padding: 0 24px 24px 24px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.statusbar-inner {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-height: 64px;
|
||||
padding: 16px 24px;
|
||||
border-radius: 8px;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
font-weight: 500;
|
||||
font-size: 16px;
|
||||
border: 1px solid transparent;
|
||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1), 0 1px 2px rgba(0, 0, 0, 0.06);
|
||||
}
|
||||
|
||||
.statusbar-inner:hover {
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1), 0 2px 4px rgba(0, 0, 0, 0.06);
|
||||
}
|
||||
|
||||
.statusbar-inner:active {
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
.statusbar-inner.operational {
|
||||
background: ${cssManager.bdTheme('#10b981', '#064e3b')};
|
||||
border-color: ${cssManager.bdTheme('#10b981', '#064e3b')};
|
||||
color: white;
|
||||
}
|
||||
|
||||
.statusbar-inner.degraded {
|
||||
background: ${cssManager.bdTheme('#f59e0b', '#78350f')};
|
||||
border-color: ${cssManager.bdTheme('#f59e0b', '#78350f')};
|
||||
color: white;
|
||||
}
|
||||
|
||||
.statusbar-inner.partial_outage {
|
||||
background: ${cssManager.bdTheme('#ef4444', '#7f1d1d')};
|
||||
border-color: ${cssManager.bdTheme('#ef4444', '#7f1d1d')};
|
||||
color: white;
|
||||
}
|
||||
|
||||
.statusbar-inner.major_outage {
|
||||
background: ${cssManager.bdTheme('#dc2626', '#450a0a')};
|
||||
border-color: ${cssManager.bdTheme('#dc2626', '#450a0a')};
|
||||
color: white;
|
||||
}
|
||||
|
||||
.statusbar-inner.maintenance {
|
||||
background: ${cssManager.bdTheme('#3b82f6', '#1e3a8a')};
|
||||
border-color: ${cssManager.bdTheme('#3b82f6', '#1e3a8a')};
|
||||
color: white;
|
||||
}
|
||||
|
||||
.status-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.status-main {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.status-icon {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.loading-skeleton {
|
||||
background: ${cssManager.bdTheme(
|
||||
'linear-gradient(90deg, #f3f4f6 25%, #e5e7eb 50%, #f3f4f6 75%)',
|
||||
'linear-gradient(90deg, #1f1f1f 25%, #262626 50%, #1f1f1f 75%)'
|
||||
)};
|
||||
background-size: 200% 100%;
|
||||
animation: loading 1.5s infinite;
|
||||
height: 64px;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
@keyframes loading {
|
||||
0% {
|
||||
background-position: 200% 0;
|
||||
}
|
||||
100% {
|
||||
background-position: -200% 0;
|
||||
}
|
||||
}
|
||||
|
||||
.status-details {
|
||||
font-size: 14px;
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
.last-updated {
|
||||
font-size: 13px;
|
||||
text-align: right;
|
||||
margin-top: 12px;
|
||||
color: ${cssManager.bdTheme('#6b7280', '#9ca3af')};
|
||||
}
|
||||
|
||||
@media (max-width: 640px) {
|
||||
.statusbar-inner {
|
||||
font-size: 14px;
|
||||
padding: 12px 16px;
|
||||
min-height: 56px;
|
||||
}
|
||||
|
||||
.status-icon {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
font-size: 12px;
|
||||
}
|
||||
}
|
||||
`,
|
||||
]
|
||||
|
||||
public render(): TemplateResult {
|
||||
const getStatusIcon = () => {
|
||||
switch (this.overallStatus.status) {
|
||||
case 'operational':
|
||||
return '✓';
|
||||
case 'degraded':
|
||||
return '!';
|
||||
case 'partial_outage':
|
||||
return '⚠';
|
||||
case 'major_outage':
|
||||
return '✕';
|
||||
case 'maintenance':
|
||||
return '🔧';
|
||||
default:
|
||||
return '';
|
||||
}
|
||||
};
|
||||
|
||||
const formatLastUpdated = () => {
|
||||
const date = new Date(this.overallStatus.lastUpdated);
|
||||
return date.toLocaleString();
|
||||
};
|
||||
|
||||
const handleClick = () => {
|
||||
if (this.expandable) {
|
||||
this.dispatchEvent(new CustomEvent('statusClick', {
|
||||
detail: { status: this.overallStatus },
|
||||
bubbles: true,
|
||||
composed: true
|
||||
}));
|
||||
}
|
||||
};
|
||||
|
||||
return html`
|
||||
<style>
|
||||
</style>
|
||||
<div class="mainbox">
|
||||
Everything is working normally!
|
||||
<div class="statusbar-container">
|
||||
${this.loading ? html`
|
||||
<div class="loading-skeleton"></div>
|
||||
` : html`
|
||||
<div class="statusbar-inner ${this.overallStatus.status}" @click=${handleClick}>
|
||||
<div class="status-content">
|
||||
<div class="status-main">
|
||||
<span class="status-icon">${getStatusIcon()}</span>
|
||||
<span>${this.overallStatus.message}</span>
|
||||
</div>
|
||||
${this.overallStatus.affectedServices > 0 ? html`
|
||||
<div class="status-details">
|
||||
${this.overallStatus.affectedServices} of ${this.overallStatus.totalServices} services affected
|
||||
</div>
|
||||
` : ''}
|
||||
</div>
|
||||
</div>
|
||||
<div class="last-updated">
|
||||
Last updated: ${formatLastUpdated()}
|
||||
</div>
|
||||
`}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
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>
|
||||
`;
|
||||
@@ -7,9 +7,13 @@ import {
|
||||
type TemplateResult,
|
||||
css,
|
||||
cssManager,
|
||||
unsafeCSS,
|
||||
} from '@design.estate/dees-element';
|
||||
import type { IStatusHistoryPoint } from '../interfaces/index.js';
|
||||
import { fonts, colors, shadows, borderRadius, spacing, commonStyles, getStatusColor } from '../styles/shared.styles.js';
|
||||
|
||||
import './internal/uplinternal-miniheading.js';
|
||||
import { demoFunc } from './upl-statuspage-statusdetails.demo.js';
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
@@ -19,7 +23,22 @@ declare global {
|
||||
|
||||
@customElement('upl-statuspage-statusdetails')
|
||||
export class UplStatuspageStatusdetails extends DeesElement {
|
||||
public static demo = () => html` <upl-statuspage-statusdetails></upl-statuspage-statusdetails> `;
|
||||
public static demo = demoFunc;
|
||||
|
||||
@property({ type: Array })
|
||||
public historyData: IStatusHistoryPoint[] = [];
|
||||
|
||||
@property({ type: String })
|
||||
public serviceId: string = '';
|
||||
|
||||
@property({ type: String })
|
||||
public serviceName: string = 'Service';
|
||||
|
||||
@property({ type: Boolean })
|
||||
public loading: boolean = false;
|
||||
|
||||
@property({ type: Number })
|
||||
public hoursToShow: number = 48;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
@@ -27,69 +46,342 @@ export class UplStatuspageStatusdetails extends DeesElement {
|
||||
|
||||
public static styles = [
|
||||
plugins.domtools.elementBasic.staticStyles,
|
||||
commonStyles,
|
||||
css`
|
||||
:host {
|
||||
position: relative;
|
||||
padding: 0px 0px 15px 0px;
|
||||
display: block;
|
||||
background: ${cssManager.bdTheme('#eeeeeb', '#222222')};;
|
||||
font-family: Inter;
|
||||
color: #fff;
|
||||
background: transparent;
|
||||
font-family: ${unsafeCSS(fonts.base)};
|
||||
color: ${colors.text.primary};
|
||||
}
|
||||
|
||||
.container {
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
padding: 0 ${unsafeCSS(spacing.lg)} ${unsafeCSS(spacing.lg)} ${unsafeCSS(spacing.lg)};
|
||||
}
|
||||
|
||||
.mainbox {
|
||||
margin: auto;
|
||||
max-width: 900px;
|
||||
text-align: right;
|
||||
background: ${cssManager.bdTheme('#ffffff', '#333333')};;
|
||||
line-height: 50px;
|
||||
border-radius: 3px;
|
||||
background: ${colors.background.card};
|
||||
border: 1px solid ${colors.border.default};
|
||||
border-radius: ${unsafeCSS(borderRadius.md)};
|
||||
padding: ${unsafeCSS(spacing.md)};
|
||||
box-shadow: ${unsafeCSS(shadows.sm)};
|
||||
}
|
||||
|
||||
.mainbox .barContainer {
|
||||
position: relative;
|
||||
display: flex;
|
||||
padding: 6px;
|
||||
gap: 2px;
|
||||
padding: ${unsafeCSS(spacing.sm)};
|
||||
background: ${colors.background.secondary};
|
||||
border-radius: ${unsafeCSS(borderRadius.base)};
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.mainbox .barContainer .bar {
|
||||
margin: 4px;
|
||||
width: 11px;
|
||||
border-radius: 3px;
|
||||
height: 40px;
|
||||
background: #2deb51;
|
||||
flex: 1;
|
||||
height: 48px;
|
||||
border-radius: ${unsafeCSS(borderRadius.sm)};
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
position: relative;
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
.mainbox .barContainer .bar:hover {
|
||||
opacity: 1;
|
||||
transform: scaleY(1.05);
|
||||
}
|
||||
|
||||
.mainbox .barContainer .bar.operational {
|
||||
background: ${colors.status.operational};
|
||||
}
|
||||
|
||||
.mainbox .barContainer .bar.degraded {
|
||||
background: ${colors.status.degraded};
|
||||
}
|
||||
|
||||
.mainbox .barContainer .bar.partial_outage {
|
||||
background: ${colors.status.partial};
|
||||
}
|
||||
|
||||
.mainbox .barContainer .bar.major_outage {
|
||||
background: ${colors.status.major};
|
||||
}
|
||||
|
||||
.mainbox .barContainer .bar.maintenance {
|
||||
background: ${colors.status.maintenance};
|
||||
}
|
||||
|
||||
.mainbox .barContainer .bar.no-data {
|
||||
background: ${colors.border.muted};
|
||||
opacity: 0.3;
|
||||
}
|
||||
|
||||
.timeIndicator {
|
||||
position: absolute;
|
||||
width: 11px;
|
||||
height: 11px;
|
||||
background: #FF9800;
|
||||
top: 56px;
|
||||
left: 400px;
|
||||
transform: rotate(45deg);
|
||||
width: 2px;
|
||||
height: calc(100% - ${unsafeCSS(spacing.md)});
|
||||
background: ${colors.text.primary};
|
||||
top: ${unsafeCSS(spacing.sm)};
|
||||
transition: left 0.3s;
|
||||
opacity: 0.5;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.time-labels {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
padding: ${unsafeCSS(spacing.sm)} ${unsafeCSS(spacing.sm)} 0;
|
||||
font-size: 12px;
|
||||
color: ${colors.text.secondary};
|
||||
font-family: ${unsafeCSS(fonts.mono)};
|
||||
}
|
||||
|
||||
.tooltip {
|
||||
position: absolute;
|
||||
background: ${cssManager.bdTheme('#1a1a1a', '#ffffff')};
|
||||
color: ${cssManager.bdTheme('#ffffff', '#1a1a1a')};
|
||||
padding: ${unsafeCSS(spacing.sm)} ${unsafeCSS(spacing.md)};
|
||||
border-radius: ${unsafeCSS(borderRadius.base)};
|
||||
font-size: 13px;
|
||||
pointer-events: none;
|
||||
opacity: 0;
|
||||
transition: opacity 0.2s;
|
||||
z-index: 10;
|
||||
white-space: nowrap;
|
||||
box-shadow: ${unsafeCSS(shadows.lg)};
|
||||
border: 1px solid ${colors.border.default};
|
||||
}
|
||||
|
||||
.tooltip.visible {
|
||||
opacity: 0.95;
|
||||
}
|
||||
|
||||
.tooltip strong {
|
||||
font-weight: 600;
|
||||
display: block;
|
||||
margin-bottom: ${unsafeCSS(spacing.xs)};
|
||||
color: ${cssManager.bdTheme('#ffffff', '#1a1a1a')};
|
||||
}
|
||||
|
||||
.tooltip div {
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.loading-skeleton {
|
||||
display: flex;
|
||||
padding: ${unsafeCSS(spacing.sm)};
|
||||
gap: 2px;
|
||||
background: ${colors.background.secondary};
|
||||
border-radius: ${unsafeCSS(borderRadius.base)};
|
||||
}
|
||||
|
||||
.loading-skeleton .skeleton-bar {
|
||||
flex: 1;
|
||||
height: 48px;
|
||||
background: ${cssManager.bdTheme(
|
||||
'linear-gradient(90deg, #f3f4f6 25%, #e5e7eb 50%, #f3f4f6 75%)',
|
||||
'linear-gradient(90deg, #1f1f1f 25%, #262626 50%, #1f1f1f 75%)'
|
||||
)};
|
||||
background-size: 200% 100%;
|
||||
animation: loading 1.5s infinite;
|
||||
border-radius: ${unsafeCSS(borderRadius.sm)};
|
||||
}
|
||||
|
||||
@keyframes loading {
|
||||
0% { background-position: 200% 0; }
|
||||
100% { background-position: -200% 0; }
|
||||
}
|
||||
|
||||
.status-legend {
|
||||
display: flex;
|
||||
gap: ${unsafeCSS(spacing.lg)};
|
||||
justify-content: center;
|
||||
margin-top: ${unsafeCSS(spacing.lg)};
|
||||
font-size: 13px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.legend-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: ${unsafeCSS(spacing.xs)};
|
||||
}
|
||||
|
||||
.legend-color {
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
border-radius: ${unsafeCSS(borderRadius.sm)};
|
||||
}
|
||||
|
||||
@media (max-width: 640px) {
|
||||
.container {
|
||||
padding: 0 ${unsafeCSS(spacing.md)} ${unsafeCSS(spacing.md)} ${unsafeCSS(spacing.md)};
|
||||
}
|
||||
|
||||
.mainbox {
|
||||
padding: ${unsafeCSS(spacing.sm)};
|
||||
}
|
||||
|
||||
.time-labels {
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
.status-legend {
|
||||
gap: ${unsafeCSS(spacing.md)};
|
||||
font-size: 12px;
|
||||
}
|
||||
}
|
||||
`,
|
||||
];
|
||||
|
||||
public render(): TemplateResult {
|
||||
const now = Date.now();
|
||||
const currentHour = new Date().getHours();
|
||||
const timeIndicatorPosition = this.calculateTimeIndicatorPosition();
|
||||
|
||||
return html`
|
||||
<style></style>
|
||||
<uplinternal-miniheading>Yesterday & Today</uplinternal-miniheading>
|
||||
<uplinternal-miniheading>${this.serviceName} - Last ${this.hoursToShow} Hours</uplinternal-miniheading>
|
||||
<div class="mainbox">
|
||||
<div class="barContainer">
|
||||
${(() => {
|
||||
let counter = 0;
|
||||
const returnArray: TemplateResult[] = [];
|
||||
while (counter < 48) {
|
||||
counter++;
|
||||
returnArray.push(html` <div class="bar"></div> `);
|
||||
}
|
||||
return returnArray;
|
||||
})()}
|
||||
<div class="timeIndicator"></div>
|
||||
</div>
|
||||
${this.loading ? html`
|
||||
<div class="loading-skeleton">
|
||||
${Array(this.hoursToShow).fill(0).map(() => html`<div class="skeleton-bar"></div>`)}
|
||||
</div>
|
||||
` : html`
|
||||
<div class="barContainer" @mouseleave=${this.hideTooltip}>
|
||||
${this.renderBars()}
|
||||
<div class="timeIndicator" style="left: ${timeIndicatorPosition}px"></div>
|
||||
</div>
|
||||
<div class="time-labels">
|
||||
<span>${this.getTimeLabel(0)}</span>
|
||||
<span>${this.getTimeLabel(Math.floor(this.hoursToShow / 4))}</span>
|
||||
<span>${this.getTimeLabel(Math.floor(this.hoursToShow / 2))}</span>
|
||||
<span>${this.getTimeLabel(Math.floor(this.hoursToShow * 3 / 4))}</span>
|
||||
<span>Now</span>
|
||||
</div>
|
||||
`}
|
||||
</div>
|
||||
${!this.loading ? html`
|
||||
<div class="status-legend">
|
||||
<div class="legend-item">
|
||||
<div class="legend-color" style="background: #2deb51"></div>
|
||||
<span>Operational</span>
|
||||
</div>
|
||||
<div class="legend-item">
|
||||
<div class="legend-color" style="background: #FF9800"></div>
|
||||
<span>Degraded</span>
|
||||
</div>
|
||||
<div class="legend-item">
|
||||
<div class="legend-color" style="background: #FF6F00"></div>
|
||||
<span>Partial Outage</span>
|
||||
</div>
|
||||
<div class="legend-item">
|
||||
<div class="legend-color" style="background: #F44336"></div>
|
||||
<span>Major Outage</span>
|
||||
</div>
|
||||
<div class="legend-item">
|
||||
<div class="legend-color" style="background: #2196F3"></div>
|
||||
<span>Maintenance</span>
|
||||
</div>
|
||||
</div>
|
||||
` : ''}
|
||||
<div class="tooltip" id="tooltip"></div>
|
||||
`;
|
||||
}
|
||||
|
||||
private renderBars(): TemplateResult[] {
|
||||
const bars: TemplateResult[] = [];
|
||||
const now = Date.now();
|
||||
|
||||
for (let i = 0; i < this.hoursToShow; i++) {
|
||||
const hourIndex = this.hoursToShow - 1 - i;
|
||||
const timestamp = now - (hourIndex * 60 * 60 * 1000);
|
||||
const dataPoint = this.findDataPointForTime(timestamp);
|
||||
|
||||
const status = dataPoint?.status || 'no-data';
|
||||
const responseTime = dataPoint?.responseTime || 0;
|
||||
|
||||
bars.push(html`
|
||||
<div
|
||||
class="bar ${status}"
|
||||
@mouseenter=${(e: MouseEvent) => this.showTooltip(e, timestamp, status, responseTime)}
|
||||
@click=${() => this.handleBarClick(timestamp, status, responseTime)}
|
||||
></div>
|
||||
`);
|
||||
}
|
||||
|
||||
return bars;
|
||||
}
|
||||
|
||||
private findDataPointForTime(timestamp: number): IStatusHistoryPoint | undefined {
|
||||
if (!this.historyData || this.historyData.length === 0) return undefined;
|
||||
|
||||
// Find the closest data point within the same hour
|
||||
const targetHour = new Date(timestamp).getHours();
|
||||
const targetDate = new Date(timestamp).toDateString();
|
||||
|
||||
return this.historyData.find(point => {
|
||||
const pointDate = new Date(point.timestamp);
|
||||
return pointDate.toDateString() === targetDate &&
|
||||
pointDate.getHours() === targetHour;
|
||||
});
|
||||
}
|
||||
|
||||
private calculateTimeIndicatorPosition(): number {
|
||||
const containerWidth = 888; // Approximate width minus padding
|
||||
const barWidth = 19; // Width + margin
|
||||
const totalBars = this.hoursToShow;
|
||||
const currentMinutes = new Date().getMinutes();
|
||||
const currentPosition = (totalBars - 1 + currentMinutes / 60) * barWidth + 6;
|
||||
return Math.min(currentPosition, containerWidth - 11);
|
||||
}
|
||||
|
||||
private getTimeLabel(hoursAgo: number): string {
|
||||
const date = new Date(Date.now() - (hoursAgo * 60 * 60 * 1000));
|
||||
if (hoursAgo >= 24) {
|
||||
return `${date.getMonth() + 1}/${date.getDate()} ${date.getHours()}:00`;
|
||||
}
|
||||
return `${date.getHours()}:00`;
|
||||
}
|
||||
|
||||
private showTooltip(event: MouseEvent, timestamp: number, status: string, responseTime: number) {
|
||||
const tooltip = this.shadowRoot?.getElementById('tooltip') as HTMLElement;
|
||||
if (!tooltip) return;
|
||||
|
||||
const date = new Date(timestamp);
|
||||
const timeStr = date.toLocaleString();
|
||||
const statusStr = status.replace(/_/g, ' ').replace('no-data', 'No Data');
|
||||
|
||||
tooltip.innerHTML = `
|
||||
<div><strong>${timeStr}</strong></div>
|
||||
<div>Status: ${statusStr}</div>
|
||||
${responseTime > 0 ? `<div>Response Time: ${responseTime.toFixed(0)}ms</div>` : ''}
|
||||
`;
|
||||
|
||||
const rect = (event.target as HTMLElement).getBoundingClientRect();
|
||||
const containerRect = this.getBoundingClientRect();
|
||||
|
||||
tooltip.style.left = `${rect.left - containerRect.left + rect.width / 2}px`;
|
||||
tooltip.style.top = `${rect.top - containerRect.top - 60}px`;
|
||||
tooltip.style.transform = 'translateX(-50%)';
|
||||
tooltip.classList.add('visible');
|
||||
}
|
||||
|
||||
private hideTooltip() {
|
||||
const tooltip = this.shadowRoot?.getElementById('tooltip') as HTMLElement;
|
||||
if (tooltip) {
|
||||
tooltip.classList.remove('visible');
|
||||
}
|
||||
}
|
||||
|
||||
private handleBarClick(timestamp: number, status: string, responseTime: number) {
|
||||
this.dispatchEvent(new CustomEvent('barClick', {
|
||||
detail: { timestamp, status, responseTime, serviceId: this.serviceId },
|
||||
bubbles: true,
|
||||
composed: true
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
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>
|
||||
`;
|
||||
@@ -5,11 +5,15 @@ import {
|
||||
customElement,
|
||||
type TemplateResult,
|
||||
css,
|
||||
cssManager
|
||||
cssManager,
|
||||
unsafeCSS
|
||||
} from '@design.estate/dees-element';
|
||||
import * as domtools from '@design.estate/dees-domtools';
|
||||
import type { IMonthlyUptime } from '../interfaces/index.js';
|
||||
import { fonts, colors, shadows, borderRadius, spacing, commonStyles } from '../styles/shared.styles.js';
|
||||
|
||||
import './internal/uplinternal-miniheading.js';
|
||||
import { demoFunc } from './upl-statuspage-statusmonth.demo.js';
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
@@ -19,7 +23,25 @@ declare global {
|
||||
|
||||
@customElement('upl-statuspage-statusmonth')
|
||||
export class UplStatuspageStatusmonth extends DeesElement {
|
||||
public static demo = () => html` <upl-statuspage-statusmonth></upl-statuspage-statusmonth> `;
|
||||
public static demo = demoFunc;
|
||||
|
||||
@property({ type: Array })
|
||||
public monthlyData: IMonthlyUptime[] = [];
|
||||
|
||||
@property({ type: String })
|
||||
public serviceId: string = '';
|
||||
|
||||
@property({ type: String })
|
||||
public serviceName: string = 'Service';
|
||||
|
||||
@property({ type: Boolean })
|
||||
public loading: boolean = false;
|
||||
|
||||
@property({ type: Boolean })
|
||||
public showTooltip: boolean = true;
|
||||
|
||||
@property({ type: Number })
|
||||
public monthsToShow: number = 5;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
@@ -27,104 +49,310 @@ export class UplStatuspageStatusmonth extends DeesElement {
|
||||
|
||||
public static styles = [
|
||||
domtools.elementBasic.staticStyles,
|
||||
commonStyles,
|
||||
css`
|
||||
:host {
|
||||
position: relative;
|
||||
padding: 0px 0px 15px 0px;
|
||||
display: block;
|
||||
background: ${cssManager.bdTheme('#eeeeeb', '#222222')};;
|
||||
font-family: Inter;
|
||||
color: #fff;
|
||||
background: transparent;
|
||||
font-family: ${unsafeCSS(fonts.base)};
|
||||
color: ${colors.text.primary};
|
||||
}
|
||||
|
||||
.container {
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
padding: 0 ${unsafeCSS(spacing.lg)} ${unsafeCSS(spacing.lg)} ${unsafeCSS(spacing.lg)};
|
||||
}
|
||||
|
||||
.mainbox {
|
||||
margin: auto;
|
||||
max-width: 900px;
|
||||
display: grid;
|
||||
grid-template-columns: repeat(5, calc(100% / 5 - 80px / 5));
|
||||
grid-column-gap: 20px;
|
||||
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
||||
gap: ${unsafeCSS(spacing.lg)};
|
||||
}
|
||||
|
||||
.statusMonth {
|
||||
background: ${cssManager.bdTheme('#ffffff', '#333333')};;
|
||||
min-height: 20px;
|
||||
display: grid;
|
||||
padding: 10px;
|
||||
grid-template-columns: repeat(6, auto);
|
||||
grid-gap: 9px;
|
||||
border-radius: 3px;
|
||||
background: ${colors.background.card};
|
||||
padding: ${unsafeCSS(spacing.lg)};
|
||||
border-radius: ${unsafeCSS(borderRadius.md)};
|
||||
border: 1px solid ${colors.border.default};
|
||||
box-shadow: ${unsafeCSS(shadows.sm)};
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.statusMonth .statusDay {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
background: #2deb51;
|
||||
border-radius: 3px;
|
||||
.month-header {
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
margin-bottom: ${unsafeCSS(spacing.md)};
|
||||
text-align: center;
|
||||
color: ${colors.text.primary};
|
||||
letter-spacing: 0.02em;
|
||||
}
|
||||
|
||||
.days-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(7, 1fr);
|
||||
gap: 3px;
|
||||
}
|
||||
|
||||
.weekday-label {
|
||||
font-size: 12px;
|
||||
text-align: center;
|
||||
color: ${colors.text.secondary};
|
||||
font-weight: 500;
|
||||
height: 20px;
|
||||
line-height: 20px;
|
||||
margin-bottom: ${unsafeCSS(spacing.xs)};
|
||||
}
|
||||
|
||||
.statusDay {
|
||||
aspect-ratio: 1;
|
||||
border-radius: ${unsafeCSS(borderRadius.sm)};
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.statusDay:hover {
|
||||
transform: scale(1.15);
|
||||
box-shadow: ${unsafeCSS(shadows.md)};
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.statusDay.operational {
|
||||
background: ${colors.status.operational};
|
||||
}
|
||||
|
||||
.statusDay.degraded {
|
||||
background: ${colors.status.degraded};
|
||||
}
|
||||
|
||||
.statusDay.partial_outage {
|
||||
background: ${colors.status.partial};
|
||||
}
|
||||
|
||||
.statusDay.major_outage {
|
||||
background: ${colors.status.major};
|
||||
}
|
||||
|
||||
.statusDay.no-data {
|
||||
background: ${colors.border.muted};
|
||||
opacity: 0.3;
|
||||
}
|
||||
|
||||
.statusDay.empty {
|
||||
background: transparent;
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.statusDay.empty:hover {
|
||||
transform: none;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.overall-uptime {
|
||||
text-align: center;
|
||||
font-size: 13px;
|
||||
margin-top: ${unsafeCSS(spacing.md)};
|
||||
color: ${colors.text.secondary};
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.loading-skeleton {
|
||||
height: 280px;
|
||||
background: ${cssManager.bdTheme(
|
||||
'linear-gradient(90deg, #f3f4f6 25%, #e5e7eb 50%, #f3f4f6 75%)',
|
||||
'linear-gradient(90deg, #1f1f1f 25%, #262626 50%, #1f1f1f 75%)'
|
||||
)};
|
||||
background-size: 200% 100%;
|
||||
animation: loading 1.5s infinite;
|
||||
border-radius: ${unsafeCSS(borderRadius.md)};
|
||||
}
|
||||
|
||||
@keyframes loading {
|
||||
0% { background-position: 200% 0; }
|
||||
100% { background-position: -200% 0; }
|
||||
}
|
||||
|
||||
.tooltip {
|
||||
position: absolute;
|
||||
background: ${cssManager.bdTheme('#1a1a1a', '#ffffff')};
|
||||
color: ${cssManager.bdTheme('#ffffff', '#1a1a1a')};
|
||||
padding: ${unsafeCSS(spacing.sm)} ${unsafeCSS(spacing.md)};
|
||||
border-radius: ${unsafeCSS(borderRadius.base)};
|
||||
font-size: 12px;
|
||||
pointer-events: none;
|
||||
opacity: 0;
|
||||
transition: opacity 0.2s;
|
||||
z-index: 10;
|
||||
white-space: nowrap;
|
||||
box-shadow: ${unsafeCSS(shadows.lg)};
|
||||
border: 1px solid ${colors.border.default};
|
||||
}
|
||||
|
||||
.tooltip.visible {
|
||||
opacity: 0.95;
|
||||
}
|
||||
|
||||
.tooltip strong {
|
||||
font-weight: 600;
|
||||
display: block;
|
||||
margin-bottom: ${unsafeCSS(spacing.xs)};
|
||||
color: ${cssManager.bdTheme('#ffffff', '#1a1a1a')};
|
||||
}
|
||||
|
||||
.no-data-message {
|
||||
grid-column: 1 / -1;
|
||||
text-align: center;
|
||||
padding: ${unsafeCSS(spacing['2xl'])};
|
||||
color: ${colors.text.secondary};
|
||||
}
|
||||
|
||||
@media (max-width: 640px) {
|
||||
.container {
|
||||
padding: 0 ${unsafeCSS(spacing.md)} ${unsafeCSS(spacing.md)} ${unsafeCSS(spacing.md)};
|
||||
}
|
||||
|
||||
.mainbox {
|
||||
grid-template-columns: 1fr;
|
||||
gap: ${unsafeCSS(spacing.md)};
|
||||
}
|
||||
|
||||
.statusMonth {
|
||||
padding: ${unsafeCSS(spacing.md)};
|
||||
}
|
||||
}
|
||||
`
|
||||
]
|
||||
|
||||
public render(): TemplateResult {
|
||||
const totalDays = this.monthlyData.reduce((sum, month) => sum + month.days.length, 0);
|
||||
|
||||
return html`
|
||||
<style></style>
|
||||
<uplinternal-miniheading>Last 150 days</uplinternal-miniheading>
|
||||
<div class="mainbox">
|
||||
<div class="statusMonth">
|
||||
${(() => {
|
||||
let counter = 0;
|
||||
const returnArray: TemplateResult[] = [];
|
||||
while (counter < 30) {
|
||||
counter++;
|
||||
returnArray.push(html` <div class="statusDay"></div> `);
|
||||
}
|
||||
return returnArray;
|
||||
})()}
|
||||
<div class="container">
|
||||
<uplinternal-miniheading>${this.serviceName} - Last ${totalDays} Days</uplinternal-miniheading>
|
||||
<div class="mainbox">
|
||||
${this.loading ? html`
|
||||
${Array(this.monthsToShow).fill(0).map(() => html`
|
||||
<div class="statusMonth">
|
||||
<div class="loading-skeleton"></div>
|
||||
</div>
|
||||
`)}
|
||||
` : this.monthlyData.length === 0 ? html`
|
||||
<div class="no-data-message">No uptime data available</div>
|
||||
` : this.monthlyData.map(month => this.renderMonth(month))}
|
||||
</div>
|
||||
<div class="statusMonth">
|
||||
${(() => {
|
||||
let counter = 0;
|
||||
const returnArray: TemplateResult[] = [];
|
||||
while (counter < 30) {
|
||||
counter++;
|
||||
returnArray.push(html` <div class="statusDay"></div> `);
|
||||
}
|
||||
return returnArray;
|
||||
})()}
|
||||
${this.showTooltip ? html`<div class="tooltip" id="tooltip"></div>` : ''}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
private renderMonth(monthData: IMonthlyUptime): TemplateResult {
|
||||
const monthDate = new Date(monthData.month + '-01');
|
||||
const monthName = monthDate.toLocaleDateString('en-US', { month: 'short', year: 'numeric' });
|
||||
const firstDayOfWeek = new Date(monthDate.getFullYear(), monthDate.getMonth(), 1).getDay();
|
||||
|
||||
return html`
|
||||
<div class="statusMonth" @mouseleave=${this.hideTooltip}>
|
||||
<div class="month-header">${monthName}</div>
|
||||
<div class="days-grid">
|
||||
${this.renderWeekdayLabels()}
|
||||
${this.renderEmptyDays(firstDayOfWeek)}
|
||||
${monthData.days.map(day => this.renderDay(day))}
|
||||
</div>
|
||||
<div class="statusMonth">
|
||||
${(() => {
|
||||
let counter = 0;
|
||||
const returnArray: TemplateResult[] = [];
|
||||
while (counter < 30) {
|
||||
counter++;
|
||||
returnArray.push(html` <div class="statusDay"></div> `);
|
||||
}
|
||||
return returnArray;
|
||||
})()}
|
||||
</div>
|
||||
<div class="statusMonth">
|
||||
${(() => {
|
||||
let counter = 0;
|
||||
const returnArray: TemplateResult[] = [];
|
||||
while (counter < 30) {
|
||||
counter++;
|
||||
returnArray.push(html` <div class="statusDay"></div> `);
|
||||
}
|
||||
return returnArray;
|
||||
})()}
|
||||
</div>
|
||||
<div class="statusMonth">
|
||||
${(() => {
|
||||
let counter = 0;
|
||||
const returnArray: TemplateResult[] = [];
|
||||
while (counter < 30) {
|
||||
counter++;
|
||||
returnArray.push(html` <div class="statusDay"></div> `);
|
||||
}
|
||||
return returnArray;
|
||||
})()}
|
||||
<div class="overall-uptime">
|
||||
${monthData.overallUptime.toFixed(2)}% uptime
|
||||
${monthData.totalIncidents > 0 ? html`<br/>${monthData.totalIncidents} incidents` : ''}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
private renderWeekdayLabels(): TemplateResult[] {
|
||||
const weekdays = ['S', 'M', 'T', 'W', 'T', 'F', 'S'];
|
||||
return weekdays.map(day => html`<div class="weekday-label">${day}</div>`);
|
||||
}
|
||||
|
||||
private renderEmptyDays(count: number): TemplateResult[] {
|
||||
return Array(count).fill(0).map(() => html`<div class="statusDay empty"></div>`);
|
||||
}
|
||||
|
||||
private renderDay(day: any): TemplateResult {
|
||||
const status = day.status || 'no-data';
|
||||
const date = new Date(day.date);
|
||||
const dayNumber = date.getDate();
|
||||
|
||||
return html`
|
||||
<div
|
||||
class="statusDay ${status}"
|
||||
@mouseenter=${(e: MouseEvent) => this.showTooltip && this.showDayTooltip(e, day)}
|
||||
@click=${() => this.handleDayClick(day)}
|
||||
>
|
||||
${status === 'major_outage' || status === 'partial_outage' ? html`
|
||||
<div style="
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
font-size: 8px;
|
||||
font-weight: bold;
|
||||
color: white;
|
||||
">${day.incidents}</div>
|
||||
` : ''}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
private showDayTooltip(event: MouseEvent, day: any) {
|
||||
const tooltip = this.shadowRoot?.getElementById('tooltip') as HTMLElement;
|
||||
if (!tooltip) return;
|
||||
|
||||
const date = new Date(day.date);
|
||||
const dateStr = date.toLocaleDateString('en-US', {
|
||||
weekday: 'long',
|
||||
year: 'numeric',
|
||||
month: 'long',
|
||||
day: 'numeric'
|
||||
});
|
||||
|
||||
let statusText = day.status.replace(/_/g, ' ');
|
||||
statusText = statusText.charAt(0).toUpperCase() + statusText.slice(1);
|
||||
|
||||
tooltip.innerHTML = `
|
||||
<div><strong>${dateStr}</strong></div>
|
||||
<div>Status: ${statusText}</div>
|
||||
<div>Uptime: ${day.uptime.toFixed(2)}%</div>
|
||||
${day.incidents > 0 ? `<div>Incidents: ${day.incidents}</div>` : ''}
|
||||
${day.totalDowntime > 0 ? `<div>Downtime: ${day.totalDowntime} minutes</div>` : ''}
|
||||
`;
|
||||
|
||||
const rect = (event.target as HTMLElement).getBoundingClientRect();
|
||||
const containerRect = this.getBoundingClientRect();
|
||||
|
||||
tooltip.style.left = `${rect.left - containerRect.left + rect.width / 2}px`;
|
||||
tooltip.style.top = `${rect.top - containerRect.top - 80}px`;
|
||||
tooltip.style.transform = 'translateX(-50%)';
|
||||
tooltip.classList.add('visible');
|
||||
}
|
||||
|
||||
private hideTooltip() {
|
||||
const tooltip = this.shadowRoot?.getElementById('tooltip') as HTMLElement;
|
||||
if (tooltip) {
|
||||
tooltip.classList.remove('visible');
|
||||
}
|
||||
}
|
||||
|
||||
private handleDayClick(day: any) {
|
||||
this.dispatchEvent(new CustomEvent('dayClick', {
|
||||
detail: {
|
||||
date: day.date,
|
||||
uptime: day.uptime,
|
||||
incidents: day.incidents,
|
||||
status: day.status,
|
||||
serviceId: this.serviceId
|
||||
},
|
||||
bubbles: true,
|
||||
composed: true
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
181
ts_web/styles/shared.styles.ts
Normal file
181
ts_web/styles/shared.styles.ts
Normal file
@@ -0,0 +1,181 @@
|
||||
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', '#0a0a0a'),
|
||||
secondary: cssManager.bdTheme('#f9fafb', '#18181b'),
|
||||
muted: cssManager.bdTheme('#f3f4f6', '#27272a'),
|
||||
card: cssManager.bdTheme('#ffffff', '#18181b')
|
||||
},
|
||||
|
||||
// Border colors
|
||||
border: {
|
||||
default: cssManager.bdTheme('#e5e7eb', '#27272a'),
|
||||
muted: cssManager.bdTheme('#f3f4f6', '#3f3f46')
|
||||
},
|
||||
|
||||
// Text colors
|
||||
text: {
|
||||
primary: cssManager.bdTheme('#0a0a0a', '#fafafa'),
|
||||
secondary: cssManager.bdTheme('#6b7280', '#a1a1aa'),
|
||||
muted: cssManager.bdTheme('#9ca3af', '#71717a')
|
||||
},
|
||||
|
||||
// Status colors
|
||||
status: {
|
||||
operational: cssManager.bdTheme('#10b981', '#064e3b'),
|
||||
degraded: cssManager.bdTheme('#f59e0b', '#78350f'),
|
||||
partial: cssManager.bdTheme('#ef4444', '#7f1d1d'),
|
||||
major: cssManager.bdTheme('#dc2626', '#450a0a'),
|
||||
maintenance: cssManager.bdTheme('#3b82f6', '#1e3a8a')
|
||||
}
|
||||
};
|
||||
|
||||
export const shadows = {
|
||||
sm: '0 1px 2px 0 rgba(0, 0, 0, 0.05)',
|
||||
base: '0 1px 3px rgba(0, 0, 0, 0.1), 0 1px 2px rgba(0, 0, 0, 0.06)',
|
||||
md: '0 4px 6px rgba(0, 0, 0, 0.1), 0 2px 4px rgba(0, 0, 0, 0.06)',
|
||||
lg: '0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05)',
|
||||
xl: '0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04)'
|
||||
};
|
||||
|
||||
export const borderRadius = {
|
||||
sm: '4px',
|
||||
base: '6px',
|
||||
md: '8px',
|
||||
lg: '12px',
|
||||
xl: '16px',
|
||||
full: '9999px'
|
||||
};
|
||||
|
||||
export const spacing = {
|
||||
xs: '4px',
|
||||
sm: '8px',
|
||||
md: '16px',
|
||||
lg: '24px',
|
||||
xl: '32px',
|
||||
'2xl': '48px',
|
||||
'3xl': '64px'
|
||||
};
|
||||
|
||||
export const commonStyles = css`
|
||||
/* Button styles */
|
||||
.button {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-family: ${unsafeCSS(fonts.base)};
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
height: 36px;
|
||||
padding: 0 16px;
|
||||
border-radius: ${unsafeCSS(borderRadius.base)};
|
||||
border: 1px solid ${colors.border.default};
|
||||
background: transparent;
|
||||
color: ${colors.text.primary};
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
transition: all 0.2s ease;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.button:hover {
|
||||
background: ${cssManager.bdTheme('#f9fafb', '#262626')};
|
||||
border-color: ${cssManager.bdTheme('#d1d5db', '#404040')};
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.button:active {
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
.button.primary {
|
||||
background: ${colors.text.primary};
|
||||
color: ${colors.background.primary};
|
||||
border-color: ${colors.text.primary};
|
||||
}
|
||||
|
||||
.button.primary:hover {
|
||||
background: ${cssManager.bdTheme('#262626', '#f4f4f5')};
|
||||
border-color: ${cssManager.bdTheme('#262626', '#f4f4f5')};
|
||||
}
|
||||
|
||||
/* Card styles */
|
||||
.card {
|
||||
background: ${colors.background.card};
|
||||
border: 1px solid ${colors.border.default};
|
||||
border-radius: ${unsafeCSS(borderRadius.md)};
|
||||
padding: ${unsafeCSS(spacing.lg)};
|
||||
box-shadow: ${unsafeCSS(shadows.sm)};
|
||||
}
|
||||
|
||||
/* Loading skeleton */
|
||||
.skeleton {
|
||||
background: ${cssManager.bdTheme(
|
||||
'linear-gradient(90deg, #f3f4f6 25%, #e5e7eb 50%, #f3f4f6 75%)',
|
||||
'linear-gradient(90deg, #1f1f1f 25%, #262626 50%, #1f1f1f 75%)'
|
||||
)};
|
||||
background-size: 200% 100%;
|
||||
animation: skeleton-loading 1.5s infinite;
|
||||
border-radius: ${unsafeCSS(borderRadius.base)};
|
||||
}
|
||||
|
||||
@keyframes skeleton-loading {
|
||||
0% { background-position: 200% 0; }
|
||||
100% { background-position: -200% 0; }
|
||||
}
|
||||
|
||||
/* Container styles */
|
||||
.container {
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
padding: 0 ${unsafeCSS(spacing.lg)};
|
||||
}
|
||||
|
||||
/* Responsive utilities */
|
||||
@media (max-width: 640px) {
|
||||
.container {
|
||||
padding: 0 ${unsafeCSS(spacing.md)};
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
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 '?';
|
||||
}
|
||||
};
|
||||
Reference in New Issue
Block a user