From fee84c3f7a7ea70ce9d47a933a100725b545d8bf Mon Sep 17 00:00:00 2001 From: kmalyjur Date: Wed, 25 Oct 2023 14:18:23 +0000 Subject: [PATCH] Fixes #36823 - Create job invocations detail page with main info --- .../api/v2/job_invocations/base.json.rabl | 4 + config/routes.rb | 2 + lib/foreman_remote_execution/engine.rb | 6 ++ .../JobInvocationActions.js | 21 ++++ .../JobInvocationConstants.js | 7 ++ .../JobInvocationOverview.js | 95 +++++++++++++++++++ .../JobInvocationSelectors.js | 4 + webpack/JobInvocationDetail/index.js | 77 +++++++++++++++ webpack/Routes/routes.js | 6 ++ 9 files changed, 222 insertions(+) create mode 100644 webpack/JobInvocationDetail/JobInvocationActions.js create mode 100644 webpack/JobInvocationDetail/JobInvocationConstants.js create mode 100644 webpack/JobInvocationDetail/JobInvocationOverview.js create mode 100644 webpack/JobInvocationDetail/JobInvocationSelectors.js create mode 100644 webpack/JobInvocationDetail/index.js diff --git a/app/views/api/v2/job_invocations/base.json.rabl b/app/views/api/v2/job_invocations/base.json.rabl index 6978b7c18..5b554337c 100644 --- a/app/views/api/v2/job_invocations/base.json.rabl +++ b/app/views/api/v2/job_invocations/base.json.rabl @@ -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), diff --git a/config/routes.rb b/config/routes.rb index 493d5b937..49e9bf85a 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -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' diff --git a/lib/foreman_remote_execution/engine.rb b/lib/foreman_remote_execution/engine.rb index b7db148d6..63e0e4f43 100644 --- a/lib/foreman_remote_execution/engine.rb +++ b/lib/foreman_remote_execution/engine.rb @@ -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 diff --git a/webpack/JobInvocationDetail/JobInvocationActions.js b/webpack/JobInvocationDetail/JobInvocationActions.js new file mode 100644 index 000000000..c93087c11 --- /dev/null +++ b/webpack/JobInvocationDetail/JobInvocationActions.js @@ -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); +}; diff --git a/webpack/JobInvocationDetail/JobInvocationConstants.js b/webpack/JobInvocationDetail/JobInvocationConstants.js new file mode 100644 index 000000000..239460d5b --- /dev/null +++ b/webpack/JobInvocationDetail/JobInvocationConstants.js @@ -0,0 +1,7 @@ +export const JOB_INVOCATION_KEY = 'JOB_INVOCATION_KEY'; + +export const STATUS = { + PENDING: 'pending', + SUCCEEDED: 'succeeded', + FAILED: 'failed', +}; diff --git a/webpack/JobInvocationDetail/JobInvocationOverview.js b/webpack/JobInvocationDetail/JobInvocationOverview.js new file mode 100644 index 000000000..b39b6b944 --- /dev/null +++ b/webpack/JobInvocationDetail/JobInvocationOverview.js @@ -0,0 +1,95 @@ +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 ( + + + {__('Effective user:')} + + {effectiveUser || } + + + + {__('Started at:')} + + {startAt && dateConverted <= dateCurrent + ? dateLocaleFormatted + : __('Not yet')} + + + + {__('SSH user:')} + + {sshUser || } + + + + {__('Template:')} + + {templateName ? ( + + ) : ( + + )} + + + + ); +}; + +JobInvocationOverview.propTypes = { + data: PropTypes.object.isRequired, +}; + +export default JobInvocationOverview; diff --git a/webpack/JobInvocationDetail/JobInvocationSelectors.js b/webpack/JobInvocationDetail/JobInvocationSelectors.js new file mode 100644 index 000000000..01b09681c --- /dev/null +++ b/webpack/JobInvocationDetail/JobInvocationSelectors.js @@ -0,0 +1,4 @@ +import { selectAPIResponse } from 'foremanReact/redux/API/APISelectors'; + +export const selectItems = state => + selectAPIResponse(state, 'JOB_INVOCATION_KEY'); diff --git a/webpack/JobInvocationDetail/index.js b/webpack/JobInvocationDetail/index.js new file mode 100644 index 000000000..032cfb1c9 --- /dev/null +++ b/webpack/JobInvocationDetail/index.js @@ -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 ( + + + + + + + + + + + + + + ); +}; + +JobInvocationDetailPage.propTypes = { + match: PropTypes.shape({ + params: PropTypes.shape({ + id: PropTypes.string.isRequired, + }), + }).isRequired, +}; + +export default JobInvocationDetailPage; diff --git a/webpack/Routes/routes.js b/webpack/Routes/routes.js index d243877d0..3f064d767 100644 --- a/webpack/Routes/routes.js +++ b/webpack/Routes/routes.js @@ -1,6 +1,7 @@ import React from 'react'; import JobWizardPage from '../JobWizard'; import JobWizardPageRerun from '../JobWizard/JobWizardPageRerun'; +import JobInvocationDetailPage from '../JobInvocationDetail'; const ForemanREXRoutes = [ { @@ -13,6 +14,11 @@ const ForemanREXRoutes = [ exact: true, render: props => , }, + { + path: '/experimental/job_invocations_detail/:id', + exact: true, + render: props => , + }, ]; export default ForemanREXRoutes;