- Progress

Signed-off-by: Dave Richer <dave@imexsystems.ca>
This commit is contained in:
Dave Richer
2024-01-24 10:07:07 -05:00
parent d0a2bb7da0
commit d740446ccb
3 changed files with 92 additions and 106 deletions

View File

@@ -1,139 +1,110 @@
import {createStructuredSelector} from "reselect"; import React, {useEffect, useMemo, useState} from 'react';
import {selectBodyshop} from "../../redux/user/user.selectors"; import axios from 'axios';
import {connect} from "react-redux"; import {Card, Space, Table, Timeline} from 'antd';
import {useCallback, useEffect, useState} from "react"; import {Bar, BarChart, CartesianGrid, LabelList, Legend, ResponsiveContainer, Tooltip, XAxis, YAxis} from 'recharts';
import axios from "axios";
import {Card, Space, Table, Timeline} from "antd";
import {Cell, LabelList, Legend, Pie, PieChart, Tooltip} from "recharts";
const mapStateToProps = createStructuredSelector({ export function JobLifecycleComponent({job, ...rest}) {
bodyshop: selectBodyshop,
});
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 [loading, setLoading] = useState(true);
const [lifecycleData, setLifecycleData] = useState(null); const [lifecycleData, setLifecycleData] = useState(null);
useEffect(() => { useEffect(() => {
async function getLifecycleData() { const getLifecycleData = async () => {
if (job && job.id) { if (job && job.id) {
setLoading(true); try {
const response = await axios.post("/job/lifecycle", { setLoading(true);
jobids: job.id, const response = await axios.post("/job/lifecycle", {jobids: job.id});
}); const data = response.data.transition[job.id];
const data = response.data.transition[job.id]; setLifecycleData(data);
setLifecycleData(data); } catch (err) {
setLoading(false); console.error(`Error getting Job Lifecycle Data: ${err.message}`);
} finally {
setLoading(false);
}
} }
} };
getLifecycleData().catch((err) => { getLifecycleData();
console.log(`Something went wrong getting Job Lifecycle Data: ${err.message}`);
setLoading(false);
});
}, [job]); }, [job]);
// // TODO - Delete this useEffect, it is for testing
// useEffect(() => {
// console.dir(lifecycleData)
// }, [lifecycleData]);
const columnKeys = [ const columnKeys = [
'start', 'start', 'end', 'value', 'prev_value', 'next_value', 'duration', 'type', 'created_at', 'updated_at', 'start_readable', 'end_readable','duration'
'end',
'value',
'prev_value',
'next_value',
'duration',
'type',
'created_at',
'updated_at',
'start_readable',
'end_readable',
]; ];
const columns = columnKeys.map(key => ({ const columns = columnKeys.map(key => ({
title: key.charAt(0).toUpperCase() + key.slice(1), // Capitalize the first letter for the title title: key.charAt(0).toUpperCase() + key.slice(1),
dataIndex: key, dataIndex: key,
key: key, 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) => ( const durationsData = useMemo(() => {
<Cell key={`cell-${index}`} fill={COLORS[index % COLORS.length]}> if (!lifecycleData) {
<LabelList dataKey="name" position="insideTop" /> return [];
<LabelList dataKey="value" position="insideBottom" /> }
</Cell>
));
}, [lifecycleData, job]);
/** const transformedData = Object.entries(lifecycleData.durations).map(([name, {value, humanReadable}]) => {
* Returns an array of objects with the name and value of the duration return {
* @type {function(): {name: *, value}[]} name,
*/ amt: value,
const durationsData = useCallback(() => { pv: humanReadable,
return Object.entries(lifecycleData.durations) .filter(([name, value]) => { uv: value,
return value > 0; }
}).map(([name, value]) => ({ })
name,
value: value / 1000 return [transformedData];
})) }, [lifecycleData]);
}, [lifecycleData, job]);
useEffect(() => {
console.dir(lifecycleData, {depth: null})
console.dir(durationsData, {depth: null})
}, [lifecycleData,durationsData]);
return ( return (
<Card loading={loading} title='Job Lifecycle Component'> <Card loading={loading} title='Job Lifecycle Component'>
{!loading ? ( {!loading ? (
lifecycleData ? ( lifecycleData ? (
<Space direction='vertical' style={{width: '100%'}}> <Space direction='vertical' style={{width: '100%'}}>
<Card type='inner' title='Table Format'>
<Table columns={columns} dataSource={lifecycleData.lifecycle}/>
</Card>
<Space direction='horizontal' style={{width: '100%'}} align='start'> <Space direction='horizontal' style={{width: '100%'}} align='start'>
<Card type='inner' title='Timeline Format'> <Card type='inner' title='Timeline Format'>
<Timeline> <Timeline>
{lifecycleData.lifecycle.map((item, index) => ( {lifecycleData.lifecycle.map((item, index) => (
<Timeline.Item key={index} color={item.value === 'Open' ? 'green' : item.value === 'Scheduled' ? 'yellow' : 'red'}> <Timeline.Item key={index}
color={item.value === 'Open' ? 'green' : item.value === 'Scheduled' ? 'yellow' : 'red'}>
{item.value} - {item.start_readable} {item.value} - {item.start_readable}
</Timeline.Item> </Timeline.Item>
))} ))}
</Timeline> </Timeline>
</Card> </Card>
<Card type='inner' title='Durations'> <Card type='inner' title='Durations'>
<PieChart width={400} height={400}> <ResponsiveContainer width="100%" height="100%">
<Pie <BarChart
data={durationsData()} width={500}
cx={200} height={300}
cy={200} data={durationsData}
labelLine={false} margin={{
label={({name, percent}) => `${name}: ${(percent * 100).toFixed(0)}%`} top: 20,
outerRadius={80} right: 30,
fill="#8884d8" left: 20,
dataKey="value" bottom: 5,
}}
> >
{renderCells()} <CartesianGrid strokeDasharray="3 3" />
</Pie> <XAxis dataKey="name" />
<Tooltip/> <YAxis />
<Legend/> <Tooltip />
</PieChart> <Legend />
<Bar dataKey="pv" stackId="a" fill="#8884d8" />
<Bar dataKey="uv" stackId="a" fill="#82ca9d" />
</BarChart>
</ResponsiveContainer>
</Card> </Card>
</Space> </Space>
<Card type='inner' title='Table Format'>
<Table columns={columns} dataSource={lifecycleData.lifecycle}/>
</Card>
</Space> </Space>
) : ( ) : (
<Card type='inner' style={{textAlign: 'center'}}> <Card type='inner' style={{textAlign: 'center'}}>
@@ -149,4 +120,4 @@ export function JobLifecycleComponent({bodyshop, job, ...rest}) {
); );
} }
export default connect(mapStateToProps, mapDispatchToProps)(JobLifecycleComponent); export default JobLifecycleComponent;

View File

@@ -294,7 +294,7 @@ export function JobsDetailPage({
tab={<span><BarsOutlined />Lifecycle</span>} tab={<span><BarsOutlined />Lifecycle</span>}
key="lifecycle" key="lifecycle"
> >
<JobLifecycleComponent job={job}/> <JobLifecycleComponent job={job} refetch={refetch} form={form}/>
</Tabs.TabPane> </Tabs.TabPane>
<Tabs.TabPane <Tabs.TabPane
forceRender forceRender

View File

@@ -6,26 +6,37 @@ const calculateStatusDuration = (transitions) => {
let statusDuration = {}; let statusDuration = {};
transitions.forEach((transition, index) => { transitions.forEach((transition, index) => {
let duration = transition.duration; let duration = transition.duration_minutes;
// If there is no prev_value, it is the first transition // If there is no prev_value, it is the first transition
if (!transition.prev_value) { if (!transition.prev_value) {
statusDuration[transition.value] = duration; statusDuration[transition.value] = {
value: duration,
humanReadable: transition.duration_readable
};
} }
// If there is no next_value, it is the last transition (the active one) // If there is no next_value, it is the last transition (the active one)
else if (!transition.next_value) { else if (!transition.next_value) {
if (statusDuration[transition.value]) { if (statusDuration[transition.value]) {
statusDuration[transition.value] += duration; statusDuration[transition.value].value += duration;
statusDuration[transition.value].humanReadable = transition.duration_readable;
} else { } else {
statusDuration[transition.value] = duration; statusDuration[transition.value] = {
value: duration,
humanReadable: transition.duration_readable
};
} }
} }
// For all other transitions // For all other transitions
else { else {
if (statusDuration[transition.value]) { if (statusDuration[transition.value]) {
statusDuration[transition.value] += duration; statusDuration[transition.value].value += duration;
statusDuration[transition.value].humanReadable = transition.duration_readable;
} else { } else {
statusDuration[transition.value] = duration; statusDuration[transition.value] = {
value: duration,
humanReadable: transition.duration_readable
};
} }
} }
}); });
@@ -53,7 +64,6 @@ const jobLifecycle = async (req, res) => {
} }
const transitionsByJobId = _.groupBy(resp.transitions, 'jobid'); const transitionsByJobId = _.groupBy(resp.transitions, 'jobid');
const groupedTransitions = {}; const groupedTransitions = {};
@@ -66,6 +76,11 @@ const jobLifecycle = async (req, res) => {
if (transition.end) { if (transition.end) {
transition.end_readable = moment(transition.end).fromNow(); 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();
}
return transition; return transition;
}); });
@@ -75,7 +90,7 @@ const jobLifecycle = async (req, res) => {
}; };
} }
console.dir(groupedTransitions, {depth: null}); console.dir(groupedTransitions, {depth: null})
return res.status(200).json({ return res.status(200).json({
jobIDs, jobIDs,