+
diff --git a/ui/apps/platform/src/Containers/Vulnerabilities/VirtualMachineCves/Overview/VirtualMachinesCvesTable.tsx b/ui/apps/platform/src/Containers/Vulnerabilities/VirtualMachineCves/Overview/VirtualMachinesCvesTable.tsx
index c32f922982bc9..12f1ea0bc504d 100644
--- a/ui/apps/platform/src/Containers/Vulnerabilities/VirtualMachineCves/Overview/VirtualMachinesCvesTable.tsx
+++ b/ui/apps/platform/src/Containers/Vulnerabilities/VirtualMachineCves/Overview/VirtualMachinesCvesTable.tsx
@@ -26,20 +26,24 @@ import { listVirtualMachines } from 'services/VirtualMachineService';
import { getTableUIState } from 'utils/getTableUIState';
import { getHasSearchApplied } from 'utils/searchUtils';
-import { getVirtualMachineSeveritiesCount } from '../aggregateUtils';
+import {
+ getVirtualMachineScannedPackagesCount,
+ getVirtualMachineSeveritiesCount,
+} from '../aggregateUtils';
import AdvancedFiltersToolbar from '../../components/AdvancedFiltersToolbar';
import SeverityCountLabels from '../../components/SeverityCountLabels';
import { DEFAULT_VM_PAGE_SIZE } from '../../constants';
import { getVirtualMachineEntityPagePath } from '../../utils/searchUtils';
+import { VIRTUAL_MACHINE_SORT_FIELD } from '../../utils/sortFields';
const searchFilterConfig = [
virtualMachinesSearchFilterConfig,
virtualMachinesClusterSearchFilterConfig,
];
-export const sortFields = ['Virtual Machine Name'];
+export const sortFields = [VIRTUAL_MACHINE_SORT_FIELD];
-export const defaultSortOption = { field: 'Virtual Machine Name', direction: 'asc' } as const;
+export const defaultSortOption = { field: VIRTUAL_MACHINE_SORT_FIELD, direction: 'asc' } as const;
function VirtualMachinesCvesTable() {
const { page, perPage, setPage, setPerPage } = useURLPagination(DEFAULT_VM_PAGE_SIZE);
@@ -169,7 +173,11 @@ function VirtualMachinesCvesTable() {
|
{virtualMachine.namespace}
|
- ? |
+
+ {getVirtualMachineScannedPackagesCount(
+ virtualMachine
+ )}
+ |
|
diff --git a/ui/apps/platform/src/Containers/Vulnerabilities/VirtualMachineCves/VirtualMachine/VirtualMachinePackagesTable.tsx b/ui/apps/platform/src/Containers/Vulnerabilities/VirtualMachineCves/VirtualMachine/VirtualMachinePackagesTable.tsx
new file mode 100644
index 0000000000000..642704dd427d2
--- /dev/null
+++ b/ui/apps/platform/src/Containers/Vulnerabilities/VirtualMachineCves/VirtualMachine/VirtualMachinePackagesTable.tsx
@@ -0,0 +1,68 @@
+import React from 'react';
+import { Table, Tbody, Td, Th, Thead, Tr } from '@patternfly/react-table';
+
+import TbodyUnified from 'Components/TableStateTemplates/TbodyUnified';
+import type { UseURLSortResult } from 'hooks/useURLSort';
+import type { TableUIState } from 'utils/getTableUIState';
+
+import type { PackageTableRow } from '../aggregateUtils';
+import { COMPONENT_SORT_FIELD } from '../../utils/sortFields';
+
+export type VirtualMachinePackagesTableProps = {
+ tableState: TableUIState;
+ getSortParams: UseURLSortResult['getSortParams'];
+ onClearFilters: () => void;
+};
+
+function VirtualMachinePackagesTable({
+ tableState,
+ getSortParams,
+ onClearFilters,
+}: VirtualMachinePackagesTableProps) {
+ const colSpan = 3;
+
+ return (
+
+
+
+ | Name |
+ Status |
+ Version |
+
+
+ (
+
+ {data.map((packageRow) => {
+ return (
+
+ | {packageRow.name} |
+
+ {packageRow.isScannable ? 'Scanned' : 'Not scanned'}
+ |
+ {packageRow.version} |
+
+ );
+ })}
+
+ )}
+ />
+
+ );
+}
+
+export default VirtualMachinePackagesTable;
diff --git a/ui/apps/platform/src/Containers/Vulnerabilities/VirtualMachineCves/VirtualMachine/VirtualMachinePage.tsx b/ui/apps/platform/src/Containers/Vulnerabilities/VirtualMachineCves/VirtualMachine/VirtualMachinePage.tsx
index 05684e786130f..6524a39335172 100644
--- a/ui/apps/platform/src/Containers/Vulnerabilities/VirtualMachineCves/VirtualMachine/VirtualMachinePage.tsx
+++ b/ui/apps/platform/src/Containers/Vulnerabilities/VirtualMachineCves/VirtualMachine/VirtualMachinePage.tsx
@@ -14,13 +14,25 @@ import {
import PageTitle from 'Components/PageTitle';
import BreadcrumbItemLink from 'Components/BreadcrumbItemLink';
+import { DEFAULT_VM_PAGE_SIZE } from 'Containers/Vulnerabilities/constants';
import useRestQuery from 'hooks/useRestQuery';
+import useURLPagination from 'hooks/useURLPagination';
+import useURLSearch from 'hooks/useURLSearch';
+import useURLSort from 'hooks/useURLSort';
import useURLStringUnion from 'hooks/useURLStringUnion';
import { getVirtualMachine } from 'services/VirtualMachineService';
import { detailsTabValues } from '../../types';
import { getOverviewPagePath } from '../../utils/searchUtils';
+import {
+ COMPONENT_SORT_FIELD,
+ CVE_EPSS_PROBABILITY_SORT_FIELD,
+ CVE_SEVERITY_SORT_FIELD,
+ CVE_SORT_FIELD,
+ CVSS_SORT_FIELD,
+} from '../../utils/sortFields';
import VirtualMachinePageHeader from './VirtualMachinePageHeader';
+import VirtualMachinePagePackages from './VirtualMachinePagePackages';
import VirtualMachinePageVulnerabilities from './VirtualMachinePageVulnerabilities';
const VULNERABILITIES_TAB_ID = 'vulnerabilities-tab-content';
@@ -30,8 +42,30 @@ const virtualMachineCveOverviewPath = getOverviewPagePath('VirtualMachine', {
entityTab: 'VirtualMachine',
});
+const sortFields = [
+ COMPONENT_SORT_FIELD,
+ CVE_EPSS_PROBABILITY_SORT_FIELD,
+ CVE_SORT_FIELD,
+ CVE_SEVERITY_SORT_FIELD,
+ CVSS_SORT_FIELD,
+];
+
+const defaultPackagesSortOption = { field: COMPONENT_SORT_FIELD, direction: 'asc' } as const;
+
+const defaultVulnerabilitiesSortOption = {
+ field: CVE_SEVERITY_SORT_FIELD,
+ direction: 'desc',
+} as const;
+
function VirtualMachinePage() {
const { virtualMachineId } = useParams() as { virtualMachineId: string };
+ const urlPagination = useURLPagination(DEFAULT_VM_PAGE_SIZE);
+ const urlSearch = useURLSearch();
+ const urlSorting = useURLSort({
+ sortFields,
+ defaultSortOption: defaultVulnerabilitiesSortOption,
+ onSort: () => urlPagination.setPage(1, 'replace'),
+ });
const fetchVirtualMachine = useCallback(
() => getVirtualMachine(virtualMachineId),
@@ -47,6 +81,17 @@ function VirtualMachinePage() {
const virtualMachineName = virtualMachineData?.name;
+ function onTabChange(value: string | number) {
+ if (value === packagesTabKey) {
+ urlSorting.setSortOption(defaultPackagesSortOption);
+ } else {
+ urlSorting.setSortOption(defaultVulnerabilitiesSortOption);
+ }
+ setActiveTabKey(value);
+ urlPagination.setPage(1, 'replace');
+ urlSearch.setSearchFilter({});
+ }
+
return (
<>
@@ -77,7 +122,7 @@ function VirtualMachinePage() {
{
- setActiveTabKey(key);
+ onTabChange(key);
}}
className="pf-v5-u-pl-md pf-v5-u-background-color-100"
>
@@ -117,11 +162,23 @@ function VirtualMachinePage() {
virtualMachineData={virtualMachineData}
isLoadingVirtualMachineData={isLoading}
errorVirtualMachineData={error}
+ urlSearch={urlSearch}
+ urlSorting={urlSorting}
+ urlPagination={urlPagination}
/>
)}
{activeTabKey === packagesTabKey && (
- packages table here
+
+
+
)}
>
diff --git a/ui/apps/platform/src/Containers/Vulnerabilities/VirtualMachineCves/VirtualMachine/VirtualMachinePagePackages.tsx b/ui/apps/platform/src/Containers/Vulnerabilities/VirtualMachineCves/VirtualMachine/VirtualMachinePagePackages.tsx
new file mode 100644
index 0000000000000..0886eb445c088
--- /dev/null
+++ b/ui/apps/platform/src/Containers/Vulnerabilities/VirtualMachineCves/VirtualMachine/VirtualMachinePagePackages.tsx
@@ -0,0 +1,191 @@
+import React, { useMemo } from 'react';
+import {
+ Flex,
+ PageSection,
+ Pagination,
+ pluralize,
+ Skeleton,
+ Split,
+ SplitItem,
+ Title,
+ Toolbar,
+ ToolbarContent,
+ ToolbarGroup,
+ ToolbarItem,
+} from '@patternfly/react-core';
+
+import CompoundSearchFilter from 'Components/CompoundSearchFilter/components/CompoundSearchFilter';
+import ComponentScannableStatusDropdown from 'Containers/Vulnerabilities/components/ComponentScannableStatusDropdown';
+import type { OnSearchPayload } from 'Components/CompoundSearchFilter/types';
+import { onURLSearch } from 'Components/CompoundSearchFilter/utils/utils';
+import { DynamicTableLabel } from 'Components/DynamicIcon';
+import SearchFilterChips from 'Components/PatternFly/SearchFilterChips';
+import type { UseURLPaginationResult } from 'hooks/useURLPagination';
+import type { UseUrlSearchReturn } from 'hooks/useURLSearch';
+import type { UseURLSortResult } from 'hooks/useURLSort';
+import type { VirtualMachine } from 'services/VirtualMachineService';
+import { getTableUIState } from 'utils/getTableUIState';
+import { getHasSearchApplied } from 'utils/searchUtils';
+
+import {
+ applyVirtualMachinePackagesTableFilters,
+ applyVirtualMachinePackagesTableSort,
+ getVirtualMachinePackagesTableData,
+} from '../aggregateUtils';
+import { virtualMachineComponentSearchFilterConfig } from '../../searchFilterConfig';
+import VirtualMachinePackagesTable from './VirtualMachinePackagesTable';
+
+export type VirtualMachinePagePackagesProps = {
+ virtualMachineData: VirtualMachine | undefined;
+ isLoadingVirtualMachineData: boolean;
+ errorVirtualMachineData: Error | undefined;
+ urlSearch: UseUrlSearchReturn;
+ urlSorting: UseURLSortResult;
+ urlPagination: UseURLPaginationResult;
+};
+
+const searchFilterConfig = [virtualMachineComponentSearchFilterConfig];
+
+function VirtualMachinePagePackages({
+ virtualMachineData,
+ isLoadingVirtualMachineData,
+ errorVirtualMachineData,
+ urlSearch,
+ urlSorting,
+ urlPagination,
+}: VirtualMachinePagePackagesProps) {
+ const { searchFilter, setSearchFilter } = urlSearch;
+ const { page, perPage, setPage, setPerPage } = urlPagination;
+ const { sortOption, getSortParams } = urlSorting;
+
+ const isFiltered = getHasSearchApplied(searchFilter);
+
+ const virtualMachinePackagesTableData = useMemo(
+ () => getVirtualMachinePackagesTableData(virtualMachineData),
+ [virtualMachineData]
+ );
+
+ const filteredVirtualMachinePackagesTableData = useMemo(
+ () =>
+ applyVirtualMachinePackagesTableFilters(virtualMachinePackagesTableData, searchFilter),
+ [virtualMachinePackagesTableData, searchFilter]
+ );
+
+ const sortedVirtualMachinePackagesTableData = useMemo(
+ () =>
+ applyVirtualMachinePackagesTableSort(
+ filteredVirtualMachinePackagesTableData,
+ Array.isArray(sortOption) ? sortOption[0].field : sortOption.field,
+ Array.isArray(sortOption) ? sortOption[0].reversed : sortOption.reversed
+ ),
+ [filteredVirtualMachinePackagesTableData, sortOption]
+ );
+
+ const paginatedVirtualMachinePackagesTableData = useMemo(() => {
+ const totalRows = sortedVirtualMachinePackagesTableData.length;
+ const maxPage = Math.max(1, Math.ceil(totalRows / perPage) || 1);
+ const safePage = Math.min(page, maxPage);
+
+ const start = (safePage - 1) * perPage;
+ const end = start + perPage;
+ return sortedVirtualMachinePackagesTableData.slice(start, end);
+ }, [sortedVirtualMachinePackagesTableData, page, perPage]);
+
+ const tableState = getTableUIState({
+ isLoading: isLoadingVirtualMachineData,
+ data: paginatedVirtualMachinePackagesTableData,
+ error: errorVirtualMachineData,
+ searchFilter,
+ });
+
+ function onClearFilters() {
+ setSearchFilter({});
+ setPage(1);
+ }
+
+ const onSearch = (payload: OnSearchPayload) => {
+ onURLSearch(searchFilter, setSearchFilter, payload);
+ setPage(1);
+ };
+
+ const onScannableStatusSelect = (
+ filterType: 'SCANNABLE',
+ checked: boolean,
+ selection: string
+ ) => {
+ const action = checked ? 'ADD' : 'REMOVE';
+ const category = filterType;
+ const value = selection;
+ onURLSearch(searchFilter, setSearchFilter, { action, category, value });
+ setPage(1);
+ };
+
+ return (
+