diff --git a/x-pack/plugins/infra/public/components/metrics/index.tsx b/x-pack/plugins/infra/public/components/metrics/index.tsx index 63180ead72c4a..ac41090b11481 100644 --- a/x-pack/plugins/infra/public/components/metrics/index.tsx +++ b/x-pack/plugins/infra/public/components/metrics/index.tsx @@ -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'; @@ -17,9 +18,18 @@ interface Props { layout: InfraMetricLayout[]; loading: boolean; nodeName: string; + onChangeRangeTime?: (time: metricTimeActions.MetricRangeTimeState) => void; } -export class Metrics extends React.PureComponent { +interface State { + crosshairValue: number | null; +} + +export class Metrics extends React.PureComponent { + public readonly state = { + crosshairValue: null, + }; + public render() { if (this.props.loading) { return ( @@ -47,8 +57,28 @@ export class Metrics extends React.PureComponent { }; 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 ( -
+
); }; + + private onCrosshairUpdate = (crosshairValue: number) => { + this.setState({ + crosshairValue, + }); + }; } diff --git a/x-pack/plugins/infra/public/components/metrics/section.tsx b/x-pack/plugins/infra/public/components/metrics/section.tsx index 2de6bc2e5fe8e..89170d053b3a9 100644 --- a/x-pack/plugins/infra/public/components/metrics/section.tsx +++ b/x-pack/plugins/infra/public/components/metrics/section.tsx @@ -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 { @@ -20,7 +24,15 @@ export class Section extends React.PureComponent { 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 ; + return ; } } diff --git a/x-pack/plugins/infra/public/components/metrics/sections/chart_section.tsx b/x-pack/plugins/infra/public/components/metrics/sections/chart_section.tsx index f03d688dc3a81..ad8506f5f62ec 100644 --- a/x-pack/plugins/infra/public/components/metrics/sections/chart_section.tsx +++ b/x-pack/plugins/infra/public/components/metrics/sections/chart_section.tsx @@ -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; @@ -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 = ( @@ -109,12 +113,18 @@ const createItemsFormatter = ( export class ChartSection extends React.PureComponent { 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) { @@ -150,6 +160,7 @@ export class ChartSection extends React.PureComponent { seriesNames={seriesLabels} itemsFormat={itemsFormatter} titleFormat={titleFormatter} + {...crossHairProps} /> {metric && metric.series.map(series => { @@ -184,4 +195,34 @@ export class ChartSection extends React.PureComponent { ); } + + 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; } diff --git a/x-pack/plugins/infra/public/components/metrics/time_controls.tsx b/x-pack/plugins/infra/public/components/metrics/time_controls.tsx new file mode 100644 index 0000000000000..f9209fbb16ccd --- /dev/null +++ b/x-pack/plugins/infra/public/components/metrics/time_controls.tsx @@ -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 = 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 = ( + + + {isLiveStreaming ? ( + + Stop refreshing + + ) : ( + + Auto-refresh + + )} + + + Reset + + + ); + + const goColor = from && to && from > to ? 'danger' : 'primary'; + const appendButton = showGoButton ? ( + + + + Go + + + + Cancel + + + ) : ( + liveStreamingButton + ); + + return ( + + + {appendButton} + + ); + } + + 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; + } +`; diff --git a/x-pack/plugins/infra/public/components/range_date_picker/index.tsx b/x-pack/plugins/infra/public/components/range_date_picker/index.tsx new file mode 100644 index 0000000000000..698b8fde1af72 --- /dev/null +++ b/x-pack/plugins/infra/public/components/range_date_picker/index.tsx @@ -0,0 +1,416 @@ +/* + * 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 { find } from 'lodash'; +import moment from 'moment'; +import React, { Fragment } from 'react'; + +import { + EuiButton, + EuiButtonEmpty, + EuiDatePicker, + EuiDatePickerRange, + EuiFieldNumber, + EuiFlexGrid, + EuiFlexGroup, + EuiFlexItem, + EuiFormControlLayout, + EuiFormRow, + EuiHorizontalRule, + EuiIcon, + EuiLink, + EuiPopover, + EuiSelect, + EuiSpacer, + EuiText, + EuiTitle, +} from '@elastic/eui'; + +const commonDates = [ + 'Today', + 'Yesterday', + 'This week', + 'Week to date', + 'This month', + 'Month to date', + 'This year', + 'Year to date', +]; + +interface RangeDatePickerProps { + startDate: moment.Moment | undefined; + endDate: moment.Moment | undefined; + onChangeRangeTime: ( + from: moment.Moment | undefined, + to: moment.Moment | undefined, + search: boolean + ) => void; + recentlyUsed: RecentlyUsed[]; + disabled?: boolean; + isLoading?: boolean; + ref?: React.RefObject; +} + +export interface RecentlyUsed { + type: string; + text: string | string[]; +} + +interface RangeDatePickerState { + startDate: moment.Moment | undefined; + endDate: moment.Moment | undefined; + isPopoverOpen: boolean; + recentlyUsed: RecentlyUsed[]; + quickSelectTime: number; + quickSelectUnit: string; +} + +export class RangeDatePicker extends React.PureComponent< + RangeDatePickerProps, + RangeDatePickerState +> { + public readonly state = { + startDate: this.props.startDate, + endDate: this.props.endDate, + isPopoverOpen: false, + recentlyUsed: [], + quickSelectTime: 1, + quickSelectUnit: 'hours', + }; + + public render() { + const { isLoading, disabled } = this.props; + const { startDate, endDate } = this.state; + const quickSelectButton = ( + + + + ); + + const commonlyUsed = this.renderCommonlyUsed(commonDates); + const recentlyUsed = this.renderRecentlyUsed([ + ...this.state.recentlyUsed, + ...this.props.recentlyUsed, + ]); + + const quickSelectPopover = ( + +
+ {this.renderQuickSelect()} + + {commonlyUsed} + + {recentlyUsed} +
+
+ ); + + return ( + + endDate : false} + fullWidth + aria-label="Start date" + disabled={disabled} + shouldCloseOnSelect + showTimeSelect + /> + } + endDateControl={ + endDate : false} + fullWidth + disabled={disabled} + isLoading={isLoading} + aria-label="End date" + shouldCloseOnSelect + showTimeSelect + popperPlacement="top-end" + /> + } + /> + + ); + } + + public resetRangeDate(startDate: moment.Moment, endDate: moment.Moment) { + this.setState({ + ...this.state, + startDate, + endDate, + }); + } + + private handleChangeStart = (date: moment.Moment | null) => { + if (date && this.state.startDate !== date) { + this.props.onChangeRangeTime(date, this.state.endDate, false); + this.setState({ + startDate: date, + }); + } + }; + + private handleChangeEnd = (date: moment.Moment | null) => { + if (date && this.state.endDate !== date) { + this.props.onChangeRangeTime(this.state.startDate, date, false); + this.setState({ + endDate: date, + }); + } + }; + + private onButtonClick = () => { + this.setState({ + isPopoverOpen: !this.state.isPopoverOpen, + }); + }; + + private closePopover = (type: string, from?: string, to?: string) => { + const { startDate, endDate, recentlyUsed } = this.managedStartEndDateFromType(type, from, to); + this.setState( + { + ...this.state, + isPopoverOpen: false, + startDate, + endDate, + recentlyUsed, + }, + () => { + if (type) { + this.props.onChangeRangeTime(startDate, endDate, true); + } + } + ); + }; + + private managedStartEndDateFromType(type: string, from?: string, to?: string) { + let { startDate, endDate } = this.state; + let recentlyUsed: RecentlyUsed[] = this.state.recentlyUsed; + let textJustUsed = type; + + if (type === 'quick-select') { + textJustUsed = `Last ${this.state.quickSelectTime} ${singularize( + this.state.quickSelectUnit, + this.state.quickSelectTime + )}`; + startDate = moment().subtract(this.state.quickSelectTime, this.state + .quickSelectUnit as moment.unitOfTime.DurationConstructor); + endDate = moment(); + } else if (type === 'Today') { + startDate = moment().startOf('day'); + endDate = moment() + .startOf('day') + .add(24, 'hour'); + } else if (type === 'Yesterday') { + startDate = moment() + .subtract(1, 'day') + .startOf('day'); + endDate = moment() + .subtract(1, 'day') + .startOf('day') + .add(24, 'hour'); + } else if (type === 'This week') { + startDate = moment().startOf('week'); + endDate = moment() + .startOf('week') + .add(1, 'week'); + } else if (type === 'Week to date') { + startDate = moment().subtract(1, 'week'); + endDate = moment(); + } else if (type === 'This month') { + startDate = moment().startOf('month'); + endDate = moment() + .startOf('month') + .add(1, 'month'); + } else if (type === 'Month to date') { + startDate = moment().subtract(1, 'month'); + endDate = moment(); + } else if (type === 'This year') { + startDate = moment().startOf('year'); + endDate = moment() + .startOf('year') + .add(1, 'year'); + } else if (type === 'Year to date') { + startDate = moment().subtract(1, 'year'); + endDate = moment(); + } else if (type === 'date-range' && to && from) { + startDate = moment(from); + endDate = moment(to); + } + + if (textJustUsed !== undefined && !find(recentlyUsed, ['text', textJustUsed])) { + recentlyUsed.unshift({ type, text: textJustUsed }); + recentlyUsed = recentlyUsed.slice(0, 5); + } + + return { + startDate, + endDate, + recentlyUsed, + }; + } + + private renderQuickSelect = () => { + const lastOptions = [ + { value: 'seconds', text: singularize('seconds', this.state.quickSelectTime) }, + { value: 'minutes', text: singularize('minutes', this.state.quickSelectTime) }, + { value: 'hours', text: singularize('hours', this.state.quickSelectTime) }, + { value: 'days', text: singularize('days', this.state.quickSelectTime) }, + { value: 'weeks', text: singularize('weeks', this.state.quickSelectTime) }, + { value: 'months', text: singularize('months', this.state.quickSelectTime) }, + { value: 'years', text: singularize('years', this.state.quickSelectTime) }, + ]; + + return ( + + + Quick select + + + + + + Last + + + + + { + this.onChange('quickSelectTime', arg); + }} + /> + + + + + { + this.onChange('quickSelectUnit', arg); + }} + /> + + + + + this.closePopover('quick-select')} style={{ minWidth: 0 }}> + Apply + + + + + + ); + }; + + private onChange = (stateType: string, args: any) => { + let value = args.currentTarget.value; + + if (stateType === 'quickSelectTime' && value !== '') { + value = parseInt(args.currentTarget.value, 10); + } + this.setState({ + ...this.state, + [stateType]: value, + }); + }; + + private renderCommonlyUsed = (recentlyCommonDates: string[]) => { + const links = recentlyCommonDates.map(date => { + return ( + + this.closePopover(date)}>{date} + + ); + }); + + return ( + + + Commonly used + + + + + {links} + + + + ); + }; + + private renderRecentlyUsed = (recentDates: RecentlyUsed[]) => { + const links = recentDates.map((date: RecentlyUsed) => { + let dateRange; + let dateLink = ( + this.closePopover(date.type)}>{dateRange || date.text} + ); + if (typeof date.text !== 'string') { + dateRange = `${date.text[0]} – ${date.text[1]}`; + dateLink = ( + this.closePopover(date.type, date.text[0], date.text[1])}> + {dateRange || date.type} + + ); + } + + return ( + + {dateLink} + + ); + }); + + return ( + + + Recently used date ranges + + + + + {links} + + + + ); + }; +} + +const singularize = (str: string, qty: number) => (qty === 1 ? str.slice(0, -1) : str); diff --git a/x-pack/plugins/infra/public/containers/metrics/with_metrics_time.tsx b/x-pack/plugins/infra/public/containers/metrics/with_metrics_time.tsx new file mode 100644 index 0000000000000..c2d4fdcc62322 --- /dev/null +++ b/x-pack/plugins/infra/public/containers/metrics/with_metrics_time.tsx @@ -0,0 +1,96 @@ +/* + * 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 { connect } from 'react-redux'; +import { createSelector } from 'reselect'; + +import { metricTimeActions, metricTimeSelectors, State } from '../../store'; +import { asChildFunctionRenderer } from '../../utils/typed_react'; +import { bindPlainActionCreators } from '../../utils/typed_redux'; +import { UrlStateContainer } from '../../utils/url_state'; + +export const withMetricsTime = connect( + (state: State) => ({ + currentTimeRange: metricTimeSelectors.selectRangeTime(state), + isAutoReloading: metricTimeSelectors.selectIsAutoReloading(state), + urlState: selectTimeUrlState(state), + }), + bindPlainActionCreators({ + setRangeTime: metricTimeActions.setRangeTime, + startMetricsAutoReload: metricTimeActions.startMetricsAutoReload, + stopMetricsAutoReload: metricTimeActions.stopMetricsAutoReload, + }) +); + +export const WithMetricsTime = asChildFunctionRenderer(withMetricsTime, { + onCleanup: ({ stopMetricsAutoReload }) => stopMetricsAutoReload(), +}); + +/** + * Url State + */ + +interface MetricTimeUrlState { + timeRange?: ReturnType; + autoReload?: ReturnType; +} + +export const WithMetricsTimeUrlState = () => ( + + {({ setRangeTime, startMetricsAutoReload, stopMetricsAutoReload, urlState }) => ( + { + if (newUrlState && newUrlState.timeRange) { + setRangeTime(newUrlState.timeRange); + } + if (newUrlState && newUrlState.autoReload) { + startMetricsAutoReload(); + } else if ( + newUrlState && + typeof newUrlState.autoReload !== 'undefined' && + !newUrlState.autoReload + ) { + stopMetricsAutoReload(); + } + }} + onInitialize={initialUrlState => { + if (initialUrlState && initialUrlState.timeRange) { + setRangeTime(initialUrlState.timeRange); + } + if (initialUrlState && initialUrlState.autoReload) { + startMetricsAutoReload(); + } + }} + /> + )} + +); + +const selectTimeUrlState = createSelector( + metricTimeSelectors.selectRangeTime, + metricTimeSelectors.selectIsAutoReloading, + (time, autoReload) => ({ + time, + autoReload, + }) +); + +const mapToUrlState = (value: any): MetricTimeUrlState | undefined => + value + ? { + timeRange: mapToTimeUrlState(value.timeRange), + autoReload: mapToAutoReloadUrlState(value.autoReload), + } + : undefined; + +const mapToTimeUrlState = (value: any) => + value && (typeof value.to === 'number' && typeof value.from === 'number') ? value : undefined; + +const mapToAutoReloadUrlState = (value: any) => (typeof value === 'boolean' ? value : undefined); diff --git a/x-pack/plugins/infra/public/pages/home/page_content.tsx b/x-pack/plugins/infra/public/pages/home/page_content.tsx index c8d1d41562e22..3145e7843208e 100644 --- a/x-pack/plugins/infra/public/pages/home/page_content.tsx +++ b/x-pack/plugins/infra/public/pages/home/page_content.tsx @@ -25,7 +25,7 @@ export const HomePageContent: React.SFC = () => ( {({ filterQueryAsJson }) => ( - {({ currentTimeRange }) => ( + {({ currentTimeRange, isAutoReloading }) => ( {({ metrics, groupBy, nodeType }) => ( ( {({ nodes, loading, refetch }) => ( 0 && isAutoReloading ? false : loading} nodeType={nodeType} options={{ ...wafflemap, metrics, fields: configuredFields }} reload={refetch} diff --git a/x-pack/plugins/infra/public/pages/metrics/index.tsx b/x-pack/plugins/infra/public/pages/metrics/index.tsx index e45aa16fb206f..73cd358c5b0bf 100644 --- a/x-pack/plugins/infra/public/pages/metrics/index.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/index.tsx @@ -3,7 +3,6 @@ * 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 { @@ -19,12 +18,17 @@ import { EuiTitle, } from '@elastic/eui'; import styled, { withTheme } from 'styled-components'; -import { InfraNodeType } from '../../../common/graphql/types'; +import { InfraNodeType, InfraTimerangeInput } from '../../../common/graphql/types'; import { AutoSizer } from '../../components/auto_sizer'; import { Header } from '../../components/header'; import { Metrics } from '../../components/metrics'; +import { MetricsTimeControls } from '../../components/metrics/time_controls'; import { ColumnarPage, PageContent } from '../../components/page'; import { WithMetrics } from '../../containers/metrics/with_metrics'; +import { + WithMetricsTime, + WithMetricsTimeUrlState, +} from '../../containers/metrics/with_metrics_time'; import { WithOptions } from '../../containers/with_options'; import { Error, ErrorPageBody } from '../error'; import { layoutCreators } from './layouts'; @@ -77,68 +81,92 @@ class MetricDetailPage extends React.PureComponent { return (
+ - {({ sourceId, timerange }) => ( - - {({ metrics, error, loading }) => { - if (error) { - return ; - } - return ( - - - - - - - - - - - - - {({ measureRef, bounds: { width = 0 } }) => { - return ( - - - - - - -

{nodeName}

-
-
-
-
- - - -
-
- ); - }} -
-
- ); - }} -
+ {({ sourceId }) => ( + + {({ + currentTimeRange, + isAutoReloading, + setRangeTime, + startMetricsAutoReload, + stopMetricsAutoReload, + }) => ( + + {({ metrics, error, loading }) => { + if (error) { + return ; + } + return ( + + + + + + + + + + + + + {({ measureRef, bounds: { width = 0 } }) => { + return ( + + + + + + + +

{nodeName}

+
+
+ +
+
+
+ + + 0 && isAutoReloading ? false : loading + } + onChangeRangeTime={setRangeTime} + /> + +
+
+ ); + }} +
+
+ ); + }} +
+ )} +
)}
@@ -179,3 +207,9 @@ const MetricsDetailsPageColumn = styled.div` display: flex; flex-direction: column; `; + +const MetricsTitleTimeRangeContainer = styled.div` + display: flex; + flex-flow: row wrap; + justify-content: space-between; +`; diff --git a/x-pack/plugins/infra/public/store/actions.ts b/x-pack/plugins/infra/public/store/actions.ts index e9ae000f165fc..ee9a2858f1c34 100644 --- a/x-pack/plugins/infra/public/store/actions.ts +++ b/x-pack/plugins/infra/public/store/actions.ts @@ -9,6 +9,7 @@ export { logMinimapActions, logPositionActions, logTextviewActions, + metricTimeActions, waffleFilterActions, waffleTimeActions, waffleOptionsActions, diff --git a/x-pack/plugins/infra/public/store/local/actions.ts b/x-pack/plugins/infra/public/store/local/actions.ts index 6420db5f0acda..8b9e0c9f5b58a 100644 --- a/x-pack/plugins/infra/public/store/local/actions.ts +++ b/x-pack/plugins/infra/public/store/local/actions.ts @@ -8,6 +8,7 @@ export { logFilterActions } from './log_filter'; export { logMinimapActions } from './log_minimap'; export { logPositionActions } from './log_position'; export { logTextviewActions } from './log_textview'; +export { metricTimeActions } from './metric_time'; export { waffleFilterActions } from './waffle_filter'; export { waffleTimeActions } from './waffle_time'; export { waffleOptionsActions } from './waffle_options'; diff --git a/x-pack/plugins/infra/public/store/local/epic.ts b/x-pack/plugins/infra/public/store/local/epic.ts index 4cfac85f00b15..274a74b1627c5 100644 --- a/x-pack/plugins/infra/public/store/local/epic.ts +++ b/x-pack/plugins/infra/public/store/local/epic.ts @@ -7,7 +7,12 @@ import { combineEpics } from 'redux-observable'; import { createLogPositionEpic } from './log_position'; +import { createMetricTimeEpic } from './metric_time'; import { createWaffleTimeEpic } from './waffle_time'; export const createLocalEpic = () => - combineEpics(createLogPositionEpic(), createWaffleTimeEpic()); + combineEpics( + createLogPositionEpic(), + createWaffleTimeEpic(), + createMetricTimeEpic() + ); diff --git a/x-pack/plugins/infra/public/store/local/metric_time/actions.ts b/x-pack/plugins/infra/public/store/local/metric_time/actions.ts new file mode 100644 index 0000000000000..8dec727c895c0 --- /dev/null +++ b/x-pack/plugins/infra/public/store/local/metric_time/actions.ts @@ -0,0 +1,21 @@ +/* + * 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 actionCreatorFactory from 'typescript-fsa'; + +const actionCreator = actionCreatorFactory('x-pack/infra/local/waffle_time'); + +export interface MetricRangeTimeState { + to: number; + from: number; + interval: string; +} + +export const setRangeTime = actionCreator('SET_RANGE_TIME'); + +export const startMetricsAutoReload = actionCreator('START_METRICS_AUTO_RELOAD'); + +export const stopMetricsAutoReload = actionCreator('STOP_METRICS_AUTO_RELOAD'); diff --git a/x-pack/plugins/infra/public/store/local/metric_time/epic.ts b/x-pack/plugins/infra/public/store/local/metric_time/epic.ts new file mode 100644 index 0000000000000..aaecdc42a215b --- /dev/null +++ b/x-pack/plugins/infra/public/store/local/metric_time/epic.ts @@ -0,0 +1,59 @@ +/* + * 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 moment from 'moment'; +import { Action } from 'redux'; +import { Epic } from 'redux-observable'; +import { timer } from 'rxjs'; +import { exhaustMap, filter, map, takeUntil, withLatestFrom } from 'rxjs/operators'; + +import { setRangeTime, startMetricsAutoReload, stopMetricsAutoReload } from './actions'; + +interface MetricTimeEpicDependencies { + selectMetricTimeUpdatePolicyInterval: (state: State) => number | null; + selectMetricRangeFromTimeRange: (state: State) => number | null; +} + +export const createMetricTimeEpic = (): Epic< + Action, + Action, + State, + MetricTimeEpicDependencies +> => ( + action$, + state$, + { selectMetricTimeUpdatePolicyInterval, selectMetricRangeFromTimeRange } +) => { + const updateInterval$ = state$.pipe( + map(selectMetricTimeUpdatePolicyInterval), + filter(isNotNull) + ); + + const range$ = state$.pipe( + map(selectMetricRangeFromTimeRange), + filter(isNotNull) + ); + + return action$.pipe( + filter(startMetricsAutoReload.match), + withLatestFrom(updateInterval$, range$), + exhaustMap(([action, updateInterval, range]) => + timer(0, updateInterval).pipe( + map(() => + setRangeTime({ + from: moment() + .subtract(range, 'ms') + .valueOf(), + to: moment().valueOf(), + interval: '1m', + }) + ), + takeUntil(action$.pipe(filter(stopMetricsAutoReload.match))) + ) + ) + ); +}; + +const isNotNull = (value: T | null): value is T => value !== null; diff --git a/x-pack/plugins/infra/public/store/local/metric_time/index.ts b/x-pack/plugins/infra/public/store/local/metric_time/index.ts new file mode 100644 index 0000000000000..1df7b682d1314 --- /dev/null +++ b/x-pack/plugins/infra/public/store/local/metric_time/index.ts @@ -0,0 +1,12 @@ +/* + * 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 * as metricTimeActions from './actions'; +import * as metricTimeSelectors from './selectors'; + +export { metricTimeActions, metricTimeSelectors }; +export * from './epic'; +export * from './reducer'; diff --git a/x-pack/plugins/infra/public/store/local/metric_time/reducer.ts b/x-pack/plugins/infra/public/store/local/metric_time/reducer.ts new file mode 100644 index 0000000000000..00a4d7d311d0d --- /dev/null +++ b/x-pack/plugins/infra/public/store/local/metric_time/reducer.ts @@ -0,0 +1,63 @@ +/* + * 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 moment from 'moment'; +import { combineReducers } from 'redux'; +import { reducerWithInitialState } from 'typescript-fsa-reducers/dist'; + +import { + MetricRangeTimeState, + setRangeTime, + startMetricsAutoReload, + stopMetricsAutoReload, +} from './actions'; + +interface ManualTimeUpdatePolicy { + policy: 'manual'; +} + +interface IntervalTimeUpdatePolicy { + policy: 'interval'; + interval: number; +} + +type TimeUpdatePolicy = ManualTimeUpdatePolicy | IntervalTimeUpdatePolicy; + +export interface MetricTimeState { + timeRange: MetricRangeTimeState; + updatePolicy: TimeUpdatePolicy; +} + +export const initialMetricTimeState: MetricTimeState = { + timeRange: { + from: moment() + .subtract(1, 'hour') + .valueOf(), + to: moment().valueOf(), + interval: '>=1m', + }, + updatePolicy: { + policy: 'manual', + }, +}; + +const timeRangeReducer = reducerWithInitialState(initialMetricTimeState.timeRange).case( + setRangeTime, + (state, { to, from }) => ({ ...state, to, from }) +); + +const updatePolicyReducer = reducerWithInitialState(initialMetricTimeState.updatePolicy) + .case(startMetricsAutoReload, () => ({ + policy: 'interval', + interval: 5000, + })) + .case(stopMetricsAutoReload, () => ({ + policy: 'manual', + })); + +export const metricTimeReducer = combineReducers({ + timeRange: timeRangeReducer, + updatePolicy: updatePolicyReducer, +}); diff --git a/x-pack/plugins/infra/public/store/local/metric_time/selectors.ts b/x-pack/plugins/infra/public/store/local/metric_time/selectors.ts new file mode 100644 index 0000000000000..cac7ac2edca05 --- /dev/null +++ b/x-pack/plugins/infra/public/store/local/metric_time/selectors.ts @@ -0,0 +1,19 @@ +/* + * 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 { MetricTimeState } from './reducer'; + +export const selectRangeTime = (state: MetricTimeState) => state.timeRange; + +export const selectIsAutoReloading = (state: MetricTimeState) => + state.updatePolicy.policy === 'interval'; + +export const selectTimeUpdatePolicyInterval = (state: MetricTimeState) => + state.updatePolicy.policy === 'interval' ? state.updatePolicy.interval : null; + +export const selectRangeFromTimeRange = (state: MetricTimeState) => { + const { to, from } = state.timeRange; + return to - from; +}; diff --git a/x-pack/plugins/infra/public/store/local/reducer.ts b/x-pack/plugins/infra/public/store/local/reducer.ts index f470b63159c15..59e890b748d5e 100644 --- a/x-pack/plugins/infra/public/store/local/reducer.ts +++ b/x-pack/plugins/infra/public/store/local/reducer.ts @@ -10,6 +10,7 @@ import { initialLogFilterState, logFilterReducer, LogFilterState } from './log_f import { initialLogMinimapState, logMinimapReducer, LogMinimapState } from './log_minimap'; import { initialLogPositionState, logPositionReducer, LogPositionState } from './log_position'; import { initialLogTextviewState, logTextviewReducer, LogTextviewState } from './log_textview'; +import { initialMetricTimeState, metricTimeReducer, MetricTimeState } from './metric_time'; import { initialWaffleFilterState, waffleFilterReducer, WaffleFilterState } from './waffle_filter'; import { initialWaffleOptionsState, @@ -23,6 +24,7 @@ export interface LocalState { logMinimap: LogMinimapState; logPosition: LogPositionState; logTextview: LogTextviewState; + metricTime: MetricTimeState; waffleFilter: WaffleFilterState; waffleTime: WaffleTimeState; waffleMetrics: WaffleOptionsState; @@ -33,6 +35,7 @@ export const initialLocalState: LocalState = { logMinimap: initialLogMinimapState, logPosition: initialLogPositionState, logTextview: initialLogTextviewState, + metricTime: initialMetricTimeState, waffleFilter: initialWaffleFilterState, waffleTime: initialWaffleTimeState, waffleMetrics: initialWaffleOptionsState, @@ -43,6 +46,7 @@ export const localReducer = combineReducers({ logMinimap: logMinimapReducer, logPosition: logPositionReducer, logTextview: logTextviewReducer, + metricTime: metricTimeReducer, waffleFilter: waffleFilterReducer, waffleTime: waffleTimeReducer, waffleMetrics: waffleOptionsReducer, diff --git a/x-pack/plugins/infra/public/store/local/selectors.ts b/x-pack/plugins/infra/public/store/local/selectors.ts index b460d8df740be..85188e144ade1 100644 --- a/x-pack/plugins/infra/public/store/local/selectors.ts +++ b/x-pack/plugins/infra/public/store/local/selectors.ts @@ -9,6 +9,7 @@ import { logFilterSelectors as innerLogFilterSelectors } from './log_filter'; import { logMinimapSelectors as innerLogMinimapSelectors } from './log_minimap'; import { logPositionSelectors as innerLogPositionSelectors } from './log_position'; import { logTextviewSelectors as innerLogTextviewSelectors } from './log_textview'; +import { metricTimeSelectors as innerMetricTimeSelectors } from './metric_time'; import { LocalState } from './reducer'; import { waffleFilterSelectors as innerWaffleFilterSelectors } from './waffle_filter'; import { waffleOptionsSelectors as innerWaffleOptionsSelectors } from './waffle_options'; @@ -34,6 +35,11 @@ export const logTextviewSelectors = globalizeSelectors( innerLogTextviewSelectors ); +export const metricTimeSelectors = globalizeSelectors( + (state: LocalState) => state.metricTime, + innerMetricTimeSelectors +); + export const waffleFilterSelectors = globalizeSelectors( (state: LocalState) => state.waffleFilter, innerWaffleFilterSelectors diff --git a/x-pack/plugins/infra/public/store/selectors.ts b/x-pack/plugins/infra/public/store/selectors.ts index 31c88387f2e27..bbe1a4455e43e 100644 --- a/x-pack/plugins/infra/public/store/selectors.ts +++ b/x-pack/plugins/infra/public/store/selectors.ts @@ -15,6 +15,7 @@ import { logMinimapSelectors as localLogMinimapSelectors, logPositionSelectors as localLogPositionSelectors, logTextviewSelectors as localLogTextviewSelectors, + metricTimeSelectors as localMetricTimeSelectors, waffleFilterSelectors as localWaffleFilterSelectors, waffleOptionsSelectors as localWaffleOptionsSelectors, waffleTimeSelectors as localWaffleTimeSelectors, @@ -36,6 +37,7 @@ export const logFilterSelectors = globalizeSelectors(selectLocal, localLogFilter export const logMinimapSelectors = globalizeSelectors(selectLocal, localLogMinimapSelectors); export const logPositionSelectors = globalizeSelectors(selectLocal, localLogPositionSelectors); export const logTextviewSelectors = globalizeSelectors(selectLocal, localLogTextviewSelectors); +export const metricTimeSelectors = globalizeSelectors(selectLocal, localMetricTimeSelectors); export const waffleFilterSelectors = globalizeSelectors(selectLocal, localWaffleFilterSelectors); export const waffleTimeSelectors = globalizeSelectors(selectLocal, localWaffleTimeSelectors); export const waffleOptionsSelectors = globalizeSelectors(selectLocal, localWaffleOptionsSelectors); diff --git a/x-pack/plugins/infra/public/store/store.ts b/x-pack/plugins/infra/public/store/store.ts index f431ecbb5b244..d18ef02875059 100644 --- a/x-pack/plugins/infra/public/store/store.ts +++ b/x-pack/plugins/infra/public/store/store.ts @@ -14,6 +14,7 @@ import { initialState, logEntriesSelectors, logPositionSelectors, + metricTimeSelectors, reducer, sharedSelectors, State, @@ -49,6 +50,8 @@ export function createStore({ apolloClient, observableApi }: StoreDependencies) selectVisibleLogMidpointOrTarget: logPositionSelectors.selectVisibleMidpointOrTarget, selectVisibleLogSummary: logPositionSelectors.selectVisibleSummary, selectWaffleTimeUpdatePolicyInterval: waffleTimeSelectors.selectTimeUpdatePolicyInterval, + selectMetricTimeUpdatePolicyInterval: metricTimeSelectors.selectTimeUpdatePolicyInterval, + selectMetricRangeFromTimeRange: metricTimeSelectors.selectRangeFromTimeRange, }; const epicMiddleware = createEpicMiddleware( diff --git a/x-pack/plugins/infra/server/lib/adapters/framework/kibana_framework_adapter.ts b/x-pack/plugins/infra/server/lib/adapters/framework/kibana_framework_adapter.ts index c9d370768f632..c8e97c89911c8 100644 --- a/x-pack/plugins/infra/server/lib/adapters/framework/kibana_framework_adapter.ts +++ b/x-pack/plugins/infra/server/lib/adapters/framework/kibana_framework_adapter.ts @@ -92,13 +92,14 @@ export class InfraKibanaBackendFrameworkAdapter implements InfraBackendFramework throw new Error('Failed to access indexPatternsService for the request'); } return this.server.indexPatternsServiceFactory({ - callCluster: async (method: string, args: object, ...rest: any[]) => { - return await this.callWithRequest( + callCluster: async (method: string, args: [object], ...rest: any[]) => { + const fieldCaps = await this.callWithRequest( request, method, { ...args, allowNoIndices: true }, ...rest ); + return fieldCaps; }, }); } diff --git a/x-pack/plugins/infra/types/eui.d.ts b/x-pack/plugins/infra/types/eui.d.ts index fdd437e78a9bf..ea4c9eefdc6f0 100644 --- a/x-pack/plugins/infra/types/eui.d.ts +++ b/x-pack/plugins/infra/types/eui.d.ts @@ -10,7 +10,7 @@ */ import { CommonProps, EuiToolTipPosition } from '@elastic/eui'; -import { Moment } from 'moment'; +import moment from 'moment'; import { MouseEventHandler, ReactType, Ref } from 'react'; import { ReactDatePickerProps } from 'react-datepicker'; import { JsonObject } from '../common/typed_json'; @@ -62,12 +62,16 @@ declare module '@elastic/eui' { > & { fullWidth?: boolean; inputRef?: Ref; - injectTimes?: Moment[]; + injectTimes?: moment.Moment[]; isInvalid?: boolean; isLoading?: boolean; - selected?: Moment | null | undefined; + selected?: moment.Moment | null | undefined; placeholder?: string; shadow?: boolean; + calendarContainer?: React.ReactNode; + onChange?: (date: moment.Moment | null) => void; + startDate?: moment.Moment | undefined; + endDate?: moment.Moment | undefined; }; export const EuiDatePicker: React.SFC; @@ -162,4 +166,25 @@ declare module '@elastic/eui' { export const EuiHideFor: React.SFC; export const EuiShowFor: React.SFC; + + type EuiDatePickerRangeProps = CommonProps & { + startDateControl: React.ReactNode; + endDateControl: React.ReactNode; + iconType?: IconType | boolean; + fullWidth?: boolean; + disabled?: boolean; + isLoading?: boolean; + dateFormat?: string; + }; + + export const EuiDatePickerRange: React.SFC; + + type EuiFieldNumberProps = CommonProps & { + defaultValue: string; + value?: number; + onChange?: (arg: any) => void; + step?: number; + }; + + export const EuiFieldNumber: React.SFC; } diff --git a/x-pack/plugins/infra/types/eui_experimental.d.ts b/x-pack/plugins/infra/types/eui_experimental.d.ts index f80a8951a87c8..3e016f491e555 100644 --- a/x-pack/plugins/infra/types/eui_experimental.d.ts +++ b/x-pack/plugins/infra/types/eui_experimental.d.ts @@ -13,6 +13,10 @@ declare module '@elastic/eui/lib/experimental' { yDomain?: number[]; showCrosshair?: boolean; showDefaultAxis?: boolean; + enableSelectionBrush?: boolean; + crosshairValue?: number; + onSelectionBrushEnd?: (args: any) => void; + onCrosshairUpdate?: (crosshairValue: number) => void; }; export const EuiSeriesChart: React.SFC; diff --git a/x-pack/plugins/infra/yarn.lock b/x-pack/plugins/infra/yarn.lock index 7b6a02bea5be1..f32b2ea0064a8 100644 --- a/x-pack/plugins/infra/yarn.lock +++ b/x-pack/plugins/infra/yarn.lock @@ -25,8 +25,8 @@ "@types/color-convert" "*" "@types/lodash@^4.14.110": - version "4.14.110" - resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.110.tgz#fb07498f84152947f30ea09d89207ca07123461e" + version "4.14.116" + resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.116.tgz#5ccf215653e3e8c786a58390751033a9adca0eb9" "@types/node@*": version "10.11.4" @@ -43,5 +43,5 @@ hoek@3.x.x: resolved "https://registry.yarnpkg.com/hoek/-/hoek-3.0.4.tgz#268adff66bb6695c69b4789a88b1e0847c3f3123" lodash@^4.17.10: - version "4.17.10" - resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.10.tgz#1b7793cf7259ea38fb3661d4d38b3260af8ae4e7" + version "4.17.11" + resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.11.tgz#b39ea6229ef607ecd89e2c8df12536891cac9b8d"