foreman_remote_execution 16.7.0 → 17.1.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 (44) hide show
  1. checksums.yaml +4 -4
  2. data/app/controllers/api/v2/job_invocations_controller.rb +24 -2
  3. data/app/controllers/job_invocations_controller.rb +1 -28
  4. data/app/views/api/v2/job_invocations/hosts.json.rabl +8 -0
  5. data/app/views/api/v2/job_invocations/main.json.rabl +1 -0
  6. data/app/views/job_templates/_alerts.html.erb +4 -0
  7. data/config/routes.rb +0 -1
  8. data/lib/foreman_remote_execution/plugin.rb +3 -3
  9. data/lib/foreman_remote_execution/version.rb +1 -1
  10. data/test/functional/api/v2/job_invocations_controller_test.rb +46 -7
  11. data/webpack/JobInvocationDetail/JobInvocationActions.js +86 -91
  12. data/webpack/JobInvocationDetail/JobInvocationConstants.js +6 -4
  13. data/webpack/JobInvocationDetail/JobInvocationDetail.scss +5 -3
  14. data/webpack/JobInvocationDetail/JobInvocationHostTable.js +92 -46
  15. data/webpack/JobInvocationDetail/JobInvocationSelectors.js +1 -9
  16. data/webpack/JobInvocationDetail/JobInvocationSystemStatusChart.js +5 -2
  17. data/webpack/JobInvocationDetail/JobInvocationToolbarButtons.js +10 -12
  18. data/webpack/JobInvocationDetail/TemplateInvocation.js +38 -26
  19. data/webpack/JobInvocationDetail/TemplateInvocationComponents/OutputCodeBlock.js +2 -1
  20. data/webpack/JobInvocationDetail/TemplateInvocationComponents/TemplateActionButtons.js +17 -6
  21. data/webpack/JobInvocationDetail/TemplateInvocationComponents/index.scss +0 -1
  22. data/webpack/JobInvocationDetail/__tests__/JobInvocationHostTablePolling.test.js +369 -0
  23. data/webpack/JobInvocationDetail/__tests__/JobInvocationPolling.test.js +228 -0
  24. data/webpack/JobInvocationDetail/__tests__/MainInformation.test.js +205 -154
  25. data/webpack/JobInvocationDetail/__tests__/TemplateInvocationPolling.test.js +254 -0
  26. data/webpack/JobInvocationDetail/__tests__/fixtures.js +2 -0
  27. data/webpack/JobInvocationDetail/index.js +25 -25
  28. data/webpack/JobWizard/JobWizard.js +5 -1
  29. data/webpack/JobWizard/JobWizard.scss +21 -0
  30. data/webpack/JobWizard/JobWizardConstants.js +10 -1
  31. data/webpack/JobWizard/steps/Schedule/RepeatHour.js +4 -2
  32. data/webpack/JobWizard/steps/Schedule/RepeatWeek.js +4 -2
  33. data/webpack/JobWizard/steps/Schedule/ScheduleRecurring.js +2 -1
  34. data/webpack/JobWizard/steps/Schedule/ScheduleType.js +4 -1
  35. data/webpack/JobWizard/steps/form/DateTimePicker.js +12 -6
  36. data/webpack/JobWizard/steps/form/FormHelpers.js +5 -4
  37. data/webpack/react_app/components/FeaturesDropdown/index.scss +2 -0
  38. data/webpack/react_app/components/TargetingHosts/TargetingHostsLabelsRow.scss +6 -2
  39. data/webpack/react_app/components/TargetingHosts/__tests__/__snapshots__/TargetingHostsPage.test.js.snap +1 -1
  40. data/webpack/react_app/components/TargetingHosts/index.js +2 -1
  41. data/webpack/react_app/redux/actions/jobInvocations/index.js +3 -2
  42. data/webpack/test_setup.js +1 -13
  43. metadata +7 -4
  44. data/webpack/__mocks__/foremanReact/constants.js +0 -25
@@ -0,0 +1,254 @@
1
+ import React from 'react';
2
+ import { createStore, applyMiddleware } from 'redux';
3
+ import thunk from 'redux-thunk';
4
+ import { Provider } from 'react-redux';
5
+ import { render, act } from '@testing-library/react';
6
+ import '@testing-library/jest-dom/extend-expect';
7
+ import * as api from 'foremanReact/redux/API';
8
+ import * as selectors from '../JobInvocationSelectors';
9
+ import { TemplateInvocation } from '../TemplateInvocation';
10
+ import { mockTemplateInvocationResponse } from './fixtures';
11
+
12
+ jest.spyOn(api, 'get');
13
+ jest.mock('../JobInvocationSelectors');
14
+
15
+ jest.mock('foremanReact/components/ToastsList', () => ({
16
+ addToast: jest.fn(payload => ({ type: 'ADD_TOAST', payload })),
17
+ }));
18
+
19
+ describe('TemplateInvocation polling', () => {
20
+ const noop = () => {};
21
+ const reducer = (state = {}) => state;
22
+ const makeStore = () => createStore(reducer, applyMiddleware(thunk));
23
+
24
+ const pollingProps = {
25
+ hostID: '1',
26
+ jobID: '1',
27
+ isInTableView: false,
28
+ isExpanded: true,
29
+ hostName: 'example-host',
30
+ hostProxy: { name: 'example-proxy', href: '#' },
31
+ showOutputType: { stderr: true, stdout: true, debug: true },
32
+ setShowOutputType: noop,
33
+ showTemplatePreview: false,
34
+ setShowTemplatePreview: noop,
35
+ showCommand: false,
36
+ setShowCommand: noop,
37
+ };
38
+
39
+ let apiGetSpy;
40
+
41
+ beforeEach(() => {
42
+ jest.useFakeTimers({ legacyFakeTimers: true });
43
+ selectors.selectTemplateInvocationStatus.mockImplementation(() => () =>
44
+ 'RESOLVED'
45
+ );
46
+ selectors.selectTemplateInvocation.mockImplementation(() => () =>
47
+ mockTemplateInvocationResponse
48
+ );
49
+ apiGetSpy = jest
50
+ .spyOn(api.APIActions, 'get')
51
+ .mockImplementation(({ handleSuccess }) => {
52
+ handleSuccess &&
53
+ handleSuccess({ data: { finished: false, auto_refresh: true } });
54
+ return { type: 'MOCK_GET' };
55
+ });
56
+ });
57
+
58
+ afterEach(() => {
59
+ jest.clearAllTimers();
60
+ jest.useRealTimers();
61
+ apiGetSpy.mockRestore();
62
+ });
63
+
64
+ it('fetches on mount when isExpanded is true', () => {
65
+ const localStore = makeStore();
66
+ render(
67
+ <Provider store={localStore}>
68
+ <TemplateInvocation {...pollingProps} isExpanded />
69
+ </Provider>
70
+ );
71
+ expect(apiGetSpy).toHaveBeenCalledTimes(1);
72
+ });
73
+
74
+ it('does not fetch on mount when isExpanded is false', () => {
75
+ const localStore = makeStore();
76
+ render(
77
+ <Provider store={localStore}>
78
+ <TemplateInvocation {...pollingProps} isExpanded={false} />
79
+ </Provider>
80
+ );
81
+ expect(apiGetSpy).not.toHaveBeenCalled();
82
+ });
83
+
84
+ it('schedules next poll via setTimeout when auto_refresh is true and not finished', () => {
85
+ const localStore = makeStore();
86
+ render(
87
+ <Provider store={localStore}>
88
+ <TemplateInvocation {...pollingProps} />
89
+ </Provider>
90
+ );
91
+ expect(apiGetSpy).toHaveBeenCalledTimes(1);
92
+
93
+ act(() => jest.advanceTimersByTime(5000));
94
+ expect(apiGetSpy).toHaveBeenCalledTimes(2);
95
+ });
96
+
97
+ it('does not schedule next poll when finished is true', () => {
98
+ apiGetSpy.mockImplementation(({ handleSuccess }) => {
99
+ handleSuccess &&
100
+ handleSuccess({ data: { finished: true, auto_refresh: true } });
101
+ return { type: 'MOCK_GET' };
102
+ });
103
+ const localStore = makeStore();
104
+ render(
105
+ <Provider store={localStore}>
106
+ <TemplateInvocation {...pollingProps} />
107
+ </Provider>
108
+ );
109
+ expect(apiGetSpy).toHaveBeenCalledTimes(1);
110
+
111
+ act(() => jest.advanceTimersByTime(5000));
112
+ expect(apiGetSpy).toHaveBeenCalledTimes(1);
113
+ });
114
+
115
+ it('does not schedule next poll when auto_refresh is false', () => {
116
+ apiGetSpy.mockImplementation(({ handleSuccess }) => {
117
+ handleSuccess &&
118
+ handleSuccess({ data: { finished: false, auto_refresh: false } });
119
+ return { type: 'MOCK_GET' };
120
+ });
121
+ const localStore = makeStore();
122
+ render(
123
+ <Provider store={localStore}>
124
+ <TemplateInvocation {...pollingProps} />
125
+ </Provider>
126
+ );
127
+ expect(apiGetSpy).toHaveBeenCalledTimes(1);
128
+
129
+ act(() => jest.advanceTimersByTime(5000));
130
+ expect(apiGetSpy).toHaveBeenCalledTimes(1);
131
+ });
132
+
133
+ it('clears the polling timeout and sets cancelled on unmount', () => {
134
+ const localStore = makeStore();
135
+ const { unmount } = render(
136
+ <Provider store={localStore}>
137
+ <TemplateInvocation {...pollingProps} />
138
+ </Provider>
139
+ );
140
+ expect(apiGetSpy).toHaveBeenCalledTimes(1);
141
+
142
+ unmount();
143
+ act(() => jest.advanceTimersByTime(5000));
144
+ expect(apiGetSpy).toHaveBeenCalledTimes(1);
145
+ });
146
+
147
+ it('cancels in-flight callback when unmounted before handleSuccess runs', () => {
148
+ let capturedHandleSuccess;
149
+ apiGetSpy.mockImplementation(({ handleSuccess }) => {
150
+ capturedHandleSuccess = handleSuccess;
151
+ return { type: 'MOCK_GET' };
152
+ });
153
+
154
+ const localStore = makeStore();
155
+ const { unmount } = render(
156
+ <Provider store={localStore}>
157
+ <TemplateInvocation {...pollingProps} />
158
+ </Provider>
159
+ );
160
+ expect(apiGetSpy).toHaveBeenCalledTimes(1);
161
+
162
+ unmount();
163
+
164
+ act(() => {
165
+ capturedHandleSuccess({ data: { finished: false, auto_refresh: true } });
166
+ jest.advanceTimersByTime(5000);
167
+ });
168
+
169
+ expect(apiGetSpy).toHaveBeenCalledTimes(1);
170
+ });
171
+
172
+ it('re-fetches when isExpanded changes from false to true', () => {
173
+ const localStore = makeStore();
174
+ const { rerender } = render(
175
+ <Provider store={localStore}>
176
+ <TemplateInvocation {...pollingProps} isExpanded={false} />
177
+ </Provider>
178
+ );
179
+ expect(apiGetSpy).not.toHaveBeenCalled();
180
+
181
+ rerender(
182
+ <Provider store={localStore}>
183
+ <TemplateInvocation {...pollingProps} isExpanded />
184
+ </Provider>
185
+ );
186
+ expect(apiGetSpy).toHaveBeenCalledTimes(1);
187
+ });
188
+
189
+ it('does not re-fetch on expand when response is already finished', () => {
190
+ selectors.selectTemplateInvocation.mockImplementation(() => () => ({
191
+ ...mockTemplateInvocationResponse,
192
+ finished: true,
193
+ }));
194
+ apiGetSpy.mockImplementation(({ handleSuccess }) => {
195
+ handleSuccess &&
196
+ handleSuccess({ data: { finished: true, auto_refresh: false } });
197
+ return { type: 'MOCK_GET' };
198
+ });
199
+
200
+ const localStore = makeStore();
201
+ const { rerender } = render(
202
+ <Provider store={localStore}>
203
+ <TemplateInvocation {...pollingProps} isExpanded={false} />
204
+ </Provider>
205
+ );
206
+ expect(apiGetSpy).not.toHaveBeenCalled();
207
+
208
+ rerender(
209
+ <Provider store={localStore}>
210
+ <TemplateInvocation {...pollingProps} isExpanded />
211
+ </Provider>
212
+ );
213
+ // Selector already returns finished=true → guard skips dispatchFetch
214
+ expect(apiGetSpy).not.toHaveBeenCalled();
215
+
216
+ rerender(
217
+ <Provider store={localStore}>
218
+ <TemplateInvocation {...pollingProps} isExpanded={false} />
219
+ </Provider>
220
+ );
221
+ rerender(
222
+ <Provider store={localStore}>
223
+ <TemplateInvocation {...pollingProps} isExpanded />
224
+ </Provider>
225
+ );
226
+ // Second expand: still finished → still no fetch
227
+ expect(apiGetSpy).not.toHaveBeenCalled();
228
+ });
229
+
230
+ it('cancels existing poll and starts fresh when isExpanded changes', () => {
231
+ const localStore = makeStore();
232
+ const { rerender } = render(
233
+ <Provider store={localStore}>
234
+ <TemplateInvocation {...pollingProps} isExpanded />
235
+ </Provider>
236
+ );
237
+ expect(apiGetSpy).toHaveBeenCalledTimes(1);
238
+
239
+ rerender(
240
+ <Provider store={localStore}>
241
+ <TemplateInvocation {...pollingProps} isExpanded={false} />
242
+ </Provider>
243
+ );
244
+ act(() => jest.advanceTimersByTime(5000));
245
+ expect(apiGetSpy).toHaveBeenCalledTimes(1);
246
+
247
+ rerender(
248
+ <Provider store={localStore}>
249
+ <TemplateInvocation {...pollingProps} isExpanded />
250
+ </Provider>
251
+ );
252
+ expect(apiGetSpy).toHaveBeenCalledTimes(2);
253
+ });
254
+ });
@@ -42,6 +42,7 @@ export const jobInvocationData = {
42
42
  id: '37ad5ead-51de-4798-bc73-a17687c4d5aa',
43
43
  state: 'stopped',
44
44
  started_at: '2024-01-01 12:34:56 +0100',
45
+ cancellable: true,
45
46
  },
