From 9118875464f9ef151bf84e22fc4bf83c3b6c21f4 Mon Sep 17 00:00:00 2001 From: imbajin Date: Sun, 12 Jul 2026 14:44:15 +0800 Subject: [PATCH 01/57] fix(license): update known dependencies for kotlin-stdlib-common --- .dockerignore | 39 ------ README.md | 20 +--- hugegraph-dist/release-docs/LICENSE | 1 - hugegraph-hubble/Dockerfile | 5 +- .../src/components/FavoriteNameInput/index.js | 46 ------- .../FavoriteNameInput/index.test.js | 44 ------- .../resources/en-US/components/common.json | 1 - .../resources/zh-CN/components/common.json | 1 - .../algorithm/LogsDetail/ExecuteLog/index.js | 13 +- .../algorithm/LogsDetail/Favorite/index.js | 10 +- .../analysis/LogsDetail/ExecuteLog/index.js | 13 +- .../analysis/LogsDetail/Favorite/index.js | 10 +- .../analysis/QueryBar/ContentCommon/index.js | 9 +- .../QueryBar/ContentCommon/index.test.js | 21 +--- .../modules/favorite-name-validation.test.js | 113 ------------------ .../modules/navigation/Home/index.module.scss | 2 +- hugegraph-hubble/hubble-fe/src/utils/rules.js | 4 +- .../hubble-fe/src/utils/rules.test.js | 13 +- hugegraph-loader/Dockerfile | 16 ++- 19 files changed, 55 insertions(+), 326 deletions(-) delete mode 100644 .dockerignore delete mode 100644 hugegraph-hubble/hubble-fe/src/components/FavoriteNameInput/index.js delete mode 100644 hugegraph-hubble/hubble-fe/src/components/FavoriteNameInput/index.test.js delete mode 100644 hugegraph-hubble/hubble-fe/src/modules/favorite-name-validation.test.js diff --git a/.dockerignore b/.dockerignore deleted file mode 100644 index af6405a34..000000000 --- a/.dockerignore +++ /dev/null @@ -1,39 +0,0 @@ -# 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. - -# Version-control and local development metadata -.git -.github -.idea -.vscode -.codex-task -**/.DS_Store - -# Build outputs and dependency caches -**/target -**/node_modules -**/.cache - -# Modules not used by the Hubble image build. Keep the Maven module POMs so -# the root reactor can still be parsed, and keep release docs used by hubble-dist. -hugegraph-client-go -hugegraph-spark-connector/** -!hugegraph-spark-connector/pom.xml -hugegraph-tools/** -!hugegraph-tools/pom.xml -hugegraph-dist/** -!hugegraph-dist/pom.xml -!hugegraph-dist/release-docs/ -!hugegraph-dist/release-docs/** diff --git a/README.md b/README.md index 7846c33f0..322e6bda5 100644 --- a/README.md +++ b/README.md @@ -406,28 +406,12 @@ docker run --rm \ Build images locally: ```bash # Loader -docker build -f hugegraph-loader/Dockerfile \ - -t hugegraph/hugegraph-loader:latest . +cd hugegraph-loader && docker build -t hugegraph/hugegraph-loader:latest . # Hubble -docker build -f hugegraph-hubble/Dockerfile \ - -t hugegraph/hugegraph-hubble:latest . +cd hugegraph-hubble && docker build -t hugegraph/hugegraph-hubble:latest . ``` -Multi-platform builds use BuildKit's automatic platform arguments. The Maven -and Node build stages run on `$BUILDPLATFORM`, while the final JRE stage uses -`$TARGETPLATFORM`. Java bytecode and frontend assets are architecture-neutral, -so they are built once without QEMU. Target-stage package installation still -runs for each architecture. - -This optimization applies only to architecture-independent build outputs. A -component that compiles native code must use a target-platform build stage or -separate platform stages. Loader and Hubble packaging is validated on arm64; -native dependencies must still be audited. Loader includes arm64 variants for -Snappy, LZ4, Commons Crypto, and gRPC tcnative. Some optional legacy HBase and -Jansi natives remain x86-only, so their fallback paths require target-runtime -validation when those optional features are used. - ## Documentation - [HugeGraph Toolchain Overview](https://hugegraph.apache.org/docs/quickstart/) diff --git a/hugegraph-dist/release-docs/LICENSE b/hugegraph-dist/release-docs/LICENSE index 6b0c88c01..7543d0822 100644 --- a/hugegraph-dist/release-docs/LICENSE +++ b/hugegraph-dist/release-docs/LICENSE @@ -484,7 +484,6 @@ 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-hubble/Dockerfile b/hugegraph-hubble/Dockerfile index 97f72ed57..4591715d7 100644 --- a/hugegraph-hubble/Dockerfile +++ b/hugegraph-hubble/Dockerfile @@ -15,12 +15,13 @@ # limitations under the License. # -FROM --platform=$BUILDPLATFORM maven:3.9.0-eclipse-temurin-11 AS build +FROM maven:3.9.0-eclipse-temurin-11 AS build ENV NODE_OPTIONS=--dns-result-order=ipv4first ARG MAVEN_ARGS +COPY . /pkg WORKDIR /pkg RUN set -x \ @@ -29,8 +30,6 @@ RUN set -x \ && apt-get clean \ && rm -rf /var/lib/apt/lists/* -COPY . /pkg - RUN set -x \ && mvn install $MAVEN_ARGS -pl hugegraph-client,hugegraph-loader -am -Dmaven.javadoc.skip=true -DskipTests -ntp \ && cd /pkg/hugegraph-hubble/ \ diff --git a/hugegraph-hubble/hubble-fe/src/components/FavoriteNameInput/index.js b/hugegraph-hubble/hubble-fe/src/components/FavoriteNameInput/index.js deleted file mode 100644 index cb1915d59..000000000 --- a/hugegraph-hubble/hubble-fe/src/components/FavoriteNameInput/index.js +++ /dev/null @@ -1,46 +0,0 @@ -/* - * 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 ( - <> - - {invalid && ( -
- {t('common.validation.favorite_name_rule')} -
- )} - - ); -}; - -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 deleted file mode 100644 index ee6a6de81..000000000 --- a/hugegraph-hubble/hubble-fe/src/components/FavoriteNameInput/index.test.js +++ /dev/null @@ -1,44 +0,0 @@ -/* - * 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 ; - }; - render(); - - 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 f2fdcafe8..44f8f3a96 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,7 +27,6 @@ "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 e8a4e004e..5e3147c27 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,7 +27,6 @@ "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 d4cf55c01..310309e02 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,10 +22,8 @@ import React, {useState, useCallback} from 'react'; import {useTranslation} from 'react-i18next'; -import {Button, Table, Space, Tag, Popconfirm} from 'antd'; +import {Button, Table, Space, Tag, Input, 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 = { @@ -69,14 +67,17 @@ 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( @@ -96,9 +97,11 @@ const ExecuteLog = props => {
{t('analysis.logs.favorite_statement')}
- @@ -166,7 +169,7 @@ const ExecuteLog = props => { placement="left" title={favoriteContent(rowData)} onConfirm={createValueHandler(onAddFavorite, rowData.content)} - okButtonProps={{disabled: !isValidFavoriteName(favoriteName)}} + okButtonProps={{disabled: disabledFavorite}} 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 7a90e5191..dbbbe8aeb 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,8 +25,6 @@ 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); @@ -50,6 +48,7 @@ const Favorite = props => { const [favoriteName, setFavoriteName] = useState(); const [searchCache, setSearchCache] = useState(''); const [search, setSearch] = useState(''); + const [isDisabledName, setDisabledName] = useState(false); const changeCollection = useCallback( rowData => { @@ -73,6 +72,7 @@ const Favorite = props => { const onChangeFavoraiteName = useCallback( e => { setFavoriteName(e.target.value); + e.target.value ? setDisabledName(false) : setDisabledName(true); }, []); const onConfirm = id => { @@ -90,9 +90,11 @@ const Favorite = props => {
{t('analysis.logs.edit_name')}
- @@ -157,7 +159,7 @@ const Favorite = props => { title={editFavoriteForm} onConfirm={createValueHandler(onSaveEditFavorite, rowData)} okText={t('analysis.logs.action.save')} - okButtonProps={{disabled: !isValidFavoriteName(favoriteName)}} + okButtonProps={{disabled: isDisabledName}} cancelText={t('common.action.cancel')} > + )} + /> + )} + {errors.graphs && ( + + {t('workbench.context.retry_graphs')} + + )} + /> + )} + + )} + + ); +}; + +export default GraphContextSwitcher; diff --git a/hugegraph-hubble/hubble-fe/src/components/GraphContextSwitcher/index.module.scss b/hugegraph-hubble/hubble-fe/src/components/GraphContextSwitcher/index.module.scss new file mode 100644 index 000000000..a6bf7499e --- /dev/null +++ b/hugegraph-hubble/hubble-fe/src/components/GraphContextSwitcher/index.module.scss @@ -0,0 +1,71 @@ +/*! + * + * 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. + */ + +.context { + display: flex; + align-items: center; + min-width: 0; + margin-left: 28px; +} + +.graphspace { + width: 160px; +} + +.graph { + width: 180px; +} + +.separator { + color: rgba(255, 255, 255, 0.56); +} + +.mode { + margin-right: 0; + border-color: rgba(255, 255, 255, 0.32); + color: rgba(255, 255, 255, 0.82); + background: rgba(255, 255, 255, 0.08); +} + +.errorStack { + position: absolute; + z-index: 20; + top: 56px; + left: 148px; + display: grid; + gap: 8px; + min-width: 360px; +} + +.error { + box-shadow: 0 4px 16px rgba(0, 0, 0, 0.16); +} + +@media (max-width: 1280px) { + .context { + margin-left: 16px; + } + + .graphspace { + width: 132px; + } + + .graph { + width: 148px; + } +} diff --git a/hugegraph-hubble/hubble-fe/src/components/GraphContextSwitcher/index.test.js b/hugegraph-hubble/hubble-fe/src/components/GraphContextSwitcher/index.test.js new file mode 100644 index 000000000..9ded00d4a --- /dev/null +++ b/hugegraph-hubble/hubble-fe/src/components/GraphContextSwitcher/index.test.js @@ -0,0 +1,220 @@ +/* + * + * 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, waitFor} from '@testing-library/react'; +import {MemoryRouter, useLocation} from 'react-router-dom'; +import GraphContextSwitcher from './index'; +import * as api from '../../api'; + +jest.mock('../../api', () => ({ + manage: { + getGraphSpaceList: jest.fn(), + getGraphList: jest.fn(), + }, +})); + +jest.mock('react-i18next', () => ({ + useTranslation: () => ({t: key => key}), +})); + +jest.mock('antd', () => { + const React = require('react'); + const Select = ({'aria-label': ariaLabel, children, disabled, onChange, value}) => ( + + ); + Select.Option = ({children, value}) => ; + return { + Alert: ({action, message}) =>
{message}{action}
, + Button: ({children, onClick}) => , + Select, + Space: ({children}) =>
{children}
, + Tag: ({children}) => {children}, + }; +}); + +const LocationProbe = () => { + const location = useLocation(); + return {location.pathname}; +}; + +const renderSwitcher = path => render( + + + + +); + +describe('GraphContextSwitcher', () => { + beforeEach(() => { + jest.clearAllMocks(); + localStorage.clear(); + sessionStorage.clear(); + api.manage.getGraphSpaceList.mockResolvedValue({ + status: 200, + data: {records: [ + {name: 'space_a', nickname: 'Space A'}, + {name: 'space_b', nickname: 'Space B'}, + ]}, + }); + api.manage.getGraphList.mockResolvedValue({ + status: 200, + data: {records: [{name: 'graph_a', nickname: 'Graph A'}]}, + }); + }); + + test('non-PD fixes GraphSpace to DEFAULT and loads its graphs', async () => { + sessionStorage.setItem('hubble_config_', JSON.stringify({pd_enabled: false})); + renderSwitcher('/navigation'); + + expect(screen.getByRole('combobox', {name: 'workbench.context.graphspace'})) + .toBeDisabled(); + await waitFor(() => { + expect(api.manage.getGraphList).toHaveBeenCalledWith( + 'DEFAULT', + {page_no: 1, page_size: -1}, + expect.any(Object) + ); + }); + expect(screen.getByText('workbench.context.non_pd_fixed')).toBeInTheDocument(); + expect(screen.getByRole('group', {name: 'workbench.context.name'})).toBeInTheDocument(); + }); + + test('route context selects and persists the current graph', async () => { + sessionStorage.setItem('hubble_config_', JSON.stringify({pd_enabled: true})); + renderSwitcher('/gremlin/space_a/graph_a'); + + await waitFor(() => { + expect(screen.getByRole('combobox', {name: 'workbench.context.graph'})) + .toHaveValue('graph_a'); + }); + expect(JSON.parse(localStorage.getItem('hubble_workbench_graph_context'))).toEqual({ + graphspace: 'space_a', + graph: 'graph_a', + }); + }); + + test('selecting a graph opens its overview', async () => { + sessionStorage.setItem('hubble_config_', JSON.stringify({pd_enabled: true})); + renderSwitcher('/graphspace/space_a'); + + const graphSelect = await screen.findByRole('combobox', { + name: 'workbench.context.graph', + }); + await screen.findByRole('option', {name: 'Graph A'}); + fireEvent.change(graphSelect, {target: {value: 'graph_a'}}); + expect(screen.getByText('/graphspace/space_a/graph/graph_a/detail')).toBeInTheDocument(); + }); + + test('shows an inline retry when graph loading fails', async () => { + sessionStorage.setItem('hubble_config_', JSON.stringify({pd_enabled: false})); + api.manage.getGraphList.mockRejectedValueOnce(new Error('offline')); + renderSwitcher('/navigation'); + + expect(await screen.findByRole('alert')).toHaveTextContent( + 'workbench.context.graphs_load_failed' + ); + fireEvent.click(screen.getByRole('button', { + name: 'workbench.context.retry_graphs', + })); + await waitFor(() => expect(api.manage.getGraphList).toHaveBeenCalledTimes(2)); + }); + + test('treats a resolved graph business error as retryable', async () => { + sessionStorage.setItem('hubble_config_', JSON.stringify({pd_enabled: false})); + api.manage.getGraphList.mockResolvedValueOnce({status: 503, data: null}); + renderSwitcher('/navigation'); + + expect(await screen.findByRole('alert')).toHaveTextContent( + 'workbench.context.graphs_load_failed' + ); + }); + + test('graph success cannot erase a concurrent GraphSpace failure', async () => { + sessionStorage.setItem('hubble_config_', JSON.stringify({pd_enabled: true})); + api.manage.getGraphSpaceList.mockResolvedValueOnce({status: 500, data: null}); + renderSwitcher('/gremlin/space_a/graph_a'); + + expect(await screen.findByRole('alert')).toHaveTextContent( + 'workbench.context.graphspaces_load_failed' + ); + expect(screen.getByRole('combobox', {name: 'workbench.context.graph'})) + .toHaveValue('graph_a'); + }); + + test('switching GraphSpace immediately removes graphs from the previous space', async () => { + sessionStorage.setItem('hubble_config_', JSON.stringify({pd_enabled: true})); + let resolveSpaceB; + api.manage.getGraphList + .mockResolvedValueOnce({ + status: 200, + data: {records: [{name: 'graph_a', nickname: 'Graph A'}]}, + }) + .mockReturnValueOnce(new Promise(resolve => { + resolveSpaceB = resolve; + })); + renderSwitcher('/graphspace/space_a'); + await screen.findByRole('option', {name: 'Graph A'}); + + fireEvent.change( + screen.getByRole('combobox', {name: 'workbench.context.graphspace'}), + {target: {value: 'space_b'}} + ); + + expect(screen.queryByRole('option', {name: 'Graph A'})).not.toBeInTheDocument(); + expect(screen.getByRole('combobox', {name: 'workbench.context.graph'})).toBeDisabled(); + resolveSpaceB({ + status: 200, + data: {records: [{name: 'graph_b', nickname: 'Graph B'}]}, + }); + expect(await screen.findByRole('option', {name: 'Graph B'})).toBeInTheDocument(); + }); + + test('stacks both failures and retries only the selected source', async () => { + sessionStorage.setItem('hubble_config_', JSON.stringify({pd_enabled: true})); + api.manage.getGraphSpaceList.mockRejectedValueOnce(new Error('pd offline')); + api.manage.getGraphList.mockRejectedValueOnce(new Error('server offline')); + renderSwitcher('/gremlin/space_a/graph_a'); + + expect(await screen.findByText('workbench.context.graphspaces_load_failed')) + .toBeInTheDocument(); + expect(await screen.findByText('workbench.context.graphs_load_failed')) + .toBeInTheDocument(); + + fireEvent.click(screen.getByRole('button', { + name: 'workbench.context.retry_graphspaces', + })); + await waitFor(() => expect(api.manage.getGraphSpaceList).toHaveBeenCalledTimes(2)); + expect(api.manage.getGraphList).toHaveBeenCalledTimes(1); + + fireEvent.click(screen.getByRole('button', { + name: 'workbench.context.retry_graphs', + })); + await waitFor(() => expect(api.manage.getGraphList).toHaveBeenCalledTimes(2)); + }); +}); diff --git a/hugegraph-hubble/hubble-fe/src/components/Topbar/index.ant.js b/hugegraph-hubble/hubble-fe/src/components/Topbar/index.ant.js index 5c8670072..d1779254e 100644 --- a/hugegraph-hubble/hubble-fe/src/components/Topbar/index.ant.js +++ b/hugegraph-hubble/hubble-fe/src/components/Topbar/index.ant.js @@ -25,6 +25,7 @@ import * as api from '../../api/index'; import * as user from '../../utils/user'; import {useCallback, useEffect, useState} from 'react'; import {useTranslation} from 'react-i18next'; +import GraphContextSwitcher from '../GraphContextSwitcher'; const {Option} = Select; @@ -106,8 +107,9 @@ const Topbar = () => { }; return ( - +
+
} - aria-label={t('login.username')} - placeholder={t('login.username')} - /> - - +
+ Hubble Desktop Workbench +

{t('login.subtitle')}

+
+ +
- } - aria-label={t('login.password')} - type="password" - placeholder={t('login.password')} - /> - - - - - -
+ } + aria-label={t('login.password')} + type="password" + placeholder={t('login.password')} + /> +
+ + + + + +
); }; diff --git a/hugegraph-hubble/hubble-fe/src/pages/Login/index.module.scss b/hugegraph-hubble/hubble-fe/src/pages/Login/index.module.scss index a23dff13e..bc3a4597a 100644 --- a/hugegraph-hubble/hubble-fe/src/pages/Login/index.module.scss +++ b/hugegraph-hubble/hubble-fe/src/pages/Login/index.module.scss @@ -17,35 +17,130 @@ */ .loginContainer { - text-align: center; - padding-top: 200px; + display: grid; + min-height: 100vh; + place-items: center; + padding: 48px; + background: + radial-gradient(circle at 15% 15%, rgba(23, 105, 224, 0.10), transparent 30%), + #f3f6fb; +} - .loginForm { - text-align: left; - width: 400px; - padding: 25px; - border: 1px solid #d9d9d9; +.loginShell { + display: grid; + grid-template-columns: minmax(0, 1fr) minmax(360px, 0.82fr); + width: min(920px, 100%); + min-height: 520px; + overflow: hidden; + border: 1px solid #d8e0ec; + border-radius: 16px; + background: #fff; + box-shadow: 0 24px 64px rgba(15, 36, 68, 0.14); +} + +.brandPanel { + display: flex; + flex-direction: column; + justify-content: space-between; + padding: 52px; + color: #fff; + background: + radial-gradient(circle at 85% 20%, rgba(55, 211, 170, 0.20), transparent 32%), + linear-gradient(145deg, #061c38, #0b3f78 72%, #1769e0); + + img { + width: 176px; + height: auto; + filter: drop-shadow(0 4px 12px rgba(0, 0, 0, 0.16)); + } +} + +.brandCopy { + max-width: 360px; + + p { + margin: 16px 0 0; + color: rgba(255, 255, 255, 0.82); + font-size: 20px; + line-height: 1.55; + } +} + +.eyebrow, +.formEyebrow { + letter-spacing: 0.08em; + text-transform: uppercase; +} + +.eyebrow { + color: #7de1c4; + font-size: 13px; + font-weight: 600; +} + +.loginForm { + align-self: center; + width: 100%; + padding: 56px 52px; + + :global(.ant-input-affix-wrapper) { + min-height: 44px; + border-radius: 8px; + } +} + +.formHeader { + margin-bottom: 32px; +} + +.formEyebrow { + color: #1769e0; + font-size: 12px; + font-weight: 600; +} + +.title { + margin: 8px 0 8px; + color: #172033; + font-size: 30px; + line-height: 1.25; +} + +.formHeader p { + margin: 0; + color: #68758a; +} + +.submitItem { + margin-top: 28px; + margin-bottom: 0; + + :global(.ant-btn) { border-radius: 8px; - display: inline-block; - // background-color: #8c8c8c; - - .title { - text-align: center; - } - - .loginFormButton { - width: 350px; - margin-bottom: 10px; - } - - .left { - text-align: left; - color: #1890ff; - } - - .right { - text-align: right; - color: #8c8c8c; - } + font-weight: 600; + box-shadow: 0 8px 18px rgba(23, 105, 224, 0.20); + } +} + +@media (max-width: 760px) { + .loginContainer { + padding: 24px; + } + + .loginShell { + grid-template-columns: 1fr; + } + + .brandPanel { + min-height: 180px; + padding: 32px; + } + + .brandCopy p { + font-size: 16px; + } + + .loginForm { + padding: 40px 32px; } } diff --git a/hugegraph-hubble/hubble-fe/src/pages/Login/login-request-error.test.js b/hugegraph-hubble/hubble-fe/src/pages/Login/login-request-error.test.js index 48c13279f..2cb0486d0 100644 --- a/hugegraph-hubble/hubble-fe/src/pages/Login/login-request-error.test.js +++ b/hugegraph-hubble/hubble-fe/src/pages/Login/login-request-error.test.js @@ -100,6 +100,11 @@ describe('Login request errors', () => { expect(screen.getByRole('textbox', {name: 'login.username'})) .toBeInTheDocument(); expect(screen.getByLabelText('login.password')).toHaveAttribute('type', 'password'); + expect(screen.getByRole('region', {name: 'login.brand_label'})) + .toBeInTheDocument(); + expect(screen.getByText('login.subtitle')).toBeInTheDocument(); + expect(screen.getByRole('img', {name: 'Apache HugeGraph'})) + .toBeInTheDocument(); }); it('keeps rejected login requests from escaping the submit handler', async () => { diff --git a/hugegraph-hubble/hubble-fe/src/styles/workbench.scss b/hugegraph-hubble/hubble-fe/src/styles/workbench.scss new file mode 100644 index 000000000..a0b968c33 --- /dev/null +++ b/hugegraph-hubble/hubble-fe/src/styles/workbench.scss @@ -0,0 +1,140 @@ +/* + * + * 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. + */ + +:root { + --workbench-color-brand: #1769e0; + --workbench-color-brand-strong: #0f56bd; + --workbench-color-text: #172033; + --workbench-color-text-secondary: #536179; + --workbench-color-border: #d8dee9; + --workbench-color-surface: #ffffff; + --workbench-color-canvas: #f2f5f9; + --workbench-color-surface-muted: #f7f9fc; + --workbench-space-1: 4px; + --workbench-space-2: 8px; + --workbench-space-3: 12px; + --workbench-space-4: 16px; + --workbench-header-height: 64px; + --workbench-radius: 6px; + --workbench-radius-lg: 10px; + --workbench-shadow-surface: 0 1px 2px rgba(16, 36, 64, 0.04), + 0 8px 24px rgba(16, 36, 64, 0.05); + --workbench-focus-ring: 0 0 0 3px rgba(23, 105, 224, 0.28); +} + +.workbench-skip-link { + position: fixed; + top: var(--workbench-space-2); + left: var(--workbench-space-2); + z-index: 1100; + padding: var(--workbench-space-2) var(--workbench-space-3); + border-radius: var(--workbench-radius); + background: var(--workbench-color-surface); + color: var(--workbench-color-brand-strong); + box-shadow: var(--workbench-focus-ring); + transform: translateY(-160%); + + &:focus { + transform: translateY(0); + } +} + +.workbench-navigation { + display: flex; + flex: none; + background: var(--workbench-color-surface); + border-right: 1px solid var(--workbench-color-border); + + .ant-layout-sider { + background: var(--workbench-color-surface); + } + + .ant-menu-inline { + border-right: 0; + } + + .ant-menu-item, + .ant-menu-submenu-title { + width: calc(100% - 16px); + margin: 4px 8px; + border-radius: var(--workbench-radius); + } + + .ant-menu-item-selected { + color: var(--workbench-color-brand-strong); + background: #eaf2ff; + font-weight: 600; + } +} + +#workbench-main { + color: var(--workbench-color-text); + + .ant-btn, + .ant-input, + .ant-input-affix-wrapper, + .ant-select-selector, + .ant-picker { + border-radius: var(--workbench-radius); + } + + .ant-table { + color: var(--workbench-color-text); + } + + .ant-table-thead > tr > th { + color: #43516a; + background: var(--workbench-color-surface-muted); + font-weight: 600; + } + + .ant-tabs-nav::before { + border-bottom-color: var(--workbench-color-border); + } + + .ant-empty-description { + color: var(--workbench-color-text-secondary); + } +} + +.workbench-page-title { + position: absolute; + width: 1px; + height: 1px; + padding: 0; + margin: -1px; + overflow: hidden; + clip: rect(0, 0, 0, 0); + white-space: nowrap; + border: 0; +} + +#workbench-main:focus { + outline: none; +} + +@media (prefers-reduced-motion: reduce) { + *, + *::before, + *::after { + scroll-behavior: auto !important; + transition-duration: 0.01ms !important; + animation-duration: 0.01ms !important; + animation-iteration-count: 1 !important; + } +} diff --git a/hugegraph-hubble/hubble-fe/src/utils/formatGraphInData.js b/hugegraph-hubble/hubble-fe/src/utils/formatGraphInData.js index 6aa60f507..993ad6ced 100644 --- a/hugegraph-hubble/hubble-fe/src/utils/formatGraphInData.js +++ b/hugegraph-hubble/hubble-fe/src/utils/formatGraphInData.js @@ -72,7 +72,7 @@ import {iconsMap} from './constants'; * @param {object} hugeData * @returns */ -const formatToGraphInData = hugeData => { +const formatToGraphInData = (hugeData, showLabel = true) => { const nodes = []; const edges = []; @@ -85,7 +85,7 @@ const formatToGraphInData = hugeData => { // label: item.label, style: { label: { - value: item.label, + value: showLabel ? item.label : '', }, keyshape: { fill: color, @@ -124,7 +124,7 @@ const formatToGraphInData = hugeData => { style: { ...item.style, label: { - value: item.label, + value: showLabel ? item.label : '', fill: '#000', }, keyshape: { diff --git a/hugegraph-hubble/hubble-fe/src/utils/formatGraphInData.test.js b/hugegraph-hubble/hubble-fe/src/utils/formatGraphInData.test.js new file mode 100644 index 000000000..47497d816 --- /dev/null +++ b/hugegraph-hubble/hubble-fe/src/utils/formatGraphInData.test.js @@ -0,0 +1,45 @@ +/* + * 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 {formatToGraphInData} from './formatGraphInData'; + +jest.mock('@antv/graphin', () => ({ + Utils: {processEdges: edges => edges}, +})); +jest.mock('./graph', () => ({})); +jest.mock('./constants', () => ({iconsMap: {}})); + +const schema = { + vertices: [{ + id: 'person', + label: 'person', + properties: {}, + '~style': {color: '#1769e0'}, + }], + edges: [{ + id: 'person-knows-person', + source: 'person', + target: 'person', + label: 'knows', + properties: {}, + '~style': {color: '#0eb880', with_arrow: true}, + }], +}; + +test('keeps labels for full schema views by default', () => { + const result = formatToGraphInData(schema); + + expect(result.nodes[0].style.label.value).toBe('person'); + expect(result.edges[0].style.label.value).toBe('knows'); +}); + +test('hides labels for compact graph-card previews', () => { + const result = formatToGraphInData(schema, false); + + expect(result.nodes[0].style.label.value).toBe(''); + expect(result.edges[0].style.label.value).toBe(''); +}); From f66122f916e504a2b95c8771d272fc5d882685e3 Mon Sep 17 00:00:00 2001 From: imbajin Date: Sun, 12 Jul 2026 13:18:36 +0800 Subject: [PATCH 07/57] fix(hubble): preserve hash on auth redirect - include URL hash in request-time login redirects - cover HTTP and business 401 redirect contracts - verify login restores query and hash context - assert unauthorized requests are not replayed --- .../src/api/request-error-semantics.test.js | 30 ++++++++++++++----- hugegraph-hubble/hubble-fe/src/api/request.js | 3 +- .../GraphResult/JaccView/index.module.scss | 2 +- .../NeighborRankView/index.module.scss | 2 +- .../GraphResult/RankApiView/index.module.scss | 2 +- .../modules/algorithm/Home/index.module.scss | 4 ++- .../algorithm/LogsDetail/ExecuteLog/index.js | 1 + .../algorithm/LogsDetail/Favorite/index.js | 1 + .../analysis/LogsDetail/ExecuteLog/index.js | 1 + .../analysis/LogsDetail/Favorite/index.js | 1 + .../QueryResult/Home/index.module.scss | 4 ++- .../QueryResult/JsonView/index.module.scss | 2 +- .../analysis/QueryResult/TableView/index.js | 4 +++ .../QueryResult/TableView/index.module.scss | 2 +- .../component/Canvas3D/index.module.scss | 7 ++++- .../modules/component/Graph/index.module.scss | 10 +++++-- .../GraphStatusView/index.module.scss | 2 +- .../SettingConfigPanel/index.module.scss | 2 +- .../GraphStatistics/Home/index.module.scss | 2 +- .../StatisticsPanel/Home/index.module.scss | 2 +- .../LabelStatistics/index.module.scss | 2 +- .../TaskNavigateView/index.module.scss | 2 +- .../layoutConfigPanel/Home/index.module.scss | 2 +- .../pages/Login/login-request-error.test.js | 26 ++++++++++++++-- 24 files changed, 89 insertions(+), 27 deletions(-) diff --git a/hugegraph-hubble/hubble-fe/src/api/request-error-semantics.test.js b/hugegraph-hubble/hubble-fe/src/api/request-error-semantics.test.js index 761f23ce0..70fe893a2 100644 --- a/hugegraph-hubble/hubble-fe/src/api/request-error-semantics.test.js +++ b/hugegraph-hubble/hubble-fe/src/api/request-error-semantics.test.js @@ -74,8 +74,9 @@ describe.each(['./request'])('%s error semantics', modulePath => { beforeEach(() => { delete window.location; window.location = { - pathname: '/navigation', - search: '?from=test', + pathname: '/gremlin/DEFAULT/hugegraph', + search: '?x=1', + hash: '#result', href: '', }; }); @@ -114,7 +115,7 @@ describe.each(['./request'])('%s error semantics', modulePath => { }); it('rejects HTTP 401 and redirects to login', async () => { - const {reject, clearLogin} = loadResponseHandlers(modulePath); + const {reject, clearLogin, instance} = loadResponseHandlers(modulePath); const error = { response: { status: 401, @@ -127,12 +128,19 @@ describe.each(['./request'])('%s error semantics', modulePath => { await expect(reject(error)).rejects.toBe(error); expect(clearLogin).toHaveBeenCalledTimes(1); - expect(sessionStorage.getItem('redirect')).toBe('/navigation?from=test'); - expect(window.location.href).toBe('/login?redirect=%2Fnavigation%3Ffrom%3Dtest'); + expect(sessionStorage.getItem('redirect')) + .toBe('/gremlin/DEFAULT/hugegraph?x=1#result'); + expect(window.location.href).toBe( + '/login?redirect=%2Fgremlin%2FDEFAULT%2Fhugegraph%3Fx%3D1%23result' + ); + expect(instance.get).not.toHaveBeenCalled(); + expect(instance.post).not.toHaveBeenCalled(); + expect(instance.put).not.toHaveBeenCalled(); + expect(instance.delete).not.toHaveBeenCalled(); }); it('rejects business 401 and redirects to login', async () => { - const {resolve, clearLogin} = loadResponseHandlers(modulePath); + const {resolve, clearLogin, instance} = loadResponseHandlers(modulePath); const response = { status: 200, data: { @@ -143,7 +151,15 @@ describe.each(['./request'])('%s error semantics', modulePath => { await expect(resolve(response)).rejects.toBe(response); expect(clearLogin).toHaveBeenCalledTimes(1); - expect(window.location.href).toBe('/login?redirect=%2Fnavigation%3Ffrom%3Dtest'); + expect(sessionStorage.getItem('redirect')) + .toBe('/gremlin/DEFAULT/hugegraph?x=1#result'); + expect(window.location.href).toBe( + '/login?redirect=%2Fgremlin%2FDEFAULT%2Fhugegraph%3Fx%3D1%23result' + ); + expect(instance.get).not.toHaveBeenCalled(); + expect(instance.post).not.toHaveBeenCalled(); + expect(instance.put).not.toHaveBeenCalled(); + expect(instance.delete).not.toHaveBeenCalled(); }); it('shows a modal warning for throttled login attempts', () => { diff --git a/hugegraph-hubble/hubble-fe/src/api/request.js b/hugegraph-hubble/hubble-fe/src/api/request.js index 0b2aacaab..6c2634545 100644 --- a/hugegraph-hubble/hubble-fe/src/api/request.js +++ b/hugegraph-hubble/hubble-fe/src/api/request.js @@ -40,7 +40,8 @@ const parseResponse = (data, headers) => { const redirectToLogin = () => { user.clearLogin(); if (window.location.pathname !== '/login') { - const redirect = `${window.location.pathname}${window.location.search}`; + const redirect = `${window.location.pathname}${window.location.search}` + + window.location.hash; sessionStorage.setItem('redirect', redirect); window.location.href = `/login?redirect=${encodeURIComponent(redirect)}`; } diff --git a/hugegraph-hubble/hubble-fe/src/modules/algorithm/GraphResult/JaccView/index.module.scss b/hugegraph-hubble/hubble-fe/src/modules/algorithm/GraphResult/JaccView/index.module.scss index 265afee53..88b7c1dad 100644 --- a/hugegraph-hubble/hubble-fe/src/modules/algorithm/GraphResult/JaccView/index.module.scss +++ b/hugegraph-hubble/hubble-fe/src/modules/algorithm/GraphResult/JaccView/index.module.scss @@ -23,7 +23,7 @@ align-items: center; flex-direction: column; width: 100%; - height: calc(100vh - 40px); + height: var(--analysis-result-height, calc(100vh - 40px)); border: 1px solid #E7E8E9; margin-left: -1px; word-break: break-all; diff --git a/hugegraph-hubble/hubble-fe/src/modules/algorithm/GraphResult/NeighborRankView/index.module.scss b/hugegraph-hubble/hubble-fe/src/modules/algorithm/GraphResult/NeighborRankView/index.module.scss index f852dd6b9..da806c990 100644 --- a/hugegraph-hubble/hubble-fe/src/modules/algorithm/GraphResult/NeighborRankView/index.module.scss +++ b/hugegraph-hubble/hubble-fe/src/modules/algorithm/GraphResult/NeighborRankView/index.module.scss @@ -23,7 +23,7 @@ align-items: center; flex-direction: column; width: 100%; - height: calc(100vh - 40px); + height: var(--analysis-result-height, calc(100vh - 40px)); border: 1px solid #E7E8E9; margin-left: -1px; word-break: break-all; diff --git a/hugegraph-hubble/hubble-fe/src/modules/algorithm/GraphResult/RankApiView/index.module.scss b/hugegraph-hubble/hubble-fe/src/modules/algorithm/GraphResult/RankApiView/index.module.scss index 265afee53..88b7c1dad 100644 --- a/hugegraph-hubble/hubble-fe/src/modules/algorithm/GraphResult/RankApiView/index.module.scss +++ b/hugegraph-hubble/hubble-fe/src/modules/algorithm/GraphResult/RankApiView/index.module.scss @@ -23,7 +23,7 @@ align-items: center; flex-direction: column; width: 100%; - height: calc(100vh - 40px); + height: var(--analysis-result-height, calc(100vh - 40px)); border: 1px solid #E7E8E9; margin-left: -1px; word-break: break-all; diff --git a/hugegraph-hubble/hubble-fe/src/modules/algorithm/Home/index.module.scss b/hugegraph-hubble/hubble-fe/src/modules/algorithm/Home/index.module.scss index 8ee5b304a..0ecc62502 100644 --- a/hugegraph-hubble/hubble-fe/src/modules/algorithm/Home/index.module.scss +++ b/hugegraph-hubble/hubble-fe/src/modules/algorithm/Home/index.module.scss @@ -17,8 +17,10 @@ */ .algorithmContent { + --analysis-result-height: clamp(360px, calc(100vh - 305px), 640px); + display: flex; flex-direction: row; margin-top: 10px; - height: calc(100vh - 1px); + height: calc(var(--analysis-result-height) + 40px); } 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 310309e02..cdf60ab4c 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 @@ -196,6 +196,7 @@ const ExecuteLog = props => { current: pageExecute, pageSize: pageSize, }} + scroll={{y: 360}} loading={isLoading} /> ); 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 dbbbe8aeb..3a5ead2ff 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 @@ -230,6 +230,7 @@ const Favorite = props => { current: pageFavorite, pageSize: pageSize, }} + scroll={{y: 360}} loading={isLoading} /> 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 2dcb9a947..b8b70cf74 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 @@ -276,6 +276,7 @@ const ExecuteLog = props => { current: pageExecute, pageSize: pageSize, }} + scroll={{y: 360}} loading={isLoading} /> ); 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 ae6d74f41..bb60038af 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 @@ -280,6 +280,7 @@ const Favorite = props => { current: pageFavorite, pageSize: pageSize, }} + scroll={{y: 360}} loading={isLoading} /> diff --git a/hugegraph-hubble/hubble-fe/src/modules/analysis/QueryResult/Home/index.module.scss b/hugegraph-hubble/hubble-fe/src/modules/analysis/QueryResult/Home/index.module.scss index 0c9f9369d..885abb3e9 100644 --- a/hugegraph-hubble/hubble-fe/src/modules/analysis/QueryResult/Home/index.module.scss +++ b/hugegraph-hubble/hubble-fe/src/modules/analysis/QueryResult/Home/index.module.scss @@ -17,6 +17,8 @@ */ .queryResult { + --analysis-result-height: clamp(360px, calc(100vh - 305px), 640px); + margin-top: 10px; :global(.ant-tabs) { @@ -90,7 +92,7 @@ .graphContainer { position: relative; - height: calc(100vh - 305px); + height: var(--analysis-result-height); display: flex; flex-direction: column; justify-content: center; diff --git a/hugegraph-hubble/hubble-fe/src/modules/analysis/QueryResult/JsonView/index.module.scss b/hugegraph-hubble/hubble-fe/src/modules/analysis/QueryResult/JsonView/index.module.scss index 2bef07f8b..0b54fd5ca 100644 --- a/hugegraph-hubble/hubble-fe/src/modules/analysis/QueryResult/JsonView/index.module.scss +++ b/hugegraph-hubble/hubble-fe/src/modules/analysis/QueryResult/JsonView/index.module.scss @@ -19,6 +19,6 @@ .jsonWrapper { position: relative; padding: 10px; - height: 100vh; + height: var(--analysis-result-height, calc(100vh - 40px)); overflow: auto; } diff --git a/hugegraph-hubble/hubble-fe/src/modules/analysis/QueryResult/TableView/index.js b/hugegraph-hubble/hubble-fe/src/modules/analysis/QueryResult/TableView/index.js index 39bcd520b..620f44cad 100644 --- a/hugegraph-hubble/hubble-fe/src/modules/analysis/QueryResult/TableView/index.js +++ b/hugegraph-hubble/hubble-fe/src/modules/analysis/QueryResult/TableView/index.js @@ -134,6 +134,10 @@ const TableView = props => { dataSource={queryResultTable?.rows || []} columns={tableColums} pagination={{position: ['bottomCenter']}} + scroll={{ + x: 'max-content', + y: 'calc(var(--analysis-result-height, 100vh) - 120px)', + }} /> ); diff --git a/hugegraph-hubble/hubble-fe/src/modules/analysis/QueryResult/TableView/index.module.scss b/hugegraph-hubble/hubble-fe/src/modules/analysis/QueryResult/TableView/index.module.scss index 440183996..4ac9abf21 100644 --- a/hugegraph-hubble/hubble-fe/src/modules/analysis/QueryResult/TableView/index.module.scss +++ b/hugegraph-hubble/hubble-fe/src/modules/analysis/QueryResult/TableView/index.module.scss @@ -18,6 +18,6 @@ .tableWrapper{ position: relative; - height: 100vh; + height: var(--analysis-result-height, calc(100vh - 40px)); overflow: auto; } diff --git a/hugegraph-hubble/hubble-fe/src/modules/component/Canvas3D/index.module.scss b/hugegraph-hubble/hubble-fe/src/modules/component/Canvas3D/index.module.scss index ed1c0b910..aebb2dd14 100644 --- a/hugegraph-hubble/hubble-fe/src/modules/component/Canvas3D/index.module.scss +++ b/hugegraph-hubble/hubble-fe/src/modules/component/Canvas3D/index.module.scss @@ -21,10 +21,15 @@ position: relative; border: 1px solid #E7E8E9; margin-left: -1px; - height: calc(100vh - 40px); + height: var(--analysis-result-height, calc(100vh - 40px)); width: auto; overflow: hidden; background-image: url('/assets/canvas_bg.png'); + + &:fullscreen { + width: 100vw; + height: 100vh; + } } .tooltip{ diff --git a/hugegraph-hubble/hubble-fe/src/modules/component/Graph/index.module.scss b/hugegraph-hubble/hubble-fe/src/modules/component/Graph/index.module.scss index 5c0cfaf90..03af37dd3 100644 --- a/hugegraph-hubble/hubble-fe/src/modules/component/Graph/index.module.scss +++ b/hugegraph-hubble/hubble-fe/src/modules/component/Graph/index.module.scss @@ -20,14 +20,14 @@ position: relative; border: 1px solid #E7E8E9; margin-left: -1px; - height: calc(100vh - 40px); + height: var(--analysis-result-height, calc(100vh - 40px)); width: auto; overflow: hidden; background-image: url('/assets/canvas_bg.png'); :global(.g6-legend-container) { top: 0px !important; - max-height: calc(100vh - 200px); + max-height: calc(var(--analysis-result-height, 100vh) - 160px); overflow-y: auto; } @@ -53,7 +53,13 @@ } &:fullscreen { + width: 100vw; + height: 100vh; background-color: #FFF; + + :global(.g6-legend-container) { + max-height: calc(100vh - 160px); + } } } diff --git a/hugegraph-hubble/hubble-fe/src/modules/component/GraphStatusView/index.module.scss b/hugegraph-hubble/hubble-fe/src/modules/component/GraphStatusView/index.module.scss index e4bf319cd..223ee69c6 100644 --- a/hugegraph-hubble/hubble-fe/src/modules/component/GraphStatusView/index.module.scss +++ b/hugegraph-hubble/hubble-fe/src/modules/component/GraphStatusView/index.module.scss @@ -22,7 +22,7 @@ justify-content: center; flex-direction: column; width: 100%; - height: calc(100vh - 40px); + height: var(--analysis-result-height, calc(100vh - 40px)); border: 1px solid #E7E8E9; margin-left: -1px; word-break: break-all; diff --git a/hugegraph-hubble/hubble-fe/src/modules/component/SettingConfigPanel/index.module.scss b/hugegraph-hubble/hubble-fe/src/modules/component/SettingConfigPanel/index.module.scss index e28356ba3..183e83aa6 100644 --- a/hugegraph-hubble/hubble-fe/src/modules/component/SettingConfigPanel/index.module.scss +++ b/hugegraph-hubble/hubble-fe/src/modules/component/SettingConfigPanel/index.module.scss @@ -24,7 +24,7 @@ flex: 1; width: 250px; padding: 15px 30px 15px 15px; - height: calc(100vh - 40px); + height: var(--analysis-result-height, calc(100vh - 40px)); overflow-y: auto; border: 1px solid #E7E8E9; margin: -1px 0 0 -1px; diff --git a/hugegraph-hubble/hubble-fe/src/modules/component/StatisticsPanel/GraphStatistics/Home/index.module.scss b/hugegraph-hubble/hubble-fe/src/modules/component/StatisticsPanel/GraphStatistics/Home/index.module.scss index 76cfe3bf6..07c0052c2 100644 --- a/hugegraph-hubble/hubble-fe/src/modules/component/StatisticsPanel/GraphStatistics/Home/index.module.scss +++ b/hugegraph-hubble/hubble-fe/src/modules/component/StatisticsPanel/GraphStatistics/Home/index.module.scss @@ -18,6 +18,6 @@ .graphStatistics{ border-top: none; - height: calc(100vh - 75px); + height: calc(var(--analysis-result-height, 100vh) - 35px); padding: 0px 5px; } diff --git a/hugegraph-hubble/hubble-fe/src/modules/component/StatisticsPanel/Home/index.module.scss b/hugegraph-hubble/hubble-fe/src/modules/component/StatisticsPanel/Home/index.module.scss index f6b3e5b3e..032a16575 100644 --- a/hugegraph-hubble/hubble-fe/src/modules/component/StatisticsPanel/Home/index.module.scss +++ b/hugegraph-hubble/hubble-fe/src/modules/component/StatisticsPanel/Home/index.module.scss @@ -22,7 +22,7 @@ position: absolute; right: 0px; width: 250px; - height: calc(100vh - 40px); + height: var(--analysis-result-height, calc(100vh - 40px)); overflow-y: auto; border: 1px solid #E7E8E9; border-top: none; diff --git a/hugegraph-hubble/hubble-fe/src/modules/component/StatisticsPanel/LabelStatistics/index.module.scss b/hugegraph-hubble/hubble-fe/src/modules/component/StatisticsPanel/LabelStatistics/index.module.scss index 6fbcc293c..1bd752523 100644 --- a/hugegraph-hubble/hubble-fe/src/modules/component/StatisticsPanel/LabelStatistics/index.module.scss +++ b/hugegraph-hubble/hubble-fe/src/modules/component/StatisticsPanel/LabelStatistics/index.module.scss @@ -18,7 +18,7 @@ .labelStatistics { border-top: none; - height: calc(100vh - 75px); + height: calc(var(--analysis-result-height, 100vh) - 35px); :global(.ant-collapse >.ant-collapse-item >.ant-collapse-header) { padding: 5px; diff --git a/hugegraph-hubble/hubble-fe/src/modules/component/TaskNavigateView/index.module.scss b/hugegraph-hubble/hubble-fe/src/modules/component/TaskNavigateView/index.module.scss index 505cdd3d0..515ab5c4b 100644 --- a/hugegraph-hubble/hubble-fe/src/modules/component/TaskNavigateView/index.module.scss +++ b/hugegraph-hubble/hubble-fe/src/modules/component/TaskNavigateView/index.module.scss @@ -19,7 +19,7 @@ .graphView { position: relative; width: 100%; - height: calc(100vh - 40px); + height: var(--analysis-result-height, calc(100vh - 40px)); display: flex; flex-direction: column; justify-content: center; diff --git a/hugegraph-hubble/hubble-fe/src/modules/component/layoutConfigPanel/Home/index.module.scss b/hugegraph-hubble/hubble-fe/src/modules/component/layoutConfigPanel/Home/index.module.scss index 0acf6bb01..633908090 100644 --- a/hugegraph-hubble/hubble-fe/src/modules/component/layoutConfigPanel/Home/index.module.scss +++ b/hugegraph-hubble/hubble-fe/src/modules/component/layoutConfigPanel/Home/index.module.scss @@ -23,7 +23,7 @@ z-index: 1; flex: 1; padding: 15px 30px 15px 15px; - height: calc(100vh - 40px); + height: var(--analysis-result-height, calc(100vh - 40px)); overflow-y: auto; border: 1px solid #E7E8E9; margin: -1px 0 0 -1px; diff --git a/hugegraph-hubble/hubble-fe/src/pages/Login/login-request-error.test.js b/hugegraph-hubble/hubble-fe/src/pages/Login/login-request-error.test.js index 2cb0486d0..ea845ef30 100644 --- a/hugegraph-hubble/hubble-fe/src/pages/Login/login-request-error.test.js +++ b/hugegraph-hubble/hubble-fe/src/pages/Login/login-request-error.test.js @@ -48,10 +48,10 @@ jest.mock('react-i18next', () => ({ }), })); -const submitLoginForm = async () => { +const submitLoginForm = async (redirect = '%2Fnavigation') => { render( { expect(configUtil.setConfig).not.toHaveBeenCalled(); expect(window.location.replace).toHaveBeenCalledWith('/navigation'); }); + + it('restores the full query and hash location after login', async () => { + api.auth.login.mockResolvedValue({ + status: 200, + data: { + user_name: 'admin', + }, + }); + api.config.getConfig.mockResolvedValue({status: 200, data: {}}); + + await submitLoginForm( + '%2Fgremlin%2FDEFAULT%2Fhugegraph%3Fx%3D1%23result' + ); + + await waitFor(() => { + expect(window.location.replace).toHaveBeenCalledWith( + '/gremlin/DEFAULT/hugegraph?x=1#result' + ); + }); + expect(api.auth.login).toHaveBeenCalledTimes(1); + expect(api.config.getConfig).toHaveBeenCalledTimes(1); + }); }); From 4832209aeb3fda73bdcbd6428574ebe6ddcea485 Mon Sep 17 00:00:00 2001 From: imbajin Date: Sun, 12 Jul 2026 13:29:00 +0800 Subject: [PATCH 08/57] fix(hubble): add graph toolbar labels - label ten icon-only graph controls with localized text - keep accessible names aligned with existing tooltips - add regression coverage for query and algorithm tools --- .../src/components/Sidebar/index.ant.js | 6 +- .../src/components/Sidebar/index.ant.test.js | 96 +++++++++++++++++++ .../src/modules/component/ClearGraph/index.js | 8 +- .../src/modules/component/FitCenter/index.js | 7 +- .../src/modules/component/FixNode/index.js | 8 +- .../src/modules/component/FullScreen/index.js | 7 +- .../src/modules/component/RedoUndo/index.js | 16 +++- .../modules/component/RefreshGraph/index.js | 7 +- .../modules/component/ZeroDegreeNode/index.js | 7 +- .../src/modules/component/ZoomGraph/index.js | 14 ++- .../component/toolbar-accessible-name.test.js | 73 ++++++++++++++ .../hubble-fe/src/routes/index.js | 10 +- .../hubble-fe/src/routes/route-guard.test.js | 41 +++++++- hugegraph-hubble/hubble-fe/src/utils/user.js | 17 +++- .../hubble-fe/src/utils/user.test.js | 19 +++- 15 files changed, 317 insertions(+), 19 deletions(-) create mode 100644 hugegraph-hubble/hubble-fe/src/components/Sidebar/index.ant.test.js create mode 100644 hugegraph-hubble/hubble-fe/src/modules/component/toolbar-accessible-name.test.js diff --git a/hugegraph-hubble/hubble-fe/src/components/Sidebar/index.ant.js b/hugegraph-hubble/hubble-fe/src/components/Sidebar/index.ant.js index b2e9796ee..88c95d939 100644 --- a/hugegraph-hubble/hubble-fe/src/components/Sidebar/index.ant.js +++ b/hugegraph-hubble/hubble-fe/src/components/Sidebar/index.ant.js @@ -43,11 +43,7 @@ const items = t => { if (!pdMode) { systemList = [MY]; } - else if (userInfo.is_superadmin) { - // systemList = [MY, ACCOUNT, RESOURCE, ROLE]; - systemList = [MY, ACCOUNT]; - } - else if (userInfo.resSpaces && userInfo.resSpaces.length > 0) { + else if (user.canAccessAccount(pdMode, userInfo)) { // systemList = [MY, RESOURCE, ROLE]; systemList = [MY, ACCOUNT]; } diff --git a/hugegraph-hubble/hubble-fe/src/components/Sidebar/index.ant.test.js b/hugegraph-hubble/hubble-fe/src/components/Sidebar/index.ant.test.js new file mode 100644 index 000000000..7b2c08da7 --- /dev/null +++ b/hugegraph-hubble/hubble-fe/src/components/Sidebar/index.ant.test.js @@ -0,0 +1,96 @@ +/* + * + * 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 {render, screen, waitFor} from '@testing-library/react'; +import {MemoryRouter} from 'react-router-dom'; +import Sidebar from './index.ant'; +import '../../i18n'; + +beforeEach(() => { + sessionStorage.clear(); + localStorage.clear(); + sessionStorage.setItem('user_', JSON.stringify({ + id: 'admin', + user_nickname: 'admin', + is_superadmin: false, + })); + sessionStorage.setItem('hubble_config_', JSON.stringify({pd_enabled: false})); +}); + +test('exposes the application menu as named primary navigation', async () => { + render( + + + + ); + + const navigation = await screen.findByRole('navigation', {name: '主导航'}); + expect(screen.getByText('理解图')).toBeInTheDocument(); + expect(screen.getByText('准备数据')).toBeInTheDocument(); + expect(screen.getByText('查询与分析')).toBeInTheDocument(); + expect(screen.getByText('系统与运维')).toBeInTheDocument(); + expect(navigation).toContainElement(screen.getByRole('link', {name: '图语言分析'})); + expect(screen.queryByRole('link', {name: '账号管理'})).not.toBeInTheDocument(); + await waitFor(() => expect(navigation).toBeVisible()); +}); + +test('shows Account for the same authorized-space user accepted by its route', async () => { + sessionStorage.setItem('user_', JSON.stringify({ + id: 'space-admin', + user_nickname: 'space-admin', + is_superadmin: false, + adminSpaces: [{name: 'SPACE'}], + })); + sessionStorage.setItem('hubble_config_', JSON.stringify({pd_enabled: true})); + + render( + + + + ); + + expect(await screen.findByRole('link', {name: '账号管理'})).toBeInTheDocument(); +}); + +test('hides Account from an analyst who can read a space but cannot administer it', async () => { + sessionStorage.setItem('user_', JSON.stringify({ + id: 'analyst', + user_nickname: 'analyst', + is_superadmin: false, + resSpaces: [{name: 'SPACE'}], + adminSpaces: [], + })); + sessionStorage.setItem('hubble_config_', JSON.stringify({pd_enabled: true})); + + render( + + + + ); + + expect(screen.queryByRole('link', {name: '账号管理'})).not.toBeInTheDocument(); +}); diff --git a/hugegraph-hubble/hubble-fe/src/modules/component/ClearGraph/index.js b/hugegraph-hubble/hubble-fe/src/modules/component/ClearGraph/index.js index a6e0c3cb2..c445821a4 100644 --- a/hugegraph-hubble/hubble-fe/src/modules/component/ClearGraph/index.js +++ b/hugegraph-hubble/hubble-fe/src/modules/component/ClearGraph/index.js @@ -58,7 +58,13 @@ const ClearGraph = props => { return ( <> - - +