foreman_remote_execution 14.1.4 → 15.0.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/app/controllers/api/v2/job_invocations_controller.rb +8 -0
- data/app/controllers/template_invocations_controller.rb +57 -0
- data/app/controllers/ui_job_wizard_controller.rb +6 -3
- data/app/helpers/remote_execution_helper.rb +5 -6
- data/app/views/api/v2/job_invocations/base.json.rabl +1 -1
- data/app/views/api/v2/job_invocations/hosts.json.rabl +1 -1
- data/app/views/api/v2/job_invocations/main.json.rabl +1 -1
- data/app/views/api/v2/job_invocations/show.json.rabl +18 -0
- data/app/views/templates/script/convert2rhel_analyze.erb +4 -4
- data/config/routes.rb +2 -0
- data/lib/foreman_remote_execution/engine.rb +3 -3
- data/lib/foreman_remote_execution/version.rb +1 -1
- data/webpack/JobInvocationDetail/JobAdditionInfo.js +214 -0
- data/webpack/JobInvocationDetail/JobInvocationConstants.js +40 -2
- data/webpack/JobInvocationDetail/JobInvocationDetail.scss +70 -0
- data/webpack/JobInvocationDetail/JobInvocationHostTable.js +177 -80
- data/webpack/JobInvocationDetail/JobInvocationHostTableToolbar.js +63 -0
- data/webpack/JobInvocationDetail/JobInvocationSelectors.js +8 -1
- data/webpack/JobInvocationDetail/JobInvocationSystemStatusChart.js +61 -10
- data/webpack/JobInvocationDetail/OpenAlInvocations.js +111 -0
- data/webpack/JobInvocationDetail/TemplateInvocation.js +202 -0
- data/webpack/JobInvocationDetail/TemplateInvocationComponents/OutputCodeBlock.js +124 -0
- data/webpack/JobInvocationDetail/TemplateInvocationComponents/OutputToggleGroup.js +156 -0
- data/webpack/JobInvocationDetail/TemplateInvocationComponents/PreviewTemplate.js +50 -0
- data/webpack/JobInvocationDetail/TemplateInvocationComponents/TemplateActionButtons.js +224 -0
- data/webpack/JobInvocationDetail/TemplateInvocationPage.js +53 -0
- data/webpack/JobInvocationDetail/__tests__/MainInformation.test.js +1 -1
- data/webpack/JobInvocationDetail/__tests__/OpenAlInvocations.test.js +110 -0
- data/webpack/JobInvocationDetail/__tests__/OutputCodeBlock.test.js +69 -0
- data/webpack/JobInvocationDetail/__tests__/TemplateInvocation.test.js +131 -0
- data/webpack/JobInvocationDetail/__tests__/fixtures.js +130 -0
- data/webpack/JobInvocationDetail/index.js +18 -3
- data/webpack/JobWizard/JobWizard.js +38 -16
- data/webpack/JobWizard/{StartsBeforeErrorAlert.js → StartsErrorAlert.js} +16 -1
- data/webpack/JobWizard/steps/AdvancedFields/__tests__/AdvancedFields.test.js +1 -1
- data/webpack/JobWizard/steps/Schedule/ScheduleFuture.js +1 -1
- data/webpack/JobWizard/steps/Schedule/ScheduleRecurring.js +5 -3
- data/webpack/JobWizard/steps/Schedule/ScheduleType.js +1 -1
- data/webpack/JobWizard/steps/Schedule/__tests__/Schedule.test.js +3 -3
- data/webpack/JobWizard/steps/form/DateTimePicker.js +13 -0
- data/webpack/JobWizard/steps/form/Formatter.js +1 -0
- data/webpack/JobWizard/steps/form/ResourceSelect.js +34 -9
- data/webpack/Routes/routes.js +6 -0
- data/webpack/__mocks__/foremanReact/Root/Context/ForemanContext/index.js +1 -0
- data/webpack/react_app/components/RegistrationExtension/RexPull.js +27 -2
- data/webpack/react_app/components/TargetingHosts/components/HostStatus.js +1 -1
- metadata +15 -3
@@ -1,10 +1,10 @@
|
|
1
1
|
/* eslint-disable camelcase */
|
2
2
|
import PropTypes from 'prop-types';
|
3
|
-
import React, { useMemo, useEffect } from 'react';
|
3
|
+
import React, { useMemo, useEffect, useState } from 'react';
|
4
4
|
import { Icon } from 'patternfly-react';
|
5
5
|
import { translate as __ } from 'foremanReact/common/I18n';
|
6
6
|
import { FormattedMessage } from 'react-intl';
|
7
|
-
import { Tr, Td } from '@patternfly/react-table';
|
7
|
+
import { Tr, Td, Tbody, ExpandableRowContent } from '@patternfly/react-table';
|
8
8
|
import {
|
9
9
|
Title,
|
10
10
|
EmptyState,
|
@@ -20,57 +20,72 @@ import {
|
|
20
20
|
useBulkSelect,
|
21
21
|
useUrlParams,
|
22
22
|
} from 'foremanReact/components/PF4/TableIndexPage/Table/TableHooks';
|
23
|
-
import Pagination from 'foremanReact/components/Pagination';
|
24
23
|
import { getControllerSearchProps } from 'foremanReact/constants';
|
25
24
|
import Columns, {
|
26
25
|
JOB_INVOCATION_HOSTS,
|
27
26
|
STATUS_UPPERCASE,
|
28
27
|
} from './JobInvocationConstants';
|
28
|
+
import { TemplateInvocation } from './TemplateInvocation';
|
29
|
+
import { OpenAlInvocations, PopupAlert } from './OpenAlInvocations';
|
30
|
+
import { RowActions } from './TemplateInvocationComponents/TemplateActionButtons';
|
31
|
+
import JobInvocationHostTableToolbar from './JobInvocationHostTableToolbar';
|
29
32
|
|
30
|
-
const JobInvocationHostTable = ({
|
33
|
+
const JobInvocationHostTable = ({
|
34
|
+
id,
|
35
|
+
targeting,
|
36
|
+
finished,
|
37
|
+
autoRefresh,
|
38
|
+
initialFilter,
|
39
|
+
}) => {
|
31
40
|
const columns = Columns();
|
32
41
|
const columnNamesKeys = Object.keys(columns);
|
33
42
|
const apiOptions = { key: JOB_INVOCATION_HOSTS };
|
43
|
+
const [selectedFilter, setSelectedFilter] = useState(initialFilter || '');
|
34
44
|
const {
|
35
45
|
searchParam: urlSearchQuery = '',
|
36
46
|
page: urlPage,
|
37
47
|
per_page: urlPerPage,
|
38
48
|
} = useUrlParams();
|
39
|
-
const
|
49
|
+
const constructFilter = (
|
50
|
+
filter = selectedFilter,
|
51
|
+
search = urlSearchQuery
|
52
|
+
) => {
|
53
|
+
const dropdownFilterClause =
|
54
|
+
filter && filter !== 'all_statuses'
|
55
|
+
? `job_invocation.result = ${filter}`
|
56
|
+
: null;
|
57
|
+
const parts = [dropdownFilterClause, search];
|
58
|
+
return parts
|
59
|
+
.filter(x => x)
|
60
|
+
.map(fragment => `(${fragment})`)
|
61
|
+
.join(' AND ');
|
62
|
+
};
|
63
|
+
|
64
|
+
const search = constructFilter();
|
65
|
+
const defaultParams = search !== '' ? { search } : {};
|
40
66
|
if (urlPage) defaultParams.page = Number(urlPage);
|
41
67
|
if (urlPerPage) defaultParams.per_page = Number(urlPerPage);
|
68
|
+
const [expandedHost, setExpandedHost] = useState([]);
|
42
69
|
const { response, status, setAPIOptions } = useAPI(
|
43
70
|
'get',
|
44
71
|
`/api/job_invocations/${id}/hosts`,
|
45
72
|
{
|
46
|
-
params:
|
73
|
+
params: defaultParams,
|
47
74
|
}
|
48
75
|
);
|
49
76
|
|
50
|
-
const
|
51
|
-
response: {
|
52
|
-
search: urlSearchQuery,
|
53
|
-
can_create: false,
|
54
|
-
results: response?.results || [],
|
55
|
-
total: response?.total || 0,
|
56
|
-
per_page: response?.perPage,
|
57
|
-
page: response?.page,
|
58
|
-
subtotal: response?.subtotal || 0,
|
59
|
-
message: response?.message || 'error',
|
60
|
-
},
|
61
|
-
status,
|
62
|
-
setAPIOptions,
|
63
|
-
};
|
64
|
-
|
65
|
-
const { setParamsAndAPI, params } = useSetParamsAndApiAndSearch({
|
77
|
+
const { params } = useSetParamsAndApiAndSearch({
|
66
78
|
defaultParams,
|
67
79
|
apiOptions,
|
68
|
-
setAPIOptions
|
80
|
+
setAPIOptions,
|
69
81
|
});
|
70
82
|
|
71
|
-
const { updateSearchQuery } = useBulkSelect({
|
83
|
+
const { updateSearchQuery: updateSearchQueryBulk } = useBulkSelect({
|
72
84
|
initialSearchQuery: urlSearchQuery,
|
73
85
|
});
|
86
|
+
const updateSearchQuery = searchQuery => {
|
87
|
+
updateSearchQueryBulk(searchQuery);
|
88
|
+
};
|
74
89
|
|
75
90
|
const controller = 'hosts';
|
76
91
|
const memoDefaultSearchProps = useMemo(
|
@@ -81,6 +96,23 @@ const JobInvocationHostTable = ({ id, targeting, finished, autoRefresh }) => {
|
|
81
96
|
`/${controller}/auto_complete_search`
|
82
97
|
);
|
83
98
|
|
99
|
+
const wrapSetSelectedFilter = filter => {
|
100
|
+
const filterSearch = constructFilter(filter);
|
101
|
+
setAPIOptions(prevOptions => {
|
102
|
+
if (prevOptions.params.search !== filterSearch) {
|
103
|
+
return {
|
104
|
+
...prevOptions,
|
105
|
+
params: {
|
106
|
+
...prevOptions.params,
|
107
|
+
search: filterSearch,
|
108
|
+
},
|
109
|
+
};
|
110
|
+
}
|
111
|
+
return prevOptions;
|
112
|
+
});
|
113
|
+
setSelectedFilter(filter);
|
114
|
+
};
|
115
|
+
|
84
116
|
useEffect(() => {
|
85
117
|
const intervalId = setInterval(() => {
|
86
118
|
if (!finished || autoRefresh) {
|
@@ -98,24 +130,31 @@ const JobInvocationHostTable = ({ id, targeting, finished, autoRefresh }) => {
|
|
98
130
|
};
|
99
131
|
}, [finished, autoRefresh, setAPIOptions]);
|
100
132
|
|
101
|
-
const
|
102
|
-
|
103
|
-
...
|
104
|
-
|
105
|
-
|
106
|
-
|
133
|
+
const wrapSetAPIOptions = newAPIOptions => {
|
134
|
+
setAPIOptions(prevOptions => ({
|
135
|
+
...prevOptions,
|
136
|
+
params: {
|
137
|
+
...prevOptions.params,
|
138
|
+
...newAPIOptions.params,
|
139
|
+
search: constructFilter(undefined, newAPIOptions?.params?.search),
|
140
|
+
},
|
141
|
+
}));
|
107
142
|
};
|
108
143
|
|
109
|
-
const
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
|
118
|
-
|
144
|
+
const combinedResponse = {
|
145
|
+
response: {
|
146
|
+
search: urlSearchQuery,
|
147
|
+
can_create: false,
|
148
|
+
results: response?.results || [],
|
149
|
+
total: response?.total || 0,
|
150
|
+
per_page: response?.perPage,
|
151
|
+
page: response?.page,
|
152
|
+
subtotal: response?.subtotal || 0,
|
153
|
+
message: response?.message || 'error',
|
154
|
+
},
|
155
|
+
status,
|
156
|
+
setAPIOptions: wrapSetAPIOptions,
|
157
|
+
};
|
119
158
|
|
120
159
|
const customEmptyState = (
|
121
160
|
<Tr ouiaId="table-empty">
|
@@ -153,48 +192,105 @@ const JobInvocationHostTable = ({ id, targeting, finished, autoRefresh }) => {
|
|
153
192
|
</Tr>
|
154
193
|
);
|
155
194
|
|
195
|
+
const { results = [] } = response;
|
196
|
+
|
197
|
+
const isHostExpanded = host => expandedHost.includes(host);
|
198
|
+
const setHostExpanded = (host, isExpanding = true) =>
|
199
|
+
setExpandedHost(prevExpanded => {
|
200
|
+
const otherExpandedHosts = prevExpanded.filter(h => h !== host);
|
201
|
+
return isExpanding ? [...otherExpandedHosts, host] : otherExpandedHosts;
|
202
|
+
});
|
203
|
+
const [showAlert, setShowAlert] = useState(false);
|
156
204
|
return (
|
157
|
-
|
158
|
-
|
159
|
-
|
160
|
-
|
161
|
-
|
162
|
-
|
163
|
-
|
164
|
-
|
165
|
-
|
166
|
-
|
167
|
-
|
168
|
-
|
169
|
-
|
170
|
-
|
171
|
-
|
172
|
-
|
173
|
-
|
174
|
-
|
175
|
-
|
176
|
-
|
177
|
-
|
178
|
-
url=""
|
179
|
-
refreshData={() => {}}
|
180
|
-
errorMessage={
|
181
|
-
status === STATUS_UPPERCASE.ERROR && response?.message
|
182
|
-
? response.message
|
183
|
-
: null
|
184
|
-
}
|
185
|
-
isPending={status === STATUS_UPPERCASE.PENDING}
|
186
|
-
isDeleteable={false}
|
187
|
-
bottomPagination={bottomPagination}
|
205
|
+
<>
|
206
|
+
{showAlert && <PopupAlert setShowAlert={setShowAlert} />}
|
207
|
+
<TableIndexPage
|
208
|
+
apiUrl=""
|
209
|
+
apiOptions={apiOptions}
|
210
|
+
customSearchProps={memoDefaultSearchProps}
|
211
|
+
controller="hosts"
|
212
|
+
creatable={false}
|
213
|
+
replacementResponse={combinedResponse}
|
214
|
+
updateSearchQuery={updateSearchQuery}
|
215
|
+
customToolbarItems={[
|
216
|
+
<OpenAlInvocations
|
217
|
+
setShowAlert={setShowAlert}
|
218
|
+
results={results}
|
219
|
+
id={id}
|
220
|
+
/>,
|
221
|
+
<JobInvocationHostTableToolbar
|
222
|
+
dropdownFilter={selectedFilter}
|
223
|
+
setDropdownFilter={wrapSetSelectedFilter}
|
224
|
+
/>,
|
225
|
+
]}
|
188
226
|
>
|
189
|
-
|
190
|
-
|
191
|
-
|
192
|
-
|
193
|
-
|
194
|
-
|
195
|
-
|
196
|
-
|
197
|
-
|
227
|
+
<Table
|
228
|
+
ouiaId="job-invocation-hosts-table"
|
229
|
+
columns={columns}
|
230
|
+
customEmptyState={
|
231
|
+
status === STATUS_UPPERCASE.RESOLVED && !response?.results?.length
|
232
|
+
? customEmptyState
|
233
|
+
: null
|
234
|
+
}
|
235
|
+
params={params}
|
236
|
+
setParams={wrapSetAPIOptions}
|
237
|
+
itemCount={response?.subtotal}
|
238
|
+
results={response?.results}
|
239
|
+
url=""
|
240
|
+
refreshData={() => {}}
|
241
|
+
errorMessage={
|
242
|
+
status === STATUS_UPPERCASE.ERROR && response?.message
|
243
|
+
? response.message
|
244
|
+
: null
|
245
|
+
}
|
246
|
+
isPending={status === STATUS_UPPERCASE.PENDING}
|
247
|
+
isDeleteable={false}
|
248
|
+
childrenOutsideTbody
|
249
|
+
>
|
250
|
+
{results?.map((result, rowIndex) => (
|
251
|
+
<Tbody key={rowIndex}>
|
252
|
+
<Tr ouiaId={`table-row-${rowIndex}`}>
|
253
|
+
<Td
|
254
|
+
expand={{
|
255
|
+
rowIndex,
|
256
|
+
isExpanded: isHostExpanded(result.id),
|
257
|
+
onToggle: () =>
|
258
|
+
setHostExpanded(result.id, !isHostExpanded(result.id)),
|
259
|
+
expandId: 'host-expandable',
|
260
|
+
}}
|
261
|
+
/>
|
262
|
+
{columnNamesKeys.slice(1).map(k => (
|
263
|
+
<Td key={k}>{columns[k].wrapper(result)}</Td>
|
264
|
+
))}
|
265
|
+
<Td isActionCell>
|
266
|
+
<RowActions hostID={result.id} jobID={id} />
|
267
|
+
</Td>
|
268
|
+
</Tr>
|
269
|
+
<Tr
|
270
|
+
isExpanded={isHostExpanded(result.id)}
|
271
|
+
ouiaId="table-row-expanded-sections"
|
272
|
+
>
|
273
|
+
<Td
|
274
|
+
dataLabel={`${result.id}-expandable-content`}
|
275
|
+
colSpan={columnNamesKeys.length + 1}
|
276
|
+
>
|
277
|
+
<ExpandableRowContent>
|
278
|
+
{result.job_status === 'cancelled' ||
|
279
|
+
result.job_status === 'N/A' ? (
|
280
|
+
<div>
|
281
|
+
{__('A task for this host has not been started')}
|
282
|
+
</div>
|
283
|
+
) : (
|
284
|
+
<TemplateInvocation hostID={result.id} jobID={id} />
|
285
|
+
)}
|
286
|
+
</ExpandableRowContent>
|
287
|
+
</Td>
|
288
|
+
</Tr>
|
289
|
+
</Tbody>
|
290
|
+
))}
|
291
|
+
</Table>
|
292
|
+
</TableIndexPage>
|
293
|
+
</>
|
198
294
|
);
|
199
295
|
};
|
200
296
|
|
@@ -203,6 +299,7 @@ JobInvocationHostTable.propTypes = {
|
|
203
299
|
targeting: PropTypes.object.isRequired,
|
204
300
|
finished: PropTypes.bool.isRequired,
|
205
301
|
autoRefresh: PropTypes.bool.isRequired,
|
302
|
+
initialFilter: PropTypes.string.isRequired,
|
206
303
|
};
|
207
304
|
|
208
305
|
JobInvocationHostTable.defaultProps = {};
|
@@ -0,0 +1,63 @@
|
|
1
|
+
import React from 'react';
|
2
|
+
import PropTypes from 'prop-types';
|
3
|
+
import { translate as __ } from 'foremanReact/common/I18n';
|
4
|
+
import { Select, SelectOption, SelectList } from '@patternfly/react-core/next'; // remove "/next" after switching to PF5
|
5
|
+
import { MenuToggle, ToolbarItem } from '@patternfly/react-core';
|
6
|
+
import { STATUS_TITLES } from './JobInvocationConstants';
|
7
|
+
|
8
|
+
const JobInvocationHostTableToolbar = ({
|
9
|
+
dropdownFilter,
|
10
|
+
setDropdownFilter,
|
11
|
+
}) => {
|
12
|
+
const [isOpen, setIsOpen] = React.useState(false);
|
13
|
+
const onSelect = (_event, itemId) => {
|
14
|
+
setDropdownFilter(itemId);
|
15
|
+
setIsOpen(false);
|
16
|
+
};
|
17
|
+
|
18
|
+
const toggle = toggleRef => (
|
19
|
+
<MenuToggle
|
20
|
+
ref={toggleRef}
|
21
|
+
onClick={() => setIsOpen(!isOpen)}
|
22
|
+
isExpanded={isOpen}
|
23
|
+
style={{
|
24
|
+
width: '200px',
|
25
|
+
}}
|
26
|
+
>
|
27
|
+
{Object.values(STATUS_TITLES).find(status => status.id === dropdownFilter)
|
28
|
+
?.title || __('All statuses')}
|
29
|
+
</MenuToggle>
|
30
|
+
);
|
31
|
+
|
32
|
+
return (
|
33
|
+
<ToolbarItem>
|
34
|
+
<Select
|
35
|
+
isOpen={isOpen}
|
36
|
+
selected={dropdownFilter}
|
37
|
+
onSelect={onSelect}
|
38
|
+
onOpenChange={newIsOpen => setIsOpen(newIsOpen)}
|
39
|
+
ouiaId="host-status-select"
|
40
|
+
toggle={toggle}
|
41
|
+
>
|
42
|
+
<SelectList>
|
43
|
+
{Object.values(STATUS_TITLES).map(result => (
|
44
|
+
<SelectOption
|
45
|
+
key={result.id}
|
46
|
+
itemId={result.id}
|
47
|
+
isSelected={result.id === dropdownFilter}
|
48
|
+
>
|
49
|
+
{result.title}
|
50
|
+
</SelectOption>
|
51
|
+
))}
|
52
|
+
</SelectList>
|
53
|
+
</Select>
|
54
|
+
</ToolbarItem>
|
55
|
+
);
|
56
|
+
};
|
57
|
+
|
58
|
+
JobInvocationHostTableToolbar.propTypes = {
|
59
|
+
dropdownFilter: PropTypes.string.isRequired,
|
60
|
+
setDropdownFilter: PropTypes.func.isRequired,
|
61
|
+
};
|
62
|
+
|
63
|
+
export default JobInvocationHostTableToolbar;
|
@@ -1,5 +1,9 @@
|
|
1
1
|
import { selectAPIResponse } from 'foremanReact/redux/API/APISelectors';
|
2
|
-
import {
|
2
|
+
import {
|
3
|
+
JOB_INVOCATION_KEY,
|
4
|
+
GET_TASK,
|
5
|
+
GET_TEMPLATE_INVOCATION,
|
6
|
+
} from './JobInvocationConstants';
|
3
7
|
|
4
8
|
export const selectItems = state =>
|
5
9
|
selectAPIResponse(state, JOB_INVOCATION_KEY);
|
@@ -8,3 +12,6 @@ export const selectTask = state => selectAPIResponse(state, GET_TASK);
|
|
8
12
|
|
9
13
|
export const selectTaskCancelable = state =>
|
10
14
|
selectTask(state).available_actions?.cancellable || false;
|
15
|
+
|
16
|
+
export const selectTemplateInvocation = state =>
|
17
|
+
selectAPIResponse(state, GET_TEMPLATE_INVOCATION);
|
@@ -17,18 +17,20 @@ import {
|
|
17
17
|
Text,
|
18
18
|
} from '@patternfly/react-core';
|
19
19
|
import {
|
20
|
-
global_palette_black_600 as
|
20
|
+
global_palette_black_600 as cancelledColor,
|
21
21
|
global_palette_black_500 as emptyChartDonut,
|
22
22
|
global_palette_red_100 as failedColor,
|
23
23
|
global_palette_blue_300 as inProgressColor,
|
24
24
|
global_palette_green_500 as successedColor,
|
25
25
|
} from '@patternfly/react-tokens';
|
26
|
+
import { STATUS_TITLES } from './JobInvocationConstants';
|
26
27
|
import './JobInvocationDetail.scss';
|
27
28
|
|
28
29
|
const JobInvocationSystemStatusChart = ({
|
29
30
|
data,
|
30
31
|
isAlreadyStarted,
|
31
32
|
formattedStartDate,
|
33
|
+
onFilterChange,
|
32
34
|
}) => {
|
33
35
|
const {
|
34
36
|
succeeded,
|
@@ -42,7 +44,7 @@ const JobInvocationSystemStatusChart = ({
|
|
42
44
|
{ title: __('Succeeded:'), count: succeeded, color: successedColor.value },
|
43
45
|
{ title: __('Failed:'), count: failed, color: failedColor.value },
|
44
46
|
{ title: __('In Progress:'), count: pending, color: inProgressColor.value },
|
45
|
-
{ title: __('
|
47
|
+
{ title: __('Cancelled:'), count: cancelled, color: cancelledColor.value },
|
46
48
|
];
|
47
49
|
const chartDonutTitle = () => {
|
48
50
|
if (total > 0) return `${succeeded.toString()}/${total}`;
|
@@ -51,6 +53,7 @@ const JobInvocationSystemStatusChart = ({
|
|
51
53
|
};
|
52
54
|
const chartSize = 105;
|
53
55
|
const [legendWidth, setLegendWidth] = useState(270);
|
56
|
+
const [cursor, setCursor] = useState('default');
|
54
57
|
|
55
58
|
// Calculates chart legend width based on its content
|
56
59
|
useEffect(() => {
|
@@ -64,9 +67,19 @@ const JobInvocationSystemStatusChart = ({
|
|
64
67
|
}
|
65
68
|
}, [isAlreadyStarted, data]);
|
66
69
|
|
70
|
+
const onChartClick = (_evt, { index }) => {
|
71
|
+
const statusKeys = Object.keys(STATUS_TITLES);
|
72
|
+
const selectedKey = statusKeys[index + 1]; // first status is ALL_STATUSES
|
73
|
+
const selectedFilter = selectedKey ? STATUS_TITLES[selectedKey]?.id : null;
|
74
|
+
|
75
|
+
if (onFilterChange && selectedFilter) {
|
76
|
+
onFilterChange(selectedFilter);
|
77
|
+
}
|
78
|
+
};
|
79
|
+
|
67
80
|
return (
|
68
81
|
<>
|
69
|
-
<FlexItem className="chart-donut">
|
82
|
+
<FlexItem className="chart-donut" style={{ cursor }}>
|
70
83
|
<ChartDonut
|
71
84
|
allowTooltip
|
72
85
|
constrainToVisibleArea
|
@@ -78,15 +91,25 @@ const JobInvocationSystemStatusChart = ({
|
|
78
91
|
}))
|
79
92
|
: [{ label: sprintf(__(`Scheduled: ${totalHosts} hosts`)), y: 1 }]
|
80
93
|
}
|
94
|
+
events={[
|
95
|
+
{
|
96
|
+
target: 'data',
|
97
|
+
eventHandlers: {
|
98
|
+
onClick: onChartClick,
|
99
|
+
onMouseOver: () => {
|
100
|
+
setCursor('pointer');
|
101
|
+
},
|
102
|
+
onMouseOut: () => {
|
103
|
+
setCursor('default');
|
104
|
+
},
|
105
|
+
},
|
106
|
+
},
|
107
|
+
]}
|
81
108
|
colorScale={
|
82
109
|
total > 0 ? chartData.map(d => d.color) : [emptyChartDonut.value]
|
83
110
|
}
|
84
111
|
labelComponent={
|
85
|
-
<ChartTooltip
|
86
|
-
pointerLength={0}
|
87
|
-
constrainToVisibleArea
|
88
|
-
renderInPortal={false}
|
89
|
-
/>
|
112
|
+
<ChartTooltip pointerLength={0} renderInPortal={false} />
|
90
113
|
}
|
91
114
|
title={chartDonutTitle}
|
92
115
|
titleComponent={
|
@@ -97,7 +120,7 @@ const JobInvocationSystemStatusChart = ({
|
|
97
120
|
subTitleComponent={
|
98
121
|
// inline style overrides PatternFly default styling
|
99
122
|
<ChartLabel
|
100
|
-
style={{ fontSize: '12px', fill:
|
123
|
+
style={{ fontSize: '12px', fill: cancelledColor.value }}
|
101
124
|
/>
|
102
125
|
}
|
103
126
|
padding={{
|
@@ -110,7 +133,7 @@ const JobInvocationSystemStatusChart = ({
|
|
110
133
|
height={chartSize}
|
111
134
|
/>
|
112
135
|
</FlexItem>
|
113
|
-
<FlexItem className="chart-legend">
|
136
|
+
<FlexItem className="chart-legend" style={{ cursor }}>
|
114
137
|
<Text ouiaId="legend-title" className="legend-title">
|
115
138
|
{__('System status')}
|
116
139
|
</Text>
|
@@ -128,6 +151,32 @@ const JobInvocationSystemStatusChart = ({
|
|
128
151
|
colorScale={chartData.map(d => d.color)}
|
129
152
|
width={legendWidth}
|
130
153
|
height={chartSize}
|
154
|
+
events={[
|
155
|
+
{
|
156
|
+
target: 'data',
|
157
|
+
eventHandlers: {
|
158
|
+
onClick: onChartClick,
|
159
|
+
onMouseOver: () => {
|
160
|
+
setCursor('pointer');
|
161
|
+
},
|
162
|
+
onMouseOut: () => {
|
163
|
+
setCursor('default');
|
164
|
+
},
|
165
|
+
},
|
166
|
+
},
|
167
|
+
{
|
168
|
+
target: 'labels',
|
169
|
+
eventHandlers: {
|
170
|
+
onClick: onChartClick,
|
171
|
+
onMouseOver: () => {
|
172
|
+
setCursor('pointer');
|
173
|
+
},
|
174
|
+
onMouseOut: () => {
|
175
|
+
setCursor('default');
|
176
|
+
},
|
177
|
+
},
|
178
|
+
},
|
179
|
+
]}
|
131
180
|
/>
|
132
181
|
) : (
|
133
182
|
<DescriptionList>
|
@@ -148,10 +197,12 @@ JobInvocationSystemStatusChart.propTypes = {
|
|
148
197
|
data: PropTypes.object.isRequired,
|
149
198
|
isAlreadyStarted: PropTypes.bool.isRequired,
|
150
199
|
formattedStartDate: PropTypes.string,
|
200
|
+
onFilterChange: PropTypes.func,
|
151
201
|
};
|
152
202
|
|
153
203
|
JobInvocationSystemStatusChart.defaultProps = {
|
154
204
|
formattedStartDate: undefined,
|
205
|
+
onFilterChange: undefined,
|
155
206
|
};
|
156
207
|
|
157
208
|
export default JobInvocationSystemStatusChart;
|
@@ -0,0 +1,111 @@
|
|
1
|
+
import React from 'react';
|
2
|
+
import PropTypes from 'prop-types';
|
3
|
+
import {
|
4
|
+
Alert,
|
5
|
+
AlertActionCloseButton,
|
6
|
+
Button,
|
7
|
+
Modal,
|
8
|
+
ModalVariant,
|
9
|
+
} from '@patternfly/react-core';
|
10
|
+
import { OutlinedWindowRestoreIcon } from '@patternfly/react-icons';
|
11
|
+
import { translate as __ } from 'foremanReact/common/I18n';
|
12
|
+
import { templateInvocationPageUrl } from './JobInvocationConstants';
|
13
|
+
|
14
|
+
export const PopupAlert = ({ setShowAlert }) => (
|
15
|
+
<Alert
|
16
|
+
ouiaId="template-invocation-new-tab-popup-alert"
|
17
|
+
variant="warning"
|
18
|
+
actionClose={<AlertActionCloseButton onClose={() => setShowAlert(false)} />}
|
19
|
+
title={__(
|
20
|
+
'Popups are blocked by your browser. Please allow popups for this site to open all invocations in new tabs.'
|
21
|
+
)}
|
22
|
+
/>
|
23
|
+
);
|
24
|
+
export const OpenAlInvocations = ({ results, id, setShowAlert }) => {
|
25
|
+
const [isModalOpen, setIsModalOpen] = React.useState(false);
|
26
|
+
const handleModalToggle = () => {
|
27
|
+
setIsModalOpen(!isModalOpen);
|
28
|
+
};
|
29
|
+
|
30
|
+
const openLink = url => {
|
31
|
+
const newWin = window.open(url);
|
32
|
+
|
33
|
+
if (!newWin || newWin.closed || typeof newWin.closed === 'undefined') {
|
34
|
+
setShowAlert(true);
|
35
|
+
}
|
36
|
+
};
|
37
|
+
const OpenAllButton = () => (
|
38
|
+
<Button
|
39
|
+
variant="link"
|
40
|
+
isInline
|
41
|
+
aria-label="open all template invocations in a new tab"
|
42
|
+
ouiaId="template-invocation-new-tab-button"
|
43
|
+
onClick={() => {
|
44
|
+
if (results.length <= 3) {
|
45
|
+
results.forEach(result => {
|
46
|
+
openLink(templateInvocationPageUrl(result.id, id), '_blank');
|
47
|
+
});
|
48
|
+
} else {
|
49
|
+
handleModalToggle();
|
50
|
+
}
|
51
|
+
}}
|
52
|
+
>
|
53
|
+
<OutlinedWindowRestoreIcon />
|
54
|
+
</Button>
|
55
|
+
);
|
56
|
+
const OpenAllModal = () => (
|
57
|
+
<Modal
|
58
|
+
ouiaId="template-invocation-new-tab-modal"
|
59
|
+
title={__('Open all invocations in new tabs')}
|
60
|
+
isOpen={isModalOpen}
|
61
|
+
onClose={handleModalToggle}
|
62
|
+
variant={ModalVariant.small}
|
63
|
+
titleIconVariant="warning"
|
64
|
+
actions={[
|
65
|
+
<Button
|
66
|
+
ouiaId="template-invocation-new-tab-modal-confirm"
|
67
|
+
key="confirm"
|
68
|
+
variant="primary"
|
69
|
+
onClick={() => {
|
70
|
+
results.forEach(result => {
|
71
|
+
openLink(templateInvocationPageUrl(result.id, id), '_blank');
|
72
|
+
});
|
73
|
+
handleModalToggle();
|
74
|
+
}}
|
75
|
+
>
|
76
|
+
{__('Open all in new tabs')}
|
77
|
+
</Button>,
|
78
|
+
<Button
|
79
|
+
ouiaId="template-invocation-new-tab-modal-cancel"
|
80
|
+
key="cancel"
|
81
|
+
variant="link"
|
82
|
+
onClick={handleModalToggle}
|
83
|
+
>
|
84
|
+
{__('Cancel')}
|
85
|
+
</Button>,
|
86
|
+
]}
|
87
|
+
>
|
88
|
+
{__('Are you sure you want to open all invocations in new tabs?')}
|
89
|
+
<br />
|
90
|
+
{__('This will open a new tab for each invocation.')}
|
91
|
+
<br />
|
92
|
+
{__('The number of invocations is:')} <b>{results.length}</b>
|
93
|
+
</Modal>
|
94
|
+
);
|
95
|
+
return (
|
96
|
+
<>
|
97
|
+
<OpenAllButton />
|
98
|
+
<OpenAllModal />
|
99
|
+
</>
|
100
|
+
);
|
101
|
+
};
|
102
|
+
|
103
|
+
OpenAlInvocations.propTypes = {
|
104
|
+
results: PropTypes.array.isRequired,
|
105
|
+
id: PropTypes.string.isRequired,
|
106
|
+
setShowAlert: PropTypes.func.isRequired,
|
107
|
+
};
|
108
|
+
|
109
|
+
PopupAlert.propTypes = {
|
110
|
+
setShowAlert: PropTypes.func.isRequired,
|
111
|
+
};
|