Skip to content

Commit

Permalink
Correctly reduce nodes/connections
Browse files Browse the repository at this point in the history
  • Loading branch information
dgieselaar committed Jan 10, 2020
1 parent a9927c8 commit b4d9480
Show file tree
Hide file tree
Showing 3 changed files with 117 additions and 82 deletions.
1 change: 0 additions & 1 deletion x-pack/legacy/plugins/apm/common/service_map.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,5 +18,4 @@ export type ConnectionNode = ServiceConnectionNode | ExternalConnectionNode;
export interface Connection {
source: ConnectionNode;
destination: ConnectionNode;
bidirectional?: boolean;
}
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ const style: cytoscape.Stylesheet[] = [
'font-family': 'Inter UI, Segoe UI, Helvetica, Arial, sans-serif',
'font-size': theme.euiFontSizeXS,
height: theme.avatarSizing.l.size,
label: 'data(id)',
label: 'data(label)',
'min-zoomed-font-size': theme.euiSizeL,
'overlay-opacity': 0,
shape: (el: cytoscape.NodeSingular) =>
Expand Down Expand Up @@ -72,7 +72,18 @@ const style: cytoscape.Stylesheet[] = [
//
// @ts-ignore
'target-distance-from-node': theme.paddingSizes.xs,
width: 1
width: 1,
'source-arrow-shape': 'none'
}
},
{
selector: 'edge[bidirectional]',
style: {
'source-arrow-shape': 'triangle',
'target-arrow-shape': 'triangle',
// @ts-ignore
'source-distance-from-node': theme.paddingSizes.xs,
'target-distance-from-node': theme.paddingSizes.xs
}
}
];
Expand Down
183 changes: 104 additions & 79 deletions x-pack/legacy/plugins/apm/public/components/app/ServiceMap/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,10 @@

