foreman_remote_execution 16.0.4 → 16.1.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 (32) hide show
  1. checksums.yaml +4 -4
  2. data/app/controllers/api/v2/job_invocations_controller.rb +33 -6
  3. data/app/controllers/job_invocations_controller.rb +28 -1
  4. data/app/controllers/template_invocations_controller.rb +1 -1
  5. data/app/helpers/remote_execution_helper.rb +1 -1
  6. data/config/routes.rb +4 -2
  7. data/lib/foreman_remote_execution/engine.rb +9 -256
  8. data/lib/foreman_remote_execution/plugin.rb +240 -0
  9. data/lib/foreman_remote_execution/version.rb +1 -1
  10. data/test/functional/api/v2/job_invocations_controller_test.rb +1 -1
  11. data/test/unit/job_invocation_report_template_test.rb +6 -6
  12. data/webpack/JobInvocationDetail/CheckboxesActions.js +327 -0
  13. data/webpack/JobInvocationDetail/{JobInvocationHostTableToolbar.js → DropdownFilter.js} +3 -6
  14. data/webpack/JobInvocationDetail/JobInvocationConstants.js +8 -9
  15. data/webpack/JobInvocationDetail/JobInvocationDetail.scss +18 -0
  16. data/webpack/JobInvocationDetail/JobInvocationHostTable.js +205 -89
  17. data/webpack/JobInvocationDetail/JobInvocationSelectors.js +30 -3
  18. data/webpack/JobInvocationDetail/JobInvocationToolbarButtons.js +23 -28
  19. data/webpack/JobInvocationDetail/OpenAllInvocationsModal.js +118 -0
  20. data/webpack/JobInvocationDetail/TemplateInvocation.js +54 -24
  21. data/webpack/JobInvocationDetail/TemplateInvocationComponents/TemplateActionButtons.js +8 -10
  22. data/webpack/JobInvocationDetail/TemplateInvocationPage.js +1 -1
  23. data/webpack/JobInvocationDetail/__tests__/MainInformation.test.js +2 -4
  24. data/webpack/JobInvocationDetail/__tests__/TableToolbarActions.test.js +204 -0
  25. data/webpack/JobInvocationDetail/__tests__/TemplateInvocation.test.js +34 -28
  26. data/webpack/JobInvocationDetail/index.js +71 -41
  27. data/webpack/JobWizard/steps/HostsAndInputs/buildHostQuery.js +26 -7
  28. data/webpack/Routes/routes.js +1 -1
  29. data/webpack/react_app/components/TargetingHosts/TargetingHostsLabelsRow.scss +1 -1
  30. metadata +8 -6
  31. data/webpack/JobInvocationDetail/OpenAlInvocations.js +0 -111
  32. data/webpack/JobInvocationDetail/__tests__/OpenAlInvocations.test.js +0 -110
