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

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>
`;
}
}