[Discover] Remove the legacy table (#201254)

- Closes https://github.com/elastic/kibana/issues/167582

## Summary

This PR removes the code related to the legacy doc table and 2 Advanced
Settings: `doc_table:legacy` and `truncate:maxHeight`.

The legacy table in Discover was replaced by the new data grid in v8.3.
The `doc_table:legacy` Advanced Setting was added to let users switch
back to the legacy table if necessary. The removal of the setting and
the legacy table entirely would allow us to reduce bundle size,
maintenance burden, and code complexity.

Also the legacy table does not support many new features which were
added to the grid only (e.g. comparing selected documents, context-aware
UI based on current solution project, column resizing, bulk row
selection, copy actions, new doc viewer flyout, and more).

Since v8.15 `doc_table:legacy` is marked as deprecated on Advanced
Settings page via https://github.com/elastic/kibana/issues/179899

Since v8.16 `truncate:maxHeight` is marked as deprecated too via
https://github.com/elastic/kibana/pull/183736

The removal of these 2 settings and the associated code is planned for
v9.

### Checklist

- [x]
[Documentation](https://www.elastic.co/guide/en/kibana/master/development-documentation.html)
was added for features that require explanation or tutorials
- [x] [Unit or functional
tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html)
were updated or added to match the most common scenarios
- [x] This was checked for breaking HTTP API changes, and any breaking
changes have been approved by the breaking-change committee. The
`release_note:breaking` label should be applied in these situations.

---------

Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>
Co-authored-by: Elastic Machine <elasticmachine@users.noreply.github.com>
This commit is contained in:
Julia Rechkunova 2024-12-03 12:03:08 +01:00 committed by GitHub
parent c1d976b470
commit 14bdd8d51b
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
128 changed files with 227 additions and 7868 deletions

View file

@ -64,7 +64,6 @@ enabled:
- test/functional/apps/dashboard/group5/config.ts
- test/functional/apps/dashboard/group6/config.ts
- test/functional/apps/discover/ccs_compatibility/config.ts
- test/functional/apps/discover/classic/config.ts
- test/functional/apps/discover/embeddable/config.ts
- test/functional/apps/discover/esql/config.ts
- test/functional/apps/discover/group1/config.ts

View file

@ -208,10 +208,6 @@ The default refresh interval for the time filter. Example:
[[timepicker-timedefaults]]`timepicker:timeDefaults`::
The default selection in the time filter.
[[truncate-maxheight]]`truncate:maxHeight`::
deprecated:[8.16.0]The maximum height that a cell occupies in a table. Set to 0 to disable
truncation.
[[enableESQL]]`enableESQL`::
This setting enables ES|QL in Kibana.
@ -340,14 +336,6 @@ 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`::
deprecated:[8.15.0] Controls the way the document table looks and works.
To use the new *Document Explorer* instead of the classic view, turn off this option.
The *Document Explorer* offers better data sorting, resizable columns, and a full screen view.
[[truncate-max-height]]`truncate:maxHeight`::
The maximum height that a cell in a table can occupy. To disable truncation, set to 0.
[float]
[[kibana-ml-settings]]

View file

@ -14,7 +14,6 @@ export {
DEFAULT_ALLOWED_LOGS_BASE_PATTERNS,
DEFAULT_COLUMNS_SETTING,
DOC_HIDE_TIME_COLUMN_SETTING,
DOC_TABLE_LEGACY,
FIELDS_LIMIT_SETTING,
HIDE_ANNOUNCEMENTS,
MAX_DOC_FIELDS_DISPLAYED,
@ -28,8 +27,6 @@ export {
SHOW_FIELD_STATISTICS,
SHOW_MULTIFIELDS,
SORT_DEFAULT_ORDER_SETTING,
TRUNCATE_MAX_HEIGHT,
TRUNCATE_MAX_HEIGHT_DEFAULT_VALUE,
IgnoredReason,
buildDataTableRecord,
buildDataTableRecordList,
@ -45,7 +42,6 @@ export {
getMessageFieldWithFallbacks,
getShouldShowFieldHandler,
isNestedFieldParent,
isLegacyTableEnabled,
usePager,
calcFieldCounts,
getLogLevelColor,

View file

@ -12,7 +12,6 @@ export const CONTEXT_STEP_SETTING = 'context:step';
export const CONTEXT_TIE_BREAKER_FIELDS_SETTING = 'context:tieBreakerFields';
export const DEFAULT_COLUMNS_SETTING = 'defaultColumns';
export const DOC_HIDE_TIME_COLUMN_SETTING = 'doc_table:hideTimeColumn';
export const DOC_TABLE_LEGACY = 'doc_table:legacy';
export const FIELDS_LIMIT_SETTING = 'fields:popularLimit';
export const HIDE_ANNOUNCEMENTS = 'hideAnnouncements';
export const MAX_DOC_FIELDS_DISPLAYED = 'discover:maxDocFieldsDisplayed';
@ -26,5 +25,3 @@ export const SEARCH_ON_PAGE_LOAD_SETTING = 'discover:searchOnPageLoad';
export const SHOW_FIELD_STATISTICS = 'discover:showFieldStatistics';
export const SHOW_MULTIFIELDS = 'discover:showMultiFields';
export const SORT_DEFAULT_ORDER_SETTING = 'discover:sort:defaultOrder';
export const TRUNCATE_MAX_HEIGHT = 'truncate:maxHeight';
export const TRUNCATE_MAX_HEIGHT_DEFAULT_VALUE = 115;

View file

@ -17,7 +17,7 @@ import type { DataTableRecord, EsHitRecord } from '../types';
import { getDocId } from './get_doc_id';
/**
* Build a record for data table, explorer + classic one
* Build a record for data grid
* @param doc the document returned from Elasticsearch
* @param dataView this current data view
* @param isAnchor determines if the given doc is the anchor doc when viewing surrounding documents

View file

@ -21,5 +21,4 @@ export * from './nested_fields';
export * from './get_field_value';
export * from './calc_field_counts';
export * from './get_visible_columns';
export { isLegacyTableEnabled } from './is_legacy_table_enabled';
export { DiscoverFlyouts, dismissAllFlyoutsExceptFor, dismissFlyouts } from './dismiss_flyouts';

View file

@ -1,25 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the "Elastic License
* 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
* Public License v 1"; you may not use this file except in compliance with, at
* your election, the "Elastic License 2.0", the "GNU Affero General Public
* License v3.0 only", or the "Server Side Public License, v 1".
*/
import type { IUiSettingsClient } from '@kbn/core-ui-settings-browser';
import { DOC_TABLE_LEGACY } from '../constants';
export function isLegacyTableEnabled({
uiSettings,
isEsqlMode,
}: {
uiSettings: IUiSettingsClient;
isEsqlMode: boolean;
}): boolean {
if (isEsqlMode) {
return false; // only show the new data grid
}
return uiSettings.get(DOC_TABLE_LEGACY);
}

View file

@ -24,7 +24,6 @@
"@kbn/field-formats-plugin",
"@kbn/field-types",
"@kbn/i18n",
"@kbn/core-ui-settings-browser",
"@kbn/expressions-plugin",
"@kbn/logs-data-access-plugin",
"@kbn/i18n-react",

View file

@ -87,8 +87,6 @@ export const DISCOVER_SHOW_MULTI_FIELDS_ID = 'discover:showMultiFields';
export const DISCOVER_SORT_DEFAULT_ORDER_ID = 'discover:sort:defaultOrder';
export const DOC_TABLE_HIDE_TIME_COLUMNS_ID = 'doc_table:hideTimeColumn';
export const DOC_TABLE_HIGHLIGHT_ID = 'doc_table:highlight';
export const DOC_TABLE_LEGACY_ID = 'doc_table:legacy';
export const TRUNCATE_MAX_HEIGHT_ID = 'truncate:maxHeight';
// Machine learning settings
export const ML_ANOMALY_DETECTION_RESULTS_ENABLE_TIME_DEFAULTS_ID =

View file

@ -2,10 +2,6 @@
This package contains components and services for the unified doc viewer component.
Discover (Classic view → Expanded document)
![image](https://github.com/elastic/kibana/assets/1178348/a0a360bf-2697-4427-a32e-c728f06f5a7e)
Discover (Document explorer → Toggle dialog with details)
![image](https://github.com/elastic/kibana/assets/1178348/c9c11587-c53f-4bcd-8d48-aaceb64981ea)

View file

@ -7,13 +7,9 @@
* License v3.0 only", or the "Server Side Public License, v 1".
*/
import type { DataView, DataViewField } from '@kbn/data-views-plugin/public';
import type { DataView } from '@kbn/data-views-plugin/public';
import type { AggregateQuery, Query } from '@kbn/es-query';
import type {
DataTableRecord,
DataTableColumnsMeta,
IgnoredReason,
} from '@kbn/discover-utils/types';
import type { DataTableRecord, DataTableColumnsMeta } from '@kbn/discover-utils/types';
import { DocViewsRegistry } from './doc_views_registry';
export interface FieldMapping {
@ -77,23 +73,3 @@ interface ComponentDocViewInput extends BaseDocViewInput {
export type DocView = ComponentDocViewInput | RenderDocViewInput;
export type DocViewFactory = () => DocView;
export interface FieldRecordLegacy {
action: {
isActive: boolean;
onFilter?: DocViewFilterFn;
onToggleColumn: ((field: string) => void) | undefined;
flattenedField: unknown;
};
field: {
displayName: string;
field: string;
scripted: boolean;
fieldType?: string;
fieldMapping?: DataViewField;
};
value: {
formattedValue: string;
ignored?: IgnoredReason;
};
}

View file

@ -12,5 +12,4 @@ export type {
DocViewFilterFn,
DocViewRenderFn,
DocViewRenderProps,
FieldRecordLegacy,
} from './services/types';

View file

@ -8,4 +8,4 @@
*/
export type { ElasticRequestState } from '.';
export type { DocViewFilterFn, DocViewRenderProps, FieldRecordLegacy } from './src/types';
export type { DocViewFilterFn, DocViewRenderProps } from './src/types';

View file

@ -15,7 +15,6 @@ import { identity } from 'lodash';
import { IUiSettingsClient } from '@kbn/core/public';
import {
DEFAULT_COLUMNS_SETTING,
DOC_TABLE_LEGACY,
MAX_DOC_FIELDS_DISPLAYED,
ROW_HEIGHT_OPTION,
SAMPLE_SIZE_SETTING,
@ -39,8 +38,6 @@ export const uiSettingsMock = {
return 10;
} else if (key === DEFAULT_COLUMNS_SETTING) {
return ['default_column'];
} else if (key === DOC_TABLE_LEGACY) {
return false;
} else if (key === SEARCH_FIELDS_FROM_SOURCE) {
return false;
} else if (key === SHOW_MULTIFIELDS) {

View file

@ -11,7 +11,6 @@ import { IUiSettingsClient } from '@kbn/core/public';
import {
CONTEXT_TIE_BREAKER_FIELDS_SETTING,
DEFAULT_COLUMNS_SETTING,
DOC_TABLE_LEGACY,
SAMPLE_SIZE_SETTING,
SAMPLE_ROWS_PER_PAGE_SETTING,
SHOW_MULTIFIELDS,
@ -27,8 +26,6 @@ export const uiSettingsMock = {
return 100;
} else if (key === DEFAULT_COLUMNS_SETTING) {
return ['default_column'];
} else if (key === DOC_TABLE_LEGACY) {
return true;
} else if (key === CONTEXT_TIE_BREAKER_FIELDS_SETTING) {
return ['_doc'];
} else if (key === SEARCH_FIELDS_FROM_SOURCE) {

View file

@ -9,7 +9,6 @@
import React, { Fragment, memo, useEffect, useRef, useMemo, useCallback } from 'react';
import './context_app.scss';
import classNames from 'classnames';
import { FormattedMessage } from '@kbn/i18n-react';
import { EuiText, EuiPage, EuiPageBody, EuiSpacer, useEuiPaddingSize } from '@elastic/eui';
import { css } from '@emotion/react';
@ -19,11 +18,7 @@ import { useExecutionContext } from '@kbn/kibana-react-plugin/public';
import { generateFilters } from '@kbn/data-plugin/public';
import { i18n } from '@kbn/i18n';
import { reportPerformanceMetricEvent } from '@kbn/ebt-tools';
import {
DOC_TABLE_LEGACY,
SEARCH_FIELDS_FROM_SOURCE,
SORT_DEFAULT_ORDER_SETTING,
} from '@kbn/discover-utils';
import { SEARCH_FIELDS_FROM_SOURCE, SORT_DEFAULT_ORDER_SETTING } from '@kbn/discover-utils';
import { UseColumnsProps, popularizeField, useColumns } from '@kbn/unified-data-table';
import { DocViewFilterFn } from '@kbn/unified-doc-viewer/types';
import { DiscoverGridSettings } from '@kbn/saved-search-plugin/common';
@ -60,7 +55,6 @@ export const ContextApp = ({ dataView, anchorId, referrer }: ContextAppProps) =>
fieldsMetadata,
} = services;
const isLegacy = useMemo(() => uiSettings.get(DOC_TABLE_LEGACY), [uiSettings]);
const useNewFieldsApi = useMemo(() => !uiSettings.get(SEARCH_FIELDS_FROM_SOURCE), [uiSettings]);
/**
@ -266,7 +260,7 @@ export const ContextApp = ({ dataView, anchorId, referrer }: ContextAppProps) =>
})}
</h1>
<TopNavMenu {...getNavBarProps()} />
<EuiPage className={classNames({ dscDocsPage: !isLegacy })}>
<EuiPage className="dscDocsPage">
<EuiPageBody
panelled
paddingSize="none"
@ -291,7 +285,6 @@ export const ContextApp = ({ dataView, anchorId, referrer }: ContextAppProps) =>
<ContextAppContentMemoized
dataView={dataView}
useNewFieldsApi={useNewFieldsApi}
isLegacy={isLegacy}
columns={columns}
grid={appState.grid}
onAddColumn={onAddColumnWithTracking}

View file

@ -10,14 +10,12 @@
import React from 'react';
import { mountWithIntl } from '@kbn/test-jest-helpers';
import { findTestSubject } from '@elastic/eui/lib/test';
import { ActionBar } from './components/action_bar/action_bar';
import { GetStateReturn } from './services/context_state';
import { SortDirection } from '@kbn/data-plugin/public';
import { UnifiedDataTable } from '@kbn/unified-data-table';
import { ContextAppContent, ContextAppContentProps } from './context_app_content';
import { LoadingStatus } from './services/context_query_state';
import { discoverServiceMock } from '../../__mocks__/services';
import { DocTableWrapper } from '../../components/doc_table/doc_table_wrapper';
import { KibanaContextProvider } from '@kbn/kibana-react-plugin/public';
import { buildDataTableRecord } from '@kbn/discover-utils';
import { act } from 'react-dom/test-utils';
@ -29,13 +27,7 @@ const dataViewMock = buildDataViewMock({
});
describe('ContextAppContent test', () => {
const mountComponent = async ({
anchorStatus,
isLegacy,
}: {
anchorStatus?: LoadingStatus;
isLegacy?: boolean;
}) => {
const mountComponent = async ({ anchorStatus }: { anchorStatus?: LoadingStatus }) => {
const hit = {
_id: '123',
_index: 'test_index',
@ -75,7 +67,6 @@ describe('ContextAppContent test', () => {
onRemoveColumn: () => {},
onSetColumns: () => {},
sort: [['order_date', 'desc']] as Array<[string, SortDirection]>,
isLegacy: isLegacy ?? true,
setAppState: () => {},
addFilter: () => {},
interceptedWarnings: [],
@ -93,29 +84,14 @@ describe('ContextAppContent test', () => {
return component;
};
it('should render legacy table correctly', async () => {
const component = await mountComponent({});
expect(component.find(DocTableWrapper).length).toBe(1);
const loadingIndicator = findTestSubject(component, 'contextApp_loadingIndicator');
expect(loadingIndicator.length).toBe(0);
expect(component.find(ActionBar).length).toBe(2);
});
it('renders loading indicator', async () => {
const component = await mountComponent({ anchorStatus: LoadingStatus.LOADING });
const loadingIndicator = findTestSubject(component, 'contextApp_loadingIndicator');
expect(component.find(DocTableWrapper).length).toBe(1);
expect(loadingIndicator.length).toBe(1);
});
it('should render discover grid correctly', async () => {
const component = await mountComponent({ isLegacy: false });
const component = await mountComponent({});
expect(component.find(UnifiedDataTable).length).toBe(1);
expect(findTestSubject(component, 'unifiedDataTableToolbar').exists()).toBe(true);
});
it('should not show display options button', async () => {
const component = await mountComponent({ isLegacy: false });
const component = await mountComponent({});
expect(findTestSubject(component, 'unifiedDataTableToolbar').exists()).toBe(true);
expect(findTestSubject(component, 'dataGridDisplaySelectorButton').exists()).toBe(false);
});

View file

@ -8,8 +8,7 @@
*/
import React, { Fragment, useCallback, useMemo, useState, FC } from 'react';
import { FormattedMessage } from '@kbn/i18n-react';
import { EuiSpacer, EuiText, useEuiPaddingSize } from '@elastic/eui';
import { EuiSpacer, useEuiPaddingSize } from '@elastic/eui';
import { css } from '@emotion/react';
import type { DataView } from '@kbn/data-views-plugin/public';
import { SortDirection } from '@kbn/data-plugin/public';
@ -45,7 +44,6 @@ import { ActionBar } from './components/action_bar/action_bar';
import { AppState } from './services/context_state';
import { SurrDocType } from './services/context';
import { MAX_CONTEXT_SIZE, MIN_CONTEXT_SIZE } from './services/constants';
import { DocTableContext } from '../../components/doc_table/doc_table_context';
import { useDiscoverServices } from '../../hooks/use_discover_services';
import { DiscoverGridFlyout } from '../../components/discover_grid_flyout';
import { onResizeGridColumn } from '../../utils/on_resize_grid_column';
@ -73,7 +71,6 @@ export interface ContextAppContentProps {
successorsStatus: LoadingStatus;
interceptedWarnings: SearchResponseWarning[];
useNewFieldsApi: boolean;
isLegacy: boolean;
setAppState: (newState: Partial<AppState>) => void;
addFilter: DocViewFilterFn;
}
@ -85,7 +82,6 @@ export function clamp(value: number) {
}
const DiscoverGridMemoized = React.memo(DiscoverGrid);
const DocTableContextMemoized = React.memo(DocTableContext);
const ActionBarMemoized = React.memo(ActionBar);
export function ContextAppContent({
@ -105,7 +101,6 @@ export function ContextAppContent({
successorsStatus,
interceptedWarnings,
useNewFieldsApi,
isLegacy,
setAppState,
addFilter,
}: ContextAppContentProps) {
@ -127,17 +122,6 @@ export function ContextAppContent({
);
const defaultStepSize = useMemo(() => parseInt(config.get(CONTEXT_STEP_SETTING), 10), [config]);
const loadingFeedback = () => {
if (isLegacy && isAnchorLoading) {
return (
<EuiText textAlign="center" data-test-subj="contextApp_loadingIndicator">
<FormattedMessage id="discover.context.loadingDescription" defaultMessage="Loading..." />
</EuiText>
);
}
return null;
};
const onChangeCount = useCallback(
(type: SurrDocType, count: number) => {
const countKey = type === SurrDocType.SUCCESSORS ? 'successorCount' : 'predecessorCount';
@ -224,59 +208,42 @@ export function ContextAppContent({
isLoading={arePredecessorsLoading}
isDisabled={isAnchorLoading}
/>
{loadingFeedback()}
</WrapperWithPadding>
{isLegacy && rows && rows.length !== 0 && (
<DocTableContextMemoized
columns={columns}
dataView={dataView}
rows={rows}
isLoading={isAnchorLoading}
onFilter={addFilter}
onAddColumn={onAddColumn}
onRemoveColumn={onRemoveColumn}
sort={sort}
useNewFieldsApi={useNewFieldsApi}
dataTestSubj="contextDocTable"
/>
)}
{!isLegacy && (
<div className="dscDocsGrid">
<CellActionsProvider getTriggerCompatibleActions={uiActions.getTriggerCompatibleActions}>
<DiscoverGridMemoized
ariaLabelledBy="surDocumentsAriaLabel"
cellActionsTriggerId={DISCOVER_CELL_ACTIONS_TRIGGER.id}
cellActionsMetadata={cellActionsMetadata}
cellActionsHandling="append"
columns={columns}
rows={rows}
dataView={dataView}
expandedDoc={expandedDoc}
loadingState={isAnchorLoading ? DataLoadingState.loading : DataLoadingState.loaded}
sampleSizeState={0}
sort={sort as SortOrder[]}
isSortEnabled={false}
showTimeCol={showTimeCol}
useNewFieldsApi={useNewFieldsApi}
isPaginationEnabled={false}
rowsPerPageState={getDefaultRowsPerPage(services.uiSettings)}
controlColumnIds={controlColumnIds}
setExpandedDoc={setExpandedDoc}
onFilter={addFilter}
onSetColumns={onSetColumns}
configRowHeight={configRowHeight}
showMultiFields={services.uiSettings.get(SHOW_MULTIFIELDS)}
maxDocFieldsDisplayed={services.uiSettings.get(MAX_DOC_FIELDS_DISPLAYED)}
renderDocumentView={renderDocumentView}
services={services}
configHeaderRowHeight={3}
settings={grid}
onResize={onResize}
externalCustomRenderers={cellRenderers}
/>
</CellActionsProvider>
</div>
)}
<div className="dscDocsGrid">
<CellActionsProvider getTriggerCompatibleActions={uiActions.getTriggerCompatibleActions}>
<DiscoverGridMemoized
ariaLabelledBy="surDocumentsAriaLabel"
cellActionsTriggerId={DISCOVER_CELL_ACTIONS_TRIGGER.id}
cellActionsMetadata={cellActionsMetadata}
cellActionsHandling="append"
columns={columns}
rows={rows}
dataView={dataView}
expandedDoc={expandedDoc}
loadingState={isAnchorLoading ? DataLoadingState.loading : DataLoadingState.loaded}
sampleSizeState={0}
sort={sort as SortOrder[]}
isSortEnabled={false}
showTimeCol={showTimeCol}
useNewFieldsApi={useNewFieldsApi}
isPaginationEnabled={false}
rowsPerPageState={getDefaultRowsPerPage(services.uiSettings)}
controlColumnIds={controlColumnIds}
setExpandedDoc={setExpandedDoc}
onFilter={addFilter}
onSetColumns={onSetColumns}
configRowHeight={configRowHeight}
showMultiFields={services.uiSettings.get(SHOW_MULTIFIELDS)}
maxDocFieldsDisplayed={services.uiSettings.get(MAX_DOC_FIELDS_DISPLAYED)}
renderDocumentView={renderDocumentView}
services={services}
configHeaderRowHeight={3}
settings={grid}
onResize={onResize}
externalCustomRenderers={cellRenderers}
/>
</CellActionsProvider>
</div>
<WrapperWithPadding>
<ActionBarMemoized
type={SurrDocType.SUCCESSORS}

View file

@ -1,7 +0,0 @@
.dscDocumentExplorerCallout {
.euiCallOutHeader__title {
display: flex;
align-items: center;
width: 100%;
}
}

View file

@ -1,58 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the "Elastic License
* 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
* Public License v 1"; you may not use this file except in compliance with, at
* your election, the "Elastic License 2.0", the "GNU Affero General Public
* License v3.0 only", or the "Server Side Public License, v 1".
*/
import React from 'react';
import { mountWithIntl } from '@kbn/test-jest-helpers';
import { KibanaContextProvider } from '@kbn/kibana-react-plugin/public';
import { CALLOUT_STATE_KEY, DocumentExplorerCallout } from './document_explorer_callout';
import { LocalStorageMock } from '../../../../__mocks__/local_storage_mock';
import { DiscoverServices } from '../../../../build_services';
const defaultServices = {
addBasePath: () => '',
docLinks: { links: { discover: { documentExplorer: '' } } },
capabilities: { advancedSettings: { save: true } },
storage: new LocalStorageMock({ [CALLOUT_STATE_KEY]: false }),
} as unknown as DiscoverServices;
const mount = (services: DiscoverServices) => {
return mountWithIntl(
<KibanaContextProvider services={services}>
<DocumentExplorerCallout />
</KibanaContextProvider>
);
};
describe('Document Explorer callout', () => {
it('should render callout', () => {
const result = mount(defaultServices);
expect(result.find('.dscDocumentExplorerCallout').exists()).toBeTruthy();
});
it('should not render callout for user without permissions', () => {
const services = {
...defaultServices,
capabilities: { advancedSettings: { save: false } },
} as unknown as DiscoverServices;
const result = mount(services);
expect(result.find('.dscDocumentExplorerCallout').exists()).toBeFalsy();
});
it('should not render callout of it was closed', () => {
const services = {
...defaultServices,
storage: new LocalStorageMock({ [CALLOUT_STATE_KEY]: true }),
} as unknown as DiscoverServices;
const result = mount(services);
expect(result.find('.dscDocumentExplorerCallout').exists()).toBeFalsy();
});
});

View file

@ -1,139 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the "Elastic License
* 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
* Public License v 1"; you may not use this file except in compliance with, at
* your election, the "Elastic License 2.0", the "GNU Affero General Public
* License v3.0 only", or the "Server Side Public License, v 1".
*/
import React, { useCallback, useMemo, useState } from 'react';
import './document_explorer_callout.scss';
import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n-react';
import {
EuiButton,
EuiButtonIcon,
EuiCallOut,
EuiFlexGroup,
EuiFlexItem,
EuiLink,
useEuiTheme,
} from '@elastic/eui';
import { css } from '@emotion/react';
import { Storage } from '@kbn/kibana-utils-plugin/public';
import { DOC_TABLE_LEGACY } from '@kbn/discover-utils';
import { useDiscoverServices } from '../../../../hooks/use_discover_services';
export const CALLOUT_STATE_KEY = 'discover:docExplorerCalloutClosed';
const getStoredCalloutState = (storage: Storage): boolean => {
const calloutClosed = storage.get(CALLOUT_STATE_KEY);
return Boolean(calloutClosed);
};
const updateStoredCalloutState = (newState: boolean, storage: Storage) => {
storage.set(CALLOUT_STATE_KEY, newState);
};
/**
* The callout that's displayed when Document explorer is disabled
*/
export const DocumentExplorerCallout = () => {
const { euiTheme } = useEuiTheme();
const { storage, capabilities, docLinks, addBasePath } = useDiscoverServices();
const [calloutClosed, setCalloutClosed] = useState(getStoredCalloutState(storage));
const onCloseCallout = useCallback(() => {
updateStoredCalloutState(true, storage);
setCalloutClosed(true);
}, [storage]);
const semiBoldStyle = useMemo(
() => css`
font-weight: ${euiTheme.font.weight.semiBold};
`,
[euiTheme.font.weight.semiBold]
);
if (calloutClosed || !capabilities.advancedSettings.save) {
return null;
}
return (
<EuiCallOut
data-test-subj="dscDocumentExplorerLegacyCallout"
className="dscDocumentExplorerCallout"
title={<CalloutTitle onCloseCallout={onCloseCallout} />}
iconType="search"
>
<p>
<FormattedMessage
id="discover.docExplorerCallout.bodyMessage"
defaultMessage="Quickly sort, select, and compare data, resize columns, and view documents in fullscreen with the {documentExplorer}."
values={{
documentExplorer: (
<span css={semiBoldStyle}>
<FormattedMessage
id="discover.docExplorerCallout.documentExplorer"
defaultMessage="Document Explorer"
/>
</span>
),
}}
/>
</p>
<EuiFlexGroup
justifyContent="flexStart"
alignItems="center"
responsive={false}
gutterSize="s"
>
<EuiFlexItem grow={false}>
<EuiButton
data-test-subj="tryDocumentExplorerButton"
iconType="tableDensityNormal"
size="s"
href={addBasePath(`/app/management/kibana/settings?query=${DOC_TABLE_LEGACY}`)}
>
<FormattedMessage
id="discover.docExplorerCallout.tryDocumentExplorer"
defaultMessage="Try Document Explorer"
/>
</EuiButton>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiLink href={docLinks.links.discover.documentExplorer} target="_blank">
<FormattedMessage
id="discover.docExplorerCallout.learnMore"
defaultMessage="Learn more"
/>
</EuiLink>
</EuiFlexItem>
</EuiFlexGroup>
</EuiCallOut>
);
};
function CalloutTitle({ onCloseCallout }: { onCloseCallout: () => void }) {
return (
<EuiFlexGroup justifyContent="spaceBetween" gutterSize="none" responsive={false}>
<EuiFlexItem grow={false}>
<FormattedMessage
id="discover.docExplorerCallout.headerMessage"
defaultMessage="A better way to explore"
/>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiButtonIcon
aria-label={i18n.translate('discover.docExplorerCallout.closeButtonAriaLabel', {
defaultMessage: 'Close',
})}
data-test-subj="dscExplorerCalloutClose"
onClick={onCloseCallout}
type="button"
iconType="cross"
/>
</EuiFlexItem>
</EuiFlexGroup>
);
}

View file

@ -1,10 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the "Elastic License
* 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
* Public License v 1"; you may not use this file except in compliance with, at
* your election, the "Elastic License 2.0", the "GNU Affero General Public
* License v3.0 only", or the "Server Side Public License, v 1".
*/
export { DocumentExplorerCallout } from './document_explorer_callout';

View file

@ -33,9 +33,6 @@ const statsTableCss = css({
width: '100%',
height: '100%',
overflowY: 'auto',
'.kbnDocTableWrapper': {
overflowX: 'hidden',
},
});
const fallBacklastReloadRequestTime$ = new BehaviorSubject(0);

View file

@ -37,8 +37,6 @@ import {
} from '@kbn/unified-data-table';
import {
DOC_HIDE_TIME_COLUMN_SETTING,
HIDE_ANNOUNCEMENTS,
isLegacyTableEnabled,
MAX_DOC_FIELDS_DISPLAYED,
ROW_HEIGHT_OPTION,
SEARCH_FIELDS_FROM_SOURCE,
@ -58,8 +56,6 @@ import { useDiscoverServices } from '../../../../hooks/use_discover_services';
import { FetchStatus } from '../../../types';
import { DiscoverStateContainer } from '../../state_management/discover_state';
import { useDataState } from '../../hooks/use_data_state';
import { DocTableInfinite } from '../../../../components/doc_table/doc_table_infinite';
import { DocumentExplorerCallout } from '../document_explorer_callout';
import {
getMaxAllowedSampleSize,
getAllowedSampleSize,
@ -88,7 +84,6 @@ const progressStyle = css`
z-index: 2;
`;
const DocTableInfiniteMemoized = React.memo(DocTableInfinite);
const DiscoverGridMemoized = React.memo(DiscoverGrid);
// export needs for testing
@ -146,11 +141,6 @@ function DiscoverDocumentsComponent({
const expandedDoc = useInternalStateSelector((state) => state.expandedDoc);
const isEsqlMode = useIsEsqlMode();
const useNewFieldsApi = useMemo(() => !uiSettings.get(SEARCH_FIELDS_FROM_SOURCE), [uiSettings]);
const hideAnnouncements = useMemo(() => uiSettings.get(HIDE_ANNOUNCEMENTS), [uiSettings]);
const isLegacy = useMemo(
() => isLegacyTableEnabled({ uiSettings, isEsqlMode }),
[uiSettings, isEsqlMode]
);
const documentState = useDataState(documents$);
const isDataLoading =
documentState.fetchStatus === FetchStatus.LOADING ||
@ -186,7 +176,6 @@ function DiscoverDocumentsComponent({
columns: currentColumns,
onAddColumn,
onRemoveColumn,
onMoveColumn,
onSetColumns,
} = useColumns({
capabilities,
@ -417,115 +406,76 @@ function DiscoverDocumentsComponent({
}
return (
<>
{isLegacy && (
<>
<EuiFlexItem grow={false}>{viewModeToggle}</EuiFlexItem>
{callouts}
</>
)}
<EuiFlexItem className="dscTable" aria-labelledby="documentsAriaLabel" css={containerStyles}>
<EuiScreenReaderOnly>
<h2 id="documentsAriaLabel">
<FormattedMessage id="discover.documentsAriaLabel" defaultMessage="Documents" />
</h2>
</EuiScreenReaderOnly>
{isLegacy && (
<>
{rows && rows.length > 0 && (
<>
{!hideAnnouncements && <DocumentExplorerCallout />}
<DocTableInfiniteMemoized
columns={currentColumns}
dataView={dataView}
rows={rows}
sort={sort || []}
isLoading={isDataLoading}
searchDescription={savedSearch.description}
sharedItemTitle={savedSearch.title}
isEsqlMode={isEsqlMode}
onAddColumn={onAddColumn}
onFilter={onAddFilter as DocViewFilterFn}
onMoveColumn={onMoveColumn}
onRemoveColumn={onRemoveColumn}
onSort={!isEsqlMode ? onSort : undefined}
useNewFieldsApi={useNewFieldsApi}
dataTestSubj="discoverDocTable"
/>
</>
)}
{loadingIndicator}
</>
)}
{!isLegacy && (
<div className="unifiedDataTable">
<CellActionsProvider
getTriggerCompatibleActions={uiActions.getTriggerCompatibleActions}
>
<DiscoverGridMemoized
ariaLabelledBy="documentsAriaLabel"
columns={currentColumns}
columnsMeta={columnsMeta}
expandedDoc={expandedDoc}
dataView={dataView}
loadingState={
isDataLoading
? DataLoadingState.loading
: isMoreDataLoading
? DataLoadingState.loadingMore
: DataLoadingState.loaded
}
rows={rows}
sort={(sort as SortOrder[]) || []}
searchDescription={savedSearch.description}
searchTitle={savedSearch.title}
setExpandedDoc={setExpandedDoc}
showTimeCol={showTimeCol}
settings={grid}
onFilter={onAddFilter as DocViewFilterFn}
onSetColumns={onSetColumns}
onSort={onSort}
onResize={onResizeDataGrid}
useNewFieldsApi={useNewFieldsApi}
configHeaderRowHeight={3}
headerRowHeightState={headerRowHeight}
onUpdateHeaderRowHeight={onUpdateHeaderRowHeight}
rowHeightState={rowHeight}
onUpdateRowHeight={onUpdateRowHeight}
isSortEnabled={true}
isPlainRecord={isEsqlMode}
isPaginationEnabled={!isEsqlMode}
rowsPerPageState={rowsPerPage ?? getDefaultRowsPerPage(services.uiSettings)}
onUpdateRowsPerPage={onUpdateRowsPerPage}
maxAllowedSampleSize={getMaxAllowedSampleSize(services.uiSettings)}
sampleSizeState={getAllowedSampleSize(sampleSizeState, services.uiSettings)}
onUpdateSampleSize={!isEsqlMode ? onUpdateSampleSize : undefined}
onFieldEdited={onFieldEdited}
configRowHeight={configRowHeight}
showMultiFields={uiSettings.get(SHOW_MULTIFIELDS)}
maxDocFieldsDisplayed={uiSettings.get(MAX_DOC_FIELDS_DISPLAYED)}
renderDocumentView={renderDocumentView}
renderCustomToolbar={renderCustomToolbarWithElements}
services={services}
totalHits={totalHits}
onFetchMoreRecords={onFetchMoreRecords}
externalCustomRenderers={cellRenderers}
customGridColumnsConfiguration={customGridColumnsConfiguration}
rowAdditionalLeadingControls={rowAdditionalLeadingControls}
additionalFieldGroups={additionalFieldGroups}
dataGridDensityState={density}
onUpdateDataGridDensity={onUpdateDensity}
onUpdateESQLQuery={stateContainer.actions.updateESQLQuery}
query={query}
cellActionsTriggerId={DISCOVER_CELL_ACTIONS_TRIGGER.id}
cellActionsMetadata={cellActionsMetadata}
cellActionsHandling="append"
/>
</CellActionsProvider>
</div>
)}
</EuiFlexItem>
</>
<EuiFlexItem className="dscTable" aria-labelledby="documentsAriaLabel" css={containerStyles}>
<EuiScreenReaderOnly>
<h2 id="documentsAriaLabel">
<FormattedMessage id="discover.documentsAriaLabel" defaultMessage="Documents" />
</h2>
</EuiScreenReaderOnly>
<div className="unifiedDataTable">
<CellActionsProvider getTriggerCompatibleActions={uiActions.getTriggerCompatibleActions}>
<DiscoverGridMemoized
ariaLabelledBy="documentsAriaLabel"
columns={currentColumns}
columnsMeta={columnsMeta}
expandedDoc={expandedDoc}
dataView={dataView}
loadingState={
isDataLoading
? DataLoadingState.loading
: isMoreDataLoading
? DataLoadingState.loadingMore
: DataLoadingState.loaded
}
rows={rows}
sort={(sort as SortOrder[]) || []}
searchDescription={savedSearch.description}
searchTitle={savedSearch.title}
setExpandedDoc={setExpandedDoc}
showTimeCol={showTimeCol}
settings={grid}
onFilter={onAddFilter as DocViewFilterFn}
onSetColumns={onSetColumns}
onSort={onSort}
onResize={onResizeDataGrid}
useNewFieldsApi={useNewFieldsApi}
configHeaderRowHeight={3}
headerRowHeightState={headerRowHeight}
onUpdateHeaderRowHeight={onUpdateHeaderRowHeight}
rowHeightState={rowHeight}
onUpdateRowHeight={onUpdateRowHeight}
isSortEnabled={true}
isPlainRecord={isEsqlMode}
isPaginationEnabled={!isEsqlMode}
rowsPerPageState={rowsPerPage ?? getDefaultRowsPerPage(services.uiSettings)}
onUpdateRowsPerPage={onUpdateRowsPerPage}
maxAllowedSampleSize={getMaxAllowedSampleSize(services.uiSettings)}
sampleSizeState={getAllowedSampleSize(sampleSizeState, services.uiSettings)}
onUpdateSampleSize={!isEsqlMode ? onUpdateSampleSize : undefined}
onFieldEdited={onFieldEdited}
configRowHeight={configRowHeight}
showMultiFields={uiSettings.get(SHOW_MULTIFIELDS)}
maxDocFieldsDisplayed={uiSettings.get(MAX_DOC_FIELDS_DISPLAYED)}
renderDocumentView={renderDocumentView}
renderCustomToolbar={renderCustomToolbarWithElements}
services={services}
totalHits={totalHits}
onFetchMoreRecords={onFetchMoreRecords}
externalCustomRenderers={cellRenderers}
customGridColumnsConfiguration={customGridColumnsConfiguration}
rowAdditionalLeadingControls={rowAdditionalLeadingControls}
additionalFieldGroups={additionalFieldGroups}
dataGridDensityState={density}
onUpdateDataGridDensity={onUpdateDensity}
onUpdateESQLQuery={stateContainer.actions.updateESQLQuery}
query={query}
cellActionsTriggerId={DISCOVER_CELL_ACTIONS_TRIGGER.id}
cellActionsMetadata={cellActionsMetadata}
cellActionsHandling="append"
/>
</CellActionsProvider>
</div>
</EuiFlexItem>
);
}

View file

@ -13,11 +13,9 @@ import { EuiFormRow, EuiSwitch } from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n-react';
import { SavedObjectSaveModal, showSaveModal, OnSaveProps } from '@kbn/saved-objects-plugin/public';
import { SavedSearch, SaveSavedSearchOptions } from '@kbn/saved-search-plugin/public';
import { isLegacyTableEnabled } from '@kbn/discover-utils';
import { DiscoverServices } from '../../../../build_services';
import { DiscoverStateContainer } from '../../state_management/discover_state';
import { getAllowedSampleSize } from '../../../../utils/get_allowed_sample_size';
import { DataSourceType, isDataSourceType } from '../../../../../common/data_sources';
async function saveDataSource({
savedSearch,
@ -127,12 +125,7 @@ export async function onSaveSearch({
savedSearch.title = newTitle;
savedSearch.description = newDescription;
savedSearch.timeRestore = newTimeRestore;
savedSearch.rowsPerPage = isLegacyTableEnabled({
uiSettings,
isEsqlMode: isDataSourceType(appState.dataSource, DataSourceType.Esql),
})
? currentRowsPerPage
: appState.rowsPerPage;
savedSearch.rowsPerPage = appState.rowsPerPage;
// save the custom value or reset it if it's invalid
const appStateSampleSize = appState.sampleSize;

View file

@ -1,136 +0,0 @@
/**
* 1. Stack content vertically so the table can scroll when its constrained by a fixed container height.
*/
// stylelint-disable selector-no-qualifying-type
.kbnDocTableWrapper {
@include euiScrollBar;
@include euiOverflowShadow;
overflow: auto;
display: flex;
flex: 1 1 100%;
flex-direction: column; /* 1 */
th {
text-align: left;
font-weight: bold;
}
.spinner {
position: absolute;
top: 40%;
left: 0;
right: 0;
z-index: $euiZLevel1;
opacity: .5;
}
// SASSTODO: add a monospace modifier to the doc-table component
.kbnDocTable__row {
font-family: $euiCodeFontFamily;
font-size: $euiFontSizeXS;
}
.embPanel & {
// Bug fix for dashboard panels https://github.com/elastic/kibana/issues/180828
mask-image: none;
}
}
.kbnDocTable__footer {
background-color: $euiColorLightShade;
padding: $euiSizeXS $euiSizeS;
text-align: center;
}
.kbnDocTable__container.loading {
opacity: .5;
}
.kbnDocTable {
th {
white-space: nowrap;
padding-right: $euiSizeS;
}
}
.kbn-table,
.kbnDocTable {
/**
* Style ES document _source in table view <dt>key:<dt><dd>value</dd>
* Use alpha so this will stand out against non-white backgrounds, e.g. the highlighted
* row in the Context Log.
*/
dl.source {
margin-bottom: 0;
line-height: 2em;
word-break: break-word;
dt,
dd {
display: inline;
}
dt {
background-color: transparentize(shade($euiColorPrimary, 20%), .9);
color: $euiTextColor;
padding: calc($euiSizeXS / 2) $euiSizeXS;
margin-right: $euiSizeXS;
word-break: normal;
border-radius: $euiBorderRadius;
}
}
}
.kbnDocTable__row {
td {
position: relative;
&:hover {
.kbnDocTableRowFilterButton {
opacity: 1;
}
}
}
}
.kbnDocTable__row--highlight {
td,
.kbnDocTableRowFilterButton {
background-color: tintOrShade($euiColorPrimary, 90%, 70%);
}
}
.kbnDocTable__error {
display: flex;
flex-direction: column;
justify-content: center;
flex: 1 0 100%;
text-align: center;
}
.table {
// Nesting
.table {
background-color: $euiColorEmptyShade;
}
}
.kbn-table {
// sub tables should not have a leading border
.table .table {
margin-bottom: 0;
tr:first-child > td {
border-top: none;
}
td.field-name {
font-weight: $euiFontWeightBold;
}
}
}
.dscTruncateByHeight {
display: inline-block;
}

View file

@ -1,2 +0,0 @@
@import 'table_header';
@import 'table_row/index';

View file

@ -1,19 +0,0 @@
.kbnDocTableHeader {
white-space: nowrap;
}
.kbnDocTableHeader button,
.kbnDocTableHeader svg {
margin-left: $euiSizeXS * .5;
}
.kbnDocTableHeader__move,
.kbnDocTableHeader__sortChange {
opacity: 0;
th:hover &,
&:focus {
opacity: 1;
}
}
.kbnDocTableHeader__actions {
display: flex;
align-items: center;
}

View file

@ -1,115 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the "Elastic License
* 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
* Public License v 1"; you may not use this file except in compliance with, at
* your election, the "Elastic License 2.0", the "GNU Affero General Public
* License v3.0 only", or the "Server Side Public License, v 1".
*/
import React, { useState } from 'react';
import {
EuiButtonEmpty,
EuiContextMenuItem,
EuiContextMenuPanel,
EuiFlexGroup,
EuiFlexItem,
EuiPagination,
EuiPopover,
} from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n-react';
import { i18n } from '@kbn/i18n';
import { euiLightVars } from '@kbn/ui-theme';
import { getRowsPerPageOptions } from '@kbn/unified-data-table';
export const MAX_ROWS_PER_PAGE_OPTION = 100;
interface ToolBarPaginationProps {
pageSize: number;
pageCount: number;
activePage: number;
onPageClick: (page: number) => void;
onPageSizeChange: (size: number) => void;
}
const TOOL_BAR_PAGINATION_STYLES = {
marginLeft: 'auto',
marginRight: euiLightVars.euiSizeL,
};
export const ToolBarPagination = ({
pageSize,
pageCount,
activePage,
onPageSizeChange,
onPageClick,
}: ToolBarPaginationProps) => {
const [isPopoverOpen, setIsPopoverOpen] = useState(false);
const rowsWord = i18n.translate('discover.docTable.rows', {
defaultMessage: 'rows',
});
const onChooseRowsClick = () => setIsPopoverOpen((prevIsPopoverOpen) => !prevIsPopoverOpen);
const closePopover = () => setIsPopoverOpen(false);
const getIconType = (size: number) => {
return size === pageSize ? 'check' : 'empty';
};
const rowsPerPageOptions = getRowsPerPageOptions(pageSize)
.filter((option) => option <= MAX_ROWS_PER_PAGE_OPTION) // legacy table is not optimized well for rendering more rows at once
.map((cur) => (
<EuiContextMenuItem
key={`${cur} rows`}
icon={getIconType(cur)}
onClick={() => {
closePopover();
onPageSizeChange(cur);
}}
>
{cur} {rowsWord}
</EuiContextMenuItem>
));
return (
<EuiFlexGroup alignItems="center" gutterSize="xs" responsive={false}>
<EuiFlexItem grow={false}>
<EuiPopover
button={
<EuiButtonEmpty
size="xs"
color="text"
iconType="arrowDown"
iconSide="right"
onClick={onChooseRowsClick}
>
<FormattedMessage
id="discover.docTable.rowsPerPage"
defaultMessage="Rows per page: {pageSize}"
values={{ pageSize }}
/>
</EuiButtonEmpty>
}
isOpen={isPopoverOpen}
closePopover={closePopover}
panelPaddingSize="none"
>
<EuiContextMenuPanel items={rowsPerPageOptions} />
</EuiPopover>
</EuiFlexItem>
<EuiFlexItem grow={false} style={TOOL_BAR_PAGINATION_STYLES}>
<EuiPagination
aria-label={i18n.translate('discover.docTable.documentsNavigation', {
defaultMessage: 'Documents navigation',
})}
pageCount={pageCount}
activePage={activePage}
onPageClick={onPageClick}
compressed
/>
</EuiFlexItem>
</EuiFlexGroup>
);
};

View file

@ -1,373 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`TableHeader with time column renders correctly 1`] = `
<tr
class="kbnDocTableHeader"
data-test-subj="docTableHeader"
>
<th
css="[object Object]"
/>
<th
data-test-subj="docTableHeaderField"
>
<span
class="kbnDocTableHeader__actions"
data-test-subj="docTableHeader-time"
>
time
<span
class="euiToolTipAnchor emotion-euiToolTipAnchor-inlineBlock"
>
<span
data-euiicon-type="clock"
tabindex="0"
>
time - this field represents the time that events occurred.
</span>
</span>
<span
class="euiToolTipAnchor emotion-euiToolTipAnchor-inlineBlock"
>
<button
aria-label="Sort time descending"
class="euiButtonIcon emotion-euiButtonIcon-xs-empty-primary"
data-test-subj="docTableHeaderFieldSort_time"
style="width: 12px; height: 12px;"
type="button"
>
<span
aria-hidden="true"
class="euiButtonIcon__icon"
color="inherit"
data-euiicon-type="sortUp"
/>
</button>
</span>
</span>
</th>
<th
data-test-subj="docTableHeaderField"
>
<span
class="kbnDocTableHeader__actions"
data-test-subj="docTableHeader-first"
>
first
<span
class="euiToolTipAnchor emotion-euiToolTipAnchor-inlineBlock"
>
<button
aria-label="Remove first column"
class="euiButtonIcon kbnDocTableHeader__move emotion-euiButtonIcon-xs-empty-text"
data-test-subj="docTableRemoveHeader-first"
style="width: 12px; height: 12px;"
type="button"
>
<span
aria-hidden="true"
class="euiButtonIcon__icon"
color="inherit"
data-euiicon-type="cross"
/>
</button>
</span>
<span
class="euiToolTipAnchor emotion-euiToolTipAnchor-inlineBlock"
>
<button
aria-label="Move first column to the right"
class="euiButtonIcon kbnDocTableHeader__move emotion-euiButtonIcon-xs-empty-text"
data-test-subj="docTableMoveRightHeader-first"
style="width: 12px; height: 12px;"
type="button"
>
<span
aria-hidden="true"
class="euiButtonIcon__icon"
color="inherit"
data-euiicon-type="sortRight"
/>
</button>
</span>
</span>
</th>
<th
data-test-subj="docTableHeaderField"
>
<span
class="kbnDocTableHeader__actions"
data-test-subj="docTableHeader-middle"
>
middle
<span
class="euiToolTipAnchor emotion-euiToolTipAnchor-inlineBlock"
>
<button
aria-label="Remove middle column"
class="euiButtonIcon kbnDocTableHeader__move emotion-euiButtonIcon-xs-empty-text"
data-test-subj="docTableRemoveHeader-middle"
style="width: 12px; height: 12px;"
type="button"
>
<span
aria-hidden="true"
class="euiButtonIcon__icon"
color="inherit"
data-euiicon-type="cross"
/>
</button>
</span>
<span
class="euiToolTipAnchor emotion-euiToolTipAnchor-inlineBlock"
>
<button
aria-label="Move middle column to the left"
class="euiButtonIcon kbnDocTableHeader__move emotion-euiButtonIcon-xs-empty-text"
data-test-subj="docTableMoveLeftHeader-middle"
style="width: 12px; height: 12px;"
type="button"
>
<span
aria-hidden="true"
class="euiButtonIcon__icon"
color="inherit"
data-euiicon-type="sortLeft"
/>
</button>
</span>
<span
class="euiToolTipAnchor emotion-euiToolTipAnchor-inlineBlock"
>
<button
aria-label="Move middle column to the right"
class="euiButtonIcon kbnDocTableHeader__move emotion-euiButtonIcon-xs-empty-text"
data-test-subj="docTableMoveRightHeader-middle"
style="width: 12px; height: 12px;"
type="button"
>
<span
aria-hidden="true"
class="euiButtonIcon__icon"
color="inherit"
data-euiicon-type="sortRight"
/>
</button>
</span>
</span>
</th>
<th
data-test-subj="docTableHeaderField"
>
<span
class="kbnDocTableHeader__actions"
data-test-subj="docTableHeader-last"
>
last
<span
class="euiToolTipAnchor emotion-euiToolTipAnchor-inlineBlock"
>
<button
aria-label="Remove last column"
class="euiButtonIcon kbnDocTableHeader__move emotion-euiButtonIcon-xs-empty-text"
data-test-subj="docTableRemoveHeader-last"
style="width: 12px; height: 12px;"
type="button"
>
<span
aria-hidden="true"
class="euiButtonIcon__icon"
color="inherit"
data-euiicon-type="cross"
/>
</button>
</span>
<span
class="euiToolTipAnchor emotion-euiToolTipAnchor-inlineBlock"
>
<button
aria-label="Move last column to the left"
class="euiButtonIcon kbnDocTableHeader__move emotion-euiButtonIcon-xs-empty-text"
data-test-subj="docTableMoveLeftHeader-last"
style="width: 12px; height: 12px;"
type="button"
>
<span
aria-hidden="true"
class="euiButtonIcon__icon"
color="inherit"
data-euiicon-type="sortLeft"
/>
</button>
</span>
</span>
</th>
</tr>
`;
exports[`TableHeader without time column renders correctly 1`] = `
<tr
class="kbnDocTableHeader"
data-test-subj="docTableHeader"
>
<th
css="[object Object]"
/>
<th
data-test-subj="docTableHeaderField"
>
<span
class="kbnDocTableHeader__actions"
data-test-subj="docTableHeader-first"
>
first
<span
class="euiToolTipAnchor emotion-euiToolTipAnchor-inlineBlock"
>
<button
aria-label="Remove first column"
class="euiButtonIcon kbnDocTableHeader__move emotion-euiButtonIcon-xs-empty-text"
data-test-subj="docTableRemoveHeader-first"
style="width: 12px; height: 12px;"
type="button"
>
<span
aria-hidden="true"
class="euiButtonIcon__icon"
color="inherit"
data-euiicon-type="cross"
/>
</button>
</span>
<span
class="euiToolTipAnchor emotion-euiToolTipAnchor-inlineBlock"
>
<button
aria-label="Move first column to the right"
class="euiButtonIcon kbnDocTableHeader__move emotion-euiButtonIcon-xs-empty-text"
data-test-subj="docTableMoveRightHeader-first"
style="width: 12px; height: 12px;"
type="button"
>
<span
aria-hidden="true"
class="euiButtonIcon__icon"
color="inherit"
data-euiicon-type="sortRight"
/>
</button>
</span>
</span>
</th>
<th
data-test-subj="docTableHeaderField"
>
<span
class="kbnDocTableHeader__actions"
data-test-subj="docTableHeader-middle"
>
middle
<span
class="euiToolTipAnchor emotion-euiToolTipAnchor-inlineBlock"
>
<button
aria-label="Remove middle column"
class="euiButtonIcon kbnDocTableHeader__move emotion-euiButtonIcon-xs-empty-text"
data-test-subj="docTableRemoveHeader-middle"
style="width: 12px; height: 12px;"
type="button"
>
<span
aria-hidden="true"
class="euiButtonIcon__icon"
color="inherit"
data-euiicon-type="cross"
/>
</button>
</span>
<span
class="euiToolTipAnchor emotion-euiToolTipAnchor-inlineBlock"
>
<button
aria-label="Move middle column to the left"
class="euiButtonIcon kbnDocTableHeader__move emotion-euiButtonIcon-xs-empty-text"
data-test-subj="docTableMoveLeftHeader-middle"
style="width: 12px; height: 12px;"
type="button"
>
<span
aria-hidden="true"
class="euiButtonIcon__icon"
color="inherit"
data-euiicon-type="sortLeft"
/>
</button>
</span>
<span
class="euiToolTipAnchor emotion-euiToolTipAnchor-inlineBlock"
>
<button
aria-label="Move middle column to the right"
class="euiButtonIcon kbnDocTableHeader__move emotion-euiButtonIcon-xs-empty-text"
data-test-subj="docTableMoveRightHeader-middle"
style="width: 12px; height: 12px;"
type="button"
>
<span
aria-hidden="true"
class="euiButtonIcon__icon"
color="inherit"
data-euiicon-type="sortRight"
/>
</button>
</span>
</span>
</th>
<th
data-test-subj="docTableHeaderField"
>
<span
class="kbnDocTableHeader__actions"
data-test-subj="docTableHeader-last"
>
last
<span
class="euiToolTipAnchor emotion-euiToolTipAnchor-inlineBlock"
>
<button
aria-label="Remove last column"
class="euiButtonIcon kbnDocTableHeader__move emotion-euiButtonIcon-xs-empty-text"
data-test-subj="docTableRemoveHeader-last"
style="width: 12px; height: 12px;"
type="button"
>
<span
aria-hidden="true"
class="euiButtonIcon__icon"
color="inherit"
data-euiicon-type="cross"
/>
</button>
</span>
<span
class="euiToolTipAnchor emotion-euiToolTipAnchor-inlineBlock"
>
<button
aria-label="Move last column to the left"
class="euiButtonIcon kbnDocTableHeader__move emotion-euiButtonIcon-xs-empty-text"
data-test-subj="docTableMoveLeftHeader-last"
style="width: 12px; height: 12px;"
type="button"
>
<span
aria-hidden="true"
class="euiButtonIcon__icon"
color="inherit"
data-euiicon-type="sortLeft"
/>
</button>
</span>
</span>
</th>
</tr>
`;

View file

@ -1,86 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the "Elastic License
* 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
* Public License v 1"; you may not use this file except in compliance with, at
* your election, the "Elastic License 2.0", the "GNU Affero General Public
* License v3.0 only", or the "Server Side Public License, v 1".
*/
import { i18n } from '@kbn/i18n';
import type { DataView } from '@kbn/data-views-plugin/public';
export interface ColumnProps {
name: string;
displayName: string;
isSortable: boolean;
isRemoveable: boolean;
colLeftIdx: number;
colRightIdx: number;
}
/**
* Returns properties necessary to display the time column
* If it's an DataView with timefield, the time column is
* prepended, not moveable and removeable
* @param timeFieldName
*/
export function getTimeColumn(timeFieldName: string): ColumnProps {
return {
name: timeFieldName,
displayName: timeFieldName,
isSortable: true,
isRemoveable: false,
colLeftIdx: -1,
colRightIdx: -1,
};
}
/**
* A given array of column names returns an array of properties
* necessary to display the columns. If the given dataView
* has a timefield, a time column is prepended
* @param columns
* @param dataView
* @param hideTimeField
* @param isShortDots
*/
export function getDisplayedColumns(
columns: string[],
dataView: DataView,
hideTimeField: boolean,
isShortDots: boolean
) {
if (!Array.isArray(columns) || typeof dataView !== 'object' || !dataView.getFieldByName) {
return [];
}
const columnProps =
columns.length === 0
? [
{
name: '__document__',
displayName: i18n.translate('discover.docTable.tableHeader.documentHeader', {
defaultMessage: 'Document',
}),
isSortable: false,
isRemoveable: false,
colLeftIdx: -1,
colRightIdx: -1,
},
]
: columns.map((column, idx) => {
const field = dataView.getFieldByName(column);
return {
name: column,
displayName: field?.displayName ?? column,
isSortable: !!(field && field.sortable),
isRemoveable: column !== '_source' || columns.length > 1,
colLeftIdx: idx - 1 < 0 ? -1 : idx - 1,
colRightIdx: idx + 1 >= columns.length ? -1 : idx + 1,
};
});
return !hideTimeField && dataView.timeFieldName
? [getTimeColumn(dataView.timeFieldName), ...columnProps]
: columnProps;
}

View file

@ -1,20 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the "Elastic License
* 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
* Public License v 1"; you may not use this file except in compliance with, at
* your election, the "Elastic License 2.0", the "GNU Affero General Public
* License v3.0 only", or the "Server Side Public License, v 1".
*/
import React from 'react';
import { EuiIconTip } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
export function DocViewTableScoreSortWarning() {
const tooltipContent = i18n.translate('discover.docViews.table.scoreSortWarningTooltip', {
defaultMessage: 'In order to retrieve values for _score, you must sort by it.',
});
return <EuiIconTip content={tooltipContent} color="warning" size="s" type="warning" />;
}

View file

@ -1,223 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the "Elastic License
* 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
* Public License v 1"; you may not use this file except in compliance with, at
* your election, the "Elastic License 2.0", the "GNU Affero General Public
* License v3.0 only", or the "Server Side Public License, v 1".
*/
import React from 'react';
import { mountWithIntl } from '@kbn/test-jest-helpers';
import type { DataView, DataViewField } from '@kbn/data-views-plugin/public';
import type { SortOrder } from '@kbn/saved-search-plugin/public';
import { TableHeader } from './table_header';
import { findTestSubject } from '@elastic/eui/lib/test';
import { KibanaContextProvider } from '@kbn/kibana-react-plugin/public';
import { DOC_HIDE_TIME_COLUMN_SETTING } from '@kbn/discover-utils';
import { FORMATS_UI_SETTINGS } from '@kbn/field-formats-plugin/common';
const defaultUiSettings = {
get: (key: string) => {
if (key === DOC_HIDE_TIME_COLUMN_SETTING) {
return false;
} else if (key === FORMATS_UI_SETTINGS.SHORT_DOTS_ENABLE) {
return false;
}
},
};
function getMockDataView() {
return {
id: 'test',
title: 'Test',
timeFieldName: 'time',
fields: [],
isTimeNanosBased: () => false,
getFieldByName: (name: string) => {
if (name === 'test1') {
return {
name,
displayName: name,
type: 'string',
aggregatable: false,
searchable: true,
sortable: true,
} as DataViewField;
} else {
return {
name,
displayName: name,
type: 'string',
aggregatable: false,
searchable: true,
sortable: false,
} as DataViewField;
}
},
} as unknown as DataView;
}
function getMockProps(props = {}) {
const defaultProps = {
dataView: getMockDataView(),
hideTimeColumn: false,
columns: ['first', 'middle', 'last'],
defaultSortOrder: 'desc',
sortOrder: [['time', 'asc']] as SortOrder[],
isShortDots: true,
onRemoveColumn: jest.fn(),
onChangeSortOrder: jest.fn(),
onMoveColumn: jest.fn(),
onPageNext: jest.fn(),
onPagePrevious: jest.fn(),
};
return Object.assign({}, defaultProps, props);
}
describe('TableHeader with time column', () => {
const props = getMockProps();
const wrapper = mountWithIntl(
<KibanaContextProvider services={{ uiSettings: defaultUiSettings }}>
<table>
<thead>
<TableHeader {...props} />
</thead>
</table>
</KibanaContextProvider>
);
test('renders correctly', () => {
const docTableHeader = findTestSubject(wrapper, 'docTableHeader');
expect(docTableHeader.getDOMNode()).toMatchSnapshot();
});
test('time column is sortable with button, cycling sort direction', () => {
findTestSubject(wrapper, 'docTableHeaderFieldSort_time').simulate('click');
expect(props.onChangeSortOrder).toHaveBeenCalledWith([['time', 'desc']]);
});
test('time column is not removeable, no button displayed', () => {
const removeButton = findTestSubject(wrapper, 'docTableRemoveHeader-time');
expect(removeButton.length).toBe(0);
});
test('time column is not moveable, no button displayed', () => {
const moveButtonLeft = findTestSubject(wrapper, 'docTableMoveLeftHeader-time');
expect(moveButtonLeft.length).toBe(0);
const moveButtonRight = findTestSubject(wrapper, 'docTableMoveRightHeader-time');
expect(moveButtonRight.length).toBe(0);
});
test('first column is removeable', () => {
const removeButton = findTestSubject(wrapper, 'docTableRemoveHeader-first');
expect(removeButton.length).toBe(1);
removeButton.simulate('click');
expect(props.onRemoveColumn).toHaveBeenCalledWith('first');
});
test('first column is not moveable to the left', () => {
const moveButtonLeft = findTestSubject(wrapper, 'docTableMoveLeftHeader-first');
expect(moveButtonLeft.length).toBe(0);
});
test('first column is moveable to the right', () => {
const moveButtonRight = findTestSubject(wrapper, 'docTableMoveRightHeader-first');
expect(moveButtonRight.length).toBe(1);
moveButtonRight.simulate('click');
expect(props.onMoveColumn).toHaveBeenCalledWith('first', 1);
});
test('middle column is moveable to the left', () => {
const moveButtonLeft = findTestSubject(wrapper, 'docTableMoveLeftHeader-middle');
expect(moveButtonLeft.length).toBe(1);
moveButtonLeft.simulate('click');
expect(props.onMoveColumn).toHaveBeenCalledWith('middle', 0);
});
test('middle column is moveable to the right', () => {
const moveButtonRight = findTestSubject(wrapper, 'docTableMoveRightHeader-middle');
expect(moveButtonRight.length).toBe(1);
moveButtonRight.simulate('click');
expect(props.onMoveColumn).toHaveBeenCalledWith('middle', 2);
});
test('last column moveable to the left', () => {
const moveButtonLeft = findTestSubject(wrapper, 'docTableMoveLeftHeader-last');
expect(moveButtonLeft.length).toBe(1);
moveButtonLeft.simulate('click');
expect(props.onMoveColumn).toHaveBeenCalledWith('last', 1);
});
});
describe('TableHeader without time column', () => {
const props = getMockProps({ hideTimeColumn: true });
const wrapper = mountWithIntl(
<KibanaContextProvider
services={{
...defaultUiSettings,
uiSettings: {
get: (key: string) => {
if (key === DOC_HIDE_TIME_COLUMN_SETTING) {
return true;
}
},
},
}}
>
<table>
<thead>
<TableHeader {...props} />
</thead>
</table>
</KibanaContextProvider>
);
test('renders correctly', () => {
const docTableHeader = findTestSubject(wrapper, 'docTableHeader');
expect(docTableHeader.getDOMNode()).toMatchSnapshot();
});
test('first column is removeable', () => {
const removeButton = findTestSubject(wrapper, 'docTableRemoveHeader-first');
expect(removeButton.length).toBe(1);
removeButton.simulate('click');
expect(props.onRemoveColumn).toHaveBeenCalledWith('first');
});
test('first column is not moveable to the left', () => {
const moveButtonLeft = findTestSubject(wrapper, 'docTableMoveLeftHeader-first');
expect(moveButtonLeft.length).toBe(0);
});
test('first column is moveable to the right', () => {
const moveButtonRight = findTestSubject(wrapper, 'docTableMoveRightHeader-first');
expect(moveButtonRight.length).toBe(1);
moveButtonRight.simulate('click');
expect(props.onMoveColumn).toHaveBeenCalledWith('first', 1);
});
test('middle column is moveable to the left', () => {
const moveButtonLeft = findTestSubject(wrapper, 'docTableMoveLeftHeader-middle');
expect(moveButtonLeft.length).toBe(1);
moveButtonLeft.simulate('click');
expect(props.onMoveColumn).toHaveBeenCalledWith('middle', 0);
});
test('middle column is moveable to the right', () => {
const moveButtonRight = findTestSubject(wrapper, 'docTableMoveRightHeader-middle');
expect(moveButtonRight.length).toBe(1);
moveButtonRight.simulate('click');
expect(props.onMoveColumn).toHaveBeenCalledWith('middle', 2);
});
test('last column moveable to the left', () => {
const moveButtonLeft = findTestSubject(wrapper, 'docTableMoveLeftHeader-last');
expect(moveButtonLeft.length).toBe(1);
moveButtonLeft.simulate('click');
expect(props.onMoveColumn).toHaveBeenCalledWith('last', 1);
});
});

View file

@ -1,71 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the "Elastic License
* 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
* Public License v 1"; you may not use this file except in compliance with, at
* your election, the "Elastic License 2.0", the "GNU Affero General Public
* License v3.0 only", or the "Server Side Public License, v 1".
*/
import React, { useMemo } from 'react';
import type { DataView } from '@kbn/data-views-plugin/public';
import { FORMATS_UI_SETTINGS } from '@kbn/field-formats-plugin/common';
import type { SortOrder } from '@kbn/saved-search-plugin/public';
import { DOC_HIDE_TIME_COLUMN_SETTING, SORT_DEFAULT_ORDER_SETTING } from '@kbn/discover-utils';
import { TableHeaderColumn } from './table_header_column';
import { getDisplayedColumns } from './helpers';
import { getDefaultSort } from '../../../../utils/sorting';
import { useDiscoverServices } from '../../../../hooks/use_discover_services';
interface Props {
columns: string[];
dataView: DataView;
onChangeSortOrder?: (sortOrder: SortOrder[]) => void;
onMoveColumn?: (name: string, index: number) => void;
onRemoveColumn?: (name: string) => void;
sortOrder: SortOrder[];
}
export function TableHeader({
columns,
dataView,
onChangeSortOrder,
onMoveColumn,
onRemoveColumn,
sortOrder,
}: Props) {
const { uiSettings } = useDiscoverServices();
const [defaultSortOrder, hideTimeColumn, isShortDots] = useMemo(
() => [
uiSettings.get(SORT_DEFAULT_ORDER_SETTING, 'desc'),
uiSettings.get(DOC_HIDE_TIME_COLUMN_SETTING, false),
uiSettings.get(FORMATS_UI_SETTINGS.SHORT_DOTS_ENABLE),
],
[uiSettings]
);
const displayedColumns = getDisplayedColumns(columns, dataView, hideTimeColumn, isShortDots);
return (
<tr data-test-subj="docTableHeader" className="kbnDocTableHeader">
<th css={{ width: '24px' }} />
{displayedColumns.map((col, index) => {
return (
<TableHeaderColumn
key={`${col.name}-${index}`}
{...col}
customLabel={dataView.getFieldByName(col.name)?.customLabel}
isTimeColumn={dataView.timeFieldName === col.name}
sortOrder={
sortOrder.length
? sortOrder
: getDefaultSort(dataView, defaultSortOrder, hideTimeColumn, false)
}
onMoveColumn={onMoveColumn}
onRemoveColumn={onRemoveColumn}
onChangeSortOrder={onChangeSortOrder}
/>
);
})}
</tr>
);
}

View file

@ -1,234 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the "Elastic License
* 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
* Public License v 1"; you may not use this file except in compliance with, at
* your election, the "Elastic License 2.0", the "GNU Affero General Public
* License v3.0 only", or the "Server Side Public License, v 1".
*/
import React from 'react';
import { i18n } from '@kbn/i18n';
import { EuiButtonIcon, EuiToolTip, EuiIconTip } from '@elastic/eui';
import type { SortOrder } from '@kbn/saved-search-plugin/public';
import { DocViewTableScoreSortWarning } from './score_sort_warning';
interface Props {
colLeftIdx: number; // idx of the column to the left, -1 if moving is not possible
colRightIdx: number; // idx of the column to the right, -1 if moving is not possible
displayName?: string;
isRemoveable: boolean;
isSortable: boolean;
isTimeColumn: boolean;
customLabel?: string;
name: string;
onChangeSortOrder?: (sortOrder: SortOrder[]) => void;
onMoveColumn?: (name: string, idx: number) => void;
onRemoveColumn?: (name: string) => void;
sortOrder: SortOrder[];
}
interface IconProps {
iconType: string;
color: 'primary' | 'text';
}
interface IconButtonProps {
active: boolean;
ariaLabel: string;
className: string;
iconProps: IconProps;
onClick: () => void | undefined;
testSubject: string;
tooltip: string;
}
const sortDirectionToIcon: Record<string, IconProps> = {
desc: { iconType: 'sortDown', color: 'primary' },
asc: { iconType: 'sortUp', color: 'primary' },
'': { iconType: 'sortable', color: 'text' },
};
const ICON_BUTTON_STYLE = { width: 12, height: 12 };
export function TableHeaderColumn({
colLeftIdx,
colRightIdx,
displayName,
isRemoveable,
isSortable,
isTimeColumn,
customLabel,
name,
onChangeSortOrder,
onMoveColumn,
onRemoveColumn,
sortOrder,
}: Props) {
const [, sortDirection = ''] = sortOrder.find((sortPair) => name === sortPair[0]) || [];
const curSortWithoutCol = sortOrder.filter((pair) => pair[0] !== name);
const curColSort = sortOrder.find((pair) => pair[0] === name);
const curColSortDir = (curColSort && curColSort[1]) || '';
const fieldName = customLabel ?? displayName;
const timeAriaLabel = i18n.translate(
'discover.docTable.tableHeader.timeFieldIconTooltipAriaLabel',
{
defaultMessage: '{timeFieldName} - this field represents the time that events occurred.',
values: { timeFieldName: fieldName },
}
);
const timeTooltip = i18n.translate('discover.docTable.tableHeader.timeFieldIconTooltip', {
defaultMessage: 'This field represents the time that events occurred.',
});
// If this is the _score column, and _score is not one of the columns inside the sort, show a
// warning that the _score will not be retrieved from Elasticsearch
const showScoreSortWarning = name === '_score' && !curColSort;
const handleChangeSortOrder = () => {
if (!onChangeSortOrder) return;
// Cycle goes Unsorted -> Asc -> Desc -> Unsorted
if (curColSort === undefined) {
onChangeSortOrder([...curSortWithoutCol, [name, 'asc']]);
} else if (curColSortDir === 'asc') {
onChangeSortOrder([...curSortWithoutCol, [name, 'desc']]);
} else if (curColSortDir === 'desc' && curSortWithoutCol.length === 0) {
// If we're at the end of the cycle and this is the only existing sort, we switch
// back to ascending sort instead of removing it.
onChangeSortOrder([[name, 'asc']]);
} else {
onChangeSortOrder(curSortWithoutCol);
}
};
const getSortButtonAriaLabel = () => {
const sortAscendingMessage = i18n.translate(
'discover.docTable.tableHeader.sortByColumnAscendingAriaLabel',
{
defaultMessage: 'Sort {columnName} ascending',
values: { columnName: name },
}
);
const sortDescendingMessage = i18n.translate(
'discover.docTable.tableHeader.sortByColumnDescendingAriaLabel',
{
defaultMessage: 'Sort {columnName} descending',
values: { columnName: name },
}
);
const stopSortingMessage = i18n.translate(
'discover.docTable.tableHeader.sortByColumnUnsortedAriaLabel',
{
defaultMessage: 'Stop sorting on {columnName}',
values: { columnName: name },
}
);
if (curColSort === undefined) {
return sortAscendingMessage;
} else if (sortDirection === 'asc') {
return sortDescendingMessage;
} else if (sortDirection === 'desc' && curSortWithoutCol.length === 0) {
return sortAscendingMessage;
} else {
return stopSortingMessage;
}
};
// action buttons displayed on the right side of the column name
const buttons: IconButtonProps[] = [
// Sort Button
{
active: isSortable && typeof onChangeSortOrder === 'function',
ariaLabel: getSortButtonAriaLabel(),
className: !sortDirection ? 'kbnDocTableHeader__sortChange' : '',
iconProps: sortDirectionToIcon[sortDirection],
onClick: handleChangeSortOrder,
testSubject: `docTableHeaderFieldSort_${name}`,
tooltip: getSortButtonAriaLabel(),
},
// Remove Button
{
active: isRemoveable && typeof onRemoveColumn === 'function',
ariaLabel: i18n.translate('discover.docTable.tableHeader.removeColumnButtonAriaLabel', {
defaultMessage: 'Remove {columnName} column',
values: { columnName: name },
}),
className: 'kbnDocTableHeader__move',
iconProps: { iconType: 'cross', color: 'text' },
onClick: () => onRemoveColumn && onRemoveColumn(name),
testSubject: `docTableRemoveHeader-${name}`,
tooltip: i18n.translate('discover.docTable.tableHeader.removeColumnButtonTooltip', {
defaultMessage: 'Remove Column',
}),
},
// Move Left Button
{
active: colLeftIdx >= 0 && typeof onMoveColumn === 'function',
ariaLabel: i18n.translate('discover.docTable.tableHeader.moveColumnLeftButtonAriaLabel', {
defaultMessage: 'Move {columnName} column to the left',
values: { columnName: name },
}),
className: 'kbnDocTableHeader__move',
iconProps: { iconType: 'sortLeft', color: 'text' },
onClick: () => onMoveColumn && onMoveColumn(name, colLeftIdx),
testSubject: `docTableMoveLeftHeader-${name}`,
tooltip: i18n.translate('discover.docTable.tableHeader.moveColumnLeftButtonTooltip', {
defaultMessage: 'Move column to the left',
}),
},
// Move Right Button
{
active: colRightIdx >= 0 && typeof onMoveColumn === 'function',
ariaLabel: i18n.translate('discover.docTable.tableHeader.moveColumnRightButtonAriaLabel', {
defaultMessage: 'Move {columnName} column to the right',
values: { columnName: name },
}),
className: 'kbnDocTableHeader__move',
iconProps: { iconType: 'sortRight', color: 'text' },
onClick: () => onMoveColumn && onMoveColumn(name, colRightIdx),
testSubject: `docTableMoveRightHeader-${name}`,
tooltip: i18n.translate('discover.docTable.tableHeader.moveColumnRightButtonTooltip', {
defaultMessage: 'Move column to the right',
}),
},
];
return (
<th data-test-subj="docTableHeaderField">
<span data-test-subj={`docTableHeader-${name}`} className="kbnDocTableHeader__actions">
{showScoreSortWarning && <DocViewTableScoreSortWarning />}
{fieldName}
{isTimeColumn && (
<EuiIconTip
key="time-icon"
type="clock"
aria-label={timeAriaLabel}
content={timeTooltip}
/>
)}
{buttons
.filter((button) => button.active)
.map((button, idx) => (
<EuiToolTip
id={`docTableHeader-${name}-tt`}
content={button.tooltip}
key={`button-${idx}`}
>
<EuiButtonIcon
aria-label={button.ariaLabel}
className={button.className}
data-test-subj={button.testSubject}
onClick={button.onClick}
iconSize="s"
style={ICON_BUTTON_STYLE}
{...button.iconProps}
/>
</EuiToolTip>
))}
</span>
</th>
);
}

View file

@ -1,141 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the "Elastic License
* 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
* Public License v 1"; you may not use this file except in compliance with, at
* your election, the "Elastic License 2.0", the "GNU Affero General Public
* License v3.0 only", or the "Server Side Public License, v 1".
*/
import React from 'react';
import { mountWithIntl, findTestSubject } from '@kbn/test-jest-helpers';
import { TableRow, TableRowProps } from './table_row';
import { createFilterManagerMock } from '@kbn/data-plugin/public/query/filter_manager/filter_manager.mock';
import { dataViewWithTimefieldMock } from '../../../__mocks__/data_view_with_timefield';
import { KibanaContextProvider } from '@kbn/kibana-react-plugin/public';
import { discoverServiceMock } from '../../../__mocks__/services';
import { DOC_HIDE_TIME_COLUMN_SETTING, MAX_DOC_FIELDS_DISPLAYED } from '@kbn/discover-utils';
import { buildDataTableRecord } from '@kbn/discover-utils';
import type { EsHitRecord } from '@kbn/discover-utils/types';
jest.mock('../utils/row_formatter', () => {
const originalModule = jest.requireActual('../utils/row_formatter');
return {
...originalModule,
formatRow: () => {
return <span data-test-subj="document-column-test">mocked_document_cell</span>;
},
};
});
const mountComponent = (props: TableRowProps) => {
return mountWithIntl(
<KibanaContextProvider
services={{
...discoverServiceMock,
uiSettings: {
get: (key: string) => {
if (key === DOC_HIDE_TIME_COLUMN_SETTING) {
return true;
} else if (key === MAX_DOC_FIELDS_DISPLAYED) {
return 100;
}
},
},
}}
>
<table>
<tbody>
<TableRow {...props} />
</tbody>
</table>
</KibanaContextProvider>
);
};
const mockHit = {
_index: 'mock_index',
_id: '1',
_score: 1,
_type: '_doc',
fields: [
{
timestamp: '2020-20-01T12:12:12.123',
},
],
_source: { message: 'mock_message', bytes: 20 },
} as unknown as EsHitRecord;
const mockFilterManager = createFilterManagerMock();
describe('Doc table row component', () => {
const mockInlineFilter = jest.fn();
const defaultProps = {
columns: ['_source'],
filter: mockInlineFilter,
dataView: dataViewWithTimefieldMock,
row: buildDataTableRecord(mockHit, dataViewWithTimefieldMock),
useNewFieldsApi: true,
filterManager: mockFilterManager,
addBasePath: (path: string) => path,
} as unknown as TableRowProps;
it('should render __document__ column', () => {
const component = mountComponent({ ...defaultProps, columns: [] });
const docTableField = findTestSubject(component, 'docTableField');
expect(docTableField.first().text()).toBe('mocked_document_cell');
});
it('should render message, _index and bytes fields', () => {
const component = mountComponent({ ...defaultProps, columns: ['message', '_index', 'bytes'] });
const fields = findTestSubject(component, 'docTableField');
expect(fields.first().text()).toBe('mock_message');
expect(fields.last().text()).toBe('20');
expect(fields.length).toBe(3);
});
it('should apply filter when pressed', () => {
const component = mountComponent({ ...defaultProps, columns: ['bytes'] });
const fields = findTestSubject(component, 'docTableField');
expect(fields.first().text()).toBe('20');
const filterInButton = findTestSubject(component, 'docTableCellFilter');
filterInButton.simulate('click');
expect(mockInlineFilter).toHaveBeenCalledWith(
dataViewWithTimefieldMock.getFieldByName('bytes'),
20,
'+'
);
});
describe('details row', () => {
it('should be empty by default', () => {
const component = mountComponent(defaultProps);
expect(findTestSubject(component, 'docViewerRowDetailsTitle').exists()).toBeFalsy();
});
it('should expand the detail row when the toggle arrow is clicked', () => {
const component = mountComponent(defaultProps);
const toggleButton = findTestSubject(component, 'docTableExpandToggleColumn');
expect(findTestSubject(component, 'docViewerRowDetailsTitle').exists()).toBeFalsy();
toggleButton.simulate('click');
expect(findTestSubject(component, 'docViewerRowDetailsTitle').exists()).toBeTruthy();
});
it('should hide the single/surrounding views for ES|QL mode', () => {
const props = {
...defaultProps,
isEsqlMode: true,
};
const component = mountComponent(props);
const toggleButton = findTestSubject(component, 'docTableExpandToggleColumn');
toggleButton.simulate('click');
expect(findTestSubject(component, 'docViewerRowDetailsTitle').text()).toBe('Expanded result');
expect(findTestSubject(component, 'docTableRowAction').length).toBeFalsy();
});
});
});

View file

@ -1,239 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the "Elastic License
* 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
* Public License v 1"; you may not use this file except in compliance with, at
* your election, the "Elastic License 2.0", the "GNU Affero General Public
* License v3.0 only", or the "Server Side Public License, v 1".
*/
import React, { Fragment, useCallback, useMemo, useState } from 'react';
import classNames from 'classnames';
import { i18n } from '@kbn/i18n';
import { EuiButtonEmpty, EuiIcon } from '@elastic/eui';
import { DataView } from '@kbn/data-views-plugin/public';
import { Filter } from '@kbn/es-query';
import type {
DataTableRecord,
EsHitRecord,
ShouldShowFieldInTableHandler,
} from '@kbn/discover-utils/types';
import { formatFieldValue } from '@kbn/discover-utils';
import { DOC_HIDE_TIME_COLUMN_SETTING, MAX_DOC_FIELDS_DISPLAYED } from '@kbn/discover-utils';
import type { DocViewFilterFn } from '@kbn/unified-doc-viewer/types';
import { UnifiedDocViewer } from '@kbn/unified-doc-viewer-plugin/public';
import { TableCell } from './table_row/table_cell';
import { formatRow, formatTopLevelObject } from '../utils/row_formatter';
import { TableRowDetails } from './table_row_details';
import { useDiscoverServices } from '../../../hooks/use_discover_services';
export type DocTableRow = EsHitRecord & {
isAnchor?: boolean;
};
export interface TableRowProps {
columns: string[];
filter?: DocViewFilterFn;
filters?: Filter[];
isEsqlMode?: boolean;
savedSearchId?: string;
row: DataTableRecord;
rows: DataTableRecord[];
dataView: DataView;
useNewFieldsApi: boolean;
shouldShowFieldHandler: ShouldShowFieldInTableHandler;
onAddColumn?: (column: string) => void;
onRemoveColumn?: (column: string) => void;
}
export const TableRow = ({
filters,
isEsqlMode,
columns,
filter,
savedSearchId,
row,
rows,
dataView,
useNewFieldsApi,
shouldShowFieldHandler,
onAddColumn,
onRemoveColumn,
}: TableRowProps) => {
const { uiSettings, fieldFormats } = useDiscoverServices();
const [maxEntries, hideTimeColumn] = useMemo(
() => [
uiSettings.get(MAX_DOC_FIELDS_DISPLAYED),
uiSettings.get(DOC_HIDE_TIME_COLUMN_SETTING, false),
],
[uiSettings]
);
const [open, setOpen] = useState(false);
const docTableRowClassName = classNames('kbnDocTable__row', {
'kbnDocTable__row--highlight': row.isAnchor,
});
const anchorDocTableRowSubj = row.isAnchor ? ' docTableAnchorRow' : '';
const mapping = useMemo(() => dataView.fields.getByName, [dataView]);
// toggle display of the rows details, a full list of the fields from each row
const toggleRow = () => setOpen((prevOpen) => !prevOpen);
/**
* Fill an element with the value of a field
*/
const displayField = (fieldName: string) => {
// If we're formatting the _source column, don't use the regular field formatter,
// but our Discover mechanism to format a hit in a better human-readable way.
if (fieldName === '_source') {
return formatRow(row, dataView, shouldShowFieldHandler, maxEntries, fieldFormats);
}
const formattedField = formatFieldValue(
row.flattened[fieldName],
row.raw,
fieldFormats,
dataView,
mapping(fieldName)
);
return (
// formatFieldValue always returns sanitized HTML
// eslint-disable-next-line react/no-danger
<div className="dscTruncateByHeight" dangerouslySetInnerHTML={{ __html: formattedField }} />
);
};
const inlineFilter = useCallback(
(column: string, type: '+' | '-') => {
const field = dataView.fields.getByName(column);
filter?.(field!, row.flattened[column], type);
},
[filter, dataView.fields, row.flattened]
);
const rowCells = [
<td className="kbnDocTableCell__toggleDetails" key="toggleDetailsCell">
<EuiButtonEmpty
onClick={toggleRow}
size="xs"
aria-expanded={open}
aria-label={i18n.translate('discover.docTable.tableRow.toggleRowDetailsButtonAriaLabel', {
defaultMessage: 'Toggle row details',
})}
data-test-subj="docTableExpandToggleColumn"
>
{open ? (
<EuiIcon type="arrowDown" color="text" size="s" />
) : (
<EuiIcon type="arrowRight" color="text" size="s" />
)}
</EuiButtonEmpty>
</td>,
];
if (dataView.timeFieldName && !hideTimeColumn) {
rowCells.push(
<TableCell
key={dataView.timeFieldName}
timefield={true}
formatted={displayField(dataView.timeFieldName)}
filterable={Boolean(mapping(dataView.timeFieldName)?.filterable && filter)}
column={dataView.timeFieldName}
inlineFilter={inlineFilter}
/>
);
}
if (columns.length === 0 && useNewFieldsApi) {
const formatted = formatRow(row, dataView, shouldShowFieldHandler, maxEntries, fieldFormats);
rowCells.push(
<TableCell
key="__document__"
timefield={false}
sourcefield={true}
formatted={formatted}
filterable={false}
column="__document__"
inlineFilter={inlineFilter}
/>
);
} else {
columns.forEach(function (column: string, index) {
const cellKey = `${column}-${index}`;
if (useNewFieldsApi && !mapping(column) && row.raw.fields && !row.raw.fields[column]) {
const innerColumns = Object.fromEntries(
Object.entries(row.raw.fields).filter(([key]) => {
return key.indexOf(`${column}.`) === 0;
})
);
rowCells.push(
<TableCell
key={cellKey}
timefield={false}
sourcefield={true}
formatted={formatTopLevelObject(row, innerColumns, dataView, maxEntries)}
filterable={false}
column={column}
inlineFilter={inlineFilter}
/>
);
} else {
// Check whether the field is defined as filterable in the mapping and does
// NOT have ignored values in it to determine whether we want to allow filtering.
// We should improve this and show a helpful tooltip why the filter buttons are not
// there/disabled when there are ignored values.
const isFilterable = Boolean(
mapping(column)?.filterable &&
typeof filter === 'function' &&
!row.raw._ignored?.includes(column)
);
rowCells.push(
<TableCell
key={cellKey}
timefield={false}
sourcefield={column === '_source'}
formatted={displayField(column)}
filterable={isFilterable}
column={column}
inlineFilter={inlineFilter}
/>
);
}
});
}
return (
<Fragment>
<tr data-test-subj={`docTableRow${anchorDocTableRowSubj}`} className={docTableRowClassName}>
{rowCells}
</tr>
<tr data-test-subj="docTableDetailsRow" className="kbnDocTableDetails__row">
{open && (
<TableRowDetails
colLength={(columns.length || 1) + 2}
isTimeBased={dataView.isTimeBased()}
dataView={dataView}
rowIndex={row.raw._index}
rowId={row.raw._id}
columns={columns}
filters={filters}
savedSearchId={savedSearchId}
isEsqlMode={isEsqlMode}
>
<UnifiedDocViewer
columns={columns}
filter={filter}
hit={row}
dataView={dataView}
onAddColumn={onAddColumn}
onRemoveColumn={onRemoveColumn}
textBasedHits={isEsqlMode ? rows : undefined}
/>
</TableRowDetails>
)}
</tr>
</Fragment>
);
};

View file

@ -1,369 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Doc table cell component renders a cell with filter buttons if it is filterable 1`] = `
<TableCell
column="foo"
filterable={true}
formatted={
<span>
formatted content
</span>
}
inlineFilter={[Function]}
sourcefield={false}
timefield={true}
>
<td
className="eui-textNoWrap kbnDocTableCell--extraWidth"
data-test-subj="docTableField"
>
<span>
formatted content
</span>
<TableCellActions
handleFilterFor={[Function]}
handleFilterOut={[Function]}
>
<span
className="kbnDocTableCell__filter"
>
<EuiToolTip
className="kbnDocTableCell__filterButton"
content="Filter for value"
delay="regular"
display="inlineBlock"
position="bottom"
>
<EuiToolTipAnchor
display="inlineBlock"
id="generated-id"
isVisible={false}
onBlur={[Function]}
onFocus={[Function]}
onKeyDown={[Function]}
onMouseOut={[Function]}
onMouseOver={[Function]}
>
<span
css="unknown styles"
onKeyDown={[Function]}
onMouseOut={[Function]}
onMouseOver={[Function]}
>
<Insertion
cache={
Object {
"insert": [Function],
"inserted": Object {
"jcaat8-euiToolTipAnchor-inlineBlock": true,
},
"key": "css",
"nonce": undefined,
"registered": Object {},
"sheet": StyleSheet {
"_alreadyInsertedOrderInsensitiveRule": true,
"_insertTag": [Function],
"before": null,
"container": <head>
<style
data-styled="active"
data-styled-version="5.3.11"
/>
<style
data-styled="active"
data-styled-version="5.3.11"
/>
<style
data-emotion="css"
data-s=""
>
.emotion-euiToolTipAnchor-inlineBlock{display:inline-block;}
</style>
<style
data-emotion="css"
data-s=""
>
.emotion-euiToolTipAnchor-inlineBlock *[disabled]{pointer-events:none;}
</style>
</head>,
"ctr": 2,
"insertionPoint": undefined,
"isSpeedy": false,
"key": "css",
"nonce": undefined,
"prepend": undefined,
"tags": Array [
<style
data-emotion="css"
data-s=""
>
.emotion-euiToolTipAnchor-inlineBlock{display:inline-block;}
</style>,
<style
data-emotion="css"
data-s=""
>
.emotion-euiToolTipAnchor-inlineBlock *[disabled]{pointer-events:none;}
</style>,
],
},
}
}
isStringTag={true}
serialized={
Object {
"map": undefined,
"name": "jcaat8-euiToolTipAnchor-inlineBlock",
"next": undefined,
"styles": "*[disabled]{pointer-events:none;};label:euiToolTipAnchor;;;display:inline-block;label:inlineBlock;;;",
"toString": [Function],
}
}
/>
<span
className="euiToolTipAnchor emotion-euiToolTipAnchor-inlineBlock"
onKeyDown={[Function]}
onMouseOut={[Function]}
onMouseOver={[Function]}
>
<button
aria-label="Filter for value"
className="kbnDocTableRowFilterButton"
data-test-subj="docTableCellFilter"
onBlur={[Function]}
onClick={[Function]}
onFocus={[Function]}
>
<EuiIcon
color="primary"
size="s"
type="plusInCircle"
>
<span
color="primary"
data-euiicon-type="plusInCircle"
size="s"
/>
</EuiIcon>
</button>
</span>
</span>
</EuiToolTipAnchor>
</EuiToolTip>
<EuiToolTip
className="kbnDocTableCell__filterButton"
content="Filter out value"
delay="regular"
display="inlineBlock"
position="bottom"
>
<EuiToolTipAnchor
display="inlineBlock"
id="generated-id"
isVisible={false}
onBlur={[Function]}
onFocus={[Function]}
onKeyDown={[Function]}
onMouseOut={[Function]}
onMouseOver={[Function]}
>
<span
css="unknown styles"
onKeyDown={[Function]}
onMouseOut={[Function]}
onMouseOver={[Function]}
>
<Insertion
cache={
Object {
"insert": [Function],
"inserted": Object {
"jcaat8-euiToolTipAnchor-inlineBlock": true,
},
"key": "css",
"nonce": undefined,
"registered": Object {},
"sheet": StyleSheet {
"_alreadyInsertedOrderInsensitiveRule": true,
"_insertTag": [Function],
"before": null,
"container": <head>
<style
data-styled="active"
data-styled-version="5.3.11"
/>
<style
data-styled="active"
data-styled-version="5.3.11"
/>
<style
data-emotion="css"
data-s=""
>
.emotion-euiToolTipAnchor-inlineBlock{display:inline-block;}
</style>
<style
data-emotion="css"
data-s=""
>
.emotion-euiToolTipAnchor-inlineBlock *[disabled]{pointer-events:none;}
</style>
</head>,
"ctr": 2,
"insertionPoint": undefined,
"isSpeedy": false,
"key": "css",
"nonce": undefined,
"prepend": undefined,
"tags": Array [
<style
data-emotion="css"
data-s=""
>
.emotion-euiToolTipAnchor-inlineBlock{display:inline-block;}
</style>,
<style
data-emotion="css"
data-s=""
>
.emotion-euiToolTipAnchor-inlineBlock *[disabled]{pointer-events:none;}
</style>,
],
},
}
}
isStringTag={true}
serialized={
Object {
"map": undefined,
"name": "jcaat8-euiToolTipAnchor-inlineBlock",
"next": undefined,
"styles": "*[disabled]{pointer-events:none;};label:euiToolTipAnchor;;;display:inline-block;label:inlineBlock;;;",
"toString": [Function],
}
}
/>
<span
className="euiToolTipAnchor emotion-euiToolTipAnchor-inlineBlock"
onKeyDown={[Function]}
onMouseOut={[Function]}
onMouseOver={[Function]}
>
<button
aria-label="Filter out value"
className="kbnDocTableRowFilterButton"
data-test-subj="docTableCellFilterNegate"
onBlur={[Function]}
onClick={[Function]}
onFocus={[Function]}
>
<EuiIcon
color="primary"
size="s"
type="minusInCircle"
>
<span
color="primary"
data-euiicon-type="minusInCircle"
size="s"
/>
</EuiIcon>
</button>
</span>
</span>
</EuiToolTipAnchor>
</EuiToolTip>
</span>
</TableCellActions>
</td>
</TableCell>
`;
exports[`Doc table cell component renders a cell without filter buttons if it is not filterable 1`] = `
<TableCell
column="foo"
filterable={false}
formatted={
<span>
formatted content
</span>
}
inlineFilter={[Function]}
sourcefield={false}
timefield={true}
>
<td
className="eui-textNoWrap kbnDocTableCell--extraWidth"
data-test-subj="docTableField"
>
<span>
formatted content
</span>
<span
className="kbnDocTableCell__filter"
/>
</td>
</TableCell>
`;
exports[`Doc table cell component renders a field that is neither a timefield or sourcefield 1`] = `
<TableCell
column="foo"
filterable={false}
formatted={
<span>
formatted content
</span>
}
inlineFilter={[Function]}
sourcefield={false}
timefield={false}
>
<td
className="kbnDocTableCell__dataField eui-textBreakAll eui-textBreakWord"
data-test-subj="docTableField"
>
<span>
formatted content
</span>
<span
className="kbnDocTableCell__filter"
/>
</td>
</TableCell>
`;
exports[`Doc table cell component renders a sourcefield 1`] = `
<TableCell
column="foo"
filterable={false}
formatted={
<span>
formatted content
</span>
}
inlineFilter={[Function]}
sourcefield={true}
timefield={false}
>
<td
className="eui-textBreakAll eui-textBreakWord"
data-test-subj="docTableField"
>
<span>
formatted content
</span>
<span
className="kbnDocTableCell__filter"
/>
</td>
</TableCell>
`;

View file

@ -1,47 +0,0 @@
.kbnDocTableCell__dataField {
white-space: pre-wrap;
}
.kbnDocTableCell__toggleDetails {
padding: $euiSizeXS 0 0 0 !important;
}
/**
* Fixes time column width in Firefox after toggle display of the rows details.
* Described issue - https://github.com/elastic/kibana/pull/104361#issuecomment-894271241
*/
.kbnDocTableCell--extraWidth {
width: 1%;
}
.kbnDocTableCell__filter {
position: absolute;
white-space: nowrap;
right: 0;
}
.kbnDocTableCell__filterButton {
font-size: $euiFontSizeXS;
padding: $euiSizeXS;
}
/**
* 1. Align icon with text in cell.
* 2. Use opacity to make this element accessible to screen readers and keyboard.
* 3. Show on focus to enable keyboard accessibility.
*/
.kbnDocTableRowFilterButton {
appearance: none;
background-color: $euiColorEmptyShade;
border: none;
padding: 0 $euiSizeXS;
font-size: $euiFontSizeS;
line-height: 1; /* 1 */
display: inline-block;
opacity: 0; /* 2 */
&:focus {
opacity: 1; /* 3 */
}
}

View file

@ -1,24 +0,0 @@
/**
* 1. Visually align the actions with the tabs. We can improve this by using flexbox instead, at a later point.
*/
.kbnDocTableDetails__actions {
float: right;
padding-top: $euiSizeS; /* 1 */
}
// Overwrite the border on the bootstrap table
.kbnDocTableDetails__row {
> td {
// Offsets negative margins from an inner flex group
padding: $euiSizeL !important;
tr:hover td {
background: tintOrShade($euiColorLightestShade, 50%, 20%);
}
}
td {
border-top: none !important;
}
}

View file

@ -1,3 +0,0 @@
@import 'cell';
@import 'details';
@import 'open';

View file

@ -1,14 +0,0 @@
/**
* 1. When switching between open and closed, the toggle changes size
* slightly which is a problem because it forces the entire table to
* re-render which is SLOW.
*/
.kbnDocTableOpen__button {
appearance: none;
background-color: transparent;
padding: 0;
border: none;
width: 14px; /* 1 */
height: 14px;
}

View file

@ -1,65 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the "Elastic License
* 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
* Public License v 1"; you may not use this file except in compliance with, at
* your election, the "Elastic License 2.0", the "GNU Affero General Public
* License v3.0 only", or the "Server Side Public License, v 1".
*/
import React from 'react';
import { mount } from 'enzyme';
import { CellProps, TableCell } from './table_cell';
const mountComponent = (props: Omit<CellProps, 'inlineFilter'>) => {
return mount(<TableCell {...props} inlineFilter={() => {}} />);
};
describe('Doc table cell component', () => {
test('renders a cell without filter buttons if it is not filterable', () => {
const component = mountComponent({
filterable: false,
column: 'foo',
timefield: true,
sourcefield: false,
formatted: <span>formatted content</span>,
});
expect(component).toMatchSnapshot();
});
it('renders a cell with filter buttons if it is filterable', () => {
expect(
mountComponent({
filterable: true,
column: 'foo',
timefield: true,
sourcefield: false,
formatted: <span>formatted content</span>,
})
).toMatchSnapshot();
});
it('renders a sourcefield', () => {
expect(
mountComponent({
filterable: false,
column: 'foo',
timefield: false,
sourcefield: true,
formatted: <span>formatted content</span>,
})
).toMatchSnapshot();
});
it('renders a field that is neither a timefield or sourcefield', () => {
expect(
mountComponent({
filterable: false,
column: 'foo',
timefield: false,
sourcefield: false,
formatted: <span>formatted content</span>,
})
).toMatchSnapshot();
});
});

View file

@ -1,43 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the "Elastic License
* 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
* Public License v 1"; you may not use this file except in compliance with, at
* your election, the "Elastic License 2.0", the "GNU Affero General Public
* License v3.0 only", or the "Server Side Public License, v 1".
*/
import React from 'react';
import classNames from 'classnames';
import { TableCellActions } from './table_cell_actions';
export interface CellProps {
timefield: boolean;
sourcefield?: boolean;
formatted: JSX.Element;
filterable: boolean;
column: string;
inlineFilter: (column: string, type: '+' | '-') => void;
}
export const TableCell = (props: CellProps) => {
const classes = classNames({
['eui-textNoWrap kbnDocTableCell--extraWidth']: props.timefield,
['eui-textBreakAll eui-textBreakWord']: props.sourcefield,
['kbnDocTableCell__dataField eui-textBreakAll eui-textBreakWord']:
!props.timefield && !props.sourcefield,
});
const handleFilterFor = () => props.inlineFilter(props.column, '+');
const handleFilterOut = () => props.inlineFilter(props.column, '-');
return (
<td className={classes} data-test-subj="docTableField">
{props.formatted}
{props.filterable ? (
<TableCellActions handleFilterOut={handleFilterOut} handleFilterFor={handleFilterFor} />
) : (
<span className="kbnDocTableCell__filter" />
)}
</td>
);
};

View file

@ -1,61 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the "Elastic License
* 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
* Public License v 1"; you may not use this file except in compliance with, at
* your election, the "Elastic License 2.0", the "GNU Affero General Public
* License v3.0 only", or the "Server Side Public License, v 1".
*/
import React from 'react';
import { EuiIcon, EuiToolTip } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
interface TableCellActionsProps {
handleFilterFor: () => void;
handleFilterOut: () => void;
}
export const TableCellActions = ({ handleFilterFor, handleFilterOut }: TableCellActionsProps) => {
return (
<span className="kbnDocTableCell__filter">
<EuiToolTip
className="kbnDocTableCell__filterButton"
position="bottom"
content={i18n.translate('discover.docTable.tableRow.filterForValueButtonTooltip', {
defaultMessage: 'Filter for value',
})}
>
<button
className="kbnDocTableRowFilterButton"
data-test-subj="docTableCellFilter"
aria-label={i18n.translate('discover.docTable.tableRow.filterForValueButtonAriaLabel', {
defaultMessage: 'Filter for value',
})}
onClick={handleFilterFor}
>
<EuiIcon type="plusInCircle" size="s" color="primary" />
</button>
</EuiToolTip>
<EuiToolTip
className="kbnDocTableCell__filterButton"
position="bottom"
content={i18n.translate('discover.docTable.tableRow.filterOutValueButtonTooltip', {
defaultMessage: 'Filter out value',
})}
>
<button
className="kbnDocTableRowFilterButton"
data-test-subj="docTableCellFilterNegate"
aria-label={i18n.translate('discover.docTable.tableRow.filterOutValueButtonAriaLabel', {
defaultMessage: 'Filter out value',
})}
onClick={handleFilterOut}
>
<EuiIcon type="minusInCircle" size="s" color="primary" />
</button>
</EuiToolTip>
</span>
);
};

View file

@ -1,127 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the "Elastic License
* 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
* Public License v 1"; you may not use this file except in compliance with, at
* your election, the "Elastic License 2.0", the "GNU Affero General Public
* License v3.0 only", or the "Server Side Public License, v 1".
*/
import React from 'react';
import { EuiFlexGroup, EuiFlexItem, EuiIcon, EuiButtonEmpty, EuiTitle } from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n-react';
import type { DataView } from '@kbn/data-views-plugin/public';
import type { Filter } from '@kbn/es-query';
import { useNavigationProps } from '../../../hooks/use_navigation_props';
interface TableRowDetailsProps {
children: JSX.Element;
colLength: number;
rowIndex: string | undefined;
rowId: string | undefined;
columns: string[];
isTimeBased: boolean;
dataView: DataView;
filters?: Filter[];
savedSearchId?: string;
isEsqlMode?: boolean;
}
export const TableRowDetails = ({
colLength,
isTimeBased,
children,
dataView,
rowIndex,
rowId,
columns,
filters,
savedSearchId,
isEsqlMode,
}: TableRowDetailsProps) => {
const { singleDocHref, contextViewHref, onOpenSingleDoc, onOpenContextView } = useNavigationProps(
{
dataView,
rowIndex,
rowId,
columns,
filters,
savedSearchId,
}
);
return (
<td colSpan={(colLength || 1) + 2}>
<EuiFlexGroup gutterSize="l" justifyContent="spaceBetween" responsive={false}>
<EuiFlexItem grow={false}>
<EuiFlexGroup gutterSize="s" alignItems="center" responsive={false}>
<EuiFlexItem grow={false}>
<EuiIcon type="folderOpen" size="m" />
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiTitle size="xxs" data-test-subj="docViewerRowDetailsTitle">
<h4>
{isEsqlMode && (
<FormattedMessage
id="discover.grid.tableRow.esqlDetailHeading"
defaultMessage="Expanded result"
/>
)}
{!isEsqlMode && (
<FormattedMessage
id="discover.docTable.tableRow.detailHeading"
defaultMessage="Expanded document"
/>
)}
</h4>
</EuiTitle>
</EuiFlexItem>
</EuiFlexGroup>
</EuiFlexItem>
{!isEsqlMode && (
<EuiFlexItem grow={false}>
<EuiFlexGroup gutterSize="l" alignItems="center" responsive={false}>
<EuiFlexItem grow={false}>
{isTimeBased && (
/* eslint-disable-next-line @elastic/eui/href-or-on-click */
<EuiButtonEmpty
size="s"
iconSize="s"
iconType="document"
flush="left"
data-test-subj="docTableRowAction"
href={contextViewHref}
onClick={onOpenContextView}
>
<FormattedMessage
id="discover.docTable.tableRow.viewSurroundingDocumentsLinkText"
defaultMessage="View surrounding documents"
/>
</EuiButtonEmpty>
)}
</EuiFlexItem>
<EuiFlexItem grow={false}>
{/* eslint-disable-next-line @elastic/eui/href-or-on-click */}
<EuiButtonEmpty
size="s"
iconSize="s"
iconType="document"
flush="left"
data-test-subj="docTableRowAction"
href={singleDocHref}
onClick={onOpenSingleDoc}
>
<FormattedMessage
id="discover.docTable.tableRow.viewSingleDocumentLinkText"
defaultMessage="View single document"
/>
</EuiButtonEmpty>
</EuiFlexItem>
</EuiFlexGroup>
</EuiFlexItem>
)}
</EuiFlexGroup>
<div data-test-subj="docViewer">{children}</div>
</td>
);
};

View file

@ -1,38 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the "Elastic License
* 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
* Public License v 1"; you may not use this file except in compliance with, at
* your election, the "Elastic License 2.0", the "GNU Affero General Public
* License v3.0 only", or the "Server Side Public License, v 1".
*/
import React from 'react';
import { DocTableEmbeddable, DocTableEmbeddableProps } from './doc_table_embeddable';
export function DiscoverDocTableEmbeddable(renderProps: DocTableEmbeddableProps) {
return (
<DocTableEmbeddable
columns={renderProps.columns}
rows={renderProps.rows}
rowsPerPageState={renderProps.rowsPerPageState}
sampleSizeState={renderProps.sampleSizeState}
onUpdateRowsPerPage={renderProps.onUpdateRowsPerPage}
totalHitCount={renderProps.totalHitCount}
dataView={renderProps.dataView}
onSort={renderProps.onSort}
onAddColumn={renderProps.onAddColumn}
onMoveColumn={renderProps.onMoveColumn}
onRemoveColumn={renderProps.onRemoveColumn}
sort={renderProps.sort}
filters={renderProps.filters}
onFilter={renderProps.onFilter}
useNewFieldsApi={renderProps.useNewFieldsApi}
searchDescription={renderProps.searchDescription}
sharedItemTitle={renderProps.sharedItemTitle}
isLoading={renderProps.isLoading}
isEsqlMode={renderProps.isEsqlMode}
interceptedWarnings={renderProps.interceptedWarnings}
/>
);
}

View file

@ -1,34 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the "Elastic License
* 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
* Public License v 1"; you may not use this file except in compliance with, at
* your election, the "Elastic License 2.0", the "GNU Affero General Public
* License v3.0 only", or the "Server Side Public License, v 1".
*/
import React, { Fragment } from 'react';
import './index.scss';
import { SkipBottomButton } from '../../application/main/components/skip_bottom_button';
import { DocTableProps, DocTableRenderProps, DocTableWrapper } from './doc_table_wrapper';
const DocTableWrapperMemoized = React.memo(DocTableWrapper);
const renderDocTable = (tableProps: DocTableRenderProps) => {
return (
<Fragment>
<SkipBottomButton onClick={tableProps.onSkipBottomButtonClick} />
<table className="kbn-table table" data-test-subj="docTable">
<thead>{tableProps.renderHeader()}</thead>
<tbody>{tableProps.renderRows(tableProps.rows)}</tbody>
</table>
<span tabIndex={-1} id="discoverBottomMarker">
&#8203;
</span>
</Fragment>
);
};
export const DocTableContext = (props: DocTableProps) => {
return <DocTableWrapperMemoized {...props} render={renderDocTable} />;
};

View file

@ -1,141 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the "Elastic License
* 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
* Public License v 1"; you may not use this file except in compliance with, at
* your election, the "Elastic License 2.0", the "GNU Affero General Public
* License v3.0 only", or the "Server Side Public License, v 1".
*/
import React, { memo, useCallback, useMemo, useRef } from 'react';
import './index.scss';
import { FormattedMessage } from '@kbn/i18n-react';
import { EuiText } from '@elastic/eui';
import { usePager } from '@kbn/discover-utils';
import type { SearchResponseWarning } from '@kbn/search-response-warnings';
import {
ToolBarPagination,
MAX_ROWS_PER_PAGE_OPTION,
} from './components/pager/tool_bar_pagination';
import { DocTableProps, DocTableRenderProps, DocTableWrapper } from './doc_table_wrapper';
import { SavedSearchEmbeddableBase } from '../../embeddable/components/saved_search_embeddable_base';
export interface DocTableEmbeddableProps extends Omit<DocTableProps, 'dataTestSubj'> {
totalHitCount?: number;
rowsPerPageState?: number;
sampleSizeState: number;
interceptedWarnings?: SearchResponseWarning[];
onUpdateRowsPerPage?: (rowsPerPage?: number) => void;
}
export type DocTableEmbeddableSearchProps = Omit<
DocTableEmbeddableProps,
'sampleSizeState' | 'isEsqlMode'
>;
const DocTableWrapperMemoized = memo(DocTableWrapper);
export const DocTableEmbeddable = (props: DocTableEmbeddableProps) => {
const onUpdateRowsPerPage = props.onUpdateRowsPerPage;
const tableWrapperRef = useRef<HTMLDivElement>(null);
const {
curPageIndex,
pageSize,
totalPages,
startIndex,
hasNextPage,
changePageIndex,
changePageSize,
} = usePager({
initialPageSize:
typeof props.rowsPerPageState === 'number' && props.rowsPerPageState > 0
? Math.min(props.rowsPerPageState, MAX_ROWS_PER_PAGE_OPTION)
: 50,
totalItems: props.rows.length,
});
const showPagination = totalPages !== 0;
const scrollTop = useCallback(() => {
if (tableWrapperRef.current) {
tableWrapperRef.current.scrollTo(0, 0);
}
}, []);
const pageOfItems = useMemo(
() => props.rows.slice(startIndex, pageSize + startIndex),
[pageSize, startIndex, props.rows]
);
const onPageChange = useCallback(
(page: number) => {
scrollTop();
changePageIndex(page);
},
[changePageIndex, scrollTop]
);
const onPageSizeChange = useCallback(
(size: number) => {
scrollTop();
changePageSize(size);
onUpdateRowsPerPage?.(size); // to update `rowsPerPage` input param for the embeddable
},
[changePageSize, scrollTop, onUpdateRowsPerPage]
);
const shouldShowLimitedResultsWarning = useMemo(
() => !hasNextPage && props.totalHitCount && props.rows.length < props.totalHitCount,
[hasNextPage, props.rows.length, props.totalHitCount]
);
const renderDocTable = useCallback(
(renderProps: DocTableRenderProps) => {
return (
<div className="kbnDocTable__container">
<table className="kbnDocTable table" data-test-subj="docTable">
<thead>{renderProps.renderHeader()}</thead>
<tbody>{renderProps.renderRows(pageOfItems)}</tbody>
</table>
</div>
);
},
[pageOfItems]
);
return (
<SavedSearchEmbeddableBase
interceptedWarnings={props.interceptedWarnings}
totalHitCount={props.totalHitCount}
isLoading={props.isLoading}
prepend={
shouldShowLimitedResultsWarning ? (
<EuiText grow={false} size="s" color="subdued">
<FormattedMessage
id="discover.docTable.limitedSearchResultLabel"
defaultMessage="Limited to {resultCount} results. Refine your search."
values={{ resultCount: props.sampleSizeState }}
/>
</EuiText>
) : undefined
}
append={
showPagination ? (
<ToolBarPagination
pageSize={pageSize}
pageCount={totalPages}
activePage={curPageIndex}
onPageClick={onPageChange}
onPageSizeChange={onPageSizeChange}
/>
) : undefined
}
>
<DocTableWrapperMemoized
ref={tableWrapperRef}
{...props}
dataTestSubj="embeddedSavedSearchDocTable"
render={renderDocTable}
/>
</SavedSearchEmbeddableBase>
);
};

View file

@ -1,155 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the "Elastic License
* 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
* Public License v 1"; you may not use this file except in compliance with, at
* your election, the "Elastic License 2.0", the "GNU Affero General Public
* License v3.0 only", or the "Server Side Public License, v 1".
*/
import React, { Fragment, memo, useCallback, useEffect, useRef, useState } from 'react';
import './index.scss';
import { FormattedMessage } from '@kbn/i18n-react';
import { debounce } from 'lodash';
import { EuiButtonEmpty } from '@elastic/eui';
import { DocTableProps, DocTableRenderProps, DocTableWrapper } from './doc_table_wrapper';
import { SkipBottomButton } from '../../application/main/components/skip_bottom_button';
import { shouldLoadNextDocPatch } from './utils/should_load_next_doc_patch';
import { useDiscoverServices } from '../../hooks/use_discover_services';
import { getAllowedSampleSize } from '../../utils/get_allowed_sample_size';
import { useAppStateSelector } from '../../application/main/state_management/discover_app_state_container';
const FOOTER_PADDING = { padding: 0 };
const DocTableWrapperMemoized = memo(DocTableWrapper);
interface DocTableInfiniteContentProps extends DocTableRenderProps {
limit: number;
onSetMaxLimit: () => void;
onBackToTop: () => void;
}
const DocTableInfiniteContent = ({
rows,
columnLength,
limit,
onSkipBottomButtonClick,
renderHeader,
renderRows,
onSetMaxLimit,
onBackToTop,
}: DocTableInfiniteContentProps) => {
const { uiSettings } = useDiscoverServices();
const sampleSize = useAppStateSelector((state) =>
getAllowedSampleSize(state.sampleSize, uiSettings)
);
const onSkipBottomButton = useCallback(() => {
onSetMaxLimit();
onSkipBottomButtonClick();
}, [onSetMaxLimit, onSkipBottomButtonClick]);
return (
<Fragment>
<SkipBottomButton onClick={onSkipBottomButton} />
<table className="kbn-table table" data-test-subj="docTable">
<thead>{renderHeader()}</thead>
<tbody>{renderRows(rows.slice(0, limit))}</tbody>
<tfoot>
<tr>
<td colSpan={(columnLength || 1) + 2} style={FOOTER_PADDING}>
{rows.length === sampleSize ? (
<div
className="kbnDocTable__footer"
data-test-subj="discoverDocTableFooter"
tabIndex={-1}
id="discoverBottomMarker"
>
<FormattedMessage
id="discover.howToSeeOtherMatchingDocumentsDescription"
defaultMessage="These are the first {sampleSize} documents matching
your search, refine your search to see others."
values={{ sampleSize }}
/>
<EuiButtonEmpty onClick={onBackToTop} data-test-subj="discoverBackToTop">
<FormattedMessage
id="discover.backToTopLinkText"
defaultMessage="Back to top."
/>
</EuiButtonEmpty>
</div>
) : (
<span tabIndex={-1} id="discoverBottomMarker">
&#8203;
</span>
)}
</td>
</tr>
</tfoot>
</table>
</Fragment>
);
};
export const DocTableInfinite = (props: DocTableProps) => {
const tableWrapperRef = useRef<HTMLDivElement>(null);
const [limit, setLimit] = useState(50);
/**
* depending on which version of Discover is displayed, different elements are scrolling
* and have therefore to be considered for calculation of infinite scrolling
*/
useEffect(() => {
// After mounting table wrapper should be initialized
const scrollDiv = tableWrapperRef.current as HTMLDivElement;
const scrollMobileElem = document.documentElement;
const scheduleCheck = debounce(() => {
const isMobileView = document.getElementsByClassName('dscSidebar__mobile').length > 0;
const usedScrollDiv = isMobileView ? scrollMobileElem : scrollDiv;
if (shouldLoadNextDocPatch(usedScrollDiv)) {
setLimit((prevLimit) => prevLimit + 50);
}
}, 50);
scrollDiv.addEventListener('scroll', scheduleCheck);
window.addEventListener('scroll', scheduleCheck);
scheduleCheck();
return () => {
scrollDiv.removeEventListener('scroll', scheduleCheck);
window.removeEventListener('scroll', scheduleCheck);
};
}, []);
const onBackToTop = useCallback(() => {
const isMobileView = document.getElementsByClassName('dscSidebar__mobile').length > 0;
const focusElem = document.querySelector('.dscSkipButton') as HTMLElement;
focusElem.focus();
// Only the desktop one needs to target a specific container
if (!isMobileView && tableWrapperRef.current) {
tableWrapperRef.current.scrollTo(0, 0);
} else if (window) {
window.scrollTo(0, 0);
}
}, []);
const setMaxLimit = useCallback(() => setLimit(props.rows.length), [props.rows.length]);
const renderDocTable = useCallback(
(tableProps: DocTableRenderProps) => (
<DocTableInfiniteContent
{...tableProps}
limit={limit}
onSetMaxLimit={setMaxLimit}
onBackToTop={onBackToTop}
/>
),
[limit, onBackToTop, setMaxLimit]
);
return <DocTableWrapperMemoized ref={tableWrapperRef} render={renderDocTable} {...props} />;
};

View file

@ -1,81 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the "Elastic License
* 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
* Public License v 1"; you may not use this file except in compliance with, at
* your election, the "Elastic License 2.0", the "GNU Affero General Public
* License v3.0 only", or the "Server Side Public License, v 1".
*/
import React from 'react';
import { EuiIcon, EuiLoadingSpinner } from '@elastic/eui';
import { findTestSubject, mountWithIntl } from '@kbn/test-jest-helpers';
import { dataViewMock } from '@kbn/discover-utils/src/__mocks__';
import { DocTableWrapper, DocTableWrapperProps } from './doc_table_wrapper';
import { discoverServiceMock } from '../../__mocks__/services';
import { KibanaContextProvider } from '@kbn/kibana-react-plugin/public';
import { buildDataTableRecord } from '@kbn/discover-utils';
import type { EsHitRecord } from '@kbn/discover-utils/types';
describe('Doc table component', () => {
const mountComponent = (customProps?: Partial<DocTableWrapperProps>) => {
const props = {
columns: ['_source'],
dataView: dataViewMock,
rows: [
{
_index: 'mock_index',
_id: '1',
_score: 1,
fields: [
{
timestamp: '2020-20-01T12:12:12.123',
},
],
_source: { message: 'mock_message', bytes: 20 },
} as EsHitRecord,
].map((row) => buildDataTableRecord(row, dataViewMock)),
sort: [['order_date', 'desc']],
isLoading: false,
searchDescription: '',
onAddColumn: () => {},
onFilter: () => {},
onMoveColumn: () => {},
onRemoveColumn: () => {},
onSort: () => {},
useNewFieldsApi: true,
dataTestSubj: 'discoverDocTable',
render: () => {
return <div data-test-subj="docTable">mock</div>;
},
...(customProps || {}),
};
return mountWithIntl(
<KibanaContextProvider services={discoverServiceMock}>
<DocTableWrapper {...props} />
</KibanaContextProvider>
);
};
it('should render infinite table correctly', () => {
const component = mountComponent();
expect(findTestSubject(component, 'discoverDocTable').exists()).toBeTruthy();
expect(findTestSubject(component, 'docTable').exists()).toBeTruthy();
expect(component.find('.kbnDocTable__error').exists()).toBeFalsy();
});
it('should render error fallback if rows array is empty', () => {
const component = mountComponent({ rows: [], isLoading: false });
expect(findTestSubject(component, 'discoverDocTable').exists()).toBeTruthy();
expect(findTestSubject(component, 'docTable').exists()).toBeFalsy();
expect(component.find('.kbnDocTable__error').find(EuiIcon).exists()).toBeTruthy();
});
it('should render loading indicator', () => {
const component = mountComponent({ rows: [], isLoading: true });
expect(findTestSubject(component, 'discoverDocTable').exists()).toBeTruthy();
expect(findTestSubject(component, 'docTable').exists()).toBeFalsy();
expect(component.find('.kbnDocTable__error').find(EuiLoadingSpinner).exists()).toBeTruthy();
});
});

View file

@ -1,253 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the "Elastic License
* 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
* Public License v 1"; you may not use this file except in compliance with, at
* your election, the "Elastic License 2.0", the "GNU Affero General Public
* License v3.0 only", or the "Server Side Public License, v 1".
*/
import React, { forwardRef, useCallback, useMemo } from 'react';
import { EuiIcon, EuiLoadingSpinner, EuiSpacer, EuiText } from '@elastic/eui';
import type { DataView, DataViewField } from '@kbn/data-views-plugin/public';
import type { SortOrder } from '@kbn/saved-search-plugin/public';
import { FormattedMessage } from '@kbn/i18n-react';
import { Filter } from '@kbn/es-query';
import type { DataTableRecord } from '@kbn/discover-utils/types';
import { SHOW_MULTIFIELDS, getShouldShowFieldHandler } from '@kbn/discover-utils';
import type { DocViewFilterFn } from '@kbn/unified-doc-viewer/types';
import { TableHeader } from './components/table_header/table_header';
import { TableRow } from './components/table_row';
import { useDiscoverServices } from '../../hooks/use_discover_services';
export interface DocTableProps {
/**
* Rows of classic table
*/
rows: DataTableRecord[];
/**
* Columns of classic table
*/
columns: string[];
/**
* Current DataView
*/
dataView: DataView;
/**
* Current sorting
*/
sort: string[][];
/**
* New fields api switch
*/
useNewFieldsApi: boolean;
/**
* Current search description
*/
searchDescription?: string;
/**
* Current shared item title
*/
sharedItemTitle?: string;
/**
* Current data test subject
*/
dataTestSubj: string;
/**
* Loading state
*/
isLoading: boolean;
/**
* Filters applied by embeddalbe
*/
filters?: Filter[];
/**
* Flag which identifies if Discover operates
* in ES|QL mode
*/
isEsqlMode?: boolean;
/**
* Saved search id
*/
savedSearchId?: string;
/**
* Filter callback
*/
onFilter?: DocViewFilterFn;
/**
* Sorting callback
*/
onSort?: (sort: string[][]) => void;
/**
* Add columns callback
*/
onAddColumn?: (column: string) => void;
/**
* Reordering column callback
*/
onMoveColumn?: (columns: string, newIdx: number) => void;
/**
* Remove column callback
*/
onRemoveColumn?: (column: string) => void;
}
export interface DocTableRenderProps {
columnLength: number;
rows: DataTableRecord[];
renderRows: (row: DataTableRecord[]) => JSX.Element[];
renderHeader: () => JSX.Element;
onSkipBottomButtonClick: () => void;
}
export interface DocTableWrapperProps extends DocTableProps {
/**
* Renders Doc table content
*/
render: (params: DocTableRenderProps) => JSX.Element;
}
const wait = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms));
export const DocTableWrapper = forwardRef(
(
{
render,
columns,
filters,
isEsqlMode,
savedSearchId,
rows,
dataView,
onSort,
onAddColumn,
onMoveColumn,
onRemoveColumn,
sort,
onFilter,
useNewFieldsApi,
searchDescription,
sharedItemTitle,
dataTestSubj,
isLoading,
}: DocTableWrapperProps,
ref
) => {
const { uiSettings } = useDiscoverServices();
const showMultiFields = useMemo(() => uiSettings.get(SHOW_MULTIFIELDS, false), [uiSettings]);
const onSkipBottomButtonClick = useCallback(async () => {
// delay scrolling to after the rows have been rendered
const bottomMarker = document.getElementById('discoverBottomMarker');
while (rows.length !== document.getElementsByClassName('kbnDocTable__row').length) {
await wait(50);
}
bottomMarker!.focus();
await wait(50);
bottomMarker!.blur();
}, [rows]);
const shouldShowFieldHandler = useMemo(
() =>
getShouldShowFieldHandler(
dataView.fields.map((field: DataViewField) => field.name),
dataView,
showMultiFields
),
[dataView, showMultiFields]
);
const renderHeader = useCallback(
() => (
<TableHeader
columns={columns}
dataView={dataView}
onChangeSortOrder={onSort}
onMoveColumn={onMoveColumn}
onRemoveColumn={onRemoveColumn}
sortOrder={sort as SortOrder[]}
/>
),
[columns, dataView, onMoveColumn, onRemoveColumn, onSort, sort]
);
const renderRows = useCallback(
(rowsToRender: DataTableRecord[]) => {
return rowsToRender.map((current) => (
<TableRow
key={`${current.id}${current.raw._score}${current.raw._version}`}
columns={columns}
filters={filters}
savedSearchId={savedSearchId}
filter={onFilter}
dataView={dataView}
row={current}
useNewFieldsApi={useNewFieldsApi}
shouldShowFieldHandler={shouldShowFieldHandler}
onAddColumn={onAddColumn}
onRemoveColumn={onRemoveColumn}
isEsqlMode={isEsqlMode}
rows={rows}
/>
));
},
[
columns,
filters,
savedSearchId,
onFilter,
dataView,
useNewFieldsApi,
shouldShowFieldHandler,
onAddColumn,
onRemoveColumn,
isEsqlMode,
rows,
]
);
return (
<div
className="kbnDocTableWrapper eui-yScroll eui-xScroll"
data-shared-item
data-title={sharedItemTitle}
data-description={searchDescription}
data-test-subj={dataTestSubj}
data-render-complete={!isLoading}
ref={ref as React.MutableRefObject<HTMLDivElement>}
>
{rows.length !== 0 &&
render({
columnLength: columns.length,
rows,
onSkipBottomButtonClick,
renderHeader,
renderRows,
})}
{!rows.length && (
<div className="kbnDocTable__error">
<EuiText size="xs" color="subdued">
{isLoading ? (
<>
<EuiLoadingSpinner />
<EuiSpacer size="m" />
<FormattedMessage id="discover.loadingResults" defaultMessage="Loading results" />
</>
) : (
<>
<EuiIcon type="discoverApp" size="m" color="subdued" />
<EuiSpacer size="m" />
<FormattedMessage
id="discover.docTable.noResultsTitle"
defaultMessage="No results found"
/>
</>
)}
</EuiText>
</div>
)}
</div>
);
}
);

