From 5ea64ed805533994799e4e023a21dd84905344b1 Mon Sep 17 00:00:00 2001 From: Dave Richer Date: Wed, 24 Jan 2024 17:18:43 -0500 Subject: [PATCH] - Progress Signed-off-by: Dave Richer --- .../job-lifecycle/job-lifecycle.component.jsx | 178 +++++++++++------- client/src/utils/DateFormatter.jsx | 3 + server/graphql-client/queries.js | 2 +- server/job/job-lifecycle.js | 74 ++------ server/utils/calculateStatusDuration.js | 59 ++++++ server/utils/durationToHumanReadable.js | 22 +++ 6 files changed, 215 insertions(+), 123 deletions(-) create mode 100644 server/utils/calculateStatusDuration.js create mode 100644 server/utils/durationToHumanReadable.js diff --git a/client/src/components/job-lifecycle/job-lifecycle.component.jsx b/client/src/components/job-lifecycle/job-lifecycle.component.jsx index 62ecb920a..3f48e826e 100644 --- a/client/src/components/job-lifecycle/job-lifecycle.component.jsx +++ b/client/src/components/job-lifecycle/job-lifecycle.component.jsx @@ -1,89 +1,135 @@ -import React, {useEffect, useMemo, useState} from 'react'; +import React, {useCallback, useEffect, useState} from 'react'; +import moment from "moment"; import axios from 'axios'; -import {Card, Space, Table, Timeline} from 'antd'; -import {Bar, BarChart, CartesianGrid, LabelList, Legend, ResponsiveContainer, Tooltip, XAxis, YAxis} from 'recharts'; +import {Card, Space, Table} from 'antd'; +import {gql, useQuery} from "@apollo/client"; +import {DateTimeFormatterFunction} from "../../utils/DateFormatter"; +import {isEmpty} from "lodash"; +import {Bar, BarChart, CartesianGrid, Legend, ResponsiveContainer, Tooltip, XAxis, YAxis} from "recharts"; + +const transformDataForChart = (durations) => { + const output = {}; + output.total = durations.total; + return durations.summations.forEach((summation) => { + output[summation.status] = summation.value + }); +}; +const getColor = (key) => { + // Generate a random color + const randomColor = '#' + Math.floor(Math.random()*16777215).toString(16); + return randomColor; +}; + export function JobLifecycleComponent({job, ...rest}) { const [loading, setLoading] = useState(true); const [lifecycleData, setLifecycleData] = useState(null); - useEffect(() => { - const getLifecycleData = async () => { - if (job && job.id) { - try { - setLoading(true); - const response = await axios.post("/job/lifecycle", {jobids: job.id}); - const data = response.data.transition[job.id]; - setLifecycleData(data); - } catch (err) { - console.error(`Error getting Job Lifecycle Data: ${err.message}`); - } finally { - setLoading(false); - } + // Used for tracking external state changes. + const {data} = useQuery(gql` + query get_job_test($id: uuid!){ + jobs_by_pk(id:$id){ + id + status } - }; + } + `, { + variables: { + id: job.id + }, + fetchPolicy: 'cache-only' + }); - getLifecycleData(); + /** + * Gets the lifecycle data for the job. + * @returns {Promise} + */ + const getLifecycleData = useCallback(async () => { + if (job && job.id) { + try { + setLoading(true); + const response = await axios.post("/job/lifecycle", {jobids: job.id}); + const data = response.data.transition[job.id]; + setLifecycleData(data); + } catch (err) { + console.error(`Error getting Job Lifecycle Data: ${err.message}`); + } finally { + setLoading(false); + } + } }, [job]); - const columnKeys = [ - 'start', 'end', 'value', 'prev_value', 'next_value', 'duration', 'type', 'created_at', 'updated_at', 'start_readable', 'end_readable','duration' + useEffect(() => { + if (!data) return; + setTimeout(() => { + getLifecycleData().catch(err => console.error(`Error getting Job Lifecycle Data: ${err.message}`)); + }, 1000); + }, [data, getLifecycleData]); + + const columns = [ + { + title: 'Value', + dataIndex: 'value', + key: 'value', + }, + { + title: 'Start', + dataIndex: 'start', + key: 'start', + render: (text) => DateTimeFormatterFunction(text), + sorter: (a, b) => moment(a.start).unix() - moment(b.start).unix(), + }, + { + title: 'Relative Start', + dataIndex: 'start_readable', + key: 'start_readable', + }, + { + title: 'End', + dataIndex: 'end', + key: 'end', + sorter: (a, b) => { + if (isEmpty(a.end) || isEmpty(b.end)) { + if (isEmpty(a.end) && isEmpty(b.end)) { + return 0; + } + return isEmpty(a.end) ? 1 : -1; + } + return moment(a.end).unix() - moment(b.end).unix(); + }, + render: (text) => isEmpty(text) ? 'N/A' : DateTimeFormatterFunction(text) + }, + { + title: 'Relative End', + dataIndex: 'end_readable', + key: 'end_readable', + }, + { + title: 'Duration', + dataIndex: 'duration_readable', + key: 'duration_readable', + sorter: (a, b) => a.duration - b.duration, + }, ]; - const columns = columnKeys.map(key => ({ - title: key.charAt(0).toUpperCase() + key.slice(1), - dataIndex: key, - key: key, - })); - - - const durationsData = useMemo(() => { - if (!lifecycleData) { - return []; - } - - const transformedData = Object.entries(lifecycleData.durations).map(([name, {value, humanReadable}]) => { - return { - name, - amt: value, - pv: humanReadable, - uv: value, - } - }) - - return [transformedData]; - }, [lifecycleData]); - - useEffect(() => { + console.log('LifeCycle Data'); console.dir(lifecycleData, {depth: null}) - console.dir(durationsData, {depth: null}) - - }, [lifecycleData,durationsData]); + }, [lifecycleData]); return ( {!loading ? ( - lifecycleData ? ( + lifecycleData && lifecycleData.lifecycle && lifecycleData.durations ? ( - - - {lifecycleData.lifecycle.map((item, index) => ( - - {item.value} - {item.start_readable} - - ))} - - - - + {lifecycleData.durations.summations.map((summation, idx) => { + + return ( + + ); + })} + + - + diff --git a/client/src/utils/DateFormatter.jsx b/client/src/utils/DateFormatter.jsx index d034266e3..e8137c54d 100644 --- a/client/src/utils/DateFormatter.jsx +++ b/client/src/utils/DateFormatter.jsx @@ -17,6 +17,9 @@ export function DateTimeFormatter(props) { ) : null; } +export function DateTimeFormatterFunction(date) { + return moment(date).format("MM/DD/YYYY hh:mm a"); +} export function TimeFormatter(props) { return props.children ? moment(props.children).format(props.format ? props.format : "hh:mm a") diff --git a/server/graphql-client/queries.js b/server/graphql-client/queries.js index 782bd3100..2bd83d1ab 100644 --- a/server/graphql-client/queries.js +++ b/server/graphql-client/queries.js @@ -519,7 +519,7 @@ exports.QUERY_PAYMENTS_FOR_EXPORT = ` }`; exports.QUERY_TRANSITIONS_BY_JOBID = `query QUERY_TRANSITIONS_BY_JOBID($jobids: [uuid!]!) { - transitions(where: {jobid: {_in: $jobids}}, order_by: {created_at: asc}) { + transitions(where: {jobid: {_in: $jobids}}, order_by: {end: desc}) { start end value diff --git a/server/job/job-lifecycle.js b/server/job/job-lifecycle.js index 07aaa037a..60d0c0be5 100644 --- a/server/job/job-lifecycle.js +++ b/server/job/job-lifecycle.js @@ -1,57 +1,14 @@ const _ = require("lodash"); const queries = require("../graphql-client/queries"); const moment = require("moment"); - -const calculateStatusDuration = (transitions) => { - let statusDuration = {}; - - transitions.forEach((transition, index) => { - let duration = transition.duration_minutes; - - // If there is no prev_value, it is the first transition - if (!transition.prev_value) { - statusDuration[transition.value] = { - value: duration, - humanReadable: transition.duration_readable - }; - } - // 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].value += duration; - statusDuration[transition.value].humanReadable = transition.duration_readable; - } else { - statusDuration[transition.value] = { - value: duration, - humanReadable: transition.duration_readable - }; - } - } - // For all other transitions - else { - if (statusDuration[transition.value]) { - statusDuration[transition.value].value += duration; - statusDuration[transition.value].humanReadable = transition.duration_readable; - } else { - statusDuration[transition.value] = { - value: duration, - humanReadable: transition.duration_readable - }; - } - } - }); - - return statusDuration; -} - +const durationToHumanReadable = require("../utils/durationToHumanReadable"); +const calculateStatusDuration = require("../utils/calculateStatusDuration"); const jobLifecycle = async (req, res) => { const {jobids} = req.body; const jobIDs = _.isArray(jobids) ? jobids : [jobids]; - const client = req.userGraphQLClient; - const resp = await client.request(queries.QUERY_TRANSITIONS_BY_JOBID, {jobids: jobIDs,}); const transitions = resp.transitions; @@ -67,19 +24,21 @@ const jobLifecycle = async (req, res) => { const transitionsByJobId = _.groupBy(resp.transitions, 'jobid'); const groupedTransitions = {}; - moment.relativeTimeThreshold('m', 30) + for (let jobId in transitionsByJobId) { let lifecycle = transitionsByJobId[jobId].map(transition => { - if (transition.start) { - transition.start_readable = moment(transition.start).fromNow(); - } - if (transition.end) { - transition.end_readable = moment(transition.end).fromNow(); - } - if(transition.duration){ - transition.duration_seconds = Math.round(transition.duration / 1000); - transition.duration_minutes = Math.round(transition.duration_seconds / 60); - transition.duration_readable = moment.duration(transition.duration).humanize(); + transition.start_readable = transition.start ? moment(transition.start).fromNow() : 'N/A'; + transition.end_readable = transition.end ? moment(transition.end).fromNow() : 'N/A'; + + if (transition.duration) { + transition.duration_seconds = Math.round(transition.duration / 1000); + transition.duration_minutes = Math.round(transition.duration_seconds / 60); + let duration = moment.duration(transition.duration); + transition.duration_readable = durationToHumanReadable(duration); + } else { + transition.duration_seconds = 0; + transition.duration_minutes = 0; + transition.duration_readable = 'N/A'; } return transition; }); @@ -90,13 +49,10 @@ const jobLifecycle = async (req, res) => { }; } - console.dir(groupedTransitions, {depth: null}) - return res.status(200).json({ jobIDs, transition: groupedTransitions, }); } - module.exports = jobLifecycle; \ No newline at end of file diff --git a/server/utils/calculateStatusDuration.js b/server/utils/calculateStatusDuration.js new file mode 100644 index 000000000..8a74e09f8 --- /dev/null +++ b/server/utils/calculateStatusDuration.js @@ -0,0 +1,59 @@ +const moment = require('moment'); +const durationToHumanReadable = require("./durationToHumanReadable"); +/** + * Calculate the duration of each status of a job + * @param transitions + * @returns {{}} + */ +const calculateStatusDuration = (transitions) => { + let statusDuration = {}; + let totalDuration = 0; + let summations = []; + + transitions.forEach((transition, index) => { + let duration = transition.duration; + totalDuration += duration; + + if (!transition.prev_value) { + statusDuration[transition.value] = { + value: duration, + humanReadable: transition.duration_readable + }; + } else if (!transition.next_value) { + if (statusDuration[transition.value]) { + statusDuration[transition.value].value += duration; + statusDuration[transition.value].humanReadable = transition.duration_readable; + } else { + statusDuration[transition.value] = { + value: duration, + humanReadable: transition.duration_readable + }; + } + } else { + if (statusDuration[transition.value]) { + statusDuration[transition.value].value += duration; + statusDuration[transition.value].humanReadable = transition.duration_readable; + } else { + statusDuration[transition.value] = { + value: duration, + humanReadable: transition.duration_readable + }; + } + } + }); + + for (let [status, {value, humanReadable}] of Object.entries(statusDuration)) { + if (status !== 'total') { + summations.push({status, value, humanReadable}); + } + } + + const humanReadableTotal = durationToHumanReadable(moment.duration(totalDuration)); + + return { + summations, + total: totalDuration, + humanReadableTotal + }; +} +module.exports = calculateStatusDuration; \ No newline at end of file diff --git a/server/utils/durationToHumanReadable.js b/server/utils/durationToHumanReadable.js new file mode 100644 index 000000000..f13e24c98 --- /dev/null +++ b/server/utils/durationToHumanReadable.js @@ -0,0 +1,22 @@ +const durationToHumanReadable = (duration) => { + if (!duration) return 'N/A'; + + let parts = []; + + let years = duration.years(); + let months = duration.months(); + let days = duration.days(); + let hours = duration.hours(); + let minutes = duration.minutes(); + let seconds = duration.seconds(); + + if (years) parts.push(years + ' year' + (years > 1 ? 's' : '')); + if (months) parts.push(months + ' month' + (months > 1 ? 's' : '')); + if (days) parts.push(days + ' day' + (days > 1 ? 's' : '')); + if (hours) parts.push(hours + ' hour' + (hours > 1 ? 's' : '')); + if (minutes) parts.push(minutes + ' minute' + (minutes > 1 ? 's' : '')); + if (!minutes && !hours && !days && !months && !years && seconds) parts.push(seconds + ' second' + (seconds > 1 ? 's' : '')); + + return parts.join(', '); +} +module.exports = durationToHumanReadable; \ No newline at end of file