diff --git a/app/controllers/api/v2/job_invocations_controller.rb b/app/controllers/api/v2/job_invocations_controller.rb
index a27b9464b..8fba2adcc 100644
--- a/app/controllers/api/v2/job_invocations_controller.rb
+++ b/app/controllers/api/v2/job_invocations_controller.rb
@@ -6,7 +6,7 @@ class JobInvocationsController < ::Api::V2::BaseController
before_action :find_optional_nested_object, :only => %w{output raw_output}
before_action :find_host, :only => %w{output raw_output}
- before_action :find_resource, :only => %w{show update destroy clone cancel rerun outputs}
+ before_action :find_resource, :only => %w{show update destroy clone cancel rerun outputs hosts}
wrap_parameters JobInvocation, :include => (JobInvocation.attribute_names + [:ssh])
@@ -28,6 +28,8 @@ def show
if params[:host_status] == 'true'
template_invocations = @template_invocations.includes(:run_host_job_task).to_a
@host_statuses = Hash[template_invocations.map { |ti| [ti.host_id, template_invocation_status(ti)] }]
+ @smart_proxy_id = Hash[template_invocations.map { |ti| [ti.host_id, ti.smart_proxy_id] }]
+ @smart_proxy_name = Hash[template_invocations.map { |ti| [ti.host_id, ti.smart_proxy_name] }]
end
end
@@ -111,6 +113,31 @@ def output
render :json => host_output(@nested_obj, @host, :default => [], :since => params[:since])
end
+ api :GET, '/job_invocations/:id/hosts', N_('List hosts belonging to job invocation')
+ param :include, ['parameters', 'all_parameters'], :desc => N_("Array of extra information types to include")
+ param_group :search_and_pagination, ::Api::V2::BaseController
+ add_scoped_search_description_for(JobInvocation)
+ param :id, :identifier, :required => true
+ def hosts
+ Rails.logger.info "Params: #{params.inspect}"
+ @hosts = @job_invocation.targeting.hosts.authorized(:view_hosts, Host)
+ @total = @job_invocation.targeting.hosts.size
+ @template_invocations = @job_invocation.template_invocations
+ .where(host: @hosts)
+ .includes(:input_values)
+ template_invocations = @template_invocations.includes(:run_host_job_task).to_a
+ @host_statuses = Hash[template_invocations.map { |ti| [ti.host_id, template_invocation_status(ti)] }]
+ @smart_proxy_id = Hash[template_invocations.map { |ti| [ti.host_id, ti.smart_proxy_id] }]
+ @smart_proxy_name = Hash[template_invocations.map { |ti| [ti.host_id, ti.smart_proxy_name] }]
+
+ if params[:include].present?
+ @parameters = params[:include].include?('parameters')
+ @all_parameters = params[:include].include?('all_parameters')
+ end
+ @hosts = @hosts.search_for(params[:search], :order => params[:order]).paginate(:page => params[:page], :per_page => params[:per_page])
+ render :hosts, :layout => 'api/v2/layouts/index_layout'
+ end
+
api :GET, '/job_invocations/:id/hosts/:host_id/raw', N_('Get raw output for a host')
param :id, :identifier, :required => true
param :host_id, :identifier, :required => true
@@ -187,7 +214,7 @@ def allowed_nested_id
def action_permission
case params[:action]
- when 'output', 'raw_output', 'outputs'
+ when 'output', 'raw_output', 'outputs', 'hosts'
:view
when 'cancel'
:cancel
diff --git a/app/views/api/v2/job_invocations/hosts.json.rabl b/app/views/api/v2/job_invocations/hosts.json.rabl
new file mode 100644
index 000000000..0467f9ce5
--- /dev/null
+++ b/app/views/api/v2/job_invocations/hosts.json.rabl
@@ -0,0 +1,15 @@
+collection @hosts
+
+attribute :name, :operatingsystem_id, :operatingsystem_name, :hostgroup_id, :hostgroup_name
+
+node :job_status do |host|
+ @host_statuses[host.id]
+end
+
+node :smart_proxy_id do |host|
+ @smart_proxy_id[host.id]
+end
+
+node :smart_proxy_name do |host|
+ @smart_proxy_name[host.id]
+end
diff --git a/config/routes.rb b/config/routes.rb
index 51d97f21b..3cf6759ac 100644
--- a/config/routes.rb
+++ b/config/routes.rb
@@ -63,6 +63,7 @@
get '/raw', :to => 'job_invocations#raw_output'
end
member do
+ get 'hosts'
post 'cancel'
post 'rerun'
get 'template_invocations', :to => 'template_invocations#template_invocations'
diff --git a/webpack/JobInvocationDetail/JobInvocationActions.js b/webpack/JobInvocationDetail/JobInvocationActions.js
index a70b80e6b..f80425065 100644
--- a/webpack/JobInvocationDetail/JobInvocationActions.js
+++ b/webpack/JobInvocationDetail/JobInvocationActions.js
@@ -15,7 +15,7 @@ import {
UPDATE_JOB,
} from './JobInvocationConstants';
-export const getData = url => dispatch => {
+export const getJobInvocation = url => dispatch => {
const fetchData = withInterval(
get({
key: JOB_INVOCATION_KEY,
diff --git a/webpack/JobInvocationDetail/JobInvocationConstants.js b/webpack/JobInvocationDetail/JobInvocationConstants.js
index 2673d3aaf..9aa75ede4 100644
--- a/webpack/JobInvocationDetail/JobInvocationConstants.js
+++ b/webpack/JobInvocationDetail/JobInvocationConstants.js
@@ -1,14 +1,19 @@
+/* eslint-disable camelcase */
+import React from 'react';
import { foremanUrl } from 'foremanReact/common/helpers';
+import { translate as __ } from 'foremanReact/common/I18n';
export const JOB_INVOCATION_KEY = 'JOB_INVOCATION_KEY';
export const CURRENT_PERMISSIONS = 'CURRENT_PERMISSIONS';
export const UPDATE_JOB = 'UPDATE_JOB';
export const CANCEL_JOB = 'CANCEL_JOB';
export const GET_TASK = 'GET_TASK';
+export const GET_TEMPLATE_INVOCATIONS = 'GET_TEMPLATE_INVOCATIONS';
export const CHANGE_ENABLED_RECURRING_LOGIC = 'CHANGE_ENABLED_RECURRING_LOGIC';
export const CANCEL_RECURRING_LOGIC = 'CANCEL_RECURRING_LOGIC';
export const GET_REPORT_TEMPLATES = 'GET_REPORT_TEMPLATES';
export const GET_REPORT_TEMPLATE_INPUTS = 'GET_REPORT_TEMPLATE_INPUTS';
+export const JOB_INVOCATION_HOSTS = 'JOB_INVOCATION_HOSTS';
export const currentPermissionsUrl = foremanUrl(
'/api/v2/permissions/current_permissions'
);
@@ -29,3 +34,63 @@ export const DATE_OPTIONS = {
hour12: false,
timeZoneName: 'short',
};
+
+const getColumnsStatus = ({ hostJobStatus, jobStatus }) => {
+ const defaultTitle = jobStatus === 'queued' ? __('Scheduled') : __('Unknown');
+
+ switch (hostJobStatus) {
+ case 'success':
+ return { title: __('Succeeded'), status: 0 };
+ case 'error':
+ return { title: __('Failed'), status: 1 };
+ case 'planned':
+ return { title: __('Scheduled'), status: 2 };
+ case 'running':
+ return { title: __('Pending'), status: 3 };
+ case 'cancelled':
+ return { title: __('Cancelled'), status: 4 };
+ default:
+ return { title: defaultTitle, status: 5 };
+ }
+};
+
+export const columns = {
+ name: {
+ title: __('Name'),
+ wrapper: ({ name }) => {name},
+ weight: 1,
+ },
+ groups: {
+ title: __('Host group'),
+ wrapper: ({ hostgroup_id, hostgroup_name }) => (
+ {hostgroup_name}
+ ),
+ weight: 2,
+ },
+ os: {
+ title: __('OS'),
+ wrapper: ({ operatingsystem_id, operatingsystem_name }) => (
+
+ {operatingsystem_name}
+
+ ),
+ weight: 3,
+ },
+ smart_proxy: {
+ title: __('Smart proxy'),
+ wrapper: ({ smart_proxy_name, smart_proxy_id }) => (
+ {smart_proxy_name}
+ ),
+ weight: 4,
+ },
+ status: {
+ title: __('Status'),
+ tableTitle: ({ job_status, status_label }) =>
+ getColumnsStatus({ hostJobStatus: job_status, jobStatus: status_label })
+ .title,
+ status: ({ job_status, status_label }) =>
+ getColumnsStatus({ hostJobStatus: job_status, jobStatus: status_label })
+ .status,
+ weight: 5,
+ },
+};
diff --git a/webpack/JobInvocationDetail/JobInvocationDetail.scss b/webpack/JobInvocationDetail/JobInvocationDetail.scss
index 9482277a0..03ca16582 100644
--- a/webpack/JobInvocationDetail/JobInvocationDetail.scss
+++ b/webpack/JobInvocationDetail/JobInvocationDetail.scss
@@ -38,4 +38,8 @@
height: $chart_size;
}
}
+
+.job-invocation-host-table-page-section {
+ padding-top: 0px;
+}
\ No newline at end of file
diff --git a/webpack/JobInvocationDetail/JobInvocationHostTable.js b/webpack/JobInvocationDetail/JobInvocationHostTable.js
new file mode 100644
index 000000000..6a8d921af
--- /dev/null
+++ b/webpack/JobInvocationDetail/JobInvocationHostTable.js
@@ -0,0 +1,134 @@
+/* eslint-disable camelcase */
+import PropTypes from 'prop-types';
+import React, { useMemo } from 'react';
+import { Tr, Td } from '@patternfly/react-table';
+import { foremanUrl } from 'foremanReact/common/helpers';
+import { useAPI } from 'foremanReact/common/hooks/API/APIHooks';
+import { Table } from 'foremanReact/components/PF4/TableIndexPage/Table/Table';
+import TableIndexPage from 'foremanReact/components/PF4/TableIndexPage/TableIndexPage';
+import { useSetParamsAndApiAndSearch } from 'foremanReact/components/PF4/TableIndexPage/Table/TableIndexHooks';
+import {
+ useBulkSelect,
+ useUrlParams,
+} from 'foremanReact/components/PF4/TableIndexPage/Table/TableHooks';
+import { getControllerSearchProps } from 'foremanReact/constants';
+import JobStatusIcon from '../react_app/components/RecentJobsCard/JobStatusIcon';
+import {
+ columns,
+ JOB_INVOCATION_HOSTS,
+ STATUS,
+} from './JobInvocationConstants';
+
+const JobInvocationHostTable = ({ id }) => {
+ const columnNamesKeys = Object.keys(columns);
+ const apiOptions = { key: JOB_INVOCATION_HOSTS, search: urlSearchQuery };
+ const {
+ search: urlSearchQuery = '',
+ page: urlPage,
+ per_page: urlPerPage,
+ } = useUrlParams();
+ const defaultParams = { search: urlSearchQuery };
+ if (urlPage) defaultParams.page = Number(urlPage);
+ if (urlPerPage) defaultParams.per_page = Number(urlPerPage);
+ const { response, status, setAPIOptions } = useAPI(
+ 'get',
+ `/api/job_invocations/${id}/hosts`,
+ {
+ params: { ...defaultParams, key: JOB_INVOCATION_HOSTS },
+ }
+ );
+
+ const { setParamsAndAPI, params } = useSetParamsAndApiAndSearch({
+ defaultParams,
+ apiOptions,
+ setAPIOptions,
+ });
+
+ const combinedResponse = {
+ response: {
+ search: '',
+ can_create: false,
+ results: response?.results || [],
+ total: response?.total || 0,
+ per_page: response?.perPage,
+ page: response?.page,
+ subtotal: response?.subtotal || 0,
+ message: response?.message || 'error',
+ },
+ status,
+ setAPIOptions,
+ };
+
+ const { updateSearchQuery } = useBulkSelect({
+ initialSearchQuery: urlSearchQuery,
+ });
+
+ const controller = 'hosts';
+ const memoDefaultSearchProps = useMemo(
+ () => getControllerSearchProps(controller),
+ [controller]
+ );
+ memoDefaultSearchProps.autocomplete.url = foremanUrl(
+ `/${controller}/auto_complete_search`
+ );
+
+ return (
+ {}}
+ errorMessage={
+ status === STATUS.ERROR && response?.message ? response.message : null
+ }
+ isPending={status === STATUS.PENDING || response?.results === undefined}
+ isDeleteable={false}
+ >
+ {response?.results?.map((result, rowIndex) => (
+
+
+ {columnNamesKeys.map(k => (
+
+ ))}
+
+ {k === 'status' ? (
+
+ ))}
+