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.
Files changed (43) hide show
  1. checksums.yaml +4 -4
  2. data/app/controllers/api/v2/job_invocations_controller.rb +34 -17
  3. data/app/helpers/remote_execution_helper.rb +2 -2
  4. data/app/lib/actions/remote_execution/proxy_action.rb +10 -5
  5. data/app/lib/actions/remote_execution/run_host_job.rb +1 -1
  6. data/app/lib/actions/remote_execution/template_invocation_progress_logging.rb +2 -3
  7. data/app/views/api/v2/job_invocations/hosts.json.rabl +15 -0
  8. data/config/routes.rb +1 -0
  9. data/db/migrate/20240312133027_extend_template_invocation_events.rb +19 -0
  10. data/lib/foreman_remote_execution/engine.rb +1 -1
  11. data/lib/foreman_remote_execution/version.rb +1 -1
  12. data/lib/tasks/foreman_remote_execution_tasks.rake +3 -0
  13. data/webpack/JobInvocationDetail/JobInvocationActions.js +1 -1
  14. data/webpack/JobInvocationDetail/JobInvocationConstants.js +84 -0
  15. data/webpack/JobInvocationDetail/JobInvocationDetail.scss +0 -1
  16. data/webpack/JobInvocationDetail/JobInvocationHostTable.js +210 -0
  17. data/webpack/JobInvocationDetail/JobInvocationSelectors.js +2 -2
  18. data/webpack/JobInvocationDetail/__tests__/MainInformation.test.js +5 -1
  19. data/webpack/JobInvocationDetail/__tests__/fixtures.js +9 -0
  20. data/webpack/JobInvocationDetail/index.js +56 -34
  21. data/webpack/__mocks__/foremanReact/components/HostDetails/DetailsCard/DefaultLoaderEmptyState.js +1 -2
  22. data/webpack/react_app/components/RecentJobsCard/JobStatusIcon.js +38 -7
  23. data/webpack/react_app/components/RecentJobsCard/constants.js +4 -0
  24. data/webpack/react_app/components/TargetingHosts/__tests__/__snapshots__/HostStatus.test.js.snap +1 -1
  25. data/webpack/react_app/components/TargetingHosts/components/HostStatus.js +6 -6
  26. metadata +6 -57
  27. data/.babelrc.js +0 -3
  28. data/.eslintignore +0 -3
  29. data/.eslintrc +0 -13
  30. data/.github/workflows/js_ci.yml +0 -32
  31. data/.github/workflows/release.yml +0 -16
  32. data/.github/workflows/ruby_ci.yml +0 -19
  33. data/.gitignore +0 -19
  34. data/.packit.yaml +0 -45
  35. data/.prettierrc +0 -4
  36. data/.rubocop.yml +0 -105
  37. data/.rubocop_todo.yml +0 -516
  38. data/.tx/config +0 -10
  39. data/Gemfile +0 -5
  40. data/app/mailers/.gitkeep +0 -0
  41. data/app/views/dashboard/.gitkeep +0 -0
  42. data/foreman_remote_execution.gemspec +0 -33
  43. data/jsconfig.json +0 -8
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: a73b13cdd10e99e44456d4b8dc1bbf8fe654dd6b83eb929e657df49d44b75078
4
- data.tar.gz: a09d30bd37238e0dadd1e8bb8b32d41292df56aea1a03f19fdd2ab6386664ab3
3
+ metadata.gz: 0c7d7e0afd2637caccf6c287144c92599fd6abb3acecdc27b4fdfa960bc12e8a
4
+ data.tar.gz: 85e3859a4e3c81e03cc0441222d79bec187156e942e1abd3f4aec17b67c1a044
5
5
  SHA512:
6
- metadata.gz: 6633435cacf5d2e835d8391bf2d035844d87db9be34c9fbd134d140a10559779b0f940109ba6feb8dfb0f35ab5b08accc5bec75c94e28ec12accc837c10c795c
7
- data.tar.gz: 0e0417551e20786be7c7c87d1e694596ef8f118dbf303fece462810a6d4577f7bd99208dc6f8555cfb91b87a73914fc96a20185d9d7e2a3f6e76103cca5db31a
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
- @hosts = @job_invocation.targeting.hosts.authorized(:view_hosts, Host)
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
- template_invocations = @template_invocations.includes(:run_host_job_task).to_a
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 template_invocation_status(template_invocation)
260
- task = template_invocation.try(:run_host_job_task)
261
- parent_task = @job_invocation.task
262
-
263
- return(parent_task.result == 'cancelled' ? 'cancelled' : 'N/A') if task.nil?
264
- return task.state if task.state == 'running' || task.state == 'planned'
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
- task.result
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' ? _('cancelled') : _('Awaiting start')) if task.nil?
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 _('error') if task.result == 'warning'
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
- sequence_id: update['sequence_id'] || seq_id,
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 || {:sequence_id => -1, :timestamp => Time.zone.now}
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
- sequence_id: last[:sequence_id] + 1,
51
+ external_id: 'exit',
47
52
  template_invocation_id: template_invocation.id,
48
53
  event: data['exit_status'],
49
- timestamp: last[: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, :sequence_id]) # rubocop:disable Rails/SkipsModelValidations
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(:sequence_id).find_each do |output|
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
- last = template_invocation.template_invocation_events.order(:sequence_id).last
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
- :sequence_id => id
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
@@ -63,6 +63,7 @@ Rails.application.routes.draw do
63
63
  get '/raw', :to => 'job_invocations#raw_output'
64
64
  end
65
65
  member do
66
+ get 'hosts'
66
67
  post 'cancel'
67
68
  post 'rerun'
68
69
  get 'template_invocations', :to => 'template_invocations#template_invocations'
@@ -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,3 +1,3 @@
1
1
  module ForemanRemoteExecution
2
- VERSION = '14.0.2'.freeze
2
+ VERSION = '14.1.1'.freeze
3
3
  end
@@ -17,6 +17,9 @@ namespace :test do
17
17
  t.pattern = "#{test_dir}/**/*_test.rb"
18
18
  t.verbose = true
19
19
  t.warning = false
20
+ t.test_files = [
21
+ Rails.root.join('test/unit/foreman/access_permissions_test.rb'),
22
+ ]
20
23
  end
21
24
  end
22
25
 
@@ -15,7 +15,7 @@ import {
15
15
  UPDATE_JOB,
16
16
  } from './JobInvocationConstants';
17
17
 
18
- export const getData = url => dispatch => {
18
+ export const getJobInvocation = url => dispatch => {
19
19
  const fetchData = withInterval(
20
20
  get({
21
21
  key: JOB_INVOCATION_KEY,
@@ -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;
@@ -38,4 +38,3 @@
38
38
  height: $chart_size;
39
39
  }
40
40
  }
41
-
@@ -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, 'GET_TASK');
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',