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 84e3ef456dcade683f4c23c11e811e37d9f689b2
Author: dark <[email protected]>
AuthorDate: Tue Aug 18 12:31:26 2026 +0800

    fix(ci): align coverage and quality checks
    
    - upload coverage from real module paths
    - cover permission update behavior
    - normalize PD states and the 120-column contract
---
 .editorconfig                                      |   4 +-
 .github/actions/upload-coverage/action.yml         |   1 +
 .github/workflows/client-ci.yml                    |   2 +-
 .github/workflows/hubble-ci.yml                    |   2 +-
 .github/workflows/loader-ci.yml                    |   2 +-
 .github/workflows/tools-ci.yml                     |   6 -
 .serena/memories/README_INDEX.md                   |   2 +-
 .serena/memories/code_style_and_conventions.md     |   2 +-
 README.md                                          |   2 +-
 hugegraph-hubble/.prettierrc                       |   2 +-
 .../controller/auth/GraphSpaceUserController.java  |   6 +-
 .../hugegraph/service/auth/AuthContextService.java |   4 +-
 .../service/auth/GraphSpaceUserService.java        | 177 ++++++++++++--
 .../service/op/OperationsPayloadParser.java        |  29 ++-
 .../GraphSpaceAuthMutationAuthorizationTest.java   |   3 +-
 .../auth/GraphSpaceAuthOwnershipTest.java          |  73 +++++-
 .../service/auth/AuthContextServiceTest.java       |   2 +
 .../service/auth/GraphSpaceUserServiceTest.java    | 266 ++++++++++++++++++++-
 .../service/op/LiveOperationsCollectorTest.java    |  40 +++-
 .../service/op/OperationsPayloadParserTest.java    |  19 ++
 .../apache/hugegraph/unit/AuthSecurityTest.java    |   3 +-
 .../unit/UserServiceCompatibilityTest.java         | 101 ++++++++
 hugegraph-hubble/hubble-fe/src/api/auth.js         |   9 +-
 .../src/modules/navigation/ConsoleItem/index.js    |   3 +-
 .../hubble-fe/src/pages/Account/EditLayer.js       |  12 +-
 .../hubble-fe/src/pages/Account/SpaceAccess.js     |   3 +-
 hugegraph-hubble/hubble-fe/src/utils/rules.test.js |   8 +-
 27 files changed, 684 insertions(+), 99 deletions(-)

diff --git a/.editorconfig b/.editorconfig
index cf79877b0..4a6e05ecb 100644
--- a/.editorconfig
+++ b/.editorconfig
@@ -21,9 +21,9 @@ root = true
 charset = utf-8
 end_of_line = lf
 insert_final_newline = true
-max_line_length = 100
+max_line_length = 120
 ij_wrap_on_typing = true
-ij_visual_guides = 100
+ij_visual_guides = 120
 
 
 [*.{java,xml,py}]
diff --git a/.github/actions/upload-coverage/action.yml 
b/.github/actions/upload-coverage/action.yml
index 143da955b..bf8ba8cd8 100644
--- a/.github/actions/upload-coverage/action.yml
+++ b/.github/actions/upload-coverage/action.yml
@@ -17,4 +17,5 @@ runs:
       uses: codecov/codecov-action@v7
       with:
         token: ${{ inputs.token }}
+        slug: ${{ github.repository }}
         files: ${{ inputs.file }}
diff --git a/.github/workflows/client-ci.yml b/.github/workflows/client-ci.yml
index e076dd256..90d7e8110 100644
--- a/.github/workflows/client-ci.yml
+++ b/.github/workflows/client-ci.yml
@@ -68,4 +68,4 @@ jobs:
         uses: ./.github/actions/upload-coverage
         with:
           token: ${{ secrets.CODECOV_TOKEN }}
-          file: target/jacoco.xml
+          file: hugegraph-client/target/jacoco.xml
diff --git a/.github/workflows/hubble-ci.yml b/.github/workflows/hubble-ci.yml
index d61370480..ae7290aa4 100644
--- a/.github/workflows/hubble-ci.yml
+++ b/.github/workflows/hubble-ci.yml
@@ -208,4 +208,4 @@ jobs:
         uses: ./.github/actions/upload-coverage
         with:
           token: ${{ secrets.CODECOV_TOKEN }}
