Skip to content

Commit cf29c3a

Browse files
authored
Merge pull request #8782 from jeffibm/workflow-status-list
Introducing workflow status list in request's show page
2 parents 05d8876 + 6ab1930 commit cf29c3a

File tree

8 files changed

+277
-0
lines changed

8 files changed

+277
-0
lines changed

app/helpers/miq_request_helper.rb

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,4 +5,8 @@ module MiqRequestHelper
55
def row_data(label, value)
66
{:cells => {:label => label, :value => value}}
77
end
8+
9+
def request_task_configuration_script_ids(miq_request)
10+
miq_request.miq_request_tasks.map { |task| task.options&.dig(:configuration_script_id) }.compact
11+
end
812
end
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
/** Types of workflow state status */
2+
export const workflowStateTypes = {
3+
success: { text: 'success', tagType: 'green' },
4+
error: { text: 'error', tagType: 'red' },
5+
failed: { text: 'failed', tagType: 'gray' },
6+
pending: { text: 'pending', tagType: 'gray' },
7+
};
8+
9+
/** Function to get the header data of workflow states table. */
10+
const headerData = () => ([
11+
{
12+
key: 'name',
13+
header: __('Name'),
14+
},
15+
{
16+
key: 'enteredTime',
17+
header: __('Entered Time'),
18+
},
19+
{
20+
key: 'finishedTime',
21+
header: __('Finished Time'),
22+
},
23+
{
24+
key: 'duration',
25+
header: __('Duration'),
26+
},
27+
]);
28+
29+
// const convertDate = (date) => `${moment(date).format('MM/DD/YYYY')} ${moment(date).format('h:mm:ss A')}`;
30+
const convertDate = (date) => {
31+
const utcDate = new Date(date);
32+
const year = utcDate.getUTCFullYear();
33+
const month = (utcDate.getUTCMonth() + 1).toString().padStart(2, '0');
34+
const day = utcDate.getUTCDate().toString().padStart(2, '0');
35+
const hours = utcDate.getUTCHours();
36+
const minutes = utcDate.getUTCMinutes().toString().padStart(2, '0');
37+
const period = hours < 12 ? 'AM' : 'PM';
38+
const hours12 = hours % 12 || 12; // Convert 0 to 12 for 12:00 AM
39+
40+
const formattedDate = `${month}/${day}/${year} ${hours12}:${minutes} ${period}`;
41+
return formattedDate.toString();
42+
};
43+
44+
/** Function to get the row data of workflow states table. */
45+
const rowData = ({ StateHistory }) => StateHistory.map((item) => ({
46+
id: item.Guid.toString(),
47+
name: item.Name,
48+
enteredTime: convertDate(item.EnteredTime.toString()),
49+
finishedTime: convertDate(item.FinishedTime.toString()),
50+
duration: item.Duration.toFixed(3).toString(),
51+
}));
52+
53+
/** Function to return the header, row and status data required for the RequestWorkflowStatus component. */
54+
export const workflowStatusData = (response) => {
55+
const type = 'ManageIQ::Providers::Workflows::AutomationManager::WorkflowInstance';
56+
if (response.type !== type) {
57+
return undefined;
58+
}
59+
const rows = response.context ? rowData(response.context) : [];
60+
const headers = headerData();
61+
const name = response.name || response.description;
62+
return {
63+
headers, rows, status: response.status, name, parentId: response.parent_id, id: response.id, type: response.type,
64+
};
65+
};
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
import React from 'react';
2+
import PropTypes from 'prop-types';
3+
import RequestWorkflowStatusItem from './request-workflow-status-item';
4+
5+
/** Component to render the Workflow status in /miq_request/show/#{id} page */
6+
const RequestWorkflowStatus = ({ ids }) => (
7+
<div className="workflow-states-list-container">
8+
{ ids.map((id) => (
9+
<RequestWorkflowStatusItem recordId={id} key={id} />
10+
))}
11+
</div>
12+
);
13+
14+
RequestWorkflowStatus.propTypes = {
15+
ids: PropTypes.arrayOf(PropTypes.number).isRequired,
16+
};
17+
18+
export default RequestWorkflowStatus;
Lines changed: 147 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,147 @@
1+
import React, { useEffect, useState, useRef } from 'react';
2+
import {
3+
Tag, Loading, Link,
4+
} from 'carbon-components-react';
5+
import PropTypes from 'prop-types';
6+
import { workflowStatusData, workflowStateTypes } from './data';
7+
import MiqDataTable from '../miq-data-table';
8+
import NotificationMessage from '../notification-message';
9+
10+
/** Component to render the Workflow status in /miq_request/show/#{id} page */
11+
const RequestWorkflowStatusItem = ({ recordId }) => {
12+
const RELOAD = 2000; // Time interval to reload the RequestWorkflowStatus component.
13+
const reloadLimit = 5; // This is to handle the Auto refresh issue causing the the server to burn out with multiple requests.
14+
const reloadCount = useRef(0);
15+
16+
const [data, setData] = useState(
17+
{
18+
isLoading: true,
19+
responseData: undefined,
20+
message: undefined,
21+
list: [],
22+
parentName: undefined,
23+
validType: false,
24+
}
25+
);
26+
27+
/** Function to get the Workflow */
28+
const getWorkflow = async() => {
29+
reloadCount.current += 1;
30+
const url = `/api/configuration_scripts/${recordId}`;
31+
API.get(url, { skipErrors: [404, 400, 500] })
32+
.then((response) => {
33+
const responseData = workflowStatusData(response);
34+
if (responseData) {
35+
API.get(`/api/configuration_script_payloads/${responseData.parentId}`).then((response2) => {
36+
if (response.context) {
37+
setData({
38+
...data,
39+
responseData,
40+
isLoading: false,
41+
parentName: response2.name,
42+
validType: true,
43+
message: responseData && responseData.status === workflowStateTypes.error.text
44+
? __('An error has occurred with this workflow') : undefined,
45+
});
46+
} else {
47+
setData({
48+
...data,
49+
responseData,
50+
isLoading: false,
51+
parentName: response2.name,
52+
validType: true,
53+
message: sprintf(__('Context is not available for "%s"'), response.name),
54+
});
55+
}
56+
});
57+
} else {
58+
setData({
59+
...data,
60+
validType: false,
61+
responseData: undefined,
62+
isLoading: false,
63+
});
64+
}
65+
});
66+
};
67+
68+
/** Logic to reload the component every so often (RELOAD). */
69+
useEffect(() => {
70+
const omitStatus = [workflowStateTypes.success.text, workflowStateTypes.error.text];
71+
if (reloadCount.current <= reloadLimit && data.responseData && data.responseData.status && !omitStatus.includes(data.responseData.status)) {
72+
const interval = setInterval(() => {
73+
setData({ ...data, isLoading: true });
74+
getWorkflow();
75+
}, RELOAD);
76+
return () => clearInterval(interval); // This represents the unmount function, in which you need to clear your interval to prevent memory leaks.
77+
}
78+
return undefined;
79+
}, [data.responseData]);
80+
81+
useEffect(() => {
82+
if (recordId) {
83+
getWorkflow();
84+
}
85+
}, [recordId]);
86+
87+
/** Function to render the status of workflow. */
88+
const renderStatusTag = () => {
89+
const status = workflowStateTypes[data.responseData.status];
90+
return (
91+
<Tag type={status.tagType} title={status.text}>
92+
{status.text.toUpperCase()}
93+
</Tag>
94+
);
95+
};
96+
97+
/** Function to render the status of workflow status. */
98+
const renderWorkflowStatus = () => (
99+
<div className="workflow-status-container">
100+
<div className="workflow-status-tag">
101+
{data.responseData && data.responseData.status && renderStatusTag()}
102+
</div>
103+
<div className="workflow-status-label">
104+
<Link href={`/workflow/show/${data.responseData.parentId}/`}>{data.parentName.toString()}</Link>
105+
</div>
106+
<div className="workflow-status-action">
107+
{data.isLoading && <Loading active small withOverlay={false} className="loading" />}
108+
</div>
109+
</div>
110+
);
111+
112+
/** Function to render the notification. */
113+
const renderNotitication = () => (
114+
<div className="workflow-notification-container">
115+
<NotificationMessage type="error" message={data.message} />
116+
</div>
117+
);
118+
119+
/** Function to render the list. */
120+
const renderList = ({ headers, rows }) => (
121+
<MiqDataTable
122+
headers={headers}
123+
rows={rows}
124+
mode="request-workflow-status"
125+
/>
126+
);
127+
128+
return (
129+
<>
130+
{
131+
data.validType && (
132+
<div className="workflow-states-container">
133+
{data.responseData && renderWorkflowStatus()}
134+
{data.message && renderNotitication()}
135+
{data.responseData && data.responseData.status && renderList(data.responseData)}
136+
</div>
137+
)
138+
}
139+
</>
140+
);
141+
};
142+
143+
RequestWorkflowStatusItem.propTypes = {
144+
recordId: PropTypes.number.isRequired,
145+
};
146+
147+
export default RequestWorkflowStatusItem;

