Skip to content

Commit

Permalink
Fixes #37286 - Add global actions to job invocation detail page
Browse files Browse the repository at this point in the history
  • Loading branch information
kmalyjur authored and adamruzicka committed May 30, 2024
1 parent 0ef2424 commit a98c856
Show file tree
Hide file tree
Showing 23 changed files with 995 additions and 65 deletions.
1 change: 1 addition & 0 deletions app/controllers/api/v2/job_invocations_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,7 @@ def cancel
api :POST, '/job_invocations/:id/rerun', N_('Rerun job on failed hosts')
param :id, :identifier, :required => true
param :failed_only, :bool
param :succeeded_only, :bool
def rerun
composer = JobInvocationComposer.from_job_invocation(@job_invocation, params)
if composer.rerun_possible?
Expand Down
23 changes: 23 additions & 0 deletions app/models/job_invocation.rb
Original file line number Diff line number Diff line change
Expand Up @@ -194,6 +194,10 @@ def failed_host_ids
failed_hosts.pluck(:id)
end

def succeeded_host_ids
succeeded_hosts.pluck(:id)
end

def failed_hosts
base = targeting.hosts
if finished?
Expand All @@ -203,6 +207,15 @@ def failed_hosts
end
end

def succeeded_hosts
base = targeting.hosts
if finished?
base.where.not(:id => not_succeeded_template_invocations.select(:host_id))
else
base.where(:id => succeeded_template_invocations.select(:host_id))
end
end

def total_hosts_count
count = _('N/A')

Expand Down Expand Up @@ -290,4 +303,14 @@ def not_failed_template_invocations
results = [:cancelled, :failed].map { |state| TemplateInvocation::TaskResultMap.status_to_task_result(state) }.flatten
template_invocations.joins(:run_host_job_task).where.not(ForemanTasks::Task.table_name => { :result => results })
end

def succeeded_template_invocations
result = TemplateInvocation::TaskResultMap.status_to_task_result(:success)
template_invocations.joins(:run_host_job_task).where(ForemanTasks::Task.table_name => { :result => result })
end

def not_succeeded_template_invocations
result = TemplateInvocation::TaskResultMap.status_to_task_result(:success)
template_invocations.joins(:run_host_job_task).where.not(ForemanTasks::Task.table_name => { :result => result })
end
end
2 changes: 2 additions & 0 deletions app/models/job_invocation_composer.rb
Original file line number Diff line number Diff line change
Expand Up @@ -232,6 +232,8 @@ def initialize(job_invocation, params = {})
@host_ids = params[:host_ids]
elsif params[:failed_only]
@host_ids = job_invocation.failed_host_ids
elsif params[:succeeded_only]
@host_ids = job_invocation.succeeded_host_ids
end
end

