Skip to content
Merged
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
73 changes: 42 additions & 31 deletions src/components/App.jsx
Original file line number Diff line number Diff line change
@@ -1,21 +1,11 @@
import React, {useEffect, useCallback} from "react";
import React, {useEffect, useCallback, lazy, Suspense} from "react";
import {Routes, Route, Navigate, useNavigate} from "react-router-dom";
import OidcCallback from "./OidcCallback";
import SilentRenew from "./SilentRenew.jsx";
import AuthChoice from "./AuthChoice.jsx";
import Login from "./Login.jsx";
import '../styles/main.css';
import NodesTable from "./NodesTable";
import Objects from "./Objects";
import ObjectDetails from "./ObjectDetails";
import ClusterOverview from "./Cluster";
import NavBar from './NavBar';
import Namespaces from "./Namespaces";
import Heartbeats from "./Heartbeats";
import Pools from "./Pools";
import Network from "./Network";
import NetworkDetails from "./NetworkDetails";
import WhoAmI from "./WhoAmI";
import {OidcProvider, useOidc} from "../context/OidcAuthContext.tsx";
import {
AuthProvider,
Expand All @@ -31,6 +21,25 @@ import logger from "../utils/logger.js";
import {useDarkMode} from "../context/DarkModeContext";
import {ThemeProvider, createTheme} from '@mui/material/styles';

// Lazy load components for code splitting
const NodesTable = lazy(() => import("./NodesTable"));
const Objects = lazy(() => import("./Objects"));
const ObjectDetails = lazy(() => import("./ObjectDetails"));
const ClusterOverview = lazy(() => import("./Cluster"));
const Namespaces = lazy(() => import("./Namespaces"));
const Heartbeats = lazy(() => import("./Heartbeats"));
const Pools = lazy(() => import("./Pools"));
const Network = lazy(() => import("./Network"));
const NetworkDetails = lazy(() => import("./NetworkDetails"));
const WhoAmI = lazy(() => import("./WhoAmI"));

// Loading component for Suspense fallback
const Loading = () => (
<div className="flex items-center justify-center min-h-screen">
<div className="text-lg">Loading...</div>
</div>
);

const DynamicThemeProvider = ({children}) => {
const {isDarkMode} = useDarkMode();

Expand Down Expand Up @@ -309,26 +318,28 @@ const App = () => {
<DynamicThemeProvider>
<NavBar/>
<div className="min-h-screen bg-inherit">
<Routes>
<Route path="/" element={<Navigate to="/cluster" replace/>}/>
<Route path="/cluster" element={<ProtectedRoute><ClusterOverview/></ProtectedRoute>}/>
<Route path="/namespaces" element={<ProtectedRoute><Namespaces/></ProtectedRoute>}/>
<Route path="/heartbeats" element={<Heartbeats/>}/>
<Route path="/nodes" element={<ProtectedRoute><NodesTable/></ProtectedRoute>}/>
<Route path="/storage-pools" element={<ProtectedRoute><Pools/></ProtectedRoute>}/>
<Route path="/network" element={<ProtectedRoute><Network/></ProtectedRoute>}/>
<Route path="/network/:networkName"
element={<ProtectedRoute><NetworkDetails/></ProtectedRoute>}/>
<Route path="/objects" element={<ProtectedRoute><Objects/></ProtectedRoute>}/>
<Route path="/objects/:objectName"
element={<ProtectedRoute><ObjectDetails/></ProtectedRoute>}/>
<Route path="/whoami" element={<ProtectedRoute><WhoAmI/></ProtectedRoute>}/>
<Route path="/silent-renew" element={<SilentRenew/>}/>
<Route path="/auth-callback" element={<OidcCallback/>}/>
<Route path="/auth-choice" element={<AuthChoice/>}/>
<Route path="/auth/login" element={<Login/>}/>
<Route path="*" element={<Navigate to="/"/>}/>
</Routes>
<Suspense fallback={<Loading/>}>
<Routes>
<Route path="/" element={<Navigate to="/cluster" replace/>}/>
<Route path="/cluster" element={<ProtectedRoute><ClusterOverview/></ProtectedRoute>}/>
<Route path="/namespaces" element={<ProtectedRoute><Namespaces/></ProtectedRoute>}/>
<Route path="/heartbeats" element={<Heartbeats/>}/>
<Route path="/nodes" element={<ProtectedRoute><NodesTable/></ProtectedRoute>}/>
<Route path="/storage-pools" element={<ProtectedRoute><Pools/></ProtectedRoute>}/>
<Route path="/network" element={<ProtectedRoute><Network/></ProtectedRoute>}/>
<Route path="/network/:networkName"
element={<ProtectedRoute><NetworkDetails/></ProtectedRoute>}/>
<Route path="/objects" element={<ProtectedRoute><Objects/></ProtectedRoute>}/>
<Route path="/objects/:objectName"
element={<ProtectedRoute><ObjectDetails/></ProtectedRoute>}/>
<Route path="/whoami" element={<ProtectedRoute><WhoAmI/></ProtectedRoute>}/>
<Route path="/silent-renew" element={<SilentRenew/>}/>
<Route path="/auth-callback" element={<OidcCallback/>}/>
<Route path="/auth-choice" element={<AuthChoice/>}/>
<Route path="/auth/login" element={<Login/>}/>
<Route path="*" element={<Navigate to="/"/>}/>
</Routes>
</Suspense>
</div>
</DynamicThemeProvider>
</OidcInitializer>
Expand Down
181 changes: 103 additions & 78 deletions src/components/Cluster.jsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
import logger from '../utils/logger.js';
import React, {useEffect, useState, useRef} from "react";
import React, {useEffect, useState, useRef, useMemo} from "react";
import {useNavigate} from "react-router-dom";
import {Box, Typography} from "@mui/material";
import axios from "axios";

import useEventStore from "../hooks/useEventStore.js";
import {
GridNodes,
Expand All @@ -19,6 +18,7 @@ import EventLogger from "../components/EventLogger";

const ClusterOverview = () => {
const navigate = useNavigate();

const nodeStatus = useEventStore((state) => state.nodeStatus);
const objectStatus = useEventStore((state) => state.objectStatus);
const heartbeatStatus = useEventStore((state) => state.heartbeatStatus);
Expand Down Expand Up @@ -49,6 +49,7 @@ const ClusterOverview = () => {

if (token) {
startEventReception(token);

// Fetch pools
axios.get(URL_POOL, {
headers: {Authorization: `Bearer ${token}`}
Expand Down Expand Up @@ -79,89 +80,113 @@ const ClusterOverview = () => {
setNetworks([]);
});
}

return () => {
isMounted.current = false;
};
}, []);

const nodeCount = Object.keys(nodeStatus).length;
const nodeStats = useMemo(() => {
const count = Object.keys(nodeStatus).length;
let frozen = 0;
let unfrozen = 0;

let frozenCount = 0;
let unfrozenCount = 0;
Object.values(nodeStatus).forEach((node) => {
const isFrozen = node?.frozen_at && node?.frozen_at !== "0001-01-01T00:00:00Z";
if (isFrozen) frozen++;
else unfrozen++;
});

Object.values(nodeStatus).forEach((node) => {
const isFrozen = node?.frozen_at && node?.frozen_at !== "0001-01-01T00:00:00Z";
if (isFrozen) frozenCount++;
else unfrozenCount++;
});
return {count, frozen, unfrozen};
}, [nodeStatus]);

const namespaces = new Set();
const statusCount = {up: 0, down: 0, warn: 0, "n/a": 0, unprovisioned: 0};
const objectsPerNamespace = {};
const statusPerNamespace = {};
const objectStats = useMemo(() => {
const namespaces = new Set();
const statusCount = {up: 0, down: 0, warn: 0, "n/a": 0, unprovisioned: 0};
const objectsPerNamespace = {};
const statusPerNamespace = {};

const extractNamespace = (objectPath) => {
const parts = objectPath.split("/");
return parts.length === 3 ? parts[0] : "root";
};
const extractNamespace = (objectPath) => {
const parts = objectPath.split("/");
return parts.length === 3 ? parts[0] : "root";
};

Object.entries(objectStatus).forEach(([objectPath, status]) => {
const ns = extractNamespace(objectPath);
namespaces.add(ns);
objectsPerNamespace[ns] = (objectsPerNamespace[ns] || 0) + 1;
Object.entries(objectStatus).forEach(([objectPath, status]) => {
const ns = extractNamespace(objectPath);
namespaces.add(ns);
objectsPerNamespace[ns] = (objectsPerNamespace[ns] || 0) + 1;

const s = status?.avail?.toLowerCase() || "n/a";
if (!statusPerNamespace[ns]) {
statusPerNamespace[ns] = {up: 0, down: 0, warn: 0, "n/a": 0, unprovisioned: 0};
}
if (s === "up" || s === "down" || s === "warn" || s === "n/a") {
statusPerNamespace[ns][s]++;
statusCount[s]++;
} else {
statusPerNamespace[ns]["n/a"]++;
statusCount["n/a"]++;
}
const s = status?.avail?.toLowerCase() || "n/a";
if (!statusPerNamespace[ns]) {
statusPerNamespace[ns] = {up: 0, down: 0, warn: 0, "n/a": 0, unprovisioned: 0};
}

// Count unprovisioned objects
const provisioned = status?.provisioned;
const isUnprovisioned = provisioned === "false" || provisioned === false;
if (isUnprovisioned) {
statusPerNamespace[ns].unprovisioned++;
statusCount.unprovisioned++;
}
});
if (s === "up" || s === "down" || s === "warn" || s === "n/a") {
statusPerNamespace[ns][s]++;
statusCount[s]++;
} else {
statusPerNamespace[ns]["n/a"]++;
statusCount["n/a"]++;
}

const namespaceCount = namespaces.size;
// Count unprovisioned objects
const provisioned = status?.provisioned;
const isUnprovisioned = provisioned === "false" || provisioned === false;
if (isUnprovisioned) {
statusPerNamespace[ns].unprovisioned++;
statusCount.unprovisioned++;
}
});

const namespaceSubtitle = Object.entries(objectsPerNamespace)
.map(([ns, count]) => ({namespace: ns, count, status: statusPerNamespace[ns]}));
const namespaceSubtitle = Object.entries(objectsPerNamespace)
.map(([ns, count]) => ({
namespace: ns,
count,
status: statusPerNamespace[ns]
}));

const heartbeatIds = new Set();
let beatingCount = 0;
let staleCount = 0;
const stateCount = {running: 0, stopped: 0, failed: 0, warning: 0, unknown: 0};
return {
objectCount: Object.keys(objectStatus).length,
namespaceCount: namespaces.size,
statusCount,
namespaceSubtitle
};
}, [objectStatus]);

Object.values(heartbeatStatus).forEach(node => {
(node.streams || []).forEach(stream => {
const peer = Object.values(stream.peers || {})[0];
const baseId = stream.id.split('.')[0];
heartbeatIds.add(baseId);
const heartbeatStats = useMemo(() => {
const heartbeatIds = new Set();
let beating = 0;
let stale = 0;
const stateCount = {running: 0, stopped: 0, failed: 0, warning: 0, unknown: 0};

if (peer?.is_beating) {
beatingCount++;
} else {
staleCount++;
}
Object.values(heartbeatStatus).forEach(node => {
(node.streams || []).forEach(stream => {
const peer = Object.values(stream.peers || {})[0];
const baseId = stream.id?.split('.')[0];
if (baseId) heartbeatIds.add(baseId);

const state = stream.state || 'unknown';
if (stateCount.hasOwnProperty(state)) {
stateCount[state]++;
} else {
stateCount.unknown++;
}
if (peer?.is_beating) {
beating++;
} else {
stale++;
}

const state = stream.state || 'unknown';
if (stateCount.hasOwnProperty(state)) {
stateCount[state]++;
} else {
stateCount.unknown++;
}
});
});
});
const heartbeatCount = heartbeatIds.size;

return {
count: heartbeatIds.size,
beating,
stale,
stateCount
};
}, [heartbeatStatus]);

return (
<Box sx={{
Expand Down Expand Up @@ -204,26 +229,26 @@ const ClusterOverview = () => {
}}>
<Box>
<GridNodes
nodeCount={nodeCount}
frozenCount={frozenCount}
unfrozenCount={unfrozenCount}
nodeCount={nodeStats.count}
frozenCount={nodeStats.frozen}
unfrozenCount={nodeStats.unfrozen}
onClick={() => navigate("/nodes")}
/>
</Box>
<Box>
<GridObjects
objectCount={Object.keys(objectStatus).length}
statusCount={statusCount}
objectCount={objectStats.objectCount}
statusCount={objectStats.statusCount}
onClick={(globalState) => navigate(globalState ? `/objects?globalState=${globalState}` : '/objects')}
/>
</Box>
<Box>
<GridHeartbeats
heartbeatCount={heartbeatCount}
beatingCount={beatingCount}
nonBeatingCount={staleCount}
stateCount={stateCount}
nodeCount={nodeCount}
heartbeatCount={heartbeatStats.count}
beatingCount={heartbeatStats.beating}
nonBeatingCount={heartbeatStats.stale}
stateCount={heartbeatStats.stateCount}
nodeCount={nodeStats.count}
onClick={(status, state) => {
const params = new URLSearchParams();
if (status) params.append('status', status);
Expand All @@ -249,8 +274,8 @@ const ClusterOverview = () => {
{/* Right side - Namespaces */}
<Box sx={{flex: 1}}>
<GridNamespaces
namespaceCount={namespaceCount}
namespaceSubtitle={namespaceSubtitle}
namespaceCount={objectStats.namespaceCount}
namespaceSubtitle={objectStats.namespaceSubtitle}
onClick={(url) => navigate(url || "/namespaces")}
/>
</Box>
Expand Down
8 changes: 3 additions & 5 deletions src/components/Network.jsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
import React, {useEffect, useState, useRef} from "react";
import React, {useEffect, useState, useRef, useMemo} from "react";
import {useNavigate} from "react-router-dom";
import {
Box,
Paper,
Typography,
Table,
TableBody,
Expand Down Expand Up @@ -49,10 +48,10 @@ const Network = () => {
}
};

fetchNetworks();
void fetchNetworks();
}, []);

const sortedNetworks = React.useMemo(() => {
const sortedNetworks = useMemo(() => {
return [...networks].sort((a, b) => {
let diff = 0;
if (sortColumn === "name") {
Expand Down Expand Up @@ -109,7 +108,6 @@ const Network = () => {
}}
>
<TableContainer
component={Paper}
sx={{
flex: 1,
minHeight: 0,
Expand Down
2 changes: 0 additions & 2 deletions src/components/NetworkDetails.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ import React, {useEffect, useState, useMemo, useRef} from "react";
import {useParams} from "react-router-dom";
import {
Box,
Paper,
Typography,
Table,
TableBody,
Expand Down Expand Up @@ -244,7 +243,6 @@ const NetworkDetails = () => {
</Box>
) : (
<TableContainer
component={Paper}
sx={{
flex: 1,
minHeight: 0,
Expand Down
Loading