update
This commit is contained in:
@@ -1,6 +1,8 @@
|
|||||||
// dees tools
|
// dees tools
|
||||||
import * as deesWccTools from '@design.estate/dees-wcctools';
|
import * as deesWccTools from '@design.estate/dees-wcctools';
|
||||||
import * as deesDomTools from '@design.estate/dees-domtools';
|
import * as deesDomTools from '@design.estate/dees-domtools';
|
||||||
|
// Import demotools to register dees-demowrapper
|
||||||
|
import '@design.estate/dees-wcctools/demotools';
|
||||||
|
|
||||||
// elements and pages
|
// elements and pages
|
||||||
import * as elements from '../ts_web/elements/index.js';
|
import * as elements from '../ts_web/elements/index.js';
|
||||||
|
|||||||
16
package.json
16
package.json
@@ -15,19 +15,19 @@
|
|||||||
"author": "Lossless GmbH",
|
"author": "Lossless GmbH",
|
||||||
"license": "UNLICENSED",
|
"license": "UNLICENSED",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@design.estate/dees-domtools": "^2.0.1",
|
"@design.estate/dees-domtools": "^2.3.3",
|
||||||
"@design.estate/dees-element": "^2.0.4",
|
"@design.estate/dees-element": "^2.0.45",
|
||||||
"@design.estate/dees-wcctools": "^1.0.73",
|
"@design.estate/dees-wcctools": "^1.1.0",
|
||||||
"@uptime.link/interfaces": "^2.0.21"
|
"@uptime.link/interfaces": "^2.0.21"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@git.zone/tsbuild": "^2.1.61",
|
"@git.zone/tsbuild": "^2.6.4",
|
||||||
"@git.zone/tsbundle": "^2.0.7",
|
"@git.zone/tsbundle": "^2.5.1",
|
||||||
"@git.zone/tsrun": "^1.2.39",
|
"@git.zone/tsrun": "^1.3.3",
|
||||||
"@git.zone/tswatch": "^2.0.5",
|
"@git.zone/tswatch": "^2.1.2",
|
||||||
"@push.rocks/projectinfo": "^5.0.1",
|
"@push.rocks/projectinfo": "^5.0.1",
|
||||||
"@push.rocks/smartenv": "^5.0.0",
|
"@push.rocks/smartenv": "^5.0.0",
|
||||||
"@types/node": "^18.11.18"
|
"@types/node": "^24.0.7"
|
||||||
},
|
},
|
||||||
"files": [
|
"files": [
|
||||||
"ts/**/*",
|
"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-assetsselector.js';
|
||||||
export * from './upl-statuspage-footer.js';
|
export * from './upl-statuspage-footer.js';
|
||||||
export * from './upl-statuspage-header.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-statusbar.js';
|
||||||
export * from './upl-statuspage-statusdetails.js';
|
export * from './upl-statuspage-statusdetails.js';
|
||||||
export * from './upl-statuspage-statusmonth.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 * as domtools from '@design.estate/dees-domtools';
|
||||||
|
import { fonts, colors, spacing } from '../../styles/shared.styles.js';
|
||||||
|
|
||||||
@customElement('uplinternal-miniheading')
|
@customElement('uplinternal-miniheading')
|
||||||
export class UplinternalMiniheading extends DeesElement {
|
export class UplinternalMiniheading extends DeesElement {
|
||||||
public render(): TemplateResult {
|
public static styles = [
|
||||||
return html`
|
domtools.elementBasic.staticStyles,
|
||||||
${domtools.elementBasic.styles}
|
css`
|
||||||
<style>
|
|
||||||
:host {
|
:host {
|
||||||
display: block;
|
display: block;
|
||||||
font-family: Inter;
|
font-family: ${unsafeCSS(fonts.base)};
|
||||||
}
|
}
|
||||||
|
|
||||||
h5 {
|
h5 {
|
||||||
display: block;
|
display: block;
|
||||||
max-width: 900px;
|
max-width: 1200px;
|
||||||
margin: 0px auto;
|
margin: 0px auto;
|
||||||
padding: 0px 0px 10px 0px;
|
padding: 0px 0px ${unsafeCSS(spacing.md)} 0px;
|
||||||
color: #707070;
|
color: ${colors.text.secondary};
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 600;
|
||||||
|
letter-spacing: 0.025em;
|
||||||
|
text-transform: uppercase;
|
||||||
}
|
}
|
||||||
</style>
|
`
|
||||||
|
];
|
||||||
|
|
||||||
|
public render(): TemplateResult {
|
||||||
|
return html`
|
||||||
<h5>${this.textContent}</h5>
|
<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,
|
type TemplateResult,
|
||||||
cssManager,
|
cssManager,
|
||||||
css,
|
css,
|
||||||
|
unsafeCSS,
|
||||||
} from '@design.estate/dees-element';
|
} from '@design.estate/dees-element';
|
||||||
import * as domtools from '@design.estate/dees-domtools';
|
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 './internal/uplinternal-miniheading.js';
|
||||||
|
import { demoFunc } from './upl-statuspage-assetsselector.demo.js';
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
interface HTMLElementTagNameMap {
|
interface HTMLElementTagNameMap {
|
||||||
@@ -19,9 +23,22 @@ declare global {
|
|||||||
|
|
||||||
@customElement('upl-statuspage-assetsselector')
|
@customElement('upl-statuspage-assetsselector')
|
||||||
export class UplStatuspageAssetsselector extends DeesElement {
|
export class UplStatuspageAssetsselector extends DeesElement {
|
||||||
public static demo = () => html`
|
public static demo = demoFunc;
|
||||||
<upl-statuspage-assetsselector></upl-statuspage-assetsselector>
|
|
||||||
`;
|
@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() {
|
constructor() {
|
||||||
super();
|
super();
|
||||||
@@ -29,35 +46,398 @@ export class UplStatuspageAssetsselector extends DeesElement {
|
|||||||
|
|
||||||
public static styles = [
|
public static styles = [
|
||||||
cssManager.defaultStyles,
|
cssManager.defaultStyles,
|
||||||
|
commonStyles,
|
||||||
css`
|
css`
|
||||||
:host {
|
:host {
|
||||||
padding: 0px 0px 15px 0px;
|
|
||||||
display: block;
|
display: block;
|
||||||
background: ${cssManager.bdTheme('#eeeeeb', '#222222')};
|
background: transparent;
|
||||||
font-family: Inter;
|
font-family: ${unsafeCSS(fonts.base)};
|
||||||
color: #fff;
|
color: ${colors.text.primary};
|
||||||
}
|
}
|
||||||
|
|
||||||
.mainbox {
|
.container {
|
||||||
margin: auto;
|
max-width: 1200px;
|
||||||
max-width: 900px;
|
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;
|
text-align: center;
|
||||||
height: 50px;
|
padding: ${unsafeCSS(spacing['2xl'])};
|
||||||
border-radius: 3px;
|
color: ${colors.text.secondary};
|
||||||
background: ${cssManager.bdTheme('#ffffff', '#333333')};;
|
}
|
||||||
|
|
||||||
|
.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 {
|
public render(): TemplateResult {
|
||||||
return html`
|
const filteredServices = this.getFilteredServices();
|
||||||
<style>
|
const selectedCount = this.services.filter(s => s.selected).length;
|
||||||
|
const categories = this.getUniqueCategories();
|
||||||
|
|
||||||
</style>
|
return html`
|
||||||
|
<div class="container">
|
||||||
<uplinternal-miniheading>Monitored Assets</uplinternal-miniheading>
|
<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>
|
</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 * 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 {
|
declare global {
|
||||||
interface HTMLElementTagNameMap {
|
interface HTMLElementTagNameMap {
|
||||||
@@ -10,18 +12,80 @@ declare global {
|
|||||||
@customElement('upl-statuspage-footer')
|
@customElement('upl-statuspage-footer')
|
||||||
export class UplStatuspageFooter extends DeesElement {
|
export class UplStatuspageFooter extends DeesElement {
|
||||||
// STATIC
|
// STATIC
|
||||||
public static demo = () => html`
|
public static demo = demoFunc;
|
||||||
<upl-statuspage-footer></upl-statuspage-footer>
|
|
||||||
`;
|
|
||||||
|
|
||||||
// INSTANCE
|
// INSTANCE
|
||||||
@property()
|
@property({ type: String })
|
||||||
public legalInfo: string = "https://lossless.gmbh";
|
public companyName: string = '';
|
||||||
|
|
||||||
@property({
|
@property({ type: String })
|
||||||
type: Boolean
|
public legalUrl: string = '';
|
||||||
})
|
|
||||||
public whitelabel = false;
|
@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() {
|
constructor() {
|
||||||
@@ -30,43 +94,545 @@ export class UplStatuspageFooter extends DeesElement {
|
|||||||
|
|
||||||
public static styles = [
|
public static styles = [
|
||||||
domtools.elementBasic.staticStyles,
|
domtools.elementBasic.staticStyles,
|
||||||
|
commonStyles,
|
||||||
css`
|
css`
|
||||||
:host {
|
:host {
|
||||||
display: block;
|
display: block;
|
||||||
background: ${cssManager.bdTheme('#ffffff', '#000000')};
|
background: ${colors.background.primary};
|
||||||
font-family: Inter;
|
font-family: ${unsafeCSS(fonts.base)};
|
||||||
color: ${cssManager.bdTheme('#333333', '#ffffff')};
|
color: ${colors.text.primary};
|
||||||
|
font-size: 14px;
|
||||||
|
border-top: 1px solid ${colors.border.default};
|
||||||
}
|
}
|
||||||
|
|
||||||
.mainbox {
|
.container {
|
||||||
max-width: 900px;
|
max-width: 1200px;
|
||||||
margin: auto;
|
margin: 0 auto;
|
||||||
padding-top: 20px;
|
padding: ${unsafeCSS(spacing['2xl'])} ${unsafeCSS(spacing.lg)};
|
||||||
padding-bottom: 20px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.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 {
|
public render(): TemplateResult {
|
||||||
|
if (this.loading) {
|
||||||
return html`
|
return html`
|
||||||
${domtools.elementBasic.styles}
|
<div class="container">
|
||||||
<style></style>
|
<div class="loading-skeleton"></div>
|
||||||
<div class="mainbox">
|
|
||||||
Hi there
|
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
public dispatchReportNewIncident() {
|
return html`
|
||||||
this.dispatchEvent(new CustomEvent('reportNewIncident', {
|
<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.handleReportIncidentClick();
|
||||||
}
|
}
|
||||||
|
|
||||||
public dispatchStatusSubscribe() {
|
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 * as domtools from '@design.estate/dees-domtools';
|
||||||
|
import { fonts } from '../styles/shared.styles.js';
|
||||||
|
import { demoFunc } from './upl-statuspage-header.demo.js';
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
interface HTMLElementTagNameMap {
|
interface HTMLElementTagNameMap {
|
||||||
@@ -10,14 +12,30 @@ declare global {
|
|||||||
@customElement('upl-statuspage-header')
|
@customElement('upl-statuspage-header')
|
||||||
export class UplStatuspageHeader extends DeesElement {
|
export class UplStatuspageHeader extends DeesElement {
|
||||||
// STATIC
|
// STATIC
|
||||||
public static demo = () => html`
|
public static demo = demoFunc;
|
||||||
<upl-statuspage-header></upl-statuspage-header>
|
|
||||||
`;
|
|
||||||
|
|
||||||
// INSTANCE
|
// INSTANCE
|
||||||
@property()
|
@property({ type: String })
|
||||||
public pageTitle: string = "Statuspage Title";
|
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() {
|
constructor() {
|
||||||
super();
|
super();
|
||||||
@@ -28,70 +46,162 @@ export class UplStatuspageHeader extends DeesElement {
|
|||||||
css`
|
css`
|
||||||
:host {
|
:host {
|
||||||
display: block;
|
display: block;
|
||||||
background: ${cssManager.bdTheme('#eeeeeb', '#222222')};
|
background: ${cssManager.bdTheme('#ffffff', '#0a0a0a')};
|
||||||
font-family: Inter;
|
font-family: ${unsafeCSS(fonts.base)};
|
||||||
color: ${cssManager.bdTheme('#333333', '#ffffff')};
|
color: ${cssManager.bdTheme('#0a0a0a', '#fafafa')};
|
||||||
|
border-bottom: 1px solid ${cssManager.bdTheme('#e5e7eb', '#262626')};
|
||||||
}
|
}
|
||||||
|
|
||||||
.mainbox {
|
.mainbox {
|
||||||
margin: auto;
|
margin: auto;
|
||||||
max-width: 900px;
|
max-width: 1200px;
|
||||||
|
padding: 0 24px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mainbox .actions {
|
.mainbox .actions {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: flex-end;
|
justify-content: flex-end;
|
||||||
padding: 20px 0px 40px 0px;
|
gap: 8px;
|
||||||
|
padding: 24px 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mainbox .actions .actionButton {
|
.mainbox .actions .actionButton {
|
||||||
background: ${cssManager.bdTheme('#00000000', '#ffffff00')};
|
background: transparent;
|
||||||
|
font-size: 14px;
|
||||||
font-size: 12px;
|
font-weight: 500;
|
||||||
border: 1px solid ${cssManager.bdTheme('#333', '#CCC')};
|
border: 1px solid ${cssManager.bdTheme('#e5e7eb', '#262626')};
|
||||||
padding: 6px 10px 7px 10px;
|
padding: 8px 16px;
|
||||||
margin-left: 10px;
|
border-radius: 6px;
|
||||||
border-radius: 3px;
|
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
user-select: none;
|
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 {
|
.mainbox .actions .actionButton:hover {
|
||||||
background: ${cssManager.bdTheme('#333333', '#efefef')};
|
background: ${cssManager.bdTheme('#f9fafb', '#262626')};
|
||||||
border: 1px solid ${cssManager.bdTheme('#333333', '#efefef')};
|
border-color: ${cssManager.bdTheme('#d1d5db', '#404040')};
|
||||||
color: ${cssManager.bdTheme('#fff', '#333333')};
|
transform: translateY(-1px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.mainbox .actions .actionButton:active {
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-content {
|
||||||
|
padding: 48px 0 64px 0;
|
||||||
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
h1 {
|
h1 {
|
||||||
margin: 0px;
|
margin: 0;
|
||||||
text-align: center;
|
font-size: 48px;
|
||||||
font-weight: 600;
|
font-weight: 700;
|
||||||
font-size: 35px;
|
letter-spacing: -0.02em;
|
||||||
|
line-height: 1.1;
|
||||||
|
color: ${cssManager.bdTheme('#0a0a0a', '#fafafa')};
|
||||||
}
|
}
|
||||||
|
|
||||||
h2 {
|
.subtitle {
|
||||||
margin: 0px;
|
margin: 16px 0 0 0;
|
||||||
margin-top: 10px;
|
font-size: 16px;
|
||||||
text-align: center;
|
font-weight: 400;
|
||||||
font-weight: 600;
|
color: ${cssManager.bdTheme('#6b7280', '#a1a1aa')};
|
||||||
font-size: 18px;
|
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 {
|
public render(): TemplateResult {
|
||||||
|
if (this.loading) {
|
||||||
|
return html`
|
||||||
|
<div class="mainbox">
|
||||||
|
<div class="loading-skeleton"></div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
return html`
|
return html`
|
||||||
${domtools.elementBasic.styles}
|
${domtools.elementBasic.styles}
|
||||||
<style>
|
<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>
|
</style>
|
||||||
<div class="mainbox">
|
<div class="mainbox">
|
||||||
<div class="actions">
|
<div class="actions">
|
||||||
<div class="actionButton" @click=${this.dispatchReportNewIncident}>report new incident</div>
|
${this.showReportButton ? html`
|
||||||
<div class="actionButton" @click=${this.dispatchStatusSubscribe}>subscribe</div>
|
<div class="actionButton" @click=${this.dispatchReportNewIncident}>Report Issue</div>
|
||||||
|
` : ''}
|
||||||
|
${this.showSubscribeButton ? html`
|
||||||
|
<div class="actionButton primary" @click=${this.dispatchStatusSubscribe}>Subscribe</div>
|
||||||
|
` : ''}
|
||||||
</div>
|
</div>
|
||||||
|
<div class="header-content">
|
||||||
|
${this.logoUrl ? html`
|
||||||
|
<img src="${this.logoUrl}" alt="Logo" class="logo">
|
||||||
|
` : ''}
|
||||||
<h1>${this.pageTitle}</h1>
|
<h1>${this.pageTitle}</h1>
|
||||||
<h2>STATUS BOARD</h2>
|
<div class="subtitle">System Status</div>
|
||||||
|
</div>
|
||||||
</div>
|
</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,
|
type TemplateResult,
|
||||||
css,
|
css,
|
||||||
cssManager,
|
cssManager,
|
||||||
|
unsafeCSS,
|
||||||
} from '@design.estate/dees-element';
|
} 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 {
|
declare global {
|
||||||
interface HTMLElementTagNameMap {
|
interface HTMLElementTagNameMap {
|
||||||
@@ -18,73 +23,490 @@ declare global {
|
|||||||
@customElement('upl-statuspage-incidents')
|
@customElement('upl-statuspage-incidents')
|
||||||
export class UplStatuspageIncidents extends DeesElement {
|
export class UplStatuspageIncidents extends DeesElement {
|
||||||
// STATIC
|
// STATIC
|
||||||
public static demo = () => html` <upl-statuspage-incidents></upl-statuspage-incidents> `;
|
public static demo = demoFunc;
|
||||||
|
|
||||||
// INSTANCE
|
// INSTANCE
|
||||||
@property({
|
@property({
|
||||||
type: Array,
|
type: Array,
|
||||||
})
|
})
|
||||||
public currentIncidences: plugins.uplInterfaces.data.IIncident[] = [];
|
public currentIncidents: IIncidentDetails[] = [];
|
||||||
|
|
||||||
@property({
|
@property({
|
||||||
type: Array,
|
type: Array,
|
||||||
})
|
})
|
||||||
public pastIncidences: plugins.uplInterfaces.data.IIncident[] = [];
|
public pastIncidents: IIncidentDetails[] = [];
|
||||||
|
|
||||||
@property({
|
@property({
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
})
|
})
|
||||||
public whitelabel = false;
|
public whitelabel = false;
|
||||||
|
|
||||||
|
@property({
|
||||||
|
type: Boolean,
|
||||||
|
})
|
||||||
|
public loading = false;
|
||||||
|
|
||||||
|
@property({
|
||||||
|
type: Number,
|
||||||
|
})
|
||||||
|
public daysToShow = 90;
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
super();
|
super();
|
||||||
}
|
}
|
||||||
|
|
||||||
public static styles = [
|
public static styles = [
|
||||||
plugins.domtools.elementBasic.staticStyles,
|
plugins.domtools.elementBasic.staticStyles,
|
||||||
|
commonStyles,
|
||||||
css`
|
css`
|
||||||
:host {
|
:host {
|
||||||
display: block;
|
display: block;
|
||||||
background: ${cssManager.bdTheme('#eeeeeb', '#222222')};
|
background: transparent;
|
||||||
font-family: Inter;
|
font-family: ${unsafeCSS(fonts.base)};
|
||||||
color: ${cssManager.bdTheme('#333333', '#ffffff')};
|
color: ${colors.text.primary};
|
||||||
}
|
}
|
||||||
|
|
||||||
.mainbox {
|
.container {
|
||||||
max-width: 900px;
|
max-width: 1200px;
|
||||||
margin: auto;
|
margin: 0 auto;
|
||||||
|
padding: 0 ${unsafeCSS(spacing.lg)} ${unsafeCSS(spacing.lg)} ${unsafeCSS(spacing.lg)};
|
||||||
}
|
}
|
||||||
|
|
||||||
.noIncidentBox {
|
.noIncidentBox {
|
||||||
background: ${cssManager.bdTheme('#ffffff', '#333333')};;
|
background: ${colors.background.card};
|
||||||
padding: 10px;
|
padding: ${unsafeCSS(spacing.xl)};
|
||||||
margin-bottom: 15px;
|
margin-bottom: ${unsafeCSS(spacing.lg)};
|
||||||
border-radius: 3px;
|
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 {
|
public render(): TemplateResult {
|
||||||
return html`
|
return html`
|
||||||
<style></style>
|
<div class="container">
|
||||||
<div class="mainbox">
|
|
||||||
<uplinternal-miniheading>Current Incidents</uplinternal-miniheading>
|
<uplinternal-miniheading>Current Incidents</uplinternal-miniheading>
|
||||||
${this.currentIncidences.length
|
${this.loading ? html`
|
||||||
? html``
|
<div class="loading-skeleton"></div>
|
||||||
: html` <div class="noIncidentBox">No incidents ongoing.</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>
|
<uplinternal-miniheading>Past Incidents</uplinternal-miniheading>
|
||||||
${this.pastIncidences.length
|
${this.loading ? html`
|
||||||
? html``
|
<div class="loading-skeleton"></div>
|
||||||
: html` <div class="noIncidentBox">No past incidents in the last 90 days.</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>
|
</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() {
|
public dispatchReportNewIncident() {
|
||||||
this.dispatchEvent(new CustomEvent('reportNewIncident', {}));
|
this.dispatchEvent(new CustomEvent('reportNewIncident', {
|
||||||
|
bubbles: true,
|
||||||
|
composed: true
|
||||||
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
public dispatchStatusSubscribe() {
|
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 * 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 {
|
declare global {
|
||||||
interface HTMLElementTagNameMap {
|
interface HTMLElementTagNameMap {
|
||||||
@@ -9,9 +12,22 @@ declare global {
|
|||||||
|
|
||||||
@customElement('upl-statuspage-statusbar')
|
@customElement('upl-statuspage-statusbar')
|
||||||
export class UplStatuspageStatusbar extends DeesElement {
|
export class UplStatuspageStatusbar extends DeesElement {
|
||||||
public static demo = () => html`
|
public static demo = demoFunc;
|
||||||
<upl-statuspage-statusbar></upl-statuspage-statusbar>
|
|
||||||
`;
|
@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() {
|
constructor() {
|
||||||
super();
|
super();
|
||||||
@@ -21,30 +37,200 @@ export class UplStatuspageStatusbar extends DeesElement {
|
|||||||
cssManager.defaultStyles,
|
cssManager.defaultStyles,
|
||||||
css`
|
css`
|
||||||
:host {
|
:host {
|
||||||
padding: 20px 0px 15px 0px;
|
padding: 0;
|
||||||
display: block;
|
display: block;
|
||||||
background: ${cssManager.bdTheme('#eeeeeb', '#222222')};;
|
background: transparent;
|
||||||
font-family: Inter;
|
font-family: ${unsafeCSS(fonts.base)};
|
||||||
color: #fff;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.mainbox {
|
.statusbar-container {
|
||||||
margin: auto;
|
margin: auto;
|
||||||
max-width: 900px;
|
max-width: 1200px;
|
||||||
text-align: center;
|
padding: 0 24px 24px 24px;
|
||||||
background: #19572E;
|
position: relative;
|
||||||
line-height: 50px;
|
}
|
||||||
border-radius: 3px;
|
|
||||||
|
.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 {
|
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`
|
return html`
|
||||||
<style>
|
<div class="statusbar-container">
|
||||||
</style>
|
${this.loading ? html`
|
||||||
<div class="mainbox">
|
<div class="loading-skeleton"></div>
|
||||||
Everything is working normally!
|
` : 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>
|
</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,
|
type TemplateResult,
|
||||||
css,
|
css,
|
||||||
cssManager,
|
cssManager,
|
||||||
|
unsafeCSS,
|
||||||
} from '@design.estate/dees-element';
|
} 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 './internal/uplinternal-miniheading.js';
|
||||||
|
import { demoFunc } from './upl-statuspage-statusdetails.demo.js';
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
interface HTMLElementTagNameMap {
|
interface HTMLElementTagNameMap {
|
||||||
@@ -19,7 +23,22 @@ declare global {
|
|||||||
|
|
||||||
@customElement('upl-statuspage-statusdetails')
|
@customElement('upl-statuspage-statusdetails')
|
||||||
export class UplStatuspageStatusdetails extends DeesElement {
|
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() {
|
constructor() {
|
||||||
super();
|
super();
|
||||||
@@ -27,69 +46,342 @@ export class UplStatuspageStatusdetails extends DeesElement {
|
|||||||
|
|
||||||
public static styles = [
|
public static styles = [
|
||||||
plugins.domtools.elementBasic.staticStyles,
|
plugins.domtools.elementBasic.staticStyles,
|
||||||
|
commonStyles,
|
||||||
css`
|
css`
|
||||||
:host {
|
:host {
|
||||||
position: relative;
|
position: relative;
|
||||||
padding: 0px 0px 15px 0px;
|
|
||||||
display: block;
|
display: block;
|
||||||
background: ${cssManager.bdTheme('#eeeeeb', '#222222')};;
|
background: transparent;
|
||||||
font-family: Inter;
|
font-family: ${unsafeCSS(fonts.base)};
|
||||||
color: #fff;
|
color: ${colors.text.primary};
|
||||||
|
}
|
||||||
|
|
||||||
|
.container {
|
||||||
|
max-width: 1200px;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 0 ${unsafeCSS(spacing.lg)} ${unsafeCSS(spacing.lg)} ${unsafeCSS(spacing.lg)};
|
||||||
}
|
}
|
||||||
|
|
||||||
.mainbox {
|
.mainbox {
|
||||||
margin: auto;
|
background: ${colors.background.card};
|
||||||
max-width: 900px;
|
border: 1px solid ${colors.border.default};
|
||||||
text-align: right;
|
border-radius: ${unsafeCSS(borderRadius.md)};
|
||||||
background: ${cssManager.bdTheme('#ffffff', '#333333')};;
|
padding: ${unsafeCSS(spacing.md)};
|
||||||
line-height: 50px;
|
box-shadow: ${unsafeCSS(shadows.sm)};
|
||||||
border-radius: 3px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.mainbox .barContainer {
|
.mainbox .barContainer {
|
||||||
position: relative;
|
position: relative;
|
||||||
display: flex;
|
display: flex;
|
||||||
padding: 6px;
|
gap: 2px;
|
||||||
|
padding: ${unsafeCSS(spacing.sm)};
|
||||||
|
background: ${colors.background.secondary};
|
||||||
|
border-radius: ${unsafeCSS(borderRadius.base)};
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mainbox .barContainer .bar {
|
.mainbox .barContainer .bar {
|
||||||
margin: 4px;
|
flex: 1;
|
||||||
width: 11px;
|
height: 48px;
|
||||||
border-radius: 3px;
|
border-radius: ${unsafeCSS(borderRadius.sm)};
|
||||||
height: 40px;
|
cursor: pointer;
|
||||||
background: #2deb51;
|
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 {
|
.timeIndicator {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
width: 11px;
|
width: 2px;
|
||||||
height: 11px;
|
height: calc(100% - ${unsafeCSS(spacing.md)});
|
||||||
background: #FF9800;
|
background: ${colors.text.primary};
|
||||||
top: 56px;
|
top: ${unsafeCSS(spacing.sm)};
|
||||||
left: 400px;
|
transition: left 0.3s;
|
||||||
transform: rotate(45deg);
|
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 {
|
public render(): TemplateResult {
|
||||||
|
const now = Date.now();
|
||||||
|
const currentHour = new Date().getHours();
|
||||||
|
const timeIndicatorPosition = this.calculateTimeIndicatorPosition();
|
||||||
|
|
||||||
return html`
|
return html`
|
||||||
<style></style>
|
<uplinternal-miniheading>${this.serviceName} - Last ${this.hoursToShow} Hours</uplinternal-miniheading>
|
||||||
<uplinternal-miniheading>Yesterday & Today</uplinternal-miniheading>
|
|
||||||
<div class="mainbox">
|
<div class="mainbox">
|
||||||
<div class="barContainer">
|
${this.loading ? html`
|
||||||
${(() => {
|
<div class="loading-skeleton">
|
||||||
let counter = 0;
|
${Array(this.hoursToShow).fill(0).map(() => html`<div class="skeleton-bar"></div>`)}
|
||||||
const returnArray: TemplateResult[] = [];
|
</div>
|
||||||
while (counter < 48) {
|
` : html`
|
||||||
counter++;
|
<div class="barContainer" @mouseleave=${this.hideTooltip}>
|
||||||
returnArray.push(html` <div class="bar"></div> `);
|
${this.renderBars()}
|
||||||
}
|
<div class="timeIndicator" style="left: ${timeIndicatorPosition}px"></div>
|
||||||
return returnArray;
|
</div>
|
||||||
})()}
|
<div class="time-labels">
|
||||||
<div class="timeIndicator"></div>
|
<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>
|
</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,
|
customElement,
|
||||||
type TemplateResult,
|
type TemplateResult,
|
||||||
css,
|
css,
|
||||||
cssManager
|
cssManager,
|
||||||
|
unsafeCSS
|
||||||
} from '@design.estate/dees-element';
|
} from '@design.estate/dees-element';
|
||||||
import * as domtools from '@design.estate/dees-domtools';
|
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 './internal/uplinternal-miniheading.js';
|
||||||
|
import { demoFunc } from './upl-statuspage-statusmonth.demo.js';
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
interface HTMLElementTagNameMap {
|
interface HTMLElementTagNameMap {
|
||||||
@@ -19,7 +23,25 @@ declare global {
|
|||||||
|
|
||||||
@customElement('upl-statuspage-statusmonth')
|
@customElement('upl-statuspage-statusmonth')
|
||||||
export class UplStatuspageStatusmonth extends DeesElement {
|
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() {
|
constructor() {
|
||||||
super();
|
super();
|
||||||
@@ -27,104 +49,310 @@ export class UplStatuspageStatusmonth extends DeesElement {
|
|||||||
|
|
||||||
public static styles = [
|
public static styles = [
|
||||||
domtools.elementBasic.staticStyles,
|
domtools.elementBasic.staticStyles,
|
||||||
|
commonStyles,
|
||||||
css`
|
css`
|
||||||
:host {
|
:host {
|
||||||
position: relative;
|
position: relative;
|
||||||
padding: 0px 0px 15px 0px;
|
|
||||||
display: block;
|
display: block;
|
||||||
background: ${cssManager.bdTheme('#eeeeeb', '#222222')};;
|
background: transparent;
|
||||||
font-family: Inter;
|
font-family: ${unsafeCSS(fonts.base)};
|
||||||
color: #fff;
|
color: ${colors.text.primary};
|
||||||
|
}
|
||||||
|
|
||||||
|
.container {
|
||||||
|
max-width: 1200px;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 0 ${unsafeCSS(spacing.lg)} ${unsafeCSS(spacing.lg)} ${unsafeCSS(spacing.lg)};
|
||||||
}
|
}
|
||||||
|
|
||||||
.mainbox {
|
.mainbox {
|
||||||
margin: auto;
|
|
||||||
max-width: 900px;
|
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: repeat(5, calc(100% / 5 - 80px / 5));
|
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
||||||
grid-column-gap: 20px;
|
gap: ${unsafeCSS(spacing.lg)};
|
||||||
}
|
}
|
||||||
|
|
||||||
.statusMonth {
|
.statusMonth {
|
||||||
background: ${cssManager.bdTheme('#ffffff', '#333333')};;
|
background: ${colors.background.card};
|
||||||
min-height: 20px;
|
padding: ${unsafeCSS(spacing.lg)};
|
||||||
display: grid;
|
border-radius: ${unsafeCSS(borderRadius.md)};
|
||||||
padding: 10px;
|
border: 1px solid ${colors.border.default};
|
||||||
grid-template-columns: repeat(6, auto);
|
box-shadow: ${unsafeCSS(shadows.sm)};
|
||||||
grid-gap: 9px;
|
position: relative;
|
||||||
border-radius: 3px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.statusMonth .statusDay {
|
.month-header {
|
||||||
width: 16px;
|
font-size: 14px;
|
||||||
height: 16px;
|
font-weight: 600;
|
||||||
background: #2deb51;
|
margin-bottom: ${unsafeCSS(spacing.md)};
|
||||||
border-radius: 3px;
|
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 {
|
public render(): TemplateResult {
|
||||||
|
const totalDays = this.monthlyData.reduce((sum, month) => sum + month.days.length, 0);
|
||||||
|
|
||||||
return html`
|
return html`
|
||||||
<style></style>
|
<div class="container">
|
||||||
<uplinternal-miniheading>Last 150 days</uplinternal-miniheading>
|
<uplinternal-miniheading>${this.serviceName} - Last ${totalDays} Days</uplinternal-miniheading>
|
||||||
<div class="mainbox">
|
<div class="mainbox">
|
||||||
|
${this.loading ? html`
|
||||||
|
${Array(this.monthsToShow).fill(0).map(() => html`
|
||||||
<div class="statusMonth">
|
<div class="statusMonth">
|
||||||
${(() => {
|
<div class="loading-skeleton"></div>
|
||||||
let counter = 0;
|
|
||||||
const returnArray: TemplateResult[] = [];
|
|
||||||
while (counter < 30) {
|
|
||||||
counter++;
|
|
||||||
returnArray.push(html` <div class="statusDay"></div> `);
|
|
||||||
}
|
|
||||||
return returnArray;
|
|
||||||
})()}
|
|
||||||
</div>
|
</div>
|
||||||
<div class="statusMonth">
|
`)}
|
||||||
${(() => {
|
` : this.monthlyData.length === 0 ? html`
|
||||||
let counter = 0;
|
<div class="no-data-message">No uptime data available</div>
|
||||||
const returnArray: TemplateResult[] = [];
|
` : this.monthlyData.map(month => this.renderMonth(month))}
|
||||||
while (counter < 30) {
|
|
||||||
counter++;
|
|
||||||
returnArray.push(html` <div class="statusDay"></div> `);
|
|
||||||
}
|
|
||||||
return returnArray;
|
|
||||||
})()}
|
|
||||||
</div>
|
</div>
|
||||||
<div class="statusMonth">
|
${this.showTooltip ? html`<div class="tooltip" id="tooltip"></div>` : ''}
|
||||||
${(() => {
|
|
||||||
let counter = 0;
|
|
||||||
const returnArray: TemplateResult[] = [];
|
|
||||||
while (counter < 30) {
|
|
||||||
counter++;
|
|
||||||
returnArray.push(html` <div class="statusDay"></div> `);
|
|
||||||
}
|
|
||||||
return returnArray;
|
|
||||||
})()}
|
|
||||||
</div>
|
</div>
|
||||||
<div class="statusMonth">
|
`;
|
||||||
${(() => {
|
|
||||||
let counter = 0;
|
|
||||||
const returnArray: TemplateResult[] = [];
|
|
||||||
while (counter < 30) {
|
|
||||||
counter++;
|
|
||||||
returnArray.push(html` <div class="statusDay"></div> `);
|
|
||||||
}
|
}
|
||||||
return returnArray;
|
|
||||||
})()}
|
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>
|
||||||
<div class="statusMonth">
|
<div class="overall-uptime">
|
||||||
${(() => {
|
${monthData.overallUptime.toFixed(2)}% uptime
|
||||||
let counter = 0;
|
${monthData.totalIncidents > 0 ? html`<br/>${monthData.totalIncidents} incidents` : ''}
|
||||||
const returnArray: TemplateResult[] = [];
|
|
||||||
while (counter < 30) {
|
|
||||||
counter++;
|
|
||||||
returnArray.push(html` <div class="statusDay"></div> `);
|
|
||||||
}
|
|
||||||
return returnArray;
|
|
||||||
})()}
|
|
||||||
</div>
|
</div>
|
||||||
</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