Skip to content

Commit

Permalink
[Data Explorer] Migrate surrounding doc viewer to Flyout
Browse files Browse the repository at this point in the history
In this PR:
* separate context application and discover state.
* allow view to register multiple redux slices
* add surrounding flyout
* restore original context application to surrounding flyout
* restore and simplify the fetch logic from context application

Issue Resolve
opensearch-project#4231
opensearch-project#4230

Signed-off-by: ananzh <[email protected]>
  • Loading branch information
ananzh committed Aug 14, 2023
1 parent 86768bc commit f55f221
Show file tree
Hide file tree
Showing 35 changed files with 1,937 additions and 80 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ export interface ViewDefinition<T = any> {
readonly title: string;
readonly ui?: {
defaults: T | (() => T) | (() => Promise<T>);
slice: Slice<T>;
slices: Slice[];
};
readonly Canvas: LazyExoticComponent<(props: ViewProps) => React.ReactElement>;
readonly Panel: LazyExoticComponent<(props: ViewProps) => React.ReactElement>;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,21 @@ export const getPreloadedState = async (

// defaults can be a function or an object
if (typeof defaults === 'function') {
rootState[view.id] = await defaults();
const defaultResult = await defaults();

// Check if the result contains the view's ID key.
// This is used to distinguish between a single registered state and multiple states.
// Multiple registered states should return an object with multiple key-value pairs with one key always equal to view.id.
if (view.id in defaultResult) {
for (const key in defaultResult) {
// The body of a for-in should be wrapped in an if statement to filter unwanted properties from the prototype
if (defaultResult.hasOwnProperty(key)) {
rootState[key] = defaultResult[key];
}
}
} else {
rootState[view.id] = defaultResult;
}
} else {
rootState[view.id] = defaults;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,13 +33,14 @@ export const configurePreloadedStore = (preloadedState: PreloadedState<RootState
};

export const getPreloadedStore = async (services: DataExplorerServices) => {
// For each view preload the data and register the slice
// For each view preload the data and register the slices
const views = services.viewRegistry.all();
views.forEach((view) => {
if (!view.ui) return;

const { slice } = view.ui;
registerSlice(slice);
view.ui.slices.forEach((slice) => {
registerSlice(slice);
});
});

const preloadedState = await loadReduxState(services);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
/*
* SPDX-License-Identifier: Apache-2.0
*
* The OpenSearch Contributors require contributions made to
* this file be licensed under the Apache-2.0 license or a
* compatible open source license.
*
* Any modifications Copyright OpenSearch Contributors. See
* GitHub history for details.
*/

/*
* 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 { i18n } from '@osd/i18n';

import {
ISearchSource,
OpenSearchQuerySortValue,
IndexPattern,
} from '../../../../../../data/public';
import { OpenSearchHitRecord } from './context';

export async function fetchAnchor(
anchorId: string,
indexPattern: IndexPattern,
searchSource: ISearchSource,
sort: OpenSearchQuerySortValue[]
): Promise<OpenSearchHitRecord> {
updateSearchSource(searchSource, anchorId, sort, indexPattern);

const response = await searchSource.fetch();
const doc = response.hits?.hits?.[0];

if (!doc) {
throw new Error(
i18n.translate('discover.context.failedToLoadAnchorDocumentErrorDescription', {
defaultMessage: 'Failed to load anchor document.',
})
);
}

return {
...doc,
isAnchor: true,
} as OpenSearchHitRecord;
}

export function updateSearchSource(
searchSource: ISearchSource,
anchorId: string,
sort: OpenSearchQuerySortValue[],
indexPattern: IndexPattern
) {
searchSource
.setParent(undefined)
.setField('index', indexPattern)
.setField('version', true)
.setField('size', 1)
.setField('query', {
query: {
constant_score: {
filter: {
ids: {
values: [anchorId],
},
},
},
},
language: 'lucene',
})
.setField('sort', sort);

return searchSource;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
/*
* SPDX-License-Identifier: Apache-2.0
*
* The OpenSearch Contributors require contributions made to
* this file be licensed under the Apache-2.0 license or a
* compatible open source license.
*
* Any modifications Copyright OpenSearch Contributors. See
* GitHub history for details.
*/

/*
* 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 { Filter, IndexPattern } from 'src/plugins/data/public';
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 { getOpenSearchQuerySearchAfter } from './utils/get_opensearch_query_search_after';
import { getOpenSearchQuerySort } from './utils/get_opensearch_query_sort';
import { getServices } from '../../../../opensearch_dashboards_services';

export enum SurrDocType {
SUCCESSORS = 'successors',
PREDECESSORS = 'predecessors',
}
export interface OpenSearchHitRecord {
fields: Record<string, any>;
sort: number[];
_source: Record<string, any>;
_id: string;
isAnchor?: boolean;
}
export type OpenSearchHitRecordList = OpenSearchHitRecord[];

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);

/**
* Fetch successor or predecessor documents of a given anchor document
*
* @param {SurrDocType} type - `successors` or `predecessors`
* @param {string} indexPatternId
* @param {OpenSearchHitRecord} 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 query
* @returns {Promise<object[]>}
*/

export async function fetchSurroundingDocs(
type: SurrDocType,
indexPattern: IndexPattern,
anchor: OpenSearchHitRecord,
tieBreakerField: string,
sortDir: SortDirection,
size: number,
filters: Filter[]
) {
if (typeof anchor !== 'object' || anchor === null || !size) {
return [];
}
const timeField = indexPattern.timeFieldName!;
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: OpenSearchHitRecordList = [];

for (const interval of intervals) {
const remainingSize = size - documents.length;

if (remainingSize <= 0) {
break;
}

const searchAfter = getOpenSearchQuerySearchAfter(type, documents, timeField, anchor, nanos);

const sort = getOpenSearchQuerySort(timeField, tieBreakerField, sortDirToApply);

const hits = await fetchHitsInInterval(
searchSource,
timeField,
sort,
sortDirToApply,
interval,
searchAfter,
remainingSize,
nanos,
anchor._id
);

documents =
type === 'successors' ? [...documents, ...hits] : [...hits.slice().reverse(), ...documents];
}

return documents;
}

export async function createSearchSource(indexPattern: IndexPattern, filters: Filter[]) {
const { data } = getServices();

const searchSource = await data.search.searchSource.create();
return searchSource
.setParent(undefined)
.setField('index', indexPattern)
.setField('filter', filters);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
/*
* SPDX-License-Identifier: Apache-2.0
*
* The OpenSearch Contributors require contributions made to
* this file be licensed under the Apache-2.0 license or a
* compatible open source license.
*
* Any modifications Copyright OpenSearch Contributors. See
* GitHub history for details.
*/

/*
* 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 { extractNanos } from './date_conversion';

describe('function extractNanos', function () {
test('extract nanos of 2014-01-01', function () {
expect(extractNanos('2014-01-01')).toBe('000000000');
});
test('extract nanos of 2014-01-01T12:12:12.234Z', function () {
expect(extractNanos('2014-01-01T12:12:12.234Z')).toBe('234000000');
});
test('extract nanos of 2014-01-01T12:12:12.234123321Z', function () {
expect(extractNanos('2014-01-01T12:12:12.234123321Z')).toBe('234123321');
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
/*
* SPDX-License-Identifier: Apache-2.0
*
* The OpenSearch Contributors require contributions made to
* this file be licensed under the Apache-2.0 license or a
* compatible open source license.
*
* Any modifications Copyright OpenSearch Contributors. See
* GitHub history for details.
*/

/*
* 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 moment from 'moment';
/**
* extract nanoseconds if available in ISO timestamp
* returns the nanos as string like this:
* 9ns -> 000000009
* 10000ns -> 0000010000
* returns 000000000 for invalid timestamps or timestamps with just date
**/
export function extractNanos(timeFieldValue: string = ''): string {
const fieldParts = timeFieldValue.split('.');
const fractionSeconds = fieldParts.length === 2 ? fieldParts[1].replace('Z', '') : '';
return fractionSeconds.length !== 9 ? fractionSeconds.padEnd(9, '0') : fractionSeconds;
}

/**
* convert an iso formatted string to number of milliseconds since
* 1970-01-01T00:00:00.000Z
* @param {string} isoValue
* @returns {number}
*/
export function convertIsoToMillis(isoValue: string): number {
const date = new Date(isoValue);
return date.getTime();
}
/**
* the given time value in milliseconds is converted to a ISO formatted string
* if nanosValue is provided, the given value replaces the fractional seconds part
* of the formated string since moment.js doesn't support formatting timestamps
* with a higher precision then microseconds
* The browser rounds date nanos values:
* 2019-09-18T06:50:12.999999999 -> browser rounds to 1568789413000000000
* 2019-09-18T06:50:59.999999999 -> browser rounds to 1568789460000000000
* 2017-12-31T23:59:59.999999999 -> browser rounds 1514761199999999999 to 1514761200000000000
*/
export function convertTimeValueToIso(timeValueMillis: number, nanosValue: string): string | null {
if (!timeValueMillis) {
return null;
}
const isoString = moment(timeValueMillis).toISOString();
if (!isoString) {
return null;
} else if (nanosValue !== '') {
return `${isoString.substring(0, isoString.length - 4)}${nanosValue}Z`;
}
return isoString;
}
Loading

0 comments on commit f55f221

Please sign in to comment.