Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fixes #38123 - add template invocation info to new job details #938

Merged
merged 1 commit into from
Jan 28, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
57 changes: 57 additions & 0 deletions app/controllers/template_invocations_controller.rb
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
class TemplateInvocationsController < ApplicationController
include Foreman::Controller::AutoCompleteSearch
include RemoteExecutionHelper
include JobInvocationsHelper

before_action :find_job_invocation, :only => %w{show_template_invocation_by_host}
before_action :find_host, :only => %w{show_template_invocation_by_host}

def controller_permission
'job_invocations'
Expand All @@ -17,4 +22,56 @@ def show
@line_sets = @line_sets.drop_while { |o| o['timestamp'].to_f <= @since } if @since
@line_counter = params[:line_counter].to_i
end

def show_template_invocation_by_host
adamruzicka marked this conversation as resolved.
Show resolved Hide resolved
@template_invocation = @job_invocation.template_invocations.find { |template_inv| template_inv.host_id == @host.id }
if @template_invocation.nil?
render :json => { :error => _('Template invocation not found') }, :status => :not_found
end
@template_invocation_task = @template_invocation.run_host_job_task

lines = normalize_line_sets(@template_invocation_task.main_action.live_output)
transformed_input_values = @template_invocation.input_values.joins(:template_input).map do |input_value|
{
name: input_value&.template_input&.name,
value: input_safe_value(input_value),
}
end

auto_refresh = @job_invocation.task.try(:pending?)
finished = @job_invocation.status_label == 'failed' || @job_invocation.status_label == 'succeeded' || @job_invocation.status_label == 'cancelled'
render :json => { :output => lines, :preview => template_invocation_preview(@template_invocation, @host), :input_values => transformed_input_values, :job_invocation_description => @job_invocation.description, :task_id => @template_invocation_task.id, :task_cancellable => @template_invocation_task.cancellable?, :host_name => @host.name, :permissions => {
:view_foreman_tasks => User.current.allowed_to?(:view_foreman_tasks),
:cancel_job_invocations => User.current.allowed_to?(:cancel_job_invocations),
:execute_jobs => User.current.allowed_to?(:create_job_invocations) && ([email protected]_host? || User.current.can?(:execute_jobs_on_infrastructure_hosts)),

},
:auto_refresh => auto_refresh, :finished => finished}, status: :ok
end

private

def find_job_invocation
@job_invocation = JobInvocation.find(params[:id])
rescue ActiveRecord::RecordNotFound
render :json => { :error => { :message => format(_("Job with id '%{id}' was not found"), :id => params['id']) } }, :status => :not_found
end

def find_host
@host = Host.find(params[:host_id])
rescue ActiveRecord::RecordNotFound
render :json => { :error => { :message => format(_("Host with id '%{id}' was not found"), :id => params['host_id']) } }, :status => :not_found
adamruzicka marked this conversation as resolved.
Show resolved Hide resolved
end

def template_invocation_preview(template_invocation, host)
renderer = InputTemplateRenderer.new(template_invocation.template, host, template_invocation)
output = load_template_from_task(template_invocation, host) || renderer.preview
if output
{:plain => output}
else
{status: :bad_request,
plain: renderer.error_message }
end
end

end
2 changes: 1 addition & 1 deletion app/views/api/v2/job_invocations/hosts.json.rabl
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
collection @hosts

attribute :name, :operatingsystem_id, :operatingsystem_name, :hostgroup_id, :hostgroup_name
attribute :name, :operatingsystem_id, :operatingsystem_name, :hostgroup_id, :hostgroup_name, :id

node :job_status do |host|
@host_statuses[host.id]
Expand Down
2 changes: 2 additions & 0 deletions config/routes.rb
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@
match 'old/job_invocations/new', to: 'job_invocations#new', via: [:get], as: 'form_new_job_invocation'
match 'old/job_invocations/:id/rerun', to: 'job_invocations#rerun', via: [:get, :post], as: 'form_rerun_job_invocation'
match 'experimental/job_invocations_detail/:id', to: 'react#index', :via => [:get], as: 'new_job_invocation_detail'
match 'job_invocations_detail/:id/host_invocation/:host_id', to: 'react#index', :via => [:get], as: 'new_job_invocation_detail_by_host'
get 'show_template_invocation_by_host/:host_id/job_invocation/:id', to: 'template_invocations#show_template_invocation_by_host'

resources :job_invocations, :only => [:create, :show, :index] do
collection do
Expand Down
6 changes: 3 additions & 3 deletions lib/foreman_remote_execution/engine.rb
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ class Engine < ::Rails::Engine
initializer 'foreman_remote_execution.register_plugin', before: :finisher_hook do |app|
app.reloader.to_prepare do
Foreman::Plugin.register :foreman_remote_execution do
requires_foreman '>= 3.13'
requires_foreman '>= 3.14'
register_global_js_file 'global'
register_gettext

