Skip to content

Commit

Permalink
Fixes #37630 - Display basic list of hosts on the new job_invocations…
Browse files Browse the repository at this point in the history
… page
  • Loading branch information
kmalyjur committed Jul 9, 2024
1 parent 356605f commit 257ee21
Show file tree
Hide file tree
Showing 13 changed files with 254 additions and 14 deletions.
29 changes: 27 additions & 2 deletions app/controllers/api/v2/job_invocations_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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])

Expand Down Expand Up @@ -111,6 +111,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
Expand Down Expand Up @@ -187,7 +212,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
Expand Down
15 changes: 15 additions & 0 deletions app/views/api/v2/job_invocations/hosts.json.rabl
Original file line number Diff line number Diff line change
@@ -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
6 changes: 6 additions & 0 deletions app/views/api/v2/template_invocations/base.json.rabl
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,9 @@ object @template_invocation
attributes :id, :template_id, :job_invocation_id, :effective_user, :host_id, :run_host_job_task_id

node(:host_name) { |ti| ti.host.name }
node(:smart_proxy) do |ti|
{
id: ti.smart_proxy&.id,
name: ti.smart_proxy&.name,
}
end
1 change: 1 addition & 0 deletions config/routes.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down
6 changes: 4 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -25,11 +25,13 @@
"@theforeman/test": ">= 12.0.1",
"@theforeman/vendor-dev": ">= 12.0.1",
"babel-eslint": "^10.0.0",
"concurrently": "^8.2.2",
"eslint": "^6.8.0",
"graphql": "^15.5.0",
"graphql-tag": "^2.11.0",
"http-server": "^14.1.1",
"prettier": "^1.19.1",
"redux-mock-store": "^1.2.2",
"graphql-tag": "^2.11.0",
"graphql": "^15.5.0",
"victory-core": "~36.8.6"
},
"peerDependencies": {
Expand Down
2 changes: 1 addition & 1 deletion webpack/JobInvocationDetail/JobInvocationActions.js
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
53 changes: 53 additions & 0 deletions webpack/JobInvocationDetail/JobInvocationConstants.js
Original file line number Diff line number Diff line change
@@ -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'
);
Expand All @@ -29,3 +34,51 @@ export const DATE_OPTIONS = {
hour12: false,
timeZoneName: 'short',
};

const getColumnsStatus = name => {
switch (name) {
case 'success':
return { title: __('Succeeded'), status: 0 };
case 'error':
return { title: __('Failed'), status: 1 };
default:
return { title: __('Unknown'), status: 2 };
}
};

export const columns = {
name: {
title: __('Name'),
wrapper: ({ name }) => <a href={`/new/hosts/${name}`}>{name}</a>,
weight: 1,
},
groups: {
title: __('Host group'),
wrapper: ({ hostgroup_id, hostgroup_name }) => (
<a href={`/hostgroups/${hostgroup_id}/edit`}>{hostgroup_name}</a>
),
weight: 2,
},
os: {
title: __('OS'),
wrapper: ({ operatingsystem_id, operatingsystem_name }) => (
<a href={`/operatingsystems/${operatingsystem_id}/edit`}>
{operatingsystem_name}
</a>
),
weight: 3,
},
smart_proxy: {
title: __('Smart proxy'),
wrapper: ({ smart_proxy_name, smart_proxy_id }) => (
<a href={`/smart_proxies/${smart_proxy_id}`}>{smart_proxy_name}</a>
),
weight: 4,
},
status: {
title: __('Status'),
tableTitle: job_status => getColumnsStatus(job_status).title,
status: job_status => getColumnsStatus(job_status).status,
weight: 5,
},
};
4 changes: 4 additions & 0 deletions webpack/JobInvocationDetail/JobInvocationDetail.scss
Original file line number Diff line number Diff line change
Expand Up @@ -38,4 +38,8 @@
height: $chart_size;
}
}

.job-invocation-host-table-page-section {
padding-top: 0px;
}

