[Maps] use EuiSuperDatePicker in QueryBar instead of Angular timepicker in Top Nav (#29235)

* Embed timepicker in query bar (#29130)

* replace kbnTimepicker directive with EuiSuperDatePicker

* remove kbnTimepicker directive

* remove bootstrap datepicker

* embed timepicker in query bar

* flesh out date picker in query bar for maps app

* wire up refresh config

* fix bug with way update function called by watcher

* get maps application functional tests working with new timepicker

* remove some changes outside of scoped work

* clean up typescript lint problems

* fix query_bar I18n lint error

* update query_bar jest test

* grab some parts missing from one cherry-pick

* pass dateRange to updateQueryAndDispatch when app state changes

* use timefilter disable methods to hide timepicker from top naav

* get selected refresh unit

* use EuiSuperUpdate button

* Fix responsive sizing of datepicker (#29)

* set isDisabled on EuiSuperUpdateButton

* review feedback

* remove ts-ignore comment from defaultProps, fix query_bar snapshot

* make new props optional

* fighting with ts linter

* do not include dateRangeFrom and dateRangeTo in isDirty when shoDateRange is not true

* pull initial query from UI settings
This commit is contained in:
Nathan Reese 2019-01-30 17:02:42 -07:00 committed by GitHub
parent 434747b37c
commit c1d6a1bd76
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
24 changed files with 607 additions and 219 deletions

View file

@ -215,7 +215,7 @@ app.directive('dashboardApp', function ($injector) {
dashboardStateManager.getPanels().find((panel) => panel.panelIndex === panelIndex);
};
$scope.updateQueryAndFetch = function (query) {
$scope.updateQueryAndFetch = function ({ query }) {
const oldQuery = $scope.model.query;
if (_.isEqual(oldQuery, query)) {
// The user can still request a reload in the query bar, even if the
@ -236,7 +236,9 @@ app.directive('dashboardApp', function ($injector) {
$scope.indexPatterns = dashboardStateManager.getPanelIndexPatterns();
};
$scope.$watch('model.query', $scope.updateQueryAndFetch);
$scope.$watch('model.query', (query) => {
$scope.updateQueryAndFetch({ query });
});
$scope.$listenAndDigestAsync(timefilter, 'fetch', () => {
dashboardStateManager.handleTimeChange(timefilter.getTime());

View file

@ -525,7 +525,9 @@ function discoverController(
}
});
$scope.$watch('state.query', $scope.updateQueryAndFetch);
$scope.$watch('state.query', (query) => {
$scope.updateQueryAndFetch({ query });
});
$scope.$watchMulti([
'rows',
@ -643,7 +645,7 @@ function discoverController(
.catch(notify.error);
};
$scope.updateQueryAndFetch = function (query) {
$scope.updateQueryAndFetch = function ({ query }) {
$state.query = migrateLegacyQuery(query);
$scope.fetch();
};

View file

@ -311,7 +311,9 @@ function VisEditor(
$appStatus.dirty = status.dirty || !savedVis.id;
});
$scope.$watch('state.query', $scope.updateQueryAndFetch);
$scope.$watch('state.query', (query) => {
$scope.updateQueryAndFetch({ query });
});
$state.replace();
@ -385,7 +387,7 @@ function VisEditor(
}
}
$scope.updateQueryAndFetch = function (query) {
$scope.updateQueryAndFetch = function ({ query }) {
$state.query = migrateLegacyQuery(query);
$scope.fetch();
};

View file

@ -1,4 +1,4 @@
// SASSTODO: Formalize this color in Kibana's styling constants
$typeaheadConjunctionColor: #7800A6;
@import 'components/typeahead/index';
@import './components/index';

View file

@ -3,6 +3,7 @@
exports[`QueryBar Should disable autoFocus on EuiFieldText when disableAutoFocus prop is true 1`] = `
<EuiFlexGroup
alignItems="stretch"
className="kbnQueryBar"
component="div"
direction="row"
gutterSize="s"
@ -90,21 +91,13 @@ exports[`QueryBar Should disable autoFocus on EuiFieldText when disableAutoFocus
component="div"
grow={false}
>
<EuiButton
aria-label="Search"
color="primary"
<EuiSuperUpdateButton
data-test-subj="querySubmitButton"
fill={true}
iconSide="left"
isDisabled={false}
isLoading={false}
needsUpdate={false}
onClick={[Function]}
type="button"
>
<FormattedMessage
defaultMessage="Refresh"
id="common.ui.queryBar.refreshButtonLabel"
values={Object {}}
/>
</EuiButton>
/>
</EuiFlexItem>
</EuiFlexGroup>
`;
@ -112,6 +105,7 @@ exports[`QueryBar Should disable autoFocus on EuiFieldText when disableAutoFocus
exports[`QueryBar Should pass the query language to the language switcher 1`] = `
<EuiFlexGroup
alignItems="stretch"
className="kbnQueryBar"
component="div"
direction="row"
gutterSize="s"
@ -199,21 +193,13 @@ exports[`QueryBar Should pass the query language to the language switcher 1`] =
component="div"
grow={false}
>
<EuiButton
aria-label="Search"
color="primary"
<EuiSuperUpdateButton
data-test-subj="querySubmitButton"
fill={true}
iconSide="left"
isDisabled={false}
isLoading={false}
needsUpdate={false}
onClick={[Function]}
type="button"
>
<FormattedMessage
defaultMessage="Refresh"
id="common.ui.queryBar.refreshButtonLabel"
values={Object {}}
/>
</EuiButton>
/>
</EuiFlexItem>
</EuiFlexGroup>
`;
@ -221,6 +207,7 @@ exports[`QueryBar Should pass the query language to the language switcher 1`] =
exports[`QueryBar Should render the given query 1`] = `
<EuiFlexGroup
alignItems="stretch"
className="kbnQueryBar"
component="div"
direction="row"
gutterSize="s"
@ -308,21 +295,13 @@ exports[`QueryBar Should render the given query 1`] = `
component="div"
grow={false}
>
<EuiButton
aria-label="Search"
color="primary"
<EuiSuperUpdateButton
data-test-subj="querySubmitButton"
fill={true}
iconSide="left"
isDisabled={false}
isLoading={false}
needsUpdate={false}
onClick={[Function]}
type="button"
>
<FormattedMessage
defaultMessage="Refresh"
id="common.ui.queryBar.refreshButtonLabel"
values={Object {}}
/>
</EuiButton>
/>
</EuiFlexItem>
</EuiFlexGroup>
`;

View file

@ -0,0 +1,2 @@
@import './query_bar';
@import './typeahead/index';

View file

@ -0,0 +1,14 @@
@include euiBreakpoint('xs', 's') {
.kbnQueryBar--withDatePicker {
> :last-child {
// EUI Flexbox adds too much margin between responded items, this just moves the last one up
margin-top: -$euiSize;
}
}
}
@include euiBreakpoint('m', 'l', 'xl') {
.kbnQueryBar__datePickerWrapper {
max-width: 40vw;
}
}

View file

@ -207,8 +207,14 @@ describe('QueryBar', () => {
component.find(QueryLanguageSwitcher).simulate('selectLanguage', 'lucene');
expect(mockStorage.set).toHaveBeenCalledWith('kibana.userQueryLanguage', 'lucene');
expect(mockCallback).toHaveBeenCalledWith({
query: '',
language: 'lucene',
dateRange: {
from: 'now-15m',
to: 'now',
},
query: {
query: '',
language: 'lucene',
},
});
});
@ -235,8 +241,14 @@ describe('QueryBar', () => {
expect(mockCallback).toHaveBeenCalledTimes(1);
expect(mockCallback).toHaveBeenCalledWith({
query: 'extension:jpg',
language: 'kuery',
dateRange: {
from: 'now-15m',
to: 'now',
},
query: {
query: 'extension:jpg',
language: 'kuery',
},
});
});

View file

@ -19,12 +19,15 @@
import { IndexPattern } from 'ui/index_patterns';
import { compact, debounce, isEqual } from 'lodash';
import classNames from 'classnames';
import _ from 'lodash';
import { compact, debounce, get, isEqual } from 'lodash';
import React, { Component } from 'react';
import { getFromLegacyIndexPattern } from 'ui/index_patterns/static_utils';
import { kfetch } from 'ui/kfetch';
import { PersistedLog } from 'ui/persisted_log';
import { Storage } from 'ui/storage';
import { timeHistory } from 'ui/timefilter/time_history';
import {
AutocompleteSuggestion,
AutocompleteSuggestionType,
@ -36,15 +39,12 @@ import { matchPairs } from '../lib/match_pairs';
import { QueryLanguageSwitcher } from './language_switcher';
import { SuggestionsComponent } from './typeahead/suggestions_component';
import {
EuiButton,
EuiFieldText,
EuiFlexGroup,
EuiFlexItem,
EuiOutsideClickDetector,
} from '@elastic/eui';
import { EuiFieldText, EuiFlexGroup, EuiFlexItem, EuiOutsideClickDetector } from '@elastic/eui';
import { FormattedMessage, InjectedIntl, injectI18n } from '@kbn/i18n/react';
// @ts-ignore
import { EuiSuperDatePicker, EuiSuperUpdateButton } from '@elastic/eui';
import { InjectedIntl, injectI18n } from '@kbn/i18n/react';
const KEY_CODES = {
LEFT: 37,
@ -66,14 +66,25 @@ interface Query {
language: string;
}
interface DateRange {
from: string;
to: string;
}
interface Props {
query: Query;
onSubmit: (query: { query: string | object; language: string }) => void;
onSubmit: (payload: { dateRange: DateRange; query: Query }) => void;
disableAutoFocus?: boolean;
appName: string;
indexPatterns: IndexPattern[];
store: Storage;
intl: InjectedIntl;
showDatePicker?: boolean;
dateRangeFrom?: string;
dateRangeTo?: string;
isRefreshPaused?: boolean;
refreshInterval?: number;
onRefreshChange?: (isPaused: boolean, refreshInterval: number) => void;
}
interface State {
@ -84,6 +95,9 @@ interface State {
suggestions: AutocompleteSuggestion[];
suggestionLimit: number;
currentProps?: Props;
dateRangeFrom: string;
dateRangeTo: string;
isDateRangeInvalid: boolean;
}
export class QueryBarUI extends Component<Props, State> {
@ -92,25 +106,41 @@ export class QueryBarUI extends Component<Props, State> {
return null;
}
let nextQuery = null;
if (nextProps.query.query !== prevState.query.query) {
return {
query: {
query: toUser(nextProps.query.query),
language: nextProps.query.language,
},
currentProps: nextProps,
nextQuery = {
query: toUser(nextProps.query.query),
language: nextProps.query.language,
};
} else if (nextProps.query.language !== prevState.query.language) {
return {
query: {
query: '',
language: nextProps.query.language,
},
currentProps: nextProps,
nextQuery = {
query: '',
language: nextProps.query.language,
};
}
return { currentProps: nextProps };
let nextDateRange = null;
if (
nextProps.dateRangeFrom !== get(prevState, 'currentProps.dateRangeFrom') ||
nextProps.dateRangeTo !== get(prevState, 'currentProps.dateRangeTo')
) {
nextDateRange = {
dateRangeFrom: nextProps.dateRangeFrom,
dateRangeTo: nextProps.dateRangeTo,
};
}
const nextState: any = {
currentProps: nextProps,
};
if (nextQuery) {
nextState.query = nextQuery;
}
if (nextDateRange) {
nextState.dateRangeFrom = nextDateRange.dateRangeFrom;
nextState.dateRangeTo = nextDateRange.dateRangeTo;
}
return nextState;
}
/*
@ -136,6 +166,9 @@ export class QueryBarUI extends Component<Props, State> {
index: null,
suggestions: [],
suggestionLimit: 50,
dateRangeFrom: _.get(this.props, 'dateRangeFrom', 'now-15m'),
dateRangeTo: _.get(this.props, 'dateRangeTo', 'now'),
isDateRangeInvalid: false,
};
public updateSuggestions = debounce(async () => {
@ -151,7 +184,15 @@ export class QueryBarUI extends Component<Props, State> {
private persistedLog: PersistedLog | null = null;
public isDirty = () => {
return this.state.query.query !== this.props.query.query;
if (!this.props.showDatePicker) {
return this.state.query.query !== this.props.query.query;
}
return (
this.state.query.query !== this.props.query.query ||
this.state.dateRangeFrom !== this.props.dateRangeFrom ||
this.state.dateRangeTo !== this.props.dateRangeTo
);
};
public increaseLimit = () => {
@ -322,6 +363,22 @@ export class QueryBarUI extends Component<Props, State> {
this.onInputChange(event.target.value);
};
public onTimeChange = ({
start,
end,
isInvalid,
}: {
start: string;
end: string;
isInvalid: boolean;
}) => {
this.setState({
dateRangeFrom: start,
dateRangeTo: end,
isDateRangeInvalid: isInvalid,
});
};
public onKeyUp = (event: React.KeyboardEvent<HTMLInputElement>) => {
if ([KEY_CODES.LEFT, KEY_CODES.RIGHT, KEY_CODES.HOME, KEY_CODES.END].includes(event.keyCode)) {
this.setState({ isSuggestionsVisible: true });
@ -408,9 +465,20 @@ export class QueryBarUI extends Component<Props, State> {
this.persistedLog.add(this.state.query.query);
}
timeHistory.add({
from: this.state.dateRangeFrom,
to: this.state.dateRangeTo,
});
this.props.onSubmit({
query: fromUser(this.state.query.query),
language: this.state.query.language,
query: {
query: fromUser(this.state.query.query),
language: this.state.query.language,
},
dateRange: {
from: this.state.dateRangeFrom,
to: this.state.dateRangeTo,
},
});
this.setState({ isSuggestionsVisible: false });
};
@ -427,8 +495,14 @@ export class QueryBarUI extends Component<Props, State> {
this.props.store.set('kibana.userQueryLanguage', language);
this.props.onSubmit({
query: '',
language,
query: {
query: '',
language,
},
dateRange: {
from: this.state.dateRangeFrom,
to: this.state.dateRangeTo,
},
});
};
@ -462,8 +536,16 @@ export class QueryBarUI extends Component<Props, State> {
}
public render() {
const classes = classNames('kbnQueryBar', {
'kbnQueryBar--withDatePicker': this.props.showDatePicker,
});
return (
<EuiFlexGroup responsive={false} gutterSize="s">
<EuiFlexGroup
className={classes}
responsive={this.props.showDatePicker ? true : false}
gutterSize="s"
>
<EuiFlexItem>
<EuiOutsideClickDetector onOutsideClick={this.onOutsideClick}>
{/* position:relative required on container so the suggestions appear under the query bar*/}
@ -533,30 +615,74 @@ export class QueryBarUI extends Component<Props, State> {
</div>
</EuiOutsideClickDetector>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiButton
aria-label={this.props.intl.formatMessage({
id: 'common.ui.queryBar.searchButtonAriaLabel',
defaultMessage: 'Search',
})}
data-test-subj="querySubmitButton"
color={this.isDirty() ? 'secondary' : 'primary'}
fill
onClick={this.onClickSubmitButton}
>
{this.isDirty() ? (
<FormattedMessage id="common.ui.queryBar.updateButtonLabel" defaultMessage="Update" />
) : (
<FormattedMessage
id="common.ui.queryBar.refreshButtonLabel"
defaultMessage="Refresh"
/>
)}
</EuiButton>
</EuiFlexItem>
<EuiFlexItem grow={false}>{this.renderUpdateButton()}</EuiFlexItem>
</EuiFlexGroup>
);
}
private renderUpdateButton() {
const button = (
<EuiSuperUpdateButton
needsUpdate={this.isDirty()}
isDisabled={this.state.isDateRangeInvalid}
onClick={this.onClickSubmitButton}
data-test-subj="querySubmitButton"
/>
);
if (this.props.showDatePicker) {
return (
<EuiFlexGroup responsive={false} gutterSize="s">
{this.renderDatePicker()}
<EuiFlexItem grow={false}>{button}</EuiFlexItem>
</EuiFlexGroup>
);
} else {
return button;
}
}
private renderDatePicker() {
if (!this.props.showDatePicker) {
return null;
}
const recentlyUsedRanges = timeHistory
.get()
.map(({ from, to }: { from: string; to: string }) => {
return {
start: from,
end: to,
};
});
const commonlyUsedRanges = config
.get('timepicker:quickRanges')
.map(({ from, to, display }: { from: string; to: string; display: string }) => {
return {
start: from,
end: to,
label: display,
};
});
return (
<EuiFlexItem className="kbnQueryBar__datePickerWrapper">
<EuiSuperDatePicker
start={this.state.dateRangeFrom}
end={this.state.dateRangeTo}
isPaused={this.props.isRefreshPaused}
refreshInterval={this.props.refreshInterval}
onTimeChange={this.onTimeChange}
onRefreshChange={this.props.onRefreshChange}
showUpdateButton={false}
recentlyUsedRanges={recentlyUsedRanges}
commonlyUsedRanges={commonlyUsedRanges}
dateFormat={config.get('dateFormat')}
/>
</EuiFlexItem>
);
}
}
// @ts-ignore
export const QueryBar = injectI18n(QueryBarUI);

View file

@ -0,0 +1,31 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
interface TimeRange {
from: string;
to: string;
mode?: string;
}
export interface TimeHistory {
add: (options: TimeRange) => void;
get: () => TimeRange[];
}
export const timeHistory: TimeHistory;

View file

@ -33,6 +33,7 @@ import {
VisualBuilderPageProvider,
TimelionPageProvider,
SharePageProvider,
TimePickerPageProvider,
} from './page_objects';
import {
@ -94,6 +95,7 @@ export default async function ({ readConfigFile }) {
visualBuilder: VisualBuilderPageProvider,
timelion: TimelionPageProvider,
share: SharePageProvider,
timePicker: TimePickerPageProvider,
},
services: {
es: commonConfig.get('services.es'),

View file

@ -32,3 +32,4 @@ export { PointSeriesPageProvider } from './point_series_page';
export { VisualBuilderPageProvider } from './visual_builder_page';
export { TimelionPageProvider } from './timelion_page';
export { SharePageProvider } from './share_page';
export { TimePickerPageProvider } from './time_picker';

View file

@ -0,0 +1,122 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
export function TimePickerPageProvider({ getService }) {
const log = getService('log');
const retry = getService('retry');
const find = getService('find');
const testSubjects = getService('testSubjects');
class TimePickerPage {
async isQuickSelectMenuOpen() {
return await testSubjects.exists('superDatePickerQuickMenu');
}
async openQuickSelectTimeMenu() {
log.debug('openQuickSelectTimeMenu');
const isMenuOpen = await this.isQuickSelectMenuOpen();
if (!isMenuOpen) {
log.debug('opening quick select menu');
await retry.try(async () => {
await testSubjects.click('superDatePickerToggleQuickMenuButton');
});
}
}
async closeQuickSelectTimeMenu() {
log.debug('closeQuickSelectTimeMenu');
const isMenuOpen = await this.isQuickSelectMenuOpen();
if (isMenuOpen) {
log.debug('closing quick select menu');
await retry.try(async () => {
await testSubjects.click('superDatePickerToggleQuickMenuButton');
});
}
}
async showStartEndTimes() {
const isShowDatesButton = await testSubjects.exists('superDatePickerShowDatesButton');
if (isShowDatesButton) {
await testSubjects.click('superDatePickerShowDatesButton');
}
}
async getRefreshConfig(keepQuickSelectOpen = false) {
await this.openQuickSelectTimeMenu();
const interval = await testSubjects.getAttribute('superDatePickerRefreshIntervalInput', 'value');
let selectedUnit;
const select = await testSubjects.find('superDatePickerRefreshIntervalUnitsSelect');
const options = await find.allDescendantDisplayedByCssSelector('option', select);
await Promise.all(options.map(async (optionElement) => {
const isSelected = await optionElement.isSelected();
if (isSelected) {
selectedUnit = await optionElement.getVisibleText();
}
}));
const toggleButtonText = await testSubjects.getVisibleText('superDatePickerToggleRefreshButton');
if (!keepQuickSelectOpen) {
await this.closeQuickSelectTimeMenu();
}
return {
interval,
units: selectedUnit,
isPaused: toggleButtonText === 'Start' ? true : false
};
}
async getTimeConfig() {
await this.showStartEndTimes();
const start = await testSubjects.getVisibleText('superDatePickerstartDatePopoverButton');
const end = await testSubjects.getVisibleText('superDatePickerendDatePopoverButton');
return {
start,
end
};
}
async pauseAutoRefresh() {
log.debug('pauseAutoRefresh');
const refreshConfig = await this.getRefreshConfig(true);
if (!refreshConfig.isPaused) {
log.debug('pause auto refresh');
await testSubjects.click('superDatePickerToggleRefreshButton');
await this.closeQuickSelectTimeMenu();
}
await this.closeQuickSelectTimeMenu();
}
async resumeAutoRefresh() {
log.debug('resumeAutoRefresh');
const refreshConfig = await this.getRefreshConfig(true);
if (refreshConfig.isPaused) {
log.debug('resume auto refresh');
await testSubjects.click('superDatePickerToggleRefreshButton');
}
await this.closeQuickSelectTimeMenu();
}
}
return new TimePickerPage();
}

View file

@ -16,7 +16,6 @@ import {
getMapReady,
getWaitingForMapReadyLayerListRaw,
} from '../selectors/map_selectors';
import { timeService } from '../kibana_services';
export const SET_SELECTED_LAYER = 'SET_SELECTED_LAYER';
export const UPDATE_LAYER_ORDER = 'UPDATE_LAYER_ORDER';
@ -35,7 +34,6 @@ export const LAYER_DATA_LOAD_STARTED = 'LAYER_DATA_LOAD_STARTED';
export const LAYER_DATA_LOAD_ENDED = 'LAYER_DATA_LOAD_ENDED';
export const LAYER_DATA_LOAD_ERROR = 'LAYER_DATA_LOAD_ERROR';
export const SET_JOINS = 'SET_JOINS';
export const SET_TIME_FILTERS = 'SET_TIME_FILTERS';
export const SET_QUERY = 'SET_QUERY';
export const TRIGGER_REFRESH_TIMER = 'TRIGGER_REFRESH_TIMER';
export const UPDATE_LAYER_PROP = 'UPDATE_LAYER_PROP';
@ -418,43 +416,17 @@ export function removeLayer(id) {
}
export function setMeta(metaJson) {
return async dispatch => {
dispatch({
type: SET_META,
meta: metaJson
});
return {
type: SET_META,
meta: metaJson
};
}
export function setTimeFiltersToKbnGlobalTime() {
return (dispatch) => {
dispatch(setTimeFilters(timeService.getTime()));
};
}
export function setTimeFilters({ from, to }) {
return async (dispatch, getState) => {
dispatch({
type: SET_TIME_FILTERS,
from,
to,
});
// Update Kibana global time
const kbnTime = timeService.getTime();
if ((to && to !== kbnTime.to) || (from && from !== kbnTime.from)) {
timeService.setTime({ from, to });
}
const dataFilters = getDataFilters(getState());
await syncDataForAllLayers(getState, dispatch, dataFilters);
};
}
export function setQuery({ query }) {
export function setQuery({ query, timeFilters }) {
return async (dispatch, getState) => {
dispatch({
type: SET_QUERY,
timeFilters,
query: {
...query,
// ensure query changes to trigger re-fetch even when query is the same because "Refresh" clicked
@ -468,21 +440,10 @@ export function setQuery({ query }) {
}
export function setRefreshConfig({ isPaused, interval }) {
return async (dispatch) => {
dispatch({
type: SET_REFRESH_CONFIG,
isPaused,
interval,
});
// Update Kibana global refresh
const kbnRefresh = timeService.getRefreshInterval();
if (isPaused !== kbnRefresh.pause || interval !== kbnRefresh.value) {
timeService.setRefreshInterval({
pause: isPaused,
value: interval,
});
}
return {
type: SET_REFRESH_CONFIG,
isPaused,
interval,
};
}

View file

@ -0,0 +1,32 @@
/*
* 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 chrome from 'ui/chrome';
const settings = chrome.getUiSettingsClient();
export function getInitialQuery({
mapStateJSON,
appState = {},
userQueryLanguage,
}) {
if (appState.query) {
return appState.query;
}
if (mapStateJSON) {
const mapState = JSON.parse(mapStateJSON);
if (mapState.query) {
return mapState.query;
}
}
return {
query: '',
language: userQueryLanguage || settings.get('search:queryLanguage')
};
}

View file

@ -0,0 +1,28 @@
/*
* 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 chrome from 'ui/chrome';
const uiSettings = chrome.getUiSettingsClient();
export function getInitialRefreshConfig({
mapStateJSON,
globalState = {},
}) {
if (mapStateJSON) {
const mapState = JSON.parse(mapStateJSON);
if (mapState.refreshConfig) {
return mapState.refreshConfig;
}
}
const defaultRefreshConfig = uiSettings.get('timepicker:refreshIntervalDefaults');
const refreshInterval = { ...defaultRefreshConfig, ...globalState.refreshInterval };
return {
isPaused: refreshInterval.pause,
interval: refreshInterval.value,
};
}

View file

@ -0,0 +1,24 @@
/*
* 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 chrome from 'ui/chrome';
const uiSettings = chrome.getUiSettingsClient();
export function getInitialTimeFilters({
mapStateJSON,
globalState = {},
}) {
if (mapStateJSON) {
const mapState = JSON.parse(mapStateJSON);
if (mapState.timeFilters) {
return mapState.timeFilters;
}
}
const defaultTime = uiSettings.get('timepicker:timeDefaults');
return { ...defaultTime, ...globalState.time };
}

View file

@ -27,6 +27,12 @@
app-name="'maps'"
on-submit="updateQueryAndDispatch"
index-patterns="indexPatterns"
show-date-picker="showDatePicker"
date-range-from="time.from"
date-range-to="time.to"
is-refresh-paused="refreshConfig.isPaused"
refresh-interval="refreshConfig.interval"
on-refresh-change="onRefreshChange"
></query-bar>
</div>

View file

@ -14,7 +14,6 @@ import { getStore } from '../store/store';
import { GisMap } from '../components/gis_map';
import {
setSelectedLayer,
setTimeFilters,
setRefreshConfig,
setGotoWithCenter,
replaceLayerList,
@ -28,40 +27,95 @@ import { SavedObjectSaveModal } from 'ui/saved_objects/components/saved_object_s
import { showSaveModal } from 'ui/saved_objects/show_saved_object_save_modal';
import { toastNotifications } from 'ui/notify';
import { getInitialLayers } from './get_initial_layers';
import { getInitialQuery } from './get_initial_query';
import { getInitialTimeFilters } from './get_initial_time_filters';
import { getInitialRefreshConfig } from './get_initial_refresh_config';
const REACT_ANCHOR_DOM_ELEMENT_ID = 'react-gis-root';
const DEFAULT_QUERY_LANGUAGE = 'kuery';
const app = uiModules.get('app/gis', []);
app.controller('GisMapController', ($scope, $route, config, kbnUrl, localStorage, AppState) => {
app.controller('GisMapController', ($scope, $route, config, kbnUrl, localStorage, AppState, globalState) => {
const savedMap = $scope.map = $route.current.locals.map;
let unsubscribe;
inspectorAdapters.requests.reset();
$scope.$listen(globalState, 'fetch_with_changes', (diff) => {
if (diff.includes('time')) {
$scope.updateQueryAndDispatch({ query: $scope.query, dateRange: globalState.time });
}
if (diff.includes('refreshInterval')) {
$scope.onRefreshChange({ isPaused: globalState.pause, refreshInterval: globalState.value });
}
});
const $state = new AppState();
$scope.$listen($state, 'fetch_with_changes', function (diff) {
if (diff.includes('query')) {
$scope.updateQueryAndDispatch($state.query);
$scope.updateQueryAndDispatch({ query: $state.query, dateRange: $scope.time });
}
});
$scope.query = {};
function syncAppAndGlobalState() {
$scope.$evalAsync(() => {
$state.query = $scope.query;
$state.save();
globalState.time = $scope.time;
globalState.refreshInterval = {
pause: $scope.refreshConfig.isPaused,
value: $scope.refreshConfig.interval,
};
globalState.save();
});
}
$scope.query = getInitialQuery({
mapStateJSON: savedMap.mapStateJSON,
appState: $state,
userQueryLanguage: localStorage.get('kibana.userQueryLanguage')
});
$scope.time = getInitialTimeFilters({
mapStateJSON: savedMap.mapStateJSON,
globalState: globalState,
});
$scope.refreshConfig = getInitialRefreshConfig({
mapStateJSON: savedMap.mapStateJSON,
globalState: globalState,
});
syncAppAndGlobalState();
$scope.indexPatterns = [];
$scope.updateQueryAndDispatch = function (newQuery) {
$scope.query = newQuery;
$scope.updateQueryAndDispatch = function ({ dateRange, query }) {
$scope.query = query;
$scope.time = dateRange;
getStore().then(store => {
// ignore outdated query
if ($scope.query !== newQuery) {
if ($scope.query !== query && $scope.time !== dateRange) {
return;
}
store.dispatch(setQuery({ query: $scope.query }));
store.dispatch(setQuery({ query: $scope.query, timeFilters: $scope.time }));
// update appState
$state.query = $scope.query;
$state.save();
syncAppAndGlobalState();
});
};
$scope.onRefreshChange = function ({ isPaused, refreshInterval }) {
$scope.refreshConfig = {
isPaused,
interval: refreshInterval ? refreshInterval : $scope.refreshConfig.interval
};
getStore().then(store => {
// ignore outdated
if ($scope.refreshConfig.isPaused !== isPaused && $scope.refreshConfig.interval !== refreshInterval) {
return;
}
store.dispatch(setRefreshConfig($scope.refreshConfig));
syncAppAndGlobalState();
});
};
@ -76,36 +130,20 @@ app.controller('GisMapController', ($scope, $route, config, kbnUrl, localStorage
});
// sync store with savedMap mapState
let queryFromSavedObject;
if (savedMap.mapStateJSON) {
const mapState = JSON.parse(savedMap.mapStateJSON);
queryFromSavedObject = mapState.query;
const timeFilters = mapState.timeFilters ? mapState.timeFilters : timefilter.getTime();
store.dispatch(setTimeFilters(timeFilters));
store.dispatch(setGotoWithCenter({
lat: mapState.center.lat,
lon: mapState.center.lon,
zoom: mapState.zoom,
}));
if (mapState.refreshConfig) {
store.dispatch(setRefreshConfig(mapState.refreshConfig));
}
}
const layerList = getInitialLayers(savedMap.layerListJSON, getDataSources(store.getState()));
store.dispatch(replaceLayerList(layerList));
// Initialize query, syncing appState and store
if ($state.query) {
$scope.updateQueryAndDispatch($state.query);
} else if (queryFromSavedObject) {
$scope.updateQueryAndDispatch(queryFromSavedObject);
} else {
$scope.updateQueryAndDispatch({
query: '',
language: localStorage.get('kibana.userQueryLanguage') || DEFAULT_QUERY_LANGUAGE
});
}
store.dispatch(setRefreshConfig($scope.refreshConfig));
store.dispatch(setQuery({ query: $scope.query, timeFilters: $scope.time }));
const root = document.getElementById(REACT_ANCHOR_DOM_ELEMENT_ID);
render(
@ -136,9 +174,7 @@ app.controller('GisMapController', ($scope, $route, config, kbnUrl, localStorage
}
function handleStoreChanges(store) {
const state = store.getState();
const nextIndexPatternIds = getUniqueIndexPatternIds(state);
const nextIndexPatternIds = getUniqueIndexPatternIds(store.getState());
if (nextIndexPatternIds !== prevIndexPatternIds) {
prevIndexPatternIds = nextIndexPatternIds;
updateIndexPatterns(nextIndexPatternIds);
@ -197,6 +233,10 @@ app.controller('GisMapController', ($scope, $route, config, kbnUrl, localStorage
return { id };
}
// Hide angular timepicer/refresh UI from top nav
timefilter.disableTimeRangeSelector();
timefilter.disableAutoRefreshSelector();
$scope.showDatePicker = true; // used by query-bar directive to enable timepikcer in query bar
$scope.topNavMenu = [{
key: 'inspect',
description: 'Open Inspector',
@ -238,6 +278,4 @@ app.controller('GisMapController', ($scope, $route, config, kbnUrl, localStorage
showSaveModal(saveModal);
}
}];
timefilter.enableTimeRangeSelector();
timefilter.enableAutoRefreshSelector();
});

View file

@ -7,26 +7,22 @@
import { connect } from 'react-redux';
import { GisMap } from './view';
import { getFlyoutDisplay, FLYOUT_STATE } from '../../store/ui';
import {
setTimeFiltersToKbnGlobalTime,
triggerRefreshTimer,
setRefreshConfig
} from '../../actions/store_actions';
import { triggerRefreshTimer } from '../../actions/store_actions';
import { getRefreshConfig } from '../../selectors/map_selectors';
function mapStateToProps(state = {}) {
const flyoutDisplay = getFlyoutDisplay(state);
return {
layerDetailsVisible: flyoutDisplay === FLYOUT_STATE.LAYER_PANEL,
addLayerVisible: flyoutDisplay === FLYOUT_STATE.ADD_LAYER_WIZARD,
noFlyoutVisible: flyoutDisplay === FLYOUT_STATE.NONE
noFlyoutVisible: flyoutDisplay === FLYOUT_STATE.NONE,
refreshConfig: getRefreshConfig(state),
};
}
function mapDispatchToProps(dispatch) {
return {
setTimeFiltersToKbnGlobalTime: () => dispatch(setTimeFiltersToKbnGlobalTime()),
triggerRefreshTimer: () => dispatch(triggerRefreshTimer()),
setRefreshConfig: (({ isPaused, interval }) => dispatch(setRefreshConfig({ isPaused, interval }))),
};
}

View file

@ -12,39 +12,41 @@ import { AddLayerPanel } from '../layer_addpanel/index';
import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
import { Toasts } from '../toasts';
import { timeService } from '../../kibana_services';
export class GisMap extends Component {
componentDidMount() {
timeService.on('timeUpdate', this.props.setTimeFiltersToKbnGlobalTime);
timeService.on('refreshIntervalUpdate', this.setRefreshTimer);
this.setRefreshTimer();
}
componentDidUpdate() {
this.setRefreshTimer();
}
componentWillUnmount() {
timeService.off('timeUpdate', this.props.setTimeFiltersToKbnGlobalTime);
timeService.off('refreshIntervalUpdate', this.setRefreshTimer);
this.clearRefreshTimer();
}
setRefreshTimer = () => {
const { isPaused, interval } = this.props.refreshConfig;
if (this.isPaused === isPaused && this.interval === interval) {
// refreshConfig is the same, nothing to do
return;
}
this.isPaused = isPaused;
this.interval = interval;
this.clearRefreshTimer();
const { value, pause } = timeService.getRefreshInterval();
if (!pause && value > 0) {
if (!isPaused && interval > 0) {
this.refreshTimerId = setInterval(
() => {
this.props.triggerRefreshTimer();
},
value
interval
);
}
this.props.setRefreshConfig({
isPaused: pause,
interval: value,
});
}
clearRefreshTimer = () => {

View file

@ -20,7 +20,6 @@ import {
MAP_EXTENT_CHANGED,
MAP_READY,
MAP_DESTROYED,
SET_TIME_FILTERS,
SET_QUERY,
UPDATE_LAYER_PROP,
UPDATE_LAYER_STYLE_FOR_SELECTED_LAYER,
@ -163,12 +162,16 @@ export function map(state = INITIAL_STATE, action) {
buffer: action.mapState.buffer,
};
return { ...state, mapState: { ...state.mapState, ...newMapState } };
case SET_TIME_FILTERS:
const { from, to } = action;
return { ...state, mapState: { ...state.mapState, timeFilters: { from, to } } };
case SET_QUERY:
const { query } = action;
return { ...state, mapState: { ...state.mapState, query } };
const { query, timeFilters } = action;
return {
...state,
mapState: {
...state.mapState,
query,
timeFilters,
}
};
case SET_REFRESH_CONFIG:
const { isPaused, interval } = action;
return {

View file

@ -8,7 +8,7 @@ import expect from 'expect.js';
export default function ({ getPageObjects, getService }) {
const PageObjects = getPageObjects(['gis', 'header']);
const PageObjects = getPageObjects(['gis', 'header', 'timePicker']);
const queryBar = getService('queryBar');
const browser = getService('browser');
const inspector = getService('inspector');
@ -25,13 +25,16 @@ export default function ({ getPageObjects, getService }) {
});
it('should update global Kibana time to value stored with map', async () => {
const kibanaTime = await PageObjects.header.getPrettyDuration();
expect(kibanaTime).to.equal('Last 17m');
const timeConfig = await PageObjects.timePicker.getTimeConfig();
expect(timeConfig.start).to.equal('~ 17 minutes ago');
expect(timeConfig.end).to.equal('now');
});
it('should update global Kibana refresh config to value stored with map', async () => {
const kibanaRefreshConfig = await PageObjects.header.getRefreshConfig();
expect(kibanaRefreshConfig).to.equal('inactive 1 second');
const kibanaRefreshConfig = await PageObjects.timePicker.getRefreshConfig();
expect(kibanaRefreshConfig.interval).to.equal('0.02');
expect(kibanaRefreshConfig.units).to.equal('minutes');
expect(kibanaRefreshConfig.isPaused).to.equal(true);
});
it('should set map location to value stored with map', async () => {

View file

@ -5,7 +5,7 @@
*/
export function GisPageProvider({ getService, getPageObjects }) {
const PageObjects = getPageObjects(['common', 'header']);
const PageObjects = getPageObjects(['common', 'header', 'timePicker']);
const log = getService('log');
const testSubjects = getService('testSubjects');
@ -204,10 +204,10 @@ export function GisPageProvider({ getService, getPageObjects }) {
async triggerSingleRefresh(refreshInterval) {
log.debug(`triggerSingleRefresh, refreshInterval: ${refreshInterval}`);
await PageObjects.header.resumeAutoRefresh();
await PageObjects.timePicker.resumeAutoRefresh();
log.debug('waiting to give time for refresh timer to fire');
await PageObjects.common.sleep(refreshInterval + (refreshInterval / 2));
await PageObjects.header.pauseAutoRefresh();
await PageObjects.timePicker.pauseAutoRefresh();
await PageObjects.header.waitUntilLoadingHasFinished();
}
}