Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
52 changes: 48 additions & 4 deletions ozone-ui/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,12 @@ ozone-ui/ # pnpm workspace root
├── recon/ # @ozone-ui/ozone-recon (Vite app)
├── scm/ # @ozone-ui/ozone-scm (Vite app)
└── om/ # @ozone-ui/ozone-om (Vite app)
├── mock/ # json-server JMX mock (server.cjs, jmxData.cjs)
└── src/
├── api/ # JMX client + section-driven data hooks/parsers
├── pages/ # Overview page + section components
├── navigation.tsx # sidebar nav items
└── App.tsx # utility bar + sidebar + routes
```

## Prerequisites
Expand All @@ -77,9 +83,39 @@ pnpm build:shared # compile @ozone-ui/shared -> packages/shared/dist

pnpm dev:recon # start the Recon app dev server
pnpm dev:scm # start the SCM app dev server
pnpm dev:om # start the OM app dev server
pnpm dev:om # start the OM app dev server (http://localhost:3000)
```

> Tip: run `pnpm build:shared --watch` (or rebuild it after edits) whenever you
> change `@ozone-ui/shared`, since apps import the compiled `dist/` output.

## Mock backends (local development)

The apps talk to their Ozone service over HTTP. To develop without a live
cluster, each app can be paired with a **json-server** mock of its backend.
Mock commands are namespaced per service (`mock:om`, and later `mock:scm`,
`mock:recon`, …) so every sub-service can host its own mock independently.

### OM (Ozone Manager)

The OM app reads runtime state from the OM JMX servlet (`GET /jmx?qry=<mbean>`).
The mock in `packages/om/mock/` replays captured JMX responses on port `9878`;
the OM dev server proxies `/jmx` to it (see `packages/om/vite.config.ts`).

```bash
cd ozone-ui
pnpm build:shared # once, and after any shared change

pnpm dev:om:mock # OM mock (:9878) + OM dev server (:3000) together
# — or run them separately —
pnpm mock:om # just the OM JMX mock on :9878
pnpm dev:om # just the OM dev server on :3000
```

Then open http://localhost:3000. To point the app at a real OM instead of the
mock, change the `/jmx` proxy target in `packages/om/vite.config.ts` (or serve
the built app from the OM itself, where `/jmx` is same-origin).

## Build

