Skip to content

Commit

Permalink
[Infra UI] Selectively persist UI state in the url (via container com…
Browse files Browse the repository at this point in the history
…ponents) (#22980)

This PR implements syncing of local redux state to the URL via container components. The persisted pieces of state are:

* waffle map time
* waffle map auto-reload state
* waffle map filter
* log scroll position
* log live-stream state
* log filter
* log minimap scale
* log text size and wrap
  • Loading branch information
weltenwort authored Sep 18, 2018
1 parent 3773fe2 commit d9f423d
Show file tree
Hide file tree
Showing 18 changed files with 735 additions and 235 deletions.
1 change: 1 addition & 0 deletions src/ui/public/routes/route_manager.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@

interface RouteConfiguration {
controller?: string | (() => void);
reloadOnSearch?: boolean;
template?: string;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ interface AutocompleteFieldProps {

interface AutocompleteFieldState {
areSuggestionsVisible: boolean;
isFocused: boolean;
selectedIndex: number | null;
}

Expand All @@ -40,6 +41,7 @@ export class AutocompleteField extends React.Component<
> {
public readonly state: AutocompleteFieldState = {
areSuggestionsVisible: false,
isFocused: false,
selectedIndex: null,
};

Expand All @@ -50,15 +52,15 @@ export class AutocompleteField extends React.Component<
const { areSuggestionsVisible, selectedIndex } = this.state;

return (
<EuiOutsideClickDetector onOutsideClick={this.hideSuggestions}>
<EuiOutsideClickDetector onOutsideClick={this.handleBlur}>
<AutocompleteContainer>
<FixedEuiFieldSearch
fullWidth
inputRef={this.handleChangeInputRef}
isLoading={isLoadingSuggestions}
isInvalid={!isValid}
onChange={this.handleChange}
onFocus={this.showSuggestions}
onFocus={this.handleFocus}
onKeyDown={this.handleKeyDown}
onKeyUp={this.handleKeyUp}
onSearch={this.submit}
Expand Down Expand Up @@ -91,7 +93,7 @@ export class AutocompleteField extends React.Component<
this.updateSuggestions();
}

if (hasNewSuggestions) {
if (hasNewSuggestions && this.state.isFocused) {
this.showSuggestions();
}
}
Expand Down Expand Up @@ -150,6 +152,14 @@ export class AutocompleteField extends React.Component<
}
};

private handleFocus = () => {
this.setState(composeStateUpdaters(withSuggestionsVisible, withFocused));
};

private handleBlur = () => {
this.setState(composeStateUpdaters(withSuggestionsHidden, withUnfocused));
};

private selectSuggestionAt = (index: number) => () => {
this.setState(withSuggestionAtIndexSelected(index));
};
Expand Down Expand Up @@ -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;

Expand Down Expand Up @@ -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<HTMLInputElement> &
EuiFieldSearchProps & {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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),
Expand All @@ -33,3 +35,39 @@ export const withLogFilter = connect(
);

export const WithLogFilter = asChildFunctionRenderer(withLogFilter);

/**
* Url State
*/

type LogFilterUrlState = ReturnType<typeof logFilterSelectors.selectLogFilterQuery>;

export const WithLogFilterUrlState = () => (
<WithLogFilter>
{({ applyFilterQuery, filterQuery }) => (
<UrlStateContainer
urlState={filterQuery}
urlStateKey="logFilter"
mapToUrlState={mapToFilterQuery}
onChange={urlState => {
if (urlState) {
applyFilterQuery(urlState);
}
}}
onInitialize={urlState => {
if (urlState) {
applyFilterQuery(urlState);
}
}}
/>
)}
</WithLogFilter>
);

const mapToFilterQuery = (value: any): LogFilterUrlState | undefined =>
value && value.kind === 'kuery' && typeof value.expression === 'string'
? {
kind: value.kind,
expression: value.expression,
}
: undefined;
50 changes: 0 additions & 50 deletions x-pack/plugins/infra/public/containers/logs/with_log_minimap.ts

This file was deleted.

101 changes: 101 additions & 0 deletions x-pack/plugins/infra/public/containers/logs/with_log_minimap.tsx
Original file line number Diff line number Diff line change
@@ -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<typeof logMinimapSelectors.selectMinimapIntervalSize>;
}

export const WithLogMinimapUrlState = () => (
<WithLogMinimap>
{({ urlState, setIntervalSize }) => (
<UrlStateContainer
urlState={urlState}
urlStateKey="logMinimap"
mapToUrlState={mapToUrlState}
onChange={newUrlState => {
if (newUrlState && newUrlState.intervalSize) {
setIntervalSize(newUrlState.intervalSize);
}
}}
onInitialize={newUrlState => {
if (newUrlState && newUrlState.intervalSize) {
setIntervalSize(newUrlState.intervalSize);
}
}}
/>
)}
</WithLogMinimap>
);

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,
})
);
34 changes: 0 additions & 34 deletions x-pack/plugins/infra/public/containers/logs/with_log_position.ts

This file was deleted.

Loading

0 comments on commit d9f423d

Please sign in to comment.