57
</template>
58
59
<script setup lang="tsx">
60
-import type { DataTableColumns, TagProps } from "naive-ui"
60
+import type { DataTableColumn, DataTableColumns, TagProps } from "naive-ui"
61
import type { SearchFormLoad, SearchFormParams } from "@/components/eventSearch/SearchForm.vue"
62
import type { ApiError } from "@/types/common"
63
-import type { EventSearchQueryParams, EventSearchResult } from "@/types/siem"
63
+import type { DisplayColumn, EventSearchQueryParams, EventSearchResult, EventSourceItem } from "@/types/siem"
64
import { useElementSize } from "@vueuse/core"
65
import { NAlert, NButton, NDataTable, NEmpty, useMessage } from "naive-ui"
66
import { computed, ref, useTemplateRef } from "vue"
95
const { width: headerWidthRef } = useElementSize(useTemplateRef("headerRef"))
96
const simpleMode = computed(() => headerWidthRef.value < 600)
97
98
-const columns = computed<DataTableColumns<EventSearchResult>>(() => [
98
+const selectedEventSource = computed<EventSourceItem | null>(() => {
99
+ const sourceName = searchFormParams.value?.sourceName
100
+ const sources = searchFormLoad.value?.eventSources
101
+ if (!sourceName || !sources) return null
102
+ return sources.find(s => s.name === sourceName) ?? null
103
+})
104
+
105
+/** Walk a dotted path (e.g. "agent.name") through a nested object. */
106
+function getNestedValue(obj: EventSearchResult, path: string): unknown {
107
+ return path.split(".").reduce<unknown>((acc, segment) => {
108
+ if (acc && typeof acc === "object") {
109
+ return (acc as Record<string, unknown>)[segment]
110
+ }
111
+ return undefined
112
+ }, obj)
113
+}
114
+
115
+function formatCellValue(val: unknown): string {
116
+ if (val === undefined || val === null || val === "") return "-"
117
+ if (Array.isArray(val)) return val.map(v => (v === null || v === undefined ? "" : String(v))).join(", ")
118
+ if (typeof val === "object") return JSON.stringify(val)
119
+ return String(val)
120
+}
121
+
122
+function buildColumnFromConfig(col: DisplayColumn): DataTableColumn<EventSearchResult> {
123
+ return {
124
+ title: col.label || col.key,
125
+ key: col.key,
126
+ width: col.width || undefined,
127
+ ellipsis: { tooltip: true },
128
+ render: row => <div>{formatCellValue(getNestedValue(row, col.key))}</div>
129
+ }
130
+}
131
+
132
+// Defaults preserved from the original hardcoded layout so behaviour is unchanged
133
+// for event sources that haven't been configured yet.
134
+const defaultColumns = computed<DataTableColumn<EventSearchResult>[]>(() => [
135
{
136
title: "Timestamp",
137
key: "Timestamp",
160
title: "Rule",
161
key: "Rule",
162
render: row => <div>{row.rule_description || row.rule?.description || "-"}</div>
127
- },
128
- {
129
- title: "Actions",
130
- key: "actions",
131
- width: 150,
132
- fixed: simpleMode.value ? undefined : "right",
133
- render: row => {
134
- return (
135
- <NButton
136
- onClick={() => selectEvent(row)}
137
- v-slots={{
138
- icon: () => <Icon name="carbon:view" />
139
- }}
140
- >
141
- View Details
142
- </NButton>
143
- )
144
- }
163
}
164
])
165
166
+const actionsColumn = computed<DataTableColumn<EventSearchResult>>(() => ({
167
+ title: "Actions",
168
+ key: "actions",
169
+ width: 150,
170
+ fixed: simpleMode.value ? undefined : "right",
171
+ render: row => (
172
+ <NButton
173
+ onClick={() => selectEvent(row)}
174
+ v-slots={{
175
+ icon: () => <Icon name="carbon:view" />
176
+ }}
177
+ >
178
+ View Details
179
+ </NButton>
180
+ )
181
+}))
182
+
183
+const columns = computed<DataTableColumns<EventSearchResult>>(() => {
184
+ const configured = selectedEventSource.value?.displayed_columns
185
+ const dataColumns =
186
+ configured && configured.length > 0 ? configured.map(buildColumnFromConfig) : defaultColumns.value
187
+ // Always keep the View Details action at the right edge — it's the only way
188
+ // to open the event drawer from the table.
189
+ return [...dataColumns, actionsColumn.value]
190
+})
191
+
192
function handleSearchFormSearch(params: SearchFormParams) {
193
searchFormParams.value = params
194
searchEvents()