feat: backend for delete functionality

Signed-off-by: Shrutim1505 <shrutimurthy2103@gmail.com>
This commit is contained in:
Shrutim1505 2025-05-23 07:29:07 +00:00
parent 3e76177d7f
commit 31c5d7cc1a
28 changed files with 1614 additions and 1183 deletions

View File

@ -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,
@ -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) => {
@ -407,6 +479,62 @@ app.get("/api/all-pods", async (req, res) => {
}
});
app.delete("/api/queues/:name", async (req, res) => {
const { name } = req.params;
const queueName = name.toLowerCase();
try {
await k8sApi.getClusterCustomObject({
group: "scheduling.volcano.sh",
version: "v1beta1",
plural: "queues",
name: queueName,
});
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 = err?.statusCode || err?.response?.statusCode || 500;
let message = "An unexpected error occurred.";
try {
const rawBody = err?.body || err?.response?.body;
// 🔹 Print the raw error body from Kubernetes
console.error("Kubernetes Error Raw Body:", rawBody);
const parsedBody =
typeof rawBody === "string" ? JSON.parse(rawBody) : rawBody;
// 🔹 Print the parsed error body
console.error("Kubernetes Error Parsed Body:", parsedBody);
if (parsedBody?.message) {
message = parsedBody.message;
}
} catch (parseErr) {
console.error("Error parsing Kubernetes error body:", parseErr);
message = err?.message || message;
}
// 🔹 Also print the full error object for debugging
console.error("Full Kubernetes Error Object:", err);
return res.status(statusCode).json({
error: "Kubernetes Error",
details: message,
});
}
});
const verifyVolcanoSetup = async () => {
try {
// Verify CRD access

View File

@ -16,6 +16,6 @@ COPY --from=builder /app/dist /usr/share/nginx/html
COPY frontend/nginx.conf /etc/nginx/nginx.conf
EXPOSE 80
EXPOSE 8080
CMD ["nginx", "-g", "daemon off;"]

View File

@ -16,15 +16,32 @@ spec:
labels:
app: volcano-dashboard
spec:
securityContext:
seLinuxOptions:
level: s0:c123,c456
seccompProfile:
type: RuntimeDefault
serviceAccountName: volcano-dashboard
containers:
- image: volcanosh/vc-dashboard-frontend:latest
imagePullPolicy: Always
name: frontend
ports:
- containerPort: 80
- containerPort: 8080
name: frontend
protocol: TCP
securityContext:
allowPrivilegeEscalation: false
capabilities:
drop:
- ALL
runAsNonRoot: true
runAsUser: 1000
volumeMounts:
- mountPath: /var/cache/nginx
name: nginx-cache
- mountPath: /run
name: nginx-run
- image: volcanosh/vc-dashboard-backend:latest
imagePullPolicy: Always
name: backend
@ -32,6 +49,18 @@ spec:
- containerPort: 3001
name: backend
protocol: TCP
securityContext:
allowPrivilegeEscalation: false
capabilities:
drop:
- ALL
runAsNonRoot: true
runAsUser: 1000
volumes:
- name: nginx-cache
emptyDir: {}
- name: nginx-run
emptyDir: {}
---
# volcano dashboard serviceAccount
@ -98,6 +127,7 @@ rules:
- get
- list
- watch
- delete
---
# volcano dashboard service
@ -115,6 +145,6 @@ spec:
- name: frontend
port: 80
protocol: TCP
targetPort: 80
targetPort: 8080
selector:
app: volcano-dashboard

View File

@ -3,7 +3,7 @@ events {}
http {
include mime.types;
server {
listen 80;
listen 8080;
server_name localhost;
location / {

View File

@ -1,46 +0,0 @@
import React from "react";
import { Button, Menu, MenuItem } from "@mui/material";
import { FilterList } from "@mui/icons-material";
const JobFilters = ({
filterType,
currentValue,
options,
handleFilterClick,
handleFilterClose,
anchorEl,
}) => {
return (
<>
<Button
size="small"
startIcon={<FilterList />}
onClick={(e) => handleFilterClick(filterType, e)}
sx={{
textTransform: "none",
padding: 0,
minWidth: "auto",
}}
>
Filter: {currentValue}
</Button>
<Menu
anchorEl={anchorEl}
open={Boolean(anchorEl)}
onClose={() => handleFilterClose(filterType, currentValue)}
>
{options.map((option) => (
<MenuItem
key={option}
onClick={() => handleFilterClose(filterType, option)}
selected={option === currentValue}
>
{option}
</MenuItem>
))}
</Menu>
</>
);
};
export default JobFilters;

View File

@ -1,48 +0,0 @@
import React from "react";
import { Box, IconButton, InputAdornment, TextField } from "@mui/material";
import { Clear, Search } from "@mui/icons-material";
const JobSearchBar = ({
searchText,
setSearchText,
handleSearch,
handleClearSearch,
fetchJobs,
}) => {
return (
<Box sx={{ display: "flex", gap: 1, alignItems: "center" }}>
<TextField
placeholder="Search jobs"
variant="outlined"
size="small"
value={searchText}
onChange={handleSearch}
sx={{ width: 200 }}
InputProps={{
startAdornment: (
<InputAdornment position="start">
<IconButton
size="small"
onClick={() => fetchJobs()}
sx={{ padding: "4px" }}
>
<Search />
</IconButton>
</InputAdornment>
),
endAdornment: searchText && (
<IconButton
size="small"
onClick={handleClearSearch}
sx={{ padding: "4px" }}
>
<Clear />
</IconButton>
),
}}
/>
</Box>
);
};
export default JobSearchBar;

View File

@ -1,171 +0,0 @@
import React from "react";
import {
Table,
TableBody,
TableCell,
TableContainer,
TableHead,
TableRow,
Paper,
Typography,
Button,
} from "@mui/material";
import { ArrowDownward, ArrowUpward, UnfoldMore } from "@mui/icons-material";
import JobStatusChip from "./JobStatusChip";
import JobFilters from "./JobFilters";
const JobTable = ({
jobs,
handleJobClick,
filters,
uniqueStatuses,
allNamespaces,
allQueues,
anchorEl,
handleFilterClick,
handleFilterClose,
sortDirection,
toggleSortDirection,
}) => {
return (
<TableContainer
component={Paper}
sx={{ maxHeight: "calc(100vh - 200px)", overflow: "auto" }}
>
<Table stickyHeader>
<TableHead>
<TableRow>
<TableCell
sx={{
backgroundColor: "background.paper",
padding: "8px 16px",
minWidth: 120,
}}
>
<Typography variant="h6">Name</Typography>
</TableCell>
<TableCell
sx={{
backgroundColor: "background.paper",
padding: "8px 16px",
minWidth: 120,
}}
>
<Typography variant="h6">Namespace</Typography>
<JobFilters
filterType="namespace"
currentValue={filters.namespace}
options={allNamespaces}
handleFilterClick={handleFilterClick}
handleFilterClose={handleFilterClose}
anchorEl={anchorEl.namespace}
/>
</TableCell>
<TableCell
sx={{
backgroundColor: "background.paper",
padding: "8px 16px",
minWidth: 120,
}}
>
<Typography variant="h6">Queue</Typography>
<JobFilters
filterType="queue"
currentValue={filters.queue}
options={allQueues}
handleFilterClick={handleFilterClick}
handleFilterClose={handleFilterClose}
anchorEl={anchorEl.queue}
/>
</TableCell>
<TableCell
sx={{
backgroundColor: "background.paper",
padding: "8px 16px",
minWidth: 120,
}}
>
<Typography variant="h6">Creation Time</Typography>
<Button
size="small"
onClick={toggleSortDirection}
startIcon={
sortDirection === "desc" ? (
<ArrowDownward />
) : sortDirection === "asc" ? (
<ArrowUpward />
) : (
<UnfoldMore />
)
}
sx={{
textTransform: "none",
padding: 0,
minWidth: "auto",
}}
>
Sort
</Button>
</TableCell>
<TableCell
sx={{
backgroundColor: "background.paper",
padding: "8px 16px",
minWidth: 120,
}}
>
<Typography variant="h6">Status</Typography>
<JobFilters
filterType="status"
currentValue={filters.status}
options={uniqueStatuses}
handleFilterClick={handleFilterClick}
handleFilterClose={handleFilterClose}
anchorEl={anchorEl.status}
/>
</TableCell>
</TableRow>
</TableHead>
<TableBody>
{jobs.map((job) => (
<TableRow
key={`${job.metadata.namespace}-${job.metadata.name}`}
sx={{
"&:nth-of-type(odd)": {
bgcolor: "action.hover",
},
"&:hover": {
bgcolor: "action.hover",
color: "primary.main",
boxShadow: "0px 4px 6px rgba(0, 0, 0, 0.1)",
},
cursor: "pointer",
}}
onClick={() => handleJobClick(job)}
>
<TableCell>{job.metadata.name}</TableCell>
<TableCell>{job.metadata.namespace}</TableCell>
<TableCell>{job.spec.queue || "N/A"}</TableCell>
<TableCell>
{new Date(
job.metadata.creationTimestamp,
).toLocaleString()}
</TableCell>
<TableCell>
<JobStatusChip
status={
job.status
? job.status.state.phase
: "Unknown"
}
/>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</TableContainer>
);
};
export default JobTable;

View File

@ -0,0 +1,89 @@
import React, { useState, useEffect } from "react";
import {
Dialog,
DialogTitle,
DialogContent,
DialogActions,
Button,
ToggleButton,
ToggleButtonGroup,
} from "@mui/material";
import Editor from "@monaco-editor/react";
import yaml from "js-yaml";
const JobEditDialog = ({ open, job, onClose, onSave }) => {
const [editorValue, setEditorValue] = useState("");
const [editMode, setEditMode] = useState("yaml");
useEffect(() => {
if (open && job) {
const initialContent = yaml.dump(job); // Always keep YAML content
setEditorValue(initialContent);
}
}, [open, job]);
const handleModeChange = (event, newMode) => {
if (newMode !== null) {
setEditMode(newMode); // Only change syntax highlighting
}
};
const handleSave = () => {
try {
const updatedJob = yaml.load(editorValue); // Always parse as YAML
onSave(updatedJob);
onClose();
} catch (err) {
console.error("Parsing error:", err);
alert("Invalid YAML format. Please check your input.");
}
};
return (
<Dialog open={open} onClose={onClose} maxWidth="md" fullWidth>
<DialogTitle
sx={{
display: "flex",
justifyContent: "space-between",
alignItems: "center",
}}
>
Edit Job
<ToggleButtonGroup
value={editMode}
exclusive
onChange={handleModeChange}
color="primary"
>
<ToggleButton value="yaml">YAML</ToggleButton>
</ToggleButtonGroup>
</DialogTitle>
<DialogContent sx={{ height: "500px" }}>
<Editor
height="100%"
language={editMode} // Just controls syntax highlight
value={editorValue}
onChange={(val) => setEditorValue(val || "")}
options={{
minimap: { enabled: false },
automaticLayout: true,
}}
/>
</DialogContent>
<DialogActions>
<Button onClick={onClose} color="primary" variant="contained">
Cancel
</Button>
<Button
onClick={handleSave}
color="primary"
variant="contained"
>
Update
</Button>
</DialogActions>
</Dialog>
);
};
export default JobEditDialog;

View File

@ -0,0 +1,30 @@
import React from "react";
import { Menu, MenuItem } from "@mui/material";
const JobFilters = ({
filterType,
currentValue,
options,
handleFilterClick,
handleFilterClose,
anchorEl,
}) => {
return (
<Menu
anchorEl={anchorEl}
open={Boolean(anchorEl)}
onClose={handleFilterClose}
>
{options.map((option) => (
<MenuItem
key={option}
onClick={() => handleFilterClick(filterType, option)}
>
{option}
</MenuItem>
))}
</Menu>
);
};
export default JobFilters;

View File

@ -0,0 +1,82 @@
import React from "react";
import {
TableContainer,
Table,
TableBody,
Paper,
useTheme,
alpha,
} from "@mui/material";
import JobTableHeader from "./JobTableHeader";
import JobTableRow from "./JobTableRow";
const JobTable = ({
jobs,
handleJobClick,
filters,
uniqueStatuses,
allNamespaces,
allQueues,
anchorEl,
handleFilterClick,
handleFilterClose,
sortDirection,
toggleSortDirection,
}) => {
const theme = useTheme();
return (
<TableContainer
component={Paper}
sx={{
maxHeight: "calc(100vh - 200px)",
overflow: "auto",
borderRadius: "16px",
boxShadow: "0 10px 30px rgba(0, 0, 0, 0.08)",
background: `linear-gradient(to bottom, ${alpha(theme.palette.background.paper, 0.9)}, ${theme.palette.background.paper})`,
backdropFilter: "blur(10px)",
border: `1px solid ${alpha(theme.palette.divider, 0.1)}`,
"&::-webkit-scrollbar": {
width: "10px",
height: "10px",
},
"&::-webkit-scrollbar-thumb": {
backgroundColor: alpha(theme.palette.primary.main, 0.2),
borderRadius: "5px",
"&:hover": {
backgroundColor: alpha(theme.palette.primary.main, 0.3),
},
},
"&::-webkit-scrollbar-track": {
backgroundColor: alpha(theme.palette.primary.main, 0.05),
borderRadius: "5px",
},
}}
>
<Table stickyHeader>
<JobTableHeader
filters={filters}
uniqueStatuses={uniqueStatuses}
allNamespaces={allNamespaces}
allQueues={allQueues}
anchorEl={anchorEl}
handleFilterClick={handleFilterClick}
handleFilterClose={handleFilterClose}
sortDirection={sortDirection}
toggleSortDirection={toggleSortDirection}
/>
<TableBody>
{jobs.map((job) => (
<JobTableRow
key={`${job.metadata.namespace}-${job.metadata.name}`}
job={job}
handleJobClick={handleJobClick}
/>
))}
</TableBody>
</Table>
</TableContainer>
);
};
export default JobTable;

View File

@ -0,0 +1,209 @@
import React from "react";
import {
TableHead,
TableRow,
TableCell,
Typography,
Button,
Box,
useTheme,
alpha,
} from "@mui/material";
import { ArrowDownward, ArrowUpward, UnfoldMore } from "@mui/icons-material";
import JobFilters from "./JobFilters";
const JobTableHeader = ({
filters,
uniqueStatuses,
allNamespaces,
allQueues,
anchorEl,
handleFilterClick,
handleFilterClose,
sortDirection,
toggleSortDirection,
}) => {
const theme = useTheme();
return (
<TableHead>
<TableRow>
<TableCell
sx={{
backgroundColor: alpha(
theme.palette.background.paper,
0.8,
),
backdropFilter: "blur(8px)",
padding: "16px 24px",
minWidth: 140,
borderBottom: `2px solid ${alpha(theme.palette.primary.main, 0.2)}`,
}}
>
<Typography
variant="subtitle1"
fontWeight="700"
color="text.primary"
>
Name
</Typography>
</TableCell>
{["Namespace", "Queue"].map((field) => (
<TableCell
key={field}
sx={{
backgroundColor: alpha(
theme.palette.background.paper,
0.8,
),
backdropFilter: "blur(8px)",
padding: "16px 24px",
borderBottom: `2px solid ${alpha(theme.palette.primary.main, 0.2)}`,
}}
>
<Box
sx={{
display: "flex",
alignItems: "center",
gap: "8px",
}}
>
<Typography
variant="subtitle1"
fontWeight="700"
color="text.primary"
>
{field}
</Typography>
<JobFilters
filterType={field.toLowerCase()}
currentValue={filters[field.toLowerCase()]}
options={
field === "Namespace"
? allNamespaces
: allQueues
}
handleFilterClick={handleFilterClick}
handleFilterClose={handleFilterClose}
anchorEl={anchorEl[field.toLowerCase()]}
/>
</Box>
</TableCell>
))}
<TableCell
sx={{
backgroundColor: alpha(
theme.palette.background.paper,
0.8,
),
backdropFilter: "blur(8px)",
padding: "16px 24px",
minWidth: 140,
borderBottom: `2px solid ${alpha(theme.palette.primary.main, 0.2)}`,
}}
>
<Typography
variant="subtitle1"
fontWeight="700"
color="text.primary"
>
Creation Time
</Typography>
<Button
size="small"
onClick={toggleSortDirection}
startIcon={
sortDirection === "desc" ? (
<ArrowDownward fontSize="small" />
) : sortDirection === "asc" ? (
<ArrowUpward fontSize="small" />
) : (
<UnfoldMore fontSize="small" />
)
}
sx={{
textTransform: "none",
padding: "4px 12px",
minWidth: "auto",
borderRadius: "20px",
marginTop: "8px",
fontSize: "0.8rem",
fontWeight: 500,
letterSpacing: "0.02em",
backgroundColor: alpha(
theme.palette.primary.main,
0.1,
),
color: theme.palette.primary.main,
"&:hover": {
backgroundColor: alpha(
theme.palette.primary.main,
0.15,
),
transform: "translateY(-2px)",
},
}}
>
Sort
</Button>
</TableCell>
<TableCell
sx={{
backgroundColor: alpha(
theme.palette.background.paper,
0.8,
),
backdropFilter: "blur(8px)",
padding: "16px 24px",
minWidth: 140,
borderBottom: `2px solid ${alpha(theme.palette.primary.main, 0.2)}`,
}}
>
<Typography
variant="subtitle1"
fontWeight="700"
color="text.primary"
>
Status
</Typography>
<JobFilters
filterType="status"
currentValue={filters.status}
options={uniqueStatuses}
handleFilterClick={handleFilterClick}
handleFilterClose={handleFilterClose}
anchorEl={anchorEl.status}
/>
</TableCell>
{/* New Actions Column */}
<TableCell
sx={{
backgroundColor: alpha(
theme.palette.background.paper,
0.8,
),
backdropFilter: "blur(8px)",
padding: "16px 24px",
minWidth: 140,
borderBottom: `2px solid ${alpha(theme.palette.primary.main, 0.2)}`,
}}
>
<Typography
variant="subtitle1"
fontWeight="700"
color="text.primary"
sx={{ letterSpacing: "0.02em" }}
>
Actions
</Typography>
</TableCell>
</TableRow>
</TableHead>
);
};
export default JobTableHeader;

View File

@ -0,0 +1,183 @@
import React, { useState } from "react";
import {
TableRow,
TableCell,
Box,
IconButton,
useTheme,
alpha,
} from "@mui/material";
import { Edit, Delete } from "@mui/icons-material";
import JobStatusChip from "../JobStatusChip";
import JobEditDialog from "./JobEditDialog"; // Create or import this component
const JobTableRow = ({
job,
handleJobClick,
handleOpenDeleteDialog,
onJobUpdate, // Function to update job after edit
}) => {
const theme = useTheme();
const [isEditDialogOpen, setIsEditDialogOpen] = useState(false);
const handleOpenEditDialog = (e) => {
e.stopPropagation();
setIsEditDialogOpen(true);
};
const handleCloseEditDialog = () => {
setIsEditDialogOpen(false);
};
const handleSaveJob = (updatedJob) => {
onJobUpdate(updatedJob);
handleCloseEditDialog();
};
return (
<>
<TableRow
hover
onClick={() => handleJobClick(job)}
sx={{
height: "60px",
transition: "all 0.3s cubic-bezier(0.4, 0, 0.2, 1)",
"&:hover": {
bgcolor: alpha(theme.palette.primary.main, 0.08),
"& .MuiTableCell-root": {
color: theme.palette.primary.main,
},
boxShadow: "0 4px 12px rgba(0, 0, 0, 0.08)",
transform: "translateY(-2px)",
},
cursor: "pointer",
"&:last-child td, &:last-child th": {
borderBottom: 0,
},
"& td": {
borderBottom: `1px solid ${alpha(theme.palette.divider, 0.1)}`,
},
}}
>
<TableCell
sx={{
padding: "16px 24px",
fontWeight: 600,
color: theme.palette.text.primary,
letterSpacing: "0.01em",
}}
>
{job.metadata.name}
</TableCell>
<TableCell
sx={{
padding: "16px 24px",
fontWeight: 500,
fontSize: "0.95rem",
}}
>
{job.metadata.namespace}
</TableCell>
<TableCell
sx={{
padding: "16px 24px",
fontWeight: 500,
fontSize: "0.95rem",
}}
>
{job.spec.queue || "N/A"}
</TableCell>
<TableCell
sx={{
padding: "16px 24px",
fontSize: "0.9rem",
color: alpha(theme.palette.text.primary, 0.85),
}}
>
{new Date(job.metadata.creationTimestamp).toLocaleString()}
</TableCell>
<TableCell sx={{ padding: "16px 24px" }}>
<Box
sx={{
display: "inline-block",
transition: "all 0.3s ease",
"&:hover": {
transform: "translateY(-2px)",
filter: "brightness(1.05)",
},
boxShadow: "0 3px 6px rgba(0, 0, 0, 0.15)",
borderRadius: "15px",
}}
>
<JobStatusChip
status={
job.status ? job.status.state.phase : "Unknown"
}
sx={{
height: "30px",
fontWeight: 600,
fontSize: "0.8rem",
padding: "0 12px",
color: "common.white",
borderRadius: "15px",
}}
/>
</Box>
</TableCell>
<TableCell sx={{ padding: "16px 24px" }}>
<Box display="flex" alignItems="center" gap={2}>
<IconButton
onClick={handleOpenEditDialog}
size="small"
sx={{
color: theme.palette.primary.main,
"&:hover": {
backgroundColor: alpha(
theme.palette.primary.main,
0.1,
),
},
}}
>
<Edit fontSize="small" />
</IconButton>
<IconButton
onClick={(e) => {
e.stopPropagation();
handleOpenDeleteDialog(job.metadata.name);
}}
size="small"
sx={{
color: theme.palette.error.main,
"&:hover": {
backgroundColor: alpha(
theme.palette.error.main,
0.1,
),
},
}}
>
<Delete fontSize="small" />
</IconButton>
</Box>
</TableCell>
</TableRow>
{/* Edit Dialog */}
<JobEditDialog
open={isEditDialogOpen}
job={job}
onClose={handleCloseEditDialog}
onSave={handleSaveJob}
/>
</>
);
};
export default JobTableRow;

View File

@ -1,83 +0,0 @@
import React from "react";
import { TableCell, Typography, Button, useTheme } from "@mui/material";
import { ArrowDownward, ArrowUpward, UnfoldMore } from "@mui/icons-material";
const JobTableHeader = ({ sortDirection, setSortDirection }) => {
const theme = useTheme();
const toggleSortDirection = () => {
setSortDirection((prev) => (prev === "asc" ? "desc" : "asc"));
};
return (
<>
<TableCell
sx={{
backgroundColor: "background.paper",
padding: "8px 16px",
minWidth: 120,
}}
>
<Typography variant="h6">Name</Typography>
</TableCell>
<TableCell
sx={{
backgroundColor: "background.paper",
padding: "8px 16px",
minWidth: 120,
}}
>
<Typography variant="h6">Namespace</Typography>
</TableCell>
<TableCell
sx={{
backgroundColor: "background.paper",
padding: "8px 16px",
minWidth: 120,
}}
>
<Typography variant="h6">Queue</Typography>
</TableCell>
<TableCell
sx={{
backgroundColor: "background.paper",
padding: "8px 16px",
minWidth: 120,
}}
>
<Typography variant="h6">Creation Time</Typography>
<Button
size="small"
onClick={toggleSortDirection}
startIcon={
sortDirection === "desc" ? (
<ArrowDownward />
) : sortDirection === "asc" ? (
<ArrowUpward />
) : (
<UnfoldMore />
)
}
sx={{
textTransform: "none",
padding: 0,
minWidth: "auto",
}}
>
Sort
</Button>
</TableCell>
<TableCell
sx={{
backgroundColor: "background.paper",
padding: "8px 16px",
minWidth: 120,
}}
>
<Typography variant="h6">Status</Typography>
</TableCell>
</>
);
};
export default JobTableHeader;

View File

@ -3,7 +3,7 @@ import { Box, Button, Typography, useTheme } from "@mui/material";
import axios from "axios";
import TitleComponent from "../Titlecomponent";
import { fetchAllNamespaces, fetchAllQueues } from "../utils";
import JobTable from "./JobTable";
import JobTable from "./JobTable/JobTable";
import JobPagination from "./JobPagination";
import JobDialog from "./JobDialog";
import SearchBar from "../Searchbar";

View File

@ -4,7 +4,7 @@ import axios from "axios";
import SearchBar from "../Searchbar";
import TitleComponent from "../Titlecomponent";
import { fetchAllNamespaces } from "../utils";
import PodsTable from "./PodsTable";
import PodsTable from "./PodsTable/PodsTable";
import PodsPagination from "./PodsPagination";
import PodDetailsDialog from "./PodDetailsDialog";
@ -69,8 +69,8 @@ const Pods = () => {
setPods(cachedPods.slice(startIndex, endIndex));
}, [cachedPods, pagination]);
const handleSearch = (value) => {
setSearchText(value);
const handleSearch = (event) => {
setSearchText(event.target.value);
setPagination((prev) => ({ ...prev, page: 1 }));
};

View File

@ -1,251 +0,0 @@
import React, { useMemo, useState, useCallback } from "react";
import {
Box,
Button,
Chip,
Menu,
MenuItem,
Paper,
Table,
TableBody,
TableCell,
TableContainer,
TableHead,
TableRow,
Typography,
useTheme,
} from "@mui/material";
import {
ArrowDownward,
ArrowUpward,
FilterList,
UnfoldMore,
} from "@mui/icons-material";
import { calculateAge } from "../utils";
const FilterMenu = ({ anchorEl, handleClose, items, filterType }) => (
<Menu
anchorEl={anchorEl}
open={Boolean(anchorEl)}
onClose={() => handleClose(filterType, null)}
>
{items.map((item) => (
<MenuItem key={item} onClick={() => handleClose(filterType, item)}>
{item}
</MenuItem>
))}
</Menu>
);
const TableHeader = ({
filters,
anchorEl,
handleFilterClick,
handleFilterClose,
allNamespaces,
onSortDirectionToggle,
sortDirection,
}) => (
<TableHead>
<TableRow>
{["Name", "Namespace", "Creation Time", "Status", "Age"].map(
(header) => (
<TableCell
key={header}
sx={{
backgroundColor: "background.paper",
padding: "8px 16px",
minWidth: 120,
}}
>
<Typography variant="h6">{header}</Typography>
{(header === "Namespace" || header === "Status") && (
<Button
size="small"
startIcon={<FilterList />}
onClick={(e) =>
handleFilterClick(header.toLowerCase(), e)
}
sx={{
textTransform: "none",
padding: 0,
minWidth: "auto",
}}
>
Filter: {filters[header.toLowerCase()]}
</Button>
)}
{header === "Creation Time" && (
<Button
size="small"
onClick={onSortDirectionToggle}
startIcon={
sortDirection === "desc" ? (
<ArrowDownward />
) : sortDirection === "asc" ? (
<ArrowUpward />
) : (
<UnfoldMore />
)
}
sx={{
textTransform: "none",
padding: 0,
minWidth: "auto",
}}
>
Sort
</Button>
)}
</TableCell>
),
)}
</TableRow>
<FilterMenu
anchorEl={anchorEl.namespace}
handleClose={handleFilterClose}
items={allNamespaces}
filterType="namespace"
/>
<FilterMenu
anchorEl={anchorEl.status}
handleClose={handleFilterClose}
items={["All", "Running", "Pending", "Succeeded", "Failed"]}
filterType="status"
/>
</TableHead>
);
const PodRow = ({ pod, getStatusColor, onPodClick }) => (
<TableRow
key={`${pod.metadata.namespace}-${pod.metadata.name}`}
sx={{
"&:nth-of-type(odd)": { bgcolor: "action.hover" },
"&:hover": {
bgcolor: "action.hover",
color: "primary.main",
boxShadow: "0px 4px 6px rgba(0, 0, 0, 0.1)",
},
cursor: "pointer",
}}
onClick={() => onPodClick(pod)}
>
<TableCell>{pod.metadata.name}</TableCell>
<TableCell>{pod.metadata.namespace}</TableCell>
<TableCell>
{new Date(pod.metadata.creationTimestamp).toLocaleString()}
</TableCell>
<TableCell>
<Chip
label={pod.status ? pod.status.phase : "Unknown"}
sx={{
bgcolor: getStatusColor(
pod.status ? pod.status.phase : "Unknown",
),
color: "common.white",
}}
/>
</TableCell>
<TableCell>{calculateAge(pod.metadata.creationTimestamp)}</TableCell>
</TableRow>
);
const PodsTable = ({
pods,
filters,
allNamespaces,
sortDirection,
onSortDirectionToggle,
onFilterChange,
onPodClick,
}) => {
const theme = useTheme();
const [anchorEl, setAnchorEl] = useState({ status: null, namespace: null });
const handleFilterClick = useCallback((filterType, event) => {
setAnchorEl((prev) => ({ ...prev, [filterType]: event.currentTarget }));
}, []);
const handleFilterClose = useCallback(
(filterType, value) => {
onFilterChange(filterType, value);
setAnchorEl((prev) => ({ ...prev, [filterType]: null }));
},
[onFilterChange],
);
const filteredPods = useMemo(
() =>
pods.filter(
(pod) =>
(filters.status === "All" ||
(pod.status && pod.status.phase === filters.status)) &&
(!filters.queue ||
filters.queue === "All" ||
pod.spec.queue === filters.queue),
),
[pods, filters],
);
const sortedPods = useMemo(
() =>
[...filteredPods].sort((a, b) => {
const compareResult =
new Date(b.metadata.creationTimestamp) -
new Date(a.metadata.creationTimestamp);
return sortDirection === "desc"
? compareResult
: -compareResult;
}),
[filteredPods, sortDirection],
);
const getStatusColor = useCallback(
(status) => {
switch (status) {
case "Failed":
return theme.palette.error.main;
case "Pending":
return theme.palette.warning.main;
case "Running":
return theme.palette.success.main;
case "Succeeded":
return theme.palette.info.main;
default:
return theme.palette.grey[500];
}
},
[theme],
);
return (
<TableContainer
component={Paper}
sx={{ maxHeight: "calc(100vh - 200px)", overflow: "auto" }}
>
<Table stickyHeader>
<TableHeader
filters={filters}
anchorEl={anchorEl}
handleFilterClick={handleFilterClick}
handleFilterClose={handleFilterClose}
allNamespaces={allNamespaces}
onSortDirectionToggle={onSortDirectionToggle}
sortDirection={sortDirection}
/>
<TableBody>
{sortedPods.map((pod) => (
<PodRow
key={pod.metadata.name}
pod={pod}
getStatusColor={getStatusColor}
onPodClick={onPodClick}
/>
))}
</TableBody>
</Table>
</TableContainer>
);
};
export default PodsTable;

View File

@ -0,0 +1,18 @@
import React from "react";
import { Menu, MenuItem } from "@mui/material";
const FilterMenu = ({ anchorEl, handleClose, items, filterType }) => (
<Menu
anchorEl={anchorEl}
open={Boolean(anchorEl)}
onClose={() => handleClose(filterType, null)}
>
{items.map((item) => (
<MenuItem key={item} onClick={() => handleClose(filterType, item)}>
{item}
</MenuItem>
))}
</Menu>
);
export default FilterMenu;

View File

@ -0,0 +1,99 @@
import React from "react";
import { TableRow, TableCell, Chip, useTheme, alpha } from "@mui/material";
import { calculateAge } from "../../utils";
const PodRow = ({ pod, getStatusColor, onPodClick }) => {
const theme = useTheme();
return (
<TableRow
hover
onClick={(e) => onPodClick(pod)}
sx={{
height: "60px",
transition: "all 0.3s cubic-bezier(0.4, 0, 0.2, 1)",
"&:hover": {
bgcolor: alpha(theme.palette.primary.main, 0.08),
"& .MuiTableCell-root": {
color: theme.palette.primary.main,
},
boxShadow: "0 4px 12px rgba(0, 0, 0, 0.08)",
transform: "translateY(-2px)",
},
cursor: "pointer",
"&:last-child td, &:last-child th": {
borderBottom: 0,
},
"& td": {
borderBottom: `1px solid ${alpha(theme.palette.divider, 0.1)}`,
},
}}
>
<TableCell
sx={{
padding: "16px 24px",
fontWeight: 600,
color: theme.palette.text.primary,
letterSpacing: "0.01em",
}}
>
{pod.metadata.name}
</TableCell>
<TableCell
sx={{
padding: "16px 24px",
fontSize: "0.95rem",
fontWeight: 500,
}}
>
{pod.metadata.namespace}
</TableCell>
<TableCell
sx={{
padding: "16px 24px",
fontSize: "0.9rem",
color: alpha(theme.palette.text.primary, 0.85),
}}
>
{new Date(pod.metadata.creationTimestamp).toLocaleString()}
</TableCell>
<TableCell sx={{ padding: "16px 24px" }}>
<Chip
label={pod.status?.phase || "Unknown"}
sx={{
bgcolor: getStatusColor(pod.status?.phase || "Unknown"),
color: "common.white",
height: "30px",
fontWeight: 600,
fontSize: "0.8rem",
letterSpacing: "0.02em",
borderRadius: "15px",
boxShadow: "0 3px 6px rgba(0, 0, 0, 0.15)",
padding: "0 12px",
transition: "all 0.3s cubic-bezier(0.4, 0, 0.2, 1)",
"&:hover": {
transform: "translateY(-2px)",
boxShadow: "0 5px 10px rgba(0, 0, 0, 0.2)",
filter: "brightness(1.05)",
},
}}
/>
</TableCell>
<TableCell
sx={{
padding: "16px 24px",
fontSize: "0.95rem",
fontWeight: 500,
}}
>
{calculateAge(pod.metadata.creationTimestamp)}
</TableCell>
</TableRow>
);
};
export default PodRow;

View File

@ -0,0 +1,137 @@
import React from "react";
import {
TableContainer,
Table,
TableBody,
Paper,
useTheme,
alpha,
} from "@mui/material";
import TableHeader from "./TableHeader";
import PodRow from "./PodRow";
const PodsTable = ({
pods,
filters,
allNamespaces,
sortDirection,
onSortDirectionToggle,
onFilterChange,
onPodClick,
}) => {
const theme = useTheme();
const [anchorEl, setAnchorEl] = React.useState({
status: null,
namespace: null,
});
const handleFilterClick = React.useCallback((filterType, event) => {
setAnchorEl((prev) => ({ ...prev, [filterType]: event.currentTarget }));
}, []);
const handleFilterClose = React.useCallback(
(filterType, value) => {
onFilterChange(filterType, value);
setAnchorEl((prev) => ({ ...prev, [filterType]: null }));
},
[onFilterChange],
);
const filteredPods = React.useMemo(
() =>
pods.filter(
(pod) =>
(filters.status === "All" ||
(pod.status && pod.status.phase === filters.status)) &&
(!filters.queue ||
filters.queue === "All" ||
pod.spec.queue === filters.queue),
),
[pods, filters],
);
const sortedPods = React.useMemo(
() =>
[...filteredPods].sort((a, b) => {
const compareResult =
new Date(b.metadata.creationTimestamp) -
new Date(a.metadata.creationTimestamp);
return sortDirection === "desc"
? compareResult
: -compareResult;
}),
[filteredPods, sortDirection],
);
const getStatusColor = React.useCallback(
(status) => {
switch (status) {
case "Failed":
return theme.palette.error.main;
case "Pending":
return theme.palette.warning.main;
case "Running":
return theme.palette.success.main;
case "Succeeded":
return theme.palette.info.main;
default:
return theme.palette.grey[500];
}
},
[theme],
);
return (
<TableContainer
component={Paper}
sx={{
maxHeight: "calc(100vh - 200px)",
overflow: "auto",
borderRadius: "16px",
boxShadow: "0 10px 30px rgba(0, 0, 0, 0.08)",
background: `linear-gradient(to bottom, ${alpha(theme.palette.background.paper, 0.9)}, ${theme.palette.background.paper})`,
backdropFilter: "blur(10px)",
border: `1px solid ${alpha(theme.palette.divider, 0.1)}`,
"&::-webkit-scrollbar": {
width: "10px",
height: "10px",
},
"&::-webkit-scrollbar-thumb": {
backgroundColor: alpha(theme.palette.primary.main, 0.2),
borderRadius: "5px",
"&:hover": {
backgroundColor: alpha(theme.palette.primary.main, 0.3),
},
},
"&::-webkit-scrollbar-track": {
backgroundColor: alpha(theme.palette.primary.main, 0.05),
borderRadius: "5px",
},
}}
>
<Table stickyHeader>
<TableHeader
filters={filters}
anchorEl={anchorEl}
handleFilterClick={handleFilterClick}
handleFilterClose={handleFilterClose}
allNamespaces={allNamespaces}
onSortDirectionToggle={onSortDirectionToggle}
sortDirection={sortDirection}
/>
<TableBody>
{sortedPods.map((pod) => (
<PodRow
key={`${pod.metadata.namespace}-${pod.metadata.name}`}
pod={pod}
getStatusColor={getStatusColor}
onPodClick={onPodClick}
/>
))}
</TableBody>
</Table>
</TableContainer>
);
};
export default PodsTable;

View File

@ -0,0 +1,176 @@
import React from "react";
import {
TableHead,
TableRow,
TableCell,
Button,
Typography,
useTheme,
alpha,
} from "@mui/material";
import {
ArrowDownward,
ArrowUpward,
FilterList,
UnfoldMore,
} from "@mui/icons-material";
import FilterMenu from "./FilterMenu";
const TableHeader = ({
filters,
anchorEl,
handleFilterClick,
handleFilterClose,
allNamespaces,
onSortDirectionToggle,
sortDirection,
}) => {
const theme = useTheme();
return (
<TableHead>
<TableRow>
{["Name", "Namespace", "Creation Time", "Status", "Age"].map(
(header) => (
<TableCell
key={header}
sx={{
backgroundColor: alpha(
theme.palette.background.paper,
0.8,
),
backdropFilter: "blur(8px)",
padding: "16px 24px",
minWidth: 140,
borderBottom: `2px solid ${alpha(
theme.palette.primary.main,
0.2,
)}`,
}}
>
<Typography
variant="subtitle1"
fontWeight="700"
color="text.primary"
sx={{ letterSpacing: "0.02em" }}
>
{header}
</Typography>
{(header === "Namespace" ||
header === "Status") && (
<Button
size="small"
startIcon={<FilterList fontSize="small" />}
onClick={(e) =>
handleFilterClick(
header.toLowerCase(),
e,
)
}
sx={{
textTransform: "none",
padding: "4px 12px",
minWidth: "auto",
borderRadius: "20px",
marginTop: "8px",
fontSize: "0.8rem",
fontWeight: 500,
letterSpacing: "0.02em",
transition:
"all 0.3s cubic-bezier(0.4, 0, 0.2, 1)",
backgroundColor:
filters[header.toLowerCase()] !==
"All"
? alpha(
theme.palette.primary
.main,
0.2,
)
: alpha(
theme.palette.primary
.main,
0.1,
),
color: theme.palette.primary.main,
"&:hover": {
backgroundColor: alpha(
theme.palette.primary.main,
0.15,
),
transform: "translateY(-2px)",
boxShadow: `0 4px 8px ${alpha(
theme.palette.primary.main,
0.2,
)}`,
},
}}
>
Filter: {filters[header.toLowerCase()]}
</Button>
)}
{header === "Creation Time" && (
<Button
size="small"
onClick={onSortDirectionToggle}
startIcon={
sortDirection === "desc" ? (
<ArrowDownward fontSize="small" />
) : sortDirection === "asc" ? (
<ArrowUpward fontSize="small" />
) : (
<UnfoldMore fontSize="small" />
)
}
sx={{
textTransform: "none",
padding: "4px 12px",
minWidth: "auto",
borderRadius: "20px",
marginTop: "8px",
fontSize: "0.8rem",
fontWeight: 500,
letterSpacing: "0.02em",
transition:
"all 0.3s cubic-bezier(0.4, 0, 0.2, 1)",
backgroundColor: alpha(
theme.palette.primary.main,
0.1,
),
color: theme.palette.primary.main,
"&:hover": {
backgroundColor: alpha(
theme.palette.primary.main,
0.15,
),
transform: "translateY(-2px)",
boxShadow: `0 4px 8px ${alpha(
theme.palette.primary.main,
0.2,
)}`,
},
}}
>
Sort
</Button>
)}
</TableCell>
),
)}
</TableRow>
<FilterMenu
anchorEl={anchorEl.namespace}
handleClose={handleFilterClose}
items={allNamespaces}
filterType="namespace"
/>
<FilterMenu
anchorEl={anchorEl.status}
handleClose={handleFilterClose}
items={["All", "Running", "Pending", "Succeeded", "Failed"]}
filterType="status"
/>
</TableHead>
);
};
export default TableHeader;

View File

@ -1,46 +0,0 @@
import React from "react";
import { Box, IconButton, InputAdornment, TextField } from "@mui/material";
import { Clear, Search } from "@mui/icons-material";
const SearchBar = ({ searchText, onSearch, onClear, onSearchSubmit }) => {
const handleSearch = (event) => {
onSearch(event.target.value);
};
return (
<Box sx={{ display: "flex", gap: 1, alignItems: "center" }}>
<TextField
placeholder="Search pods"
variant="outlined"
size="small"
value={searchText}
onChange={handleSearch}
sx={{ width: 200 }}
InputProps={{
startAdornment: (
<InputAdornment position="start">
<IconButton
size="small"
onClick={onSearchSubmit}
sx={{ padding: "4px" }}
>
<Search />
</IconButton>
</InputAdornment>
),
endAdornment: searchText && (
<IconButton
size="small"
onClick={onClear}
sx={{ padding: "4px" }}
>
<Clear />
</IconButton>
),
}}
/>
</Box>
);
};
export default SearchBar;

View File

@ -15,57 +15,27 @@ const EditQueueDialog = ({ open, queue, onClose, onSave }) => {
const [editorValue, setEditorValue] = useState("");
const [editMode, setEditMode] = useState("yaml");
const convertContent = (content, fromMode, toMode) => {
try {
if (fromMode === "yaml" && toMode === "json") {
const parsedContent = yaml.load(content);
return JSON.stringify(parsedContent, null, 2);
} else if (fromMode === "json" && toMode === "yaml") {
const parsedContent = JSON.parse(content);
return yaml.dump(parsedContent);
}
return content;
} catch (error) {
console.error("Conversion error:", error);
alert("Error converting content. Please check your input.");
return content;
}
};
useEffect(() => {
if (open && queue) {
const content =
editMode === "yaml"
? yaml.dump(queue)
: JSON.stringify(queue, null, 2);
const content = yaml.dump(queue); // Always YAML
setEditorValue(content);
}
}, [open, queue, editMode]);
}, [open, queue]);
const handleModeChange = (event, newMode) => {
if (newMode !== null) {
const convertedContent = convertContent(
editorValue,
editMode,
newMode,
);
setEditMode(newMode);
setEditorValue(convertedContent);
setEditMode(newMode); // Only for syntax highlighting
}
};
const handleSave = () => {
try {
const updatedQueue =
editMode === "yaml"
? yaml.load(editorValue)
: JSON.parse(editorValue);
const updatedQueue = yaml.load(editorValue); // Always parse as YAML
onSave(updatedQueue);
onClose();
} catch (error) {
console.error("Error parsing edited content:", error);
alert("Invalid YAML/JSON format. Please check your input.");
alert("Invalid YAML format. Please check your input.");
}
};
@ -86,13 +56,12 @@ const EditQueueDialog = ({ open, queue, 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}
language={editMode} // only affects syntax highlighting
value={editorValue}
onChange={(value) => setEditorValue(value || "")}
options={{

View File

@ -1,9 +1,11 @@
import React, { useState } from "react";
import React, { useState, useEffect } from "react";
import {
TableContainer,
Table,
TableBody,
Paper,
TableRow,
TableCell,
useTheme,
alpha,
} from "@mui/material";
@ -23,12 +25,20 @@ const QueueTable = ({
uniqueStates,
handleFilterClose,
setAnchorEl,
handleDelete,
onQueueUpdate,
}) => {
const theme = useTheme();
const [queues, setQueues] = useState([]);
useEffect(() => {
setQueues(sortedQueues);
}, [sortedQueues]);
const [openDeleteDialog, setOpenDeleteDialog] = useState(false);
const [queueToDelete, setQueueToDelete] = useState(null);
const [deleteError, setDeleteError] = useState(null);
const [isDeleting, setIsDeleting] = useState(false);
const handleOpenDeleteDialog = (queueName) => {
setQueueToDelete(queueName);
setOpenDeleteDialog(true);
@ -37,13 +47,77 @@ const QueueTable = ({
const handleCloseDeleteDialog = () => {
setOpenDeleteDialog(false);
setQueueToDelete(null);
setDeleteError(null);
setIsDeleting(false);
};
const confirmDelete = () => {
if (handleDelete && queueToDelete) {
handleDelete(queueToDelete);
const handleDelete = async () => {
try {
setIsDeleting(true);
const response = await fetch(
`/api/queues/${encodeURIComponent(queueToDelete)}`,
{
method: "DELETE",
headers: {
"Content-Type": "application/json",
Accept: "application/json",
},
},
);
let data = {};
const contentType = response.headers.get("content-type");
const text = await response.text();
let isJsonResponse = false;
try {
if (
(contentType && contentType.includes("application/json")) ||
(text && !text.trim().startsWith("<"))
) {
data = text ? JSON.parse(text) : {};
isJsonResponse = true;
}
} catch (parseError) {
console.warn("Failed to parse response as JSON:", parseError);
}
if (!response.ok) {
let customMessage = `queues.scheduling.volcano.sh "${queueToDelete}" is forbidden.`;
let errorType = "UnknownError";
if (
isJsonResponse &&
typeof data === "object" &&
(data.message || data.details)
) {
customMessage = data.message || data.details;
if (customMessage.toLowerCase().includes("denied")) {
errorType = "ValidationError";
} else {
errorType = "KubernetesError";
}
}
const fullMessage = `Cannot delete "${queueToDelete}". Error message: ${customMessage}`;
const error = new Error(fullMessage);
error.type = errorType;
error.status = response.status;
throw error;
}
// Success
setQueues((prev) =>
prev.filter((queue) => queue.metadata.name !== queueToDelete),
);
handleCloseDeleteDialog();
} catch (error) {
console.error("Error deleting queue:", error);
setDeleteError(error.message || "An unexpected error occurred.");
} finally {
setIsDeleting(false);
}
handleCloseDeleteDialog();
};
return (
@ -94,23 +168,39 @@ const QueueTable = ({
setAnchorEl={setAnchorEl}
/>
<TableBody>
{sortedQueues.map((queue) => (
<QueueTableRow
key={queue.metadata.name}
queue={queue}
allocatedFields={allocatedFields}
handleQueueClick={handleQueueClick}
handleOpenDeleteDialog={handleOpenDeleteDialog}
/>
))}
{queues.length === 0 ? (
<TableRow>
<TableCell
colSpan={allocatedFields.length + 2}
align="center"
>
No queues found.
</TableCell>
</TableRow>
) : (
queues.map((queue) => (
<QueueTableRow
key={queue.metadata.name}
queue={queue}
allocatedFields={allocatedFields}
handleQueueClick={handleQueueClick}
handleOpenDeleteDialog={
handleOpenDeleteDialog
}
onQueueUpdate={onQueueUpdate}
/>
))
)}
</TableBody>
</Table>
</TableContainer>
<QueueTableDeleteDialog
open={openDeleteDialog}
onClose={handleCloseDeleteDialog}
onConfirm={confirmDelete}
onConfirm={handleDelete}
queueToDelete={queueToDelete}
error={deleteError}
isDeleting={isDeleting}
/>
</React.Fragment>
);

View File

@ -3,110 +3,60 @@ import {
Dialog,
DialogTitle,
DialogContent,
DialogContentText,
DialogActions,
IconButton,
Button,
useTheme,
alpha,
Alert,
} from "@mui/material";
import CloseIcon from "@mui/icons-material/Close";
const QueueTableDeleteDialog = ({
open,
onClose,
onConfirm,
queueToDelete,
error,
}) => {
const theme = useTheme();
const showOnlyError = Boolean(error);
return (
<Dialog
open={open}
onClose={onClose}
PaperProps={{
sx: {
borderRadius: "16px",
boxShadow: "0 8px 24px rgba(0, 0, 0, 0.15)",
border: `1px solid ${alpha(theme.palette.divider, 0.1)}`,
backgroundImage: `linear-gradient(135deg, ${alpha(theme.palette.background.paper, 0.95)}, ${theme.palette.background.paper})`,
backdropFilter: "blur(10px)",
},
}}
>
<Dialog open={open} onClose={onClose}>
<DialogTitle
sx={{
fontWeight: 700,
color: theme.palette.error.main,
fontSize: "1.2rem",
textAlign: "center",
borderBottom: `1px solid ${alpha(theme.palette.divider, 0.1)}`,
padding: "24px 32px",
}}
>
Confirm Deletion
</DialogTitle>
<DialogContent sx={{ padding: "24px 32px" }}>
<DialogContentText
sx={{
textAlign: "center",
marginBottom: "24px",
color: theme.palette.text.primary,
fontSize: "1rem",
}}
>
Are you sure you want to delete queue "{queueToDelete}"?
This action cannot be undone.
</DialogContentText>
</DialogContent>
<DialogActions
sx={{
display: "flex",
justifyContent: "center",
padding: "0 32px 24px",
borderTop: `1px solid ${alpha(theme.palette.divider, 0.1)}`,
justifyContent: "space-between",
alignItems: "center",
}}
>
<Button
onClick={onClose}
variant="outlined"
color="error"
sx={{
textTransform: "none",
fontSize: "0.9rem",
fontWeight: 500,
padding: "8px 24px",
borderRadius: "8px",
border: `1px solid ${theme.palette.error.main}`,
color: theme.palette.error.main,
"&:hover": {
backgroundColor: alpha(
theme.palette.error.main,
0.08,
),
borderColor: theme.palette.error.dark,
},
}}
>
Cancel
</Button>
<Button
onClick={onConfirm}
variant="contained"
color="error"
sx={{
textTransform: "none",
fontSize: "0.9rem",
fontWeight: 600,
padding: "8px 24px",
marginLeft: "16px",
borderRadius: "8px",
backgroundColor: theme.palette.error.main,
"&:hover": {
backgroundColor: theme.palette.error.dark,
},
}}
>
Delete
</Button>
{showOnlyError ? "Error" : "Delete Queue"}
<IconButton onClick={onClose} size="small">
<CloseIcon />
</IconButton>
</DialogTitle>
<DialogContent>
{showOnlyError ? (
<Alert severity="error">{error}</Alert>
) : (
`Are you sure you want to delete queue "${queueToDelete}"? This action cannot be undone.`
)}
</DialogContent>
<DialogActions>
{!showOnlyError && (
<Button onClick={onClose} color="primary">
Cancel
</Button>
)}
{!showOnlyError && (
<Button
onClick={onConfirm}
color="error"
variant="contained"
>
Delete
</Button>
)}
</DialogActions>
</Dialog>
);

View File

@ -1,113 +0,0 @@
import React from "react";
import {
Container,
Row,
Col,
Form,
Button,
InputGroup,
Card,
} from "react-bootstrap";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import {
faSearch,
faTimes,
faSyncAlt,
} from "@fortawesome/free-solid-svg-icons";
const QueueToolbar = ({
searchText,
handleSearch,
handleClearSearch,
handleRefresh,
fetchQueues,
isRefreshing,
}) => {
return (
<Card className="mb-4 border-0 bg-light shadow-sm rounded-xl">
<Card.Body className="py-3">
<Container fluid className="px-0">
<Row className="align-items-center g-3">
<Col xs={12} md={5} lg={4} xl={3}>
<div className="position-relative">
<InputGroup
className="border rounded-pill overflow-hidden shadow-sm bg-white"
style={{ height: "40px" }}
>
<Button
variant="outline-white"
className="border-0 bg-transparent text-primary position-absolute start-0 d-flex align-items-center px-3"
onClick={() => fetchQueues()}
disabled={isRefreshing}
style={{ height: "100%" }}
>
<FontAwesomeIcon
icon={faSearch}
className="me-2 text-black"
/>
{isRefreshing && (
<span
className="spinner-border spinner-border-sm text-primary"
role="status"
></span>
)}
</Button>
<Form.Control
placeholder="Search queues..."
value={searchText}
onChange={handleSearch}
className="border-0 shadow-none px-4 ps-5"
style={{
fontSize: "0.9rem",
height: "100%",
}}
/>
{searchText && (
<Button
variant="outline-white"
className="border-0 bg-transparent position-absolute end-0 d-flex align-items-center px-3"
onClick={handleClearSearch}
disabled={isRefreshing}
style={{ height: "100%" }}
>
<FontAwesomeIcon
icon={faTimes}
className="text-secondary"
/>
</Button>
)}
</InputGroup>
</div>
</Col>
<Col className="d-flex justify-content-md-end">
<Button
variant="outline-danger"
size="lg"
className="rounded-pill px-5 py-2 d-flex align-items-center justify-content-center shadow-sm fw-medium border-2"
onClick={handleRefresh}
disabled={isRefreshing}
style={{
transition: "all 0.3s ease",
height: "40px",
}}
>
<FontAwesomeIcon
icon={faSyncAlt}
className="me-2"
spin={isRefreshing}
/>
<span>
{isRefreshing
? "Refreshing..."
: "Refresh Queue Status"}
</span>
</Button>
</Col>
</Row>
</Container>
</Card.Body>
</Card>
);
};
export default QueueToolbar;

View File

@ -1,7 +1,7 @@
import React, { useCallback, useEffect, useMemo, useState } from "react";
import { Box, Typography } from "@mui/material";
import axios from "axios";
import { parseCPU, parseMemoryToMi } from "../utils"; // Adjust this path based on your project structure
import { parseCPU, parseMemoryToMi } from "../utils";
import SearchBar from "../Searchbar";
import QueueTable from "./QueueTable/QueueTable";
import QueuePagination from "./QueuePagination";
@ -12,20 +12,13 @@ const Queues = () => {
const [queues, setQueues] = useState([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
const [filters, setFilters] = useState({
status: "All",
});
const [filters, setFilters] = useState({ status: "All" });
const [selectedQueueYaml, setSelectedQueueYaml] = useState("");
const [openDialog, setOpenDialog] = useState(false);
const [anchorEl, setAnchorEl] = useState({
status: null,
});
const [anchorEl, setAnchorEl] = useState({ status: null });
const [searchText, setSearchText] = useState("");
const [selectedQueueName, setSelectedQueueName] = useState("");
const [pagination, setPagination] = useState({
page: 1,
rowsPerPage: 10,
});
const [pagination, setPagination] = useState({ page: 1, rowsPerPage: 10 });
const [totalQueues, setTotalQueues] = useState(0);
const [sortConfig, setSortConfig] = useState({
field: null,
@ -35,7 +28,6 @@ const Queues = () => {
const fetchQueues = useCallback(async () => {
setLoading(true);
setError(null);
try {
const response = await axios.get(`/api/queues`, {
params: {
@ -65,16 +57,19 @@ const Queues = () => {
fetchQueues();
}, [fetchQueues]);
const handleSearch = (event) => {
const handleSearch = useCallback((event) => {
setSearchText(event.target.value);
setPagination((prev) => ({ ...prev, page: 1 }));
};
}, []);
const handleClearSearch = () => {
const handleClearSearch = useCallback(() => {
setSearchText("");
setPagination((prev) => ({ ...prev, page: 1 }));
}, []);
useEffect(() => {
fetchQueues();
};
}, [searchText, pagination.page, filters]);
const handleRefresh = useCallback(() => {
setPagination((prev) => ({ ...prev, page: 1 }));
@ -225,7 +220,7 @@ const Queues = () => {
handleClearSearch={handleClearSearch}
handleRefresh={handleRefresh}
fetchData={fetchQueues}
isRefreshing={false} // Update if needed
isRefreshing={loading}
placeholder="Search queues..."
refreshLabel="Refresh Queues"
/>

508
package-lock.json generated
View File

@ -25,7 +25,7 @@
"concurrently": "^9.1.2",
"husky": "^9.1.7",
"lint-staged": "^15.4.3",
"prettier": "^3.5.1",
"prettier": "^3.5.3",
"vitest": "^3.0.8"
}
},
@ -118,20 +118,20 @@
}
},
"frontend/node_modules/eslint": {
"version": "9.24.0",
"resolved": "https://registry.npmjs.org/eslint/-/eslint-9.24.0.tgz",
"integrity": "sha512-eh/jxIEJyZrvbWRe4XuVclLPDYSYYYgLy5zXGGxD6j8zjSAxFEzI2fL/8xNq6O2yKqVt+eF2YhV+hxjV6UKXwQ==",
"version": "9.25.1",
"resolved": "https://registry.npmjs.org/eslint/-/eslint-9.25.1.tgz",
"integrity": "sha512-E6Mtz9oGQWDCpV12319d59n4tx9zOTXSTmc8BLVxBx+G/0RdM5MvEEJLU9c0+aleoePYYgVTOsRblx433qmhWQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@eslint-community/eslint-utils": "^4.2.0",
"@eslint-community/regexpp": "^4.12.1",
"@eslint/config-array": "^0.20.0",
"@eslint/config-helpers": "^0.2.0",
"@eslint/core": "^0.12.0",
"@eslint/config-helpers": "^0.2.1",
"@eslint/core": "^0.13.0",
"@eslint/eslintrc": "^3.3.1",
"@eslint/js": "9.24.0",
"@eslint/plugin-kit": "^0.2.7",
"@eslint/js": "9.25.1",
"@eslint/plugin-kit": "^0.2.8",
"@humanfs/node": "^0.16.6",
"@humanwhocodes/module-importer": "^1.0.1",
"@humanwhocodes/retry": "^0.4.2",
@ -212,14 +212,14 @@
}
},
"node_modules/@asamuzakjp/css-color": {
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/@asamuzakjp/css-color/-/css-color-3.1.2.tgz",
"integrity": "sha512-nwgc7jPn3LpZ4JWsoHtuwBsad1qSSLDDX634DdG0PBJofIuIEtSWk4KkRmuXyu178tjuHAbwiMNNzwqIyLYxZw==",
"version": "3.1.4",
"resolved": "https://registry.npmjs.org/@asamuzakjp/css-color/-/css-color-3.1.4.tgz",
"integrity": "sha512-SeuBV4rnjpFNjI8HSgKUwteuFdkHwkboq31HWzznuqgySQir+jSTczoWVVL4jvOjKjuH80fMDG0Fvg1Sb+OJsA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@csstools/css-calc": "^2.1.2",
"@csstools/css-color-parser": "^3.0.8",
"@csstools/css-calc": "^2.1.3",
"@csstools/css-color-parser": "^3.0.9",
"@csstools/css-parser-algorithms": "^3.0.4",
"@csstools/css-tokenizer": "^3.0.3",
"lru-cache": "^10.4.3"
@ -2017,9 +2017,9 @@
}
},
"node_modules/@csstools/css-calc": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/@csstools/css-calc/-/css-calc-2.1.2.tgz",
"integrity": "sha512-TklMyb3uBB28b5uQdxjReG4L80NxAqgrECqLZFQbyLekwwlcDDS8r3f07DKqeo8C4926Br0gf/ZDe17Zv4wIuw==",
"version": "2.1.3",
"resolved": "https://registry.npmjs.org/@csstools/css-calc/-/css-calc-2.1.3.tgz",
"integrity": "sha512-XBG3talrhid44BY1x3MHzUx/aTG8+x/Zi57M4aTKK9RFB4aLlF3TTSzfzn8nWVHWL3FgAXAxmupmDd6VWww+pw==",
"dev": true,
"funding": [
{
@ -2041,9 +2041,9 @@
}
},
"node_modules/@csstools/css-color-parser": {
"version": "3.0.8",
"resolved": "https://registry.npmjs.org/@csstools/css-color-parser/-/css-color-parser-3.0.8.tgz",
"integrity": "sha512-pdwotQjCCnRPuNi06jFuP68cykU1f3ZWExLe/8MQ1LOs8Xq+fTkYgd+2V8mWUWMrOn9iS2HftPVaMZDaXzGbhQ==",
"version": "3.0.9",
"resolved": "https://registry.npmjs.org/@csstools/css-color-parser/-/css-color-parser-3.0.9.tgz",
"integrity": "sha512-wILs5Zk7BU86UArYBJTPy/FMPPKVKHMj1ycCEyf3VUptol0JNRLFU/BZsJ4aiIHJEbSLiizzRrw8Pc1uAEDrXw==",
"dev": true,
"funding": [
{
@ -2058,7 +2058,7 @@
"license": "MIT",
"dependencies": {
"@csstools/color-helpers": "^5.0.2",
"@csstools/css-calc": "^2.1.2"
"@csstools/css-calc": "^2.1.3"
},
"engines": {
"node": ">=18"
@ -2264,9 +2264,9 @@
"license": "MIT"
},
"node_modules/@esbuild/aix-ppc64": {
"version": "0.25.2",
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.2.tgz",
"integrity": "sha512-wCIboOL2yXZym2cgm6mlA742s9QeJ8DjGVaL39dLN4rRwrOgOyYSnOaFPhKZGLb2ngj4EyfAFjsNJwPXZvseag==",
"version": "0.25.3",
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.3.tgz",
"integrity": "sha512-W8bFfPA8DowP8l//sxjJLSLkD8iEjMc7cBVyP+u4cEv9sM7mdUCkgsj+t0n/BWPFtv7WWCN5Yzj0N6FJNUUqBQ==",
"cpu": [
"ppc64"
],
@ -2281,9 +2281,9 @@
}
},
"node_modules/@esbuild/android-arm": {
"version": "0.25.2",
"resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.2.tgz",
"integrity": "sha512-NQhH7jFstVY5x8CKbcfa166GoV0EFkaPkCKBQkdPJFvo5u+nGXLEH/ooniLb3QI8Fk58YAx7nsPLozUWfCBOJA==",
"version": "0.25.3",
"resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.3.tgz",
"integrity": "sha512-PuwVXbnP87Tcff5I9ngV0lmiSu40xw1At6i3GsU77U7cjDDB4s0X2cyFuBiDa1SBk9DnvWwnGvVaGBqoFWPb7A==",
"cpu": [
"arm"
],
@ -2298,9 +2298,9 @@
}
},
"node_modules/@esbuild/android-arm64": {
"version": "0.25.2",
"resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.2.tgz",
"integrity": "sha512-5ZAX5xOmTligeBaeNEPnPaeEuah53Id2tX4c2CVP3JaROTH+j4fnfHCkr1PjXMd78hMst+TlkfKcW/DlTq0i4w==",
"version": "0.25.3",
"resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.3.tgz",
"integrity": "sha512-XelR6MzjlZuBM4f5z2IQHK6LkK34Cvv6Rj2EntER3lwCBFdg6h2lKbtRjpTTsdEjD/WSe1q8UyPBXP1x3i/wYQ==",
"cpu": [
"arm64"
],
@ -2315,9 +2315,9 @@
}
},
"node_modules/@esbuild/android-x64": {
"version": "0.25.2",
"resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.2.tgz",
"integrity": "sha512-Ffcx+nnma8Sge4jzddPHCZVRvIfQ0kMsUsCMcJRHkGJ1cDmhe4SsrYIjLUKn1xpHZybmOqCWwB0zQvsjdEHtkg==",
"version": "0.25.3",
"resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.3.tgz",
"integrity": "sha512-ogtTpYHT/g1GWS/zKM0cc/tIebFjm1F9Aw1boQ2Y0eUQ+J89d0jFY//s9ei9jVIlkYi8AfOjiixcLJSGNSOAdQ==",
"cpu": [
"x64"
],
@ -2332,9 +2332,9 @@
}
},
"node_modules/@esbuild/darwin-arm64": {
"version": "0.25.2",
"resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.2.tgz",
"integrity": "sha512-MpM6LUVTXAzOvN4KbjzU/q5smzryuoNjlriAIx+06RpecwCkL9JpenNzpKd2YMzLJFOdPqBpuub6eVRP5IgiSA==",
"version": "0.25.3",
"resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.3.tgz",
"integrity": "sha512-eESK5yfPNTqpAmDfFWNsOhmIOaQA59tAcF/EfYvo5/QWQCzXn5iUSOnqt3ra3UdzBv073ykTtmeLJZGt3HhA+w==",
"cpu": [
"arm64"
],
@ -2349,9 +2349,9 @@
}
},
"node_modules/@esbuild/darwin-x64": {
"version": "0.25.2",
"resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.2.tgz",
"integrity": "sha512-5eRPrTX7wFyuWe8FqEFPG2cU0+butQQVNcT4sVipqjLYQjjh8a8+vUTfgBKM88ObB85ahsnTwF7PSIt6PG+QkA==",
"version": "0.25.3",
"resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.3.tgz",
"integrity": "sha512-Kd8glo7sIZtwOLcPbW0yLpKmBNWMANZhrC1r6K++uDR2zyzb6AeOYtI6udbtabmQpFaxJ8uduXMAo1gs5ozz8A==",
"cpu": [
"x64"
],
@ -2366,9 +2366,9 @@
}
},
"node_modules/@esbuild/freebsd-arm64": {
"version": "0.25.2",
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.2.tgz",
"integrity": "sha512-mLwm4vXKiQ2UTSX4+ImyiPdiHjiZhIaE9QvC7sw0tZ6HoNMjYAqQpGyui5VRIi5sGd+uWq940gdCbY3VLvsO1w==",
"version": "0.25.3",
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.3.tgz",
"integrity": "sha512-EJiyS70BYybOBpJth3M0KLOus0n+RRMKTYzhYhFeMwp7e/RaajXvP+BWlmEXNk6uk+KAu46j/kaQzr6au+JcIw==",
"cpu": [
"arm64"
],
@ -2383,9 +2383,9 @@
}
},
"node_modules/@esbuild/freebsd-x64": {
"version": "0.25.2",
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.2.tgz",
"integrity": "sha512-6qyyn6TjayJSwGpm8J9QYYGQcRgc90nmfdUb0O7pp1s4lTY+9D0H9O02v5JqGApUyiHOtkz6+1hZNvNtEhbwRQ==",
"version": "0.25.3",
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.3.tgz",
"integrity": "sha512-Q+wSjaLpGxYf7zC0kL0nDlhsfuFkoN+EXrx2KSB33RhinWzejOd6AvgmP5JbkgXKmjhmpfgKZq24pneodYqE8Q==",
"cpu": [
"x64"
],
@ -2400,9 +2400,9 @@
}
},
"node_modules/@esbuild/linux-arm": {
"version": "0.25.2",
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.2.tgz",
"integrity": "sha512-UHBRgJcmjJv5oeQF8EpTRZs/1knq6loLxTsjc3nxO9eXAPDLcWW55flrMVc97qFPbmZP31ta1AZVUKQzKTzb0g==",
"version": "0.25.3",
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.3.tgz",
"integrity": "sha512-dUOVmAUzuHy2ZOKIHIKHCm58HKzFqd+puLaS424h6I85GlSDRZIA5ycBixb3mFgM0Jdh+ZOSB6KptX30DD8YOQ==",
"cpu": [
"arm"
],
@ -2417,9 +2417,9 @@
}
},
"node_modules/@esbuild/linux-arm64": {
"version": "0.25.2",
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.2.tgz",
"integrity": "sha512-gq/sjLsOyMT19I8obBISvhoYiZIAaGF8JpeXu1u8yPv8BE5HlWYobmlsfijFIZ9hIVGYkbdFhEqC0NvM4kNO0g==",
"version": "0.25.3",
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.3.tgz",
"integrity": "sha512-xCUgnNYhRD5bb1C1nqrDV1PfkwgbswTTBRbAd8aH5PhYzikdf/ddtsYyMXFfGSsb/6t6QaPSzxtbfAZr9uox4A==",
"cpu": [
"arm64"
],
@ -2434,9 +2434,9 @@
}
},
"node_modules/@esbuild/linux-ia32": {
"version": "0.25.2",
"resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.2.tgz",
"integrity": "sha512-bBYCv9obgW2cBP+2ZWfjYTU+f5cxRoGGQ5SeDbYdFCAZpYWrfjjfYwvUpP8MlKbP0nwZ5gyOU/0aUzZ5HWPuvQ==",
"version": "0.25.3",
"resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.3.tgz",
"integrity": "sha512-yplPOpczHOO4jTYKmuYuANI3WhvIPSVANGcNUeMlxH4twz/TeXuzEP41tGKNGWJjuMhotpGabeFYGAOU2ummBw==",
"cpu": [
"ia32"
],
@ -2451,9 +2451,9 @@
}
},
"node_modules/@esbuild/linux-loong64": {
"version": "0.25.2",
"resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.2.tgz",
"integrity": "sha512-SHNGiKtvnU2dBlM5D8CXRFdd+6etgZ9dXfaPCeJtz+37PIUlixvlIhI23L5khKXs3DIzAn9V8v+qb1TRKrgT5w==",
"version": "0.25.3",
"resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.3.tgz",
"integrity": "sha512-P4BLP5/fjyihmXCELRGrLd793q/lBtKMQl8ARGpDxgzgIKJDRJ/u4r1A/HgpBpKpKZelGct2PGI4T+axcedf6g==",
"cpu": [
"loong64"
],
@ -2468,9 +2468,9 @@
}
},
"node_modules/@esbuild/linux-mips64el": {
"version": "0.25.2",
"resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.2.tgz",
"integrity": "sha512-hDDRlzE6rPeoj+5fsADqdUZl1OzqDYow4TB4Y/3PlKBD0ph1e6uPHzIQcv2Z65u2K0kpeByIyAjCmjn1hJgG0Q==",
"version": "0.25.3",
"resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.3.tgz",
"integrity": "sha512-eRAOV2ODpu6P5divMEMa26RRqb2yUoYsuQQOuFUexUoQndm4MdpXXDBbUoKIc0iPa4aCO7gIhtnYomkn2x+bag==",
"cpu": [
"mips64el"
],
@ -2485,9 +2485,9 @@
}
},
"node_modules/@esbuild/linux-ppc64": {
"version": "0.25.2",
"resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.2.tgz",
"integrity": "sha512-tsHu2RRSWzipmUi9UBDEzc0nLc4HtpZEI5Ba+Omms5456x5WaNuiG3u7xh5AO6sipnJ9r4cRWQB2tUjPyIkc6g==",
"version": "0.25.3",
"resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.3.tgz",
"integrity": "sha512-ZC4jV2p7VbzTlnl8nZKLcBkfzIf4Yad1SJM4ZMKYnJqZFD4rTI+pBG65u8ev4jk3/MPwY9DvGn50wi3uhdaghg==",
"cpu": [
"ppc64"
],
@ -2502,9 +2502,9 @@
}
},
"node_modules/@esbuild/linux-riscv64": {
"version": "0.25.2",
"resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.2.tgz",
"integrity": "sha512-k4LtpgV7NJQOml/10uPU0s4SAXGnowi5qBSjaLWMojNCUICNu7TshqHLAEbkBdAszL5TabfvQ48kK84hyFzjnw==",
"version": "0.25.3",
"resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.3.tgz",
"integrity": "sha512-LDDODcFzNtECTrUUbVCs6j9/bDVqy7DDRsuIXJg6so+mFksgwG7ZVnTruYi5V+z3eE5y+BJZw7VvUadkbfg7QA==",
"cpu": [
"riscv64"
],
@ -2519,9 +2519,9 @@
}
},
"node_modules/@esbuild/linux-s390x": {
"version": "0.25.2",
"resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.2.tgz",
"integrity": "sha512-GRa4IshOdvKY7M/rDpRR3gkiTNp34M0eLTaC1a08gNrh4u488aPhuZOCpkF6+2wl3zAN7L7XIpOFBhnaE3/Q8Q==",
"version": "0.25.3",
"resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.3.tgz",
"integrity": "sha512-s+w/NOY2k0yC2p9SLen+ymflgcpRkvwwa02fqmAwhBRI3SC12uiS10edHHXlVWwfAagYSY5UpmT/zISXPMW3tQ==",
"cpu": [
"s390x"
],
@ -2536,9 +2536,9 @@
}
},
"node_modules/@esbuild/linux-x64": {
"version": "0.25.2",
"resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.2.tgz",
"integrity": "sha512-QInHERlqpTTZ4FRB0fROQWXcYRD64lAoiegezDunLpalZMjcUcld3YzZmVJ2H/Cp0wJRZ8Xtjtj0cEHhYc/uUg==",
"version": "0.25.3",
"resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.3.tgz",
"integrity": "sha512-nQHDz4pXjSDC6UfOE1Fw9Q8d6GCAd9KdvMZpfVGWSJztYCarRgSDfOVBY5xwhQXseiyxapkiSJi/5/ja8mRFFA==",
"cpu": [
"x64"
],
@ -2553,9 +2553,9 @@
}
},
"node_modules/@esbuild/netbsd-arm64": {
"version": "0.25.2",
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.2.tgz",
"integrity": "sha512-talAIBoY5M8vHc6EeI2WW9d/CkiO9MQJ0IOWX8hrLhxGbro/vBXJvaQXefW2cP0z0nQVTdQ/eNyGFV1GSKrxfw==",
"version": "0.25.3",
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.3.tgz",
"integrity": "sha512-1QaLtOWq0mzK6tzzp0jRN3eccmN3hezey7mhLnzC6oNlJoUJz4nym5ZD7mDnS/LZQgkrhEbEiTn515lPeLpgWA==",
"cpu": [
"arm64"
],
@ -2570,9 +2570,9 @@
}
},
"node_modules/@esbuild/netbsd-x64": {
"version": "0.25.2",
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.2.tgz",
"integrity": "sha512-voZT9Z+tpOxrvfKFyfDYPc4DO4rk06qamv1a/fkuzHpiVBMOhpjK+vBmWM8J1eiB3OLSMFYNaOaBNLXGChf5tg==",
"version": "0.25.3",
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.3.tgz",
"integrity": "sha512-i5Hm68HXHdgv8wkrt+10Bc50zM0/eonPb/a/OFVfB6Qvpiirco5gBA5bz7S2SHuU+Y4LWn/zehzNX14Sp4r27g==",
"cpu": [
"x64"
],
@ -2587,9 +2587,9 @@
}
},
"node_modules/@esbuild/openbsd-arm64": {
"version": "0.25.2",
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.2.tgz",
"integrity": "sha512-dcXYOC6NXOqcykeDlwId9kB6OkPUxOEqU+rkrYVqJbK2hagWOMrsTGsMr8+rW02M+d5Op5NNlgMmjzecaRf7Tg==",
"version": "0.25.3",
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.3.tgz",
"integrity": "sha512-zGAVApJEYTbOC6H/3QBr2mq3upG/LBEXr85/pTtKiv2IXcgKV0RT0QA/hSXZqSvLEpXeIxah7LczB4lkiYhTAQ==",
"cpu": [
"arm64"
],
@ -2604,9 +2604,9 @@
}
},
"node_modules/@esbuild/openbsd-x64": {
"version": "0.25.2",
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.2.tgz",
"integrity": "sha512-t/TkWwahkH0Tsgoq1Ju7QfgGhArkGLkF1uYz8nQS/PPFlXbP5YgRpqQR3ARRiC2iXoLTWFxc6DJMSK10dVXluw==",
"version": "0.25.3",
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.3.tgz",
"integrity": "sha512-fpqctI45NnCIDKBH5AXQBsD0NDPbEFczK98hk/aa6HJxbl+UtLkJV2+Bvy5hLSLk3LHmqt0NTkKNso1A9y1a4w==",
"cpu": [
"x64"
],
@ -2621,9 +2621,9 @@
}
},
"node_modules/@esbuild/sunos-x64": {
"version": "0.25.2",
"resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.2.tgz",
"integrity": "sha512-cfZH1co2+imVdWCjd+D1gf9NjkchVhhdpgb1q5y6Hcv9TP6Zi9ZG/beI3ig8TvwT9lH9dlxLq5MQBBgwuj4xvA==",
"version": "0.25.3",
"resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.3.tgz",
"integrity": "sha512-ROJhm7d8bk9dMCUZjkS8fgzsPAZEjtRJqCAmVgB0gMrvG7hfmPmz9k1rwO4jSiblFjYmNvbECL9uhaPzONMfgA==",
"cpu": [
"x64"
],
@ -2638,9 +2638,9 @@
}
},
"node_modules/@esbuild/win32-arm64": {
"version": "0.25.2",
"resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.2.tgz",
"integrity": "sha512-7Loyjh+D/Nx/sOTzV8vfbB3GJuHdOQyrOryFdZvPHLf42Tk9ivBU5Aedi7iyX+x6rbn2Mh68T4qq1SDqJBQO5Q==",
"version": "0.25.3",
"resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.3.tgz",
"integrity": "sha512-YWcow8peiHpNBiIXHwaswPnAXLsLVygFwCB3A7Bh5jRkIBFWHGmNQ48AlX4xDvQNoMZlPYzjVOQDYEzWCqufMQ==",
"cpu": [
"arm64"
],
@ -2655,9 +2655,9 @@
}
},
"node_modules/@esbuild/win32-ia32": {
"version": "0.25.2",
"resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.2.tgz",
"integrity": "sha512-WRJgsz9un0nqZJ4MfhabxaD9Ft8KioqU3JMinOTvobbX6MOSUigSBlogP8QB3uxpJDsFS6yN+3FDBdqE5lg9kg==",
"version": "0.25.3",
"resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.3.tgz",
"integrity": "sha512-qspTZOIGoXVS4DpNqUYUs9UxVb04khS1Degaw/MnfMe7goQ3lTfQ13Vw4qY/Nj0979BGvMRpAYbs/BAxEvU8ew==",
"cpu": [
"ia32"
],
@ -2672,9 +2672,9 @@
}
},
"node_modules/@esbuild/win32-x64": {
"version": "0.25.2",
"resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.2.tgz",
"integrity": "sha512-kM3HKb16VIXZyIeVrM1ygYmZBKybX8N4p754bw390wGO3Tf2j4L2/WYL+4suWujpgf6GBYs3jv7TyUivdd05JA==",
"version": "0.25.3",
"resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.3.tgz",
"integrity": "sha512-ICgUR+kPimx0vvRzf+N/7L7tVSQeE3BYY+NhHRHXS1kBuPO7z2+7ea2HbhDyZdTephgvNvKrlDDKUexuCVBVvg==",
"cpu": [
"x64"
],
@ -2756,9 +2756,9 @@
}
},
"node_modules/@eslint/core": {
"version": "0.12.0",
"resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.12.0.tgz",
"integrity": "sha512-cmrR6pytBuSMTaBweKoGMwu3EiHiEC+DoyupPmlZ0HxBJBtIxwe+j/E4XPIKNx+Q74c8lXKPwYawBf5glsTkHg==",
"version": "0.13.0",
"resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.13.0.tgz",
"integrity": "sha512-yfkgDw1KR66rkT5A8ci4irzDysN7FRpq3ttJolR88OqQikAWqwA8j5VZyas+vjyBNFIJ7MfybJ9plMILI2UrCw==",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
@ -2806,9 +2806,9 @@
}
},
"node_modules/@eslint/js": {
"version": "9.24.0",
"resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.24.0.tgz",
"integrity": "sha512-uIY/y3z0uvOGX8cp1C2fiC4+ZmBhp6yZWkojtHL1YEMnRt1Y63HB9TM17proGEmeG7HeUY+UP36F0aknKYTpYA==",
"version": "9.25.1",
"resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.25.1.tgz",
"integrity": "sha512-dEIwmjntEx8u3Uvv+kr3PDeeArL8Hw07H9kyYxCjnM9pBjfEhk6uLXSchxxzgiwtRhhzVzqmUSDFBOi1TuZ7qg==",
"dev": true,
"license": "MIT",
"engines": {
@ -2839,19 +2839,6 @@
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
}
},
"node_modules/@eslint/plugin-kit/node_modules/@eslint/core": {
"version": "0.13.0",
"resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.13.0.tgz",
"integrity": "sha512-yfkgDw1KR66rkT5A8ci4irzDysN7FRpq3ttJolR88OqQikAWqwA8j5VZyas+vjyBNFIJ7MfybJ9plMILI2UrCw==",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
"@types/json-schema": "^7.0.15"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
}
},
"node_modules/@fortawesome/fontawesome-common-types": {
"version": "6.7.2",
"resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-common-types/-/fontawesome-common-types-6.7.2.tgz",
@ -3450,6 +3437,19 @@
"license": "MIT",
"optional": true
},
"node_modules/@noble/hashes": {
"version": "1.8.0",
"resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.8.0.tgz",
"integrity": "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==",
"dev": true,
"license": "MIT",
"engines": {
"node": "^14.21.3 || >=16"
},
"funding": {
"url": "https://paulmillr.com/funding/"
}
},
"node_modules/@nodelib/fs.scandir": {
"version": "2.1.5",
"resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
@ -3488,6 +3488,16 @@
"node": ">= 8"
}
},
"node_modules/@paralleldrive/cuid2": {
"version": "2.2.2",
"resolved": "https://registry.npmjs.org/@paralleldrive/cuid2/-/cuid2-2.2.2.tgz",
"integrity": "sha512-ZOBkgDwEdoYVlSeRbYYXs0S9MejQofiVYoTbKzy/6GQa39/q5tQU2IX46+shYnUkpEl3wc+J6wRlar7r2EK2xA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@noble/hashes": "^1.1.5"
}
},
"node_modules/@pkgjs/parseargs": {
"version": "0.11.0",
"resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz",
@ -4375,9 +4385,9 @@
"peer": true
},
"node_modules/@vitejs/plugin-react": {
"version": "4.4.0",
"resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.4.0.tgz",
"integrity": "sha512-x/EztcTKVj+TDeANY1WjNeYsvZjZdfWRMP/KXi5Yn8BoTzpa13ZltaQqKfvWYbX8CE10GOHHdC5v86jY9x8i/g==",
"version": "4.4.1",
"resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.4.1.tgz",
"integrity": "sha512-IpEm5ZmeXAP/osiBXVVP5KjFMzbWOonMs0NaQQl+xYnUAcq4oHUBsF2+p4MgKWG4YMmFYJU8A6sxRPuowllm6w==",
"dev": true,
"license": "MIT",
"dependencies": {
@ -4395,16 +4405,16 @@
}
},
"node_modules/@vitest/browser": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/@vitest/browser/-/browser-3.1.1.tgz",
"integrity": "sha512-A+A69mMtrj1RPh96LfXGc309KSXhy2MslvyL+cp9+Y5EVdoJD4KfXDx/3SSlRGN70+hIoJ3RRbTidTvj18PZ/A==",
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/@vitest/browser/-/browser-3.1.2.tgz",
"integrity": "sha512-dwL6hQg3NSDP3Z4xzIZL0xHq/AkQAPQ4StFpWVlY2zbRJtK3Y2YqdFZ7YmZjszTETN1BDQZRn/QOrcP+c8ATgg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@testing-library/dom": "^10.4.0",
"@testing-library/user-event": "^14.6.1",
"@vitest/mocker": "3.1.1",
"@vitest/utils": "3.1.1",
"@vitest/mocker": "3.1.2",
"@vitest/utils": "3.1.2",
"magic-string": "^0.30.17",
"sirv": "^3.0.1",
"tinyrainbow": "^2.0.0",
@ -4415,7 +4425,7 @@
},
"peerDependencies": {
"playwright": "*",
"vitest": "3.1.1",
"vitest": "3.1.2",
"webdriverio": "^7.0.0 || ^8.0.0 || ^9.0.0"
},
"peerDependenciesMeta": {
@ -4431,14 +4441,14 @@
}
},
"node_modules/@vitest/expect": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-3.1.1.tgz",
"integrity": "sha512-q/zjrW9lgynctNbwvFtQkGK9+vvHA5UzVi2V8APrp1C6fG6/MuYYkmlx4FubuqLycCeSdHD5aadWfua/Vr0EUA==",
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-3.1.2.tgz",
"integrity": "sha512-O8hJgr+zREopCAqWl3uCVaOdqJwZ9qaDwUP7vy3Xigad0phZe9APxKhPcDNqYYi0rX5oMvwJMSCAXY2afqeTSA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@vitest/spy": "3.1.1",
"@vitest/utils": "3.1.1",
"@vitest/spy": "3.1.2",
"@vitest/utils": "3.1.2",
"chai": "^5.2.0",
"tinyrainbow": "^2.0.0"
},
@ -4447,13 +4457,13 @@
}
},
"node_modules/@vitest/mocker": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-3.1.1.tgz",
"integrity": "sha512-bmpJJm7Y7i9BBELlLuuM1J1Q6EQ6K5Ye4wcyOpOMXMcePYKSIYlpcrCm4l/O6ja4VJA5G2aMJiuZkZdnxlC3SA==",
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-3.1.2.tgz",
"integrity": "sha512-kOtd6K2lc7SQ0mBqYv/wdGedlqPdM/B38paPY+OwJ1XiNi44w3Fpog82UfOibmHaV9Wod18A09I9SCKLyDMqgw==",
"dev": true,
"license": "MIT",
"dependencies": {
"@vitest/spy": "3.1.1",
"@vitest/spy": "3.1.2",
"estree-walker": "^3.0.3",
"magic-string": "^0.30.17"
},
@ -4474,9 +4484,9 @@
}
},
"node_modules/@vitest/pretty-format": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-3.1.1.tgz",
"integrity": "sha512-dg0CIzNx+hMMYfNmSqJlLSXEmnNhMswcn3sXO7Tpldr0LiGmg3eXdLLhwkv2ZqgHb/d5xg5F7ezNFRA1fA13yA==",
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-3.1.2.tgz",
"integrity": "sha512-R0xAiHuWeDjTSB3kQ3OQpT8Rx3yhdOAIm/JM4axXxnG7Q/fS8XUwggv/A4xzbQA+drYRjzkMnpYnOGAc4oeq8w==",
"dev": true,
"license": "MIT",
"dependencies": {
@ -4487,13 +4497,13 @@
}
},
"node_modules/@vitest/runner": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-3.1.1.tgz",
"integrity": "sha512-X/d46qzJuEDO8ueyjtKfxffiXraPRfmYasoC4i5+mlLEJ10UvPb0XH5M9C3gWuxd7BAQhpK42cJgJtq53YnWVA==",
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-3.1.2.tgz",
"integrity": "sha512-bhLib9l4xb4sUMPXnThbnhX2Yi8OutBMA8Yahxa7yavQsFDtwY/jrUZwpKp2XH9DhRFJIeytlyGpXCqZ65nR+g==",
"dev": true,
"license": "MIT",
"dependencies": {
"@vitest/utils": "3.1.1",
"@vitest/utils": "3.1.2",
"pathe": "^2.0.3"
},
"funding": {
@ -4501,13 +4511,13 @@
}
},
"node_modules/@vitest/snapshot": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-3.1.1.tgz",
"integrity": "sha512-bByMwaVWe/+1WDf9exFxWWgAixelSdiwo2p33tpqIlM14vW7PRV5ppayVXtfycqze4Qhtwag5sVhX400MLBOOw==",
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-3.1.2.tgz",
"integrity": "sha512-Q1qkpazSF/p4ApZg1vfZSQ5Yw6OCQxVMVrLjslbLFA1hMDrT2uxtqMaw8Tc/jy5DLka1sNs1Y7rBcftMiaSH/Q==",
"dev": true,
"license": "MIT",
"dependencies": {
"@vitest/pretty-format": "3.1.1",
"@vitest/pretty-format": "3.1.2",
"magic-string": "^0.30.17",
"pathe": "^2.0.3"
},
@ -4516,9 +4526,9 @@
}
},
"node_modules/@vitest/spy": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-3.1.1.tgz",
"integrity": "sha512-+EmrUOOXbKzLkTDwlsc/xrwOlPDXyVk3Z6P6K4oiCndxz7YLpp/0R0UsWVOKT0IXWjjBJuSMk6D27qipaupcvQ==",
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-3.1.2.tgz",
"integrity": "sha512-OEc5fSXMws6sHVe4kOFyDSj/+4MSwst0ib4un0DlcYgQvRuYQ0+M2HyqGaauUMnjq87tmUaMNDxKQx7wNfVqPA==",
"dev": true,
"license": "MIT",
"dependencies": {
@ -4529,35 +4539,35 @@
}
},
"node_modules/@vitest/ui": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/@vitest/ui/-/ui-3.1.1.tgz",
"integrity": "sha512-2HpiRIYg3dlvAJBV9RtsVswFgUSJK4Sv7QhpxoP0eBGkYwzGIKP34PjaV00AULQi9Ovl6LGyZfsetxDWY5BQdQ==",
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/@vitest/ui/-/ui-3.1.2.tgz",
"integrity": "sha512-+YPgKiLpFEyBVJNHDkRcSDcLrrnr20lyU4HQoI9Jtq1MdvoX8usql9h38mQw82MBU1Zo5BPC6sw+sXZ6NS18CQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@vitest/utils": "3.1.1",
"@vitest/utils": "3.1.2",
"fflate": "^0.8.2",
"flatted": "^3.3.3",
"pathe": "^2.0.3",
"sirv": "^3.0.1",
"tinyglobby": "^0.2.12",
"tinyglobby": "^0.2.13",
"tinyrainbow": "^2.0.0"
},
"funding": {
"url": "https://opencollective.com/vitest"
},
"peerDependencies": {
"vitest": "3.1.1"
"vitest": "3.1.2"
}
},
"node_modules/@vitest/utils": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-3.1.1.tgz",
"integrity": "sha512-1XIjflyaU2k3HMArJ50bwSh3wKWPD6Q47wz/NUSmRV0zNywPc4w79ARjg/i/aNINHwA+mIALhUVqD9/aUvZNgg==",
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-3.1.2.tgz",
"integrity": "sha512-5GGd0ytZ7BH3H6JTj9Kw7Prn1Nbg0wZVrIvou+UWxm54d+WoXXgAgjFJ8wn3LdagWLFSEfpPeyYrByZaGEZHLg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@vitest/pretty-format": "3.1.1",
"@vitest/pretty-format": "3.1.2",
"loupe": "^3.1.3",
"tinyrainbow": "^2.0.0"
},
@ -5224,9 +5234,9 @@
}
},
"node_modules/caniuse-lite": {
"version": "1.0.30001714",
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001714.tgz",
"integrity": "sha512-mtgapdwDLSSBnCI3JokHM7oEQBLxiJKVRtg10AxM1AyeiKcM96f0Mkbqeq+1AbiCtvMcHRulAAEMu693JrSWqg==",
"version": "1.0.30001715",
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001715.tgz",
"integrity": "sha512-7ptkFGMm2OAOgvZpwgA4yjQ5SQbrNVGdRjzH0pBdy1Fasvcr+KAeECmbCAECzTuDuoX0FCY8KzUxjf9+9kfZEw==",
"funding": [
{
"type": "opencollective",
@ -5717,13 +5727,13 @@
"license": "MIT"
},
"node_modules/cssstyle": {
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-4.3.0.tgz",
"integrity": "sha512-6r0NiY0xizYqfBvWp1G7WXJ06/bZyrk7Dc6PHql82C/pKGUTKu4yAX4Y8JPamb1ob9nBKuxWzCGTRuGwU3yxJQ==",
"version": "4.3.1",
"resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-4.3.1.tgz",
"integrity": "sha512-ZgW+Jgdd7i52AaLYCriF8Mxqft0gD/R9i9wi6RWBhs1pqdPEzPjym7rvRKi397WmQFf3SlyUsszhw+VVCbx79Q==",
"dev": true,
"license": "MIT",
"dependencies": {
"@asamuzakjp/css-color": "^3.1.1",
"@asamuzakjp/css-color": "^3.1.2",
"rrweb-cssom": "^0.8.0"
},
"engines": {
@ -6035,9 +6045,9 @@
"license": "MIT"
},
"node_modules/electron-to-chromium": {
"version": "1.5.138",
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.138.tgz",
"integrity": "sha512-FWlQc52z1dXqm+9cCJ2uyFgJkESd+16j6dBEjsgDNuHjBpuIzL8/lRc0uvh1k8RNI6waGo6tcy2DvwkTBJOLDg==",
"version": "1.5.140",
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.140.tgz",
"integrity": "sha512-o82Rj+ONp4Ip7Cl1r7lrqx/pXhbp/lh9DpKcMNscFJdh8ebyRofnc7Sh01B4jx403RI0oqTBvlZ7OBIZLMr2+Q==",
"license": "ISC"
},
"node_modules/emoji-regex": {
@ -6057,9 +6067,9 @@
}
},
"node_modules/entities": {
"version": "4.5.0",
"resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz",
"integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==",
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/entities/-/entities-6.0.0.tgz",
"integrity": "sha512-aKstq2TDOndCn4diEyp9Uq/Flu2i1GlLkc6XIDQSDMuaFE3OPW5OphLCyQ5SpSJZTb4reN+kTcYru5yIfXoRPw==",
"dev": true,
"license": "BSD-2-Clause",
"engines": {
@ -6204,9 +6214,9 @@
}
},
"node_modules/es-module-lexer": {
"version": "1.6.0",
"resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.6.0.tgz",
"integrity": "sha512-qqnD1yMU6tk/jnaMosogGySTZP8YtUgAffA9nMN+E/rjxcfRQ6IEk7IiozUjgxKoFHBGjTLnrHB/YC45r/59EQ==",
"version": "1.7.0",
"resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.7.0.tgz",
"integrity": "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==",
"dev": true,
"license": "MIT"
},
@ -6269,9 +6279,9 @@
}
},
"node_modules/esbuild": {
"version": "0.25.2",
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.2.tgz",
"integrity": "sha512-16854zccKPnC+toMywC+uKNeYSv+/eXkevRAfwRD/G9Cleq66m8XFIrigkbvauLLlCfDL45Q2cWegSg53gGBnQ==",
"version": "0.25.3",
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.3.tgz",
"integrity": "sha512-qKA6Pvai73+M2FtftpNKRxJ78GIjmFXFxd/1DVBqGo/qNhLSfv+G12n9pNoWdytJC8U00TrViOwpjT0zgqQS8Q==",
"dev": true,
"hasInstallScript": true,
"license": "MIT",
@ -6282,31 +6292,31 @@
"node": ">=18"
},
"optionalDependencies": {
"@esbuild/aix-ppc64": "0.25.2",
"@esbuild/android-arm": "0.25.2",
"@esbuild/android-arm64": "0.25.2",
"@esbuild/android-x64": "0.25.2",
"@esbuild/darwin-arm64": "0.25.2",
"@esbuild/darwin-x64": "0.25.2",
"@esbuild/freebsd-arm64": "0.25.2",
"@esbuild/freebsd-x64": "0.25.2",
"@esbuild/linux-arm": "0.25.2",
"@esbuild/linux-arm64": "0.25.2",
"@esbuild/linux-ia32": "0.25.2",
"@esbuild/linux-loong64": "0.25.2",
"@esbuild/linux-mips64el": "0.25.2",
"@esbuild/linux-ppc64": "0.25.2",
"@esbuild/linux-riscv64": "0.25.2",
"@esbuild/linux-s390x": "0.25.2",
"@esbuild/linux-x64": "0.25.2",
"@esbuild/netbsd-arm64": "0.25.2",
"@esbuild/netbsd-x64": "0.25.2",
"@esbuild/openbsd-arm64": "0.25.2",
"@esbuild/openbsd-x64": "0.25.2",
"@esbuild/sunos-x64": "0.25.2",
"@esbuild/win32-arm64": "0.25.2",
"@esbuild/win32-ia32": "0.25.2",
"@esbuild/win32-x64": "0.25.2"
"@esbuild/aix-ppc64": "0.25.3",
"@esbuild/android-arm": "0.25.3",
"@esbuild/android-arm64": "0.25.3",
"@esbuild/android-x64": "0.25.3",
"@esbuild/darwin-arm64": "0.25.3",
"@esbuild/darwin-x64": "0.25.3",
"@esbuild/freebsd-arm64": "0.25.3",
"@esbuild/freebsd-x64": "0.25.3",
"@esbuild/linux-arm": "0.25.3",
"@esbuild/linux-arm64": "0.25.3",
"@esbuild/linux-ia32": "0.25.3",
"@esbuild/linux-loong64": "0.25.3",
"@esbuild/linux-mips64el": "0.25.3",
"@esbuild/linux-ppc64": "0.25.3",
"@esbuild/linux-riscv64": "0.25.3",
"@esbuild/linux-s390x": "0.25.3",
"@esbuild/linux-x64": "0.25.3",
"@esbuild/netbsd-arm64": "0.25.3",
"@esbuild/netbsd-x64": "0.25.3",
"@esbuild/openbsd-arm64": "0.25.3",
"@esbuild/openbsd-x64": "0.25.3",
"@esbuild/sunos-x64": "0.25.3",
"@esbuild/win32-arm64": "0.25.3",
"@esbuild/win32-ia32": "0.25.3",
"@esbuild/win32-x64": "0.25.3"
}
},
"node_modules/escalade": {
@ -6441,9 +6451,9 @@
}
},
"node_modules/eslint-plugin-react-refresh": {
"version": "0.4.19",
"resolved": "https://registry.npmjs.org/eslint-plugin-react-refresh/-/eslint-plugin-react-refresh-0.4.19.tgz",
"integrity": "sha512-eyy8pcr/YxSYjBoqIFSrlbn9i/xvxUFa8CjzAYo9cFjgGXqq1hyjihcpZvxRLalpaWmueWR81xn7vuKmAFijDQ==",
"version": "0.4.20",
"resolved": "https://registry.npmjs.org/eslint-plugin-react-refresh/-/eslint-plugin-react-refresh-0.4.20.tgz",
"integrity": "sha512-XpbHQ2q5gUF8BGOX4dHe+71qoirYMhApEPZ7sfhF/dNnOF1UXnCMGZf79SFTBO7Bz5YEIT4TMieSlJBWhP9WBA==",
"dev": true,
"license": "MIT",
"peerDependencies": {
@ -7574,16 +7584,6 @@
"he": "bin/he"
}
},
"node_modules/hexoid": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/hexoid/-/hexoid-2.0.0.tgz",
"integrity": "sha512-qlspKUK7IlSQv2o+5I7yhUd7TxlOG2Vr5LTa3ve2XSNVKAL/n/u/7KLvKmFNimomDIKvZFXWHv0T12mv7rT8Aw==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=8"
}
},
"node_modules/hoist-non-react-statics": {
"version": "3.3.2",
"resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz",
@ -9295,9 +9295,9 @@
"license": "MIT"
},
"node_modules/nodemon": {
"version": "3.1.9",
"resolved": "https://registry.npmjs.org/nodemon/-/nodemon-3.1.9.tgz",
"integrity": "sha512-hdr1oIb2p6ZSxu3PB2JWWYS7ZQ0qvaZsc3hK8DR8f02kRzc8rjYmxAIvdz+aYC+8F2IjNaB7HMcSDg8nQpJxyg==",
"version": "3.1.10",
"resolved": "https://registry.npmjs.org/nodemon/-/nodemon-3.1.10.tgz",
"integrity": "sha512-WDjw3pJ0/0jMFmyNDp3gvY2YizjLmmOUQo6DEBY+JgdvW/yQ9mEeSw6H5ythl5Ny2ytb7f9C2nIbjSxMNzbJXw==",
"dev": true,
"license": "MIT",
"dependencies": {
@ -9686,13 +9686,13 @@
}
},
"node_modules/parse5": {
"version": "7.2.1",
"resolved": "https://registry.npmjs.org/parse5/-/parse5-7.2.1.tgz",
"integrity": "sha512-BuBYQYlv1ckiPdQi/ohiivi9Sagc9JG+Ozs0r7b/0iK3sKmrb0b9FdWdBbOdx6hBCM/F9Ir82ofnBhtZOjCRPQ==",
"version": "7.3.0",
"resolved": "https://registry.npmjs.org/parse5/-/parse5-7.3.0.tgz",
"integrity": "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==",
"dev": true,
"license": "MIT",
"dependencies": {
"entities": "^4.5.0"
"entities": "^6.0.0"
},
"funding": {
"url": "https://github.com/inikulin/parse5?sponsor=1"
@ -11742,16 +11742,19 @@
}
},
"node_modules/supertest/node_modules/formidable": {
"version": "3.5.2",
"resolved": "https://registry.npmjs.org/formidable/-/formidable-3.5.2.tgz",
"integrity": "sha512-Jqc1btCy3QzRbJaICGwKcBfGWuLADRerLzDqi2NwSt/UkXLsHJw2TVResiaoBufHVHy9aSgClOHCeJsSsFLTbg==",
"version": "3.5.4",
"resolved": "https://registry.npmjs.org/formidable/-/formidable-3.5.4.tgz",
"integrity": "sha512-YikH+7CUTOtP44ZTnUhR7Ic2UASBPOqmaRkRKxRbywPTe5VxF7RRCck4af9wutiZ/QKM5nME9Bie2fFaPz5Gug==",
"dev": true,
"license": "MIT",
"dependencies": {
"@paralleldrive/cuid2": "^2.2.2",
"dezalgo": "^1.0.4",
"hexoid": "^2.0.0",
"once": "^1.4.0"
},
"engines": {
"node": ">=14.0.0"
},
"funding": {
"url": "https://ko-fi.com/tunnckoCore/commissions"
}
@ -11883,13 +11886,13 @@
"license": "MIT"
},
"node_modules/tinyglobby": {
"version": "0.2.12",
"resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.12.tgz",
"integrity": "sha512-qkf4trmKSIiMTs/E63cxH+ojC2unam7rJ0WrauAzpT3ECNTxGRMlaXxVbfxMUC/w0LaYk6jQ4y/nGR9uBO3tww==",
"version": "0.2.13",
"resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.13.tgz",
"integrity": "sha512-mEwzpUgrLySlveBwEVDMKk5B57bhLPYovRfPAXD5gA/98Opn0rCDj3GtLwFvCvH5RK9uPCExUROW5NjDwvqkxw==",
"dev": true,
"license": "MIT",
"dependencies": {
"fdir": "^6.4.3",
"fdir": "^6.4.4",
"picomatch": "^4.0.2"
},
"engines": {
@ -11900,9 +11903,9 @@
}
},
"node_modules/tinyglobby/node_modules/fdir": {
"version": "6.4.3",
"resolved": "https://registry.npmjs.org/fdir/-/fdir-6.4.3.tgz",
"integrity": "sha512-PMXmW2y1hDDfTSRc9gaXIuCCRpuoz3Kaz8cUelp3smouvfT632ozg2vrT6lJsHKKOF59YLbOGfAWGUcKEfRMQw==",
"version": "6.4.4",
"resolved": "https://registry.npmjs.org/fdir/-/fdir-6.4.4.tgz",
"integrity": "sha512-1NZP+GK4GfuAv3PqKvxQRDMjdSRZjnkq7KfhlNrCNNlZ0ygQFpebfrnfnq/W7fpUnAv9aGWmY1zKx7FYL3gwhg==",
"dev": true,
"license": "MIT",
"peerDependencies": {
@ -12481,9 +12484,9 @@
}
},
"node_modules/vite-node": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/vite-node/-/vite-node-3.1.1.tgz",
"integrity": "sha512-V+IxPAE2FvXpTCHXyNem0M+gWm6J7eRyWPR6vYoG/Gl+IscNOjXzztUhimQgTxaAoUoj40Qqimaa0NLIOOAH4w==",
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/vite-node/-/vite-node-3.1.2.tgz",
"integrity": "sha512-/8iMryv46J3aK13iUXsei5G/A3CUlW4665THCPS+K8xAaqrVWiGB4RfXMQXCLjpK9P2eK//BczrVkn5JLAk6DA==",
"dev": true,
"license": "MIT",
"dependencies": {
@ -12504,9 +12507,9 @@
}
},
"node_modules/vite/node_modules/fdir": {
"version": "6.4.3",
"resolved": "https://registry.npmjs.org/fdir/-/fdir-6.4.3.tgz",
"integrity": "sha512-PMXmW2y1hDDfTSRc9gaXIuCCRpuoz3Kaz8cUelp3smouvfT632ozg2vrT6lJsHKKOF59YLbOGfAWGUcKEfRMQw==",
"version": "6.4.4",
"resolved": "https://registry.npmjs.org/fdir/-/fdir-6.4.4.tgz",
"integrity": "sha512-1NZP+GK4GfuAv3PqKvxQRDMjdSRZjnkq7KfhlNrCNNlZ0ygQFpebfrnfnq/W7fpUnAv9aGWmY1zKx7FYL3gwhg==",
"dev": true,
"license": "MIT",
"peerDependencies": {
@ -12532,31 +12535,32 @@
}
},
"node_modules/vitest": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/vitest/-/vitest-3.1.1.tgz",
"integrity": "sha512-kiZc/IYmKICeBAZr9DQ5rT7/6bD9G7uqQEki4fxazi1jdVl2mWGzedtBs5s6llz59yQhVb7FFY2MbHzHCnT79Q==",
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/vitest/-/vitest-3.1.2.tgz",
"integrity": "sha512-WaxpJe092ID1C0mr+LH9MmNrhfzi8I65EX/NRU/Ld016KqQNRgxSOlGNP1hHN+a/F8L15Mh8klwaF77zR3GeDQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@vitest/expect": "3.1.1",
"@vitest/mocker": "3.1.1",
"@vitest/pretty-format": "^3.1.1",
"@vitest/runner": "3.1.1",
"@vitest/snapshot": "3.1.1",
"@vitest/spy": "3.1.1",
"@vitest/utils": "3.1.1",
"@vitest/expect": "3.1.2",
"@vitest/mocker": "3.1.2",
"@vitest/pretty-format": "^3.1.2",
"@vitest/runner": "3.1.2",
"@vitest/snapshot": "3.1.2",
"@vitest/spy": "3.1.2",
"@vitest/utils": "3.1.2",
"chai": "^5.2.0",
"debug": "^4.4.0",
"expect-type": "^1.2.0",
"expect-type": "^1.2.1",
"magic-string": "^0.30.17",
"pathe": "^2.0.3",
"std-env": "^3.8.1",
"std-env": "^3.9.0",
"tinybench": "^2.9.0",
"tinyexec": "^0.3.2",
"tinyglobby": "^0.2.13",
"tinypool": "^1.0.2",
"tinyrainbow": "^2.0.0",
"vite": "^5.0.0 || ^6.0.0",
"vite-node": "3.1.1",
"vite-node": "3.1.2",
"why-is-node-running": "^2.3.0"
},
"bin": {
@ -12572,8 +12576,8 @@
"@edge-runtime/vm": "*",
"@types/debug": "^4.1.12",
"@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0",
"@vitest/browser": "3.1.1",
"@vitest/ui": "3.1.1",
"@vitest/browser": "3.1.2",
"@vitest/ui": "3.1.2",
"happy-dom": "*",
"jsdom": "*"
},

View File

@ -40,7 +40,7 @@
"concurrently": "^9.1.2",
"husky": "^9.1.7",
"lint-staged": "^15.4.3",
"prettier": "^3.5.1",
"prettier": "^3.5.3",
"vitest": "^3.0.8"
},
"dependencies": {