This commit is contained in:
Karan Yadav 2025-05-26 02:34:20 -04:00 committed by GitHub
commit 78900ff969
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 259 additions and 19 deletions

View File

@ -4,6 +4,7 @@ import {
CoreV1Api,
CustomObjectsApi,
KubeConfig,
Watch,
} from "@kubernetes/client-node";
import yaml from "js-yaml";
@ -16,6 +17,87 @@ kc.loadFromDefault();
const k8sApi = kc.makeApiClient(CustomObjectsApi);
const k8sCoreApi = kc.makeApiClient(CoreV1Api);
const watcher = new Watch(kc);
app.get(`/api/get-update-events`, (req, res) => {
res.setHeader("Content-Type", "text/event-stream");
res.setHeader("Cache-Control", "no-cache");
res.setHeader("Connection", "keep-alive");
watcher.watch(
"/api/v1/pods",
{},
(phase, pod) => {
res.write(
`data: ${JSON.stringify({ phase, type: "pod", data: pod })}\n\n`,
);
},
(err) => {
if (err) {
console.log("Error watching pods:", err);
} else {
console.log("Pod watcher started successfully");
}
},
);
watcher.watch(
"/api/v1/namespaces",
{},
(phase, namespace) => {
res.write(
`data: ${JSON.stringify({ phase, type: "namespace", data: namespace.metadata.name })}\n\n`,
);
},
(err) => {
if (err) {
console.log("Error watching namespaces:", err);
} else {
console.log("Namespace watcher started successfully");
}
},
);
watcher.watch(
"/apis/batch.volcano.sh/v1alpha1/jobs",
{},
(phase, job) => {
res.write(
`data: ${JSON.stringify({ phase, type: "job", data: job })}\n\n`,
);
},
(err) => {
if (err) {
console.log("Error watching jobs:", err);
} else {
console.log("Job watcher started successfully");
}
},
);
watcher.watch(
"/apis/scheduling.volcano.sh/v1beta1/queues",
{},
(phase, queue) => {
res.write(
`data: ${JSON.stringify({ phase, type: "queue", data: queue })}\n\n`,
);
},
(err) => {
if (err) {
console.log("Error watching queues:", err);
} else {
console.log("Queue watcher started successfully");
}
},
);
req.on("close", () => {
console.log("Client disconnected, stopping watchers");
res.end();
});
});
app.get("/api/jobs", async (req, res) => {
try {
const namespace = req.query.namespace || "";

View File

@ -1,4 +1,3 @@
import React from "react";
import {
BrowserRouter as Router,
Route,
@ -13,25 +12,28 @@ import Pods from "./components/pods/Pods";
import { ThemeProvider } from "@mui/material/styles";
import { theme } from "./theme";
import "bootstrap/dist/css/bootstrap.min.css";
import EventProvider from "./contexts/EventProvider";
function App() {
return (
<ThemeProvider theme={theme}>
<Router>
<Routes>
<Route path="/" element={<Layout />}>
<Route
index
element={<Navigate to="/dashboard" replace />}
/>
<Route path="dashboard" element={<Dashboard />} />
<Route path="jobs" element={<Jobs />} />
<Route path="queues" element={<Queues />} />
<Route path="pods" element={<Pods />} />
</Route>
</Routes>
</Router>
</ThemeProvider>
<EventProvider>
<ThemeProvider theme={theme}>
<Router>
<Routes>
<Route path="/" element={<Layout />}>
<Route
index
element={<Navigate to="/dashboard" replace />}
/>
<Route path="dashboard" element={<Dashboard />} />
<Route path="jobs" element={<Jobs />} />
<Route path="queues" element={<Queues />} />
<Route path="pods" element={<Pods />} />
</Route>
</Routes>
</Router>
</ThemeProvider>
</EventProvider>
);
}

View File

@ -7,6 +7,7 @@ import JobTable from "./JobTable/JobTable";
import JobPagination from "./JobPagination";
import JobDialog from "./JobDialog";
import SearchBar from "../Searchbar";
import { useEvent } from "../../contexts/EventContext";
const Jobs = () => {
const [jobs, setJobs] = useState([]);
@ -37,6 +38,8 @@ const Jobs = () => {
const [totalJobs, setTotalJobs] = useState(0);
const [sortDirection, setSortDirection] = useState("");
const { onUpdateEvent } = useEvent();
const fetchJobs = useCallback(async () => {
setLoading(true);
setError(null);
@ -186,6 +189,27 @@ const Jobs = () => {
setSortDirection((prev) => (prev === "asc" ? "desc" : "asc"));
}, []);
useEffect(() => {
const handleJobUpdate = (obj) => {
if (obj.type === "job") {
const jobData = obj.data;
setCachedJobs((prevJobs) => {
const jobs = prevJobs.filter(
(job) => job.metadata.name !== jobData.metadata.name,
);
if (obj.phase === "DELETED") {
return jobs;
} else {
return [...jobs, jobData];
}
});
}
};
onUpdateEvent(handleJobUpdate);
}, []);
return (
<Box sx={{ bgcolor: "background.default", minHeight: "100vh", p: 3 }}>
{error && (

View File

@ -7,6 +7,7 @@ import { fetchAllNamespaces } from "../utils";
import PodsTable from "./PodsTable/PodsTable";
import PodsPagination from "./PodsPagination";
import PodDetailsDialog from "./PodDetailsDialog";
import { useEvent } from "../../contexts/EventContext";
const Pods = () => {
const [pods, setPods] = useState([]);
@ -29,6 +30,7 @@ const Pods = () => {
});
const [totalPods, setTotalPods] = useState(0);
const [sortDirection, setSortDirection] = useState("");
const { onUpdateEvent } = useEvent();
const fetchPods = useCallback(async () => {
setLoading(true);
@ -139,6 +141,38 @@ const Pods = () => {
setSortDirection((prev) => (prev === "asc" ? "desc" : "asc"));
}, []);
useEffect(() => {
const handlePodUpdate = (obj) => {
if (obj.type === "pod") {
const podData = obj.data;
setCachedPods((prevPods) => {
const pods = prevPods.filter(
(pod) => pod.metadata.name !== podData.metadata.name,
);
if (obj.phase === "DELETED") {
return pods;
} else {
return [...pods, podData];
}
});
} else if (obj.type === "namespace") {
const namespace = obj.data;
setAllNamespaces((prevNamespaces) => {
if (obj.phase === "DELETED") {
return prevNamespaces.filter((ns) => ns !== namespace);
} else if (!prevNamespaces.includes(namespace)) {
return [...prevNamespaces, namespace];
}
return prevNamespaces;
});
}
};
onUpdateEvent(handlePodUpdate);
}, []);
return (
<Box sx={{ bgcolor: "background.default", minHeight: "100vh", p: 3 }}>
{error && (

View File

@ -1,9 +1,22 @@
import React from "react";
import React, { useEffect, useState } from "react";
import { TableRow, TableCell, Chip, useTheme, alpha } from "@mui/material";
import { calculateAge } from "../../utils";
const PodRow = ({ pod, getStatusColor, onPodClick }) => {
const theme = useTheme();
const [podAge, setPodAge] = useState(
calculateAge(pod.metadata.creationTimestamp),
);
useEffect(() => {
var intervalId = setInterval(() => {
setPodAge(calculateAge(pod.metadata.creationTimestamp));
}, 1000);
return () => {
clearInterval(intervalId);
};
}, []);
return (
<TableRow
@ -90,7 +103,7 @@ const PodRow = ({ pod, getStatusColor, onPodClick }) => {
fontWeight: 500,
}}
>
{calculateAge(pod.metadata.creationTimestamp)}
{podAge}
</TableCell>
</TableRow>
);

View File

@ -7,6 +7,7 @@ import QueueTable from "./QueueTable/QueueTable";
import QueuePagination from "./QueuePagination";
import QueueYamlDialog from "./QueueYamlDialog";
import TitleComponent from "../Titlecomponent";
import { useEvent } from "../../contexts/EventContext";
const Queues = () => {
const [queues, setQueues] = useState([]);
@ -24,6 +25,7 @@ const Queues = () => {
field: null,
direction: "asc",
});
const { onUpdateEvent } = useEvent();
const fetchQueues = useCallback(async () => {
setLoading(true);
@ -207,6 +209,27 @@ const Queues = () => {
return Array.from(fields).sort();
}, [queues]);
useEffect(() => {
const handleQueueUpdate = (obj) => {
if (obj.type === "queue") {
const qData = obj.data;
setQueues((prevQueues) => {
const queues = prevQueues.filter(
(q) => q.metadata.name !== qData.metadata.name,
);
if (obj.phase === "DELETED") {
return queues;
} else {
return [...queues, qData];
}
});
}
};
onUpdateEvent(handleQueueUpdate);
}, []);
return (
<Box sx={{ bgcolor: "background.default", minHeight: "100vh", p: 3 }}>
{error && (

View File

@ -0,0 +1,11 @@
import { createContext, useContext } from "react";
export const EventContext = createContext(null);
export const useEvent = () => {
const context = useContext(EventContext);
if (!context) {
throw new Error("useEventContext must be used within an EventProvider");
}
return context;
};

View File

@ -0,0 +1,51 @@
import { useCallback, useEffect, useRef } from "react";
import { EventContext } from "./EventContext";
const EventProvider = ({ children }) => {
const esRef = useRef(null);
const updateCallbackRef = useRef(null);
const onUpdateEvent = useCallback((callback) => {
updateCallbackRef.current = callback;
}, []);
const getUpdateEvents = () => {
try {
const es = new EventSource("/api/get-update-events");
es.onopen = () => {
esRef.current = es;
};
es.onmessage = (event) => {
const obj = JSON.parse(event.data);
if (updateCallbackRef.current) {
updateCallbackRef.current(obj);
}
};
es.onerror = (err) => {
console.error("EventSource error:", err);
};
} catch (err) {
console.error("Error fetching update events:", err);
}
};
useEffect(() => {
getUpdateEvents();
return () => {
if (esRef.current) {
esRef.current.close();
}
esRef.current = null;
updateCallbackRef.current = null;
};
}, []);
return (
<EventContext.Provider value={{ onUpdateEvent }}>
{children}
</EventContext.Provider>
);
};
export default EventProvider;