= ({
- recordType,
- setRecordType,
- dataSource,
- setDataSource,
- showDuplicates,
- setShowDuplicates,
- allowLoki,
- allowProm,
- allowFlow,
- allowConnection,
- allowShowDuplicates,
- deduperMark,
- allowPktDrops,
- useTopK,
- limit,
- setLimit,
- match,
- setMatch,
- packetLoss,
- setPacketLoss
-}) => {
- const { t } = useTranslation('plugin__netobserv-plugin');
-
- const recordTypeOptions: RecordTypeOption[] = [
- {
- label: t('Conversation'),
- value: 'allConnections'
- },
- {
- label: t('Flow'),
- value: 'flowLog'
- }
- ];
-
- const dataSourceOptions: DataSourceOption[] = [
- {
- label: t('Loki'),
- value: 'loki'
- },
- {
- label: t('Prometheus'),
- value: 'prom'
- },
- {
- label: t('Auto'),
- value: 'auto'
- }
- ];
-
- const matchOptions: MatchOption[] = [
- {
- label: t('Match all'),
- value: 'all'
- },
- {
- label: t('Match any'),
- value: 'any'
- }
- ];
-
- const packetLossOptions: PacketLossOption[] = [
- {
- label: t('Fully dropped'),
- value: 'dropped'
- },
- {
- label: t('Containing drops'),
- value: 'hasDrops'
- },
- {
- label: t('Without drops'),
- value: 'sent'
- },
- {
- label: t('All'),
- value: 'all'
- }
- ];
-
- const values = useTopK ? topValues : limitValues;
-
- return (
- <>
-
-
-
-
- {t('Log type')}
-
-
-
- {recordTypeOptions.map(opt => {
- const disabled =
- (!allowFlow && opt.value === 'flowLog') || (!allowConnection && opt.value === 'allConnections');
- return (
-
-
-
- setRecordType(opt.value)}
- label={opt.label}
- data-test={`recordType-${opt.value}`}
- id={`recordType-${opt.value}`}
- value={opt.value}
- />
-
-
-
- );
- })}
-
-
-
-
-
- {t('Datasource')}
-
-
-
- {dataSourceOptions.map(opt => {
- const disabled = (!allowProm && opt.value === 'prom') || (!allowLoki && opt.value === 'loki');
- return (
-
-
-
- setDataSource(opt.value)}
- label={opt.label}
- data-test={`dataSource-${opt.value}`}
- id={`dataSource-${opt.value}`}
- value={opt.value}
- />
-
-
-
- );
- })}
-
- {deduperMark && (
-
-
-
-
- {t('Duplicated flows')}
-
-
-
-
- setShowDuplicates(!showDuplicates)}
- label={t('Show duplicates')}
- data-test={'show-duplicates'}
- id={'show-duplicates'}
- />
-
-
- )}
-
-
-
-
- {t('Match filters')}
-
-
-
- {matchOptions.map(opt => (
-
-
- setMatch(opt.value)}
- label={opt.label}
- data-test={`match-${opt.value}`}
- id={`match-${opt.value}`}
- value={opt.value}
- />
-
-
- ))}
-
-
-
-
- {t('Filter flows by their drop status. Only packets dropped by the kernel are monitored here.')}
-
-
- - {t('Fully dropped shows the flows that are 100% dropped')}
-
-
- - {t('Containing drops shows the flows having at least one packet dropped')}
-
-
- - {t('Without drops show the flows having 0% dropped')}
-
-
- - {t('All shows everything')}
-
-
- }
- >
-
-
- {t('Drops filter')}
-
-
-
- {packetLossOptions.map(opt => {
- const disabled = !allowPktDrops && opt.value !== 'all';
- return (
-
-
-
- setPacketLoss(opt.value)}
- label={opt.label}
- data-test={`packet-loss-${opt.value}`}
- id={`packet-loss-${opt.value}`}
- value={opt.value}
- />
-
-
-
- );
- })}
-
-
-
-
-
- {useTopK ? t('Top / Bottom') : t('Limit')}
-
-
-
- {values.map(l => (
-
-
- setLimit(l)}
- value={String(l)}
- />
-
-
- ))}
-
- >
- );
-};
-
-export const QueryOptionsDropdown: React.FC = props => {
+export const QueryOptionsDropdown: React.FC = props => {
const { t } = useTranslation('plugin__netobserv-plugin');
const [isOpen, setOpen] = React.useState(false);
return (
diff --git a/web/src/components/dropdowns/query-options-panel.tsx b/web/src/components/dropdowns/query-options-panel.tsx
new file mode 100644
index 000000000..cd009a485
--- /dev/null
+++ b/web/src/components/dropdowns/query-options-panel.tsx
@@ -0,0 +1,358 @@
+import { Checkbox, Radio, Text, TextContent, TextVariants, Tooltip } from '@patternfly/react-core';
+import { InfoAltIcon } from '@patternfly/react-icons';
+import * as React from 'react';
+import { useTranslation } from 'react-i18next';
+import { DataSource, Match, PacketLoss, RecordType } from '../../model/flow-query';
+import { QueryOptionsProps } from './query-options-dropdown';
+
+export const topValues = [5, 10, 15];
+export const limitValues = [50, 100, 500, 1000];
+
+type RecordTypeOption = { label: string; value: RecordType };
+type DataSourceOption = { label: string; value: DataSource };
+type MatchOption = { label: string; value: Match };
+
+type PacketLossOption = { label: string; value: PacketLoss };
+
+// Exported for tests
+export const QueryOptionsPanel: React.FC = ({
+ recordType,
+ setRecordType,
+ dataSource,
+ setDataSource,
+ showDuplicates,
+ setShowDuplicates,
+ allowLoki,
+ allowProm,
+ allowFlow,
+ allowConnection,
+ allowShowDuplicates,
+ deduperMark,
+ allowPktDrops,
+ useTopK,
+ limit,
+ setLimit,
+ match,
+ setMatch,
+ packetLoss,
+ setPacketLoss
+}) => {
+ const { t } = useTranslation('plugin__netobserv-plugin');
+
+ const recordTypeOptions: RecordTypeOption[] = [
+ {
+ label: t('Conversation'),
+ value: 'allConnections'
+ },
+ {
+ label: t('Flow'),
+ value: 'flowLog'
+ }
+ ];
+
+ const dataSourceOptions: DataSourceOption[] = [
+ {
+ label: t('Loki'),
+ value: 'loki'
+ },
+ {
+ label: t('Prometheus'),
+ value: 'prom'
+ },
+ {
+ label: t('Auto'),
+ value: 'auto'
+ }
+ ];
+
+ const matchOptions: MatchOption[] = [
+ {
+ label: t('Match all'),
+ value: 'all'
+ },
+ {
+ label: t('Match any'),
+ value: 'any'
+ }
+ ];
+
+ const packetLossOptions: PacketLossOption[] = [
+ {
+ label: t('Fully dropped'),
+ value: 'dropped'
+ },
+ {
+ label: t('Containing drops'),
+ value: 'hasDrops'
+ },
+ {
+ label: t('Without drops'),
+ value: 'sent'
+ },
+ {
+ label: t('All'),
+ value: 'all'
+ }
+ ];
+
+ const values = useTopK ? topValues : limitValues;
+
+ return (
+ <>
+
+
+
+
+ {t('Log type')}
+
+
+
+ {recordTypeOptions.map(opt => {
+ const disabled =
+ (!allowFlow && opt.value === 'flowLog') || (!allowConnection && opt.value === 'allConnections');
+ return (
+
+
+
+ setRecordType(opt.value)}
+ label={opt.label}
+ data-test={`recordType-${opt.value}`}
+ id={`recordType-${opt.value}`}
+ value={opt.value}
+ />
+
+
+
+ );
+ })}
+
+
+
+
+
+ {t('Datasource')}
+
+
+
+ {dataSourceOptions.map(opt => {
+ const disabled = (!allowProm && opt.value === 'prom') || (!allowLoki && opt.value === 'loki');
+ return (
+
+
+
+ setDataSource(opt.value)}
+ label={opt.label}
+ data-test={`dataSource-${opt.value}`}
+ id={`dataSource-${opt.value}`}
+ value={opt.value}
+ />
+
+
+
+ );
+ })}
+
+ {deduperMark && (
+
+
+
+
+ {t('Duplicated flows')}
+
+
+
+
+ setShowDuplicates(!showDuplicates)}
+ label={t('Show duplicates')}
+ data-test={'show-duplicates'}
+ id={'show-duplicates'}
+ />
+
+
+ )}
+
+
+
+
+ {t('Match filters')}
+
+
+
+ {matchOptions.map(opt => (
+
+
+ setMatch(opt.value)}
+ label={opt.label}
+ data-test={`match-${opt.value}`}
+ id={`match-${opt.value}`}
+ value={opt.value}
+ />
+
+
+ ))}
+
+
+
+
+ {t('Filter flows by their drop status. Only packets dropped by the kernel are monitored here.')}
+
+
+ - {t('Fully dropped shows the flows that are 100% dropped')}
+
+
+ - {t('Containing drops shows the flows having at least one packet dropped')}
+
+
+ - {t('Without drops show the flows having 0% dropped')}
+
+
+ - {t('All shows everything')}
+
+
+ }
+ >
+
+
+ {t('Drops filter')}
+
+
+
+ {packetLossOptions.map(opt => {
+ const disabled = !allowPktDrops && opt.value !== 'all';
+ return (
+
+
+
+ setPacketLoss(opt.value)}
+ label={opt.label}
+ data-test={`packet-loss-${opt.value}`}
+ id={`packet-loss-${opt.value}`}
+ value={opt.value}
+ />
+
+
+
+ );
+ })}
+
+
+
+
+
+ {useTopK ? t('Top / Bottom') : t('Limit')}
+
+
+
+ {values.map(l => (
+
+
+ setLimit(l)}
+ value={String(l)}
+ />
+
+
+ ))}
+
+ >
+ );
+};
+
+export default QueryOptionsPanel;
diff --git a/web/src/components/dropdowns/table-display-dropdown.tsx b/web/src/components/dropdowns/table-display-dropdown.tsx
index 7d56cf70f..3cc449d18 100644
--- a/web/src/components/dropdowns/table-display-dropdown.tsx
+++ b/web/src/components/dropdowns/table-display-dropdown.tsx
@@ -1,62 +1,17 @@
-import { Radio, Select, Text, TextVariants, Tooltip } from '@patternfly/react-core';
-import { InfoAltIcon } from '@patternfly/react-icons';
-import * as _ from 'lodash';
+import { Select, Text, TextVariants } from '@patternfly/react-core';
import * as React from 'react';
import { useTranslation } from 'react-i18next';
import './table-display-dropdown.css';
+import { TableDisplayOptions } from './table-display-options';
export type Size = 's' | 'm' | 'l';
-export interface TableDisplayOptionsProps {
+export interface TableDisplayDropdownProps {
size: Size;
setSize: (v: Size) => void;
}
-export const TableDisplayOptions: React.FC = ({ size, setSize }) => {
- const { t } = useTranslation('plugin__netobserv-plugin');
-
- const sizeOptions = {
- s: t('Compact'),
- m: t('Normal'),
- l: t('Large')
- };
-
- return (
- <>
-
-
-
-
- {t('Row size')}
-
-
-
- {_.map(sizeOptions, (name, key) => {
- return (
-
-
- setSize(key as Size)}
- label={name}
- data-test={`size-${key}`}
- id={`size-${key}`}
- value={key}
- />
-
-
- );
- })}
-
- >
- );
-};
-
-export const TableDisplayDropdown: React.FC<{
- size: Size;
- setSize: (v: Size) => void;
-}> = ({ size, setSize }) => {
+export const TableDisplayDropdown: React.FC = ({ size, setSize }) => {
const { t } = useTranslation('plugin__netobserv-plugin');
const [isOpen, setOpen] = React.useState(false);
diff --git a/web/src/components/dropdowns/table-display-options.tsx b/web/src/components/dropdowns/table-display-options.tsx
new file mode 100644
index 000000000..3e2b7a997
--- /dev/null
+++ b/web/src/components/dropdowns/table-display-options.tsx
@@ -0,0 +1,55 @@
+import { Radio, Text, TextVariants, Tooltip } from '@patternfly/react-core';
+import { InfoAltIcon } from '@patternfly/react-icons';
+import * as _ from 'lodash';
+import * as React from 'react';
+import { useTranslation } from 'react-i18next';
+
+export type Size = 's' | 'm' | 'l';
+
+export interface TableDisplayOptionsProps {
+ size: Size;
+ setSize: (v: Size) => void;
+}
+
+export const TableDisplayOptions: React.FC = ({ size, setSize }) => {
+ const { t } = useTranslation('plugin__netobserv-plugin');
+
+ const sizeOptions = {
+ s: t('Compact'),
+ m: t('Normal'),
+ l: t('Large')
+ };
+
+ return (
+ <>
+
+
+
+
+ {t('Row size')}
+
+
+
+ {_.map(sizeOptions, (name, key) => {
+ return (
+
+
+ setSize(key as Size)}
+ label={name}
+ data-test={`size-${key}`}
+ id={`size-${key}`}
+ value={key}
+ />
+
+
+ );
+ })}
+
+ >
+ );
+};
+
+export default TableDisplayOptions;
diff --git a/web/src/components/dropdowns/topology-display-dropdown.tsx b/web/src/components/dropdowns/topology-display-dropdown.tsx
index 22ecd4be9..5d97cb073 100644
--- a/web/src/components/dropdowns/topology-display-dropdown.tsx
+++ b/web/src/components/dropdowns/topology-display-dropdown.tsx
@@ -1,240 +1,10 @@
-import { Checkbox, Flex, FlexItem, Select, Switch, Text, TextVariants, Tooltip } from '@patternfly/react-core';
-import { InfoAltIcon } from '@patternfly/react-icons';
+import { Select, Text, TextVariants } from '@patternfly/react-core';
import * as React from 'react';
import { useTranslation } from 'react-i18next';
import { FlowScope, MetricType, StatFunction } from '../../model/flow-query';
-import { MetricScopeOptions } from '../../model/metrics';
-import { LayoutName, TopologyGroupTypes, TopologyOptions } from '../../model/topology';
-import GroupDropdown from './group-dropdown';
-import LayoutDropdown from './layout-dropdown';
-import TruncateDropdown, { TruncateLength } from './truncate-dropdown';
-
-import MetricFunctionDropdown from './metric-function-dropdown';
-import MetricTypeDropdown from './metric-type-dropdown';
-import ScopeDropdown from './scope-dropdown';
+import { TopologyOptions } from '../../model/topology';
import './topology-display-dropdown.css';
-
-export type Size = 's' | 'm' | 'l';
-
-export interface TopologyDisplayOptionsProps {
- metricFunction: StatFunction;
- setMetricFunction: (f: StatFunction) => void;
- metricType: MetricType;
- setMetricType: (t: MetricType) => void;
- metricScope: FlowScope;
- setMetricScope: (s: FlowScope) => void;
- topologyOptions: TopologyOptions;
- setTopologyOptions: (o: TopologyOptions) => void;
- allowPktDrop: boolean;
- allowDNSMetric: boolean;
- allowRTTMetric: boolean;
- allowedScopes: FlowScope[];
-}
-
-export const TopologyDisplayOptions: React.FC = ({
- metricFunction,
- setMetricFunction,
- metricType,
- setMetricType,
- metricScope,
- setMetricScope,
- topologyOptions,
- setTopologyOptions,
- allowPktDrop,
- allowDNSMetric,
- allowRTTMetric,
- allowedScopes
-}) => {
- const { t } = useTranslation('plugin__netobserv-plugin');
-
- const setLayout = (layout: LayoutName) => {
- setTopologyOptions({
- ...topologyOptions,
- layout
- });
- };
-
- const setGroupType = (groupTypes: TopologyGroupTypes) => {
- setTopologyOptions({
- ...topologyOptions,
- groupTypes
- });
- };
-
- const setTruncateLength = (truncateLength: TruncateLength) => {
- setTopologyOptions({
- ...topologyOptions,
- truncateLength
- });
- };
-
- return (
- <>
-
-
-
-
- {t('Edge labels')}
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- {t('Scope')}
-
-
-
-
-
-
-
-
-
-
-
- {t('Groups')}
-
-
-
-
-
-
-
-
-
-
-
- {t('Layout')}
-
-
-
-
-
-
-
-
-
-
-
- {t('Show')}
-
-
-
-
- setTopologyOptions({
- ...topologyOptions,
- edges: !topologyOptions.edges
- })
- }
- />
-
- setTopologyOptions({
- ...topologyOptions,
- edgeTags: !topologyOptions.edgeTags
- })
- }
- />
-
- setTopologyOptions({
- ...topologyOptions,
- nodeBadges: !topologyOptions.nodeBadges
- })
- }
- />
-
-
-
-
-
- {t('Truncate labels')}
-
-
-
-
-
-
-
-
-
-
- setTopologyOptions({
- ...topologyOptions,
- startCollapsed: !topologyOptions.startCollapsed
- })
- }
- isReversed
- />
-
-
- >
- );
-};
+import { TopologyDisplayOptions } from './topology-display-options';
export const TopologyDisplayDropdown: React.FC<{
metricFunction: StatFunction;
diff --git a/web/src/components/dropdowns/topology-display-options.tsx b/web/src/components/dropdowns/topology-display-options.tsx
new file mode 100644
index 000000000..dfb47294a
--- /dev/null
+++ b/web/src/components/dropdowns/topology-display-options.tsx
@@ -0,0 +1,238 @@
+import { Checkbox, Flex, FlexItem, Switch, Text, TextVariants, Tooltip } from '@patternfly/react-core';
+import { InfoAltIcon } from '@patternfly/react-icons';
+import * as React from 'react';
+import { useTranslation } from 'react-i18next';
+import { FlowScope, MetricType, StatFunction } from '../../model/flow-query';
+import { MetricScopeOptions } from '../../model/metrics';
+import { LayoutName, TopologyGroupTypes, TopologyOptions } from '../../model/topology';
+import GroupDropdown from './group-dropdown';
+import LayoutDropdown from './layout-dropdown';
+import TruncateDropdown, { TruncateLength } from './truncate-dropdown';
+
+import MetricFunctionDropdown from './metric-function-dropdown';
+import MetricTypeDropdown from './metric-type-dropdown';
+import ScopeDropdown from './scope-dropdown';
+
+export type Size = 's' | 'm' | 'l';
+
+export interface TopologyDisplayOptionsProps {
+ metricFunction: StatFunction;
+ setMetricFunction: (f: StatFunction) => void;
+ metricType: MetricType;
+ setMetricType: (t: MetricType) => void;
+ metricScope: FlowScope;
+ setMetricScope: (s: FlowScope) => void;
+ topologyOptions: TopologyOptions;
+ setTopologyOptions: (o: TopologyOptions) => void;
+ allowPktDrop: boolean;
+ allowDNSMetric: boolean;
+ allowRTTMetric: boolean;
+ allowedScopes: FlowScope[];
+}
+
+export const TopologyDisplayOptions: React.FC = ({
+ metricFunction,
+ setMetricFunction,
+ metricType,
+ setMetricType,
+ metricScope,
+ setMetricScope,
+ topologyOptions,
+ setTopologyOptions,
+ allowPktDrop,
+ allowDNSMetric,
+ allowRTTMetric,
+ allowedScopes
+}) => {
+ const { t } = useTranslation('plugin__netobserv-plugin');
+
+ const setLayout = (layout: LayoutName) => {
+ setTopologyOptions({
+ ...topologyOptions,
+ layout
+ });
+ };
+
+ const setGroupType = (groupTypes: TopologyGroupTypes) => {
+ setTopologyOptions({
+ ...topologyOptions,
+ groupTypes
+ });
+ };
+
+ const setTruncateLength = (truncateLength: TruncateLength) => {
+ setTopologyOptions({
+ ...topologyOptions,
+ truncateLength
+ });
+ };
+
+ return (
+ <>
+
+
+
+
+ {t('Edge labels')}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {t('Scope')}
+
+
+
+
+
+
+
+
+
+
+
+ {t('Groups')}
+
+
+
+
+
+
+
+
+
+
+
+ {t('Layout')}
+
+
+
+
+
+
+
+
+
+
+
+ {t('Show')}
+
+
+
+
+ setTopologyOptions({
+ ...topologyOptions,
+ edges: !topologyOptions.edges
+ })
+ }
+ />
+
+ setTopologyOptions({
+ ...topologyOptions,
+ edgeTags: !topologyOptions.edgeTags
+ })
+ }
+ />
+
+ setTopologyOptions({
+ ...topologyOptions,
+ nodeBadges: !topologyOptions.nodeBadges
+ })
+ }
+ />
+
+
+
+
+
+ {t('Truncate labels')}
+
+
+
+
+
+
+
+
+
+
+ setTopologyOptions({
+ ...topologyOptions,
+ startCollapsed: !topologyOptions.startCollapsed
+ })
+ }
+ isReversed
+ />
+
+
+ >
+ );
+};
+
+export default TopologyDisplayOptions;
diff --git a/web/src/components/netflow-topology/element-panel-content.tsx b/web/src/components/netflow-topology/element-panel-content.tsx
new file mode 100644
index 000000000..8af8dc627
--- /dev/null
+++ b/web/src/components/netflow-topology/element-panel-content.tsx
@@ -0,0 +1,155 @@
+import {
+ Accordion,
+ AccordionContent,
+ AccordionItem,
+ AccordionToggle,
+ Button,
+ Divider,
+ Flex,
+ FlexItem,
+ Text,
+ TextContent,
+ TextVariants
+} from '@patternfly/react-core';
+import { FilterIcon, TimesIcon } from '@patternfly/react-icons';
+import { BaseEdge, BaseNode } from '@patternfly/react-topology';
+import * as React from 'react';
+import { useTranslation } from 'react-i18next';
+import { Filter, FilterDefinition } from '../../model/filters';
+import { GraphElementPeer, isElementFiltered, NodeData, toggleElementFilter } from '../../model/topology';
+import { createPeer } from '../../utils/metrics';
+import { ElementFields } from './element-fields';
+
+export interface ElementPanelContentProps {
+ element: GraphElementPeer;
+ filters: Filter[];
+ setFilters: (filters: Filter[]) => void;
+ filterDefinitions: FilterDefinition[];
+}
+
+export const ElementPanelContent: React.FC = ({
+ element,
+ filters,
+ setFilters,
+ filterDefinitions
+}) => {
+ const { t } = useTranslation('plugin__netobserv-plugin');
+ const [hidden, setHidden] = React.useState([]);
+ const data = element.getData();
+
+ const toggle = React.useCallback(
+ (id: string) => {
+ const index = hidden.indexOf(id);
+ const newExpanded: string[] =
+ index >= 0 ? [...hidden.slice(0, index), ...hidden.slice(index + 1, hidden.length)] : [...hidden, id];
+ setHidden(newExpanded);
+ },
+ [hidden]
+ );
+
+ const clusterName = React.useCallback(
+ (d: NodeData) => {
+ if (!d.peer.clusterName) {
+ return <>>;
+ }
+ const fields = createPeer({ clusterName: d.peer.clusterName });
+ const isFiltered = isElementFiltered(fields, filters, filterDefinitions);
+ return (
+
+ {t('Cluster name')}
+
+ {d.peer.clusterName}
+
+ : }
+ onClick={() => toggleElementFilter(fields, isFiltered, filters, setFilters, filterDefinitions)}
+ />
+
+
+
+ );
+ },
+ [filterDefinitions, filters, setFilters, t]
+ );
+
+ if (element instanceof BaseNode && data) {
+ return (
+ <>
+ {clusterName(data)}
+
+ >
+ );
+ } else if (element instanceof BaseEdge) {
+ // Edge A to B (prefering neutral naming here as there is no assumption about what is source, what is destination
+ const aData: NodeData = element.getSource().getData();
+ const bData: NodeData = element.getTarget().getData();
+ return (
+
+
+
+ {
+ toggle('source')}
+ isExpanded={!hidden.includes('source')}
+ id={'source'}
+ >
+ {t('Source')}
+
+ }
+
+
+
+
+
+
+
+
+ {
+ toggle('destination')}
+ isExpanded={!hidden.includes('destination')}
+ id={'destination'}
+ >
+ {t('Destination')}
+
+ }
+
+
+
+
+
+
+ );
+ }
+ return <>>;
+};
+
+export default ElementPanelContent;
diff --git a/web/src/components/netflow-topology/element-panel.tsx b/web/src/components/netflow-topology/element-panel.tsx
index 3b1d20a62..b066957ee 100644
--- a/web/src/components/netflow-topology/element-panel.tsx
+++ b/web/src/components/netflow-topology/element-panel.tsx
@@ -1,173 +1,31 @@
import {
- Accordion,
- AccordionContent,
- AccordionItem,
- AccordionToggle,
- Button,
Divider,
DrawerActions,
DrawerCloseButton,
DrawerHead,
DrawerPanelBody,
DrawerPanelContent,
- Flex,
- FlexItem,
Tab,
Tabs,
TabTitleText,
Text,
- TextContent,
TextVariants
} from '@patternfly/react-core';
-import { FilterIcon, TimesIcon } from '@patternfly/react-icons';
-import { BaseEdge, BaseNode } from '@patternfly/react-topology';
+import { BaseEdge } from '@patternfly/react-topology';
import _ from 'lodash';
import * as React from 'react';
import { useTranslation } from 'react-i18next';
import { TopologyMetrics } from '../../api/loki';
import { Filter, FilterDefinition } from '../../model/filters';
import { MetricType } from '../../model/flow-query';
-import { GraphElementPeer, isElementFiltered, NodeData, toggleElementFilter } from '../../model/topology';
-import { createPeer } from '../../utils/metrics';
+import { GraphElementPeer, NodeData } from '../../model/topology';
import { defaultSize, maxSize, minSize } from '../../utils/panel';
import { TruncateLength } from '../dropdowns/truncate-dropdown';
-import { ElementFields } from './element-fields';
+import { ElementPanelContent } from './element-panel-content';
import { ElementPanelMetrics } from './element-panel-metrics';
import './element-panel.css';
import { PeerResourceLink } from './peer-resource-link';
-export interface ElementPanelDetailsContentProps {
- element: GraphElementPeer;
- filters: Filter[];
- setFilters: (filters: Filter[]) => void;
- filterDefinitions: FilterDefinition[];
-}
-
-export const ElementPanelDetailsContent: React.FC = ({
- element,
- filters,
- setFilters,
- filterDefinitions
-}) => {
- const { t } = useTranslation('plugin__netobserv-plugin');
- const [hidden, setHidden] = React.useState([]);
- const data = element.getData();
-
- const toggle = React.useCallback(
- (id: string) => {
- const index = hidden.indexOf(id);
- const newExpanded: string[] =
- index >= 0 ? [...hidden.slice(0, index), ...hidden.slice(index + 1, hidden.length)] : [...hidden, id];
- setHidden(newExpanded);
- },
- [hidden]
- );
-
- const clusterName = React.useCallback(
- (d: NodeData) => {
- if (!d.peer.clusterName) {
- return <>>;
- }
- const fields = createPeer({ clusterName: d.peer.clusterName });
- const isFiltered = isElementFiltered(fields, filters, filterDefinitions);
- return (
-
- {t('Cluster name')}
-
- {d.peer.clusterName}
-
- : }
- onClick={() => toggleElementFilter(fields, isFiltered, filters, setFilters, filterDefinitions)}
- />
-
-
-
- );
- },
- [filterDefinitions, filters, setFilters, t]
- );
-
- if (element instanceof BaseNode && data) {
- return (
- <>
- {clusterName(data)}
-
- >
- );
- } else if (element instanceof BaseEdge) {
- // Edge A to B (prefering neutral naming here as there is no assumption about what is source, what is destination
- const aData: NodeData = element.getSource().getData();
- const bData: NodeData = element.getTarget().getData();
- return (
-
-
-
- {
- toggle('source')}
- isExpanded={!hidden.includes('source')}
- id={'source'}
- >
- {t('Source')}
-
- }
-
-
-
-
-
-
-
-
- {
- toggle('destination')}
- isExpanded={!hidden.includes('destination')}
- id={'destination'}
- >
- {t('Destination')}
-
- }
-
-
-
-
-
-
- );
- }
- return <>>;
-};
-
export interface ElementPanelProps {
onClose: () => void;
element: GraphElementPeer;
@@ -249,7 +107,7 @@ export const ElementPanel: React.FC = ({
role="region"
>
{t('Details')}}>
- ', () => {
const now = new Date();
diff --git a/web/src/components/query-summary/__tests__/metrics-query-summary.spec.tsx b/web/src/components/query-summary/__tests__/metrics-query-summary.spec.tsx
index e6343ca0b..93310457a 100644
--- a/web/src/components/query-summary/__tests__/metrics-query-summary.spec.tsx
+++ b/web/src/components/query-summary/__tests__/metrics-query-summary.spec.tsx
@@ -2,7 +2,8 @@ import { mount, shallow } from 'enzyme';
import * as React from 'react';
import { NetflowMetrics } from '../../../api/loki';
import { metrics } from '../../__tests-data__/metrics';
-import { MetricsQuerySummary, MetricsQuerySummaryContent } from '../metrics-query-summary';
+import { MetricsQuerySummary } from '../metrics-query-summary';
+import { MetricsQuerySummaryContent } from '../metrics-query-summary-content';
describe(' ', () => {
const now = new Date();
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 c6b8df2c5..e70c6deea 100644
--- a/web/src/components/query-summary/__tests__/summary-panel.spec.tsx
+++ b/web/src/components/query-summary/__tests__/summary-panel.spec.tsx
@@ -4,7 +4,8 @@ import * as React from 'react';
import { NetflowMetrics } from 'src/api/loki';
import { FlowsSample } from '../../../components/__tests-data__/flows';
import { RecordType } from '../../../model/flow-query';
-import SummaryPanel, { SummaryPanelContent } from '../summary-panel';
+import { SummaryPanel } from '../summary-panel';
+import { SummaryPanelContent } from '../summary-panel-content';
describe(' ', () => {
const now = new Date();
diff --git a/web/src/components/query-summary/flows-query-summary-content.tsx b/web/src/components/query-summary/flows-query-summary-content.tsx
new file mode 100644
index 000000000..8d7ef90b9
--- /dev/null
+++ b/web/src/components/query-summary/flows-query-summary-content.tsx
@@ -0,0 +1,155 @@
+import { Flex, FlexItem, Text, TextVariants, Tooltip } from '@patternfly/react-core';
+import { InfoCircleIcon } from '@patternfly/react-icons';
+import _ from 'lodash';
+import * as React from 'react';
+import { useTranslation } from 'react-i18next';
+import { Record } from '../../api/ipfix';
+import { RecordType } from '../../model/flow-query';
+import { rangeToSeconds, TimeRange } from '../../utils/datetime';
+import { valueFormat } from '../../utils/format';
+import StatsQuerySummary from './stats-query-summary';
+
+export interface FlowsQuerySummaryContentProps {
+ flows: Record[];
+ type: RecordType;
+ numQueries?: number;
+ limitReached: boolean;
+ range: number | TimeRange;
+ loading?: boolean;
+ lastRefresh?: Date;
+ lastDuration?: number;
+ warningMessage?: string;
+ slownessReason?: string;
+ direction: 'row' | 'column';
+ className?: string;
+ isShowQuerySummary?: boolean;
+ toggleQuerySummary?: () => void;
+}
+
+export const FlowsQuerySummaryContent: React.FC = ({
+ flows,
+ type,
+ numQueries,
+ limitReached,
+ range,
+ loading,
+ lastRefresh,
+ lastDuration,
+ warningMessage,
+ slownessReason,
+ direction,
+ className,
+ isShowQuerySummary,
+ toggleQuerySummary
+}) => {
+ const { t } = useTranslation('plugin__netobserv-plugin');
+ const filteredFlows = flows.filter(
+ r => !r.labels._RecordType || ['endConnection', 'flowLog'].includes(r.labels._RecordType)
+ );
+
+ const rangeInSeconds = rangeToSeconds(range);
+
+ const counters = React.useCallback(() => {
+ const bytes = filteredFlows.map(f => f.fields.Bytes || 0).reduce((a, b) => a + b, 0);
+ const packets = filteredFlows.map(f => f.fields.Packets || 0).reduce((a, b) => a + b, 0);
+
+ return (
+ <>
+ {bytes > 0 && (
+
+ {t('Filtered sum of bytes')}}>
+
+ {valueFormat(bytes, 0, t('B'), limitReached)}
+
+
+
+ )}
+ {packets > 0 && (
+
+ {t('Filtered sum of packets')}}>
+
+ {valueFormat(packets, 0, t('Packets'), limitReached, true)}
+
+
+
+ )}
+ {bytes > 0 && (
+
+ {t('Filtered average speed')}}>
+
+ {valueFormat(bytes / rangeInSeconds, 2, t('Bps'), limitReached)}
+
+
+
+ )}
+ >
+ );
+ }, [filteredFlows, limitReached, rangeInSeconds, t]);
+
+ return (
+
+ {direction === 'row' && (
+
+
+ {t('Summary')}
+
+
+ )}
+
+ {!_.isEmpty(flows) && (
+
+
+ {limitReached && (
+
+ {t('Query limit reached')}}>
+
+
+
+ )}
+
+
+ {type === 'flowLog' ? t('Filtered flows count') : t('Filtered ended conversations count')}
+
+ }
+ >
+
+ {valueFormat(
+ filteredFlows!.length,
+ 0,
+ type === 'flowLog' ? t('Flows') : t('Ended conversations'),
+ limitReached,
+ true
+ )}
+
+
+
+
+
+ )}
+ {counters()}
+ {direction === 'row' && toggleQuerySummary && (
+
+
+ {isShowQuerySummary ? t('See less') : t('See more')}
+
+
+ )}
+
+ );
+};
+
+export default FlowsQuerySummaryContent;
diff --git a/web/src/components/query-summary/flows-query-summary.tsx b/web/src/components/query-summary/flows-query-summary.tsx
index f4f216c8d..0c8cb6b08 100644
--- a/web/src/components/query-summary/flows-query-summary.tsx
+++ b/web/src/components/query-summary/flows-query-summary.tsx
@@ -1,158 +1,12 @@
-import { Card, Flex, FlexItem, Text, TextVariants, Tooltip } from '@patternfly/react-core';
-import { InfoCircleIcon } from '@patternfly/react-icons';
+import { Card } from '@patternfly/react-core';
import _ from 'lodash';
import * as React from 'react';
-import { useTranslation } from 'react-i18next';
import { Record } from '../../api/ipfix';
import { Stats } from '../../api/loki';
import { RecordType } from '../../model/flow-query';
-import { rangeToSeconds, TimeRange } from '../../utils/datetime';
-import { valueFormat } from '../../utils/format';
+import { TimeRange } from '../../utils/datetime';
+import { FlowsQuerySummaryContent } from './flows-query-summary-content';
import './query-summary.css';
-import StatsQuerySummary from './stats-query-summary';
-
-export interface FlowsQuerySummaryContentProps {
- flows: Record[];
- type: RecordType;
- numQueries?: number;
- limitReached: boolean;
- range: number | TimeRange;
- loading?: boolean;
- lastRefresh?: Date;
- lastDuration?: number;
- warningMessage?: string;
- slownessReason?: string;
- direction: 'row' | 'column';
- className?: string;
- isShowQuerySummary?: boolean;
- toggleQuerySummary?: () => void;
-}
-
-export const FlowsQuerySummaryContent: React.FC = ({
- flows,
- type,
- numQueries,
- limitReached,
- range,
- loading,
- lastRefresh,
- lastDuration,
- warningMessage,
- slownessReason,
- direction,
- className,
- isShowQuerySummary,
- toggleQuerySummary
-}) => {
- const { t } = useTranslation('plugin__netobserv-plugin');
- const filteredFlows = flows.filter(
- r => !r.labels._RecordType || ['endConnection', 'flowLog'].includes(r.labels._RecordType)
- );
-
- const rangeInSeconds = rangeToSeconds(range);
-
- const counters = React.useCallback(() => {
- const bytes = filteredFlows.map(f => f.fields.Bytes || 0).reduce((a, b) => a + b, 0);
- const packets = filteredFlows.map(f => f.fields.Packets || 0).reduce((a, b) => a + b, 0);
-
- return (
- <>
- {bytes > 0 && (
-
- {t('Filtered sum of bytes')}}>
-
- {valueFormat(bytes, 0, t('B'), limitReached)}
-
-
-
- )}
- {packets > 0 && (
-
- {t('Filtered sum of packets')}}>
-
- {valueFormat(packets, 0, t('Packets'), limitReached, true)}
-
-
-
- )}
- {bytes > 0 && (
-
- {t('Filtered average speed')}}>
-
- {valueFormat(bytes / rangeInSeconds, 2, t('Bps'), limitReached)}
-
-
-
- )}
- >
- );
- }, [filteredFlows, limitReached, rangeInSeconds, t]);
-
- return (
-
- {direction === 'row' && (
-
-
- {t('Summary')}
-
-
- )}
-
- {!_.isEmpty(flows) && (
-
-
- {limitReached && (
-
- {t('Query limit reached')}}>
-
-
-
- )}
-
-
- {type === 'flowLog' ? t('Filtered flows count') : t('Filtered ended conversations count')}
-
- }
- >
-
- {valueFormat(
- filteredFlows!.length,
- 0,
- type === 'flowLog' ? t('Flows') : t('Ended conversations'),
- limitReached,
- true
- )}
-
-
-
-
-
- )}
- {counters()}
- {direction === 'row' && toggleQuerySummary && (
-
-
- {isShowQuerySummary ? t('See less') : t('See more')}
-
-
- )}
-
- );
-};
export interface FlowQuerySummaryProps {
flows: Record[];
diff --git a/web/src/components/query-summary/metrics-query-summary-content.tsx b/web/src/components/query-summary/metrics-query-summary-content.tsx
new file mode 100644
index 000000000..b2f22446e
--- /dev/null
+++ b/web/src/components/query-summary/metrics-query-summary-content.tsx
@@ -0,0 +1,261 @@
+import { Flex, FlexItem, Text, TextVariants, Tooltip } from '@patternfly/react-core';
+import { DomainIcon, OutlinedClockIcon, TachometerAltIcon } from '@patternfly/react-icons';
+import _ from 'lodash';
+import * as React from 'react';
+import { useTranslation } from 'react-i18next';
+import { getRateMetricKey, NetflowMetrics } from '../../api/loki';
+import { MetricType } from '../../model/flow-query';
+import { valueFormat } from '../../utils/format';
+import StatsQuerySummary from './stats-query-summary';
+
+const exposedMetrics: MetricType[] = [
+ 'Bytes',
+ 'Packets',
+ 'PktDropBytes',
+ 'PktDropPackets',
+ 'DnsLatencyMs',
+ 'TimeFlowRttNs'
+];
+
+export interface MetricsQuerySummaryContentProps {
+ metrics: NetflowMetrics;
+ numQueries?: number;
+ dataSources?: string[];
+ loading?: boolean;
+ lastRefresh?: Date;
+ lastDuration?: number;
+ warningMessage?: string;
+ slownessReason?: string;
+ direction: 'row' | 'column';
+ className?: string;
+ isShowQuerySummary?: boolean;
+ toggleQuerySummary?: () => void;
+ isDark?: boolean;
+}
+
+export const MetricsQuerySummaryContent: React.FC = ({
+ metrics,
+ numQueries,
+ dataSources,
+ loading,
+ lastRefresh,
+ lastDuration,
+ warningMessage,
+ slownessReason,
+ direction,
+ className,
+ isShowQuerySummary,
+ toggleQuerySummary,
+ isDark
+}) => {
+ const { t } = useTranslation('plugin__netobserv-plugin');
+
+ const getMetrics = React.useCallback(
+ (metricType: MetricType) => {
+ switch (metricType) {
+ case 'Bytes':
+ case 'Packets':
+ return metrics.rateMetrics?.[getRateMetricKey(metricType)];
+ case 'PktDropBytes':
+ case 'PktDropPackets':
+ return metrics.droppedRateMetrics?.[getRateMetricKey(metricType)];
+ case 'DnsLatencyMs':
+ return metrics.dnsLatencyMetrics?.avg;
+ case 'TimeFlowRttNs':
+ return metrics.rttMetrics?.avg;
+ default:
+ return undefined;
+ }
+ },
+ [metrics]
+ );
+
+ const getAppMetrics = React.useCallback(
+ (metricType: MetricType) => {
+ switch (metricType) {
+ case 'Bytes':
+ case 'Packets':
+ return metrics.totalRateMetric?.[getRateMetricKey(metricType)];
+ case 'PktDropBytes':
+ case 'PktDropPackets':
+ return metrics.totalDroppedRateMetric?.[getRateMetricKey(metricType)];
+ case 'DnsLatencyMs':
+ return metrics.totalDnsLatencyMetric?.avg;
+ case 'TimeFlowRttNs':
+ return metrics.totalRttMetric?.avg;
+ default:
+ return undefined;
+ }
+ },
+ [metrics]
+ );
+
+ const counters = React.useCallback(
+ (metricType: MetricType) => {
+ const metrics = getMetrics(metricType);
+ const appMetrics = getAppMetrics(metricType);
+ if (!metrics || _.isEmpty(metrics)) {
+ return undefined;
+ }
+ const avgSum = metrics.map(m => m.stats.avg).reduce((prev, cur) => prev + cur, 0);
+ const absTotal = metrics.map(m => m.stats.total).reduce((prev, cur) => prev + cur, 0);
+
+ switch (metricType) {
+ case 'Bytes':
+ case 'PktDropBytes': {
+ const textColor = metricType === 'PktDropBytes' ? (isDark ? '#C9190B' : '#A30000') : undefined;
+ const droppedText = metricType === 'PktDropBytes' ? ' ' + t('dropped') : '';
+ const textAbs = appMetrics
+ ? `${t('Filtered sum of top-k bytes')}${droppedText} / ${t('Filtered total bytes')}${droppedText}`
+ : `${t('Filtered sum of top-k bytes')}${droppedText}`;
+ const textRate = appMetrics
+ ? `${t('Filtered top-k byte rate')}${droppedText} / ${t('Filtered total byte rate')}${droppedText}`
+ : `${t('Filtered top-k byte rate')}${droppedText}`;
+ const valAbs = appMetrics
+ ? valueFormat(absTotal, 1, t('B')) + ' / ' + valueFormat(appMetrics.stats.total, 1, t('B'))
+ : valueFormat(absTotal, 1, t('B'));
+ const valRate = appMetrics
+ ? valueFormat(avgSum, 2, t('Bps')) + ' / ' + valueFormat(appMetrics.stats.avg, 2, t('Bps'))
+ : valueFormat(avgSum, 2, t('Bps'));
+ return [
+
+ {textAbs}}>
+
+
+ {valAbs}
+
+
+
+ ,
+
+ {textRate}}>
+
+
+
+ {valRate}
+
+
+
+
+ ];
+ }
+ case 'Packets':
+ case 'PktDropPackets': {
+ const textColor = metricType === 'PktDropPackets' ? (isDark ? '#C9190B' : '#A30000') : undefined;
+ const droppedText = metricType === 'PktDropPackets' ? ' ' + t('dropped') : '';
+ const textAbs = appMetrics
+ ? `${t('Filtered sum of top-k packets')}${droppedText} / ${t('Filtered total packets')}${droppedText}`
+ : `${t('Filtered sum of top-k packets')}${droppedText}`;
+ const valAbs =
+ (appMetrics ? `${absTotal} / ${appMetrics.stats.total}` : String(absTotal)) + ' ' + t('Packets');
+ return [
+
+ {textAbs}}>
+
+
+ {valAbs}
+
+
+
+
+ ];
+ }
+ case 'DnsLatencyMs': {
+ const textAvg = appMetrics
+ ? `${t('Filtered avg DNS Latency')} | ${t('Filtered overall avg DNS Latency')}`
+ : t('Filtered avg DNS Latency');
+ const valAvg = appMetrics
+ ? `${valueFormat(avgSum / metrics.length, 2, t('ms'))} | ${valueFormat(appMetrics.stats.avg, 2, t('ms'))}`
+ : String(valueFormat(avgSum / metrics.length, 2, t('ms')));
+ return [
+
+ {textAvg}}>
+
+
+
+ {valAvg}
+
+
+
+
+ ];
+ }
+ case 'TimeFlowRttNs': {
+ const textAvg = appMetrics ? `${t('Filtered avg RTT')} | ${t('Overall avg RTT')}` : t('Filtered avg RTT');
+ const valAvg = appMetrics
+ ? `${valueFormat(avgSum / metrics.length, 2, t('ms'))} | ${valueFormat(appMetrics.stats.avg, 2, t('ms'))}`
+ : String(valueFormat(avgSum / metrics.length, 2, t('ms')));
+ return [
+
+ {textAvg}}>
+
+
+
+ {valAvg}
+
+
+
+
+ ];
+ }
+ default: {
+ return undefined;
+ }
+ }
+ },
+ [getAppMetrics, getMetrics, isDark, t]
+ );
+
+ const metricsToShow = React.useCallback(() => {
+ const filtered = exposedMetrics
+ .filter(mt => getMetrics(mt) !== undefined)
+ .flatMap((mt: MetricType) => counters(mt));
+ // limit the number of metrics to show horizontally since we don't have enough room
+ if (direction === 'row') {
+ return filtered.slice(0, 5);
+ }
+ return filtered;
+ }, [counters, direction, getMetrics]);
+
+ return (
+
+ {direction === 'row' && (
+
+
+ {t('Summary')}
+
+
+ )}
+
+
+
+ {metricsToShow()}
+ {direction === 'row' && toggleQuerySummary && (
+
+
+ {isShowQuerySummary ? t('See less') : t('See more')}
+
+
+ )}
+
+ );
+};
+
+export default MetricsQuerySummaryContent;
diff --git a/web/src/components/query-summary/metrics-query-summary.tsx b/web/src/components/query-summary/metrics-query-summary.tsx
index 5e423ad1b..d2fa1750d 100644
--- a/web/src/components/query-summary/metrics-query-summary.tsx
+++ b/web/src/components/query-summary/metrics-query-summary.tsx
@@ -1,263 +1,8 @@
-import { Card, Flex, FlexItem, Text, TextVariants, Tooltip } from '@patternfly/react-core';
-import { DomainIcon, OutlinedClockIcon, TachometerAltIcon } from '@patternfly/react-icons';
-import _ from 'lodash';
+import { Card } from '@patternfly/react-core';
import * as React from 'react';
-import { useTranslation } from 'react-i18next';
-import { getRateMetricKey, NetflowMetrics, Stats } from '../../api/loki';
-import { MetricType } from '../../model/flow-query';
-import { valueFormat } from '../../utils/format';
+import { NetflowMetrics, Stats } from '../../api/loki';
+import { MetricsQuerySummaryContent } from './metrics-query-summary-content';
import './query-summary.css';
-import StatsQuerySummary from './stats-query-summary';
-
-const exposedMetrics: MetricType[] = [
- 'Bytes',
- 'Packets',
- 'PktDropBytes',
- 'PktDropPackets',
- 'DnsLatencyMs',
- 'TimeFlowRttNs'
-];
-
-export interface MetricsQuerySummaryContentProps {
- metrics: NetflowMetrics;
- numQueries?: number;
- dataSources?: string[];
- loading?: boolean;
- lastRefresh?: Date;
- lastDuration?: number;
- warningMessage?: string;
- slownessReason?: string;
- direction: 'row' | 'column';
- className?: string;
- isShowQuerySummary?: boolean;
- toggleQuerySummary?: () => void;
- isDark?: boolean;
-}
-
-export const MetricsQuerySummaryContent: React.FC = ({
- metrics,
- numQueries,
- dataSources,
- loading,
- lastRefresh,
- lastDuration,
- warningMessage,
- slownessReason,
- direction,
- className,
- isShowQuerySummary,
- toggleQuerySummary,
- isDark
-}) => {
- const { t } = useTranslation('plugin__netobserv-plugin');
-
- const getMetrics = React.useCallback(
- (metricType: MetricType) => {
- switch (metricType) {
- case 'Bytes':
- case 'Packets':
- return metrics.rateMetrics?.[getRateMetricKey(metricType)];
- case 'PktDropBytes':
- case 'PktDropPackets':
- return metrics.droppedRateMetrics?.[getRateMetricKey(metricType)];
- case 'DnsLatencyMs':
- return metrics.dnsLatencyMetrics?.avg;
- case 'TimeFlowRttNs':
- return metrics.rttMetrics?.avg;
- default:
- return undefined;
- }
- },
- [metrics]
- );
-
- const getAppMetrics = React.useCallback(
- (metricType: MetricType) => {
- switch (metricType) {
- case 'Bytes':
- case 'Packets':
- return metrics.totalRateMetric?.[getRateMetricKey(metricType)];
- case 'PktDropBytes':
- case 'PktDropPackets':
- return metrics.totalDroppedRateMetric?.[getRateMetricKey(metricType)];
- case 'DnsLatencyMs':
- return metrics.totalDnsLatencyMetric?.avg;
- case 'TimeFlowRttNs':
- return metrics.totalRttMetric?.avg;
- default:
- return undefined;
- }
- },
- [metrics]
- );
-
- const counters = React.useCallback(
- (metricType: MetricType) => {
- const metrics = getMetrics(metricType);
- const appMetrics = getAppMetrics(metricType);
- if (!metrics || _.isEmpty(metrics)) {
- return undefined;
- }
- const avgSum = metrics.map(m => m.stats.avg).reduce((prev, cur) => prev + cur, 0);
- const absTotal = metrics.map(m => m.stats.total).reduce((prev, cur) => prev + cur, 0);
-
- switch (metricType) {
- case 'Bytes':
- case 'PktDropBytes': {
- const textColor = metricType === 'PktDropBytes' ? (isDark ? '#C9190B' : '#A30000') : undefined;
- const droppedText = metricType === 'PktDropBytes' ? ' ' + t('dropped') : '';
- const textAbs = appMetrics
- ? `${t('Filtered sum of top-k bytes')}${droppedText} / ${t('Filtered total bytes')}${droppedText}`
- : `${t('Filtered sum of top-k bytes')}${droppedText}`;
- const textRate = appMetrics
- ? `${t('Filtered top-k byte rate')}${droppedText} / ${t('Filtered total byte rate')}${droppedText}`
- : `${t('Filtered top-k byte rate')}${droppedText}`;
- const valAbs = appMetrics
- ? valueFormat(absTotal, 1, t('B')) + ' / ' + valueFormat(appMetrics.stats.total, 1, t('B'))
- : valueFormat(absTotal, 1, t('B'));
- const valRate = appMetrics
- ? valueFormat(avgSum, 2, t('Bps')) + ' / ' + valueFormat(appMetrics.stats.avg, 2, t('Bps'))
- : valueFormat(avgSum, 2, t('Bps'));
- return [
-
- {textAbs}}>
-
-
- {valAbs}
-
-
-
- ,
-
- {textRate}}>
-
-
-
- {valRate}
-
-
-
-
- ];
- }
- case 'Packets':
- case 'PktDropPackets': {
- const textColor = metricType === 'PktDropPackets' ? (isDark ? '#C9190B' : '#A30000') : undefined;
- const droppedText = metricType === 'PktDropPackets' ? ' ' + t('dropped') : '';
- const textAbs = appMetrics
- ? `${t('Filtered sum of top-k packets')}${droppedText} / ${t('Filtered total packets')}${droppedText}`
- : `${t('Filtered sum of top-k packets')}${droppedText}`;
- const valAbs =
- (appMetrics ? `${absTotal} / ${appMetrics.stats.total}` : String(absTotal)) + ' ' + t('Packets');
- return [
-
- {textAbs}}>
-
-
- {valAbs}
-
-
-
-
- ];
- }
- case 'DnsLatencyMs': {
- const textAvg = appMetrics
- ? `${t('Filtered avg DNS Latency')} | ${t('Filtered overall avg DNS Latency')}`
- : t('Filtered avg DNS Latency');
- const valAvg = appMetrics
- ? `${valueFormat(avgSum / metrics.length, 2, t('ms'))} | ${valueFormat(appMetrics.stats.avg, 2, t('ms'))}`
- : String(valueFormat(avgSum / metrics.length, 2, t('ms')));
- return [
-
- {textAvg}}>
-
-
-
- {valAvg}
-
-
-
-
- ];
- }
- case 'TimeFlowRttNs': {
- const textAvg = appMetrics ? `${t('Filtered avg RTT')} | ${t('Overall avg RTT')}` : t('Filtered avg RTT');
- const valAvg = appMetrics
- ? `${valueFormat(avgSum / metrics.length, 2, t('ms'))} | ${valueFormat(appMetrics.stats.avg, 2, t('ms'))}`
- : String(valueFormat(avgSum / metrics.length, 2, t('ms')));
- return [
-
- {textAvg}}>
-
-
-
- {valAvg}
-
-
-
-
- ];
- }
- default: {
- return undefined;
- }
- }
- },
- [getAppMetrics, getMetrics, isDark, t]
- );
-
- const metricsToShow = React.useCallback(() => {
- const filtered = exposedMetrics
- .filter(mt => getMetrics(mt) !== undefined)
- .flatMap((mt: MetricType) => counters(mt));
- // limit the number of metrics to show horizontally since we don't have enough room
- if (direction === 'row') {
- return filtered.slice(0, 5);
- }
- return filtered;
- }, [counters, direction, getMetrics]);
-
- return (
-
- {direction === 'row' && (
-
-
- {t('Summary')}
-
-
- )}
-
-
-
- {metricsToShow()}
- {direction === 'row' && toggleQuerySummary && (
-
-
- {isShowQuerySummary ? t('See less') : t('See more')}
-
-
- )}
-
- );
-};
export interface MetricsQuerySummaryProps {
stats?: Stats;
diff --git a/web/src/components/query-summary/summary-panel-content.tsx b/web/src/components/query-summary/summary-panel-content.tsx
new file mode 100644
index 000000000..5fcf2cdc7
--- /dev/null
+++ b/web/src/components/query-summary/summary-panel-content.tsx
@@ -0,0 +1,447 @@
+import { ResourceLink } from '@openshift-console/dynamic-plugin-sdk';
+import {
+ Accordion,
+ AccordionContent,
+ AccordionExpandedContentBody,
+ AccordionItem,
+ AccordionToggle,
+ Text,
+ TextContent,
+ TextVariants
+} from '@patternfly/react-core';
+import _ from 'lodash';
+import * as React from 'react';
+import { useTranslation } from 'react-i18next';
+import { Record } from '../../api/ipfix';
+import { NetflowMetrics, Stats } from '../../api/loki';
+import { RecordType } from '../../model/flow-query';
+import { compareStrings } from '../../utils/base-compare';
+import { config } from '../../utils/config';
+import { TimeRange } from '../../utils/datetime';
+import { formatDuration, formatDurationAboveMillisecond, formatDurationAboveNanosecond } from '../../utils/duration';
+import { compareIPs } from '../../utils/ip';
+import { comparePorts, formatPort } from '../../utils/port';
+import { formatProtocol } from '../../utils/protocol';
+import { FlowsQuerySummaryContent } from './flows-query-summary-content';
+import { MetricsQuerySummaryContent } from './metrics-query-summary-content';
+
+type TypeCardinality = {
+ type: string;
+ objects: K8SObjectCardinality[];
+};
+
+type K8SObjectCardinality = {
+ namespace?: string;
+ names: string[];
+};
+
+export interface SummaryPanelContentProps {
+ flows?: Record[];
+ metrics: NetflowMetrics;
+ type: RecordType;
+ stats?: Stats;
+ maxChunkAge: number;
+ limit: number;
+ range: number | TimeRange;
+ lastRefresh?: Date;
+ lastDuration?: number;
+ warningMessage?: string;
+ slownessReason?: string;
+ showDNSLatency?: boolean;
+ showRTTLatency?: boolean;
+}
+
+export const SummaryPanelContent: React.FC = ({
+ flows,
+ metrics,
+ type,
+ stats,
+ maxChunkAge,
+ limit,
+ range,
+ lastRefresh,
+ lastDuration,
+ warningMessage,
+ slownessReason,
+ showDNSLatency,
+ showRTTLatency
+}) => {
+ const { t } = useTranslation('plugin__netobserv-plugin');
+ const [expanded, setExpanded] = React.useState('');
+
+ const accordionItem = (id: string, label: string, content: JSX.Element) => {
+ return (
+
+ {
+ if (id === expanded) {
+ setExpanded('');
+ } else {
+ setExpanded(id);
+ }
+ }}
+ isExpanded={expanded === id}
+ id={id}
+ >
+ {label}
+
+
+ {content}
+
+
+ );
+ };
+
+ const typeCardinalityContent = (tc: TypeCardinality) => {
+ return (
+ <>
+ {tc.objects
+ .sort((a, b) => compareStrings(a.namespace ? a.namespace : '', b.namespace ? b.namespace : ''))
+ .flatMap(o => (
+
+ {o.namespace && }
+ {o.names
+ .sort((a, b) => compareStrings(a, b))
+ .map(n => (
+
+ ))}
+
+ ))}
+ >
+ );
+ };
+
+ const listCardinalityContent = (
+ values: (string | number)[],
+ compareFn?: (a: string | number, b: string | number) => number
+ ) => {
+ const sortedStrings = compareFn
+ ? (values.sort((a: string | number, b: string | number) => compareFn(a, b)) as string[])
+ : values;
+ return (
+ <>
+ {sortedStrings.map((v: string) => (
+
+ {v}
+
+ ))}
+ >
+ );
+ };
+
+ const configContent = () => {
+ return (
+
+ {`${t('Configuration')}`}
+ {`${t('Sampling')}: ${config.sampling}`}
+ {!Number.isNaN(maxChunkAge) && (
+ {`${t('Max chunk age')}: ${formatDuration(maxChunkAge)}`}
+ )}
+
+ );
+ };
+
+ const versionContent = () => {
+ return (
+
+ {`${t('Version')}`}
+ {`${t('Number')}: ${config.buildVersion}`}
+ {`${t('Date')}: ${config.buildDate}`}
+
+ );
+ };
+
+ const cardinalityContent = () => {
+ const rateMetrics = !_.isEmpty(metrics.rateMetrics?.bytes)
+ ? metrics.rateMetrics!.bytes!
+ : !_.isEmpty(metrics.rateMetrics?.packets)
+ ? metrics.rateMetrics!.packets!
+ : [];
+
+ //regroup all k8s objects per type + namespace
+ const namespaces: string[] = [];
+ const typesCardinality: TypeCardinality[] = [];
+ let addresses: string[] = [];
+ let ports: number[] = [];
+ let protocols: number[] = [];
+
+ if (flows && flows.length) {
+ //list all types
+ const types = Array.from(new Set(flows.flatMap(f => [f.labels.SrcK8S_Type, f.labels.DstK8S_Type])));
+ types
+ .filter((t: string | undefined) => t !== undefined)
+ .forEach((type: string) => {
+ const tc: TypeCardinality = { type, objects: [] };
+
+ const typeFilteredFlows = flows.filter(f => [f.labels.SrcK8S_Type, f.labels.DstK8S_Type].includes(type));
+ //list all namespaces of this type
+ const typeNamespaces = new Set(
+ typeFilteredFlows.flatMap(f => [f.labels.SrcK8S_Namespace, f.labels.DstK8S_Namespace])
+ );
+ typeNamespaces.forEach(namespace => {
+ const namespaceFilteredFlows = typeFilteredFlows.filter(f =>
+ [f.labels.SrcK8S_Namespace, f.labels.DstK8S_Namespace].includes(namespace)
+ );
+
+ const nsObject: K8SObjectCardinality = {
+ namespace,
+ names: []
+ };
+
+ //add all names of this namespace of type
+ namespaceFilteredFlows.forEach(record => {
+ const srcName =
+ record.labels.SrcK8S_Type === type && record.labels.SrcK8S_Namespace === namespace
+ ? record.fields.SrcK8S_Name
+ : undefined;
+ if (srcName && !nsObject.names.includes(srcName)) {
+ nsObject.names.push(srcName);
+ }
+ const dstName =
+ record.labels.DstK8S_Type === type && record.labels.DstK8S_Namespace === namespace
+ ? record.fields.DstK8S_Name
+ : undefined;
+ if (dstName && !nsObject.names.includes(dstName)) {
+ nsObject.names.push(dstName);
+ }
+ });
+
+ if (!_.isEmpty(nsObject.names)) {
+ tc.objects.push(nsObject);
+ }
+
+ if (namespace && !namespaces.includes(namespace)) {
+ namespaces.push(namespace);
+ }
+ });
+ typesCardinality.push(tc);
+ });
+
+ addresses = Array.from(
+ new Set(flows.map(f => f.fields.SrcAddr || '').concat(flows.map(f => f.fields.DstAddr || '')))
+ );
+ ports = Array.from(
+ new Set(
+ flows
+ .filter(f => f.fields.SrcPort)
+ .map(f => f.fields.SrcPort)
+ .concat(flows.filter(f => f.fields.DstPort).map(f => f.fields.DstPort)) as number[]
+ )
+ );
+ protocols = Array.from(new Set(flows.map(f => f.fields.Proto || NaN)));
+ } else if (rateMetrics) {
+ function manageTypeCardinality(hostName?: string, namespace?: string, type?: string, name?: string) {
+ if (namespace && !namespaces.includes(namespace)) {
+ namespaces.push(namespace);
+ }
+
+ if (type) {
+ let tc = typesCardinality.find(t => t.type === type);
+ if (!tc) {
+ tc = { type: type, objects: [] };
+ typesCardinality.push(tc);
+ }
+
+ let object = tc.objects.find(o => o.namespace === namespace);
+ if (!object) {
+ object = { names: [], namespace: namespace };
+ tc.objects.push(object);
+ }
+
+ if (name && !object.names.includes(name)) {
+ object.names.push(name);
+ }
+ }
+
+ if (hostName) {
+ manageTypeCardinality('', '', 'Node', hostName);
+ }
+ }
+
+ rateMetrics.forEach(m => {
+ manageTypeCardinality(m.source.hostName, m.source.namespace, m.source.resource?.type, m.source.resource?.name);
+ manageTypeCardinality(
+ m.destination.hostName,
+ m.destination.namespace,
+ m.destination.resource?.type,
+ m.destination.resource?.name
+ );
+ });
+
+ addresses = Array.from(
+ new Set(rateMetrics.map(m => m.source.addr).concat(rateMetrics.map(m => m.destination.addr)))
+ ).filter(v => !_.isEmpty(v)) as string[];
+ }
+
+ if (!_.isEmpty(namespaces)) {
+ typesCardinality.push({
+ type: 'Namespace',
+ objects: [{ names: namespaces }]
+ });
+ }
+
+ return addresses.length || typesCardinality.length || ports.length || protocols.length ? (
+
+ {`${t('Cardinality')} ${
+ !_.isEmpty(rateMetrics) ? t('(top {{count}} metrics)', { count: limit }) : ''
+ }`}
+
+ {addresses.length
+ ? accordionItem(
+ 'addresses',
+ t('{{count}} IP(s)', { count: addresses.length }),
+ listCardinalityContent(addresses, compareIPs)
+ )
+ : undefined}
+ {typesCardinality.length
+ ? typesCardinality.map(tc =>
+ accordionItem(
+ tc.type,
+ `${tc.objects.map(o => o.names.length).reduce((a, b) => a + b, 0)} ${tc.type}(s)`,
+ typeCardinalityContent(tc)
+ )
+ )
+ : undefined}
+ {ports.length
+ ? accordionItem(
+ 'ports',
+ t('{{count}} Port(s)', { count: ports.length }),
+ listCardinalityContent(
+ //sort ports before format to keep number order
+ ports.sort((p1, p2) => comparePorts(p1, p2)).map(p => formatPort(p))
+ )
+ )
+ : undefined}
+ {protocols.length
+ ? accordionItem(
+ 'protocols',
+ t('{{count}} Protocol(s)', { count: protocols.length }),
+ listCardinalityContent(
+ protocols.map(p => formatProtocol(p, t)),
+ compareStrings
+ )
+ )
+ : undefined}
+
+
+ ) : undefined;
+ };
+
+ const dnsLatency = (filteredFlows: Record[]) => {
+ const filteredDNSFlows = filteredFlows.filter(f => f.fields.DnsLatencyMs !== undefined);
+
+ const dnsLatency = filteredDNSFlows.length
+ ? filteredDNSFlows.map(f => f.fields.DnsLatencyMs!).reduce((a, b) => a + b, 0) / filteredDNSFlows.length
+ : NaN;
+
+ return (
+ {`${t('DNS latency')}: ${
+ isNaN(dnsLatency) ? t('n/a') : formatDurationAboveMillisecond(dnsLatency)
+ }`}
+ );
+ };
+
+ const rttLatency = (filteredFlows: Record[]) => {
+ const filteredRTTFlows = filteredFlows.filter(f => f.fields.TimeFlowRttNs !== undefined);
+
+ const rtt = filteredRTTFlows.length
+ ? filteredRTTFlows.map(f => f.fields.TimeFlowRttNs!).reduce((a, b) => a + b, 0) / filteredRTTFlows.length
+ : NaN;
+
+ return (
+ {`${t('Flow RTT')}: ${
+ isNaN(rtt) ? t('n/a') : formatDurationAboveNanosecond(rtt)
+ }`}
+ );
+ };
+
+ const timeContent = () => {
+ const filteredFlows = flows || [];
+ const duration = filteredFlows.length
+ ? filteredFlows
+ .map(f => (f.fields.TimeFlowEndMs || 0) - (f.fields.TimeFlowStartMs || 0))
+ .reduce((a, b) => a + b, 0) / filteredFlows.length
+ : 0;
+ const collectionLatency = filteredFlows.length
+ ? filteredFlows
+ .map(f => (f.fields.TimeReceived || 0) * 1000 - (f.fields.TimeFlowEndMs || 0))
+ .reduce((a, b) => a + b, 0) / filteredFlows.length
+ : 0;
+
+ return flows && flows.length ? (
+
+ {`${t('Average time')}`}
+ {`${t('Duration')}: ${formatDurationAboveMillisecond(duration)}`}
+ {showRTTLatency ? rttLatency(filteredFlows) : <>>}
+ {`${t('Collection latency')}: ${formatDurationAboveMillisecond(
+ collectionLatency
+ )}`}
+ {showDNSLatency ? dnsLatency(filteredFlows) : <>>}
+
+ ) : (
+ <>>
+ );
+ };
+
+ return (
+ <>
+
+ {!_.isEmpty(flows) && stats?.limitReached && (
+
+ {t(
+ // eslint-disable-next-line max-len
+ 'Flow per request limit reached, following metrics can be inaccurate. Narrow down your search or increase limit.'
+ )}
+
+ )}
+ {`${t('Results')} ${
+ _.isEmpty(flows) ? t('(top {{count}} metrics)', { count: limit }) : ''
+ }`}
+ {_.isEmpty(flows) ? (
+
+ ) : (
+
+ )}
+
+
+ {timeContent()}
+
+ {cardinalityContent()}
+ {/*TODO: NETOBSERV-225 for extra stats on query*/}
+
+ {configContent()}
+
+ {versionContent()}
+ >
+ );
+};
+
+export default SummaryPanelContent;
diff --git a/web/src/components/query-summary/summary-panel.tsx b/web/src/components/query-summary/summary-panel.tsx
index 542379104..cf5e7409d 100644
--- a/web/src/components/query-summary/summary-panel.tsx
+++ b/web/src/components/query-summary/summary-panel.tsx
@@ -1,456 +1,22 @@
-import { ResourceLink } from '@openshift-console/dynamic-plugin-sdk';
import {
- Accordion,
- AccordionContent,
- AccordionExpandedContentBody,
- AccordionItem,
- AccordionToggle,
DrawerActions,
DrawerCloseButton,
DrawerHead,
DrawerPanelBody,
DrawerPanelContent,
Text,
- TextContent,
TextVariants
} from '@patternfly/react-core';
-import _ from 'lodash';
import * as React from 'react';
import { useTranslation } from 'react-i18next';
import { Record } from '../../api/ipfix';
import { NetflowMetrics, Stats } from '../../api/loki';
import { RecordType } from '../../model/flow-query';
-import { compareStrings } from '../../utils/base-compare';
-import { config } from '../../utils/config';
import { TimeRange } from '../../utils/datetime';
-import { formatDuration, formatDurationAboveMillisecond, formatDurationAboveNanosecond } from '../../utils/duration';
-import { compareIPs } from '../../utils/ip';
import { defaultSize, maxSize, minSize } from '../../utils/panel';
-import { comparePorts, formatPort } from '../../utils/port';
-import { formatProtocol } from '../../utils/protocol';
-import { FlowsQuerySummaryContent } from './flows-query-summary';
-import { MetricsQuerySummaryContent } from './metrics-query-summary';
+import { SummaryPanelContent } from './summary-panel-content';
import './summary-panel.css';
-type TypeCardinality = {
- type: string;
- objects: K8SObjectCardinality[];
-};
-
-type K8SObjectCardinality = {
- namespace?: string;
- names: string[];
-};
-
-export interface SummaryPanelContentProps {
- flows?: Record[];
- metrics: NetflowMetrics;
- type: RecordType;
- stats?: Stats;
- maxChunkAge: number;
- limit: number;
- range: number | TimeRange;
- lastRefresh?: Date;
- lastDuration?: number;
- warningMessage?: string;
- slownessReason?: string;
- showDNSLatency?: boolean;
- showRTTLatency?: boolean;
-}
-
-export const SummaryPanelContent: React.FC = ({
- flows,
- metrics,
- type,
- stats,
- maxChunkAge,
- limit,
- range,
- lastRefresh,
- lastDuration,
- warningMessage,
- slownessReason,
- showDNSLatency,
- showRTTLatency
-}) => {
- const { t } = useTranslation('plugin__netobserv-plugin');
- const [expanded, setExpanded] = React.useState('');
-
- const accordionItem = (id: string, label: string, content: JSX.Element) => {
- return (
-
- {
- if (id === expanded) {
- setExpanded('');
- } else {
- setExpanded(id);
- }
- }}
- isExpanded={expanded === id}
- id={id}
- >
- {label}
-
-
- {content}
-
-
- );
- };
-
- const typeCardinalityContent = (tc: TypeCardinality) => {
- return (
- <>
- {tc.objects
- .sort((a, b) => compareStrings(a.namespace ? a.namespace : '', b.namespace ? b.namespace : ''))
- .flatMap(o => (
-
- {o.namespace && }
- {o.names
- .sort((a, b) => compareStrings(a, b))
- .map(n => (
-
- ))}
-
- ))}
- >
- );
- };
-
- const listCardinalityContent = (
- values: (string | number)[],
- compareFn?: (a: string | number, b: string | number) => number
- ) => {
- const sortedStrings = compareFn
- ? (values.sort((a: string | number, b: string | number) => compareFn(a, b)) as string[])
- : values;
- return (
- <>
- {sortedStrings.map((v: string) => (
-
- {v}
-
- ))}
- >
- );
- };
-
- const configContent = () => {
- return (
-
- {`${t('Configuration')}`}
- {`${t('Sampling')}: ${config.sampling}`}
- {!Number.isNaN(maxChunkAge) && (
- {`${t('Max chunk age')}: ${formatDuration(maxChunkAge)}`}
- )}
-
- );
- };
-
- const versionContent = () => {
- return (
-
- {`${t('Version')}`}
- {`${t('Number')}: ${config.buildVersion}`}
- {`${t('Date')}: ${config.buildDate}`}
-
- );
- };
-
- const cardinalityContent = () => {
- const rateMetrics = !_.isEmpty(metrics.rateMetrics?.bytes)
- ? metrics.rateMetrics!.bytes!
- : !_.isEmpty(metrics.rateMetrics?.packets)
- ? metrics.rateMetrics!.packets!
- : [];
-
- //regroup all k8s objects per type + namespace
- const namespaces: string[] = [];
- const typesCardinality: TypeCardinality[] = [];
- let addresses: string[] = [];
- let ports: number[] = [];
- let protocols: number[] = [];
-
- if (flows && flows.length) {
- //list all types
- const types = Array.from(new Set(flows.flatMap(f => [f.labels.SrcK8S_Type, f.labels.DstK8S_Type])));
- types
- .filter((t: string | undefined) => t !== undefined)
- .forEach((type: string) => {
- const tc: TypeCardinality = { type, objects: [] };
-
- const typeFilteredFlows = flows.filter(f => [f.labels.SrcK8S_Type, f.labels.DstK8S_Type].includes(type));
- //list all namespaces of this type
- const typeNamespaces = new Set(
- typeFilteredFlows.flatMap(f => [f.labels.SrcK8S_Namespace, f.labels.DstK8S_Namespace])
- );
- typeNamespaces.forEach(namespace => {
- const namespaceFilteredFlows = typeFilteredFlows.filter(f =>
- [f.labels.SrcK8S_Namespace, f.labels.DstK8S_Namespace].includes(namespace)
- );
-
- const nsObject: K8SObjectCardinality = {
- namespace,
- names: []
- };
-
- //add all names of this namespace of type
- namespaceFilteredFlows.forEach(record => {
- const srcName =
- record.labels.SrcK8S_Type === type && record.labels.SrcK8S_Namespace === namespace
- ? record.fields.SrcK8S_Name
- : undefined;
- if (srcName && !nsObject.names.includes(srcName)) {
- nsObject.names.push(srcName);
- }
- const dstName =
- record.labels.DstK8S_Type === type && record.labels.DstK8S_Namespace === namespace
- ? record.fields.DstK8S_Name
- : undefined;
- if (dstName && !nsObject.names.includes(dstName)) {
- nsObject.names.push(dstName);
- }
- });
-
- if (!_.isEmpty(nsObject.names)) {
- tc.objects.push(nsObject);
- }
-
- if (namespace && !namespaces.includes(namespace)) {
- namespaces.push(namespace);
- }
- });
- typesCardinality.push(tc);
- });
-
- addresses = Array.from(
- new Set(flows.map(f => f.fields.SrcAddr || '').concat(flows.map(f => f.fields.DstAddr || '')))
- );
- ports = Array.from(
- new Set(
- flows
- .filter(f => f.fields.SrcPort)
- .map(f => f.fields.SrcPort)
- .concat(flows.filter(f => f.fields.DstPort).map(f => f.fields.DstPort)) as number[]
- )
- );
- protocols = Array.from(new Set(flows.map(f => f.fields.Proto || NaN)));
- } else if (rateMetrics) {
- function manageTypeCardinality(hostName?: string, namespace?: string, type?: string, name?: string) {
- if (namespace && !namespaces.includes(namespace)) {
- namespaces.push(namespace);
- }
-
- if (type) {
- let tc = typesCardinality.find(t => t.type === type);
- if (!tc) {
- tc = { type: type, objects: [] };
- typesCardinality.push(tc);
- }
-
- let object = tc.objects.find(o => o.namespace === namespace);
- if (!object) {
- object = { names: [], namespace: namespace };
- tc.objects.push(object);
- }
-
- if (name && !object.names.includes(name)) {
- object.names.push(name);
- }
- }
-
- if (hostName) {
- manageTypeCardinality('', '', 'Node', hostName);
- }
- }
-
- rateMetrics.forEach(m => {
- manageTypeCardinality(m.source.hostName, m.source.namespace, m.source.resource?.type, m.source.resource?.name);
- manageTypeCardinality(
- m.destination.hostName,
- m.destination.namespace,
- m.destination.resource?.type,
- m.destination.resource?.name
- );
- });
-
- addresses = Array.from(
- new Set(rateMetrics.map(m => m.source.addr).concat(rateMetrics.map(m => m.destination.addr)))
- ).filter(v => !_.isEmpty(v)) as string[];
- }
-
- if (!_.isEmpty(namespaces)) {
- typesCardinality.push({
- type: 'Namespace',
- objects: [{ names: namespaces }]
- });
- }
-
- return addresses.length || typesCardinality.length || ports.length || protocols.length ? (
-
- {`${t('Cardinality')} ${
- !_.isEmpty(rateMetrics) ? t('(top {{count}} metrics)', { count: limit }) : ''
- }`}
-
- {addresses.length
- ? accordionItem(
- 'addresses',
- t('{{count}} IP(s)', { count: addresses.length }),
- listCardinalityContent(addresses, compareIPs)
- )
- : undefined}
- {typesCardinality.length
- ? typesCardinality.map(tc =>
- accordionItem(
- tc.type,
- `${tc.objects.map(o => o.names.length).reduce((a, b) => a + b, 0)} ${tc.type}(s)`,
- typeCardinalityContent(tc)
- )
- )
- : undefined}
- {ports.length
- ? accordionItem(
- 'ports',
- t('{{count}} Port(s)', { count: ports.length }),
- listCardinalityContent(
- //sort ports before format to keep number order
- ports.sort((p1, p2) => comparePorts(p1, p2)).map(p => formatPort(p))
- )
- )
- : undefined}
- {protocols.length
- ? accordionItem(
- 'protocols',
- t('{{count}} Protocol(s)', { count: protocols.length }),
- listCardinalityContent(
- protocols.map(p => formatProtocol(p, t)),
- compareStrings
- )
- )
- : undefined}
-
-
- ) : undefined;
- };
-
- const dnsLatency = (filteredFlows: Record[]) => {
- const filteredDNSFlows = filteredFlows.filter(f => f.fields.DnsLatencyMs !== undefined);
-
- const dnsLatency = filteredDNSFlows.length
- ? filteredDNSFlows.map(f => f.fields.DnsLatencyMs!).reduce((a, b) => a + b, 0) / filteredDNSFlows.length
- : NaN;
-
- return (
- {`${t('DNS latency')}: ${
- isNaN(dnsLatency) ? t('n/a') : formatDurationAboveMillisecond(dnsLatency)
- }`}
- );
- };
-
- const rttLatency = (filteredFlows: Record[]) => {
- const filteredRTTFlows = filteredFlows.filter(f => f.fields.TimeFlowRttNs !== undefined);
-
- const rtt = filteredRTTFlows.length
- ? filteredRTTFlows.map(f => f.fields.TimeFlowRttNs!).reduce((a, b) => a + b, 0) / filteredRTTFlows.length
- : NaN;
-
- return (
- {`${t('Flow RTT')}: ${
- isNaN(rtt) ? t('n/a') : formatDurationAboveNanosecond(rtt)
- }`}
- );
- };
-
- const timeContent = () => {
- const filteredFlows = flows || [];
- const duration = filteredFlows.length
- ? filteredFlows
- .map(f => (f.fields.TimeFlowEndMs || 0) - (f.fields.TimeFlowStartMs || 0))
- .reduce((a, b) => a + b, 0) / filteredFlows.length
- : 0;
- const collectionLatency = filteredFlows.length
- ? filteredFlows
- .map(f => (f.fields.TimeReceived || 0) * 1000 - (f.fields.TimeFlowEndMs || 0))
- .reduce((a, b) => a + b, 0) / filteredFlows.length
- : 0;
-
- return flows && flows.length ? (
-
- {`${t('Average time')}`}
- {`${t('Duration')}: ${formatDurationAboveMillisecond(duration)}`}
- {showRTTLatency ? rttLatency(filteredFlows) : <>>}
- {`${t('Collection latency')}: ${formatDurationAboveMillisecond(
- collectionLatency
- )}`}
- {showDNSLatency ? dnsLatency(filteredFlows) : <>>}
-
- ) : (
- <>>
- );
- };
-
- return (
- <>
-
- {!_.isEmpty(flows) && stats?.limitReached && (
-
- {t(
- // eslint-disable-next-line max-len
- 'Flow per request limit reached, following metrics can be inaccurate. Narrow down your search or increase limit.'
- )}
-
- )}
- {`${t('Results')} ${
- _.isEmpty(flows) ? t('(top {{count}} metrics)', { count: limit }) : ''
- }`}
- {_.isEmpty(flows) ? (
-
- ) : (
-
- )}
-
-
- {timeContent()}
-
- {cardinalityContent()}
- {/*TODO: NETOBSERV-225 for extra stats on query*/}
-
- {configContent()}
-
- {versionContent()}
- >
- );
-};
-
export interface SummaryPanelProps {
onClose: () => void;
flows?: Record[];