From 9da75eeb68cf134c93026ff2d6eba67391ba32c6 Mon Sep 17 00:00:00 2001 From: kmalyjur Date: Mon, 5 Feb 2024 10:42:21 +0000 Subject: [PATCH] Fixes #37122 - Update system status chart in job invocations detail page --- .../api/v2/job_invocations/base.json.rabl | 2 + .../JobInvocationConstants.js | 10 ++ .../JobInvocationDetail.scss | 39 ++++++ .../JobInvocationOverview.js | 34 ++--- .../JobInvocationSystemStatusChart.js | 120 ++++++++++++++++++ webpack/JobInvocationDetail/index.js | 43 ++++++- 6 files changed, 217 insertions(+), 31 deletions(-) create mode 100644 webpack/JobInvocationDetail/JobInvocationDetail.scss create mode 100644 webpack/JobInvocationDetail/JobInvocationSystemStatusChart.js diff --git a/app/views/api/v2/job_invocations/base.json.rabl b/app/views/api/v2/job_invocations/base.json.rabl index 7e253b7ae..ec6133cde 100644 --- a/app/views/api/v2/job_invocations/base.json.rabl +++ b/app/views/api/v2/job_invocations/base.json.rabl @@ -11,8 +11,10 @@ node do |invocation| :succeeded => invocation_count(invocation, :output_key => :success_count), :failed => invocation_count(invocation, :output_key => :failed_count), :pending => invocation_count(invocation, :output_key => :pending_count), + :cancelled => invocation_count(invocation, :output_key => :cancelled_count), :total => invocation_count(invocation, :output_key => :total_count), :missing => invocation.missing_hosts_count, + :total_hosts => invocation.total_hosts_count, } end diff --git a/webpack/JobInvocationDetail/JobInvocationConstants.js b/webpack/JobInvocationDetail/JobInvocationConstants.js index 239460d5b..7bec1ac8d 100644 --- a/webpack/JobInvocationDetail/JobInvocationConstants.js +++ b/webpack/JobInvocationDetail/JobInvocationConstants.js @@ -5,3 +5,13 @@ export const STATUS = { SUCCEEDED: 'succeeded', FAILED: 'failed', }; + +export const DATE_OPTIONS = { + day: 'numeric', + month: 'short', + year: 'numeric', + hour: '2-digit', + minute: '2-digit', + hour12: false, + timeZoneName: 'short', +}; diff --git a/webpack/JobInvocationDetail/JobInvocationDetail.scss b/webpack/JobInvocationDetail/JobInvocationDetail.scss new file mode 100644 index 000000000..1b7d3a94e --- /dev/null +++ b/webpack/JobInvocationDetail/JobInvocationDetail.scss @@ -0,0 +1,39 @@ +.chart-donut { + height: 105px; + width: 105px; + margin-bottom: 15px; + + .chart-title tspan { + font-size: 20px !important; + } + + .chart-subtitle tspan { + font-size: 12px !important; + fill: #6A6E73 !important; + } +} + +.chart-legend { + height: 105px; + width: 270px; + + .legend-title { + font-weight: bold; + font-size: 14px; + margin-left: 8px; + margin-bottom: 0; + } + + .pf-c-description-list { + margin-left: 8px; + margin-top: 8px; + + .pf-c-description-list__term .pf-c-description-list__text { + font-weight: normal; + } + } +} + +.pf-c-divider { + max-height: 105px !important; +} diff --git a/webpack/JobInvocationDetail/JobInvocationOverview.js b/webpack/JobInvocationDetail/JobInvocationOverview.js index 25f62f886..18dddb564 100644 --- a/webpack/JobInvocationDetail/JobInvocationOverview.js +++ b/webpack/JobInvocationDetail/JobInvocationOverview.js @@ -7,12 +7,15 @@ import { DescriptionListGroup, DescriptionListDescription, } from '@patternfly/react-core'; +import { translate as __ } from 'foremanReact/common/I18n'; import DefaultLoaderEmptyState from 'foremanReact/components/HostDetails/DetailsCard/DefaultLoaderEmptyState'; -import { translate as __, documentLocale } from 'foremanReact/common/I18n'; -const JobInvocationOverview = ({ data }) => { +const JobInvocationOverview = ({ + data, + isAlreadyStarted, + formattedStartDate, +}) => { const { - start_at: startAt, ssh_user: sshUser, template_id: templateId, template_name: templateName, @@ -22,27 +25,6 @@ const JobInvocationOverview = ({ 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 ( { {__('Started at:')} - {formattedStartDate} + {isAlreadyStarted ? formattedStartDate : __('Not yet')} @@ -99,6 +81,8 @@ const JobInvocationOverview = ({ data }) => { JobInvocationOverview.propTypes = { data: PropTypes.object.isRequired, + isAlreadyStarted: PropTypes.bool.isRequired, + formattedStartDate: PropTypes.string.isRequired, }; export default JobInvocationOverview; diff --git a/webpack/JobInvocationDetail/JobInvocationSystemStatusChart.js b/webpack/JobInvocationDetail/JobInvocationSystemStatusChart.js new file mode 100644 index 000000000..d5991396e --- /dev/null +++ b/webpack/JobInvocationDetail/JobInvocationSystemStatusChart.js @@ -0,0 +1,120 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { + ChartDonut, + ChartLabel, + ChartLegend, + ChartTooltip, +} from '@patternfly/react-charts'; +import { + DescriptionList, + DescriptionListTerm, + DescriptionListGroup, + DescriptionListDescription, + FlexItem, + Text, +} from '@patternfly/react-core'; +import { translate as __ } from 'foremanReact/common/I18n'; +import './JobInvocationDetail.scss'; + +const JobInvocationSystemStatusChart = ({ + data, + isAlreadyStarted, + formattedStartDate, +}) => { + const { + succeeded, + failed, + pending, + cancelled, + total, + total_hosts: totalHosts, // includes scheduled + } = data; + const chartData = [ + { title: __('Succeeded:'), count: succeeded, color: '#3E8635' }, + { title: __('Failed:'), count: failed, color: '#C9190B' }, + { title: __('In Progress:'), count: pending, color: '#2B9AF3' }, + { title: __('Canceled:'), count: cancelled, color: '#6A6E73' }, + ]; + const chartDonutTitle = () => { + if (total > 0) return `${succeeded.toString()}/${total}`; + if (totalHosts > 0) return `0/${totalHosts}`; + return '0'; + }; + + return ( + <> + + 0 + ? chartData.map(d => ({ + label: `${d.title} ${d.count} hosts`, + y: d.count, + })) + : [{ label: `Scheduled: ${totalHosts} hosts`, y: 1 }] + } + colorScale={total > 0 ? chartData.map(d => d.color) : ['#8A8D90']} + labelComponent={ + + } + title={chartDonutTitle} + titleComponent={} + subTitle={__('Systems')} + subTitleComponent={} + padding={{ + bottom: 0, + left: 0, + right: 0, + top: 0, + }} + width={105} + height={105} + /> + + + {__('System status')} + {isAlreadyStarted ? ( + ({ + name: `${d.title} ${d.count}`, + symbol: { type: 'circle' }, + }))} + colorScale={chartData.map(d => d.color)} + width={270} + height={105} + /> + ) : ( + + + {__('Scheduled at:')} + + {formattedStartDate} + + + + )} + + + ); +}; + +JobInvocationSystemStatusChart.propTypes = { + data: PropTypes.object.isRequired, + isAlreadyStarted: PropTypes.bool.isRequired, + formattedStartDate: PropTypes.string.isRequired, +}; + +export default JobInvocationSystemStatusChart; diff --git a/webpack/JobInvocationDetail/index.js b/webpack/JobInvocationDetail/index.js index 032cfb1c9..7caed113c 100644 --- a/webpack/JobInvocationDetail/index.js +++ b/webpack/JobInvocationDetail/index.js @@ -2,13 +2,19 @@ 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 { translate as __, documentLocale } 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'; +import JobInvocationSystemStatusChart from './JobInvocationSystemStatusChart'; +import { + JOB_INVOCATION_KEY, + STATUS, + DATE_OPTIONS, +} from './JobInvocationConstants'; +import './JobInvocationDetail.scss'; const JobInvocationDetailPage = ({ match: { @@ -17,11 +23,28 @@ const JobInvocationDetailPage = ({ }) => { const dispatch = useDispatch(); const items = useSelector(selectItems); - const { description, status_label: statusLabel, task } = items; + const { + description, + status_label: statusLabel, + task, + start_at: startAt, + } = items; const finished = statusLabel === STATUS.FAILED || statusLabel === STATUS.SUCCEEDED; const autoRefresh = task?.state === STATUS.PENDING || false; + let isAlreadyStarted = false; + let formattedStartDate; + if (startAt) { + // Ensures date string compatibility across browsers + const convertedDate = new Date(startAt.replace(/[-.]/g, '/')); + isAlreadyStarted = convertedDate.getTime() <= new Date().getTime(); + formattedStartDate = convertedDate.toLocaleString( + documentLocale(), + DATE_OPTIONS + ); + } + useEffect(() => { dispatch(getData(`/api/job_invocations/${id}`)); if (finished && !autoRefresh) { @@ -49,15 +72,23 @@ const JobInvocationDetailPage = ({ > - - + + - +