You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@hugegraph.apache.org by ji...@apache.org on 2026/07/12 06:45:27 UTC
(hugegraph-toolchain) 01/02: fix(hubble): harden frontend validation
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 <ji...@apache.org>
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');