diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/BigNumber/BigNumberTotal/controlPanel.test.ts b/superset-frontend/plugins/plugin-chart-echarts/src/BigNumber/BigNumberTotal/controlPanel.test.ts index 7d8e74156d4d..a71d3520a486 100644 --- a/superset-frontend/plugins/plugin-chart-echarts/src/BigNumber/BigNumberTotal/controlPanel.test.ts +++ b/superset-frontend/plugins/plugin-chart-echarts/src/BigNumber/BigNumberTotal/controlPanel.test.ts @@ -72,6 +72,16 @@ describe('BigNumber Total Control Panel Config', () => { row.some((control: any) => control.name === 'conditional_formatting'), ); expect(conditionalFormattingRow).toBeTruthy(); + + // Check header_alignment control exists in one of the rows + const headerAlignmentRow = thirdSection.find(row => + row.some((control: any) => control.name === 'header_alignment'), + ); + expect(headerAlignmentRow).toBeTruthy(); + expect(headerAlignmentRow![0]).toMatchObject({ + name: 'header_alignment', + config: expect.objectContaining({ default: 'left' }), + }); }); test('should have y_axis_format override with correct label', () => { 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 d926f06de012..906b01e91c98 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 @@ -32,6 +32,7 @@ import { subtitleControl, showMetricNameControl, metricNameFontSizeWithVisibility, + headerAlignmentControl, } from '../sharedControls'; export default { @@ -124,6 +125,7 @@ export default { }, }, ], + [headerAlignmentControl], ], }, ], diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/BigNumber/BigNumberTotal/transformProps.test.ts b/superset-frontend/plugins/plugin-chart-echarts/src/BigNumber/BigNumberTotal/transformProps.test.ts index 00522b9d0a8d..54f5898c9492 100644 --- a/superset-frontend/plugins/plugin-chart-echarts/src/BigNumber/BigNumberTotal/transformProps.test.ts +++ b/superset-frontend/plugins/plugin-chart-echarts/src/BigNumber/BigNumberTotal/transformProps.test.ts @@ -54,6 +54,7 @@ describe('BigNumberTotal transformProps', () => { yAxisFormat: 'SMART_NUMBER', conditionalFormatting: [{ color: 'red', op: '>', value: 0 }], currencyFormat: { symbol: '$', symbolPosition: 'prefix' }, + headerAlignment: 'center', }; const baseDatasource = { @@ -86,6 +87,7 @@ describe('BigNumberTotal transformProps', () => { expect(result.subtitle).toBe(baseFormData.subheader); expect(result.onContextMenu).toBe(onContextMenu); expect(result.refs).toEqual({}); + expect(result.headerAlignment).toBe('center'); // headerFormatter should be set even if there's no data expect(typeof result.headerFormatter).toBe('function'); // colorThresholdFormatters fallback to empty array when getColorFormatters returns falsy 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 3c7c78ecc707..2d7df441c319 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 @@ -63,6 +63,7 @@ export default function transformProps( currencyFormat, subheader, subheaderFontSize, + headerAlignment, } = formData; const refs: Refs = {}; const { @@ -143,5 +144,6 @@ export default function transformProps( metricName: originalLabel, showMetricName, metricNameFontSize, + headerAlignment, }; } diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/BigNumber/BigNumberViz.test.tsx b/superset-frontend/plugins/plugin-chart-echarts/src/BigNumber/BigNumberViz.test.tsx index 659c6b24c667..d01ca1e50024 100644 --- a/superset-frontend/plugins/plugin-chart-echarts/src/BigNumber/BigNumberViz.test.tsx +++ b/superset-frontend/plugins/plugin-chart-echarts/src/BigNumber/BigNumberViz.test.tsx @@ -23,11 +23,15 @@ import { getNumberFormatter, TimeFormatter, } from '@superset-ui/core'; -import { render, fireEvent } from '../../../../spec/helpers/testing-library'; +import { + render, + fireEvent, + waitFor, +} from '../../../../spec/helpers/testing-library'; import BigNumberVis from './BigNumberViz'; import Echart from '../components/Echart'; import { EventHandlers } from '../types'; -import { BigNumberWithTrendlineFormData } from './types'; +import { BigNumberWithTrendlineFormData, HeaderAlignment } from './types'; jest.mock('../components/Echart', () => ({ __esModule: true, @@ -303,3 +307,147 @@ describe('BigNumberViz trendline and subheader rendering', () => { expect(container.querySelector('.subheader-line')).not.toBeInTheDocument(); }); }); + +describe('BigNumberViz alignment', () => { + const renderViz = (headerAlignment?: HeaderAlignment) => + render( + , + ).container; + + test('defaults to left alignment', () => { + const container = renderViz(); + + const textContainer = container.querySelector('.text-container'); + expect(container.firstElementChild).toHaveStyle({ + alignItems: 'flex-start', + }); + expect(textContainer).toHaveStyle({ alignItems: 'flex-start' }); + const headerLine = container.querySelector('.header-line'); + expect(headerLine).toHaveStyle({ justifyContent: 'flex-start' }); + }); + + test('centers content when headerAlignment is "center"', () => { + const container = renderViz('center'); + + const textContainer = container.querySelector('.text-container'); + expect(container.firstElementChild).toHaveStyle({ alignItems: 'center' }); + expect(textContainer).toHaveStyle({ alignItems: 'center' }); + const headerLine = container.querySelector('.header-line'); + expect(headerLine).toHaveStyle({ justifyContent: 'center' }); + }); + + test('right-aligns content when headerAlignment is "right"', () => { + const container = renderViz('right'); + + const textContainer = container.querySelector('.text-container'); + expect(container.firstElementChild).toHaveStyle({ alignItems: 'flex-end' }); + expect(textContainer).toHaveStyle({ alignItems: 'flex-end' }); + const headerLine = container.querySelector('.header-line'); + expect(headerLine).toHaveStyle({ justifyContent: 'flex-end' }); + }); + + test.each([ + ['center', 'center'], + ['right', 'flex-end'], + ] as const)( + 'aligns the outer chart and text container for "%s" with a trendline', + (headerAlignment, alignItems) => { + const { container } = render( + , + ); + + expect(container.firstElementChild).toHaveStyle({ alignItems }); + expect(container.querySelector('.text-container')).toHaveStyle({ + alignItems, + }); + }, + ); + + test('keeps the value and subheader centered when the trendline layout overflows', async () => { + const offsetHeightDescriptor = Object.getOwnPropertyDescriptor( + HTMLElement.prototype, + 'offsetHeight', + ); + // Force shouldApplyOverflow to report an overflow, which switches + // .text-container from a flex to a block container. + Object.defineProperty(HTMLElement.prototype, 'offsetHeight', { + configurable: true, + get: () => 1000, + }); + + try { + const { container } = render( + , + ); + + await waitFor(() => { + expect(container.querySelector('.text-container')).toHaveStyle({ + display: 'block', + }); + }); + + const headerLine = container.querySelector('.header-line'); + expect(headerLine).toHaveStyle({ justifyContent: 'center' }); + const subheaderLine = container.querySelector('.subheader-line'); + expect(subheaderLine).toHaveStyle({ textAlign: 'center' }); + } finally { + if (offsetHeightDescriptor) { + Object.defineProperty( + HTMLElement.prototype, + 'offsetHeight', + offsetHeightDescriptor, + ); + } else { + delete (HTMLElement.prototype as { offsetHeight?: number }) + .offsetHeight; + } + } + }); +}); 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 ea4e89cd0ee7..c789209aeef0 100644 --- a/superset-frontend/plugins/plugin-chart-echarts/src/BigNumber/BigNumberViz.tsx +++ b/superset-frontend/plugins/plugin-chart-echarts/src/BigNumber/BigNumberViz.tsx @@ -28,12 +28,18 @@ import { } from '@superset-ui/core'; import { styled, useTheme } from '@apache-superset/core/theme'; import Echart from '../components/Echart'; -import { BigNumberVizProps } from './types'; +import { BigNumberVizProps, HeaderAlignment } from './types'; import { PROPORTION } from './constants'; import { EventHandlers } from '../types'; const defaultNumberFormatter = getNumberFormatter(); +const ALIGN_ITEMS: Record = { + left: 'flex-start', + center: 'center', + right: 'flex-end', +}; + function BigNumberVis({ className = '', headerFormatter = defaultNumberFormatter, @@ -47,9 +53,11 @@ function BigNumberVis({ subheader = '', subheaderFontSize = PROPORTION.SUBHEADER, subtitleFontSize = PROPORTION.SUBHEADER, + headerAlignment = 'left', ...props }: BigNumberVizProps) { const theme = useTheme(); + const alignItems = ALIGN_ITEMS[headerAlignment]; // Convert state to hooks const [elementsRendered, setElementsRendered] = useState(false); @@ -132,6 +140,7 @@ function BigNumberVis({ style={{ fontSize, height: 'auto', + textAlign: headerAlignment, }} > {text} @@ -170,6 +179,7 @@ function BigNumberVis({ style={{ fontSize, height: 'auto', + textAlign: headerAlignment, }} > {text} @@ -236,6 +246,9 @@ function BigNumberVis({ style={{ display: 'flex', alignItems: 'center', + // header-line is always a flex container, so text-align has no + // effect on the number's position; justify-content does. + justifyContent: alignItems, fontSize, height: 'auto', color: numberColor, @@ -275,6 +288,7 @@ function BigNumberVis({ style={{ fontSize, height: maxHeight, + textAlign: headerAlignment, }} > {text} @@ -321,6 +335,7 @@ function BigNumberVis({ style={{ fontSize: `${fontSize}px`, height: maxHeight, + textAlign: headerAlignment, }} > {text} @@ -425,11 +440,12 @@ function BigNumberVis({ const overflow = shouldApplyOverflow(allTextHeight); return ( -
+
-
+
{renderFallbackWarning()} {renderMetricName((metricNameFontSize || 0) * height)} {renderKicker((kickerFontSize || 0) * height)} 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 d9d5b85a1cc7..762fe6ad94ca 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 @@ -35,6 +35,7 @@ import { subtitleControl, showMetricNameControl, metricNameFontSizeWithVisibility, + headerAlignmentControl, } from '../sharedControls'; const config: ControlPanelConfig = { @@ -235,6 +236,7 @@ const config: ControlPanelConfig = { }, }, ], + [headerAlignmentControl], ], }, { diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/BigNumber/BigNumberWithTrendline/transformProps.test.ts b/superset-frontend/plugins/plugin-chart-echarts/src/BigNumber/BigNumberWithTrendline/transformProps.test.ts index 225f9eef833d..6dab0f542be7 100644 --- a/superset-frontend/plugins/plugin-chart-echarts/src/BigNumber/BigNumberWithTrendline/transformProps.test.ts +++ b/superset-frontend/plugins/plugin-chart-echarts/src/BigNumber/BigNumberWithTrendline/transformProps.test.ts @@ -113,6 +113,7 @@ describe('BigNumberWithTrendline transformProps', () => { compareSuffix: 'WoW', colorPicker: { r: 0, g: 0, b: 0 }, currencyFormat: { symbol: '$', symbolPosition: 'prefix' }, + headerAlignment: 'right', }; const baseDatasource = { @@ -141,6 +142,7 @@ describe('BigNumberWithTrendline transformProps', () => { ); expect(result.bigNumber).toBeNull(); expect(result.subtitle).toBe('subtitle message'); + expect(result.headerAlignment).toBe('right'); }); test('should calculate subheader as percent change with suffix', () => { 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 fa57d2a8b710..9b4a4640e74d 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 @@ -114,6 +114,7 @@ export default function transformProps( showXAxisMinMaxLabels = false, showYAxis = false, showYAxisMinMaxLabels = false, + headerAlignment, } = formData; const granularity = extractTimegrain(rawFormData); const { @@ -416,5 +417,6 @@ export default function transformProps( onContextMenu, xValueFormatter: formatTime, refs, + headerAlignment, }; } 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 5c7647fa576a..44c2f1d72671 100644 --- a/superset-frontend/plugins/plugin-chart-echarts/src/BigNumber/sharedControls.ts +++ b/superset-frontend/plugins/plugin-chart-echarts/src/BigNumber/sharedControls.ts @@ -113,3 +113,22 @@ export const metricNameFontSizeWithVisibility: CustomControlItem = { resetOnHide: false, }, }; + +export const headerAlignmentControl: CustomControlItem = { + name: 'header_alignment', + config: { + type: 'SelectControl', + label: t('Alignment'), + renderTrigger: true, + clearable: false, + default: 'left', + options: [ + { label: t('Left'), value: 'left' }, + { label: t('Center'), value: 'center' }, + { label: t('Right'), value: 'right' }, + ], + description: t( + 'Horizontal alignment of the metric value and supporting text', + ), + }, +}; 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 d466a1781c85..d069b79219ed 100644 --- a/superset-frontend/plugins/plugin-chart-echarts/src/BigNumber/types.ts +++ b/superset-frontend/plugins/plugin-chart-echarts/src/BigNumber/types.ts @@ -34,10 +34,13 @@ export interface BigNumberDatum { [key: string]: number | string | null; } +export type HeaderAlignment = 'left' | 'center' | 'right'; + export type BigNumberTotalFormData = QueryFormData & { metric?: QueryFormMetric; yAxisFormat?: string; forceTimestampFormatting?: boolean; + headerAlignment?: HeaderAlignment; }; export type BigNumberWithTrendlineFormData = BigNumberTotalFormData & { @@ -107,4 +110,5 @@ export type BigNumberVizProps = { formData?: BigNumberWithTrendlineFormData; refs: Refs; colorThresholdFormatters?: ColorFormatters; + headerAlignment?: HeaderAlignment; }; diff --git a/superset/translations/messages.pot b/superset/translations/messages.pot index 8cbf1347e6b3..d20b7a13ad8d 100644 --- a/superset/translations/messages.pot +++ b/superset/translations/messages.pot @@ -1536,6 +1536,9 @@ msgstr "" msgid "Alerts & reports" msgstr "" +msgid "Alignment" +msgstr "" + msgid "Align +/-" msgstr "" @@ -7209,6 +7212,9 @@ msgstr "" msgid "Horizontal alignment" msgstr "" +msgid "Horizontal alignment of the metric value and supporting text" +msgstr "" + msgid "Host" msgstr ""