Skip to content

Commit

Permalink
Fixes #36823 - Create job invocations detail page with main info
Browse files Browse the repository at this point in the history
  • Loading branch information
kmalyjur authored and adamruzicka committed Nov 29, 2023
1 parent e827589 commit ea8a0d6
Show file tree
Hide file tree
Showing 10 changed files with 248 additions and 0 deletions.
5 changes: 5 additions & 0 deletions app/helpers/remote_execution_helper.rb
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,11 @@ def job_invocation_task_buttons(task)
:disabled => !task.cancellable?,
:method => :post)
end
if Setting[:lab_features]
buttons << link_to(_('New UI'), new_job_invocation_detail_path(:id => job_invocation.id),
class: 'btn btn-default',
title: _('Switch to the new job invocation detail UI'))
end
return buttons
end

Expand Down
14 changes: 14 additions & 0 deletions app/views/api/v2/job_invocations/base.json.rabl
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,11 @@ object @job_invocation
attributes :id, :description, :job_category, :targeting_id, :status, :start_at, :status_label, :ssh_user, :time_to_pickup

node do |invocation|
pattern_template = invocation.pattern_template_invocations.first
{
:template_id => pattern_template&.template_id,
:template_name => pattern_template&.template_name,
:effective_user => pattern_template&.effective_user,
:succeeded => invocation_count(invocation, :output_key => :success_count),
:failed => invocation_count(invocation, :output_key => :failed_count),
:pending => invocation_count(invocation, :output_key => :pending_count),
Expand All @@ -15,3 +19,13 @@ end
child :task => :dynflow_task do
attributes :id, :state
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)),
}
end
end
2 changes: 2 additions & 0 deletions config/routes.rb
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@
match 'job_invocations/:id/rerun', to: 'react#index', :via => [:get], as: 'rerun_job_invocation'
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'

resources :job_invocations, :only => [:create, :show, :index] do
collection do
get 'preview_job_invocations_per_host'
Expand Down
6 changes: 6 additions & 0 deletions lib/foreman_remote_execution/engine.rb
Original file line number Diff line number Diff line change
Expand Up @@ -244,6 +244,12 @@ class Engine < ::Rails::Engine
parent: :monitor_menu,
after: :audits

menu :labs_menu, :job_invocations_detail,
url_hash: { controller: :job_invocations, action: :show },
caption: N_('Job invocations detail'),
parent: :lab_features_menu,
url: '/experimental/job_invocations_detail/1'

register_custom_status HostStatus::ExecutionStatus
# add dashboard widget
# widget 'foreman_remote_execution_widget', name: N_('Foreman plugin template widget'), sizex: 4, sizey: 1
Expand Down
22 changes: 22 additions & 0 deletions webpack/JobInvocationDetail/JobInvocationActions.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { get } from 'foremanReact/redux/API';
import {
withInterval,
stopInterval,
} from 'foremanReact/redux/middlewares/IntervalMiddleware';
import { JOB_INVOCATION_KEY } from './JobInvocationConstants';

export const getData = url => dispatch => {
const fetchData = withInterval(
get({
key: JOB_INVOCATION_KEY,
params: { include_permissions: true },
url,
handleError: () => {
dispatch(stopInterval(JOB_INVOCATION_KEY));
},
}),
1000
);

dispatch(fetchData);
};
7 changes: 7 additions & 0 deletions webpack/JobInvocationDetail/JobInvocationConstants.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
export const JOB_INVOCATION_KEY = 'JOB_INVOCATION_KEY';

export const STATUS = {
PENDING: 'pending',
SUCCEEDED: 'succeeded',
FAILED: 'failed',
};
104 changes: 104 additions & 0 deletions webpack/JobInvocationDetail/JobInvocationOverview.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
import React from 'react';
import PropTypes from 'prop-types';
import {
Button,
DescriptionList,
DescriptionListTerm,
DescriptionListGroup,
DescriptionListDescription,
} from '@patternfly/react-core';
import DefaultLoaderEmptyState from 'foremanReact/components/HostDetails/DetailsCard/DefaultLoaderEmptyState';
import { translate as __, documentLocale } from 'foremanReact/common/I18n';

