Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
5 changes: 5 additions & 0 deletions src/plugins/dashboard/public/application/dashboard_app.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -169,6 +169,11 @@ export function DashboardApp({
setLastReloadTime(() => new Date().getTime());
})
);
subscriptions.add(
data.query.timefilter.timefilter.getAutoRefreshFetch$().subscribe(() => {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: We already have a shortcut const for timefilter declared above. It also might be a bit cleaner to merge the two subs that do the same thing:

subscriptions.add(
  merge(data.search.session.onRefresh$, timeFilter.getAutoRefreshFetch$()).subscribe(() =>
    setLastReloadTime(() => new Date().getTime())
    )
);

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Improved 👍

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
12 changes: 12 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,18 @@ export function calculateBounds(
};
}

export function getAbsoluteTimeRange(
timeRange: TimeRange,
{ forceNow }: { forceNow?: Date } = {}
): TimeRange {
const from = dateMath.parse(timeRange.from, { forceNow });
const to = dateMath.parse(timeRange.to, { forceNow, roundUp: true });
return {
from: from ? from.toISOString() : timeRange.from,
to: to ? to.toISOString() : timeRange.to,
};
}

export function getTime(
indexPattern: IIndexPattern | undefined,
timeRange: TimeRange,
Expand Down
17 changes: 11 additions & 6 deletions src/plugins/data/common/search/expressions/esaggs/esaggs_fn.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ import {
import { FormatFactory } from '../../../field_formats/utils';
import { IndexPatternExpressionType } from '../../../index_patterns/expressions';
import { IndexPatternsContract } from '../../../index_patterns/index_patterns';
import { calculateBounds } from '../../../query';
import { getAbsoluteTimeRange } from '../../../query';

import { AggsStart, AggExpressionType } from '../../aggs';
import { ISearchStartSearchSource } from '../../search_source';
Expand Down Expand Up @@ -62,6 +62,7 @@ export interface EsaggsStartDependencies {
deserializeFieldFormat: FormatFactory;
indexPatterns: IndexPatternsContract;
searchSource: ISearchStartSearchSource;
getNow?: () => Date;
}

/** @internal */
Expand Down Expand Up @@ -118,9 +119,13 @@ export async function handleEsaggsRequest(
args: Arguments,
params: RequestHandlerParams
): Promise<Datatable> {
const resolvedTimeRange = input?.timeRange && calculateBounds(input.timeRange);
const resolvedTimeRange =
params.timeRange && getAbsoluteTimeRange(params.timeRange, { forceNow: params.getNow?.() });

const response = await handleRequest(params);
const response = await handleRequest({
...params,
timeRange: resolvedTimeRange,
});

const table: Datatable = {
type: 'datatable',
Expand All @@ -139,12 +144,12 @@ export async function handleEsaggsRequest(
indexPatternId: params.indexPattern?.id,
appliedTimeRange:
column.aggConfig.params.field?.name &&
input?.timeRange &&
resolvedTimeRange &&
args.timeFields &&
args.timeFields.includes(column.aggConfig.params.field?.name)
? {
from: resolvedTimeRange?.min?.toISOString(),
to: resolvedTimeRange?.max?.toISOString(),
from: resolvedTimeRange?.from,
to: resolvedTimeRange?.to,
}
: undefined,
...column.aggConfig.serialize(),
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 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 {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this moved from timefilter to this new nowProvider
Long term we should make apps responsible for getting this from the URL and setting it into nowProvider

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);
});
50 changes: 50 additions & 0 deletions src/plugins/data/public/now_provider/now_provider.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
/*
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would this make more sense inside of utils/ since it's not exposing a service?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it is more of a service, then an util because:

  • it holds state
  • even though now it isn't needed as a service outside of a data plugin, it is used by multiple services inside data plugin: session service / time filter / esaggs
  • later, when we fix, that that service read forceNow param from URL (legacy functionality moved from time filter), apps will call forceNow on that service

* 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 {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i am wondering why is this not part of session service ? i would expect that when creating a session its 'fixed in time' by converting the relative time to absolute.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is not part of session service because it is also includes functionality the previously was part of time filter about getting now from the URL (so not only session can control "now"):
https://github.com/elastic/kibana/pull/84405/files/c92374a20f65fe24447bdef8c5b1dbf8c94dfeec#diff-fe41d6d47fbb6465cde6d519ae701f58b0259c89ecf3779048a49651a43b6276R29

// 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;
}
}
Loading