View file

@ -1,2 +0,0 @@
@import 'doc_table';
@import 'components/index';

View file

@ -1,7 +0,0 @@
// Special handling for images coming from the image field formatter
// See discover_grid.scss for more explanation on those values
.rowFormatter__value img {
vertical-align: middle;
max-height: lineHeightFromBaseline(2) !important;
max-width: ($euiSizeXXL * 12.5) !important;
}

View file

@ -1,313 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the "Elastic License
* 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
* Public License v 1"; you may not use this file except in compliance with, at
* your election, the "Elastic License 2.0", the "GNU Affero General Public
* License v3.0 only", or the "Server Side Public License, v 1".
*/
import ReactDOM from 'react-dom/server';
import { formatRow, formatTopLevelObject } from './row_formatter';
import { DataView } from '@kbn/data-views-plugin/public';
import { fieldFormatsMock } from '@kbn/field-formats-plugin/common/mocks';
import { DiscoverServices } from '../../../build_services';
import { stubbedSavedObjectIndexPattern } from '@kbn/data-plugin/common/stubs';
import { buildDataTableRecord } from '@kbn/discover-utils';
describe('Row formatter', () => {
let services: DiscoverServices;
const createIndexPattern = () => {
const id = 'my-index';
const {
type,
version,
attributes: { timeFieldName, fields, title },
} = stubbedSavedObjectIndexPattern(id);
return new DataView({
spec: { id, type, version, timeFieldName, fields: JSON.parse(fields), title },
fieldFormats: fieldFormatsMock,
shortDotsEnable: false,
metaFields: ['_id', '_type', '_score'],
});
};
const dataView = createIndexPattern();
const rawHit = {
_id: 'a',
_index: 'foo',
_score: 1,
_source: {
foo: 'bar',
number: 42,
hello: '<h1>World</h1>',
also: 'with "quotes" or \'single quotes\'',
},
};
const hit = buildDataTableRecord(rawHit, dataView);
const shouldShowField = (fieldName: string) =>
dataView.fields.getAll().some((fld) => fld.name === fieldName);
beforeEach(() => {
services = {
fieldFormats: {
getDefaultInstance: jest.fn(() => ({ convert: (value: unknown) => value })),
getFormatterForField: jest.fn(() => ({ convert: (value: unknown) => value })),
},
} as unknown as DiscoverServices;
});
it('formats document properly', () => {
expect(formatRow(hit, dataView, shouldShowField, 100, services.fieldFormats))
.toMatchInlineSnapshot(`
<TemplateComponent
defPairs={
Array [
Array [
"also",
"with \\"quotes\\" or 'single quotes'",
"also",
],
Array [
"foo",
"bar",
"foo",
],
Array [
"hello",
"<h1>World</h1>",
"hello",
],
Array [
"number",
42,
"number",
],
Array [
"_id",
"a",
"_id",
],
Array [
"_score",
1,
"_score",
],
]
}
/>
`);
});
it('limits number of rendered items', () => {
services = {
uiSettings: {
get: () => 1,
},
fieldFormats: {
getDefaultInstance: jest.fn(() => ({ convert: (value: unknown) => value })),
getFormatterForField: jest.fn(() => ({ convert: (value: unknown) => value })),
},
} as unknown as DiscoverServices;
expect(formatRow(hit, dataView, () => false, 1, services.fieldFormats)).toMatchInlineSnapshot(`
<TemplateComponent
defPairs={
Array [
Array [
"also",
"with \\"quotes\\" or 'single quotes'",
"also",
],
Array [
"and 4 more fields",
"",
null,
],
]
}
/>
`);
});
it('formats document with highlighted fields first', () => {
const highLightHit = buildDataTableRecord(
{ ...rawHit, highlight: { number: ['42'] } },
dataView
);
expect(formatRow(highLightHit, dataView, shouldShowField, 100, services.fieldFormats))
.toMatchInlineSnapshot(`
<TemplateComponent
defPairs={
Array [
Array [
"number",
42,
"number",
],
Array [
"also",
"with \\"quotes\\" or 'single quotes'",
"also",
],
Array [
"foo",
"bar",
"foo",
],
Array [
"hello",
"<h1>World</h1>",
"hello",
],
Array [
"_id",
"a",
"_id",
],
Array [
"_score",
1,
"_score",
],
]
}
/>
`);
});
it('formats top level objects using formatter', () => {
dataView.getFieldByName = jest.fn().mockReturnValue({
name: 'subfield',
});
dataView.getFormatterForField = jest.fn().mockReturnValue({
convert: () => 'formatted',
});
expect(
formatTopLevelObject(
{
fields: {
'object.value': [5, 10],
},
},
{
'object.value': [5, 10],
getByName: jest.fn(),
},
dataView,
100
)
).toMatchInlineSnapshot(`
<TemplateComponent
defPairs={
Array [
Array [
"object.value",
"formatted, formatted",
"object.value",
],
]
}
/>
`);
});
it('formats top level objects in alphabetical order', () => {
dataView.getFieldByName = jest.fn().mockReturnValue({
name: 'subfield',
});
dataView.getFormatterForField = jest.fn().mockReturnValue({
convert: () => 'formatted',
});
const formatted = ReactDOM.renderToStaticMarkup(
formatTopLevelObject(
{ fields: { 'a.zzz': [100], 'a.ccc': [50] } },
{ 'a.zzz': [100], 'a.ccc': [50], getByName: jest.fn() },
dataView,
100
)
);
expect(formatted.indexOf('<dt>a.ccc:</dt>')).toBeLessThan(formatted.indexOf('<dt>a.zzz:</dt>'));
});
it('formats top level objects with subfields and highlights', () => {
dataView.getFieldByName = jest.fn().mockReturnValue({
name: 'subfield',
});
dataView.getFormatterForField = jest.fn().mockReturnValue({
convert: () => 'formatted',
});
expect(
formatTopLevelObject(
{
fields: {
'object.value': [5, 10],
'object.keys': ['a', 'b'],
},
highlight: {
'object.keys': 'a',
},
},
{
'object.value': [5, 10],
'object.keys': ['a', 'b'],
getByName: jest.fn(),
},
dataView,
100
)
).toMatchInlineSnapshot(`
<TemplateComponent
defPairs={
Array [
Array [
"object.keys",
"formatted, formatted",
"object.keys",
],
Array [
"object.value",
"formatted, formatted",
"object.value",
],
]
}
/>
`);
});
it('formats top level objects, converting unknown fields to string', () => {
dataView.getFieldByName = jest.fn();
dataView.getFormatterForField = jest.fn();
expect(
formatTopLevelObject(
{
fields: {
'object.value': [5, 10],
},
},
{
'object.value': [5, 10],
getByName: jest.fn(),
},
dataView,
100
)
).toMatchInlineSnapshot(`
<TemplateComponent
defPairs={
Array [
Array [
"object.value",
"5, 10",
"object.value",
],
]
}
/>
`);
});
});

