[Discover] Add EUIDataGrid to surrounding documents (#99447)

* [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] add disabling control columns option, change loading feedback

* [Discover] clean up, update functional tests

* [Discover] remove invalid translations

* [Discover] support both no results found and loading feedback

* [Discover] provide loading status for discover

* [Discover] fix functional test

* [Discover] add useDataGridColumns test, update by comments

* [Discover] fix types

Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
Dmitry Tomashevich 2021-06-01 14:29:01 +03:00 committed by GitHub
parent 1168e11639
commit 69883de634
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
40 changed files with 599 additions and 225 deletions

View file

@ -7,7 +7,7 @@
*/
import { IUiSettingsClient } from 'kibana/public';
import { DEFAULT_COLUMNS_SETTING, SAMPLE_SIZE_SETTING } from '../../common';
import { DEFAULT_COLUMNS_SETTING, DOC_TABLE_LEGACY, SAMPLE_SIZE_SETTING } from '../../common';
export const uiSettingsMock = ({
get: (key: string) => {
@ -15,6 +15,8 @@ export const uiSettingsMock = ({
return 10;
} else if (key === DEFAULT_COLUMNS_SETTING) {
return ['default_column'];
} else if (key === DOC_TABLE_LEGACY) {
return true;
}
},
} as unknown) as IUiSettingsClient;

View file

@ -2,8 +2,9 @@
anchor-id="contextAppRoute.anchorId"
columns="contextAppRoute.state.columns"
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>
sort="contextAppRoute.state.sort"></context-app>

View file

@ -15,19 +15,12 @@ import { getState } from './context_state';
import contextAppRouteTemplate from './context.html';
import { getRootBreadcrumbs } from '../helpers/breadcrumbs';
const k7Breadcrumbs = ($route) => {
const { indexPattern } = $route.current.locals;
const { id } = $route.current.params;
const k7Breadcrumbs = () => {
return [
...getRootBreadcrumbs(),
{
text: i18n.translate('discover.context.breadcrumb', {
defaultMessage: 'Context of {indexPatternTitle}#{docId}',
values: {
indexPatternTitle: indexPattern.title,
docId: id,
},
defaultMessage: 'Surrounding documents',
}),
},
];
@ -51,6 +44,14 @@ getAngularModule().config(($routeProvider) => {
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,
@ -59,14 +60,8 @@ function ContextAppRouteController($routeParams, $scope, $route) {
setFilters,
setAppState,
flushToUrl,
} = 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,
});
} = stateContainer;
this.stateContainer = stateContainer;
this.state = { ...appState.getState() };
this.anchorId = $routeParams.id;
this.indexPattern = indexPattern;

View file

@ -8,22 +8,21 @@
import { EsQuerySortValue, SortDirection } from '../../../../../../data/public';
import { createIndexPatternsStub, createSearchSourceStub } from './_stubs';
import { AnchorHitRecord, fetchAnchorProvider } from './anchor';
import { fetchAnchorProvider } from './anchor';
import { EsHitRecord, EsHitRecordList } from './context';
describe('context app', function () {
let fetchAnchor: (
indexPatternId: string,
anchorId: string,
sort: EsQuerySortValue[]
) => Promise<AnchorHitRecord>;
) => Promise<EsHitRecord>;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
let searchSourceStub: any;
describe('function fetchAnchor', function () {
beforeEach(() => {
searchSourceStub = createSearchSourceStub([
{ _id: 'hit1', fields: [], sort: [], _source: {} },
]);
searchSourceStub = createSearchSourceStub(([{ _id: 'hit1' }] as unknown) as EsHitRecordList);
fetchAnchor = fetchAnchorProvider(createIndexPatternsStub(), searchSourceStub);
});
@ -139,16 +138,14 @@ describe('context app', function () {
{ _doc: SortDirection.desc },
]).then((anchorDocument) => {
expect(anchorDocument).toHaveProperty('property1', 'value1');
expect(anchorDocument).toHaveProperty('$$_isAnchor', true);
expect(anchorDocument).toHaveProperty('isAnchor', true);
});
});
});
describe('useNewFields API', () => {
beforeEach(() => {
searchSourceStub = createSearchSourceStub([
{ _id: 'hit1', fields: [], sort: [], _source: {} },
]);
searchSourceStub = createSearchSourceStub(([{ _id: 'hit1' }] as unknown) as EsHitRecordList);
fetchAnchor = fetchAnchorProvider(createIndexPatternsStub(), searchSourceStub, true);
});

View file

@ -16,11 +16,6 @@ import {
} from '../../../../../../data/public';
import { EsHitRecord } from './context';
export interface AnchorHitRecord extends EsHitRecord {
// eslint-disable-next-line @typescript-eslint/naming-convention
$$_isAnchor: boolean;
}
export function fetchAnchorProvider(
indexPatterns: IndexPatternsContract,
searchSource: ISearchSource,
@ -30,7 +25,7 @@ export function fetchAnchorProvider(
indexPatternId: string,
anchorId: string,
sort: EsQuerySortValue[]
): Promise<AnchorHitRecord> {
): Promise<EsHitRecord> {
const indexPattern = await indexPatterns.get(indexPatternId);
searchSource
.setParent(undefined)
@ -66,8 +61,7 @@ export function fetchAnchorProvider(
return {
...get(response, ['hits', 'hits', 0]),
// eslint-disable-next-line @typescript-eslint/naming-convention
$$_isAnchor: true,
} as AnchorHitRecord;
isAnchor: true,
} as EsHitRecord;
};
}

View file

@ -11,7 +11,7 @@ import { get, last } from 'lodash';
import { createIndexPatternsStub, createContextSearchSourceStub } from './_stubs';
import { EsHitRecordList, fetchContextProvider } from './context';
import { setServices, SortDirection } from '../../../../kibana_services';
import { AnchorHitRecord } from './anchor';
import { EsHitRecord } from './context';
import { Query } from '../../../../../../data/public';
import { DiscoverServices } from '../../../../build_services';
@ -75,7 +75,7 @@ describe('context app', function () {
return fetchContextProvider(createIndexPatternsStub()).fetchSurroundingDocs(
'predecessors',
indexPatternId,
anchor as AnchorHitRecord,
anchor as EsHitRecord,
timeField,
tieBreakerField,
sortDir,
@ -267,7 +267,7 @@ describe('context app', function () {
return fetchContextProvider(createIndexPatternsStub(), true).fetchSurroundingDocs(
'predecessors',
indexPatternId,
anchor as AnchorHitRecord,
anchor as EsHitRecord,
timeField,
tieBreakerField,
sortDir,

View file

@ -13,7 +13,7 @@ import { createIndexPatternsStub, createContextSearchSourceStub } from './_stubs
import { setServices, SortDirection } from '../../../../kibana_services';
import { Query } from '../../../../../../data/public';
import { EsHitRecordList, fetchContextProvider } from './context';
import { AnchorHitRecord } from './anchor';
import { EsHitRecord } from './context';
import { DiscoverServices } from '../../../../build_services';
const MS_PER_DAY = 24 * 60 * 60 * 1000;
@ -75,7 +75,7 @@ describe('context app', function () {
return fetchContextProvider(createIndexPatternsStub()).fetchSurroundingDocs(
'successors',
indexPatternId,
anchor as AnchorHitRecord,
anchor as EsHitRecord,
timeField,
tieBreakerField,
sortDir,
@ -270,7 +270,7 @@ describe('context app', function () {
return fetchContextProvider(createIndexPatternsStub(), true).fetchSurroundingDocs(
'successors',
indexPatternId,
anchor as AnchorHitRecord,
anchor as EsHitRecord,
timeField,
tieBreakerField,
sortDir,

View file

@ -6,6 +6,7 @@
* Side Public License, v 1.
*/
import type { estypes } from '@elastic/elasticsearch';
import { Filter, IndexPatternsContract, IndexPattern } from 'src/plugins/data/public';
import { reverseSortDir, SortDirection } from './utils/sorting';
import { extractNanos, convertIsoToMillis } from './utils/date_conversion';
@ -14,17 +15,19 @@ import { generateIntervals } from './utils/generate_intervals';
import { getEsQuerySearchAfter } from './utils/get_es_query_search_after';
import { getEsQuerySort } from './utils/get_es_query_sort';
import { getServices } from '../../../../kibana_services';
import { AnchorHitRecord } from './anchor';
export type SurrDocType = 'successors' | 'predecessors';
export interface EsHitRecord {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
fields: Record<string, any>;
sort: number[];
// eslint-disable-next-line @typescript-eslint/no-explicit-any
_source: Record<string, any>;
_id: string;
}
export type EsHitRecord = Required<
Pick<
estypes.SearchResponse['hits']['hits'][number],
'_id' | 'fields' | 'sort' | '_index' | '_version'
>
> & {
_source?: Record<string, unknown>;
_score?: number;
isAnchor?: boolean;
};
export type EsHitRecordList = EsHitRecord[];
const DAY_MILLIS = 24 * 60 * 60 * 1000;
@ -53,7 +56,7 @@ function fetchContextProvider(indexPatterns: IndexPatternsContract, useNewFields
async function fetchSurroundingDocs(
type: SurrDocType,
indexPatternId: string,
anchor: AnchorHitRecord,
anchor: EsHitRecord,
timeField: string,
tieBreakerField: string,
sortDir: SortDirection,
@ -71,7 +74,7 @@ function fetchContextProvider(indexPatterns: IndexPatternsContract, useNewFields
const timeValueMillis =
nanos !== '' ? convertIsoToMillis(anchor.fields[timeField][0]) : anchor.sort[0];
const intervals = generateIntervals(LOOKUP_OFFSETS, timeValueMillis, type, sortDir);
const intervals = generateIntervals(LOOKUP_OFFSETS, timeValueMillis as number, type, sortDir);
let documents: EsHitRecordList = [];
for (const interval of intervals) {

View file

@ -28,23 +28,23 @@ export function getEsQuerySearchAfter(
// already surrounding docs -> first or last record is used
const afterTimeRecIdx = type === 'successors' && documents.length ? documents.length - 1 : 0;
const afterTimeDoc = documents[afterTimeRecIdx];
let afterTimeValue: string | number = afterTimeDoc.sort[0];
let afterTimeValue = afterTimeDoc.sort[0] as string | number;
if (nanoSeconds) {
afterTimeValue = useNewFieldsApi
? (afterTimeDoc.fields[timeFieldName] as Array<string | number>)[0]
: (afterTimeDoc._source[timeFieldName] as string | number);
? afterTimeDoc.fields[timeFieldName][0]
: afterTimeDoc._source?.[timeFieldName];
}
return [afterTimeValue, afterTimeDoc.sort[1]];
return [afterTimeValue, afterTimeDoc.sort[1] as string | number];
}
// if data_nanos adapt timestamp value for sorting, since numeric value was rounded by browser
// ES search_after also works when number is provided as string
const searchAfter = new Array(2) as EsQuerySearchAfter;
searchAfter[0] = anchor.sort[0];
searchAfter[0] = anchor.sort[0] as string | number;
if (nanoSeconds) {
searchAfter[0] = useNewFieldsApi
? (anchor.fields[timeFieldName] as Array<string | number>)[0]
: (anchor._source[timeFieldName] as string | number);
? anchor.fields[timeFieldName][0]
: anchor._source?.[timeFieldName];
}
searchAfter[1] = anchor.sort[1];
searchAfter[1] = anchor.sort[1] as string | number;
return searchAfter;
}

View file

@ -13,7 +13,7 @@ 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 { AnchorHitRecord, fetchAnchorProvider } from '../api/anchor';
import { fetchAnchorProvider } from '../api/anchor';
import { EsHitRecord, EsHitRecordList, fetchContextProvider, SurrDocType } from '../api/context';
import { getQueryParameterActions } from '../query_parameters';
import {
@ -77,11 +77,12 @@ export function QueryActionsProvider(Promise: DiscoverPromise) {
}
setLoadingStatus(state)('anchor');
const [[, sortDir]] = sort;
return Promise.try(() =>
fetchAnchor(indexPatternId, anchorId, [fromPairs([sort]), { [tieBreakerField]: sort[1] }])
fetchAnchor(indexPatternId, anchorId, [fromPairs(sort), { [tieBreakerField]: sortDir }])
).then(
(anchorDocument: AnchorHitRecord) => {
(anchorDocument: EsHitRecord) => {
setLoadedStatus(state)('anchor');
state.rows.anchor = anchorDocument;
return anchorDocument;
@ -120,7 +121,7 @@ export function QueryActionsProvider(Promise: DiscoverPromise) {
}
setLoadingStatus(state)(type);
const [sortField, sortDir] = sort;
const [[sortField, sortDir]] = sort;
return Promise.try(() =>
fetchSurroundingDocs(

View file

@ -10,6 +10,7 @@ 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;
@ -29,7 +30,7 @@ beforeEach(() => {
anchorId: '',
columns: [],
filters: [],
sort: ['field', SortDirection.asc],
sort: [['field', SortDirection.asc]],
tieBreakerField: '',
},
loadingStatus: {
@ -39,8 +40,7 @@ beforeEach(() => {
},
rows: {
all: [],
// eslint-disable-next-line @typescript-eslint/naming-convention
anchor: { $$_isAnchor: true, fields: [], sort: [], _source: [], _id: '' },
anchor: ({ isAnchor: true, fields: [], sort: [], _id: '' } as unknown) as EsHitRecord,
predecessors: [],
successors: [],
},
@ -129,7 +129,7 @@ describe('context query_parameter actions', function () {
indexPatternId: 'INDEX_PATTERN',
predecessorCount: 100,
successorCount: 100,
sort: ['field', SortDirection.asc],
sort: [['field', SortDirection.asc]],
tieBreakerField: '',
});
@ -142,7 +142,7 @@ describe('context query_parameter actions', function () {
indexPatternId: 'INDEX_PATTERN',
predecessorCount: 100,
successorCount: 100,
sort: ['field', SortDirection.asc],
sort: [['field', SortDirection.asc]],
tieBreakerField: '',
});
});

View file

@ -3,11 +3,14 @@
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"
status="contextApp.state.loadingStatus.anchor.status"
reason="contextApp.state.loadingStatus.anchor.reason"
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"
@ -18,5 +21,4 @@
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>
top-nav-menu="contextApp.topNavMenu"></context-app-legacy>

View file

@ -34,6 +34,8 @@ getAngularModule().directive('contextApp', function ContextApp() {
anchorId: '=',
columns: '=',
indexPattern: '=',
appState: '=',
stateContainer: '=',
filters: '=',
predecessorCount: '=',
successorCount: '=',
@ -55,7 +57,6 @@ function ContextAppController($scope, Private) {
);
this.state.useNewFieldsApi = useNewFieldsApi;
this.topNavMenu = navigation.ui.TopNavMenu;
this.actions = _.mapValues(
{
...queryParameterActions,

View file

@ -7,7 +7,7 @@
*/
import { Filter } from '../../../../data/public';
import { AnchorHitRecord } from './context/api/anchor';
import { EsHitRecord } from './context/api/context';
import { EsHitRecordList } from './context/api/context';
import { SortDirection } from './context/api/utils/sorting';
@ -48,13 +48,13 @@ export interface QueryParameters {
indexPatternId: string;
predecessorCount: number;
successorCount: number;
sort: [string, SortDirection];
sort: Array<[string, SortDirection]>;
tieBreakerField: string;
}
interface ContextRows {
all: EsHitRecordList;
anchor: AnchorHitRecord;
anchor: EsHitRecord;
predecessors: EsHitRecordList;
successors: EsHitRecordList;
}

View file

@ -45,8 +45,10 @@ describe('Test Discover Context State', () => {
"filters": Array [],
"predecessorCount": 4,
"sort": Array [
"time",
"desc",
Array [
"time",
"desc",
],
],
"successorCount": 4,
}
@ -60,7 +62,7 @@ describe('Test Discover Context State', () => {
state.setAppState({ predecessorCount: 10 });
state.flushToUrl();
expect(getCurrentUrl()).toMatchInlineSnapshot(
`"/#?_a=(columns:!(_source),filters:!(),predecessorCount:10,sort:!(time,desc),successorCount:4)"`
`"/#?_a=(columns:!(_source),filters:!(),predecessorCount:10,sort:!(!(time,desc)),successorCount:4)"`
);
});
test('getState -> url to appState syncing', async () => {
@ -183,7 +185,7 @@ describe('Test Discover Context State', () => {
`);
state.flushToUrl();
expect(getCurrentUrl()).toMatchInlineSnapshot(
`"/#?_g=(filters:!(('$state':(store:globalState),meta:(alias:!n,disabled:!f,index:'logstash-*',key:extension,negate:!f,params:(query:jpg),type:phrase),query:(match:(extension:(query:jpg,type:phrase))))))&_a=(columns:!(_source),filters:!(('$state':(store:appState),meta:(alias:!n,disabled:!f,index:'logstash-*',key:extension,negate:!t,params:(query:png),type:phrase),query:(match:(extension:(query:png,type:phrase))))),predecessorCount:4,sort:!(time,desc),successorCount:4)"`
`"/#?_g=(filters:!(('$state':(store:globalState),meta:(alias:!n,disabled:!f,index:'logstash-*',key:extension,negate:!f,params:(query:jpg),type:phrase),query:(match:(extension:(query:jpg,type:phrase))))))&_a=(columns:!(_source),filters:!(('$state':(store:appState),meta:(alias:!n,disabled:!f,index:'logstash-*',key:extension,negate:!t,params:(query:png),type:phrase),query:(match:(extension:(query:png,type:phrase))))),predecessorCount:4,sort:!(!(time,desc)),successorCount:4)"`
);
});
});

View file

@ -35,7 +35,7 @@ export interface AppState {
/**
* Sorting of the records to be fetched, assumed to be a legacy parameter
*/
sort: string[];
sort: string[][];
/**
* Number of records to be fetched after the anchor records (older records)
*/
@ -50,7 +50,7 @@ interface GlobalState {
filters: Filter[];
}
interface GetStateParams {
export interface GetStateParams {
/**
* Number of records to be fetched when 'Load' link/button is clicked
*/
@ -81,7 +81,7 @@ interface GetStateParams {
uiSettings: IUiSettingsClient;
}
interface GetStateReturn {
export interface GetStateReturn {
/**
* Global state, the _g part of the URL
*/
@ -276,7 +276,7 @@ function createInitialAppState(
columns: ['_source'],
filters: [],
predecessorCount: parseInt(defaultSize, 10),
sort: [timeFieldName, 'desc'],
sort: [[timeFieldName, 'desc']],
successorCount: parseInt(defaultSize, 10),
};
if (typeof urlState !== 'object') {

View file

@ -10,13 +10,13 @@
opts="opts"
reset-query="resetQuery"
result-state="resultState"
fetch-status="fetchStatus"
rows="rows"
search-source="volatileSearchSource"
state="state"
top-nav-menu="topNavMenu"
use-new-fields-api="useNewFieldsApi"
unmapped-fields-config="unmappedFieldsConfig"
refresh-app-state="refreshAppState"
>
refresh-app-state="refreshAppState">
</discover>
</discover-app>
</discover-app>

View file

@ -8,7 +8,14 @@
import { Capabilities, IUiSettingsClient } from 'kibana/public';
import { popularizeField } from '../../../helpers/popularize_field';
import { IndexPattern, IndexPatternsContract } from '../../../../kibana_services';
import { AppState } from '../../discover_state';
import {
AppState as DiscoverState,
GetStateReturn as DiscoverGetStateReturn,
} from '../../discover_state';
import {
AppState as ContextState,
GetStateReturn as ContextGetStateReturn,
} from '../../context_state';
import { SORT_DEFAULT_ORDER_SETTING } from '../../../../../common';
/**
@ -67,8 +74,8 @@ export function getStateColumnActions({
indexPattern: IndexPattern;
indexPatterns: IndexPatternsContract;
useNewFieldsApi: boolean;
setAppState: (state: Partial<AppState>) => void;
state: AppState;
setAppState: DiscoverGetStateReturn['setAppState'] | ContextGetStateReturn['setAppState'];
state: DiscoverState | ContextState;
}) {
function onAddColumn(columnName: string) {
if (capabilities.discover.save) {

View file

@ -95,8 +95,8 @@
index-pattern="indexPattern"
filter="filter"
class="kbnDocTable__row"
ng-class="{'kbnDocTable__row--highlight': row['$$_isAnchor']}"
data-test-subj="docTableRow{{ row['$$_isAnchor'] ? ' docTableAnchorRow' : ''}}"
ng-class="{'kbnDocTable__row--highlight': row['isAnchor']}"
data-test-subj="docTableRow{{ row['isAnchor'] ? ' docTableAnchorRow' : ''}}"
on-add-column="onAddColumn"
on-remove-column="onRemoveColumn"
use-new-fields-api="useNewFieldsApi"

View file

@ -0,0 +1,24 @@
@import '../../../../../../core/public/mixins';
.dscDocsPage {
@include kibanaFullBodyHeight(54px); // action bar height
}
.dscDocsContent {
display: flex;
flex-direction: column;
height: 100%;
}
.dscDocsGrid {
flex: 1 1 100%;
overflow: auto;
&__cell--highlight {
background-color: tintOrShade($euiColorPrimary, 90%, 70%);
}
.euiDataGridRowCell.euiDataGridRowCell--firstColumn {
padding: 0;
}
}

View file

@ -7,14 +7,34 @@
*/
import React from 'react';
import { ContextAppLegacy } from './context_app_legacy';
import { IIndexPattern } from '../../../../../data/common/index_patterns';
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';
jest.mock('../../../kibana_services', () => {
return {
getServices: () => ({
metadata: {
branch: 'test',
},
capabilities: {
discover: {
save: true,
},
},
uiSettings: mockUiSettings,
}),
};
});
describe('ContextAppLegacy test', () => {
const hit = {
@ -35,16 +55,19 @@ describe('ContextAppLegacy test', () => {
};
const indexPattern = {
id: 'test_index_pattern',
} as IIndexPattern;
} as IndexPattern;
const defaultProps = {
columns: ['_source'],
filter: () => {},
hits: [hit],
sorting: ['order_date', 'desc'],
hits: ([hit] as unknown) as EsHitRecordList,
sorting: [['order_date', 'desc']] as Array<[string, SortDirection]>,
minimumVisibleRows: 5,
indexPattern,
status: 'loaded',
reason: 'no reason',
appState: ({} as unknown) as AppState,
stateContainer: ({} as unknown) as GetStateReturn,
anchorId: 'test_anchor_id',
anchorStatus: 'loaded',
anchorReason: 'no reason',
defaultStepSize: 5,
predecessorCount: 10,
successorCount: 10,
@ -55,6 +78,8 @@ describe('ContextAppLegacy test', () => {
predecessorStatus: 'loaded',
successorStatus: 'loaded',
topNavMenu: TopNavMenuMock,
useNewFieldsApi: false,
isPaginationEnabled: false,
};
const topNavProps = {
appName: 'context',
@ -80,7 +105,7 @@ describe('ContextAppLegacy test', () => {
it('renders loading indicator', () => {
const props = { ...defaultProps };
props.status = 'loading';
props.anchorStatus = 'loading';
const component = mountWithIntl(<ContextAppLegacy {...props} />);
expect(component.find(DocTableLegacy).length).toBe(0);
const loadingIndicator = findTestSubject(component, 'contextApp_loadingIndicator');
@ -91,8 +116,8 @@ describe('ContextAppLegacy test', () => {
it('renders error message', () => {
const props = { ...defaultProps };
props.status = 'failed';
props.reason = 'something went wrong';
props.anchorStatus = 'failed';
props.anchorReason = 'something went wrong';
const component = mountWithIntl(<ContextAppLegacy {...props} />);
expect(component.find(DocTableLegacy).length).toBe(0);
expect(component.find(TopNavMenuMock).length).toBe(0);

View file

@ -6,29 +6,43 @@
* Side Public License, v 1.
*/
import React from 'react';
import React, { useState, Fragment } from 'react';
import classNames from 'classnames';
import { FormattedMessage, I18nProvider } from '@kbn/i18n/react';
import { EuiHorizontalRule, EuiText, EuiPageContent, EuiPage } from '@elastic/eui';
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 { IIndexPattern, IndexPatternField } from '../../../../../data/common/index_patterns';
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: Array<Record<string, unknown>>;
indexPattern: IIndexPattern;
filter: (field: IndexPatternField | string, value: string, type: '+' | '-') => void;
hits: EsHitRecordList;
indexPattern: IndexPattern;
appState: AppState;
stateContainer: GetStateReturn;
filter: DocViewFilterFn;
minimumVisibleRows: number;
sorting: string[];
status: string;
reason: string;
sorting: Array<[string, SortDirection]>;
anchorId: string;
anchorStatus: string;
anchorReason: string;
predecessorStatus: string;
successorStatus: string;
defaultStepSize: number;
predecessorCount: number;
successorCount: number;
@ -36,11 +50,10 @@ export interface ContextAppProps {
successorAvailable: number;
onChangePredecessorCount: (count: number) => void;
onChangeSuccessorCount: (count: number) => void;
predecessorStatus: string;
successorStatus: string;
useNewFieldsApi?: boolean;
}
const DataGridMemoized = React.memo(DiscoverGrid);
const PREDECESSOR_TYPE = 'predecessors';
const SUCCESSOR_TYPE = 'successors';
@ -49,9 +62,36 @@ function isLoading(status: string) {
}
export function ContextAppLegacy(renderProps: ContextAppProps) {
const status = renderProps.status;
const isLoaded = status === LoadingStatus.LOADED;
const isFailed = status === LoadingStatus.FAILED;
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 {
@ -60,8 +100,6 @@ export function ContextAppLegacy(renderProps: ContextAppProps) {
predecessorCount,
predecessorAvailable,
successorAvailable,
predecessorStatus,
successorStatus,
onChangePredecessorCount,
onChangeSuccessorCount,
} = renderProps;
@ -73,27 +111,44 @@ export function ContextAppLegacy(renderProps: ContextAppProps) {
onChangeCount: isPredecessorType ? onChangePredecessorCount : onChangeSuccessorCount,
isLoading: isPredecessorType ? isLoading(predecessorStatus) : isLoading(successorStatus),
type,
isDisabled: !isLoaded,
isDisabled: !isAnchorLoaded,
} as ActionBarProps;
};
const docTableProps = () => {
const {
hits,
filter,
sorting,
return {
ariaLabelledBy: 'surDocumentsAriaLabel',
columns,
rows,
indexPattern,
minimumVisibleRows,
expandedDoc,
isLoading: isLoading(anchorStatus),
sampleSize: 0,
sort: sorting,
isSortEnabled: false,
showTimeCol: !config.get(DOC_HIDE_TIME_COLUMN_SETTING, false) && !!indexPattern.timeFieldName,
services,
useNewFieldsApi,
} = renderProps;
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: hits,
rows,
onFilter: filter,
onAddColumn,
onRemoveColumn,
sort: sorting.map((el) => [el]),
useNewFieldsApi,
} as DocTableLegacyProps;
@ -114,7 +169,7 @@ export function ContextAppLegacy(renderProps: ContextAppProps) {
};
const loadingFeedback = () => {
if (status === LoadingStatus.UNINITIALIZED || status === LoadingStatus.LOADING) {
if (anchorStatus === LoadingStatus.UNINITIALIZED || anchorStatus === LoadingStatus.LOADING) {
return (
<EuiText textAlign="center" data-test-subj="contextApp_loadingIndicator">
<FormattedMessage id="discover.context.loadingDescription" defaultMessage="Loading..." />
@ -127,25 +182,42 @@ export function ContextAppLegacy(renderProps: ContextAppProps) {
return (
<I18nProvider>
{isFailed ? (
<ContextErrorMessage status={status} reason={renderProps.reason} />
<ContextErrorMessage status={anchorStatus} reason={renderProps.anchorReason} />
) : (
<div>
<Fragment>
<TopNavMenu {...getNavBarProps()} />
<EuiPage>
<EuiPageContent paddingSize="s" className="dscCxtAppContent">
<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)} />
{loadingFeedback()}
{isLegacy && loadingFeedback()}
<EuiHorizontalRule margin="xs" />
{isLoaded ? (
<div className="discover-table">
<DocTableLegacy {...docTableProps()} />
{isLegacy ? (
isAnchorLoaded && (
<div className="discover-table">
<DocTableLegacy {...legacyDocTableProps()} />
</div>
)
) : (
<div className="dscDocsGrid">
<DataGridMemoized {...docTableProps()} />
</div>
) : null}
)}
<EuiHorizontalRule margin="xs" />
<ActionBar {...actionBarProps(SUCCESSOR_TYPE)} />
</EuiPageContent>
</EuiPage>
</div>
</Fragment>
)}
</I18nProvider>
);

View file

@ -14,11 +14,14 @@ export function createContextAppLegacy(reactDirective: any) {
['filter', { watchDepth: 'reference' }],
['hits', { watchDepth: 'reference' }],
['indexPattern', { watchDepth: 'reference' }],
['appState', { watchDepth: 'reference' }],
['stateContainer', { watchDepth: 'reference' }],
['sorting', { watchDepth: 'reference' }],
['columns', { watchDepth: 'collection' }],
['minimumVisibleRows', { watchDepth: 'reference' }],
['status', { watchDepth: 'reference' }],
['reason', { watchDepth: 'reference' }],
['anchorId', { watchDepth: 'reference' }],
['anchorStatus', { watchDepth: 'reference' }],
['anchorReason', { watchDepth: 'reference' }],
['defaultStepSize', { watchDepth: 'reference' }],
['predecessorCount', { watchDepth: 'reference' }],
['predecessorAvailable', { watchDepth: 'reference' }],

View file

@ -20,6 +20,7 @@ export function createDiscoverDirective(reactDirective: any) {
['opts', { watchDepth: 'reference' }],
['resetQuery', { watchDepth: 'reference' }],
['resultState', { watchDepth: 'reference' }],
['fetchStatus', { watchDepth: 'reference' }],
['rows', { watchDepth: 'reference' }],
['savedSearch', { watchDepth: 'reference' }],
['searchSource', { watchDepth: 'reference' }],

View file

@ -34,9 +34,12 @@ import { esFilters, IndexPatternField, search } from '../../../../data/public';
import { DiscoverSidebarResponsive } from './sidebar';
import { DiscoverProps } from './types';
import { SortPairArr } from '../angular/doc_table/lib/get_sort';
import { SEARCH_FIELDS_FROM_SOURCE } from '../../../common';
import {
DOC_HIDE_TIME_COLUMN_SETTING,
DOC_TABLE_LEGACY,
SEARCH_FIELDS_FROM_SOURCE,
} from '../../../common';
import { popularizeField } from '../helpers/popularize_field';
import { getStateColumnActions } from '../angular/doc_table/actions/columns';
import { DocViewFilterFn } from '../doc_views/doc_views_types';
import { DiscoverGrid } from './discover_grid/discover_grid';
import { DiscoverTopNav } from './discover_topnav';
@ -44,6 +47,7 @@ import { ElasticSearchHit } from '../doc_views/doc_views_types';
import { setBreadcrumbsTitle } from '../helpers/breadcrumbs';
import { addHelpMenuToAppChrome } from './help_menu/help_menu_util';
import { InspectorSession } from '../../../../inspector/public';
import { useDataGridColumns } from '../helpers/use_data_grid_columns';
const DocTableLegacyMemoized = React.memo(DocTableLegacy);
const SidebarMemoized = React.memo(DiscoverSidebarResponsive);
@ -96,7 +100,7 @@ export function Discover({
}, [opts.chartAggConfigs]);
const contentCentered = resultState === 'uninitialized';
const isLegacy = services.uiSettings.get('doc_table:legacy');
const isLegacy = services.uiSettings.get(DOC_TABLE_LEGACY);
const useNewFieldsApi = !services.uiSettings.get(SEARCH_FIELDS_FROM_SOURCE);
const updateQuery = useCallback(
(_payload, isUpdate?: boolean) => {
@ -108,6 +112,16 @@ export function Discover({
[opts]
);
const { columns, onAddColumn, onRemoveColumn, onMoveColumn, onSetColumns } = useDataGridColumns({
capabilities,
config,
indexPattern,
indexPatterns,
setAppState,
state,
useNewFieldsApi,
});
useEffect(() => {
const pageTitleSuffix = savedSearch.id && savedSearch.title ? `: ${savedSearch.title}` : '';
chrome.docTitle.change(`Discover${pageTitleSuffix}`);
@ -116,20 +130,6 @@ export function Discover({
addHelpMenuToAppChrome(chrome, docLinks);
}, [savedSearch, chrome, docLinks]);
const { onAddColumn, onRemoveColumn, onMoveColumn, onSetColumns } = useMemo(
() =>
getStateColumnActions({
capabilities,
config,
indexPattern,
indexPatterns,
setAppState,
state,
useNewFieldsApi,
}),
[capabilities, config, indexPattern, indexPatterns, setAppState, state, useNewFieldsApi]
);
const onOpenInspector = useCallback(() => {
// prevent overlapping
setExpandedDoc(undefined);
@ -225,12 +225,6 @@ export function Discover({
}
};
const columns = useMemo(() => {
if (!state.columns) {
return [];
}
return useNewFieldsApi ? state.columns.filter((col) => col !== '_source') : state.columns;
}, [state, useNewFieldsApi]);
return (
<I18nProvider>
<EuiPage className="dscPage" data-fetch-counter={fetchCounter}>
@ -439,13 +433,13 @@ export function Discover({
searchTitle={opts.savedSearch.lastSavedTitle}
setExpandedDoc={setExpandedDoc}
showTimeCol={
!config.get('doc_table:hideTimeColumn', false) &&
!config.get(DOC_HIDE_TIME_COLUMN_SETTING, false) &&
!!indexPattern.timeFieldName
}
services={services}
settings={state.grid}
onAddColumn={onAddColumn}
onFilter={onAddFilter as DocViewFilterFn}
onAddColumn={onAddColumn}
onRemoveColumn={onRemoveColumn}
onSetColumns={onSetColumns}
onSort={onSort}

View file

@ -66,6 +66,7 @@
text-align: right;
}
.euiDataGrid__loading,
.euiDataGrid__noResults {
display: flex;
flex-direction: column;

View file

@ -14,11 +14,12 @@ import {
EuiDataGridStyle,
EuiDataGridProps,
EuiDataGrid,
EuiIcon,
EuiScreenReaderOnly,
EuiSpacer,
EuiText,
htmlIdGenerator,
EuiLoadingSpinner,
EuiIcon,
} from '@elastic/eui';
import { IndexPattern } from '../../../kibana_services';
import { DocViewFilterFn, ElasticSearchHit } from '../../doc_views/doc_views_types';
@ -88,9 +89,9 @@ export interface DiscoverGridProps {
*/
onSetColumns: (columns: string[]) => void;
/**
* function to change sorting of the documents
* function to change sorting of the documents, skipped when isSortEnabled is set to false
*/
onSort: (sort: string[][]) => void;
onSort?: (sort: string[][]) => void;
/**
* Array of documents provided by Elasticsearch
*/
@ -123,6 +124,10 @@ export interface DiscoverGridProps {
* Determines whether the time columns should be displayed (legacy settings)
*/
showTimeCol: boolean;
/**
* Manage user sorting control
*/
isSortEnabled?: boolean;
/**
* Current sort setting
*/
@ -131,6 +136,14 @@ export interface DiscoverGridProps {
* How the data is fetched
*/
useNewFieldsApi: boolean;
/**
* Manage pagination control
*/
isPaginationEnabled?: boolean;
/**
* List of used control columns (available: 'openDetails', 'select')
*/
controlColumnIds?: string[];
}
export const EuiDataGridMemoized = React.memo((props: EuiDataGridProps) => {
@ -159,6 +172,9 @@ export const DiscoverGrid = ({
showTimeCol,
sort,
useNewFieldsApi,
isSortEnabled = true,
isPaginationEnabled = true,
controlColumnIds = ['openDetails', 'select'],
}: DiscoverGridProps) => {
const [selectedDocs, setSelectedDocs] = useState<string[]>([]);
const [isFilterActive, setIsFilterActive] = useState(false);
@ -210,14 +226,16 @@ export const DiscoverGrid = ({
const onChangePage = (pageIndex: number) =>
setPagination((paginationData) => ({ ...paginationData, pageIndex }));
return {
onChangeItemsPerPage,
onChangePage,
pageIndex: pagination.pageIndex > pageCount - 1 ? 0 : pagination.pageIndex,
pageSize: pagination.pageSize,
pageSizeOptions: pageSizeArr,
};
}, [pagination, pageCount]);
return isPaginationEnabled
? {
onChangeItemsPerPage,
onChangePage,
pageIndex: pagination.pageIndex > pageCount - 1 ? 0 : pagination.pageIndex,
pageSize: pagination.pageSize,
pageSizeOptions: pageSizeArr,
}
: undefined;
}, [pagination, pageCount, isPaginationEnabled]);
/**
* Sorting
@ -226,9 +244,11 @@ export const DiscoverGrid = ({
const onTableSort = useCallback(
(sortingColumnsData) => {
onSort(sortingColumnsData.map(({ id, direction }: SortObj) => [id, direction]));
if (isSortEnabled && onSort) {
onSort(sortingColumnsData.map(({ id, direction }: SortObj) => [id, direction]));
}
},
[onSort]
[onSort, isSortEnabled]
);
/**
@ -253,8 +273,16 @@ export const DiscoverGrid = ({
const randomId = useMemo(() => htmlIdGenerator()(), []);
const euiGridColumns = useMemo(
() => getEuiGridColumns(displayedColumns, settings, indexPattern, showTimeCol, defaultColumns),
[displayedColumns, indexPattern, showTimeCol, settings, defaultColumns]
() =>
getEuiGridColumns(
displayedColumns,
settings,
indexPattern,
showTimeCol,
defaultColumns,
isSortEnabled
),
[displayedColumns, indexPattern, showTimeCol, settings, defaultColumns, isSortEnabled]
);
const schemaDetectors = useMemo(() => getSchemaDetectors(), []);
const columnsVisibility = useMemo(
@ -266,11 +294,16 @@ export const DiscoverGrid = ({
}),
[displayedColumns, indexPattern, showTimeCol, onSetColumns]
);
const sorting = useMemo(() => ({ columns: sortingColumns, onSort: onTableSort }), [
sortingColumns,
onTableSort,
]);
const lead = useMemo(() => getLeadControlColumns(), []);
const sorting = useMemo(() => {
if (isSortEnabled) {
return { columns: sortingColumns, onSort: onTableSort };
}
return { columns: sortingColumns, onSort: () => {} };
}, [sortingColumns, onTableSort, isSortEnabled]);
const lead = useMemo(
() => getLeadControlColumns().filter(({ id }) => controlColumnIds.includes(id)),
[controlColumnIds]
);
const additionalControls = useMemo(
() =>
@ -286,6 +319,18 @@ export const DiscoverGrid = ({
[usedSelectedDocs, isFilterActive, rows, setIsFilterActive]
);
if (!rowCount && isLoading) {
return (
<div className="euiDataGrid__loading">
<EuiText size="xs" color="subdued">
<EuiLoadingSpinner />
<EuiSpacer size="s" />
<FormattedMessage id="discover.loadingResults" defaultMessage="Loading results" />
</EuiText>
</div>
);
}
if (!rowCount) {
return (
<div className="euiDataGrid__noResults">
@ -348,10 +393,12 @@ export const DiscoverGrid = ({
? {
...toolbarVisibility,
showColumnSelector: false,
showSortSelector: isSortEnabled,
additionalControls,
}
: {
...toolbarVisibility,
showSortSelector: isSortEnabled,
additionalControls,
}
}

View file

@ -12,7 +12,14 @@ import { indexPatternWithTimefieldMock } from '../../../__mocks__/index_pattern_
describe('Discover grid columns ', function () {
it('returns eui grid columns without time column', async () => {
const actual = getEuiGridColumns(['extension', 'message'], {}, indexPatternMock, false, false);
const actual = getEuiGridColumns(
['extension', 'message'],
{},
indexPatternMock,
false,
false,
true
);
expect(actual).toMatchInlineSnapshot(`
Array [
Object {
@ -54,6 +61,7 @@ describe('Discover grid columns ', function () {
{},
indexPatternWithTimefieldMock,
false,
true,
true
);
expect(actual).toMatchInlineSnapshot(`
@ -94,7 +102,8 @@ describe('Discover grid columns ', function () {
{},
indexPatternWithTimefieldMock,
true,
false
false,
true
);
expect(actual).toMatchInlineSnapshot(`
Array [

View file

@ -53,7 +53,8 @@ export function buildEuiGridColumn(
columnName: string,
columnWidth: number | undefined = 0,
indexPattern: IndexPattern,
defaultColumns: boolean
defaultColumns: boolean,
isSortEnabled: boolean
) {
const timeString = i18n.translate('discover.timeLabel', {
defaultMessage: 'Time',
@ -62,7 +63,7 @@ export function buildEuiGridColumn(
const column: EuiDataGridColumn = {
id: columnName,
schema: getSchemaByKbnType(indexPatternField?.type),
isSortable: indexPatternField?.sortable === true,
isSortable: isSortEnabled && indexPatternField?.sortable === true,
display:
columnName === '_source'
? i18n.translate('discover.grid.documentHeader', {
@ -100,7 +101,8 @@ export function getEuiGridColumns(
settings: DiscoverGridSettings | undefined,
indexPattern: IndexPattern,
showTimeCol: boolean,
defaultColumns: boolean
defaultColumns: boolean,
isSortEnabled: boolean
) {
const timeFieldName = indexPattern.timeFieldName;
const getColWidth = (column: string) => settings?.columns?.[column]?.width ?? 0;
@ -108,12 +110,12 @@ export function getEuiGridColumns(
if (showTimeCol && indexPattern.timeFieldName && !columns.find((col) => col === timeFieldName)) {
const usedColumns = [indexPattern.timeFieldName, ...columns];
return usedColumns.map((column) =>
buildEuiGridColumn(column, getColWidth(column), indexPattern, defaultColumns)
buildEuiGridColumn(column, getColWidth(column), indexPattern, defaultColumns, isSortEnabled)
);
}
return columns.map((column) =>
buildEuiGridColumn(column, getColWidth(column), indexPattern, defaultColumns)
buildEuiGridColumn(column, getColWidth(column), indexPattern, defaultColumns, isSortEnabled)
);
}

View file

@ -51,7 +51,14 @@ describe('document selection', () => {
const component = mountWithIntl(
<DiscoverGridContext.Provider value={contextMock}>
<SelectButton rowIndex={0} />
<SelectButton
rowIndex={0}
setCellProps={jest.fn()}
columnId="test"
isExpanded={false}
isDetails={false}
isExpandable={false}
/>
</DiscoverGridContext.Provider>
);
@ -73,7 +80,14 @@ describe('document selection', () => {
const component = mountWithIntl(
<DiscoverGridContext.Provider value={contextMock}>
<SelectButton rowIndex={0} />
<SelectButton
rowIndex={0}
setCellProps={jest.fn()}
columnId="test"
isExpanded={false}
isDetails={false}
isExpandable={false}
/>
</DiscoverGridContext.Provider>
);
@ -95,7 +109,14 @@ describe('document selection', () => {
const component = mountWithIntl(
<DiscoverGridContext.Provider value={contextMock}>
<SelectButton rowIndex={0} />
<SelectButton
rowIndex={0}
setCellProps={jest.fn()}
columnId="test"
isExpanded={false}
isDetails={false}
isExpandable={false}
/>
</DiscoverGridContext.Provider>
);
@ -117,7 +138,14 @@ describe('document selection', () => {
const component = mountWithIntl(
<DiscoverGridContext.Provider value={contextMock}>
<SelectButton rowIndex={0} />
<SelectButton
rowIndex={0}
setCellProps={jest.fn()}
columnId="test"
isExpanded={false}
isDetails={false}
isExpandable={false}
/>
</DiscoverGridContext.Provider>
);

View file

@ -5,7 +5,8 @@
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import React, { useCallback, useState, useContext, useMemo } from 'react';
import React, { useCallback, useState, useContext, useMemo, useEffect } from 'react';
import classNames from 'classnames';
import {
EuiButtonEmpty,
EuiContextMenuItem,
@ -13,9 +14,11 @@ import {
EuiCopy,
EuiPopover,
EuiCheckbox,
EuiDataGridCellValueElementProps,
} from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n/react';
import classNames from 'classnames';
import themeDark from '@elastic/eui/dist/eui_theme_dark.json';
import themeLight from '@elastic/eui/dist/eui_theme_light.json';
import { ElasticSearchHit } from '../../doc_views/doc_views_types';
import { DiscoverGridContext } from './discover_grid_context';
@ -27,11 +30,25 @@ export const getDocId = (doc: ElasticSearchHit & { _routing?: string }) => {
const routing = doc._routing ? doc._routing : '';
return [doc._index, doc._id, routing].join('::');
};
export const SelectButton = ({ rowIndex }: { rowIndex: number }) => {
const ctx = useContext(DiscoverGridContext);
const doc = useMemo(() => ctx.rows[rowIndex], [ctx.rows, rowIndex]);
export const SelectButton = ({ rowIndex, setCellProps }: EuiDataGridCellValueElementProps) => {
const { selectedDocs, expanded, rows, isDarkMode, setSelectedDocs } = useContext(
DiscoverGridContext
);
const doc = useMemo(() => rows[rowIndex], [rows, rowIndex]);
const id = useMemo(() => getDocId(doc), [doc]);
const checked = useMemo(() => ctx.selectedDocs.includes(id), [ctx.selectedDocs, id]);
const checked = useMemo(() => selectedDocs.includes(id), [selectedDocs, id]);
useEffect(() => {
if (expanded && doc && expanded._id === doc._id) {
setCellProps({
style: {
backgroundColor: isDarkMode ? themeDark.euiColorHighlight : themeLight.euiColorHighlight,
},
});
} else {
setCellProps({ style: undefined });
}
}, [expanded, doc, setCellProps, isDarkMode]);
return (
<EuiCheckbox
@ -41,10 +58,10 @@ export const SelectButton = ({ rowIndex }: { rowIndex: number }) => {
data-test-subj={`dscGridSelectDoc-${id}`}
onChange={() => {
if (checked) {
const newSelection = ctx.selectedDocs.filter((docId) => docId !== id);
ctx.setSelectedDocs(newSelection);
const newSelection = selectedDocs.filter((docId) => docId !== id);
setSelectedDocs(newSelection);
} else {
ctx.setSelectedDocs([...ctx.selectedDocs, id]);
setSelectedDocs([...selectedDocs, id]);
}
}}
/>

View file

@ -12,6 +12,7 @@ import themeDark from '@elastic/eui/dist/eui_theme_dark.json';
import themeLight from '@elastic/eui/dist/eui_theme_light.json';
import { i18n } from '@kbn/i18n';
import { DiscoverGridContext } from './discover_grid_context';
import { EsHitRecord } from '../../angular/context/api/context';
/**
* Button to expand a given row
*/
@ -19,7 +20,11 @@ export const ExpandButton = ({ rowIndex, setCellProps }: EuiDataGridCellValueEle
const { expanded, setExpanded, rows, isDarkMode } = useContext(DiscoverGridContext);
const current = rows[rowIndex];
useEffect(() => {
if (expanded && current && expanded._id === current._id) {
if ((current as EsHitRecord).isAnchor) {
setCellProps({
className: 'dscDocsGrid__cell--highlight',
});
} else if (expanded && current && expanded._id === current._id) {
setCellProps({
style: {
backgroundColor: isDarkMode ? themeDark.euiColorHighlight : themeLight.euiColorHighlight,

View file

@ -21,6 +21,7 @@ import { ElasticSearchHit } from '../../doc_views/doc_views_types';
import { DiscoverGridContext } from './discover_grid_context';
import { JsonCodeEditor } from '../json_code_editor/json_code_editor';
import { defaultMonacoEditorWidth } from './constants';
import { EsHitRecord } from '../../angular/context/api/context';
export const getRenderCellValueFn = (
indexPattern: IndexPattern,
@ -38,7 +39,11 @@ export const getRenderCellValueFn = (
const ctx = useContext(DiscoverGridContext);
useEffect(() => {
if (ctx.expanded && row && ctx.expanded._id === row._id) {
if ((row as EsHitRecord).isAnchor) {
setCellProps({
className: 'dscDocsGrid__cell--highlight',
});
} else if (ctx.expanded && row && ctx.expanded._id === row._id) {
setCellProps({
style: {
backgroundColor: ctx.isDarkMode

View file

@ -33,6 +33,7 @@ import { getServices, IndexPattern, ISearchSource } from '../../kibana_services'
import { SEARCH_EMBEDDABLE_TYPE } from './constants';
import { SavedSearch } from '../..';
import {
DOC_HIDE_TIME_COLUMN_SETTING,
SAMPLE_SIZE_SETTING,
SEARCH_FIELDS_FROM_SOURCE,
SORT_DEFAULT_ORDER_SETTING,
@ -256,7 +257,7 @@ export class SearchEmbeddable
if (this.savedSearch.grid) {
searchScope.settings = this.savedSearch.grid;
}
searchScope.showTimeCol = !this.services.uiSettings.get('doc_table:hideTimeColumn', false);
searchScope.showTimeCol = !this.services.uiSettings.get(DOC_HIDE_TIME_COLUMN_SETTING, false);
searchScope.filter = async (field, value, operator) => {
let filters = esFilters.generateFilters(

View file

@ -0,0 +1,67 @@
/*
* 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 { renderHook } from '@testing-library/react-hooks';
import { useDataGridColumns } from './use_data_grid_columns';
import { indexPatternMock } from '../../__mocks__/index_pattern';
import { configMock } from '../../__mocks__/config';
import { indexPatternsMock } from '../../__mocks__/index_patterns';
import { AppState } from '../angular/context_state';
import { Capabilities } from '../../../../../core/types';
describe('useDataGridColumns', () => {
const defaultProps = {
capabilities: ({ discover: { save: true } } as unknown) as Capabilities,
config: configMock,
indexPattern: indexPatternMock,
indexPatterns: indexPatternsMock,
setAppState: () => {},
state: {
columns: ['Time', 'message'],
} as AppState,
useNewFieldsApi: false,
};
test('should return valid result', () => {
const { result } = renderHook(() => {
return useDataGridColumns(defaultProps);
});
expect(result.current.columns).toEqual(['Time', 'message']);
expect(result.current.onAddColumn).toBeInstanceOf(Function);
expect(result.current.onRemoveColumn).toBeInstanceOf(Function);
expect(result.current.onMoveColumn).toBeInstanceOf(Function);
expect(result.current.onSetColumns).toBeInstanceOf(Function);
});
test('should skip _source column when useNewFieldsApi is set to true', () => {
const { result } = renderHook(() => {
return useDataGridColumns({
...defaultProps,
state: {
columns: ['Time', '_source'],
},
useNewFieldsApi: true,
});
});
expect(result.current.columns).toEqual(['Time']);
});
test('should return empty columns array', () => {
const { result } = renderHook(() => {
return useDataGridColumns({
...defaultProps,
state: {
columns: [],
},
});
});
expect(result.current.columns).toEqual([]);
});
});

View file

@ -0,0 +1,70 @@
/*
* 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 { useMemo } from 'react';
import { Capabilities, IUiSettingsClient } from 'kibana/public';
import { IndexPattern, IndexPatternsContract } from '../../kibana_services';
import {
AppState as DiscoverState,
GetStateReturn as DiscoverGetStateReturn,
} from '../angular/discover_state';
import {
AppState as ContextState,
GetStateReturn as ContextGetStateReturn,
} from '../angular/context_state';
import { getStateColumnActions } from '../angular/doc_table/actions/columns';
interface UseDataGridColumnsProps {
capabilities: Capabilities;
config: IUiSettingsClient;
indexPattern: IndexPattern;
indexPatterns: IndexPatternsContract;
useNewFieldsApi: boolean;
setAppState: DiscoverGetStateReturn['setAppState'] | ContextGetStateReturn['setAppState'];
state: DiscoverState | ContextState;
}
export const useDataGridColumns = ({
capabilities,
config,
indexPattern,
indexPatterns,
setAppState,
state,
useNewFieldsApi,
}: UseDataGridColumnsProps) => {
const { onAddColumn, onRemoveColumn, onSetColumns, onMoveColumn } = useMemo(
() =>
getStateColumnActions({
capabilities,
config,
indexPattern,
indexPatterns,
setAppState,
state,
useNewFieldsApi,
}),
[capabilities, config, indexPattern, indexPatterns, setAppState, state, useNewFieldsApi]
);
const columns = useMemo(() => {
if (!state.columns) {
return [];
}
return useNewFieldsApi ? state.columns.filter((col) => col !== '_source') : state.columns;
}, [state, useNewFieldsApi]);
return {
columns,
onAddColumn,
onRemoveColumn,
onMoveColumn,
onSetColumns,
};
};

View file

@ -19,7 +19,6 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
const retry = getService('retry');
const filterBar = getService('filterBar');
const dataGrid = getService('dataGrid');
const docTable = getService('docTable');
const PageObjects = getPageObjects([
'common',
'discover',
@ -67,16 +66,17 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
await dataGrid.clickRowToggle({ rowIndex: 0 });
const rowActions = await dataGrid.getRowActions({ rowIndex: 0 });
await rowActions[1].click();
// entering the context view (contains the legacy type)
const contextFields = await docTable.getFields();
const contextFields = await dataGrid.getFields();
const anchorTimestamp = contextFields[0][0];
return anchorTimestamp === firstTimestamp;
});
});
it('should open the context view with the same columns', async () => {
const columnNames = await docTable.getHeaderFields();
expect(columnNames).to.eql(['Time', ...TEST_COLUMN_NAMES]);
const columnNames = await dataGrid.getHeaderFields();
expect(columnNames).to.eql(['Time (@timestamp)', ...TEST_COLUMN_NAMES]);
});
it('should open the context view with the filters disabled', async () => {
@ -111,7 +111,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
expect(await browser.getCurrentUrl()).to.contain('#/context');
await PageObjects.header.waitUntilLoadingHasFinished();
await retry.waitFor('document table has a length of 6', async () => {
const nrOfDocs = (await docTable.getBodyRows()).length;
const nrOfDocs = (await dataGrid.getBodyRows()).length;
return nrOfDocs === 6;
});
});

View file

@ -135,6 +135,7 @@ export class DataGridService extends FtrService {
if (!table) {
return [];
}
const cells = await table.findAllByCssSelector('.euiDataGridRowCell');
const rows: WebElementWrapper[][] = [];
@ -173,14 +174,13 @@ export class DataGridService extends FtrService {
}
public async getHeaderFields(): Promise<string[]> {
const result = await this.find.allByCssSelector('.euiDataGridHeaderCell__content');
const result = await this.find.allByCssSelector(
'.euiDataGridHeaderCell__button > .euiDataGridHeaderCell__content'
);
const textArr = [];
let idx = 0;
for (const cell of result) {
if (idx > 1) {
textArr.push(await cell.getVisibleText());
}
idx++;
textArr.push(await cell.getVisibleText());
}
return Promise.resolve(textArr);
}

View file

@ -1553,7 +1553,6 @@
"discover.bucketIntervalTooltip.tooLargeBucketsText": "大きすぎるバケット",
"discover.bucketIntervalTooltip.tooManyBucketsText": "バケットが多すぎます",
"discover.clearSelection": "選択した項目をクリア",
"discover.context.breadcrumb": "{indexPatternTitle}#{docId} のコンテキスト",
"discover.context.failedToLoadAnchorDocumentDescription": "アンカードキュメントの読み込みに失敗しました",
"discover.context.failedToLoadAnchorDocumentErrorDescription": "アンカードキュメントの読み込みに失敗しました。",
"discover.context.loadButtonLabel": "読み込み",

View file

@ -1562,7 +1562,6 @@
"discover.bucketIntervalTooltip.tooLargeBucketsText": "存储桶过大",
"discover.bucketIntervalTooltip.tooManyBucketsText": "存储桶过多",
"discover.clearSelection": "清除所选内容",
"discover.context.breadcrumb": "{indexPatternTitle}#{docId} 的上下文",
"discover.context.failedToLoadAnchorDocumentDescription": "无法加载定位点文档",
"discover.context.failedToLoadAnchorDocumentErrorDescription": "无法加载定位点文档。",
"discover.context.loadButtonLabel": "加载",