[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>
This commit is contained in:
Vault Automation 2026-05-11 10:30:11 -06:00 committed by GitHub
parent 417d3dbcb0
commit 72c3492cef
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 149 additions and 11 deletions

View File

@ -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<Args> {
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);
}

View File

@ -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<Args> {
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]) };

View File

@ -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);
}

View File

@ -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)) {

View File

@ -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');
});
});