This commit is contained in:
2025-12-24 10:57:43 +00:00
commit ba79b4bfb6
118 changed files with 292546 additions and 0 deletions

View File

@@ -0,0 +1 @@
export * from './upladmin-incident-form.js';

View File

@@ -0,0 +1,70 @@
import { html, css, cssManager } from '@design.estate/dees-element';
import type { IServiceStatus } from '../../interfaces/index.js';
import './upladmin-incident-form.js';
export const demoFunc = () => html`
<style>
${css`
.demo-container {
display: flex;
flex-direction: column;
gap: 32px;
padding: 24px;
max-width: 900px;
margin: 0 auto;
background: ${cssManager.bdTheme('#fafafa', '#0a0a0a')};
min-height: 100vh;
}
.demo-section {
margin-bottom: 24px;
}
.demo-section h3 {
margin: 0 0 16px 0;
font-size: 14px;
font-weight: 600;
color: ${cssManager.bdTheme('#71717a', '#a1a1aa')};
text-transform: uppercase;
letter-spacing: 0.05em;
}
`}
</style>
<div class="demo-container">
<div class="demo-section">
<h3>Create New Incident</h3>
<upladmin-incident-form
.availableServices=${[
{ id: 'api', name: 'api', displayName: 'API Server', currentStatus: 'operational', lastChecked: Date.now(), uptime30d: 99.98, uptime90d: 99.95, responseTime: 45 },
{ id: 'web', name: 'web', displayName: 'Web Application', currentStatus: 'operational', lastChecked: Date.now(), uptime30d: 99.99, uptime90d: 99.97, responseTime: 120 },
{ id: 'db', name: 'db', displayName: 'Database', currentStatus: 'operational', lastChecked: Date.now(), uptime30d: 99.999, uptime90d: 99.998, responseTime: 5 },
{ id: 'cdn', name: 'cdn', displayName: 'CDN', currentStatus: 'degraded', lastChecked: Date.now(), uptime30d: 99.5, uptime90d: 99.8, responseTime: 200 },
{ id: 'cache', name: 'cache', displayName: 'Redis Cache', currentStatus: 'operational', lastChecked: Date.now(), uptime30d: 99.99, uptime90d: 99.98, responseTime: 2 },
] as IServiceStatus[]}
></upladmin-incident-form>
</div>
<div class="demo-section">
<h3>Edit Existing Incident</h3>
<upladmin-incident-form
.incident=${{
id: 'inc-123',
title: 'Database Connection Issues',
severity: 'major',
status: 'identified',
affectedServices: ['db', 'api'],
impact: 'Users may experience slow response times and occasional timeouts when accessing the application.',
rootCause: 'Connection pool exhaustion due to a memory leak in the database driver.',
}}
.availableServices=${[
{ id: 'api', name: 'api', displayName: 'API Server', currentStatus: 'degraded', lastChecked: Date.now(), uptime30d: 99.98, uptime90d: 99.95, responseTime: 45 },
{ id: 'web', name: 'web', displayName: 'Web Application', currentStatus: 'operational', lastChecked: Date.now(), uptime30d: 99.99, uptime90d: 99.97, responseTime: 120 },
{ id: 'db', name: 'db', displayName: 'Database', currentStatus: 'partial_outage', lastChecked: Date.now(), uptime30d: 99.999, uptime90d: 99.998, responseTime: 5 },
{ id: 'cdn', name: 'cdn', displayName: 'CDN', currentStatus: 'operational', lastChecked: Date.now(), uptime30d: 99.5, uptime90d: 99.8, responseTime: 200 },
{ id: 'cache', name: 'cache', displayName: 'Redis Cache', currentStatus: 'operational', lastChecked: Date.now(), uptime30d: 99.99, uptime90d: 99.98, responseTime: 2 },
] as IServiceStatus[]}
></upladmin-incident-form>
</div>
</div>
`;

View File

