Merge 4dbfd5b514
into 08a0f8f1d2
This commit is contained in:
commit
78900ff969
|
@ -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 || "";
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
@ -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 && (
|
||||
|
|
|
@ -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 && (
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
|
|
|
@ -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 && (
|
||||
|
|
|
@ -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;
|
||||
};
|
|
@ -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;
|
Loading…
Reference in New Issue