[PipelineViewer] Test PipelineViewer sub-components (#20329)

* Rename config view to PipelineViewer.

* Decouple CollapsibleStatement from if/else using props.children.

* Add tests for PipelineViewer component.

* Test Metric component.

* Test CollapsibleStatement component.

* Test PluginStatement component.

* Test Queue component.

* Test StatementListHeading component.

* Test StatementSection component. Move StatementList component to dedicated file.

* Test StatementList component.

* Test Statement component.

* Run prettier on edited files.
This commit is contained in:
Justin Kambic 2018-07-09 11:51:52 -04:00
parent 3b3dc0ba02
commit da2448f4a2
26 changed files with 1681 additions and 274 deletions

View file

@ -0,0 +1,32 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`CollapsibleStatement component renders child components 1`] = `
<EuiFlexGroup
alignItems="center"
className="pipelineViewer__statement"
component="div"
direction="row"
gutterSize="none"
justifyContent="flexStart"
responsive={false}
wrap={false}
>
<EuiFlexItem
component="div"
grow={false}
key="statementId"
>
<EuiButtonIcon
aria-label={true}
color="text"
iconType="arrowDown"
onClick={[Function]}
size="s"
type="button"
/>
</EuiFlexItem>
<div>
child element
</div>
</EuiFlexGroup>
`;

View file

