Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
.leapp-report-details {
dd {
white-space: pre-wrap;
}
}
134 changes: 134 additions & 0 deletions webpack/components/PreupgradeReportsTable/ReportDetails.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
import React from 'react';
import PropTypes from 'prop-types';
import {
DescriptionList,
DescriptionListGroup,
DescriptionListTerm,
DescriptionListDescription,
Label,
LabelGroup,
} from '@patternfly/react-core';
import { translate as __ } from 'foremanReact/common/I18n';
import './PreupgradeReportsTable.scss';

export const renderSeverityLabel = severity => {
switch (severity) {
case 'high':
return <Label color="red">{__('High')}</Label>;
case 'medium':
return <Label color="orange">{__('Medium')}</Label>;
case 'low':
return <Label color="blue">{__('Low')}</Label>;
case 'info':
return <Label color="grey">{__('Info')}</Label>;
default:
return <Label color="grey">{severity || __('Info')}</Label>;
}
};

const ReportDetails = ({ entry }) => (
<DescriptionList isHorizontal isCompact className="leapp-report-details">
{entry.title && (
<DescriptionListGroup>
<DescriptionListTerm>{__('Title')}</DescriptionListTerm>
<DescriptionListDescription>{entry.title}</DescriptionListDescription>
</DescriptionListGroup>
)}

{entry.severity && (
<DescriptionListGroup>
<DescriptionListTerm>{__('Risk Factor')}</DescriptionListTerm>
<DescriptionListDescription>
{renderSeverityLabel(entry.severity)}
</DescriptionListDescription>
</DescriptionListGroup>
)}

{entry.summary && (
<DescriptionListGroup>
<DescriptionListTerm>{__('Summary')}</DescriptionListTerm>
<DescriptionListDescription>{entry.summary}</DescriptionListDescription>
</DescriptionListGroup>
)}

{entry.tags && entry.tags.length > 0 && (
<DescriptionListGroup>
<DescriptionListTerm>{__('Tags')}</DescriptionListTerm>
<DescriptionListDescription>
<LabelGroup>
{entry.tags.map(tag => (
<Label key={tag} color="blue">
{tag}
</Label>
))}
</LabelGroup>
</DescriptionListDescription>
</DescriptionListGroup>
)}

{entry.detail?.external?.filter(link => link.url).length > 0 && (
<DescriptionListGroup>
<DescriptionListTerm>{__('Links')}</DescriptionListTerm>
<DescriptionListDescription>
{entry.detail.external
.filter(link => link.url)
.map((item, i) => (
<div key={item.url || i}>
<a href={item.url} target="_blank" rel="noopener noreferrer">
{item.title || item.url}
</a>
</div>
))}
</DescriptionListDescription>
</DescriptionListGroup>
)}

{entry.detail?.remediations?.length > 0 &&
entry.detail.remediations.map((item, i) => (
<DescriptionListGroup key={`remediations-${i}`}>
<DescriptionListTerm>
{item.type === 'command' ? __('Command') : __('Hint')}
</DescriptionListTerm>
<DescriptionListDescription>
{item.type === 'command' ? (
<code>
{Array.isArray(item.context)
? item.context.join(' ')
: item.context}
</code>
) : (
item.context
)}
</DescriptionListDescription>
</DescriptionListGroup>
))}
</DescriptionList>
);

ReportDetails.propTypes = {
entry: PropTypes.shape({
title: PropTypes.string,
severity: PropTypes.string,
summary: PropTypes.string,
tags: PropTypes.arrayOf(PropTypes.string),
detail: PropTypes.shape({
external: PropTypes.arrayOf(
PropTypes.shape({
url: PropTypes.string,
title: PropTypes.string,
})
),
remediations: PropTypes.arrayOf(
PropTypes.shape({
type: PropTypes.string,
context: PropTypes.oneOfType([
PropTypes.string,
PropTypes.arrayOf(PropTypes.string),
]),
})
),
}),
}).isRequired,
};

