mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 17:59:23 -04:00
* Fixes storybook anomaly score generation and better utilizes available screen space * Closes #71770 for APM service maps by replacing breadthfirst layout with one from the cytoscape-dagre extension. Also replaces the taxi edges with cubic bezier edges. Finally, this adds the ability to drag individual nodes around the service map. * Removes unused code * removes commented line of code * - Adds ability for scripts/notice.js to check files with the .tsx file extension - Adds attribution for `applyCubicBezierStyles` * Refine comment text and MIT license url
This commit is contained in:
parent
738370c908
commit
d405da9b2c
12 changed files with 113 additions and 88 deletions
|
@ -281,6 +281,13 @@ WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
|||
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
|
||||
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||
|
||||
---
|
||||
This product includes code in the function applyCubicBezierStyles that was
|
||||
inspired by a public Codepen, which was available under a "MIT" license.
|
||||
|
||||
Copyright (c) 2020 by Guillaume (https://codepen.io/guillaumethomas/pen/xxbbBKO)
|
||||
MIT License http://www.opensource.org/licenses/mit-license
|
||||
|
||||
---
|
||||
This product includes code that is adapted from mapbox-gl-js, which is
|
||||
available under a "BSD-3-Clause" license.
|
||||
|
|
|
@ -41,7 +41,7 @@ interface Options {
|
|||
* into the repository.
|
||||
*/
|
||||
export async function generateNoticeFromSource({ productName, directory, log }: Options) {
|
||||
const globs = ['**/*.{js,less,css,ts}'];
|
||||
const globs = ['**/*.{js,less,css,ts,tsx}'];
|
||||
|
||||
const options = {
|
||||
cwd: directory,
|
||||
|
|
|
@ -305,6 +305,7 @@
|
|||
"concat-stream": "1.6.2",
|
||||
"content-disposition": "0.5.3",
|
||||
"cytoscape": "^3.10.0",
|
||||
"cytoscape-dagre": "^2.2.2",
|
||||
"d3-array": "1.2.4",
|
||||
"dedent": "^0.7.0",
|
||||
"del": "^5.1.0",
|
||||
|
|
|
@ -13,6 +13,7 @@ import React, {
|
|||
useState,
|
||||
} from 'react';
|
||||
import cytoscape from 'cytoscape';
|
||||
import dagre from 'cytoscape-dagre';
|
||||
import { debounce } from 'lodash';
|
||||
import { useTheme } from '../../../hooks/useTheme';
|
||||
import {
|
||||
|
@ -22,6 +23,8 @@ import {
|
|||
} from './cytoscapeOptions';
|
||||
import { useUiTracker } from '../../../../../observability/public';
|
||||
|
||||
cytoscape.use(dagre);
|
||||
|
||||
export const CytoscapeContext = createContext<cytoscape.Core | undefined>(
|
||||
undefined
|
||||
);
|
||||
|
@ -30,7 +33,6 @@ interface CytoscapeProps {
|
|||
children?: ReactNode;
|
||||
elements: cytoscape.ElementDefinition[];
|
||||
height: number;
|
||||
width: number;
|
||||
serviceName?: string;
|
||||
style?: CSSProperties;
|
||||
}
|
||||
|
@ -57,59 +59,52 @@ function useCytoscape(options: cytoscape.CytoscapeOptions) {
|
|||
return [ref, cy] as [React.MutableRefObject<any>, cytoscape.Core | undefined];
|
||||
}
|
||||
|
||||
function rotatePoint(
|
||||
{ x, y }: { x: number; y: number },
|
||||
degreesRotated: number
|
||||
) {
|
||||
const radiansPerDegree = Math.PI / 180;
|
||||
const θ = radiansPerDegree * degreesRotated;
|
||||
const cosθ = Math.cos(θ);
|
||||
const sinθ = Math.sin(θ);
|
||||
function getLayoutOptions(nodeHeight: number): cytoscape.LayoutOptions {
|
||||
return {
|
||||
x: x * cosθ - y * sinθ,
|
||||
y: x * sinθ + y * cosθ,
|
||||
};
|
||||
}
|
||||
|
||||
function getLayoutOptions(
|
||||
selectedRoots: string[],
|
||||
height: number,
|
||||
width: number,
|
||||
nodeHeight: number
|
||||
): cytoscape.LayoutOptions {
|
||||
return {
|
||||
name: 'breadthfirst',
|
||||
// @ts-ignore DefinitelyTyped is incorrect here. Roots can be an Array
|
||||
roots: selectedRoots.length ? selectedRoots : undefined,
|
||||
name: 'dagre',
|
||||
fit: true,
|
||||
padding: nodeHeight,
|
||||
spacingFactor: 1.2,
|
||||
// @ts-ignore
|
||||
// Rotate nodes counter-clockwise to transform layout from top→bottom to left→right.
|
||||
// The extra 5° achieves the effect of separating overlapping taxi-styled edges.
|
||||
transform: (node: any, pos: cytoscape.Position) => rotatePoint(pos, -95),
|
||||
// swap width/height of boundingBox to compensate for the rotation
|
||||
boundingBox: { x1: 0, y1: 0, w: height, h: width },
|
||||
nodeSep: nodeHeight,
|
||||
edgeSep: 32,
|
||||
rankSep: 128,
|
||||
rankDir: 'LR',
|
||||
ranker: 'network-simplex',
|
||||
};
|
||||
}
|
||||
|
||||
function selectRoots(cy: cytoscape.Core): string[] {
|
||||
const bfs = cy.elements().bfs({
|
||||
roots: cy.elements().leaves(),
|
||||
/*
|
||||
* @notice
|
||||
* This product includes code in the function applyCubicBezierStyles that was
|
||||
* inspired by a public Codepen, which was available under a "MIT" license.
|
||||
*
|
||||
* Copyright (c) 2020 by Guillaume (https://codepen.io/guillaumethomas/pen/xxbbBKO)
|
||||
* MIT License http://www.opensource.org/licenses/mit-license
|
||||
*/
|
||||
function applyCubicBezierStyles(edges: cytoscape.EdgeCollection) {
|
||||
edges.forEach((edge) => {
|
||||
const { x: x0, y: y0 } = edge.source().position();
|
||||
const { x: x1, y: y1 } = edge.target().position();
|
||||
const x = x1 - x0;
|
||||
const y = y1 - y0;
|
||||
const z = Math.sqrt(x * x + y * y);
|
||||
const costheta = z === 0 ? 0 : x / z;
|
||||
const alpha = 0.25;
|
||||
// Two values for control-point-distances represent a pair symmetric quadratic
|
||||
// bezier curves joined in the middle as a seamless cubic bezier curve:
|
||||
edge.style('control-point-distances', [
|
||||
-alpha * y * costheta,
|
||||
alpha * y * costheta,
|
||||
]);
|
||||
edge.style('control-point-weights', [alpha, 1 - alpha]);
|
||||
});
|
||||
const furthestNodeFromLeaves = bfs.path.last();
|
||||
return cy
|
||||
.elements()
|
||||
.roots()
|
||||
.union(furthestNodeFromLeaves)
|
||||
.map((el) => el.id());
|
||||
}
|
||||
|
||||
export function Cytoscape({
|
||||
children,
|
||||
elements,
|
||||
height,
|
||||
width,
|
||||
serviceName,
|
||||
style,
|
||||
}: CytoscapeProps) {
|
||||
|
@ -151,13 +146,7 @@ export function Cytoscape({
|
|||
} else {
|
||||
resetConnectedEdgeStyle();
|
||||
}
|
||||
|
||||
const selectedRoots = selectRoots(event.cy);
|
||||
const layout = cy.layout(
|
||||
getLayoutOptions(selectedRoots, height, width, nodeHeight)
|
||||
);
|
||||
|
||||
layout.run();
|
||||
cy.layout(getLayoutOptions(nodeHeight)).run();
|
||||
}
|
||||
};
|
||||
let layoutstopDelayTimeout: NodeJS.Timeout;
|
||||
|
@ -180,6 +169,7 @@ export function Cytoscape({
|
|||
event.cy.fit(undefined, nodeHeight);
|
||||
}
|
||||
}, 0);
|
||||
applyCubicBezierStyles(event.cy.edges());
|
||||
};
|
||||
// debounce hover tracking so it doesn't spam telemetry with redundant events
|
||||
const trackNodeEdgeHover = debounce(
|
||||
|
@ -211,6 +201,9 @@ export function Cytoscape({
|
|||
console.debug('cytoscape:', event);
|
||||
}
|
||||
};
|
||||
const dragHandler: cytoscape.EventHandler = (event) => {
|
||||
applyCubicBezierStyles(event.target.connectedEdges());
|
||||
};
|
||||
|
||||
if (cy) {
|
||||
cy.on('data layoutstop select unselect', debugHandler);
|
||||
|
@ -220,6 +213,7 @@ export function Cytoscape({
|
|||
cy.on('mouseout', 'edge, node', mouseoutHandler);
|
||||
cy.on('select', 'node', selectHandler);
|
||||
cy.on('unselect', 'node', unselectHandler);
|
||||
cy.on('drag', 'node', dragHandler);
|
||||
|
||||
cy.remove(cy.elements());
|
||||
cy.add(elements);
|
||||
|
@ -239,19 +233,11 @@ export function Cytoscape({
|
|||
cy.removeListener('mouseout', 'edge, node', mouseoutHandler);
|
||||
cy.removeListener('select', 'node', selectHandler);
|
||||
cy.removeListener('unselect', 'node', unselectHandler);
|
||||
cy.removeListener('drag', 'node', dragHandler);
|
||||
}
|
||||
clearTimeout(layoutstopDelayTimeout);
|
||||
};
|
||||
}, [
|
||||
cy,
|
||||
elements,
|
||||
height,
|
||||
serviceName,
|
||||
trackApmEvent,
|
||||
width,
|
||||
nodeHeight,
|
||||
theme,
|
||||
]);
|
||||
}, [cy, elements, height, serviceName, trackApmEvent, nodeHeight, theme]);
|
||||
|
||||
return (
|
||||
<CytoscapeContext.Provider value={cy}>
|
||||
|
|
|
@ -71,6 +71,7 @@ export function Popover({ focusedServiceName }: PopoverProps) {
|
|||
cy.on('select', 'node', selectHandler);
|
||||
cy.on('unselect', 'node', deselect);
|
||||
cy.on('data viewport', deselect);
|
||||
cy.on('drag', 'node', deselect);
|
||||
}
|
||||
|
||||
return () => {
|
||||
|
@ -78,6 +79,7 @@ export function Popover({ focusedServiceName }: PopoverProps) {
|
|||
cy.removeListener('select', 'node', selectHandler);
|
||||
cy.removeListener('unselect', 'node', deselect);
|
||||
cy.removeListener('data viewport', undefined, deselect);
|
||||
cy.removeListener('drag', 'node', deselect);
|
||||
}
|
||||
};
|
||||
}, [cy, deselect]);
|
||||
|
|
|
@ -49,13 +49,11 @@ storiesOf('app/ServiceMap/Cytoscape', module)
|
|||
},
|
||||
];
|
||||
const height = 300;
|
||||
const width = 1340;
|
||||
const serviceName = 'opbeans-python';
|
||||
return (
|
||||
<Cytoscape
|
||||
elements={elements}
|
||||
height={height}
|
||||
width={width}
|
||||
serviceName={serviceName}
|
||||
/>
|
||||
);
|
||||
|
@ -330,7 +328,7 @@ storiesOf('app/ServiceMap/Cytoscape', module)
|
|||
},
|
||||
},
|
||||
];
|
||||
return <Cytoscape elements={elements} height={600} width={1340} />;
|
||||
return <Cytoscape elements={elements} height={600} />;
|
||||
},
|
||||
{
|
||||
info: { propTables: false, source: false },
|
||||
|
|
|
@ -35,6 +35,8 @@ function setSessionJson(json: string) {
|
|||
window.sessionStorage.setItem(SESSION_STORAGE_KEY, json);
|
||||
}
|
||||
|
||||
const getCytoscapeHeight = () => window.innerHeight - 300;
|
||||
|
||||
storiesOf(STORYBOOK_PATH, module)
|
||||
.addDecorator((storyFn) => <EuiThemeProvider>{storyFn()}</EuiThemeProvider>)
|
||||
.add(
|
||||
|
@ -43,16 +45,17 @@ storiesOf(STORYBOOK_PATH, module)
|
|||
const [size, setSize] = useState<number>(10);
|
||||
const [json, setJson] = useState<string>('');
|
||||
const [elements, setElements] = useState<any[]>(
|
||||
generateServiceMapElements(size)
|
||||
generateServiceMapElements({ size, hasAnomalies: true })
|
||||
);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<EuiFlexGroup>
|
||||
<EuiFlexItem>
|
||||
<EuiButton
|
||||
onClick={() => {
|
||||
setElements(generateServiceMapElements(size));
|
||||
setElements(
|
||||
generateServiceMapElements({ size, hasAnomalies: true })
|
||||
);
|
||||
setJson('');
|
||||
}}
|
||||
>
|
||||
|
@ -79,7 +82,7 @@ storiesOf(STORYBOOK_PATH, module)
|
|||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
|
||||
<Cytoscape elements={elements} height={600} width={1340} />
|
||||
<Cytoscape elements={elements} height={getCytoscapeHeight()} />
|
||||
|
||||
{json && (
|
||||
<EuiCodeEditor
|
||||
|
@ -121,7 +124,7 @@ storiesOf(STORYBOOK_PATH, module)
|
|||
|
||||
return (
|
||||
<div>
|
||||
<Cytoscape elements={elements} height={600} width={1340} />
|
||||
<Cytoscape elements={elements} height={getCytoscapeHeight()} />
|
||||
<EuiForm isInvalid={error !== undefined} error={error}>
|
||||
<EuiFlexGroup>
|
||||
<EuiFlexItem>
|
||||
|
@ -204,8 +207,7 @@ storiesOf(STORYBOOK_PATH, module)
|
|||
<div>
|
||||
<Cytoscape
|
||||
elements={exampleResponseTodo.elements}
|
||||
height={600}
|
||||
width={1340}
|
||||
height={getCytoscapeHeight()}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
@ -224,8 +226,7 @@ storiesOf(STORYBOOK_PATH, module)
|
|||
<div>
|
||||
<Cytoscape
|
||||
elements={exampleResponseOpbeansBeats.elements}
|
||||
height={600}
|
||||
width={1340}
|
||||
height={getCytoscapeHeight()}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
@ -244,8 +245,7 @@ storiesOf(STORYBOOK_PATH, module)
|
|||
<div>
|
||||
<Cytoscape
|
||||
elements={exampleResponseHipsterStore.elements}
|
||||
height={600}
|
||||
width={1340}
|
||||
height={getCytoscapeHeight()}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
@ -264,8 +264,7 @@ storiesOf(STORYBOOK_PATH, module)
|
|||
<div>
|
||||
<Cytoscape
|
||||
elements={exampleResponseOneDomainManyIPs.elements}
|
||||
height={600}
|
||||
width={1340}
|
||||
height={getCytoscapeHeight()}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
|
|
@ -4,9 +4,13 @@
|
|||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { getSeverity } from '../Popover/getSeverity';
|
||||
|
||||
export function generateServiceMapElements(size: number): any[] {
|
||||
export function generateServiceMapElements({
|
||||
size,
|
||||
hasAnomalies,
|
||||
}: {
|
||||
size: number;
|
||||
hasAnomalies: boolean;
|
||||
}): any[] {
|
||||
const services = range(size).map((i) => {
|
||||
const name = getName();
|
||||
const anomalyScore = randn(101);
|
||||
|
@ -15,11 +19,14 @@ export function generateServiceMapElements(size: number): any[] {
|
|||
'service.environment': 'production',
|
||||
'service.name': name,
|
||||
'agent.name': getAgentName(),
|
||||
anomaly_score: anomalyScore,
|
||||
anomaly_severity: getSeverity(anomalyScore),
|
||||
actual_value: Math.random() * 2000000,
|
||||
typical_value: Math.random() * 1000000,
|
||||
ml_job_id: `${name}-request-high_mean_response_time`,
|
||||
serviceAnomalyStats: hasAnomalies
|
||||
? {
|
||||
transactionType: 'request',
|
||||
anomalyScore,
|
||||
actualValue: Math.random() * 2000000,
|
||||
jobId: `${name}-request-high_mean_response_time`,
|
||||
}
|
||||
: undefined,
|
||||
};
|
||||
});
|
||||
|
||||
|
@ -146,7 +153,7 @@ const NAMES = [
|
|||
'leech',
|
||||
'loki',
|
||||
'longshot',
|
||||
'lumpkin,',
|
||||
'lumpkin',
|
||||
'madame-web',
|
||||
'magician',
|
||||
'magneto',
|
||||
|
|
|
@ -168,9 +168,7 @@ const getStyle = (theme: EuiTheme): cytoscape.Stylesheet[] => {
|
|||
{
|
||||
selector: 'edge',
|
||||
style: {
|
||||
'curve-style': 'taxi',
|
||||
// @ts-ignore
|
||||
'taxi-direction': 'auto',
|
||||
'curve-style': 'unbundled-bezier',
|
||||
'line-color': lineColor,
|
||||
'overlay-opacity': 0,
|
||||
'target-arrow-color': lineColor,
|
||||
|
@ -264,7 +262,6 @@ ${theme.eui.euiColorLightShade}`,
|
|||
export const getCytoscapeOptions = (
|
||||
theme: EuiTheme
|
||||
): cytoscape.CytoscapeOptions => ({
|
||||
autoungrabify: true,
|
||||
boxSelectionEnabled: false,
|
||||
maxZoom: 3,
|
||||
minZoom: 0.2,
|
||||
|
|
|
@ -57,7 +57,7 @@ export function ServiceMap({ serviceName }: ServiceMapProps) {
|
|||
}
|
||||
}, [license, serviceName, urlParams]);
|
||||
|
||||
const { ref, height, width } = useRefDimensions();
|
||||
const { ref, height } = useRefDimensions();
|
||||
|
||||
useTrackPageview({ app: 'apm', path: 'service_map' });
|
||||
useTrackPageview({ app: 'apm', path: 'service_map', delay: 15000 });
|
||||
|
@ -78,7 +78,6 @@ export function ServiceMap({ serviceName }: ServiceMapProps) {
|
|||
height={height}
|
||||
serviceName={serviceName}
|
||||
style={getCytoscapeDivStyle(theme)}
|
||||
width={width}
|
||||
>
|
||||
<Controls />
|
||||
<BetaBadge />
|
||||
|
|
7
x-pack/plugins/apm/typings/cytoscape_dagre.d.ts
vendored
Normal file
7
x-pack/plugins/apm/typings/cytoscape_dagre.d.ts
vendored
Normal file
|
@ -0,0 +1,7 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
declare module 'cytoscape-dagre';
|
22
yarn.lock
22
yarn.lock
|
@ -10035,6 +10035,13 @@ cypress@4.11.0:
|
|||
url "0.11.0"
|
||||
yauzl "2.10.0"
|
||||
|
||||
cytoscape-dagre@^2.2.2:
|
||||
version "2.2.2"
|
||||
resolved "https://registry.yarnpkg.com/cytoscape-dagre/-/cytoscape-dagre-2.2.2.tgz#5f32a85c0ba835f167efee531df9e89ac58ff411"
|
||||
integrity sha512-zsg36qNwua/L2stJSWkcbSDcvW3E6VZf6KRe6aLnQJxuXuz89tMqI5EVYVKEcNBgzTEzFMFv0PE3T0nD4m6VDw==
|
||||
dependencies:
|
||||
dagre "^0.8.2"
|
||||
|
||||
cytoscape@^3.10.0:
|
||||
version "3.10.0"
|
||||
resolved "https://registry.yarnpkg.com/cytoscape/-/cytoscape-3.10.0.tgz#3b462e0d35121ecd2d2702f470915fd6dae01777"
|
||||
|
@ -10247,6 +10254,14 @@ d@1:
|
|||
dependencies:
|
||||
es5-ext "^0.10.9"
|
||||
|
||||
dagre@^0.8.2:
|
||||
version "0.8.5"
|
||||
resolved "https://registry.yarnpkg.com/dagre/-/dagre-0.8.5.tgz#ba30b0055dac12b6c1fcc247817442777d06afee"
|
||||
integrity sha512-/aTqmnRta7x7MCCpExk7HQL2O4owCT2h8NT//9I1OQ9vt29Pa0BzSAkR5lwFUcQ7491yVi/3CXU9jQ5o0Mn2Sw==
|
||||
dependencies:
|
||||
graphlib "^2.1.8"
|
||||
lodash "^4.17.15"
|
||||
|
||||
damerau-levenshtein@^1.0.4:
|
||||
version "1.0.4"
|
||||
resolved "https://registry.yarnpkg.com/damerau-levenshtein/-/damerau-levenshtein-1.0.4.tgz#03191c432cb6eea168bb77f3a55ffdccb8978514"
|
||||
|
@ -14390,6 +14405,13 @@ graceful-fs@~1.1:
|
|||
resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-1.1.14.tgz#07078db5f6377f6321fceaaedf497de124dc9465"
|
||||
integrity sha1-BweNtfY3f2Mh/Oqu30l94STclGU=
|
||||
|
||||
graphlib@^2.1.8:
|
||||
version "2.1.8"
|
||||
resolved "https://registry.yarnpkg.com/graphlib/-/graphlib-2.1.8.tgz#5761d414737870084c92ec7b5dbcb0592c9d35da"
|
||||
integrity sha512-jcLLfkpoVGmH7/InMC/1hIvOPSUh38oJtGhvrOFGzioE1DZ+0YW16RgmOJhHiuWTvGiJQ9Z1Ik43JvkRPRvE+A==
|
||||
dependencies:
|
||||
lodash "^4.17.15"
|
||||
|
||||
graphql-anywhere@^4.1.0-alpha.0:
|
||||
version "4.1.16"
|
||||
resolved "https://registry.yarnpkg.com/graphql-anywhere/-/graphql-anywhere-4.1.16.tgz#82bb59643e30183cfb7b485ed4262a7b39d8a6c1"
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue