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
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 2db78eb6979bd486194e94b58c14978db6d08c02f675a042bd9827c9101b0639
|
|
4
|
+
data.tar.gz: c9060344c28a222b93fed7f26794fde15827030ae7b3b6197a66b9f86413b656
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 478f4cc9af2467020b4498bdd757d9f3b33a5e233c65b0b941cb0eda2deacf0689077907298e111c7cc1ec13e64508c9cd33d0870df70fe8a7247dd99e9433a9
|
|
7
|
+
data.tar.gz: f2bf38fe739e5043ba595448090a4231a52ef923464eec41b470d71c8ccdf3f83c8faec3a47627c5a606601d343b42302093a3a22474f57dded90375933a506e
|
|
@@ -116,13 +116,11 @@ module Api
|
|
|
116
116
|
def hosts
|
|
117
117
|
set_hosts_and_template_invocations
|
|
118
118
|
set_statuses_and_smart_proxies
|
|
119
|
-
@total = @
|
|
119
|
+
@total = @hosts.size
|
|
120
120
|
@hosts = @hosts.search_for(params[:search], :order => params[:order]).paginate(:page => params[:page], :per_page => params[:per_page])
|
|
121
|
+
@subtotal = @hosts.total_entries
|
|
121
122
|
if params[:awaiting]
|
|
122
123
|
@hosts = @hosts.select { |host| @host_statuses[host.id] == 'N/A' }
|
|
123
|
-
@subtotal = @hosts.size
|
|
124
|
-
else
|
|
125
|
-
@subtotal = @hosts.respond_to?(:total_entries) ? @hosts.total_entries : @hosts.sizes
|
|
126
124
|
end
|
|
127
125
|
render :hosts, :layout => 'api/v2/layouts/index_layout'
|
|
128
126
|
end
|
|
@@ -303,7 +301,7 @@ module Api
|
|
|
303
301
|
@pattern_template_invocations = @job_invocation.pattern_template_invocations.includes(:input_values)
|
|
304
302
|
@hosts = @job_invocation.targeting.hosts.authorized(:view_hosts, Host)
|
|
305
303
|
|
|
306
|
-
|
|
304
|
+
if params[:search].present?
|
|
307
305
|
@hosts = @hosts.joins(:template_invocations)
|
|
308
306
|
.where(:template_invocations => { :job_invocation_id => @job_invocation.id})
|
|
309
307
|
end
|
|
@@ -11,6 +11,15 @@ module ::ProxyAPI
|
|
|
11
11
|
raise ProxyException.new(url, e, N_('Unable to fetch public key'))
|
|
12
12
|
end
|
|
13
13
|
|
|
14
|
+
def ca_pubkey
|
|
15
|
+
get('ca_pubkey')&.strip
|
|
16
|
+
rescue RestClient::ResourceNotFound => e
|
|
17
|
+
Rails.logger.warn(format(N_("Unable to fetch CA public key: %{error}"), error: e.message))
|
|
18
|
+
nil
|
|
19
|
+
rescue => e
|
|
20
|
+
raise ProxyException.new(url, e, N_('Unable to fetch CA public key'))
|
|
21
|
+
end
|
|
22
|
+
|
|
14
23
|
def drop_from_known_hosts(hostname)
|
|
15
24
|
delete('known_hosts/' + hostname)
|
|
16
25
|
rescue => e
|
|
@@ -108,7 +108,12 @@ module ForemanRemoteExecution
|
|
|
108
108
|
end
|
|
109
109
|
|
|
110
110
|
def remote_execution_ssh_keys
|
|
111
|
-
|
|
111
|
+
# only include public keys from SSH proxies that don't have SSH cert verification configured
|
|
112
|
+
remote_execution_proxies(%w(SSH Script), false).values.flatten.uniq.map { |proxy| proxy.pubkey if proxy.ca_pubkey.blank? }.compact.uniq
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
def remote_execution_ssh_ca_keys
|
|
116
|
+
remote_execution_proxies(%w(SSH Script), false).values.flatten.uniq.map { |proxy| proxy.ca_pubkey }.compact.uniq
|
|
112
117
|
end
|
|
113
118
|
|
|
114
119
|
def drop_execution_interface_cache
|
|
@@ -139,10 +144,13 @@ module ForemanRemoteExecution
|
|
|
139
144
|
|
|
140
145
|
def extend_host_params_hash(params)
|
|
141
146
|
keys = remote_execution_ssh_keys
|
|
147
|
+
ca_keys = remote_execution_ssh_ca_keys
|
|
142
148
|
source = 'global'
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
149
|
+
{keys: keys, ca_keys: ca_keys}.each do |key_set_name, key_set|
|
|
150
|
+
if key_set.present?
|
|
151
|
+
value, safe_value = params.fetch("remote_execution_ssh_#{key_set_name}", {}).values_at(:value, :safe_value).map { |v| [v].flatten.compact }
|
|
152
|
+
params["remote_execution_ssh_#{key_set_name}"] = {:value => value + key_set, :safe_value => safe_value + key_set, :source => source}
|
|
153
|
+
end
|
|
146
154
|
end
|
|
147
155
|
[:remote_execution_ssh_user, :remote_execution_effective_user_method,
|
|
148
156
|
:remote_execution_connect_by_ip].each do |key|
|
|
@@ -11,6 +11,10 @@ module ForemanRemoteExecution
|
|
|
11
11
|
self[:pubkey] || update_pubkey
|
|
12
12
|
end
|
|
13
13
|
|
|
14
|
+
def ca_pubkey
|
|
15
|
+
self[:ca_pubkey] || update_ca_pubkey
|
|
16
|
+
end
|
|
17
|
+
|
|
14
18
|
def update_pubkey
|
|
15
19
|
return unless has_feature?(%w(SSH Script))
|
|
16
20
|
|
|
@@ -19,6 +23,15 @@ module ForemanRemoteExecution
|
|
|
19
23
|
key
|
|
20
24
|
end
|
|
21
25
|
|
|
26
|
+
def update_ca_pubkey
|
|
27
|
+
return unless has_feature?(%w(SSH Script))
|
|
28
|
+
|
|
29
|
+
# smart proxy is not required to have a CA pubkey, in which case an empty string is returned
|
|
30
|
+
key = ::ProxyAPI::RemoteExecutionSSH.new(:url => url).ca_pubkey&.presence
|
|
31
|
+
self.update_attribute(:ca_pubkey, key)
|
|
32
|
+
key
|
|
33
|
+
end
|
|
34
|
+
|
|
22
35
|
def drop_host_from_known_hosts(host)
|
|
23
36
|
::ProxyAPI::RemoteExecutionSSH.new(:url => url).drop_from_known_hosts(host)
|
|
24
37
|
end
|
|
@@ -26,6 +39,7 @@ module ForemanRemoteExecution
|
|
|
26
39
|
def refresh
|
|
27
40
|
errors = super
|
|
28
41
|
update_pubkey
|
|
42
|
+
update_ca_pubkey
|
|
29
43
|
errors
|
|
30
44
|
end
|
|
31
45
|
end
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
attribute :ca_pubkey => :remote_execution_ca_pubkey
|
|
@@ -213,6 +213,7 @@ Foreman::Plugin.register :foreman_remote_execution do
|
|
|
213
213
|
extend_template_helpers ForemanRemoteExecution::RendererMethods
|
|
214
214
|
|
|
215
215
|
extend_rabl_template 'api/v2/smart_proxies/main', 'api/v2/smart_proxies/pubkey'
|
|
216
|
+
extend_rabl_template 'api/v2/smart_proxies/main', 'api/v2/smart_proxies/ca_pubkey'
|
|
216
217
|
extend_rabl_template 'api/v2/interfaces/main', 'api/v2/interfaces/execution_flag'
|
|
217
218
|
extend_rabl_template 'api/v2/subnets/show', 'api/v2/subnets/remote_execution_proxies'
|
|
218
219
|
extend_rabl_template 'api/v2/hosts/main', 'api/v2/host/main'
|
|
@@ -12,6 +12,7 @@ class ForemanRemoteExecutionHostExtensionsTest < ActiveSupport::TestCase
|
|
|
12
12
|
|
|
13
13
|
before do
|
|
14
14
|
SmartProxy.any_instance.stubs(:pubkey).returns(sshkey)
|
|
15
|
+
SmartProxy.any_instance.stubs(:ca_pubkey).returns(nil)
|
|
15
16
|
Setting[:remote_execution_ssh_user] = 'root'
|
|
16
17
|
Setting[:remote_execution_effective_user_method] = 'sudo'
|
|
17
18
|
end
|
|
@@ -61,6 +62,48 @@ class ForemanRemoteExecutionHostExtensionsTest < ActiveSupport::TestCase
|
|
|
61
62
|
end
|
|
62
63
|
end
|
|
63
64
|
|
|
65
|
+
describe 'has ssh CA key configured' do
|
|
66
|
+
let(:host) { FactoryBot.create(:host, :with_execution) }
|
|
67
|
+
let(:sshkey) { 'ssh-rsa AAAAB3NzaC1yc2EAAAABJQ foo@example.com' }
|
|
68
|
+
let(:ca_sshkey) { 'ssh-rsa AAAAB3NzaC1yc2EAAAABJE bar@example.com' }
|
|
69
|
+
|
|
70
|
+
before do
|
|
71
|
+
SmartProxy.any_instance.stubs(:pubkey).returns(sshkey)
|
|
72
|
+
SmartProxy.any_instance.stubs(:ca_pubkey).returns(ca_sshkey)
|
|
73
|
+
Setting[:remote_execution_ssh_user] = 'root'
|
|
74
|
+
Setting[:remote_execution_effective_user_method] = 'sudo'
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
it 'has CA ssh keys in the parameters' do
|
|
78
|
+
assert_includes host.remote_execution_ssh_ca_keys, ca_sshkey
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
it 'excludes ssh keys from proxies that have SSH CA key configured' do
|
|
82
|
+
assert_empty host.remote_execution_ssh_keys
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
it 'merges ssh CA keys from host parameters and proxies' do
|
|
86
|
+
key = 'ssh-rsa not-even-a-key something@somewhere.com'
|
|
87
|
+
host.host_parameters << FactoryBot.create(:host_parameter, :host => host, :name => 'remote_execution_ssh_ca_keys', :value => [key])
|
|
88
|
+
assert_includes host.host_param('remote_execution_ssh_ca_keys'), key
|
|
89
|
+
assert_includes host.host_param('remote_execution_ssh_ca_keys'), ca_sshkey
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
it 'has ssh CA keys in the parameters even when no user specified' do
|
|
93
|
+
FactoryBot.create(:smart_proxy, :ssh)
|
|
94
|
+
host.interfaces.first.subnet.remote_execution_proxies.clear
|
|
95
|
+
User.current = nil
|
|
96
|
+
assert_includes host.remote_execution_ssh_ca_keys, ca_sshkey
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
it 'merges ssh CA key as a string from host parameters and proxies' do
|
|
100
|
+
key = 'ssh-rsa not-even-a-key something@somewhere.com'
|
|
101
|
+
host.host_parameters << FactoryBot.create(:host_parameter, :host => host, :name => 'remote_execution_ssh_ca_keys', :value => key)
|
|
102
|
+
assert_includes host.host_param('remote_execution_ssh_ca_keys'), key
|
|
103
|
+
assert_includes host.host_param('remote_execution_ssh_ca_keys'), ca_sshkey
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
|
|
64
107
|
context 'host has multiple nics' do
|
|
65
108
|
let(:host) { FactoryBot.build(:host, :with_execution) }
|
|
66
109
|
|
|
@@ -24,7 +24,13 @@ import TableIndexPage from 'foremanReact/components/PF4/TableIndexPage/TableInde
|
|
|
24
24
|
import { getControllerSearchProps } from 'foremanReact/constants';
|
|
25
25
|
import { Icon } from 'patternfly-react';
|
|
26
26
|
import PropTypes from 'prop-types';
|
|
27
|
-
import React, {
|
|
27
|
+
import React, {
|
|
28
|
+
useEffect,
|
|
29
|
+
useMemo,
|
|
30
|
+
useState,
|
|
31
|
+
useRef,
|
|
32
|
+
useCallback,
|
|
33
|
+
} from 'react';
|
|
28
34
|
import { FormattedMessage } from 'react-intl';
|
|
29
35
|
import { useHistory } from 'react-router-dom';
|
|
30
36
|
import { useForemanSettings } from 'foremanReact/Root/Context/ForemanContext';
|
|
@@ -61,9 +67,47 @@ const JobInvocationHostTable = ({
|
|
|
61
67
|
const [allHostsIds, setAllHostsIds] = useState([]);
|
|
62
68
|
|
|
63
69
|
// Expansive items
|
|
64
|
-
const [expandedHost, setExpandedHost] = useState(
|
|
70
|
+
const [expandedHost, setExpandedHost] = useState(new Set());
|
|
65
71
|
const prevStatusLabel = useRef(statusLabel);
|
|
66
72
|
|
|
73
|
+
const [hostInvocationStates, setHostInvocationStates] = useState({});
|
|
74
|
+
|
|
75
|
+
const getInvocationState = hostId =>
|
|
76
|
+
hostInvocationStates[hostId] || {
|
|
77
|
+
showOutputType: { stderr: true, stdout: true, debug: true },
|
|
78
|
+
showTemplatePreview: false,
|
|
79
|
+
showCommand: false,
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
const updateInvocationState = (hostId, stateKey, value) => {
|
|
83
|
+
setHostInvocationStates(prevStates => {
|
|
84
|
+
const currentHostState = getInvocationState(hostId);
|
|
85
|
+
|
|
86
|
+
const newValue =
|
|
87
|
+
typeof value === 'function' ? value(currentHostState[stateKey]) : value;
|
|
88
|
+
|
|
89
|
+
return {
|
|
90
|
+
...prevStates,
|
|
91
|
+
[hostId]: {
|
|
92
|
+
...currentHostState,
|
|
93
|
+
[stateKey]: newValue,
|
|
94
|
+
},
|
|
95
|
+
};
|
|
96
|
+
});
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
const isHostExpanded = hostId => expandedHost.has(hostId);
|
|
100
|
+
const setHostExpanded = (hostId, isExpanding = true) =>
|
|
101
|
+
setExpandedHost(prevExpandedSet => {
|
|
102
|
+
const newSet = new Set(prevExpandedSet);
|
|
103
|
+
if (isExpanding) {
|
|
104
|
+
newSet.add(hostId);
|
|
105
|
+
} else {
|
|
106
|
+
newSet.delete(hostId);
|
|
107
|
+
}
|
|
108
|
+
return newSet;
|
|
109
|
+
});
|
|
110
|
+
|
|
67
111
|
// Page table params
|
|
68
112
|
// Parse URL
|
|
69
113
|
const {
|
|
@@ -93,19 +137,22 @@ const JobInvocationHostTable = ({
|
|
|
93
137
|
});
|
|
94
138
|
|
|
95
139
|
// Search filter
|
|
96
|
-
const constructFilter = (
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
140
|
+
const constructFilter = useCallback(
|
|
141
|
+
(filter = initialFilter, search = urlSearchQuery) => {
|
|
142
|
+
const dropdownFilterClause =
|
|
143
|
+
filter && filter !== 'all_statuses'
|
|
144
|
+
? `job_invocation.result = ${filter}`
|
|
145
|
+
: null;
|
|
146
|
+
const parts = [dropdownFilterClause, search];
|
|
147
|
+
return parts
|
|
148
|
+
.filter(x => x)
|
|
149
|
+
.map(fragment => `(${fragment})`)
|
|
150
|
+
.join(' AND ');
|
|
151
|
+
},
|
|
152
|
+
[initialFilter, urlSearchQuery]
|
|
153
|
+
);
|
|
107
154
|
|
|
108
|
-
const handleResponse = (data, key) => {
|
|
155
|
+
const handleResponse = useCallback((data, key) => {
|
|
109
156
|
if (key === JOB_INVOCATION_HOSTS) {
|
|
110
157
|
const ids = data.data.results.map(i => i.id);
|
|
111
158
|
|
|
@@ -114,76 +161,94 @@ const JobInvocationHostTable = ({
|
|
|
114
161
|
}
|
|
115
162
|
|
|
116
163
|
setStatus(STATUS_UPPERCASE.RESOLVED);
|
|
117
|
-
};
|
|
164
|
+
}, []);
|
|
118
165
|
|
|
119
166
|
// Call hosts data with params
|
|
120
|
-
const makeApiCall = (
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
response
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
167
|
+
const makeApiCall = useCallback(
|
|
168
|
+
(requestParams, callParams = {}) => {
|
|
169
|
+
dispatch(
|
|
170
|
+
APIActions.get({
|
|
171
|
+
key: callParams.key ?? ALL_JOB_HOSTS,
|
|
172
|
+
url: callParams.url ?? `/api/job_invocations/${id}/hosts`,
|
|
173
|
+
params: requestParams,
|
|
174
|
+
handleSuccess: data => handleResponse(data, callParams.key),
|
|
175
|
+
handleError: () => setStatus(STATUS_UPPERCASE.ERROR),
|
|
176
|
+
errorToast: ({ response }) =>
|
|
177
|
+
response?.data?.error?.full_messages?.[0] || response,
|
|
178
|
+
})
|
|
179
|
+
);
|
|
180
|
+
},
|
|
181
|
+
[dispatch, id, handleResponse]
|
|
182
|
+
);
|
|
133
183
|
|
|
134
|
-
const filterApiCall =
|
|
135
|
-
|
|
184
|
+
const filterApiCall = useCallback(
|
|
185
|
+
newAPIOptions => {
|
|
186
|
+
const newParams = newAPIOptions?.params ?? newAPIOptions ?? {};
|
|
136
187
|
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
188
|
+
const filterSearch = constructFilter(
|
|
189
|
+
initialFilter,
|
|
190
|
+
newParams.search ?? urlSearchQuery
|
|
191
|
+
);
|
|
141
192
|
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
193
|
+
const finalParams = {
|
|
194
|
+
...defaultParams,
|
|
195
|
+
...newParams,
|
|
196
|
+
};
|
|
146
197
|
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
198
|
+
if (filterSearch === AWAITING_STATUS_FILTER) {
|
|
199
|
+
finalParams.awaiting = 'true';
|
|
200
|
+
} else if (filterSearch !== '') {
|
|
201
|
+
finalParams.search = filterSearch;
|
|
202
|
+
}
|
|
152
203
|
|
|
153
|
-
|
|
204
|
+
makeApiCall(finalParams, { key: JOB_INVOCATION_HOSTS });
|
|
154
205
|
|
|
155
|
-
|
|
206
|
+
const urlSearchParams = new URLSearchParams(window.location.search);
|
|
156
207
|
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
208
|
+
['page', 'per_page', 'order'].forEach(key => {
|
|
209
|
+
if (finalParams[key]) urlSearchParams.set(key, finalParams[key]);
|
|
210
|
+
});
|
|
160
211
|
|
|
161
|
-
|
|
162
|
-
|
|
212
|
+
history.push({ search: urlSearchParams.toString() });
|
|
213
|
+
},
|
|
214
|
+
[
|
|
215
|
+
initialFilter,
|
|
216
|
+
urlSearchQuery,
|
|
217
|
+
defaultParams,
|
|
218
|
+
makeApiCall,
|
|
219
|
+
history,
|
|
220
|
+
constructFilter,
|
|
221
|
+
]
|
|
222
|
+
);
|
|
163
223
|
|
|
164
224
|
// Filter change
|
|
165
|
-
const handleFilterChange =
|
|
166
|
-
|
|
167
|
-
|
|
225
|
+
const handleFilterChange = useCallback(
|
|
226
|
+
newFilter => {
|
|
227
|
+
onFilterUpdate(newFilter);
|
|
228
|
+
},
|
|
229
|
+
[onFilterUpdate]
|
|
230
|
+
);
|
|
168
231
|
|
|
169
232
|
// Effects
|
|
170
233
|
// run after mount
|
|
234
|
+
const initializedRef = useRef(false);
|
|
171
235
|
useEffect(() => {
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
236
|
+
if (!initializedRef.current) {
|
|
237
|
+
// Job Invo template load
|
|
238
|
+
makeApiCall(
|
|
239
|
+
{},
|
|
240
|
+
{
|
|
241
|
+
url: `/job_invocations/${id}/hosts`,
|
|
242
|
+
key: LIST_TEMPLATE_INVOCATIONS,
|
|
243
|
+
}
|
|
244
|
+
);
|
|
245
|
+
|
|
246
|
+
if (initialFilter === '') {
|
|
247
|
+
onFilterUpdate('all_statuses');
|
|
178
248
|
}
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
if (initialFilter === '') {
|
|
182
|
-
onFilterUpdate('all_statuses');
|
|
249
|
+
initializedRef.current = true;
|
|
183
250
|
}
|
|
184
|
-
|
|
185
|
-
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
186
|
-
}, []);
|
|
251
|
+
}, [makeApiCall, id, initialFilter, onFilterUpdate]);
|
|
187
252
|
|
|
188
253
|
useEffect(() => {
|
|
189
254
|
if (initialFilter !== '') filterApiCall();
|
|
@@ -192,8 +257,7 @@ const JobInvocationHostTable = ({
|
|
|
192
257
|
prevStatusLabel.current = statusLabel;
|
|
193
258
|
filterApiCall();
|
|
194
259
|
}
|
|
195
|
-
|
|
196
|
-
}, [initialFilter, statusLabel, id]);
|
|
260
|
+
}, [initialFilter, statusLabel, id, filterApiCall]);
|
|
197
261
|
|
|
198
262
|
const {
|
|
199
263
|
updateSearchQuery: updateSearchQueryBulk,
|
|
@@ -307,29 +371,18 @@ const JobInvocationHostTable = ({
|
|
|
307
371
|
</Tr>
|
|
308
372
|
);
|
|
309
373
|
|
|
310
|
-
const isHostExpanded = host => expandedHost.includes(host.id);
|
|
311
|
-
|
|
312
|
-
const setHostExpanded = (host, isExpanding = true) =>
|
|
313
|
-
setExpandedHost(prevExpanded => {
|
|
314
|
-
const otherExpandedHosts = prevExpanded.filter(h => h !== host.id);
|
|
315
|
-
return isExpanding
|
|
316
|
-
? [...otherExpandedHosts, host.id]
|
|
317
|
-
: otherExpandedHosts;
|
|
318
|
-
});
|
|
319
|
-
|
|
320
374
|
const pageHostIds = results.map(h => h.id);
|
|
321
375
|
|
|
322
376
|
const areAllPageRowsExpanded =
|
|
323
377
|
pageHostIds.length > 0 &&
|
|
324
|
-
pageHostIds.every(hostId => expandedHost.
|
|
378
|
+
pageHostIds.every(hostId => expandedHost.has(hostId));
|
|
325
379
|
|
|
326
380
|
const onExpandAll = () => {
|
|
327
381
|
setExpandedHost(() => {
|
|
328
382
|
if (areAllPageRowsExpanded) {
|
|
329
|
-
return
|
|
383
|
+
return new Set();
|
|
330
384
|
}
|
|
331
|
-
|
|
332
|
-
return pageHostIds;
|
|
385
|
+
return new Set(pageHostIds);
|
|
333
386
|
});
|
|
334
387
|
};
|
|
335
388
|
|
|
@@ -393,54 +446,88 @@ const JobInvocationHostTable = ({
|
|
|
393
446
|
isDeleteable={false}
|
|
394
447
|
childrenOutsideTbody
|
|
395
448
|
>
|
|
396
|
-
{results.map((result, rowIndex) =>
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
<
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
449
|
+
{results.map((result, rowIndex) => {
|
|
450
|
+
const currentInvocationState = getInvocationState(result.id);
|
|
451
|
+
return (
|
|
452
|
+
<Tbody key={result.id}>
|
|
453
|
+
<Tr ouiaId={`table-row-${result.id}`}>
|
|
454
|
+
<Td
|
|
455
|
+
expand={{
|
|
456
|
+
rowIndex,
|
|
457
|
+
isExpanded: isHostExpanded(result.id),
|
|
458
|
+
onToggle: () =>
|
|
459
|
+
setHostExpanded(result.id, !isHostExpanded(result.id)),
|
|
460
|
+
expandId: 'host-expandable',
|
|
461
|
+
}}
|
|
462
|
+
/>
|
|
463
|
+
<RowSelectTd
|
|
464
|
+
rowData={result}
|
|
465
|
+
selectOne={selectOne}
|
|
466
|
+
isSelected={isSelected}
|
|
467
|
+
/>
|
|
468
|
+
{columnNamesKeys.map(k => (
|
|
469
|
+
<Td key={k}>{columns[k].wrapper(result)}</Td>
|
|
470
|
+
))}
|
|
471
|
+
<Td isActionCell>
|
|
472
|
+
<RowActions hostID={result.id} jobID={id} />
|
|
473
|
+
</Td>
|
|
474
|
+
</Tr>
|
|
475
|
+
<Tr
|
|
476
|
+
isExpanded={isHostExpanded(result.id)}
|
|
477
|
+
ouiaId="table-row-expanded-sections"
|
|
478
|
+
className={!isHostExpanded(result.id) ? 'row-hidden' : ''}
|
|
423
479
|
>
|
|
424
|
-
<
|
|
425
|
-
{result.
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
480
|
+
<Td
|
|
481
|
+
dataLabel={`${result.id}-expandable-content`}
|
|
482
|
+
colSpan={columnNamesKeys.length + 3}
|
|
483
|
+
>
|
|
484
|
+
<ExpandableRowContent>
|
|
485
|
+
{result.job_status === 'cancelled' ||
|
|
486
|
+
result.job_status === 'N/A' ? (
|
|
487
|
+
<div>
|
|
488
|
+
{__('A task for this host has not been started')}
|
|
489
|
+
</div>
|
|
490
|
+
) : (
|
|
491
|
+
<TemplateInvocation
|
|
492
|
+
key={result.id}
|
|
493
|
+
hostID={result.id}
|
|
494
|
+
jobID={id}
|
|
495
|
+
isInTableView
|
|
496
|
+
isExpanded={isHostExpanded(result.id)}
|
|
497
|
+
showOutputType={currentInvocationState.showOutputType}
|
|
498
|
+
showTemplatePreview={
|
|
499
|
+
currentInvocationState.showTemplatePreview
|
|
500
|
+
}
|
|
501
|
+
showCommand={currentInvocationState.showCommand}
|
|
502
|
+
setShowOutputType={value =>
|
|
503
|
+
updateInvocationState(
|
|
504
|
+
result.id,
|
|
505
|
+
'showOutputType',
|
|
506
|
+
value
|
|
507
|
+
)
|
|
508
|
+
}
|
|
509
|
+
setShowTemplatePreview={value =>
|
|
510
|
+
updateInvocationState(
|
|
511
|
+
result.id,
|
|
512
|
+
'showTemplatePreview',
|
|
513
|
+
value
|
|
514
|
+
)
|
|
515
|
+
}
|
|
516
|
+
setShowCommand={value =>
|
|
517
|
+
updateInvocationState(
|
|
518
|
+
result.id,
|
|
519
|
+
'showCommand',
|
|
520
|
+
value
|
|
521
|
+
)
|
|
522
|
+
}
|
|
523
|
+
/>
|
|
524
|
+
)}
|
|
525
|
+
</ExpandableRowContent>
|
|
526
|
+
</Td>
|
|
527
|
+
</Tr>
|
|
528
|
+
</Tbody>
|
|
529
|
+
);
|
|
530
|
+
})}
|
|
444
531
|
</Table>
|
|
445
532
|
</TableIndexPage>
|
|
446
533
|
</>
|