@ -0,0 +1,36 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Metric component does not render warning badge when no warning present 1`] = `
<EuiFlexItem
className="pipelineViewer__metricFlexItem"
component="div"
grow={false}
>
<EuiText
className="pipelineViewer__metric metricClass"
color="subdued"
grow={true}
size="s"
>
<span>
220
</span>
</EuiText>
</EuiFlexItem>
`;
exports[`Metric component renders warning badge 1`] = `
<EuiFlexItem
className="pipelineViewer__metricFlexItem"
component="div"
grow={false}
>
<EuiBadge
className="metricClass"
color="warning"
iconSide="left"
>
220
</EuiBadge>
</EuiFlexItem>
`;

View file

@ -0,0 +1,177 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`PipelineViewer component passes expected props 1`] = `
<EuiPage
restrictWidth={false}
>
<EuiPageContent
className="pipelineViewer"
horizontalPosition="center"
panelPaddingSize="l"
verticalPosition="center"
>
<StatementSection
detailVertex={null}
elements={
Array [
Object {
"depth": 0,
"id": "standardInput",
"parentId": null,
},
]
}
headingText="Inputs"
iconType="logstashInput"
onShowVertexDetails={[Function]}
/>
<EuiSpacer
size="l"
/>
<Queue
queue={
Object {
"hasExplicitId": false,
"id": "__QUEUE__",
"meta": null,
"stats": Array [],
}
}
/>
<EuiSpacer
size="l"
/>
<StatementSection
detailVertex={null}
elements={
Array [
Object {
"depth": 0,
"id": "mutate",
"parentId": null,
},
]
}
headingText="Filters"
iconType="logstashFilter"
onShowVertexDetails={[Function]}
/>
<EuiSpacer
size="l"
/>
<StatementSection
detailVertex={null}
elements={
Array [
Object {
"depth": 0,
"id": "elasticsearch",
"parentId": null,
},
]
}
headingText="Outputs"
iconType="logstashOutput"
onShowVertexDetails={[Function]}
/>
</EuiPageContent>
</EuiPage>
`;
exports[`PipelineViewer component renders DetailDrawer when selected vertex is not null 1`] = `
<EuiPage
restrictWidth={false}
>
<EuiPageContent
className="pipelineViewer"
horizontalPosition="center"
panelPaddingSize="l"
verticalPosition="center"
>
<StatementSection
detailVertex={
Object {
"id": "stdin",
}
}
elements={
Array [
Object {
"depth": 0,
"id": "standardInput",
"parentId": null,
},
]
}
headingText="Inputs"
iconType="logstashInput"
onShowVertexDetails={[Function]}
/>
<EuiSpacer
size="l"
/>
<Queue
queue={
Object {
"hasExplicitId": false,
"id": "__QUEUE__",
"meta": null,
"stats": Array [],
}
}
/>
<EuiSpacer
size="l"
/>
<StatementSection
detailVertex={
Object {
"id": "stdin",
}
}
elements={
Array [
Object {
"depth": 0,
"id": "mutate",
"parentId": null,
},
]
}
headingText="Filters"
iconType="logstashFilter"
onShowVertexDetails={[Function]}
/>
<EuiSpacer
size="l"
/>
<StatementSection
detailVertex={
Object {
"id": "stdin",
}
}
elements={
Array [
Object {
"depth": 0,
"id": "elasticsearch",
"parentId": null,
},
]
}
headingText="Outputs"
iconType="logstashOutput"
onShowVertexDetails={[Function]}
/>
<DetailDrawer
onHide={[Function]}
vertex={
Object {
"id": "stdin",
}
}
/>
</EuiPageContent>
</EuiPage>
`;

View file

@ -0,0 +1,429 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`PluginStatement component adds warning highlight for cpu time 1`] = `
<EuiFlexGroup
alignItems="center"
className="pipelineViewer__statement"
component="div"
direction="row"
gutterSize="none"
justifyContent="spaceBetween"
responsive={true}
wrap={false}
>
<EuiFlexItem
component="div"
grow={false}
>
<EuiFlexGroup
alignItems="center"
component="div"
direction="row"
gutterSize="xs"
justifyContent="flexStart"
responsive={false}
wrap={false}
>
<EuiFlexItem
component="div"
grow={false}
>
<EuiButtonEmpty
className="pipelineViewer__plugin"
color="primary"
flush="left"
iconSide="left"
iconType="dot"
onClick={[Function]}
size="xs"
type="button"
>
<span>
mutate
</span>
</EuiButtonEmpty>
</EuiFlexItem>
<EuiFlexItem
component="div"
grow={false}
>
<EuiBadge
color="default"
iconSide="left"
onClick={[Function]}
onClickAriaLabel="View details"
>
mutatePlugin
</EuiBadge>
</EuiFlexItem>
</EuiFlexGroup>
</EuiFlexItem>
<EuiFlexItem
component="div"
grow={false}
>
<EuiFlexGroup
alignItems="stretch"
component="div"
direction="row"
gutterSize="s"
justifyContent="flexStart"
responsive={true}
wrap={false}
>
<Metric
className="pipelineViewer__metric--cpuTime"
key="cpuMetric"
value="25%"
warning={true}
/>
<Metric
className="pipelineViewer__metric--eventMillis"
key="eventMillis"
value="100 ms/e"
warning={false}
/>
<Metric
className="pipelineViewer__metric--events"
key="eventsReceived"
value="120 e/s received"
/>
</EuiFlexGroup>
</EuiFlexItem>
</EuiFlexGroup>
`;
exports[`PluginStatement component adds warning highlight for event millis 1`] = `
<EuiFlexGroup
alignItems="center"
className="pipelineViewer__statement"
component="div"
direction="row"
gutterSize="none"
justifyContent="spaceBetween"
responsive={true}
wrap={false}
>
<EuiFlexItem
component="div"
grow={false}
>
<EuiFlexGroup
alignItems="center"
component="div"
direction="row"
gutterSize="xs"
justifyContent="flexStart"
responsive={false}
wrap={false}
>
<EuiFlexItem
component="div"
grow={false}
>
<EuiButtonEmpty
className="pipelineViewer__plugin"
color="primary"
flush="left"
iconSide="left"
iconType="dot"
onClick={[Function]}
size="xs"
type="button"
>
<span>
mutate
</span>
</EuiButtonEmpty>
</EuiFlexItem>
<EuiFlexItem
component="div"
grow={false}
>
<EuiBadge
color="default"
iconSide="left"
onClick={[Function]}
onClickAriaLabel="View details"
>
mutatePlugin
</EuiBadge>
</EuiFlexItem>
</EuiFlexGroup>
</EuiFlexItem>
<EuiFlexItem
component="div"
grow={false}
>
<EuiFlexGroup
alignItems="stretch"
component="div"
direction="row"
gutterSize="s"
justifyContent="flexStart"
responsive={true}
wrap={false}
>
<Metric
className="pipelineViewer__metric--cpuTime"
key="cpuMetric"
value="25%"
warning={false}
/>
<Metric
className="pipelineViewer__metric--eventMillis"
key="eventMillis"
value="100 ms/e"
warning={true}
/>
<Metric
className="pipelineViewer__metric--events"
key="eventsReceived"
value="120 e/s received"
/>
</EuiFlexGroup>
</EuiFlexItem>
</EuiFlexGroup>
`;
exports[`PluginStatement component does not render explicit id field if no id is specified 1`] = `
<EuiFlexGroup
alignItems="center"
className="pipelineViewer__statement"
component="div"
direction="row"
gutterSize="none"
justifyContent="spaceBetween"
responsive={true}
wrap={false}
>
<EuiFlexItem
component="div"
grow={false}
>
<EuiFlexGroup
alignItems="center"
component="div"
direction="row"
gutterSize="xs"
justifyContent="flexStart"
responsive={false}
wrap={false}
>
<EuiFlexItem
component="div"
grow={false}
>
<EuiButtonEmpty
className="pipelineViewer__plugin"
color="primary"
flush="left"
iconSide="left"
iconType="dot"
onClick={[Function]}
size="xs"
type="button"
>
<span>
stdin
</span>
</EuiButtonEmpty>
</EuiFlexItem>
</EuiFlexGroup>
</EuiFlexItem>
<EuiFlexItem
component="div"
grow={false}
>
<EuiFlexGroup
alignItems="stretch"
component="div"
direction="row"
gutterSize="s"
justifyContent="flexStart"
responsive={true}
wrap={false}
>
<Metric
className="pipelineViewer__metric--eventsEmitted"
key="eventsEmitted"
value="125 e/s emitted"
/>
</EuiFlexGroup>
</EuiFlexItem>
</EuiFlexGroup>
`;
exports[`PluginStatement component renders input metrics and explicit id fields 1`] = `
<EuiFlexGroup
alignItems="center"
className="pipelineViewer__statement"
component="div"
direction="row"
gutterSize="none"
justifyContent="spaceBetween"
responsive={true}
wrap={false}
>
<EuiFlexItem
component="div"
grow={false}
>
<EuiFlexGroup
alignItems="center"
component="div"
direction="row"
gutterSize="xs"
justifyContent="flexStart"
responsive={false}
wrap={false}
>
<EuiFlexItem
component="div"
grow={false}
>
<EuiButtonEmpty
className="pipelineViewer__plugin"
color="primary"
flush="left"
iconSide="left"
iconType="dot"
onClick={[Function]}
size="xs"
type="button"
>
<span>
stdin
</span>
</EuiButtonEmpty>
</EuiFlexItem>
<EuiFlexItem
component="div"
grow={false}
>
<EuiBadge
color="default"
iconSide="left"
onClick={[Function]}
onClickAriaLabel="View details"
>
standardInput
</EuiBadge>
</EuiFlexItem>
</EuiFlexGroup>
</EuiFlexItem>
<EuiFlexItem
component="div"
grow={false}
>
<EuiFlexGroup
alignItems="stretch"
component="div"
direction="row"
gutterSize="s"
justifyContent="flexStart"
responsive={true}
wrap={false}
>
<Metric
className="pipelineViewer__metric--eventsEmitted"
key="eventsEmitted"
value="125 e/s emitted"
/>
</EuiFlexGroup>
</EuiFlexItem>
</EuiFlexGroup>
`;
exports[`PluginStatement component renders processor statement metrics 1`] = `
<EuiFlexGroup
alignItems="center"
className="pipelineViewer__statement"
component="div"
direction="row"
gutterSize="none"
justifyContent="spaceBetween"
responsive={true}
wrap={false}
>
<EuiFlexItem
component="div"
grow={false}
>
<EuiFlexGroup
alignItems="center"
component="div"
direction="row"
gutterSize="xs"
justifyContent="flexStart"
responsive={false}
wrap={false}
>
<EuiFlexItem
component="div"
grow={false}
>
<EuiButtonEmpty
className="pipelineViewer__plugin"
color="primary"
flush="left"
iconSide="left"
iconType="dot"
onClick={[Function]}
size="xs"
type="button"
>
<span>
mutate
</span>
</EuiButtonEmpty>
</EuiFlexItem>
<EuiFlexItem
component="div"
grow={false}
>
<EuiBadge
color="default"
iconSide="left"
onClick={[Function]}
onClickAriaLabel="View details"
>
mutatePlugin
</EuiBadge>
</EuiFlexItem>
</EuiFlexGroup>
</EuiFlexItem>
<EuiFlexItem
component="div"
grow={false}
>
<EuiFlexGroup
alignItems="stretch"
component="div"
direction="row"
gutterSize="s"
justifyContent="flexStart"
responsive={true}
wrap={false}
>
<Metric
className="pipelineViewer__metric--cpuTime"
key="cpuMetric"
value="25%"
warning={false}
/>
<Metric
className="pipelineViewer__metric--eventMillis"
key="eventMillis"
value="100 ms/e"
warning={false}
/>
<Metric
className="pipelineViewer__metric--events"
key="eventsReceived"
value="120 e/s received"
/>
</EuiFlexGroup>
</EuiFlexItem>
</EuiFlexGroup>
`;

View file

@ -0,0 +1,19 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Queue component renders default elements 1`] = `
<div>
<StatementListHeading
iconType="logstashQueue"
title="Queue"
/>
<EuiSpacer
size="s"
/>
<EuiText
className="pipelineViewer__queueMessage"
grow={true}
>
Queue metrics not available
</EuiText>
</div>
`;

