mirror of
https://github.com/apache/superset.git
synced 2024-09-17 11:09:47 -04:00
feat(plugin-chart-table): Implement showing totals (#1034)
* feat(plugin-chart-table): implement totals row * Fix typo * Fix totals with percentage metrics * Code review fixes * Use dnd with percentage metrics and sortby controls * Make totals checkbox tooltip more descriptive * Remove console.log * Change totals tooltip * Fix typing error * Use array destructuring * Fix typo
This commit is contained in:
parent
62906906a5
commit
f4eeebf368
@ -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,
|
||||
}),
|
||||
};
|
||||
|
@ -69,6 +69,7 @@ import {
|
||||
dnd_adhoc_filters,
|
||||
dnd_adhoc_metric,
|
||||
dnd_adhoc_metrics,
|
||||
dnd_timeseries_limit_metric,
|
||||
dndColumnsControl,
|
||||
dndEntity,
|
||||
dndGroupByControl,
|
||||
@ -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,
|
||||
|
@ -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": "*",
|
||||
|
@ -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';
|
||||
@ -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
|
||||
@ -70,6 +73,8 @@ export default function DataTable<D extends object>({
|
||||
tableClassName,
|
||||
columns,
|
||||
data,
|
||||
totals,
|
||||
totalsHeaderSpan,
|
||||
serverPaginationData,
|
||||
width: initialWidth = '100%',
|
||||
height: initialHeight = 300,
|
||||
@ -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>
|
||||
);
|
||||
|
||||
|
@ -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;
|
||||
@ -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) {
|
||||
@ -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();
|
||||
@ -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"
|
||||
@ -197,7 +211,7 @@ function StickyWrap({
|
||||
visibility: 'hidden',
|
||||
}}
|
||||
>
|
||||
{React.cloneElement(table, {}, theadWithRef, tbody)}
|
||||
{React.cloneElement(table, {}, theadWithRef, tbody, tfootWithRef)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@ -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
|
||||
@ -272,6 +302,7 @@ function StickyWrap({
|
||||
>
|
||||
{headerTable}
|
||||
{bodyTable}
|
||||
{footerTable}
|
||||
{sizerTable}
|
||||
</div>
|
||||
);
|
||||
|
@ -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};
|
||||
}
|
||||
|
@ -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,
|
||||
@ -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';
|
||||
|
||||
@ -154,6 +147,7 @@ export default function TableChart<D extends DataRecord = DataRecord>(
|
||||
height,
|
||||
width,
|
||||
data,
|
||||
totals,
|
||||
isRawRecords,
|
||||
rowCount = 0,
|
||||
columns: columnsMeta,
|
||||
@ -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
|
||||
@ -241,10 +235,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) &&
|
||||
@ -263,12 +253,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 cellProps = {
|
||||
// show raw number in title in case of numeric values
|
||||
@ -346,10 +331,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"
|
||||
|
@ -18,11 +18,11 @@
|
||||
*/
|
||||
import {
|
||||
buildQueryContext,
|
||||
ensureIsArray,
|
||||
getMetricLabel,
|
||||
QueryMode,
|
||||
removeDuplicates,
|
||||
ensureIsArray,
|
||||
QueryObject,
|
||||
removeDuplicates,
|
||||
} from '@superset-ui/core';
|
||||
import { PostProcessingRule } from '@superset-ui/core/src/query/types/PostProcessing';
|
||||
import { BuildQuery } from '@superset-ui/core/src/chart/registries/ChartBuildQueryRegistrySingleton';
|
||||
@ -115,13 +115,25 @@ const buildQuery: BuildQuery<TableChartFormData> = (formData: TableChartFormData
|
||||
// Because we use same buildQuery for all table on the page we need split them by id
|
||||
options?.hooks?.setCachedChanges({ [formData.slice_id]: queryObject.filters });
|
||||
|
||||
const extraQueries: QueryObject[] = [];
|
||||
if (metrics && formData.show_totals && queryMode === QueryMode.aggregate) {
|
||||
extraQueries.push({
|
||||
...queryObject,
|
||||
columns: [],
|
||||
row_limit: 0,
|
||||
row_offset: 0,
|
||||
post_processing: [],
|
||||
});
|
||||
}
|
||||
|
||||
if (formData.server_pagination) {
|
||||
return [
|
||||
{ ...queryObject },
|
||||
{ ...queryObject, row_limit: 0, row_offset: 0, post_processing: [], is_rowcount: true },
|
||||
...extraQueries,
|
||||
];
|
||||
}
|
||||
return [queryObject];
|
||||
return [queryObject, ...extraQueries];
|
||||
});
|
||||
};
|
||||
|
||||
|
@ -25,6 +25,8 @@ import {
|
||||
QueryMode,
|
||||
QueryFormColumn,
|
||||
ChartDataResponseResult,
|
||||
isFeatureEnabled,
|
||||
FeatureFlag,
|
||||
} from '@superset-ui/core';
|
||||
import {
|
||||
D3_TIME_FORMAT_OPTIONS,
|
||||
@ -111,6 +113,11 @@ const percent_metrics: typeof sharedControls.metrics = {
|
||||
validators: [],
|
||||
};
|
||||
|
||||
const dnd_percent_metrics = {
|
||||
...percent_metrics,
|
||||
type: 'DndMetricSelect',
|
||||
};
|
||||
|
||||
const config: ControlPanelConfig = {
|
||||
controlPanelSections: [
|
||||
sections.legacyTimeseriesTime,
|
||||
@ -148,7 +155,9 @@ const config: ControlPanelConfig = {
|
||||
[
|
||||
{
|
||||
name: 'percent_metrics',
|
||||
config: percent_metrics,
|
||||
config: isFeatureEnabled(FeatureFlag.ENABLE_EXPLORE_DRAG_AND_DROP)
|
||||
? dnd_percent_metrics
|
||||
: percent_metrics,
|
||||
},
|
||||
],
|
||||
[
|
||||
@ -230,6 +239,20 @@ const config: ControlPanelConfig = {
|
||||
},
|
||||
},
|
||||
],
|
||||
[
|
||||
{
|
||||
name: 'show_totals',
|
||||
config: {
|
||||
type: 'CheckboxControl',
|
||||
label: t('Show totals'),
|
||||
default: true,
|
||||
description: t(
|
||||
'Show total aggregations of selected metrics. Note that row limit does not apply to the result.',
|
||||
),
|
||||
visibility: isAggMode,
|
||||
},
|
||||
},
|
||||
],
|
||||
['adhoc_filters'],
|
||||
],
|
||||
},
|
||||
@ -334,7 +357,7 @@ const config: ControlPanelConfig = {
|
||||
name: 'column_config',
|
||||
config: {
|
||||
type: 'ColumnConfigControl',
|
||||
label: t('Cuztomize columns'),
|
||||
label: t('Customize columns'),
|
||||
description: t('Further customize how to display each column'),
|
||||
renderTrigger: true,
|
||||
mapStateToProps(explore, control, chart) {
|
||||
|
@ -183,7 +183,7 @@ const transformProps = (chartProps: TableChartProps): TableChartTransformedProps
|
||||
height,
|
||||
width,
|
||||
rawFormData: formData,
|
||||
queriesData,
|
||||
queriesData = [],
|
||||
initialValues: filters = {},
|
||||
ownCurrentState: serverPaginationData = {},
|
||||
hooks: { onAddFilter: onChangeFilter, setDataMask = () => {} },
|
||||
@ -200,17 +200,31 @@ const transformProps = (chartProps: TableChartProps): TableChartTransformedProps
|
||||
server_page_length: serverPageLength = 10,
|
||||
order_desc: sortDesc = false,
|
||||
query_mode: queryMode,
|
||||
show_totals: showTotals,
|
||||
} = formData;
|
||||
|
||||
const [metrics, percentMetrics, columns] = processColumns(chartProps);
|
||||
const data = processDataRecords(queriesData?.[0]?.data, columns);
|
||||
const rowCount = queriesData?.[1]?.data?.[0]?.rowcount as number;
|
||||
|
||||
let baseQuery;
|
||||
let countQuery;
|
||||
let totalQuery;
|
||||
let rowCount;
|
||||
if (serverPagination) {
|
||||
[baseQuery, countQuery, totalQuery] = queriesData;
|
||||
rowCount = (countQuery?.data?.[0]?.rowcount as number) ?? 0;
|
||||
} else {
|
||||
[baseQuery, totalQuery] = queriesData;
|
||||
rowCount = baseQuery?.rowcount ?? 0;
|
||||
}
|
||||
const data = processDataRecords(baseQuery?.data, columns);
|
||||
const totals = showTotals && queryMode === QueryMode.aggregate ? totalQuery?.data[0] : undefined;
|
||||
|
||||
return {
|
||||
height,
|
||||
width,
|
||||
isRawRecords: queryMode === QueryMode.raw,
|
||||
data,
|
||||
totals,
|
||||
columns,
|
||||
serverPagination,
|
||||
metrics,
|
||||
|
@ -90,6 +90,7 @@ export interface TableChartTransformedProps<D extends DataRecord = DataRecord> {
|
||||
setDataMask: SetDataMaskHook;
|
||||
isRawRecords?: boolean;
|
||||
data: D[];
|
||||
totals?: D;
|
||||
columns: DataColumnMeta[];
|
||||
metrics?: (keyof D)[];
|
||||
percentMetrics?: (keyof D)[];
|
||||
|
@ -17,7 +17,7 @@
|
||||
* under the License.
|
||||
*/
|
||||
import { FilterXSS, getDefaultWhiteList } from 'xss';
|
||||
import { DataRecordValue } from '@superset-ui/core';
|
||||
import { DataRecordValue, GenericDataType, getNumberFormatter } from '@superset-ui/core';
|
||||
import { DataColumnMeta } from '../types';
|
||||
|
||||
const xss = new FilterXSS({
|
||||
@ -35,14 +35,15 @@ const xss = new FilterXSS({
|
||||
function isProbablyHTML(text: string) {
|
||||
return /<[^>]+>/.test(text);
|
||||
}
|
||||
|
||||
/**
|
||||
* Format text for cell value.
|
||||
*/
|
||||
export default function formatValue(
|
||||
function formatValue(
|
||||
formatter: DataColumnMeta['formatter'],
|
||||
value: DataRecordValue,
|
||||
): [boolean, string] {
|
||||
if (value === null) {
|
||||
if (value === null || typeof value === 'undefined') {
|
||||
return [false, 'N/A'];
|
||||
}
|
||||
if (formatter) {
|
||||
@ -54,3 +55,16 @@ export default function formatValue(
|
||||
}
|
||||
return [false, value.toString()];
|
||||
}
|
||||
|
||||
export function formatColumnValue(column: DataColumnMeta, value: DataRecordValue) {
|
||||
const { dataType, formatter, config = {} } = column;
|
||||
const isNumber = dataType === GenericDataType.NUMERIC;
|
||||
const smallNumberFormatter =
|
||||
config.d3SmallNumberFormat === undefined
|
||||
? formatter
|
||||
: getNumberFormatter(config.d3SmallNumberFormat);
|
||||
return formatValue(
|
||||
isNumber && typeof value === 'number' && Math.abs(value) < 1 ? smallNumberFormatter : formatter,
|
||||
value,
|
||||
);
|
||||
}
|
||||
|
@ -4689,6 +4689,11 @@
|
||||
resolved "https://registry.yarnpkg.com/@types/d3-array/-/d3-array-2.0.0.tgz#a0d63a296a2d8435a9ec59393dcac746c6174a96"
|
||||
integrity sha512-rGqfPVowNDTszSFvwoZIXvrPG7s/qKzm9piCRIH6xwTTRu7pPZ3ootULFnPkTt74B6i5lN0FpLQL24qGOw1uZA==
|
||||
|
||||
"@types/d3-array@^2.9.0":
|
||||
version "2.9.0"
|
||||
resolved "https://registry.yarnpkg.com/@types/d3-array/-/d3-array-2.9.0.tgz#fb6c3d7d7640259e68771cd90cc5db5ac1a1a012"
|
||||
integrity sha512-sdBMGfNvLUkBypPMEhOcKcblTQfgHbqbYrUqRE31jOwdDHBJBxz4co2MDAq93S4Cp++phk4UiwoEg/1hK3xXAQ==
|
||||
|
||||
"@types/d3-cloud@^1.2.1":
|
||||
version "1.2.3"
|
||||
resolved "https://registry.yarnpkg.com/@types/d3-cloud/-/d3-cloud-1.2.3.tgz#cfaac9cb601968c27094903a82687336de69b518"
|
||||
@ -5041,10 +5046,10 @@
|
||||
dependencies:
|
||||
"@types/react" "*"
|
||||
|
||||
"@types/react-table@^7.0.19":
|
||||
version "7.0.19"
|
||||
resolved "https://registry.yarnpkg.com/@types/react-table/-/react-table-7.0.19.tgz#a70603ac0ffdeaf399fc6919aacb32fc42e9dd40"
|
||||
integrity sha512-RYyEY7Yry6F2JsKhHeFsGdzuFF1hMqBStQrrazDzpBl4m/ECGHJxFVQjLBRzRwK+47ZKNPm79f7qEpHirbiCLA==
|
||||
"@types/react-table@^7.0.29":
|
||||
version "7.0.29"
|
||||
resolved "https://registry.yarnpkg.com/@types/react-table/-/react-table-7.0.29.tgz#af2c82f2d6a39be5bc0f191b30501309a8db0949"
|
||||
integrity sha512-RCGVKGlTDv3jbj37WJ5HhN3sPb0W/2rqlvyGUtvawnnyrxgI2BGgASvU93rq2jwanVp5J9l1NYAeiGlNhdaBGw==
|
||||
dependencies:
|
||||
"@types/react" "*"
|
||||
|
||||
@ -15968,10 +15973,10 @@ marksy@^8.0.0:
|
||||
he "^1.2.0"
|
||||
marked "^0.3.12"
|
||||
|
||||
match-sorter@^6.1.0:
|
||||
version "6.1.0"
|
||||
resolved "https://registry.yarnpkg.com/match-sorter/-/match-sorter-6.1.0.tgz#7fec6808d94311a35fef7fd842a11634f2361bd7"
|
||||
integrity sha512-sKPMf4kbF7Dm5Crx0bbfLpokK68PUJ/0STUIOPa1ZmTZEA3lCaPK3gapQR573oLmvdkTfGojzySkIwuq6Z6xRQ==
|
||||
match-sorter@^6.3.0:
|
||||
version "6.3.0"
|
||||
resolved "https://registry.yarnpkg.com/match-sorter/-/match-sorter-6.3.0.tgz#454a1b31ed218cddbce6231a0ecb5fdc549fed01"
|
||||
integrity sha512-efYOf/wUpNb8FgNY+cOD2EIJI1S5I7YPKsw0LBp7wqPh5pmMS6i/wr3ZWwfwrAw1NvqTA2KUReVRWDX84lUcOQ==
|
||||
dependencies:
|
||||
"@babel/runtime" "^7.12.5"
|
||||
remove-accents "0.4.2"
|
||||
@ -19292,10 +19297,10 @@ react-syntax-highlighter@^13.5.0:
|
||||
prismjs "^1.21.0"
|
||||
refractor "^3.1.0"
|
||||
|
||||
react-table@^7.2.1:
|
||||
version "7.6.1"
|
||||
resolved "https://registry.yarnpkg.com/react-table/-/react-table-7.6.1.tgz#33ad2bfc58de619678694c5452597544f4864266"
|
||||
integrity sha512-XQyvombPoFbNiDHWAXdxbN78kFpsT1/aJuRSupFfBhO3FJtEVIIq2xbV1NvLzrd1YwfCYRm07ln5OHlfz0SXBg==
|
||||
react-table@^7.6.3:
|
||||
version "7.6.3"
|
||||
resolved "https://registry.yarnpkg.com/react-table/-/react-table-7.6.3.tgz#76434392b3f62344bdb704f5b227c2f29c1ffb14"
|
||||
integrity sha512-hfPF13zDLxPMpLKzIKCE8RZud9T/XrRTsaCIf8zXpWZIZ2juCl7qrGpo3AQw9eAetXV5DP7s2GDm+hht7qq5Dw==
|
||||
|
||||
react-test-renderer@^16.0.0-0, react-test-renderer@^16.13.1:
|
||||
version "16.13.1"
|
||||
@ -19715,7 +19720,7 @@ regenerator-runtime@^0.11.0:
|
||||
resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.11.1.tgz#be05ad7f9bf7d22e056f9726cee5017fbf19e2e9"
|
||||
integrity sha512-MguG95oij0fC3QV3URf4V2SDYGJhJnJGqvIIgdECeODCT98wSWDAJ94SSuVpYQUoTcGUIL6L4yNB7j1DFFHSBg==
|
||||
|
||||
regenerator-runtime@^0.13.2, regenerator-runtime@^0.13.3, regenerator-runtime@^0.13.4, regenerator-runtime@^0.13.5, regenerator-runtime@^0.13.7:
|
||||
regenerator-runtime@^0.13.2, regenerator-runtime@^0.13.3, regenerator-runtime@^0.13.4, regenerator-runtime@^0.13.7:
|
||||
version "0.13.7"
|
||||
resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.13.7.tgz#cac2dacc8a1ea675feaabaeb8ae833898ae46f55"
|
||||
integrity sha512-a54FxoJDIr27pgf7IgeQGxmqUNYrcV338lf/6gH456HZ/PhX+5BcwHXG9ajESmwe6WRO0tAzRUrRmNONWgkrew==
|
||||
@ -23318,7 +23323,7 @@ xmlchars@^2.1.1, xmlchars@^2.2.0:
|
||||
resolved "https://registry.yarnpkg.com/xmlchars/-/xmlchars-2.2.0.tgz#060fe1bcb7f9c76fe2a17db86a9bc3ab894210cb"
|
||||
integrity sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==
|
||||
|
||||
xss@^1.0.6:
|
||||
xss@^1.0.8:
|
||||
version "1.0.8"
|
||||
resolved "https://registry.yarnpkg.com/xss/-/xss-1.0.8.tgz#32feb87feb74b3dcd3d404b7a68ababf10700535"
|
||||
integrity sha512-3MgPdaXV8rfQ/pNn16Eio6VXYPTkqwa0vc7GkiymmY/DqR1SE/7VPAAVZz1GJsJFrllMYO3RHfEaiUGjab6TNw==
|
||||
|
Loading…
Reference in New Issue
Block a user