Co-authored-by: Michail Yasonik <michail.yasonik@elastic.co> Co-authored-by: Marta Bondyra <marta.bondyra@elastic.co> Co-authored-by: Dave Snider <dave.snider@gmail.com> Co-authored-by: Andrea Del Rio <delrio.andre@gmail.com> Co-authored-by: cchaos <caroline.horn@elastic.co> Co-authored-by: Matthias Wilhelm <matthias.wilhelm@elastic.co> Co-authored-by: Michail Yasonik <michail.yasonik@elastic.co> Co-authored-by: Marta Bondyra <marta.bondyra@elastic.co> Co-authored-by: Dave Snider <dave.snider@gmail.com> Co-authored-by: Andrea Del Rio <delrio.andre@gmail.com> Co-authored-by: cchaos <caroline.horn@elastic.co>
This commit is contained in:
parent
9f202aaf29
commit
755b042d87
|
@ -263,6 +263,10 @@ Hides the "Time" column in *Discover* and in all saved searches on dashboards.
|
|||
Highlights results in *Discover* and saved searches on dashboards. Highlighting
|
||||
slows requests when working on big documents.
|
||||
|
||||
[[doctable-legacy]]`doc_table:legacy`::
|
||||
Control the way the Discover's table looks and works. Set this property to `true` to revert to the legacy implementation.
|
||||
|
||||
|
||||
[float]
|
||||
[[kibana-ml-settings]]
|
||||
==== Machine learning
|
||||
|
|
|
@ -1,5 +1,19 @@
|
|||
@include euiHeaderAffordForFixed;
|
||||
|
||||
.euiDataGrid__restrictBody {
|
||||
.headerGlobalNav,
|
||||
.kbnQueryBar {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.euiDataGrid__restrictBody.euiBody--headerIsFixed {
|
||||
.euiFlyout {
|
||||
top: 0;
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.chrHeaderHelpMenu__version {
|
||||
text-transform: none;
|
||||
}
|
||||
|
|
|
@ -27,4 +27,5 @@ export const FIELDS_LIMIT_SETTING = 'fields:popularLimit';
|
|||
export const CONTEXT_DEFAULT_SIZE_SETTING = 'context:defaultSize';
|
||||
export const CONTEXT_STEP_SETTING = 'context:step';
|
||||
export const CONTEXT_TIE_BREAKER_FIELDS_SETTING = 'context:tieBreakerFields';
|
||||
export const DOC_TABLE_LEGACY = 'doc_table:legacy';
|
||||
export const MODIFY_COLUMNS_ON_SWITCH = 'discover:modifyColumnsOnSwitch';
|
||||
|
|
|
@ -22,29 +22,40 @@ import { IndexPattern } from '../../../data/common';
|
|||
import { indexPatterns } from '../../../data/public';
|
||||
|
||||
const fields = [
|
||||
{
|
||||
name: '_source',
|
||||
type: '_source',
|
||||
scripted: false,
|
||||
filterable: false,
|
||||
aggregatable: false,
|
||||
},
|
||||
{
|
||||
name: '_index',
|
||||
type: 'string',
|
||||
scripted: false,
|
||||
filterable: true,
|
||||
aggregatable: false,
|
||||
},
|
||||
{
|
||||
name: 'message',
|
||||
type: 'string',
|
||||
scripted: false,
|
||||
filterable: false,
|
||||
aggregatable: false,
|
||||
},
|
||||
{
|
||||
name: 'extension',
|
||||
type: 'string',
|
||||
scripted: false,
|
||||
filterable: true,
|
||||
aggregatable: true,
|
||||
},
|
||||
{
|
||||
name: 'bytes',
|
||||
type: 'number',
|
||||
scripted: false,
|
||||
filterable: true,
|
||||
aggregatable: true,
|
||||
},
|
||||
{
|
||||
name: 'scripted',
|
||||
|
@ -62,16 +73,21 @@ const indexPattern = ({
|
|||
id: 'the-index-pattern-id',
|
||||
title: 'the-index-pattern-title',
|
||||
metaFields: ['_index', '_score'],
|
||||
formatField: jest.fn(),
|
||||
flattenHit: undefined,
|
||||
formatHit: jest.fn((hit) => hit._source),
|
||||
fields,
|
||||
getComputedFields: () => ({}),
|
||||
getComputedFields: () => ({ docvalueFields: [], scriptFields: {}, storedFields: ['*'] }),
|
||||
getSourceFiltering: () => ({}),
|
||||
getFieldByName: () => ({}),
|
||||
timeFieldName: '',
|
||||
docvalueFields: [],
|
||||
} as unknown) as IndexPattern;
|
||||
|
||||
indexPattern.flattenHit = indexPatterns.flattenHitWrapper(indexPattern, indexPattern.metaFields);
|
||||
indexPattern.isTimeBased = () => !!indexPattern.timeFieldName;
|
||||
indexPattern.formatField = (hit: Record<string, any>, fieldName: string) => {
|
||||
return fieldName === '_source' ? hit._source : indexPattern.flattenHit(hit)[fieldName];
|
||||
};
|
||||
|
||||
export const indexPatternMock = indexPattern;
|
||||
|
|
|
@ -24,7 +24,6 @@ import moment from 'moment';
|
|||
import dateMath from '@elastic/datemath';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { createSearchSessionRestorationDataProvider, getState, splitState } from './discover_state';
|
||||
|
||||
import { RequestAdapter } from '../../../../inspector/public';
|
||||
import {
|
||||
connectToQueryState,
|
||||
|
@ -35,6 +34,7 @@ import {
|
|||
import { getSortArray } from './doc_table';
|
||||
import * as columnActions from './doc_table/actions/columns';
|
||||
import indexTemplateLegacy from './discover_legacy.html';
|
||||
import indexTemplateGrid from './discover_datagrid.html';
|
||||
import { addHelpMenuToAppChrome } from '../components/help_menu/help_menu_util';
|
||||
import { discoverResponseHandler } from './response_handler';
|
||||
import {
|
||||
|
@ -124,7 +124,9 @@ app.config(($routeProvider) => {
|
|||
};
|
||||
const discoverRoute = {
|
||||
...defaults,
|
||||
template: indexTemplateLegacy,
|
||||
template: getServices().uiSettings.get('doc_table:legacy', true)
|
||||
? indexTemplateLegacy
|
||||
: indexTemplateGrid,
|
||||
reloadOnSearch: false,
|
||||
resolve: {
|
||||
savedObjects: function ($route, Promise) {
|
||||
|
@ -340,6 +342,8 @@ function discoverController($element, $route, $scope, $timeout, Promise, uiCapab
|
|||
$scope.minimumVisibleRows = 50;
|
||||
$scope.fetchStatus = fetchStatuses.UNINITIALIZED;
|
||||
$scope.showSaveQuery = uiCapabilities.discover.saveQuery;
|
||||
$scope.showTimeCol =
|
||||
!config.get('doc_table:hideTimeColumn', false) && $scope.indexPattern.timeFieldName;
|
||||
|
||||
let abortController;
|
||||
$scope.$on('$destroy', () => {
|
||||
|
@ -414,7 +418,7 @@ function discoverController($element, $route, $scope, $timeout, Promise, uiCapab
|
|||
const query = $scope.searchSource.getField('query') || data.query.queryString.getDefaultQuery();
|
||||
const sort = getSortArray(savedSearch.sort, $scope.indexPattern);
|
||||
|
||||
return {
|
||||
const defaultState = {
|
||||
query,
|
||||
sort: !sort.length
|
||||
? getDefaultSort($scope.indexPattern, config.get(SORT_DEFAULT_ORDER_SETTING, 'desc'))
|
||||
|
@ -427,6 +431,11 @@ function discoverController($element, $route, $scope, $timeout, Promise, uiCapab
|
|||
interval: 'auto',
|
||||
filters: _.cloneDeep($scope.searchSource.getOwnField('filter')),
|
||||
};
|
||||
if (savedSearch.grid) {
|
||||
defaultState.grid = savedSearch.grid;
|
||||
}
|
||||
|
||||
return defaultState;
|
||||
}
|
||||
|
||||
$scope.state.index = $scope.indexPattern.id;
|
||||
|
@ -440,6 +449,8 @@ function discoverController($element, $route, $scope, $timeout, Promise, uiCapab
|
|||
indexPatternList: $route.current.locals.savedObjects.ip.list,
|
||||
config: config,
|
||||
setHeaderActionMenu: getHeaderActionMenuMounter(),
|
||||
filterManager,
|
||||
setAppState,
|
||||
data,
|
||||
};
|
||||
|
||||
|
@ -783,6 +794,17 @@ function discoverController($element, $route, $scope, $timeout, Promise, uiCapab
|
|||
const columns = columnActions.moveColumn($scope.state.columns, columnName, newIndex);
|
||||
setAppState({ columns });
|
||||
};
|
||||
|
||||
$scope.setColumns = function setColumns(columns) {
|
||||
// remove first element of columns if it's the configured timeFieldName, which is prepended automatically
|
||||
const actualColumns =
|
||||
$scope.indexPattern.timeFieldName && $scope.indexPattern.timeFieldName === columns[0]
|
||||
? columns.slice(1)
|
||||
: columns;
|
||||
$scope.state = { ...$scope.state, columns: actualColumns };
|
||||
setAppState({ columns: actualColumns });
|
||||
};
|
||||
|
||||
async function setupVisualization() {
|
||||
// If no timefield has been specified we don't create a histogram of messages
|
||||
if (!getTimeField()) return;
|
||||
|
|
|
@ -0,0 +1,31 @@
|
|||
<discover-app>
|
||||
<discover
|
||||
fetch="fetch"
|
||||
fetch-counter="fetchCounter"
|
||||
fetch-error="fetchError"
|
||||
field-counts="fieldCounts"
|
||||
histogram-data="histogramData"
|
||||
hits="hits"
|
||||
index-pattern="indexPattern"
|
||||
on-add-column="addColumn"
|
||||
on-add-filter="filterQuery"
|
||||
on-change-interval="changeInterval"
|
||||
on-remove-column="removeColumn"
|
||||
on-set-columns="setColumns"
|
||||
on-sort="setSortOrder"
|
||||
opts="opts"
|
||||
reset-query="resetQuery"
|
||||
result-state="resultState"
|
||||
rows="rows"
|
||||
search-source="searchSource"
|
||||
set-index-pattern="setIndexPattern"
|
||||
show-save-query="showSaveQuery"
|
||||
state="state"
|
||||
time-filter-update-handler="timefilterUpdateHandler"
|
||||
time-range="timeRange"
|
||||
top-nav-menu="topNavMenu"
|
||||
update-query="updateQuery"
|
||||
update-saved-query-id="updateSavedQueryId"
|
||||
>
|
||||
</discover>
|
||||
</discover-app>
|
|
@ -1,6 +1,5 @@
|
|||
<discover-app>
|
||||
<discover-legacy
|
||||
add-column="addColumn"
|
||||
fetch="fetch"
|
||||
fetch-counter="fetchCounter"
|
||||
fetch-error="fetchError"
|
||||
|
@ -9,6 +8,7 @@
|
|||
hits="hits"
|
||||
index-pattern="indexPattern"
|
||||
minimum-visible-rows="minimumVisibleRows"
|
||||
on-add-column="addColumn"
|
||||
on-add-filter="filterQuery"
|
||||
on-move-column="moveColumn"
|
||||
on-change-interval="changeInterval"
|
||||
|
@ -20,7 +20,6 @@
|
|||
reset-query="resetQuery"
|
||||
result-state="resultState"
|
||||
rows="rows"
|
||||
saved-search="savedSearch"
|
||||
search-source="searchSource"
|
||||
set-index-pattern="setIndexPattern"
|
||||
show-save-query="showSaveQuery"
|
||||
|
|
|
@ -36,6 +36,7 @@ import {
|
|||
SearchSessionInfoProvider,
|
||||
} from '../../../../data/public';
|
||||
import { migrateLegacyQuery } from '../helpers/migrate_legacy_query';
|
||||
import { DiscoverGridSettings } from '../components/discover_grid/types';
|
||||
import { DISCOVER_APP_URL_GENERATOR, DiscoverUrlGeneratorState } from '../../url_generator';
|
||||
|
||||
export interface AppState {
|
||||
|
@ -47,6 +48,10 @@ export interface AppState {
|
|||
* Array of applied filters
|
||||
*/
|
||||
filters?: Filter[];
|
||||
/**
|
||||
* Data Grid related state
|
||||
*/
|
||||
grid?: DiscoverGridSettings;
|
||||
/**
|
||||
* id of the used index pattern
|
||||
*/
|
||||
|
|
|
@ -0,0 +1,52 @@
|
|||
/*
|
||||
* Licensed to Elasticsearch B.V. under one or more contributor
|
||||
* license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright
|
||||
* ownership. Elasticsearch B.V. licenses this file to you under
|
||||
* the Apache License, Version 2.0 (the "License"); you may
|
||||
* not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
import { Discover } from './discover';
|
||||
|
||||
export function createDiscoverDirective(reactDirective: any) {
|
||||
return reactDirective(Discover, [
|
||||
['fetch', { watchDepth: 'reference' }],
|
||||
['fetchCounter', { watchDepth: 'reference' }],
|
||||
['fetchError', { watchDepth: 'reference' }],
|
||||
['fieldCounts', { watchDepth: 'reference' }],
|
||||
['histogramData', { watchDepth: 'reference' }],
|
||||
['hits', { watchDepth: 'reference' }],
|
||||
['indexPattern', { watchDepth: 'reference' }],
|
||||
['onAddColumn', { watchDepth: 'reference' }],
|
||||
['onAddFilter', { watchDepth: 'reference' }],
|
||||
['onChangeInterval', { watchDepth: 'reference' }],
|
||||
['onRemoveColumn', { watchDepth: 'reference' }],
|
||||
['onSetColumns', { watchDepth: 'reference' }],
|
||||
['onSort', { watchDepth: 'reference' }],
|
||||
['opts', { watchDepth: 'reference' }],
|
||||
['resetQuery', { watchDepth: 'reference' }],
|
||||
['resultState', { watchDepth: 'reference' }],
|
||||
['rows', { watchDepth: 'reference' }],
|
||||
['searchSource', { watchDepth: 'reference' }],
|
||||
['setColumns', { watchDepth: 'reference' }],
|
||||
['setIndexPattern', { watchDepth: 'reference' }],
|
||||
['showSaveQuery', { watchDepth: 'reference' }],
|
||||
['state', { watchDepth: 'reference' }],
|
||||
['timefilterUpdateHandler', { watchDepth: 'reference' }],
|
||||
['timeRange', { watchDepth: 'reference' }],
|
||||
['topNavMenu', { watchDepth: 'reference' }],
|
||||
['updateQuery', { watchDepth: 'reference' }],
|
||||
['updateSavedQueryId', { watchDepth: 'reference' }],
|
||||
]);
|
||||
}
|
|
@ -0,0 +1,56 @@
|
|||
/*
|
||||
* Licensed to Elasticsearch B.V. under one or more contributor
|
||||
* license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright
|
||||
* ownership. Elasticsearch B.V. licenses this file to you under
|
||||
* the Apache License, Version 2.0 (the "License"); you may
|
||||
* not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import * as React from 'react';
|
||||
import { I18nProvider } from '@kbn/i18n/react';
|
||||
import { DiscoverGrid, DiscoverGridProps } from './discover_grid/discover_grid';
|
||||
import { getServices } from '../../kibana_services';
|
||||
|
||||
export const DataGridMemoized = React.memo((props: DiscoverGridProps) => (
|
||||
<DiscoverGrid {...props} />
|
||||
));
|
||||
|
||||
export function DiscoverGridEmbeddable(props: DiscoverGridProps) {
|
||||
return (
|
||||
<I18nProvider>
|
||||
<DataGridMemoized {...props} services={getServices()} />
|
||||
</I18nProvider>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* this is just needed for the embeddable
|
||||
*/
|
||||
export function createDiscoverGridDirective(reactDirective: any) {
|
||||
return reactDirective(DiscoverGridEmbeddable, [
|
||||
['columns', { watchDepth: 'collection' }],
|
||||
['indexPattern', { watchDepth: 'reference' }],
|
||||
['onAddColumn', { watchDepth: 'reference', wrapApply: false }],
|
||||
['onFilter', { watchDepth: 'reference', wrapApply: false }],
|
||||
['onRemoveColumn', { watchDepth: 'reference', wrapApply: false }],
|
||||
['onSetColumns', { watchDepth: 'reference', wrapApply: false }],
|
||||
['onSort', { watchDepth: 'reference', wrapApply: false }],
|
||||
['rows', { watchDepth: 'collection' }],
|
||||
['sampleSize', { watchDepth: 'reference' }],
|
||||
['searchDescription', { watchDepth: 'reference' }],
|
||||
['searchTitle', { watchDepth: 'reference' }],
|
||||
['settings', { watchDepth: 'reference' }],
|
||||
['showTimeCol', { watchDepth: 'value' }],
|
||||
['sort', { watchDepth: 'value' }],
|
||||
]);
|
||||
}
|
|
@ -21,7 +21,6 @@ import { DiscoverLegacy } from './discover_legacy';
|
|||
|
||||
export function createDiscoverLegacyDirective(reactDirective: any) {
|
||||
return reactDirective(DiscoverLegacy, [
|
||||
['addColumn', { watchDepth: 'reference' }],
|
||||
['fetch', { watchDepth: 'reference' }],
|
||||
['fetchCounter', { watchDepth: 'reference' }],
|
||||
['fetchError', { watchDepth: 'reference' }],
|
||||
|
@ -30,6 +29,7 @@ export function createDiscoverLegacyDirective(reactDirective: any) {
|
|||
['hits', { watchDepth: 'reference' }],
|
||||
['indexPattern', { watchDepth: 'reference' }],
|
||||
['minimumVisibleRows', { watchDepth: 'reference' }],
|
||||
['onAddColumn', { watchDepth: 'reference' }],
|
||||
['onAddFilter', { watchDepth: 'reference' }],
|
||||
['onChangeInterval', { watchDepth: 'reference' }],
|
||||
['onMoveColumn', { watchDepth: 'reference' }],
|
||||
|
|
|
@ -35,6 +35,10 @@ discover-app {
|
|||
}
|
||||
}
|
||||
|
||||
.dscPageContent {
|
||||
border: $euiBorderThin;
|
||||
}
|
||||
|
||||
.dscPageContent,
|
||||
.dscPageContent__inner {
|
||||
height: 100%;
|
||||
|
@ -46,6 +50,7 @@ discover-app {
|
|||
|
||||
.dscResultCount {
|
||||
padding: $euiSizeS;
|
||||
min-height: $euiSize * 3;
|
||||
|
||||
@include euiBreakpoint('xs', 's') {
|
||||
.dscResultCount__toggle {
|
||||
|
@ -76,6 +81,13 @@ discover-app {
|
|||
padding: $euiSizeS;
|
||||
}
|
||||
|
||||
// new slimmer layout for data grid
|
||||
.dscHistogramGrid {
|
||||
display: flex;
|
||||
height: $euiSize * 8;
|
||||
padding: $euiSizeS $euiSizeS 0 $euiSizeS;
|
||||
}
|
||||
|
||||
.dscTable {
|
||||
// SASSTODO: add a monospace modifier to the doc-table component
|
||||
.kbnDocTable__row {
|
||||
|
|
321
src/plugins/discover/public/application/components/discover.tsx
Normal file
321
src/plugins/discover/public/application/components/discover.tsx
Normal file
|
@ -0,0 +1,321 @@
|
|||
/*
|
||||
* Licensed to Elasticsearch B.V. under one or more contributor
|
||||
* license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright
|
||||
* ownership. Elasticsearch B.V. licenses this file to you under
|
||||
* the Apache License, Version 2.0 (the "License"); you may
|
||||
* not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import './discover.scss';
|
||||
import React, { useState, useRef } from 'react';
|
||||
import {
|
||||
EuiButtonEmpty,
|
||||
EuiButtonIcon,
|
||||
EuiFlexGroup,
|
||||
EuiFlexItem,
|
||||
EuiHideFor,
|
||||
EuiHorizontalRule,
|
||||
EuiPage,
|
||||
EuiPageBody,
|
||||
EuiPageContent,
|
||||
EuiSpacer,
|
||||
} from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { FormattedMessage, I18nProvider } from '@kbn/i18n/react';
|
||||
import classNames from 'classnames';
|
||||
import { HitsCounter } from './hits_counter';
|
||||
import { TimechartHeader } from './timechart_header';
|
||||
import { getServices } from '../../kibana_services';
|
||||
import { DiscoverUninitialized, DiscoverHistogram } from '../angular/directives';
|
||||
import { DiscoverNoResults } from './no_results';
|
||||
import { LoadingSpinner } from './loading_spinner/loading_spinner';
|
||||
import { search } from '../../../../data/public';
|
||||
import {
|
||||
DiscoverSidebarResponsive,
|
||||
DiscoverSidebarResponsiveProps,
|
||||
} from './sidebar/discover_sidebar_responsive';
|
||||
import { DiscoverProps } from './discover_legacy';
|
||||
import { SortPairArr } from '../angular/doc_table/lib/get_sort';
|
||||
import { DiscoverGrid, DiscoverGridProps } from './discover_grid/discover_grid';
|
||||
|
||||
export const SidebarMemoized = React.memo((props: DiscoverSidebarResponsiveProps) => (
|
||||
<DiscoverSidebarResponsive {...props} />
|
||||
));
|
||||
|
||||
export const DataGridMemoized = React.memo((props: DiscoverGridProps) => (
|
||||
<DiscoverGrid {...props} />
|
||||
));
|
||||
|
||||
export function Discover({
|
||||
fetch,
|
||||
fetchCounter,
|
||||
fetchError,
|
||||
fieldCounts,
|
||||
histogramData,
|
||||
hits,
|
||||
indexPattern,
|
||||
onAddColumn,
|
||||
onAddFilter,
|
||||
onChangeInterval,
|
||||
onRemoveColumn,
|
||||
onSetColumns,
|
||||
onSort,
|
||||
opts,
|
||||
resetQuery,
|
||||
resultState,
|
||||
rows,
|
||||
searchSource,
|
||||
setIndexPattern,
|
||||
showSaveQuery,
|
||||
state,
|
||||
timefilterUpdateHandler,
|
||||
timeRange,
|
||||
topNavMenu,
|
||||
updateQuery,
|
||||
updateSavedQueryId,
|
||||
}: DiscoverProps) {
|
||||
const scrollableDesktop = useRef<HTMLDivElement>(null);
|
||||
const collapseIcon = useRef<HTMLButtonElement>(null);
|
||||
const [toggleOn, toggleChart] = useState(true);
|
||||
const [isSidebarClosed, setIsSidebarClosed] = useState(false);
|
||||
const services = getServices();
|
||||
const { TopNavMenu } = services.navigation.ui;
|
||||
const { trackUiMetric } = services;
|
||||
const { savedSearch, indexPatternList, config } = opts;
|
||||
const bucketAggConfig = opts.chartAggConfigs?.aggs[1];
|
||||
const bucketInterval =
|
||||
bucketAggConfig && search.aggs.isDateHistogramBucketAggConfig(bucketAggConfig)
|
||||
? bucketAggConfig.buckets?.getInterval()
|
||||
: undefined;
|
||||
const contentCentered = resultState === 'uninitialized';
|
||||
const showTimeCol = !config.get('doc_table:hideTimeColumn', false) && indexPattern.timeFieldName;
|
||||
const columns =
|
||||
state.columns &&
|
||||
state.columns.length > 0 &&
|
||||
// check if all columns where removed except the configured timeField (this can't be removed)
|
||||
!(state.columns.length === 1 && state.columns[0] === indexPattern.timeFieldName)
|
||||
? state.columns
|
||||
: ['_source'];
|
||||
// if columns include _source this is considered as default view, so you can't remove columns
|
||||
// until you add a column using Discover's sidebar
|
||||
const defaultColumns = columns.includes('_source');
|
||||
|
||||
return (
|
||||
<I18nProvider>
|
||||
<EuiPage className="dscPage" data-fetch-counter={fetchCounter}>
|
||||
<TopNavMenu
|
||||
appName="discover"
|
||||
config={topNavMenu}
|
||||
indexPatterns={[indexPattern]}
|
||||
onQuerySubmit={updateQuery}
|
||||
onSavedQueryIdChange={updateSavedQueryId}
|
||||
query={state.query}
|
||||
setMenuMountPoint={opts.setHeaderActionMenu}
|
||||
savedQueryId={state.savedQuery}
|
||||
screenTitle={savedSearch.title}
|
||||
showDatePicker={indexPattern.isTimeBased()}
|
||||
showSaveQuery={showSaveQuery}
|
||||
showSearchBar={true}
|
||||
useDefaultBehaviors={true}
|
||||
/>
|
||||
<EuiPageBody className="dscPageBody" aria-describedby="savedSearchTitle">
|
||||
<h1 id="savedSearchTitle" className="euiScreenReaderOnly">
|
||||
{savedSearch.title}
|
||||
</h1>
|
||||
<EuiFlexGroup className="dscPageBody__contents" gutterSize="none">
|
||||
<EuiFlexItem grow={false}>
|
||||
<SidebarMemoized
|
||||
columns={state.columns || []}
|
||||
fieldCounts={fieldCounts}
|
||||
hits={rows}
|
||||
indexPatternList={indexPatternList}
|
||||
onAddField={onAddColumn}
|
||||
onAddFilter={onAddFilter}
|
||||
onRemoveField={onRemoveColumn}
|
||||
selectedIndexPattern={searchSource && searchSource.getField('index')}
|
||||
services={services}
|
||||
setIndexPattern={setIndexPattern}
|
||||
isClosed={isSidebarClosed}
|
||||
trackUiMetric={trackUiMetric}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
<EuiHideFor sizes={['xs', 's']}>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiButtonIcon
|
||||
iconType={isSidebarClosed ? 'menuRight' : 'menuLeft'}
|
||||
iconSize="m"
|
||||
size="s"
|
||||
onClick={() => setIsSidebarClosed(!isSidebarClosed)}
|
||||
data-test-subj="collapseSideBarButton"
|
||||
aria-controls="discover-sidebar"
|
||||
aria-expanded={isSidebarClosed ? 'false' : 'true'}
|
||||
aria-label={i18n.translate('discover.toggleSidebarAriaLabel', {
|
||||
defaultMessage: 'Toggle sidebar',
|
||||
})}
|
||||
buttonRef={collapseIcon}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
</EuiHideFor>
|
||||
<EuiFlexItem className="dscPageContent__wrapper">
|
||||
<EuiPageContent
|
||||
verticalPosition={contentCentered ? 'center' : undefined}
|
||||
horizontalPosition={contentCentered ? 'center' : undefined}
|
||||
paddingSize="none"
|
||||
className={classNames('dscPageContent', {
|
||||
'dscPageContent--centered': contentCentered,
|
||||
})}
|
||||
>
|
||||
{resultState === 'none' && (
|
||||
<DiscoverNoResults
|
||||
timeFieldName={opts.timefield}
|
||||
queryLanguage={state.query?.language || ''}
|
||||
data={opts.data}
|
||||
error={fetchError}
|
||||
/>
|
||||
)}
|
||||
{resultState === 'uninitialized' && <DiscoverUninitialized onRefresh={fetch} />}
|
||||
{resultState === 'loading' && <LoadingSpinner />}
|
||||
{resultState === 'ready' && (
|
||||
<EuiFlexGroup
|
||||
className="dscPageContent__inner"
|
||||
direction="column"
|
||||
alignItems="stretch"
|
||||
gutterSize="none"
|
||||
responsive={false}
|
||||
>
|
||||
<EuiFlexItem grow={false} className="dscResultCount">
|
||||
<EuiFlexGroup alignItems="center" justifyContent="spaceBetween">
|
||||
<EuiFlexItem
|
||||
grow={false}
|
||||
className="dscResuntCount__title eui-textTruncate eui-textNoWrap"
|
||||
>
|
||||
<HitsCounter
|
||||
hits={hits > 0 ? hits : 0}
|
||||
showResetButton={!!(savedSearch && savedSearch.id)}
|
||||
onResetQuery={resetQuery}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
{toggleOn && (
|
||||
<EuiFlexItem className="dscResultCount__actions">
|
||||
<TimechartHeader
|
||||
dateFormat={opts.config.get('dateFormat')}
|
||||
timeRange={timeRange}
|
||||
options={search.aggs.intervalOptions}
|
||||
onChangeInterval={onChangeInterval}
|
||||
stateInterval={state.interval || ''}
|
||||
bucketInterval={bucketInterval}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
)}
|
||||
<EuiFlexItem className="dscResultCount__toggle" grow={false}>
|
||||
<EuiButtonEmpty
|
||||
size="xs"
|
||||
iconType={toggleOn ? 'eyeClosed' : 'eye'}
|
||||
onClick={() => {
|
||||
toggleChart(!toggleOn);
|
||||
}}
|
||||
>
|
||||
{toggleOn
|
||||
? i18n.translate('discover.hideChart', {
|
||||
defaultMessage: 'Hide chart',
|
||||
})
|
||||
: i18n.translate('discover.showChart', {
|
||||
defaultMessage: 'Show chart',
|
||||
})}
|
||||
</EuiButtonEmpty>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiFlexItem>
|
||||
{toggleOn && opts.timefield && (
|
||||
<EuiFlexItem grow={false}>
|
||||
<section
|
||||
aria-label={i18n.translate(
|
||||
'discover.histogramOfFoundDocumentsAriaLabel',
|
||||
{
|
||||
defaultMessage: 'Histogram of found documents',
|
||||
}
|
||||
)}
|
||||
className="dscTimechart"
|
||||
>
|
||||
{opts.chartAggConfigs && histogramData && rows.length !== 0 && (
|
||||
<div className="dscHistogramGrid" data-test-subj="discoverChart">
|
||||
<DiscoverHistogram
|
||||
chartData={histogramData}
|
||||
timefilterUpdateHandler={timefilterUpdateHandler}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</section>
|
||||
<EuiSpacer size="s" />
|
||||
</EuiFlexItem>
|
||||
)}
|
||||
|
||||
<EuiHorizontalRule margin="none" />
|
||||
|
||||
<EuiFlexItem className="eui-yScroll">
|
||||
<section
|
||||
className="dscTable eui-yScroll"
|
||||
aria-labelledby="documentsAriaLabel"
|
||||
ref={scrollableDesktop}
|
||||
tabIndex={-1}
|
||||
>
|
||||
<h2 className="euiScreenReaderOnly" id="documentsAriaLabel">
|
||||
<FormattedMessage
|
||||
id="discover.documentsAriaLabel"
|
||||
defaultMessage="Documents"
|
||||
/>
|
||||
</h2>
|
||||
{rows && rows.length && (
|
||||
<div className="dscDiscoverGrid">
|
||||
<DataGridMemoized
|
||||
ariaLabelledBy="documentsAriaLabel"
|
||||
columns={columns}
|
||||
defaultColumns={defaultColumns}
|
||||
indexPattern={indexPattern}
|
||||
rows={rows}
|
||||
sort={(state.sort as SortPairArr[]) || []}
|
||||
sampleSize={opts.sampleSize}
|
||||
searchDescription={opts.savedSearch.description}
|
||||
searchTitle={opts.savedSearch.lastSavedTitle}
|
||||
showTimeCol={Boolean(showTimeCol)}
|
||||
services={services}
|
||||
settings={state.grid}
|
||||
onAddColumn={onAddColumn}
|
||||
onFilter={onAddFilter}
|
||||
onRemoveColumn={onRemoveColumn}
|
||||
onSetColumns={onSetColumns}
|
||||
onSort={onSort}
|
||||
onResize={(colSettings: { columnId: string; width: number }) => {
|
||||
const grid = { ...state.grid } || {};
|
||||
const newColumns = { ...grid.columns } || {};
|
||||
newColumns[colSettings.columnId] = {
|
||||
width: colSettings.width,
|
||||
};
|
||||
const newGrid = { ...grid, columns: newColumns };
|
||||
opts.setAppState({ grid: newGrid });
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</section>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
)}
|
||||
</EuiPageContent>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiPageBody>
|
||||
</EuiPage>
|
||||
</I18nProvider>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,38 @@
|
|||
/*
|
||||
* Licensed to Elasticsearch B.V. under one or more contributor
|
||||
* license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright
|
||||
* ownership. Elasticsearch B.V. licenses this file to you under
|
||||
* the Apache License, Version 2.0 (the "License"); you may
|
||||
* not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
// data types
|
||||
export const kibanaJSON = 'kibana-json';
|
||||
export const geoPoint = 'geo-point';
|
||||
export const unknownType = 'unknown';
|
||||
export const gridStyle = {
|
||||
border: 'all',
|
||||
fontSize: 's',
|
||||
cellPadding: 's',
|
||||
rowHover: 'none',
|
||||
};
|
||||
|
||||
export const pageSizeArr = [25, 50, 100];
|
||||
export const defaultPageSize = 25;
|
||||
export const toolbarVisibility = {
|
||||
showColumnSelector: {
|
||||
allowHide: false,
|
||||
allowReorder: true,
|
||||
},
|
||||
showStyleSelector: false,
|
||||
};
|
|
@ -0,0 +1,68 @@
|
|||
.dscDiscoverGrid {
|
||||
width: 100%;
|
||||
max-width: 100%;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
|
||||
.euiDataGrid__controls {
|
||||
border: none;
|
||||
border-bottom: $euiBorderThin;
|
||||
}
|
||||
|
||||
.euiDataGridRowCell:first-of-type,
|
||||
.euiDataGrid--headerShade.euiDataGrid--bordersAll .euiDataGridHeaderCell:first-of-type {
|
||||
border-left: none;
|
||||
border-right: none;
|
||||
}
|
||||
|
||||
.euiDataGridRowCell:last-of-type,
|
||||
.euiDataGridHeaderCell:last-of-type {
|
||||
border-right: none;
|
||||
}
|
||||
}
|
||||
|
||||
.dscDiscoverGrid__footer {
|
||||
background-color: $euiColorLightShade;
|
||||
padding: $euiSize / 2 $euiSize;
|
||||
margin-top: $euiSize / 4;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.dscTable__flyoutHeader {
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
// We only truncate if the cell is not a control column.
|
||||
.euiDataGridHeader {
|
||||
.euiDataGridHeaderCell__content {
|
||||
@include euiTextTruncate;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
.euiDataGridHeaderCell__popover {
|
||||
flex-grow: 0;
|
||||
flex-basis: auto;
|
||||
width: auto;
|
||||
padding-left: $euiSizeXS;
|
||||
}
|
||||
}
|
||||
|
||||
.euiDataGridRowCell--numeric {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.euiDataGrid__noResults {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
flex: 1 0 100%;
|
||||
text-align: center;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.dscFormatSource {
|
||||
@include euiTextTruncate;
|
||||
}
|
|
@ -0,0 +1,336 @@
|
|||
/*
|
||||
* Licensed to Elasticsearch B.V. under one or more contributor
|
||||
* license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright
|
||||
* ownership. Elasticsearch B.V. licenses this file to you under
|
||||
* the Apache License, Version 2.0 (the "License"); you may
|
||||
* not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import React, { useCallback, useMemo, useState } from 'react';
|
||||
import { FormattedMessage } from '@kbn/i18n/react';
|
||||
import './discover_grid.scss';
|
||||
import {
|
||||
EuiDataGridSorting,
|
||||
EuiDataGridStyle,
|
||||
EuiDataGridProps,
|
||||
EuiDataGrid,
|
||||
EuiIcon,
|
||||
EuiScreenReaderOnly,
|
||||
EuiSpacer,
|
||||
EuiText,
|
||||
htmlIdGenerator,
|
||||
} from '@elastic/eui';
|
||||
import { IndexPattern } from '../../../kibana_services';
|
||||
import { DocViewFilterFn, ElasticSearchHit } from '../../doc_views/doc_views_types';
|
||||
import { getPopoverContents, getSchemaDetectors } from './discover_grid_schema';
|
||||
import { DiscoverGridFlyout } from './discover_grid_flyout';
|
||||
import { DiscoverGridContext } from './discover_grid_context';
|
||||
import { getRenderCellValueFn } from './get_render_cell_value';
|
||||
import { DiscoverGridSettings } from './types';
|
||||
import { SortPairArr } from '../../angular/doc_table/lib/get_sort';
|
||||
import {
|
||||
getEuiGridColumns,
|
||||
getLeadControlColumns,
|
||||
getVisibleColumns,
|
||||
} from './discover_grid_columns';
|
||||
import { defaultPageSize, gridStyle, pageSizeArr, toolbarVisibility } from './constants';
|
||||
import { DiscoverServices } from '../../../build_services';
|
||||
|
||||
interface SortObj {
|
||||
id: string;
|
||||
direction: string;
|
||||
}
|
||||
|
||||
export interface DiscoverGridProps {
|
||||
/**
|
||||
* Determines which element labels the grid for ARIA
|
||||
*/
|
||||
ariaLabelledBy: string;
|
||||
/**
|
||||
* Determines which columns are displayed
|
||||
*/
|
||||
columns: string[];
|
||||
/**
|
||||
* Determines whether the given columns are the default ones, so parts of the document
|
||||
* are displayed (_source) with limited actions (cannor move, remove columns)
|
||||
* Implemented for matching with legacy behavior
|
||||
*/
|
||||
defaultColumns: boolean;
|
||||
/**
|
||||
* The used index pattern
|
||||
*/
|
||||
indexPattern: IndexPattern;
|
||||
/**
|
||||
* Function used to add a column in the document flyout
|
||||
*/
|
||||
onAddColumn: (column: string) => void;
|
||||
/**
|
||||
* Function to add a filter in the grid cell or document flyout
|
||||
*/
|
||||
onFilter: DocViewFilterFn;
|
||||
/**
|
||||
* Function used in the grid header and flyout to remove a column
|
||||
* @param column
|
||||
*/
|
||||
onRemoveColumn: (column: string) => void;
|
||||
/**
|
||||
* Function triggered when a column is resized by the user
|
||||
*/
|
||||
onResize?: (colSettings: { columnId: string; width: number }) => void;
|
||||
/**
|
||||
* Function to set all columns
|
||||
*/
|
||||
onSetColumns: (columns: string[]) => void;
|
||||
/**
|
||||
* function to change sorting of the documents
|
||||
*/
|
||||
onSort: (sort: string[][]) => void;
|
||||
/**
|
||||
* Array of documents provided by Elasticsearch
|
||||
*/
|
||||
rows?: ElasticSearchHit[];
|
||||
/**
|
||||
* The max size of the documents returned by Elasticsearch
|
||||
*/
|
||||
sampleSize: number;
|
||||
/**
|
||||
* Grid display settings persisted in Elasticsearch (e.g. column width)
|
||||
*/
|
||||
settings?: DiscoverGridSettings;
|
||||
/**
|
||||
* Saved search description
|
||||
*/
|
||||
searchDescription?: string;
|
||||
/**
|
||||
* Saved search title
|
||||
*/
|
||||
searchTitle?: string;
|
||||
/**
|
||||
* Discover plugin services
|
||||
*/
|
||||
services: DiscoverServices;
|
||||
/**
|
||||
* Determines whether the time columns should be displayed (legacy settings)
|
||||
*/
|
||||
showTimeCol: boolean;
|
||||
/**
|
||||
* Current sort setting
|
||||
*/
|
||||
sort: SortPairArr[];
|
||||
}
|
||||
|
||||
export const EuiDataGridMemoized = React.memo((props: EuiDataGridProps) => {
|
||||
return <EuiDataGrid {...props} />;
|
||||
});
|
||||
|
||||
export const DiscoverGrid = ({
|
||||
ariaLabelledBy,
|
||||
columns,
|
||||
defaultColumns,
|
||||
indexPattern,
|
||||
onAddColumn,
|
||||
onFilter,
|
||||
onRemoveColumn,
|
||||
onResize,
|
||||
onSetColumns,
|
||||
onSort,
|
||||
rows,
|
||||
sampleSize,
|
||||
searchDescription,
|
||||
searchTitle,
|
||||
services,
|
||||
settings,
|
||||
showTimeCol,
|
||||
sort,
|
||||
}: DiscoverGridProps) => {
|
||||
const [expanded, setExpanded] = useState<ElasticSearchHit | undefined>(undefined);
|
||||
|
||||
/**
|
||||
* Pagination
|
||||
*/
|
||||
const [pagination, setPagination] = useState({ pageIndex: 0, pageSize: defaultPageSize });
|
||||
const rowCount = useMemo(() => (rows ? rows.length : 0), [rows]);
|
||||
const pageCount = useMemo(() => Math.ceil(rowCount / pagination.pageSize), [
|
||||
rowCount,
|
||||
pagination,
|
||||
]);
|
||||
const isOnLastPage = pagination.pageIndex === pageCount - 1;
|
||||
|
||||
const paginationObj = useMemo(() => {
|
||||
const onChangeItemsPerPage = (pageSize: number) =>
|
||||
setPagination((paginationData) => ({ ...paginationData, pageSize }));
|
||||
|
||||
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]);
|
||||
|
||||
/**
|
||||
* Sorting
|
||||
*/
|
||||
const sortingColumns = useMemo(() => sort.map(([id, direction]) => ({ id, direction })), [sort]);
|
||||
|
||||
const onTableSort = useCallback(
|
||||
(sortingColumnsData) => {
|
||||
onSort(sortingColumnsData.map(({ id, direction }: SortObj) => [id, direction]));
|
||||
},
|
||||
[onSort]
|
||||
);
|
||||
|
||||
/**
|
||||
* Cell rendering
|
||||
*/
|
||||
const renderCellValue = useMemo(
|
||||
() =>
|
||||
getRenderCellValueFn(
|
||||
indexPattern,
|
||||
rows,
|
||||
rows ? rows.map((hit) => indexPattern.flattenHit(hit)) : []
|
||||
),
|
||||
[rows, indexPattern]
|
||||
);
|
||||
|
||||
/**
|
||||
* Render variables
|
||||
*/
|
||||
const showDisclaimer = rowCount === sampleSize && isOnLastPage;
|
||||
const randomId = useMemo(() => htmlIdGenerator()(), []);
|
||||
|
||||
const euiGridColumns = useMemo(
|
||||
() => getEuiGridColumns(columns, settings, indexPattern, showTimeCol, defaultColumns),
|
||||
[columns, indexPattern, showTimeCol, settings, defaultColumns]
|
||||
);
|
||||
const schemaDetectors = useMemo(() => getSchemaDetectors(), []);
|
||||
const popoverContents = useMemo(() => getPopoverContents(), []);
|
||||
const columnsVisibility = useMemo(
|
||||
() => ({
|
||||
visibleColumns: getVisibleColumns(columns, indexPattern, showTimeCol) as string[],
|
||||
setVisibleColumns: (newColumns: string[]) => {
|
||||
onSetColumns(newColumns);
|
||||
},
|
||||
}),
|
||||
[columns, indexPattern, showTimeCol, onSetColumns]
|
||||
);
|
||||
const sorting = useMemo(() => ({ columns: sortingColumns, onSort: onTableSort }), [
|
||||
sortingColumns,
|
||||
onTableSort,
|
||||
]);
|
||||
const lead = useMemo(() => getLeadControlColumns(), []);
|
||||
|
||||
if (!rowCount) {
|
||||
return (
|
||||
<div className="euiDataGrid__noResults">
|
||||
<EuiText size="xs" color="subdued">
|
||||
<EuiIcon type="discoverApp" size="m" color="subdued" />
|
||||
<EuiSpacer size="s" />
|
||||
<FormattedMessage id="discover.noResultsFound" defaultMessage="No results found" />
|
||||
</EuiText>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<DiscoverGridContext.Provider
|
||||
value={{
|
||||
expanded,
|
||||
setExpanded,
|
||||
rows: rows || [],
|
||||
onFilter,
|
||||
indexPattern,
|
||||
isDarkMode: services.uiSettings.get('theme:darkMode'),
|
||||
}}
|
||||
>
|
||||
<>
|
||||
<EuiDataGridMemoized
|
||||
aria-describedby={randomId}
|
||||
aria-labelledby={ariaLabelledBy}
|
||||
columns={euiGridColumns}
|
||||
columnVisibility={columnsVisibility}
|
||||
data-test-subj="docTable"
|
||||
gridStyle={gridStyle as EuiDataGridStyle}
|
||||
leadingControlColumns={lead}
|
||||
onColumnResize={(col: { columnId: string; width: number }) => {
|
||||
if (onResize) {
|
||||
onResize(col);
|
||||
}
|
||||
}}
|
||||
pagination={paginationObj}
|
||||
popoverContents={popoverContents}
|
||||
renderCellValue={renderCellValue}
|
||||
rowCount={rowCount}
|
||||
schemaDetectors={schemaDetectors}
|
||||
sorting={sorting as EuiDataGridSorting}
|
||||
toolbarVisibility={
|
||||
defaultColumns
|
||||
? {
|
||||
...toolbarVisibility,
|
||||
showColumnSelector: false,
|
||||
}
|
||||
: toolbarVisibility
|
||||
}
|
||||
/>
|
||||
|
||||
{showDisclaimer && (
|
||||
<p className="dscDiscoverGrid__footer">
|
||||
<FormattedMessage
|
||||
id="discover.howToSeeOtherMatchingDocumentsDescriptionGrid"
|
||||
defaultMessage="These are the first {sampleSize} documents matching your search, refine your search to see others."
|
||||
values={{ sampleSize }}
|
||||
/>
|
||||
<a href={`#${ariaLabelledBy}`}>
|
||||
<FormattedMessage id="discover.backToTopLinkText" defaultMessage="Back to top." />
|
||||
</a>
|
||||
</p>
|
||||
)}
|
||||
{searchTitle && (
|
||||
<EuiScreenReaderOnly>
|
||||
<p id={String(randomId)}>
|
||||
{searchDescription ? (
|
||||
<FormattedMessage
|
||||
id="discover.searchGenerationWithDescriptionGrid"
|
||||
defaultMessage="Table generated by search {searchTitle} ({searchDescription})"
|
||||
values={{ searchTitle, searchDescription }}
|
||||
/>
|
||||
) : (
|
||||
<FormattedMessage
|
||||
id="discover.searchGenerationWithDescription"
|
||||
defaultMessage="Table generated by search {searchTitle}"
|
||||
values={{ searchTitle }}
|
||||
/>
|
||||
)}
|
||||
</p>
|
||||
</EuiScreenReaderOnly>
|
||||
)}
|
||||
{expanded && (
|
||||
<DiscoverGridFlyout
|
||||
indexPattern={indexPattern}
|
||||
hit={expanded}
|
||||
columns={columns}
|
||||
onFilter={onFilter}
|
||||
onRemoveColumn={onRemoveColumn}
|
||||
onAddColumn={onAddColumn}
|
||||
onClose={() => setExpanded(undefined)}
|
||||
services={services}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
</DiscoverGridContext.Provider>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,80 @@
|
|||
/*
|
||||
* Licensed to Elasticsearch B.V. under one or more contributor
|
||||
* license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright
|
||||
* ownership. Elasticsearch B.V. licenses this file to you under
|
||||
* the Apache License, Version 2.0 (the "License"); you may
|
||||
* not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import React from 'react';
|
||||
import { mountWithIntl } from '@kbn/test/jest';
|
||||
import { findTestSubject } from '@elastic/eui/lib/test';
|
||||
import { FilterInBtn, FilterOutBtn } from './discover_grid_cell_actions';
|
||||
import { DiscoverGridContext } from './discover_grid_context';
|
||||
|
||||
import { indexPatternMock } from '../../../__mocks__/index_pattern';
|
||||
import { esHits } from '../../../__mocks__/es_hits';
|
||||
import { EuiButton } from '@elastic/eui';
|
||||
|
||||
describe('Discover cell actions ', function () {
|
||||
it('triggers filter function when FilterInBtn is clicked', async () => {
|
||||
const contextMock = {
|
||||
expanded: undefined,
|
||||
setExpanded: jest.fn(),
|
||||
rows: esHits,
|
||||
onFilter: jest.fn(),
|
||||
indexPattern: indexPatternMock,
|
||||
isDarkMode: false,
|
||||
};
|
||||
|
||||
const component = mountWithIntl(
|
||||
<DiscoverGridContext.Provider value={contextMock}>
|
||||
<FilterInBtn
|
||||
Component={(props: any) => <EuiButton {...props} />}
|
||||
rowIndex={1}
|
||||
columnId={'extension'}
|
||||
isExpanded={false}
|
||||
closePopover={jest.fn()}
|
||||
/>
|
||||
</DiscoverGridContext.Provider>
|
||||
);
|
||||
const button = findTestSubject(component, 'filterForButton');
|
||||
await button.simulate('click');
|
||||
expect(contextMock.onFilter).toHaveBeenCalledWith('extension', 'jpg', '+');
|
||||
});
|
||||
it('triggers filter function when FilterOutBtn is clicked', async () => {
|
||||
const contextMock = {
|
||||
expanded: undefined,
|
||||
setExpanded: jest.fn(),
|
||||
rows: esHits,
|
||||
onFilter: jest.fn(),
|
||||
indexPattern: indexPatternMock,
|
||||
isDarkMode: false,
|
||||
};
|
||||
|
||||
const component = mountWithIntl(
|
||||
<DiscoverGridContext.Provider value={contextMock}>
|
||||
<FilterOutBtn
|
||||
Component={(props: any) => <EuiButton {...props} />}
|
||||
rowIndex={1}
|
||||
columnId={'extension'}
|
||||
isExpanded={false}
|
||||
closePopover={jest.fn()}
|
||||
/>
|
||||
</DiscoverGridContext.Provider>
|
||||
);
|
||||
const button = findTestSubject(component, 'filterOutButton');
|
||||
await button.simulate('click');
|
||||
expect(contextMock.onFilter).toHaveBeenCalledWith('extension', 'jpg', '-');
|
||||
});
|
||||
});
|
|
@ -0,0 +1,97 @@
|
|||
/*
|
||||
* Licensed to Elasticsearch B.V. under one or more contributor
|
||||
* license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright
|
||||
* ownership. Elasticsearch B.V. licenses this file to you under
|
||||
* the Apache License, Version 2.0 (the "License"); you may
|
||||
* not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import React, { useContext } from 'react';
|
||||
import { EuiDataGridColumnCellActionProps } from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { IndexPatternField } from '../../../../../data/common/index_patterns/fields';
|
||||
import { DiscoverGridContext } from './discover_grid_context';
|
||||
|
||||
export const FilterInBtn = ({
|
||||
Component,
|
||||
rowIndex,
|
||||
columnId,
|
||||
}: EuiDataGridColumnCellActionProps) => {
|
||||
const context = useContext(DiscoverGridContext);
|
||||
const buttonTitle = i18n.translate('discover.grid.filterForAria', {
|
||||
defaultMessage: 'Filter for this {value}',
|
||||
values: { value: columnId },
|
||||
});
|
||||
|
||||
return (
|
||||
<Component
|
||||
onClick={() => {
|
||||
const row = context.rows[rowIndex];
|
||||
const flattened = context.indexPattern.flattenHit(row);
|
||||
|
||||
if (flattened) {
|
||||
context.onFilter(columnId, flattened[columnId], '+');
|
||||
}
|
||||
}}
|
||||
iconType="plusInCircle"
|
||||
aria-label={buttonTitle}
|
||||
title={buttonTitle}
|
||||
data-test-subj="filterForButton"
|
||||
>
|
||||
{i18n.translate('discover.grid.filterFor', {
|
||||
defaultMessage: 'Filter for',
|
||||
})}
|
||||
</Component>
|
||||
);
|
||||
};
|
||||
|
||||
export const FilterOutBtn = ({
|
||||
Component,
|
||||
rowIndex,
|
||||
columnId,
|
||||
}: EuiDataGridColumnCellActionProps) => {
|
||||
const context = useContext(DiscoverGridContext);
|
||||
const buttonTitle = i18n.translate('discover.grid.filterOutAria', {
|
||||
defaultMessage: 'Filter out this {value}',
|
||||
values: { value: columnId },
|
||||
});
|
||||
|
||||
return (
|
||||
<Component
|
||||
onClick={() => {
|
||||
const row = context.rows[rowIndex];
|
||||
const flattened = context.indexPattern.flattenHit(row);
|
||||
|
||||
if (flattened) {
|
||||
context.onFilter(columnId, flattened[columnId], '-');
|
||||
}
|
||||
}}
|
||||
iconType="minusInCircle"
|
||||
aria-label={buttonTitle}
|
||||
title={buttonTitle}
|
||||
data-test-subj="filterOutButton"
|
||||
>
|
||||
{i18n.translate('discover.grid.filterOut', {
|
||||
defaultMessage: 'Filter out',
|
||||
})}
|
||||
</Component>
|
||||
);
|
||||
};
|
||||
|
||||
export function buildCellActions(field: IndexPatternField) {
|
||||
if (!field.aggregatable && !field.searchable) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return [FilterInBtn, FilterOutBtn];
|
||||
}
|
|
@ -0,0 +1,154 @@
|
|||
/*
|
||||
* Licensed to Elasticsearch B.V. under one or more contributor
|
||||
* license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright
|
||||
* ownership. Elasticsearch B.V. licenses this file to you under
|
||||
* the Apache License, Version 2.0 (the "License"); you may
|
||||
* not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import { indexPatternMock } from '../../../__mocks__/index_pattern';
|
||||
import { getEuiGridColumns } from './discover_grid_columns';
|
||||
import { indexPatternWithTimefieldMock } from '../../../__mocks__/index_pattern_with_timefield';
|
||||
|
||||
describe('Discover grid columns ', function () {
|
||||
it('returns eui grid columns without time column', async () => {
|
||||
const actual = getEuiGridColumns(['extension', 'message'], {}, indexPatternMock, false, false);
|
||||
expect(actual).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
Object {
|
||||
"actions": Object {
|
||||
"showHide": Object {
|
||||
"iconType": "cross",
|
||||
"label": "Remove column",
|
||||
},
|
||||
"showMoveLeft": true,
|
||||
"showMoveRight": true,
|
||||
},
|
||||
"cellActions": undefined,
|
||||
"display": undefined,
|
||||
"id": "extension",
|
||||
"isSortable": undefined,
|
||||
"schema": "unknown",
|
||||
},
|
||||
Object {
|
||||
"actions": Object {
|
||||
"showHide": Object {
|
||||
"iconType": "cross",
|
||||
"label": "Remove column",
|
||||
},
|
||||
"showMoveLeft": true,
|
||||
"showMoveRight": true,
|
||||
},
|
||||
"cellActions": undefined,
|
||||
"display": undefined,
|
||||
"id": "message",
|
||||
"isSortable": undefined,
|
||||
"schema": "unknown",
|
||||
},
|
||||
]
|
||||
`);
|
||||
});
|
||||
it('returns eui grid columns without time column showing default columns', async () => {
|
||||
const actual = getEuiGridColumns(
|
||||
['extension', 'message'],
|
||||
{},
|
||||
indexPatternWithTimefieldMock,
|
||||
false,
|
||||
true
|
||||
);
|
||||
expect(actual).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
Object {
|
||||
"actions": Object {
|
||||
"showHide": false,
|
||||
"showMoveLeft": false,
|
||||
"showMoveRight": false,
|
||||
},
|
||||
"cellActions": undefined,
|
||||
"display": undefined,
|
||||
"id": "extension",
|
||||
"isSortable": undefined,
|
||||
"schema": "unknown",
|
||||
},
|
||||
Object {
|
||||
"actions": Object {
|
||||
"showHide": false,
|
||||
"showMoveLeft": false,
|
||||
"showMoveRight": false,
|
||||
},
|
||||
"cellActions": undefined,
|
||||
"display": undefined,
|
||||
"id": "message",
|
||||
"isSortable": undefined,
|
||||
"schema": "unknown",
|
||||
},
|
||||
]
|
||||
`);
|
||||
});
|
||||
it('returns eui grid columns with time column', async () => {
|
||||
const actual = getEuiGridColumns(
|
||||
['extension', 'message'],
|
||||
{},
|
||||
indexPatternWithTimefieldMock,
|
||||
true,
|
||||
false
|
||||
);
|
||||
expect(actual).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
Object {
|
||||
"actions": Object {
|
||||
"showHide": false,
|
||||
"showMoveLeft": true,
|
||||
"showMoveRight": true,
|
||||
},
|
||||
"cellActions": undefined,
|
||||
"display": "Time (timestamp)",
|
||||
"id": "timestamp",
|
||||
"initialWidth": 180,
|
||||
"isSortable": undefined,
|
||||
"schema": "unknown",
|
||||
},
|
||||
Object {
|
||||
"actions": Object {
|
||||
"showHide": Object {
|
||||
"iconType": "cross",
|
||||
"label": "Remove column",
|
||||
},
|
||||
"showMoveLeft": true,
|
||||
"showMoveRight": true,
|
||||
},
|
||||
"cellActions": undefined,
|
||||
"display": undefined,
|
||||
"id": "extension",
|
||||
"isSortable": undefined,
|
||||
"schema": "unknown",
|
||||
},
|
||||
Object {
|
||||
"actions": Object {
|
||||
"showHide": Object {
|
||||
"iconType": "cross",
|
||||
"label": "Remove column",
|
||||
},
|
||||
"showMoveLeft": true,
|
||||
"showMoveRight": true,
|
||||
},
|
||||
"cellActions": undefined,
|
||||
"display": undefined,
|
||||
"id": "message",
|
||||
"isSortable": undefined,
|
||||
"schema": "unknown",
|
||||
},
|
||||
]
|
||||
`);
|
||||
});
|
||||
});
|
|
@ -0,0 +1,122 @@
|
|||
/*
|
||||
* Licensed to Elasticsearch B.V. under one or more contributor
|
||||
* license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright
|
||||
* ownership. Elasticsearch B.V. licenses this file to you under
|
||||
* the Apache License, Version 2.0 (the "License"); you may
|
||||
* not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import React from 'react';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { EuiDataGridColumn, EuiScreenReaderOnly } from '@elastic/eui';
|
||||
import { ExpandButton } from './discover_grid_expand_button';
|
||||
import { DiscoverGridSettings } from './types';
|
||||
import { IndexPattern } from '../../../../../data/common/index_patterns/index_patterns';
|
||||
import { buildCellActions } from './discover_grid_cell_actions';
|
||||
import { getSchemaByKbnType } from './discover_grid_schema';
|
||||
|
||||
export function getLeadControlColumns() {
|
||||
return [
|
||||
{
|
||||
id: 'openDetails',
|
||||
width: 32,
|
||||
headerCellRender: () => (
|
||||
<EuiScreenReaderOnly>
|
||||
<span>
|
||||
{i18n.translate('discover.controlColumnHeader', {
|
||||
defaultMessage: 'Control column',
|
||||
})}
|
||||
</span>
|
||||
</EuiScreenReaderOnly>
|
||||
),
|
||||
rowCellRender: ExpandButton,
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
export function buildEuiGridColumn(
|
||||
columnName: string,
|
||||
columnWidth: number | undefined = 0,
|
||||
indexPattern: IndexPattern,
|
||||
defaultColumns: boolean
|
||||
) {
|
||||
const timeString = i18n.translate('discover.timeLabel', {
|
||||
defaultMessage: 'Time',
|
||||
});
|
||||
const indexPatternField = indexPattern.getFieldByName(columnName);
|
||||
const column: EuiDataGridColumn = {
|
||||
id: columnName,
|
||||
schema: getSchemaByKbnType(indexPatternField?.type),
|
||||
isSortable: indexPatternField?.sortable,
|
||||
display: indexPatternField?.displayName,
|
||||
actions: {
|
||||
showHide:
|
||||
defaultColumns || columnName === indexPattern.timeFieldName
|
||||
? false
|
||||
: {
|
||||
label: i18n.translate('discover.removeColumnLabel', {
|
||||
defaultMessage: 'Remove column',
|
||||
}),
|
||||
iconType: 'cross',
|
||||
},
|
||||
showMoveLeft: !defaultColumns,
|
||||
showMoveRight: !defaultColumns,
|
||||
},
|
||||
cellActions: indexPatternField ? buildCellActions(indexPatternField) : [],
|
||||
};
|
||||
|
||||
if (column.id === indexPattern.timeFieldName) {
|
||||
column.display = `${timeString} (${indexPattern.timeFieldName})`;
|
||||
column.initialWidth = 180;
|
||||
}
|
||||
if (columnWidth > 0) {
|
||||
column.initialWidth = Number(columnWidth);
|
||||
}
|
||||
return column;
|
||||
}
|
||||
|
||||
export function getEuiGridColumns(
|
||||
columns: string[],
|
||||
settings: DiscoverGridSettings | undefined,
|
||||
indexPattern: IndexPattern,
|
||||
showTimeCol: boolean,
|
||||
defaultColumns: boolean
|
||||
) {
|
||||
const timeFieldName = indexPattern.timeFieldName;
|
||||
const getColWidth = (column: string) => settings?.columns?.[column]?.width ?? 0;
|
||||
|
||||
if (showTimeCol && indexPattern.timeFieldName && !columns.find((col) => col === timeFieldName)) {
|
||||
const usedColumns = [indexPattern.timeFieldName, ...columns];
|
||||
return usedColumns.map((column) =>
|
||||
buildEuiGridColumn(column, getColWidth(column), indexPattern, defaultColumns)
|
||||
);
|
||||
}
|
||||
|
||||
return columns.map((column) =>
|
||||
buildEuiGridColumn(column, getColWidth(column), indexPattern, defaultColumns)
|
||||
);
|
||||
}
|
||||
|
||||
export function getVisibleColumns(
|
||||
columns: string[],
|
||||
indexPattern: IndexPattern,
|
||||
showTimeCol: boolean
|
||||
) {
|
||||
const timeFieldName = indexPattern.timeFieldName;
|
||||
|
||||
if (showTimeCol && !columns.find((col) => col === timeFieldName)) {
|
||||
return [timeFieldName, ...columns];
|
||||
}
|
||||
|
||||
return columns;
|
||||
}
|
|
@ -0,0 +1,34 @@
|
|||
/*
|
||||
* Licensed to Elasticsearch B.V. under one or more contributor
|
||||
* license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright
|
||||
* ownership. Elasticsearch B.V. licenses this file to you under
|
||||
* the Apache License, Version 2.0 (the "License"); you may
|
||||
* not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import React from 'react';
|
||||
import { DocViewFilterFn, ElasticSearchHit } from '../../doc_views/doc_views_types';
|
||||
import { IndexPattern } from '../../../kibana_services';
|
||||
|
||||
export interface GridContext {
|
||||
expanded: ElasticSearchHit | undefined;
|
||||
setExpanded: (hit: ElasticSearchHit | undefined) => void;
|
||||
rows: ElasticSearchHit[];
|
||||
onFilter: DocViewFilterFn;
|
||||
indexPattern: IndexPattern;
|
||||
isDarkMode: boolean;
|
||||
}
|
||||
|
||||
const defaultContext = ({} as unknown) as GridContext;
|
||||
|
||||
export const DiscoverGridContext = React.createContext<GridContext>(defaultContext);
|
|
@ -0,0 +1,106 @@
|
|||
/*
|
||||
* Licensed to Elasticsearch B.V. under one or more contributor
|
||||
* license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright
|
||||
* ownership. Elasticsearch B.V. licenses this file to you under
|
||||
* the Apache License, Version 2.0 (the "License"); you may
|
||||
* not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import React from 'react';
|
||||
import { mountWithIntl } from '@kbn/test/jest';
|
||||
import { findTestSubject } from '@elastic/eui/lib/test';
|
||||
import { ExpandButton } from './discover_grid_expand_button';
|
||||
import { DiscoverGridContext } from './discover_grid_context';
|
||||
import { indexPatternMock } from '../../../__mocks__/index_pattern';
|
||||
import { esHits } from '../../../__mocks__/es_hits';
|
||||
|
||||
describe('Discover grid view button ', function () {
|
||||
it('when no document is expanded, setExpanded is called with current document', async () => {
|
||||
const contextMock = {
|
||||
expanded: undefined,
|
||||
setExpanded: jest.fn(),
|
||||
rows: esHits,
|
||||
onFilter: jest.fn(),
|
||||
indexPattern: indexPatternMock,
|
||||
isDarkMode: false,
|
||||
};
|
||||
|
||||
const component = mountWithIntl(
|
||||
<DiscoverGridContext.Provider value={contextMock}>
|
||||
<ExpandButton
|
||||
rowIndex={0}
|
||||
setCellProps={jest.fn()}
|
||||
columnId="test"
|
||||
isExpanded={false}
|
||||
isDetails={false}
|
||||
isExpandable={false}
|
||||
/>
|
||||
</DiscoverGridContext.Provider>
|
||||
);
|
||||
const button = findTestSubject(component, 'docTableExpandToggleColumn');
|
||||
await button.simulate('click');
|
||||
expect(contextMock.setExpanded).toHaveBeenCalledWith(esHits[0]);
|
||||
});
|
||||
it('when the current document is expanded, setExpanded is called with undefined', async () => {
|
||||
const contextMock = {
|
||||
expanded: esHits[0],
|
||||
setExpanded: jest.fn(),
|
||||
rows: esHits,
|
||||
onFilter: jest.fn(),
|
||||
indexPattern: indexPatternMock,
|
||||
isDarkMode: false,
|
||||
};
|
||||
|
||||
const component = mountWithIntl(
|
||||
<DiscoverGridContext.Provider value={contextMock}>
|
||||
<ExpandButton
|
||||
rowIndex={0}
|
||||
setCellProps={jest.fn()}
|
||||
columnId="test"
|
||||
isExpanded={false}
|
||||
isDetails={false}
|
||||
isExpandable={false}
|
||||
/>
|
||||
</DiscoverGridContext.Provider>
|
||||
);
|
||||
const button = findTestSubject(component, 'docTableExpandToggleColumn');
|
||||
await button.simulate('click');
|
||||
expect(contextMock.setExpanded).toHaveBeenCalledWith(undefined);
|
||||
});
|
||||
it('when another document is expanded, setExpanded is called with the current document', async () => {
|
||||
const contextMock = {
|
||||
expanded: esHits[0],
|
||||
setExpanded: jest.fn(),
|
||||
rows: esHits,
|
||||
onFilter: jest.fn(),
|
||||
indexPattern: indexPatternMock,
|
||||
isDarkMode: false,
|
||||
};
|
||||
|
||||
const component = mountWithIntl(
|
||||
<DiscoverGridContext.Provider value={contextMock}>
|
||||
<ExpandButton
|
||||
rowIndex={1}
|
||||
setCellProps={jest.fn()}
|
||||
columnId="test"
|
||||
isExpanded={false}
|
||||
isDetails={false}
|
||||
isExpandable={false}
|
||||
/>
|
||||
</DiscoverGridContext.Provider>
|
||||
);
|
||||
const button = findTestSubject(component, 'docTableExpandToggleColumn');
|
||||
await button.simulate('click');
|
||||
expect(contextMock.setExpanded).toHaveBeenCalledWith(esHits[1]);
|
||||
});
|
||||
});
|
|
@ -0,0 +1,62 @@
|
|||
/*
|
||||
* Licensed to Elasticsearch B.V. under one or more contributor
|
||||
* license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright
|
||||
* ownership. Elasticsearch B.V. licenses this file to you under
|
||||
* the Apache License, Version 2.0 (the "License"); you may
|
||||
* not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import React, { useContext, useEffect } from 'react';
|
||||
import { EuiButtonIcon, EuiDataGridCellValueElementProps, EuiToolTip } from '@elastic/eui';
|
||||
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';
|
||||
/**
|
||||
* Button to expand a given row
|
||||
*/
|
||||
export const ExpandButton = ({ rowIndex, setCellProps }: EuiDataGridCellValueElementProps) => {
|
||||
const { expanded, setExpanded, rows, isDarkMode } = useContext(DiscoverGridContext);
|
||||
const current = rows[rowIndex];
|
||||
useEffect(() => {
|
||||
if (expanded && current && expanded._id === current._id) {
|
||||
setCellProps({
|
||||
style: {
|
||||
backgroundColor: isDarkMode ? themeDark.euiColorHighlight : themeLight.euiColorHighlight,
|
||||
},
|
||||
});
|
||||
} else {
|
||||
setCellProps({ style: undefined });
|
||||
}
|
||||
}, [expanded, current, setCellProps, isDarkMode]);
|
||||
|
||||
const isCurrentRowExpanded = current === expanded;
|
||||
const buttonLabel = i18n.translate('discover.grid.viewDoc', {
|
||||
defaultMessage: 'Toggle dialog with details',
|
||||
});
|
||||
|
||||
return (
|
||||
<EuiToolTip content={buttonLabel} delay="long">
|
||||
<EuiButtonIcon
|
||||
size="s"
|
||||
iconSize="s"
|
||||
aria-label={buttonLabel}
|
||||
data-test-subj="docTableExpandToggleColumn"
|
||||
onClick={() => setExpanded(isCurrentRowExpanded ? undefined : current)}
|
||||
color={isCurrentRowExpanded ? 'primary' : 'subdued'}
|
||||
iconType={isCurrentRowExpanded ? 'minimize' : 'expand'}
|
||||
isSelected={isCurrentRowExpanded}
|
||||
/>
|
||||
</EuiToolTip>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,143 @@
|
|||
/*
|
||||
* Licensed to Elasticsearch B.V. under one or more contributor
|
||||
* license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright
|
||||
* ownership. Elasticsearch B.V. licenses this file to you under
|
||||
* the Apache License, Version 2.0 (the "License"); you may
|
||||
* not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import {
|
||||
EuiFlexGroup,
|
||||
EuiFlexItem,
|
||||
EuiFlyout,
|
||||
EuiFlyoutBody,
|
||||
EuiFlyoutHeader,
|
||||
EuiTitle,
|
||||
EuiButtonEmpty,
|
||||
EuiText,
|
||||
EuiSpacer,
|
||||
EuiPortal,
|
||||
} from '@elastic/eui';
|
||||
import { DocViewer } from '../doc_viewer/doc_viewer';
|
||||
import { IndexPattern } from '../../../kibana_services';
|
||||
import { DocViewFilterFn, ElasticSearchHit } from '../../doc_views/doc_views_types';
|
||||
import { DiscoverServices } from '../../../build_services';
|
||||
import { getContextUrl } from '../../helpers/get_context_url';
|
||||
|
||||
interface Props {
|
||||
columns: string[];
|
||||
hit: ElasticSearchHit;
|
||||
indexPattern: IndexPattern;
|
||||
onAddColumn: (column: string) => void;
|
||||
onClose: () => void;
|
||||
onFilter: DocViewFilterFn;
|
||||
onRemoveColumn: (column: string) => void;
|
||||
services: DiscoverServices;
|
||||
}
|
||||
|
||||
/**
|
||||
* Flyout displaying an expanded Elasticsearch document
|
||||
*/
|
||||
export function DiscoverGridFlyout({
|
||||
hit,
|
||||
indexPattern,
|
||||
columns,
|
||||
onFilter,
|
||||
onClose,
|
||||
onRemoveColumn,
|
||||
onAddColumn,
|
||||
services,
|
||||
}: Props) {
|
||||
return (
|
||||
<EuiPortal>
|
||||
<EuiFlyout onClose={onClose} size="m" data-test-subj="docTableDetailsFlyout">
|
||||
<EuiFlyoutHeader hasBorder>
|
||||
<EuiTitle
|
||||
size="s"
|
||||
className="dscTable__flyoutHeader"
|
||||
data-test-subj="docTableRowDetailsTitle"
|
||||
>
|
||||
<h2>
|
||||
{i18n.translate('discover.grid.tableRow.detailHeading', {
|
||||
defaultMessage: 'Expanded document',
|
||||
})}
|
||||
</h2>
|
||||
</EuiTitle>
|
||||
|
||||
<EuiSpacer size="s" />
|
||||
<EuiFlexGroup responsive={false} gutterSize="m" alignItems="center">
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiText size="s">
|
||||
<strong>
|
||||
{i18n.translate('discover.grid.tableRow.viewText', {
|
||||
defaultMessage: 'View:',
|
||||
})}
|
||||
</strong>
|
||||
</EuiText>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiButtonEmpty
|
||||
size="xs"
|
||||
iconType="document"
|
||||
href={`#/doc/${indexPattern.id}/${hit._index}?id=${encodeURIComponent(
|
||||
hit._id as string
|
||||
)}`}
|
||||
data-test-subj="docTableRowAction"
|
||||
>
|
||||
{i18n.translate('discover.grid.tableRow.viewSingleDocumentLinkTextSimple', {
|
||||
defaultMessage: 'Single document',
|
||||
})}
|
||||
</EuiButtonEmpty>
|
||||
</EuiFlexItem>
|
||||
{indexPattern.isTimeBased() && indexPattern.id && (
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiButtonEmpty
|
||||
size="xs"
|
||||
iconType="documents"
|
||||
href={getContextUrl(hit._id, indexPattern.id, columns, services.filterManager)}
|
||||
data-test-subj="docTableRowAction"
|
||||
>
|
||||
{i18n.translate('discover.grid.tableRow.viewSurroundingDocumentsLinkTextSimple', {
|
||||
defaultMessage: 'Surrounding documents',
|
||||
})}
|
||||
</EuiButtonEmpty>
|
||||
</EuiFlexItem>
|
||||
)}
|
||||
</EuiFlexGroup>
|
||||
</EuiFlyoutHeader>
|
||||
<EuiFlyoutBody>
|
||||
<DocViewer
|
||||
hit={hit}
|
||||
columns={columns}
|
||||
indexPattern={indexPattern}
|
||||
filter={(mapping, value, mode) => {
|
||||
onFilter(mapping, value, mode);
|
||||
onClose();
|
||||
}}
|
||||
onRemoveColumn={(columnName: string) => {
|
||||
onRemoveColumn(columnName);
|
||||
onClose();
|
||||
}}
|
||||
onAddColumn={(columnName: string) => {
|
||||
onAddColumn(columnName);
|
||||
onClose();
|
||||
}}
|
||||
/>
|
||||
</EuiFlyoutBody>
|
||||
</EuiFlyout>
|
||||
</EuiPortal>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,103 @@
|
|||
/*
|
||||
* Licensed to Elasticsearch B.V. under one or more contributor
|
||||
* license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright
|
||||
* ownership. Elasticsearch B.V. licenses this file to you under
|
||||
* the Apache License, Version 2.0 (the "License"); you may
|
||||
* not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import React, { ReactNode } from 'react';
|
||||
import { EuiCodeBlock } from '@elastic/eui';
|
||||
import { geoPoint, kibanaJSON, unknownType } from './constants';
|
||||
import { KBN_FIELD_TYPES } from '../../../../../data/common';
|
||||
|
||||
export function getSchemaByKbnType(kbnType: string | undefined) {
|
||||
// Default DataGrid schemas: boolean, numeric, datetime, json, currency, string
|
||||
switch (kbnType) {
|
||||
case KBN_FIELD_TYPES.IP:
|
||||
case KBN_FIELD_TYPES.GEO_SHAPE:
|
||||
case KBN_FIELD_TYPES.NUMBER:
|
||||
return 'numeric';
|
||||
case KBN_FIELD_TYPES.BOOLEAN:
|
||||
return 'boolean';
|
||||
case KBN_FIELD_TYPES.STRING:
|
||||
return 'string';
|
||||
case KBN_FIELD_TYPES.DATE:
|
||||
return 'datetime';
|
||||
case KBN_FIELD_TYPES._SOURCE:
|
||||
return kibanaJSON;
|
||||
case KBN_FIELD_TYPES.GEO_POINT:
|
||||
return geoPoint;
|
||||
default:
|
||||
return unknownType;
|
||||
}
|
||||
}
|
||||
|
||||
export function getSchemaDetectors() {
|
||||
return [
|
||||
{
|
||||
type: kibanaJSON,
|
||||
detector() {
|
||||
return 0; // this schema is always explicitly defined
|
||||
},
|
||||
sortTextAsc: '',
|
||||
sortTextDesc: '',
|
||||
icon: '',
|
||||
color: '',
|
||||
},
|
||||
{
|
||||
type: unknownType,
|
||||
detector() {
|
||||
return 0; // this schema is always explicitly defined
|
||||
},
|
||||
sortTextAsc: '',
|
||||
sortTextDesc: '',
|
||||
icon: '',
|
||||
color: '',
|
||||
},
|
||||
{
|
||||
type: geoPoint,
|
||||
detector() {
|
||||
return 0; // this schema is always explicitly defined
|
||||
},
|
||||
sortTextAsc: '',
|
||||
sortTextDesc: '',
|
||||
icon: 'tokenGeo',
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns custom popover content for certain schemas
|
||||
*/
|
||||
export function getPopoverContents() {
|
||||
return {
|
||||
[geoPoint]: ({ children }: { children: ReactNode }) => {
|
||||
return <span className="geo-point">{children}</span>;
|
||||
},
|
||||
[unknownType]: ({ children }: { children: ReactNode }) => {
|
||||
return (
|
||||
<EuiCodeBlock isCopyable language="json" paddingSize="none" transparentBackground={true}>
|
||||
{children}
|
||||
</EuiCodeBlock>
|
||||
);
|
||||
},
|
||||
[kibanaJSON]: ({ children }: { children: ReactNode }) => {
|
||||
return (
|
||||
<EuiCodeBlock isCopyable language="json" paddingSize="none" transparentBackground={true}>
|
||||
{children}
|
||||
</EuiCodeBlock>
|
||||
);
|
||||
},
|
||||
};
|
||||
}
|
|
@ -0,0 +1,132 @@
|
|||
/*
|
||||
* Licensed to Elasticsearch B.V. under one or more contributor
|
||||
* license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright
|
||||
* ownership. Elasticsearch B.V. licenses this file to you under
|
||||
* the Apache License, Version 2.0 (the "License"); you may
|
||||
* not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import React from 'react';
|
||||
import { shallow } from 'enzyme';
|
||||
import { getRenderCellValueFn } from './get_render_cell_value';
|
||||
import { indexPatternMock } from '../../../__mocks__/index_pattern';
|
||||
const rows = [
|
||||
{
|
||||
_id: '1',
|
||||
_index: 'test',
|
||||
_type: 'test',
|
||||
_score: 1,
|
||||
_source: { bytes: 100 },
|
||||
},
|
||||
];
|
||||
|
||||
describe('Discover grid cell rendering', function () {
|
||||
it('renders bytes column correctly', () => {
|
||||
const DiscoverGridCellValue = getRenderCellValueFn(
|
||||
indexPatternMock,
|
||||
rows,
|
||||
rows.map((row) => indexPatternMock.flattenHit(row))
|
||||
);
|
||||
const component = shallow(
|
||||
<DiscoverGridCellValue
|
||||
rowIndex={0}
|
||||
columnId="bytes"
|
||||
isDetails={false}
|
||||
isExpanded={false}
|
||||
isExpandable={true}
|
||||
setCellProps={jest.fn()}
|
||||
/>
|
||||
);
|
||||
expect(component.html()).toMatchInlineSnapshot(`"<span>100</span>"`);
|
||||
});
|
||||
it('renders _source column correctly', () => {
|
||||
const DiscoverGridCellValue = getRenderCellValueFn(
|
||||
indexPatternMock,
|
||||
rows,
|
||||
rows.map((row) => indexPatternMock.flattenHit(row))
|
||||
);
|
||||
const component = shallow(
|
||||
<DiscoverGridCellValue
|
||||
rowIndex={0}
|
||||
columnId="_source"
|
||||
isDetails={false}
|
||||
isExpanded={false}
|
||||
isExpandable={true}
|
||||
setCellProps={jest.fn()}
|
||||
/>
|
||||
);
|
||||
expect(component.html()).toMatchInlineSnapshot(
|
||||
`"<dl class=\\"euiDescriptionList euiDescriptionList--inline euiDescriptionList--compressed\\"><dt class=\\"euiDescriptionList__title\\">bytes</dt><dd class=\\"euiDescriptionList__description\\">100</dd></dl>"`
|
||||
);
|
||||
});
|
||||
|
||||
it('renders _source column correctly when isDetails is set to true', () => {
|
||||
const DiscoverGridCellValue = getRenderCellValueFn(
|
||||
indexPatternMock,
|
||||
rows,
|
||||
rows.map((row) => indexPatternMock.flattenHit(row))
|
||||
);
|
||||
const component = shallow(
|
||||
<DiscoverGridCellValue
|
||||
rowIndex={0}
|
||||
columnId="_source"
|
||||
isDetails={true}
|
||||
isExpanded={false}
|
||||
isExpandable={true}
|
||||
setCellProps={jest.fn()}
|
||||
/>
|
||||
);
|
||||
expect(component.html()).toMatchInlineSnapshot(`
|
||||
"<span>{
|
||||
"bytes": 100
|
||||
}</span>"
|
||||
`);
|
||||
});
|
||||
|
||||
it('renders correctly when invalid row is given', () => {
|
||||
const DiscoverGridCellValue = getRenderCellValueFn(
|
||||
indexPatternMock,
|
||||
rows,
|
||||
rows.map((row) => indexPatternMock.flattenHit(row))
|
||||
);
|
||||
const component = shallow(
|
||||
<DiscoverGridCellValue
|
||||
rowIndex={1}
|
||||
columnId="bytes"
|
||||
isDetails={false}
|
||||
isExpanded={false}
|
||||
isExpandable={true}
|
||||
setCellProps={jest.fn()}
|
||||
/>
|
||||
);
|
||||
expect(component.html()).toMatchInlineSnapshot(`"<span>-</span>"`);
|
||||
});
|
||||
it('renders correctly when invalid column is given', () => {
|
||||
const DiscoverGridCellValue = getRenderCellValueFn(
|
||||
indexPatternMock,
|
||||
rows,
|
||||
rows.map((row) => indexPatternMock.flattenHit(row))
|
||||
);
|
||||
const component = shallow(
|
||||
<DiscoverGridCellValue
|
||||
rowIndex={0}
|
||||
columnId="bytes-invalid"
|
||||
isDetails={false}
|
||||
isExpanded={false}
|
||||
isExpandable={true}
|
||||
setCellProps={jest.fn()}
|
||||
/>
|
||||
);
|
||||
expect(component.html()).toMatchInlineSnapshot(`"<span>-</span>"`);
|
||||
});
|
||||
});
|
|
@ -0,0 +1,116 @@
|
|||
/*
|
||||
* Licensed to Elasticsearch B.V. under one or more contributor
|
||||
* license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright
|
||||
* ownership. Elasticsearch B.V. licenses this file to you under
|
||||
* the Apache License, Version 2.0 (the "License"); you may
|
||||
* not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import React, { Fragment, useContext, useEffect } from 'react';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import themeLight from '@elastic/eui/dist/eui_theme_light.json';
|
||||
import themeDark from '@elastic/eui/dist/eui_theme_dark.json';
|
||||
|
||||
import {
|
||||
EuiDataGridCellValueElementProps,
|
||||
EuiDescriptionList,
|
||||
EuiDescriptionListTitle,
|
||||
EuiDescriptionListDescription,
|
||||
} from '@elastic/eui';
|
||||
import { IndexPattern } from '../../../kibana_services';
|
||||
import { ElasticSearchHit } from '../../doc_views/doc_views_types';
|
||||
import { DiscoverGridContext } from './discover_grid_context';
|
||||
|
||||
export const getRenderCellValueFn = (
|
||||
indexPattern: IndexPattern,
|
||||
rows: ElasticSearchHit[] | undefined,
|
||||
rowsFlattened: Array<Record<string, unknown>>
|
||||
) => ({ rowIndex, columnId, isDetails, setCellProps }: EuiDataGridCellValueElementProps) => {
|
||||
const row = rows ? (rows[rowIndex] as Record<string, unknown>) : undefined;
|
||||
const rowFlattened = rowsFlattened
|
||||
? (rowsFlattened[rowIndex] as Record<string, unknown>)
|
||||
: undefined;
|
||||
|
||||
const field = indexPattern.fields.getByName(columnId);
|
||||
const ctx = useContext(DiscoverGridContext);
|
||||
|
||||
useEffect(() => {
|
||||
if (ctx.expanded && row && ctx.expanded._id === row._id) {
|
||||
setCellProps({
|
||||
style: {
|
||||
backgroundColor: ctx.isDarkMode
|
||||
? themeDark.euiColorHighlight
|
||||
: themeLight.euiColorHighlight,
|
||||
},
|
||||
});
|
||||
} else {
|
||||
setCellProps({ style: undefined });
|
||||
}
|
||||
}, [ctx, row, setCellProps]);
|
||||
|
||||
if (typeof row === 'undefined' || typeof rowFlattened === 'undefined') {
|
||||
return <span>-</span>;
|
||||
}
|
||||
|
||||
if (field && field.type === '_source') {
|
||||
if (isDetails) {
|
||||
// nicely formatted JSON for the expanded view
|
||||
return <span>{JSON.stringify(row[columnId], null, 2)}</span>;
|
||||
}
|
||||
const formatted = indexPattern.formatHit(row);
|
||||
|
||||
return (
|
||||
<EuiDescriptionList type="inline" compressed>
|
||||
{Object.keys(formatted).map((key) => (
|
||||
<Fragment key={key}>
|
||||
<EuiDescriptionListTitle>{key}</EuiDescriptionListTitle>
|
||||
<EuiDescriptionListDescription dangerouslySetInnerHTML={{ __html: formatted[key] }} />
|
||||
</Fragment>
|
||||
))}
|
||||
</EuiDescriptionList>
|
||||
);
|
||||
}
|
||||
|
||||
if (!field?.type && rowFlattened && typeof rowFlattened[columnId] === 'object') {
|
||||
if (isDetails) {
|
||||
// nicely formatted JSON for the expanded view
|
||||
return <span>{JSON.stringify(rowFlattened[columnId], null, 2)}</span>;
|
||||
}
|
||||
|
||||
return <span>{JSON.stringify(rowFlattened[columnId])}</span>;
|
||||
}
|
||||
|
||||
if (field?.type === 'geo_point' && rowFlattened && rowFlattened[columnId]) {
|
||||
const valueFormatted = rowFlattened[columnId] as { lat: number; lon: number };
|
||||
return (
|
||||
<div>
|
||||
{i18n.translate('discover.latitudeAndLongitude', {
|
||||
defaultMessage: 'Lat: {lat} Lon: {lon}',
|
||||
values: {
|
||||
lat: valueFormatted?.lat,
|
||||
lon: valueFormatted?.lon,
|
||||
},
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const valueFormatted = indexPattern.formatField(row, columnId);
|
||||
if (typeof valueFormatted === 'undefined') {
|
||||
return <span>-</span>;
|
||||
}
|
||||
return (
|
||||
// eslint-disable-next-line react/no-danger
|
||||
<span dangerouslySetInnerHTML={{ __html: indexPattern.formatField(row, columnId) }} />
|
||||
);
|
||||
};
|
|
@ -0,0 +1,29 @@
|
|||
/*
|
||||
* Licensed to Elasticsearch B.V. under one or more contributor
|
||||
* license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright
|
||||
* ownership. Elasticsearch B.V. licenses this file to you under
|
||||
* the Apache License, Version 2.0 (the "License"); you may
|
||||
* not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
/**
|
||||
* User configurable state of data grid, persisted in saved search
|
||||
*/
|
||||
export interface DiscoverGridSettings {
|
||||
columns?: Record<string, DiscoverGridSettingsColumn>;
|
||||
}
|
||||
|
||||
export interface DiscoverGridSettingsColumn {
|
||||
width?: number;
|
||||
}
|
|
@ -67,7 +67,6 @@ function getProps(indexPattern: IndexPattern) {
|
|||
} as unknown) as DiscoverServices;
|
||||
|
||||
return {
|
||||
addColumn: jest.fn(),
|
||||
fetch: jest.fn(),
|
||||
fetchCounter: 0,
|
||||
fetchError: undefined,
|
||||
|
@ -75,6 +74,7 @@ function getProps(indexPattern: IndexPattern) {
|
|||
hits: esHits.length,
|
||||
indexPattern,
|
||||
minimumVisibleRows: 10,
|
||||
onAddColumn: jest.fn(),
|
||||
onAddFilter: jest.fn(),
|
||||
onChangeInterval: jest.fn(),
|
||||
onMoveColumn: jest.fn(),
|
||||
|
|
|
@ -63,46 +63,161 @@ import {
|
|||
import { DocViewFilterFn, ElasticSearchHit } from '../doc_views/doc_views_types';
|
||||
|
||||
export interface DiscoverProps {
|
||||
addColumn: (column: string) => void;
|
||||
/**
|
||||
* Function to fetch documents from Elasticsearch
|
||||
*/
|
||||
fetch: () => void;
|
||||
/**
|
||||
* Counter how often data was fetched (used for testing)
|
||||
*/
|
||||
fetchCounter: number;
|
||||
/**
|
||||
* Error in case of a failing document fetch
|
||||
*/
|
||||
fetchError?: Error;
|
||||
/**
|
||||
* Statistics by fields calculated using the fetched documents
|
||||
*/
|
||||
fieldCounts: Record<string, number>;
|
||||
/**
|
||||
* Histogram aggregation data
|
||||
*/
|
||||
histogramData?: Chart;
|
||||
/**
|
||||
* Number of documents found by recent fetch
|
||||
*/
|
||||
hits: number;
|
||||
/**
|
||||
* Current IndexPattern
|
||||
*/
|
||||
indexPattern: IndexPattern;
|
||||
/**
|
||||
* Value needed for legacy "infinite" loading functionality
|
||||
* Determins how much records are rendered using the legacy table
|
||||
* Increased when scrolling down
|
||||
*/
|
||||
minimumVisibleRows: number;
|
||||
/**
|
||||
* Function to add a column to state
|
||||
*/
|
||||
onAddColumn: (column: string) => void;
|
||||
/**
|
||||
* Function to add a filter to state
|
||||
*/
|
||||
onAddFilter: DocViewFilterFn;
|
||||
/**
|
||||
* Function to change the used time interval of the date histogram
|
||||
*/
|
||||
onChangeInterval: (interval: string) => void;
|
||||
/**
|
||||
* Function to move a given column to a given index, used in legacy table
|
||||
*/
|
||||
onMoveColumn: (columns: string, newIdx: number) => void;
|
||||
/**
|
||||
* Function to remove a given column from state
|
||||
*/
|
||||
onRemoveColumn: (column: string) => void;
|
||||
/**
|
||||
* Function to replace columns in state
|
||||
*/
|
||||
onSetColumns: (columns: string[]) => void;
|
||||
/**
|
||||
* Function to scroll down the legacy table to the bottom
|
||||
*/
|
||||
onSkipBottomButtonClick: () => void;
|
||||
/**
|
||||
* Function to change sorting of the table, triggers a fetch
|
||||
*/
|
||||
onSort: (sort: string[][]) => void;
|
||||
opts: {
|
||||
/**
|
||||
* Date histogram aggregation config
|
||||
*/
|
||||
chartAggConfigs?: AggConfigs;
|
||||
/**
|
||||
* Client of uiSettings
|
||||
*/
|
||||
config: IUiSettingsClient;
|
||||
/**
|
||||
* Data plugin
|
||||
*/
|
||||
data: DataPublicPluginStart;
|
||||
fixedScroll: (el: HTMLElement) => void;
|
||||
/**
|
||||
* Data plugin filter manager
|
||||
*/
|
||||
filterManager: FilterManager;
|
||||
/**
|
||||
* List of available index patterns
|
||||
*/
|
||||
indexPatternList: Array<SavedObject<IndexPatternAttributes>>;
|
||||
/**
|
||||
* The number of documents that can be displayed in the table/grid
|
||||
*/
|
||||
sampleSize: number;
|
||||
/**
|
||||
* Current instance of SavedSearch
|
||||
*/
|
||||
savedSearch: SavedSearch;
|
||||
/**
|
||||
* Function to set the header menu
|
||||
*/
|
||||
setHeaderActionMenu: (menuMount: MountPoint | undefined) => void;
|
||||
/**
|
||||
* Timefield of the currently used index pattern
|
||||
*/
|
||||
timefield: string;
|
||||
/**
|
||||
* Function to set the current state
|
||||
*/
|
||||
setAppState: (state: Partial<AppState>) => void;
|
||||
};
|
||||
/**
|
||||
* Function to reset the current query
|
||||
*/
|
||||
resetQuery: () => void;
|
||||
/**
|
||||
* Current state of the actual query, one of 'uninitialized', 'loading' ,'ready', 'none'
|
||||
*/
|
||||
resultState: string;
|
||||
/**
|
||||
* Array of document of the recent successful search request
|
||||
*/
|
||||
rows: ElasticSearchHit[];
|
||||
/**
|
||||
* Instance of SearchSource, the high level search API
|
||||
*/
|
||||
searchSource: ISearchSource;
|
||||
/**
|
||||
* Function to change the current index pattern
|
||||
*/
|
||||
setIndexPattern: (id: string) => void;
|
||||
/**
|
||||
* Determines whether the user should be able to use the save query feature
|
||||
*/
|
||||
showSaveQuery: boolean;
|
||||
/**
|
||||
* Current app state of URL
|
||||
*/
|
||||
state: AppState;
|
||||
/**
|
||||
* Function to update the time filter
|
||||
*/
|
||||
timefilterUpdateHandler: (ranges: { from: number; to: number }) => void;
|
||||
/**
|
||||
* Currently selected time range
|
||||
*/
|
||||
timeRange?: { from: string; to: string };
|
||||
/**
|
||||
* Menu data of top navigation (New, save ...)
|
||||
*/
|
||||
topNavMenu: TopNavMenuData[];
|
||||
/**
|
||||
* Function to update the actual query
|
||||
*/
|
||||
updateQuery: (payload: { dateRange: TimeRange; query?: Query }, isUpdate?: boolean) => void;
|
||||
/**
|
||||
* Function to update the actual savedQuery id
|
||||
*/
|
||||
updateSavedQueryId: (savedQueryId?: string) => void;
|
||||
}
|
||||
|
||||
|
@ -114,7 +229,6 @@ export const SidebarMemoized = React.memo((props: DiscoverSidebarResponsiveProps
|
|||
));
|
||||
|
||||
export function DiscoverLegacy({
|
||||
addColumn,
|
||||
fetch,
|
||||
fetchCounter,
|
||||
fieldCounts,
|
||||
|
@ -123,6 +237,7 @@ export function DiscoverLegacy({
|
|||
hits,
|
||||
indexPattern,
|
||||
minimumVisibleRows,
|
||||
onAddColumn,
|
||||
onAddFilter,
|
||||
onChangeInterval,
|
||||
onMoveColumn,
|
||||
|
@ -192,7 +307,7 @@ export function DiscoverLegacy({
|
|||
fieldCounts={fieldCounts}
|
||||
hits={rows}
|
||||
indexPatternList={indexPatternList}
|
||||
onAddField={addColumn}
|
||||
onAddField={onAddColumn}
|
||||
onAddFilter={onAddFilter}
|
||||
onRemoveField={onRemoveColumn}
|
||||
selectedIndexPattern={searchSource && searchSource.getField('index')}
|
||||
|
@ -206,6 +321,8 @@ export function DiscoverLegacy({
|
|||
<EuiFlexItem grow={false}>
|
||||
<EuiButtonIcon
|
||||
iconType={isSidebarClosed ? 'menuRight' : 'menuLeft'}
|
||||
iconSize="m"
|
||||
size="s"
|
||||
onClick={() => setIsSidebarClosed(!isSidebarClosed)}
|
||||
data-test-subj="collapseSideBarButton"
|
||||
aria-controls="discover-sidebar"
|
||||
|
@ -335,7 +452,7 @@ export function DiscoverLegacy({
|
|||
sort={state.sort || []}
|
||||
searchDescription={opts.savedSearch.description}
|
||||
searchTitle={opts.savedSearch.lastSavedTitle}
|
||||
onAddColumn={addColumn}
|
||||
onAddColumn={onAddColumn}
|
||||
onFilter={onAddFilter}
|
||||
onMoveColumn={onMoveColumn}
|
||||
onRemoveColumn={onRemoveColumn}
|
||||
|
|
|
@ -6,6 +6,7 @@ exports[`Render <DocViewer/> with 3 different tabs 1`] = `
|
|||
>
|
||||
<EuiTabbedContent
|
||||
autoFocus="initial"
|
||||
size="s"
|
||||
tabs={
|
||||
Array [
|
||||
Object {
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
.kbnDocViewerTable {
|
||||
margin-top: $euiSizeS;
|
||||
@include euiBreakpoint('xs', 's') {
|
||||
table-layout: fixed;
|
||||
}
|
||||
|
|
|
@ -57,7 +57,7 @@ export function DocViewer(renderProps: DocViewRenderProps) {
|
|||
|
||||
return (
|
||||
<div className="kbnDocViewer">
|
||||
<EuiTabbedContent tabs={tabs} />
|
||||
<EuiTabbedContent size="s" tabs={tabs} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -31,7 +31,7 @@ exports[`FieldName renders a geo field 1`] = `
|
|||
</div>
|
||||
`;
|
||||
|
||||
exports[`FieldName renders a number field by providing a field record, useShortDots is set to false 1`] = `
|
||||
exports[`FieldName renders a number field by providing a field record 1`] = `
|
||||
<div
|
||||
class="euiFlexGroup euiFlexGroup--gutterSmall euiFlexGroup--alignItemsCenter euiFlexGroup--directionRow"
|
||||
>
|
||||
|
|
|
@ -27,7 +27,7 @@ test('FieldName renders a string field by providing fieldType and fieldName', ()
|
|||
expect(component).toMatchSnapshot();
|
||||
});
|
||||
|
||||
test('FieldName renders a number field by providing a field record, useShortDots is set to false', () => {
|
||||
test('FieldName renders a number field by providing a field record', () => {
|
||||
const component = render(<FieldName fieldName={'test.test.test'} fieldType={'number'} />);
|
||||
expect(component).toMatchSnapshot();
|
||||
});
|
||||
|
|
|
@ -56,7 +56,6 @@ function getComponent({
|
|||
}: {
|
||||
selected?: boolean;
|
||||
showDetails?: boolean;
|
||||
useShortDots?: boolean;
|
||||
field?: IndexPatternField;
|
||||
}) {
|
||||
const indexPattern = getStubIndexPattern(
|
||||
|
|
|
@ -19,51 +19,58 @@
|
|||
|
||||
import { groupFields } from './group_fields';
|
||||
import { getDefaultFieldFilter } from './field_filter';
|
||||
import { IndexPatternField } from '../../../../../../data/common/index_patterns/fields';
|
||||
|
||||
const fields = [
|
||||
{
|
||||
name: 'category',
|
||||
type: 'string',
|
||||
esTypes: ['text'],
|
||||
count: 1,
|
||||
scripted: false,
|
||||
searchable: true,
|
||||
aggregatable: true,
|
||||
readFromDocValues: true,
|
||||
},
|
||||
{
|
||||
name: 'currency',
|
||||
type: 'string',
|
||||
esTypes: ['keyword'],
|
||||
count: 0,
|
||||
scripted: false,
|
||||
searchable: true,
|
||||
aggregatable: true,
|
||||
readFromDocValues: true,
|
||||
},
|
||||
{
|
||||
name: 'customer_birth_date',
|
||||
type: 'date',
|
||||
esTypes: ['date'],
|
||||
count: 0,
|
||||
scripted: false,
|
||||
searchable: true,
|
||||
aggregatable: true,
|
||||
readFromDocValues: true,
|
||||
},
|
||||
];
|
||||
|
||||
const fieldCounts = {
|
||||
category: 1,
|
||||
currency: 1,
|
||||
customer_birth_date: 1,
|
||||
};
|
||||
|
||||
describe('group_fields', function () {
|
||||
it('should group fields in selected, popular, unpopular group', function () {
|
||||
const fields = [
|
||||
{
|
||||
name: 'category',
|
||||
type: 'string',
|
||||
esTypes: ['text'],
|
||||
count: 1,
|
||||
scripted: false,
|
||||
searchable: true,
|
||||
aggregatable: true,
|
||||
readFromDocValues: true,
|
||||
},
|
||||
{
|
||||
name: 'currency',
|
||||
type: 'string',
|
||||
esTypes: ['keyword'],
|
||||
count: 0,
|
||||
scripted: false,
|
||||
searchable: true,
|
||||
aggregatable: true,
|
||||
readFromDocValues: true,
|
||||
},
|
||||
{
|
||||
name: 'customer_birth_date',
|
||||
type: 'date',
|
||||
esTypes: ['date'],
|
||||
count: 0,
|
||||
scripted: false,
|
||||
searchable: true,
|
||||
aggregatable: true,
|
||||
readFromDocValues: true,
|
||||
},
|
||||
];
|
||||
|
||||
const fieldCounts = {
|
||||
category: 1,
|
||||
currency: 1,
|
||||
customer_birth_date: 1,
|
||||
};
|
||||
|
||||
const fieldFilterState = getDefaultFieldFilter();
|
||||
|
||||
const actual = groupFields(fields as any, ['currency'], 5, fieldCounts, fieldFilterState);
|
||||
const actual = groupFields(
|
||||
fields as IndexPatternField[],
|
||||
['currency'],
|
||||
5,
|
||||
fieldCounts,
|
||||
fieldFilterState
|
||||
);
|
||||
expect(actual).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"popular": Array [
|
||||
|
@ -111,4 +118,34 @@ describe('group_fields', function () {
|
|||
}
|
||||
`);
|
||||
});
|
||||
|
||||
it('should sort selected fields by columns order ', function () {
|
||||
const fieldFilterState = getDefaultFieldFilter();
|
||||
|
||||
const actual1 = groupFields(
|
||||
fields as IndexPatternField[],
|
||||
['customer_birth_date', 'currency', 'unknown'],
|
||||
5,
|
||||
fieldCounts,
|
||||
fieldFilterState
|
||||
);
|
||||
expect(actual1.selected.map((field) => field.name)).toEqual([
|
||||
'customer_birth_date',
|
||||
'currency',
|
||||
'unknown',
|
||||
]);
|
||||
|
||||
const actual2 = groupFields(
|
||||
fields as IndexPatternField[],
|
||||
['currency', 'customer_birth_date', 'unknown'],
|
||||
5,
|
||||
fieldCounts,
|
||||
fieldFilterState
|
||||
);
|
||||
expect(actual2.selected.map((field) => field.name)).toEqual([
|
||||
'currency',
|
||||
'customer_birth_date',
|
||||
'unknown',
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -70,6 +70,15 @@ export function groupFields(
|
|||
result.unpopular.push(field);
|
||||
}
|
||||
}
|
||||
// add columns, that are not part of the index pattern, to be removeable
|
||||
for (const column of columns) {
|
||||
if (!result.selected.find((field) => field.name === column)) {
|
||||
result.selected.push({ name: column, displayName: column } as IndexPatternField);
|
||||
}
|
||||
}
|
||||
result.selected.sort((a, b) => {
|
||||
return columns.indexOf(a.name) - columns.indexOf(b.name);
|
||||
});
|
||||
|
||||
return result;
|
||||
}
|
||||
|
|
|
@ -36,6 +36,7 @@ import {
|
|||
import { Container, Embeddable } from '../../../../embeddable/public';
|
||||
import * as columnActions from '../angular/doc_table/actions/columns';
|
||||
import searchTemplate from './search_template.html';
|
||||
import searchTemplateGrid from './search_template_datagrid.html';
|
||||
import { ISearchEmbeddable, SearchInput, SearchOutput } from './types';
|
||||
import { SortOrder } from '../angular/doc_table/components/table_header/helpers';
|
||||
import { getSortForSearchSource } from '../angular/doc_table';
|
||||
|
@ -49,23 +50,29 @@ import {
|
|||
import { SEARCH_EMBEDDABLE_TYPE } from './constants';
|
||||
import { SavedSearch } from '../..';
|
||||
import { SAMPLE_SIZE_SETTING, SORT_DEFAULT_ORDER_SETTING } from '../../../common';
|
||||
import { DiscoverGridSettings } from '../components/discover_grid/types';
|
||||
import { DiscoverServices } from '../../build_services';
|
||||
import { ElasticSearchHit } from '../doc_views/doc_views_types';
|
||||
import { getDefaultSort } from '../angular/doc_table/lib/get_default_sort';
|
||||
|
||||
interface SearchScope extends ng.IScope {
|
||||
columns?: string[];
|
||||
settings?: DiscoverGridSettings;
|
||||
description?: string;
|
||||
sort?: SortOrder[];
|
||||
sharedItemTitle?: string;
|
||||
inspectorAdapters?: Adapters;
|
||||
setSortOrder?: (sortPair: SortOrder[]) => void;
|
||||
setColumns?: (columns: string[]) => void;
|
||||
removeColumn?: (column: string) => void;
|
||||
addColumn?: (column: string) => void;
|
||||
moveColumn?: (column: string, index: number) => void;
|
||||
filter?: (field: IFieldType, value: string[], operator: string) => void;
|
||||
hits?: any[];
|
||||
hits?: ElasticSearchHit[];
|
||||
indexPattern?: IndexPattern;
|
||||
totalHitCount?: number;
|
||||
isLoading?: boolean;
|
||||
showTimeCol?: boolean;
|
||||
}
|
||||
|
||||
interface SearchEmbeddableConfig {
|
||||
|
@ -77,6 +84,7 @@ interface SearchEmbeddableConfig {
|
|||
indexPatterns?: IndexPattern[];
|
||||
editable: boolean;
|
||||
filterManager: FilterManager;
|
||||
services: DiscoverServices;
|
||||
}
|
||||
|
||||
export class SearchEmbeddable
|
||||
|
@ -95,6 +103,7 @@ export class SearchEmbeddable
|
|||
public readonly type = SEARCH_EMBEDDABLE_TYPE;
|
||||
private filterManager: FilterManager;
|
||||
private abortController?: AbortController;
|
||||
private services: DiscoverServices;
|
||||
|
||||
private prevTimeRange?: TimeRange;
|
||||
private prevFilters?: Filter[];
|
||||
|
@ -111,6 +120,7 @@ export class SearchEmbeddable
|
|||
indexPatterns,
|
||||
editable,
|
||||
filterManager,
|
||||
services,
|
||||
}: SearchEmbeddableConfig,
|
||||
initialInput: SearchInput,
|
||||
private readonly executeTriggerActions: UiActionsStart['executeTriggerActions'],
|
||||
|
@ -128,7 +138,7 @@ export class SearchEmbeddable
|
|||
},
|
||||
parent
|
||||
);
|
||||
|
||||
this.services = services;
|
||||
this.filterManager = filterManager;
|
||||
this.savedSearch = savedSearch;
|
||||
this.$rootScope = $rootScope;
|
||||
|
@ -138,8 +148,8 @@ export class SearchEmbeddable
|
|||
};
|
||||
this.initializeSearchScope();
|
||||
|
||||
this.autoRefreshFetchSubscription = getServices()
|
||||
.timefilter.getAutoRefreshFetch$()
|
||||
this.autoRefreshFetchSubscription = this.services.timefilter
|
||||
.getAutoRefreshFetch$()
|
||||
.subscribe(this.fetch);
|
||||
|
||||
this.subscription = this.getUpdated$().subscribe(() => {
|
||||
|
@ -167,7 +177,9 @@ export class SearchEmbeddable
|
|||
if (!this.searchScope) {
|
||||
throw new Error('Search scope not defined');
|
||||
}
|
||||
this.searchInstance = this.$compile(searchTemplate)(this.searchScope);
|
||||
this.searchInstance = this.$compile(
|
||||
this.services.uiSettings.get('doc_table:legacy', true) ? searchTemplate : searchTemplateGrid
|
||||
)(this.searchScope);
|
||||
const rootNode = angular.element(domNode);
|
||||
rootNode.append(this.searchInstance);
|
||||
|
||||
|
@ -250,6 +262,15 @@ export class SearchEmbeddable
|
|||
this.updateInput({ columns });
|
||||
};
|
||||
|
||||
searchScope.setColumns = (columns: string[]) => {
|
||||
this.updateInput({ columns });
|
||||
};
|
||||
|
||||
if (this.savedSearch.grid) {
|
||||
searchScope.settings = this.savedSearch.grid;
|
||||
}
|
||||
searchScope.showTimeCol = !this.services.uiSettings.get('doc_table:hideTimeColumn', false);
|
||||
|
||||
searchScope.filter = async (field, value, operator) => {
|
||||
let filters = esFilters.generateFilters(
|
||||
this.filterManager,
|
||||
|
@ -286,13 +307,13 @@ export class SearchEmbeddable
|
|||
if (this.abortController) this.abortController.abort();
|
||||
this.abortController = new AbortController();
|
||||
|
||||
searchSource.setField('size', getServices().uiSettings.get(SAMPLE_SIZE_SETTING));
|
||||
searchSource.setField('size', this.services.uiSettings.get(SAMPLE_SIZE_SETTING));
|
||||
searchSource.setField(
|
||||
'sort',
|
||||
getSortForSearchSource(
|
||||
this.searchScope.sort,
|
||||
this.searchScope.indexPattern,
|
||||
getServices().uiSettings.get(SORT_DEFAULT_ORDER_SETTING)
|
||||
this.services.uiSettings.get(SORT_DEFAULT_ORDER_SETTING)
|
||||
)
|
||||
);
|
||||
|
||||
|
|
|
@ -103,6 +103,7 @@ export class SearchEmbeddableFactory
|
|||
filterManager,
|
||||
editable: getServices().capabilities.discover.save as boolean,
|
||||
indexPatterns: indexPattern ? [indexPattern] : [],
|
||||
services: getServices(),
|
||||
},
|
||||
input,
|
||||
executeTriggerActions,
|
||||
|
|
|
@ -1,20 +1,20 @@
|
|||
<doc-table
|
||||
sorting="sort"
|
||||
columns="columns"
|
||||
data-shared-item
|
||||
data-title="{{sharedItemTitle}}"
|
||||
data-description="{{description}}"
|
||||
render-complete
|
||||
class="panel-content"
|
||||
columns="columns"
|
||||
data-description="{{description}}"
|
||||
data-shared-item
|
||||
data-test-subj="embeddedSavedSearchDocTable"
|
||||
data-title="{{sharedItemTitle}}"
|
||||
filter="filter"
|
||||
hits="hits"
|
||||
index-pattern="indexPattern"
|
||||
is-loading="isLoading"
|
||||
on-add-column="addColumn"
|
||||
on-change-sort-order="setSortOrder"
|
||||
on-move-column="moveColumn"
|
||||
on-remove-column="removeColumn"
|
||||
data-test-subj="embeddedSavedSearchDocTable"
|
||||
hits="hits"
|
||||
index-pattern="indexPattern"
|
||||
render-complete
|
||||
sorting="sort"
|
||||
total-hit-count="totalHitCount"
|
||||
is-loading="isLoading"
|
||||
>
|
||||
</doc-table>
|
||||
|
|
|
@ -0,0 +1,19 @@
|
|||
<discover-grid
|
||||
class="dscDiscoverGrid"
|
||||
columns="columns"
|
||||
data-description="{{description}}"
|
||||
data-shared-item
|
||||
data-title="{{sharedItemTitle}}"
|
||||
index-pattern="indexPattern"
|
||||
on-add-column="addColumn"
|
||||
on-filter="filter"
|
||||
on-remove-column="removeColumn"
|
||||
on-set-columns="setColumns"
|
||||
on-sort="setSortOrder"
|
||||
render-complete
|
||||
rows="hits"
|
||||
sample-size="500"
|
||||
settings="settings"
|
||||
show-time-col="showTimeCol"
|
||||
sort="sort"
|
||||
></discover-grid>
|
|
@ -51,7 +51,7 @@ describe('getSharingData', () => {
|
|||
"searchRequest": Object {
|
||||
"body": Object {
|
||||
"_source": Object {},
|
||||
"fields": undefined,
|
||||
"fields": Array [],
|
||||
"query": Object {
|
||||
"bool": Object {
|
||||
"filter": Array [],
|
||||
|
@ -68,7 +68,9 @@ describe('getSharingData', () => {
|
|||
},
|
||||
},
|
||||
],
|
||||
"stored_fields": undefined,
|
||||
"stored_fields": Array [
|
||||
"*",
|
||||
],
|
||||
},
|
||||
"index": "the-index-pattern-title",
|
||||
},
|
||||
|
|
|
@ -53,6 +53,9 @@ export async function persistSavedSearch(
|
|||
|
||||
savedSearch.columns = state.columns || [];
|
||||
savedSearch.sort = (state.sort as SortOrder[]) || [];
|
||||
if (state.grid) {
|
||||
savedSearch.grid = state.grid;
|
||||
}
|
||||
|
||||
try {
|
||||
const id = await savedSearch.save(saveOptions);
|
||||
|
|
|
@ -41,6 +41,7 @@ import { createTableRowDirective } from './application/angular/doc_table/compone
|
|||
import { createPagerFactory } from './application/angular/doc_table/lib/pager/pager_factory';
|
||||
import { createInfiniteScrollDirective } from './application/angular/doc_table/infinite_scroll';
|
||||
import { createDocViewerDirective } from './application/angular/doc_viewer';
|
||||
import { createDiscoverGridDirective } from './application/components/create_discover_grid_directive';
|
||||
import { createRenderCompleteDirective } from './application/angular/directives/render_complete';
|
||||
import {
|
||||
initAngularBootstrap,
|
||||
|
@ -55,6 +56,8 @@ import {
|
|||
import { DiscoverStartPlugins } from './plugin';
|
||||
import { getScopedHistory } from './kibana_services';
|
||||
import { createDiscoverLegacyDirective } from './application/components/create_discover_legacy_directive';
|
||||
import { createDiscoverDirective } from './application/components/create_discover_directive';
|
||||
|
||||
/**
|
||||
* returns the main inner angular module, it contains all the parts of Angular Discover
|
||||
* needs to render, so in the end the current 'kibana' angular module is no longer necessary
|
||||
|
@ -136,7 +139,8 @@ export function initializeInnerAngularModule(
|
|||
.config(watchMultiDecorator)
|
||||
.run(registerListenEventListener)
|
||||
.directive('renderComplete', createRenderCompleteDirective)
|
||||
.directive('discoverLegacy', createDiscoverLegacyDirective);
|
||||
.directive('discoverLegacy', createDiscoverLegacyDirective)
|
||||
.directive('discover', createDiscoverDirective);
|
||||
}
|
||||
|
||||
function createLocalPromiseModule() {
|
||||
|
@ -188,6 +192,7 @@ function createDocTableModule() {
|
|||
.directive('kbnTableRow', createTableRowDirective)
|
||||
.directive('toolBarPagerButtons', createToolBarPagerButtonsDirective)
|
||||
.directive('kbnInfiniteScroll', createInfiniteScrollDirective)
|
||||
.directive('discoverGrid', createDiscoverGridDirective)
|
||||
.directive('docViewer', createDocViewerDirective)
|
||||
.directive('contextAppLegacy', createContextAppLegacy);
|
||||
}
|
||||
|
|
|
@ -26,6 +26,7 @@ export function createSavedSearchClass(savedObjects: SavedObjectsStart) {
|
|||
description: 'text',
|
||||
hits: 'integer',
|
||||
columns: 'keyword',
|
||||
grid: 'object',
|
||||
sort: 'keyword',
|
||||
version: 'integer',
|
||||
};
|
||||
|
@ -45,6 +46,7 @@ export function createSavedSearchClass(savedObjects: SavedObjectsStart) {
|
|||
description: 'text',
|
||||
hits: 'integer',
|
||||
columns: 'keyword',
|
||||
grid: 'object',
|
||||
sort: 'keyword',
|
||||
version: 'integer',
|
||||
},
|
||||
|
|
|
@ -19,6 +19,7 @@
|
|||
|
||||
import { SearchSource } from '../../../data/public';
|
||||
import { SavedObjectSaveOpts } from '../../../saved_objects/public';
|
||||
import { DiscoverGridSettings } from '../application/components/discover_grid/types';
|
||||
|
||||
export type SortOrder = [string, string];
|
||||
export interface SavedSearch {
|
||||
|
@ -28,6 +29,7 @@ export interface SavedSearch {
|
|||
description?: string;
|
||||
columns: string[];
|
||||
sort: SortOrder[];
|
||||
grid: DiscoverGridSettings;
|
||||
destroy: () => void;
|
||||
save: (saveOptions: SavedObjectSaveOpts) => Promise<string>;
|
||||
lastSavedTitle?: string;
|
||||
|
|
|
@ -53,6 +53,7 @@ export const searchSavedObjectType: SavedObjectsType = {
|
|||
},
|
||||
sort: { type: 'keyword', index: false, doc_values: false },
|
||||
title: { type: 'text' },
|
||||
grid: { type: 'object', enabled: false },
|
||||
version: { type: 'integer' },
|
||||
},
|
||||
},
|
||||
|
|
|
@ -33,6 +33,7 @@ import {
|
|||
CONTEXT_DEFAULT_SIZE_SETTING,
|
||||
CONTEXT_STEP_SETTING,
|
||||
CONTEXT_TIE_BREAKER_FIELDS_SETTING,
|
||||
DOC_TABLE_LEGACY,
|
||||
MODIFY_COLUMNS_ON_SWITCH,
|
||||
} from '../common';
|
||||
|
||||
|
@ -165,6 +166,23 @@ export const uiSettings: Record<string, UiSettingsParams> = {
|
|||
category: ['discover'],
|
||||
schema: schema.arrayOf(schema.string()),
|
||||
},
|
||||
[DOC_TABLE_LEGACY]: {
|
||||
name: i18n.translate('discover.advancedSettings.docTableVersionName', {
|
||||
defaultMessage: 'Use legacy table',
|
||||
}),
|
||||
value: true,
|
||||
description: i18n.translate('discover.advancedSettings.docTableVersionDescription', {
|
||||
defaultMessage:
|
||||
'Discover uses a new table layout that includes better data sorting, drag-and-drop columns, and a full screen ' +
|
||||
'view. Enable this option if you prefer to fall back to the legacy table.',
|
||||
}),
|
||||
category: ['discover'],
|
||||
schema: schema.boolean(),
|
||||
metric: {
|
||||
type: METRIC_TYPE.CLICK,
|
||||
name: 'discover:useLegacyDataGrid',
|
||||
},
|
||||
},
|
||||
[MODIFY_COLUMNS_ON_SWITCH]: {
|
||||
name: i18n.translate('discover.advancedSettings.discover.modifyColumnsOnSwitchTitle', {
|
||||
defaultMessage: 'Modify columns when changing index patterns',
|
||||
|
|
60
test/functional/apps/dashboard/embeddable_data_grid.ts
Normal file
60
test/functional/apps/dashboard/embeddable_data_grid.ts
Normal file
|
@ -0,0 +1,60 @@
|
|||
/*
|
||||
* Licensed to Elasticsearch B.V. under one or more contributor
|
||||
* license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright
|
||||
* ownership. Elasticsearch B.V. licenses this file to you under
|
||||
* the Apache License, Version 2.0 (the "License"); you may
|
||||
* not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
import expect from '@kbn/expect';
|
||||
import { FtrProviderContext } from '../../ftr_provider_context';
|
||||
|
||||
export default function ({ getService, getPageObjects }: FtrProviderContext) {
|
||||
const dashboardAddPanel = getService('dashboardAddPanel');
|
||||
const filterBar = getService('filterBar');
|
||||
const esArchiver = getService('esArchiver');
|
||||
const kibanaServer = getService('kibanaServer');
|
||||
const find = getService('find');
|
||||
const PageObjects = getPageObjects(['common', 'dashboard', 'header', 'timePicker', 'discover']);
|
||||
|
||||
describe('dashboard embeddable data grid', () => {
|
||||
before(async () => {
|
||||
await esArchiver.loadIfNeeded('logstash_functional');
|
||||
await esArchiver.loadIfNeeded('dashboard/current/data');
|
||||
await esArchiver.loadIfNeeded('dashboard/current/kibana');
|
||||
await kibanaServer.uiSettings.replace({
|
||||
defaultIndex: '0bf35f60-3dc9-11e8-8660-4d65aa086b3c',
|
||||
'doc_table:legacy': false,
|
||||
});
|
||||
await PageObjects.common.navigateToApp('dashboard');
|
||||
await filterBar.ensureFieldEditorModalIsClosed();
|
||||
await PageObjects.dashboard.gotoDashboardLandingPage();
|
||||
await PageObjects.dashboard.clickNewDashboard();
|
||||
await PageObjects.timePicker.setDefaultDataRange();
|
||||
});
|
||||
|
||||
describe('saved search filters', function () {
|
||||
it('are added when a cell filter is clicked', async function () {
|
||||
await dashboardAddPanel.addSavedSearch('Rendering-Test:-saved-search');
|
||||
await find.clickByCssSelector(`[role="gridcell"]:nth-child(2)`);
|
||||
await find.clickByCssSelector(`[data-test-subj="filterOutButton"]`);
|
||||
await PageObjects.header.waitUntilLoadingHasFinished();
|
||||
await find.clickByCssSelector(`[role="gridcell"]:nth-child(2)`);
|
||||
await find.clickByCssSelector(`[data-test-subj="filterForButton"]`);
|
||||
const filterCount = await filterBar.getFilterCount();
|
||||
expect(filterCount).to.equal(2);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
|
@ -54,6 +54,7 @@ export default function ({ getService, loadTestFile }: FtrProviderContext) {
|
|||
loadTestFile(require.resolve('./empty_dashboard'));
|
||||
loadTestFile(require.resolve('./url_field_formatter'));
|
||||
loadTestFile(require.resolve('./embeddable_rendering'));
|
||||
loadTestFile(require.resolve('./embeddable_data_grid'));
|
||||
loadTestFile(require.resolve('./create_and_add_embeddables'));
|
||||
loadTestFile(require.resolve('./edit_embeddable_redirects'));
|
||||
loadTestFile(require.resolve('./edit_visualizations'));
|
||||
|
|
67
test/functional/apps/discover/_data_grid.ts
Normal file
67
test/functional/apps/discover/_data_grid.ts
Normal file
|
@ -0,0 +1,67 @@
|
|||
/*
|
||||
* Licensed to Elasticsearch B.V. under one or more contributor
|
||||
* license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright
|
||||
* ownership. Elasticsearch B.V. licenses this file to you under
|
||||
* the Apache License, Version 2.0 (the "License"); you may
|
||||
* not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
import expect from '@kbn/expect';
|
||||
|
||||
export default function ({
|
||||
getService,
|
||||
getPageObjects,
|
||||
}: {
|
||||
getService: (service: string) => any;
|
||||
getPageObjects: (pageObjects: string[]) => any;
|
||||
}) {
|
||||
describe('discover data grid tests', function describeDiscoverDataGrid() {
|
||||
const esArchiver = getService('esArchiver');
|
||||
const PageObjects = getPageObjects(['common', 'discover', 'timePicker']);
|
||||
const kibanaServer = getService('kibanaServer');
|
||||
const defaultSettings = { defaultIndex: 'logstash-*', 'doc_table:legacy': false };
|
||||
const testSubjects = getService('testSubjects');
|
||||
|
||||
before(async function () {
|
||||
await esArchiver.load('discover');
|
||||
await esArchiver.loadIfNeeded('logstash_functional');
|
||||
await kibanaServer.uiSettings.replace(defaultSettings);
|
||||
await PageObjects.common.navigateToApp('discover');
|
||||
await PageObjects.timePicker.setDefaultAbsoluteRange();
|
||||
});
|
||||
|
||||
after(async function () {
|
||||
await kibanaServer.uiSettings.replace({ 'doc_table:legacy': true });
|
||||
});
|
||||
|
||||
it('can add fields to the table', async function () {
|
||||
const getTitles = async () =>
|
||||
(await testSubjects.getVisibleText('dataGridHeader')).replace(/\s|\r?\n|\r/g, ' ');
|
||||
|
||||
expect(await getTitles()).to.be('Time (@timestamp) _source');
|
||||
|
||||
await PageObjects.discover.clickFieldListItemAdd('bytes');
|
||||
expect(await getTitles()).to.be('Time (@timestamp) bytes');
|
||||
|
||||
await PageObjects.discover.clickFieldListItemAdd('agent');
|
||||
expect(await getTitles()).to.be('Time (@timestamp) bytes agent');
|
||||
|
||||
await PageObjects.discover.clickFieldListItemAdd('bytes');
|
||||
expect(await getTitles()).to.be('Time (@timestamp) agent');
|
||||
|
||||
await PageObjects.discover.clickFieldListItemAdd('agent');
|
||||
expect(await getTitles()).to.be('Time (@timestamp) _source');
|
||||
});
|
||||
});
|
||||
}
|
91
test/functional/apps/discover/_data_grid_context.ts
Normal file
91
test/functional/apps/discover/_data_grid_context.ts
Normal file
|
@ -0,0 +1,91 @@
|
|||
/*
|
||||
* Licensed to Elasticsearch B.V. under one or more contributor
|
||||
* license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright
|
||||
* ownership. Elasticsearch B.V. licenses this file to you under
|
||||
* the Apache License, Version 2.0 (the "License"); you may
|
||||
* not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import expect from '@kbn/expect';
|
||||
import { FtrProviderContext } from '../../ftr_provider_context';
|
||||
|
||||
const TEST_COLUMN_NAMES = ['@message'];
|
||||
const TEST_FILTER_COLUMN_NAMES = [
|
||||
['extension', 'jpg'],
|
||||
['geo.src', 'IN'],
|
||||
];
|
||||
|
||||
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', 'timePicker', 'settings']);
|
||||
const defaultSettings = { defaultIndex: 'logstash-*', 'doc_table:legacy': false };
|
||||
const kibanaServer = getService('kibanaServer');
|
||||
const esArchiver = getService('esArchiver');
|
||||
|
||||
describe('discover data grid context tests', () => {
|
||||
before(async () => {
|
||||
await esArchiver.loadIfNeeded('logstash_functional');
|
||||
await PageObjects.timePicker.setDefaultAbsoluteRangeViaUiSettings();
|
||||
await kibanaServer.uiSettings.update(defaultSettings);
|
||||
await PageObjects.common.navigateToApp('discover');
|
||||
|
||||
for (const columnName of TEST_COLUMN_NAMES) {
|
||||
await PageObjects.discover.clickFieldListItemAdd(columnName);
|
||||
}
|
||||
|
||||
for (const [columnName, value] of TEST_FILTER_COLUMN_NAMES) {
|
||||
await PageObjects.discover.clickFieldListItem(columnName);
|
||||
await PageObjects.discover.clickFieldListPlusFilter(columnName, value);
|
||||
}
|
||||
});
|
||||
after(async () => {
|
||||
await PageObjects.timePicker.resetDefaultAbsoluteRangeViaUiSettings();
|
||||
});
|
||||
|
||||
it('should open the context view with the selected document as anchor', async () => {
|
||||
// check the anchor timestamp in the context view
|
||||
await retry.waitFor('selected document timestamp matches anchor timestamp ', async () => {
|
||||
// get the timestamp of the first row
|
||||
const discoverFields = await dataGrid.getFields();
|
||||
const firstTimestamp = discoverFields[0][0];
|
||||
|
||||
// navigate to the context view
|
||||
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 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]);
|
||||
});
|
||||
|
||||
it('should open the context view with the filters disabled', async () => {
|
||||
let disabledFilterCounter = 0;
|
||||
for (const [columnName, value] of TEST_FILTER_COLUMN_NAMES) {
|
||||
if (await filterBar.hasFilter(columnName, value, false)) {
|
||||
disabledFilterCounter++;
|
||||
}
|
||||
}
|
||||
expect(disabledFilterCounter).to.be(TEST_FILTER_COLUMN_NAMES.length);
|
||||
});
|
||||
});
|
||||
}
|
91
test/functional/apps/discover/_data_grid_doc_navigation.ts
Normal file
91
test/functional/apps/discover/_data_grid_doc_navigation.ts
Normal file
|
@ -0,0 +1,91 @@
|
|||
/*
|
||||
* Licensed to Elasticsearch B.V. under one or more contributor
|
||||
* license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright
|
||||
* ownership. Elasticsearch B.V. licenses this file to you under
|
||||
* the Apache License, Version 2.0 (the "License"); you may
|
||||
* not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
import expect from '@kbn/expect';
|
||||
import { FtrProviderContext } from '../../ftr_provider_context';
|
||||
|
||||
export default function ({ getService, getPageObjects }: FtrProviderContext) {
|
||||
const filterBar = getService('filterBar');
|
||||
const dataGrid = getService('dataGrid');
|
||||
const testSubjects = getService('testSubjects');
|
||||
const PageObjects = getPageObjects(['common', 'discover', 'timePicker', 'context']);
|
||||
const esArchiver = getService('esArchiver');
|
||||
const retry = getService('retry');
|
||||
const kibanaServer = getService('kibanaServer');
|
||||
const defaultSettings = { defaultIndex: 'logstash-*', 'doc_table:legacy': false };
|
||||
|
||||
describe('discover data grid doc link', function () {
|
||||
beforeEach(async function () {
|
||||
await esArchiver.loadIfNeeded('logstash_functional');
|
||||
await esArchiver.loadIfNeeded('discover');
|
||||
await PageObjects.timePicker.setDefaultAbsoluteRangeViaUiSettings();
|
||||
await kibanaServer.uiSettings.update(defaultSettings);
|
||||
await PageObjects.common.navigateToApp('discover');
|
||||
});
|
||||
|
||||
it('should open the doc view of the selected document', async function () {
|
||||
// navigate to the doc view
|
||||
await dataGrid.clickRowToggle({ rowIndex: 0 });
|
||||
|
||||
// click the open action
|
||||
await retry.try(async () => {
|
||||
const rowActions = await dataGrid.getRowActions({ rowIndex: 0 });
|
||||
if (!rowActions.length) {
|
||||
throw new Error('row actions empty, trying again');
|
||||
}
|
||||
await rowActions[0].click();
|
||||
});
|
||||
|
||||
const hasDocHit = await testSubjects.exists('doc-hit');
|
||||
expect(hasDocHit).to.be(true);
|
||||
});
|
||||
|
||||
it('add filter should create an exists filter if value is null (#7189)', async function () {
|
||||
await PageObjects.discover.waitUntilSearchingHasFinished();
|
||||
// Filter special document
|
||||
await filterBar.addFilter('agent', 'is', 'Missing/Fields');
|
||||
await PageObjects.discover.waitUntilSearchingHasFinished();
|
||||
|
||||
await retry.try(async () => {
|
||||
// navigate to the doc view
|
||||
await dataGrid.clickRowToggle({ rowIndex: 0 });
|
||||
|
||||
const details = await dataGrid.getDetailsRow();
|
||||
await dataGrid.addInclusiveFilter(details, 'referer');
|
||||
await PageObjects.discover.waitUntilSearchingHasFinished();
|
||||
|
||||
const hasInclusiveFilter = await filterBar.hasFilter(
|
||||
'referer',
|
||||
'exists',
|
||||
true,
|
||||
false,
|
||||
true
|
||||
);
|
||||
expect(hasInclusiveFilter).to.be(true);
|
||||
|
||||
await dataGrid.clickRowToggle({ rowIndex: 0 });
|
||||
const detailsExcluding = await dataGrid.getDetailsRow();
|
||||
await dataGrid.removeInclusiveFilter(detailsExcluding, 'referer');
|
||||
await PageObjects.discover.waitUntilSearchingHasFinished();
|
||||
const hasExcludeFilter = await filterBar.hasFilter('referer', 'exists', true, false, false);
|
||||
expect(hasExcludeFilter).to.be(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
132
test/functional/apps/discover/_data_grid_doc_table.ts
Normal file
132
test/functional/apps/discover/_data_grid_doc_table.ts
Normal file
|
@ -0,0 +1,132 @@
|
|||
/*
|
||||
* Licensed to Elasticsearch B.V. under one or more contributor
|
||||
* license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright
|
||||
* ownership. Elasticsearch B.V. licenses this file to you under
|
||||
* the Apache License, Version 2.0 (the "License"); you may
|
||||
* not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
import expect from '@kbn/expect';
|
||||
import { FtrProviderContext } from '../../ftr_provider_context';
|
||||
|
||||
export default function ({ getService, getPageObjects }: FtrProviderContext) {
|
||||
const dataGrid = getService('dataGrid');
|
||||
const log = getService('log');
|
||||
const retry = getService('retry');
|
||||
const esArchiver = getService('esArchiver');
|
||||
const kibanaServer = getService('kibanaServer');
|
||||
const PageObjects = getPageObjects(['common', 'discover', 'header', 'timePicker']);
|
||||
const defaultSettings = {
|
||||
defaultIndex: 'logstash-*',
|
||||
'doc_table:legacy': false,
|
||||
};
|
||||
|
||||
describe('discover data grid doc table', function describeIndexTests() {
|
||||
const defaultRowsLimit = 25;
|
||||
|
||||
before(async function () {
|
||||
log.debug('load kibana index with default index pattern');
|
||||
await esArchiver.load('discover');
|
||||
await esArchiver.loadIfNeeded('logstash_functional');
|
||||
await kibanaServer.uiSettings.replace(defaultSettings);
|
||||
await PageObjects.timePicker.setDefaultAbsoluteRangeViaUiSettings();
|
||||
await PageObjects.common.navigateToApp('discover');
|
||||
});
|
||||
|
||||
it('should show the first 50 rows by default', async function () {
|
||||
// with the default range the number of hits is ~14000
|
||||
const rows = await dataGrid.getDocTableRows();
|
||||
expect(rows.length).to.be(defaultRowsLimit);
|
||||
});
|
||||
|
||||
it('should refresh the table content when changing time window', async function () {
|
||||
const initialRows = await dataGrid.getDocTableRows();
|
||||
|
||||
const fromTime = 'Sep 20, 2015 @ 23:00:00.000';
|
||||
const toTime = 'Sep 20, 2015 @ 23:14:00.000';
|
||||
|
||||
await PageObjects.timePicker.setAbsoluteRange(fromTime, toTime);
|
||||
await PageObjects.discover.waitUntilSearchingHasFinished();
|
||||
|
||||
const finalRows = await PageObjects.discover.getDocTableRows();
|
||||
expect(finalRows.length).to.be.below(initialRows.length);
|
||||
await PageObjects.timePicker.setDefaultAbsoluteRange();
|
||||
});
|
||||
|
||||
describe('expand a document row', function () {
|
||||
const rowToInspect = 1;
|
||||
|
||||
it('should expand the detail row when the toggle arrow is clicked', async function () {
|
||||
await retry.try(async function () {
|
||||
await dataGrid.clickRowToggle({ isAnchorRow: false, rowIndex: rowToInspect - 1 });
|
||||
const detailsEl = await dataGrid.getDetailsRows();
|
||||
const defaultMessageEl = await detailsEl[0].findByTestSubject('docTableRowDetailsTitle');
|
||||
expect(defaultMessageEl).to.be.ok();
|
||||
await dataGrid.closeFlyout();
|
||||
});
|
||||
});
|
||||
|
||||
it('should show the detail panel actions', async function () {
|
||||
await retry.try(async function () {
|
||||
await dataGrid.clickRowToggle({ isAnchorRow: false, rowIndex: rowToInspect - 1 });
|
||||
const [surroundingActionEl, singleActionEl] = await dataGrid.getRowActions({
|
||||
isAnchorRow: false,
|
||||
rowIndex: rowToInspect - 1,
|
||||
});
|
||||
expect(surroundingActionEl).to.be.ok();
|
||||
expect(singleActionEl).to.be.ok();
|
||||
await dataGrid.closeFlyout();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('add and remove columns', function () {
|
||||
const extraColumns = ['phpmemory', 'ip'];
|
||||
|
||||
afterEach(async function () {
|
||||
for (const column of extraColumns) {
|
||||
await PageObjects.discover.clickFieldListItemRemove(column);
|
||||
await PageObjects.header.waitUntilLoadingHasFinished();
|
||||
}
|
||||
});
|
||||
|
||||
it('should add more columns to the table', async function () {
|
||||
for (const column of extraColumns) {
|
||||
await PageObjects.discover.clearFieldSearchInput();
|
||||
await PageObjects.discover.findFieldByName(column);
|
||||
await PageObjects.discover.clickFieldListItemAdd(column);
|
||||
await PageObjects.header.waitUntilLoadingHasFinished();
|
||||
// test the header now
|
||||
const header = await dataGrid.getHeaderFields();
|
||||
expect(header.join(' ')).to.have.string(column);
|
||||
}
|
||||
});
|
||||
|
||||
it('should remove columns from the table', async function () {
|
||||
for (const column of extraColumns) {
|
||||
await PageObjects.discover.clearFieldSearchInput();
|
||||
await PageObjects.discover.findFieldByName(column);
|
||||
await PageObjects.discover.clickFieldListItemAdd(column);
|
||||
await PageObjects.header.waitUntilLoadingHasFinished();
|
||||
}
|
||||
// remove the second column
|
||||
await PageObjects.discover.clickFieldListItemAdd(extraColumns[1]);
|
||||
await PageObjects.header.waitUntilLoadingHasFinished();
|
||||
// test that the second column is no longer there
|
||||
const header = await dataGrid.getHeaderFields();
|
||||
expect(header.join(' ')).to.not.have.string(extraColumns[1]);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
99
test/functional/apps/discover/_data_grid_field_data.ts
Normal file
99
test/functional/apps/discover/_data_grid_field_data.ts
Normal file
|
@ -0,0 +1,99 @@
|
|||
/*
|
||||
* Licensed to Elasticsearch B.V. under one or more contributor
|
||||
* license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright
|
||||
* ownership. Elasticsearch B.V. licenses this file to you under
|
||||
* the Apache License, Version 2.0 (the "License"); you may
|
||||
* not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
import expect from '@kbn/expect';
|
||||
import { FtrProviderContext } from '../../ftr_provider_context';
|
||||
|
||||
export default function ({ getService, getPageObjects }: FtrProviderContext) {
|
||||
const retry = getService('retry');
|
||||
const esArchiver = getService('esArchiver');
|
||||
const kibanaServer = getService('kibanaServer');
|
||||
const toasts = getService('toasts');
|
||||
const queryBar = getService('queryBar');
|
||||
const PageObjects = getPageObjects(['common', 'header', 'discover', 'visualize', 'timePicker']);
|
||||
const defaultSettings = { defaultIndex: 'logstash-*', 'doc_table:legacy': false };
|
||||
const dataGrid = getService('dataGrid');
|
||||
|
||||
describe('discover data grid field data tests', function describeIndexTests() {
|
||||
this.tags('includeFirefox');
|
||||
before(async function () {
|
||||
await esArchiver.load('discover');
|
||||
await esArchiver.loadIfNeeded('logstash_functional');
|
||||
await PageObjects.timePicker.setDefaultAbsoluteRangeViaUiSettings();
|
||||
await kibanaServer.uiSettings.update(defaultSettings);
|
||||
await PageObjects.common.navigateToApp('discover');
|
||||
});
|
||||
describe('field data', function () {
|
||||
it('search php should show the correct hit count', async function () {
|
||||
const expectedHitCount = '445';
|
||||
await retry.try(async function () {
|
||||
await queryBar.setQuery('php');
|
||||
await queryBar.submitQuery();
|
||||
const hitCount = await PageObjects.discover.getHitCount();
|
||||
expect(hitCount).to.be(expectedHitCount);
|
||||
});
|
||||
});
|
||||
|
||||
it('the search term should be highlighted in the field data', async function () {
|
||||
// marks is the style that highlights the text in yellow
|
||||
const marks = await PageObjects.discover.getMarks();
|
||||
expect(marks.length).to.be(25);
|
||||
expect(marks.indexOf('php')).to.be(0);
|
||||
});
|
||||
|
||||
it('search type:apache should show the correct hit count', async function () {
|
||||
const expectedHitCount = '11,156';
|
||||
await queryBar.setQuery('type:apache');
|
||||
await queryBar.submitQuery();
|
||||
await retry.try(async function tryingForTime() {
|
||||
const hitCount = await PageObjects.discover.getHitCount();
|
||||
expect(hitCount).to.be(expectedHitCount);
|
||||
});
|
||||
});
|
||||
|
||||
it('doc view should show Time and _source columns', async function () {
|
||||
const expectedHeader = 'Time (@timestamp) _source';
|
||||
const DocHeader = await dataGrid.getHeaderFields();
|
||||
expect(DocHeader.join(' ')).to.be(expectedHeader);
|
||||
});
|
||||
|
||||
it('doc view should sort ascending', async function () {
|
||||
const expectedTimeStamp = 'Sep 20, 2015 @ 00:00:00.000';
|
||||
await dataGrid.clickDocSortAsc();
|
||||
await PageObjects.discover.waitUntilSearchingHasFinished();
|
||||
|
||||
await retry.try(async function tryingForTime() {
|
||||
const rowData = await dataGrid.getFields();
|
||||
expect(rowData[0][0].startsWith(expectedTimeStamp)).to.be.ok();
|
||||
});
|
||||
});
|
||||
|
||||
it('a bad syntax query should show an error message', async function () {
|
||||
const expectedError =
|
||||
'Expected ":", "<", "<=", ">", ">=", AND, OR, end of input, ' +
|
||||
'whitespace but "(" found.';
|
||||
await queryBar.setQuery('xxx(yyy))');
|
||||
await queryBar.submitQuery();
|
||||
const { message } = await toasts.getErrorToast();
|
||||
expect(message).to.contain(expectedError);
|
||||
await toasts.dismissToast();
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
|
@ -131,13 +131,14 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
|
|||
});
|
||||
|
||||
it('should add more columns to the table', async function () {
|
||||
const [column] = extraColumns;
|
||||
await PageObjects.discover.findFieldByName(column);
|
||||
log.debug(`add a ${column} column`);
|
||||
await PageObjects.discover.clickFieldListItemAdd(column);
|
||||
await PageObjects.header.waitUntilLoadingHasFinished();
|
||||
// test the header now
|
||||
expect(await PageObjects.discover.getDocHeader()).to.have.string(column);
|
||||
for (const column of extraColumns) {
|
||||
await PageObjects.discover.clearFieldSearchInput();
|
||||
await PageObjects.discover.findFieldByName(column);
|
||||
await PageObjects.discover.clickFieldListItemAdd(column);
|
||||
await PageObjects.header.waitUntilLoadingHasFinished();
|
||||
// test the header now
|
||||
expect(await PageObjects.discover.getDocHeader()).to.have.string(column);
|
||||
}
|
||||
});
|
||||
|
||||
it('should remove columns from the table', async function () {
|
||||
|
|
|
@ -51,5 +51,10 @@ export default function ({ getService, loadTestFile }: FtrProviderContext) {
|
|||
loadTestFile(require.resolve('./_date_nanos'));
|
||||
loadTestFile(require.resolve('./_date_nanos_mixed'));
|
||||
loadTestFile(require.resolve('./_indexpattern_without_timefield'));
|
||||
loadTestFile(require.resolve('./_data_grid'));
|
||||
loadTestFile(require.resolve('./_data_grid_context'));
|
||||
loadTestFile(require.resolve('./_data_grid_field_data'));
|
||||
loadTestFile(require.resolve('./_data_grid_doc_navigation'));
|
||||
loadTestFile(require.resolve('./_data_grid_doc_table'));
|
||||
});
|
||||
}
|
||||
|
|
|
@ -24,10 +24,15 @@ interface TabbedGridData {
|
|||
columns: string[];
|
||||
rows: string[][];
|
||||
}
|
||||
interface SelectOptions {
|
||||
isAnchorRow?: boolean;
|
||||
rowIndex: number;
|
||||
}
|
||||
|
||||
export function DataGridProvider({ getService }: FtrProviderContext) {
|
||||
export function DataGridProvider({ getService, getPageObjects }: FtrProviderContext) {
|
||||
const find = getService('find');
|
||||
const testSubjects = getService('testSubjects');
|
||||
const PageObjects = getPageObjects(['common', 'header']);
|
||||
|
||||
class DataGrid {
|
||||
async getDataGridTableData(): Promise<TabbedGridData> {
|
||||
|
@ -103,6 +108,137 @@ export function DataGridProvider({ getService }: FtrProviderContext) {
|
|||
[data-test-subj="dataGridRowCell"]:nth-of-type(${columnIndex})`
|
||||
);
|
||||
}
|
||||
public async getFields() {
|
||||
const rows = await find.allByCssSelector('.euiDataGridRow');
|
||||
|
||||
const result = [];
|
||||
for (const row of rows) {
|
||||
const cells = await row.findAllByClassName('euiDataGridRowCell__truncate');
|
||||
const cellsText = [];
|
||||
let cellIdx = 0;
|
||||
for (const cell of cells) {
|
||||
if (cellIdx > 0) {
|
||||
cellsText.push(await cell.getVisibleText());
|
||||
}
|
||||
cellIdx++;
|
||||
}
|
||||
result.push(cellsText);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
public async getTable(selector: string = 'docTable') {
|
||||
return await testSubjects.find(selector);
|
||||
}
|
||||
|
||||
public async getBodyRows(): Promise<WebElementWrapper[]> {
|
||||
const table = await this.getTable();
|
||||
return await table.findAllByTestSubject('dataGridRow');
|
||||
}
|
||||
|
||||
public async getDocTableRows() {
|
||||
const table = await this.getTable();
|
||||
return await table.findAllByTestSubject('dataGridRow');
|
||||
}
|
||||
|
||||
public async getAnchorRow(): Promise<WebElementWrapper> {
|
||||
const table = await this.getTable();
|
||||
return await table.findByTestSubject('~docTableAnchorRow');
|
||||
}
|
||||
|
||||
public async getRow(options: SelectOptions): Promise<WebElementWrapper> {
|
||||
return options.isAnchorRow
|
||||
? await this.getAnchorRow()
|
||||
: (await this.getBodyRows())[options.rowIndex];
|
||||
}
|
||||
|
||||
public async clickRowToggle(
|
||||
options: SelectOptions = { isAnchorRow: false, rowIndex: 0 }
|
||||
): Promise<void> {
|
||||
const row = await this.getRow(options);
|
||||
const toggle = await row.findByTestSubject('~docTableExpandToggleColumn');
|
||||
await toggle.click();
|
||||
}
|
||||
|
||||
public async getDetailsRows(): Promise<WebElementWrapper[]> {
|
||||
return await testSubjects.findAll('docTableDetailsFlyout');
|
||||
}
|
||||
|
||||
public async closeFlyout() {
|
||||
await testSubjects.click('euiFlyoutCloseButton');
|
||||
}
|
||||
|
||||
public async getHeaderFields(): Promise<string[]> {
|
||||
const result = await find.allByCssSelector('.euiDataGridHeaderCell__content');
|
||||
const textArr = [];
|
||||
let idx = 0;
|
||||
for (const cell of result) {
|
||||
if (idx > 0) {
|
||||
textArr.push(await cell.getVisibleText());
|
||||
}
|
||||
idx++;
|
||||
}
|
||||
return Promise.resolve(textArr);
|
||||
}
|
||||
|
||||
public async getRowActions(
|
||||
options: SelectOptions = { isAnchorRow: false, rowIndex: 0 }
|
||||
): Promise<WebElementWrapper[]> {
|
||||
const detailsRow = (await this.getDetailsRows())[options.rowIndex];
|
||||
return await detailsRow.findAllByTestSubject('~docTableRowAction');
|
||||
}
|
||||
|
||||
public async clickDocSortAsc() {
|
||||
await find.clickByCssSelector('.euiDataGridHeaderCell__button');
|
||||
await find.clickByButtonText('Sort New-Old');
|
||||
}
|
||||
|
||||
public async clickDocSortDesc() {
|
||||
await find.clickByCssSelector('.euiDataGridHeaderCell__button');
|
||||
await find.clickByButtonText('Sort Old-New');
|
||||
}
|
||||
public async getDetailsRow(): Promise<WebElementWrapper> {
|
||||
const detailRows = await this.getDetailsRows();
|
||||
return detailRows[0];
|
||||
}
|
||||
public async addInclusiveFilter(
|
||||
detailsRow: WebElementWrapper,
|
||||
fieldName: string
|
||||
): Promise<void> {
|
||||
const tableDocViewRow = await this.getTableDocViewRow(detailsRow, fieldName);
|
||||
const addInclusiveFilterButton = await this.getAddInclusiveFilterButton(tableDocViewRow);
|
||||
await addInclusiveFilterButton.click();
|
||||
await PageObjects.header.awaitGlobalLoadingIndicatorHidden();
|
||||
}
|
||||
|
||||
public async getAddInclusiveFilterButton(
|
||||
tableDocViewRow: WebElementWrapper
|
||||
): Promise<WebElementWrapper> {
|
||||
return await tableDocViewRow.findByTestSubject(`~addInclusiveFilterButton`);
|
||||
}
|
||||
|
||||
public async getTableDocViewRow(
|
||||
detailsRow: WebElementWrapper,
|
||||
fieldName: string
|
||||
): Promise<WebElementWrapper> {
|
||||
return await detailsRow.findByTestSubject(`~tableDocViewRow-${fieldName}`);
|
||||
}
|
||||
|
||||
public async getRemoveInclusiveFilterButton(
|
||||
tableDocViewRow: WebElementWrapper
|
||||
): Promise<WebElementWrapper> {
|
||||
return await tableDocViewRow.findByTestSubject(`~removeInclusiveFilterButton`);
|
||||
}
|
||||
|
||||
public async removeInclusiveFilter(
|
||||
detailsRow: WebElementWrapper,
|
||||
fieldName: string
|
||||
): Promise<void> {
|
||||
const tableDocViewRow = await this.getTableDocViewRow(detailsRow, fieldName);
|
||||
const addInclusiveFilterButton = await this.getRemoveInclusiveFilterButton(tableDocViewRow);
|
||||
await addInclusiveFilterButton.click();
|
||||
await PageObjects.header.awaitGlobalLoadingIndicatorHidden();
|
||||
}
|
||||
}
|
||||
|
||||
return new DataGrid();
|
||||
|
|
Loading…
Reference in a new issue