mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 17:59:23 -04:00
[Monitoring] [Pipeline Viewer] Remove obsolete code (#20122)
* Remove PipelineViewer component, replace with Config Viewer as index export. * Remove tests for obsolete component. * Remove obsolete code. * Remove obsolete CSS. * Remove old SVG class for graph edges. * Remove more graph rendering code. * Remove obsolete properties from graph classes. * Remove unused constants. * Remove obsolete keys from subtitle props. * Fix broken unit tests.
This commit is contained in:
parent
d3369fe9dc
commit
15667e5473
22 changed files with 13 additions and 1725 deletions
|
@ -121,27 +121,6 @@ export const LOGSTASH = {
|
|||
* Constants used by Logstash Pipeline Viewer code
|
||||
*/
|
||||
PIPELINE_VIEWER: {
|
||||
GRAPH: {
|
||||
EDGES: {
|
||||
SVG_CLASS: 'lspvEdge',
|
||||
LABEL_RADIUS: 8,
|
||||
// This is something we may play with later.
|
||||
// 1 seems to be the best value however, without it the edges sometimes make weird loops in complex graphs
|
||||
ROUTING_MARGIN_PX: 1,
|
||||
ARROW_START: 5
|
||||
},
|
||||
VERTICES: {
|
||||
BORDER_RADIUS_PX: 4,
|
||||
MARGIN_PX: 35,
|
||||
WIDTH_PX: 320,
|
||||
HEIGHT_PX: 85,
|
||||
|
||||
/**
|
||||
* Vertical distance between vertices, as measured from top-border-to-top-border
|
||||
*/
|
||||
VERTICAL_DISTANCE_PX: 20
|
||||
}
|
||||
},
|
||||
ICON: {
|
||||
HEIGHT_PX: 18,
|
||||
WIDTH_PX: 18
|
||||
|
|
|
@ -1,141 +0,0 @@
|
|||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { shallow } from 'enzyme';
|
||||
import { PipelineViewer } from '../index';
|
||||
import { DetailDrawer } from '../views/detail_drawer';
|
||||
import { Graph } from '../models/graph';
|
||||
|
||||
|
||||
describe('PipelineViewer component', () => {
|
||||
describe('detail drawer', () => {
|
||||
let graph;
|
||||
let timeseriesTooltipXValueFormatter;
|
||||
let pipelineState;
|
||||
let wrapper;
|
||||
let vertex;
|
||||
|
||||
beforeEach(() => {
|
||||
graph = new Graph();
|
||||
graph.update({
|
||||
vertices: [
|
||||
{
|
||||
id: 'terminal_logger',
|
||||
explicit_id: true,
|
||||
type: 'plugin',
|
||||
plugin_type: 'input',
|
||||
config_name: 'stdin',
|
||||
stats: {}
|
||||
},
|
||||
{
|
||||
id: '__QUEUE__',
|
||||
explicit_id: false,
|
||||
type: 'queue',
|
||||
stats: {}
|
||||
},
|
||||
{
|
||||
id: '5y890f3e5c135c037eb40ba88d69b040faaeb954bb10510e95294259ffdd88e8',
|
||||
explicit_id: false,
|
||||
type: 'plugin',
|
||||
plugin_type: 'output',
|
||||
config_name: 'elasticsearch',
|
||||
stats: {}
|
||||
}
|
||||
],
|
||||
edges: [
|
||||
{
|
||||
id: '8f1gae28a11c3d99d1adf44f793763db6b9c61379e0ad518371b49aa67ef902f',
|
||||
from: 'terminal_logger',
|
||||
to: '__QUEUE__',
|
||||
type: 'plain'
|
||||
},
|
||||
{
|
||||
id: '8ue2ae28a11c3d99d1ado9f3epq0bvjd6b9c61379e0ad518371b49aa67ef902f',
|
||||
from: '__QUEUE__',
|
||||
to: '5y890f3e5c135c037eb40ba88d69b040faaeb954bb10510e95294259ffdd88e8',
|
||||
type: 'plain'
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
timeseriesTooltipXValueFormatter = () => {};
|
||||
pipelineState = {
|
||||
config: {
|
||||
graph
|
||||
}
|
||||
};
|
||||
|
||||
wrapper = shallow((
|
||||
<PipelineViewer
|
||||
pipelineState={pipelineState}
|
||||
timeseriesTooltipXValueFormatter={timeseriesTooltipXValueFormatter}
|
||||
/>
|
||||
));
|
||||
|
||||
vertex = graph.getVertices()[0];
|
||||
});
|
||||
|
||||
it('creates null vertex state by default', () => {
|
||||
expect(
|
||||
wrapper.instance().state.detailDrawer
|
||||
).toEqual({ vertex: null });
|
||||
});
|
||||
|
||||
it('is rendered for newly-selected vertex', () => {
|
||||
const component = wrapper.instance();
|
||||
|
||||
component.onShowVertexDetails(vertex);
|
||||
|
||||
expect(wrapper.find(DetailDrawer).length).toEqual(0);
|
||||
expect(component.state.detailDrawer.vertex).toEqual(vertex);
|
||||
|
||||
wrapper.update();
|
||||
|
||||
expect(wrapper.find(DetailDrawer).length).toEqual(1);
|
||||
});
|
||||
|
||||
it('is hidden if current vertex is selected again', () => {
|
||||
const component = wrapper.instance();
|
||||
|
||||
component.onShowVertexDetails(vertex);
|
||||
|
||||
wrapper.update();
|
||||
|
||||
// showing drawer for selected vertex
|
||||
expect(wrapper.find(DetailDrawer).length).toEqual(1);
|
||||
|
||||
component.onShowVertexDetails(vertex);
|
||||
|
||||
wrapper.update();
|
||||
|
||||
// drawer toggled off for second consecutive selection of vertex
|
||||
expect(wrapper.find(DetailDrawer).length).toEqual(0);
|
||||
expect(component.state.detailDrawer.vertex).toEqual(null);
|
||||
});
|
||||
|
||||
it('remains visible for new vertex', () => {
|
||||
const component = wrapper.instance();
|
||||
|
||||
component.onShowVertexDetails(vertex);
|
||||
|
||||
wrapper.update();
|
||||
|
||||
// showing drawer for first vertex
|
||||
expect(wrapper.find(DetailDrawer).length).toEqual(1);
|
||||
|
||||
// select different vertex
|
||||
const secondVertex = graph.getVertices()[1];
|
||||
component.onShowVertexDetails(secondVertex);
|
||||
|
||||
wrapper.update();
|
||||
|
||||
// still visible for second vertex
|
||||
expect(wrapper.find(DetailDrawer).length).toEqual(1);
|
||||
expect(component.state.detailDrawer.vertex).toEqual(secondVertex);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -4,77 +4,4 @@
|
|||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { ColaGraph } from './views/cola_graph';
|
||||
import { DetailDrawer } from './views/detail_drawer';
|
||||
import { PropTypes } from 'prop-types';
|
||||
|
||||
export class PipelineViewer extends React.Component {
|
||||
constructor() {
|
||||
super();
|
||||
this.state = {
|
||||
detailDrawer: {
|
||||
vertex: null
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
onShowVertexDetails = (vertex) => {
|
||||
if (vertex === this.state.detailDrawer.vertex) {
|
||||
this.onHideVertexDetails();
|
||||
}
|
||||
else {
|
||||
this.setState({
|
||||
detailDrawer: {
|
||||
vertex
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
onHideVertexDetails = () => {
|
||||
this.setState({
|
||||
detailDrawer: {
|
||||
vertex: null
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
renderDetailDrawer = () => {
|
||||
if (!this.state.detailDrawer.vertex) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<DetailDrawer
|
||||
vertex={this.state.detailDrawer.vertex}
|
||||
onHide={this.onHideVertexDetails}
|
||||
timeseriesTooltipXValueFormatter={this.props.timeseriesTooltipXValueFormatter}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
const graph = this.props.pipelineState.config.graph;
|
||||
|
||||
return (
|
||||
<div className="lspvContainer">
|
||||
<ColaGraph
|
||||
graph={graph}
|
||||
onShowVertexDetails={this.onShowVertexDetails}
|
||||
detailVertex={this.state.detailDrawer.vertex}
|
||||
/>
|
||||
{ this.renderDetailDrawer() }
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
PipelineViewer.propTypes = {
|
||||
pipelineState: PropTypes.shape({
|
||||
config: PropTypes.shape({
|
||||
graph: PropTypes.object.isRequired
|
||||
})
|
||||
}),
|
||||
timeseriesTooltipXValueFormatter: PropTypes.func.isRequired
|
||||
};
|
||||
export { ConfigViewer } from './views/config_viewer';
|
||||
|
|
|
@ -7,7 +7,6 @@
|
|||
import expect from 'expect.js';
|
||||
import { BooleanEdge } from '../boolean_edge';
|
||||
import { Edge } from '../edge';
|
||||
import { LOGSTASH } from '../../../../../../../common/constants';
|
||||
|
||||
describe('BooleanEdge', () => {
|
||||
let graph;
|
||||
|
@ -32,10 +31,4 @@ describe('BooleanEdge', () => {
|
|||
const booleanEdge = new BooleanEdge(graph, edgeJson);
|
||||
expect(booleanEdge).to.be.a(Edge);
|
||||
});
|
||||
|
||||
it('should have the correct SVG CSS class', () => {
|
||||
const booleanEdge = new BooleanEdge(graph, edgeJson);
|
||||
const edgeSvgClass = LOGSTASH.PIPELINE_VIEWER.GRAPH.EDGES.SVG_CLASS;
|
||||
expect(booleanEdge.svgClass).to.be(`${edgeSvgClass} ${edgeSvgClass}Boolean ${edgeSvgClass}Boolean--true`);
|
||||
});
|
||||
});
|
|
@ -6,7 +6,6 @@
|
|||
|
||||
import expect from 'expect.js';
|
||||
import { Edge } from '../edge';
|
||||
import { LOGSTASH } from '../../../../../../../common/constants';
|
||||
|
||||
describe('Edge', () => {
|
||||
let graph;
|
||||
|
@ -26,20 +25,6 @@ describe('Edge', () => {
|
|||
};
|
||||
});
|
||||
|
||||
it('should initialize the webcola representation', () => {
|
||||
const edge = new Edge(graph, edgeJson);
|
||||
expect(edge.cola).to.eql({
|
||||
edge: edge,
|
||||
source: 'bar',
|
||||
target: 17
|
||||
});
|
||||
});
|
||||
|
||||
it('should have a D3-friendly ID', () => {
|
||||
const edge = new Edge(graph, edgeJson);
|
||||
expect(edge.htmlAttrId).to.be('myif_myes');
|
||||
});
|
||||
|
||||
it('should have the correct from vertex', () => {
|
||||
const edge = new Edge(graph, edgeJson);
|
||||
expect(edge.fromId).to.be('myif');
|
||||
|
@ -51,9 +36,4 @@ describe('Edge', () => {
|
|||
expect(edge.toId).to.be('myes');
|
||||
expect(edge.to).to.be(graph.verticesById.myes);
|
||||
});
|
||||
|
||||
it('should have the correct SVG CSS class', () => {
|
||||
const edge = new Edge(graph, edgeJson);
|
||||
expect(edge.svgClass).to.be(LOGSTASH.PIPELINE_VIEWER.GRAPH.EDGES.SVG_CLASS);
|
||||
});
|
||||
});
|
|
@ -6,7 +6,7 @@
|
|||
|
||||
import expect from 'expect.js';
|
||||
import { edgeFactory } from '../edge_factory';
|
||||
import { PlainEdge } from '../plain_edge';
|
||||
import { Edge } from '../edge';
|
||||
import { BooleanEdge } from '../boolean_edge';
|
||||
|
||||
describe('edgeFactory', () => {
|
||||
|
@ -27,9 +27,9 @@ describe('edgeFactory', () => {
|
|||
};
|
||||
});
|
||||
|
||||
it('returns a PlainEdge when edge type is plain', () => {
|
||||
it('returns an Edge when edge type is plain', () => {
|
||||
edgeJson.type = 'plain';
|
||||
expect(edgeFactory(graph, edgeJson)).to.be.a(PlainEdge);
|
||||
expect(edgeFactory(graph, edgeJson)).to.be.a(Edge);
|
||||
});
|
||||
|
||||
it('returns a BooleanEdge when edge type is boolean', () => {
|
||||
|
|
|
@ -144,16 +144,6 @@ describe('Graph', () => {
|
|||
});
|
||||
});
|
||||
|
||||
it('identifies cola representations of vertices correctly', () => {
|
||||
const colaVertices = graph.colaVertices;
|
||||
expect(colaVertices).to.be.an(Array);
|
||||
expect(colaVertices.length).to.be(5);
|
||||
|
||||
expect(colaVertices[0]).to.have.property('vertex');
|
||||
expect(colaVertices[0]).to.have.property('width');
|
||||
expect(colaVertices[0]).to.have.property('height');
|
||||
});
|
||||
|
||||
it('identifies the correct edges', () => {
|
||||
const edges = graph.edges;
|
||||
expect(edges).to.be.an(Array);
|
||||
|
@ -163,81 +153,6 @@ describe('Graph', () => {
|
|||
expect(edge).to.be.an(Edge);
|
||||
});
|
||||
});
|
||||
|
||||
it('identifies cola representations of edges correctly', () => {
|
||||
const colaEdges = graph.colaEdges;
|
||||
expect(colaEdges).to.be.an(Array);
|
||||
expect(colaEdges.length).to.be(4);
|
||||
|
||||
colaEdges.forEach(colaEdge => {
|
||||
expect(colaEdge).to.have.property('edge');
|
||||
expect(colaEdge).to.have.property('source');
|
||||
expect(colaEdge).to.have.property('target');
|
||||
});
|
||||
});
|
||||
|
||||
it('identifies its root vertices correctly', () => {
|
||||
const roots = graph.roots;
|
||||
expect(roots).to.be.an(Array);
|
||||
expect(roots.length).to.be(1);
|
||||
|
||||
roots.forEach(root => {
|
||||
expect(root).to.be.a(Vertex);
|
||||
expect(root.json.id).to.be('my-prefix:my-really-long-named-generator');
|
||||
});
|
||||
});
|
||||
|
||||
it('identifies its leaf vertices correctly', () => {
|
||||
const leaves = graph.leaves;
|
||||
expect(leaves).to.be.an(Array);
|
||||
expect(leaves.length).to.be(2);
|
||||
|
||||
leaves.forEach(leaf => {
|
||||
expect(leaf).to.be.a(Vertex);
|
||||
});
|
||||
});
|
||||
|
||||
it('identifies the highest vertex rank correctly', () => {
|
||||
expect(graph.maxRank).to.be(3);
|
||||
});
|
||||
|
||||
describe('vertex layout ranking', () => {
|
||||
const expectedRanks = [
|
||||
['my-prefix:my-really-long-named-generator'],
|
||||
['my-queue'],
|
||||
['my-if'],
|
||||
['my-grok', 'my-sleep']
|
||||
];
|
||||
|
||||
it('should store a 2d array of the vertices in the expected ranks', () => {
|
||||
const result = graph.verticesByLayoutRank;
|
||||
expectedRanks.forEach((expectedVertexIds, rank) => {
|
||||
const resultVertices = result[rank];
|
||||
expectedVertexIds.forEach(expectedVertexId => {
|
||||
const expectedVertex = graph.getVertexById(expectedVertexId);
|
||||
expect(resultVertices).to.contain(expectedVertex);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('should add a .layoutRank property to each Vertex', () => {
|
||||
expectedRanks.forEach((expectedVertexIds, rank) => {
|
||||
expectedVertexIds.forEach(expectedVertexId => {
|
||||
const vertex = graph.getVertexById(expectedVertexId);
|
||||
expect(vertex.layoutRank).to.be(rank);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('should classify the if triangle correctly', () => {
|
||||
expect(graph.triangularIfGroups.length).to.be(1);
|
||||
expect(graph.triangularIfGroups[0]).to.eql({
|
||||
ifVertex: graph.getVertexById('my-if'),
|
||||
trueVertex: graph.getVertexById('my-grok'),
|
||||
falseVertex: graph.getVertexById('my-sleep'),
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('assigning pipeline stages', () => {
|
||||
|
|
|
@ -1,40 +0,0 @@
|
|||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
import expect from 'expect.js';
|
||||
import { PlainEdge } from '../plain_edge';
|
||||
import { Edge } from '../edge';
|
||||
import { LOGSTASH } from '../../../../../../../common/constants';
|
||||
|
||||
describe('PlainEdge', () => {
|
||||
let graph;
|
||||
let edgeJson;
|
||||
|
||||
beforeEach(() => {
|
||||
graph = {
|
||||
verticesById: {
|
||||
mygenerator: {},
|
||||
myqueue: {}
|
||||
}
|
||||
};
|
||||
edgeJson = {
|
||||
id: 'abcdef',
|
||||
from: 'mygenerator',
|
||||
to: 'myqueue'
|
||||
};
|
||||
});
|
||||
|
||||
it('should be an instance of Edge', () => {
|
||||
const plainEdge = new PlainEdge(graph, edgeJson);
|
||||
expect(plainEdge).to.be.a(Edge);
|
||||
});
|
||||
|
||||
it('should have the correct SVG CSS class', () => {
|
||||
const plainEdge = new PlainEdge(graph, edgeJson);
|
||||
const edgeSvgClass = LOGSTASH.PIPELINE_VIEWER.GRAPH.EDGES.SVG_CLASS;
|
||||
expect(plainEdge.svgClass).to.be(`${edgeSvgClass} ${edgeSvgClass}Plain`);
|
||||
});
|
||||
});
|
|
@ -6,7 +6,6 @@
|
|||
|
||||
import expect from 'expect.js';
|
||||
import { Graph } from '../';
|
||||
import { LOGSTASH } from '../../../../../../../common/constants';
|
||||
|
||||
describe('Vertex', () => {
|
||||
let graph;
|
||||
|
@ -44,16 +43,6 @@ describe('Vertex', () => {
|
|||
graph.update(graphJson);
|
||||
});
|
||||
|
||||
it('should initialize the webcola representation', () => {
|
||||
const margin = LOGSTASH.PIPELINE_VIEWER.GRAPH.VERTICES.MARGIN_PX;
|
||||
const vertex = graph.getVertexById('my-queue');
|
||||
expect(vertex.cola).to.eql({
|
||||
vertex: vertex,
|
||||
width: LOGSTASH.PIPELINE_VIEWER.GRAPH.VERTICES.WIDTH_PX + margin,
|
||||
height: LOGSTASH.PIPELINE_VIEWER.GRAPH.VERTICES.HEIGHT_PX + margin
|
||||
});
|
||||
});
|
||||
|
||||
it('should update the internal json property when update() is called', () => {
|
||||
const vertex = graph.getVertexById('my-queue');
|
||||
const updatedJson = {
|
||||
|
@ -63,18 +52,6 @@ describe('Vertex', () => {
|
|||
expect(vertex.json).to.eql(updatedJson);
|
||||
});
|
||||
|
||||
it('should not change the webcola index after update', () => {
|
||||
const verticesIds = graph.getVertices().map(v => [v.id, v.colaIndex]);
|
||||
graph.update(graphJson);
|
||||
verticesIds.forEach(idAndIndex => {
|
||||
const [id, colaIndex] = idAndIndex;
|
||||
const v = graph.getVertexById(id);
|
||||
console.log("MATCH", v.id, id);
|
||||
expect(v).not.to.be(undefined);
|
||||
expect(v.colaIndex).to.be(colaIndex);
|
||||
});
|
||||
});
|
||||
|
||||
it('should have the correct name', () => {
|
||||
const vertex = graph.getVertexById('my-queue');
|
||||
expect(vertex.name).to.be('some-name');
|
||||
|
@ -85,20 +62,12 @@ describe('Vertex', () => {
|
|||
expect(vertex1.id).to.be(vertex1.json.id);
|
||||
});
|
||||
|
||||
it('should have the correct htmlAttrId', () => {
|
||||
const vertex1 = graph.getVertexById('my-prefix:my-really-long-named-generator');
|
||||
expect(vertex1.htmlAttrId).to.be('my_prefix_my_really_long_named_generator');
|
||||
|
||||
const vertex2 = graph.getVertexById('my-queue');
|
||||
expect(vertex2.htmlAttrId).to.be('my_queue');
|
||||
});
|
||||
|
||||
it('should have the correct subtitle', () => {
|
||||
const vertex1 = graph.getVertexById('my-prefix:my-really-long-named-generator');
|
||||
expect(vertex1.subtitle).to.eql({ display: 'my-prefi … enerator', complete: 'my-prefix:my-really-long-named-generator' });
|
||||
expect(vertex1.subtitle).to.eql('my-prefix:my-really-long-named-generator');
|
||||
|
||||
const vertex2 = graph.getVertexById('my-queue');
|
||||
expect(vertex2.subtitle).to.eql({ display: 'my-queue', complete: 'my-queue' });
|
||||
expect(vertex2.subtitle).to.eql('my-queue');
|
||||
});
|
||||
|
||||
it('should have the correct number of incoming edges', () => {
|
||||
|
@ -169,67 +138,6 @@ describe('Vertex', () => {
|
|||
expect(vertex5.outgoingVertices.length).to.be(0);
|
||||
});
|
||||
|
||||
it('should correctly identify as a root vertex', () => {
|
||||
const vertex1 = graph.getVertexById('my-prefix:my-really-long-named-generator');
|
||||
expect(vertex1.isRoot).to.be(true);
|
||||
|
||||
const vertex2 = graph.getVertexById('my-queue');
|
||||
expect(vertex2.isRoot).to.be(false);
|
||||
|
||||
const vertex3 = graph.getVertexById('my-if');
|
||||
expect(vertex3.isRoot).to.be(false);
|
||||
|
||||
const vertex4 = graph.getVertexById('my-grok');
|
||||
expect(vertex4.isRoot).to.be(false);
|
||||
|
||||
const vertex5 = graph.getVertexById('my-sleep');
|
||||
expect(vertex5.isRoot).to.be(false);
|
||||
});
|
||||
|
||||
it('should correctly identify as a leaf vertex', () => {
|
||||
const vertex1 = graph.getVertexById('my-prefix:my-really-long-named-generator');
|
||||
expect(vertex1.isLeaf).to.be(false);
|
||||
|
||||
const vertex2 = graph.getVertexById('my-queue');
|
||||
expect(vertex2.isLeaf).to.be(false);
|
||||
|
||||
const vertex3 = graph.getVertexById('my-if');
|
||||
expect(vertex3.isLeaf).to.be(false);
|
||||
|
||||
const vertex4 = graph.getVertexById('my-grok');
|
||||
expect(vertex4.isLeaf).to.be(true);
|
||||
|
||||
const vertex5 = graph.getVertexById('my-sleep');
|
||||
expect(vertex5.isLeaf).to.be(true);
|
||||
});
|
||||
|
||||
it('should have the correct rank', () => {
|
||||
const vertex1 = graph.getVertexById('my-prefix:my-really-long-named-generator');
|
||||
expect(vertex1.rank).to.be(0);
|
||||
|
||||
const vertex2 = graph.getVertexById('my-queue');
|
||||
expect(vertex2.rank).to.be(1);
|
||||
|
||||
const vertex3 = graph.getVertexById('my-if');
|
||||
expect(vertex3.rank).to.be(2);
|
||||
|
||||
const vertex4 = graph.getVertexById('my-grok');
|
||||
expect(vertex4.rank).to.be(3);
|
||||
|
||||
const vertex5 = graph.getVertexById('my-sleep');
|
||||
expect(vertex5.rank).to.be(3);
|
||||
});
|
||||
|
||||
it('should have the correct source location', () => {
|
||||
const vertex = graph.getVertexById('my-grok');
|
||||
expect(vertex.sourceLocation).to.be('apc.conf@33:4');
|
||||
});
|
||||
|
||||
it('should have the correct source text', () => {
|
||||
const vertex = graph.getVertexById('my-grok');
|
||||
expect(vertex.sourceText).to.be('foobar');
|
||||
});
|
||||
|
||||
it('should have the correct metadata', () => {
|
||||
const vertex = graph.getVertexById('my-grok');
|
||||
expect(vertex.meta).to.eql({ source_text: 'foobar', source_line: 33, source_column: 4 });
|
||||
|
@ -243,43 +151,7 @@ describe('Vertex', () => {
|
|||
expect(vertex2.stats).to.eql({});
|
||||
});
|
||||
|
||||
it('should correctly identify if it has custom stats', () => {
|
||||
const vertex1 = graph.getVertexById('my-sleep');
|
||||
expect(vertex1.hasCustomStats).to.be(true);
|
||||
|
||||
const vertex2 = graph.getVertexById('my-grok');
|
||||
expect(vertex2.hasCustomStats).to.be(false);
|
||||
});
|
||||
|
||||
it('should correctly report custom stats', () => {
|
||||
const vertex1 = graph.getVertexById('my-sleep');
|
||||
expect(vertex1.customStats).to.eql({ mystat1: 100 });
|
||||
|
||||
const vertex2 = graph.getVertexById('my-grok');
|
||||
expect(vertex2.customStats).to.eql({});
|
||||
});
|
||||
|
||||
describe('lineage', () => {
|
||||
it('should have the correct ancestors', () => {
|
||||
const vertex1 = graph.getVertexById('my-prefix:my-really-long-named-generator');
|
||||
expect(vertex1.ancestors()).to.eql({ vertices: [], edges: [] });
|
||||
|
||||
const vertex2 = graph.getVertexById('my-queue');
|
||||
expect(vertex2.ancestors().vertices.length).to.be(1);
|
||||
expect(vertex2.ancestors().edges.length).to.be(1);
|
||||
|
||||
const vertex3 = graph.getVertexById('my-if');
|
||||
expect(vertex3.ancestors().vertices.length).to.be(2);
|
||||
expect(vertex3.ancestors().edges.length).to.be(2);
|
||||
|
||||
const vertex4 = graph.getVertexById('my-grok');
|
||||
expect(vertex4.ancestors().vertices.length).to.be(3);
|
||||
expect(vertex4.ancestors().edges.length).to.be(3);
|
||||
|
||||
const vertex5 = graph.getVertexById('my-sleep');
|
||||
expect(vertex5.ancestors().vertices.length).to.be(3);
|
||||
expect(vertex5.ancestors().edges.length).to.be(3);
|
||||
});
|
||||
|
||||
it('should have the correct descendants', () => {
|
||||
const vertex1 = graph.getVertexById('my-prefix:my-really-long-named-generator');
|
||||
|
@ -301,28 +173,6 @@ describe('Vertex', () => {
|
|||
expect(vertex5.descendants()).to.eql({ vertices: [], edges: [] });
|
||||
});
|
||||
|
||||
it('should have the correct lineage', () => {
|
||||
const vertex1 = graph.getVertexById('my-prefix:my-really-long-named-generator');
|
||||
expect(vertex1.lineage().vertices.length).to.be(5);
|
||||
expect(vertex1.lineage().edges.length).to.be(4);
|
||||
|
||||
const vertex2 = graph.getVertexById('my-queue');
|
||||
expect(vertex2.lineage().vertices.length).to.be(5);
|
||||
expect(vertex2.lineage().edges.length).to.be(4);
|
||||
|
||||
const vertex3 = graph.getVertexById('my-if');
|
||||
expect(vertex3.lineage().vertices.length).to.be(5);
|
||||
expect(vertex3.lineage().edges.length).to.be(4);
|
||||
|
||||
const vertex4 = graph.getVertexById('my-grok');
|
||||
expect(vertex4.lineage().vertices.length).to.be(4);
|
||||
expect(vertex4.lineage().edges.length).to.be(3);
|
||||
|
||||
const vertex5 = graph.getVertexById('my-sleep');
|
||||
expect(vertex5.lineage().vertices.length).to.be(4);
|
||||
expect(vertex5.lineage().edges.length).to.be(3);
|
||||
});
|
||||
|
||||
describe('it should handle complex topologies correctly', () => {
|
||||
/**
|
||||
* I1
|
||||
|
@ -372,23 +222,9 @@ describe('Vertex', () => {
|
|||
graph = new Graph();
|
||||
graph.update(complexGraphJson);
|
||||
});
|
||||
|
||||
it('should calculate the lineage correctly', () => {
|
||||
const vertex1 = graph.getVertexById('F5');
|
||||
expect(vertex1.lineage().vertices.length).to.be(9);
|
||||
expect(vertex1.lineage().edges.length).to.be(10);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('should have the correct events per current period', () => {
|
||||
const vertex1 = graph.getVertexById('my-sleep');
|
||||
expect(vertex1.eventsPerCurrentPeriod).to.be(20);
|
||||
|
||||
const vertex2 = graph.getVertexById('my-grok');
|
||||
expect(vertex2.eventsPerCurrentPeriod).to.be(null);
|
||||
});
|
||||
|
||||
it('should correctly identify if it has an explicit ID', () => {
|
||||
const vertex1 = graph.getVertexById('my-prefix:my-really-long-named-generator');
|
||||
expect(vertex1.hasExplicitId).to.be(false);
|
||||
|
|
|
@ -18,8 +18,4 @@ export class BooleanEdge extends Edge {
|
|||
get isFalse() {
|
||||
return this.when === false;
|
||||
}
|
||||
|
||||
get svgClass() {
|
||||
return `${super.svgClass} ${super.svgClass}Boolean ${super.svgClass}Boolean--${this.when}`;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -4,22 +4,10 @@
|
|||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { LOGSTASH } from '../../../../../../common/constants';
|
||||
|
||||
export class Edge {
|
||||
constructor(graph, json) {
|
||||
this.graph = graph;
|
||||
this.update(json);
|
||||
|
||||
this.cola = this._makeCola();
|
||||
}
|
||||
|
||||
_makeCola() {
|
||||
return {
|
||||
edge: this,
|
||||
source: this.from.cola,
|
||||
target: this.to.cola
|
||||
};
|
||||
}
|
||||
|
||||
update(json) {
|
||||
|
@ -30,12 +18,6 @@ export class Edge {
|
|||
return this.json.id;
|
||||
}
|
||||
|
||||
get htmlAttrId() {
|
||||
// Substitute any non-word characters with an underscore so
|
||||
// D3 selections don't interpret them as special selector syntax
|
||||
return this.json.id.replace(/\W/, '_');
|
||||
}
|
||||
|
||||
get from() {
|
||||
return this.graph.verticesById[this.fromId];
|
||||
}
|
||||
|
@ -51,8 +33,4 @@ export class Edge {
|
|||
get toId() {
|
||||
return this.json.to;
|
||||
}
|
||||
|
||||
get svgClass() {
|
||||
return LOGSTASH.PIPELINE_VIEWER.GRAPH.EDGES.SVG_CLASS;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -4,14 +4,14 @@
|
|||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { PlainEdge } from './plain_edge';
|
||||
import { Edge } from './edge';
|
||||
import { BooleanEdge } from './boolean_edge';
|
||||
|
||||
export function edgeFactory(graph, edgeJson) {
|
||||
const type = edgeJson.type;
|
||||
switch (type) {
|
||||
case 'plain':
|
||||
return new PlainEdge(graph, edgeJson);
|
||||
return new Edge(graph, edgeJson);
|
||||
case 'boolean':
|
||||
return new BooleanEdge(graph, edgeJson);
|
||||
default:
|
||||
|
|
|
@ -26,14 +26,7 @@ export class IfVertex extends Vertex {
|
|||
}
|
||||
|
||||
get subtitle() {
|
||||
return {
|
||||
complete: this.name,
|
||||
display: this.truncateStringForDisplay(this.name, this.displaySubtitleMaxLength)
|
||||
};
|
||||
}
|
||||
|
||||
get displaySubtitleMaxLength() {
|
||||
return 39;
|
||||
return this.name;
|
||||
}
|
||||
|
||||
get trueEdge() {
|
||||
|
|
|
@ -7,7 +7,6 @@
|
|||
import { vertexFactory } from './vertex_factory';
|
||||
import { edgeFactory } from './edge_factory';
|
||||
import { QueueVertex } from './queue_vertex';
|
||||
import { IfVertex } from './if_vertex';
|
||||
import { PluginVertex } from './plugin_vertex';
|
||||
|
||||
export class Graph {
|
||||
|
@ -24,9 +23,6 @@ export class Graph {
|
|||
}
|
||||
|
||||
getVertices() {
|
||||
// We need a stable order for webcola
|
||||
// constraints don't work by anything other than index :(
|
||||
|
||||
// Its safe to cache vertices because vertices are never added or removed from the graph. This is because
|
||||
// such changes also result in changing the hash of the pipeline, which ends up creating a new graph altogether.
|
||||
if (this.vertexCache === undefined) {
|
||||
|
@ -35,10 +31,6 @@ export class Graph {
|
|||
return this.vertexCache;
|
||||
}
|
||||
|
||||
get inputVertices() {
|
||||
return this.getVertices().filter(v => v.isInput);
|
||||
}
|
||||
|
||||
get queueVertex() {
|
||||
return this.getVertices().find(v => v instanceof QueueVertex);
|
||||
}
|
||||
|
@ -47,26 +39,10 @@ export class Graph {
|
|||
return this.getVertices().filter(v => v.isProcessor);
|
||||
}
|
||||
|
||||
get outputVertices() {
|
||||
return this.getVertices().filter(v => v.isOutput);
|
||||
}
|
||||
|
||||
get ifVertices() {
|
||||
return this.getVertices().filter(v => v instanceof IfVertex);
|
||||
}
|
||||
|
||||
get colaVertices() {
|
||||
return this.getVertices().map(v => v.cola);
|
||||
}
|
||||
|
||||
get edges() {
|
||||
return Object.values(this.edgesById);
|
||||
}
|
||||
|
||||
get colaEdges() {
|
||||
return this.edges.map(e => e.cola);
|
||||
}
|
||||
|
||||
update(jsonRepresentation) {
|
||||
this.json = jsonRepresentation;
|
||||
|
||||
|
@ -99,199 +75,9 @@ export class Graph {
|
|||
}
|
||||
});
|
||||
|
||||
// These maps are what the vertices use for their .rank and .reverseRank getters
|
||||
this.vertexRankById = this._bfs().distances;
|
||||
|
||||
// A separate rank algorithm used for formatting purposes
|
||||
this.verticesByLayoutRank = this.calculateVerticesByLayoutRank();
|
||||
|
||||
// For layout purposes we treat triangular ifs, that is to say
|
||||
// 'if' vertices of rank N with both T and F children at rank N+1
|
||||
// in special ways to get a clean render.
|
||||
this.triangularIfGroups = this.calculateTriangularIfGroups();
|
||||
|
||||
this.annotateVerticesWithStages();
|
||||
}
|
||||
|
||||
verticesByRank() {
|
||||
const byRank = [];
|
||||
Object.values(this.verticesById).forEach(vertex => {
|
||||
const rank = vertex.rank;
|
||||
if (byRank[rank] === undefined) {
|
||||
byRank[rank] = [];
|
||||
}
|
||||
byRank[rank].push(vertex);
|
||||
});
|
||||
return byRank;
|
||||
}
|
||||
|
||||
// Can only be run after layout ranks are calculated!
|
||||
calculateTriangularIfGroups() {
|
||||
return this.getVertices().filter(v => {
|
||||
return v.typeString === 'if' &&
|
||||
!v.outgoingVertices.find(outV => outV.layoutRank !== (v.layoutRank + 1));
|
||||
}).map(ifV => {
|
||||
const trueEdge = ifV.outgoingEdges.filter(e => e.when === true)[0];
|
||||
const falseEdge = ifV.outgoingEdges.filter(e => e.when === false)[0];
|
||||
const result = { ifVertex: ifV };
|
||||
if (trueEdge) {
|
||||
result.trueVertex = trueEdge.to;
|
||||
}
|
||||
if (falseEdge) {
|
||||
result.falseVertex = falseEdge.to;
|
||||
}
|
||||
return result;
|
||||
});
|
||||
}
|
||||
|
||||
calculateVerticesByLayoutRank() {
|
||||
// We will mutate this throughout this function
|
||||
// to produce our output
|
||||
const result = this.verticesByRank();
|
||||
|
||||
// Find the rank of a vertex in our output
|
||||
// Normally you'd grab that information from `vertex.layoutRank`
|
||||
// but since we're recomputing that here we need something directly linked
|
||||
// to the intermediate result
|
||||
const rankOf = (vertex) => {
|
||||
const foundRankVertices = result.find((rankVertices) => {
|
||||
return rankVertices.find(v => v === vertex);
|
||||
});
|
||||
return result.indexOf(foundRankVertices);
|
||||
};
|
||||
|
||||
// This function is really an engine for applying rules
|
||||
// These rules are evaluated in order. Each rule can produce one 'promotion', that is it
|
||||
// can specify that a single vertex of rank N be promoted to rank N+1
|
||||
// These rules will be repeatedly invoked on a rank's vertices until the rule has no effect
|
||||
// which is determined by the rule returning `null`
|
||||
const promotionRules = [
|
||||
// Our first rule is that vertices that are pointed to by other nodes within the rank, but do
|
||||
// not point to other nodes within the rank should be promoted
|
||||
// This produces a more desirable layout by mostly eliminating horizontal links, which must
|
||||
// cross over other links thus creating a confusing layout most of the time.
|
||||
(vertices) => {
|
||||
const found = vertices.find((v) => {
|
||||
const hasIncomingOfSameRank = v.incomingVertices.find(inV => rankOf(inV) === rankOf(v));
|
||||
const hasOutgoingOfSameRank = v.outgoingVertices.find(outV => rankOf(outV) === rankOf(v));
|
||||
return hasIncomingOfSameRank && hasOutgoingOfSameRank === undefined;
|
||||
});
|
||||
if (found) {
|
||||
return found;
|
||||
}
|
||||
|
||||
return null;
|
||||
},
|
||||
// This rule is quite simple, simply limiting the maximum number of nodes in a rank to 3.
|
||||
// Beyond this number the graph becomes too compact, links often start crossing over each other
|
||||
// and readability suffers
|
||||
(vertices) => {
|
||||
if (vertices.length > 3) {
|
||||
return vertices[0];
|
||||
}
|
||||
return null;
|
||||
}
|
||||
];
|
||||
|
||||
// This is the core of this function, wherein we iterate through the ranks and apply the rules
|
||||
for (let rank = 0; rank < result.length; rank++) {
|
||||
const vertices = result[rank];
|
||||
// Iterate through each rule
|
||||
promotionRules.forEach(rule => {
|
||||
let ruleConverged = false;
|
||||
// Execute each rule against the vertices within the rank until the rule has no more
|
||||
// mutations to make
|
||||
while(!ruleConverged) {
|
||||
const promotedVertex = rule(vertices, result);
|
||||
// If the rule has found a vertex to promote
|
||||
if (promotedVertex !== null) {
|
||||
const promotedIndex = vertices.indexOf(promotedVertex);
|
||||
// move the vertex found by the rule from this rank and move it to the next one
|
||||
vertices.splice(promotedIndex, 1)[0];
|
||||
// We may be making a new rank, if so we'll need to seed it with an empty array
|
||||
if (result[rank + 1] === undefined) {
|
||||
result[rank + 1] = [];
|
||||
}
|
||||
result[rank + 1].push(promotedVertex);
|
||||
} else {
|
||||
ruleConverged = true;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Set separated rank as a property on each vertex
|
||||
for (let rank = 0; rank < result.length; rank++) {
|
||||
const rankVertices = result[rank];
|
||||
rankVertices.forEach(v => v.layoutRank = rank);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
get roots() {
|
||||
return this.getVertices().filter((v) => v.isRoot);
|
||||
}
|
||||
|
||||
get leaves() {
|
||||
return this.getVertices().filter((v) => v.isLeaf);
|
||||
}
|
||||
|
||||
get maxRank() {
|
||||
return Math.max.apply(null, this.getVertices().map(v => v.rank));
|
||||
}
|
||||
|
||||
_getReverseVerticesByRank() {
|
||||
return this.getVertices().reduce((acc, v) => {
|
||||
const rank = v.reverseRank;
|
||||
if (acc.get(rank) === undefined) {
|
||||
acc.set(rank, []);
|
||||
}
|
||||
acc.get(rank).push(v);
|
||||
return acc;
|
||||
}, new Map());
|
||||
}
|
||||
|
||||
_bfs() {
|
||||
return this._bfsTraversalUsing(this.roots, 'outgoing');
|
||||
}
|
||||
|
||||
_reverseBfs() {
|
||||
return this._bfsTraversalUsing(this.leaves, 'incoming');
|
||||
}
|
||||
|
||||
/**
|
||||
* Performs a breadth-first or reverse-breadth-first search
|
||||
* @param {array} startingVertices Where to start the search - either this.roots (for breadth-first) or this.leaves (for reverse-breadth-first)
|
||||
* @param {string} vertexType Either 'outgoing' (for breadth-first) or 'incoming' (for reverse-breadth-first)
|
||||
*/
|
||||
_bfsTraversalUsing(startingVertices, vertexType) {
|
||||
const distances = {};
|
||||
const parents = {};
|
||||
const queue = [];
|
||||
const vertexTypePropertyName = `${vertexType}Vertices`;
|
||||
|
||||
startingVertices.forEach((v) => {
|
||||
distances[v.id] = 0;
|
||||
queue.push(v);
|
||||
});
|
||||
while (queue.length > 0) {
|
||||
const currentVertex = queue.shift();
|
||||
const currentDistance = distances[currentVertex.id];
|
||||
|
||||
currentVertex[vertexTypePropertyName].forEach((vertex) => {
|
||||
if (distances[vertex.id] === undefined) {
|
||||
distances[vertex.id] = currentDistance + 1;
|
||||
parents[vertex.id] = currentVertex;
|
||||
queue.push(vertex);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return { distances, parents };
|
||||
}
|
||||
|
||||
|
||||
get startVertices() {
|
||||
return this.getVertices().filter(v => v.incomingEdges.length === 0);
|
||||
}
|
||||
|
|
|
@ -1,13 +0,0 @@
|
|||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
import { Edge } from './edge';
|
||||
|
||||
export class PlainEdge extends Edge {
|
||||
get svgClass() {
|
||||
return `${super.svgClass} ${super.svgClass}Plain`;
|
||||
}
|
||||
}
|
|
@ -4,43 +4,16 @@
|
|||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { LOGSTASH } from '../../../../../../common/constants';
|
||||
|
||||
export class Vertex {
|
||||
constructor(graph, json) {
|
||||
this.graph = graph;
|
||||
this.update(json);
|
||||
|
||||
// Version of the representation used by webcola
|
||||
// this object is a bridge back to here, and also can be mutated by webcola
|
||||
// and d3, which like to change objects
|
||||
this.cola = this._makeCola();
|
||||
}
|
||||
|
||||
update(json) {
|
||||
this.json = json;
|
||||
}
|
||||
|
||||
// Should only be called by the constructor!
|
||||
// There is no reason to have > 1 instance of this!
|
||||
// There is really no good reason to add any additional fields here
|
||||
_makeCola() {
|
||||
const margin = LOGSTASH.PIPELINE_VIEWER.GRAPH.VERTICES.MARGIN_PX;
|
||||
return {
|
||||
vertex: this,
|
||||
// The margin size must be added since this is actually the size of the bounding box
|
||||
width: LOGSTASH.PIPELINE_VIEWER.GRAPH.VERTICES.WIDTH_PX + margin,
|
||||
height: LOGSTASH.PIPELINE_VIEWER.GRAPH.VERTICES.HEIGHT_PX + margin
|
||||
};
|
||||
}
|
||||
|
||||
get colaIndex() {
|
||||
if (!this._colaIndex) {
|
||||
this._colaIndex = this.graph.getVertices().indexOf(this);
|
||||
}
|
||||
return this._colaIndex;
|
||||
}
|
||||
|
||||
get name() {
|
||||
return this.json.config_name;
|
||||
}
|
||||
|
@ -49,21 +22,8 @@ export class Vertex {
|
|||
return this.json.id;
|
||||
}
|
||||
|
||||
get htmlAttrId() {
|
||||
// Substitute any non-word characters with an underscore so
|
||||
// D3 selections don't interpret them as special selector syntax
|
||||
return this.json.id.replace(/\W/g, '_');
|
||||
}
|
||||
|
||||
get subtitle() {
|
||||
return {
|
||||
complete: this.id,
|
||||
display: this.truncateStringForDisplay(this.id, this.displaySubtitleMaxLength)
|
||||
};
|
||||
}
|
||||
|
||||
get displaySubtitleMaxLength() {
|
||||
return 19;
|
||||
return this.id;
|
||||
}
|
||||
|
||||
get incomingEdges() {
|
||||
|
@ -82,26 +42,6 @@ export class Vertex {
|
|||
return this.outgoingEdges.map(e => e.to);
|
||||
}
|
||||
|
||||
get isRoot() {
|
||||
return this.incomingVertices.length === 0;
|
||||
}
|
||||
|
||||
get isLeaf() {
|
||||
return this.outgoingVertices.length === 0;
|
||||
}
|
||||
|
||||
get rank() {
|
||||
return this.graph.vertexRankById[this.id];
|
||||
}
|
||||
|
||||
get sourceLocation() {
|
||||
return `apc.conf@${this.meta.source_line}:${this.meta.source_column}`;
|
||||
}
|
||||
|
||||
get sourceText() {
|
||||
return this.meta.source_text;
|
||||
}
|
||||
|
||||
get meta() {
|
||||
return this.json.meta;
|
||||
}
|
||||
|
@ -110,53 +50,6 @@ export class Vertex {
|
|||
return this.json.stats || {};
|
||||
}
|
||||
|
||||
get hasCustomStats() {
|
||||
return Object.keys(this.customStats).length > 0;
|
||||
}
|
||||
get customStats() {
|
||||
return Object.keys(this.stats)
|
||||
.filter(k => !(k.match(/^events\./)))
|
||||
.filter(k => k !== 'name')
|
||||
.reduce((acc, k) => {
|
||||
acc[k] = this.stats[k];
|
||||
return acc;
|
||||
}, {});
|
||||
}
|
||||
|
||||
lineage() {
|
||||
const ancestors = this.ancestors();
|
||||
const descendants = this.descendants();
|
||||
|
||||
const vertices = [];
|
||||
vertices.push.apply(vertices, ancestors.vertices);
|
||||
vertices.push(this);
|
||||
vertices.push.apply(vertices, descendants.vertices);
|
||||
|
||||
const edges = ancestors.edges.concat(descendants.edges);
|
||||
|
||||
return { vertices, edges };
|
||||
}
|
||||
|
||||
ancestors() {
|
||||
const vertices = [];
|
||||
const edges = [];
|
||||
const pending = [this];
|
||||
const seen = {};
|
||||
while (pending.length > 0) {
|
||||
const vertex = pending.pop();
|
||||
vertex.incomingEdges.forEach(edge => {
|
||||
edges.push(edge);
|
||||
const from = edge.from;
|
||||
if (seen[from.id] !== true) {
|
||||
vertices.push(from);
|
||||
pending.push(from);
|
||||
seen[from.id] = true;
|
||||
}
|
||||
});
|
||||
}
|
||||
return { vertices, edges };
|
||||
}
|
||||
|
||||
descendants() {
|
||||
const vertices = [];
|
||||
const edges = [];
|
||||
|
@ -177,26 +70,7 @@ export class Vertex {
|
|||
return { vertices, edges };
|
||||
}
|
||||
|
||||
get eventsPerCurrentPeriod() {
|
||||
if (!this.stats.hasOwnProperty('events.in')) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (this.stats['events.in'].max - this.stats['events.in'].min);
|
||||
}
|
||||
|
||||
get hasExplicitId() {
|
||||
return Boolean(this.json.explicit_id);
|
||||
}
|
||||
|
||||
truncateStringForDisplay(completeString, maxDisplayLength) {
|
||||
if (completeString.length <= maxDisplayLength) {
|
||||
return completeString;
|
||||
}
|
||||
|
||||
const ellipses = ' \u2026 ';
|
||||
const eachHalfMaxDisplayLength = Math.floor((maxDisplayLength - ellipses.length) / 2);
|
||||
|
||||
return `${completeString.substr(0, eachHalfMaxDisplayLength)}${ellipses}${completeString.substr(-eachHalfMaxDisplayLength)}`;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -167,9 +167,7 @@ describe('DetailDrawer component', () => {
|
|||
const vertex = {
|
||||
title: 'if',
|
||||
typeString: 'if',
|
||||
subtitle: {
|
||||
complete: '[type] == "apache_log"'
|
||||
}
|
||||
subtitle: '[type] == "apache_log"'
|
||||
};
|
||||
|
||||
const component = (
|
||||
|
|
|
@ -1,470 +0,0 @@
|
|||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import d3 from 'd3';
|
||||
import { PluginVertex } from '../models/graph/plugin_vertex';
|
||||
import { IfVertex } from '../models/graph/if_vertex';
|
||||
import { QueueVertex } from '../models/graph/queue_vertex';
|
||||
import {
|
||||
enterInputVertex,
|
||||
enterProcessorVertex,
|
||||
enterIfVertex,
|
||||
enterQueueVertex,
|
||||
updateInputVertex,
|
||||
updateProcessorVertex
|
||||
} from './vertex_content_renderer';
|
||||
import { LOGSTASH } from '../../../../../common/constants';
|
||||
import { makeEdgeBetween, d3adaptor } from 'webcola';
|
||||
|
||||
function makeMarker(svgDefs, id, fill) {
|
||||
svgDefs.append('marker')
|
||||
.attr('id', id)
|
||||
.attr('viewBox', '0 -5 10 10')
|
||||
.attr('refX', 5)
|
||||
.attr('markerWidth', 3)
|
||||
.attr('markerHeight', 3)
|
||||
.attr('orient', 'auto')
|
||||
.append('path')
|
||||
.attr('d', 'M0,-5L10,0L0,5L2,0')
|
||||
.attr('stroke-width', '0px')
|
||||
.attr('fill', fill);
|
||||
}
|
||||
|
||||
function makeBackground(parentEl) {
|
||||
return parentEl
|
||||
.append('rect')
|
||||
.attr('width', '100%')
|
||||
.attr('height', '100%')
|
||||
.attr('fill', '#efefef');
|
||||
}
|
||||
|
||||
function makeGroup(parentEl) {
|
||||
return parentEl
|
||||
.append('g');
|
||||
}
|
||||
|
||||
function makeNodes(nodesLayer, colaVertices) {
|
||||
const nodes = nodesLayer
|
||||
.selectAll('.lspvVertex')
|
||||
.data(colaVertices, d => d.vertex.htmlAttrId);
|
||||
|
||||
nodes
|
||||
.enter()
|
||||
.append('g')
|
||||
.attr('id', d => `nodeg-${d.vertex.htmlAttrId}`)
|
||||
.attr('class', d => `lspvVertex ${d.vertex.typeString}`)
|
||||
.attr('width', LOGSTASH.PIPELINE_VIEWER.GRAPH.VERTICES.WIDTH_PX)
|
||||
.attr('height', LOGSTASH.PIPELINE_VIEWER.GRAPH.VERTICES.HEIGHT_PX);
|
||||
|
||||
nodes
|
||||
.append('rect')
|
||||
.attr('class', 'lspvVertexBounding')
|
||||
.attr('rx', LOGSTASH.PIPELINE_VIEWER.GRAPH.VERTICES.BORDER_RADIUS_PX)
|
||||
.attr('ry', LOGSTASH.PIPELINE_VIEWER.GRAPH.VERTICES.BORDER_RADIUS_PX);
|
||||
|
||||
return nodes;
|
||||
}
|
||||
|
||||
function addNodesMouseBehaviors(nodes, onMouseover, onMouseout, onMouseclick) {
|
||||
nodes.on('mouseover', onMouseover);
|
||||
nodes.on('mouseout', onMouseout);
|
||||
nodes.on('click', onMouseclick);
|
||||
}
|
||||
|
||||
function makeInputNodes(nodes) {
|
||||
const inputs = nodes.filter(node => (node.vertex instanceof PluginVertex) && node.vertex.isInput);
|
||||
inputs.call(enterInputVertex);
|
||||
|
||||
return inputs;
|
||||
}
|
||||
|
||||
function makeProcessorNodes(nodes) {
|
||||
const processors = nodes.filter(node => (node.vertex instanceof PluginVertex) && node.vertex.isProcessor);
|
||||
processors.call(enterProcessorVertex);
|
||||
|
||||
return processors;
|
||||
}
|
||||
|
||||
function makeIfNodes(nodes) {
|
||||
const ifs = nodes.filter(d => d.vertex instanceof IfVertex);
|
||||
ifs.call(enterIfVertex);
|
||||
|
||||
return ifs;
|
||||
}
|
||||
|
||||
function makeQueueNode(nodes) {
|
||||
const queue = nodes.filter(d => d.vertex instanceof QueueVertex);
|
||||
queue.call(enterQueueVertex);
|
||||
|
||||
return queue;
|
||||
}
|
||||
|
||||
// Line function for drawing paths between nodes
|
||||
const lineFunction = d3.svg.line()
|
||||
// Null check that handles a bug in webcola where sometimes these values are null for a tick
|
||||
.x(d => d ? d.x : null)
|
||||
.y(d => d ? d.y : null);
|
||||
|
||||
export class ColaGraph extends React.Component {
|
||||
constructor() {
|
||||
super();
|
||||
this.state = {};
|
||||
|
||||
this.width = 1000;
|
||||
this.height = 1000;
|
||||
}
|
||||
|
||||
renderGraph(svgEl) {
|
||||
this.d3cola = d3adaptor()
|
||||
.avoidOverlaps(true)
|
||||
.size([this.width, this.height]);
|
||||
|
||||
const outer = d3.select(svgEl);
|
||||
const background = makeBackground(outer);
|
||||
|
||||
const svgDefs = outer.append('defs');
|
||||
makeMarker(svgDefs, 'lspvPlainMarker', '#000');
|
||||
makeMarker(svgDefs, 'lspvTrueMarker', '#1BAFD2');
|
||||
makeMarker(svgDefs, 'lspvFalseMarker', '#EE408A');
|
||||
|
||||
// Set initial zoom to 100%. You need both the translate and scale options
|
||||
const zoom = d3.behavior.zoom().translate([100, 100]).scale(1);
|
||||
const vis = outer
|
||||
.append('g')
|
||||
.attr('transform', 'translate(0,0) scale(1)');
|
||||
|
||||
const redraw = () => {
|
||||
vis.attr('transform', `translate(${d3.event.translate}) scale(${d3.event.scale})`);
|
||||
};
|
||||
|
||||
outer.call(d3.behavior.zoom().on('zoom', redraw));
|
||||
background.call(zoom.on('zoom', redraw));
|
||||
|
||||
this.nodesLayer = makeGroup(vis);
|
||||
this.nodes = makeNodes(this.nodesLayer, this.graph.colaVertices);
|
||||
|
||||
this.inputs = makeInputNodes(this.nodes);
|
||||
this.processors = makeProcessorNodes(this.nodes);
|
||||
this.ifs = makeIfNodes(this.nodes);
|
||||
this.queue = makeQueueNode(this.nodes);
|
||||
|
||||
addNodesMouseBehaviors(this.nodes, this.onMouseover, this.onMouseout, this.onMouseclick);
|
||||
|
||||
this.linksLayer = makeGroup(vis);
|
||||
|
||||
const ifTriangleColaGroups = this.graph.triangularIfGroups.map(group => {
|
||||
return { leaves: Object.values(group).map(v => v.colaIndex) };
|
||||
});
|
||||
|
||||
this.d3cola
|
||||
.nodes(this.graph.colaVertices)
|
||||
.links(this.graph.colaEdges)
|
||||
.groups(ifTriangleColaGroups)
|
||||
.constraints(this._getConstraints())
|
||||
// This number controls the max number of iterations for the layout iteration to
|
||||
// solve the constraints. Higher numbers usually wind up in a better layout
|
||||
.start(10000);
|
||||
|
||||
this.makeLinks();
|
||||
|
||||
let tickStart;
|
||||
let ticks = 0;
|
||||
|
||||
// Minimum amount of time between reflows
|
||||
// We want a value that looks interactive but doesn't waste CPU time rendering intermediate results
|
||||
const reflowEvery = 1000; // 1s
|
||||
// Amount of time to allow the solver to run
|
||||
// We want a value that isn't so long that the user gets irritated using the graph due to the CPU being monopolized
|
||||
// by the constraint solver
|
||||
const maxDuration = 10000; // 10s
|
||||
let lastReflow = new Date();
|
||||
this.d3cola
|
||||
.on('tick', () => {
|
||||
const now = new Date();
|
||||
|
||||
ticks++;
|
||||
if (ticks === 1) {
|
||||
tickStart = now;
|
||||
}
|
||||
|
||||
const elapsedSinceLastReflow = now - lastReflow;
|
||||
if (ticks === 1 || elapsedSinceLastReflow >= reflowEvery) {
|
||||
this.reflow();
|
||||
lastReflow = now;
|
||||
}
|
||||
const totalElapsed = now - tickStart;
|
||||
if (totalElapsed >= maxDuration) {
|
||||
this.d3cola.stop();
|
||||
this.reflow();
|
||||
console.log("Logstash graph visualizer constraint timeout! Rendering will stop here.");
|
||||
}
|
||||
})
|
||||
.on('end', this.reflow);
|
||||
}
|
||||
|
||||
// Actually render the latest webcola state
|
||||
reflow = () => {
|
||||
this.setNodeBounds();
|
||||
this.routeAndLabelEdges();
|
||||
}
|
||||
|
||||
setNodeBounds = () => {
|
||||
this.nodes.each((d) => d.innerBounds = d.bounds.inflate(-LOGSTASH.PIPELINE_VIEWER.GRAPH.VERTICES.MARGIN_PX));
|
||||
this.nodes.attr('transform', (d) => `translate(${d.innerBounds.x}, ${d.innerBounds.y})`);
|
||||
this.nodes.select('rect')
|
||||
.attr('width', (d) => d.innerBounds.width())
|
||||
.attr('height', (d) => d.innerBounds.height());
|
||||
}
|
||||
|
||||
routeAndLabelEdges = () => {
|
||||
this.links.attr('d', (d) => {
|
||||
const arrowStart = LOGSTASH.PIPELINE_VIEWER.GRAPH.EDGES.ARROW_START;
|
||||
const route = makeEdgeBetween(d.source.innerBounds, d.target.innerBounds, arrowStart);
|
||||
return lineFunction([route.sourceIntersection, route.arrowStart]);
|
||||
});
|
||||
this.routeEdges();
|
||||
this.labelEdges();
|
||||
}
|
||||
|
||||
routeEdges() {
|
||||
this.d3cola.prepareEdgeRouting(LOGSTASH.PIPELINE_VIEWER.GRAPH.EDGES.ROUTING_MARGIN_PX);
|
||||
this.links.select('path').attr('d', (d) => {
|
||||
try {
|
||||
return lineFunction(this.d3cola.routeEdge(d));
|
||||
} catch (err) {
|
||||
console.error('Could not exec line function!', err);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
labelEdges() {
|
||||
// Use a regular function instead of () => since we want the dom element via `this`,
|
||||
// only accessible via d3 setting 'this' AFAIK
|
||||
this.booleanLabels.each(function () {
|
||||
const path = d3.select(this.parentNode).select('path')[0][0];
|
||||
const pathLength = path.getTotalLength();
|
||||
if (pathLength === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const center = path.getPointAtLength(pathLength / 2);
|
||||
const group = d3.select(this);
|
||||
group.select('circle')
|
||||
.attr('cx', center.x)
|
||||
.attr('cy', center.y);
|
||||
|
||||
// Offset by to vertically center the text
|
||||
const textVerticalOffset = 5;
|
||||
group.select('text')
|
||||
.attr('x', center.x)
|
||||
.attr('y', center.y + textVerticalOffset);
|
||||
});
|
||||
}
|
||||
|
||||
makeLinks() {
|
||||
this.links = this.linksLayer.selectAll('.link')
|
||||
.data(this.graph.colaEdges);
|
||||
|
||||
const linkGroup = this.links.enter()
|
||||
.append('g')
|
||||
.attr('id', (d) => `lspvEdge-${d.edge.htmlAttrId}`)
|
||||
.attr('class', (d) => d.edge.svgClass);
|
||||
linkGroup.append('path');
|
||||
|
||||
const booleanLinks = linkGroup.filter('.lspvEdgeBoolean');
|
||||
this.booleanLabels = booleanLinks
|
||||
.append('g')
|
||||
.attr('class', 'lspvBooleanLabel');
|
||||
|
||||
this.booleanLabels
|
||||
.append('circle')
|
||||
.attr('r', LOGSTASH.PIPELINE_VIEWER.GRAPH.EDGES.LABEL_RADIUS);
|
||||
this.booleanLabels
|
||||
.append('text')
|
||||
.attr('text-anchor', 'middle') // Position the text on its vertical
|
||||
.text(d => d.edge.when ? 'T' : 'F');
|
||||
}
|
||||
|
||||
updateGraph(nextProps = {}, nextState = {}) {
|
||||
this.processors.call(updateProcessorVertex);
|
||||
this.inputs.call(updateInputVertex);
|
||||
|
||||
this.nodesLayer.selectAll('.lspvVertexBounding-highlighted').classed('lspvVertexBounding-highlighted', false);
|
||||
this.nodesLayer.selectAll('.lspvVertex-grayed').classed('lspvVertex-grayed', false);
|
||||
this.linksLayer.selectAll('.lspvEdge-grayed').classed('lspvEdge-grayed', false);
|
||||
|
||||
const hoverNode = nextState.hoverNode;
|
||||
if (hoverNode) {
|
||||
const selection = this.nodesLayer
|
||||
.selectAll('#nodeg-' + hoverNode.vertex.htmlAttrId)
|
||||
.selectAll('rect');
|
||||
selection.classed('lspvVertexBounding-highlighted', true);
|
||||
|
||||
const lineage = hoverNode.vertex.lineage();
|
||||
|
||||
const lineageVertices = lineage.vertices;
|
||||
const nonLineageVertices = this.graph.getVertices().filter(v => lineageVertices.indexOf(v) === -1);
|
||||
const grayedVertices = this.nodesLayer.selectAll('g.lspvVertex').filter(d => nonLineageVertices.indexOf(d.vertex) >= 0);
|
||||
grayedVertices.classed('lspvVertex-grayed', true);
|
||||
|
||||
const lineageEdges = lineage.edges;
|
||||
const nonLineageEdges = this.graph.edges.filter(e => lineageEdges.indexOf(e) === -1);
|
||||
const grayedEdges = this.linksLayer.selectAll('.lspvEdge').filter(d => nonLineageEdges.indexOf(d.edge) >= 0);
|
||||
grayedEdges.classed('lspvEdge-grayed', true);
|
||||
}
|
||||
|
||||
const detailVertex = nextProps.detailVertex;
|
||||
if (detailVertex) {
|
||||
const selection = this.nodesLayer
|
||||
.selectAll('#nodeg-' + detailVertex.htmlAttrId)
|
||||
.selectAll('rect');
|
||||
selection.classed('lspvVertexBounding-highlighted', true);
|
||||
}
|
||||
}
|
||||
|
||||
onMouseover = (node) => {
|
||||
this.setState({ hoverNode: node });
|
||||
}
|
||||
|
||||
onMouseout = () => {
|
||||
this.setState({ hoverNode: null });
|
||||
}
|
||||
|
||||
onMouseclick = (e) => {
|
||||
this.props.onShowVertexDetails(e.vertex);
|
||||
}
|
||||
|
||||
get graph() {
|
||||
return this.props.graph;
|
||||
}
|
||||
|
||||
_getConstraints() {
|
||||
// To understand webcola constraints please read:
|
||||
// https://github.com/tgdwyer/WebCola/wiki/Constraints
|
||||
const constraints = [];
|
||||
const verticesByRank = this.graph.verticesByLayoutRank;
|
||||
|
||||
// Lay out triangle groups as... a triangle! That is to say,
|
||||
// with an if in the middle and the true on the left and the false on the right
|
||||
this.graph.triangularIfGroups.forEach(group => {
|
||||
if (group.trueVertex && group.falseVertex) {
|
||||
Object.values(group).forEach(v => v.isInTriangleGroup = true);
|
||||
constraints.push({
|
||||
type: 'alignment',
|
||||
axis: 'x',
|
||||
offsets: [
|
||||
{ node: group.ifVertex.colaIndex, offset: 0 },
|
||||
// The offsets here are oddly sensitive. If you use lower values than the width of
|
||||
// the node the layout gets all crazy and overlappy for reasons I don't understand
|
||||
{ node: group.trueVertex.colaIndex, offset: -LOGSTASH.PIPELINE_VIEWER.GRAPH.VERTICES.WIDTH_PX },
|
||||
{ node: group.falseVertex.colaIndex, offset: LOGSTASH.PIPELINE_VIEWER.GRAPH.VERTICES.WIDTH_PX }
|
||||
]
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
for (let rank = 0; rank < verticesByRank.length; rank++) {
|
||||
const vertices = verticesByRank[rank];
|
||||
|
||||
// Ensure that nodes of an equal rank are aligned on the y axis.
|
||||
constraints.push(
|
||||
{
|
||||
type: 'alignment',
|
||||
axis: 'y',
|
||||
offsets: vertices.map(v => {
|
||||
return { node: v.colaIndex, offset: 0 };
|
||||
})
|
||||
}
|
||||
);
|
||||
|
||||
if (rank > 0) {
|
||||
const previousVertices = verticesByRank[rank - 1];
|
||||
|
||||
// Prevent sibling nodes from overlapping
|
||||
vertices.forEach((vertex, index) => {
|
||||
const previousParents = previousVertices.filter(previousVertex => {
|
||||
return previousVertex.outgoingVertices.find(v => v === vertex);
|
||||
});
|
||||
|
||||
const nodeXGap = LOGSTASH.PIPELINE_VIEWER.GRAPH.VERTICES.WIDTH_PX + LOGSTASH.PIPELINE_VIEWER.GRAPH.VERTICES.MARGIN_PX;
|
||||
const rightSibling = vertices[index + 1];
|
||||
// We don't need to add constraints for nodes in triangle groups since they have
|
||||
// a constraint that keeps them separately already
|
||||
if (rightSibling && !rightSibling.isInTriangleGroup && !vertex.isInTriangleGroup) {
|
||||
constraints.push({
|
||||
axis: "x",
|
||||
right: vertex.colaIndex,
|
||||
left: rightSibling.colaIndex,
|
||||
gap: nodeXGap
|
||||
});
|
||||
}
|
||||
|
||||
// Ensure that nodes of rank N that have a single outbound connection to a node of rank N+1
|
||||
// are positioned vertically inline
|
||||
// We start by checking if the current node has exactly one parent in the previous rank
|
||||
// if it has > 1 parent then we don't really know where to put it
|
||||
if (previousParents.length === 1) {
|
||||
const previousParent = previousParents[0];
|
||||
// We further check that the connected parent isn't also connected to other nodes in this rank
|
||||
// otherwise the nodes would have to overlap if we aligned them
|
||||
if (previousParent.outgoingVertices.filter(v => v.layoutRank === rank).length === 1) {
|
||||
constraints.push({
|
||||
axis: 'x',
|
||||
left: previousParent.colaIndex,
|
||||
right: vertex.colaIndex,
|
||||
gap: 0,
|
||||
equality: true
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Ensure that all nodes of a given rank are at the same exact distance below others
|
||||
previousVertices.forEach(previousVertex => {
|
||||
constraints.push({
|
||||
axis: 'y',
|
||||
left: previousVertex.colaIndex,
|
||||
right: vertex.colaIndex,
|
||||
// Multiplying the gap by two works much better for large graphs giving more space to route edges
|
||||
gap: LOGSTASH.PIPELINE_VIEWER.GRAPH.VERTICES.HEIGHT_PX * 2,
|
||||
equality: true
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return constraints;
|
||||
}
|
||||
|
||||
render() {
|
||||
const viewBox = `0,0,${this.width},${this.height}`;
|
||||
return (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
ref={svgEl => this.renderGraph(svgEl)}
|
||||
width="100%"
|
||||
height="100%"
|
||||
preserveAspectRatio="xMinYMin meet"
|
||||
viewBox={viewBox}
|
||||
pointerEvents="all"
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.updateGraph();
|
||||
}
|
||||
|
||||
shouldComponentUpdate(nextProps, nextState) {
|
||||
// Let D3 control updates to this component's DOM.
|
||||
this.updateGraph(nextProps, nextState);
|
||||
|
||||
// Since D3 is controlling any updates to this component's DOM,
|
||||
// we don't want React to update this component's DOM.
|
||||
return false;
|
||||
}
|
||||
}
|
|
@ -210,7 +210,7 @@ function renderPluginBasicInfo(vertex) {
|
|||
}
|
||||
|
||||
function renderIfBasicInfo(vertex) {
|
||||
const ifCode = `if (${vertex.subtitle.complete}) {
|
||||
const ifCode = `if (${vertex.subtitle}) {
|
||||
...
|
||||
}`;
|
||||
|
||||
|
|
|
@ -1,203 +0,0 @@
|
|||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
import inputIcon from '@elastic/eui/src/components/icon/assets/logstash_input.svg';
|
||||
import filterIcon from '@elastic/eui/src/components/icon/assets/logstash_filter.svg';
|
||||
import outputIcon from '@elastic/eui/src/components/icon/assets/logstash_output.svg';
|
||||
import queueIcon from '@elastic/eui/src/components/icon/assets/logstash_queue.svg';
|
||||
import ifIcon from '@elastic/eui/src/components/icon/assets/logstash_if.svg';
|
||||
import { PluginVertex } from '../models/graph/plugin_vertex';
|
||||
import { IfVertex } from '../models/graph/if_vertex';
|
||||
import { LOGSTASH } from '../../../../../common/constants';
|
||||
import { formatMetric } from '../../../../lib/format_number';
|
||||
|
||||
// Each vertex consists of two lines (rows) of text
|
||||
// - The first line shows the name and ID of the vertex
|
||||
// - The second line shows stats about the vertex
|
||||
// There is also an icon denoting the type of vertex
|
||||
|
||||
const BASE_OFFSET_LEFT_PX = 7;
|
||||
const FIRST_LINE_OFFSET_TOP_PX = 18;
|
||||
const SECOND_LINE_OFFSET_TOP_PX = FIRST_LINE_OFFSET_TOP_PX + 22;
|
||||
|
||||
const PCT_EXECUTION_OFFSET_LEFT_PX = BASE_OFFSET_LEFT_PX;
|
||||
const PCT_EXECUTION_OFFSET_TOP_PX = SECOND_LINE_OFFSET_TOP_PX;
|
||||
|
||||
const PCT_EXECUTION_BG_OFFSET_LEFT_PX = PCT_EXECUTION_OFFSET_LEFT_PX - 4;
|
||||
const PCT_EXECUTION_BG_OFFSET_TOP_PX = PCT_EXECUTION_OFFSET_TOP_PX - 15;
|
||||
const PCT_EXECUTION_BG_WIDTH_PX = 43;
|
||||
const PCT_EXECUTION_BG_HEIGHT_PX = 20;
|
||||
const PCT_EXECUTION_BG_RADIUS_PX = 5;
|
||||
|
||||
const EVENT_DURATION_OFFSET_LEFT_PX = BASE_OFFSET_LEFT_PX + 50;
|
||||
const EVENT_DURATION_OFFSET_TOP_PX = SECOND_LINE_OFFSET_TOP_PX;
|
||||
|
||||
const EVENT_DURATION_BG_OFFSET_LEFT_PX = EVENT_DURATION_OFFSET_LEFT_PX - 6;
|
||||
const EVENT_DURATION_BG_OFFSET_TOP_PX = EVENT_DURATION_OFFSET_TOP_PX - 15;
|
||||
const EVENT_DURATION_BG_WIDTH_PX = 89;
|
||||
const EVENT_DURATION_BG_HEIGHT_PX = 20;
|
||||
const EVENT_DURATION_BG_RADIUS_PX = 5;
|
||||
|
||||
const EVENTS_PER_SECOND_OFFSET_LEFT_PX = BASE_OFFSET_LEFT_PX + 136;
|
||||
const EVENTS_PER_SECOND_OFFSET_TOP_PX = SECOND_LINE_OFFSET_TOP_PX;
|
||||
|
||||
const ICON_OFFSET_LEFT_PX = BASE_OFFSET_LEFT_PX + 258;
|
||||
const ICON_OFFSET_TOP_PX = FIRST_LINE_OFFSET_TOP_PX + 9;
|
||||
|
||||
function renderHeader(colaObjects, title, subtitle) {
|
||||
const pluginHeader = colaObjects
|
||||
.append('text')
|
||||
.attr('class', 'lspvHeader')
|
||||
.attr('x', BASE_OFFSET_LEFT_PX)
|
||||
.attr('y', FIRST_LINE_OFFSET_TOP_PX);
|
||||
|
||||
pluginHeader
|
||||
.append('tspan')
|
||||
.attr('class', 'lspvVertexTitle')
|
||||
.text(title);
|
||||
|
||||
// For plugin vertices, either we have an explicitly-set plugin ID or an
|
||||
// auto-generated plugin ID. For explicitly-set plugin IDs, show the ID.
|
||||
pluginHeader
|
||||
.filter(d => {
|
||||
const vertex = d.vertex;
|
||||
return (vertex instanceof PluginVertex && vertex.hasExplicitId) ||
|
||||
(vertex instanceof IfVertex);
|
||||
})
|
||||
.append('tspan')
|
||||
.attr('class', 'lspvVertexSubtitle')
|
||||
.text(d => subtitle ? ` (${subtitle(d).display})` : null)
|
||||
.append('title')
|
||||
.text(d => subtitle ? subtitle(d).complete : null);
|
||||
}
|
||||
|
||||
function renderIcon(selection, icon) {
|
||||
selection
|
||||
.append('image')
|
||||
.attr('xlink:href', icon)
|
||||
.attr('x', ICON_OFFSET_LEFT_PX)
|
||||
.attr('y', ICON_OFFSET_TOP_PX)
|
||||
.attr('height', LOGSTASH.PIPELINE_VIEWER.ICON.HEIGHT_PX)
|
||||
.attr('width', LOGSTASH.PIPELINE_VIEWER.ICON.WIDTH_PX);
|
||||
}
|
||||
|
||||
export function enterInputVertex(inputs) {
|
||||
renderHeader(
|
||||
inputs,
|
||||
(d => d.vertex.title),
|
||||
(d => d.vertex.subtitle)
|
||||
);
|
||||
|
||||
renderIcon(inputs, inputIcon);
|
||||
|
||||
inputs
|
||||
.append('text')
|
||||
.attr('class', 'lspvStat')
|
||||
.attr('data-lspv-events-per-second', '')
|
||||
.attr('x', BASE_OFFSET_LEFT_PX)
|
||||
.attr('y', SECOND_LINE_OFFSET_TOP_PX);
|
||||
}
|
||||
|
||||
export function enterProcessorVertex(processors) {
|
||||
renderHeader(
|
||||
processors,
|
||||
(d => d.vertex.title),
|
||||
(d => d.vertex.subtitle)
|
||||
);
|
||||
|
||||
processors
|
||||
.append('rect')
|
||||
.attr('data-lspv-percent-execution-bg', '')
|
||||
.attr('x', PCT_EXECUTION_BG_OFFSET_LEFT_PX)
|
||||
.attr('y', PCT_EXECUTION_BG_OFFSET_TOP_PX)
|
||||
.attr('width', PCT_EXECUTION_BG_WIDTH_PX)
|
||||
.attr('height', PCT_EXECUTION_BG_HEIGHT_PX)
|
||||
.attr('ry', PCT_EXECUTION_BG_RADIUS_PX)
|
||||
.attr('rx', PCT_EXECUTION_BG_RADIUS_PX)
|
||||
.attr('fill', 'none');
|
||||
|
||||
processors
|
||||
.append('text')
|
||||
.attr('class', 'lspvStat')
|
||||
.attr('data-lspv-percent-execution', '')
|
||||
.attr('x', PCT_EXECUTION_OFFSET_LEFT_PX)
|
||||
.attr('y', PCT_EXECUTION_OFFSET_TOP_PX);
|
||||
|
||||
processors
|
||||
.append('rect')
|
||||
.attr('data-lspv-per-event-duration-in-millis-bg', '')
|
||||
.attr('x', EVENT_DURATION_BG_OFFSET_LEFT_PX)
|
||||
.attr('y', EVENT_DURATION_BG_OFFSET_TOP_PX)
|
||||
.attr('width', EVENT_DURATION_BG_WIDTH_PX)
|
||||
.attr('height', EVENT_DURATION_BG_HEIGHT_PX)
|
||||
.attr('ry', EVENT_DURATION_BG_RADIUS_PX)
|
||||
.attr('rx', EVENT_DURATION_BG_RADIUS_PX)
|
||||
.attr('fill', 'none');
|
||||
|
||||
processors
|
||||
.append('text')
|
||||
.attr('class', 'lspvStat')
|
||||
.attr('data-lspv-per-event-duration-in-millis', '')
|
||||
.attr('x', EVENT_DURATION_OFFSET_LEFT_PX)
|
||||
.attr('y', EVENT_DURATION_OFFSET_TOP_PX);
|
||||
|
||||
processors
|
||||
.append('text')
|
||||
.attr('class', 'lspvStat')
|
||||
.attr('data-lspv-events-per-second', '')
|
||||
.attr('x', EVENTS_PER_SECOND_OFFSET_LEFT_PX)
|
||||
.attr('y', EVENTS_PER_SECOND_OFFSET_TOP_PX);
|
||||
|
||||
renderIcon(processors, d => d.vertex.pluginType === 'filter' ? filterIcon : outputIcon);
|
||||
}
|
||||
|
||||
export function enterIfVertex(ifs) {
|
||||
renderHeader(
|
||||
ifs,
|
||||
(d => d.vertex.title),
|
||||
(d => d.vertex.subtitle)
|
||||
);
|
||||
|
||||
renderIcon(ifs, ifIcon);
|
||||
}
|
||||
|
||||
export function enterQueueVertex(queueVertex) {
|
||||
renderHeader(
|
||||
queueVertex,
|
||||
(d => d.vertex.title),
|
||||
);
|
||||
|
||||
renderIcon(queueVertex, queueIcon);
|
||||
}
|
||||
|
||||
export function updateInputVertex(inputs) {
|
||||
inputs.selectAll('[data-lspv-events-per-second]')
|
||||
.text(d => formatMetric(d.vertex.latestEventsPerSecond, '0.[00]a', 'e/s emitted'));
|
||||
}
|
||||
|
||||
export function updateProcessorVertex(processors) {
|
||||
processors.selectAll('[data-lspv-percent-execution]')
|
||||
.text(d => {
|
||||
const pct = d.vertex.percentOfTotalProcessorTime || 0;
|
||||
return formatMetric(Math.round(pct), '0', '%', { prependSpace: false });
|
||||
});
|
||||
|
||||
processors.selectAll('[data-lspv-percent-execution-bg]')
|
||||
.attr('fill', d => {
|
||||
return d.vertex.isTimeConsuming() ? 'orange' : 'none';
|
||||
});
|
||||
|
||||
processors.selectAll('[data-lspv-per-event-duration-in-millis]')
|
||||
.text(d => formatMetric(d.vertex.latestMillisPerEvent, '0.[00]a', 'ms/e'));
|
||||
|
||||
processors.selectAll('[data-lspv-per-event-duration-in-millis-bg]')
|
||||
.attr('fill', d => {
|
||||
return d.vertex.isSlow() ? 'orange' : 'none';
|
||||
});
|
||||
|
||||
processors.selectAll('[data-lspv-events-per-second]')
|
||||
.text(d => formatMetric(d.vertex.latestEventsPerSecond, '0.[00]a', 'e/s received'));
|
||||
}
|
|
@ -8,7 +8,7 @@ import React from 'react';
|
|||
import { render } from 'react-dom';
|
||||
import moment from 'moment';
|
||||
import { uiModules } from 'ui/modules';
|
||||
import { ConfigViewer } from 'plugins/monitoring/components/logstash/pipeline_viewer/views/config_viewer';
|
||||
import { ConfigViewer } from 'plugins/monitoring/components/logstash/pipeline_viewer';
|
||||
import { Pipeline } from 'plugins/monitoring/components/logstash/pipeline_viewer/models/pipeline';
|
||||
import { List } from 'plugins/monitoring/components/logstash/pipeline_viewer/models/list';
|
||||
import { PipelineState } from 'plugins/monitoring/components/logstash/pipeline_viewer/models/pipeline_state';
|
||||
|
|
|
@ -2,106 +2,6 @@
|
|||
border-bottom: 1px solid #d4d4d4;
|
||||
}
|
||||
|
||||
.lspvContainer {
|
||||
// Same color as the sidebar. Needed to fill in the Y axis of the sidebar
|
||||
// TODO: Do we have a color lookup variable that should be used here?
|
||||
background-color: #F6F6F6;
|
||||
cursor: grab;
|
||||
}
|
||||
|
||||
.lspvVertexTitle {
|
||||
fill: black;
|
||||
font-family: "Open Sans", "Lato", "Helvetica Neue", Helvetica, Arial;
|
||||
font-weight: bold;
|
||||
font-size: 13px;
|
||||
stroke: none;
|
||||
}
|
||||
|
||||
.lspvVertexSubtitle {
|
||||
fill: black;
|
||||
font-family: "Open Sans", "Lato", "Helvetica Neue", Helvetica, Arial;
|
||||
font-weight: normal;
|
||||
font-size: 13px;
|
||||
stroke: none;
|
||||
}
|
||||
|
||||
.lspvHeader {
|
||||
fill: black;
|
||||
font-family: "Open Sans", "Lato", "Helvetica Neue", Helvetica, Arial;
|
||||
font-size: 13px;
|
||||
stroke: none;
|
||||
}
|
||||
|
||||
.lspvVertex {
|
||||
cursor: pointer;
|
||||
|
||||
&.lspvVertex-grayed {
|
||||
opacity: 0.3;
|
||||
}
|
||||
}
|
||||
|
||||
.lspvVertexBounding {
|
||||
fill: #fff;
|
||||
stroke: #d4d4d4;
|
||||
stroke-width: 1px;
|
||||
vector-effect: non-scaling-stroke;
|
||||
|
||||
&.lspvVertexBounding-highlighted {
|
||||
stroke: #6badbf;
|
||||
}
|
||||
}
|
||||
|
||||
.lspvStat {
|
||||
fill: black;
|
||||
font-family: "Open Sans", "Lato", "Helvetica Neue", Helvetica, Arial;
|
||||
font-weight: normal;
|
||||
font-size: 13px;
|
||||
stroke: none;
|
||||
}
|
||||
|
||||
.lspvEdge {
|
||||
stroke: #000;
|
||||
stroke-width: 2px;
|
||||
stroke-linejoin: round;
|
||||
opacity: 0.5;
|
||||
marker-end: url(#lspvPlainMarker);
|
||||
fill: none;
|
||||
|
||||
&.lspvEdge-grayed {
|
||||
opacity: 0.1;
|
||||
}
|
||||
}
|
||||
|
||||
.lspvEdgeBoolean {
|
||||
text {
|
||||
font-size: 14px;
|
||||
font-weight: bold;
|
||||
fill: #fff;
|
||||
stroke: none;
|
||||
}
|
||||
}
|
||||
|
||||
@trueBooleanFill: #1BAFD2;
|
||||
@falseBooleanFill: #EE408A;
|
||||
|
||||
.lspvEdgeBoolean--true {
|
||||
stroke: @trueBooleanFill;
|
||||
marker-end: url(#lspvTrueMarker);
|
||||
|
||||
circle {
|
||||
fill: @trueBooleanFill;
|
||||
}
|
||||
}
|
||||
|
||||
.lspvEdgeBoolean--false {
|
||||
stroke: @falseBooleanFill;
|
||||
marker-end: url(#lspvFalseMarker);
|
||||
|
||||
circle {
|
||||
fill: @falseBooleanFill;
|
||||
}
|
||||
}
|
||||
|
||||
img.lspvDetailDrawerIcon {
|
||||
display: inline;
|
||||
margin: 0 5px 0 0;
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue