This is an automated email from the ASF dual-hosted git repository.

imbajin pushed a commit to branch hubble-dev
in repository https://gitbox.apache.org/repos/asf/hugegraph-toolchain.git

commit f35ea2a3bc617f63b5eb586e9a9588cc49f17c1e
Author: imbajin <[email protected]>
AuthorDate: Sun Jul 12 14:44:08 2026 +0800

    fix(hubble): harden frontend validation
---
 .../analysis/QueryBar/ContentCommon/index.js       |  5 +-
 .../analysis/QueryBar/ContentCommon/index.test.js  | 62 ++++++++++++++++++++++
 .../modules/navigation/ConsoleItem/index.test.js   | 12 +++++
 .../hubble-fe/src/modules/navigation/Item/index.js | 10 ++++
 .../src/modules/navigation/Item/index.module.scss  |  7 +++
 hugegraph-hubble/hubble-fe/src/utils/rules.js      | 19 ++++++-
 hugegraph-hubble/hubble-fe/src/utils/rules.test.js | 12 +++++
 7 files changed, 124 insertions(+), 3 deletions(-)

diff --git 
a/hugegraph-hubble/hubble-fe/src/modules/analysis/QueryBar/ContentCommon/index.js
 
b/hugegraph-hubble/hubble-fe/src/modules/analysis/QueryBar/ContentCommon/index.js
index 46195c1c..a06e2d03 100644
--- 
a/hugegraph-hubble/hubble-fe/src/modules/analysis/QueryBar/ContentCommon/index.js
+++ 
b/hugegraph-hubble/hubble-fe/src/modules/analysis/QueryBar/ContentCommon/index.js
@@ -27,6 +27,7 @@ import {UpOutlined, DownOutlined, QuestionCircleOutlined} 
from '@ant-design/icon
 import {GREMLIN_EXECUTES_MODE} from '../../../../utils/constants';
 import GraphAnalysisContext from '../../../Context';
 import classnames from 'classnames';
+import {isValidFavoriteName} from '../../../../utils/rules';
 import c from './index.module.scss';
 import * as api from '../../../../api/index';
 import KeyboardAction from '../../../../components/KeyboardAction';
@@ -139,7 +140,7 @@ const ContentCommon = props => {
         e => {
             const favoriteName = e.target.value;
             setFavoriteName(favoriteName);
-            favoriteName ? setDisabledFavorite(false) : 
setDisabledFavorite(true);
+            setDisabledFavorite(!isValidFavoriteName(favoriteName));
         },
         []
     );
@@ -152,6 +153,8 @@ const ContentCommon = props => {
                 maxLength={48}
                 value={favoriteName}
                 onChange={onChangeFavoraiteName}
+                status={favoriteName && !isValidFavoriteName(favoriteName)
+                    ? 'error' : undefined}
             />
             <Space style={{marginTop: '24px'}}>
                 <Button type='primary' onClick={onOkFavorite} 
disabled={disabledFavorite}>
diff --git 
a/hugegraph-hubble/hubble-fe/src/modules/analysis/QueryBar/ContentCommon/index.test.js
 
b/hugegraph-hubble/hubble-fe/src/modules/analysis/QueryBar/ContentCommon/index.test.js
new file mode 100644
index 00000000..5a72884c
--- /dev/null
+++ 
b/hugegraph-hubble/hubble-fe/src/modules/analysis/QueryBar/ContentCommon/index.test.js
@@ -0,0 +1,62 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0.
+ */
+
+import {fireEvent, render, screen} from '@testing-library/react';
+
+import * as api from '../../../../api/index';
+import GraphAnalysisContext from '../../../Context';
+import ContentCommon from './index';
+
+jest.mock('../../../../api/index', () => ({
+    analysis: {addFavoriate: jest.fn().mockResolvedValue({status: 200})},
+}));
+jest.mock('antd', () => ({
+    ...jest.requireActual('antd'),
+    message: {success: jest.fn(), error: jest.fn()},
+}));
+jest.mock('react-i18next', () => ({
+    initReactI18next: {type: '3rdParty', init: jest.fn()},
+    useTranslation: () => ({t: key => key}),
+}));
+
+const renderContent = () => render(
+    <GraphAnalysisContext.Provider value={{graphSpace: 'DEFAULT', graph: 
'hugegraph'}}>
+        <ContentCommon
+            codeEditorContent='g.V()'
+            setCodeEditorContent={jest.fn()}
+            executeMode='QUERY'
+            onExecuteModeChange={jest.fn()}
+            activeTab='Gremlin'
+            onExecute={jest.fn()}
+            onRefresh={jest.fn()}
+            isEmptyQuery={false}
+            favoriteCardVisible
+            setFavoriteCardVisible={jest.fn()}
+        />
+    </GraphAnalysisContext.Provider>
+);
+
+beforeEach(() => {
+    api.analysis.addFavoriate.mockResolvedValue({status: 200});
+});
+
+test('keeps favorite submission disabled until the name is 
backend-compatible', () => {
+    renderContent();
+    const input = 
screen.getByPlaceholderText('analysis.query.favorite_name_placeholder');
+    const submit = screen.getAllByRole('button', {name: 
'analysis.query.favorite'})
+        .find(button => button.closest('.ant-popover'));
+
+    fireEvent.change(input, {target: {value: 'query-name'}});
+    expect(submit).toBeDisabled();
+    fireEvent.click(submit);
+    expect(api.analysis.addFavoriate).not.toHaveBeenCalled();
+
+    fireEvent.change(input, {target: {value: 'query_name'}});
+    expect(submit).toBeEnabled();
+    fireEvent.click(submit);
+    expect(api.analysis.addFavoriate).toHaveBeenCalledTimes(1);
+});
diff --git 
a/hugegraph-hubble/hubble-fe/src/modules/navigation/ConsoleItem/index.test.js 
b/hugegraph-hubble/hubble-fe/src/modules/navigation/ConsoleItem/index.test.js
index 19be67bb..420f240c 100644
--- 
a/hugegraph-hubble/hubble-fe/src/modules/navigation/ConsoleItem/index.test.js
+++ 
b/hugegraph-hubble/hubble-fe/src/modules/navigation/ConsoleItem/index.test.js
@@ -102,3 +102,15 @@ test('reports a blocked popup without probing the 
Dashboard', async () => {
         'navigation_page.dashboard_popup_blocked'
     );
 });