@@ -0,0 +1,240 @@
1
+ Foreman::Plugin.register :foreman_remote_execution do
2
+ requires_foreman '>= 3.15'
3
+ register_global_js_file 'global'
4
+ register_gettext
5
+
6
+ apipie_documented_controllers ["#{ForemanRemoteExecution::Engine.root}/app/controllers/api/v2/*.rb"]
7
+ ApipieDSL.configuration.dsl_classes_matchers += [
8
+ "#{ForemanRemoteExecution::Engine.root}/app/lib/foreman_remote_execution/renderer/**/*.rb",
9
+ ]
10
+ automatic_assets(false)
11
+ precompile_assets(ForemanRemoteExecution::Engine.assets_to_precompile)
12
+
13
+ # Add settings to a Remote Execution category
14
+ settings do
15
+ category :remote_execution, N_('Remote Execution') do
16
+ setting 'remote_execution_fallback_proxy',
17
+ type: :boolean,
18
+ description: N_('Search the host for any proxy with Remote Execution, useful when the host has no subnet or the subnet does not have an execution proxy'),
19
+ default: false,
20
+ full_name: N_('Fallback to Any Proxy')
21
+ setting 'remote_execution_global_proxy',
22
+ type: :boolean,
23
+ description: N_('Search for remote execution proxy outside of the proxies assigned to the host. The search will be limited to the host\'s organization and location.'),
24
+ default: true,
25
+ full_name: N_('Enable Global Proxy')
26
+ setting 'remote_execution_ssh_user',
27
+ type: :string,
28
+ description: N_('Default user to use for SSH. You may override per host by setting a parameter called remote_execution_ssh_user.'),
29
+ default: 'root',
30
+ full_name: N_('SSH User')
31
+ setting 'remote_execution_effective_user',
32
+ type: :string,
33
+ description: N_('Default user to use for executing the script. If the user differs from the SSH user, su or sudo is used to switch the user.'),
34
+ default: 'root',
35
+ full_name: N_('Effective User')
36
+ setting 'remote_execution_effective_user_method',
37
+ type: :string,
38
+ description: N_('What command should be used to switch to the effective user. One of %s') % ::ScriptExecutionProvider::EFFECTIVE_USER_METHODS.inspect,
39
+ default: 'sudo',
40
+ full_name: N_('Effective User Method'),
41
+ collection: proc { Hash[::ScriptExecutionProvider::EFFECTIVE_USER_METHODS.map { |method| [method, method] }] }
42
+ setting 'remote_execution_effective_user_password',
43
+ type: :string,
44
+ description: N_('Effective user password'),
45
+ default: '',
46
+ full_name: N_('Effective user password'),
47
+ encrypted: true
48
+ setting 'remote_execution_sync_templates',
49
+ type: :boolean,
50
+ description: N_('Whether we should sync templates from disk when running db:seed.'),
51
+ default: true,
52
+ full_name: N_('Sync Job Templates')
53
+ setting 'remote_execution_ssh_port',
54
+ type: :integer,
55
+ description: N_('Port to use for SSH communication. Default port 22. You may override per host by setting a parameter called remote_execution_ssh_port.'),
56
+ default: 22,
57
+ full_name: N_('SSH Port')
58
+ setting 'remote_execution_connect_by_ip',
59
+ type: :boolean,
60
+ description: N_('Should the ip addresses on host interfaces be preferred over the fqdn? '\
61
+ 'It is useful when DNS not resolving the fqdns properly. You may override this per host by setting a parameter called remote_execution_connect_by_ip. '\
62
+ 'For dual-stacked hosts you should consider the remote_execution_connect_by_ip_prefer_ipv6 setting'),
63
+ default: false,
64
+ full_name: N_('Connect by IP')
65
+ setting 'remote_execution_connect_by_ip_prefer_ipv6',
66
+ type: :boolean,
67
+ description: N_('When connecting using ip address, should the IPv6 addresses be preferred? '\
68
+ 'If no IPv6 address is set, it falls back to IPv4 automatically. You may override this per host by setting a parameter called remote_execution_connect_by_ip_prefer_ipv6. '\
69
+ 'By default and for compatibility, IPv4 will be preferred over IPv6 by default'),
70
+ default: false,
71
+ full_name: N_('Prefer IPv6 over IPv4')
72
+ setting 'remote_execution_ssh_password',
73
+ type: :string,
74
+ description: N_('Default password to use for SSH. You may override per host by setting a parameter called remote_execution_ssh_password'),
75
+ default: nil,
76
+ full_name: N_('Default SSH password'),
77
+ encrypted: true
78
+ setting 'remote_execution_ssh_key_passphrase',
79
+ type: :string,
80
+ description: N_('Default key passphrase to use for SSH. You may override per host by setting a parameter called remote_execution_ssh_key_passphrase'),
81
+ default: nil,
82
+ full_name: N_('Default SSH key passphrase'),
83
+ encrypted: true
84
+ setting 'remote_execution_cleanup_working_dirs',
85
+ type: :boolean,
86
+ description: N_('When enabled, working directories will be removed after task completion. You may override this per host by setting a parameter called remote_execution_cleanup_working_dirs.'),
87
+ default: true,
88
+ full_name: N_('Cleanup working directories')
89
+ setting 'remote_execution_cockpit_url',
90
+ type: :string,
91
+ description: N_('Where to find the Cockpit instance for the Web Console button. By default, no button is shown.'),
92
+ default: nil,
93
+ full_name: N_('Cockpit URL')
94
+ setting 'remote_execution_form_job_template',
95
+ type: :string,
96
+ description: N_('Choose a job template that is pre-selected in job invocation form'),
97
+ default: 'Run Command - Script Default',
98
+ full_name: N_('Form Job Template'),
99
+ collection: proc { Hash[JobTemplate.unscoped.map { |template| [template.name, template.name] }] }
100
+ setting 'remote_execution_job_invocation_report_template',
101
+ type: :string,
102
+ description: N_('Select a report template used for generating a report for a particular remote execution job'),
103
+ default: 'Job - Invocation Report',
104
+ full_name: N_('Job Invocation Report Template'),
105
+ collection: proc { ForemanRemoteExecution.job_invocation_report_templates_select }
106
+ setting 'remote_execution_time_to_pickup',
107
+ type: :integer,
108
+ description: N_('Time in seconds within which the host has to pick up a job. If the job is not picked up within this limit, the job will be cancelled. Defaults to 1 day. Applies only to pull-mqtt based jobs.'),
109
+ default: 24 * 60 * 60,
110
+ full_name: N_('Time to pickup')
111
+ end
112
+ end
113
+
114
+ # Add permissions
115
+ security_block :foreman_remote_execution do
116
+ permission :view_job_templates, { :job_templates => [:index, :show, :revision, :auto_complete_search, :auto_complete_job_category, :preview, :export],
117
+ :'api/v2/job_templates' => [:index, :show, :revision, :export],
118
+ :'api/v2/template_inputs' => [:index, :show],
119
+ :'api/v2/foreign_input_sets' => [:index, :show],
120
+ :ui_job_wizard => [:categories, :template, :resources, :job_invocation]}, :resource_type => 'JobTemplate'
121
+ permission :create_job_templates, { :job_templates => [:new, :create, :clone_template, :import],
122
+ :'api/v2/job_templates' => [:create, :clone, :import] }, :resource_type => 'JobTemplate'
123
+ permission :edit_job_templates, { :job_templates => [:edit, :update],
124
+ :'api/v2/job_templates' => [:update],
125
+ :'api/v2/template_inputs' => [:create, :update, :destroy],
126
+ :'api/v2/foreign_input_sets' => [:create, :update, :destroy]}, :resource_type => 'JobTemplate'
127
+ permission :view_remote_execution_features, { :remote_execution_features => [:index, :show],
128
+ :'api/v2/remote_execution_features' => [:index, :show, :available_remote_execution_features]},
129
+ :resource_type => 'RemoteExecutionFeature'
130
+ permission :edit_remote_execution_features, { :remote_execution_features => [:update],
131
+ :'api/v2/remote_execution_features' => [:update]}, :resource_type => 'RemoteExecutionFeature'
132
+ permission :destroy_job_templates, { :job_templates => [:destroy],
133
+ :'api/v2/job_templates' => [:destroy] }, :resource_type => 'JobTemplate'
134
+ permission :lock_job_templates, { :job_templates => [:lock, :unlock] }, :resource_type => 'JobTemplate'
135
+ permission :create_job_invocations, { :job_invocations => [:new, :create, :legacy_create, :refresh, :rerun, :preview_hosts],
136
+ 'api/v2/job_invocations' => [:create, :rerun] }, :resource_type => 'JobInvocation'
137
+ permission :view_job_invocations, { :job_invocations => [:index, :chart, :show, :auto_complete_search, :preview_job_invocations_per_host, :list_jobs_hosts], :template_invocations => [:show, :show_template_invocation_by_host],
138
+ 'api/v2/job_invocations' => [:index, :show, :output, :raw_output, :outputs, :hosts] }, :resource_type => 'JobInvocation'
139
+ permission :view_template_invocations, { :template_invocations => [:show, :template_invocation_preview, :show_template_invocation_by_host], :job_invocations => [:list_jobs_hosts],
140
+ 'api/v2/template_invocations' => [:template_invocations], :ui_job_wizard => [:job_invocation] }, :resource_type => 'TemplateInvocation'
141
+ permission :create_template_invocations, {}, :resource_type => 'TemplateInvocation'
142
+ permission :execute_jobs_on_infrastructure_hosts, {}, :resource_type => 'JobInvocation'
143
+ permission :cancel_job_invocations, { :job_invocations => [:cancel], 'api/v2/job_invocations' => [:cancel] }, :resource_type => 'JobInvocation'
144
+ # this permissions grants user to get auto completion hints when setting up filters
145
+ permission :filter_autocompletion_for_template_invocation, { :template_invocations => [:auto_complete_search, :index] },
146
+ :resource_type => 'TemplateInvocation'
147
+ permission :cockpit_hosts, { 'cockpit' => [:redirect, :host_ssh_params] }, :resource_type => 'Host'
148
+ end
149
+
150
+ user_permissions = [
151
+ :view_job_templates,
152
+ :view_job_invocations,
153
+ :create_job_invocations,
154
+ :create_template_invocations,
155
+ :view_hosts,
156
+ :view_smart_proxies,
157
+ :view_remote_execution_features,
158
+ ].freeze
159
+ manager_permissions = user_permissions + [
160
+ :cancel_job_invocations,
161
+ :destroy_job_templates,
162
+ :edit_job_templates,
163
+ :create_job_templates,
164
+ :lock_job_templates,
165
+ :view_audit_logs,
166
+ :filter_autocompletion_for_template_invocation,
167
+ :edit_remote_execution_features,
168
+ ]
169
+
170
+ # Add a new role called 'Remote Execution User ' if it doesn't exist
171
+ role 'Remote Execution User', user_permissions, 'Role with permissions to run remote execution jobs against hosts'
172
+ role 'Remote Execution Manager', manager_permissions, 'Role with permissions to manage job templates, remote execution features, cancel jobs and view audit logs'
173
+
174
+ add_all_permissions_to_default_roles(except: [:execute_jobs_on_infrastructure_hosts])
175
+ add_permissions_to_default_roles({
176
+ Role::MANAGER => [:execute_jobs_on_infrastructure_hosts],
177
+ Role::SITE_MANAGER => user_permissions + [:execute_jobs_on_infrastructure_hosts],
178
+ })
179
+
180
+ # add menu entry
181
+ menu :top_menu, :job_templates,
182
+ url_hash: { controller: :job_templates, action: :index },
183
+ caption: N_('Job Templates'),
184
+ parent: :hosts_menu,
185
+ after: :provisioning_templates
186
+ menu :admin_menu, :remote_execution_features,
187
+ url_hash: { controller: :remote_execution_features, action: :index },
188
+ caption: N_('Remote Execution Features'),
189
+ parent: :administer_menu,
190
+ after: :bookmarks
191
+
192
+ menu :top_menu, :job_invocations,
193
+ url_hash: { controller: :job_invocations, action: :index },
194
+ caption: N_('Jobs'),
195
+ parent: :monitor_menu,
196
+ after: :audits
197
+
198
+ register_custom_status HostStatus::ExecutionStatus
199
+ # add dashboard widget
200
+ # widget 'foreman_remote_execution_widget', name: N_('Foreman plugin template widget'), sizex: 4, sizey: 1
201
+ widget 'dashboard/latest-jobs', :name => N_('Latest Jobs'), :sizex => 6, :sizey => 1
202
+
203
+ parameter_filter Subnet, :remote_execution_proxies, :remote_execution_proxy_ids => []
204
+ parameter_filter Nic::Interface do |ctx|
205
+ ctx.permit :execution
206
+ end
207
+
208
+ register_graphql_query_field :job_invocations, '::Types::JobInvocation', :collection_field
209
+ register_graphql_query_field :job_invocation, '::Types::JobInvocation', :record_field
210
+
211
+ register_graphql_mutation_field :create_job_invocation, ::Mutations::JobInvocations::Create
212
+
213
+ extend_template_helpers ForemanRemoteExecution::RendererMethods
214
+
215
+ extend_rabl_template 'api/v2/smart_proxies/main', 'api/v2/smart_proxies/pubkey'
216
+ extend_rabl_template 'api/v2/interfaces/main', 'api/v2/interfaces/execution_flag'
217
+ extend_rabl_template 'api/v2/subnets/show', 'api/v2/subnets/remote_execution_proxies'
218
+ extend_rabl_template 'api/v2/hosts/main', 'api/v2/host/main'
219
+ parameter_filter ::Subnet, :remote_execution_proxy_ids
220
+
221
+ describe_host do
222
+ multiple_actions_provider :rex_hosts_multiple_actions
223
+ overview_buttons_provider :rex_host_overview_buttons
224
+ end
225
+
226
+ # Extend Registration module
227
+ extend_allowed_registration_vars :remote_execution_interface
228
+ extend_allowed_registration_vars :setup_remote_execution_pull
229
+ ForemanTasks.dynflow.eager_load_actions!
230
+ extend_observable_events(
231
+ ::Dynflow::Action.descendants.select do |klass|
232
+ klass <= ::Actions::ObservableAction
233
+ end.map(&:namespaced_event_names) +
234
+ RemoteExecutionFeature.all.pluck(:label).flat_map do |label|
235
+ [::Actions::RemoteExecution::RunHostJob, ::Actions::RemoteExecution::RunHostsJob].map do |klass|
236
+ klass.feature_job_event_names(label)
237
+ end
238
+ end
239
+ )
240
+ end
@@ -1,3 +1,3 @@
1
1
  module ForemanRemoteExecution