export default ReportDetails;
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
import React from 'react';
import { render, screen, waitFor, fireEvent } from '@testing-library/react';
import {
render,
screen,
waitFor,
fireEvent,
within,
} from '@testing-library/react';
import '@testing-library/jest-dom/extend-expect';
import { Provider } from 'react-redux';
import configureMockStore from 'redux-mock-store';
Expand All @@ -23,8 +29,15 @@ const mockEntries = Array.from({ length: 12 }, (_, i) => ({
title: `Report Entry ${i + 1}`,
hostname: 'example.com',
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: 'cmd' }] : [] },
detail: {
remediations:
i === 0 ? [{ type: 'command', context: ['echo', 'fix_command'] }] : [],
external:
i === 0 ? [{ url: 'http://example.com', title: 'External Link' }] : [],
},
}));

describe('PreupgradeReportsTable', () => {
Expand All @@ -48,10 +61,10 @@ describe('PreupgradeReportsTable', () => {
});
});

const renderComponent = () =>
const renderComponent = (data = mockJobData) =>
render(
<Provider store={store}>
<PreupgradeReportsTable data={mockJobData} />
<PreupgradeReportsTable data={data} />
</Provider>
);

Expand All @@ -62,37 +75,67 @@ describe('PreupgradeReportsTable', () => {
it('renders data', async () => {
renderComponent();
expandSection();
await waitFor(() => screen.getByText('Report Entry 1', { selector: 'td' }));
expect(
screen.getByText('Report Entry 1', { selector: 'td' })
).toBeInTheDocument();
});

it('expands a row and shows details', async () => {
renderComponent();
expandSection();
await waitFor(() => screen.getByText('Report Entry 1', { selector: 'td' }));

const rowExpandButtons = screen.getAllByLabelText('Details');
fireEvent.click(rowExpandButtons[0]);

expect(await screen.findByText('Summary')).toBeInTheDocument();
expect(
await screen.findByText('Summary for report entry 1')
).toBeInTheDocument();
});

it('expands all rows', async () => {
renderComponent();
expandSection();
await waitFor(() => screen.getByText('Report Entry 1', { selector: 'td' }));

await waitFor(() => screen.getByText('Report Entry 1'));
const expandAllButton = screen.getByLabelText('Expand all rows');
fireEvent.click(expandAllButton);

expect(screen.getByText('Report Entry 1')).toBeInTheDocument();
expect(screen.getByText('Report Entry 5')).toBeInTheDocument();
expect(screen.queryByText('Report Entry 6')).not.toBeInTheDocument();
expect(
await screen.findByText('Summary for report entry 1')
).toBeInTheDocument();
expect(
await screen.findByText('Summary for report entry 5')
).toBeInTheDocument();
});

it('paginates to the next page', async () => {
renderComponent();
expandSection();
await waitFor(() => screen.getByText('Report Entry 1'));
await waitFor(() => screen.getByText('Report Entry 1', { selector: 'td' }));

fireEvent.click(screen.getAllByLabelText('Go to next page')[0]);
await waitFor(() => screen.getByText('Report Entry 6', { selector: 'td' }));

await waitFor(() => screen.getByText('Report Entry 6'));
expect(screen.getByText('Report Entry 10')).toBeInTheDocument();
expect(screen.queryByText('Report Entry 1')).not.toBeInTheDocument();
expect(
screen.getByText('Report Entry 10', { selector: 'td' })
).toBeInTheDocument();
});

it('changes perPage limit to 10', async () => {
renderComponent();
expandSection();
await waitFor(() => screen.getByText('Report Entry 1'));
await waitFor(() => screen.getByText('Report Entry 1', { selector: 'td' }));

fireEvent.click(screen.getAllByLabelText('Items per page')[0]);
fireEvent.click(screen.getAllByText('10 per page')[0]);

await waitFor(() => {
expect(screen.getByText('Report Entry 10')).toBeInTheDocument();
expect(screen.queryByText('Report Entry 11')).not.toBeInTheDocument();
expect(
screen.getByText('Report Entry 10', { selector: 'td' })
).toBeInTheDocument();
});
});

Expand All @@ -106,12 +149,38 @@ describe('PreupgradeReportsTable', () => {
return { type: 'EMPTY' };
};
});

renderComponent();
expandSection();

await waitFor(() => {
expect(screen.getByText('The preupgrade report shows no issues.')).toBeInTheDocument();
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);
expect(
screen.queryByText('Leapp preupgrade report')
).not.toBeInTheDocument();
});

it('displays correct inhibitor status based on flags', async () => {
renderComponent();
expandSection();
await waitFor(() => screen.getByText('Report Entry 1', { selector: 'td' }));

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();

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();
});
});
Loading