import theme from '@elastic/eui/dist/eui_theme_light.json';
import React, { useMemo, useEffect, useState, useRef } from 'react';
import { find } from 'lodash';
import { find, isEqual, sortBy } from 'lodash';
import { i18n } from '@kbn/i18n';
import { EuiButton } from '@elastic/eui';
import { ValuesType } from 'utility-types';
import { toMountPoint } from '../../../../../../../../src/plugins/kibana_react/public';
import { ServiceMapAPIResponse } from '../../../../server/lib/service_map/get_service_map';
import {
Expand Down Expand Up @@ -72,21 +73,18 @@ ${theme.euiColorLightShade}`,

const MAX_REQUESTS = 15;

function getConnectionNodeId(
node: ConnectionNode,
destMap: Record<string, ServiceConnectionNode> = {}
): string {
function getConnectionNodeId(node: ConnectionNode): string {
if ('destination.address' in node) {
const mapped = destMap[node['destination.address']];
return mapped
? getConnectionNodeId(mapped, destMap)
: node['destination.address'];
// use a prefix to distinguish exernal destination ids from services
return `>${node['destination.address']}`;
}
return node['service.name'];
}

function getEdgeId(source: ConnectionNode, destination: ConnectionNode) {
return `${getConnectionNodeId(source)}~${getConnectionNodeId(destination)}`;
function getConnectionId(connection: Connection) {
return `${getConnectionNodeId(connection.source)}~${getConnectionNodeId(
connection.destination
)}`;
}

export function ServiceMap({ serviceName }: ServiceMapProps) {
Expand Down Expand Up @@ -187,51 +185,64 @@ export function ServiceMap({ serviceName }: ServiceMapProps) {
};
}, {} as Record<string, ServiceConnectionNode>);

const services = responses.flatMap(response => response.services);
const serviceNodes = responses
.flatMap(response => response.services)
.map(service => ({
...service,
id: service['service.name']
}));

const nodesById = responses
.flatMap(response => response.connections)
.flatMap(connection => [connection.source, connection.destination])
.concat(services)
.reduce((acc, node) => {
const nodeId = getConnectionNodeId(node, destMap);
// maps destination.address to service.name if possible
function getConnectionNode(node: ConnectionNode) {
const mappedNode =
('destination.address' in node &&
destMap[node['destination.address']]) ||
node;

return {
...acc,
[nodeId]: destMap[nodeId] || node
};
}, {} as Record<string, ConnectionNode>);
return {
...mappedNode,
id: getConnectionNodeId(mappedNode)
};
}

const edgesById = responses
// build connections with mapped nodes
const connections = responses
.flatMap(response => response.connections)
.reduce((acc, connection) => {
const source =
nodesById[getConnectionNodeId(connection.source, destMap)];
const destination =
nodesById[getConnectionNodeId(connection.destination, destMap)];

if (acc[getEdgeId(destination, source)]) {
return {
...acc,
[getEdgeId(destination, source)]: {
source,
destination,
bidirectional: true
}
};
}
.map(connection => {
const source = getConnectionNode(connection.source);
const destination = getConnectionNode(connection.destination);

return {
...acc,
[getEdgeId(source, destination)]: {
source,
destination
}
source,
destination,
id: getConnectionId({ source, destination })
};
}, {} as Record<string, Connection>);
})
.filter(connection => connection.source.id !== connection.destination.id);

const nodes = connections
.flatMap(connection => [connection.source, connection.destination])
.concat(serviceNodes);

type ConnectionWithId = ValuesType<typeof connections>;
type ConnectionNodeWithId = ValuesType<typeof nodes>;

const connectionsById = connections.reduce((connectionMap, connection) => {
return {
...connectionMap,
[connection.id]: connection
};
}, {} as Record<string, ConnectionWithId>);

const nodesById = nodes.reduce((nodeMap, node) => {
return {
...nodeMap,
[node.id]: node
};
}, {} as Record<string, ConnectionNodeWithId>);

return [
...(Object.values(nodesById) as ConnectionNode[]).map(node => {
const cyNodes = (Object.values(nodesById) as ConnectionNodeWithId[]).map(
node => {
let data = {};

if ('service.name' in node) {
Expand All @@ -247,36 +258,53 @@ export function ServiceMap({ serviceName }: ServiceMapProps) {
return {
group: 'nodes' as const,
data: {
id: getConnectionNodeId(node, destMap),
id: node.id,
label:
'service.name' in node
? node['service.name']
: node['destination.address'],
...data
}
};
}),
...(Object.values(edgesById) as Connection[])
.filter(connection => connection.source !== connection.destination)
.map(connection => {
return {
group: 'edges' as const,
data: {
id: getEdgeId(connection.source, connection.destination),
source: getConnectionNodeId(connection.source, destMap),
target: getConnectionNodeId(connection.destination, destMap)
},
style: connection.bidirectional
? {
'source-arrow-shape': 'triangle',
'target-arrow-shape': 'triangle',
'source-distance-from-node': theme.paddingSizes.xs,
'target-distance-from-node': theme.paddingSizes.xs
}
: {
'source-arrow-shape': 'none',
'target-arrow-shape': 'triangle',
'target-distance-from-node': theme.paddingSizes.xs
}
};
})
];
}
);

// instead of adding connections in two directions,
// we add a `bidirectional` flag to use in styling
const dedupedConnections = (sortBy(
Object.values(connectionsById),
// make sure that order is stable
'id'
) as ConnectionWithId[]).reduce<
Array<ConnectionWithId & { bidirectional?: boolean }>
>((prev, connection) => {
const reversedConnection = prev.find(
c =>
c.destination.id === connection.source.id &&
c.source.id === connection.destination.id
);

if (reversedConnection) {
reversedConnection.bidirectional = true;
return prev;
}

return prev.concat(connection);
}, []);

const cyEdges = dedupedConnections.map(connection => {
return {
group: 'edges' as const,
data: {
id: connection.id,
source: connection.source.id,
target: connection.destination.id,
bidirectional: connection.bidirectional ? true : undefined
}
};
}, []);

return [...cyNodes, ...cyEdges];
}, [responses, search]);

const license = useLicense();
Expand All @@ -290,10 +318,7 @@ export function ServiceMap({ serviceName }: ServiceMapProps) {
const openToast = useRef<string | null>(null);

const newData = elements.filter(element => {
return !find(
renderedElements.current,
el => el.data.id === element.data.id
);
return !find(renderedElements.current, el => isEqual(el, element));
});

const updateMap = () => {
Expand Down

0 comments on commit b4d9480

Please sign in to comment.