diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.datapublicpluginstart.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.datapublicpluginstart.md index 7bae0bca701bf0..c7810b18c55a9e 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.datapublicpluginstart.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.datapublicpluginstart.md @@ -20,6 +20,7 @@ export interface DataPublicPluginStart | [autocomplete](./kibana-plugin-plugins-data-public.datapublicpluginstart.autocomplete.md) | AutocompleteStart | autocomplete service [AutocompleteStart](./kibana-plugin-plugins-data-public.autocompletestart.md) | | [fieldFormats](./kibana-plugin-plugins-data-public.datapublicpluginstart.fieldformats.md) | FieldFormatsStart | field formats service [FieldFormatsStart](./kibana-plugin-plugins-data-public.fieldformatsstart.md) | | [indexPatterns](./kibana-plugin-plugins-data-public.datapublicpluginstart.indexpatterns.md) | IndexPatternsContract | index patterns service [IndexPatternsContract](./kibana-plugin-plugins-data-public.indexpatternscontract.md) | +| [nowProvider](./kibana-plugin-plugins-data-public.datapublicpluginstart.nowprovider.md) | NowProviderPublicContract | | | [query](./kibana-plugin-plugins-data-public.datapublicpluginstart.query.md) | QueryStart | query service [QueryStart](./kibana-plugin-plugins-data-public.querystart.md) | | [search](./kibana-plugin-plugins-data-public.datapublicpluginstart.search.md) | ISearchStart | search service [ISearchStart](./kibana-plugin-plugins-data-public.isearchstart.md) | | [ui](./kibana-plugin-plugins-data-public.datapublicpluginstart.ui.md) | DataPublicPluginStartUi | prewired UI components [DataPublicPluginStartUi](./kibana-plugin-plugins-data-public.datapublicpluginstartui.md) | diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.datapublicpluginstart.nowprovider.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.datapublicpluginstart.nowprovider.md new file mode 100644 index 00000000000000..4a93c25e288157 --- /dev/null +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.datapublicpluginstart.nowprovider.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [DataPublicPluginStart](./kibana-plugin-plugins-data-public.datapublicpluginstart.md) > [nowProvider](./kibana-plugin-plugins-data-public.datapublicpluginstart.nowprovider.md) + +## DataPublicPluginStart.nowProvider property + +Signature: + +```typescript +nowProvider: NowProviderPublicContract; +``` diff --git a/src/plugins/dashboard/public/application/dashboard_app.tsx b/src/plugins/dashboard/public/application/dashboard_app.tsx index f33383427342bd..8f4bc8bc6ef1aa 100644 --- a/src/plugins/dashboard/public/application/dashboard_app.tsx +++ b/src/plugins/dashboard/public/application/dashboard_app.tsx @@ -173,10 +173,14 @@ export function DashboardApp({ ).subscribe(() => refreshDashboardContainer()) ); subscriptions.add( - data.search.session.onRefresh$.subscribe(() => { + merge( + data.search.session.onRefresh$, + data.query.timefilter.timefilter.getAutoRefreshFetch$() + ).subscribe(() => { setLastReloadTime(() => new Date().getTime()); }) ); + dashboardStateManager.registerChangeListener(() => { // we aren't checking dirty state because there are changes the container needs to know about // that won't make the dashboard "dirty" - like a view mode change. diff --git a/src/plugins/dashboard/public/application/lib/session_restoration.ts b/src/plugins/dashboard/public/application/lib/session_restoration.ts index 5f05fa122e161c..890b81b5418be2 100644 --- a/src/plugins/dashboard/public/application/lib/session_restoration.ts +++ b/src/plugins/dashboard/public/application/lib/session_restoration.ts @@ -43,17 +43,22 @@ function getUrlGeneratorState({ data, getAppState, getDashboardId, - forceAbsoluteTime, // TODO: not implemented + forceAbsoluteTime, }: { data: DataPublicPluginStart; getAppState: () => DashboardAppState; getDashboardId: () => string; + /** + * Can force time range from time filter to convert from relative to absolute time range + */ forceAbsoluteTime: boolean; }): DashboardUrlGeneratorState { const appState = getAppState(); return { dashboardId: getDashboardId(), - timeRange: data.query.timefilter.timefilter.getTime(), + timeRange: forceAbsoluteTime + ? data.query.timefilter.timefilter.getAbsoluteTime() + : data.query.timefilter.timefilter.getTime(), filters: data.query.filterManager.getFilters(), query: data.query.queryString.formatQuery(appState.query), savedQuery: appState.savedQuery, diff --git a/src/plugins/data/common/query/timefilter/get_time.test.ts b/src/plugins/data/common/query/timefilter/get_time.test.ts index 4dba157a6f5546..5b771531367610 100644 --- a/src/plugins/data/common/query/timefilter/get_time.test.ts +++ b/src/plugins/data/common/query/timefilter/get_time.test.ts @@ -19,7 +19,7 @@ import moment from 'moment'; import sinon from 'sinon'; -import { getTime } from './get_time'; +import { getTime, getAbsoluteTimeRange } from './get_time'; describe('get_time', () => { describe('getTime', () => { @@ -90,4 +90,19 @@ describe('get_time', () => { clock.restore(); }); }); + describe('getAbsoluteTimeRange', () => { + test('should forward absolute timerange as is', () => { + const from = '2000-01-01T00:00:00.000Z'; + const to = '2000-02-01T00:00:00.000Z'; + expect(getAbsoluteTimeRange({ from, to })).toEqual({ from, to }); + }); + + test('should convert relative to absolute', () => { + const clock = sinon.useFakeTimers(moment.utc([2000, 1, 0, 0, 0, 0, 0]).valueOf()); + const from = '2000-01-01T00:00:00.000Z'; + const to = moment.utc(clock.now).toISOString(); + expect(getAbsoluteTimeRange({ from, to: 'now' })).toEqual({ from, to }); + clock.restore(); + }); + }); }); diff --git a/src/plugins/data/common/query/timefilter/get_time.ts b/src/plugins/data/common/query/timefilter/get_time.ts index 6e4eda95accc73..bb7b5760240b7c 100644 --- a/src/plugins/data/common/query/timefilter/get_time.ts +++ b/src/plugins/data/common/query/timefilter/get_time.ts @@ -34,6 +34,17 @@ export function calculateBounds( }; } +export function getAbsoluteTimeRange( + timeRange: TimeRange, + { forceNow }: { forceNow?: Date } = {} +): TimeRange { + const { min, max } = calculateBounds(timeRange, { forceNow }); + return { + from: min ? min.toISOString() : timeRange.from, + to: max ? max.toISOString() : timeRange.to, + }; +} + export function getTime( indexPattern: IIndexPattern | undefined, timeRange: TimeRange, diff --git a/src/plugins/data/common/search/expressions/esaggs/esaggs_fn.ts b/src/plugins/data/common/search/expressions/esaggs/esaggs_fn.ts index 2274fcfd6b8d5d..e9bf03af192ba6 100644 --- a/src/plugins/data/common/search/expressions/esaggs/esaggs_fn.ts +++ b/src/plugins/data/common/search/expressions/esaggs/esaggs_fn.ts @@ -62,6 +62,7 @@ export interface EsaggsStartDependencies { deserializeFieldFormat: FormatFactory; indexPatterns: IndexPatternsContract; searchSource: ISearchStartSearchSource; + getNow?: () => Date; } /** @internal */ @@ -118,7 +119,8 @@ export async function handleEsaggsRequest( args: Arguments, params: RequestHandlerParams ): Promise { - const resolvedTimeRange = input?.timeRange && calculateBounds(input.timeRange); + const resolvedTimeRange = + input?.timeRange && calculateBounds(input.timeRange, { forceNow: params.getNow?.() }); const response = await handleRequest(params); diff --git a/src/plugins/data/common/search/expressions/esaggs/request_handler.ts b/src/plugins/data/common/search/expressions/esaggs/request_handler.ts index b773aad67c3f89..29a841b18706c0 100644 --- a/src/plugins/data/common/search/expressions/esaggs/request_handler.ts +++ b/src/plugins/data/common/search/expressions/esaggs/request_handler.ts @@ -51,6 +51,7 @@ export interface RequestHandlerParams { searchSourceService: ISearchStartSearchSource; timeFields?: string[]; timeRange?: TimeRange; + getNow?: () => Date; } export const handleRequest = async ({ @@ -67,7 +68,9 @@ export const handleRequest = async ({ searchSourceService, timeFields, timeRange, + getNow, }: RequestHandlerParams) => { + const forceNow = getNow?.(); const searchSource = await searchSourceService.create(); searchSource.setField('index', indexPattern); @@ -115,7 +118,7 @@ export const handleRequest = async ({ if (timeRange && allTimeFields.length > 0) { timeFilterSearchSource.setField('filter', () => { return allTimeFields - .map((fieldName) => getTime(indexPattern, timeRange, { fieldName })) + .map((fieldName) => getTime(indexPattern, timeRange, { fieldName, forceNow })) .filter(isRangeFilter); }); } @@ -183,7 +186,7 @@ export const handleRequest = async ({ } } - const parsedTimeRange = timeRange ? calculateBounds(timeRange) : null; + const parsedTimeRange = timeRange ? calculateBounds(timeRange, { forceNow }) : null; const tabifyParams = { metricsAtAllLevels, partialRows, diff --git a/src/plugins/data/public/mocks.ts b/src/plugins/data/public/mocks.ts index 67c1ff7e09dd74..c34139caa553e5 100644 --- a/src/plugins/data/public/mocks.ts +++ b/src/plugins/data/public/mocks.ts @@ -22,6 +22,7 @@ import { fieldFormatsServiceMock } from './field_formats/mocks'; import { searchServiceMock } from './search/mocks'; import { queryServiceMock } from './query/mocks'; import { AutocompleteStart, AutocompleteSetup } from './autocomplete'; +import { createNowProviderMock } from './now_provider/mocks'; export type Setup = jest.Mocked>; export type Start = jest.Mocked>; @@ -76,6 +77,7 @@ const createStartContract = (): Start => { get: jest.fn().mockReturnValue(Promise.resolve({})), clearCache: jest.fn(), } as unknown) as IndexPatternsContract, + nowProvider: createNowProviderMock(), }; }; diff --git a/src/plugins/data/public/now_provider/index.ts b/src/plugins/data/public/now_provider/index.ts new file mode 100644 index 00000000000000..662ab87dd534b6 --- /dev/null +++ b/src/plugins/data/public/now_provider/index.ts @@ -0,0 +1,24 @@ +/* + * 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 { + NowProvider, + NowProviderInternalContract, + NowProviderPublicContract, +} from './now_provider'; diff --git a/src/plugins/data/public/now_provider/lib/get_force_now_from_url.test.ts b/src/plugins/data/public/now_provider/lib/get_force_now_from_url.test.ts new file mode 100644 index 00000000000000..4e3233474b040f --- /dev/null +++ b/src/plugins/data/public/now_provider/lib/get_force_now_from_url.test.ts @@ -0,0 +1,50 @@ +/* + * 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. + */ + +import { getForceNowFromUrl } from './get_force_now_from_url'; +const originalLocation = window.location; +afterAll(() => { + window.location = originalLocation; +}); + +function mockLocation(url: string) { + // @ts-ignore + delete window.location; + // @ts-ignore + window.location = new URL(url); +} + +test('should get force now from URL', () => { + const dateString = '1999-01-01T00:00:00.000Z'; + mockLocation(`https://elastic.co/?forceNow=${dateString}`); + + expect(getForceNowFromUrl()).toEqual(new Date(dateString)); +}); + +test('should throw if force now is invalid', () => { + const dateString = 'invalid-date'; + mockLocation(`https://elastic.co/?forceNow=${dateString}`); + + expect(() => getForceNowFromUrl()).toThrowError(); +}); + +test('should return undefined if no forceNow', () => { + mockLocation(`https://elastic.co/`); + expect(getForceNowFromUrl()).toBe(undefined); +}); diff --git a/src/plugins/data/public/query/timefilter/lib/parse_querystring.ts b/src/plugins/data/public/now_provider/lib/get_force_now_from_url.ts similarity index 74% rename from src/plugins/data/public/query/timefilter/lib/parse_querystring.ts rename to src/plugins/data/public/now_provider/lib/get_force_now_from_url.ts index 5982bfd0bd276d..906eec1ab143e2 100644 --- a/src/plugins/data/public/query/timefilter/lib/parse_querystring.ts +++ b/src/plugins/data/public/now_provider/lib/get_force_now_from_url.ts @@ -16,10 +16,25 @@ * specific language governing permissions and limitations * under the License. */ + import { parse } from 'query-string'; /** @internal */ -export function parseQueryString() { +export function getForceNowFromUrl(): Date | undefined { + const forceNow = parseQueryString().forceNow as string; + if (!forceNow) { + return; + } + + const ts = Date.parse(forceNow); + if (isNaN(ts)) { + throw new Error(`forceNow query parameter, ${forceNow}, can't be parsed by Date.parse`); + } + return new Date(ts); +} + +/** @internal */ +function parseQueryString() { // window.location.search is an empty string // get search from href const hrefSplit = window.location.href.split('?'); diff --git a/src/plugins/data/public/now_provider/lib/index.ts b/src/plugins/data/public/now_provider/lib/index.ts new file mode 100644 index 00000000000000..990db5ae3c77cb --- /dev/null +++ b/src/plugins/data/public/now_provider/lib/index.ts @@ -0,0 +1,20 @@ +/* + * 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 { getForceNowFromUrl } from './get_force_now_from_url'; diff --git a/src/plugins/data/public/query/timefilter/lib/get_force_now.ts b/src/plugins/data/public/now_provider/mocks.ts similarity index 67% rename from src/plugins/data/public/query/timefilter/lib/get_force_now.ts rename to src/plugins/data/public/now_provider/mocks.ts index fe68656f0c3aaf..1021d2b48cfe53 100644 --- a/src/plugins/data/public/query/timefilter/lib/get_force_now.ts +++ b/src/plugins/data/public/now_provider/mocks.ts @@ -17,18 +17,12 @@ * under the License. */ -import { parseQueryString } from './parse_querystring'; +import { NowProviderInternalContract } from './now_provider'; -/** @internal */ -export function getForceNow() { - const forceNow = parseQueryString().forceNow as string; - if (!forceNow) { - return; - } - - const ticks = Date.parse(forceNow); - if (isNaN(ticks)) { - throw new Error(`forceNow query parameter, ${forceNow}, can't be parsed by Date.parse`); - } - return new Date(ticks); -} +export const createNowProviderMock = (): jest.Mocked => { + return { + get: jest.fn(() => new Date()), + set: jest.fn(), + reset: jest.fn(), + }; +}; diff --git a/src/plugins/data/public/now_provider/now_provider.test.ts b/src/plugins/data/public/now_provider/now_provider.test.ts new file mode 100644 index 00000000000000..e557065ff2b677 --- /dev/null +++ b/src/plugins/data/public/now_provider/now_provider.test.ts @@ -0,0 +1,55 @@ +/* + * 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. + */ + +import { NowProvider, NowProviderInternalContract } from './now_provider'; + +let mockDateFromUrl: undefined | Date; +let nowProvider: NowProviderInternalContract; + +jest.mock('./lib', () => ({ + // @ts-ignore + ...jest.requireActual('./lib'), + getForceNowFromUrl: () => mockDateFromUrl, +})); + +beforeEach(() => { + nowProvider = new NowProvider(); +}); +afterEach(() => { + mockDateFromUrl = undefined; +}); + +test('should return Date.now() by default', async () => { + const now = Date.now(); + await new Promise((r) => setTimeout(r, 10)); + expect(nowProvider.get().getTime()).toBeGreaterThan(now); +}); + +test('should forceNow from URL', async () => { + mockDateFromUrl = new Date('1999-01-01T00:00:00.000Z'); + nowProvider = new NowProvider(); + expect(nowProvider.get()).toEqual(mockDateFromUrl); +}); + +test('should forceNow from URL if custom now is set', async () => { + mockDateFromUrl = new Date('1999-01-01T00:00:00.000Z'); + nowProvider = new NowProvider(); + nowProvider.set(new Date()); + expect(nowProvider.get()).toEqual(mockDateFromUrl); +}); diff --git a/src/plugins/data/public/now_provider/now_provider.ts b/src/plugins/data/public/now_provider/now_provider.ts new file mode 100644 index 00000000000000..a55fa691ef1dae --- /dev/null +++ b/src/plugins/data/public/now_provider/now_provider.ts @@ -0,0 +1,50 @@ +/* + * 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. + */ + +import { PublicMethodsOf } from '@kbn/utility-types'; +import { getForceNowFromUrl } from './lib'; + +export type NowProviderInternalContract = PublicMethodsOf; +export type NowProviderPublicContract = Pick; + +/** + * Used to synchronize time between parallel searches with relative time range that rely on `now`. + */ +export class NowProvider { + // TODO: service shouldn't access params in the URL + // instead it should be handled by apps + private readonly nowFromUrl = getForceNowFromUrl(); + private now?: Date; + + constructor() {} + + get(): Date { + if (this.nowFromUrl) return this.nowFromUrl; // now forced from URL always takes precedence + if (this.now) return this.now; + return new Date(); + } + + set(now: Date) { + this.now = now; + } + + reset() { + this.now = undefined; + } +} diff --git a/src/plugins/data/public/plugin.ts b/src/plugins/data/public/plugin.ts index 0d94fd3b2ef3bb..b033763ff661ad 100644 --- a/src/plugins/data/public/plugin.ts +++ b/src/plugins/data/public/plugin.ts @@ -62,6 +62,7 @@ import { SavedObjectsClientPublicToCommon } from './index_patterns'; import { getIndexPatternLoad } from './index_patterns/expressions'; import { UsageCollectionSetup } from '../../usage_collection/public'; import { getTableViewDescription } from './utils/table_inspector_view'; +import { NowProvider, NowProviderInternalContract } from './now_provider'; export class DataPublicPlugin implements @@ -77,6 +78,7 @@ export class DataPublicPlugin private readonly queryService: QueryService; private readonly storage: IStorageWrapper; private usageCollection: UsageCollectionSetup | undefined; + private readonly nowProvider: NowProviderInternalContract; constructor(initializerContext: PluginInitializerContext) { this.searchService = new SearchService(initializerContext); @@ -84,6 +86,7 @@ export class DataPublicPlugin this.fieldFormatsService = new FieldFormatsService(); this.autocomplete = new AutocompleteService(initializerContext); this.storage = new Storage(window.localStorage); + this.nowProvider = new NowProvider(); } public setup( @@ -96,9 +99,17 @@ export class DataPublicPlugin this.usageCollection = usageCollection; + const searchService = this.searchService.setup(core, { + bfetch, + usageCollection, + expressions, + nowProvider: this.nowProvider, + }); + const queryService = this.queryService.setup({ uiSettings: core.uiSettings, storage: this.storage, + nowProvider: this.nowProvider, }); uiActions.registerTrigger(applyFilterTrigger); @@ -121,12 +132,6 @@ export class DataPublicPlugin })) ); - const searchService = this.searchService.setup(core, { - bfetch, - usageCollection, - expressions, - }); - inspector.registerView( getTableViewDescription(() => ({ uiActions: startServices().plugins.uiActions, @@ -200,6 +205,7 @@ export class DataPublicPlugin indexPatterns, query, search, + nowProvider: this.nowProvider, }; const SearchBar = createSearchBar({ diff --git a/src/plugins/data/public/public.api.md b/src/plugins/data/public/public.api.md index 04a63c988ead05..939b5967118797 100644 --- a/src/plugins/data/public/public.api.md +++ b/src/plugins/data/public/public.api.md @@ -580,6 +580,10 @@ export interface DataPublicPluginStart { autocomplete: AutocompleteStart; fieldFormats: FieldFormatsStart; indexPatterns: IndexPatternsContract; + // Warning: (ae-forgotten-export) The symbol "NowProviderPublicContract" needs to be exported by the entry point index.d.ts + // + // (undocumented) + nowProvider: NowProviderPublicContract; query: QueryStart; search: ISearchStart; ui: DataPublicPluginStartUi; @@ -2628,7 +2632,7 @@ export const UI_SETTINGS: { // src/plugins/data/public/index.ts:433:1 - (ae-forgotten-export) The symbol "propFilter" needs to be exported by the entry point index.d.ts // src/plugins/data/public/index.ts:436:1 - (ae-forgotten-export) The symbol "toAbsoluteDates" needs to be exported by the entry point index.d.ts // src/plugins/data/public/query/state_sync/connect_to_query_state.ts:45:5 - (ae-forgotten-export) The symbol "FilterStateStore" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/search/session/session_service.ts:50:5 - (ae-forgotten-export) The symbol "UrlGeneratorStateMapping" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/search/session/session_service.ts:51:5 - (ae-forgotten-export) The symbol "UrlGeneratorStateMapping" needs to be exported by the entry point index.d.ts // (No @packageDocumentation comment for this package) diff --git a/src/plugins/data/public/query/query_service.ts b/src/plugins/data/public/query/query_service.ts index fe7fdcbb1d113c..e5f681549b06ee 100644 --- a/src/plugins/data/public/query/query_service.ts +++ b/src/plugins/data/public/query/query_service.ts @@ -28,6 +28,7 @@ import { createQueryStateObservable } from './state_sync/create_global_query_obs import { QueryStringManager, QueryStringContract } from './query_string'; import { buildEsQuery, getEsQueryConfig } from '../../common'; import { getUiSettings } from '../services'; +import { NowProviderInternalContract } from '../now_provider'; import { IndexPattern } from '..'; /** @@ -38,6 +39,7 @@ import { IndexPattern } from '..'; interface QueryServiceSetupDependencies { storage: IStorageWrapper; uiSettings: IUiSettingsClient; + nowProvider: NowProviderInternalContract; } interface QueryServiceStartDependencies { @@ -53,10 +55,10 @@ export class QueryService { state$!: ReturnType; - public setup({ storage, uiSettings }: QueryServiceSetupDependencies) { + public setup({ storage, uiSettings, nowProvider }: QueryServiceSetupDependencies) { this.filterManager = new FilterManager(uiSettings); - const timefilterService = new TimefilterService(); + const timefilterService = new TimefilterService(nowProvider); this.timefilter = timefilterService.setup({ uiSettings, storage, diff --git a/src/plugins/data/public/query/state_sync/connect_to_query_state.test.ts b/src/plugins/data/public/query/state_sync/connect_to_query_state.test.ts index c970dd521c142b..3ad1f3daa992ba 100644 --- a/src/plugins/data/public/query/state_sync/connect_to_query_state.test.ts +++ b/src/plugins/data/public/query/state_sync/connect_to_query_state.test.ts @@ -28,6 +28,7 @@ import { StubBrowserStorage } from '@kbn/test/jest'; import { connectToQueryState } from './connect_to_query_state'; import { TimefilterContract } from '../timefilter'; import { QueryState } from './types'; +import { createNowProviderMock } from '../../now_provider/mocks'; const connectToQueryGlobalState = (query: QueryStart, state: BaseStateContainer) => connectToQueryState(query, state, { @@ -79,6 +80,7 @@ describe('connect_to_global_state', () => { queryService.setup({ uiSettings: setupMock.uiSettings, storage: new Storage(new StubBrowserStorage()), + nowProvider: createNowProviderMock(), }); queryServiceStart = queryService.start({ uiSettings: setupMock.uiSettings, @@ -312,6 +314,7 @@ describe('connect_to_app_state', () => { queryService.setup({ uiSettings: setupMock.uiSettings, storage: new Storage(new StubBrowserStorage()), + nowProvider: createNowProviderMock(), }); queryServiceStart = queryService.start({ uiSettings: setupMock.uiSettings, @@ -490,6 +493,7 @@ describe('filters with different state', () => { queryService.setup({ uiSettings: setupMock.uiSettings, storage: new Storage(new StubBrowserStorage()), + nowProvider: createNowProviderMock(), }); queryServiceStart = queryService.start({ uiSettings: setupMock.uiSettings, diff --git a/src/plugins/data/public/query/state_sync/sync_state_with_url.test.ts b/src/plugins/data/public/query/state_sync/sync_state_with_url.test.ts index 47af09bfc7c0ee..129a145ee06938 100644 --- a/src/plugins/data/public/query/state_sync/sync_state_with_url.test.ts +++ b/src/plugins/data/public/query/state_sync/sync_state_with_url.test.ts @@ -33,6 +33,7 @@ import { StubBrowserStorage } from '@kbn/test/jest'; import { TimefilterContract } from '../timefilter'; import { syncQueryStateWithUrl } from './sync_state_with_url'; import { QueryState } from './types'; +import { createNowProviderMock } from '../../now_provider/mocks'; const setupMock = coreMock.createSetup(); const startMock = coreMock.createStart(); @@ -73,6 +74,7 @@ describe('sync_query_state_with_url', () => { queryService.setup({ uiSettings: setupMock.uiSettings, storage: new Storage(new StubBrowserStorage()), + nowProvider: createNowProviderMock(), }); queryServiceStart = queryService.start({ uiSettings: startMock.uiSettings, diff --git a/src/plugins/data/public/query/timefilter/timefilter.test.ts b/src/plugins/data/public/query/timefilter/timefilter.test.ts index 6c1a4eff786f60..d8dfed002ea809 100644 --- a/src/plugins/data/public/query/timefilter/timefilter.test.ts +++ b/src/plugins/data/public/query/timefilter/timefilter.test.ts @@ -19,21 +19,12 @@ jest.useFakeTimers(); -jest.mock('./lib/parse_querystring', () => ({ - parseQueryString: () => { - return { - // Can not access local variable from within a mock - // @ts-ignore - forceNow: global.nowTime, - }; - }, -})); - import sinon from 'sinon'; import moment from 'moment'; import { Timefilter } from './timefilter'; import { Subscription } from 'rxjs'; import { TimeRange, RefreshInterval } from '../../../common'; +import { createNowProviderMock } from '../../now_provider/mocks'; import { timefilterServiceMock } from './timefilter_service.mock'; const timefilterSetupMock = timefilterServiceMock.createSetupContract(); @@ -42,16 +33,16 @@ const timefilterConfig = { timeDefaults: { from: 'now-15m', to: 'now' }, refreshIntervalDefaults: { pause: false, value: 0 }, }; -const timefilter = new Timefilter(timefilterConfig, timefilterSetupMock.history); + +const nowProviderMock = createNowProviderMock(); +const timefilter = new Timefilter(timefilterConfig, timefilterSetupMock.history, nowProviderMock); function stubNowTime(nowTime: any) { - // @ts-ignore - global.nowTime = nowTime; + nowProviderMock.get.mockImplementation(() => (nowTime ? new Date(nowTime) : new Date())); } function clearNowTimeStub() { - // @ts-ignore - delete global.nowTime; + nowProviderMock.get.mockReset(); } test('isTimeTouched is initially set to false', () => { diff --git a/src/plugins/data/public/query/timefilter/timefilter.ts b/src/plugins/data/public/query/timefilter/timefilter.ts index 7278ceaaddcced..3ee2a3962e8fff 100644 --- a/src/plugins/data/public/query/timefilter/timefilter.ts +++ b/src/plugins/data/public/query/timefilter/timefilter.ts @@ -22,10 +22,11 @@ import { Subject, BehaviorSubject } from 'rxjs'; import moment from 'moment'; import { PublicMethodsOf } from '@kbn/utility-types'; import { areRefreshIntervalsDifferent, areTimeRangesDifferent } from './lib/diff_time_picker_vals'; -import { getForceNow } from './lib/get_force_now'; import { TimefilterConfig, InputTimeRange, TimeRangeBounds } from './types'; +import { NowProviderInternalContract } from '../../now_provider'; import { calculateBounds, + getAbsoluteTimeRange, getTime, IIndexPattern, RefreshInterval, @@ -60,7 +61,11 @@ export class Timefilter { private readonly timeDefaults: TimeRange; private readonly refreshIntervalDefaults: RefreshInterval; - constructor(config: TimefilterConfig, timeHistory: TimeHistoryContract) { + constructor( + config: TimefilterConfig, + timeHistory: TimeHistoryContract, + private readonly nowProvider: NowProviderInternalContract + ) { this._history = timeHistory; this.timeDefaults = config.timeDefaults; this.refreshIntervalDefaults = config.refreshIntervalDefaults; @@ -109,6 +114,13 @@ export class Timefilter { }; }; + /** + * Same as {@link getTime}, but also converts relative time range to absolute time range + */ + public getAbsoluteTime() { + return getAbsoluteTimeRange(this._time, { forceNow: this.nowProvider.get() }); + } + /** * Updates timefilter time. * Emits 'timeUpdate' and 'fetch' events when time changes @@ -177,7 +189,7 @@ export class Timefilter { public createFilter = (indexPattern: IIndexPattern, timeRange?: TimeRange) => { return getTime(indexPattern, timeRange ? timeRange : this._time, { - forceNow: this.getForceNow(), + forceNow: this.nowProvider.get(), }); }; @@ -186,7 +198,7 @@ export class Timefilter { } public calculateBounds(timeRange: TimeRange): TimeRangeBounds { - return calculateBounds(timeRange, { forceNow: this.getForceNow() }); + return calculateBounds(timeRange, { forceNow: this.nowProvider.get() }); } public getActiveBounds(): TimeRangeBounds | undefined { @@ -234,10 +246,6 @@ export class Timefilter { public getRefreshIntervalDefaults(): RefreshInterval { return _.cloneDeep(this.refreshIntervalDefaults); } - - private getForceNow = () => { - return getForceNow(); - }; } export type TimefilterContract = PublicMethodsOf; diff --git a/src/plugins/data/public/query/timefilter/timefilter_service.mock.ts b/src/plugins/data/public/query/timefilter/timefilter_service.mock.ts index 9f1c64a1739a5f..72ff49b9178fca 100644 --- a/src/plugins/data/public/query/timefilter/timefilter_service.mock.ts +++ b/src/plugins/data/public/query/timefilter/timefilter_service.mock.ts @@ -46,6 +46,7 @@ const createSetupContractMock = () => { createFilter: jest.fn(), getRefreshIntervalDefaults: jest.fn(), getTimeDefaults: jest.fn(), + getAbsoluteTime: jest.fn(), }; const historyMock: jest.Mocked = { diff --git a/src/plugins/data/public/query/timefilter/timefilter_service.ts b/src/plugins/data/public/query/timefilter/timefilter_service.ts index 35b46de5f21b25..1f0c0345a0eae5 100644 --- a/src/plugins/data/public/query/timefilter/timefilter_service.ts +++ b/src/plugins/data/public/query/timefilter/timefilter_service.ts @@ -21,6 +21,7 @@ import { IUiSettingsClient } from 'src/core/public'; import { IStorageWrapper } from 'src/plugins/kibana_utils/public'; import { TimeHistory, Timefilter, TimeHistoryContract, TimefilterContract } from './index'; import { UI_SETTINGS } from '../../../common'; +import { NowProviderInternalContract } from '../../now_provider'; /** * Filter Service @@ -33,13 +34,15 @@ export interface TimeFilterServiceDependencies { } export class TimefilterService { + constructor(private readonly nowProvider: NowProviderInternalContract) {} + public setup({ uiSettings, storage }: TimeFilterServiceDependencies): TimefilterSetup { const timefilterConfig = { timeDefaults: uiSettings.get(UI_SETTINGS.TIMEPICKER_TIME_DEFAULTS), refreshIntervalDefaults: uiSettings.get(UI_SETTINGS.TIMEPICKER_REFRESH_INTERVAL_DEFAULTS), }; const history = new TimeHistory(storage); - const timefilter = new Timefilter(timefilterConfig, history); + const timefilter = new Timefilter(timefilterConfig, history, this.nowProvider); return { timefilter, diff --git a/src/plugins/data/public/search/aggs/aggs_service.test.ts b/src/plugins/data/public/search/aggs/aggs_service.test.ts index bc4992384b0c21..86a466b6997106 100644 --- a/src/plugins/data/public/search/aggs/aggs_service.test.ts +++ b/src/plugins/data/public/search/aggs/aggs_service.test.ts @@ -31,6 +31,7 @@ import { AggsStartDependencies, createGetConfig, } from './aggs_service'; +import { createNowProviderMock } from '../../now_provider/mocks'; const { uiSettings } = coreMock.createSetup(); @@ -44,6 +45,7 @@ describe('AggsService - public', () => { setupDeps = { registerFunction: expressionsPluginMock.createSetupContract().registerFunction, uiSettings, + nowProvider: createNowProviderMock(), }; startDeps = { fieldFormats: fieldFormatsServiceMock.createStartContract(), diff --git a/src/plugins/data/public/search/aggs/aggs_service.ts b/src/plugins/data/public/search/aggs/aggs_service.ts index 7b5edac0280d9d..59032a8608632f 100644 --- a/src/plugins/data/public/search/aggs/aggs_service.ts +++ b/src/plugins/data/public/search/aggs/aggs_service.ts @@ -22,7 +22,6 @@ import { Subscription } from 'rxjs'; import { IUiSettingsClient } from 'src/core/public'; import { ExpressionsServiceSetup } from 'src/plugins/expressions/common'; import { FieldFormatsStart } from '../../field_formats'; -import { getForceNow } from '../../query/timefilter/lib/get_force_now'; import { calculateBounds, TimeRange } from '../../../common'; import { aggsRequiredUiSettings, @@ -33,6 +32,7 @@ import { } from '../../../common/search/aggs'; import { AggsSetup, AggsStart } from './types'; import { IndexPatternsContract } from '../../index_patterns'; +import { NowProviderInternalContract } from '../../now_provider'; /** * Aggs needs synchronous access to specific uiSettings. Since settings can change @@ -63,6 +63,7 @@ export function createGetConfig( export interface AggsSetupDependencies { registerFunction: ExpressionsServiceSetup['registerFunction']; uiSettings: IUiSettingsClient; + nowProvider: NowProviderInternalContract; } /** @internal */ @@ -82,15 +83,17 @@ export class AggsService { private readonly initializedAggTypes = new Map(); private getConfig?: AggsCommonStartDependencies['getConfig']; private subscriptions: Subscription[] = []; + private nowProvider!: NowProviderInternalContract; /** - * getForceNow uses window.location, so we must have a separate implementation + * NowGetter uses window.location, so we must have a separate implementation * of calculateBounds on the client and the server. */ private calculateBounds = (timeRange: TimeRange) => - calculateBounds(timeRange, { forceNow: getForceNow() }); + calculateBounds(timeRange, { forceNow: this.nowProvider.get() }); - public setup({ registerFunction, uiSettings }: AggsSetupDependencies): AggsSetup { + public setup({ registerFunction, uiSettings, nowProvider }: AggsSetupDependencies): AggsSetup { + this.nowProvider = nowProvider; this.getConfig = createGetConfig(uiSettings, aggsRequiredUiSettings, this.subscriptions); return this.aggsCommonService.setup({ registerFunction }); diff --git a/src/plugins/data/public/search/expressions/esaggs.ts b/src/plugins/data/public/search/expressions/esaggs.ts index d8d90ea464a73a..63138ee1b0454e 100644 --- a/src/plugins/data/public/search/expressions/esaggs.ts +++ b/src/plugins/data/public/search/expressions/esaggs.ts @@ -53,6 +53,7 @@ export function getFunctionDefinition({ deserializeFieldFormat, indexPatterns, searchSource, + getNow, } = await getStartDependencies(); const indexPattern = await indexPatterns.create(args.index.value, true); @@ -75,6 +76,7 @@ export function getFunctionDefinition({ searchSourceService: searchSource, timeFields: args.timeFields, timeRange: get(input, 'timeRange', undefined), + getNow, }); }, }); @@ -102,12 +104,13 @@ export function getEsaggs({ return getFunctionDefinition({ getStartDependencies: async () => { const [, , self] = await getStartServices(); - const { fieldFormats, indexPatterns, search } = self; + const { fieldFormats, indexPatterns, search, nowProvider } = self; return { aggs: search.aggs, deserializeFieldFormat: fieldFormats.deserialize.bind(fieldFormats), indexPatterns, searchSource: search.searchSource, + getNow: () => nowProvider.get(), }; }, }); diff --git a/src/plugins/data/public/search/search_service.ts b/src/plugins/data/public/search/search_service.ts index 1c49de8f0ff4ba..eef6190fe78c59 100644 --- a/src/plugins/data/public/search/search_service.ts +++ b/src/plugins/data/public/search/search_service.ts @@ -53,12 +53,14 @@ import { } from '../../common/search/aggs/buckets/shard_delay'; import { aggShardDelay } from '../../common/search/aggs/buckets/shard_delay_fn'; import { DataPublicPluginStart, DataStartDependencies } from '../types'; +import { NowProviderInternalContract } from '../now_provider'; /** @internal */ export interface SearchServiceSetupDependencies { bfetch: BfetchPublicSetup; expressions: ExpressionsSetup; usageCollection?: UsageCollectionSetup; + nowProvider: NowProviderInternalContract; } /** @internal */ @@ -79,7 +81,7 @@ export class SearchService implements Plugin { public setup( { http, getStartServices, notifications, uiSettings }: CoreSetup, - { bfetch, expressions, usageCollection }: SearchServiceSetupDependencies + { bfetch, expressions, usageCollection, nowProvider }: SearchServiceSetupDependencies ): ISearchSetup { this.usageCollector = createUsageCollector(getStartServices, usageCollection); @@ -87,7 +89,8 @@ export class SearchService implements Plugin { this.sessionService = new SessionService( this.initializerContext, getStartServices, - this.sessionsClient + this.sessionsClient, + nowProvider ); /** * A global object that intercepts all searches and provides convenience methods for cancelling @@ -118,6 +121,7 @@ export class SearchService implements Plugin { const aggs = this.aggsService.setup({ registerFunction: expressions.registerFunction, uiSettings, + nowProvider, }); if (this.initializerContext.config.get().search.aggs.shardDelay.enabled) { diff --git a/src/plugins/data/public/search/session/search_session_state.test.ts b/src/plugins/data/public/search/session/search_session_state.test.ts index 539fc8252b2a5f..c2cb75b9a74938 100644 --- a/src/plugins/data/public/search/session/search_session_state.test.ts +++ b/src/plugins/data/public/search/session/search_session_state.test.ts @@ -31,6 +31,7 @@ describe('Session state container', () => { state.transitions.start(); expect(state.selectors.getState()).toBe(SearchSessionState.None); expect(state.get().sessionId).not.toBeUndefined(); + expect(state.get().startTime).not.toBeUndefined(); }); test('track', () => { @@ -56,6 +57,7 @@ describe('Session state container', () => { state.transitions.clear(); expect(state.selectors.getState()).toBe(SearchSessionState.None); expect(state.get().sessionId).toBeUndefined(); + expect(state.get().startTime).toBeUndefined(); }); test('cancel', () => { diff --git a/src/plugins/data/public/search/session/search_session_state.ts b/src/plugins/data/public/search/session/search_session_state.ts index 7a35a65a600d75..0894de13d53a93 100644 --- a/src/plugins/data/public/search/session/search_session_state.ts +++ b/src/plugins/data/public/search/session/search_session_state.ts @@ -105,6 +105,11 @@ export interface SessionStateInternal { * If user has explicitly canceled search requests */ isCanceled: boolean; + + /** + * Start time of current session + */ + startTime?: Date; } const createSessionDefaultState: < @@ -132,7 +137,11 @@ export interface SessionPureTransitions< } export const sessionPureTransitions: SessionPureTransitions = { - start: (state) => () => ({ ...createSessionDefaultState(), sessionId: uuid.v4() }), + start: (state) => () => ({ + ...createSessionDefaultState(), + sessionId: uuid.v4(), + startTime: new Date(), + }), restore: (state) => (sessionId: string) => ({ ...createSessionDefaultState(), sessionId, @@ -216,6 +225,7 @@ export const createSessionStateContainer = ( ): { stateContainer: SessionStateContainer; sessionState$: Observable; + sessionStartTime$: Observable; } => { const stateContainer = createStateContainer( createSessionDefaultState(), @@ -229,8 +239,16 @@ export const createSessionStateContainer = ( distinctUntilChanged(), shareReplay(1) ); + + const sessionStartTime$: Observable = stateContainer.state$.pipe( + map(() => stateContainer.get().startTime), + distinctUntilChanged(), + shareReplay(1) + ); + return { stateContainer, sessionState$, + sessionStartTime$, }; }; diff --git a/src/plugins/data/public/search/session/session_service.test.ts b/src/plugins/data/public/search/session/session_service.test.ts index cf083239b15719..aeca7b4d63da7a 100644 --- a/src/plugins/data/public/search/session/session_service.test.ts +++ b/src/plugins/data/public/search/session/session_service.test.ts @@ -23,17 +23,22 @@ import { take, toArray } from 'rxjs/operators'; import { getSessionsClientMock } from './mocks'; import { BehaviorSubject } from 'rxjs'; import { SearchSessionState } from './search_session_state'; +import { createNowProviderMock } from '../../now_provider/mocks'; +import { NowProviderInternalContract } from '../../now_provider'; describe('Session service', () => { let sessionService: ISessionService; let state$: BehaviorSubject; + let nowProvider: jest.Mocked; beforeEach(() => { const initializerContext = coreMock.createPluginInitializerContext(); + nowProvider = createNowProviderMock(); sessionService = new SessionService( initializerContext, coreMock.createSetup().getStartServices, getSessionsClientMock(), + nowProvider, { freezeState: false } // needed to use mocks inside state container ); state$ = new BehaviorSubject(SearchSessionState.None); @@ -44,8 +49,10 @@ describe('Session service', () => { it('Creates and clears a session', async () => { sessionService.start(); expect(sessionService.getSessionId()).not.toBeUndefined(); + expect(nowProvider.set).toHaveBeenCalled(); sessionService.clear(); expect(sessionService.getSessionId()).toBeUndefined(); + expect(nowProvider.reset).toHaveBeenCalled(); }); it('Restores a session', async () => { diff --git a/src/plugins/data/public/search/session/session_service.ts b/src/plugins/data/public/search/session/session_service.ts index 2bbb762fcfe9fa..e2185d8148708c 100644 --- a/src/plugins/data/public/search/session/session_service.ts +++ b/src/plugins/data/public/search/session/session_service.ts @@ -29,6 +29,7 @@ import { SessionStateContainer, } from './search_session_state'; import { ISessionsClient } from './sessions_client'; +import { NowProviderInternalContract } from '../../now_provider'; export type ISessionService = PublicContract; @@ -60,40 +61,54 @@ export class SessionService { private readonly state: SessionStateContainer; private searchSessionInfoProvider?: SearchSessionInfoProvider; - private appChangeSubscription$?: Subscription; + private subscription = new Subscription(); private curApp?: string; constructor( initializerContext: PluginInitializerContext, getStartServices: StartServicesAccessor, private readonly sessionsClient: ISessionsClient, + private readonly nowProvider: NowProviderInternalContract, { freezeState = true }: { freezeState: boolean } = { freezeState: true } ) { - const { stateContainer, sessionState$ } = createSessionStateContainer({ + const { + stateContainer, + sessionState$, + sessionStartTime$, + } = createSessionStateContainer({ freeze: freezeState, }); this.state$ = sessionState$; this.state = stateContainer; + this.subscription.add( + sessionStartTime$.subscribe((startTime) => { + if (startTime) this.nowProvider.set(startTime); + else this.nowProvider.reset(); + }) + ); + getStartServices().then(([coreStart]) => { // Apps required to clean up their sessions before unmounting // Make sure that apps don't leave sessions open. - this.appChangeSubscription$ = coreStart.application.currentAppId$.subscribe((appName) => { - if (this.state.get().sessionId) { - const message = `Application '${this.curApp}' had an open session while navigating`; - if (initializerContext.env.mode.dev) { - // TODO: This setTimeout is necessary due to a race condition while navigating. - setTimeout(() => { - coreStart.fatalErrors.add(message); - }, 100); - } else { - // eslint-disable-next-line no-console - console.warn(message); - this.clear(); + this.subscription.add( + coreStart.application.currentAppId$.subscribe((appName) => { + if (this.state.get().sessionId) { + const message = `Application '${this.curApp}' had an open session while navigating`; + if (initializerContext.env.mode.dev) { + // TODO: This setTimeout is necessary due to a race condition while navigating. + setTimeout(() => { + coreStart.fatalErrors.add(message); + }, 100); + } else { + // eslint-disable-next-line no-console + console.warn(message); + this.clear(); + } } - } - this.curApp = appName; - }); + this.curApp = appName; + }) + ); }); } @@ -122,9 +137,7 @@ export class SessionService { } public destroy() { - if (this.appChangeSubscription$) { - this.appChangeSubscription$.unsubscribe(); - } + this.subscription.unsubscribe(); this.clear(); } diff --git a/src/plugins/data/public/types.ts b/src/plugins/data/public/types.ts index c7b66acfc6c7a5..e38cabe313b6c7 100644 --- a/src/plugins/data/public/types.ts +++ b/src/plugins/data/public/types.ts @@ -32,6 +32,7 @@ import { IndexPatternsContract } from './index_patterns'; import { IndexPatternSelectProps, StatefulSearchBarProps } from './ui'; import { UsageCollectionSetup } from '../../usage_collection/public'; import { Setup as InspectorSetup } from '../../inspector/public'; +import { NowProviderPublicContract } from './now_provider'; export interface DataPublicPluginEnhancements { search: SearchEnhancements; @@ -118,6 +119,8 @@ export interface DataPublicPluginStart { * {@link DataPublicPluginStartUi} */ ui: DataPublicPluginStartUi; + + nowProvider: NowProviderPublicContract; } export interface IDataPluginServices extends Partial { diff --git a/src/plugins/discover/public/application/angular/discover_state.ts b/src/plugins/discover/public/application/angular/discover_state.ts index f52bc8b49ba42b..9bf3cc587a3d8f 100644 --- a/src/plugins/discover/public/application/angular/discover_state.ts +++ b/src/plugins/discover/public/application/angular/discover_state.ts @@ -282,11 +282,14 @@ function createUrlGeneratorState({ appStateContainer, data, getSavedSearchId, - forceAbsoluteTime, // TODO: not implemented + forceAbsoluteTime, }: { appStateContainer: StateContainer; data: DataPublicPluginStart; getSavedSearchId: () => string | undefined; + /** + * Can force time range from time filter to convert from relative to absolute time range + */ forceAbsoluteTime: boolean; }): DiscoverUrlGeneratorState { const appState = appStateContainer.get(); @@ -295,7 +298,9 @@ function createUrlGeneratorState({ indexPatternId: appState.index, query: appState.query, savedSearchId: getSavedSearchId(), - timeRange: data.query.timefilter.timefilter.getTime(), // TODO: handle relative time range + timeRange: forceAbsoluteTime + ? data.query.timefilter.timefilter.getAbsoluteTime() + : data.query.timefilter.timefilter.getTime(), searchSessionId: data.search.session.getSessionId(), columns: appState.columns, sort: appState.sort, diff --git a/src/plugins/discover/public/application/embeddable/search_embeddable.ts b/src/plugins/discover/public/application/embeddable/search_embeddable.ts index e4a8ab7bc67ffa..6919dc1bef2861 100644 --- a/src/plugins/discover/public/application/embeddable/search_embeddable.ts +++ b/src/plugins/discover/public/application/embeddable/search_embeddable.ts @@ -29,7 +29,6 @@ import { Filter, TimeRange, FilterManager, - getTime, Query, IFieldType, } from '../../../../data/public'; @@ -98,7 +97,6 @@ export class SearchEmbeddable private panelTitle: string = ''; private filtersSearchSource?: ISearchSource; private searchInstance?: JQLite; - private autoRefreshFetchSubscription?: Subscription; private subscription?: Subscription; public readonly type = SEARCH_EMBEDDABLE_TYPE; private filterManager: FilterManager; @@ -148,10 +146,6 @@ export class SearchEmbeddable }; this.initializeSearchScope(); - this.autoRefreshFetchSubscription = this.services.timefilter - .getAutoRefreshFetch$() - .subscribe(this.fetch); - this.subscription = this.getUpdated$().subscribe(() => { this.panelTitle = this.output.title || ''; @@ -199,9 +193,7 @@ export class SearchEmbeddable if (this.subscription) { this.subscription.unsubscribe(); } - if (this.autoRefreshFetchSubscription) { - this.autoRefreshFetchSubscription.unsubscribe(); - } + if (this.abortController) this.abortController.abort(); } @@ -224,7 +216,7 @@ export class SearchEmbeddable const timeRangeSearchSource = searchSource.create(); timeRangeSearchSource.setField('filter', () => { if (!this.searchScope || !this.input.timeRange) return; - return getTime(indexPattern, this.input.timeRange); + return this.services.timefilter.createFilter(indexPattern, this.input.timeRange); }); this.filtersSearchSource = searchSource.create(); diff --git a/src/plugins/visualizations/public/embeddable/visualize_embeddable.ts b/src/plugins/visualizations/public/embeddable/visualize_embeddable.ts index 3956f930758d76..590b65e1af13d4 100644 --- a/src/plugins/visualizations/public/embeddable/visualize_embeddable.ts +++ b/src/plugins/visualizations/public/embeddable/visualize_embeddable.ts @@ -108,7 +108,6 @@ export class VisualizeEmbeddable private vis: Vis; private domNode: any; public readonly type = VISUALIZE_EMBEDDABLE_TYPE; - private autoRefreshFetchSubscription: Subscription; private abortController?: AbortController; private readonly deps: VisualizeEmbeddableFactoryDeps; private readonly inspectorAdapters?: Adapters; @@ -152,10 +151,6 @@ export class VisualizeEmbeddable this.attributeService = attributeService; this.savedVisualizationsLoader = savedVisualizationsLoader; - this.autoRefreshFetchSubscription = timefilter - .getAutoRefreshFetch$() - .subscribe(this.updateHandler.bind(this)); - this.subscriptions.push( this.getUpdated$().subscribe(() => { const isDirty = this.handleChanges(); @@ -368,7 +363,6 @@ export class VisualizeEmbeddable this.handler.destroy(); this.handler.getElement().remove(); } - this.autoRefreshFetchSubscription.unsubscribe(); } public reload = () => { diff --git a/src/plugins/visualize/public/application/components/visualize_top_nav.tsx b/src/plugins/visualize/public/application/components/visualize_top_nav.tsx index 627d5cd00147b0..becabc60eff160 100644 --- a/src/plugins/visualize/public/application/components/visualize_top_nav.tsx +++ b/src/plugins/visualize/public/application/components/visualize_top_nav.tsx @@ -190,6 +190,17 @@ const TopNav = ({ } }, [vis.params, vis.type, services.data.indexPatterns, vis.data.indexPattern]); + useEffect(() => { + const autoRefreshFetchSub = services.data.query.timefilter.timefilter + .getAutoRefreshFetch$() + .subscribe(() => { + visInstance.embeddableHandler.reload(); + }); + return () => { + autoRefreshFetchSub.unsubscribe(); + }; + }, [services.data.query.timefilter.timefilter, visInstance.embeddableHandler]); + return isChromeVisible ? ( /** * Most visualizations have all search bar components enabled. diff --git a/test/functional/services/dashboard/panel_actions.ts b/test/functional/services/dashboard/panel_actions.ts index 7e5ef068e21842..8ae7a6b50c111c 100644 --- a/test/functional/services/dashboard/panel_actions.ts +++ b/test/functional/services/dashboard/panel_actions.ts @@ -33,6 +33,7 @@ export function DashboardPanelActionsProvider({ getService, getPageObjects }: Ft const log = getService('log'); const testSubjects = getService('testSubjects'); const PageObjects = getPageObjects(['header', 'common']); + const inspector = getService('inspector'); return new (class DashboardPanelActions { async findContextMenu(parent?: WebElementWrapper) { @@ -163,6 +164,16 @@ export function DashboardPanelActionsProvider({ getService, getPageObjects }: Ft await this.openInspector(header); } + async getSearchSessionIdByTitle(title: string) { + await this.openInspectorByTitle(title); + await inspector.openInspectorRequestsView(); + const searchSessionId = await ( + await testSubjects.find('inspectorRequestSearchSessionId') + ).getAttribute('data-search-session-id'); + await inspector.close(); + return searchSessionId; + } + async openInspector(parent?: WebElementWrapper) { await this.openContextMenu(parent); const exists = await testSubjects.exists(OPEN_INSPECTOR_TEST_SUBJ); diff --git a/x-pack/plugins/lens/public/editor_frame_service/embeddable/embeddable.test.tsx b/x-pack/plugins/lens/public/editor_frame_service/embeddable/embeddable.test.tsx index 04f747f42ac666..efa535d5bcf8ac 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/embeddable/embeddable.test.tsx +++ b/x-pack/plugins/lens/public/editor_frame_service/embeddable/embeddable.test.tsx @@ -4,7 +4,6 @@ * you may not use this file except in compliance with the Elastic License. */ -import { Subject } from 'rxjs'; import { Embeddable, LensByValueInput, @@ -13,13 +12,7 @@ import { LensEmbeddableInput, } from './embeddable'; import { ReactExpressionRendererProps } from 'src/plugins/expressions/public'; -import { - Query, - TimeRange, - Filter, - TimefilterContract, - IndexPatternsContract, -} from 'src/plugins/data/public'; +import { Query, TimeRange, Filter, IndexPatternsContract } from 'src/plugins/data/public'; import { Document } from '../../persistence'; import { dataPluginMock } from '../../../../../../src/plugins/data/public/mocks'; import { VIS_EVENT_TO_TRIGGER } from '../../../../../../src/plugins/visualizations/public/embeddable'; @@ -536,49 +529,4 @@ describe('embeddable', () => { expect(expressionRenderer).toHaveBeenCalledTimes(1); }); - - it('should re-render on auto refresh fetch observable', async () => { - const timeRange: TimeRange = { from: 'now-15d', to: 'now' }; - const query: Query = { language: 'kquery', query: '' }; - const filters: Filter[] = [{ meta: { alias: 'test', negate: false, disabled: true } }]; - - const autoRefreshFetchSubject = new Subject(); - const timefilter = ({ - getAutoRefreshFetch$: () => autoRefreshFetchSubject.asObservable(), - } as unknown) as TimefilterContract; - - const embeddable = new Embeddable( - { - timefilter, - attributeService, - expressionRenderer, - basePath, - indexPatternService: {} as IndexPatternsContract, - editable: true, - getTrigger, - documentToExpression: () => - Promise.resolve({ - type: 'expression', - chain: [ - { type: 'function', function: 'my', arguments: {} }, - { type: 'function', function: 'expression', arguments: {} }, - ], - }), - }, - { id: '123', timeRange, query, filters } as LensEmbeddableInput - ); - await embeddable.initializeSavedVis({ - id: '123', - timeRange, - query, - filters, - } as LensEmbeddableInput); - embeddable.render(mountpoint); - - act(() => { - autoRefreshFetchSubject.next(); - }); - - expect(expressionRenderer).toHaveBeenCalledTimes(2); - }); }); diff --git a/x-pack/plugins/lens/public/editor_frame_service/embeddable/embeddable.tsx b/x-pack/plugins/lens/public/editor_frame_service/embeddable/embeddable.tsx index e2d637dd6684a5..949d58c38037d9 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/embeddable/embeddable.tsx +++ b/x-pack/plugins/lens/public/editor_frame_service/embeddable/embeddable.tsx @@ -96,7 +96,6 @@ export class Embeddable private expression: string | undefined | null; private domNode: HTMLElement | Element | undefined; private subscription: Subscription; - private autoRefreshFetchSubscription: Subscription; private isInitialized = false; private activeData: Partial | undefined; @@ -127,10 +126,6 @@ export class Embeddable this.onContainerStateChanged(this.input) ); - this.autoRefreshFetchSubscription = deps.timefilter - .getAutoRefreshFetch$() - .subscribe(this.reload.bind(this)); - const input$ = this.getInput$(); // Lens embeddable does not re-render when embeddable input changes in @@ -450,6 +445,5 @@ export class Embeddable if (this.subscription) { this.subscription.unsubscribe(); } - this.autoRefreshFetchSubscription.unsubscribe(); } } diff --git a/x-pack/plugins/ml/public/application/contexts/kibana/__mocks__/use_timefilter.ts b/x-pack/plugins/ml/public/application/contexts/kibana/__mocks__/use_timefilter.ts index 949de51cf2c2de..fce14c68bfd383 100644 --- a/x-pack/plugins/ml/public/application/contexts/kibana/__mocks__/use_timefilter.ts +++ b/x-pack/plugins/ml/public/application/contexts/kibana/__mocks__/use_timefilter.ts @@ -4,36 +4,9 @@ * you may not use this file except in compliance with the Elastic License. */ -import { TimefilterContract } from '../../../../../../../../src/plugins/data/public'; -import { Observable } from 'rxjs'; +import { dataPluginMock } from '../../../../../../../../src/plugins/data/public/mocks'; -/** - * Copy from {@link '../../../../../../../../src/plugins/data/public/query/timefilter/timefilter_service.mock'} - */ -const timefilterMock: jest.Mocked = { - isAutoRefreshSelectorEnabled: jest.fn(), - isTimeRangeSelectorEnabled: jest.fn(), - isTimeTouched: jest.fn(), - getEnabledUpdated$: jest.fn(), - getTimeUpdate$: jest.fn(), - getRefreshIntervalUpdate$: jest.fn(), - getAutoRefreshFetch$: jest.fn(() => new Observable()), - getFetch$: jest.fn(), - getTime: jest.fn(), - setTime: jest.fn(), - setRefreshInterval: jest.fn(), - getRefreshInterval: jest.fn(), - getActiveBounds: jest.fn(), - disableAutoRefreshSelector: jest.fn(), - disableTimeRangeSelector: jest.fn(), - enableAutoRefreshSelector: jest.fn(), - enableTimeRangeSelector: jest.fn(), - getBounds: jest.fn(), - calculateBounds: jest.fn(), - createFilter: jest.fn(), - getRefreshIntervalDefaults: jest.fn(), - getTimeDefaults: jest.fn(), -}; +const timefilterMock = dataPluginMock.createStartContract().query.timefilter.timefilter; export const useTimefilter = jest.fn(() => { return timefilterMock; diff --git a/x-pack/test/send_search_to_background_integration/tests/apps/dashboard/async_search/async_search.ts b/x-pack/test/send_search_to_background_integration/tests/apps/dashboard/async_search/async_search.ts index 4859b2474f860e..a1de22318ade4b 100644 --- a/x-pack/test/send_search_to_background_integration/tests/apps/dashboard/async_search/async_search.ts +++ b/x-pack/test/send_search_to_background_integration/tests/apps/dashboard/async_search/async_search.ts @@ -6,17 +6,14 @@ import expect from '@kbn/expect'; import { FtrProviderContext } from '../../../../ftr_provider_context'; -import { getSearchSessionIdByPanelProvider } from './get_search_session_id_by_panel'; export default function ({ getService, getPageObjects }: FtrProviderContext) { const es = getService('es'); const testSubjects = getService('testSubjects'); const log = getService('log'); const PageObjects = getPageObjects(['common', 'header', 'dashboard', 'visChart']); - const getSearchSessionIdByPanel = getSearchSessionIdByPanelProvider(getService); + const dashboardPanelActions = getService('dashboardPanelActions'); const queryBar = getService('queryBar'); - const browser = getService('browser'); - const sendToBackground = getService('sendToBackground'); describe('dashboard with async search', () => { before(async function () { @@ -63,74 +60,24 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { // but only single error toast because searches are grouped expect((await testSubjects.findAll('searchTimeoutError')).length).to.be(1); - const panel1SessionId1 = await getSearchSessionIdByPanel('Sum of Bytes by Extension'); - const panel2SessionId1 = await getSearchSessionIdByPanel( + const panel1SessionId1 = await dashboardPanelActions.getSearchSessionIdByTitle( + 'Sum of Bytes by Extension' + ); + const panel2SessionId1 = await dashboardPanelActions.getSearchSessionIdByTitle( 'Sum of Bytes by Extension (Delayed 5s)' ); expect(panel1SessionId1).to.be(panel2SessionId1); await queryBar.clickQuerySubmitButton(); - const panel1SessionId2 = await getSearchSessionIdByPanel('Sum of Bytes by Extension'); - const panel2SessionId2 = await getSearchSessionIdByPanel( + const panel1SessionId2 = await dashboardPanelActions.getSearchSessionIdByTitle( + 'Sum of Bytes by Extension' + ); + const panel2SessionId2 = await dashboardPanelActions.getSearchSessionIdByTitle( 'Sum of Bytes by Extension (Delayed 5s)' ); expect(panel1SessionId2).to.be(panel2SessionId2); expect(panel1SessionId1).not.to.be(panel1SessionId2); }); - - describe('Send to background', () => { - before(async () => { - await PageObjects.common.navigateToApp('dashboard'); - }); - - it('Restore using non-existing sessionId errors out. Refresh starts a new session and completes.', async () => { - await PageObjects.dashboard.loadSavedDashboard('Not Delayed'); - const url = await browser.getCurrentUrl(); - const fakeSessionId = '__fake__'; - const savedSessionURL = `${url}&searchSessionId=${fakeSessionId}`; - await browser.get(savedSessionURL); - await PageObjects.header.waitUntilLoadingHasFinished(); - await sendToBackground.expectState('restored'); - await testSubjects.existOrFail('embeddableErrorLabel'); // expected that panel errors out because of non existing session - - const session1 = await getSearchSessionIdByPanel('Sum of Bytes by Extension'); - expect(session1).to.be(fakeSessionId); - - await sendToBackground.refresh(); - await PageObjects.header.waitUntilLoadingHasFinished(); - await sendToBackground.expectState('completed'); - await testSubjects.missingOrFail('embeddableErrorLabel'); - const session2 = await getSearchSessionIdByPanel('Sum of Bytes by Extension'); - expect(session2).not.to.be(fakeSessionId); - }); - - it('Saves and restores a session', async () => { - await PageObjects.dashboard.loadSavedDashboard('Not Delayed'); - await PageObjects.dashboard.waitForRenderComplete(); - await sendToBackground.expectState('completed'); - await sendToBackground.save(); - await sendToBackground.expectState('backgroundCompleted'); - const savedSessionId = await getSearchSessionIdByPanel('Sum of Bytes by Extension'); - - // load URL to restore a saved session - const url = await browser.getCurrentUrl(); - const savedSessionURL = `${url}&searchSessionId=${savedSessionId}`; - await browser.get(savedSessionURL); - await PageObjects.header.waitUntilLoadingHasFinished(); - await PageObjects.dashboard.waitForRenderComplete(); - - // Check that session is restored - await sendToBackground.expectState('restored'); - await testSubjects.missingOrFail('embeddableErrorLabel'); - const data = await PageObjects.visChart.getBarChartData('Sum of bytes'); - expect(data.length).to.be(5); - - // switching dashboard to edit mode (or any other non-fetch required) state change - // should leave session state untouched - await PageObjects.dashboard.switchToEditMode(); - await sendToBackground.expectState('restored'); - }); - }); }); } diff --git a/x-pack/test/send_search_to_background_integration/tests/apps/dashboard/async_search/get_search_session_id_by_panel.ts b/x-pack/test/send_search_to_background_integration/tests/apps/dashboard/async_search/get_search_session_id_by_panel.ts deleted file mode 100644 index 6de85ca5459db3..00000000000000 --- a/x-pack/test/send_search_to_background_integration/tests/apps/dashboard/async_search/get_search_session_id_by_panel.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. - */ - -// HELPERS -export function getSearchSessionIdByPanelProvider(getService: any) { - const dashboardPanelActions = getService('dashboardPanelActions'); - const inspector = getService('inspector'); - const testSubjects = getService('testSubjects'); - - return async function getSearchSessionIdByPanel(panelTitle: string) { - await dashboardPanelActions.openInspectorByTitle(panelTitle); - await inspector.openInspectorRequestsView(); - const searchSessionId = await ( - await testSubjects.find('inspectorRequestSearchSessionId') - ).getAttribute('data-search-session-id'); - await inspector.close(); - return searchSessionId; - }; -} diff --git a/x-pack/test/send_search_to_background_integration/tests/apps/dashboard/async_search/index.ts b/x-pack/test/send_search_to_background_integration/tests/apps/dashboard/async_search/index.ts index 83085983fef055..7a14a97e57066c 100644 --- a/x-pack/test/send_search_to_background_integration/tests/apps/dashboard/async_search/index.ts +++ b/x-pack/test/send_search_to_background_integration/tests/apps/dashboard/async_search/index.ts @@ -24,6 +24,8 @@ export default function ({ loadTestFile, getService }: FtrProviderContext) { }); loadTestFile(require.resolve('./async_search')); + loadTestFile(require.resolve('./send_to_background')); + loadTestFile(require.resolve('./send_to_background_relative_time')); loadTestFile(require.resolve('./sessions_in_space')); }); } diff --git a/x-pack/test/send_search_to_background_integration/tests/apps/dashboard/async_search/send_to_background.ts b/x-pack/test/send_search_to_background_integration/tests/apps/dashboard/async_search/send_to_background.ts new file mode 100644 index 00000000000000..2edaeb1918b252 --- /dev/null +++ b/x-pack/test/send_search_to_background_integration/tests/apps/dashboard/async_search/send_to_background.ts @@ -0,0 +1,83 @@ +/* + * 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 expect from '@kbn/expect'; +import { FtrProviderContext } from '../../../../ftr_provider_context'; + +export default function ({ getService, getPageObjects }: FtrProviderContext) { + const es = getService('es'); + const testSubjects = getService('testSubjects'); + const log = getService('log'); + const PageObjects = getPageObjects(['common', 'header', 'dashboard', 'visChart']); + const dashboardPanelActions = getService('dashboardPanelActions'); + const browser = getService('browser'); + const sendToBackground = getService('sendToBackground'); + + describe('send to background', () => { + before(async function () { + const { body } = await es.info(); + if (!body.version.number.includes('SNAPSHOT')) { + log.debug('Skipping because this build does not have the required shard_delay agg'); + this.skip(); + } + await PageObjects.common.navigateToApp('dashboard'); + }); + + it('Restore using non-existing sessionId errors out. Refresh starts a new session and completes.', async () => { + await PageObjects.dashboard.loadSavedDashboard('Not Delayed'); + const url = await browser.getCurrentUrl(); + const fakeSessionId = '__fake__'; + const savedSessionURL = `${url}&searchSessionId=${fakeSessionId}`; + await browser.get(savedSessionURL); + await PageObjects.header.waitUntilLoadingHasFinished(); + await sendToBackground.expectState('restored'); + await testSubjects.existOrFail('embeddableErrorLabel'); // expected that panel errors out because of non existing session + + const session1 = await dashboardPanelActions.getSearchSessionIdByTitle( + 'Sum of Bytes by Extension' + ); + expect(session1).to.be(fakeSessionId); + + await sendToBackground.refresh(); + await PageObjects.header.waitUntilLoadingHasFinished(); + await sendToBackground.expectState('completed'); + await testSubjects.missingOrFail('embeddableErrorLabel'); + const session2 = await dashboardPanelActions.getSearchSessionIdByTitle( + 'Sum of Bytes by Extension' + ); + expect(session2).not.to.be(fakeSessionId); + }); + + it('Saves and restores a session', async () => { + await PageObjects.dashboard.loadSavedDashboard('Not Delayed'); + await PageObjects.dashboard.waitForRenderComplete(); + await sendToBackground.expectState('completed'); + await sendToBackground.save(); + await sendToBackground.expectState('backgroundCompleted'); + const savedSessionId = await dashboardPanelActions.getSearchSessionIdByTitle( + 'Sum of Bytes by Extension' + ); + + // load URL to restore a saved session + const url = await browser.getCurrentUrl(); + const savedSessionURL = `${url}&searchSessionId=${savedSessionId}`; + await browser.get(savedSessionURL); + await PageObjects.header.waitUntilLoadingHasFinished(); + await PageObjects.dashboard.waitForRenderComplete(); + + // Check that session is restored + await sendToBackground.expectState('restored'); + await testSubjects.missingOrFail('embeddableErrorLabel'); + const data = await PageObjects.visChart.getBarChartData('Sum of bytes'); + expect(data.length).to.be(5); + + // switching dashboard to edit mode (or any other non-fetch required) state change + // should leave session state untouched + await PageObjects.dashboard.switchToEditMode(); + await sendToBackground.expectState('restored'); + }); + }); +} diff --git a/x-pack/test/send_search_to_background_integration/tests/apps/dashboard/async_search/send_to_background_relative_time.ts b/x-pack/test/send_search_to_background_integration/tests/apps/dashboard/async_search/send_to_background_relative_time.ts new file mode 100644 index 00000000000000..2234ca3e3b034a --- /dev/null +++ b/x-pack/test/send_search_to_background_integration/tests/apps/dashboard/async_search/send_to_background_relative_time.ts @@ -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 expect from '@kbn/expect'; +import { FtrProviderContext } from '../../../../ftr_provider_context'; + +export default function ({ getService, getPageObjects }: FtrProviderContext) { + const testSubjects = getService('testSubjects'); + const log = getService('log'); + const retry = getService('retry'); + const PageObjects = getPageObjects([ + 'common', + 'header', + 'dashboard', + 'visChart', + 'home', + 'timePicker', + ]); + const dashboardPanelActions = getService('dashboardPanelActions'); + const inspector = getService('inspector'); + const pieChart = getService('pieChart'); + const find = getService('find'); + const dashboardExpect = getService('dashboardExpect'); + const queryBar = getService('queryBar'); + const browser = getService('browser'); + const sendToBackground = getService('sendToBackground'); + + describe('send to background with relative time', () => { + before(async () => { + await PageObjects.common.sleep(5000); // this part was copied from `x-pack/test/functional/apps/dashboard/_async_dashboard.ts` and this was sleep was needed because of flakiness + await PageObjects.common.navigateToUrl('home', '/tutorial_directory/sampleData', { + useActualUrl: true, + }); + await PageObjects.header.waitUntilLoadingHasFinished(); + // use sample data set because it has recent relative time range and bunch of different visualizations + await PageObjects.home.addSampleDataSet('flights'); + await retry.tryForTime(10000, async () => { + const isInstalled = await PageObjects.home.isSampleDataSetInstalled('flights'); + expect(isInstalled).to.be(true); + }); + await PageObjects.common.navigateToApp('dashboard'); + }); + + after(async () => { + await PageObjects.common.navigateToUrl('home', '/tutorial_directory/sampleData', { + useActualUrl: true, + }); + await PageObjects.header.waitUntilLoadingHasFinished(); + await PageObjects.home.removeSampleDataSet('flights'); + const isInstalled = await PageObjects.home.isSampleDataSetInstalled('flights'); + expect(isInstalled).to.be(false); + }); + + it('Saves and restores a session with relative time ranges', async () => { + await PageObjects.dashboard.loadSavedDashboard('[Flights] Global Flight Dashboard'); + await PageObjects.header.waitUntilLoadingHasFinished(); + await PageObjects.timePicker.pauseAutoRefresh(); // sample data has auto-refresh on + await queryBar.submitQuery(); + await PageObjects.header.waitUntilLoadingHasFinished(); + await PageObjects.dashboard.waitForRenderComplete(); + await checkSampleDashboardLoaded(); + + await sendToBackground.expectState('completed'); + await sendToBackground.save(); + await sendToBackground.expectState('backgroundCompleted'); + const savedSessionId = await dashboardPanelActions.getSearchSessionIdByTitle( + '[Flights] Airline Carrier' + ); + const resolvedTimeRange = await getResolvedTimeRangeFromPanel('[Flights] Airline Carrier'); + + // load URL to restore a saved session + const url = await browser.getCurrentUrl(); + const savedSessionURL = `${url}&searchSessionId=${savedSessionId}` + .replace('now-24h', `'${resolvedTimeRange.gte}'`) + .replace('now', `'${resolvedTimeRange.lte}'`); + log.debug('Trying to restore session by URL:', savedSessionId); + await browser.get(savedSessionURL); + await PageObjects.header.waitUntilLoadingHasFinished(); + await PageObjects.dashboard.waitForRenderComplete(); + await checkSampleDashboardLoaded(); + + // Check that session is restored + await sendToBackground.expectState('restored'); + }); + }); + + // HELPERS + + async function getResolvedTimeRangeFromPanel( + panelTitle: string + ): Promise<{ gte: string; lte: string }> { + await dashboardPanelActions.openInspectorByTitle(panelTitle); + await inspector.openInspectorRequestsView(); + await (await inspector.getOpenRequestDetailRequestButton()).click(); + const request = JSON.parse(await inspector.getCodeEditorValue()); + return request.query.bool.filter.find((f: any) => f.range).range.timestamp; + } + + async function checkSampleDashboardLoaded() { + log.debug('Checking no error labels'); + await testSubjects.missingOrFail('embeddableErrorLabel'); + log.debug('Checking pie charts rendered'); + await pieChart.expectPieSliceCount(4); + log.debug('Checking area, bar and heatmap charts rendered'); + await dashboardExpect.seriesElementCount(15); + log.debug('Checking saved searches rendered'); + await dashboardExpect.savedSearchRowCount(50); + log.debug('Checking input controls rendered'); + await dashboardExpect.inputControlItemCount(3); + log.debug('Checking tag cloud rendered'); + await dashboardExpect.tagCloudWithValuesFound(['Sunny', 'Rain', 'Clear', 'Cloudy', 'Hail']); + log.debug('Checking vega chart rendered'); + const tsvb = await find.existsByCssSelector('.vgaVis__view'); + expect(tsvb).to.be(true); + } +} diff --git a/x-pack/test/send_search_to_background_integration/tests/apps/dashboard/async_search/sessions_in_space.ts b/x-pack/test/send_search_to_background_integration/tests/apps/dashboard/async_search/sessions_in_space.ts index 97fba9e3aef913..7d00761b2fa9f3 100644 --- a/x-pack/test/send_search_to_background_integration/tests/apps/dashboard/async_search/sessions_in_space.ts +++ b/x-pack/test/send_search_to_background_integration/tests/apps/dashboard/async_search/sessions_in_space.ts @@ -5,7 +5,6 @@ */ import { FtrProviderContext } from '../../../../ftr_provider_context'; -import { getSearchSessionIdByPanelProvider } from './get_search_session_id_by_panel'; export default function ({ getService, getPageObjects }: FtrProviderContext) { const testSubjects = getService('testSubjects'); @@ -19,7 +18,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { 'security', 'timePicker', ]); - const getSearchSessionIdByPanel = getSearchSessionIdByPanelProvider(getService); + const dashboardPanelActions = getService('dashboardPanelActions'); const browser = getService('browser'); const sendToBackground = getService('sendToBackground'); @@ -77,7 +76,9 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await sendToBackground.expectState('completed'); await sendToBackground.save(); await sendToBackground.expectState('backgroundCompleted'); - const savedSessionId = await getSearchSessionIdByPanel('A Pie in another space'); + const savedSessionId = await dashboardPanelActions.getSearchSessionIdByTitle( + 'A Pie in another space' + ); // load URL to restore a saved session const url = await browser.getCurrentUrl();