Files
bodyshop/client/src/components/dashboard-components/job-lifecycle/job-lifecycle-dashboard.component.jsx
2025-12-04 16:22:41 -05:00

208 lines
7.1 KiB
JavaScript

import { Card, Table, Tag } from "antd";
import axios from "axios";
import { useEffect, useState } from "react";
import { useTranslation } from "react-i18next";
import dayjs from "../../../utils/day";
import LoadingSkeleton from "../../loading-skeleton/loading-skeleton.component";
import DashboardRefreshRequired from "../refresh-required.component";
const fortyFiveDaysAgo = () => dayjs().subtract(45, "day").toLocaleString();
export default function JobLifecycleDashboardComponent({ data, bodyshop, ...cardProps }) {
const { t } = useTranslation();
const [loading, setLoading] = useState(false);
const [lifecycleData, setLifecycleData] = useState(null);
useEffect(() => {
async function getLifecycleData() {
if (data && data.job_lifecycle) {
setLoading(true);
const response = await axios.post("/job/lifecycle", {
jobids: data.job_lifecycle.map((x) => x.id),
statuses: bodyshop.md_ro_statuses
});
setLifecycleData(response.data.durations);
setLoading(false);
}
}
getLifecycleData().catch((e) => {
console.error(`Error in getLifecycleData: ${e}`);
});
}, [data, bodyshop]);
const columns = [
{
title: t("job_lifecycle.columns.status"),
dataIndex: "status",
bgColor: "red",
key: "status",
render: (text, record) => {
return <Tag color={record.color}>{record.status}</Tag>;
}
},
{
title: t("job_lifecycle.columns.human_readable"),
dataIndex: "humanReadable",
key: "humanReadable"
},
{
title: t("job_lifecycle.columns.average_human_readable"),
dataIndex: "averageHumanReadable",
key: "averageHumanReadable"
},
{
title: t("job_lifecycle.columns.status_count"),
key: "statusCount",
render: (text, record) => {
return lifecycleData.statusCounts[record.status];
}
},
{
title: t("job_lifecycle.columns.percentage"),
dataIndex: "percentage",
key: "percentage",
render: (text, record) => {
return record.percentage.toFixed(2) + "%";
}
}
];
if (!data) return null;
if (!data.job_lifecycle || !lifecycleData) return <DashboardRefreshRequired {...cardProps} />;
const extra = `${t("job_lifecycle.content.calculated_based_on")} ${lifecycleData.jobs} ${t("job_lifecycle.content.jobs_in_since")} ${fortyFiveDaysAgo()}`;
return (
<Card title={t("job_lifecycle.titles.dashboard")} {...cardProps}>
<LoadingSkeleton loading={loading}>
<div style={{ overflow: "scroll", height: "100%" }}>
<div
id="bar-container"
style={{
display: "flex",
width: "100%",
height: "100px",
textAlign: "center",
borderRadius: "5px",
borderWidth: "5px",
borderStyle: "solid",
borderColor: "var(--bar-border-color)",
margin: 0,
padding: 0
}}
>
{lifecycleData.summations.map((key, index, array) => {
const isFirst = index === 0;
const isLast = index === array.length - 1;
return (
<div
key={key.status}
style={{
overflow: "hidden",
display: "flex",
flexDirection: "column",
justifyContent: "center",
alignItems: "center",
margin: 0,
padding: 0,
borderTop: "1px solid var(--bar-border-color)",
borderBottom: "1px solid var(--bar-border-color)",
borderLeft: isFirst ? "1px solid var(--bar-border-color)" : undefined,
borderRight: isLast ? "1px solid var(--bar-border-color)" : undefined,
backgroundColor: key.color,
width: `${key.percentage}%`
}}
aria-label={`${key.status} | ${key.roundedPercentage} | ${key.humanReadable}`}
title={`${key.status} | ${key.roundedPercentage} | ${key.humanReadable}`}
>
{key.percentage > 15 ? (
<>
<div>{key.roundedPercentage}</div>
<div
style={{
backgroundColor: "var(--tag-wrapper-bg)",
borderRadius: "5px",
paddingRight: "2px",
paddingLeft: "2px",
fontSize: "0.8rem"
}}
>
{key.status}
</div>
</>
) : null}
</div>
);
})}
</div>
<Card
extra={extra}
type="inner"
title={t("job_lifecycle.content.legend_title")}
style={{ marginTop: "10px" }}
>
<div
style={{
display: "flex",
flexWrap: "wrap",
gap: 8
}}
>
{lifecycleData.summations.map((key) => (
<Tag
key={key.status}
color={key.color}
style={{
// IMPORTANT: let the tag grow with its content
width: "auto",
padding: 0,
margin: 0,
display: "inline-flex",
alignItems: "center",
justifyContent: "center",
boxSizing: "border-box"
}}
>
<div
aria-label={`${key.status} | ${key.roundedPercentage} | ${key.humanReadable}`}
title={`${key.status} | ${key.roundedPercentage} | ${key.humanReadable}`}
style={{
backgroundColor: "var(--tag-wrapper-bg)",
color: "var(--tag-wrapper-text)",
padding: "4px 8px",
textAlign: "center",
whiteSpace: "nowrap" // keep it on one line while letting the pill expand
}}
>
{key.status} [{lifecycleData.statusCounts[key.status]}] ({key.roundedPercentage})
</div>
</Tag>
))}
</div>
</Card>
<Card style={{ marginTop: "5px" }} type="inner" title={t("job_lifecycle.titles.top_durations")}>
<Table
size="small"
pagination={false}
columns={columns}
rowKey={(record) => record.status}
dataSource={lifecycleData.summations.sort((a, b) => b.value - a.value).slice(0, 3)}
/>
</Card>
</div>
</LoadingSkeleton>
</Card>
);
}
export const JobLifecycleDashboardGQL = `
job_lifecycle: jobs(where: {
actual_in: {
_gte: "${dayjs().subtract(45, "day").toISOString()}"
}
}) {
id
actual_in
} `;