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 #36823 - Create job invocations detail page with main info #839

Merged
merged 1 commit into from
Nov 29, 2023
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
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
adamruzicka marked this conversation as resolved.
Show resolved Hide resolved
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;
Loading