2
- VERSION = '16.0.4'.freeze
2
+ VERSION = '16.1.0'.freeze
3
3
  end
@@ -91,7 +91,7 @@ module Api
91
91
  end
92
92
 
93
93
  test 'should create with schedule' do
94
- @attrs[:scheduling] = { start_at: Time.now.to_s }
94
+ @attrs[:scheduling] = { start_at: (Time.now + 1.hour).to_s }
95
95
  post :create, params: { job_invocation: @attrs }
96
96
  invocation = ActiveSupport::JSON.decode(@response.body)
97
97
  assert_equal invocation['mode'], 'future'
@@ -28,9 +28,9 @@ class JobReportTemplateTest < ActiveSupport::TestCase
28
28
  describe 'task reporting' do
29
29
  let(:fake_outputs) do
30
30
  [
31
- { 'output_type' => 'stderr', 'output' => "error" },
32
- { 'output_type' => 'stdout', 'output' => "output" },
33
- { 'output_type' => 'debug', 'output' => "debug" },
31
+ { 'output_type' => 'stderr', 'output' => "error\n" },
32
+ { 'output_type' => 'stdout', 'output' => "output\n" },
33
+ { 'output_type' => 'debug', 'output' => "debug\n" },
34
34
  ]
35
35
  end
36
36
  let(:fake_task) { FakeTask.new(result: 'success', action_continuous_output: fake_outputs, :ended_at => Time.new(2020, 12, 1, 0, 0, 0).utc) }