```bash
Expand Down Expand Up @@ -167,14 +203,22 @@ export default function App() {
- **`components/`** (derived from the components recurring across the mockups)
- `UtilityBar` — global top bar (leading/title, centre, actions).
- `Sidebar` — collapsible, router-aware navigation rail driven by `items`
(with `path`s) and `logo` props; integrates with `react-router-dom`.
- `AppLayout` — page shell (sider + header + content).
(with `path`s, plus `group`/`divider` entries) and `logo` props; integrates
with `react-router-dom`.
- `AppLayout` — page shell with an optional full-width `utilityBar` slot above
the sider + content row.
- `PageHeader` — page title with breadcrumb, subtitle and actions.
- `Section` — labelled content block: title, optional supporting text and
actions, followed by its content.
- `Card` — surface with `outlined`/`elevated`/`filled` emphasis and an
optional `collapsible` header.
- `KeyValuePair` — label/value pair (vertical or horizontal, optional link/copy).
- `KeyValuePair` — label/value pair (vertical or horizontal, optional
link/copy and an info `tooltip`).
- `DataTable` — themed Ant Design table with an optional title + filter/actions
toolbar and a `TablePagination` footer (client-side paging via `paginated`).
- `Chip` — pill: `full`/`dot` variant, `standard`/`small` size, colour and
`selected`/`closable` states.
- `SearchInput` — text field with a leading search glyph (table toolbars).
- `Alert` — inline status banner (info/success/warning/error).
- `TextLink` — themed inline link with optional external affordance.
- `IconButton` — square icon-only button with accessible label + tooltip.
Expand Down
2 changes: 2 additions & 0 deletions ozone-ui/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@
"dev:recon": "pnpm --filter @ozone-ui/ozone-recon run dev",
"dev:scm": "pnpm --filter @ozone-ui/ozone-scm run dev",
"dev:om": "pnpm --filter @ozone-ui/ozone-om run dev",
"mock:om": "pnpm --filter @ozone-ui/ozone-om run mock:om",
"dev:om:mock": "pnpm --filter @ozone-ui/ozone-om run dev:om:mock",
"clean": "rm -rf build && pnpm -r exec rm -rf dist node_modules",
"clean:cache": "rm -rf node_modules/.vite && pnpm -r exec rm -rf node_modules/.vite",
"clean:all": "pnpm run clean && pnpm run clean:cache",
Expand Down
168 changes: 168 additions & 0 deletions ozone-ui/packages/om/mock/jmxData.cjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,168 @@
/*
* 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.
*/

/**
* Captured Ozone Manager JMX responses, used by the json-server mock (server.cjs)
* to serve the OM UI without a live cluster. Bean payloads mirror real
* `GET /jmx?qry=...` responses; a few very large class-path strings are trimmed
* (they are not surfaced in the UI).
*/

const ozoneManagerInfo = {
name: 'Hadoop:service=OzoneManager,name=OzoneManagerInfo,component=ServerRuntime',
modelerType: 'org.apache.hadoop.ozone.om.OzoneManager',
RpcPort: '9862',
RatisRoles:
' { HostName: node1.test.site.com | Node-Id: om1546336043 | Ratis-Port : 9872 | Role: FOLLOWER} { HostName: node2.test.site.com | Node-Id: om1546336047 | Ratis-Port : 9872 | Role: LEADER} { HostName: node3.test.site.com | Node-Id: om1546336039 | Ratis-Port : 9872 | Role: FOLLOWER} ',
RatisLogDirectory: '/var/lib/hadoop-ozone/om/ratis',
RocksDbDirectory: '/var/lib/hadoop-ozone/om/data',
Version: '2.3.0, r0a1b2c3d4e5f60718293a4b5c6d7e8f901234567',
SoftwareVersion: '2.3.0',
StartedTimeInMillis: 1785178223133,
CompileInfo: 'built from source (branch master, commit 0a1b2c3)',
};

const runtime = {
name: 'java.lang:type=Runtime',
modelerType: 'sun.management.RuntimeImpl',
BootClassPathSupported: true,
VmName: 'OpenJDK 64-Bit Server VM',
VmVendor: 'AdoptOpenJDK',
VmVersion: '25.232-b09',
LibraryPath: ':/opt/ozone/current/lib/hadoop-ozone/share/ozone/lib',
Uptime: 78876304,
ManagementSpecVersion: '1.2',
SpecName: 'Java Virtual Machine Specification',
SpecVendor: 'Oracle Corporation',
SpecVersion: '1.8',
Name: '2455265@node1.test.site.com',
ClassPath: '/etc/hadoop-ozone/conf:<...trimmed...>',
StartTime: 1785178198793,
SystemProperties: [
{ key: 'java.runtime.name', value: 'OpenJDK Runtime Environment' },
{ key: 'java.runtime.version', value: '1.8.0_232-b09' },
{ key: 'java.version', value: '1.8.0_232' },
{ key: 'java.vm.name', value: 'OpenJDK 64-Bit Server VM' },
{ key: 'java.vm.vendor', value: 'AdoptOpenJDK' },
{ key: 'java.vm.version', value: '25.232-b09' },
{ key: 'java.home', value: '/usr/lib/jvm/java-1.8.0/jre' },
{ key: 'java.io.tmpdir', value: '/tmp' },
{ key: 'user.name', value: 'hdfs' },
{ key: 'user.timezone', value: 'UTC' },
{ key: 'os.name', value: 'Linux' },
{ key: 'os.arch', value: 'amd64' },
{ key: 'os.version', value: '5.4.243-1.el7.elrepo.x86_64' },
{ key: 'file.encoding', value: 'UTF-8' },
{ key: 'hadoop.home.dir', value: '/opt/ozone/current/lib/hadoop-ozone' },
{ key: 'hadoop.id.str', value: 'hdds-hdfs' },
{ key: 'hadoop.log.dir', value: '/var/log/hadoop-ozone' },
{ key: 'hadoop.log.file', value: 'ozone.log' },
{ key: 'hadoop.root.logger', value: 'INFO,console' },
{ key: 'hadoop.security.logger', value: 'INFO,NullAppender' },
{ key: 'hadoop.policy.file', value: 'hadoop-policy.xml' },
{ key: 'proc_om', value: '' },
{ key: 'sun.java.command', value: 'org.apache.hadoop.ozone.om.OzoneManagerStarter' },
{
key: 'java.library.path',
value: ':/opt/ozone/current/lib/hadoop-ozone/share/ozone/lib',
},
{
key: 'org.apache.ratis.thirdparty.io.netty.allocator.useCacheForAllThreads',
value: 'false',
},
{ key: 'sun.security.krb5.disableReferrals', value: 'true' },
{ key: 'jdk.tls.ephemeralDHKeySize', value: '2048' },
],
InputArguments: [
'-Dproc_om',
'-Dorg.apache.ratis.thirdparty.io.netty.allocator.useCacheForAllThreads=false',
'-Xmx2511M',
'-Xloggc:/var/log/hadoop-ozone/gc-OM-2026-07-27_18-49-49.log',
'-verbose:gc',
'-XX:+PrintGCDetails',
'-XX:+PrintGCTimeStamps',
'-XX:+PrintGCDateStamps',
'-XX:+UseConcMarkSweepGC',
'-XX:CMSInitiatingOccupancyFraction=70',
'-XX:+CMSParallelRemarkEnabled',
'-Dsun.security.krb5.disableReferrals=true',
'-Djdk.tls.ephemeralDHKeySize=2048',
'-Dcom.sun.management.jmxremote.ssl.enabled.protocols=TLSv1.2',
'-XX:OnOutOfMemoryError=/opt/ozone/bin/oom-handler.sh',
'-Dlog4j.configurationFile=/etc/hadoop-ozone/conf/om-audit-log4j2.properties',
'-Djava.library.path=:/opt/ozone/current/lib/hadoop-ozone/share/ozone/lib',
'-Dhadoop.log.dir=/var/log/hadoop-ozone',
'-Dhadoop.log.file=ozone.log',
'-Dhadoop.home.dir=/opt/ozone/current/lib/hadoop-ozone',
'-Dhadoop.id.str=hdds-hdfs',
'-Dhadoop.root.logger=INFO,console',
'-Dhadoop.policy.file=hadoop-policy.xml',
'-Dhadoop.security.logger=INFO,NullAppender',
],
ObjectName: 'java.lang:type=Runtime',
};

const ratisRaftServer = {
name: 'Ratis:service=RaftServer,group=group-0A1B2C3D4E5F,id=om1546336043',
modelerType: 'org.apache.ratis.server.impl.RaftServerImpl$RaftServerJmxAdapter',
Id: 'om1546336043',
LeaderId: 'om1546336047',
Role: ' FOLLOWER',
Groups: ['group-0A1B2C3D4E5F'],
Followers: [],
CurrentTerm: 5,
GroupId: 'group-0A1B2C3D4E5F',
};

const leaderElectionCount = {
name: 'ratis:name=ratis.leader_election.om1546336043@group-0A1B2C3D4E5F.electionCount',
modelerType: 'com.codahale.metrics.JmxReporter$JmxCounter',
Count: 1,
};

const leaderElectionElapsed = {
name: 'ratis:name=ratis.leader_election.om1546336043@group-0A1B2C3D4E5F.lastLeaderElectionElapsedTime',
modelerType: 'com.codahale.metrics.JmxReporter$JmxGauge',
Value: 78848822,
};

const deletingServiceMetrics = {
name: 'Hadoop:service=OzoneManager,name=DeletingServiceMetrics',
modelerType: 'DeletingServiceMetrics',
'tag.Context': 'ozone',
'tag.Hostname': 'node1.test.site.com',
MetricsResetTimeStamp: 1785178221,
KeysReclaimedInInterval: 0,
ReclaimedSizeInInterval: 0,
LastAOSPurgeTermId: 5,
LastAOSPurgeTransactionId: 23245,
NumKeysPurged: 1275,
};

/**
* Ordered match table. The mock server picks the first entry whose `test`
* matches the requested `qry` and returns `{ beans }`.
*/
module.exports = [
{ test: /component=ServerRuntime/i, beans: [ozoneManagerInfo] },
{ test: /java\.lang:type=Runtime/i, beans: [runtime] },
{ test: /service=RaftServer/i, beans: [ratisRaftServer] },
{ test: /electionCount/i, beans: [leaderElectionCount] },
{ test: /lastLeaderElectionElapsedTime/i, beans: [leaderElectionElapsed] },
{ test: /DeletingServiceMetrics/i, beans: [deletingServiceMetrics] },
];
46 changes: 46 additions & 0 deletions ozone-ui/packages/om/mock/server.cjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
/*
* 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.
*/

/**
* json-server based mock for the Ozone Manager JMX endpoint.
*
* Serves `GET /jmx?qry=<mbean query>` from the captured responses in
* jmxData.cjs so the OM UI can run without a live cluster. Start with
* `pnpm mock:om` (or `pnpm dev:om:mock` to run it alongside the OM dev
* server, which proxies /jmx to it).
*/

/* eslint-disable @typescript-eslint/no-require-imports */
const jsonServer = require('json-server');
const jmxData = require('./jmxData.cjs');

const PORT = process.env.MOCK_PORT ? Number(process.env.MOCK_PORT) : 9878;

const server = jsonServer.create();
server.use(jsonServer.defaults());

server.get('/jmx', (req, res) => {
const qry = String(req.query.qry || '');
const match = jmxData.find((entry) => entry.test.test(qry));
// Mirror the real JMX servlet, which always returns a { beans: [...] } shape.
res.json({ beans: match ? match.beans : [] });
});

server.listen(PORT, () => {
console.log(`OM JMX mock listening on http://localhost:${PORT}/jmx?qry=...`);
});
3 changes: 3 additions & 0 deletions ozone-ui/packages/om/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,11 @@
"name": "@ozone-ui/ozone-om",
"private": true,
"version": "1.0.0",
"type": "module",
"scripts": {
"dev": "vite --port=3000",
"mock:om": "node mock/server.cjs",
"dev:om:mock": "npm-run-all -p mock:om dev",
"build": "vite build",
"lint": "eslint ."
},
Expand Down
65 changes: 48 additions & 17 deletions ozone-ui/packages/om/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,26 +15,57 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { useState } from 'react';
import { Button } from 'antd';
import './App.css';

