diff --git a/docs/sources/panels-visualizations/visualizations/table/index.md b/docs/sources/panels-visualizations/visualizations/table/index.md index f795848e51b1..914e09ba23c5 100644 --- a/docs/sources/panels-visualizations/visualizations/table/index.md +++ b/docs/sources/panels-visualizations/visualizations/table/index.md @@ -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. + +### 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. diff --git a/packages/grafana-data/src/transformations/fieldReducer.ts b/packages/grafana-data/src/transformations/fieldReducer.ts index 59691aaf81cc..4ce873ab34c6 100644 --- a/packages/grafana-data/src/transformations/fieldReducer.ts +++ b/packages/grafana-data/src/transformations/fieldReducer.ts @@ -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; diff --git a/packages/grafana-ui/src/components/Table/FooterRow.tsx b/packages/grafana-ui/src/components/Table/FooterRow.tsx index 59a4659f3d8b..8e9cc1e423c1 100644 --- a/packages/grafana-ui/src/components/Table/FooterRow.tsx +++ b/packages/grafana-ui/src/components/Table/FooterRow.tsx @@ -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))} ); })} @@ -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] }); } diff --git a/packages/grafana-ui/src/components/Table/Table.test.tsx b/packages/grafana-ui/src/components/Table/Table.test.tsx index b6ce1f0d003c..95ca40bf7a56 100644 --- a/packages/grafana-ui/src/components/Table/Table.test.tsx +++ b/packages/grafana-ui/src/components/Table/Table.test.tsx @@ -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(