From bee57a143eb1781a4f882b8322a8db3677ee94d4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felix=20St=C3=BCrmer?= Date: Tue, 3 Jul 2018 21:13:38 +0200 Subject: [PATCH 1/3] Replace jump menu with EUI datepicker This replaces the "Jump to time" menu with the EUI datepicker. It also tweaks the design of the streaming toggle. The date and time formats of the datepicker are depend on the current browser locale via the corresponding Moment.js format string tokens. --- x-pack/package.json | 1 + .../components/logging/log_jump_menu.tsx | 113 ------------------ .../logging/log_live_stream_controls.tsx | 57 --------- .../components/logging/log_time_controls.tsx | 85 +++++++++++++ .../logging_legacy/with_jump_menu_props.ts | 17 --- ...s_props.ts => with_time_controls_props.ts} | 12 +- .../plugins/infra/public/pages/logs/logs.tsx | 32 ++--- x-pack/plugins/infra/types/eui.d.ts | 56 ++++++++- x-pack/yarn.lock | 10 +- 9 files changed, 176 insertions(+), 207 deletions(-) delete mode 100644 x-pack/plugins/infra/public/components/logging/log_jump_menu.tsx delete mode 100644 x-pack/plugins/infra/public/components/logging/log_live_stream_controls.tsx create mode 100644 x-pack/plugins/infra/public/components/logging/log_time_controls.tsx delete mode 100644 x-pack/plugins/infra/public/containers/logging_legacy/with_jump_menu_props.ts rename x-pack/plugins/infra/public/containers/logging_legacy/{with_live_stream_controls_props.ts => with_time_controls_props.ts} (72%) diff --git a/x-pack/package.json b/x-pack/package.json index 7f0b0a7a57e4d..5aede69e465f7 100644 --- a/x-pack/package.json +++ b/x-pack/package.json @@ -44,6 +44,7 @@ "@types/pngjs": "^3.3.1", "@types/prop-types": "^15.5.3", "@types/react": "^16.3.14", + "@types/react-datepicker": "^1.1.5", "@types/react-dom": "^16.0.5", "@types/react-redux": "^6.0.2", "@types/react-router-dom": "^4.2.6", diff --git a/x-pack/plugins/infra/public/components/logging/log_jump_menu.tsx b/x-pack/plugins/infra/public/components/logging/log_jump_menu.tsx deleted file mode 100644 index 2b630d0936c70..0000000000000 --- a/x-pack/plugins/infra/public/components/logging/log_jump_menu.tsx +++ /dev/null @@ -1,113 +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 { EuiButtonEmpty, EuiContextMenu, EuiPopover } from '@elastic/eui'; -import { timeDay, timeHour, timeMonth, timeWeek, timeYear } from 'd3-time'; -import * as React from 'react'; - -interface LogJumpMenuProps { - jumpToTime: (time: number) => any; -} - -interface LogJumpMenuState { - isShown: boolean; -} - -export class LogJumpMenu extends React.Component { - public readonly state = { - isShown: false, - }; - - public show = () => { - this.setState({ - isShown: true, - }); - }; - - public hide = () => { - this.setState({ - isShown: false, - }); - }; - - public toggleVisibility = () => { - this.setState(state => ({ - isShown: !state.isShown, - })); - }; - - public jumpToTime = (time: number) => { - this.hide(); - this.props.jumpToTime(time); - }; - - public getPanels = () => [ - { - id: 'jumpToPredefinedTargets', - items: [ - { - name: 'Now', - onClick: () => this.jumpToTime(Date.now()), - }, - { - name: 'Previous Hour', - onClick: () => this.jumpToTime(timeHour.floor(new Date()).getTime()), - }, - { - name: 'Previous Day', - onClick: () => this.jumpToTime(timeDay.floor(new Date()).getTime()), - }, - { - name: 'Previous Week', - onClick: () => this.jumpToTime(timeWeek.floor(new Date()).getTime()), - }, - { - name: 'Previous Month', - onClick: () => this.jumpToTime(timeMonth.floor(new Date()).getTime()), - }, - { - name: 'Previous Year', - onClick: () => this.jumpToTime(timeYear.floor(new Date()).getTime()), - }, - { - name: 'Custom Time', - panel: 'jumpToCustomTarget', - }, - ], - title: 'Jump to...', - }, - { - content:
form goes here
, - id: 'jumpToCustomTarget', - title: 'Custom time', - }, - ]; - - public render() { - const { isShown } = this.state; - - const menuButton = ( - - Jump to time - - ); - - return ( - - - - ); - } -} diff --git a/x-pack/plugins/infra/public/components/logging/log_live_stream_controls.tsx b/x-pack/plugins/infra/public/components/logging/log_live_stream_controls.tsx deleted file mode 100644 index 9b603595fa35b..0000000000000 --- a/x-pack/plugins/infra/public/components/logging/log_live_stream_controls.tsx +++ /dev/null @@ -1,57 +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 { EuiButton } from '@elastic/eui'; -import * as React from 'react'; - -interface LogLiveStreamControlsProps { - className?: string; - isLiveStreaming: boolean; - enableLiveStreaming: () => any; - disableLiveStreaming: () => any; -} - -export class LogLiveStreamControls extends React.Component { - public startStreaming = () => { - this.props.enableLiveStreaming(); - }; - - public stopStreaming = () => { - this.props.disableLiveStreaming(); - }; - - public render() { - const { className, isLiveStreaming } = this.props; - - if (isLiveStreaming) { - return ( - - Stop streaming - - ); - } else { - return ( - - Stream live - - ); - } - } -} diff --git a/x-pack/plugins/infra/public/components/logging/log_time_controls.tsx b/x-pack/plugins/infra/public/components/logging/log_time_controls.tsx new file mode 100644 index 0000000000000..4c0a655567884 --- /dev/null +++ b/x-pack/plugins/infra/public/components/logging/log_time_controls.tsx @@ -0,0 +1,85 @@ +/* + * 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 { EuiDatePicker, EuiFilterButton, EuiFilterGroup } from '@elastic/eui'; +import moment, { Moment } from 'moment'; +import React from 'react'; +import styled from 'styled-components'; + +const noop = () => undefined; + +interface LogTimeControlsProps { + currentTime: number; + disableLiveStreaming: () => any; + enableLiveStreaming: () => any; + isLiveStreaming: boolean; + jumpToTime: (time: number) => any; +} + +export class LogTimeControls extends React.PureComponent { + public render() { + const { + currentTime, + disableLiveStreaming, + enableLiveStreaming, + isLiveStreaming, + } = this.props; + + const currentMoment = moment(currentTime); + + if (isLiveStreaming) { + return ( + + + + + + Stop streaming + + + ); + } else { + return ( + + + + + + Stream live + + + ); + } + } + + private handleChangeDate = (date: Moment | null) => { + if (date !== null) { + this.props.jumpToTime(date.valueOf()); + } + }; +} + +const InlineWrapper = styled.div` + display: inline-block; +`; diff --git a/x-pack/plugins/infra/public/containers/logging_legacy/with_jump_menu_props.ts b/x-pack/plugins/infra/public/containers/logging_legacy/with_jump_menu_props.ts deleted file mode 100644 index c81015f1ee0ed..0000000000000 --- a/x-pack/plugins/infra/public/containers/logging_legacy/with_jump_menu_props.ts +++ /dev/null @@ -1,17 +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 { bindPlainActionCreators } from '../../utils/typed_redux'; -import { targetActions } from './state'; - -export const withJumpMenuProps = connect( - () => ({}), - bindPlainActionCreators({ - jumpToTime: targetActions.jumpToTime, - }) -); diff --git a/x-pack/plugins/infra/public/containers/logging_legacy/with_live_stream_controls_props.ts b/x-pack/plugins/infra/public/containers/logging_legacy/with_time_controls_props.ts similarity index 72% rename from x-pack/plugins/infra/public/containers/logging_legacy/with_live_stream_controls_props.ts rename to x-pack/plugins/infra/public/containers/logging_legacy/with_time_controls_props.ts index ddee4f1f311cf..96a9ac20a0ade 100644 --- a/x-pack/plugins/infra/public/containers/logging_legacy/with_live_stream_controls_props.ts +++ b/x-pack/plugins/infra/public/containers/logging_legacy/with_time_controls_props.ts @@ -8,10 +8,17 @@ import { connect } from 'react-redux'; import { isIntervalLoadingPolicy } from '../../utils/loading_state'; import { bindPlainActionCreators } from '../../utils/typed_redux'; -import { entriesActions, entriesSelectors, State } from './state'; +import { + entriesActions, + entriesSelectors, + sharedSelectors, + State, + targetActions, +} from './state'; -export const withLiveStreamControlsProps = connect( +export const withTimeControlsProps = connect( (state: State) => ({ + currentTime: sharedSelectors.selectVisibleMidpointOrTargetTime(state), isLiveStreaming: isIntervalLoadingPolicy( entriesSelectors.selectEntriesEndLoadingState(state).policy ), @@ -19,5 +26,6 @@ export const withLiveStreamControlsProps = connect( bindPlainActionCreators({ disableLiveStreaming: entriesActions.stopLiveStreaming, enableLiveStreaming: entriesActions.startLiveStreaming, + jumpToTime: targetActions.jumpToTime, }) ); diff --git a/x-pack/plugins/infra/public/pages/logs/logs.tsx b/x-pack/plugins/infra/public/pages/logs/logs.tsx index c67effc6e1c2b..3b599b1def948 100644 --- a/x-pack/plugins/infra/public/pages/logs/logs.tsx +++ b/x-pack/plugins/infra/public/pages/logs/logs.tsx @@ -19,8 +19,6 @@ import { AutoSizer } from '../../components/auto_sizer'; import { Toolbar } from '../../components/eui'; import { Header } from '../../components/header'; import { LogCustomizationMenu } from '../../components/logging/log_customization_menu'; -import { LogJumpMenu } from '../../components/logging/log_jump_menu'; -import { LogLiveStreamControls } from '../../components/logging/log_live_stream_controls'; import { LogMinimap } from '../../components/logging/log_minimap'; import { LogMinimapScaleControls } from '../../components/logging/log_minimap_scale_controls'; import { LogPositionText } from '../../components/logging/log_position_text'; @@ -29,30 +27,35 @@ import { LogStatusbar, LogStatusbarItem } from '../../components/logging/log_sta import { LogTextScaleControls } from '../../components/logging/log_text_scale_controls'; import { ScrollableLogTextStreamView } from '../../components/logging/log_text_stream'; import { LogTextWrapControls } from '../../components/logging/log_text_wrap_controls'; +import { LogTimeControls } from '../../components/logging/log_time_controls'; import { withLibs } from '../../containers/libs'; import { State, targetActions } from '../../containers/logging_legacy/state'; -import { withJumpMenuProps } from '../../containers/logging_legacy/with_jump_menu_props'; -import { withLiveStreamControlsProps } from '../../containers/logging_legacy/with_live_stream_controls_props'; import { withLogSearchControlsProps } from '../../containers/logging_legacy/with_log_search_controls_props'; import { withMinimapProps } from '../../containers/logging_legacy/with_minimap_props'; import { withMinimapScaleControlsProps } from '../../containers/logging_legacy/with_minimap_scale_controls_props'; import { withStreamItems } from '../../containers/logging_legacy/with_stream_items'; import { withTextScaleControlsProps } from '../../containers/logging_legacy/with_text_scale_controls_props'; import { withTextWrapControlsProps } from '../../containers/logging_legacy/with_text_wrap_controls_props'; +import { withTimeControlsProps } from '../../containers/logging_legacy/with_time_controls_props'; import { withVisibleLogEntries } from '../../containers/logging_legacy/with_visible_log_entries'; -// TODO: split out containers - -const ConnectedLogJumpMenu = withJumpMenuProps(LogJumpMenu); -const ConnectedLogLiveStreamControls = withLiveStreamControlsProps(LogLiveStreamControls); const ConnectedLogMinimap = withMinimapProps(LogMinimap); const ConnectedLogMinimapScaleControls = withMinimapScaleControlsProps(LogMinimapScaleControls); const ConnectedLogPositionText = withVisibleLogEntries(LogPositionText); -const ConnectedLogSearchControls = withLogSearchControlsProps(LogSearchControls); -const ConnectedLogTextScaleControls = withTextScaleControlsProps(LogTextScaleControls); -const ConnectedLogTextWrapControls = withTextWrapControlsProps(LogTextWrapControls); -const ConnectedScrollableLogTextStreamView = withStreamItems(ScrollableLogTextStreamView); +const ConnectedLogSearchControls = withLogSearchControlsProps( + LogSearchControls +); +const ConnectedLogTextScaleControls = withTextScaleControlsProps( + LogTextScaleControls +); +const ConnectedLogTextWrapControls = withTextWrapControlsProps( + LogTextWrapControls +); +const ConnectedTimeControls = withTimeControlsProps(LogTimeControls); +const ConnectedScrollableLogTextStreamView = withStreamItems( + ScrollableLogTextStreamView +); interface LogsPageProps { libs: InfraFrontendLibs; @@ -101,9 +104,6 @@ export const LogsPage = withLibs( - - - @@ -112,7 +112,7 @@ export const LogsPage = withLibs( - + diff --git a/x-pack/plugins/infra/types/eui.d.ts b/x-pack/plugins/infra/types/eui.d.ts index 0977fdeabd995..f972c548de1f7 100644 --- a/x-pack/plugins/infra/types/eui.d.ts +++ b/x-pack/plugins/infra/types/eui.d.ts @@ -10,7 +10,15 @@ */ declare module '@elastic/eui' { - import { SFC } from 'react'; + import { Moment } from 'moment'; + import { + ChangeEventHandler, + MouseEventHandler, + ReactType, + Ref, + SFC, + } from 'react'; + import { ReactDatePickerProps } from 'react-datepicker'; export interface EuiBreadcrumbDefinition { text: React.ReactNode; @@ -35,4 +43,50 @@ declare module '@elastic/eui' { type EuiHeaderBreadcrumbsProps = EuiBreadcrumbsProps; export const EuiHeaderBreadcrumbs: React.SFC; + + type EuiDatePickerProps = CommonProps & + Pick< + ReactDatePickerProps, + Exclude< + keyof ReactDatePickerProps, + | 'monthsShown' + | 'showWeekNumbers' + | 'fixedHeight' + | 'dropdownMode' + | 'useShortMonthInDropdown' + | 'todayButton' + | 'timeCaption' + | 'disabledKeyboardNavigation' + | 'isClearable' + | 'withPortal' + | 'ref' + | 'placeholderText' + > + > & { + fullWidth?: boolean; + inputRef?: Ref; + injectTimes?: Moment[]; + isInvalid?: boolean; + isLoading?: boolean; + placeholder?: string; + shadow?: boolean; + }; + export const EuiDatePicker: React.SFC; + + type EuiFilterGroupProps = CommonProps; + export const EuiFilterGroup: React.SFC; + + type EuiFilterButtonProps = CommonProps & { + color?: ButtonColor; + href?: string; + iconSide?: ButtonIconSide; + iconType?: IconType; + isDisabled?: boolean; + isSelected?: boolean; + onClick: MouseEventHandler; + rel?: string; + target?: string; + type?: string; + }; + export const EuiFilterButton: React.SFC; } diff --git a/x-pack/yarn.lock b/x-pack/yarn.lock index bfc3ab79fd9fb..5c89eb4e501e8 100644 --- a/x-pack/yarn.lock +++ b/x-pack/yarn.lock @@ -276,6 +276,14 @@ version "15.5.3" resolved "https://registry.yarnpkg.com/@types/prop-types/-/prop-types-15.5.3.tgz#bef071852dca2a2dbb65fecdb7bfb30cedae2de2" +"@types/react-datepicker@^1.1.5": + version "1.1.5" + resolved "https://registry.yarnpkg.com/@types/react-datepicker/-/react-datepicker-1.1.5.tgz#2070d5ab86eacf69f5e9573b1d69af9982841c02" + dependencies: + "@types/react" "*" + moment ">=2.14.0" + popper.js "^1.14.1" + "@types/react-dom@^16.0.5": version "16.0.6" resolved "https://registry.yarnpkg.com/@types/react-dom/-/react-dom-16.0.6.tgz#f1a65a4e7be8ed5d123f8b3b9eacc913e35a1a3c" @@ -5645,7 +5653,7 @@ moment-timezone@^0.5.14: dependencies: moment ">= 2.9.0" -moment@2.22.2: +moment@2.22.2, moment@>=2.14.0: version "2.22.2" resolved "https://registry.yarnpkg.com/moment/-/moment-2.22.2.tgz#3c257f9839fc0e93ff53149632239eb90783ff66" From a9137065ec1f55c14f4c1b5de44897f0fceee50a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felix=20St=C3=BCrmer?= Date: Thu, 5 Jul 2018 22:54:33 +0200 Subject: [PATCH 2/3] Track scroll target in the location This wraps the `ScrollableLogTextStreamView` in a HoC, which in turn uses the `withStateFromLocation` HoC to retrieve the scroll state from the URL. It also updates the location accordingly when the visible log interval changes. The connection between the location and the app state is implemented as a HoC, because the location can be seen as a part of the UI. Additionally, this keeps the implementation agnostic of the state management used in the app, be it redux or apollo-link-state. The serialization is performed using the `mapStateToRisonAppLocation` and `mapRisonAppLocationToState` functions, that use the `_a` key of the query string for the sake of consistency with many parts of Kibana. --- x-pack/plugins/infra/common/time/time_key.ts | 6 + .../with_text_stream_scroll_state.tsx | 119 +++++++++++++++ .../containers/with_state_from_location.tsx | 137 ++++++++++++++++++ .../plugins/infra/public/pages/logs/logs.tsx | 9 +- x-pack/plugins/infra/types/rison_node.d.ts | 16 ++ 5 files changed, 281 insertions(+), 6 deletions(-) create mode 100644 x-pack/plugins/infra/public/containers/logging_legacy/with_text_stream_scroll_state.tsx create mode 100644 x-pack/plugins/infra/public/containers/with_state_from_location.tsx create mode 100644 x-pack/plugins/infra/types/rison_node.d.ts diff --git a/x-pack/plugins/infra/common/time/time_key.ts b/x-pack/plugins/infra/common/time/time_key.ts index 11ffaf49a2558..1693e8775ccad 100644 --- a/x-pack/plugins/infra/common/time/time_key.ts +++ b/x-pack/plugins/infra/common/time/time_key.ts @@ -14,6 +14,12 @@ export interface TimeKey { export type Comparator = (firstValue: any, secondValue: any) => number; +export const isTimeKey = (value: any): value is TimeKey => + value && + typeof value === 'object' && + typeof value.time === 'number' && + typeof value.tiebreaker === 'number'; + export function compareTimeKeys( firstKey: TimeKey, secondKey: TimeKey, diff --git a/x-pack/plugins/infra/public/containers/logging_legacy/with_text_stream_scroll_state.tsx b/x-pack/plugins/infra/public/containers/logging_legacy/with_text_stream_scroll_state.tsx new file mode 100644 index 0000000000000..8fc7700694369 --- /dev/null +++ b/x-pack/plugins/infra/public/containers/logging_legacy/with_text_stream_scroll_state.tsx @@ -0,0 +1,119 @@ +/* + * 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 { isTimeKey, TimeKey } from '../../../common/time'; +import { + mapRisonAppLocationToState, + mapStateToRisonAppLocation, + withStateFromLocation, +} from '../../containers/with_state_from_location'; + +interface TextStreamScrollState { + target: TimeKey; +} + +const withTargetStateFromLocation = withStateFromLocation< + TextStreamScrollState +>({ + mapLocationToState: mapRisonAppLocationToState(locationState => ({ + target: isTimeKey(locationState.target) + ? { + tiebreaker: locationState.target.tiebreaker, + time: locationState.target.time, + } + : { + tiebreaker: 0, + time: Date.now(), + }, + })), + mapStateToLocation: mapStateToRisonAppLocation(state => ({ + target: { + tiebreaker: state.target.tiebreaker, + time: state.target.time, + }, + })), +}); + +interface InjectedTextStreamScrollStateProps extends TextStreamScrollState { + jumpToTarget: (target: TimeKey) => any; + reportVisibleInterval: ( + params: { + pagesBeforeStart: number; + pagesAfterEnd: number; + startKey: TimeKey | null; + middleKey: TimeKey | null; + endKey: TimeKey | null; + } + ) => any; +} + +interface TextStreamScrollStateProps + extends InjectedTextStreamScrollStateProps { + pushStateInLocation: (state: TextStreamScrollState) => void; + replaceStateInLocation: (state: TextStreamScrollState) => void; +} + +export const withTextStreamScrollState = < + WrappedComponentProps extends InjectedTextStreamScrollStateProps +>( + WrappedComponent: React.ComponentType +) => { + const wrappedName = WrappedComponent.displayName || WrappedComponent.name; + + return withTargetStateFromLocation( + class WithTextStreamScrollState extends React.PureComponent< + TextStreamScrollStateProps & WrappedComponentProps + > { + public static displayName = `WithStateFromLocation(${wrappedName})`; + + public componentDidMount() { + this.jumpToTarget(this.props.target); + } + + // public componentDidUpdate(prevProps: TextStreamScrollStateProps) { + // if (this.props.target !== prevProps.target) { + // this.jumpToTarget(this.props.target); + // } + // } + + public render() { + const { + pushStateInLocation, + replaceStateInLocation, + ...otherProps + } = this.props as TextStreamScrollStateProps; + + return ( + + ); + } + + private jumpToTarget = (target: TimeKey) => { + this.props.jumpToTarget(target); + }; + + private reportVisibleInterval = (params: { + pagesBeforeStart: number; + pagesAfterEnd: number; + startKey: TimeKey | null; + middleKey: TimeKey | null; + endKey: TimeKey | null; + }) => { + if (params.middleKey) { + this.props.replaceStateInLocation({ + target: params.middleKey, + }); + } + return this.props.reportVisibleInterval(params); + }; + } + ); +}; diff --git a/x-pack/plugins/infra/public/containers/with_state_from_location.tsx b/x-pack/plugins/infra/public/containers/with_state_from_location.tsx new file mode 100644 index 0000000000000..86e053b429ca2 --- /dev/null +++ b/x-pack/plugins/infra/public/containers/with_state_from_location.tsx @@ -0,0 +1,137 @@ +/* + * 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 { Location } from 'history'; +import omit from 'lodash/fp/omit'; +import { + parse as parseQueryString, + stringify as stringifyQueryString, +} from 'querystring'; +import React from 'react'; +import { RouteComponentProps, withRouter } from 'react-router'; +import { decode_object, encode_object } from 'rison-node'; + +interface AnyObject { + [key: string]: any; +} + +type Omit = Pick>; + +interface WithStateFromLocationOptions { + mapLocationToState: (location: Location) => StateInLocation; + mapStateToLocation: (state: StateInLocation, location: Location) => Location; +} + +type InjectedPropsFromLocation = Partial & { + pushStateInLocation?: (state: StateInLocation) => void; + replaceStateInLocation?: (state: StateInLocation) => void; +}; + +export const withStateFromLocation = ({ + mapLocationToState, + mapStateToLocation, +}: WithStateFromLocationOptions) => < + WrappedComponentProps extends InjectedPropsFromLocation +>( + WrappedComponent: React.ComponentType +) => { + const wrappedName = WrappedComponent.displayName || WrappedComponent.name; + + return withRouter( + class WithStateFromLocation extends React.PureComponent< + RouteComponentProps<{}> & + Omit> + > { + public static displayName = `WithStateFromLocation(${wrappedName})`; + + public render() { + const { location } = this.props; + const otherProps = omit( + ['location', 'history', 'match', 'staticContext'], + this.props + ); + + const stateFromLocation = mapLocationToState(location); + + return ( + + ); + } + + private pushStateInLocation = (state: StateInLocation) => { + const { history, location } = this.props; + + const newLocation = mapStateToLocation(state, this.props.location); + + if (newLocation !== location) { + history.push(newLocation); + } + }; + + private replaceStateInLocation = (state: StateInLocation) => { + const { history, location } = this.props; + + const newLocation = mapStateToLocation(state, this.props.location); + + if (newLocation !== location) { + history.replace(newLocation); + } + }; + } + ); +}; + +const decodeRisonAppState = (queryValues: { _a?: string }): AnyObject => { + try { + return queryValues && queryValues._a ? decode_object(queryValues._a) : {}; + } catch (error) { + if ( + error instanceof Error && + error.message.startsWith('rison decoder error') + ) { + return {}; + } + throw error; + } +}; + +const encodeRisonAppState = (state: AnyObject) => ({ + _a: encode_object(state), +}); + +export const mapRisonAppLocationToState = ( + mapState: (risonAppState: AnyObject) => State = (state: AnyObject) => + state as State +) => (location: Location): State => { + const queryValues = parseQueryString(location.search.substring(1)); + const decodedState = decodeRisonAppState(queryValues); + return mapState(decodedState); +}; + +export const mapStateToRisonAppLocation = ( + mapState: (state: State) => AnyObject = (state: State) => state +) => (state: State, location: Location): Location => { + const previousQueryValues = parseQueryString(location.search.substring(1)); + const previousState = decodeRisonAppState(previousQueryValues); + + const encodedState = encodeRisonAppState({ + ...previousState, + ...mapState(state), + }); + const newQueryValues = stringifyQueryString({ + ...previousQueryValues, + ...encodedState, + }); + return { + ...location, + search: `?${newQueryValues}`, + }; +}; diff --git a/x-pack/plugins/infra/public/pages/logs/logs.tsx b/x-pack/plugins/infra/public/pages/logs/logs.tsx index 3b599b1def948..71d775c80d346 100644 --- a/x-pack/plugins/infra/public/pages/logs/logs.tsx +++ b/x-pack/plugins/infra/public/pages/logs/logs.tsx @@ -30,12 +30,13 @@ import { LogTextWrapControls } from '../../components/logging/log_text_wrap_cont import { LogTimeControls } from '../../components/logging/log_time_controls'; import { withLibs } from '../../containers/libs'; -import { State, targetActions } from '../../containers/logging_legacy/state'; +import { State } from '../../containers/logging_legacy/state'; import { withLogSearchControlsProps } from '../../containers/logging_legacy/with_log_search_controls_props'; import { withMinimapProps } from '../../containers/logging_legacy/with_minimap_props'; import { withMinimapScaleControlsProps } from '../../containers/logging_legacy/with_minimap_scale_controls_props'; import { withStreamItems } from '../../containers/logging_legacy/with_stream_items'; import { withTextScaleControlsProps } from '../../containers/logging_legacy/with_text_scale_controls_props'; +import { withTextStreamScrollState } from '../../containers/logging_legacy/with_text_stream_scroll_state'; import { withTextWrapControlsProps } from '../../containers/logging_legacy/with_text_wrap_controls_props'; import { withTimeControlsProps } from '../../containers/logging_legacy/with_time_controls_props'; import { withVisibleLogEntries } from '../../containers/logging_legacy/with_visible_log_entries'; @@ -54,7 +55,7 @@ const ConnectedLogTextWrapControls = withTextWrapControlsProps( ); const ConnectedTimeControls = withTimeControlsProps(LogTimeControls); const ConnectedScrollableLogTextStreamView = withStreamItems( - ScrollableLogTextStreamView + withTextStreamScrollState(ScrollableLogTextStreamView) ); interface LogsPageProps { @@ -84,10 +85,6 @@ export const LogsPage = withLibs( }; } - public componentDidMount() { - this.state.store.dispatch(targetActions.jumpToTime(Date.now())); - } - public componentDidUpdate(prevProps: LogsPageProps) { if (this.props.libs !== prevProps.libs) { this.state.libs.next(this.props.libs); diff --git a/x-pack/plugins/infra/types/rison_node.d.ts b/x-pack/plugins/infra/types/rison_node.d.ts new file mode 100644 index 0000000000000..92458b5113c25 --- /dev/null +++ b/x-pack/plugins/infra/types/rison_node.d.ts @@ -0,0 +1,16 @@ +/* + * 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. + */ +// tslint:disable:variable-name + +declare module 'rison-node' { + export const decode_object: ( + input: string + ) => ResultObject; + + export const encode_object: ( + input: InputObject + ) => string; +} From 4c588e20fb8b5c00489ffc7f138b67f10f5b2de2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felix=20St=C3=BCrmer?= Date: Thu, 12 Jul 2018 15:58:01 +0200 Subject: [PATCH 3/3] Format code in accordance with new prettier rules --- .../components/logging/log_time_controls.tsx | 13 ++----------- .../with_text_stream_scroll_state.tsx | 19 +++++-------------- .../with_time_controls_props.ts | 8 +------- .../containers/with_state_from_location.tsx | 18 ++++-------------- .../plugins/infra/public/pages/logs/logs.tsx | 12 +++--------- 5 files changed, 15 insertions(+), 55 deletions(-) diff --git a/x-pack/plugins/infra/public/components/logging/log_time_controls.tsx b/x-pack/plugins/infra/public/components/logging/log_time_controls.tsx index 4c0a655567884..93b3b0e10c7a7 100644 --- a/x-pack/plugins/infra/public/components/logging/log_time_controls.tsx +++ b/x-pack/plugins/infra/public/components/logging/log_time_controls.tsx @@ -21,12 +21,7 @@ interface LogTimeControlsProps { export class LogTimeControls extends React.PureComponent { public render() { - const { - currentTime, - disableLiveStreaming, - enableLiveStreaming, - isLiveStreaming, - } = this.props; + const { currentTime, disableLiveStreaming, enableLiveStreaming, isLiveStreaming } = this.props; const currentMoment = moment(currentTime); @@ -61,11 +56,7 @@ export class LogTimeControls extends React.PureComponent { injectTimes={[currentMoment]} /> - + Stream live diff --git a/x-pack/plugins/infra/public/containers/logging_legacy/with_text_stream_scroll_state.tsx b/x-pack/plugins/infra/public/containers/logging_legacy/with_text_stream_scroll_state.tsx index 8fc7700694369..41d9b1081ad02 100644 --- a/x-pack/plugins/infra/public/containers/logging_legacy/with_text_stream_scroll_state.tsx +++ b/x-pack/plugins/infra/public/containers/logging_legacy/with_text_stream_scroll_state.tsx @@ -17,9 +17,7 @@ interface TextStreamScrollState { target: TimeKey; } -const withTargetStateFromLocation = withStateFromLocation< - TextStreamScrollState ->({ +const withTargetStateFromLocation = withStateFromLocation({ mapLocationToState: mapRisonAppLocationToState(locationState => ({ target: isTimeKey(locationState.target) ? { @@ -52,8 +50,7 @@ interface InjectedTextStreamScrollStateProps extends TextStreamScrollState { ) => any; } -interface TextStreamScrollStateProps - extends InjectedTextStreamScrollStateProps { +interface TextStreamScrollStateProps extends InjectedTextStreamScrollStateProps { pushStateInLocation: (state: TextStreamScrollState) => void; replaceStateInLocation: (state: TextStreamScrollState) => void; } @@ -82,17 +79,11 @@ export const withTextStreamScrollState = < // } public render() { - const { - pushStateInLocation, - replaceStateInLocation, - ...otherProps - } = this.props as TextStreamScrollStateProps; + const { pushStateInLocation, replaceStateInLocation, ...otherProps } = this + .props as TextStreamScrollStateProps; return ( - + ); } diff --git a/x-pack/plugins/infra/public/containers/logging_legacy/with_time_controls_props.ts b/x-pack/plugins/infra/public/containers/logging_legacy/with_time_controls_props.ts index 96a9ac20a0ade..c5aff37f9a8b6 100644 --- a/x-pack/plugins/infra/public/containers/logging_legacy/with_time_controls_props.ts +++ b/x-pack/plugins/infra/public/containers/logging_legacy/with_time_controls_props.ts @@ -8,13 +8,7 @@ import { connect } from 'react-redux'; import { isIntervalLoadingPolicy } from '../../utils/loading_state'; import { bindPlainActionCreators } from '../../utils/typed_redux'; -import { - entriesActions, - entriesSelectors, - sharedSelectors, - State, - targetActions, -} from './state'; +import { entriesActions, entriesSelectors, sharedSelectors, State, targetActions } from './state'; export const withTimeControlsProps = connect( (state: State) => ({ diff --git a/x-pack/plugins/infra/public/containers/with_state_from_location.tsx b/x-pack/plugins/infra/public/containers/with_state_from_location.tsx index 86e053b429ca2..7f1d6baf7288f 100644 --- a/x-pack/plugins/infra/public/containers/with_state_from_location.tsx +++ b/x-pack/plugins/infra/public/containers/with_state_from_location.tsx @@ -6,10 +6,7 @@ import { Location } from 'history'; import omit from 'lodash/fp/omit'; -import { - parse as parseQueryString, - stringify as stringifyQueryString, -} from 'querystring'; +import { parse as parseQueryString, stringify as stringifyQueryString } from 'querystring'; import React from 'react'; import { RouteComponentProps, withRouter } from 'react-router'; import { decode_object, encode_object } from 'rison-node'; @@ -49,10 +46,7 @@ export const withStateFromLocation = ({ public render() { const { location } = this.props; - const otherProps = omit( - ['location', 'history', 'match', 'staticContext'], - this.props - ); + const otherProps = omit(['location', 'history', 'match', 'staticContext'], this.props); const stateFromLocation = mapLocationToState(location); @@ -93,10 +87,7 @@ const decodeRisonAppState = (queryValues: { _a?: string }): AnyObject => { try { return queryValues && queryValues._a ? decode_object(queryValues._a) : {}; } catch (error) { - if ( - error instanceof Error && - error.message.startsWith('rison decoder error') - ) { + if (error instanceof Error && error.message.startsWith('rison decoder error')) { return {}; } throw error; @@ -108,8 +99,7 @@ const encodeRisonAppState = (state: AnyObject) => ({ }); export const mapRisonAppLocationToState = ( - mapState: (risonAppState: AnyObject) => State = (state: AnyObject) => - state as State + mapState: (risonAppState: AnyObject) => State = (state: AnyObject) => state as State ) => (location: Location): State => { const queryValues = parseQueryString(location.search.substring(1)); const decodedState = decodeRisonAppState(queryValues); diff --git a/x-pack/plugins/infra/public/pages/logs/logs.tsx b/x-pack/plugins/infra/public/pages/logs/logs.tsx index 71d775c80d346..e4fc409393dbb 100644 --- a/x-pack/plugins/infra/public/pages/logs/logs.tsx +++ b/x-pack/plugins/infra/public/pages/logs/logs.tsx @@ -44,15 +44,9 @@ import { withVisibleLogEntries } from '../../containers/logging_legacy/with_visi const ConnectedLogMinimap = withMinimapProps(LogMinimap); const ConnectedLogMinimapScaleControls = withMinimapScaleControlsProps(LogMinimapScaleControls); const ConnectedLogPositionText = withVisibleLogEntries(LogPositionText); -const ConnectedLogSearchControls = withLogSearchControlsProps( - LogSearchControls -); -const ConnectedLogTextScaleControls = withTextScaleControlsProps( - LogTextScaleControls -); -const ConnectedLogTextWrapControls = withTextWrapControlsProps( - LogTextWrapControls -); +const ConnectedLogSearchControls = withLogSearchControlsProps(LogSearchControls); +const ConnectedLogTextScaleControls = withTextScaleControlsProps(LogTextScaleControls); +const ConnectedLogTextWrapControls = withTextWrapControlsProps(LogTextWrapControls); const ConnectedTimeControls = withTimeControlsProps(LogTimeControls); const ConnectedScrollableLogTextStreamView = withStreamItems( withTextStreamScrollState(ScrollableLogTextStreamView)