MotorSignal-App/website/components/ops/operations-create-incident.tsx

217 lines
8.7 KiB
TypeScript

'use client';
import React, { useState } from 'react';
import { IncidentSeverity } from './types';
interface OperationsCreateIncidentProps {
onIncidentCreated: (caseRef: string) => void;
onCancel: () => void;
}
export function OperationsCreateIncident({
onIncidentCreated,
onCancel,
}: OperationsCreateIncidentProps) {
const [title, setTitle] = useState('');
const [summary, setSummary] = useState('');
const [severity, setSeverity] = useState<IncidentSeverity>('medium');
const [isSubmitting, setIsSubmitting] = useState(false);
const [errorMessage, setErrorMessage] = useState<string | null>(null);
async function handleSubmit(e: React.FormEvent) {
e.preventDefault();
if (!title.trim()) {
setErrorMessage('Incident title is required.');
return;
}
if (!summary.trim()) {
setErrorMessage('Incident summary is required.');
return;
}
setIsSubmitting(true);
setErrorMessage(null);
try {
const response = await fetch('/api/ops/incidents', {
method: 'POST',
headers: {
'content-type': 'application/json',
accept: 'application/json',
},
body: JSON.stringify({
title: title.trim(),
summary: summary.trim(),
severity,
}),
});
if (response.status === 401) {
setErrorMessage('Your Operations session has expired.');
setIsSubmitting(false);
return;
}
if (response.status === 403) {
setErrorMessage('You do not have authority to create incidents.');
setIsSubmitting(false);
return;
}
if (response.status === 422) {
const data = await response.json().catch(() => null);
setErrorMessage(data?.detail ?? 'Please check the incident fields and try again.');
setIsSubmitting(false);
return;
}
if (!response.ok) {
const data = await response.json().catch(() => null);
setErrorMessage(data?.detail ?? 'Operations platform is temporarily unavailable.');
setIsSubmitting(false);
return;
}
const data = await response.json();
if (data?.case_reference) {
onIncidentCreated(data.case_reference);
} else {
onCancel();
}
} catch {
setErrorMessage('Unable to connect to Operations platform.');
setIsSubmitting(false);
}
}
return (
<div className="max-w-4xl mx-auto space-y-8">
{/* Header */}
<div className="flex flex-col sm:flex-row sm:items-end justify-between gap-4 pb-5 border-b border-outline-variant/30">
<div>
<span className="text-xs font-mono text-primary-fixed-dim font-semibold">
New Incident Record
</span>
<h1 className="font-headline-xl text-2xl sm:text-3xl lg:text-4xl font-extrabold text-on-surface tracking-tight mt-1.5">
Create Operational Incident
</h1>
<p className="text-xs sm:text-sm text-on-surface-variant mt-1.5">
Case reference and open status will be generated automatically.
</p>
</div>
<button
type="button"
onClick={onCancel}
className="inline-flex items-center gap-1.5 px-4 py-2.5 rounded-xl bg-surface-container-high hover:bg-surface-bright border border-outline-variant/40 text-xs sm:text-sm font-semibold text-on-surface transition-colors focus-visible:ring-2 focus-visible:ring-primary-container focus-visible:outline-none cursor-pointer"
>
Cancel
</button>
</div>
{/* Error Alert */}
{errorMessage && (
<div className="p-4 rounded-xl bg-error/10 border border-error/30 text-error flex items-center gap-3 text-xs sm:text-sm">
<span className="material-symbols-outlined text-[20px]">error</span>
<span>{errorMessage}</span>
</div>
)}
{/* Form Card */}
<form onSubmit={handleSubmit} className="bg-surface-container-low p-6 sm:p-8 rounded-2xl border border-outline-variant/30 space-y-6 shadow-sm">
{/* Title */}
<div className="space-y-2">
<label htmlFor="incident-title" className="block text-xs sm:text-sm font-semibold text-on-surface">
Incident Title <span className="text-error">*</span>
</label>
<input
id="incident-title"
type="text"
required
maxLength={255}
value={title}
onChange={(e) => setTitle(e.target.value)}
placeholder="e.g. Ingress latency spike on edge cluster"
className="w-full bg-surface-container px-4 py-2.5 rounded-xl border border-outline-variant/40 text-xs sm:text-sm text-on-surface placeholder:text-outline focus-visible:ring-2 focus-visible:ring-primary-container focus-visible:outline-none"
/>
</div>
{/* Severity Selection */}
<div className="space-y-1.5">
<label className="block text-xs font-semibold text-on-surface">
Initial Severity <span className="text-error">*</span>
</label>
<div className="grid grid-cols-2 sm:grid-cols-4 gap-2.5">
{(['low', 'medium', 'high', 'critical'] as IncidentSeverity[]).map((sev) => {
const isSelected = severity === sev;
return (
<button
key={sev}
type="button"
onClick={() => setSeverity(sev)}
className={`px-3 py-2.5 rounded-lg border text-xs font-mono font-semibold capitalize transition-all flex flex-col items-center gap-1 focus-visible:ring-2 focus-visible:ring-primary-container focus-visible:outline-none ${
isSelected
? sev === 'critical'
? 'bg-error/20 border-error text-error shadow-[0_0_12px_rgba(255,100,100,0.2)]'
: sev === 'high'
? 'bg-amber-500/20 border-amber-500 text-amber-300'
: sev === 'medium'
? 'bg-primary-container/20 border-primary-container text-primary-container'
: 'bg-surface-container-high border-outline text-on-surface'
: 'bg-surface-container border-outline-variant/30 text-outline hover:text-on-surface hover:bg-surface-container-high'
}`}
>
<span>{sev}</span>
</button>
);
})}
</div>
</div>
{/* Summary */}
<div className="space-y-1.5">
<div className="flex items-center justify-between">
<label htmlFor="incident-summary" className="block text-xs font-semibold text-on-surface">
Summary & Symptoms <span className="text-error">*</span>
</label>
<span className="text-[10px] font-mono text-outline">
{summary.length} / 4000
</span>
</div>
<textarea
id="incident-summary"
required
rows={5}
maxLength={4000}
value={summary}
onChange={(e) => setSummary(e.target.value)}
placeholder="Describe the incident symptoms, initial scope, and observed system behavior..."
className="w-full bg-surface-container px-3.5 py-2.5 rounded-lg border border-outline-variant/40 text-xs text-on-surface placeholder:text-outline focus-visible:ring-2 focus-visible:ring-primary-container focus-visible:outline-none resize-y"
/>
</div>
{/* Actions Bar */}
<div className="flex items-center justify-end gap-3 pt-4 border-t border-outline-variant/20">
<button
type="button"
onClick={onCancel}
disabled={isSubmitting}
className="px-5 py-2.5 rounded-xl bg-surface-container border border-outline-variant/30 text-xs sm:text-sm font-semibold text-on-surface-variant hover:text-on-surface transition-colors focus-visible:ring-2 focus-visible:ring-primary-container focus-visible:outline-none cursor-pointer"
>
Cancel
</button>
<button
type="submit"
disabled={isSubmitting}
className="flex items-center gap-2 px-6 py-2.5 rounded-xl bg-primary-container text-on-primary-container text-xs sm:text-sm font-bold shadow-md hover:bg-primary-container/90 transition-all focus-visible:ring-2 focus-visible:ring-primary-container focus-visible:outline-none disabled:opacity-50 cursor-pointer"
>
{isSubmitting && (
<span className="material-symbols-outlined text-[18px] animate-spin">
progress_activity
</span>
)}
<span>{isSubmitting ? 'Creating...' : 'Create Incident'}</span>
</button>
</div>
</form>
</div>
);
}