Skip to content
This repository has been archived by the owner on Dec 10, 2021. It is now read-only.

feat(plugin-chart-table): Implement showing totals #1034

Merged
merged 11 commits into from
Apr 6, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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 @@ -111,3 +111,15 @@ export const dnd_adhoc_metric: SharedControlConfig<'DndMetricSelect'> = {
description: t('Metric'),
default: (c: Control) => mainMetric(c.savedMetrics),
};

export const dnd_timeseries_limit_metric: SharedControlConfig<'DndMetricSelect'> = {
type: 'DndMetricSelect',
label: t('Sort by'),
default: null,
description: t('Metric used to define the top series'),
mapStateToProps: ({ datasource }) => ({
columns: datasource?.columns || [],
savedMetrics: datasource?.metrics || [],
datasourceType: datasource?.type,
}),
};
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@ import {
dnd_adhoc_filters,
dnd_adhoc_metric,
dnd_adhoc_metrics,
dnd_timeseries_limit_metric,
dndColumnsControl,
dndEntity,
dndGroupByControl,
Expand Down Expand Up @@ -480,7 +481,7 @@ const sharedControls = {
time_range,
row_limit,
limit,
timeseries_limit_metric,
timeseries_limit_metric: enableExploreDnd ? dnd_timeseries_limit_metric : timeseries_limit_metric,
series: enableExploreDnd ? dndSeries : series,
entity: enableExploreDnd ? dndEntity : entity,
x,
Expand Down
12 changes: 6 additions & 6 deletions plugins/plugin-chart-table/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -29,14 +29,14 @@
"@emotion/core": "^10.0.28",
"@superset-ui/chart-controls": "0.17.27",
"@superset-ui/core": "0.17.27",
"@types/d3-array": "^2.0.0",
"@types/react-table": "^7.0.19",
"@types/d3-array": "^2.9.0",
"@types/react-table": "^7.0.29",
"d3-array": "^2.4.0",
"match-sorter": "^6.1.0",
"match-sorter": "^6.3.0",
"memoize-one": "^5.1.1",
"react-table": "^7.2.1",
"regenerator-runtime": "^0.13.5",
"xss": "^1.0.6"
"react-table": "^7.6.3",
"regenerator-runtime": "^0.13.7",
"xss": "^1.0.8"
},
"peerDependencies": {
"@types/react": "*",
Expand Down
15 changes: 15 additions & 0 deletions plugins/plugin-chart-table/src/DataTable/DataTable.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ import {
IdType,
Row,
} from 'react-table';
import { t } from '@superset-ui/core';
import { matchSorter, rankings } from 'match-sorter';
import GlobalFilter, { GlobalFilterProps } from './components/GlobalFilter';
import SelectPageSize, { SelectPageSizeProps, SizeOption } from './components/SelectPageSize';
Expand All @@ -44,6 +45,8 @@ import { PAGE_SIZE_OPTIONS } from '../consts';

export interface DataTableProps<D extends object> extends TableOptions<D> {
tableClassName?: string;
totals?: { value: string; className?: string }[];
totalsHeaderSpan?: number;
searchInput?: boolean | GlobalFilterProps<D>['searchInput'];
selectPageSize?: boolean | SelectPageSizeProps['selectRenderer'];
pageSizeOptions?: SizeOption[]; // available page size options
Expand All @@ -70,6 +73,8 @@ export default function DataTable<D extends object>({
tableClassName,
columns,
data,
totals,
totalsHeaderSpan,
serverPaginationData,
width: initialWidth = '100%',
height: initialHeight = 300,
Expand Down Expand Up @@ -229,6 +234,16 @@ export default function DataTable<D extends object>({
</tr>
)}
</tbody>
{totals && (
<tfoot>
<tr key="totals" className="dt-totals">
<td colSpan={totalsHeaderSpan}>{t('Totals')}</td>
{totals.map(item => (
<td className={item.className}>{item.value}</td>
))}
</tr>
</tfoot>
)}
</table>
);

Expand Down
101 changes: 66 additions & 35 deletions plugins/plugin-chart-table/src/DataTable/hooks/useSticky.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -43,10 +43,11 @@ type TrWithTh = ReactElementWithChildren<'tr', Th[]>;
type TrWithTd = ReactElementWithChildren<'tr', Td[]>;
type Thead = ReactElementWithChildren<'thead', TrWithTh>;
type Tbody = ReactElementWithChildren<'tbody', TrWithTd>;
type Tfoot = ReactElementWithChildren<'tfoot', TrWithTd>;
type Col = ReactElementWithChildren<'col', null>;
type ColGroup = ReactElementWithChildren<'colgroup', Col>;

export type Table = ReactElementWithChildren<'table', (Thead | Tbody | ColGroup)[]>;
export type Table = ReactElementWithChildren<'table', (Thead | Tbody | Tfoot | ColGroup)[]>;
export type TableRenderer = () => Table;
export type GetTableSize = () => Partial<StickyState> | undefined;
export type SetStickyState = (size?: Partial<StickyState>) => void;
Expand Down Expand Up @@ -118,11 +119,18 @@ function StickyWrap({
}
let thead: Thead | undefined;
let tbody: Tbody | undefined;
let tfoot: Tfoot | undefined;

React.Children.forEach(table.props.children, node => {
if (!node) {
return;
}
if (node.type === 'thead') {
thead = node;
} else if (node.type === 'tbody') {
tbody = node;
} else if (node.type === 'tfoot') {
tfoot = node;
}
});
if (!thead || !tbody) {
Expand All @@ -134,7 +142,9 @@ function StickyWrap({
}, [thead]);

const theadRef = useRef<HTMLTableSectionElement>(null); // original thead for layout computation
const tfootRef = useRef<HTMLTableSectionElement>(null); // original tfoot for layout computation
const scrollHeaderRef = useRef<HTMLDivElement>(null); // fixed header
const scrollFooterRef = useRef<HTMLDivElement>(null); // fixed footer
const scrollBodyRef = useRef<HTMLDivElement>(null); // main body

const scrollBarSize = getScrollBarSize();
Expand All @@ -147,47 +157,51 @@ function StickyWrap({

// update scrollable area and header column sizes when mounted
useLayoutEffect(() => {
if (theadRef.current) {
const bodyThead = theadRef.current;
const theadHeight = bodyThead.clientHeight;
if (!theadHeight) {
return;
}
const fullTableHeight = (bodyThead.parentNode as HTMLTableElement).clientHeight;
const ths = bodyThead.childNodes[0].childNodes as NodeListOf<HTMLTableHeaderCellElement>;
const widths = Array.from(ths).map(th => th.clientWidth);
const [hasVerticalScroll, hasHorizontalScroll] = needScrollBar({
width: maxWidth,
height: maxHeight - theadHeight,
innerHeight: fullTableHeight,
innerWidth: widths.reduce(sum),
scrollBarSize,
});
// real container height, include table header and space for
// horizontal scroll bar
const realHeight = Math.min(
maxHeight,
hasHorizontalScroll ? fullTableHeight + scrollBarSize : fullTableHeight,
);
setStickyState({
hasVerticalScroll,
hasHorizontalScroll,
setStickyState,
width: maxWidth,
height: maxHeight,
realHeight,
tableHeight: fullTableHeight,
bodyHeight: realHeight - theadHeight,
columnWidths: widths,
});
if (!theadRef.current) {
return;
}
const bodyThead = theadRef.current;
const theadHeight = bodyThead.clientHeight;
const tfootHeight = tfootRef.current ? tfootRef.current.clientHeight : 0;
if (!theadHeight) {
return;
}
const fullTableHeight = (bodyThead.parentNode as HTMLTableElement).clientHeight;
const ths = bodyThead.childNodes[0].childNodes as NodeListOf<HTMLTableHeaderCellElement>;
const widths = Array.from(ths).map(th => th.clientWidth);
const [hasVerticalScroll, hasHorizontalScroll] = needScrollBar({
width: maxWidth,
height: maxHeight - theadHeight - tfootHeight,
innerHeight: fullTableHeight,
innerWidth: widths.reduce(sum),
scrollBarSize,
});
// real container height, include table header, footer and space for
// horizontal scroll bar
const realHeight = Math.min(
maxHeight,
hasHorizontalScroll ? fullTableHeight + scrollBarSize : fullTableHeight,
);
setStickyState({
hasVerticalScroll,
hasHorizontalScroll,
setStickyState,
width: maxWidth,
height: maxHeight,
realHeight,
tableHeight: fullTableHeight,
bodyHeight: realHeight - theadHeight - tfootHeight,
columnWidths: widths,
});
}, [maxWidth, maxHeight, setStickyState, scrollBarSize]);

let sizerTable: ReactElement | undefined;
let headerTable: ReactElement | undefined;
let footerTable: ReactElement | undefined;
let bodyTable: ReactElement | undefined;
if (needSizer) {
const theadWithRef = React.cloneElement(thead, { ref: theadRef });
const tfootWithRef = tfoot && React.cloneElement(tfoot, { ref: tfootRef });
sizerTable = (
<div
key="sizer"
Expand All @@ -197,7 +211,7 @@ function StickyWrap({
visibility: 'hidden',
}}
>
{React.cloneElement(table, {}, theadWithRef, tbody)}
{React.cloneElement(table, {}, theadWithRef, tbody, tfootWithRef)}
</div>
);
}
Expand Down Expand Up @@ -242,10 +256,26 @@ function StickyWrap({
</div>
);

footerTable = tfoot && (
<div
key="footer"
ref={scrollFooterRef}
style={{
overflow: 'hidden',
}}
>
{React.cloneElement(table, mergeStyleProp(table, fixedTableLayout), headerColgroup, tfoot)}
{footerTable}
</div>
);

const onScroll: UIEventHandler<HTMLDivElement> = e => {
if (scrollHeaderRef.current) {
scrollHeaderRef.current.scrollLeft = e.currentTarget.scrollLeft;
}
if (scrollFooterRef.current) {
scrollFooterRef.current.scrollLeft = e.currentTarget.scrollLeft;
}
};
bodyTable = (
<div
Expand All @@ -272,6 +302,7 @@ function StickyWrap({
>
{headerTable}
{bodyTable}
{footerTable}
{sizerTable}
</div>
);
Expand Down
3 changes: 3 additions & 0 deletions plugins/plugin-chart-table/src/Styles.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,9 @@ export default styled.div`
.dt-metric {
text-align: right;
}
.dt-totals {
font-weight: bold;
}
.dt-is-null {
color: ${({ theme: { colors } }) => colors.grayscale.light1};
}
Expand Down
54 changes: 30 additions & 24 deletions plugins/plugin-chart-table/src/TableChart.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,20 +16,13 @@
* specific language governing permissions and limitations
* under the License.
*/
import React, { useState, useMemo, useCallback, CSSProperties } from 'react';
import { ColumnInstance, DefaultSortTypes, ColumnWithLooseAccessor } from 'react-table';
import React, { CSSProperties, useCallback, useMemo, useState } from 'react';
import { ColumnInstance, ColumnWithLooseAccessor, DefaultSortTypes } from 'react-table';
import { extent as d3Extent, max as d3Max } from 'd3-array';
import { FaSort, FaSortUp as FaSortAsc, FaSortDown as FaSortDesc } from 'react-icons/fa';
import {
t,
tn,
DataRecordValue,
DataRecord,
GenericDataType,
getNumberFormatter,
} from '@superset-ui/core';
import { FaSort, FaSortDown as FaSortDesc, FaSortUp as FaSortAsc } from 'react-icons/fa';
import { DataRecord, DataRecordValue, GenericDataType, t, tn } from '@superset-ui/core';

import { TableChartTransformedProps, DataColumnMeta } from './types';
import { DataColumnMeta, TableChartTransformedProps } from './types';
import DataTable, {
DataTableProps,
SearchInputProps,
Expand All @@ -38,7 +31,7 @@ import DataTable, {
} from './DataTable';

import Styles from './Styles';
import formatValue from './utils/formatValue';
import { formatColumnValue } from './utils/formatValue';
import { PAGE_SIZE_OPTIONS } from './consts';
import { updateExternalFormData } from './DataTable/utils/externalAPIs';

Expand Down Expand Up @@ -154,6 +147,7 @@ export default function TableChart<D extends DataRecord = DataRecord>(
height,
width,
data,
totals,
isRawRecords,
rowCount = 0,
columns: columnsMeta,
Expand Down Expand Up @@ -220,7 +214,7 @@ export default function TableChart<D extends DataRecord = DataRecord>(

const getColumnConfigs = useCallback(
(column: DataColumnMeta, i: number): ColumnWithLooseAccessor<D> => {
const { key, label, dataType, isMetric, formatter, config = {} } = column;
const { key, label, dataType, isMetric, config = {} } = column;
const isNumber = dataType === GenericDataType.NUMERIC;
const isFilter = !isNumber && emitFilter;
const textAlign = config.horizontalAlign
Expand All @@ -235,10 +229,6 @@ export default function TableChart<D extends DataRecord = DataRecord>(
config.alignPositiveNegative === undefined ? defaultAlignPN : config.alignPositiveNegative;
const colorPositiveNegative =
config.colorPositiveNegative === undefined ? defaultColorPN : config.colorPositiveNegative;
const smallNumberFormatter =
config.d3SmallNumberFormat === undefined
? formatter
: getNumberFormatter(config.d3SmallNumberFormat);

const valueRange =
(config.showCellBars === undefined ? showCellBars : config.showCellBars) &&
Expand All @@ -257,12 +247,7 @@ export default function TableChart<D extends DataRecord = DataRecord>(
// so we ask TS not to check.
accessor: ((datum: D) => datum[key]) as never,
Cell: ({ value }: { column: ColumnInstance<D>; value: DataRecordValue }) => {
const [isHtml, text] = formatValue(
isNumber && typeof value === 'number' && Math.abs(value) < 1
? smallNumberFormatter
: formatter,
value,
);
const [isHtml, text] = formatColumnValue(column, value);
const html = isHtml ? { __html: text } : undefined;
const style: CSSProperties = {
background: valueRange
Expand Down Expand Up @@ -340,10 +325,31 @@ export default function TableChart<D extends DataRecord = DataRecord>(
updateExternalFormData(setDataMask, pageNumber, pageSize);
};

const totalsFormatted =
totals &&
columnsMeta
.filter(column => Object.keys(totals).includes(column.key))
.reduce(
(acc: { value: string; className: string }[], column) => [
...acc,
{
value: formatColumnValue(column, totals[column.key])[1],
className: column.dataType === GenericDataType.NUMERIC ? 'dt-metric' : '',
},
],
[],
);

const totalsHeaderSpan =
totalsFormatted &&
columnsMeta.filter(column => !column.isPercentMetric).length - totalsFormatted.length;

return (
<Styles>
<DataTable<D>
columns={columns}
totals={totalsFormatted}
totalsHeaderSpan={totalsHeaderSpan}
data={data}
rowCount={rowCount}
tableClassName="table table-striped table-condensed"
Expand Down
Loading