mirror of
https://github.com/elastic/kibana.git
synced 2025-04-25 02:09:32 -04:00
[Discover] Migrate angular context app controllers to react (#100530)
* [Discover] migrate remaining context files from js to ts * [Discover] get rid of any types * [Discover] replace constants with enums, update imports * [Discover] use unknown instead of any, correct types * [Discover] skip any type for tests * [Discover] add euiDataGrid view * [Discover] add support dataGrid columns, provide ability to do not change sorting, highlight anchor doc, rename legacy variables * [Discover] update context_legacy test and types * [Discover] update unit tests, add context header * [Discover] update unit and functional tests * [Discover] remove docTable from context test which uses new data grid * [Discover] update EsHitRecord type, use it for context app. add no pagination support * [Discover] resolve type error in test * [Discover] move fetching methods * [Discover] complete fetching part * [Discover] remove redundant controller * [Discover] split up context state and components * [Discover] revert redundant css class * [Discover] rename component * [Discover] revert to upstream changes * [Discover] return upstream changes * [Discover] refactoring, context test update * [Discover] add tests for fetching methods, remove redundant files * [Discover] remove redundant angular utils, add filter test * [Discover] refactor error feedback * [Discover] fix functional test * [Discover] provide defaultSize * [Discover] clean up code * [Discover] fix eslint error * [Discover] fiix context settings * [Discover] return tieBreaker field check * [Discover] optimize things * [Discover] optimize rerenders * Update src/plugins/discover/public/application/components/context_app/context_app.tsx Co-authored-by: Matthias Wilhelm <ankertal@gmail.com> * [Discover] resolve comments * [Discover] replace url instead of pushing to history. refactoring Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> Co-authored-by: Matthias Wilhelm <ankertal@gmail.com>
This commit is contained in:
parent
246e7be3e5
commit
0cfd04c87d
40 changed files with 1200 additions and 1158 deletions
|
@ -18,4 +18,5 @@ export const indexPatternsMock = ({
|
|||
return indexPatternMock;
|
||||
}
|
||||
},
|
||||
} as unknown) as IndexPatternsService;
|
||||
updateSavedObject: jest.fn(),
|
||||
} as unknown) as jest.Mocked<IndexPatternsService>;
|
||||
|
|
|
@ -7,7 +7,13 @@
|
|||
*/
|
||||
|
||||
import { IUiSettingsClient } from 'kibana/public';
|
||||
import { DEFAULT_COLUMNS_SETTING, DOC_TABLE_LEGACY, SAMPLE_SIZE_SETTING } from '../../common';
|
||||
import {
|
||||
CONTEXT_TIE_BREAKER_FIELDS_SETTING,
|
||||
DEFAULT_COLUMNS_SETTING,
|
||||
DOC_TABLE_LEGACY,
|
||||
SAMPLE_SIZE_SETTING,
|
||||
SEARCH_FIELDS_FROM_SOURCE,
|
||||
} from '../../common';
|
||||
|
||||
export const uiSettingsMock = ({
|
||||
get: (key: string) => {
|
||||
|
@ -17,6 +23,10 @@ export const uiSettingsMock = ({
|
|||
return ['default_column'];
|
||||
} else if (key === DOC_TABLE_LEGACY) {
|
||||
return true;
|
||||
} else if (key === CONTEXT_TIE_BREAKER_FIELDS_SETTING) {
|
||||
return ['_doc'];
|
||||
} else if (key === SEARCH_FIELDS_FROM_SOURCE) {
|
||||
return false;
|
||||
}
|
||||
},
|
||||
} as unknown) as IUiSettingsClient;
|
||||
|
|
|
@ -1,10 +1,5 @@
|
|||
<context-app
|
||||
anchor-id="contextAppRoute.anchorId"
|
||||
columns="contextAppRoute.state.columns"
|
||||
<context-app-legacy
|
||||
index-pattern="contextAppRoute.indexPattern"
|
||||
app-state="contextAppRoute.state"
|
||||
state-container="contextAppRoute.stateContainer"
|
||||
filters="contextAppRoute.filters"
|
||||
predecessor-count="contextAppRoute.state.predecessorCount"
|
||||
successor-count="contextAppRoute.state.successorCount"
|
||||
sort="contextAppRoute.state.sort"></context-app>
|
||||
index-pattern-id="contextAppRoute.indexPatternId"
|
||||
anchor-id="contextAppRoute.anchorId">
|
||||
</context-app-legacy>
|
||||
|
|
|
@ -6,12 +6,8 @@
|
|||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import _ from 'lodash';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { CONTEXT_DEFAULT_SIZE_SETTING } from '../../../common';
|
||||
import { getAngularModule, getServices } from '../../kibana_services';
|
||||
import './context_app';
|
||||
import { getState } from './context_state';
|
||||
import contextAppRouteTemplate from './context.html';
|
||||
import { getRootBreadcrumbs } from '../helpers/breadcrumbs';
|
||||
|
||||
|
@ -28,9 +24,14 @@ const k7Breadcrumbs = () => {
|
|||
|
||||
getAngularModule().config(($routeProvider) => {
|
||||
$routeProvider.when('/context/:indexPatternId/:id*', {
|
||||
controller: ContextAppRouteController,
|
||||
controller: function ($routeParams, $scope, $route) {
|
||||
this.indexPattern = $route.current.locals.indexPattern.ip;
|
||||
this.anchorId = $routeParams.id;
|
||||
this.indexPatternId = $route.current.params.indexPatternId;
|
||||
},
|
||||
k7Breadcrumbs,
|
||||
controllerAs: 'contextAppRoute',
|
||||
reloadOnSearch: false,
|
||||
resolve: {
|
||||
indexPattern: ($route, Promise) => {
|
||||
const indexPattern = getServices().indexPatterns.get($route.current.params.indexPatternId);
|
||||
|
@ -40,57 +41,3 @@ getAngularModule().config(($routeProvider) => {
|
|||
template: contextAppRouteTemplate,
|
||||
});
|
||||
});
|
||||
|
||||
function ContextAppRouteController($routeParams, $scope, $route) {
|
||||
const filterManager = getServices().filterManager;
|
||||
const indexPattern = $route.current.locals.indexPattern.ip;
|
||||
const stateContainer = getState({
|
||||
defaultStepSize: getServices().uiSettings.get(CONTEXT_DEFAULT_SIZE_SETTING),
|
||||
timeFieldName: indexPattern.timeFieldName,
|
||||
storeInSessionStorage: getServices().uiSettings.get('state:storeInSessionStorage'),
|
||||
history: getServices().history(),
|
||||
toasts: getServices().core.notifications.toasts,
|
||||
uiSettings: getServices().core.uiSettings,
|
||||
});
|
||||
const {
|
||||
startSync: startStateSync,
|
||||
stopSync: stopStateSync,
|
||||
appState,
|
||||
getFilters,
|
||||
setFilters,
|
||||
setAppState,
|
||||
flushToUrl,
|
||||
} = stateContainer;
|
||||
this.stateContainer = stateContainer;
|
||||
this.state = { ...appState.getState() };
|
||||
this.anchorId = $routeParams.id;
|
||||
this.indexPattern = indexPattern;
|
||||
filterManager.setFilters(_.cloneDeep(getFilters()));
|
||||
startStateSync();
|
||||
|
||||
// take care of parameter changes in UI
|
||||
$scope.$watchGroup(
|
||||
[
|
||||
'contextAppRoute.state.columns',
|
||||
'contextAppRoute.state.predecessorCount',
|
||||
'contextAppRoute.state.successorCount',
|
||||
],
|
||||
(newValues) => {
|
||||
const [columns, predecessorCount, successorCount] = newValues;
|
||||
if (Array.isArray(columns) && predecessorCount >= 0 && successorCount >= 0) {
|
||||
setAppState({ columns, predecessorCount, successorCount });
|
||||
flushToUrl(true);
|
||||
}
|
||||
}
|
||||
);
|
||||
// take care of parameter filter changes
|
||||
const filterObservable = filterManager.getUpdates$().subscribe(() => {
|
||||
setFilters(filterManager);
|
||||
$route.reload();
|
||||
});
|
||||
|
||||
$scope.$on('$destroy', () => {
|
||||
stopStateSync();
|
||||
filterObservable.unsubscribe();
|
||||
});
|
||||
}
|
||||
|
|
|
@ -9,7 +9,7 @@
|
|||
import moment from 'moment';
|
||||
import { get, last } from 'lodash';
|
||||
import { createIndexPatternsStub, createContextSearchSourceStub } from './_stubs';
|
||||
import { EsHitRecordList, fetchContextProvider } from './context';
|
||||
import { EsHitRecordList, fetchContextProvider, SurrDocType } from './context';
|
||||
import { setServices, SortDirection } from '../../../../kibana_services';
|
||||
import { EsHitRecord } from './context';
|
||||
import { Query } from '../../../../../../data/public';
|
||||
|
@ -73,7 +73,7 @@ describe('context app', function () {
|
|||
};
|
||||
|
||||
return fetchContextProvider(createIndexPatternsStub()).fetchSurroundingDocs(
|
||||
'predecessors',
|
||||
SurrDocType.PREDECESSORS,
|
||||
indexPatternId,
|
||||
anchor as EsHitRecord,
|
||||
timeField,
|
||||
|
@ -265,7 +265,7 @@ describe('context app', function () {
|
|||
};
|
||||
|
||||
return fetchContextProvider(createIndexPatternsStub(), true).fetchSurroundingDocs(
|
||||
'predecessors',
|
||||
SurrDocType.PREDECESSORS,
|
||||
indexPatternId,
|
||||
anchor as EsHitRecord,
|
||||
timeField,
|
||||
|
|
|
@ -12,7 +12,7 @@ import { get, last } from 'lodash';
|
|||
import { createIndexPatternsStub, createContextSearchSourceStub } from './_stubs';
|
||||
import { setServices, SortDirection } from '../../../../kibana_services';
|
||||
import { Query } from '../../../../../../data/public';
|
||||
import { EsHitRecordList, fetchContextProvider } from './context';
|
||||
import { EsHitRecordList, fetchContextProvider, SurrDocType } from './context';
|
||||
import { EsHitRecord } from './context';
|
||||
import { DiscoverServices } from '../../../../build_services';
|
||||
|
||||
|
@ -73,7 +73,7 @@ describe('context app', function () {
|
|||
};
|
||||
|
||||
return fetchContextProvider(createIndexPatternsStub()).fetchSurroundingDocs(
|
||||
'successors',
|
||||
SurrDocType.SUCCESSORS,
|
||||
indexPatternId,
|
||||
anchor as EsHitRecord,
|
||||
timeField,
|
||||
|
@ -268,7 +268,7 @@ describe('context app', function () {
|
|||
};
|
||||
|
||||
return fetchContextProvider(createIndexPatternsStub(), true).fetchSurroundingDocs(
|
||||
'successors',
|
||||
SurrDocType.SUCCESSORS,
|
||||
indexPatternId,
|
||||
anchor as EsHitRecord,
|
||||
timeField,
|
||||
|
|
|
@ -16,7 +16,11 @@ import { getEsQuerySearchAfter } from './utils/get_es_query_search_after';
|
|||
import { getEsQuerySort } from './utils/get_es_query_sort';
|
||||
import { getServices } from '../../../../kibana_services';
|
||||
|
||||
export type SurrDocType = 'successors' | 'predecessors';
|
||||
export enum SurrDocType {
|
||||
SUCCESSORS = 'successors',
|
||||
PREDECESSORS = 'predecessors',
|
||||
}
|
||||
|
||||
export type EsHitRecord = Required<
|
||||
Pick<
|
||||
estypes.SearchResponse['hits']['hits'][number],
|
||||
|
@ -68,7 +72,7 @@ function fetchContextProvider(indexPatterns: IndexPatternsContract, useNewFields
|
|||
}
|
||||
const indexPattern = await indexPatterns.get(indexPatternId);
|
||||
const searchSource = await createSearchSource(indexPattern, filters);
|
||||
const sortDirToApply = type === 'successors' ? sortDir : reverseSortDir(sortDir);
|
||||
const sortDirToApply = type === SurrDocType.SUCCESSORS ? sortDir : reverseSortDir(sortDir);
|
||||
|
||||
const nanos = indexPattern.isTimeNanosBased() ? extractNanos(anchor.fields[timeField][0]) : '';
|
||||
const timeValueMillis =
|
||||
|
@ -108,7 +112,9 @@ function fetchContextProvider(indexPatterns: IndexPatternsContract, useNewFields
|
|||
);
|
||||
|
||||
documents =
|
||||
type === 'successors' ? [...documents, ...hits] : [...hits.slice().reverse(), ...documents];
|
||||
type === SurrDocType.SUCCESSORS
|
||||
? [...documents, ...hits]
|
||||
: [...hits.slice().reverse(), ...documents];
|
||||
}
|
||||
|
||||
return documents;
|
||||
|
|
|
@ -7,6 +7,7 @@
|
|||
*/
|
||||
|
||||
import { SortDirection } from '../../../../../../../data/public';
|
||||
import { SurrDocType } from '../context';
|
||||
|
||||
export type IntervalValue = number | null;
|
||||
|
||||
|
@ -31,12 +32,12 @@ export function* asPairs(iterable: Iterable<IntervalValue>): IterableIterator<In
|
|||
export function generateIntervals(
|
||||
offsets: number[],
|
||||
startTime: number,
|
||||
type: string,
|
||||
type: SurrDocType,
|
||||
sort: SortDirection
|
||||
): IterableIterator<IntervalValue[]> {
|
||||
const offsetSign =
|
||||
(sort === SortDirection.asc && type === 'successors') ||
|
||||
(sort === SortDirection.desc && type === 'predecessors')
|
||||
(sort === SortDirection.asc && type === SurrDocType.SUCCESSORS) ||
|
||||
(sort === SortDirection.desc && type === SurrDocType.PREDECESSORS)
|
||||
? 1
|
||||
: -1;
|
||||
// ending with `null` opens the last interval
|
||||
|
|
|
@ -26,7 +26,8 @@ export function getEsQuerySearchAfter(
|
|||
): EsQuerySearchAfter {
|
||||
if (documents.length) {
|
||||
// already surrounding docs -> first or last record is used
|
||||
const afterTimeRecIdx = type === 'successors' && documents.length ? documents.length - 1 : 0;
|
||||
const afterTimeRecIdx =
|
||||
type === SurrDocType.SUCCESSORS && documents.length ? documents.length - 1 : 0;
|
||||
const afterTimeDoc = documents[afterTimeRecIdx];
|
||||
let afterTimeValue = afterTimeDoc.sort[0] as string | number;
|
||||
if (nanoSeconds) {
|
||||
|
|
|
@ -10,10 +10,14 @@ import React from 'react';
|
|||
import { mountWithIntl } from '@kbn/test/jest';
|
||||
import { ActionBar, ActionBarProps } from './action_bar';
|
||||
import { findTestSubject } from '@elastic/eui/lib/test';
|
||||
import { MAX_CONTEXT_SIZE, MIN_CONTEXT_SIZE } from '../../query_parameters/constants';
|
||||
import {
|
||||
MAX_CONTEXT_SIZE,
|
||||
MIN_CONTEXT_SIZE,
|
||||
} from '../../../../components/context_app/utils/constants';
|
||||
import { SurrDocType } from '../../api/context';
|
||||
|
||||
describe('Test Discover Context ActionBar for successor | predecessor records', () => {
|
||||
['successors', 'predecessors'].forEach((type) => {
|
||||
[SurrDocType.SUCCESSORS, SurrDocType.PREDECESSORS].forEach((type) => {
|
||||
const onChangeCount = jest.fn();
|
||||
const props = {
|
||||
defaultStepSize: 5,
|
||||
|
@ -31,7 +35,7 @@ describe('Test Discover Context ActionBar for successor | predecessor records',
|
|||
|
||||
test(`${type}: Load button click`, () => {
|
||||
btn.simulate('click');
|
||||
expect(onChangeCount).toHaveBeenCalledWith(25);
|
||||
expect(onChangeCount).toHaveBeenCalledWith(type, 25);
|
||||
});
|
||||
|
||||
test(`${type}: Load button click doesnt submit when MAX_CONTEXT_SIZE was reached`, () => {
|
||||
|
@ -44,13 +48,13 @@ describe('Test Discover Context ActionBar for successor | predecessor records',
|
|||
test(`${type}: Count input change submits on blur`, () => {
|
||||
input.simulate('change', { target: { valueAsNumber: 123 } });
|
||||
input.simulate('blur');
|
||||
expect(onChangeCount).toHaveBeenCalledWith(123);
|
||||
expect(onChangeCount).toHaveBeenCalledWith(type, 123);
|
||||
});
|
||||
|
||||
test(`${type}: Count input change submits on return`, () => {
|
||||
input.simulate('change', { target: { valueAsNumber: 124 } });
|
||||
input.simulate('submit');
|
||||
expect(onChangeCount).toHaveBeenCalledWith(124);
|
||||
expect(onChangeCount).toHaveBeenCalledWith(type, 124);
|
||||
});
|
||||
|
||||
test(`${type}: Count input doesnt submits values higher than MAX_CONTEXT_SIZE `, () => {
|
||||
|
@ -68,7 +72,7 @@ describe('Test Discover Context ActionBar for successor | predecessor records',
|
|||
});
|
||||
|
||||
test(`${type}: Warning about limitation of additional records`, () => {
|
||||
if (type === 'predecessors') {
|
||||
if (type === SurrDocType.PREDECESSORS) {
|
||||
expect(findTestSubject(wrapper, 'predecessorsWarningMsg').text()).toBe(
|
||||
'No documents newer than the anchor could be found.'
|
||||
);
|
||||
|
|
|
@ -19,7 +19,10 @@ import {
|
|||
} from '@elastic/eui';
|
||||
import { ActionBarWarning } from './action_bar_warning';
|
||||
import { SurrDocType } from '../../api/context';
|
||||
import { MAX_CONTEXT_SIZE, MIN_CONTEXT_SIZE } from '../../query_parameters/constants';
|
||||
import {
|
||||
MAX_CONTEXT_SIZE,
|
||||
MIN_CONTEXT_SIZE,
|
||||
} from '../../../../components/context_app/utils/constants';
|
||||
|
||||
export interface ActionBarProps {
|
||||
/**
|
||||
|
@ -45,9 +48,10 @@ export interface ActionBarProps {
|
|||
isLoading: boolean;
|
||||
/**
|
||||
* is triggered when the input containing count is changed
|
||||
* @param type
|
||||
* @param count
|
||||
*/
|
||||
onChangeCount: (count: number) => void;
|
||||
onChangeCount: (type: SurrDocType, count: number) => void;
|
||||
/**
|
||||
* can be `predecessors` or `successors`, usage in context:
|
||||
* predecessors action bar + records (these are newer records)
|
||||
|
@ -67,13 +71,13 @@ export function ActionBar({
|
|||
type,
|
||||
}: ActionBarProps) {
|
||||
const showWarning = !isDisabled && !isLoading && docCountAvailable < docCount;
|
||||
const isSuccessor = type === 'successors';
|
||||
const isSuccessor = type === SurrDocType.SUCCESSORS;
|
||||
const [newDocCount, setNewDocCount] = useState(docCount);
|
||||
const isValid = (value: number) => value >= MIN_CONTEXT_SIZE && value <= MAX_CONTEXT_SIZE;
|
||||
const onSubmit = (ev: React.FormEvent<HTMLFormElement>) => {
|
||||
ev.preventDefault();
|
||||
if (newDocCount !== docCount && isValid(newDocCount)) {
|
||||
onChangeCount(newDocCount);
|
||||
onChangeCount(type, newDocCount);
|
||||
}
|
||||
};
|
||||
useEffect(() => {
|
||||
|
@ -100,7 +104,7 @@ export function ActionBar({
|
|||
const value = newDocCount + defaultStepSize;
|
||||
if (isValid(value)) {
|
||||
setNewDocCount(value);
|
||||
onChangeCount(value);
|
||||
onChangeCount(type, value);
|
||||
}
|
||||
}}
|
||||
flush="right"
|
||||
|
@ -131,7 +135,7 @@ export function ActionBar({
|
|||
}}
|
||||
onBlur={() => {
|
||||
if (newDocCount !== docCount && isValid(newDocCount)) {
|
||||
onChangeCount(newDocCount);
|
||||
onChangeCount(type, newDocCount);
|
||||
}
|
||||
}}
|
||||
type="number"
|
||||
|
|
|
@ -12,7 +12,7 @@ import { EuiCallOut } from '@elastic/eui';
|
|||
import { SurrDocType } from '../../api/context';
|
||||
|
||||
export function ActionBarWarning({ docCount, type }: { docCount: number; type: SurrDocType }) {
|
||||
if (type === 'predecessors') {
|
||||
if (type === SurrDocType.PREDECESSORS) {
|
||||
return (
|
||||
<EuiCallOut
|
||||
color="warning"
|
||||
|
|
|
@ -1,210 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0 and the Server Side Public License, v 1; you may not use this file except
|
||||
* in compliance with, at your election, the Elastic License 2.0 or the Server
|
||||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { fromPairs } from 'lodash';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
|
||||
import { getServices } from '../../../../kibana_services';
|
||||
import { SEARCH_FIELDS_FROM_SOURCE } from '../../../../../common';
|
||||
import { MarkdownSimple, toMountPoint } from '../../../../../../kibana_react/public';
|
||||
import { fetchAnchorProvider } from '../api/anchor';
|
||||
import { EsHitRecord, EsHitRecordList, fetchContextProvider, SurrDocType } from '../api/context';
|
||||
import { getQueryParameterActions } from '../query_parameters';
|
||||
import {
|
||||
ContextAppState,
|
||||
FailureReason,
|
||||
LoadingStatus,
|
||||
LoadingStatusEntry,
|
||||
LoadingStatusState,
|
||||
QueryParameters,
|
||||
} from '../../context_app_state';
|
||||
|
||||
interface DiscoverPromise extends PromiseConstructor {
|
||||
try: <T>(fn: () => Promise<T>) => Promise<T>;
|
||||
}
|
||||
|
||||
export function QueryActionsProvider(Promise: DiscoverPromise) {
|
||||
const { filterManager, indexPatterns, data, uiSettings } = getServices();
|
||||
const useNewFieldsApi = !uiSettings.get(SEARCH_FIELDS_FROM_SOURCE);
|
||||
const fetchAnchor = fetchAnchorProvider(
|
||||
indexPatterns,
|
||||
data.search.searchSource.createEmpty(),
|
||||
useNewFieldsApi
|
||||
);
|
||||
const { fetchSurroundingDocs } = fetchContextProvider(indexPatterns, useNewFieldsApi);
|
||||
const { setPredecessorCount, setQueryParameters, setSuccessorCount } = getQueryParameterActions(
|
||||
filterManager,
|
||||
indexPatterns
|
||||
);
|
||||
|
||||
const setFailedStatus = (state: ContextAppState) => (
|
||||
subject: keyof LoadingStatusState,
|
||||
details: LoadingStatusEntry = {}
|
||||
) =>
|
||||
(state.loadingStatus[subject] = {
|
||||
status: LoadingStatus.FAILED,
|
||||
reason: FailureReason.UNKNOWN,
|
||||
...details,
|
||||
});
|
||||
|
||||
const setLoadedStatus = (state: ContextAppState) => (subject: keyof LoadingStatusState) =>
|
||||
(state.loadingStatus[subject] = {
|
||||
status: LoadingStatus.LOADED,
|
||||
});
|
||||
|
||||
const setLoadingStatus = (state: ContextAppState) => (subject: keyof LoadingStatusState) =>
|
||||
(state.loadingStatus[subject] = {
|
||||
status: LoadingStatus.LOADING,
|
||||
});
|
||||
|
||||
const fetchAnchorRow = (state: ContextAppState) => () => {
|
||||
const {
|
||||
queryParameters: { indexPatternId, anchorId, sort, tieBreakerField },
|
||||
} = state;
|
||||
|
||||
if (!tieBreakerField) {
|
||||
return Promise.reject(
|
||||
setFailedStatus(state)('anchor', {
|
||||
reason: FailureReason.INVALID_TIEBREAKER,
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
setLoadingStatus(state)('anchor');
|
||||
const [[, sortDir]] = sort;
|
||||
|
||||
return Promise.try(() =>
|
||||
fetchAnchor(indexPatternId, anchorId, [fromPairs(sort), { [tieBreakerField]: sortDir }])
|
||||
).then(
|
||||
(anchorDocument: EsHitRecord) => {
|
||||
setLoadedStatus(state)('anchor');
|
||||
state.rows.anchor = anchorDocument;
|
||||
return anchorDocument;
|
||||
},
|
||||
(error: Error) => {
|
||||
setFailedStatus(state)('anchor', { error });
|
||||
getServices().toastNotifications.addDanger({
|
||||
title: i18n.translate('discover.context.unableToLoadAnchorDocumentDescription', {
|
||||
defaultMessage: 'Unable to load the anchor document',
|
||||
}),
|
||||
text: toMountPoint(<MarkdownSimple>{error.message}</MarkdownSimple>),
|
||||
});
|
||||
throw error;
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
const fetchSurroundingRows = (type: SurrDocType, state: ContextAppState) => {
|
||||
const {
|
||||
queryParameters: { indexPatternId, sort, tieBreakerField },
|
||||
rows: { anchor },
|
||||
} = state;
|
||||
const filters = getServices().filterManager.getFilters();
|
||||
|
||||
const count =
|
||||
type === 'successors'
|
||||
? state.queryParameters.successorCount
|
||||
: state.queryParameters.predecessorCount;
|
||||
|
||||
if (!tieBreakerField) {
|
||||
return Promise.reject(
|
||||
setFailedStatus(state)(type, {
|
||||
reason: FailureReason.INVALID_TIEBREAKER,
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
setLoadingStatus(state)(type);
|
||||
const [[sortField, sortDir]] = sort;
|
||||
|
||||
return Promise.try(() =>
|
||||
fetchSurroundingDocs(
|
||||
type,
|
||||
indexPatternId,
|
||||
anchor,
|
||||
sortField,
|
||||
tieBreakerField,
|
||||
sortDir,
|
||||
count,
|
||||
filters
|
||||
)
|
||||
).then(
|
||||
(documents: EsHitRecordList) => {
|
||||
setLoadedStatus(state)(type);
|
||||
state.rows[type] = documents;
|
||||
return documents;
|
||||
},
|
||||
(error: Error) => {
|
||||
setFailedStatus(state)(type, { error });
|
||||
getServices().toastNotifications.addDanger({
|
||||
title: i18n.translate('discover.context.unableToLoadDocumentDescription', {
|
||||
defaultMessage: 'Unable to load documents',
|
||||
}),
|
||||
text: toMountPoint(<MarkdownSimple>{error.message}</MarkdownSimple>),
|
||||
});
|
||||
throw error;
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
const fetchContextRows = (state: ContextAppState) => () =>
|
||||
Promise.all([
|
||||
fetchSurroundingRows('predecessors', state),
|
||||
fetchSurroundingRows('successors', state),
|
||||
]);
|
||||
|
||||
const fetchAllRows = (state: ContextAppState) => () =>
|
||||
Promise.try(fetchAnchorRow(state)).then(fetchContextRows(state));
|
||||
|
||||
const fetchContextRowsWithNewQueryParameters = (state: ContextAppState) => (
|
||||
queryParameters: QueryParameters
|
||||
) => {
|
||||
setQueryParameters(state)(queryParameters);
|
||||
return fetchContextRows(state)();
|
||||
};
|
||||
|
||||
const fetchAllRowsWithNewQueryParameters = (state: ContextAppState) => (
|
||||
queryParameters: QueryParameters
|
||||
) => {
|
||||
setQueryParameters(state)(queryParameters);
|
||||
return fetchAllRows(state)();
|
||||
};
|
||||
|
||||
const fetchGivenPredecessorRows = (state: ContextAppState) => (count: number) => {
|
||||
setPredecessorCount(state)(count);
|
||||
return fetchSurroundingRows('predecessors', state);
|
||||
};
|
||||
|
||||
const fetchGivenSuccessorRows = (state: ContextAppState) => (count: number) => {
|
||||
setSuccessorCount(state)(count);
|
||||
return fetchSurroundingRows('successors', state);
|
||||
};
|
||||
|
||||
const setAllRows = (state: ContextAppState) => (
|
||||
predecessorRows: EsHitRecordList,
|
||||
anchorRow: EsHitRecord,
|
||||
successorRows: EsHitRecordList
|
||||
) =>
|
||||
(state.rows.all = [
|
||||
...(predecessorRows || []),
|
||||
...(anchorRow ? [anchorRow] : []),
|
||||
...(successorRows || []),
|
||||
]);
|
||||
|
||||
return {
|
||||
fetchAllRows,
|
||||
fetchAllRowsWithNewQueryParameters,
|
||||
fetchAnchorRow,
|
||||
fetchContextRows,
|
||||
fetchContextRowsWithNewQueryParameters,
|
||||
fetchGivenPredecessorRows,
|
||||
fetchGivenSuccessorRows,
|
||||
setAllRows,
|
||||
};
|
||||
}
|
|
@ -1,10 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0 and the Server Side Public License, v 1; you may not use this file except
|
||||
* in compliance with, at your election, the Elastic License 2.0 or the Server
|
||||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
export { QueryActionsProvider } from './actions';
|
||||
export { createInitialLoadingStatusState } from './state';
|
|
@ -1,17 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0 and the Server Side Public License, v 1; you may not use this file except
|
||||
* in compliance with, at your election, the Elastic License 2.0 or the Server
|
||||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import { LoadingStatus, LoadingStatusState } from '../../context_app_state';
|
||||
|
||||
export function createInitialLoadingStatusState(): LoadingStatusState {
|
||||
return {
|
||||
anchor: LoadingStatus.UNINITIALIZED,
|
||||
predecessors: LoadingStatus.UNINITIALIZED,
|
||||
successors: LoadingStatus.UNINITIALIZED,
|
||||
};
|
||||
}
|
|
@ -1,160 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0 and the Server Side Public License, v 1; you may not use this file except
|
||||
* in compliance with, at your election, the Elastic License 2.0 or the Server
|
||||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import { getQueryParameterActions } from './actions';
|
||||
import { FilterManager, SortDirection } from '../../../../../../data/public';
|
||||
import { coreMock } from '../../../../../../../core/public/mocks';
|
||||
import { ContextAppState, LoadingStatus, QueryParameters } from '../../context_app_state';
|
||||
import { EsHitRecord } from '../api/context';
|
||||
const setupMock = coreMock.createSetup();
|
||||
|
||||
let state: ContextAppState;
|
||||
let filterManager: FilterManager;
|
||||
let filterManagerSpy: jest.SpyInstance;
|
||||
|
||||
beforeEach(() => {
|
||||
filterManager = new FilterManager(setupMock.uiSettings);
|
||||
filterManagerSpy = jest.spyOn(filterManager, 'addFilters');
|
||||
|
||||
state = {
|
||||
queryParameters: {
|
||||
defaultStepSize: 3,
|
||||
indexPatternId: 'INDEX_PATTERN_ID',
|
||||
predecessorCount: 10,
|
||||
successorCount: 10,
|
||||
anchorId: '',
|
||||
columns: [],
|
||||
filters: [],
|
||||
sort: [['field', SortDirection.asc]],
|
||||
tieBreakerField: '',
|
||||
},
|
||||
loadingStatus: {
|
||||
anchor: LoadingStatus.UNINITIALIZED,
|
||||
predecessors: LoadingStatus.UNINITIALIZED,
|
||||
successors: LoadingStatus.UNINITIALIZED,
|
||||
},
|
||||
rows: {
|
||||
all: [],
|
||||
anchor: ({ isAnchor: true, fields: [], sort: [], _id: '' } as unknown) as EsHitRecord,
|
||||
predecessors: [],
|
||||
successors: [],
|
||||
},
|
||||
useNewFieldsApi: true,
|
||||
};
|
||||
});
|
||||
|
||||
describe('context query_parameter actions', function () {
|
||||
describe('action addFilter', () => {
|
||||
it('should pass the given arguments to the filterManager', () => {
|
||||
const { addFilter } = getQueryParameterActions(filterManager);
|
||||
|
||||
addFilter(state)('FIELD_NAME', 'FIELD_VALUE', 'FILTER_OPERATION');
|
||||
|
||||
// get the generated filter
|
||||
const generatedFilter = filterManagerSpy.mock.calls[0][0][0];
|
||||
const queryKeys = Object.keys(generatedFilter.query.match_phrase);
|
||||
expect(filterManagerSpy.mock.calls.length).toBe(1);
|
||||
expect(queryKeys[0]).toBe('FIELD_NAME');
|
||||
expect(generatedFilter.query.match_phrase[queryKeys[0]]).toBe('FIELD_VALUE');
|
||||
});
|
||||
|
||||
it('should pass the index pattern id to the filterManager', () => {
|
||||
const { addFilter } = getQueryParameterActions(filterManager);
|
||||
addFilter(state)('FIELD_NAME', 'FIELD_VALUE', 'FILTER_OPERATION');
|
||||
const generatedFilter = filterManagerSpy.mock.calls[0][0][0];
|
||||
expect(generatedFilter.meta.index).toBe('INDEX_PATTERN_ID');
|
||||
});
|
||||
});
|
||||
describe('action setPredecessorCount', () => {
|
||||
it('should set the predecessorCount to the given value', () => {
|
||||
const { setPredecessorCount } = getQueryParameterActions(filterManager);
|
||||
setPredecessorCount(state)(20);
|
||||
expect(state.queryParameters.predecessorCount).toBe(20);
|
||||
});
|
||||
|
||||
it('should limit the predecessorCount to 0 as a lower bound', () => {
|
||||
const { setPredecessorCount } = getQueryParameterActions(filterManager);
|
||||
setPredecessorCount(state)(-1);
|
||||
expect(state.queryParameters.predecessorCount).toBe(0);
|
||||
});
|
||||
|
||||
it('should limit the predecessorCount to 10000 as an upper bound', () => {
|
||||
const { setPredecessorCount } = getQueryParameterActions(filterManager);
|
||||
setPredecessorCount(state)(20000);
|
||||
expect(state.queryParameters.predecessorCount).toBe(10000);
|
||||
});
|
||||
});
|
||||
describe('action setSuccessorCount', () => {
|
||||
it('should set the successorCount to the given value', function () {
|
||||
const { setSuccessorCount } = getQueryParameterActions(filterManager);
|
||||
setSuccessorCount(state)(20);
|
||||
|
||||
expect(state.queryParameters.successorCount).toBe(20);
|
||||
});
|
||||
|
||||
it('should limit the successorCount to 0 as a lower bound', () => {
|
||||
const { setSuccessorCount } = getQueryParameterActions(filterManager);
|
||||
setSuccessorCount(state)(-1);
|
||||
expect(state.queryParameters.successorCount).toBe(0);
|
||||
});
|
||||
|
||||
it('should limit the successorCount to 10000 as an upper bound', () => {
|
||||
const { setSuccessorCount } = getQueryParameterActions(filterManager);
|
||||
setSuccessorCount(state)(20000);
|
||||
expect(state.queryParameters.successorCount).toBe(10000);
|
||||
});
|
||||
});
|
||||
describe('action setQueryParameters', function () {
|
||||
const { setQueryParameters } = getQueryParameterActions(filterManager);
|
||||
|
||||
it('should update the queryParameters with valid properties from the given object', function () {
|
||||
const newState = {
|
||||
...state,
|
||||
queryParameters: {
|
||||
...state.queryParameters,
|
||||
additionalParameter: 'ADDITIONAL_PARAMETER',
|
||||
},
|
||||
};
|
||||
|
||||
const actualState = setQueryParameters(newState)({
|
||||
anchorId: 'ANCHOR_ID',
|
||||
columns: ['column'],
|
||||
defaultStepSize: 3,
|
||||
filters: [],
|
||||
indexPatternId: 'INDEX_PATTERN',
|
||||
predecessorCount: 100,
|
||||
successorCount: 100,
|
||||
sort: [['field', SortDirection.asc]],
|
||||
tieBreakerField: '',
|
||||
});
|
||||
|
||||
expect(actualState).toEqual({
|
||||
additionalParameter: 'ADDITIONAL_PARAMETER',
|
||||
anchorId: 'ANCHOR_ID',
|
||||
columns: ['column'],
|
||||
defaultStepSize: 3,
|
||||
filters: [],
|
||||
indexPatternId: 'INDEX_PATTERN',
|
||||
predecessorCount: 100,
|
||||
successorCount: 100,
|
||||
sort: [['field', SortDirection.asc]],
|
||||
tieBreakerField: '',
|
||||
});
|
||||
});
|
||||
|
||||
it('should ignore invalid properties', function () {
|
||||
const newState = { ...state };
|
||||
|
||||
setQueryParameters(newState)(({
|
||||
additionalParameter: 'ADDITIONAL_PARAMETER',
|
||||
} as unknown) as QueryParameters);
|
||||
|
||||
expect(state.queryParameters).toEqual(newState.queryParameters);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -1,82 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0 and the Server Side Public License, v 1; you may not use this file except
|
||||
* in compliance with, at your election, the Elastic License 2.0 or the Server
|
||||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import { pick } from 'lodash';
|
||||
|
||||
import {
|
||||
IndexPatternsContract,
|
||||
FilterManager,
|
||||
esFilters,
|
||||
Filter,
|
||||
IndexPatternField,
|
||||
} from '../../../../../../data/public';
|
||||
import { popularizeField } from '../../../helpers/popularize_field';
|
||||
import { ContextAppState, QueryParameters } from '../../context_app_state';
|
||||
import { MAX_CONTEXT_SIZE, MIN_CONTEXT_SIZE, QUERY_PARAMETER_KEYS } from './constants';
|
||||
|
||||
export function getQueryParameterActions(
|
||||
filterManager: FilterManager,
|
||||
indexPatterns?: IndexPatternsContract
|
||||
) {
|
||||
const setPredecessorCount = (state: ContextAppState) => (predecessorCount: number) => {
|
||||
return (state.queryParameters.predecessorCount = clamp(
|
||||
MIN_CONTEXT_SIZE,
|
||||
MAX_CONTEXT_SIZE,
|
||||
predecessorCount
|
||||
));
|
||||
};
|
||||
|
||||
const setSuccessorCount = (state: ContextAppState) => (successorCount: number) => {
|
||||
return (state.queryParameters.successorCount = clamp(
|
||||
MIN_CONTEXT_SIZE,
|
||||
MAX_CONTEXT_SIZE,
|
||||
successorCount
|
||||
));
|
||||
};
|
||||
|
||||
const setQueryParameters = (state: ContextAppState) => (queryParameters: QueryParameters) => {
|
||||
return Object.assign(state.queryParameters, pick(queryParameters, QUERY_PARAMETER_KEYS));
|
||||
};
|
||||
|
||||
const updateFilters = () => (filters: Filter[]) => {
|
||||
filterManager.setFilters(filters);
|
||||
};
|
||||
|
||||
const addFilter = (state: ContextAppState) => async (
|
||||
field: IndexPatternField | string,
|
||||
values: unknown,
|
||||
operation: string
|
||||
) => {
|
||||
const indexPatternId = state.queryParameters.indexPatternId;
|
||||
const newFilters = esFilters.generateFilters(
|
||||
filterManager,
|
||||
field,
|
||||
values,
|
||||
operation,
|
||||
indexPatternId
|
||||
);
|
||||
filterManager.addFilters(newFilters);
|
||||
if (indexPatterns) {
|
||||
const indexPattern = await indexPatterns.get(indexPatternId);
|
||||
const fieldName = typeof field === 'string' ? field : field.name;
|
||||
await popularizeField(indexPattern, fieldName, indexPatterns);
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
addFilter,
|
||||
updateFilters,
|
||||
setPredecessorCount,
|
||||
setQueryParameters,
|
||||
setSuccessorCount,
|
||||
};
|
||||
}
|
||||
|
||||
function clamp(minimum: number, maximum: number, value: number) {
|
||||
return Math.max(Math.min(maximum, value), minimum);
|
||||
}
|
|
@ -1,11 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0 and the Server Side Public License, v 1; you may not use this file except
|
||||
* in compliance with, at your election, the Elastic License 2.0 or the Server
|
||||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
export { getQueryParameterActions } from './actions';
|
||||
export { MAX_CONTEXT_SIZE, MIN_CONTEXT_SIZE, QUERY_PARAMETER_KEYS } from './constants';
|
||||
export { createInitialQueryParametersState } from './state';
|
|
@ -1,24 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0 and the Server Side Public License, v 1; you may not use this file except
|
||||
* in compliance with, at your election, the Elastic License 2.0 or the Server
|
||||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
export function createInitialQueryParametersState(
|
||||
defaultStepSize: number = 5,
|
||||
tieBreakerField: string = '_doc'
|
||||
) {
|
||||
return {
|
||||
anchorId: null,
|
||||
columns: [],
|
||||
defaultStepSize,
|
||||
filters: [],
|
||||
indexPatternId: null,
|
||||
predecessorCount: 0,
|
||||
successorCount: 0,
|
||||
sort: [],
|
||||
tieBreakerField,
|
||||
};
|
||||
}
|
|
@ -1,24 +0,0 @@
|
|||
<!-- Context App Legacy -->
|
||||
<context-app-legacy
|
||||
filter="contextApp.actions.addFilter"
|
||||
hits="contextApp.state.rows.all"
|
||||
index-pattern="contextApp.indexPattern"
|
||||
app-state="contextApp.appState"
|
||||
state-container="contextApp.stateContainer"
|
||||
sorting="contextApp.state.queryParameters.sort"
|
||||
columns="contextApp.state.queryParameters.columns"
|
||||
minimum-visible-rows="contextApp.state.rows.all.length"
|
||||
anchor-id="contextApp.anchorId"
|
||||
anchor-status="contextApp.state.loadingStatus.anchor.status"
|
||||
anchor-reason="contextApp.state.loadingStatus.anchor.reason"
|
||||
default-step-size="contextApp.state.queryParameters.defaultStepSize"
|
||||
predecessor-count="contextApp.state.queryParameters.predecessorCount"
|
||||
predecessor-available="contextApp.state.rows.predecessors.length"
|
||||
predecessor-status="contextApp.state.loadingStatus.predecessors.status"
|
||||
on-change-predecessor-count="contextApp.actions.fetchGivenPredecessorRows"
|
||||
successor-count="contextApp.state.queryParameters.successorCount"
|
||||
successor-available="contextApp.state.rows.successors.length"
|
||||
successor-status="contextApp.state.loadingStatus.successors.status"
|
||||
on-change-successor-count="contextApp.actions.fetchGivenSuccessorRows"
|
||||
use-new-fields-api="contextApp.state.useNewFieldsApi"
|
||||
top-nav-menu="contextApp.topNavMenu"></context-app-legacy>
|
|
@ -1,129 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0 and the Server Side Public License, v 1; you may not use this file except
|
||||
* in compliance with, at your election, the Elastic License 2.0 or the Server
|
||||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import _ from 'lodash';
|
||||
import {
|
||||
CONTEXT_STEP_SETTING,
|
||||
CONTEXT_TIE_BREAKER_FIELDS_SETTING,
|
||||
SEARCH_FIELDS_FROM_SOURCE,
|
||||
} from '../../../common';
|
||||
import { getAngularModule, getServices } from '../../kibana_services';
|
||||
import contextAppTemplate from './context_app.html';
|
||||
import './context/components/action_bar';
|
||||
import { getFirstSortableField } from './context/api/utils/sorting';
|
||||
import {
|
||||
createInitialQueryParametersState,
|
||||
getQueryParameterActions,
|
||||
QUERY_PARAMETER_KEYS,
|
||||
} from './context/query_parameters';
|
||||
import { createInitialLoadingStatusState, QueryActionsProvider } from './context/query';
|
||||
import { callAfterBindingsWorkaround } from './context/helpers/call_after_bindings_workaround';
|
||||
|
||||
getAngularModule().directive('contextApp', function ContextApp() {
|
||||
return {
|
||||
bindToController: true,
|
||||
controller: callAfterBindingsWorkaround(ContextAppController),
|
||||
controllerAs: 'contextApp',
|
||||
restrict: 'E',
|
||||
scope: {
|
||||
anchorId: '=',
|
||||
columns: '=',
|
||||
indexPattern: '=',
|
||||
appState: '=',
|
||||
stateContainer: '=',
|
||||
filters: '=',
|
||||
predecessorCount: '=',
|
||||
successorCount: '=',
|
||||
sort: '=',
|
||||
},
|
||||
template: contextAppTemplate,
|
||||
};
|
||||
});
|
||||
|
||||
function ContextAppController($scope, Private) {
|
||||
const { filterManager, indexPatterns, uiSettings, navigation } = getServices();
|
||||
const useNewFieldsApi = !uiSettings.get(SEARCH_FIELDS_FROM_SOURCE);
|
||||
const queryParameterActions = getQueryParameterActions(filterManager, indexPatterns);
|
||||
const queryActions = Private(QueryActionsProvider);
|
||||
this.state = createInitialState(
|
||||
parseInt(uiSettings.get(CONTEXT_STEP_SETTING), 10),
|
||||
getFirstSortableField(this.indexPattern, uiSettings.get(CONTEXT_TIE_BREAKER_FIELDS_SETTING)),
|
||||
useNewFieldsApi
|
||||
);
|
||||
this.state.useNewFieldsApi = useNewFieldsApi;
|
||||
this.topNavMenu = navigation.ui.TopNavMenu;
|
||||
this.actions = _.mapValues(
|
||||
{
|
||||
...queryParameterActions,
|
||||
...queryActions,
|
||||
},
|
||||
(action) => (...args) => action(this.state)(...args)
|
||||
);
|
||||
|
||||
$scope.$watchGroup(
|
||||
[
|
||||
() => this.state.rows.predecessors,
|
||||
() => this.state.rows.anchor,
|
||||
() => this.state.rows.successors,
|
||||
],
|
||||
(newValues) => this.actions.setAllRows(...newValues)
|
||||
);
|
||||
|
||||
/**
|
||||
* Sync properties to state
|
||||
*/
|
||||
$scope.$watchCollection(
|
||||
() => ({
|
||||
..._.pick(this, QUERY_PARAMETER_KEYS),
|
||||
indexPatternId: this.indexPattern.id,
|
||||
}),
|
||||
(newQueryParameters) => {
|
||||
const { queryParameters } = this.state;
|
||||
if (
|
||||
newQueryParameters.indexPatternId !== queryParameters.indexPatternId ||
|
||||
newQueryParameters.anchorId !== queryParameters.anchorId ||
|
||||
!_.isEqual(newQueryParameters.sort, queryParameters.sort)
|
||||
) {
|
||||
this.actions.fetchAllRowsWithNewQueryParameters(_.cloneDeep(newQueryParameters));
|
||||
} else if (
|
||||
newQueryParameters.predecessorCount !== queryParameters.predecessorCount ||
|
||||
newQueryParameters.successorCount !== queryParameters.successorCount ||
|
||||
!_.isEqual(newQueryParameters.filters, queryParameters.filters)
|
||||
) {
|
||||
this.actions.fetchContextRowsWithNewQueryParameters(_.cloneDeep(newQueryParameters));
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
/**
|
||||
* Sync state to properties
|
||||
*/
|
||||
$scope.$watchCollection(
|
||||
() => ({
|
||||
predecessorCount: this.state.queryParameters.predecessorCount,
|
||||
successorCount: this.state.queryParameters.successorCount,
|
||||
}),
|
||||
(newParameters) => {
|
||||
_.assign(this, newParameters);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
function createInitialState(defaultStepSize, tieBreakerField, useNewFieldsApi) {
|
||||
return {
|
||||
queryParameters: createInitialQueryParametersState(defaultStepSize, tieBreakerField),
|
||||
rows: {
|
||||
all: [],
|
||||
anchor: null,
|
||||
predecessors: [],
|
||||
successors: [],
|
||||
},
|
||||
loadingStatus: createInitialLoadingStatusState(),
|
||||
useNewFieldsApi,
|
||||
};
|
||||
}
|
|
@ -1,60 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0 and the Server Side Public License, v 1; you may not use this file except
|
||||
* in compliance with, at your election, the Elastic License 2.0 or the Server
|
||||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import { Filter } from '../../../../data/public';
|
||||
import { EsHitRecord } from './context/api/context';
|
||||
import { EsHitRecordList } from './context/api/context';
|
||||
import { SortDirection } from './context/api/utils/sorting';
|
||||
|
||||
export interface ContextAppState {
|
||||
loadingStatus: LoadingStatusState;
|
||||
queryParameters: QueryParameters;
|
||||
rows: ContextRows;
|
||||
useNewFieldsApi: boolean;
|
||||
}
|
||||
|
||||
export enum LoadingStatus {
|
||||
FAILED = 'failed',
|
||||
LOADED = 'loaded',
|
||||
LOADING = 'loading',
|
||||
UNINITIALIZED = 'uninitialized',
|
||||
}
|
||||
export enum FailureReason {
|
||||
UNKNOWN = 'unknown',
|
||||
INVALID_TIEBREAKER = 'invalid_tiebreaker',
|
||||
}
|
||||
export type LoadingStatusEntry = Partial<{
|
||||
status: LoadingStatus;
|
||||
reason: FailureReason;
|
||||
error: Error;
|
||||
}>;
|
||||
|
||||
export interface LoadingStatusState {
|
||||
anchor: LoadingStatusEntry | LoadingStatus;
|
||||
predecessors: LoadingStatusEntry | LoadingStatus;
|
||||
successors: LoadingStatusEntry | LoadingStatus;
|
||||
}
|
||||
|
||||
export interface QueryParameters {
|
||||
anchorId: string;
|
||||
columns: string[];
|
||||
defaultStepSize: number;
|
||||
filters: Filter[];
|
||||
indexPatternId: string;
|
||||
predecessorCount: number;
|
||||
successorCount: number;
|
||||
sort: Array<[string, SortDirection]>;
|
||||
tieBreakerField: string;
|
||||
}
|
||||
|
||||
interface ContextRows {
|
||||
all: EsHitRecordList;
|
||||
anchor: EsHitRecord;
|
||||
predecessors: EsHitRecordList;
|
||||
successors: EsHitRecordList;
|
||||
}
|
|
@ -0,0 +1,64 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0 and the Server Side Public License, v 1; you may not use this file except
|
||||
* in compliance with, at your election, the Elastic License 2.0 or the Server
|
||||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import { EsHitRecord } from './context/api/context';
|
||||
import { EsHitRecordList } from './context/api/context';
|
||||
|
||||
export interface ContextFetchState {
|
||||
/**
|
||||
* Documents listed before anchor
|
||||
*/
|
||||
predecessors: EsHitRecordList;
|
||||
/**
|
||||
* Documents after anchor
|
||||
*/
|
||||
successors: EsHitRecordList;
|
||||
/**
|
||||
* Anchor document
|
||||
*/
|
||||
anchor: EsHitRecord;
|
||||
/**
|
||||
* Anchor fetch status
|
||||
*/
|
||||
anchorStatus: LoadingStatusEntry;
|
||||
/**
|
||||
* Predecessors fetch status
|
||||
*/
|
||||
predecessorsStatus: LoadingStatusEntry;
|
||||
/**
|
||||
* Successors fetch status
|
||||
*/
|
||||
successorsStatus: LoadingStatusEntry;
|
||||
}
|
||||
|
||||
export enum LoadingStatus {
|
||||
FAILED = 'failed',
|
||||
LOADED = 'loaded',
|
||||
LOADING = 'loading',
|
||||
UNINITIALIZED = 'uninitialized',
|
||||
}
|
||||
|
||||
export enum FailureReason {
|
||||
UNKNOWN = 'unknown',
|
||||
INVALID_TIEBREAKER = 'invalid_tiebreaker',
|
||||
}
|
||||
|
||||
export interface LoadingStatusEntry {
|
||||
value: LoadingStatus;
|
||||
error?: Error;
|
||||
reason?: FailureReason;
|
||||
}
|
||||
|
||||
export const getInitialContextQueryState = (): ContextFetchState => ({
|
||||
anchor: {} as EsHitRecord,
|
||||
predecessors: [],
|
||||
successors: [],
|
||||
anchorStatus: { value: LoadingStatus.UNINITIALIZED },
|
||||
predecessorsStatus: { value: LoadingStatus.UNINITIALIZED },
|
||||
successorsStatus: { value: LoadingStatus.UNINITIALIZED },
|
||||
});
|
|
@ -23,7 +23,7 @@ describe('Test Discover Context State', () => {
|
|||
history = createBrowserHistory();
|
||||
history.push('/');
|
||||
state = getState({
|
||||
defaultStepSize: '4',
|
||||
defaultSize: 4,
|
||||
timeFieldName: 'time',
|
||||
history,
|
||||
uiSettings: {
|
||||
|
|
|
@ -16,7 +16,7 @@ import {
|
|||
withNotifyOnErrors,
|
||||
ReduxLikeStateContainer,
|
||||
} from '../../../../kibana_utils/public';
|
||||
import { esFilters, FilterManager, Filter, Query } from '../../../../data/public';
|
||||
import { esFilters, FilterManager, Filter, SortDirection } from '../../../../data/public';
|
||||
import { handleSourceColumnState } from './helpers';
|
||||
|
||||
export interface AppState {
|
||||
|
@ -40,7 +40,6 @@ export interface AppState {
|
|||
* Number of records to be fetched after the anchor records (older records)
|
||||
*/
|
||||
successorCount: number;
|
||||
query?: Query;
|
||||
}
|
||||
|
||||
interface GlobalState {
|
||||
|
@ -54,7 +53,7 @@ export interface GetStateParams {
|
|||
/**
|
||||
* Number of records to be fetched when 'Load' link/button is clicked
|
||||
*/
|
||||
defaultStepSize: string;
|
||||
defaultSize: number;
|
||||
/**
|
||||
* The timefield used for sorting
|
||||
*/
|
||||
|
@ -124,7 +123,7 @@ const APP_STATE_URL_KEY = '_a';
|
|||
* provides helper functions to start/stop syncing with URL
|
||||
*/
|
||||
export function getState({
|
||||
defaultStepSize,
|
||||
defaultSize,
|
||||
timeFieldName,
|
||||
storeInSessionStorage = false,
|
||||
history,
|
||||
|
@ -142,7 +141,7 @@ export function getState({
|
|||
|
||||
const appStateFromUrl = stateStorage.get(APP_STATE_URL_KEY) as AppState;
|
||||
const appStateInitial = createInitialAppState(
|
||||
defaultStepSize,
|
||||
defaultSize,
|
||||
timeFieldName,
|
||||
appStateFromUrl,
|
||||
uiSettings
|
||||
|
@ -190,7 +189,7 @@ export function getState({
|
|||
const mergedState = { ...oldState, ...newState };
|
||||
|
||||
if (!isEqualState(oldState, mergedState)) {
|
||||
appStateContainer.set(mergedState);
|
||||
stateStorage.set(APP_STATE_URL_KEY, mergedState, { replace: true });
|
||||
}
|
||||
},
|
||||
getFilters: () => [
|
||||
|
@ -267,17 +266,17 @@ function getFilters(state: AppState | GlobalState): Filter[] {
|
|||
* default state. The default size is the default number of successor/predecessor records to fetch
|
||||
*/
|
||||
function createInitialAppState(
|
||||
defaultSize: string,
|
||||
defaultSize: number,
|
||||
timeFieldName: string,
|
||||
urlState: AppState,
|
||||
uiSettings: IUiSettingsClient
|
||||
): AppState {
|
||||
const defaultState = {
|
||||
const defaultState: AppState = {
|
||||
columns: ['_source'],
|
||||
filters: [],
|
||||
predecessorCount: parseInt(defaultSize, 10),
|
||||
sort: [[timeFieldName, 'desc']],
|
||||
successorCount: parseInt(defaultSize, 10),
|
||||
predecessorCount: defaultSize,
|
||||
successorCount: defaultSize,
|
||||
sort: [[timeFieldName, SortDirection.desc]],
|
||||
};
|
||||
if (typeof urlState !== 'object') {
|
||||
return defaultState;
|
||||
|
|
|
@ -8,4 +8,4 @@
|
|||
|
||||
import React from 'react';
|
||||
|
||||
export const TopNavMenuMock = () => <div>Hello World</div>;
|
||||
export const mockTopNavMenu = () => <div>Hello World</div>;
|
||||
|
|
|
@ -0,0 +1,64 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0 and the Server Side Public License, v 1; you may not use this file except
|
||||
* in compliance with, at your election, the Elastic License 2.0 or the Server
|
||||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
export const mockAnchorHit = {
|
||||
_id: '123',
|
||||
_index: 'the-index-pattern-id',
|
||||
fields: { order_date: ['2021-06-07T18:52:17.000Z'] },
|
||||
sort: [1623091937000, 2092],
|
||||
isAnchor: true,
|
||||
_version: 1,
|
||||
};
|
||||
|
||||
export const mockPredecessorHits = [
|
||||
{
|
||||
_id: '1',
|
||||
_index: 'the-index-pattern-id',
|
||||
fields: { order_date: ['2021-06-07T19:14:29.000Z'] },
|
||||
sort: ['2021-06-07T19:14:29.000Z', 2092],
|
||||
_version: 1,
|
||||
},
|
||||
{
|
||||
_id: '2',
|
||||
_index: 'the-index-pattern-id',
|
||||
fields: { order_date: ['2021-06-07T19:14:12.000Z'] },
|
||||
sort: ['2021-06-07T19:14:12.000Z', 2431],
|
||||
_version: 1,
|
||||
},
|
||||
{
|
||||
_id: '3',
|
||||
_index: 'the-index-pattern-id',
|
||||
fields: { order_date: ['2021-06-07T19:10:22.000Z'] },
|
||||
sort: ['2021-06-07T19:10:22.000Z', 2435],
|
||||
_version: 1,
|
||||
},
|
||||
];
|
||||
|
||||
export const mockSuccessorHits = [
|
||||
{
|
||||
_id: '11',
|
||||
_index: 'the-index-pattern-id',
|
||||
fields: { order_date: ['2021-06-07T18:49:39.000Z'] },
|
||||
sort: ['2021-06-07T18:49:39.000Z', 2382],
|
||||
_version: 1,
|
||||
},
|
||||
{
|
||||
_id: '22',
|
||||
_index: 'the-index-pattern-id',
|
||||
fields: { order_date: ['2021-06-07T18:48:28.000Z'] },
|
||||
sort: ['2021-06-07T18:48:28.000Z', 2631],
|
||||
_version: 1,
|
||||
},
|
||||
{
|
||||
_id: '33',
|
||||
_index: 'the-index-pattern-id',
|
||||
fields: { order_date: ['2021-06-07T18:47:16.000Z'] },
|
||||
sort: ['2021-06-07T18:47:16.000Z', 2437],
|
||||
_version: 1,
|
||||
},
|
||||
];
|
|
@ -0,0 +1,100 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0 and the Server Side Public License, v 1; you may not use this file except
|
||||
* in compliance with, at your election, the Elastic License 2.0 or the Server
|
||||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { waitFor } from '@testing-library/react';
|
||||
import { mountWithIntl } from '@kbn/test/jest';
|
||||
import { createFilterManagerMock } from '../../../../../data/public/query/filter_manager/filter_manager.mock';
|
||||
import { mockTopNavMenu } from './__mocks__/top_nav_menu';
|
||||
import { ContextAppContent } from './context_app_content';
|
||||
import { indexPatternMock } from '../../../__mocks__/index_pattern';
|
||||
import { ContextApp } from './context_app';
|
||||
import { setServices } from '../../../kibana_services';
|
||||
import { DiscoverServices } from '../../../build_services';
|
||||
import { indexPatternsMock } from '../../../__mocks__/index_patterns';
|
||||
import { act } from 'react-dom/test-utils';
|
||||
import { uiSettingsMock } from '../../../__mocks__/ui_settings';
|
||||
|
||||
const mockFilterManager = createFilterManagerMock();
|
||||
const mockNavigationPlugin = { ui: { TopNavMenu: mockTopNavMenu } };
|
||||
|
||||
describe('ContextApp test', () => {
|
||||
const defaultProps = {
|
||||
indexPattern: indexPatternMock,
|
||||
indexPatternId: 'the-index-pattern-id',
|
||||
anchorId: 'mocked_anchor_id',
|
||||
};
|
||||
|
||||
const topNavProps = {
|
||||
appName: 'context',
|
||||
showSearchBar: true,
|
||||
showQueryBar: false,
|
||||
showFilterBar: true,
|
||||
showSaveQuery: false,
|
||||
showDatePicker: false,
|
||||
indexPatterns: [indexPatternMock],
|
||||
useDefaultBehaviors: true,
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
setServices(({
|
||||
data: {
|
||||
search: {
|
||||
searchSource: {
|
||||
createEmpty: jest.fn(),
|
||||
},
|
||||
},
|
||||
},
|
||||
capabilities: {
|
||||
discover: {
|
||||
save: true,
|
||||
},
|
||||
},
|
||||
indexPatterns: indexPatternsMock,
|
||||
toastNotifications: { addDanger: () => {} },
|
||||
navigation: mockNavigationPlugin,
|
||||
core: { notifications: { toasts: [] } },
|
||||
history: () => {},
|
||||
filterManager: mockFilterManager,
|
||||
uiSettings: uiSettingsMock,
|
||||
} as unknown) as DiscoverServices);
|
||||
});
|
||||
|
||||
it('renders correctly', async () => {
|
||||
const component = mountWithIntl(<ContextApp {...defaultProps} />);
|
||||
await waitFor(() => {
|
||||
expect(component.find(ContextAppContent).length).toBe(1);
|
||||
const topNavMenu = component.find(mockTopNavMenu);
|
||||
expect(topNavMenu.length).toBe(1);
|
||||
expect(topNavMenu.props()).toStrictEqual(topNavProps);
|
||||
});
|
||||
});
|
||||
|
||||
it('should set filters correctly', async () => {
|
||||
const component = mountWithIntl(<ContextApp {...defaultProps} />);
|
||||
|
||||
await act(async () => {
|
||||
component.find(ContextAppContent).invoke('addFilter')(
|
||||
'message',
|
||||
'2021-06-08T07:52:19.000Z',
|
||||
'+'
|
||||
);
|
||||
});
|
||||
|
||||
expect(mockFilterManager.addFilters.mock.calls.length).toBe(1);
|
||||
expect(mockFilterManager.addFilters.mock.calls[0][0]).toEqual([
|
||||
{
|
||||
$state: { store: 'appState' },
|
||||
meta: { alias: null, disabled: false, index: 'the-index-pattern-id', negate: false },
|
||||
query: { match_phrase: { message: '2021-06-08T07:52:19.000Z' } },
|
||||
},
|
||||
]);
|
||||
expect(indexPatternsMock.updateSavedObject.mock.calls.length).toBe(1);
|
||||
expect(indexPatternsMock.updateSavedObject.mock.calls[0]).toEqual([indexPatternMock, 0, true]);
|
||||
});
|
||||
});
|
|
@ -0,0 +1,178 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0 and the Server Side Public License, v 1; you may not use this file except
|
||||
* in compliance with, at your election, the Elastic License 2.0 or the Server
|
||||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import React, { Fragment, memo, useEffect, useRef, useMemo, useCallback } from 'react';
|
||||
import './context_app.scss';
|
||||
import classNames from 'classnames';
|
||||
import { FormattedMessage, I18nProvider } from '@kbn/i18n/react';
|
||||
import { EuiText, EuiPageContent, EuiPage, EuiSpacer } from '@elastic/eui';
|
||||
import { cloneDeep } from 'lodash';
|
||||
import { esFilters, SortDirection } from '../../../../../data/public';
|
||||
import { DOC_TABLE_LEGACY, SEARCH_FIELDS_FROM_SOURCE } from '../../../../common';
|
||||
import { ContextErrorMessage } from '../context_error_message';
|
||||
import { IndexPattern, IndexPatternField } from '../../../../../data/common';
|
||||
import { LoadingStatus } from '../../angular/context_query_state';
|
||||
import { getServices } from '../../../kibana_services';
|
||||
import { AppState, isEqualFilters } from '../../angular/context_state';
|
||||
import { useDataGridColumns } from '../../helpers/use_data_grid_columns';
|
||||
import { useContextAppState } from './use_context_app_state';
|
||||
import { useContextAppFetch } from './use_context_app_fetch';
|
||||
import { popularizeField } from '../../helpers/popularize_field';
|
||||
import { ContextAppContent } from './context_app_content';
|
||||
import { SurrDocType } from '../../angular/context/api/context';
|
||||
|
||||
const ContextAppContentMemoized = memo(ContextAppContent);
|
||||
|
||||
export interface ContextAppProps {
|
||||
indexPattern: IndexPattern;
|
||||
indexPatternId: string;
|
||||
anchorId: string;
|
||||
}
|
||||
|
||||
export const ContextApp = ({ indexPattern, indexPatternId, anchorId }: ContextAppProps) => {
|
||||
const services = getServices();
|
||||
const { uiSettings: config, capabilities, indexPatterns, navigation, filterManager } = services;
|
||||
|
||||
const isLegacy = useMemo(() => config.get(DOC_TABLE_LEGACY), [config]);
|
||||
const useNewFieldsApi = useMemo(() => !config.get(SEARCH_FIELDS_FROM_SOURCE), [config]);
|
||||
|
||||
/**
|
||||
* Context app state
|
||||
*/
|
||||
const { appState, setAppState } = useContextAppState({ indexPattern, services });
|
||||
const prevAppState = useRef<AppState>();
|
||||
|
||||
/**
|
||||
* Context fetched state
|
||||
*/
|
||||
const { fetchedState, fetchContextRows, fetchAllRows, fetchSurroundingRows } = useContextAppFetch(
|
||||
{
|
||||
anchorId,
|
||||
indexPatternId,
|
||||
indexPattern,
|
||||
appState,
|
||||
useNewFieldsApi,
|
||||
services,
|
||||
}
|
||||
);
|
||||
|
||||
/**
|
||||
* Fetch docs on ui changes
|
||||
*/
|
||||
useEffect(() => {
|
||||
if (!prevAppState.current) {
|
||||
fetchAllRows();
|
||||
} else if (prevAppState.current.predecessorCount !== appState.predecessorCount) {
|
||||
fetchSurroundingRows(SurrDocType.PREDECESSORS);
|
||||
} else if (prevAppState.current.successorCount !== appState.successorCount) {
|
||||
fetchSurroundingRows(SurrDocType.SUCCESSORS);
|
||||
} else if (!isEqualFilters(prevAppState.current.filters, appState.filters)) {
|
||||
fetchContextRows();
|
||||
}
|
||||
|
||||
prevAppState.current = cloneDeep(appState);
|
||||
}, [appState, indexPatternId, anchorId, fetchContextRows, fetchAllRows, fetchSurroundingRows]);
|
||||
|
||||
const { columns, onAddColumn, onRemoveColumn, onSetColumns } = useDataGridColumns({
|
||||
capabilities,
|
||||
config,
|
||||
indexPattern,
|
||||
indexPatterns,
|
||||
state: appState,
|
||||
useNewFieldsApi,
|
||||
setAppState,
|
||||
});
|
||||
const rows = useMemo(
|
||||
() => [
|
||||
...(fetchedState.predecessors || []),
|
||||
...(fetchedState.anchor._id ? [fetchedState.anchor] : []),
|
||||
...(fetchedState.successors || []),
|
||||
],
|
||||
[fetchedState.predecessors, fetchedState.anchor, fetchedState.successors]
|
||||
);
|
||||
|
||||
const addFilter = useCallback(
|
||||
async (field: IndexPatternField | string, values: unknown, operation: string) => {
|
||||
const newFilters = esFilters.generateFilters(
|
||||
filterManager,
|
||||
field,
|
||||
values,
|
||||
operation,
|
||||
indexPatternId
|
||||
);
|
||||
filterManager.addFilters(newFilters);
|
||||
if (indexPatterns) {
|
||||
const fieldName = typeof field === 'string' ? field : field.name;
|
||||
await popularizeField(indexPattern, fieldName, indexPatterns);
|
||||
}
|
||||
},
|
||||
[filterManager, indexPatternId, indexPatterns, indexPattern]
|
||||
);
|
||||
|
||||
const TopNavMenu = navigation.ui.TopNavMenu;
|
||||
const getNavBarProps = () => {
|
||||
return {
|
||||
appName: 'context',
|
||||
showSearchBar: true,
|
||||
showQueryBar: false,
|
||||
showFilterBar: true,
|
||||
showSaveQuery: false,
|
||||
showDatePicker: false,
|
||||
indexPatterns: [indexPattern],
|
||||
useDefaultBehaviors: true,
|
||||
};
|
||||
};
|
||||
|
||||
return (
|
||||
<I18nProvider>
|
||||
{fetchedState.anchorStatus.value === LoadingStatus.FAILED ? (
|
||||
<ContextErrorMessage status={fetchedState.anchorStatus} />
|
||||
) : (
|
||||
<Fragment>
|
||||
<TopNavMenu {...getNavBarProps()} />
|
||||
<EuiPage className={classNames({ dscDocsPage: !isLegacy })}>
|
||||
<EuiPageContent paddingSize="s" className="dscDocsContent">
|
||||
<EuiSpacer size="s" />
|
||||
<EuiText>
|
||||
<strong>
|
||||
<FormattedMessage
|
||||
id="discover.context.contextOfTitle"
|
||||
defaultMessage="Documents surrounding #{anchorId}"
|
||||
values={{ anchorId }}
|
||||
/>
|
||||
</strong>
|
||||
</EuiText>
|
||||
<EuiSpacer size="s" />
|
||||
<ContextAppContentMemoized
|
||||
services={services}
|
||||
indexPattern={indexPattern}
|
||||
useNewFieldsApi={useNewFieldsApi}
|
||||
isLegacy={isLegacy}
|
||||
columns={columns}
|
||||
onAddColumn={onAddColumn}
|
||||
onRemoveColumn={onRemoveColumn}
|
||||
onSetColumns={onSetColumns}
|
||||
sort={appState.sort as [[string, SortDirection]]}
|
||||
predecessorCount={appState.predecessorCount}
|
||||
successorCount={appState.successorCount}
|
||||
setAppState={setAppState}
|
||||
addFilter={addFilter}
|
||||
rows={rows}
|
||||
predecessors={fetchedState.predecessors}
|
||||
successors={fetchedState.successors}
|
||||
anchorStatus={fetchedState.anchorStatus.value}
|
||||
predecessorsStatus={fetchedState.predecessorsStatus.value}
|
||||
successorsStatus={fetchedState.successorsStatus.value}
|
||||
/>
|
||||
</EuiPageContent>
|
||||
</EuiPage>
|
||||
</Fragment>
|
||||
)}
|
||||
</I18nProvider>
|
||||
);
|
||||
};
|
|
@ -9,34 +9,27 @@
|
|||
import React from 'react';
|
||||
import { mountWithIntl } from '@kbn/test/jest';
|
||||
import { uiSettingsMock as mockUiSettings } from '../../../__mocks__/ui_settings';
|
||||
import { IndexPattern } from '../../../../../data/common/index_patterns';
|
||||
import { ContextAppLegacy } from './context_app_legacy';
|
||||
import { DocTableLegacy } from '../../angular/doc_table/create_doc_table_react';
|
||||
import { findTestSubject } from '@elastic/eui/lib/test';
|
||||
import { ActionBar } from '../../angular/context/components/action_bar/action_bar';
|
||||
import { ContextErrorMessage } from '../context_error_message';
|
||||
import { TopNavMenuMock } from './__mocks__/top_nav_menu';
|
||||
import { AppState, GetStateReturn } from '../../angular/context_state';
|
||||
import { SortDirection } from 'src/plugins/data/common';
|
||||
import { EsHitRecordList } from '../../angular/context/api/context';
|
||||
import { ContextAppContent, ContextAppContentProps } from './context_app_content';
|
||||
import { getServices } from '../../../kibana_services';
|
||||
import { LoadingStatus } from '../../angular/context_query_state';
|
||||
import { indexPatternMock } from '../../../__mocks__/index_pattern';
|
||||
import { DiscoverGrid } from '../discover_grid/discover_grid';
|
||||
|
||||
jest.mock('../../../kibana_services', () => {
|
||||
return {
|
||||
getServices: () => ({
|
||||
metadata: {
|
||||
branch: 'test',
|
||||
},
|
||||
capabilities: {
|
||||
discover: {
|
||||
save: true,
|
||||
},
|
||||
},
|
||||
uiSettings: mockUiSettings,
|
||||
}),
|
||||
};
|
||||
});
|
||||
|
||||
describe('ContextAppLegacy test', () => {
|
||||
describe('ContextAppContent test', () => {
|
||||
const hit = {
|
||||
_id: '123',
|
||||
_index: 'test_index',
|
||||
|
@ -53,75 +46,59 @@ describe('ContextAppLegacy test', () => {
|
|||
fields: [{ order_date: ['2020-10-19T13:35:02.000Z'] }],
|
||||
sort: [1603114502000, 2092],
|
||||
};
|
||||
const indexPattern = {
|
||||
id: 'test_index_pattern',
|
||||
} as IndexPattern;
|
||||
const defaultProps = {
|
||||
columns: ['_source'],
|
||||
filter: () => {},
|
||||
hits: ([hit] as unknown) as EsHitRecordList,
|
||||
sorting: [['order_date', 'desc']] as Array<[string, SortDirection]>,
|
||||
minimumVisibleRows: 5,
|
||||
indexPattern,
|
||||
const defaultProps = ({
|
||||
columns: ['Time (@timestamp)', '_source'],
|
||||
indexPattern: indexPatternMock,
|
||||
appState: ({} as unknown) as AppState,
|
||||
stateContainer: ({} as unknown) as GetStateReturn,
|
||||
anchorId: 'test_anchor_id',
|
||||
anchorStatus: 'loaded',
|
||||
anchorReason: 'no reason',
|
||||
anchorStatus: LoadingStatus.LOADED,
|
||||
predecessorsStatus: LoadingStatus.LOADED,
|
||||
successorsStatus: LoadingStatus.LOADED,
|
||||
rows: ([hit] as unknown) as EsHitRecordList,
|
||||
predecessors: [],
|
||||
successors: [],
|
||||
defaultStepSize: 5,
|
||||
predecessorCount: 10,
|
||||
successorCount: 10,
|
||||
predecessorAvailable: 10,
|
||||
successorAvailable: 10,
|
||||
onChangePredecessorCount: jest.fn(),
|
||||
onChangeSuccessorCount: jest.fn(),
|
||||
predecessorStatus: 'loaded',
|
||||
successorStatus: 'loaded',
|
||||
topNavMenu: TopNavMenuMock,
|
||||
useNewFieldsApi: false,
|
||||
isPaginationEnabled: false,
|
||||
};
|
||||
const topNavProps = {
|
||||
appName: 'context',
|
||||
showSearchBar: true,
|
||||
showQueryBar: false,
|
||||
showFilterBar: true,
|
||||
showSaveQuery: false,
|
||||
showDatePicker: false,
|
||||
indexPatterns: [indexPattern],
|
||||
useDefaultBehaviors: true,
|
||||
};
|
||||
onAddColumn: () => {},
|
||||
onRemoveColumn: () => {},
|
||||
onSetColumns: () => {},
|
||||
services: getServices(),
|
||||
sort: [['order_date', 'desc']] as Array<[string, SortDirection]>,
|
||||
isLegacy: true,
|
||||
setAppState: () => {},
|
||||
addFilter: () => {},
|
||||
} as unknown) as ContextAppContentProps;
|
||||
|
||||
it('renders correctly', () => {
|
||||
const component = mountWithIntl(<ContextAppLegacy {...defaultProps} />);
|
||||
it('should render legacy table correctly', () => {
|
||||
const component = mountWithIntl(<ContextAppContent {...defaultProps} />);
|
||||
expect(component.find(DocTableLegacy).length).toBe(1);
|
||||
const loadingIndicator = findTestSubject(component, 'contextApp_loadingIndicator');
|
||||
expect(loadingIndicator.length).toBe(0);
|
||||
expect(component.find(ActionBar).length).toBe(2);
|
||||
const topNavMenu = component.find(TopNavMenuMock);
|
||||
expect(topNavMenu.length).toBe(1);
|
||||
expect(topNavMenu.props()).toStrictEqual(topNavProps);
|
||||
});
|
||||
|
||||
it('renders loading indicator', () => {
|
||||
const props = { ...defaultProps };
|
||||
props.anchorStatus = 'loading';
|
||||
const component = mountWithIntl(<ContextAppLegacy {...props} />);
|
||||
props.anchorStatus = LoadingStatus.LOADING;
|
||||
const component = mountWithIntl(<ContextAppContent {...props} />);
|
||||
expect(component.find(DocTableLegacy).length).toBe(0);
|
||||
const loadingIndicator = findTestSubject(component, 'contextApp_loadingIndicator');
|
||||
expect(loadingIndicator.length).toBe(1);
|
||||
expect(component.find(ActionBar).length).toBe(2);
|
||||
expect(component.find(TopNavMenuMock).length).toBe(1);
|
||||
});
|
||||
|
||||
it('renders error message', () => {
|
||||
const props = { ...defaultProps };
|
||||
props.anchorStatus = 'failed';
|
||||
props.anchorReason = 'something went wrong';
|
||||
const component = mountWithIntl(<ContextAppLegacy {...props} />);
|
||||
props.anchorStatus = LoadingStatus.FAILED;
|
||||
const component = mountWithIntl(<ContextAppContent {...props} />);
|
||||
expect(component.find(DocTableLegacy).length).toBe(0);
|
||||
expect(component.find(TopNavMenuMock).length).toBe(0);
|
||||
const errorMessage = component.find(ContextErrorMessage);
|
||||
expect(errorMessage.length).toBe(1);
|
||||
});
|
||||
|
||||
it('should render discover grid correctly', () => {
|
||||
const props = { ...defaultProps, isLegacy: false };
|
||||
const component = mountWithIntl(<ContextAppContent {...props} />);
|
||||
expect(component.find(DiscoverGrid).length).toBe(1);
|
||||
});
|
||||
});
|
|
@ -0,0 +1,199 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0 and the Server Side Public License, v 1; you may not use this file except
|
||||
* in compliance with, at your election, the Elastic License 2.0 or the Server
|
||||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import React, { useState, Fragment, useMemo, useCallback } from 'react';
|
||||
import { FormattedMessage } from '@kbn/i18n/react';
|
||||
import { EuiHorizontalRule, EuiText } from '@elastic/eui';
|
||||
import { CONTEXT_STEP_SETTING, DOC_HIDE_TIME_COLUMN_SETTING } from '../../../../common';
|
||||
import { IndexPattern, IndexPatternField } from '../../../../../data/common';
|
||||
import { SortDirection } from '../../../../../data/public';
|
||||
import {
|
||||
DocTableLegacy,
|
||||
DocTableLegacyProps,
|
||||
} from '../../angular/doc_table/create_doc_table_react';
|
||||
import { LoadingStatus } from '../../angular/context_query_state';
|
||||
import { ActionBar } from '../../angular/context/components/action_bar/action_bar';
|
||||
import { DiscoverGrid, DiscoverGridProps } from '../discover_grid/discover_grid';
|
||||
import { ElasticSearchHit } from '../../doc_views/doc_views_types';
|
||||
import { AppState } from '../../angular/context_state';
|
||||
import { EsHitRecord, EsHitRecordList, SurrDocType } from '../../angular/context/api/context';
|
||||
import { DiscoverServices } from '../../../build_services';
|
||||
import { MAX_CONTEXT_SIZE, MIN_CONTEXT_SIZE } from './utils/constants';
|
||||
|
||||
export interface ContextAppContentProps {
|
||||
columns: string[];
|
||||
onAddColumn: (columnsName: string) => void;
|
||||
onRemoveColumn: (columnsName: string) => void;
|
||||
onSetColumns: (columnsNames: string[]) => void;
|
||||
services: DiscoverServices;
|
||||
indexPattern: IndexPattern;
|
||||
predecessorCount: number;
|
||||
successorCount: number;
|
||||
rows: EsHitRecordList;
|
||||
sort: [[string, SortDirection]];
|
||||
predecessors: EsHitRecordList;
|
||||
successors: EsHitRecordList;
|
||||
anchorStatus: LoadingStatus;
|
||||
predecessorsStatus: LoadingStatus;
|
||||
successorsStatus: LoadingStatus;
|
||||
useNewFieldsApi: boolean;
|
||||
isLegacy: boolean;
|
||||
setAppState: (newState: Partial<AppState>) => void;
|
||||
addFilter: (
|
||||
field: IndexPatternField | string,
|
||||
values: unknown,
|
||||
operation: string
|
||||
) => Promise<void>;
|
||||
}
|
||||
|
||||
const controlColumnIds = ['openDetails'];
|
||||
|
||||
export function clamp(value: number) {
|
||||
return Math.max(Math.min(MAX_CONTEXT_SIZE, value), MIN_CONTEXT_SIZE);
|
||||
}
|
||||
|
||||
const DataGridMemoized = React.memo(DiscoverGrid);
|
||||
const DocTableLegacyMemoized = React.memo(DocTableLegacy);
|
||||
const ActionBarMemoized = React.memo(ActionBar);
|
||||
|
||||
export function ContextAppContent({
|
||||
columns,
|
||||
onAddColumn,
|
||||
onRemoveColumn,
|
||||
onSetColumns,
|
||||
services,
|
||||
indexPattern,
|
||||
predecessorCount,
|
||||
successorCount,
|
||||
rows,
|
||||
sort,
|
||||
predecessors,
|
||||
successors,
|
||||
anchorStatus,
|
||||
predecessorsStatus,
|
||||
successorsStatus,
|
||||
useNewFieldsApi,
|
||||
isLegacy,
|
||||
setAppState,
|
||||
addFilter,
|
||||
}: ContextAppContentProps) {
|
||||
const { uiSettings: config } = services;
|
||||
|
||||
const [expandedDoc, setExpandedDoc] = useState<EsHitRecord | undefined>(undefined);
|
||||
const isAnchorLoaded = anchorStatus === LoadingStatus.LOADED;
|
||||
const isAnchorLoading =
|
||||
anchorStatus === LoadingStatus.LOADING || anchorStatus === LoadingStatus.UNINITIALIZED;
|
||||
const arePredecessorsLoading =
|
||||
predecessorsStatus === LoadingStatus.LOADING ||
|
||||
predecessorsStatus === LoadingStatus.UNINITIALIZED;
|
||||
const areSuccessorsLoading =
|
||||
successorsStatus === LoadingStatus.LOADING || successorsStatus === LoadingStatus.UNINITIALIZED;
|
||||
|
||||
const showTimeCol = useMemo(
|
||||
() => !config.get(DOC_HIDE_TIME_COLUMN_SETTING, false) && !!indexPattern.timeFieldName,
|
||||
[config, indexPattern]
|
||||
);
|
||||
const defaultStepSize = useMemo(() => parseInt(config.get(CONTEXT_STEP_SETTING), 10), [config]);
|
||||
|
||||
const docTableProps = () => {
|
||||
return {
|
||||
ariaLabelledBy: 'surDocumentsAriaLabel',
|
||||
columns,
|
||||
rows: rows as ElasticSearchHit[],
|
||||
indexPattern,
|
||||
expandedDoc,
|
||||
isLoading: isAnchorLoading,
|
||||
sampleSize: 0,
|
||||
sort: sort as [[string, SortDirection]],
|
||||
isSortEnabled: false,
|
||||
showTimeCol,
|
||||
services,
|
||||
useNewFieldsApi,
|
||||
isPaginationEnabled: false,
|
||||
controlColumnIds,
|
||||
setExpandedDoc,
|
||||
onFilter: addFilter,
|
||||
onAddColumn,
|
||||
onRemoveColumn,
|
||||
onSetColumns,
|
||||
} as DiscoverGridProps;
|
||||
};
|
||||
|
||||
const legacyDocTableProps = () => {
|
||||
// @ts-expect-error doesn't implement full DocTableLegacyProps interface
|
||||
return {
|
||||
columns,
|
||||
indexPattern,
|
||||
minimumVisibleRows: rows.length,
|
||||
rows,
|
||||
onFilter: addFilter,
|
||||
onAddColumn,
|
||||
onRemoveColumn,
|
||||
sort,
|
||||
useNewFieldsApi,
|
||||
} as DocTableLegacyProps;
|
||||
};
|
||||
|
||||
const loadingFeedback = () => {
|
||||
if (
|
||||
isLegacy &&
|
||||
(anchorStatus === LoadingStatus.UNINITIALIZED || anchorStatus === LoadingStatus.LOADING)
|
||||
) {
|
||||
return (
|
||||
<EuiText textAlign="center" data-test-subj="contextApp_loadingIndicator">
|
||||
<FormattedMessage id="discover.context.loadingDescription" defaultMessage="Loading..." />
|
||||
</EuiText>
|
||||
);
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
const onChangeCount = useCallback(
|
||||
(type: SurrDocType, count: number) => {
|
||||
const countKey = type === SurrDocType.SUCCESSORS ? 'successorCount' : 'predecessorCount';
|
||||
setAppState({ [countKey]: clamp(count) });
|
||||
},
|
||||
[setAppState]
|
||||
);
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
<ActionBarMemoized
|
||||
type={SurrDocType.PREDECESSORS}
|
||||
defaultStepSize={defaultStepSize}
|
||||
docCount={predecessorCount}
|
||||
docCountAvailable={predecessors.length}
|
||||
onChangeCount={onChangeCount}
|
||||
isLoading={arePredecessorsLoading}
|
||||
isDisabled={!isAnchorLoaded}
|
||||
/>
|
||||
{loadingFeedback()}
|
||||
<EuiHorizontalRule margin="xs" />
|
||||
{isLegacy && isAnchorLoaded && (
|
||||
<div className="discover-table">
|
||||
<DocTableLegacyMemoized {...legacyDocTableProps()} />
|
||||
</div>
|
||||
)}
|
||||
{!isLegacy && (
|
||||
<div className="dscDocsGrid">
|
||||
<DataGridMemoized {...docTableProps()} />
|
||||
</div>
|
||||
)}
|
||||
<EuiHorizontalRule margin="xs" />
|
||||
<ActionBarMemoized
|
||||
type={SurrDocType.SUCCESSORS}
|
||||
defaultStepSize={defaultStepSize}
|
||||
docCount={successorCount}
|
||||
docCountAvailable={successors.length}
|
||||
onChangeCount={onChangeCount}
|
||||
isLoading={areSuccessorsLoading}
|
||||
isDisabled={!isAnchorLoaded}
|
||||
/>
|
||||
</Fragment>
|
||||
);
|
||||
}
|
|
@ -1,224 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0 and the Server Side Public License, v 1; you may not use this file except
|
||||
* in compliance with, at your election, the Elastic License 2.0 or the Server
|
||||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import React, { useState, Fragment } from 'react';
|
||||
import classNames from 'classnames';
|
||||
import { FormattedMessage, I18nProvider } from '@kbn/i18n/react';
|
||||
import './context_app_legacy.scss';
|
||||
import { EuiHorizontalRule, EuiText, EuiPageContent, EuiPage, EuiSpacer } from '@elastic/eui';
|
||||
import { DOC_HIDE_TIME_COLUMN_SETTING, DOC_TABLE_LEGACY } from '../../../../common';
|
||||
import { ContextErrorMessage } from '../context_error_message';
|
||||
import {
|
||||
DocTableLegacy,
|
||||
DocTableLegacyProps,
|
||||
} from '../../angular/doc_table/create_doc_table_react';
|
||||
import { IndexPattern } from '../../../../../data/common/index_patterns';
|
||||
import { LoadingStatus } from '../../angular/context_app_state';
|
||||
import { ActionBar, ActionBarProps } from '../../angular/context/components/action_bar/action_bar';
|
||||
import { TopNavMenuProps } from '../../../../../navigation/public';
|
||||
import { DiscoverGrid, DiscoverGridProps } from '../discover_grid/discover_grid';
|
||||
import { DocViewFilterFn } from '../../doc_views/doc_views_types';
|
||||
import { getServices, SortDirection } from '../../../kibana_services';
|
||||
import { GetStateReturn, AppState } from '../../angular/context_state';
|
||||
import { useDataGridColumns } from '../../helpers/use_data_grid_columns';
|
||||
import { EsHitRecord, EsHitRecordList } from '../../angular/context/api/context';
|
||||
|
||||
export interface ContextAppProps {
|
||||
topNavMenu: React.ComponentType<TopNavMenuProps>;
|
||||
columns: string[];
|
||||
hits: EsHitRecordList;
|
||||
indexPattern: IndexPattern;
|
||||
appState: AppState;
|
||||
stateContainer: GetStateReturn;
|
||||
filter: DocViewFilterFn;
|
||||
minimumVisibleRows: number;
|
||||
sorting: Array<[string, SortDirection]>;
|
||||
anchorId: string;
|
||||
anchorStatus: string;
|
||||
anchorReason: string;
|
||||
predecessorStatus: string;
|
||||
successorStatus: string;
|
||||
defaultStepSize: number;
|
||||
predecessorCount: number;
|
||||
successorCount: number;
|
||||
predecessorAvailable: number;
|
||||
successorAvailable: number;
|
||||
onChangePredecessorCount: (count: number) => void;
|
||||
onChangeSuccessorCount: (count: number) => void;
|
||||
useNewFieldsApi?: boolean;
|
||||
}
|
||||
|
||||
const DataGridMemoized = React.memo(DiscoverGrid);
|
||||
const PREDECESSOR_TYPE = 'predecessors';
|
||||
const SUCCESSOR_TYPE = 'successors';
|
||||
|
||||
function isLoading(status: string) {
|
||||
return status !== LoadingStatus.LOADED && status !== LoadingStatus.FAILED;
|
||||
}
|
||||
|
||||
export function ContextAppLegacy(renderProps: ContextAppProps) {
|
||||
const services = getServices();
|
||||
const { uiSettings: config, capabilities, indexPatterns } = services;
|
||||
const {
|
||||
indexPattern,
|
||||
anchorId,
|
||||
anchorStatus,
|
||||
predecessorStatus,
|
||||
successorStatus,
|
||||
appState,
|
||||
stateContainer,
|
||||
hits: rows,
|
||||
sorting,
|
||||
filter,
|
||||
minimumVisibleRows,
|
||||
useNewFieldsApi,
|
||||
} = renderProps;
|
||||
const [expandedDoc, setExpandedDoc] = useState<EsHitRecord | undefined>(undefined);
|
||||
const isAnchorLoaded = anchorStatus === LoadingStatus.LOADED;
|
||||
const isFailed = anchorStatus === LoadingStatus.FAILED;
|
||||
const isLegacy = config.get(DOC_TABLE_LEGACY);
|
||||
|
||||
const { columns, onAddColumn, onRemoveColumn, onSetColumns } = useDataGridColumns({
|
||||
capabilities,
|
||||
config,
|
||||
indexPattern,
|
||||
indexPatterns,
|
||||
setAppState: stateContainer.setAppState,
|
||||
state: appState,
|
||||
useNewFieldsApi: !!useNewFieldsApi,
|
||||
});
|
||||
|
||||
const actionBarProps = (type: string) => {
|
||||
const {
|
||||
defaultStepSize,
|
||||
successorCount,
|
||||
predecessorCount,
|
||||
predecessorAvailable,
|
||||
successorAvailable,
|
||||
onChangePredecessorCount,
|
||||
onChangeSuccessorCount,
|
||||
} = renderProps;
|
||||
const isPredecessorType = type === PREDECESSOR_TYPE;
|
||||
return {
|
||||
defaultStepSize,
|
||||
docCount: isPredecessorType ? predecessorCount : successorCount,
|
||||
docCountAvailable: isPredecessorType ? predecessorAvailable : successorAvailable,
|
||||
onChangeCount: isPredecessorType ? onChangePredecessorCount : onChangeSuccessorCount,
|
||||
isLoading: isPredecessorType ? isLoading(predecessorStatus) : isLoading(successorStatus),
|
||||
type,
|
||||
isDisabled: !isAnchorLoaded,
|
||||
} as ActionBarProps;
|
||||
};
|
||||
|
||||
const docTableProps = () => {
|
||||
return {
|
||||
ariaLabelledBy: 'surDocumentsAriaLabel',
|
||||
columns,
|
||||
rows,
|
||||
indexPattern,
|
||||
expandedDoc,
|
||||
isLoading: isLoading(anchorStatus),
|
||||
sampleSize: 0,
|
||||
sort: sorting,
|
||||
isSortEnabled: false,
|
||||
showTimeCol: !config.get(DOC_HIDE_TIME_COLUMN_SETTING, false) && !!indexPattern.timeFieldName,
|
||||
services,
|
||||
useNewFieldsApi,
|
||||
isPaginationEnabled: false,
|
||||
controlColumnIds: ['openDetails'],
|
||||
setExpandedDoc,
|
||||
onFilter: filter,
|
||||
onAddColumn,
|
||||
onRemoveColumn,
|
||||
onSetColumns,
|
||||
} as DiscoverGridProps;
|
||||
};
|
||||
|
||||
const legacyDocTableProps = () => {
|
||||
// @ts-expect-error doesn't implement full DocTableLegacyProps interface
|
||||
return {
|
||||
columns,
|
||||
indexPattern,
|
||||
minimumVisibleRows,
|
||||
rows,
|
||||
onFilter: filter,
|
||||
onAddColumn,
|
||||
onRemoveColumn,
|
||||
sort: sorting.map((el) => [el]),
|
||||
useNewFieldsApi,
|
||||
} as DocTableLegacyProps;
|
||||
};
|
||||
|
||||
const TopNavMenu = renderProps.topNavMenu;
|
||||
const getNavBarProps = () => {
|
||||
return {
|
||||
appName: 'context',
|
||||
showSearchBar: true,
|
||||
showQueryBar: false,
|
||||
showFilterBar: true,
|
||||
showSaveQuery: false,
|
||||
showDatePicker: false,
|
||||
indexPatterns: [renderProps.indexPattern],
|
||||
useDefaultBehaviors: true,
|
||||
};
|
||||
};
|
||||
|
||||
const loadingFeedback = () => {
|
||||
if (anchorStatus === LoadingStatus.UNINITIALIZED || anchorStatus === LoadingStatus.LOADING) {
|
||||
return (
|
||||
<EuiText textAlign="center" data-test-subj="contextApp_loadingIndicator">
|
||||
<FormattedMessage id="discover.context.loadingDescription" defaultMessage="Loading..." />
|
||||
</EuiText>
|
||||
);
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
return (
|
||||
<I18nProvider>
|
||||
{isFailed ? (
|
||||
<ContextErrorMessage status={anchorStatus} reason={renderProps.anchorReason} />
|
||||
) : (
|
||||
<Fragment>
|
||||
<TopNavMenu {...getNavBarProps()} />
|
||||
<EuiPage className={classNames({ dscDocsPage: !isLegacy })}>
|
||||
<EuiPageContent paddingSize="s" className="dscDocsContent">
|
||||
<EuiSpacer size="s" />
|
||||
<EuiText>
|
||||
<strong>
|
||||
<FormattedMessage
|
||||
id="discover.context.contextOfTitle"
|
||||
defaultMessage="Documents surrounding #{anchorId}"
|
||||
values={{ anchorId }}
|
||||
/>
|
||||
</strong>
|
||||
</EuiText>
|
||||
<EuiSpacer size="s" />
|
||||
<ActionBar {...actionBarProps(PREDECESSOR_TYPE)} />
|
||||
{isLegacy && loadingFeedback()}
|
||||
<EuiHorizontalRule margin="xs" />
|
||||
{isLegacy ? (
|
||||
isAnchorLoaded && (
|
||||
<div className="discover-table">
|
||||
<DocTableLegacy {...legacyDocTableProps()} />
|
||||
</div>
|
||||
)
|
||||
) : (
|
||||
<div className="dscDocsGrid">
|
||||
<DataGridMemoized {...docTableProps()} />
|
||||
</div>
|
||||
)}
|
||||
<EuiHorizontalRule margin="xs" />
|
||||
<ActionBar {...actionBarProps(SUCCESSOR_TYPE)} />
|
||||
</EuiPageContent>
|
||||
</EuiPage>
|
||||
</Fragment>
|
||||
)}
|
||||
</I18nProvider>
|
||||
);
|
||||
}
|
|
@ -6,32 +6,13 @@
|
|||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import { ContextAppLegacy } from './context_app_legacy';
|
||||
import { ContextApp } from './context_app';
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
export function createContextAppLegacy(reactDirective: any) {
|
||||
return reactDirective(ContextAppLegacy, [
|
||||
['filter', { watchDepth: 'reference' }],
|
||||
['hits', { watchDepth: 'reference' }],
|
||||
return reactDirective(ContextApp, [
|
||||
['indexPattern', { watchDepth: 'reference' }],
|
||||
['appState', { watchDepth: 'reference' }],
|
||||
['stateContainer', { watchDepth: 'reference' }],
|
||||
['sorting', { watchDepth: 'reference' }],
|
||||
['columns', { watchDepth: 'collection' }],
|
||||
['minimumVisibleRows', { watchDepth: 'reference' }],
|
||||
['indexPatternId', { watchDepth: 'reference' }],
|
||||
['anchorId', { watchDepth: 'reference' }],
|
||||
['anchorStatus', { watchDepth: 'reference' }],
|
||||
['anchorReason', { watchDepth: 'reference' }],
|
||||
['defaultStepSize', { watchDepth: 'reference' }],
|
||||
['predecessorCount', { watchDepth: 'reference' }],
|
||||
['predecessorAvailable', { watchDepth: 'reference' }],
|
||||
['predecessorStatus', { watchDepth: 'reference' }],
|
||||
['onChangePredecessorCount', { watchDepth: 'reference' }],
|
||||
['successorCount', { watchDepth: 'reference' }],
|
||||
['successorAvailable', { watchDepth: 'reference' }],
|
||||
['successorStatus', { watchDepth: 'reference' }],
|
||||
['onChangeSuccessorCount', { watchDepth: 'reference' }],
|
||||
['useNewFieldsApi', { watchDepth: 'reference' }],
|
||||
['topNavMenu', { watchDepth: 'reference' }],
|
||||
]);
|
||||
}
|
||||
|
|
|
@ -0,0 +1,200 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0 and the Server Side Public License, v 1; you may not use this file except
|
||||
* in compliance with, at your election, the Elastic License 2.0 or the Server
|
||||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import { act, renderHook } from '@testing-library/react-hooks';
|
||||
import { setServices, getServices } from '../../../kibana_services';
|
||||
import { SortDirection } from '../../../../../data/public';
|
||||
import { createFilterManagerMock } from '../../../../../data/public/query/filter_manager/filter_manager.mock';
|
||||
import { CONTEXT_TIE_BREAKER_FIELDS_SETTING } from '../../../../common';
|
||||
import { DiscoverServices } from '../../../build_services';
|
||||
import { indexPatternMock } from '../../../__mocks__/index_pattern';
|
||||
import { indexPatternsMock } from '../../../__mocks__/index_patterns';
|
||||
import { FailureReason, LoadingStatus } from '../../angular/context_query_state';
|
||||
import { ContextAppFetchProps, useContextAppFetch } from './use_context_app_fetch';
|
||||
import {
|
||||
mockAnchorHit,
|
||||
mockPredecessorHits,
|
||||
mockSuccessorHits,
|
||||
} from './__mocks__/use_context_app_fetch';
|
||||
|
||||
const mockFilterManager = createFilterManagerMock();
|
||||
|
||||
jest.mock('../../angular/context/api/context', () => {
|
||||
const originalModule = jest.requireActual('../../angular/context/api/context');
|
||||
return {
|
||||
...originalModule,
|
||||
fetchContextProvider: () => ({
|
||||
fetchSurroundingDocs: (type: string, indexPatternId: string) => {
|
||||
if (!indexPatternId) {
|
||||
throw new Error();
|
||||
}
|
||||
return type === 'predecessors' ? mockPredecessorHits : mockSuccessorHits;
|
||||
},
|
||||
}),
|
||||
};
|
||||
});
|
||||
|
||||
jest.mock('../../angular/context/api/anchor', () => ({
|
||||
fetchAnchorProvider: () => (indexPatternId: string) => {
|
||||
if (!indexPatternId) {
|
||||
throw new Error();
|
||||
}
|
||||
return mockAnchorHit;
|
||||
},
|
||||
}));
|
||||
|
||||
const initDefaults = (tieBreakerFields: string[], indexPatternId = 'the-index-pattern-id') => {
|
||||
const dangerNotification = jest.fn();
|
||||
|
||||
setServices(({
|
||||
data: {
|
||||
search: {
|
||||
searchSource: {
|
||||
createEmpty: jest.fn(),
|
||||
},
|
||||
},
|
||||
},
|
||||
indexPatterns: indexPatternsMock,
|
||||
toastNotifications: { addDanger: dangerNotification },
|
||||
core: { notifications: { toasts: [] } },
|
||||
history: () => {},
|
||||
filterManager: mockFilterManager,
|
||||
uiSettings: {
|
||||
get: (key: string) => {
|
||||
if (key === CONTEXT_TIE_BREAKER_FIELDS_SETTING) {
|
||||
return tieBreakerFields;
|
||||
}
|
||||
},
|
||||
},
|
||||
} as unknown) as DiscoverServices);
|
||||
|
||||
return {
|
||||
dangerNotification,
|
||||
props: ({
|
||||
anchorId: 'mock_anchor_id',
|
||||
indexPatternId,
|
||||
indexPattern: indexPatternMock,
|
||||
appState: {
|
||||
sort: [['order_date', SortDirection.desc]],
|
||||
predecessorCount: 2,
|
||||
successorCount: 2,
|
||||
},
|
||||
useNewFieldsApi: false,
|
||||
services: getServices(),
|
||||
} as unknown) as ContextAppFetchProps,
|
||||
};
|
||||
};
|
||||
|
||||
describe('test useContextAppFetch', () => {
|
||||
it('should fetch all correctly', async () => {
|
||||
const { props } = initDefaults(['_doc']);
|
||||
|
||||
const { result } = renderHook(() => {
|
||||
return useContextAppFetch(props);
|
||||
});
|
||||
|
||||
expect(result.current.fetchedState.anchorStatus.value).toBe(LoadingStatus.UNINITIALIZED);
|
||||
expect(result.current.fetchedState.predecessorsStatus.value).toBe(LoadingStatus.UNINITIALIZED);
|
||||
expect(result.current.fetchedState.successorsStatus.value).toBe(LoadingStatus.UNINITIALIZED);
|
||||
|
||||
await act(async () => {
|
||||
await result.current.fetchAllRows();
|
||||
});
|
||||
|
||||
expect(result.current.fetchedState.anchorStatus.value).toBe(LoadingStatus.LOADED);
|
||||
expect(result.current.fetchedState.predecessorsStatus.value).toBe(LoadingStatus.LOADED);
|
||||
expect(result.current.fetchedState.successorsStatus.value).toBe(LoadingStatus.LOADED);
|
||||
expect(result.current.fetchedState.anchor).toEqual({ ...mockAnchorHit, isAnchor: true });
|
||||
expect(result.current.fetchedState.predecessors).toEqual(mockPredecessorHits);
|
||||
expect(result.current.fetchedState.successors).toEqual(mockSuccessorHits);
|
||||
});
|
||||
|
||||
it('should set anchorStatus to failed when tieBreakingField array is empty', async () => {
|
||||
const { props } = initDefaults([]);
|
||||
|
||||
const { result } = renderHook(() => {
|
||||
return useContextAppFetch(props);
|
||||
});
|
||||
|
||||
expect(result.current.fetchedState.anchorStatus.value).toBe(LoadingStatus.UNINITIALIZED);
|
||||
|
||||
await act(async () => {
|
||||
await result.current.fetchAllRows();
|
||||
});
|
||||
|
||||
expect(result.current.fetchedState.anchorStatus.value).toBe(LoadingStatus.FAILED);
|
||||
expect(result.current.fetchedState.anchorStatus.reason).toBe(FailureReason.INVALID_TIEBREAKER);
|
||||
expect(result.current.fetchedState.anchor).toEqual({});
|
||||
expect(result.current.fetchedState.predecessors).toEqual([]);
|
||||
expect(result.current.fetchedState.successors).toEqual([]);
|
||||
});
|
||||
|
||||
it('should set anchorStatus to failed when invalid indexPatternId provided', async () => {
|
||||
const { props, dangerNotification } = initDefaults(['_doc'], '');
|
||||
|
||||
const { result } = renderHook(() => {
|
||||
return useContextAppFetch(props);
|
||||
});
|
||||
|
||||
expect(result.current.fetchedState.anchorStatus.value).toBe(LoadingStatus.UNINITIALIZED);
|
||||
|
||||
await act(async () => {
|
||||
await result.current.fetchAllRows();
|
||||
});
|
||||
|
||||
expect(dangerNotification.mock.calls.length).toBe(1);
|
||||
expect(result.current.fetchedState.anchorStatus.value).toBe(LoadingStatus.FAILED);
|
||||
expect(result.current.fetchedState.anchorStatus.reason).toBe(FailureReason.UNKNOWN);
|
||||
expect(result.current.fetchedState.anchor).toEqual({});
|
||||
expect(result.current.fetchedState.predecessors).toEqual([]);
|
||||
expect(result.current.fetchedState.successors).toEqual([]);
|
||||
});
|
||||
|
||||
it('should fetch context rows correctly', async () => {
|
||||
const { props } = initDefaults(['_doc']);
|
||||
|
||||
const { result } = renderHook(() => {
|
||||
return useContextAppFetch(props);
|
||||
});
|
||||
|
||||
expect(result.current.fetchedState.predecessorsStatus.value).toBe(LoadingStatus.UNINITIALIZED);
|
||||
expect(result.current.fetchedState.successorsStatus.value).toBe(LoadingStatus.UNINITIALIZED);
|
||||
|
||||
await act(async () => {
|
||||
await result.current.fetchContextRows(mockAnchorHit);
|
||||
});
|
||||
|
||||
expect(result.current.fetchedState.predecessorsStatus.value).toBe(LoadingStatus.LOADED);
|
||||
expect(result.current.fetchedState.successorsStatus.value).toBe(LoadingStatus.LOADED);
|
||||
expect(result.current.fetchedState.predecessors).toEqual(mockPredecessorHits);
|
||||
expect(result.current.fetchedState.successors).toEqual(mockSuccessorHits);
|
||||
});
|
||||
|
||||
it('should set context rows statuses to failed when invalid indexPatternId provided', async () => {
|
||||
const { props, dangerNotification } = initDefaults(['_doc'], '');
|
||||
|
||||
const { result } = renderHook(() => {
|
||||
return useContextAppFetch(props);
|
||||
});
|
||||
|
||||
expect(result.current.fetchedState.predecessorsStatus.value).toBe(LoadingStatus.UNINITIALIZED);
|
||||
expect(result.current.fetchedState.successorsStatus.value).toBe(LoadingStatus.UNINITIALIZED);
|
||||
|
||||
await act(async () => {
|
||||
await result.current.fetchContextRows(mockAnchorHit);
|
||||
});
|
||||
|
||||
expect(dangerNotification.mock.calls.length).toBe(2); // for successors and predecessors
|
||||
expect(result.current.fetchedState.predecessorsStatus.value).toBe(LoadingStatus.FAILED);
|
||||
expect(result.current.fetchedState.successorsStatus.value).toBe(LoadingStatus.FAILED);
|
||||
expect(result.current.fetchedState.successorsStatus.reason).toBe(FailureReason.UNKNOWN);
|
||||
expect(result.current.fetchedState.successorsStatus.reason).toBe(FailureReason.UNKNOWN);
|
||||
expect(result.current.fetchedState.predecessors).toEqual([]);
|
||||
expect(result.current.fetchedState.successors).toEqual([]);
|
||||
});
|
||||
});
|
|
@ -0,0 +1,186 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0 and the Server Side Public License, v 1; you may not use this file except
|
||||
* in compliance with, at your election, the Elastic License 2.0 or the Server
|
||||
* Side Public License, v 1.
|
||||
*/
|
||||
import React, { useCallback, useMemo, useState } from 'react';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { fromPairs } from 'lodash';
|
||||
import { CONTEXT_TIE_BREAKER_FIELDS_SETTING } from '../../../../common';
|
||||
import { DiscoverServices } from '../../../build_services';
|
||||
import { fetchAnchorProvider } from '../../angular/context/api/anchor';
|
||||
import { EsHitRecord, fetchContextProvider, SurrDocType } from '../../angular/context/api/context';
|
||||
import { MarkdownSimple, toMountPoint } from '../../../../../kibana_react/public';
|
||||
import { IndexPattern, SortDirection } from '../../../../../data/public';
|
||||
import {
|
||||
ContextFetchState,
|
||||
FailureReason,
|
||||
getInitialContextQueryState,
|
||||
LoadingStatus,
|
||||
} from '../../angular/context_query_state';
|
||||
import { AppState } from '../../angular/context_state';
|
||||
import { getFirstSortableField } from '../../angular/context/api/utils/sorting';
|
||||
|
||||
const createError = (statusKey: string, reason: FailureReason, error?: Error) => ({
|
||||
[statusKey]: { value: LoadingStatus.FAILED, error, reason },
|
||||
});
|
||||
|
||||
export interface ContextAppFetchProps {
|
||||
anchorId: string;
|
||||
indexPatternId: string;
|
||||
indexPattern: IndexPattern;
|
||||
appState: AppState;
|
||||
useNewFieldsApi: boolean;
|
||||
services: DiscoverServices;
|
||||
}
|
||||
|
||||
export function useContextAppFetch({
|
||||
anchorId,
|
||||
indexPatternId,
|
||||
indexPattern,
|
||||
appState,
|
||||
useNewFieldsApi,
|
||||
services,
|
||||
}: ContextAppFetchProps) {
|
||||
const { uiSettings: config, data, indexPatterns, toastNotifications, filterManager } = services;
|
||||
|
||||
const searchSource = useMemo(() => {
|
||||
return data.search.searchSource.createEmpty();
|
||||
}, [data.search.searchSource]);
|
||||
const tieBreakerField = useMemo(
|
||||
() => getFirstSortableField(indexPattern, config.get(CONTEXT_TIE_BREAKER_FIELDS_SETTING)),
|
||||
[config, indexPattern]
|
||||
);
|
||||
const fetchAnchor = useMemo(() => {
|
||||
return fetchAnchorProvider(indexPatterns, searchSource, useNewFieldsApi);
|
||||
}, [indexPatterns, searchSource, useNewFieldsApi]);
|
||||
const { fetchSurroundingDocs } = useMemo(
|
||||
() => fetchContextProvider(indexPatterns, useNewFieldsApi),
|
||||
[indexPatterns, useNewFieldsApi]
|
||||
);
|
||||
|
||||
const [fetchedState, setFetchedState] = useState<ContextFetchState>(
|
||||
getInitialContextQueryState()
|
||||
);
|
||||
|
||||
const setState = useCallback((values: Partial<ContextFetchState>) => {
|
||||
setFetchedState((prevState) => ({ ...prevState, ...values }));
|
||||
}, []);
|
||||
|
||||
const fetchAnchorRow = useCallback(async () => {
|
||||
const { sort } = appState;
|
||||
const [[, sortDir]] = sort;
|
||||
const errorTitle = i18n.translate('discover.context.unableToLoadAnchorDocumentDescription', {
|
||||
defaultMessage: 'Unable to load the anchor document',
|
||||
});
|
||||
|
||||
if (!tieBreakerField) {
|
||||
setState(createError('anchorStatus', FailureReason.INVALID_TIEBREAKER));
|
||||
toastNotifications.addDanger({
|
||||
title: errorTitle,
|
||||
text: toMountPoint(
|
||||
<MarkdownSimple>
|
||||
{i18n.translate('discover.context.invalidTieBreakerFiledSetting', {
|
||||
defaultMessage: 'Invalid tie breaker field setting',
|
||||
})}
|
||||
</MarkdownSimple>
|
||||
),
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
setState({ anchorStatus: { value: LoadingStatus.LOADING } });
|
||||
const anchor = await fetchAnchor(indexPatternId, anchorId, [
|
||||
fromPairs(sort),
|
||||
{ [tieBreakerField]: sortDir },
|
||||
]);
|
||||
setState({ anchor, anchorStatus: { value: LoadingStatus.LOADED } });
|
||||
return anchor;
|
||||
} catch (error) {
|
||||
setState(createError('anchorStatus', FailureReason.UNKNOWN, error));
|
||||
toastNotifications.addDanger({
|
||||
title: errorTitle,
|
||||
text: toMountPoint(<MarkdownSimple>{error.message}</MarkdownSimple>),
|
||||
});
|
||||
}
|
||||
}, [
|
||||
appState,
|
||||
tieBreakerField,
|
||||
setState,
|
||||
toastNotifications,
|
||||
fetchAnchor,
|
||||
indexPatternId,
|
||||
anchorId,
|
||||
]);
|
||||
|
||||
const fetchSurroundingRows = useCallback(
|
||||
async (type: SurrDocType, fetchedAnchor?: EsHitRecord) => {
|
||||
const filters = filterManager.getFilters();
|
||||
const { sort } = appState;
|
||||
const [[sortField, sortDir]] = sort;
|
||||
|
||||
const count =
|
||||
type === SurrDocType.PREDECESSORS ? appState.predecessorCount : appState.successorCount;
|
||||
const anchor = fetchedAnchor || fetchedState.anchor;
|
||||
const statusKey = `${type}Status`;
|
||||
const errorTitle = i18n.translate('discover.context.unableToLoadDocumentDescription', {
|
||||
defaultMessage: 'Unable to load documents',
|
||||
});
|
||||
|
||||
try {
|
||||
setState({ [statusKey]: { value: LoadingStatus.LOADING } });
|
||||
const rows = await fetchSurroundingDocs(
|
||||
type,
|
||||
indexPatternId,
|
||||
anchor as EsHitRecord,
|
||||
sortField,
|
||||
tieBreakerField,
|
||||
sortDir as SortDirection,
|
||||
count,
|
||||
filters
|
||||
);
|
||||
setState({ [type]: rows, [statusKey]: { value: LoadingStatus.LOADED } });
|
||||
} catch (error) {
|
||||
setState(createError(statusKey, FailureReason.UNKNOWN, error));
|
||||
toastNotifications.addDanger({
|
||||
title: errorTitle,
|
||||
text: toMountPoint(<MarkdownSimple>{error.message}</MarkdownSimple>),
|
||||
});
|
||||
}
|
||||
},
|
||||
[
|
||||
filterManager,
|
||||
appState,
|
||||
fetchedState.anchor,
|
||||
tieBreakerField,
|
||||
setState,
|
||||
fetchSurroundingDocs,
|
||||
indexPatternId,
|
||||
toastNotifications,
|
||||
]
|
||||
);
|
||||
|
||||
const fetchContextRows = useCallback(
|
||||
(anchor?: EsHitRecord) =>
|
||||
Promise.allSettled([
|
||||
fetchSurroundingRows(SurrDocType.PREDECESSORS, anchor),
|
||||
fetchSurroundingRows(SurrDocType.SUCCESSORS, anchor),
|
||||
]),
|
||||
[fetchSurroundingRows]
|
||||
);
|
||||
|
||||
const fetchAllRows = useCallback(
|
||||
() => fetchAnchorRow().then((anchor) => anchor && fetchContextRows(anchor)),
|
||||
[fetchAnchorRow, fetchContextRows]
|
||||
);
|
||||
|
||||
return {
|
||||
fetchedState,
|
||||
fetchAllRows,
|
||||
fetchContextRows,
|
||||
fetchSurroundingRows,
|
||||
};
|
||||
}
|
|
@ -0,0 +1,77 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0 and the Server Side Public License, v 1; you may not use this file except
|
||||
* in compliance with, at your election, the Elastic License 2.0 or the Server
|
||||
* Side Public License, v 1.
|
||||
*/
|
||||
import { useEffect, useMemo, useState } from 'react';
|
||||
|
||||
import { cloneDeep } from 'lodash';
|
||||
import { CONTEXT_DEFAULT_SIZE_SETTING } from '../../../../common';
|
||||
import { IndexPattern } from '../../../../../data/public';
|
||||
import { DiscoverServices } from '../../../build_services';
|
||||
import { AppState, getState } from '../../angular/context_state';
|
||||
|
||||
export function useContextAppState({
|
||||
indexPattern,
|
||||
services,
|
||||
}: {
|
||||
indexPattern: IndexPattern;
|
||||
services: DiscoverServices;
|
||||
}) {
|
||||
const { uiSettings: config, history, core, filterManager } = services;
|
||||
|
||||
const stateContainer = useMemo(() => {
|
||||
return getState({
|
||||
defaultSize: parseInt(config.get(CONTEXT_DEFAULT_SIZE_SETTING), 10),
|
||||
timeFieldName: indexPattern.timeFieldName!,
|
||||
storeInSessionStorage: config.get('state:storeInSessionStorage'),
|
||||
history: history(),
|
||||
toasts: core.notifications.toasts,
|
||||
uiSettings: config,
|
||||
});
|
||||
}, [config, history, indexPattern, core.notifications.toasts]);
|
||||
|
||||
const [appState, setState] = useState<AppState>(stateContainer.appState.getState());
|
||||
|
||||
/**
|
||||
* Sync with app state container
|
||||
*/
|
||||
useEffect(() => {
|
||||
stateContainer.startSync();
|
||||
|
||||
return () => stateContainer.stopSync();
|
||||
}, [stateContainer]);
|
||||
|
||||
useEffect(() => {
|
||||
const unsubscribeAppState = stateContainer.appState.subscribe((newState) => {
|
||||
setState((prevState) => ({ ...prevState, ...newState }));
|
||||
});
|
||||
|
||||
return () => unsubscribeAppState();
|
||||
}, [stateContainer, setState]);
|
||||
|
||||
/**
|
||||
* Take care of filters
|
||||
*/
|
||||
useEffect(() => {
|
||||
const filters = stateContainer.appState.getState().filters;
|
||||
if (filters) {
|
||||
filterManager.setAppFilters(cloneDeep(filters));
|
||||
}
|
||||
|
||||
const { setFilters } = stateContainer;
|
||||
const filterObservable = filterManager.getUpdates$().subscribe(() => {
|
||||
setFilters(filterManager);
|
||||
});
|
||||
|
||||
return () => filterObservable.unsubscribe();
|
||||
}, [filterManager, stateContainer]);
|
||||
|
||||
return {
|
||||
appState,
|
||||
stateContainer,
|
||||
setAppState: stateContainer.setAppState,
|
||||
};
|
||||
}
|
|
@ -6,8 +6,5 @@
|
|||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import { createInitialQueryParametersState } from './state';
|
||||
|
||||
export const MAX_CONTEXT_SIZE = 10000; // Elasticsearch's default maximum size limit
|
||||
export const MIN_CONTEXT_SIZE = 0;
|
||||
export const QUERY_PARAMETER_KEYS = Object.keys(createInitialQueryParametersState());
|
|
@ -10,31 +10,33 @@ import React from 'react';
|
|||
import { mountWithIntl } from '@kbn/test/jest';
|
||||
import { ReactWrapper } from 'enzyme';
|
||||
import { ContextErrorMessage } from './context_error_message';
|
||||
import { FailureReason, LoadingStatus } from '../../angular/context_app_state';
|
||||
import { FailureReason, LoadingStatus } from '../../angular/context_query_state';
|
||||
import { findTestSubject } from '@elastic/eui/lib/test';
|
||||
|
||||
describe('loading spinner', function () {
|
||||
let component: ReactWrapper;
|
||||
|
||||
it('ContextErrorMessage does not render on loading', () => {
|
||||
component = mountWithIntl(<ContextErrorMessage status={LoadingStatus.LOADING} />);
|
||||
component = mountWithIntl(<ContextErrorMessage status={{ value: LoadingStatus.LOADING }} />);
|
||||
expect(findTestSubject(component, 'contextErrorMessageTitle').length).toBe(0);
|
||||
});
|
||||
|
||||
it('ContextErrorMessage does not render on success loading', () => {
|
||||
component = mountWithIntl(<ContextErrorMessage status={LoadingStatus.LOADED} />);
|
||||
component = mountWithIntl(<ContextErrorMessage status={{ value: LoadingStatus.LOADED }} />);
|
||||
expect(findTestSubject(component, 'contextErrorMessageTitle').length).toBe(0);
|
||||
});
|
||||
|
||||
it('ContextErrorMessage renders just the title if the reason is not specifically handled', () => {
|
||||
component = mountWithIntl(<ContextErrorMessage status={LoadingStatus.FAILED} />);
|
||||
component = mountWithIntl(<ContextErrorMessage status={{ value: LoadingStatus.FAILED }} />);
|
||||
expect(findTestSubject(component, 'contextErrorMessageTitle').length).toBe(1);
|
||||
expect(findTestSubject(component, 'contextErrorMessageBody').text()).toBe('');
|
||||
});
|
||||
|
||||
it('ContextErrorMessage renders the reason for unknown errors', () => {
|
||||
component = mountWithIntl(
|
||||
<ContextErrorMessage status={LoadingStatus.FAILED} reason={FailureReason.UNKNOWN} />
|
||||
<ContextErrorMessage
|
||||
status={{ value: LoadingStatus.FAILED, reason: FailureReason.UNKNOWN }}
|
||||
/>
|
||||
);
|
||||
expect(findTestSubject(component, 'contextErrorMessageTitle').length).toBe(1);
|
||||
expect(findTestSubject(component, 'contextErrorMessageBody').length).toBe(1);
|
||||
|
|
|
@ -9,21 +9,21 @@
|
|||
import React from 'react';
|
||||
import { EuiCallOut, EuiText } from '@elastic/eui';
|
||||
import { FormattedMessage, I18nProvider } from '@kbn/i18n/react';
|
||||
import { FailureReason, LoadingStatus } from '../../angular/context_app_state';
|
||||
import {
|
||||
FailureReason,
|
||||
LoadingStatus,
|
||||
LoadingStatusEntry,
|
||||
} from '../../angular/context_query_state';
|
||||
|
||||
export interface ContextErrorMessageProps {
|
||||
/**
|
||||
* the status of the loading action
|
||||
*/
|
||||
status: string;
|
||||
/**
|
||||
* the reason of the error
|
||||
*/
|
||||
reason?: string;
|
||||
status: LoadingStatusEntry;
|
||||
}
|
||||
|
||||
export function ContextErrorMessage({ status, reason }: ContextErrorMessageProps) {
|
||||
if (status !== LoadingStatus.FAILED) {
|
||||
export function ContextErrorMessage({ status }: ContextErrorMessageProps) {
|
||||
if (status.value !== LoadingStatus.FAILED) {
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
|
@ -40,7 +40,7 @@ export function ContextErrorMessage({ status, reason }: ContextErrorMessageProps
|
|||
data-test-subj="contextErrorMessageTitle"
|
||||
>
|
||||
<EuiText data-test-subj="contextErrorMessageBody">
|
||||
{reason === FailureReason.UNKNOWN && (
|
||||
{status.reason === FailureReason.UNKNOWN && (
|
||||
<FormattedMessage
|
||||
id="discover.context.reloadPageDescription.reloadOrVisitTextMessage"
|
||||
defaultMessage="Please reload or go back to the document list to select a valid anchor document."
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue