diff --git a/x-pack/plugins/monitoring/public/components/logstash/pipeline_viewer/views/__test__/__snapshots__/detail_drawer.test.js.snap b/x-pack/plugins/monitoring/public/components/logstash/pipeline_viewer/views/__test__/__snapshots__/detail_drawer.test.js.snap index a332f27d9062..337b7738fd92 100644 --- a/x-pack/plugins/monitoring/public/components/logstash/pipeline_viewer/views/__test__/__snapshots__/detail_drawer.test.js.snap +++ b/x-pack/plugins/monitoring/public/components/logstash/pipeline_viewer/views/__test__/__snapshots__/detail_drawer.test.js.snap @@ -7,14 +7,24 @@ exports[`DetailDrawer component If vertices shows basic info and no stats for if > + + +

- if

@@ -38,7 +43,7 @@ exports[`DetailDrawer component If vertices shows basic info and no stats for if > + + +

- grok filter

@@ -109,7 +119,7 @@ exports[`DetailDrawer component Plugin vertices Plugin does not have explicit ID > + + +

- grok filter

@@ -402,7 +417,7 @@ exports[`DetailDrawer component Plugin vertices Plugin has explicit ID shows bas > + parse_apache_logline - + .

+ + +

- queue

@@ -688,7 +711,7 @@ exports[`DetailDrawer component Queue vertices shows basic info and no stats for > + + + -

- -

+

+ + {name} + + + ); +} + +function renderIfStatement({ condition }, onVertexSelected) { + return [ + renderStatementName('if', onVertexSelected), + ( + + + {condition} + + + ) + ]; +} + +function getStatementBody({ + isIf, + statement, + statement: { vertex }, + onShowVertexDetails +}) { + const showVertexDetailsClicked = () => { onShowVertexDetails(vertex); }; + + return isIf + ? renderIfStatement(statement, showVertexDetailsClicked) + : renderStatementName('else', showVertexDetailsClicked); +} + +function getToggleIconType(isCollapsed) { + return isCollapsed ? 'arrowRight' : 'arrowDown'; +} + +export function CollapsibleStatement(props) { + const { + collapse, + expand, + id, + isCollapsed + } = props; + + const toggleClicked = () => { + if (isCollapsed) { + expand(id); + } else { + collapse(id); + } + }; + + return ( + + + + + {getStatementBody(props)} + + ); +} + +CollapsibleStatement.propTypes = { + collapse: PropTypes.func.isRequired, + expand: PropTypes.func.isRequired, + id: PropTypes.string.isRequired, + isIf: PropTypes.bool.isRequired, + isCollapsed: PropTypes.bool.isRequired, + onShowVertexDetails: PropTypes.func.isRequired, + statement: PropTypes.object.isRequired, +}; diff --git a/x-pack/plugins/monitoring/public/components/logstash/pipeline_viewer/views/config_viewer/config_viewer.js b/x-pack/plugins/monitoring/public/components/logstash/pipeline_viewer/views/config_viewer/config_viewer.js new file mode 100644 index 000000000000..417f76da5f08 --- /dev/null +++ b/x-pack/plugins/monitoring/public/components/logstash/pipeline_viewer/views/config_viewer/config_viewer.js @@ -0,0 +1,113 @@ +/* + * 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 { DetailDrawer } from '../detail_drawer'; +import { Queue } from './queue'; +import { StatementSection } from './statement_section'; +import { + EuiSpacer, + EuiPage, + EuiPageContent, +} from '@elastic/eui'; + +export class ConfigViewer 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 ( + + ); + } + + render() { + const { + inputs, + filters, + outputs, + queue + } = this.props.pipeline; + + return ( + + + + + + + + + + { this.renderDetailDrawer() } + + + ); + } +} + +ConfigViewer.propTypes = { + pipeline: PropTypes.shape({ + inputs: PropTypes.array.isRequired, + filters: PropTypes.array.isRequired, + outputs: PropTypes.array.isRequired, + queue: PropTypes.object.isRequired, + }).isRequired +}; diff --git a/x-pack/plugins/monitoring/public/components/logstash/pipeline_viewer/views/config_viewer/index.js b/x-pack/plugins/monitoring/public/components/logstash/pipeline_viewer/views/config_viewer/index.js new file mode 100644 index 000000000000..4ac6722ff5c8 --- /dev/null +++ b/x-pack/plugins/monitoring/public/components/logstash/pipeline_viewer/views/config_viewer/index.js @@ -0,0 +1,7 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export { ConfigViewer } from './config_viewer'; diff --git a/x-pack/plugins/monitoring/public/components/logstash/pipeline_viewer/views/config_viewer/metric.js b/x-pack/plugins/monitoring/public/components/logstash/pipeline_viewer/views/config_viewer/metric.js new file mode 100644 index 000000000000..feb74298458f --- /dev/null +++ b/x-pack/plugins/monitoring/public/components/logstash/pipeline_viewer/views/config_viewer/metric.js @@ -0,0 +1,52 @@ +/* + * 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 { + EuiFlexItem, + EuiBadge, + EuiText, +} from '@elastic/eui'; +import classNames from 'classnames'; + +export function Metric({ className, value, warning }) { + + const classes = classNames( + 'configViewer__metric', + className, + ); + + let stylizedValue; + if (warning) { + stylizedValue = ( + + {value} + + ); + } else { + stylizedValue = ( + + + {value} + + + ); + } + return ( + + {stylizedValue} + + ); +} + +Metric.propTypes = { + className: PropTypes.string.isRequired, + value: PropTypes.string.isRequired, +}; diff --git a/x-pack/plugins/monitoring/public/components/logstash/pipeline_viewer/views/config_viewer/plugin_statement.js b/x-pack/plugins/monitoring/public/components/logstash/pipeline_viewer/views/config_viewer/plugin_statement.js new file mode 100644 index 000000000000..1c89e2c17f61 --- /dev/null +++ b/x-pack/plugins/monitoring/public/components/logstash/pipeline_viewer/views/config_viewer/plugin_statement.js @@ -0,0 +1,142 @@ +/* + * 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 { + EuiButtonEmpty, + EuiFlexGroup, + EuiFlexItem, + EuiBadge, +} from '@elastic/eui'; +import { formatMetric } from '../../../../../lib/format_number'; +import { Metric } from './metric'; + +function getInputStatementMetrics({ latestEventsPerSecond }) { + return [( + + )]; +} + +function getProcessorStatementMetrics(processorVertex) { + const { + latestMillisPerEvent, + latestEventsPerSecond, + percentOfTotalProcessorTime, + } = processorVertex; + + return [ + ( + + ), + ( + + ), + ( + + ) + ]; +} + +function renderPluginStatementMetrics(pluginType, vertex) { + return pluginType === 'input' + ? getInputStatementMetrics(vertex) + : getProcessorStatementMetrics(vertex); +} + +export function PluginStatement({ + statement: { + hasExplicitId, + id, + name, + pluginType, + vertex + }, + onShowVertexDetails +}) { + const statementMetrics = renderPluginStatementMetrics(pluginType, vertex); + const onNameButtonClick = () => { onShowVertexDetails(vertex); }; + + return ( + + + + + + {name} + + + { + hasExplicitId && + + + {id} + + + } + + + { + statementMetrics && + + + {statementMetrics} + + + } + + ); +} + +PluginStatement.propTypes = { + statement: PropTypes.shape({ + hasExplicitId: PropTypes.bool.isRequired, + id: PropTypes.string.isRequired, + name: PropTypes.string.isRequired, + pluginType: PropTypes.string.isRequired, + vertex: PropTypes.object.isRequired, + }).isRequired, + onShowVertexDetails: PropTypes.func.isRequired, +}; diff --git a/x-pack/plugins/monitoring/public/components/logstash/pipeline_viewer/views/config_viewer/queue.js b/x-pack/plugins/monitoring/public/components/logstash/pipeline_viewer/views/config_viewer/queue.js new file mode 100644 index 000000000000..8dc5971857c6 --- /dev/null +++ b/x-pack/plugins/monitoring/public/components/logstash/pipeline_viewer/views/config_viewer/queue.js @@ -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 { EuiSpacer, EuiText } from '@elastic/eui'; + +export function Queue() { + return ( +
+ + + + Queue metrics not available + +
+ ); +} diff --git a/x-pack/plugins/monitoring/public/components/logstash/pipeline_viewer/views/config_viewer/statement.js b/x-pack/plugins/monitoring/public/components/logstash/pipeline_viewer/views/config_viewer/statement.js new file mode 100644 index 000000000000..545c7934db8e --- /dev/null +++ b/x-pack/plugins/monitoring/public/components/logstash/pipeline_viewer/views/config_viewer/statement.js @@ -0,0 +1,78 @@ +/* + * 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 { PluginStatement as PluginStatementModel } from '../../models/pipeline/plugin_statement'; +import { CollapsibleStatement } from './collapsible_statement'; +import { IfElement } from '../../models/list/if_element'; +import { PluginStatement } from './plugin_statement'; + +function renderNestingSpacers(depth) { + const spacers = []; + for (let i = 0; i < depth; i += 1) { + spacers.push(
); + } + return spacers; +} + +function renderStatement({ + collapse, + element, + element: { + id, + statement, + }, + expand, + isCollapsed, + onShowVertexDetails +}) { + if (statement instanceof PluginStatementModel) { + return ( + + ); + } + + return ( + + ); +} + +export function Statement(props) { + const { depth } = props.element; + + return ( +
  • +
    + {renderNestingSpacers(depth)} +
    + {renderStatement(props)} +
  • + ); +} + +Statement.propTypes = { + collapse: PropTypes.func.isRequired, + element: PropTypes.shape({ + depth: PropTypes.number.isRequired, + id: PropTypes.string.isRequired, + statement: PropTypes.object.isRequired + }).isRequired, + expand: PropTypes.func.isRequired, + isCollapsed: PropTypes.bool.isRequired, + onShowVertexDetails: PropTypes.func.isRequired +}; diff --git a/x-pack/plugins/monitoring/public/components/logstash/pipeline_viewer/views/config_viewer/statement_list_heading.js b/x-pack/plugins/monitoring/public/components/logstash/pipeline_viewer/views/config_viewer/statement_list_heading.js new file mode 100644 index 000000000000..18ae8c3277a4 --- /dev/null +++ b/x-pack/plugins/monitoring/public/components/logstash/pipeline_viewer/views/config_viewer/statement_list_heading.js @@ -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 PropTypes from 'prop-types'; +import { + EuiFlexGroup, + EuiFlexItem, + EuiIcon, + EuiTitle +} from '@elastic/eui'; + +export function StatementListHeading({ + iconType, + title +}) { + return ( + + + + + + +

    {title}

    +
    +
    +
    + ); +} + +StatementListHeading.propTypes = { + iconType: PropTypes.string.isRequired, + title: PropTypes.string.isRequired, +}; diff --git a/x-pack/plugins/monitoring/public/components/logstash/pipeline_viewer/views/config_viewer/statement_section.js b/x-pack/plugins/monitoring/public/components/logstash/pipeline_viewer/views/config_viewer/statement_section.js new file mode 100644 index 000000000000..f0f165c82ea7 --- /dev/null +++ b/x-pack/plugins/monitoring/public/components/logstash/pipeline_viewer/views/config_viewer/statement_section.js @@ -0,0 +1,120 @@ +/* + * 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 { StatementListHeading } from './statement_list_heading'; +import { Statement } from './statement'; +import { EuiSpacer } from '@elastic/eui'; + +export function StatementSection({ + iconType, + headingText, + elements, + onShowVertexDetails +}) { + if (!elements.length) { return null; } + + return ( +
    + + + +
    + ); +} + +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 + : ( + + ); + } + + render() { + const { elements } = this.props; + + return ( +
      + { + elements.map(this.renderStatement) + } +
    + ); + } +} + +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, +}; diff --git a/x-pack/plugins/monitoring/public/components/logstash/pipeline_viewer/views/detail_drawer.js b/x-pack/plugins/monitoring/public/components/logstash/pipeline_viewer/views/detail_drawer.js index c384802c4f45..b1b5d70827f2 100644 --- a/x-pack/plugins/monitoring/public/components/logstash/pipeline_viewer/views/detail_drawer.js +++ b/x-pack/plugins/monitoring/public/components/logstash/pipeline_viewer/views/detail_drawer.js @@ -20,7 +20,8 @@ import { EuiFlexGroup, EuiFlexItem, EuiButtonIcon, - EuiSpacer + EuiSpacer, + EuiBadge, } from '@elastic/eui'; import { Sparkline } from '../../../sparkline'; import { formatMetric } from '../../../../lib/format_number'; @@ -189,7 +190,7 @@ function renderBasicStats(vertex, timeseriesTooltipXValueFormatter) { function renderPluginBasicInfo(vertex) { if (vertex.hasExplicitId) { return ( -

    This {vertex.typeString}'s ID is { vertex.id }.

    +

    This {vertex.typeString}'s ID is { vertex.id }.

    ); } @@ -268,17 +269,22 @@ export function DetailDrawer({ vertex, onHide, timeseriesTooltipXValueFormatter > + + { renderIcon(vertex) } + -

    { renderIcon(vertex) }{ renderTitle(vertex) }

    +

    { renderTitle(vertex) }

    diff --git a/x-pack/plugins/monitoring/public/directives/logstash/pipeline_viewer/index.js b/x-pack/plugins/monitoring/public/directives/logstash/pipeline_viewer/index.js index 7a50219686e3..4886f5c3df41 100644 --- a/x-pack/plugins/monitoring/public/directives/logstash/pipeline_viewer/index.js +++ b/x-pack/plugins/monitoring/public/directives/logstash/pipeline_viewer/index.js @@ -8,7 +8,9 @@ import React from 'react'; import { render } from 'react-dom'; import moment from 'moment'; import { uiModules } from 'ui/modules'; -import { PipelineViewer } from 'plugins/monitoring/components/logstash/pipeline_viewer'; +import { ConfigViewer } from 'plugins/monitoring/components/logstash/pipeline_viewer/views/config_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'; const uiModule = uiModules.get('monitoring/directives', []); @@ -28,13 +30,19 @@ uiModule.directive('monitoringLogstashPipelineViewer', ($injector) => { scope.$watch('pipeline', (updatedPipeline) => { pipelineState.update(updatedPipeline); - const pipelineViewer = ( - ); - render(pipelineViewer, $el[0]); + render(configViewer, $el[0]); }); } }; diff --git a/x-pack/plugins/monitoring/public/less/components/logstash/config_viewer.less b/x-pack/plugins/monitoring/public/less/components/logstash/config_viewer.less new file mode 100644 index 000000000000..b237affc0c53 --- /dev/null +++ b/x-pack/plugins/monitoring/public/less/components/logstash/config_viewer.less @@ -0,0 +1,98 @@ +@import (reference) '~ui/styles/variables/colors'; + +monitoring-main[page="pipeline"] { + background: @globalColorLightestGray; + min-height: 100vh; +} + +.configViewer { + max-width: 1000px; +} + +.configViewer__statement { + padding-left: 12px; +} + +.configViewer__plugin { + margin-left: 4px; +} + +.configViewer__spaceContainer { + background-color: white; + align-self: stretch; + display: flex; + // Separates the left border spaces properly + border-bottom: solid 2px white; +} + +.configViewer__spacer { + width: 12px; + align-self: stretch; + margin-left: 12px; + border-left: 1px @globalColorMediumGray dashed; + + // This allows the border to be flush + &:last-child { + width: 0px; + } + + &:first-child { + // Odd number is because of the single pixel border. + margin-left: 23px; + } +} + +.configViewer__metric { + text-align: right; + + &--cputTime { + width: 40px; + } + + &--events, &--eventsEmitted { + width: 160px; + } + + &--eventMillis { + width: 80px; + } +} + +.configViewer__queueMessage { + margin-left: 24px; + color: @globalColorDarkGray; +} + +.configViewer__list { + .configViewer__listItem { + display: flex; + min-height: 32px; + align-items: center; + padding-right: 12px; + + &:nth-child(2n+1) { + background: #fafafa; + } + } +} + +.configViewer__conditional { + font-weight: bold; +} + +@media (max-width: 768px) { + .configViewer { + .configViewer__spacer { + border: none; + } + + .configViewer__metricFlexItem { + margin-bottom: 4px !important; + } + + .configViewer__metric { + text-align: left; + padding-left: 32px; + } + } +} diff --git a/x-pack/plugins/monitoring/public/less/main.less b/x-pack/plugins/monitoring/public/less/main.less index c140aabfc0a0..08be28be7d69 100644 --- a/x-pack/plugins/monitoring/public/less/main.less +++ b/x-pack/plugins/monitoring/public/less/main.less @@ -11,6 +11,7 @@ @import './components/chart'; @import './components/sparkline'; @import './components/status_icon'; +@import './components/logstash/config_viewer'; @import './components/logstash/pipeline_viewer'; @import './components/logstash/pipeline_card_group'; @import './components/logstash/beta_icon';