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.
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',