View file

@ -1,87 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the "Elastic License
* 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
* Public License v 1"; you may not use this file except in compliance with, at
* your election, the "Elastic License 2.0", the "GNU Affero General Public
* License v3.0 only", or the "Server Side Public License, v 1".
*/
import React, { Fragment } from 'react';
import type { DataView } from '@kbn/data-views-plugin/public';
import { FieldFormatsStart } from '@kbn/field-formats-plugin/public';
import type {
DataTableRecord,
ShouldShowFieldInTableHandler,
FormattedHit,
} from '@kbn/discover-utils/types';
import { formatHit } from '@kbn/discover-utils';
import './row_formatter.scss';
interface Props {
defPairs: FormattedHit;
}
const TemplateComponent = ({ defPairs }: Props) => {
return (
<dl className={'source dscTruncateByHeight'}>
{defPairs.map((pair, idx) => (
<Fragment key={idx}>
<dt>
{pair[0]}
{!!pair[1] && ':'}
</dt>
<dd
className="rowFormatter__value"
// eslint-disable-next-line react/no-danger
dangerouslySetInnerHTML={{ __html: pair[1] }}
/>{' '}
</Fragment>
))}
</dl>
);
};
export const formatRow = (
hit: DataTableRecord,
dataView: DataView,
shouldShowFieldHandler: ShouldShowFieldInTableHandler,
maxEntries: number,
fieldFormats: FieldFormatsStart
) => {
const pairs = formatHit(hit, dataView, shouldShowFieldHandler, maxEntries, fieldFormats);
return <TemplateComponent defPairs={pairs} />;
};
export const formatTopLevelObject = (
// eslint-disable-next-line @typescript-eslint/no-explicit-any
row: Record<string, any>,
// eslint-disable-next-line @typescript-eslint/no-explicit-any
fields: Record<string, any>,
dataView: DataView,
maxEntries: number
) => {
const highlights = row.highlight ?? {};
const highlightPairs: FormattedHit = [];
const sourcePairs: FormattedHit = [];
const sorted = Object.entries(fields).sort(([keyA], [keyB]) => keyA.localeCompare(keyB));
sorted.forEach(([key, values]) => {
const field = dataView.getFieldByName(key);
const displayKey = fields.getByName ? fields.getByName(key)?.displayName : undefined;
const formatter = field
? dataView.getFormatterForField(field)
: { convert: (v: unknown, ..._: unknown[]) => String(v) };
if (!values.map) return;
const formatted = values
.map((val: unknown) =>
formatter.convert(val, 'html', {
field,
hit: row,
})
)
.join(', ');
const pairs = highlights[key] ? highlightPairs : sourcePairs;
pairs.push([displayKey ? displayKey : key, formatted, key]);
});
return <TemplateComponent defPairs={[...highlightPairs, ...sourcePairs].slice(0, maxEntries)} />;
};

