Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
156 changes: 150 additions & 6 deletions backend/src/server.js
Original file line number Diff line number Diff line change
Expand Up @@ -277,21 +277,34 @@ app.get("/api/pods", async (req, res) => {

let filteredPods = response.items || [];

// Apply search filter
// Improved search filter to search in both name and namespace
if (searchTerm) {
filteredPods = filteredPods.filter((pod) =>
pod.metadata.name
.toLowerCase()
.includes(searchTerm.toLowerCase()),
);
const searchLower = searchTerm.toLowerCase();
filteredPods = filteredPods.filter((pod) => {
const podName = pod.metadata.name.toLowerCase();
const podNamespace = pod.metadata.namespace.toLowerCase();
return (
podName.includes(searchLower) ||
podNamespace.includes(searchLower)
);
});
}

// Status filter
if (statusFilter && statusFilter !== "All") {
filteredPods = filteredPods.filter(
(pod) => pod.status.phase === statusFilter,
);
}

// Sort pods by creation timestamp (newest first)
filteredPods.sort((a, b) => {
return (
new Date(b.metadata.creationTimestamp) -
new Date(a.metadata.creationTimestamp)
);
});

res.json({
items: filteredPods,
totalCount: filteredPods.length,
Expand Down Expand Up @@ -392,6 +405,65 @@ app.get("/api/all-queues", async (req, res) => {
res.status(500).json({ error: "Failed to fetch all queues" });
}
});
app.patch("/api/jobs/:namespace/:name", async (req, res) => {
try {
const { namespace, name } = req.params;
const patchData = req.body;

const options = {
headers: { "Content-Type": "application/merge-patch+json" },
};

const response = await k8sApi.patchNamespacedCustomObject(
"batch.volcano.sh",
"v1alpha1",
namespace,
"jobs",
name,
patchData,
undefined,
undefined,
undefined,
options,
);

res.json({ message: "Job updated successfully", data: response.body });
} catch (error) {
console.error("Error updating job:", error);
res.status(500).json({ error: "Failed to update job" });
}
});
app.patch("/api/queues/:namespace/:name", async (req, res) => {
try {
const { namespace, name } = req.params;
const patchData = req.body;

const options = {
headers: { "Content-Type": "application/merge-patch+json" },
};

const response = await k8sApi.patchNamespacedCustomObject(
"scheduling.volcano.sh",
"v1alpha1",
namespace,
"queues",
name,
patchData,
undefined,
undefined,
undefined,
options,
);

res.json({
message: "Queue updated successfully",
data: response.body,
});
} catch (error) {
console.error("Error updating queue:", error);
res.status(500).json({ error: "Failed to update queue" });
}
});

// Get all Pods (no pagination)
app.get("/api/all-pods", async (req, res) => {
Expand All @@ -407,6 +479,78 @@ app.get("/api/all-pods", async (req, res) => {
}
});

// DELETE /api/queues/:name
app.delete("/api/queues/:name", async (req, res) => {
const { name } = req.params;
const queueName = name.toLowerCase();

// Disallow deleting protected system queues
if (["root", "default"].includes(queueName)) {
return res.status(403).json({
error: `Cannot delete "${queueName}" queue`,
details: `The "${queueName}" queue is protected from deletion as it is a core system queue.`,
});
}

try {
// Ensure the queue exists
await k8sApi.getClusterCustomObject({
group: "scheduling.volcano.sh",
version: "v1beta1",
plural: "queues",
name: queueName,
});

// Delete the queue
const { body } = await k8sApi.deleteClusterCustomObject({
group: "scheduling.volcano.sh",
version: "v1beta1",
plural: "queues",
name: queueName,
body: { propagationPolicy: "Foreground" },
});

return res.json({ message: "Queue deleted successfully", data: body });
} catch (err) {
const { statusCode, body, message, code } = err || {};

if (statusCode === 403) {
return res.status(403).json({
error: "Forbidden",
details:
"The dashboard service account does not have permission to delete queues. Please ask your cluster administrator to update the RBAC rules.",
});
}

if (statusCode === 404) {
throw new Error(
`The specified queue "${req.params.name}" does not exist in the cluster. ` +
`Please ensure the queue name is correct and try again.`,
);
}

console.error("Error deleting queue:", body || err);
return res.status(500).json({
error: "Failed to delete queue",
details:
message ||
"An unexpected error occurred while attempting to delete the queue.",
code,
});
}
});
// Delete a Pod
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This PR also support delete pods? But seems that pod doesn't have much error cases such as 4XX? Do we need to support pod deletion in this PR?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

POD and job deletion would be completed in next PR @JesseStutler

// app.delete("/api/pods/:namespace/:name", async (req, res) => {
// try {
// const { namespace, name } = req.params;
// const response = await k8sCoreApi.deleteNamespacedPod(name, namespace);
// res.json({ message: "Pod deleted successfully", data: response.body });
// } catch (error) {
// console.error("Error deleting pod:", error);
// res.status(500).json({ error: "Failed to delete pod" });
// }
// });

const verifyVolcanoSetup = async () => {
try {
// Verify CRD access
Expand Down
2 changes: 1 addition & 1 deletion deployment/volcano-dashboard.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,6 @@ subjects:
namespace: volcano-system
---

# volcano dashboard cluster role
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This comment should not added

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ok

apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
Expand Down Expand Up @@ -127,6 +126,7 @@ rules:
- get
- list
- watch
- delete
---

# volcano dashboard service
Expand Down
71 changes: 60 additions & 11 deletions frontend/src/components/jobs/JobTable/JobEditDialog.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,27 +15,75 @@ const JobEditDialog = ({ open, job, onClose, onSave }) => {
const [editorValue, setEditorValue] = useState("");
const [editMode, setEditMode] = useState("yaml");

const convertContent = (content, fromMode, toMode) => {
try {
if (fromMode === "yaml" && toMode === "json") {
const parsed = yaml.load(content);
return JSON.stringify(parsed, null, 2);
} else if (fromMode === "json" && toMode === "yaml") {
const parsed = JSON.parse(content);
return yaml.dump(parsed);
}
return content;
} catch (err) {
console.error("Conversion error:", err);
alert("Error converting content. Please check your input.");
return content;
}
};

useEffect(() => {
if (open && job) {
const initialContent = yaml.dump(job); // Always keep YAML content
const initialContent =
editMode === "yaml"
? yaml.dump(job)
: JSON.stringify(job, null, 2);
setEditorValue(initialContent);
}
}, [open, job]);
}, [open, job, editMode]);

const handleModeChange = (event, newMode) => {
if (newMode !== null) {
setEditMode(newMode); // Only change syntax highlighting
const converted = convertContent(editorValue, editMode, newMode);
setEditMode(newMode);
setEditorValue(converted);
}
};

const handleSave = () => {
const handleSave = async () => {
try {
const updatedJob = yaml.load(editorValue); // Always parse as YAML
onSave(updatedJob);
onClose();
const updatedJob =
editMode === "yaml"
? yaml.load(editorValue)
: JSON.parse(editorValue);

const namespace = updatedJob.metadata?.namespace || "default";
const jobName = updatedJob.metadata?.name;

if (!jobName) {
alert("Job name is missing. Please check your input.");
return;
}

const response = await fetch(`/api/jobs/${namespace}/${jobName}`, {
method: "PATCH",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(updatedJob),
});

if (!response.ok) {
throw new Error(`Failed to update job: ${response.statusText}`);
}

const data = await response.json();
console.log("Job updated successfully:", data);

onSave(updatedJob); // Notify parent component
onClose(); // Close dialog
} catch (err) {
console.error("Parsing error:", err);
alert("Invalid YAML format. Please check your input.");
console.error("Error updating job:", err);
alert("Failed to update job. Please check your input.");
}
};

Expand All @@ -56,12 +104,13 @@ const JobEditDialog = ({ open, job, onClose, onSave }) => {
color="primary"
>
<ToggleButton value="yaml">YAML</ToggleButton>
<ToggleButton value="json">JSON</ToggleButton>
</ToggleButtonGroup>
</DialogTitle>
<DialogContent sx={{ height: "500px" }}>
<Editor
height="100%"
language={editMode} // Just controls syntax highlight
language={editMode}
value={editorValue}
onChange={(val) => setEditorValue(val || "")}
options={{
Expand Down
Loading