function App() {
const [count, setCount] = useState(0);
import { Routes, Route, Navigate } from 'react-router-dom';
import { AppstoreOutlined } from '@ant-design/icons';
import { AppLayout, Chip, IconButton, Sidebar, UtilityBar } from '@ozone-ui/shared';
import { navItems, SIDEBAR_WIDTH } from './navigation';
import OverviewPage from './pages/Overview/OverviewPage';
import Placeholder from './pages/Placeholder';

/** Product branding: the app name plus a chip showing the current host. */
const BrandTitle = () => {
const host = window.location.hostname;
return (
<span style={{ display: 'inline-flex', alignItems: 'center', gap: 8 }}>
Ozone Manager
{host && (
<Chip color="neutral" size="small">
{host}
</Chip>
)}
</span>
);
};

const utilityBar = (
<UtilityBar
leading={
<IconButton
icon={<AppstoreOutlined style={{ fontSize: 18 }} />}
label="App switcher"
tooltip={null}
/>
}
branding={<BrandTitle />}
/>
);

function App() {
return (
<div className="App">
<h1>Ozone OM</h1>
<div className="card">
<Button type="primary" onClick={() => setCount((count) => count + 1)}>
Count is {count}
</Button>
<p>
Edit <code>src/App.tsx</code> and save to test HMR
</p>
</div>
<p className="read-the-docs">Click on the Vite and React logos to learn more</p>
</div>
<AppLayout utilityBar={utilityBar} sider={<Sidebar items={navItems} width={SIDEBAR_WIDTH} />}>
<Routes>
<Route path="/" element={<OverviewPage />} />
<Route path="/configuration" element={<Placeholder title="Configuration" />} />
<Route path="/rpc" element={<Placeholder title="Remote Procedure Call" />} />
<Route path="/ozone-manager" element={<Placeholder title="Ozone Manager" />} />
<Route path="/jmx-info" element={<Placeholder title="JMX" />} />
<Route path="/stacks" element={<Placeholder title="Stacks" />} />
<Route path="/documentation" element={<Placeholder title="Documentation" />} />
<Route path="/log-levels" element={<Placeholder title="Log levels" />} />
<Route path="*" element={<Navigate to="/" replace />} />
</Routes>
</AppLayout>
);
}

Expand Down
Loading
Loading