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 056af345c fix(web): render panels on theme tokens so dark mode stays
readable (#5662)
056af345c is described below
commit 056af345c9df7158e611a1ae6337ae55fb164faf
Author: Apulupie <[email protected]>
AuthorDate: Mon Oct 19 20:07:04 2026 +0800
fix(web): render panels on theme tokens so dark mode stays readable (#5662)
Verified against `rocketmq-studio` at 96af74093 before merging: the defect
is still present on the
current trunk and the change merges cleanly.
Maintainer follow-ups on top of the contributor commit:
- fix(web): cover the panels the dark-mode token pass missed
Each follow-up commit message records the review finding it addresses.
Locally verified: targeted suites green on the integration branch; a full
backend and frontend
build plus test pass runs on the trunk once this batch lands.
---
web/src/components/AlertRuleAssetList.tsx | 20 +++++++++-
web/src/components/GrafanaDashboardList.tsx | 8 +++-
web/src/pages/ai/components/AssistantBubble.tsx | 4 +-
.../pages/ai/components/PromptTemplateModal.tsx | 9 ++++-
.../__tests__/PromptTemplateModal.test.tsx | 40 +++++++++++++++++++
.../pages/instance/__tests__/ConsumerPage.test.tsx | 40 ++++++++++++++++++-
.../pages/instance/__tests__/MessagePage.test.tsx | 46 +++++++++++++++++++++-
web/src/pages/instance/consumer.tsx | 10 ++++-
web/src/pages/instance/message.tsx | 6 ++-
web/src/pages/studio/BrokerCluster.tsx | 28 ++++++++++---
10 files changed, 194 insertions(+), 17 deletions(-)
diff --git a/web/src/components/AlertRuleAssetList.tsx
b/web/src/components/AlertRuleAssetList.tsx
index 78fcd4de8..295e464c8 100644
--- a/web/src/components/AlertRuleAssetList.tsx
+++ b/web/src/components/AlertRuleAssetList.tsx
@@ -16,7 +16,19 @@
*/
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
-import { Alert, App, Button, Input, Modal, Select, Space, Table, Tag,
Typography } from 'antd';
+import {
+ Alert,
+ App,
+ Button,
+ Input,
+ Modal,
+ Select,
+ Space,
+ Table,
+ Tag,
+ Typography,
+ theme,
+} from 'antd';
import type { ColumnsType } from 'antd/es/table';
import { ArrowClockwise, DownloadSimple, Eye } from '@phosphor-icons/react';
import { useLang } from '../i18n/LangContext';
@@ -40,6 +52,7 @@ const SEVERITY_COLORS: Record<string, string> = {
export const AlertRuleAssetList: React.FC = () => {
const { t } = useLang();
const { message } = App.useApp();
+ const { token } = theme.useToken();
const [assets, setAssets] = useState<AlertRuleAssetInfo[]>([]);
const [searchText, setSearchText] = useState('');
const [selectedSeverities, setSelectedSeverities] = useState<string[]>([]);
@@ -292,7 +305,10 @@ export const AlertRuleAssetList: React.FC = () => {
style={{
maxHeight: 480,
overflow: 'auto',
- background: '#f5f5f5',
+ // Theme fill instead of a fixed light grey: the preview
inherits the active
+ // theme's text color, so a hardcoded light background made it
unreadable in
+ // dark mode (the panel keeps the light-mode look the token
resolves to).
+ background: token.colorFillQuaternary,
padding: 16,
borderRadius: 6,
fontSize: 14,
diff --git a/web/src/components/GrafanaDashboardList.tsx
b/web/src/components/GrafanaDashboardList.tsx
index bf35092d8..c1d3e7377 100644
--- a/web/src/components/GrafanaDashboardList.tsx
+++ b/web/src/components/GrafanaDashboardList.tsx
@@ -16,7 +16,7 @@
*/
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
-import { Alert, App, Button, Input, Modal, Select, Space, Table, Typography }
from 'antd';
+import { Alert, App, Button, Input, Modal, Select, Space, Table, Typography,
theme } from 'antd';
import type { ColumnsType } from 'antd/es/table';
import { ArrowClockwise, DownloadSimple, Eye } from '@phosphor-icons/react';
import { useLang } from '../i18n/LangContext';
@@ -35,6 +35,7 @@ const { Paragraph, Text } = Typography;
export const GrafanaDashboardList: React.FC = () => {
const { t } = useLang();
const { message } = App.useApp();
+ const { token } = theme.useToken();
const [dashboards, setDashboards] = useState<GrafanaDashboardInfo[]>([]);
const [searchText, setSearchText] = useState('');
const [selectedTags, setSelectedTags] = useState<string[]>([]);
@@ -305,7 +306,10 @@ export const GrafanaDashboardList: React.FC = () => {
style={{
maxHeight: 480,
overflow: 'auto',
- background: '#f5f5f5',
+ // Theme fill instead of a fixed light grey: the preview
inherits the active
+ // theme's text color, so a hardcoded light background made it
unreadable in
+ // dark mode (the panel keeps the light-mode look the token
resolves to).
+ background: token.colorFillQuaternary,
padding: 16,
borderRadius: 6,
fontSize: 14,
diff --git a/web/src/pages/ai/components/AssistantBubble.tsx
b/web/src/pages/ai/components/AssistantBubble.tsx
index c230f3f25..34632a297 100644
--- a/web/src/pages/ai/components/AssistantBubble.tsx
+++ b/web/src/pages/ai/components/AssistantBubble.tsx
@@ -146,7 +146,9 @@ const AssistantBubble = ({
width: 36,
height: 36,
borderRadius: '50%',
- background: '#ffffff',
+ // Theme surface instead of a fixed white: the avatar matches the
neighbouring bubble
+ // card, which already sits on colorBgElevated (white in light
mode, elevated in dark).
+ background: token.colorBgElevated,
border: `1px solid ${token.colorBorderSecondary}`,
flexShrink: 0,
display: 'flex',
diff --git a/web/src/pages/ai/components/PromptTemplateModal.tsx
b/web/src/pages/ai/components/PromptTemplateModal.tsx
index 4b8307a1c..77927fa11 100644
--- a/web/src/pages/ai/components/PromptTemplateModal.tsx
+++ b/web/src/pages/ai/components/PromptTemplateModal.tsx
@@ -32,6 +32,7 @@ import {
Tag,
Typography,
message,
+ theme,
} from 'antd';
import { Plus, Trash } from '@phosphor-icons/react';
import { useLang } from '../../../i18n/LangContext';
@@ -95,6 +96,7 @@ const PromptTemplateModal = ({
const userId = useAuthStore((state) => state.userId);
const username = useAuthStore((state) => state.user);
const templateOwner = userId != null ? `id:${userId}` : username ?
`name:${username}` : undefined;
+ const { token } = theme.useToken();
const [templates, setTemplates] = useState<PromptTemplate[]>([]);
const [loadedOwner, setLoadedOwner] = useState<string | undefined>();
const [storageAvailable, setStorageAvailable] = useState(true);
@@ -354,8 +356,11 @@ const PromptTemplateModal = ({
{/* 保存当前输入是次要操作,下沉到弹窗底部并弱化呈现 */}
<div
style={{
- background: '#fafafa',
- border: '1px solid #f0f0f0',
+ // Theme tokens instead of fixed light values: the block's text
inherits the
+ // active theme's color, so a hardcoded light background made it
unreadable in
+ // dark mode.
+ background: token.colorFillQuaternary,
+ border: `1px solid ${token.colorBorderSecondary}`,
borderRadius: 8,
padding: 12,
}}
diff --git a/web/src/pages/ai/components/__tests__/PromptTemplateModal.test.tsx
b/web/src/pages/ai/components/__tests__/PromptTemplateModal.test.tsx
index 51aa35f5d..5c0845551 100644
--- a/web/src/pages/ai/components/__tests__/PromptTemplateModal.test.tsx
+++ b/web/src/pages/ai/components/__tests__/PromptTemplateModal.test.tsx
@@ -15,6 +15,7 @@
* limitations under the License.
*/
+import { ConfigProvider, theme } from 'antd';
import { act, fireEvent, render, screen, waitFor } from
'@testing-library/react';
import { afterEach, beforeAll, describe, expect, it, vi } from 'vitest';
import PromptTemplateModal from '../PromptTemplateModal';
@@ -26,6 +27,14 @@ import {
} from '../../promptTemplates';
import useAuthStore from '../../../../stores/authStore';
+// Comparing raw CSS text would depend on how each side spells a colour, so
both the element's
+// value and the expected token go through the same parser before they are
compared.
+const normalizeBackground = (value: string): string => {
+ const probe = document.createElement('span');
+ probe.style.background = value;
+ return probe.style.background;
+};
+
describe('prompt templates with denied storage access', () => {
beforeAll(() => {
Object.defineProperty(window, 'matchMedia', {
@@ -112,6 +121,37 @@ describe('prompt templates with denied storage access', ()
=> {
else storage.setItem('rocketmq-studio-ai-prompt-templates', original);
}
});
+
+ it('rendersTheSaveBlockOnThemeTokensInDarkModeTest', async () => {
+ render(
+ <ConfigProvider theme={{ algorithm: theme.darkAlgorithm }}>
+ <PromptTemplateModal
+ open
+ onClose={vi.fn()}
+ inputValue=""
+ mode="chat"
+ enhance={false}
+ onApply={vi.fn()}
+ />
+ </ConfigProvider>,
+ );
+
+ const saveBlock = (await screen.findByText('保存当前输入')).closest('div') as
HTMLElement;
+ // Fixed light values leave the dark theme's light text unreadable on
them, so the block has
+ // to follow the theme's fill and border tokens instead.
+ const dark = theme.getDesignToken({ algorithm: theme.darkAlgorithm });
+ expect(normalizeBackground(saveBlock.style.background)).toBe(
+ normalizeBackground(dark.colorFillQuaternary),
+ );
+ expect(normalizeBackground(saveBlock.style.background)).not.toBe(
+ normalizeBackground('#fafafa'),
+ );
+ // Same parser trick for the border: jsdom spells the token's hex as
rgb(), so both sides of
+ // the comparison go through a probe before they are matched.
+ const borderProbe = document.createElement('span');
+ borderProbe.style.border = `1px solid ${dark.colorBorderSecondary}`;
+ expect(saveBlock.style.border).toBe(borderProbe.style.border);
+ });
});
describe('prompt templates scoped to the signed-in operator', () => {
diff --git a/web/src/pages/instance/__tests__/ConsumerPage.test.tsx
b/web/src/pages/instance/__tests__/ConsumerPage.test.tsx
index 2c8c51b2b..b7e49403d 100644
--- a/web/src/pages/instance/__tests__/ConsumerPage.test.tsx
+++ b/web/src/pages/instance/__tests__/ConsumerPage.test.tsx
@@ -15,7 +15,7 @@
* limitations under the License.
*/
-import { App, Modal, message } from 'antd';
+import { App, ConfigProvider, Modal, message, theme } from 'antd';
import { act, cleanup, fireEvent, render, screen, waitFor, within } from
'@testing-library/react';
import userEvent from '@testing-library/user-event';
import type React from 'react';
@@ -125,6 +125,14 @@ const renderWithProviders = (ui: React.ReactElement,
initialEntry = '/instance/c
</App>,
);
+// Comparing raw CSS text would depend on how each side spells a colour, so
both the element's
+// value and the expected token go through the same parser before they are
compared.
+const normalizeBackground = (value: string): string => {
+ const probe = document.createElement('span');
+ probe.style.background = value;
+ return probe.style.background;
+};
+
beforeAll(installBrowserMocks);
// antd's Spin keys its `.ant-spin-blur` class on internal state that follows
the Table's
@@ -888,6 +896,36 @@ describe('Consumer page', () => {
expect(within(panel).getByText('处理建议')).toBeInTheDocument();
});
+ it('renders the health summary card on the theme fill token so dark mode
stays readable', async () => {
+ const user = userEvent.setup({ pointerEventsCheck: 0 });
+ render(
+ <ConfigProvider theme={{ algorithm: theme.darkAlgorithm }}>
+ <App>
+ <LangProvider>
+ <MemoryRouter initialEntries={['/instance/consumer']}>
+ <ConsumerPage />
+ </MemoryRouter>
+ </LangProvider>
+ </App>
+ </ConfigProvider>,
+ );
+
+ await user.click(await screen.findByRole('button', { name: /详情/ }));
+ await user.click(await screen.findByRole('tab', { name: /健康诊断/ }));
+ const panel = await screen.findByRole('tabpanel', { name: /健康诊断/ });
+ const summaryCard = (await
within(panel).findByText('健康分:')).closest('.ant-card');
+
+ // A fixed light grey leaves the dark theme's light text unreadable on it,
so the summary
+ // card has to follow the theme's fill token instead.
+ const darkFill = theme.getDesignToken({ algorithm: theme.darkAlgorithm
}).colorFillQuaternary;
+ expect(normalizeBackground((summaryCard as
HTMLElement).style.background)).toBe(
+ normalizeBackground(darkFill),
+ );
+ expect(normalizeBackground((summaryCard as
HTMLElement).style.background)).not.toBe(
+ normalizeBackground('#fafafa'),
+ );
+ });
+
it('filters queue progress to the topic of the clicked distribution button',
async () => {
vi.mocked(consumerService.getConsumerSubscriptions).mockResolvedValue([
{
diff --git a/web/src/pages/instance/__tests__/MessagePage.test.tsx
b/web/src/pages/instance/__tests__/MessagePage.test.tsx
index ae6452114..b1377a483 100644
--- a/web/src/pages/instance/__tests__/MessagePage.test.tsx
+++ b/web/src/pages/instance/__tests__/MessagePage.test.tsx
@@ -15,7 +15,7 @@
* limitations under the License.
*/
-import { App } from 'antd';
+import { App, ConfigProvider, theme } from 'antd';
import { fireEvent, render, screen, waitFor, within } from
'@testing-library/react';
import userEvent from '@testing-library/user-event';
import type React from 'react';
@@ -105,6 +105,14 @@ const renderWithProviders = (ui: React.ReactElement) =>
const lastElement = <T,>(elements: T[]): T => elements[elements.length - 1]!;
+// Comparing raw CSS text would depend on how each side spells a colour, so
both the element's
+// value and the expected token go through the same parser before they are
compared.
+const normalizeBackground = (value: string): string => {
+ const probe = document.createElement('span');
+ probe.style.background = value;
+ return probe.style.background;
+};
+
describe('Message page query history', () => {
beforeEach(() => {
localStorage.clear();
@@ -803,6 +811,42 @@ describe('Message page query history', () => {
expect(locationItems[2]).toHaveTextContent('-');
});
+ it('renders the message body on the theme fill token so dark mode stays
readable', async () => {
+ const user = userEvent.setup();
+ messageServiceMocks.queryMessages.mockResolvedValue([
+ { ...createMessage('MID-DARK-BODY'), body: '{"dark":true}' },
+ ]);
+ render(
+ <ConfigProvider theme={{ algorithm: theme.darkAlgorithm }}>
+ <App>
+ <LangProvider>
+ <MemoryRouter>
+ <MessagePage />
+ </MemoryRouter>
+ </LangProvider>
+ </App>
+ </ConfigProvider>,
+ );
+
+ await user.click(lastElement(screen.getAllByRole('combobox')));
+ await user.click(lastElement(await screen.findAllByText('order-create')));
+ await user.click(screen.getByRole('button', { name: /^search查询$/ }));
+ const row = await screen.findByRole('row', { name: /MID-DARK-BODY/ });
+ await user.click(within(row).getByRole('button', { name: /详情/ }));
+
+ const dialog = await screen.findByRole('dialog', { name: '消息详情' });
+ const bodyPanel = within(dialog).getByText(/"dark": true/);
+ // A fixed light background leaves the dark theme's light text unreadable
on it, so the
+ // panel has to follow the theme's fill token instead.
+ const darkFill = theme.getDesignToken({ algorithm: theme.darkAlgorithm
}).colorFillQuaternary;
+ expect(normalizeBackground((bodyPanel as
HTMLElement).style.background)).toBe(
+ normalizeBackground(darkFill),
+ );
+ expect(normalizeBackground((bodyPanel as
HTMLElement).style.background)).not.toBe(
+ normalizeBackground('#f5f5f5'),
+ );
+ });
+
it('renders a message larger than a megabyte with the matching unit', async
() => {
const user = userEvent.setup();
messageServiceMocks.queryMessages.mockResolvedValue([
diff --git a/web/src/pages/instance/consumer.tsx
b/web/src/pages/instance/consumer.tsx
index 0c0170c67..e68ef4029 100644
--- a/web/src/pages/instance/consumer.tsx
+++ b/web/src/pages/instance/consumer.tsx
@@ -44,6 +44,7 @@ import {
Progress,
Switch,
message,
+ theme,
} from 'antd';
import {
Plus,
@@ -246,6 +247,7 @@ const ConsumerPageContent = ({
reloadInstances,
}: ConsumerPageContentProps) => {
const { t, lang } = useLang();
+ const { token } = theme.useToken();
const isCloudInstance =
selectedInstance?.vendor === 'ALIYUN' || selectedInstance?.vendor ===
'TENCENT';
const hasSelectedInstance = Boolean(selectedInstanceId);
@@ -2003,7 +2005,9 @@ const ConsumerPageContent = ({
<Card
size="small"
style={{
- background: '#fafafa',
+ // Theme fill instead of a fixed light grey, which
left this summary
+ // card unreadable in dark mode.
+ background: token.colorFillQuaternary,
borderRadius: 8,
}}
styles={{ body: { padding: '8px 16px' } }}
@@ -2154,7 +2158,9 @@ const ConsumerPageContent = ({
size="small"
style={{
marginBottom: 16,
- background: '#fafafa',
+ // Theme fill instead of a fixed light grey, which
left this summary
+ // card unreadable in dark mode.
+ background: token.colorFillQuaternary,
borderRadius: 8,
}}
styles={{ body: { padding: '8px 16px' } }}
diff --git a/web/src/pages/instance/message.tsx
b/web/src/pages/instance/message.tsx
index ccab0a707..cbc2c462f 100644
--- a/web/src/pages/instance/message.tsx
+++ b/web/src/pages/instance/message.tsx
@@ -37,6 +37,7 @@ import {
Statistic,
Tooltip,
message,
+ theme,
} from 'antd';
import {
SearchOutlined,
@@ -348,6 +349,7 @@ const MessagePageContent = ({
reloadInstances,
}: InstanceFilterProps) => {
const { t } = useLang();
+ const { token } = theme.useToken();
const [topicOptions, setTopicOptions] = useState<string[]>([]);
const [topicError, setTopicError] = useState<string | null>(null);
const [topicLoading, setTopicLoading] = useState(false);
@@ -1069,7 +1071,9 @@ const MessagePageContent = ({
<Paragraph
copyable={{ text: selectedMsg.body }}
style={{
- background: '#f5f5f5',
+ // Theme fill instead of a fixed light grey: the body text
inherits the active
+ // theme's color, so a hardcoded light background made it
unreadable in dark mode.
+ background: token.colorFillQuaternary,
padding: '12px 16px',
borderRadius: 6,
fontFamily: "'SF Mono', Monaco, 'Cascadia Code', Consolas,
monospace",
diff --git a/web/src/pages/studio/BrokerCluster.tsx
b/web/src/pages/studio/BrokerCluster.tsx
index 177edd6d3..6c07639aa 100644
--- a/web/src/pages/studio/BrokerCluster.tsx
+++ b/web/src/pages/studio/BrokerCluster.tsx
@@ -16,7 +16,20 @@
*/
import { useCallback, useEffect, useRef, useState } from 'react';
-import { Table, Button, Tag, Tabs, Card, Space, Switch, Progress, Spin, App,
Select } from 'antd';
+import {
+ Table,
+ Button,
+ Tag,
+ Tabs,
+ Card,
+ Space,
+ Switch,
+ Progress,
+ Spin,
+ App,
+ Select,
+ theme,
+} from 'antd';
import {
ArrowClockwise,
Cloud,
@@ -193,6 +206,7 @@ const BrokerClusterPage = () => {
const requestedInstanceRef = useRef<string | undefined>(undefined);
const { t } = useLang();
const { message } = App.useApp();
+ const { token } = theme.useToken();
const clearData = useCallback(() => {
setBrokerData([]);
@@ -371,7 +385,8 @@ const BrokerClusterPage = () => {
<code
style={{
fontSize: 14,
- background: '#f5f5f5',
+ // Theme fill instead of a fixed light grey, unreadable in dark
mode.
+ background: token.colorFillQuaternary,
padding: '2px 6px',
borderRadius: 4,
}}
@@ -428,7 +443,8 @@ const BrokerClusterPage = () => {
<code
style={{
fontSize: 14,
- background: '#f5f5f5',
+ // Theme fill instead of a fixed light grey, unreadable in dark
mode.
+ background: token.colorFillQuaternary,
padding: '2px 6px',
borderRadius: 4,
}}
@@ -467,7 +483,8 @@ const BrokerClusterPage = () => {
<code
style={{
fontSize: 14,
- background: '#f5f5f5',
+ // Theme fill instead of a fixed light grey, unreadable in dark
mode.
+ background: token.colorFillQuaternary,
padding: '2px 6px',
borderRadius: 4,
}}
@@ -484,7 +501,8 @@ const BrokerClusterPage = () => {
<code
style={{
fontSize: 14,
- background: '#f5f5f5',
+ // Theme fill instead of a fixed light grey, unreadable in dark
mode.
+ background: token.colorFillQuaternary,
padding: '2px 6px',
borderRadius: 4,
}}