4 Commits

18 changed files with 1333 additions and 361 deletions

67
.nogit/theme-test.html Normal file
View File

@@ -0,0 +1,67 @@
<!DOCTYPE html>
<html>
<head>
<title>Theme Test</title>
<script type="module" src="../dist_bundle/bundle.js"></script>
<style>
body {
margin: 0;
padding: 24px;
font-family: system-ui, sans-serif;
}
.section {
margin-bottom: 32px;
}
.title {
font-size: 16px;
font-weight: 600;
margin-bottom: 16px;
}
.grid {
display: grid;
grid-template-columns: repeat(5, 1fr);
gap: 12px;
}
#theme-toggle {
position: fixed;
top: 16px;
right: 16px;
padding: 8px 16px;
cursor: pointer;
}
</style>
</head>
<body>
<button id="theme-toggle">Toggle Theme</button>
<div class="section">
<div class="title">Severity Variants</div>
<div class="grid">
<upladmin-option-card variant="critical" icon="lucide:AlertCircle" label="Critical" description="Severe impact"></upladmin-option-card>
<upladmin-option-card variant="major" icon="lucide:AlertTriangle" label="Major" description="Significant impact"></upladmin-option-card>
<upladmin-option-card variant="minor" icon="lucide:Info" label="Minor" description="Limited impact"></upladmin-option-card>
<upladmin-option-card variant="maintenance" icon="lucide:Wrench" label="Maintenance" description="Scheduled work" selected></upladmin-option-card>
</div>
</div>
<div class="section">
<div class="title">Status Variants</div>
<div class="grid">
<upladmin-option-card variant="investigating" icon="lucide:Search" label="Investigating" description="Looking into it"></upladmin-option-card>
<upladmin-option-card variant="identified" icon="lucide:Target" label="Identified" description="Root cause found"></upladmin-option-card>
<upladmin-option-card variant="monitoring" icon="lucide:Eye" label="Monitoring" description="Fix applied" selected></upladmin-option-card>
<upladmin-option-card variant="resolved" icon="lucide:CheckCircle" label="Resolved" description="Issue fixed"></upladmin-option-card>
<upladmin-option-card variant="postmortem" icon="lucide:FileText" label="Postmortem" description="Analysis complete"></upladmin-option-card>
</div>
</div>
<script>
document.getElementById('theme-toggle').addEventListener('click', () => {
const isDark = document.documentElement.style.colorScheme === 'dark';
document.documentElement.style.colorScheme = isDark ? 'light' : 'dark';
document.body.style.background = isDark ? '#ffffff' : '#09090b';
document.body.style.color = isDark ? '#09090b' : '#fafafa';
});
</script>
</body>
</html>

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

View File

@@ -1,5 +1,24 @@
# Changelog
## 2025-12-26 - 1.2.0 - feat(elements)
add upladmin-option-card component and migrate option/status UIs to use it; refactor monitor form multitoggle subscriptions and event handling; improve theme color handling and dark-mode styles; add demos, Playwright snapshots, and migration plan
- Add new upladmin-option-card web component (implementation, index export, and demo).
- Replace inline option/status card markup with <upladmin-option-card> in incident-form and incident-update.
- Refactor upladmin-monitor-form: subscribe/unsubscribe multitoggle change subjects, handle lifecycle (firstUpdated/disconnected), and adjust event handlers (@newValue/@change usage).
- Swap hardcoded color tokens for cssManager.bdTheme for better light/dark theming; add dark-mode media tweak for filter select.
- Add Playwright snapshot images (.playwright-mcp) and a detailed readme.plan.md describing interface migration tasks.
## 2025-12-24 - 1.1.0 - feat(monitor)
add extended monitor statuses, check configuration, status overrides/paused indicators, and incident update templates
- Extend TStatusType with new statuses: initializing, error, paused.
- Add statusMode, manualStatus, paused, checkType, checkConfig and intervalMs to service and monitor interfaces.
- Update monitor list UI to show manual-override and paused indicators, new status badges, and include new statuses in status filter.
- Add quick templates to incident update form that prefill both status and message; update applyTemplate accordingly.
- Enhance monitor form to support checkType/ICheckConfig, statusMode selection, pause flag, interval options and additional validation (domain & PageRank search term).
- Add styles and icons for new statuses and status indicator badges.
## 2025-12-24 - 1.0.3 - fix(catalog_admin)
no changes detected, no release required

View File