132 changes: 132 additions & 0 deletions webpack/JobInvocationDetail/JobInvocationHostTable.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
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 = ({ data }) => {
const { id } = data;
const columnNamesKeys = Object.keys(columns);
const apiOptions = { key: JOB_INVOCATION_HOSTS, search: urlSearchQuery };
const {
searchParam: 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({
results: response?.results || [],
metadata: {
total: response?.total || 0,
page: response?.page,
selectable: response?.subtotal || 0,
},
initialSearchQuery: urlSearchQuery,
});

const controller = 'hosts';
const memoDefaultSearchProps = useMemo(
() => getControllerSearchProps(controller),
[controller]
);
memoDefaultSearchProps.autocomplete.url = foremanUrl(
`/${controller}/auto_complete_search`
);

return (
<TableIndexPage
apiUrl=""
apiOptions={apiOptions}
customSearchProps={memoDefaultSearchProps}
controller="hosts"
creatable={false}
replacementResponse={combinedResponse}
updateSearchQuery={updateSearchQuery}
>
<Table
ouiaId="job-invocation-hosts-table"
columns={columns}
params={params}
setParams={setParamsAndAPI}
itemCount={response?.subtotal}
results={response?.results}
url=""
refreshData={() => {}}
errorMessage={
status === STATUS.ERROR && response?.message ? response.message : null
}
isPending={status === STATUS.PENDING || response?.results === undefined}
isDeleteable={false}
>
{response?.results?.map((result, rowIndex) => (
<Tr key={rowIndex} ouiaId={`table-row-${rowIndex}`}>
{columnNamesKeys.map(k => (
<Td key={k}>
{k === 'status' ? (
<JobStatusIcon status={columns[k].status(result.job_status)}>
{columns[k].tableTitle(result.job_status)}
</JobStatusIcon>
) : (
columns[k].wrapper(result)
)}
</Td>
))}
</Tr>
))}
</Table>
</TableIndexPage>
);
};

JobInvocationHostTable.propTypes = {
data: PropTypes.object.isRequired,
};

JobInvocationHostTable.defaultProps = {};

export default JobInvocationHostTable;
4 changes: 2 additions & 2 deletions webpack/JobInvocationDetail/JobInvocationSelectors.js
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import { selectAPIResponse } from 'foremanReact/redux/API/APISelectors';
import { JOB_INVOCATION_KEY } from './JobInvocationConstants';
import { JOB_INVOCATION_KEY, GET_TASK } from './JobInvocationConstants';

export const selectItems = state =>
selectAPIResponse(state, JOB_INVOCATION_KEY);

export const selectTask = state => selectAPIResponse(state, 'GET_TASK');
export const selectTask = state => selectAPIResponse(state, GET_TASK);

export const selectTaskCancelable = state =>
selectTask(state).available_actions?.cancellable || false;
6 changes: 4 additions & 2 deletions webpack/JobInvocationDetail/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { translate as __, documentLocale } from 'foremanReact/common/I18n';
import PageLayout from 'foremanReact/routes/common/PageLayout/PageLayout';
import { useAPI } from 'foremanReact/common/hooks/API/APIHooks';
import { stopInterval } from 'foremanReact/redux/middlewares/IntervalMiddleware';
import { getData, getTask } from './JobInvocationActions';
import { getJobInvocation, getTask } from './JobInvocationActions';
import {
CURRENT_PERMISSIONS,
DATE_OPTIONS,
Expand All @@ -19,6 +19,7 @@ import JobInvocationOverview from './JobInvocationOverview';
import { selectItems } from './JobInvocationSelectors';
import JobInvocationSystemStatusChart from './JobInvocationSystemStatusChart';
import JobInvocationToolbarButtons from './JobInvocationToolbarButtons';
import JobInvocationHostTable from './JobInvocationHostTable';

const JobInvocationDetailPage = ({
match: {
Expand Down Expand Up @@ -57,7 +58,7 @@ const JobInvocationDetailPage = ({
}

useEffect(() => {
dispatch(getData(`/api/job_invocations/${id}`));
dispatch(getJobInvocation(`/api/job_invocations/${id}?host_status=true`));
if (finished && !autoRefresh) {
dispatch(stopInterval(JOB_INVOCATION_KEY));
}
Expand Down Expand Up @@ -120,6 +121,7 @@ const JobInvocationDetailPage = ({
/>
</Flex>
</Flex>
{items.id !== undefined && <JobInvocationHostTable data={items} />}
</PageLayout>
);
};
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,4 @@
import React from 'react';
import { translate as __ } from '../../../common/I18n';

const DefaultLoaderEmptyState = () => (
<span className="disabled-text">{__('Not available')}</span>
);

const DefaultLoaderEmptyState = () => <div />;
export default DefaultLoaderEmptyState;
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
import React from 'react';

const Table = () => <div />;
export default Table;

0 comments on commit 257ee21

Please sign in to comment.