Skip to content

Commit

Permalink
NETOBSERV-474 summary filters by source or dest
Browse files Browse the repository at this point in the history
!QE NOTICE: graph node id generation changed!

Allow to filter by source, dest or common via the summary panel filters

This is also adding Owner as part of the information displayed per node.
It modifies the whole topology node data so that the entire
"TopologyMetricPeer" (returned from metrics) object is now part of it.

It also makes metrics matching for groups simpler because the parent information is now contained in the id itself

Also, some refactoring on element-panel to extract new components: element-field(s), peer-resource-link, summary-filter-button
  • Loading branch information
jotak committed Nov 29, 2022
1 parent 786607b commit 72915fe
Show file tree
Hide file tree
Showing 16 changed files with 612 additions and 575 deletions.
6 changes: 3 additions & 3 deletions web/locales/en/plugin__netobserv-plugin.json
Original file line number Diff line number Diff line change
Expand Up @@ -171,8 +171,6 @@
"Add {{name}} filter": "Add {{name}} filter",
"Unpin this element": "Unpin this element",
"Pin this element": "Pin this element",
"Name": "Name",
"Node Name": "Node Name",
"IP": "IP",
"No information available for this content. Change scope to get more details.": "No information available for this content. Change scope to get more details.",
"Source to destination:": "Source to destination:",
Expand All @@ -182,7 +180,6 @@
"Both:": "Both:",
"{{type}} rate": "{{type}} rate",
"Edge": "Edge",
"Unknown": "Unknown",
"Unable to get topology": "Unable to get topology",
"Query is slow": "Query is slow",
"Overview": "Overview",
Expand Down Expand Up @@ -233,15 +230,18 @@
"Query summary": "Query summary",
"Find in view": "Find in view",
"External": "External",
"Unknown": "Unknown",
"Names": "Names",
"Kinds": "Kinds",
"Owner Kinds": "Owner Kinds",
"Ports": "Ports",
"MAC": "MAC",
"Node IP": "Node IP",
"Node Name": "Node Name",
"Kubernetes Objects": "Kubernetes Objects",
"Owner Kubernetes Objects": "Owner Kubernetes Objects",
"IPs & Ports": "IPs & Ports",
"Name": "Name",
"Kind": "Kind",
"Owner Kind": "Owner Kind",
"Port": "Port",
Expand Down
8 changes: 4 additions & 4 deletions web/src/api/loki.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,13 +64,13 @@ export interface RawTopologyMetrics {
}

export interface TopologyMetricPeer {
id: string;
addr?: string;
name?: string;
namespace?: string;
ownerName?: string;
ownerType?: string;
type?: string;
owner?: { name: string; type: string };
resource?: { name: string; type: string };
hostName?: string;
resourceKind?: string;
displayName?: string;
}

Expand Down
57 changes: 57 additions & 0 deletions web/src/components/filters/summary-filter-button.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import * as React from 'react';
import { useTranslation } from 'react-i18next';
import { OptionsMenu, OptionsMenuItem, OptionsMenuPosition, OptionsMenuToggle } from '@patternfly/react-core';
import { FilterIcon } from '@patternfly/react-icons';
import { Filter } from '../../model/filters';
import { FilterDir, isElementFiltered, toggleElementFilter } from '../../model/topology';
import { TopologyMetricPeer } from '../../api/loki';
import { NodeType } from '../../model/flow-query';

export interface SummaryFilterButtonProps {
id: string;
filterType: NodeType;
fields: Partial<TopologyMetricPeer>;
activeFilters: Filter[];
setFilters: (filters: Filter[]) => void;
}

const srcFilter: FilterDir = 'src';
const dstFilter: FilterDir = 'dst';
const anyFilter: FilterDir = 'any';

