diff --git a/.changeset/number-tile-duration-factor-on-output-switch.md b/.changeset/number-tile-duration-factor-on-output-switch.md new file mode 100644 index 0000000000..529c5727f7 --- /dev/null +++ b/.changeset/number-tile-duration-factor-on-output-switch.md @@ -0,0 +1,11 @@ +--- +"@hyperdx/app": patch +--- + +fix(dashboards): seed the duration unit when switching a number tile to Duration + +Switching the Output format to Duration (or Time) on a number tile now seeds the +input unit from the datasource precision. Previously the unit stayed on Seconds, +so a nanosecond trace Duration value was read as seconds (a 367ms value showed as +roughly 11.7 years) until the user re-picked Nanoseconds by hand. Tiles whose +source has no detected duration precision keep the existing behavior. diff --git a/packages/app/src/components/ChartDisplaySettingsDrawer.tsx b/packages/app/src/components/ChartDisplaySettingsDrawer.tsx index 6c6cab067c..01b1d9445c 100644 --- a/packages/app/src/components/ChartDisplaySettingsDrawer.tsx +++ b/packages/app/src/components/ChartDisplaySettingsDrawer.tsx @@ -195,6 +195,18 @@ export default function ChartDisplaySettingsDrawer({ const showBackgroundChart = displayType === DisplayType.Number; const isBackgroundChartDisabled = configType === 'sql'; + // When the datasource auto-detects a time-based format (e.g. a trace Duration + // column, factor 1e-9 for nanoseconds), seed that factor if the user switches + // the output to duration / time in the format control. Without it the prior + // factor sticks and a nanosecond value renders as seconds (367,700,000 ns + // read as ~11.7y). Only meaningful when the detected default is itself + // time-based; otherwise there is no source precision to seed from. + const preferredDurationFactor = + defaultNumberFormat?.output === 'duration' || + defaultNumberFormat?.output === 'time' + ? defaultNumberFormat.factor + : undefined; + return ( diff --git a/packages/app/src/components/NumberFormat.tsx b/packages/app/src/components/NumberFormat.tsx index 4827774e8c..3167c42718 100644 --- a/packages/app/src/components/NumberFormat.tsx +++ b/packages/app/src/components/NumberFormat.tsx @@ -157,7 +157,15 @@ export const NumberFormatForm: React.FC<{ control: Control>; setValue: UseFormSetValue>; disclaimer?: React.ReactNode; -}> = ({ control, setValue, disclaimer }) => { + /** + * Source-derived duration factor (e.g. 1e-9 for a nanosecond trace Duration + * column). When set, switching the output to a time-based format (duration or + * time) seeds `numberFormat.factor` from it instead of keeping the prior + * factor, so the value is read with the right input unit. Left undefined for + * sources with no detected duration precision, where the factor is untouched. + */ + preferredDurationFactor?: number; +}> = ({ control, setValue, disclaimer, preferredDurationFactor }) => { const format = useWatch({ control, name: 'numberFormat' }) ?? DEFAULT_NUMBER_FORMAT; @@ -198,6 +206,17 @@ export const NumberFormatForm: React.FC<{ 'numberFormat.numericUnit', DEFAULT_NUMERIC_UNIT_BY_OUTPUT[newOutput] ?? undefined, ); + // Time-based formats interpret the value through `factor` + // (input unit to seconds). Seed it from the source-derived + // precision so a nanosecond Duration column is not misread as + // seconds; otherwise the prior factor (often Seconds = 1) + // sticks and the user has to re-pick the unit by hand. + if ( + (newOutput === 'duration' || newOutput === 'time') && + preferredDurationFactor != null + ) { + setValue('numberFormat.factor', preferredDurationFactor); + } }} /> )} diff --git a/packages/app/src/components/__tests__/ChartDisplaySettingsDrawer.test.tsx b/packages/app/src/components/__tests__/ChartDisplaySettingsDrawer.test.tsx index 365cc5cba7..6493989799 100644 --- a/packages/app/src/components/__tests__/ChartDisplaySettingsDrawer.test.tsx +++ b/packages/app/src/components/__tests__/ChartDisplaySettingsDrawer.test.tsx @@ -347,5 +347,102 @@ describe('ChartDisplaySettingsDrawer', () => { numberFormat: { output: 'currency' }, }); }); + + // Switching the output to Duration on a tile whose source is a nanosecond + // Duration column should seed the factor from the source precision (1e-9), + // not keep the prior Seconds factor (which would read the value as seconds). + it('seeds the duration factor from the source precision when output switches to Duration', async () => { + const onChange = jest.fn(); + const user = userEvent.setup(); + + renderWithMantine( + , + ); + + await user.selectOptions( + screen.getByRole('combobox', { name: /output format/i }), + 'duration', + ); + await user.click(screen.getByRole('button', { name: /apply/i })); + + expect(onChange).toHaveBeenCalledTimes(1); + expect(onChange.mock.calls[0][0].numberFormat).toMatchObject({ + output: 'duration', + factor: 1e-9, + }); + }); + + // With no source-derived duration format (non-duration source), switching + // to Duration must not force a factor; the prior value (Seconds = 1) stays + // and the user picks the input unit manually as before. + it('does not force a factor when no source duration format is available', async () => { + const onChange = jest.fn(); + const user = userEvent.setup(); + + renderWithMantine( + , + ); + + await user.selectOptions( + screen.getByRole('combobox', { name: /output format/i }), + 'duration', + ); + await user.click(screen.getByRole('button', { name: /apply/i })); + + expect(onChange).toHaveBeenCalledTimes(1); + expect(onChange.mock.calls[0][0].numberFormat).toMatchObject({ + output: 'duration', + factor: 1, + }); + }); + + // The Time output reads the value through the same `factor` select as + // Duration, so switching to Time also seeds the source-derived factor. + it('seeds the duration factor when output switches to Time', async () => { + const onChange = jest.fn(); + const user = userEvent.setup(); + + renderWithMantine( + , + ); + + await user.selectOptions( + screen.getByRole('combobox', { name: /output format/i }), + 'time', + ); + await user.click(screen.getByRole('button', { name: /apply/i })); + + expect(onChange).toHaveBeenCalledTimes(1); + expect(onChange.mock.calls[0][0].numberFormat).toMatchObject({ + output: 'time', + factor: 1e-9, + }); + }); }); });