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


##########
ozone-ui/packages/om/src/api/overview.ts:
##########
@@ -0,0 +1,340 @@
+/**
+ * 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;
+}
+
+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)
+    .map((row, index) => {
+      const [hostName = '', nodeId = '', ratisPort = '', roleRaw = ''] = row;
+      const role = roleRaw.toUpperCase();
+      return {
+        key: nodeId || String(index),
+        hostName,
+        nodeId,
+        ratisPort,
+        role,
+        // The leader has no "readiness"; followers are shown as synced with 
the leader.
+        readiness: role === 'LEADER' ? null : 'Synced',

Review Comment:
   Should this column show the readiness value the bean returns? Right now 
every follower shows a green **Synced**, so an OM that is not ready still looks 
healthy. 



##########
ozone-ui/packages/om/src/pages/Overview/sections/InstanceDetailsSection.tsx:
##########
@@ -0,0 +1,103 @@
+/**
+ * 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 } from 'react';
+import { Divider, Empty, Skeleton } from 'antd';
+import { useSuspenseQueries } from '@tanstack/react-query';
+import { Card, KeyValuePair, Section, spacing } from '@ozone-ui/shared';
+import {
+  JMX_QUERY,
+  formatElapsed,
+  formatStarted,
+  parseRatisRoles,
+  type LeaderElectionCountBean,
+  type LeaderElectionElapsedBean,
+  type OzoneManagerInfoBean,
+  type RatisServerBean,
+} from '../../../api/overview';
+import { jmxQueryOptions } from '../../../api/useJmx';
+
+const kvGridStyle: React.CSSProperties = {
+  display: 'grid',
+  gridTemplateColumns: 'repeat(auto-fill, minmax(200px, 1fr))',
+  gap: `${spacing.lg}px ${spacing.xl}px`,
+};
+
+const InstanceDetailsContent: React.FC = () => {
+  // Fetch all four beans in parallel (avoids an intra-component suspense 
waterfall).
+  const [omInfoQ, ratisQ, countQ, elapsedQ] = useSuspenseQueries({

Review Comment:
   Should these two pick up the shared refresh interval? I noticed they only 
fetch once, so **Election Count** and **Last Election Elapsed Time** stay 
frozen while the chip still says **Live Sync**.



##########
ozone-ui/packages/shared/src/components/SyncChip/SyncChip.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, { useState } from 'react';
+import { Dropdown, message, 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' });
+      message.success(`${dbSync.label} triggered`);
+      setOpen(false);
+    } catch (err) {
+      // Surface the failure and keep the dropdown open so it's visible.
+      message.error(err instanceof Error ? err.message : `${dbSync.label} 
failed`);
+    } finally {
+      setDbSyncing(false);
+    }
+  };
+
+  const dropdownContent = (
+    <div
+      style={{
+        width: 280,
+        background: semanticColors.bgElevated,
+        borderRadius: radius.lg,
+        border: `1px solid ${semanticColors.border}`,
+        boxShadow: '0 4px 16px rgba(35, 43, 48, 0.12)',
+        padding: `${spacing.xs}px 0`,
+        overflow: 'hidden',
+      }}
+    >
+      {/* Auto Refresh row */}
+      <div style={dropdownRowStyle}>
+        <div style={{ flex: 1 }}>
+          <div style={rowLabelStyle}>
+            <span>Auto Refresh</span>
+            <Tooltip title="Toggles automatic background polling for on-screen 
metrics, tables, and event streams.">
+              <span style={{ display: 'inline-flex', color: 
semanticColors.textTertiary }}>
+                <Icon name="info" size={14} />
+              </span>
+            </Tooltip>
+          </div>
+          {lastRefreshedAt && (
+            <Typography.Text style={rowDescStyle}>
+              Refreshed at {formatRefreshed(lastRefreshedAt)}
+            </Typography.Text>
+          )}
+        </div>
+        <Switch checked={enabled} onChange={setEnabled} size="default" />
+      </div>
+
+      {/* Database Sync row — Recon only */}
+      {dbSync && (
+        <>
+          <div
+            style={{ height: 1, background: semanticColors.border, 
marginBlock: spacing.xs }}
+            aria-hidden
+          />
+          <div style={dropdownRowStyle}>
+            <div style={{ flex: 1 }}>
+              <div style={rowLabelStyle}>
+                <span>{dbSync.label}</span>
+              </div>
+              {dbSync.description && (
+                <Typography.Text 
style={rowDescStyle}>{dbSync.description}</Typography.Text>
+              )}
+            </div>
+            <IconButton
+              icon={<Icon name="reports" size={16} />}
+              label={dbSync.label}
+              tooltip={dbSync.tooltip ?? `Trigger ${dbSync.label}`}
+              loading={dbSyncing}
+              onClick={handleDbSync}
+            />
+          </div>
+        </>
+      )}
+    </div>
+  );
+
+  return (
+    <Dropdown
+      open={open}
+      onOpenChange={setOpen}
+      overlay={dropdownContent}

Review Comment:
   nit: Worth switching to `popupRender`? `overlay` is on antd's deprecated 
list. I just see it warning in dev.
   
   ```suggestion
         popupRender={() => dropdownContent}
   ```
   
   https://5x.ant.design/components/dropdown



##########
ozone-ui/packages/om/src/navigation.tsx:
##########
@@ -0,0 +1,88 @@
+/**
+ * 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 { type MenuItem } from '@ozone-ui/shared';
+import {
+  ApiOutlined,
+  BarChartOutlined,
+  BlockOutlined,
+  BookOutlined,
+  ClusterOutlined,
+  ControlOutlined,
+  DashboardOutlined,
+  HistoryOutlined,
+} from '@ant-design/icons';
+
+/** Common footprint for the navigation glyphs. */
+const ICON_SIZE = 18;
+const iconStyle = { fontSize: ICON_SIZE };
+
+/** A leaf navigation item paired with the icon it renders in the rail. */
+const navItem = (key: string, label: string, path: string, icon: 
MenuItem['icon']): MenuItem => ({
+  key,
+  label,
+  path,
+  icon,
+});
+
+/**
+ * Ozone Manager navigation rail. Mirrors the "Sidebar Navigation" in the 
design:
+ * primary items, then a "Diagnostics" group and a "Links" group.
+ */
+export const navItems: MenuItem[] = [
+  navItem('overview', 'Overview', '/', <DashboardOutlined style={iconStyle} 
/>),
+  navItem(
+    'configuration',
+    'Configuration',
+    '/configuration',
+    <ControlOutlined style={iconStyle} />
+  ),
+  {
+    type: 'group',
+    key: 'group-diagnostics',
+    label: 'Diagnostics',
+    children: [
+      navItem('rpc', 'Remote Procedure Call', '/rpc', <ApiOutlined 
style={iconStyle} />),
+      navItem(
+        'ozone-manager',
+        'Ozone Manager',
+        '/ozone-manager',
+        <ClusterOutlined style={iconStyle} />
+      ),
+      navItem('jmx', 'JMX', '/jmx-info', <BarChartOutlined style={iconStyle} 
/>),
+      navItem('stacks', 'Stacks', '/stacks', <BlockOutlined style={iconStyle} 
/>),
+    ],
+  },
+  {
+    type: 'group',
+    key: 'group-links',
+    label: 'Links',
+    children: [
+      navItem(
+        'documentation',
+        'Documentation',
+        '/documentation',
+        <BookOutlined style={iconStyle} />
+      ),
+      navItem('log-levels', 'Log levels', '/log-levels', <HistoryOutlined 
style={iconStyle} />),
+    ],
+  },
+];
+
+/** Product branding shown in the top utility bar. */

Review Comment:
   nit: This comment looks like it came from another place. Should it describe 
the sidebar width, or should it be dropped?



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