mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 17:59:23 -04:00
[Search] Search Sessions with relative time range (#84405)
This commit is contained in:
parent
02695ef5ad
commit
d3303f28bb
48 changed files with 672 additions and 276 deletions
|
@ -20,6 +20,7 @@ export interface DataPublicPluginStart
|
|||
| [autocomplete](./kibana-plugin-plugins-data-public.datapublicpluginstart.autocomplete.md) | <code>AutocompleteStart</code> | autocomplete service [AutocompleteStart](./kibana-plugin-plugins-data-public.autocompletestart.md) |
|
||||
| [fieldFormats](./kibana-plugin-plugins-data-public.datapublicpluginstart.fieldformats.md) | <code>FieldFormatsStart</code> | field formats service [FieldFormatsStart](./kibana-plugin-plugins-data-public.fieldformatsstart.md) |
|
||||
| [indexPatterns](./kibana-plugin-plugins-data-public.datapublicpluginstart.indexpatterns.md) | <code>IndexPatternsContract</code> | index patterns service [IndexPatternsContract](./kibana-plugin-plugins-data-public.indexpatternscontract.md) |
|
||||
| [nowProvider](./kibana-plugin-plugins-data-public.datapublicpluginstart.nowprovider.md) | <code>NowProviderPublicContract</code> | |
|
||||
| [query](./kibana-plugin-plugins-data-public.datapublicpluginstart.query.md) | <code>QueryStart</code> | query service [QueryStart](./kibana-plugin-plugins-data-public.querystart.md) |
|
||||
| [search](./kibana-plugin-plugins-data-public.datapublicpluginstart.search.md) | <code>ISearchStart</code> | search service [ISearchStart](./kibana-plugin-plugins-data-public.isearchstart.md) |
|
||||
| [ui](./kibana-plugin-plugins-data-public.datapublicpluginstart.ui.md) | <code>DataPublicPluginStartUi</code> | prewired UI components [DataPublicPluginStartUi](./kibana-plugin-plugins-data-public.datapublicpluginstartui.md) |
|
||||
|
|
|
@ -0,0 +1,11 @@
|
|||
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
|
||||
|
||||
[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
|
||||
|
||||
<b>Signature:</b>
|
||||
|
||||
```typescript
|
||||
nowProvider: NowProviderPublicContract;
|
||||
```
|
|
@ -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.
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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<Datatable> {
|
||||
const resolvedTimeRange = input?.timeRange && calculateBounds(input.timeRange);
|
||||
const resolvedTimeRange =
|
||||
input?.timeRange && calculateBounds(input.timeRange, { forceNow: params.getNow?.() });
|
||||
|
||||
const response = await handleRequest(params);
|
||||
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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<ReturnType<Plugin['setup']>>;
|
||||
export type Start = jest.Mocked<ReturnType<Plugin['start']>>;
|
||||
|
@ -76,6 +77,7 @@ const createStartContract = (): Start => {
|
|||
get: jest.fn().mockReturnValue(Promise.resolve({})),
|
||||
clearCache: jest.fn(),
|
||||
} as unknown) as IndexPatternsContract,
|
||||
nowProvider: createNowProviderMock(),
|
||||
};
|
||||
};
|
||||
|
||||
|
|
24
src/plugins/data/public/now_provider/index.ts
Normal file
24
src/plugins/data/public/now_provider/index.ts
Normal file
|
@ -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';
|
|
@ -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);
|
||||
});
|
|
@ -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('?');
|
20
src/plugins/data/public/now_provider/lib/index.ts
Normal file
20
src/plugins/data/public/now_provider/lib/index.ts
Normal file
|
@ -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';
|
|
@ -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<NowProviderInternalContract> => {
|
||||
return {
|
||||
get: jest.fn(() => new Date()),
|
||||
set: jest.fn(),
|
||||
reset: jest.fn(),
|
||||
};
|
||||
};
|
55
src/plugins/data/public/now_provider/now_provider.test.ts
Normal file
55
src/plugins/data/public/now_provider/now_provider.test.ts
Normal file
|
@ -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);
|
||||
});
|
50
src/plugins/data/public/now_provider/now_provider.ts
Normal file
50
src/plugins/data/public/now_provider/now_provider.ts
Normal file
|
@ -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<NowProvider>;
|
||||
export type NowProviderPublicContract = Pick<NowProviderInternalContract, 'get'>;
|
||||
|
||||
/**
|
||||
* 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;
|
||||
}
|
||||
}
|
|
@ -61,6 +61,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
|
||||
|
@ -76,6 +77,7 @@ export class DataPublicPlugin
|
|||
private readonly queryService: QueryService;
|
||||
private readonly storage: IStorageWrapper;
|
||||
private usageCollection: UsageCollectionSetup | undefined;
|
||||
private readonly nowProvider: NowProviderInternalContract;
|
||||
|
||||
constructor(initializerContext: PluginInitializerContext<ConfigSchema>) {
|
||||
this.searchService = new SearchService(initializerContext);
|
||||
|
@ -83,6 +85,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(
|
||||
|
@ -95,9 +98,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);
|
||||
|
@ -120,12 +131,6 @@ export class DataPublicPlugin
|
|||
}))
|
||||
);
|
||||
|
||||
const searchService = this.searchService.setup(core, {
|
||||
bfetch,
|
||||
usageCollection,
|
||||
expressions,
|
||||
});
|
||||
|
||||
inspector.registerView(
|
||||
getTableViewDescription(() => ({
|
||||
uiActions: startServices().plugins.uiActions,
|
||||
|
@ -195,6 +200,7 @@ export class DataPublicPlugin
|
|||
indexPatterns,
|
||||
query,
|
||||
search,
|
||||
nowProvider: this.nowProvider,
|
||||
};
|
||||
|
||||
const SearchBar = createSearchBar({
|
||||
|
|
|
@ -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;
|
||||
|
@ -2620,7 +2624,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)
|
||||
|
||||
|
|
|
@ -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<typeof createQueryStateObservable>;
|
||||
|
||||
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,
|
||||
|
|
|
@ -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<QueryState>) =>
|
||||
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,
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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', () => {
|
||||
|
|
|
@ -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<Timefilter>;
|
||||
|
|
|
@ -46,6 +46,7 @@ const createSetupContractMock = () => {
|
|||
createFilter: jest.fn(),
|
||||
getRefreshIntervalDefaults: jest.fn(),
|
||||
getTimeDefaults: jest.fn(),
|
||||
getAbsoluteTime: jest.fn(),
|
||||
};
|
||||
|
||||
const historyMock: jest.Mocked<TimeHistoryContract> = {
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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(),
|
||||
|
|
|
@ -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 });
|
||||
|
|
|
@ -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(),
|
||||
};
|
||||
},
|
||||
});
|
||||
|
|
|
@ -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<ISearchSetup, ISearchStart> {
|
|||
|
||||
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<ISearchSetup, ISearchStart> {
|
|||
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<ISearchSetup, ISearchStart> {
|
|||
const aggs = this.aggsService.setup({
|
||||
registerFunction: expressions.registerFunction,
|
||||
uiSettings,
|
||||
nowProvider,
|
||||
});
|
||||
|
||||
if (this.initializerContext.config.get().search.aggs.shardDelay.enabled) {
|
||||
|
|
|
@ -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', () => {
|
||||
|
|
|
@ -105,6 +105,11 @@ export interface SessionStateInternal<SearchDescriptor = unknown> {
|
|||
* 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 = <SearchDescriptor = unknown>(
|
|||
): {
|
||||
stateContainer: SessionStateContainer<SearchDescriptor>;
|
||||
sessionState$: Observable<SearchSessionState>;
|
||||
sessionStartTime$: Observable<Date | undefined>;
|
||||
} => {
|
||||
const stateContainer = createStateContainer(
|
||||
createSessionDefaultState(),
|
||||
|
@ -229,8 +239,16 @@ export const createSessionStateContainer = <SearchDescriptor = unknown>(
|
|||
distinctUntilChanged(),
|
||||
shareReplay(1)
|
||||
);
|
||||
|
||||
const sessionStartTime$: Observable<Date | undefined> = stateContainer.state$.pipe(
|
||||
map(() => stateContainer.get().startTime),
|
||||
distinctUntilChanged(),
|
||||
shareReplay(1)
|
||||
);
|
||||
|
||||
return {
|
||||
stateContainer,
|
||||
sessionState$,
|
||||
sessionStartTime$,
|
||||
};
|
||||
};
|
||||
|
|
|
@ -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<SearchSessionState>;
|
||||
let nowProvider: jest.Mocked<NowProviderInternalContract>;
|
||||
|
||||
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>(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 () => {
|
||||
|
|
|
@ -29,6 +29,7 @@ import {
|
|||
SessionStateContainer,
|
||||
} from './search_session_state';
|
||||
import { ISessionsClient } from './sessions_client';
|
||||
import { NowProviderInternalContract } from '../../now_provider';
|
||||
|
||||
export type ISessionService = PublicContract<SessionService>;
|
||||
|
||||
|
@ -60,40 +61,54 @@ export class SessionService {
|
|||
private readonly state: SessionStateContainer<TrackSearchDescriptor>;
|
||||
|
||||
private searchSessionInfoProvider?: SearchSessionInfoProvider;
|
||||
private appChangeSubscription$?: Subscription;
|
||||
private subscription = new Subscription();
|
||||
private curApp?: string;
|
||||
|
||||
constructor(
|
||||
initializerContext: PluginInitializerContext<ConfigSchema>,
|
||||
getStartServices: StartServicesAccessor,
|
||||
private readonly sessionsClient: ISessionsClient,
|
||||
private readonly nowProvider: NowProviderInternalContract,
|
||||
{ freezeState = true }: { freezeState: boolean } = { freezeState: true }
|
||||
) {
|
||||
const { stateContainer, sessionState$ } = createSessionStateContainer<TrackSearchDescriptor>({
|
||||
const {
|
||||
stateContainer,
|
||||
sessionState$,
|
||||
sessionStartTime$,
|
||||
} = createSessionStateContainer<TrackSearchDescriptor>({
|
||||
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();
|
||||
}
|
||||
|
||||
|
|
|
@ -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<CoreStart> {
|
||||
|
|
|
@ -282,11 +282,14 @@ function createUrlGeneratorState({
|
|||
appStateContainer,
|
||||
data,
|
||||
getSavedSearchId,
|
||||
forceAbsoluteTime, // TODO: not implemented
|
||||
forceAbsoluteTime,
|
||||
}: {
|
||||
appStateContainer: StateContainer<AppState>;
|
||||
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,
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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 = () => {
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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<DefaultInspectorAdapters> | 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();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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<TimefilterContract> = {
|
||||
isAutoRefreshSelectorEnabled: jest.fn(),
|
||||
isTimeRangeSelectorEnabled: jest.fn(),
|
||||
isTimeTouched: jest.fn(),
|
||||
getEnabledUpdated$: jest.fn(),
|
||||
getTimeUpdate$: jest.fn(),
|
||||
getRefreshIntervalUpdate$: jest.fn(),
|
||||
getAutoRefreshFetch$: jest.fn(() => new Observable<unknown>()),
|
||||
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;
|
||||
|
|
|
@ -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');
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
};
|
||||
}
|
|
@ -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'));
|
||||
});
|
||||
}
|
||||
|
|
|
@ -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');
|
||||
});
|
||||
});
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue