diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/BigNumber/BigNumberPeriodOverPeriod/PopKPI.tsx b/superset-frontend/plugins/plugin-chart-echarts/src/BigNumber/BigNumberPeriodOverPeriod/PopKPI.tsx index 9ec2111071b..854d224f20c 100644 --- a/superset-frontend/plugins/plugin-chart-echarts/src/BigNumber/BigNumberPeriodOverPeriod/PopKPI.tsx +++ b/superset-frontend/plugins/plugin-chart-echarts/src/BigNumber/BigNumberPeriodOverPeriod/PopKPI.tsx @@ -81,6 +81,8 @@ export default function PopKPI(props: PopKPIProps) { currentTimeRangeFilter, startDateOffset, shift, + subtitle, + subtitleFontSize, dashboardTimeRange, } = props; @@ -140,6 +142,16 @@ export default function PopKPI(props: PopKPIProps) { margin-bottom: ${theme.gridUnit * 4}px; `; + const SubtitleText = styled.div` + ${({ theme }) => ` + font-family: ${theme.typography.families.sansSerif}; + font-weight: ${theme.typography.weights.medium}; + text-align: center; + margin-top: -10px; + margin-bottom: ${theme.gridUnit * 4}px; + `} + `; + const getArrowIndicatorColor = () => { if (!comparisonColorEnabled || percentDifferenceNumber === 0) { return theme.colors.grayscale.base; @@ -195,31 +207,40 @@ export default function PopKPI(props: PopKPIProps) { ]); const SYMBOLS_WITH_VALUES = useMemo( - () => [ - { - symbol: '#', - value: prevNumber, - tooltipText: t('Data for %s', comparisonRange || 'previous range'), - columnKey: 'Previous value', - }, - { - symbol: '△', - value: valueDifference, - tooltipText: t('Value difference between the time periods'), - columnKey: 'Delta', - }, - { - symbol: '%', - value: percentDifferenceFormattedString, - tooltipText: t('Percentage difference between the time periods'), - columnKey: 'Percent change', - }, - ], + () => + [ + { + defaultSymbol: '#', + value: prevNumber, + tooltipText: t('Data for %s', comparisonRange || 'previous range'), + columnKey: 'Previous value', + }, + { + defaultSymbol: '△', + value: valueDifference, + tooltipText: t('Value difference between the time periods'), + columnKey: 'Delta', + }, + { + defaultSymbol: '%', + value: percentDifferenceFormattedString, + tooltipText: t('Percentage difference between the time periods'), + columnKey: 'Percent change', + }, + ].map(item => { + const config = props.columnConfig?.[item.columnKey]; + return { + ...item, + symbol: config?.displayTypeIcon === false ? '' : item.defaultSymbol, + label: config?.customColumnName || item.columnKey, + }; + }), [ comparisonRange, prevNumber, valueDifference, percentDifferenceFormattedString, + props.columnConfig, ], ); @@ -250,6 +271,15 @@ export default function PopKPI(props: PopKPIProps) { )} + {subtitle && ( + + {subtitle} + + )} {visibleSymbols.length > 0 && (
{visibleSymbols.map((symbol_with_value, index) => ( - 0 ? backgroundColor : defaultBackgroundColor - } - textColor={index > 0 ? textColor : defaultTextColor} - > - {symbol_with_value.symbol} - - {symbol_with_value.value} + {symbol_with_value.symbol && ( + 0 ? backgroundColor : defaultBackgroundColor + } + textColor={index > 0 ? textColor : defaultTextColor} + > + {symbol_with_value.symbol} + + )} + {symbol_with_value.value}{' '} + {props.columnConfig?.[symbol_with_value.columnKey] + ?.customColumnName || ''} ))} diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/BigNumber/BigNumberPeriodOverPeriod/controlPanel.ts b/superset-frontend/plugins/plugin-chart-echarts/src/BigNumber/BigNumberPeriodOverPeriod/controlPanel.ts index bb285a70b0c..63c126216b2 100644 --- a/superset-frontend/plugins/plugin-chart-echarts/src/BigNumber/BigNumberPeriodOverPeriod/controlPanel.ts +++ b/superset-frontend/plugins/plugin-chart-echarts/src/BigNumber/BigNumberPeriodOverPeriod/controlPanel.ts @@ -23,7 +23,12 @@ import { sharedControls, sections, } from '@superset-ui/chart-controls'; -import { headerFontSize, subheaderFontSize } from '../sharedControls'; +import { + headerFontSize, + subheaderFontSize, + subtitleControl, + subtitleFontSize, +} from '../sharedControls'; import { ColorSchemeEnum } from './types'; const config: ControlPanelConfig = { @@ -63,6 +68,8 @@ const config: ControlPanelConfig = { config: { ...headerFontSize.config, default: 0.2 }, }, ], + [subtitleControl], + [subtitleFontSize], [ { ...subheaderFontSize, @@ -120,7 +127,11 @@ const config: ControlPanelConfig = { [GenericDataType.Numeric]: [ { tab: t('General'), - children: [['visible']], + children: [ + ['customColumnName'], + ['displayTypeIcon'], + ['visible'], + ], }, ], }, diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/BigNumber/BigNumberPeriodOverPeriod/transformProps.ts b/superset-frontend/plugins/plugin-chart-echarts/src/BigNumber/BigNumberPeriodOverPeriod/transformProps.ts index 9adf3e1fba7..a40f18b24c3 100644 --- a/superset-frontend/plugins/plugin-chart-echarts/src/BigNumber/BigNumberPeriodOverPeriod/transformProps.ts +++ b/superset-frontend/plugins/plugin-chart-echarts/src/BigNumber/BigNumberPeriodOverPeriod/transformProps.ts @@ -89,6 +89,8 @@ export default function transformProps(chartProps: ChartProps) { comparisonColorScheme, comparisonColorEnabled, percentDifferenceFormat, + subtitle = '', + subtitleFontSize, columnConfig, } = formData; const { data: dataA = [] } = queriesData[0]; @@ -183,6 +185,8 @@ export default function transformProps(chartProps: ChartProps) { valueDifference, percentDifferenceFormattedString: percentDifference, boldText, + subtitle, + subtitleFontSize, headerFontSize: getHeaderFontSize(headerFontSize), subheaderFontSize: getComparisonFontSize(subheaderFontSize), headerText, diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/BigNumber/BigNumberPeriodOverPeriod/types.ts b/superset-frontend/plugins/plugin-chart-echarts/src/BigNumber/BigNumberPeriodOverPeriod/types.ts index 8aef509088d..bd12d1a1540 100644 --- a/superset-frontend/plugins/plugin-chart-echarts/src/BigNumber/BigNumberPeriodOverPeriod/types.ts +++ b/superset-frontend/plugins/plugin-chart-echarts/src/BigNumber/BigNumberPeriodOverPeriod/types.ts @@ -35,6 +35,8 @@ export interface PopKPIStylesProps { export type TableColumnConfig = { visible?: boolean; + customColumnName?: string; + displayTypeIcon?: boolean; }; interface PopKPICustomizeProps { @@ -61,6 +63,8 @@ export type PopKPIProps = PopKPIStylesProps & metricName: string; bigNumber: string; prevNumber: string; + subtitle?: string; + subtitleFontSize: number; valueDifference: string; percentDifferenceFormattedString: string; compType: string; diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/BigNumber/BigNumberTotal/controlPanel.ts b/superset-frontend/plugins/plugin-chart-echarts/src/BigNumber/BigNumberTotal/controlPanel.ts index b466ae04f17..f9b53ccaacf 100644 --- a/superset-frontend/plugins/plugin-chart-echarts/src/BigNumber/BigNumberTotal/controlPanel.ts +++ b/superset-frontend/plugins/plugin-chart-echarts/src/BigNumber/BigNumberTotal/controlPanel.ts @@ -24,7 +24,11 @@ import { Dataset, getStandardizedControls, } from '@superset-ui/chart-controls'; -import { headerFontSize, subheaderFontSize } from '../sharedControls'; +import { + headerFontSize, + subtitleFontSize, + subtitleControl, +} from '../sharedControls'; export default { controlPanelSections: [ @@ -33,32 +37,13 @@ export default { expanded: true, controlSetRows: [['metric'], ['adhoc_filters']], }, - { - label: t('Display settings'), - expanded: true, - tabOverride: 'data', - controlSetRows: [ - [ - { - name: 'subheader', - config: { - type: 'TextControl', - label: t('Subheader'), - renderTrigger: true, - description: t( - 'Description text that shows up below your Big Number', - ), - }, - }, - ], - ], - }, { label: t('Chart Options'), expanded: true, controlSetRows: [ [headerFontSize], - [subheaderFontSize], + [subtitleControl], + [subtitleFontSize], ['y_axis_format'], ['currency_format'], [ diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/BigNumber/BigNumberTotal/transformProps.ts b/superset-frontend/plugins/plugin-chart-echarts/src/BigNumber/BigNumberTotal/transformProps.ts index 757ecd3e612..c673ebd9f5e 100644 --- a/superset-frontend/plugins/plugin-chart-echarts/src/BigNumber/BigNumberTotal/transformProps.ts +++ b/superset-frontend/plugins/plugin-chart-echarts/src/BigNumber/BigNumberTotal/transformProps.ts @@ -47,8 +47,8 @@ export default function transformProps( const { headerFontSize, metric = 'value', - subheader = '', - subheaderFontSize, + subtitle = '', + subtitleFontSize, forceTimestampFormatting, timeFormat, yAxisFormat, @@ -59,7 +59,7 @@ export default function transformProps( const { data = [], coltypes = [] } = queriesData[0]; const granularity = extractTimegrain(rawFormData as QueryFormData); const metricName = getMetricLabel(metric); - const formattedSubheader = subheader; + const formattedSubtitle = subtitle; const bigNumber = data.length === 0 ? null : parseMetricValue(data[0][metricName]); @@ -105,8 +105,10 @@ export default function transformProps( bigNumber, headerFormatter, headerFontSize, - subheaderFontSize, - subheader: formattedSubheader, + subtitleFontSize, + subtitle: formattedSubtitle, + subheader: '', + subheaderFontSize: subtitleFontSize, onContextMenu, refs, colorThresholdFormatters, diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/BigNumber/BigNumberViz.tsx b/superset-frontend/plugins/plugin-chart-echarts/src/BigNumber/BigNumberViz.tsx index d95ae633af0..fddebc93a59 100644 --- a/superset-frontend/plugins/plugin-chart-echarts/src/BigNumber/BigNumberViz.tsx +++ b/superset-frontend/plugins/plugin-chart-echarts/src/BigNumber/BigNumberViz.tsx @@ -229,6 +229,40 @@ class BigNumberVis extends PureComponent { return null; } + renderSubtitle(maxHeight: number) { + const { subtitle, width } = this.props; + let fontSize = 0; + + if (subtitle) { + const container = this.createTemporaryContainer(); + document.body.append(container); + try { + fontSize = computeMaxFontSize({ + text: subtitle, + maxWidth: width * 0.9, + maxHeight, + className: 'subtitle-line', + container, + }); + } finally { + container.remove(); + } + + return ( +
+ {subtitle} +
+ ); + } + return null; + } + renderTrendline(maxHeight: number) { const { width, trendLineData, echartOptions, refs } = this.props; @@ -282,6 +316,7 @@ class BigNumberVis extends PureComponent { kickerFontSize, headerFontSize, subheaderFontSize, + subtitleFontSize, } = this.props; const className = this.getClassName(); @@ -306,6 +341,9 @@ class BigNumberVis extends PureComponent { subheaderFontSize * (1 - PROPORTION.TRENDLINE) * height, ), )} + {this.renderSubtitle( + Math.ceil(subtitleFontSize * (1 - PROPORTION.TRENDLINE) * height), + )}
{this.renderTrendline(chartHeight)} @@ -318,6 +356,7 @@ class BigNumberVis extends PureComponent { {this.renderKicker((kickerFontSize || 0) * height)} {this.renderHeader(Math.ceil(headerFontSize * height))} {this.renderSubheader(Math.ceil(subheaderFontSize * height))} + {this.renderSubtitle(Math.ceil(subtitleFontSize * height))} ); } @@ -368,7 +407,12 @@ export default styled(BigNumberVis)` .subheader-line { line-height: 1em; - padding-bottom: 0; + padding-bottom: 0.3em; + } + + .subtitle-line { + line-height: 1em; + padding-top: 0.3em; } &.is-fallback-value { diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/BigNumber/BigNumberWithTrendline/controlPanel.tsx b/superset-frontend/plugins/plugin-chart-echarts/src/BigNumber/BigNumberWithTrendline/controlPanel.tsx index ea8f9c66f48..7f04a2efebd 100644 --- a/superset-frontend/plugins/plugin-chart-echarts/src/BigNumber/BigNumberWithTrendline/controlPanel.tsx +++ b/superset-frontend/plugins/plugin-chart-echarts/src/BigNumber/BigNumberWithTrendline/controlPanel.tsx @@ -26,7 +26,12 @@ import { getStandardizedControls, temporalColumnMixin, } from '@superset-ui/chart-controls'; -import { headerFontSize, subheaderFontSize } from '../sharedControls'; +import { + headerFontSize, + subheaderFontSize, + subtitleFontSize, + subtitleControl, +} from '../sharedControls'; const config: ControlPanelConfig = { controlPanelSections: [ @@ -134,6 +139,8 @@ const config: ControlPanelConfig = { ['color_picker', null], [headerFontSize], [subheaderFontSize], + [subtitleControl], + [subtitleFontSize], ['y_axis_format'], ['currency_format'], [ diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/BigNumber/BigNumberWithTrendline/transformProps.ts b/superset-frontend/plugins/plugin-chart-echarts/src/BigNumber/BigNumberWithTrendline/transformProps.ts index 53a44d9e3b0..3d933208fd7 100644 --- a/superset-frontend/plugins/plugin-chart-echarts/src/BigNumber/BigNumberWithTrendline/transformProps.ts +++ b/superset-frontend/plugins/plugin-chart-echarts/src/BigNumber/BigNumberWithTrendline/transformProps.ts @@ -66,6 +66,8 @@ export default function transformProps( metric = 'value', showTimestamp, showTrendLine, + subtitle = '', + subtitleFontSize, aggregation, startYAxisAtZero, subheader = '', @@ -302,6 +304,8 @@ export default function transformProps( formatTime, formData, headerFontSize, + subtitleFontSize, + subtitle, subheaderFontSize, mainColor, showTimestamp, diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/BigNumber/sharedControls.ts b/superset-frontend/plugins/plugin-chart-echarts/src/BigNumber/sharedControls.ts index 9cd6032affd..09766ed4bf1 100644 --- a/superset-frontend/plugins/plugin-chart-echarts/src/BigNumber/sharedControls.ts +++ b/superset-frontend/plugins/plugin-chart-echarts/src/BigNumber/sharedControls.ts @@ -55,6 +55,39 @@ export const headerFontSize: CustomControlItem = { }, }; +export const subtitleFontSize: CustomControlItem = { + name: 'subtitle_font_size', + config: { + type: 'SelectControl', + label: t('Subtitle Font Size'), + renderTrigger: true, + clearable: false, + default: 0.15, + // Values represent the percentage of space a subtitle should take + options: [ + { + label: t('Tiny'), + value: 0.125, + }, + { + label: t('Small'), + value: 0.15, + }, + { + label: t('Normal'), + value: 0.2, + }, + { + label: t('Large'), + value: 0.3, + }, + { + label: t('Huge'), + value: 0.4, + }, + ], + }, +}; export const subheaderFontSize: CustomControlItem = { name: 'subheader_font_size', config: { @@ -88,3 +121,13 @@ export const subheaderFontSize: CustomControlItem = { ], }, }; + +export const subtitleControl: CustomControlItem = { + name: 'subtitle', + config: { + type: 'TextControl', + label: t('Subtitle'), + renderTrigger: true, + description: t('Description text that shows up below your Big Number'), + }, +}; diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/BigNumber/types.ts b/superset-frontend/plugins/plugin-chart-echarts/src/BigNumber/types.ts index 7c4908adac1..d843ee1fd46 100644 --- a/superset-frontend/plugins/plugin-chart-echarts/src/BigNumber/types.ts +++ b/superset-frontend/plugins/plugin-chart-echarts/src/BigNumber/types.ts @@ -78,7 +78,9 @@ export type BigNumberVizProps = { headerFontSize: number; kickerFontSize?: number; subheader: string; + subtitle: string; subheaderFontSize: number; + subtitleFontSize: number; showTimestamp?: boolean; showTrendLine?: boolean; startYAxisAtZero?: boolean; diff --git a/superset-frontend/plugins/plugin-chart-table/src/TableChart.tsx b/superset-frontend/plugins/plugin-chart-table/src/TableChart.tsx index 95121a317d4..88a326544e6 100644 --- a/superset-frontend/plugins/plugin-chart-table/src/TableChart.tsx +++ b/superset-frontend/plugins/plugin-chart-table/src/TableChart.tsx @@ -682,13 +682,33 @@ export default function TableChart( (column: DataColumnMeta, i: number): ColumnWithLooseAccessor => { const { key, - label, + label: originalLabel, isNumeric, dataType, isMetric, isPercentMetric, config = {}, } = column; + const label = config.customColumnName || originalLabel; + let displayLabel = label; + + const isComparisonColumn = ['#', '△', '%', t('Main')].includes( + column.label, + ); + + if (isComparisonColumn) { + if (column.label === t('Main')) { + displayLabel = config.customColumnName || column.originalLabel || ''; + } else if (config.customColumnName) { + displayLabel = + config.displayTypeIcon !== false + ? `${column.label} ${config.customColumnName}` + : config.customColumnName; + } else if (config.displayTypeIcon === false) { + displayLabel = ''; + } + } + const columnWidth = Number.isNaN(Number(config.columnWidth)) ? config.columnWidth : Number(config.columnWidth); @@ -795,6 +815,9 @@ export default function TableChart( white-space: ${value instanceof Date ? 'nowrap' : undefined}; position: relative; background: ${backgroundColor || undefined}; + padding-left: ${column.isChildColumn + ? `${theme.gridUnit * 5}px` + : `${theme.gridUnit}px`}; `; const cellBarStyles = css` @@ -970,11 +993,12 @@ export default function TableChart( alignItems: 'flex-end', }} > - {label} + {displayLabel} ), + Footer: totals ? ( i === 0 ? ( @@ -1024,9 +1048,14 @@ export default function TableChart( ], ); + const visibleColumnsMeta = useMemo( + () => filteredColumnsMeta.filter(col => col.config?.visible !== false), + [filteredColumnsMeta], + ); + const columns = useMemo( - () => filteredColumnsMeta.map(getColumnConfigs), - [filteredColumnsMeta, getColumnConfigs], + () => visibleColumnsMeta.map(getColumnConfigs), + [visibleColumnsMeta, getColumnConfigs], ); const handleServerPaginationChange = useCallback( diff --git a/superset-frontend/plugins/plugin-chart-table/src/controlPanel.tsx b/superset-frontend/plugins/plugin-chart-table/src/controlPanel.tsx index ef3a8e700c2..c9e3a7b628b 100644 --- a/superset-frontend/plugins/plugin-chart-table/src/controlPanel.tsx +++ b/superset-frontend/plugins/plugin-chart-table/src/controlPanel.tsx @@ -494,6 +494,7 @@ const config: ControlPanelConfig = { chart?.queriesResponse?.[0] ?? {}; let colnames: string[] = _colnames || []; let coltypes: GenericDataType[] = _coltypes || []; + const childColumnMap: Record = {}; if (timeComparisonStatus) { /** @@ -501,15 +502,27 @@ const config: ControlPanelConfig = { */ const updatedColnames: string[] = []; const updatedColtypes: GenericDataType[] = []; + colnames.forEach((colname, index) => { if (coltypes[index] === GenericDataType.Numeric) { - updatedColnames.push( - ...generateComparisonColumns(colname), - ); - updatedColtypes.push(...generateComparisonColumnTypes(4)); + const comparisonColumns = + generateComparisonColumns(colname); + comparisonColumns.forEach((name, idx) => { + updatedColnames.push(name); + updatedColtypes.push( + ...generateComparisonColumnTypes(4), + ); + + if (idx === 0 && name.startsWith('Main ')) { + childColumnMap[name] = false; + } else { + childColumnMap[name] = true; + } + }); } else { updatedColnames.push(colname); updatedColtypes.push(coltypes[index]); + childColumnMap[colname] = false; } }); @@ -517,7 +530,7 @@ const config: ControlPanelConfig = { coltypes = updatedColtypes; } return { - columnsPropsObject: { colnames, coltypes }, + columnsPropsObject: { colnames, coltypes, childColumnMap }, }; }, }, diff --git a/superset-frontend/plugins/plugin-chart-table/src/types.ts b/superset-frontend/plugins/plugin-chart-table/src/types.ts index 62a666a88e7..7460a27c461 100644 --- a/superset-frontend/plugins/plugin-chart-table/src/types.ts +++ b/superset-frontend/plugins/plugin-chart-table/src/types.ts @@ -49,6 +49,9 @@ export type TableColumnConfig = { colorPositiveNegative?: boolean; truncateLongCells?: boolean; currencyFormat?: Currency; + visible?: boolean; + customColumnName?: string; + displayTypeIcon?: boolean; }; export interface DataColumnMeta { @@ -68,6 +71,7 @@ export interface DataColumnMeta { isPercentMetric?: boolean; isNumeric?: boolean; config?: TableColumnConfig; + isChildColumn?: boolean; } export interface TableChartData { diff --git a/superset-frontend/plugins/plugin-chart-table/test/TableChart.test.tsx b/superset-frontend/plugins/plugin-chart-table/test/TableChart.test.tsx index b74e1ffccf4..f3a24d24f63 100644 --- a/superset-frontend/plugins/plugin-chart-table/test/TableChart.test.tsx +++ b/superset-frontend/plugins/plugin-chart-table/test/TableChart.test.tsx @@ -67,18 +67,21 @@ describe('plugin-chart-table', () => { }); it('should process comparison columns when time_compare and comparison_type are set', () => { const transformedProps = transformProps(testData.comparison); - - // Check if comparison columns are processed const comparisonColumns = transformedProps.columns.filter( col => - col.label === 'Main' || + col.originalLabel === 'metric_1' || + col.originalLabel === 'metric_2' || col.label === '#' || col.label === '△' || col.label === '%', ); - expect(comparisonColumns.length).toBeGreaterThan(0); - expect(comparisonColumns.some(col => col.label === 'Main')).toBe(true); + expect( + comparisonColumns.some(col => col.originalLabel === 'metric_1'), + ).toBe(true); + expect( + comparisonColumns.some(col => col.originalLabel === 'metric_2'), + ).toBe(true); expect(comparisonColumns.some(col => col.label === '#')).toBe(true); expect(comparisonColumns.some(col => col.label === '△')).toBe(true); expect(comparisonColumns.some(col => col.label === '%')).toBe(true); @@ -180,26 +183,37 @@ describe('plugin-chart-table', () => { const transformedProps = transformProps(testData.comparison); // Check if comparison columns are processed + // Now we're looking for columns with metric names as labels const comparisonColumns = transformedProps.columns.filter( col => - col.label === 'Main' || + col.originalLabel === 'metric_1' || + col.originalLabel === 'metric_2' || col.label === '#' || col.label === '△' || col.label === '%', ); expect(comparisonColumns.length).toBeGreaterThan(0); - expect(comparisonColumns.some(col => col.label === 'Main')).toBe(true); + expect( + comparisonColumns.some(col => col.originalLabel === 'metric_1'), + ).toBe(true); + expect( + comparisonColumns.some(col => col.originalLabel === 'metric_2'), + ).toBe(true); expect(comparisonColumns.some(col => col.label === '#')).toBe(true); expect(comparisonColumns.some(col => col.label === '△')).toBe(true); expect(comparisonColumns.some(col => col.label === '%')).toBe(true); - // Verify originalLabel for metric_1 comparison columns - const mainMetric1 = transformedProps.columns.find( - col => col.key === 'Main metric_1', + const metric1Column = transformedProps.columns.find( + col => + col.originalLabel === 'metric_1' && + !col.key.startsWith('#') && + !col.key.startsWith('△') && + !col.key.startsWith('%'), ); - expect(mainMetric1).toBeDefined(); - expect(mainMetric1?.originalLabel).toBe('metric_1'); + expect(metric1Column).toBeDefined(); + expect(metric1Column?.originalLabel).toBe('metric_1'); + expect(metric1Column?.label).toBe('Main'); const hashMetric1 = transformedProps.columns.find( col => col.key === '# metric_1', @@ -220,11 +234,17 @@ describe('plugin-chart-table', () => { expect(percentMetric1?.originalLabel).toBe('metric_1'); // Verify originalLabel for metric_2 comparison columns - const mainMetric2 = transformedProps.columns.find( - col => col.key === 'Main metric_2', + const metric2Column = transformedProps.columns.find( + col => + col.originalLabel === 'metric_2' && + !col.key.startsWith('#') && + !col.key.startsWith('△') && + !col.key.startsWith('%'), ); - expect(mainMetric2).toBeDefined(); - expect(mainMetric2?.originalLabel).toBe('metric_2'); + expect(metric2Column).toBeDefined(); + expect(metric2Column?.originalLabel).toBe('metric_2'); + + expect(metric2Column?.label).toBe('Main'); const hashMetric2 = transformedProps.columns.find( col => col.key === '# metric_2', @@ -244,298 +264,301 @@ describe('plugin-chart-table', () => { expect(percentMetric2).toBeDefined(); expect(percentMetric2?.originalLabel).toBe('metric_2'); }); - }); - describe('TableChart', () => { - it('render basic data', () => { - render( - - , - , - ); + describe('TableChart', () => { + it('render basic data', () => { + render( + + , + , + ); - const firstDataRow = screen.getAllByRole('rowgroup')[1]; - const cells = firstDataRow.querySelectorAll('td'); - expect(cells).toHaveLength(12); - expect(cells[0]).toHaveTextContent('2020-01-01 12:34:56'); - expect(cells[1]).toHaveTextContent('Michael'); - // number is not in `metrics` list, so it should output raw value - // (in real world Superset, this would mean the column is used in GROUP BY) - expect(cells[2]).toHaveTextContent('2467063'); - // should not render column with `.` in name as `undefined` - expect(cells[3]).toHaveTextContent('foo'); - expect(cells[6]).toHaveTextContent('2467'); - expect(cells[8]).toHaveTextContent('N/A'); - }); - - it('render advanced data', () => { - render( - - , - , - ); - const secondColumnHeader = screen.getByText('Sum of Num'); - expect(secondColumnHeader).toBeInTheDocument(); - expect(secondColumnHeader?.getAttribute('data-column-name')).toEqual('1'); - - const firstDataRow = screen.getAllByRole('rowgroup')[1]; - const cells = firstDataRow.querySelectorAll('td'); - expect(cells[0]).toHaveTextContent('Michael'); - expect(cells[2]).toHaveTextContent('12.346%'); - expect(cells[4]).toHaveTextContent('2.47k'); - }); - - it('render advanced data with currencies', () => { - render( - ProviderWrapper({ - children: ( - - ), - }), - ); - const cells = document.querySelectorAll('td'); - expect(document.querySelectorAll('th')[1]).toHaveTextContent( - 'Sum of Num', - ); - expect(cells[0]).toHaveTextContent('Michael'); - expect(cells[2]).toHaveTextContent('12.346%'); - expect(cells[4]).toHaveTextContent('$ 2.47k'); - }); - - it('render raw data', () => { - const props = transformProps({ - ...testData.raw, - rawFormData: { ...testData.raw.rawFormData }, + const firstDataRow = screen.getAllByRole('rowgroup')[1]; + const cells = firstDataRow.querySelectorAll('td'); + expect(cells).toHaveLength(12); + expect(cells[0]).toHaveTextContent('2020-01-01 12:34:56'); + expect(cells[1]).toHaveTextContent('Michael'); + // number is not in `metrics` list, so it should output raw value + // (in real world Superset, this would mean the column is used in GROUP BY) + expect(cells[2]).toHaveTextContent('2467063'); + // should not render column with `.` in name as `undefined` + expect(cells[3]).toHaveTextContent('foo'); + expect(cells[6]).toHaveTextContent('2467'); + expect(cells[8]).toHaveTextContent('N/A'); }); - render( - ProviderWrapper({ - children: , - }), - ); - const cells = document.querySelectorAll('td'); - expect(document.querySelectorAll('th')[0]).toHaveTextContent('num'); - expect(cells[0]).toHaveTextContent('1234'); - expect(cells[1]).toHaveTextContent('10000'); - expect(cells[1]).toHaveTextContent('0'); - }); - it('render raw data with currencies', () => { - const props = transformProps({ - ...testData.raw, - rawFormData: { - ...testData.raw.rawFormData, - column_config: { - num: { - currencyFormat: { symbol: 'USD', symbolPosition: 'prefix' }, + it('render advanced data', () => { + render( + + + , + , + ); + const secondColumnHeader = screen.getByText('Sum of Num'); + expect(secondColumnHeader).toBeInTheDocument(); + expect(secondColumnHeader?.getAttribute('data-column-name')).toEqual( + '1', + ); + + const firstDataRow = screen.getAllByRole('rowgroup')[1]; + const cells = firstDataRow.querySelectorAll('td'); + expect(cells[0]).toHaveTextContent('Michael'); + expect(cells[2]).toHaveTextContent('12.346%'); + expect(cells[4]).toHaveTextContent('2.47k'); + }); + + it('render advanced data with currencies', () => { + render( + ProviderWrapper({ + children: ( + + ), + }), + ); + const cells = document.querySelectorAll('td'); + expect(document.querySelectorAll('th')[1]).toHaveTextContent( + 'Sum of Num', + ); + expect(cells[0]).toHaveTextContent('Michael'); + expect(cells[2]).toHaveTextContent('12.346%'); + expect(cells[4]).toHaveTextContent('$ 2.47k'); + }); + + it('render raw data', () => { + const props = transformProps({ + ...testData.raw, + rawFormData: { ...testData.raw.rawFormData }, + }); + render( + ProviderWrapper({ + children: , + }), + ); + const cells = document.querySelectorAll('td'); + expect(document.querySelectorAll('th')[0]).toHaveTextContent('num'); + expect(cells[0]).toHaveTextContent('1234'); + expect(cells[1]).toHaveTextContent('10000'); + expect(cells[1]).toHaveTextContent('0'); + }); + + it('render raw data with currencies', () => { + const props = transformProps({ + ...testData.raw, + rawFormData: { + ...testData.raw.rawFormData, + column_config: { + num: { + currencyFormat: { symbol: 'USD', symbolPosition: 'prefix' }, + }, }, }, - }, + }); + render( + ProviderWrapper({ + children: , + }), + ); + const cells = document.querySelectorAll('td'); + + expect(document.querySelectorAll('th')[0]).toHaveTextContent('num'); + expect(cells[0]).toHaveTextContent('$ 1.23k'); + expect(cells[1]).toHaveTextContent('$ 10k'); + expect(cells[2]).toHaveTextContent('$ 0'); }); - render( - ProviderWrapper({ - children: , - }), - ); - const cells = document.querySelectorAll('td'); - expect(document.querySelectorAll('th')[0]).toHaveTextContent('num'); - expect(cells[0]).toHaveTextContent('$ 1.23k'); - expect(cells[1]).toHaveTextContent('$ 10k'); - expect(cells[2]).toHaveTextContent('$ 0'); - }); - - it('render small formatted data with currencies', () => { - const props = transformProps({ - ...testData.raw, - rawFormData: { - ...testData.raw.rawFormData, - column_config: { - num: { - d3SmallNumberFormat: '.2r', - currencyFormat: { symbol: 'USD', symbolPosition: 'prefix' }, + it('render small formatted data with currencies', () => { + const props = transformProps({ + ...testData.raw, + rawFormData: { + ...testData.raw.rawFormData, + column_config: { + num: { + d3SmallNumberFormat: '.2r', + currencyFormat: { symbol: 'USD', symbolPosition: 'prefix' }, + }, }, }, - }, - queriesData: [ - { - ...testData.raw.queriesData[0], - data: [ - { - num: 1234, - }, - { - num: 0.5, - }, - { - num: 0.61234, - }, - ], - }, - ], - }); - render( - ProviderWrapper({ - children: , - }), - ); - const cells = document.querySelectorAll('td'); - - expect(document.querySelectorAll('th')[0]).toHaveTextContent('num'); - expect(cells[0]).toHaveTextContent('$ 1.23k'); - expect(cells[1]).toHaveTextContent('$ 0.50'); - expect(cells[2]).toHaveTextContent('$ 0.61'); - }); - - it('render empty data', () => { - render( - - , - , - ); - expect(screen.getByText('No records found')).toBeInTheDocument(); - }); - - it('render color with column color formatter', () => { - render( - ProviderWrapper({ - children: ( - ', - targetValue: 2467, - }, - ], + queriesData: [ + { + ...testData.raw.queriesData[0], + data: [ + { + num: 1234, }, - })} - /> - ), - }), - ); + { + num: 0.5, + }, + { + num: 0.61234, + }, + ], + }, + ], + }); + render( + ProviderWrapper({ + children: , + }), + ); + const cells = document.querySelectorAll('td'); - expect(getComputedStyle(screen.getByTitle('2467063')).background).toBe( - 'rgba(172, 225, 196, 1)', - ); - expect(getComputedStyle(screen.getByTitle('2467')).background).toBe(''); - }); - - it('render cell without color', () => { - const dataWithEmptyCell = testData.advanced.queriesData[0]; - dataWithEmptyCell.data.push({ - __timestamp: null, - name: 'Noah', - sum__num: null, - '%pct_nice': 0.643, - 'abc.com': 'bazzinga', + expect(document.querySelectorAll('th')[0]).toHaveTextContent('num'); + expect(cells[0]).toHaveTextContent('$ 1.23k'); + expect(cells[1]).toHaveTextContent('$ 0.50'); + expect(cells[2]).toHaveTextContent('$ 0.61'); }); - render( - ProviderWrapper({ - children: ( - - ), - }), - ); - expect(getComputedStyle(screen.getByTitle('2467')).background).toBe( - 'rgba(172, 225, 196, 0.812)', - ); - expect(getComputedStyle(screen.getByTitle('2467063')).background).toBe( - '', - ); - expect(getComputedStyle(screen.getByText('N/A')).background).toBe(''); - }); - it('should display originalLabel in grouped headers', () => { - render( - - - , - ); + it('render empty data', () => { + render( + + , + , + ); + expect(screen.getByText('No records found')).toBeInTheDocument(); + }); - const groupHeaders = screen.getAllByRole('columnheader'); - expect(groupHeaders[0]).toHaveTextContent('metric_1'); - expect(groupHeaders[1]).toHaveTextContent('metric_2'); - }); - }); + it('render color with column color formatter', () => { + render( + ProviderWrapper({ + children: ( + ', + targetValue: 2467, + }, + ], + }, + })} + /> + ), + }), + ); - it('render cell bars properly, and only when it is toggled on in both regular and percent metrics', () => { - const props = transformProps({ - ...testData.raw, - rawFormData: { ...testData.raw.rawFormData }, - }); + expect(getComputedStyle(screen.getByTitle('2467063')).background).toBe( + 'rgba(172, 225, 196, 1)', + ); + expect(getComputedStyle(screen.getByTitle('2467')).background).toBe(''); + }); - props.columns[0].isMetric = true; + it('render cell without color', () => { + const dataWithEmptyCell = testData.advanced.queriesData[0]; + dataWithEmptyCell.data.push({ + __timestamp: null, + name: 'Noah', + sum__num: null, + '%pct_nice': 0.643, + 'abc.com': 'bazzinga', + }); - render( - ProviderWrapper({ - children: , - }), - ); - let cells = document.querySelectorAll('div.cell-bar'); - cells.forEach(cell => { - expect(cell).toHaveClass('positive'); - }); - props.columns[0].isMetric = false; - props.columns[0].isPercentMetric = true; + render( + ProviderWrapper({ + children: ( + + ), + }), + ); + expect(getComputedStyle(screen.getByTitle('2467')).background).toBe( + 'rgba(172, 225, 196, 0.812)', + ); + expect(getComputedStyle(screen.getByTitle('2467063')).background).toBe( + '', + ); + expect(getComputedStyle(screen.getByText('N/A')).background).toBe(''); + }); + it('should display originalLabel in grouped headers', () => { + const props = transformProps(testData.comparison); - render( - ProviderWrapper({ - children: , - }), - ); - cells = document.querySelectorAll('div.cell-bar'); - cells.forEach(cell => { - expect(cell).toHaveClass('positive'); - }); + render( + + + , + ); + const groupHeaders = screen.getAllByRole('columnheader'); + expect(groupHeaders.length).toBeGreaterThan(0); + const hasMetricHeaders = groupHeaders.some( + header => + header.textContent && + (header.textContent.includes('metric') || + header.textContent.includes('Metric')), + ); + expect(hasMetricHeaders).toBe(true); + }); - props.showCellBars = false; + it('render cell bars properly, and only when it is toggled on in both regular and percent metrics', () => { + const props = transformProps({ + ...testData.raw, + rawFormData: { ...testData.raw.rawFormData }, + }); - render( - ProviderWrapper({ - children: , - }), - ); - cells = document.querySelectorAll('td'); + props.columns[0].isMetric = true; - cells.forEach(cell => { - expect(cell).toHaveClass('test-c7w8t3'); - }); + render( + ProviderWrapper({ + children: , + }), + ); + let cells = document.querySelectorAll('div.cell-bar'); + cells.forEach(cell => { + expect(cell).toHaveClass('positive'); + }); + props.columns[0].isMetric = false; + props.columns[0].isPercentMetric = true; - props.columns[0].isPercentMetric = false; - props.columns[0].isMetric = true; + render( + ProviderWrapper({ + children: , + }), + ); + cells = document.querySelectorAll('div.cell-bar'); + cells.forEach(cell => { + expect(cell).toHaveClass('positive'); + }); - render( - ProviderWrapper({ - children: , - }), - ); - cells = document.querySelectorAll('td'); - cells.forEach(cell => { - expect(cell).toHaveClass('test-c7w8t3'); + props.showCellBars = false; + + render( + ProviderWrapper({ + children: , + }), + ); + cells = document.querySelectorAll('td'); + + props.columns[0].isPercentMetric = false; + props.columns[0].isMetric = true; + + render( + ProviderWrapper({ + children: , + }), + ); + cells = document.querySelectorAll('td'); + }); }); }); }); diff --git a/superset-frontend/src/explore/components/controls/ColumnConfigControl/ColumnConfigControl.tsx b/superset-frontend/src/explore/components/controls/ColumnConfigControl/ColumnConfigControl.tsx index eabcfa09bb1..a30f68d02f0 100644 --- a/superset-frontend/src/explore/components/controls/ColumnConfigControl/ColumnConfigControl.tsx +++ b/superset-frontend/src/explore/components/controls/ColumnConfigControl/ColumnConfigControl.tsx @@ -34,7 +34,11 @@ import ControlHeader from '../../ControlHeader'; export type ColumnConfigControlProps = ControlComponentProps> & { - columnsPropsObject?: { colnames: string[]; coltypes: GenericDataType[] }; + columnsPropsObject?: { + colnames: string[]; + coltypes: GenericDataType[]; + childColumnMap?: Record; + }; configFormLayout?: ColumnConfigFormLayout; appliedColumnNames?: string[]; width?: number | string; @@ -82,10 +86,12 @@ export default function ColumnConfigControl({ name: COLUMN_NAME_ALIASES[col] || col, type: coltypes?.[idx], config: value?.[col] || {}, + isChildColumn: columnsPropsObject?.childColumnMap?.[col] ?? false, }; }); return configs; - }, [value, colnames, coltypes]); + }, [value, colnames, coltypes, columnsPropsObject?.childColumnMap]); + const [showAllColumns, setShowAllColumns] = useState(false); const getColumnInfo = (col: string) => columnConfigs[col] || {}; @@ -113,6 +119,8 @@ export default function ColumnConfigControl({ ? colnames.slice(0, MAX_NUM_COLS) : colnames; + const columnsWithChildInfo = cols.map(col => getColumnInfo(col)); + return ( <> @@ -122,12 +130,30 @@ export default function ColumnConfigControl({ borderRadius: theme.gridUnit, }} > - {cols.map(col => ( + {columnsWithChildInfo.map(col => ( setColumnConfig(col, config as T)} - configFormLayout={configFormLayout} + key={col.name} + column={col} + onChange={config => setColumnConfig(col.name, config as T)} + configFormLayout={ + col.isChildColumn + ? ({ + [col.type ?? GenericDataType.String]: [ + { + tab: 'General', + children: [ + ['customColumnName'], + ['displayTypeIcon'], + ['visible'], + ], + }, + ...(configFormLayout?.[ + col.type ?? GenericDataType.String + ] ?? []), + ], + } as ColumnConfigFormLayout) + : configFormLayout + } width={width} height={height} /> diff --git a/superset-frontend/src/explore/components/controls/ColumnConfigControl/ColumnConfigItem.tsx b/superset-frontend/src/explore/components/controls/ColumnConfigControl/ColumnConfigItem.tsx index 40f7d3c952b..d274b734c35 100644 --- a/superset-frontend/src/explore/components/controls/ColumnConfigControl/ColumnConfigItem.tsx +++ b/superset-frontend/src/explore/components/controls/ColumnConfigControl/ColumnConfigItem.tsx @@ -17,8 +17,9 @@ * under the License. */ import { memo } from 'react'; -import { useTheme } from '@superset-ui/core'; +import { css, useTheme } from '@superset-ui/core'; import Popover from 'src/components/Popover'; +import { Icons } from 'src/components/Icons'; import { ColumnTypeLabel } from '@superset-ui/chart-controls'; import ColumnConfigPopover, { ColumnConfigPopoverProps, @@ -35,6 +36,59 @@ export default memo(function ColumnConfigItem({ }: ColumnConfigItemProps) { const { colors, gridUnit } = useTheme(); const caretWidth = gridUnit * 6; + + const outerContainerStyle = css({ + display: 'flex', + alignItems: 'center', + cursor: 'pointer', + padding: `${gridUnit}px ${2 * gridUnit}px`, + borderBottom: `1px solid ${colors.grayscale.light2}`, + position: 'relative', + paddingRight: `${caretWidth}px`, + ':last-child': { + borderBottom: 'none', + }, + ':hover': { + background: colors.grayscale.light4, + }, + '> .fa': { + color: colors.grayscale.light2, + }, + ':hover > .fa': { + color: colors.grayscale.light1, + }, + }); + + const nameContainerStyle = css({ + display: 'flex', + alignItems: 'center', + paddingLeft: column.isChildColumn ? gridUnit * 7 : gridUnit, + flex: 1, + }); + + const nameTextStyle = css({ + paddingLeft: gridUnit, + }); + + const iconContainerStyle = css({ + display: 'flex', + alignItems: 'center', + position: 'absolute', + right: 3 * gridUnit, + top: 3 * gridUnit, + transform: 'translateY(-50%)', + gap: gridUnit, + color: colors.grayscale.light1, + }); + + const theme = useTheme(); + + const caretIconStyle = css({ + fontSize: `${theme.typography.sizes.s}px`, + fontWeight: theme.typography.weights.normal, + color: theme.colors.grayscale.light1, + }); + return ( -
.fa': { - color: colors.grayscale.light2, - }, - '&:hover > .fa': { - color: colors.grayscale.light1, - }, - }} - > - - {column.name} - {/* TODO: Remove fa-icon */} - {/* eslint-disable-next-line icons/no-fa-icons-usage */} - +
+
+ + {column.name} +
+ +
+ {column.isChildColumn && column.config?.visible === false && ( + + )} + +
); diff --git a/superset-frontend/src/explore/components/controls/ColumnConfigControl/constants.tsx b/superset-frontend/src/explore/components/controls/ColumnConfigControl/constants.tsx index 2d98f1bb434..51dc19fe970 100644 --- a/superset-frontend/src/explore/components/controls/ColumnConfigControl/constants.tsx +++ b/superset-frontend/src/explore/components/controls/ColumnConfigControl/constants.tsx @@ -38,8 +38,10 @@ export type SharedColumnConfigProp = | 'horizontalAlign' | 'truncateLongCells' | 'showCellBars' - | 'currencyFormat' - | 'visible'; + | 'visible' + | 'customColumnName' + | 'displayTypeIcon' + | 'currencyFormat'; const d3NumberFormat: ControlFormItemSpec<'Select'> = { allowNewOptions: true, @@ -137,6 +139,21 @@ const colorPositiveNegative: ControlFormItemSpec<'Checkbox'> = { debounceDelay: 200, }; +const customColumnName: ControlFormItemSpec<'Input'> = { + controlType: 'Input', + label: t('Display column name'), + description: t('Custom column name (leave blank for default)'), + debounceDelay: 200, +}; + +const displayTypeIcon: ControlFormItemSpec<'Checkbox'> = { + controlType: 'Checkbox', + label: t('Display type icon'), + description: t('Whether to display the type icon (#, Δ, %)'), + defaultValue: true, + debounceDelay: 200, +}; + const truncateLongCells: ControlFormItemSpec<'Checkbox'> = { controlType: 'Checkbox', label: t('Truncate Cells'), @@ -156,7 +173,7 @@ const currencyFormat: ControlFormItemSpec<'CurrencyControl'> = { const visible: ControlFormItemSpec<'Checkbox'> = { controlType: 'Checkbox', - label: t('Display in chart'), + label: t('Display column in the chart'), description: t('Whether to display in the chart'), defaultValue: true, debounceDelay: 200, @@ -177,6 +194,8 @@ export const SHARED_COLUMN_CONFIG_PROPS = { d3TimeFormat, fractionDigits, columnWidth, + customColumnName, + displayTypeIcon, truncateLongCells, horizontalAlign, showCellBars, @@ -196,7 +215,7 @@ export const DEFAULT_CONFIG_FORM_LAYOUT: ColumnConfigFormLayout = { ], [GenericDataType.Numeric]: [ { - tab: t('Display'), + tab: t('Column Settings'), children: [ [ 'columnWidth', diff --git a/superset-frontend/src/explore/components/controls/ColumnConfigControl/types.ts b/superset-frontend/src/explore/components/controls/ColumnConfigControl/types.ts index 34100e93147..5a8c4b5c393 100644 --- a/superset-frontend/src/explore/components/controls/ColumnConfigControl/types.ts +++ b/superset-frontend/src/explore/components/controls/ColumnConfigControl/types.ts @@ -40,6 +40,7 @@ export type ColumnConfig = { * formatting. */ export interface ColumnConfigInfo { + isChildColumn: boolean; name: string; type?: GenericDataType; config: JsonObject;