+ size="small"
+ rowKey="name"
+ pagination={false}
+ dataSource={module.stream_health.groups}
+ columns={[
+ { title: 'Group', dataIndex: 'name' },
+ { title: 'Consumers', dataIndex: 'consumers', width: 120 },
+ { title: 'Pending', dataIndex: 'pending', width: 120 },
+ { title: 'Last delivered', dataIndex: 'last_delivered_id', width: 220 },
+ ]}
+ />
+ ) : null}
+
+
+ Recent messages
+
+ setMessageLimit(Number(value || 5))} />
+
+
+
+ {messageError ? : null}
+
}}
+ renderItem={(item) => (
+
+
+ {item.message_id}
+
+
+
+ )}
+ />
+
+
+ Read by message ID
+ setMessageId(event.target.value)}
+ onSearch={loadById}
+ style={{ marginTop: 8 }}
+ placeholder="1700000000000-0"
+ />
+
+ {selectedMessage ? : null}
+
+ ) : null}
+
+ )
+}
+
+function ModulesTab() {
+ const { result, loading, refreshing, load, refresh } = useDefinitionSection('/custom/modules/', 'Modules')
+ const [search, setSearch] = useState('')
+ const [selected, setSelected] = useState(null)
+ const rows = useMemo(() => (
+ (result?.items || []).filter((item) => includesSearch([
+ item.name,
+ item.description,
+ item.stream_name,
+ item.path,
+ ], search))
+ ), [result?.items, search])
+
+ const columns = useMemo>(() => [
+ { title: 'Module', dataIndex: 'name', width: 340, render: (name: string) => {name} },
+ { title: 'Description', dataIndex: 'description', ellipsis: true },
+ { title: 'Stream', dataIndex: 'stream_name', width: 380, ellipsis: true },
+ { title: 'Threads', dataIndex: 'thread_num', width: 100 },
+ { title: 'Length', width: 100, render: (_, record) => record.stream_health.length },
+ { title: 'Last ID', width: 180, render: (_, record) => record.stream_health.last_id || '—' },
+ { title: 'Path', dataIndex: 'path', width: 280, render: (path: string) => },
+ ], [])
+
+ return (
+ <>
+
+
+
+ size="small"
+ rowKey="path"
+ loading={loading}
+ columns={columns}
+ dataSource={rows}
+ onRow={(record) => ({ onClick: () => setSelected(record), style: { cursor: 'pointer' } })}
+ locale={{ emptyText: loading ? : }}
+ scroll={{ x: 1600 }}
+ />
+ setSelected(null)} />
+ >
+ )
+}
+
+function PlaybooksTab() {
+ const { result, loading, refreshing, load, refresh } = useDefinitionSection('/custom/playbooks/', 'Playbooks')
+ const [source, setSource] = useState('all')
+ const [search, setSearch] = useState('')
+ const [selected, setSelected] = useState(null)
+ const rows = useMemo(() => (
+ filterBySource(result?.items || [], source).filter((item) => includesSearch([
+ item.name,
+ item.description,
+ item.path,
+ ...item.tags,
+ ], search))
+ ), [result?.items, search, source])
+
+ const columns = useMemo>(() => [
+ { title: 'Playbook', dataIndex: 'name', width: 260, render: (name: string) => {name} },
+ { title: 'Source', dataIndex: 'source', width: 110, render: (value: SourceType) => },
+ {
+ title: 'Tags',
+ dataIndex: 'tags',
+ width: 260,
+ render: (tags: string[]) => (
+
+ {(tags || []).map((tag) => {tag})}
+
+ ),
+ },
+ { title: 'Description', dataIndex: 'description', ellipsis: true },
+ { title: 'Path', dataIndex: 'path', width: 300, render: (path: string) => },
+ ], [])
+
+ return (
+ <>
+
+
+
+ size="small"
+ rowKey="path"
+ loading={loading}
+ columns={columns}
+ dataSource={rows}
+ onRow={(record) => ({ onClick: () => setSelected(record), style: { cursor: 'pointer' } })}
+ locale={{ emptyText: loading ? : }}
+ scroll={{ x: 1300 }}
+ />
+ setSelected(null)} size="min(760px, calc(100vw - 48px))">
+ {selected ? (
+
+
+
+ {(selected.tags || []).map((tag) => {tag})}
+
+ {selected.description || 'No description.'}
+ },
+ ]} />
+
+ ) : null}
+
+ >
+ )
+}
+
+function SiemTab() {
+ const { result, loading, refreshing, load, refresh } = useDefinitionSection('/custom/siem/', 'SIEM YAML')
+ const [backend, setBackend] = useState<'all' | 'ELK' | 'Splunk'>('all')
+ const [search, setSearch] = useState('')
+ const [selected, setSelected] = useState(null)
+ const rows = useMemo(() => (
+ (result?.items || [])
+ .filter((item) => backend === 'all' || item.backend === backend)
+ .filter((item) => includesSearch([item.name, item.backend, item.description, item.path], search))
+ ), [backend, result?.items, search])
+
+ const columns = useMemo>(() => [
+ { title: 'Index', dataIndex: 'name', width: 220, render: (name: string) => {name} },
+ { title: 'Backend', dataIndex: 'backend', width: 120, render: (value: string) => {value} },
+ { title: 'Description', dataIndex: 'description', ellipsis: true },
+ { title: 'Fields', dataIndex: 'field_count', width: 100 },
+ { title: 'Key fields', dataIndex: 'key_field_count', width: 120 },
+ { title: 'Path', dataIndex: 'path', width: 300, render: (path: string) => },
+ ], [])
+
+ const fieldColumns = useMemo>(() => [
+ { title: 'Name', dataIndex: 'name', width: 220 },
+ { title: 'Type', dataIndex: 'type', width: 120 },
+ { title: 'Key field', dataIndex: 'is_key_field', width: 110, render: (value: boolean) => value ? Key field : '—' },
+ { title: 'Description', dataIndex: 'description' },
+ {
+ title: 'Sample values',
+ dataIndex: 'sample_values',
+ width: 260,
+ render: (values: unknown[]) => (
+
+ {JSON.stringify(values || [])}
+
+ ),
+ },
+ ], [])
+
+ return (
+ <>
+
+ value={backend}
+ onChange={(value) => setBackend(value)}
+ style={{ width: 140 }}
+ options={[
+ { label: 'All backends', value: 'all' },
+ { label: 'ELK', value: 'ELK' },
+ { label: 'Splunk', value: 'Splunk' },
+ ]}
+ />
+ )}
+ />
+
+
+ size="small"
+ rowKey="path"
+ loading={loading}
+ columns={columns}
+ dataSource={rows}
+ onRow={(record) => ({ onClick: () => setSelected(record), style: { cursor: 'pointer' } })}
+ locale={{ emptyText: loading ? : }}
+ scroll={{ x: 1400 }}
+ />
+ setSelected(null)} size="min(1080px, calc(100vw - 48px))">
+ {selected ? (
+
+ {selected.backend} },
+ { key: 'description', label: 'Description', children: selected.description || '—' },
+ { key: 'path', label: 'Path', children: },
+ ]} />
+
+ size="small"
+ rowKey="name"
+ columns={fieldColumns}
+ dataSource={selected.fields || []}
+ pagination={false}
+ scroll={{ x: 900, y: 520 }}
+ />
+
+ ) : null}
+
+ >
+ )
+}
+
+export default function CustomDefinitions() {
+ return (
+
+
Modules, children: },
+ { key: 'playbooks', label: Playbooks, children: },
+ { key: 'siem', label: SIEM YAML, children: },
+ ]}
+ />
+
+ )
+}
diff --git a/frontend/src/pages/RuntimeSettings.tsx b/frontend/src/pages/RuntimeSettings.tsx
index 40896b9..05b7d9b 100644
--- a/frontend/src/pages/RuntimeSettings.tsx
+++ b/frontend/src/pages/RuntimeSettings.tsx
@@ -1,5 +1,5 @@
import {useCallback, useEffect, useState} from 'react'
-import {Alert, Button, Card, Col, Divider, Form, InputNumber, List, message, Row, Select, Space, Tag, Tooltip, Typography} from 'antd'
+import {Button, Card, Col, Divider, Form, InputNumber, message, Row, Select, Space, Tooltip, Typography} from 'antd'
import {QuestionCircleOutlined} from '@ant-design/icons'
import client from '../api/client'
@@ -9,43 +9,6 @@ interface RuntimeConfig {
updated_at?: string
}
-interface CustomDefinitionItem {
- name?: string
- source: 'official' | 'custom'
- path: string
- stream_name?: string
- backend?: string
- description?: string
- playbook?: string
- prompt?: string
- language?: string
-}
-
-interface CustomDefinitionError {
- path: string
- error: string
-}
-
-interface CustomDefinitionSection {
- items: CustomDefinitionItem[]
- errors: CustomDefinitionError[]
-}
-
-interface CustomDefinitionRefreshResult {
- success: boolean
- counts: {
- modules: number
- playbooks: number
- siem: number
- prompts: number
- errors: number
- }
- modules: CustomDefinitionSection
- playbooks: CustomDefinitionSection
- siem: CustomDefinitionSection
- prompts: CustomDefinitionSection
-}
-
function initialValues(): RuntimeConfig {
return {
prompt_language: 'en',
@@ -77,50 +40,10 @@ function helpLabel(label: string, help: string) {
)
}
-function CustomDefinitionSectionView({ title, section }: { title: string; section: CustomDefinitionSection }) {
- return (
-
-
{title}
-
(
-
-
-
- {item.name || item.playbook || item.prompt}
- {item.source}
- {item.stream_name ? {item.stream_name} : null}
- {item.backend ? {item.backend} : null}
- {item.prompt ? {item.prompt} : null}
- {item.language ? {item.language} : null}
-
- {item.path}
-
-
- )}
- />
- {section.errors.map((error) => (
-
- ))}
-
- )
-}
-
export default function RuntimeSettings() {
const [form] = Form.useForm()
const [loading, setLoading] = useState(false)
const [saving, setSaving] = useState(false)
- const [refreshingDefinitions, setRefreshingDefinitions] = useState(false)
- const [definitionResult, setDefinitionResult] = useState(null)
const loadConfig = useCallback(async () => {
setLoading(true)
@@ -153,23 +76,6 @@ export default function RuntimeSettings() {
}
}
- const refreshDefinitions = async () => {
- setRefreshingDefinitions(true)
- try {
- const { data } = await client.post('/settings/runtime/custom-definitions/refresh/')
- setDefinitionResult(data)
- if (data.success) {
- message.success('Custom definitions refreshed')
- } else {
- message.warning(`Custom definitions refreshed with ${data.counts.errors} error(s)`)
- }
- } catch (error: unknown) {
- message.error(apiErrorMessage(error, 'Failed to refresh custom definitions'))
- } finally {
- setRefreshingDefinitions(false)
- }
- }
-
return (
@@ -207,44 +113,6 @@ export default function RuntimeSettings() {
-
- Custom Definitions
-
-
- Refresh and validate Module, Playbook, and SIEM YAML definitions after updating files in the custom directory.
- Dependency or helper module changes still require reinstalling custom packages and restarting related containers.
-
-
-
-
- {definitionResult ? (
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- ) : null}