@cryptotaxi247 / CoPilot / commits / d4ecd720

feat(event-sources): customer-portal renders configured columns (issue #833 slice 3/3) (#870)

Last slice of issue #833. Customer portal now mirrors the SOC portal's column-render path (read-only) — end customers see whatever the SOC admin configured per event source via slice 2 (#869), with a graceful fallback to the prior hardcoded 5-column layout when nothing is configured. No config UI on the customer portal by design — SOC admins are the only ones who set columns, customers just consume them. Slice 1's GET /siem/event_sources/{customer_code} already returns displayed_columns since #868. Changes: - types/siem.ts: new DisplayColumn interface; EventSourceItem.displayed_columns is now DisplayColumn[] | null. - components/eventSearch/List.vue: - Resolve selectedEventSource by looking up the picked sourceName in searchFormLoad.eventSources (no SearchForm.vue plumbing needed). - Add getNestedValue / formatCellValue helpers — same dotted-path walker + array/object formatting as the SOC portal. - Hardcoded data columns extracted into defaultColumns; the View Details action is split out as actionsColumn so it always pins to the right edge regardless of which data columns precede it. - The active `columns` computed picks configured layout when present, defaults otherwise, then appends the actions column. Co-authored-by: taylor_socfortress <taylor.walton@socfortress.co> Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

taylorcopilot committed May 9, 2026 at 12:44 UTC d4ecd720395106edd7723ab891bf61125a264dc9
2 files changed +75 -21
customer-portal/src/components/eventSearch/List.vue
+65 -21
@@ -57,10 +57,10 @@
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,7 +95,43 @@ function selectEvent(event: EventSearchResult) {
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",
@@ -124,27 +160,35 @@ const columns = computed<DataTableColumns<EventSearchResult>>(() => [
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()
customer-portal/src/types/siem.ts
+10
@@ -1,3 +1,12 @@
1 +export interface DisplayColumn {
2 + /** Field path in the event _source object (dotted, e.g. "agent.name"). */
3 + key: string
4 + /** Human-readable column header. */
5 + label: string
6 + /** Optional pixel width hint. */
7 + width?: number | null
8 +}
9 +
10 export interface EventSourceItem {
11 id: number
12 customer_code: string
@@ -6,6 +15,7 @@ export interface EventSourceItem {
15 event_type: string
16 time_field: string
17 enabled: boolean
18 + displayed_columns?: DisplayColumn[] | null
19 created_at: string
20 updated_at: string
21 }