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 57dbc8606c813bd57c8cee6f8844cd01732b058b
Author: dark <[email protected]>
AuthorDate: Sat Aug 29 22:07:03 2026 +0800

    fix(hubble): align operations and auth health
    
    - align and summarize operations source status
    - preserve authentication recovery flows
    - periodically revalidate verified authentication mode
---
 .../service/op/LiveOperationsCollector.java        |  58 ++++++++++-
 .../service/op/LiveOperationsCollectorTest.java    |  57 ++++++++++
 hugegraph-hubble/hubble-fe/src/App.js              |  20 +++-
 hugegraph-hubble/hubble-fe/src/App.test.js         |  92 +++++++++++++++++
 .../src/i18n/resources/en-US/modules/pages.json    |   6 ++
 .../src/i18n/resources/zh-CN/modules/pages.json    |   6 ++
 .../src/pages/Operations/Overview.test.js          |  13 ++-
 .../hubble-fe/src/pages/Operations/components.js   | 115 +++++++++++++++------
 .../src/pages/Operations/components.test.js        |  11 +-
 .../hubble-fe/src/pages/Operations/operations.scss |  18 ++++
 .../hubble-fe/src/pages/Schema/index.js            |   5 +-
 .../src/pages/Schema/schema-list-recovery.test.js  |  25 +++++
 12 files changed, 379 insertions(+), 47 deletions(-)

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 98fa3a3fb..c21f1be62 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
@@ -259,7 +259,10 @@ public class LiveOperationsCollector implements 
OperationsCollector {
                                                               now);
                 this.mergeNodes(nodes, topology.getNodes());
                 facts.putAll(topology.getFacts());
