diff --git a/x-pack/platform/plugins/shared/lens/common/expressions/datatable/datatable.ts b/x-pack/platform/plugins/shared/lens/common/expressions/datatable/datatable.ts index 11faf78dc3b4b..3d001ecb08c4a 100644 --- a/x-pack/platform/plugins/shared/lens/common/expressions/datatable/datatable.ts +++ b/x-pack/platform/plugins/shared/lens/common/expressions/datatable/datatable.ts @@ -7,6 +7,7 @@ import { i18n } from '@kbn/i18n'; import type { ExecutionContext } from '@kbn/expressions-plugin/common'; +import type { DataGridDensity } from '@kbn/unified-data-table'; import type { FormatFactory, RowHeightMode } from '../../types'; import type { DatatableColumnResult } from './datatable_column'; import type { DatatableExpressionFunction } from './types'; @@ -32,6 +33,7 @@ export interface DatatableArgs { headerRowHeight?: RowHeightMode; headerRowHeightLines?: number; pageSize?: PagingState['size']; + density?: DataGridDensity; } export const getDatatable = ( @@ -87,6 +89,10 @@ export const getDatatable = ( types: ['number'], help: '', }, + density: { + types: ['string'], + help: '', + }, }, async fn(...args) { /** Build optimization: prevent adding extra code into initial bundle **/ diff --git a/x-pack/platform/plugins/shared/lens/public/visualizations/datatable/components/density_settings.test.tsx b/x-pack/platform/plugins/shared/lens/public/visualizations/datatable/components/density_settings.test.tsx new file mode 100644 index 0000000000000..51cdc82d75217 --- /dev/null +++ b/x-pack/platform/plugins/shared/lens/public/visualizations/datatable/components/density_settings.test.tsx @@ -0,0 +1,90 @@ +/* + * 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; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { render, screen, fireEvent } from '@testing-library/react'; +import { DataGridDensity } from '@kbn/unified-data-table'; +import { DensitySettings, type DensitySettingsProps } from './density_settings'; + +describe('DensitySettings', () => { + const defaultProps: DensitySettingsProps = { + dataGridDensity: DataGridDensity.NORMAL, + onChange: jest.fn(), + }; + + beforeEach(() => { + jest.clearAllMocks(); + }); + + const renderDensitySettingsComponent = (propsOverrides: Partial = {}) => { + const rtlRender = render(); + return { + ...rtlRender, + rerender: (newProps: Partial) => + rtlRender.rerender(), + }; + }; + + it('renders the density settings component with label', () => { + renderDensitySettingsComponent(); + + expect(screen.getByLabelText('Density')).toBeInTheDocument(); + expect(screen.getByTestId('lnsDensitySettings')).toBeInTheDocument(); + }); + + it('displays all three density options and selects the provided option', () => { + renderDensitySettingsComponent(); + + expect(screen.getByRole('button', { name: 'Compact', pressed: false })).toBeInTheDocument(); + expect(screen.getByRole('button', { name: 'Normal', pressed: true })).toBeInTheDocument(); + expect(screen.getByRole('button', { name: 'Expanded', pressed: false })).toBeInTheDocument(); + }); + + it('calls onChange with compact density when compact option is clicked', () => { + renderDensitySettingsComponent(); + + fireEvent.click(screen.getByRole('button', { name: 'Compact' })); + + expect(defaultProps.onChange).toHaveBeenCalledTimes(1); + expect(defaultProps.onChange).toHaveBeenCalledWith(DataGridDensity.COMPACT); + }); + + it('calls onChange with expanded density when expanded option is clicked', () => { + renderDensitySettingsComponent(); + + fireEvent.click(screen.getByRole('button', { name: 'Expanded' })); + + expect(defaultProps.onChange).toHaveBeenCalledTimes(1); + expect(defaultProps.onChange).toHaveBeenCalledWith(DataGridDensity.EXPANDED); + }); + + it('falls back to NORMAL density when an invalid density is provided', () => { + renderDensitySettingsComponent({ + dataGridDensity: 'invalid' as DataGridDensity, + }); + + // The component should still render without errors + expect(screen.getByLabelText('Density')).toBeInTheDocument(); + + // The Normal button should be pressed + expect(screen.getByRole('button', { name: 'Normal', pressed: true })).toBeInTheDocument(); + }); + + it('updates selection when props change', () => { + const { rerender } = renderDensitySettingsComponent(); + + // Initial render should have Normal selected + expect(screen.getByRole('button', { name: 'Normal', pressed: true })); + + // Update props to Compact + rerender({ dataGridDensity: DataGridDensity.COMPACT }); + + // Now Compact should be pressed + expect(screen.getByRole('button', { name: 'Compact', pressed: true })).toBeInTheDocument(); + expect(screen.getByRole('button', { name: 'Normal', pressed: false })).toBeInTheDocument(); + }); +}); diff --git a/x-pack/platform/plugins/shared/lens/public/visualizations/datatable/components/density_settings.tsx b/x-pack/platform/plugins/shared/lens/public/visualizations/datatable/components/density_settings.tsx new file mode 100644 index 0000000000000..6f38ac622ad85 --- /dev/null +++ b/x-pack/platform/plugins/shared/lens/public/visualizations/datatable/components/density_settings.tsx @@ -0,0 +1,77 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { useCallback } from 'react'; +import { EuiButtonGroup, EuiFormRow } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { DataGridDensity } from '@kbn/unified-data-table'; + +export interface DensitySettingsProps { + dataGridDensity: DataGridDensity; + onChange: (density: DataGridDensity) => void; +} + +const densityValues = Object.values(DataGridDensity); + +const getValidDensity = (density: string) => { + const isValidDensity = densityValues.includes(density as DataGridDensity); + return isValidDensity ? (density as DataGridDensity) : DataGridDensity.NORMAL; +}; + +const densityLabel = i18n.translate('xpack.lens.table.densityLabel', { + defaultMessage: 'Density', +}); + +const densityOptions = [ + { + id: DataGridDensity.COMPACT, + label: i18n.translate('xpack.lens.table.labelCompact', { + defaultMessage: 'Compact', + }), + }, + { + id: DataGridDensity.NORMAL, + label: i18n.translate('xpack.lens.table.labelNormal', { + defaultMessage: 'Normal', + }), + }, + { + id: DataGridDensity.EXPANDED, + label: i18n.translate('xpack.lens.table.labelExpanded', { + defaultMessage: 'Expanded', + }), + }, +]; + +export const DensitySettings: React.FC = ({ dataGridDensity, onChange }) => { + // Falls back to NORMAL density when an invalid density is provided + const validDensity = getValidDensity(dataGridDensity); + + const setDensity = useCallback( + (density: string) => { + onChange(getValidDensity(density)); + }, + [onChange] + ); + + return ( + + + + ); +}; diff --git a/x-pack/platform/plugins/shared/lens/public/visualizations/datatable/components/table_basic.test.tsx b/x-pack/platform/plugins/shared/lens/public/visualizations/datatable/components/table_basic.test.tsx index e1ca865e1b450..46dfbfb23539d 100644 --- a/x-pack/platform/plugins/shared/lens/public/visualizations/datatable/components/table_basic.test.tsx +++ b/x-pack/platform/plugins/shared/lens/public/visualizations/datatable/components/table_basic.test.tsx @@ -23,6 +23,7 @@ import { PaletteOutput } from '@kbn/coloring'; import { getTransposeId } from '@kbn/transpose-utils'; import { CustomPaletteState } from '@kbn/charts-plugin/common'; import { getCellColorFn } from '../../../shared_components/coloring/get_cell_color_fn'; +import { DataGridDensity } from '@kbn/unified-data-table'; jest.mock('../../../shared_components/coloring/get_cell_color_fn', () => { const mod = jest.requireActual('../../../shared_components/coloring/get_cell_color_fn'); @@ -762,4 +763,59 @@ describe('DatatableComponent', () => { }); }); }); + + describe('gridStyle', () => { + it('should apply default grid style when density is not provided', () => { + renderDatatableComponent(); + const table = screen.getByTestId('lnsDataTable'); + expect(table).toHaveClass(/cellPadding-m-fontSize-m/); + }); + it('should apply normal grid style when density is normal', () => { + renderDatatableComponent({ + args: { + ...args, + density: DataGridDensity.NORMAL, + }, + }); + const table = screen.getByTestId('lnsDataTable'); + expect(table).toHaveClass(/cellPadding-m-fontSize-m/); + }); + it('should apply compact grid style when density is compact', () => { + renderDatatableComponent({ + args: { + ...args, + density: DataGridDensity.COMPACT, + }, + }); + const table = screen.getByTestId('lnsDataTable'); + expect(table).toHaveClass(/cellPadding-s-fontSize-s/); + }); + it('should apply expanded grid style when density is expanded', () => { + renderDatatableComponent({ + args: { + ...args, + density: DataGridDensity.EXPANDED, + }, + }); + const table = screen.getByTestId('lnsDataTable'); + expect(table).toHaveClass(/cellPadding-l-fontSize-l/); + }); + it('should update grid style when density changes', () => { + const { rerender } = renderDatatableComponent({ + args: { + ...args, + density: DataGridDensity.NORMAL, + }, + }); + const table = screen.getByTestId('lnsDataTable'); + expect(table).toHaveClass(/cellPadding-m-fontSize-m/); + rerender({ + args: { + ...args, + density: DataGridDensity.COMPACT, + }, + }); + expect(table).toHaveClass(/cellPadding-s-fontSize-s/); + }); + }); }); diff --git a/x-pack/platform/plugins/shared/lens/public/visualizations/datatable/components/table_basic.tsx b/x-pack/platform/plugins/shared/lens/public/visualizations/datatable/components/table_basic.tsx index bd15b41a49cbc..45e99c6628d0f 100644 --- a/x-pack/platform/plugins/shared/lens/public/visualizations/datatable/components/table_basic.tsx +++ b/x-pack/platform/plugins/shared/lens/public/visualizations/datatable/components/table_basic.tsx @@ -34,6 +34,8 @@ import { getColorCategories } from '@kbn/chart-expressions-common'; import { getOriginalId } from '@kbn/transpose-utils'; import { css } from '@emotion/react'; import { CoreTheme } from '@kbn/core/public'; +import { DATA_GRID_DENSITY_STYLE_MAP } from '@kbn/unified-data-table/src/hooks/use_data_grid_density'; +import { DATA_GRID_STYLE_NORMAL } from '@kbn/unified-data-table/src/constants'; import { getKbnPalettes } from '@kbn/palettes'; import type { LensTableRowContextMenuEvent } from '../../../types'; import type { FormatFactory } from '../../../../common/types'; @@ -69,7 +71,7 @@ import { getColumnAlignment } from '../utils'; export const DataContext = React.createContext({}); -const gridStyle: EuiDataGridStyle = { +const DATA_GRID_STYLE_DEFAULT: EuiDataGridStyle = { border: 'horizontal', header: 'shade', footer: 'shade', @@ -501,6 +503,16 @@ export const DatatableComponent = (props: DatatableRenderProps) => { } }, [columnConfig.columns, alignments, props.data, columns]); + const gridStyle = useMemo( + () => ({ + ...DATA_GRID_STYLE_DEFAULT, + ...(props.args.density + ? DATA_GRID_DENSITY_STYLE_MAP[props.args.density] + : DATA_GRID_STYLE_NORMAL), + }), + [props.args.density] + ); + if (isEmpty) { return (
{ @@ -45,6 +46,7 @@ describe('datatable toolbar', () => { const renderAndOpenToolbar = async (overrides = {}) => { const ROW_HEIGHT_SETTINGS_TEST_ID = 'lnsRowHeightSettings'; const HEADER_HEIGHT_SETTINGS_TEST_ID = 'lnsHeaderHeightSettings'; + const DENSITY_SETTINGS_TEST_ID = 'lnsDensitySettings'; const rtlRender = render(); @@ -78,6 +80,9 @@ describe('datatable toolbar', () => { selectHeaderHeightOption: selectOptionFromButtonGroup(HEADER_HEIGHT_SETTINGS_TEST_ID), getPaginationSwitch, clickPaginationSwitch, + // Density + getDensityValue: getSelectedButtonInGroup(DENSITY_SETTINGS_TEST_ID), + selectDensityOption: selectOptionFromButtonGroup(DENSITY_SETTINGS_TEST_ID), }; }; @@ -87,12 +92,14 @@ describe('datatable toolbar', () => { getRowHeightValue, getHeaderHeightValue, getPaginationSwitch, + getDensityValue, } = await renderAndOpenToolbar(); expect(getRowHeightValue()).toHaveTextContent(/custom/i); expect(getHeaderHeightValue()).toHaveTextContent(/custom/i); expect(getHeaderHeightCustomValue()).toHaveValue(3); expect(getPaginationSwitch()).not.toBeChecked(); + expect(getDensityValue()).toHaveTextContent(/normal/i); }); it('should reflect passed state in the UI', async () => { @@ -102,6 +109,7 @@ describe('datatable toolbar', () => { getPaginationSwitch, getHeaderHeightCustomValue, getRowHeightCustomValue, + getDensityValue, } = await renderAndOpenToolbar({ state: { ...defaultProps.state, @@ -110,6 +118,7 @@ describe('datatable toolbar', () => { headerRowHeight: 'custom', headerRowHeightLines: 4, paging: { size: 10, enabled: true }, + density: DataGridDensity.COMPACT, }, }); @@ -118,6 +127,7 @@ describe('datatable toolbar', () => { expect(getHeaderHeightValue()).toHaveTextContent(/custom/i); expect(getHeaderHeightCustomValue()).toHaveValue(4); expect(getPaginationSwitch()).toBeChecked(); + expect(getDensityValue()).toHaveTextContent(/compact/i); }); it('should change row height to "Auto" mode when selected', async () => { @@ -190,4 +200,22 @@ describe('datatable toolbar', () => { paging: { ...defaultPagingState, enabled: false }, }); }); + + it('should change density to "Compact" when selected', async () => { + const { selectDensityOption } = await renderAndOpenToolbar(); + selectDensityOption(/compact/i); + expect(defaultProps.setState).toHaveBeenCalledTimes(1); + expect(defaultProps.setState).toHaveBeenCalledWith({ + density: DataGridDensity.COMPACT, + }); + }); + + it('should change density to "Expanded" when selected', async () => { + const { selectDensityOption } = await renderAndOpenToolbar(); + selectDensityOption(/expanded/i); + expect(defaultProps.setState).toHaveBeenCalledTimes(1); + expect(defaultProps.setState).toHaveBeenCalledWith({ + density: DataGridDensity.EXPANDED, + }); + }); }); diff --git a/x-pack/platform/plugins/shared/lens/public/visualizations/datatable/components/toolbar.tsx b/x-pack/platform/plugins/shared/lens/public/visualizations/datatable/components/toolbar.tsx index 1f8eaf44e1f90..9933bad25e69f 100644 --- a/x-pack/platform/plugins/shared/lens/public/visualizations/datatable/components/toolbar.tsx +++ b/x-pack/platform/plugins/shared/lens/public/visualizations/datatable/components/toolbar.tsx @@ -8,7 +8,7 @@ import React, { useCallback, useState } from 'react'; import { i18n } from '@kbn/i18n'; import { EuiFlexGroup, EuiFormRow, EuiSwitch, EuiToolTip } from '@elastic/eui'; -import { RowHeightSettings, ROWS_HEIGHT_OPTIONS } from '@kbn/unified-data-table'; +import { DataGridDensity, RowHeightSettings, ROWS_HEIGHT_OPTIONS } from '@kbn/unified-data-table'; import { ToolbarPopover } from '../../../shared_components'; import type { VisualizationToolbarProps } from '../../../types'; import type { DatatableVisualizationState } from '../visualization'; @@ -21,6 +21,7 @@ import { DEFAULT_ROW_HEIGHT_LINES, ROW_HEIGHT_LINES_KEYS, } from './constants'; +import { DensitySettings } from './density_settings'; type LineCounts = { [key in keyof typeof ROW_HEIGHT_LINES_KEYS]: number; @@ -86,6 +87,16 @@ export function DataTableToolbar(props: VisualizationToolbarProps { + setState({ + ...state, + density, + }); + }, + [setState, state] + ); + return ( + { return { @@ -925,6 +926,25 @@ describe('Datatable Visualization', () => { ); }); + it('sets density based on state', () => { + expect(getDatatableExpressionArgs({ ...defaultExpressionTableState }).density).toEqual([ + DataGridDensity.NORMAL, + ]); + + for (const DENSITY of [ + DataGridDensity.COMPACT, + DataGridDensity.NORMAL, + DataGridDensity.EXPANDED, + ]) { + expect( + getDatatableExpressionArgs({ + ...defaultExpressionTableState, + density: DENSITY, + }).density + ).toEqual([DENSITY]); + } + }); + describe('palette/colorMapping/colorMode', () => { const colorMapping: ColorMapping.Config = { paletteId: 'default', diff --git a/x-pack/platform/plugins/shared/lens/public/visualizations/datatable/visualization.tsx b/x-pack/platform/plugins/shared/lens/public/visualizations/datatable/visualization.tsx index b569974cfc672..3e275cff48a3a 100644 --- a/x-pack/platform/plugins/shared/lens/public/visualizations/datatable/visualization.tsx +++ b/x-pack/platform/plugins/shared/lens/public/visualizations/datatable/visualization.tsx @@ -24,6 +24,7 @@ import { LayerTypes } from '@kbn/expression-xy-plugin/public'; import { buildExpression, buildExpressionFunction } from '@kbn/expressions-plugin/common'; import useObservable from 'react-use/lib/useObservable'; import { getSortingCriteria } from '@kbn/sort-predicates'; +import { DataGridDensity } from '@kbn/unified-data-table'; import { getKbnPalettes } from '@kbn/palettes'; import type { FormBasedPersistedState } from '../../datasources/form_based/types'; import type { @@ -71,6 +72,7 @@ export interface DatatableVisualizationState { rowHeightLines?: number; headerRowHeightLines?: number; paging?: PagingState; + density?: DataGridDensity; } const visualizationLabel = i18n.translate('xpack.lens.datatable.label', { @@ -643,6 +645,7 @@ export const getDatatableVisualization = ({ rowHeightLines: state.rowHeightLines ?? DEFAULT_ROW_HEIGHT_LINES, headerRowHeightLines: state.headerRowHeightLines ?? DEFAULT_HEADER_ROW_HEIGHT_LINES, pageSize: state.paging?.enabled ? state.paging.size : undefined, + density: state.density ?? DataGridDensity.NORMAL, }).toAst(); return { diff --git a/x-pack/test/functional/apps/lens/group2/table.ts b/x-pack/test/functional/apps/lens/group2/table.ts index 7de5645b16b03..3c15faab4038d 100644 --- a/x-pack/test/functional/apps/lens/group2/table.ts +++ b/x-pack/test/functional/apps/lens/group2/table.ts @@ -42,6 +42,23 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { }); }); + it('should apply compact density correctly', async () => { + await lens.openVisualOptions(); + + await lens.setDataTableDensity('compact'); + expect(await lens.checkDataTableDensity('s')).to.be(true); + }); + + it('should apply expanded density correctly', async () => { + await lens.setDataTableDensity('expanded'); + expect(await lens.checkDataTableDensity('l')).to.be(true); + }); + + it('should apply normal density correctly', async () => { + await lens.setDataTableDensity('normal'); + expect(await lens.checkDataTableDensity('m')).to.be(true); + }); + it('should able to sort a last_value column correctly in a table', async () => { // configure last_value with a keyword field await lens.configureDimension({ diff --git a/x-pack/test/functional/page_objects/lens_page.ts b/x-pack/test/functional/page_objects/lens_page.ts index acc1d4f56ff46..635a9c4ba6c3a 100644 --- a/x-pack/test/functional/page_objects/lens_page.ts +++ b/x-pack/test/functional/page_objects/lens_page.ts @@ -2059,5 +2059,17 @@ export function LensPageProvider({ getService, getPageObjects }: FtrProviderCont async toggleDebug(enable: boolean = true) { await browser.execute(`window.ELASTIC_LENS_LOGGER = arguments[0];`, enable); }, + + async setDataTableDensity(value: string) { + const settings = await testSubjects.find('lnsDensitySettings'); + const option = await settings.findByTestSubject(value); + await option.click(); + }, + + async checkDataTableDensity(size: 'l' | 'm' | 's') { + return find.existsByCssSelector( + `[data-test-subj="lnsDataTable"][class*="cellPadding-${size}-fontSize-${size}"]` + ); + }, }); }