foreman_remote_execution 16.3.1 → 16.4.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 +3 -5
- data/app/lib/proxy_api/remote_execution_ssh.rb +9 -0
- data/app/models/concerns/foreman_remote_execution/host_extensions.rb +12 -4
- data/app/models/concerns/foreman_remote_execution/smart_proxy_extensions.rb +14 -0
- data/app/views/api/v2/smart_proxies/ca_pubkey.json.rabl +1 -0
- data/db/migrate/20250606125543_add_ca_pub_key_to_smart_proxy.rb +5 -0
- data/lib/foreman_remote_execution/plugin.rb +1 -0
- data/lib/foreman_remote_execution/version.rb +1 -1
- data/test/unit/concerns/host_extensions_test.rb +43 -0
- data/webpack/JobInvocationDetail/JobInvocationDetail.scss +4 -0
- data/webpack/JobInvocationDetail/JobInvocationHostTable.js +216 -129
- data/webpack/JobInvocationDetail/TemplateInvocation.js +19 -15
- data/webpack/JobInvocationDetail/TemplateInvocationComponents/OutputToggleGroup.js +43 -25
- data/webpack/JobInvocationDetail/TemplateInvocationPage.js +16 -1
- data/webpack/JobInvocationDetail/__tests__/TemplateInvocation.test.js +114 -72
- data/webpack/JobInvocationDetail/index.js +4 -5
- data/webpack/JobWizard/JobWizard.js +11 -10
- data/webpack/JobWizard/autofill.js +10 -2
- data/webpack/JobWizard/steps/AdvancedFields/__tests__/__snapshots__/AdvancedFields.test.js.snap +1 -1
- data/webpack/JobWizard/steps/CategoryAndTemplate/index.js +25 -13
- data/webpack/JobWizard/steps/HostsAndInputs/SelectAPI.js +16 -11
- data/webpack/JobWizard/steps/form/ResourceSelect.js +18 -16
- data/webpack/JobWizard/steps/form/SearchSelect.js +6 -7
- data/webpack/JobWizard/validation.js +1 -3
- data/webpack/react_app/components/TargetingHosts/index.js +49 -32
- metadata +5 -3
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import React, {
|
|
1
|
+
import React, { useEffect, useRef } from 'react';
|
|
2
2
|
import { isEmpty } from 'lodash';
|
|
3
3
|
import PropTypes from 'prop-types';
|
|
4
4
|
import { ClipboardCopyButton, Alert, Skeleton } from '@patternfly/react-core';
|
|
@@ -60,6 +60,12 @@ export const TemplateInvocation = ({
|
|
|
60
60
|
isExpanded,
|
|
61
61
|
hostName,
|
|
62
62
|
hostProxy,
|
|
63
|
+
showOutputType,
|
|
64
|
+
setShowOutputType,
|
|
65
|
+
showTemplatePreview,
|
|
66
|
+
setShowTemplatePreview,
|
|
67
|
+
showCommand,
|
|
68
|
+
setShowCommand,
|
|
63
69
|
}) => {
|
|
64
70
|
const intervalRef = useRef(null);
|
|
65
71
|
const templateURL = showTemplateInvocationUrl(hostID, jobID);
|
|
@@ -74,14 +80,6 @@ export const TemplateInvocation = ({
|
|
|
74
80
|
responseRef.current = response;
|
|
75
81
|
}, [response]);
|
|
76
82
|
|
|
77
|
-
const [showOutputType, setShowOutputType] = useState({
|
|
78
|
-
stderr: true,
|
|
79
|
-
stdout: true,
|
|
80
|
-
debug: true,
|
|
81
|
-
});
|
|
82
|
-
const [showTemplatePreview, setShowTemplatePreview] = useState(false);
|
|
83
|
-
const [showCommand, setShowCommand] = useState(false);
|
|
84
|
-
|
|
85
83
|
useEffect(() => {
|
|
86
84
|
const dispatchFetch = () => {
|
|
87
85
|
dispatch(
|
|
@@ -123,12 +121,8 @@ export const TemplateInvocation = ({
|
|
|
123
121
|
};
|
|
124
122
|
}, [isExpanded, dispatch, templateURL, hostID]);
|
|
125
123
|
|
|
126
|
-
if (!
|
|
127
|
-
return
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
if ((status === STATUS.PENDING && isEmpty(response)) || !response) {
|
|
131
|
-
return <Skeleton />;
|
|
124
|
+
if (!response || (status === STATUS.PENDING && isEmpty(response))) {
|
|
125
|
+
return <Skeleton data-testid="template-invocation-skeleton" />;
|
|
132
126
|
}
|
|
133
127
|
|
|
134
128
|
const errorMessage =
|
|
@@ -239,6 +233,16 @@ TemplateInvocation.propTypes = {
|
|
|
239
233
|
jobID: PropTypes.oneOfType([PropTypes.string, PropTypes.number]).isRequired,
|
|
240
234
|
isInTableView: PropTypes.bool,
|
|
241
235
|
isExpanded: PropTypes.bool,
|
|
236
|
+
showOutputType: PropTypes.shape({
|
|
237
|
+
stderr: PropTypes.bool,
|
|
238
|
+
stdout: PropTypes.bool,
|
|
239
|
+
debug: PropTypes.bool,
|
|
240
|
+
}).isRequired,
|
|
241
|
+
setShowOutputType: PropTypes.func.isRequired,
|
|
242
|
+
showTemplatePreview: PropTypes.bool.isRequired,
|
|
243
|
+
setShowTemplatePreview: PropTypes.func.isRequired,
|
|
244
|
+
showCommand: PropTypes.bool.isRequired,
|
|
245
|
+
setShowCommand: PropTypes.func.isRequired,
|
|
242
246
|
};
|
|
243
247
|
|
|
244
248
|
TemplateInvocation.defaultProps = {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import React from 'react';
|
|
1
|
+
import React, { useCallback } from 'react';
|
|
2
2
|
import PropTypes from 'prop-types';
|
|
3
3
|
import {
|
|
4
4
|
ToggleGroup,
|
|
@@ -27,31 +27,49 @@ export const OutputToggleGroup = ({
|
|
|
27
27
|
taskCancellable,
|
|
28
28
|
permissions,
|
|
29
29
|
}) => {
|
|
30
|
-
const handleSTDERRClick =
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
30
|
+
const handleSTDERRClick = useCallback(
|
|
31
|
+
_isSelected => {
|
|
32
|
+
setShowOutputType(prevShowOutputType => ({
|
|
33
|
+
...prevShowOutputType,
|
|
34
|
+
stderr: _isSelected,
|
|
35
|
+
}));
|
|
36
|
+
},
|
|
37
|
+
[setShowOutputType]
|
|
38
|
+
);
|
|
36
39
|
|
|
37
|
-
const handleSTDOUTClick =
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
setShowOutputType
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
40
|
+
const handleSTDOUTClick = useCallback(
|
|
41
|
+
_isSelected => {
|
|
42
|
+
setShowOutputType(prevShowOutputType => ({
|
|
43
|
+
...prevShowOutputType,
|
|
44
|
+
stdout: _isSelected,
|
|
45
|
+
}));
|
|
46
|
+
},
|
|
47
|
+
[setShowOutputType]
|
|
48
|
+
);
|
|
49
|
+
|
|
50
|
+
const handleDEBUGClick = useCallback(
|
|
51
|
+
_isSelected => {
|
|
52
|
+
setShowOutputType(prevShowOutputType => ({
|
|
53
|
+
...prevShowOutputType,
|
|
54
|
+
debug: _isSelected,
|
|
55
|
+
}));
|
|
56
|
+
},
|
|
57
|
+
[setShowOutputType]
|
|
58
|
+
);
|
|
59
|
+
|
|
60
|
+
const handlePreviewTemplateClick = useCallback(
|
|
61
|
+
_isSelected => {
|
|
62
|
+
setShowTemplatePreview(_isSelected);
|
|
63
|
+
},
|
|
64
|
+
[setShowTemplatePreview]
|
|
65
|
+
);
|
|
66
|
+
|
|
67
|
+
const handleCommandClick = useCallback(
|
|
68
|
+
_isSelected => {
|
|
69
|
+
setShowCommand(_isSelected);
|
|
70
|
+
},
|
|
71
|
+
[setShowCommand]
|
|
72
|
+
);
|
|
55
73
|
|
|
56
74
|
const toggleGroupItems = {
|
|
57
75
|
stderr: {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import React from 'react';
|
|
1
|
+
import React, { useState } from 'react';
|
|
2
2
|
import PropTypes from 'prop-types';
|
|
3
3
|
import { useSelector } from 'react-redux';
|
|
4
4
|
import PageLayout from 'foremanReact/routes/common/PageLayout/PageLayout';
|
|
@@ -26,6 +26,15 @@ const TemplateInvocationPage = ({
|
|
|
26
26
|
],
|
|
27
27
|
isPf4: true,
|
|
28
28
|
};
|
|
29
|
+
|
|
30
|
+
const [showOutputType, setShowOutputType] = useState({
|
|
31
|
+
stderr: true,
|
|
32
|
+
stdout: true,
|
|
33
|
+
debug: true,
|
|
34
|
+
});
|
|
35
|
+
const [showTemplatePreview, setShowTemplatePreview] = useState(false);
|
|
36
|
+
const [showCommand, setShowCommand] = useState(false);
|
|
37
|
+
|
|
29
38
|
return (
|
|
30
39
|
<PageLayout
|
|
31
40
|
header={description}
|
|
@@ -39,6 +48,12 @@ const TemplateInvocationPage = ({
|
|
|
39
48
|
isExpanded
|
|
40
49
|
hostName={hostName}
|
|
41
50
|
hostProxy={hostProxy}
|
|
51
|
+
showOutputType={showOutputType}
|
|
52
|
+
setShowOutputType={setShowOutputType}
|
|
53
|
+
showTemplatePreview={showTemplatePreview}
|
|
54
|
+
setShowTemplatePreview={setShowTemplatePreview}
|
|
55
|
+
showCommand={showCommand}
|
|
56
|
+
setShowCommand={setShowCommand}
|
|
42
57
|
/>
|
|
43
58
|
</PageLayout>
|
|
44
59
|
);
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
2
|
import configureMockStore from 'redux-mock-store';
|
|
3
3
|
import { Provider } from 'react-redux';
|
|
4
|
-
import { render, screen,
|
|
4
|
+
import { render, screen, fireEvent } from '@testing-library/react';
|
|
5
5
|
import '@testing-library/jest-dom/extend-expect';
|
|
6
6
|
import * as api from 'foremanReact/redux/API';
|
|
7
7
|
import * as selectors from '../JobInvocationSelectors';
|
|
@@ -10,12 +10,7 @@ import { mockTemplateInvocationResponse } from './fixtures';
|
|
|
10
10
|
|
|
11
11
|
jest.spyOn(api, 'get');
|
|
12
12
|
jest.mock('../JobInvocationSelectors');
|
|
13
|
-
|
|
14
|
-
'RESOLVED'
|
|
15
|
-
);
|
|
16
|
-
selectors.selectTemplateInvocation.mockImplementation(() => () =>
|
|
17
|
-
mockTemplateInvocationResponse
|
|
18
|
-
);
|
|
13
|
+
|
|
19
14
|
const mockStore = configureMockStore([]);
|
|
20
15
|
const store = mockStore({
|
|
21
16
|
HOSTS_API: {
|
|
@@ -24,79 +19,122 @@ const store = mockStore({
|
|
|
24
19
|
},
|
|
25
20
|
},
|
|
26
21
|
});
|
|
22
|
+
|
|
23
|
+
Object.assign(navigator, {
|
|
24
|
+
clipboard: {
|
|
25
|
+
writeText: jest.fn().mockResolvedValue(undefined),
|
|
26
|
+
},
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
const mockProps = {
|
|
30
|
+
hostID: '1',
|
|
31
|
+
jobID: '1',
|
|
32
|
+
isInTableView: false,
|
|
33
|
+
isExpanded: true,
|
|
34
|
+
hostName: 'example-host',
|
|
35
|
+
hostProxy: { name: 'example-proxy', href: '#' },
|
|
36
|
+
showOutputType: { stderr: true, stdout: true, debug: true },
|
|
37
|
+
setShowOutputType: jest.fn(),
|
|
38
|
+
showTemplatePreview: false,
|
|
39
|
+
setShowTemplatePreview: jest.fn(),
|
|
40
|
+
showCommand: false,
|
|
41
|
+
setShowCommand: jest.fn(),
|
|
42
|
+
};
|
|
43
|
+
|
|
27
44
|
describe('TemplateInvocation', () => {
|
|
28
|
-
|
|
45
|
+
beforeEach(() => {
|
|
46
|
+
selectors.selectTemplateInvocationStatus.mockImplementation(() => () =>
|
|
47
|
+
'RESOLVED'
|
|
48
|
+
);
|
|
49
|
+
selectors.selectTemplateInvocation.mockImplementation(() => () =>
|
|
50
|
+
mockTemplateInvocationResponse
|
|
51
|
+
);
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
test('render', () => {
|
|
29
55
|
render(
|
|
30
56
|
<Provider store={store}>
|
|
31
|
-
<TemplateInvocation
|
|
32
|
-
hostID="1"
|
|
33
|
-
jobID="1"
|
|
34
|
-
isInTableView={false}
|
|
35
|
-
isExpanded
|
|
36
|
-
hostName="example-host"
|
|
37
|
-
hostProxy={{ name: 'example-proxy', href: '#' }}
|
|
38
|
-
/>
|
|
57
|
+
<TemplateInvocation {...mockProps} />
|
|
39
58
|
</Provider>
|
|
40
59
|
);
|
|
41
60
|
|
|
42
61
|
expect(screen.getByText('example-host')).toBeInTheDocument();
|
|
43
62
|
expect(screen.getByText('example-proxy')).toBeInTheDocument();
|
|
44
|
-
|
|
45
63
|
expect(screen.getByText(/using Smart Proxy/)).toBeInTheDocument();
|
|
46
64
|
expect(screen.getByText(/Target:/)).toBeInTheDocument();
|
|
47
|
-
|
|
48
65
|
expect(screen.getByText('This is red text')).toBeInTheDocument();
|
|
49
66
|
expect(screen.getByText('This is default text')).toBeInTheDocument();
|
|
67
|
+
expect(screen.getByLabelText('Copy to clipboard')).toBeInTheDocument();
|
|
50
68
|
});
|
|
51
|
-
|
|
52
|
-
|
|
69
|
+
|
|
70
|
+
test('shows "No output" message when all toggles are off', () => {
|
|
71
|
+
const { rerender } = render(
|
|
53
72
|
<Provider store={store}>
|
|
54
|
-
<TemplateInvocation
|
|
55
|
-
hostID="1"
|
|
56
|
-
jobID="1"
|
|
57
|
-
isInTableView={false}
|
|
58
|
-
isExpanded
|
|
59
|
-
hostName="example-host"
|
|
60
|
-
hostProxy={{ name: 'example-proxy', href: '#' }}
|
|
61
|
-
/>
|
|
73
|
+
<TemplateInvocation {...mockProps} />
|
|
62
74
|
</Provider>
|
|
63
75
|
);
|
|
64
76
|
|
|
65
|
-
act(() => {
|
|
66
|
-
fireEvent.click(screen.getByText('STDOUT'));
|
|
67
|
-
fireEvent.click(screen.getByText('DEBUG'));
|
|
68
|
-
fireEvent.click(screen.getByText('STDERR'));
|
|
69
|
-
});
|
|
70
77
|
expect(
|
|
71
|
-
screen.
|
|
72
|
-
).
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
78
|
+
screen.queryByText('No output for the selected filters')
|
|
79
|
+
).not.toBeInTheDocument();
|
|
80
|
+
|
|
81
|
+
const newProps = {
|
|
82
|
+
...mockProps,
|
|
83
|
+
showOutputType: { stderr: false, stdout: false, debug: false },
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
rerender(
|
|
87
|
+
<Provider store={store}>
|
|
88
|
+
<TemplateInvocation {...newProps} />
|
|
89
|
+
</Provider>
|
|
90
|
+
);
|
|
77
91
|
|
|
78
|
-
act(() => {
|
|
79
|
-
fireEvent.click(screen.getByText('STDOUT'));
|
|
80
|
-
});
|
|
81
92
|
expect(
|
|
82
|
-
screen.
|
|
83
|
-
).
|
|
84
|
-
|
|
93
|
+
screen.getByText('No output for the selected filters')
|
|
94
|
+
).toBeInTheDocument();
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
test('correctly filters specific output types', () => {
|
|
98
|
+
const { rerender } = render(
|
|
99
|
+
<Provider store={store}>
|
|
100
|
+
<TemplateInvocation {...mockProps} />
|
|
101
|
+
</Provider>
|
|
102
|
+
);
|
|
103
|
+
|
|
104
|
+
expect(screen.getByText('Exit status: 1')).toBeInTheDocument(); // stdout
|
|
85
105
|
expect(
|
|
86
|
-
screen.
|
|
87
|
-
).
|
|
106
|
+
screen.getByText('StandardError: Job execution failed')
|
|
107
|
+
).toBeInTheDocument(); // debug
|
|
88
108
|
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
109
|
+
// Turn off stdout
|
|
110
|
+
rerender(
|
|
111
|
+
<Provider store={store}>
|
|
112
|
+
<TemplateInvocation
|
|
113
|
+
{...mockProps}
|
|
114
|
+
showOutputType={{ stderr: true, stdout: false, debug: true }}
|
|
115
|
+
/>
|
|
116
|
+
</Provider>
|
|
117
|
+
);
|
|
118
|
+
expect(screen.queryByText('Exit status: 1')).not.toBeInTheDocument();
|
|
92
119
|
expect(
|
|
93
|
-
screen.
|
|
94
|
-
).
|
|
95
|
-
|
|
120
|
+
screen.getByText('StandardError: Job execution failed')
|
|
121
|
+
).toBeInTheDocument();
|
|
122
|
+
|
|
123
|
+
// Turn off debug
|
|
124
|
+
rerender(
|
|
125
|
+
<Provider store={store}>
|
|
126
|
+
<TemplateInvocation
|
|
127
|
+
{...mockProps}
|
|
128
|
+
showOutputType={{ stderr: true, stdout: false, debug: false }}
|
|
129
|
+
/>
|
|
130
|
+
</Provider>
|
|
131
|
+
);
|
|
132
|
+
expect(screen.queryByText('Exit status: 1')).not.toBeInTheDocument();
|
|
96
133
|
expect(
|
|
97
|
-
screen.
|
|
98
|
-
).
|
|
134
|
+
screen.queryByText('StandardError: Job execution failed')
|
|
135
|
+
).not.toBeInTheDocument();
|
|
99
136
|
});
|
|
137
|
+
|
|
100
138
|
test('displays an error alert when there is an error', async () => {
|
|
101
139
|
selectors.selectTemplateInvocationStatus.mockImplementation(() => () =>
|
|
102
140
|
'ERROR'
|
|
@@ -106,14 +144,7 @@ describe('TemplateInvocation', () => {
|
|
|
106
144
|
}));
|
|
107
145
|
render(
|
|
108
146
|
<Provider store={store}>
|
|
109
|
-
<TemplateInvocation
|
|
110
|
-
hostID="1"
|
|
111
|
-
jobID="1"
|
|
112
|
-
isInTableView={false}
|
|
113
|
-
isExpanded
|
|
114
|
-
hostName="example-host"
|
|
115
|
-
hostProxy={{ name: 'example-proxy', href: '#' }}
|
|
116
|
-
/>
|
|
147
|
+
<TemplateInvocation {...mockProps} />
|
|
117
148
|
</Provider>
|
|
118
149
|
);
|
|
119
150
|
|
|
@@ -129,19 +160,30 @@ describe('TemplateInvocation', () => {
|
|
|
129
160
|
selectors.selectTemplateInvocationStatus.mockImplementation(() => () =>
|
|
130
161
|
'PENDING'
|
|
131
162
|
);
|
|
132
|
-
selectors.selectTemplateInvocation.mockImplementation(() => () =>
|
|
163
|
+
selectors.selectTemplateInvocation.mockImplementation(() => () => null);
|
|
133
164
|
render(
|
|
134
165
|
<Provider store={store}>
|
|
135
|
-
<TemplateInvocation
|
|
136
|
-
hostID="1"
|
|
137
|
-
jobID="1"
|
|
138
|
-
isInTableView={false}
|
|
139
|
-
isExpanded
|
|
140
|
-
hostName="example-host"
|
|
141
|
-
/>
|
|
166
|
+
<TemplateInvocation {...mockProps} />
|
|
142
167
|
</Provider>
|
|
143
168
|
);
|
|
144
169
|
|
|
145
|
-
expect(
|
|
170
|
+
expect(
|
|
171
|
+
screen.getByTestId('template-invocation-skeleton')
|
|
172
|
+
).toBeInTheDocument();
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
test('copies text to clipboard when clicked', async () => {
|
|
176
|
+
render(
|
|
177
|
+
<Provider store={store}>
|
|
178
|
+
<TemplateInvocation {...mockProps} />
|
|
179
|
+
</Provider>
|
|
180
|
+
);
|
|
181
|
+
|
|
182
|
+
const copyButton = screen.getByLabelText('Copy to clipboard');
|
|
183
|
+
fireEvent.click(copyButton);
|
|
184
|
+
expect(navigator.clipboard.writeText).toHaveBeenCalledTimes(1);
|
|
185
|
+
expect(
|
|
186
|
+
await screen.findByText('Successfully copied to clipboard!')
|
|
187
|
+
).toBeInTheDocument();
|
|
146
188
|
});
|
|
147
189
|
});
|
|
@@ -79,15 +79,14 @@ const JobInvocationDetailPage = ({
|
|
|
79
79
|
return () => {
|
|
80
80
|
dispatch(stopInterval(JOB_INVOCATION_KEY));
|
|
81
81
|
};
|
|
82
|
-
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
83
82
|
}, [dispatch, id, finished, autoRefresh]);
|
|
84
83
|
|
|
84
|
+
const taskId = task?.id;
|
|
85
85
|
useEffect(() => {
|
|
86
|
-
if (
|
|
87
|
-
dispatch(getTask(`${
|
|
86
|
+
if (taskId !== undefined) {
|
|
87
|
+
dispatch(getTask(`${taskId}`));
|
|
88
88
|
}
|
|
89
|
-
|
|
90
|
-
}, [dispatch, task?.id]);
|
|
89
|
+
}, [dispatch, taskId]);
|
|
91
90
|
|
|
92
91
|
const pageStatus =
|
|
93
92
|
items.id === undefined
|
|
@@ -89,9 +89,6 @@ export const JobWizard = ({ rerunData }) => {
|
|
|
89
89
|
concurrency_control = {},
|
|
90
90
|
},
|
|
91
91
|
}) => {
|
|
92
|
-
if (category !== job_category) {
|
|
93
|
-
setCategory(job_category);
|
|
94
|
-
}
|
|
95
92
|
const advancedTemplateValues = {};
|
|
96
93
|
const defaultTemplateValues = {};
|
|
97
94
|
const inputs = template_inputs;
|
|
@@ -131,8 +128,7 @@ export const JobWizard = ({ rerunData }) => {
|
|
|
131
128
|
};
|
|
132
129
|
});
|
|
133
130
|
},
|
|
134
|
-
|
|
135
|
-
[category.length]
|
|
131
|
+
[setTemplateValues, setAdvancedValues]
|
|
136
132
|
);
|
|
137
133
|
useEffect(() => {
|
|
138
134
|
if (rerunData) {
|
|
@@ -153,8 +149,7 @@ export const JobWizard = ({ rerunData }) => {
|
|
|
153
149
|
},
|
|
154
150
|
});
|
|
155
151
|
}
|
|
156
|
-
|
|
157
|
-
}, [rerunData]);
|
|
152
|
+
}, [rerunData, setDefaults]);
|
|
158
153
|
useEffect(() => {
|
|
159
154
|
if (jobTemplateID) {
|
|
160
155
|
dispatch(
|
|
@@ -199,8 +194,14 @@ export const JobWizard = ({ rerunData }) => {
|
|
|
199
194
|
})
|
|
200
195
|
);
|
|
201
196
|
}
|
|
202
|
-
|
|
203
|
-
|
|
197
|
+
}, [
|
|
198
|
+
rerunData,
|
|
199
|
+
jobTemplateID,
|
|
200
|
+
dispatch,
|
|
201
|
+
setDefaults,
|
|
202
|
+
setTemplateValues,
|
|
203
|
+
setAdvancedValues,
|
|
204
|
+
]);
|
|
204
205
|
|
|
205
206
|
const [isStartsBeforeError, setIsStartsBeforeError] = useState(false);
|
|
206
207
|
const [isStartsAtError, setIsStartsAtError] = useState(false);
|
|
@@ -514,7 +515,7 @@ JobWizard.propTypes = {
|
|
|
514
515
|
}),
|
|
515
516
|
execution_timeout_interval: PropTypes.number,
|
|
516
517
|
time_to_pickup: PropTypes.number,
|
|
517
|
-
remote_execution_feature_id: PropTypes.
|
|
518
|
+
remote_execution_feature_id: PropTypes.number,
|
|
518
519
|
template_invocations: PropTypes.arrayOf(
|
|
519
520
|
PropTypes.shape({
|
|
520
521
|
template_id: PropTypes.number,
|
|
@@ -95,6 +95,14 @@ export const useAutoFill = ({
|
|
|
95
95
|
});
|
|
96
96
|
}
|
|
97
97
|
}
|
|
98
|
-
|
|
99
|
-
|
|
98
|
+
}, [
|
|
99
|
+
fills,
|
|
100
|
+
setFills,
|
|
101
|
+
setSelectedTargets,
|
|
102
|
+
setHostsSearchQuery,
|
|
103
|
+
setJobTemplateID,
|
|
104
|
+
setTemplateValues,
|
|
105
|
+
setAdvancedValues,
|
|
106
|
+
dispatch,
|
|
107
|
+
]);
|
|
100
108
|
};
|
|
@@ -55,8 +55,13 @@ const ConnectedCategoryAndTemplate = ({
|
|
|
55
55
|
})
|
|
56
56
|
);
|
|
57
57
|
}
|
|
58
|
-
|
|
59
|
-
|
|
58
|
+
}, [
|
|
59
|
+
jobCategoriesStatus,
|
|
60
|
+
dispatch,
|
|
61
|
+
isCategoryPreselected,
|
|
62
|
+
setCategory,
|
|
63
|
+
setJobTemplate,
|
|
64
|
+
]);
|
|
60
65
|
|
|
61
66
|
const jobCategories = useSelector(selectJobCategories);
|
|
62
67
|
const jobTemplatesSearch = useSelector(selectJobTemplatesSearch);
|
|
@@ -73,22 +78,29 @@ const ConnectedCategoryAndTemplate = ({
|
|
|
73
78
|
per_page: 'all',
|
|
74
79
|
}),
|
|
75
80
|
handleSuccess: response => {
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
81
|
+
const filteredTemplates = filterJobTemplates(
|
|
82
|
+
response?.data?.results
|
|
83
|
+
);
|
|
84
|
+
setJobTemplate(current => {
|
|
85
|
+
// Check if current template is in the new category's template list.
|
|
86
|
+
// This preserves the user's selection when changing categories on rerun,
|
|
87
|
+
// preventing the category from flashing and reverting back (Issue #38899).
|
|
88
|
+
// We check the state value (current) rather than the prop to avoid race conditions.
|
|
89
|
+
if (
|
|
90
|
+
current &&
|
|
91
|
+
filteredTemplates.some(template => template.id === current)
|
|
92
|
+
) {
|
|
93
|
+
return current;
|
|
94
|
+
}
|
|
95
|
+
// Otherwise, select the first template from the new category
|
|
96
|
+
return Number(filteredTemplates[0]?.id) || null;
|
|
97
|
+
});
|
|
85
98
|
},
|
|
86
99
|
})
|
|
87
100
|
);
|
|
88
101
|
}
|
|
89
102
|
}
|
|
90
|
-
|
|
91
|
-
}, [category, dispatch]);
|
|
103
|
+
}, [category, dispatch, jobTemplatesSearch, setJobTemplate]);
|
|
92
104
|
|
|
93
105
|
const jobTemplates = useSelector(selectJobTemplates);
|
|
94
106
|
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import React from 'react';
|
|
1
|
+
import React, { useCallback } from 'react';
|
|
2
2
|
import { useSelector, useDispatch } from 'react-redux';
|
|
3
3
|
import URI from 'urijs';
|
|
4
4
|
import { SelectVariant } from '@patternfly/react-core/deprecated';
|
|
@@ -8,16 +8,21 @@ import { SearchSelect } from '../form/SearchSelect';
|
|
|
8
8
|
|
|
9
9
|
export const useNameSearchAPI = (apiKey, url) => {
|
|
10
10
|
const dispatch = useDispatch();
|
|
11
|
-
|
|
12
|
-
const onSearch =
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
11
|
+
|
|
12
|
+
const onSearch = useCallback(
|
|
13
|
+
search => {
|
|
14
|
+
const uri = new URI(url);
|
|
15
|
+
dispatch(
|
|
16
|
+
get({
|
|
17
|
+
key: apiKey,
|
|
18
|
+
url: uri.addSearch({
|
|
19
|
+
search: `name~"${search}"`,
|
|
20
|
+
}),
|
|
21
|
+
})
|
|
22
|
+
);
|
|
23
|
+
},
|
|
24
|
+
[dispatch, apiKey, url]
|
|
25
|
+
);
|
|
21
26
|
|
|
22
27
|
const response = useSelector(state => selectResponse(state, apiKey));
|
|
23
28
|
const isLoading = useSelector(state => selectIsLoading(state, apiKey));
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import React, { useState, useEffect } from 'react';
|
|
1
|
+
import React, { useState, useEffect, useRef, useCallback } from 'react';
|
|
2
2
|
import PropTypes from 'prop-types';
|
|
3
3
|
import {
|
|
4
4
|
Select,
|
|
@@ -25,28 +25,30 @@ export const ResourceSelect = ({
|
|
|
25
25
|
const { perPage } = useForemanSettings();
|
|
26
26
|
const maxResults = perPage;
|
|
27
27
|
const dispatch = useDispatch();
|
|
28
|
-
const
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
28
|
+
const onSearch = useCallback(
|
|
29
|
+
search => {
|
|
30
|
+
const uri = new URI(url);
|
|
31
|
+
dispatch(
|
|
32
|
+
get({
|
|
33
|
+
key: apiKey,
|
|
34
|
+
url: uri.addSearch(search),
|
|
35
|
+
})
|
|
36
|
+
);
|
|
37
|
+
},
|
|
38
|
+
[dispatch, apiKey, url]
|
|
39
|
+
);
|
|
37
40
|
|
|
38
41
|
const response = useSelector(state => selectResponse(state, apiKey));
|
|
39
42
|
const isLoading = useSelector(state => selectIsLoading(state, apiKey));
|
|
40
43
|
const [isOpen, setIsOpen] = useState(false);
|
|
41
44
|
const [typingTimeout, setTypingTimeout] = useState(null);
|
|
45
|
+
const initializedRef = useRef(false);
|
|
42
46
|
useEffect(() => {
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
47
|
+
if (!initializedRef.current) {
|
|
48
|
+
onSearch(selected ? { id: selected } : {});
|
|
49
|
+
initializedRef.current = true;
|
|
46
50
|
}
|
|
47
|
-
|
|
48
|
-
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
49
|
-
}, []);
|
|
51
|
+
}, [onSearch, selected]);
|
|
50
52
|
let selectOptions = [];
|
|
51
53
|
if (response.subtotal > maxResults) {
|
|
52
54
|
selectOptions = [
|