@@ -1,6 +1,6 @@
{
"name": "@uptime.link/statuspage-admin",
"version": "1.0.3",
"version": "1.2.0",
"private": false,
"description": "Admin components for managing UptimeLink status pages, monitors, and incidents.",
"main": "dist_ts_web/index.js",

396
readme.plan.md Normal file
View File

@@ -0,0 +1,396 @@
# Plan: Migrate Shared Interfaces to @uptime.link/interfaces
## Overview
Move shared type definitions from `catalog_admin/ts_web/interfaces/` to the canonical `../interfaces` package (`@uptime.link/interfaces`) to ensure consistency across all uptime.link packages.
## Decisions (Resolved)
1. **Check Config**: Use base type + discriminated union variants (elegant, type-safe)
2. **Incident Status**: Create unified type in shared package, migrate both packages
3. **Form Interfaces**: Keep local in catalog_admin (UI-specific)
4. **Versioning**: Manual releases - will notify when ready to publish
---
## Task 1: Add Base Types to Shared Package
**File: `../interfaces/ts/data/types.ts`** (new)
```typescript
// Status types for monitors/services
export type TStatusType =
| 'operational'
| 'degraded'
| 'partial_outage'
| 'major_outage'
| 'maintenance'
| 'initializing'
| 'error'
| 'paused';
// Check types (discriminant values)
export type TCheckType = 'assumption' | 'function' | 'pwa' | 'pagerank';
// Status mode for monitors
export type TStatusMode = 'auto' | 'manual';
// Incident severity
export type TIncidentSeverity = 'critical' | 'major' | 'minor' | 'maintenance';
// Incident status (unified)
export type TIncidentStatus =
| 'investigating'
| 'identified'
| 'monitoring'
| 'resolved'
| 'postmortem';
```
---
## Task 2: Refactor Check Interfaces with Base + Variants
**File: `../interfaces/ts/data/checks/index.ts`** (refactor)
```typescript
import { TStatusType, TCheckType } from '../types.js';
// ============================================
// Base Interface
// ============================================
export interface ICheckBase {
id: string;
name: string;
description?: string;
enabled: boolean;
intervalMs?: number;
lastRun?: number;
lastResult?: 'success' | 'failure' | 'pending';
}
// ============================================
// Discriminated Variants
// ============================================
export interface IAssumptionCheck extends ICheckBase {
checkType: 'assumption';
assumedStatus: TStatusType;
}
export interface IFunctionCheck extends ICheckBase {
checkType: 'function';
functionUrl: string;
expectedStatusCode?: number;
timeoutMs?: number;
headers?: Record<string, string>;
}
export interface IPwaCheck extends ICheckBase {
checkType: 'pwa';
targetUrl: string;
lighthouseThreshold?: number;
categories?: ('performance' | 'accessibility' | 'best-practices' | 'seo')[];
}
export interface IPageRankCheck extends ICheckBase {
checkType: 'pagerank';
targetUrl: string;
minimumRank?: number;
searchEngine?: 'google' | 'bing';
}
// ============================================
// Union Type (for UI and generic handling)
// ============================================
export type TCheck =
| IAssumptionCheck
| IFunctionCheck
| IPwaCheck
| IPageRankCheck;
// ============================================
// Check Collection
// ============================================
export interface ICheckCollection {
id: string;
name: string;
checks: TCheck[];
}
```
---
## Task 3: Create Unified Incident Interface
**File: `../interfaces/ts/data/incident.ts`** (refactor)
```typescript
import { TIncidentSeverity, TIncidentStatus } from './types.js';
export interface IIncidentUpdate {
id: string;
incidentId: string;
status: TIncidentStatus;
message: string;
createdAt: number;
createdBy?: string;
}
export interface IIncident {
id: string;
title: string;
description: string;
severity: TIncidentSeverity;
status: TIncidentStatus;
// Affected services
affectedServiceIds: string[];
// Timeline
createdAt: number;
updatedAt: number;
resolvedAt?: number;
// Updates history
updates: IIncidentUpdate[];
// Metadata
createdBy?: string;
isScheduled?: boolean;
scheduledStartTime?: number;
scheduledEndTime?: number;
}
```
---
## Task 4: Add Service Status Interface
**File: `../interfaces/ts/data/servicestatus.ts`** (new)
```typescript
import { TStatusType, TStatusMode, TCheckType } from './types.js';
export interface IServiceStatus {
id: string;
name: string;
displayName: string;
description?: string;
// Current state
currentStatus: TStatusType;
lastChecked: number;
responseTime: number;
// Uptime metrics
uptime30d: number;
uptime90d: number;
// Organization
category?: string;
dependencies?: string[];
// Status management
statusMode: TStatusMode;
manualStatus?: TStatusType;
paused: boolean;
// Check configuration (references check collection)
checkType?: TCheckType;
checkCollectionId?: string;
intervalMs?: number;
}
export interface IStatusHistoryPoint {
timestamp: number;
status: TStatusType;
responseTime?: number;
}
export interface IOverallStatus {
status: TStatusType;
message?: string;
lastUpdated: number;
}
```
---
## Task 5: Add Status Page Config
**File: `../interfaces/ts/data/statuspageconfig.ts`** (new)
```typescript
import { IOverallStatus } from './servicestatus.js';
export interface IStatusPageConfig {
id: string;
name: string;
slug: string;
// Branding
logoUrl?: string;
faviconUrl?: string;
primaryColor?: string;
// Content
headerTitle: string;
headerDescription?: string;
// Features
showHistoricalUptime: boolean;
showResponseTime: boolean;
showSubscribeButton: boolean;
// Service grouping
serviceGroups: IServiceGroup[];
// Overall status override
overallStatus?: IOverallStatus;
}
export interface IServiceGroup {
id: string;
name: string;
description?: string;
serviceIds: string[];
expanded: boolean;
}
```
---
## Task 6: Update Shared Package Exports
**File: `../interfaces/ts/data/index.ts`** (update)
```typescript
export * from './types.js';
export * from './checks/index.js';
export * from './incident.js';
export * from './servicestatus.js';
export * from './statuspageconfig.js';
// ... existing exports
```
---
## Task 7: Update catalog_admin Interfaces
**File: `catalog_admin/ts_web/interfaces/index.ts`** (refactor)
```typescript
// Re-export shared types from @uptime.link/interfaces
export {
// Types
TStatusType,
TCheckType,
TStatusMode,
TIncidentSeverity,
TIncidentStatus,
// Check interfaces
ICheckBase,
IAssumptionCheck,
IFunctionCheck,
IPwaCheck,
IPageRankCheck,
TCheck,
ICheckCollection,
// Incident interfaces
IIncident,
IIncidentUpdate,
// Service/Status interfaces
IServiceStatus,
IStatusHistoryPoint,
IOverallStatus,
IStatusPageConfig,
IServiceGroup,
} from '@uptime.link/interfaces';
// ============================================
// Form Interfaces (UI-specific, kept local)
// ============================================
import type { TStatusType, TCheckType, TStatusMode, TIncidentSeverity } from '@uptime.link/interfaces';
export interface IMonitorFormData {
name: string;
displayName: string;
description?: string;
category?: string;
checkType: TCheckType;
intervalMs: number;
statusMode: TStatusMode;
paused: boolean;
// Check-specific fields (form flattens the discriminated union)
assumedStatus?: TStatusType;
functionUrl?: string;
expectedStatusCode?: number;
targetUrl?: string;
lighthouseThreshold?: number;
minimumRank?: number;
}
export interface IIncidentFormData {
title: string;
description: string;
severity: TIncidentSeverity;
affectedServiceIds: string[];
isScheduled: boolean;
scheduledStartTime?: number;
scheduledEndTime?: number;
}
export interface IIncidentUpdateFormData {
status: string;
message: string;
}
```
---
## Task 8: Update Component Imports
Scan and update all components that import from local interfaces to ensure they work with the new types. Key files:
- `ts_web/elements/upladmin-monitor-form/upladmin-monitor-form.ts`
- `ts_web/elements/upladmin-monitor-list/upladmin-monitor-list.ts`
- `ts_web/elements/upladmin-incident-form/upladmin-incident-form.ts`
- `ts_web/elements/upladmin-incident-list/upladmin-incident-list.ts`
- `ts_web/elements/upladmin-incident-update/upladmin-incident-update.ts`
---
## Task 9: Build and Test
1. Build `../interfaces`: `cd ../interfaces && pnpm build`
2. **Notify for release** of `@uptime.link/interfaces`
3. Update dependency: `pnpm update @uptime.link/interfaces`
4. Build catalog_admin: `pnpm build`
5. Verify no type errors
6. Test UI components manually
---
## Files Summary
### New files in `../interfaces`:
- `ts/data/types.ts`
- `ts/data/servicestatus.ts`
- `ts/data/statuspageconfig.ts`
### Modified files in `../interfaces`:
- `ts/data/checks/index.ts` (refactor to base + variants)
- `ts/data/incident.ts` (unified interface)
- `ts/data/index.ts` (add exports)
### Modified files in `catalog_admin`:
- `ts_web/interfaces/index.ts` (re-export from shared + local form types)
- Component files (if import paths change)

View File

@@ -3,6 +3,6 @@
*/
export const commitinfo = {
name: '@uptime.link/statuspage-admin',
version: '1.0.3',
version: '1.2.0',
description: 'Admin components for managing UptimeLink status pages, monitors, and incidents.'
}

View File

@@ -1,3 +1,6 @@
// Shared components
export * from './upladmin-option-card/index.js';
// Monitor components
export * from './upladmin-monitor-form/index.js';
export * from './upladmin-monitor-list/index.js';

View File

@@ -168,51 +168,6 @@ export class UpladminIncidentForm extends DeesElement {
gap: ${unsafeCSS(sharedStyles.spacing.sm)};
}
.option-card {
display: flex;
flex-direction: column;
align-items: center;
gap: 10px;
padding: 18px 14px;
background: ${sharedStyles.colors.background.primary};
border: 2px solid ${sharedStyles.colors.border.default};
border-radius: ${unsafeCSS(sharedStyles.borderRadius.base)};
cursor: pointer;
transition: all ${unsafeCSS(sharedStyles.durations.fast)} ${unsafeCSS(sharedStyles.easings.default)};
text-align: center;
}
.option-card:hover {
border-color: ${sharedStyles.colors.border.strong};
background: ${sharedStyles.colors.background.muted};
}
.option-card.selected {
border-color: ${sharedStyles.colors.accent.primary};
background: ${cssManager.bdTheme('rgba(59, 130, 246, 0.05)', 'rgba(96, 165, 250, 0.1)')};
}
.option-card input {
display: none;
}
.option-label {
font-size: 13px;
font-weight: 600;
color: ${sharedStyles.colors.text.primary};
}
.option-desc {
font-size: 11px;
color: ${sharedStyles.colors.text.muted};
line-height: 1.3;
}
.severity-critical dees-icon { --icon-color: ${sharedStyles.colors.status.majorOutage}; }
.severity-major dees-icon { --icon-color: ${sharedStyles.colors.status.partialOutage}; }
.severity-minor dees-icon { --icon-color: ${sharedStyles.colors.status.degraded}; }
.severity-maintenance dees-icon { --icon-color: ${sharedStyles.colors.status.maintenance}; }
.field-label {
display: block;
font-size: 13px;
@@ -363,20 +318,14 @@ export class UpladminIncidentForm extends DeesElement {
<label class="field-label required">Severity</label>
<div class="option-grid">
${severityOptions.map(opt => html`
<label
class="option-card severity-${opt.value} ${this.formData.severity === opt.value ? 'selected' : ''}"
@click="${() => this.handleSeverityChange(opt.value)}"
>
<input
type="radio"
name="severity"
value="${opt.value}"
?checked="${this.formData.severity === opt.value}"
/>
<dees-icon .icon=${this.severityIcons[opt.value]} .iconSize=${24}></dees-icon>
<span class="option-label">${opt.label}</span>
<span class="option-desc">${opt.desc}</span>
</label>
<upladmin-option-card
.variant=${opt.value}
.icon=${this.severityIcons[opt.value]}
.label=${opt.label}
.description=${opt.desc}
?selected=${this.formData.severity === opt.value}
@click=${() => this.handleSeverityChange(opt.value)}
></upladmin-option-card>
`)}
</div>
</div>
@@ -385,19 +334,13 @@ export class UpladminIncidentForm extends DeesElement {
<label class="field-label required">Status</label>
<div class="option-grid">
${statusOptions.map(opt => html`
<label
class="option-card ${this.formData.status === opt.value ? 'selected' : ''}"
@click="${() => this.handleStatusChange(opt.value)}"
>
<input
type="radio"
name="status"
value="${opt.value}"
?checked="${this.formData.status === opt.value}"
/>
<dees-icon .icon=${this.statusIcons[opt.value]} .iconSize=${24}></dees-icon>
<span class="option-label">${opt.label}</span>
</label>
<upladmin-option-card
.variant=${opt.value}
.icon=${this.statusIcons[opt.value]}
.label=${opt.label}
?selected=${this.formData.status === opt.value}
@click=${() => this.handleStatusChange(opt.value)}
></upladmin-option-card>
`)}
</div>
</div>

View File

@@ -260,8 +260,8 @@ export class UpladminIncidentList extends DeesElement {
.incident-status.postmortem {
background: ${cssManager.bdTheme('rgba(168, 85, 247, 0.1)', 'rgba(168, 85, 247, 0.2)')};
color: #a855f7;
--icon-color: #a855f7;
color: ${cssManager.bdTheme('#9333ea', '#a855f7')};
--icon-color: ${cssManager.bdTheme('#9333ea', '#a855f7')};
}
.incident-meta {

View File

@@ -157,52 +157,6 @@ export class UpladminIncidentUpdate extends DeesElement {
gap: ${unsafeCSS(sharedStyles.spacing.sm)};
}
.status-option {
display: flex;
flex-direction: column;
align-items: center;
gap: 10px;
padding: 18px 14px;
background: ${sharedStyles.colors.background.primary};
border: 2px solid ${sharedStyles.colors.border.default};
border-radius: ${unsafeCSS(sharedStyles.borderRadius.base)};
cursor: pointer;
transition: all ${unsafeCSS(sharedStyles.durations.fast)} ${unsafeCSS(sharedStyles.easings.default)};
text-align: center;
}
.status-option:hover {
border-color: ${sharedStyles.colors.border.strong};
background: ${sharedStyles.colors.background.muted};
}
.status-option.selected {
border-color: ${sharedStyles.colors.accent.primary};
background: ${cssManager.bdTheme('rgba(59, 130, 246, 0.05)', 'rgba(96, 165, 250, 0.1)')};
}
.status-option input {
display: none;
}
.status-option.investigating dees-icon { --icon-color: ${sharedStyles.colors.status.partialOutage}; }
.status-option.identified dees-icon { --icon-color: ${sharedStyles.colors.status.degraded}; }
.status-option.monitoring dees-icon { --icon-color: ${sharedStyles.colors.status.maintenance}; }
.status-option.resolved dees-icon { --icon-color: ${sharedStyles.colors.status.operational}; }
.status-option.postmortem dees-icon { --icon-color: #a855f7; }
.status-label {
font-size: 13px;
font-weight: 600;
color: ${sharedStyles.colors.text.primary};
}
.status-desc {
font-size: 11px;
color: ${sharedStyles.colors.text.muted};
line-height: 1.3;
}
.field-label {
display: block;
font-size: 13px;
@@ -308,11 +262,12 @@ export class UpladminIncidentUpdate extends DeesElement {
{ value: 'postmortem', label: 'Postmortem', desc: 'Analysis complete' },
];
const templates: Array<{ icon: string; label: string; message: string }> = [
{ icon: 'lucide:Search', label: 'Started investigating', message: 'We are currently investigating this issue.' },
{ icon: 'lucide:Target', label: 'Issue identified', message: 'We have identified the root cause and are working on a fix.' },
{ icon: 'lucide:Rocket', label: 'Fix deployed', message: 'A fix has been deployed. We are monitoring the results.' },
{ icon: 'lucide:CheckCircle', label: 'Resolved', message: 'This incident has been resolved. All systems are operating normally.' },
const templates: Array<{ icon: string; label: string; status: TIncidentStatus; message: string }> = [
{ icon: 'lucide:Search', label: 'Started investigating', status: 'investigating', message: 'We are currently investigating this issue.' },
{ icon: 'lucide:Target', label: 'Issue identified', status: 'identified', message: 'We have identified the root cause and are working on a fix.' },
{ icon: 'lucide:Rocket', label: 'Fix deployed', status: 'monitoring', message: 'A fix has been deployed. We are monitoring the results.' },
{ icon: 'lucide:CheckCircle', label: 'Resolved', status: 'resolved', message: 'This incident has been resolved. All systems are operating normally.' },
{ icon: 'lucide:FileText', label: 'Postmortem', status: 'postmortem', message: 'Postmortem will be released shortly.' },
];
const severityIcons: Record<string, string> = {
@@ -340,41 +295,37 @@ export class UpladminIncidentUpdate extends DeesElement {
<div class="update-body">
<dees-form>
<div>
<label class="field-label required">Status</label>
<div class="status-grid">
${statusOptions.map(opt => html`
<label
class="status-option ${opt.value} ${this.formData.status === opt.value ? 'selected' : ''}"
@click="${() => this.handleStatusChange(opt.value)}"
>
<input
type="radio"
name="status"
value="${opt.value}"
?checked="${this.formData.status === opt.value}"
/>
<dees-icon .icon=${this.statusIcons[opt.value]} .iconSize=${24}></dees-icon>
<span class="status-label">${opt.label}</span>
<span class="status-desc">${opt.desc}</span>
</label>
`)}
</div>
</div>
<div>
<label class="field-label required">Update Message</label>
<div class="template-section">
<div class="template-label">Quick templates:</div>
<label class="field-label">Quick Templates</label>
<div class="template-label">Select a template to prefill status and message:</div>
<div class="template-buttons">
${templates.map(tpl => html`
<button type="button" class="template-btn" @click="${() => this.applyTemplate(tpl.message)}">
<button type="button" class="template-btn" @click="${() => this.applyTemplate(tpl)}">
<dees-icon .icon=${tpl.icon} .iconSize=${12}></dees-icon>
${tpl.label}
</button>
`)}
</div>
</div>
<div>
<label class="field-label required">Status</label>
<div class="status-grid">
${statusOptions.map(opt => html`
<upladmin-option-card
.variant=${opt.value}
.icon=${this.statusIcons[opt.value]}
.label=${opt.label}
.description=${opt.desc}
?selected=${this.formData.status === opt.value}
@click=${() => this.handleStatusChange(opt.value)}
></upladmin-option-card>
`)}
</div>
</div>
<div>
<label class="field-label required">Update Message</label>
<dees-input-text
key="message"
inputType="textarea"
@@ -430,8 +381,8 @@ export class UpladminIncidentUpdate extends DeesElement {
this.formData = { ...this.formData, status };
}
private applyTemplate(message: string) {
this.formData = { ...this.formData, message };
private applyTemplate(template: { status: TIncidentStatus; message: string }) {
this.formData = { ...this.formData, status: template.status, message: template.message };
}
private validate(): boolean {

View File

@@ -10,8 +10,9 @@ import {
unsafeCSS,
state,
} from '@design.estate/dees-element';
import type { DeesInputMultitoggle } from '@design.estate/dees-catalog';
import * as sharedStyles from '../../styles/shared.styles.js';
import type { IMonitorFormData, IServiceStatus } from '../../interfaces/index.js';
import type { IMonitorFormData, IServiceStatus, ICheckConfig, TStatusType, TCheckType, TStatusMode } from '../../interfaces/index.js';
import { demoFunc } from './upladmin-monitor-form.demo.js';
declare global {
@@ -20,8 +21,6 @@ declare global {
}
}
type TStatusType = 'operational' | 'degraded' | 'partial_outage' | 'major_outage' | 'maintenance';
@customElement('upladmin-monitor-form')
export class UpladminMonitorForm extends DeesElement {
public static demo = demoFunc;
@@ -45,80 +44,90 @@ export class UpladminMonitorForm extends DeesElement {
description: '',
category: '',
dependencies: [],
currentStatus: 'operational',
statusMode: 'auto',
paused: false,
checkType: 'assumption',
checkConfig: { domain: '' },
intervalMs: 300000,
};
@state()
accessor errors: Record<string, string> = {};
private statusIcons: Record<TStatusType, string> = {
operational: 'lucide:CheckCircle',
degraded: 'lucide:AlertTriangle',
partial_outage: 'lucide:AlertOctagon',
major_outage: 'lucide:XCircle',
maintenance: 'lucide:Wrench',
private checkTypeLabels: Record<TCheckType, string> = {
assumption: 'Assumption',
function: 'Function',
pwa: 'PWA',
pagerank: 'PageRank',
};
private getCheckTypeLabel(): string {
return this.checkTypeLabels[this.formData.checkType] || 'Assumption';
}
private getStatusModeLabel(): string {
return this.formData.statusMode === 'auto' ? 'Auto' : 'Manual';
}
private intervalOptions = [
{ value: 60000, label: '1 min' },
{ value: 300000, label: '5 min' },
{ value: 900000, label: '15 min' },
{ value: 1800000, label: '30 min' },
{ value: 3600000, label: '1 hour' },
];
public static styles = [
plugins.domtools.elementBasic.staticStyles,
sharedStyles.commonStyles,
css`
:host {
display: block;
font-family: ${unsafeCSS(sharedStyles.fonts.base)};
}
.form-container {
background: ${sharedStyles.colors.background.secondary};
border: 1px solid ${sharedStyles.colors.border.default};
border-radius: ${unsafeCSS(sharedStyles.borderRadius.lg)};
background: ${cssManager.bdTheme('#ffffff', '#09090b')};
border: 1px solid ${cssManager.bdTheme('#e5e7eb', '#27272a')};
border-radius: 12px;
overflow: hidden;
}
.form-header {
display: flex;
align-items: center;
gap: ${unsafeCSS(sharedStyles.spacing.md)};
padding: ${unsafeCSS(sharedStyles.spacing.lg)};
border-bottom: 1px solid ${sharedStyles.colors.border.default};
background: ${sharedStyles.colors.background.muted};
gap: 12px;
padding: 20px 24px;
border-bottom: 1px solid ${cssManager.bdTheme('#e5e7eb', '#27272a')};
}
.form-header dees-icon {
--icon-color: ${cssManager.bdTheme('#3b82f6', '#60a5fa')};
}
.form-title-wrapper {
flex: 1;
}
.form-title {
font-size: 18px;
font-weight: 600;
color: ${sharedStyles.colors.text.primary};
color: ${cssManager.bdTheme('#09090b', '#fafafa')};
margin: 0;
}
.form-subtitle {
font-size: 13px;
color: ${sharedStyles.colors.text.muted};
margin-top: 4px;
font-size: 14px;
color: ${cssManager.bdTheme('#71717a', '#a1a1aa')};
margin: 4px 0 0 0;
}
.form-body {
display: grid;
gap: ${unsafeCSS(sharedStyles.spacing.lg)};
padding: ${unsafeCSS(sharedStyles.spacing.lg)};
}
dees-form {
display: contents;
padding: 24px;
display: flex;
flex-direction: column;
gap: 24px;
}
.form-row {
display: grid;
grid-template-columns: 1fr 1fr;
gap: ${unsafeCSS(sharedStyles.spacing.md)};
gap: 16px;
}
@media (max-width: 600px) {
@@ -127,89 +136,64 @@ export class UpladminMonitorForm extends DeesElement {
}
}
.form-section {
display: flex;
flex-direction: column;
gap: 16px;
}
.section-label {
font-size: 14px;
font-weight: 500;
color: ${cssManager.bdTheme('#09090b', '#fafafa')};
margin: 0;
}
.config-box {
background: ${cssManager.bdTheme('#fafafa', '#18181b')};
border: 1px solid ${cssManager.bdTheme('#e5e7eb', '#27272a')};
border-radius: 8px;
padding: 16px;
display: flex;
flex-direction: column;
gap: 16px;
}
.editor-container {
border: 1px solid ${cssManager.bdTheme('#e5e7eb', '#27272a')};
border-radius: 8px;
overflow: hidden;
}
.editor-container dees-editor {
height: 180px;
display: block;
}
.editor-hint {
font-size: 12px;
color: ${cssManager.bdTheme('#71717a', '#a1a1aa')};
margin-top: 8px;
}
.form-actions {
display: flex;
justify-content: flex-end;
gap: ${unsafeCSS(sharedStyles.spacing.sm)};
padding: ${unsafeCSS(sharedStyles.spacing.md)} ${unsafeCSS(sharedStyles.spacing.lg)};
border-top: 1px solid ${sharedStyles.colors.border.default};
background: ${sharedStyles.colors.background.muted};
gap: 8px;
padding: 16px 24px;
border-top: 1px solid ${cssManager.bdTheme('#e5e7eb', '#27272a')};
background: ${cssManager.bdTheme('#fafafa', '#18181b')};
}
.status-section {
margin-top: ${unsafeCSS(sharedStyles.spacing.sm)};
}
.status-options {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(140px, 1fr));
gap: ${unsafeCSS(sharedStyles.spacing.sm)};
}
.status-option {
.search-engines {
display: flex;
align-items: center;
gap: 12px;
padding: 14px 16px;
background: ${sharedStyles.colors.background.primary};
border: 2px solid ${sharedStyles.colors.border.default};
border-radius: ${unsafeCSS(sharedStyles.borderRadius.base)};
cursor: pointer;
transition: all ${unsafeCSS(sharedStyles.durations.fast)} ${unsafeCSS(sharedStyles.easings.default)};
}
.status-option:hover {
border-color: ${sharedStyles.colors.border.strong};
background: ${sharedStyles.colors.background.muted};
}
.status-option.selected {
border-color: ${sharedStyles.colors.accent.primary};
background: ${cssManager.bdTheme('rgba(59, 130, 246, 0.05)', 'rgba(96, 165, 250, 0.1)')};
}
.status-option input {
display: none;
}
.status-option dees-icon {
flex-shrink: 0;
}
.status-option.operational dees-icon { --icon-color: ${sharedStyles.colors.status.operational}; }
.status-option.degraded dees-icon { --icon-color: ${sharedStyles.colors.status.degraded}; }
.status-option.partial_outage dees-icon { --icon-color: ${sharedStyles.colors.status.partialOutage}; }
.status-option.major_outage dees-icon { --icon-color: ${sharedStyles.colors.status.majorOutage}; }
.status-option.maintenance dees-icon { --icon-color: ${sharedStyles.colors.status.maintenance}; }
.status-label {
font-size: 14px;
font-weight: 500;
color: ${sharedStyles.colors.text.primary};
}
.field-label {
display: block;
font-size: 13px;
font-weight: 500;
color: ${sharedStyles.colors.text.primary};
margin-bottom: ${unsafeCSS(sharedStyles.spacing.sm)};
}
.field-label.required::after {
content: ' *';
color: ${sharedStyles.colors.accent.danger};
}
/* Style dees-input components */
dees-input-text,
dees-input-dropdown {
--dees-input-background: ${sharedStyles.colors.background.primary};
--dees-input-border-color: ${sharedStyles.colors.border.default};
gap: 16px;
}
`
];
private subscriptions: Array<{ unsubscribe: () => void }> = [];
async connectedCallback() {
await super.connectedCallback();
if (this.monitor) {
@@ -217,121 +201,185 @@ export class UpladminMonitorForm extends DeesElement {
}
}
async disconnectedCallback() {
await super.disconnectedCallback();
this.subscriptions.forEach(sub => sub.unsubscribe());
this.subscriptions = [];
}
async firstUpdated() {
await this.updateComplete;
this.setupMultitoggleSubscriptions();
}
updated(changedProperties: Map<string, unknown>) {
if (changedProperties.has('monitor') && this.monitor) {
this.formData = { ...this.monitor };
}
// Re-setup subscriptions after each render in case elements changed
this.setupMultitoggleSubscriptions();
}
private subscribedElements = new WeakSet<Element>();
private setupMultitoggleSubscriptions() {
// Subscribe to check type toggle (only if not already subscribed)
const checkTypeToggle = this.shadowRoot?.querySelector('#checkTypeToggle') as DeesInputMultitoggle;
if (checkTypeToggle && !this.subscribedElements.has(checkTypeToggle)) {
this.subscribedElements.add(checkTypeToggle);
const sub = checkTypeToggle.changeSubject.subscribe(() => {
this.handleCheckTypeChange(checkTypeToggle.selectedOption);
});
this.subscriptions.push(sub);
}
// Subscribe to status mode toggle (only if not already subscribed)
const statusModeToggle = this.shadowRoot?.querySelector('#statusModeToggle') as DeesInputMultitoggle;
if (statusModeToggle && !this.subscribedElements.has(statusModeToggle)) {
this.subscribedElements.add(statusModeToggle);
const sub = statusModeToggle.changeSubject.subscribe(() => {
this.handleStatusModeChange(statusModeToggle.selectedOption);
});
this.subscriptions.push(sub);
}
}
public render(): TemplateResult {
const isEdit = !!this.monitor?.id;
const statusOptions: Array<{ value: TStatusType; label: string }> = [
{ value: 'operational', label: 'Operational' },
{ value: 'degraded', label: 'Degraded' },
{ value: 'partial_outage', label: 'Partial Outage' },
{ value: 'major_outage', label: 'Major Outage' },
{ value: 'maintenance', label: 'Maintenance' },
];
const categoryOptions = this.categories.map(cat => ({ key: cat, option: cat, payload: null }));
const dependencyOptions = this.availableMonitors
.filter(m => m.id !== this.monitor?.id)
.map(m => ({ key: m.id, option: m.displayName || m.name, payload: null }));
const intervalOptions = this.intervalOptions.map(opt => ({ key: String(opt.value), option: opt.label, payload: null }));
return html`
<div class="form-container">
<div class="form-header">
<dees-icon .icon=${isEdit ? 'lucide:Pencil' : 'lucide:Plus'} .iconSize=${24}></dees-icon>
<div class="form-title-wrapper">
<div>
<h2 class="form-title">${isEdit ? 'Edit Monitor' : 'Create Monitor'}</h2>
<p class="form-subtitle">
${isEdit ? 'Update the monitor configuration' : 'Add a new service to monitor'}
</p>
<p class="form-subtitle">${isEdit ? 'Update monitor configuration' : 'Add a new service to monitor'}</p>
</div>
</div>
<div class="form-body">
<dees-form>
<!-- Basic Info -->
<div class="form-row">
<dees-input-text
key="name"
label="Internal Name"
.value="${this.formData.name}"
placeholder="api-server"
required
description="Lowercase, no spaces. Used as identifier."
@changeSubject="${this.handleNameChange}"
.label=${'Internal Name'}
.value=${this.formData.name}
.placeholder=${'api-server'}
.required=${true}
.description=${'Lowercase, no spaces'}
@changeSubject=${(e: CustomEvent) => this.updateField('name', e.detail.value)}
></dees-input-text>
<dees-input-text
key="displayName"
label="Display Name"
.value="${this.formData.displayName}"
placeholder="API Server"
required
description="Human-readable name shown to users."
@changeSubject="${this.handleDisplayNameChange}"
.label=${'Display Name'}
.value=${this.formData.displayName}
.placeholder=${'API Server'}
.required=${true}
.description=${'Human-readable name'}
@changeSubject=${(e: CustomEvent) => this.updateField('displayName', e.detail.value)}
></dees-input-text>
</div>
<dees-input-text
key="description"
label="Description"
inputType="textarea"
.value="${this.formData.description || ''}"
placeholder="Brief description of what this service does..."
@changeSubject="${this.handleDescriptionChange}"
.label=${'Description'}
.inputType=${'textarea'}
.value=${this.formData.description || ''}
.placeholder=${'Brief description of this service...'}
@changeSubject=${(e: CustomEvent) => this.updateField('description', e.detail.value)}
></dees-input-text>
<div class="form-row">
<dees-input-dropdown
key="category"
label="Category"
.options="${categoryOptions}"
.selectedOption="${this.formData.category || ''}"
placeholder="Select category..."
@selectedOption="${this.handleCategoryChange}"
.label=${'Category'}
.options=${categoryOptions}
.selectedOption=${this.formData.category || ''}
@selectedOption=${(e: CustomEvent) => this.updateField('category', e.detail)}
></dees-input-dropdown>
<dees-input-dropdown
key="dependencies"
label="Dependencies"
.options="${dependencyOptions}"
.selectedOptions="${this.formData.dependencies || []}"
multiple
description="Services this monitor depends on."
@selectedOption="${this.handleDependenciesChange}"
.label=${'Dependencies'}
.options=${dependencyOptions}
.selectedOptions=${this.formData.dependencies || []}
.multiple=${true}
@selectedOption=${(e: CustomEvent) => this.updateField('dependencies', Array.isArray(e.detail) ? e.detail : [e.detail])}
></dees-input-dropdown>
</div>
<div class="status-section">
<label class="field-label required">Current Status</label>
<div class="status-options">
${statusOptions.map(opt => html`
<label
class="status-option ${opt.value} ${this.formData.currentStatus === opt.value ? 'selected' : ''}"
@click="${() => this.handleStatusChange(opt.value)}"
>
<input
type="radio"
name="currentStatus"
value="${opt.value}"
?checked="${this.formData.currentStatus === opt.value}"
/>
<dees-icon .icon=${this.statusIcons[opt.value]} .iconSize=${20}></dees-icon>
<span class="status-label">${opt.label}</span>
</label>
`)}
<!-- Check Type -->
<div class="form-section">
<dees-input-multitoggle
id="checkTypeToggle"
.label=${'Check Type'}
.options=${['Assumption', 'Function', 'PWA', 'PageRank']}
.selectedOption=${this.getCheckTypeLabel()}
></dees-input-multitoggle>
</div>
<!-- Check Configuration -->
<div class="form-section">
<div class="config-box">
${this.renderCheckConfigFields()}
</div>
</div>
<!-- Interval -->
<dees-input-dropdown
.label=${'Check Interval'}
.options=${intervalOptions}
.selectedOption=${String(this.formData.intervalMs)}
@selectedOption=${(e: CustomEvent) => this.updateField('intervalMs', parseInt(e.detail))}
></dees-input-dropdown>
<!-- Pause Toggle -->
<dees-input-checkbox
.label=${'Pause Monitor'}
.description=${'When paused, status will show as "paused" and checks won\'t run'}
.value=${this.formData.paused}
@newValue=${(e: CustomEvent) => this.updateField('paused', e.detail)}
></dees-input-checkbox>
<!-- Status Override (Edit mode only) -->
${isEdit ? html`
<div class="form-section">
<dees-input-multitoggle
id="statusModeToggle"
.label=${'Status Mode'}
.description=${'Auto uses check results, Manual lets you override'}
.options=${['Auto', 'Manual']}
.selectedOption=${this.getStatusModeLabel()}
></dees-input-multitoggle>
${this.formData.statusMode === 'manual' ? html`
<dees-input-radiogroup
.label=${'Manual Status'}
.options=${[
{ key: 'operational', option: 'Operational' },
{ key: 'degraded', option: 'Degraded' },
{ key: 'partial_outage', option: 'Partial Outage' },
{ key: 'major_outage', option: 'Major Outage' },
{ key: 'maintenance', option: 'Maintenance' },
]}
.selectedOption=${this.formData.manualStatus || 'operational'}
.direction=${'horizontal'}
@change=${(e: CustomEvent) => this.updateField('manualStatus', e.detail.value)}
></dees-input-radiogroup>
` : ''}
</div>
` : ''}
</dees-form>
</div>
<div class="form-actions">
<dees-button type="discreet" @click="${this.handleCancel}" ?disabled="${this.loading}">
<dees-button .type=${'discreet'} @click=${this.handleCancel} ?disabled=${this.loading}>
Cancel
</dees-button>
<dees-button type="highlighted" @click="${this.handleSave}" ?disabled="${this.loading}">
<dees-button .type=${'highlighted'} @click=${this.handleSave} ?disabled=${this.loading}>
${this.loading ? html`<dees-spinner .size=${16}></dees-spinner>` : ''}
${isEdit ? 'Update Monitor' : 'Create Monitor'}
</dees-button>
@@ -340,40 +388,168 @@ export class UpladminMonitorForm extends DeesElement {
`;
}
private handleNameChange(e: CustomEvent) {
this.formData = { ...this.formData, name: e.detail };
if (this.errors.name) {
this.errors = { ...this.errors, name: '' };
private renderCheckConfigFields(): TemplateResult {
const config = this.formData.checkConfig;
switch (this.formData.checkType) {
case 'assumption':
return html`
<dees-input-text
.label=${'Domain'}
.value=${config.domain || ''}
.placeholder=${'api.example.com'}
.required=${true}
@changeSubject=${(e: CustomEvent) => this.updateCheckConfig('domain', e.detail.value)}
></dees-input-text>
<div class="form-row">
<dees-input-text
.label=${'Expected Status Code'}
.value=${config.expectedStatusCode || ''}
.placeholder=${'200'}
@changeSubject=${(e: CustomEvent) => this.updateCheckConfig('expectedStatusCode', e.detail.value)}
></dees-input-text>
<dees-input-text
.label=${'Expected Title'}
.value=${config.expectedTitle || ''}
.placeholder=${'Page Title'}
@changeSubject=${(e: CustomEvent) => this.updateCheckConfig('expectedTitle', e.detail.value)}
></dees-input-text>
</div>
`;
case 'function':
return html`
<dees-input-text
.label=${'Domain'}
.value=${config.domain || ''}
.placeholder=${'api.example.com'}
.required=${true}
@changeSubject=${(e: CustomEvent) => this.updateCheckConfig('domain', e.detail.value)}
></dees-input-text>
<div>
<p class="section-label">Function Definition</p>
<div class="editor-container">
<dees-editor
.value=${config.functionDef || `async (context: { domain: string }) => {
const response = await fetch(\`https://\${context.domain}\`);
return response.status === 200;
}`}
.language=${'typescript'}
.options=${{
lineNumbers: 'on',
minimap: { enabled: false },
scrollBeyondLastLine: false,
fontSize: 13,
tabSize: 2,
}}
@change=${(e: CustomEvent) => this.updateCheckConfig('functionDef', e.detail)}
></dees-editor>
</div>
<p class="editor-hint">Return true for success, false for failure</p>
</div>
`;
case 'pwa':
return html`
<dees-input-text
.label=${'Domain'}
.value=${config.domain || ''}
.placeholder=${'example.com'}
.required=${true}
.description=${'Domain to run Lighthouse PWA analysis on'}
@changeSubject=${(e: CustomEvent) => this.updateCheckConfig('domain', e.detail.value)}
></dees-input-text>
`;
case 'pagerank':
return html`
<div class="form-row">
<dees-input-text
.label=${'Domain'}
.value=${config.domain || ''}
.placeholder=${'example.com'}
.required=${true}
@changeSubject=${(e: CustomEvent) => this.updateCheckConfig('domain', e.detail.value)}
></dees-input-text>
<dees-input-text
.label=${'Search Term'}
.value=${config.searchTerm || ''}
.placeholder=${'your brand name'}
.required=${true}
@changeSubject=${(e: CustomEvent) => this.updateCheckConfig('searchTerm', e.detail.value)}
></dees-input-text>
</div>
<div class="search-engines">
<dees-input-checkbox
.label=${'Google'}
.value=${config.checkGoogle !== false}
@newValue=${(e: CustomEvent) => this.updateCheckConfig('checkGoogle', e.detail)}
></dees-input-checkbox>
<dees-input-checkbox
.label=${'Bing'}
.value=${config.checkBing === true}
@newValue=${(e: CustomEvent) => this.updateCheckConfig('checkBing', e.detail)}
></dees-input-checkbox>
</div>
<div class="form-row">
${config.checkGoogle !== false ? html`
<dees-input-text
.label=${'Google Min Rank'}
.value=${config.googleMinRank?.toString() || ''}
.placeholder=${'10'}
.description=${'Alert if rank drops below this'}
@changeSubject=${(e: CustomEvent) => this.updateCheckConfig('googleMinRank', parseInt(e.detail.value) || undefined)}
></dees-input-text>
` : ''}
${config.checkBing ? html`
<dees-input-text
.label=${'Bing Min Rank'}
.value=${config.bingMinRank?.toString() || ''}
.placeholder=${'10'}
.description=${'Alert if rank drops below this'}
@changeSubject=${(e: CustomEvent) => this.updateCheckConfig('bingMinRank', parseInt(e.detail.value) || undefined)}
></dees-input-text>
` : ''}
</div>
`;
default:
return html``;
}
}
private handleDisplayNameChange(e: CustomEvent) {
this.formData = { ...this.formData, displayName: e.detail };
if (this.errors.displayName) {
this.errors = { ...this.errors, displayName: '' };
private updateField(field: keyof IMonitorFormData, value: any) {
this.formData = { ...this.formData, [field]: value };
if (this.errors[field]) {
this.errors = { ...this.errors, [field]: '' };
}
}
private handleDescriptionChange(e: CustomEvent) {
this.formData = { ...this.formData, description: e.detail };
private updateCheckConfig(field: keyof ICheckConfig, value: any) {
this.formData = {
...this.formData,
checkConfig: { ...this.formData.checkConfig, [field]: value },
};
}
private handleCategoryChange(e: CustomEvent) {
this.formData = { ...this.formData, category: e.detail };
private handleCheckTypeChange(label: string) {
const checkType = (Object.keys(this.checkTypeLabels) as TCheckType[])
.find(key => this.checkTypeLabels[key] === label) || 'assumption';
const domain = this.formData.checkConfig?.domain || '';
this.formData = {
...this.formData,
checkType,
checkConfig: { domain },
};
}
private handleDependenciesChange(e: CustomEvent) {
const selected = e.detail;
if (Array.isArray(selected)) {
this.formData = { ...this.formData, dependencies: selected };
} else if (selected) {
// Single selection mode, convert to array
this.formData = { ...this.formData, dependencies: [selected] };
}
}
private handleStatusChange(status: TStatusType) {
this.formData = { ...this.formData, currentStatus: status };
private handleStatusModeChange(label: string) {
const mode: TStatusMode = label === 'Auto' ? 'auto' : 'manual';
this.formData = {
...this.formData,
statusMode: mode,
manualStatus: mode === 'manual' && !this.formData.manualStatus ? 'operational' : this.formData.manualStatus,
};
}
private validate(): boolean {
@@ -389,6 +565,14 @@ export class UpladminMonitorForm extends DeesElement {
errors.displayName = 'Display name is required';
}
if (!this.formData.checkConfig?.domain?.trim()) {
errors.domain = 'Domain is required';
}
if (this.formData.checkType === 'pagerank' && !this.formData.checkConfig?.searchTerm?.trim()) {
errors.searchTerm = 'Search term is required for PageRank checks';
}
this.errors = errors;
return Object.keys(errors).length === 0;
}
@@ -419,7 +603,11 @@ export class UpladminMonitorForm extends DeesElement {
description: '',
category: '',
dependencies: [],
currentStatus: 'operational',
statusMode: 'auto',
paused: false,
checkType: 'assumption',
checkConfig: { domain: '' },
intervalMs: 300000,
};
this.errors = {};
}

View File

@@ -11,7 +11,7 @@ import {
state,
} from '@design.estate/dees-element';
import * as sharedStyles from '../../styles/shared.styles.js';
import type { IServiceStatus } from '../../interfaces/index.js';
import type { IServiceStatus, TStatusType } from '../../interfaces/index.js';
import { demoFunc } from './upladmin-monitor-list.demo.js';
import type { Column, ITableAction, DeesTable } from '@design.estate/dees-catalog';
@@ -21,8 +21,6 @@ declare global {
}
}
type TStatusType = 'operational' | 'degraded' | 'partial_outage' | 'major_outage' | 'maintenance';
@customElement('upladmin-monitor-list')
export class UpladminMonitorList extends DeesElement {
public static demo = demoFunc;
@@ -45,6 +43,9 @@ export class UpladminMonitorList extends DeesElement {
partial_outage: 'lucide:AlertOctagon',
major_outage: 'lucide:XCircle',
maintenance: 'lucide:Wrench',
initializing: 'lucide:Loader',
error: 'lucide:AlertCircle',
paused: 'lucide:PauseCircle',
};
private statusLabels: Record<TStatusType, string> = {
@@ -53,6 +54,9 @@ export class UpladminMonitorList extends DeesElement {
partial_outage: 'Partial Outage',
major_outage: 'Major Outage',
maintenance: 'Maintenance',
initializing: 'Initializing',
error: 'Monitor Error',
paused: 'Paused',
};
public static styles = [
@@ -124,6 +128,12 @@ export class UpladminMonitorList extends DeesElement {
box-shadow: 0 0 0 3px ${cssManager.bdTheme('rgba(59, 130, 246, 0.1)', 'rgba(96, 165, 250, 0.15)')};
}
@media (prefers-color-scheme: dark) {
.filter-select {
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 12 12'%3E%3Cpath fill='%23a1a1aa' d='M2.5 4.5L6 8l3.5-3.5'/%3E%3C/svg%3E");
}
}
.table-container {
padding: 0;
}
@@ -183,6 +193,51 @@ export class UpladminMonitorList extends DeesElement {
--icon-color: ${sharedStyles.colors.status.maintenance};
}
.status-badge.initializing {
background: ${cssManager.bdTheme('rgba(107, 114, 128, 0.1)', 'rgba(107, 114, 128, 0.15)')};
color: ${cssManager.bdTheme('#6b7280', '#9ca3af')};
--icon-color: ${cssManager.bdTheme('#6b7280', '#9ca3af')};
}
.status-badge.error {
background: ${cssManager.bdTheme('rgba(220, 38, 38, 0.1)', 'rgba(220, 38, 38, 0.15)')};
color: ${cssManager.bdTheme('#dc2626', '#f87171')};
--icon-color: ${cssManager.bdTheme('#dc2626', '#f87171')};
}
.status-badge.paused {
background: ${cssManager.bdTheme('rgba(139, 92, 246, 0.1)', 'rgba(139, 92, 246, 0.15)')};
color: ${cssManager.bdTheme('#8b5cf6', '#a78bfa')};
--icon-color: ${cssManager.bdTheme('#8b5cf6', '#a78bfa')};
}
/* Status indicators for override and pause */
.status-cell {
display: flex;
align-items: center;
gap: 8px;
}
.status-indicator {
display: inline-flex;
align-items: center;
justify-content: center;
width: 20px;
height: 20px;
border-radius: 4px;
font-size: 12px;
}
.status-indicator.override {
background: ${cssManager.bdTheme('rgba(234, 179, 8, 0.15)', 'rgba(234, 179, 8, 0.2)')};
--icon-color: ${cssManager.bdTheme('#d97706', '#fbbf24')};
}
.status-indicator.paused {
background: ${cssManager.bdTheme('rgba(139, 92, 246, 0.15)', 'rgba(139, 92, 246, 0.2)')};
--icon-color: ${cssManager.bdTheme('#8b5cf6', '#a78bfa')};
}
.monitor-info {
display: flex;
flex-direction: column;
@@ -316,10 +371,22 @@ export class UpladminMonitorList extends DeesElement {
header: 'Status',
sortable: true,
renderer: (value, item) => html`
<div class="status-cell">
<span class="status-badge ${item.currentStatus}">
<dees-icon .icon=${this.statusIcons[item.currentStatus]} .iconSize=${14}></dees-icon>
${this.statusLabels[item.currentStatus]}
</span>
${item.statusMode === 'manual' ? html`
<span class="status-indicator override" title="Manual Override">
<dees-icon .icon=${'lucide:AlertTriangle'} .iconSize=${12}></dees-icon>
</span>
` : ''}
${item.paused && item.currentStatus !== 'paused' ? html`
<span class="status-indicator paused" title="Execution Paused">
<dees-icon .icon=${'lucide:Pause'} .iconSize=${12}></dees-icon>
</span>
` : ''}
</div>
`,
},
{
@@ -391,6 +458,9 @@ export class UpladminMonitorList extends DeesElement {
<option value="partial_outage" ?selected="${this.statusFilter === 'partial_outage'}">Partial Outage</option>
<option value="major_outage" ?selected="${this.statusFilter === 'major_outage'}">Major Outage</option>
<option value="maintenance" ?selected="${this.statusFilter === 'maintenance'}">Maintenance</option>
<option value="paused" ?selected="${this.statusFilter === 'paused'}">Paused</option>
<option value="initializing" ?selected="${this.statusFilter === 'initializing'}">Initializing</option>
<option value="error" ?selected="${this.statusFilter === 'error'}">Monitor Error</option>
</select>
${this.categories.length > 0 ? html`

View File

@@ -0,0 +1 @@
export * from './upladmin-option-card.js';

View File

@@ -0,0 +1,118 @@
import { html, css, cssManager } from '@design.estate/dees-element';
import type { UpladminOptionCard } from './upladmin-option-card.js';
export const demoFunc = () => html`
<style>
.demo-container {
padding: 24px;
background: ${cssManager.bdTheme('#fafafa', '#0a0a0a')};
min-height: 100vh;
}
.demo-section {
margin-bottom: 32px;
}
.demo-title {
font-size: 14px;
font-weight: 600;
color: ${cssManager.bdTheme('#09090b', '#fafafa')};
margin-bottom: 16px;
}
.demo-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(130px, 1fr));
gap: 12px;
}
</style>
<div class="demo-container">
<div class="demo-section">
<div class="demo-title">Severity Variants</div>
<div class="demo-grid">
<upladmin-option-card
variant="critical"
icon="lucide:AlertCircle"
label="Critical"
description="Severe impact"
></upladmin-option-card>
<upladmin-option-card
variant="major"
icon="lucide:AlertTriangle"
label="Major"
description="Significant impact"
></upladmin-option-card>
<upladmin-option-card
variant="minor"
icon="lucide:Info"
label="Minor"
description="Limited impact"
></upladmin-option-card>
<upladmin-option-card
variant="maintenance"
icon="lucide:Wrench"
label="Maintenance"
description="Scheduled work"
selected
></upladmin-option-card>
</div>
</div>
<div class="demo-section">
<div class="demo-title">Status Variants</div>
<div class="demo-grid">
<upladmin-option-card
variant="investigating"
icon="lucide:Search"
label="Investigating"
description="Looking into it"
></upladmin-option-card>
<upladmin-option-card
variant="identified"
icon="lucide:Target"
label="Identified"
description="Root cause found"
></upladmin-option-card>
<upladmin-option-card
variant="monitoring"
icon="lucide:Eye"
label="Monitoring"
description="Fix applied"
selected
></upladmin-option-card>
<upladmin-option-card
variant="resolved"
icon="lucide:CheckCircle"
label="Resolved"
description="Issue fixed"
></upladmin-option-card>
<upladmin-option-card
variant="postmortem"
icon="lucide:FileText"
label="Postmortem"
description="Analysis complete"
></upladmin-option-card>
</div>
</div>
<div class="demo-section">
<div class="demo-title">States</div>
<div class="demo-grid">
<upladmin-option-card
variant="primary"
icon="lucide:Star"
label="Normal"
></upladmin-option-card>
<upladmin-option-card
variant="primary"
icon="lucide:Star"
label="Selected"
selected
></upladmin-option-card>
<upladmin-option-card
variant="primary"
icon="lucide:Star"
label="Disabled"
disabled
></upladmin-option-card>
</div>
</div>
</div>
`;

View File

@@ -0,0 +1,178 @@
import {
DeesElement,
property,
html,
customElement,
type TemplateResult,
css,
cssManager,
unsafeCSS,
} from '@design.estate/dees-element';
import * as sharedStyles from '../../styles/shared.styles.js';
import { demoFunc } from './upladmin-option-card.demo.js';
declare global {
interface HTMLElementTagNameMap {
'upladmin-option-card': UpladminOptionCard;
}
}
export type TOptionVariant =
// Severity variants
| 'critical' | 'major' | 'minor' | 'maintenance'
// Status variants
| 'investigating' | 'identified' | 'monitoring' | 'resolved' | 'postmortem'
// Generic variants
| 'default' | 'primary' | 'success' | 'warning' | 'danger' | 'info';
@customElement('upladmin-option-card')
export class UpladminOptionCard extends DeesElement {
public static demo = demoFunc;
@property({ type: String })
accessor icon: string = '';
@property({ type: String })
accessor label: string = '';
@property({ type: String })
accessor description: string = '';
@property({ type: String, reflect: true })
accessor variant: TOptionVariant = 'default';
@property({ type: Boolean, reflect: true })
accessor selected: boolean = false;
@property({ type: Boolean, reflect: true })
accessor disabled: boolean = false;
public static styles = [
cssManager.defaultStyles,
css`
:host {
display: block;
font-family: ${unsafeCSS(sharedStyles.fonts.base)};
}
.option-card {
display: flex;
flex-direction: column;
align-items: center;
gap: 10px;
padding: 18px 14px;
background: ${cssManager.bdTheme('#ffffff', '#09090b')};
border: 2px solid ${cssManager.bdTheme('#e4e4e7', '#27272a')};
border-radius: 6px;
cursor: pointer;
transition: all 0.1s ease;
text-align: center;
user-select: none;
}
.option-card:hover:not(.disabled) {
border-color: ${cssManager.bdTheme('#d4d4d8', '#3f3f46')};
background: ${cssManager.bdTheme('#f4f4f5', '#18181b')};
}
:host([selected]) .option-card {
border-color: ${cssManager.bdTheme('#3b82f6', '#60a5fa')};
background: ${cssManager.bdTheme('rgba(59, 130, 246, 0.05)', 'rgba(96, 165, 250, 0.1)')};
}
:host([disabled]) .option-card {
opacity: 0.5;
cursor: not-allowed;
}
.option-label {
font-size: 13px;
font-weight: 600;
color: ${cssManager.bdTheme('#09090b', '#fafafa')};
}
.option-desc {
font-size: 11px;
color: ${cssManager.bdTheme('#a1a1aa', '#71717a')};
line-height: 1.3;
}
/* Variant icon colors - all using bdTheme for proper light/dark support */
/* Severity variants */
:host([variant="critical"]) dees-icon {
--icon-color: ${cssManager.bdTheme('#dc2626', '#f87171')};
}
:host([variant="major"]) dees-icon {
--icon-color: ${cssManager.bdTheme('#ea580c', '#fb923c')};
}
:host([variant="minor"]) dees-icon {
--icon-color: ${cssManager.bdTheme('#ca8a04', '#fbbf24')};
}
:host([variant="maintenance"]) dees-icon {
--icon-color: ${cssManager.bdTheme('#2563eb', '#60a5fa')};
}
/* Status variants */
:host([variant="investigating"]) dees-icon {
--icon-color: ${cssManager.bdTheme('#ea580c', '#fb923c')};
}
:host([variant="identified"]) dees-icon {
--icon-color: ${cssManager.bdTheme('#ca8a04', '#fbbf24')};
}
:host([variant="monitoring"]) dees-icon {
--icon-color: ${cssManager.bdTheme('#2563eb', '#60a5fa')};
}
:host([variant="resolved"]) dees-icon {
--icon-color: ${cssManager.bdTheme('#16a34a', '#22c55e')};
}
:host([variant="postmortem"]) dees-icon {
--icon-color: ${cssManager.bdTheme('#9333ea', '#a855f7')};
}
/* Generic variants */
:host([variant="default"]) dees-icon {
--icon-color: ${cssManager.bdTheme('#71717a', '#a1a1aa')};
}
:host([variant="primary"]) dees-icon {
--icon-color: ${cssManager.bdTheme('#3b82f6', '#60a5fa')};
}
:host([variant="success"]) dees-icon {
--icon-color: ${cssManager.bdTheme('#16a34a', '#22c55e')};
}
:host([variant="warning"]) dees-icon {
--icon-color: ${cssManager.bdTheme('#ca8a04', '#fbbf24')};
}
:host([variant="danger"]) dees-icon {
--icon-color: ${cssManager.bdTheme('#dc2626', '#f87171')};
}
:host([variant="info"]) dees-icon {
--icon-color: ${cssManager.bdTheme('#2563eb', '#60a5fa')};
}
dees-icon {
color: var(--icon-color);
}
`,
];
public render(): TemplateResult {
return html`
<div class="option-card ${this.disabled ? 'disabled' : ''}" @click="${this.handleClick}">
${this.icon ? html`<dees-icon .icon=${this.icon} .iconSize=${24}></dees-icon>` : ''}
${this.label ? html`<span class="option-label">${this.label}</span>` : ''}
${this.description ? html`<span class="option-desc">${this.description}</span>` : ''}
</div>
`;
}
private handleClick() {
if (this.disabled) return;
this.dispatchEvent(new CustomEvent('select', {
detail: { selected: !this.selected },
bubbles: true,
composed: true,
}));
}
}

View File

@@ -1,10 +1,33 @@
// Re-export interfaces from the public catalog for consistency
// Status types
export type TStatusType = 'operational' | 'degraded' | 'partial_outage' | 'major_outage' | 'maintenance' | 'initializing' | 'error' | 'paused';
export type TCheckType = 'assumption' | 'function' | 'pwa' | 'pagerank';
export type TStatusMode = 'auto' | 'manual';
// Check configuration interface
export interface ICheckConfig {
domain: string;
// Assumption check fields
expectedTitle?: string;
expectedStatusCode?: string;
expectedDescription?: string;
// Function check fields
functionDef?: string;
// PageRank check fields
searchTerm?: string;
checkBing?: boolean;
checkGoogle?: boolean;
bingMinRank?: number;
googleMinRank?: number;
}
export interface IServiceStatus {
id: string;
name: string;
displayName: string;
description?: string;
currentStatus: 'operational' | 'degraded' | 'partial_outage' | 'major_outage' | 'maintenance';
currentStatus: TStatusType;
lastChecked: number;
uptime30d: number;
uptime90d: number;
@@ -12,11 +35,19 @@ export interface IServiceStatus {
category?: string;
dependencies?: string[];
selected?: boolean;
// Status management
statusMode?: TStatusMode;
manualStatus?: TStatusType;
paused?: boolean;
// Check configuration
checkType?: TCheckType;
checkConfig?: ICheckConfig;
intervalMs?: number;
}
export interface IStatusHistoryPoint {
timestamp: number;
status: 'operational' | 'degraded' | 'partial_outage' | 'major_outage' | 'maintenance';
status: TStatusType;
responseTime?: number;
errorRate?: number;
}
@@ -44,7 +75,7 @@ export interface IIncidentDetails {
}
export interface IOverallStatus {
status: 'operational' | 'degraded' | 'partial_outage' | 'major_outage' | 'maintenance';
status: TStatusType;
message: string;
lastUpdated: number;
affectedServices: number;
@@ -77,7 +108,14 @@ export interface IMonitorFormData {
description?: string;
category?: string;
dependencies?: string[];
currentStatus: 'operational' | 'degraded' | 'partial_outage' | 'major_outage' | 'maintenance';
// Status management
statusMode: TStatusMode;
manualStatus?: TStatusType;
paused: boolean;
// Check configuration
checkType: TCheckType;
checkConfig: ICheckConfig;
intervalMs: number;
}
export interface IIncidentFormData {