Skip to content

Commit 69173e3

Browse files
Merge pull request #184 from vzhou-p/feature/pod-group
add PodGroup dashboard support
2 parents 20baf5d + d2f8338 commit 69173e3

File tree

13 files changed

+3893
-985
lines changed

13 files changed

+3893
-985
lines changed

backend/src/server.js

Lines changed: 122 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -137,6 +137,116 @@ app.get("/api/job/:namespace/:name/yaml", async (req, res) => {
137137
}
138138
});
139139

140+
// Get all PodGroups
141+
app.get("/api/podgroups", async (req, res) => {
142+
try {
143+
const namespace = req.query.namespace || "";
144+
const searchTerm = req.query.search || "";
145+
const statusFilter = req.query.status || "";
146+
147+
console.log("Fetching podgroups with params:", {
148+
namespace,
149+
searchTerm,
150+
statusFilter,
151+
});
152+
153+
let response;
154+
if (namespace === "" || namespace === "All") {
155+
response = await k8sApi.listClusterCustomObject({
156+
group: "scheduling.volcano.sh",
157+
version: "v1beta1",
158+
plural: "podgroups",
159+
});
160+
} else {
161+
response = await k8sApi.listNamespacedCustomObject({
162+
group: "scheduling.volcano.sh",
163+
version: "v1beta1",
164+
namespace,
165+
plural: "podgroups",
166+
});
167+
}
168+
169+
// k8sApi (CustomObjectsApi) v1.2.0 ObjectParamAPI returns the body directly
170+
let filteredPodGroups = response.items || [];
171+
172+
if (searchTerm) {
173+
filteredPodGroups = filteredPodGroups.filter((pg) =>
174+
pg.metadata.name
175+
.toLowerCase()
176+
.includes(searchTerm.toLowerCase()),
177+
);
178+
}
179+
180+
if (statusFilter && statusFilter !== "All") {
181+
filteredPodGroups = filteredPodGroups.filter(
182+
(pg) => pg.status?.phase === statusFilter,
183+
);
184+
}
185+
186+
res.json({
187+
items: filteredPodGroups,
188+
totalCount: filteredPodGroups.length,
189+
});
190+
} catch (err) {
191+
console.error("Error fetching podgroups:", err);
192+
res.status(500).json({
193+
error: "Failed to fetch podgroups",
194+
details: err.message,
195+
});
196+
}
197+
});
198+
199+
// Get details of a specific PodGroup
200+
app.get("/api/podgroups/:namespace/:name", async (req, res) => {
201+
try {
202+
const { namespace, name } = req.params;
203+
const response = await k8sApi.getNamespacedCustomObject({
204+
group: "scheduling.volcano.sh",
205+
version: "v1beta1",
206+
namespace,
207+
plural: "podgroups",
208+
name,
209+
});
210+
res.json(response);
211+
} catch (err) {
212+
console.error("Error fetching podgroup:", err);
213+
res.status(500).json({
214+
error: "Failed to fetch podgroup",
215+
details: err.message,
216+
});
217+
}
218+
});
219+
220+
// Get YAML of a specific PodGroup
221+
app.get("/api/podgroups/:namespace/:name/yaml", async (req, res) => {
222+
try {
223+
const { namespace, name } = req.params;
224+
const response = await k8sApi.getNamespacedCustomObject({
225+
group: "scheduling.volcano.sh",
226+
version: "v1beta1",
227+
namespace,
228+
plural: "podgroups",
229+
name,
230+
});
231+
232+
const formattedYaml = yaml.dump(response, {
233+
indent: 2,
234+
lineWidth: -1,
235+
noRefs: true,
236+
sortKeys: false,
237+
});
238+
239+
res.setHeader("Content-Type", "text/yaml");
240+
res.send(formattedYaml);
241+
} catch (error) {
242+
console.error("Error fetching podgroup YAML:", error);
243+
res.status(500).json({
244+
error: "Failed to fetch podgroup YAML",
245+
details: error.message,
246+
});
247+
}
248+
});
249+
140250
// Get details of a specific Queue
141251
app.get("/api/queues/:name", async (req, res) => {
142252
const name = req.params.name;
@@ -516,18 +626,15 @@ app.patch("/api/jobs/:namespace/:name", async (req, res) => {
516626
headers: { "Content-Type": "application/merge-patch+json" },
517627
};
518628

519-
const response = await k8sApi.patchNamespacedCustomObject(
520-
"batch.volcano.sh",
521-
"v1alpha1",
629+
const response = await k8sApi.patchNamespacedCustomObject({
630+
group: "batch.volcano.sh",
631+
version: "v1alpha1",
522632
namespace,
523-
"jobs",
633+
plural: "jobs",
524634
name,
525-
patchData,
526-
undefined,
527-
undefined,
528-
undefined,
635+
body: patchData,
529636
options,
530-
);
637+
});
531638

532639
res.json({ message: "Job updated successfully", data: response.body });
533640
} catch (error) {
@@ -544,18 +651,15 @@ app.patch("/api/queues/:namespace/:name", async (req, res) => {
544651
headers: { "Content-Type": "application/merge-patch+json" },
545652
};
546653

547-
const response = await k8sApi.patchNamespacedCustomObject(
548-
"scheduling.volcano.sh",
549-
"v1alpha1",
654+
const response = await k8sApi.patchNamespacedCustomObject({
655+
group: "scheduling.volcano.sh",
656+
version: "v1alpha1",
550657
namespace,
551-
"queues",
658+
plural: "queues",
552659
name,
553-
patchData,
554-
undefined,
555-
undefined,
556-
undefined,
660+
body: patchData,
557661
options,
558-
);
662+
});
559663

560664
res.json({
561665
message: "Queue updated successfully",

backend/tests/server.test.js

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,4 +40,42 @@ describe("backend", () => {
4040
expect(res.body.items).toHaveLength(2);
4141
expect(res.body.items[0].metadata.name).toBe("job1");
4242
});
43+
44+
it("should fetch podgroups", async () => {
45+
const mockResponse = {
46+
items: [
47+
{
48+
metadata: { name: "pg1", namespace: "default" },
49+
status: { phase: "Running" },
50+
},
51+
{
52+
metadata: { name: "pg2", namespace: "default" },
53+
status: { phase: "Pending" },
54+
},
55+
],
56+
};
57+
58+
const stub = sandbox.stub(
59+
CustomObjectsApi.prototype,
60+
"listClusterCustomObject",
61+
);
62+
63+
// Updated for OBJECT arguments using sinon matchers
64+
stub.withArgs(
65+
sinon.match({
66+
group: "scheduling.volcano.sh",
67+
version: "v1beta1",
68+
plural: "podgroups",
69+
}),
70+
).resolves(mockResponse);
71+
72+
// Fallback for others
73+
stub.resolves({ items: [] });
74+
75+
const res = await request(app).get("/api/podgroups");
76+
77+
expect(res.status).toBe(200);
78+
expect(res.body.items).toHaveLength(2);
79+
expect(res.body.items[0].metadata.name).toBe("pg1");
80+
});
4381
});

frontend/src/App.jsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import Dashboard from "./components/dashboard/Dashboard";
1010
import Jobs from "./components/jobs/Jobs";
1111
import Queues from "./components/queues/Queues";
1212
import Pods from "./components/pods/Pods";
13+
import PodGroups from "./components/podgroups/PodGroups";
1314
import { ThemeProvider } from "@mui/material/styles";
1415
import { theme } from "./theme";
1516
import "bootstrap/dist/css/bootstrap.min.css";
@@ -28,6 +29,7 @@ function App() {
2829
<Route path="jobs" element={<Jobs />} />
2930
<Route path="queues" element={<Queues />} />
3031
<Route path="pods" element={<Pods />} />
32+
<Route path="podgroups" element={<PodGroups />} />
3133
</Route>
3234
</Routes>
3335
</Router>

frontend/src/components/Layout.jsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import CloudIcon from "@mui/icons-material/Cloud";
1818
import HomeIcon from "@mui/icons-material/Home";
1919
import AssignmentIcon from "@mui/icons-material/Assignment";
2020
import WorkspacesIcon from "@mui/icons-material/Workspaces";
21+
import CategoryIcon from "@mui/icons-material/Category";
2122

2223
// use relative path to load Logo
2324
import volcanoLogo from "../assets/volcano-icon-color.svg";
@@ -41,6 +42,7 @@ const Layout = () => {
4142
{ text: "Jobs", icon: <AssignmentIcon />, path: "/jobs" },
4243
{ text: "Queues", icon: <CloudIcon />, path: "/queues" },
4344
{ text: "Pods", icon: <WorkspacesIcon />, path: "/pods" },
45+
{ text: "PodGroups", icon: <CategoryIcon />, path: "/podgroups" },
4446
];
4547

4648
return (
Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
import React from "react";
2+
import {
3+
Box,
4+
Button,
5+
Dialog,
6+
DialogActions,
7+
DialogContent,
8+
DialogTitle,
9+
} from "@mui/material";
10+
11+
const PodGroupDialog = ({ open, handleClose, selectedName, selectedYaml }) => {
12+
return (
13+
<Dialog
14+
open={open}
15+
onClose={handleClose}
16+
maxWidth={false}
17+
fullWidth
18+
PaperProps={{
19+
sx: {
20+
width: "80%",
21+
maxWidth: "800px",
22+
maxHeight: "90vh",
23+
m: 2,
24+
bgcolor: "background.paper",
25+
},
26+
}}
27+
>
28+
<DialogTitle>PodGroup YAML - {selectedName}</DialogTitle>
29+
<DialogContent>
30+
<Box
31+
sx={{
32+
mt: 2,
33+
mb: 2,
34+
fontFamily: "monospace",
35+
fontSize: "1.2rem",
36+
whiteSpace: "pre-wrap",
37+
overflow: "auto",
38+
maxHeight: "calc(90vh - 150px)",
39+
bgcolor: "grey.50",
40+
p: 2,
41+
borderRadius: 1,
42+
"& .yaml-key": {
43+
fontWeight: 700,
44+
color: "#000",
45+
},
46+
}}
47+
>
48+
<pre
49+
dangerouslySetInnerHTML={{
50+
__html: selectedYaml,
51+
}}
52+
/>
53+
</Box>
54+
</DialogContent>
55+
<DialogActions>
56+
<Box
57+
sx={{
58+
display: "flex",
59+
justifyContent: "flex-end",
60+
mt: 2,
61+
width: "100%",
62+
px: 2,
63+
pb: 2,
64+
}}
65+
>
66+
<Button
67+
variant="contained"
68+
color="primary"
69+
onClick={handleClose}
70+
sx={{
71+
minWidth: "100px",
72+
"&:hover": {
73+
bgcolor: "primary.dark",
74+
},
75+
}}
76+
>
77+
Close
78+
</Button>
79+
</Box>
80+
</DialogActions>
81+
</Dialog>
82+
);
83+
};
84+
85+
export default PodGroupDialog;

0 commit comments

Comments
 (0)