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

TablePanel: Add support for Count calculation per column or per entire dataset #58134

Merged
merged 10 commits into from Nov 28, 2022
Expand Up @@ -160,3 +160,15 @@ Columns with filters applied have a blue funnel displayed next to the title.
{{< figure src="/static/img/docs/tables/filtered-column.png" max-width="500px" caption="Filtered column" class="docs-image--no-shadow" >}}

To remove the filter, click the blue funnel icon and then click **Clear filter**.

## Table footer

You can use the table footer to show [calculations]({{< relref "../../calculation-types/" >}}) on fields.

After enabling the table footer, you can select your **Calculation** and select the **Fields** that should be calculated. Not selecting any field apply the calculation to all numeric fields.
Copy link
Collaborator

Choose a reason for hiding this comment

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

After you enable the table footer:

  1. Select the Calculation.
  2. Select the Fields you want to calculate.

The system applies the calculation to all numeric fields if you do not select a field.


### Count rows

On selecting the **Count** calculation, you will see the **Count rows** switch.

By enabling this option the footer will show the number of rows in the dataset instead of the number of values in the selected fields.
Comment on lines +172 to +174
Copy link
Collaborator

Choose a reason for hiding this comment

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

I think you can combine these two statements by saying:

Select the Count calculation when you want to show the number of rows in the dataset instead of the number of values in the selected fields.

Expand Up @@ -291,7 +291,7 @@ export function doStandardCalcs(field: Field, ignoreNulls: boolean, nullAsZero:
} as FieldCalcs;

const data = field.values;
calcs.count = data.length;
calcs.count = ignoreNulls ? data.length : data.toArray().filter((val) => val != null).length;

const isNumberField = field.type === FieldType.number || FieldType.time;

Expand Down
15 changes: 11 additions & 4 deletions packages/grafana-ui/src/components/Table/FooterRow.tsx
Expand Up @@ -40,9 +40,7 @@ export const FooterRow = (props: FooterRowProps) => {
data-testid={e2eSelectorsTable.footer}
style={height ? { height: `${height}px` } : undefined}
>
{footerGroup.headers.map((column: ColumnInstance, index: number) =>
renderFooterCell(column, tableStyles, height)
)}
{footerGroup.headers.map((column: ColumnInstance) => renderFooterCell(column, tableStyles, height))}
</div>
);
})}
Expand Down Expand Up @@ -71,10 +69,19 @@ function renderFooterCell(column: ColumnInstance, tableStyles: TableStyles, heig
);
}

export function getFooterValue(index: number, footerValues?: FooterItem[]) {
export function getFooterValue(index: number, footerValues?: FooterItem[], isCountRowsSet?: boolean) {
if (footerValues === undefined) {
return EmptyCell;
}

if (isCountRowsSet) {
const count = footerValues[index];
if (typeof count !== 'string') {
return EmptyCell;
}

return FooterCell({ value: [{ Count: count }] });
}

return FooterCell({ value: footerValues[index] });
}
115 changes: 115 additions & 0 deletions packages/grafana-ui/src/components/Table/Table.test.tsx
Expand Up @@ -399,6 +399,121 @@ describe('Table', () => {
});
});

