foreman_remote_execution 14.0.2 → 14.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (78) 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/version.rb +1 -1
  11. data/webpack/JobInvocationDetail/JobInvocationActions.js +1 -1
  12. data/webpack/JobInvocationDetail/JobInvocationConstants.js +84 -0
  13. data/webpack/JobInvocationDetail/JobInvocationDetail.scss +0 -1
  14. data/webpack/JobInvocationDetail/JobInvocationHostTable.js +210 -0
  15. data/webpack/JobInvocationDetail/JobInvocationSelectors.js +2 -2
  16. data/webpack/JobInvocationDetail/__tests__/MainInformation.test.js +5 -1
  17. data/webpack/JobInvocationDetail/__tests__/fixtures.js +9 -0
  18. data/webpack/JobInvocationDetail/index.js +56 -34
  19. data/webpack/__mocks__/foremanReact/components/HostDetails/DetailsCard/DefaultLoaderEmptyState.js +1 -2
  20. data/webpack/react_app/components/RecentJobsCard/JobStatusIcon.js +38 -7
  21. data/webpack/react_app/components/RecentJobsCard/constants.js +4 -0
  22. data/webpack/react_app/components/TargetingHosts/__tests__/__snapshots__/HostStatus.test.js.snap +1 -1
  23. data/webpack/react_app/components/TargetingHosts/components/HostStatus.js +6 -6
  24. metadata +6 -94
  25. data/.babelrc.js +0 -3
  26. data/.eslintignore +0 -3
  27. data/.eslintrc +0 -13
  28. data/.github/workflows/js_ci.yml +0 -32
  29. data/.github/workflows/release.yml +0 -16
  30. data/.github/workflows/ruby_ci.yml +0 -19
  31. data/.gitignore +0 -19
  32. data/.packit.yaml +0 -45
  33. data/.prettierrc +0 -4
  34. data/.rubocop.yml +0 -105
  35. data/.rubocop_todo.yml +0 -516
  36. data/.tx/config +0 -10
  37. data/Gemfile +0 -5
  38. data/app/mailers/.gitkeep +0 -0
  39. data/app/views/dashboard/.gitkeep +0 -0
  40. data/foreman_remote_execution.gemspec +0 -33
  41. data/jsconfig.json +0 -8
  42. data/test/benchmark/run_hosts_job_benchmark.rb +0 -70
  43. data/test/benchmark/targeting_benchmark.rb +0 -31
  44. data/test/factories/foreman_remote_execution_factories.rb +0 -147
  45. data/test/functional/api/v2/foreign_input_sets_controller_test.rb +0 -58
  46. data/test/functional/api/v2/job_invocations_controller_test.rb +0 -446
  47. data/test/functional/api/v2/job_templates_controller_test.rb +0 -110
  48. data/test/functional/api/v2/registration_controller_test.rb +0 -73
  49. data/test/functional/api/v2/remote_execution_features_controller_test.rb +0 -34
  50. data/test/functional/api/v2/template_invocations_controller_test.rb +0 -33
  51. data/test/functional/cockpit_controller_test.rb +0 -16
  52. data/test/functional/job_invocations_controller_test.rb +0 -132
  53. data/test/functional/job_templates_controller_test.rb +0 -31
  54. data/test/functional/ui_job_wizard_controller_test.rb +0 -16
  55. data/test/graphql/mutations/job_invocations/create_test.rb +0 -58
  56. data/test/graphql/queries/job_invocation_query_test.rb +0 -31
  57. data/test/graphql/queries/job_invocations_query_test.rb +0 -35
  58. data/test/helpers/remote_execution_helper_test.rb +0 -46
  59. data/test/support/remote_execution_helper.rb +0 -5
  60. data/test/test_plugin_helper.rb +0 -9
  61. data/test/unit/actions/run_host_job_test.rb +0 -115
  62. data/test/unit/actions/run_hosts_job_test.rb +0 -214
  63. data/test/unit/api_params_test.rb +0 -25
  64. data/test/unit/concerns/foreman_tasks_cleaner_extensions_test.rb +0 -29
  65. data/test/unit/concerns/host_extensions_test.rb +0 -219
  66. data/test/unit/concerns/nic_extensions_test.rb +0 -9
  67. data/test/unit/execution_task_status_mapper_test.rb +0 -92
  68. data/test/unit/input_template_renderer_test.rb +0 -503
  69. data/test/unit/job_invocation_composer_test.rb +0 -974
  70. data/test/unit/job_invocation_report_template_test.rb +0 -60
  71. data/test/unit/job_invocation_test.rb +0 -232
  72. data/test/unit/job_template_effective_user_test.rb +0 -37
  73. data/test/unit/job_template_test.rb +0 -316
  74. data/test/unit/remote_execution_feature_test.rb +0 -86
  75. data/test/unit/remote_execution_provider_test.rb +0 -298
  76. data/test/unit/renderer_scope_input_test.rb +0 -49
  77. data/test/unit/targeting_test.rb +0 -206
  78. data/test/unit/template_invocation_input_value_test.rb +0 -38
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: a73b13cdd10e99e44456d4b8dc1bbf8fe654dd6b83eb929e657df49d44b75078
4
- data.tar.gz: a09d30bd37238e0dadd1e8bb8b32d41292df56aea1a03f19fdd2ab6386664ab3
3
+ metadata.gz: e0b4dd4f17f6d245a627cf09e014206e0392de844821d35cef065a9af835b399
4
+ data.tar.gz: 4b60be78f70b3791c02f57c08060cc771aeb33f5cfd985335f26f25f1db030c7
5
5
  SHA512:
6
- metadata.gz: 6633435cacf5d2e835d8391bf2d035844d87db9be34c9fbd134d140a10559779b0f940109ba6feb8dfb0f35ab5b08accc5bec75c94e28ec12accc837c10c795c
7
- data.tar.gz: 0e0417551e20786be7c7c87d1e694596ef8f118dbf303fece462810a6d4577f7bd99208dc6f8555cfb91b87a73914fc96a20185d9d7e2a3f6e76103cca5db31a
6
+ metadata.gz: 9f87b034acd048bf1a5fe968a418a3b03cd76880342e9aeec52303b19e8bfb097013f877c9a43a34ffc23cb8b5cc9614c30dba16188c8dbfa101fc0c94d18d52
7
+ data.tar.gz: a90d5d70bc9bf9a0480202699c62d48f7e78ad1f518d6421fbf478c866a29fe265a3a7a7a9ac9e4a9a15177212877ea3dbb2101696384faef7fc7eb9a0c27650
@@ -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
@@ -1,3 +1,3 @@
1
1
  module ForemanRemoteExecution
2
- VERSION = '14.0.2'.freeze
2
+ VERSION = '14.1.0'.freeze
3
3
  end
@@ -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',