Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Lens] New summary row feature for datatable #101075

Merged
merged 22 commits into from
Jun 9, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
2b39b45
:sparkles: New summary row feature for datatable
dej611 Jun 1, 2021
0ec6e9c
:sparkles: Allow empty strings behind flag + tests
dej611 Jun 3, 2021
e684272
:bug: Address the transition problem + refactor
dej611 Jun 3, 2021
a64d921
Merge branch 'master' into lens/total-row-table
kibanamachine Jun 3, 2021
0e14bd4
:white_check_mark: Add some unit tests
dej611 Jun 3, 2021
79e715b
:white_check_mark: Add first functional tests
dej611 Jun 3, 2021
bc58cd4
Merge branch 'lens/total-row-table' of github.com:dej611/kibana into …
dej611 Jun 3, 2021
cea3233
:ok_hand: first feedback addressed
dej611 Jun 3, 2021
48e85c3
:sparkles: Make it handle numeric array values
dej611 Jun 3, 2021
743606a
:memo: Improved message
dej611 Jun 3, 2021
d7c6946
Merge branch 'master' into lens/total-row-table
kibanamachine Jun 4, 2021
9417ccc
:white_check_mark: Fix functional test
dej611 Jun 4, 2021
678fd95
:fire: Remove warning message for last value
dej611 Jun 4, 2021
842add2
:rotating_light: Remove unused import
dej611 Jun 4, 2021
7b664c9
Merge branch 'master' into lens/total-row-table
kibanamachine Jun 7, 2021
bd069e4
:bug: Fix a bug with last value
dej611 Jun 7, 2021
9a0e2a1
:ok_hand: Integrated feedback
dej611 Jun 7, 2021
36b9745
:lipstick: Migrated to combobox
dej611 Jun 7, 2021
8ebeede
:white_check_mark: Fix unit tests + restore right data-test-id
dej611 Jun 8, 2021
4056906
Merge remote-tracking branch 'upstream/master' into lens/total-row-table
dej611 Jun 8, 2021
cf47e1d
:label: Fix type issue
dej611 Jun 8, 2021
77a4017
:ok_hand: Address all issues reported
dej611 Jun 8, 2021
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
*/

import React from 'react';
import { EuiButtonGroup } from '@elastic/eui';
import { EuiButtonGroup, EuiComboBox, EuiFieldText } from '@elastic/eui';
import { FramePublicAPI, VisualizationDimensionEditorProps } from '../../types';
import { DatatableVisualizationState } from '../visualization';
import { createMockDatasource, createMockFramePublicAPI } from '../../editor_frame_service/mocks';
Expand Down Expand Up @@ -212,4 +212,64 @@ describe('data table dimension editor', () => {

expect(instance.find(PalettePanelContainer).exists()).toBe(true);
});

it('should show the summary field for non numeric columns', () => {
const instance = mountWithIntl(<TableDimensionEditor {...props} />);
expect(instance.find('[data-test-subj="lnsDatatable_summaryrow_function"]').exists()).toBe(
false
);
expect(instance.find('[data-test-subj="lnsDatatable_summaryrow_label"]').exists()).toBe(false);
});

it('should set the summary row function default to "none"', () => {
frame.activeData!.first.columns[0].meta.type = 'number';
const instance = mountWithIntl(<TableDimensionEditor {...props} />);
expect(
instance
.find('[data-test-subj="lnsDatatable_summaryrow_function"]')
.find(EuiComboBox)
.prop('selectedOptions')
).toEqual([{ value: 'none', label: 'None' }]);

expect(instance.find('[data-test-subj="lnsDatatable_summaryrow_label"]').exists()).toBe(false);
});

it('should show the summary row label input ony when summary row is different from "none"', () => {
frame.activeData!.first.columns[0].meta.type = 'number';
state.columns[0].summaryRow = 'sum';
const instance = mountWithIntl(<TableDimensionEditor {...props} />);
expect(
instance
.find('[data-test-subj="lnsDatatable_summaryrow_function"]')
.find(EuiComboBox)
.prop('selectedOptions')
).toEqual([{ value: 'sum', label: 'Sum' }]);

expect(
instance
.find('[data-test-subj="lnsDatatable_summaryrow_label"]')
.find(EuiFieldText)
.prop('value')
).toBe('Sum');
});

