|
1 |
| -import React from "react"; |
2 |
| -import { Box, Typography, Grid } from "@mui/material"; |
3 |
| -import { Doughnut } from "react-chartjs-2"; |
4 |
| -import { Chart as ChartJS, ArcElement, Tooltip, Legend, Title } from "chart.js"; |
| 1 | +import React from 'react'; |
| 2 | +import { Box, Card, CardContent, Typography, Chip, Paper, Fade, alpha } from '@mui/material'; |
| 3 | +import { PieChart, Pie, Cell, ResponsiveContainer, Tooltip } from 'recharts'; |
5 | 4 |
|
6 |
| -ChartJS.register(ArcElement, Tooltip, Legend, Title); |
7 |
| - |
8 |
| -const JobStatusPieChart = ({ data }) => { |
9 |
| - if (!data || !Array.isArray(data)) { |
10 |
| - return ( |
11 |
| - <Box sx={{ height: 300, width: "100%", position: "relative" }}> |
12 |
| - <Typography>No data available</Typography> |
13 |
| - </Box> |
14 |
| - ); |
15 |
| - } |
16 |
| - |
17 |
| - const statusCounts = data.reduce( |
18 |
| - (acc, job) => { |
19 |
| - const status = job.status; |
20 |
| - if (status?.succeeded) { |
21 |
| - acc.Completed++; |
22 |
| - } else if ( |
23 |
| - status?.state?.phase === "Running" || |
24 |
| - status?.state?.phase === "Pending" |
25 |
| - ) { |
26 |
| - acc.Running++; |
27 |
| - } else if (status?.state?.phase === "Failed") { |
28 |
| - acc.Failed++; |
29 |
| - } |
30 |
| - return acc; |
31 |
| - }, |
32 |
| - { |
33 |
| - Completed: 0, |
34 |
| - Running: 0, |
35 |
| - Failed: 0, |
36 |
| - }, |
37 |
| - ); |
38 |
| - |
39 |
| - const total = Object.values(statusCounts).reduce((a, b) => a + b, 0); |
40 |
| - const colors = { |
41 |
| - Completed: "#4caf50", |
42 |
| - Running: "#2196f3", |
43 |
| - Failed: "#f44336", |
44 |
| - }; |
45 |
| - |
46 |
| - const chartData = { |
47 |
| - labels: Object.keys(statusCounts), |
48 |
| - datasets: [ |
49 |
| - { |
50 |
| - data: Object.values(statusCounts), |
51 |
| - backgroundColor: Object.values(colors), |
52 |
| - borderColor: "white", |
53 |
| - borderWidth: 2, |
54 |
| - hoverBorderColor: "white", |
55 |
| - hoverBorderWidth: 3, |
56 |
| - }, |
57 |
| - ], |
58 |
| - }; |
59 |
| - |
60 |
| - const options = { |
61 |
| - responsive: true, |
62 |
| - maintainAspectRatio: false, |
63 |
| - cutout: "70%", |
64 |
| - plugins: { |
65 |
| - legend: { |
66 |
| - display: false, |
67 |
| - }, |
68 |
| - tooltip: { |
69 |
| - enabled: false, |
70 |
| - }, |
71 |
| - }, |
| 5 | +const JobStatusPieChart = ({ jobStatusData, colors }) => { |
| 6 | + const CustomTooltip = ({ active, payload }) => { |
| 7 | + if (active && payload && payload.length) { |
| 8 | + const data = payload[0].payload; |
| 9 | + return ( |
| 10 | + <Paper sx={{ |
| 11 | + p: 2, |
| 12 | + bgcolor: colors.white, |
| 13 | + boxShadow: '0 8px 32px rgba(0,0,0,0.12)', |
| 14 | + border: '1px solid rgba(0,0,0,0.1)', |
| 15 | + borderRadius: 2 |
| 16 | + }}> |
| 17 | + <Typography variant="body2" fontWeight="bold" sx={{ color: data.color }}> |
| 18 | + {data.name}: {data.value} jobs |
| 19 | + </Typography> |
| 20 | + </Paper> |
| 21 | + ); |
| 22 | + } |
| 23 | + return null; |
72 | 24 | };
|
73 | 25 |
|
74 |
| - const hasData = Object.values(statusCounts).some((count) => count > 0); |
75 |
| - |
76 | 26 | return (
|
77 |
| - <Box |
78 |
| - sx={{ |
79 |
| - height: "100%", |
80 |
| - display: "flex", |
81 |
| - flexDirection: "column", |
82 |
| - }} |
83 |
| - > |
84 |
| - <Typography variant="h6" align="center" sx={{ mb: 1 }}> |
85 |
| - Jobs Status |
86 |
| - </Typography> |
87 |
| - |
88 |
| - <Box |
89 |
| - sx={{ |
90 |
| - display: "flex", |
91 |
| - flexDirection: "row", |
92 |
| - justifyContent: "space-between", |
93 |
| - alignItems: "flex-start", |
94 |
| - flexWrap: "wrap", |
95 |
| - gap: 2, |
96 |
| - width: "100%", |
97 |
| - }} |
98 |
| - > |
99 |
| - <Box |
100 |
| - sx={{ |
101 |
| - flex: 1, |
102 |
| - minWidth: "250px", // Ensure that the chart is not too small |
103 |
| - height: "300px", // Fixed height to adapt to various screens |
104 |
| - position: "relative", |
105 |
| - display: "flex", |
106 |
| - alignItems: "center", |
107 |
| - justifyContent: "center", |
108 |
| - }} |
109 |
| - > |
110 |
| - <Doughnut |
111 |
| - data={chartData} |
112 |
| - options={{ |
113 |
| - ...options, |
114 |
| - maintainAspectRatio: false, |
115 |
| - }} |
116 |
| - /> |
117 |
| - <Typography |
118 |
| - variant="h4" |
119 |
| - sx={{ |
120 |
| - position: "absolute", |
121 |
| - top: "50%", |
122 |
| - left: "50%", |
123 |
| - transform: "translate(-50%, -50%)", |
124 |
| - textAlign: "center", |
125 |
| - }} |
126 |
| - > |
127 |
| - {total} |
| 27 | + <Fade in={true} timeout={1000}> |
| 28 | + <Card sx={{ |
| 29 | + height: 400, |
| 30 | + bgcolor: colors.white, |
| 31 | + borderRadius: 3, |
| 32 | + boxShadow: '0 4px 20px rgba(0,0,0,0.08)', |
| 33 | + border: `1px solid ${alpha(colors.primary, 0.1)}` |
| 34 | + }}> |
| 35 | + <CardContent sx={{ p: 4 }}> |
| 36 | + <Typography variant="h6" fontWeight="bold" gutterBottom sx={{ color: colors.secondary, textAlign: 'center' }}> |
| 37 | + Job Status Distribution |
128 | 38 | </Typography>
|
129 |
| - </Box> |
130 |
| - |
131 |
| - <Box |
132 |
| - sx={{ |
133 |
| - flex: 1, |
134 |
| - minWidth: "250px", |
135 |
| - display: "flex", |
136 |
| - flexDirection: "column", |
137 |
| - justifyContent: "flex-start", |
138 |
| - alignItems: "flex-start", |
139 |
| - textAlign: "left", |
140 |
| - }} |
141 |
| - > |
142 |
| - {Object.entries(statusCounts).map(([status, count]) => ( |
143 |
| - <Box |
144 |
| - key={status} |
145 |
| - sx={{ |
146 |
| - display: "flex", |
147 |
| - alignItems: "center", |
148 |
| - mb: 1.5, |
149 |
| - }} |
150 |
| - > |
151 |
| - <Box |
152 |
| - sx={{ |
153 |
| - width: 12, |
154 |
| - height: 12, |
155 |
| - borderRadius: "50%", |
156 |
| - backgroundColor: colors[status], |
157 |
| - mr: 1, |
158 |
| - }} |
159 |
| - /> |
160 |
| - <Typography |
161 |
| - variant="body2" |
162 |
| - sx={{ mr: 2, minWidth: 70 }} |
163 |
| - > |
164 |
| - {status} |
165 |
| - </Typography> |
166 |
| - <Typography variant="body2" color="text.secondary"> |
167 |
| - {count} ( |
168 |
| - {total > 0 |
169 |
| - ? ((count / total) * 100).toFixed(1) |
170 |
| - : 0} |
171 |
| - %) |
| 39 | + {jobStatusData.length > 0 ? ( |
| 40 | + <Box display="flex" flexDirection="column" alignItems="center" height="100%"> |
| 41 | + <ResponsiveContainer width="100%" height={240}> |
| 42 | + <PieChart> |
| 43 | + <Pie |
| 44 | + data={jobStatusData} |
| 45 | + cx="50%" |
| 46 | + cy="50%" |
| 47 | + outerRadius={80} |
| 48 | + innerRadius={35} |
| 49 | + paddingAngle={3} |
| 50 | + dataKey="value" |
| 51 | + > |
| 52 | + {jobStatusData.map((entry, index) => ( |
| 53 | + <Cell |
| 54 | + key={`cell-${index}`} |
| 55 | + fill={entry.color} |
| 56 | + stroke={colors.white} |
| 57 | + strokeWidth={2} |
| 58 | + /> |
| 59 | + ))} |
| 60 | + </Pie> |
| 61 | + <Tooltip content={<CustomTooltip />} /> |
| 62 | + </PieChart> |
| 63 | + </ResponsiveContainer> |
| 64 | + |
| 65 | + {/* Centered Legend */} |
| 66 | + <Box display="flex" justifyContent="center" gap={2} mt={1}> |
| 67 | + {jobStatusData.map((item) => ( |
| 68 | + <Chip |
| 69 | + key={item.name} |
| 70 | + label={`${item.name}: ${item.value}`} |
| 71 | + sx={{ |
| 72 | + bgcolor: alpha(item.color, 0.1), |
| 73 | + color: item.color, |
| 74 | + fontWeight: 500, |
| 75 | + border: `1px solid ${alpha(item.color, 0.2)}` |
| 76 | + }} |
| 77 | + variant="outlined" |
| 78 | + /> |
| 79 | + ))} |
| 80 | + </Box> |
| 81 | + </Box> |
| 82 | + ) : ( |
| 83 | + <Box display="flex" alignItems="center" justifyContent="center" height={280}> |
| 84 | + <Typography color="text.secondary" variant="body1"> |
| 85 | + No job data available |
172 | 86 | </Typography>
|
173 | 87 | </Box>
|
174 |
| - ))} |
175 |
| - </Box> |
176 |
| - </Box> |
177 |
| - </Box> |
| 88 | + )} |
| 89 | + </CardContent> |
| 90 | + </Card> |
| 91 | + </Fade> |
178 | 92 | );
|
179 | 93 | };
|
180 | 94 |
|
|
0 commit comments