Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions hugegraph-dist/scripts/dependency/known-dependencies.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Comment thread
imbajin marked this conversation as resolved.
kotlin-stdlib-jdk7-1.2.71.jar
kotlin-stdlib-jdk7-1.6.10.jar
kotlin-stdlib-jdk8-1.2.71.jar
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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"
}
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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": "非法的数据格式"
}
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -139,7 +140,7 @@ const ContentCommon = props => {
e => {
const favoriteName = e.target.value;
setFavoriteName(favoriteName);
favoriteName ? setDisabledFavorite(false) : setDisabledFavorite(true);
setDisabledFavorite(!isValidFavoriteName(favoriteName));
Comment thread
imbajin marked this conversation as resolved.
},
[]
);
Expand All @@ -152,6 +153,8 @@ const ContentCommon = props => {
maxLength={48}
value={favoriteName}
onChange={onChangeFavoraiteName}
status={favoriteName && !isValidFavoriteName(favoriteName)
Comment thread
imbajin marked this conversation as resolved.
Outdated
? 'error' : undefined}
/>
<Space style={{marginTop: '24px'}}>
<Button type='primary' onClick={onOkFavorite} disabled={disabledFavorite}>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
/*
*
* 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();
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);
});
Original file line number Diff line number Diff line change
Expand Up @@ -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();
});
10 changes: 10 additions & 0 deletions hugegraph-hubble/hubble-fe/src/modules/navigation/Item/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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'>
Comment thread
imbajin marked this conversation as resolved.
{reasons.join('; ')}
</div>
);
}
return res;
};
return (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,3 +31,10 @@
width: 160px;
margin-bottom: 10px;
}

.reason {
width: 160px;
color: #8c8c8c;

@VGalaxies VGalaxies Jul 12, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ P2 · Increase the disabled-reason text contrast

📐 Measured result

Metric Observed Required / target
Text contrast 3.36:1 >= 4.5:1

The changed 12px reason text uses #8c8c8c over the white navigation background, producing a 3.36:1 contrast ratio.

References: WCAG 2.2 contrast minimum

Warning

Impact: Low-vision users may be unable to read the persistent explanation of why Dashboard operations are disabled.

🛠️ Suggested change

Use a darker text color that reaches at least 4.5:1, such as #595959, which provides approximately 7.00:1 contrast.


🤖 Codex review · GPT-5.6 Sol · effort: xhigh

font-size: 12px;
line-height: 18px;
}
19 changes: 17 additions & 2 deletions hugegraph-hubble/hubble-fe/src/utils/rules.js
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down Expand Up @@ -192,6 +205,8 @@ export {
isNoramlName,
isJDBC,
isAccountName,
isFavoriteName,
isValidFavoriteName,
isUUID,
isInt,
};
22 changes: 22 additions & 0 deletions hugegraph-hubble/hubble-fe/src/utils/rules.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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': {
Expand All @@ -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': '非法的数据格式',
},
};
Expand Down Expand Up @@ -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');
});
Comment thread
imbajin marked this conversation as resolved.

it('uses Chinese messages when the active language is Chinese', async () => {
await i18n.changeLanguage('zh-CN');

Expand Down
Loading