You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@rocketmq.apache.org by li...@apache.org on 2026/10/15 10:15:34 UTC
(rocketmq-dashboard) branch rocketmq-studio updated: fix(settings): report a failed AI configuration load instead of an empty form (#6550)
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 cdbd1fa8f fix(settings): report a failed AI configuration load instead of an empty form (#6550)
cdbd1fa8f is described below
commit cdbd1fa8f1dbe9360d708922195d140b73cdc3a6
Author: Apulupie <16...@users.noreply.github.com>
AuthorDate: Thu Oct 15 18:15:28 2026 +0800
fix(settings): report a failed AI configuration load instead of an empty form (#6550)
The AI assistant settings tab reported a failed configuration load as a transient toast and then
rendered the form anyway, with every field empty and the API key slot prompting for a key - the tab
claimed that nothing is configured. The load request is admin-only on the server
(`AuthInterceptor.isAdminOnlyGetPath` covers `/api/llm/config` and `/api/llm/models`), so a reader
opening Settings sees that blank configuration on every visit, and an administrator's transient
failure is indistinguishable from an empty configuration as well.
The tab keeps the server's reason and offers a retry: a failed load now renders an error notice
above the form with the message the request failed with ("Admin permission required" for a reader)
and a retry button that re-runs the load, clearing the notice once it succeeds. `AlertRuleAssetList`
already uses this shape for its own list, and the home page's assistant load failure is handled the
same way in #6082. No new copy: the notice and the button reuse `llm.loadFailed` and `common.retry`.
---
web/src/pages/settings/AiAssistantTab.tsx | 33 ++++++++++++++++++++--
.../settings/__tests__/AiAssistantTab.test.tsx | 20 +++++++++++++
2 files changed, 50 insertions(+), 3 deletions(-)
diff --git a/web/src/pages/settings/AiAssistantTab.tsx b/web/src/pages/settings/AiAssistantTab.tsx
index 9e87e4d0c..56d33e674 100644
--- a/web/src/pages/settings/AiAssistantTab.tsx
+++ b/web/src/pages/settings/AiAssistantTab.tsx
@@ -44,6 +44,7 @@ import {
type LlmTestResult,
} from '../../api/llm';
import { fallbackModelOptions } from '../studio/llmModelOptions';
+import { describeThrownMessage } from '../../utils/apiError';
const DEFAULT_BASE_URL: Record<string, string> = {
tongyi: 'https://dashscope.aliyuncs.com/compatible-mode/v1',
@@ -118,6 +119,8 @@ export const AiAssistantTab = () => {
const [testResult, setTestResult] = useState<TestState | null>(null);
const testRequestIdRef = useRef(0);
const configGenerationRef = useRef(0);
+ const [loadError, setLoadError] = useState<string | null>(null);
+ const [reloadNonce, setReloadNonce] = useState(0);
const buildModelOptions = (
nextProvider: string,
@@ -157,10 +160,16 @@ export const AiAssistantTab = () => {
Promise.all([getLlmConfig(), getLlmModels().catch(() => null)])
.then(([config, models]) => {
if (cancelled) return;
+ setLoadError(null);
applyConfig(config, models?.data?.map((m) => m.id || '').filter(Boolean) ?? []);
})
- .catch(() => {
- if (!cancelled) message.error(t('llm.loadFailed'));
+ .catch((error) => {
+ if (cancelled) return;
+ // An unreadable configuration must not be presented as an empty one: the form below would
+ // then claim that no provider, model or API key is configured. The endpoint is admin-only,
+ // so a reader always ends up here, and the server's reason is what says so.
+ setLoadError(describeThrownMessage(error) || t('llm.loadFailed'));
+ message.error(t('llm.loadFailed'));
})
.finally(() => {
if (!cancelled) setLoading(false);
@@ -171,7 +180,7 @@ export const AiAssistantTab = () => {
configGenerationRef.current += 1;
};
// eslint-disable-next-line react-hooks/exhaustive-deps
- }, []);
+ }, [reloadNonce]);
const invalidateTestRequest = () => {
testRequestIdRef.current += 1;
@@ -330,6 +339,24 @@ export const AiAssistantTab = () => {
return (
<Flex vertical gap={24} style={{ maxWidth: 860 }}>
+ {loadError && (
+ <Alert
+ type="error"
+ showIcon
+ message={loadError}
+ action={
+ <Button
+ size="small"
+ onClick={() => {
+ setLoading(true);
+ setReloadNonce((nonce) => nonce + 1);
+ }}
+ >
+ {t('common.retry')}
+ </Button>
+ }
+ />
+ )}
<Form
form={form}
layout="vertical"
diff --git a/web/src/pages/settings/__tests__/AiAssistantTab.test.tsx b/web/src/pages/settings/__tests__/AiAssistantTab.test.tsx
index 643d1012c..a279b3eef 100644
--- a/web/src/pages/settings/__tests__/AiAssistantTab.test.tsx
+++ b/web/src/pages/settings/__tests__/AiAssistantTab.test.tsx
@@ -85,6 +85,26 @@ describe('AiAssistantTab', () => {
expect(screen.getByDisplayValue('qwen3.8-max')).toBeInTheDocument();
});
+ it('reports a failed configuration load with its reason and can retry it', async () => {
+ llmApiMocks.getLlmConfig.mockRejectedValueOnce(new Error('Admin permission required'));
+ const user = userEvent.setup();
+ renderPage();
+
+ // A configuration the tab could not read must not be presented as an empty one: the request
+ // answered 403 (the endpoint is admin-only) and the form would otherwise claim nothing is set.
+ expect(await screen.findByText('Admin permission required')).toBeInTheDocument();
+ expect(llmApiMocks.getLlmConfig).toHaveBeenCalledTimes(1);
+
+ // antd inserts a space between the two characters of a CJK button label.
+ await user.click(screen.getByRole('button', { name: /重\s*试/ }));
+ await waitFor(() => expect(llmApiMocks.getLlmConfig).toHaveBeenCalledTimes(2));
+ // The retry resolves, so the loaded configuration replaces the failure notice.
+ expect(await screen.findByText('密钥已配置')).toBeInTheDocument();
+ await waitFor(() =>
+ expect(screen.queryByText('Admin permission required')).not.toBeInTheDocument(),
+ );
+ });
+
it('renders assistant configuration labels in English when English is selected', async () => {
localStorage.setItem(LANGUAGE_STORAGE_KEY, 'en');
renderPage();