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,
           }}

Reply via email to