foreman_remote_execution 14.1.4 → 15.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (48) hide show
  1. checksums.yaml +4 -4
  2. data/app/controllers/api/v2/job_invocations_controller.rb +8 -0
  3. data/app/controllers/template_invocations_controller.rb +57 -0
  4. data/app/controllers/ui_job_wizard_controller.rb +6 -3
  5. data/app/helpers/remote_execution_helper.rb +5 -6
  6. data/app/views/api/v2/job_invocations/base.json.rabl +1 -1
  7. data/app/views/api/v2/job_invocations/hosts.json.rabl +1 -1
  8. data/app/views/api/v2/job_invocations/main.json.rabl +1 -1
  9. data/app/views/api/v2/job_invocations/show.json.rabl +18 -0
  10. data/app/views/templates/script/convert2rhel_analyze.erb +4 -4
  11. data/config/routes.rb +2 -0
  12. data/lib/foreman_remote_execution/engine.rb +3 -3
  13. data/lib/foreman_remote_execution/version.rb +1 -1
  14. data/webpack/JobInvocationDetail/JobAdditionInfo.js +214 -0
  15. data/webpack/JobInvocationDetail/JobInvocationConstants.js +40 -2
  16. data/webpack/JobInvocationDetail/JobInvocationDetail.scss +70 -0
  17. data/webpack/JobInvocationDetail/JobInvocationHostTable.js +177 -80
  18. data/webpack/JobInvocationDetail/JobInvocationHostTableToolbar.js +63 -0
  19. data/webpack/JobInvocationDetail/JobInvocationSelectors.js +8 -1
  20. data/webpack/JobInvocationDetail/JobInvocationSystemStatusChart.js +61 -10
  21. data/webpack/JobInvocationDetail/OpenAlInvocations.js +111 -0
  22. data/webpack/JobInvocationDetail/TemplateInvocation.js +202 -0
  23. data/webpack/JobInvocationDetail/TemplateInvocationComponents/OutputCodeBlock.js +124 -0
  24. data/webpack/JobInvocationDetail/TemplateInvocationComponents/OutputToggleGroup.js +156 -0
  25. data/webpack/JobInvocationDetail/TemplateInvocationComponents/PreviewTemplate.js +50 -0
  26. data/webpack/JobInvocationDetail/TemplateInvocationComponents/TemplateActionButtons.js +224 -0
  27. data/webpack/JobInvocationDetail/TemplateInvocationPage.js +53 -0
  28. data/webpack/JobInvocationDetail/__tests__/MainInformation.test.js +1 -1
  29. data/webpack/JobInvocationDetail/__tests__/OpenAlInvocations.test.js +110 -0
  30. data/webpack/JobInvocationDetail/__tests__/OutputCodeBlock.test.js +69 -0
  31. data/webpack/JobInvocationDetail/__tests__/TemplateInvocation.test.js +131 -0
  32. data/webpack/JobInvocationDetail/__tests__/fixtures.js +130 -0
  33. data/webpack/JobInvocationDetail/index.js +18 -3
  34. data/webpack/JobWizard/JobWizard.js +38 -16
  35. data/webpack/JobWizard/{StartsBeforeErrorAlert.js → StartsErrorAlert.js} +16 -1
  36. data/webpack/JobWizard/steps/AdvancedFields/__tests__/AdvancedFields.test.js +1 -1
  37. data/webpack/JobWizard/steps/Schedule/ScheduleFuture.js +1 -1
  38. data/webpack/JobWizard/steps/Schedule/ScheduleRecurring.js +5 -3
  39. data/webpack/JobWizard/steps/Schedule/ScheduleType.js +1 -1
  40. data/webpack/JobWizard/steps/Schedule/__tests__/Schedule.test.js +3 -3
  41. data/webpack/JobWizard/steps/form/DateTimePicker.js +13 -0
  42. data/webpack/JobWizard/steps/form/Formatter.js +1 -0
  43. data/webpack/JobWizard/steps/form/ResourceSelect.js +34 -9
  44. data/webpack/Routes/routes.js +6 -0
  45. data/webpack/__mocks__/foremanReact/Root/Context/ForemanContext/index.js +1 -0
  46. data/webpack/react_app/components/RegistrationExtension/RexPull.js +27 -2
  47. data/webpack/react_app/components/TargetingHosts/components/HostStatus.js +1 -1
  48. 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 = ({ id, targeting, finished, autoRefresh }) => {
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 defaultParams = { search: urlSearchQuery };
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: { ...defaultParams, key: JOB_INVOCATION_HOSTS },
73
+ params: defaultParams,
47
74
  }
48
75
  );
49
76
 
50
- const combinedResponse = {
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: combinedResponse.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 onPagination = newPagination => {
102
- setParamsAndAPI({
103
- ...params,
104
- ...newPagination,
105
- search: urlSearchQuery,
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 bottomPagination = (
110
- <Pagination
111
- ouiaId="table-hosts-bottom-pagination"
112
- key="table-bottom-pagination"
113
- page={params.page}
114
- perPage={params.perPage}
115
- itemCount={response?.subtotal}
116
- onChange={onPagination}
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
- <TableIndexPage
158
- apiUrl=""
159
- apiOptions={apiOptions}
160
- customSearchProps={memoDefaultSearchProps}
161
- controller="hosts"
162
- creatable={false}
163
- replacementResponse={combinedResponse}
164
- updateSearchQuery={updateSearchQuery}
165
- >
166
- <Table
167
- ouiaId="job-invocation-hosts-table"
168
- columns={columns}
169
- customEmptyState={
170
- status === STATUS_UPPERCASE.RESOLVED && !response?.results?.length
171
- ? customEmptyState
172
- : null
173
- }
174
- params={params}
175
- setParams={setParamsAndAPI}
176
- itemCount={response?.subtotal}
177
- results={response?.results}
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
- {response?.results?.map((result, rowIndex) => (
190
- <Tr key={rowIndex} ouiaId={`table-row-${rowIndex}`}>
191
- {columnNamesKeys.map(k => (
192
- <Td key={k}>{columns[k].wrapper(result)}</Td>
193
- ))}
194
- </Tr>
195
- ))}
196
- </Table>
197
- </TableIndexPage>
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 { JOB_INVOCATION_KEY, GET_TASK } from './JobInvocationConstants';
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 canceledColor,
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: __('Canceled:'), count: cancelled, color: canceledColor.value },
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: canceledColor.value }}
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
+ };