feat(admin-ui): introduce view layer and refactor admin UI to use view components, consolidate demos, and update interfaces

This commit is contained in:
2025-12-27 12:33:14 +00:00
parent 87ac6e506f
commit c5632dae77
18 changed files with 875 additions and 754 deletions

View File

@@ -1,5 +1,15 @@
# Changelog
## 2025-12-27 - 1.3.0 - feat(admin-ui)
introduce view layer and refactor admin UI to use view components, consolidate demos, and update interfaces
- Added a new views/ layer with view components: upladmin-dashboard-view, upladmin-monitors-view, upladmin-incidents-view, upladmin-config-view and exported them from ts_web/views/index.ts
- Refactored upladmin-app to use the new view components (updated menu routes/content to view tags)
- Removed multiple demo page files under ts_web/pages (consolidated demo surface into the view-based app)
- Updated upladmin-statuspage-config: changed activeSection to a property, removed the side navigation markup/CSS and simplified layout to be view-driven
- Reworked ts_web/interfaces to re-export core types from @uptime.link/interfaces and added UI-specific interfaces and form types
- Bumped dependency @uptime.link/interfaces to ^2.1.0 in package.json
## 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

View File

@@ -19,7 +19,7 @@
"@design.estate/dees-domtools": "^2.3.6",
"@design.estate/dees-element": "^2.1.3",
"@design.estate/dees-wcctools": "^3.2.0",
"@uptime.link/interfaces": "^2.0.21"
"@uptime.link/interfaces": "^2.1.0"
},
"devDependencies": {
"@git.zone/tsbuild": "^4.0.2",

19
pnpm-lock.yaml generated
View File

@@ -21,8 +21,8 @@ importers:
specifier: ^3.2.0
version: 3.2.0
'@uptime.link/interfaces':
specifier: ^2.0.21
version: 2.0.21
specifier: ^2.1.0
version: 2.1.0
devDependencies:
'@git.zone/tsbuild':
specifier: ^4.0.2
@@ -65,9 +65,6 @@ packages:
peerDependencies:
'@push.rocks/smartserve': '>=1.1.0'
'@apiglobal/typedrequest-interfaces@2.0.1':
resolution: {integrity: sha512-Oi7pNU4vKo5UvcCJmqkH43Us237Ws/Pp/WDYnwnonRnTmIMd+6QjNfN/gXcPnP6tbamk8r8Xzcz9mgnSDM2ysw==}
'@aws-crypto/crc32@5.2.0':
resolution: {integrity: sha512-nLbCWqQNgUiwwtFsen1AdzAtvuLRsQS8rYgMuxCrdKf9kOssamGLuPwyTY9wyYblNr9+1XM8v6zoDTPPSIeANg==}
engines: {node: '>=16.0.0'}
@@ -1512,8 +1509,8 @@ packages:
'@ungap/structured-clone@1.3.0':
resolution: {integrity: sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==}
'@uptime.link/interfaces@2.0.21':
resolution: {integrity: sha512-sy7WBzHOxU7Kt0BGofK0R3CS8D8QtbTXB00i75QKYXkEesdLd91SbFL80wTupKfjzeldE0ejUVSgvtlZEr8XlQ==}
'@uptime.link/interfaces@2.1.0':
resolution: {integrity: sha512-KlsAUp4Vvb4TeFNBDq4MZ9v4RaeLPr1GRlj4mkNmGLSfEiSasj1FWGLEj8iRdfeUuxMu0WRpAvWe2Ol0MRzoqg==}
'@webcontainer/api@1.2.0':
resolution: {integrity: sha512-tzoKBd4lLdhHy5GHFpUkl+ndoSba8JqmB7x0ZQFnWfjbcbQOvKQfxA8MEMUYhgqjWHnbrWdAfnBEHz5f5lYG5A==}
@@ -2976,8 +2973,6 @@ snapshots:
'@push.rocks/smartstring': 4.1.0
'@push.rocks/smarturl': 3.1.0
'@apiglobal/typedrequest-interfaces@2.0.1': {}
'@aws-crypto/crc32@5.2.0':
dependencies:
'@aws-crypto/util': 5.2.0
@@ -5254,10 +5249,10 @@ snapshots:
'@ungap/structured-clone@1.3.0': {}
'@uptime.link/interfaces@2.0.21':
'@uptime.link/interfaces@2.1.0':
dependencies:
'@apiglobal/typedrequest-interfaces': 2.0.1
'@tsclass/tsclass': 4.4.4
'@api.global/typedrequest-interfaces': 3.0.19
'@tsclass/tsclass': 9.3.0
'@webcontainer/api@1.2.0': {}

View File

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

View File

@@ -33,7 +33,7 @@ export class UpladminStatuspageConfig extends DeesElement {
@state()
accessor formData: IStatusPageConfig = {};
@state()
@property({ type: String })
accessor activeSection: string = 'branding';
@state()
@@ -49,67 +49,10 @@ export class UpladminStatuspageConfig extends DeesElement {
}
.config-container {
display: grid;
grid-template-columns: 220px 1fr;
gap: ${unsafeCSS(sharedStyles.spacing.lg)};
display: block;
min-height: 500px;
}
@media (max-width: 768px) {
.config-container {
grid-template-columns: 1fr;
}
}
.config-nav {
background: ${sharedStyles.colors.background.secondary};
border: 1px solid ${sharedStyles.colors.border.default};
border-radius: ${unsafeCSS(sharedStyles.borderRadius.lg)};
padding: ${unsafeCSS(sharedStyles.spacing.sm)};
height: fit-content;
}
.nav-item {
display: flex;
align-items: center;
gap: 12px;
width: 100%;
padding: 14px 16px;
font-size: 14px;
font-weight: 500;
font-family: ${unsafeCSS(sharedStyles.fonts.base)};
color: ${sharedStyles.colors.text.secondary};
background: transparent;
border: none;
border-radius: ${unsafeCSS(sharedStyles.borderRadius.base)};
cursor: pointer;
text-align: left;
transition: all ${unsafeCSS(sharedStyles.durations.fast)} ${unsafeCSS(sharedStyles.easings.default)};
}
.nav-item:hover {
background: ${sharedStyles.colors.background.muted};
color: ${sharedStyles.colors.text.primary};
}
.nav-item.active {
background: ${sharedStyles.colors.accent.primary};
color: white;
}
.nav-item.active dees-icon {
--icon-color: white;
}
.nav-item dees-icon {
--icon-color: ${sharedStyles.colors.text.muted};
transition: color ${unsafeCSS(sharedStyles.durations.fast)} ${unsafeCSS(sharedStyles.easings.default)};
}
.nav-item:hover dees-icon {
--icon-color: ${sharedStyles.colors.text.primary};
}
.config-content {
background: ${sharedStyles.colors.background.secondary};
border: 1px solid ${sharedStyles.colors.border.default};
@@ -343,18 +286,6 @@ export class UpladminStatuspageConfig extends DeesElement {
return html`
<div class="config-container">
<nav class="config-nav">
${sections.map(section => html`
<button
class="nav-item ${this.activeSection === section.id ? 'active' : ''}"
@click="${() => this.activeSection = section.id}"
>
<dees-icon .icon=${section.icon} .iconSize=${18}></dees-icon>
<span>${section.label}</span>
</button>
`)}
</nav>
<div class="config-content">
<div class="content-header">
<div>

View File

@@ -1,4 +1,5 @@
export * from './elements/index.js';
export * from './views/index.js';
export * from './pages/index.js';
export * from './interfaces/index.js';
export * from './services/index.js';

View File

@@ -1,19 +1,63 @@
// Re-export interfaces from the public catalog for consistency
// ============================================
// Re-export shared types from @uptime.link/interfaces
// ============================================
// 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';
import { data } from '@uptime.link/interfaces';
// Check configuration interface
// Re-export core types
export type TStatusType = data.TStatusType;
export type TCheckType = data.TCheckType;
export type TStatusMode = data.TStatusMode;
export type TIncidentSeverity = data.TIncidentSeverity;
export type TIncidentStatus = data.TIncidentStatus;
export type TCheckResultStatus = data.TCheckResultStatus;
export type TCheckLastResult = data.TCheckLastResult;
// Re-export check configuration interfaces
export type ICheckBase = data.ICheckBase;
export type IAssumptionCheckConfig = data.IAssumptionCheckConfig;
export type IFunctionCheckConfig = data.IFunctionCheckConfig;
export type IPwaCheckConfig = data.IPwaCheckConfig;
export type IPageRankCheckConfig = data.IPageRankCheckConfig;
export type TCheckConfig = data.TCheckConfig;
// Re-export check execution interfaces
export type IAssumptionCheck = data.IAssumptionCheck;
export type IFunctionCheck = data.IFunctionCheck;
export type IPwaCheck = data.IPwaCheck;
export type IPageRankCheck = data.IPageRankCheck;
export type ICheckCollection = data.ICheckCollection;
// Re-export incident interfaces
export type IIncident = data.IIncident;
export type IIncidentUpdateBase = data.IIncidentUpdate;
// Re-export status page config
export type IServiceGroup = data.IServiceGroup;
// ============================================
// Extended/UI-specific Interfaces
// ============================================
/**
* Flat check configuration for forms.
* Maps to TCheckConfig discriminated union when saving.
*/
export interface ICheckConfig {
domain: string;
// Assumption check fields
expectedTitle?: string;
expectedStatusCode?: string;
expectedDescription?: string;
assumedStatus?: TStatusType;
// Function check fields
functionDef?: string;
functionUrl?: string;
expectedStatusCodeNum?: number;
timeoutMs?: number;
// PWA check fields
targetUrl?: string;
lighthouseThreshold?: number;
// PageRank check fields
searchTerm?: string;
checkBing?: boolean;
@@ -22,6 +66,10 @@ export interface ICheckConfig {
googleMinRank?: number;
}
/**
* Service status for display and management.
* Extended with UI-specific fields.
*/
export interface IServiceStatus {
id: string;
name: string;
@@ -45,6 +93,9 @@ export interface IServiceStatus {
intervalMs?: number;
}
/**
* Status history point.
*/
export interface IStatusHistoryPoint {
timestamp: number;
status: TStatusType;
@@ -52,28 +103,9 @@ export interface IStatusHistoryPoint {
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;
}
/**
* Overall status with service counts for dashboard.
*/
export interface IOverallStatus {
status: TStatusType;
message: string;
@@ -82,6 +114,9 @@ export interface IOverallStatus {
totalServices: number;
}
/**
* Status page configuration.
*/
export interface IStatusPageConfig {
apiEndpoint?: string;
refreshInterval?: number;
@@ -100,7 +135,41 @@ export interface IStatusPageConfig {
legalUrl?: string;
}
// Admin-specific interfaces
/**
* Incident update entry (UI version with timestamp).
*/
export interface IIncidentUpdate {
id: string;
timestamp: number;
status: TIncidentStatus;
message: string;
author?: string;
}
/**
* Incident details for display.
*/
export interface IIncidentDetails {
id: string;
title: string;
status: TIncidentStatus;
severity: TIncidentSeverity;
affectedServices: string[];
startTime: number;
endTime?: number;
updates: IIncidentUpdate[];
impact: string;
rootCause?: string;
resolution?: string;
}
// ============================================
// Form Interfaces (UI-specific)
// ============================================
/**
* Monitor form data for creating/editing monitors.
*/
export interface IMonitorFormData {
id?: string;
name: string;
@@ -118,19 +187,25 @@ export interface IMonitorFormData {
intervalMs: number;
}
/**
* Incident form data for creating/editing incidents.
*/
export interface IIncidentFormData {
id?: string;
title: string;
severity: 'critical' | 'major' | 'minor' | 'maintenance';
status: 'investigating' | 'identified' | 'monitoring' | 'resolved' | 'postmortem';
severity: TIncidentSeverity;
status: TIncidentStatus;
affectedServices: string[];
impact: string;
rootCause?: string;
resolution?: string;
}
/**
* Incident update form data.
*/
export interface IIncidentUpdateFormData {
status: 'investigating' | 'identified' | 'monitoring' | 'resolved' | 'postmortem';
status: TIncidentStatus;
message: string;
author?: string;
}

View File

@@ -1,83 +0,0 @@
import { html, cssManager } from "@design.estate/dees-element";
import type { IStatusPageConfig } from '../interfaces/index.js';
import '../elements/index.js';
export const adminpageConfig = () => html`
<style>
.demo-page-wrapper {
min-height: 100vh;
background: ${cssManager.bdTheme('#fafafa', '#0a0a0a')};
padding: 24px;
}
</style>
<div class="demo-page-wrapper">
<dees-demowrapper
.runAfterRender=${async (wrapperElement: any) => {
const config = wrapperElement.querySelector('upladmin-statuspage-config') as any;
const configData: IStatusPageConfig = {
companyName: 'CloudFlow Inc.',
companyLogo: '',
supportEmail: 'support@cloudflow.io',
statusPageUrl: 'https://status.cloudflow.io',
legalUrl: 'https://cloudflow.io/terms',
apiEndpoint: 'https://api.cloudflow.io/status',
refreshInterval: 60,
showHistoricalDays: 90,
theme: 'auto',
language: 'en',
timeZone: 'UTC',
dateFormat: 'relative',
enableWebSocket: true,
enableNotifications: false,
whitelabel: false,
};
config.config = configData;
}}
>
<upladmin-statuspage-config></upladmin-statuspage-config>
</dees-demowrapper>
</div>
`;
export const adminpageConfigWhitelabel = () => html`
<style>
.demo-page-wrapper {
min-height: 100vh;
background: ${cssManager.bdTheme('#fafafa', '#0a0a0a')};
padding: 24px;
}
</style>
<div class="demo-page-wrapper">
<dees-demowrapper
.runAfterRender=${async (wrapperElement: any) => {
const config = wrapperElement.querySelector('upladmin-statuspage-config') as any;
const configData: IStatusPageConfig = {
companyName: 'Enterprise Corp',
companyLogo: 'https://via.placeholder.com/200x60/1a1a2e/ffffff?text=ENTERPRISE',
supportEmail: 'support@enterprise.com',
statusPageUrl: 'https://status.enterprise.com',
legalUrl: 'https://enterprise.com/legal',
apiEndpoint: 'https://api.enterprise.com/v2/status',
refreshInterval: 30,
showHistoricalDays: 180,
theme: 'dark',
language: 'en',
timeZone: 'America/New_York',
dateFormat: 'absolute',
enableWebSocket: true,
enableNotifications: true,
whitelabel: true,
};
config.config = configData;
}}
>
<upladmin-statuspage-config></upladmin-statuspage-config>
</dees-demowrapper>
</div>
`;

View File

@@ -1,156 +0,0 @@
import { html, cssManager } from "@design.estate/dees-element";
import type { IServiceStatus, IIncidentDetails } from '../interfaces/index.js';
import '../elements/index.js';
export const adminpageDashboard = () => html`
<style>
.demo-page-wrapper {
min-height: 100vh;
background: ${cssManager.bdTheme('#fafafa', '#0a0a0a')};
padding: 24px;
}
</style>
<div class="demo-page-wrapper">
<dees-demowrapper
.runAfterRender=${async (wrapperElement: any) => {
const dashboard = wrapperElement.querySelector('upladmin-dashboard') as any;
// Demo monitors
const monitors: IServiceStatus[] = [
{
id: 'api-server',
name: 'api-server',
displayName: 'API Server',
description: 'Main REST API endpoint',
currentStatus: 'operational',
lastChecked: Date.now(),
uptime30d: 99.98,
uptime90d: 99.95,
responseTime: 45,
category: 'Core Services',
},
{
id: 'web-app',
name: 'web-app',
displayName: 'Web Application',
description: 'Customer-facing web application',
currentStatus: 'operational',
lastChecked: Date.now(),
uptime30d: 99.99,
uptime90d: 99.97,
responseTime: 120,
category: 'Core Services',
},
{
id: 'database-primary',
name: 'database-primary',
displayName: 'Primary Database',
description: 'PostgreSQL primary node',
currentStatus: 'operational',
lastChecked: Date.now(),
uptime30d: 99.999,
uptime90d: 99.998,
responseTime: 5,
category: 'Infrastructure',
},
{
id: 'cdn',
name: 'cdn',
displayName: 'Content Delivery Network',
description: 'Global CDN for static assets',
currentStatus: 'degraded',
lastChecked: Date.now(),
uptime30d: 99.5,
uptime90d: 99.8,
responseTime: 200,
category: 'Infrastructure',
},
{
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: 500,
category: 'External Services',
},
{
id: 'payment-gateway',
name: 'payment-gateway',
displayName: 'Payment Gateway',
description: 'Payment processing integration',
currentStatus: 'maintenance',
lastChecked: Date.now(),
uptime30d: 99.95,
uptime90d: 99.9,
responseTime: 350,
category: 'External Services',
},
];
// Demo incidents
const incidents: IIncidentDetails[] = [
{
id: 'inc-001',
title: 'CDN Performance Degradation',
status: 'monitoring',
severity: 'minor',
affectedServices: ['cdn'],
startTime: Date.now() - 2 * 60 * 60 * 1000,
impact: 'Some users may experience slower loading times for images and static assets.',
updates: [
{
id: 'upd-001',
timestamp: Date.now() - 2 * 60 * 60 * 1000,
status: 'investigating',
message: 'We are investigating reports of slow asset loading.',
author: 'Platform Team',
},
{
id: 'upd-002',
timestamp: Date.now() - 1 * 60 * 60 * 1000,
status: 'identified',
message: 'We have identified the issue as a problem with one of our CDN edge nodes.',
author: 'Platform Team',
},
{
id: 'upd-003',
timestamp: Date.now() - 30 * 60 * 1000,
status: 'monitoring',
message: 'Traffic has been rerouted to healthy nodes. Monitoring for stability.',
author: 'Platform Team',
},
],
},
{
id: 'inc-002',
title: 'Payment Gateway Scheduled Maintenance',
status: 'investigating',
severity: 'maintenance',
affectedServices: ['payment-gateway'],
startTime: Date.now() - 30 * 60 * 1000,
impact: 'Payment processing is temporarily unavailable during the maintenance window.',
updates: [
{
id: 'upd-004',
timestamp: Date.now() - 30 * 60 * 1000,
status: 'investigating',
message: 'Scheduled maintenance has begun. Expected duration: 2 hours.',
author: 'DevOps Team',
},
],
},
];
dashboard.monitors = monitors;
dashboard.incidents = incidents;
}}
>
<upladmin-dashboard></upladmin-dashboard>
</dees-demowrapper>
</div>
`;

View File

@@ -1,166 +0,0 @@
import { html, cssManager } from "@design.estate/dees-element";
import type { IServiceStatus, IIncidentDetails } from '../interfaces/index.js';
import '../elements/index.js';
export const adminpageIncidents = () => html`
<style>
.demo-page-wrapper {
min-height: 100vh;
background: ${cssManager.bdTheme('#fafafa', '#0a0a0a')};
padding: 24px;
}
</style>
<div class="demo-page-wrapper">
<dees-demowrapper
.runAfterRender=${async (wrapperElement: any) => {
const incidentList = wrapperElement.querySelector('upladmin-incident-list') as any;
const incidents: IIncidentDetails[] = [
{
id: 'inc-001',
title: 'CDN Performance Degradation',
status: 'monitoring',
severity: 'minor',
affectedServices: ['cdn'],
startTime: Date.now() - 2 * 60 * 60 * 1000,
impact: 'Some users may experience slower loading times for images and static assets.',
updates: [
{ id: 'upd-001', timestamp: Date.now() - 2 * 60 * 60 * 1000, status: 'investigating', message: 'We are investigating reports of slow asset loading.', author: 'Platform Team' },
{ id: 'upd-002', timestamp: Date.now() - 1 * 60 * 60 * 1000, status: 'identified', message: 'We have identified the issue as a problem with one of our CDN edge nodes.', author: 'Platform Team' },
{ id: 'upd-003', timestamp: Date.now() - 30 * 60 * 1000, status: 'monitoring', message: 'Traffic has been rerouted to healthy nodes. Monitoring for stability.', author: 'Platform Team' },
],
},
{
id: 'inc-002',
title: 'Payment Gateway Scheduled Maintenance',
status: 'investigating',
severity: 'maintenance',
affectedServices: ['payment-gateway'],
startTime: Date.now() - 30 * 60 * 1000,
impact: 'Payment processing is temporarily unavailable during the maintenance window.',
updates: [
{ id: 'upd-004', timestamp: Date.now() - 30 * 60 * 1000, status: 'investigating', message: 'Scheduled maintenance has begun. Expected duration: 2 hours.', author: 'DevOps Team' },
],
},
{
id: 'inc-003',
title: 'Search Engine Partial Outage',
status: 'identified',
severity: 'major',
affectedServices: ['search-engine', 'api-server'],
startTime: Date.now() - 45 * 60 * 1000,
impact: 'Search functionality is degraded. Some queries may timeout or return incomplete results.',
updates: [
{ id: 'upd-005', timestamp: Date.now() - 45 * 60 * 1000, status: 'investigating', message: 'We are aware of issues with search functionality.', author: 'Engineering Team' },
{ id: 'upd-006', timestamp: Date.now() - 20 * 60 * 1000, status: 'identified', message: 'Root cause identified: disk space exhaustion on search cluster nodes.', author: 'Engineering Team' },
],
},
{
id: 'inc-004',
title: 'API Server Outage',
status: 'resolved',
severity: 'critical',
affectedServices: ['api-server', 'web-app'],
startTime: Date.now() - 24 * 60 * 60 * 1000,
endTime: Date.now() - 23 * 60 * 60 * 1000,
impact: 'Complete service unavailability for all API-dependent services.',
rootCause: 'Database connection pool exhaustion due to a query performance regression.',
resolution: 'Rolled back recent deployment and optimized database queries.',
updates: [
{ id: 'upd-007', timestamp: Date.now() - 24 * 60 * 60 * 1000, status: 'investigating', message: 'We are aware of service unavailability and actively investigating.', author: 'Platform Team' },
{ id: 'upd-008', timestamp: Date.now() - 23.5 * 60 * 60 * 1000, status: 'identified', message: 'Root cause identified as database connection pool exhaustion.', author: 'Platform Team' },
{ id: 'upd-009', timestamp: Date.now() - 23 * 60 * 60 * 1000, status: 'resolved', message: 'Service has been restored. All systems operational.', author: 'Platform Team' },
],
},
{
id: 'inc-005',
title: 'Email Delivery Delays',
status: 'resolved',
severity: 'minor',
affectedServices: ['email-service'],
startTime: Date.now() - 48 * 60 * 60 * 1000,
endTime: Date.now() - 46 * 60 * 60 * 1000,
impact: 'Email notifications may be delayed by up to 30 minutes.',
rootCause: 'Third-party email provider experiencing capacity issues.',
resolution: 'Provider resolved their capacity issues.',
updates: [
{ id: 'upd-010', timestamp: Date.now() - 48 * 60 * 60 * 1000, status: 'investigating', message: 'Investigating reports of delayed email delivery.', author: 'Support Team' },
{ id: 'upd-011', timestamp: Date.now() - 46 * 60 * 60 * 1000, status: 'resolved', message: 'Email delivery has returned to normal.', author: 'Support Team' },
],
},
];
incidentList.incidents = incidents;
}}
>
<upladmin-incident-list></upladmin-incident-list>
</dees-demowrapper>
</div>
`;
export const adminpageIncidentForm = () => html`
<style>
.demo-page-wrapper {
min-height: 100vh;
background: ${cssManager.bdTheme('#fafafa', '#0a0a0a')};
padding: 24px;
}
</style>
<div class="demo-page-wrapper">
<dees-demowrapper
.runAfterRender=${async (wrapperElement: any) => {
const incidentForm = wrapperElement.querySelector('upladmin-incident-form') as any;
const services: IServiceStatus[] = [
{ id: 'api-server', name: 'api-server', displayName: 'API Server', currentStatus: 'operational', lastChecked: Date.now(), uptime30d: 99.98, uptime90d: 99.95, responseTime: 45 },
{ id: 'web-app', name: 'web-app', displayName: 'Web Application', currentStatus: 'operational', lastChecked: Date.now(), uptime30d: 99.99, uptime90d: 99.97, responseTime: 120 },
{ id: 'database-primary', name: 'database-primary', displayName: 'Primary Database', currentStatus: 'operational', lastChecked: Date.now(), uptime30d: 99.999, uptime90d: 99.998, responseTime: 5 },
{ id: 'cdn', name: 'cdn', displayName: 'Content Delivery Network', currentStatus: 'degraded', lastChecked: Date.now(), uptime30d: 99.5, uptime90d: 99.8, responseTime: 200 },
{ id: 'email-service', name: 'email-service', displayName: 'Email Service', currentStatus: 'operational', lastChecked: Date.now(), uptime30d: 99.9, uptime90d: 99.85, responseTime: 500 },
{ id: 'payment-gateway', name: 'payment-gateway', displayName: 'Payment Gateway', currentStatus: 'maintenance', lastChecked: Date.now(), uptime30d: 99.95, uptime90d: 99.9, responseTime: 350 },
];
incidentForm.availableServices = services;
}}
>
<upladmin-incident-form></upladmin-incident-form>
</dees-demowrapper>
</div>
`;
export const adminpageIncidentUpdate = () => html`
<style>
.demo-page-wrapper {
min-height: 100vh;
background: ${cssManager.bdTheme('#fafafa', '#0a0a0a')};
padding: 24px;
}
</style>
<div class="demo-page-wrapper">
<dees-demowrapper
.runAfterRender=${async (wrapperElement: any) => {
const incidentUpdate = wrapperElement.querySelector('upladmin-incident-update') as any;
incidentUpdate.incident = {
id: 'inc-001',
title: 'CDN Performance Degradation',
status: 'monitoring',
severity: 'minor',
affectedServices: ['cdn'],
startTime: Date.now() - 2 * 60 * 60 * 1000,
impact: 'Some users may experience slower loading times for images and static assets.',
updates: [
{ id: 'upd-001', timestamp: Date.now() - 2 * 60 * 60 * 1000, status: 'investigating', message: 'We are investigating reports of slow asset loading.', author: 'Platform Team' },
{ id: 'upd-002', timestamp: Date.now() - 1 * 60 * 60 * 1000, status: 'identified', message: 'We have identified the issue as a problem with one of our CDN edge nodes.', author: 'Platform Team' },
{ id: 'upd-003', timestamp: Date.now() - 30 * 60 * 1000, status: 'monitoring', message: 'Traffic has been rerouted to healthy nodes. Monitoring for stability.', author: 'Platform Team' },
],
};
}}
>
<upladmin-incident-update></upladmin-incident-update>
</dees-demowrapper>
</div>
`;

View File

@@ -1,213 +0,0 @@
import { html, cssManager } from "@design.estate/dees-element";
import type { IServiceStatus } from '../interfaces/index.js';
import '../elements/index.js';
export const adminpageMonitors = () => html`
<style>
.demo-page-wrapper {
min-height: 100vh;
background: ${cssManager.bdTheme('#fafafa', '#0a0a0a')};
padding: 24px;
}
</style>
<div class="demo-page-wrapper">
<dees-demowrapper
.runAfterRender=${async (wrapperElement: any) => {
const monitorList = wrapperElement.querySelector('upladmin-monitor-list') as any;
const monitors: IServiceStatus[] = [
{
id: 'api-server',
name: 'api-server',
displayName: 'API Server',
description: 'Main REST API endpoint',
currentStatus: 'operational',
lastChecked: Date.now(),
uptime30d: 99.98,
uptime90d: 99.95,
responseTime: 45,
category: 'Core Services',
},
{
id: 'web-app',
name: 'web-app',
displayName: 'Web Application',
description: 'Customer-facing web application',
currentStatus: 'operational',
lastChecked: Date.now(),
uptime30d: 99.99,
uptime90d: 99.97,
responseTime: 120,
category: 'Core Services',
},
{
id: 'database-primary',
name: 'database-primary',
displayName: 'Primary Database',
description: 'PostgreSQL primary node',
currentStatus: 'operational',
lastChecked: Date.now(),
uptime30d: 99.999,
uptime90d: 99.998,
responseTime: 5,
category: 'Infrastructure',
},
{
id: 'database-replica',
name: 'database-replica',
displayName: 'Database Replica',
description: 'PostgreSQL read replica',
currentStatus: 'operational',
lastChecked: Date.now(),
uptime30d: 99.99,
uptime90d: 99.95,
responseTime: 8,
category: 'Infrastructure',
},
{
id: 'cdn',
name: 'cdn',
displayName: 'Content Delivery Network',
description: 'Global CDN for static assets',
currentStatus: 'degraded',
lastChecked: Date.now(),
uptime30d: 99.5,
uptime90d: 99.8,
responseTime: 200,
category: 'Infrastructure',
},
{
id: 'redis-cache',
name: 'redis-cache',
displayName: 'Redis Cache',
description: 'In-memory caching layer',
currentStatus: 'operational',
lastChecked: Date.now(),
uptime30d: 99.99,
uptime90d: 99.98,
responseTime: 2,
category: 'Infrastructure',
},
{
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: 500,
category: 'External Services',
},
{
id: 'payment-gateway',
name: 'payment-gateway',
displayName: 'Payment Gateway',
description: 'Payment processing integration',
currentStatus: 'maintenance',
lastChecked: Date.now(),
uptime30d: 99.95,
uptime90d: 99.9,
responseTime: 350,
category: 'External Services',
},
{
id: 'sms-service',
name: 'sms-service',
displayName: 'SMS Service',
description: 'SMS notifications and 2FA',
currentStatus: 'operational',
lastChecked: Date.now(),
uptime30d: 99.8,
uptime90d: 99.75,
responseTime: 800,
category: 'External Services',
},
{
id: 'search-engine',
name: 'search-engine',
displayName: 'Search Engine',
description: 'Elasticsearch cluster',
currentStatus: 'partial_outage',
lastChecked: Date.now(),
uptime30d: 98.5,
uptime90d: 99.2,
responseTime: 150,
category: 'Core Services',
},
];
monitorList.monitors = monitors;
}}
>
<upladmin-monitor-list></upladmin-monitor-list>
</dees-demowrapper>
</div>
`;
export const adminpageMonitorForm = () => html`
<style>
.demo-page-wrapper {
min-height: 100vh;
background: ${cssManager.bdTheme('#fafafa', '#0a0a0a')};
padding: 24px;
}
</style>
<div class="demo-page-wrapper">
<dees-demowrapper
.runAfterRender=${async (wrapperElement: any) => {
const monitorForm = wrapperElement.querySelector('upladmin-monitor-form') as any;
const availableMonitors: IServiceStatus[] = [
{ id: 'api-server', name: 'api-server', displayName: 'API Server', currentStatus: 'operational', lastChecked: Date.now(), uptime30d: 99.98, uptime90d: 99.95, responseTime: 45 },
{ id: 'database-primary', name: 'database-primary', displayName: 'Primary Database', currentStatus: 'operational', lastChecked: Date.now(), uptime30d: 99.999, uptime90d: 99.998, responseTime: 5 },
];
monitorForm.availableMonitors = availableMonitors;
monitorForm.categories = ['Core Services', 'Infrastructure', 'External Services', 'Web Services'];
}}
>
<upladmin-monitor-form></upladmin-monitor-form>
</dees-demowrapper>
</div>
`;
export const adminpageMonitorFormEdit = () => html`
<style>
.demo-page-wrapper {
min-height: 100vh;
background: ${cssManager.bdTheme('#fafafa', '#0a0a0a')};
padding: 24px;
}
</style>
<div class="demo-page-wrapper">
<dees-demowrapper
.runAfterRender=${async (wrapperElement: any) => {
const monitorForm = wrapperElement.querySelector('upladmin-monitor-form') as any;
const availableMonitors: IServiceStatus[] = [
{ id: 'api-server', name: 'api-server', displayName: 'API Server', currentStatus: 'operational', lastChecked: Date.now(), uptime30d: 99.98, uptime90d: 99.95, responseTime: 45 },
{ id: 'database-primary', name: 'database-primary', displayName: 'Primary Database', currentStatus: 'operational', lastChecked: Date.now(), uptime30d: 99.999, uptime90d: 99.998, responseTime: 5 },
];
monitorForm.availableMonitors = availableMonitors;
monitorForm.categories = ['Core Services', 'Infrastructure', 'External Services', 'Web Services'];
monitorForm.monitor = {
id: 'cdn',
name: 'cdn',
displayName: 'Content Delivery Network',
description: 'Global CDN for static assets and media files',
category: 'Infrastructure',
dependencies: ['api-server'],
currentStatus: 'degraded',
};
}}
>
<upladmin-monitor-form></upladmin-monitor-form>
</dees-demowrapper>
</div>
`;

View File

@@ -1,5 +1,3 @@
export * from './adminpage-dashboard.js';
export * from './adminpage-monitors.js';
export * from './adminpage-incidents.js';
export * from './adminpage-config.js';
// Consolidated app demo - showcases all views through dees-appui
export { demoFunc as adminpageApp } from './upladmin-app/upladmin-app.demo.js';
export { UpladminApp } from './upladmin-app/upladmin-app.js';

View File

@@ -11,14 +11,11 @@ import type { DeesAppuiBase } from '@design.estate/dees-catalog';
import { adminState } from '../../services/admin-state.js';
import { demoFunc } from './upladmin-app.demo.js';
// Import components directly
import '../../elements/upladmin-dashboard/upladmin-dashboard.js';
import '../../elements/upladmin-monitor-list/upladmin-monitor-list.js';
import '../../elements/upladmin-monitor-form/upladmin-monitor-form.js';
import '../../elements/upladmin-incident-list/upladmin-incident-list.js';
import '../../elements/upladmin-incident-form/upladmin-incident-form.js';
import '../../elements/upladmin-incident-update/upladmin-incident-update.js';
import '../../elements/upladmin-statuspage-config/upladmin-statuspage-config.js';
// Import view components
import '../../views/upladmin-dashboard-view/upladmin-dashboard-view.js';
import '../../views/upladmin-monitors-view/upladmin-monitors-view.js';
import '../../views/upladmin-incidents-view/upladmin-incidents-view.js';
import '../../views/upladmin-config-view/upladmin-config-view.js';
declare global {
interface HTMLElementTagNameMap {
@@ -176,14 +173,14 @@ export class UpladminApp extends DeesElement {
id: 'dashboard',
name: 'Dashboard',
iconName: 'lucide:layoutDashboard',
content: 'upladmin-dashboard',
content: 'upladmin-dashboard-view',
route: 'dashboard',
},
{
id: 'monitors',
name: 'Monitors',
iconName: 'lucide:activity',
content: 'upladmin-monitor-list',
content: 'upladmin-monitors-view',
route: 'monitors',
badge: adminState.monitors.length,
},
@@ -191,7 +188,7 @@ export class UpladminApp extends DeesElement {
id: 'monitor-form',
name: 'Monitor',
iconName: 'lucide:activity',
content: 'upladmin-monitor-form',
content: 'upladmin-monitors-view',
route: 'monitors/:id',
cache: false,
},
@@ -199,7 +196,7 @@ export class UpladminApp extends DeesElement {
id: 'incidents',
name: 'Incidents',
iconName: 'lucide:alertCircle',
content: 'upladmin-incident-list',
content: 'upladmin-incidents-view',
route: 'incidents',
badge: adminState.getActiveIncidents().length,
badgeVariant: adminState.getActiveIncidents().length > 0 ? 'warning' : 'default',
@@ -208,7 +205,7 @@ export class UpladminApp extends DeesElement {
id: 'incident-form',
name: 'Incident',
iconName: 'lucide:alertCircle',
content: 'upladmin-incident-form',
content: 'upladmin-incidents-view',
route: 'incidents/:id',
cache: false,
},
@@ -216,7 +213,7 @@ export class UpladminApp extends DeesElement {
id: 'incident-update',
name: 'Post Update',
iconName: 'lucide:messageSquarePlus',
content: 'upladmin-incident-update',
content: 'upladmin-incidents-view',
route: 'incidents/:id/update',
cache: false,
},
@@ -224,9 +221,17 @@ export class UpladminApp extends DeesElement {
id: 'config',
name: 'Settings',
iconName: 'lucide:settings',
content: 'upladmin-statuspage-config',
content: 'upladmin-config-view',
route: 'config',
},
{
id: 'config-section',
name: 'Settings',
iconName: 'lucide:settings',
content: 'upladmin-config-view',
route: 'config/:section',
cache: false,
},
],
mainMenu: {

4
ts_web/views/index.ts Normal file
View File

@@ -0,0 +1,4 @@
export * from './upladmin-dashboard-view/upladmin-dashboard-view.js';
export * from './upladmin-monitors-view/upladmin-monitors-view.js';
export * from './upladmin-incidents-view/upladmin-incidents-view.js';
export * from './upladmin-config-view/upladmin-config-view.js';

View File

@@ -0,0 +1,124 @@
import {
DeesElement,
customElement,
html,
state,
css,
cssManager,
} from '@design.estate/dees-element';
import type { DeesAppuiBase } from '@design.estate/dees-catalog';
// View lifecycle interfaces (defined locally as they're not exported from dees-catalog)
interface IViewActivationContext {
appui: DeesAppuiBase;
viewId: string;
params?: Record<string, string>;
}
interface IViewLifecycle {
onActivate?: (context: IViewActivationContext) => void | Promise<void>;
onDeactivate?: () => void | Promise<void>;
}
import { adminState } from '../../services/admin-state.js';
import '../../elements/upladmin-statuspage-config/upladmin-statuspage-config.js';
type TConfigSection = 'branding' | 'urls' | 'behavior' | 'advanced';
@customElement('upladmin-config-view')
export class UpladminConfigView extends DeesElement implements IViewLifecycle {
@state()
accessor activeSection: TConfigSection = 'branding';
@state()
accessor loading: boolean = false;
private appuiRef: DeesAppuiBase | null = null;
public static styles = [
cssManager.defaultStyles,
css`
:host {
display: block;
height: 100%;
}
`,
];
async onActivate(context: IViewActivationContext): Promise<void> {
this.appuiRef = context.appui;
// Check route params for section
if (context.params?.section) {
const section = context.params.section as TConfigSection;
if (['branding', 'urls', 'behavior', 'advanced'].includes(section)) {
this.activeSection = section;
}
}
// Set secondary menu for configuration sections
this.updateSecondaryMenu();
// No content tabs for config
context.appui.setContentTabs([]);
}
private updateSecondaryMenu(): void {
if (!this.appuiRef) return;
this.appuiRef.setSecondaryMenu({
heading: 'Configuration',
groups: [
{
name: 'Settings',
iconName: 'lucide:settings',
items: [
{
key: 'branding',
iconName: 'lucide:palette',
action: () => this.setSection('branding'),
},
{
key: 'urls',
iconName: 'lucide:link',
action: () => this.setSection('urls'),
},
{
key: 'behavior',
iconName: 'lucide:sliders',
action: () => this.setSection('behavior'),
},
{
key: 'advanced',
iconName: 'lucide:wrench',
action: () => this.setSection('advanced'),
},
],
},
],
});
// Select current section
this.appuiRef.setSecondaryMenuSelection(this.activeSection);
}
private setSection(section: TConfigSection): void {
this.activeSection = section;
this.appuiRef?.setSecondaryMenuSelection(section);
}
private handleConfigSave = (e: CustomEvent): void => {
console.log('Config saved:', e.detail);
// In a real implementation, this would save to the backend
};
render() {
return html`
<upladmin-statuspage-config
.config=${adminState.config || {}}
.activeSection=${this.activeSection}
.loading=${this.loading}
@configSave=${this.handleConfigSave}
></upladmin-statuspage-config>
`;
}
}

View File

@@ -0,0 +1,60 @@
import {
DeesElement,
customElement,
html,
state,
css,
cssManager,
} from '@design.estate/dees-element';
import type { DeesAppuiBase } from '@design.estate/dees-catalog';
// View lifecycle interfaces (defined locally as they're not exported from dees-catalog)
interface IViewActivationContext {
appui: DeesAppuiBase;
viewId: string;
params?: Record<string, string>;
}
interface IViewLifecycle {
onActivate?: (context: IViewActivationContext) => void | Promise<void>;
onDeactivate?: () => void | Promise<void>;
}
import { adminState } from '../../services/admin-state.js';
import '../../elements/upladmin-dashboard/upladmin-dashboard.js';
@customElement('upladmin-dashboard-view')
export class UpladminDashboardView extends DeesElement implements IViewLifecycle {
@state()
accessor loading: boolean = true;
public static styles = [
cssManager.defaultStyles,
css`
:host {
display: block;
height: 100%;
}
`,
];
async onActivate(context: IViewActivationContext): Promise<void> {
// Dashboard has no secondary menu - clear any existing
context.appui.clearSecondaryMenu();
// No content tabs for dashboard
context.appui.setContentTabs([]);
// Load data
this.loading = false;
}
render() {
return html`
<upladmin-dashboard
.monitors=${adminState.monitors}
.incidents=${adminState.incidents}
.loading=${this.loading}
></upladmin-dashboard>
`;
}
}

View File

@@ -0,0 +1,292 @@
import {
DeesElement,
customElement,
html,
state,
css,
cssManager,
} from '@design.estate/dees-element';
import type { DeesAppuiBase } from '@design.estate/dees-catalog';
// View lifecycle interfaces (defined locally as they're not exported from dees-catalog)
interface IViewActivationContext {
appui: DeesAppuiBase;
viewId: string;
params?: Record<string, string>;
}
interface IViewLifecycle {
onActivate?: (context: IViewActivationContext) => void | Promise<void>;
onDeactivate?: () => void | Promise<void>;
}
import { adminState } from '../../services/admin-state.js';
import type { IIncidentDetails, TIncidentSeverity, TIncidentStatus } from '../../interfaces/index.js';
import '../../elements/upladmin-incident-list/upladmin-incident-list.js';
import '../../elements/upladmin-incident-form/upladmin-incident-form.js';
import '../../elements/upladmin-incident-update/upladmin-incident-update.js';
type TViewMode = 'list' | 'form' | 'update';
type TTimeFilter = 'current' | 'past' | 'all';
@customElement('upladmin-incidents-view')
export class UpladminIncidentsView extends DeesElement implements IViewLifecycle {
@state()
accessor currentMode: TViewMode = 'list';
@state()
accessor selectedIncidentId: string | null = null;
@state()
accessor timeFilter: TTimeFilter = 'current';
@state()
accessor severityFilter: TIncidentSeverity | 'all' = 'all';
@state()
accessor loading: boolean = false;
private appuiRef: DeesAppuiBase | null = null;
public static styles = [
cssManager.defaultStyles,
css`
:host {
display: block;
height: 100%;
}
`,
];
async onActivate(context: IViewActivationContext): Promise<void> {
this.appuiRef = context.appui;
// Check route params and view ID
if (context.params?.id) {
if (context.viewId === 'incident-update') {
this.currentMode = 'update';
this.selectedIncidentId = context.params.id;
} else {
this.currentMode = 'form';
this.selectedIncidentId = context.params.id === 'create' ? null : context.params.id;
}
} else {
this.currentMode = 'list';
this.selectedIncidentId = null;
}
// Set secondary menu
this.updateSecondaryMenu();
// No content tabs - incident-list has internal tabs
context.appui.setContentTabs([]);
}
private updateSecondaryMenu(): void {
if (!this.appuiRef) return;
const activeCount = adminState.getActiveIncidents().length;
const pastCount = adminState.incidents.filter((i) => i.status === 'resolved' || i.status === 'postmortem').length;
this.appuiRef.setSecondaryMenu({
heading: 'Incidents',
groups: [
{
name: 'Filter',
iconName: 'lucide:filter',
items: [
{
key: 'current',
iconName: 'lucide:alertCircle',
action: () => this.setTimeFilter('current'),
badge: activeCount,
badgeVariant: activeCount > 0 ? 'error' : 'default',
},
{
key: 'past',
iconName: 'lucide:history',
action: () => this.setTimeFilter('past'),
badge: pastCount,
},
{
key: 'all',
iconName: 'lucide:list',
action: () => this.setTimeFilter('all'),
badge: adminState.incidents.length,
},
],
},
{
name: 'Severity',
iconName: 'lucide:alertTriangle',
collapsed: true,
items: [
{
key: 'critical',
iconName: 'lucide:xCircle',
action: () => this.setSeverityFilter('critical'),
},
{
key: 'major',
iconName: 'lucide:alertOctagon',
action: () => this.setSeverityFilter('major'),
},
{
key: 'minor',
iconName: 'lucide:alertTriangle',
action: () => this.setSeverityFilter('minor'),
},
{
key: 'maintenance',
iconName: 'lucide:wrench',
action: () => this.setSeverityFilter('maintenance'),
},
],
},
{
name: 'Actions',
iconName: 'lucide:zap',
items: [
{
key: 'create',
iconName: 'lucide:plus',
action: () => this.showForm(null),
},
],
},
],
});
// Select current filter
this.appuiRef.setSecondaryMenuSelection(this.timeFilter);
}
private setTimeFilter(filter: TTimeFilter): void {
this.timeFilter = filter;
this.severityFilter = 'all';
this.appuiRef?.setSecondaryMenuSelection(filter);
}
private setSeverityFilter(severity: TIncidentSeverity): void {
this.severityFilter = severity;
this.appuiRef?.setSecondaryMenuSelection(severity);
}
private showForm(incidentId: string | null): void {
this.currentMode = 'form';
this.selectedIncidentId = incidentId;
}
private showUpdate(incidentId: string): void {
this.currentMode = 'update';
this.selectedIncidentId = incidentId;
}
private showList(): void {
this.currentMode = 'list';
this.selectedIncidentId = null;
}
private get filteredIncidents(): IIncidentDetails[] {
let incidents = adminState.incidents;
// Apply time filter
if (this.timeFilter === 'current') {
incidents = incidents.filter(
(i) => i.status !== 'resolved' && i.status !== 'postmortem'
);
} else if (this.timeFilter === 'past') {
incidents = incidents.filter(
(i) => i.status === 'resolved' || i.status === 'postmortem'
);
}
// Apply severity filter
if (this.severityFilter !== 'all') {
incidents = incidents.filter((i) => i.severity === this.severityFilter);
}
return incidents;
}
private handleIncidentSave = (e: CustomEvent): void => {
console.log('Incident saved:', e.detail);
this.showList();
};
private handleUpdateSave = (e: CustomEvent): void => {
console.log('Update saved:', e.detail);
this.showList();
};
private handleCancel = (): void => {
this.showList();
};
private handleIncidentEdit = (e: CustomEvent): void => {
const incident = e.detail?.incident as IIncidentDetails;
if (incident) {
this.showForm(incident.id);
}
};
private handleIncidentAddUpdate = (e: CustomEvent): void => {
const incident = e.detail?.incident as IIncidentDetails;
if (incident) {
this.showUpdate(incident.id);
}
};
render() {
if (this.currentMode === 'update') {
const incident = this.selectedIncidentId
? adminState.incidents.find((i) => i.id === this.selectedIncidentId)
: null;
return html`
<upladmin-incident-update
.incident=${incident}
.loading=${this.loading}
@updateSave=${this.handleUpdateSave}
@cancel=${this.handleCancel}
></upladmin-incident-update>
`;
}
if (this.currentMode === 'form') {
const incident = this.selectedIncidentId
? adminState.incidents.find((i) => i.id === this.selectedIncidentId)
: null;
return html`
<upladmin-incident-form
.incident=${incident
? {
id: incident.id,
title: incident.title,
severity: incident.severity,
status: incident.status,
affectedServices: incident.affectedServices,
impact: incident.impact,
rootCause: incident.rootCause,
resolution: incident.resolution,
}
: null}
.availableServices=${adminState.monitors}
.loading=${this.loading}
@incidentSave=${this.handleIncidentSave}
@cancel=${this.handleCancel}
></upladmin-incident-form>
`;
}
return html`
<upladmin-incident-list
.incidents=${this.filteredIncidents}
.loading=${this.loading}
@incidentAdd=${() => this.showForm(null)}
@incidentEdit=${this.handleIncidentEdit}
@incidentAddUpdate=${this.handleIncidentAddUpdate}
></upladmin-incident-list>
`;
}
}

View File

@@ -0,0 +1,244 @@
import {
DeesElement,
customElement,
html,
state,
css,
cssManager,
} from '@design.estate/dees-element';
import type { DeesAppuiBase } from '@design.estate/dees-catalog';
// View lifecycle interfaces (defined locally as they're not exported from dees-catalog)
interface IViewActivationContext {
appui: DeesAppuiBase;
viewId: string;
params?: Record<string, string>;
}
interface IViewLifecycle {
onActivate?: (context: IViewActivationContext) => void | Promise<void>;
onDeactivate?: () => void | Promise<void>;
}
import { adminState } from '../../services/admin-state.js';
import type { IServiceStatus, TStatusType } from '../../interfaces/index.js';
import '../../elements/upladmin-monitor-list/upladmin-monitor-list.js';
import '../../elements/upladmin-monitor-form/upladmin-monitor-form.js';
type TViewMode = 'list' | 'form';
type TStatusFilter = 'all' | 'issues' | 'maintenance';
@customElement('upladmin-monitors-view')
export class UpladminMonitorsView extends DeesElement implements IViewLifecycle {
@state()
accessor currentMode: TViewMode = 'list';
@state()
accessor selectedMonitorId: string | null = null;
@state()
accessor statusFilter: TStatusFilter = 'all';
@state()
accessor categoryFilter: string = 'all';
@state()
accessor loading: boolean = false;
private appuiRef: DeesAppuiBase | null = null;
public static styles = [
cssManager.defaultStyles,
css`
:host {
display: block;
height: 100%;
}
`,
];
async onActivate(context: IViewActivationContext): Promise<void> {
this.appuiRef = context.appui;
// Check route params for edit mode
if (context.params?.id) {
this.currentMode = 'form';
this.selectedMonitorId = context.params.id === 'create' ? null : context.params.id;
} else {
this.currentMode = 'list';
this.selectedMonitorId = null;
}
// Set secondary menu with categories
this.updateSecondaryMenu();
// Set content tabs for status filtering
context.appui.setContentTabs([
{
key: 'All',
iconName: 'lucide:activity',
action: () => this.setStatusFilter('all'),
},
{
key: 'Issues',
iconName: 'lucide:alertTriangle',
action: () => this.setStatusFilter('issues'),
},
{
key: 'Maintenance',
iconName: 'lucide:wrench',
action: () => this.setStatusFilter('maintenance'),
},
]);
}
private updateSecondaryMenu(): void {
if (!this.appuiRef) return;
const categories = this.getCategories();
this.appuiRef.setSecondaryMenu({
heading: 'Monitors',
groups: [
{
name: 'Categories',
iconName: 'lucide:folder',
items: [
{
key: 'all',
iconName: 'lucide:list',
action: () => this.setCategoryFilter('all'),
badge: adminState.monitors.length,
},
...categories.map((cat) => ({
key: cat,
iconName: 'lucide:folder',
action: () => this.setCategoryFilter(cat),
badge: adminState.monitors.filter((m) => m.category === cat).length,
})),
],
},
{
name: 'Actions',
iconName: 'lucide:zap',
items: [
{
key: 'add',
iconName: 'lucide:plus',
action: () => this.showForm(null),
},
],
},
],
});
// Select current category
this.appuiRef.setSecondaryMenuSelection(this.categoryFilter);
}
private getCategories(): string[] {
const cats = new Set<string>();
for (const m of adminState.monitors) {
if (m.category) cats.add(m.category);
}
return Array.from(cats).sort();
}
private setStatusFilter(filter: TStatusFilter): void {
this.statusFilter = filter;
}
private setCategoryFilter(category: string): void {
this.categoryFilter = category;
this.appuiRef?.setSecondaryMenuSelection(category);
}
private showForm(monitorId: string | null): void {
this.currentMode = 'form';
this.selectedMonitorId = monitorId;
}
private showList(): void {
this.currentMode = 'list';
this.selectedMonitorId = null;
}
private get filteredMonitors(): IServiceStatus[] {
let monitors = adminState.monitors;
// Apply category filter
if (this.categoryFilter !== 'all') {
monitors = monitors.filter((m) => m.category === this.categoryFilter);
}
// Apply status filter
if (this.statusFilter === 'issues') {
monitors = monitors.filter((m) =>
['degraded', 'partial_outage', 'major_outage', 'error'].includes(m.currentStatus)
);
} else if (this.statusFilter === 'maintenance') {
monitors = monitors.filter((m) => m.currentStatus === 'maintenance');
}
return monitors;
}
private handleMonitorSave = (e: CustomEvent): void => {
// Handle save logic
console.log('Monitor saved:', e.detail);
this.showList();
};
private handleCancel = (): void => {
this.showList();
};
private handleMonitorEdit = (e: CustomEvent): void => {
const monitor = e.detail?.monitor as IServiceStatus;
if (monitor) {
this.showForm(monitor.id);
}
};
render() {
if (this.currentMode === 'form') {
const monitor = this.selectedMonitorId
? adminState.monitors.find((m) => m.id === this.selectedMonitorId)
: null;
return html`
<upladmin-monitor-form
.monitor=${monitor
? {
id: monitor.id,
name: monitor.name,
displayName: monitor.displayName,
description: monitor.description,
category: monitor.category,
dependencies: monitor.dependencies,
statusMode: monitor.statusMode || 'auto',
manualStatus: monitor.manualStatus,
paused: monitor.paused || false,
checkType: monitor.checkType || 'assumption',
checkConfig: monitor.checkConfig || { domain: '' },
intervalMs: monitor.intervalMs || 60000,
}
: null}
.availableMonitors=${adminState.monitors}
.categories=${this.getCategories()}
.loading=${this.loading}
@monitorSave=${this.handleMonitorSave}
@cancel=${this.handleCancel}
></upladmin-monitor-form>
`;
}
return html`
<upladmin-monitor-list
.monitors=${this.filteredMonitors}
.loading=${this.loading}
@monitorAdd=${() => this.showForm(null)}
@monitorEdit=${this.handleMonitorEdit}
></upladmin-monitor-list>
`;
}
}