Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Infra UI] Selectively persist UI state in the url (via container components) #22980

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<LogFilterUrlState>
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The mixing TS generics and JSX together reads weird. Can you assign that to a variable then just use the variable? I haven't tried that before so I'm not sure if it will work but it if it does it might make it more readable.

Copy link
Member Author

@weltenwort weltenwort Sep 13, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unfortunately that is not easily possible. The generic type argument has to be passed at instantiation time, which is handled by JSX. If I would convert the UrlStateContainer component into a class-based component, a no-op inheritance could nail down the type. Alternatively, we could manually type the passed properties sufficiently so the inference figures it out. Typing the return type of mapToUrlState should do it. Would you think that improves the situation?

<UrlStateContainer
  urlState={urlState}
  urlStateKey="logPosition"
  mapToUrlState={mapToUrlState}
  ...
/>

const mapToUrlState = (value: any): LogPositionUrlState | undefined => ...

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah... that's a lot easier to read.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ok, change coming up...

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

const mapToFilterQuery = (value: any) =>
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<LogMinimapUrlState>
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) =>
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