Skip to content

Commit bce9c5f

Browse files
committed
Improved alert tab, group by alert type
1 parent d9e2902 commit bce9c5f

File tree

1 file changed

+297
-65
lines changed

1 file changed

+297
-65
lines changed

spark-ui/src/tabs/AlertsTab.tsx

Lines changed: 297 additions & 65 deletions
Original file line numberDiff line numberDiff line change
@@ -1,76 +1,308 @@
1-
import { Alert, AlertTitle } from "@mui/material";
1+
import CheckCircleOutlineIcon from "@mui/icons-material/CheckCircleOutline";
2+
import ErrorOutlineIcon from "@mui/icons-material/ErrorOutline";
3+
import ExpandLessIcon from "@mui/icons-material/ExpandLess";
4+
import ExpandMoreIcon from "@mui/icons-material/ExpandMore";
5+
import LaunchIcon from "@mui/icons-material/Launch";
6+
import WarningAmberIcon from "@mui/icons-material/WarningAmber";
7+
import {
8+
Alert,
9+
AlertTitle,
10+
Box,
11+
Button,
12+
Chip,
13+
Collapse,
14+
Divider,
15+
Paper,
16+
Typography,
17+
} from "@mui/material";
218
import { Stack } from "@mui/system";
3-
import React, { FC } from "react";
19+
import React, { FC, useMemo, useState } from "react";
20+
import { useNavigate } from "react-router-dom";
421
import { useAppSelector } from "../Hooks";
522

23+
interface GroupedAlerts {
24+
[alertName: string]: {
25+
title: string;
26+
alerts: any[];
27+
errorCount: number;
28+
warningCount: number;
29+
};
30+
}
31+
632
export const AlertsTab: FC<{}> = (): JSX.Element => {
733
const alerts = useAppSelector((state) => state.spark.alerts);
8-
const errorsCount = alerts?.alerts.filter((alert) => alert.type === "error")
9-
.length;
10-
const warningsCount = alerts?.alerts.filter(
34+
const [expandedGroups, setExpandedGroups] = useState<Set<string>>(new Set());
35+
const navigate = useNavigate();
36+
37+
// Group alerts by name
38+
const groupedAlerts = useMemo(() => {
39+
if (!alerts?.alerts) return {};
40+
41+
const grouped: GroupedAlerts = {};
42+
43+
alerts.alerts.forEach((alert) => {
44+
if (!grouped[alert.name]) {
45+
grouped[alert.name] = {
46+
title: alert.title,
47+
alerts: [],
48+
errorCount: 0,
49+
warningCount: 0,
50+
};
51+
}
52+
53+
grouped[alert.name].alerts.push(alert);
54+
if (alert.type === "error") {
55+
grouped[alert.name].errorCount++;
56+
} else {
57+
grouped[alert.name].warningCount++;
58+
}
59+
});
60+
61+
return grouped;
62+
}, [alerts]);
63+
64+
const toggleGroup = (groupName: string) => {
65+
setExpandedGroups((prev) => {
66+
const newSet = new Set(prev);
67+
if (newSet.has(groupName)) {
68+
newSet.delete(groupName);
69+
} else {
70+
newSet.add(groupName);
71+
}
72+
return newSet;
73+
});
74+
};
75+
76+
const handleGoToAlert = (alert: any) => {
77+
if (alert.source.type === "sql") {
78+
// Navigate to summary with sqlId and nodeId
79+
navigate(`/summary?sqlid=${alert.source.sqlId}&nodeids=${alert.source.sqlNodeId}`);
80+
} else {
81+
// Navigate to resources for status alerts
82+
navigate("/resources");
83+
}
84+
};
85+
86+
const totalErrors = alerts?.alerts.filter(
87+
(alert) => alert.type === "error",
88+
).length;
89+
const totalWarnings = alerts?.alerts.filter(
1190
(alert) => alert.type === "warning",
1291
).length;
1392

93+
if (alerts?.alerts.length === 0) {
94+
return (
95+
<Box
96+
sx={{
97+
height: "100%",
98+
display: "flex",
99+
justifyContent: "center",
100+
alignItems: "center",
101+
}}
102+
>
103+
<Alert severity="success" icon={<CheckCircleOutlineIcon />}>
104+
No alerts 😎
105+
</Alert>
106+
</Box>
107+
);
108+
}
109+
14110
return (
15-
<>
16-
{alerts?.alerts.length === 0 ? (
17-
<div
18-
style={{
19-
height: "100%",
20-
display: "flex",
21-
justifyContent: "center",
22-
alignItems: "center",
23-
}}
24-
>
25-
<Alert severity="success">No alerts 😎</Alert>
26-
</div>
27-
) : (
28-
<div
29-
style={{
30-
display: "flex",
31-
alignItems: "center",
32-
height: "100%",
33-
flexDirection: "column",
34-
}}
35-
>
36-
<div style={{ display: "flex", margin: "15px 0" }}>
37-
<Alert
38-
style={{ margin: "0 10px 0 10px" }}
39-
severity="error"
40-
>{`Errors - ${errorsCount}`}</Alert>
41-
<Alert
42-
style={{ margin: "0 10px 0 10px" }}
43-
severity="warning"
44-
>{`Alerts - ${warningsCount}`}</Alert>
45-
</div>
46-
<div
47-
style={{
48-
overflow: "auto",
49-
height: "100%",
50-
padding: "0 15px 15px 15px",
51-
width: "100%",
52-
}}
53-
>
54-
<Stack
55-
sx={{ width: "100%", whiteSpace: "break-spaces" }}
56-
spacing={2}
57-
>
58-
{alerts?.alerts.map((alert) => {
59-
return (
60-
<Alert key={alert.id} severity={alert.type}>
61-
<AlertTitle>{alert.title}</AlertTitle>
62-
{alert.message}
63-
{"\n"}
64-
{alert.location}
65-
{"\n"}
66-
{`Suggestions: ${alert.suggestion}`}
67-
</Alert>
68-
);
69-
})}
70-
</Stack>
71-
</div>
72-
</div>
73-
)}
74-
</>
111+
<Box
112+
sx={{
113+
height: "100%",
114+
display: "flex",
115+
flexDirection: "column",
116+
overflow: "hidden",
117+
}}
118+
>
119+
{/* Summary Header */}
120+
<Box sx={{ padding: 2, display: "flex", gap: 2 }}>
121+
<Alert severity="error" sx={{ flex: 1 }}>
122+
{`Errors - ${totalErrors}`}
123+
</Alert>
124+
<Alert severity="warning" sx={{ flex: 1 }}>
125+
{`Warnings - ${totalWarnings}`}
126+
</Alert>
127+
</Box>
128+
129+
{/* Alert Groups Section */}
130+
<Box
131+
sx={{
132+
flex: 1,
133+
overflow: "auto",
134+
padding: 2,
135+
}}
136+
>
137+
<Stack spacing={2}>
138+
{Object.entries(groupedAlerts).map(([alertName, groupData]) => {
139+
const isExpanded = expandedGroups.has(alertName);
140+
const totalCount = groupData.errorCount + groupData.warningCount;
141+
142+
return (
143+
<Paper
144+
key={alertName}
145+
elevation={2}
146+
sx={{
147+
overflow: "hidden",
148+
border: "1px solid",
149+
borderColor: "divider",
150+
}}
151+
>
152+
{/* Group Header */}
153+
<Box
154+
onClick={() => toggleGroup(alertName)}
155+
sx={{
156+
cursor: "pointer",
157+
padding: 2,
158+
display: "flex",
159+
justifyContent: "space-between",
160+
alignItems: "center",
161+
backgroundColor: "background.paper",
162+
"&:hover": {
163+
backgroundColor: "action.hover",
164+
},
165+
}}
166+
>
167+
<Box sx={{ display: "flex", alignItems: "center", gap: 2 }}>
168+
{groupData.errorCount > 0 ? (
169+
<ErrorOutlineIcon color="error" sx={{ fontSize: 28 }} />
170+
) : (
171+
<WarningAmberIcon color="warning" sx={{ fontSize: 28 }} />
172+
)}
173+
<Box>
174+
<Typography variant="h6" fontWeight="600">
175+
{groupData.title}
176+
</Typography>
177+
<Typography
178+
variant="caption"
179+
color="text.secondary"
180+
sx={{ fontStyle: "italic" }}
181+
>
182+
{alertName}
183+
</Typography>
184+
</Box>
185+
</Box>
186+
187+
<Box sx={{ display: "flex", alignItems: "center", gap: 1.5 }}>
188+
{groupData.errorCount > 0 && (
189+
<Chip
190+
size="small"
191+
label={`${groupData.errorCount} error${groupData.errorCount !== 1 ? "s" : ""
192+
}`}
193+
color="error"
194+
variant="outlined"
195+
/>
196+
)}
197+
{groupData.warningCount > 0 && (
198+
<Chip
199+
size="small"
200+
label={`${groupData.warningCount} warning${groupData.warningCount !== 1 ? "s" : ""
201+
}`}
202+
color="warning"
203+
variant="outlined"
204+
/>
205+
)}
206+
{isExpanded ? (
207+
<ExpandLessIcon sx={{ fontSize: 28 }} />
208+
) : (
209+
<ExpandMoreIcon sx={{ fontSize: 28 }} />
210+
)}
211+
</Box>
212+
</Box>
213+
214+
{/* Expanded Alert Details */}
215+
<Collapse in={isExpanded} timeout="auto" unmountOnExit>
216+
<Divider />
217+
<Box sx={{ padding: 2, backgroundColor: "background.default" }}>
218+
<Stack spacing={2}>
219+
{groupData.alerts.map((alert) => (
220+
<Alert
221+
key={alert.id}
222+
severity={alert.type}
223+
sx={{
224+
"& .MuiAlert-message": {
225+
width: "100%",
226+
},
227+
}}
228+
>
229+
<AlertTitle sx={{ fontWeight: "600" }}>
230+
{alert.title}
231+
</AlertTitle>
232+
<Typography
233+
variant="body2"
234+
sx={{
235+
whiteSpace: "pre-wrap",
236+
mb: 1,
237+
}}
238+
>
239+
{alert.message}
240+
</Typography>
241+
<Divider sx={{ my: 1 }} />
242+
<Typography
243+
variant="body2"
244+
color="text.secondary"
245+
sx={{
246+
fontStyle: "italic",
247+
mb: 1,
248+
}}
249+
>
250+
📍 {alert.location}
251+
</Typography>
252+
<Box
253+
sx={{
254+
backgroundColor: "action.hover",
255+
padding: 1.5,
256+
borderRadius: 1,
257+
borderLeft: "3px solid",
258+
borderColor:
259+
alert.type === "error"
260+
? "error.main"
261+
: "warning.main",
262+
mb: 1.5,
263+
}}
264+
>
265+
<Typography
266+
variant="body2"
267+
fontWeight="600"
268+
gutterBottom
269+
>
270+
💡 Suggestions:
271+
</Typography>
272+
<Typography
273+
variant="body2"
274+
sx={{
275+
whiteSpace: "pre-wrap",
276+
}}
277+
>
278+
{alert.suggestion}
279+
</Typography>
280+
</Box>
281+
<Button
282+
variant="contained"
283+
size="small"
284+
startIcon={<LaunchIcon />}
285+
onClick={(e) => {
286+
e.stopPropagation();
287+
handleGoToAlert(alert);
288+
}}
289+
sx={{
290+
textTransform: "none",
291+
fontWeight: "600",
292+
}}
293+
>
294+
Go to Alert
295+
</Button>
296+
</Alert>
297+
))}
298+
</Stack>
299+
</Box>
300+
</Collapse>
301+
</Paper>
302+
);
303+
})}
304+
</Stack>
305+
</Box>
306+
</Box>
75307
);
76308
};

0 commit comments

Comments
 (0)