Expand Down
5 changes: 3 additions & 2 deletions app/views/api/v2/job_invocations/base.json.rabl
Original file line number Diff line number Diff line change
Expand Up @@ -25,9 +25,10 @@ end
if params.key?(:include_permissions)
node :permissions do |invocation|
authorizer = Authorizer.new(User.current)
edit_job_templates_permission = Permission.where(name: "edit_job_templates", resource_type: "JobTemplate").first
{
"edit_job_templates" => (edit_job_templates_permission && authorizer.can?("edit_job_templates", invocation, false)),
"edit_job_templates" => authorizer.can?("edit_job_templates", invocation, false),
"view_foreman_tasks" => authorizer.can?("view_foreman_tasks", invocation.task, false),
"edit_recurring_logics" => authorizer.can?("edit_recurring_logics", invocation.recurring_logic, false),
}
end
end
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,8 @@
"prettier": "^1.19.1",
"redux-mock-store": "^1.2.2",
"graphql-tag": "^2.11.0",
"graphql": "^15.5.0"
"graphql": "^15.5.0",
"victory-core": "~36.8.6"
},
"peerDependencies": {
"@theforeman/vendor": ">= 12.0.1"
Expand Down
137 changes: 134 additions & 3 deletions webpack/JobInvocationDetail/JobInvocationActions.js
Original file line number Diff line number Diff line change
@@ -1,9 +1,19 @@
import { get } from 'foremanReact/redux/API';
import { translate as __, sprintf } from 'foremanReact/common/I18n';
import { foremanUrl } from 'foremanReact/common/helpers';
import { addToast } from 'foremanReact/components/ToastsList';
import { APIActions, get } from 'foremanReact/redux/API';
import {
withInterval,
stopInterval,
withInterval,
} from 'foremanReact/redux/middlewares/IntervalMiddleware';
import { JOB_INVOCATION_KEY } from './JobInvocationConstants';
import {
CANCEL_JOB,
CANCEL_RECURRING_LOGIC,
CHANGE_ENABLED_RECURRING_LOGIC,
GET_TASK,
JOB_INVOCATION_KEY,
UPDATE_JOB,
} from './JobInvocationConstants';

export const getData = url => dispatch => {
const fetchData = withInterval(
Expand All @@ -20,3 +30,124 @@ export const getData = url => dispatch => {

dispatch(fetchData);
};

export const updateJob = jobId => dispatch => {
const url = foremanUrl(`/api/job_invocations/${jobId}`);
dispatch(
APIActions.get({
url,
key: UPDATE_JOB,
})
);
};

export const cancelJob = (jobId, force) => dispatch => {
const infoToast = () =>
force
? sprintf(__('Trying to abort the job %s.'), jobId)
: sprintf(__('Trying to cancel the job %s.'), jobId);
const errorToast = response =>
force
? sprintf(__(`Could not abort the job %s: ${response}`), jobId)
: sprintf(__(`Could not cancel the job %s: ${response}`), jobId);
const url = force
? `/job_invocations/${jobId}/cancel?force=true`
: `/job_invocations/${jobId}/cancel`;

dispatch(
APIActions.post({
url,
key: CANCEL_JOB,
errorToast: ({ response }) =>
errorToast(
// eslint-disable-next-line camelcase
response?.data?.error?.full_messages ||
response?.data?.error?.message ||
'Unknown error.'
),
handleSuccess: () => {
dispatch(
addToast({
key: `cancel-job-error`,
type: 'info',
message: infoToast(),
})
);
dispatch(updateJob(jobId));
},
})
);
};

export const getTask = taskId => dispatch => {
dispatch(
get({
key: GET_TASK,
url: `/foreman_tasks/api/tasks/${taskId}`,
})
);
};

export const enableRecurringLogic = (
recurrenceId,
enabled,
jobId
) => dispatch => {
const successToast = () =>
enabled
? sprintf(__('Recurring logic %s disabled successfully.'), recurrenceId)
: sprintf(__('Recurring logic %s enabled successfully.'), recurrenceId);
const errorToast = response =>
enabled
? sprintf(
__(`Could not disable recurring logic %s: ${response}`),
recurrenceId
)
: sprintf(
__(`Could not enable recurring logic %s: ${response}`),
recurrenceId
);
const url = `/foreman_tasks/api/recurring_logics/${recurrenceId}`;
dispatch(
APIActions.put({
url,
key: CHANGE_ENABLED_RECURRING_LOGIC,
params: { recurring_logic: { enabled: !enabled } },
successToast,
errorToast: ({ response }) =>
errorToast(
// eslint-disable-next-line camelcase
response?.data?.error?.full_messages ||
response?.data?.error?.message ||
'Unknown error.'
),
handleSuccess: () => dispatch(updateJob(jobId)),
})
);
};

export const cancelRecurringLogic = (recurrenceId, jobId) => dispatch => {
const successToast = () =>
sprintf(__('Recurring logic %s cancelled successfully.'), recurrenceId);
const errorToast = response =>
sprintf(
__(`Could not cancel recurring logic %s: ${response}`),
recurrenceId
);
const url = `/foreman_tasks/recurring_logics/${recurrenceId}/cancel`;
dispatch(
APIActions.post({
url,
key: CANCEL_RECURRING_LOGIC,
successToast,
errorToast: ({ response }) =>
errorToast(
// eslint-disable-next-line camelcase
response?.data?.error?.full_messages ||
response?.data?.error?.message ||
'Unknown error.'
),
handleSuccess: () => dispatch(updateJob(jobId)),
})
);
};
14 changes: 14 additions & 0 deletions webpack/JobInvocationDetail/JobInvocationConstants.js
Original file line number Diff line number Diff line change
@@ -1,9 +1,23 @@
import { foremanUrl } from 'foremanReact/common/helpers';

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 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 currentPermissionsUrl = foremanUrl(
'/api/v2/permissions/current_permissions'
);

export const STATUS = {
PENDING: 'pending',
SUCCEEDED: 'succeeded',
FAILED: 'failed',
CANCELLED: 'cancelled',
};

export const DATE_OPTIONS = {
Expand Down
7 changes: 5 additions & 2 deletions webpack/JobInvocationDetail/JobInvocationDetail.scss
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
.job-invocation-detail-page-section {
.job-invocation-detail-flex {
$chart_size: 105px;

padding-top: 0px;
padding-left: 10px;

.chart-donut {
height: $chart_size;
width: $chart_size;
Expand Down Expand Up @@ -36,3 +38,4 @@
height: $chart_size;
}
}

5 changes: 5 additions & 0 deletions webpack/JobInvocationDetail/JobInvocationSelectors.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,8 @@ import { JOB_INVOCATION_KEY } from './JobInvocationConstants';

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

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

export const selectTaskCancelable = state =>
selectTask(state).available_actions?.cancellable || false;
22 changes: 13 additions & 9 deletions webpack/JobInvocationDetail/JobInvocationSystemStatusChart.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import React, { useEffect, useState } from 'react';
import PropTypes from 'prop-types';
import React, { useEffect, useState } from 'react';
import { translate as __, sprintf } from 'foremanReact/common/I18n';
import DefaultLoaderEmptyState from 'foremanReact/components/HostDetails/DetailsCard/DefaultLoaderEmptyState';
import {
ChartDonut,
ChartLabel,
Expand All @@ -9,20 +10,19 @@ import {
} from '@patternfly/react-charts';
import {
DescriptionList,
DescriptionListTerm,
DescriptionListGroup,
DescriptionListDescription,
DescriptionListGroup,
DescriptionListTerm,
FlexItem,
Text,
} from '@patternfly/react-core';
import {
global_palette_green_500 as successedColor,
global_palette_red_100 as failedColor,
global_palette_blue_300 as inProgressColor,
global_palette_black_600 as canceledColor,
global_palette_black_500 as emptyChartDonut,
global_palette_red_100 as failedColor,
global_palette_blue_300 as inProgressColor,
global_palette_green_500 as successedColor,
} from '@patternfly/react-tokens';
import DefaultLoaderEmptyState from 'foremanReact/components/HostDetails/DetailsCard/DefaultLoaderEmptyState';
import './JobInvocationDetail.scss';

const JobInvocationSystemStatusChart = ({
Expand All @@ -35,9 +35,9 @@ const JobInvocationSystemStatusChart = ({
failed,
pending,
cancelled,
total,
total_hosts: totalHosts, // includes scheduled
} = data;
const total = succeeded + failed + pending + cancelled;
const chartData = [
{ title: __('Succeeded:'), count: succeeded, color: successedColor.value },
{ title: __('Failed:'), count: failed, color: failedColor.value },
Expand Down Expand Up @@ -82,7 +82,11 @@ const JobInvocationSystemStatusChart = ({
total > 0 ? chartData.map(d => d.color) : [emptyChartDonut.value]
}
labelComponent={
<ChartTooltip pointerLength={0} constrainToVisibleArea />
<ChartTooltip
pointerLength={0}
constrainToVisibleArea
renderInPortal={false}
/>
}
title={chartDonutTitle}
titleComponent={
Expand Down
Loading

0 comments on commit a98c856

Please sign in to comment.