317 lines
12 KiB
JavaScript
317 lines
12 KiB
JavaScript
import { useCallback, useEffect, useState } from "react";
|
|
import dayjs from "../../utils/day";
|
|
import axios from "axios";
|
|
import { Badge, Card, Space, Table, Tag } from "antd";
|
|
import { gql, useQuery } from "@apollo/client";
|
|
import { DateTimeFormatterFunction } from "../../utils/DateFormatter";
|
|
import { isEmpty } from "lodash";
|
|
import { useTranslation } from "react-i18next";
|
|
import "./job-lifecycle.styles.scss";
|
|
import BlurWrapperComponent from "../feature-wrapper/blur-wrapper.component";
|
|
import UpsellComponent, { upsellEnum } from "../upsell/upsell.component";
|
|
import { HasFeatureAccess } from "../feature-wrapper/feature-wrapper.component";
|
|
import { connect } from "react-redux";
|
|
import { createStructuredSelector } from "reselect";
|
|
import { selectBodyshop } from "../../redux/user/user.selectors";
|
|
|
|
const mapStateToProps = createStructuredSelector({
|
|
bodyshop: selectBodyshop
|
|
});
|
|
|
|
const mapDispatchToProps = () => ({
|
|
//setUserLanguage: language => dispatch(setUserLanguage(language))
|
|
});
|
|
|
|
// show text on bar if text can fit
|
|
export function JobLifecycleComponent({ bodyshop, job, statuses }) {
|
|
const [loading, setLoading] = useState(true);
|
|
const [lifecycleData, setLifecycleData] = useState(null);
|
|
const { t } = useTranslation(); // Used for tracking external state changes.
|
|
const hasLifeCycleAccess = HasFeatureAccess({ bodyshop, featureName: "lifecycle" });
|
|
const { data } = useQuery(
|
|
gql`
|
|
query get_job_test($id: uuid!) {
|
|
jobs_by_pk(id: $id) {
|
|
id
|
|
status
|
|
}
|
|
}
|
|
`,
|
|
{
|
|
variables: {
|
|
id: job.id
|
|
},
|
|
fetchPolicy: "cache-only"
|
|
}
|
|
);
|
|
|
|
/**
|
|
* Gets the lifecycle data for the job.
|
|
* @returns {Promise<void>}
|
|
*/
|
|
const getLifecycleData = useCallback(async () => {
|
|
if (job && job.id && statuses && statuses.statuses) {
|
|
try {
|
|
setLoading(true);
|
|
const response = await axios.post("/job/lifecycle", {
|
|
jobids: job.id,
|
|
statuses: statuses.statuses
|
|
});
|
|
const data = response.data.transition[job.id];
|
|
setLifecycleData(data);
|
|
} catch (err) {
|
|
console.error(`${t("job_lifecycle.errors.fetch")}: ${err.message}`);
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
}
|
|
}, [job, statuses, t]);
|
|
|
|
useEffect(() => {
|
|
if (!data) return;
|
|
setTimeout(() => {
|
|
getLifecycleData().catch((err) => console.error(`${t("job_lifecycle.errors.fetch")}: ${err.message}`));
|
|
}, 500);
|
|
}, [data, getLifecycleData, t]);
|
|
|
|
const columns = [
|
|
{
|
|
title: t("job_lifecycle.columns.value"),
|
|
dataIndex: "value",
|
|
key: "value",
|
|
render: (text) => (
|
|
<BlurWrapperComponent
|
|
featureName="lifecycle"
|
|
bypass
|
|
valueProp="children"
|
|
overrideValueFunction="RandomSmallString:2"
|
|
>
|
|
<span>{text}</span>
|
|
</BlurWrapperComponent>
|
|
)
|
|
},
|
|
{
|
|
title: t("job_lifecycle.columns.start"),
|
|
dataIndex: "start",
|
|
key: "start",
|
|
sorter: (a, b) => dayjs(a.start).unix() - dayjs(b.start).unix(),
|
|
render: (text) => (
|
|
<BlurWrapperComponent featureName="lifecycle" bypass valueProp="children" overrideValueFunction="RandomDate">
|
|
<span>{DateTimeFormatterFunction(text)}</span>
|
|
</BlurWrapperComponent>
|
|
)
|
|
},
|
|
{
|
|
title: t("job_lifecycle.columns.relative_start"),
|
|
dataIndex: "start_readable",
|
|
key: "start_readable"
|
|
},
|
|
{
|
|
title: t("job_lifecycle.columns.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 dayjs(a.end).unix() - dayjs(b.end).unix();
|
|
},
|
|
render: (text) => (
|
|
<BlurWrapperComponent featureName="lifecycle" bypass valueProp="children" overrideValueFunction="RandomDate">
|
|
<span>{isEmpty(text) ? t("job_lifecycle.content.not_available") : DateTimeFormatterFunction(text)}</span>
|
|
</BlurWrapperComponent>
|
|
)
|
|
},
|
|
{
|
|
title: t("job_lifecycle.columns.relative_end"),
|
|
dataIndex: "end_readable",
|
|
key: "end_readable"
|
|
},
|
|
{
|
|
title: t("job_lifecycle.columns.duration"),
|
|
dataIndex: "duration_readable",
|
|
key: "duration_readable",
|
|
sorter: (a, b) => a.duration - b.duration
|
|
}
|
|
];
|
|
|
|
return (
|
|
<Card loading={loading} title={t("job_lifecycle.content.title")}>
|
|
{!loading ? (
|
|
lifecycleData && lifecycleData.lifecycle && lifecycleData.durations ? (
|
|
<Space direction="vertical" style={{ width: "100%" }}>
|
|
<Card
|
|
type="inner"
|
|
title={
|
|
<Space direction="horizontal" size="small">
|
|
<Badge status="processing" count={lifecycleData.durations.totalStatuses} />
|
|
{t("job_lifecycle.content.title_durations")}
|
|
</Space>
|
|
}
|
|
style={{ width: "100%" }}
|
|
>
|
|
{!hasLifeCycleAccess && (
|
|
<Card type="inner" style={{ marginTop: "10px" }}>
|
|
<UpsellComponent upsell={upsellEnum().lifecycle.general} />
|
|
</Card>
|
|
)}
|
|
<BlurWrapperComponent featureName="lifecycle" bypass>
|
|
<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.durations.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>
|
|
</BlurWrapperComponent>
|
|
<Card type="inner" title={t("job_lifecycle.content.legend_title")} style={{ marginTop: "10px" }}>
|
|
<div>
|
|
{lifecycleData.durations.summations.map((key) => (
|
|
<Tag key={key.status} color={key.color} style={{ width: "13vh", padding: "4px", margin: "4px" }}>
|
|
<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",
|
|
textAlign: "center"
|
|
}}
|
|
>
|
|
{key.status} (
|
|
<BlurWrapperComponent
|
|
featureName="lifecycle"
|
|
bypass
|
|
overrideValueFunction="RandomAmount"
|
|
valueProp="children"
|
|
>
|
|
<span>{key.roundedPercentage}</span>
|
|
</BlurWrapperComponent>
|
|
)
|
|
</div>
|
|
</Tag>
|
|
))}
|
|
</div>
|
|
</Card>
|
|
{lifecycleData?.durations?.humanReadableTotal ||
|
|
(lifecycleData.lifecycle[0] &&
|
|
lifecycleData.lifecycle[0].value &&
|
|
lifecycleData?.durations?.totalCurrentStatusDuration?.humanReadable) ? (
|
|
<Card style={{ marginTop: "10px" }}>
|
|
<ul>
|
|
{lifecycleData.durations && lifecycleData.durations.humanReadableTotal && (
|
|
<li>
|
|
<span style={{ fontWeight: "bold" }}>
|
|
{t("job_lifecycle.content.previous_status_accumulated_time")}:
|
|
</span>{" "}
|
|
{lifecycleData.durations.humanReadableTotal}
|
|
</li>
|
|
)}
|
|
{lifecycleData.lifecycle[0] &&
|
|
lifecycleData.lifecycle[0].value &&
|
|
lifecycleData?.durations?.totalCurrentStatusDuration?.humanReadable && (
|
|
<li>
|
|
<span style={{ fontWeight: "bold" }}>
|
|
{t("job_lifecycle.content.current_status_accumulated_time")} (
|
|
{lifecycleData.lifecycle[0].value}):
|
|
</span>
|
|
{lifecycleData.durations.totalCurrentStatusDuration.humanReadable}
|
|
</li>
|
|
)}
|
|
</ul>
|
|
</Card>
|
|
) : null}
|
|
</Card>
|
|
<Card
|
|
type="inner"
|
|
title={
|
|
<>
|
|
<Space direction="horizontal" size="small">
|
|
<Badge status="processing" count={lifecycleData.lifecycle.length} />
|
|
{t("job_lifecycle.content.title_transitions")}
|
|
</Space>
|
|
</>
|
|
}
|
|
>
|
|
<Table
|
|
style={{
|
|
overflow: "auto",
|
|
width: "100%"
|
|
}}
|
|
columns={columns}
|
|
dataSource={lifecycleData.lifecycle}
|
|
rowKey="start"
|
|
/>
|
|
</Card>
|
|
</Space>
|
|
) : (
|
|
<Card type="inner" style={{ textAlign: "center" }}>
|
|
{t("job_lifecycle.content.data_unavailable")}
|
|
</Card>
|
|
)
|
|
) : (
|
|
<Card type="inner" title={t("job_lifecycle.content.title_loading")}>
|
|
{t("job_lifecycle.content.loading")}
|
|
</Card>
|
|
)}
|
|
</Card>
|
|
);
|
|
}
|
|
|
|
export default connect(mapStateToProps, mapDispatchToProps)(JobLifecycleComponent);
|