it("should show the correct summary row name when user's changes summary label", () => {
frame.activeData!.first.columns[0].meta.type = 'number';
state.columns[0].summaryRow = 'sum';
state.columns[0].summaryLabel = 'MySum';
const instance = mountWithIntl(<TableDimensionEditor {...props} />);
expect(
instance
.find('[data-test-subj="lnsDatatable_summaryrow_function"]')
.find(EuiComboBox)
.prop('selectedOptions')
).toEqual([{ value: 'sum', label: 'Sum' }]);

expect(
instance
.find('[data-test-subj="lnsDatatable_summaryrow_label"]')
.find(EuiFieldText)
.prop('value')
).toBe('MySum');
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
* 2.0.
*/

import React, { useState } from 'react';
import React, { useCallback, useState } from 'react';
import { i18n } from '@kbn/i18n';
import {
EuiFormRow,
Expand All @@ -16,25 +16,35 @@ import {
EuiFlexItem,
EuiFlexGroup,
EuiButtonEmpty,
EuiFieldText,
EuiComboBox,
} from '@elastic/eui';
import { PaletteRegistry } from 'src/plugins/charts/public';
import { VisualizationDimensionEditorProps } from '../../types';
import { DatatableVisualizationState } from '../visualization';
import { ColumnState, DatatableVisualizationState } from '../visualization';
import { getOriginalId } from '../transpose_helpers';
import {
CustomizablePalette,
applyPaletteParams,
defaultPaletteParams,
FIXED_PROGRESSION,
getStopsForFixedMode,
useDebouncedValue,
} from '../../shared_components/';
import { PalettePanelContainer } from './palette_panel_container';
import { findMinMaxByColumnId } from './shared_utils';
import './dimension_editor.scss';
import {
getDefaultSummaryLabel,
getFinalSummaryConfiguration,
getSummaryRowOptions,
} from '../summary';
import { isNumericField } from '../utils';

const idPrefix = htmlIdGenerator()();

type ColumnType = DatatableVisualizationState['columns'][number];
type SummaryRowType = Extract<ColumnState['summaryRow'], string>;

function updateColumnWith(
state: DatatableVisualizationState,
Expand All @@ -58,20 +68,41 @@ export function TableDimensionEditor(
const { state, setState, frame, accessor } = props;
const column = state.columns.find(({ columnId }) => accessor === columnId);
const [isPaletteOpen, setIsPaletteOpen] = useState(false);
const onSummaryLabelChangeToDebounce = useCallback(
(newSummaryLabel: string | undefined) => {
setState({
...state,
columns: updateColumnWith(state, accessor, { summaryLabel: newSummaryLabel }),
});
},
[accessor, setState, state]
);
const { inputValue: summaryLabel, handleInputChange: onSummaryLabelChange } = useDebouncedValue<
string | undefined
>(
{
onChange: onSummaryLabelChangeToDebounce,
value: column?.summaryLabel,
},
{ allowEmptyString: true } // empty string is a valid label for this feature
);

if (!column) return null;
if (column.isTransposed) return null;

const currentData = frame.activeData?.[state.layerId];

// either read config state or use same logic as chart itself
const isNumericField =
currentData?.columns.find((col) => col.id === accessor || getOriginalId(col.id) === accessor)
?.meta.type === 'number';

const currentAlignment = column?.alignment || (isNumericField ? 'right' : 'left');
const isNumeric = isNumericField(currentData, accessor);
const currentAlignment = column?.alignment || (isNumeric ? 'right' : 'left');
const currentColorMode = column?.colorMode || 'none';
const hasDynamicColoring = currentColorMode !== 'none';
// when switching from one operation to another, make sure to keep the configuration consistent
const { summaryRow, summaryLabel: fallbackSummaryLabel } = getFinalSummaryConfiguration(
accessor,
column,
currentData
);

const visibleColumnsCount = state.columns.filter((c) => !c.hidden).length;

Expand Down Expand Up @@ -175,7 +206,61 @@ export function TableDimensionEditor(
/>
</EuiFormRow>
)}
{isNumericField && (
{isNumeric && (
<>
<EuiFormRow
fullWidth
label={i18n.translate('xpack.lens.table.summaryRow.label', {
defaultMessage: 'Summary Row',
})}
display="columnCompressed"
>
<EuiComboBox
fullWidth
compressed
isClearable={false}
data-test-subj="lnsDatatable_summaryrow_function"
placeholder={i18n.translate('xpack.lens.indexPattern.fieldPlaceholder', {
defaultMessage: 'Field',
})}
options={getSummaryRowOptions()}
selectedOptions={[
{
label: getDefaultSummaryLabel(summaryRow),
value: summaryRow,
},
]}
singleSelection={{ asPlainText: true }}
onChange={(choices) => {
const newValue = choices[0].value as SummaryRowType;
setState({
...state,
columns: updateColumnWith(state, accessor, { summaryRow: newValue }),
});
}}
/>
</EuiFormRow>
{summaryRow !== 'none' && (
<EuiFormRow
display="columnCompressed"
fullWidth
label={i18n.translate('xpack.lens.table.summaryRow.customlabel', {
defaultMessage: 'Summary label',
})}
>
<EuiFieldText
compressed
data-test-subj="lnsDatatable_summaryrow_label"
value={summaryLabel ?? fallbackSummaryLabel}
onChange={(e) => {
onSummaryLabelChange(e.target.value);
}}
/>
</EuiFormRow>
)}
</>
)}
{isNumeric && (
<>
<EuiFormRow
display="columnCompressed"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -539,4 +539,106 @@ describe('DatatableComponent', () => {
c: { min: 3, max: 3 },
});
});

test('it does render a summary footer if at least one column has it configured', () => {
const { data, args } = sampleArgs();

const wrapper = mountWithIntl(
<DatatableComponent
data={data}
args={{
...args,
columns: [
...args.columns.slice(0, 2),
{
columnId: 'c',
type: 'lens_datatable_column',
summaryRow: 'sum',
summaryLabel: 'Sum',
summaryRowValue: 3,
},
],
sortingColumnId: 'b',
sortingDirection: 'desc',
}}
formatFactory={() => ({ convert: (x) => x } as IFieldFormat)}
dispatchEvent={onDispatchEvent}
getType={jest.fn()}
renderMode="display"
paletteService={chartPluginMock.createPaletteRegistry()}
uiSettings={({ get: jest.fn() } as unknown) as IUiSettingsClient}
/>
);
expect(wrapper.find('[data-test-subj="lnsDataTable-footer-a"]').exists()).toEqual(false);
expect(wrapper.find('[data-test-subj="lnsDataTable-footer-c"]').first().text()).toEqual(
'Sum: 3'
);
});

test('it does render a summary footer with just the raw value for empty label', () => {
const { data, args } = sampleArgs();

const wrapper = mountWithIntl(
<DatatableComponent
data={data}
args={{
...args,
columns: [
...args.columns.slice(0, 2),
{
columnId: 'c',
type: 'lens_datatable_column',
summaryRow: 'sum',
summaryLabel: '',
summaryRowValue: 3,
},
],
sortingColumnId: 'b',
sortingDirection: 'desc',
}}
formatFactory={() => ({ convert: (x) => x } as IFieldFormat)}
dispatchEvent={onDispatchEvent}
getType={jest.fn()}
renderMode="display"
paletteService={chartPluginMock.createPaletteRegistry()}
uiSettings={({ get: jest.fn() } as unknown) as IUiSettingsClient}
/>
);

expect(wrapper.find('[data-test-subj="lnsDataTable-footer-c"]').first().text()).toEqual('3');
});

test('it does not render the summary row if the only column with summary is hidden', () => {
const { data, args } = sampleArgs();

const wrapper = mountWithIntl(
<DatatableComponent
data={data}
args={{
...args,
columns: [
...args.columns.slice(0, 2),
{
columnId: 'c',
type: 'lens_datatable_column',
summaryRow: 'sum',
summaryLabel: '',
summaryRowValue: 3,
hidden: true,
},
],
sortingColumnId: 'b',
sortingDirection: 'desc',
}}
formatFactory={() => ({ convert: (x) => x } as IFieldFormat)}
dispatchEvent={onDispatchEvent}
getType={jest.fn()}
renderMode="display"
paletteService={chartPluginMock.createPaletteRegistry()}
uiSettings={({ get: jest.fn() } as unknown) as IUiSettingsClient}
/>
);

expect(wrapper.find('[data-test-subj="lnsDataTable-footer-c"]').exists()).toBe(false);
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ import {
} from './table_actions';
import { findMinMaxByColumnId } from './shared_utils';
import { CUSTOM_PALETTE } from '../../shared_components/coloring/constants';
import { getFinalSummaryConfiguration } from '../summary';

export const DataContext = React.createContext<DataContextType>({});

Expand Down Expand Up @@ -286,6 +287,40 @@ export const DatatableComponent = (props: DatatableRenderProps) => {
[onEditAction, sortBy, sortDirection]
);

const renderSummaryRow = useMemo(() => {
const columnsWithSummary = columnConfig.columns
.filter((col) => !!col.columnId && !col.hidden)
.map((config) => ({
columnId: config.columnId,
summaryRowValue: config.summaryRowValue,
...getFinalSummaryConfiguration(config.columnId, config, firstTable),
}))
.filter(({ summaryRow }) => summaryRow !== 'none');
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If the only column with a summary row is hidden, it will show a weird empty row (note the thin gray line below the thick black one):
Screenshot 2021-06-08 at 14 05 02

Should be easy to fix take into account the hidden flag and filter columnsWithSummary


if (columnsWithSummary.length) {
const summaryLookup = Object.fromEntries(
columnsWithSummary.map(({ summaryRowValue, summaryLabel, columnId }) => [
columnId,
summaryLabel === '' ? `${summaryRowValue}` : `${summaryLabel}: ${summaryRowValue}`,
])
);
return ({ columnId }: { columnId: string }) => {
const currentAlignment = alignments && alignments[columnId];
const alignmentClassName = `lnsTableCell--${currentAlignment}`;
const columnName =
columns.find(({ id }) => id === columnId)?.displayAsText?.replace(/ /g, '-') || columnId;
return summaryLookup[columnId] != null ? (
<div
className={`lnsTableCell ${alignmentClassName}`}
data-test-subj={`lnsDataTable-footer-${columnName}`}
>
{summaryLookup[columnId]}
</div>
) : null;
};
}
}, [columnConfig.columns, alignments, firstTable, columns]);

if (isEmpty) {
return <EmptyPlaceholder icon={LensIconChartDatatable} />;
}
Expand Down Expand Up @@ -323,6 +358,7 @@ export const DatatableComponent = (props: DatatableRenderProps) => {
sorting={sorting}
onColumnResize={onColumnResize}
toolbarVisibility={false}
renderFooterCellValue={renderSummaryRow}
/>
</DataContext.Provider>
</VisualizationContainer>
Expand Down
Loading