foreman_remote_execution 14.0.2 → 14.1.1
Sign up to get free protection for your applications and to get access to all the features.
- 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',
|