foreman_remote_execution 14.0.2 → 14.1.1
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 +34 -17
- data/app/helpers/remote_execution_helper.rb +2 -2
- data/app/lib/actions/remote_execution/proxy_action.rb +10 -5
- data/app/lib/actions/remote_execution/run_host_job.rb +1 -1
- data/app/lib/actions/remote_execution/template_invocation_progress_logging.rb +2 -3
- data/app/views/api/v2/job_invocations/hosts.json.rabl +15 -0
- data/config/routes.rb +1 -0
- data/db/migrate/20240312133027_extend_template_invocation_events.rb +19 -0
- data/lib/foreman_remote_execution/engine.rb +1 -1
- data/lib/foreman_remote_execution/version.rb +1 -1
- data/lib/tasks/foreman_remote_execution_tasks.rake +3 -0
- data/webpack/JobInvocationDetail/JobInvocationActions.js +1 -1
- data/webpack/JobInvocationDetail/JobInvocationConstants.js +84 -0
- data/webpack/JobInvocationDetail/JobInvocationDetail.scss +0 -1
- data/webpack/JobInvocationDetail/JobInvocationHostTable.js +210 -0
- data/webpack/JobInvocationDetail/JobInvocationSelectors.js +2 -2
- data/webpack/JobInvocationDetail/__tests__/MainInformation.test.js +5 -1
- data/webpack/JobInvocationDetail/__tests__/fixtures.js +9 -0
- data/webpack/JobInvocationDetail/index.js +56 -34
- data/webpack/__mocks__/foremanReact/components/HostDetails/DetailsCard/DefaultLoaderEmptyState.js +1 -2
- data/webpack/react_app/components/RecentJobsCard/JobStatusIcon.js +38 -7
- data/webpack/react_app/components/RecentJobsCard/constants.js +4 -0
- data/webpack/react_app/components/TargetingHosts/__tests__/__snapshots__/HostStatus.test.js.snap +1 -1
- data/webpack/react_app/components/TargetingHosts/components/HostStatus.js +6 -6
- metadata +6 -57
- data/.babelrc.js +0 -3
- data/.eslintignore +0 -3
- data/.eslintrc +0 -13
- data/.github/workflows/js_ci.yml +0 -32
- data/.github/workflows/release.yml +0 -16
- data/.github/workflows/ruby_ci.yml +0 -19
- data/.gitignore +0 -19
- data/.packit.yaml +0 -45
- data/.prettierrc +0 -4
- data/.rubocop.yml +0 -105
- data/.rubocop_todo.yml +0 -516
- data/.tx/config +0 -10
- data/Gemfile +0 -5
- data/app/mailers/.gitkeep +0 -0
- data/app/views/dashboard/.gitkeep +0 -0
- data/foreman_remote_execution.gemspec +0 -33
- data/jsconfig.json +0 -8
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 0c7d7e0afd2637caccf6c287144c92599fd6abb3acecdc27b4fdfa960bc12e8a
|
4
|
+
data.tar.gz: 85e3859a4e3c81e03cc0441222d79bec187156e942e1abd3f4aec17b67c1a044
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: e24ea3d8af7755d15f5ed7b72b606590835414521b5bd100b137fcffdf0496bb6a2f52d94140622f18ed7121bf07725ffed2be76d7ef4ca648664f6b47a9b385
|
7
|
+
data.tar.gz: b8ef21fe312ee53f7dd60e746c0fed2e8e60386c06202cddc6c3ad23da3beaccdbd0f01214517a4a80c17546d1c32fa74a80c4de4a0d783be1a78dda8703ddcd
|
@@ -3,10 +3,11 @@ module Api
|
|
3
3
|
class JobInvocationsController < ::Api::V2::BaseController
|
4
4
|
include ::Api::Version2
|
5
5
|
include ::Foreman::Renderer
|
6
|
+
include RemoteExecutionHelper
|
6
7
|
|
7
8
|
before_action :find_optional_nested_object, :only => %w{output raw_output}
|
8
9
|
before_action :find_host, :only => %w{output raw_output}
|
9
|
-
before_action :find_resource, :only => %w{show update destroy clone cancel rerun outputs}
|
10
|
+
before_action :find_resource, :only => %w{show update destroy clone cancel rerun outputs hosts}
|
10
11
|
|
11
12
|
wrap_parameters JobInvocation, :include => (JobInvocation.attribute_names + [:ssh])
|
12
13
|
|
@@ -20,14 +21,9 @@ module Api
|
|
20
21
|
param :id, :identifier, :required => true
|
21
22
|
param :host_status, :bool, required: false, desc: N_('Show Job status for the hosts')
|
22
23
|
def show
|
23
|
-
|
24
|
-
@template_invocations = @job_invocation.template_invocations
|
25
|
-
.where(host: @hosts)
|
26
|
-
.includes(:input_values)
|
27
|
-
|
24
|
+
set_hosts_and_template_invocations
|
28
25
|
if params[:host_status] == 'true'
|
29
|
-
|
30
|
-
@host_statuses = Hash[template_invocations.map { |ti| [ti.host_id, template_invocation_status(ti)] }]
|
26
|
+
set_statuses_and_smart_proxies
|
31
27
|
end
|
32
28
|
end
|
33
29
|
|
@@ -111,6 +107,19 @@ module Api
|
|
111
107
|
render :json => host_output(@nested_obj, @host, :default => [], :since => params[:since])
|
112
108
|
end
|
113
109
|
|
110
|
+
api :GET, '/job_invocations/:id/hosts', N_('List hosts belonging to job invocation')
|
111
|
+
param_group :search_and_pagination, ::Api::V2::BaseController
|
112
|
+
add_scoped_search_description_for(JobInvocation)
|
113
|
+
param :id, :identifier, :required => true
|
114
|
+
def hosts
|
115
|
+
set_hosts_and_template_invocations
|
116
|
+
set_statuses_and_smart_proxies
|
117
|
+
@total = @job_invocation.targeting.hosts.size
|
118
|
+
@hosts = @hosts.search_for(params[:search], :order => params[:order]).paginate(:page => params[:page], :per_page => params[:per_page])
|
119
|
+
@subtotal = @hosts.respond_to?(:total_entries) ? @hosts.total_entries : @hosts.sizes
|
120
|
+
render :hosts, :layout => 'api/v2/layouts/index_layout'
|
121
|
+
end
|
122
|
+
|
114
123
|
api :GET, '/job_invocations/:id/hosts/:host_id/raw', N_('Get raw output for a host')
|
115
124
|
param :id, :identifier, :required => true
|
116
125
|
param :host_id, :identifier, :required => true
|
@@ -187,7 +196,7 @@ module Api
|
|
187
196
|
|
188
197
|
def action_permission
|
189
198
|
case params[:action]
|
190
|
-
when 'output', 'raw_output', 'outputs'
|
199
|
+
when 'output', 'raw_output', 'outputs', 'hosts'
|
191
200
|
:view
|
192
201
|
when 'cancel'
|
193
202
|
:cancel
|
@@ -256,15 +265,23 @@ module Api
|
|
256
265
|
resource_class.where(nil)
|
257
266
|
end
|
258
267
|
|
259
|
-
def
|
260
|
-
|
261
|
-
|
262
|
-
|
263
|
-
|
264
|
-
|
265
|
-
return 'error' if task.result == 'warning'
|
268
|
+
def set_hosts_and_template_invocations
|
269
|
+
@hosts = @job_invocation.targeting.hosts.authorized(:view_hosts, Host)
|
270
|
+
@template_invocations = @job_invocation.template_invocations
|
271
|
+
.where(host: @hosts)
|
272
|
+
.includes(:input_values)
|
273
|
+
end
|
266
274
|
|
267
|
-
|
275
|
+
def set_statuses_and_smart_proxies
|
276
|
+
template_invocations = @template_invocations.includes(:run_host_job_task).to_a
|
277
|
+
hosts = @hosts.to_a
|
278
|
+
@host_statuses = Hash[hosts.map do |host|
|
279
|
+
template_invocation = template_invocations.find { |ti| ti.host_id == host.id }
|
280
|
+
task = template_invocation.try(:run_host_job_task)
|
281
|
+
[host.id, template_invocation_status(task, @job_invocation.task)]
|
282
|
+
end]
|
283
|
+
@smart_proxy_id = Hash[template_invocations.map { |ti| [ti.host_id, ti.smart_proxy_id] }]
|
284
|
+
@smart_proxy_name = Hash[template_invocations.map { |ti| [ti.host_id, ti.smart_proxy_name] }]
|
268
285
|
end
|
269
286
|
end
|
270
287
|
end
|
@@ -18,9 +18,9 @@ module RemoteExecutionHelper
|
|
18
18
|
end
|
19
19
|
|
20
20
|
def template_invocation_status(task, parent_task)
|
21
|
-
return(parent_task.result == 'cancelled' ?
|
21
|
+
return(parent_task.result == 'cancelled' ? 'cancelled' : 'N/A') if task.nil?
|
22
22
|
return task.state if task.state == 'running' || task.state == 'planned'
|
23
|
-
return
|
23
|
+
return 'error' if task.result == 'warning'
|
24
24
|
|
25
25
|
task.result
|
26
26
|
end
|
@@ -33,7 +33,7 @@ module Actions
|
|
33
33
|
{
|
34
34
|
# For N-1 compatibility, we assume that the output provided here is
|
35
35
|
# complete
|
36
|
-
|
36
|
+
external_id: update['id'] || seq_id,
|
37
37
|
template_invocation_id: template_invocation.id,
|
38
38
|
event: update['output'],
|
39
39
|
timestamp: Time.at(update['timestamp']).getlocal,
|
@@ -41,17 +41,22 @@ module Actions
|
|
41
41
|
}
|
42
42
|
end
|
43
43
|
if data['exit_status']
|
44
|
-
last = events.last || {:
|
44
|
+
last = events.last || {:timestamp => Time.zone.now}
|
45
|
+
exit_timestamp = if data['exit_status_timestamp']
|
46
|
+
Time.at(data['exit_status_timestamp']).getlocal
|
47
|
+
else
|
48
|
+
last[:timestamp] + 1
|
49
|
+
end
|
45
50
|
events << {
|
46
|
-
|
51
|
+
external_id: 'exit',
|
47
52
|
template_invocation_id: template_invocation.id,
|
48
53
|
event: data['exit_status'],
|
49
|
-
timestamp:
|
54
|
+
timestamp: exit_timestamp,
|
50
55
|
event_type: 'exit',
|
51
56
|
}
|
52
57
|
end
|
53
58
|
events.each_slice(1000) do |batch|
|
54
|
-
TemplateInvocationEvent.insert_all(batch, unique_by: [:template_invocation_id, :
|
59
|
+
TemplateInvocationEvent.insert_all(batch, unique_by: [:template_invocation_id, :external_id]) # rubocop:disable Rails/SkipsModelValidations
|
55
60
|
end
|
56
61
|
end
|
57
62
|
end
|
@@ -160,7 +160,7 @@ module Actions
|
|
160
160
|
# This is enough, the error will get shown using add_exception at the end of the method
|
161
161
|
end
|
162
162
|
|
163
|
-
task.template_invocation.template_invocation_events.order(:
|
163
|
+
task.template_invocation.template_invocation_events.order(:timestamp).find_each do |output|
|
164
164
|
if output.event_type == 'exit'
|
165
165
|
continuous_output.add_output(_('Exit status: %s') % output.event, 'stdout', output.timestamp)
|
166
166
|
else
|
@@ -6,13 +6,12 @@ module Actions
|
|
6
6
|
end
|
7
7
|
|
8
8
|
def log_template_invocation_exception(exception)
|
9
|
-
|
10
|
-
id = last ? last.sequence_id + 1 : 0
|
9
|
+
id = 'exception-' + SecureRandom.hex(4)
|
11
10
|
template_invocation.template_invocation_events.create!(
|
12
11
|
:event_type => 'debug',
|
13
12
|
:event => "#{exception.class}: #{exception.message}",
|
14
13
|
:timestamp => Time.zone.now,
|
15
|
-
:
|
14
|
+
:external_id => id
|
16
15
|
)
|
17
16
|
end
|
18
17
|
|
@@ -0,0 +1,15 @@
|
|
1
|
+
collection @hosts
|
2
|
+
|
3
|
+
attribute :name, :operatingsystem_id, :operatingsystem_name, :hostgroup_id, :hostgroup_name
|
4
|
+
|
5
|
+
node :job_status do |host|
|
6
|
+
@host_statuses[host.id]
|
7
|
+
end
|
8
|
+
|
9
|
+
node :smart_proxy_id do |host|
|
10
|
+
@smart_proxy_id[host.id]
|
11
|
+
end
|
12
|
+
|
13
|
+
node :smart_proxy_name do |host|
|
14
|
+
@smart_proxy_name[host.id]
|
15
|
+
end
|
data/config/routes.rb
CHANGED
@@ -0,0 +1,19 @@
|
|
1
|
+
class ExtendTemplateInvocationEvents < ActiveRecord::Migration[6.1]
|
2
|
+
def up
|
3
|
+
change_table :template_invocation_events do |t|
|
4
|
+
t.string :external_id
|
5
|
+
end
|
6
|
+
|
7
|
+
TemplateInvocationEvent.update_all("external_id = CASE WHEN event_type = 'exit' THEN 'exit' ELSE sequence_id::varchar END")
|
8
|
+
|
9
|
+
remove_index :template_invocation_events, name: :unique_template_invocation_events_index
|
10
|
+
remove_column :template_invocation_events, :sequence_id
|
11
|
+
|
12
|
+
change_table :template_invocation_events do |t|
|
13
|
+
t.index [:template_invocation_id, :external_id],
|
14
|
+
unique: true,
|
15
|
+
name: 'unique_template_invocation_events_index'
|
16
|
+
t.change :external_id, :string, null: false
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
@@ -176,7 +176,7 @@ module ForemanRemoteExecution
|
|
176
176
|
permission :create_job_invocations, { :job_invocations => [:new, :create, :legacy_create, :refresh, :rerun, :preview_hosts],
|
177
177
|
'api/v2/job_invocations' => [:create, :rerun] }, :resource_type => 'JobInvocation'
|
178
178
|
permission :view_job_invocations, { :job_invocations => [:index, :chart, :show, :auto_complete_search, :preview_job_invocations_per_host], :template_invocations => [:show],
|
179
|
-
'api/v2/job_invocations' => [:index, :show, :output, :raw_output, :outputs] }, :resource_type => 'JobInvocation'
|
179
|
+
'api/v2/job_invocations' => [:index, :show, :output, :raw_output, :outputs, :hosts] }, :resource_type => 'JobInvocation'
|
180
180
|
permission :view_template_invocations, { :template_invocations => [:show],
|
181
181
|
'api/v2/template_invocations' => [:template_invocations], :ui_job_wizard => [:job_invocation] }, :resource_type => 'TemplateInvocation'
|
182
182
|
permission :create_template_invocations, {}, :resource_type => 'TemplateInvocation'
|
@@ -1,14 +1,21 @@
|
|
1
|
+
/* eslint-disable camelcase */
|
2
|
+
import React from 'react';
|
1
3
|
import { foremanUrl } from 'foremanReact/common/helpers';
|
4
|
+
import { translate as __ } from 'foremanReact/common/I18n';
|
5
|
+
import { useForemanHostDetailsPageUrl } from 'foremanReact/Root/Context/ForemanContext';
|
6
|
+
import JobStatusIcon from '../react_app/components/RecentJobsCard/JobStatusIcon';
|
2
7
|
|
3
8
|
export const JOB_INVOCATION_KEY = 'JOB_INVOCATION_KEY';
|
4
9
|
export const CURRENT_PERMISSIONS = 'CURRENT_PERMISSIONS';
|
5
10
|
export const UPDATE_JOB = 'UPDATE_JOB';
|
6
11
|
export const CANCEL_JOB = 'CANCEL_JOB';
|
7
12
|
export const GET_TASK = 'GET_TASK';
|
13
|
+
export const GET_TEMPLATE_INVOCATIONS = 'GET_TEMPLATE_INVOCATIONS';
|
8
14
|
export const CHANGE_ENABLED_RECURRING_LOGIC = 'CHANGE_ENABLED_RECURRING_LOGIC';
|
9
15
|
export const CANCEL_RECURRING_LOGIC = 'CANCEL_RECURRING_LOGIC';
|
10
16
|
export const GET_REPORT_TEMPLATES = 'GET_REPORT_TEMPLATES';
|
11
17
|
export const GET_REPORT_TEMPLATE_INPUTS = 'GET_REPORT_TEMPLATE_INPUTS';
|
18
|
+
export const JOB_INVOCATION_HOSTS = 'JOB_INVOCATION_HOSTS';
|
12
19
|
export const currentPermissionsUrl = foremanUrl(
|
13
20
|
'/api/v2/permissions/current_permissions'
|
14
21
|
);
|
@@ -20,6 +27,12 @@ export const STATUS = {
|
|
20
27
|
CANCELLED: 'cancelled',
|
21
28
|
};
|
22
29
|
|
30
|
+
export const STATUS_UPPERCASE = {
|
31
|
+
RESOLVED: 'RESOLVED',
|
32
|
+
ERROR: 'ERROR',
|
33
|
+
PENDING: 'PENDING',
|
34
|
+
};
|
35
|
+
|
23
36
|
export const DATE_OPTIONS = {
|
24
37
|
day: 'numeric',
|
25
38
|
month: 'short',
|
@@ -29,3 +42,74 @@ export const DATE_OPTIONS = {
|
|
29
42
|
hour12: false,
|
30
43
|
timeZoneName: 'short',
|
31
44
|
};
|
45
|
+
|
46
|
+
const Columns = () => {
|
47
|
+
const getColumnsStatus = ({ hostJobStatus }) => {
|
48
|
+
switch (hostJobStatus) {
|
49
|
+
case 'success':
|
50
|
+
return { title: __('Succeeded'), status: 0 };
|
51
|
+
case 'error':
|
52
|
+
return { title: __('Failed'), status: 1 };
|
53
|
+
case 'planned':
|
54
|
+
return { title: __('Scheduled'), status: 2 };
|
55
|
+
case 'running':
|
56
|
+
return { title: __('Pending'), status: 3 };
|
57
|
+
case 'cancelled':
|
58
|
+
return { title: __('Cancelled'), status: 4 };
|
59
|
+
case 'N/A':
|
60
|
+
return { title: __('Awaiting start'), status: 5 };
|
61
|
+
default:
|
62
|
+
return { title: hostJobStatus, status: 6 };
|
63
|
+
}
|
64
|
+
};
|
65
|
+
const hostDetailsPageUrl = useForemanHostDetailsPageUrl();
|
66
|
+
|
67
|
+
return {
|
68
|
+
name: {
|
69
|
+
title: __('Name'),
|
70
|
+
wrapper: ({ name }) => (
|
71
|
+
<a href={`${hostDetailsPageUrl}${name}`}>{name}</a>
|
72
|
+
),
|
73
|
+
weight: 1,
|
74
|
+
},
|
75
|
+
groups: {
|
76
|
+
title: __('Host group'),
|
77
|
+
wrapper: ({ hostgroup_id, hostgroup_name }) => (
|
78
|
+
<a href={`/hostgroups/${hostgroup_id}/edit`}>{hostgroup_name}</a>
|
79
|
+
),
|
80
|
+
weight: 2,
|
81
|
+
},
|
82
|
+
os: {
|
83
|
+
title: __('OS'),
|
84
|
+
wrapper: ({ operatingsystem_id, operatingsystem_name }) => (
|
85
|
+
<a href={`/operatingsystems/${operatingsystem_id}/edit`}>
|
86
|
+
{operatingsystem_name}
|
87
|
+
</a>
|
88
|
+
),
|
89
|
+
weight: 3,
|
90
|
+
},
|
91
|
+
smart_proxy: {
|
92
|
+
title: __('Smart proxy'),
|
93
|
+
wrapper: ({ smart_proxy_name, smart_proxy_id }) => (
|
94
|
+
<a href={`/smart_proxies/${smart_proxy_id}`}>{smart_proxy_name}</a>
|
95
|
+
),
|
96
|
+
weight: 4,
|
97
|
+
},
|
98
|
+
status: {
|
99
|
+
title: __('Status'),
|
100
|
+
wrapper: ({ job_status }) => {
|
101
|
+
const { title, status } = getColumnsStatus({
|
102
|
+
hostJobStatus: job_status,
|
103
|
+
});
|
104
|
+
return (
|
105
|
+
<JobStatusIcon status={status}>
|
106
|
+
{title || __('Unknown')}
|
107
|
+
</JobStatusIcon>
|
108
|
+
);
|
109
|
+
},
|
110
|
+
weight: 5,
|
111
|
+
},
|
112
|
+
};
|
113
|
+
};
|
114
|
+
|
115
|
+
export default Columns;
|
@@ -0,0 +1,210 @@
|
|
1
|
+
/* eslint-disable camelcase */
|
2
|
+
import PropTypes from 'prop-types';
|
3
|
+
import React, { useMemo, useEffect } from 'react';
|
4
|
+
import { Icon } from 'patternfly-react';
|
5
|
+
import { translate as __ } from 'foremanReact/common/I18n';
|
6
|
+
import { FormattedMessage } from 'react-intl';
|
7
|
+
import { Tr, Td } from '@patternfly/react-table';
|
8
|
+
import {
|
9
|
+
Title,
|
10
|
+
EmptyState,
|
11
|
+
EmptyStateVariant,
|
12
|
+
EmptyStateBody,
|
13
|
+
} from '@patternfly/react-core';
|
14
|
+
import { foremanUrl } from 'foremanReact/common/helpers';
|
15
|
+
import { useAPI } from 'foremanReact/common/hooks/API/APIHooks';
|
16
|
+
import { Table } from 'foremanReact/components/PF4/TableIndexPage/Table/Table';
|
17
|
+
import TableIndexPage from 'foremanReact/components/PF4/TableIndexPage/TableIndexPage';
|
18
|
+
import { useSetParamsAndApiAndSearch } from 'foremanReact/components/PF4/TableIndexPage/Table/TableIndexHooks';
|
19
|
+
import {
|
20
|
+
useBulkSelect,
|
21
|
+
useUrlParams,
|
22
|
+
} from 'foremanReact/components/PF4/TableIndexPage/Table/TableHooks';
|
23
|
+
import Pagination from 'foremanReact/components/Pagination';
|
24
|
+
import { getControllerSearchProps } from 'foremanReact/constants';
|
25
|
+
import Columns, {
|
26
|
+
JOB_INVOCATION_HOSTS,
|
27
|
+
STATUS_UPPERCASE,
|
28
|
+
} from './JobInvocationConstants';
|
29
|
+
|
30
|
+
const JobInvocationHostTable = ({ id, targeting, finished, autoRefresh }) => {
|
31
|
+
const columns = Columns();
|
32
|
+
const columnNamesKeys = Object.keys(columns);
|
33
|
+
const apiOptions = { key: JOB_INVOCATION_HOSTS };
|
34
|
+
const {
|
35
|
+
searchParam: urlSearchQuery = '',
|
36
|
+
page: urlPage,
|
37
|
+
per_page: urlPerPage,
|
38
|
+
} = useUrlParams();
|
39
|
+
const defaultParams = { search: urlSearchQuery };
|
40
|
+
if (urlPage) defaultParams.page = Number(urlPage);
|
41
|
+
if (urlPerPage) defaultParams.per_page = Number(urlPerPage);
|
42
|
+
const { response, status, setAPIOptions } = useAPI(
|
43
|
+
'get',
|
44
|
+
`/api/job_invocations/${id}/hosts`,
|
45
|
+
{
|
46
|
+
params: { ...defaultParams, key: JOB_INVOCATION_HOSTS },
|
47
|
+
}
|
48
|
+
);
|
49
|
+
|
50
|
+
const combinedResponse = {
|
51
|
+
response: {
|
52
|
+
search: urlSearchQuery,
|
53
|
+
can_create: false,
|
54
|
+
results: response?.results || [],
|
55
|
+
total: response?.total || 0,
|
56
|
+
per_page: response?.perPage,
|
57
|
+
page: response?.page,
|
58
|
+
subtotal: response?.subtotal || 0,
|
59
|
+
message: response?.message || 'error',
|
60
|
+
},
|
61
|
+
status,
|
62
|
+
setAPIOptions,
|
63
|
+
};
|
64
|
+
|
65
|
+
const { setParamsAndAPI, params } = useSetParamsAndApiAndSearch({
|
66
|
+
defaultParams,
|
67
|
+
apiOptions,
|
68
|
+
setAPIOptions: combinedResponse.setAPIOptions,
|
69
|
+
});
|
70
|
+
|
71
|
+
const { updateSearchQuery } = useBulkSelect({
|
72
|
+
initialSearchQuery: urlSearchQuery,
|
73
|
+
});
|
74
|
+
|
75
|
+
const controller = 'hosts';
|
76
|
+
const memoDefaultSearchProps = useMemo(
|
77
|
+
() => getControllerSearchProps(controller),
|
78
|
+
[controller]
|
79
|
+
);
|
80
|
+
memoDefaultSearchProps.autocomplete.url = foremanUrl(
|
81
|
+
`/${controller}/auto_complete_search`
|
82
|
+
);
|
83
|
+
|
84
|
+
useEffect(() => {
|
85
|
+
const intervalId = setInterval(() => {
|
86
|
+
if (!finished || autoRefresh) {
|
87
|
+
setAPIOptions(prevOptions => ({
|
88
|
+
...prevOptions,
|
89
|
+
params: {
|
90
|
+
...prevOptions.params,
|
91
|
+
},
|
92
|
+
}));
|
93
|
+
}
|
94
|
+
}, 5000);
|
95
|
+
|
96
|
+
return () => {
|
97
|
+
clearInterval(intervalId);
|
98
|
+
};
|
99
|
+
}, [finished, autoRefresh, setAPIOptions]);
|
100
|
+
|
101
|
+
const onPagination = newPagination => {
|
102
|
+
setParamsAndAPI({
|
103
|
+
...params,
|
104
|
+
...newPagination,
|
105
|
+
search: urlSearchQuery,
|
106
|
+
});
|
107
|
+
};
|
108
|
+
|
109
|
+
const bottomPagination = (
|
110
|
+
<Pagination
|
111
|
+
ouiaId="table-hosts-bottom-pagination"
|
112
|
+
key="table-bottom-pagination"
|
113
|
+
page={params.page}
|
114
|
+
perPage={params.perPage}
|
115
|
+
itemCount={response?.subtotal}
|
116
|
+
onChange={onPagination}
|
117
|
+
/>
|
118
|
+
);
|
119
|
+
|
120
|
+
const customEmptyState = (
|
121
|
+
<Tr ouiaId="table-empty">
|
122
|
+
<Td colSpan={100}>
|
123
|
+
<EmptyState variant={EmptyStateVariant.xl}>
|
124
|
+
<span className="empty-state-icon">
|
125
|
+
<Icon name="add-circle-o" type="pf" size="2x" />
|
126
|
+
</span>
|
127
|
+
<Title ouiaId="empty-state-header" headingLevel="h5" size="4xl">
|
128
|
+
{__('No Results')}
|
129
|
+
</Title>
|
130
|
+
<EmptyStateBody>
|
131
|
+
<div className="empty-state-description">
|
132
|
+
{targeting?.targeting_type === 'dynamic_query' ? (
|
133
|
+
<FormattedMessage
|
134
|
+
id="view-dynamic-hosts"
|
135
|
+
defaultMessage={__(
|
136
|
+
'The dynamic query is still being processed. You can {viewTheHosts} targeted by the query.'
|
137
|
+
)}
|
138
|
+
values={{
|
139
|
+
viewTheHosts: (
|
140
|
+
<a href={`/new/hosts?search=${targeting?.search_query}`}>
|
141
|
+
{__('view the hosts')}
|
142
|
+
</a>
|
143
|
+
),
|
144
|
+
}}
|
145
|
+
/>
|
146
|
+
) : (
|
147
|
+
__('No hosts found')
|
148
|
+
)}
|
149
|
+
</div>
|
150
|
+
</EmptyStateBody>
|
151
|
+
</EmptyState>
|
152
|
+
</Td>
|
153
|
+
</Tr>
|
154
|
+
);
|
155
|
+
|
156
|
+
return (
|
157
|
+
<TableIndexPage
|
158
|
+
apiUrl=""
|
159
|
+
apiOptions={apiOptions}
|
160
|
+
customSearchProps={memoDefaultSearchProps}
|
161
|
+
controller="hosts"
|
162
|
+
creatable={false}
|
163
|
+
replacementResponse={combinedResponse}
|
164
|
+
updateSearchQuery={updateSearchQuery}
|
165
|
+
>
|
166
|
+
<Table
|
167
|
+
ouiaId="job-invocation-hosts-table"
|
168
|
+
columns={columns}
|
169
|
+
customEmptyState={
|
170
|
+
status === STATUS_UPPERCASE.RESOLVED && !response?.results?.length
|
171
|
+
? customEmptyState
|
172
|
+
: null
|
173
|
+
}
|
174
|
+
params={params}
|
175
|
+
setParams={setParamsAndAPI}
|
176
|
+
itemCount={response?.subtotal}
|
177
|
+
results={response?.results}
|
178
|
+
url=""
|
179
|
+
refreshData={() => {}}
|
180
|
+
errorMessage={
|
181
|
+
status === STATUS_UPPERCASE.ERROR && response?.message
|
182
|
+
? response.message
|
183
|
+
: null
|
184
|
+
}
|
185
|
+
isPending={status === STATUS_UPPERCASE.PENDING}
|
186
|
+
isDeleteable={false}
|
187
|
+
bottomPagination={bottomPagination}
|
188
|
+
>
|
189
|
+
{response?.results?.map((result, rowIndex) => (
|
190
|
+
<Tr key={rowIndex} ouiaId={`table-row-${rowIndex}`}>
|
191
|
+
{columnNamesKeys.map(k => (
|
192
|
+
<Td key={k}>{columns[k].wrapper(result)}</Td>
|
193
|
+
))}
|
194
|
+
</Tr>
|
195
|
+
))}
|
196
|
+
</Table>
|
197
|
+
</TableIndexPage>
|
198
|
+
);
|
199
|
+
};
|
200
|
+
|
201
|
+
JobInvocationHostTable.propTypes = {
|
202
|
+
id: PropTypes.string.isRequired,
|
203
|
+
targeting: PropTypes.object.isRequired,
|
204
|
+
finished: PropTypes.bool.isRequired,
|
205
|
+
autoRefresh: PropTypes.bool.isRequired,
|
206
|
+
};
|
207
|
+
|
208
|
+
JobInvocationHostTable.defaultProps = {};
|
209
|
+
|
210
|
+
export default JobInvocationHostTable;
|
@@ -1,10 +1,10 @@
|
|
1
1
|
import { selectAPIResponse } from 'foremanReact/redux/API/APISelectors';
|
2
|
-
import { JOB_INVOCATION_KEY } from './JobInvocationConstants';
|
2
|
+
import { JOB_INVOCATION_KEY, GET_TASK } from './JobInvocationConstants';
|
3
3
|
|
4
4
|
export const selectItems = state =>
|
5
5
|
selectAPIResponse(state, JOB_INVOCATION_KEY);
|
6
6
|
|
7
|
-
export const selectTask = state => selectAPIResponse(state,
|
7
|
+
export const selectTask = state => selectAPIResponse(state, GET_TASK);
|
8
8
|
|
9
9
|
export const selectTaskCancelable = state =>
|
10
10
|
selectTask(state).available_actions?.cancellable || false;
|
@@ -76,6 +76,10 @@ api.get.mockImplementation(({ handleSuccess, ...action }) => {
|
|
76
76
|
return { type: 'get', ...action };
|
77
77
|
});
|
78
78
|
|
79
|
+
jest.mock('../JobInvocationHostTable.js', () => () => (
|
80
|
+
<div data-testid="mock-table">Mock Table</div>
|
81
|
+
));
|
82
|
+
|
79
83
|
const reportTemplateJobId = mockReportTemplatesResponse.results[0].id;
|
80
84
|
|
81
85
|
const mockStore = configureMockStore([thunk]);
|
@@ -207,7 +211,7 @@ describe('JobInvocationDetailPage', () => {
|
|
207
211
|
{ key: GET_REPORT_TEMPLATES, url: '/api/report_templates' },
|
208
212
|
{
|
209
213
|
key: JOB_INVOCATION_KEY,
|
210
|
-
url: `/api/job_invocations/${jobId}`,
|
214
|
+
url: `/api/job_invocations/${jobId}?host_status=true`,
|
211
215
|
},
|
212
216
|
{
|
213
217
|
key: GET_REPORT_TEMPLATE_INPUTS,
|
@@ -1,4 +1,7 @@
|
|
1
1
|
export const jobInvocationData = {
|
2
|
+
search: '',
|
3
|
+
per_page: 20,
|
4
|
+
page: 1,
|
2
5
|
id: 123,
|
3
6
|
description: 'Description',
|
4
7
|
job_category: 'Commands',
|
@@ -40,6 +43,9 @@ export const jobInvocationData = {
|
|
40
43
|
};
|
41
44
|
|
42
45
|
export const jobInvocationDataScheduled = {
|
46
|
+
search: '',
|
47
|
+
per_page: 20,
|
48
|
+
page: 1,
|
43
49
|
id: 456,
|
44
50
|
description: 'Description',
|
45
51
|
job_category: 'Commands',
|
@@ -62,6 +68,9 @@ export const jobInvocationDataScheduled = {
|
|
62
68
|
};
|
63
69
|
|
64
70
|
export const jobInvocationDataRecurring = {
|
71
|
+
search: '',
|
72
|
+
per_page: 20,
|
73
|
+
page: 1,
|
65
74
|
id: 789,
|
66
75
|
description: 'Description',
|
67
76
|
job_category: 'Commands',
|