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 committed Oct 23, 2023
1 parent 5bf78eb commit c07d707
Show file tree
Hide file tree
Showing 10 changed files with 222 additions and 0 deletions.
3 changes: 3 additions & 0 deletions app/controllers/api/v2/job_invocations_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,9 @@ def show
param :ssh_user, String, :required => false, :desc => N_('Set SSH user')
param :password, String, :required => false, :desc => N_('Set SSH password')
param :key_passphrase, String, :required => false, :desc => N_('Set SSH key passphrase')
param :template_id, String, :required => false, :desc => N_('ID of the job template')
param :template_name, String, :required => false, :desc => N_('Name of the job template')
param :effective_user, String, :required => false, :desc => N_('Effective user of the job template')

param :recurrence, Hash, :desc => N_('Create a recurring job') do
param :cron_line, String, :required => false, :desc => N_('How often the job should occur, in the cron format')
Expand Down
3 changes: 3 additions & 0 deletions app/views/api/v2/job_invocations/base.json.rabl
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@ attributes :id, :description, :job_category, :targeting_id, :status, :start_at,

node do |invocation|
{
:template_id => invocation.pattern_template_invocations.first.template_id,
:template_name => invocation.pattern_template_invocations.first.template_name,
:effective_user => invocation.pattern_template_invocations.first.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 Down
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]

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_detail, action: :index },
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
21 changes: 21 additions & 0 deletions webpack/JobInvocationDetail/JobInvocationActions.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
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,
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',
};
93 changes: 93 additions & 0 deletions webpack/JobInvocationDetail/JobInvocationOverview.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
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,
} = data;

const dateOptions = {
day: 'numeric',
month: 'short',
year: 'numeric',
hour: '2-digit',
minute: '2-digit',
hour12: false,
timeZoneName: 'short',
};
const dateConverted = new Date(startAt);
const dateLocaleFormatted = dateConverted
.toLocaleString(documentLocale(), dateOptions)
.replace(/(\d{4}) /, '$1, ');
const dateCurrent = new Date();

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>
{startAt && dateConverted <= dateCurrent
? dateLocaleFormatted
: __('Not yet')}
</DescriptionListDescription>
</DescriptionListGroup>
<DescriptionListGroup>
<DescriptionListTerm>{__('SSH user:')}</DescriptionListTerm>
<DescriptionListDescription>
{sshUser || <DefaultLoaderEmptyState />}
</DescriptionListDescription>
</DescriptionListGroup>
<DescriptionListGroup>
<DescriptionListTerm>{__('Template:')}</DescriptionListTerm>
<DescriptionListDescription>
{templateName ? (
<Button
ouiaId="subnet-link"
variant="link"
component="a"
isInline
href={`/job_templates/${templateId}/edit`}
>
{templateName}
</Button>
) : (
<DefaultLoaderEmptyState />
)}
</DescriptionListDescription>
</DescriptionListGroup>
</DescriptionList>
);
};

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

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

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 c07d707

Please sign in to comment.