mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 17:59:23 -04:00
[Autocomplete] Support useTimeFilter option (#81515)
* pass timefilter to autocomplete * ignoreTimeRange advanced setting * Show all results in filter bar autocomplete * Round timerange to minute for autocomplete memoization * Change useTimeFilter param name Update autocomplete tests and docs * Fix maps test useTimeFilter in uptime * docs * useTimeRange in apm * remove date validation * Update src/plugins/data/common/constants.ts Co-authored-by: Lukas Olson <olson.lukas@gmail.com> * docs Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> Co-authored-by: Lukas Olson <olson.lukas@gmail.com>
This commit is contained in:
parent
ccb0b35452
commit
8842e9b7a9
20 changed files with 136 additions and 46 deletions
|
@ -23,4 +23,5 @@ export interface QuerySuggestionGetFnArgs
|
|||
| [selectionEnd](./kibana-plugin-plugins-data-public.querysuggestiongetfnargs.selectionend.md) | <code>number</code> | |
|
||||
| [selectionStart](./kibana-plugin-plugins-data-public.querysuggestiongetfnargs.selectionstart.md) | <code>number</code> | |
|
||||
| [signal](./kibana-plugin-plugins-data-public.querysuggestiongetfnargs.signal.md) | <code>AbortSignal</code> | |
|
||||
| [useTimeRange](./kibana-plugin-plugins-data-public.querysuggestiongetfnargs.usetimerange.md) | <code>boolean</code> | |
|
||||
|
||||
|
|
|
@ -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) > [QuerySuggestionGetFnArgs](./kibana-plugin-plugins-data-public.querysuggestiongetfnargs.md) > [useTimeRange](./kibana-plugin-plugins-data-public.querysuggestiongetfnargs.usetimerange.md)
|
||||
|
||||
## QuerySuggestionGetFnArgs.useTimeRange property
|
||||
|
||||
<b>Signature:</b>
|
||||
|
||||
```typescript
|
||||
useTimeRange?: boolean;
|
||||
```
|
|
@ -37,5 +37,6 @@ UI_SETTINGS: {
|
|||
readonly INDEXPATTERN_PLACEHOLDER: "indexPattern:placeholder";
|
||||
readonly FILTERS_PINNED_BY_DEFAULT: "filters:pinnedByDefault";
|
||||
readonly FILTERS_EDITOR_SUGGEST_VALUES: "filterEditor:suggestValues";
|
||||
readonly AUTOCOMPLETE_USE_TIMERANGE: "autocomplete:useTimeRange";
|
||||
}
|
||||
```
|
||||
|
|
|
@ -37,5 +37,6 @@ UI_SETTINGS: {
|
|||
readonly INDEXPATTERN_PLACEHOLDER: "indexPattern:placeholder";
|
||||
readonly FILTERS_PINNED_BY_DEFAULT: "filters:pinnedByDefault";
|
||||
readonly FILTERS_EDITOR_SUGGEST_VALUES: "filterEditor:suggestValues";
|
||||
readonly AUTOCOMPLETE_USE_TIMERANGE: "autocomplete:useTimeRange";
|
||||
}
|
||||
```
|
||||
|
|
|
@ -49,4 +49,5 @@ export const UI_SETTINGS = {
|
|||
INDEXPATTERN_PLACEHOLDER: 'indexPattern:placeholder',
|
||||
FILTERS_PINNED_BY_DEFAULT: 'filters:pinnedByDefault',
|
||||
FILTERS_EDITOR_SUGGEST_VALUES: 'filterEditor:suggestValues',
|
||||
AUTOCOMPLETE_USE_TIMERANGE: 'autocomplete:useTimeRange',
|
||||
} as const;
|
||||
|
|
|
@ -39,6 +39,7 @@ export interface QuerySuggestionGetFnArgs {
|
|||
selectionStart: number;
|
||||
selectionEnd: number;
|
||||
signal?: AbortSignal;
|
||||
useTimeRange?: boolean;
|
||||
boolFilter?: any;
|
||||
}
|
||||
|
||||
|
|
|
@ -21,6 +21,26 @@ import { stubIndexPattern, stubFields } from '../../stubs';
|
|||
import { setupValueSuggestionProvider, ValueSuggestionsGetFn } from './value_suggestion_provider';
|
||||
import { IUiSettingsClient, CoreSetup } from 'kibana/public';
|
||||
|
||||
jest.mock('../../services', () => ({
|
||||
getQueryService: () => ({
|
||||
timefilter: {
|
||||
timefilter: {
|
||||
createFilter: () => {
|
||||
return {
|
||||
time: 'fake',
|
||||
};
|
||||
},
|
||||
getTime: () => {
|
||||
return {
|
||||
to: 'now',
|
||||
from: 'now-15m',
|
||||
};
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
}));
|
||||
|
||||
describe('FieldSuggestions', () => {
|
||||
let getValueSuggestions: ValueSuggestionsGetFn;
|
||||
let http: any;
|
||||
|
@ -94,6 +114,7 @@ describe('FieldSuggestions', () => {
|
|||
indexPattern: stubIndexPattern,
|
||||
field,
|
||||
query: '',
|
||||
useTimeRange: false,
|
||||
});
|
||||
|
||||
expect(http.fetch).toHaveBeenCalled();
|
||||
|
@ -107,6 +128,7 @@ describe('FieldSuggestions', () => {
|
|||
indexPattern: stubIndexPattern,
|
||||
field,
|
||||
query: '',
|
||||
useTimeRange: false,
|
||||
};
|
||||
|
||||
await getValueSuggestions(args);
|
||||
|
@ -123,6 +145,7 @@ describe('FieldSuggestions', () => {
|
|||
indexPattern: stubIndexPattern,
|
||||
field,
|
||||
query: '',
|
||||
useTimeRange: false,
|
||||
};
|
||||
|
||||
const { now } = Date;
|
||||
|
@ -146,50 +169,76 @@ describe('FieldSuggestions', () => {
|
|||
indexPattern: stubIndexPattern,
|
||||
field: fields[0],
|
||||
query: '',
|
||||
useTimeRange: false,
|
||||
});
|
||||
await getValueSuggestions({
|
||||
indexPattern: stubIndexPattern,
|
||||
field: fields[0],
|
||||
query: 'query',
|
||||
useTimeRange: false,
|
||||
});
|
||||
await getValueSuggestions({
|
||||
indexPattern: stubIndexPattern,
|
||||
field: fields[1],
|
||||
query: '',
|
||||
useTimeRange: false,
|
||||
});
|
||||
await getValueSuggestions({
|
||||
indexPattern: stubIndexPattern,
|
||||
field: fields[1],
|
||||
query: 'query',
|
||||
useTimeRange: false,
|
||||
});
|
||||
|
||||
const customIndexPattern = {
|
||||
...stubIndexPattern,
|
||||
title: 'customIndexPattern',
|
||||
useTimeRange: false,
|
||||
};
|
||||
|
||||
await getValueSuggestions({
|
||||
indexPattern: customIndexPattern,
|
||||
field: fields[0],
|
||||
query: '',
|
||||
useTimeRange: false,
|
||||
});
|
||||
await getValueSuggestions({
|
||||
indexPattern: customIndexPattern,
|
||||
field: fields[0],
|
||||
query: 'query',
|
||||
useTimeRange: false,
|
||||
});
|
||||
await getValueSuggestions({
|
||||
indexPattern: customIndexPattern,
|
||||
field: fields[1],
|
||||
query: '',
|
||||
useTimeRange: false,
|
||||
});
|
||||
await getValueSuggestions({
|
||||
indexPattern: customIndexPattern,
|
||||
field: fields[1],
|
||||
query: 'query',
|
||||
useTimeRange: false,
|
||||
});
|
||||
|
||||
expect(http.fetch).toHaveBeenCalledTimes(8);
|
||||
});
|
||||
|
||||
it('should apply timefilter', async () => {
|
||||
const [field] = stubFields.filter(
|
||||
({ type, aggregatable }) => type === 'string' && aggregatable
|
||||
);
|
||||
|
||||
await getValueSuggestions({
|
||||
indexPattern: stubIndexPattern,
|
||||
field,
|
||||
query: '',
|
||||
useTimeRange: true,
|
||||
});
|
||||
const callParams = http.fetch.mock.calls[0][1];
|
||||
|
||||
expect(JSON.parse(callParams.body).filters).toHaveLength(1);
|
||||
expect(http.fetch).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -17,15 +17,16 @@
|
|||
* under the License.
|
||||
*/
|
||||
|
||||
import dateMath from '@elastic/datemath';
|
||||
import { memoize } from 'lodash';
|
||||
import { CoreSetup } from 'src/core/public';
|
||||
import { IIndexPattern, IFieldType, UI_SETTINGS } from '../../../common';
|
||||
import { IIndexPattern, IFieldType, UI_SETTINGS, buildQueryFromFilters } from '../../../common';
|
||||
import { getQueryService } from '../../services';
|
||||
|
||||
function resolver(title: string, field: IFieldType, query: string, boolFilter: any) {
|
||||
function resolver(title: string, field: IFieldType, query: string, filters: any[]) {
|
||||
// Only cache results for a minute
|
||||
const ttl = Math.floor(Date.now() / 1000 / 60);
|
||||
|
||||
return [ttl, query, title, field.name, JSON.stringify(boolFilter)].join('|');
|
||||
return [ttl, query, title, field.name, JSON.stringify(filters)].join('|');
|
||||
}
|
||||
|
||||
export type ValueSuggestionsGetFn = (args: ValueSuggestionsGetFnArgs) => Promise<any[]>;
|
||||
|
@ -34,18 +35,31 @@ interface ValueSuggestionsGetFnArgs {
|
|||
indexPattern: IIndexPattern;
|
||||
field: IFieldType;
|
||||
query: string;
|
||||
useTimeRange?: boolean;
|
||||
boolFilter?: any[];
|
||||
signal?: AbortSignal;
|
||||
}
|
||||
|
||||
const getAutocompleteTimefilter = (indexPattern: IIndexPattern) => {
|
||||
const { timefilter } = getQueryService().timefilter;
|
||||
const timeRange = timefilter.getTime();
|
||||
|
||||
// Use a rounded timerange so that memoizing works properly
|
||||
const roundedTimerange = {
|
||||
from: dateMath.parse(timeRange.from)!.startOf('minute').toISOString(),
|
||||
to: dateMath.parse(timeRange.to)!.endOf('minute').toISOString(),
|
||||
};
|
||||
return timefilter.createFilter(indexPattern, roundedTimerange);
|
||||
};
|
||||
|
||||
export const getEmptyValueSuggestions = (() => Promise.resolve([])) as ValueSuggestionsGetFn;
|
||||
|
||||
export const setupValueSuggestionProvider = (core: CoreSetup): ValueSuggestionsGetFn => {
|
||||
const requestSuggestions = memoize(
|
||||
(index: string, field: IFieldType, query: string, boolFilter: any = [], signal?: AbortSignal) =>
|
||||
(index: string, field: IFieldType, query: string, filters: any = [], signal?: AbortSignal) =>
|
||||
core.http.fetch(`/api/kibana/suggestions/values/${index}`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ query, field: field.name, boolFilter }),
|
||||
body: JSON.stringify({ query, field: field.name, filters }),
|
||||
signal,
|
||||
}),
|
||||
resolver
|
||||
|
@ -55,12 +69,15 @@ export const setupValueSuggestionProvider = (core: CoreSetup): ValueSuggestionsG
|
|||
indexPattern,
|
||||
field,
|
||||
query,
|
||||
useTimeRange,
|
||||
boolFilter,
|
||||
signal,
|
||||
}: ValueSuggestionsGetFnArgs): Promise<any[]> => {
|
||||
const shouldSuggestValues = core!.uiSettings.get<boolean>(
|
||||
UI_SETTINGS.FILTERS_EDITOR_SUGGEST_VALUES
|
||||
);
|
||||
useTimeRange =
|
||||
useTimeRange ?? core!.uiSettings.get<boolean>(UI_SETTINGS.AUTOCOMPLETE_USE_TIMERANGE);
|
||||
const { title } = indexPattern;
|
||||
|
||||
if (field.type === 'boolean') {
|
||||
|
@ -69,6 +86,9 @@ export const setupValueSuggestionProvider = (core: CoreSetup): ValueSuggestionsG
|
|||
return [];
|
||||
}
|
||||
|
||||
return await requestSuggestions(title, field, query, boolFilter, signal);
|
||||
const timeFilter = useTimeRange ? getAutocompleteTimefilter(indexPattern) : undefined;
|
||||
const filterQuery = timeFilter ? buildQueryFromFilters([timeFilter], indexPattern).filter : [];
|
||||
const filters = [...(boolFilter ? boolFilter : []), ...filterQuery];
|
||||
return await requestSuggestions(title, field, query, filters, signal);
|
||||
};
|
||||
};
|
||||
|
|
|
@ -1840,6 +1840,8 @@ export interface QuerySuggestionGetFnArgs {
|
|||
selectionStart: number;
|
||||
// (undocumented)
|
||||
signal?: AbortSignal;
|
||||
// (undocumented)
|
||||
useTimeRange?: boolean;
|
||||
}
|
||||
|
||||
// Warning: (ae-missing-release-tag) "QuerySuggestionTypes" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
|
||||
|
@ -2309,6 +2311,7 @@ export const UI_SETTINGS: {
|
|||
readonly INDEXPATTERN_PLACEHOLDER: "indexPattern:placeholder";
|
||||
readonly FILTERS_PINNED_BY_DEFAULT: "filters:pinnedByDefault";
|
||||
readonly FILTERS_EDITOR_SUGGEST_VALUES: "filterEditor:suggestValues";
|
||||
readonly AUTOCOMPLETE_USE_TIMERANGE: "autocomplete:useTimeRange";
|
||||
};
|
||||
|
||||
|
||||
|
|
|
@ -24,9 +24,14 @@ 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 { calculateBounds, getTime, RefreshInterval, TimeRange } from '../../../common';
|
||||
import {
|
||||
calculateBounds,
|
||||
getTime,
|
||||
IIndexPattern,
|
||||
RefreshInterval,
|
||||
TimeRange,
|
||||
} from '../../../common';
|
||||
import { TimeHistoryContract } from './time_history';
|
||||
import { IndexPattern } from '../../index_patterns';
|
||||
|
||||
// TODO: remove!
|
||||
|
||||
|
@ -170,7 +175,7 @@ export class Timefilter {
|
|||
}
|
||||
};
|
||||
|
||||
public createFilter = (indexPattern: IndexPattern, timeRange?: TimeRange) => {
|
||||
public createFilter = (indexPattern: IIndexPattern, timeRange?: TimeRange) => {
|
||||
return getTime(indexPattern, timeRange ? timeRange : this._time, {
|
||||
forceNow: this.getForceNow(),
|
||||
});
|
||||
|
|
|
@ -85,6 +85,8 @@ export class PhraseSuggestorUI<T extends PhraseSuggestorProps> extends React.Com
|
|||
field,
|
||||
query,
|
||||
signal: this.abortController.signal,
|
||||
// Show all results in filter bar autocomplete
|
||||
useTimeRange: false,
|
||||
});
|
||||
|
||||
this.setState({ suggestions, isLoading: false });
|
||||
|
|
|
@ -45,7 +45,7 @@ export function registerValueSuggestionsRoute(
|
|||
{
|
||||
field: schema.string(),
|
||||
query: schema.string(),
|
||||
boolFilter: schema.maybe(schema.any()),
|
||||
filters: schema.maybe(schema.any()),
|
||||
},
|
||||
{ unknowns: 'allow' }
|
||||
),
|
||||
|
@ -53,7 +53,7 @@ export function registerValueSuggestionsRoute(
|
|||
},
|
||||
async (context, request, response) => {
|
||||
const config = await config$.pipe(first()).toPromise();
|
||||
const { field: fieldName, query, boolFilter } = request.body;
|
||||
const { field: fieldName, query, filters } = request.body;
|
||||
const { index } = request.params;
|
||||
const { client } = context.core.elasticsearch.legacy;
|
||||
const signal = getRequestAbortedSignal(request.events.aborted$);
|
||||
|
@ -66,7 +66,7 @@ export function registerValueSuggestionsRoute(
|
|||
const indexPattern = await findIndexPatternById(context.core.savedObjects.client, index);
|
||||
|
||||
const field = indexPattern && getFieldByName(fieldName, indexPattern);
|
||||
const body = await getBody(autocompleteSearchOptions, field || fieldName, query, boolFilter);
|
||||
const body = await getBody(autocompleteSearchOptions, field || fieldName, query, filters);
|
||||
|
||||
try {
|
||||
const result = await client.callAsCurrentUser('search', { index, body }, { signal });
|
||||
|
@ -88,7 +88,7 @@ async function getBody(
|
|||
{ timeout, terminate_after }: Record<string, any>,
|
||||
field: IFieldType | string,
|
||||
query: string,
|
||||
boolFilter: Filter[] = []
|
||||
filters: Filter[] = []
|
||||
) {
|
||||
const isFieldObject = (f: any): f is IFieldType => Boolean(f && f.name);
|
||||
|
||||
|
@ -108,7 +108,7 @@ async function getBody(
|
|||
terminate_after,
|
||||
query: {
|
||||
bool: {
|
||||
filter: boolFilter,
|
||||
filter: filters,
|
||||
},
|
||||
},
|
||||
aggs: {
|
||||
|
|
|
@ -1181,6 +1181,7 @@ export const UI_SETTINGS: {
|
|||
readonly INDEXPATTERN_PLACEHOLDER: "indexPattern:placeholder";
|
||||
readonly FILTERS_PINNED_BY_DEFAULT: "filters:pinnedByDefault";
|
||||
readonly FILTERS_EDITOR_SUGGEST_VALUES: "filterEditor:suggestValues";
|
||||
readonly AUTOCOMPLETE_USE_TIMERANGE: "autocomplete:useTimeRange";
|
||||
};
|
||||
|
||||
// Warning: (ae-missing-release-tag) "usageProvider" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
|
||||
|
|
|
@ -684,5 +684,17 @@ export function getUiSettings(): Record<string, UiSettingsParams<unknown>> {
|
|||
}),
|
||||
schema: schema.boolean(),
|
||||
},
|
||||
[UI_SETTINGS.AUTOCOMPLETE_USE_TIMERANGE]: {
|
||||
name: i18n.translate('data.advancedSettings.autocompleteIgnoreTimerange', {
|
||||
defaultMessage: 'Use time range',
|
||||
description: 'Restrict autocomplete results to the current time range',
|
||||
}),
|
||||
value: true,
|
||||
description: i18n.translate('data.advancedSettings.autocompleteIgnoreTimerangeText', {
|
||||
defaultMessage:
|
||||
'Disable this property to get autocomplete suggestions from your full dataset, rather than from the current time range.',
|
||||
}),
|
||||
schema: schema.boolean(),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
|
|
@ -26,23 +26,7 @@ export function getBoolFilter({
|
|||
serviceName?: string;
|
||||
urlParams: IUrlParams;
|
||||
}) {
|
||||
const { start, end } = urlParams;
|
||||
|
||||
if (!start || !end) {
|
||||
throw new Error('Date range was not defined');
|
||||
}
|
||||
|
||||
const boolFilter: ESFilter[] = [
|
||||
{
|
||||
range: {
|
||||
'@timestamp': {
|
||||
gte: new Date(start).getTime(),
|
||||
lte: new Date(end).getTime(),
|
||||
format: 'epoch_millis',
|
||||
},
|
||||
},
|
||||
},
|
||||
];
|
||||
const boolFilter: ESFilter[] = [];
|
||||
|
||||
if (serviceName) {
|
||||
boolFilter.push({
|
||||
|
|
|
@ -111,6 +111,7 @@ export function KueryBar() {
|
|||
query: inputValue,
|
||||
selectionStart,
|
||||
selectionEnd: selectionStart,
|
||||
useTimeRange: true,
|
||||
})) || []
|
||||
)
|
||||
.filter((suggestion) => !startsWith(suggestion.text, 'span.'))
|
||||
|
|
|
@ -27,7 +27,7 @@ const wrapAsSuggestions = (start: number, end: number, query: string, values: st
|
|||
|
||||
export const setupGetValueSuggestions: KqlQuerySuggestionProvider = () => {
|
||||
return async (
|
||||
{ indexPatterns, boolFilter, signal },
|
||||
{ indexPatterns, boolFilter, useTimeRange, signal },
|
||||
{ start, end, prefix, suffix, fieldName, nestedPath }
|
||||
): Promise<QuerySuggestion[]> => {
|
||||
const fullFieldName = nestedPath ? `${nestedPath}.${fieldName}` : fieldName;
|
||||
|
@ -49,6 +49,7 @@ export const setupGetValueSuggestions: KqlQuerySuggestionProvider = () => {
|
|||
field,
|
||||
query,
|
||||
boolFilter,
|
||||
useTimeRange,
|
||||
signal,
|
||||
}).then((valueSuggestions) => {
|
||||
const quotedValues = valueSuggestions.map((value) =>
|
||||
|
|
|
@ -67,7 +67,7 @@ export function KueryBar({
|
|||
let currentRequestCheck: string;
|
||||
|
||||
const [getUrlParams, updateUrlParams] = useUrlParams();
|
||||
const { search: kuery, dateRangeStart, dateRangeEnd } = getUrlParams();
|
||||
const { search: kuery } = getUrlParams();
|
||||
|
||||
useEffect(() => {
|
||||
updateSearchText(kuery);
|
||||
|
@ -105,16 +105,7 @@ export function KueryBar({
|
|||
query: inputValue,
|
||||
selectionStart: selectionStart || 0,
|
||||
selectionEnd: selectionStart || 0,
|
||||
boolFilter: [
|
||||
{
|
||||
range: {
|
||||
'@timestamp': {
|
||||
gte: dateRangeStart,
|
||||
lte: dateRangeEnd,
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
useTimeRange: true,
|
||||
})) || []
|
||||
).filter((suggestion: QuerySuggestion) => !suggestion.text.startsWith('span.'));
|
||||
if (currentRequest !== currentRequestCheck) {
|
||||
|
|
|
@ -10,13 +10,14 @@ import { FtrProviderContext } from '../../ftr_provider_context';
|
|||
export default function ({ getService, getPageObjects }: FtrProviderContext) {
|
||||
const esArchiver = getService('esArchiver');
|
||||
const queryBar = getService('queryBar');
|
||||
const PageObjects = getPageObjects(['common']);
|
||||
const PageObjects = getPageObjects(['common', 'timePicker']);
|
||||
|
||||
describe('value suggestions', function describeIndexTests() {
|
||||
before(async function () {
|
||||
await esArchiver.loadIfNeeded('logstash_functional');
|
||||
await esArchiver.load('dashboard/drilldowns');
|
||||
await PageObjects.common.navigateToApp('discover');
|
||||
await PageObjects.timePicker.setDefaultAbsoluteRange();
|
||||
});
|
||||
|
||||
after(async () => {
|
||||
|
|
|
@ -7,13 +7,17 @@
|
|||
import expect from '@kbn/expect';
|
||||
|
||||
export default function ({ getService, getPageObjects }) {
|
||||
const PageObjects = getPageObjects(['maps']);
|
||||
const PageObjects = getPageObjects(['maps', 'timePicker']);
|
||||
const security = getService('security');
|
||||
|
||||
describe('vector styling', () => {
|
||||
before(async () => {
|
||||
await security.testUser.setRoles(['test_logstash_reader', 'global_maps_all']);
|
||||
await PageObjects.maps.loadSavedMap('document example');
|
||||
await PageObjects.timePicker.setAbsoluteRange(
|
||||
'Mar 1, 2015 @ 00:00:00.000',
|
||||
'Mar 1, 2016 @ 00:00:00.000'
|
||||
);
|
||||
});
|
||||
after(async () => {
|
||||
await PageObjects.maps.refreshAndClearUnsavedChangesWarning();
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue