diff --git a/app/helpers/miq_request_helper.rb b/app/helpers/miq_request_helper.rb index 9501712458c..418ebf678fc 100644 --- a/app/helpers/miq_request_helper.rb +++ b/app/helpers/miq_request_helper.rb @@ -5,4 +5,8 @@ module MiqRequestHelper def row_data(label, value) {:cells => {:label => label, :value => value}} end + + def request_task_configuration_script_ids(miq_request) + miq_request.miq_request_tasks.map { |task| task.options&.dig(:configuration_script_id) }.compact + end end diff --git a/app/javascript/components/request-workflow-status/data.js b/app/javascript/components/request-workflow-status/data.js new file mode 100644 index 00000000000..88978ecf793 --- /dev/null +++ b/app/javascript/components/request-workflow-status/data.js @@ -0,0 +1,65 @@ +/** Types of workflow state status */ +export const workflowStateTypes = { + success: { text: 'success', tagType: 'green' }, + error: { text: 'error', tagType: 'red' }, + failed: { text: 'failed', tagType: 'gray' }, + pending: { text: 'pending', tagType: 'gray' }, +}; + +/** Function to get the header data of workflow states table. */ +const headerData = () => ([ + { + key: 'name', + header: __('Name'), + }, + { + key: 'enteredTime', + header: __('Entered Time'), + }, + { + key: 'finishedTime', + header: __('Finished Time'), + }, + { + key: 'duration', + header: __('Duration'), + }, +]); + +// const convertDate = (date) => `${moment(date).format('MM/DD/YYYY')} ${moment(date).format('h:mm:ss A')}`; +const convertDate = (date) => { + const utcDate = new Date(date); + const year = utcDate.getUTCFullYear(); + const month = (utcDate.getUTCMonth() + 1).toString().padStart(2, '0'); + const day = utcDate.getUTCDate().toString().padStart(2, '0'); + const hours = utcDate.getUTCHours(); + const minutes = utcDate.getUTCMinutes().toString().padStart(2, '0'); + const period = hours < 12 ? 'AM' : 'PM'; + const hours12 = hours % 12 || 12; // Convert 0 to 12 for 12:00 AM + + const formattedDate = `${month}/${day}/${year} ${hours12}:${minutes} ${period}`; + return formattedDate.toString(); +}; + +/** Function to get the row data of workflow states table. */ +const rowData = ({ StateHistory }) => StateHistory.map((item) => ({ + id: item.Guid.toString(), + name: item.Name, + enteredTime: convertDate(item.EnteredTime.toString()), + finishedTime: convertDate(item.FinishedTime.toString()), + duration: item.Duration.toFixed(3).toString(), +})); + +/** Function to return the header, row and status data required for the RequestWorkflowStatus component. */ +export const workflowStatusData = (response) => { + const type = 'ManageIQ::Providers::Workflows::AutomationManager::WorkflowInstance'; + if (response.type !== type) { + return undefined; + } + const rows = response.context ? rowData(response.context) : []; + const headers = headerData(); + const name = response.name || response.description; + return { + headers, rows, status: response.status, name, parentId: response.parent_id, id: response.id, type: response.type, + }; +}; diff --git a/app/javascript/components/request-workflow-status/index.jsx b/app/javascript/components/request-workflow-status/index.jsx new file mode 100644 index 00000000000..3b1ba4768f0 --- /dev/null +++ b/app/javascript/components/request-workflow-status/index.jsx @@ -0,0 +1,18 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import RequestWorkflowStatusItem from './request-workflow-status-item'; + +/** Component to render the Workflow status in /miq_request/show/#{id} page */ +const RequestWorkflowStatus = ({ ids }) => ( +
+ { ids.map((id) => ( + + ))} +
+); + +RequestWorkflowStatus.propTypes = { + ids: PropTypes.arrayOf(PropTypes.number).isRequired, +}; + +export default RequestWorkflowStatus; diff --git a/app/javascript/components/request-workflow-status/request-workflow-status-item.jsx b/app/javascript/components/request-workflow-status/request-workflow-status-item.jsx new file mode 100644 index 00000000000..bda8b785457 --- /dev/null +++ b/app/javascript/components/request-workflow-status/request-workflow-status-item.jsx @@ -0,0 +1,147 @@ +import React, { useEffect, useState, useRef } from 'react'; +import { + Tag, Loading, Link, +} from 'carbon-components-react'; +import PropTypes from 'prop-types'; +import { workflowStatusData, workflowStateTypes } from './data'; +import MiqDataTable from '../miq-data-table'; +import NotificationMessage from '../notification-message'; + +/** Component to render the Workflow status in /miq_request/show/#{id} page */ +const RequestWorkflowStatusItem = ({ recordId }) => { + const RELOAD = 2000; // Time interval to reload the RequestWorkflowStatus component. + const reloadLimit = 5; // This is to handle the Auto refresh issue causing the the server to burn out with multiple requests. + const reloadCount = useRef(0); + + const [data, setData] = useState( + { + isLoading: true, + responseData: undefined, + message: undefined, + list: [], + parentName: undefined, + validType: false, + } + ); + + /** Function to get the Workflow */ + const getWorkflow = async() => { + reloadCount.current += 1; + const url = `/api/configuration_scripts/${recordId}`; + API.get(url, { skipErrors: [404, 400, 500] }) + .then((response) => { + const responseData = workflowStatusData(response); + if (responseData) { + API.get(`/api/configuration_script_payloads/${responseData.parentId}`).then((response2) => { + if (response.context) { + setData({ + ...data, + responseData, + isLoading: false, + parentName: response2.name, + validType: true, + message: responseData && responseData.status === workflowStateTypes.error.text + ? __('An error has occurred with this workflow') : undefined, + }); + } else { + setData({ + ...data, + responseData, + isLoading: false, + parentName: response2.name, + validType: true, + message: sprintf(__('Context is not available for "%s"'), response.name), + }); + } + }); + } else { + setData({ + ...data, + validType: false, + responseData: undefined, + isLoading: false, + }); + } + }); + }; + + /** Logic to reload the component every so often (RELOAD). */ + useEffect(() => { + const omitStatus = [workflowStateTypes.success.text, workflowStateTypes.error.text]; + if (reloadCount.current <= reloadLimit && data.responseData && data.responseData.status && !omitStatus.includes(data.responseData.status)) { + const interval = setInterval(() => { + setData({ ...data, isLoading: true }); + getWorkflow(); + }, RELOAD); + return () => clearInterval(interval); // This represents the unmount function, in which you need to clear your interval to prevent memory leaks. + } + return undefined; + }, [data.responseData]); + + useEffect(() => { + if (recordId) { + getWorkflow(); + } + }, [recordId]); + + /** Function to render the status of workflow. */ + const renderStatusTag = () => { + const status = workflowStateTypes[data.responseData.status]; + return ( + + {status.text.toUpperCase()} + + ); + }; + + /** Function to render the status of workflow status. */ + const renderWorkflowStatus = () => ( +
+
+ {data.responseData && data.responseData.status && renderStatusTag()} +
+
+ {data.parentName.toString()} +
+
+ {data.isLoading && } +
+
+ ); + + /** Function to render the notification. */ + const renderNotitication = () => ( +
+ +
+ ); + + /** Function to render the list. */ + const renderList = ({ headers, rows }) => ( + + ); + + return ( + <> + { + data.validType && ( +
+ {data.responseData && renderWorkflowStatus()} + {data.message && renderNotitication()} + {data.responseData && data.responseData.status && renderList(data.responseData)} +
+ ) + } + + ); +}; + +RequestWorkflowStatusItem.propTypes = { + recordId: PropTypes.number.isRequired, +}; + +export default RequestWorkflowStatusItem; diff --git a/app/javascript/packs/component-definitions-common.js b/app/javascript/packs/component-definitions-common.js index 526816abcfb..691b545147b 100644 --- a/app/javascript/packs/component-definitions-common.js +++ b/app/javascript/packs/component-definitions-common.js @@ -102,6 +102,7 @@ import ReportDataTable from '../components/data-tables/report-data-table/report- import RetirementForm from '../components/retirement-form'; import RoleList from '../components/data-tables/role-list'; import RequestsTable from '../components/data-tables/requests-table'; +import RequestWorkflowStatus from '../components/request-workflow-status'; import RoutersForm from '../components/routers-form'; import ServiceDialogFromForm from '../components/service-dialog-from-form/service-dialog-from'; import ServiceDetailStdout from '../components/service-detail-stdout'; @@ -269,6 +270,7 @@ ManageIQ.component.addReact('ReportList', ReportList); ManageIQ.component.addReact('RetirementForm', RetirementForm); ManageIQ.component.addReact('RoleList', RoleList); ManageIQ.component.addReact('RequestsTable', RequestsTable); +ManageIQ.component.addReact('RequestWorkflowStatus', RequestWorkflowStatus); ManageIQ.component.addReact('RoutersForm', RoutersForm); ManageIQ.component.addReact('SearchBar', SearchBar); ManageIQ.component.addReact('ServiceDialogFromForm', ServiceDialogFromForm); diff --git a/app/stylesheet/application-webpack.scss b/app/stylesheet/application-webpack.scss index 44a952e4c4b..7d01d704c5e 100644 --- a/app/stylesheet/application-webpack.scss +++ b/app/stylesheet/application-webpack.scss @@ -26,4 +26,5 @@ @import './tree.scss'; @import './toolbar.scss'; @import './widget.scss'; +@import './workflows.scss'; @import './cloud-container-projects-dashboard.scss'; diff --git a/app/stylesheet/workflows.scss b/app/stylesheet/workflows.scss new file mode 100644 index 00000000000..8d709ee1667 --- /dev/null +++ b/app/stylesheet/workflows.scss @@ -0,0 +1,34 @@ +.workflow-states-list-container { + margin-bottom: 20px; + + .workflow-states-container { + padding: 10px; + border: 1px solid lightgray; + border-bottom: 0; + + &:last-child { + border-bottom: 1px solid lightgray; + } + + .workflow-status-container { + display: flex; + flex-direction: row; + align-items: center; + + .workflow-status-label { + font-weight:bold; + font-size: 14px; + margin-right: 5px; + } + + .workflow-status-tag { + margin-right: 5px; + } + } + + .workflow-notification-container { + margin-top: 10px; + } + } +} + diff --git a/app/views/miq_request/_st_prov_show.html.haml b/app/views/miq_request/_st_prov_show.html.haml index d8abdd5c773..b5bda77229f 100644 --- a/app/views/miq_request/_st_prov_show.html.haml +++ b/app/views/miq_request/_st_prov_show.html.haml @@ -30,3 +30,9 @@ = render :partial => "miq_request/request_dialog_details", :locals => {:wf => wf, :field => field} %hr + + - record_ids = request_task_configuration_script_ids(@miq_request) + - if record_ids.any? + %h3 + = _("Workflow States") + = react('RequestWorkflowStatus', {:ids => record_ids})