This is an automated email from the ASF dual-hosted git repository.
lizhimins pushed a commit to branch rocketmq-studio
in repository https://gitbox.apache.org/repos/asf/rocketmq-dashboard.git
The following commit(s) were added to refs/heads/rocketmq-studio by this push:
new ce8acd080 fix(web): promote the byte unit at the rounding boundary and
localize MiniBar labels (#4680)
ce8acd080 is described below
commit ce8acd0806f908702dc5bb8cd5af4da684e84612
Author: btlqql <[email protected]>
AuthorDate: Mon Sep 21 20:17:36 2026 +0800
fix(web): promote the byte unit at the rounding boundary and localize
MiniBar labels (#4680)
Two web display fixes from the same author, consolidated into one change.
1. `formatBytes` chose the unit from the unrounded value, so 1023.96
rendered as "1024.0 B". It now re-checks after `toFixed` and promotes to the
next unit at the boundary.
2. `MiniBar` hard-coded Chinese `aria-label` strings for the trend and
empty states; the dashboard renders it without a label, so screen readers got
Chinese text in an English UI. Both now come from `charts.trendData` /
`charts.noTrendData` with zh/en entries.
Consolidates #4679 and #4680 (same author, both web display layer).
---
web/src/components/MiniBar.tsx | 14 ++++++--
web/src/components/__tests__/MiniBar.test.tsx | 51 ++++++++++++++++++++++++++-
web/src/i18n/translations.ts | 3 ++
web/src/utils/format.test.ts | 7 ++++
web/src/utils/format.ts | 9 ++++-
5 files changed, 80 insertions(+), 4 deletions(-)
diff --git a/web/src/components/MiniBar.tsx b/web/src/components/MiniBar.tsx
index ee895a728..3d3454cab 100644
--- a/web/src/components/MiniBar.tsx
+++ b/web/src/components/MiniBar.tsx
@@ -15,6 +15,8 @@
* limitations under the License.
*/
+import { useLang } from '../i18n/LangContext';
+
interface MiniBarProps {
data: number[];
color?: string;
@@ -24,9 +26,15 @@ interface MiniBarProps {
}
const MiniBar = ({ data, color = '#1677ff', height = 32, width = 120, label }:
MiniBarProps) => {
+ const { lang, t } = useLang();
+
if (!data.length) {
return (
- <span aria-label={label || '暂无趋势数据'} style={{ color: '#8c8c8c' }}>
+ <span
+ role="img"
+ aria-label={label || t('charts.noTrendData')}
+ style={{ color: '#8c8c8c' }}
+ >
—
</span>
);
@@ -38,7 +46,9 @@ const MiniBar = ({ data, color = '#1677ff', height = 32,
width = 120, label }: M
return (
<div
role="img"
- aria-label={label || `趋势数据:${data.join('、')}`}
+ aria-label={
+ label || t('charts.trendData', { values: data.join(lang === 'zh' ? '、'
: ', ') })
+ }
style={{
display: 'inline-flex',
alignItems: 'flex-end',
diff --git a/web/src/components/__tests__/MiniBar.test.tsx
b/web/src/components/__tests__/MiniBar.test.tsx
index 931deed5e..b22617e5f 100644
--- a/web/src/components/__tests__/MiniBar.test.tsx
+++ b/web/src/components/__tests__/MiniBar.test.tsx
@@ -15,13 +15,24 @@
* limitations under the License.
*/
-import { describe, expect, it } from 'vitest';
+import { beforeEach, describe, expect, it } from 'vitest';
import { render, screen } from '@testing-library/react';
import MiniBar from '../MiniBar';
+import { LangProvider } from '../../i18n/LangContext';
+import { LANGUAGE_STORAGE_KEY } from '../../i18n/languagePreference';
const getBarHeights = () =>
Array.from(screen.getByRole('img').children, (bar) => (bar as
HTMLElement).style.height);
+const getTrendLabel = () => screen.getByRole('img').getAttribute('aria-label');
+
+const renderTrend = (data: number[], label?: string) =>
+ render(
+ <LangProvider>
+ <MiniBar data={data} height={20} {...(label === undefined ? {} : { label
})} />
+ </LangProvider>,
+ );
+
describe('MiniBar', () => {
it('renders zero values without a visible bar', () => {
render(<MiniBar data={[0, 0, 0]} height={20} label="Throughput trend" />);
@@ -35,3 +46,41 @@ describe('MiniBar', () => {
expect(getBarHeights()).toEqual(['0px', '4px', '20px']);
});
});
+
+describe('MiniBar accessible trend labels', () => {
+ beforeEach(() => {
+ localStorage.clear();
+ });
+
+ it('describes the trend in the active language', () => {
+ localStorage.setItem(LANGUAGE_STORAGE_KEY, 'en');
+
+ renderTrend([1, 2, 3]);
+
+ expect(getTrendLabel()).toBe('Trend: 1, 2, 3');
+ });
+
+ it('keeps the Chinese label and separator for the Chinese UI', () => {
+ localStorage.setItem(LANGUAGE_STORAGE_KEY, 'zh');
+
+ renderTrend([1, 2, 3]);
+
+ expect(getTrendLabel()).toBe('趋势数据:1、2、3');
+ });
+
+ it('describes an empty trend in the active language', () => {
+ localStorage.setItem(LANGUAGE_STORAGE_KEY, 'en');
+
+ renderTrend([]);
+
+ expect(getTrendLabel()).toBe('No trend data yet');
+ });
+
+ it('prefers an explicit label over the localized default', () => {
+ localStorage.setItem(LANGUAGE_STORAGE_KEY, 'en');
+
+ renderTrend([4], 'Throughput');
+
+ expect(getTrendLabel()).toBe('Throughput');
+ });
+});
diff --git a/web/src/i18n/translations.ts b/web/src/i18n/translations.ts
index 9a7bbe368..aeb4aff90 100644
--- a/web/src/i18n/translations.ts
+++ b/web/src/i18n/translations.ts
@@ -116,6 +116,9 @@ const translations: Record<string, Record<Lang, string>> = {
'dashboard.consumerGroups': { zh: '{n} 消费组', en: '{n} Groups' },
'dashboard.healthy': { zh: '健康', en: 'Healthy' },
'dashboard.last12h': { zh: '近 12 小时', en: 'Last 12 hours' },
+ // ─── Shared mini charts ───
+ 'charts.noTrendData': { zh: '暂无趋势数据', en: 'No trend data yet' },
+ 'charts.trendData': { zh: '趋势数据:{values}', en: 'Trend: {values}' },
'dashboardTraffic.title': { zh: '流量洞察', en: 'Traffic Insights' },
'dashboardTraffic.activeClusters': { zh: '活跃集群', en: 'Active Clusters' },
'dashboardTraffic.topClusterShare': { zh: '最高流量占比', en: 'Top Traffic Share'
},
diff --git a/web/src/utils/format.test.ts b/web/src/utils/format.test.ts
index efaadc28f..e76c8031b 100644
--- a/web/src/utils/format.test.ts
+++ b/web/src/utils/format.test.ts
@@ -28,6 +28,13 @@ describe('formatBytes', () => {
expect(huge).not.toContain('undefined');
});
+ it('promotes to the next unit when rounding reaches the boundary', () => {
+ expect(formatBytes(1023.96)).toBe('1.0 KB');
+ expect(formatBytes(1024 * 1024 - 1)).toBe('1.0 MB');
+ expect(formatBytes(1024 * 1024 - 1, 0)).toBe('1 MB');
+ expect(formatBytes(1024 * 1024 * 1024 - 1, 2)).toBe('1.00 GB');
+ expect(formatBytes(1024 ** 6)).toBe('1024.0 PB');
+ });
it('handles non-finite input', () => {
expect(formatBytes(Number.NaN)).toBe('-');
expect(formatBytes(Number.POSITIVE_INFINITY)).toBe('-');
diff --git a/web/src/utils/format.ts b/web/src/utils/format.ts
index f73baf09c..defeee373 100644
--- a/web/src/utils/format.ts
+++ b/web/src/utils/format.ts
@@ -146,13 +146,20 @@ export function formatBytes(bytes: number, decimals = 1):
string {
const units = ['B', 'KB', 'MB', 'GB', 'TB', 'PB'];
const k = 1024;
+ const digits = safeDecimals(decimals);
let i = 0;
let value = Math.abs(bytes);
while (value >= k && i < units.length - 1) {
value /= k;
i += 1;
}
- return `${value.toFixed(safeDecimals(decimals))} ${units[i]}`;
+ // The unit is chosen from the unrounded value, so a value just below a
boundary used to render
+ // as 1024.0 KB - a mantissa of 1024 that the loop above exists to avoid.
+ while (i < units.length - 1 && Number(value.toFixed(digits)) >= k) {
+ value /= k;
+ i += 1;
+ }
+ return `${value.toFixed(digits)} ${units[i]}`;
}
/**