479 lines
15 KiB
TypeScript
479 lines
15 KiB
TypeScript
|
|
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; message: string }> = [
|
||
|
|
{ icon: 'lucide:Search', label: 'Started investigating', message: 'We are currently investigating this issue.' },
|
||
|
|
{ icon: 'lucide:Target', label: 'Issue identified', message: 'We have identified the root cause and are working on a fix.' },
|
||
|
|
{ icon: 'lucide:Rocket', label: 'Fix deployed', message: 'A fix has been deployed. We are monitoring the results.' },
|
||
|
|
{ icon: 'lucide:CheckCircle', label: 'Resolved', message: 'This incident has been resolved. All systems are operating normally.' },
|
||
|
|
];
|
||
|
|
|
||
|
|
const 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>
|
||
|
|
<label class="field-label required">Status</label>
|
||
|
|
<div class="status-grid">
|
||
|
|
${statusOptions.map(opt => html`
|
||
|
|
<label
|
||
|
|
class="status-option ${opt.value} ${this.formData.status === opt.value ? 'selected' : ''}"
|
||
|
|
@click="${() => this.handleStatusChange(opt.value)}"
|
||
|
|
>
|
||
|
|
<input
|
||
|
|
type="radio"
|
||
|
|
name="status"
|
||
|
|
value="${opt.value}"
|
||
|
|
?checked="${this.formData.status === opt.value}"
|
||
|
|
/>
|
||
|
|
<dees-icon .icon=${this.statusIcons[opt.value]} .iconSize=${24}></dees-icon>
|
||
|
|
<span class="status-label">${opt.label}</span>
|
||
|
|
<span class="status-desc">${opt.desc}</span>
|
||
|
|
</label>
|
||
|
|
`)}
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<div>
|
||
|
|
<label class="field-label required">Update Message</label>
|
||
|
|
<div class="template-section">
|
||
|
|
<div class="template-label">Quick templates:</div>
|
||
|
|
<div class="template-buttons">
|
||
|
|
${templates.map(tpl => html`
|
||
|
|
<button type="button" class="template-btn" @click="${() => this.applyTemplate(tpl.message)}">
|
||
|
|
<dees-icon .icon=${tpl.icon} .iconSize=${12}></dees-icon>
|
||
|
|
${tpl.label}
|
||
|
|
</button>
|
||
|
|
`)}
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
<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(message: string) {
|
||
|
|
this.formData = { ...this.formData, message };
|
||
|
|
}
|
||
|
|
|
||
|
|
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 = {};
|
||
|
|
}
|
||
|
|
}
|