From f59bdf90303fb6a931c7fa6d16424d6a2da682df Mon Sep 17 00:00:00 2001 From: Dave Richer Date: Tue, 23 Jan 2024 12:35:58 -0500 Subject: [PATCH] - Progress Update Signed-off-by: Dave Richer --- .../job-lifecycle/job-lifecycle.component.jsx | 100 +++++++++++++++--- server/graphql-client/queries.js | 5 +- server/job/job-lifecycle.js | 75 ++++++++++--- 3 files changed, 147 insertions(+), 33 deletions(-) diff --git a/client/src/components/job-lifecycle/job-lifecycle.component.jsx b/client/src/components/job-lifecycle/job-lifecycle.component.jsx index c9303177e..3d356c575 100644 --- a/client/src/components/job-lifecycle/job-lifecycle.component.jsx +++ b/client/src/components/job-lifecycle/job-lifecycle.component.jsx @@ -1,9 +1,10 @@ import {createStructuredSelector} from "reselect"; import {selectBodyshop} from "../../redux/user/user.selectors"; import {connect} from "react-redux"; -import {useEffect, useState} from "react"; +import {useCallback, useEffect, useState} from "react"; import axios from "axios"; import {Card, Space, Table, Timeline} from "antd"; +import {Cell, LabelList, Legend, Pie, PieChart, Tooltip} from "recharts"; const mapStateToProps = createStructuredSelector({ bodyshop: selectBodyshop, @@ -12,6 +13,9 @@ const mapDispatchToProps = (dispatch) => ({ //setUserLanguage: language => dispatch(setUserLanguage(language)) }); +const COLORS = ['#0088FE', '#00C49F', '#FFBB28', '#FF8042', '#8884d8']; + + export function JobLifecycleComponent({bodyshop, job, ...rest}) { const [loading, setLoading] = useState(true); const [lifecycleData, setLifecycleData] = useState(null); @@ -24,8 +28,8 @@ export function JobLifecycleComponent({bodyshop, job, ...rest}) { const response = await axios.post("/job/lifecycle", { jobids: job.id, }); - console.dir(response.data.data.transitions, {depth: null}); - setLifecycleData(response.data.data.transitions); + const data = response.data.transition[job.id]; + setLifecycleData(data); setLoading(false); } } @@ -36,6 +40,11 @@ export function JobLifecycleComponent({bodyshop, job, ...rest}) { }); }, [job]); + // // TODO - Delete this useEffect, it is for testing + // useEffect(() => { + // console.dir(lifecycleData) + // }, [lifecycleData]); + const columnKeys = [ 'start', 'end', @@ -54,23 +63,82 @@ export function JobLifecycleComponent({bodyshop, job, ...rest}) { key: key, })); + /** + * Returns an array of cells for the Pie Chart + * @type {function(): *[]} + */ + const renderCells = useCallback(() => { + const entires = Object + .entries(lifecycleData.durations) + .filter(([name, value]) => { + return value !== 0; + }) + + return entires.map(([name, value], index) => ( + + + + + )); + }, [lifecycleData, job]); + + /** + * Returns an array of objects with the name and value of the duration + * @type {function(): {name: *, value}[]} + */ + const durationsData = useCallback(() => { + return Object.entries(lifecycleData.durations) .filter(([name, value]) => { + return value !== 0; + }).map(([name, value]) => ({ + name, + value: value / 1000 + })) + }, [lifecycleData, job]); + return ( {!loading ? ( - - - + lifecycleData ? ( + + +
+ + + + + {lifecycleData.lifecycle.map((item, index) => ( + + {item.value} - {new Date(item.start).toLocaleString()} + + ))} + + + + + `${name}: ${(percent * 100).toFixed(0)}%`} + outerRadius={80} + fill="#8884d8" + dataKey="value" + > + {renderCells()} + + + + + + + + + ) : ( + + There is currently no lifecycle data for this job. - - - {lifecycleData.map((item, index) => ( - - {item.value} - {new Date(item.start).toLocaleString()} - - ))} - - - + ) ) : ( Loading Job Timelines.... diff --git a/server/graphql-client/queries.js b/server/graphql-client/queries.js index a642a4988..782bd3100 100644 --- a/server/graphql-client/queries.js +++ b/server/graphql-client/queries.js @@ -518,8 +518,8 @@ exports.QUERY_PAYMENTS_FOR_EXPORT = ` } }`; -exports.QUERY_TRANSITIONS_BY_JOBID = `query QUERY_TRANSITIONS_BY_JOBID($jobid: uuid!) { - transitions(where: {jobid: {_eq: $jobid}}, order_by: {id: asc}) { +exports.QUERY_TRANSITIONS_BY_JOBID = `query QUERY_TRANSITIONS_BY_JOBID($jobids: [uuid!]!) { + transitions(where: {jobid: {_in: $jobids}}, order_by: {created_at: asc}) { start end value @@ -529,6 +529,7 @@ exports.QUERY_TRANSITIONS_BY_JOBID = `query QUERY_TRANSITIONS_BY_JOBID($jobid: u type created_at updated_at + jobid } }`; diff --git a/server/job/job-lifecycle.js b/server/job/job-lifecycle.js index 207a514e3..8eddd86a5 100644 --- a/server/job/job-lifecycle.js +++ b/server/job/job-lifecycle.js @@ -1,28 +1,73 @@ const _ = require("lodash"); const queries = require("../graphql-client/queries"); -const jobLifecycle = (req, res) => { + +const calculateStatusDuration = (transitions) => { + let statusDuration = {}; + + transitions.forEach((transition, index) => { + let duration = transition.duration; + + // If there is no prev_value, it is the first transition + if (!transition.prev_value) { + statusDuration[transition.value] = duration; + } + // If there is no next_value, it is the last transition (the active one) + else if (!transition.next_value) { + if (statusDuration[transition.value]) { + statusDuration[transition.value] += duration; + } else { + statusDuration[transition.value] = duration; + } + } + // For all other transitions + else { + if (statusDuration[transition.value]) { + statusDuration[transition.value] += duration; + } else { + statusDuration[transition.value] = duration; + } + } + }); + + return statusDuration; +} + + +const jobLifecycle = async (req, res) => { const {jobids} = req.body; - return _.isArray(jobids) ? - handleMultipleJobs(jobids, req, res) : - handleSingleJob(jobids, req, res); -}; + const jobIDs = _.isArray(jobids) ? jobids : [jobids]; -const handleMultipleJobs = (jobIDs, req, res) => { - return res.status(200).send(jobIDs); -} - -const handleSingleJob = async (jobIds, req, res) => { const client = req.userGraphQLClient; - const resp = await client.request(queries.QUERY_TRANSITIONS_BY_JOBID, {jobid: jobIds,}); + const resp = await client.request(queries.QUERY_TRANSITIONS_BY_JOBID, {jobids: jobIDs,}); + + const transitions = resp.transitions; + + if (!transitions) { + return res.status(200).json({ + jobIDs, + transitions: [] + }); - const response = { - jobIds, - data: resp } - return res.status(200).json(response); + const transitionsByJobId = _.groupBy(resp.transitions, 'jobid'); + + const groupedTransitions = {}; + + for (let jobId in transitionsByJobId) { + groupedTransitions[jobId] = { + lifecycle: transitionsByJobId[jobId], + durations: calculateStatusDuration(transitionsByJobId[jobId]) + }; + } + + return res.status(200).json({ + jobIDs, + transition: groupedTransitions, + }); } + module.exports = jobLifecycle; \ No newline at end of file