@@ -50,9 +50,9 @@ class JobReportTemplateTest < ActiveSupport::TestCase
50
50
  row = rows.first
51
51
  assert_equal host.name, row['Host']
52
52
  assert_equal 'success', row['Result']
53
- assert_equal 'error', row['stderr']
54
- assert_equal 'output', row['stdout']
55
- assert_equal 'debug', row['debug']
53
+ assert_equal "error\n", row['stderr']
54
+ assert_equal "output\n", row['stdout']
55
+ assert_equal "debug\n", row['debug']
56
56
  assert_kind_of Time, Time.zone.parse(row['Finished']), 'Parsing of time column failed'
57
57
  end
58
58
  end
@@ -0,0 +1,327 @@
1
+ /* eslint-disable max-lines */
2
+ import {
3
+ Button,
4
+ Divider,
5
+ Dropdown,
6
+ DropdownItem,
7
+ DropdownList,
8
+ MenuToggle,
9
+ Tooltip,
10
+ } from '@patternfly/react-core';
11
+ import {
12
+ EllipsisVIcon,
13
+ OutlinedWindowRestoreIcon,
14
+ } from '@patternfly/react-icons';
15
+ import axios from 'axios';
16
+ import { foremanUrl } from 'foremanReact/common/helpers';
17
+ import { useAPI } from 'foremanReact/common/hooks/API/APIHooks';
18
+ import { translate as __, sprintf } from 'foremanReact/common/I18n';
19
+ import { addToast } from 'foremanReact/components/ToastsList';
20
+ import PropTypes from 'prop-types';
21
+ import React, { useState } from 'react';
22
+ import { useDispatch, useSelector } from 'react-redux';
23
+
24
+ import {
25
+ DIRECT_OPEN_HOST_LIMIT,
26
+ MAX_HOSTS_API_SIZE,
27
+ templateInvocationPageUrl,
28
+ } from './JobInvocationConstants';
29
+ import {
30
+ selectHasPermission,
31
+ selectItems,
32
+ selectTaskCancelable,
33
+ } from './JobInvocationSelectors';
34
+ import OpenAllInvocationsModal, { PopupAlert } from './OpenAllInvocationsModal';
35
+
36
+ /* eslint-disable camelcase */
37
+ const ActionsKebab = ({
38
+ selectedIds,
39
+ failedCount,
40
+ isTaskCancelable,
41
+ hasCancelPermission,
42
+ handleTaskAction,
43
+ handleOpenHosts,
44
+ isDropdownOpen,
45
+ setIsDropdownOpen,
46
+ }) => {
47
+ const dropdownItems = [
48
+ <DropdownItem
49
+ ouiaId="cancel-host-dropdown-item"
50
+ onClick={() => handleTaskAction('cancel')}
51
+ key="cancel"
52
+ component="button"
53
+ isDisabled={
54
+ selectedIds.length === 0 || !isTaskCancelable || !hasCancelPermission
55
+ }
56
+ >
57
+ {__('Cancel selected')}
58
+ </DropdownItem>,
59
+ <DropdownItem
60
+ ouiaId="abort-host-dropdown-item"
61
+ onClick={() => handleTaskAction('abort')}
62
+ key="abort"
63
+ component="button"
64
+ isDisabled={
65
+ selectedIds.length === 0 || !isTaskCancelable || !hasCancelPermission
66
+ }
67
+ >
68
+ {__('Abort selected')}
69
+ </DropdownItem>,
70
+ <Divider component="li" key="separator" />,
71
+ <DropdownItem
72
+ ouiaId="open-failed-dropdown-item"
73
+ key="open-failed"
74
+ onClick={() => handleOpenHosts('failed')}
75
+ isDisabled={failedCount === 0}
76
+ >
77
+ {sprintf(__('Open all failed runs (%s)'), failedCount)}
78
+ </DropdownItem>,
79
+ ];
80
+
81
+ return (
82
+ <Dropdown
83
+ isOpen={isDropdownOpen}
84
+ onOpenChange={setIsDropdownOpen}
85
+ onSelect={() => setIsDropdownOpen(false)}
86
+ ouiaId="actions-kebab"
87
+ shouldFocusToggleOnSelect
88
+ toggle={toggleRef => (
89
+ <MenuToggle
90
+ aria-label="actions dropdown toggle"
91
+ id="toggle-kebab"
92
+ isExpanded={isDropdownOpen}
93
+ onClick={() => setIsDropdownOpen(prev => !prev)}
94
+ ref={toggleRef}
95
+ variant="plain"
96
+ >
97
+ <EllipsisVIcon />
98
+ </MenuToggle>
99
+ )}
100
+ >
101
+ <DropdownList>{dropdownItems}</DropdownList>
102
+ </Dropdown>
103
+ );
104
+ };
105
+
106
+ export const CheckboxesActions = ({
107
+ selectedIds,
108
+ failedCount,
109
+ jobID,
110
+ filter,
111
+ bulkParams,
112
+ }) => {
113
+ const [isModalOpen, setIsModalOpen] = useState(false);
114
+ const [showAlert, setShowAlert] = useState(false);
115
+ const [isOpenFailed, setIsOpenFailed] = useState(false);
116
+ const [isDropdownOpen, setIsDropdownOpen] = useState(false);
117
+ const isTaskCancelable = useSelector(selectTaskCancelable);
118
+ const dispatch = useDispatch();
119
+
120
+ const hasCreatePermission = useSelector(
121
+ selectHasPermission('create_job_invocations')
122
+ );
123
+ const hasCancelPermission = useSelector(
124
+ selectHasPermission('cancel_job_invocations')
125
+ );
126
+ const jobSearchQuery = useSelector(selectItems)?.targeting?.search_query;
127
+ const filterQuery =
128
+ filter && filter !== 'all_statuses'
129
+ ? ` and job_invocation.result = ${filter}`
130
+ : '';
131
+ const combinedQuery = `${bulkParams}${filterQuery}`;
132
+
133
+ const { response: failedHostsData } = useAPI(
134
+ 'get',
135
+ foremanUrl(`/api/job_invocations/${jobID}/hosts`),
136
+ {
137
+ params: {
138
+ per_page: MAX_HOSTS_API_SIZE,
139
+ search: `job_invocation.result = failed`,
140
+ },
141
+ skip: failedCount === 0,
142
+ }
143
+ );
144
+
145
+ const failedHosts = failedHostsData?.results || [];
146
+
147
+ const openLink = url => {
148
+ const newWin = window.open(url);
149
+
150
+ if (!newWin || newWin.closed || typeof newWin.closed === 'undefined') {
151
+ setShowAlert(true);
152
+ }
153
+ };
154
+
155
+ const handleOpenHosts = async (type = 'all') => {
156
+ if (type === 'failed') {
157
+ if (failedCount <= DIRECT_OPEN_HOST_LIMIT) {
158
+ failedHosts.forEach(host =>
159
+ openLink(templateInvocationPageUrl(host.id, jobID))
160
+ );
161
+ return;
162
+ }
163
+ setIsOpenFailed(true);
164
+ setIsModalOpen(true);
165
+ return;
166
+ }
167
+
168
+ if (selectedIds.length <= DIRECT_OPEN_HOST_LIMIT) {
169
+ selectedIds.forEach(id => openLink(templateInvocationPageUrl(id, jobID)));
170
+ return;
171
+ }
172
+
173
+ setIsOpenFailed(false);
174
+ setIsModalOpen(true);
175
+ };
176
+
177
+ const cancelJobTasks = (search, action) => async () => {
178
+ dispatch(
179
+ addToast({
180
+ key: `cancel-job-info`,
181
+ type: 'info',
182
+ message: sprintf(__('Trying to %s the task'), action),
183
+ })
184
+ );
185
+
186
+ try {
187
+ const response = await axios.post(
188
+ `/api/v2/job_invocations/${jobID}/cancel`,
189
+ {
190
+ search,
191
+ force: action !== 'cancel',
192
+ }
193
+ );
194
+
195
+ const cancelledTasks = response.data?.cancelled;
196
+ const pastTenseAction =
197
+ action === 'cancel' ? __('cancelled') : __('aborted');
198
+
199
+ if (cancelledTasks && cancelledTasks.length > 0) {
200
+ const idList = cancelledTasks.join(', ');
201
+ dispatch(
202
+ addToast({
203
+ key: `success-tasks-cancelled`,
204
+ type: 'success',
205
+ message: sprintf(
206
+ __('%s task(s) successfully %s: %s'),
207
+ cancelledTasks.length,
208
+ pastTenseAction,
209
+ idList
210
+ ),
211
+ })
212
+ );
213
+ } else {
214
+ dispatch(
215
+ addToast({
216
+ key: `warn-no-tasks-cancelled-${Date.now()}`,
217
+ type: 'warning',
218
+ message: sprintf(__('Task(s) were not %s'), pastTenseAction),
219
+ })
220
+ );
221
+ }
222
+ } catch (error) {
223
+ dispatch(
224
+ addToast({
225
+ key: `error-cancelling-tasks`,
226
+ type: 'danger',
227
+ message: error.response?.data?.error || __('An error occurred.'),
228
+ })
229
+ );
230
+ }
231
+ };
232
+
233
+ const handleTaskAction = action => {
234
+ dispatch(cancelJobTasks(combinedQuery, action));
235
+ };
236
+
237
+ const OpenAllButton = () => (
238
+ <Button
239
+ aria-label="open all template invocations in new tab"
240
+ className="open-all-button"
241
+ isDisabled={selectedIds.length === 0}
242
+ isInline
243
+ onClick={() => handleOpenHosts('all')}
244
+ ouiaId="template-invocation-new-tab-button"
245
+ variant="link"
246
+ >
247
+ <Tooltip content={__('Open selected in new tab')}>
248
+ <OutlinedWindowRestoreIcon />
249
+ </Tooltip>
250
+ </Button>
251
+ );
252
+
253
+ const RerunSelectedButton = () => (
254
+ <Button
255
+ aria-label="rerun selected template invocations"
256
+ className="rerun-selected-button"
257
+ component="a"
258
+ href={foremanUrl(
259
+ `/job_invocations/${jobID}/rerun?search=(${jobSearchQuery}) AND (${combinedQuery})`
260
+ )}
261
+ // eslint-disable-next-line camelcase
262
+ isDisabled={selectedIds.length === 0 || !hasCreatePermission}
263
+ isInline
264
+ ouiaId="template-invocation-rerun-selected-button"
265
+ variant="secondary"
266
+ >
267
+ {__('Rerun')}
268
+ </Button>
269
+ );
270
+
271
+ return (
272
+ <>
273
+ <OpenAllButton />
274
+ <RerunSelectedButton />
275
+ <ActionsKebab
276
+ selectedIds={selectedIds}
277
+ failedCount={failedCount}
278
+ isTaskCancelable={isTaskCancelable}
279
+ hasCancelPermission={hasCancelPermission}
280
+ handleTaskAction={handleTaskAction}
281
+ handleOpenHosts={handleOpenHosts}
282
+ isDropdownOpen={isDropdownOpen}
283
+ setIsDropdownOpen={setIsDropdownOpen}
284
+ />
285
+ {showAlert && <PopupAlert setShowAlert={setShowAlert} />}
286
+ <OpenAllInvocationsModal
287
+ isOpen={isModalOpen}
288
+ onClose={() => setIsModalOpen(false)}
289
+ failedCount={failedCount}
290
+ failedHosts={failedHosts}
291
+ jobID={jobID}
292
+ isOpenFailed={isOpenFailed}
293
+ setShowAlert={setShowAlert}
294
+ selectedIds={selectedIds}
295
+ />
296
+ </>
297
+ );
298
+ };
299
+
300
+ ActionsKebab.propTypes = {
301
+ selectedIds: PropTypes.array.isRequired,
302
+ failedCount: PropTypes.number.isRequired,
303
+ isTaskCancelable: PropTypes.bool,
304
+ hasCancelPermission: PropTypes.bool,
305
+ handleTaskAction: PropTypes.func.isRequired,
306
+ handleOpenHosts: PropTypes.func.isRequired,
307
+ isDropdownOpen: PropTypes.bool.isRequired,
308
+ setIsDropdownOpen: PropTypes.func.isRequired,
309
+ };
310
+
311
+ ActionsKebab.defaultProps = {
312
+ isTaskCancelable: false,
313
+ hasCancelPermission: false,
314
+ };
315
+
316
+ CheckboxesActions.propTypes = {
317
+ selectedIds: PropTypes.array.isRequired,
318
+ failedCount: PropTypes.number.isRequired,
319
+ jobID: PropTypes.string.isRequired,
320
+ bulkParams: PropTypes.string,
321
+ filter: PropTypes.string,
322
+ };
323
+
324
+ CheckboxesActions.defaultProps = {
325
+ bulkParams: '',
326
+ filter: '',
327
+ };
@@ -10,10 +10,7 @@ import {
10
10
  } from '@patternfly/react-core';
