Skip to content

Commit

Permalink
[Search] Search Sessions with relative time range (elastic#84405) (el…
Browse files Browse the repository at this point in the history
  • Loading branch information
Dosant authored Jan 12, 2021
1 parent fb29691 commit efaebd8
Show file tree
Hide file tree
Showing 48 changed files with 672 additions and 276 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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) |
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->

[Home](./index.md) &gt; [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) &gt; [DataPublicPluginStart](./kibana-plugin-plugins-data-public.datapublicpluginstart.md) &gt; [nowProvider](./kibana-plugin-plugins-data-public.datapublicpluginstart.nowprovider.md)

## DataPublicPluginStart.nowProvider property

<b>Signature:</b>

```typescript
nowProvider: NowProviderPublicContract;
```
6 changes: 5 additions & 1 deletion src/plugins/dashboard/public/application/dashboard_app.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
17 changes: 16 additions & 1 deletion src/plugins/data/common/query/timefilter/get_time.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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', () => {
Expand Down Expand Up @@ -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();
});
});
});
11 changes: 11 additions & 0 deletions src/plugins/data/common/query/timefilter/get_time.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ export interface EsaggsStartDependencies {
deserializeFieldFormat: FormatFactory;
indexPatterns: IndexPatternsContract;
searchSource: ISearchStartSearchSource;
getNow?: () => Date;
}

/** @internal */
Expand Down Expand Up @@ -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);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ export interface RequestHandlerParams {
searchSourceService: ISearchStartSearchSource;
timeFields?: string[];
timeRange?: TimeRange;
getNow?: () => Date;
}

export const handleRequest = async ({
Expand All @@ -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);
Expand Down Expand Up @@ -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);
});
}
Expand Down Expand Up @@ -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,
Expand Down
2 changes: 2 additions & 0 deletions src/plugins/data/public/mocks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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']>>;
Expand Down Expand Up @@ -76,6 +77,7 @@ const createStartContract = (): Start => {
get: jest.fn().mockReturnValue(Promise.resolve({})),
clearCache: jest.fn(),
} as unknown) as IndexPatternsContract,
nowProvider: createNowProviderMock(),
};
};

Expand Down
24 changes: 24 additions & 0 deletions src/plugins/data/public/now_provider/index.ts
Original file line number Diff line number Diff line change
@@ -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';
Original file line number Diff line number Diff line change
@@ -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);
});
Original file line number Diff line number Diff line change
Expand Up @@ -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('?');
Expand Down
20 changes: 20 additions & 0 deletions src/plugins/data/public/now_provider/lib/index.ts
Original file line number Diff line number Diff line change
@@ -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';
Original file line number Diff line number Diff line change
Expand Up @@ -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 changes: 55 additions & 0 deletions src/plugins/data/public/now_provider/now_provider.test.ts
Original file line number Diff line number Diff line change
@@ -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);
});
Loading

0 comments on commit efaebd8

Please sign in to comment.