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 <[email protected]>
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();