describe('on table footer enabled and count calculation selected', () => {
it('should show count of non-null values', async () => {
getTestContext({
footerOptions: { show: true, reducer: ['count'] },
data: toDataFrame({
name: 'A',
fields: [
{
name: 'number',
type: FieldType.number,
values: [1, 1, 1, 2, null],
config: {
custom: {
filterable: true,
},
},
},
],
}),
});

expect(within(getFooter()).getByRole('columnheader').getElementsByTagName('span')[0].textContent).toEqual('4');
});

it('should show count of rows when `count rows` is selected', async () => {
getTestContext({
footerOptions: { show: true, reducer: ['count'], countRows: true },
data: toDataFrame({
name: 'A',
fields: [
{
name: 'number1',
type: FieldType.number,
values: [1, 1, 1, 2, null],
config: {
custom: {
filterable: true,
},
},
},
],
}),
});

expect(within(getFooter()).getByRole('columnheader').getElementsByTagName('span')[0].textContent).toEqual(
'Count:'
);
expect(within(getFooter()).getByRole('columnheader').getElementsByTagName('span')[1].textContent).toEqual('5');
});

it('should show correct counts when turning `count rows` on and off', async () => {
const { rerender } = getTestContext({
footerOptions: { show: true, reducer: ['count'], countRows: true },
data: toDataFrame({
name: 'A',
fields: [
{
name: 'number1',
type: FieldType.number,
values: [1, 1, 1, 2, null],
config: {
custom: {
filterable: true,
},
},
},
],
}),
});

expect(within(getFooter()).getByRole('columnheader').getElementsByTagName('span')[0].textContent).toEqual(
'Count:'
);
expect(within(getFooter()).getByRole('columnheader').getElementsByTagName('span')[1].textContent).toEqual('5');

const onSortByChange = jest.fn();
const onCellFilterAdded = jest.fn();
const onColumnResize = jest.fn();
const props: Props = {
ariaLabel: 'aria-label',
data: getDefaultDataFrame(),
height: 600,
width: 800,
onSortByChange,
onCellFilterAdded,
onColumnResize,
};

const propOverrides = {
footerOptions: { show: true, reducer: ['count'], countRows: false },
data: toDataFrame({
name: 'A',
fields: [
{
name: 'number',
type: FieldType.number,
values: [1, 1, 1, 2, null],
config: {
custom: {
filterable: true,
},
},
},
],
}),
};

Object.assign(props, propOverrides);

rerender(<Table {...props} />);

expect(within(getFooter()).getByRole('columnheader').getElementsByTagName('span')[0].textContent).toEqual('4');
});
});

