foreman_remote_execution 16.6.5 → 17.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/app/controllers/api/v2/job_invocations_controller.rb +22 -13
- data/app/views/api/v2/job_invocations/main.json.rabl +16 -11
- data/app/views/job_templates/_alerts.html.erb +4 -0
- data/lib/foreman_remote_execution/plugin.rb +1 -1
- data/lib/foreman_remote_execution/version.rb +1 -1
- data/package.json +3 -3
- data/test/functional/api/v2/job_invocations_controller_test.rb +68 -3
- data/webpack/JobInvocationDetail/CheckboxesActions.js +4 -10
- data/webpack/JobInvocationDetail/JobInvocationActions.js +80 -90
- data/webpack/JobInvocationDetail/JobInvocationConstants.js +7 -6
- data/webpack/JobInvocationDetail/JobInvocationEmptyState.js +52 -0
- data/webpack/JobInvocationDetail/JobInvocationHostTable.js +76 -42
- data/webpack/JobInvocationDetail/JobInvocationSelectors.js +1 -19
- data/webpack/JobInvocationDetail/JobInvocationToolbarButtons.js +202 -153
- data/webpack/JobInvocationDetail/TemplateInvocation.js +31 -25
- data/webpack/JobInvocationDetail/__tests__/JobInvocationDetailEmptyState.test.js +53 -0
- data/webpack/JobInvocationDetail/__tests__/JobInvocationHostTablePolling.test.js +336 -0
- data/webpack/JobInvocationDetail/__tests__/JobInvocationPolling.test.js +223 -0
- data/webpack/JobInvocationDetail/__tests__/MainInformation.test.js +260 -111
- data/webpack/JobInvocationDetail/__tests__/TableToolbarActions.test.js +3 -1
- data/webpack/JobInvocationDetail/__tests__/TemplateInvocationPolling.test.js +254 -0
- data/webpack/JobInvocationDetail/__tests__/fixtures.js +2 -0
- data/webpack/JobInvocationDetail/__tests__/foremanTestHelpers.js +30 -0
- data/webpack/JobInvocationDetail/index.js +50 -30
- data/webpack/__mocks__/foremanReact/Root/Context/ForemanContext/index.js +15 -0
- data/webpack/__mocks__/foremanReact/common/hooks/Permissions/permissionHooks.js +1 -0
- data/webpack/__mocks__/foremanReact/redux/API/APISelectors.js +5 -0
- data/webpack/test_setup.js +1 -13
- metadata +11 -3
|
@@ -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: {
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import PropTypes from 'prop-types';
|
|
3
|
+
import { getForemanContext } from 'foremanReact/Root/Context/ForemanContext';
|
|
4
|
+
|
|
5
|
+
export const createForemanContextWrapper = (
|
|
6
|
+
permissions = ['view_job_invocations', 'create_job_invocations']
|
|
7
|
+
) => {
|
|
8
|
+
const foremanContextValue = {
|
|
9
|
+
context: {
|
|
10
|
+
metadata: {
|
|
11
|
+
permissions: new Set(permissions),
|
|
12
|
+
UISettings: { perPage: 20 },
|
|
13
|
+
},
|
|
14
|
+
},
|
|
15
|
+
setContext: jest.fn(),
|
|
16
|
+
};
|
|
17
|
+
const ForemanContext = getForemanContext(foremanContextValue);
|
|
18
|
+
|
|
19
|
+
const ForemanContextWrapper = ({ children }) => (
|
|
20
|
+
<ForemanContext.Provider value={foremanContextValue}>
|
|
21
|
+
{children}
|
|
22
|
+
</ForemanContext.Provider>
|
|
23
|
+
);
|
|
24
|
+
|
|
25
|
+
ForemanContextWrapper.propTypes = {
|
|
26
|
+
children: PropTypes.node.isRequired,
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
return ForemanContextWrapper;
|
|
30
|
+
};
|
|
@@ -5,29 +5,35 @@ import {
|
|
|
5
5
|
PageSectionVariants,
|
|
6
6
|
Skeleton,
|
|
7
7
|
} from '@patternfly/react-core';
|
|
8
|
-
import React, { useEffect, 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 {
|
|
15
|
-
import {
|
|
14
|
+
import { STATUS as API_STATUS } from 'foremanReact/constants';
|
|
15
|
+
import {
|
|
16
|
+
selectAPIErrorMessage,
|
|
17
|
+
selectAPIHttpStatus,
|
|
18
|
+
selectAPIStatus,
|
|
19
|
+
} from 'foremanReact/redux/API/APISelectors';
|
|
16
20
|
|
|
17
21
|
import { JobAdditionInfo } from './JobAdditionInfo';
|
|
18
22
|
import JobInvocationHostTable from './JobInvocationHostTable';
|
|
19
23
|
import JobInvocationOverview from './JobInvocationOverview';
|
|
20
24
|
import JobInvocationSystemStatusChart from './JobInvocationSystemStatusChart';
|
|
25
|
+
import JobInvocationEmptyState from './JobInvocationEmptyState';
|
|
21
26
|
import JobInvocationToolbarButtons from './JobInvocationToolbarButtons';
|
|
22
|
-
import {
|
|
27
|
+
import {
|
|
28
|
+
getJobInvocation,
|
|
29
|
+
stopJobInvocationPolling,
|
|
30
|
+
isJobFinished,
|
|
31
|
+
} from './JobInvocationActions';
|
|
23
32
|
import './JobInvocationDetail.scss';
|
|
24
33
|
import {
|
|
25
|
-
CURRENT_PERMISSIONS,
|
|
26
34
|
DATE_OPTIONS,
|
|
27
35
|
JOB_INVOCATION_KEY,
|
|
28
|
-
STATUS,
|
|
29
36
|
STATUS_UPPERCASE,
|
|
30
|
-
currentPermissionsUrl,
|
|
31
37
|
} from './JobInvocationConstants';
|
|
32
38
|
import { selectItems } from './JobInvocationSelectors';
|
|
33
39
|
|
|
@@ -46,14 +52,17 @@ const JobInvocationDetailPage = ({
|
|
|
46
52
|
start_at: startAt,
|
|
47
53
|
targeting = {},
|
|
48
54
|
} = items;
|
|
49
|
-
const finished =
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
55
|
+
const finished = isJobFinished(statusLabel);
|
|
56
|
+
const pollTimeoutRef = useRef({ timeoutId: null, cancel: () => {} });
|
|
57
|
+
const jobInvocationApiStatus = useSelector(state =>
|
|
58
|
+
selectAPIStatus(state, JOB_INVOCATION_KEY)
|
|
59
|
+
);
|
|
60
|
+
const jobInvocationErrorMessage = useSelector(state =>
|
|
61
|
+
selectAPIErrorMessage(state, JOB_INVOCATION_KEY)
|
|
62
|
+
);
|
|
63
|
+
const jobInvocationHttpStatus = useSelector(state =>
|
|
64
|
+
selectAPIHttpStatus(state, JOB_INVOCATION_KEY)
|
|
65
|
+
);
|
|
57
66
|
const [selectedFilter, setSelectedFilter] = useState('');
|
|
58
67
|
|
|
59
68
|
const handleFilterChange = newFilter => {
|
|
@@ -72,21 +81,30 @@ const JobInvocationDetailPage = ({
|
|
|
72
81
|
}
|
|
73
82
|
|
|
74
83
|
useEffect(() => {
|
|
75
|
-
dispatch(getJobInvocation(`/api/job_invocations/${id}
|
|
76
|
-
if (finished && !autoRefresh) {
|
|
77
|
-
dispatch(stopInterval(JOB_INVOCATION_KEY));
|
|
78
|
-
}
|
|
84
|
+
dispatch(getJobInvocation(`/api/job_invocations/${id}`, pollTimeoutRef));
|
|
79
85
|
return () => {
|
|
80
|
-
|
|
86
|
+
stopJobInvocationPolling(pollTimeoutRef);
|
|
81
87
|
};
|
|
82
|
-
}, [dispatch, id
|
|
88
|
+
}, [dispatch, id]);
|
|
83
89
|
|
|
84
|
-
const
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
90
|
+
const apiFailed = jobInvocationApiStatus === API_STATUS.ERROR;
|
|
91
|
+
|
|
92
|
+
const backendErrorMessage = useMemo(() => {
|
|
93
|
+
if (jobInvocationApiStatus === API_STATUS.ERROR) {
|
|
94
|
+
return jobInvocationErrorMessage || null;
|
|
88
95
|
}
|
|
89
|
-
|
|
96
|
+
return null;
|
|
97
|
+
}, [jobInvocationApiStatus, jobInvocationErrorMessage]);
|
|
98
|
+
|
|
99
|
+
if (apiFailed) {
|
|
100
|
+
return (
|
|
101
|
+
<JobInvocationEmptyState
|
|
102
|
+
jobInvocationId={id}
|
|
103
|
+
httpStatus={jobInvocationHttpStatus}
|
|
104
|
+
errorMessage={backendErrorMessage}
|
|
105
|
+
/>
|
|
106
|
+
);
|
|
107
|
+
}
|
|
90
108
|
|
|
91
109
|
const pageStatus =
|
|
92
110
|
items.id === undefined
|
|
@@ -123,7 +141,11 @@ const JobInvocationDetailPage = ({
|
|
|
123
141
|
<PageLayout
|
|
124
142
|
header={description}
|
|
125
143
|
breadcrumbOptions={breadcrumbOptions}
|
|
126
|
-
toolbarButtons={
|
|
144
|
+
toolbarButtons={
|
|
145
|
+
items.id !== undefined && (
|
|
146
|
+
<JobInvocationToolbarButtons jobId={id} data={items} />
|
|
147
|
+
)
|
|
148
|
+
}
|
|
127
149
|
searchable={false}
|
|
128
150
|
>
|
|
129
151
|
<Flex className="job-invocation-detail-flex">
|
|
@@ -183,10 +205,8 @@ const JobInvocationDetailPage = ({
|
|
|
183
205
|
<JobInvocationHostTable
|
|
184
206
|
id={id}
|
|
185
207
|
targeting={targeting}
|
|
186
|
-
finished={finished}
|
|
187
|
-
autoRefresh={autoRefresh}
|
|
188
208
|
initialFilter={selectedFilter}
|
|
189
|
-
|
|
209
|
+
jobFinished={finished}
|
|
190
210
|
onFilterUpdate={handleFilterChange}
|
|
191
211
|
/>
|
|
192
212
|
</SkeletonLoader>
|
|
@@ -1,6 +1,21 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
|
|
3
|
+
const getForemanContext = contextData => {
|
|
4
|
+
window.tfm_forced_singletons = window.tfm_forced_singletons || {};
|
|
5
|
+
|
|
6
|
+
if (!window.tfm_forced_singletons.Context) {
|
|
7
|
+
window.tfm_forced_singletons.Context = React.createContext(contextData);
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
return window.tfm_forced_singletons.Context;
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
export { getForemanContext };
|
|
1
14
|
export const useForemanOrganization = () => ({ id: 1 });
|
|
2
15
|
export const useForemanLocation = () => ({ id: 2 });
|
|
3
16
|
export const useForemanVersion = () => '3.7';
|
|
4
17
|
export const useForemanHostsPageUrl = () => '/hosts';
|
|
5
18
|
export const useForemanHostDetailsPageUrl = () => '/hosts/';
|
|
6
19
|
export const useForemanSettings = () => ({ perPage: 20 });
|
|
20
|
+
export const useForemanPermissions = () =>
|
|
21
|
+
new Set(['view_job_invocations', 'create_job_invocations']);
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export const usePermissions = jest.fn(() => true);
|
|
@@ -19,3 +19,8 @@ export const selectAPIErrorMessage = (state, key) => {
|
|
|
19
19
|
const error = selectAPIError(state, key);
|
|
20
20
|
return error && error.message;
|
|
21
21
|
};
|
|
22
|
+
|
|
23
|
+
export const selectAPIHttpStatus = (state, key) => {
|
|
24
|
+
const error = selectAPIError(state, key);
|
|
25
|
+
return error?.response?.status;
|
|
26
|
+
};
|
data/webpack/test_setup.js
CHANGED
|
@@ -1,13 +1 @@
|
|
|
1
|
-
import '
|
|
2
|
-
import 'regenerator-runtime/runtime';
|
|
3
|
-
|
|
4
|
-
import { configure } from '@theforeman/test';
|
|
5
|
-
// eslint-disable-next-line import/no-extraneous-dependencies
|
|
6
|
-
import Adapter from 'enzyme-adapter-react-16';
|
|
7
|
-
|
|
8
|
-
configure({ adapter: new Adapter() });
|
|
9
|
-
|
|
10
|
-
// Mocking translation function
|
|
11
|
-
global.__ = str => str;
|
|
12
|
-
global.n__ = str => str;
|
|
13
|
-
global.Jed = { sprintf: str => str };
|
|
1
|
+
import 'foremanJSTestSetup';
|
metadata
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: foreman_remote_execution
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version:
|
|
4
|
+
version: 17.0.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Foreman Remote Execution team
|
|
8
8
|
bindir: bin
|
|
9
9
|
cert_chain: []
|
|
10
|
-
date: 2026-
|
|
10
|
+
date: 2026-08-03 00:00:00.000000000 Z
|
|
11
11
|
dependencies:
|
|
12
12
|
- !ruby/object:Gem::Dependency
|
|
13
13
|
name: deface
|
|
@@ -202,6 +202,7 @@ files:
|
|
|
202
202
|
- app/views/job_invocations/show.js.erb
|
|
203
203
|
- app/views/job_invocations/show.json.erb
|
|
204
204
|
- app/views/job_invocations/welcome.html.erb
|
|
205
|
+
- app/views/job_templates/_alerts.html.erb
|
|
205
206
|
- app/views/job_templates/_custom_tab_headers.html.erb
|
|
206
207
|
- app/views/job_templates/_custom_tabs.html.erb
|
|
207
208
|
- app/views/job_templates/_import_job_template_modal.html.erb
|
|
@@ -382,6 +383,7 @@ files:
|
|
|
382
383
|
- webpack/JobInvocationDetail/JobInvocationActions.js
|
|
383
384
|
- webpack/JobInvocationDetail/JobInvocationConstants.js
|
|
384
385
|
- webpack/JobInvocationDetail/JobInvocationDetail.scss
|
|
386
|
+
- webpack/JobInvocationDetail/JobInvocationEmptyState.js
|
|
385
387
|
- webpack/JobInvocationDetail/JobInvocationHostTable.js
|
|
386
388
|
- webpack/JobInvocationDetail/JobInvocationOverview.js
|
|
387
389
|
- webpack/JobInvocationDetail/JobInvocationSelectors.js
|
|
@@ -395,11 +397,16 @@ files:
|
|
|
395
397
|
- webpack/JobInvocationDetail/TemplateInvocationComponents/TemplateActionButtons.js
|
|
396
398
|
- webpack/JobInvocationDetail/TemplateInvocationComponents/index.scss
|
|
397
399
|
- webpack/JobInvocationDetail/TemplateInvocationPage.js
|
|
400
|
+
- webpack/JobInvocationDetail/__tests__/JobInvocationDetailEmptyState.test.js
|
|
401
|
+
- webpack/JobInvocationDetail/__tests__/JobInvocationHostTablePolling.test.js
|
|
402
|
+
- webpack/JobInvocationDetail/__tests__/JobInvocationPolling.test.js
|
|
398
403
|
- webpack/JobInvocationDetail/__tests__/MainInformation.test.js
|
|
399
404
|
- webpack/JobInvocationDetail/__tests__/OutputCodeBlock.test.js
|
|
400
405
|
- webpack/JobInvocationDetail/__tests__/TableToolbarActions.test.js
|
|
401
406
|
- webpack/JobInvocationDetail/__tests__/TemplateInvocation.test.js
|
|
407
|
+
- webpack/JobInvocationDetail/__tests__/TemplateInvocationPolling.test.js
|
|
402
408
|
- webpack/JobInvocationDetail/__tests__/fixtures.js
|
|
409
|
+
- webpack/JobInvocationDetail/__tests__/foremanTestHelpers.js
|
|
403
410
|
- webpack/JobInvocationDetail/index.js
|
|
404
411
|
- webpack/JobWizard/Footer.js
|
|
405
412
|
- webpack/JobWizard/JobWizard.js
|
|
@@ -469,6 +476,7 @@ files:
|
|
|
469
476
|
- webpack/__mocks__/foremanReact/common/globalIdHelpers.js
|
|
470
477
|
- webpack/__mocks__/foremanReact/common/helpers.js
|
|
471
478
|
- webpack/__mocks__/foremanReact/common/hooks/API/APIHooks.js
|
|
479
|
+
- webpack/__mocks__/foremanReact/common/hooks/Permissions/permissionHooks.js
|
|
472
480
|
- webpack/__mocks__/foremanReact/components/AutoComplete/AutoCompleteActions.js
|
|
473
481
|
- webpack/__mocks__/foremanReact/components/AutoComplete/AutoCompleteConstants.js
|
|
474
482
|
- webpack/__mocks__/foremanReact/components/BreadcrumbBar/index.js
|
|
@@ -560,7 +568,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
560
568
|
- !ruby/object:Gem::Version
|
|
561
569
|
version: '0'
|
|
562
570
|
requirements: []
|
|
563
|
-
rubygems_version: 4.0.
|
|
571
|
+
rubygems_version: 4.0.16
|
|
564
572
|
specification_version: 4
|
|
565
573
|
summary: A plugin bringing remote execution to the Foreman, completing the config
|
|
566
574
|
management functionality with remote management functionality.
|