This is an automated email from the ASF dual-hosted git repository. imbajin pushed a commit to branch master in repository https://gitbox.apache.org/repos/asf/hugegraph-toolchain.git
commit 1c2900c62d1c419b0451ede4cca6341a99887730 Author: dark <[email protected]> AuthorDate: Sat Aug 29 17:23:09 2026 +0800 fix(hubble): stabilize async task capabilities - improve task execution feedback - gate scoped task actions - retry unverified configuration - initialize the Computer manager --- .../org/apache/hugegraph/driver/HugeClient.java | 2 + .../driver/HugeClientCompatibilityTest.java | 13 +++ .../hugegraph/controller/ConfigController.java | 3 + .../controller/algorithm/OlapAlgoController.java | 13 +++ .../hugegraph/controller/auth/UserController.java | 4 +- .../service/algorithm/OlapAlgoService.java | 25 ++++- .../apache/hugegraph/service/auth/UserService.java | 3 +- .../service/algorithm/OlapAlgoServiceTest.java | 90 +++++++++++++++++ .../hugegraph/unit/ConfigControllerTest.java | 3 + .../org/apache/hugegraph/unit/UnitTestSuite.java | 2 + .../unit/UserServiceCompatibilityTest.java | 6 +- hugegraph-hubble/hubble-fe/src/App.js | 19 +++- hugegraph-hubble/hubble-fe/src/App.test.js | 43 +++++++- hugegraph-hubble/hubble-fe/src/api/analysis.js | 8 ++ .../src/i18n/resources/en-US/modules/analysis.json | 13 ++- .../src/i18n/resources/zh-CN/modules/analysis.json | 13 ++- .../algorithmsForm/AlgorithmNameHeader/index.js | 6 +- .../modules/algorithm/algorithmsForm/Home/index.js | 2 + .../algorithm/algorithmsForm/MaxDepthItem/index.js | 3 +- .../Olap/BetweennessCentrality/index.js | 2 + .../Olap/BetweennessCentralityVermeer/index.js | 2 + .../Olap/ClosenessCentrality/index.js | 2 + .../Olap/ClosenessCentralityVermeer/index.js | 2 + .../Olap/ClusterCoefficient/index.js | 2 + .../algorithmsForm/Olap/DegreeCentrality/index.js | 2 + .../Olap/DegreeCentralityVermeer/index.js | 2 + .../Olap/FilterSubGraphMatching/index.js | 2 + .../Olap/FilteredRingsDetection/index.js | 2 + .../algorithm/algorithmsForm/Olap/KCore/index.js | 2 + .../algorithmsForm/Olap/KCoreVermeer/index.js | 2 + .../Olap/LabelPropagationAlgorithm/index.js | 2 + .../Olap/LabelPropagationAlgorithmVermeer/index.js | 2 + .../algorithm/algorithmsForm/Olap/Links/index.js | 2 + .../algorithm/algorithmsForm/Olap/Louvain/index.js | 2 + .../algorithmsForm/Olap/PageRank/index.js | 2 + .../algorithmsForm/Olap/PageRankVermeer/index.js | 2 + .../algorithmsForm/Olap/PersonalPageRank/index.js | 2 + .../algorithmsForm/Olap/RingsDetection/index.js | 2 + .../algorithmsForm/Olap/SSSPVermeer/index.js | 2 + .../algorithmsForm/Olap/TriangleCount/index.js | 2 + .../Olap/TriangleCountVermeer/index.js | 2 + .../Olap/WeaklyConnectedComponent/index.js | 2 + .../Olap/WeaklyConnectedComponentVermeer/index.js | 2 + .../algorithm/algorithmsForm/OlapHome/index.js | 63 ++++++++++-- .../algorithmsForm/OlapHome/index.test.js | 111 +++++++++++++++++++++ .../algorithmsForm/Oltp/AdamicAdar/index.js | 2 + .../algorithmsForm/Oltp/AllPaths/index.js | 2 + .../algorithmsForm/Oltp/Crosspoints/index.js | 5 +- .../Oltp/CustomizedCrosspoints/Home/index.js | 2 + .../Oltp/CustomizedPaths/Home/index.js | 5 +- .../algorithm/algorithmsForm/Oltp/Egonet/index.js | 5 +- .../algorithmsForm/Oltp/FindShortestPath/index.js | 5 +- .../Oltp/FindShortestPathWithWeight/index.js | 2 + .../Oltp/FusiformSimilarity/index.js | 2 + .../Oltp/JaccardSimilarityGet/index.js | 2 + .../Oltp/JaccardSimilarityPost/index.js | 2 + .../algorithmsForm/Oltp/KneighborGet/index.js | 2 + .../algorithmsForm/Oltp/KneighborPost/index.js | 5 +- .../algorithm/algorithmsForm/Oltp/KoutGet/index.js | 2 + .../algorithmsForm/Oltp/KoutPost/Home/index.js | 5 +- .../Oltp/MultiNodesShortestPath/index.js | 5 +- .../algorithmsForm/Oltp/NeighborRankApi/index.js | 2 + .../algorithm/algorithmsForm/Oltp/Paths/index.js | 5 +- .../algorithm/algorithmsForm/Oltp/RankApi/index.js | 4 +- .../algorithm/algorithmsForm/Oltp/Rays/index.js | 5 +- .../Oltp/ResourceAllocation/index.js | 2 + .../algorithm/algorithmsForm/Oltp/Rings/index.js | 2 + .../algorithmsForm/Oltp/SameNeighbors/index.js | 2 + .../Oltp/SameNeighborsBatch/index.js | 2 + .../algorithmsForm/Oltp/ShortestPath/index.js | 2 + .../Oltp/SingleSourceShortestPath/index.js | 2 + .../Oltp/TemplatePaths/Home/index.js | 2 + .../algorithmsForm/PersistentForm/index.js | 20 ++++ .../algorithmsForm/PersistentForm/index.test.js | 82 ++++++++++++++- .../algorithmsForm/algorithm-dom-props.test.js | 14 +++ .../src/modules/asyncTasks/Detail/index.js | 99 +++++++++++++++--- .../modules/asyncTasks/Detail/index.module.scss | 12 +++ .../src/modules/asyncTasks/Detail/index.test.js | 104 +++++++++++++++++++ .../src/modules/asyncTasks/Result/index.js | 90 ++++++++++------- .../modules/asyncTasks/Result/index.module.scss | 12 +++ .../hubble-fe/src/pages/Account/EditLayer.js | 3 +- .../pages/Account/account-edit-recovery.test.js | 20 +++- 82 files changed, 943 insertions(+), 89 deletions(-) diff --git a/hugegraph-client/src/main/java/org/apache/hugegraph/driver/HugeClient.java b/hugegraph-client/src/main/java/org/apache/hugegraph/driver/HugeClient.java index 64e19f48f..171801c0f 100644 --- a/hugegraph-client/src/main/java/org/apache/hugegraph/driver/HugeClient.java +++ b/hugegraph-client/src/main/java/org/apache/hugegraph/driver/HugeClient.java @@ -192,6 +192,7 @@ public class HugeClient implements Closeable { this.traverser = new TraverserManager(client, this.graph); this.variable = new VariablesManager(client, graphSpace, graph); this.job = new JobManager(client, graphSpace, graph); + this.computer = new ComputerManager(client, graphSpace, graph); this.task = new TaskManager(client, graphSpace, graph); } else { /* @@ -205,6 +206,7 @@ public class HugeClient implements Closeable { this.traverser = null; this.variable = null; this.job = null; + this.computer = null; this.task = null; } } diff --git a/hugegraph-client/src/test/java/org/apache/hugegraph/driver/HugeClientCompatibilityTest.java b/hugegraph-client/src/test/java/org/apache/hugegraph/driver/HugeClientCompatibilityTest.java index 049c542ae..3bef153ff 100644 --- a/hugegraph-client/src/test/java/org/apache/hugegraph/driver/HugeClientCompatibilityTest.java +++ b/hugegraph-client/src/test/java/org/apache/hugegraph/driver/HugeClientCompatibilityTest.java @@ -19,6 +19,7 @@ package org.apache.hugegraph.driver; import java.util.Arrays; +import org.apache.hugegraph.client.RestClient; import org.apache.hugegraph.exception.ServerException; import org.apache.hugegraph.structure.auth.TokenPayload; import org.apache.hugegraph.structure.auth.User; @@ -81,6 +82,18 @@ public class HugeClientCompatibilityTest { Assert.assertFalse(this.client.requiresBasicGremlinAuth()); } + @Test + public void shouldInitializeAndResetComputerWithGraphScope() { + RestClient restClient = Mockito.mock(RestClient.class); + Whitebox.setInternalState(this.client, "apiVersionChecked", true); + + this.client.initManagers(restClient, "DEFAULT", "hugegraph"); + Assert.assertNotNull(this.client.computer()); + + this.client.initManagers(restClient, "DEFAULT", null); + Assert.assertNull(this.client.computer()); + } + @Test public void shouldFindCurrentUserFromVerifiedTokenIdentity() { TokenPayload payload = Mockito.mock(TokenPayload.class); diff --git a/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/controller/ConfigController.java b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/controller/ConfigController.java index 8ee22aa1c..a908100e0 100644 --- a/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/controller/ConfigController.java +++ b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/controller/ConfigController.java @@ -59,11 +59,13 @@ public class ConfigController { Map<String, Object> capabilities = new HashMap<>(); boolean pdEnabled = config.get(HubbleOptions.PD_ENABLED); if (pdEnabled) { + capabilities.put("server_capabilities_verified", true); capabilities.put("auth_enabled", this.authModeService.enabled()); capabilities.put("graph_create_enabled", true); capabilities.put("cypher_enabled", true); return capabilities; } + capabilities.put("server_capabilities_verified", false); capabilities.put("auth_enabled", true); capabilities.put("graph_create_enabled", false); capabilities.put("cypher_enabled", false); @@ -78,6 +80,7 @@ public class ConfigController { capabilities.put("graph_create_enabled", client.supportsGraphCreate()); capabilities.put("cypher_enabled", client.supportsCypher()); + capabilities.put("server_capabilities_verified", true); return capabilities; } catch (RuntimeException ignored) { // Keep bootstrap resilient when the Server is temporarily unavailable. diff --git a/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/controller/algorithm/OlapAlgoController.java b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/controller/algorithm/OlapAlgoController.java index e20e716f9..b20c3cf00 100644 --- a/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/controller/algorithm/OlapAlgoController.java +++ b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/controller/algorithm/OlapAlgoController.java @@ -18,6 +18,9 @@ package org.apache.hugegraph.controller.algorithm; +import java.util.Collections; +import java.util.Map; + import lombok.extern.log4j.Log4j2; import org.apache.hugegraph.common.Constant; import org.apache.hugegraph.controller.BaseController; @@ -26,6 +29,7 @@ import org.apache.hugegraph.entity.algorithm.OlapEntity; import org.apache.hugegraph.entity.query.OlapView; import org.apache.hugegraph.service.algorithm.OlapAlgoService; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; @@ -40,6 +44,15 @@ public class OlapAlgoController extends BaseController { @Autowired private OlapAlgoService service; + @GetMapping("capability") + public Map<String, Boolean> capability( + @PathVariable("graphspace") String graphspace, + @PathVariable("graph") String graph) { + HugeClient client = this.authClient(graphspace, graph); + return Collections.singletonMap( + "available", this.service.computerAvailable(client)); + } + @PostMapping public OlapView olapView(@PathVariable("graphspace") String graphspace, @PathVariable("graph") String graph, diff --git a/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/controller/auth/UserController.java b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/controller/auth/UserController.java index a0c9d2495..fd4d7b1b7 100644 --- a/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/controller/auth/UserController.java +++ b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/controller/auth/UserController.java @@ -72,10 +72,10 @@ public class UserController extends BaseController { } @PostMapping - public void create(@RequestBody UserEntity userEntity) { + public UserEntity create(@RequestBody UserEntity userEntity) { HugeClient client = this.requireAccountManager(); this.checkAccountGrantScope(client, null, userEntity); - userService.add(client, userEntity); + return userService.add(client, userEntity); } @PostMapping("batch") diff --git a/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/service/algorithm/OlapAlgoService.java b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/service/algorithm/OlapAlgoService.java index be3b8abcc..ed14c3095 100644 --- a/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/service/algorithm/OlapAlgoService.java +++ b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/service/algorithm/OlapAlgoService.java @@ -22,6 +22,7 @@ import org.apache.hugegraph.config.HugeConfig; import org.apache.hugegraph.driver.HugeClient; import org.apache.hugegraph.entity.algorithm.OlapEntity; import org.apache.hugegraph.entity.query.OlapView; +import org.apache.hugegraph.exception.ServerCapabilityUnavailableException; import org.apache.hugegraph.service.query.ExecuteHistoryService; import lombok.extern.log4j.Log4j2; import org.springframework.beans.factory.annotation.Autowired; @@ -37,6 +38,16 @@ public class OlapAlgoService { @Autowired private ExecuteHistoryService historyService; + public boolean computerAvailable(HugeClient client) { + try { + client.computer().list(1L); + return true; + } catch (RuntimeException e) { + log.debug("HugeGraph Computer is unavailable: {}", e.getMessage()); + return false; + } + } + public OlapView olapView(HugeClient client, String graphspace, OlapEntity body) { Map<String, Object> params = body.getParams(); if (!"DEFAULT".equals(graphspace)) { @@ -48,7 +59,19 @@ public class OlapAlgoService { params.put("k8s.worker_memory", "5Gi"); params.putAll(body.getParams()); } - long taskid = client.computer().create(body.getAlgorithm(), body.getWorker(), params); + long taskid; + try { + taskid = client.computer().create(body.getAlgorithm(), + body.getWorker(), params); + } catch (RuntimeException e) { + String detail = e.getMessage(); + if (detail == null || detail.trim().isEmpty() || + "request_failure".equals(detail)) { + throw new ServerCapabilityUnavailableException( + "server.capability.computer.unavailable", e); + } + throw e; + } return OlapView.builder().taskId(taskid).build(); } } diff --git a/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/service/auth/UserService.java b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/service/auth/UserService.java index 058ce1949..18676daff 100644 --- a/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/service/auth/UserService.java +++ b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/service/auth/UserService.java @@ -219,7 +219,7 @@ public class UserService extends AuthService { return userEntity; } - public void add(HugeClient client, UserEntity ue) { + public UserEntity add(HugeClient client, UserEntity ue) { boolean permissionPresets = isPdEnabled() && client.supportsDefaultRole(); this.validatePermissionMutation(client, ue, true, @@ -254,6 +254,7 @@ public class UserService extends AuthService { superAdminAttempted, error); throw error; } + return this.convert(client, newUser); } private void rollbackNewAccount(HugeClient client, User user, diff --git a/hugegraph-hubble/hubble-be/src/test/java/org/apache/hugegraph/service/algorithm/OlapAlgoServiceTest.java b/hugegraph-hubble/hubble-be/src/test/java/org/apache/hugegraph/service/algorithm/OlapAlgoServiceTest.java new file mode 100644 index 000000000..1bd15a570 --- /dev/null +++ b/hugegraph-hubble/hubble-be/src/test/java/org/apache/hugegraph/service/algorithm/OlapAlgoServiceTest.java @@ -0,0 +1,90 @@ +/* + * 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. + */ + +package org.apache.hugegraph.service.algorithm; + +import java.util.Collections; + +import org.junit.Test; +import org.mockito.Mockito; + +import org.apache.hugegraph.driver.ComputerManager; +import org.apache.hugegraph.driver.HugeClient; +import org.apache.hugegraph.entity.algorithm.OlapEntity; +import org.apache.hugegraph.exception.ServerCapabilityUnavailableException; +import org.apache.hugegraph.testutil.Assert; + +public class OlapAlgoServiceTest { + + @Test + public void testComputerAvailabilityUsesTaskEndpoint() { + HugeClient client = Mockito.mock(HugeClient.class); + ComputerManager computer = Mockito.mock(ComputerManager.class); + Mockito.when(client.computer()).thenReturn(computer); + Mockito.when(computer.list(1L)).thenReturn(Collections.emptyList()); + + Assert.assertTrue(new OlapAlgoService().computerAvailable(client)); + + Mockito.when(computer.list(1L)) + .thenThrow(new RuntimeException("request_failure")); + Assert.assertFalse(new OlapAlgoService().computerAvailable(client)); + } + + @Test + public void testMissingComputerErrorExplainsRequiredEnvironment() { + HugeClient client = Mockito.mock(HugeClient.class); + ComputerManager computer = Mockito.mock(ComputerManager.class); + Mockito.when(client.computer()).thenReturn(computer); + Mockito.when(computer.create( + Mockito.anyString(), Mockito.anyLong(), Mockito.anyMap())) + .thenThrow(new RuntimeException((String) null)); + + Throwable error = Assert.assertThrows( + ServerCapabilityUnavailableException.class, + () -> new OlapAlgoService().olapView( + client, "DEFAULT", entity())); + + Assert.assertEquals("server.capability.computer.unavailable", + error.getMessage()); + } + + @Test + public void testMeaningfulComputerErrorRemainsVisible() { + HugeClient client = Mockito.mock(HugeClient.class); + ComputerManager computer = Mockito.mock(ComputerManager.class); + Mockito.when(client.computer()).thenReturn(computer); + RuntimeException failure = new RuntimeException("invalid worker"); + Mockito.when(computer.create( + Mockito.anyString(), Mockito.anyLong(), Mockito.anyMap())) + .thenThrow(failure); + + Throwable error = Assert.assertThrows( + RuntimeException.class, + () -> new OlapAlgoService().olapView( + client, "DEFAULT", entity())); + + Assert.assertSame(failure, error); + } + + private static OlapEntity entity() { + return OlapEntity.builder() + .algorithm("page-rank") + .worker(2L) + .params(Collections.emptyMap()) + .build(); + } +} diff --git a/hugegraph-hubble/hubble-be/src/test/java/org/apache/hugegraph/unit/ConfigControllerTest.java b/hugegraph-hubble/hubble-be/src/test/java/org/apache/hugegraph/unit/ConfigControllerTest.java index e99ed7393..630e44add 100644 --- a/hugegraph-hubble/hubble-be/src/test/java/org/apache/hugegraph/unit/ConfigControllerTest.java +++ b/hugegraph-hubble/hubble-be/src/test/java/org/apache/hugegraph/unit/ConfigControllerTest.java @@ -57,6 +57,7 @@ public class ConfigControllerTest { Map<String, Object> result = controller.getConfig(); Assert.assertEquals(Map.of("pd_enabled", false, + "server_capabilities_verified", true, "auth_enabled", false, "graph_create_enabled", false, "cypher_enabled", false), result); @@ -89,6 +90,7 @@ public class ConfigControllerTest { Map<String, Object> result = controller.getConfig(); Assert.assertEquals(Map.of("pd_enabled", false, + "server_capabilities_verified", true, "auth_enabled", true, "graph_create_enabled", true, "cypher_enabled", false), result); @@ -114,6 +116,7 @@ public class ConfigControllerTest { Map<String, Object> result = controller.getConfig(); Assert.assertEquals(Map.of("pd_enabled", false, + "server_capabilities_verified", false, "auth_enabled", true, "graph_create_enabled", false, "cypher_enabled", false), result); diff --git a/hugegraph-hubble/hubble-be/src/test/java/org/apache/hugegraph/unit/UnitTestSuite.java b/hugegraph-hubble/hubble-be/src/test/java/org/apache/hugegraph/unit/UnitTestSuite.java index 9ce4fe4e5..c7ed4f658 100644 --- a/hugegraph-hubble/hubble-be/src/test/java/org/apache/hugegraph/unit/UnitTestSuite.java +++ b/hugegraph-hubble/hubble-be/src/test/java/org/apache/hugegraph/unit/UnitTestSuite.java @@ -27,6 +27,7 @@ import org.apache.hugegraph.controller.schema.SchemaControllerSecurityTest; import org.apache.hugegraph.controller.space.GraphSpaceControllerTest; import org.apache.hugegraph.controller.space.SchemaTemplateControllerSecurityTest; import org.apache.hugegraph.handler.ResponseAdvisorStatusTest; +import org.apache.hugegraph.service.algorithm.OlapAlgoServiceTest; import org.apache.hugegraph.service.load.IngestTransactionIntegrationTest; import org.apache.hugegraph.service.auth.AuthContextServiceTest; import org.apache.hugegraph.service.auth.AuthModeServiceTest; @@ -74,6 +75,7 @@ import org.junit.runners.Suite; LangChainControllerSecurityTest.class, LegacyFacadeRemovalTest.class, MessageSourceHandlerTest.class, + OlapAlgoServiceTest.class, SchemaControllerSecurityTest.class, SchemaTemplateControllerSecurityTest.class, GroovySchemaCompatibilityTest.class, diff --git a/hugegraph-hubble/hubble-be/src/test/java/org/apache/hugegraph/unit/UserServiceCompatibilityTest.java b/hugegraph-hubble/hubble-be/src/test/java/org/apache/hugegraph/unit/UserServiceCompatibilityTest.java index 3075790d8..600fe3728 100644 --- a/hugegraph-hubble/hubble-be/src/test/java/org/apache/hugegraph/unit/UserServiceCompatibilityTest.java +++ b/hugegraph-hubble/hubble-be/src/test/java/org/apache/hugegraph/unit/UserServiceCompatibilityTest.java @@ -67,7 +67,7 @@ public class UserServiceCompatibilityTest { Mockito.when(this.client.graphSpace()).thenReturn(this.graphSpace); Mockito.when(this.client.graphs()).thenReturn(this.graphs); Mockito.when(this.auth.createUser(Mockito.any(User.class))) - .thenReturn(new User()); + .thenReturn(user("created-id")); Mockito.when(this.auth.listSuperAdmin()) .thenReturn(java.util.Collections.emptyList()); this.service = new UserService(); @@ -80,11 +80,13 @@ public class UserServiceCompatibilityTest { public void testStandaloneUserCreationOmitsPdOnlyNickname() { Mockito.when(this.config.get(HubbleOptions.PD_ENABLED)).thenReturn(false); - this.service.add(this.client, userEntity("display-name")); + UserEntity created = + this.service.add(this.client, userEntity("display-name")); ArgumentCaptor<User> request = ArgumentCaptor.forClass(User.class); Mockito.verify(this.auth).createUser(request.capture()); Assert.assertNull(request.getValue().nickname()); + Assert.assertEquals("created-id", created.getId()); } @Test diff --git a/hugegraph-hubble/hubble-fe/src/App.js b/hugegraph-hubble/hubble-fe/src/App.js index ec6ee6aa8..5b6b951c0 100644 --- a/hugegraph-hubble/hubble-fe/src/App.js +++ b/hugegraph-hubble/hubble-fe/src/App.js @@ -27,29 +27,40 @@ import * as api from './api'; import {setConfig} from './utils/config'; import {useEffect, useState} from 'react'; +const CONFIG_RETRY_DELAY_MS = 2000; + function App() { const [configReady, setConfigReady] = useState(false); const [configError, setConfigError] = useState(false); useEffect(() => { let active = true; - api.config.getConfig() - .then(response => { + let retryTimer; + const loadConfig = () => { + api.config.getConfig().then(response => { if (response?.status !== 200 || !response.data) { throw new Error('invalid_hubble_config'); } if (active) { setConfig(response.data); setConfigReady(true); + setConfigError(false); + if (response.data.server_capabilities_verified === false) { + retryTimer = window.setTimeout( + loadConfig, CONFIG_RETRY_DELAY_MS + ); + } } - }) - .catch(() => { + }).catch(() => { if (active) { setConfigError(true); } }); + }; + loadConfig(); return () => { active = false; + window.clearTimeout(retryTimer); }; }, []); diff --git a/hugegraph-hubble/hubble-fe/src/App.test.js b/hugegraph-hubble/hubble-fe/src/App.test.js index a3ed5534d..20eb51794 100644 --- a/hugegraph-hubble/hubble-fe/src/App.test.js +++ b/hugegraph-hubble/hubble-fe/src/App.test.js @@ -16,7 +16,7 @@ * under the License. */ -import {render, screen} from '@testing-library/react'; +import {act, render, screen, waitFor} from '@testing-library/react'; import {MemoryRouter} from 'react-router-dom'; import App from './App'; import * as api from './api'; @@ -70,3 +70,44 @@ test('shows a retry surface when configuration bootstrap fails', async () => { ); expect(screen.getByRole('button', {name: 'Retry'})).toBeInTheDocument(); }); + +test('revalidates fail-closed server capabilities until verified', async () => { + jest.useFakeTimers(); + api.config.getConfig + .mockResolvedValueOnce({ + status: 200, + data: { + pd_enabled: false, + auth_enabled: true, + server_capabilities_verified: false, + }, + }) + .mockResolvedValueOnce({ + status: 200, + data: { + pd_enabled: false, + auth_enabled: false, + server_capabilities_verified: true, + }, + }); + + render( + <MemoryRouter + future={{v7_startTransition: true, v7_relativeSplatPath: true}} + > + <App /> + </MemoryRouter> + ); + expect(await screen.findByTestId('app-route')).toBeInTheDocument(); + + await act(async () => { + jest.advanceTimersByTime(2000); + }); + await waitFor(() => expect(api.config.getConfig).toHaveBeenCalledTimes(2)); + expect(JSON.parse(sessionStorage.getItem('hubble_config_'))) + .toMatchObject({ + auth_enabled: false, + server_capabilities_verified: true, + }); + jest.useRealTimers(); +}); diff --git a/hugegraph-hubble/hubble-fe/src/api/analysis.js b/hugegraph-hubble/hubble-fe/src/api/analysis.js index eddbf1abf..835614c75 100644 --- a/hugegraph-hubble/hubble-fe/src/api/analysis.js +++ b/hugegraph-hubble/hubble-fe/src/api/analysis.js @@ -170,6 +170,13 @@ const postOlapInfo = (graphspace, graph, data) => { }); }; +const getOlapCapability = (graphspace, graph, config = {}) => { + return request.get( + `/graphspaces/${graphspace}/graphs/${graph}/algorithms/olap/capability`, + config + ); +}; + const runOltpInfo = (graphspace, graph, data) => { return runAlgorithmRequest(() => { return request.post( @@ -226,6 +233,7 @@ export { fetchManageTaskList, addFavoriate, postOlapInfo, + getOlapCapability, runOltpInfo, runOlapVermeer, fetchAsyncTaskResult, diff --git a/hugegraph-hubble/hubble-fe/src/i18n/resources/en-US/modules/analysis.json b/hugegraph-hubble/hubble-fe/src/i18n/resources/en-US/modules/analysis.json index eefd7cfaf..03640f187 100644 --- a/hugegraph-hubble/hubble-fe/src/i18n/resources/en-US/modules/analysis.json +++ b/hugegraph-hubble/hubble-fe/src/i18n/resources/en-US/modules/analysis.json @@ -477,6 +477,13 @@ }, "placeholder": "Algorithm Query", "run": "Run", + "run_shortcut": "Run (⌘/Ctrl + Enter)", + "computer_environment_title": "HugeGraph Computer environment required", + "computer_environment_description": "These whole-graph algorithms run asynchronously through HugeGraph Computer. Configure its compute environment, including Kubernetes when required by the deployment, before submitting a task", + "computer_unavailable_title": "HugeGraph Computer unavailable", + "computer_unavailable_description": "This graph cannot reach HugeGraph Computer. Configure Computer and its Kubernetes environment before running batch algorithms", + "vermeer_unavailable_title": "Vermeer computation is unavailable", + "vermeer_unavailable_description": "This Hubble version cannot securely submit Vermeer compute tasks. Configure HugeGraph Computer or upgrade the Vermeer integration before running batch algorithms", "task_submit_success": "{{name}} task submitted successfully", "common": { "instance_num": "Instance Count", @@ -560,7 +567,7 @@ }, "mode": { "OLTP": "Interactive exploration", - "OLAP": "Batch analysis (async)" + "OLAP": "Cluster batch computation" }, "capacity_item": { "tooltip": "Maximum number of vertices visited during traversal" @@ -879,6 +886,7 @@ "result_title": "Task result", "result_context": "{{graphspace}} / {{graph}} / Task {{taskId}}", "result_back": "Back to async tasks", + "result_open_full": "Open full result", "delete_failed": "Delete failed", "abort_failed": "Abort failed", "delete_confirm_title": "Delete Task", @@ -930,7 +938,8 @@ "aborting": "Aborting", "delete": "Delete", "check_result": "View Result", - "check_reason": "View Reason" + "check_reason": "View Reason", + "collapse_result": "Hide Result" } }, "topbar": { diff --git a/hugegraph-hubble/hubble-fe/src/i18n/resources/zh-CN/modules/analysis.json b/hugegraph-hubble/hubble-fe/src/i18n/resources/zh-CN/modules/analysis.json index 8d681f628..fa91e30fd 100644 --- a/hugegraph-hubble/hubble-fe/src/i18n/resources/zh-CN/modules/analysis.json +++ b/hugegraph-hubble/hubble-fe/src/i18n/resources/zh-CN/modules/analysis.json @@ -477,6 +477,13 @@ }, "placeholder": "算法查询", "run": "运行", + "run_shortcut": "运行(⌘/Ctrl + Enter)", + "computer_environment_title": "需要 HugeGraph Computer 计算环境", + "computer_environment_description": "这些全图算法通过 HugeGraph Computer 异步执行。提交任务前需先配置计算环境;部署要求 Kubernetes 时还需完成对应集群配置", + "computer_unavailable_title": "HugeGraph Computer 暂不可用", + "computer_unavailable_description": "当前图无法连接 HugeGraph Computer。请完成 Computer 与 Kubernetes 环境配置后再运行批量算法", + "vermeer_unavailable_title": "当前无法运行 Vermeer 计算", + "vermeer_unavailable_description": "当前 Hubble 版本无法安全提交 Vermeer 计算任务。请改用已配置的 HugeGraph Computer,或升级 Vermeer 集成后再运行批量算法", "task_submit_success": "{{name}} 算法任务提交成功", "common": { "instance_num": "实例数", @@ -560,7 +567,7 @@ }, "mode": { "OLTP": "交互式探索", - "OLAP": "异步批量分析" + "OLAP": "集群批量计算" }, "capacity_item": { "tooltip": "遍历过程中最大的访问的顶点数目" @@ -879,6 +886,7 @@ "result_title": "任务结果", "result_context": "{{graphspace}} / {{graph}} / 任务 {{taskId}}", "result_back": "返回异步任务", + "result_open_full": "查看完整结果", "delete_failed": "删除失败", "abort_failed": "终止失败", "delete_confirm_title": "删除任务", @@ -930,7 +938,8 @@ "aborting": "终止中", "delete": "删除", "check_result": "查看结果", - "check_reason": "查看原因" + "check_reason": "查看原因", + "collapse_result": "收起结果" } }, "topbar": { diff --git a/hugegraph-hubble/hubble-fe/src/modules/algorithm/algorithmsForm/AlgorithmNameHeader/index.js b/hugegraph-hubble/hubble-fe/src/modules/algorithm/algorithmsForm/AlgorithmNameHeader/index.js index ae95ae77d..a44c34416 100644 --- a/hugegraph-hubble/hubble-fe/src/modules/algorithm/algorithmsForm/AlgorithmNameHeader/index.js +++ b/hugegraph-hubble/hubble-fe/src/modules/algorithm/algorithmsForm/AlgorithmNameHeader/index.js @@ -120,7 +120,11 @@ const AlgorithmNameHeader = props => { return ( <Tooltip placement="rightTop" - title={<span style={{color: '#000'}}>{t('analysis.algorithm.run')}</span>} + title={( + <span style={{color: '#000'}}> + {t('analysis.algorithm.run_shortcut')} + </span> + )} color={'#fff'} > <Button diff --git a/hugegraph-hubble/hubble-fe/src/modules/algorithm/algorithmsForm/Home/index.js b/hugegraph-hubble/hubble-fe/src/modules/algorithm/algorithmsForm/Home/index.js index ba2721929..6425770be 100644 --- a/hugegraph-hubble/hubble-fe/src/modules/algorithm/algorithmsForm/Home/index.js +++ b/hugegraph-hubble/hubble-fe/src/modules/algorithm/algorithmsForm/Home/index.js @@ -104,6 +104,8 @@ const AlgorithmFormHome = props => { currentAlgorithm={currentAlgorithm} updateCurrentAlgorithm={updateCurrentAlgorithm} canRunLouvain={canRunLouvain} + graphSpace={graphSpace} + graph={graph} /> </div> ); diff --git a/hugegraph-hubble/hubble-fe/src/modules/algorithm/algorithmsForm/MaxDepthItem/index.js b/hugegraph-hubble/hubble-fe/src/modules/algorithm/algorithmsForm/MaxDepthItem/index.js index c596faad7..9cd54e788 100644 --- a/hugegraph-hubble/hubble-fe/src/modules/algorithm/algorithmsForm/MaxDepthItem/index.js +++ b/hugegraph-hubble/hubble-fe/src/modules/algorithm/algorithmsForm/MaxDepthItem/index.js @@ -33,7 +33,8 @@ const MaxDepthItem = props => { <Form.Item label='max_depth' name='max_depth' - rules={[{required: true}, {validator: validator}]} + initialValue={3} + rules={[{validator: validator}]} tooltip={t('analysis.algorithm.max_depth_item.tooltip')} > <InputNumber /> diff --git a/hugegraph-hubble/hubble-fe/src/modules/algorithm/algorithmsForm/Olap/BetweennessCentrality/index.js b/hugegraph-hubble/hubble-fe/src/modules/algorithm/algorithmsForm/Olap/BetweennessCentrality/index.js index c39acc55b..3bac1ef8b 100644 --- a/hugegraph-hubble/hubble-fe/src/modules/algorithm/algorithmsForm/Olap/BetweennessCentrality/index.js +++ b/hugegraph-hubble/hubble-fe/src/modules/algorithm/algorithmsForm/Olap/BetweennessCentrality/index.js @@ -134,6 +134,8 @@ const BetweennessCentrality = props => { } > <Form + isDisabled={!isEnableRun} + isRunning={isRequiring} form={form} onFinish={onFormFinish} onValuesChange={_.debounce(onFormValuesChange, 300)} diff --git a/hugegraph-hubble/hubble-fe/src/modules/algorithm/algorithmsForm/Olap/BetweennessCentralityVermeer/index.js b/hugegraph-hubble/hubble-fe/src/modules/algorithm/algorithmsForm/Olap/BetweennessCentralityVermeer/index.js index 6d25a3a3e..d40e4f95c 100644 --- a/hugegraph-hubble/hubble-fe/src/modules/algorithm/algorithmsForm/Olap/BetweennessCentralityVermeer/index.js +++ b/hugegraph-hubble/hubble-fe/src/modules/algorithm/algorithmsForm/Olap/BetweennessCentralityVermeer/index.js @@ -138,6 +138,8 @@ const BetweennessCentralityVermeer = props => { forceRender > <Form + isDisabled={!isEnableRun} + isRunning={isRequiring} disabled={graphStatus !== LOADED} form={form} onFinish={onFormFinish} diff --git a/hugegraph-hubble/hubble-fe/src/modules/algorithm/algorithmsForm/Olap/ClosenessCentrality/index.js b/hugegraph-hubble/hubble-fe/src/modules/algorithm/algorithmsForm/Olap/ClosenessCentrality/index.js index 8882d13f9..54ba426e4 100644 --- a/hugegraph-hubble/hubble-fe/src/modules/algorithm/algorithmsForm/Olap/ClosenessCentrality/index.js +++ b/hugegraph-hubble/hubble-fe/src/modules/algorithm/algorithmsForm/Olap/ClosenessCentrality/index.js @@ -132,6 +132,8 @@ const ClosenessCentrality = props => { } > <Form + isDisabled={!isEnableRun} + isRunning={isRequiring} form={form} onFinish={onFormFinish} onValuesChange={_.debounce(onFormValuesChange, 300)} diff --git a/hugegraph-hubble/hubble-fe/src/modules/algorithm/algorithmsForm/Olap/ClosenessCentralityVermeer/index.js b/hugegraph-hubble/hubble-fe/src/modules/algorithm/algorithmsForm/Olap/ClosenessCentralityVermeer/index.js index 7aa39be4a..8f1f42af0 100644 --- a/hugegraph-hubble/hubble-fe/src/modules/algorithm/algorithmsForm/Olap/ClosenessCentralityVermeer/index.js +++ b/hugegraph-hubble/hubble-fe/src/modules/algorithm/algorithmsForm/Olap/ClosenessCentralityVermeer/index.js @@ -142,6 +142,8 @@ const ClosenessCentralityVermeer = props => { forceRender > <Form + isDisabled={!isEnableRun} + isRunning={isRequiring} form={form} disabled={graphStatus !== LOADED} onFinish={onFormFinish} diff --git a/hugegraph-hubble/hubble-fe/src/modules/algorithm/algorithmsForm/Olap/ClusterCoefficient/index.js b/hugegraph-hubble/hubble-fe/src/modules/algorithm/algorithmsForm/Olap/ClusterCoefficient/index.js index ff98e1951..fa132afda 100644 --- a/hugegraph-hubble/hubble-fe/src/modules/algorithm/algorithmsForm/Olap/ClusterCoefficient/index.js +++ b/hugegraph-hubble/hubble-fe/src/modules/algorithm/algorithmsForm/Olap/ClusterCoefficient/index.js @@ -132,6 +132,8 @@ const ClusterCoefficient = props => { } > <Form + isDisabled={!isEnableRun} + isRunning={isRequiring} form={form} onFinish={onFormFinish} onValuesChange={_.debounce(onFormValuesChange, 300)} diff --git a/hugegraph-hubble/hubble-fe/src/modules/algorithm/algorithmsForm/Olap/DegreeCentrality/index.js b/hugegraph-hubble/hubble-fe/src/modules/algorithm/algorithmsForm/Olap/DegreeCentrality/index.js index 6e3e23203..f01663fd3 100644 --- a/hugegraph-hubble/hubble-fe/src/modules/algorithm/algorithmsForm/Olap/DegreeCentrality/index.js +++ b/hugegraph-hubble/hubble-fe/src/modules/algorithm/algorithmsForm/Olap/DegreeCentrality/index.js @@ -131,6 +131,8 @@ const DegreeCentrality = props => { } > <Form + isDisabled={!isEnableRun} + isRunning={isRequiring} form={form} onFinish={onFormFinish} onValuesChange={_.debounce(onFormValuesChange, 300)} diff --git a/hugegraph-hubble/hubble-fe/src/modules/algorithm/algorithmsForm/Olap/DegreeCentralityVermeer/index.js b/hugegraph-hubble/hubble-fe/src/modules/algorithm/algorithmsForm/Olap/DegreeCentralityVermeer/index.js index 16350b2fb..47d3f69b3 100644 --- a/hugegraph-hubble/hubble-fe/src/modules/algorithm/algorithmsForm/Olap/DegreeCentralityVermeer/index.js +++ b/hugegraph-hubble/hubble-fe/src/modules/algorithm/algorithmsForm/Olap/DegreeCentralityVermeer/index.js @@ -144,6 +144,8 @@ const DegreeCentralityVermeer = props => { forceRender > <Form + isDisabled={!isEnableRun} + isRunning={isRequiring} form={form} disabled={graphStatus !== LOADED} onFinish={onFormFinish} diff --git a/hugegraph-hubble/hubble-fe/src/modules/algorithm/algorithmsForm/Olap/FilterSubGraphMatching/index.js b/hugegraph-hubble/hubble-fe/src/modules/algorithm/algorithmsForm/Olap/FilterSubGraphMatching/index.js index faaeb4c5c..bc1e0d2cc 100644 --- a/hugegraph-hubble/hubble-fe/src/modules/algorithm/algorithmsForm/Olap/FilterSubGraphMatching/index.js +++ b/hugegraph-hubble/hubble-fe/src/modules/algorithm/algorithmsForm/Olap/FilterSubGraphMatching/index.js @@ -137,6 +137,8 @@ const FilterSubGraphMatching = props => { } > <Form + isDisabled={!isEnableRun} + isRunning={isRequiring} form={form} onFinish={onFormFinish} onValuesChange={_.debounce(onFormValuesChange, 300)} diff --git a/hugegraph-hubble/hubble-fe/src/modules/algorithm/algorithmsForm/Olap/FilteredRingsDetection/index.js b/hugegraph-hubble/hubble-fe/src/modules/algorithm/algorithmsForm/Olap/FilteredRingsDetection/index.js index c498681b8..198675a62 100644 --- a/hugegraph-hubble/hubble-fe/src/modules/algorithm/algorithmsForm/Olap/FilteredRingsDetection/index.js +++ b/hugegraph-hubble/hubble-fe/src/modules/algorithm/algorithmsForm/Olap/FilteredRingsDetection/index.js @@ -137,6 +137,8 @@ const FilteredRingsDetection = props => { } > <Form + isDisabled={!isEnableRun} + isRunning={isRequiring} form={form} onFinish={onFormFinish} onValuesChange={_.debounce(onFormValuesChange, 300)} diff --git a/hugegraph-hubble/hubble-fe/src/modules/algorithm/algorithmsForm/Olap/KCore/index.js b/hugegraph-hubble/hubble-fe/src/modules/algorithm/algorithmsForm/Olap/KCore/index.js index bd60438fb..c1baeaf15 100644 --- a/hugegraph-hubble/hubble-fe/src/modules/algorithm/algorithmsForm/Olap/KCore/index.js +++ b/hugegraph-hubble/hubble-fe/src/modules/algorithm/algorithmsForm/Olap/KCore/index.js @@ -133,6 +133,8 @@ const KCore = props => { } > <Form + isDisabled={!isEnableRun} + isRunning={isRequiring} form={form} onFinish={onFormFinish} onValuesChange={_.debounce(onFormValuesChange, 300)} diff --git a/hugegraph-hubble/hubble-fe/src/modules/algorithm/algorithmsForm/Olap/KCoreVermeer/index.js b/hugegraph-hubble/hubble-fe/src/modules/algorithm/algorithmsForm/Olap/KCoreVermeer/index.js index 7d043562c..7f151a3ff 100644 --- a/hugegraph-hubble/hubble-fe/src/modules/algorithm/algorithmsForm/Olap/KCoreVermeer/index.js +++ b/hugegraph-hubble/hubble-fe/src/modules/algorithm/algorithmsForm/Olap/KCoreVermeer/index.js @@ -142,6 +142,8 @@ const KCoreVermeer = props => { forceRender > <Form + isDisabled={!isEnableRun} + isRunning={isRequiring} form={form} disabled={graphStatus !== LOADED} onFinish={onFormFinish} diff --git a/hugegraph-hubble/hubble-fe/src/modules/algorithm/algorithmsForm/Olap/LabelPropagationAlgorithm/index.js b/hugegraph-hubble/hubble-fe/src/modules/algorithm/algorithmsForm/Olap/LabelPropagationAlgorithm/index.js index a0cc74aa9..d62852628 100644 --- a/hugegraph-hubble/hubble-fe/src/modules/algorithm/algorithmsForm/Olap/LabelPropagationAlgorithm/index.js +++ b/hugegraph-hubble/hubble-fe/src/modules/algorithm/algorithmsForm/Olap/LabelPropagationAlgorithm/index.js @@ -130,6 +130,8 @@ const LabelPropagationAlgorithm = props => { } > <Form + isDisabled={!isEnableRun} + isRunning={isRequiring} form={form} onFinish={onFormFinish} onValuesChange={_.debounce(onFormValuesChange, 300)} diff --git a/hugegraph-hubble/hubble-fe/src/modules/algorithm/algorithmsForm/Olap/LabelPropagationAlgorithmVermeer/index.js b/hugegraph-hubble/hubble-fe/src/modules/algorithm/algorithmsForm/Olap/LabelPropagationAlgorithmVermeer/index.js index b3b710162..e0f81a70f 100644 --- a/hugegraph-hubble/hubble-fe/src/modules/algorithm/algorithmsForm/Olap/LabelPropagationAlgorithmVermeer/index.js +++ b/hugegraph-hubble/hubble-fe/src/modules/algorithm/algorithmsForm/Olap/LabelPropagationAlgorithmVermeer/index.js @@ -143,6 +143,8 @@ const LabelPropagationAlgorithmVermeer = props => { forceRender > <Form + isDisabled={!isEnableRun} + isRunning={isRequiring} form={form} disabled={graphStatus !== LOADED} onFinish={onFormFinish} diff --git a/hugegraph-hubble/hubble-fe/src/modules/algorithm/algorithmsForm/Olap/Links/index.js b/hugegraph-hubble/hubble-fe/src/modules/algorithm/algorithmsForm/Olap/Links/index.js index 2110d92bf..c533b89ae 100644 --- a/hugegraph-hubble/hubble-fe/src/modules/algorithm/algorithmsForm/Olap/Links/index.js +++ b/hugegraph-hubble/hubble-fe/src/modules/algorithm/algorithmsForm/Olap/Links/index.js @@ -135,6 +135,8 @@ const Links = props => { } > <Form + isDisabled={!isEnableRun} + isRunning={isRequiring} form={form} onFinish={onFormFinish} onValuesChange={_.debounce(onFormValuesChange, 300)} diff --git a/hugegraph-hubble/hubble-fe/src/modules/algorithm/algorithmsForm/Olap/Louvain/index.js b/hugegraph-hubble/hubble-fe/src/modules/algorithm/algorithmsForm/Olap/Louvain/index.js index c69e51f96..9df26c8c5 100644 --- a/hugegraph-hubble/hubble-fe/src/modules/algorithm/algorithmsForm/Olap/Louvain/index.js +++ b/hugegraph-hubble/hubble-fe/src/modules/algorithm/algorithmsForm/Olap/Louvain/index.js @@ -172,6 +172,8 @@ const Louvain = props => { forceRender > <Form + isDisabled={!isEnableRun} + isRunning={isRequiring} form={form} onFinish={onFormFinish} onValuesChange={debouncedRunAvailability} diff --git a/hugegraph-hubble/hubble-fe/src/modules/algorithm/algorithmsForm/Olap/PageRank/index.js b/hugegraph-hubble/hubble-fe/src/modules/algorithm/algorithmsForm/Olap/PageRank/index.js index 99e613419..0e125894d 100644 --- a/hugegraph-hubble/hubble-fe/src/modules/algorithm/algorithmsForm/Olap/PageRank/index.js +++ b/hugegraph-hubble/hubble-fe/src/modules/algorithm/algorithmsForm/Olap/PageRank/index.js @@ -131,6 +131,8 @@ const PageRank = props => { } > <Form + isDisabled={!isEnableRun} + isRunning={isRequiring} form={form} onFinish={onFormFinish} onValuesChange={_.debounce(onFormValuesChange, 300)} diff --git a/hugegraph-hubble/hubble-fe/src/modules/algorithm/algorithmsForm/Olap/PageRankVermeer/index.js b/hugegraph-hubble/hubble-fe/src/modules/algorithm/algorithmsForm/Olap/PageRankVermeer/index.js index 40af1a3c9..6100e3fcb 100644 --- a/hugegraph-hubble/hubble-fe/src/modules/algorithm/algorithmsForm/Olap/PageRankVermeer/index.js +++ b/hugegraph-hubble/hubble-fe/src/modules/algorithm/algorithmsForm/Olap/PageRankVermeer/index.js @@ -131,6 +131,8 @@ const PageRankVermeer = props => { forceRender > <Form + isDisabled={!isEnableRun} + isRunning={isRequiring} form={form} disabled={graphStatus !== LOADED} onFinish={onFormFinish} diff --git a/hugegraph-hubble/hubble-fe/src/modules/algorithm/algorithmsForm/Olap/PersonalPageRank/index.js b/hugegraph-hubble/hubble-fe/src/modules/algorithm/algorithmsForm/Olap/PersonalPageRank/index.js index b84462eee..1ede08edd 100644 --- a/hugegraph-hubble/hubble-fe/src/modules/algorithm/algorithmsForm/Olap/PersonalPageRank/index.js +++ b/hugegraph-hubble/hubble-fe/src/modules/algorithm/algorithmsForm/Olap/PersonalPageRank/index.js @@ -134,6 +134,8 @@ const PersonalPageRank = props => { } > <Form + isDisabled={!isEnableRun} + isRunning={isRequiring} form={form} onFinish={onFormFinish} onValuesChange={_.debounce(onFormValuesChange, 300)} diff --git a/hugegraph-hubble/hubble-fe/src/modules/algorithm/algorithmsForm/Olap/RingsDetection/index.js b/hugegraph-hubble/hubble-fe/src/modules/algorithm/algorithmsForm/Olap/RingsDetection/index.js index 67aae215c..f59bce8b7 100644 --- a/hugegraph-hubble/hubble-fe/src/modules/algorithm/algorithmsForm/Olap/RingsDetection/index.js +++ b/hugegraph-hubble/hubble-fe/src/modules/algorithm/algorithmsForm/Olap/RingsDetection/index.js @@ -129,6 +129,8 @@ const RingsDetection = props => { } > <Form + isDisabled={!isEnableRun} + isRunning={isRequiring} form={form} onFinish={onFormFinish} onValuesChange={_.debounce(onFormValuesChange, 300)} diff --git a/hugegraph-hubble/hubble-fe/src/modules/algorithm/algorithmsForm/Olap/SSSPVermeer/index.js b/hugegraph-hubble/hubble-fe/src/modules/algorithm/algorithmsForm/Olap/SSSPVermeer/index.js index 7b96ed620..7206406b5 100644 --- a/hugegraph-hubble/hubble-fe/src/modules/algorithm/algorithmsForm/Olap/SSSPVermeer/index.js +++ b/hugegraph-hubble/hubble-fe/src/modules/algorithm/algorithmsForm/Olap/SSSPVermeer/index.js @@ -134,6 +134,8 @@ const SSSPVermeer = props => { forceRender > <Form + isDisabled={!isEnableRun} + isRunning={isRequiring} form={form} disabled={graphStatus !== LOADED} onFinish={onFormFinish} diff --git a/hugegraph-hubble/hubble-fe/src/modules/algorithm/algorithmsForm/Olap/TriangleCount/index.js b/hugegraph-hubble/hubble-fe/src/modules/algorithm/algorithmsForm/Olap/TriangleCount/index.js index d824f23d6..116f552fe 100644 --- a/hugegraph-hubble/hubble-fe/src/modules/algorithm/algorithmsForm/Olap/TriangleCount/index.js +++ b/hugegraph-hubble/hubble-fe/src/modules/algorithm/algorithmsForm/Olap/TriangleCount/index.js @@ -133,6 +133,8 @@ const TriangleCount = props => { } > <Form + isDisabled={!isEnableRun} + isRunning={isRequiring} form={form} onFinish={onFormFinish} onValuesChange={_.debounce(onFormValuesChange, 300)} diff --git a/hugegraph-hubble/hubble-fe/src/modules/algorithm/algorithmsForm/Olap/TriangleCountVermeer/index.js b/hugegraph-hubble/hubble-fe/src/modules/algorithm/algorithmsForm/Olap/TriangleCountVermeer/index.js index a8678c300..72a6e0004 100644 --- a/hugegraph-hubble/hubble-fe/src/modules/algorithm/algorithmsForm/Olap/TriangleCountVermeer/index.js +++ b/hugegraph-hubble/hubble-fe/src/modules/algorithm/algorithmsForm/Olap/TriangleCountVermeer/index.js @@ -133,6 +133,8 @@ const TriangleCountVermeer = props => { forceRender > <Form + isDisabled={!isEnableRun} + isRunning={isRequiring} form={form} disabled={graphStatus !== LOADED} onFinish={onFormFinish} diff --git a/hugegraph-hubble/hubble-fe/src/modules/algorithm/algorithmsForm/Olap/WeaklyConnectedComponent/index.js b/hugegraph-hubble/hubble-fe/src/modules/algorithm/algorithmsForm/Olap/WeaklyConnectedComponent/index.js index 064dba69f..0b7958feb 100644 --- a/hugegraph-hubble/hubble-fe/src/modules/algorithm/algorithmsForm/Olap/WeaklyConnectedComponent/index.js +++ b/hugegraph-hubble/hubble-fe/src/modules/algorithm/algorithmsForm/Olap/WeaklyConnectedComponent/index.js @@ -129,6 +129,8 @@ const WeaklyConnectedComponent = props => { } > <Form + isDisabled={!isEnableRun} + isRunning={isRequiring} form={form} onFinish={onFormFinish} onValuesChange={_.debounce(onFormValuesChange, 300)} diff --git a/hugegraph-hubble/hubble-fe/src/modules/algorithm/algorithmsForm/Olap/WeaklyConnectedComponentVermeer/index.js b/hugegraph-hubble/hubble-fe/src/modules/algorithm/algorithmsForm/Olap/WeaklyConnectedComponentVermeer/index.js index 2d0159ac6..859df8a26 100644 --- a/hugegraph-hubble/hubble-fe/src/modules/algorithm/algorithmsForm/Olap/WeaklyConnectedComponentVermeer/index.js +++ b/hugegraph-hubble/hubble-fe/src/modules/algorithm/algorithmsForm/Olap/WeaklyConnectedComponentVermeer/index.js @@ -135,6 +135,8 @@ const WeaklyConnectedComponentVermeer = props => { forceRender > <Form + isDisabled={!isEnableRun} + isRunning={isRequiring} form={form} disabled={graphStatus !== LOADED} onFinish={onFormFinish} diff --git a/hugegraph-hubble/hubble-fe/src/modules/algorithm/algorithmsForm/OlapHome/index.js b/hugegraph-hubble/hubble-fe/src/modules/algorithm/algorithmsForm/OlapHome/index.js index cc153a057..38d415633 100644 --- a/hugegraph-hubble/hubble-fe/src/modules/algorithm/algorithmsForm/OlapHome/index.js +++ b/hugegraph-hubble/hubble-fe/src/modules/algorithm/algorithmsForm/OlapHome/index.js @@ -20,8 +20,9 @@ * @file Olap图算法表单列表 */ -import React, {useContext} from 'react'; -import {Collapse, Tooltip} from 'antd'; +import React, {useContext, useEffect, useState} from 'react'; +import {Alert, Collapse, Tooltip} from 'antd'; +import * as api from '../../../../api'; import GraphAnalysisContext from '../../../Context'; import _ from 'lodash'; import { @@ -44,6 +45,8 @@ const OlapFormHome = props => { currentAlgorithm, updateCurrentAlgorithm, canRunLouvain, + graphSpace, + graph, } = props; const {ALGORITHM_NAME, ALGORITHM_MODE} = useTranslatedConstants(); const {t} = useTranslation(); @@ -99,6 +102,32 @@ const OlapFormHome = props => { const {OLAP} = ALGORITHM_MODE; const {isVermeer, graphStatus} = useContext(GraphAnalysisContext); + const [computerAvailable, setComputerAvailable] = useState(null); + + useEffect(() => { + let cancelled = false; + if (isVermeer || !graphSpace || !graph) { + setComputerAvailable(null); + return undefined; + } + setComputerAvailable(null); + api.analysis.getOlapCapability(graphSpace, graph, { + suppressBusinessErrorToast: true, + }).then(response => { + if (!cancelled) { + setComputerAvailable( + response?.status === 200 && response?.data?.available === true + ); + } + }).catch(() => { + if (!cancelled) { + setComputerAvailable(false); + } + }); + return () => { + cancelled = true; + }; + }, [graph, graphSpace, isVermeer]); const getSearchedList = (arr, value) => { return arr.filter(item => isAlgorithmNameMatched(item, value, t)); @@ -131,16 +160,36 @@ const OlapFormHome = props => { ), })).filter(group => !_.isEmpty(group.items)); const isEmptyBasicOlap = _.isEmpty(visibleGroups); - const shouldDisableForm = isVermeer && graphStatus !== GRAPH_LOAD_STATUS.LOADED; + const shouldDisableForm = isVermeer + ? graphStatus !== GRAPH_LOAD_STATUS.LOADED + : computerAvailable !== true; + const capabilityTitle = isVermeer + ? 'analysis.algorithm.vermeer_unavailable_title' + : computerAvailable === false + ? 'analysis.algorithm.computer_unavailable_title' + : 'analysis.algorithm.computer_environment_title'; + const capabilityDescription = isVermeer + ? 'analysis.algorithm.vermeer_unavailable_description' + : computerAvailable === false + ? 'analysis.algorithm.computer_unavailable_description' + : 'analysis.algorithm.computer_environment_description'; return ( <div> {!isEmptyBasicOlap && ( - <Tooltip title={shouldDisableForm ? t(TEXT_PATH.ALGORITHM_COMMON + '.query_tooltip') : ''}> - <div className={c.algorithmCatagery}>{OLAP}</div> - </Tooltip> + <> + <Tooltip title={shouldDisableForm ? t(TEXT_PATH.ALGORITHM_COMMON + '.query_tooltip') : ''}> + <div className={c.algorithmCatagery}>{OLAP}</div> + </Tooltip> + <Alert + showIcon + type={shouldDisableForm ? 'warning' : 'info'} + message={t(capabilityTitle)} + description={t(capabilityDescription)} + /> + </> )} - {visibleGroups.map(group => ( + {!isVermeer && computerAvailable === true && visibleGroups.map(group => ( <div key={group.key}> <div className={c.algorithmGoal}> {t(`analysis.algorithm.group.${group.key}`)} diff --git a/hugegraph-hubble/hubble-fe/src/modules/algorithm/algorithmsForm/OlapHome/index.test.js b/hugegraph-hubble/hubble-fe/src/modules/algorithm/algorithmsForm/OlapHome/index.test.js new file mode 100644 index 000000000..e60c3a5fd --- /dev/null +++ b/hugegraph-hubble/hubble-fe/src/modules/algorithm/algorithmsForm/OlapHome/index.test.js @@ -0,0 +1,111 @@ +/* + * 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 GraphAnalysisContext from '../../../Context'; +import OlapFormHome from '.'; +import * as api from '../../../../api'; + +jest.mock('../../../../api', () => ({ + analysis: { + getOlapCapability: jest.fn(), + }, +})); + +jest.mock('../Olap/OlapItem', () => ({algorithmName}) => ( + <div data-testid="olap-item">{algorithmName}</div> +)); + +jest.mock('../../../../utils/constants', () => ({ + GRAPH_LOAD_STATUS: {LOADED: 'LOADED'}, + isAlgorithmNameMatched: () => true, + TEXT_PATH: {ALGORITHM_COMMON: 'analysis.algorithm.common'}, + useTranslatedConstants: () => ({ + ALGORITHM_MODE: {OLAP: 'OLAP'}, + ALGORITHM_NAME: { + PAGE_RANK: 'PageRank', + WEAKLY_CONNECTED_COMPONENT: 'WCC', + DEGREE_CENTRALIT: 'Degree', + CLOSENESS_CENTRALITY: 'Closeness', + TRIANGLE_COUNT: 'Triangle', + RINGS_DETECTION: 'Rings', + FILTERED_RINGS_DETECTION: 'FilteredRings', + LINKS: 'Links', + CLUSTER_COEFFICIENT: 'Cluster', + BETWEENNESS_CENTRALITY: 'Betweenness', + LABEL_PROPAGATION_ALGORITHM: 'LabelPropagation', + LOUVAIN: 'Louvain', + FILTER_SUBGRAPH_MATCHING: 'Subgraph', + K_CORE: 'KCore', + PERSONAL_PAGE_RANK: 'PersonalPageRank', + SSSP: 'SSSP', + }, + }), +})); + +jest.mock('react-i18next', () => ({ + useTranslation: () => ({t: key => key}), +})); + +const renderHome = () => render( + <GraphAnalysisContext.Provider + value={{isVermeer: false, graphStatus: 'LOADED'}} + > + <OlapFormHome + graphSpace="DEFAULT" + graph="hugegraph" + search="" + currentAlgorithm="" + updateCurrentAlgorithm={jest.fn()} + onOlapFormSubmit={jest.fn()} + canRunLouvain + /> + </GraphAnalysisContext.Provider> +); + +test('does not render runnable Computer algorithms when capability is unavailable', + async () => { + api.analysis.getOlapCapability.mockResolvedValue({ + status: 200, + data: {available: false}, + }); + + renderHome(); + + expect(await screen.findByText( + 'analysis.algorithm.computer_unavailable_title' + )).toBeInTheDocument(); + expect(screen.queryByTestId('olap-item')).not.toBeInTheDocument(); + }); + +test('renders Computer algorithms only after capability succeeds', async () => { + api.analysis.getOlapCapability.mockResolvedValue({ + status: 200, + data: {available: true}, + }); + + renderHome(); + + await waitFor(() => { + expect(screen.getAllByTestId('olap-item').length).toBeGreaterThan(0); + }); + expect(api.analysis.getOlapCapability).toHaveBeenCalledWith( + 'DEFAULT', + 'hugegraph', + {suppressBusinessErrorToast: true} + ); +}); diff --git a/hugegraph-hubble/hubble-fe/src/modules/algorithm/algorithmsForm/Oltp/AdamicAdar/index.js b/hugegraph-hubble/hubble-fe/src/modules/algorithm/algorithmsForm/Oltp/AdamicAdar/index.js index 09b16d87d..f309a1a0e 100644 --- a/hugegraph-hubble/hubble-fe/src/modules/algorithm/algorithmsForm/Oltp/AdamicAdar/index.js +++ b/hugegraph-hubble/hubble-fe/src/modules/algorithm/algorithmsForm/Oltp/AdamicAdar/index.js @@ -137,6 +137,8 @@ const AdamicAdar = props => { } > <Form + isDisabled={!isEnableRun} + isRunning={isRequiring} form={form} onFinish={onFormFinish} onValuesChange={_.debounce(onFormValuesChange, 300)} diff --git a/hugegraph-hubble/hubble-fe/src/modules/algorithm/algorithmsForm/Oltp/AllPaths/index.js b/hugegraph-hubble/hubble-fe/src/modules/algorithm/algorithmsForm/Oltp/AllPaths/index.js index 3fd351ac7..06a5f5a50 100644 --- a/hugegraph-hubble/hubble-fe/src/modules/algorithm/algorithmsForm/Oltp/AllPaths/index.js +++ b/hugegraph-hubble/hubble-fe/src/modules/algorithm/algorithmsForm/Oltp/AllPaths/index.js @@ -225,6 +225,8 @@ const AllPaths = props => { } > <Form + isDisabled={!isEnableRun} + isRunning={isRequiring} form={crosspointsForm} onFinish={onFormFinish} onValuesChange={_.debounce(onFormValuesChange, 300)} diff --git a/hugegraph-hubble/hubble-fe/src/modules/algorithm/algorithmsForm/Oltp/Crosspoints/index.js b/hugegraph-hubble/hubble-fe/src/modules/algorithm/algorithmsForm/Oltp/Crosspoints/index.js index 0cfdc3ff8..44aada75c 100644 --- a/hugegraph-hubble/hubble-fe/src/modules/algorithm/algorithmsForm/Oltp/Crosspoints/index.js +++ b/hugegraph-hubble/hubble-fe/src/modules/algorithm/algorithmsForm/Oltp/Crosspoints/index.js @@ -124,6 +124,8 @@ const Crosspoints = props => { } > <Form + isDisabled={!isEnableRun} + isRunning={isRequiring} form={crosspointsForm} onFinish={onFormFinish} onValuesChange={_.debounce(onFormValuesChange, 300)} @@ -167,7 +169,8 @@ const Crosspoints = props => { <Form.Item label='max_depth' name='max_depth' - rules={[{required: true}, {validator: maxDepthValidator}]} + initialValue={3} + rules={[{validator: maxDepthValidator}]} tooltip={t('analysis.algorithm.max_depth_item.tooltip')} > <InputNumber /> diff --git a/hugegraph-hubble/hubble-fe/src/modules/algorithm/algorithmsForm/Oltp/CustomizedCrosspoints/Home/index.js b/hugegraph-hubble/hubble-fe/src/modules/algorithm/algorithmsForm/Oltp/CustomizedCrosspoints/Home/index.js index 12d97aaa7..1b0bb27a5 100644 --- a/hugegraph-hubble/hubble-fe/src/modules/algorithm/algorithmsForm/Oltp/CustomizedCrosspoints/Home/index.js +++ b/hugegraph-hubble/hubble-fe/src/modules/algorithm/algorithmsForm/Oltp/CustomizedCrosspoints/Home/index.js @@ -138,6 +138,8 @@ const CustomizedCrosspoints = props => { } > <Form + isDisabled={!isEnableRun} + isRunning={isRequiring} form={form} onFinish={onFormFinish} onValuesChange={_.debounce(onFormValuesChange, 300)} diff --git a/hugegraph-hubble/hubble-fe/src/modules/algorithm/algorithmsForm/Oltp/CustomizedPaths/Home/index.js b/hugegraph-hubble/hubble-fe/src/modules/algorithm/algorithmsForm/Oltp/CustomizedPaths/Home/index.js index f0e2b9c56..ce944d935 100644 --- a/hugegraph-hubble/hubble-fe/src/modules/algorithm/algorithmsForm/Oltp/CustomizedPaths/Home/index.js +++ b/hugegraph-hubble/hubble-fe/src/modules/algorithm/algorithmsForm/Oltp/CustomizedPaths/Home/index.js @@ -146,6 +146,8 @@ const CustomizedPaths = props => { } > <Form + isDisabled={!isEnableRun} + isRunning={isRequiring} form={form} onFinish={onFormFinish} className={s.oltpForms} @@ -157,7 +159,8 @@ const CustomizedPaths = props => { <Form.Item label='max_depth' name='max_depth' - rules={[{required: true}, {validator: integerValidator}]} + initialValue={3} + rules={[{validator: integerValidator}]} tooltip={t('analysis.algorithm.max_depth_item.tooltip')} > <InputNumber /> diff --git a/hugegraph-hubble/hubble-fe/src/modules/algorithm/algorithmsForm/Oltp/Egonet/index.js b/hugegraph-hubble/hubble-fe/src/modules/algorithm/algorithmsForm/Oltp/Egonet/index.js index b0425b9ef..4910e2828 100644 --- a/hugegraph-hubble/hubble-fe/src/modules/algorithm/algorithmsForm/Oltp/Egonet/index.js +++ b/hugegraph-hubble/hubble-fe/src/modules/algorithm/algorithmsForm/Oltp/Egonet/index.js @@ -229,6 +229,8 @@ const Egonet = props => { } > <Form + isDisabled={!isEnableRun} + isRunning={isRequiring} form={egonetForm} onFinish={onFormFinish} onValuesChange={_.debounce(onFormValuesChange, 300)} @@ -261,7 +263,8 @@ const Egonet = props => { <Form.Item label='max_depth' name='max_depth' - rules={[{required: true}, {validator: positiveIntegerValidator}]} + initialValue={3} + rules={[{validator: positiveIntegerValidator}]} tooltip={t('analysis.algorithm.max_depth_item.tooltip')} > <InputNumber /> diff --git a/hugegraph-hubble/hubble-fe/src/modules/algorithm/algorithmsForm/Oltp/FindShortestPath/index.js b/hugegraph-hubble/hubble-fe/src/modules/algorithm/algorithmsForm/Oltp/FindShortestPath/index.js index e1228c658..85fc9c4e9 100644 --- a/hugegraph-hubble/hubble-fe/src/modules/algorithm/algorithmsForm/Oltp/FindShortestPath/index.js +++ b/hugegraph-hubble/hubble-fe/src/modules/algorithm/algorithmsForm/Oltp/FindShortestPath/index.js @@ -130,6 +130,8 @@ const FindShortestPath = props => { } > <Form + isDisabled={!isEnableRun} + isRunning={isRequiring} form={form} onFinish={onFormFinish} onValuesChange={_.debounce(onFormValuesChange, 300)} @@ -162,8 +164,9 @@ const FindShortestPath = props => { <Form.Item name='max_depth' label="max_depth" + initialValue={3} tooltip={t('analysis.algorithm.oltp.common.max_steps')} - rules={[{required: true}, {validator: positiveIntegerValidator}]} + rules={[{validator: positiveIntegerValidator}]} > <InputNumber /> </Form.Item> diff --git a/hugegraph-hubble/hubble-fe/src/modules/algorithm/algorithmsForm/Oltp/FindShortestPathWithWeight/index.js b/hugegraph-hubble/hubble-fe/src/modules/algorithm/algorithmsForm/Oltp/FindShortestPathWithWeight/index.js index acb51059e..0e44b1167 100644 --- a/hugegraph-hubble/hubble-fe/src/modules/algorithm/algorithmsForm/Oltp/FindShortestPathWithWeight/index.js +++ b/hugegraph-hubble/hubble-fe/src/modules/algorithm/algorithmsForm/Oltp/FindShortestPathWithWeight/index.js @@ -130,6 +130,8 @@ const FindShortestPathWithWeight = props => { } > <Form + isDisabled={!isEnableRun} + isRunning={isRequiring} form={form} onFinish={onFormFinish} onValuesChange={_.debounce(onFormValuesChange, 300)} diff --git a/hugegraph-hubble/hubble-fe/src/modules/algorithm/algorithmsForm/Oltp/FusiformSimilarity/index.js b/hugegraph-hubble/hubble-fe/src/modules/algorithm/algorithmsForm/Oltp/FusiformSimilarity/index.js index 387ce1477..d5f65beb8 100644 --- a/hugegraph-hubble/hubble-fe/src/modules/algorithm/algorithmsForm/Oltp/FusiformSimilarity/index.js +++ b/hugegraph-hubble/hubble-fe/src/modules/algorithm/algorithmsForm/Oltp/FusiformSimilarity/index.js @@ -129,6 +129,8 @@ const FusiformSimilarity = props => { } > <Form + isDisabled={!isEnableRun} + isRunning={isRequiring} form={form} onFinish={onFormFinish} onValuesChange={_.debounce(onFormValuesChange, 300)} diff --git a/hugegraph-hubble/hubble-fe/src/modules/algorithm/algorithmsForm/Oltp/JaccardSimilarityGet/index.js b/hugegraph-hubble/hubble-fe/src/modules/algorithm/algorithmsForm/Oltp/JaccardSimilarityGet/index.js index 0aea8f084..11ebb59a7 100644 --- a/hugegraph-hubble/hubble-fe/src/modules/algorithm/algorithmsForm/Oltp/JaccardSimilarityGet/index.js +++ b/hugegraph-hubble/hubble-fe/src/modules/algorithm/algorithmsForm/Oltp/JaccardSimilarityGet/index.js @@ -123,6 +123,8 @@ const JaccardSimilarityGet = props => { } > <Form + isDisabled={!isEnableRun} + isRunning={isRequiring} form={crosspointsForm} onFinish={onFormFinish} onValuesChange={_.debounce(onFormValuesChange, 300)} diff --git a/hugegraph-hubble/hubble-fe/src/modules/algorithm/algorithmsForm/Oltp/JaccardSimilarityPost/index.js b/hugegraph-hubble/hubble-fe/src/modules/algorithm/algorithmsForm/Oltp/JaccardSimilarityPost/index.js index 61532b41a..64625f20d 100644 --- a/hugegraph-hubble/hubble-fe/src/modules/algorithm/algorithmsForm/Oltp/JaccardSimilarityPost/index.js +++ b/hugegraph-hubble/hubble-fe/src/modules/algorithm/algorithmsForm/Oltp/JaccardSimilarityPost/index.js @@ -245,6 +245,8 @@ const JaccardSimilarityPost = props => { } > <Form + isDisabled={!isEnableRun} + isRunning={isRequiring} form={form} onFinish={onFormFinish} onValuesChange={_.debounce(onFormValuesChange, 300)} diff --git a/hugegraph-hubble/hubble-fe/src/modules/algorithm/algorithmsForm/Oltp/KneighborGet/index.js b/hugegraph-hubble/hubble-fe/src/modules/algorithm/algorithmsForm/Oltp/KneighborGet/index.js index 9ed5efee1..5aabd1a0f 100644 --- a/hugegraph-hubble/hubble-fe/src/modules/algorithm/algorithmsForm/Oltp/KneighborGet/index.js +++ b/hugegraph-hubble/hubble-fe/src/modules/algorithm/algorithmsForm/Oltp/KneighborGet/index.js @@ -125,6 +125,8 @@ const KneighborGet = props => { } > <Form + isDisabled={!isEnableRun} + isRunning={isRequiring} form={crosspointsForm} onFinish={onFormFinish} onValuesChange={_.debounce(onFormValuesChange, 300)} diff --git a/hugegraph-hubble/hubble-fe/src/modules/algorithm/algorithmsForm/Oltp/KneighborPost/index.js b/hugegraph-hubble/hubble-fe/src/modules/algorithm/algorithmsForm/Oltp/KneighborPost/index.js index 00a04ffdd..d002b8de8 100644 --- a/hugegraph-hubble/hubble-fe/src/modules/algorithm/algorithmsForm/Oltp/KneighborPost/index.js +++ b/hugegraph-hubble/hubble-fe/src/modules/algorithm/algorithmsForm/Oltp/KneighborPost/index.js @@ -257,6 +257,8 @@ const KneighborPost = props => { } > <Form + isDisabled={!isEnableRun} + isRunning={isRequiring} form={form} onFinish={onFormFinish} onValuesChange={_.debounce(onFormValuesChange, 300)} @@ -274,7 +276,8 @@ const KneighborPost = props => { <Form.Item label='max_depth' name='max_depth' - rules={[{required: true}, {validator: positiveIntegerValidator}]} + initialValue={3} + rules={[{validator: positiveIntegerValidator}]} tooltip={t('analysis.algorithm.max_depth_item.tooltip')} > <Input /> diff --git a/hugegraph-hubble/hubble-fe/src/modules/algorithm/algorithmsForm/Oltp/KoutGet/index.js b/hugegraph-hubble/hubble-fe/src/modules/algorithm/algorithmsForm/Oltp/KoutGet/index.js index 49e99b8b8..c9d19e258 100644 --- a/hugegraph-hubble/hubble-fe/src/modules/algorithm/algorithmsForm/Oltp/KoutGet/index.js +++ b/hugegraph-hubble/hubble-fe/src/modules/algorithm/algorithmsForm/Oltp/KoutGet/index.js @@ -131,6 +131,8 @@ const KoutGet = props => { } > <Form + isDisabled={!isEnableRun} + isRunning={isRequiring} form={crosspointsForm} onFinish={onFormFinish} onValuesChange={_.debounce(onFormValuesChange, 300)} diff --git a/hugegraph-hubble/hubble-fe/src/modules/algorithm/algorithmsForm/Oltp/KoutPost/Home/index.js b/hugegraph-hubble/hubble-fe/src/modules/algorithm/algorithmsForm/Oltp/KoutPost/Home/index.js index 450ca9f15..0c29c4153 100644 --- a/hugegraph-hubble/hubble-fe/src/modules/algorithm/algorithmsForm/Oltp/KoutPost/Home/index.js +++ b/hugegraph-hubble/hubble-fe/src/modules/algorithm/algorithmsForm/Oltp/KoutPost/Home/index.js @@ -155,6 +155,8 @@ const KoutPost = props => { } > <Form + isDisabled={!isEnableRun} + isRunning={isRequiring} form={form} onFinish={onFormFinish} onValuesChange={_.debounce(onFormValuesChange, 300)} @@ -172,7 +174,8 @@ const KoutPost = props => { <Form.Item label='max_depth' name='max_depth' - rules={[{required: true}, {validator: positiveIntegerValidator}]} + initialValue={3} + rules={[{validator: positiveIntegerValidator}]} tooltip={t('analysis.algorithm.form.step.max_depth')} > <Input /> diff --git a/hugegraph-hubble/hubble-fe/src/modules/algorithm/algorithmsForm/Oltp/MultiNodesShortestPath/index.js b/hugegraph-hubble/hubble-fe/src/modules/algorithm/algorithmsForm/Oltp/MultiNodesShortestPath/index.js index d3bd13c6a..ed986d229 100644 --- a/hugegraph-hubble/hubble-fe/src/modules/algorithm/algorithmsForm/Oltp/MultiNodesShortestPath/index.js +++ b/hugegraph-hubble/hubble-fe/src/modules/algorithm/algorithmsForm/Oltp/MultiNodesShortestPath/index.js @@ -229,6 +229,8 @@ const MultiNodesShortestPath = props => { } > <Form + isDisabled={!isEnableRun} + isRunning={isRequiring} form={form} onFinish={onFormFinish} onValuesChange={_.debounce(onFormValuesChange, 300)} @@ -241,8 +243,9 @@ const MultiNodesShortestPath = props => { <Form.Item name='max_depth' label="max_depth" + initialValue={3} tooltip={t('analysis.algorithm.max_depth_item.tooltip')} - rules={[{required: true}, {validator: positiveIntegerValidator}]} + rules={[{validator: positiveIntegerValidator}]} > <InputNumber /> </Form.Item> diff --git a/hugegraph-hubble/hubble-fe/src/modules/algorithm/algorithmsForm/Oltp/NeighborRankApi/index.js b/hugegraph-hubble/hubble-fe/src/modules/algorithm/algorithmsForm/Oltp/NeighborRankApi/index.js index b9fa572d2..9ebe98cc9 100644 --- a/hugegraph-hubble/hubble-fe/src/modules/algorithm/algorithmsForm/Oltp/NeighborRankApi/index.js +++ b/hugegraph-hubble/hubble-fe/src/modules/algorithm/algorithmsForm/Oltp/NeighborRankApi/index.js @@ -269,6 +269,8 @@ const NeighborRankApi = props => { } > <Form + isDisabled={!isEnableRun} + isRunning={isRequiring} form={form} onFinish={onFormFinish} onValuesChange={_.debounce(onFormValuesChange, 300)} diff --git a/hugegraph-hubble/hubble-fe/src/modules/algorithm/algorithmsForm/Oltp/Paths/index.js b/hugegraph-hubble/hubble-fe/src/modules/algorithm/algorithmsForm/Oltp/Paths/index.js index dd57b14d3..50e6de567 100644 --- a/hugegraph-hubble/hubble-fe/src/modules/algorithm/algorithmsForm/Oltp/Paths/index.js +++ b/hugegraph-hubble/hubble-fe/src/modules/algorithm/algorithmsForm/Oltp/Paths/index.js @@ -121,6 +121,8 @@ const Paths = props => { } > <Form + isDisabled={!isEnableRun} + isRunning={isRequiring} form={pathsForm} onFinish={onFormFinish} onValuesChange={_.debounce(onFormValuesChange, 300)} @@ -148,7 +150,8 @@ const Paths = props => { <Form.Item label='max_depth' name='max_depth' - rules={[{required: true}, {validator: maxDepthValidator}]} + initialValue={3} + rules={[{validator: maxDepthValidator}]} tooltip={t('analysis.algorithm.oltp.common.max_steps')} > <InputNumber /> diff --git a/hugegraph-hubble/hubble-fe/src/modules/algorithm/algorithmsForm/Oltp/RankApi/index.js b/hugegraph-hubble/hubble-fe/src/modules/algorithm/algorithmsForm/Oltp/RankApi/index.js index aa5e8cc1a..081ee6c8d 100644 --- a/hugegraph-hubble/hubble-fe/src/modules/algorithm/algorithmsForm/Oltp/RankApi/index.js +++ b/hugegraph-hubble/hubble-fe/src/modules/algorithm/algorithmsForm/Oltp/RankApi/index.js @@ -41,7 +41,7 @@ const {RANK_API} = ALGORITHM_NAME; const initialValue = { alpha: 0.85, max_degree: 10000, - max_depth: 5, + max_depth: 3, limit: 100, max_diff: 0.0001, sorted: true, @@ -137,6 +137,8 @@ const RankApi = props => { } > <Form + isDisabled={!isEnableRun} + isRunning={isRequiring} form={form} onFinish={onFormFinish} onValuesChange={_.debounce(onFormValuesChange, 300)} diff --git a/hugegraph-hubble/hubble-fe/src/modules/algorithm/algorithmsForm/Oltp/Rays/index.js b/hugegraph-hubble/hubble-fe/src/modules/algorithm/algorithmsForm/Oltp/Rays/index.js index 28c7928af..2402d4ea7 100644 --- a/hugegraph-hubble/hubble-fe/src/modules/algorithm/algorithmsForm/Oltp/Rays/index.js +++ b/hugegraph-hubble/hubble-fe/src/modules/algorithm/algorithmsForm/Oltp/Rays/index.js @@ -125,6 +125,8 @@ const Rays = props => { } > <Form + isDisabled={!isEnableRun} + isRunning={isRequiring} form={form} onFinish={onFormFinish} onValuesChange={_.debounce(onFormValuesChange, 300)} @@ -160,7 +162,8 @@ const Rays = props => { <Form.Item label='max_depth' name='max_depth' - rules={[{required: true}, {validator: positiveIntegerValidator}]} + initialValue={3} + rules={[{validator: positiveIntegerValidator}]} tooltip={t('analysis.algorithm.max_depth_item.tooltip')} > <InputNumber /> diff --git a/hugegraph-hubble/hubble-fe/src/modules/algorithm/algorithmsForm/Oltp/ResourceAllocation/index.js b/hugegraph-hubble/hubble-fe/src/modules/algorithm/algorithmsForm/Oltp/ResourceAllocation/index.js index 474354359..9a36de580 100644 --- a/hugegraph-hubble/hubble-fe/src/modules/algorithm/algorithmsForm/Oltp/ResourceAllocation/index.js +++ b/hugegraph-hubble/hubble-fe/src/modules/algorithm/algorithmsForm/Oltp/ResourceAllocation/index.js @@ -120,6 +120,8 @@ const ResourceAllocation = props => { } > <Form + isDisabled={!isEnableRun} + isRunning={isRequiring} form={resourceAllocationForm} onFinish={onFormFinish} onValuesChange={_.debounce(onFormValuesChange, 300)} diff --git a/hugegraph-hubble/hubble-fe/src/modules/algorithm/algorithmsForm/Oltp/Rings/index.js b/hugegraph-hubble/hubble-fe/src/modules/algorithm/algorithmsForm/Oltp/Rings/index.js index 07eaf1b79..98f1014e3 100644 --- a/hugegraph-hubble/hubble-fe/src/modules/algorithm/algorithmsForm/Oltp/Rings/index.js +++ b/hugegraph-hubble/hubble-fe/src/modules/algorithm/algorithmsForm/Oltp/Rings/index.js @@ -126,6 +126,8 @@ const Rings = props => { } > <Form + isDisabled={!isEnableRun} + isRunning={isRequiring} form={crosspointsForm} onFinish={onFormFinish} onValuesChange={_.debounce(onFormValuesChange, 300)} diff --git a/hugegraph-hubble/hubble-fe/src/modules/algorithm/algorithmsForm/Oltp/SameNeighbors/index.js b/hugegraph-hubble/hubble-fe/src/modules/algorithm/algorithmsForm/Oltp/SameNeighbors/index.js index e48fa0242..f8557a80d 100644 --- a/hugegraph-hubble/hubble-fe/src/modules/algorithm/algorithmsForm/Oltp/SameNeighbors/index.js +++ b/hugegraph-hubble/hubble-fe/src/modules/algorithm/algorithmsForm/Oltp/SameNeighbors/index.js @@ -125,6 +125,8 @@ const SameNeighbors = props => { } > <Form + isDisabled={!isEnableRun} + isRunning={isRequiring} form={crosspointsForm} onFinish={onFormFinish} onValuesChange={_.debounce(onFormValuesChange, 300)} diff --git a/hugegraph-hubble/hubble-fe/src/modules/algorithm/algorithmsForm/Oltp/SameNeighborsBatch/index.js b/hugegraph-hubble/hubble-fe/src/modules/algorithm/algorithmsForm/Oltp/SameNeighborsBatch/index.js index f77c45c41..88710d7a9 100644 --- a/hugegraph-hubble/hubble-fe/src/modules/algorithm/algorithmsForm/Oltp/SameNeighborsBatch/index.js +++ b/hugegraph-hubble/hubble-fe/src/modules/algorithm/algorithmsForm/Oltp/SameNeighborsBatch/index.js @@ -125,6 +125,8 @@ const SameNeighborsBatch = props => { } > <Form + isDisabled={!isEnableRun} + isRunning={isRequiring} form={form} onFinish={onFormFinish} onValuesChange={_.debounce(onFormValuesChange, 300)} diff --git a/hugegraph-hubble/hubble-fe/src/modules/algorithm/algorithmsForm/Oltp/ShortestPath/index.js b/hugegraph-hubble/hubble-fe/src/modules/algorithm/algorithmsForm/Oltp/ShortestPath/index.js index 2007c9f7b..b775c8c66 100644 --- a/hugegraph-hubble/hubble-fe/src/modules/algorithm/algorithmsForm/Oltp/ShortestPath/index.js +++ b/hugegraph-hubble/hubble-fe/src/modules/algorithm/algorithmsForm/Oltp/ShortestPath/index.js @@ -126,6 +126,8 @@ const ShortestPath = props => { } > <Form + isDisabled={!isEnableRun} + isRunning={isRequiring} form={crosspointsForm} onFinish={onFormFinish} onValuesChange={_.debounce(onFormValuesChange, 300)} diff --git a/hugegraph-hubble/hubble-fe/src/modules/algorithm/algorithmsForm/Oltp/SingleSourceShortestPath/index.js b/hugegraph-hubble/hubble-fe/src/modules/algorithm/algorithmsForm/Oltp/SingleSourceShortestPath/index.js index fb19a58df..27089399b 100644 --- a/hugegraph-hubble/hubble-fe/src/modules/algorithm/algorithmsForm/Oltp/SingleSourceShortestPath/index.js +++ b/hugegraph-hubble/hubble-fe/src/modules/algorithm/algorithmsForm/Oltp/SingleSourceShortestPath/index.js @@ -131,6 +131,8 @@ const SingleSourceShortestPath = props => { } > <Form + isDisabled={!isEnableRun} + isRunning={isRequiring} form={form} onFinish={onFormFinish} onValuesChange={_.debounce(onFormValuesChange, 300)} diff --git a/hugegraph-hubble/hubble-fe/src/modules/algorithm/algorithmsForm/Oltp/TemplatePaths/Home/index.js b/hugegraph-hubble/hubble-fe/src/modules/algorithm/algorithmsForm/Oltp/TemplatePaths/Home/index.js index a93f9f83b..de3dfe7b9 100644 --- a/hugegraph-hubble/hubble-fe/src/modules/algorithm/algorithmsForm/Oltp/TemplatePaths/Home/index.js +++ b/hugegraph-hubble/hubble-fe/src/modules/algorithm/algorithmsForm/Oltp/TemplatePaths/Home/index.js @@ -147,6 +147,8 @@ const TemplatePaths = props => { } > <Form + isDisabled={!isEnableRun} + isRunning={isRequiring} form={form} onFinish={onFormFinish} className={s.oltpForms} diff --git a/hugegraph-hubble/hubble-fe/src/modules/algorithm/algorithmsForm/PersistentForm/index.js b/hugegraph-hubble/hubble-fe/src/modules/algorithm/algorithmsForm/PersistentForm/index.js index f52569612..42c3a6fe0 100644 --- a/hugegraph-hubble/hubble-fe/src/modules/algorithm/algorithmsForm/PersistentForm/index.js +++ b/hugegraph-hubble/hubble-fe/src/modules/algorithm/algorithmsForm/PersistentForm/index.js @@ -140,6 +140,9 @@ const PersistentForm = props => { const [fallbackForm] = AntForm.useForm(); const { form = fallbackForm, + isDisabled = false, + isRunning = false, + onKeyDown, onValuesChange, ...formProps } = props; @@ -166,10 +169,27 @@ const PersistentForm = props => { [onValuesChange, storageKey] ); + const handleKeyDown = useCallback( + event => { + onKeyDown?.(event); + if (event.defaultPrevented || event.key !== 'Enter' + || (!event.metaKey && !event.ctrlKey) + || event.altKey || event.shiftKey + || event.nativeEvent?.isComposing || event.repeat + || isDisabled || isRunning) { + return; + } + event.preventDefault(); + form.submit(); + }, + [form, isDisabled, isRunning, onKeyDown] + ); + return ( <AntForm {...formProps} form={form} + onKeyDown={handleKeyDown} onValuesChange={handleValuesChange} /> ); diff --git a/hugegraph-hubble/hubble-fe/src/modules/algorithm/algorithmsForm/PersistentForm/index.test.js b/hugegraph-hubble/hubble-fe/src/modules/algorithm/algorithmsForm/PersistentForm/index.test.js index 5cc0f91ea..658db2af1 100644 --- a/hugegraph-hubble/hubble-fe/src/modules/algorithm/algorithmsForm/PersistentForm/index.test.js +++ b/hugegraph-hubble/hubble-fe/src/modules/algorithm/algorithmsForm/PersistentForm/index.test.js @@ -87,6 +87,56 @@ test('persists and restores parameters per graph and algorithm', async () => { await waitFor(() => expect(screen.getByLabelText('source')).toHaveValue('1:alice')); }); +test('submits with Cmd/Ctrl+Enter', async () => { + const onFinish = jest.fn(); + const {container} = render( + <Form onFinish={onFinish}> + <Form.Item name='source' initialValue='1:marko'> + <Input /> + </Form.Item> + </Form> + ); + const form = container.querySelector('form'); + + fireEvent.keyDown(form, {key: 'Enter', metaKey: true}); + await waitFor(() => expect(onFinish).toHaveBeenCalledTimes(1)); + + fireEvent.keyDown(form, {key: 'Enter', ctrlKey: true}); + await waitFor(() => expect(onFinish).toHaveBeenCalledTimes(2)); +}); + +test.each([ + ['plain Enter', {key: 'Enter'}], + ['Alt modifier', {key: 'Enter', metaKey: true, altKey: true}], + ['Shift modifier', {key: 'Enter', metaKey: true, shiftKey: true}], + ['composition', {key: 'Enter', metaKey: true, isComposing: true}], + ['repeat', {key: 'Enter', metaKey: true, repeat: true}], +])('ignores shortcut during %s', async (name, event) => { + const onFinish = jest.fn(); + const {container} = render(<Form onFinish={onFinish} />); + + fireEvent.keyDown(container.querySelector('form'), event); + + await Promise.resolve(); + expect(onFinish).not.toHaveBeenCalled(); +}); + +test.each([ + ['disabled', {isDisabled: true}], + ['loading', {isRunning: true}], +])('does not submit while the run action is %s', async (name, state) => { + const onFinish = jest.fn(); + const {container} = render(<Form {...state} onFinish={onFinish} />); + + fireEvent.keyDown(container.querySelector('form'), { + key: 'Enter', + metaKey: true, + }); + + await Promise.resolve(); + expect(onFinish).not.toHaveBeenCalled(); +}); + const algorithmSourceFiles = directory => fs.readdirSync(directory, {withFileTypes: true}) .flatMap(entry => { const entryPath = path.join(directory, entry.name); @@ -182,14 +232,42 @@ test('renders examples through real shared and OLAP resource field components', </GraphAnalysisContext.Provider> ); - expect(screen.getByLabelText('max_depth')) - .toHaveAttribute('placeholder', 'For example: 10'); + expect(screen.getByLabelText('max_depth')).toHaveValue('3'); expect(screen.getByLabelText('k8s.computer_cpu')) .toHaveAttribute('placeholder', 'For example: 2'); expect(screen.getByLabelText('k8s.worker_request_memory')) .toHaveAttribute('placeholder', 'For example: 1Gi'); }); +test('defaults optional max_depth parameters to 3', () => { + const algorithmRoot = path.resolve(__dirname, '..'); + const requiredFields = []; + const fieldsWithoutDefault = []; + + algorithmSourceFiles(algorithmRoot).forEach(file => { + const source = fs.readFileSync(file, 'utf8'); + const itemBlocks = source.match(/<Form\.Item\b[\s\S]*?<\/Form\.Item>/g) || []; + + itemBlocks.forEach(block => { + if (!/name\s*=\s*['"]max_depth['"]/.test(block)) { + return; + } + const isRequired = /required\s*:\s*true/.test(block); + const hasDefault = /initialValue\s*=\s*\{3\}/.test(block) + || /max_depth\s*:\s*3/.test(source); + if (isRequired) { + requiredFields.push(path.relative(algorithmRoot, file)); + } + if (!hasDefault) { + fieldsWithoutDefault.push(path.relative(algorithmRoot, file)); + } + }); + }); + + expect(requiredFields).toEqual([]); + expect(fieldsWithoutDefault).toEqual([]); +}); + test('resets an existing form when the next graph has no saved draft', async () => { const view = renderForm(); fireEvent.change(screen.getByLabelText('source'), {target: {value: '1:alice'}}); diff --git a/hugegraph-hubble/hubble-fe/src/modules/algorithm/algorithmsForm/algorithm-dom-props.test.js b/hugegraph-hubble/hubble-fe/src/modules/algorithm/algorithmsForm/algorithm-dom-props.test.js index 0edf395ad..89f7f682b 100644 --- a/hugegraph-hubble/hubble-fe/src/modules/algorithm/algorithmsForm/algorithm-dom-props.test.js +++ b/hugegraph-hubble/hubble-fe/src/modules/algorithm/algorithmsForm/algorithm-dom-props.test.js @@ -52,3 +52,17 @@ test('algorithm panels forward only the collapse control contract', () => { expect(offenders).toEqual([]); }); + +test('max_depth defaults stay optional', () => { + const offenders = collectJavaScript(__dirname).flatMap(file => { + const source = fs.readFileSync(file, 'utf8'); + const fields = source.match( + /name=['"]max_depth['"][\s\S]*?<\/Form\.Item>/gu + ) ?? []; + return fields.some(field => field.includes('required: true')) + ? [path.relative(__dirname, file)] + : []; + }); + + expect(offenders).toEqual([]); +}); diff --git a/hugegraph-hubble/hubble-fe/src/modules/asyncTasks/Detail/index.js b/hugegraph-hubble/hubble-fe/src/modules/asyncTasks/Detail/index.js index dbe631a5b..21ad80d9d 100644 --- a/hugegraph-hubble/hubble-fe/src/modules/asyncTasks/Detail/index.js +++ b/hugegraph-hubble/hubble-fe/src/modules/asyncTasks/Detail/index.js @@ -20,7 +20,8 @@ * @file 任务管理 table页面 */ -import React, {useState, useCallback, useContext} from 'react'; +import React, {useState, useCallback, useContext, useEffect} from 'react'; +import {Link} from 'react-router-dom'; import {Table, Tag, Spin, message, Button, Typography, Modal} from 'antd'; import GraphAnalysisContext from '../../Context'; import {CloseOutlined} from '@ant-design/icons'; @@ -36,16 +37,22 @@ import { import {intersection, size} from 'lodash-es'; import {format} from 'date-fns'; import {useTranslation} from 'react-i18next'; +import {AsyncTaskResultContent} from '../Result'; import c from './index.module.scss'; const {Text} = Typography; const {FAILED, SUCCESS, DELETING, CANCELLING} = Async_Taskt_Status; -const TaskAction = ({onAction, args, children}) => { +const TaskAction = ({onAction, args, children, ...buttonProps}) => { const handleClick = useCallback(() => onAction(...args), [args, onAction]); return ( - <Button type='link' style={{margin: '10px'}} onClick={handleClick}> + <Button + type='link' + style={{margin: '10px'}} + onClick={handleClick} + {...buttonProps} + > {children} </Button> ); @@ -65,6 +72,7 @@ const AsyncTaskDetail = props => { const {graphSpace: currentGraphSpace, graph: currentGraph, isVermeer} = useContext(GraphAnalysisContext); const {records: asyncManageTaskDataRecords, total: asyncManageTaskDataTotal} = asyncManageTaskData || {}; const [selectedRowKeys, setSelectedRowKeys] = useState([]); + const [expandedTaskId, setExpandedTaskId] = useState(); const { taskTypeNames, taskStatusNames, @@ -123,10 +131,27 @@ const AsyncTaskDetail = props => { return res; }; - const viewResult = useCallback( - (text, rowData, index) => { - window.open(`/asyncTasks/result/${currentGraphSpace}/${currentGraph}/${rowData.id}`); - }, [currentGraph, currentGraphSpace]); + const toggleResult = useCallback(taskId => { + setExpandedTaskId(current => ( + current === taskId ? undefined : taskId + )); + }, []); + + useEffect(() => { + setExpandedTaskId(undefined); + }, [currentGraph, currentGraphSpace, page, pageSize]); + + useEffect(() => { + if (expandedTaskId === undefined) { + return; + } + const stillVisible = asyncManageTaskDataRecords?.some( + task => task.id === expandedTaskId + ); + if (!stillVisible) { + setExpandedTaskId(undefined); + } + }, [asyncManageTaskDataRecords, expandedTaskId]); const deleteTaskByIds = useCallback( taskIdArr => { @@ -248,6 +273,7 @@ const AsyncTaskDetail = props => { const {'task_status': status, 'task_type': type, id: taskId} = rowData; const allowCheckResTypeArr = ['gremlin', 'computer-dis', 'cypher']; const isAllowCheckRes = status === SUCCESS && allowCheckResTypeArr.includes(type); + const isResultExpanded = expandedTaskId === taskId; const allowAbortStatusArr = ['scheduling', 'scheduled', 'queued', 'running', 'restoring']; const isAllowAbort = allowAbortStatusArr.includes(status); const { @@ -260,17 +286,26 @@ const AsyncTaskDetail = props => { return ( <div style={{whiteSpace: 'nowrap'}}> {status === FAILED && ( - <a - style={{margin: '10px'}} - href={`/asyncTasks/result/${currentGraphSpace}/${currentGraph}/${taskId}`} + <TaskAction + onAction={toggleResult} + args={[taskId]} + aria-expanded={isResultExpanded} + aria-controls={`async-task-result-${taskId}`} > {reason} - </a> + </TaskAction> )} {isAllowCheckRes && ( - <TaskAction onAction={viewResult} args={[result, rowData, index]}> - {resultText} + <TaskAction + onAction={toggleResult} + args={[taskId]} + aria-expanded={isResultExpanded} + aria-controls={`async-task-result-${taskId}`} + > + {isResultExpanded + ? t('analysis.async_task.action.collapse_result') + : resultText} </TaskAction> )} {!isAllowAbort && ( @@ -325,6 +360,44 @@ const AsyncTaskDetail = props => { columns={columns} dataSource={asyncManageTaskDataRecords} onChange={onPageChange} + expandable={{ + expandedRowKeys: expandedTaskId === undefined + ? [] : [expandedTaskId], + onExpand: (expanded, rowData) => { + setExpandedTaskId(expanded ? rowData.id : undefined); + }, + rowExpandable: rowData => { + const resultTypes = [ + 'gremlin', + 'computer-dis', + 'cypher', + ]; + return rowData.task_status === FAILED + || (rowData.task_status === SUCCESS + && resultTypes.includes(rowData.task_type)); + }, + expandedRowRender: rowData => ( + <div + id={`async-task-result-${rowData.id}`} + className={c.inlineResult} + > + <div className={c.inlineResultHeader}> + <strong> + {t('analysis.async_task.result_title')} + </strong> + <Link to={`/asyncTasks/result/${currentGraphSpace}/${currentGraph}/${rowData.id}`}> + {t('analysis.async_task.result_open_full')} + </Link> + </div> + <AsyncTaskResultContent + compact + graphspace={currentGraphSpace} + graph={currentGraph} + taskId={rowData.id} + /> + </div> + ), + }} pagination={{ position: ['bottomRight'], total: asyncManageTaskDataTotal, diff --git a/hugegraph-hubble/hubble-fe/src/modules/asyncTasks/Detail/index.module.scss b/hugegraph-hubble/hubble-fe/src/modules/asyncTasks/Detail/index.module.scss index 7c5f64fa7..42e2f9d9c 100644 --- a/hugegraph-hubble/hubble-fe/src/modules/asyncTasks/Detail/index.module.scss +++ b/hugegraph-hubble/hubble-fe/src/modules/asyncTasks/Detail/index.module.scss @@ -45,4 +45,16 @@ .clearSelection { color: inherit; } + + .inlineResult { + padding: var(--workbench-space-3) var(--workbench-space-4); + } + + .inlineResultHeader { + display: flex; + align-items: center; + justify-content: space-between; + gap: var(--workbench-space-3); + margin-bottom: var(--workbench-space-2); + } } diff --git a/hugegraph-hubble/hubble-fe/src/modules/asyncTasks/Detail/index.test.js b/hugegraph-hubble/hubble-fe/src/modules/asyncTasks/Detail/index.test.js new file mode 100644 index 000000000..3be9a4df0 --- /dev/null +++ b/hugegraph-hubble/hubble-fe/src/modules/asyncTasks/Detail/index.test.js @@ -0,0 +1,104 @@ +/* + * + * 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 {MemoryRouter} from 'react-router-dom'; +import GraphAnalysisContext from '../../Context'; +import AsyncTaskDetail from './index'; + +jest.mock('../../../api/index', () => ({ + analysis: { + deleteAsyncTask: jest.fn(), + abortAsyncTask: jest.fn(), + }, +})); +jest.mock('lodash-es', () => ({ + intersection: (left, right) => left.filter(value => right.includes(value)), + size: value => value.length, +})); +jest.mock('../Result', () => ({ + AsyncTaskResultContent: ({taskId}) => ( + <div data-testid="inline-result">result {taskId}</div> + ), +})); +jest.mock('react-i18next', () => ({ + useTranslation: () => ({t: key => key}), +})); + +beforeAll(() => { + window.matchMedia = window.matchMedia || (() => ({ + matches: false, + addListener: jest.fn(), + removeListener: jest.fn(), + })); +}); + +const renderDetail = () => render( + <MemoryRouter future={{v7_startTransition: true, v7_relativeSplatPath: true}}> + <GraphAnalysisContext.Provider + value={{ + graphSpace: 'DEFAULT', + graph: 'hugegraph', + isVermeer: false, + }} + > + <AsyncTaskDetail + page={1} + pageSize={10} + onPageChange={jest.fn()} + getAsynTaskList={jest.fn()} + asyncManageTaskData={{ + records: [{ + id: 4, + task_name: 'g.V().count()', + task_type: 'gremlin', + task_status: 'success', + task_create: 1787991956000, + task_update: 1787991957000, + }], + total: 1, + }} + loading={false} + /> + </GraphAnalysisContext.Provider> + </MemoryRouter> +); + +it('controls row expansion without opening a new browser tab', () => { + window.open = jest.fn(); + renderDetail(); + + const view = screen.getByRole('button', { + name: 'analysis.async_task.action.check_result', + }); + expect(view).toHaveAttribute('aria-expanded', 'false'); + fireEvent.click(view); + + expect(screen.getByRole('button', { + name: 'analysis.async_task.action.collapse_result', + })).toHaveAttribute('aria-expanded', 'true'); + expect(document.querySelector('.ant-table-row-expand-icon')) + .toHaveClass('ant-table-row-expand-icon-expanded'); + expect(window.open).not.toHaveBeenCalled(); + + fireEvent.click(screen.getByRole('button', { + name: 'analysis.async_task.action.collapse_result', + })); + expect(document.querySelector('.ant-table-row-expand-icon')) + .not.toHaveClass('ant-table-row-expand-icon-expanded'); +}); diff --git a/hugegraph-hubble/hubble-fe/src/modules/asyncTasks/Result/index.js b/hugegraph-hubble/hubble-fe/src/modules/asyncTasks/Result/index.js index 8976a133b..458c4da00 100644 --- a/hugegraph-hubble/hubble-fe/src/modules/asyncTasks/Result/index.js +++ b/hugegraph-hubble/hubble-fe/src/modules/asyncTasks/Result/index.js @@ -29,14 +29,13 @@ import ReactJsonView from 'react-json-view'; import convertStringToJSON from '../../../utils/convertStringToJSON'; import c from './index.module.scss'; -const AsyncTaskResult = () => { +export const AsyncTaskResultContent = ({ + graphspace, + graph, + taskId, + compact = false, +}) => { const {t} = useTranslation(); - const { - graphspace, - graph, - taskId, - } = useParams(); - const [asyncTaskResultJson, setAsyncTaskResultJson] = useState(); const [loading, setLoading] = useState(true); const [error, setError] = useState(false); @@ -89,6 +88,48 @@ const AsyncTaskResult = () => { && asyncTaskResultJson !== null && asyncTaskResultJson !== 'null'; + return ( + <div className={compact ? c.inlineResultBody : c.resultBody}> + {error && ( + <Alert + showIcon + type='error' + message={t('analysis.async_task.result_load_failed')} + action={( + <Button size='small' onClick={getResult}> + {t('analysis.async_task.retry_result')} + </Button> + )} + /> + )} + {loading && <Spin tip={t('analysis.async_task.result_loading')} />} + {!loading && !error && ( + !hasResult ? ( + <Empty description={t('analysis.async_task.no_result')} /> + ) : resultForJSON === null ? ( + asyncTaskResultJson + ) : ( + <ReactJsonView + src={resultForJSON} + name={false} + displayObjectSize={false} + displayDataTypes={false} + groupArraysAfterLength={50} + /> + ) + )} + </div> + ); +}; + +const AsyncTaskResult = () => { + const {t} = useTranslation(); + const { + graphspace, + graph, + taskId, + } = useParams(); + return ( <div className={c.pageCanvas}> <section className={c.asyncTaskResult}> @@ -107,36 +148,11 @@ const AsyncTaskResult = () => { {t('analysis.async_task.result_back')} </Link> </header> - <div className={c.resultBody}> - {error && ( - <Alert - showIcon - type='error' - message={t('analysis.async_task.result_load_failed')} - action={( - <Button size='small' onClick={getResult}> - {t('analysis.async_task.retry_result')} - </Button> - )} - /> - )} - {loading && <Spin tip={t('analysis.async_task.result_loading')} />} - {!loading && !error && ( - !hasResult ? ( - <Empty description={t('analysis.async_task.no_result')} /> - ) : resultForJSON === null ? ( - asyncTaskResultJson - ) : ( - <ReactJsonView - src={resultForJSON} - name={false} - displayObjectSize={false} - displayDataTypes={false} - groupArraysAfterLength={50} - /> - ) - )} - </div> + <AsyncTaskResultContent + graphspace={graphspace} + graph={graph} + taskId={taskId} + /> </section> </div> ); diff --git a/hugegraph-hubble/hubble-fe/src/modules/asyncTasks/Result/index.module.scss b/hugegraph-hubble/hubble-fe/src/modules/asyncTasks/Result/index.module.scss index 59d40ac52..f98bb0cf3 100644 --- a/hugegraph-hubble/hubble-fe/src/modules/asyncTasks/Result/index.module.scss +++ b/hugegraph-hubble/hubble-fe/src/modules/asyncTasks/Result/index.module.scss @@ -68,6 +68,18 @@ overflow-wrap: anywhere; } +.inlineResultBody { + max-height: 420px; + min-height: 112px; + padding: var(--workbench-space-4); + overflow: auto; + border: 1px solid var(--workbench-color-border); + border-radius: var(--workbench-radius-lg); + background: var(--workbench-color-surface); + white-space: pre-wrap; + overflow-wrap: anywhere; +} + @media (max-width: 900px) { .resultHeader { flex-direction: column; diff --git a/hugegraph-hubble/hubble-fe/src/pages/Account/EditLayer.js b/hugegraph-hubble/hubble-fe/src/pages/Account/EditLayer.js index 13616c29e..b22718677 100644 --- a/hugegraph-hubble/hubble-fe/src/pages/Account/EditLayer.js +++ b/hugegraph-hubble/hubble-fe/src/pages/Account/EditLayer.js @@ -88,8 +88,9 @@ const EditLayer = ({ onCancel(); refresh(); onCreated?.({ + user_id: res.data?.id, user_name: values.user_name, - is_superadmin: values.is_superadmin, + is_superadmin: Boolean(values.is_superadmin), }); return; } diff --git a/hugegraph-hubble/hubble-fe/src/pages/Account/account-edit-recovery.test.js b/hugegraph-hubble/hubble-fe/src/pages/Account/account-edit-recovery.test.js index c5ecf6510..530c7465c 100644 --- a/hugegraph-hubble/hubble-fe/src/pages/Account/account-edit-recovery.test.js +++ b/hugegraph-hubble/hubble-fe/src/pages/Account/account-edit-recovery.test.js @@ -85,8 +85,19 @@ beforeEach(() => { test('creates a non-elevated account when preset API is unavailable', async () => { mockAuthContext = {capabilities: ['accounts_manage']}; - api.auth.addUser.mockResolvedValue({status: 200}); - render(<EditLayer {...props} data={{}} op='create' />); + const onCreated = jest.fn(); + api.auth.addUser.mockResolvedValue({ + status: 200, + data: {id: 'user-42'}, + }); + render( + <EditLayer + {...props} + data={{}} + op='create' + onCreated={onCreated} + /> + ); await act(async () => undefined); expect(screen.queryByText('account.form.permission_preset')) @@ -111,6 +122,11 @@ test('creates a non-elevated account when preset API is unavailable', expect(payload).not.toHaveProperty('graphspace_permissions'); expect(payload).not.toHaveProperty('adminSpaces'); expect(payload).not.toHaveProperty('is_superadmin'); + expect(onCreated).toHaveBeenCalledWith({ + user_id: 'user-42', + user_name: 'alice', + is_superadmin: false, + }); }); test('keeps global super administrator identity in account creation', async () => {