11
11
  import { STATUS_TITLES } from './JobInvocationConstants';
12
12
 
13
- const JobInvocationHostTableToolbar = ({
14
- dropdownFilter,
15
- setDropdownFilter,
16
- }) => {
13
+ const DropdownFilter = ({ dropdownFilter, setDropdownFilter }) => {
17
14
  const [isOpen, setIsOpen] = React.useState(false);
18
15
  const onSelect = (_event, itemId) => {
19
16
  setDropdownFilter(itemId);
@@ -60,9 +57,9 @@ const JobInvocationHostTableToolbar = ({
60
57
  );
61
58
  };
62
59
 
63
- JobInvocationHostTableToolbar.propTypes = {
60
+ DropdownFilter.propTypes = {
64
61
  dropdownFilter: PropTypes.string.isRequired,
65
62
  setDropdownFilter: PropTypes.func.isRequired,
66
63
  };
67
64
 
68
- export default JobInvocationHostTableToolbar;
65
+ export default DropdownFilter;
@@ -1,7 +1,7 @@
1
1
  /* eslint-disable camelcase */
2
2
  import React from 'react';
3
- import { foremanUrl } from 'foremanReact/common/helpers';
4
3
  import { translate as __ } from 'foremanReact/common/I18n';
4
+ import { foremanUrl } from 'foremanReact/common/helpers';
5
5
  import { useForemanHostDetailsPageUrl } from 'foremanReact/Root/Context/ForemanContext';
6
6
  import JobStatusIcon from '../react_app/components/RecentJobsCard/JobStatusIcon';
7
7
 
@@ -16,18 +16,22 @@ export const CANCEL_RECURRING_LOGIC = 'CANCEL_RECURRING_LOGIC';
16
16
  export const GET_REPORT_TEMPLATES = 'GET_REPORT_TEMPLATES';
17
17
  export const GET_REPORT_TEMPLATE_INPUTS = 'GET_REPORT_TEMPLATE_INPUTS';
18
18
  export const JOB_INVOCATION_HOSTS = 'JOB_INVOCATION_HOSTS';
19
+ export const GET_TEMPLATE_INVOCATION = 'GET_TEMPLATE_INVOCATION';
20
+ export const MAX_HOSTS_API_SIZE = 100;
21
+ export const DIRECT_OPEN_HOST_LIMIT = 3;
22
+ export const ALL_JOB_HOSTS = 'ALL_JOB_HOSTS';
19
23
  export const currentPermissionsUrl = foremanUrl(
20
24
  '/api/v2/permissions/current_permissions'
21
25
  );
22
- export const GET_TEMPLATE_INVOCATION = 'GET_TEMPLATE_INVOCATION';
26
+
23
27
  export const showTemplateInvocationUrl = (hostID, jobID) =>
24
28
  `/show_template_invocation_by_host/${hostID}/job_invocation/${jobID}`;
29
+ export const LIST_TEMPLATE_INVOCATIONS = 'LIST_TEMPLATE_INVOCATIONS';
25
30
 
26
31
  export const templateInvocationPageUrl = (hostID, jobID) =>
27
32
  `/job_invocations_detail/${jobID}/host_invocation/${hostID}`;
28
33
 
29
- export const jobInvocationDetailsUrl = id =>
30
- `/experimental/job_invocations_detail/${id}`;
34
+ export const jobInvocationDetailsUrl = id => `/job_invocations/${id}`;
31
35
 
32
36
  export const STATUS = {
33
37
  PENDING: 'pending',
@@ -135,11 +139,6 @@ const Columns = () => {
135
139
  },
136
140
  weight: 5,
137
141
  },
138
- actions: {
139
- title: ' ',
140
- weight: 6,
141
- wrapper: () => null,
142
- },
143
142
  };
144
143
  };
145
144