@@ -0,0 +1,585 @@
import * as plugins from '../../plugins.js';
import {
DeesElement,
property,
html,
customElement,
type TemplateResult,
css,
cssManager,
unsafeCSS,
state,
} from '@design.estate/dees-element';
import * as sharedStyles from '../../styles/shared.styles.js';
import type { IIncidentFormData, IServiceStatus } from '../../interfaces/index.js';
import { demoFunc } from './upladmin-incident-form.demo.js';
declare global {
interface HTMLElementTagNameMap {
'upladmin-incident-form': UpladminIncidentForm;
}
}
type TSeverity = 'critical' | 'major' | 'minor' | 'maintenance';
type TIncidentStatus = 'investigating' | 'identified' | 'monitoring' | 'resolved' | 'postmortem';
@customElement('upladmin-incident-form')
export class UpladminIncidentForm extends DeesElement {
public static demo = demoFunc;
@property({ type: Object })
accessor incident: IIncidentFormData | null = null;
@property({ type: Array })
accessor availableServices: IServiceStatus[] = [];
@property({ type: Boolean })
accessor loading: boolean = false;
@state()
accessor formData: IIncidentFormData = {
title: '',
severity: 'minor',
status: 'investigating',
affectedServices: [],
impact: '',
rootCause: '',
resolution: '',
};
@state()
accessor errors: Record<string, string> = {};
private severityIcons: Record<TSeverity, string> = {
critical: 'lucide:AlertCircle',
major: 'lucide:AlertTriangle',
minor: 'lucide:Info',
maintenance: 'lucide:Wrench',
};
private statusIcons: Record<TIncidentStatus, string> = {
investigating: 'lucide:Search',
identified: 'lucide:Target',
monitoring: 'lucide:Eye',
resolved: 'lucide:CheckCircle',
postmortem: 'lucide:FileText',
};
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)};
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};
}
.form-header dees-icon {
--icon-color: ${cssManager.bdTheme('#f97316', '#fb923c')};
}
.form-title-wrapper {
flex: 1;
}
.form-title {
font-size: 18px;
font-weight: 600;
color: ${sharedStyles.colors.text.primary};
margin: 0;
}
.form-subtitle {
font-size: 13px;
color: ${sharedStyles.colors.text.muted};
margin-top: 4px;
}
.form-body {
display: grid;
gap: ${unsafeCSS(sharedStyles.spacing.lg)};
padding: ${unsafeCSS(sharedStyles.spacing.lg)};
}
dees-form {
display: contents;
}
.form-row {
display: grid;
grid-template-columns: 1fr 1fr;
gap: ${unsafeCSS(sharedStyles.spacing.md)};
}
@media (max-width: 600px) {
.form-row {
grid-template-columns: 1fr;
}
}
.form-section {
margin-top: ${unsafeCSS(sharedStyles.spacing.md)};
padding-top: ${unsafeCSS(sharedStyles.spacing.md)};
border-top: 1px solid ${sharedStyles.colors.border.light};
}
.section-title {
display: flex;
align-items: center;
gap: 8px;
font-size: 14px;
font-weight: 600;
color: ${sharedStyles.colors.text.primary};
margin-bottom: ${unsafeCSS(sharedStyles.spacing.md)};
}
.section-title dees-icon {
--icon-color: ${sharedStyles.colors.text.muted};
}
.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};
}
.option-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(130px, 1fr));
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;
font-weight: 500;
color: ${sharedStyles.colors.text.primary};
margin-bottom: ${unsafeCSS(sharedStyles.spacing.sm)};
}
.field-label.required::after {
content: ' *';
color: ${sharedStyles.colors.accent.danger};
}
.services-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
gap: ${unsafeCSS(sharedStyles.spacing.sm)};
max-height: 220px;
overflow-y: auto;
padding: 4px;
}
.service-checkbox {
display: flex;
align-items: center;
gap: 10px;
padding: 12px 14px;
background: ${sharedStyles.colors.background.primary};
border: 1px solid ${sharedStyles.colors.border.default};
border-radius: ${unsafeCSS(sharedStyles.borderRadius.base)};
cursor: pointer;
transition: all ${unsafeCSS(sharedStyles.durations.fast)} ${unsafeCSS(sharedStyles.easings.default)};
}
.service-checkbox:hover {
background: ${sharedStyles.colors.background.muted};
border-color: ${sharedStyles.colors.border.strong};
}
.service-checkbox.selected {
border-color: ${sharedStyles.colors.accent.primary};
background: ${cssManager.bdTheme('rgba(59, 130, 246, 0.05)', 'rgba(96, 165, 250, 0.1)')};
}
.service-checkbox input {
width: 16px;
height: 16px;
accent-color: ${sharedStyles.colors.accent.primary};
cursor: pointer;
}
.service-checkbox label {
flex: 1;
font-size: 13px;
color: ${sharedStyles.colors.text.primary};
cursor: pointer;
}
.status-dot {
width: 8px;
height: 8px;
border-radius: 50%;
flex-shrink: 0;
}
.status-dot.operational { background: ${sharedStyles.colors.status.operational}; }
.status-dot.degraded { background: ${sharedStyles.colors.status.degraded}; }
.status-dot.partial_outage { background: ${sharedStyles.colors.status.partialOutage}; }
.status-dot.major_outage { background: ${sharedStyles.colors.status.majorOutage}; }
.status-dot.maintenance { background: ${sharedStyles.colors.status.maintenance}; }
.error-text {
display: flex;
align-items: center;
gap: 6px;
font-size: 12px;
color: ${sharedStyles.colors.accent.danger};
margin-top: ${unsafeCSS(sharedStyles.spacing.xs)};
}
.error-text dees-icon {
--icon-color: ${sharedStyles.colors.accent.danger};
}
/* Style dees-input components */
dees-input-text {
--dees-input-background: ${sharedStyles.colors.background.primary};
--dees-input-border-color: ${sharedStyles.colors.border.default};
}
`
];
async connectedCallback() {
await super.connectedCallback();
if (this.incident) {
this.formData = { ...this.incident };
}
}
updated(changedProperties: Map<string, unknown>) {
if (changedProperties.has('incident') && this.incident) {
this.formData = { ...this.incident };
}
}
public render(): TemplateResult {
const isEdit = !!this.incident?.id;
const severityOptions: Array<{ value: TSeverity; label: string; desc: string }> = [
{ value: 'critical', label: 'Critical', desc: 'Major system failure' },
{ value: 'major', label: 'Major', desc: 'Significant impact' },
{ value: 'minor', label: 'Minor', desc: 'Limited impact' },
{ value: 'maintenance', label: 'Maintenance', desc: 'Planned work' },
];
const statusOptions: Array<{ value: TIncidentStatus; label: string }> = [
{ value: 'investigating', label: 'Investigating' },
{ value: 'identified', label: 'Identified' },
{ value: 'monitoring', label: 'Monitoring' },
{ value: 'resolved', label: 'Resolved' },
{ value: 'postmortem', label: 'Postmortem' },
];
return html`
<div class="form-container">
<div class="form-header">
<dees-icon .icon=${isEdit ? 'lucide:Pencil' : 'lucide:AlertTriangle'} .iconSize=${24}></dees-icon>
<div class="form-title-wrapper">
<h2 class="form-title">${isEdit ? 'Edit Incident' : 'Create Incident'}</h2>
<p class="form-subtitle">
${isEdit ? 'Update incident details' : 'Report a new incident or maintenance'}
</p>
</div>
</div>
<div class="form-body">
<dees-form>
<dees-input-text
key="title"
label="Incident Title"
.value="${this.formData.title}"
placeholder="Brief description of the incident"
required
@changeSubject="${this.handleTitleChange}"
></dees-input-text>
<div>
<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>
`)}
</div>
</div>
<div>
<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>
`)}
</div>
</div>
<div>
<label class="field-label required">Affected Services</label>
<div class="services-grid">
${this.availableServices.map(service => html`
<div
class="service-checkbox ${this.formData.affectedServices.includes(service.id) ? 'selected' : ''}"
@click="${() => this.toggleService(service.id)}"
>
<input
type="checkbox"
id="service-${service.id}"
?checked="${this.formData.affectedServices.includes(service.id)}"
/>
<span class="status-dot ${service.currentStatus}"></span>
<label for="service-${service.id}">${service.displayName || service.name}</label>
</div>
`)}
</div>
${this.errors.affectedServices ? html`
<div class="error-text">
<dees-icon .icon=${'lucide:AlertCircle'} .iconSize=${12}></dees-icon>
${this.errors.affectedServices}
</div>
` : ''}
</div>
<dees-input-text
key="impact"
label="Impact Description"
inputType="textarea"
.value="${this.formData.impact}"
placeholder="Describe how users are affected by this incident..."
required
@changeSubject="${this.handleImpactChange}"
></dees-input-text>
<div class="form-section">
<div class="section-title">
<dees-icon .icon=${'lucide:FileSearch'} .iconSize=${16}></dees-icon>
Resolution Details (Optional)
</div>
<div class="form-row">
<dees-input-text
key="rootCause"
label="Root Cause"
inputType="textarea"
.value="${this.formData.rootCause || ''}"
placeholder="What caused this incident..."
@changeSubject="${this.handleRootCauseChange}"
></dees-input-text>
<dees-input-text
key="resolution"
label="Resolution"
inputType="textarea"
.value="${this.formData.resolution || ''}"
placeholder="How was this incident resolved..."
@changeSubject="${this.handleResolutionChange}"
></dees-input-text>
</div>
</div>
</dees-form>
</div>
<div class="form-actions">
<dees-button type="discreet" @click="${this.handleCancel}" ?disabled="${this.loading}">
Cancel
</dees-button>
<dees-button type="highlighted" @click="${this.handleSave}" ?disabled="${this.loading}">
${this.loading ? html`<dees-spinner .size=${16}></dees-spinner>` : ''}
${isEdit ? 'Update Incident' : 'Create Incident'}
</dees-button>
</div>
</div>
`;
}
private handleTitleChange(e: CustomEvent) {
this.formData = { ...this.formData, title: e.detail };
if (this.errors.title) {
this.errors = { ...this.errors, title: '' };
}
}
private handleImpactChange(e: CustomEvent) {
this.formData = { ...this.formData, impact: e.detail };
if (this.errors.impact) {
this.errors = { ...this.errors, impact: '' };
}
}
private handleRootCauseChange(e: CustomEvent) {
this.formData = { ...this.formData, rootCause: e.detail };
}
private handleResolutionChange(e: CustomEvent) {
this.formData = { ...this.formData, resolution: e.detail };
}
private handleSeverityChange(severity: TSeverity) {
this.formData = { ...this.formData, severity };
}
private handleStatusChange(status: TIncidentStatus) {
this.formData = { ...this.formData, status };
}
private toggleService(serviceId: string) {
const current = this.formData.affectedServices;
if (current.includes(serviceId)) {
this.formData = {
...this.formData,
affectedServices: current.filter(id => id !== serviceId)
};
} else {
this.formData = {
...this.formData,
affectedServices: [...current, serviceId]
};
}
if (this.errors.affectedServices) {
this.errors = { ...this.errors, affectedServices: '' };
}
}
private validate(): boolean {
const errors: Record<string, string> = {};
if (!this.formData.title?.trim()) {
errors.title = 'Title is required';
}
if (this.formData.affectedServices.length === 0) {
errors.affectedServices = 'At least one service must be selected';
}
if (!this.formData.impact?.trim()) {
errors.impact = 'Impact description is required';
}
this.errors = errors;
return Object.keys(errors).length === 0;
}
private handleSave() {
if (!this.validate()) {
return;
}
this.dispatchEvent(new CustomEvent('incidentSave', {
detail: { incident: { ...this.formData } },
bubbles: true,
composed: true
}));
}
private handleCancel() {
this.dispatchEvent(new CustomEvent('incidentCancel', {
bubbles: true,
composed: true
}));
}
public reset() {
this.formData = {
title: '',
severity: 'minor',
status: 'investigating',
affectedServices: [],
impact: '',
rootCause: '',
resolution: '',
};
this.errors = {};
}
public setIncident(incident: IIncidentFormData) {
this.formData = { ...incident };
this.errors = {};
}
}