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 9c86974c84f9b2038cb9be6d74aa63ad15de75f4
Author: dark <[email protected]>
AuthorDate: Sat Aug 29 23:45:52 2026 +0800

    fix(hubble): close final auth and Store gaps
    
    - clear account identity when entering anonymous mode
    - mark deadline-cancelled Stores down
    - cover storage cleanup and fanout cancellation
---
 .github/workflows/hubble-ci.yml                    |  1 +
 hugegraph-hubble/AGENTS.md                         | 32 +++--------
 .../hugegraph/controller/BaseController.java       |  9 ++-
 .../hugegraph/controller/ConfigController.java     |  6 +-
 .../hugegraph/controller/auth/UserController.java  |  5 ++
 .../controller/graphs/GraphsController.java        |  2 +-
 .../service/op/DefaultOperationsDataService.java   |  6 +-
 .../service/op/LiveOperationsCollector.java        | 59 +++++++++++++++-----
 .../service/op/OperationsPayloadParser.java        | 18 ++++--
 .../auth/AccountMutationAuthorizationTest.java     | 17 ++++++
 .../op/DefaultOperationsDataServiceTest.java       | 37 +++++++++++++
 .../service/op/LiveOperationsCollectorTest.java    | 64 +++++++++++++++++++++-
 .../service/op/OperationsPayloadParserTest.java    | 19 +++++++
 .../hugegraph/unit/ConfigControllerTest.java       |  4 +-
 .../unit/GraphsControllerCanonicalTest.java        |  4 +-
 hugegraph-hubble/hubble-fe/src/App.js              | 13 +++++
 hugegraph-hubble/hubble-fe/src/App.test.js         |  4 ++
 .../hubble-fe/src/pages/Account/EditLayer.js       | 34 ++++++++----
 .../pages/Account/account-edit-recovery.test.js    | 42 ++++++++++++++
 .../src/pages/Account/account-recovery.test.js     | 28 +++++++++-
 .../hubble-fe/src/pages/Account/index.js           |  4 +-
 21 files changed, 338 insertions(+), 70 deletions(-)

diff --git a/.github/workflows/hubble-ci.yml b/.github/workflows/hubble-ci.yml
index db2ec0ba2..8a753b01e 100644
--- a/.github/workflows/hubble-ci.yml
+++ b/.github/workflows/hubble-ci.yml
@@ -112,6 +112,7 @@ jobs:
           yarn install --frozen-lockfile --network-timeout 600000 
--prefer-offline --ignore-engines
           yarn lint
           yarn i18n:check
+          yarn test --watchAll=false --runInBand
           node --test ../hubble-dist/assembly/travis/ui_auth.test.js
 
       - name: Install Playwright Chromium
diff --git a/hugegraph-hubble/AGENTS.md b/hugegraph-hubble/AGENTS.md
index 4793edafc..e47b828c2 100644
--- a/hugegraph-hubble/AGENTS.md
+++ b/hugegraph-hubble/AGENTS.md
@@ -2,38 +2,20 @@
 
 ## Authentication and connection boundary
 
-The `1.8/master` path is the source of truth. The backend detects 
authentication
-mode from HugeGraph Server and uses one connection resolver. The resolver
-chooses either a direct server URL or an address discovered from PD; callers
-must not reimplement `usePD` or infer connection state from page-local flags.
-In PD mode the server address returned by discovery is authoritative, so a
-manual server URL is not required.
-
-Use the unauthenticated HugeGraph client for anonymous mode. Do not manufacture
-an empty token or an administrator session. Anonymous mode has no account
-context and account/permission routes are hidden or rejected at the capability
-boundary.
+The `1.8/master` path is the source of truth. The backend detects 
authentication mode from HugeGraph Server and uses one connection resolver. The 
resolver chooses either a direct server URL or an address discovered from PD; 
callers must not reimplement `usePD` or infer connection state from page-local 
flags. In PD mode the server address returned by discovery is authoritative, so 
a manual server URL is not required.
+
+Use the unauthenticated HugeGraph client for anonymous mode. Do not 
manufacture an empty token or an administrator session. Anonymous mode has no 
account context and account/permission routes are hidden or rejected at the 
capability boundary.
 
 ## Compatibility policy
 
 Compatibility is intentionally one-way:
 
 - `1.8/master`: modern GraphSpace/auth contracts and the complete UI.
-- `1.7`: thin fallback for the legacy response shape; keep the core workflow
-  usable without adding version branches to controllers or React pages.
-- `1.5` standalone: core graph/schema/data operations only. GraphSpace
-  management is unsupported and should degrade with an explicit capability
-  response. Do not add a PD variant for 1.5.
+- `1.7`: thin fallback for the legacy response shape; keep the core workflow 
usable without adding version branches to controllers or React pages.
+- `1.5` standalone: core graph/schema/data operations only. GraphSpace 
management is unsupported and should degrade with an explicit capability 
response. Do not add a PD variant for 1.5.
 