Expand Down Expand Up @@ -170,9 +170,9 @@ class Engine < ::Rails::Engine
permission :lock_job_templates, { :job_templates => [:lock, :unlock] }, :resource_type => 'JobTemplate'
permission :create_job_invocations, { :job_invocations => [:new, :create, :legacy_create, :refresh, :rerun, :preview_hosts],
'api/v2/job_invocations' => [:create, :rerun] }, :resource_type => 'JobInvocation'
permission :view_job_invocations, { :job_invocations => [:index, :chart, :show, :auto_complete_search, :preview_job_invocations_per_host], :template_invocations => [:show],
permission :view_job_invocations, { :job_invocations => [:index, :chart, :show, :auto_complete_search, :preview_job_invocations_per_host], :template_invocations => [:show, :show_template_invocation_by_host],
'api/v2/job_invocations' => [:index, :show, :output, :raw_output, :outputs, :hosts] }, :resource_type => 'JobInvocation'
permission :view_template_invocations, { :template_invocations => [:show],
permission :view_template_invocations, { :template_invocations => [:show, :template_invocation_preview, :show_template_invocation_by_host],
'api/v2/template_invocations' => [:template_invocations], :ui_job_wizard => [:job_invocation] }, :resource_type => 'TemplateInvocation'
permission :create_template_invocations, {}, :resource_type => 'TemplateInvocation'
permission :execute_jobs_on_infrastructure_hosts, {}, :resource_type => 'JobInvocation'
Expand Down
19 changes: 19 additions & 0 deletions webpack/JobInvocationDetail/JobInvocationConstants.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,15 @@ export const JOB_INVOCATION_HOSTS = 'JOB_INVOCATION_HOSTS';
export const currentPermissionsUrl = foremanUrl(
'/api/v2/permissions/current_permissions'
);
export const GET_TEMPLATE_INVOCATION = 'GET_TEMPLATE_INVOCATION';
export const showTemplateInvocationUrl = (hostID, jobID) =>
`/show_template_invocation_by_host/${hostID}/job_invocation/${jobID}`;

export const templateInvocationPageUrl = (hostID, jobID) =>
`/job_invocations_detail/${jobID}/host_invocation/${hostID}`;

export const jobInvocationDetailsUrl = id =>
`/experimental/job_invocations_detail/${id}`;

export const STATUS = {
PENDING: 'pending',
Expand Down Expand Up @@ -65,6 +74,11 @@ const Columns = () => {
const hostDetailsPageUrl = useForemanHostDetailsPageUrl();

return {
expand: {
title: '',
weight: 0,
wrapper: () => null,
},
name: {
title: __('Name'),
wrapper: ({ name }) => (
Expand Down Expand Up @@ -109,6 +123,11 @@ const Columns = () => {
},
weight: 5,
},
actions: {
title: '',
weight: 6,
wrapper: () => null,
},
};
};

Expand Down
61 changes: 61 additions & 0 deletions webpack/JobInvocationDetail/JobInvocationDetail.scss
Original file line number Diff line number Diff line change
Expand Up @@ -45,3 +45,64 @@
.job-invocation-details section:nth-child(3) {
padding-bottom: 0;
}

.template-invocation {
&.output-in-table-view {
div.invocation-output {
overflow: auto;
max-height: 25em;
}
}
div.invocation-output {
display: block;
padding: 9.5px;
margin: 0 0 10px;
font-size: 12px;
word-break: break-all;
word-wrap: break-word;
color: rgba(255, 255, 255, 1);
background-color: rgba(47, 47, 47, 1);
border: 1px solid #000000;
border-radius: 0px;
font-family: Menlo, Monaco, Consolas, monospace;

div.printable {
min-height: 50px;
}

div.line.stderr,
div.line.error,
div.line.debug {
color: red;
}

div.line span.counter {
float: left;
clear: left;
}

div.line div.content {
position: relative;
margin-left: 50px;
white-space: pre-wrap;
}

a {
color: #ffffff;
}

a.scroll-link{
position: relative;
bottom: 10px;
float: right;
}
}

.template-invocation-preview {
margin-top: 10px;
}

.pf-c-toggle-group {
margin-bottom: 10px;
}
}
148 changes: 106 additions & 42 deletions webpack/JobInvocationDetail/JobInvocationHostTable.js
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
/* eslint-disable camelcase */
import PropTypes from 'prop-types';
import React, { useMemo, useEffect } from 'react';
import React, { useMemo, useEffect, useState } from 'react';
import { Icon } from 'patternfly-react';
import { translate as __ } from 'foremanReact/common/I18n';
import { FormattedMessage } from 'react-intl';
import { Tr, Td } from '@patternfly/react-table';
import { Tr, Td, Tbody, ExpandableRowContent } from '@patternfly/react-table';
import {
Title,
EmptyState,
Expand All @@ -26,6 +26,9 @@ import Columns, {
JOB_INVOCATION_HOSTS,
STATUS_UPPERCASE,
} from './JobInvocationConstants';
import { TemplateInvocation } from './TemplateInvocation';
import { OpenAlInvocations, PopupAlert } from './OpenAlInvocations';
import { RowActions } from './TemplateInvocationComponents/TemplateActionButtons';

const JobInvocationHostTable = ({ id, targeting, finished, autoRefresh }) => {
const columns = Columns();
Expand All @@ -39,11 +42,18 @@ const JobInvocationHostTable = ({ id, targeting, finished, autoRefresh }) => {
const defaultParams = { search: urlSearchQuery };
if (urlPage) defaultParams.page = Number(urlPage);
if (urlPerPage) defaultParams.per_page = Number(urlPerPage);
const [expandedHost, setExpandedHost] = useState([]);
const { response, status, setAPIOptions } = useAPI(
'get',
`/api/job_invocations/${id}/hosts`,
{
params: { ...defaultParams, key: JOB_INVOCATION_HOSTS },
params: {
...defaultParams,
},
key: JOB_INVOCATION_HOSTS,
handleSuccess: ({ data }) => {
if (data?.results?.length === 1) setExpandedHost([data.results[0].id]);
},
}
);

Expand Down Expand Up @@ -153,48 +163,102 @@ const JobInvocationHostTable = ({ id, targeting, finished, autoRefresh }) => {
</Tr>
);

const { results = [] } = response;

const isHostExpanded = host => expandedHost.includes(host);
const setHostExpanded = (host, isExpanding = true) =>
setExpandedHost(prevExpanded => {
const otherExpandedHosts = prevExpanded.filter(h => h !== host);
return isExpanding ? [...otherExpandedHosts, host] : otherExpandedHosts;
});
const [showAlert, setShowAlert] = useState(false);
return (
<TableIndexPage
apiUrl=""
apiOptions={apiOptions}
customSearchProps={memoDefaultSearchProps}
controller="hosts"
creatable={false}
replacementResponse={combinedResponse}
updateSearchQuery={updateSearchQuery}
>
<Table
ouiaId="job-invocation-hosts-table"
columns={columns}
customEmptyState={
status === STATUS_UPPERCASE.RESOLVED && !response?.results?.length
? customEmptyState
: null
}
params={params}
setParams={setParamsAndAPI}
itemCount={response?.subtotal}
results={response?.results}
url=""
refreshData={() => {}}
errorMessage={
status === STATUS_UPPERCASE.ERROR && response?.message
? response.message
: null
<>
{showAlert && <PopupAlert setShowAlert={setShowAlert} />}
<TableIndexPage
apiUrl=""
apiOptions={apiOptions}
customSearchProps={memoDefaultSearchProps}
controller="hosts"
creatable={false}
replacementResponse={combinedResponse}
updateSearchQuery={updateSearchQuery}
customToolbarItems={
<OpenAlInvocations
setShowAlert={setShowAlert}
results={results}
id={id}
/>
}
isPending={status === STATUS_UPPERCASE.PENDING}
isDeleteable={false}
bottomPagination={bottomPagination}
>
{response?.results?.map((result, rowIndex) => (
<Tr key={rowIndex} ouiaId={`table-row-${rowIndex}`}>
{columnNamesKeys.map(k => (
<Td key={k}>{columns[k].wrapper(result)}</Td>
))}
</Tr>
))}
</Table>
</TableIndexPage>
<Table
ouiaId="job-invocation-hosts-table"
columns={columns}
customEmptyState={
status === STATUS_UPPERCASE.RESOLVED && !results?.length
? customEmptyState
: null
}
params={params}
setParams={setParamsAndAPI}
itemCount={response?.subtotal}
results={results}
url=""
refreshData={() => {}}
errorMessage={
status === STATUS_UPPERCASE.ERROR && response?.message
? response.message
: null
}
isPending={status === STATUS_UPPERCASE.PENDING}
isDeleteable={false}
bottomPagination={bottomPagination}
childrenOutsideTbody
>
{results?.map((result, rowIndex) => (
<Tbody key={rowIndex}>
<Tr ouiaId={`table-row-${rowIndex}`}>
<Td
expand={{
rowIndex,
isExpanded: isHostExpanded(result.id),
onToggle: () =>
setHostExpanded(result.id, !isHostExpanded(result.id)),
expandId: 'host-expandable',
}}
/>
{columnNamesKeys.slice(1).map(k => (
<Td key={k}>{columns[k].wrapper(result)}</Td>
))}
<Td isActionCell>
<RowActions hostID={result.id} jobID={id} />
</Td>
</Tr>
<Tr
isExpanded={isHostExpanded(result.id)}
ouiaId="table-row-expanded-sections"
>
<Td
dataLabel={`${result.id}-expandable-content`}
colSpan={columnNamesKeys.length + 1}
>
<ExpandableRowContent>
adamruzicka marked this conversation as resolved.
Show resolved Hide resolved
{result.job_status === 'cancelled' ||
result.job_status === 'N/A' ? (
<div>
{__('A task for this host has not been started')}
</div>
) : (
<TemplateInvocation hostID={result.id} jobID={id} />
)}
</ExpandableRowContent>
</Td>
</Tr>
</Tbody>
))}
</Table>
</TableIndexPage>
</>
);
};

Expand Down
Loading
Loading