diff --git a/webpack/components/PreupgradeReportsTable/PreupgradeReportsTable.scss b/webpack/components/PreupgradeReportsTable/PreupgradeReportsTable.scss index 20abd21..2fc6baf 100644 --- a/webpack/components/PreupgradeReportsTable/PreupgradeReportsTable.scss +++ b/webpack/components/PreupgradeReportsTable/PreupgradeReportsTable.scss @@ -3,3 +3,12 @@ white-space: pre-wrap; } } + +.leapp-expanded-tbody { + border: 1px solid var(--pf-v5-global--BorderColor--100); + box-shadow: var(--pf-v5-global--BoxShadow--sm); + + tr:first-child > td { + background-color: var(--pf-v5-global--primary-color--light-background, #e7f1fa); + } +} diff --git a/webpack/components/PreupgradeReportsTable/__tests__/PreupgradeReportsTable.test.js b/webpack/components/PreupgradeReportsTable/__tests__/PreupgradeReportsTable.test.js index 3b3a995..bff7edc 100644 --- a/webpack/components/PreupgradeReportsTable/__tests__/PreupgradeReportsTable.test.js +++ b/webpack/components/PreupgradeReportsTable/__tests__/PreupgradeReportsTable.test.js @@ -1,17 +1,19 @@ -import React from 'react'; +import '@testing-library/jest-dom/extend-expect'; + import { + fireEvent, render, screen, waitFor, - fireEvent, within, } from '@testing-library/react'; -import '@testing-library/jest-dom/extend-expect'; + +import { APIActions } from 'foremanReact/redux/API'; +import PreupgradeReportsTable from '../index'; import { Provider } from 'react-redux'; +import React from 'react'; import configureMockStore from 'redux-mock-store'; import thunk from 'redux-thunk'; -import { APIActions } from 'foremanReact/redux/API'; -import PreupgradeReportsTable from '../index'; jest.mock('foremanReact/redux/API'); @@ -24,17 +26,25 @@ const mockJobData = { template_name: 'Run preupgrade via Leapp', }; +// Entry 0 (id=1): command remediation + inhibitor flag → fixable + selectable +// Entry 1 (id=2): hint-only remediation → has_remediation=Yes, NOT selectable +// Entries 2-11: no remediations → has_remediation=No, NOT selectable const mockEntries = Array.from({ length: 12 }, (_, i) => ({ id: i + 1, title: `Report Entry ${i + 1}`, hostname: 'example.com', + host_id: 100 + i, severity: i === 0 ? 'high' : 'low', summary: `Summary for report entry ${i + 1}`, tags: i === 0 ? ['security', 'network'] : [], flags: i === 0 ? ['inhibitor'] : [], detail: { remediations: - i === 0 ? [{ type: 'command', context: ['echo', 'fix_command'] }] : [], + i === 0 + ? [{ type: 'command', context: ['echo', 'fix_command'] }] + : i === 1 + ? [{ type: 'hint', context: 'Do something manually' }] + : null, external: i === 0 ? [{ url: 'http://example.com', title: 'External Link' }] : [], }, @@ -59,6 +69,8 @@ describe('PreupgradeReportsTable', () => { return { type: 'MOCK_API_SUCCESS' }; }; }); + + APIActions.post.mockImplementation(() => () => ({ type: 'MOCK_API_POST' })); }); const renderComponent = (data = mockJobData) => @@ -68,28 +80,52 @@ describe('PreupgradeReportsTable', () => { ); - const expandSection = () => { + const expandSection = () => fireEvent.click(screen.getByText('Leapp preupgrade report')); - }; + + const waitForTable = () => + waitFor(() => screen.getByText('Report Entry 1', { selector: 'td' })); it('renders data', async () => { renderComponent(); expandSection(); - await waitFor(() => screen.getByText('Report Entry 1', { selector: 'td' })); + await waitForTable(); expect( screen.getByText('Report Entry 1', { selector: 'td' }) ).toBeInTheDocument(); }); - it('expands a row and shows details', async () => { + it('does not render anything for non-Leapp jobs', () => { + renderComponent({ id: 55, template_name: 'Standard RHEL Update' }); + expect( + screen.queryByText('Leapp preupgrade report') + ).not.toBeInTheDocument(); + }); + + it('renders empty state message when no issues found', async () => { + APIActions.get.mockImplementation(({ key, handleSuccess }) => { + return () => { + if (key.includes('GET_LEAPP_REPORT_LIST')) + handleSuccess({ results: [{ id: mockReportId }] }); + if (key.includes('GET_LEAPP_REPORT_DETAIL')) + handleSuccess({ id: mockReportId, preupgrade_report_entries: [] }); + return { type: 'EMPTY' }; + }; + }); renderComponent(); expandSection(); - await waitFor(() => screen.getByText('Report Entry 1', { selector: 'td' })); - - const rowExpandButtons = screen.getAllByLabelText('Details'); - fireEvent.click(rowExpandButtons[0]); + await waitFor(() => + expect( + screen.getByText('The preupgrade report shows no issues.') + ).toBeInTheDocument() + ); + }); - expect(await screen.findByText('Summary')).toBeInTheDocument(); + it('expands a row and shows details', async () => { + renderComponent(); + expandSection(); + await waitForTable(); + fireEvent.click(screen.getAllByLabelText('Details')[0]); expect( await screen.findByText('Summary for report entry 1') ).toBeInTheDocument(); @@ -98,11 +134,8 @@ describe('PreupgradeReportsTable', () => { it('expands all rows', async () => { renderComponent(); expandSection(); - await waitFor(() => screen.getByText('Report Entry 1', { selector: 'td' })); - - const expandAllButton = screen.getByLabelText('Expand all rows'); - fireEvent.click(expandAllButton); - + await waitForTable(); + fireEvent.click(screen.getByLabelText('Expand all rows')); expect( await screen.findByText('Summary for report entry 1') ).toBeInTheDocument(); @@ -114,11 +147,9 @@ describe('PreupgradeReportsTable', () => { it('paginates to the next page', async () => { renderComponent(); expandSection(); - await waitFor(() => screen.getByText('Report Entry 1', { selector: 'td' })); - + await waitForTable(); fireEvent.click(screen.getAllByLabelText('Go to next page')[0]); await waitFor(() => screen.getByText('Report Entry 6', { selector: 'td' })); - expect( screen.getByText('Report Entry 10', { selector: 'td' }) ).toBeInTheDocument(); @@ -127,19 +158,168 @@ describe('PreupgradeReportsTable', () => { it('changes perPage limit to 10', async () => { renderComponent(); expandSection(); - await waitFor(() => screen.getByText('Report Entry 1', { selector: 'td' })); - + await waitForTable(); fireEvent.click(screen.getAllByLabelText('Items per page')[0]); fireEvent.click(screen.getAllByText('10 per page')[0]); - - await waitFor(() => { + await waitFor(() => expect( screen.getByText('Report Entry 10', { selector: 'td' }) - ).toBeInTheDocument(); - }); + ).toBeInTheDocument() + ); }); - it('renders empty state message when no issues found', async () => { + it('displays correct inhibitor status based on flags', async () => { + renderComponent(); + expandSection(); + await waitForTable(); + + const row1 = screen + .getByText('Report Entry 1', { selector: 'td' }) + .closest('tr'); + expect( + within(row1.querySelector('td[data-label="Inhibitor?"]')).getByText('Yes') + ).toBeInTheDocument(); + + const row2 = screen + .getByText('Report Entry 2', { selector: 'td' }) + .closest('tr'); + expect( + within(row2.querySelector('td[data-label="Inhibitor?"]')).getByText('No') + ).toBeInTheDocument(); + }); + + it('shows Has Remediation? Yes for any remediation type, not only command', async () => { + renderComponent(); + expandSection(); + await waitForTable(); + + // id=1: command → Yes + const row1 = screen + .getByText('Report Entry 1', { selector: 'td' }) + .closest('tr'); + expect( + within(row1.querySelector('td[data-label="Has Remediation?"]')).getByText( + 'Yes' + ) + ).toBeInTheDocument(); + + // id=2: hint-only → still Yes (display column shows any remediations) + const row2 = screen + .getByText('Report Entry 2', { selector: 'td' }) + .closest('tr'); + expect( + within(row2.querySelector('td[data-label="Has Remediation?"]')).getByText( + 'Yes' + ) + ).toBeInTheDocument(); + + // id=3: no remediations → No + const row3 = screen + .getByText('Report Entry 3', { selector: 'td' }) + .closest('tr'); + expect( + within(row3.querySelector('td[data-label="Has Remediation?"]')).getByText( + 'No' + ) + ).toBeInTheDocument(); + }); + + it('renders Fix Selected button disabled initially', async () => { + renderComponent(); + expandSection(); + await waitForTable(); + expect(screen.getByRole('button', { name: 'Fix Selected' })).toBeDisabled(); + }); + + it('enables Fix Selected after selecting a fixable (command) row', async () => { + renderComponent(); + expandSection(); + await waitForTable(); + + // checkboxes[0] = SelectAll, checkboxes[1] = entry id=1 (command remediation) + fireEvent.click(screen.getAllByRole('checkbox')[1]); + + await waitFor(() => + expect( + screen.getByRole('button', { name: 'Fix Selected' }) + ).not.toBeDisabled() + ); + }); + + it('Fix Selected dispatches APIActions.post with correct feature, host_ids and remediation_ids', async () => { + renderComponent(); + expandSection(); + await waitForTable(); + + fireEvent.click(screen.getAllByRole('checkbox')[1]); // select entry id=1 (host_id=100) + + await waitFor(() => + expect( + screen.getByRole('button', { name: 'Fix Selected' }) + ).not.toBeDisabled() + ); + + fireEvent.click(screen.getByRole('button', { name: 'Fix Selected' })); + + expect(APIActions.post).toHaveBeenCalledWith( + expect.objectContaining({ + url: expect.stringContaining('/api/job_invocations'), + params: { + job_invocation: { + feature: 'leapp_remediation_plan', + host_ids: [100], + inputs: { remediation_ids: '1' }, + }, + }, + }) + ); + }); + + it('hint-only row checkbox is disabled and does not enable Fix Selected', async () => { + renderComponent(); + expandSection(); + await waitForTable(); + + // checkboxes[2] = entry id=2, which has hint-only remediation — must be disabled + const hintCheckbox = screen.getAllByRole('checkbox')[2]; + expect(hintCheckbox).toBeDisabled(); + + fireEvent.click(hintCheckbox); + expect(screen.getByRole('button', { name: 'Fix Selected' })).toBeDisabled(); + }); + + it('renders Run Upgrade button enabled when entries are present', async () => { + renderComponent(); + expandSection(); + await waitForTable(); + expect( + screen.getByRole('button', { name: 'Run Upgrade' }) + ).not.toBeDisabled(); + }); + + it('Run Upgrade dispatches APIActions.post with correct feature and all host_ids', async () => { + renderComponent(); + expandSection(); + await waitForTable(); + + fireEvent.click(screen.getByRole('button', { name: 'Run Upgrade' })); + + expect(APIActions.post).toHaveBeenCalledWith( + expect.objectContaining({ + url: expect.stringContaining('/api/job_invocations'), + params: { + job_invocation: { + feature: 'leapp_upgrade', + host_ids: expect.arrayContaining([100, 101, 102]), + }, + }, + }) + ); + const callParams = APIActions.post.mock.calls[0][0].params.job_invocation; + expect(callParams.inputs).toBeUndefined(); + }); + + it('does not render toolbar buttons when report has no entries', async () => { APIActions.get.mockImplementation(({ key, handleSuccess }) => { return () => { if (key.includes('GET_LEAPP_REPORT_LIST')) @@ -151,36 +331,35 @@ describe('PreupgradeReportsTable', () => { }); renderComponent(); expandSection(); - await waitFor(() => { - expect( - screen.getByText('The preupgrade report shows no issues.') - ).toBeInTheDocument(); - }); - }); - - it('does not render anything for non-Leapp jobs', () => { - const nonLeappData = { id: 55, template_name: 'Standard RHEL Update' }; - renderComponent(nonLeappData); + await waitFor(() => + screen.getByText('The preupgrade report shows no issues.') + ); expect( - screen.queryByText('Leapp preupgrade report') + screen.queryByRole('button', { name: 'Fix Selected' }) + ).not.toBeInTheDocument(); + expect( + screen.queryByRole('button', { name: 'Run Upgrade' }) ).not.toBeInTheDocument(); }); - it('displays correct inhibitor status based on flags', async () => { + it('renders the SelectAll checkbox', async () => { renderComponent(); expandSection(); - await waitFor(() => screen.getByText('Report Entry 1', { selector: 'td' })); + await waitForTable(); + expect(screen.getAllByRole('checkbox').length).toBeGreaterThan(0); + }); - const row1 = screen - .getByText('Report Entry 1', { selector: 'td' }) - .closest('tr'); - const inhibitorCell1 = row1.querySelector('td[data-label="Inhibitor?"]'); - expect(within(inhibitorCell1).getByText('Yes')).toBeInTheDocument(); + it('selecting all fixable rows enables Fix Selected', async () => { + renderComponent(); + expandSection(); + await waitForTable(); - const row2 = screen - .getByText('Report Entry 2', { selector: 'td' }) - .closest('tr'); - const inhibitorCell2 = row2.querySelector('td[data-label="Inhibitor?"]'); - expect(within(inhibitorCell2).getByText('No')).toBeInTheDocument(); + fireEvent.click(screen.getByLabelText('Select all')); + + await waitFor(() => + expect( + screen.getByRole('button', { name: 'Fix Selected' }) + ).not.toBeDisabled() + ); }); }); diff --git a/webpack/components/PreupgradeReportsTable/index.js b/webpack/components/PreupgradeReportsTable/index.js index a732e0f..029e342 100644 --- a/webpack/components/PreupgradeReportsTable/index.js +++ b/webpack/components/PreupgradeReportsTable/index.js @@ -1,106 +1,151 @@ +/* eslint-disable max-lines */ import PropTypes from 'prop-types'; -import React, { useEffect, useState } from 'react'; +import React, { useCallback, useEffect, useMemo, useState } from 'react'; import { useDispatch } from 'react-redux'; -import { ExpandableSection, Tooltip } from '@patternfly/react-core'; +import { + Button, + ExpandableSection, + Toolbar, + ToolbarContent, + ToolbarGroup, + ToolbarItem, + Tooltip, +} from '@patternfly/react-core'; import { ExpandableRowContent, Tbody, Td, Tr } from '@patternfly/react-table'; import { translate as __ } from 'foremanReact/common/I18n'; +import { foremanUrl } from 'foremanReact/common/helpers'; import { Table } from 'foremanReact/components/PF4/TableIndexPage/Table/Table'; +import SelectAllCheckbox from 'foremanReact/components/PF4/TableIndexPage/Table/SelectAllCheckbox'; +import { useBulkSelect } from 'foremanReact/components/PF4/TableIndexPage/Table/TableHooks'; +import { RowSelectTd } from 'foremanReact/components/PF4/TableIndexPage/RowSelectTd'; import { getColumnHelpers } from 'foremanReact/components/PF4/TableIndexPage/Table/helpers'; import { APIActions } from 'foremanReact/redux/API'; import { STATUS } from 'foremanReact/constants'; -import { entriesPage } from '../PreupgradeReports/PreupgradeReportsHelpers'; +import { + entriesPage, + entryFixable, +} from '../PreupgradeReports/PreupgradeReportsHelpers'; import ReportDetails, { renderSeverityLabel } from './ReportDetails'; +import './PreupgradeReportsTable.scss'; + +const LEAPP_TEMPLATE_NAME = 'Run preupgrade via Leapp'; + +const isRowFixable = entryFixable; + +const submitJobInvocation = ( + dispatch, + setError, + feature, + hostIds, + remediationIds +) => { + const payload = { + job_invocation: { + feature, + host_ids: hostIds, + ...(remediationIds != null + ? { inputs: { remediation_ids: remediationIds } } + : {}), + }, + }; + + dispatch( + APIActions.post({ + key: `CREATE_JOB_INVOCATION_${feature}`, + url: foremanUrl('/api/job_invocations'), + params: payload, + handleSuccess: response => { + const result = response.data || response; + if (result?.id) { + window.location.assign(foremanUrl(`/job_invocations/${result.id}`)); + } + }, + handleError: err => setError(err), + }) + ); +}; const PreupgradeReportsTable = ({ data = {} }) => { const [error, setError] = useState(null); - - const [isReportExpanded, setIsReportExpanded] = useState(false); // Outer expansion state (Leapp Report Section) + const [isReportExpanded, setIsReportExpanded] = useState(false); const [pagination, setPagination] = useState({ page: 1, perPage: 5 }); const [reportData, setReportData] = useState(null); const [status, setStatus] = useState(STATUS.RESOLVED); - const [expandedRowIds, setExpandedRowIds] = useState(new Set()); // Inner table expansion state (Rows) + const [expandedRowIds, setExpandedRowIds] = useState(new Set()); const dispatch = useDispatch(); // eslint-disable-next-line camelcase - const isLeappJob = data?.template_name?.includes('Run preupgrade via Leapp'); + const isLeappJob = data?.template_name?.includes(LEAPP_TEMPLATE_NAME); - const columns = { - title: { - title: __('Title'), - }, - host: { - title: __('Host'), - wrapper: entry => - entry.hostname || (reportData && reportData.hostname) || '-', - }, - risk_factor: { - title: __('Risk Factor'), - wrapper: ({ severity }) => renderSeverityLabel(severity), - }, - has_remediation: { - title: __('Has Remediation?'), - wrapper: entry => - entry.detail && entry.detail.remediations ? __('Yes') : __('No'), - }, - inhibitor: { - title: __('Inhibitor?'), - wrapper: entry => - entry.flags && entry.flags.some(flag => flag === 'inhibitor') ? ( - - {__('Yes')} - - ) : ( - __('No') - ), - }, - }; + const columns = useMemo( + () => ({ + title: { title: __('Title') }, + host: { + title: __('Host'), + wrapper: entry => entry.hostname || reportData?.hostname || '-', + }, + risk_factor: { + title: __('Risk Factor'), + wrapper: ({ severity }) => renderSeverityLabel(severity), + }, + has_remediation: { + title: __('Has Remediation?'), + wrapper: entry => (entry.detail?.remediations ? __('Yes') : __('No')), + }, + inhibitor: { + title: __('Inhibitor?'), + wrapper: entry => + entry.flags?.some(flag => flag === 'inhibitor') ? ( + + {__('Yes')} + + ) : ( + __('No') + ), + }, + }), + // eslint-disable-next-line react-hooks/exhaustive-deps + [reportData?.hostname] + ); useEffect(() => { let isMounted = true; - if (!isLeappJob || !isReportExpanded || reportData) { - return undefined; - } - setStatus(STATUS.PENDING); + if (!isLeappJob || !isReportExpanded || reportData) return undefined; + + const fail = err => { + if (!isMounted) return; + setError(err); + setStatus(STATUS.ERROR); + }; + + const succeed = response => { + if (!isMounted) return; + setReportData(response?.data || response || {}); + setStatus(STATUS.RESOLVED); + }; + setStatus(STATUS.PENDING); dispatch( APIActions.get({ key: `GET_LEAPP_REPORT_LIST_${data.id}`, url: `/api/job_invocations/${data.id}/preupgrade_reports`, handleSuccess: listResponse => { if (!isMounted) return; - const listPayload = listResponse.data || listResponse; - const summary = listPayload.results?.[0]; + const summary = (listResponse.data || listResponse).results?.[0]; if (summary?.id) { dispatch( APIActions.get({ key: `GET_LEAPP_REPORT_DETAIL_${summary.id}`, url: `/api/preupgrade_reports/${summary.id}`, - handleSuccess: detailResponse => { - if (isMounted) { - const detailPayload = detailResponse.data || detailResponse; - setReportData(detailPayload); - setStatus(STATUS.RESOLVED); - } - }, - handleError: err => { - if (isMounted) { - setError(err); - setStatus(STATUS.ERROR); - } - }, + handleSuccess: detailResponse => succeed(detailResponse), + handleError: err => fail(err), }) ); - } else if (isMounted) { - setReportData({}); - setStatus(STATUS.RESOLVED); - } - }, - handleError: err => { - if (isMounted) { - setError(err); - setStatus(STATUS.ERROR); + return; } + succeed(); }, + handleError: err => fail(err), }) ); @@ -110,54 +155,201 @@ const PreupgradeReportsTable = ({ data = {} }) => { }, [isReportExpanded, data.id, isLeappJob, reportData, dispatch]); // eslint-disable-next-line camelcase - const entries = reportData?.preupgrade_report_entries || []; - const pagedEntries = entriesPage(entries, pagination); + const entries = useMemo(() => reportData?.preupgrade_report_entries || [], [ + reportData, + ]); - const handleParamsChange = newParams => { + const pagedEntries = useMemo( + () => entriesPage(entries, pagination), + // eslint-disable-next-line react-hooks/exhaustive-deps + [entries, pagination.page, pagination.perPage] + ); + + const getHostId = useCallback( + entry => + entry.host_id || + entry.hostId || + // eslint-disable-next-line camelcase + reportData?.host_id || + reportData?.host?.id || + // eslint-disable-next-line camelcase + data?.targeting?.host_id, + [reportData, data] + ); + + const handleParamsChange = useCallback(newParams => { setPagination(prev => ({ ...prev, page: newParams.page || prev.page, perPage: newParams.per_page || prev.perPage, })); setExpandedRowIds(new Set()); - }; + }, []); - const toggleRowExpansion = (id, isExpanding) => { + const toggleRowExpansion = useCallback((id, isExpanding) => { setExpandedRowIds(prev => { - const newSet = new Set(prev); - if (isExpanding) { - newSet.add(id); - } else { - newSet.delete(id); - } - return newSet; + const next = new Set(prev); + if (isExpanding) next.add(id); + else next.delete(id); + return next; }); - }; + }, []); + + const { inclusionSet, exclusionSet, ...selectAllOptions } = useBulkSelect({ + results: pagedEntries, + metadata: { + total: entries.length, + page: pagination.page, + selectable: entries.length, + }, + initialSearchQuery: '', + }); + + const { + selectAll, + selectPage, + selectNone, + selectOne, + areAllRowsSelected, + isSelected, + } = selectAllOptions; + + const rawSelectedIds = + areAllRowsSelected() || exclusionSet.size > 0 + ? entries.map(e => e.id).filter(id => !exclusionSet.has(id)) + : Array.from(inclusionSet); + + const validFixableIds = useMemo( + () => entries.filter(isRowFixable).map(e => e.id), + [entries] + ); + + // eslint-disable-next-line react-hooks/exhaustive-deps + const selectedIds = useMemo( + () => rawSelectedIds.filter(id => validFixableIds.includes(id)), + // eslint-disable-next-line react-hooks/exhaustive-deps + [rawSelectedIds.join(','), validFixableIds] + ); + + const pagedFixableEntries = useMemo(() => pagedEntries.filter(isRowFixable), [ + pagedEntries, + ]); + + const areAllPageFixableSelected = + pagedFixableEntries.length > 0 && + pagedFixableEntries.every(e => selectedIds.includes(e.id)); + + const areAllFixableSelected = + validFixableIds.length > 0 && + validFixableIds.every(id => selectedIds.includes(id)); const areAllRowsExpanded = pagedEntries.length > 0 && pagedEntries.every(entry => expandedRowIds.has(entry.id)); - const onExpandAll = () => { - setExpandedRowIds(() => { - if (areAllRowsExpanded) { - return new Set(); - } - return new Set(pagedEntries.map(e => e.id)); - }); - }; + const onExpandAll = useCallback(() => { + setExpandedRowIds( + areAllRowsExpanded ? new Set() : new Set(pagedEntries.map(e => e.id)) + ); + }, [areAllRowsExpanded, pagedEntries]); + + const [columnKeys, keysToColumnNames] = useMemo( + () => getColumnHelpers(columns), + [columns] + ); - const [columnKeys, keysToColumnNames] = getColumnHelpers(columns); + const hostIdsForSelected = useMemo( + () => + Array.from( + new Set( + entries + .filter(e => selectedIds.includes(e.id)) + .map(getHostId) + .filter(Boolean) + ) + ), + [entries, selectedIds, getHostId] + ); + + const allHostIds = useMemo( + () => Array.from(new Set(entries.map(getHostId).filter(Boolean))), + [entries, getHostId] + ); if (!isLeappJob) return null; + const isFixSelectedDisabled = + validFixableIds.length === 0 || + selectedIds.length === 0 || + hostIdsForSelected.length === 0; + return ( setIsReportExpanded(val)} toggleText={__('Leapp preupgrade report')} > + {entries.length > 0 && status === STATUS.RESOLVED && ( + + + + + + + + + + + + + + + + + + )} + { errorMessage={ status === STATUS.ERROR && error?.message ? error.message : null } - showCheckboxes={false} + showCheckboxes refreshData={() => {}} isDeleteable={false} emptyMessage={__('The preupgrade report shows no issues.')} setParams={handleParamsChange} childrenOutsideTbody onExpandAll={onExpandAll} - // Inverted per PatternFly implementation to ensure correct toggle icon state areAllRowsExpanded={!areAllRowsExpanded} > {pagedEntries.map((entry, rowIndex) => { const isRowExpanded = expandedRowIds.has(entry.id); + return ( - +
{ toggleRowExpansion(entry.id, isOpen), }} /> + isRowFixable(entry) && isSelected(id)} + isSelectable={isRowFixable} + /> {columnKeys.map(key => ( {columns[key].wrapper @@ -209,7 +411,7 @@ const PreupgradeReportsTable = ({ data = {} }) => { isExpanded={isRowExpanded} ouiaId={`table-row-details-${rowIndex}`} > - + {isRowExpanded && } @@ -227,6 +429,9 @@ PreupgradeReportsTable.propTypes = { data: PropTypes.shape({ id: PropTypes.number, template_name: PropTypes.string, + targeting: PropTypes.shape({ + host_id: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), + }), }), };