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

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


The following commit(s) were added to refs/heads/master by this push:
     new f9c71924 fix(hubble): harden frontend validation and update license 
dependencies (#741)
f9c71924 is described below

commit f9c719248b52539a78d43d98ef610b7566ff9c44
Author: imbajin <[email protected]>
AuthorDate: Sun Jul 12 22:42:27 2026 +0800

    fix(hubble): harden frontend validation and update license dependencies 
(#741)
    
    * fix(hubble): harden frontend validation
    
    * fix(license): update known dependencies for kotlin-stdlib-common
    
    * fix(hubble): address review comments on frontend validation
    
    * fix(hubble): address review feedback
    
    - align favorite-name validation across all editors
    - show localized validation guidance for invalid names
    - allow navigation rows to grow for disabled reasons
    - add Kotlin dependency to the packaged license
    - cover all favorite creation and rename flows
---
 hugegraph-dist/release-docs/LICENSE                |   1 +
 .../scripts/dependency/known-dependencies.txt      |   1 +
 .../src/components/FavoriteNameInput/index.js      |  46 +++++++++
 .../src/components/FavoriteNameInput/index.test.js |  44 ++++++++
 .../i18n/resources/en-US/components/common.json    |   1 +
 .../i18n/resources/zh-CN/components/common.json    |   1 +
 .../algorithm/LogsDetail/ExecuteLog/index.js       |  13 +--
 .../modules/algorithm/LogsDetail/Favorite/index.js |  10 +-
 .../analysis/LogsDetail/ExecuteLog/index.js        |  13 +--
 .../modules/analysis/LogsDetail/Favorite/index.js  |  10 +-
 .../analysis/QueryBar/ContentCommon/index.js       |  10 +-
 .../analysis/QueryBar/ContentCommon/index.test.js  |  77 ++++++++++++++
 .../src/modules/favorite-name-validation.test.js   | 113 +++++++++++++++++++++
 .../modules/navigation/ConsoleItem/index.test.js   |  12 +++
 .../src/modules/navigation/Home/index.module.scss  |   2 +-
 .../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 |  23 +++++
 19 files changed, 377 insertions(+), 36 deletions(-)

diff --git a/hugegraph-dist/release-docs/LICENSE 
b/hugegraph-dist/release-docs/LICENSE
index 7543d082..6b0c88c0 100644
--- a/hugegraph-dist/release-docs/LICENSE
+++ b/hugegraph-dist/release-docs/LICENSE
@@ -484,6 +484,7 @@ See licenses/ for text of these licenses.
        hugegraph-hubble/ui/favicon.ico
        (Apache License, Version 2.0) * kotlin-stdlib 
(org.jetbrains.kotlin:kotlin-stdlib:1.6.20 - 
https://github.com/JetBrains/kotlin)
        (Apache License, Version 2.0) * kotlin-stdlib-common 
(org.jetbrains.kotlin:kotlin-stdlib-common:1.5.31 - 
https://github.com/JetBrains/kotlin)
+       (Apache License, Version 2.0) * kotlin-stdlib-common 
(org.jetbrains.kotlin:kotlin-stdlib-common:1.6.20 - 
https://github.com/JetBrains/kotlin)
        (Apache License, Version 2.0) * kotlin-stdlib-jdk7 
(org.jetbrains.kotlin:kotlin-stdlib-jdk7:1.2.71 - 
https://github.com/JetBrains/kotlin)
        (Apache License, Version 2.0) * kotlin-stdlib-jdk7 
(org.jetbrains.kotlin:kotlin-stdlib-jdk7:1.6.10 - 
https://github.com/JetBrains/kotlin)
        (Apache License, Version 2.0) * kotlin-stdlib-jdk8 
(org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.2.71 - 
https://github.com/JetBrains/kotlin)
diff --git a/hugegraph-dist/scripts/dependency/known-dependencies.txt 
b/hugegraph-dist/scripts/dependency/known-dependencies.txt
index ba38b94c..b9a88cdd 100644
--- a/hugegraph-dist/scripts/dependency/known-dependencies.txt
+++ b/hugegraph-dist/scripts/dependency/known-dependencies.txt
@@ -348,6 +348,7 @@ kerby-xdr-1.0.1.jar
 kerby-xdr-2.0.0.jar
 kotlin-stdlib-1.6.20.jar
 kotlin-stdlib-common-1.5.31.jar
+kotlin-stdlib-common-1.6.20.jar
 kotlin-stdlib-jdk7-1.2.71.jar
 kotlin-stdlib-jdk7-1.6.10.jar
 kotlin-stdlib-jdk8-1.2.71.jar
diff --git 
a/hugegraph-hubble/hubble-fe/src/components/FavoriteNameInput/index.js 
b/hugegraph-hubble/hubble-fe/src/components/FavoriteNameInput/index.js
new file mode 100644
index 00000000..cb1915d5
--- /dev/null
+++ b/hugegraph-hubble/hubble-fe/src/components/FavoriteNameInput/index.js
@@ -0,0 +1,46 @@
+/*
+ * 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
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import {Input} from 'antd';
+import {useTranslation} from 'react-i18next';
+
+import {isValidFavoriteName} from '../../utils/rules';
+
+const FavoriteNameInput = props => {
+    const {t} = useTranslation();
+    const {value, ...inputProps} = props;
+    const invalid = value && !isValidFavoriteName(value);
+
+    return (
+        <>
+            <Input
+                {...inputProps}
+                value={value}
+                showCount
+                maxLength={48}
+                status={invalid ? 'error' : undefined}
+            />
+            {invalid && (
+                <div role='alert' style={{color: '#ff4d4f', fontSize: '12px'}}>
+                    {t('common.validation.favorite_name_rule')}
+                </div>
+            )}
+        </>
+    );
+};
+
+export default FavoriteNameInput;
diff --git 
a/hugegraph-hubble/hubble-fe/src/components/FavoriteNameInput/index.test.js 
b/hugegraph-hubble/hubble-fe/src/components/FavoriteNameInput/index.test.js
new file mode 100644
index 00000000..ee6a6de8
--- /dev/null
+++ b/hugegraph-hubble/hubble-fe/src/components/FavoriteNameInput/index.test.js
@@ -0,0 +1,44 @@
+/*
+ * 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
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import {fireEvent, render, screen} from '@testing-library/react';
+import {useCallback, useState} from 'react';
+
+import FavoriteNameInput from './index';
+
+jest.mock('react-i18next', () => ({
+    initReactI18next: {type: '3rdParty', init: jest.fn()},
+    useTranslation: () => ({t: key => key}),
+}));
+
+test('explains invalid favorite names and clears the error for valid names', 
() => {
+    const FavoriteNameEditor = () => {
+        const [name, setName] = useState('');
+        const onChange = useCallback(e => setName(e.target.value), []);
+        return <FavoriteNameInput value={name} onChange={onChange} />;
+    };
+    render(<FavoriteNameEditor />);
+
+    const input = screen.getByRole('textbox');
+    fireEvent.change(input, {target: {value: 'query-name'}});
+    expect(screen.getByRole('alert')).toHaveTextContent(
+        'common.validation.favorite_name_rule'
+    );
+
+    fireEvent.change(input, {target: {value: 'query_name'}});
+    expect(screen.queryByRole('alert')).not.toBeInTheDocument();
+});
diff --git 
a/hugegraph-hubble/hubble-fe/src/i18n/resources/en-US/components/common.json 
b/hugegraph-hubble/hubble-fe/src/i18n/resources/en-US/components/common.json
index 44f8f3a9..f2fdcafe 100644
--- a/hugegraph-hubble/hubble-fe/src/i18n/resources/en-US/components/common.json
+++ b/hugegraph-hubble/hubble-fe/src/i18n/resources/en-US/components/common.json
@@ -27,6 +27,7 @@
       "normal_name_rule": "Use Chinese characters, letters, numbers, or 
underscores only, up to 20 characters",
       "jdbc_rule": "Enter a valid JDBC URL, for example: 
jdbc:mysql://127.0.0.1:3306/db_name",
       "account_name_rule": "Account name must be within 16 characters and 
cannot start or end with an underscore",
+      "favorite_name_rule": "Use Chinese characters, letters, numbers, or 
underscores only, up to 48 characters",
       "invalid_data_format": "Invalid data format"
     }
   },
diff --git 
a/hugegraph-hubble/hubble-fe/src/i18n/resources/zh-CN/components/common.json 
b/hugegraph-hubble/hubble-fe/src/i18n/resources/zh-CN/components/common.json
index 5e3147c2..e8a4e004 100644
--- a/hugegraph-hubble/hubble-fe/src/i18n/resources/zh-CN/components/common.json
+++ b/hugegraph-hubble/hubble-fe/src/i18n/resources/zh-CN/components/common.json
@@ -27,6 +27,7 @@
       "normal_name_rule": "只能包含中文、字母、数字、_, 不能超过20个字符",
       "jdbc_rule": "请输入正确的jdbc url, 例如:jdbc:mysql://127.0.0.1:3306/db_name",
       "account_name_rule": "账号名不超过16个字符,且不能以下划线开始和结尾",
+      "favorite_name_rule": "只能包含中文、字母、数字、_, 不能超过48个字符",
       "invalid_data_format": "非法的数据格式"
     }
   },
diff --git 
a/hugegraph-hubble/hubble-fe/src/modules/algorithm/LogsDetail/ExecuteLog/index.js
 
b/hugegraph-hubble/hubble-fe/src/modules/algorithm/LogsDetail/ExecuteLog/index.js
index 310309e0..d4cf55c0 100644
--- 
a/hugegraph-hubble/hubble-fe/src/modules/algorithm/LogsDetail/ExecuteLog/index.js
+++ 
b/hugegraph-hubble/hubble-fe/src/modules/algorithm/LogsDetail/ExecuteLog/index.js
@@ -22,8 +22,10 @@
 
 import React, {useState, useCallback} from 'react';
 import {useTranslation} from 'react-i18next';
-import {Button, Table, Space, Tag, Input, Popconfirm} from 'antd';
+import {Button, Table, Space, Tag, Popconfirm} from 'antd';
 import ExecutionContent from '../../../../components/ExecutionContent';
+import FavoriteNameInput from '../../../../components/FavoriteNameInput';
+import {isValidFavoriteName} from '../../../../utils/rules';
 import c from './index.module.scss';
 
 const EXECUTE_TYPE_KEY = {
@@ -67,17 +69,14 @@ const ExecuteLog = props => {
     } = props;
 
     const [favoriteName, setFavoriteName] = useState();
-    const [disabledFavorite, setDisabledFavorite]  = useState(true);
 
     const onFavoraiteName = useCallback(
         e => {
             setFavoriteName(e.target.value);
-            e.target.value ? setDisabledFavorite(false) : 
setDisabledFavorite(true);
         }, []);
 
     const onFavoriteCard = useCallback(() => {
         setFavoriteName('');
-        setDisabledFavorite(true);
     }, []);
 
     const updateAddCollection = useCallback(
@@ -97,11 +96,9 @@ const ExecuteLog = props => {
             <div style={{marginBottom: '16px'}}>
                 {t('analysis.logs.favorite_statement')}
             </div>
-            <Input
+            <FavoriteNameInput
                 style={{marginBottom: '18px'}}
                 placeholder={t('analysis.logs.favorite_name_placeholder')}
-                showCount
-                maxLength={48}
                 value={favoriteName}
                 onChange={onFavoraiteName}
             />
@@ -169,7 +166,7 @@ const ExecuteLog = props => {
                             placement="left"
                             title={favoriteContent(rowData)}
                             onConfirm={createValueHandler(onAddFavorite, 
rowData.content)}
-                            okButtonProps={{disabled: disabledFavorite}}
+                            okButtonProps={{disabled: 
!isValidFavoriteName(favoriteName)}}
                             okText={t('analysis.logs.action.favorite')}
                             cancelText={t('common.action.cancel')}
                         >
diff --git 
a/hugegraph-hubble/hubble-fe/src/modules/algorithm/LogsDetail/Favorite/index.js 
b/hugegraph-hubble/hubble-fe/src/modules/algorithm/LogsDetail/Favorite/index.js
index dbbbe8ae..7a90e519 100644
--- 
a/hugegraph-hubble/hubble-fe/src/modules/algorithm/LogsDetail/Favorite/index.js
+++ 
b/hugegraph-hubble/hubble-fe/src/modules/algorithm/LogsDetail/Favorite/index.js
@@ -25,6 +25,8 @@ import {useTranslation} from 'react-i18next';
 import Highlighter from 'react-highlight-words';
 import {Button, Table, Input, Popconfirm, Modal} from 'antd';
 import ExecutionContent from '../../../../components/ExecutionContent';
+import FavoriteNameInput from '../../../../components/FavoriteNameInput';
+import {isValidFavoriteName} from '../../../../utils/rules';
 import c from './index.module.scss';
 
 const createValueHandler = (handler, value) => () => handler(value);
@@ -48,7 +50,6 @@ const Favorite = props => {
     const [favoriteName, setFavoriteName] = useState();
     const [searchCache, setSearchCache] = useState('');
     const [search, setSearch] = useState('');
-    const [isDisabledName, setDisabledName]  = useState(false);
 
     const changeCollection = useCallback(
         rowData => {
@@ -72,7 +73,6 @@ const Favorite = props => {
     const onChangeFavoraiteName = useCallback(
         e => {
             setFavoriteName(e.target.value);
-            e.target.value ? setDisabledName(false) : setDisabledName(true);
         }, []);
 
     const onConfirm = id => {
@@ -90,11 +90,9 @@ const Favorite = props => {
             <div style={{marginBottom: '16px'}}>
                 {t('analysis.logs.edit_name')}
             </div>
-            <Input
+            <FavoriteNameInput
                 style={{marginBottom: '18px'}}
                 placeholder={t('analysis.logs.favorite_name_placeholder')}
-                showCount
-                maxLength={48}
                 value={favoriteName}
                 onChange={onChangeFavoraiteName}
             />
@@ -159,7 +157,7 @@ const Favorite = props => {
                             title={editFavoriteForm}
                             onConfirm={createValueHandler(onSaveEditFavorite, 
rowData)}
                             okText={t('analysis.logs.action.save')}
-                            okButtonProps={{disabled: isDisabledName}}
+                            okButtonProps={{disabled: 
!isValidFavoriteName(favoriteName)}}
                             cancelText={t('common.action.cancel')}
                         >
                             <Button
diff --git 
a/hugegraph-hubble/hubble-fe/src/modules/analysis/LogsDetail/ExecuteLog/index.js
 
b/hugegraph-hubble/hubble-fe/src/modules/analysis/LogsDetail/ExecuteLog/index.js
index 2dcb9a94..b29f02dc 100644
--- 
a/hugegraph-hubble/hubble-fe/src/modules/analysis/LogsDetail/ExecuteLog/index.js
+++ 
b/hugegraph-hubble/hubble-fe/src/modules/analysis/LogsDetail/ExecuteLog/index.js
@@ -22,8 +22,10 @@
 
 import {useState, useCallback} from 'react';
 import {useTranslation} from 'react-i18next';
-import {Button, Table, Space, Tag, Input, Popconfirm} from 'antd';
+import {Button, Table, Space, Tag, Popconfirm} from 'antd';
 import ExecutionContent from '../../../../components/ExecutionContent';
+import FavoriteNameInput from '../../../../components/FavoriteNameInput';
+import {isValidFavoriteName} from '../../../../utils/rules';
 import c from './index.module.scss';
 
 const EXECUTE_TYPE_KEY = {
@@ -116,7 +118,6 @@ const ExecuteLog = props => {
     } = props;
 
     const [favoriteName, setFavoriteName] = useState();
-    const [disabledFavorite, setDisabledFavorite]  = useState(true);
 
     const loadStatements = useCallback(
         (text, rowData, index) => {
@@ -130,7 +131,6 @@ const ExecuteLog = props => {
     const onFavoraiteName = useCallback(
         e => {
             setFavoriteName(e.target.value);
-            e.target.value ? setDisabledFavorite(false) : 
setDisabledFavorite(true);
         },
         []
     );
@@ -138,7 +138,6 @@ const ExecuteLog = props => {
     const onFavoriteCard = useCallback(
         () => {
             setFavoriteName('');
-            setDisabledFavorite(true);
         },
         []
     );
@@ -163,11 +162,9 @@ const ExecuteLog = props => {
                 <div style={{marginBottom: '16px'}}>
                     {t('analysis.logs.favorite_statement')}
                 </div>
-                <Input
+                <FavoriteNameInput
                     style={{marginBottom: '18px'}}
                     placeholder={t('analysis.logs.favorite_name_placeholder')}
-                    showCount
-                    maxLength={48}
                     value={favoriteName}
                     onChange={onFavoraiteName}
                 />
@@ -254,7 +251,7 @@ const ExecuteLog = props => {
                         onAddFavorite={onAddFavorite}
                         onFavoriteCard={onFavoriteCard}
                         loadStatements={loadStatements}
-                        disabledFavorite={disabledFavorite}
+                        disabledFavorite={!isValidFavoriteName(favoriteName)}
                         t={t}
                     />
                 );
diff --git 
a/hugegraph-hubble/hubble-fe/src/modules/analysis/LogsDetail/Favorite/index.js 
b/hugegraph-hubble/hubble-fe/src/modules/analysis/LogsDetail/Favorite/index.js
index ae6d74f4..5da1be01 100644
--- 
a/hugegraph-hubble/hubble-fe/src/modules/analysis/LogsDetail/Favorite/index.js
+++ 
b/hugegraph-hubble/hubble-fe/src/modules/analysis/LogsDetail/Favorite/index.js
@@ -24,6 +24,8 @@ import React, {useState, useCallback} from 'react';
 import {useTranslation} from 'react-i18next';
 import {Button, Table, Input, Popconfirm, Modal} from 'antd';
 import ExecutionContent from '../../../../components/ExecutionContent';
+import FavoriteNameInput from '../../../../components/FavoriteNameInput';
+import {isValidFavoriteName} from '../../../../utils/rules';
 import Highlighter from 'react-highlight-words';
 import c from './index.module.scss';
 
@@ -98,7 +100,6 @@ const Favorite = props => {
     const [favoriteName, setFavoriteName] = useState();
     const [searchCache, setSearchCache] = useState('');
     const [search, setSearch] = useState('');
-    const [isDisabledName, setDisabledName]  = useState(false);
 
     const loadStatements = useCallback(
         (content, index) => {
@@ -135,7 +136,6 @@ const Favorite = props => {
     const onChangeFavoraiteName = useCallback(
         e => {
             setFavoriteName(e.target.value);
-            e.target.value ? setDisabledName(false) : setDisabledName(true);
         },
         []
     );
@@ -155,11 +155,9 @@ const Favorite = props => {
             <div style={{marginBottom: '16px'}}>
                 {t('analysis.logs.edit_name')}
             </div>
-            <Input
+            <FavoriteNameInput
                 style={{marginBottom: '18px'}}
                 placeholder={t('analysis.logs.favorite_name_placeholder')}
-                showCount
-                maxLength={48}
                 value={favoriteName}
                 onChange={onChangeFavoraiteName}
             />
@@ -225,7 +223,7 @@ const Favorite = props => {
                         onEditFavorite={onEditFavorite}
                         onConfirm={onConfirm}
                         editFavoriteForm={editFavoriteForm}
-                        isDisabledName={isDisabledName}
+                        isDisabledName={!isValidFavoriteName(favoriteName)}
                         t={t}
                     />
                 );
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..7fe4dd4a 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
@@ -22,11 +22,13 @@
 
 import React, {useCallback, useState, useContext} from 'react';
 import {useTranslation} from 'react-i18next';
-import {Button, Tooltip, Dropdown, Input, Popover, message, Space} from 'antd';
+import {Button, Tooltip, Dropdown, Popover, message, Space} from 'antd';
 import {UpOutlined, DownOutlined, QuestionCircleOutlined} from 
'@ant-design/icons';
 import {GREMLIN_EXECUTES_MODE} from '../../../../utils/constants';
 import GraphAnalysisContext from '../../../Context';
 import classnames from 'classnames';
+import {isValidFavoriteName} from '../../../../utils/rules';
+import FavoriteNameInput from '../../../../components/FavoriteNameInput';
 import c from './index.module.scss';
 import * as api from '../../../../api/index';
 import KeyboardAction from '../../../../components/KeyboardAction';
@@ -139,17 +141,15 @@ const ContentCommon = props => {
         e => {
             const favoriteName = e.target.value;
             setFavoriteName(favoriteName);
-            favoriteName ? setDisabledFavorite(false) : 
setDisabledFavorite(true);
+            setDisabledFavorite(!isValidFavoriteName(favoriteName));
         },
         []
     );
 
     const favoriteContent = (
         <>
-            <Input
+            <FavoriteNameInput
                 placeholder={t('analysis.query.favorite_name_placeholder')}
-                showCount
-                maxLength={48}
                 value={favoriteName}
                 onChange={onChangeFavoraiteName}
             />
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..aac91325
--- /dev/null
+++ 
b/hugegraph-hubble/hubble-fe/src/modules/analysis/QueryBar/ContentCommon/index.test.js
@@ -0,0 +1,77 @@
+/*
+ *
+ * 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 (the
+ * "License"); you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+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'));
+    expect(submit).toBeDefined();
+
+    fireEvent.change(input, {target: {value: 'query-name'}});
+    expect(submit).toBeDisabled();
+    expect(screen.getByRole('alert')).toHaveTextContent(
+        'common.validation.favorite_name_rule'
+    );
+    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/favorite-name-validation.test.js 
b/hugegraph-hubble/hubble-fe/src/modules/favorite-name-validation.test.js
new file mode 100644
index 00000000..fcfe9b36
--- /dev/null
+++ b/hugegraph-hubble/hubble-fe/src/modules/favorite-name-validation.test.js
@@ -0,0 +1,113 @@
+/*
+ * 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
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import {fireEvent, render, screen} from '@testing-library/react';
+
+import AlgorithmExecuteLog from './algorithm/LogsDetail/ExecuteLog';
+import AlgorithmFavorite from './algorithm/LogsDetail/Favorite';
+import AnalysisExecuteLog from './analysis/LogsDetail/ExecuteLog';
+import AnalysisFavorite from './analysis/LogsDetail/Favorite';
+
+jest.mock('react-i18next', () => ({
+    initReactI18next: {type: '3rdParty', init: jest.fn()},
+    useTranslation: () => ({t: key => key}),
+}));
+jest.mock('../components/ExecutionContent', () => () => <div />);
+
+beforeEach(() => {
+    window.matchMedia = jest.fn().mockImplementation(query => ({
+        matches: false,
+        media: query,
+        addListener: jest.fn(),
+        removeListener: jest.fn(),
+    }));
+});
+
+const executeRecord = {
+    id: 1,
+    content: 'g.V()',
+    status: 'SUCCESS',
+    type: 'GREMLIN',
+};
+const favoriteRecord = {
+    id: 1,
+    name: 'valid_name',
+    content: 'g.V()',
+};
+
+const assertInvalidNameDisablesConfirm = async (actionName, confirmName) => {
+    fireEvent.click(screen.getByRole('button', {name: actionName}));
+    const input = await screen.findByPlaceholderText(
+        'analysis.logs.favorite_name_placeholder'
+    );
+    fireEvent.change(input, {target: {value: 'invalid-name'}});
+    const confirm = screen.getAllByRole('button', {name: confirmName})
+        .find(button => button.disabled);
+    expect(confirm).toBeDefined();
+};
+
+test.each([
+    ['analysis', AnalysisExecuteLog, {
+        executeLogsDataRecords: [executeRecord],
+        executeLogsDataTotal: 1,
+        onLoadContent: jest.fn(),
+    }],
+    ['algorithm', AlgorithmExecuteLog, {
+        executionLogsDataRecords: [executeRecord],
+        executionLogsDataTotal: 1,
+    }],
+])('%s execution log rejects invalid favorite names', async (_, Component, 
dataProps) => {
+    render(<Component
+        {...dataProps}
+        pageExecute={1}
+        pageSize={10}
+        onExecutePageChange={jest.fn()}
+        onAddCollection={jest.fn()}
+    />);
+
+    await assertInvalidNameDisablesConfirm(
+        'analysis.logs.action.favorite',
+        'analysis.logs.action.favorite'
+    );
+});
+
+test.each([
+    ['analysis', AnalysisFavorite, {
+        onChangeSearchValue: jest.fn(),
+        onLoadContent: jest.fn(),
+    }],
+    ['algorithm', AlgorithmFavorite, {
+        onChangeFavorSearch: jest.fn(),
+    }],
+])('%s favorite editor rejects invalid renamed favorites', async (_, 
Component, dataProps) => {
+    render(<Component
+        {...dataProps}
+        favoriteQueriesDataRecords={[favoriteRecord]}
+        favoriteQueriesDataTotal={1}
+        pageFavorite={1}
+        pageSize={10}
+        onFavoritePageChange={jest.fn()}
+        onSortChange={jest.fn()}
+        onEditCollection={jest.fn()}
+        onDel={jest.fn()}
+    />);
+
+    await assertInvalidNameDisablesConfirm(
+        'analysis.logs.action.edit_name',
+        'analysis.logs.action.save'
+    );
+});
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/Home/index.module.scss 
b/hugegraph-hubble/hubble-fe/src/modules/navigation/Home/index.module.scss
index e5e17415..06c7e6d5 100644
--- a/hugegraph-hubble/hubble-fe/src/modules/navigation/Home/index.module.scss
+++ b/hugegraph-hubble/hubble-fe/src/modules/navigation/Home/index.module.scss
@@ -28,7 +28,7 @@
 }
 
 .container {
-    height: 230px;
+    min-height: 230px;
     display: flex;
     column-gap: 20px;
     align-items: center;
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..3255860d 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 => typeof value === 'string' && 
/^[A-Za-z0-9_\u4e00-\u9fa5]{1,48}$/.test(value);
+
+const isFavoriteName = msg => ({
+    validator(_, value) {
+        if (isValidFavoriteName(value)) {
+            return Promise.resolve();
+        }
+        return Promise.reject(new Error(
+            typeof msg === 'string' ? msg : 
validationMessage('favorite_name_rule')
+        ));
+    },
+});
+
 // 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..c99754a0 100644
--- a/hugegraph-hubble/hubble-fe/src/utils/rules.test.js
+++ b/hugegraph-hubble/hubble-fe/src/utils/rules.test.js
@@ -38,6 +38,8 @@ jest.mock('../i18n', () => ({
                     'Enter a valid JDBC URL, for example: 
jdbc:mysql://127.0.0.1:3306/db_name',
                 'common.validation.account_name_rule':
                     'Account name must be within 16 characters and cannot 
start or end with an underscore',
+                'common.validation.favorite_name_rule':
+                    'Use Chinese characters, letters, numbers, or underscores 
only, up to 48 characters',
                 'common.validation.invalid_data_format': 'Invalid data format',
             },
             'zh-CN': {
@@ -53,6 +55,7 @@ jest.mock('../i18n', () => ({
                 'common.validation.jdbc_rule':
                     '请输入正确的jdbc url, 例如:jdbc:mysql://127.0.0.1:3306/db_name',
                 'common.validation.account_name_rule': 
'账号名不超过16个字符,且不能以下划线开始和结尾',
+                'common.validation.favorite_name_rule': '只能包含中文、字母、数字、_, 
不能超过48个字符',
                 'common.validation.invalid_data_format': '非法的数据格式',
             },
         };
@@ -140,6 +143,25 @@ 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 i18n.changeLanguage('en-US');
+        await expect(rules.isFavoriteName().validator(null, 'query_2026'))
+            .resolves.toBeUndefined();
+        await expect(rules.isFavoriteName().validator(null, '我的查询_123'))
+            .resolves.toBeUndefined();
+        await expect(rules.isFavoriteName().validator(null, 'query-2026'))
+            .rejects.toThrow('Use Chinese characters, letters, numbers, or 
underscores only, up to 48 characters');
+        await expect(rules.isFavoriteName().validator(null, undefined))
+            .rejects.toThrow('Use Chinese characters, letters, numbers, or 
underscores only, up to 48 characters');
+        await expect(rules.isFavoriteName().validator(null, null))
+            .rejects.toThrow('Use Chinese characters, letters, numbers, or 
underscores only, up to 48 characters');
+    });
+
     it('uses Chinese messages when the active language is Chinese', async () 
=> {
         await i18n.changeLanguage('zh-CN');
 
@@ -156,6 +178,7 @@ describe('rules i18n defaults', () => {
             [rules.isNoramlName(), '只能包含中文、字母、数字、_, 不能超过20个字符'],
             [rules.isJDBC(), '请输入正确的jdbc url, 
例如:jdbc:mysql://127.0.0.1:3306/db_name'],
             [rules.isAccountName(), '账号名不超过16个字符,且不能以下划线开始和结尾'],
+            [rules.isFavoriteName(), '只能包含中文、字母、数字、_, 不能超过48个字符'],
             [rules.isUUID(), '非法的数据格式'],
             [rules.isInt(), '非法的数据格式'],
         ];


Reply via email to