const JobInvocationOverview = ({ data }) => {
const {
start_at: startAt,
ssh_user: sshUser,
template_id: templateId,
template_name: templateName,
effective_user: effectiveUser,
permissions,
} = data;
const canEditJobTemplates = permissions
? permissions.edit_job_templates
: false;
const dateOptions = {
day: 'numeric',
month: 'short',
year: 'numeric',
hour: '2-digit',
minute: '2-digit',
hour12: false,
timeZoneName: 'short',
};
let formattedStartDate = __('Not yet');

if (startAt) {
// Ensures date string compatibility across browsers
const convertedDate = new Date(startAt.replace(/[-.]/g, '/'));
if (convertedDate.getTime() <= new Date().getTime()) {
formattedStartDate = convertedDate.toLocaleString(
documentLocale(),
dateOptions
);
}
}

return (
<DescriptionList
columnModifier={{
default: '2Col',
}}
isHorizontal
isCompact
isFluid
isAutoColumnWidths
>
<DescriptionListGroup>
<DescriptionListTerm>{__('Effective user:')}</DescriptionListTerm>
<DescriptionListDescription>
{effectiveUser || <DefaultLoaderEmptyState />}
</DescriptionListDescription>
</DescriptionListGroup>
<DescriptionListGroup>
<DescriptionListTerm>{__('Started at:')}</DescriptionListTerm>
<DescriptionListDescription>
{formattedStartDate}
</DescriptionListDescription>
</DescriptionListGroup>
<DescriptionListGroup>
<DescriptionListTerm>{__('SSH user:')}</DescriptionListTerm>
<DescriptionListDescription>
{sshUser || <DefaultLoaderEmptyState />}
</DescriptionListDescription>
</DescriptionListGroup>
<DescriptionListGroup>
<DescriptionListTerm>{__('Template:')}</DescriptionListTerm>
<DescriptionListDescription>
{templateName ? (
<Button
ouiaId="template-link"
variant="link"
component="a"
isInline
isDisabled={!canEditJobTemplates}
href={
templateId ? `/job_templates/${templateId}/edit` : undefined
}
>
{templateName}
</Button>
) : (
<DefaultLoaderEmptyState />
)}
</DescriptionListDescription>
</DescriptionListGroup>
</DescriptionList>
);
};

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

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

export const selectItems = state =>
selectAPIResponse(state, JOB_INVOCATION_KEY);
77 changes: 77 additions & 0 deletions webpack/JobInvocationDetail/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
import React, { useEffect } from 'react';
import { useSelector, useDispatch } from 'react-redux';
import PropTypes from 'prop-types';
import { Divider, PageSection, Flex, FlexItem } from '@patternfly/react-core';
import { translate as __ } from 'foremanReact/common/I18n';
import PageLayout from 'foremanReact/routes/common/PageLayout/PageLayout';
import { stopInterval } from 'foremanReact/redux/middlewares/IntervalMiddleware';
import { getData } from './JobInvocationActions';
import { selectItems } from './JobInvocationSelectors';
import JobInvocationOverview from './JobInvocationOverview';
import { JOB_INVOCATION_KEY, STATUS } from './JobInvocationConstants';

const JobInvocationDetailPage = ({
match: {
params: { id },
},
}) => {
const dispatch = useDispatch();
const items = useSelector(selectItems);
const { description, status_label: statusLabel, task } = items;
const finished =
statusLabel === STATUS.FAILED || statusLabel === STATUS.SUCCEEDED;
const autoRefresh = task?.state === STATUS.PENDING || false;

useEffect(() => {
dispatch(getData(`/api/job_invocations/${id}`));
if (finished && !autoRefresh) {
dispatch(stopInterval(JOB_INVOCATION_KEY));
}
return () => {
dispatch(stopInterval(JOB_INVOCATION_KEY));
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [dispatch, id, finished, autoRefresh]);

const breadcrumbOptions = {
breadcrumbItems: [
{ caption: __('Jobs'), url: `/job_invocations` },
{ caption: description },
],
isPf4: true,
};

return (
<PageLayout
header={description}
breadcrumbOptions={breadcrumbOptions}
searchable={false}
>
<React.Fragment>
<PageSection isFilled variant="light">
<Flex>
<FlexItem> </FlexItem>
<Divider
orientation={{
default: 'vertical',
}}
/>
<FlexItem>
<JobInvocationOverview data={items} />
</FlexItem>
</Flex>
</PageSection>
</React.Fragment>
</PageLayout>
);
};

JobInvocationDetailPage.propTypes = {
match: PropTypes.shape({
params: PropTypes.shape({
id: PropTypes.string.isRequired,
}),
}).isRequired,
};

export default JobInvocationDetailPage;
6 changes: 6 additions & 0 deletions webpack/Routes/routes.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import React from 'react';
import JobWizardPage from '../JobWizard';
import JobWizardPageRerun from '../JobWizard/JobWizardPageRerun';
import JobInvocationDetailPage from '../JobInvocationDetail';

const ForemanREXRoutes = [
{
Expand All @@ -13,6 +14,11 @@ const ForemanREXRoutes = [
exact: true,
render: props => <JobWizardPageRerun {...props} />,
},
{
path: '/experimental/job_invocations_detail/:id',
exact: true,
render: props => <JobInvocationDetailPage {...props} />,
},
];

export default ForemanREXRoutes;

0 comments on commit ea8a0d6

Please sign in to comment.