mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 01:38:56 -04:00
Date nanos context typescript conversion (#38735)
This commit is contained in:
parent
5badcbd0cc
commit
2d9975c147
13 changed files with 425 additions and 306 deletions
|
@ -48,7 +48,25 @@ describe('context app', function () {
|
|||
getSearchSourceStub = createSearchSourceStubProvider([], '@timestamp', MS_PER_DAY * 8);
|
||||
Private.stub(SearchSourceProvider, getSearchSourceStub);
|
||||
|
||||
fetchPredecessors = Private(fetchContextProvider).fetchPredecessors;
|
||||
fetchPredecessors = (indexPatternId, timeField, sortDir, timeValIso, timeValNr, tieBreakerField, tieBreakerValue, size) => {
|
||||
const anchor = {
|
||||
_source: {
|
||||
[timeField]: timeValIso
|
||||
},
|
||||
sort: [timeValNr, tieBreakerValue]
|
||||
};
|
||||
|
||||
return Private(fetchContextProvider).fetchSurroundingDocs(
|
||||
'predecessors',
|
||||
indexPatternId,
|
||||
anchor,
|
||||
timeField,
|
||||
tieBreakerField,
|
||||
sortDir,
|
||||
size,
|
||||
[]
|
||||
);
|
||||
};
|
||||
}));
|
||||
|
||||
it('should perform exactly one query when enough hits are returned', function () {
|
||||
|
|
|
@ -47,7 +47,25 @@ describe('context app', function () {
|
|||
getSearchSourceStub = createSearchSourceStubProvider([], '@timestamp');
|
||||
Private.stub(SearchSourceProvider, getSearchSourceStub);
|
||||
|
||||
fetchSuccessors = Private(fetchContextProvider).fetchSuccessors;
|
||||
fetchSuccessors = (indexPatternId, timeField, sortDir, timeValIso, timeValNr, tieBreakerField, tieBreakerValue, size) => {
|
||||
const anchor = {
|
||||
_source: {
|
||||
[timeField]: timeValIso
|
||||
},
|
||||
sort: [timeValNr, tieBreakerValue]
|
||||
};
|
||||
|
||||
return Private(fetchContextProvider).fetchSurroundingDocs(
|
||||
'successors',
|
||||
indexPatternId,
|
||||
anchor,
|
||||
timeField,
|
||||
tieBreakerField,
|
||||
sortDir,
|
||||
size,
|
||||
[]
|
||||
);
|
||||
};
|
||||
}));
|
||||
|
||||
it('should perform exactly one query when enough hits are returned', function () {
|
||||
|
|
|
@ -1,251 +0,0 @@
|
|||
/*
|
||||
* 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.
|
||||
*/
|
||||
// @ts-check
|
||||
|
||||
// @ts-ignore
|
||||
import { SearchSourceProvider } from 'ui/courier';
|
||||
import { reverseSortDirection } from './utils/sorting';
|
||||
import {
|
||||
extractNanoSeconds,
|
||||
convertIsoToNanosAsStr,
|
||||
convertIsoToMillis,
|
||||
convertTimeValueToIso
|
||||
} from './utils/date_conversion';
|
||||
|
||||
/**
|
||||
* @typedef {Object} SearchResult
|
||||
* @prop {{ total: number, hits: any[] }} hits
|
||||
* @prop {Object} aggregations
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {Object} SearchSourceT
|
||||
* @prop {function(): Promise<SearchResult>} fetch
|
||||
* @prop {function(string, any): SearchSourceT} setField
|
||||
* @prop {function(any): SearchSourceT} setParent
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {'asc' | 'desc'} SortDirection
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {'successors' |'predecessors'} SurroundingDocType
|
||||
*/
|
||||
|
||||
const DAY_MILLIS = 24 * 60 * 60 * 1000;
|
||||
|
||||
// look from 1 day up to 10000 days into the past and future
|
||||
const LOOKUP_OFFSETS = [0, 1, 7, 30, 365, 10000].map((days) => days * DAY_MILLIS);
|
||||
|
||||
function fetchContextProvider(indexPatterns, Private) {
|
||||
/**
|
||||
* @type {{new(): SearchSourceT}}
|
||||
*/
|
||||
const SearchSource = Private(SearchSourceProvider);
|
||||
|
||||
return {
|
||||
// @ts-ignore / for testing
|
||||
fetchPredecessors: (...args) => fetchSurroundingDocs('predecessors', ...args),
|
||||
// @ts-ignore / for testing
|
||||
fetchSuccessors: (...args) => fetchSurroundingDocs('successors', ...args),
|
||||
fetchSurroundingDocs,
|
||||
};
|
||||
|
||||
/**
|
||||
* Fetch successor or predecessor documents of a given anchor document
|
||||
*
|
||||
* @param {SurroundingDocType} type - `successors` or `predecessors`
|
||||
* @param {string} indexPatternId
|
||||
* @param {string} timeFieldName - name of the timefield, that's sorted on
|
||||
* @param {SortDirection} timeFieldSortDir - direction of sorting
|
||||
* @param {string} timeFieldIsoValue - value of the anchors timefield in ISO format
|
||||
* @param {number} timeFieldNumValue - value of the anchors timefield in numeric format (invalid for nanos)
|
||||
* @param {string} tieBreakerField - name of 2nd param for sorting
|
||||
* @param {string} tieBreakerValue - value of 2nd param for sorting
|
||||
* @param {number} size - number of records to retrieve
|
||||
* @param {any[]} filters - to apply in the elastic query
|
||||
* @returns {Promise<object[]>}
|
||||
*/
|
||||
async function fetchSurroundingDocs(
|
||||
type,
|
||||
indexPatternId,
|
||||
timeFieldName,
|
||||
timeFieldSortDir,
|
||||
timeFieldIsoValue,
|
||||
timeFieldNumValue,
|
||||
tieBreakerField,
|
||||
tieBreakerValue,
|
||||
size,
|
||||
filters
|
||||
) {
|
||||
const indexPattern = await indexPatterns.get(indexPatternId);
|
||||
const searchSource = await createSearchSource(indexPattern, filters);
|
||||
const sortDir = type === 'successors' ? timeFieldSortDir : reverseSortDirection(timeFieldSortDir);
|
||||
const nanoSeconds = indexPattern.isTimeNanosBased() ? extractNanoSeconds(timeFieldIsoValue) : '';
|
||||
const timeValueMillis = nanoSeconds !== '' ? convertIsoToMillis(timeFieldIsoValue) : timeFieldNumValue;
|
||||
|
||||
const offsetSign = (timeFieldSortDir === 'asc' && type === 'successors' || timeFieldSortDir === 'desc' && type === 'predecessors')
|
||||
? 1
|
||||
: -1;
|
||||
|
||||
// ending with `null` opens the last interval
|
||||
const intervals = asPairs([...LOOKUP_OFFSETS.map(offset => timeValueMillis + offset * offsetSign), null]);
|
||||
|
||||
let documents = [];
|
||||
for (const [iStartTimeValue, iEndTimeValue] of intervals) {
|
||||
const remainingSize = size - documents.length;
|
||||
|
||||
if (remainingSize <= 0) {
|
||||
break;
|
||||
}
|
||||
const afterTimeRecIdx = type === 'successors' && documents.length ? documents.length - 1 : 0;
|
||||
const afterTimeValue = nanoSeconds
|
||||
? convertIsoToNanosAsStr(documents.length ? documents[afterTimeRecIdx]._source[timeFieldName] : timeFieldIsoValue)
|
||||
: timeFieldNumValue;
|
||||
const afterTieBreakerValue = documents.length > 0 ? documents[afterTimeRecIdx].sort[1] : tieBreakerValue;
|
||||
|
||||
const hits = await fetchHitsInInterval(
|
||||
searchSource,
|
||||
timeFieldName,
|
||||
sortDir,
|
||||
iStartTimeValue,
|
||||
iEndTimeValue,
|
||||
afterTimeValue,
|
||||
tieBreakerField,
|
||||
afterTieBreakerValue,
|
||||
remainingSize,
|
||||
nanoSeconds
|
||||
);
|
||||
|
||||
documents = type === 'successors'
|
||||
? [...documents, ...hits]
|
||||
: [...hits.slice().reverse(), ...documents];
|
||||
}
|
||||
|
||||
return documents;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Object} indexPattern
|
||||
* @param {any[]} filters
|
||||
* @returns {Promise<Object>}
|
||||
*/
|
||||
async function createSearchSource(indexPattern, filters) {
|
||||
return new SearchSource()
|
||||
.setParent(false)
|
||||
.setField('index', indexPattern)
|
||||
.setField('filter', filters);
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch the hits between `(afterTimeValue, tieBreakerValue)` and
|
||||
* `endRangeMillis` from the `searchSource` using the given `timeField` and
|
||||
* `tieBreakerField` fields up to a maximum of `maxCount` documents. The
|
||||
* documents are sorted by `(timeField, tieBreakerField)` using the
|
||||
* `timeSortDirection` for both fields
|
||||
*
|
||||
* The `searchSource` is assumed to have the appropriate index pattern
|
||||
* and filters set.
|
||||
*
|
||||
* @param {SearchSourceT} searchSource
|
||||
* @param {string} timeFieldName
|
||||
* @param {SortDirection} timeFieldSortDir
|
||||
* @param {number} startRangeMillis
|
||||
* @param {number | null} endRangeMillis
|
||||
* @param {number| string} afterTimeValue
|
||||
* @param {string} tieBreakerField
|
||||
* @param {number} tieBreakerValue
|
||||
* @param {number} maxCount
|
||||
* @param {string} nanosValue
|
||||
* @returns {Promise<object[]>}
|
||||
*/
|
||||
async function fetchHitsInInterval(
|
||||
searchSource,
|
||||
timeFieldName,
|
||||
timeFieldSortDir,
|
||||
startRangeMillis,
|
||||
endRangeMillis,
|
||||
afterTimeValue,
|
||||
tieBreakerField,
|
||||
tieBreakerValue,
|
||||
maxCount,
|
||||
nanosValue
|
||||
) {
|
||||
|
||||
const startRange = {
|
||||
[timeFieldSortDir === 'asc' ? 'gte' : 'lte']: convertTimeValueToIso(startRangeMillis, nanosValue),
|
||||
};
|
||||
const endRange = endRangeMillis === null ? {} : {
|
||||
[timeFieldSortDir === 'asc' ? 'lte' : 'gte']: convertTimeValueToIso(endRangeMillis, nanosValue),
|
||||
};
|
||||
|
||||
const response = await searchSource
|
||||
.setField('size', maxCount)
|
||||
.setField('query', {
|
||||
query: {
|
||||
constant_score: {
|
||||
filter: {
|
||||
range: {
|
||||
[timeFieldName]: {
|
||||
format: 'strict_date_optional_time',
|
||||
...startRange,
|
||||
...endRange,
|
||||
}
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
language: 'lucene'
|
||||
})
|
||||
.setField('searchAfter', [
|
||||
afterTimeValue,
|
||||
tieBreakerValue
|
||||
])
|
||||
.setField('sort', [
|
||||
{ [timeFieldName]: timeFieldSortDir },
|
||||
{ [tieBreakerField]: timeFieldSortDir },
|
||||
])
|
||||
.setField('version', true)
|
||||
.fetch();
|
||||
|
||||
return response.hits ? response.hits.hits : [];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a sequence of pairs from the iterable that looks like
|
||||
* `[[x_0, x_1], [x_1, x_2], [x_2, x_3], ..., [x_(n-1), x_n]]`.
|
||||
*
|
||||
* @param {Iterable<any>} iterable
|
||||
* @returns {IterableIterator<(any[])>}
|
||||
*/
|
||||
function* asPairs(iterable) {
|
||||
let currentPair = [];
|
||||
for (const value of iterable) {
|
||||
currentPair = [...currentPair, value].slice(-2);
|
||||
if (currentPair.length === 2) {
|
||||
yield currentPair;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export {
|
||||
fetchContextProvider,
|
||||
};
|
123
src/legacy/core_plugins/kibana/public/context/api/context.ts
Normal file
123
src/legacy/core_plugins/kibana/public/context/api/context.ts
Normal file
|
@ -0,0 +1,123 @@
|
|||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
// @ts-ignore
|
||||
import { SearchSourceProvider, SearchSource } from 'ui/courier';
|
||||
import { IPrivate } from 'ui/private';
|
||||
import { IndexPatternEnhanced, IndexPatternGetProvider } from 'ui/index_patterns/_index_pattern';
|
||||
import { Filter } from '@kbn/es-query';
|
||||
import { reverseSortDir, SortDirection } from './utils/sorting';
|
||||
import { extractNanos, convertIsoToMillis } from './utils/date_conversion';
|
||||
import { fetchHitsInInterval } from './utils/fetch_hits_in_interval';
|
||||
import { generateIntervals } from './utils/generate_intervals';
|
||||
import { getEsQuerySearchAfter } from './utils/get_es_query_search_after';
|
||||
import { getEsQuerySort } from './utils/get_es_query_sort';
|
||||
|
||||
export type SurrDocType = 'successors' | 'predecessors';
|
||||
export interface EsHitRecord {
|
||||
fields: Record<string, any>;
|
||||
sort: number[];
|
||||
_source: Record<string, any>;
|
||||
}
|
||||
export type EsHitRecordList = EsHitRecord[];
|
||||
|
||||
const DAY_MILLIS = 24 * 60 * 60 * 1000;
|
||||
|
||||
// look from 1 day up to 10000 days into the past and future
|
||||
const LOOKUP_OFFSETS = [0, 1, 7, 30, 365, 10000].map(days => days * DAY_MILLIS);
|
||||
|
||||
function fetchContextProvider(indexPatterns: IndexPatternGetProvider, Private: IPrivate) {
|
||||
const SearchSourcePrivate: any = Private(SearchSourceProvider);
|
||||
|
||||
return {
|
||||
fetchSurroundingDocs,
|
||||
};
|
||||
|
||||
/**
|
||||
* Fetch successor or predecessor documents of a given anchor document
|
||||
*
|
||||
* @param {SurrDocType} type - `successors` or `predecessors`
|
||||
* @param {string} indexPatternId
|
||||
* @param {EsHitRecord} anchor - anchor record
|
||||
* @param {string} timeField - name of the timefield, that's sorted on
|
||||
* @param {string} tieBreakerField - name of the tie breaker, the 2nd sort field
|
||||
* @param {SortDirection} sortDir - direction of sorting
|
||||
* @param {number} size - number of records to retrieve
|
||||
* @param {Filter[]} filters - to apply in the elastic query
|
||||
* @returns {Promise<object[]>}
|
||||
*/
|
||||
async function fetchSurroundingDocs(
|
||||
type: SurrDocType,
|
||||
indexPatternId: string,
|
||||
anchor: EsHitRecord,
|
||||
timeField: string,
|
||||
tieBreakerField: string,
|
||||
sortDir: SortDirection,
|
||||
size: number,
|
||||
filters: Filter[]
|
||||
) {
|
||||
const indexPattern = await indexPatterns.get(indexPatternId);
|
||||
const searchSource = await createSearchSource(indexPattern, filters);
|
||||
const sortDirToApply = type === 'successors' ? sortDir : reverseSortDir(sortDir);
|
||||
|
||||
const nanos = indexPattern.isTimeNanosBased() ? extractNanos(anchor._source[timeField]) : '';
|
||||
const timeValueMillis =
|
||||
nanos !== '' ? convertIsoToMillis(anchor._source[timeField]) : anchor.sort[0];
|
||||
|
||||
const intervals = generateIntervals(LOOKUP_OFFSETS, timeValueMillis, type, sortDir);
|
||||
let documents: EsHitRecordList = [];
|
||||
|
||||
for (const interval of intervals) {
|
||||
const remainingSize = size - documents.length;
|
||||
|
||||
if (remainingSize <= 0) {
|
||||
break;
|
||||
}
|
||||
|
||||
const searchAfter = getEsQuerySearchAfter(type, documents, timeField, anchor, nanos);
|
||||
|
||||
const sort = getEsQuerySort(timeField, tieBreakerField, sortDirToApply);
|
||||
|
||||
const hits = await fetchHitsInInterval(
|
||||
searchSource,
|
||||
timeField,
|
||||
sort,
|
||||
sortDirToApply,
|
||||
interval,
|
||||
searchAfter,
|
||||
remainingSize,
|
||||
nanos
|
||||
);
|
||||
|
||||
documents =
|
||||
type === 'successors' ? [...documents, ...hits] : [...hits.slice().reverse(), ...documents];
|
||||
}
|
||||
|
||||
return documents;
|
||||
}
|
||||
|
||||
async function createSearchSource(indexPattern: IndexPatternEnhanced, filters: Filter[]) {
|
||||
return new SearchSourcePrivate()
|
||||
.setParent(false)
|
||||
.setField('index', indexPattern)
|
||||
.setField('filter', filters);
|
||||
}
|
||||
}
|
||||
|
||||
export { fetchContextProvider };
|
|
@ -16,19 +16,11 @@
|
|||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import { reverseSortDir, SortDirection } from '../sorting';
|
||||
|
||||
import expect from '@kbn/expect';
|
||||
|
||||
import {
|
||||
reverseSortDirection,
|
||||
} from '../sorting';
|
||||
|
||||
|
||||
describe('context app', function () {
|
||||
describe('function reverseSortDirection', function () {
|
||||
it('should reverse a direction given as a string', function () {
|
||||
expect(reverseSortDirection('asc')).to.eql('desc');
|
||||
expect(reverseSortDirection('desc')).to.eql('asc');
|
||||
});
|
||||
describe('function reverseSortDir', function() {
|
||||
test('reverse a given sort direction', function() {
|
||||
expect(reverseSortDir(SortDirection.asc)).toBe(SortDirection.desc);
|
||||
expect(reverseSortDir(SortDirection.desc)).toBe(SortDirection.asc);
|
||||
});
|
||||
});
|
|
@ -24,7 +24,7 @@ import moment from 'moment';
|
|||
* 9ns -> 000000009
|
||||
* 10000ns -> 0000010000
|
||||
*/
|
||||
export function extractNanoSeconds(timeFieldValue: string = ''): string {
|
||||
export function extractNanos(timeFieldValue: string = ''): string {
|
||||
const fractionSeconds = timeFieldValue.split('.')[1].replace('Z', '');
|
||||
return fractionSeconds.length !== 9 ? fractionSeconds.padEnd(9, '0') : fractionSeconds;
|
||||
}
|
||||
|
@ -33,7 +33,7 @@ export function extractNanoSeconds(timeFieldValue: string = ''): string {
|
|||
* extract the nanoseconds as string of a given ISO formatted timestamp
|
||||
*/
|
||||
export function convertIsoToNanosAsStr(isoValue: string): string {
|
||||
const nanos = extractNanoSeconds(isoValue);
|
||||
const nanos = extractNanos(isoValue);
|
||||
const millis = convertIsoToMillis(isoValue);
|
||||
return `${millis}${nanos.substr(3, 6)}`;
|
||||
}
|
||||
|
|
|
@ -0,0 +1,82 @@
|
|||
/*
|
||||
* 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 { SearchSource } from 'ui/courier';
|
||||
import { convertTimeValueToIso } from './date_conversion';
|
||||
import { SortDirection } from './sorting';
|
||||
import { EsHitRecordList } from '../context';
|
||||
import { IntervalValue } from './generate_intervals';
|
||||
import { EsQuerySort } from './get_es_query_sort';
|
||||
import { EsQuerySearchAfter } from './get_es_query_search_after';
|
||||
|
||||
interface RangeQuery {
|
||||
format: string;
|
||||
lte?: string | null;
|
||||
gte?: string | null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch the hits between a given `interval` up to a maximum of `maxCount` documents.
|
||||
* The documents are sorted by `sort`
|
||||
*
|
||||
* The `searchSource` is assumed to have the appropriate index pattern
|
||||
* and filters set.
|
||||
*/
|
||||
export async function fetchHitsInInterval(
|
||||
searchSource: SearchSource,
|
||||
timeField: string,
|
||||
sort: EsQuerySort,
|
||||
sortDir: SortDirection,
|
||||
interval: IntervalValue[],
|
||||
searchAfter: EsQuerySearchAfter,
|
||||
maxCount: number,
|
||||
nanosValue: string
|
||||
): Promise<EsHitRecordList> {
|
||||
const range: RangeQuery = {
|
||||
format: 'strict_date_optional_time',
|
||||
};
|
||||
const [start, stop] = interval;
|
||||
|
||||
if (start) {
|
||||
range[sortDir === SortDirection.asc ? 'gte' : 'lte'] = convertTimeValueToIso(start, nanosValue);
|
||||
}
|
||||
|
||||
if (stop) {
|
||||
range[sortDir === SortDirection.asc ? 'lte' : 'gte'] = convertTimeValueToIso(stop, nanosValue);
|
||||
}
|
||||
const response = await searchSource
|
||||
.setField('size', maxCount)
|
||||
.setField('query', {
|
||||
query: {
|
||||
constant_score: {
|
||||
filter: {
|
||||
range: {
|
||||
[timeField]: range,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
language: 'lucene',
|
||||
})
|
||||
.setField('searchAfter', searchAfter)
|
||||
.setField('sort', sort)
|
||||
.setField('version', true)
|
||||
.fetch();
|
||||
|
||||
return response.hits ? response.hits.hits : [];
|
||||
}
|
|
@ -0,0 +1,54 @@
|
|||
/*
|
||||
* 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 { SortDirection } from './sorting';
|
||||
|
||||
export type IntervalValue = number | null;
|
||||
|
||||
/**
|
||||
* Generate a sequence of pairs from the iterable that looks like
|
||||
* `[[x_0, x_1], [x_1, x_2], [x_2, x_3], ..., [x_(n-1), x_n]]`.
|
||||
*/
|
||||
export function* asPairs(iterable: Iterable<IntervalValue>): IterableIterator<IntervalValue[]> {
|
||||
let currentPair: IntervalValue[] = [];
|
||||
for (const value of iterable) {
|
||||
currentPair = [...currentPair, value].slice(-2);
|
||||
if (currentPair.length === 2) {
|
||||
yield currentPair;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a iterable containing intervals `[start,end]` for Elasticsearch date range queries
|
||||
* depending on type (`successors` or `predecessors`) and sort (`asc`, `desc`) these are ascending or descending intervals.
|
||||
*/
|
||||
export function generateIntervals(
|
||||
offsets: number[],
|
||||
startTime: number,
|
||||
type: string,
|
||||
sort: SortDirection
|
||||
): IterableIterator<IntervalValue[]> {
|
||||
const offsetSign =
|
||||
(sort === SortDirection.asc && type === 'successors') ||
|
||||
(sort === SortDirection.desc && type === 'predecessors')
|
||||
? 1
|
||||
: -1;
|
||||
// ending with `null` opens the last interval
|
||||
return asPairs([...offsets.map(offset => startTime + offset * offsetSign), null]);
|
||||
}
|
|
@ -0,0 +1,52 @@
|
|||
/*
|
||||
* 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 { convertIsoToNanosAsStr } from './date_conversion';
|
||||
import { SurrDocType, EsHitRecordList, EsHitRecord } from '../context';
|
||||
|
||||
export type EsQuerySearchAfter = [string | number, string | number];
|
||||
|
||||
/**
|
||||
* Get the searchAfter query value for elasticsearch
|
||||
* When there are already documents available, which means successors or predecessors
|
||||
* were already fetched, the new searchAfter for the next fetch has to be the sort value
|
||||
* of the first (prececessor), or last (successor) of the list
|
||||
*/
|
||||
export function getEsQuerySearchAfter(
|
||||
type: SurrDocType,
|
||||
documents: EsHitRecordList,
|
||||
timeFieldName: string,
|
||||
anchor: EsHitRecord,
|
||||
nanoSeconds: string
|
||||
): EsQuerySearchAfter {
|
||||
if (documents.length) {
|
||||
// already surrounding docs -> first or last record is used
|
||||
const afterTimeRecIdx = type === 'successors' && documents.length ? documents.length - 1 : 0;
|
||||
const afterTimeDoc = documents[afterTimeRecIdx];
|
||||
const afterTimeValue = nanoSeconds
|
||||
? convertIsoToNanosAsStr(afterTimeDoc._source[timeFieldName])
|
||||
: afterTimeDoc.sort[0];
|
||||
return [afterTimeValue, afterTimeDoc.sort[1]];
|
||||
}
|
||||
// if data_nanos adapt timestamp value for sorting, since numeric value was rounded by browser
|
||||
// ES search_after also works when number is provided as string
|
||||
return [
|
||||
nanoSeconds ? convertIsoToNanosAsStr(anchor._source[timeFieldName]) : anchor.sort[0],
|
||||
anchor.sort[1],
|
||||
];
|
||||
}
|
|
@ -0,0 +1,38 @@
|
|||
/*
|
||||
* 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 { SortDirection } from './sorting';
|
||||
|
||||
type EsQuerySortValue = Record<string, SortDirection>;
|
||||
|
||||
export type EsQuerySort = [EsQuerySortValue, EsQuerySortValue];
|
||||
|
||||
/**
|
||||
* Returns `EsQuerySort` which is used to sort records in the ES query
|
||||
* https://www.elastic.co/guide/en/elasticsearch/reference/current/search-request-sort.html
|
||||
* @param timeField
|
||||
* @param tieBreakerField
|
||||
* @param sortDir
|
||||
*/
|
||||
export function getEsQuerySort(
|
||||
timeField: string,
|
||||
tieBreakerField: string,
|
||||
sortDir: SortDirection
|
||||
): EsQuerySort {
|
||||
return [{ [timeField]: sortDir }, { [tieBreakerField]: sortDir }];
|
||||
}
|
|
@ -17,52 +17,36 @@
|
|||
* under the License.
|
||||
*/
|
||||
|
||||
import { IndexPattern } from 'src/legacy/core_plugins/data/public';
|
||||
|
||||
export enum SortDirection {
|
||||
asc = 'asc',
|
||||
desc = 'desc',
|
||||
}
|
||||
|
||||
/**
|
||||
* The list of field names that are allowed for sorting, but not included in
|
||||
* index pattern fields.
|
||||
*
|
||||
* @constant
|
||||
* @type {string[]}
|
||||
*/
|
||||
const META_FIELD_NAMES = ['_seq_no', '_doc', '_uid'];
|
||||
const META_FIELD_NAMES: string[] = ['_seq_no', '_doc', '_uid'];
|
||||
|
||||
/**
|
||||
* Returns a field from the intersection of the set of sortable fields in the
|
||||
* given index pattern and a given set of candidate field names.
|
||||
*
|
||||
* @param {IndexPattern} indexPattern - The index pattern to search for
|
||||
* sortable fields
|
||||
* @param {string[]} fields - The list of candidate field names
|
||||
*
|
||||
* @returns {string[]}
|
||||
*/
|
||||
function getFirstSortableField(indexPattern, fieldNames) {
|
||||
const sortableFields = fieldNames.filter((fieldName) => (
|
||||
META_FIELD_NAMES.includes(fieldName)
|
||||
|| (indexPattern.fields.byName[fieldName] || { sortable: false }).sortable
|
||||
));
|
||||
export function getFirstSortableField(indexPattern: IndexPattern, fieldNames: string[]) {
|
||||
const sortableFields = fieldNames.filter(
|
||||
fieldName =>
|
||||
META_FIELD_NAMES.includes(fieldName) ||
|
||||
// @ts-ignore
|
||||
(indexPattern.fields.byName[fieldName] || { sortable: false }).sortable
|
||||
);
|
||||
return sortableFields[0];
|
||||
}
|
||||
|
||||
/**
|
||||
* A sort order string.
|
||||
*
|
||||
* @typedef {('asc'|'desc')} SortDirection
|
||||
*/
|
||||
|
||||
/**
|
||||
* Return the reversed sort direction.
|
||||
*
|
||||
* @param {(SortDirection)} sortDirection
|
||||
*
|
||||
* @returns {(SortDirection)}
|
||||
*/
|
||||
function reverseSortDirection(sortDirection) {
|
||||
return (sortDirection === 'asc' ? 'desc' : 'asc');
|
||||
export function reverseSortDir(sortDirection: SortDirection) {
|
||||
return sortDirection === SortDirection.asc ? SortDirection.desc : SortDirection.asc;
|
||||
}
|
||||
|
||||
|
||||
export {
|
||||
getFirstSortableField,
|
||||
reverseSortDirection,
|
||||
};
|
|
@ -108,17 +108,16 @@ export function QueryActionsProvider(Private, Promise) {
|
|||
}
|
||||
|
||||
setLoadingStatus(state)(type);
|
||||
const [sortField, sortDir] = sort;
|
||||
|
||||
return Promise.try(() => (
|
||||
fetchSurroundingDocs(
|
||||
type,
|
||||
indexPatternId,
|
||||
sort[0],
|
||||
sort[1],
|
||||
anchor.fields[sort[0]][0],
|
||||
anchor.sort[0],
|
||||
anchor,
|
||||
sortField,
|
||||
tieBreakerField,
|
||||
anchor.sort[1],
|
||||
sortDir,
|
||||
count,
|
||||
filters
|
||||
)
|
||||
|
|
|
@ -29,6 +29,16 @@ export interface IndexPattern {
|
|||
title: string;
|
||||
timeFieldName?: string;
|
||||
}
|
||||
// 'enhanced' IndexPattern returned by IndexPatternProvider
|
||||
// reason for having this seperate interface are several tests
|
||||
// that currently depend on an interface without methods to succeed
|
||||
export interface IndexPatternEnhanced extends IndexPattern {
|
||||
isTimeNanosBased: () => boolean;
|
||||
}
|
||||
|
||||
export interface IndexPatternGetProvider {
|
||||
get: (id: string) => IndexPatternEnhanced;
|
||||
}
|
||||
|
||||
export interface StaticIndexPatternField {
|
||||
name: string;
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue