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.
- checksums.yaml +4 -4
- data/app/controllers/api/v2/job_invocations_controller.rb +24 -2
- data/app/controllers/job_invocations_controller.rb +1 -28
- data/app/views/api/v2/job_invocations/hosts.json.rabl +8 -0
- data/app/views/api/v2/job_invocations/main.json.rabl +1 -0
- data/app/views/job_templates/_alerts.html.erb +4 -0
- data/config/routes.rb +0 -1
- data/lib/foreman_remote_execution/plugin.rb +3 -3
- data/lib/foreman_remote_execution/version.rb +1 -1
- data/test/functional/api/v2/job_invocations_controller_test.rb +46 -7
- data/webpack/JobInvocationDetail/JobInvocationActions.js +86 -91
- data/webpack/JobInvocationDetail/JobInvocationConstants.js +6 -4
- data/webpack/JobInvocationDetail/JobInvocationDetail.scss +5 -3
- data/webpack/JobInvocationDetail/JobInvocationHostTable.js +92 -46
- data/webpack/JobInvocationDetail/JobInvocationSelectors.js +1 -9
- data/webpack/JobInvocationDetail/JobInvocationSystemStatusChart.js +5 -2
- data/webpack/JobInvocationDetail/JobInvocationToolbarButtons.js +10 -12
- data/webpack/JobInvocationDetail/TemplateInvocation.js +38 -26
- data/webpack/JobInvocationDetail/TemplateInvocationComponents/OutputCodeBlock.js +2 -1
- data/webpack/JobInvocationDetail/TemplateInvocationComponents/TemplateActionButtons.js +17 -6
- data/webpack/JobInvocationDetail/TemplateInvocationComponents/index.scss +0 -1
- data/webpack/JobInvocationDetail/__tests__/JobInvocationHostTablePolling.test.js +369 -0
- data/webpack/JobInvocationDetail/__tests__/JobInvocationPolling.test.js +228 -0
- data/webpack/JobInvocationDetail/__tests__/MainInformation.test.js +205 -154
- data/webpack/JobInvocationDetail/__tests__/TemplateInvocationPolling.test.js +254 -0
- data/webpack/JobInvocationDetail/__tests__/fixtures.js +2 -0
- data/webpack/JobInvocationDetail/index.js +25 -25
- data/webpack/JobWizard/JobWizard.js +5 -1
- data/webpack/JobWizard/JobWizard.scss +21 -0
- data/webpack/JobWizard/JobWizardConstants.js +10 -1
- data/webpack/JobWizard/steps/Schedule/RepeatHour.js +4 -2
- data/webpack/JobWizard/steps/Schedule/RepeatWeek.js +4 -2
- data/webpack/JobWizard/steps/Schedule/ScheduleRecurring.js +2 -1
- data/webpack/JobWizard/steps/Schedule/ScheduleType.js +4 -1
- data/webpack/JobWizard/steps/form/DateTimePicker.js +12 -6
- data/webpack/JobWizard/steps/form/FormHelpers.js +5 -4
- data/webpack/react_app/components/FeaturesDropdown/index.scss +2 -0
- data/webpack/react_app/components/TargetingHosts/TargetingHostsLabelsRow.scss +6 -2
- data/webpack/react_app/components/TargetingHosts/__tests__/__snapshots__/TargetingHostsPage.test.js.snap +1 -1
- data/webpack/react_app/components/TargetingHosts/index.js +2 -1
- data/webpack/react_app/redux/actions/jobInvocations/index.js +3 -2
- data/webpack/test_setup.js +1 -13
- metadata +7 -4
- 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 {
|
|
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
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
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
|
-
|
|
91
|
+
stopJobInvocationPolling(pollTimeoutRef);
|
|
91
92
|
};
|
|
92
|
-
}, [dispatch, id
|
|
93
|
+
}, [dispatch, id]);
|
|
93
94
|
|
|
94
|
-
const
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
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
|
|
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
|
-
|
|
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(
|
|
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 {
|
|
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 <
|
|
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(
|
|
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(
|
|
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 <
|
|
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() +
|
|
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(
|
|
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
|
-
|
|
14
|
-
)}-${`0${d.getDate()}`.slice(
|
|
15
|
-
|
|
16
|
-
)}:${`0${d.getMinutes()}`.slice(
|
|
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,
|
|
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,
|
|
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
|
-
<
|
|
12
|
-
|
|
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
|
-
|
|
16
|
+
isInline
|
|
16
17
|
>
|
|
17
18
|
<Icon isInline>
|
|
18
19
|
<HelpIcon />
|
|
19
20
|
</Icon>
|
|
20
|
-
</
|
|
21
|
+
</Button>
|
|
21
22
|
</Popover>
|
|
22
23
|
);
|
|
23
24
|
};
|