Skip to content

Commit

Permalink
[Infra UI] Add timepicker and chart dragging to Node detail page (#23830
Browse files Browse the repository at this point in the history
)

* refactor how to fields if no indexes

* wip to create global date picker

* finish date range picker component

* wip to integrate range date picker to metrics detail page

* fixes #23828

* full integration with date range pciker

* fix yarn.lock

* Fix interval for Metrics query

* Fix build

* Fix bug on Recently used date ranges when you pick a date range

* Add reset and cancel button for metric time control

* set default value to 1 + fix bugs

Reset and Cancel was not really working

* Add SectionBrush/Zoom on graph who will also set the date range

* add crosshair calue between metrics
  • Loading branch information
XavierM authored Oct 15, 2018
1 parent 92c01f4 commit 2b689ef
Show file tree
Hide file tree
Showing 24 changed files with 1,142 additions and 78 deletions.
34 changes: 32 additions & 2 deletions x-pack/plugins/infra/public/components/metrics/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import React from 'react';

import { InfraMetricData } from '../../../common/graphql/types';
import { InfraMetricLayout, InfraMetricLayoutSection } from '../../pages/metrics/layouts/types';
import { metricTimeActions } from '../../store';
import { InfraLoadingPanel } from '../loading';
import { Section } from './section';

Expand All @@ -17,9 +18,18 @@ interface Props {
layout: InfraMetricLayout[];
loading: boolean;
nodeName: string;
onChangeRangeTime?: (time: metricTimeActions.MetricRangeTimeState) => void;
}

