From 2c2388a14f73717e8c93085139c85384148850ee Mon Sep 17 00:00:00 2001 From: Jeffrey Phillips Date: Wed, 1 Jul 2026 11:30:56 -0400 Subject: [PATCH] CONSOLE-5398: Remove events card from node overview dashboard --- .../src/components/nodes/NodeWorkload.tsx | 2 +- .../src/components/nodes/NodesPage.tsx | 2 +- .../nodes/node-dashboard/InventoryCard.tsx | 8 +- .../nodes/node-dashboard/NodeDashboard.tsx | 40 +++-- .../__tests__/InventoryCard.spec.tsx | 38 ++++- .../__tests__/NodeDashboard.spec.tsx | 113 ++++++++++++++ .../components/dashboard/DashboardGrid.tsx | 21 ++- .../__tests__/DashboardGrid.spec.tsx | 146 ++++++++++++++++++ 8 files changed, 348 insertions(+), 22 deletions(-) create mode 100644 frontend/packages/console-app/src/components/nodes/node-dashboard/__tests__/NodeDashboard.spec.tsx create mode 100644 frontend/packages/console-shared/src/components/dashboard/__tests__/DashboardGrid.spec.tsx diff --git a/frontend/packages/console-app/src/components/nodes/NodeWorkload.tsx b/frontend/packages/console-app/src/components/nodes/NodeWorkload.tsx index ccf72a51a74..d57f35550f0 100644 --- a/frontend/packages/console-app/src/components/nodes/NodeWorkload.tsx +++ b/frontend/packages/console-app/src/components/nodes/NodeWorkload.tsx @@ -4,7 +4,7 @@ import { PodsPage } from '@console/internal/components/pod-list'; import type { PageComponentProps } from '@console/internal/components/utils'; import { NodeSubNavPage } from './NodeSubNavPage'; -const WORKLOAD_PAGE_ID = 'workload'; +export const WORKLOAD_PAGE_ID = 'workload'; const NodePodsPage: FC> = ({ obj }) => ( = ({ obj }) => { const nodeName = getName(obj); + const nodeMgmtV1Enabled = useFlag(FLAG_NODE_MGMT_V1); const podResource = useMemo( () => @@ -38,7 +42,9 @@ const NodePodInventoryItem: ComponentType<{ obj: NodeKind }> = ({ obj }) => { return ; } - const basePath = `${resourcePathFromModel(NodeModel, nodeName)}/pods`; + const basePath = nodeMgmtV1Enabled + ? `${resourcePathFromModel(NodeModel, nodeName)}/${WORKLOAD_PAGE_ID}/pods` + : `${resourcePathFromModel(NodeModel, nodeName)}/pods`; return ( diff --git a/frontend/packages/console-app/src/components/nodes/node-dashboard/NodeDashboard.tsx b/frontend/packages/console-app/src/components/nodes/node-dashboard/NodeDashboard.tsx index e3ca7d7f10a..309a396ef91 100644 --- a/frontend/packages/console-app/src/components/nodes/node-dashboard/NodeDashboard.tsx +++ b/frontend/packages/console-app/src/components/nodes/node-dashboard/NodeDashboard.tsx @@ -1,6 +1,8 @@ import type { FC } from 'react'; -import { useReducer, useCallback, useEffect } from 'react'; +import { useMemo, useReducer, useCallback, useEffect } from 'react'; import * as _ from 'lodash'; +import { FLAG_NODE_MGMT_V1 } from '@console/app/src/consts'; +import { useFlag } from '@console/dynamic-plugin-sdk/src/utils/flags'; import type { NodeKind } from '@console/internal/module/k8s'; import Dashboard from '@console/shared/src/components/dashboard/Dashboard'; import DashboardGrid from '@console/shared/src/components/dashboard/DashboardGrid'; @@ -75,6 +77,7 @@ const reducer = (state: NodeDashboardState, action: NodeDashboardAction) => { }; const NodeDashboard: FC = ({ obj }) => { + const nodeMgmtV1Enabled = useFlag(FLAG_NODE_MGMT_V1); const [state, dispatch] = useReducer(reducer, initialState(obj)); useEffect(() => { @@ -94,20 +97,35 @@ const NodeDashboard: FC = ({ obj }) => { [], ); - const context = { - obj, - cpuLimit: state.cpuLimit, - memoryLimit: state.memoryLimit, - healthCheck: state.healthCheck, - setCPULimit, - setMemoryLimit, - setHealthCheck, - }; + const context = useMemo( + () => ({ + obj, + cpuLimit: state.cpuLimit, + memoryLimit: state.memoryLimit, + healthCheck: state.healthCheck, + setCPULimit, + setMemoryLimit, + setHealthCheck, + }), + [ + obj, + setCPULimit, + setHealthCheck, + setMemoryLimit, + state.cpuLimit, + state.healthCheck, + state.memoryLimit, + ], + ); return ( - + ); diff --git a/frontend/packages/console-app/src/components/nodes/node-dashboard/__tests__/InventoryCard.spec.tsx b/frontend/packages/console-app/src/components/nodes/node-dashboard/__tests__/InventoryCard.spec.tsx index 286a9eaa5c2..fe378d0a7fc 100644 --- a/frontend/packages/console-app/src/components/nodes/node-dashboard/__tests__/InventoryCard.spec.tsx +++ b/frontend/packages/console-app/src/components/nodes/node-dashboard/__tests__/InventoryCard.spec.tsx @@ -1,5 +1,6 @@ import { useResolvedExtensions } from '@openshift/dynamic-plugin-sdk'; import { render, screen } from '@testing-library/react'; +import { useFlag } from '@console/dynamic-plugin-sdk/src/utils/flags'; import { useK8sWatchResource } from '@console/internal/components/utils/k8s-watch-hook'; import type { NodeKind } from '@console/internal/module/k8s'; import InventoryCard from '../InventoryCard'; @@ -10,15 +11,24 @@ jest.mock('@openshift/dynamic-plugin-sdk', () => ({ useResolvedExtensions: jest.fn(), })); +jest.mock('@console/dynamic-plugin-sdk/src/utils/flags', () => ({ + useFlag: jest.fn(() => false), +})); + jest.mock('@console/internal/components/utils/k8s-watch-hook', () => ({ useK8sWatchResource: jest.fn(), })); +const mockResourceInventoryItem = jest.fn(); + jest.mock('@console/shared/src/components/dashboard/inventory-card/InventoryItem', () => ({ InventoryItem: ({ title, count }: { title: string; count?: number }) => (
{`${title}: ${count ?? 0}`}
), - ResourceInventoryItem: () =>
Pod Inventory
, + ResourceInventoryItem: (props: { basePath: string }) => { + mockResourceInventoryItem(props); + return
Pod Inventory
; + }, })); const MockExtensionInventoryItem = jest.fn(() => ( @@ -27,6 +37,7 @@ const MockExtensionInventoryItem = jest.fn(() => ( const useResolvedExtensionsMock = useResolvedExtensions as jest.Mock; const useK8sWatchResourceMock = useK8sWatchResource as jest.Mock; +const useFlagMock = useFlag as jest.Mock; describe('InventoryCard', () => { const mockNode: NodeKind = { @@ -59,6 +70,7 @@ describe('InventoryCard', () => { beforeEach(() => { jest.clearAllMocks(); + useFlagMock.mockReturnValue(false); useResolvedExtensionsMock.mockReturnValue([[], true]); useK8sWatchResourceMock.mockReturnValue([[], true, undefined]); }); @@ -126,6 +138,30 @@ describe('InventoryCard', () => { expect(screen.getByText('Extension Inventory Item')).toBeVisible(); }); + it('should use legacy pods path when FLAG_NODE_MGMT_V1 is disabled', () => { + useFlagMock.mockReturnValue(false); + + renderWithContext(); + + expect(mockResourceInventoryItem).toHaveBeenCalledWith( + expect.objectContaining({ + basePath: '/k8s/cluster/nodes/test-node/pods', + }), + ); + }); + + it('should use workload pods path when FLAG_NODE_MGMT_V1 is enabled', () => { + useFlagMock.mockReturnValue(true); + + renderWithContext(); + + expect(mockResourceInventoryItem).toHaveBeenCalledWith( + expect.objectContaining({ + basePath: '/k8s/cluster/nodes/test-node/workload/pods', + }), + ); + }); + it('should sort inventory items by priority from highest to lowest', () => { useResolvedExtensionsMock.mockReturnValue([ [ diff --git a/frontend/packages/console-app/src/components/nodes/node-dashboard/__tests__/NodeDashboard.spec.tsx b/frontend/packages/console-app/src/components/nodes/node-dashboard/__tests__/NodeDashboard.spec.tsx new file mode 100644 index 00000000000..1f8445cd716 --- /dev/null +++ b/frontend/packages/console-app/src/components/nodes/node-dashboard/__tests__/NodeDashboard.spec.tsx @@ -0,0 +1,113 @@ +import { render } from '@testing-library/react'; +import * as flagsModule from '@console/dynamic-plugin-sdk/src/utils/flags'; +import type { NodeKind } from '@console/internal/module/k8s'; +import * as DashboardGridModule from '@console/shared/src/components/dashboard/DashboardGrid'; +import ActivityCard from '../ActivityCard'; +import NodeDashboard from '../NodeDashboard'; + +jest.mock('@console/dynamic-plugin-sdk/src/utils/flags', () => ({ + useFlag: jest.fn(), +})); + +jest.mock('@console/shared/src/components/dashboard/Dashboard', () => ({ + __esModule: true, + default: ({ children }) =>
{children}
, +})); + +jest.mock('@console/shared/src/components/dashboard/DashboardGrid', () => ({ + __esModule: true, + default: jest.fn(() => null), +})); + +const mockNode: NodeKind = { + apiVersion: 'v1', + kind: 'Node', + metadata: { + name: 'test-node', + uid: 'test-node-uid', + resourceVersion: '12345', + creationTimestamp: '2024-01-01T00:00:00Z', + }, + spec: {}, + status: { + conditions: [], + addresses: [], + }, +}; + +describe('NodeDashboard', () => { + beforeEach(() => { + jest.spyOn(flagsModule, 'useFlag').mockReturnValue(false); + jest.spyOn(DashboardGridModule, 'default').mockReturnValue(null); + }); + + afterEach(() => { + jest.restoreAllMocks(); + }); + + describe('when rendering with node object', () => { + it('should render Dashboard with DashboardGrid', () => { + render(); + + expect(DashboardGridModule.default).toHaveBeenCalled(); + }); + + it('should pass mainCards and leftCards to DashboardGrid', () => { + render(); + + expect(DashboardGridModule.default).toHaveBeenCalledWith( + expect.objectContaining({ + mainCards: expect.arrayContaining([ + expect.objectContaining({ Card: expect.any(Function) }), + ]), + leftCards: expect.arrayContaining([ + expect.objectContaining({ Card: expect.any(Function) }), + ]), + }), + expect.any(Object), + ); + }); + }); + + describe('when NODE_MGMT_V1 flag is disabled', () => { + it('should pass rightCards, mainCards, and leftCards to DashboardGrid', () => { + jest.spyOn(flagsModule, 'useFlag').mockReturnValue(false); + + render(); + + expect(DashboardGridModule.default).toHaveBeenCalledWith( + expect.objectContaining({ + rightCards: [{ Card: ActivityCard }], + mainCards: expect.arrayContaining([ + expect.objectContaining({ Card: expect.any(Function) }), + ]), + leftCards: expect.arrayContaining([ + expect.objectContaining({ Card: expect.any(Function) }), + ]), + }), + expect.any(Object), + ); + }); + }); + + describe('when NODE_MGMT_V1 flag is enabled', () => { + it('should not pass rightCards to DashboardGrid', () => { + jest.spyOn(flagsModule, 'useFlag').mockReturnValue(true); + + render(); + + expect(DashboardGridModule.default).toHaveBeenCalledWith( + expect.objectContaining({ + rightCards: undefined, + mainCards: expect.arrayContaining([ + expect.objectContaining({ Card: expect.any(Function) }), + ]), + leftCards: expect.arrayContaining([ + expect.objectContaining({ Card: expect.any(Function) }), + ]), + }), + expect.any(Object), + ); + }); + }); +}); diff --git a/frontend/packages/console-shared/src/components/dashboard/DashboardGrid.tsx b/frontend/packages/console-shared/src/components/dashboard/DashboardGrid.tsx index e098953db56..163163e11f3 100644 --- a/frontend/packages/console-shared/src/components/dashboard/DashboardGrid.tsx +++ b/frontend/packages/console-shared/src/components/dashboard/DashboardGrid.tsx @@ -35,6 +35,9 @@ const DashboardGrid: FC = ({ mainCards, leftCards, rightCards smallGrid, ]); + const mainGridSpan = + leftCards?.length && rightCards?.length ? 6 : leftCards?.length || rightCards?.length ? 9 : 12; + return (
{smallGrid ? ( @@ -51,15 +54,19 @@ const DashboardGrid: FC = ({ mainCards, leftCards, rightCards ) : ( - - {leftGridCards} - - + {leftCards?.length ? ( + + {leftGridCards} + + ) : null} + {mainGridCards} - - {rightGridCards} - + {rightCards?.length ? ( + + {rightGridCards} + + ) : null} )}
diff --git a/frontend/packages/console-shared/src/components/dashboard/__tests__/DashboardGrid.spec.tsx b/frontend/packages/console-shared/src/components/dashboard/__tests__/DashboardGrid.spec.tsx new file mode 100644 index 00000000000..a13012ec79a --- /dev/null +++ b/frontend/packages/console-shared/src/components/dashboard/__tests__/DashboardGrid.spec.tsx @@ -0,0 +1,146 @@ +import { GridItem } from '@patternfly/react-core'; +import { render, screen } from '@testing-library/react'; +import type { OverviewGridCard } from '@console/dynamic-plugin-sdk'; +import * as refWidthHook from '@console/internal/components/utils/ref-width-hook'; +import DashboardGrid from '../DashboardGrid'; + +jest.mock('@patternfly/react-core', () => ({ + ...jest.requireActual('@patternfly/react-core'), + GridItem: jest.fn(jest.requireActual('@patternfly/react-core').GridItem), +})); + +jest.mock('@console/internal/components/utils/ref-width-hook', () => ({ + useRefWidth: jest.fn(), +})); + +const createMockCards = (count: number, label: string): OverviewGridCard[] => + Array.from({ length: count }, (_, index) => ({ + Card: () =>
{`${label} Card ${index + 1}`}
, + span: 12, + })); + +const getLayoutGridItemSizes = () => + jest + .mocked(GridItem) + .mock.calls.filter(([{ lg }]) => lg !== undefined) + .map(([{ lg, md, sm }]) => ({ lg, md, sm })); + +const expectLayoutGridItems = (...sizes: number[]) => { + expect(getLayoutGridItemSizes()).toEqual(sizes.map((size) => ({ lg: size, md: size, sm: size }))); +}; + +describe('DashboardGrid', () => { + beforeEach(() => { + (refWidthHook.useRefWidth as jest.Mock).mockReturnValue([jest.fn(), 1200]); + jest.mocked(GridItem).mockClear(); + }); + + afterEach(() => { + jest.restoreAllMocks(); + }); + + describe('when rendering with main cards only', () => { + it('should render main cards in a 12-column grid', () => { + const mainCards = createMockCards(2, 'Main'); + + render(); + + expect(screen.getByText('Main Card 1')).toBeVisible(); + expect(screen.getByText('Main Card 2')).toBeVisible(); + + expectLayoutGridItems(12); + }); + + it('should not render left or right columns', () => { + const mainCards = createMockCards(1, 'Main'); + + render(); + + expect(screen.queryByText('Left Card 1')).not.toBeInTheDocument(); + expect(screen.queryByText('Right Card 1')).not.toBeInTheDocument(); + + expectLayoutGridItems(12); + }); + }); + + describe('when rendering with left cards', () => { + it('should render both main and left cards', () => { + const mainCards = createMockCards(1, 'Main'); + const leftCards = createMockCards(1, 'Left'); + + render(); + + expect(screen.getByText('Main Card 1')).toBeVisible(); + expect(screen.getByText('Left Card 1')).toBeVisible(); + + expectLayoutGridItems(3, 9); + }); + }); + + describe('when rendering with right cards', () => { + it('should render both main and right cards', () => { + const mainCards = createMockCards(1, 'Main'); + const rightCards = createMockCards(1, 'Right'); + + render(); + + expect(screen.getByText('Main Card 1')).toBeVisible(); + expect(screen.getByText('Right Card 1')).toBeVisible(); + + expectLayoutGridItems(9, 3); + }); + }); + + describe('when rendering with left and right cards', () => { + it('should render all three card sections', () => { + const mainCards = createMockCards(1, 'Main'); + const leftCards = createMockCards(1, 'Left'); + const rightCards = createMockCards(1, 'Right'); + + render(); + + expect(screen.getByText('Main Card 1')).toBeVisible(); + expect(screen.getByText('Left Card 1')).toBeVisible(); + expect(screen.getByText('Right Card 1')).toBeVisible(); + + expectLayoutGridItems(3, 6, 3); + }); + }); + + describe('when viewport is small', () => { + beforeEach(() => { + (refWidthHook.useRefWidth as jest.Mock).mockReturnValue([jest.fn(), 800]); + }); + + it('should render all cards stacked vertically', () => { + const mainCards = createMockCards(1, 'Main'); + const leftCards = createMockCards(1, 'Left'); + const rightCards = createMockCards(1, 'Right'); + + render(); + + expect(screen.getByText('Main Card 1')).toBeVisible(); + expect(screen.getByText('Left Card 1')).toBeVisible(); + expect(screen.getByText('Right Card 1')).toBeVisible(); + + expectLayoutGridItems(12, 12, 12); + }); + }); + + describe('when rendering multiple cards', () => { + it('should render all cards in their respective sections', () => { + const mainCards = createMockCards(3, 'Main'); + const leftCards = createMockCards(2, 'Left'); + + render(); + + expect(screen.getByText('Main Card 1')).toBeVisible(); + expect(screen.getByText('Main Card 2')).toBeVisible(); + expect(screen.getByText('Main Card 3')).toBeVisible(); + expect(screen.getByText('Left Card 1')).toBeVisible(); + expect(screen.getByText('Left Card 2')).toBeVisible(); + + expectLayoutGridItems(3, 9); + }); + }); +});