diff --git a/frontend/src/components/layout/Sidebar.tsx b/frontend/src/components/layout/Sidebar.tsx
index 3f1ed27..057a270 100644
--- a/frontend/src/components/layout/Sidebar.tsx
+++ b/frontend/src/components/layout/Sidebar.tsx
@@ -17,7 +17,6 @@ import {
Puzzle,
Settings,
ArrowRightLeft,
- Sliders,
} from 'lucide-react'
import { Tooltip } from '../ui/Tooltip'
import { ProfileModal } from '../ProfileModal'
@@ -158,13 +157,6 @@ export function Sidebar() {
href: '/settings/system',
matchPaths: ['/settings/system'],
},
- {
- id: 'settings-incidents',
- label: 'Incidents',
- icon: Sliders,
- href: '/settings/incidents',
- matchPaths: ['/settings/incidents'],
- },
{
id: 'settings-migrations',
label: 'Migrations',
diff --git a/frontend/src/hooks/useIncidents.ts b/frontend/src/hooks/useIncidents.ts
index 3beaf61..4976b34 100644
--- a/frontend/src/hooks/useIncidents.ts
+++ b/frontend/src/hooks/useIncidents.ts
@@ -47,8 +47,6 @@ export function useIncidents(params: ListIncidentsParams = {}): UseIncidentsResu
params.severity,
params.limit,
params.page,
- // eslint-disable-next-line react-hooks/exhaustive-deps
- JSON.stringify(params.customFields),
])
useEffect(() => {
@@ -80,8 +78,6 @@ export function useIncidents(params: ListIncidentsParams = {}): UseIncidentsResu
params.severity,
params.limit,
params.page,
- // eslint-disable-next-line react-hooks/exhaustive-deps
- JSON.stringify(params.customFields),
])
return {
diff --git a/frontend/src/pages/IncidentsListPage.tsx b/frontend/src/pages/IncidentsListPage.tsx
index 10ad4d5..3c422bb 100644
--- a/frontend/src/pages/IncidentsListPage.tsx
+++ b/frontend/src/pages/IncidentsListPage.tsx
@@ -7,7 +7,6 @@ import { SkeletonTable } from '../components/ui/Skeleton'
import { EmptyIncidentsList } from '../components/ui/EmptyState'
import { GeneralError } from '../components/ui/ErrorState'
import { useIncidents } from '../hooks/useIncidents'
-import { listCustomFields, CustomFieldDefinition } from '../api/customFields'
import { Search, Plus, ChevronLeft, ChevronRight } from 'lucide-react'
import type { Incident } from '../api/types'
@@ -23,28 +22,17 @@ export function IncidentsListPage() {
const [searchQuery, setSearchQuery] = useState('')
const [currentPage, setCurrentPage] = useState(1)
const [showCreateModal, setShowCreateModal] = useState(false)
- const [customFieldDefs, setCustomFieldDefs] = useState
([])
- const [customFieldFilters, setCustomFieldFilters] = useState>({})
-
- useEffect(() => {
- listCustomFields().then(setCustomFieldDefs).catch(() => {})
- }, [])
// Reset to page 1 whenever server-side filters change
useEffect(() => {
setCurrentPage(1)
- }, [statusFilter, severityFilter, customFieldFilters])
-
- const activeCustomFieldFilters = Object.fromEntries(
- Object.entries(customFieldFilters).filter(([, v]) => v.trim() !== '')
- )
+ }, [statusFilter, severityFilter])
const { incidents, loading, error, total, refetch } = useIncidents({
status: statusFilter || undefined,
severity: severityFilter || undefined,
limit: PAGE_SIZE,
page: currentPage,
- customFields: Object.keys(activeCustomFieldFilters).length > 0 ? activeCustomFieldFilters : undefined,
})
// Client-side search filters the current page's results
@@ -122,32 +110,6 @@ export function IncidentsListPage() {
- {/* Custom field filters */}
- {customFieldDefs.map(def => (
- def.field_type === 'dropdown' ? (
-
- ) : (
- setCustomFieldFilters(prev => ({ ...prev, [def.key]: e.target.value }))}
- className="px-3 py-2 border border-border rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-brand-primary focus:border-transparent w-36"
- />
- )
- ))}
-
{/* Results Count */}
{loading ? '...' : searchQuery
@@ -169,7 +131,7 @@ export function IncidentsListPage() {
{!loading && !error && filteredIncidents.length === 0 && (
0)}
+ hasFilters={!!(statusFilter || severityFilter || searchQuery)}
/>
)}
diff --git a/frontend/src/pages/SettingsIncidentsPage.tsx b/frontend/src/pages/SettingsIncidentsPage.tsx
deleted file mode 100644
index c1c2b6d..0000000
--- a/frontend/src/pages/SettingsIncidentsPage.tsx
+++ /dev/null
@@ -1,372 +0,0 @@
-import { useState, useEffect } from 'react'
-import { Plus, Trash2, ChevronUp, ChevronDown, Pencil, X } from 'lucide-react'
-import { Button } from '../components/ui/Button'
-import {
- listCustomFields,
- createCustomField,
- updateCustomField,
- deleteCustomField,
- reorderCustomFields,
- CustomFieldDefinition,
- DropdownOption,
- FieldType,
-} from '../api/customFields'
-
-type FormState = {
- name: string
- key: string
- field_type: FieldType
- options: DropdownOption[]
- newOptionLabel: string
- newOptionValue: string
-}
-
-const emptyForm = (): FormState => ({
- name: '',
- key: '',
- field_type: 'string',
- options: [],
- newOptionLabel: '',
- newOptionValue: '',
-})
-
-function toSlug(name: string): string {
- return name
- .toLowerCase()
- .replace(/[^a-z0-9]+/g, '_')
- .replace(/^_+|_+$/g, '')
- .replace(/^([^a-z])/, '_$1')
-}
-
-export function SettingsIncidentsPage() {
- const [fields, setFields] = useState([])
- const [loading, setLoading] = useState(true)
- const [error, setError] = useState('')
- const [showForm, setShowForm] = useState(false)
- const [editingId, setEditingId] = useState(null)
- const [form, setForm] = useState(emptyForm())
- const [saving, setSaving] = useState(false)
- const [formError, setFormError] = useState('')
-
- useEffect(() => {
- load()
- }, [])
-
- async function load() {
- setLoading(true)
- try {
- setFields(await listCustomFields())
- setError('')
- } catch {
- setError('Failed to load custom fields')
- } finally {
- setLoading(false)
- }
- }
-
- function openCreate() {
- setEditingId(null)
- setForm(emptyForm())
- setFormError('')
- setShowForm(true)
- }
-
- function openEdit(f: CustomFieldDefinition) {
- setEditingId(f.id)
- setForm({
- name: f.name,
- key: f.key,
- field_type: f.field_type,
- options: f.options ?? [],
- newOptionLabel: '',
- newOptionValue: '',
- })
- setFormError('')
- setShowForm(true)
- }
-
- function cancelForm() {
- setShowForm(false)
- setEditingId(null)
- setForm(emptyForm())
- setFormError('')
- }
-
- function handleNameChange(name: string) {
- setForm(prev => ({
- ...prev,
- name,
- key: editingId ? prev.key : toSlug(name),
- }))
- }
-
- function addOption() {
- const label = form.newOptionLabel.trim()
- const value = form.newOptionValue.trim() || toSlug(label)
- if (!label || !value) return
- setForm(prev => ({
- ...prev,
- options: [...prev.options, { label, value }],
- newOptionLabel: '',
- newOptionValue: '',
- }))
- }
-
- function removeOption(idx: number) {
- setForm(prev => ({ ...prev, options: prev.options.filter((_, i) => i !== idx) }))
- }
-
- async function handleSave() {
- setFormError('')
- if (!form.name.trim()) { setFormError('Name is required'); return }
- if (!form.key.trim()) { setFormError('Key is required'); return }
- if (form.field_type === 'dropdown' && form.options.length === 0) {
- setFormError('Dropdown fields require at least one option')
- return
- }
-
- setSaving(true)
- try {
- const payload = {
- name: form.name.trim(),
- key: form.key.trim(),
- field_type: form.field_type,
- options: form.options,
- }
- if (editingId) {
- await updateCustomField(editingId, payload)
- } else {
- await createCustomField(payload)
- }
- await load()
- cancelForm()
- } catch (e: unknown) {
- const msg = e instanceof Error ? e.message : 'Failed to save'
- setFormError(msg.includes('409') || msg.includes('already exists') ? 'A field with that key already exists' : msg)
- } finally {
- setSaving(false)
- }
- }
-
- async function handleDelete(f: CustomFieldDefinition) {
- if (!confirm(`Delete field "${f.name}"? This cannot be undone.`)) return
- try {
- await deleteCustomField(f.id)
- await load()
- } catch (e: unknown) {
- const msg = e instanceof Error ? e.message : ''
- if (msg.includes('409') || msg.includes('in use')) {
- alert(`Cannot delete "${f.name}" — it has values on existing incidents.`)
- } else {
- alert('Failed to delete field')
- }
- }
- }
-
- async function move(idx: number, dir: -1 | 1) {
- const next = [...fields]
- const target = idx + dir
- if (target < 0 || target >= next.length) return
- const a = next[idx]!
- const b = next[target]!
- next[idx] = b
- next[target] = a
- setFields(next)
- await reorderCustomFields(next.map((f, i) => ({ id: f.id, order: i })))
- }
-
- return (
-
-
-
-
-
Incident Settings
-
Configure custom fields and incident schema
-
-
-
-
-
- {error && (
-
{error}
- )}
-
- {/* Custom Fields card */}
-
-
-
-
Custom Fields
-
- Define metadata fields that appear on every incident
-
-
- {!showForm && (
-
- )}
-
-
- {/* Inline form */}
- {showForm && (
-
-
- {editingId ? 'Edit field' : 'New field'}
-
- {formError && (
-
{formError}
- )}
-
-
-
- handleNameChange(e.target.value)}
- />
-
-
-
- setForm(prev => ({ ...prev, key: e.target.value }))}
- readOnly={!!editingId}
- />
-
-
-
-
-
-
-
- {form.field_type === 'dropdown' && (
-
-
- {form.options.map((opt, i) => (
-
- {opt.label}
- {opt.value}
-
-
- ))}
-
- setForm(prev => ({ ...prev, newOptionLabel: e.target.value }))}
- onKeyDown={e => e.key === 'Enter' && addOption()}
- />
- setForm(prev => ({ ...prev, newOptionValue: e.target.value }))}
- onKeyDown={e => e.key === 'Enter' && addOption()}
- />
-
-
-
- )}
-
-
-
-
-
-
- )}
-
- {/* Field list */}
- {loading ? (
-
Loading…
- ) : fields.length === 0 ? (
-
- No custom fields yet. Add one to start capturing extra metadata on incidents.
-
- ) : (
-
-
-
- |
- Name |
- Key |
- Type |
- Options |
- Actions |
-
-
-
- {fields.map((f, idx) => (
-
- |
-
-
-
-
- |
- {f.name} |
- {f.key} |
-
-
- {f.field_type === 'string' ? 'Text' : f.field_type === 'number' ? 'Number' : 'Dropdown'}
-
- |
-
- {f.field_type === 'dropdown' && f.options?.length > 0
- ? f.options.map(o => o.label).join(', ')
- : '—'}
- |
-
-
-
-
-
- |
-
- ))}
-
-
- )}
-
-
-
- )
-}