export const SummaryFilterButton: React.FC<SummaryFilterButtonProps> = ({
id,
filterType,
fields,
activeFilters,
setFilters
}) => {
const { t } = useTranslation('plugin__netobserv-plugin');
const [isOpen, setIsOpen] = React.useState(false);

const selected = [srcFilter, dstFilter, anyFilter].filter(dir =>
isElementFiltered(filterType, fields, dir, activeFilters, t)
);

const onSelect = (dir: FilterDir) => {
toggleElementFilter(filterType, fields, dir, selected.includes(dir), activeFilters, setFilters, t);
};

const menuItem = (id: FilterDir, label: string) => (
<OptionsMenuItem id={id} key={id} isSelected={selected.includes(id)} onSelect={() => onSelect(id)}>
{label}
</OptionsMenuItem>
);

return (
<OptionsMenu
id={id}
data-test={id}
toggle={<OptionsMenuToggle toggleTemplate={<FilterIcon />} onToggle={setIsOpen} hideCaret />}
menuItems={[menuItem('src', t('Source')), menuItem('dst', t('Destination')), menuItem('any', t('Common'))]}
isOpen={isOpen}
position={OptionsMenuPosition.right}
isPlain
/>
);
};
4 changes: 2 additions & 2 deletions web/src/components/metrics/metrics-helper.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -103,8 +103,8 @@ const getPeerName = (
export const toNamedMetric = (
t: TFunction,
m: TopologyMetrics,
data?: NodeData,
truncateLength: TruncateLength = TruncateLength.OFF
truncateLength: TruncateLength,
data?: NodeData
): NamedMetric => {
const srcName = getPeerName(t, m.source, m.scope, truncateLength);
const srcFullName = getPeerName(t, m.source, m.scope);
Expand Down
7 changes: 3 additions & 4 deletions web/src/components/netflow-overview/netflow-overview.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@ import { useTranslation } from 'react-i18next';
import { TopologyMetrics } from '../../api/loki';
import { MetricType } from '../../model/flow-query';
import { getStat } from '../../model/topology';
import { peersEqual } from '../../utils/metrics';
import { getOverviewPanelInfo, OverviewPanel, OverviewPanelId } from '../../utils/overview-panels';
import { TruncateLength } from '../dropdowns/truncate-dropdown';
import LokiError from '../messages/loki-error';
Expand Down Expand Up @@ -101,9 +100,9 @@ export const NetflowOverview: React.FC<NetflowOverviewProps> = ({
//limit to top X since multiple queries can run in parallel
const topKMetrics = metrics
.sort((a, b) => getStat(b.stats, 'sum') - getStat(a.stats, 'sum'))
.map(m => toNamedMetric(t, m, undefined, truncateLength));
const namedTotalMetric = toNamedMetric(t, totalMetric, undefined, truncateLength);
const noInternalTopK = topKMetrics.filter(m => !peersEqual(m.source, m.destination));
.map(m => toNamedMetric(t, m, truncateLength));
const namedTotalMetric = toNamedMetric(t, totalMetric, truncateLength);
const noInternalTopK = topKMetrics.filter(m => m.source.id !== m.destination.id);

const smallerTexts = truncateLength >= TruncateLength.M;

Expand Down
14 changes: 7 additions & 7 deletions web/src/components/netflow-topology/2d/styles/styleNode.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import * as React from 'react';
import * as _ from 'lodash';
import { Tooltip, TooltipPosition } from '@patternfly/react-core';
import {
CubeIcon,
Expand Down Expand Up @@ -32,8 +34,6 @@ import {
} from '@patternfly/react-topology';
import useDetailsLevel from '@patternfly/react-topology/dist/esm/hooks/useDetailsLevel';
import { TFunction } from 'i18next';
import * as _ from 'lodash';
import * as React from 'react';
import { useTranslation } from 'react-i18next';
import { Decorated, NodeData } from '../../../../model/topology';
import BaseNode from '../components/node';
Expand Down Expand Up @@ -88,7 +88,7 @@ const renderIcon = (data: Decorated<NodeData>, element: NodePeer): React.ReactNo
const iconSize =
(shape === NodeShape.trapezoid ? width : Math.min(width, height)) -
(shape === NodeShape.stadium ? 5 : ICON_PADDING) * 2;
const Component = getTypeIcon(data.resourceKind);
const Component = getTypeIcon(data.peer.resourceKind);

return (
<g transform={`translate(${(width - iconSize) / 2}, ${(height - iconSize) / 2})`}>
Expand Down Expand Up @@ -200,21 +200,21 @@ const renderDecorators = (
element,
TopologyQuadrant.lowerRight,
<LevelDownAltIcon />,
t('Step into this {{name}}', { name: data.resourceKind?.toLowerCase() }),
t('Step into this {{name}}', { name: data.peer.resourceKind?.toLowerCase() }),
false,
onStepIntoClick,
getShapeDecoratorCenter,
MEDIUM_DECORATOR_PADDING
)}
{(data.namespace || data.name || data.addr || data.host) &&
{(data.peer.namespace || data.peer.resource || data.peer.owner || data.peer.addr || data.peer.hostName) &&
renderClickableDecorator(
t,
element,
TopologyQuadrant.lowerLeft,
isFiltered ? <TimesIcon /> : <FilterIcon />,
isFiltered
? t('Remove {{name}} filter', { name: data.resourceKind?.toLowerCase() })
: t('Add {{name}} filter', { name: data.resourceKind?.toLowerCase() }),
? t('Remove {{name}} filter', { name: data.peer.resourceKind?.toLowerCase() })
: t('Add {{name}} filter', { name: data.peer.resourceKind?.toLowerCase() }),
isFiltered,
onFilterClick,
getShapeDecoratorCenter,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Button, DrawerCloseButton } from '@patternfly/react-core';
import { DrawerCloseButton, OptionsMenuToggle } from '@patternfly/react-core';
import { BaseEdge, BaseNode, NodeModel } from '@patternfly/react-topology';
import { mount, shallow } from 'enzyme';
import * as React from 'react';
Expand All @@ -8,16 +8,18 @@ import { MetricFunction, MetricScope, MetricType } from '../../../model/flow-que
import { ElementPanel, ElementPanelDetailsContent, ElementPanelMetricsContent } from '../element-panel';
import { dataSample } from '../__tests-data__/metrics';
import { NodeData } from '../../../model/topology';
import { createPeer } from '../../../utils/metrics';
import { TruncateLength } from '../../../components/dropdowns/truncate-dropdown';

describe('<ElementPanel />', () => {
const getNode = (kind: string, name: string, addr: string) => {
const bn = new BaseNode<NodeModel, NodeData>();
bn.setData({
nodeType: 'resource',
resourceKind: kind,
name,
addr
peer: createPeer({
addr: addr,
resource: { name, type: kind }
})
});
return bn;
};
Expand Down Expand Up @@ -61,7 +63,7 @@ describe('<ElementPanel />', () => {
expect(wrapper.find(ElementPanelDetailsContent)).toBeTruthy();

//check node infos
expect(wrapper.find('#addressValue').last().text()).toBe('10.129.0.15');
expect(wrapper.find('#node-info-address').last().text()).toBe('IP10.129.0.15');

//update to edge
wrapper.setProps({ ...mocks, element: getEdge() });
Expand All @@ -87,9 +89,11 @@ describe('<ElementPanel />', () => {

it('should filter <ElementPanelDetailsContent />', async () => {
const wrapper = mount(<ElementPanelDetailsContent {...mocks} />);
const filterButtons = wrapper.find(Button);
const ipFilters = wrapper.find(OptionsMenuToggle).last();
// Two buttons: first for pod filter, second for IP filter
filterButtons.last().simulate('click');
ipFilters.last().simulate('click');
expect(wrapper.find('li').length).toBe(3);
wrapper.find('[id="any"]').at(0).simulate('click');
expect(mocks.setFilters).toHaveBeenCalledWith([
{
def: expect.any(Object),
Expand Down
37 changes: 37 additions & 0 deletions web/src/components/netflow-topology/element-field.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import { Flex, FlexItem, Text, TextContent, TextVariants } from '@patternfly/react-core';
import * as React from 'react';
import { NodeType } from '../../model/flow-query';
import { TopologyMetricPeer } from '../../api/loki';
import { Filter } from '../../model/filters';
import { SummaryFilterButton } from '../filters/summary-filter-button';
import { PeerResourceLink } from './peer-resource-link';

export const ElementField: React.FC<{
id: string;
label: string;
filterType: NodeType;
forcedText?: string;
fields: Partial<TopologyMetricPeer>;
activeFilters: Filter[];
setFilters: (filters: Filter[]) => void;
}> = ({ id, label, filterType, forcedText, fields, activeFilters, setFilters }) => {
return (
<TextContent id={id} className="record-field-container">
<Text component={TextVariants.h4}>{label}</Text>
<Flex>
<FlexItem flex={{ default: 'flex_1' }}>
{forcedText ? <Text>{forcedText}</Text> : <PeerResourceLink fields={fields} />}
</FlexItem>
<FlexItem>
<SummaryFilterButton
id={id + '-filter'}
activeFilters={activeFilters}
filterType={filterType}
fields={fields}
setFilters={setFilters}
/>
</FlexItem>
</Flex>
</TextContent>
);
};
109 changes: 109 additions & 0 deletions web/src/components/netflow-topology/element-fields.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
import * as React from 'react';
import { Text, TextContent, TextVariants } from '@patternfly/react-core';
import { useTranslation } from 'react-i18next';
import { Filter } from '../../model/filters';
import { NodeData } from '../../model/topology';
import { ElementField } from './element-field';

export const ElementFields: React.FC<{
id: string;
data: NodeData;
forceFirstAsText?: boolean;
activeFilters: Filter[];
setFilters: (filters: Filter[]) => void;
}> = ({ id, data, forceFirstAsText, activeFilters, setFilters }) => {
const { t } = useTranslation('plugin__netobserv-plugin');

const fragments = [];
let forceAsText = forceFirstAsText;
let forceLabel = forceFirstAsText ? t('Name') : undefined;
if (data.peer.resource) {
fragments.push(
<ElementField
id={id + '-resource'}
key={id + '-resource'}
label={forceLabel || data.peer.resource.type}
forcedText={forceAsText ? data.peer.resource.name : undefined}
activeFilters={activeFilters}
filterType={'resource'}
fields={data.peer}
setFilters={setFilters}
/>
);
forceLabel = forceAsText = undefined;
}
if (data.peer.owner && data.peer.owner.type !== data.peer.resource?.type) {
fragments.push(
<ElementField
id={id + '-owner'}
key={id + '-owner'}
label={forceLabel || data.peer.owner.type}
forcedText={forceAsText ? data.peer.owner.name : undefined}
activeFilters={activeFilters}
filterType={'owner'}
fields={{ owner: data.peer.owner, namespace: data.peer.namespace }}
setFilters={setFilters}
/>
);
forceLabel = forceAsText = undefined;
}
if (data.peer.namespace) {
fragments.push(
<ElementField
id={id + '-namespace'}
key={id + '-namespace'}
label={forceLabel || t('Namespace')}
forcedText={forceAsText ? data.peer.namespace : undefined}
activeFilters={activeFilters}
filterType={'namespace'}
fields={{ namespace: data.peer.namespace }}
setFilters={setFilters}
/>
);
forceLabel = forceAsText = undefined;
}
if (data.peer.hostName) {
fragments.push(
<ElementField
id={id + '-host'}
key={id + '-host'}
label={forceLabel || t('Node')}
forcedText={forceAsText ? data.peer.hostName : undefined}
activeFilters={activeFilters}
filterType={'host'}
fields={{ hostName: data.peer.hostName }}
setFilters={setFilters}
/>
);
forceLabel = forceAsText = undefined;
}
if (data.peer.addr) {
fragments.push(
<ElementField
id={id + '-address'}
key={id + '-address'}
label={t('IP')}
activeFilters={activeFilters}
filterType={'resource'}
fields={{ addr: data.peer.addr }}
setFilters={setFilters}
/>
);
}

return (
<>
{fragments.length > 0 ? (
fragments
) : (
<TextContent id={id + '-no-infos'} className="record-field-container">
{
<Text component={TextVariants.p}>
{t('No information available for this content. Change scope to get more details.')}
</Text>
}
</TextContent>
)}
</>
);
};
Loading

0 comments on commit 72915fe

Please sign in to comment.