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.
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
+ };