[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:
Justin Kambic 2018-06-26 10:49:11 -04:00
parent d3369fe9dc
commit 15667e5473
22 changed files with 13 additions and 1725 deletions

View file

@ -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

View file

@ -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);
});
});
});

View file

@ -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';

View file

@ -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`);
});
});

View file

@ -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);
});
});

View file

@ -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', () => {

View file

@ -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', () => {

View file

@ -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`);
});
});

View file

@ -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);

View file

@ -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}`;
}
}

View file

@ -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;
}
}

View file

@ -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:

View file

@ -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() {

View file

@ -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);
}

View file

@ -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`;
}
}

View file

@ -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)}`;
}
}

View file

@ -167,9 +167,7 @@ describe('DetailDrawer component', () => {
const vertex = {
title: 'if',
typeString: 'if',
subtitle: {
complete: '[type] == "apache_log"'
}
subtitle: '[type] == "apache_log"'
};
const component = (

View file

@ -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;
}
}

View file

@ -210,7 +210,7 @@ function renderPluginBasicInfo(vertex) {
}
function renderIfBasicInfo(vertex) {
const ifCode = `if (${vertex.subtitle.complete}) {
const ifCode = `if (${vertex.subtitle}) {
...
}`;

View file

@ -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'));
}

View file

@ -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';

View file

@ -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;