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.
Files changed (30) hide show
  1. checksums.yaml +4 -4
  2. data/app/controllers/api/v2/job_invocations_controller.rb +22 -13
  3. data/app/views/api/v2/job_invocations/main.json.rabl +16 -11
  4. data/app/views/job_templates/_alerts.html.erb +4 -0
  5. data/lib/foreman_remote_execution/plugin.rb +1 -1
  6. data/lib/foreman_remote_execution/version.rb +1 -1
  7. data/package.json +3 -3
  8. data/test/functional/api/v2/job_invocations_controller_test.rb +68 -3
  9. data/webpack/JobInvocationDetail/CheckboxesActions.js +4 -10
  10. data/webpack/JobInvocationDetail/JobInvocationActions.js +80 -90
  11. data/webpack/JobInvocationDetail/JobInvocationConstants.js +7 -6
  12. data/webpack/JobInvocationDetail/JobInvocationEmptyState.js +52 -0
  13. data/webpack/JobInvocationDetail/JobInvocationHostTable.js +76 -42
  14. data/webpack/JobInvocationDetail/JobInvocationSelectors.js +1 -19
  15. data/webpack/JobInvocationDetail/JobInvocationToolbarButtons.js +202 -153
  16. data/webpack/JobInvocationDetail/TemplateInvocation.js +31 -25
  17. data/webpack/JobInvocationDetail/__tests__/JobInvocationDetailEmptyState.test.js +53 -0
  18. data/webpack/JobInvocationDetail/__tests__/JobInvocationHostTablePolling.test.js +336 -0
  19. data/webpack/JobInvocationDetail/__tests__/JobInvocationPolling.test.js +223 -0
  20. data/webpack/JobInvocationDetail/__tests__/MainInformation.test.js +260 -111
  21. data/webpack/JobInvocationDetail/__tests__/TableToolbarActions.test.js +3 -1
  22. data/webpack/JobInvocationDetail/__tests__/TemplateInvocationPolling.test.js +254 -0
  23. data/webpack/JobInvocationDetail/__tests__/fixtures.js +2 -0
  24. data/webpack/JobInvocationDetail/__tests__/foremanTestHelpers.js +30 -0
  25. data/webpack/JobInvocationDetail/index.js +50 -30
  26. data/webpack/__mocks__/foremanReact/Root/Context/ForemanContext/index.js +15 -0
  27. data/webpack/__mocks__/foremanReact/common/hooks/Permissions/permissionHooks.js +1 -0
  28. data/webpack/__mocks__/foremanReact/redux/API/APISelectors.js +5 -0
  29. data/webpack/test_setup.js +1 -13
  30. 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 { stopInterval } from 'foremanReact/redux/middlewares/IntervalMiddleware';
15
- import { useAPI } from 'foremanReact/common/hooks/API/APIHooks';
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 { getJobInvocation, getTask } from './JobInvocationActions';
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
- statusLabel === STATUS.FAILED ||
51
- statusLabel === STATUS.SUCCEEDED ||
52
- statusLabel === STATUS.CANCELLED;
53
- const autoRefresh = task?.state === STATUS.PENDING || false;
54
- useAPI('get', currentPermissionsUrl, {
55
- key: CURRENT_PERMISSIONS,
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}?host_status=true`));
76
- if (finished && !autoRefresh) {
77
- dispatch(stopInterval(JOB_INVOCATION_KEY));
78
- }
84
+ dispatch(getJobInvocation(`/api/job_invocations/${id}`, pollTimeoutRef));
79
85
  return () => {
80
- dispatch(stopInterval(JOB_INVOCATION_KEY));
86
+ stopJobInvocationPolling(pollTimeoutRef);
81
87
  };
82
- }, [dispatch, id, finished, autoRefresh]);
88
+ }, [dispatch, id]);
83
89
 
84
- const taskId = task?.id;
85
- useEffect(() => {
86
- if (taskId !== undefined) {
87
- dispatch(getTask(`${taskId}`));
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
- }, [dispatch, taskId]);
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={<JobInvocationToolbarButtons jobId={id} data={items} />}
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
- statusLabel={statusLabel}
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
+ };
@@ -1,13 +1 @@
1
- import 'core-js/shim';
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: 16.6.5
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-06-23 00:00:00.000000000 Z
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.10
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.