Files

482 lines
16 KiB
TypeScript
Raw Permalink Normal View History

2025-12-24 10:57:43 +00:00
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 { IIncidentUpdateFormData, IIncidentDetails } from '../../interfaces/index.js';
import { demoFunc } from './upladmin-incident-update.demo.js';
declare global {
interface HTMLElementTagNameMap {
'upladmin-incident-update': UpladminIncidentUpdate;
}
}
type TIncidentStatus = 'investigating' | 'identified' | 'monitoring' | 'resolved' | 'postmortem';
@customElement('upladmin-incident-update')
export class UpladminIncidentUpdate extends DeesElement {
public static demo = demoFunc;
@property({ type: Object })
accessor incident: IIncidentDetails | null = null;
@property({ type: Boolean })
accessor loading: boolean = false;
@state()
accessor formData: IIncidentUpdateFormData = {
status: 'investigating',
message: '',
author: '',
};
@state()
accessor errors: Record<string, string> = {};
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)};
}
.update-container {
background: ${sharedStyles.colors.background.secondary};
border: 1px solid ${sharedStyles.colors.border.default};
border-radius: ${unsafeCSS(sharedStyles.borderRadius.lg)};
overflow: hidden;
}
.update-header {
padding: ${unsafeCSS(sharedStyles.spacing.lg)};
border-bottom: 1px solid ${sharedStyles.colors.border.default};
background: ${sharedStyles.colors.background.muted};
}
.update-title-row {
display: flex;
align-items: center;
gap: ${unsafeCSS(sharedStyles.spacing.md)};
margin-bottom: ${unsafeCSS(sharedStyles.spacing.sm)};
}
.update-title-row dees-icon {
--icon-color: ${cssManager.bdTheme('#3b82f6', '#60a5fa')};
}
.update-title {
font-size: 18px;
font-weight: 600;
color: ${sharedStyles.colors.text.primary};
margin: 0;
}
.incident-info {
display: flex;
align-items: center;
gap: ${unsafeCSS(sharedStyles.spacing.sm)};
padding-left: 36px;
}
.incident-name {
font-size: 14px;
color: ${sharedStyles.colors.text.secondary};
}
.severity-badge {
display: inline-flex;
align-items: center;
gap: 6px;
padding: 4px 10px;
font-size: 11px;
font-weight: 500;
border-radius: 9999px;
text-transform: uppercase;
}
.severity-badge dees-icon {
font-size: 12px;
}
.severity-badge.critical {
background: ${cssManager.bdTheme('rgba(239, 68, 68, 0.1)', 'rgba(239, 68, 68, 0.2)')};
color: ${sharedStyles.colors.status.majorOutage};
--icon-color: ${sharedStyles.colors.status.majorOutage};
}
.severity-badge.major {
background: ${cssManager.bdTheme('rgba(249, 115, 22, 0.1)', 'rgba(249, 115, 22, 0.2)')};
color: ${sharedStyles.colors.status.partialOutage};
--icon-color: ${sharedStyles.colors.status.partialOutage};
}
.severity-badge.minor {
background: ${cssManager.bdTheme('rgba(234, 179, 8, 0.1)', 'rgba(234, 179, 8, 0.2)')};
color: ${sharedStyles.colors.status.degraded};
--icon-color: ${sharedStyles.colors.status.degraded};
}
.severity-badge.maintenance {
background: ${cssManager.bdTheme('rgba(59, 130, 246, 0.1)', 'rgba(59, 130, 246, 0.2)')};
color: ${sharedStyles.colors.status.maintenance};
--icon-color: ${sharedStyles.colors.status.maintenance};
}
.update-body {
display: grid;
gap: ${unsafeCSS(sharedStyles.spacing.lg)};
padding: ${unsafeCSS(sharedStyles.spacing.lg)};
}
dees-form {
display: contents;
}
.status-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(130px, 1fr));
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;
font-weight: 500;
color: ${sharedStyles.colors.text.primary};
margin-bottom: ${unsafeCSS(sharedStyles.spacing.xs)};
}
.field-label.required::after {
content: ' *';
color: ${sharedStyles.colors.accent.danger};
}
.template-section {
margin-bottom: ${unsafeCSS(sharedStyles.spacing.sm)};
}
.template-label {
font-size: 12px;
color: ${sharedStyles.colors.text.muted};
margin-bottom: 8px;
}
.template-buttons {
display: flex;
flex-wrap: wrap;
gap: 8px;
}
.template-btn {
display: inline-flex;
align-items: center;
gap: 6px;
padding: 8px 14px;
font-size: 12px;
font-weight: 500;
font-family: ${unsafeCSS(sharedStyles.fonts.base)};
color: ${sharedStyles.colors.text.secondary};
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)};
}
.template-btn:hover {
background: ${sharedStyles.colors.background.muted};
border-color: ${sharedStyles.colors.border.strong};
color: ${sharedStyles.colors.text.primary};
}
.template-btn dees-icon {
--icon-color: currentColor;
opacity: 0.6;
}
.update-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};
}
/* 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.formData,
status: this.incident.status,
};
}
}
updated(changedProperties: Map<string, unknown>) {
if (changedProperties.has('incident') && this.incident) {
this.formData = {
...this.formData,
status: this.incident.status,
};
}
}
public render(): TemplateResult {
if (!this.incident) {
return html`<div class="update-container">No incident selected</div>`;
}
const statusOptions: Array<{ value: TIncidentStatus; label: string; desc: string }> = [
{ value: 'investigating', label: 'Investigating', desc: 'Looking into the issue' },
{ value: 'identified', label: 'Identified', desc: 'Root cause found' },
{ value: 'monitoring', label: 'Monitoring', desc: 'Fix applied, watching' },
{ value: 'resolved', label: 'Resolved', desc: 'Issue is fixed' },
{ value: 'postmortem', label: 'Postmortem', desc: 'Analysis complete' },
];
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.' },
2025-12-24 10:57:43 +00:00
];
const severityIcons: Record<string, string> = {
critical: 'lucide:AlertCircle',
major: 'lucide:AlertTriangle',
minor: 'lucide:Info',
maintenance: 'lucide:Wrench',
};
return html`
<div class="update-container">
<div class="update-header">
<div class="update-title-row">
<dees-icon .icon=${'lucide:MessageSquarePlus'} .iconSize=${24}></dees-icon>
<h2 class="update-title">Post Update</h2>
</div>
<div class="incident-info">
<span class="severity-badge ${this.incident.severity}">
<dees-icon .icon=${severityIcons[this.incident.severity]} .iconSize=${12}></dees-icon>
${this.incident.severity}
</span>
<span class="incident-name">${this.incident.title}</span>
</div>
</div>
<div class="update-body">
<dees-form>
<div class="template-section">
<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)}">
<dees-icon .icon=${tpl.icon} .iconSize=${12}></dees-icon>
${tpl.label}
</button>
`)}
</div>
</div>
2025-12-24 10:57:43 +00:00
<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>
<dees-input-text
key="message"
inputType="textarea"
.value="${this.formData.message}"
placeholder="Provide an update on the incident status..."
required
@changeSubject="${this.handleMessageChange}"
></dees-input-text>
</div>
<dees-input-text
key="author"
label="Author (Optional)"
.value="${this.formData.author || ''}"
placeholder="Your name or team name"
@changeSubject="${this.handleAuthorChange}"
></dees-input-text>
</dees-form>
</div>
<div class="update-actions">
<dees-button type="discreet" @click="${this.handleCancel}" ?disabled="${this.loading}">
Cancel
</dees-button>
${this.formData.status === 'resolved' ? html`
<dees-button type="highlighted" @click="${this.handlePost}" ?disabled="${this.loading}" style="--dees-button-background: ${sharedStyles.colors.status.operational}">
${this.loading ? html`<dees-spinner .size=${16}></dees-spinner>` : html`<dees-icon .icon=${'lucide:CheckCircle'} .iconSize=${16}></dees-icon>`}
Resolve Incident
</dees-button>
` : html`
<dees-button type="highlighted" @click="${this.handlePost}" ?disabled="${this.loading}">
${this.loading ? html`<dees-spinner .size=${16}></dees-spinner>` : html`<dees-icon .icon=${'lucide:Send'} .iconSize=${16}></dees-icon>`}
Post Update
</dees-button>
`}
</div>
</div>
`;
}
private handleMessageChange(e: CustomEvent) {
this.formData = { ...this.formData, message: e.detail };
if (this.errors.message) {
this.errors = { ...this.errors, message: '' };
}
}
private handleAuthorChange(e: CustomEvent) {
this.formData = { ...this.formData, author: e.detail };
}
private handleStatusChange(status: TIncidentStatus) {
this.formData = { ...this.formData, status };
}
private applyTemplate(template: { status: TIncidentStatus; message: string }) {
this.formData = { ...this.formData, status: template.status, message: template.message };
2025-12-24 10:57:43 +00:00
}
private validate(): boolean {
const errors: Record<string, string> = {};
if (!this.formData.message?.trim()) {
errors.message = 'Update message is required';
}
this.errors = errors;
return Object.keys(errors).length === 0;
}
private handlePost() {
if (!this.validate()) {
return;
}
this.dispatchEvent(new CustomEvent('updatePost', {
detail: {
incidentId: this.incident?.id,
update: { ...this.formData }
},
bubbles: true,
composed: true
}));
}
private handleCancel() {
this.dispatchEvent(new CustomEvent('updateCancel', {
bubbles: true,
composed: true
}));
}
public reset() {
this.formData = {
status: this.incident?.status || 'investigating',
message: '',
author: '',
};
this.errors = {};
}
}