View file

@ -0,0 +1,164 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Statement component renders a CollapsibleStatement with else body for non-IfElement 1`] = `
<li
className="pipelineViewer__listItem"
>
<div
className="pipelineViewer__spaceContainer"
/>
<CollapsibleStatement
collapse={[MockFunction]}
expand={[MockFunction]}
id="mutate2"
isCollapsed={false}
>
<EuiFlexItem
component="div"
grow={false}
key="statementName"
>
<EuiButtonEmpty
color="text"
flush="left"
iconSide="left"
onClick={[Function]}
size="xs"
type="button"
>
<span
className="pipelineViewer__conditional"
>
else
</span>
</EuiButtonEmpty>
</EuiFlexItem>
</CollapsibleStatement>
</li>
`;
exports[`Statement component renders a CollapsibleStatement with if body for branch model 1`] = `
<li
className="pipelineViewer__listItem"
>
<div
className="pipelineViewer__spaceContainer"
/>
<CollapsibleStatement
collapse={[MockFunction]}
expand={[MockFunction]}
id="ifStatement"
isCollapsed={false}
>
<EuiFlexItem
component="div"
grow={false}
key="statementName"
>
<EuiButtonEmpty
color="text"
flush="left"
iconSide="left"
onClick={[Function]}
size="xs"
type="button"
>
<span
className="pipelineViewer__conditional"
>
if
</span>
</EuiButtonEmpty>
</EuiFlexItem>
<EuiFlexItem
component="div"
grow={false}
key="ifContent"
>
<EuiCodeBlock
fontSize="s"
paddingSize="none"
transparentBackground={true}
/>
</EuiFlexItem>
</CollapsibleStatement>
</li>
`;
exports[`Statement component renders a PluginStatement component for plugin model 1`] = `
<li
className="pipelineViewer__listItem"
>
<div
className="pipelineViewer__spaceContainer"
/>
<PluginStatement
onShowVertexDetails={[MockFunction]}
statement={
PluginStatement {
"hasExplicitId": true,
"id": "mutate2",
"meta": null,
"name": "mutate",
"pluginType": "filter",
"stats": Array [],
"vertex": Object {
"hasExplicitId": true,
"id": "mutate2",
"latestEventsPerSecond": 23,
"meta": null,
"name": "mutate",
"pluginType": "filter",
"stats": Array [],
},
}
}
/>
</li>
`;
exports[`Statement component renders spacers for element with depth > 0 1`] = `
<li
className="pipelineViewer__listItem"
>
<div
className="pipelineViewer__spaceContainer"
>
<div
className="pipelineViewer__spacer"
key="spacer_0"
/>
<div
className="pipelineViewer__spacer"
key="spacer_1"
/>
</div>
<CollapsibleStatement
collapse={[MockFunction]}
expand={[MockFunction]}
id="mutate2"
isCollapsed={false}
>
<EuiFlexItem
component="div"
grow={false}
key="statementName"
>
<EuiButtonEmpty
color="text"
flush="left"
iconSide="left"
onClick={[Function]}
size="xs"
type="button"
>
<span
className="pipelineViewer__conditional"
>
else
</span>
</EuiButtonEmpty>
</EuiFlexItem>
</CollapsibleStatement>
</li>
`;

View file

@ -0,0 +1,36 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`StatementList renders nested elements as expected 1`] = `
<ul
className="pipelineViewer__list"
>
<Statement
collapse={[Function]}
element={
Object {
"depth": 0,
"id": "mutateIf",
"parentId": null,
}
}
expand={[Function]}
isCollapsed={false}
key="mutateIf"
onShowVertexDetails={[MockFunction]}
/>
<Statement
collapse={[Function]}
element={
Object {
"depth": 1,
"id": "mutate",
"parentId": "mutateIf",
}
}
expand={[Function]}
isCollapsed={false}
key="mutate"
onShowVertexDetails={[MockFunction]}
/>
</ul>
`;

View file

@ -0,0 +1,35 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`StatementListHeading component renders title and icon type 1`] = `
<EuiFlexGroup
alignItems="baseline"
component="div"
direction="row"
gutterSize="s"
justifyContent="flexStart"
responsive={false}
wrap={false}
>
<EuiFlexItem
component="div"
grow={false}
>
<EuiIcon
size="m"
type="logstashInput"
/>
</EuiFlexItem>
<EuiFlexItem
component="div"
grow={false}
>
<EuiTitle
size="s"
>
<h4>
Filters
</h4>
</EuiTitle>
</EuiFlexItem>
</EuiFlexGroup>
`;

View file

@ -0,0 +1,28 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`StatementSection component renders heading text, correct icon type, and elements for StatementSection 1`] = `
<div>
<StatementListHeading
iconType="logstashInput"
title="Inputs"
/>
<EuiSpacer
size="s"
/>
<StatementList
elements={
Array [
Object {
"id": "standardInput",
"parentId": null,
},
Object {
"id": "fileInput",
"parentId": null,
},
]
}
onShowVertexDetails={[MockFunction]}
/>
</div>
`;

View file

@ -0,0 +1,54 @@
/*
* 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 { CollapsibleStatement } from '../collapsible_statement';
import { shallow } from 'enzyme';
import { EuiButtonIcon } from '@elastic/eui';
describe('CollapsibleStatement component', () => {
let props;
let collapse;
let expand;
beforeEach(() => {
collapse = jest.fn();
expand = jest.fn();
props = {
collapse,
expand,
id: 'statementId',
isCollapsed: false,
};
});
it('renders child components', () => {
const child = <div>child element</div>;
const wrapper = shallow(
<CollapsibleStatement {...props}>{child}</CollapsibleStatement>
);
expect(wrapper).toMatchSnapshot();
});
it('calls collapse if component is expanded', () => {
const wrapper = shallow(<CollapsibleStatement {...props} />);
wrapper.find(EuiButtonIcon).simulate('click');
expect(collapse).toHaveBeenCalledTimes(1);
expect(collapse).toHaveBeenCalledWith('statementId');
});
it('calls expand if component is collapsed', () => {
props.isCollapsed = true;
const wrapper = shallow(<CollapsibleStatement {...props} />);
wrapper.find(EuiButtonIcon).simulate('click');
expect(expand).toHaveBeenCalledTimes(1);
expect(expand).toHaveBeenCalledWith('statementId');
});
});

View file

@ -19,15 +19,10 @@ describe('DetailDrawer component', () => {
test('shows vertex title', () => {
const vertex = {
title: 'grok'
title: 'grok',
};
const component = (
<DetailDrawer
vertex={vertex}
onHide={onHide}
/>
);
const component = <DetailDrawer vertex={vertex} onHide={onHide} />;
const renderedComponent = shallow(component);
expect(renderedComponent).toMatchSnapshot();
});
@ -43,46 +38,34 @@ describe('DetailDrawer component', () => {
id: 'parse_apache_logline',
stats: {
events_in: {
data: [
[ 1516131120000, 200 ],
[ 1516131180000, 203 ]
],
data: [[1516131120000, 200], [1516131180000, 203]],
timeRange: {
min: 1516131138639,
max: 1516135440463
}
max: 1516135440463,
},
},
events_out: {
data: [
[ 1516131120000, 199 ],
[ 1516131180000, 200 ]
],
data: [[1516131120000, 199], [1516131180000, 200]],
timeRange: {
min: 1516131138639,
max: 1516135440463
}
max: 1516135440463,
},
},
millis_per_event: {
data: [
[ 1516131120000, 0.21 ],
[ 1516131180000, 0.23 ]
],
data: [[1516131120000, 0.21], [1516131180000, 0.23]],
timeRange: {
min: 1516131138639,
max: 1516135440463
}
}
max: 1516135440463,
},
},
},
eventsPerSecond: {
data: [
[ 1516131120000, 32 ],
[ 1516131180000, 36 ]
],
data: [[1516131120000, 32], [1516131180000, 36]],
timeRange: {
min: 1516131138639,
max: 1516135440463
}
}
max: 1516135440463,
},
},
};
const component = (
@ -107,46 +90,34 @@ describe('DetailDrawer component', () => {
id: 'foobarbazqux',
stats: {
events_in: {
data: [
[ 1516131120000, 200 ],
[ 1516131180000, 203 ]
],
data: [[1516131120000, 200], [1516131180000, 203]],
timeRange: {
min: 1516131138639,
max: 1516135440463
}
max: 1516135440463,
},
},
events_out: {
data: [
[ 1516131120000, 199 ],
[ 1516131180000, 200 ]
],
data: [[1516131120000, 199], [1516131180000, 200]],
timeRange: {
min: 1516131138639,
max: 1516135440463
}
max: 1516135440463,
},
},
millis_per_event: {
data: [
[ 1516131120000, 0.21 ],
[ 1516131180000, 0.23 ]
],
data: [[1516131120000, 0.21], [1516131180000, 0.23]],
timeRange: {
min: 1516131138639,
max: 1516135440463
}
}
max: 1516135440463,
},
},
},
eventsPerSecond: {
data: [
[ 1516131120000, 32 ],
[ 1516131180000, 36 ]
],
data: [[1516131120000, 32], [1516131180000, 36]],
timeRange: {
min: 1516131138639,
max: 1516135440463
}
}
max: 1516135440463,
},
},
};
const component = (
@ -167,7 +138,7 @@ describe('DetailDrawer component', () => {
const vertex = {
title: 'if',
typeString: 'if',
subtitle: '[type] == "apache_log"'
subtitle: '[type] == "apache_log"',
};
const component = (
@ -186,7 +157,7 @@ describe('DetailDrawer component', () => {
test('shows basic info and no stats for queue', () => {
const vertex = {
title: 'queue',
typeString: 'queue'
typeString: 'queue',
};
const component = (

View file

@ -0,0 +1,34 @@
/*
* 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 { Metric } from '../metric';
import { shallow } from 'enzyme';
describe('Metric component', () => {
let metric;
beforeEach(() => {
metric = {
className: 'metricClass',
warning: true,
value: '220',
};
});
it('renders warning badge', () => {
const wrapper = shallow(<Metric {...metric} />);
expect(wrapper).toMatchSnapshot();
});
it('does not render warning badge when no warning present', () => {
metric.warning = false;
const wrapper = shallow(<Metric {...metric} />);
expect(wrapper).toMatchSnapshot();
});
});

View file

@ -0,0 +1,85 @@
/*
* 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 { PipelineViewer } from '../pipeline_viewer';
import { shallow } from 'enzyme';
import { get } from 'lodash';
describe('PipelineViewer component', () => {
let pipeline;
let component;
beforeEach(() => {
pipeline = {
inputs: [
{
depth: 0,
id: 'standardInput',
parentId: null,
},
],
filters: [
{
depth: 0,
id: 'mutate',
parentId: null,
},
],
outputs: [
{
depth: 0,
id: 'elasticsearch',
parentId: null,
},
],
queue: {
id: '__QUEUE__',
hasExplicitId: false,
stats: [],
meta: null,
},
};
component = <PipelineViewer pipeline={pipeline} />;
});
it('passes expected props', () => {
const renderedComponent = shallow(component);
expect(renderedComponent).toMatchSnapshot();
});
it('changes selected vertex', () => {
const vertex = { id: 'stdin' };
const instance = shallow(component).instance();
instance.onShowVertexDetails(vertex);
expect(get(instance, 'state.detailDrawer.vertex')).toBe(vertex);
});
it('toggles selected vertex on second pass', () => {
const vertex = { id: 'stdin' };
const instance = shallow(component).instance();
instance.onShowVertexDetails(vertex);
instance.onShowVertexDetails(vertex);
expect(get(instance, 'state.detailDrawer.vertex')).toBeNull();
});
it('renders DetailDrawer when selected vertex is not null', () => {
const vertex = { id: 'stdin' };
const wrapper = shallow(component);
const instance = wrapper.instance();
instance.onShowVertexDetails(vertex);
wrapper.update();
expect(wrapper).toMatchSnapshot();
});
});

View file

@ -0,0 +1,102 @@
/*
* 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 { PluginStatement } from '../plugin_statement';
import { shallow } from 'enzyme';
import { EuiButtonEmpty, EuiBadge } from '@elastic/eui';
describe('PluginStatement component', () => {
let props;
let onShowVertexDetails;
let isSlow;
let isTimeConsuming;
let processorStatement;
beforeEach(() => {
onShowVertexDetails = jest.fn();
props = {
statement: {
hasExplicitId: true,
id: 'standardInput',
name: 'stdin',
pluginType: 'input',
vertex: {
latestEventsPerSecond: 125,
},
},
onShowVertexDetails,
};
isSlow = jest.fn().mockImplementation(() => false);
isTimeConsuming = jest.fn().mockImplementation(() => false);
processorStatement = {
hasExplicitId: true,
id: 'mutatePlugin',
name: 'mutate',
pluginType: 'filter',
vertex: {
latestMillisPerEvent: 100,
latestEventsPerSecond: 120,
percentOfTotalProcessorTime: 25,
isSlow,
isTimeConsuming,
},
};
});
const render = props => shallow(<PluginStatement {...props} />);
it('renders input metrics and explicit id fields', () => {
expect(render(props)).toMatchSnapshot();
});
it('does not render explicit id field if no id is specified', () => {
props.statement.id = 'dcbb2c37b4fedd3d7b852b5052f03dw3fbe1545a';
props.statement.hasExplicitId = false;
expect(render(props)).toMatchSnapshot();
});
it('renders processor statement metrics', () => {
props.statement = processorStatement;
expect(render(props)).toMatchSnapshot();
expect(isSlow).toHaveBeenCalledTimes(1);
expect(isTimeConsuming).toHaveBeenCalledTimes(1);
});
it('adds warning highlight for cpu time', () => {
props.statement = processorStatement;
props.statement.vertex.isTimeConsuming = jest
.fn()
.mockImplementation(() => true);
expect(render(props)).toMatchSnapshot();
});
it('adds warning highlight for event millis', () => {
props.statement = processorStatement;
props.statement.vertex.isSlow = jest.fn().mockImplementation(() => true);
expect(render(props)).toMatchSnapshot();
});
it('handles name button click', () => {
const { vertex } = props.statement;
const wrapper = render(props);
wrapper.find(EuiButtonEmpty).simulate('click');
expect(onShowVertexDetails).toHaveBeenCalledTimes(1);
expect(onShowVertexDetails).toHaveBeenCalledWith(vertex);
});
it('handles id badge click', () => {
const { vertex } = props.statement;
const wrapper = render(props);
wrapper.find(EuiBadge).simulate('click');
expect(onShowVertexDetails).toHaveBeenCalledTimes(1);
expect(onShowVertexDetails).toHaveBeenCalledWith(vertex);
});
});

View file

@ -0,0 +1,15 @@
/*
* 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 { Queue } from '../queue';
import { shallow } from 'enzyme';
describe('Queue component', () => {
it('renders default elements', () => {
expect(shallow(<Queue />)).toMatchSnapshot();
});
});

View file

@ -0,0 +1,90 @@
/*
* 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 { Statement } from '../statement';
import { PluginStatement } from '../../models/pipeline/plugin_statement';
import { PluginStatement as PluginStatementComponent } from '../plugin_statement';
import { IfElement } from '../../models/list/if_element';
import { CollapsibleStatement } from '../collapsible_statement';
import { shallow } from 'enzyme';
import { EuiButtonEmpty } from '@elastic/eui';
describe('Statement component', () => {
let props;
let pluginStatement;
let branchElement;
let collapse;
let expand;
let onShowVertexDetails;
beforeEach(() => {
collapse = jest.fn();
expand = jest.fn();
onShowVertexDetails = jest.fn();
props = {
collapse,
element: {
depth: 0,
id: 'mutate2',
statement: {},
},
expand,
isCollapsed: false,
onShowVertexDetails,
};
pluginStatement = new PluginStatement({
hasExplicitId: true,
id: 'mutate2',
latestEventsPerSecond: 23,
meta: null,
name: 'mutate',
pluginType: 'filter',
stats: [],
});
branchElement = new IfElement(
{
id: 'ifStatement',
name: 'ifStatement',
},
0,
null
);
});
it('renders a PluginStatement component for plugin model', () => {
props.element.statement = pluginStatement;
const wrapper = shallow(<Statement {...props} />);
expect(wrapper).toMatchSnapshot();
expect(wrapper.find(PluginStatementComponent)).toHaveLength(1);
});
it('renders spacers for element with depth > 0', () => {
props.element.depth = 2;
expect(shallow(<Statement {...props} />)).toMatchSnapshot();
});
it('renders a CollapsibleStatement with if body for branch model', () => {
props.element = branchElement;
const wrapper = shallow(<Statement {...props} />);
expect(wrapper).toMatchSnapshot();
expect(wrapper.find(CollapsibleStatement)).toHaveLength(1);
});
it('renders a CollapsibleStatement with else body for non-IfElement', () => {
expect(shallow(<Statement {...props} />)).toMatchSnapshot();
});
it(`selects the element's vertex when the name is clicked`, () => {
props.element = branchElement;
const wrapper = shallow(<Statement {...props} />);
wrapper.find(EuiButtonEmpty).simulate('click');
expect(onShowVertexDetails).toHaveBeenCalledTimes(1);
});
});