View file

@ -1,42 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the "Elastic License
* 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
* Public License v 1"; you may not use this file except in compliance with, at
* your election, the "Elastic License 2.0", the "GNU Affero General Public
* License v3.0 only", or the "Server Side Public License, v 1".
*/
import { shouldLoadNextDocPatch } from './should_load_next_doc_patch';
describe('shouldLoadNextDocPatch', () => {
test('next patch should not be loaded', () => {
const scrollingDomEl = {
scrollHeight: 500,
scrollTop: 100,
clientHeight: 100,
} as HTMLElement;
expect(shouldLoadNextDocPatch(scrollingDomEl)).toBeFalsy();
});
test('next patch should be loaded', () => {
const scrollingDomEl = {
scrollHeight: 500,
scrollTop: 350,
clientHeight: 100,
} as HTMLElement;
expect(shouldLoadNextDocPatch(scrollingDomEl)).toBeTruthy();
});
test("next patch should be loaded even there's a decimal scroll height", () => {
const scrollingDomEl = {
scrollHeight: 500,
scrollTop: 350.34234234,
clientHeight: 100,
} as HTMLElement;
expect(shouldLoadNextDocPatch(scrollingDomEl)).toBeTruthy();
});
});

View file

@ -1,27 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the "Elastic License
* 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
* Public License v 1"; you may not use this file except in compliance with, at
* your election, the "Elastic License 2.0", the "GNU Affero General Public
* License v3.0 only", or the "Server Side Public License, v 1".
*/
// use a buffer to start rendering more documents before the user completely scrolles down
const verticalScrollBuffer = 100;
/**
* Helper function to determine if the next patch of 50 documents should be loaded
*/
export function shouldLoadNextDocPatch(domEl: HTMLElement) {
// the height of the scrolling div, including content not visible on the screen due to overflow.
const scrollHeight = domEl.scrollHeight;
// the number of pixels that the div is is scrolled vertically
const scrollTop = domEl.scrollTop;
// the inner height of the scrolling div, excluding content that's visible on the screen
const clientHeight = domEl.clientHeight;
const consumedHeight = scrollTop + clientHeight;
const remainingHeight = scrollHeight - consumedHeight;
return remainingHeight < verticalScrollBuffer;
}

