HDDS-15985. Add new Overview page for Ozone Manager - #10900
Conversation
|
@chihsuan would you like to take a look as well? |
Thanks for the ping! Sure, I'll take a look. 🙂 |
chihsuan
left a comment
There was a problem hiding this comment.
Nice work overall, @spacemonkd! The page structure is clean, and the mock setup makes local development easy.
I tested it with both the mock and a real OM and found a few issues. Please see the inline comments.
I also wanted to share a few broader suggestions. None of these need to be addressed in this PR: 🙂
- Unit tests: I understand that the current diff is already quite large 😅. the pure parsers in
overview.tsshould be easy to cover. - JMX as a UI contract: The UI currently depends directly on JMX bean shapes, which can change silently when the Java implementation evolves. It might be worth considering documenting or validating the bean shapes consumed by the UI. Longer term, a dedicated REST endpoint could provide a more stable contract.
- Data fetching: It would be useful to decide early whether the new UIs should adopt a server-state library such as TanStack Query. I left an inline comment with more context.
- Storybook: As the shared component library grows, what do you think about setting up Storybook? I think this could help develop, review, and document shared components in isolation.
Happy to help with any of these follow-ups.
|
|
||
| export interface OzoneManagerInfoBean { | ||
| RpcPort: string; | ||
| RatisRoles: string; |
There was a problem hiding this 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:
I would change RatisRoles: string to string[][] and update the mock.
| * `{ HostName: h1 | Node-Id: om1 | Ratis-Port : 9872 | Role: FOLLOWER } {...}`. | ||
| */ | ||
| export function parseRatisRoles(raw: string, currentNodeId?: string): RatisRole[] { | ||
| const groups = raw?.match(/\{[^}]*\}/g) ?? []; |
There was a problem hiding this 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?
| }, | ||
| // JMX endpoint — proxied to the json-server mock in dev (see mock/server.cjs). | ||
| '/jmx': { | ||
| target: 'http://localhost:9878', |
There was a problem hiding this 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'?
| } | ||
| const size = Number(match[1]); | ||
| const unit = (match[2] ?? 'B').toUpperCase(); | ||
| const megabytes = unit === 'G' ? size * 1024 : unit === 'K' ? Math.round(size / 1024) : size; |
There was a problem hiding this 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
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`;
}| loading, | ||
| error, | ||
| } = useJmxBean<OzoneManagerInfoBean>(JMX_QUERY.omInfo, refreshToken); | ||
| const { data: ratis } = useJmxBean<RatisServerBean>(JMX_QUERY.ratisServer, refreshToken); |
There was a problem hiding this 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?
| /** 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]; |
There was a problem hiding this 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?
| dataIndex: 'hostName', | ||
| key: 'hostName', | ||
| render: (hostName: string, row) => ( | ||
| <TextLink href="#" style={{ fontWeight: row.isCurrent ? 600 : undefined }}> |
| * 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[]> { |
There was a problem hiding this 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 and Apache APISIX Dashboard. Since this introduces a dependency, it could also be tracked as a follow-up if it is outside this PR’s scope.
| * 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> = ({ |
There was a problem hiding this 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:
<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. 🙂

What changes were proposed in this pull request?
HDDS-15985. Add new Overview page for Ozone Manager
Please describe your PR in detail:
This PR adds the new React based Overview page for Ozone Manager
What is the link to the Apache JIRA
https://issues.apache.org/jira/browse/HDDS-15985
How was this patch tested?
Patch was tested manually via mocked DB.
Screen.Recording.2026-07-29.at.23.20.12.mov
Output of "Copy Arguments" button at the end: