From 72c3492cef5ae5e9fdebc3c282797dae747f790b Mon Sep 17 00:00:00 2001 From: Vault Automation Date: Mon, 11 May 2026 10:30:11 -0600 Subject: [PATCH] [UI][VAULT-44837] Update Credential units total to only show 4 decimal places (#14625) (#14711) * Add tests for chart helper * Ensure the decimal places are 4 for credential units total * Add new chart-helpers method * Add jsdoc comment Co-authored-by: Kianna <30884335+kiannaquach@users.noreply.github.com> --- ui/app/components/billing/metric-card.ts | 12 +++- ui/app/components/billing/summary-card.ts | 11 +++- ui/app/utils/chart-helpers.js | 60 ++++++++++++++++++- ui/app/utils/metrics-helpers.ts | 7 +-- ui/tests/unit/utils/chart-helpers-test.js | 70 ++++++++++++++++++++++- 5 files changed, 149 insertions(+), 11 deletions(-) diff --git a/ui/app/components/billing/metric-card.ts b/ui/app/components/billing/metric-card.ts index be969b9cb0..62cdc7c995 100644 --- a/ui/app/components/billing/metric-card.ts +++ b/ui/app/components/billing/metric-card.ts @@ -5,7 +5,7 @@ import Component from '@glimmer/component'; import { toLabel } from 'core/helpers/to-label'; -import { calculateSum } from 'vault/utils/chart-helpers'; +import { calculateSum, toFixedDisplay } from 'vault/utils/chart-helpers'; import { NormalizedBillingMetrics } from 'vault/utils/metrics-helpers'; interface Args { @@ -18,6 +18,16 @@ export default class MetricCard extends Component { get total() { const sums = Object.values(this.args.metrics).filter((metric) => metric !== undefined); + if (this.args.title === 'Credential units') { + const totalCredentialUnits = calculateSum(sums, 4); + + if (typeof totalCredentialUnits === 'number') { + return toFixedDisplay(totalCredentialUnits, 4); + } + + return totalCredentialUnits; + } + return calculateSum(sums); } diff --git a/ui/app/components/billing/summary-card.ts b/ui/app/components/billing/summary-card.ts index bab54c9f32..fed7f086a7 100644 --- a/ui/app/components/billing/summary-card.ts +++ b/ui/app/components/billing/summary-card.ts @@ -7,13 +7,14 @@ import Component from '@glimmer/component'; import { toLabel } from 'core/helpers/to-label'; import { NormalizedMetricsData } from 'vault/vault/billing/overview'; import { NormalizedBillingMetrics } from 'vault/utils/metrics-helpers'; +import { toFixedDisplay } from 'vault/utils/chart-helpers'; interface SummaryMetricInfo { label: string; tooltipText?: string; count?: number; showBadge?: boolean; - total?: number | boolean | undefined; + total?: number | string | boolean | undefined; } interface Args { title: string; @@ -58,7 +59,13 @@ export default class SummaryCard extends Component { summaryMetric = (key: string): SummaryMetricInfo => { if (this.summaryMetricMap?.[key]) { - this.summaryMetricMap[key].total = this.args.normalizedMetricData[key]; + const value = this.args.normalizedMetricData[key]; + // Format CREDENTIAL_UNITS_TOTAL with 4 decimal places to preserve trailing zeros + if (key === NormalizedBillingMetrics.CREDENTIAL_UNITS_TOTAL && typeof value === 'number') { + this.summaryMetricMap[key].total = toFixedDisplay(value, 4); + } else { + this.summaryMetricMap[key].total = value; + } } return this.summaryMetricMap[key] || { label: toLabel([key]) }; diff --git a/ui/app/utils/chart-helpers.js b/ui/app/utils/chart-helpers.js index 6f2689fcd9..a77c3cc760 100644 --- a/ui/app/utils/chart-helpers.js +++ b/ui/app/utils/chart-helpers.js @@ -39,9 +39,65 @@ export function calculateAverage(dataset, objectKey) { return checkIntegers ? Math.round(mean(integers)) : null; } -export function calculateSum(integerArray) { +/** + * Calculates the sum of an array of numbers with optional decimal precision. + * This function fixes floating-point arithmetic errors by rounding to a specified + * number of decimal places. For example, 48.7888 + 0.0112 = 48.800000000000004 + * in JavaScript, but with fixedDecimalPlaces=4, it returns 48.8. + * + * @param {number[]} integerArray - Array of numbers to sum + * @param {number} [fixedDecimalPlaces] - Optional number of decimal places for precision. + * If provided, the sum is rounded to this precision. + * @returns {number|null} Returns the sum as a number, or null if invalid input. + * When fixedDecimalPlaces is provided, returns a number rounded + * to that precision (e.g., 48.8 instead of 48.800000000000004). + * + * @example + * calculateSum([2, 3]) // Returns 5 + * calculateSum([48.7888, 0.0112], 4) // Returns 48.8 (fixes floating-point error) + * calculateSum([73.1832, 0.0168], 4) // Returns 73.2 + * calculateSum([10, 20, 30], 4) // Returns 60 + * calculateSum(['one', 2]) // Returns null (invalid input) + */ +export function calculateSum(integerArray, fixedDecimalPlaces) { if (!Array.isArray(integerArray) || integerArray.some((n) => typeof n !== 'number')) { return null; } - return integerArray.reduce((a, b) => a + b, 0); + + const sum = integerArray.reduce((a, b) => a + b, 0); + + if (typeof fixedDecimalPlaces === 'number' && fixedDecimalPlaces >= 0) { + return parseFloat(sum.toFixed(fixedDecimalPlaces)); + } + + return sum; +} + +/** + * Formats a number for display with a fixed number of decimal places. + * This helper function converts numbers to strings with trailing zeros preserved, + * which is useful for displaying values like "48.8000" instead of "48.8". + * + * @param {number} number - The number to format + * @param {number} decimalPlaces - The number of decimal places to display + * @returns {number|string} Returns the original number if invalid inputs, + * returns 0 as-is for zero values, + * otherwise returns a string with fixed decimal places + * + * @example + * toFixedDisplay(48.8, 4) // Returns "48.8000" + * toFixedDisplay(73.2, 4) // Returns "73.2000" + * toFixedDisplay(0, 4) // Returns 0 (not "0.0000") + * toFixedDisplay(100, 2) // Returns "100.00" + */ +export function toFixedDisplay(number, decimalPlaces) { + if (typeof number !== 'number' || typeof decimalPlaces !== 'number' || decimalPlaces < 0) { + return number; + } + + if (number === 0) { + return 0; + } + + return number.toFixed(decimalPlaces); } diff --git a/ui/app/utils/metrics-helpers.ts b/ui/app/utils/metrics-helpers.ts index 2338737e14..79a24974cf 100644 --- a/ui/app/utils/metrics-helpers.ts +++ b/ui/app/utils/metrics-helpers.ts @@ -89,11 +89,8 @@ export function normalizeMetricData(metric: Month | null | undefined) { typeof normalized[NormalizedBillingMetrics.ID_TOKEN_UNITS_TOTAL] === 'number' ? normalized[NormalizedBillingMetrics.ID_TOKEN_UNITS_TOTAL] : 0; - normalized[NormalizedBillingMetrics.CREDENTIAL_UNITS_TOTAL] = calculateSum([ - sshUnitsTotal, - pkiUnitsTotal, - idTokenUnitsTotal, - ]); + normalized[NormalizedBillingMetrics.CREDENTIAL_UNITS_TOTAL] = + calculateSum([sshUnitsTotal, pkiUnitsTotal, idTokenUnitsTotal], 4) ?? 0; // Explicitly set any missing metric keys to 0. for (const metricsKey of Object.values(NormalizedBillingMetrics)) { diff --git a/ui/tests/unit/utils/chart-helpers-test.js b/ui/tests/unit/utils/chart-helpers-test.js index dc04fd5ff4..e8c78c4139 100644 --- a/ui/tests/unit/utils/chart-helpers-test.js +++ b/ui/tests/unit/utils/chart-helpers-test.js @@ -3,7 +3,12 @@ * SPDX-License-Identifier: BUSL-1.1 */ -import { numericalAxisLabel, calculateAverage, calculateSum } from 'vault/utils/chart-helpers'; +import { + numericalAxisLabel, + calculateAverage, + calculateSum, + toFixedDisplay, +} from 'vault/utils/chart-helpers'; import { module, test } from 'qunit'; const SMALL_NUMBERS = [0, 7, 27, 103, 999]; @@ -65,4 +70,67 @@ module('Unit | Utility | chart-helpers', function () { assert.strictEqual(calculateSum(['one', 2]), null, 'returns null if array contains non-integers'); assert.strictEqual(calculateSum('not an array'), null, 'returns null if an array is not passed'); }); + + test('calculateSum with fixedDecimalPlaces parameter', function (assert) { + assert.strictEqual(calculateSum([2.5, 3.7], 1), 6.2, 'rounds sum to 1 decimal place'); + assert.strictEqual(calculateSum([2.5555, 3.7777], 2), 6.33, 'rounds sum to 2 decimal places'); + assert.strictEqual( + calculateSum([48.7888, 0.0112], 4), + 48.8, + 'handles floating-point precision issues with 4 decimal places' + ); + assert.strictEqual( + calculateSum([73.1832, 0.0168], 4), + 73.2, + 'correctly sums and rounds to 4 decimal places' + ); + assert.strictEqual( + calculateSum([10, 20, 30], 4), + 60, + 'works with whole numbers when fixedDecimalPlaces is provided' + ); + assert.strictEqual( + calculateSum([1.11111, 2.22222, 3.33333], 4), + 6.6667, + 'rounds sum of multiple numbers to 4 decimal places' + ); + assert.strictEqual(calculateSum([0.1, 0.2], 4), 0.3, 'handles classic floating-point issue (0.1 + 0.2)'); + assert.strictEqual(calculateSum([2, 3], 0), 5, 'rounds to 0 decimal places (whole number)'); + }); + + test('toFixedDisplay formats numbers with fixed decimal places', function (assert) { + assert.strictEqual(toFixedDisplay(48.8, 4), '48.8000', 'formats number with trailing zeros'); + assert.strictEqual(toFixedDisplay(73.2, 4), '73.2000', 'preserves 4 decimal places'); + assert.strictEqual(toFixedDisplay(100, 2), '100.00', 'formats whole number with decimals'); + assert.strictEqual(toFixedDisplay(0, 4), 0, 'returns 0 as number, not formatted string'); + assert.strictEqual(toFixedDisplay(1.23456, 2), '1.23', 'rounds to specified decimal places'); + assert.strictEqual(toFixedDisplay('not a number', 4), 'not a number', 'returns non-number as-is'); + assert.strictEqual(toFixedDisplay(5.5, -1), 5.5, 'returns number as-is for negative decimal places'); + }); + + test('calculateSum and toFixedDisplay work together', function (assert) { + const sum1 = calculateSum([48.7888, 0.0112], 4); + assert.strictEqual(sum1, 48.8, 'calculateSum returns number with fixed precision'); + assert.strictEqual( + toFixedDisplay(sum1, 4), + '48.8000', + 'toFixedDisplay formats for display with trailing zeros' + ); + + const sum2 = calculateSum([73.1832, 0.0168], 4); + assert.strictEqual(sum2, 73.2, 'calculateSum handles floating-point precision'); + assert.strictEqual(toFixedDisplay(sum2, 4), '73.2000', 'toFixedDisplay preserves trailing zeros'); + + const sum3 = calculateSum([10, 20, 30], 4); + assert.strictEqual(sum3, 60, 'calculateSum works with whole numbers'); + assert.strictEqual( + toFixedDisplay(sum3, 4), + '60.0000', + 'toFixedDisplay adds decimal places to whole numbers' + ); + + const sum4 = calculateSum([0, 0, 0], 4); + assert.strictEqual(sum4, 0, 'calculateSum returns 0 for zero sum'); + assert.strictEqual(toFixedDisplay(sum4, 4), 0, 'toFixedDisplay returns 0 as-is, not formatted'); + }); });