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


##########
ozone-ui/packages/om/src/api/overview.ts:
##########
@@ -0,0 +1,313 @@
+/**
+ * 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;
+  RatisRoles: string;

Review Comment:
    I tried to run this against a real OM, and found the Overview page renders 
blank. It seems that `RatisRoles` in the JMX bean is an array of tuples, not a 
string:
   
   
https://github.com/apache/ozone/blob/ebe71f64b754ffe6b6a2965282833d4bd292879d/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/OMMXBean.java#L32
   
    I would change `RatisRoles: string` to `string[][]` and update the mock.



##########
ozone-ui/packages/om/vite.config.ts:
##########
@@ -74,6 +74,11 @@ export default defineConfig({
       '/api': {
         target: 'http://localhost:9862',
       },
+      // JMX endpoint — proxied to the json-server mock in dev (see 
mock/server.cjs).
+      '/jmx': {
+        target: 'http://localhost:9878',

Review Comment:
   I hit a port conflict here when trying to run this against a real OM. `9878` 
is also the S3 Gateway port in the Compose cluster, and pointing at a real OM 
requires editing this file.
   
   Would it make sense to support an env override? e.g. `target: 
process.env.OM_JMX_TARGET ?? 'http://localhost:9878'`?



##########
ozone-ui/packages/om/src/pages/Overview/sections/RolesSection.tsx:
##########
@@ -0,0 +1,139 @@
+/**
+ * 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 type { TableColumnsType } from 'antd';
+import { Chip, DataTable, KeyValuePair, Section, TextLink } from 
'@ozone-ui/shared';
+import {
+  JMX_QUERY,
+  formatElapsed,
+  parseRatisRoles,
+  type LeaderElectionCountBean,
+  type LeaderElectionElapsedBean,
+  type OzoneManagerInfoBean,
+  type RatisRole,
+  type RatisServerBean,
+} from '../../../api/overview';
+import { useJmxBean } from '../../../api/useJmx';
+import SectionBody from '../SectionBody';
+import type { SectionProps } from './InstanceDetailsSection';
+
+/** Grid for the per-host details revealed when a role row is expanded. */
+const detailsGridStyle: React.CSSProperties = {
+  display: 'grid',
+  gridTemplateColumns: 'repeat(auto-fill, minmax(200px, 1fr))',
+  gap: '16px 24px',
+  padding: '4px 8px 8px',
+};
+
+const columns: TableColumnsType<RatisRole> = [
+  {
+    title: 'Host Name',
+    dataIndex: 'hostName',
+    key: 'hostName',
+    render: (hostName: string, row) => (
+      <TextLink href="#" style={{ fontWeight: row.isCurrent ? 600 : undefined 
}}>
+        {hostName}
+      </TextLink>
+    ),
+  },
+  { title: 'Node ID', dataIndex: 'nodeId', key: 'nodeId' },
+  { title: 'Ratis Port', dataIndex: 'ratisPort', key: 'ratisPort' },
+  {
+    title: 'Role',
+    dataIndex: 'role',
+    key: 'role',
+    render: (role: string) => (
+      <Chip color={role === 'LEADER' ? 'blue' : 'neutral'} size="small">
+        {role.charAt(0) + role.slice(1).toLowerCase()}
+      </Chip>
+    ),
+  },
+  {
+    title: 'Leader Readiness',
+    dataIndex: 'readiness',
+    key: 'readiness',
+    render: (readiness: RatisRole['readiness']) =>
+      readiness ? (
+        <Chip color={readiness === 'Synced' ? 'green' : 'orange'} size="small">
+          {readiness}
+        </Chip>
+      ) : (
+        '—'
+      ),
+  },
+];
+
+/** "Ozone Manager Roles" HA table. Sourced from the OM ServerRuntime bean. */
+export const RolesSection: React.FC<SectionProps> = ({ refreshToken }) => {
+  const {
+    data: omInfo,
+    loading,
+    error,
+  } = useJmxBean<OzoneManagerInfoBean>(JMX_QUERY.omInfo, refreshToken);
+  const { data: ratis } = useJmxBean<RatisServerBean>(JMX_QUERY.ratisServer, 
refreshToken);

Review Comment:
   These hooks only consume data, so failures in the Ratis or leader-election 
queries appear as `—` or missing fields. Could we handle these query errors, at 
least by showing a partial-data warning? Or do we plan to do this in a 
follow-up?



##########
ozone-ui/packages/om/src/api/jmx.ts:
##########
@@ -0,0 +1,73 @@
+/**
+ * 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 axios from 'axios';
+
+/**
+ * The OM exposes runtime state via its JMX servlet at `GET /jmx?qry=<query>`,
+ * always returning `{ beans: [...] }`. In development Vite proxies `/jmx` to 
the
+ * json-server mock (see `mock/server.cjs`).
+ *
+ * Fetches are keyed and de-duplicated by query string (see {@link 
fetchJmxBeans}):
+ * several sections of a page may depend on the same MBean (e.g. the OM
+ * ServerRuntime bean feeds Instance Details, Roles and Metadata Volume), yet 
the
+ * query is only issued once. Sections also fetch lazily, so a query is never
+ * sent for a section that is not rendered — this keeps us from pulling the 
full
+ * multi-thousand-line JMX dump when only a few beans are needed.
+ */
+const client = axios.create({ baseURL: '' });
+
+export interface JmxResponse<T> {
+  beans: T[];
+}
+
+/** Issue a JMX query and return the matching MBeans (no caching). */
+export async function queryJmx<T>(qry: string): Promise<T[]> {
+  const { data } = await client.get<JmxResponse<T>>('/jmx', { params: { qry } 
});
+  return data?.beans ?? [];
+}
+
+/** In-flight / resolved query cache, keyed by the JMX query string. */
+const cache = new Map<string, Promise<unknown[]>>();
+
+/**
+ * Fetch MBeans for a query, sharing a single request across all callers that 
ask
+ * for the same query. Failed requests are evicted so they can be retried.
+ */
+export function fetchJmxBeans<T>(qry: string): Promise<T[]> {

Review Comment:
   Since this is the first data-fetching page in the new UI, though, do we want 
to consider a library like `TanStack Query` for this layer?
   
   As more OM and SCM pages add API queries, this may become difficult to 
maintain and introduce edge cases such as the refresh/cache race.
   
   Other ASF React UIs already use this approach, including the [Apache Airflow 
UI](https://github.com/apache/airflow/blob/main/airflow-core/src/airflow/ui/package.json)
 and [Apache APISIX 
Dashboard](https://github.com/apache/apisix-dashboard/blob/master/package.json).
 Since this introduces a dependency, it could also be tracked as a follow-up if 
it is outside this PR’s scope.



##########
ozone-ui/packages/om/src/pages/Overview/sections/RolesSection.tsx:
##########
@@ -0,0 +1,139 @@
+/**
+ * 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 type { TableColumnsType } from 'antd';
+import { Chip, DataTable, KeyValuePair, Section, TextLink } from 
'@ozone-ui/shared';
+import {
+  JMX_QUERY,
+  formatElapsed,
+  parseRatisRoles,
+  type LeaderElectionCountBean,
+  type LeaderElectionElapsedBean,
+  type OzoneManagerInfoBean,
+  type RatisRole,
+  type RatisServerBean,
+} from '../../../api/overview';
+import { useJmxBean } from '../../../api/useJmx';
+import SectionBody from '../SectionBody';
+import type { SectionProps } from './InstanceDetailsSection';
+
+/** Grid for the per-host details revealed when a role row is expanded. */
+const detailsGridStyle: React.CSSProperties = {
+  display: 'grid',
+  gridTemplateColumns: 'repeat(auto-fill, minmax(200px, 1fr))',
+  gap: '16px 24px',
+  padding: '4px 8px 8px',
+};
+
+const columns: TableColumnsType<RatisRole> = [
+  {
+    title: 'Host Name',
+    dataIndex: 'hostName',
+    key: 'hostName',
+    render: (hostName: string, row) => (
+      <TextLink href="#" style={{ fontWeight: row.isCurrent ? 600 : undefined 
}}>

Review Comment:
   Is `href="#"` intentional here, or a placeholder for now?
   
   If the real link is planned for a follow-up, would plain text work better 
for now so it doesn't read as clickable?
   
   <img width="1418" height="310" alt="Image" 
src="https://github.com/user-attachments/assets/e617f2df-6ef7-40cb-83a0-79c954f2e800";
 />



##########
ozone-ui/packages/om/src/api/overview.ts:
##########
@@ -0,0 +1,313 @@
+/**
+ * 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;
+  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` string, e.g.
+ * `{ HostName: h1 | Node-Id: om1 | Ratis-Port : 9872 | Role: FOLLOWER } 
{...}`.
+ */
+export function parseRatisRoles(raw: string, currentNodeId?: string): 
RatisRole[] {
+  const groups = raw?.match(/\{[^}]*\}/g) ?? [];

Review Comment:
   Same issue as above: with a real OM, `raw.match(...)` throws `TypeError: 
raw.match` is not a function. Could we parse the tuple array here?



##########
ozone-ui/packages/om/src/api/overview.ts:
##########
@@ -0,0 +1,313 @@
+/**
+ * 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;
+  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` string, e.g.
+ * `{ HostName: h1 | Node-Id: om1 | Ratis-Port : 9872 | Role: FOLLOWER } 
{...}`.
+ */
+export function parseRatisRoles(raw: string, currentNodeId?: string): 
RatisRole[] {
+  const groups = raw?.match(/\{[^}]*\}/g) ?? [];
+  return groups.map((group, index) => {
+    const fields: Record<string, string> = {};
+    group
+      .replace(/[{}]/g, '')
+      .split('|')
+      .forEach((part) => {
+        const sep = part.indexOf(':');
+        if (sep === -1) {
+          return;
+        }
+        fields[part.slice(0, sep).trim()] = part.slice(sep + 1).trim();
+      });
+    const role = (fields.Role ?? '').toUpperCase();
+    const nodeId = fields['Node-Id'] ?? '';
+    return {
+      key: nodeId || String(index),
+      hostName: fields.HostName ?? '',
+      nodeId,
+      ratisPort: fields['Ratis-Port'] ?? '',
+      role,
+      // The leader has no "readiness"; followers are shown as synced with the 
leader.
+      readiness: role === 'LEADER' ? null : 'Synced',
+      isCurrent: !!currentNodeId && nodeId === currentNodeId,
+    };
+  });
+}
+
+const MEMORY_GC = /Xm[xsn]|Xss|gc|CMS|Heap|Memory/i;
+
+function categorize(parameter: string): JvmParameterCategory {
+  return MEMORY_GC.test(parameter) ? 'Memory & GC' : 'System & Framework';
+}
+
+/** Split a single JVM argument into a `{ parameter, value }` pair. */
+function splitArgument(arg: string): { parameter: string; value: string } {
+  if (arg.startsWith('-D')) {
+    const eq = arg.indexOf('=');
+    return eq === -1
+      ? { parameter: arg, value: 'Present' }
+      : { parameter: arg.slice(0, eq), value: arg.slice(eq + 1) };
+  }
+  if (arg.startsWith('-XX:')) {
+    const body = arg.slice(4);
+    if (body.startsWith('+')) {
+      return { parameter: arg, value: 'Enabled' };
+    }
+    if (body.startsWith('-')) {
+      return { parameter: arg, value: 'Disabled' };
+    }
+    const eq = body.indexOf('=');
+    return eq === -1
+      ? { parameter: arg, value: 'Present' }
+      : { parameter: `-XX:${body.slice(0, eq)}`, value: body.slice(eq + 1) };
+  }
+  if (arg.startsWith('-Xloggc:')) {
+    return { parameter: '-Xloggc', value: arg.slice('-Xloggc:'.length) };
+  }
+  if (/^-Xm[xsn]/.test(arg) || arg.startsWith('-Xss')) {
+    return { parameter: arg.slice(0, 4), value: arg.slice(4) };
+  }
+  if (arg.startsWith('-verbose:')) {
+    return { parameter: '-verbose', value: arg.slice('-verbose:'.length) };
+  }
+  return { parameter: arg, value: 'Present' };
+}
+
+/** Parse JVM `InputArguments` into categorised parameter rows. */
+export function parseJvmArguments(args: string[]): JvmParameter[] {
+  return (args ?? []).map((arg, index) => {
+    const { parameter, value } = splitArgument(arg);
+    return { key: `arg-${index}`, parameter, value, category: 
categorize(parameter) };
+  });
+}
+
+/** Map JVM `SystemProperties` into parameter rows (for the "Show JVM Modules" 
toggle). */
+export function toSystemPropertyRows(props: SystemProperty[]): JvmParameter[] {
+  return (props ?? []).map((prop, index) => ({
+    key: `prop-${index}`,
+    parameter: prop.key,
+    value: prop.value === '' ? '—' : prop.value,
+    category: 'System Property',
+  }));
+}
+
+function formatHeap(xmx: string | undefined): string {
+  if (!xmx) {
+    return 'Not set';
+  }
+  const match = xmx.slice(4).match(/^(\d+)\s*([kKmMgG])?/);
+  if (!match) {
+    return xmx.slice(4);
+  }
+  const size = Number(match[1]);
+  const unit = (match[2] ?? 'B').toUpperCase();
+  const megabytes = unit === 'G' ? size * 1024 : unit === 'K' ? 
Math.round(size / 1024) : size;

Review Comment:
   I noticed that the JVM interprets an `-Xmx` value without a suffix as bytes, 
but the `B` case currently labels the raw value as MB. For example, 
`-Xmx2511000000` would be displayed as `2,511,000,000 MB`.
   
   I think we should divide byte values by 1024x1024 before displaying them as 
MB and could probably consider using a switch/case for better readability
   
   ```js
     function formatHeap(xmx: string | undefined): string {
       if (!xmx) {
         return 'Not set';
       }
   
       const match = xmx.slice(4).match(/^(\d+)\s*([kKmMgG])?/);
       if (!match) {
         return xmx.slice(4);
       }
   
       const size = Number(match[1]);
       const unit = (match[2] ?? 'B').toUpperCase();
   
       let megabytes: number;
       switch (unit) {
         case 'G':
           megabytes = size * 1024;
           break;
         case 'M':
           megabytes = size;
           break;
         case 'K':
           megabytes = size / 1024;
           break;
         default:
           megabytes = size / (1024 * 1024);
           break;
       }
   
       return `${Math.round(megabytes).toLocaleString('en-US')} MB`;
     }
   ```



##########
ozone-ui/packages/om/src/api/jmx.ts:
##########
@@ -0,0 +1,73 @@
+/**
+ * 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 axios from 'axios';
+
+/**
+ * The OM exposes runtime state via its JMX servlet at `GET /jmx?qry=<query>`,
+ * always returning `{ beans: [...] }`. In development Vite proxies `/jmx` to 
the
+ * json-server mock (see `mock/server.cjs`).
+ *
+ * Fetches are keyed and de-duplicated by query string (see {@link 
fetchJmxBeans}):
+ * several sections of a page may depend on the same MBean (e.g. the OM
+ * ServerRuntime bean feeds Instance Details, Roles and Metadata Volume), yet 
the
+ * query is only issued once. Sections also fetch lazily, so a query is never
+ * sent for a section that is not rendered — this keeps us from pulling the 
full
+ * multi-thousand-line JMX dump when only a few beans are needed.
+ */
+const client = axios.create({ baseURL: '' });
+
+export interface JmxResponse<T> {
+  beans: T[];
+}
+
+/** Issue a JMX query and return the matching MBeans (no caching). */
+export async function queryJmx<T>(qry: string): Promise<T[]> {
+  const { data } = await client.get<JmxResponse<T>>('/jmx', { params: { qry } 
});
+  return data?.beans ?? [];
+}
+
+/** In-flight / resolved query cache, keyed by the JMX query string. */
+const cache = new Map<string, Promise<unknown[]>>();
+
+/**
+ * Fetch MBeans for a query, sharing a single request across all callers that 
ask
+ * for the same query. Failed requests are evicted so they can be retried.
+ */
+export function fetchJmxBeans<T>(qry: string): Promise<T[]> {
+  let pending = cache.get(qry) as Promise<T[]> | undefined;
+  if (!pending) {
+    pending = queryJmx<T>(qry).catch((err) => {
+      cache.delete(qry);
+      throw err;
+    });
+    cache.set(qry, pending as Promise<unknown[]>);
+  }
+  return pending;
+}
+
+/** Fetch a single MBean for a query (the first bean), or `undefined`. */
+export async function fetchJmxBean<T>(qry: string): Promise<T | undefined> {
+  const beans = await fetchJmxBeans<T>(qry);
+  return beans[0];

Review Comment:
   When no MBean matches a query, the JMX servlet can return 200 OK with 
`{"beans":[]}`.
   
   I think this might happen when an MBean isn't yet registered, during service 
reinitialization, or when the UI and OM expose different MBean versions. Would 
it be worth representing “no matching bean” as an explicit empty state and 
showing a message such as No JMX data available?



##########
ozone-ui/packages/om/src/pages/Overview/SectionBody.tsx:
##########
@@ -0,0 +1,50 @@
+/**
+ * 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 { Skeleton } from 'antd';
+import { Alert } from '@ozone-ui/shared';
+
+export interface SectionBodyProps {
+  loading: boolean;
+  error?: Error;
+  /** Number of skeleton rows to show while loading. Defaults to 2. */
+  skeletonRows?: number;
+  children: React.ReactNode;
+}
+
+/**
+ * Renders a section's async state: a skeleton while loading, an error alert on
+ * failure, or the resolved content.
+ */
+export const SectionBody: React.FC<SectionBodyProps> = ({

Review Comment:
   `SectionBody` handles loading and errors, but not the empty state here. I 
think we can consider centralizing it so that state management is all in this 
component.
   
   For example, adding an `isEmpty` prop rendering a small "No data" 
placeholder would make
   this visible with a minimal diff.
   
   Longer term, if we adopt TanStack Query (see the other comment), its 
Suspense mode
   would restructure this nicely, like below:
   
   ```tsx
   <ErrorBoundary fallback={<Alert ... />}>
     <Suspense fallback={<Skeleton rows={2} />}>
       <InstanceDetailsContent />  {/* useSuspenseQuery inside, data typed 
non-null */}
     </Suspense>
   </ErrorBoundary>
   ```
   
   Not asking for that in this PR, just sharing some thoughts. 🙂 



-- 
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