46
47
  template_invocations: [
47
48
  {
@@ -113,6 +114,7 @@ export const jobInvocationDataRecurring = {
113
114
  task: {
114
115
  id: '37ad5ead-51de-4798-bc73-a17687c4d5aa',
115
116
  state: 'scheduled',
117
+ cancellable: true,
116
118
  },
117
119
  mode: 'recurring',
118
120
  recurrence: {
@@ -5,13 +5,12 @@ import {
5
5
  PageSectionVariants,
6
6
  Skeleton,
7
7
  } from '@patternfly/react-core';
8
- import React, { useEffect, useMemo, useState } from 'react';
8
+ import React, { useEffect, useMemo, useRef, useState } from 'react';
9
9
  import { translate as __, documentLocale } from 'foremanReact/common/I18n';
10
10
  import { useDispatch, useSelector } from 'react-redux';
11
11
  import PageLayout from 'foremanReact/routes/common/PageLayout/PageLayout';
12
12
  import PropTypes from 'prop-types';
13
13
  import SkeletonLoader from 'foremanReact/components/common/SkeletonLoader';
14
- import { stopInterval } from 'foremanReact/redux/middlewares/IntervalMiddleware';
15
14
  import { STATUS as API_STATUS } from 'foremanReact/constants';
16
15
  import {
17
16
  selectAPIErrorMessage,
@@ -25,12 +24,15 @@ import JobInvocationOverview from './JobInvocationOverview';
25
24
  import JobInvocationSystemStatusChart from './JobInvocationSystemStatusChart';
26
25
  import JobInvocationEmptyState from './JobInvocationEmptyState';
27
26
  import JobInvocationToolbarButtons from './JobInvocationToolbarButtons';
28
- import { getJobInvocation, getTask } from './JobInvocationActions';
27
+ import {
28
+ getJobInvocation,
29
+ stopJobInvocationPolling,
30
+ isJobFinished,
31
+ } from './JobInvocationActions';
29
32
  import './JobInvocationDetail.scss';
30
33
  import {
31
34
  DATE_OPTIONS,
32
35
  JOB_INVOCATION_KEY,
33
- STATUS,
34
36
  STATUS_UPPERCASE,
35
37
  } from './JobInvocationConstants';
36
38
  import { selectItems } from './JobInvocationSelectors';
@@ -50,11 +52,13 @@ const JobInvocationDetailPage = ({
50
52
  start_at: startAt,
51
53
  targeting = {},
52
54
  } = items;
53
- const finished =
54
- statusLabel === STATUS.FAILED ||
55
- statusLabel === STATUS.SUCCEEDED ||
56
- statusLabel === STATUS.CANCELLED;
57
- const autoRefresh = task?.state === STATUS.PENDING || false;
55
+ const finished = isJobFinished(statusLabel);
56
+ const pollTimeoutRef = useRef({ timeoutId: null, cancel: () => {} });
57
+ const permissionsRef = useRef(null);
58
+ if (items.permissions && !permissionsRef.current) {
59
+ permissionsRef.current = items.permissions;
60
+ }
61
+ const permissions = items.permissions || permissionsRef.current;
58
62
  const jobInvocationApiStatus = useSelector(state =>
59
63
  selectAPIStatus(state, JOB_INVOCATION_KEY)
60
64
  );
@@ -82,21 +86,16 @@ const JobInvocationDetailPage = ({
82
86
  }
83
87
 
84
88
  useEffect(() => {
85
- dispatch(getJobInvocation(`/api/job_invocations/${id}`));
86
- if (finished && !autoRefresh) {
87
- dispatch(stopInterval(JOB_INVOCATION_KEY));
88
- }
89
+ dispatch(getJobInvocation(`/api/job_invocations/${id}`, pollTimeoutRef));
89
90
  return () => {
90
- dispatch(stopInterval(JOB_INVOCATION_KEY));
91
+ stopJobInvocationPolling(pollTimeoutRef);
91
92
  };
92
- }, [dispatch, id, finished, autoRefresh]);
93
+ }, [dispatch, id]);
93
94
 
94
- const taskId = task?.id;
95
- useEffect(() => {
96
- if (taskId !== undefined) {
97
- dispatch(getTask(`${taskId}`));
98
- }
99
- }, [dispatch, taskId]);
95
+ const dataWithPermissions = useMemo(() => ({ ...items, permissions }), [
96
+ items,
97
+ permissions,
98
+ ]);
100
99
 
101
100
  const apiFailed = jobInvocationApiStatus === API_STATUS.ERROR;
102
101
 
@@ -154,7 +153,10 @@ const JobInvocationDetailPage = ({
154
153
  breadcrumbOptions={breadcrumbOptions}
155
154
  toolbarButtons={
156
155
  items.id !== undefined && (
157
- <JobInvocationToolbarButtons jobId={id} data={items} />
156
+ <JobInvocationToolbarButtons
157
+ jobId={id}
158
+ data={dataWithPermissions}
159
+ />
158
160
  )
159
161
  }
160
162
  searchable={false}
@@ -216,10 +218,8 @@ const JobInvocationDetailPage = ({
216
218
  <JobInvocationHostTable
217
219
  id={id}
218
220
  targeting={targeting}
219
- finished={finished}
220
- autoRefresh={autoRefresh}
221
221
  initialFilter={selectedFilter}
222
- statusLabel={statusLabel}
222
+ jobFinished={finished}
223
223
  onFilterUpdate={handleFilterChange}
224
224
  />
225
225
  </SkeletonLoader>
@@ -18,6 +18,7 @@ import {
18
18
  WIZARD_TITLES,
19
19
  SCHEDULE_TYPES,
20
20
  initialScheduleState,
21
+ STARTS_ERROR_CHECK_INTERVAL_MS,
21
22
  } from './JobWizardConstants';
22
23
  import {
23
24
  selectTemplateError,
@@ -237,7 +238,10 @@ export const JobWizard = ({ rerunData }) => {
237
238
  }
238
239
  };
239
240
  updateStartsError();
240
- const interval = setInterval(updateStartsError, 5000);
241
+ const interval = setInterval(
242
+ updateStartsError,
243
+ STARTS_ERROR_CHECK_INTERVAL_MS
244
+ );
241
245
 
242
246
  return () => {
243
247
  interval && clearInterval(interval);
@@ -1,5 +1,6 @@
1
1
  .job-wizard {
2
2
  font-size: var(--pf-v5-global--FontSize--md);
3
+
3
4
  .wizard-title {
4
5
  margin-bottom: 25px;
5
6
  }
@@ -15,6 +16,7 @@
15
16
  var(--pf-v5-cwizard__toggle--ZIndex) + 1
16
17
  ); // So the select box can be shown above the wizard footer and navigation toggle
17
18
  }
19
+
18
20
  .pf-v5-c-wizard__main-body {
19
21
  @media (max-width: 600px) {
20
22
  max-width: 100%;
@@ -41,6 +43,7 @@
41
43
  .pf-v5-c-chip-group.pf-m-category {
42
44
  margin-bottom: 10px;
43
45
  }
46
+
44
47
  .pf-v5-c-select__toggle-typeahead {
45
48
  border: 0px;
46
49
  }
@@ -50,62 +53,76 @@
50
53
  flex-wrap: nowrap;
51
54
  }
52
55
  }
56
+
53
57
  .foreman-search-field {
54
58
  width: 100%;
55
59
  }
56
60
  }
61
+
57
62
  input[type='radio'],
58
63
  input[type='checkbox'] {
59
64
  margin: 0;
60
65
  }
66
+
61
67
  .schedule-tab {
62
68
  #repeat-on-weekly {
63
69
  display: grid;
64
70
  grid-template-columns: repeat(7, 1fr);
65
71
  }
72
+
66
73
  .pf-v5-l-grid {
67
74
  gap: var(--pf-v5-cform--GridGap);
68
75
  }
76
+
69
77
  #repeat-on-hourly {
70
78
  max-height: 300px;
71
79
  overflow: scroll;
72
80
  }
81
+
73
82
  .schedule-radio label {
74
83
  width: 100%;
75
84
  }
85
+
76
86
  .schedule-radio input {
77
87
  align-self: center;
78
88
  }
89
+
79
90
  .schedule-radio-repeat-text {
80
91
  width: 100px;
81
92
  display: inline-block;
82
93
  margin-right: 5px;
83
94
  align-self: center;
84
95
  }
96
+
85
97
  .schedule-radio-title {
86
98
  width: 80px;
87
99
  display: inline-block;
88
100
  align-self: center;
89
101
  }
102
+
90
103
  .schedule-radio-occurences {
91
104
  display: inline-block;
92
105
  align-self: center;
93
106
  }
107
+
94
108
  .schedule-radio-wrapper {
95
109
  display: flex;
96
110
  }
97
111
  }
112
+
98
113
  .future-schedule-tab {
99
114
  .clear-datetime-button {
100
115
  margin-left: 10px;
101
116
  align-self: center;
102
117
  font-size: var(--pf-v5-global--FontSize--md);
103
118
  }
119
+
104
120
  .pf-v5-c-form__group-control {
105
121
  display: flex;
106
122
  flex-wrap: wrap;
107
123
  }
108
124
  }
125
+
109
126
  .pf-v5-c-date-picker {
110
127
  vertical-align: top;
111
128
  }
@@ -119,10 +136,12 @@
119
136
  // overwriting bootstrap/_forms.scss margin: 4px 0 0;
120
137
  margin: 0;
121
138
  }
139
+
122
140
  textarea {
123
141
  min-height: 40px;
124
142
  min-width: 100px;
125
143
  }
144
+
126
145
  .pf-v5-c-modal-box {
127
146
  width: auto;
128
147
  }
@@ -132,9 +151,11 @@
132
151
  margin-left: 10px;
133
152
  }
134
153
  }
154
+
135
155
  .pf-v5-c-radio__body {
136
156
  font-size: var(--pf-v5-cradio__label--FontSize);
137
157
  }
158
+
138
159
  .reset-default {
139
160
  padding-bottom: 0;
140
161
  }
@@ -1,6 +1,9 @@
1
1
  import { translate as __ } from 'foremanReact/common/I18n';
2
2
  import { foremanUrl } from 'foremanReact/common/helpers';
3
- import { getControllerSearchProps } from 'foremanReact/constants';
3
+ import {
4
+ getControllerSearchProps,
5
+ MS_PER_SECOND,
6
+ } from 'foremanReact/constants';
4
7
 
5
8
  export const JOB_TEMPLATES = 'JOB_TEMPLATES';
6
9
  export const JOB_CATEGORIES = 'JOB_CATEGORIES';
@@ -78,3 +81,9 @@ export const HOST_IDS = 'HOST_IDS';
78
81
  export const REX_FEATURE = 'REX_FEATURE';
79
82
 
80
83
  export const JOB_API_KEY = 'JOB_API_KEY';
84
+
85
+ export const SUNDAY_BASE_YEAR = 2017;
86
+ export const DEFAULT_MINUTE_OPTIONS = [0, 15, 30, 45];
87
+ export const STARTS_ERROR_CHECK_INTERVAL_MS = 5000;
88
+ export const DATE_PADDING_SLICE = -2;
89
+ export const DEBOUNCE_INPUT_MS = MS_PER_SECOND;
@@ -12,13 +12,15 @@ import {
12
12
  SelectVariant,
13
13
  } from '@patternfly/react-core/deprecated';
14
14
  import { translate as __ } from 'foremanReact/common/I18n';
15
+ import { MINUTES_PER_HOUR } from 'foremanReact/constants';
15
16
  import { helpLabel } from '../form/FormHelpers';
17
+ import { DEFAULT_MINUTE_OPTIONS } from '../../JobWizardConstants';
16
18
 
17
19
  export const RepeatHour = ({ repeatData, setRepeatData }) => {
18
20
  const isValidMinute = newMinute =>
19
21
  Number.isInteger(parseInt(newMinute, 10)) &&
20
22
  newMinute >= 0 &&
21
- newMinute < 60;
23
+ newMinute < MINUTES_PER_HOUR;
22
24
 
23
25
  const { minute } = repeatData;
24
26
  useEffect(() => {
@@ -27,7 +29,7 @@ export const RepeatHour = ({ repeatData, setRepeatData }) => {
27
29
  }
28
30
  }, [minute, setRepeatData]);
29
31
  const [minuteOpen, setMinuteOpen] = useState(false);
30
- const [options, setOptions] = useState([0, 15, 30, 45]);
32
+ const [options, setOptions] = useState(DEFAULT_MINUTE_OPTIONS);
31
33
  const [isAlertOpen, setIsAlertOpen] = useState(false);
32
34
  return (
33
35
  <FormGroup
@@ -2,15 +2,17 @@ import React, { useEffect } from 'react';
2
2
  import PropTypes from 'prop-types';
3
3
  import { FormGroup, Checkbox } from '@patternfly/react-core';
4
4
  import { translate as __, documentLocale } from 'foremanReact/common/I18n';
5
+ import { DAYS_PER_WEEK } from 'foremanReact/constants';
5
6
  import { RepeatDaily } from './RepeatDaily';
6
7
  import { noop } from '../../../helpers';
8
+ import { SUNDAY_BASE_YEAR } from '../../JobWizardConstants';
7
9
 
8
10
  export const getWeekDays = () => {
9
11
  const locale = documentLocale().replace(/-/g, '_');
10
- const baseDate = new Date(Date.UTC(2017, 0, 1)); // just a Sunday
12
+ const baseDate = new Date(Date.UTC(SUNDAY_BASE_YEAR, 0, 1)); // just a Sunday
11
13
  const weekDays = [];
12
14
  const formatOptions = { weekday: 'short', timeZone: 'UTC' };
13
- for (let i = 0; i < 7; i++) {
15
+ for (let i = 0; i < DAYS_PER_WEEK; i++) {
14
16
  try {
15
17
  weekDays.push(baseDate.toLocaleDateString(locale, formatOptions));
16
18
  } catch {
@@ -12,6 +12,7 @@ import {
12
12
  } from '@patternfly/react-core';
13
13
  import { ExclamationCircleIcon } from '@patternfly/react-icons';
14
14
  import { translate as __ } from 'foremanReact/common/I18n';
15
+ import { MS_PER_MINUTE } from 'foremanReact/constants';
15
16
  import { RepeatOn } from './RepeatOn';
16
17
  import { SCHEDULE_TYPES } from '../../JobWizardConstants';
17
18
  import { PurposeField } from './PurposeField';
@@ -114,7 +115,7 @@ export const ScheduleRecurring = ({
114
115
  setScheduleValue(current => ({
115
116
  ...current,
116
117
  startsAt: new Date(
117
- new Date().getTime() + 60000
118
+ new Date().getTime() + MS_PER_MINUTE
118
119
  ).toISOString(), // 1 minute in the future
119
120
  isFuture: true,
120
121
  }))
@@ -2,6 +2,7 @@ import React from 'react';
2
2
  import PropTypes from 'prop-types';
3
3
  import { Form, FormGroup, Radio, Divider } from '@patternfly/react-core';
4
4
  import { translate as __ } from 'foremanReact/common/I18n';
5
+ import { MS_PER_MINUTE } from 'foremanReact/constants';
5
6
  import {
6
7
  WIZARD_TITLES,
7
8
  SCHEDULE_TYPES,
@@ -49,7 +50,9 @@ export const ScheduleType = ({
49
50
  onChange={() => {
50
51
  setScheduleValue(current => ({
51
52
  ...current,
52
- startsAt: new Date(new Date().getTime() + 60000).toISOString(), // 1 minute in the future
53
+ startsAt: new Date(
54
+ new Date().getTime() + MS_PER_MINUTE
55
+ ).toISOString(), // 1 minute in the future
53
56
  scheduleType: SCHEDULE_TYPES.FUTURE,
54
57
  repeatType: repeatTypes.noRepeat,
55
58
  }));
@@ -7,13 +7,19 @@ import {
7
7
  } from '@patternfly/react-core';
8
8
  import { debounce } from 'lodash';
9
9
  import { translate as __, documentLocale } from 'foremanReact/common/I18n';
10
+ import {
11
+ DATE_PADDING_SLICE,
12
+ DEBOUNCE_INPUT_MS,
13
+ } from '../../JobWizardConstants';
10
14
 
11
15
  const formatDateTime = d =>
12
16
  `${d.getFullYear()}-${`0${d.getMonth() + 1}`.slice(
13
- -2
14
- )}-${`0${d.getDate()}`.slice(-2)} ${`0${d.getHours()}`.slice(
15
- -2
16
- )}:${`0${d.getMinutes()}`.slice(-2)}:${`0${d.getSeconds()}`.slice(-2)}`;
17
+ DATE_PADDING_SLICE
18
+ )}-${`0${d.getDate()}`.slice(DATE_PADDING_SLICE)} ${`0${d.getHours()}`.slice(
19
+ DATE_PADDING_SLICE
20
+ )}:${`0${d.getMinutes()}`.slice(
21
+ DATE_PADDING_SLICE
22
+ )}:${`0${d.getSeconds()}`.slice(DATE_PADDING_SLICE)}`;
17
23
 
18
24
  export const DateTimePicker = ({
19
25
  dateTime,
@@ -103,7 +109,7 @@ export const DateTimePicker = ({
103
109
  aria-label={`${ariaLabel} datepicker`}
104
110
  value={formattedDate}
105
111
  placeholder="yyyy/mm/dd"
106
- onChange={debounce(onDateChange, 1000, {
112
+ onChange={debounce(onDateChange, DEBOUNCE_INPUT_MS, {
107
113
  leading: false,
108
114
  trailing: true,
109
115
  })}
@@ -123,7 +129,7 @@ export const DateTimePicker = ({
123
129
  time={dateTime ? dateObject.toString() : ''}
124
130
  inputProps={dateTime ? {} : { value: '' }}
125
131
  placeholder={includeSeconds ? 'hh:mm:ss' : 'hh:mm'}
126
- onChange={debounce(onTimeChange, 1000, {
132
+ onChange={debounce(onTimeChange, DEBOUNCE_INPUT_MS, {
127
133
  leading: false,
128
134
  trailing: true,
129
135
  })}
@@ -8,16 +8,17 @@ export const helpLabel = (text, id) => {
8
8
  if (!text) return null;
9
9
  return (
10
10
  <Popover id={`${id}-help`} bodyContent={text} aria-label="help-text">
11
- <button
12
- type="button"
11
+ <Button
12
+ ouiaId={`${id}-help-button`}
13
+ variant="plain"
13
14
  aria-label={__('open-help-tooltip-button')}
14
15
  onClick={e => e.preventDefault()}
15
- className="pf-v5-c-form__group-label-help"
16
+ isInline
16
17
  >
17
18
  <Icon isInline>
18
19
  <HelpIcon />
19
20
  </Icon>
20
- </button>
21
+ </Button>
21
22
  </Popover>
22
23
  );
23
24
  };