diff --git a/config/sample-config.yaml b/config/sample-config.yaml index 624d4742f..f6ce35d40 100644 --- a/config/sample-config.yaml +++ b/config/sample-config.yaml @@ -192,6 +192,7 @@ frontend: docURL: http://kubernetes.io/docs/user-guide/identifiers#names field: SrcK8S_Name filter: src_name + calculated: kubeObject(SrcK8S_Type,SrcK8S_Namespace,SrcK8S_Name,0) default: true width: 15 - id: SrcK8S_Type @@ -213,6 +214,7 @@ frontend: docURL: https://kubernetes.io/docs/concepts/overview/working-with-objects/owners-dependents/ field: SrcK8S_OwnerName filter: src_owner_name + calculated: kubeObject(SrcK8S_OwnerType,SrcK8S_Namespace,SrcK8S_OwnerName,0) default: false width: 15 - id: SrcK8S_OwnerType @@ -236,6 +238,7 @@ frontend: docURL: http://kubernetes.io/docs/user-guide/identifiers#namespaces field: SrcK8S_Namespace filter: src_namespace + calculated: kubeObject('Namespace','',SrcK8S_Namespace,0) default: true width: 15 - id: SrcAddr @@ -277,24 +280,25 @@ frontend: docURL: https://kubernetes.io/docs/concepts/architecture/nodes/ field: SrcK8S_HostName filter: src_host_name + calculated: kubeObject('Node','',SrcK8S_HostName,0) default: false width: 15 - id: SrcK8S_Object group: Source name: Kubernetes Object - calculated: getConcatenatedValue(SrcAddr,SrcPort,SrcK8S_Type,SrcK8S_Namespace,SrcK8S_Name) + calculated: kubeObject(SrcK8S_Type,SrcK8S_Namespace,SrcK8S_Name,1) or concat(SrcAddr,':',SrcPort) default: false width: 15 - id: SrcK8S_OwnerObject group: Source name: Owner Kubernetes Object - calculated: getConcatenatedValue(SrcAddr,SrcPort,SrcK8S_OwnerType,SrcK8S_Namespace,SrcK8S_OwnerName) + calculated: kubeObject(SrcK8S_OwnerType,SrcK8S_Namespace,SrcK8S_OwnerName,1) default: false width: 15 - id: SrcAddrPort group: Source name: IP & Port - calculated: getConcatenatedValue(SrcAddr,SrcPort) + calculated: concat(SrcAddr,':',SrcPort) default: false width: 15 - id: SrcZone @@ -312,6 +316,7 @@ frontend: docURL: http://kubernetes.io/docs/user-guide/identifiers#names field: DstK8S_Name filter: dst_name + calculated: kubeObject(DstK8S_Type,DstK8S_Namespace,DstK8S_Name,0) default: true width: 15 - id: DstK8S_Type @@ -333,6 +338,7 @@ frontend: docURL: https://kubernetes.io/docs/concepts/overview/working-with-objects/owners-dependents/ field: DstK8S_OwnerName filter: dst_owner_name + calculated: kubeObject(DstK8S_OwnerType,DstK8S_Namespace,DstK8S_OwnerName,0) default: false width: 15 - id: DstK8S_OwnerType @@ -356,6 +362,7 @@ frontend: docURL: http://kubernetes.io/docs/user-guide/identifiers#namespaces field: DstK8S_Namespace filter: dst_namespace + calculated: kubeObject('Namespace','',DstK8S_Namespace,0) default: true width: 15 - id: DstAddr @@ -397,24 +404,25 @@ frontend: docURL: https://kubernetes.io/docs/concepts/architecture/nodes/ field: DstK8S_HostName filter: dst_host_name + calculated: kubeObject('Node','',DstK8S_HostName,0) default: false width: 15 - id: DstK8S_Object group: Destination name: Kubernetes Object - calculated: getConcatenatedValue(DstAddr,DstPort,DstK8S_Type,DstK8S_Namespace,DstK8S_Name) + calculated: kubeObject(DstK8S_Type,DstK8S_Namespace,DstK8S_Name,1) or concat(DstAddr,':',DstPort) default: false width: 15 - id: DstK8S_OwnerObject group: Destination name: Owner Kubernetes Object - calculated: getConcatenatedValue(DstAddr,DstPort,DstK8S_OwnerType,DstK8S_Namespace,DstK8S_OwnerName) + calculated: kubeObject(DstK8S_OwnerType,DstK8S_Namespace,DstK8S_OwnerName,1) default: false width: 15 - id: DstAddrPort group: Destination name: IP & Port - calculated: getConcatenatedValue(DstAddr,DstPort) + calculated: concat(DstAddr,':',DstPort) default: false width: 15 - id: DstZone @@ -427,52 +435,52 @@ frontend: feature: zones - id: K8S_Name name: Names - calculated: getSrcOrDstValue(SrcK8S_Name,DstK8S_Name) + calculated: '[SrcK8S_Name,DstK8S_Name]' default: false width: 15 - id: K8S_Type name: Kinds - calculated: getSrcOrDstValue(SrcK8S_Type,DstK8S_Type) + calculated: '[SrcK8S_Type,DstK8S_Type]' default: false width: 10 - id: K8S_OwnerName name: Owners - calculated: getSrcOrDstValue(SrcK8S_OwnerName,DstK8S_OwnerName) + calculated: '[SrcK8S_OwnerName,DstK8S_OwnerName]' default: false width: 15 - id: K8S_OwnerType name: Owner Kinds - calculated: getSrcOrDstValue(SrcK8S_OwnerType,DstK8S_OwnerType) + calculated: '[SrcK8S_OwnerType,DstK8S_OwnerType]' default: false width: 10 - id: K8S_Namespace name: Namespaces - calculated: getSrcOrDstValue(SrcK8S_Namespace,DstK8S_Namespace) + calculated: '[SrcK8S_Namespace,DstK8S_Namespace]' default: false width: 15 - id: Addr name: IP - calculated: getSrcOrDstValue(SrcAddr,DstAddr) + calculated: '[SrcAddr,DstAddr]' default: false width: 10 - id: Port name: Ports - calculated: getSrcOrDstValue(SrcPort,DstPort) + calculated: '[SrcPort,DstPort]' default: false width: 10 - id: Mac name: MAC - calculated: getSrcOrDstValue(SrcMac,DstMac) + calculated: '[SrcMac,DstMac]' default: false width: 10 - id: K8S_HostIP name: Node IP - calculated: getSrcOrDstValue(SrcK8S_HostIP,DstK8S_HostIP) + calculated: '[SrcK8S_HostIP,DstK8S_HostIP]' default: false width: 10 - id: K8S_HostName name: Node Name - calculated: getSrcOrDstValue(SrcK8S_HostName,DstK8S_HostName) + calculated: '[SrcK8S_HostName,DstK8S_HostName]' default: false width: 15 - id: K8S_Object @@ -581,7 +589,7 @@ frontend: - id: CollectionTime name: Collection Time tooltip: Reception time of the record by the collector. - calculated: multiply(TimeReceived,1000), + calculated: multiply(TimeReceived,1000) field: TimeReceived default: false width: 15 diff --git a/web/src/components/__tests-data__/columns.ts b/web/src/components/__tests-data__/columns.ts index a0a99bd0a..e4886ff9f 100644 --- a/web/src/components/__tests-data__/columns.ts +++ b/web/src/components/__tests-data__/columns.ts @@ -45,6 +45,7 @@ export const ColumnConfigSampleDefs = [ docURL: 'http://kubernetes.io/docs/user-guide/identifiers#names', field: 'SrcK8S_Name', filter: 'src_name', + calculated: 'kubeObject(SrcK8S_Type,SrcK8S_Namespace,SrcK8S_Name,0)', default: true, width: 15 }, @@ -66,6 +67,7 @@ export const ColumnConfigSampleDefs = [ docURL: 'https://kubernetes.io/docs/concepts/overview/working-with-objects/owners-dependents/', field: 'SrcK8S_OwnerName', filter: 'src_owner_name', + calculated: 'kubeObject(SrcK8S_OwnerType,SrcK8S_Namespace,SrcK8S_OwnerName,0)', default: false, width: 15 }, @@ -88,6 +90,7 @@ export const ColumnConfigSampleDefs = [ docURL: 'http://kubernetes.io/docs/user-guide/identifiers#namespaces', field: 'SrcK8S_Namespace', filter: 'src_namespace', + calculated: `kubeObject('Namespace','',SrcK8S_Namespace,0)`, default: true, width: 15 }, @@ -139,6 +142,7 @@ export const ColumnConfigSampleDefs = [ docURL: 'https://kubernetes.io/docs/concepts/architecture/nodes/', field: 'SrcK8S_HostName', filter: 'src_host_name', + calculated: `kubeObject('Node','',SrcK8S_HostName,0)`, default: false, width: 15 }, @@ -146,6 +150,7 @@ export const ColumnConfigSampleDefs = [ id: 'SrcK8S_Object', group: 'Source', name: 'Kubernetes Object', + calculated: `kubeObject(SrcK8S_Type,SrcK8S_Namespace,SrcK8S_Name,1) or concat(SrcAddr,':',SrcPort)`, default: false, width: 15 }, @@ -153,6 +158,7 @@ export const ColumnConfigSampleDefs = [ id: 'SrcK8S_OwnerObject', group: 'Source', name: 'Owner Kubernetes Object', + calculated: `kubeObject(SrcK8S_OwnerType,SrcK8S_Namespace,SrcK8S_OwnerName,1)`, default: false, width: 15 }, @@ -160,6 +166,7 @@ export const ColumnConfigSampleDefs = [ id: 'SrcAddrPort', group: 'Source', name: 'IP & Port', + calculated: `concat(SrcAddr,':',SrcPort)`, default: false, width: 15 }, @@ -171,6 +178,7 @@ export const ColumnConfigSampleDefs = [ docURL: 'http://kubernetes.io/docs/user-guide/identifiers#names', field: 'DstK8S_Name', filter: 'dst_name', + calculated: `kubeObject(DstK8S_Type,DstK8S_Namespace,DstK8S_Name,0)`, default: true, width: 15 }, @@ -192,6 +200,7 @@ export const ColumnConfigSampleDefs = [ docURL: 'https://kubernetes.io/docs/concepts/overview/working-with-objects/owners-dependents/', field: 'DstK8S_OwnerName', filter: 'dst_owner_name', + calculated: `kubeObject(DstK8S_OwnerType,DstK8S_Namespace,DstK8S_OwnerName,0)`, default: false, width: 15 }, @@ -214,6 +223,7 @@ export const ColumnConfigSampleDefs = [ docURL: 'http://kubernetes.io/docs/user-guide/identifiers#namespaces', field: 'DstK8S_Namespace', filter: 'dst_namespace', + calculated: `kubeObject('Namespace','',DstK8S_Namespace,0)`, default: true, width: 15 }, @@ -265,6 +275,7 @@ export const ColumnConfigSampleDefs = [ docURL: 'https://kubernetes.io/docs/concepts/architecture/nodes/', field: 'DstK8S_HostName', filter: 'dst_host_name', + calculated: `kubeObject('Node','',DstK8S_HostName,0)`, default: false, width: 15 }, @@ -272,6 +283,7 @@ export const ColumnConfigSampleDefs = [ id: 'DstK8S_Object', group: 'Destination', name: 'Kubernetes Object', + calculated: `kubeObject(DstK8S_Type,DstK8S_Namespace,DstK8S_Name,1) or concat(DstAddr,':',DstPort)`, default: false, width: 15 }, @@ -279,6 +291,7 @@ export const ColumnConfigSampleDefs = [ id: 'DstK8S_OwnerObject', group: 'Destination', name: 'Owner Kubernetes Object', + calculated: `kubeObject(DstK8S_OwnerType,DstK8S_Namespace,DstK8S_OwnerName,1)`, default: false, width: 15 }, @@ -286,84 +299,98 @@ export const ColumnConfigSampleDefs = [ id: 'DstAddrPort', group: 'Destination', name: 'IP & Port', + calculated: `concat(DstAddr,':',DstPort)`, default: false, width: 15 }, { id: 'K8S_Name', name: 'Names', + calculated: '[SrcK8S_Name,DstK8S_Name]', default: false, width: 15 }, { id: 'K8S_Type', name: 'Kinds', + calculated: '[SrcK8S_Type,DstK8S_Type]', default: false, width: 10 }, { id: 'K8S_OwnerName', name: 'Owners', + calculated: '[SrcK8S_OwnerName,DstK8S_OwnerName]', default: false, width: 15 }, { id: 'K8S_OwnerType', name: 'Owner Kinds', + calculated: '[SrcK8S_OwnerType,DstK8S_OwnerType]', default: false, width: 10 }, { id: 'K8S_Namespace', name: 'Namespaces', + calculated: '[SrcK8S_Namespace,DstK8S_Namespace]', default: false, width: 15 }, { id: 'Addr', name: 'IP', + calculated: '[SrcAddr,DstAddr]', default: false, width: 10 }, { id: 'Port', name: 'Ports', + calculated: '[SrcPort,DstPort]', default: false, width: 10 }, { id: 'Mac', name: 'MAC', + calculated: '[SrcMac,DstMac]', default: false, width: 10 }, { id: 'K8S_HostIP', name: 'Node IP', + calculated: '[SrcK8S_HostIP,DstK8S_HostIP]', default: false, width: 10 }, { id: 'K8S_HostName', name: 'Node Name', + calculated: '[SrcK8S_HostName,DstK8S_HostName]', default: false, width: 15 }, { id: 'K8S_Object', name: 'Kubernetes Objects', + calculated: '[column.SrcK8S_Object,column.DstK8S_Object]', default: false, width: 15 }, { id: 'K8S_OwnerObject', name: 'Owner Kubernetes Objects', + calculated: '[column.SrcK8S_OwnerObject,column.DstK8S_OwnerObject]', default: false, width: 15 }, { id: 'AddrPort', name: 'IPs & Ports', + calculated: '[column.SrcAddrPort,column.DstAddrPort]', default: false, width: 15 }, @@ -470,6 +497,26 @@ export const ColumnConfigSampleDefs = [ filter: 'dns_flag_response_code', default: false, width: 5 + }, + { + id: 'IcmpType', + group: 'ICMP', + name: 'Type', + tooltip: 'The type of the ICMP message.', + field: 'IcmpType', + filter: 'icmp_type', + default: false, + width: 10 + }, + { + id: 'IcmpCode', + group: 'ICMP', + name: 'Code', + tooltip: 'The code of the ICMP message.', + field: 'IcmpCode', + filter: 'icmp_code', + default: false, + width: 10 } ]; diff --git a/web/src/components/__tests-data__/flows.ts b/web/src/components/__tests-data__/flows.ts index 7878f679c..61650d70f 100644 --- a/web/src/components/__tests-data__/flows.ts +++ b/web/src/components/__tests-data__/flows.ts @@ -37,7 +37,9 @@ export const FlowsSample: Record[] = [ _RecordType: 'flowLog', FlowDirection: FlowDirection.Egress, SrcK8S_Namespace: 'default', - DstK8S_Namespace: 'default' + DstK8S_Namespace: 'default', + SrcK8S_Type: 'Pod', + DstK8S_Type: 'Pod' }, key: 1, fields: { diff --git a/web/src/components/drawer/record/__tests__/record-field.spec.tsx b/web/src/components/drawer/record/__tests__/record-field.spec.tsx index 13ade4d2c..bc0c1f874 100644 --- a/web/src/components/drawer/record/__tests__/record-field.spec.tsx +++ b/web/src/components/drawer/record/__tests__/record-field.spec.tsx @@ -47,7 +47,7 @@ describe('', () => { name: 'DNS Latency', isSelected: true, value: f => (f.fields.DnsLatencyMs === undefined ? Number.NaN : f.fields.DnsLatencyMs), - sort: (a, b, col) => compareNumbers(col.value(a) as number, col.value(b) as number), + sort: (a, b, col) => compareNumbers(col.value!(a) as number, col.value!(b) as number), width: 5 }} {...mocks} diff --git a/web/src/components/drawer/record/__tests__/record-panel.spec.tsx b/web/src/components/drawer/record/__tests__/record-panel.spec.tsx index 0106d3746..3b4366889 100644 --- a/web/src/components/drawer/record/__tests__/record-panel.spec.tsx +++ b/web/src/components/drawer/record/__tests__/record-panel.spec.tsx @@ -27,14 +27,17 @@ describe('', () => { expect(wrapper.find(RecordPanel)).toBeTruthy(); expect(wrapper.find('#record-panel-test')).toHaveLength(1); // all columns with data + JSON field - // sample contains 18 fields + // sample contains 20 fields // JSON tab represent 1 extra field - expect(wrapper.find('.record-field-container')).toHaveLength(18 + 1); - + expect(wrapper.find('.record-field-container')).toHaveLength(20 + 1); + // No ICMP + expect(wrapper.find({ 'data-test-id': 'drawer-field-IcmpType' })).toHaveLength(0); + expect(wrapper.find({ 'data-test-id': 'drawer-field-IcmpCode' })).toHaveLength(0); // same with 4 valid fields + json wrapper.setProps({ record: UnknownFlow }); expect(wrapper.find('.record-field-container')).toHaveLength(4 + 1); }); + it('should close on click', async () => { const wrapper = shallow(); const closeButton = wrapper.find(DrawerCloseButton); @@ -42,4 +45,19 @@ describe('', () => { closeButton.simulate('click'); expect(mocks.onClose).toHaveBeenCalled(); }); + + it('should render ICMP', async () => { + const flowWithICMP = { + ...mocks.record, + fields: { + ...mocks.record.fields, + IcmpType: 8, + IcmpCode: 0 + } + }; + const wrapper = shallow(); + expect(wrapper.find(RecordPanel)).toBeTruthy(); + expect(wrapper.find({ 'data-test-id': 'drawer-field-IcmpType' })).toHaveLength(1); + expect(wrapper.find({ 'data-test-id': 'drawer-field-IcmpCode' })).toHaveLength(1); + }); }); diff --git a/web/src/components/drawer/record/record-field.tsx b/web/src/components/drawer/record/record-field.tsx index aadb3b473..10c573c7c 100644 --- a/web/src/components/drawer/record/record-field.tsx +++ b/web/src/components/drawer/record/record-field.tsx @@ -5,19 +5,12 @@ import * as React from 'react'; import { useTranslation } from 'react-i18next'; import { Link } from 'react-router-dom'; import { FlowDirection, getDirectionDisplayString, Record } from '../../../api/ipfix'; -import { Column, ColumnsId, getFullColumnName } from '../../../utils/columns'; +import { Column, ColumnsId, getFullColumnName, isKubeObj, KubeObj } from '../../../utils/columns'; import { dateFormatter, getFormattedDate, timeMSFormatter, utcDateTimeFormatter } from '../../../utils/datetime'; import { dnsCodesNames, dnsErrorsValues, getDNSErrorDescription, getDNSRcodeDescription } from '../../../utils/dns'; import { getDSCPDocUrl, getDSCPServiceClassDescription, getDSCPServiceClassName } from '../../../utils/dscp'; import { formatDurationAboveMillisecond, formatDurationAboveNanosecond } from '../../../utils/duration'; -import { - getICMPCode, - getICMPDocUrl, - getICMPType, - icmpAllCodesValues, - icmpAllTypesValues, - isValidICMPProto -} from '../../../utils/icmp'; +import { getICMPCode, getICMPDocUrl, getICMPType, icmpAllTypesValues, isValidICMPProto } from '../../../utils/icmp'; import { dropCausesNames, getDropCauseDescription, getDropCauseDocUrl } from '../../../utils/pkt-drop'; import { formatPort } from '../../../utils/port'; import { formatProtocol, getProtocolDocUrl } from '../../../utils/protocol'; @@ -136,35 +129,40 @@ export const RecordField: React.FC = ({ ); }; + const kubeObjContainer = (k: KubeObj) => { + const main = kubeObjContent(k.name, k.kind, k.namespace); + if (k.showNamespace && k.namespace) { + return doubleContainer(main, kindContent('Namespace', k.namespace), false); + } + return singleContainer(main); + }; + const kubeObjContent = (value: string | undefined, kind: string | undefined, ns: string | undefined) => { // Note: namespace is not mandatory here (e.g. Node objects) if (value && kind) { return (
{resourceIconText(value, kind, ns)} - - {ns && ( - <> - {t('Namespace')} - {ns} - - )} - {kind} - {value} - + {kubeTooltip(value, kind, ns)}
); } return undefined; }; - const explicitKubeObjContent = (ip: string, port: number, kind?: string, namespace?: string, name?: string) => { - // Note: namespace is not mandatory here (e.g. Node objects) - if (name && kind) { - return doubleContainer(kubeObjContent(name, kind, namespace), kindContent('Namespace', namespace), false); - } else { - return ipPortContent(ip, port); - } + const kubeTooltip = (value: string, kind: string, ns: string | undefined) => { + return ( + + {ns && ( + <> + {t('Namespace')} + {ns} + + )} + {kind} + {value} + + ); }; const kindContent = (kind: 'Namespace' | 'Node', value?: string) => { @@ -182,18 +180,6 @@ export const RecordField: React.FC = ({ return undefined; }; - const ipPortContent = (ip: string, port: number, singleText = false) => { - if (singleText) { - return singleContainer(simpleTextWithTooltip(port && !Number.isNaN(port) ? `${ip}:${String(port)}` : ip)); - } else { - return doubleContainer( - simpleTextWithTooltip(ip), - simpleTextWithTooltip(port && !Number.isNaN(port) ? String(port) : undefined), - false - ); - } - }; - const dateTimeContent = (date: Date | undefined) => { if (!date) { return emptyText(); @@ -292,6 +278,10 @@ export const RecordField: React.FC = ({ }; const content = (c: Column) => { + if (!c.value) { + // Value function not configured + return emptyText(); + } const value = c.value(flow); switch (c.id) { case ColumnsId.collectiontime: @@ -310,129 +300,6 @@ export const RecordField: React.FC = ({ ) : undefined ); - case ColumnsId.name: - return doubleContainer( - kubeObjContent(flow.fields.SrcK8S_Name, flow.labels.SrcK8S_Type, flow.labels.SrcK8S_Namespace), - kubeObjContent(flow.fields.DstK8S_Name, flow.labels.DstK8S_Type, flow.labels.DstK8S_Namespace) - ); - case ColumnsId.srcname: - return singleContainer(kubeObjContent(value as string, flow.labels.SrcK8S_Type, flow.labels.SrcK8S_Namespace)); - case ColumnsId.dstname: - return singleContainer(kubeObjContent(value as string, flow.labels.DstK8S_Type, flow.labels.DstK8S_Namespace)); - case ColumnsId.owner: - return doubleContainer( - kubeObjContent(flow.labels.SrcK8S_OwnerName, flow.fields.SrcK8S_OwnerType, flow.labels.SrcK8S_Namespace), - kubeObjContent(flow.labels.DstK8S_OwnerName, flow.fields.DstK8S_OwnerType, flow.labels.DstK8S_Namespace) - ); - case ColumnsId.srcowner: - return singleContainer( - kubeObjContent(value as string, flow.fields.SrcK8S_OwnerType, flow.labels.SrcK8S_Namespace) - ); - case ColumnsId.dstowner: - return singleContainer( - kubeObjContent(value as string, flow.fields.DstK8S_OwnerType, flow.labels.DstK8S_Namespace) - ); - case ColumnsId.addrport: - return doubleContainer( - ipPortContent(flow.fields.SrcAddr || '', flow.fields.SrcPort || NaN), - ipPortContent(flow.fields.DstAddr || '', flow.fields.DstPort || NaN) - ); - case ColumnsId.srcaddrport: - return singleContainer(ipPortContent(flow.fields.SrcAddr || '', flow.fields.SrcPort || NaN)); - case ColumnsId.dstaddrport: - return singleContainer(ipPortContent(flow.fields.DstAddr || '', flow.fields.DstPort || NaN)); - case ColumnsId.kubeobject: - return doubleContainer( - explicitKubeObjContent( - flow.fields.SrcAddr || '', - flow.fields.SrcPort || NaN, - flow.labels.SrcK8S_Type, - flow.labels.SrcK8S_Namespace, - flow.fields.SrcK8S_Name - ), - explicitKubeObjContent( - flow.fields.DstAddr || '', - flow.fields.DstPort || NaN, - flow.labels.DstK8S_Type, - flow.labels.DstK8S_Namespace, - flow.fields.DstK8S_Name - ) - ); - case ColumnsId.srckubeobject: - return singleContainer( - explicitKubeObjContent( - flow.fields.SrcAddr || '', - flow.fields.SrcPort || NaN, - flow.labels.SrcK8S_Type, - flow.labels.SrcK8S_Namespace, - flow.fields.SrcK8S_Name - ) - ); - case ColumnsId.dstkubeobject: - return singleContainer( - explicitKubeObjContent( - flow.fields.DstAddr || '', - flow.fields.DstPort || NaN, - flow.labels.DstK8S_Type, - flow.labels.DstK8S_Namespace, - flow.fields.DstK8S_Name - ) - ); - case ColumnsId.ownerkubeobject: - return doubleContainer( - explicitKubeObjContent( - flow.fields.SrcAddr || '', - flow.fields.SrcPort || NaN, - flow.fields.SrcK8S_OwnerType, - flow.labels.SrcK8S_Namespace, - flow.labels.SrcK8S_OwnerName - ), - explicitKubeObjContent( - flow.fields.DstAddr || '', - flow.fields.DstPort || NaN, - flow.fields.DstK8S_OwnerType, - flow.labels.DstK8S_Namespace, - flow.labels.DstK8S_OwnerName - ) - ); - case ColumnsId.srcownerkubeobject: - return singleContainer( - explicitKubeObjContent( - flow.fields.SrcAddr || '', - flow.fields.SrcPort || NaN, - flow.fields.SrcK8S_OwnerType, - flow.labels.SrcK8S_Namespace, - flow.labels.DstK8S_OwnerName - ) - ); - case ColumnsId.dstownerkubeobject: - return singleContainer( - explicitKubeObjContent( - flow.fields.DstAddr || '', - flow.fields.DstPort || NaN, - flow.fields.DstK8S_OwnerType, - flow.labels.DstK8S_Namespace, - flow.labels.DstK8S_OwnerName - ) - ); - case ColumnsId.namespace: - return doubleContainer( - kindContent('Namespace', flow.labels.SrcK8S_Namespace), - kindContent('Namespace', flow.labels.DstK8S_Namespace) - ); - case ColumnsId.srcnamespace: - case ColumnsId.dstnamespace: { - return singleContainer(kindContent('Namespace', value as string)); - } - case ColumnsId.hostname: - return doubleContainer( - kindContent('Node', flow.fields.SrcK8S_HostName), - kindContent('Node', flow.fields.DstK8S_HostName) - ); - case ColumnsId.srchostname: - case ColumnsId.dsthostname: { - return singleContainer(kindContent('Node', value as string)); - } case ColumnsId.port: return doubleContainer( simpleTextWithTooltip(flow.fields.SrcPort ? formatPort(flow.fields.SrcPort) : ''), @@ -495,18 +362,19 @@ export const RecordField: React.FC = ({ } case ColumnsId.icmptype: { let child = emptyText(); - if (Array.isArray(value) && value.length) { - if (isValidICMPProto(Number(value[0]))) { - const type = getICMPType(Number(value[0]), Number(value[1]) as icmpAllTypesValues); + if (typeof value === 'number' && !isNaN(value)) { + const proto = Number(flow.fields.Proto); + if (isValidICMPProto(proto)) { + const type = getICMPType(proto, value as icmpAllTypesValues); if (type && detailed) { - child = clickableContent(type.name, type.description || '', getICMPDocUrl(Number(value[0]))); + child = clickableContent(type.name, type.description || '', getICMPDocUrl(proto)); } else { - child = simpleTextWithTooltip(type?.name || String(value[1]))!; + child = simpleTextWithTooltip(type?.name || String(value))!; } } else { child = errorTextValue( - String(value[1]), - t('ICMP type provided but protocol is {{proto}}', { proto: formatProtocol(value[0] as number, t) }) + String(value), + t('ICMP type provided but protocol is {{proto}}', { proto: formatProtocol(proto, t) }) ); } } @@ -514,22 +382,20 @@ export const RecordField: React.FC = ({ } case ColumnsId.icmpcode: { let child = emptyText(); - if (Array.isArray(value) && value.length) { - if (isValidICMPProto(Number(value[0]))) { - const code = getICMPCode( - Number(value[0]), - Number(value[1]) as icmpAllTypesValues, - Number(value[2]) as icmpAllCodesValues - ); + if (typeof value === 'number' && !isNaN(value)) { + const proto = Number(flow.fields.Proto); + const typez = Number(flow.fields.IcmpType) as icmpAllTypesValues; + if (isValidICMPProto(proto)) { + const code = getICMPCode(proto, typez, value); if (code && detailed) { - child = clickableContent(code.name, code.description || '', getICMPDocUrl(Number(value[0]))); + child = clickableContent(code.name, code.description || '', getICMPDocUrl(proto)); } else { - child = simpleTextWithTooltip(code?.name || String(value[2]))!; + child = simpleTextWithTooltip(code?.name || String(value))!; } } else { child = errorTextValue( - String(value[1]), - t('ICMP code provided but protocol is {{proto}}', { proto: formatProtocol(value[0] as number, t) }) + String(value), + t('ICMP code provided but protocol is {{proto}}', { proto: formatProtocol(proto, t) }) ); } } @@ -648,13 +514,19 @@ export const RecordField: React.FC = ({ ); } default: + if (value === undefined) { + return emptyText(); + } if (Array.isArray(value) && value.length) { // we can only show two values properly with containers if (value.length === 2) { - return doubleContainer(simpleTextWithTooltip(String(value[0])), simpleTextWithTooltip(String(value[1]))); + const contents = value.map(v => (isKubeObj(v) ? kubeObjContainer(v) : simpleTextWithTooltip(String(v)))); + return doubleContainer(contents[0], contents[1]); } // else we will show values as single joigned string return singleContainer(simpleTextWithTooltip(value.map(v => String(v)).join(', '))); + } else if (value && isKubeObj(value)) { + return kubeObjContainer(value); } else { return singleContainer(simpleTextWithTooltip(String(value))); } diff --git a/web/src/components/drawer/record/record-panel.tsx b/web/src/components/drawer/record/record-panel.tsx index d10fdf303..7a9ba8bdb 100644 --- a/web/src/components/drawer/record/record-panel.tsx +++ b/web/src/components/drawer/record/record-panel.tsx @@ -81,8 +81,11 @@ export const RecordPanel: React.FC = ({ const getVisibleColumns = React.useCallback(() => { const forbiddenColumns = [ColumnsId.ifdirs, ColumnsId.interfaces]; return columns.filter((c: Column) => { - const value = c.value(record); - return !forbiddenColumns.includes(c.id) && value !== null && value !== '' && !Number.isNaN(value); + if (!c.fieldValue) { + return false; + } + const value = c.fieldValue(record); + return !forbiddenColumns.includes(c.id) && value !== '' && !Number.isNaN(value); }); }, [columns, record]); @@ -97,8 +100,8 @@ export const RecordPanel: React.FC = ({ ); const getFilter = (col: Column) => { - if (record) { - const value = col.value(record); + if (record && col.fieldValue) { + const value = col.fieldValue(record); switch (col.id) { case ColumnsId.endtime: return getTimeRangeFilter(col, value); diff --git a/web/src/components/netflow-traffic.tsx b/web/src/components/netflow-traffic.tsx index 2deda01af..0342ce4ee 100644 --- a/web/src/components/netflow-traffic.tsx +++ b/web/src/components/netflow-traffic.tsx @@ -187,17 +187,13 @@ export const NetflowTraffic: React.FC = ({ return getAvailablePanels().filter(panel => panel.isSelected); }, [getAvailablePanels]); - const getAvailableColumns = React.useCallback( - (isSidePanel = false) => { - return model.columns.filter( - col => - (!isSidePanel || !col.isCommon) && - (isConnectionTracking() || ![ColumnsId.recordtype, ColumnsId.hashid].includes(col.id)) && - (!col.feature || model.config.features.includes(col.feature)) - ); - }, - [model.columns, model.config.features, isConnectionTracking] - ); + const getAvailableColumns = React.useCallback(() => { + return model.columns.filter( + col => + (isConnectionTracking() || ![ColumnsId.recordtype, ColumnsId.hashid].includes(col.id)) && + (!col.feature || model.config.features.includes(col.feature)) + ); + }, [model.columns, model.config.features, isConnectionTracking]); const getSelectedColumns = React.useCallback(() => { return getAvailableColumns().filter(column => column.isSelected); @@ -901,7 +897,7 @@ export const NetflowTraffic: React.FC = ({ scopes={getAvailableScopes()} canSwitchTypes={isFlow() && isConnectionTracking()} clearSelections={clearSelections} - availableColumns={getAvailableColumns(true)} + availableColumns={getAvailableColumns()} maxChunkAge={model.config.maxChunkAgeMs} selectedColumns={getSelectedColumns()} /> diff --git a/web/src/components/query-summary/__tests__/summary-panel.spec.tsx b/web/src/components/query-summary/__tests__/summary-panel.spec.tsx index 499ce4060..d323c496d 100644 --- a/web/src/components/query-summary/__tests__/summary-panel.spec.tsx +++ b/web/src/components/query-summary/__tests__/summary-panel.spec.tsx @@ -37,11 +37,13 @@ describe('', () => { const wrapper = mount(); expect(wrapper.find(Accordion)).toHaveLength(1); - expect(wrapper.find(AccordionItem)).toHaveLength(3); + expect(wrapper.find(AccordionItem)).toHaveLength(5); expect(wrapper.find('#addresses').last().text()).toBe('5 IP(s)'); expect(wrapper.find('#ports').last().text()).toBe('4 Port(s)'); expect(wrapper.find('#protocols').last().text()).toBe('1 Protocol(s)'); + expect(wrapper.find('#Pod').last().text()).toBe('2 Pod(s)'); + expect(wrapper.find('#Namespace').last().text()).toBe('1 Namespace(s)'); }); it('should toggle panel', async () => { diff --git a/web/src/components/tabs/netflow-table/netflow-table-row.tsx b/web/src/components/tabs/netflow-table/netflow-table-row.tsx index c5841e136..f8650bc87 100644 --- a/web/src/components/tabs/netflow-table/netflow-table-row.tsx +++ b/web/src/components/tabs/netflow-table/netflow-table-row.tsx @@ -55,7 +55,7 @@ export const NetflowTableRow: React.FC = ({ diff --git a/web/src/utils/__tests__/columns.spec.ts b/web/src/utils/__tests__/columns.spec.ts new file mode 100644 index 000000000..b9a4fe933 --- /dev/null +++ b/web/src/utils/__tests__/columns.spec.ts @@ -0,0 +1,100 @@ +import { Record } from '../../api/ipfix'; +import { FullConfigResultSample } from '../../components/__tests-data__/config'; +import { ColumnsId, getDefaultColumns } from '../columns'; + +describe('Columns', () => { + const flow: Record = { + fields: { + SrcAddr: '10.0.0.1', + SrcPort: 42000, + DstAddr: '10.0.0.2', + DstPort: 8080, + SrcK8S_Name: 'client', + DstK8S_Name: 'server' + }, + labels: { + SrcK8S_Namespace: 'foo', + DstK8S_Namespace: 'bar', + SrcK8S_Type: 'Pod', + DstK8S_Type: 'Service' + }, + key: 0 + }; + + const { columns, fields } = FullConfigResultSample; + const defColumns = getDefaultColumns(columns, fields); + + it('should calculate IP+Port value', () => { + const col = defColumns.find(c => c.id === ('SrcAddrPort' as ColumnsId)); + expect(col).toBeDefined(); + const value = col?.value!(flow); + expect(value).toEqual('10.0.0.1:42000'); + }); + + it('should calculate src+dst IP+Port values', () => { + const col = defColumns.find(c => c.id === ('AddrPort' as ColumnsId)); + expect(col).toBeDefined(); + const value = col?.value!(flow); + expect(value).toEqual(['10.0.0.1:42000', '10.0.0.2:8080']); + }); + + it('should calculate k8s name values', () => { + const col = defColumns.find(c => c.id === ('SrcK8S_Name' as ColumnsId)); + expect(col).toBeDefined(); + const value = col?.value!(flow); + expect(value).toEqual({ kind: 'Pod', name: 'client', namespace: 'foo', showNamespace: false }); + }); + + it('should calculate k8s owner name when empty', () => { + const col = defColumns.find(c => c.id === ('DstK8S_OwnerName' as ColumnsId)); + expect(col).toBeDefined(); + const value = col?.value!(flow); + expect(value).toBeUndefined(); + }); + + it('should calculate k8s namespace values', () => { + const col = defColumns.find(c => c.id === ('SrcK8S_Namespace' as ColumnsId)); + expect(col).toBeDefined(); + const value = col?.value!(flow); + expect(value).toEqual({ kind: 'Namespace', name: 'foo', showNamespace: false }); + }); + + it('should calculate k8s object values', () => { + const col = defColumns.find(c => c.id === ('SrcK8S_Object' as ColumnsId)); + expect(col).toBeDefined(); + const value = col?.value!(flow); + expect(value).toEqual({ kind: 'Pod', name: 'client', namespace: 'foo', showNamespace: true }); + }); + + it('should fallback on IP', () => { + const withoutName: Record = { ...flow, fields: { ...flow.fields, SrcK8S_Name: undefined } }; + const col = defColumns.find(c => c.id === ('SrcK8S_Object' as ColumnsId)); + expect(col).toBeDefined(); + const value = col?.value!(withoutName); + expect(value).toEqual('10.0.0.1:42000'); + }); + + it('should calculate src+dst K8S types', () => { + const col = defColumns.find(c => c.id === ('K8S_Type' as ColumnsId)); + expect(col).toBeDefined(); + const value = col?.value!(flow); + expect(value).toEqual(['Pod', 'Service']); + }); + + it('should calculate src+dst K8S objects', () => { + const col = defColumns.find(c => c.id === ('K8S_Object' as ColumnsId)); + expect(col).toBeDefined(); + const value = col?.value!(flow); + expect(value).toEqual([ + { kind: 'Pod', name: 'client', namespace: 'foo', showNamespace: true }, + { kind: 'Service', name: 'server', namespace: 'bar', showNamespace: true } + ]); + }); + + it('should calculate src+dst K8S owner objects when empty', () => { + const col = defColumns.find(c => c.id === ('K8S_OwnerObject' as ColumnsId)); + expect(col).toBeDefined(); + const value = col?.value!(flow); + expect(value).toEqual([undefined, undefined]); + }); +}); diff --git a/web/src/utils/column-parser.ts b/web/src/utils/column-parser.ts new file mode 100644 index 000000000..fb36df79c --- /dev/null +++ b/web/src/utils/column-parser.ts @@ -0,0 +1,161 @@ +import { getRecordValue, Record } from '../api/ipfix'; +import { Column, ColumnConfigDef, ColumnsId, ColValue } from './columns'; +import { FieldConfig, FieldType } from './fields'; + +const getColumnOrRecordValue = ( + columns: Column[], + record: Record, + arg: string, + defaultValue: string | number +): ColValue => { + if (arg.startsWith(`'`) && arg.endsWith(`'`)) { + // literal + return arg.substring(1, arg.length - 1); + } else if (arg.startsWith('column.')) { + const colId = arg.replace('column.', ''); + const found = columns.find(c => c.id === colId); + if (found && found.value) { + return found.value(record); + } + return defaultValue; + } + return getRecordValue(record, arg, defaultValue); +}; + +const funcs: { [name: string]: (columns: Column[], record: Record, args: string[]) => ColValue } = { + concat: (columns: Column[], record: Record, args: string[]): ColValue => { + if (args.length < 2) { + console.error('getDefaultColumns - invalid parameters for concat calculated value', args); + return ''; + } + return args.map(a => getColumnOrRecordValue(columns, record, a, '')).join(''); + }, + kubeObject: (columns: Column[], record: Record, args: string[]): ColValue => { + if (args.length !== 4) { + console.error('getDefaultColumns - invalid parameters for kubeObject calculated value', args); + return ''; + } + const kind = String(getColumnOrRecordValue(columns, record, args[0], '')); + const namespace = String(getColumnOrRecordValue(columns, record, args[1], '')); + const name = String(getColumnOrRecordValue(columns, record, args[2], '')); + if (!name || !kind) { + return undefined; + } + return { + kind: kind, + name: name, + namespace: namespace || undefined, // convert empty string to undefined + showNamespace: args[3] === '1' + }; + }, + substract: (columns: Column[], record: Record, args: string[]): ColValue => { + if (args.length !== 2) { + console.error('getDefaultColumns - invalid parameters for substract calculated value', args); + return ''; + } + return ( + (getColumnOrRecordValue(columns, record, args[0], 0) as number) - + (getColumnOrRecordValue(columns, record, args[1], 0) as number) + ); + }, + multiply: (columns: Column[], record: Record, args: string[]): ColValue => { + if (args.length !== 2) { + console.error('getDefaultColumns - invalid parameters for multiply calculated value', args); + return ''; + } + return (getColumnOrRecordValue(columns, record, args[0], 0) as number) * Number(args[1]); + } +}; + +const parseORs = (columns: Column[], calculatedValue: string): ((record: Record) => ColValue)[] => { + // parseORs returns a closure [(Record) => ColValue] to pre-process as much as possible + const ors = calculatedValue.split(' or '); + return ors.map(or => { + for (const name in funcs) { + if (or.startsWith(name + '(')) { + const regex = new RegExp(name + '|\\(|\\)', 'g'); + const repl = or.replaceAll(regex, ''); + const args = repl.split(','); + return (record: Record) => funcs[name](columns, record, args); + } + } + return () => undefined; + }); +}; + +const forceType = (id: ColumnsId, value: ColValue, type?: FieldType): ColValue => { + if (!type) { + console.error('Column ' + id + " doesn't specify type"); + } + // check if value type match and convert it if needed + if (value && value !== '' && typeof value !== type && !Array.isArray(value)) { + switch (type) { + case 'number': + return Number(value); + case 'string': + return String(value); + default: + throw new Error('forceType error: type ' + type + ' is not managed'); + } + } else { + // else return value directly + return value; + } +}; + +export type ValueFunc = (record: Record) => ColValue; + +export const fromFieldFunc = ( + def: ColumnConfigDef, + fields: FieldConfig[] | undefined, + field: FieldConfig | undefined +): ValueFunc | undefined => { + if (fields) { + return (r: Record) => { + const result: ColValue[] = fields.map(fc => { + const value = getRecordValue(r, fc.name, undefined); + return forceType(def.id as ColumnsId, value, fc.type); + }); + return result.flatMap(r => r) as ColValue; + }; + } else if (field) { + return (r: Record) => { + const value = getRecordValue(r, field!.name, ''); + return forceType(def.id as ColumnsId, value, field!.type); + }; + } + return undefined; +}; + +export const computeValueFunc = ( + def: ColumnConfigDef, + columns: Column[], + fields: FieldConfig[] | undefined, + field: FieldConfig | undefined +): ValueFunc | undefined => { + if (def.calculated) { + if (def.calculated.startsWith('[') && def.calculated.endsWith(']')) { + const values = def.calculated.replaceAll(/\[|\]/g, '').split(','); + return (r: Record) => { + const result = values.map(v => getColumnOrRecordValue(columns, r, v, '')); + return result.flatMap(r => r) as ColValue; + }; + } + const orFuncs = parseORs(columns, def.calculated!); + return (r: Record) => { + for (const orFunc of orFuncs) { + const result = orFunc(r); + if (result) { + return result; + } + } + return undefined; + }; + } + + const fromField = fromFieldFunc(def, fields, field); + if (fromField === undefined) { + console.warn('column.value called on ' + def.id + ' but not configured'); + } + return fromField; +}; diff --git a/web/src/utils/columns.ts b/web/src/utils/columns.ts index 2e568a1ed..6f30f1004 100644 --- a/web/src/utils/columns.ts +++ b/web/src/utils/columns.ts @@ -1,9 +1,10 @@ import _ from 'lodash'; -import { getRecordValue, Record } from '../api/ipfix'; +import { Record } from '../api/ipfix'; import { Feature } from '../model/config'; import { FilterId } from '../model/filters'; import { compareNumbers, compareStrings } from './base-compare'; -import { FieldConfig, FieldType } from './fields'; +import { computeValueFunc, fromFieldFunc } from './column-parser'; +import { FieldConfig } from './fields'; import { compareIPs } from './ip'; import { comparePorts } from './port'; import { compareProtocols } from './protocol'; @@ -36,8 +37,6 @@ export enum ColumnsId { srcport = 'SrcPort', dstport = 'DstPort', addrport = 'AddrPort', - srcaddrport = 'SrcAddrPort', - dstaddrport = 'DstAddrPort', proto = 'Proto', icmptype = 'IcmpType', icmpcode = 'IcmpCode', @@ -98,6 +97,17 @@ export interface ColumnConfigDef { feature?: Feature; } +export interface KubeObj { + name: string; + kind: string; + namespace?: string; + showNamespace: boolean; +} +export type ColValue = string | number | KubeObj | string[] | number[] | KubeObj[] | undefined; +export const isKubeObj = (v: ColValue): v is KubeObj => { + return (v as KubeObj)?.kind !== undefined; +}; + export interface Column { id: ColumnsId; group?: string; @@ -108,7 +118,8 @@ export interface Column { quickFilter?: FilterId; isSelected: boolean; isCommon?: boolean; - value: (flow: Record) => string | number | string[] | number[]; + value?: (flow: Record) => ColValue; + fieldValue?: (flow: Record) => ColValue; sort(a: Record, b: Record, col: Column): number; // width in "em" width: number; @@ -190,115 +201,9 @@ export const getShortColumnName = (col?: Column): string => { return ''; }; -export const getSrcOrDstValue = (v1?: string | number, v2?: string | number): string[] | number[] => { - if (v1 && !Number.isNaN(v1) && v2 && !Number.isNaN(v2)) { - return [v1 as number, v2 as number]; - } else if (v1 || v2) { - return [v1 ? (v1 as string) : '', v2 ? (v2 as string) : '']; - } else { - return []; - } -}; - -/* concatenate kind / namespace / pod or ip / port for display - * if kubernetes objects Kind Namespace & Pod field are not resolved, will fallback on - * ip:port or ip only if port is not provided - */ -export const getConcatenatedValue = ( - ip: string, - port: number, - kind?: string, - namespace?: string, - pod?: string -): string => { - if (kind && namespace && pod) { - return `${kind}.${namespace}.${pod}`; - } - if (!Number.isNaN(port)) { - return `${ip}:${String(port)}`; - } - return ip; -}; - export const getDefaultColumns = (columnDefs: ColumnConfigDef[], fieldConfigs: FieldConfig[]): Column[] => { const columns: Column[] = []; - function getColumnOrRecordValue(record: Record, arg: string, defaultValue: string | number) { - if (arg.startsWith('column.')) { - const colId = arg.replace('column.', ''); - const found = columns.find(c => c.id === colId); - if (found) { - return found.value(record); - } - return defaultValue; - } - return getRecordValue(record, arg, defaultValue); - } - - function calculatedValue(record: Record, calculatedValue: string) { - if (calculatedValue.startsWith('getSrcOrDstValue')) { - const args = calculatedValue.replaceAll(/getSrcOrDstValue|\(|\)/g, '').split(','); - if (args.length !== 2) { - console.error('getDefaultColumns - invalid parameters for getSrcOrDstValue calculated value', calculatedValue); - return ''; - } - return getSrcOrDstValue(...args.flatMap(f => getColumnOrRecordValue(record, f, ''))); - } else if (calculatedValue.startsWith('getConcatenatedValue')) { - const args = calculatedValue.replaceAll(/getConcatenatedValue|\(|\)/g, '').split(','); - if (args.length < 2) { - console.error( - 'getDefaultColumns - invalid parameters for getConcatenatedValue calculated value', - calculatedValue - ); - return ''; - } - return getConcatenatedValue( - getColumnOrRecordValue(record, args[0], '') as string, - getColumnOrRecordValue(record, args[1], NaN) as number, - args.length > 2 ? (getColumnOrRecordValue(record, args[2], '') as string) : undefined, - args.length > 3 ? (getColumnOrRecordValue(record, args[3], '') as string) : undefined - ); - } else if (calculatedValue.startsWith('substract')) { - const args = calculatedValue.replaceAll(/substract|\(|\)/g, '').split(','); - if (args.length < 2) { - console.error('getDefaultColumns - invalid parameters for substract calculated value', calculatedValue); - return ''; - } - return ( - (getColumnOrRecordValue(record, args[0], 0) as number) - (getColumnOrRecordValue(record, args[1], 0) as number) - ); - } else if (calculatedValue.startsWith('multiply')) { - const args = calculatedValue.replaceAll(/multiply|\(|\)/g, '').split(','); - if (args.length < 2) { - console.error('getDefaultColumns - invalid parameters for multiply calculated value', calculatedValue); - return ''; - } - return (getColumnOrRecordValue(record, args[0], 0) as number) * Number(args[1]); - } - return ''; - } - - // eslint-disable-next-line @typescript-eslint/no-explicit-any - function forceType(id: ColumnsId, value: any, type?: FieldType) { - if (!type) { - console.error('Column ' + id + " doesn't specify type"); - } - // check if value type match and convert it if needed - if (value && value !== '' && typeof value !== type && !Array.isArray(value)) { - switch (type) { - case 'number': - return Number(value); - case 'string': - return String(value); - default: - throw new Error('forceType error: type ' + type + ' is not managed'); - } - } else { - // else return value directly - return value; - } - } - // add a column for each definition columnDefs.forEach(d => { const id = d.id as ColumnsId; @@ -322,65 +227,29 @@ export const getDefaultColumns = (columnDefs: ColumnConfigDef[], fieldConfigs: F docURL: !_.isEmpty(d.docURL) ? d.docURL : undefined, quickFilter: !_.isEmpty(d.filter) ? (d.filter as FilterId) : undefined, isSelected: d.default === true, - isCommon: !_.isEmpty(d.calculated), - value: (r: Record) => { - if (!_.isEmpty(d.calculated)) { - if (d.calculated!.startsWith('[') && d.calculated!.endsWith(']')) { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const result: any = []; - if (d.calculated?.includes('column')) { - // consider all items as columns or fields - const values = d.calculated!.replaceAll(/\[|\]/g, '').split(','); - values.forEach(v => { - result.push(getColumnOrRecordValue(r, v, '')); - }); - } else { - // consider all items as functions - const values = d.calculated!.replaceAll(/\[|\]/g, '').split('),'); - values.forEach(v => { - result.push(calculatedValue(r, `${v})`)); - }); - } - return result; - } else { - return calculatedValue(r, d.calculated!); - } - } else if (fields) { - const arr: (string | number | undefined)[] = []; - fields.forEach(fc => { - const value = getRecordValue(r, fc.name, undefined); - arr.push(forceType(id, value, fc.type)); - }); - return arr; - } else if (field) { - const value = getRecordValue(r, field!.name, ''); - return forceType(id, value, field!.type); - } else { - console.warn('column.value called on ' + id + ' but not configured'); - return null; - } - }, + isCommon: d.calculated !== undefined && d.calculated.startsWith('['), + value: computeValueFunc(d, columns, fields, field), + fieldValue: fromFieldFunc(d, fields, field), sort: (a: Record, b: Record, col: Column) => { - if (d.calculated) { + if (!col.fieldValue) { return -1; - } else { - const valA = col.value(a); - const valB = col.value(b); - if (typeof valA === 'number' && typeof valB === 'number') { - if (col.id.includes('Port')) { - return comparePorts(valA, valB); - } else if (col.id.includes('Proto')) { - return compareProtocols(valA, valB); - } - return compareNumbers(valA, valB); - } else if (typeof valA === 'string' && typeof valB === 'string') { - if (col.id.includes('IP')) { - return compareIPs(valA, valB); - } - return compareStrings(valA, valB); + } + const valA = col.fieldValue(a); + const valB = col.fieldValue(b); + if (typeof valA === 'number' && typeof valB === 'number') { + if (col.id.includes('Port')) { + return comparePorts(valA, valB); + } else if (col.id.includes('Proto')) { + return compareProtocols(valA, valB); + } + return compareNumbers(valA, valB); + } else if (typeof valA === 'string' && typeof valB === 'string') { + if (col.id.includes('IP')) { + return compareIPs(valA, valB); } - return 0; + return compareStrings(valA, valB); } + return 0; }, width: d.width || 15, feature: d.feature