@@ -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;
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
Reference in New Issue
Block a user