Skip to content

Commit

Permalink
[Data Explorer][Discover 2.0] restore single and surroundings doc view (
Browse files Browse the repository at this point in the history
#4816)

* add initial route logic to single/surroundings doc view and re-organize files
* restore surrounding doc view comp
* restore single doc view comp

Signed-off-by: ananzh <ananzh@amazon.com>
  • Loading branch information
ananzh committed Aug 30, 2023
1 parent 03ae2bd commit 4aa6d37
Show file tree
Hide file tree
Showing 38 changed files with 2,640 additions and 28 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,8 @@ import { DocViewInspectButton } from './data_grid_table_docview_inspect_button';
import { DataGridFlyout } from './data_grid_table_flyout';
import { DiscoverGridContextProvider } from './data_grid_table_context';
import { toolbarVisibility } from './constants';
import { DocViewFilterFn } from '../../doc_views/doc_views_types';
import { DocViewFilterFn, OpenSearchSearchHit } from '../../doc_views/doc_views_types';
import { DiscoverServices } from '../../../build_services';
import { OpenSearchSearchHit } from '../../doc_views/doc_views_types';
import { usePagination } from '../utils/use_pagination';
import { SortOrder } from '../../../saved_searches/types';

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ import { mount, shallow } from 'enzyme';
import { DocViewer } from './doc_viewer';
import { findTestSubject } from 'test_utils/helpers';
import { getDocViewsRegistry } from '../../../opensearch_dashboards_services';
import { DocViewRenderProps } from '../../doc_views/doc_views_types';
import { DocViewRenderProps } from '../../../doc_views/doc_views_types';

jest.mock('../../../opensearch_dashboards_services', () => {
let registry: any[] = [];
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@
import React from 'react';
import { mount } from 'enzyme';
import { DocViewRenderTab } from './doc_viewer_render_tab';
import { DocViewRenderProps } from '../../doc_views/doc_views_types';
import { DocViewRenderProps } from '../../../doc_views/doc_views_types';

test('Mounting and unmounting DocViewerRenderTab', () => {
const unmountFn = jest.fn();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@
*/

import React, { useRef, useEffect } from 'react';
import { DocViewRenderFn, DocViewRenderProps } from '../../doc_views/doc_views_types';
import { DocViewRenderFn, DocViewRenderProps } from '../../../doc_views/doc_views_types';

interface Props {
render: DocViewRenderFn;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import React from 'react';
import { shallow } from 'enzyme';
import { DocViewerLinks } from './doc_viewer_links';
import { getDocViewsLinksRegistry } from '../../../opensearch_dashboards_services';
import { DocViewLinkRenderProps } from '../../doc_views_links/doc_views_links_types';
import { DocViewLinkRenderProps } from '../doc_views/doc_views_links/doc_views_links_types';

jest.mock('../../../opensearch_dashboards_services', () => {
let registry: any[] = [];
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
# Discover Context App Implementation Notes

## Principles
**Single Source of Truth**: A good user experience depends on the UI displaying consistent information across the whole page. To achieve this, there should always be a single source of truth for the application's state. In the updated application, this is managed via the useContextState and useQueryActions hooks, which manage the application state and actions respectively.

**Unidirectional Data Flow**: While a single state promotes rendering consistency, it does little to make the state changes easier to reason about. To avoid having state mutations scattered all over the code, this app implements a unidirectional data flow architecture. That means that the state is treated as immutable throughout the application except for actions, which may modify it to cause re-render and updates.

**Unit-Testability**: Creating unit tests for large parts of the UI code is made easy by expressing as much of the logic as possible as side-effect-free functions. The only place where side-effects are allowed are actions.

**Loose Coupling**: An attempt was made to couple the parts that make up this app as loosely as possible. This means using pure functions whenever possible and isolating the components diligently. It does not access the OpenSearch Dashboards AppState directly but communicates only via its properties.

## Concepts
To adhere to the principles mentioned above, this app borrows some concepts from the redux architecture that forms a circular unidirectional data flow.

**State**: The `contextAppState` and `contextQueryState` are the single sources of truth and may only be modified by actions.

**Action**: Actions are functions that are called in response to user or system actions and may modify the state they are bound to via their closure. For example, the `setContextAppState` and `fetchSurroundingRows` functions in the `useContextState` and `useQueryActions` hooks, respectively.

## Implementation
The updated application leverages React hooks to manage state and actions. The useContextState hook manages the application state and provides functions to update the state, while the useQueryActions hook manages the fetching of documents and provides functions to fetch the anchor document, surrounding documents, and all documents.

The `useContextState` hook uses the useState and useEffect hooks to manage the application state and side effects. The `contextAppState` is the application state, and the `setContextAppState` function is used to update the state. The useEffect hook is used to reset the `contextQueryState` and to fetch the surrounding documents based on the `contextAppState`.

The `useQueryActions` hook uses the useState, useMemo, and useCallback hooks to manage the query state and actions. The `contextQueryState` is the query state, and various functions are provided to update the state and fetch documents. The useMemo hook is used to derive the rows from the contextQueryState, and the useCallback hook is used to create memoized versions of the functions that update the state and fetch documents.

The `useQueryActions` hook provides several functions for fetching documents:

**fetchAnchorRow**: Fetches the anchor document.

**fetchSurroundingRows**: Fetches the surrounding documents (predecessors or successors) of the anchor document.

**fetchContextRows**: Fetches both the predecessors and successors of the anchor document.

**fetchAllRows**: Fetches the anchor document and then fetches the surrounding documents.
**resetContextQueryState**: Resets the contextQueryState to its initial state.

These functions update the `contextQueryState` to reflect the loading status and the fetched documents.


## Directory Structure

**components/action_bar**: Defines the `ActionBar` component.

**api/anchor.ts**: Exports `fetchAnchor()` function that creates and executes the query for the anchor document. It also exports `updateSearchSource()` function which updates the search source with specified parameters.

**api/context.ts**: Exports `fetchSurroundingDocs()` function that fetches the surrounding documents (either successors or predecessors) of a specified anchor document. It also exports `createSearchSource()` function that creates a search source with specified index pattern and filters.

**api/utils**: Exports various functions used to create and transform
queries.

**utils/context_state**: Exports functions for fetching surrounding documents, creating a search source, and managing application and global states. Additionally, several helper functions are exported for comparing filters and states, retrieving filters from a state, and creating the initial app state. The module also defines constants for the global and app state URL keys.

**utils/use_query_actions**: Defines a React hook to manage and fetch data related to OpenSearch documents. The hook maintains a local state, contextQueryState, to track the status of fetching operations and the fetched documents. It provides several functions: `fetchAnchorRow()` to fetch the anchor document, `fetchSurroundingRows()` to fetch surrounding documents, `fetchContextRows()` to fetch both predecessors and successors, and `fetchAllRows()` to fetch the anchor and all surrounding documents. Additionally, it provides a `resetContextQueryState()` function to reset the local state to its initial value. Each fetch operation updates the contextQueryState and, in case of an error, displays a toast notification with a failure message.

**utils/use_context_query**: Defines a React hook that manages the application state and synchronization with the URL and OpenSearch Dashboards services. The `startSync` and `stopSync` functions are used to start and stop the synchronization of the application state with the URL. The `setContextAppState` function is used to update the application state and immediately reflect the changes in the URL. The hook returns the current `contextAppState` and the `setContextAppState` function, which can be used by the components that consume this hook.
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
// Prefix all styles with "cxt" to avoid conflicts.
// Examples
// cxtChart
// cxtChart__legend
// cxtChart__legend--small
// cxtChart__legend-isLoading

@import "components/action_bar/index";
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
/*
* 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 sinon, { SinonSpy } from 'sinon';
import moment from 'moment';
import { OpenSearchHitRecordList } from './context';

type Hit = {
[key in string]: number;
} & {
sort: [number, number];
};

interface SearchSourceStub {
_stubHits: any[];
_stubTimeField?: string;
_createStubHit: (timestamp: any, tiebreaker?: number) => Record<string, any>;
setParent: SinonSpy;
setField: SinonSpy;
getField: SinonSpy;
fetch: SinonSpy;
}

export function createIndexPatternsStub() {
return {
get: sinon.spy((indexPatternId) =>
Promise.resolve({
id: indexPatternId,
isTimeNanosBased: () => false,
popularizeField: () => {},
})
),
};
}

/**
* A stubbed search source with a `fetch` method that returns all of `_stubHits`.
*/
export function createSearchSourceStub(hits: OpenSearchHitRecordList, timeField?: string) {
const searchSourceStub: Partial<SearchSourceStub> = {
_stubHits: hits,
_stubTimeField: timeField,
_createStubHit: (timestamp: number, tiebreaker = 0) => ({
[searchSourceStub._stubTimeField]: timestamp,
sort: [timestamp, tiebreaker],
}),
};

searchSourceStub.setParent = sinon.spy(() => searchSourceStub);
searchSourceStub.setField = sinon.spy(() => searchSourceStub);

searchSourceStub.getField = sinon.spy((key: string) => {
const previousSetCall = searchSourceStub.setField?.withArgs(key).lastCall;
return previousSetCall ? previousSetCall.args[1] : null;
});

searchSourceStub.fetch = sinon.spy(() =>
Promise.resolve({
hits: {
hits: searchSourceStub._stubHits,
total: searchSourceStub._stubHits.length,
},
})
);

return searchSourceStub as SearchSourceStub;
}

/**
* A stubbed search source with a `fetch` method that returns a filtered set of `_stubHits`.
*/
export function createContextSearchSourceStub(
hits: OpenSearchHitRecordList,
timeField: string = '@timestamp'
) {
const searchSourceStub = createSearchSourceStub(hits, timeField);

searchSourceStub.fetch = sinon.spy(() => {
const timeFieldStr = searchSourceStub._stubTimeField as string;
const lastQuery = searchSourceStub.setField.withArgs('query').lastCall.args[1];
const timeRange = lastQuery.query.bool.must.constant_score.filter.range[timeFieldStr];
const lastSort = searchSourceStub.setField.withArgs('sort').lastCall.args[1];
const sortDirection = lastSort[0][timeFieldStr];
const sortFunction =
sortDirection === 'asc'
? (first: Hit, second: Hit) => first[timeFieldStr] - second[timeFieldStr]
: (first: Hit, second: Hit) => second[timeFieldStr] - first[timeFieldStr];
const filteredHits = searchSourceStub._stubHits
.filter(
(hit: Hit) =>
moment(hit[timeFieldStr]).isSameOrAfter(timeRange.gte) &&
moment(hit[timeFieldStr]).isSameOrBefore(timeRange.lte)
)
.sort(sortFunction);

return Promise.resolve({
hits: {
hits: filteredHits,
total: filteredHits.length,
},
});
});

return searchSourceStub;
}
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;
}
Loading

0 comments on commit 4aa6d37

Please sign in to comment.