chihsuan commented on code in PR #10900:
URL: https://github.com/apache/ozone/pull/10900#discussion_r3888259256


##########
ozone-ui/packages/om/src/pages/Overview/sections/JvmSection.tsx:
##########
@@ -0,0 +1,253 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import React, { Suspense, useMemo, useState } from 'react';
+import {
+  Button,
+  Dropdown,
+  Empty,
+  message,
+  Skeleton,
+  type MenuProps,
+  type TableColumnsType,
+} from 'antd';
+import { DownOutlined } from '@ant-design/icons';
+import { Card, Chip, DataTable, Icon, KeyValuePair, Section, SearchInput } 
from '@ozone-ui/shared';
+import {
+  JMX_QUERY,
+  buildJvmHighlights,
+  parseJvmArguments,
+  toSystemPropertyRows,
+  type JvmParameter,
+  type JvmParameterCategory,
+  type RuntimeBean,
+} from '../../../api/overview';
+import { useSuspenseJmxBean } from '../../../api/useJmx';
+
+const highlightsGridStyle: React.CSSProperties = {
+  display: 'grid',
+  gridTemplateColumns: 'repeat(auto-fill, minmax(220px, 1fr))',
+  gap: '16px 24px',
+};
+
+const categoryColor: Record<JvmParameterCategory, 'blue' | 'orange' | 
'neutral'> = {
+  'System & Framework': 'blue',
+  'Memory & GC': 'orange',
+  'System Property': 'neutral',
+};
+
+const monospace: React.CSSProperties = {
+  fontFamily: "'Roboto Mono', monospace",
+  fontSize: 12,
+};
+
+const escapeXml = (s: string) =>
+  s.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, 
'&gt;').replace(/"/g, '&quot;');
+
+/** Render parameter rows as a Hadoop-style XML configuration snippet. */
+const buildConfigXml = (params: JvmParameter[]): string => {
+  const body = params
+    .map(
+      (p) =>
+        `  <property>\n    <name>${escapeXml(p.parameter)}</name>\n    
<value>${escapeXml(
+          p.value
+        )}</value>\n  </property>`
+    )
+    .join('\n');
+  return `<configuration>\n${body}\n</configuration>`;
+};
+
+const columns: TableColumnsType<JvmParameter> = [
+  {
+    title: 'Parameter',
+    dataIndex: 'parameter',
+    key: 'parameter',
+    width: '34%',
+    ellipsis: true,
+    render: (parameter: string) => <span style={monospace}>{parameter}</span>,
+  },
+  {
+    title: 'Value',
+    dataIndex: 'value',
+    key: 'value',
+    width: '40%',
+    ellipsis: true,
+    render: (value: string) => <span style={monospace}>{value}</span>,
+  },
+  {
+    title: 'Category',
+    dataIndex: 'category',
+    key: 'category',
+    width: '26%',
+    render: (category: JvmParameterCategory) => (
+      <Chip color={categoryColor[category]} size="small">
+        {category}
+      </Chip>
+    ),
+  },
+];
+
+const JvmContent: React.FC = () => {
+  const { data: runtime, isEmpty } = 
useSuspenseJmxBean<RuntimeBean>(JMX_QUERY.runtime);
+
+  const [search, setSearch] = useState('');
+  const [category, setCategory] = useState<'All' | 
JvmParameterCategory>('All');
+  const [showModules, setShowModules] = useState(false);
+  const [selectedRowKeys, setSelectedRowKeys] = useState<React.Key[]>([]);
+
+  const highlights = useMemo(() => (runtime ? buildJvmHighlights(runtime) : 
[]), [runtime]);
+
+  const allRows = useMemo<JvmParameter[]>(() => {
+    if (!runtime) {
+      return [];
+    }
+    const args = parseJvmArguments(runtime.InputArguments);
+    return showModules ? [...args, 
...toSystemPropertyRows(runtime.SystemProperties)] : args;
+  }, [runtime, showModules]);
+
+  const rows = useMemo(() => {
+    const needle = search.trim().toLowerCase();
+    return allRows.filter((row) => {
+      if (category !== 'All' && row.category !== category) {
+        return false;
+      }
+      if (!needle) {
+        return true;
+      }
+      return (
+        row.parameter.toLowerCase().includes(needle) || 
row.value.toLowerCase().includes(needle)
+      );
+    });
+  }, [allRows, category, search]);
+
+  const categoryOptions = [
+    { label: 'All', value: 'All' },
+    { label: 'System & Framework', value: 'System & Framework' },
+    { label: 'Memory & GC', value: 'Memory & GC' },
+    ...(showModules ? [{ label: 'System Property', value: 'System Property' }] 
: []),
+  ];
+
+  const categoryMenu: MenuProps = {
+    items: categoryOptions.map((o) => ({ key: o.value, label: o.label })),
+    selectable: true,
+    selectedKeys: [category],
+    onClick: ({ key }) => setCategory(key as 'All' | JvmParameterCategory),
+  };
+
+  // Copy the selected rows (or all filtered rows when none are selected) as a
+  // Hadoop-style XML configuration snippet. Selection resolves against the 
full
+  // row set so it survives search/category filtering.
+  const copyArguments = async () => {
+    const chosen = selectedRowKeys.length
+      ? allRows.filter((r) => selectedRowKeys.includes(r.key))
+      : rows;
+    if (!chosen.length) {
+      return;
+    }
+    await navigator.clipboard.writeText(buildConfigXml(chosen));

Review Comment:
   Should we add error handling around `navigator.clipboard`? It only exists in 
a secure context, and the OM UI is served over plain HTTP in non-secure 
clusters. 



##########
ozone-ui/packages/shared/src/components/SyncChip/SyncChip.tsx:
##########
@@ -0,0 +1,249 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import React, { useState } from 'react';
+import { Dropdown, Switch, Tooltip, Typography } from 'antd';
+import { colors, radius, semanticColors, spacing, textStyles } from 
'../../theme/tokens';
+import { useSyncConfig } from '../../data/SyncConfigContext';
+import { fetchJson } from '../../data/fetchJson';
+import Icon from '../Icon/Icon';
+import IconButton from '../IconButton/IconButton';
+
+/**
+ * Configuration for the optional "Database Sync" row in the dropdown. This row
+ * is Recon-specific and must be omitted for OM, SCM and DN — the row is hidden
+ * when this prop is absent.
+ */
+export interface DbSyncConfig {
+  /** Row label, e.g. `"Database Sync"`. */
+  label: string;
+  /** Status description, e.g. `"Delta update 1s ago, 3:01 PM"`. */
+  description?: string;
+  /** Tooltip on the sync icon button. */
+  tooltip?: string;
+  /**
+   * Endpoint to call when the user clicks the sync button.
+   * `SyncChip` issues a `POST` via `fetchJson` and manages the loading state.
+   */
+  url: string;
+}
+
+export interface SyncChipProps {
+  /** Timestamp of the last data refresh; shown as "Refreshed at …" under Auto 
Refresh. */
+  lastRefreshedAt?: Date;
+  /**
+   * Optional Recon-specific "Database Sync" row. Omit for OM, SCM and DN.
+   */
+  dbSync?: DbSyncConfig;
+}
+
+function formatRefreshed(d: Date): string {
+  return d.toLocaleString('en-US', {
+    month: 'short',
+    day: 'numeric',
+    year: 'numeric',
+    hour: 'numeric',
+    minute: '2-digit',
+    second: '2-digit',
+    hour12: true,
+  });
+}
+
+const dropdownRowStyle: React.CSSProperties = {
+  display: 'flex',
+  alignItems: 'flex-start',
+  justifyContent: 'space-between',
+  gap: spacing.xl,
+  padding: `${spacing.sm}px ${spacing.md}px`,
+};
+
+const rowLabelStyle: React.CSSProperties = {
+  display: 'flex',
+  alignItems: 'center',
+  gap: spacing.xs,
+  fontSize: textStyles.bodyStandard.fontSize,
+  fontWeight: 600,
+  color: semanticColors.textPrimary,
+};
+
+const rowDescStyle: React.CSSProperties = {
+  fontSize: textStyles.bodySmall.fontSize,
+  color: semanticColors.textSecondary,
+  lineHeight: `${textStyles.bodySmall.lineHeight}px`,
+  marginTop: spacing.xxs,
+  maxWidth: 200,
+};
+
+/**
+ * Utility-bar chip showing the current auto-refresh state. Reads
+ * `enabled`/`setEnabled` from the nearest `SyncConfigProvider`.
+ *
+ * - **Live Sync** (auto-refresh on): green pill — bg `green[50]`, text 
`green[950]`.
+ * - **Manual Sync** (off): grey pill — bg `pewter[50]`, text `pewter[950]`.
+ */
+export const SyncChip: React.FC<SyncChipProps> = ({ lastRefreshedAt, dbSync }) 
=> {
+  const { enabled, setEnabled } = useSyncConfig();
+  const [open, setOpen] = useState(false);
+  const [dbSyncing, setDbSyncing] = useState(false);
+
+  const bgColor = enabled ? colors.green[50] : colors.pewter[50];
+  const textColor = enabled ? colors.green[950] : colors.pewter[950];
+  const dotColor = enabled ? colors.green[600] : colors.pewter[400];
+  const chipLabel = enabled ? 'Live Sync' : 'Manual Sync';
+
+  const handleDbSync = async () => {
+    if (!dbSync || dbSyncing) {
+      return;
+    }
+    setDbSyncing(true);
+    try {
+      await fetchJson(dbSync.url, { method: 'POST' });
+    } finally {

Review Comment:
   There is no catch here, so a failed sync closes the dropdown and looks like 
it worked. Should we surface the error?



##########
ozone-ui/packages/shared/src/components/ErrorBoundary/QueryErrorBoundary.tsx:
##########
@@ -0,0 +1,86 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import React from 'react';
+import { QueryErrorResetBoundary } from '@tanstack/react-query';
+import { HttpError } from '../../data/fetchJson';
+import { NetworkErrorState, ServerErrorState } from '../ErrorState/ErrorState';
+import { ErrorBoundary } from './ErrorBoundary';
+
+/** Arguments passed to a custom {@link QueryErrorBoundary} fallback. */
+export interface QueryErrorFallbackProps {
+  error: Error;
+  /** Reset the failed queries and clear the boundary (wired to Retry). */
+  retry: () => void;
+}
+
+export interface QueryErrorBoundaryProps {
+  children: React.ReactNode;
+  /** Override the default (network vs. 500) error page. */
+  fallback?: (props: QueryErrorFallbackProps) => React.ReactNode;
+  /** Reset the boundary when any of these values change (e.g. the route). */
+  resetKeys?: unknown[];
+}
+
+/**
+ * The default fallback: a server-side failure (HTTP 5xx) shows the 500 state;
+ * anything else — a network/timeout failure (native `fetch` rejects with a
+ * `TypeError`), an aborted request, or an unclassified error — shows the 
network
+ * state. Both wire their action button to `retry`.
+ */
+function defaultFallback({ error, retry }: QueryErrorFallbackProps): 
React.ReactNode {
+  if (error instanceof HttpError && error.status >= 500) {
+    return <ServerErrorState onAction={retry} />;
+  }
+  return <NetworkErrorState onAction={retry} />;

Review Comment:
   Should 4xx have its own branch? Right now, everything that is not a 5xx 
shows **Network Error**, even a render-time TypeError.



##########
ozone-ui/packages/shared/src/data/fetchJson.ts:
##########
@@ -0,0 +1,87 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+/** Query-string parameters. `undefined`/`null` values are skipped. */
+export type QueryParams = Record<string, string | number | boolean | undefined 
| null>;
+
+export interface FetchJsonOptions extends Omit<RequestInit, 'body'> {
+  /** Query-string parameters appended to `url`. */
+  params?: QueryParams;
+  /** Request body; objects are JSON-encoded with a JSON content-type. */
+  body?: BodyInit | Record<string, unknown> | null;
+}
+
+/** Error thrown for non-2xx responses, carrying the HTTP status. */
+export class HttpError extends Error {
+  constructor(
+    public readonly status: number,
+    public readonly url: string,
+    message?: string
+  ) {
+    super(message ?? `Request to ${url} failed with status ${status}`);
+    this.name = 'HttpError';
+  }
+}
+
+function withParams(url: string, params?: QueryParams): string {
+  if (!params) {
+    return url;
+  }
+  const search = new URLSearchParams();
+  for (const [key, value] of Object.entries(params)) {
+    if (value !== undefined && value !== null) {
+      search.append(key, String(value));
+    }
+  }
+  const qs = search.toString();
+  return qs ? `${url}${url.includes('?') ? '&' : '?'}${qs}` : url;
+}
+
+/**
+ * Minimal JSON fetch helper built on the native `fetch` API — the standard
+ * transport for the Ozone service UIs (no third-party HTTP client). Appends
+ * query parameters, JSON-encodes object bodies, throws {@link HttpError} on
+ * non-2xx responses, and parses the JSON response as `T`.
+ */
+export async function fetchJson<T>(url: string, options: FetchJsonOptions = 
{}): Promise<T> {
+  const { params, body, headers, ...rest } = options;
+
+  const isJsonBody =
+    body != null &&
+    typeof body === 'object' &&
+    !(body instanceof FormData) &&
+    !(body instanceof Blob);
+
+  const response = await fetch(withParams(url, params), {
+    ...rest,
+    headers: {
+      Accept: 'application/json',
+      ...(isJsonBody ? { 'Content-Type': 'application/json' } : {}),
+      ...headers,
+    },
+    body: isJsonBody ? JSON.stringify(body) : (body as BodyInit | null | 
undefined),
+  });
+
+  if (!response.ok) {
+    throw new HttpError(response.status, url, `${response.status} 
${response.statusText}`);

Review Comment:
   Could we keep the response body on `HttpError`? The status text alone drops 
whatever the server explained.



##########
ozone-ui/packages/om/src/api/overview.ts:
##########
@@ -0,0 +1,327 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import moment from 'moment';
+
+/**
+ * JMX MBean queries used by the Overview sections. Kept in one place so each
+ * section references a query by name; sections that share a query (e.g. the OM
+ * ServerRuntime bean) are de-duplicated to a single request by the JMX cache.
+ */
+export const JMX_QUERY = {
+  /** OM ServerRuntime bean: RPC port, ratis roles, data dirs, version, build. 
*/
+  omInfo: 'Hadoop:service=*,name=*,component=ServerRuntime',
+  /** This node's Ratis RaftServer bean: id, leader, role, group. */
+  ratisServer: 'Ratis:service=RaftServer,group=*,id=*',
+  /** JVM runtime bean: input arguments and system properties. */
+  runtime: 'java.lang:type=Runtime',
+  /**
+   * Ratis leader-election metrics for the current node. The name patterns are
+   * matched by the mock; a live cluster may need the node id/group 
interpolated
+   * (e.g. `ratis:name=ratis.leader_election.<id>@<group>.electionCount`).
+   */
+  leaderElectionCount: 'ratis:name=ratis.leader_election.*electionCount',
+  leaderElectionElapsed: 
'ratis:name=ratis.leader_election.*lastLeaderElectionElapsedTime',
+} as const;
+
+/* --------------------------------- Beans ---------------------------------- 
*/
+
+export interface OzoneManagerInfoBean {
+  RpcPort: string;
+  Namespace: string;
+  /**
+   * OM Ratis peers, one row per node. Each row is a tuple
+   * `[hostName, nodeId, ratisPort, role, leaderReadiness]` (see
+   * `OMMXBean.getRatisRoles` / `OmUtils.format`). On error the bean returns a
+   * single-element row `[message]`.
+   */
+  RatisRoles: string[][];
+  RatisLogDirectory: string;
+  RocksDbDirectory: string;
+  Version: string;
+  SoftwareVersion: string;
+  StartedTimeInMillis: number;
+  CompileInfo: string;
+}
+
+export interface RatisServerBean {
+  Id: string;
+  LeaderId: string;
+  Role: string;
+  GroupId: string;
+  CurrentTerm: number;
+}
+
+/** Ratis leader-election count metric (current node). */
+export interface LeaderElectionCountBean {
+  Count: number;
+}
+
+/** Ratis last-leader-election elapsed-time metric in milliseconds (current 
node). */
+export interface LeaderElectionElapsedBean {
+  Value: number;
+}
+
+export interface SystemProperty {
+  key: string;
+  value: string;
+}
+
+export interface RuntimeBean {
+  VmName: string;
+  VmVendor: string;
+  VmVersion: string;
+  Name: string;
+  InputArguments: string[];
+  SystemProperties: SystemProperty[];
+}
+
+/* ------------------------------ View models ------------------------------- 
*/
+
+export interface KeyValue {
+  key: string;
+  label: string;
+  value: string;
+  copyable?: boolean;
+  tooltip?: string;
+}
+
+export type RatisRoleName = 'LEADER' | 'FOLLOWER' | string;
+
+export interface RatisRole {
+  key: string;
+  hostName: string;
+  nodeId: string;
+  ratisPort: string;
+  role: RatisRoleName;
+  /** Derived follower sync state; `null` for the leader row. */
+  readiness: 'Synced' | 'Lagging' | null;
+  /** True for the node serving this JMX endpoint. */
+  isCurrent: boolean;
+}
+
+export type JvmParameterCategory = 'System & Framework' | 'Memory & GC' | 
'System Property';
+
+export interface JvmParameter {
+  key: string;
+  parameter: string;
+  value: string;
+  category: JvmParameterCategory;
+}
+
+/* -------------------------------- Parsers --------------------------------- 
*/
+
+/**
+ * Parse the OM `RatisRoles` bean — an array of
+ * `[hostName, nodeId, ratisPort, role, leaderReadiness]` tuples. Rows that 
don't
+ * carry at least the first four fields (e.g. the single-element error row the
+ * bean returns when there is no leader) are skipped.
+ */
+export function parseRatisRoles(rows: string[][] | undefined, currentNodeId?: 
string): RatisRole[] {
+  return (rows ?? [])
+    .filter((row) => Array.isArray(row) && row.length >= 4)

Review Comment:
   When there is no leader, the bean returns a one-element message row, and 
this filter drops it, so the table is empty. Should we show the message?  I 
noticed the old om-overview.html rendered that message.



##########
ozone-ui/packages/om/src/api/overview.ts:
##########
@@ -0,0 +1,327 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import moment from 'moment';
+
+/**
+ * JMX MBean queries used by the Overview sections. Kept in one place so each
+ * section references a query by name; sections that share a query (e.g. the OM
+ * ServerRuntime bean) are de-duplicated to a single request by the JMX cache.
+ */
+export const JMX_QUERY = {
+  /** OM ServerRuntime bean: RPC port, ratis roles, data dirs, version, build. 
*/
+  omInfo: 'Hadoop:service=*,name=*,component=ServerRuntime',
+  /** This node's Ratis RaftServer bean: id, leader, role, group. */
+  ratisServer: 'Ratis:service=RaftServer,group=*,id=*',
+  /** JVM runtime bean: input arguments and system properties. */
+  runtime: 'java.lang:type=Runtime',
+  /**
+   * Ratis leader-election metrics for the current node. The name patterns are
+   * matched by the mock; a live cluster may need the node id/group 
interpolated
+   * (e.g. `ratis:name=ratis.leader_election.<id>@<group>.electionCount`).
+   */
+  leaderElectionCount: 'ratis:name=ratis.leader_election.*electionCount',
+  leaderElectionElapsed: 
'ratis:name=ratis.leader_election.*lastLeaderElectionElapsedTime',
+} as const;
+
+/* --------------------------------- Beans ---------------------------------- 
*/
+
+export interface OzoneManagerInfoBean {
+  RpcPort: string;
+  Namespace: string;
+  /**
+   * OM Ratis peers, one row per node. Each row is a tuple
+   * `[hostName, nodeId, ratisPort, role, leaderReadiness]` (see
+   * `OMMXBean.getRatisRoles` / `OmUtils.format`). On error the bean returns a
+   * single-element row `[message]`.
+   */
+  RatisRoles: string[][];
+  RatisLogDirectory: string;
+  RocksDbDirectory: string;
+  Version: string;
+  SoftwareVersion: string;
+  StartedTimeInMillis: number;
+  CompileInfo: string;

Review Comment:
   Just curious, I couldn't find `CompileInfo` on `OMMXBean` or its parent 
`ServiceRuntimeInfo`. On a real OM the Compiled field renders blank.
   
   
https://github.com/apache/ozone/blob/8e872d33a159a65fa06209494fe24c68984c1830/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/OMMXBean.java#L28-L44



##########
ozone-ui/packages/om/src/api/useJmx.ts:
##########
@@ -0,0 +1,100 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import { useQuery, useSuspenseQuery } from '@tanstack/react-query';
+import { useRefetchInterval } from '@ozone-ui/shared';
+import { queryJmx } from './jmx';
+
+export interface JmxBeanState<T> {
+  data?: T;
+  isLoading: boolean;
+  isError: boolean;
+  error: Error | null;
+  /** The query succeeded but no MBean matched (`{ beans: [] }`). */
+  isEmpty: boolean;
+}
+
+export interface SuspenseJmxBeanState<T> {
+  /** The first matching MBean, or `undefined` when the query returned none. */
+  data?: T;
+  /** The query succeeded but no MBean matched (`{ beans: [] }`). */
+  isEmpty: boolean;
+}
+
+export interface UseJmxBeanOptions {
+  /**
+   * Auto-refresh interval in milliseconds. Omit or pass `false` to disable
+   * polling (the default). This is the hook-level hook for a future
+   * auto-polling toggle.
+   */
+  refetchInterval?: number | false;
+  /** Disable the query until a dependency is ready. Defaults to `true`. */
+  enabled?: boolean;
+}
+
+/** The shared cache key for a JMX query, so callers can invalidate by prefix. 
*/
+export const JMX_QUERY_KEY = 'jmx';
+
+/**
+ * TanStack Query options for a JMX query. Shared by the plain and suspense 
hooks
+ * (and usable with `useSuspenseQueries`) so every JMX read dedupes on the same
+ * `['jmx', qry]` key.
+ */
+export function jmxQueryOptions<T>(qry: string) {
+  return {
+    queryKey: [JMX_QUERY_KEY, qry] as const,
+    queryFn: () => queryJmx<T>(qry),
+  };
+}
+
+/**
+ * Fetch a single JMX MBean (the first bean) for a section via TanStack Query.
+ * Requests are de-duplicated by query key, so multiple sections depending on 
the
+ * same MBean share one network call. Refresh by invalidating the `['jmx']` 
key.
+ */
+export function useJmxBean<T>(qry: string, options: UseJmxBeanOptions = {}): 
JmxBeanState<T> {

Review Comment:
    Is this `method` still needed? It looks like nothing calls it, and it does 
not follow the "Live Sync interval" as the suspense version does.



##########
ozone-ui/packages/shared/src/components/UtilityBar/UtilityBar.tsx:
##########
@@ -81,11 +95,16 @@ export const UtilityBar: React.FC<UtilityBarProps> = ({
         marginLeft: center ? 0 : 'auto',
         display: 'flex',
         alignItems: 'center',
-        gap: spacing.xs,
-        color: semanticColors.textDisabled,
+        gap: spacing.sm,
+        color: semanticColors.textSecondary,
       }}
     >
-      {actions}
+      <IconButton
+        icon={<QuestionCircleOutlined style={{ fontSize: 18 }} />}
+        label="Help"
+        onClick={onHelp}

Review Comment:
    Should the `Help` button render only when `onHelp` is passed? Currently, 
the button is there, but clicking it does nothing.
   
   
https://github.com/user-attachments/assets/7fb9f528-5e8f-43d2-abd2-0ab6fdfdc6ca
   



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to