diff --git a/packages/kbn-resizable-layout/src/panels_resizable.tsx b/packages/kbn-resizable-layout/src/panels_resizable.tsx index 968e5203047fe..300c9130b8100 100644 --- a/packages/kbn-resizable-layout/src/panels_resizable.tsx +++ b/packages/kbn-resizable-layout/src/panels_resizable.tsx @@ -64,6 +64,17 @@ export const PanelsResizable = ({ () => setResizeWithPortalsHackIsResizing(false), [] ); + const baseButtonCss = css` + background-color: transparent !important; + gap: 0 !important; + + &:not(:hover):not(:focus) { + &:before, + &:after { + width: 0; + } + } + `; const defaultButtonCss = css` z-index: 3; `; @@ -207,9 +218,10 @@ export const PanelsResizable = ({ { expect(component.find(EuiDataGrid).prop('toolbarVisibility')).toMatchInlineSnapshot(` Object { - "additionalControls": , + "additionalControls": null, "showColumnSelector": false, "showDisplaySelector": Object { "additionalDisplaySettings": { expect(component.find(EuiDataGrid).prop('toolbarVisibility')).toMatchInlineSnapshot(` Object { - "additionalControls": , + "additionalControls": null, "showColumnSelector": false, "showDisplaySelector": Object { "allowDensity": false, @@ -360,7 +360,7 @@ describe('UnifiedDataTable', () => { expect(component.find(EuiDataGrid).prop('toolbarVisibility')).toMatchInlineSnapshot(` Object { - "additionalControls": , + "additionalControls": null, "showColumnSelector": false, "showDisplaySelector": undefined, "showFullScreenSelector": true, @@ -511,6 +511,52 @@ describe('UnifiedDataTable', () => { }); }); + describe('renderCustomToolbar', () => { + it('should render a custom toolbar', async () => { + let toolbarParams: Record = {}; + let gridParams: Record = {}; + const renderCustomToolbarMock = jest.fn((props) => { + toolbarParams = props.toolbarProps; + gridParams = props.gridProps; + return
Custom layout
; + }); + const component = await getComponent({ + ...getProps(), + renderCustomToolbar: renderCustomToolbarMock, + }); + + // custom toolbar should be rendered + expect(findTestSubject(component, 'custom-toolbar').exists()).toBe(true); + + expect(renderCustomToolbarMock).toHaveBeenLastCalledWith( + expect.objectContaining({ + toolbarProps: expect.objectContaining({ + hasRoomForGridControls: true, + }), + gridProps: expect.objectContaining({ + additionalControls: null, + }), + }) + ); + + // the default eui controls should be available for custom rendering + expect(toolbarParams?.columnSortingControl).toBeTruthy(); + expect(toolbarParams?.keyboardShortcutsControl).toBeTruthy(); + expect(gridParams?.additionalControls).toBe(null); + + // additional controls become available after selecting a document + act(() => { + component + .find('[data-gridcell-column-id="select"] .euiCheckbox__input') + .first() + .simulate('change'); + }); + + expect(toolbarParams?.keyboardShortcutsControl).toBeTruthy(); + expect(gridParams?.additionalControls).toBeTruthy(); + }); + }); + describe('gridStyleOverride', () => { it('should render the grid with the default style if no gridStyleOverride is provided', async () => { const component = await getComponent({ diff --git a/packages/kbn-unified-data-table/src/components/data_table.tsx b/packages/kbn-unified-data-table/src/components/data_table.tsx index 22a625f479e3b..4ce88e52e6c8d 100644 --- a/packages/kbn-unified-data-table/src/components/data_table.tsx +++ b/packages/kbn-unified-data-table/src/components/data_table.tsx @@ -27,8 +27,11 @@ import { EuiDataGridControlColumn, EuiDataGridCustomBodyProps, EuiDataGridCellValueElementProps, + EuiDataGridCustomToolbarProps, + EuiDataGridToolBarVisibilityOptions, EuiDataGridToolBarVisibilityDisplaySelectorOptions, EuiDataGridStyle, + EuiDataGridProps, } from '@elastic/eui'; import type { DataView } from '@kbn/data-views-plugin/public'; import { @@ -66,6 +69,17 @@ import { import { UnifiedDataTableFooter } from './data_table_footer'; import { UnifiedDataTableAdditionalDisplaySettings } from './data_table_additional_display_settings'; +export interface UnifiedDataTableRenderCustomToolbarProps { + toolbarProps: EuiDataGridCustomToolbarProps; + gridProps: { + additionalControls?: EuiDataGridToolBarVisibilityOptions['additionalControls']; + }; +} + +export type UnifiedDataTableRenderCustomToolbar = ( + props: UnifiedDataTableRenderCustomToolbarProps +) => React.ReactElement; + export type SortOrder = [string, string]; export enum DataLoadingState { @@ -288,6 +302,12 @@ export interface UnifiedDataTableProps { * It receives #EuiDataGridCustomBodyProps as its only argument. */ renderCustomGridBody?: (args: EuiDataGridCustomBodyProps) => React.ReactNode; + /** + * Optional render for the grid toolbar + * @param toolbarProps + * @param gridProps + */ + renderCustomToolbar?: UnifiedDataTableRenderCustomToolbar; /** * An optional list of the EuiDataGridControlColumn type for setting trailing control columns standard for EuiDataGrid. */ @@ -360,6 +380,7 @@ export const UnifiedDataTable = ({ onFieldEdited, services, renderCustomGridBody, + renderCustomToolbar, trailingControlColumns, totalHits, onFetchMoreRecords, @@ -709,8 +730,12 @@ export const UnifiedDataTable = ({ : internalControlColumns; }, [canSetExpandedDoc, externalControlColumns, controlColumnIds]); - const additionalControls = useMemo( - () => ( + const additionalControls = useMemo(() => { + if (!externalAdditionalControls && !usedSelectedDocs.length) { + return null; + } + + return ( <> {usedSelectedDocs.length ? ( - ), - [usedSelectedDocs, isFilterActive, rows, externalAdditionalControls] + ); + }, [usedSelectedDocs, isFilterActive, rows, externalAdditionalControls]); + + const renderCustomToolbarFn: EuiDataGridProps['renderCustomToolbar'] | undefined = useMemo( + () => + renderCustomToolbar + ? (toolbarProps) => + renderCustomToolbar({ + toolbarProps, + gridProps: { + additionalControls, + }, + }) + : undefined, + [renderCustomToolbar, additionalControls] ); const showDisplaySelector = useMemo(() => { @@ -852,10 +890,12 @@ export const UnifiedDataTable = ({ inMemory={inMemory} gridStyle={gridStyleOverride ?? GRID_STYLE} renderCustomGridBody={renderCustomGridBody} + renderCustomToolbar={renderCustomToolbarFn} trailingControlColumns={trailingControlColumns} /> {loadingState !== DataLoadingState.loading && + !usedSelectedDocs.length && // hide footer when showing selected documents isPaginationEnabled && ( // we hide the footer for Surrounding Documents page - + + + + + + {selectedDocs.length} + + } > diff --git a/src/plugins/discover/public/application/context/context_app_content.test.tsx b/src/plugins/discover/public/application/context/context_app_content.test.tsx index f6809d63c035d..f6c87772ec913 100644 --- a/src/plugins/discover/public/application/context/context_app_content.test.tsx +++ b/src/plugins/discover/public/application/context/context_app_content.test.tsx @@ -104,5 +104,6 @@ describe('ContextAppContent test', () => { it('should render discover grid correctly', async () => { const component = await mountComponent({ isLegacy: false }); expect(component.find(UnifiedDataTable).length).toBe(1); + expect(findTestSubject(component, 'dscGridToolbar').exists()).toBe(true); }); }); diff --git a/src/plugins/discover/public/application/context/context_app_content.tsx b/src/plugins/discover/public/application/context/context_app_content.tsx index 81ca3e6f81b66..5b10995fad145 100644 --- a/src/plugins/discover/public/application/context/context_app_content.tsx +++ b/src/plugins/discover/public/application/context/context_app_content.tsx @@ -26,8 +26,9 @@ import { ROW_HEIGHT_OPTION, SHOW_MULTIFIELDS, } from '@kbn/discover-utils'; -import { DataLoadingState, UnifiedDataTable } from '@kbn/unified-data-table'; +import { DataLoadingState } from '@kbn/unified-data-table'; import { DocViewFilterFn } from '@kbn/unified-doc-viewer/types'; +import { DiscoverGrid } from '../../components/discover_grid'; import { getDefaultRowsPerPage } from '../../../common/constants'; import { LoadingStatus } from './services/context_query_state'; import { ActionBar } from './components/action_bar/action_bar'; @@ -37,7 +38,6 @@ 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 { DISCOVER_TOUR_STEP_ANCHOR_IDS } from '../../components/discover_tour'; export interface ContextAppContentProps { columns: string[]; @@ -66,7 +66,7 @@ export function clamp(value: number) { return Math.max(Math.min(MAX_CONTEXT_SIZE, value), MIN_CONTEXT_SIZE); } -const DiscoverGridMemoized = React.memo(UnifiedDataTable); +const DiscoverGridMemoized = React.memo(DiscoverGrid); const DocTableContextMemoized = React.memo(DocTableContext); const ActionBarMemoized = React.memo(ActionBar); @@ -191,7 +191,6 @@ export function ContextAppContent({ diff --git a/src/plugins/discover/public/application/main/components/document_explorer_callout/document_explorer_callout.tsx b/src/plugins/discover/public/application/main/components/document_explorer_callout/document_explorer_callout.tsx index a18389806f8f9..9822f0081fd30 100644 --- a/src/plugins/discover/public/application/main/components/document_explorer_callout/document_explorer_callout.tsx +++ b/src/plugins/discover/public/application/main/components/document_explorer_callout/document_explorer_callout.tsx @@ -60,6 +60,7 @@ export const DocumentExplorerCallout = () => { return ( } iconType="search" diff --git a/src/plugins/discover/public/application/main/components/document_explorer_callout/document_explorer_update_callout.tsx b/src/plugins/discover/public/application/main/components/document_explorer_callout/document_explorer_update_callout.tsx index 617790c28907b..82ed197d2977b 100644 --- a/src/plugins/discover/public/application/main/components/document_explorer_callout/document_explorer_update_callout.tsx +++ b/src/plugins/discover/public/application/main/components/document_explorer_callout/document_explorer_update_callout.tsx @@ -51,6 +51,7 @@ export const DocumentExplorerUpdateCallout = () => { return ( } iconType="tableDensityNormal" diff --git a/src/plugins/discover/public/application/main/components/layout/discover_documents.test.tsx b/src/plugins/discover/public/application/main/components/layout/discover_documents.test.tsx index fdbd153122cd1..187c2b4719249 100644 --- a/src/plugins/discover/public/application/main/components/layout/discover_documents.test.tsx +++ b/src/plugins/discover/public/application/main/components/layout/discover_documents.test.tsx @@ -9,6 +9,7 @@ import React from 'react'; import { act } from 'react-dom/test-utils'; import { BehaviorSubject } from 'rxjs'; +import { findTestSubject } from '@elastic/eui/lib/test'; import { mountWithIntl } from '@kbn/test-jest-helpers'; import { setHeaderActionMenuMounter } from '../../../../kibana_services'; import { DataDocuments$ } from '../../services/discover_data_state_container'; @@ -40,6 +41,7 @@ async function mountComponent(fetchStatus: FetchStatus, hits: EsHitRecord[]) { stateContainer.dataState.data$.documents$ = documents$; const props = { + viewModeToggle:
test
, dataView: dataViewMock, onAddFilter: jest.fn(), stateContainer, @@ -76,6 +78,9 @@ describe('Discover documents layout', () => { const component = await mountComponent(FetchStatus.COMPLETE, esHitsMock); expect(component.find('.dscDocuments__loading').exists()).toBeFalsy(); expect(component.find('.dscTable').exists()).toBeTruthy(); + expect(findTestSubject(component, 'dscGridToolbar').exists()).toBe(true); + expect(findTestSubject(component, 'dscGridToolbarBottom').exists()).toBe(true); + expect(findTestSubject(component, 'viewModeToggle').exists()).toBe(true); }); test('should set rounded width to state on resize column', () => { diff --git a/src/plugins/discover/public/application/main/components/layout/discover_documents.tsx b/src/plugins/discover/public/application/main/components/layout/discover_documents.tsx index 60367b83d02ed..774d47d577a6d 100644 --- a/src/plugins/discover/public/application/main/components/layout/discover_documents.tsx +++ b/src/plugins/discover/public/application/main/components/layout/discover_documents.tsx @@ -23,7 +23,6 @@ import type { DataTableRecord } from '@kbn/discover-utils/types'; import { SearchResponseWarnings } from '@kbn/search-response-warnings'; import { DataLoadingState, - UnifiedDataTable, useColumns, type DataTableColumnTypes, getTextBasedColumnTypes, @@ -38,7 +37,10 @@ import { SHOW_MULTIFIELDS, SORT_DEFAULT_ORDER_SETTING, } from '@kbn/discover-utils'; +import { i18n } from '@kbn/i18n'; +import useObservable from 'react-use/lib/useObservable'; import type { DocViewFilterFn } from '@kbn/unified-doc-viewer/types'; +import { DiscoverGrid } from '../../../../components/discover_grid'; import { getDefaultRowsPerPage } from '../../../../../common/constants'; import { useInternalStateSelector } from '../../services/discover_internal_state_container'; import { useAppStateSelector } from '../../services/discover_app_state_container'; @@ -60,8 +62,11 @@ import { getAllowedSampleSize, } from '../../../../utils/get_allowed_sample_size'; import { DiscoverGridFlyout } from '../../../../components/discover_grid_flyout'; +import { getRenderCustomToolbarWithElements } from '../../../../components/discover_grid/render_custom_toolbar'; import { useSavedSearchInitial } from '../../services/discover_state_provider'; import { useFetchMoreRecords } from './use_fetch_more_records'; +import { ErrorCallout } from '../../../../components/common/error_callout'; +import { SelectedVSAvailableCallout } from './selected_vs_available_callout'; const containerStyles = css` position: relative; @@ -74,7 +79,7 @@ const progressStyle = css` const TOUR_STEPS = { expandButton: DISCOVER_TOUR_STEP_ANCHOR_IDS.expandDocument }; const DocTableInfiniteMemoized = React.memo(DocTableInfinite); -const DiscoverGridMemoized = React.memo(UnifiedDataTable); +const DiscoverGridMemoized = React.memo(DiscoverGrid); // export needs for testing export const onResize = ( @@ -92,11 +97,13 @@ export const onResize = ( }; function DiscoverDocumentsComponent({ + viewModeToggle, dataView, onAddFilter, stateContainer, onFieldEdited, }: { + viewModeToggle: React.ReactElement | undefined; dataView: DataView; onAddFilter?: DocViewFilterFn; stateContainer: DiscoverStateContainer; @@ -249,6 +256,86 @@ function DiscoverDocumentsComponent({ [dataView, onAddColumn, onAddFilter, onRemoveColumn, query, savedSearch.id, setExpandedDoc] ); + const dataState = useDataState(stateContainer.dataState.data$.main$); + const documents = useObservable(stateContainer.dataState.data$.documents$); + + const callouts = useMemo( + () => ( + <> + {dataState.error && ( + + )} + + {!!documentState.interceptedWarnings?.length && ( + + )} + + ), + [ + dataState.error, + isTextBasedQuery, + currentColumns, + documents?.textBasedQueryColumns, + documentState.interceptedWarnings, + ] + ); + + const gridAnnouncementCallout = useMemo(() => { + if (hideAnnouncements || isLegacy) { + return null; + } + + return !isTextBasedQuery ? ( + + + + ) : null; + }, [hideAnnouncements, isLegacy, isTextBasedQuery]); + + const loadingIndicator = useMemo( + () => + isDataLoading ? ( + + ) : null, + [isDataLoading] + ); + + const renderCustomToolbar = useMemo( + () => + getRenderCustomToolbarWithElements({ + leftSide: viewModeToggle, + bottomSection: ( + <> + {callouts} + {gridAnnouncementCallout} + {loadingIndicator} + + ), + }), + [viewModeToggle, callouts, gridAnnouncementCallout, loadingIndicator] + ); + if (isDataViewLoading || (isEmptyDataResult && isDataLoading)) { return (
@@ -262,111 +349,103 @@ function DiscoverDocumentsComponent({ } return ( - - -

- -

-
- {!!documentState.interceptedWarnings?.length && ( - - )} - {isLegacy && rows && rows.length > 0 && ( - <> - {!hideAnnouncements && } - - - )} - {!isLegacy && ( + <> + {isLegacy && ( <> - {!hideAnnouncements && !isTextBasedQuery && ( - - - - )} -
- - - -
+ {viewModeToggle} + {callouts} )} - {isDataLoading && ( - - )} -
+ + +

+ +

+
+ {isLegacy && ( + <> + {rows && rows.length > 0 && ( + <> + {!hideAnnouncements && } + + + )} + {loadingIndicator} + + )} + {!isLegacy && ( + <> +
+ + + +
+ + )} +
+ ); } diff --git a/src/plugins/discover/public/application/main/components/layout/discover_layout.scss b/src/plugins/discover/public/application/main/components/layout/discover_layout.scss index d3978fe1c1d82..55972b4f7f629 100644 --- a/src/plugins/discover/public/application/main/components/layout/discover_layout.scss +++ b/src/plugins/discover/public/application/main/components/layout/discover_layout.scss @@ -32,16 +32,6 @@ discover-app { height: 100%; } -.dscSidebarResizeButton { - background-color: transparent !important; - - &:not(:hover):not(:focus) { - &:before, &:after { - width: 0; - } - } -} - .dscPageContent__wrapper { overflow: hidden; // Ensures horizontal scroll of table display: flex; @@ -53,10 +43,6 @@ discover-app { position: relative; overflow: hidden; height: 100%; - - .euiDataGrid__controls { - border-top: none; - } } .dscPageContent--centered { diff --git a/src/plugins/discover/public/application/main/components/layout/discover_main_content.test.tsx b/src/plugins/discover/public/application/main/components/layout/discover_main_content.test.tsx index 2901b49de1586..b12208cc76299 100644 --- a/src/plugins/discover/public/application/main/components/layout/discover_main_content.test.tsx +++ b/src/plugins/discover/public/application/main/components/layout/discover_main_content.test.tsx @@ -125,12 +125,17 @@ describe('Discover main content component', () => { describe('DocumentViewModeToggle', () => { it('should show DocumentViewModeToggle when isPlainRecord is false', async () => { const component = await mountComponent(); - expect(component.find(DocumentViewModeToggle).exists()).toBe(true); + expect(component.find(DiscoverDocuments).prop('viewModeToggle')).toBeDefined(); }); it('should not show DocumentViewModeToggle when isPlainRecord is true', async () => { const component = await mountComponent({ isPlainRecord: true }); - expect(component.find(DocumentViewModeToggle).exists()).toBe(false); + expect(component.find(DiscoverDocuments).prop('viewModeToggle')).toBeUndefined(); + }); + + it('should show DocumentViewModeToggle for Field Statistics', async () => { + const component = await mountComponent({ viewMode: VIEW_MODE.AGGREGATED_LEVEL }); + expect(component.find(DocumentViewModeToggle).exists()).toBe(true); }); }); diff --git a/src/plugins/discover/public/application/main/components/layout/discover_main_content.tsx b/src/plugins/discover/public/application/main/components/layout/discover_main_content.tsx index e241a52b1d259..8b6ff5880d3dc 100644 --- a/src/plugins/discover/public/application/main/components/layout/discover_main_content.tsx +++ b/src/plugins/discover/public/application/main/components/layout/discover_main_content.tsx @@ -6,10 +6,9 @@ * Side Public License, v 1. */ -import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; +import { EuiFlexGroup, EuiFlexItem, EuiHorizontalRule } from '@elastic/eui'; import { DragDrop, type DropType, DropOverlayWrapper } from '@kbn/dom-drag-drop'; -import useObservable from 'react-use/lib/useObservable'; -import React, { useCallback } from 'react'; +import React, { useCallback, useMemo } from 'react'; import { DataView } from '@kbn/data-views-plugin/common'; import { METRIC_TYPE } from '@kbn/analytics'; import { i18n } from '@kbn/i18n'; @@ -21,9 +20,7 @@ import { DiscoverStateContainer } from '../../services/discover_state'; import { FieldStatisticsTab } from '../field_stats_table'; import { DiscoverDocuments } from './discover_documents'; import { DOCUMENTS_VIEW_CLICK, FIELD_STATISTICS_VIEW_CLICK } from '../field_stats_table/constants'; -import { ErrorCallout } from '../../../../components/common/error_callout'; -import { useDataState } from '../../hooks/use_data_state'; -import { SelectedVSAvailableCallout } from './selected_vs_available_callout'; +import { useAppStateSelector } from '../../services/discover_app_state_container'; const DROP_PROPS = { value: { @@ -76,10 +73,16 @@ export const DiscoverMainContent = ({ [trackUiMetric, stateContainer] ); - const dataState = useDataState(stateContainer.dataState.data$.main$); - const documents = useObservable(stateContainer.dataState.data$.documents$); const isDropAllowed = Boolean(onDropFieldToTable); + const viewModeToggle = useMemo(() => { + return !isPlainRecord ? ( + + ) : undefined; + }, [viewMode, setDiscoverViewMode, isPlainRecord]); + + const showChart = useAppStateSelector((state) => !state.hideChart); + return ( - - {!isPlainRecord && ( - - )} - - {dataState.error && ( - - )} - - + {showChart && } {viewMode === VIEW_MODE.DOCUMENT_LEVEL ? ( ) : ( - + <> + {viewModeToggle} + + )} diff --git a/src/plugins/discover/public/application/main/components/layout/discover_resizable_layout.tsx b/src/plugins/discover/public/application/main/components/layout/discover_resizable_layout.tsx index 32491a38d86fd..e0859617f0057 100644 --- a/src/plugins/discover/public/application/main/components/layout/discover_resizable_layout.tsx +++ b/src/plugins/discover/public/application/main/components/layout/discover_resizable_layout.tsx @@ -71,7 +71,6 @@ export const DiscoverResizableLayout = ({ minFlexPanelSize={minMainPanelWidth} fixedPanel={} flexPanel={} - resizeButtonClassName="dscSidebarResizeButton" data-test-subj="discoverLayout" onFixedPanelSizeChange={setSidebarWidth} /> diff --git a/src/plugins/discover/public/components/discover_grid/__snapshots__/render_custom_toolbar.test.tsx.snap b/src/plugins/discover/public/components/discover_grid/__snapshots__/render_custom_toolbar.test.tsx.snap new file mode 100644 index 0000000000000..9f2af58f1dccb --- /dev/null +++ b/src/plugins/discover/public/components/discover_grid/__snapshots__/render_custom_toolbar.test.tsx.snap @@ -0,0 +1,237 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`renderCustomToolbar should render correctly for smaller screens 1`] = ` + + + + + + + + +
+
+ keyboard +
+
+ display +
+
+ fullScreen +
+
+
+
+
+
+
+`; + +exports[`renderCustomToolbar should render correctly with an element 1`] = ` + + + +
+ left +
+
+ + + + +
+ additional +
+
+ +
+ column +
+
+ +
+ columnSorting +
+
+
+ +
+
+ keyboard +
+
+ display +
+
+ fullScreen +
+
+
+
+
+
+
+
+ bottom +
+
+
+`; + +exports[`renderCustomToolbar should render successfully 1`] = ` + + + + + + +
+ column +
+
+ +
+ columnSorting +
+
+ +
+ additional +
+
+
+
+
+ + + +
+
+ keyboard +
+
+ display +
+
+ fullScreen +
+
+
+
+
+
+
+`; diff --git a/src/plugins/discover/public/components/discover_grid/discover_grid.tsx b/src/plugins/discover/public/components/discover_grid/discover_grid.tsx new file mode 100644 index 0000000000000..fe2586e32f895 --- /dev/null +++ b/src/plugins/discover/public/components/discover_grid/discover_grid.tsx @@ -0,0 +1,20 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import React from 'react'; +import { UnifiedDataTable, type UnifiedDataTableProps } from '@kbn/unified-data-table'; +import { renderCustomToolbar } from './render_custom_toolbar'; + +/** + * Customized version of the UnifiedDataTable + * @param props + * @constructor + */ +export const DiscoverGrid: React.FC = (props) => { + return ; +}; diff --git a/src/plugins/discover/public/components/discover_grid/index.ts b/src/plugins/discover/public/components/discover_grid/index.ts new file mode 100644 index 0000000000000..b9057e101ee9e --- /dev/null +++ b/src/plugins/discover/public/components/discover_grid/index.ts @@ -0,0 +1,9 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +export { DiscoverGrid } from './discover_grid'; diff --git a/src/plugins/discover/public/components/discover_grid/render_custom_toolbar.scss b/src/plugins/discover/public/components/discover_grid/render_custom_toolbar.scss new file mode 100644 index 0000000000000..e24f7f8c6fcb1 --- /dev/null +++ b/src/plugins/discover/public/components/discover_grid/render_custom_toolbar.scss @@ -0,0 +1,61 @@ +.dscGridToolbar { + padding: $euiSizeS $euiSizeS $euiSizeXS; +} + +.dscGridToolbarControlButton .euiDataGrid__controlBtn { + block-size: $euiSizeXL; + border: $euiBorderThin; + border-radius: $euiBorderRadiusSmall; + + // making the icons larger than the default size + & svg { + inline-size: $euiSize; + block-size: $euiSize; + } + + // cancel default background and font changes + &.euiDataGrid__controlBtn--active { + font-weight: $euiFontWeightMedium; + } + &:active, &:focus { + background: transparent; + } + + // add toolbar control animation + transition: transform $euiAnimSpeedNormal ease-in-out; + &:hover { + transform: translateY(-1px); + } + &:active { + transform: translateY(0); + } +} + +.dscGridToolbarControlGroup { + box-shadow: inset 0 0 0 $euiBorderWidthThin $euiBorderColor; + border-radius: $euiBorderRadiusSmall; + display: inline-flex; + align-items: stretch; + flex-direction: row; +} + +.dscGridToolbarControlIconButton .euiButtonIcon { + inline-size: $euiSizeXL; + block-size: $euiSizeXL; + + // cancel default behaviour + &:hover, &:active, &:focus { + background: transparent; + animation: none !important; + transform: none !important; + } + + .dscGridToolbarControlIconButton + & { + border-inline-start: $euiBorderThin; + border-radius: 0; + } +} + +.dscGridToolbarBottom { + position: relative; // for placing a loading indicator correctly +} diff --git a/src/plugins/discover/public/components/discover_grid/render_custom_toolbar.test.tsx b/src/plugins/discover/public/components/discover_grid/render_custom_toolbar.test.tsx new file mode 100644 index 0000000000000..3b8a4bb9457f9 --- /dev/null +++ b/src/plugins/discover/public/components/discover_grid/render_custom_toolbar.test.tsx @@ -0,0 +1,63 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import React from 'react'; +import { renderCustomToolbar, getRenderCustomToolbarWithElements } from './render_custom_toolbar'; + +describe('renderCustomToolbar', () => { + it('should render successfully', () => { + expect( + renderCustomToolbar({ + toolbarProps: { + hasRoomForGridControls: true, + columnControl: 'column', + columnSortingControl: 'columnSorting', + displayControl: 'display', + fullScreenControl: 'fullScreen', + keyboardShortcutsControl: 'keyboard', + }, + gridProps: { additionalControls: 'additional' }, + }) + ).toMatchSnapshot(); + }); + + it('should render correctly for smaller screens', () => { + expect( + renderCustomToolbar({ + toolbarProps: { + hasRoomForGridControls: false, + columnControl: 'column', + columnSortingControl: 'columnSorting', + displayControl: 'display', + fullScreenControl: 'fullScreen', + keyboardShortcutsControl: 'keyboard', + }, + gridProps: { additionalControls: 'additional' }, + }) + ).toMatchSnapshot(); + }); + + it('should render correctly with an element', () => { + expect( + getRenderCustomToolbarWithElements({ + leftSide:
left
, + bottomSection:
bottom
, + })({ + toolbarProps: { + hasRoomForGridControls: true, + columnControl: 'column', + columnSortingControl: 'columnSorting', + displayControl: 'display', + fullScreenControl: 'fullScreen', + keyboardShortcutsControl: 'keyboard', + }, + gridProps: { additionalControls: 'additional' }, + }) + ).toMatchSnapshot(); + }); +}); diff --git a/src/plugins/discover/public/components/discover_grid/render_custom_toolbar.tsx b/src/plugins/discover/public/components/discover_grid/render_custom_toolbar.tsx new file mode 100644 index 0000000000000..2baeb4c287a16 --- /dev/null +++ b/src/plugins/discover/public/components/discover_grid/render_custom_toolbar.tsx @@ -0,0 +1,129 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import React from 'react'; +import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; +import type { + UnifiedDataTableRenderCustomToolbarProps, + UnifiedDataTableRenderCustomToolbar, +} from '@kbn/unified-data-table'; +import './render_custom_toolbar.scss'; + +interface RenderCustomToolbarProps extends UnifiedDataTableRenderCustomToolbarProps { + leftSide?: React.ReactElement; + bottomSection?: React.ReactElement; +} + +export const renderCustomToolbar = (props: RenderCustomToolbarProps): React.ReactElement => { + const { + leftSide, + bottomSection, + toolbarProps: { + hasRoomForGridControls, + columnControl, + columnSortingControl, + fullScreenControl, + keyboardShortcutsControl, + displayControl, + }, + gridProps: { additionalControls }, + } = props; + + const buttons = hasRoomForGridControls ? ( + <> + {leftSide && additionalControls && ( + +
{additionalControls}
+
+ )} + {columnControl && ( + +
{columnControl}
+
+ )} + {columnSortingControl && ( + +
{columnSortingControl}
+
+ )} + {!leftSide && additionalControls && ( + +
{additionalControls}
+
+ )} + + ) : null; + + return ( + <> + + + {leftSide || ( + + {buttons} + + )} + + + + {Boolean(leftSide) && buttons} + {(keyboardShortcutsControl || displayControl || fullScreenControl) && ( + +
+ {keyboardShortcutsControl && ( +
+ {keyboardShortcutsControl} +
+ )} + {displayControl && ( +
{displayControl}
+ )} + {fullScreenControl && ( +
{fullScreenControl}
+ )} +
+
+ )} +
+
+
+ {bottomSection ? ( +
+ {bottomSection} +
+ ) : null} + + ); +}; + +/** + * Render custom element on the left side and all controls to the right + */ +export const getRenderCustomToolbarWithElements = ({ + leftSide, + bottomSection, +}: { + leftSide?: React.ReactElement; + bottomSection?: React.ReactElement; +}): UnifiedDataTableRenderCustomToolbar => { + const reservedSpace = <>; + return (props) => + renderCustomToolbar({ + ...props, + leftSide: leftSide || reservedSpace, + bottomSection, + }); +}; diff --git a/src/plugins/discover/public/components/doc_table/_doc_table.scss b/src/plugins/discover/public/components/doc_table/_doc_table.scss index 4553cdc05fdad..8a9b629a9694b 100644 --- a/src/plugins/discover/public/components/doc_table/_doc_table.scss +++ b/src/plugins/discover/public/components/doc_table/_doc_table.scss @@ -4,6 +4,7 @@ // stylelint-disable selector-no-qualifying-type .kbnDocTableWrapper { @include euiScrollBar; + @include euiOverflowShadow; overflow: auto; display: flex; flex: 1 1 100%; diff --git a/src/plugins/discover/public/components/view_mode_toggle/view_mode_toggle.tsx b/src/plugins/discover/public/components/view_mode_toggle/view_mode_toggle.tsx index 63fcbcc40db37..79c9213e76395 100644 --- a/src/plugins/discover/public/components/view_mode_toggle/view_mode_toggle.tsx +++ b/src/plugins/discover/public/components/view_mode_toggle/view_mode_toggle.tsx @@ -6,11 +6,11 @@ * Side Public License, v 1. */ -import React from 'react'; +import React, { useMemo } from 'react'; import { EuiTab, EuiTabs, useEuiTheme } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n-react'; import { css } from '@emotion/react'; -import { SHOW_FIELD_STATISTICS } from '@kbn/discover-utils'; +import { DOC_TABLE_LEGACY, SHOW_FIELD_STATISTICS } from '@kbn/discover-utils'; import { VIEW_MODE } from '../../../common/constants'; import { useDiscoverServices } from '../../hooks/use_discover_services'; @@ -23,9 +23,16 @@ export const DocumentViewModeToggle = ({ }) => { const { euiTheme } = useEuiTheme(); const { uiSettings } = useDiscoverServices(); + const isLegacy = useMemo(() => uiSettings.get(DOC_TABLE_LEGACY), [uiSettings]); + const includesNormalTabsStyle = viewMode === VIEW_MODE.AGGREGATED_LEVEL || isLegacy; + const tabsPadding = includesNormalTabsStyle ? euiTheme.size.s : 0; const tabsCss = css` - padding: 0 ${euiTheme.size.s}; + padding: ${tabsPadding} ${tabsPadding} 0 ${tabsPadding}; + + .euiTab__content { + line-height: ${euiTheme.size.xl}; + } `; const showViewModeToggle = uiSettings.get(SHOW_FIELD_STATISTICS) ?? false; @@ -35,11 +42,10 @@ export const DocumentViewModeToggle = ({ } return ( - + setDiscoverViewMode(VIEW_MODE.DOCUMENT_LEVEL)} - className="dscViewModeToggle__tab" data-test-subj="dscViewModeDocumentButton" > @@ -47,7 +53,6 @@ export const DocumentViewModeToggle = ({ setDiscoverViewMode(VIEW_MODE.AGGREGATED_LEVEL)} - className="dscViewModeToggle__tab" data-test-subj="dscViewModeFieldStatsButton" > data-test-subj={dataTestSubj} > {isLoading && } - - - {Boolean(prepend) && {prepend}} - {!!totalHitCount && ( - - - - )} - - + {Boolean(prepend || totalHitCount) && ( + + + {Boolean(prepend) && {prepend}} + + {!!totalHitCount && ( + + + + )} + + + )} {children} diff --git a/src/plugins/discover/public/embeddable/saved_search_embeddable_component.tsx b/src/plugins/discover/public/embeddable/saved_search_embeddable_component.tsx index 43085e3c0902e..b9bd2112f6ee6 100644 --- a/src/plugins/discover/public/embeddable/saved_search_embeddable_component.tsx +++ b/src/plugins/discover/public/embeddable/saved_search_embeddable_component.tsx @@ -47,7 +47,6 @@ export function SavedSearchEmbeddableComponent({ sampleSizeState={fetchedSampleSize} loadingState={searchProps.isLoading ? DataLoadingState.loading : DataLoadingState.loaded} showFullScreenButton={false} - showColumnTokens query={query} className="unifiedDataTable" /> diff --git a/src/plugins/discover/public/embeddable/saved_search_grid.tsx b/src/plugins/discover/public/embeddable/saved_search_grid.tsx index 1cadf5414f8a3..9b0653bd63352 100644 --- a/src/plugins/discover/public/embeddable/saved_search_grid.tsx +++ b/src/plugins/discover/public/embeddable/saved_search_grid.tsx @@ -5,21 +5,22 @@ * in compliance with, at your election, the Elastic License 2.0 or the Server * Side Public License, v 1. */ -import React, { useCallback, useState } from 'react'; +import React, { useCallback, useMemo, useState } from 'react'; import type { DataTableRecord } from '@kbn/discover-utils/types'; import { AggregateQuery, Query } from '@kbn/es-query'; import type { SearchResponseInterceptedWarning } from '@kbn/search-response-warnings'; +import { MAX_DOC_FIELDS_DISPLAYED, ROW_HEIGHT_OPTION, SHOW_MULTIFIELDS } from '@kbn/discover-utils'; import { - DataLoadingState as DiscoverGridLoadingState, - UnifiedDataTable, + type UnifiedDataTableProps, type DataTableColumnTypes, + DataLoadingState as DiscoverGridLoadingState, } from '@kbn/unified-data-table'; -import type { UnifiedDataTableProps } from '@kbn/unified-data-table'; +import { DiscoverGrid } from '../components/discover_grid'; import './saved_search_grid.scss'; -import { MAX_DOC_FIELDS_DISPLAYED, ROW_HEIGHT_OPTION, SHOW_MULTIFIELDS } from '@kbn/discover-utils'; import { DiscoverGridFlyout } from '../components/discover_grid_flyout'; import { SavedSearchEmbeddableBase } from './saved_search_embeddable_base'; -import { DISCOVER_TOUR_STEP_ANCHOR_IDS } from '../components/discover_tour'; +import { getRenderCustomToolbarWithElements } from '../components/discover_grid/render_custom_toolbar'; +import { TotalDocuments } from '../application/main/components/total_documents/total_documents'; export interface DiscoverGridEmbeddableProps extends Omit { @@ -32,7 +33,7 @@ export interface DiscoverGridEmbeddableProps savedSearchId?: string; } -export const DiscoverGridMemoized = React.memo(UnifiedDataTable); +export const DiscoverGridMemoized = React.memo(DiscoverGrid); export function DiscoverGridEmbeddable(props: DiscoverGridEmbeddableProps) { const { interceptedWarnings, ...gridProps } = props; @@ -71,9 +72,20 @@ export function DiscoverGridEmbeddable(props: DiscoverGridEmbeddableProps) { ] ); + const renderCustomToolbar = useMemo( + () => + getRenderCustomToolbarWithElements({ + leftSide: + typeof props.totalHitCount === 'number' ? ( + + ) : undefined, + }), + [props.totalHitCount] + ); + return ( ); diff --git a/test/functional/apps/discover/embeddable/_saved_search_embeddable.ts b/test/functional/apps/discover/embeddable/_saved_search_embeddable.ts index 2fb99d9ebb43f..03f40c5b6ebcf 100644 --- a/test/functional/apps/discover/embeddable/_saved_search_embeddable.ts +++ b/test/functional/apps/discover/embeddable/_saved_search_embeddable.ts @@ -145,5 +145,10 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await addSearchEmbeddableToDashboard(); await testSubjects.missingOrFail('dataGridFullScreenButton'); }); + + it('should show the the grid toolbar', async () => { + await addSearchEmbeddableToDashboard(); + await testSubjects.existOrFail('dscGridToolbar'); + }); }); } diff --git a/test/functional/apps/discover/group2/_data_grid.ts b/test/functional/apps/discover/group2/_data_grid.ts index 58052ce6665ca..d869044613873 100644 --- a/test/functional/apps/discover/group2/_data_grid.ts +++ b/test/functional/apps/discover/group2/_data_grid.ts @@ -46,5 +46,9 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await PageObjects.unifiedFieldList.clickFieldListItemRemove('agent'); expect(await getTitles()).to.be('@timestamp Document'); }); + + it('should show the the grid toolbar', async () => { + await testSubjects.existOrFail('dscGridToolbar'); + }); }); } diff --git a/test/functional/apps/discover/group2/_data_grid_context.ts b/test/functional/apps/discover/group2/_data_grid_context.ts index ae5030f226b82..e8e218625687b 100644 --- a/test/functional/apps/discover/group2/_data_grid_context.ts +++ b/test/functional/apps/discover/group2/_data_grid_context.ts @@ -97,6 +97,10 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { expect(disabledFilterCounter).to.be(TEST_FILTER_COLUMN_NAMES.length); }); + it('should show the the grid toolbar', async () => { + await testSubjects.existOrFail('dscGridToolbar'); + }); + it('navigates to context view from embeddable', async () => { await PageObjects.common.navigateToApp('discover'); await PageObjects.discover.saveSearch('my search'); diff --git a/test/functional/apps/discover/group3/_view_mode_toggle.ts b/test/functional/apps/discover/group3/_view_mode_toggle.ts new file mode 100644 index 0000000000000..c47aad66c9a01 --- /dev/null +++ b/test/functional/apps/discover/group3/_view_mode_toggle.ts @@ -0,0 +1,130 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import expect from '@kbn/expect'; +import { FtrProviderContext } from '../ftr_provider_context'; + +export default function ({ getService, getPageObjects }: FtrProviderContext) { + const PageObjects = getPageObjects([ + 'common', + 'discover', + 'timePicker', + 'dashboard', + 'unifiedFieldList', + 'header', + ]); + const esArchiver = getService('esArchiver'); + const retry = getService('retry'); + const testSubjects = getService('testSubjects'); + const queryBar = getService('queryBar'); + const kibanaServer = getService('kibanaServer'); + const security = getService('security'); + const defaultSettings = { + defaultIndex: 'logstash-*', + }; + + describe('discover view mode toggle', function () { + before(async () => { + await security.testUser.setRoles(['kibana_admin', 'test_logstash_reader']); + await esArchiver.loadIfNeeded('test/functional/fixtures/es_archiver/logstash_functional'); + await kibanaServer.importExport.load('test/functional/fixtures/kbn_archiver/discover'); + }); + + after(async () => { + await kibanaServer.importExport.unload('test/functional/fixtures/kbn_archiver/discover'); + await esArchiver.unload('test/functional/fixtures/es_archiver/logstash_functional'); + await kibanaServer.savedObjects.cleanStandardList(); + }); + + [true, false].forEach((useLegacyTable) => { + describe(`isLegacy: ${useLegacyTable}`, function () { + before(async function () { + await PageObjects.timePicker.setDefaultAbsoluteRangeViaUiSettings(); + await kibanaServer.uiSettings.update({ + ...defaultSettings, + 'doc_table:legacy': useLegacyTable, + }); + await PageObjects.common.navigateToApp('discover'); + await PageObjects.discover.waitUntilSearchingHasFinished(); + }); + + after(async () => { + await kibanaServer.uiSettings.replace({}); + }); + + it('should show Documents tab', async () => { + await testSubjects.existOrFail('dscViewModeToggle'); + + if (!useLegacyTable) { + await testSubjects.existOrFail('dscGridToolbar'); + } + + const documentsTab = await testSubjects.find('dscViewModeDocumentButton'); + expect(await documentsTab.getAttribute('aria-selected')).to.be('true'); + }); + + it('should show Document Explorer info callout', async () => { + await testSubjects.existOrFail( + useLegacyTable ? 'dscDocumentExplorerLegacyCallout' : 'dscDocumentExplorerTourCallout' + ); + }); + + it('should show an error callout', async () => { + await queryBar.setQuery('@message::'); // invalid + await queryBar.submitQuery(); + await PageObjects.header.waitUntilLoadingHasFinished(); + + await testSubjects.existOrFail('discoverMainError'); + + await queryBar.clearQuery(); + await queryBar.submitQuery(); + await PageObjects.header.waitUntilLoadingHasFinished(); + + await testSubjects.missingOrFail('discoverMainError'); + }); + + it('should show Field Statistics tab', async () => { + await testSubjects.click('dscViewModeFieldStatsButton'); + + await retry.try(async () => { + const fieldStatsTab = await testSubjects.find('dscViewModeFieldStatsButton'); + expect(await fieldStatsTab.getAttribute('aria-selected')).to.be('true'); + }); + + await testSubjects.existOrFail('dscViewModeToggle'); + }); + + it('should not show view mode toggle for text-based searches', async () => { + await testSubjects.click('dscViewModeDocumentButton'); + + await retry.try(async () => { + const documentsTab = await testSubjects.find('dscViewModeDocumentButton'); + expect(await documentsTab.getAttribute('aria-selected')).to.be('true'); + }); + + await testSubjects.existOrFail('dscViewModeToggle'); + + await PageObjects.discover.selectTextBaseLang(); + + await testSubjects.missingOrFail('dscViewModeToggle'); + + if (!useLegacyTable) { + await testSubjects.existOrFail('dscGridToolbar'); + } + }); + + it('should show text-based columns callout', async () => { + await testSubjects.missingOrFail('dscSelectedColumnsCallout'); + await PageObjects.unifiedFieldList.clickFieldListItemAdd('extension'); + await PageObjects.header.waitUntilLoadingHasFinished(); + await testSubjects.existOrFail('dscSelectedColumnsCallout'); + }); + }); + }); + }); +} diff --git a/test/functional/apps/discover/group3/index.ts b/test/functional/apps/discover/group3/index.ts index 9af02c006b14b..5827c1e7ed805 100644 --- a/test/functional/apps/discover/group3/index.ts +++ b/test/functional/apps/discover/group3/index.ts @@ -24,5 +24,6 @@ export default function ({ getService, loadTestFile }: FtrProviderContext) { loadTestFile(require.resolve('./_sidebar')); loadTestFile(require.resolve('./_request_counts')); loadTestFile(require.resolve('./_doc_viewer')); + loadTestFile(require.resolve('./_view_mode_toggle')); }); } diff --git a/x-pack/plugins/translations/translations/fr-FR.json b/x-pack/plugins/translations/translations/fr-FR.json index 6ed84d45aff89..3b9767376293e 100644 --- a/x-pack/plugins/translations/translations/fr-FR.json +++ b/x-pack/plugins/translations/translations/fr-FR.json @@ -2445,7 +2445,6 @@ "unifiedDataTable.tableHeader.timeFieldIconTooltipAriaLabel": "{timeFieldName} – Ce champ représente l'heure à laquelle les événements se sont produits.", "unifiedDataTable.searchGenerationWithDescription": "Tableau généré par la recherche {searchTitle}", "unifiedDataTable.searchGenerationWithDescriptionGrid": "Tableau généré par la recherche {searchTitle} ({searchDescription})", - "unifiedDataTable.selectedDocumentsNumber": "{nr} documents sélectionnés", "unifiedDataTable.clearSelection": "Effacer la sélection", "unifiedDataTable.controlColumnHeader": "Colonne de commande", "unifiedDataTable.copyToClipboardJSON": "Copier les documents dans le presse-papiers (JSON)", diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index 15d66275a00ba..def22f2a7011e 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -2460,7 +2460,6 @@ "unifiedDataTable.tableHeader.timeFieldIconTooltipAriaLabel": "{timeFieldName} - このフィールドはイベントの発生時刻を表します。", "unifiedDataTable.searchGenerationWithDescription": "検索{searchTitle}で生成されたテーブル", "unifiedDataTable.searchGenerationWithDescriptionGrid": "検索{searchTitle}で生成されたテーブル({searchDescription})", - "unifiedDataTable.selectedDocumentsNumber": "{nr}個のドキュメントが選択されました", "unifiedDataTable.clearSelection": "選択した項目をクリア", "unifiedDataTable.controlColumnHeader": "列の制御", "unifiedDataTable.copyToClipboardJSON": "ドキュメントをクリップボードにコピー(JSON)", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index b3ee160e83978..a7db489a6cbc9 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -2460,7 +2460,6 @@ "unifiedDataTable.tableHeader.timeFieldIconTooltipAriaLabel": "{timeFieldName} - 此字段表示事件发生的时间。", "unifiedDataTable.searchGenerationWithDescription": "搜索 {searchTitle} 生成的表", "unifiedDataTable.searchGenerationWithDescriptionGrid": "搜索 {searchTitle} 生成的表({searchDescription})", - "unifiedDataTable.selectedDocumentsNumber": "{nr} 个文档已选择", "unifiedDataTable.clearSelection": "清除所选内容", "unifiedDataTable.controlColumnHeader": "控制列", "unifiedDataTable.copyToClipboardJSON": "将文档复制到剪贴板 (JSON)",