-                pdStatus = available(topology.getStatus(), now,
+                pdStatus = available(this.moreSevereStatus(
+                                     topology.getStatus(),
+                                     this.nodeStatus(topology.getNodes(), 
"PD")),
+                                     now,
                                      topology.getReason());
                 clusterParsed = true;
             } catch (MalformedUpstreamException e) {
@@ -272,6 +275,9 @@ public class LiveOperationsCollector implements 
OperationsCollector {
                                                               stores, now);
                 this.mergeNodes(nodes, topology.getNodes());
                 facts.putAll(topology.getFacts());
+                storesStatus = available(this.nodeStatus(
+                                         topology.getNodes(), "STORE"), now);
+                this.reconcileStoreFacts(nodes, facts);
                 storesParsed = true;
             } catch (MalformedUpstreamException e) {
                 storesStatus = malformed(now);
@@ -291,6 +297,56 @@ public class LiveOperationsCollector implements 
OperationsCollector {
         sources.put("stores", storesStatus);
     }
 
+    private String nodeStatus(List<Node> nodes, String type) {
+        long total = nodes.stream()
+                          .filter(node -> type.equals(node.getType()))
+                          .count();
+        long up = nodes.stream()
+                       .filter(node -> type.equals(node.getType()))
+                       .filter(node -> "UP".equals(node.getStatus()))
+                       .count();
+        long down = nodes.stream()
+                         .filter(node -> type.equals(node.getType()))
+                         .filter(node -> "DOWN".equals(node.getStatus()))
+                         .count();
+        if (total == 0L) {
+            return "UNKNOWN";
+        }
+        if (up == total) {
+            return "UP";
+        }
+        if (down == total) {
+            return "DOWN";
+        }
+        return "DEGRADED";
+    }
+
+    private String moreSevereStatus(String first, String second) {
+        if ("DOWN".equals(first) || "DOWN".equals(second)) {
+            return "DOWN";
+        }
+        if ("DEGRADED".equals(first) || "DEGRADED".equals(second)) {
+            return "DEGRADED";
+        }
+        if ("UNKNOWN".equals(first) || "UNKNOWN".equals(second)) {
+            return "UNKNOWN";
+        }
+        return "UP";
+    }
+
+    private void reconcileStoreFacts(List<Node> nodes,
+                                     Map<String, Long> facts) {
+        long stores = nodes.stream()
+                           .filter(node -> "STORE".equals(node.getType()))
+                           .count();
+        long storesUp = nodes.stream()
+                             .filter(node -> "STORE".equals(node.getType()))
+                             .filter(node -> "UP".equals(node.getStatus()))
+                             .count();
+        facts.put("stores", stores);
+        facts.put("stores_up", storesUp);
+    }
+
     private void mergeNodes(List<Node> nodes, List<Node> additions) {
         for (Node addition : additions) {
             int existingIndex = -1;
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 dfab65386..91160cbc9 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
@@ -202,6 +202,63 @@ public class LiveOperationsCollectorTest {
                                   .allMatch(node -> 
"UP".equals(node.getStatus())));
     }
 
+    @Test
+    public void testOfflinePdDegradesPdSourceWhileClusterHasQuorum()
+           throws IOException {
+        String cluster = "{\"status\":0,\"data\":{" +
+                         "\"state\":\"Cluster_OK\"," +
+                         "\"pdList\":[" +
+                         "{\"restUrl\":\"http://pd-1:8620\","; +
+                         "\"state\":\"Up\",\"role\":\"Leader\"}," +
+                         "{\"raftUrl\":\"pd-2:8610\"," +
+                         "\"state\":\"Offline\",\"role\":\"Follower\"}]," +
+                         "\"pdLeader\":{\"restUrl\":\"http://pd-1:8620\","; +
+                         "\"state\":\"Up\",\"role\":\"Leader\"}}}";
+        HttpServer pd = pdServer(200, cluster, 200, stores());
+        Snapshot snapshot;
+        try {
+            snapshot = collector(true, pd).collect(serverClient(), false);
+        } finally {
+            pd.stop(0);
+        }
+
+        Assert.assertEquals("DEGRADED", snapshot.getStatus());
+        Assert.assertEquals("DEGRADED",
+                            snapshot.getSources().get("pd").getStatus());
+        Assert.assertEquals(1L, snapshot.getNodes().stream()
+                .filter(node -> "PD".equals(node.getType()))
+                .filter(node -> "DOWN".equals(node.getStatus()))
+                .count());
+    }
+
+    @Test
+    public void testOfflineStoreDegradesStoreSource() throws IOException {
+        String stores = "{\"status\":0,\"data\":{\"stores\":[" +
+                        "{\"storeId\":\"1\",\"address\":\"store-1:8500\"," +
+                        "\"state\":\"Up\"}," +
+                        "{\"storeId\":\"2\",\"address\":\"store-2:8500\"," +
+                        "\"state\":\"Offline\"}]}}";
+        HttpServer pd = pdServer(200, cluster(), 200, stores);
+        Snapshot snapshot;
+        try {
+            snapshot = collector(true, pd).collect(serverClient(), false);
+        } finally {
+            pd.stop(0);
+        }
+
+        Assert.assertEquals("DEGRADED", snapshot.getStatus());
+        Assert.assertEquals("DEGRADED",
+                            snapshot.getSources().get("stores").getStatus());
+        Assert.assertEquals(Long.valueOf(2L),
+                            snapshot.getFacts().get("stores"));
+        Assert.assertEquals(Long.valueOf(1L),
+                            snapshot.getFacts().get("stores_up"));
+        Assert.assertEquals(1L, snapshot.getNodes().stream()
+                .filter(node -> "STORE".equals(node.getType()))
+                .filter(node -> "DOWN".equals(node.getStatus()))
+                .count());
+    }
+
     @Test
     public void testPdNotReadyReasonSurvivesMetricsFailure() throws 
IOException {
         String notReady = cluster().replace("Cluster_OK", "Cluster_Not_Ready");
diff --git a/hugegraph-hubble/hubble-fe/src/App.js 
b/hugegraph-hubble/hubble-fe/src/App.js
index e08c9a6c7..6ea227328 100644
--- a/hugegraph-hubble/hubble-fe/src/App.js
+++ b/hugegraph-hubble/hubble-fe/src/App.js
@@ -28,6 +28,7 @@ import {setConfig} from './utils/config';
 import {useEffect, useState} from 'react';
 
 const CONFIG_RETRY_DELAY_MS = 2000;
+const CONFIG_REVALIDATE_DELAY_MS = 30_000;
 
 function App() {
     const [configReady, setConfigReady] = useState(false);
@@ -36,6 +37,7 @@ function App() {
 
     useEffect(() => {
         let active = true;
+        let hasSafeConfig = false;
         let retryTimer;
         const loadConfig = () => {
             api.config.getConfig().then(response => {
@@ -43,19 +45,27 @@ function App() {
                     throw new Error('invalid_hubble_config');
                 }
                 if (active) {
+                    hasSafeConfig = true;
                     setConfig(response.data);
                     setConfigRevision(value => value + 1);
                     setConfigReady(true);
                     setConfigError(false);
-                    if (response.data.server_capabilities_verified === false) {
+                    const unverified = response.data
+                        .server_capabilities_verified === false;
+                    const delay = unverified
+                        ? CONFIG_RETRY_DELAY_MS : CONFIG_REVALIDATE_DELAY_MS;
+                    retryTimer = window.setTimeout(loadConfig, delay);
+                }
+            }).catch(() => {
+                if (active) {
+                    if (hasSafeConfig) {
                         retryTimer = window.setTimeout(
                             loadConfig, CONFIG_RETRY_DELAY_MS
                         );
                     }
-                }
-            }).catch(() => {
-                if (active) {
-                    setConfigError(true);
+                    else {
+                        setConfigError(true);
+                    }
                 }
             });
         };
diff --git a/hugegraph-hubble/hubble-fe/src/App.test.js 
b/hugegraph-hubble/hubble-fe/src/App.test.js
index 9c0e69361..5407c8b60 100644
--- a/hugegraph-hubble/hubble-fe/src/App.test.js
+++ b/hugegraph-hubble/hubble-fe/src/App.test.js
@@ -122,3 +122,95 @@ test('revalidates fail-closed server capabilities until 
verified', async () => {
         .toHaveAttribute('data-auth-enabled', 'false');
     jest.useRealTimers();
 });
+
+test('continues revalidation after a transient retry failure', async () => {
+    jest.useFakeTimers();
+    api.config.getConfig
+        .mockResolvedValueOnce({
+            status: 200,
+            data: {
+                pd_enabled: true,
+                auth_enabled: true,
+                server_capabilities_verified: false,
+            },
+        })
+        .mockRejectedValueOnce(new Error('temporarily unavailable'))
+        .mockResolvedValueOnce({
+            status: 200,
+            data: {
+                pd_enabled: true,
+                auth_enabled: false,
+                server_capabilities_verified: true,
+            },
+        });
+
+    render(
+        <MemoryRouter
+            future={{v7_startTransition: true, v7_relativeSplatPath: true}}
+        >
+            <App />
+        </MemoryRouter>
+    );
+    expect(await screen.findByTestId('app-route')).toHaveAttribute(
+        'data-auth-enabled', 'true'
+    );
+
+    await act(async () => {
+        jest.advanceTimersByTime(2000);
+    });
+    await waitFor(() => expect(api.config.getConfig).toHaveBeenCalledTimes(2));
+    expect(screen.queryByRole('alert')).not.toBeInTheDocument();
+    expect(screen.getByTestId('app-route')).toHaveAttribute(
+        'data-auth-enabled', 'true'
+    );
+
+    await act(async () => {
+        jest.advanceTimersByTime(2000);
+    });
+    await waitFor(() => expect(api.config.getConfig).toHaveBeenCalledTimes(3));
+    expect(screen.getByTestId('app-route')).toHaveAttribute(
+        'data-auth-enabled', 'false'
+    );
+    jest.useRealTimers();
+});
+
+test('periodically revalidates a verified authentication mode', async () => {
+    jest.useFakeTimers();
+    api.config.getConfig
+        .mockResolvedValueOnce({
+            status: 200,
+            data: {
+                pd_enabled: true,
+                auth_enabled: false,
+                server_capabilities_verified: true,
+            },
+        })
+        .mockResolvedValueOnce({
+            status: 200,
+            data: {
+                pd_enabled: true,
+                auth_enabled: true,
+                server_capabilities_verified: true,
+            },
+        });
+
+    render(
+        <MemoryRouter
+            future={{v7_startTransition: true, v7_relativeSplatPath: true}}
+        >
+            <App />
+        </MemoryRouter>
+    );
+    expect(await screen.findByTestId('app-route')).toHaveAttribute(
+        'data-auth-enabled', 'false'
+    );
+
+    await act(async () => {
+        jest.advanceTimersByTime(30_000);
+    });
+    await waitFor(() => expect(api.config.getConfig).toHaveBeenCalledTimes(2));
+    expect(screen.getByTestId('app-route')).toHaveAttribute(
+        'data-auth-enabled', 'true'
+    );
+    jest.useRealTimers();
+});
diff --git 
a/hugegraph-hubble/hubble-fe/src/i18n/resources/en-US/modules/pages.json 
b/hugegraph-hubble/hubble-fe/src/i18n/resources/en-US/modules/pages.json
index d5bd10cfc..4fd812f84 100644
--- a/hugegraph-hubble/hubble-fe/src/i18n/resources/en-US/modules/pages.json
+++ b/hugegraph-hubble/hubble-fe/src/i18n/resources/en-US/modules/pages.json
@@ -1357,6 +1357,12 @@
     "availability_unsupported": "Unsupported",
     "availability_not_applicable": "Not applicable",
     "availability_malformed": "Malformed",
+    "source_health_normal": "Normal",
+    "source_health_partial": "Abnormal - Partially available",
+    "source_health_unavailable": "Abnormal - Unavailable",
+    "source_health_not_applicable": "Not applicable in this deployment",
+    "source_topology_status": "Node status",
+    "source_collection_status": "Metrics collection",
     "metric_scope_store_only": "{{metric}} metrics are collected from Store 
nodes, not {{nodeType}} nodes.",
     "metric_scope_backend": "Backend metrics are provided by Server and Store 
nodes, not {{nodeType}} nodes.",
     "reason_upstream_timeout": "Upstream timed out",
diff --git 
a/hugegraph-hubble/hubble-fe/src/i18n/resources/zh-CN/modules/pages.json 
b/hugegraph-hubble/hubble-fe/src/i18n/resources/zh-CN/modules/pages.json
index e9bf99f62..e6ea0732b 100644
--- a/hugegraph-hubble/hubble-fe/src/i18n/resources/zh-CN/modules/pages.json
+++ b/hugegraph-hubble/hubble-fe/src/i18n/resources/zh-CN/modules/pages.json
@@ -1357,6 +1357,12 @@
     "availability_unsupported": "不支持",
     "availability_not_applicable": "不适用",
     "availability_malformed": "响应异常",
+    "source_health_normal": "正常",
+    "source_health_partial": "异常 - 部分可用",
+    "source_health_unavailable": "异常 - 不可用",
+    "source_health_not_applicable": "当前部署不适用",
+    "source_topology_status": "节点状态",
+    "source_collection_status": "指标采集",
     "metric_scope_store_only": "{{metric}}指标从 Store 节点采集,不会在 {{nodeType}} 
节点采集。",
     "metric_scope_backend": "后端指标由 Server 和 Store 节点提供,不会在 {{nodeType}} 节点采集。",
     "reason_upstream_timeout": "上游超时",
diff --git a/hugegraph-hubble/hubble-fe/src/pages/Operations/Overview.test.js 
b/hugegraph-hubble/hubble-fe/src/pages/Operations/Overview.test.js
index 268ab9652..c3fe5fa9b 100644
--- a/hugegraph-hubble/hubble-fe/src/pages/Operations/Overview.test.js
+++ b/hugegraph-hubble/hubble-fe/src/pages/Operations/Overview.test.js
@@ -80,8 +80,10 @@ test('keeps unknown and partial source states explicit', 
async () => {
     expect(await screen.findByText('Attention')).toBeInTheDocument();
     expect(screen.getByRole('radiogroup', {name: 'Overview view'}))
         .toBeInTheDocument();
-    expect(screen.getByText('Malformed')).toBeInTheDocument();
-    expect(screen.getByText('Unsupported')).toBeInTheDocument();
+    expect(screen.getByLabelText(/PD.*Abnormal - Unavailable.*Malformed/))
+        .toBeInTheDocument();
+    expect(screen.getByLabelText(/Store.*Not applicable in this deployment/))
+        .toBeInTheDocument();
     expect(screen.getByText(/stale/i)).toBeInTheDocument();
 });
 
@@ -448,7 +450,8 @@ test('keeps every failed source visible when the whole 
cluster is down', async (
 
     const sources = await screen.findByRole('region', {name: 'Source 
freshness'});
     expect(within(sources).getAllByText('DOWN')).toHaveLength(3);
-    expect(within(sources).getAllByText('Unavailable')).toHaveLength(3);
+    expect(within(sources).getAllByLabelText(/Abnormal - Unavailable/))
+        .toHaveLength(3);
     expect(screen.getByRole('region', {name: 'Items needing attention'}))
         .toBeInTheDocument();
 });
@@ -478,8 +481,10 @@ test('keeps healthy freshness compact but preserves 
stale-source recovery contex
     renderOverview();
 
     const sources = await screen.findByRole('region', {name: 'Source 
freshness'});
-    expect(within(sources).getAllByText(/Last success/)).toHaveLength(1);
+    expect(within(sources).queryByText(/Last 
success/)).not.toBeInTheDocument();
     expect(within(sources).getByText(/Stale/)).toBeInTheDocument();
+    expect(within(sources).getByLabelText(/Stale.*Last success/))
+        .toBeInTheDocument();
 });
 
 test('shows a concise healthy state when no node needs attention', async () => 
{
diff --git a/hugegraph-hubble/hubble-fe/src/pages/Operations/components.js 
b/hugegraph-hubble/hubble-fe/src/pages/Operations/components.js
index 213151785..036d78954 100644
--- a/hugegraph-hubble/hubble-fe/src/pages/Operations/components.js
+++ b/hugegraph-hubble/hubble-fe/src/pages/Operations/components.js
@@ -65,6 +65,21 @@ const displayHealthStatus = (status, t) => (
     status === 'DEGRADED' ? t('operations.status_degraded') : status
 );
 
+const sourceHealthSummary = (source, t) => {
+    const status = source.status ?? 'UNKNOWN';
+    const availability = source.availability ?? 'UNSUPPORTED';
+    if (availability === 'UNSUPPORTED') {
+        return t('operations.source_health_not_applicable');
+    }
+    if (status === 'UP' && availability === 'AVAILABLE') {
+        return t('operations.source_health_normal');
+    }
+    if (status === 'DOWN' || ['UNAVAILABLE', 
'MALFORMED'].includes(availability)) {
+        return t('operations.source_health_unavailable');
+    }
+    return t('operations.source_health_partial');
+};
+
 const HealthStatus = ({status = 'UNKNOWN', reason, stale = false, size = 
'normal'}) => {
     const {t} = useTranslation();
     const normalized = STATUS_ICON[status] ? status : 'UNKNOWN';
@@ -124,36 +139,78 @@ const SourceStrip = ({sources = {}, detailed = false,
                     i18n.language,
                     t('operations.unavailable')
                 ) : null;
-                const showLastSuccess = detailed || source.stale
-                    || source.status !== 'UP' || source.availability !== 
'AVAILABLE';
+                const availability = t(`operations.availability_${(
+                    source.availability ?? 'UNSUPPORTED'
+                ).toLowerCase()}`, {
+                    defaultValue: source.availability ?? 'UNSUPPORTED',
+                });
+                const healthSummary = sourceHealthSummary(source, t);
+                const lastSuccess = source.last_success_at ? formatObservedAt(
+                    source.last_success_at,
+                    i18n.language,
+                    t('operations.unavailable')
+                ) : null;
+                const sourceDetails = [
+                    healthSummary,
+                    `${t('operations.source_topology_status')}: ${
+                        displayHealthStatus(source.status ?? 'UNKNOWN', t)
+                    }`,
+                    `${t('operations.source_collection_status')}: 
${availability}`,
+                    observed ? `${t('operations.observed_at')}: ${observed}` : 
null,
+                    source.stale ? t('operations.stale') : null,
+                    source.reason ? formatReason(source.reason, t) : null,
+                    lastSuccess
+                        ? `${t('operations.last_success')}: ${lastSuccess}` : 
null,
+                ].filter(Boolean).join(' · ');
+                const sourceLabel = displayNodeType(name === 'stores'
+                    ? 'STORE' : name.toUpperCase());
+                const sourceSummary = `${sourceLabel} ${
+                    displayHealthStatus(source.status ?? 'UNKNOWN', t)
+                } · ${sourceDetails}`;
                 return (
-                    <div className='operations-source' key={name}>
-                        <strong>
-                            {displayNodeType(name === 'stores'
-                                ? 'STORE' : name.toUpperCase())}
-                        </strong>
-                        <HealthStatus
-                            status={source.status}
-                            reason={source.reason}
-                        />
-                        <span className='operations-source-state'>
-                            {t(`operations.availability_${(
-                                source.availability ?? 'UNSUPPORTED'
-                            ).toLowerCase()}`, {defaultValue: 
source.availability ?? 'UNSUPPORTED'})}
-                            {detailed && observed
-                                ? ` · ${t('operations.observed_at')}: 
${observed}`
-                                : (age ? ` · ${age}` : '')}
-                            {source.stale ? ` · ${t('operations.stale')}` : ''}
-                            {source.reason ? ` · ${formatReason(source.reason, 
t)}` : ''}
-                            {showLastSuccess && source.last_success_at
-                                ? ` · ${t('operations.last_success')}: 
${formatObservedAt(
-                                    source.last_success_at,
-                                    i18n.language,
-                                    t('operations.unavailable')
-                                )}`
-                                : ''}
-                        </span>
-                    </div>
+                    <Tooltip key={name} title={detailed ? null : 
sourceDetails}>
+                        <div
+                            className='operations-source'
+                            tabIndex={detailed ? undefined : 0}
+                            aria-label={detailed ? undefined : sourceSummary}
+                        >
+                            <strong>{sourceLabel}</strong>
+                            <HealthStatus
+                                status={source.status}
+                                reason={detailed ? source.reason : undefined}
+                            />
+                            {!detailed && (
+                                <span className='operations-source-summary'>
+                                    {healthSummary}
+                                    {age ? ` · ${age}` : ''}
+                                    {source.stale
+                                        ? ` · ${t('operations.stale')}` : ''}
+                                </span>
+                            )}
+                            {detailed && (
+                                <span className='operations-source-state'>
+                                    {healthSummary}
+                                    {` · 
${t('operations.source_topology_status')}: ${
+                                        displayHealthStatus(
+                                            source.status ?? 'UNKNOWN', t
+                                        )
+                                    }`}
+                                    {` · 
${t('operations.source_collection_status')}: ${
+                                        availability
+                                    }`}
+                                    {observed
+                                        ? ` · ${t('operations.observed_at')}: 
${observed}`
+                                        : (age ? ` · ${age}` : '')}
+                                    {source.stale ? ` · 
${t('operations.stale')}` : ''}
+                                    {source.reason
+                                        ? ` · ${formatReason(source.reason, 
t)}` : ''}
+                                    {lastSuccess
+                                        ? ` · ${t('operations.last_success')}: 
${lastSuccess}`
+                                        : ''}
+                                </span>
+                            )}
+                        </div>
+                    </Tooltip>
                 );
             })}
         </section>
diff --git a/hugegraph-hubble/hubble-fe/src/pages/Operations/components.test.js 
b/hugegraph-hubble/hubble-fe/src/pages/Operations/components.test.js
index 432f8e368..e3152cf89 100644
--- a/hugegraph-hubble/hubble-fe/src/pages/Operations/components.test.js
+++ b/hugegraph-hubble/hubble-fe/src/pages/Operations/components.test.js
@@ -53,12 +53,11 @@ test('localizes the standalone deployment reason code', 
async () => {
         />
     );
 
-    expect(screen.getByText(/当前部署模式不支持/)).toBeInTheDocument();
-    expect(screen.queryByText(/deployment mode 
unsupported/)).not.toBeInTheDocument();
-    const statusInfo = screen.getAllByRole('img', {name: /当前无法确认/})
-        .find(element => 
element.getAttribute('aria-label').includes('当前部署模式不支持'));
-    expect(statusInfo).toHaveAccessibleName(/当前部署模式不支持/);
-    expect(statusInfo).not.toHaveAccessibleName(/观测时间|最近成功/);
+    expect(screen.queryByText(/当前部署模式不支持/)).not.toBeInTheDocument();
+    expect(screen.getByLabelText(/PD.*当前部署不适用.*当前部署模式不支持/))
+        .toHaveAccessibleName(/当前部署模式不支持/);
+    expect(screen.queryByText(/deployment mode unsupported/))
+        .not.toBeInTheDocument();
 });
 
 test('uses a concise Attention label and explains the degraded state', () => {
diff --git a/hugegraph-hubble/hubble-fe/src/pages/Operations/operations.scss 
b/hugegraph-hubble/hubble-fe/src/pages/Operations/operations.scss
index c95a6f7dd..13232b3e9 100644
--- a/hugegraph-hubble/hubble-fe/src/pages/Operations/operations.scss
+++ b/hugegraph-hubble/hubble-fe/src/pages/Operations/operations.scss
@@ -184,6 +184,16 @@
     text-align: right;
 }
 
+.operations-source-summary {
+    min-width: 0;
+    margin-left: auto;
+    overflow: hidden;
+    color: var(--workbench-color-text-tertiary);
+    font-size: 12px;
+    text-overflow: ellipsis;
+    white-space: nowrap;
+}
+
 .operations-source-strip.is-detailed .operations-source {
     align-items: flex-start;
     flex-wrap: wrap;
@@ -335,6 +345,14 @@
 
     .operations-health {
         grid-column: 2;
+        min-width: 0;
+        flex-wrap: wrap;
+        row-gap: 2px;
+        white-space: normal;
+
+        .operations-health-stale {
+            white-space: nowrap;
+        }
     }
 }
 
diff --git a/hugegraph-hubble/hubble-fe/src/pages/Schema/index.js 
b/hugegraph-hubble/hubble-fe/src/pages/Schema/index.js
index c6514214f..59b49307b 100644
--- a/hugegraph-hubble/hubble-fe/src/pages/Schema/index.js
+++ b/hugegraph-hubble/hubble-fe/src/pages/Schema/index.js
@@ -111,6 +111,7 @@ const Schema = () => {
     const {canManage, canWrite} = useGraphspaceAccess(graphspace);
     const {context: authContext} = useAuthContext();
     const username = authContext?.username;
+    const anonymous = authContext?.mode === 'NON_AUTH';
     const templateWriteEnabled = pdMode && canWrite;
     const workbenchContext = readWorkbenchGraphContext();
     const currentGraph = workbenchContext.graphspace === graphspace
@@ -124,8 +125,8 @@ const Schema = () => {
     const listKey = JSON.stringify([graphspace, query, current]);
 
     const canEditTemplate = useCallback(row => (
-        canManage || (canWrite && row.creator === username)
-    ), [canManage, canWrite, username]);
+        canManage || (canWrite && (anonymous || row.creator === username))
+    ), [anonymous, canManage, canWrite, username]);
 
     const editSchema = useCallback(data => {
         if (!canEditTemplate(data)) {
diff --git 
a/hugegraph-hubble/hubble-fe/src/pages/Schema/schema-list-recovery.test.js 
b/hugegraph-hubble/hubble-fe/src/pages/Schema/schema-list-recovery.test.js
index e1dce7c69..7230ed522 100644
--- a/hugegraph-hubble/hubble-fe/src/pages/Schema/schema-list-recovery.test.js
+++ b/hugegraph-hubble/hubble-fe/src/pages/Schema/schema-list-recovery.test.js
@@ -239,6 +239,31 @@ it('lets read-write users manage only their own saved 
templates', async () => {
         .not.toBeInTheDocument();
 });
 
+it('lets anonymous PD users manage saved templates', async () => {
+    mockAccess = {canManage: false, canWrite: true};
+    mockAuthContext = {context: {mode: 'NON_AUTH'}};
+    api.manage.getGraphSpace.mockResolvedValue({
+        status: 200,
+        data: {nickname: 'Space'},
+    });
+    api.manage.getSchemaList.mockResolvedValue({
+        status: 200,
+        data: {
+            records: [{name: 'anonymous_schema', creator: null}],
+            total: 1,
+        },
+    });
+
+    render(<Schema />);
+
+    expect(await screen.findByRole('button', {name: 'Create template'}))
+        .toBeInTheDocument();
+    expect(screen.getByRole('button', {name: 'Edit anonymous_schema'}))
+        .toBeInTheDocument();
+    expect(screen.getByRole('button', {name: 'Delete anonymous_schema'}))
+        .toBeInTheDocument();
+});
+
 it('links a non-PD read-only library without graph context to the graph 
overview', async () => {
     mockGraphspace = 'DEFAULT';
     mockPdEnabled = false;

Reply via email to