View file

@ -11,7 +11,7 @@ import React, { useMemo, useEffect, useState, type ReactElement, useCallback } f
import { EuiFlexGroup, EuiFlexItem, EuiTab, EuiTabs, useEuiTheme } from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n-react';
import { css } from '@emotion/react';
import { isLegacyTableEnabled, SHOW_FIELD_STATISTICS } from '@kbn/discover-utils';
import { SHOW_FIELD_STATISTICS } from '@kbn/discover-utils';
import type { DataView } from '@kbn/data-views-plugin/common';
import useMountedState from 'react-use/lib/useMountedState';
import { VIEW_MODE } from '../../../common/constants';
@ -42,10 +42,6 @@ export const DocumentViewModeToggle = ({
dataVisualizer: dataVisualizerService,
aiops: aiopsService,
} = useDiscoverServices();
const isLegacy = useMemo(
() => isLegacyTableEnabled({ uiSettings, isEsqlMode }),
[uiSettings, isEsqlMode]
);
const [showPatternAnalysisTab, setShowPatternAnalysisTab] = useState<boolean | null>(null);
const showFieldStatisticsTab = useMemo(
@ -93,7 +89,7 @@ export const DocumentViewModeToggle = ({
}
}, [showPatternAnalysisTab, viewMode, setDiscoverViewMode]);
const includesNormalTabsStyle = viewMode === VIEW_MODE.AGGREGATED_LEVEL || isLegacy;
const includesNormalTabsStyle = viewMode === VIEW_MODE.AGGREGATED_LEVEL;
const containerPadding = includesNormalTabsStyle ? euiTheme.size.s : 0;
const containerCss = css`

View file

@ -15,7 +15,6 @@ import {
DOC_HIDE_TIME_COLUMN_SETTING,
SEARCH_FIELDS_FROM_SOURCE,
SORT_DEFAULT_ORDER_SETTING,
isLegacyTableEnabled,
} from '@kbn/discover-utils';
import {
FetchContext,
@ -28,7 +27,6 @@ import { DataGridDensity, DataLoadingState, useColumns } from '@kbn/unified-data
import { DocViewFilterFn } from '@kbn/unified-doc-viewer/types';
import { DiscoverGridSettings } from '@kbn/saved-search-plugin/common';
import useObservable from 'react-use/lib/useObservable';
import { DiscoverDocTableEmbeddable } from '../../components/doc_table/create_doc_table_embeddable';
import { useDiscoverServices } from '../../hooks/use_discover_services';
import { getSortForEmbeddable } from '../../utils';
import { getAllowedSampleSize, getMaxAllowedSampleSize } from '../../utils/get_allowed_sample_size';
@ -53,7 +51,6 @@ interface SavedSearchEmbeddableComponentProps {
stateManager: SearchEmbeddableStateManager;
}
const DiscoverDocTableEmbeddableMemoized = React.memo(DiscoverDocTableEmbeddable);
const DiscoverGridEmbeddableMemoized = React.memo(DiscoverGridEmbeddable);
export function SearchEmbeddableGridComponent({
@ -105,14 +102,6 @@ export function SearchEmbeddableGridComponent({
);
const isEsql = useMemo(() => isEsqlMode(savedSearch), [savedSearch]);
const useLegacyTable = useMemo(
() =>
isLegacyTableEnabled({
uiSettings: discoverServices.uiSettings,
isEsqlMode: isEsql,
}),
[discoverServices, isEsql]
);
const sort = useMemo(() => {
return getSortForEmbeddable(savedSearch.sort, dataView, discoverServices.uiSettings, isEsql);
@ -233,19 +222,6 @@ export function SearchEmbeddableGridComponent({
useNewFieldsApi,
};
if (useLegacyTable) {
return (
<DiscoverDocTableEmbeddableMemoized
{...sharedProps}
{...onStateEditedProps}
filters={savedSearchFilters}
isEsqlMode={isEsql}
isLoading={Boolean(loading)}
sharedItemTitle={panelTitle || savedSearchTitle}
/>
);
}
return (
<DiscoverGridEmbeddableMemoized
{...sharedProps}

View file

@ -21,14 +21,13 @@ import {
import { DEFAULT_APP_CATEGORIES } from '@kbn/core/public';
import { ENABLE_ESQL } from '@kbn/esql-utils';
import { setStateToKbnUrl } from '@kbn/kibana-utils-plugin/public';
import { SEARCH_EMBEDDABLE_TYPE, TRUNCATE_MAX_HEIGHT } from '@kbn/discover-utils';
import { SEARCH_EMBEDDABLE_TYPE } from '@kbn/discover-utils';
import { SavedSearchAttributes, SavedSearchType } from '@kbn/saved-search-plugin/common';
import { i18n } from '@kbn/i18n';
import { PLUGIN_ID } from '../common';
import { registerFeature } from './register_feature';
import { buildServices, UrlTracker } from './build_services';
import { ViewSavedSearchAction } from './embeddable/actions/view_saved_search_action';
import { injectTruncateStyles } from './utils/truncate_styles';
import { initializeKbnUrlTracking } from './utils/initialize_kbn_url_tracking';
import {
DiscoverContextAppLocator,
@ -285,7 +284,6 @@ export class DiscoverPlugin
plugins.uiActions.addTriggerAction('CONTEXT_MENU_TRIGGER', viewSavedSearchAction);
plugins.uiActions.registerTrigger(SEARCH_EMBEDDABLE_CELL_ACTIONS_TRIGGER);
plugins.uiActions.registerTrigger(DISCOVER_CELL_ACTIONS_TRIGGER);
injectTruncateStyles(core.uiSettings.get(TRUNCATE_MAX_HEIGHT));
const isEsqlEnabled = core.uiSettings.get(ENABLE_ESQL);

View file

@ -1,48 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the "Elastic License
* 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
* Public License v 1"; you may not use this file except in compliance with, at
* your election, the "Elastic License 2.0", the "GNU Affero General Public
* License v3.0 only", or the "Server Side Public License, v 1".
*/
import createCache from '@emotion/cache';
import { cache } from '@emotion/css';
import { serializeStyles } from '@emotion/serialize';
/**
* The following emotion cache management was introduced here
* https://ntsim.uk/posts/how-to-update-or-remove-global-styles-in-emotion/
*/
const TRUNCATE_GRADIENT_HEIGHT = 15;
const globalThemeCache = createCache({ key: 'truncation' });
const buildStylesheet = (maxHeight: number) => {
if (!maxHeight) {
return [
`
.dscTruncateByHeight {
max-height: none;
}`,
];
}
return [
`
.dscTruncateByHeight {
overflow: hidden;
max-height: ${maxHeight}px !important;
}
.dscTruncateByHeight:before {
top: ${maxHeight - TRUNCATE_GRADIENT_HEIGHT}px;
}
`,
];
};
export const injectTruncateStyles = (maxHeight: number) => {
const serialized = serializeStyles(buildStylesheet(maxHeight), cache.registered);
if (!globalThemeCache.inserted[serialized.name]) {
globalThemeCache.insert('', serialized, globalThemeCache.sheet, true);
}
};

View file

@ -23,13 +23,10 @@ import {
CONTEXT_DEFAULT_SIZE_SETTING,
CONTEXT_STEP_SETTING,
CONTEXT_TIE_BREAKER_FIELDS_SETTING,
DOC_TABLE_LEGACY,
MODIFY_COLUMNS_ON_SWITCH,
SEARCH_FIELDS_FROM_SOURCE,
MAX_DOC_FIELDS_DISPLAYED,
SHOW_MULTIFIELDS,
TRUNCATE_MAX_HEIGHT,
TRUNCATE_MAX_HEIGHT_DEFAULT_VALUE,
SHOW_FIELD_STATISTICS,
ROW_HEIGHT_OPTION,
} from '@kbn/discover-utils';
@ -183,42 +180,6 @@ export const getUiSettings: (
category: ['discover'],
schema: schema.arrayOf(schema.string()),
},
[DOC_TABLE_LEGACY]: {
name: i18n.translate('discover.advancedSettings.disableDocumentExplorer', {
defaultMessage: 'Document Explorer or classic view',
}),
value: false,
description: i18n.translate('discover.advancedSettings.disableDocumentExplorerDescription', {
defaultMessage:
'To use the new {documentExplorerDocs} instead of the classic view, turn off this option. ' +
'The Document Explorer offers better data sorting, resizable columns, and a full screen view.',
values: {
documentExplorerDocs:
`<a href=${docLinks.links.discover.documentExplorer} style="font-weight: 600;"
target="_blank" rel="noopener">` +
i18n.translate('discover.advancedSettings.documentExplorerLinkText', {
defaultMessage: 'Document Explorer',
}) +
'</a>',
},
}),
requiresPageReload: true,
category: ['discover'],
schema: schema.boolean(),
metric: {
type: METRIC_TYPE.CLICK,
name: 'discover:useLegacyDataGrid',
},
deprecation: {
message: i18n.translate(
'discover.advancedSettings.discover.disableDocumentExplorerDeprecation',
{
defaultMessage: 'This setting is deprecated and will be removed in Kibana 9.0.',
}
),
docLinksKey: 'discoverSettings',
},
},
[MODIFY_COLUMNS_ON_SWITCH]: {
name: i18n.translate('discover.advancedSettings.discover.modifyColumnsOnSwitchTitle', {
defaultMessage: 'Modify columns when changing data views',
@ -316,23 +277,4 @@ export const getUiSettings: (
}),
schema: schema.number({ min: -1 }),
},
[TRUNCATE_MAX_HEIGHT]: {
name: i18n.translate('discover.advancedSettings.params.maxCellHeightTitle', {
defaultMessage: 'Maximum cell height in the classic table',
}),
value: TRUNCATE_MAX_HEIGHT_DEFAULT_VALUE,
category: ['discover'],
description: i18n.translate('discover.advancedSettings.params.maxCellHeightText', {
defaultMessage:
'The maximum height that a cell in a table should occupy. Set to 0 to disable truncation.',
}),
schema: schema.number({ min: 0 }),
requiresPageReload: true,
deprecation: {
message: i18n.translate('discover.advancedSettings.discover.maxCellHeightDeprecation', {
defaultMessage: 'This setting is deprecated and will be removed in Kibana 9.0.',
}),
docLinksKey: 'discoverSettings',
},
},
});

View file

@ -244,10 +244,6 @@ export const stackManagementSchema: MakeSchemaFrom<UsageStats> = {
type: 'keyword',
_meta: { description: 'Non-default value of setting.' },
},
'truncate:maxHeight': {
type: 'long',
_meta: { description: 'Non-default value of setting.' },
},
'timepicker:timeDefaults': {
type: 'keyword',
_meta: { description: 'Non-default value of setting.' },
@ -424,10 +420,6 @@ export const stackManagementSchema: MakeSchemaFrom<UsageStats> = {
type: 'boolean',
_meta: { description: 'Non-default value of setting.' },
},
'doc_table:legacy': {
type: 'boolean',
_meta: { description: 'Non-default value of setting.' },
},
'discover:modifyColumnsOnSwitch': {
type: 'boolean',
_meta: { description: 'Non-default value of setting.' },

View file

@ -30,7 +30,6 @@ export interface UsageStats {
'autocomplete:valueSuggestionMethod': string;
'search:timeout': number;
'visualization:visualize:legacyHeatmapChartsLibrary': boolean;
'doc_table:legacy': boolean;
'discover:modifyColumnsOnSwitch': boolean;
'discover:searchFieldsFromSource': boolean;
'discover:showFieldStatistics': boolean;
@ -101,7 +100,6 @@ export interface UsageStats {
'fileUpload:maxFileSize': string;
'ml:anomalyDetection:results:enableTimeDefaults': boolean;
'ml:anomalyDetection:results:timeDefaults': string;
'truncate:maxHeight': number;
'timepicker:timeDefaults': string;
'timepicker:refreshIntervalDefaults': string;
'timepicker:quickRanges': string;

View file

@ -10492,12 +10492,6 @@
"description": "Non-default value of setting."
}
},
"truncate:maxHeight": {
"type": "long",
"_meta": {
"description": "Non-default value of setting."
}
},
"timepicker:timeDefaults": {
"type": "keyword",
"_meta": {
@ -10765,12 +10759,6 @@
"description": "Non-default value of setting."
}
},
"doc_table:legacy": {
"type": "boolean",
"_meta": {
"description": "Non-default value of setting."
}
},
"discover:modifyColumnsOnSwitch": {
"type": "boolean",
"_meta": {

View file

@ -32,31 +32,17 @@ describe('getHeight', () => {
test('when using document explorer, returning the available height in the flyout', () => {
const monacoMock = getMonacoMock(500, 0);
const height = getHeight(monacoMock, true, DEFAULT_MARGIN_BOTTOM);
const height = getHeight(monacoMock, DEFAULT_MARGIN_BOTTOM);
expect(height).toBe(484);
const heightCustom = getHeight(monacoMock, true, 80);
const heightCustom = getHeight(monacoMock, 80);
expect(heightCustom).toBe(420);
});
test('when using document explorer, returning the available height in the flyout has a minimun guarenteed height', () => {
const monacoMock = getMonacoMock(500);
const height = getHeight(monacoMock, true, DEFAULT_MARGIN_BOTTOM);
const height = getHeight(monacoMock, DEFAULT_MARGIN_BOTTOM);
expect(height).toBe(400);
});
test('when using classic table, its displayed inline without scrolling', () => {
const monacoMock = getMonacoMock(100);
const height = getHeight(monacoMock, false, DEFAULT_MARGIN_BOTTOM);
expect(height).toBe(1020);
});
test('when using classic table, limited height > 500 lines to allow scrolling', () => {
const monacoMock = getMonacoMock(1000);
const height = getHeight(monacoMock, false, DEFAULT_MARGIN_BOTTOM);
expect(height).toBe(5020);
});
});

View file

@ -8,7 +8,7 @@
*/
import { monaco } from '@kbn/monaco';
import { MAX_LINES_CLASSIC_TABLE, MIN_HEIGHT } from './source';
import { MIN_HEIGHT } from './source';
// Displayed margin of the tab content to the window bottom
export const DEFAULT_MARGIN_BOTTOM = 16;
@ -28,7 +28,6 @@ export function getTabContentAvailableHeight(
export function getHeight(
editor: monaco.editor.IStandaloneCodeEditor,
useDocExplorer: boolean,
decreaseAvailableHeightBy: number
) {
const editorElement = editor?.getDomNode();
@ -36,17 +35,6 @@ export function getHeight(
return 0;
}
let result;
if (useDocExplorer) {
result = getTabContentAvailableHeight(editorElement, decreaseAvailableHeightBy);
} else {
// takes care of the classic table, display a maximum of 500 lines
// why not display it all? Due to performance issues when the browser needs to render it all
const lineHeight = editor.getOption(monaco.editor.EditorOption.lineHeight);
const lineCount = editor.getModel()?.getLineCount() || 1;
const displayedLineCount =
lineCount > MAX_LINES_CLASSIC_TABLE ? MAX_LINES_CLASSIC_TABLE : lineCount;
result = editor.getTopForLineNumber(displayedLineCount + 1) + lineHeight;
}
const result = getTabContentAvailableHeight(editorElement, decreaseAvailableHeightBy);
return Math.max(result, MIN_HEIGHT);
}

View file

@ -17,7 +17,7 @@ import { i18n } from '@kbn/i18n';
import type { DataView } from '@kbn/data-views-plugin/public';
import type { DataTableRecord } from '@kbn/discover-utils/types';
import { ElasticRequestState } from '@kbn/unified-doc-viewer';
import { isLegacyTableEnabled, SEARCH_FIELDS_FROM_SOURCE } from '@kbn/discover-utils';
import { SEARCH_FIELDS_FROM_SOURCE } from '@kbn/discover-utils';
import { omit } from 'lodash';
import { getUnifiedDocViewerServices } from '../../plugin';
import { useEsDocSearch } from '../../hooks';
@ -36,9 +36,6 @@ interface SourceViewerProps {
onRefresh: () => void;
}
// Ihe number of lines displayed without scrolling used for classic table, which renders the component
// inline limitation was necessary to enable virtualized scrolling, which improves performance
export const MAX_LINES_CLASSIC_TABLE = 500;
// Minimum height for the source content to guarantee minimum space when the flyout is scrollable.
export const MIN_HEIGHT = 400;
@ -57,10 +54,6 @@ export const DocViewerSource = ({
const [jsonValue, setJsonValue] = useState<string>('');
const { uiSettings } = getUnifiedDocViewerServices();
const useNewFieldsApi = !uiSettings.get(SEARCH_FIELDS_FROM_SOURCE);
const useDocExplorer = !isLegacyTableEnabled({
uiSettings,
isEsqlMode: Array.isArray(textBasedHits),
});
const [requestState, hit] = useEsDocSearch({
id,
index,
@ -75,9 +68,7 @@ export const DocViewerSource = ({
}
}, [requestState, hit]);
// setting editor height
// - classic view: based on lines height and count to stretch and fit its content
// - explorer: to fill the available space of the document flyout
// setting editor height to fill the available space of the document flyout
useEffect(() => {
if (!editor) {
return;
@ -88,11 +79,7 @@ export const DocViewerSource = ({
return;
}
const height = getHeight(
editor,
useDocExplorer,
decreaseAvailableHeightBy ?? DEFAULT_MARGIN_BOTTOM
);
const height = getHeight(editor, decreaseAvailableHeightBy ?? DEFAULT_MARGIN_BOTTOM);
if (height === 0) {
return;
}
@ -102,7 +89,7 @@ export const DocViewerSource = ({
} else {
setEditorHeight(height);
}
}, [editor, jsonValue, useDocExplorer, setEditorHeight, decreaseAvailableHeightBy]);
}, [editor, jsonValue, setEditorHeight, decreaseAvailableHeightBy]);
const loadingState = (
<div className="sourceViewer__loading">

View file

@ -1,14 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the "Elastic License
* 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
* Public License v 1"; you may not use this file except in compliance with, at
* your election, the "Elastic License 2.0", the "GNU Affero General Public
* License v3.0 only", or the "Server Side Public License, v 1".
*/
import { DocViewerLegacyTable } from './table';
// Required for usage in React.lazy
// eslint-disable-next-line import/no-default-export
export default DocViewerLegacyTable;

View file

@ -1,452 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the "Elastic License
* 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
* Public License v 1"; you may not use this file except in compliance with, at
* your election, the "Elastic License 2.0", the "GNU Affero General Public
* License v3.0 only", or the "Server Side Public License, v 1".
*/
import React from 'react';
import { mountWithIntl } from '@kbn/test-jest-helpers';
import { findTestSubject } from '@elastic/eui/lib/test';
import { DocViewerLegacyTable } from './table';
import type { DataView } from '@kbn/data-views-plugin/public';
import type { DocViewRenderProps } from '@kbn/unified-doc-viewer/types';
import { buildDataTableRecord } from '@kbn/discover-utils';
import { setUnifiedDocViewerServices } from '../../../plugin';
import type { UnifiedDocViewerServices } from '../../../types';
const services = {
uiSettings: {
get: (key: string) => {
if (key === 'discover:showMultiFields') {
return true;
}
},
},
fieldFormats: {
getDefaultInstance: jest.fn(() => ({ convert: (value: unknown) => value })),
getFormatterForField: jest.fn(() => ({ convert: (value: unknown) => value })),
},
};
const dataView = {
fields: {
getAll: () => [
{
name: '_index',
type: 'string',
scripted: false,
filterable: true,
},
{
name: 'message',
type: 'string',
scripted: false,
filterable: false,
},
{
name: 'extension',
type: 'string',
scripted: false,
filterable: true,
},
{
name: 'bytes',
type: 'number',
scripted: false,
filterable: true,
},
{
name: 'scripted',
type: 'number',
scripted: true,
filterable: false,
},
],
},
metaFields: ['_index', '_score'],
getFormatterForField: jest.fn(() => ({ convert: (value: unknown) => value })),
} as unknown as DataView;
dataView.fields.getByName = (name: string) => {
return dataView.fields.getAll().find((field) => field.name === name);
};
const mountComponent = (
props: DocViewRenderProps,
overrides?: Partial<UnifiedDocViewerServices>
) => {
setUnifiedDocViewerServices({ ...services, ...overrides } as UnifiedDocViewerServices);
return mountWithIntl(<DocViewerLegacyTable {...props} />);
};
describe('DocViewTable at Discover', () => {
// At Discover's main view, all buttons are rendered
// check for existence of action buttons and warnings
const hit = buildDataTableRecord(
{
_index: 'logstash-2014.09.09',
_id: 'id123',
_score: 1,
_source: {
message:
'Lorem ipsum dolor sit amet, consectetuer adipiscing elit. \
Aenean commodo ligula eget dolor. Aenean massa. Cum sociis natoque penatibus \
et magnis dis parturient montes, nascetur ridiculus mus. Donec quam felis, \
ultricies nec, pellentesque eu, pretium quis, sem. Nulla consequat massa quis enim. \
Donec pede justo, fringilla vel, aliquet nec, vulputate eget, arcu. In enim justo, \
rhoncus ut, imperdiet a, venenatis vitae, justo. Nullam dictum felis eu pede mollis pretium. \
Integer tincidunt. Cras dapibus. Vivamus elementum semper nisi. Aenean vulputate eleifend tellus. \
Phasellus ullamcorper ipsum rutrum nunc. Nunc nonummy metus. Vestibulum volutpat pretium libero. Cras id dui. Aenean ut',
extension: 'html',
not_mapped: 'yes',
bytes: 100,
objectArray: [{ foo: true }],
relatedContent: {
test: 1,
},
scripted: 123,
_underscore: 123,
},
},
dataView
);
const props = {
hit,
columns: ['extension'],
dataView,
filter: jest.fn(),
onAddColumn: jest.fn(),
onRemoveColumn: jest.fn(),
};
const component = mountComponent(props);
[
{
_property: '_index',
addInclusiveFilterButton: true,
noMappingWarning: false,
toggleColumnButton: true,
underscoreWarning: false,
},
{
_property: 'message',
addInclusiveFilterButton: false,
noMappingWarning: false,
toggleColumnButton: true,
underscoreWarning: false,
},
{
_property: '_underscore',
addInclusiveFilterButton: false,
noMappingWarning: false,
toggleColumnButton: true,
underScoreWarning: true,
},
{
_property: 'scripted',
addInclusiveFilterButton: false,
noMappingWarning: false,
toggleColumnButton: true,
underScoreWarning: false,
},
{
_property: 'not_mapped',
addInclusiveFilterButton: false,
noMappingWarning: true,
toggleColumnButton: true,
underScoreWarning: false,
},
].forEach((check) => {
const rowComponent = findTestSubject(component, `tableDocViewRow-${check._property}`);
it(`renders row for ${check._property}`, () => {
expect(rowComponent.length).toBe(1);
});
(['addInclusiveFilterButton', 'toggleColumnButton', 'underscoreWarning'] as const).forEach(
(element) => {
const elementExist = check[element];
if (typeof elementExist === 'boolean') {
const btn = findTestSubject(rowComponent, element, '^=');
it(`renders ${element} for '${check._property}' correctly`, () => {
const disabled = btn.length ? btn.props().disabled : true;
const clickAble = btn.length && !disabled ? true : false;
expect(clickAble).toBe(elementExist);
});
}
}
);
});
});
describe('DocViewTable at Discover Context', () => {
// here no toggleColumnButtons are rendered
const hit = buildDataTableRecord(
{
_index: 'logstash-2014.09.09',
_id: 'id123',
_score: 1,
_source: {
message:
'Lorem ipsum dolor sit amet, consectetuer adipiscing elit. \
Aenean commodo ligula eget dolor. Aenean massa. Cum sociis natoque penatibus \
et magnis dis parturient montes, nascetur ridiculus mus. Donec quam felis, \
ultricies nec, pellentesque eu, pretium quis, sem. Nulla consequat massa quis enim. \
Donec pede justo, fringilla vel, aliquet nec, vulputate eget, arcu. In enim justo, \
rhoncus ut, imperdiet a, venenatis vitae, justo. Nullam dictum felis eu pede mollis pretium. \
Integer tincidunt. Cras dapibus. Vivamus elementum semper nisi. Aenean vulputate eleifend tellus. \
Phasellus ullamcorper ipsum rutrum nunc. Nunc nonummy metus. Vestibulum volutpat pretium libero. Cras id dui. Aenean ut',
},
},
dataView
);
const props = {
hit,
columns: ['extension'],
dataView,
filter: jest.fn(),
};
const component = mountComponent(props);
it(`renders no toggleColumnButton`, () => {
const foundLength = findTestSubject(component, 'toggleColumnButtons').length;
expect(foundLength).toBe(0);
});
it(`renders addInclusiveFilterButton`, () => {
const row = findTestSubject(component, `tableDocViewRow-_index`);
const btn = findTestSubject(row, 'addInclusiveFilterButton');
expect(btn.length).toBe(1);
btn.simulate('click');
expect(props.filter).toBeCalled();
});
});
describe('DocViewTable at Discover Doc', () => {
const hit = buildDataTableRecord(
{
_index: 'logstash-2014.09.09',
_score: 1,
_id: 'id123',
_source: {
extension: 'html',
not_mapped: 'yes',
},
},
dataView
);
// here no action buttons are rendered
const props = {
hit,
dataView,
hideActionsColumn: true,
};
const component = mountComponent(props);
const foundLength = findTestSubject(component, 'addInclusiveFilterButton').length;
it(`renders no action buttons`, () => {
expect(foundLength).toBe(0);
});
});
describe('DocViewTable at Discover Doc with Fields API', () => {
const dataViewCommerce = {
fields: {
getAll: () => [
{
name: '_index',
type: 'string',
scripted: false,
filterable: true,
},
{
name: 'category',
type: 'string',
scripted: false,
filterable: true,
},
{
name: 'category.keyword',
displayName: 'category.keyword',
type: 'string',
scripted: false,
filterable: true,
spec: {
subType: {
multi: {
parent: 'category',
},
},
},
},
{
name: 'customer_first_name',
type: 'string',
scripted: false,
filterable: true,
},
{
name: 'customer_first_name.keyword',
displayName: 'customer_first_name.keyword',
type: 'string',
scripted: false,
filterable: false,
spec: {
subType: {
multi: {
parent: 'customer_first_name',
},
},
},
},
{
name: 'customer_first_name.nickname',
displayName: 'customer_first_name.nickname',
type: 'string',
scripted: false,
filterable: false,
spec: {
subType: {
multi: {
parent: 'customer_first_name',
},
},
},
},
{
name: 'city',
displayName: 'city',
type: 'keyword',
isMapped: true,
readFromDocValues: true,
searchable: true,
shortDotsEnable: false,
scripted: false,
filterable: false,
},
{
name: 'city.raw',
displayName: 'city.raw',
type: 'string',
isMapped: true,
spec: {
subType: {
multi: {
parent: 'city',
},
},
},
shortDotsEnable: false,
scripted: false,
filterable: false,
},
],
},
metaFields: ['_index', '_type', '_score', '_id'],
getFormatterForField: jest.fn(() => ({ convert: (value: unknown) => value })),
} as unknown as DataView;
dataViewCommerce.fields.getByName = (name: string) => {
return dataViewCommerce.fields.getAll().find((field) => field.name === name);
};
const fieldsHit = buildDataTableRecord(
{
_index: 'logstash-2014.09.09',
_id: 'id123',
_score: 1.0,
fields: {
category: "Women's Clothing",
'category.keyword': "Women's Clothing",
customer_first_name: 'Betty',
'customer_first_name.keyword': 'Betty',
'customer_first_name.nickname': 'Betsy',
'city.raw': 'Los Angeles',
},
},
dataView
);
const props = {
hit: fieldsHit,
columns: ['Document'],
dataView: dataViewCommerce,
filter: jest.fn(),
onAddColumn: jest.fn(),
onRemoveColumn: jest.fn(),
};
it('renders multifield rows if showMultiFields flag is set', () => {
const component = mountComponent(props);
const categoryKeywordRow = findTestSubject(component, 'tableDocViewRow-category.keyword');
expect(categoryKeywordRow.length).toBe(1);
expect(findTestSubject(component, 'tableDocViewRow-customer_first_name.keyword').length).toBe(
1
);
expect(findTestSubject(component, 'tableDocViewRow-customer_first_name.nickname').length).toBe(
1
);
expect(
findTestSubject(component, 'tableDocViewRow-category.keyword-multifieldBadge').length
).toBe(1);
expect(
findTestSubject(component, 'tableDocViewRow-customer_first_name.keyword-multifieldBadge')
.length
).toBe(1);
expect(
findTestSubject(component, 'tableDocViewRow-customer_first_name.nickname-multifieldBadge')
.length
).toBe(1);
expect(findTestSubject(component, 'tableDocViewRow-city.raw').length).toBe(1);
});
it('does not render multifield rows if showMultiFields flag is not set', () => {
const overridedServices = {
uiSettings: {
get: (key: string) => {
if (key === 'discover:showMultiFields') {
return false;
}
},
},
} as unknown as UnifiedDocViewerServices;
const component = mountComponent(props, overridedServices);
const categoryKeywordRow = findTestSubject(component, 'tableDocViewRow-category.keyword');
expect(categoryKeywordRow.length).toBe(0);
expect(findTestSubject(component, 'tableDocViewRow-customer_first_name.keyword').length).toBe(
0
);
expect(findTestSubject(component, 'tableDocViewRow-customer_first_name.nickname').length).toBe(
0
);
expect(
findTestSubject(component, 'tableDocViewRow-customer_first_name.keyword-multifieldBadge')
.length
).toBe(0);
expect(findTestSubject(component, 'tableDocViewRow-customer_first_name').length).toBe(1);
expect(
findTestSubject(component, 'tableDocViewRow-customer_first_name.nickname-multifieldBadge')
.length
).toBe(0);
expect(findTestSubject(component, 'tableDocViewRow-city').length).toBe(0);
expect(findTestSubject(component, 'tableDocViewRow-city.raw').length).toBe(1);
});
});

View file

@ -1,123 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the "Elastic License
* 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
* Public License v 1"; you may not use this file except in compliance with, at
* your election, the "Elastic License 2.0", the "GNU Affero General Public
* License v3.0 only", or the "Server Side Public License, v 1".
*/
import '../table.scss';
import React, { useCallback, useMemo } from 'react';
import { EuiInMemoryTable } from '@elastic/eui';
import { getFieldIconType } from '@kbn/field-utils/src/utils/get_field_icon_type';
import {
SHOW_MULTIFIELDS,
formatFieldValue,
getIgnoredReason,
getShouldShowFieldHandler,
isNestedFieldParent,
} from '@kbn/discover-utils';
import type { DocViewRenderProps, FieldRecordLegacy } from '@kbn/unified-doc-viewer/types';
import { getUnifiedDocViewerServices } from '../../../plugin';
import { ACTIONS_COLUMN, MAIN_COLUMNS } from './table_columns';
export const DocViewerLegacyTable = ({
columns,
hit,
dataView,
hideActionsColumn,
filter,
onAddColumn,
onRemoveColumn,
}: DocViewRenderProps) => {
const { fieldFormats, uiSettings } = getUnifiedDocViewerServices();
const showMultiFields = useMemo(() => uiSettings.get(SHOW_MULTIFIELDS), [uiSettings]);
const mapping = useCallback((name: string) => dataView.fields.getByName(name), [dataView.fields]);
const tableColumns = useMemo(() => {
return !hideActionsColumn ? [ACTIONS_COLUMN, ...MAIN_COLUMNS] : MAIN_COLUMNS;
}, [hideActionsColumn]);
const onToggleColumn = useMemo(() => {
if (!onRemoveColumn || !onAddColumn || !columns) {
return undefined;
}
return (field: string) => {
if (columns.includes(field)) {
onRemoveColumn(field);
} else {
onAddColumn(field);
}
};
}, [onRemoveColumn, onAddColumn, columns]);
const onSetRowProps = useCallback(({ field: { field } }: FieldRecordLegacy) => {
return {
key: field,
className: 'kbnDocViewer__tableRow',
'data-test-subj': `tableDocViewRow-${field}`,
};
}, []);
const shouldShowFieldHandler = useMemo(
() => getShouldShowFieldHandler(Object.keys(hit.flattened), dataView, showMultiFields),
[hit.flattened, dataView, showMultiFields]
);
const items: FieldRecordLegacy[] = Object.keys(hit.flattened)
.filter(shouldShowFieldHandler)
.sort((fieldA, fieldB) => {
const mappingA = mapping(fieldA);
const mappingB = mapping(fieldB);
const nameA = !mappingA || !mappingA.displayName ? fieldA : mappingA.displayName;
const nameB = !mappingB || !mappingB.displayName ? fieldB : mappingB.displayName;
return nameA.localeCompare(nameB);
})
.map((field) => {
const fieldMapping = mapping(field);
const displayName = fieldMapping?.displayName ?? field;
const fieldType = isNestedFieldParent(field, dataView)
? 'nested'
: fieldMapping
? getFieldIconType(fieldMapping)
: undefined;
const ignored = getIgnoredReason(fieldMapping ?? field, hit.raw._ignored);
return {
action: {
onToggleColumn,
onFilter: filter,
isActive: !!columns?.includes(field),
flattenedField: hit.flattened[field],
},
field: {
field,
displayName,
fieldMapping,
fieldType,
scripted: Boolean(fieldMapping?.scripted),
},
value: {
formattedValue: formatFieldValue(
hit.flattened[field],
hit.raw,
fieldFormats,
dataView,
fieldMapping
),
ignored,
},
};
});
return (
<EuiInMemoryTable
tableLayout="auto"
className="kbnDocViewer__table"
items={items}
columns={tableColumns}
rowProps={onSetRowProps}
responsiveBreakpoint={false}
/>
);
};

View file

@ -1,67 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the "Elastic License
* 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
* Public License v 1"; you may not use this file except in compliance with, at
* your election, the "Elastic License 2.0", the "GNU Affero General Public
* License v3.0 only", or the "Server Side Public License, v 1".
*/
import React from 'react';
import type { DataViewField } from '@kbn/data-views-plugin/public';
import type { DocViewFilterFn } from '@kbn/unified-doc-viewer/types';
import { DocViewTableRowBtnFilterRemove } from './table_row_btn_filter_remove';
import { DocViewTableRowBtnFilterExists } from './table_row_btn_filter_exists';
import { DocViewTableRowBtnToggleColumn } from './table_row_btn_toggle_column';
import { DocViewTableRowBtnFilterAdd } from './table_row_btn_filter_add';
interface TableActionsProps {
field: string;
isActive: boolean;
flattenedField: unknown;
fieldMapping?: DataViewField;
onFilter: DocViewFilterFn;
onToggleColumn: ((field: string) => void) | undefined;
ignoredValue: boolean;
}
export const TableActions = ({
isActive,
field,
fieldMapping,
flattenedField,
onToggleColumn,
onFilter,
ignoredValue,
}: TableActionsProps) => {
return (
<div className="kbnDocViewer__buttons">
{onFilter && (
<DocViewTableRowBtnFilterAdd
disabled={!fieldMapping || !fieldMapping.filterable || ignoredValue}
onClick={() => onFilter(fieldMapping, flattenedField, '+')}
/>
)}
{onFilter && (
<DocViewTableRowBtnFilterRemove
disabled={!fieldMapping || !fieldMapping.filterable || ignoredValue}
onClick={() => onFilter(fieldMapping, flattenedField, '-')}
/>
)}
{onToggleColumn && (
<DocViewTableRowBtnToggleColumn
active={isActive}
fieldname={field}
onClick={() => onToggleColumn(field)}
/>
)}
{onFilter && (
<DocViewTableRowBtnFilterExists
disabled={!fieldMapping || !fieldMapping.filterable}
onClick={() => onFilter('_exists_', field, '+')}
scripted={fieldMapping && fieldMapping.scripted}
/>
)}
</div>
);
};

View file

@ -1,115 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the "Elastic License
* 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
* Public License v 1"; you may not use this file except in compliance with, at
* your election, the "Elastic License 2.0", the "GNU Affero General Public
* License v3.0 only", or the "Server Side Public License, v 1".
*/
import { EuiBasicTableColumn, EuiText } from '@elastic/eui';
import React from 'react';
import { FormattedMessage } from '@kbn/i18n-react';
import type { FieldRecordLegacy } from '@kbn/unified-doc-viewer/types';
import { FieldName } from '@kbn/unified-doc-viewer';
import { TableActions } from './table_cell_actions';
import { TableFieldValue } from '../table_cell_value';
export const ACTIONS_COLUMN: EuiBasicTableColumn<FieldRecordLegacy> = {
field: 'action',
className: 'kbnDocViewer__tableActionsCell',
width: '108px',
mobileOptions: { header: false },
name: (
<EuiText size="xs">
<strong>
<FormattedMessage
id="unifiedDocViewer.fieldChooser.discoverField.actions"
defaultMessage="Actions"
/>
</strong>
</EuiText>
),
render: (
{ flattenedField, isActive, onFilter, onToggleColumn }: FieldRecordLegacy['action'],
{ field: { field, fieldMapping }, value: { ignored } }: FieldRecordLegacy
) => {
return (
<TableActions
isActive={isActive}
field={field}
fieldMapping={fieldMapping}
flattenedField={flattenedField}
onFilter={onFilter!}
onToggleColumn={onToggleColumn}
ignoredValue={!!ignored}
/>
);
},
};
export const MAIN_COLUMNS: Array<EuiBasicTableColumn<FieldRecordLegacy>> = [
{
field: 'field',
className: 'kbnDocViewer__tableFieldNameCell',
mobileOptions: { header: false },
width: '30%',
name: (
<EuiText size="xs">
<strong>
<FormattedMessage
id="unifiedDocViewer.fieldChooser.discoverField.name"
defaultMessage="Field"
/>
</strong>
</EuiText>
),
render: ({
field,
fieldType,
displayName,
fieldMapping,
scripted,
}: FieldRecordLegacy['field']) => {
return field ? (
<FieldName
fieldName={displayName}
fieldType={fieldType}
fieldMapping={fieldMapping}
scripted={scripted}
/>
) : (
<span>&nbsp;</span>
);
},
},
{
field: 'value',
className: 'kbnDocViewer__tableValueCell',
mobileOptions: { header: false },
name: (
<EuiText size="xs">
<strong>
<FormattedMessage
id="unifiedDocViewer.fieldChooser.discoverField.value"
defaultMessage="Value"
/>
</strong>
</EuiText>
),
render: (
{ formattedValue, ignored }: FieldRecordLegacy['value'],
{ field: { field }, action: { flattenedField } }: FieldRecordLegacy
) => {
return (
<TableFieldValue
field={field}
formattedValue={formattedValue}
rawValue={flattenedField}
ignoreReason={ignored}
isDetails={false}
isLegacy={true}
/>
);
},
},
];

View file

@ -1,51 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the "Elastic License
* 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
* Public License v 1"; you may not use this file except in compliance with, at
* your election, the "Elastic License 2.0", the "GNU Affero General Public
* License v3.0 only", or the "Server Side Public License, v 1".
*/
import React from 'react';
import { FormattedMessage } from '@kbn/i18n-react';
import { EuiToolTip, EuiButtonIcon } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
export interface Props {
onClick: () => void;
disabled: boolean;
}
export function DocViewTableRowBtnFilterAdd({ onClick, disabled = false }: Props) {
const tooltipContent = disabled ? (
<FormattedMessage
id="unifiedDocViewer.docViews.table.unindexedFieldsCanNotBeSearchedTooltip"
defaultMessage="Unindexed fields or ignored values cannot be searched"
/>
) : (
<FormattedMessage
id="unifiedDocViewer.docViews.table.filterForValueButtonTooltip"
defaultMessage="Filter for value"
/>
);
return (
<EuiToolTip content={tooltipContent}>
<EuiButtonIcon
aria-label={i18n.translate(
'unifiedDocViewer.docViews.table.filterForValueButtonAriaLabel',
{
defaultMessage: 'Filter for value',
}
)}
className="kbnDocViewer__actionButton"
data-test-subj="addInclusiveFilterButton"
disabled={disabled}
onClick={onClick}
iconType={'plusInCircle'}
iconSize={'s'}
/>
</EuiToolTip>
);
}

View file

@ -1,63 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the "Elastic License
* 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
* Public License v 1"; you may not use this file except in compliance with, at
* your election, the "Elastic License 2.0", the "GNU Affero General Public
* License v3.0 only", or the "Server Side Public License, v 1".
*/
import React from 'react';
import { FormattedMessage } from '@kbn/i18n-react';
import { EuiToolTip, EuiButtonIcon } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
export interface Props {
onClick: () => void;
disabled?: boolean;
scripted?: boolean;
}
export function DocViewTableRowBtnFilterExists({
onClick,
disabled = false,
scripted = false,
}: Props) {
const tooltipContent = disabled ? (
scripted ? (
<FormattedMessage
id="unifiedDocViewer.docViews.table.unableToFilterForPresenceOfScriptedFieldsTooltip"
defaultMessage="Unable to filter for presence of scripted fields"
/>
) : (
<FormattedMessage
id="unifiedDocViewer.docViews.table.unableToFilterForPresenceOfMetaFieldsTooltip"
defaultMessage="Unable to filter for presence of meta fields"
/>
)
) : (
<FormattedMessage
id="unifiedDocViewer.docViews.table.filterForFieldPresentButtonTooltip"
defaultMessage="Filter for field present"
/>
);
return (
<EuiToolTip content={tooltipContent}>
<EuiButtonIcon
aria-label={i18n.translate(
'unifiedDocViewer.docViews.table.filterForFieldPresentButtonAriaLabel',
{
defaultMessage: 'Filter for field present',
}
)}
onClick={onClick}
className="kbnDocViewer__actionButton"
data-test-subj="addExistsFilterButton"
disabled={disabled}
iconType={'filter'}
iconSize={'s'}
/>
</EuiToolTip>
);
}

View file

@ -1,51 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the "Elastic License
* 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
* Public License v 1"; you may not use this file except in compliance with, at
* your election, the "Elastic License 2.0", the "GNU Affero General Public
* License v3.0 only", or the "Server Side Public License, v 1".
*/
import React from 'react';
import { FormattedMessage } from '@kbn/i18n-react';
import { i18n } from '@kbn/i18n';
import { EuiToolTip, EuiButtonIcon } from '@elastic/eui';
export interface Props {
onClick: () => void;
disabled?: boolean;
}
export function DocViewTableRowBtnFilterRemove({ onClick, disabled = false }: Props) {
const tooltipContent = disabled ? (
<FormattedMessage
id="unifiedDocViewer.docViews.table.unindexedFieldsCanNotBeSearchedTooltip"
defaultMessage="Unindexed fields or ignored values cannot be searched"
/>
) : (
<FormattedMessage
id="unifiedDocViewer.docViews.table.filterOutValueButtonTooltip"
defaultMessage="Filter out value"
/>
);
return (
<EuiToolTip content={tooltipContent}>
<EuiButtonIcon
aria-label={i18n.translate(
'unifiedDocViewer.docViews.table.filterOutValueButtonAriaLabel',
{
defaultMessage: 'Filter out value',
}
)}
className="kbnDocViewer__actionButton"
data-test-subj="removeInclusiveFilterButton"
disabled={disabled}
onClick={onClick}
iconType={'minusInCircle'}
iconSize={'s'}
/>
</EuiToolTip>
);
}

View file

@ -1,70 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the "Elastic License
* 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
* Public License v 1"; you may not use this file except in compliance with, at
* your election, the "Elastic License 2.0", the "GNU Affero General Public
* License v3.0 only", or the "Server Side Public License, v 1".
*/
import React from 'react';
import { FormattedMessage } from '@kbn/i18n-react';
import { i18n } from '@kbn/i18n';
import { EuiToolTip, EuiButtonIcon } from '@elastic/eui';
export interface Props {
active: boolean;
disabled?: boolean;
onClick: () => void;
fieldname: string;
}
export function DocViewTableRowBtnToggleColumn({
onClick,
active,
disabled = false,
fieldname = '',
}: Props) {
if (disabled) {
return (
<EuiButtonIcon
aria-label={i18n.translate(
'unifiedDocViewer.docViews.table.toggleColumnInTableButtonAriaLabel',
{
defaultMessage: 'Toggle column in table',
}
)}
className="kbnDocViewer__actionButton"
data-test-subj="toggleColumnButton"
disabled
iconType={'listAdd'}
iconSize={'s'}
/>
);
}
return (
<EuiToolTip
content={
<FormattedMessage
id="unifiedDocViewer.docViews.table.toggleColumnInTableButtonTooltip"
defaultMessage="Toggle column in table"
/>
}
>
<EuiButtonIcon
aria-label={i18n.translate(
'unifiedDocViewer.docViews.table.toggleColumnInTableButtonAriaLabel',
{
defaultMessage: 'Toggle column in table',
}
)}
aria-pressed={active}
onClick={onClick}
className="kbnDocViewer__actionButton"
data-test-subj={`toggleColumnButton-${fieldname}`}
iconType={'listAdd'}
iconSize={'s'}
/>
</EuiToolTip>
);
}

View file

@ -1,59 +1,3 @@
.kbnDocViewer {
.euiTableRowCell {
vertical-align: top;
}
}
.kbnDocViewer__tableRow {
font-size: $euiFontSizeXS;
font-family: $euiCodeFontFamily;
// set min-width for each column except actions
.euiTableRowCell:nth-child(n+2) {
min-width: $euiSizeM * 9;
}
.kbnDocViewer__buttons {
// Show all icons if one is focused,
&:focus-within {
.kbnDocViewer__actionButton {
opacity: 1;
}
}
}
&:hover {
.kbnDocViewer__actionButton {
opacity: 1;
}
}
.kbnDocViewer__actionButton {
@include euiBreakpoint('m', 'l', 'xl') {
opacity: 0;
}
&:focus {
opacity: 1;
}
}
}
.kbnDocViewer__tableActionsCell,
.kbnDocViewer__tableFieldNameCell {
.euiTableCellContent {
align-items: flex-start;
padding: $euiSizeXS;
}
}
.kbnDocViewer__tableValueCell {
.euiTableCellContent {
flex-direction: column;
align-items: flex-start;
}
}
.kbnDocViewer__value {
word-break: break-all;
word-wrap: break-word;

View file

@ -9,24 +9,10 @@
import React from 'react';
import { render, screen } from '@testing-library/react';
import { TRUNCATE_MAX_HEIGHT, TRUNCATE_MAX_HEIGHT_DEFAULT_VALUE } from '@kbn/discover-utils';
import type { IUiSettingsClient } from '@kbn/core-ui-settings-browser';
import { TableFieldValue } from './table_cell_value';
import { setUnifiedDocViewerServices } from '../../plugin';
import { mockUnifiedDocViewerServices } from '../../__mocks__';
const mockServices = {
...mockUnifiedDocViewerServices,
};
let mockTruncateMaxHeightSetting: number | undefined;
mockServices.uiSettings.get = ((key: string) => {
if (key === TRUNCATE_MAX_HEIGHT) {
return mockTruncateMaxHeightSetting ?? TRUNCATE_MAX_HEIGHT_DEFAULT_VALUE;
}
return;
}) as IUiSettingsClient['get'];
setUnifiedDocViewerServices(mockUnifiedDocViewerServices);
let mockScrollHeight = 0;
@ -35,7 +21,6 @@ jest.spyOn(HTMLElement.prototype, 'scrollHeight', 'get').mockImplementation(() =
describe('TableFieldValue', () => {
afterEach(() => {
mockScrollHeight = 0;
mockTruncateMaxHeightSetting = undefined;
});
it('should render correctly', async () => {
@ -121,97 +106,4 @@ describe('TableFieldValue', () => {
expect(valueElement.getAttribute('css')).toBeNull();
expect(valueElement.classList.contains('kbnDocViewer__value--truncated')).toBe(false);
});
it('should truncate a long value in legacy table correctly', async () => {
mockScrollHeight = 1000;
const value = 'long value'.repeat(300);
render(
<TableFieldValue
formattedValue={value}
rawValue={value}
field="message"
ignoreReason={undefined}
isDetails={false}
isLegacy={true}
/>
);
expect(screen.getByText(value)).toBeInTheDocument();
let toggleButton = screen.getByTestId('toggleLongFieldValue-message');
expect(toggleButton).toBeInTheDocument();
expect(toggleButton.getAttribute('aria-expanded')).toBe('false');
let valueElement = screen.getByTestId('tableDocViewRow-message-value');
expect(valueElement.getAttribute('css')).toBeDefined();
expect(valueElement.classList.contains('kbnDocViewer__value--truncated')).toBe(true);
toggleButton.click();
toggleButton = screen.getByTestId('toggleLongFieldValue-message');
expect(toggleButton).toBeInTheDocument();
expect(toggleButton.getAttribute('aria-expanded')).toBe('true');
valueElement = screen.getByTestId('tableDocViewRow-message-value');
expect(valueElement.getAttribute('css')).toBeNull();
expect(valueElement.classList.contains('kbnDocViewer__value--truncated')).toBe(false);
toggleButton.click();
toggleButton = screen.getByTestId('toggleLongFieldValue-message');
expect(toggleButton).toBeInTheDocument();
expect(toggleButton.getAttribute('aria-expanded')).toBe('false');
valueElement = screen.getByTestId('tableDocViewRow-message-value');
expect(valueElement.getAttribute('css')).toBeDefined();
expect(valueElement.classList.contains('kbnDocViewer__value--truncated')).toBe(true);
});
it('should not truncate a long value in legacy table if limit is not reached', async () => {
mockScrollHeight = 112;
const value = 'long value'.repeat(300);
render(
<TableFieldValue
formattedValue={value}
rawValue={value}
field="message"
ignoreReason={undefined}
isDetails={true}
isLegacy={true}
/>
);
expect(screen.getByText(value)).toBeInTheDocument();
expect(screen.queryByTestId('toggleLongFieldValue-message')).toBeNull();
const valueElement = screen.getByTestId('tableDocViewRow-message-value');
expect(valueElement.getAttribute('css')).toBeNull();
expect(valueElement.classList.contains('kbnDocViewer__value--truncated')).toBe(false);
});
it('should not truncate a long value in legacy table if setting is 0', async () => {
mockScrollHeight = 1000;
mockTruncateMaxHeightSetting = 0;
const value = 'long value'.repeat(300);
render(
<TableFieldValue
formattedValue={value}
rawValue={value}
field="message"
ignoreReason={undefined}
isDetails={true}
isLegacy={true}
/>
);
expect(screen.getByText(value)).toBeInTheDocument();
expect(screen.queryByTestId('toggleLongFieldValue-message')).toBeNull();
const valueElement = screen.getByTestId('tableDocViewRow-message-value');
expect(valueElement.getAttribute('css')).toBeNull();
expect(valueElement.classList.contains('kbnDocViewer__value--truncated')).toBe(false);
});
});

View file

@ -21,9 +21,7 @@ import {
import classnames from 'classnames';
import React, { Fragment, useCallback, useState } from 'react';
import { i18n } from '@kbn/i18n';
import { IgnoredReason, TRUNCATE_MAX_HEIGHT } from '@kbn/discover-utils';
import { FieldRecordLegacy } from '@kbn/unified-doc-viewer/types';
import { getUnifiedDocViewerServices } from '../../plugin';
import { IgnoredReason } from '@kbn/discover-utils';
const DOC_VIEWER_DEFAULT_TRUNCATE_MAX_HEIGHT = 110;
@ -96,14 +94,14 @@ const IgnoreWarning: React.FC<IgnoreWarningProps> = React.memo(({ rawValue, reas
);
});
type TableFieldValueProps = Pick<FieldRecordLegacy['field'], 'field'> & {
formattedValue: FieldRecordLegacy['value']['formattedValue'];
interface TableFieldValueProps {
field: string;
formattedValue: string;
rawValue: unknown;
ignoreReason?: IgnoredReason;
isDetails?: boolean; // true when inside EuiDataGrid cell popover
isLegacy?: boolean; // true when inside legacy table
isHighlighted?: boolean; // whether it's matching a search term
};
}
export const TableFieldValue = ({
formattedValue,
@ -111,14 +109,10 @@ export const TableFieldValue = ({
rawValue,
ignoreReason,
isDetails,
isLegacy,
isHighlighted,
}: TableFieldValueProps) => {
const { euiTheme } = useEuiTheme();
const { uiSettings } = getUnifiedDocViewerServices();
const truncationHeight = isLegacy
? uiSettings.get(TRUNCATE_MAX_HEIGHT)
: DOC_VIEWER_DEFAULT_TRUNCATE_MAX_HEIGHT;
const truncationHeight = DOC_VIEWER_DEFAULT_TRUNCATE_MAX_HEIGHT;
const [containerRef, setContainerRef] = useState<HTMLDivElement | null>(null);
useResizeObserver(containerRef);

View file

@ -9,7 +9,6 @@
import React from 'react';
import type { CoreSetup, Plugin } from '@kbn/core/public';
import { isLegacyTableEnabled } from '@kbn/discover-utils';
import { i18n } from '@kbn/i18n';
import { DocViewsRegistry } from '@kbn/unified-doc-viewer';
import { EuiDelayRender, EuiSkeletonText } from '@elastic/eui';
@ -31,9 +30,6 @@ const fallback = (
</EuiDelayRender>
);
const LazyDocViewerLegacyTable = dynamic(() => import('./components/doc_viewer_table/legacy'), {
fallback,
});
const LazyDocViewerTable = dynamic(() => import('./components/doc_viewer_table'), { fallback });
const LazySourceViewer = dynamic(() => import('./components/doc_viewer_source'), { fallback });
@ -65,17 +61,7 @@ export class UnifiedDocViewerPublicPlugin
}),
order: 10,
component: (props) => {
const { textBasedHits } = props;
const { uiSettings } = getUnifiedDocViewerServices();
const LazyDocView = isLegacyTableEnabled({
uiSettings,
isEsqlMode: Array.isArray(textBasedHits),
})
? LazyDocViewerLegacyTable
: LazyDocViewerTable;
return <LazyDocView {...props} />;
return <LazyDocViewerTable {...props} />;
},
});

View file

@ -1,168 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the "Elastic License
* 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
* Public License v 1"; you may not use this file except in compliance with, at
* your election, the "Elastic License 2.0", the "GNU Affero General Public
* License v3.0 only", or the "Server Side Public License, v 1".
*/
import expect from '@kbn/expect';
import { FtrProviderContext } from '../../../ftr_provider_context';
const TEST_COLUMN_NAMES = ['@message'];
const TEST_FILTER_COLUMN_NAMES = [
['extension', 'jpg', 'extension.raw'],
['geo.src', 'IN', 'geo.src'],
];
export default function ({ getService, getPageObjects }: FtrProviderContext) {
const retry = getService('retry');
const docTable = getService('docTable');
const filterBar = getService('filterBar');
const { common, discover, timePicker, dashboard, context, header, unifiedFieldList } =
getPageObjects([
'common',
'discover',
'timePicker',
'dashboard',
'context',
'header',
'unifiedFieldList',
]);
const testSubjects = getService('testSubjects');
const dashboardAddPanel = getService('dashboardAddPanel');
const browser = getService('browser');
const kibanaServer = getService('kibanaServer');
describe('context link in discover classic', () => {
before(async () => {
await timePicker.setDefaultAbsoluteRangeViaUiSettings();
await kibanaServer.uiSettings.update({
'doc_table:legacy': true,
defaultIndex: 'logstash-*',
});
await common.navigateToApp('discover');
await header.waitUntilLoadingHasFinished();
for (const columnName of TEST_COLUMN_NAMES) {
await unifiedFieldList.clickFieldListItemAdd(columnName);
}
for (const [columnName, value] of TEST_FILTER_COLUMN_NAMES) {
await unifiedFieldList.clickFieldListItem(columnName);
await unifiedFieldList.clickFieldListPlusFilter(columnName, value);
}
});
after(async () => {
await kibanaServer.uiSettings.replace({});
});
it('should open the context view with the same columns', async () => {
const columnNames = await docTable.getHeaderFields();
expect(columnNames).to.eql(['@timestamp', ...TEST_COLUMN_NAMES]);
});
it('should open the context view with the selected document as anchor and allows selecting next anchor', async () => {
/**
* Helper function to get the first timestamp of the document table
* @param isAnchorRow - determins if just the anchor row of context should be selected
*/
const getTimestamp = async (isAnchorRow: boolean = false) => {
const contextFields = await docTable.getFields({ isAnchorRow });
return contextFields[0][0];
};
// get the timestamp of the first row
const firstDiscoverTimestamp = await getTimestamp();
// check the anchor timestamp in the context view
await retry.waitFor('selected document timestamp matches anchor timestamp ', async () => {
// navigate to the context view
await docTable.clickRowToggle({ rowIndex: 0 });
const rowActions = await docTable.getRowActions({ rowIndex: 0 });
await rowActions[0].click();
await context.waitUntilContextLoadingHasFinished();
const anchorTimestamp = await getTimestamp(true);
return anchorTimestamp === firstDiscoverTimestamp;
});
await retry.waitFor('next anchor timestamp matches previous anchor timestamp', async () => {
// get the timestamp of the first row
const firstContextTimestamp = await getTimestamp(false);
await docTable.clickRowToggle({ rowIndex: 0 });
const rowActions = await docTable.getRowActions({ rowIndex: 0 });
await rowActions[0].click();
await context.waitUntilContextLoadingHasFinished();
const anchorTimestamp = await getTimestamp(true);
return anchorTimestamp === firstContextTimestamp;
});
});
it('should open the context view with the filters disabled', async () => {
let disabledFilterCounter = 0;
for (const [_, value, columnId] of TEST_FILTER_COLUMN_NAMES) {
if (await filterBar.hasFilter(columnId, value, false)) {
disabledFilterCounter++;
}
}
expect(disabledFilterCounter).to.be(TEST_FILTER_COLUMN_NAMES.length);
});
// bugfix: https://github.com/elastic/kibana/issues/92099
it('should navigate to the first document and then back to discover', async () => {
await context.waitUntilContextLoadingHasFinished();
// navigate to the doc view
await docTable.clickRowToggle({ rowIndex: 0 });
// click the open action
await retry.try(async () => {
const rowActions = await docTable.getRowActions({ rowIndex: 0 });
if (!rowActions.length) {
throw new Error('row actions empty, trying again');
}
await rowActions[1].click();
});
const hasDocHit = await testSubjects.exists('doc-hit');
expect(hasDocHit).to.be(true);
await testSubjects.click('~breadcrumb & ~first');
await discover.waitForDiscoverAppOnScreen();
await discover.waitForDocTableLoadingComplete();
});
it('navigates to doc view from embeddable', async () => {
await common.navigateToApp('discover');
await discover.saveSearch('my classic search');
await header.waitUntilLoadingHasFinished();
await dashboard.navigateToApp();
await dashboard.gotoDashboardLandingPage();
await dashboard.clickNewDashboard();
await dashboardAddPanel.addSavedSearch('my classic search');
await header.waitUntilLoadingHasFinished();
await docTable.clickRowToggle({ rowIndex: 0 });
const rowActions = await docTable.getRowActions({ rowIndex: 0 });
await rowActions[1].click();
await common.sleep(250);
// close popup
const alert = await browser.getAlert();
await alert?.accept();
if (await testSubjects.exists('confirmModalConfirmButton')) {
await testSubjects.click('confirmModalConfirmButton');
}
await retry.waitFor('navigate to doc view', async () => {
const currentUrl = await browser.getCurrentUrl();
return currentUrl.includes('#/doc');
});
await retry.waitFor('doc view being rendered', async () => {
return await discover.isShowingDocViewer();
});
});
});
}

View file

@ -1,69 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the "Elastic License
* 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
* Public License v 1"; you may not use this file except in compliance with, at
* your election, the "Elastic License 2.0", the "GNU Affero General Public
* License v3.0 only", or the "Server Side Public License, v 1".
*/
import { FtrProviderContext } from '../../../ftr_provider_context';
const TEST_INDEX_PATTERN = 'logstash-*';
const TEST_ANCHOR_ID = 'AU_x3_BrGFA8no6QjjaI';
const TEST_ANCHOR_FILTER_FIELD = 'geo.src';
const TEST_ANCHOR_FILTER_VALUE = 'IN';
const TEST_COLUMN_NAMES = ['extension', 'geo.src'];
export default function ({ getService, getPageObjects }: FtrProviderContext) {
const docTable = getService('docTable');
const filterBar = getService('filterBar');
const retry = getService('retry');
const kibanaServer = getService('kibanaServer');
const PageObjects = getPageObjects(['common', 'context']);
describe('context filters', function contextSize() {
before(async function () {
await kibanaServer.uiSettings.update({ 'doc_table:legacy': true });
});
after(async function () {
await kibanaServer.uiSettings.replace({});
});
beforeEach(async function () {
await PageObjects.context.navigateTo(TEST_INDEX_PATTERN, TEST_ANCHOR_ID, {
columns: TEST_COLUMN_NAMES,
});
});
it('inclusive filter should be addable via expanded doc table rows', async function () {
await retry.waitFor(`filter ${TEST_ANCHOR_FILTER_FIELD} in filterbar`, async () => {
await docTable.toggleRowExpanded({ isAnchorRow: true });
const anchorDetailsRow = await docTable.getAnchorDetailsRow();
await docTable.addInclusiveFilter(anchorDetailsRow, TEST_ANCHOR_FILTER_FIELD);
await PageObjects.context.waitUntilContextLoadingHasFinished();
return await filterBar.hasFilter(TEST_ANCHOR_FILTER_FIELD, TEST_ANCHOR_FILTER_VALUE, true);
});
await retry.waitFor(`filter matching docs in docTable`, async () => {
const fields = await docTable.getFields();
return fields
.map((row) => row[2])
.every((fieldContent) => fieldContent === TEST_ANCHOR_FILTER_VALUE);
});
});
it('filter for presence should be addable via expanded doc table rows', async function () {
await docTable.toggleRowExpanded({ isAnchorRow: true });
await retry.waitFor('an exists filter in the filterbar', async () => {
const anchorDetailsRow = await docTable.getAnchorDetailsRow();
await docTable.addExistsFilter(anchorDetailsRow, TEST_ANCHOR_FILTER_FIELD);
await PageObjects.context.waitUntilContextLoadingHasFinished();
return await filterBar.hasFilter(TEST_ANCHOR_FILTER_FIELD, 'exists', true);
});
});
});
}

View file

@ -33,9 +33,7 @@ export default function ({ getService, getPageObjects, loadTestFile }: FtrProvid
loadTestFile(require.resolve('./_context_accessibility'));
loadTestFile(require.resolve('./_context_navigation'));
loadTestFile(require.resolve('./_discover_navigation'));
loadTestFile(require.resolve('./classic/_discover_navigation'));
loadTestFile(require.resolve('./_filters'));
loadTestFile(require.resolve('./classic/_filters'));
loadTestFile(require.resolve('./_size'));
loadTestFile(require.resolve('./_date_nanos'));
loadTestFile(require.resolve('./_date_nanos_custom_timestamp'));

View file

@ -30,7 +30,6 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
);
await kibanaServer.uiSettings.replace({
defaultIndex: '0bf35f60-3dc9-11e8-8660-4d65aa086b3c',
'doc_table:legacy': false,
});
await dashboard.navigateToApp();
await filterBar.ensureFieldEditorModalIsClosed();

View file

@ -23,12 +23,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
const browser = getService('browser');
const queryBar = getService('queryBar');
const security = getService('security');
const { dashboard, discover, header, timePicker } = getPageObjects([
'dashboard',
'discover',
'header',
'timePicker',
]);
const { dashboard, header, timePicker } = getPageObjects(['dashboard', 'header', 'timePicker']);
describe('dashboard filter bar', () => {
before(async () => {
@ -201,12 +196,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
it('are added when a cell magnifying glass is clicked', async function () {
await dashboardAddPanel.addSavedSearch('Rendering-Test:-saved-search');
await dashboard.waitForRenderComplete();
const isLegacyDefault = await discover.useLegacyTable();
if (isLegacyDefault) {
await testSubjects.click('docTableCellFilter');
} else {
await dataGrid.clickCellFilterForButtonExcludingControlColumns(1, 1);
}
await dataGrid.clickCellFilterForButtonExcludingControlColumns(1, 1);
const filterCount = await filterBar.getFilterCount();
expect(filterCount).to.equal(1);
});

View file

@ -13,16 +13,10 @@ import { PIE_CHART_VIS_NAME } from '../../../page_objects/dashboard_page';
import { FtrProviderContext } from '../../../ftr_provider_context';
export default function ({ getService, getPageObjects }: FtrProviderContext) {
const dashboardExpect = getService('dashboardExpect');
const pieChart = getService('pieChart');
const elasticChart = getService('elasticChart');
const dashboardVisualizations = getService('dashboardVisualizations');
const { dashboard, header, timePicker, discover } = getPageObjects([
'dashboard',
'header',
'timePicker',
'discover',
]);
const { dashboard, header, timePicker } = getPageObjects(['dashboard', 'header', 'timePicker']);
const browser = getService('browser');
const log = getService('log');
const kibanaServer = getService('kibanaServer');
@ -59,28 +53,13 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
fields: ['bytes', 'agent'],
});
const isLegacyDefault = await discover.useLegacyTable();
if (isLegacyDefault) {
await dashboardExpect.docTableFieldCount(150);
const docCount = await dataGrid.getDocCount();
expect(docCount).to.above(10);
// Set to time range with no data
await timePicker.setAbsoluteRange(
'Jan 1, 2000 @ 00:00:00.000',
'Jan 1, 2000 @ 01:00:00.000'
);
await dashboardExpect.docTableFieldCount(0);
} else {
const docCount = await dataGrid.getDocCount();
expect(docCount).to.above(10);
// Set to time range with no data
await timePicker.setAbsoluteRange(
'Jan 1, 2000 @ 00:00:00.000',
'Jan 1, 2000 @ 01:00:00.000'
);
const noResults = await dataGrid.hasNoResults();
expect(noResults).to.be.ok();
}
// Set to time range with no data
await timePicker.setAbsoluteRange('Jan 1, 2000 @ 00:00:00.000', 'Jan 1, 2000 @ 01:00:00.000');
const noResults = await dataGrid.hasNoResults();
expect(noResults).to.be.ok();
});
it('Timepicker start, end, interval values are set by url', async () => {

View file

@ -1,75 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the "Elastic License
* 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
* Public License v 1"; you may not use this file except in compliance with, at
* your election, the "Elastic License 2.0", the "GNU Affero General Public
* License v3.0 only", or the "Server Side Public License, v 1".
*/
import expect from '@kbn/expect';
import { FtrProviderContext } from '../ftr_provider_context';
export default function ({ getService, getPageObjects }: FtrProviderContext) {
const docTable = getService('docTable');
const filterBar = getService('filterBar');
const testSubjects = getService('testSubjects');
const { common, discover, timePicker } = getPageObjects(['common', 'discover', 'timePicker']);
const esArchiver = getService('esArchiver');
const retry = getService('retry');
const kibanaServer = getService('kibanaServer');
describe('classic table doc link', function contextSize() {
before(async () => {
await esArchiver.loadIfNeeded('test/functional/fixtures/es_archiver/logstash_functional');
await kibanaServer.importExport.load('test/functional/fixtures/kbn_archiver/discover');
await timePicker.setDefaultAbsoluteRangeViaUiSettings();
await kibanaServer.uiSettings.update({
defaultIndex: 'logstash-*',
'doc_table:legacy': true,
'discover:searchFieldsFromSource': true,
});
});
after(async () => {
await kibanaServer.importExport.unload('test/functional/fixtures/kbn_archiver/discover');
await kibanaServer.uiSettings.replace({});
});
beforeEach(async function () {
await timePicker.setDefaultAbsoluteRangeViaUiSettings();
await common.navigateToApp('discover');
await discover.waitForDocTableLoadingComplete();
});
it('should open the doc view of the selected document', async function () {
// navigate to the doc view
await docTable.clickRowToggle({ rowIndex: 0 });
// click the open action
await retry.try(async () => {
const rowActions = await docTable.getRowActions({ rowIndex: 0 });
if (!rowActions.length) {
throw new Error('row actions empty, trying again');
}
await rowActions[1].click();
});
await retry.waitFor('hit loaded', async () => {
const hasDocHit = await testSubjects.exists('doc-hit');
return !!hasDocHit;
});
});
it('should create an exists filter from the doc view of the selected document', async function () {
await discover.waitUntilSearchingHasFinished();
await docTable.toggleRowExpanded();
const detailsRow = await docTable.getDetailsRow();
await docTable.addExistsFilter(detailsRow, '@timestamp');
const hasExistsFilter = await filterBar.hasFilter('@timestamp', 'exists', true, false, false);
expect(hasExistsFilter).to.be(true);
});
});
}

View file

@ -1,98 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the "Elastic License
* 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
* Public License v 1"; you may not use this file except in compliance with, at
* your election, the "Elastic License 2.0", the "GNU Affero General Public
* License v3.0 only", or the "Server Side Public License, v 1".
*/
import expect from '@kbn/expect';
import { FtrProviderContext } from '../ftr_provider_context';
export default function ({ getService, getPageObjects }: FtrProviderContext) {
const log = getService('log');
const retry = getService('retry');
const esArchiver = getService('esArchiver');
const kibanaServer = getService('kibanaServer');
const { common, discover, timePicker, settings, unifiedFieldList } = getPageObjects([
'common',
'discover',
'timePicker',
'settings',
'unifiedFieldList',
]);
const defaultSettings = {
defaultIndex: 'logstash-*',
'discover:searchFieldsFromSource': false,
'doc_table:legacy': true,
};
describe('discover uses fields API test', function describeIndexTests() {
before(async function () {
log.debug('load kibana index with default index pattern');
await kibanaServer.savedObjects.clean({ types: ['search', 'index-pattern'] });
await kibanaServer.importExport.load('test/functional/fixtures/kbn_archiver/discover.json');
await esArchiver.loadIfNeeded('test/functional/fixtures/es_archiver/logstash_functional');
await kibanaServer.uiSettings.replace(defaultSettings);
await common.navigateToApp('discover');
await timePicker.setDefaultAbsoluteRange();
});
after(async () => {
await kibanaServer.uiSettings.replace({});
});
it('should correctly display documents', async function () {
log.debug('check if Document title exists in the grid');
expect(await discover.getDocHeader()).to.have.string('Document');
const rowData = await discover.getDocTableIndex(1);
log.debug('check the newest doc timestamp in UTC (check diff timezone in last test)');
expect(rowData.startsWith('Sep 22, 2015 @ 23:50:13.253')).to.be.ok();
const expectedHitCount = '14,004';
await retry.try(async function () {
expect(await discover.getHitCount()).to.be(expectedHitCount);
});
});
it('adding a column removes a default column', async function () {
await unifiedFieldList.clickFieldListItemAdd('_score');
expect(await discover.getDocHeader()).to.have.string('_score');
expect(await discover.getDocHeader()).not.to.have.string('Document');
});
it('removing a column adds a default column', async function () {
await unifiedFieldList.clickFieldListItemRemove('_score');
expect(await discover.getDocHeader()).not.to.have.string('_score');
expect(await discover.getDocHeader()).to.have.string('Document');
});
it('displays _source viewer in doc viewer', async function () {
await discover.clickDocTableRowToggle(0);
await discover.isShowingDocViewer();
await discover.clickDocViewerTab('doc_view_source');
await discover.expectSourceViewerToExist();
});
it('switches to _source column when fields API is no longer used', async function () {
await settings.navigateTo();
await settings.clickKibanaSettings();
await settings.toggleAdvancedSettingCheckbox('discover:searchFieldsFromSource');
await common.navigateToApp('discover');
await timePicker.setDefaultAbsoluteRange();
expect(await discover.getDocHeader()).to.have.string('_source');
});
it('switches to Document column when fields API is used', async function () {
await settings.navigateTo();
await settings.clickKibanaSettings();
await settings.toggleAdvancedSettingCheckbox('discover:searchFieldsFromSource');
await common.navigateToApp('discover');
await timePicker.setDefaultAbsoluteRange();
expect(await discover.getDocHeader()).to.have.string('Document');
});
});
}

View file

@ -1,309 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the "Elastic License
* 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
* Public License v 1"; you may not use this file except in compliance with, at
* your election, the "Elastic License 2.0", the "GNU Affero General Public
* License v3.0 only", or the "Server Side Public License, v 1".
*/
import expect from '@kbn/expect';
import { FtrProviderContext } from '../ftr_provider_context';
export default function ({ getService, getPageObjects }: FtrProviderContext) {
const browser = getService('browser');
const log = getService('log');
const retry = getService('retry');
const esArchiver = getService('esArchiver');
const kibanaServer = getService('kibanaServer');
const docTable = getService('docTable');
const queryBar = getService('queryBar');
const find = getService('find');
const { common, discover, header, timePicker, unifiedFieldList } = getPageObjects([
'common',
'discover',
'header',
'timePicker',
'unifiedFieldList',
]);
const defaultSettings = {
defaultIndex: 'logstash-*',
hideAnnouncements: true,
'doc_table:legacy': true,
};
const testSubjects = getService('testSubjects');
describe('discover doc table', function describeIndexTests() {
const rowsHardLimit = 500;
before(async function () {
log.debug('load kibana index with default index pattern');
await kibanaServer.savedObjects.cleanStandardList();
await kibanaServer.importExport.load('test/functional/fixtures/kbn_archiver/discover.json');
// and load a set of makelogs data
await esArchiver.loadIfNeeded('test/functional/fixtures/es_archiver/logstash_functional');
await kibanaServer.uiSettings.replace(defaultSettings);
await timePicker.setDefaultAbsoluteRangeViaUiSettings();
log.debug('discover doc table');
await common.navigateToApp('discover');
});
after(async function () {
await kibanaServer.importExport.unload('test/functional/fixtures/kbn_archiver/discover.json');
await kibanaServer.savedObjects.cleanStandardList();
await kibanaServer.uiSettings.replace({});
});
it('should show records by default', async function () {
// with the default range the number of hits is ~14000
const rows = await discover.getDocTableRows();
expect(rows.length).to.be.greaterThan(0);
});
it('should refresh the table content when changing time window', async function () {
const initialRows = await discover.getDocTableRows();
const fromTime = 'Sep 20, 2015 @ 23:00:00.000';
const toTime = 'Sep 20, 2015 @ 23:14:00.000';
await timePicker.setAbsoluteRange(fromTime, toTime);
await discover.waitUntilSearchingHasFinished();
const finalRows = await discover.getDocTableRows();
expect(finalRows.length).to.be.below(initialRows.length);
await timePicker.setDefaultAbsoluteRange();
});
describe('classic table in window 900x700', function () {
before(async () => {
await browser.setWindowSize(900, 700);
await common.navigateToApp('discover');
await discover.waitUntilSearchingHasFinished();
});
it('should load more rows when scrolling down the document table', async function () {
const initialRows = await testSubjects.findAll('docTableRow');
await testSubjects.scrollIntoView('discoverBackToTop');
// now count the rows
await retry.waitFor('next batch of documents to be displayed', async () => {
const actual = await testSubjects.findAll('docTableRow');
log.debug(`initial doc nr: ${initialRows.length}, actual doc nr: ${actual.length}`);
return actual.length > initialRows.length;
});
});
});
describe('classic table in window 600x700', function () {
before(async () => {
await browser.setWindowSize(600, 700);
await common.navigateToApp('discover');
await discover.waitUntilSearchingHasFinished();
});
it('should load more rows when scrolling down the document table', async function () {
const initialRows = await testSubjects.findAll('docTableRow');
await testSubjects.scrollIntoView('discoverBackToTop');
// now count the rows
await retry.waitFor('next batch of documents to be displayed', async () => {
const actual = await testSubjects.findAll('docTableRow');
log.debug(`initial doc nr: ${initialRows.length}, actual doc nr: ${actual.length}`);
return actual.length > initialRows.length;
});
});
});
describe('legacy', function () {
before(async () => {
await common.navigateToApp('discover');
await discover.waitUntilSearchingHasFinished();
});
after(async () => {
await kibanaServer.uiSettings.replace({});
});
it(`should load up to ${rowsHardLimit} rows when scrolling at the end of the table`, async function () {
const initialRows = await testSubjects.findAll('docTableRow');
// click the Skip to the end of the table
await discover.skipToEndOfDocTable();
// now count the rows
const finalRows = await testSubjects.findAll('docTableRow');
expect(finalRows.length).to.be.above(initialRows.length);
expect(finalRows.length).to.be(rowsHardLimit);
await discover.backToTop();
});
it('should go the end and back to top of the classic table when using the accessible buttons', async function () {
// click the Skip to the end of the table
await discover.skipToEndOfDocTable();
// now check the footer text content
const footer = await discover.getDocTableFooter();
expect(await footer.getVisibleText()).to.have.string(rowsHardLimit);
await discover.backToTop();
// check that the skip to end of the table button now has focus
const skipButton = await testSubjects.find('discoverSkipTableButton');
const activeElement = await find.activeElement();
const activeElementText = await activeElement.getVisibleText();
const skipButtonText = await skipButton.getVisibleText();
expect(skipButtonText === activeElementText).to.be(true);
});
describe('expand a document row', function () {
const rowToInspect = 1;
beforeEach(async function () {
// close the toggle if open
const details = await docTable.getDetailsRows();
if (details.length) {
await docTable.clickRowToggle({ isAnchorRow: false, rowIndex: rowToInspect - 1 });
}
});
it('should expand the detail row when the toggle arrow is clicked', async function () {
await retry.try(async function () {
await docTable.clickRowToggle({ isAnchorRow: false, rowIndex: rowToInspect - 1 });
const detailsEl = await docTable.getDetailsRows();
const defaultMessageEl = await detailsEl[0].findByTestSubject(
'docViewerRowDetailsTitle'
);
expect(defaultMessageEl).to.be.ok();
});
});
it('should show the detail panel actions', async function () {
await retry.try(async function () {
await docTable.clickRowToggle({ isAnchorRow: false, rowIndex: rowToInspect - 1 });
// const detailsEl = await discover.getDocTableRowDetails(rowToInspect);
const [surroundingActionEl, singleActionEl] = await docTable.getRowActions({
isAnchorRow: false,
rowIndex: rowToInspect - 1,
});
expect(surroundingActionEl).to.be.ok();
expect(singleActionEl).to.be.ok();
// TODO: test something more meaninful here?
});
});
it('should not close the detail panel actions when data is re-requested', async function () {
await retry.try(async function () {
const nrOfFetches = await discover.getNrOfFetches();
await docTable.clickRowToggle({ isAnchorRow: false, rowIndex: rowToInspect - 1 });
const detailsEl = await docTable.getDetailsRows();
const defaultMessageEl = await detailsEl[0].findByTestSubject(
'docViewerRowDetailsTitle'
);
expect(defaultMessageEl).to.be.ok();
await queryBar.submitQuery();
const nrOfFetchesResubmit = await discover.getNrOfFetches();
expect(nrOfFetchesResubmit).to.be.above(nrOfFetches);
const defaultMessageElResubmit = await detailsEl[0].findByTestSubject(
'docViewerRowDetailsTitle'
);
expect(defaultMessageElResubmit).to.be.ok();
});
});
it('should show allow toggling columns from the expanded document', async function () {
await discover.clickNewSearchButton();
await retry.try(async function () {
await docTable.clickRowToggle({ isAnchorRow: false, rowIndex: rowToInspect - 1 });
// add columns
const fields = ['_id', '_index', 'agent'];
for (const field of fields) {
await testSubjects.click(`toggleColumnButton-${field}`);
await testSubjects.click(`tableDocViewRow-${field}`); // to suppress the appeared tooltip
}
const headerWithFields = await docTable.getHeaderFields();
expect(headerWithFields.join(' ')).to.contain(fields.join(' '));
// remove columns
for (const field of fields) {
await testSubjects.click(`toggleColumnButton-${field}`);
await testSubjects.click(`tableDocViewRow-${field}`);
}
const headerWithoutFields = await docTable.getHeaderFields();
expect(headerWithoutFields.join(' ')).not.to.contain(fields.join(' '));
});
});
});
describe('add and remove columns', function () {
const extraColumns = ['phpmemory', 'ip'];
const expectedFieldLength: Record<string, number> = {
phpmemory: 1,
ip: 4,
};
afterEach(async function () {
for (const column of extraColumns) {
await unifiedFieldList.clickFieldListItemRemove(column);
await header.waitUntilLoadingHasFinished();
}
});
it('should add more columns to the table', async function () {
for (const column of extraColumns) {
await unifiedFieldList.clearFieldSearchInput();
await unifiedFieldList.findFieldByName(column);
await unifiedFieldList.waitUntilFieldlistHasCountOfFields(expectedFieldLength[column]);
await retry.waitFor('field to appear', async function () {
return await testSubjects.exists(`field-${column}`);
});
await unifiedFieldList.clickFieldListItemAdd(column);
await header.waitUntilLoadingHasFinished();
// test the header now
const docHeader = await find.byCssSelector('thead > tr:nth-child(1)');
const docHeaderText = await docHeader.getVisibleText();
expect(docHeaderText).to.have.string(column);
}
});
it('should remove columns from the table', async function () {
for (const column of extraColumns) {
await unifiedFieldList.clearFieldSearchInput();
await unifiedFieldList.findFieldByName(column);
await unifiedFieldList.waitUntilFieldlistHasCountOfFields(expectedFieldLength[column]);
await unifiedFieldList.clickFieldListItemAdd(column);
await header.waitUntilLoadingHasFinished();
}
// remove the second column
await unifiedFieldList.clickFieldListItemRemove(extraColumns[1]);
await header.waitUntilLoadingHasFinished();
// test that the second column is no longer there
const docHeader = await find.byCssSelector('thead > tr:nth-child(1)');
expect(await docHeader.getVisibleText()).to.not.have.string(extraColumns[1]);
});
});
it('should make the document table scrollable', async function () {
await unifiedFieldList.clearFieldSearchInput();
const dscTableWrapper = await find.byCssSelector('.kbnDocTableWrapper');
const fieldNames = await unifiedFieldList.getAllFieldNames();
const clientHeight = await dscTableWrapper.getAttribute('clientHeight');
let fieldCounter = 0;
const checkScrollable = async () => {
const scrollWidth = await dscTableWrapper.getAttribute('scrollWidth');
const clientWidth = await dscTableWrapper.getAttribute('clientWidth');
log.debug(`scrollWidth: ${scrollWidth}, clientWidth: ${clientWidth}`);
return Number(scrollWidth) > Number(clientWidth);
};
const addColumn = async () => {
await unifiedFieldList.clickFieldListItemAdd(fieldNames[fieldCounter++]);
};
await addColumn();
const isScrollable = await checkScrollable();
expect(isScrollable).to.be(false);
await retry.waitForWithTimeout('container to be scrollable', 60 * 1000, async () => {
await addColumn();
return await checkScrollable();
});
// so now we need to check if the horizontal scrollbar is displayed
const newClientHeight = await dscTableWrapper.getAttribute('clientHeight');
expect(Number(clientHeight)).to.be.above(Number(newClientHeight));
});
});
});
}

View file

@ -1,56 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the "Elastic License
* 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
* Public License v 1"; you may not use this file except in compliance with, at
* your election, the "Elastic License 2.0", the "GNU Affero General Public
* License v3.0 only", or the "Server Side Public License, v 1".
*/
import { FtrProviderContext } from '../ftr_provider_context';
export default function ({ getService, getPageObjects }: FtrProviderContext) {
const esArchiver = getService('esArchiver');
const kibanaServer = getService('kibanaServer');
const { common, unifiedFieldList } = getPageObjects(['common', 'unifiedFieldList']);
const find = getService('find');
const log = getService('log');
const retry = getService('retry');
const security = getService('security');
describe('discover doc table newline handling', function describeIndexTests() {
before(async function () {
await security.testUser.setRoles(['kibana_admin', 'kibana_message_with_newline']);
await esArchiver.loadIfNeeded('test/functional/fixtures/es_archiver/message_with_newline');
await kibanaServer.importExport.load(
'test/functional/fixtures/kbn_archiver/message_with_newline.json'
);
await kibanaServer.uiSettings.replace({
defaultIndex: 'newline-test',
'doc_table:legacy': true,
});
await common.navigateToApp('discover');
});
after(async () => {
await security.testUser.restoreDefaults();
await esArchiver.unload('test/functional/fixtures/es_archiver/message_with_newline');
await kibanaServer.savedObjects.cleanStandardList();
await kibanaServer.uiSettings.replace({});
});
it('should break text on newlines', async function () {
await unifiedFieldList.clickFieldListItemToggle('message');
const dscTableRows = await find.allByCssSelector('.kbnDocTable__row');
await retry.waitFor('height of multi-line content > single-line content', async () => {
const heightWithoutNewline = await dscTableRows[0].getAttribute('clientHeight');
const heightWithNewline = await dscTableRows[1].getAttribute('clientHeight');
log.debug(`Without newlines: ${heightWithoutNewline}, With newlines: ${heightWithNewline}`);
await common.sleep(10000);
return Number(heightWithNewline) > Number(heightWithoutNewline);
});
});
});
}

View file

@ -1,95 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the "Elastic License
* 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
* Public License v 1"; you may not use this file except in compliance with, at
* your election, the "Elastic License 2.0", the "GNU Affero General Public
* License v3.0 only", or the "Server Side Public License, v 1".
*/
import { FtrProviderContext } from '../ftr_provider_context';
export default function ({ getService, getPageObjects }: FtrProviderContext) {
const esArchiver = getService('esArchiver');
const kibanaServer = getService('kibanaServer');
const testSubjects = getService('testSubjects');
const security = getService('security');
const dataGrid = getService('dataGrid');
const dashboardAddPanel = getService('dashboardAddPanel');
const dashboardPanelActions = getService('dashboardPanelActions');
const { common, discover, dashboard, header, timePicker } = getPageObjects([
'common',
'discover',
'dashboard',
'header',
'timePicker',
]);
const defaultSettings = {
defaultIndex: 'logstash-*',
'doc_table:legacy': true,
};
describe('discover esql grid with legacy setting', function () {
before(async () => {
await security.testUser.setRoles(['kibana_admin', 'test_logstash_reader']);
await kibanaServer.importExport.load('test/functional/fixtures/kbn_archiver/discover');
await esArchiver.loadIfNeeded('test/functional/fixtures/es_archiver/logstash_functional');
await kibanaServer.uiSettings.replace(defaultSettings);
await common.navigateToApp('discover');
await timePicker.setDefaultAbsoluteRange();
});
after(async () => {
await kibanaServer.savedObjects.cleanStandardList();
await kibanaServer.importExport.unload('test/functional/fixtures/kbn_archiver/discover');
await esArchiver.unload('test/functional/fixtures/es_archiver/logstash_functional');
await kibanaServer.uiSettings.replace({});
});
it('should render esql view correctly', async function () {
const savedSearchESQL = 'testESQLWithLegacySetting';
await header.waitUntilLoadingHasFinished();
await discover.waitUntilSearchingHasFinished();
await testSubjects.existOrFail('docTableHeader');
await testSubjects.missingOrFail('euiDataGridBody');
await discover.selectTextBaseLang();
await header.waitUntilLoadingHasFinished();
await discover.waitUntilSearchingHasFinished();
await testSubjects.missingOrFail('docTableHeader');
await testSubjects.existOrFail('euiDataGridBody');
await dataGrid.clickRowToggle({ rowIndex: 0 });
await testSubjects.existOrFail('docViewerFlyout');
await discover.saveSearch(savedSearchESQL);
await common.navigateToApp('dashboard');
await dashboard.clickNewDashboard();
await timePicker.setDefaultAbsoluteRange();
await dashboardAddPanel.clickOpenAddPanel();
await dashboardAddPanel.addSavedSearch(savedSearchESQL);
await header.waitUntilLoadingHasFinished();
await testSubjects.missingOrFail('docTableHeader');
await testSubjects.existOrFail('euiDataGridBody');
await dataGrid.clickRowToggle({ rowIndex: 0 });
await testSubjects.existOrFail('docViewerFlyout');
await dashboardPanelActions.removePanelByTitle(savedSearchESQL);
await dashboardAddPanel.addSavedSearch('A Saved Search');
await header.waitUntilLoadingHasFinished();
await testSubjects.existOrFail('docTableHeader');
await testSubjects.missingOrFail('euiDataGridBody');
});
});
}

View file

@ -1,63 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the "Elastic License
* 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
* Public License v 1"; you may not use this file except in compliance with, at
* your election, the "Elastic License 2.0", the "GNU Affero General Public
* License v3.0 only", or the "Server Side Public License, v 1".
*/
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 { common, timePicker } = getPageObjects(['common', 'timePicker']);
const find = getService('find');
const testSubjects = getService('testSubjects');
describe('discover tab', function describeIndexTests() {
this.tags('includeFirefox');
before(async function () {
await kibanaServer.savedObjects.clean({ types: ['search', 'index-pattern'] });
await kibanaServer.importExport.load('test/functional/fixtures/kbn_archiver/discover.json');
await esArchiver.loadIfNeeded('test/functional/fixtures/es_archiver/logstash_functional');
await kibanaServer.uiSettings.replace({
defaultIndex: 'logstash-*',
'discover:searchFieldsFromSource': true,
'doc_table:legacy': true,
});
await timePicker.setDefaultAbsoluteRangeViaUiSettings();
await common.navigateToApp('discover');
});
after(async function () {
await kibanaServer.uiSettings.replace({});
});
it('doc view should show @timestamp and _source columns', async function () {
const expectedHeader = '@timestamp\n_source';
const docHeader = await find.byCssSelector('thead > tr:nth-child(1)');
const docHeaderText = await docHeader.getVisibleText();
expect(docHeaderText).to.be(expectedHeader);
});
it('doc view should sort ascending', async function () {
const expectedTimeStamp = 'Sep 20, 2015 @ 00:00:00.000';
await testSubjects.click('docTableHeaderFieldSort_@timestamp');
// we don't technically need this sleep here because the tryForTime will retry and the
// results will match on the 2nd or 3rd attempt, but that debug output is huge in this
// case and it can be avoided with just a few seconds sleep.
await common.sleep(2000);
await retry.try(async function tryingForTime() {
const row = await find.byCssSelector(`tr.kbnDocTable__row:nth-child(1)`);
const rowData = await row.getVisibleText();
expect(rowData.startsWith(expectedTimeStamp)).to.be.ok();
});
});
});
}

View file

@ -1,56 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the "Elastic License
* 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
* Public License v 1"; you may not use this file except in compliance with, at
* your election, the "Elastic License 2.0", the "GNU Affero General Public
* License v3.0 only", or the "Server Side Public License, v 1".
*/
import expect from '@kbn/expect';
import { FtrProviderContext } from '../ftr_provider_context';
export default function ({ getService, getPageObjects }: FtrProviderContext) {
const retry = getService('retry');
const kibanaServer = getService('kibanaServer');
const { common, timePicker } = getPageObjects(['common', 'timePicker']);
const find = getService('find');
const esArchiver = getService('esArchiver');
const testSubjects = getService('testSubjects');
describe('discover tab with new fields API', function describeIndexTests() {
before(async function () {
await kibanaServer.savedObjects.clean({ types: ['search', 'index-pattern'] });
await kibanaServer.importExport.load('test/functional/fixtures/kbn_archiver/discover.json');
await esArchiver.loadIfNeeded('test/functional/fixtures/es_archiver/logstash_functional');
await kibanaServer.uiSettings.replace({
defaultIndex: 'logstash-*',
'discover:searchFieldsFromSource': false,
'doc_table:legacy': true,
});
await timePicker.setDefaultAbsoluteRangeViaUiSettings();
await common.navigateToApp('discover');
});
after(async function () {
await kibanaServer.uiSettings.replace({});
});
it('doc view should sort ascending', async function () {
const expectedTimeStamp = 'Sep 20, 2015 @ 00:00:00.000';
await testSubjects.click('docTableHeaderFieldSort_@timestamp');
// we don't technically need this sleep here because the tryForTime will retry and the
// results will match on the 2nd or 3rd attempt, but that debug output is huge in this
// case and it can be avoided with just a few seconds sleep.
await common.sleep(2000);
await retry.try(async function tryingForTime() {
const row = await find.byCssSelector(`tr.kbnDocTable__row:nth-child(1)`);
const rowData = await row.getVisibleText();
expect(rowData.startsWith(expectedTimeStamp)).to.be.ok();
});
});
});
}

View file

@ -1,49 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the "Elastic License
* 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
* Public License v 1"; you may not use this file except in compliance with, at
* your election, the "Elastic License 2.0", the "GNU Affero General Public
* License v3.0 only", or the "Server Side Public License, v 1".
*/
import expect from '@kbn/expect';
import { FtrProviderContext } from '../ftr_provider_context';
export default function ({ getService, getPageObjects }: FtrProviderContext) {
const esArchiver = getService('esArchiver');
const { common, discover, timePicker } = getPageObjects(['common', 'discover', 'timePicker']);
const kibanaServer = getService('kibanaServer');
const testSubjects = getService('testSubjects');
const browser = getService('browser');
describe('test hide announcements', function () {
before(async function () {
await kibanaServer.importExport.load('test/functional/fixtures/kbn_archiver/discover.json');
await esArchiver.loadIfNeeded('test/functional/fixtures/es_archiver/logstash_functional');
await kibanaServer.uiSettings.replace({
defaultIndex: 'logstash-*',
'doc_table:legacy': true,
});
await common.navigateToApp('discover');
await timePicker.setDefaultAbsoluteRange();
});
after(async () => {
await kibanaServer.uiSettings.replace({});
});
it('should display try document explorer button', async function () {
await discover.selectIndexPattern('logstash-*');
const tourButtonExists = await testSubjects.exists('tryDocumentExplorerButton');
expect(tourButtonExists).to.be(true);
});
it('should not display try document explorer button', async function () {
await kibanaServer.uiSettings.update({ hideAnnouncements: true });
await browser.refresh();
const tourButtonExists = await testSubjects.exists('tryDocumentExplorerButton');
expect(tourButtonExists).to.be(false);
});
});
}

View file

@ -1,19 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the "Elastic License
* 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
* Public License v 1"; you may not use this file except in compliance with, at
* your election, the "Elastic License 2.0", the "GNU Affero General Public
* License v3.0 only", or the "Server Side Public License, v 1".
*/
import { FtrConfigProviderContext } from '@kbn/test';
export default async function ({ readConfigFile }: FtrConfigProviderContext) {
const functionalConfig = await readConfigFile(require.resolve('../../../config.base.js'));
return {
...functionalConfig.getAll(),
testFiles: [require.resolve('.')],
};
}

View file

@ -1,34 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the "Elastic License
* 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
* Public License v 1"; you may not use this file except in compliance with, at
* your election, the "Elastic License 2.0", the "GNU Affero General Public
* License v3.0 only", or the "Server Side Public License, v 1".
*/
import { FtrProviderContext } from '../ftr_provider_context';
export default function ({ getService, loadTestFile }: FtrProviderContext) {
const esArchiver = getService('esArchiver');
const browser = getService('browser');
describe('discover/classic', function () {
before(async function () {
await browser.setWindowSize(1300, 800);
});
after(async function unloadMakelogs() {
await esArchiver.unload('test/functional/fixtures/es_archiver/logstash_functional');
});
loadTestFile(require.resolve('./_discover_fields_api'));
loadTestFile(require.resolve('./_doc_table'));
loadTestFile(require.resolve('./_doc_table_newline'));
loadTestFile(require.resolve('./_field_data'));
loadTestFile(require.resolve('./_field_data_with_fields_api'));
loadTestFile(require.resolve('./_classic_table_doc_navigation'));
loadTestFile(require.resolve('./_hide_announcements'));
loadTestFile(require.resolve('./_esql_grid'));
});
}

View file

@ -45,14 +45,13 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
});
it('shows a list of records of indices with date & date_nanos fields in the right order', async function () {
const isLegacy = await discover.useLegacyTable();
const rowData1 = await discover.getDocTableIndex(1);
expect(rowData1).to.contain('Jan 1, 2019 @ 12:10:30.124000000');
const rowData2 = await discover.getDocTableIndex(isLegacy ? 3 : 2);
const rowData2 = await discover.getDocTableIndex(2);
expect(rowData2).to.contain('Jan 1, 2019 @ 12:10:30.123498765');
const rowData3 = await discover.getDocTableIndex(isLegacy ? 5 : 3);
const rowData3 = await discover.getDocTableIndex(3);
expect(rowData3).to.contain('Jan 1, 2019 @ 12:10:30.123456789');
const rowData4 = await discover.getDocTableIndex(isLegacy ? 7 : 4);
const rowData4 = await discover.getDocTableIndex(4);
expect(rowData4).to.contain('Jan 1, 2019 @ 12:10:30.123000000');
});
});

View file

@ -34,7 +34,6 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
const dataViews = getService('dataViews');
const testSubjects = getService('testSubjects');
const security = getService('security');
const docTable = getService('docTable');
const defaultSettings = {
defaultIndex: 'logstash-*',
hideAnnouncements: true,
@ -228,7 +227,6 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
await timePicker.setDefaultAbsoluteRangeViaUiSettings();
await kibanaServer.uiSettings.update({
...defaultSettings,
'doc_table:legacy': false,
'doc_table:hideTimeColumn': hideTimeFieldColumnSetting,
});
await common.navigateToApp('discover');
@ -319,103 +317,6 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
});
});
});
// These tests are skipped as they take a lot of time to run. Temporary unskip them to validate current functionality if necessary.
describe.skip('legacy table', () => {
beforeEach(async () => {
await kibanaServer.uiSettings.update({
...defaultSettings,
'doc_table:hideTimeColumn': hideTimeFieldColumnSetting,
'doc_table:legacy': true,
});
await common.navigateToApp('discover');
await discover.waitUntilSearchingHasFinished();
});
it('should render initial columns correctly', async () => {
// no columns
await discover.loadSavedSearch(`${SEARCH_NO_COLUMNS}${savedSearchSuffix}`);
await discover.waitUntilSearchingHasFinished();
expect(await docTable.getHeaderFields()).to.eql(
hideTimeFieldColumnSetting ? ['Summary'] : ['@timestamp', 'Summary']
);
await discover.loadSavedSearch(`${SEARCH_NO_COLUMNS}${savedSearchSuffix}-`);
await discover.waitUntilSearchingHasFinished();
expect(await docTable.getHeaderFields()).to.eql(['Summary']);
await discover.loadSavedSearch(`${SEARCH_NO_COLUMNS}${savedSearchSuffix}ESQL`);
await discover.waitUntilSearchingHasFinished();
expect(await dataGrid.getHeaderFields()).to.eql(
hideTimeFieldColumnSetting ? ['Summary'] : ['@timestamp', 'Summary']
);
await discover.loadSavedSearch(`${SEARCH_NO_COLUMNS}${savedSearchSuffix}ESQLdrop`);
await discover.waitUntilSearchingHasFinished();
expect(await dataGrid.getHeaderFields()).to.eql(['Summary']);
// only @timestamp is selected
await discover.loadSavedSearch(`${SEARCH_WITH_ONLY_TIMESTAMP}${savedSearchSuffix}`);
await discover.waitUntilSearchingHasFinished();
expect(await docTable.getHeaderFields()).to.eql(
hideTimeFieldColumnSetting ? ['@timestamp'] : ['@timestamp', '@timestamp']
);
await discover.loadSavedSearch(`${SEARCH_WITH_ONLY_TIMESTAMP}${savedSearchSuffix}-`);
await discover.waitUntilSearchingHasFinished();
expect(await docTable.getHeaderFields()).to.eql(['@timestamp']);
await discover.loadSavedSearch(`${SEARCH_WITH_ONLY_TIMESTAMP}${savedSearchSuffix}ESQL`);
await discover.waitUntilSearchingHasFinished();
expect(await dataGrid.getHeaderFields()).to.eql(
hideTimeFieldColumnSetting ? ['@timestamp'] : ['@timestamp', 'Summary']
);
});
it('should render selected columns correctly', async () => {
// with selected columns
await discover.loadSavedSearch(`${SEARCH_WITH_SELECTED_COLUMNS}${savedSearchSuffix}`);
await discover.waitUntilSearchingHasFinished();
expect(await docTable.getHeaderFields()).to.eql(
hideTimeFieldColumnSetting
? ['bytes', 'extension']
: ['@timestamp', 'bytes', 'extension']
);
await discover.loadSavedSearch(`${SEARCH_WITH_SELECTED_COLUMNS}${savedSearchSuffix}-`);
await discover.waitUntilSearchingHasFinished();
expect(await docTable.getHeaderFields()).to.eql(['bytes', 'extension']);
await discover.loadSavedSearch(
`${SEARCH_WITH_SELECTED_COLUMNS}${savedSearchSuffix}ESQL`
);
await discover.waitUntilSearchingHasFinished();
expect(await dataGrid.getHeaderFields()).to.eql(['bytes', 'extension']);
// with selected columns and @timestamp
await discover.loadSavedSearch(
`${SEARCH_WITH_SELECTED_COLUMNS_AND_TIMESTAMP}${savedSearchSuffix}`
);
await discover.waitUntilSearchingHasFinished();
expect(await docTable.getHeaderFields()).to.eql(
hideTimeFieldColumnSetting
? ['bytes', 'extension', '@timestamp']
: ['@timestamp', 'bytes', 'extension', '@timestamp']
);
await discover.loadSavedSearch(
`${SEARCH_WITH_SELECTED_COLUMNS_AND_TIMESTAMP}${savedSearchSuffix}-`
);
await discover.waitUntilSearchingHasFinished();
expect(await docTable.getHeaderFields()).to.eql(['bytes', 'extension', '@timestamp']);
await discover.loadSavedSearch(
`${SEARCH_WITH_SELECTED_COLUMNS_AND_TIMESTAMP}${savedSearchSuffix}ESQL`
);
await discover.waitUntilSearchingHasFinished();
expect(await dataGrid.getHeaderFields()).to.eql(['bytes', 'extension', '@timestamp']);
});
});
});
});
});

Some files were not shown because too many files have changed in this diff Show more