diff --git a/src/ui/public/routes/route_manager.d.ts b/src/ui/public/routes/route_manager.d.ts index 5a06be87c4775..1bef336685e94 100644 --- a/src/ui/public/routes/route_manager.d.ts +++ b/src/ui/public/routes/route_manager.d.ts @@ -23,6 +23,7 @@ interface RouteConfiguration { controller?: string | (() => void); + reloadOnSearch?: boolean; template?: string; } diff --git a/x-pack/plugins/infra/public/components/autocomplete_field/autocomplete_field.tsx b/x-pack/plugins/infra/public/components/autocomplete_field/autocomplete_field.tsx index e2a1331c2fc2d..982de13ad9685 100644 --- a/x-pack/plugins/infra/public/components/autocomplete_field/autocomplete_field.tsx +++ b/x-pack/plugins/infra/public/components/autocomplete_field/autocomplete_field.tsx @@ -31,6 +31,7 @@ interface AutocompleteFieldProps { interface AutocompleteFieldState { areSuggestionsVisible: boolean; + isFocused: boolean; selectedIndex: number | null; } @@ -40,6 +41,7 @@ export class AutocompleteField extends React.Component< > { public readonly state: AutocompleteFieldState = { areSuggestionsVisible: false, + isFocused: false, selectedIndex: null, }; @@ -50,7 +52,7 @@ export class AutocompleteField extends React.Component< const { areSuggestionsVisible, selectedIndex } = this.state; return ( - + { + this.setState(composeStateUpdaters(withSuggestionsVisible, withFocused)); + }; + + private handleBlur = () => { + this.setState(composeStateUpdaters(withSuggestionsHidden, withUnfocused)); + }; + private selectSuggestionAt = (index: number) => () => { this.setState(withSuggestionAtIndexSelected(index)); }; @@ -196,10 +206,6 @@ export class AutocompleteField extends React.Component< this.setState(withSuggestionsVisible); }; - private hideSuggestions = () => { - this.setState(withSuggestionsHidden); - }; - private submit = () => { const { isValid, onSubmit, value } = this.props; @@ -266,6 +272,16 @@ const withSuggestionsHidden = (state: AutocompleteFieldState) => ({ selectedIndex: null, }); +const withFocused = (state: AutocompleteFieldState) => ({ + ...state, + isFocused: true, +}); + +const withUnfocused = (state: AutocompleteFieldState) => ({ + ...state, + isFocused: false, +}); + const FixedEuiFieldSearch: React.SFC< React.InputHTMLAttributes & EuiFieldSearchProps & { diff --git a/x-pack/plugins/infra/public/containers/logs/with_log_filter.ts b/x-pack/plugins/infra/public/containers/logs/with_log_filter.tsx similarity index 57% rename from x-pack/plugins/infra/public/containers/logs/with_log_filter.ts rename to x-pack/plugins/infra/public/containers/logs/with_log_filter.tsx index 178389d473086..aff1eca5c0614 100644 --- a/x-pack/plugins/infra/public/containers/logs/with_log_filter.ts +++ b/x-pack/plugins/infra/public/containers/logs/with_log_filter.tsx @@ -4,13 +4,15 @@ * you may not use this file except in compliance with the Elastic License. */ +import React from 'react'; import { connect } from 'react-redux'; import { logFilterActions, logFilterSelectors, State } from '../../store'; import { asChildFunctionRenderer } from '../../utils/typed_react'; import { bindPlainActionCreators } from '../../utils/typed_redux'; +import { UrlStateContainer } from '../../utils/url_state'; -export const withLogFilter = connect( +const withLogFilter = connect( (state: State) => ({ filterQuery: logFilterSelectors.selectLogFilterQuery(state), filterQueryDraft: logFilterSelectors.selectLogFilterQueryDraft(state), @@ -33,3 +35,39 @@ export const withLogFilter = connect( ); export const WithLogFilter = asChildFunctionRenderer(withLogFilter); + +/** + * Url State + */ + +type LogFilterUrlState = ReturnType; + +export const WithLogFilterUrlState = () => ( + + {({ applyFilterQuery, filterQuery }) => ( + { + if (urlState) { + applyFilterQuery(urlState); + } + }} + onInitialize={urlState => { + if (urlState) { + applyFilterQuery(urlState); + } + }} + /> + )} + +); + +const mapToFilterQuery = (value: any): LogFilterUrlState | undefined => + value && value.kind === 'kuery' && typeof value.expression === 'string' + ? { + kind: value.kind, + expression: value.expression, + } + : undefined; diff --git a/x-pack/plugins/infra/public/containers/logs/with_log_minimap.ts b/x-pack/plugins/infra/public/containers/logs/with_log_minimap.ts deleted file mode 100644 index c28d1e9a075a8..0000000000000 --- a/x-pack/plugins/infra/public/containers/logs/with_log_minimap.ts +++ /dev/null @@ -1,50 +0,0 @@ -/* - * 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 { connect } from 'react-redux'; - -import { logMinimapActions, logMinimapSelectors, State } from '../../store'; -import { asChildFunctionRenderer } from '../../utils/typed_react'; -import { bindPlainActionCreators } from '../../utils/typed_redux'; - -export const withLogMinimap = connect( - (state: State) => ({ - availableIntervalSizes, - intervalSize: logMinimapSelectors.selectMinimapIntervalSize(state), - }), - bindPlainActionCreators({ - setIntervalSize: logMinimapActions.setMinimapIntervalSize, - }) -); - -export const WithLogMinimap = asChildFunctionRenderer(withLogMinimap); - -export const availableIntervalSizes = [ - { - label: '1 Year', - intervalSize: 1000 * 60 * 60 * 24 * 365, - }, - { - label: '1 Month', - intervalSize: 1000 * 60 * 60 * 24 * 30, - }, - { - label: '1 Week', - intervalSize: 1000 * 60 * 60 * 24 * 7, - }, - { - label: '1 Day', - intervalSize: 1000 * 60 * 60 * 24, - }, - { - label: '1 Hour', - intervalSize: 1000 * 60 * 60, - }, - { - label: '1 Minute', - intervalSize: 1000 * 60, - }, -]; diff --git a/x-pack/plugins/infra/public/containers/logs/with_log_minimap.tsx b/x-pack/plugins/infra/public/containers/logs/with_log_minimap.tsx new file mode 100644 index 0000000000000..a5ad500eaeb45 --- /dev/null +++ b/x-pack/plugins/infra/public/containers/logs/with_log_minimap.tsx @@ -0,0 +1,101 @@ +/* + * 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 { logMinimapActions, logMinimapSelectors, State } from '../../store'; +import { asChildFunctionRenderer } from '../../utils/typed_react'; +import { bindPlainActionCreators } from '../../utils/typed_redux'; +import { UrlStateContainer } from '../../utils/url_state'; + +export const withLogMinimap = connect( + (state: State) => ({ + availableIntervalSizes, + intervalSize: logMinimapSelectors.selectMinimapIntervalSize(state), + urlState: selectMinimapUrlState(state), + }), + bindPlainActionCreators({ + setIntervalSize: logMinimapActions.setMinimapIntervalSize, + }) +); + +export const WithLogMinimap = asChildFunctionRenderer(withLogMinimap); + +export const availableIntervalSizes = [ + { + label: '1 Year', + intervalSize: 1000 * 60 * 60 * 24 * 365, + }, + { + label: '1 Month', + intervalSize: 1000 * 60 * 60 * 24 * 30, + }, + { + label: '1 Week', + intervalSize: 1000 * 60 * 60 * 24 * 7, + }, + { + label: '1 Day', + intervalSize: 1000 * 60 * 60 * 24, + }, + { + label: '1 Hour', + intervalSize: 1000 * 60 * 60, + }, + { + label: '1 Minute', + intervalSize: 1000 * 60, + }, +]; + +/** + * Url State + */ + +interface LogMinimapUrlState { + intervalSize?: ReturnType; +} + +export const WithLogMinimapUrlState = () => ( + + {({ urlState, setIntervalSize }) => ( + { + if (newUrlState && newUrlState.intervalSize) { + setIntervalSize(newUrlState.intervalSize); + } + }} + onInitialize={newUrlState => { + if (newUrlState && newUrlState.intervalSize) { + setIntervalSize(newUrlState.intervalSize); + } + }} + /> + )} + +); + +const mapToUrlState = (value: any): LogMinimapUrlState | undefined => + value + ? { + intervalSize: mapToIntervalSizeUrlState(value.intervalSize), + } + : undefined; + +const mapToIntervalSizeUrlState = (value: any) => + value && typeof value === 'number' ? value : undefined; + +const selectMinimapUrlState = createSelector( + logMinimapSelectors.selectMinimapIntervalSize, + intervalSize => ({ + intervalSize, + }) +); diff --git a/x-pack/plugins/infra/public/containers/logs/with_log_position.ts b/x-pack/plugins/infra/public/containers/logs/with_log_position.ts deleted file mode 100644 index 9f7bcf85479ed..0000000000000 --- a/x-pack/plugins/infra/public/containers/logs/with_log_position.ts +++ /dev/null @@ -1,34 +0,0 @@ -/* - * 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 { connect } from 'react-redux'; - -import { logPositionActions, logPositionSelectors, State } from '../../store'; -import { asChildFunctionRenderer } from '../../utils/typed_react'; -import { bindPlainActionCreators } from '../../utils/typed_redux'; - -export const withLogPosition = connect( - (state: State) => ({ - firstVisiblePosition: logPositionSelectors.selectFirstVisiblePosition(state), - isAutoReloading: logPositionSelectors.selectIsAutoReloading(state), - lastVisiblePosition: logPositionSelectors.selectFirstVisiblePosition(state), - targetPosition: logPositionSelectors.selectTargetPosition(state), - visibleTimeInterval: logPositionSelectors.selectVisibleTimeInterval(state), - visibleMidpoint: logPositionSelectors.selectVisibleMidpointOrTargetTime(state), - }), - bindPlainActionCreators({ - jumpToTargetPosition: logPositionActions.jumpToTargetPosition, - jumpToTargetPositionTime: logPositionActions.jumpToTargetPositionTime, - reportVisiblePositions: logPositionActions.reportVisiblePositions, - reportVisibleSummary: logPositionActions.reportVisibleSummary, - startLiveStreaming: logPositionActions.startAutoReload, - stopLiveStreaming: logPositionActions.stopAutoReload, - }) -); - -export const WithLogPosition = asChildFunctionRenderer(withLogPosition, { - onInitialize: props => props.jumpToTargetPositionTime(Date.now()), -}); diff --git a/x-pack/plugins/infra/public/containers/logs/with_log_position.tsx b/x-pack/plugins/infra/public/containers/logs/with_log_position.tsx new file mode 100644 index 0000000000000..77345810e2abd --- /dev/null +++ b/x-pack/plugins/infra/public/containers/logs/with_log_position.tsx @@ -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 { connect } from 'react-redux'; +import { createSelector } from 'reselect'; + +import { pickTimeKey } from '../../../common/time'; +import { logPositionActions, logPositionSelectors, State } from '../../store'; +import { asChildFunctionRenderer } from '../../utils/typed_react'; +import { bindPlainActionCreators } from '../../utils/typed_redux'; +import { UrlStateContainer } from '../../utils/url_state'; + +export const withLogPosition = connect( + (state: State) => ({ + firstVisiblePosition: logPositionSelectors.selectFirstVisiblePosition(state), + isAutoReloading: logPositionSelectors.selectIsAutoReloading(state), + lastVisiblePosition: logPositionSelectors.selectFirstVisiblePosition(state), + targetPosition: logPositionSelectors.selectTargetPosition(state), + urlState: selectPositionUrlState(state), + visibleTimeInterval: logPositionSelectors.selectVisibleTimeInterval(state), + visibleMidpoint: logPositionSelectors.selectVisibleMidpointOrTarget(state), + visibleMidpointTime: logPositionSelectors.selectVisibleMidpointOrTargetTime(state), + }), + bindPlainActionCreators({ + jumpToTargetPosition: logPositionActions.jumpToTargetPosition, + jumpToTargetPositionTime: logPositionActions.jumpToTargetPositionTime, + reportVisiblePositions: logPositionActions.reportVisiblePositions, + reportVisibleSummary: logPositionActions.reportVisibleSummary, + startLiveStreaming: logPositionActions.startAutoReload, + stopLiveStreaming: logPositionActions.stopAutoReload, + }) +); + +export const WithLogPosition = asChildFunctionRenderer(withLogPosition); + +/** + * Url State + */ + +interface LogPositionUrlState { + position?: ReturnType; + streamLive?: ReturnType; +} + +export const WithLogPositionUrlState = () => ( + + {({ + jumpToTargetPosition, + jumpToTargetPositionTime, + startLiveStreaming, + stopLiveStreaming, + urlState, + }) => ( + { + if (newUrlState && newUrlState.position) { + jumpToTargetPosition(newUrlState.position); + } + if (newUrlState && newUrlState.streamLive) { + startLiveStreaming(5000); + } else if ( + newUrlState && + typeof newUrlState.streamLive !== 'undefined' && + !newUrlState.streamLive + ) { + stopLiveStreaming(); + } + }} + onInitialize={initialUrlState => { + if (initialUrlState && initialUrlState.position) { + jumpToTargetPosition(initialUrlState.position); + } else { + jumpToTargetPositionTime(Date.now()); + } + if (initialUrlState && initialUrlState.streamLive) { + startLiveStreaming(5000); + } + }} + /> + )} + +); + +const selectPositionUrlState = createSelector( + logPositionSelectors.selectVisibleMidpointOrTarget, + logPositionSelectors.selectIsAutoReloading, + (position, streamLive) => ({ + position: position ? pickTimeKey(position) : null, + streamLive, + }) +); + +const mapToUrlState = (value: any): LogPositionUrlState | undefined => + value + ? { + position: mapToPositionUrlState(value.position), + streamLive: mapToStreamLiveUrlState(value.streamLive), + } + : undefined; + +const mapToPositionUrlState = (value: any) => + value && (typeof value.time === 'number' && typeof value.tiebreaker === 'number') + ? pickTimeKey(value) + : undefined; + +const mapToStreamLiveUrlState = (value: any) => (typeof value === 'boolean' ? value : undefined); diff --git a/x-pack/plugins/infra/public/containers/logs/with_log_textview.tsx b/x-pack/plugins/infra/public/containers/logs/with_log_textview.tsx new file mode 100644 index 0000000000000..5d22bc481b783 --- /dev/null +++ b/x-pack/plugins/infra/public/containers/logs/with_log_textview.tsx @@ -0,0 +1,91 @@ +/* + * 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 { TextScale } from '../../../common/log_text_scale'; +import { logTextviewActions, logTextviewSelectors, State } from '../../store'; +import { asChildFunctionRenderer } from '../../utils/typed_react'; +import { bindPlainActionCreators } from '../../utils/typed_redux'; +import { UrlStateContainer } from '../../utils/url_state'; + +const availableTextScales = ['large', 'medium', 'small'] as TextScale[]; + +export const withLogTextview = connect( + (state: State) => ({ + availableTextScales, + textScale: logTextviewSelectors.selectTextviewScale(state), + urlState: selectTextviewUrlState(state), + wrap: logTextviewSelectors.selectTextviewWrap(state), + }), + bindPlainActionCreators({ + setTextScale: logTextviewActions.setTextviewScale, + setTextWrap: logTextviewActions.setTextviewWrap, + }) +); + +export const WithLogTextview = asChildFunctionRenderer(withLogTextview); + +/** + * Url State + */ + +interface LogTextviewUrlState { + textScale?: ReturnType; + wrap?: ReturnType; +} + +export const WithLogTextviewUrlState = () => ( + + {({ urlState, setTextScale, setTextWrap }) => ( + { + if (newUrlState && newUrlState.textScale) { + setTextScale(newUrlState.textScale); + } + if (newUrlState && typeof newUrlState.wrap !== 'undefined') { + setTextWrap(newUrlState.wrap); + } + }} + onInitialize={newUrlState => { + if (newUrlState && newUrlState.textScale) { + setTextScale(newUrlState.textScale); + } + if (newUrlState && typeof newUrlState.wrap !== 'undefined') { + setTextWrap(newUrlState.wrap); + } + }} + /> + )} + +); + +const mapToUrlState = (value: any): LogTextviewUrlState | undefined => + value + ? { + textScale: mapToTextScaleUrlState(value.textScale), + wrap: mapToWrapUrlState(value.wrap), + } + : undefined; + +const mapToTextScaleUrlState = (value: any) => + availableTextScales.includes(value) ? (value as TextScale) : undefined; + +const mapToWrapUrlState = (value: any) => (typeof value === 'boolean' ? value : undefined); + +const selectTextviewUrlState = createSelector( + logTextviewSelectors.selectTextviewScale, + logTextviewSelectors.selectTextviewWrap, + (textScale, wrap) => ({ + textScale, + wrap, + }) +); diff --git a/x-pack/plugins/infra/public/containers/logs/with_text_scale_controls_props.ts b/x-pack/plugins/infra/public/containers/logs/with_text_scale_controls_props.ts deleted file mode 100644 index a468e2f4a536e..0000000000000 --- a/x-pack/plugins/infra/public/containers/logs/with_text_scale_controls_props.ts +++ /dev/null @@ -1,24 +0,0 @@ -/* - * 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 { connect } from 'react-redux'; - -import { TextScale } from '../../../common/log_text_scale'; -import { logTextviewActions, logTextviewSelectors, State } from '../../store'; -import { asChildFunctionRenderer } from '../../utils/typed_react'; -import { bindPlainActionCreators } from '../../utils/typed_redux'; - -export const withTextScale = connect( - (state: State) => ({ - availableTextScales: ['large', 'medium', 'small'] as TextScale[], - textScale: logTextviewSelectors.selectTextviewScale(state), - }), - bindPlainActionCreators({ - setTextScale: logTextviewActions.setTextviewScale, - }) -); - -export const WithTextScale = asChildFunctionRenderer(withTextScale); diff --git a/x-pack/plugins/infra/public/containers/logs/with_text_wrap_controls_props.ts b/x-pack/plugins/infra/public/containers/logs/with_text_wrap_controls_props.ts deleted file mode 100644 index c5a0d701c12e8..0000000000000 --- a/x-pack/plugins/infra/public/containers/logs/with_text_wrap_controls_props.ts +++ /dev/null @@ -1,22 +0,0 @@ -/* - * 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 { connect } from 'react-redux'; - -import { logTextviewActions, logTextviewSelectors, State } from '../../store'; -import { asChildFunctionRenderer } from '../../utils/typed_react'; -import { bindPlainActionCreators } from '../../utils/typed_redux'; - -export const withTextWrap = connect( - (state: State) => ({ - wrap: logTextviewSelectors.selectTextviewWrap(state), - }), - bindPlainActionCreators({ - setTextWrap: logTextviewActions.setTextviewWrap, - }) -); - -export const WithTextWrap = asChildFunctionRenderer(withTextWrap); diff --git a/x-pack/plugins/infra/public/containers/waffle/with_waffle_filters.ts b/x-pack/plugins/infra/public/containers/waffle/with_waffle_filters.tsx similarity index 60% rename from x-pack/plugins/infra/public/containers/waffle/with_waffle_filters.ts rename to x-pack/plugins/infra/public/containers/waffle/with_waffle_filters.tsx index 2d0c8361dea36..80f912574c72b 100644 --- a/x-pack/plugins/infra/public/containers/waffle/with_waffle_filters.ts +++ b/x-pack/plugins/infra/public/containers/waffle/with_waffle_filters.tsx @@ -4,11 +4,13 @@ * you may not use this file except in compliance with the Elastic License. */ +import React from 'react'; import { connect } from 'react-redux'; import { sharedSelectors, State, waffleFilterActions, waffleFilterSelectors } from '../../store'; import { asChildFunctionRenderer } from '../../utils/typed_react'; import { bindPlainActionCreators } from '../../utils/typed_redux'; +import { UrlStateContainer } from '../../utils/url_state'; export const withWaffleFilter = connect( (state: State) => ({ @@ -34,3 +36,39 @@ export const withWaffleFilter = connect( ); export const WithWaffleFilter = asChildFunctionRenderer(withWaffleFilter); + +/** + * Url State + */ + +type WaffleFilterUrlState = ReturnType; + +export const WithWaffleFilterUrlState = () => ( + + {({ applyFilterQuery, filterQuery }) => ( + { + if (urlState) { + applyFilterQuery(urlState); + } + }} + onInitialize={urlState => { + if (urlState) { + applyFilterQuery(urlState); + } + }} + /> + )} + +); + +const mapToUrlState = (value: any): WaffleFilterUrlState | undefined => + value && value.kind === 'kuery' && typeof value.expression === 'string' + ? { + kind: value.kind, + expression: value.expression, + } + : undefined; diff --git a/x-pack/plugins/infra/public/containers/waffle/with_waffle_time.ts b/x-pack/plugins/infra/public/containers/waffle/with_waffle_time.ts deleted file mode 100644 index 5811377316744..0000000000000 --- a/x-pack/plugins/infra/public/containers/waffle/with_waffle_time.ts +++ /dev/null @@ -1,26 +0,0 @@ -/* - * 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 { connect } from 'react-redux'; - -import { State, waffleTimeActions, waffleTimeSelectors } from '../../store'; -import { asChildFunctionRenderer } from '../../utils/typed_react'; -import { bindPlainActionCreators } from '../../utils/typed_redux'; - -export const withWaffleTime = connect( - (state: State) => ({ - currentTime: waffleTimeSelectors.selectCurrentTime(state), - currentTimeRange: waffleTimeSelectors.selectCurrentTimeRange(state), - isAutoReloading: waffleTimeSelectors.selectIsAutoReloading(state), - }), - bindPlainActionCreators({ - jumpToTime: waffleTimeActions.jumpToTime, - startAutoReload: waffleTimeActions.startAutoReload, - stopAutoReload: waffleTimeActions.stopAutoReload, - }) -); - -export const WithWaffleTime = asChildFunctionRenderer(withWaffleTime); diff --git a/x-pack/plugins/infra/public/containers/waffle/with_waffle_time.tsx b/x-pack/plugins/infra/public/containers/waffle/with_waffle_time.tsx new file mode 100644 index 0000000000000..9158c16e355f8 --- /dev/null +++ b/x-pack/plugins/infra/public/containers/waffle/with_waffle_time.tsx @@ -0,0 +1,94 @@ +/* + * 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 { State, waffleTimeActions, waffleTimeSelectors } from '../../store'; +import { asChildFunctionRenderer } from '../../utils/typed_react'; +import { bindPlainActionCreators } from '../../utils/typed_redux'; +import { UrlStateContainer } from '../../utils/url_state'; + +export const withWaffleTime = connect( + (state: State) => ({ + currentTime: waffleTimeSelectors.selectCurrentTime(state), + currentTimeRange: waffleTimeSelectors.selectCurrentTimeRange(state), + isAutoReloading: waffleTimeSelectors.selectIsAutoReloading(state), + urlState: selectTimeUrlState(state), + }), + bindPlainActionCreators({ + jumpToTime: waffleTimeActions.jumpToTime, + startAutoReload: waffleTimeActions.startAutoReload, + stopAutoReload: waffleTimeActions.stopAutoReload, + }) +); + +export const WithWaffleTime = asChildFunctionRenderer(withWaffleTime); + +/** + * Url State + */ + +interface WaffleTimeUrlState { + time?: ReturnType; + autoReload?: ReturnType; +} + +export const WithWaffleTimeUrlState = () => ( + + {({ jumpToTime, startAutoReload, stopAutoReload, urlState }) => ( + { + if (newUrlState && newUrlState.time) { + jumpToTime(newUrlState.time); + } + if (newUrlState && newUrlState.autoReload) { + startAutoReload(); + } else if ( + newUrlState && + typeof newUrlState.autoReload !== 'undefined' && + !newUrlState.autoReload + ) { + stopAutoReload(); + } + }} + onInitialize={initialUrlState => { + if (initialUrlState) { + jumpToTime(initialUrlState.time ? initialUrlState.time : Date.now()); + } + if (initialUrlState && initialUrlState.autoReload) { + startAutoReload(); + } + }} + /> + )} + +); + +const selectTimeUrlState = createSelector( + waffleTimeSelectors.selectCurrentTime, + waffleTimeSelectors.selectIsAutoReloading, + (time, autoReload) => ({ + time, + autoReload, + }) +); + +const mapToUrlState = (value: any): WaffleTimeUrlState | undefined => + value + ? { + time: mapToTimeUrlState(value.time), + autoReload: mapToAutoReloadUrlState(value.autoReload), + } + : undefined; + +const mapToTimeUrlState = (value: any) => (value && typeof value === 'number' ? value : undefined); + +const mapToAutoReloadUrlState = (value: any) => (typeof value === 'boolean' ? value : undefined); diff --git a/x-pack/plugins/infra/public/lib/adapters/framework/kibana_framework_adapter.ts b/x-pack/plugins/infra/public/lib/adapters/framework/kibana_framework_adapter.ts index 47cad18de6679..2d9bab756c104 100644 --- a/x-pack/plugins/infra/public/lib/adapters/framework/kibana_framework_adapter.ts +++ b/x-pack/plugins/infra/public/lib/adapters/framework/kibana_framework_adapter.ts @@ -138,6 +138,7 @@ export class InfraKibanaFrameworkAdapter implements InfraFrameworkAdapter { uiRoutes.enable(); uiRoutes.otherwise({ + reloadOnSearch: false, template: '', }); diff --git a/x-pack/plugins/infra/public/pages/home.tsx b/x-pack/plugins/infra/public/pages/home.tsx index 2eb56976b7cde..d9269969bd331 100644 --- a/x-pack/plugins/infra/public/pages/home.tsx +++ b/x-pack/plugins/infra/public/pages/home.tsx @@ -14,9 +14,12 @@ import { ColumnarPage, PageContent } from '../components/page'; import { Waffle } from '../components/waffle'; import { WaffleTimeControls } from '../components/waffle/waffle_time_controls'; -import { WithWaffleFilter } from '../containers/waffle/with_waffle_filters'; +import { + WithWaffleFilter, + WithWaffleFilterUrlState, +} from '../containers/waffle/with_waffle_filters'; import { WithWaffleNodes } from '../containers/waffle/with_waffle_nodes'; -import { WithWaffleTime } from '../containers/waffle/with_waffle_time'; +import { WithWaffleTime, WithWaffleTimeUrlState } from '../containers/waffle/with_waffle_time'; import { WithKueryAutocompletion } from '../containers/with_kuery_autocompletion'; import { WithOptions } from '../containers/with_options'; @@ -24,6 +27,8 @@ export class HomePage extends React.PureComponent { public render() { return ( + +
diff --git a/x-pack/plugins/infra/public/pages/logs/logs.tsx b/x-pack/plugins/infra/public/pages/logs/logs.tsx index 1f78e043e19c1..a745089b8a6fc 100644 --- a/x-pack/plugins/infra/public/pages/logs/logs.tsx +++ b/x-pack/plugins/infra/public/pages/logs/logs.tsx @@ -20,19 +20,22 @@ import { ScrollableLogTextStreamView } from '../../components/logging/log_text_s import { LogTextWrapControls } from '../../components/logging/log_text_wrap_controls'; import { LogTimeControls } from '../../components/logging/log_time_controls'; import { ColumnarPage, PageContent } from '../../components/page'; -import { WithLogFilter } from '../../containers/logs/with_log_filter'; -import { WithLogMinimap } from '../../containers/logs/with_log_minimap'; -import { WithLogPosition } from '../../containers/logs/with_log_position'; +import { WithLogFilter, WithLogFilterUrlState } from '../../containers/logs/with_log_filter'; +import { WithLogMinimap, WithLogMinimapUrlState } from '../../containers/logs/with_log_minimap'; +import { WithLogPosition, WithLogPositionUrlState } from '../../containers/logs/with_log_position'; +import { WithLogTextview, WithLogTextviewUrlState } from '../../containers/logs/with_log_textview'; import { WithStreamItems } from '../../containers/logs/with_stream_items'; import { WithSummary } from '../../containers/logs/with_summary'; -import { WithTextScale } from '../../containers/logs/with_text_scale_controls_props'; -import { WithTextWrap } from '../../containers/logs/with_text_wrap_controls_props'; import { WithKueryAutocompletion } from '../../containers/with_kuery_autocompletion'; export class LogsPage extends React.Component { public render() { return ( + + + +
@@ -42,6 +45,7 @@ export class LogsPage extends React.Component { {({ applyFilterQueryFromKueryExpression, + filterQuery, filterQueryDraft, isFilterQueryDraftValid, setFilterQueryDraftFromKueryExpression, @@ -72,33 +76,31 @@ export class LogsPage extends React.Component { /> )} - - {({ wrap, setTextWrap }) => ( - - )} - - - {({ availableTextScales, textScale, setTextScale }) => ( - + + {({ availableTextScales, textScale, setTextScale, setTextWrap, wrap }) => ( + <> + + + )} - + {({ - visibleMidpoint, + visibleMidpointTime, isAutoReloading, jumpToTargetPositionTime, startLiveStreaming, stopLiveStreaming, }) => ( {({ measureRef, content: { width = 0, height = 0 } }) => ( - - - {({ textScale }) => ( - - {({ wrap }) => ( - + + + {({ textScale, wrap }) => ( + + {({ + isAutoReloading, + jumpToTargetPosition, + reportVisiblePositions, + targetPosition, + }) => ( + {({ - isAutoReloading, - jumpToTargetPosition, - reportVisiblePositions, - targetPosition, + hasMoreAfterEnd, + hasMoreBeforeStart, + isLoadingMore, + isReloading, + items, + lastLoadedTime, }) => ( - - {({ - hasMoreAfterEnd, - hasMoreBeforeStart, - isLoadingMore, - isReloading, - items, - lastLoadedTime, - }) => ( - - )} - + )} - + )} - + )} - + )} {({ measureRef, content: { width = 0, height = 0 } }) => { return ( - + {({ intervalSize }) => ( @@ -172,7 +170,7 @@ export class LogsPage extends React.Component { {({ jumpToTargetPosition, reportVisibleSummary, - visibleMidpoint, + visibleMidpointTime, visibleTimeInterval, }) => ( )} diff --git a/x-pack/plugins/infra/public/utils/url_state.tsx b/x-pack/plugins/infra/public/utils/url_state.tsx new file mode 100644 index 0000000000000..418aecd332207 --- /dev/null +++ b/x-pack/plugins/infra/public/utils/url_state.tsx @@ -0,0 +1,152 @@ +/* + * 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 { History, Location } from 'history'; +import throttle from 'lodash/fp/throttle'; +import { parse as parseQueryString, stringify as stringifyQueryString } from 'querystring'; +import React from 'react'; +import { Route, RouteProps } from 'react-router'; +import { decode, encode, RisonValue } from 'rison-node'; + +interface UrlStateContainerProps { + urlState: UrlState | undefined; + urlStateKey: string; + mapToUrlState?: (value: any) => UrlState | undefined; + onChange?: (urlState: UrlState, previousUrlState: UrlState | undefined) => void; + onInitialize?: (urlState: UrlState | undefined) => void; +} + +interface UrlStateContainerLifecycleProps extends UrlStateContainerProps { + location: Location; + history: History; +} + +class UrlStateContainerLifecycle extends React.Component< + UrlStateContainerLifecycleProps +> { + public render() { + return null; + } + + public componentDidUpdate({ + location: prevLocation, + urlState: prevUrlState, + }: UrlStateContainerLifecycleProps) { + const { history, location, urlState } = this.props; + + if (urlState !== prevUrlState) { + this.replaceStateInLocation(urlState); + } + + if (history.action === 'POP' && location !== prevLocation) { + this.handleLocationChange(prevLocation, location); + } + } + + public componentDidMount() { + const { location } = this.props; + + this.handleInitialize(location); + } + + // tslint:disable-next-line:member-ordering this is really a method despite what tslint thinks + private replaceStateInLocation = throttle(1000, (urlState: UrlState | undefined) => { + const { history, location, urlStateKey } = this.props; + + const newLocation = updateLocationWithUrlState(urlStateKey, urlState, this.props.location); + + if (newLocation !== location) { + history.replace(newLocation); + } + }); + + private handleInitialize = (location: Location) => { + const { onInitialize, mapToUrlState, urlStateKey } = this.props; + + if (!onInitialize || !mapToUrlState) { + return; + } + + const newQueryValue = parseLocation(location)[urlStateKey]; + const newUrlStateString = Array.isArray(newQueryValue) ? newQueryValue[0] : newQueryValue; + const newUrlState = mapToUrlState(decodeRisonUrlState(newUrlStateString)); + + onInitialize(newUrlState); + }; + + private handleLocationChange = (prevLocation: Location, newLocation: Location) => { + const { onChange, mapToUrlState, urlStateKey } = this.props; + + if (!onChange || !mapToUrlState) { + return; + } + + const previousQueryValue = parseLocation(prevLocation)[urlStateKey]; + const newQueryValue = parseLocation(newLocation)[urlStateKey]; + + const previousUrlStateString = Array.isArray(previousQueryValue) + ? previousQueryValue[0] + : previousQueryValue; + const newUrlStateString = Array.isArray(newQueryValue) ? newQueryValue[0] : newQueryValue; + + if (previousUrlStateString !== newUrlStateString) { + const previousUrlState = mapToUrlState(decodeRisonUrlState(previousUrlStateString)); + const newUrlState = mapToUrlState(decodeRisonUrlState(newUrlStateString)); + + if (typeof newUrlState !== 'undefined') { + onChange(newUrlState, previousUrlState); + } + } + }; +} + +export const UrlStateContainer = ( + props: UrlStateContainerProps +) => ( + > + {({ history, location }) => ( + history={history} location={location} {...props} /> + )} + +); + +const decodeRisonUrlState = (value: string): RisonValue | undefined => { + try { + return value ? decode(value) : undefined; + } catch (error) { + if (error instanceof Error && error.message.startsWith('rison decoder error')) { + return {}; + } + throw error; + } +}; + +const encodeRisonUrlState = (state: any) => encode(state); + +const parseLocation = (location: Location) => parseQueryString(location.search.substring(1)); + +const updateLocationWithUrlState = ( + stateKey: string, + urlState: UrlState | undefined, + location: Location +): Location => { + const previousQueryValues = parseLocation(location); + const encodedUrlState = + typeof urlState !== 'undefined' ? encodeRisonUrlState(urlState) : undefined; + const newQueryString = stringifyQueryString({ + ...previousQueryValues, + [stateKey]: encodedUrlState, + }); + + if (newQueryString === location.search.substring(1)) { + return location; + } else { + return { + ...location, + search: `?${newQueryString}`, + }; + } +}; diff --git a/x-pack/plugins/infra/types/rison_node.d.ts b/x-pack/plugins/infra/types/rison_node.d.ts index 92458b5113c25..5448c4bcd74a9 100644 --- a/x-pack/plugins/infra/types/rison_node.d.ts +++ b/x-pack/plugins/infra/types/rison_node.d.ts @@ -6,11 +6,19 @@ // tslint:disable:variable-name declare module 'rison-node' { - export const decode_object: ( - input: string - ) => ResultObject; + export type RisonValue = null | boolean | number | string | RisonObject | RisonArray; - export const encode_object: ( - input: InputObject - ) => string; + export interface RisonArray extends Array {} + + export interface RisonObject { + [key: string]: RisonValue; + } + + export const decode: (input: string) => RisonValue; + + export const decode_object: (input: string) => RisonObject; + + export const encode: (input: Input) => string; + + export const encode_object: (input: Input) => string; }