-Version checks belong in the client compatibility adapter and connection
-resolver. New code should consume capabilities, not compare literal versions.
-When an old image cannot satisfy a capability, mark the test as `needs input`
-or `skipped` with the exact image tag and reason.
+Version checks belong in the client compatibility adapter and connection 
resolver. New code should consume capabilities, not compare literal versions. 
When an old image cannot satisfy a capability, mark the test as `needs input` 
or `skipped` with the exact image tag and reason.
 
 ## Verification
 
-For UI changes, use Chrome to exercise login/non-auth mode, connection
-switching, and account/GraphSpace visibility. Static inspection and unit tests
-are not a substitute for this interaction check. Keep screenshots collected
-from the running UI in the documentation assets referenced by
-`README.md`.
+For UI changes, use Chrome to exercise login/non-auth mode, connection 
switching, and account/GraphSpace visibility. Static inspection and unit tests 
are not a substitute for this interaction check. Keep screenshots collected 
from the running UI in the documentation assets referenced by `README.md`.
diff --git 
a/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/controller/BaseController.java
 
b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/controller/BaseController.java
index a410c7d55..eec2c0e70 100644
--- 
a/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/controller/BaseController.java
+++ 
b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/controller/BaseController.java
@@ -191,11 +191,16 @@ public abstract class BaseController {
 
     protected HugeClient requireGraphSpaceWrite(String graphSpace) {
         HugeClient client = this.authClient(null, null);
+        this.requireGraphSpaceWrite(client, graphSpace);
+        client.assignGraph(graphSpace, null);
+        return client;
+    }
+
+    protected void requireGraphSpaceWrite(HugeClient client,
+                                          String graphSpace) {
         this.requireGraphSpaceAccess(client, graphSpace);
         this.authContextService.requireGraphSpaceWrite(
                 client, this.getUser(), graphSpace);
-        client.assignGraph(graphSpace, null);
-        return client;
     }
 
     protected HugeClient requireGraphSpaceAuthorizationAdmin(
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 6c212acfe..2793fbb6a 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
@@ -61,8 +61,8 @@ public class ConfigController {
         if (pdEnabled) {
             capabilities.put("server_capabilities_verified", false);
             capabilities.put("auth_enabled", true);
-            capabilities.put("graph_create_enabled", true);
-            capabilities.put("cypher_enabled", true);
+            capabilities.put("graph_create_enabled", false);
+            capabilities.put("cypher_enabled", false);
             if (hugeClientPoolService == null) {
                 return capabilities;
             }
@@ -70,6 +70,8 @@ public class ConfigController {
                 capabilities.put("auth_enabled",
                                  this.authModeService.update(
                                  client.isServerAuthEnabled()));
+                capabilities.put("graph_create_enabled", true);
+                capabilities.put("cypher_enabled", true);
                 capabilities.put("server_capabilities_verified", true);
                 return capabilities;
             } catch (RuntimeException ignored) {
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 fd4d7b1b7..0c5aa4cd1 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
@@ -115,6 +115,11 @@ public class UserController extends BaseController {
         }
         this.checkAccountGrantScope(client, current.getName(), userEntity);
         userService.update(client, userEntity);
+        if (Objects.equals(this.getUser(), current.getName()) &&
+            userEntity.getPassword() != null &&
+            !userEntity.getPassword().isEmpty()) {
+            this.clearAuthSession();
+        }
     }
 
     @DeleteMapping("{id}")
diff --git 
a/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/controller/graphs/GraphsController.java
 
b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/controller/graphs/GraphsController.java
index 8e2bef18f..0554ba625 100644
--- 
a/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/controller/graphs/GraphsController.java
+++ 
b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/controller/graphs/GraphsController.java
@@ -337,7 +337,7 @@ public class GraphsController extends BaseController {
         String targetGraphSpace = graphCloneEntity.getGraphSpace() == null ?
                                   graphspace :
                                   graphCloneEntity.getGraphSpace();
-        this.requireGraphSpaceAccess(client, targetGraphSpace);
+        this.requireGraphSpaceWrite(client, targetGraphSpace);
         return this.graphsService.clone(
                 client, graphCloneEntity.convertMap(graphspace, graph));
     }
diff --git 
a/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/service/op/DefaultOperationsDataService.java
 
b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/service/op/DefaultOperationsDataService.java
index b95fca4db..180394eb0 100644
--- 
a/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/service/op/DefaultOperationsDataService.java
+++ 
b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/service/op/DefaultOperationsDataService.java
@@ -51,10 +51,10 @@ import 
org.apache.hugegraph.service.op.OperationsModels.SourceStatus;
 public class DefaultOperationsDataService implements OperationsDataService {
 
     private static final Set<String> PD_FACTS = Set.of(
-            "graphs", "partitions", "replicas", "stores", "stores_up",
-            "data_size_bytes");
+            "graphs", "partitions", "replicas", "data_size_bytes");
     private static final Set<String> STORE_FACTS = Set.of(
-            "capacity_total_bytes", "capacity_used_bytes");
+            "stores", "stores_up", "capacity_total_bytes",
+            "capacity_used_bytes");
 
     private final OperationsCollector collector;
     private final long ttlMillis;
diff --git 
a/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/service/op/LiveOperationsCollector.java
 
b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/service/op/LiveOperationsCollector.java
index 5d2dd9dc4..a633f9fe7 100644
--- 
a/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/service/op/LiveOperationsCollector.java
+++ 
b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/service/op/LiveOperationsCollector.java
@@ -38,6 +38,7 @@ import java.util.concurrent.ExecutorService;
 import java.util.concurrent.Executors;
 import java.util.concurrent.Future;
 import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
 import java.util.concurrent.atomic.AtomicInteger;
 import java.util.stream.Collectors;
 
@@ -210,7 +211,14 @@ public class LiveOperationsCollector implements 
OperationsCollector {
     private void collectServer(HugeClient client, boolean includeMetrics,
                                long now, Map<String, SourceStatus> sources,
                                List<Node> nodes) {
-        List<String> urls = this.discoveredServerURLs();
+        long started = System.nanoTime();
+        List<String> urls;
+        try {
+            urls = this.discoveredServerURLs();
+        } catch (RuntimeException e) {
+            sources.put("server", unavailable(metricReason(e), now));
+            return;
+        }
         if (urls.isEmpty()) {
             this.collectSingleServer(client, this.serverIdentity,
                                      "HugeGraph Server", includeMetrics, now,
@@ -222,6 +230,10 @@ public class LiveOperationsCollector implements 
OperationsCollector {
         String reason = null;
         String authContext = client.getAuthContext();
         List<Future<ServerResult>> futures;
+        long elapsedMillis = TimeUnit.NANOSECONDS.toMillis(
+                             System.nanoTime() - started);
+        long remainingMillis = Math.max(
+                               1L, this.storeDeadlineMillis - elapsedMillis);
         try {
             futures = this.storeExecutor.invokeAll(
                     urls.stream().map(url ->
@@ -229,7 +241,7 @@ public class LiveOperationsCollector implements 
OperationsCollector {
                             this.collectDiscoveredServer(
                                  url, authContext, includeMetrics, now))
                         .collect(Collectors.toList()),
-                    this.storeDeadlineMillis, TimeUnit.MILLISECONDS);
+                    remainingMillis, TimeUnit.MILLISECONDS);
         } catch (InterruptedException e) {
             Thread.currentThread().interrupt();
             sources.put("server", unavailable("upstream_interrupted", now));
@@ -292,10 +304,25 @@ public class LiveOperationsCollector implements 
OperationsCollector {
         if (!this.pdEnabled || this.serverClients == null) {
             return Collections.emptyList();
         }
-        return this.serverClients.urls().stream()
-                .filter(url -> url != null && !url.trim().isEmpty())
-                .distinct()
-                .collect(Collectors.toList());
+        Future<List<String>> discovery = this.storeExecutor.submit(() ->
+                this.serverClients.urls().stream()
+                    .filter(url -> url != null && !url.trim().isEmpty())
+                    .distinct()
+                    .collect(Collectors.toList()));
+        try {
+            return discovery.get(this.storeDeadlineMillis,
+                                 TimeUnit.MILLISECONDS);
+        } catch (TimeoutException e) {
+            discovery.cancel(true);
+            throw new UpstreamRequestException("upstream_deadline", e);
+        } catch (InterruptedException e) {
+            Thread.currentThread().interrupt();
+            discovery.cancel(true);
+            throw new UpstreamRequestException("upstream_interrupted", e);
+        } catch (ExecutionException e) {
+            throw new UpstreamRequestException("upstream_unavailable",
+                                               e.getCause());
+        }
     }
 
     private static ServerClientProvider serverClients(
@@ -659,7 +686,12 @@ public class LiveOperationsCollector implements 
OperationsCollector {
                     result = StoreMetricResult.failure(
                              job, "upstream_unavailable", now);
                 }
-                nodes.set(job.getNodeIndex(), result.getNode());
+                Node node = result.getNode();
+                if (result.getSuccessfulGroups() == 0 &&
+                    directStoreFailure(result.getFailureReason())) {
+                    node = copyNodeWithStatus(node, "DOWN");
+                }
+                nodes.set(job.getNodeIndex(), node);
                 successfulGroups += result.getSuccessfulGroups();
                 if (result.getFailureReason() != null) {
                     partial = true;
@@ -714,11 +746,8 @@ public class LiveOperationsCollector implements 
OperationsCollector {
                 statuses.put(group, metricStatus(e, now, true));
             }
         }
-        Node node = copyNode(job.getNode(), metrics, statuses);
-        if (successfulGroups == 0 && directStoreFailure(failureReason)) {
-            node = copyNodeWithStatus(node, "DOWN");
-        }
-        return new StoreMetricResult(node, successfulGroups, failureReason);
+        return new StoreMetricResult(copyNode(job.getNode(), metrics, 
statuses),
+                                     successfulGroups, failureReason);
     }
 
     private static boolean directStoreFailure(String reason) {
@@ -988,8 +1017,10 @@ public class LiveOperationsCollector implements 
OperationsCollector {
             return "malformed_response";
         }
         String message = error.getMessage();
-        if ("upstream_timeout".equals(message)) {
-            return "upstream_timeout";
+        if ("upstream_timeout".equals(message) ||
+            "upstream_deadline".equals(message) ||
+            "upstream_interrupted".equals(message)) {
+            return message;
         }
         if (message != null && message.startsWith("upstream_http_status_")) {
             return "upstream_rejected";
diff --git 
a/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/service/op/OperationsPayloadParser.java
 
b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/service/op/OperationsPayloadParser.java
index 5d809fb41..663b805ad 100644
--- 
a/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/service/op/OperationsPayloadParser.java
+++ 
b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/service/op/OperationsPayloadParser.java
@@ -219,15 +219,15 @@ public class OperationsPayloadParser {
             if (value == null) {
                 continue;
             }
-            if (line.startsWith("process_uptime_seconds{")) {
+            if (prometheusMetric(line, "process_uptime_seconds")) {
                 result.put("uptime_seconds", value);
-            } else if (line.startsWith("system_cpu_count{")) {
+            } else if (prometheusMetric(line, "system_cpu_count")) {
                 result.put("cpu_count", value);
-            } else if (line.startsWith("jvm_threads_live_threads{")) {
+            } else if (prometheusMetric(line, "jvm_threads_live_threads")) {
                 result.put("threads_live", value);
-            } else if (line.startsWith("process_cpu_usage{")) {
+            } else if (prometheusMetric(line, "process_cpu_usage")) {
                 result.put("process_cpu_usage", value);
-            } else if (line.startsWith("system_cpu_usage{")) {
+            } else if (prometheusMetric(line, "system_cpu_usage")) {
                 result.put("system_cpu_usage", value);
             } else if (line.startsWith("jvm_memory_used_bytes{") &&
                        line.contains("area=\"heap\"")) {
@@ -251,6 +251,14 @@ public class OperationsPayloadParser {
         return result;
     }
 
+    private static boolean prometheusMetric(String line, String name) {
+        if (!line.startsWith(name) || line.length() == name.length()) {
+            return false;
+        }
+        char boundary = line.charAt(name.length());
+        return boundary == '{' || Character.isWhitespace(boundary);
+    }
+
     private JsonNode data(String payload, String source) {
         try {
             JsonNode root = this.mapper.readTree(payload);
diff --git 
a/hugegraph-hubble/hubble-be/src/test/java/org/apache/hugegraph/controller/auth/AccountMutationAuthorizationTest.java
 
b/hugegraph-hubble/hubble-be/src/test/java/org/apache/hugegraph/controller/auth/AccountMutationAuthorizationTest.java
index 412edbfcc..3cb79e77e 100644
--- 
a/hugegraph-hubble/hubble-be/src/test/java/org/apache/hugegraph/controller/auth/AccountMutationAuthorizationTest.java
+++ 
b/hugegraph-hubble/hubble-be/src/test/java/org/apache/hugegraph/controller/auth/AccountMutationAuthorizationTest.java
@@ -323,6 +323,23 @@ public class AccountMutationAuthorizationTest {
                                       captor.getValue().getId());
     }
 
+    @Test
+    public void testEditingOwnPasswordClearsCachedSessionCredentials() {
+        TestUserController controller = accountController("admin", "ADMIN");
+        UserEntity current = account("canonical-id", "admin", true);
+        Mockito.when(this.authorizationService.get(this.client,
+                                                   "canonical-id"))
+               .thenReturn(current);
+        UserEntity update = new UserEntity();
+        update.setPassword("new-password");
+
+        controller.update("canonical-id", update);
+
+        Mockito.verify(this.authorizationService)
+               .update(this.client, update);
+        Assert.assertTrue(controller.authSessionCleared());
+    }
+
     @Test
     public void testDeleteUsesFetchedCanonicalUserId() {
         TestUserController controller = accountController("admin", "ADMIN");
diff --git 
a/hugegraph-hubble/hubble-be/src/test/java/org/apache/hugegraph/service/op/DefaultOperationsDataServiceTest.java
 
b/hugegraph-hubble/hubble-be/src/test/java/org/apache/hugegraph/service/op/DefaultOperationsDataServiceTest.java
index 1b7825418..f20cbf613 100644
--- 
a/hugegraph-hubble/hubble-be/src/test/java/org/apache/hugegraph/service/op/DefaultOperationsDataServiceTest.java
+++ 
b/hugegraph-hubble/hubble-be/src/test/java/org/apache/hugegraph/service/op/DefaultOperationsDataServiceTest.java
@@ -273,6 +273,29 @@ public class DefaultOperationsDataServiceTest {
                             facts.get("capacity_total_bytes"));
     }
 
+    @SuppressWarnings("unchecked")
+    @Test
+    public void testPdFailureKeepsFreshStoreCounts() {
+        AtomicInteger calls = new AtomicInteger();
+        OperationsCollector collector = (client, metrics) ->
+                calls.getAndIncrement() == 0 ?
+                factSnapshot("AVAILABLE", "AVAILABLE", 100L, 1000L, 3L, 3L) :
+                factSnapshot("UNAVAILABLE", "AVAILABLE", null, 2000L, 2L, 2L);
+        DefaultOperationsDataService service = new 
DefaultOperationsDataService(
+                collector, 5, CLOCK);
+        Set<String> capabilities = Set.of(
+                OperationsCapabilityService.HEALTH_READ,
+                OperationsCapabilityService.TOPOLOGY_READ);
+
+        service.overview(client("token-a"), capabilities, false);
+        Map<String, Object> result = service.overview(client("token-a"),
+                                                      capabilities, true);
+
+        Map<String, Long> facts = (Map<String, Long>) result.get("facts");
+        Assert.assertEquals(Long.valueOf(2L), facts.get("stores"));
+        Assert.assertEquals(Long.valueOf(2L), facts.get("stores_up"));
+    }
+
     @SuppressWarnings("unchecked")
     @Test
     public void testStoreFailureKeepsFreshPdFacts() {
@@ -552,6 +575,14 @@ public class DefaultOperationsDataServiceTest {
     private static Snapshot factSnapshot(String pdAvailability,
                                          String storesAvailability,
                                          Long dataSize, Long capacity) {
+        return factSnapshot(pdAvailability, storesAvailability, dataSize,
+                            capacity, 3L, 3L);
+    }
+
+    private static Snapshot factSnapshot(String pdAvailability,
+                                         String storesAvailability,
+                                         Long dataSize, Long capacity,
+                                         Long stores, Long storesUp) {
         Map<String, SourceStatus> sources = new LinkedHashMap<>();
         sources.put("pd", factSource(pdAvailability));
         sources.put("stores", factSource(storesAvailability));
@@ -563,6 +594,12 @@ public class DefaultOperationsDataServiceTest {
             facts.put("capacity_total_bytes", capacity);
             facts.put("capacity_used_bytes", capacity / 2L);
         }
+        if (stores != null) {
+            facts.put("stores", stores);
+        }
+        if (storesUp != null) {
+            facts.put("stores_up", storesUp);
+        }
         String status = "AVAILABLE".equals(pdAvailability) &&
                         "AVAILABLE".equals(storesAvailability) ?
                         "UP" : "DEGRADED";
diff --git 
a/hugegraph-hubble/hubble-be/src/test/java/org/apache/hugegraph/service/op/LiveOperationsCollectorTest.java
 
b/hugegraph-hubble/hubble-be/src/test/java/org/apache/hugegraph/service/op/LiveOperationsCollectorTest.java
index 9869b963e..b075a8507 100644
--- 
a/hugegraph-hubble/hubble-be/src/test/java/org/apache/hugegraph/service/op/LiveOperationsCollectorTest.java
+++ 
b/hugegraph-hubble/hubble-be/src/test/java/org/apache/hugegraph/service/op/LiveOperationsCollectorTest.java
@@ -122,6 +122,49 @@ public class LiveOperationsCollectorTest {
                                     .getAvailability());
     }
 
+    @Test
+    public void testPdServerDiscoveryUsesOperationsDeadline()
+           throws IOException {
+        HttpServer pd = pdServer(200, cluster(), 200, stores());
+        LiveOperationsCollector.ServerClientProvider servers =
+                new LiveOperationsCollector.ServerClientProvider() {
+            @Override
+            public java.util.List<String> urls() {
+                try {
+                    Thread.sleep(10000L);
+                } catch (InterruptedException e) {
+                    Thread.currentThread().interrupt();
+                    throw new IllegalStateException(e);
+                }
+                return Collections.emptyList();
+            }
+
+            @Override
+            public HugeClient create(String url, String authContext,
+                                     int timeout) {
+                throw new AssertionError("Server probe must not start");
+            }
+        };
+        LiveOperationsCollector collector = collector(true, pd, servers, 50);
+
+        long started = System.nanoTime();
+        Snapshot snapshot;
+        try {
+            snapshot = collector.collect(serverClient(), false);
+        } finally {
+            collector.close();
+            pd.stop(0);
+        }
+        long elapsed = TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - 
started);
+
+        Assert.assertTrue(elapsed < 3000L);
+        Assert.assertEquals("UNAVAILABLE",
+                            snapshot.getSources().get("server")
+                                    .getAvailability());
+        Assert.assertEquals("upstream_deadline",
+                            snapshot.getSources().get("server").getReason());
+    }
+
     @Test
     public void testNonPdModeIsDownWhenOnlySupportedSourceFails() {
         Snapshot snapshot = collector(false, null).collect(
@@ -821,6 +864,10 @@ public class LiveOperationsCollectorTest {
                                     .getAvailability());
         Assert.assertEquals("upstream_deadline",
                             snapshot.getSources().get("stores").getReason());
+        Assert.assertEquals("DOWN",
+                            snapshot.getSources().get("stores").getStatus());
+        Assert.assertEquals(Long.valueOf(0L),
+                            snapshot.getFacts().get("stores_up"));
         Assert.assertEquals(0, http.activeRequests());
     }
 
@@ -850,6 +897,14 @@ public class LiveOperationsCollectorTest {
                 .filter(node -> "upstream_deadline".equals(
                         node.getMetricStatuses().get("system").getReason()))
                 .count());
+        Assert.assertEquals(1L, snapshot.getNodes().stream()
+                .filter(node -> "STORE".equals(node.getType()))
+                .filter(node -> "DOWN".equals(node.getStatus()))
+                .count());
+        Assert.assertEquals("DEGRADED",
+                            snapshot.getSources().get("stores").getStatus());
+        Assert.assertEquals(Long.valueOf(1L),
+                            snapshot.getFacts().get("stores_up"));
     }
 
     @Test
@@ -894,13 +949,20 @@ public class LiveOperationsCollectorTest {
     private static LiveOperationsCollector collector(
             boolean pdEnabled, HttpServer pd,
             LiveOperationsCollector.ServerClientProvider servers) {
+        return collector(pdEnabled, pd, servers, 5000);
+    }
+
+    private static LiveOperationsCollector collector(
+            boolean pdEnabled, HttpServer pd,
+            LiveOperationsCollector.ServerClientProvider servers,
+            int deadlineMillis) {
         String pdBase = "http://127.0.0.1:"; + pd.getAddress().getPort();
         return new LiveOperationsCollector(
                 pdEnabled, pdBase, "hubble", "secret", "store-hubble",
                 "store-secret", "server-under-test",
                 new OperationsHttpClient(1000, 1000, 8192),
                 new OperationsPayloadParser(new ObjectMapper()), CLOCK,
-                16, 5000, Collections.singleton(pdBase), servers);
+                16, deadlineMillis, Collections.singleton(pdBase), servers);
     }
 
     private static LiveOperationsCollector collector(RecordingHttpClient http,
diff --git 
a/hugegraph-hubble/hubble-be/src/test/java/org/apache/hugegraph/service/op/OperationsPayloadParserTest.java
 
b/hugegraph-hubble/hubble-be/src/test/java/org/apache/hugegraph/service/op/OperationsPayloadParserTest.java
index 6de3107b1..e5bb3ea72 100644
--- 
a/hugegraph-hubble/hubble-be/src/test/java/org/apache/hugegraph/service/op/OperationsPayloadParserTest.java
+++ 
b/hugegraph-hubble/hubble-be/src/test/java/org/apache/hugegraph/service/op/OperationsPayloadParserTest.java
@@ -262,6 +262,25 @@ public class OperationsPayloadParserTest {
         Assert.assertFalse(metrics.toString().contains("secret"));
     }
 
+    @Test
+    public void testParsesUnlabelledPdPrometheusMetrics() {
+        OperationsPayloadParser parser = new OperationsPayloadParser(MAPPER);
+        String payload = "process_uptime_seconds 12\n" +
+                         "system_cpu_count 2\n" +
+                         "jvm_threads_live_threads 8\n" +
+                         "process_cpu_usage 0.1\n" +
+                         "system_cpu_usage 0.2\n" +
+                         "process_uptime_seconds_total 99\n";
+
+        Map<String, Object> metrics = parser.parsePdPrometheusMetrics(payload);
+
+        Assert.assertEquals(12D, metrics.get("uptime_seconds"));
+        Assert.assertEquals(2D, metrics.get("cpu_count"));
+        Assert.assertEquals(8D, metrics.get("threads_live"));
+        Assert.assertEquals(0.1D, metrics.get("process_cpu_usage"));
+        Assert.assertEquals(0.2D, metrics.get("system_cpu_usage"));
+    }
+
     @Test(expected = MalformedUpstreamException.class)
     public void testRejectsPrometheusWithoutRecognizedMetrics() {
         OperationsPayloadParser parser = new OperationsPayloadParser(MAPPER);
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 07e528fb1..3e0e235ad 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
@@ -85,8 +85,8 @@ public class ConfigControllerTest {
         Assert.assertEquals(Map.of("pd_enabled", true,
                                    "server_capabilities_verified", false,
                                    "auth_enabled", true,
-                                   "graph_create_enabled", true,
-                                   "cypher_enabled", true), result);
+                                   "graph_create_enabled", false,
+                                   "cypher_enabled", false), result);
     }
 
     @Test
diff --git 
a/hugegraph-hubble/hubble-be/src/test/java/org/apache/hugegraph/unit/GraphsControllerCanonicalTest.java
 
b/hugegraph-hubble/hubble-be/src/test/java/org/apache/hugegraph/unit/GraphsControllerCanonicalTest.java
index ca0f4f05e..b558e94e7 100644
--- 
a/hugegraph-hubble/hubble-be/src/test/java/org/apache/hugegraph/unit/GraphsControllerCanonicalTest.java
+++ 
b/hugegraph-hubble/hubble-be/src/test/java/org/apache/hugegraph/unit/GraphsControllerCanonicalTest.java
@@ -271,8 +271,8 @@ public class GraphsControllerCanonicalTest {
         }
 
         @Override
-        protected void requireGraphSpaceAccess(HugeClient client,
-                                               String graphSpace) {
+        protected void requireGraphSpaceWrite(HugeClient client,
+                                              String graphSpace) {
             this.checkedGraphSpace = graphSpace;
         }
     }
diff --git a/hugegraph-hubble/hubble-fe/src/App.js 
b/hugegraph-hubble/hubble-fe/src/App.js
index 70dee756e..2043e1955 100644
--- a/hugegraph-hubble/hubble-fe/src/App.js
+++ b/hugegraph-hubble/hubble-fe/src/App.js
@@ -25,6 +25,7 @@ import Layout from './layout.ant';
 import {AuthContextProvider} from './auth/AuthContext';
 import * as api from './api';
 import {setConfig} from './utils/config';
+import {clearLogin} from './utils/user';
 import {useEffect, useState} from 'react';
 
 const CONFIG_RETRY_DELAY_MS = 2000;
@@ -45,6 +46,7 @@ function App() {
         let active = true;
         let hasSafeConfig = false;
         let mountedConfigSignature;
+        let mountedAuthEnabled;
         let retryTimer;
         const loadConfig = () => {
             api.config.getConfig().then(response => {
@@ -54,6 +56,17 @@ function App() {
                 if (active) {
                     hasSafeConfig = true;
                     const nextSignature = routeConfigSignature(response.data);
+                    const nextAuthEnabled
+                            = response.data.auth_enabled !== false;
+                    const verified
+                            = response.data.server_capabilities_verified !== 
false;
+                    if (verified && mountedAuthEnabled !== false
+                        && !nextAuthEnabled) {
+                        clearLogin();
+                    }
+                    if (verified) {
+                        mountedAuthEnabled = nextAuthEnabled;
+                    }
                     setConfig(response.data);
                     if (nextSignature !== mountedConfigSignature) {
                         mountedConfigSignature = nextSignature;
diff --git a/hugegraph-hubble/hubble-fe/src/App.test.js 
b/hugegraph-hubble/hubble-fe/src/App.test.js
index fa2d95a2a..c8584ae6a 100644
--- a/hugegraph-hubble/hubble-fe/src/App.test.js
+++ b/hugegraph-hubble/hubble-fe/src/App.test.js
@@ -87,6 +87,8 @@ test('shows a retry surface when configuration bootstrap 
fails', async () => {
 
 test('revalidates fail-closed server capabilities until verified', async () => 
{
     jest.useFakeTimers();
+    sessionStorage.setItem('user_', JSON.stringify({user_name: 'admin'}));
+    localStorage.setItem('user', 'admin');
     api.config.getConfig
         .mockResolvedValueOnce({
             status: 200,
@@ -127,6 +129,8 @@ test('revalidates fail-closed server capabilities until 
verified', async () => {
         });
     expect(screen.getByTestId('app-route'))
         .toHaveAttribute('data-auth-enabled', 'false');
+    expect(sessionStorage.getItem('user_')).toBeNull();
+    expect(localStorage.getItem('user')).toBeNull();
     jest.useRealTimers();
 });
 
diff --git a/hugegraph-hubble/hubble-fe/src/pages/Account/EditLayer.js 
b/hugegraph-hubble/hubble-fe/src/pages/Account/EditLayer.js
index e83b6afd3..39c33392d 100644
--- a/hugegraph-hubble/hubble-fe/src/pages/Account/EditLayer.js
+++ b/hugegraph-hubble/hubble-fe/src/pages/Account/EditLayer.js
@@ -97,7 +97,7 @@ const EditLayer = ({
             throw res;
         });
     }, [onCancel, onCreated, refresh, t]);
-    const updateUser = useCallback(values => {
+    const updateUser = useCallback(async values => {
         const profile = toProfilePayload(values);
         const superAdminChanged = permissionPresetsSupported
             && Boolean(values.is_superadmin) !== Boolean(detail.is_superadmin);
@@ -109,17 +109,29 @@ const EditLayer = ({
                     : PERMISSION_PRESETS.GS_READ_ONLY,
             })
             : profile;
-        return api.auth.updateUser(data.id, payload, 
PAGE_ERROR_CONFIG).then(res => {
-            if (res.status === 200) {
-                message.success(t('common.msg.update_success'));
-                onCancel();
-                refresh();
-
-                return;
+        const requestUpdate = async update => {
+            const response = await api.auth.updateUser(
+                data.id, update, PAGE_ERROR_CONFIG
+            );
+            if (response.status !== 200) {
+                throw response;
             }
-
-            throw res;
-        });
+        };
+        if (superAdminChanged && profile.user_password) {
+            await requestUpdate(toPermissionPayload({
+                user_name: profile.user_name,
+                permission_preset: values.is_superadmin
+                    ? PERMISSION_PRESETS.SUPER_ADMIN
+                    : PERMISSION_PRESETS.GS_READ_ONLY,
+            }));
+            await requestUpdate(profile);
+        }
+        else {
+            await requestUpdate(payload);
+        }
+        message.success(t('common.msg.update_success'));
+        onCancel();
+        refresh();
     }, [
         data.id,
         detail.is_superadmin,
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 8f8172ee4..2b88966dd 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
@@ -317,6 +317,48 @@ test('updates a password without resubmitting unchanged 
permissions', async () =
     expect(payload).not.toHaveProperty('graphspace_permissions');
 });
 
+test('separates simultaneous password and super administrator changes', async 
() => {
+    mockAuthContext = {
+        capabilities: ['accounts_manage', 'account_permission_presets'],
+    };
+    api.auth.getUserInfo.mockResolvedValue({
+        status: 200,
+        data: {
+            user_name: 'alice',
+            is_superadmin: false,
+            permission_preset: 'GS_READ_ONLY',
+            graphspace_permissions: [],
+        },
+    });
+    api.auth.updateUser.mockResolvedValue({status: 200});
+
+    render(<EditLayer {...props} data={{id: 'alice'}} op='edit' />);
+
+    await screen.findByDisplayValue('alice');
+    fireEvent.change(screen.getByPlaceholderText(
+        'account.form.default_password_placeholder'
+    ), {target: {value: 'new-password'}});
+    fireEvent.click(screen.getByRole('switch'));
+    fireEvent.click(document.querySelector(
+        '.ant-modal-footer .ant-btn-primary'
+    ));
+
+    await waitFor(() => expect(api.auth.updateUser).toHaveBeenCalledTimes(2));
+    
expect(api.auth.updateUser.mock.calls[0][1]).toEqual(expect.objectContaining({
+        user_name: 'alice',
+        permission_preset: 'SUPER_ADMIN',
+        is_superadmin: true,
+    }));
+    expect(api.auth.updateUser.mock.calls[0][1])
+        .not.toHaveProperty('user_password');
+    
expect(api.auth.updateUser.mock.calls[1][1]).toEqual(expect.objectContaining({
+        user_name: 'alice',
+        user_password: 'new-password',
+    }));
+    expect(api.auth.updateUser.mock.calls[1][1])
+        .not.toHaveProperty('permission_preset');
+});
+
 test('keeps GraphSpace membership out of the account profile form', async () 
=> {
     mockAuthContext = {
         capabilities: ['accounts_manage', 'account_permission_presets'],
diff --git 
a/hugegraph-hubble/hubble-fe/src/pages/Account/account-recovery.test.js 
b/hugegraph-hubble/hubble-fe/src/pages/Account/account-recovery.test.js
index f672c4120..dc32f96d2 100644
--- a/hugegraph-hubble/hubble-fe/src/pages/Account/account-recovery.test.js
+++ b/hugegraph-hubble/hubble-fe/src/pages/Account/account-recovery.test.js
@@ -66,7 +66,7 @@ jest.mock('./SpaceAccess', () => props => (
             </button>
         )}
         {props.pendingAccount && (
-            <span>
+            <span data-testid="pending-account">
                 pending {props.pendingAccount.user_name}
                 {' '}
                 {props.pendingAccount.graphspaces.join(',')}
@@ -242,6 +242,32 @@ test('super administrators retain all account management 
actions', async () => {
     expect(screen.getByText('common.action.delete')).toBeInTheDocument();
 });
 
+test('preserves every GraphSpace when managing an account membership', async 
() => {
+    api.auth.getAllUserList.mockResolvedValue({
+        status: 200,
+        data: {
+            records: [{
+                id: 'analyst-id',
+                user_name: 'analyst',
+                graphspace_permissions: [
+                    {graphspace: 'SPACE_A', permission_preset: 'GS_READ_ONLY'},
+                    {graphspace: 'SPACE_B', permission_preset: 'GS_READ_ONLY'},
+                ],
+            }],
+            total: 1,
+        },
+    });
+
+    render(<Account />);
+
+    expect(await screen.findByText('analyst')).toBeInTheDocument();
+    await userEvent.click(screen.getByRole('button', {name: 
'account.action.more'}));
+    await 
userEvent.click(screen.getByText('account.action.manage_membership'));
+
+    expect(await screen.findByTestId('pending-account'))
+        .toHaveTextContent('pending analyst SPACE_A,SPACE_B');
+});
+
 test('returns a guided account creation to the selected GraphSpaces', async () 
=> {
     mockAuthContext.context.actions.members = ['read', 'add', 'remove'];
     api.auth.getAllUserList.mockResolvedValue({
diff --git a/hugegraph-hubble/hubble-fe/src/pages/Account/index.js 
b/hugegraph-hubble/hubble-fe/src/pages/Account/index.js
index 6c5ab50b3..59b037882 100644
--- a/hugegraph-hubble/hubble-fe/src/pages/Account/index.js
+++ b/hugegraph-hubble/hubble-fe/src/pages/Account/index.js
@@ -95,10 +95,12 @@ const GlobalAccounts = ({
     }, []);
 
     const showMembership = useCallback(row => {
+        const graphspaces = getPresetSpaces(row);
         onAssignMember?.({
             user_id: row.id,
             user_name: row.user_name,
-            graphspace: getPresetSpaces(row)[0],
+            graphspace: graphspaces[0],
+            graphspaces,
         });
     }, [onAssignMember]);
 

Reply via email to