+
+test('shows why operations are disabled when Dashboard is unavailable', async 
() => {
+    api.auth.getDashboard.mockResolvedValue({status: 500});
+    render(
+        <MemoryRouter future={{v7_relativeSplatPath: true, v7_startTransition: 
true}}>
+            <ConsoleItem />
+        </MemoryRouter>
+    );
+
+    expect(await screen.findByText('navigation_page.dashboard_unavailable'))
+        .toBeInTheDocument();
+});
diff --git a/hugegraph-hubble/hubble-fe/src/modules/navigation/Item/index.js 
b/hugegraph-hubble/hubble-fe/src/modules/navigation/Item/index.js
index e0d8def7..61501b8f 100644
--- a/hugegraph-hubble/hubble-fe/src/modules/navigation/Item/index.js
+++ b/hugegraph-hubble/hubble-fe/src/modules/navigation/Item/index.js
@@ -77,6 +77,16 @@ const Item = props => {
             );
             res.push(content);
         }
+        const reasons = [...new Set(listData
+            .filter(item => item.disabled && item.reason)
+            .map(item => item.reason))];
+        if (reasons.length > 0) {
+            res.push(
+                <div className={style.reason} role='status' 
key='disabled-reason'>
+                    {reasons.join('; ')}
+                </div>
+            );
+        }
         return res;
     };
     return (
diff --git 
a/hugegraph-hubble/hubble-fe/src/modules/navigation/Item/index.module.scss 
b/hugegraph-hubble/hubble-fe/src/modules/navigation/Item/index.module.scss
index 7925371f..a4004cad 100644
--- a/hugegraph-hubble/hubble-fe/src/modules/navigation/Item/index.module.scss
+++ b/hugegraph-hubble/hubble-fe/src/modules/navigation/Item/index.module.scss
@@ -31,3 +31,10 @@
     width: 160px;
     margin-bottom: 10px;
 }
+
+.reason {
+    width: 160px;
+    color: #8c8c8c;
+    font-size: 12px;
+    line-height: 18px;
+}
diff --git a/hugegraph-hubble/hubble-fe/src/utils/rules.js 
b/hugegraph-hubble/hubble-fe/src/utils/rules.js
index 5752a197..cacf1cf1 100644
--- a/hugegraph-hubble/hubble-fe/src/utils/rules.js
+++ b/hugegraph-hubble/hubble-fe/src/utils/rules.js
@@ -149,15 +149,28 @@ const isAccountName = msg => ({
         let res2 = /.*_$/.test(value);
 
         if (!res || res1 || res2) {
-            return Promise.reject(
+            return Promise.reject(new Error(
                 typeof msg === 'string' ? msg : 
validationMessage('account_name_rule')
-            );
+            ));
         }
 
         return Promise.resolve();
     },
 });
 
+const isValidFavoriteName = value => /^[A-Za-z0-9_]{1,48}$/.test(value);
+
+const isFavoriteName = msg => ({
+    validator(_, value) {
+        if (isValidFavoriteName(value)) {
+            return Promise.resolve();
+        }
+        return Promise.reject(new Error(
+            typeof msg === 'string' ? msg : 'Use letters, numbers, or 
underscores only'
+        ));
+    },
+});
+
 // UUID validation
 const isUUID = () => ({
     validator(_, value) {
@@ -192,6 +205,8 @@ export {
     isNoramlName,
     isJDBC,
     isAccountName,
+    isFavoriteName,
+    isValidFavoriteName,
     isUUID,
     isInt,
 };
diff --git a/hugegraph-hubble/hubble-fe/src/utils/rules.test.js 
b/hugegraph-hubble/hubble-fe/src/utils/rules.test.js
index fca659fd..4aaeafe5 100644
--- a/hugegraph-hubble/hubble-fe/src/utils/rules.test.js
+++ b/hugegraph-hubble/hubble-fe/src/utils/rules.test.js
@@ -140,6 +140,18 @@ describe('rules i18n defaults', () => {
         expect(await validate(rules.isAccountName('custom 
account'))).toBe('custom account');
     });
 
+    it('rejects invalid account names with an Error object', async () => {
+        await expect(rules.isAccountName().validator(null, 
'name_too_long_123'))
+            .rejects.toBeInstanceOf(Error);
+    });
+
+    it('accepts only backend-compatible favorite names', async () => {
+        await expect(rules.isFavoriteName().validator(null, 'query_2026'))
+            .resolves.toBeUndefined();
+        await expect(rules.isFavoriteName().validator(null, 'query-2026'))
+            .rejects.toBeInstanceOf(Error);
+    });
+
     it('uses Chinese messages when the active language is Chinese', async () 
=> {
         await i18n.changeLanguage('zh-CN');
 

Reply via email to