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 = () => { setConsoleUrlDialogOpen(false)} maxWidth="sm" - fullWidth sx={{ - '& .MuiDialog-paper': { - minWidth: {xs: '90vw', sm: '500px'}, - maxWidth: '90vw', - mx: {xs: 2, sm: 0}, - my: {xs: 2, sm: 0} - } - }}> - Console URL + fullWidth> + Console URL { }}> - + + + {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}) => ( + handleBatchNodeActionClick(name)}> + {icon} + {name.charAt(0).toUpperCase() + name.slice(1)} + + ))} + + + {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}) => ( - handleBatchNodeActionClick(name)} - role="menuitem" - aria-label={name.charAt(0).toUpperCase() + name.slice(1)}> - {icon} - {name.charAt(0).toUpperCase() + name.slice(1)} - - ))} - - - - - setIndividualNodeMenuAnchor(null)}> - - {INSTANCE_ACTIONS.map(({name, icon}) => ( - handleIndividualNodeActionClick(name)} - role="menuitem" - aria-label={name.charAt(0).toUpperCase() + name.slice(1)}> - {icon} - {name.charAt(0).toUpperCase() + name.slice(1)} - - ))} - - - + + setIndividualNodeMenuAnchor(null)} + anchorOrigin={{vertical: 'bottom', horizontal: 'right'}} + transformOrigin={{vertical: 'top', horizontal: 'right'}} + > + {INSTANCE_ACTIONS.map(({name, icon}) => ( + handleIndividualNodeActionClick(name)}> + {icon} + {name.charAt(0).toUpperCase() + name.slice(1)} + + ))} + )} @@ -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);