foreman_leapp 3.2.1 → 3.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/Rakefile +0 -15
- data/lib/foreman_leapp/version.rb +1 -1
- data/webpack/components/PreupgradeReportsTable/PreupgradeReportsTable.scss +14 -0
- data/webpack/components/PreupgradeReportsTable/ReportDetails.js +134 -0
- data/webpack/components/PreupgradeReportsTable/__tests__/PreupgradeReportsTable.test.js +321 -29
- data/webpack/components/PreupgradeReportsTable/index.js +371 -87
- metadata +5 -17
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: a7e5030bef562d53e8f4212fa76732f0901ec4b827620a5f07028fd11bcf36d4
|
|
4
|
+
data.tar.gz: eac05d03ce3b746a996751d2b5affb5ea1fabea4d74dd8899f5771b5bc69f8ec
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 77e129433f0175d3951ccc870e66bda4f37930d888a83704142e80d95490e7a9c76b134ab3337c293e03c15bc9c770e8835ba9bec8e11e32d66087ff0a23a5a4
|
|
7
|
+
data.tar.gz: 29c9135b5faf4f6564c71d24cc989a6adc3d4a67c731a5bfca38d3618ec75745f9f1492295356386a7204971d53853558bf5a6d335f5252f55bc0d557bb42f7d
|
data/Rakefile
CHANGED
|
@@ -6,21 +6,6 @@ begin
|
|
|
6
6
|
rescue LoadError
|
|
7
7
|
puts 'You must `gem install bundler` and `bundle install` to run rake tasks'
|
|
8
8
|
end
|
|
9
|
-
begin
|
|
10
|
-
require 'rdoc/task'
|
|
11
|
-
rescue LoadError
|
|
12
|
-
require 'rdoc/rdoc'
|
|
13
|
-
require 'rake/rdoctask'
|
|
14
|
-
RDoc::Task = Rake::RDocTask
|
|
15
|
-
end
|
|
16
|
-
|
|
17
|
-
RDoc::Task.new(:rdoc) do |rdoc|
|
|
18
|
-
rdoc.rdoc_dir = 'rdoc'
|
|
19
|
-
rdoc.title = 'ForemanLeapp'
|
|
20
|
-
rdoc.options << '--line-numbers'
|
|
21
|
-
rdoc.rdoc_files.include('README.rdoc')
|
|
22
|
-
rdoc.rdoc_files.include('lib/**/*.rb')
|
|
23
|
-
end
|
|
24
9
|
|
|
25
10
|
APP_RAKEFILE = File.expand_path('test/dummy/Rakefile', __dir__)
|
|
26
11
|
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
.leapp-report-details {
|
|
2
|
+
dd {
|
|
3
|
+
white-space: pre-wrap;
|
|
4
|
+
}
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
.leapp-expanded-tbody {
|
|
8
|
+
border: 1px solid var(--pf-v5-global--BorderColor--100);
|
|
9
|
+
box-shadow: var(--pf-v5-global--BoxShadow--sm);
|
|
10
|
+
|
|
11
|
+
tr:first-child > td {
|
|
12
|
+
background-color: var(--pf-v5-global--primary-color--light-background, #e7f1fa);
|
|
13
|
+
}
|
|
14
|
+
}
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import PropTypes from 'prop-types';
|
|
3
|
+
import {
|
|
4
|
+
DescriptionList,
|
|
5
|
+
DescriptionListGroup,
|
|
6
|
+
DescriptionListTerm,
|
|
7
|
+
DescriptionListDescription,
|
|
8
|
+
Label,
|
|
9
|
+
LabelGroup,
|
|
10
|
+
} from '@patternfly/react-core';
|
|
11
|
+
import { translate as __ } from 'foremanReact/common/I18n';
|
|
12
|
+
import './PreupgradeReportsTable.scss';
|
|
13
|
+
|
|
14
|
+
export const renderSeverityLabel = severity => {
|
|
15
|
+
switch (severity) {
|
|
16
|
+
case 'high':
|
|
17
|
+
return <Label color="red">{__('High')}</Label>;
|
|
18
|
+
case 'medium':
|
|
19
|
+
return <Label color="orange">{__('Medium')}</Label>;
|
|
20
|
+
case 'low':
|
|
21
|
+
return <Label color="blue">{__('Low')}</Label>;
|
|
22
|
+
case 'info':
|
|
23
|
+
return <Label color="grey">{__('Info')}</Label>;
|
|
24
|
+
default:
|
|
25
|
+
return <Label color="grey">{severity || __('Info')}</Label>;
|
|
26
|
+
}
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
const ReportDetails = ({ entry }) => (
|
|
30
|
+
<DescriptionList isHorizontal isCompact className="leapp-report-details">
|
|
31
|
+
{entry.title && (
|
|
32
|
+
<DescriptionListGroup>
|
|
33
|
+
<DescriptionListTerm>{__('Title')}</DescriptionListTerm>
|
|
34
|
+
<DescriptionListDescription>{entry.title}</DescriptionListDescription>
|
|
35
|
+
</DescriptionListGroup>
|
|
36
|
+
)}
|
|
37
|
+
|
|
38
|
+
{entry.severity && (
|
|
39
|
+
<DescriptionListGroup>
|
|
40
|
+
<DescriptionListTerm>{__('Risk Factor')}</DescriptionListTerm>
|
|
41
|
+
<DescriptionListDescription>
|
|
42
|
+
{renderSeverityLabel(entry.severity)}
|
|
43
|
+
</DescriptionListDescription>
|
|
44
|
+
</DescriptionListGroup>
|
|
45
|
+
)}
|
|
46
|
+
|
|
47
|
+
{entry.summary && (
|
|
48
|
+
<DescriptionListGroup>
|
|
49
|
+
<DescriptionListTerm>{__('Summary')}</DescriptionListTerm>
|
|
50
|
+
<DescriptionListDescription>{entry.summary}</DescriptionListDescription>
|
|
51
|
+
</DescriptionListGroup>
|
|
52
|
+
)}
|
|
53
|
+
|
|
54
|
+
{entry.tags && entry.tags.length > 0 && (
|
|
55
|
+
<DescriptionListGroup>
|
|
56
|
+
<DescriptionListTerm>{__('Tags')}</DescriptionListTerm>
|
|
57
|
+
<DescriptionListDescription>
|
|
58
|
+
<LabelGroup>
|
|
59
|
+
{entry.tags.map(tag => (
|
|
60
|
+
<Label key={tag} color="blue">
|
|
61
|
+
{tag}
|
|
62
|
+
</Label>
|
|
63
|
+
))}
|
|
64
|
+
</LabelGroup>
|
|
65
|
+
</DescriptionListDescription>
|
|
66
|
+
</DescriptionListGroup>
|
|
67
|
+
)}
|
|
68
|
+
|
|
69
|
+
{entry.detail?.external?.filter(link => link.url).length > 0 && (
|
|
70
|
+
<DescriptionListGroup>
|
|
71
|
+
<DescriptionListTerm>{__('Links')}</DescriptionListTerm>
|
|
72
|
+
<DescriptionListDescription>
|
|
73
|
+
{entry.detail.external
|
|
74
|
+
.filter(link => link.url)
|
|
75
|
+
.map((item, i) => (
|
|
76
|
+
<div key={item.url || i}>
|
|
77
|
+
<a href={item.url} target="_blank" rel="noopener noreferrer">
|
|
78
|
+
{item.title || item.url}
|
|
79
|
+
</a>
|
|
80
|
+
</div>
|
|
81
|
+
))}
|
|
82
|
+
</DescriptionListDescription>
|
|
83
|
+
</DescriptionListGroup>
|
|
84
|
+
)}
|
|
85
|
+
|
|
86
|
+
{entry.detail?.remediations?.length > 0 &&
|
|
87
|
+
entry.detail.remediations.map((item, i) => (
|
|
88
|
+
<DescriptionListGroup key={`remediations-${i}`}>
|
|
89
|
+
<DescriptionListTerm>
|
|
90
|
+
{item.type === 'command' ? __('Command') : __('Hint')}
|
|
91
|
+
</DescriptionListTerm>
|
|
92
|
+
<DescriptionListDescription>
|
|
93
|
+
{item.type === 'command' ? (
|
|
94
|
+
<code>
|
|
95
|
+
{Array.isArray(item.context)
|
|
96
|
+
? item.context.join(' ')
|
|
97
|
+
: item.context}
|
|
98
|
+
</code>
|
|
99
|
+
) : (
|
|
100
|
+
item.context
|
|
101
|
+
)}
|
|
102
|
+
</DescriptionListDescription>
|
|
103
|
+
</DescriptionListGroup>
|
|
104
|
+
))}
|
|
105
|
+
</DescriptionList>
|
|
106
|
+
);
|
|
107
|
+
|
|
108
|
+
ReportDetails.propTypes = {
|
|
109
|
+
entry: PropTypes.shape({
|
|
110
|
+
title: PropTypes.string,
|
|
111
|
+
severity: PropTypes.string,
|
|
112
|
+
summary: PropTypes.string,
|
|
113
|
+
tags: PropTypes.arrayOf(PropTypes.string),
|
|
114
|
+
detail: PropTypes.shape({
|
|
115
|
+
external: PropTypes.arrayOf(
|
|
116
|
+
PropTypes.shape({
|
|
117
|
+
url: PropTypes.string,
|
|
118
|
+
title: PropTypes.string,
|
|
119
|
+
})
|
|
120
|
+
),
|
|
121
|
+
remediations: PropTypes.arrayOf(
|
|
122
|
+
PropTypes.shape({
|
|
123
|
+
type: PropTypes.string,
|
|
124
|
+
context: PropTypes.oneOfType([
|
|
125
|
+
PropTypes.string,
|
|
126
|
+
PropTypes.arrayOf(PropTypes.string),
|
|
127
|
+
]),
|
|
128
|
+
})
|
|
129
|
+
),
|
|
130
|
+
}),
|
|
131
|
+
}).isRequired,
|
|
132
|
+
};
|
|
133
|
+
|
|
134
|
+
export default ReportDetails;
|
|
@@ -1,11 +1,19 @@
|
|
|
1
|
-
import React from 'react';
|
|
2
|
-
import { render, screen, waitFor, fireEvent } from '@testing-library/react';
|
|
3
1
|
import '@testing-library/jest-dom/extend-expect';
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
fireEvent,
|
|
5
|
+
render,
|
|
6
|
+
screen,
|
|
7
|
+
waitFor,
|
|
8
|
+
within,
|
|
9
|
+
} from '@testing-library/react';
|
|
10
|
+
|
|
11
|
+
import { APIActions } from 'foremanReact/redux/API';
|
|
12
|
+
import PreupgradeReportsTable from '../index';
|
|
4
13
|
import { Provider } from 'react-redux';
|
|
14
|
+
import React from 'react';
|
|
5
15
|
import configureMockStore from 'redux-mock-store';
|
|
6
16
|
import thunk from 'redux-thunk';
|
|
7
|
-
import { APIActions } from 'foremanReact/redux/API';
|
|
8
|
-
import PreupgradeReportsTable from '../index';
|
|
9
17
|
|
|
10
18
|
jest.mock('foremanReact/redux/API');
|
|
11
19
|
|
|
@@ -18,13 +26,28 @@ const mockJobData = {
|
|
|
18
26
|
template_name: 'Run preupgrade via Leapp',
|
|
19
27
|
};
|
|
20
28
|
|
|
29
|
+
// Entry 0 (id=1): command remediation + inhibitor flag → fixable + selectable
|
|
30
|
+
// Entry 1 (id=2): hint-only remediation → has_remediation=Yes, NOT selectable
|
|
31
|
+
// Entries 2-11: no remediations → has_remediation=No, NOT selectable
|
|
21
32
|
const mockEntries = Array.from({ length: 12 }, (_, i) => ({
|
|
22
33
|
id: i + 1,
|
|
23
34
|
title: `Report Entry ${i + 1}`,
|
|
24
35
|
hostname: 'example.com',
|
|
36
|
+
host_id: 100 + i,
|
|
25
37
|
severity: i === 0 ? 'high' : 'low',
|
|
38
|
+
summary: `Summary for report entry ${i + 1}`,
|
|
39
|
+
tags: i === 0 ? ['security', 'network'] : [],
|
|
26
40
|
flags: i === 0 ? ['inhibitor'] : [],
|
|
27
|
-
detail: {
|
|
41
|
+
detail: {
|
|
42
|
+
remediations:
|
|
43
|
+
i === 0
|
|
44
|
+
? [{ type: 'command', context: ['echo', 'fix_command'] }]
|
|
45
|
+
: i === 1
|
|
46
|
+
? [{ type: 'hint', context: 'Do something manually' }]
|
|
47
|
+
: null,
|
|
48
|
+
external:
|
|
49
|
+
i === 0 ? [{ url: 'http://example.com', title: 'External Link' }] : [],
|
|
50
|
+
},
|
|
28
51
|
}));
|
|
29
52
|
|
|
30
53
|
describe('PreupgradeReportsTable', () => {
|
|
@@ -46,57 +69,301 @@ describe('PreupgradeReportsTable', () => {
|
|
|
46
69
|
return { type: 'MOCK_API_SUCCESS' };
|
|
47
70
|
};
|
|
48
71
|
});
|
|
72
|
+
|
|
73
|
+
APIActions.post.mockImplementation(() => () => ({ type: 'MOCK_API_POST' }));
|
|
49
74
|
});
|
|
50
75
|
|
|
51
|
-
const renderComponent = () =>
|
|
76
|
+
const renderComponent = (data = mockJobData) =>
|
|
52
77
|
render(
|
|
53
78
|
<Provider store={store}>
|
|
54
|
-
<PreupgradeReportsTable data={
|
|
79
|
+
<PreupgradeReportsTable data={data} />
|
|
55
80
|
</Provider>
|
|
56
81
|
);
|
|
57
82
|
|
|
58
|
-
const expandSection = () =>
|
|
83
|
+
const expandSection = () =>
|
|
59
84
|
fireEvent.click(screen.getByText('Leapp preupgrade report'));
|
|
60
|
-
|
|
85
|
+
|
|
86
|
+
const waitForTable = () =>
|
|
87
|
+
waitFor(() => screen.getByText('Report Entry 1', { selector: 'td' }));
|
|
61
88
|
|
|
62
89
|
it('renders data', async () => {
|
|
63
90
|
renderComponent();
|
|
64
91
|
expandSection();
|
|
92
|
+
await waitForTable();
|
|
93
|
+
expect(
|
|
94
|
+
screen.getByText('Report Entry 1', { selector: 'td' })
|
|
95
|
+
).toBeInTheDocument();
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
it('does not render anything for non-Leapp jobs', () => {
|
|
99
|
+
renderComponent({ id: 55, template_name: 'Standard RHEL Update' });
|
|
100
|
+
expect(
|
|
101
|
+
screen.queryByText('Leapp preupgrade report')
|
|
102
|
+
).not.toBeInTheDocument();
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
it('refetches when status_label transitions (e.g. Running → Succeeded)', async () => {
|
|
106
|
+
const { rerender } = render(
|
|
107
|
+
<Provider store={store}>
|
|
108
|
+
<PreupgradeReportsTable
|
|
109
|
+
data={{ ...mockJobData, status_label: 'Running' }}
|
|
110
|
+
/>
|
|
111
|
+
</Provider>
|
|
112
|
+
);
|
|
113
|
+
expandSection();
|
|
114
|
+
await waitForTable();
|
|
115
|
+
|
|
116
|
+
const callCountAfterFirstFetch = APIActions.get.mock.calls.length;
|
|
65
117
|
|
|
66
|
-
|
|
118
|
+
rerender(
|
|
119
|
+
<Provider store={store}>
|
|
120
|
+
<PreupgradeReportsTable
|
|
121
|
+
data={{ ...mockJobData, status_label: 'Succeeded' }}
|
|
122
|
+
/>
|
|
123
|
+
</Provider>
|
|
124
|
+
);
|
|
67
125
|
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
126
|
+
await waitFor(() =>
|
|
127
|
+
expect(APIActions.get.mock.calls.length).toBeGreaterThan(
|
|
128
|
+
callCountAfterFirstFetch
|
|
129
|
+
)
|
|
130
|
+
);
|
|
131
|
+
expect(
|
|
132
|
+
screen.getByText('Report Entry 1', { selector: 'td' })
|
|
133
|
+
).toBeInTheDocument();
|
|
71
134
|
});
|
|
72
135
|
|
|
73
|
-
it('
|
|
136
|
+
it('does not refetch on collapse/re-expand when status_label is unchanged', async () => {
|
|
137
|
+
renderComponent({ ...mockJobData, status_label: 'Succeeded' });
|
|
138
|
+
expandSection();
|
|
139
|
+
await waitForTable();
|
|
140
|
+
|
|
141
|
+
const callCountAfterFirstFetch = APIActions.get.mock.calls.length;
|
|
142
|
+
|
|
143
|
+
fireEvent.click(screen.getByText('Leapp preupgrade report')); // collapse
|
|
144
|
+
fireEvent.click(screen.getByText('Leapp preupgrade report')); // re-expand
|
|
145
|
+
|
|
146
|
+
expect(APIActions.get.mock.calls.length).toBe(callCountAfterFirstFetch);
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
it('renders empty state message when no issues found', async () => {
|
|
150
|
+
APIActions.get.mockImplementation(({ key, handleSuccess }) => {
|
|
151
|
+
return () => {
|
|
152
|
+
if (key.includes('GET_LEAPP_REPORT_LIST'))
|
|
153
|
+
handleSuccess({ results: [{ id: mockReportId }] });
|
|
154
|
+
if (key.includes('GET_LEAPP_REPORT_DETAIL'))
|
|
155
|
+
handleSuccess({ id: mockReportId, preupgrade_report_entries: [] });
|
|
156
|
+
return { type: 'EMPTY' };
|
|
157
|
+
};
|
|
158
|
+
});
|
|
74
159
|
renderComponent();
|
|
75
160
|
expandSection();
|
|
76
|
-
await waitFor(() =>
|
|
161
|
+
await waitFor(() =>
|
|
162
|
+
expect(
|
|
163
|
+
screen.getByText('The preupgrade report shows no issues.')
|
|
164
|
+
).toBeInTheDocument()
|
|
165
|
+
);
|
|
166
|
+
});
|
|
77
167
|
|
|
78
|
-
|
|
168
|
+
it('expands a row and shows details', async () => {
|
|
169
|
+
renderComponent();
|
|
170
|
+
expandSection();
|
|
171
|
+
await waitForTable();
|
|
172
|
+
fireEvent.click(screen.getAllByLabelText('Details')[0]);
|
|
173
|
+
expect(
|
|
174
|
+
await screen.findByText('Summary for report entry 1')
|
|
175
|
+
).toBeInTheDocument();
|
|
176
|
+
});
|
|
79
177
|
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
178
|
+
it('expands all rows', async () => {
|
|
179
|
+
renderComponent();
|
|
180
|
+
expandSection();
|
|
181
|
+
await waitForTable();
|
|
182
|
+
fireEvent.click(screen.getByLabelText('Expand all rows'));
|
|
183
|
+
expect(
|
|
184
|
+
await screen.findByText('Summary for report entry 1')
|
|
185
|
+
).toBeInTheDocument();
|
|
186
|
+
expect(
|
|
187
|
+
await screen.findByText('Summary for report entry 5')
|
|
188
|
+
).toBeInTheDocument();
|
|
83
189
|
});
|
|
84
190
|
|
|
85
|
-
it('
|
|
191
|
+
it('paginates to the next page', async () => {
|
|
86
192
|
renderComponent();
|
|
87
193
|
expandSection();
|
|
88
|
-
await
|
|
194
|
+
await waitForTable();
|
|
195
|
+
fireEvent.click(screen.getAllByLabelText('Go to next page')[0]);
|
|
196
|
+
await waitFor(() => screen.getByText('Report Entry 6', { selector: 'td' }));
|
|
197
|
+
expect(
|
|
198
|
+
screen.getByText('Report Entry 10', { selector: 'td' })
|
|
199
|
+
).toBeInTheDocument();
|
|
200
|
+
});
|
|
89
201
|
|
|
202
|
+
it('changes perPage limit to 10', async () => {
|
|
203
|
+
renderComponent();
|
|
204
|
+
expandSection();
|
|
205
|
+
await waitForTable();
|
|
90
206
|
fireEvent.click(screen.getAllByLabelText('Items per page')[0]);
|
|
91
207
|
fireEvent.click(screen.getAllByText('10 per page')[0]);
|
|
208
|
+
await waitFor(() =>
|
|
209
|
+
expect(
|
|
210
|
+
screen.getByText('Report Entry 10', { selector: 'td' })
|
|
211
|
+
).toBeInTheDocument()
|
|
212
|
+
);
|
|
213
|
+
});
|
|
92
214
|
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
215
|
+
it('displays correct inhibitor status based on flags', async () => {
|
|
216
|
+
renderComponent();
|
|
217
|
+
expandSection();
|
|
218
|
+
await waitForTable();
|
|
219
|
+
|
|
220
|
+
const row1 = screen
|
|
221
|
+
.getByText('Report Entry 1', { selector: 'td' })
|
|
222
|
+
.closest('tr');
|
|
223
|
+
expect(
|
|
224
|
+
within(row1.querySelector('td[data-label="Inhibitor?"]')).getByText('Yes')
|
|
225
|
+
).toBeInTheDocument();
|
|
226
|
+
|
|
227
|
+
const row2 = screen
|
|
228
|
+
.getByText('Report Entry 2', { selector: 'td' })
|
|
229
|
+
.closest('tr');
|
|
230
|
+
expect(
|
|
231
|
+
within(row2.querySelector('td[data-label="Inhibitor?"]')).getByText('No')
|
|
232
|
+
).toBeInTheDocument();
|
|
97
233
|
});
|
|
98
234
|
|
|
99
|
-
it('
|
|
235
|
+
it('shows Has Remediation? Yes for any remediation type, not only command', async () => {
|
|
236
|
+
renderComponent();
|
|
237
|
+
expandSection();
|
|
238
|
+
await waitForTable();
|
|
239
|
+
|
|
240
|
+
// id=1: command → Yes
|
|
241
|
+
const row1 = screen
|
|
242
|
+
.getByText('Report Entry 1', { selector: 'td' })
|
|
243
|
+
.closest('tr');
|
|
244
|
+
expect(
|
|
245
|
+
within(row1.querySelector('td[data-label="Has Remediation?"]')).getByText(
|
|
246
|
+
'Yes'
|
|
247
|
+
)
|
|
248
|
+
).toBeInTheDocument();
|
|
249
|
+
|
|
250
|
+
// id=2: hint-only → still Yes (display column shows any remediations)
|
|
251
|
+
const row2 = screen
|
|
252
|
+
.getByText('Report Entry 2', { selector: 'td' })
|
|
253
|
+
.closest('tr');
|
|
254
|
+
expect(
|
|
255
|
+
within(row2.querySelector('td[data-label="Has Remediation?"]')).getByText(
|
|
256
|
+
'Yes'
|
|
257
|
+
)
|
|
258
|
+
).toBeInTheDocument();
|
|
259
|
+
|
|
260
|
+
// id=3: no remediations → No
|
|
261
|
+
const row3 = screen
|
|
262
|
+
.getByText('Report Entry 3', { selector: 'td' })
|
|
263
|
+
.closest('tr');
|
|
264
|
+
expect(
|
|
265
|
+
within(row3.querySelector('td[data-label="Has Remediation?"]')).getByText(
|
|
266
|
+
'No'
|
|
267
|
+
)
|
|
268
|
+
).toBeInTheDocument();
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
it('renders Fix Selected button disabled initially', async () => {
|
|
272
|
+
renderComponent();
|
|
273
|
+
expandSection();
|
|
274
|
+
await waitForTable();
|
|
275
|
+
expect(screen.getByRole('button', { name: 'Fix Selected' })).toBeDisabled();
|
|
276
|
+
});
|
|
277
|
+
|
|
278
|
+
it('enables Fix Selected after selecting a fixable (command) row', async () => {
|
|
279
|
+
renderComponent();
|
|
280
|
+
expandSection();
|
|
281
|
+
await waitForTable();
|
|
282
|
+
|
|
283
|
+
// checkboxes[0] = SelectAll, checkboxes[1] = entry id=1 (command remediation)
|
|
284
|
+
fireEvent.click(screen.getAllByRole('checkbox')[1]);
|
|
285
|
+
|
|
286
|
+
await waitFor(() =>
|
|
287
|
+
expect(
|
|
288
|
+
screen.getByRole('button', { name: 'Fix Selected' })
|
|
289
|
+
).not.toBeDisabled()
|
|
290
|
+
);
|
|
291
|
+
});
|
|
292
|
+
|
|
293
|
+
it('Fix Selected dispatches APIActions.post with correct feature, host_ids and remediation_ids', async () => {
|
|
294
|
+
renderComponent();
|
|
295
|
+
expandSection();
|
|
296
|
+
await waitForTable();
|
|
297
|
+
|
|
298
|
+
fireEvent.click(screen.getAllByRole('checkbox')[1]); // select entry id=1 (host_id=100)
|
|
299
|
+
|
|
300
|
+
await waitFor(() =>
|
|
301
|
+
expect(
|
|
302
|
+
screen.getByRole('button', { name: 'Fix Selected' })
|
|
303
|
+
).not.toBeDisabled()
|
|
304
|
+
);
|
|
305
|
+
|
|
306
|
+
fireEvent.click(screen.getByRole('button', { name: 'Fix Selected' }));
|
|
307
|
+
|
|
308
|
+
expect(APIActions.post).toHaveBeenCalledWith(
|
|
309
|
+
expect.objectContaining({
|
|
310
|
+
url: expect.stringContaining('/api/job_invocations'),
|
|
311
|
+
params: {
|
|
312
|
+
job_invocation: {
|
|
313
|
+
feature: 'leapp_remediation_plan',
|
|
314
|
+
host_ids: [100],
|
|
315
|
+
inputs: { remediation_ids: '1' },
|
|
316
|
+
},
|
|
317
|
+
},
|
|
318
|
+
})
|
|
319
|
+
);
|
|
320
|
+
});
|
|
321
|
+
|
|
322
|
+
it('hint-only row checkbox is disabled and does not enable Fix Selected', async () => {
|
|
323
|
+
renderComponent();
|
|
324
|
+
expandSection();
|
|
325
|
+
await waitForTable();
|
|
326
|
+
|
|
327
|
+
// checkboxes[2] = entry id=2, which has hint-only remediation — must be disabled
|
|
328
|
+
const hintCheckbox = screen.getAllByRole('checkbox')[2];
|
|
329
|
+
expect(hintCheckbox).toBeDisabled();
|
|
330
|
+
|
|
331
|
+
fireEvent.click(hintCheckbox);
|
|
332
|
+
expect(screen.getByRole('button', { name: 'Fix Selected' })).toBeDisabled();
|
|
333
|
+
});
|
|
334
|
+
|
|
335
|
+
it('renders Run Upgrade button enabled when entries are present', async () => {
|
|
336
|
+
renderComponent();
|
|
337
|
+
expandSection();
|
|
338
|
+
await waitForTable();
|
|
339
|
+
expect(
|
|
340
|
+
screen.getByRole('button', { name: 'Run Upgrade' })
|
|
341
|
+
).not.toBeDisabled();
|
|
342
|
+
});
|
|
343
|
+
|
|
344
|
+
it('Run Upgrade dispatches APIActions.post with correct feature and all host_ids', async () => {
|
|
345
|
+
renderComponent();
|
|
346
|
+
expandSection();
|
|
347
|
+
await waitForTable();
|
|
348
|
+
|
|
349
|
+
fireEvent.click(screen.getByRole('button', { name: 'Run Upgrade' }));
|
|
350
|
+
|
|
351
|
+
expect(APIActions.post).toHaveBeenCalledWith(
|
|
352
|
+
expect.objectContaining({
|
|
353
|
+
url: expect.stringContaining('/api/job_invocations'),
|
|
354
|
+
params: {
|
|
355
|
+
job_invocation: {
|
|
356
|
+
feature: 'leapp_upgrade',
|
|
357
|
+
host_ids: expect.arrayContaining([100, 101, 102]),
|
|
358
|
+
},
|
|
359
|
+
},
|
|
360
|
+
})
|
|
361
|
+
);
|
|
362
|
+
const callParams = APIActions.post.mock.calls[0][0].params.job_invocation;
|
|
363
|
+
expect(callParams.inputs).toBeUndefined();
|
|
364
|
+
});
|
|
365
|
+
|
|
366
|
+
it('does not render toolbar buttons when report has no entries', async () => {
|
|
100
367
|
APIActions.get.mockImplementation(({ key, handleSuccess }) => {
|
|
101
368
|
return () => {
|
|
102
369
|
if (key.includes('GET_LEAPP_REPORT_LIST'))
|
|
@@ -106,12 +373,37 @@ describe('PreupgradeReportsTable', () => {
|
|
|
106
373
|
return { type: 'EMPTY' };
|
|
107
374
|
};
|
|
108
375
|
});
|
|
376
|
+
renderComponent();
|
|
377
|
+
expandSection();
|
|
378
|
+
await waitFor(() =>
|
|
379
|
+
screen.getByText('The preupgrade report shows no issues.')
|
|
380
|
+
);
|
|
381
|
+
expect(
|
|
382
|
+
screen.queryByRole('button', { name: 'Fix Selected' })
|
|
383
|
+
).not.toBeInTheDocument();
|
|
384
|
+
expect(
|
|
385
|
+
screen.queryByRole('button', { name: 'Run Upgrade' })
|
|
386
|
+
).not.toBeInTheDocument();
|
|
387
|
+
});
|
|
109
388
|
|
|
389
|
+
it('renders the SelectAll checkbox', async () => {
|
|
110
390
|
renderComponent();
|
|
111
391
|
expandSection();
|
|
392
|
+
await waitForTable();
|
|
393
|
+
expect(screen.getAllByRole('checkbox').length).toBeGreaterThan(0);
|
|
394
|
+
});
|
|
112
395
|
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
396
|
+
it('selecting all fixable rows enables Fix Selected', async () => {
|
|
397
|
+
renderComponent();
|
|
398
|
+
expandSection();
|
|
399
|
+
await waitForTable();
|
|
400
|
+
|
|
401
|
+
fireEvent.click(screen.getByLabelText('Select all'));
|
|
402
|
+
|
|
403
|
+
await waitFor(() =>
|
|
404
|
+
expect(
|
|
405
|
+
screen.getByRole('button', { name: 'Fix Selected' })
|
|
406
|
+
).not.toBeDisabled()
|
|
407
|
+
);
|
|
116
408
|
});
|
|
117
409
|
});
|
|
@@ -1,145 +1,374 @@
|
|
|
1
|
+
/* eslint-disable max-lines */
|
|
1
2
|
import PropTypes from 'prop-types';
|
|
2
|
-
import React, {
|
|
3
|
+
import React, {
|
|
4
|
+
useCallback,
|
|
5
|
+
useEffect,
|
|
6
|
+
useMemo,
|
|
7
|
+
useRef,
|
|
8
|
+
useState,
|
|
9
|
+
} from 'react';
|
|
3
10
|
import { useDispatch } from 'react-redux';
|
|
4
|
-
import {
|
|
11
|
+
import {
|
|
12
|
+
Button,
|
|
13
|
+
ExpandableSection,
|
|
14
|
+
Toolbar,
|
|
15
|
+
ToolbarContent,
|
|
16
|
+
ToolbarGroup,
|
|
17
|
+
ToolbarItem,
|
|
18
|
+
Tooltip,
|
|
19
|
+
} from '@patternfly/react-core';
|
|
20
|
+
import { ExpandableRowContent, Tbody, Td, Tr } from '@patternfly/react-table';
|
|
5
21
|
import { translate as __ } from 'foremanReact/common/I18n';
|
|
22
|
+
import { foremanUrl } from 'foremanReact/common/helpers';
|
|
6
23
|
import { Table } from 'foremanReact/components/PF4/TableIndexPage/Table/Table';
|
|
24
|
+
import SelectAllCheckbox from 'foremanReact/components/PF4/TableIndexPage/Table/SelectAllCheckbox';
|
|
25
|
+
import { useBulkSelect } from 'foremanReact/components/PF4/TableIndexPage/Table/TableHooks';
|
|
26
|
+
import { RowSelectTd } from 'foremanReact/components/PF4/TableIndexPage/RowSelectTd';
|
|
27
|
+
import { getColumnHelpers } from 'foremanReact/components/PF4/TableIndexPage/Table/helpers';
|
|
7
28
|
import { APIActions } from 'foremanReact/redux/API';
|
|
8
29
|
import { STATUS } from 'foremanReact/constants';
|
|
30
|
+
import {
|
|
31
|
+
entriesPage,
|
|
32
|
+
entryFixable,
|
|
33
|
+
} from '../PreupgradeReports/PreupgradeReportsHelpers';
|
|
34
|
+
import ReportDetails, { renderSeverityLabel } from './ReportDetails';
|
|
35
|
+
import './PreupgradeReportsTable.scss';
|
|
9
36
|
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
const
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
37
|
+
const LEAPP_TEMPLATE_NAME = 'Run preupgrade via Leapp';
|
|
38
|
+
|
|
39
|
+
const isRowFixable = entryFixable;
|
|
40
|
+
|
|
41
|
+
const submitJobInvocation = (
|
|
42
|
+
dispatch,
|
|
43
|
+
setError,
|
|
44
|
+
feature,
|
|
45
|
+
hostIds,
|
|
46
|
+
remediationIds
|
|
47
|
+
) => {
|
|
48
|
+
const payload = {
|
|
49
|
+
job_invocation: {
|
|
50
|
+
feature,
|
|
51
|
+
host_ids: hostIds,
|
|
52
|
+
...(remediationIds != null
|
|
53
|
+
? { inputs: { remediation_ids: remediationIds } }
|
|
54
|
+
: {}),
|
|
55
|
+
},
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
dispatch(
|
|
59
|
+
APIActions.post({
|
|
60
|
+
key: `CREATE_JOB_INVOCATION_${feature}`,
|
|
61
|
+
url: foremanUrl('/api/job_invocations'),
|
|
62
|
+
params: payload,
|
|
63
|
+
handleSuccess: response => {
|
|
64
|
+
const result = response.data || response;
|
|
65
|
+
if (result?.id) {
|
|
66
|
+
window.location.assign(foremanUrl(`/job_invocations/${result.id}`));
|
|
67
|
+
}
|
|
68
|
+
},
|
|
69
|
+
handleError: err => setError(err),
|
|
70
|
+
})
|
|
71
|
+
);
|
|
25
72
|
};
|
|
26
73
|
|
|
27
74
|
const PreupgradeReportsTable = ({ data = {} }) => {
|
|
28
75
|
const [error, setError] = useState(null);
|
|
29
|
-
const [
|
|
76
|
+
const [isReportExpanded, setIsReportExpanded] = useState(false);
|
|
30
77
|
const [pagination, setPagination] = useState({ page: 1, perPage: 5 });
|
|
31
78
|
const [reportData, setReportData] = useState(null);
|
|
32
79
|
const [status, setStatus] = useState(STATUS.RESOLVED);
|
|
80
|
+
const [expandedRowIds, setExpandedRowIds] = useState(new Set());
|
|
81
|
+
|
|
33
82
|
const dispatch = useDispatch();
|
|
34
83
|
// eslint-disable-next-line camelcase
|
|
35
|
-
const isLeappJob = data?.template_name?.includes(
|
|
84
|
+
const isLeappJob = data?.template_name?.includes(LEAPP_TEMPLATE_NAME);
|
|
36
85
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
86
|
+
// eslint-disable-next-line camelcase
|
|
87
|
+
const jobStatusLabel = data?.status_label;
|
|
88
|
+
|
|
89
|
+
const lastFetchedKeyRef = useRef(null);
|
|
90
|
+
|
|
91
|
+
const columns = useMemo(
|
|
92
|
+
() => ({
|
|
93
|
+
title: { title: __('Title') },
|
|
94
|
+
host: {
|
|
95
|
+
title: __('Host'),
|
|
96
|
+
wrapper: entry => entry.hostname || reportData?.hostname || '-',
|
|
97
|
+
},
|
|
98
|
+
risk_factor: {
|
|
99
|
+
title: __('Risk Factor'),
|
|
100
|
+
wrapper: ({ severity }) => renderSeverityLabel(severity),
|
|
101
|
+
},
|
|
102
|
+
has_remediation: {
|
|
103
|
+
title: __('Has Remediation?'),
|
|
104
|
+
wrapper: entry => (entry.detail?.remediations ? __('Yes') : __('No')),
|
|
105
|
+
},
|
|
106
|
+
inhibitor: {
|
|
107
|
+
title: __('Inhibitor?'),
|
|
108
|
+
wrapper: entry =>
|
|
109
|
+
entry.flags?.some(flag => flag === 'inhibitor') ? (
|
|
110
|
+
<Tooltip content={__('This issue inhibits the upgrade.')}>
|
|
111
|
+
<span>{__('Yes')}</span>
|
|
112
|
+
</Tooltip>
|
|
113
|
+
) : (
|
|
114
|
+
__('No')
|
|
115
|
+
),
|
|
116
|
+
},
|
|
117
|
+
}),
|
|
118
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
119
|
+
[reportData?.hostname]
|
|
120
|
+
);
|
|
67
121
|
|
|
68
122
|
useEffect(() => {
|
|
69
123
|
let isMounted = true;
|
|
70
|
-
|
|
124
|
+
const fetchKey = `${data.id}:${jobStatusLabel}`;
|
|
125
|
+
|
|
126
|
+
if (
|
|
127
|
+
!isLeappJob ||
|
|
128
|
+
!isReportExpanded ||
|
|
129
|
+
lastFetchedKeyRef.current === fetchKey
|
|
130
|
+
)
|
|
71
131
|
return undefined;
|
|
72
|
-
}
|
|
73
|
-
setStatus(STATUS.PENDING);
|
|
74
132
|
|
|
133
|
+
const fail = err => {
|
|
134
|
+
if (!isMounted) return;
|
|
135
|
+
setError(err);
|
|
136
|
+
setStatus(STATUS.ERROR);
|
|
137
|
+
};
|
|
138
|
+
|
|
139
|
+
const succeed = response => {
|
|
140
|
+
if (!isMounted) return;
|
|
141
|
+
lastFetchedKeyRef.current = fetchKey;
|
|
142
|
+
setReportData(response?.data || response || null);
|
|
143
|
+
setStatus(STATUS.RESOLVED);
|
|
144
|
+
};
|
|
145
|
+
|
|
146
|
+
setStatus(STATUS.PENDING);
|
|
75
147
|
dispatch(
|
|
76
148
|
APIActions.get({
|
|
77
149
|
key: `GET_LEAPP_REPORT_LIST_${data.id}`,
|
|
78
150
|
url: `/api/job_invocations/${data.id}/preupgrade_reports`,
|
|
79
151
|
handleSuccess: listResponse => {
|
|
80
152
|
if (!isMounted) return;
|
|
81
|
-
const
|
|
82
|
-
const summary = listPayload.results?.[0];
|
|
153
|
+
const summary = (listResponse.data || listResponse).results?.[0];
|
|
83
154
|
if (summary?.id) {
|
|
84
155
|
dispatch(
|
|
85
156
|
APIActions.get({
|
|
86
157
|
key: `GET_LEAPP_REPORT_DETAIL_${summary.id}`,
|
|
87
158
|
url: `/api/preupgrade_reports/${summary.id}`,
|
|
88
|
-
handleSuccess: detailResponse =>
|
|
89
|
-
|
|
90
|
-
const detailPayload = detailResponse.data || detailResponse;
|
|
91
|
-
setReportData(detailPayload);
|
|
92
|
-
setStatus(STATUS.RESOLVED);
|
|
93
|
-
}
|
|
94
|
-
},
|
|
95
|
-
handleError: err => {
|
|
96
|
-
if (isMounted) {
|
|
97
|
-
setError(err);
|
|
98
|
-
setStatus(STATUS.ERROR);
|
|
99
|
-
}
|
|
100
|
-
},
|
|
159
|
+
handleSuccess: detailResponse => succeed(detailResponse),
|
|
160
|
+
handleError: err => fail(err),
|
|
101
161
|
})
|
|
102
162
|
);
|
|
103
|
-
|
|
104
|
-
setReportData({});
|
|
105
|
-
setStatus(STATUS.RESOLVED);
|
|
106
|
-
}
|
|
107
|
-
},
|
|
108
|
-
handleError: err => {
|
|
109
|
-
if (isMounted) {
|
|
110
|
-
setError(err);
|
|
111
|
-
setStatus(STATUS.ERROR);
|
|
163
|
+
return;
|
|
112
164
|
}
|
|
165
|
+
succeed(null);
|
|
113
166
|
},
|
|
167
|
+
handleError: err => fail(err),
|
|
114
168
|
})
|
|
115
169
|
);
|
|
116
170
|
|
|
117
171
|
return () => {
|
|
118
172
|
isMounted = false;
|
|
119
173
|
};
|
|
120
|
-
}, [
|
|
174
|
+
}, [isReportExpanded, data.id, isLeappJob, dispatch, jobStatusLabel]);
|
|
121
175
|
|
|
122
176
|
// eslint-disable-next-line camelcase
|
|
123
|
-
const entries = reportData?.preupgrade_report_entries || []
|
|
124
|
-
|
|
177
|
+
const entries = useMemo(() => reportData?.preupgrade_report_entries || [], [
|
|
178
|
+
reportData,
|
|
179
|
+
]);
|
|
180
|
+
|
|
181
|
+
const pagedEntries = useMemo(
|
|
182
|
+
() => entriesPage(entries, pagination),
|
|
183
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
184
|
+
[entries, pagination.page, pagination.perPage]
|
|
185
|
+
);
|
|
186
|
+
|
|
187
|
+
const getHostId = useCallback(
|
|
188
|
+
entry =>
|
|
189
|
+
entry.host_id ||
|
|
190
|
+
entry.hostId ||
|
|
191
|
+
// eslint-disable-next-line camelcase
|
|
192
|
+
reportData?.host_id ||
|
|
193
|
+
reportData?.host?.id ||
|
|
194
|
+
// eslint-disable-next-line camelcase
|
|
195
|
+
data?.targeting?.host_id,
|
|
196
|
+
[reportData, data]
|
|
197
|
+
);
|
|
125
198
|
|
|
126
|
-
const handleParamsChange = newParams => {
|
|
199
|
+
const handleParamsChange = useCallback(newParams => {
|
|
127
200
|
setPagination(prev => ({
|
|
128
201
|
...prev,
|
|
129
202
|
page: newParams.page || prev.page,
|
|
130
203
|
perPage: newParams.per_page || prev.perPage,
|
|
131
204
|
}));
|
|
132
|
-
|
|
205
|
+
setExpandedRowIds(new Set());
|
|
206
|
+
}, []);
|
|
207
|
+
|
|
208
|
+
const toggleRowExpansion = useCallback((id, isExpanding) => {
|
|
209
|
+
setExpandedRowIds(prev => {
|
|
210
|
+
const next = new Set(prev);
|
|
211
|
+
if (isExpanding) next.add(id);
|
|
212
|
+
else next.delete(id);
|
|
213
|
+
return next;
|
|
214
|
+
});
|
|
215
|
+
}, []);
|
|
216
|
+
|
|
217
|
+
const { inclusionSet, exclusionSet, ...selectAllOptions } = useBulkSelect({
|
|
218
|
+
results: pagedEntries,
|
|
219
|
+
metadata: {
|
|
220
|
+
total: entries.length,
|
|
221
|
+
page: pagination.page,
|
|
222
|
+
selectable: entries.length,
|
|
223
|
+
},
|
|
224
|
+
initialSearchQuery: '',
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
const {
|
|
228
|
+
selectAll,
|
|
229
|
+
selectPage,
|
|
230
|
+
selectNone,
|
|
231
|
+
selectOne,
|
|
232
|
+
areAllRowsSelected,
|
|
233
|
+
isSelected,
|
|
234
|
+
} = selectAllOptions;
|
|
235
|
+
|
|
236
|
+
const rawSelectedIds =
|
|
237
|
+
areAllRowsSelected() || exclusionSet.size > 0
|
|
238
|
+
? entries.map(e => e.id).filter(id => !exclusionSet.has(id))
|
|
239
|
+
: Array.from(inclusionSet);
|
|
240
|
+
|
|
241
|
+
const validFixableIds = useMemo(
|
|
242
|
+
() => entries.filter(isRowFixable).map(e => e.id),
|
|
243
|
+
[entries]
|
|
244
|
+
);
|
|
245
|
+
|
|
246
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
247
|
+
const selectedIds = useMemo(
|
|
248
|
+
() => rawSelectedIds.filter(id => validFixableIds.includes(id)),
|
|
249
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
250
|
+
[rawSelectedIds.join(','), validFixableIds]
|
|
251
|
+
);
|
|
252
|
+
|
|
253
|
+
const pagedFixableEntries = useMemo(() => pagedEntries.filter(isRowFixable), [
|
|
254
|
+
pagedEntries,
|
|
255
|
+
]);
|
|
256
|
+
|
|
257
|
+
const areAllPageFixableSelected =
|
|
258
|
+
pagedFixableEntries.length > 0 &&
|
|
259
|
+
pagedFixableEntries.every(e => selectedIds.includes(e.id));
|
|
260
|
+
|
|
261
|
+
const areAllFixableSelected =
|
|
262
|
+
validFixableIds.length > 0 &&
|
|
263
|
+
validFixableIds.every(id => selectedIds.includes(id));
|
|
264
|
+
|
|
265
|
+
const areAllRowsExpanded =
|
|
266
|
+
pagedEntries.length > 0 &&
|
|
267
|
+
pagedEntries.every(entry => expandedRowIds.has(entry.id));
|
|
268
|
+
|
|
269
|
+
const onExpandAll = useCallback(() => {
|
|
270
|
+
setExpandedRowIds(
|
|
271
|
+
areAllRowsExpanded ? new Set() : new Set(pagedEntries.map(e => e.id))
|
|
272
|
+
);
|
|
273
|
+
}, [areAllRowsExpanded, pagedEntries]);
|
|
274
|
+
|
|
275
|
+
const [columnKeys, keysToColumnNames] = useMemo(
|
|
276
|
+
() => getColumnHelpers(columns),
|
|
277
|
+
[columns]
|
|
278
|
+
);
|
|
279
|
+
|
|
280
|
+
const hostIdsForSelected = useMemo(
|
|
281
|
+
() =>
|
|
282
|
+
Array.from(
|
|
283
|
+
new Set(
|
|
284
|
+
entries
|
|
285
|
+
.filter(e => selectedIds.includes(e.id))
|
|
286
|
+
.map(getHostId)
|
|
287
|
+
.filter(Boolean)
|
|
288
|
+
)
|
|
289
|
+
),
|
|
290
|
+
[entries, selectedIds, getHostId]
|
|
291
|
+
);
|
|
292
|
+
|
|
293
|
+
const allHostIds = useMemo(
|
|
294
|
+
() => Array.from(new Set(entries.map(getHostId).filter(Boolean))),
|
|
295
|
+
[entries, getHostId]
|
|
296
|
+
);
|
|
133
297
|
|
|
134
298
|
if (!isLeappJob) return null;
|
|
135
299
|
|
|
300
|
+
const isFixSelectedDisabled =
|
|
301
|
+
validFixableIds.length === 0 ||
|
|
302
|
+
selectedIds.length === 0 ||
|
|
303
|
+
hostIdsForSelected.length === 0;
|
|
304
|
+
|
|
136
305
|
return (
|
|
137
306
|
<ExpandableSection
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
onToggle={(_event, val) => setIsExpanded(val)}
|
|
307
|
+
isExpanded={isReportExpanded}
|
|
308
|
+
onToggle={(_event, val) => setIsReportExpanded(val)}
|
|
141
309
|
toggleText={__('Leapp preupgrade report')}
|
|
142
310
|
>
|
|
311
|
+
{entries.length > 0 && status === STATUS.RESOLVED && (
|
|
312
|
+
<Toolbar ouiaId="leapp-report-toolbar">
|
|
313
|
+
<ToolbarContent>
|
|
314
|
+
<ToolbarGroup variant="filter-group">
|
|
315
|
+
<ToolbarItem>
|
|
316
|
+
<SelectAllCheckbox
|
|
317
|
+
selectAll={selectAll}
|
|
318
|
+
selectPage={selectPage}
|
|
319
|
+
selectNone={selectNone}
|
|
320
|
+
selectedCount={selectedIds.length}
|
|
321
|
+
pageRowCount={pagedFixableEntries.length}
|
|
322
|
+
totalCount={validFixableIds.length}
|
|
323
|
+
areAllRowsOnPageSelected={areAllPageFixableSelected}
|
|
324
|
+
areAllRowsSelected={areAllFixableSelected}
|
|
325
|
+
/>
|
|
326
|
+
</ToolbarItem>
|
|
327
|
+
</ToolbarGroup>
|
|
328
|
+
<ToolbarGroup
|
|
329
|
+
align={{ default: 'alignRight' }}
|
|
330
|
+
variant="button-group"
|
|
331
|
+
>
|
|
332
|
+
<ToolbarItem>
|
|
333
|
+
<Button
|
|
334
|
+
variant="secondary"
|
|
335
|
+
isDisabled={isFixSelectedDisabled}
|
|
336
|
+
onClick={() =>
|
|
337
|
+
submitJobInvocation(
|
|
338
|
+
dispatch,
|
|
339
|
+
setError,
|
|
340
|
+
'leapp_remediation_plan',
|
|
341
|
+
hostIdsForSelected,
|
|
342
|
+
selectedIds.join(',')
|
|
343
|
+
)
|
|
344
|
+
}
|
|
345
|
+
ouiaId="fix-selected-button"
|
|
346
|
+
>
|
|
347
|
+
{__('Fix Selected')}
|
|
348
|
+
</Button>
|
|
349
|
+
</ToolbarItem>
|
|
350
|
+
<ToolbarItem>
|
|
351
|
+
<Button
|
|
352
|
+
variant="primary"
|
|
353
|
+
isDisabled={allHostIds.length === 0}
|
|
354
|
+
onClick={() =>
|
|
355
|
+
submitJobInvocation(
|
|
356
|
+
dispatch,
|
|
357
|
+
setError,
|
|
358
|
+
'leapp_upgrade',
|
|
359
|
+
allHostIds
|
|
360
|
+
)
|
|
361
|
+
}
|
|
362
|
+
ouiaId="run-upgrade-button"
|
|
363
|
+
>
|
|
364
|
+
{__('Run Upgrade')}
|
|
365
|
+
</Button>
|
|
366
|
+
</ToolbarItem>
|
|
367
|
+
</ToolbarGroup>
|
|
368
|
+
</ToolbarContent>
|
|
369
|
+
</Toolbar>
|
|
370
|
+
)}
|
|
371
|
+
|
|
143
372
|
<Table
|
|
144
373
|
ouiaId="leapp-report-table"
|
|
145
374
|
columns={columns}
|
|
@@ -156,20 +385,75 @@ const PreupgradeReportsTable = ({ data = {} }) => {
|
|
|
156
385
|
errorMessage={
|
|
157
386
|
status === STATUS.ERROR && error?.message ? error.message : null
|
|
158
387
|
}
|
|
159
|
-
showCheckboxes
|
|
388
|
+
showCheckboxes
|
|
160
389
|
refreshData={() => {}}
|
|
161
390
|
isDeleteable={false}
|
|
162
391
|
emptyMessage={__('The preupgrade report shows no issues.')}
|
|
163
392
|
setParams={handleParamsChange}
|
|
164
|
-
|
|
393
|
+
childrenOutsideTbody
|
|
394
|
+
onExpandAll={onExpandAll}
|
|
395
|
+
areAllRowsExpanded={!areAllRowsExpanded}
|
|
396
|
+
>
|
|
397
|
+
{pagedEntries.map((entry, rowIndex) => {
|
|
398
|
+
const isRowExpanded = expandedRowIds.has(entry.id);
|
|
399
|
+
|
|
400
|
+
return (
|
|
401
|
+
<Tbody
|
|
402
|
+
key={entry.id}
|
|
403
|
+
isExpanded={isRowExpanded}
|
|
404
|
+
className={isRowExpanded ? 'leapp-expanded-tbody' : ''}
|
|
405
|
+
>
|
|
406
|
+
<Tr ouiaId={`table-row-${rowIndex}`}>
|
|
407
|
+
<Td
|
|
408
|
+
expand={{
|
|
409
|
+
rowIndex,
|
|
410
|
+
isExpanded: isRowExpanded,
|
|
411
|
+
onToggle: (_event, _rowIndex, isOpen) =>
|
|
412
|
+
toggleRowExpansion(entry.id, isOpen),
|
|
413
|
+
}}
|
|
414
|
+
/>
|
|
415
|
+
<RowSelectTd
|
|
416
|
+
rowData={entry}
|
|
417
|
+
selectOne={selectOne}
|
|
418
|
+
isSelected={id => isRowFixable(entry) && isSelected(id)}
|
|
419
|
+
isSelectable={isRowFixable}
|
|
420
|
+
/>
|
|
421
|
+
{columnKeys.map(key => (
|
|
422
|
+
<Td key={key} dataLabel={keysToColumnNames[key]}>
|
|
423
|
+
{columns[key].wrapper
|
|
424
|
+
? columns[key].wrapper(entry)
|
|
425
|
+
: entry[key]}
|
|
426
|
+
</Td>
|
|
427
|
+
))}
|
|
428
|
+
</Tr>
|
|
429
|
+
<Tr
|
|
430
|
+
isExpanded={isRowExpanded}
|
|
431
|
+
ouiaId={`table-row-details-${rowIndex}`}
|
|
432
|
+
>
|
|
433
|
+
<Td colSpan={columnKeys.length + 2}>
|
|
434
|
+
<ExpandableRowContent>
|
|
435
|
+
{isRowExpanded && <ReportDetails entry={entry} />}
|
|
436
|
+
</ExpandableRowContent>
|
|
437
|
+
</Td>
|
|
438
|
+
</Tr>
|
|
439
|
+
</Tbody>
|
|
440
|
+
);
|
|
441
|
+
})}
|
|
442
|
+
</Table>
|
|
165
443
|
</ExpandableSection>
|
|
166
444
|
);
|
|
167
445
|
};
|
|
168
446
|
|
|
169
447
|
PreupgradeReportsTable.propTypes = {
|
|
170
448
|
data: PropTypes.shape({
|
|
171
|
-
id: PropTypes.
|
|
449
|
+
id: PropTypes.number,
|
|
450
|
+
// eslint-disable-next-line camelcase
|
|
172
451
|
template_name: PropTypes.string,
|
|
452
|
+
// eslint-disable-next-line camelcase
|
|
453
|
+
status_label: PropTypes.string,
|
|
454
|
+
targeting: PropTypes.shape({
|
|
455
|
+
host_id: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
|
|
456
|
+
}),
|
|
173
457
|
}),
|
|
174
458
|
};
|
|
175
459
|
|
metadata
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: foreman_leapp
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 3.
|
|
4
|
+
version: 3.4.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Foreman Leapp team
|
|
8
8
|
bindir: bin
|
|
9
9
|
cert_chain: []
|
|
10
|
-
date: 2026-
|
|
10
|
+
date: 2026-05-15 00:00:00.000000000 Z
|
|
11
11
|
dependencies:
|
|
12
12
|
- !ruby/object:Gem::Dependency
|
|
13
13
|
name: foreman_remote_execution
|
|
@@ -37,20 +37,6 @@ dependencies:
|
|
|
37
37
|
- - ">="
|
|
38
38
|
- !ruby/object:Gem::Version
|
|
39
39
|
version: '5.0'
|
|
40
|
-
- !ruby/object:Gem::Dependency
|
|
41
|
-
name: rdoc
|
|
42
|
-
requirement: !ruby/object:Gem::Requirement
|
|
43
|
-
requirements:
|
|
44
|
-
- - "~>"
|
|
45
|
-
- !ruby/object:Gem::Version
|
|
46
|
-
version: '6.2'
|
|
47
|
-
type: :development
|
|
48
|
-
prerelease: false
|
|
49
|
-
version_requirements: !ruby/object:Gem::Requirement
|
|
50
|
-
requirements:
|
|
51
|
-
- - "~>"
|
|
52
|
-
- !ruby/object:Gem::Version
|
|
53
|
-
version: '6.2'
|
|
54
40
|
description: A Foreman plugin to support inplace RHEL upgrades with Leapp utility.
|
|
55
41
|
email:
|
|
56
42
|
- foreman-dev@googlegroups.com
|
|
@@ -186,6 +172,8 @@ files:
|
|
|
186
172
|
- webpack/components/PreupgradeReportsList/components/images/i_severity-low.svg
|
|
187
173
|
- webpack/components/PreupgradeReportsList/components/images/i_severity-med.svg
|
|
188
174
|
- webpack/components/PreupgradeReportsList/index.js
|
|
175
|
+
- webpack/components/PreupgradeReportsTable/PreupgradeReportsTable.scss
|
|
176
|
+
- webpack/components/PreupgradeReportsTable/ReportDetails.js
|
|
189
177
|
- webpack/components/PreupgradeReportsTable/__tests__/PreupgradeReportsTable.test.js
|
|
190
178
|
- webpack/components/PreupgradeReportsTable/index.js
|
|
191
179
|
- webpack/consts.js
|
|
@@ -212,7 +200,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
212
200
|
- !ruby/object:Gem::Version
|
|
213
201
|
version: '0'
|
|
214
202
|
requirements: []
|
|
215
|
-
rubygems_version: 4.0.
|
|
203
|
+
rubygems_version: 4.0.10
|
|
216
204
|
specification_version: 4
|
|
217
205
|
summary: A Foreman plugin for Leapp utility.
|
|
218
206
|
test_files:
|