describe('when mounted with data and sub-data', () => {
it('then correct rows should be rendered and new table is rendered when expander is clicked', () => {
getTestContext({
Expand Down
50 changes: 37 additions & 13 deletions packages/grafana-ui/src/components/Table/Table.tsx
Expand Up @@ -12,7 +12,7 @@ import {
import usePrevious from 'react-use/lib/usePrevious';
import { VariableSizeList } from 'react-window';

import { DataFrame, getFieldDisplayName, Field } from '@grafana/data';
import { DataFrame, getFieldDisplayName, Field, ReducerID } from '@grafana/data';

import { useStyles2, useTheme2 } from '../../themes';
import { CustomScrollbar } from '../CustomScrollbar/CustomScrollbar';
Expand Down Expand Up @@ -188,10 +188,27 @@ export const Table = memo((props: Props) => {
return Array(data.length).fill(0);
}, [data]);

const isCountRowsSet = Boolean(
footerOptions?.countRows &&
footerOptions.reducer &&
footerOptions.reducer.length &&
footerOptions.reducer[0] === ReducerID.count
);

// React-table column definitions
const memoizedColumns = useMemo(
() => getColumns(data, width, columnMinWidth, expandedIndexes, setExpandedIndexes, !!subData?.length, footerItems),
[data, width, columnMinWidth, footerItems, subData, expandedIndexes]
() =>
getColumns(
data,
width,
columnMinWidth,
expandedIndexes,
setExpandedIndexes,
!!subData?.length,
footerItems,
isCountRowsSet
),
[data, width, columnMinWidth, footerItems, subData, expandedIndexes, isCountRowsSet]
);

// Internal react table state reducer
Expand Down Expand Up @@ -244,17 +261,24 @@ export const Table = memo((props: Props) => {
return;
}

if (footerOptions.show) {
setFooterItems(
getFooterItems(
headerGroups[0].headers as unknown as Array<{ field: Field }>,
createFooterCalculationValues(rows),
footerOptions,
theme
)
);
} else {
if (!footerOptions.show) {
setFooterItems(undefined);
return;
}

const footerItems = getFooterItems(
headerGroups[0].headers as unknown as Array<{ field: Field }>,
createFooterCalculationValues(rows),
footerOptions,
theme
);

if (isCountRowsSet) {
const footerItemsCountRows: FooterItem[] = new Array(footerItems.length).fill(undefined);
footerItemsCountRows[0] = data.length.toString();
setFooterItems(footerItemsCountRows);
} else {
setFooterItems(footerItems);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [footerOptions, theme, state.filters, data]);
Expand Down
1 change: 1 addition & 0 deletions packages/grafana-ui/src/components/Table/types.ts
Expand Up @@ -50,4 +50,5 @@ export interface TableFooterCalc {
reducer: string[]; // actually 1 value
fields?: string[];
enablePagination?: boolean;
countRows?: boolean;
}
5 changes: 3 additions & 2 deletions packages/grafana-ui/src/components/Table/utils.tsx
Expand Up @@ -68,7 +68,8 @@ export function getColumns(
expandedIndexes: Set<number>,
setExpandedIndexes: (indexes: Set<number>) => void,
expander: boolean,
footerValues?: FooterItem[]
footerValues?: FooterItem[],
isCountRowsSet?: boolean
): GrafanaTableColumn[] {
const columns: GrafanaTableColumn[] = expander
? [
Expand Down Expand Up @@ -134,7 +135,7 @@ export function getColumns(
minWidth: fieldTableOptions.minWidth ?? columnMinWidth,
filter: memoizeOne(filterByValue(field)),
justifyContent: getTextAlign(field),
Footer: getFooterValue(fieldIndex, footerValues),
Footer: getFooterValue(fieldIndex, footerValues, isCountRowsSet),
});
}

Expand Down
1 change: 1 addition & 0 deletions public/app/plugins/panel/table/models.gen.ts
Expand Up @@ -27,6 +27,7 @@ export const defaultPanelOptions: PanelOptions = {
footer: {
zoltanbedi marked this conversation as resolved.
Show resolved Hide resolved
show: false,
reducer: [],
countRows: false,
},
};

Expand Down
12 changes: 11 additions & 1 deletion public/app/plugins/panel/table/module.tsx
Expand Up @@ -131,6 +131,14 @@ export const plugin = new PanelPlugin<PanelOptions, TableFieldOptions>(TablePane
defaultValue: [ReducerID.sum],
showIf: (cfg) => cfg.footer?.show,
})
.addBooleanSwitch({
Copy link
Collaborator

Choose a reason for hiding this comment

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

Looks good! I'm thinking we should add a description here and maybe change the name of this option as well to make it more clear.

Copy link
Collaborator

Choose a reason for hiding this comment

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

And I'd say we might to reverse the language, like "Count Per Field" with a description along the lines of "Count the non-empty results of each field separately"

Copy link
Contributor Author

Choose a reason for hiding this comment

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

From this I understand we also reverse the functionality. E.g: We show a count for all fields in one column and when the "Count Per Field" switch is active we show a count for each field. This would change all existing table panels for the users. Is this something we'd want?

path: 'footer.countRows',
category: [footerCategory],
name: 'Count rows',
description: 'Display a single count for all data rows',
defaultValue: defaultPanelOptions.footer?.countRows,
showIf: (cfg) => cfg.footer?.reducer?.length === 1 && cfg.footer?.reducer[0] === ReducerID.count,
})
.addMultiSelect({
path: 'footer.fields',
category: [footerCategory],
Expand All @@ -156,7 +164,9 @@ export const plugin = new PanelPlugin<PanelOptions, TableFieldOptions>(TablePane
},
},
defaultValue: '',
showIf: (cfg) => cfg.footer?.show,
showIf: (cfg) =>
(cfg.footer?.show && !cfg.footer?.countRows) ||
(cfg.footer?.reducer?.length === 1 && cfg.footer?.reducer[0] !== ReducerID.count),
})
.addCustomEditor({
id: 'footer.enablePagination',
Expand Down