main
vue 207 lines 5.51 KB
Raw
1 <template>
2 <div class="flex flex-col gap-1">
3 <div class="flex items-end justify-between gap-3">
4 <p class="py-1 text-sm">
5 Showing
6 <span class="font-semibold">{{ events.length }}</span>
7 of
8 <span class="font-semibold">{{ totalEvents }}</span>
9 events
10 </p>
11
12 <div class="flex flex-wrap items-center gap-3">
13 <n-tooltip class="px-2! py-1! text-xs!">
14 <template #trigger>
15 <Icon :name="InfoIcon" :size="15" class="cursor-help" />
16 </template>
17 Click table row to view event details
18 </n-tooltip>
19 <n-button
20 text
21 :disabled="!eventSource"
22 title="Configure which columns to display for this event source"
23 @click="emit('configure-columns')"
24 >
25 <template #icon>
26 <Icon :name="SettingsIcon" :size="15" />
27 </template>
28 Columns
29 </n-button>
30 </div>
31 </div>
32
33 <n-data-table
34 :columns
35 :loading="loadingEvents"
36 :data="events"
37 size="small"
38 :scroll-x
39 :row-key="(row: EventSearchResult) => row._id || JSON.stringify(row)"
40 :row-props
41 class="[&_.n-data-table-th\_\_title]:whitespace-nowrap"
42 >
43 <template #empty>
44 <n-empty description="No events found">
45 <template #extra>Try adjusting your query or expanding the time range</template>
46 </n-empty>
47 </template>
48 </n-data-table>
49
50 <div v-if="scrollId && events.length < totalEvents" class="mt-3 flex justify-center">
51 <n-button :loading="loadingMore" @click="emit('load-more')">Load More</n-button>
52 </div>
53 </div>
54 </template>
55
56 <script setup lang="ts">
57 import type { DataTableColumns } from "naive-ui"
58 import type { EventSearchResult } from "@/types/events.d"
59 import type { DisplayColumn, EventSource } from "@/types/eventSources.d"
60 import { NButton, NDataTable, NEmpty, NTooltip } from "naive-ui"
61 import { computed, h } from "vue"
62 import Icon from "@/components/common/Icon.vue"
63
64 const props = defineProps<{
65 events: EventSearchResult[]
66 totalEvents: number
67 loadingEvents: boolean
68 loadingMore: boolean
69 hasSearched: boolean
70 scrollId: string | null
71 eventSource: EventSource | null
72 }>()
73
74 const emit = defineEmits<{
75 "load-more": []
76 "configure-columns": []
77 "row-select": [event: EventSearchResult]
78 }>()
79
80 const MIN_COLUMN_WIDTH = 120
81 const SettingsIcon = "carbon:settings"
82 const InfoIcon = "carbon:information"
83
84 function resolveColumnWidth(width?: number | null): number {
85 if (width == null || width < MIN_COLUMN_WIDTH) return MIN_COLUMN_WIDTH
86 return width
87 }
88
89 function normalizeColumns(cols: DataTableColumns<EventSearchResult>): DataTableColumns<EventSearchResult> {
90 return cols.map(col => ({
91 ...col,
92 width: resolveColumnWidth(typeof col.width === "number" ? col.width : undefined)
93 }))
94 }
95
96 const defaultColumns: DataTableColumns<EventSearchResult> = [
97 {
98 title: "Timestamp",
99 key: "timestamp",
100 width: 180,
101 sorter: (a, b) => {
102 const timeA = a.timestamp || a["@timestamp"] || ""
103 const timeB = b.timestamp || b["@timestamp"] || ""
104 return new Date(timeA).getTime() - new Date(timeB).getTime()
105 },
106 render(row) {
107 const ts = row.timestamp || row["@timestamp"]
108 if (!ts) return "-"
109 return new Date(ts).toLocaleString()
110 }
111 },
112 {
113 title: "Source",
114 key: "agent_name",
115 width: 140,
116 ellipsis: { tooltip: true },
117 render(row) {
118 return row.agent_name || row.source || "-"
119 }
120 },
121 {
122 title: "Rule",
123 key: "rule_description",
124 width: 200,
125 ellipsis: { tooltip: true },
126 render(row) {
127 return row.rule_description || row.rule_id || "-"
128 }
129 },
130 {
131 title: "Level",
132 key: "rule_level",
133 width: 80,
134 sorter: (a, b) => (Number(a.rule_level) || 0) - (Number(b.rule_level) || 0),
135 render(row) {
136 if (row.rule_level === undefined || row.rule_level === null) return "-"
137 const level = Number(row.rule_level)
138 let type: "default" | "warning" | "error" | "success" | "info" = "default"
139 if (level >= 12) type = "error"
140 else if (level >= 8) type = "warning"
141 else if (level >= 4) type = "info"
142 return h("span", { class: `level-${type}` }, String(row.rule_level))
143 }
144 },
145 {
146 title: "Summary",
147 key: "full_log",
148 width: 320,
149 ellipsis: { tooltip: true },
150 render(row) {
151 return row.full_log || row.data || row.message || "-"
152 }
153 }
154 ]
155
156 function getNestedValue(obj: EventSearchResult, path: string): unknown {
157 return path.split(".").reduce<unknown>((acc, segment) => {
158 if (acc && typeof acc === "object") {
159 return (acc as Record<string, unknown>)[segment]
160 }
161 return undefined
162 }, obj)
163 }
164
165 function formatCellValue(val: unknown): string {
166 if (val === undefined || val === null || val === "") return "-"
167 if (Array.isArray(val)) return val.map(v => (v === null || v === undefined ? "" : String(v))).join(", ")
168 if (typeof val === "object") return JSON.stringify(val)
169 return String(val)
170 }
171
172 function buildColumnFromConfig(col: DisplayColumn): DataTableColumns<EventSearchResult>[number] {
173 return {
174 title: col.label || col.key,
175 key: col.key,
176 width: resolveColumnWidth(col.width),
177 ellipsis: { tooltip: true },
178 render(row: EventSearchResult) {
179 return formatCellValue(getNestedValue(row, col.key))
180 }
181 }
182 }
183
184 const columns = computed<DataTableColumns<EventSearchResult>>(() => {
185 const configured = props.eventSource?.displayed_columns
186 if (configured && configured.length > 0) {
187 return normalizeColumns(configured.map(buildColumnFromConfig))
188 }
189 return normalizeColumns(defaultColumns)
190 })
191
192 const scrollX = computed(() =>
193 columns.value.reduce(
194 (sum, col) => sum + resolveColumnWidth(typeof col.width === "number" ? col.width : undefined),
195 0
196 )
197 )
198
199 function rowProps(row: EventSearchResult) {
200 return {
201 style: "cursor: pointer",
202 onClick: () => {
203 emit("row-select", row)
204 }
205 }
206 }
207 </script>