Skip to content

Commit

Permalink
fix(RHINENG-12778): Add tooltip for locked OS versions
Browse files Browse the repository at this point in the history
This PR notifies a user if a system's OS version is locked. When version
locking is enabled for a system i.e. a system's OS is tied to a release
version, that can affect the count of applicable and installable
errata\advisories.

The backend now passes a key from the `/systems` api endpoint
called `rhsm_lock` and the value is the locked version number. With this
PR, we check on the Systems table as well as the CVEs -> CVEDetails
tables. When the `rhsm_lock` parameter has a value, we include an icon
with a tooltip that tells the user "Your RHEL version is locked..." and
at which version it is locked at.
  • Loading branch information
Michael Johnson authored and johnsonm325 committed Feb 4, 2025
1 parent 572579b commit b1fa4a1
Show file tree
Hide file tree
Showing 7 changed files with 202 additions and 91 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import React from 'react';
import propTypes from 'prop-types';
import { Flex, FlexItem, Tooltip } from '@patternfly/react-core';
import { InfoCircleIcon } from '@patternfly/react-icons';

const VersionLockIconTooltip = ({ rhsmLock, osName }) => {
return (
<Tooltip
content={`Your RHEL version is locked at version ${rhsmLock}`}
>
<Flex flex={{ default: 'inlineFlex' }}>
<FlexItem spacer={{ default: 'spacerSm' }}>{osName}</FlexItem>
<FlexItem spacer={{ default: 'spacerSm' }}>
<InfoCircleIcon size="sm" color="var(--pf-v5-global--info-color--100)" data-testid="version-lock-icon" />
</FlexItem>
</Flex>
</Tooltip>
);
};

VersionLockIconTooltip.propTypes = {
rhsmLock: propTypes.string,
osName: propTypes.string
};

export default VersionLockIconTooltip;
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import VersionLockIconTooltip from './VersionLockIconTooltip';
import { render, screen, waitFor } from '@testing-library/react';
import '@testing-library/jest-dom';
import userEvent from '@testing-library/user-event';

