From 66ebdf288ef31df3e1bf1d963dc68a2ea1ab322d Mon Sep 17 00:00:00 2001 From: Kamil Gabryjelski Date: Tue, 6 Apr 2021 16:31:40 +0200 Subject: [PATCH] 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 --- .../src/shared-controls/dndControls.tsx | 12 +++ .../src/shared-controls/index.tsx | 3 +- .../plugins/plugin-chart-table/package.json | 12 +-- .../src/DataTable/DataTable.tsx | 15 +++ .../src/DataTable/hooks/useSticky.tsx | 101 ++++++++++++------ .../plugins/plugin-chart-table/src/Styles.tsx | 3 + .../plugin-chart-table/src/TableChart.tsx | 54 +++++----- .../plugin-chart-table/src/buildQuery.ts | 18 +++- .../plugin-chart-table/src/controlPanel.tsx | 27 ++++- .../plugin-chart-table/src/transformProps.ts | 20 +++- .../plugins/plugin-chart-table/src/types.ts | 1 + .../src/utils/formatValue.ts | 20 +++- .../superset-ui/yarn.lock | 33 +++--- 13 files changed, 228 insertions(+), 91 deletions(-) diff --git a/superset-frontend/temporary_superset_ui/superset-ui/packages/superset-ui-chart-controls/src/shared-controls/dndControls.tsx b/superset-frontend/temporary_superset_ui/superset-ui/packages/superset-ui-chart-controls/src/shared-controls/dndControls.tsx index 11ee885b25dd7..cdb1aa11c1051 100644 --- a/superset-frontend/temporary_superset_ui/superset-ui/packages/superset-ui-chart-controls/src/shared-controls/dndControls.tsx +++ b/superset-frontend/temporary_superset_ui/superset-ui/packages/superset-ui-chart-controls/src/shared-controls/dndControls.tsx @@ -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, + }), +}; diff --git a/superset-frontend/temporary_superset_ui/superset-ui/packages/superset-ui-chart-controls/src/shared-controls/index.tsx b/superset-frontend/temporary_superset_ui/superset-ui/packages/superset-ui-chart-controls/src/shared-controls/index.tsx index 90e099ffdbaff..3bb9c13e1d4aa 100644 --- a/superset-frontend/temporary_superset_ui/superset-ui/packages/superset-ui-chart-controls/src/shared-controls/index.tsx +++ b/superset-frontend/temporary_superset_ui/superset-ui/packages/superset-ui-chart-controls/src/shared-controls/index.tsx @@ -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, diff --git a/superset-frontend/temporary_superset_ui/superset-ui/plugins/plugin-chart-table/package.json b/superset-frontend/temporary_superset_ui/superset-ui/plugins/plugin-chart-table/package.json index 15555c56222da..d6dd6e92cce22 100644 --- a/superset-frontend/temporary_superset_ui/superset-ui/plugins/plugin-chart-table/package.json +++ b/superset-frontend/temporary_superset_ui/superset-ui/plugins/plugin-chart-table/package.json @@ -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": "*", diff --git a/superset-frontend/temporary_superset_ui/superset-ui/plugins/plugin-chart-table/src/DataTable/DataTable.tsx b/superset-frontend/temporary_superset_ui/superset-ui/plugins/plugin-chart-table/src/DataTable/DataTable.tsx index 8a1e091a8769d..9fc0551d157a9 100644 --- a/superset-frontend/temporary_superset_ui/superset-ui/plugins/plugin-chart-table/src/DataTable/DataTable.tsx +++ b/superset-frontend/temporary_superset_ui/superset-ui/plugins/plugin-chart-table/src/DataTable/DataTable.tsx @@ -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 extends TableOptions { tableClassName?: string; + totals?: { value: string; className?: string }[]; + totalsHeaderSpan?: number; searchInput?: boolean | GlobalFilterProps['searchInput']; selectPageSize?: boolean | SelectPageSizeProps['selectRenderer']; pageSizeOptions?: SizeOption[]; // available page size options @@ -70,6 +73,8 @@ export default function DataTable({ tableClassName, columns, data, + totals, + totalsHeaderSpan, serverPaginationData, width: initialWidth = '100%', height: initialHeight = 300, @@ -229,6 +234,16 @@ export default function DataTable({ )} + {totals && ( + + + {t('Totals')} + {totals.map(item => ( + {item.value} + ))} + + + )} ); diff --git a/superset-frontend/temporary_superset_ui/superset-ui/plugins/plugin-chart-table/src/DataTable/hooks/useSticky.tsx b/superset-frontend/temporary_superset_ui/superset-ui/plugins/plugin-chart-table/src/DataTable/hooks/useSticky.tsx index 1dbdeead69bf9..0227bf32b01cf 100644 --- a/superset-frontend/temporary_superset_ui/superset-ui/plugins/plugin-chart-table/src/DataTable/hooks/useSticky.tsx +++ b/superset-frontend/temporary_superset_ui/superset-ui/plugins/plugin-chart-table/src/DataTable/hooks/useSticky.tsx @@ -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 | undefined; export type SetStickyState = (size?: Partial) => 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(null); // original thead for layout computation + const tfootRef = useRef(null); // original tfoot for layout computation const scrollHeaderRef = useRef(null); // fixed header + const scrollFooterRef = useRef(null); // fixed footer const scrollBodyRef = useRef(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; - 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; + 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 = (
- {React.cloneElement(table, {}, theadWithRef, tbody)} + {React.cloneElement(table, {}, theadWithRef, tbody, tfootWithRef)}
); } @@ -242,10 +256,26 @@ function StickyWrap({ ); + footerTable = tfoot && ( +
+ {React.cloneElement(table, mergeStyleProp(table, fixedTableLayout), headerColgroup, tfoot)} + {footerTable} +
+ ); + const onScroll: UIEventHandler = e => { if (scrollHeaderRef.current) { scrollHeaderRef.current.scrollLeft = e.currentTarget.scrollLeft; } + if (scrollFooterRef.current) { + scrollFooterRef.current.scrollLeft = e.currentTarget.scrollLeft; + } }; bodyTable = (
{headerTable} {bodyTable} + {footerTable} {sizerTable}
); diff --git a/superset-frontend/temporary_superset_ui/superset-ui/plugins/plugin-chart-table/src/Styles.tsx b/superset-frontend/temporary_superset_ui/superset-ui/plugins/plugin-chart-table/src/Styles.tsx index f04fe4ad4c3af..deeecf4e32d85 100644 --- a/superset-frontend/temporary_superset_ui/superset-ui/plugins/plugin-chart-table/src/Styles.tsx +++ b/superset-frontend/temporary_superset_ui/superset-ui/plugins/plugin-chart-table/src/Styles.tsx @@ -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}; } diff --git a/superset-frontend/temporary_superset_ui/superset-ui/plugins/plugin-chart-table/src/TableChart.tsx b/superset-frontend/temporary_superset_ui/superset-ui/plugins/plugin-chart-table/src/TableChart.tsx index 4b76196b5d957..1428ea7cf3684 100644 --- a/superset-frontend/temporary_superset_ui/superset-ui/plugins/plugin-chart-table/src/TableChart.tsx +++ b/superset-frontend/temporary_superset_ui/superset-ui/plugins/plugin-chart-table/src/TableChart.tsx @@ -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( height, width, data, + totals, isRawRecords, rowCount = 0, columns: columnsMeta, @@ -220,7 +214,7 @@ export default function TableChart( const getColumnConfigs = useCallback( (column: DataColumnMeta, i: number): ColumnWithLooseAccessor => { - 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( 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( // so we ask TS not to check. accessor: ((datum: D) => datum[key]) as never, Cell: ({ value }: { column: ColumnInstance; 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( 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 ( columns={columns} + totals={totalsFormatted} + totalsHeaderSpan={totalsHeaderSpan} data={data} rowCount={rowCount} tableClassName="table table-striped table-condensed" diff --git a/superset-frontend/temporary_superset_ui/superset-ui/plugins/plugin-chart-table/src/buildQuery.ts b/superset-frontend/temporary_superset_ui/superset-ui/plugins/plugin-chart-table/src/buildQuery.ts index 8d1781dd9a97b..afe1db500edaf 100644 --- a/superset-frontend/temporary_superset_ui/superset-ui/plugins/plugin-chart-table/src/buildQuery.ts +++ b/superset-frontend/temporary_superset_ui/superset-ui/plugins/plugin-chart-table/src/buildQuery.ts @@ -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 = (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]; }); }; diff --git a/superset-frontend/temporary_superset_ui/superset-ui/plugins/plugin-chart-table/src/controlPanel.tsx b/superset-frontend/temporary_superset_ui/superset-ui/plugins/plugin-chart-table/src/controlPanel.tsx index 17524462ccc2c..4ffdade93497a 100644 --- a/superset-frontend/temporary_superset_ui/superset-ui/plugins/plugin-chart-table/src/controlPanel.tsx +++ b/superset-frontend/temporary_superset_ui/superset-ui/plugins/plugin-chart-table/src/controlPanel.tsx @@ -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) { diff --git a/superset-frontend/temporary_superset_ui/superset-ui/plugins/plugin-chart-table/src/transformProps.ts b/superset-frontend/temporary_superset_ui/superset-ui/plugins/plugin-chart-table/src/transformProps.ts index 0d3e3cdb0b94c..5cea763818204 100644 --- a/superset-frontend/temporary_superset_ui/superset-ui/plugins/plugin-chart-table/src/transformProps.ts +++ b/superset-frontend/temporary_superset_ui/superset-ui/plugins/plugin-chart-table/src/transformProps.ts @@ -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, diff --git a/superset-frontend/temporary_superset_ui/superset-ui/plugins/plugin-chart-table/src/types.ts b/superset-frontend/temporary_superset_ui/superset-ui/plugins/plugin-chart-table/src/types.ts index 963d69786b543..279a58481bd48 100644 --- a/superset-frontend/temporary_superset_ui/superset-ui/plugins/plugin-chart-table/src/types.ts +++ b/superset-frontend/temporary_superset_ui/superset-ui/plugins/plugin-chart-table/src/types.ts @@ -90,6 +90,7 @@ export interface TableChartTransformedProps { setDataMask: SetDataMaskHook; isRawRecords?: boolean; data: D[]; + totals?: D; columns: DataColumnMeta[]; metrics?: (keyof D)[]; percentMetrics?: (keyof D)[]; diff --git a/superset-frontend/temporary_superset_ui/superset-ui/plugins/plugin-chart-table/src/utils/formatValue.ts b/superset-frontend/temporary_superset_ui/superset-ui/plugins/plugin-chart-table/src/utils/formatValue.ts index c35d1f3a0bf14..31bdb711ae802 100644 --- a/superset-frontend/temporary_superset_ui/superset-ui/plugins/plugin-chart-table/src/utils/formatValue.ts +++ b/superset-frontend/temporary_superset_ui/superset-ui/plugins/plugin-chart-table/src/utils/formatValue.ts @@ -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, + ); +} diff --git a/superset-frontend/temporary_superset_ui/superset-ui/yarn.lock b/superset-frontend/temporary_superset_ui/superset-ui/yarn.lock index 37c277c58836f..86594b3a89307 100644 --- a/superset-frontend/temporary_superset_ui/superset-ui/yarn.lock +++ b/superset-frontend/temporary_superset_ui/superset-ui/yarn.lock @@ -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==