diff --git a/src/components/NodeCard.jsx b/src/components/InstanceCard.jsx
similarity index 72%
rename from src/components/NodeCard.jsx
rename to src/components/InstanceCard.jsx
index 4ba398e..f90e3ea 100644
--- a/src/components/NodeCard.jsx
+++ b/src/components/InstanceCard.jsx
@@ -1,4 +1,4 @@
-import React, {forwardRef, useState} from "react";
+import React, {useState} from "react";
import {
Box,
Typography,
@@ -14,17 +14,7 @@ import ArticleIcon from "@mui/icons-material/Article";
import {grey, blue, red} from "@mui/material/colors";
import logger from '../utils/logger.js';
-const BoxWithRef = forwardRef((props, ref) => (
-
-));
-BoxWithRef.displayName = 'BoxWithRef';
-
-const IconButtonWithRef = forwardRef((props, ref) => (
-
-));
-IconButtonWithRef.displayName = 'IconButtonWithRef';
-
-const NodeCard = ({
+const InstanceCard = ({
node,
nodeData = {},
selectedNodes = [],
@@ -32,10 +22,8 @@ const NodeCard = ({
actionInProgress = false,
setIndividualNodeMenuAnchor = () => logger.warn("setIndividualNodeMenuAnchor not provided"),
setCurrentNode = () => logger.warn("setCurrentNode not provided"),
- individualNodeMenuAnchorRef = null,
getColor = () => grey[500],
getNodeState = () => ({avail: "unknown", frozen: "unfrozen", state: null}),
- parseProvisionedState = (state) => !!state,
instanceName,
onOpenLogs = () => logger.warn("onOpenLogs not provided"),
onViewInstance,
@@ -49,7 +37,7 @@ const NodeCard = ({
}
const {avail, frozen, state} = getNodeState(node);
- const isInstanceNotProvisioned = nodeData?.provisioned !== undefined ? !parseProvisionedState(nodeData.provisioned) : false;
+ const isInstanceNotProvisioned = nodeData?.provisioned === false || nodeData?.provisioned === "false";
const handleCardClick = (e) => {
if (e.target.closest('button') || e.target.closest('input') || e.target.closest('.no-click')) {
@@ -60,8 +48,14 @@ const NodeCard = ({
}
};
+ const handleMenuOpen = (e) => {
+ e.stopPropagation();
+ setCurrentNode(node);
+ setIndividualNodeMenuAnchor(e.currentTarget);
+ };
+
return (
-
- {/* Bloc gauche : checkbox + nom */}
e.stopPropagation()} className="no-click">
(view resources)
@@ -112,16 +101,7 @@ const NodeCard = ({
- {/* Bloc central : freeze / not provisioned / state, juste avant logs+status */}
-
+
{frozen === "frozen" && (
@@ -138,17 +118,9 @@ const NodeCard = ({
{state && {state}}
- {/* Bloc droite : logs + rond de status + bouton d’actions, fixes */}
-
+
- {
e.stopPropagation();
onOpenLogs(node, resolvedInstanceName);
@@ -157,7 +129,7 @@ const NodeCard = ({
aria-label={`View logs for instance ${resolvedInstanceName || node}`}
>
-
+
@@ -169,25 +141,19 @@ const NodeCard = ({
/>
- {
- e.stopPropagation();
- e.persist();
- setCurrentNode(node);
- setIndividualNodeMenuAnchor(e.currentTarget);
- }}
+
-
+
-
+
);
};
-export default NodeCard;
+export default InstanceCard;
diff --git a/src/components/ObjectDetails.jsx b/src/components/ObjectDetails.jsx
index 27b3432..31db47e 100644
--- a/src/components/ObjectDetails.jsx
+++ b/src/components/ObjectDetails.jsx
@@ -5,23 +5,21 @@ import {
Box,
Button,
CircularProgress,
- ClickAwayListener,
Dialog,
DialogActions,
DialogContent,
DialogTitle,
- ListItemIcon,
- ListItemText,
+ Menu,
MenuItem,
- Paper,
- Popper,
- Snackbar,
Typography,
Drawer,
IconButton,
TextField,
useTheme,
Grid,
+ Snackbar,
+ ListItemIcon,
+ ListItemText,
} from "@mui/material";
import CloseIcon from "@mui/icons-material/Close";
import {green, grey, orange, red} from "@mui/material/colors";
@@ -33,7 +31,7 @@ import ActionDialogManager from "../components/ActionDialogManager";
import HeaderSection from "./HeaderSection";
import ConfigSection from "./ConfigSection";
import KeysSection from "./KeysSection";
-import NodeCard from "./NodeCard";
+import InstanceCard from "./InstanceCard.jsx";
import LogsViewer from "./LogsViewer";
import {INSTANCE_ACTIONS, OBJECT_ACTIONS} from "../constants/actions";
import {parseObjectPath} from "../utils/objectUtils.jsx";
@@ -67,29 +65,25 @@ const ObjectDetail = () => {
const decodedObjectName = decodeURIComponent(objectName);
const {namespace, kind, name} = parseObjectPath(decodedObjectName);
const navigate = useNavigate();
+ const theme = useTheme();
- const objectStatus = useEventStore((s) => s.objectStatus);
- const objectInstanceStatus = useEventStore((s) => s.objectInstanceStatus);
+ const objectStatus = useEventStore((s) => s.objectStatus[decodedObjectName]);
+ const objectInstanceStatus = useEventStore((s) => s.objectInstanceStatus[decodedObjectName]);
const instanceMonitor = useEventStore((s) => s.instanceMonitor);
- const instanceConfig = useEventStore((s) => s.instanceConfig);
+ const instanceConfig = useEventStore((s) => s.instanceConfig[decodedObjectName]);
const clearConfigUpdate = useEventStore((s) => s.clearConfigUpdate);
- const theme = useTheme();
-
const [configLoading, setConfigLoading] = useState(false);
const [configError, setConfigError] = useState(null);
const [configNode, setConfigNode] = useState(null);
const [configDialogOpen, setConfigDialogOpen] = useState(false);
const [selectedNodes, setSelectedNodes] = useState([]);
- const [nodesActionsAnchor, setNodesActionsAnchor] = useState(null);
- const nodesActionsAnchorRef = useRef(null);
+ const [actionsMenuAnchor, setActionsMenuAnchor] = useState(null);
const [individualNodeMenuAnchor, setIndividualNodeMenuAnchor] = useState(null);
- const individualNodeMenuAnchorRef = useRef(null);
const [currentNode, setCurrentNode] = useState(null);
const [objectMenuAnchor, setObjectMenuAnchor] = useState(null);
- const objectMenuAnchorRef = useRef(null);
const [pendingAction, setPendingAction] = useState(null);
const [actionInProgress, setActionInProgress] = useState(false);
@@ -137,13 +131,37 @@ const ObjectDetail = () => {
const isProcessingConfigUpdate = useRef(false);
const isMounted = useRef(true);
- useEffect(() => {
- isMounted.current = true;
- return () => {
- isMounted.current = false;
- closeEventSource();
- };
- }, [decodedObjectName]);
+ const objectData = useMemo(() => {
+ const avail = objectStatus?.avail || "n/a";
+ const frozen = objectStatus?.frozen === "frozen" ? "frozen" : "unfrozen";
+ let globalExpect = null;
+ if (objectInstanceStatus) {
+ for (const node of Object.keys(objectInstanceStatus)) {
+ const monitorKey = `${node}:${decodedObjectName}`;
+ const monitor = instanceMonitor[monitorKey] || {};
+ if (monitor.global_expect && monitor.global_expect !== "none") {
+ globalExpect = monitor.global_expect;
+ break;
+ }
+ }
+ }
+ return {avail, frozen, globalExpect};
+ }, [objectStatus, objectInstanceStatus, instanceMonitor, decodedObjectName]);
+
+ const memoizedObjectData = useMemo(() => {
+ if (!objectInstanceStatus) return {};
+ const enhanced = {};
+ Object.keys(objectInstanceStatus).forEach(node => {
+ enhanced[node] = {
+ ...objectInstanceStatus[node],
+ instanceConfig: instanceConfig?.[node] || {resources: {}},
+ instanceMonitor: instanceMonitor[`${node}:${decodedObjectName}`] || {resources: {}},
+ };
+ });
+ return enhanced;
+ }, [objectInstanceStatus, instanceConfig, instanceMonitor, decodedObjectName]);
+
+ const nodesList = useMemo(() => Object.keys(memoizedObjectData), [memoizedObjectData]);
const fetchInitialObjectData = useCallback(async () => {
setInitialDataError(null);
@@ -177,36 +195,34 @@ const ObjectDetail = () => {
}
}, [decodedObjectName]);
- const openSnackbar = useCallback((msg, sev = "success") => {
- setSnackbar({open: true, message: msg, severity: sev});
- }, []);
-
- const closeSnackbar = useCallback(() => {
- setSnackbar((s) => ({...s, open: false}));
- }, []);
+ useEffect(() => {
+ const token = localStorage.getItem("authToken");
+ if (token) {
+ const filters = objectEventTypes.map(type => {
+ if (["CONNECTION_OPENED", "CONNECTION_ERROR", "RECONNECTION_ATTEMPT", "MAX_RECONNECTIONS_REACHED", "CONNECTION_CLOSED"].includes(type)) {
+ return type;
+ } else {
+ return `${type},path=${decodedObjectName}`;
+ }
+ });
+ startEventReception(token, filters);
+ }
+ return () => closeEventSource();
+ }, [decodedObjectName, objectEventTypes]);
- const openActionDialog = useCallback((action, context = null) => {
- setPendingAction({action, ...(context ? context : {})});
- setSeats(1);
- setGreetTimeout("5s");
- if (action === "console") {
- setConsoleDialogOpen(true);
- } else if (action === "freeze") {
- setCheckboxes(DEFAULT_CHECKBOXES);
- setConfirmDialogOpen(true);
- } else if (action === "stop") {
- setStopCheckbox(DEFAULT_STOP_CHECKBOX);
- setStopDialogOpen(true);
- } else if (action === "unprovision") {
- setUnprovisionCheckboxes(DEFAULT_UNPROVISION_CHECKBOXES);
- setUnprovisionDialogOpen(true);
- } else if (action === "purge") {
- setPurgeCheckboxes(DEFAULT_PURGE_CHECKBOXES);
- setPurgeDialogOpen(true);
+ useEffect(() => {
+ const hasData = objectInstanceStatus && Object.keys(objectInstanceStatus).length > 0;
+ if (!hasData) {
+ fetchInitialObjectData().finally(() => setInitialLoading(false));
} else {
- setSimpleDialogOpen(true);
+ setInitialLoading(false);
}
+ }, [objectInstanceStatus, fetchInitialObjectData]);
+
+ const openSnackbar = useCallback((msg, sev = "success") => {
+ setSnackbar({open: true, message: msg, severity: sev});
}, []);
+ const closeSnackbar = useCallback(() => setSnackbar((s) => ({...s, open: false})), []);
const postActionUrl = useCallback(({node, objectName, action}) => {
const {namespace, kind, name} = parseObjectPath(objectName);
@@ -329,57 +345,28 @@ const ObjectDetail = () => {
}
}, [decodedObjectName, configLoading]);
- const getColor = useCallback((status) => {
- if (status === "up" || status === true) return green[500];
- if (status === "down" || status === false) return red[500];
- if (status === "warn") return orange[500];
- return grey[500];
- }, []);
-
- const getNodeState = useCallback((node) => {
- const instanceStatus = objectInstanceStatus[decodedObjectName] || {};
- const monitorKey = `${node}:${decodedObjectName}`;
- const monitor = instanceMonitor[monitorKey] || {};
- const avail = instanceStatus[node]?.avail || '';
- const frozen = instanceStatus[node]?.frozen_at && instanceStatus[node]?.frozen_at !== "0001-01-01T00:00:00Z" ? "frozen" : "unfrozen";
- const state = monitor.state !== "idle" ? monitor.state : null;
- return {avail, frozen, state};
- }, [objectInstanceStatus, instanceMonitor, decodedObjectName]);
-
- const getObjectStatus = useCallback(() => {
- const obj = objectStatus[decodedObjectName] || {};
- const avail = obj?.avail;
- const frozen = obj?.frozen;
- const nodes = Object.keys(objectInstanceStatus[decodedObjectName] || {});
- let globalExpect = null;
- for (const node of nodes) {
- const monitorKey = `${node}:${decodedObjectName}`;
- const monitor = instanceMonitor[monitorKey] || {};
- if (monitor.global_expect && monitor.global_expect !== "none") {
- globalExpect = monitor.global_expect;
- break;
- }
+ const openActionDialog = useCallback((action, context = null) => {
+ setPendingAction({action, ...(context ? context : {})});
+ setSeats(1);
+ setGreetTimeout("5s");
+ if (action === "console") {
+ setConsoleDialogOpen(true);
+ } else if (action === "freeze") {
+ setCheckboxes(DEFAULT_CHECKBOXES);
+ setConfirmDialogOpen(true);
+ } else if (action === "stop") {
+ setStopCheckbox(DEFAULT_STOP_CHECKBOX);
+ setStopDialogOpen(true);
+ } else if (action === "unprovision") {
+ setUnprovisionCheckboxes(DEFAULT_UNPROVISION_CHECKBOXES);
+ setUnprovisionDialogOpen(true);
+ } else if (action === "purge") {
+ setPurgeCheckboxes(DEFAULT_PURGE_CHECKBOXES);
+ setPurgeDialogOpen(true);
+ } else {
+ setSimpleDialogOpen(true);
}
- return {avail, frozen, globalExpect};
- }, [objectStatus, objectInstanceStatus, instanceMonitor, decodedObjectName]);
-
- const handleNodesActionsOpen = useCallback((e) => setNodesActionsAnchor(e.currentTarget), []);
- const handleNodesActionsClose = useCallback(() => setNodesActionsAnchor(null), []);
- const handleBatchNodeActionClick = useCallback((action) => {
- openActionDialog(action, {batch: "nodes"});
- handleNodesActionsClose();
- }, [openActionDialog, handleNodesActionsClose]);
-
- const handleIndividualNodeActionClick = useCallback((action) => {
- if (!currentNode) return;
- openActionDialog(action, {node: currentNode});
- setIndividualNodeMenuAnchor(null);
- }, [openActionDialog, currentNode]);
-
- const handleObjectActionClick = useCallback((action) => {
- openActionDialog(action);
- setObjectMenuAnchor(null);
- }, [openActionDialog]);
+ }, []);
const handleDialogConfirm = useCallback(() => {
if (!pendingAction || !pendingAction.action) {
@@ -428,6 +415,22 @@ const ObjectDetail = () => {
setSelectedNodes(prev => prev.includes(node) ? prev.filter(n => n !== node) : [...prev, node]);
}, []);
+ const handleBatchNodeActionClick = (action) => {
+ openActionDialog(action, {batch: "nodes"});
+ setActionsMenuAnchor(null);
+ };
+
+ const handleIndividualNodeActionClick = (action) => {
+ if (!currentNode) return;
+ openActionDialog(action, {node: currentNode});
+ setIndividualNodeMenuAnchor(null);
+ };
+
+ const handleObjectActionClick = (action) => {
+ openActionDialog(action);
+ setObjectMenuAnchor(null);
+ };
+
const handleViewInstance = useCallback((node) => {
navigate(`/nodes/${node}/objects/${encodeURIComponent(decodedObjectName)}`);
}, [decodedObjectName, navigate]);
@@ -444,6 +447,22 @@ const ObjectDetail = () => {
setSelectedInstanceForLogs(null);
}, []);
+ const getColor = useCallback((status) => {
+ if (status === "up" || status === true) return green[500];
+ if (status === "down" || status === false) return red[500];
+ if (status === "warn") return orange[500];
+ return grey[500];
+ }, []);
+
+ const getNodeState = useCallback((node) => {
+ const nodeStatus = objectInstanceStatus?.[node] || {};
+ const monitor = instanceMonitor[`${node}:${decodedObjectName}`] || {};
+ const avail = nodeStatus.avail || '';
+ const frozen = nodeStatus.frozen_at && nodeStatus.frozen_at !== "0001-01-01T00:00:00Z" ? "frozen" : "unfrozen";
+ const state = monitor.state !== "idle" ? monitor.state : null;
+ return {avail, frozen, state};
+ }, [objectInstanceStatus, instanceMonitor, decodedObjectName]);
+
const startResizing = useCallback((e) => {
e.preventDefault();
const isTouch = e.type === 'touchstart';
@@ -474,61 +493,28 @@ const ObjectDetail = () => {
document.body.style.cursor = "ew-resize";
}, [drawerWidth, minDrawerWidth, maxDrawerWidth]);
- // SSE + initial data fetch
- useEffect(() => {
- const token = localStorage.getItem("authToken");
- if (token) {
- const filters = objectEventTypes.map(type => {
- if (["CONNECTION_OPENED", "CONNECTION_ERROR", "RECONNECTION_ATTEMPT", "MAX_RECONNECTIONS_REACHED", "CONNECTION_CLOSED"].includes(type)) {
- return type;
- } else {
- return `${type},path=${decodedObjectName}`;
- }
- });
- startEventReception(token, filters);
- }
- const hasData = objectInstanceStatus[decodedObjectName] && Object.keys(objectInstanceStatus[decodedObjectName]).length > 0;
- if (!hasData) fetchInitialObjectData();
- return () => closeEventSource();
- }, [decodedObjectName, objectEventTypes, fetchInitialObjectData, objectInstanceStatus]);
-
- // Initial config loading effect (restored with immediate false if data exists)
- const objectData = objectInstanceStatus?.[decodedObjectName];
useEffect(() => {
const loadInitialConfig = async () => {
- if (objectData) {
- // If we already have instance data, we can stop the loading indicator immediately
- setInitialLoading(false);
- const nodes = Object.keys(objectInstanceStatus[decodedObjectName] || {});
+ if (objectInstanceStatus && Object.keys(objectInstanceStatus).length > 0) {
+ const nodes = Object.keys(objectInstanceStatus);
const initialNode = nodes.find((node) => {
- return objectData[node]?.encap && Object.values(objectData[node].encap).some(
+ const nodeData = objectInstanceStatus[node];
+ return nodeData?.encap && Object.values(nodeData.encap).some(
(container) => container.resources && Object.keys(container.resources).length > 0
);
}) || nodes[0];
if (initialNode) {
- try {
- await fetchConfig(initialNode);
- } catch (err) {
- setConfigError("Failed to load initial configuration.");
- }
- } else {
- setConfigError("No nodes available to fetch configuration.");
+ await fetchConfig(initialNode);
}
- } else {
- setConfigError("No object data available.");
- setInitialLoading(false);
}
};
- loadInitialConfig().catch(() => {
- });
- }, [decodedObjectName, objectData, objectInstanceStatus, fetchConfig]);
+ loadInitialConfig();
+ }, [objectInstanceStatus, fetchConfig]);
- // Config updates effect
useEffect(() => {
- if (!isMounted.current) return;
- let subscription;
+ let unsubscribe = null;
try {
- subscription = useEventStore.subscribe(
+ const maybeUnsubscribe = useEventStore.subscribe(
(state) => state.configUpdates,
async (updates) => {
if (!isMounted.current || isProcessingConfigUpdate.current) return;
@@ -552,24 +538,25 @@ const ObjectDetail = () => {
},
{fireImmediately: false}
);
+ if (typeof maybeUnsubscribe === 'function') {
+ unsubscribe = maybeUnsubscribe;
+ } else {
+ logger.warn("[ObjectDetail] Subscription is not a function:", maybeUnsubscribe);
+ }
} catch (err) {
logger.warn("[ObjectDetail] Failed to subscribe to configUpdates:", err);
}
return () => {
- if (typeof subscription === 'function') subscription();
- else if (subscription) logger.warn("[ObjectDetail] Subscription is not a function:", subscription);
+ if (typeof unsubscribe === 'function') unsubscribe();
};
}, [decodedObjectName, clearConfigUpdate, fetchConfig, openSnackbar]);
- // Instance config effect
useEffect(() => {
- if (!isMounted.current) return;
- let subscription;
+ let unsubscribe = null;
try {
- subscription = useEventStore.subscribe(
+ const maybeUnsubscribe = useEventStore.subscribe(
(state) => state.instanceConfig,
(newConfig) => {
- if (!isMounted.current) return;
const config = newConfig[decodedObjectName];
if (config && configNode) {
setConfigDialogOpen(true);
@@ -577,30 +564,20 @@ const ObjectDetail = () => {
}
}
);
+ if (typeof maybeUnsubscribe === 'function') {
+ unsubscribe = maybeUnsubscribe;
+ } else {
+ logger.warn("[ObjectDetail] Subscription is not a function:", maybeUnsubscribe);
+ }
} catch (err) {
logger.warn("[ObjectDetail] Failed to subscribe to instanceConfig:", err);
- return;
}
return () => {
- if (typeof subscription === 'function') subscription();
- else if (subscription) logger.warn("[ObjectDetail] Subscription is not a function:", subscription);
+ if (typeof unsubscribe === 'function') unsubscribe();
};
}, [decodedObjectName, configNode, openSnackbar]);
- const memoizedObjectData = useMemo(() => {
- const enhanced = {};
- const data = objectInstanceStatus?.[decodedObjectName] || {};
- Object.keys(data).forEach(node => {
- enhanced[node] = {
- ...data[node],
- instanceConfig: instanceConfig?.[decodedObjectName]?.[node] || {resources: {}},
- instanceMonitor: instanceMonitor[`${node}:${decodedObjectName}`] || {resources: {}},
- };
- });
- return enhanced;
- }, [objectInstanceStatus, instanceConfig, instanceMonitor, decodedObjectName]);
-
- const memoizedNodes = useMemo(() => Object.keys(memoizedObjectData), [memoizedObjectData]);
+ const showKeys = ["cfg", "sec"].includes(kind);
if (initialLoading) {
return (
@@ -611,13 +588,12 @@ const ObjectDetail = () => {
);
}
- const showKeys = ["cfg", "sec"].includes(kind);
- if (Object.keys(memoizedObjectData).length === 0) {
+ if (nodesList.length === 0 && !initialDataError) {
return (
{decodedObjectName}
- {initialDataError ? `Error loading object: ${initialDataError}` : "No information available for object."}
+ No information available for object.
{showKeys && }
{
}}>
{
objectData}
getColor={getColor}
- objectMenuAnchorRef={objectMenuAnchorRef}
/>
@@ -741,7 +717,8 @@ const ObjectDetail = () => {
helperText="Number of simultaneous users allowed in the console"/>
setGreetTimeout(e.target.value)}
+ value={greetTimeout}
+ onChange={(e) => setGreetTimeout(e.target.value)}
helperText="Time to wait for console connection (e.g., 5s, 10s)"/>
@@ -751,15 +728,8 @@ const ObjectDetail = () => {
{showKeys && }
@@ -802,12 +772,32 @@ const ObjectDetail = () => {
{!["sec", "cfg", "usr"].includes(kind) && (
<>
-
+
- {memoizedNodes.map(node => (
- setActionsMenuAnchor(null)}
+ anchorOrigin={{vertical: 'bottom', horizontal: 'right'}}
+ transformOrigin={{vertical: 'top', horizontal: 'right'}}
+ >
+ {INSTANCE_ACTIONS.map(({name, icon}) => (
+
+ ))}
+
+
+ {nodesList.map(node => (
+ {
parseProvisionedState={parseProvisionedState}
setPendingAction={setPendingAction}
setSimpleDialogOpen={setSimpleDialogOpen}
- individualNodeMenuAnchorRef={individualNodeMenuAnchorRef}
namespace={namespace}
kind={kind}
instanceName={name}
@@ -830,46 +819,21 @@ const ObjectDetail = () => {
onViewInstance={handleViewInstance}
/>
))}
-
-
-
- {INSTANCE_ACTIONS.map(({name, icon}) => (
-
- ))}
-
-
-
-
- setIndividualNodeMenuAnchor(null)}>
-
- {INSTANCE_ACTIONS.map(({name, icon}) => (
-
- ))}
-
-
-
+
+
>
)}
@@ -886,7 +850,7 @@ const ObjectDetail = () => {
{logsDrawerOpen && selectedNodeForLogs && (
() => );
jest.mock('@mui/icons-material/PriorityHigh', () => () => );
-describe('NodeCard Component', () => {
+describe('InstanceCard Component', () => {
const user = userEvent.setup();
beforeEach(() => {
@@ -77,7 +77,7 @@ describe('NodeCard Component', () => {
test('renders node name correctly', () => {
render(
-
+
);
@@ -92,7 +92,7 @@ describe('NodeCard Component', () => {
render(
-
+
);
@@ -104,7 +104,7 @@ describe('NodeCard Component', () => {
render(
-
+
);
@@ -120,7 +120,7 @@ describe('NodeCard Component', () => {
render(
-
+
);
@@ -135,7 +135,7 @@ describe('NodeCard Component', () => {
render(
-
+
);
@@ -150,7 +150,7 @@ describe('NodeCard Component', () => {
render(
-
+
);
@@ -175,7 +175,7 @@ describe('NodeCard Component', () => {
render(
- {
render(
- {
render(
-
+
);
expect(screen.getByTestId('AcUnitIcon')).toBeInTheDocument();
});
- test('shows not provisioned icon when instance is not provisioned', () => {
+ test('shows not provisioned icon when instance is not provisioned (false)', () => {
const nodeData = {provisioned: false};
- const parseProvisionedState = jest.fn(() => false);
render(
-
+
+
+ );
+
+ expect(screen.getByTestId('PriorityHighIcon')).toBeInTheDocument();
+ });
+
+ test('shows not provisioned icon when instance is not provisioned ("false")', () => {
+ const nodeData = {provisioned: "false"};
+
+ render(
+
+
);
expect(screen.getByTestId('PriorityHighIcon')).toBeInTheDocument();
- expect(parseProvisionedState).toHaveBeenCalledWith(false);
+ });
+
+ test('does NOT show not provisioned icon when provisioned is "n/a"', () => {
+ const nodeData = {provisioned: "n/a"};
+
+ render(
+
+
+
+ );
+
+ expect(screen.queryByTestId('PriorityHighIcon')).not.toBeInTheDocument();
+ });
+
+ test('does NOT show not provisioned icon when provisioned is true', () => {
+ const nodeData = {provisioned: true};
+
+ render(
+
+
+
+ );
+
+ expect(screen.queryByTestId('PriorityHighIcon')).not.toBeInTheDocument();
});
test('displays node state when available', () => {
@@ -243,7 +273,7 @@ describe('NodeCard Component', () => {
render(
-
+
);
@@ -253,7 +283,7 @@ describe('NodeCard Component', () => {
test('handles default functions gracefully', () => {
render(
-
+
);
@@ -271,7 +301,7 @@ describe('NodeCard Component', () => {
test('does not show view instance button when onViewInstance is not provided', () => {
render(
-
+
);
@@ -283,7 +313,7 @@ describe('NodeCard Component', () => {
test('handles null node prop gracefully', () => {
render(
-
+
);
@@ -293,7 +323,7 @@ describe('NodeCard Component', () => {
test('disables actions button when actionInProgress is true', () => {
render(
-
+
);
@@ -307,7 +337,7 @@ describe('NodeCard Component', () => {
render(
- {
render(
-
+
);
diff --git a/src/eventSourceManager.jsx b/src/eventSourceManager.jsx
index 650c73a..9e96942 100644
--- a/src/eventSourceManager.jsx
+++ b/src/eventSourceManager.jsx
@@ -119,7 +119,7 @@ const createQueryString = (filters = DEFAULT_FILTERS, objectName = null) => {
? OBJECT_SPECIFIC_FILTERS.map(filter => `${filter},path=${encodeURIComponent(objectName)}`)
: validFilters;
- return `replay=true&${queryFilters.map(filter => `filter=${encodeURIComponent(filter)}`).join('&')}`;
+ return `cache=true&${queryFilters.map(filter => `filter=${encodeURIComponent(filter)}`).join('&')}`;
};
// Get current token
@@ -632,7 +632,7 @@ const handleAuthError = (token, url) => {
navigationService.redirectToAuth();
};
-const handleReconnection = (url, _token, filters) => {
+const handleReconnection = (url, token, filters) => {
if (reconnectAttempts < MAX_RECONNECT_ATTEMPTS && isPageActive) {
reconnectAttempts++;
diff --git a/src/tests/eventSourceManager.test.jsx b/src/tests/eventSourceManager.test.jsx
index 3b3f6a8..10e9cf5 100644
--- a/src/tests/eventSourceManager.test.jsx
+++ b/src/tests/eventSourceManager.test.jsx
@@ -152,7 +152,7 @@ describe('eventSourceManager', () => {
addEventListener: jest.fn(),
close: jest.fn(),
readyState: 1, // OPEN state
- url: URL_NODE_EVENT + '?replay=true&filter=NodeStatusUpdated',
+ url: URL_NODE_EVENT + '?cache=true&filter=NodeStatusUpdated',
};
mockLoggerEventSource = {
@@ -161,7 +161,7 @@ describe('eventSourceManager', () => {
addEventListener: jest.fn(),
close: jest.fn(),
readyState: 1,
- url: URL_NODE_EVENT + '?replay=true&filter=ObjectStatusUpdated',
+ url: URL_NODE_EVENT + '?cache=true&filter=ObjectStatusUpdated',
};
// Mock EventSourcePolyfill to return our mock
@@ -335,7 +335,7 @@ describe('eventSourceManager', () => {
test('should configure EventSource without objectName', () => {
eventSourceManager.configureEventSource('fake-token');
expect(EventSourcePolyfill).toHaveBeenCalled();
- expect(EventSourcePolyfill.mock.calls[0][0]).toContain('replay=true');
+ expect(EventSourcePolyfill.mock.calls[0][0]).toContain('cache=true');
});
test('should create an EventSource with valid token via startEventReception', () => {
@@ -492,7 +492,7 @@ describe('eventSourceManager', () => {
// Coverage for lines 726-727, 745: updateEventSourceToken with open EventSource
test('should restart EventSource when updateEventSourceToken called with open connection', () => {
// Create initial EventSource — mockEventSource needs a url for updateEventSourceToken to parse
- mockEventSource.url = `${URL_NODE_EVENT}?replay=true&filter=NodeStatusUpdated`;
+ mockEventSource.url = `${URL_NODE_EVENT}?cache=true&filter=NodeStatusUpdated`;
eventSourceManager.createEventSource(URL_NODE_EVENT, 'fake-token');
// Prepare mock for the recreated EventSource
@@ -502,7 +502,7 @@ describe('eventSourceManager', () => {
addEventListener: jest.fn(),
close: jest.fn(),
readyState: 1,
- url: `${URL_NODE_EVENT}?replay=true&filter=NodeStatusUpdated`,
+ url: `${URL_NODE_EVENT}?cache=true&filter=NodeStatusUpdated`,
};
EventSourcePolyfill.mockImplementation(() => recreatedMock);
@@ -1526,7 +1526,7 @@ describe('eventSourceManager', () => {
describe('Utility functions and helpers', () => {
test('should create query string with default filters', () => {
eventSourceManager.configureEventSource('fake-token');
- expect(EventSourcePolyfill).toHaveBeenCalledWith(expect.stringContaining('replay=true'), expect.any(Object));
+ expect(EventSourcePolyfill).toHaveBeenCalledWith(expect.stringContaining('cache=true'), expect.any(Object));
});
test('should handle invalid filters in createQueryString', () => {
@@ -1552,7 +1552,7 @@ describe('eventSourceManager', () => {
test('should create query string without objectName', () => {
eventSourceManager.configureEventSource('fake-token');
const url = EventSourcePolyfill.mock.calls[0][0];
- expect(url).toContain('replay=true');
+ expect(url).toContain('cache=true');
expect(url).not.toContain('path=');
});
@@ -1739,7 +1739,7 @@ describe('eventSourceManager', () => {
addEventListener: jest.fn(),
close: jest.fn(),
readyState: 1,
- url: `${URL_NODE_EVENT}?replay=true&filter=ObjectStatusUpdated`,
+ url: `${URL_NODE_EVENT}?cache=true&filter=ObjectStatusUpdated`,
};
EventSourcePolyfill.mockImplementation(() => nextLoggerMock);