describe('VersionLockIconTooltip', () => {
it('display tooltip with data', async () => {
render(
<VersionLockIconTooltip rhsmLock='8.1' osName='RHEL 8.1' />
);

const hoverBox = screen.getByTestId('version-lock-icon');

userEvent.hover(hoverBox);

await waitFor(() => {
expect (screen.queryByRole('tooltip')).toBeVisible();
expect(screen.queryByRole('tooltip')).toHaveTextContent(
'Your RHEL version is locked at version 8.1'
);
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ jest.mock('@unleash/proxy-client-react', () => ( {
...jest.requireActual('@unleash/proxy-client-react'),
useFlag: () => true,
useFlagsStatus: () => ({ flagsReady: true })
}))
}));

const state = {
...initialState
Expand Down
42 changes: 42 additions & 0 deletions src/Components/SmartComponents/SystemsPage/SystemsPage.fixtures.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
export const systemRows = [
{
culled_timestamp: '2025-02-07T20:02:37.035592+00:00',
cve_count: 2032,
display_name: 'system1',
id: '1234',
insights_id: null,
inventory_group: [],
inventory_id: '4321',
last_evaluation: '2025-01-24T20:02:41.474380+00:00',
last_upload: '2025-01-24T20:02:38.366587+00:00',
opt_out: false,
os: 'RHEL 8.1',
rhsm_lock: '8.1',
rules_evaluation: '2025-01-24T20:02:41.474380+00:00',
selected: false,
stale_timestamp: '2025-01-26T01:02:37.035592+00:00',
stale_warning_timestamp: '2025-01-31T20:02:37.035592+00:00',
tags: [],
updated: '2025-01-24T20:02:37.035592+00:00'
},
{
culled_timestamp: '2025-02-07T20:02:37.035592+00:00',
cve_count: 4,
display_name: 'system1',
id: '5678',
insights_id: null,
inventory_group: [],
inventory_id: '8765',
last_evaluation: '2025-01-24T20:02:41.474380+00:00',
last_upload: '2025-01-24T20:02:38.366587+00:00',
opt_out: false,
os: 'RHEL 8.0',
rhsm_lock: '',
rules_evaluation: '2025-01-24T20:02:41.474380+00:00',
selected: false,
stale_timestamp: '2025-01-26T01:02:37.035592+00:00',
stale_warning_timestamp: '2025-01-31T20:02:37.035592+00:00',
tags: [],
updated: '2025-01-24T20:02:37.035592+00:00'
}
];
46 changes: 37 additions & 9 deletions src/Components/SmartComponents/SystemsPage/SystemsPage.test.js
Original file line number Diff line number Diff line change
@@ -1,36 +1,64 @@
import SystemsPage from './SystemsPage';
import { render } from '@testing-library/react';
import { render, screen, waitFor } from '@testing-library/react';
import '@testing-library/jest-dom';
import { useDispatch, useSelector } from 'react-redux';
import TestWrapper from '../../../Utilities/TestWrapper';
import { initialState } from '../../../Store/Reducers/SystemsPageStore';
import { entitiesInitialState, initialState } from '../../../Store/Reducers/SystemsPageStore';
import configureStore from 'redux-mock-store';

jest.mock('../../../Helpers/Hooks', () => ({
...jest.requireActual('../../../Helpers/Hooks'),
useRbac: () => [[true, true, true, true], false]
}));

jest.mock("react-redux", () => ({
...jest.requireActual("react-redux"),
useDispatch: jest.fn(),
useSelector: jest.fn()
}));

jest.mock('@unleash/proxy-client-react', () => ( {
...jest.requireActual('@unleash/proxy-client-react'),
useFlag: () => true,
useFlagsStatus: () => ({ flagsReady: true })
}));

const customMiddleWare = store => next => action => {
useSelector.mockImplementation(callback => {
return callback({ SystemsPageStore: initialState });
return callback({ SystemsPageStore: initialState, entities: entitiesInitialState });
});
next(action);
};

const mockStore = configureStore([customMiddleWare]);
const store = mockStore(initialState);
const store = mockStore({ SystemsPageStore: initialState, entities: entitiesInitialState });

describe('SystemsPage', () => {
let fetchData = jest.fn();
let systems = { data: [], meta: {} };
beforeEach(() => {
useSelector.mockImplementation(callback => {
return callback({ SystemsPageStore: initialState, entities: entitiesInitialState });
});
useDispatch.mockReturnValue(jest.fn());
});

afterEach(() => {
useSelector.mockClear();
store.clearActions();
});

it.skip('should call fetchData function', () => {
it('should call fetchData function', async () => {
render(
<TestWrapper store={ store }>
<SystemsPage systems={systems} fetchData={fetchData} />
<SystemsPage />
</TestWrapper>
);

expect(fetchData).toBeCalled;
await waitFor(() => {
expect(
screen.getByRole('heading', {
name: /vulnerability systems/i
})
).toBeVisible()
});
});
});
144 changes: 63 additions & 81 deletions src/Helpers/constants.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import {
} from '@patternfly/react-icons';
import { InsightsLink } from '@redhat-cloud-services/frontend-components/InsightsLink';
import { groupOrWorkspace } from './MiscHelper';
import VersionLockIconTooltip from '../Components/PresentationalComponents/VersionLockIconTooltip/VersionLockIconTooltip';

export const SERVICE_NAME = 'Vulnerability';
export const DEFAULT_PAGE_SIZE = 20;
Expand Down Expand Up @@ -667,6 +668,60 @@ export const VULNERABILITIES_HEADER = [
}
];

const groupsColumn = (isWorkspaceEnabled) => (
{
key: 'groups',
title: groupOrWorkspace(isWorkspaceEnabled),
inventoryGroupsFeatureFlag: true,
isShownByDefault: true,
renderFunc: (data, value, { inventory_group: group }) =>
<span>
{isEmpty(group) ?
<span className="pf-v5-u-disabled-color-200">No {groupOrWorkspace(isWorkspaceEnabled).toLowerCase()}</span>
: group[0].name} {/* currently, one group at maximum is supported */}
</span>
}
);

const tagsColumn = {
key: 'tags',
title: intl.formatMessage(messages.systemsColumnHeaderTags),
props: { isStatic: true },
isShownByDefault: true
};

const osColumn = {
key: 'os',
sortKey: 'os',
dataLabel: intl.formatMessage(messages.systemsColumnHeaderOS),
title: (
<Tooltip content={intl.formatMessage(messages.systemsColumnHeaderOSFull)}>
<span>
{intl.formatMessage(messages.systemsColumnHeaderOS)}
</span>
</Tooltip>
),
/*eslint-disable camelcase*/
renderFunc: (osName, _, { rhsm_lock }) =>
rhsm_lock?.length
? <VersionLockIconTooltip rhsmLock={rhsm_lock} osName={osName} />
: <span>{osName}</span>,
/*eslint-enable camelcase*/
cellTransforms: [nowrap],
isShownByDefault: true
};

const updatedColumn = (props) => (
{
key: 'updated',
title: intl.formatMessage(messages.systemsColumnHeaderUpdated),
transforms: [nowrap],
cellTransforms: [nowrap],
props,
isShownByDefault: true
}
);

export const getSystemsExposedHeader = (isWorkspaceEnabled) => [
{
key: 'display_name',
Expand All @@ -692,38 +747,9 @@ export const getSystemsExposedHeader = (isWorkspaceEnabled) => [
}

},
{
key: 'groups',
title: groupOrWorkspace(isWorkspaceEnabled),
isShownByDefault: true,
inventoryGroupsFeatureFlag: true,
renderFunc: (data, value, { inventory_group: group }) =>
<span>
{isEmpty(group) ?
<span className="pf-v5-u-disabled-color-200">No {groupOrWorkspace(groupOrWorkspace).toLowerCase()}</span>
: group[0].name} {/* currently, one group at maximum is supported */}
</span>
},
{
key: 'tags',
title: intl.formatMessage(messages.systemsColumnHeaderTags),
props: { isStatic: true },
isShownByDefault: true
},
{
key: 'os',
sortKey: 'os',
dataLabel: intl.formatMessage(messages.systemsColumnHeaderOS),
title: (
<Tooltip content={intl.formatMessage(messages.systemsColumnHeaderOSFull)}>
<span>
{intl.formatMessage(messages.systemsColumnHeaderOS)}
</span>
</Tooltip>
),
cellTransforms: [nowrap],
isShownByDefault: true
},
groupsColumn(isWorkspaceEnabled),
tagsColumn,
osColumn,
{
key: 'advisories_list',
sortKey: 'advisories_list',
Expand Down Expand Up @@ -755,13 +781,7 @@ export const getSystemsExposedHeader = (isWorkspaceEnabled) => [
/>,
isShownByDefault: true
},
{
key: 'updated',
title: intl.formatMessage(messages.systemsColumnHeaderUpdated),
transforms: [nowrap],
cellTransforms: [nowrap],
isShownByDefault: true
},
updatedColumn(),
{
key: 'remediation',
sortKey: 'remediation',
Expand Down Expand Up @@ -810,55 +830,17 @@ export const getSystemsHeader = (isWorkspaceEnabled) => [
isShownByDefault: true,
isUnhidable: true
},
{
key: 'groups',
title: groupOrWorkspace(isWorkspaceEnabled),
inventoryGroupsFeatureFlag: true,
isShownByDefault: true,
renderFunc: (data, value, { inventory_group: group }) =>
<span>
{isEmpty(group) ?
<span className="pf-v5-u-disabled-color-200">No {groupOrWorkspace(isWorkspaceEnabled).toLowerCase()}</span>
: group[0].name} {/* currently, one group at maximum is supported */}
</span>
},
{
key: 'tags',
title: intl.formatMessage(messages.systemsColumnHeaderTags),
props: { isStatic: true },
isShownByDefault: true
},
{
key: 'os',
sortKey: 'os',
dataLabel: intl.formatMessage(messages.systemsColumnHeaderOS),
title: (
<Tooltip content={intl.formatMessage(messages.systemsColumnHeaderOSFull)}>
<span>
{intl.formatMessage(messages.systemsColumnHeaderOS)}
</span>
</Tooltip>
),
cellTransforms: [nowrap],
isShownByDefault: true
},
groupsColumn(isWorkspaceEnabled),
tagsColumn,
osColumn,
{
key: 'cve_count',
sortKey: 'cve_count',
title: intl.formatMessage(messages.systemsColumnHeaderCveCount),
renderFunc: value => (value !== null ? String(value) : intl.formatMessage(messages.systemsTableDisabled)),
isShownByDefault: true
},
{
key: 'updated',
title: intl.formatMessage(messages.systemsColumnHeaderUpdated),
transforms: [nowrap],
cellTransforms: [nowrap],
props: {
width: 20
},
isShownByDefault: true
}
updatedColumn({ width: 20 })
];

export const SYSTEM_DETAILS_HEADER = [
Expand Down
10 changes: 10 additions & 0 deletions src/Store/Reducers/SystemsPageStore.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import * as ActionTypes from '../ActionTypes';
import { error, applyGlobalFilter } from './reducersHelper';
import { DEFAULT_PAGE_SIZE, SYSTEMS_DEFAULT_FILTERS } from './../../Helpers/constants';
import { systemRows } from '../../Components/SmartComponents/SystemsPage/SystemsPage.fixtures';

export const initialState = {
isLoading: true,
Expand All @@ -21,6 +22,15 @@ export const initialState = {
error
};

export const entitiesInitialState = {
rows: systemRows,
total: 2,
meta: {},
selectedRows: [],
selectedRowsCount: 0,
loaded: true
};

export const SystemsPageStore = (state = initialState, action) => {
let newState = { ...state };
switch (action.type) {
Expand Down

0 comments on commit b1fa4a1

Please sign in to comment.