-          file: target/site/jacoco/*.xml
+          file: hugegraph-hubble/hubble-be/target/jacoco.xml
diff --git a/.github/workflows/loader-ci.yml b/.github/workflows/loader-ci.yml
index f8eee02d3..8389ecab6 100644
--- a/.github/workflows/loader-ci.yml
+++ b/.github/workflows/loader-ci.yml
@@ -94,4 +94,4 @@ jobs:
         uses: ./.github/actions/upload-coverage
         with:
           token: ${{ secrets.CODECOV_TOKEN }}
-          file: target/jacoco.xml
+          file: hugegraph-loader/target/jacoco.xml
diff --git a/.github/workflows/tools-ci.yml b/.github/workflows/tools-ci.yml
index 340f71aa1..4613bcd0f 100644
--- a/.github/workflows/tools-ci.yml
+++ b/.github/workflows/tools-ci.yml
@@ -59,9 +59,3 @@ jobs:
       - name: Run test
         run: |
           mvn test -Dtest=FuncTestSuite -pl hugegraph-tools -ntp
-
-      - name: Upload coverage to Codecov
-        uses: ./.github/actions/upload-coverage
-        with:
-          token: ${{ secrets.CODECOV_TOKEN }}
-          file: target/jacoco.xml
diff --git a/.serena/memories/README_INDEX.md b/.serena/memories/README_INDEX.md
index 3f506906f..4f5dd6806 100644
--- a/.serena/memories/README_INDEX.md
+++ b/.serena/memories/README_INDEX.md
@@ -212,7 +212,7 @@ git --no-pager diff HEAD~1
 4. ❌ **DON'T** use `System.out.println` (use logger instead)
 5. ❌ **DON'T** forget Apache 2.0 license headers
 6. ❌ **DON'T** use tabs (use 4 spaces for Java, 2 for frontend)
-7. ❌ **DON'T** exceed 100 character line length
+7. ❌ **DON'T** exceed 120 character line length
 8. ❌ **DON'T** commit code that fails CI checks
 
 ## Getting Help
diff --git a/.serena/memories/code_style_and_conventions.md 
b/.serena/memories/code_style_and_conventions.md
index 0c14759c6..594063a5f 100644
--- a/.serena/memories/code_style_and_conventions.md
+++ b/.serena/memories/code_style_and_conventions.md
@@ -12,7 +12,7 @@
 ### Basic Formatting
 - **Indentation**: 4 spaces (NO TABS)
 - **Continuation Indent**: 8 spaces
-- **Line Length**: Maximum 100 characters
+- **Line Length**: Maximum 120 characters
 - **Line Wrapping**: Enabled for long lines
 - **Blank Lines**:
   - Keep max 1 blank line in declarations
diff --git a/README.md b/README.md
index 3edc10958..bdc68a7cd 100644
--- a/README.md
+++ b/README.md
@@ -388,7 +388,7 @@ mvn clean install -DskipTests -Dmaven.javadoc.skip=true -ntp
 ### Code Style
 
 Checkstyle is enforced via `tools/checkstyle.xml`:
-- Max line length: 100 characters
+- Max line length: 120 characters
 - 4-space indentation (no tabs)
 - No star imports
 - No `System.out.println`
diff --git a/hugegraph-hubble/.prettierrc b/hugegraph-hubble/.prettierrc
index afb777869..b6d7333f3 100644
--- a/hugegraph-hubble/.prettierrc
+++ b/hugegraph-hubble/.prettierrc
@@ -1,6 +1,6 @@
 {
   "singleQuote": true,
   "tabWidth": 2,
-  "printWidth": 80,
+  "printWidth": 120,
   "trailingComma": "none"
 }
diff --git 
a/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/controller/auth/GraphSpaceUserController.java
 
b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/controller/auth/GraphSpaceUserController.java
index 692c385d0..03dd4fb70 100644
--- 
a/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/controller/auth/GraphSpaceUserController.java
+++ 
b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/controller/auth/GraphSpaceUserController.java
@@ -145,10 +145,8 @@ public class GraphSpaceUserController extends 
AuthController {
     private HugeClient requireMemberManager(String graphSpace,
                                             String userId) {
         HugeClient client = this.requireGraphSpaceManager(graphSpace);
-        User account = client.auth().getUser(userId);
-        if (account != null &&
-            client.auth().listSpaceAdmin(graphSpace)
-                  .contains(account.name())) {
+        if (client.auth().listSuperAdmin().contains(userId) ||
+            client.auth().listSpaceAdmin(graphSpace).contains(userId)) {
             return this.requireGraphSpaceAuthorizationAdmin(graphSpace);
         }
         return client;
diff --git 
a/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/service/auth/AuthContextService.java
 
b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/service/auth/AuthContextService.java
index 8e0c19105..c0e699172 100644
--- 
a/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/service/auth/AuthContextService.java
+++ 
b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/service/auth/AuthContextService.java
@@ -209,8 +209,8 @@ public class AuthContextService {
                 capabilities.add(GRAPHSPACES_MANAGE);
             }
         }
-        if (pdEnabled && (SUPERADMIN.equals(role) ||
-                          SPACEADMIN.equals(role))) {
+        if (pdEnabled && permissionPresets &&
+            (SUPERADMIN.equals(role) || SPACEADMIN.equals(role))) {
             capabilities.add(GRAPHSPACE_MEMBERS_MANAGE);
         }
         if (pdEnabled && SUPERADMIN.equals(role)) {
diff --git 
a/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/service/auth/GraphSpaceUserService.java
 
b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/service/auth/GraphSpaceUserService.java
index d02ea3538..7feeadedc 100644
--- 
a/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/service/auth/GraphSpaceUserService.java
+++ 
b/hugegraph-hubble/hubble-be/src/main/java/org/apache/hugegraph/service/auth/GraphSpaceUserService.java
@@ -223,27 +223,56 @@ public class GraphSpaceUserService extends AuthService {
                                  String userId, String preset) {
         requirePermissionPreset(preset);
         requirePermissionPresets(client);
-        User account = client.auth().getUser(userId);
-        E.checkNotNull(account, "User");
-        this.clearCustomRoles(client, graphSpace, userId);
-        this.clearDefaultRoles(client, graphSpace, account.name());
-        if (!client.auth().listSpaceMember(graphSpace)
-                   .contains(account.name())) {
-            client.auth().addSpaceMember(account.name(), graphSpace);
-        }
-        if ("GS_ADMIN".equals(preset)) {
-            if (!client.auth().listSpaceAdmin(graphSpace)
-                       .contains(account.name())) {
-                client.auth().addSpaceAdmin(account.name(), graphSpace);
+        E.checkArgument(!client.auth().listSuperAdmin().contains(userId),
+                        "Can't assign GraphSpace preset to super " +
+                        "administrator '%s'", userId);
+        boolean wasMember =
+                client.auth().listSpaceMember(graphSpace).contains(userId);
+        String username = userId;
+        SpacePresetState previous = null;
+        try {
+            if (!wasMember) {
+                client.auth().addSpaceMember(userId, graphSpace);
             }
-            this.setDefaultRole(client, graphSpace, account.name(), "analyst");
-            return;
-        }
-        if (client.auth().listSpaceAdmin(graphSpace).contains(account.name())) 
{
-            client.auth().delSpaceAdmin(account.name(), graphSpace);
+            User account = client.auth().getUser(userId);
+            E.checkNotNull(account, "User");
+            username = account.name();
+            previous = this.capturePresetState(client, graphSpace, userId,
+                                               username, wasMember);
+            previous.customRoles.forEach(
+                    belong -> this.belongService.deleteById(
+                            client, graphSpace, belong.getId()));
+            if (previous.analyst) {
+                client.graphSpace().deleteDefaultRole(
+                        graphSpace, username, "analyst");
+            }
+            if (previous.observer) {
+                client.graphSpace().deleteDefaultRole(
+                        graphSpace, username, "observer");
+            }
+            if ("GS_ADMIN".equals(preset)) {
+                if (!previous.admin) {
+                    client.auth().addSpaceAdmin(username, graphSpace);
+                }
+                this.setDefaultRole(client, graphSpace, username, "analyst");
+                return;
+            }
+            if (previous.admin) {
+                client.auth().delSpaceAdmin(username, graphSpace);
+            }
+            String role = "GS_READ_ONLY".equals(preset) ?
+                          "observer" : "analyst";
+            this.setDefaultRole(client, graphSpace, username, role);
+        } catch (RuntimeException e) {
+            if (previous == null) {
+                this.rollbackNewMember(client, graphSpace, userId,
+                                       wasMember, e);
+            } else {
+                this.restorePresetState(client, graphSpace, userId, username,
+                                        previous, e);
+            }
+            throw e;
         }
-        String role = "GS_READ_ONLY".equals(preset) ? "observer" : "analyst";
-        this.setDefaultRole(client, graphSpace, account.name(), role);
     }
 
     public void removeSpacePreset(HugeClient client, String graphSpace,
@@ -258,6 +287,97 @@ public class GraphSpaceUserService extends AuthService {
         }
     }
 
+    private SpacePresetState capturePresetState(HugeClient client,
+                                                String graphSpace,
+                                                String userId,
+                                                String username,
+                                                boolean member) {
+        List<BelongEntity> customRoles = this.belongService.list(
+                client, graphSpace, null, userId);
+        boolean analyst = client.graphSpace().checkDefaultRole(
+                graphSpace, username, "analyst");
+        boolean observer = client.graphSpace().checkDefaultRole(
+                graphSpace, username, "observer");
+        boolean admin = client.auth().listSpaceAdmin(graphSpace)
+                              .contains(username);
+        return new SpacePresetState(customRoles, member, admin,
+                                    analyst, observer);
+    }
+
+    private void restorePresetState(HugeClient client, String graphSpace,
+                                    String userId, String username,
+                                    SpacePresetState previous,
+                                    RuntimeException failure) {
+        previous.customRoles.forEach(belong -> {
+            this.tryRestore(() -> {
+                Set<String> currentRoles = this.belongService.list(
+                        client, graphSpace, null, userId).stream()
+                        .map(BelongEntity::getRoleId)
+                        .collect(Collectors.toSet());
+                if (!currentRoles.contains(belong.getRoleId())) {
+                    this.belongService.add(client, graphSpace,
+                                           belong.getRoleId(), userId);
+                }
+            }, graphSpace, userId,
+                            "custom role " + belong.getRoleId(), failure);
+        });
+        this.tryRestore(() -> this.restoreDefaultRole(
+                client, graphSpace, username, "analyst", previous.analyst),
+                        graphSpace, userId, "analyst role", failure);
+        this.tryRestore(() -> this.restoreDefaultRole(
+                client, graphSpace, username, "observer", previous.observer),
+                        graphSpace, userId, "observer role", failure);
+        this.tryRestore(() -> {
+            boolean current = client.auth().listSpaceAdmin(graphSpace)
+                                    .contains(username);
+            if (previous.admin && !current) {
+                client.auth().addSpaceAdmin(username, graphSpace);
+            } else if (!previous.admin && current) {
+                client.auth().delSpaceAdmin(username, graphSpace);
+            }
+        }, graphSpace, userId, "administrator", failure);
+        this.rollbackNewMember(client, graphSpace, userId,
+                               previous.member, failure);
+    }
+
+    private void restoreDefaultRole(HugeClient client, String graphSpace,
+                                    String username, String role,
+                                    boolean expected) {
+        boolean current = client.graphSpace().checkDefaultRole(
+                graphSpace, username, role);
+        if (expected && !current) {
+            client.graphSpace().setDefaultRole(graphSpace, username, role);
+        } else if (!expected && current) {
+            client.graphSpace().deleteDefaultRole(graphSpace, username, role);
+        }
+    }
+
+    private void rollbackNewMember(HugeClient client, String graphSpace,
+                                   String userId, boolean expected,
+                                   RuntimeException failure) {
+        this.tryRestore(() -> {
+            boolean current = client.auth().listSpaceMember(graphSpace)
+                                    .contains(userId);
+            if (expected && !current) {
+                client.auth().addSpaceMember(userId, graphSpace);
+            } else if (!expected && current) {
+                client.auth().delSpaceMember(userId, graphSpace);
+            }
+        }, graphSpace, userId, "membership", failure);
+    }
+
+    private void tryRestore(Runnable action, String graphSpace,
+                            String userId, String state,
+                            RuntimeException failure) {
+        try {
+            action.run();
+        } catch (RuntimeException rollbackFailure) {
+            failure.addSuppressed(rollbackFailure);
+            log.warn("Failed to restore GraphSpace {} for '{}' in '{}'",
+                     state, userId, graphSpace, rollbackFailure);
+        }
+    }
+
     private static void requireLegacyRoleAssignments(HugeClient client) {
         if (client.supportsDefaultRole()) {
             throw new ParameterizedException(
@@ -367,4 +487,23 @@ public class GraphSpaceUserService extends AuthService {
         }
         return users;
     }
+
+    private static class SpacePresetState {
+
+        private final List<BelongEntity> customRoles;
+        private final boolean member;
+        private final boolean admin;
+        private final boolean analyst;
+        private final boolean observer;
+
+        private SpacePresetState(List<BelongEntity> customRoles,
+                                 boolean member, boolean admin,
+                                 boolean analyst, boolean observer) {
+            this.customRoles = customRoles;
+            this.member = member;
+            this.admin = admin;
+            this.analyst = analyst;
+            this.observer = observer;
+        }
+    }
 }
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 f36e946c8..c5631914d 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
@@ -565,16 +565,27 @@ public class OperationsPayloadParser {
             return "UNKNOWN";
         }
         String value = state.toUpperCase(Locale.ROOT);
-        if (value.contains("OK") || value.equals("UP")) {
-            return "UP";
-        }
-        if (value.contains("WARN") || value.contains("DEGRADED")) {
-            return "DEGRADED";
+        switch (value) {
+            case "CLUSTER_OK":
+            case "OK":
+            case "UP":
+                return "UP";
+            case "CLUSTER_WARN":
+            case "CLUSTER_NOT_READY":
+            case "CLUSTER_OFFLINE":
+            case "WARN":
+            case "WARNING":
+            case "DEGRADED":
+            case "NOT_READY":
+            case "OFFLINE":
+                return "DEGRADED";
+            case "CLUSTER_FAULT":
+            case "FAULT":
+            case "DOWN":
+                return "DOWN";
+            default:
+                return "UNKNOWN";
         }
-        if (value.contains("DOWN") || value.contains("FAULT")) {
-            return "DOWN";
-        }
-        return "UNKNOWN";
     }
 
     private String health(String state) {
diff --git 
a/hugegraph-hubble/hubble-be/src/test/java/org/apache/hugegraph/controller/auth/GraphSpaceAuthMutationAuthorizationTest.java
 
b/hugegraph-hubble/hubble-be/src/test/java/org/apache/hugegraph/controller/auth/GraphSpaceAuthMutationAuthorizationTest.java
index 9cf151c9e..e96ae5bac 100644
--- 
a/hugegraph-hubble/hubble-be/src/test/java/org/apache/hugegraph/controller/auth/GraphSpaceAuthMutationAuthorizationTest.java
+++ 
b/hugegraph-hubble/hubble-be/src/test/java/org/apache/hugegraph/controller/auth/GraphSpaceAuthMutationAuthorizationTest.java
@@ -275,7 +275,8 @@ public class GraphSpaceAuthMutationAuthorizationTest {
                         .contentType(MediaType.APPLICATION_JSON)
                         .content("{\"role_id\":\"r\",\"user_id\":\"u\"}"));
         assertForbidden(mvc(access),
-                
post("/api/v1.3/graphspaces/SPACE/auth/accesses").contentType(MediaType.APPLICATION_JSON).content("{}"));
+                post("/api/v1.3/graphspaces/SPACE/auth/accesses")
+                    .contentType(MediaType.APPLICATION_JSON).content("{}"));
         assertForbidden(mvc(target),
                 
post("/api/v1.3/graphspaces/SPACE/auth/targets").contentType(MediaType.APPLICATION_JSON).content("{}"));
     }
diff --git 
a/hugegraph-hubble/hubble-be/src/test/java/org/apache/hugegraph/controller/auth/GraphSpaceAuthOwnershipTest.java
 
b/hugegraph-hubble/hubble-be/src/test/java/org/apache/hugegraph/controller/auth/GraphSpaceAuthOwnershipTest.java
index bf8a7d083..3eaefb867 100644
--- 
a/hugegraph-hubble/hubble-be/src/test/java/org/apache/hugegraph/controller/auth/GraphSpaceAuthOwnershipTest.java
+++ 
b/hugegraph-hubble/hubble-be/src/test/java/org/apache/hugegraph/controller/auth/GraphSpaceAuthOwnershipTest.java
@@ -86,6 +86,8 @@ public class GraphSpaceAuthOwnershipTest {
                .thenReturn(Collections.emptyList());
         Mockito.when(this.auth.listSpaceMember(Mockito.anyString()))
                .thenReturn(Collections.emptyList());
+        Mockito.when(this.auth.listSuperAdmin())
+               .thenReturn(Collections.emptyList());
     }
 
     @Test
@@ -474,7 +476,8 @@ public class GraphSpaceAuthOwnershipTest {
                                           .id("belong-a")
                                           .userId("user-id")
                                           .build();
-        Mockito.when(belongs.list(this.client, "SPACE_A", null, 
"user-id")).thenReturn(Collections.singletonList(scoped));
+        Mockito.when(belongs.list(this.client, "SPACE_A", null, "user-id"))
+               .thenReturn(Collections.singletonList(scoped));
         GraphSpaceUserService service = new GraphSpaceUserService();
         ReflectionTestUtils.setField(service, "belongService", belongs);
 
@@ -490,15 +493,18 @@ public class GraphSpaceAuthOwnershipTest {
     @Test
     public void testReadOnlyPresetAppliesSpaceWideObserver() {
         User account = new User();
-        account.setId("user-id");
+        account.setId("graph-user");
         account.name("graph-user");
-        Mockito.when(this.auth.getUser("user-id")).thenReturn(account);
+        Mockito.when(this.auth.getUser("graph-user")).thenReturn(account);
         BelongService belongs = Mockito.mock(BelongService.class);
-        Mockito.when(belongs.list(this.client, "SPACE_A", null, 
"user-id")).thenReturn(Collections.emptyList());
+        Mockito.when(belongs.list(this.client, "SPACE_A", null,
+                                  "graph-user"))
+               .thenReturn(Collections.emptyList());
         GraphSpaceUserService service = new GraphSpaceUserService();
         ReflectionTestUtils.setField(service, "belongService", belongs);
 
-        service.applySpacePreset(this.client, "SPACE_A", "user-id", 
"GS_READ_ONLY");
+        service.applySpacePreset(this.client, "SPACE_A", "graph-user",
+                                 "GS_READ_ONLY");
 
         Mockito.verify(this.auth).addSpaceMember("graph-user", "SPACE_A");
         Mockito.verify(this.graphSpace).setDefaultRole(
@@ -588,6 +594,63 @@ public class GraphSpaceAuthOwnershipTest {
            .andExpect(status().isForbidden());
     }
 
+    @Test
+    public void testSpaceAdminCanAssignPresetToNewMemberWithoutPreRead()
+            throws Exception {
+        UserService authorization = Mockito.mock(UserService.class);
+        Mockito.when(authorization.isAssignSpaceAdmin(this.client, "SPACE"))
+               .thenReturn(true);
+        GraphSpaceUserService members =
+                Mockito.mock(GraphSpaceUserService.class);
+        TestGraphSpaceUserController controller =
+                new TestGraphSpaceUserController(this.client);
+        setBaseUserService(controller, authorization);
+        ReflectionTestUtils.setField(controller, "userService", members);
+        MockMvc mvc = MockMvcBuilders.standaloneSetup(controller)
+                                     .setControllerAdvice(
+                                             new ExceptionAdvisor())
+                                     .build();
+
+        mvc.perform(put("/api/v1.3/graphspaces/SPACE/auth/users/" +
+                        "new-member/preset")
+                    .contentType(MediaType.APPLICATION_JSON)
+                    .content("{\"permission_preset\":\"GS_READ_WRITE\"}"))
+           .andExpect(status().isOk());
+
+        Mockito.verify(members).applySpacePreset(
+                this.client, "SPACE", "new-member", "GS_READ_WRITE");
+        Mockito.verify(this.auth, Mockito.never())
+               .getUser(Mockito.anyString());
+    }
+
+    @Test
+    public void testSpaceAdminCannotChangeGlobalAdminPreset()
+            throws Exception {
+        Mockito.when(this.auth.listSuperAdmin())
+               .thenReturn(Collections.singletonList("global-admin"));
+        UserService authorization = Mockito.mock(UserService.class);
+        Mockito.when(authorization.isAssignSpaceAdmin(this.client, "SPACE"))
+               .thenReturn(true);
+        GraphSpaceUserService members =
+                Mockito.mock(GraphSpaceUserService.class);
+        TestGraphSpaceUserController controller =
+                new TestGraphSpaceUserController(this.client);
+        setBaseUserService(controller, authorization);
+        ReflectionTestUtils.setField(controller, "userService", members);
+        MockMvc mvc = MockMvcBuilders.standaloneSetup(controller)
+                                     .setControllerAdvice(
+                                             new ExceptionAdvisor())
+                                     .build();
+
+        mvc.perform(put("/api/v1.3/graphspaces/SPACE/auth/users/" +
+                        "global-admin/preset")
+                    .contentType(MediaType.APPLICATION_JSON)
+                    .content("{\"permission_preset\":\"GS_READ_WRITE\"}"))
+           .andExpect(status().isForbidden());
+
+        Mockito.verifyZeroInteractions(members);
+    }
+
     private static Target target(String id, String graphSpace) {
         Target target = new Target();
         target.setId(id);
diff --git 
a/hugegraph-hubble/hubble-be/src/test/java/org/apache/hugegraph/service/auth/AuthContextServiceTest.java
 
b/hugegraph-hubble/hubble-be/src/test/java/org/apache/hugegraph/service/auth/AuthContextServiceTest.java
index 3eb8eecc6..7f1b082b3 100644
--- 
a/hugegraph-hubble/hubble-be/src/test/java/org/apache/hugegraph/service/auth/AuthContextServiceTest.java
+++ 
b/hugegraph-hubble/hubble-be/src/test/java/org/apache/hugegraph/service/auth/AuthContextServiceTest.java
@@ -116,6 +116,8 @@ public class AuthContextServiceTest {
 
         Assert.assertFalse(capabilities(context).contains(
                            "account_permission_presets"));
+        Assert.assertFalse(capabilities(context).contains(
+                           "graphspace_members_manage"));
         Assert.assertTrue(actions(context, "members").isEmpty());
         Assert.assertTrue(actions(context, "roles").isEmpty());
         Assert.assertTrue(actions(context, "authorizations").isEmpty());
diff --git 
a/hugegraph-hubble/hubble-be/src/test/java/org/apache/hugegraph/service/auth/GraphSpaceUserServiceTest.java
 
b/hugegraph-hubble/hubble-be/src/test/java/org/apache/hugegraph/service/auth/GraphSpaceUserServiceTest.java
index acace9f1d..a9489d97c 100644
--- 
a/hugegraph-hubble/hubble-be/src/test/java/org/apache/hugegraph/service/auth/GraphSpaceUserServiceTest.java
+++ 
b/hugegraph-hubble/hubble-be/src/test/java/org/apache/hugegraph/service/auth/GraphSpaceUserServiceTest.java
@@ -23,6 +23,7 @@ import java.util.Map;
 
 import org.junit.Before;
 import org.junit.Test;
+import org.mockito.InOrder;
 import org.mockito.Mockito;
 import org.springframework.test.util.ReflectionTestUtils;
 
@@ -56,6 +57,8 @@ public class GraphSpaceUserServiceTest {
         Mockito.when(this.client.auth()).thenReturn(this.auth);
         Mockito.when(this.client.graphSpace()).thenReturn(this.graphSpace);
         Mockito.when(this.client.supportsDefaultRole()).thenReturn(true);
+        Mockito.when(this.auth.listSuperAdmin())
+               .thenReturn(Collections.emptyList());
     }
 
     @Test
@@ -155,14 +158,14 @@ public class GraphSpaceUserServiceTest {
 
     @Test
     public void testApplyReadOnlyPresetAndRemoveUnrequestedSpace() {
-        User user = user("u-1", "alice");
+        User user = user("alice", "alice");
         Mockito.when(this.client.findUserByName("alice")).thenReturn(user);
-        Mockito.when(this.auth.getUser("u-1")).thenReturn(user);
+        Mockito.when(this.auth.getUser("alice")).thenReturn(user);
         Mockito.when(this.graphSpace.listGraphSpace())
                .thenReturn(java.util.Arrays.asList("team", "old"));
         Mockito.when(this.belongService.list(
                              Mockito.eq(this.client), Mockito.anyString(),
-                             Mockito.isNull(), Mockito.eq("u-1")))
+                             Mockito.isNull(), Mockito.eq("alice")))
                .thenReturn(Collections.emptyList());
         Mockito.when(this.auth.listSpaceMember("team"))
                .thenReturn(Collections.emptyList());
@@ -184,17 +187,17 @@ public class GraphSpaceUserServiceTest {
 
     @Test
     public void testApplyAdminPresetAddsManagementAndWriteAccess() {
-        User user = user("u-1", "alice");
-        Mockito.when(this.auth.getUser("u-1")).thenReturn(user);
+        User user = user("alice", "alice");
+        Mockito.when(this.auth.getUser("alice")).thenReturn(user);
         Mockito.when(this.belongService.list(
-                             this.client, "team", null, "u-1"))
+                             this.client, "team", null, "alice"))
                .thenReturn(Collections.emptyList());
         Mockito.when(this.auth.listSpaceMember("team"))
                .thenReturn(Collections.emptyList());
         Mockito.when(this.auth.listSpaceAdmin("team"))
                .thenReturn(Collections.emptyList());
 
-        this.service.applySpacePreset(this.client, "team", "u-1",
+        this.service.applySpacePreset(this.client, "team", "alice",
                                       "GS_ADMIN");
 
         Mockito.verify(this.auth).addSpaceMember("alice", "team");
@@ -205,17 +208,17 @@ public class GraphSpaceUserServiceTest {
 
     @Test
     public void testApplyReadWritePresetRemovesAdminAccess() {
-        User user = user("u-1", "alice");
-        Mockito.when(this.auth.getUser("u-1")).thenReturn(user);
+        User user = user("alice", "alice");
+        Mockito.when(this.auth.getUser("alice")).thenReturn(user);
         Mockito.when(this.belongService.list(
-                             this.client, "team", null, "u-1"))
+                             this.client, "team", null, "alice"))
                .thenReturn(Collections.emptyList());
         Mockito.when(this.auth.listSpaceMember("team"))
                .thenReturn(Collections.singletonList("alice"));
         Mockito.when(this.auth.listSpaceAdmin("team"))
                .thenReturn(Collections.singletonList("alice"));
 
-        this.service.applySpacePreset(this.client, "team", "u-1",
+        this.service.applySpacePreset(this.client, "team", "alice",
                                       "GS_READ_WRITE");
 
         Mockito.verify(this.auth).delSpaceAdmin("alice", "team");
@@ -225,6 +228,247 @@ public class GraphSpaceUserServiceTest {
                .addSpaceMember(Mockito.anyString(), Mockito.anyString());
     }
 
+    @Test
+    public void testApplyPresetAddsMemberBeforeScopedUserRead() {
+        User user = user("alice", "alice");
+        Mockito.when(this.auth.listSpaceMember("team"))
+               .thenReturn(Collections.emptyList());
+        Mockito.when(this.auth.getUser("alice")).thenReturn(user);
+        Mockito.when(this.belongService.list(
+                             this.client, "team", null, "alice"))
+               .thenReturn(Collections.emptyList());
+        Mockito.when(this.auth.listSpaceAdmin("team"))
+               .thenReturn(Collections.emptyList());
+
+        this.service.applySpacePreset(this.client, "team", "alice",
+                                      "GS_READ_ONLY");
+
+        InOrder order = Mockito.inOrder(this.auth);
+        order.verify(this.auth).addSpaceMember("alice", "team");
+        order.verify(this.auth).getUser("alice");
+        Mockito.verify(this.graphSpace)
+               .setDefaultRole("team", "alice", "observer");
+    }
+
+    @Test
+    public void testApplyPresetRollsBackBootstrapMemberOnUserReadFailure() {
+        Mockito.when(this.auth.listSpaceMember("team"))
+               .thenReturn(Collections.emptyList())
+               .thenReturn(Collections.singletonList("alice"));
+        Mockito.when(this.auth.getUser("alice"))
+               .thenThrow(new RuntimeException("read failed"));
+
+        Assert.assertThrows(RuntimeException.class,
+                            () -> this.service.applySpacePreset(
+                                    this.client, "team", "alice",
+                                    "GS_READ_ONLY"));
+
+        InOrder order = Mockito.inOrder(this.auth);
+        order.verify(this.auth).addSpaceMember("alice", "team");
+        order.verify(this.auth).getUser("alice");
+        order.verify(this.auth).delSpaceMember("alice", "team");
+    }
+
+    @Test
+    public void testApplyPresetReconcilesMemberAfterLostAddResponse() {
+        Mockito.when(this.auth.listSpaceMember("team"))
+               .thenReturn(Collections.emptyList())
+               .thenReturn(Collections.singletonList("alice"));
+        Mockito.when(this.auth.addSpaceMember("alice", "team"))
+               .thenThrow(new RuntimeException("response lost"));
+
+        Assert.assertThrows(RuntimeException.class,
+                            () -> this.service.applySpacePreset(
+                                    this.client, "team", "alice",
+                                    "GS_READ_ONLY"));
+
+        Mockito.verify(this.auth).delSpaceMember("alice", "team");
+        Mockito.verify(this.auth, Mockito.never()).getUser("alice");
+    }
+
+    @Test
+    public void testApplyPresetRollsBackMemberOnDefaultRoleFailure() {
+        User user = user("alice", "alice");
+        Mockito.when(this.auth.listSpaceMember("team"))
+               .thenReturn(Collections.emptyList())
+               .thenReturn(Collections.singletonList("alice"));
+        Mockito.when(this.auth.getUser("alice")).thenReturn(user);
+        Mockito.when(this.belongService.list(
+                             this.client, "team", null, "alice"))
+               .thenReturn(Collections.emptyList());
+        Mockito.when(this.auth.listSpaceAdmin("team"))
+               .thenReturn(Collections.emptyList());
+        Mockito.when(this.graphSpace.setDefaultRole(
+                             "team", "alice", "observer"))
+               .thenThrow(new RuntimeException("role failed"));
+
+        Assert.assertThrows(RuntimeException.class,
+                            () -> this.service.applySpacePreset(
+                                    this.client, "team", "alice",
+                                    "GS_READ_ONLY"));
+
+        InOrder order = Mockito.inOrder(this.auth, this.graphSpace);
+        order.verify(this.auth).addSpaceMember("alice", "team");
+        order.verify(this.auth).getUser("alice");
+        order.verify(this.graphSpace)
+             .setDefaultRole("team", "alice", "observer");
+        order.verify(this.auth).delSpaceMember("alice", "team");
+    }
+
+    @Test
+    public void testApplyAdminPresetRollsBackAdminAndMemberOnRoleFailure() {
+        User user = user("alice", "alice");
+        Mockito.when(this.auth.listSpaceMember("team"))
+               .thenReturn(Collections.emptyList())
+               .thenReturn(Collections.singletonList("alice"));
+        Mockito.when(this.auth.getUser("alice")).thenReturn(user);
+        Mockito.when(this.belongService.list(
+                             this.client, "team", null, "alice"))
+               .thenReturn(Collections.emptyList());
+        Mockito.when(this.auth.listSpaceAdmin("team"))
+               .thenReturn(Collections.emptyList())
+               .thenReturn(Collections.singletonList("alice"));
+        Mockito.when(this.graphSpace.setDefaultRole(
+                             "team", "alice", "analyst"))
+               .thenThrow(new RuntimeException("role failed"));
+
+        Assert.assertThrows(RuntimeException.class,
+                            () -> this.service.applySpacePreset(
+                                    this.client, "team", "alice",
+                                    "GS_ADMIN"));
+
+        InOrder order = Mockito.inOrder(this.auth, this.graphSpace);
+        order.verify(this.auth).addSpaceMember("alice", "team");
+        order.verify(this.auth).getUser("alice");
+        order.verify(this.auth).addSpaceAdmin("alice", "team");
+        order.verify(this.graphSpace)
+             .setDefaultRole("team", "alice", "analyst");
+        order.verify(this.auth).delSpaceAdmin("alice", "team");
+        order.verify(this.auth).delSpaceMember("alice", "team");
+    }
+
+    @Test
+    public void testApplyPresetRestoresExistingAdminStateOnFailure() {
+        User user = user("alice", "alice");
+        BelongEntity custom = BelongEntity.builder()
+                                          .id("belong-1")
+                                          .userId("alice")
+                                          .roleId("role-1")
+                                          .build();
+        Mockito.when(this.auth.listSpaceMember("team"))
+               .thenReturn(Collections.singletonList("alice"));
+        Mockito.when(this.auth.getUser("alice")).thenReturn(user);
+        Mockito.when(this.belongService.list(
+                             this.client, "team", null, "alice"))
+               .thenReturn(Collections.singletonList(custom))
+               .thenReturn(Collections.emptyList());
+        Mockito.when(this.graphSpace.checkDefaultRole(
+                             "team", "alice", "analyst"))
+               .thenReturn(true, false);
+        Mockito.when(this.graphSpace.checkDefaultRole(
+                             "team", "alice", "observer"))
+               .thenReturn(false);
+        Mockito.when(this.auth.listSpaceAdmin("team"))
+               .thenReturn(Collections.singletonList("alice"))
+               .thenReturn(Collections.emptyList());
+        Mockito.when(this.graphSpace.setDefaultRole(
+                             "team", "alice", "analyst"))
+               .thenThrow(new RuntimeException("role failed"))
+               .thenReturn(Collections.emptyMap());
+
+        Assert.assertThrows(RuntimeException.class,
+                            () -> this.service.applySpacePreset(
+                                    this.client, "team", "alice",
+                                    "GS_READ_WRITE"));
+
+        Mockito.verify(this.belongService)
+               .add(this.client, "team", "role-1", "alice");
+        Mockito.verify(this.graphSpace, Mockito.times(2))
+               .setDefaultRole("team", "alice", "analyst");
+        Mockito.verify(this.auth).addSpaceAdmin("alice", "team");
+        Mockito.verify(this.auth, Mockito.never())
+               .delSpaceMember(Mockito.anyString(), Mockito.anyString());
+    }
+
+    @Test
+    public void testApplyPresetContinuesRestoringCustomRolesAfterFailure() {
+        User user = user("alice", "alice");
+        BelongEntity first = BelongEntity.builder()
+                                         .id("belong-1")
+                                         .userId("alice")
+                                         .roleId("role-1")
+                                         .build();
+        BelongEntity second = BelongEntity.builder()
+                                          .id("belong-2")
+                                          .userId("alice")
+                                          .roleId("role-2")
+                                          .build();
+        Mockito.when(this.auth.listSpaceMember("team"))
+               .thenReturn(Collections.singletonList("alice"));
+        Mockito.when(this.auth.getUser("alice")).thenReturn(user);
+        Mockito.when(this.belongService.list(
+                             this.client, "team", null, "alice"))
+               .thenReturn(java.util.Arrays.asList(first, second))
+               .thenReturn(Collections.emptyList())
+               .thenReturn(Collections.emptyList());
+        Mockito.when(this.graphSpace.checkDefaultRole(
+                             "team", "alice", "analyst"))
+               .thenReturn(false);
+        Mockito.when(this.graphSpace.checkDefaultRole(
+                             "team", "alice", "observer"))
+               .thenReturn(false);
+        Mockito.when(this.auth.listSpaceAdmin("team"))
+               .thenReturn(Collections.emptyList());
+        Mockito.when(this.graphSpace.setDefaultRole(
+                             "team", "alice", "observer"))
+               .thenThrow(new RuntimeException("role failed"));
+        Mockito.doThrow(new RuntimeException("first restore failed"))
+               .when(this.belongService)
+               .add(this.client, "team", "role-1", "alice");
+
+        Assert.assertThrows(RuntimeException.class,
+                            () -> this.service.applySpacePreset(
+                                    this.client, "team", "alice",
+                                    "GS_READ_ONLY"));
+
+        Mockito.verify(this.belongService)
+               .add(this.client, "team", "role-1", "alice");
+        Mockito.verify(this.belongService)
+               .add(this.client, "team", "role-2", "alice");
+    }
+
+    @Test
+    public void testApplyPresetRemovesRoleCommittedBeforeClientFailure() {
+        User user = user("alice", "alice");
+        Mockito.when(this.auth.listSpaceMember("team"))
+               .thenReturn(Collections.emptyList())
+               .thenReturn(Collections.singletonList("alice"));
+        Mockito.when(this.auth.getUser("alice")).thenReturn(user);
+        Mockito.when(this.belongService.list(
+                             this.client, "team", null, "alice"))
+               .thenReturn(Collections.emptyList());
+        Mockito.when(this.graphSpace.checkDefaultRole(
+                             "team", "alice", "analyst"))
+               .thenReturn(false);
+        Mockito.when(this.graphSpace.checkDefaultRole(
+                             "team", "alice", "observer"))
+               .thenReturn(false, true);
+        Mockito.when(this.auth.listSpaceAdmin("team"))
+               .thenReturn(Collections.emptyList());
+        Mockito.when(this.graphSpace.setDefaultRole(
+                             "team", "alice", "observer"))
+               .thenThrow(new RuntimeException("response lost"));
+
+        Assert.assertThrows(RuntimeException.class,
+                            () -> this.service.applySpacePreset(
+                                    this.client, "team", "alice",
+                                    "GS_READ_ONLY"));
+
+        Mockito.verify(this.graphSpace)
+               .deleteDefaultRole("team", "alice", "observer");
+        Mockito.verify(this.auth).delSpaceMember("alice", "team");
+    }
+
     @Test
     public void testRemovePresetCleansEveryGrantType() {
         User user = user("u-1", "alice");
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 bb5d4bf7b..daef59055 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
@@ -162,8 +162,7 @@ public class LiveOperationsCollectorTest {
     }
 
     @Test
-    public void testPdDegradedStatusMakesOverallSnapshotDegraded()
-           throws IOException {
+    public void testPdDegradedStatusMakesOverallSnapshotDegraded() throws 
IOException {
         String degraded = cluster().replace("Cluster_OK", "Cluster_Warn");
         HttpServer pd = pdServer(200, degraded, 200, stores());
         Snapshot snapshot;
@@ -174,13 +173,36 @@ public class LiveOperationsCollectorTest {
         }
 
         Assert.assertEquals("DEGRADED", snapshot.getStatus());
-        Assert.assertEquals("DEGRADED",
-                            snapshot.getSources().get("pd").getStatus());
+        Assert.assertEquals("DEGRADED", 
snapshot.getSources().get("pd").getStatus());
     }
 
     @Test
-    public void testPdUnknownStatusMakesOverallSnapshotDegraded()
-           throws IOException {
+    public void testPdNotReadyKeepsPdNodesUpAndOverallDegraded() throws 
IOException {
+        String notReady = cluster().replace("Cluster_OK", "Cluster_Not_Ready");
+        HttpServer pd = pdServer(200, notReady, 200, stores());
+        Snapshot snapshot;
+        try {
+            snapshot = collector(true, pd).collect(serverClient(), false);
+        } finally {
+            pd.stop(0);
+        }
+
+        OperationsModels.SourceStatus pdSource = 
snapshot.getSources().get("pd");
+        Assert.assertEquals("AVAILABLE", pdSource.getAvailability());
+        Assert.assertTrue(pdSource.isFresh());
+        Assert.assertEquals("DEGRADED", pdSource.getStatus());
+        Assert.assertEquals("DEGRADED", snapshot.getStatus());
+        long pdCount = snapshot.getNodes().stream()
+                               .filter(node -> "PD".equals(node.getType()))
+                               .count();
+        Assert.assertEquals(1L, pdCount);
+        Assert.assertTrue(snapshot.getNodes().stream()
+                                  .filter(node -> "PD".equals(node.getType()))
+                                  .allMatch(node -> 
"UP".equals(node.getStatus())));
+    }
+
+    @Test
+    public void testPdUnknownStatusMakesOverallSnapshotDegraded() throws 
IOException {
         String unknown = cluster().replace("Cluster_OK", "Cluster_Starting");
         HttpServer pd = pdServer(200, unknown, 200, stores());
         Snapshot snapshot;
@@ -191,8 +213,7 @@ public class LiveOperationsCollectorTest {
         }
 
         Assert.assertEquals("DEGRADED", snapshot.getStatus());
-        Assert.assertEquals("UNKNOWN",
-                            snapshot.getSources().get("pd").getStatus());
+        Assert.assertEquals("UNKNOWN", 
snapshot.getSources().get("pd").getStatus());
     }
 
     @Test
@@ -202,8 +223,7 @@ public class LiveOperationsCollectorTest {
 
         Snapshot snapshot = collector.collect(serverClient(), false);
 
-        Assert.assertEquals("UNKNOWN",
-                            snapshot.getSources().get("pd").getStatus());
+        Assert.assertEquals("DEGRADED", 
snapshot.getSources().get("pd").getStatus());
         Assert.assertEquals(0, http.leaderRequests());
     }
 
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 805de2106..de195a44d 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
@@ -100,6 +100,25 @@ public class OperationsPayloadParserTest {
                            "capacity_used_bytes"));
     }
 
+    @Test
+    public void testMapsExactClusterStates() {
+        OperationsPayloadParser parser = new OperationsPayloadParser(MAPPER);
+        String[] states = {"Cluster_OK", "Cluster_Warn", "Cluster_Not_Ready",
+                           "Cluster_Offline", "Cluster_Fault", null, 
"Cluster_OKish"};
+        String[] expected = {"UP", "DEGRADED", "DEGRADED", "DEGRADED", "DOWN",
+                             "UNKNOWN", "UNKNOWN"};
+        String stores = "{\"status\":0,\"data\":{\"stores\":[]}}";
+
+        for (int i = 0; i < states.length; i++) {
+            String state = states[i];
+            String field = state == null ? "" : "\"state\":\"" +
+                           state + "\",";
+            String cluster = "{\"status\":0,\"data\":{" + field +
+                             "\"pdList\":[]}}";
+            Assert.assertEquals(expected[i], parser.parseTopology(cluster, 
stores).getStatus());
+        }
+    }
+
     @Test
     public void testParsesOnlyStoreMetricTargetsAndInternalHostMap() {
         OperationsPayloadParser parser = new OperationsPayloadParser(MAPPER);
diff --git 
a/hugegraph-hubble/hubble-be/src/test/java/org/apache/hugegraph/unit/AuthSecurityTest.java
 
b/hugegraph-hubble/hubble-be/src/test/java/org/apache/hugegraph/unit/AuthSecurityTest.java
index 3c4e9b065..e947eb2a2 100644
--- 
a/hugegraph-hubble/hubble-be/src/test/java/org/apache/hugegraph/unit/AuthSecurityTest.java
+++ 
b/hugegraph-hubble/hubble-be/src/test/java/org/apache/hugegraph/unit/AuthSecurityTest.java
@@ -415,7 +415,8 @@ public class AuthSecurityTest {
                .thenReturn(false);
         AuthModeService mode = new AuthModeService(config);
         ReflectionTestUtils.setField(interceptor, "authMode", mode);
-        MockHttpServletRequest request = new MockHttpServletRequest("GET", 
"/api/v1.3/graphspaces/SPACE/graphs/graph/schema");
+        MockHttpServletRequest request = new MockHttpServletRequest(
+                "GET", "/api/v1.3/graphspaces/SPACE/graphs/graph/schema");
 
         Assert.assertTrue(interceptor.preHandle(request, new 
MockHttpServletResponse(), null));
         Assert.assertEquals(1, interceptor.unauthClients);
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 2e60a2260..24a9947d3 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
@@ -269,6 +269,107 @@ public class UserServiceCompatibilityTest {
                 Mockito.anyString(), Mockito.anyString());
     }
 
+    @Test
+    public void testModernUserUpdateReconcilesAdminSpaces() {
+        
Mockito.when(this.config.get(HubbleOptions.PD_ENABLED)).thenReturn(true);
+        Mockito.when(this.client.supportsDefaultRole()).thenReturn(true);
+        Mockito.when(this.graphSpace.listGraphSpace())
+               .thenReturn(Arrays.asList("OLD", "NEW"));
+        Mockito.when(this.auth.listSpaceAdmin("OLD"))
+               .thenReturn(Collections.singletonList("user"));
+        Mockito.when(this.auth.listSpaceAdmin("NEW"))
+               .thenReturn(Collections.emptyList());
+        Mockito.when(this.client.findUserByName("user"))
+               .thenReturn(user("user"));
+        UserEntity account = UserEntity.builder()
+                                       .id("user")
+                                       .name("user")
+                                       .adminSpaces(
+                                               
Collections.singletonList("NEW"))
+                                       .build();
+        account.setSuperadmin(true);
+
+        this.service.update(this.client, account);
+
+        Mockito.verify(this.graphSpaceUsers).applySpacePreset(
+                this.client, "NEW", "user", "GS_ADMIN");
+        Mockito.verify(this.graphSpaceUsers).removeSpacePreset(
+                this.client, "OLD", "user");
+        Mockito.verify(this.auth).addSuperAdmin("user");
+    }
+
+    @Test
+    public void testLegacyUserUpdateReconcilesAdminSpaces() {
+        
Mockito.when(this.config.get(HubbleOptions.PD_ENABLED)).thenReturn(true);
+        Mockito.when(this.client.supportsDefaultRole()).thenReturn(false);
+        Mockito.when(this.graphSpace.listGraphSpace())
+               .thenReturn(Arrays.asList("OLD", "NEW"));
+        Mockito.when(this.auth.listSpaceAdmin("OLD"))
+               .thenReturn(Collections.singletonList("user"));
+        Mockito.when(this.auth.listSpaceAdmin("NEW"))
+               .thenReturn(Collections.emptyList());
+        Mockito.when(this.client.findUserByName("user"))
+               .thenReturn(user("user"));
+        Mockito.when(this.auth.listSuperAdmin())
+               .thenReturn(Collections.singletonList("user"));
+        UserEntity account = UserEntity.builder()
+                                       .id("user")
+                                       .name("user")
+                                       .adminSpaces(
+                                               
Collections.singletonList("NEW"))
+                                       .build();
+
+        this.service.update(this.client, account);
+
+        Mockito.verify(this.auth).addSpaceAdmin("user", "NEW");
+        Mockito.verify(this.auth).delSpaceAdmin("user", "OLD");
+        Mockito.verify(this.auth).delSuperAdmin("user");
+        Mockito.verify(this.graphSpaceUsers, Mockito.never())
+               .applySpacePreset(Mockito.any(), Mockito.anyString(),
+                                 Mockito.anyString(), Mockito.anyString());
+    }
+
+    @Test
+    public void testPdUserListReportsPresetAndCustomRoleState() {
+        
Mockito.when(this.config.get(HubbleOptions.PD_ENABLED)).thenReturn(true);
+        Mockito.when(this.client.supportsDefaultRole()).thenReturn(true);
+        Mockito.when(this.auth.listUsers())
+               .thenReturn(Collections.singletonList(user("alice")));
+        Mockito.when(this.graphSpace.listGraphSpace())
+               .thenReturn(Arrays.asList("ADMIN", "WRITE", "READ", "CUSTOM"));
+        Mockito.when(this.auth.listSpaceAdmin("ADMIN"))
+               .thenReturn(Collections.singletonList("alice"));
+        Mockito.when(this.auth.listSpaceAdmin("WRITE"))
+               .thenReturn(Collections.emptyList());
+        Mockito.when(this.auth.listSpaceAdmin("READ"))
+               .thenReturn(Collections.emptyList());
+        Mockito.when(this.auth.listSpaceAdmin("CUSTOM"))
+               .thenReturn(Collections.emptyList());
+        Mockito.when(this.graphSpace.checkDefaultRole(
+                "WRITE", "alice", "analyst")).thenReturn(true);
+        Mockito.when(this.graphSpace.checkDefaultRole(
+                "READ", "alice", "observer")).thenReturn(true);
+        Mockito.when(this.graphSpaceUsers.hasCustomRoles(
+                this.client, "CUSTOM", "alice")).thenReturn(true);
+
+        UserEntity account = this.service.listUsers(this.client).get(0);
+
+        Assert.assertEquals(Integer.valueOf(1), account.getSpacenum());
+        Assert.assertEquals(Collections.singletonList("ADMIN"),
+                            account.getAdminSpaces());
+        Assert.assertEquals(3, account.getGraphspacePermissions().size());
+        Assert.assertEquals("GS_ADMIN",
+                            account.getGraphspacePermissions().get(0)
+                                   .get("permission_preset"));
+        Assert.assertEquals("GS_READ_WRITE",
+                            account.getGraphspacePermissions().get(1)
+                                   .get("permission_preset"));
+        Assert.assertEquals("GS_READ_ONLY",
+                            account.getGraphspacePermissions().get(2)
+                                   .get("permission_preset"));
+        Assert.assertEquals("LEGACY_CUSTOM", account.getPermissionPreset());
+    }
+
     @Test
     public void testLegacyUserCreationSkipsPermissionPresetApis() {
         
Mockito.when(this.config.get(HubbleOptions.PD_ENABLED)).thenReturn(true);
diff --git a/hugegraph-hubble/hubble-fe/src/api/auth.js 
b/hugegraph-hubble/hubble-fe/src/api/auth.js
index 541c099f8..5b7075c89 100644
--- a/hugegraph-hubble/hubble-fe/src/api/auth.js
+++ b/hugegraph-hubble/hubble-fe/src/api/auth.js
@@ -98,18 +98,15 @@ const getSpaceMembers = (graphspace, params, config = {}) 
=> {
 };
 
 const getSpaceAdmins = (graphspace, params, config = {}) => {
-    return request.get(scopedAuthPath(graphspace, 'users/spaceadmin'),
-        {...config, params});
+    return request.get(scopedAuthPath(graphspace, 'users/spaceadmin'), 
{...config, params});
 };
 
 const setSpaceAdmin = (graphspace, id, config) => {
-    return request.post(scopedAuthPath(graphspace, 'users/spaceadmin', id),
-        undefined, config);
+    return request.post(scopedAuthPath(graphspace, 'users/spaceadmin', id), 
undefined, config);
 };
 
 const removeSpaceAdmin = (graphspace, id, config) => {
-    return request.delete(scopedAuthPath(graphspace, 'users/spaceadmin', id),
-        undefined, config);
+    return request.delete(scopedAuthPath(graphspace, 'users/spaceadmin', id), 
undefined, config);
 };
 
 const setSpacePreset = (graphspace, id, preset, config) => {
diff --git 
a/hugegraph-hubble/hubble-fe/src/modules/navigation/ConsoleItem/index.js 
b/hugegraph-hubble/hubble-fe/src/modules/navigation/ConsoleItem/index.js
index fc47de692..18ebe299c 100644
--- a/hugegraph-hubble/hubble-fe/src/modules/navigation/ConsoleItem/index.js
+++ b/hugegraph-hubble/hubble-fe/src/modules/navigation/ConsoleItem/index.js
@@ -105,8 +105,7 @@ const ConsoleItem = ({embedded = false}) => {
             ? () => openDashboard(dashboard.url + path)
             : undefined,
     });
-    const nativeItem = (titleKey, path, required, modeAvailable = true,
-        modeReason = '') => {
+    const nativeItem = (titleKey, path, required, modeAvailable = true, 
modeReason = '') => {
         const available = modeAvailable && capabilities.includes(required);
         const disabled = capabilitiesLoading || Boolean(capabilitiesError) || 
!available;
         return {
diff --git a/hugegraph-hubble/hubble-fe/src/pages/Account/EditLayer.js 
b/hugegraph-hubble/hubble-fe/src/pages/Account/EditLayer.js
index 9d957fe21..8e25be843 100644
--- a/hugegraph-hubble/hubble-fe/src/pages/Account/EditLayer.js
+++ b/hugegraph-hubble/hubble-fe/src/pages/Account/EditLayer.js
@@ -36,8 +36,7 @@ import {
 const PAGE_ERROR_CONFIG = {suppressBusinessErrorToast: true};
 const DEFAULT_ALLOWED_OPERATIONS = {create: true, edit: true, auth: true};
 const PRESERVE_PERMISSIONS = 'PRESERVE_PERMISSIONS';
-const permissionPresetChanged = (prev, next) => (prev.permission_preset !== 
next.permission_preset
-);
+const permissionPresetChanged = (prev, next) => prev.permission_preset !== 
next.permission_preset;
 const toProfilePayload = values => ({
     user_name: values.user_name,
     user_nickname: values.user_nickname,
@@ -107,8 +106,7 @@ const EditLayer = ({
                         && values.permission_preset !== PRESERVE_PERMISSIONS
             ? toPermissionPayload(values)
             : toProfilePayload(values);
-        return api.auth.updateUser(data.id, payload, PAGE_ERROR_CONFIG
-        ).then(res => {
+        return api.auth.updateUser(data.id, payload, 
PAGE_ERROR_CONFIG).then(res => {
             if (res.status === 200) {
                 message.success(t('common.msg.update_success'));
                 onCancel();
@@ -126,8 +124,7 @@ const EditLayer = ({
             ...values,
             permission_preset: PERMISSION_PRESETS.GS_ADMIN,
         });
-        return api.auth.updateAdminspace(data.id, payload.adminSpaces, 
PAGE_ERROR_CONFIG
-        ).then(res => {
+        return api.auth.updateAdminspace(data.id, payload.adminSpaces, 
PAGE_ERROR_CONFIG).then(res => {
             if (res.status === 200) {
                 message.success(t('common.msg.set_success'));
                 onCancel();
@@ -226,8 +223,7 @@ const EditLayer = ({
                     if (op !== 'detail') {
                         form.setFieldsValue({
                             ...res.data,
-                            permission_preset: getAccountPreset(res.data)
-                                               ?? PRESERVE_PERMISSIONS,
+                            permission_preset: getAccountPreset(res.data) ?? 
PRESERVE_PERMISSIONS,
                             graphspaces: op === 'auth'
                                 ? (res.data?.adminSpaces ?? [])
                                 : getPresetSpaces(res.data),
diff --git a/hugegraph-hubble/hubble-fe/src/pages/Account/SpaceAccess.js 
b/hugegraph-hubble/hubble-fe/src/pages/Account/SpaceAccess.js
index be575f469..8dd3fa1b0 100644
--- a/hugegraph-hubble/hubble-fe/src/pages/Account/SpaceAccess.js
+++ b/hugegraph-hubble/hubble-fe/src/pages/Account/SpaceAccess.js
@@ -85,8 +85,7 @@ const rolesPreset = roles => {
     if (presets.some(preset => preset === null)) {
         return null;
     }
-    if (values.some(role => (role?.permission_preset ?? role?.permissionPreset)
-                            === PERMISSION_PRESETS.GS_ADMIN)) {
+    if (values.some(role => (role?.permission_preset ?? 
role?.permissionPreset) === PERMISSION_PRESETS.GS_ADMIN)) {
         return PERMISSION_PRESETS.GS_ADMIN;
     }
     return values.length > 0 && presets.every(Boolean) && new 
Set(presets).size === 1 ? presets[0] : null;
diff --git a/hugegraph-hubble/hubble-fe/src/utils/rules.test.js 
b/hugegraph-hubble/hubble-fe/src/utils/rules.test.js
index 662d39e0a..3ff86a5d8 100644
--- a/hugegraph-hubble/hubble-fe/src/utils/rules.test.js
+++ b/hugegraph-hubble/hubble-fe/src/utils/rules.test.js
@@ -56,8 +56,8 @@ jest.mock('../i18n', () => ({
                 'common.validation.jdbc_rule':
                     '请输入正确的jdbc url, 例如:jdbc:mysql://127.0.0.1:3306/db_name',
                 'common.validation.account_name_rule':
-                    '使用 1–16 个不含空格的字符,支持中文/东亚兼容字符、字母、数字和下划线;'
-                    + '下划线不能位于首尾',
+                    '使用 1–16 个不含空格的字符,支持中文/东亚兼容字符、字母、数字和'
+                    + '下划线;下划线不能位于首尾',
                 'common.validation.favorite_name_rule': '只能包含中文、字母、数字、_, 
不能超过48个字符',
                 'common.validation.invalid_data_format': '非法的数据格式',
             },
@@ -194,8 +194,8 @@ describe('rules i18n defaults', () => {
             [rules.isJDBC(), '请输入正确的jdbc url, 
例如:jdbc:mysql://127.0.0.1:3306/db_name'],
             [
                 rules.isAccountName(),
-                '使用 1–16 个不含空格的字符,支持中文/东亚兼容字符、字母、数字和下划线;'
-                + '下划线不能位于首尾',
+                '使用 1–16 个不含空格的字符,支持中文/东亚兼容字符、字母、数字和'
+                + '下划线;下划线不能位于首尾',
             ],
             [rules.isFavoriteName(), '只能包含中文、字母、数字、_, 不能超过48个字符'],
             [rules.isUUID(), '非法的数据格式'],

Reply via email to