app/javascript/packs/component-definitions-common.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,7 @@ import ReportDataTable from '../components/data-tables/report-data-table/report-
102102
import RetirementForm from '../components/retirement-form';
103103
import RoleList from '../components/data-tables/role-list';
104104
import RequestsTable from '../components/data-tables/requests-table';
105+
import RequestWorkflowStatus from '../components/request-workflow-status';
105106
import RoutersForm from '../components/routers-form';
106107
import ServiceDialogFromForm from '../components/service-dialog-from-form/service-dialog-from';
107108
import ServiceDetailStdout from '../components/service-detail-stdout';
@@ -270,6 +271,7 @@ ManageIQ.component.addReact('ReportList', ReportList);
270271
ManageIQ.component.addReact('RetirementForm', RetirementForm);
271272
ManageIQ.component.addReact('RoleList', RoleList);
272273
ManageIQ.component.addReact('RequestsTable', RequestsTable);
274+
ManageIQ.component.addReact('RequestWorkflowStatus', RequestWorkflowStatus);
273275
ManageIQ.component.addReact('RoutersForm', RoutersForm);
274276
ManageIQ.component.addReact('SearchBar', SearchBar);
275277
ManageIQ.component.addReact('ServiceDialogFromForm', ServiceDialogFromForm);

app/stylesheet/application-webpack.scss

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,4 +26,5 @@
2626
@import './tree.scss';
2727
@import './toolbar.scss';
2828
@import './widget.scss';
29+
@import './workflows.scss';
2930
@import './cloud-container-projects-dashboard.scss';

app/stylesheet/workflows.scss

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
.workflow-states-list-container {
2+
margin-bottom: 20px;
3+
4+
.workflow-states-container {
5+
padding: 10px;
6+
border: 1px solid lightgray;
7+
border-bottom: 0;
8+
9+
&:last-child {
10+
border-bottom: 1px solid lightgray;
11+
}
12+
13+
.workflow-status-container {
14+
display: flex;
15+
flex-direction: row;
16+
align-items: center;
17+
18+
.workflow-status-label {
19+
font-weight:bold;
20+
font-size: 14px;
21+
margin-right: 5px;
22+
}
23+
24+
.workflow-status-tag {
25+
margin-right: 5px;
26+
}
27+
}
28+
29+
.workflow-notification-container {
30+
margin-top: 10px;
31+
}
32+
}
33+
}
34+

app/views/miq_request/_st_prov_show.html.haml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,3 +30,9 @@
3030
= render :partial => "miq_request/request_dialog_details",
3131
:locals => {:wf => wf, :field => field}
3232
%hr
33+
34+
- record_ids = request_task_configuration_script_ids(@miq_request)
35+
- if record_ids.any?
36+
%h3
37+
= _("Workflow States")
38+
= react('RequestWorkflowStatus', {:ids => record_ids})

0 commit comments

Comments
 (0)