[Monitoring] [Logstash] Add Config View (#18597)

* Patch ConfigView changes from x-pack-kibana to OSS Kibana.

* Remove old css.

* Update style for queue statement.

* WIP modifying based on designer feedback.

* Add flatten function and list class.

* Rename functions to be more descriptive.

* WIP checkin. Modify components to handle flattened object list.

* Finish moving flatten logic into classes, add tests.

* Simplify flattening, remove non-native dependency. Add more tests.

* Add defaults to simplify function call.

* Refactor two blocks into a function.

* Begin adapting components for list.

* Add collapse functionality.

* Add expand functionality.

* Nested collapsed elements remain collapsed on parent expansion.

* Style section headers.

* Update Plugin statement styles.

* Add DetailDrawer support.

* Update statement formatting.

* Add stats to plugin element rows.

* Resolve conflicts.

* Remove obsolete code.

* Reorganize code.

* Remove warnings.

* Add PropTypes. Add keys to arrays and iterables.

* Update color for borders/buttons.

* Add stat class. Clean up code.

* Convert plugin statement component from class to function.

* Update queue metrics message.

* Update style to make view more responsive.

* Change section heading size.

* Remove gutter from metrics group.

* Change name "stat" to "metric".

* Remove obsolete export line, simplify declaration, based on PR feedback.

* Add new functional component in place of model class.

* Add PropTypes to several components. Rename a function. Add keys to metrics.

* Convert stateless classes to functional components.

* Prefer ES6 syntax over bindings for Component methods.

* Do not render statement section if there are no statements.

* design cleanup for pipeline viewer

* Update CSS to add min-height to page.

* Rename "elements" to "statements". Delete unused LESS variables.

* Revert naming of "statements" to "elements" for StatementList component.

* Update jest snapshots for DetailDrawer.
This commit is contained in:
Justin Kambic 2018-06-18 23:42:12 -04:00 committed by GitHub
parent 61a3c9f1ca
commit 6f7dfcfff2
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
14 changed files with 893 additions and 53 deletions

View file

@ -7,14 +7,24 @@ exports[`DetailDrawer component If vertices shows basic info and no stats for if
>
<EuiFlyoutHeader>
<EuiFlexGroup
alignItems="center"
alignItems="baseline"
component="div"
direction="row"
gutterSize="l"
gutterSize="s"
justifyContent="flexStart"
responsive={true}
wrap={false}
>
<EuiFlexItem
component="div"
grow={false}
>
<img
className="lspvDetailDrawerIcon"
height={18}
width={18}
/>
</EuiFlexItem>
<EuiFlexItem
component="div"
grow={true}
@ -23,11 +33,6 @@ exports[`DetailDrawer component If vertices shows basic info and no stats for if
size="m"
>
<h2>
<img
className="lspvDetailDrawerIcon"
height={18}
width={18}
/>
if
</h2>
</EuiTitle>
@ -38,7 +43,7 @@ exports[`DetailDrawer component If vertices shows basic info and no stats for if
>
<EuiButtonIcon
aria-label="Close"
color="primary"
color="text"
iconType="cross"
onClick={[MockFunction]}
type="button"
@ -78,14 +83,24 @@ exports[`DetailDrawer component Plugin vertices Plugin does not have explicit ID
>
<EuiFlyoutHeader>
<EuiFlexGroup
alignItems="center"
alignItems="baseline"
component="div"
direction="row"
gutterSize="l"
gutterSize="s"
justifyContent="flexStart"
responsive={true}
wrap={false}
>
<EuiFlexItem
component="div"
grow={false}
>
<img
className="lspvDetailDrawerIcon"
height={18}
width={18}
/>
</EuiFlexItem>
<EuiFlexItem
component="div"
grow={true}
@ -94,11 +109,6 @@ exports[`DetailDrawer component Plugin vertices Plugin does not have explicit ID
size="m"
>
<h2>
<img
className="lspvDetailDrawerIcon"
height={18}
width={18}
/>
grok filter
</h2>
</EuiTitle>
@ -109,7 +119,7 @@ exports[`DetailDrawer component Plugin vertices Plugin does not have explicit ID
>
<EuiButtonIcon
aria-label="Close"
color="primary"
color="text"
iconType="cross"
onClick={[MockFunction]}
type="button"
@ -371,14 +381,24 @@ exports[`DetailDrawer component Plugin vertices Plugin has explicit ID shows bas
>
<EuiFlyoutHeader>
<EuiFlexGroup
alignItems="center"
alignItems="baseline"
component="div"
direction="row"
gutterSize="l"
gutterSize="s"
justifyContent="flexStart"
responsive={true}
wrap={false}
>
<EuiFlexItem
component="div"
grow={false}
>
<img
className="lspvDetailDrawerIcon"
height={18}
width={18}
/>
</EuiFlexItem>
<EuiFlexItem
component="div"
grow={true}
@ -387,11 +407,6 @@ exports[`DetailDrawer component Plugin vertices Plugin has explicit ID shows bas
size="m"
>
<h2>
<img
className="lspvDetailDrawerIcon"
height={18}
width={18}
/>
grok filter
</h2>
</EuiTitle>
@ -402,7 +417,7 @@ exports[`DetailDrawer component Plugin vertices Plugin has explicit ID shows bas
>
<EuiButtonIcon
aria-label="Close"
color="primary"
color="text"
iconType="cross"
onClick={[MockFunction]}
type="button"
@ -418,9 +433,12 @@ exports[`DetailDrawer component Plugin vertices Plugin has explicit ID shows bas
This
plugin
's ID is
<strong>
<EuiBadge
color="default"
iconSide="left"
>
parse_apache_logline
</strong>
</EuiBadge>
.
</p>
<EuiTable
@ -657,14 +675,24 @@ exports[`DetailDrawer component Queue vertices shows basic info and no stats for
>
<EuiFlyoutHeader>
<EuiFlexGroup
alignItems="center"
alignItems="baseline"
component="div"
direction="row"
gutterSize="l"
gutterSize="s"
justifyContent="flexStart"
responsive={true}
wrap={false}
>
<EuiFlexItem
component="div"
grow={false}
>
<img
className="lspvDetailDrawerIcon"
height={18}
width={18}
/>
</EuiFlexItem>
<EuiFlexItem
component="div"
grow={true}
@ -673,11 +701,6 @@ exports[`DetailDrawer component Queue vertices shows basic info and no stats for
size="m"
>
<h2>
<img
className="lspvDetailDrawerIcon"
height={18}
width={18}
/>
queue
</h2>
</EuiTitle>
@ -688,7 +711,7 @@ exports[`DetailDrawer component Queue vertices shows basic info and no stats for
>
<EuiButtonIcon
aria-label="Close"
color="primary"
color="text"
iconType="cross"
onClick={[MockFunction]}
type="button"
@ -718,14 +741,24 @@ exports[`DetailDrawer component shows vertex title 1`] = `
>
<EuiFlyoutHeader>
<EuiFlexGroup
alignItems="center"
alignItems="baseline"
component="div"
direction="row"
gutterSize="l"
gutterSize="s"
justifyContent="flexStart"
responsive={true}
wrap={false}
>
<EuiFlexItem
component="div"
grow={false}
>
<img
className="lspvDetailDrawerIcon"
height={18}
width={18}
/>
</EuiFlexItem>
<EuiFlexItem
component="div"
grow={true}
@ -733,13 +766,7 @@ exports[`DetailDrawer component shows vertex title 1`] = `
<EuiTitle
size="m"
>
<h2>
<img
className="lspvDetailDrawerIcon"
height={18}
width={18}
/>
</h2>
<h2 />
</EuiTitle>
</EuiFlexItem>
<EuiFlexItem
@ -748,7 +775,7 @@ exports[`DetailDrawer component shows vertex title 1`] = `
>
<EuiButtonIcon
aria-label="Close"
color="primary"
color="text"
iconType="cross"
onClick={[MockFunction]}
type="button"

View file

@ -0,0 +1,121 @@
/*
* 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,
EuiButtonIcon,
EuiCodeBlock,
EuiFlexGroup,
EuiFlexItem
} from '@elastic/eui';
function renderStatementName(name, onVertexSelected) {
return (
<EuiFlexItem
grow={false}
key="statementName"
>
<EuiButtonEmpty
color="text"
size="xs"
onClick={onVertexSelected}
flush="left"
>
<span className="configViewer__conditional">{name}</span>
</EuiButtonEmpty>
</EuiFlexItem>
);
}
function renderIfStatement({ condition }, onVertexSelected) {
return [
renderStatementName('if', onVertexSelected),
(
<EuiFlexItem
key="ifContent"
grow={false}
>
<EuiCodeBlock
fontSize="s"
paddingSize="none"
transparentBackground={true}
>
{condition}
</EuiCodeBlock>
</EuiFlexItem>
)
];
}
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 (
<EuiFlexGroup
responsive={false}
gutterSize="none"
alignItems="center"
className="configViewer__statement"
>
<EuiFlexItem
key={id}
grow={false}
>
<EuiButtonIcon
aria-label
color="text"
iconType={getToggleIconType(isCollapsed)}
onClick={toggleClicked}
size="s"
/>
</EuiFlexItem>
{getStatementBody(props)}
</EuiFlexGroup>
);
}
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,
};

View file

@ -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 (
<DetailDrawer
vertex={this.state.detailDrawer.vertex}
onHide={this.onHideVertexDetails}
timeseriesTooltipXValueFormatter={this.props.timeseriesTooltipXValueFormatter}
/>
);
}
render() {
const {
inputs,
filters,
outputs,
queue
} = this.props.pipeline;
return (
<EuiPage>
<EuiPageContent verticalPosition="center" horizontalPosition="center" className="configViewer">
<StatementSection
iconType="logstashInput"
headingText="Inputs"
elements={inputs}
onShowVertexDetails={this.onShowVertexDetails}
detailVertex={this.state.detailDrawer.vertex}
/>
<EuiSpacer />
<Queue queue={queue} />
<EuiSpacer />
<StatementSection
iconType="logstashFilter"
headingText="Filters"
elements={filters}
onShowVertexDetails={this.onShowVertexDetails}
detailVertex={this.state.detailDrawer.vertex}
/>
<EuiSpacer />
<StatementSection
iconType="logstashOutput"
headingText="Outputs"
elements={outputs}
onShowVertexDetails={this.onShowVertexDetails}
detailVertex={this.state.detailDrawer.vertex}
/>
{ this.renderDetailDrawer() }
</EuiPageContent>
</EuiPage>
);
}
}
ConfigViewer.propTypes = {
pipeline: PropTypes.shape({
inputs: PropTypes.array.isRequired,
filters: PropTypes.array.isRequired,
outputs: PropTypes.array.isRequired,
queue: PropTypes.object.isRequired,
}).isRequired
};

View file

@ -0,0 +1,7 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
export { ConfigViewer } from './config_viewer';

View file

@ -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 = (
<EuiBadge color="warning" classname={className}>
{value}
</EuiBadge>
);
} else {
stylizedValue = (
<EuiText size="s" color="subdued" className={classes}>
<span>
{value}
</span>
</EuiText>
);
}
return (
<EuiFlexItem
className="configViewer__metricFlexItem"
grow={false}
>
{stylizedValue}
</EuiFlexItem>
);
}
Metric.propTypes = {
className: PropTypes.string.isRequired,
value: PropTypes.string.isRequired,
};

View file

@ -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 [(
<Metric
key="eventsEmitted"
className="configViewer__metric--eventsEmitted"
value={formatMetric(latestEventsPerSecond, '0.[00]a', 'e/s emitted')}
/>
)];
}
function getProcessorStatementMetrics(processorVertex) {
const {
latestMillisPerEvent,
latestEventsPerSecond,
percentOfTotalProcessorTime,
} = processorVertex;
return [
(
<Metric
key="cpuMetric"
className="configViewer__metric--cpuTime"
warning={processorVertex.isTimeConsuming()}
value={formatMetric(Math.round(percentOfTotalProcessorTime || 0), '0', '%', { prependSpace: false })}
/>
),
(
<Metric
key="eventMillis"
className="configViewer__metric--eventMillis"
warning={processorVertex.isSlow()}
value={formatMetric(latestMillisPerEvent, '0.[00]a', 'ms/e')}
/>
),
(
<Metric
key="eventsReceived"
className="configViewer__metric--events"
value={formatMetric(latestEventsPerSecond, '0.[00]a', 'e/s received')}
/>
)
];
}
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 (
<EuiFlexGroup
gutterSize="none"
justifyContent="spaceBetween"
alignItems="center"
className="configViewer__statement"
>
<EuiFlexItem grow={false}>
<EuiFlexGroup
gutterSize="xs"
responsive={false}
alignItems="center"
>
<EuiFlexItem grow={false}>
<EuiButtonEmpty
size="xs"
color="primary"
iconType="dot"
flush="left"
className="configViewer__plugin"
onClick={onNameButtonClick}
>
<span>{name}</span>
</EuiButtonEmpty>
</EuiFlexItem>
{
hasExplicitId &&
<EuiFlexItem grow={false}>
<EuiBadge
onClick={onNameButtonClick}
onClickAriaLabel="View details"
>
{id}
</EuiBadge>
</EuiFlexItem>
}
</EuiFlexGroup>
</EuiFlexItem>
{
statementMetrics &&
<EuiFlexItem grow={false}>
<EuiFlexGroup
gutterSize="s"
>
{statementMetrics}
</EuiFlexGroup>
</EuiFlexItem>
}
</EuiFlexGroup>
);
}
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,
};

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 { EuiSpacer, EuiText } from '@elastic/eui';
export function Queue() {
return (
<div className="configStatementList">
<StatementListHeading
iconType="logstashQueue"
title="Queue"
/>
<EuiSpacer size="s" />
<EuiText className="configViewer__queueMessage">
Queue metrics not available
</EuiText>
</div>
);
}

View file

@ -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(<div key={`spacer_${i}`} className="configViewer__spacer" />);
}
return spacers;
}
function renderStatement({
collapse,
element,
element: {
id,
statement,
},
expand,
isCollapsed,
onShowVertexDetails
}) {
if (statement instanceof PluginStatementModel) {
return (
<PluginStatement
statement={statement}
onShowVertexDetails={onShowVertexDetails}
/>
);
}
return (
<CollapsibleStatement
expand={expand}
collapse={collapse}
statement={statement}
isIf={element instanceof IfElement}
isCollapsed={isCollapsed}
id={id}
onShowVertexDetails={onShowVertexDetails}
/>
);
}
export function Statement(props) {
const { depth } = props.element;
return (
<li className={`configViewer__listItem`}>
<div className="configViewer__spaceContainer">
{renderNestingSpacers(depth)}
</div>
{renderStatement(props)}
</li>
);
}
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
};

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 PropTypes from 'prop-types';
import {
EuiFlexGroup,
EuiFlexItem,
EuiIcon,
EuiTitle
} from '@elastic/eui';
export function StatementListHeading({
iconType,
title
}) {
return (
<EuiFlexGroup
gutterSize="s"
responsive={false}
alignItems="baseline"
>
<EuiFlexItem grow={false}>
<EuiIcon
type={iconType}
/>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiTitle size="s">
<h4>{title}</h4>
</EuiTitle>
</EuiFlexItem>
</EuiFlexGroup>
);
}
StatementListHeading.propTypes = {
iconType: PropTypes.string.isRequired,
title: PropTypes.string.isRequired,
};

View file

@ -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 (
<div className="configStatementList">
<StatementListHeading
iconType={iconType}
title={headingText}
/>
<EuiSpacer size="s" />
<StatementList
elements={elements}
onShowVertexDetails={onShowVertexDetails}
/>
</div>
);
}
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="configViewer__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

@ -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 (
<p>This {vertex.typeString}&#39;s ID is <strong>{ vertex.id }</strong>.</p>
<p>This {vertex.typeString}&#39;s ID is <EuiBadge>{ vertex.id }</EuiBadge>.</p>
);
}
@ -268,17 +269,22 @@ export function DetailDrawer({ vertex, onHide, timeseriesTooltipXValueFormatter
>
<EuiFlyoutHeader>
<EuiFlexGroup
alignItems="center"
alignItems="baseline"
gutterSize="s"
>
<EuiFlexItem grow={false}>
{ renderIcon(vertex) }
</EuiFlexItem>
<EuiFlexItem>
<EuiTitle>
<h2>{ renderIcon(vertex) }{ renderTitle(vertex) }</h2>
<h2>{ renderTitle(vertex) }</h2>
</EuiTitle>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiButtonIcon
onClick={onHide}
iconType="cross"
color="text"
aria-label="Close"
/>
</EuiFlexItem>

View file

@ -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 = (
<PipelineViewer
pipelineState={pipelineState}
const configViewer = (
<ConfigViewer
pipeline={
List.fromPipeline(
Pipeline.fromPipelineGraph(
pipelineState.config.graph
)
)
}
timeseriesTooltipXValueFormatter={timeseriesTooltipXValueFormatter}
/>
);
render(pipelineViewer, $el[0]);
render(configViewer, $el[0]);
});
}
};

View file

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

View file

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