foreman_remote_execution 17.2.1 → 18.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/foreign_input_sets_controller.rb +4 -0
- data/db/seeds.d/60-ssh_proxy_feature.rb +2 -2
- data/db/seeds.d/90-bookmarks.rb +1 -1
- data/db/seeds.d/95-mail_notifications.rb +1 -1
- data/lib/foreman_remote_execution/plugin.rb +1 -1
- data/lib/foreman_remote_execution/version.rb +1 -1
- data/webpack/JobInvocationDetail/JobInvocationConstants.js +10 -0
- data/webpack/JobInvocationDetail/JobInvocationHostTable.js +8 -1
- data/webpack/JobInvocationDetail/__tests__/JobInvocationHostTablePolling.test.js +113 -7
- data/webpack/JobInvocationDetail/__tests__/areAllHostsTerminal.test.js +40 -0
- data/webpack/JobWizard/steps/AdvancedFields/__tests__/AdvancedFields.test.js +191 -212
- metadata +4 -3
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 4b301e962ec24666ada0b4a297277232ee94dc74a36b86875f4ce2230659c254
|
|
4
|
+
data.tar.gz: 923c5679450846add2cfba3a6741654e7bfd06f23e9e33b8ad5f19dfd2b6c217
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 561489f3cee2bf28837084baa1fd3fb8b3a68b8acd632c0337f1b3204dc176acafefb85ecc15d058c464038ec38413a56899ce4ebd2d723596571622cb9c130f
|
|
7
|
+
data.tar.gz: '0938ab65b49a36d63fb709d905322559305ebd691f83bcf1aee8d81b9cb6cffd83db2fe50381295b7f589d37beddc457a7ee0aef3b8c02819f12008c9f461461'
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
f = Feature.where(:name => 'SSH').first_or_create
|
|
2
|
-
raise "Unable to create proxy feature: #{format_errors
|
|
2
|
+
raise "Unable to create proxy feature: #{SeedHelper.format_errors(f)}" if f.nil? || f.errors.any?
|
|
3
3
|
|
|
4
4
|
f = Feature.where(:name => 'Script').first_or_create
|
|
5
|
-
raise "Unable to create proxy feature: #{format_errors
|
|
5
|
+
raise "Unable to create proxy feature: #{SeedHelper.format_errors(f)}" if f.nil? || f.errors.any?
|
data/db/seeds.d/90-bookmarks.rb
CHANGED
|
@@ -15,6 +15,6 @@ Bookmark.without_auditing do
|
|
|
15
15
|
b = Bookmark.where(:name => input[:name], :controller => input[:controller]).first || Bookmark.new
|
|
16
16
|
b.attributes = attributes
|
|
17
17
|
b.save
|
|
18
|
-
raise "Unable to create bookmark: #{format_errors
|
|
18
|
+
raise "Unable to create bookmark: #{SeedHelper.format_errors(b)}" if b.errors.any?
|
|
19
19
|
end
|
|
20
20
|
end
|
|
@@ -18,7 +18,7 @@ notifications.each do |notification|
|
|
|
18
18
|
created_notification = RexMailNotification.create(notification)
|
|
19
19
|
if created_notification.nil? || created_notification.errors.any?
|
|
20
20
|
raise ::Foreman::Exception.new(N_("Unable to create mail notification: %s"),
|
|
21
|
-
format_errors(created_notification))
|
|
21
|
+
SeedHelper.format_errors(created_notification))
|
|
22
22
|
end
|
|
23
23
|
end
|
|
24
24
|
end
|
|
@@ -55,6 +55,16 @@ export const STATUS_TITLES = {
|
|
|
55
55
|
NOT_STARTED: { id: 'N/A', title: __('Scheduled') },
|
|
56
56
|
};
|
|
57
57
|
|
|
58
|
+
export const TERMINAL_HOST_STATUSES = new Set([
|
|
59
|
+
'success',
|
|
60
|
+
'error',
|
|
61
|
+
'cancelled',
|
|
62
|
+
]);
|
|
63
|
+
|
|
64
|
+
export const areAllHostsTerminal = results =>
|
|
65
|
+
results?.length > 0 &&
|
|
66
|
+
results.every(host => TERMINAL_HOST_STATUSES.has(host.job_status));
|
|
67
|
+
|
|
58
68
|
export const DATE_OPTIONS = {
|
|
59
69
|
day: 'numeric',
|
|
60
70
|
month: 'short',
|
|
@@ -42,6 +42,7 @@ import Columns, {
|
|
|
42
42
|
STATUS_UPPERCASE,
|
|
43
43
|
AWAITING_STATUS_FILTER,
|
|
44
44
|
AUTO_REFRESH_INTERVAL_MS,
|
|
45
|
+
areAllHostsTerminal,
|
|
45
46
|
} from './JobInvocationConstants';
|
|
46
47
|
import { TemplateInvocation } from './TemplateInvocation';
|
|
47
48
|
import { RowActions } from './TemplateInvocationComponents/TemplateActionButtons';
|
|
@@ -196,7 +197,13 @@ const JobInvocationHostTable = ({
|
|
|
196
197
|
if (thisRequest !== requestIdRef.current) return;
|
|
197
198
|
if (!mountedRef.current) return;
|
|
198
199
|
updateHostsState(data);
|
|
199
|
-
|
|
200
|
+
const hasActiveFilter = !!(
|
|
201
|
+
currentPollParams.current.search ||
|
|
202
|
+
currentPollParams.current.awaiting
|
|
203
|
+
);
|
|
204
|
+
const pageAllTerminal =
|
|
205
|
+
!hasActiveFilter && areAllHostsTerminal(data.data?.results);
|
|
206
|
+
if (!jobFinishedRef.current && !pageAllTerminal) {
|
|
200
207
|
pollTimeoutId.current = setTimeout(
|
|
201
208
|
() => makeApiCall(currentPollParams.current),
|
|
202
209
|
AUTO_REFRESH_INTERVAL_MS
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
/* eslint-disable max-lines */
|
|
1
2
|
import React from 'react';
|
|
2
3
|
import { render, act } from '@testing-library/react';
|
|
3
4
|
import '@testing-library/jest-dom';
|
|
@@ -44,13 +45,28 @@ const hostsResponse = {
|
|
|
44
45
|
operatingsystem_name: 'RHEL 9',
|
|
45
46
|
hostgroup_id: 1,
|
|
46
47
|
hostgroup_name: 'default',
|
|
47
|
-
job_status: '
|
|
48
|
+
job_status: 'running',
|
|
48
49
|
smart_proxy_id: 1,
|
|
49
50
|
smart_proxy_name: 'proxy1',
|
|
50
51
|
},
|
|
51
52
|
],
|
|
52
53
|
};
|
|
53
54
|
|
|
55
|
+
const terminalHostsResponse = {
|
|
56
|
+
...hostsResponse,
|
|
57
|
+
results: [{ ...hostsResponse.results[0], job_status: 'success' }],
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
const mixedHostsResponse = {
|
|
61
|
+
...hostsResponse,
|
|
62
|
+
total: 2,
|
|
63
|
+
subtotal: 2,
|
|
64
|
+
results: [
|
|
65
|
+
{ ...hostsResponse.results[0], id: 1, job_status: 'success' },
|
|
66
|
+
{ ...hostsResponse.results[0], id: 2, job_status: 'running' },
|
|
67
|
+
],
|
|
68
|
+
};
|
|
69
|
+
|
|
54
70
|
let apiGetSpy;
|
|
55
71
|
let hostsCalls;
|
|
56
72
|
let pendingCallbacks;
|
|
@@ -66,9 +82,9 @@ const flushPendingCallbacks = () => {
|
|
|
66
82
|
batch.forEach(cb => cb());
|
|
67
83
|
};
|
|
68
84
|
|
|
69
|
-
const renderTable = (props = {}) => {
|
|
85
|
+
const renderTable = (props = {}, historyEntries = ['/']) => {
|
|
70
86
|
const store = createStore();
|
|
71
|
-
const history = createMemoryHistory();
|
|
87
|
+
const history = createMemoryHistory({ initialEntries: historyEntries });
|
|
72
88
|
const Wrapper = createForemanContextWrapper();
|
|
73
89
|
|
|
74
90
|
const defaultProps = {
|
|
@@ -93,16 +109,14 @@ const renderTable = (props = {}) => {
|
|
|
93
109
|
return { ...result, store, history };
|
|
94
110
|
};
|
|
95
111
|
|
|
96
|
-
const setupSuccessMock = () => {
|
|
112
|
+
const setupSuccessMock = (response = hostsResponse) => {
|
|
97
113
|
apiGetSpy = jest
|
|
98
114
|
.spyOn(APIActions, 'get')
|
|
99
115
|
.mockImplementation(opts => dispatch => {
|
|
100
116
|
if (opts.key === JOB_INVOCATION_HOSTS) {
|
|
101
117
|
hostsCalls.push(opts);
|
|
102
118
|
if (opts.handleSuccess) {
|
|
103
|
-
pendingCallbacks.push(() =>
|
|
104
|
-
opts.handleSuccess({ data: hostsResponse })
|
|
105
|
-
);
|
|
119
|
+
pendingCallbacks.push(() => opts.handleSuccess({ data: response }));
|
|
106
120
|
}
|
|
107
121
|
}
|
|
108
122
|
});
|
|
@@ -311,6 +325,98 @@ describe('JobInvocationHostTable polling', () => {
|
|
|
311
325
|
expect(hostsCalls.length).toBeGreaterThan(callsAfterFilterChange);
|
|
312
326
|
});
|
|
313
327
|
|
|
328
|
+
it('stops polling when all hosts on the page are terminal', () => {
|
|
329
|
+
apiGetSpy.mockRestore();
|
|
330
|
+
setupSuccessMock(terminalHostsResponse);
|
|
331
|
+
|
|
332
|
+
renderTable();
|
|
333
|
+
|
|
334
|
+
expect(hostsCalls).toHaveLength(1);
|
|
335
|
+
|
|
336
|
+
act(() => {
|
|
337
|
+
flushPendingCallbacks();
|
|
338
|
+
});
|
|
339
|
+
|
|
340
|
+
act(() => {
|
|
341
|
+
jest.advanceTimersByTime(10000);
|
|
342
|
+
});
|
|
343
|
+
|
|
344
|
+
act(() => {
|
|
345
|
+
flushPendingCallbacks();
|
|
346
|
+
});
|
|
347
|
+
|
|
348
|
+
expect(hostsCalls).toHaveLength(1);
|
|
349
|
+
});
|
|
350
|
+
|
|
351
|
+
it('continues polling when at least one host on the page is non-terminal', () => {
|
|
352
|
+
apiGetSpy.mockRestore();
|
|
353
|
+
setupSuccessMock(mixedHostsResponse);
|
|
354
|
+
|
|
355
|
+
renderTable();
|
|
356
|
+
|
|
357
|
+
expect(hostsCalls).toHaveLength(1);
|
|
358
|
+
|
|
359
|
+
act(() => {
|
|
360
|
+
flushPendingCallbacks();
|
|
361
|
+
});
|
|
362
|
+
|
|
363
|
+
act(() => {
|
|
364
|
+
jest.advanceTimersByTime(5000);
|
|
365
|
+
});
|
|
366
|
+
|
|
367
|
+
act(() => {
|
|
368
|
+
flushPendingCallbacks();
|
|
369
|
+
});
|
|
370
|
+
|
|
371
|
+
expect(hostsCalls).toHaveLength(2);
|
|
372
|
+
});
|
|
373
|
+
|
|
374
|
+
it('continues polling when all hosts on the page are terminal but a status filter is active', () => {
|
|
375
|
+
apiGetSpy.mockRestore();
|
|
376
|
+
setupSuccessMock(terminalHostsResponse);
|
|
377
|
+
|
|
378
|
+
renderTable({ initialFilter: 'success' });
|
|
379
|
+
|
|
380
|
+
expect(hostsCalls).toHaveLength(1);
|
|
381
|
+
|
|
382
|
+
act(() => {
|
|
383
|
+
flushPendingCallbacks();
|
|
384
|
+
});
|
|
385
|
+
|
|
386
|
+
act(() => {
|
|
387
|
+
jest.advanceTimersByTime(5000);
|
|
388
|
+
});
|
|
389
|
+
|
|
390
|
+
act(() => {
|
|
391
|
+
flushPendingCallbacks();
|
|
392
|
+
});
|
|
393
|
+
|
|
394
|
+
expect(hostsCalls).toHaveLength(2);
|
|
395
|
+
});
|
|
396
|
+
|
|
397
|
+
it('continues polling when all hosts on the page are terminal but a search query is active', () => {
|
|
398
|
+
apiGetSpy.mockRestore();
|
|
399
|
+
setupSuccessMock(terminalHostsResponse);
|
|
400
|
+
|
|
401
|
+
renderTable({}, ['/?search=host1']);
|
|
402
|
+
|
|
403
|
+
expect(hostsCalls).toHaveLength(1);
|
|
404
|
+
|
|
405
|
+
act(() => {
|
|
406
|
+
flushPendingCallbacks();
|
|
407
|
+
});
|
|
408
|
+
|
|
409
|
+
act(() => {
|
|
410
|
+
jest.advanceTimersByTime(5000);
|
|
411
|
+
});
|
|
412
|
+
|
|
413
|
+
act(() => {
|
|
414
|
+
flushPendingCallbacks();
|
|
415
|
+
});
|
|
416
|
+
|
|
417
|
+
expect(hostsCalls).toHaveLength(2);
|
|
418
|
+
});
|
|
419
|
+
|
|
314
420
|
it('sends include_permissions only on the first request', () => {
|
|
315
421
|
renderTable();
|
|
316
422
|
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { areAllHostsTerminal } from '../JobInvocationConstants';
|
|
2
|
+
|
|
3
|
+
describe('areAllHostsTerminal', () => {
|
|
4
|
+
it('returns false for an empty results array', () => {
|
|
5
|
+
expect(areAllHostsTerminal([])).toBe(false);
|
|
6
|
+
});
|
|
7
|
+
|
|
8
|
+
it('returns false for undefined results', () => {
|
|
9
|
+
expect(areAllHostsTerminal(undefined)).toBe(false);
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
it('returns true when every host is in a terminal state', () => {
|
|
13
|
+
expect(
|
|
14
|
+
areAllHostsTerminal([
|
|
15
|
+
{ id: 1, job_status: 'success' },
|
|
16
|
+
{ id: 2, job_status: 'error' },
|
|
17
|
+
{ id: 3, job_status: 'cancelled' },
|
|
18
|
+
])
|
|
19
|
+
).toBe(true);
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
it('returns false when at least one host is non-terminal', () => {
|
|
23
|
+
expect(
|
|
24
|
+
areAllHostsTerminal([
|
|
25
|
+
{ id: 1, job_status: 'success' },
|
|
26
|
+
{ id: 2, job_status: 'running' },
|
|
27
|
+
])
|
|
28
|
+
).toBe(false);
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
it('returns false when every host is non-terminal', () => {
|
|
32
|
+
expect(
|
|
33
|
+
areAllHostsTerminal([
|
|
34
|
+
{ id: 1, job_status: 'running' },
|
|
35
|
+
{ id: 2, job_status: 'planned' },
|
|
36
|
+
{ id: 3, job_status: 'N/A' },
|
|
37
|
+
])
|
|
38
|
+
).toBe(false);
|
|
39
|
+
});
|
|
40
|
+
});
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
/* eslint-disable max-lines */
|
|
2
2
|
import React from 'react';
|
|
3
3
|
import { Provider } from 'react-redux';
|
|
4
|
-
import {
|
|
5
|
-
import
|
|
4
|
+
import { screen, render, act, fireEvent } from '@testing-library/react';
|
|
5
|
+
import userEvent from '@testing-library/user-event';
|
|
6
|
+
import '@testing-library/jest-dom';
|
|
6
7
|
import { MockedProvider } from '@apollo/client/testing';
|
|
7
8
|
import * as api from 'foremanReact/redux/API';
|
|
8
9
|
import { JobWizard } from '../../../JobWizard';
|
|
@@ -20,11 +21,55 @@ import { WIZARD_TITLES } from '../../../JobWizardConstants';
|
|
|
20
21
|
const lodash = require('lodash');
|
|
21
22
|
|
|
22
23
|
lodash.debounce = fn => fn;
|
|
24
|
+
|
|
23
25
|
const store = testSetup(selectors, api);
|
|
24
26
|
mockApi(api);
|
|
25
27
|
|
|
26
28
|
jest.useFakeTimers();
|
|
27
29
|
|
|
30
|
+
const clickOptions = { skipHover: true };
|
|
31
|
+
|
|
32
|
+
const wizardStepButton = title =>
|
|
33
|
+
screen.getByText(title, { selector: 'button' });
|
|
34
|
+
|
|
35
|
+
const clickWizardStep = async title => {
|
|
36
|
+
await act(async () => {
|
|
37
|
+
fireEvent.click(wizardStepButton(title));
|
|
38
|
+
jest.advanceTimersByTime(1000);
|
|
39
|
+
});
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
const typeIntoField = async (field, value) => {
|
|
43
|
+
await act(async () => {
|
|
44
|
+
await userEvent.click(field, {}, clickOptions);
|
|
45
|
+
await userEvent.clear(field);
|
|
46
|
+
userEvent.paste(field, value);
|
|
47
|
+
jest.advanceTimersByTime(1000);
|
|
48
|
+
});
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
const renderJobWizard = ({ customStore = store, withGql = true } = {}) => {
|
|
52
|
+
const wizard = (
|
|
53
|
+
<Provider store={customStore}>
|
|
54
|
+
<JobWizard />
|
|
55
|
+
</Provider>
|
|
56
|
+
);
|
|
57
|
+
|
|
58
|
+
if (withGql) {
|
|
59
|
+
return render(
|
|
60
|
+
<MockedProvider mocks={gqlMock} addTypename={false}>
|
|
61
|
+
{wizard}
|
|
62
|
+
</MockedProvider>
|
|
63
|
+
);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
return render(wizard);
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
const goToAdvancedStep = async () => {
|
|
70
|
+
await clickWizardStep(WIZARD_TITLES.advanced);
|
|
71
|
+
};
|
|
72
|
+
|
|
28
73
|
describe('AdvancedFields', () => {
|
|
29
74
|
beforeEach(() => {
|
|
30
75
|
jest.spyOn(selectors, 'selectRouterSearch');
|
|
@@ -32,87 +77,45 @@ describe('AdvancedFields', () => {
|
|
|
32
77
|
'host_ids[]': ['105', '37'],
|
|
33
78
|
}));
|
|
34
79
|
});
|
|
80
|
+
|
|
35
81
|
afterEach(() => {
|
|
36
82
|
selectors.selectRouterSearch.mockRestore();
|
|
37
83
|
});
|
|
38
|
-
it('should save data between steps for advanced fields', async () => {
|
|
39
|
-
const wrapper = mount(
|
|
40
|
-
<MockedProvider mocks={gqlMock} addTypename={false}>
|
|
41
|
-
<Provider store={store}>
|
|
42
|
-
<JobWizard />
|
|
43
|
-
</Provider>
|
|
44
|
-
</MockedProvider>
|
|
45
|
-
);
|
|
46
|
-
// setup
|
|
47
|
-
wrapper
|
|
48
|
-
.find('.pf-v5-c-button.pf-v5-c-select__toggle-button')
|
|
49
|
-
.simulate('click');
|
|
50
|
-
wrapper
|
|
51
|
-
.find('.pf-v5-c-select__menu-item')
|
|
52
|
-
.first()
|
|
53
|
-
.simulate('click');
|
|
54
|
-
|
|
55
|
-
// test
|
|
56
|
-
expect(
|
|
57
|
-
wrapper.find('.pf-v5-c-wizard__nav-link.pf-m-disabled')
|
|
58
|
-
).toHaveLength(0);
|
|
59
|
-
wrapper
|
|
60
|
-
.find('.pf-v5-c-wizard__nav-link')
|
|
61
|
-
.at(2)
|
|
62
|
-
.simulate('click'); // Advanced step
|
|
63
84
|
|
|
64
|
-
|
|
65
|
-
|
|
85
|
+
it('preserves advanced field values when navigating between wizard steps', async () => {
|
|
86
|
+
renderJobWizard();
|
|
87
|
+
await goToAdvancedStep();
|
|
88
|
+
|
|
89
|
+
const effectiveUserInput = screen.getByLabelText('effective user', {
|
|
90
|
+
selector: 'input',
|
|
91
|
+
});
|
|
92
|
+
const advancedTemplateInput = screen.getByLabelText('adv plain hidden', {
|
|
93
|
+
selector: 'textarea',
|
|
66
94
|
});
|
|
67
|
-
const
|
|
68
|
-
const advancedTemplateInput = () =>
|
|
69
|
-
wrapper.find('.pf-v5-c-form__group-control textarea');
|
|
70
|
-
const effectiveUesrValue = 'effective user new value';
|
|
95
|
+
const effectiveUserValue = 'effective user new value';
|
|
71
96
|
const advancedTemplateInputValue = 'advanced input new value';
|
|
72
|
-
effectiveUserInput().getDOMNode().value = effectiveUesrValue;
|
|
73
|
-
|
|
74
|
-
effectiveUserInput().simulate('change');
|
|
75
|
-
wrapper.update();
|
|
76
|
-
advancedTemplateInput().getDOMNode().value = advancedTemplateInputValue;
|
|
77
|
-
advancedTemplateInput().simulate('change');
|
|
78
|
-
expect(effectiveUserInput().prop('value')).toEqual(effectiveUesrValue);
|
|
79
|
-
expect(advancedTemplateInput().prop('value')).toEqual(
|
|
80
|
-
advancedTemplateInputValue
|
|
81
|
-
);
|
|
82
97
|
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
.at(1)
|
|
86
|
-
.simulate('click');
|
|
98
|
+
await typeIntoField(effectiveUserInput, effectiveUserValue);
|
|
99
|
+
await typeIntoField(advancedTemplateInput, advancedTemplateInputValue);
|
|
87
100
|
|
|
88
|
-
expect(
|
|
89
|
-
|
|
90
|
-
).toEqual('Target hosts and inputs');
|
|
91
|
-
wrapper
|
|
92
|
-
.find('.pf-v5-c-wizard__nav-link')
|
|
93
|
-
.at(2)
|
|
94
|
-
.simulate('click'); // Advanced step
|
|
101
|
+
expect(effectiveUserInput).toHaveValue(effectiveUserValue);
|
|
102
|
+
expect(advancedTemplateInput).toHaveValue(advancedTemplateInputValue);
|
|
95
103
|
|
|
96
|
-
await
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
expect(advancedTemplateInput().prop('value')).toEqual(
|
|
101
|
-
advancedTemplateInputValue
|
|
104
|
+
await clickWizardStep(WIZARD_TITLES.hostsAndInputs);
|
|
105
|
+
expect(wizardStepButton(WIZARD_TITLES.hostsAndInputs)).toHaveAttribute(
|
|
106
|
+
'aria-current',
|
|
107
|
+
'step'
|
|
102
108
|
);
|
|
109
|
+
|
|
110
|
+
await goToAdvancedStep();
|
|
111
|
+
|
|
112
|
+
expect(effectiveUserInput).toHaveValue(effectiveUserValue);
|
|
113
|
+
expect(advancedTemplateInput).toHaveValue(advancedTemplateInputValue);
|
|
103
114
|
});
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
<JobWizard />
|
|
109
|
-
</Provider>
|
|
110
|
-
</MockedProvider>
|
|
111
|
-
);
|
|
112
|
-
await act(async () => {
|
|
113
|
-
fireEvent.click(screen.getByText(WIZARD_TITLES.advanced));
|
|
114
|
-
jest.advanceTimersByTime(1000); // to handle pf4 date picker popover
|
|
115
|
-
});
|
|
115
|
+
|
|
116
|
+
it('preserves template field values when navigating between wizard steps', async () => {
|
|
117
|
+
renderJobWizard();
|
|
118
|
+
await goToAdvancedStep();
|
|
116
119
|
|
|
117
120
|
const searchValue = 'search test';
|
|
118
121
|
const textValue = 'I am a text';
|
|
@@ -131,134 +134,126 @@ describe('AdvancedFields', () => {
|
|
|
131
134
|
|
|
132
135
|
fireEvent.click(selectField);
|
|
133
136
|
await act(async () => {
|
|
134
|
-
await
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
});
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
fireEvent.change(timeField, {
|
|
149
|
-
target: { value: timeValue },
|
|
150
|
-
});
|
|
151
|
-
jest.advanceTimersByTime(1000); // to handle pf4 date picker popover
|
|
152
|
-
});
|
|
153
|
-
expect(
|
|
154
|
-
screen.getByLabelText('adv plain hidden', {
|
|
155
|
-
selector: 'textarea',
|
|
156
|
-
}).value
|
|
157
|
-
).toBe(textValue);
|
|
158
|
-
expect(searchField.value).toBe(searchValue);
|
|
159
|
-
expect(screen.getByLabelText('adv date datepicker').value).toBe(dateValue);
|
|
160
|
-
expect(timeField.value).toBe(timeValue);
|
|
161
|
-
await act(async () => {
|
|
162
|
-
fireEvent.click(screen.getByText(WIZARD_TITLES.categoryAndTemplate));
|
|
137
|
+
await userEvent.click(screen.getByText('option 2'), {}, clickOptions);
|
|
138
|
+
await userEvent.click(
|
|
139
|
+
screen.getAllByText(WIZARD_TITLES.advanced)[0],
|
|
140
|
+
{},
|
|
141
|
+
clickOptions
|
|
142
|
+
);
|
|
143
|
+
|
|
144
|
+
await userEvent.click(resourceSelectField, {}, clickOptions);
|
|
145
|
+
await userEvent.click(screen.getByText('resource2'), {}, clickOptions);
|
|
146
|
+
await typeIntoField(textField, textValue);
|
|
147
|
+
await typeIntoField(searchField, searchValue);
|
|
148
|
+
fireEvent.change(dateField, { target: { value: dateValue } });
|
|
149
|
+
fireEvent.change(timeField, { target: { value: timeValue } });
|
|
150
|
+
jest.advanceTimersByTime(1000);
|
|
163
151
|
});
|
|
152
|
+
|
|
153
|
+
expect(textField).toHaveValue(textValue);
|
|
154
|
+
expect(searchField).toHaveValue(searchValue);
|
|
155
|
+
expect(dateField).toHaveValue(dateValue);
|
|
156
|
+
expect(timeField).toHaveValue(timeValue);
|
|
157
|
+
|
|
158
|
+
await clickWizardStep(WIZARD_TITLES.categoryAndTemplate);
|
|
164
159
|
expect(screen.getAllByText(WIZARD_TITLES.categoryAndTemplate)).toHaveLength(
|
|
165
160
|
3
|
|
166
161
|
);
|
|
167
162
|
|
|
168
|
-
await
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
expect(
|
|
173
|
-
expect(
|
|
174
|
-
expect(
|
|
175
|
-
expect(
|
|
176
|
-
expect(screen.
|
|
177
|
-
expect(screen.
|
|
178
|
-
expect(screen.queryAllByDisplayValue('resource1')).toHaveLength(0);
|
|
179
|
-
expect(screen.queryAllByDisplayValue('resource2')).toHaveLength(1);
|
|
163
|
+
await goToAdvancedStep();
|
|
164
|
+
|
|
165
|
+
expect(textField).toHaveValue(textValue);
|
|
166
|
+
expect(searchField).toHaveValue(searchValue);
|
|
167
|
+
expect(dateField).toHaveValue(dateValue);
|
|
168
|
+
expect(timeField).toHaveValue(timeValue);
|
|
169
|
+
expect(screen.queryByText('option 1')).not.toBeInTheDocument();
|
|
170
|
+
expect(screen.getByText('option 2')).toBeInTheDocument();
|
|
171
|
+
expect(screen.queryByDisplayValue('resource1')).not.toBeInTheDocument();
|
|
172
|
+
expect(screen.getByDisplayValue('resource2')).toBeInTheDocument();
|
|
180
173
|
});
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
<JobWizard />
|
|
186
|
-
</Provider>
|
|
187
|
-
</MockedProvider>
|
|
188
|
-
);
|
|
189
|
-
await act(async () => {
|
|
190
|
-
fireEvent.click(screen.getByText(WIZARD_TITLES.advanced));
|
|
191
|
-
jest.advanceTimersByTime(1000); // to handle pf4 date picker popover
|
|
192
|
-
});
|
|
174
|
+
|
|
175
|
+
it('fills default values into advanced fields', async () => {
|
|
176
|
+
renderJobWizard();
|
|
177
|
+
await goToAdvancedStep();
|
|
193
178
|
|
|
194
179
|
expect(
|
|
195
|
-
screen.getByLabelText('ssh user', {
|
|
196
|
-
|
|
197
|
-
}).value
|
|
198
|
-
).toBe('');
|
|
180
|
+
screen.getByLabelText('ssh user', { selector: 'input' })
|
|
181
|
+
).toHaveValue('');
|
|
199
182
|
expect(
|
|
200
|
-
screen.getByLabelText('effective user', {
|
|
201
|
-
|
|
202
|
-
}).value
|
|
203
|
-
).toBe('default effective user');
|
|
183
|
+
screen.getByLabelText('effective user', { selector: 'input' })
|
|
184
|
+
).toHaveValue('default effective user');
|
|
204
185
|
expect(
|
|
205
|
-
screen.getByLabelText('timeout to kill', {
|
|
206
|
-
|
|
207
|
-
}).value
|
|
208
|
-
).toBe('2');
|
|
209
|
-
|
|
186
|
+
screen.getByLabelText('timeout to kill', { selector: 'input' })
|
|
187
|
+
).toHaveValue('2');
|
|
210
188
|
expect(
|
|
211
|
-
screen.getByLabelText('description preview', {
|
|
212
|
-
|
|
213
|
-
}).value
|
|
214
|
-
).toBe(
|
|
189
|
+
screen.getByLabelText('description preview', { selector: 'input' })
|
|
190
|
+
).toHaveValue(
|
|
215
191
|
'template1 with inputs adv plain hidden="Default val" adv plain select="" adv resource select="" adv search="" adv date="" plain hidden="Default val"'
|
|
216
192
|
);
|
|
217
193
|
});
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
194
|
+
|
|
195
|
+
it('renders advanced fields step with expected form fields', async () => {
|
|
196
|
+
renderJobWizard();
|
|
197
|
+
await goToAdvancedStep();
|
|
198
|
+
|
|
199
|
+
expect(
|
|
200
|
+
screen.getByLabelText('adv plain hidden', { selector: 'textarea' })
|
|
201
|
+
).toBeInTheDocument();
|
|
202
|
+
expect(
|
|
203
|
+
screen.getByLabelText('adv plain select toggle')
|
|
204
|
+
).toBeInTheDocument();
|
|
205
|
+
expect(
|
|
206
|
+
screen.getByLabelText('adv resource select toggle')
|
|
207
|
+
).toBeInTheDocument();
|
|
208
|
+
expect(
|
|
209
|
+
screen.getByLabelText('ssh user', { selector: 'input' })
|
|
210
|
+
).toBeInTheDocument();
|
|
211
|
+
expect(
|
|
212
|
+
screen.getByLabelText('effective user', { selector: 'input' })
|
|
213
|
+
).toBeInTheDocument();
|
|
214
|
+
expect(
|
|
215
|
+
screen.getByLabelText('description preview', { selector: 'input' })
|
|
216
|
+
).toBeInTheDocument();
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
it('updates description preview when editing the description template', async () => {
|
|
220
|
+
renderJobWizard({ withGql: false });
|
|
221
|
+
await goToAdvancedStep();
|
|
228
222
|
|
|
229
223
|
const textField = screen.getByLabelText('adv plain hidden', {
|
|
230
224
|
selector: 'textarea',
|
|
231
225
|
});
|
|
232
|
-
await
|
|
233
|
-
|
|
234
|
-
target: { value: 'test command' },
|
|
235
|
-
});
|
|
236
|
-
});
|
|
226
|
+
await typeIntoField(textField, 'test command');
|
|
227
|
+
|
|
237
228
|
const descriptionValue = 'Run %{adv plain hidden} %{wrong command name}';
|
|
238
229
|
|
|
239
230
|
await act(async () => {
|
|
240
|
-
|
|
231
|
+
await userEvent.click(
|
|
232
|
+
screen.getByText('Edit job description template'),
|
|
233
|
+
{},
|
|
234
|
+
clickOptions
|
|
235
|
+
);
|
|
241
236
|
});
|
|
242
237
|
|
|
243
238
|
const editText = screen.getByLabelText('description edit', {
|
|
244
239
|
selector: 'input',
|
|
245
240
|
});
|
|
246
|
-
await
|
|
247
|
-
target: { value: descriptionValue },
|
|
248
|
-
});
|
|
241
|
+
await typeIntoField(editText, descriptionValue);
|
|
249
242
|
await act(async () => {
|
|
250
|
-
|
|
243
|
+
await userEvent.click(
|
|
244
|
+
screen.getByText('Preview job description'),
|
|
245
|
+
{},
|
|
246
|
+
clickOptions
|
|
247
|
+
);
|
|
251
248
|
});
|
|
249
|
+
|
|
252
250
|
expect(
|
|
253
|
-
screen.getByLabelText('description preview', {
|
|
254
|
-
|
|
255
|
-
}).value
|
|
256
|
-
).toBe('Run test command %{wrong command name}');
|
|
251
|
+
screen.getByLabelText('description preview', { selector: 'input' })
|
|
252
|
+
).toHaveValue('Run test command %{wrong command name}');
|
|
257
253
|
});
|
|
258
254
|
|
|
259
|
-
it('
|
|
255
|
+
it('shows template name in description preview when template has no inputs', async () => {
|
|
260
256
|
jest.spyOn(api, 'get');
|
|
261
|
-
|
|
262
257
|
jest.spyOn(selectors, 'selectTemplateInputs');
|
|
263
258
|
jest.spyOn(selectors, 'selectAdvancedTemplateInputs');
|
|
264
259
|
selectors.selectTemplateInputs.mockImplementation(() => []);
|
|
@@ -289,23 +284,20 @@ describe('AdvancedFields', () => {
|
|
|
289
284
|
}
|
|
290
285
|
return { type: 'get', ...action };
|
|
291
286
|
});
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
);
|
|
297
|
-
await act(async () => {
|
|
298
|
-
fireEvent.click(screen.getByText(WIZARD_TITLES.advanced));
|
|
299
|
-
jest.advanceTimersByTime(1000); // to handle pf4 date picker popover
|
|
300
|
-
});
|
|
287
|
+
|
|
288
|
+
renderJobWizard({ withGql: false });
|
|
289
|
+
await goToAdvancedStep();
|
|
290
|
+
|
|
301
291
|
expect(
|
|
302
|
-
screen.getByLabelText('description preview', {
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
292
|
+
screen.getByLabelText('description preview', { selector: 'input' })
|
|
293
|
+
).toHaveValue('template1');
|
|
294
|
+
|
|
295
|
+
selectors.selectTemplateInputs.mockRestore();
|
|
296
|
+
selectors.selectAdvancedTemplateInputs.mockRestore();
|
|
297
|
+
api.get.mockRestore();
|
|
306
298
|
});
|
|
307
299
|
|
|
308
|
-
it('
|
|
300
|
+
it('uses description_format for description preview', async () => {
|
|
309
301
|
jest.spyOn(api, 'get');
|
|
310
302
|
jest.spyOn(selectors, 'selectTemplateInputs');
|
|
311
303
|
selectors.selectTemplateInputs.mockImplementation(() => [
|
|
@@ -334,7 +326,6 @@ describe('AdvancedFields', () => {
|
|
|
334
326
|
...jobTemplateResponse.job_template,
|
|
335
327
|
description_format: 'Run %{command}',
|
|
336
328
|
},
|
|
337
|
-
|
|
338
329
|
template_inputs: [
|
|
339
330
|
{
|
|
340
331
|
name: 'command',
|
|
@@ -363,46 +354,34 @@ describe('AdvancedFields', () => {
|
|
|
363
354
|
}
|
|
364
355
|
return { type: 'get', ...action };
|
|
365
356
|
});
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
);
|
|
371
|
-
await act(async () => {
|
|
372
|
-
fireEvent.click(screen.getByText(WIZARD_TITLES.advanced));
|
|
373
|
-
jest.advanceTimersByTime(1000); // to handle pf4 date picker popover
|
|
374
|
-
});
|
|
357
|
+
|
|
358
|
+
renderJobWizard({ withGql: false });
|
|
359
|
+
await goToAdvancedStep();
|
|
360
|
+
|
|
375
361
|
expect(
|
|
376
|
-
screen.getByLabelText('description preview', {
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
362
|
+
screen.getByLabelText('description preview', { selector: 'input' })
|
|
363
|
+
).toHaveValue('Run Default val');
|
|
364
|
+
|
|
365
|
+
selectors.selectTemplateInputs.mockRestore();
|
|
366
|
+
api.get.mockRestore();
|
|
380
367
|
});
|
|
381
368
|
|
|
382
|
-
it('search
|
|
383
|
-
jest.useFakeTimers();
|
|
384
|
-
mockApi(api);
|
|
369
|
+
it('dispatches resource search when typing in resource select', async () => {
|
|
385
370
|
const newStore = testSetup(selectors, api);
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
await act(async () => {
|
|
392
|
-
fireEvent.click(screen.getByText(WIZARD_TITLES.advanced));
|
|
393
|
-
jest.advanceTimersByTime(1000); // to handle pf4 date picker popover
|
|
394
|
-
});
|
|
371
|
+
mockApi(api);
|
|
372
|
+
|
|
373
|
+
renderJobWizard({ customStore: newStore, withGql: false });
|
|
374
|
+
await goToAdvancedStep();
|
|
375
|
+
|
|
395
376
|
const resourceSelectField = screen.getByLabelText(
|
|
396
377
|
'adv resource select typeahead input'
|
|
397
378
|
);
|
|
398
379
|
|
|
380
|
+
await typeIntoField(resourceSelectField, 'some search');
|
|
399
381
|
await act(async () => {
|
|
400
|
-
await fireEvent.change(resourceSelectField, {
|
|
401
|
-
target: { value: 'some search' },
|
|
402
|
-
});
|
|
403
|
-
|
|
404
382
|
jest.advanceTimersByTime(10000);
|
|
405
383
|
});
|
|
384
|
+
|
|
406
385
|
const actions = newStore.getActions();
|
|
407
386
|
const resourceSearchAction = actions.filter(
|
|
408
387
|
action => action.key === 'ForemanTasksTask'
|
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: 18.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-09-
|
|
10
|
+
date: 2026-09-15 00:00:00.000000000 Z
|
|
11
11
|
dependencies:
|
|
12
12
|
- !ruby/object:Gem::Dependency
|
|
13
13
|
name: deface
|
|
@@ -405,6 +405,7 @@ files:
|
|
|
405
405
|
- webpack/JobInvocationDetail/__tests__/TableToolbarActions.test.js
|
|
406
406
|
- webpack/JobInvocationDetail/__tests__/TemplateInvocation.test.js
|
|
407
407
|
- webpack/JobInvocationDetail/__tests__/TemplateInvocationPolling.test.js
|
|
408
|
+
- webpack/JobInvocationDetail/__tests__/areAllHostsTerminal.test.js
|
|
408
409
|
- webpack/JobInvocationDetail/__tests__/fixtures.js
|
|
409
410
|
- webpack/JobInvocationDetail/__tests__/foremanTestHelpers.js
|
|
410
411
|
- webpack/JobInvocationDetail/index.js
|
|
@@ -566,7 +567,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
566
567
|
- !ruby/object:Gem::Version
|
|
567
568
|
version: '0'
|
|
568
569
|
requirements: []
|
|
569
|
-
rubygems_version: 4.0.
|
|
570
|
+
rubygems_version: 4.0.20
|
|
570
571
|
specification_version: 4
|
|
571
572
|
summary: A plugin bringing remote execution to the Foreman, completing the config
|
|
572
573
|
management functionality with remote management functionality.
|