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();