export class Metrics extends React.PureComponent<Props> {
interface State {
crosshairValue: number | null;
}

export class Metrics extends React.PureComponent<Props, State> {
public readonly state = {
crosshairValue: null,
};

public render() {
if (this.props.loading) {
return (
Expand Down Expand Up @@ -47,8 +57,28 @@ export class Metrics extends React.PureComponent<Props> {
};

private renderSection = (layout: InfraMetricLayout) => (section: InfraMetricLayoutSection) => {
let sectionProps = {};
if (section.type === 'chart') {
const { onChangeRangeTime } = this.props;
sectionProps = {
onChangeRangeTime,
crosshairValue: this.state.crosshairValue,
onCrosshairUpdate: this.onCrosshairUpdate,
};
}
return (
<Section section={section} metrics={this.props.metrics} key={`${layout.id}-${section.id}`} />
<Section
section={section}
metrics={this.props.metrics}
key={`${layout.id}-${section.id}`}
{...sectionProps}
/>
);
};

private onCrosshairUpdate = (crosshairValue: number) => {
this.setState({
crosshairValue,
});
};
}
14 changes: 13 additions & 1 deletion x-pack/plugins/infra/public/components/metrics/section.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,15 @@
import React from 'react';
import { InfraMetricData } from '../../../common/graphql/types';
import { InfraMetricLayoutSection } from '../../pages/metrics/layouts/types';
import { metricTimeActions } from '../../store';
import { sections } from './sections';

interface Props {
section: InfraMetricLayoutSection;
metrics: InfraMetricData[];
onChangeRangeTime?: (time: metricTimeActions.MetricRangeTimeState) => void;
crosshairValue?: number;
onCrosshairUpdate?: (crosshairValue: number) => void;
}

export class Section extends React.PureComponent<Props> {
Expand All @@ -20,7 +24,15 @@ export class Section extends React.PureComponent<Props> {
if (!metric) {
return null;
}
let sectionProps = {};
if (this.props.section.type === 'chart') {
sectionProps = {
onChangeRangeTime: this.props.onChangeRangeTime,
crosshairValue: this.props.crosshairValue,
onCrosshairUpdate: this.props.onCrosshairUpdate,
};
}
const Component = sections[this.props.section.type];
return <Component section={this.props.section} metric={metric} />;
return <Component section={this.props.section} metric={metric} {...sectionProps} />;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import {
InfraMetricLayoutSection,
InfraMetricLayoutVisualizationType,
} from '../../../pages/metrics/layouts/types';
import { metricTimeActions } from '../../../store';
import { createFormatter } from '../../../utils/formatters';

const MARGIN_LEFT = 60;
Expand All @@ -38,6 +39,9 @@ const chartComponentsByType = {
interface Props {
section: InfraMetricLayoutSection;
metric: InfraMetricData;
onChangeRangeTime?: (time: metricTimeActions.MetricRangeTimeState) => void;
crosshairValue?: number;
onCrosshairUpdate?: (crosshairValue: number) => void;
}

const isInfraMetricLayoutVisualizationType = (
Expand Down Expand Up @@ -109,12 +113,18 @@ const createItemsFormatter = (

export class ChartSection extends React.PureComponent<Props> {
public render() {
const { section, metric } = this.props;
const { crosshairValue, section, metric, onCrosshairUpdate } = this.props;
const { visConfig } = section;
const crossHairProps = {
crosshairValue,
onCrosshairUpdate,
};
const chartProps: EuiSeriesChartProps = {
xType: 'time',
showCrosshair: false,
showDefaultAxis: false,
enableSelectionBrush: true,
onSelectionBrushEnd: this.handleSelectionBrushEnd,
};
const stacked = visConfig && visConfig.stacked;
if (stacked) {
Expand Down Expand Up @@ -150,6 +160,7 @@ export class ChartSection extends React.PureComponent<Props> {
seriesNames={seriesLabels}
itemsFormat={itemsFormatter}
titleFormat={titleFormatter}
{...crossHairProps}
/>
{metric &&
metric.series.map(series => {
Expand Down Expand Up @@ -184,4 +195,34 @@ export class ChartSection extends React.PureComponent<Props> {
</EuiPageContentBody>
);
}

private handleSelectionBrushEnd = (area: Area) => {
const { onChangeRangeTime } = this.props;
const { startX, endX } = area.domainArea;
if (onChangeRangeTime) {
onChangeRangeTime({
to: endX.valueOf(),
from: startX.valueOf(),
} as metricTimeActions.MetricRangeTimeState);
}
};
}

interface DomainArea {
startX: moment.Moment;
endX: moment.Moment;
startY: number;
endY: number;
}

interface DrawArea {
x0: number;
x1: number;
y0: number;
y1: number;
}

interface Area {
domainArea: DomainArea;
drawArea: DrawArea;
}
209 changes: 209 additions & 0 deletions x-pack/plugins/infra/public/components/metrics/time_controls.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,209 @@
/*
* 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 { EuiButton, EuiButtonEmpty, EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
import moment, { Moment } from 'moment';
import React from 'react';
import styled from 'styled-components';

import { RangeDatePicker, RecentlyUsed } from '../range_date_picker';

import { metricTimeActions } from '../../store';

interface MetricsTimeControlsProps {
currentTimeRange: metricTimeActions.MetricRangeTimeState;
isLiveStreaming?: boolean;
onChangeRangeTime?: (time: metricTimeActions.MetricRangeTimeState) => void;
startLiveStreaming?: () => void;
stopLiveStreaming?: () => void;
}

interface MetricsTimeControlsState {
showGoButton: boolean;
to: moment.Moment | undefined;
from: moment.Moment | undefined;
recentlyUsed: RecentlyUsed[];
}

export class MetricsTimeControls extends React.Component<
MetricsTimeControlsProps,
MetricsTimeControlsState
> {
public dateRangeRef: React.RefObject<any> = React.createRef();
public readonly state = {
showGoButton: false,
to: moment().millisecond(this.props.currentTimeRange.to),
from: moment().millisecond(this.props.currentTimeRange.from),
recentlyUsed: [],
};
public render() {
const { currentTimeRange, isLiveStreaming } = this.props;
const { showGoButton, to, from, recentlyUsed } = this.state;

const liveStreamingButton = (
<EuiFlexGroup gutterSize="s" justifyContent="flexStart">
<EuiFlexItem grow={false}>
{isLiveStreaming ? (
<EuiButton
color="primary"
iconSide="left"
iconType="pause"
onClick={this.stopLiveStreaming}
>
Stop refreshing
</EuiButton>
) : (
<EuiButton iconSide="left" iconType="play" onClick={this.startLiveStreaming}>
Auto-refresh
</EuiButton>
)}
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiButtonEmpty onClick={this.resetSearch}>Reset</EuiButtonEmpty>
</EuiFlexItem>
</EuiFlexGroup>
);

const goColor = from && to && from > to ? 'danger' : 'primary';
const appendButton = showGoButton ? (
<EuiFlexGroup gutterSize="s" justifyContent="flexStart">
<EuiFlexItem grow={false}>
<EuiButton color={goColor} fill onClick={this.searchRangeTime}>
Go
</EuiButton>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiButtonEmpty onClick={this.cancelSearch}>Cancel</EuiButtonEmpty>
</EuiFlexItem>
</EuiFlexGroup>
) : (
liveStreamingButton
);

return (
<MetricsTimeControlsContainer>
<RangeDatePicker
key={`${currentTimeRange.from}-${currentTimeRange.to}`}
startDate={moment(currentTimeRange.from)}
endDate={moment(currentTimeRange.to)}
onChangeRangeTime={this.handleChangeDate}
isLoading={isLiveStreaming}
disabled={isLiveStreaming}
recentlyUsed={recentlyUsed}
ref={this.dateRangeRef}
/>
{appendButton}
</MetricsTimeControlsContainer>
);
}

private handleChangeDate = (
from: Moment | undefined,
to: Moment | undefined,
search: boolean
) => {
const { onChangeRangeTime } = this.props;
const duration = moment.duration(from && to ? from.diff(to) : 0);
const milliseconds = duration.asMilliseconds();
if (to && from && onChangeRangeTime && search && to > from) {
this.setState({
showGoButton: false,
to,
from,
});
onChangeRangeTime({
to: to && to.valueOf(),
from: from && from.valueOf(),
} as metricTimeActions.MetricRangeTimeState);
} else if (milliseconds !== 0) {
this.setState({
showGoButton: true,
to,
from,
});
}
};

private searchRangeTime = () => {
const { onChangeRangeTime } = this.props;
const { to, from, recentlyUsed } = this.state;
if (to && from && onChangeRangeTime && to > from) {
this.setState({
...this.state,
showGoButton: false,
recentlyUsed: [
...recentlyUsed,
...[
{
type: 'date-range',
text: [from.format('L LTS'), to.format('L LTS')],
},
],
],
});
onChangeRangeTime({
to: to.valueOf(),
from: from.valueOf(),
} as metricTimeActions.MetricRangeTimeState);
}
};

private startLiveStreaming = () => {
const { startLiveStreaming } = this.props;

if (startLiveStreaming) {
startLiveStreaming();
}
};

private stopLiveStreaming = () => {
const { stopLiveStreaming } = this.props;

if (stopLiveStreaming) {
stopLiveStreaming();
}
};

private cancelSearch = () => {
const { onChangeRangeTime } = this.props;
const to = moment(this.props.currentTimeRange.to);
const from = moment(this.props.currentTimeRange.from);

this.setState({
...this.state,
showGoButton: false,
to,
from,
});
this.dateRangeRef.current.resetRangeDate(from, to);
if (onChangeRangeTime) {
onChangeRangeTime({
to: to && to.valueOf(),
from: from && from.valueOf(),
} as metricTimeActions.MetricRangeTimeState);
}
};

private resetSearch = () => {
const { onChangeRangeTime } = this.props;
const to = moment();
const from = moment().subtract(1, 'hour');
if (onChangeRangeTime) {
onChangeRangeTime({
to: to.valueOf(),
from: from.valueOf(),
} as metricTimeActions.MetricRangeTimeState);
}
};
}
const MetricsTimeControlsContainer = styled.div`
display: flex;
justify-content: right;
flex-flow: row wrap;
& > div:first-child {
margin-right: 0.5rem;
}
`;
Loading

0 comments on commit 2b689ef

Please sign in to comment.