View file

@ -0,0 +1,64 @@
/*
* 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 { StatementList } from '../statement_list';
import { Statement } from '../statement';
import { shallow } from 'enzyme';
describe('StatementList', () => {
let props;
let onShowVertexDetails;
beforeEach(() => {
onShowVertexDetails = jest.fn();
props = {
elements: [
{
id: 'mutateIf',
parentId: null,
depth: 0,
},
{
id: 'mutate',
parentId: 'mutateIf',
depth: 1,
},
],
onShowVertexDetails,
};
});
const render = props => shallow(<StatementList {...props} />);
it('renders nested elements as expected', () => {
const wrapper = render(props);
expect(wrapper).toMatchSnapshot();
expect(wrapper.find(Statement).length).toBe(2);
});
it('renders children elements when parent is collapsed', () => {
const wrapper = render(props);
const instance = wrapper.instance();
instance.collapse('mutateIf');
wrapper.update();
expect(wrapper.find(Statement).length).toBe(1);
});
it('renders children after expanding collapsed elements', () => {
const wrapper = render(props);
const instance = wrapper.instance();
instance.collapse('mutateIf');
wrapper.update();
expect(wrapper.find(Statement).length).toBe(1);
instance.expand('mutateIf');
wrapper.update();
expect(wrapper.find(Statement).length).toBe(2);
});
});

View file

@ -0,0 +1,24 @@
/*
* 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 { StatementListHeading } from '../statement_list_heading';
import { shallow } from 'enzyme';
describe('StatementListHeading component', () => {
let props;
beforeEach(() => {
props = {
iconType: 'logstashInput',
title: 'Filters',
};
});
it('renders title and icon type', () => {
expect(shallow(<StatementListHeading {...props} />)).toMatchSnapshot();
});
});

View file

@ -0,0 +1,43 @@
/*
* 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 { StatementSection } from '../statement_section';
import { shallow } from 'enzyme';
describe('StatementSection component', () => {
let props;
let onShowVertexDetails;
beforeEach(() => {
onShowVertexDetails = jest.fn();
props = {
elements: [
{
id: 'standardInput',
parentId: null,
},
{
id: 'fileInput',
parentId: null,
},
],
headingText: 'Inputs',
iconType: 'logstashInput',
onShowVertexDetails,
};
});
it('renders heading text, correct icon type, and elements for StatementSection', () => {
expect(shallow(<StatementSection {...props} />)).toMatchSnapshot();
});
it('renders nothing if elements array is empty', () => {
props.elements = [];
const wrapper = shallow(<StatementSection {...props} />);
expect(wrapper.instance()).toBe(null);
});
});

View file

@ -7,23 +7,14 @@
import React from 'react';
import PropTypes from 'prop-types';
import {
EuiButtonIcon,
EuiFlexGroup,
EuiFlexItem
} from '@elastic/eui';
import { EuiButtonIcon, EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
function getToggleIconType(isCollapsed) {
return isCollapsed ? 'arrowRight' : 'arrowDown';
}
export function CollapsibleStatement(props) {
const {
collapse,
expand,
id,
isCollapsed
} = props;
const { collapse, expand, id, isCollapsed } = props;
const toggleClicked = () => {
if (isCollapsed) {
@ -40,10 +31,7 @@ export function CollapsibleStatement(props) {
alignItems="center"
className="pipelineViewer__statement"
>
<EuiFlexItem
key={id}
grow={false}
>
<EuiFlexItem key={id} grow={false}>
<EuiButtonIcon
aria-label
color="text"

View file

@ -6,19 +6,11 @@
import React from 'react';
import PropTypes from 'prop-types';
import {
EuiFlexItem,
EuiBadge,
EuiText,
} from '@elastic/eui';
import { EuiFlexItem, EuiBadge, EuiText } from '@elastic/eui';
import classNames from 'classnames';
export function Metric({ className, value, warning }) {
const classes = classNames(
'pipelineViewer__metric',
className,
);
export function Metric({ className, warning, value }) {
const classes = classNames('pipelineViewer__metric', className);
let stylizedValue;
if (warning) {
@ -30,17 +22,12 @@ export function Metric({ className, value, warning }) {
} else {
stylizedValue = (
<EuiText size="s" color="subdued" className={classes}>
<span>
{value}
</span>
<span>{value}</span>
</EuiText>
);
}
return (
<EuiFlexItem
className="pipelineViewer__metricFlexItem"
grow={false}
>
<EuiFlexItem className="pipelineViewer__metricFlexItem" grow={false}>
{stylizedValue}
</EuiFlexItem>
);
@ -49,4 +36,5 @@ export function Metric({ className, value, warning }) {
Metric.propTypes = {
className: PropTypes.string.isRequired,
value: PropTypes.string.isRequired,
warning: PropTypes.bool,
};

View file

@ -16,13 +16,13 @@ import { formatMetric } from '../../../../lib/format_number';
import { Metric } from './metric';
function getInputStatementMetrics({ latestEventsPerSecond }) {
return [(
return [
<Metric
key="eventsEmitted"
className="pipelineViewer__metric--eventsEmitted"
value={formatMetric(latestEventsPerSecond, '0.[00]a', 'e/s emitted')}
/>
)];
/>,
];
}
function getProcessorStatementMetrics(processorVertex) {
@ -33,29 +33,28 @@ function getProcessorStatementMetrics(processorVertex) {
} = processorVertex;
return [
(
<Metric
key="cpuMetric"
className="pipelineViewer__metric--cpuTime"
warning={processorVertex.isTimeConsuming()}
value={formatMetric(Math.round(percentOfTotalProcessorTime || 0), '0', '%', { prependSpace: false })}
/>
),
(
<Metric
key="eventMillis"
className="pipelineViewer__metric--eventMillis"
warning={processorVertex.isSlow()}
value={formatMetric(latestMillisPerEvent, '0.[00]a', 'ms/e')}
/>
),
(
<Metric
key="eventsReceived"
className="pipelineViewer__metric--events"
value={formatMetric(latestEventsPerSecond, '0.[00]a', 'e/s received')}
/>
)
<Metric
key="cpuMetric"
className="pipelineViewer__metric--cpuTime"
warning={processorVertex.isTimeConsuming()}
value={formatMetric(
Math.round(percentOfTotalProcessorTime || 0),
'0',
'%',
{ prependSpace: false }
)}
/>,
<Metric
key="eventMillis"
className="pipelineViewer__metric--eventMillis"
warning={processorVertex.isSlow()}
value={formatMetric(latestMillisPerEvent, '0.[00]a', 'ms/e')}
/>,
<Metric
key="eventsReceived"
className="pipelineViewer__metric--events"
value={formatMetric(latestEventsPerSecond, '0.[00]a', 'e/s received')}
/>,
];
}
@ -66,45 +65,36 @@ function renderPluginStatementMetrics(pluginType, vertex) {
}
export function PluginStatement({
statement: {
hasExplicitId,
id,
name,
pluginType,
vertex
},
onShowVertexDetails
statement: { hasExplicitId, id, name, pluginType, vertex },
onShowVertexDetails,
}) {
const statementMetrics = renderPluginStatementMetrics(pluginType, vertex);
const onNameButtonClick = () => { onShowVertexDetails(vertex); };
const onNameButtonClick = () => {
onShowVertexDetails(vertex);
};
return (
<EuiFlexGroup
gutterSize="none"
justifyContent="spaceBetween"
alignItems="center"
className="pipelineViewer__statement"
gutterSize="none"
justifyContent="spaceBetween"
>
<EuiFlexItem grow={false}>
<EuiFlexGroup
gutterSize="xs"
responsive={false}
alignItems="center"
>
<EuiFlexGroup alignItems="center" gutterSize="xs" responsive={false}>
<EuiFlexItem grow={false}>
<EuiButtonEmpty
size="xs"
color="primary"
iconType="dot"
flush="left"
className="pipelineViewer__plugin"
color="primary"
flush="left"
iconType="dot"
onClick={onNameButtonClick}
size="xs"
>
<span>{name}</span>
</EuiButtonEmpty>
</EuiFlexItem>
{
hasExplicitId &&
{hasExplicitId && (
<EuiFlexItem grow={false}>
<EuiBadge
onClick={onNameButtonClick}
@ -113,30 +103,29 @@ export function PluginStatement({
{id}
</EuiBadge>
</EuiFlexItem>
}
)}
</EuiFlexGroup>
</EuiFlexItem>
{
statementMetrics &&
{statementMetrics && (
<EuiFlexItem grow={false}>
<EuiFlexGroup
gutterSize="s"
>
{statementMetrics}
</EuiFlexGroup>
<EuiFlexGroup gutterSize="s">{statementMetrics}</EuiFlexGroup>
</EuiFlexItem>
}
)}
</EuiFlexGroup>
);
}
PluginStatement.propTypes = {
onShowVertexDetails: PropTypes.func.isRequired,
statement: PropTypes.shape({
hasExplicitId: PropTypes.bool.isRequired,
id: PropTypes.string.isRequired,
name: PropTypes.string.isRequired,
pluginType: PropTypes.string.isRequired,
vertex: PropTypes.object.isRequired,
vertex: PropTypes.shape({
latestEventsPerSecond: PropTypes.number.isRequired,
latestMillisPerEvent: PropTypes.number,
percentOfTotalProcessorTime: PropTypes.number,
}).isRequired,
}).isRequired,
onShowVertexDetails: PropTypes.func.isRequired,
};

View file

@ -11,10 +11,7 @@ import { EuiSpacer, EuiText } from '@elastic/eui';
export function Queue() {
return (
<div>
<StatementListHeading
iconType="logstashQueue"
title="Queue"
/>
<StatementListHeading iconType="logstashQueue" title="Queue" />
<EuiSpacer size="s" />
<EuiText className="pipelineViewer__queueMessage">
Queue metrics not available

View file

@ -6,10 +6,7 @@
import React from 'react';
import PropTypes from 'prop-types';
import {
EuiButtonEmpty,
EuiCodeBlock,
EuiFlexItem } from '@elastic/eui';
import { EuiButtonEmpty, EuiCodeBlock, EuiFlexItem } from '@elastic/eui';
import { PluginStatement as PluginStatementModel } from '../models/pipeline/plugin_statement';
import { CollapsibleStatement } from './collapsible_statement';
import { IfElement } from '../models/list/if_element';
@ -17,10 +14,7 @@ import { PluginStatement } from './plugin_statement';
function renderStatementName(name, onVertexSelected) {
return (
<EuiFlexItem
grow={false}
key="statementName"
>
<EuiFlexItem grow={false} key="statementName">
<EuiButtonEmpty
color="text"
size="xs"
@ -36,30 +30,22 @@ function renderStatementName(name, onVertexSelected) {
function renderIfStatement({ condition }, onVertexSelected) {
return [
renderStatementName('if', onVertexSelected),
(
<EuiFlexItem
key="ifContent"
grow={false}
<EuiFlexItem key="ifContent" grow={false}>
<EuiCodeBlock
fontSize="s"
paddingSize="none"
transparentBackground={true}
>
<EuiCodeBlock
fontSize="s"
paddingSize="none"
transparentBackground={true}
>
{condition}
</EuiCodeBlock>
</EuiFlexItem>
)
{condition}
</EuiCodeBlock>
</EuiFlexItem>,
];
}
function getStatementBody(
isIf,
statement,
vertex,
onShowVertexDetails
) {
const showVertexDetailsClicked = () => { onShowVertexDetails(vertex); };
function getStatementBody(isIf, statement, vertex, onShowVertexDetails) {
const showVertexDetailsClicked = () => {
onShowVertexDetails(vertex);
};
return isIf
? renderIfStatement(statement, showVertexDetailsClicked)
@ -69,7 +55,9 @@ function getStatementBody(
function renderNestingSpacers(depth) {
const spacers = [];
for (let i = 0; i < depth; i += 1) {
spacers.push(<div key={`spacer_${i}`} className="pipelineViewer__spacer" />);
spacers.push(
<div key={`spacer_${i}`} className="pipelineViewer__spacer" />
);
}
return spacers;
}
@ -80,11 +68,11 @@ function renderStatement({
element: {
id,
statement,
statement: { vertex }
statement: { vertex },
},
expand,
isCollapsed,
onShowVertexDetails
onShowVertexDetails,
}) {
if (statement instanceof PluginStatementModel) {
return (
@ -132,9 +120,9 @@ Statement.propTypes = {
element: PropTypes.shape({
depth: PropTypes.number.isRequired,
id: PropTypes.string.isRequired,
statement: PropTypes.object.isRequired
statement: PropTypes.object.isRequired,
}).isRequired,
expand: PropTypes.func.isRequired,
isCollapsed: PropTypes.bool.isRequired,
onShowVertexDetails: PropTypes.func.isRequired
onShowVertexDetails: PropTypes.func.isRequired,
};

View file

@ -0,0 +1,93 @@
/*
* 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 PropTypes from 'prop-types';
import { Statement } from './statement';
function getCollapsedChildIds(elements, collapsedIds) {
const collapsedChildIds = new Set();
elements.forEach(({ id, parentId }) => {
if (collapsedIds.has(parentId) || collapsedChildIds.has(parentId)) {
collapsedChildIds.add(id);
}
});
return collapsedChildIds;
}
export class StatementList extends React.PureComponent {
constructor(props) {
super(props);
this.state = {
collapsedIds: new Set(),
collapsedChildIds: new Set(),
};
}
expand = elementId => {
const collapsedIds = new Set(this.state.collapsedIds);
collapsedIds.delete(elementId);
this.updateCollapsedElement(collapsedIds);
};
collapse = elementId => {
const collapsedIds = new Set(this.state.collapsedIds);
collapsedIds.add(elementId);
this.updateCollapsedElement(collapsedIds);
};
updateCollapsedElement = collapsedIds => {
const { elements } = this.props;
const collapsedChildIds = getCollapsedChildIds(elements, collapsedIds);
this.setState({
collapsedIds,
collapsedChildIds,
});
};
elementIsCollapsed = elementId => this.state.collapsedIds.has(elementId);
renderStatement = element => {
const { id, parentId } = element;
const { onShowVertexDetails } = this.props;
return this.state.collapsedIds.has(parentId) ||
this.state.collapsedChildIds.has(parentId) ? null : (
<Statement
key={id}
element={element}
collapse={this.collapse}
expand={this.expand}
isCollapsed={this.elementIsCollapsed(id)}
onShowVertexDetails={onShowVertexDetails}
/>
);
};
render() {
const { elements } = this.props;
return (
<ul className="pipelineViewer__list">
{elements.map(this.renderStatement)}
</ul>
);
}
}
StatementList.propTypes = {
elements: PropTypes.arrayOf(
PropTypes.shape({
id: PropTypes.string.isRequired,
// top-level elements have null parentId
parentId: PropTypes.string,
})
).isRequired,
onShowVertexDetails: PropTypes.func.isRequired,
};

View file

@ -7,23 +7,22 @@
import React from 'react';
import PropTypes from 'prop-types';
import { StatementListHeading } from './statement_list_heading';
import { Statement } from './statement';
import { EuiSpacer } from '@elastic/eui';
import { StatementList } from './statement_list';
export function StatementSection({
iconType,
headingText,
elements,
onShowVertexDetails
onShowVertexDetails,
}) {
if (!elements.length) { return null; }
if (!elements.length) {
return null;
}
return (
<div>
<StatementListHeading
iconType={iconType}
title={headingText}
/>
<StatementListHeading iconType={iconType} title={headingText} />
<EuiSpacer size="s" />
<StatementList
elements={elements}
@ -33,88 +32,15 @@ export function StatementSection({
);
}
function getCollapsedChildIds(elements, collapsedIds) {
const collapsedChildIds = new Set();
elements.forEach(({ id, parentId }) => {
if (collapsedIds.has(parentId) || collapsedChildIds.has(parentId)) {
collapsedChildIds.add(id);
}
});
return collapsedChildIds;
}
class StatementList extends React.PureComponent {
constructor(props) {
super(props);
this.state = {
collapsedIds: new Set(),
collapsedChildIds: new Set()
};
}
expand = elementId => {
const collapsedIds = new Set(this.state.collapsedIds);
collapsedIds.delete(elementId);
this.updateCollapsedElement(collapsedIds);
}
collapse = elementId => {
const collapsedIds = new Set(this.state.collapsedIds);
collapsedIds.add(elementId);
this.updateCollapsedElement(collapsedIds);
}
updateCollapsedElement = collapsedIds => {
const { elements } = this.props;
const collapsedChildIds = getCollapsedChildIds(elements, collapsedIds);
this.setState({
collapsedIds,
collapsedChildIds
});
}
elementIsCollapsed = elementId => this.state.collapsedIds.has(elementId);
renderStatement = element => {
const { id, parentId } = element;
const { onShowVertexDetails } = this.props;
return this.state.collapsedIds.has(parentId) || this.state.collapsedChildIds.has(parentId)
? null
: (
<Statement
key={id}
element={element}
collapse={this.collapse}
expand={this.expand}
isCollapsed={this.elementIsCollapsed(id)}
onShowVertexDetails={onShowVertexDetails}
/>
);
}
render() {
const { elements } = this.props;
return (
<ul className="pipelineViewer__list">
{
elements.map(this.renderStatement)
}
</ul>
);
}
}
StatementList.propTypes = {
StatementSection.propTypes = {
elements: PropTypes.arrayOf(
PropTypes.shape({
id: PropTypes.string.isRequired,
// top-level elements have null parentId
parentId: PropTypes.string
parentId: PropTypes.string,
})
).isRequired,
headingText: PropTypes.string.isRequired,
iconType: PropTypes.string.isRequired,
onShowVertexDetails: PropTypes.func.isRequired,
};