From 31cf20e748267525ff2cf8947220ac887d8594b3 Mon Sep 17 00:00:00 2001 From: aryunewaskar77-art Date: Sun, 17 May 2026 00:03:33 +0530 Subject: [PATCH 1/4] i18n(zh-CN): translate dashboard UI section into Chinese Signed-off-by: aryunewaskar77-art --- frontend/package.json | 4 + frontend/public/locales/en/translation.json | 152 ++++++++++ frontend/public/locales/zh/translation.json | 156 +++++++++++ .../components/Charts/JobStatusPieChart.jsx | 8 +- .../components/Charts/PodStatusLineChart.jsx | 6 +- .../Charts/QueueResourcesBarChart.jsx | 22 +- frontend/src/components/CreateDialog.jsx | 42 +-- frontend/src/components/Layout.jsx | 33 ++- frontend/src/components/Searchbar.jsx | 4 +- frontend/src/components/Titlecomponent.jsx | 1 - .../src/components/dashboard/Dashboard.jsx | 13 +- .../components/dashboard/DashboardHeader.jsx | 6 +- .../src/components/dashboard/ErrorDisplay.jsx | 9 +- .../dashboard/StatCardsContainer.jsx | 10 +- .../src/components/jobs/JobPagination.jsx | 18 +- .../src/components/jobs/JobStatusChip.jsx | 5 +- .../jobs/JobTable/CreateJobDialog.jsx | 16 +- .../jobs/JobTable/JobEditDialog.jsx | 12 +- .../components/jobs/JobTable/JobFilters.jsx | 9 +- .../src/components/jobs/JobTable/JobTable.jsx | 8 +- .../jobs/JobTable/JobTableDeleteDialog.jsx | 10 +- .../jobs/JobTable/JobTableHeader.jsx | 29 +- frontend/src/components/jobs/Jobs.jsx | 28 +- .../components/podgroups/PodGroupDialog.jsx | 6 +- .../src/components/podgroups/PodGroups.jsx | 27 +- .../PodGroupsTable/PodGroupsTable.jsx | 4 +- .../PodGroupsTable/PodGroupsTableHeader.jsx | 24 +- .../src/components/pods/PodDetailsDialog.jsx | 6 +- frontend/src/components/pods/Pods.jsx | 30 +- .../src/components/pods/PodsPagination.jsx | 10 +- .../components/pods/PodsTable/FilterMenu.jsx | 35 ++- .../src/components/pods/PodsTable/PodRow.jsx | 9 +- .../components/pods/PodsTable/TableHeader.jsx | 261 ++++++++++-------- .../src/components/queues/QueuePagination.jsx | 10 +- .../queues/QueueTable/EditQueueDialog.jsx | 12 +- .../queues/QueueTable/QueueTable.jsx | 10 +- .../QueueTable/QueueTableDeleteDialog.jsx | 10 +- .../queues/QueueTable/QueueTableHeader.jsx | 22 +- .../queues/QueueTable/QueueTableRow.jsx | 8 +- .../src/components/queues/QueueYamlDialog.jsx | 6 +- frontend/src/components/queues/Queues.jsx | 28 +- frontend/src/i18n.js | 21 ++ frontend/src/main.jsx | 1 + 43 files changed, 811 insertions(+), 330 deletions(-) create mode 100644 frontend/public/locales/en/translation.json create mode 100644 frontend/public/locales/zh/translation.json create mode 100644 frontend/src/i18n.js diff --git a/frontend/package.json b/frontend/package.json index 01635b8d..6b749b3a 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -25,6 +25,9 @@ "bootstrap": "^5.3.5", "chart.js": "^4.4.7", "dotenv": "^16.4.7", + "i18next": "^26.2.0", + "i18next-browser-languagedetector": "^8.2.1", + "i18next-http-backend": "^4.0.0", "js-yaml": "^4.1.0", "lodash": "^4.17.21", "lucide-react": "^0.511.0", @@ -32,6 +35,7 @@ "react-bootstrap": "^2.10.9", "react-chartjs-2": "^5.3.0", "react-dom": "^19.0.0", + "react-i18next": "^17.0.8", "react-router-dom": "^7.1.5" }, "devDependencies": { diff --git a/frontend/public/locales/en/translation.json b/frontend/public/locales/en/translation.json new file mode 100644 index 00000000..4a9fba52 --- /dev/null +++ b/frontend/public/locales/en/translation.json @@ -0,0 +1,152 @@ +{ + "dashboard": "Dashboard", + "jobs": "Jobs", + "queues": "Queues", + "pods": "Pods", + "pod_groups": "PodGroups", + "app_title": "Volcano Dashboard", + "volcano_logo_alt": "Volcano Logo", + "toggle_drawer": "toggle drawer", + "refreshing": "Refreshing...", + "jobs_status_title": "Volcano Jobs Status", + "search_jobs_placeholder": "Search jobs...", + "refresh_jobs_label": "Refresh Job Listings", + "create_job_label": "Create Job", + "create_job_dialog_title": "Create a Job", + "job_name_label": "Job Name", + "job_created_success": "Job created successfully!", + "job_created_error": "Error creating job: ", + "network_error": "Network error: ", + "fetch_jobs_error": "Failed to fetch jobs: ", + "fetch_job_yaml_error": "Failed to fetch job YAML: ", + "name_required": "{{resourceType}} name is required", + "container_name_required": "Container name is required", + "container_image_required": "Container image is required", + "container_port_required": "Container port is required and must be positive", + "weight_required": "Weight is required and must be positive", + "cpu": "CPU", + "memory": "Memory", + "custom_scalar_resources": "Custom Scalar Resources", + "key_label": "Key", + "value_label": "Value", + "namespace": "Namespace", + "container_name": "Container Name", + "container_image": "Container Image", + "container_port": "Container Port", + "weight_asterisk": "Weight *", + "reclaimable": "Reclaimable", + "cancel": "Cancel", + "create": "Create", + "guarantee": "Guarantee", + "capability": "Capability", + "deserved": "Deserved", + "no_data_available": "No data available", + "jobs_status": "Jobs Status", + "running_pods": "Running Pods", + "pod_status_timeline": "Pod Status Timeline", + "resources_suffix": " Resources", + "allocated_suffix": " Allocated", + "capacity_suffix": " Capacity", + "memory_gi": "Memory (Gi)", + "cpu_cores": "CPU Cores", + "pod_count": "Pod Count", + "gpu_count": "GPU Count", + "amount": "Amount", + "queue_resources": "Queue Resources", + "no_data_selected_resource": "No data available for selected resource type", + "pod_group_yaml_title": "PodGroup YAML - {{selectedName}}", + "close": "Close", + "fetch_podgroups_error": "Failed to fetch podgroups: ", + "fetch_yaml_error": "Failed to fetch YAML: ", + "create_podgroup_not_implemented": "Create PodGroup not implemented yet", + "podgroups_title": "Volcano PodGroups", + "search_podgroups_placeholder": "Search PodGroups...", + "refresh_listings_label": "Refresh PodGroups", + "create_podgroup_label": "Create PodGroup", + "name_label": "Name", + "refresh_data": "Refresh Data", + "total_jobs": "Total Jobs", + "total_jobs_count": "Total Jobs: {{count}}", + "active_queues": "Active Queues", + "complete_rate": "Complete Rate", + "fetch_dashboard_error": "Error fetching dashboard data:", + "fetch_pods_error": "Failed to fetch pods: ", + "create_pod_error": "Error creating pod: ", + "create_pod_success": "Pod created successfully!", + "pods_status_title": "Volcano Pods Status", + "search_pods_placeholder": "Search Pods...", + "refresh_pods_label": "Refresh Pods", + "create_pod_label": "Create Pod", + "create_pod_dialog_title": "Create a Pod", + "pod_name_label": "Pod Name", + "pod_details_title": "Pod Details", + "rows_per_page": "Rows per page:", + "per_page": "per page", + "total_pods": "Total Pods", + "total_pods_count": "Total Pods: {{count}}", + "weight": "Weight", + "actions": "Actions", + "search_queues_placeholder": "Search queues...", + "refresh": "Refresh", + "create_queue": "Create Queue", + "delete_queue": "Delete Queue", + "queue_name": "Queue Name", + "queue_yaml": "Queue YAML", + "delete_queue_confirm_msg": "Are you sure you want to delete queue \"{{queueToDelete}}\"? This action cannot be undone.", + "delete_job": "Delete Job", + "delete_job_confirm_msg": "Are you sure you want to delete job \"{{jobToDelete}}\"? This action cannot be undone.", + "delete_job_failed": "Job \"{{namespace}}/{{name}}\" could not be deleted.", + "unexpected_error": "An unexpected error occurred.", + "all": "All", + "name": "Name", + "min_member": "Min Member", + "creation_time": "Creation Time", + "sort": "Sort", + "status": "Status", + "queue": "Queue", + "delete": "Delete", + "age": "Age", + "queues_status_title": "Volcano Queues Status", + "refresh_queues_label": "Refresh Queues", + "create_queue_error": "Failed to create queue: ", + "create_queue_success": "Queue created successfully!", + "fetch_queues_error": "Failed to fetch queues: ", + "edit_queue": "Edit Queue", + "form": "Form", + "update": "Update", + "updating": "Updating...", + "create_job_yaml_title": "Create Job (YAML)", + "yaml_specification_label": "Paste or type your Job YAML specification below:", + "yaml_object_error": "YAML must describe an object.", + "invalid_yaml_error": "Invalid YAML format.", + "no_jobs_found": "No jobs found.", + "delete_job_error_msg": "Job \"{{namespace}}/{{name}}\" could not be deleted.", + "no_podgroups_found": "No podgroups found.", + "delete_queue_forbidden": "queues.scheduling.volcano.sh \"{{queueToDelete}}\" is forbidden.", + "delete_queue_error_msg": "Cannot delete \"{{queueToDelete}}\". Error message: {{customMessage}}", + "no_queues_found": "No queues found.", + "edit_job": "Edit Job", + "invalid_yaml_alert": "Invalid YAML format. Please check your input.", + "completed": "Completed", + "running": "Running", + "failed": "Failed", + "pending": "Pending", + "succeeded": "Succeeded", + "unknown": "Unknown", + "open": "Open", + "closing": "Closing", + "closed": "Closed", + "crashloopbackoff": "CrashLoopBackOff", + "terminating": "Terminating", + "containercreating": "ContainerCreating", + "error": "Error", + "items_per_page": "{{count}} per page", + "total_queues": "Total Queues", + "total_queues_count": "Total Queues: {{count}}", + "total_podgroups": "Total PodGroups", + "total_podgroups_count": "Total PodGroups: {{count}}", + "state": "State", + "guarantee_resources_optional": "Guarantee Resources (Optional)", + "capability_resources_optional": "Capability Resources (Optional)", + "deserved_resources_optional": "Deserved Resources (Optional)" +} diff --git a/frontend/public/locales/zh/translation.json b/frontend/public/locales/zh/translation.json new file mode 100644 index 00000000..4f2e31a0 --- /dev/null +++ b/frontend/public/locales/zh/translation.json @@ -0,0 +1,156 @@ +{ + "dashboard": "仪表板", + "jobs": "作业", + "queues": "队列", + "pods": "Pods", + "pod_groups": "PodGroups", + "app_title": "Volcano 仪表板", + "volcano_logo_alt": "Volcano 图标", + "toggle_drawer": "切换侧边栏", + "refreshing": "正在刷新...", + "jobs_status_title": "Volcano 作业状态", + "podgroups_title": "Volcano PodGroups", + "pods_status_title": "Volcano Pods 状态", + "queues_status_title": "Volcano 队列状态", + "search_jobs_placeholder": "搜索作业...", + "refresh_jobs_label": "刷新作业列表", + "create_job_label": "创建作业", + "create_job_dialog_title": "创建作业", + "job_name_label": "作业名称", + "job_created_success": "作业创建成功!", + "job_created_error": "创建作业时出错:", + "network_error": "网络错误:", + "fetch_jobs_error": "获取作业失败:", + "fetch_job_yaml_error": "获取作业 YAML 失败:", + "name_required": "{{resourceType}} 名称是必填的", + "container_name_required": "容器名称是必填的", + "container_image_required": "容器镜像是必填的", + "container_port_required": "容器端口是必填的且必须为正数", + "weight_required": "权重是必填的且必须为正数", + "cpu": "CPU", + "memory": "内存", + "nvidia.com/gpu": "GPU", + "custom_scalar_resources": "自定义标量资源", + "key_label": "键", + "value_label": "值", + "namespace": "命名空间", + "container_name": "容器名称", + "container_image": "容器镜像", + "container_port": "容器端口", + "weight_asterisk": "权重 *", + "reclaimable": "可回收", + "cancel": "取消", + "create": "创建", + "guarantee": "保证", + "capability": "能力", + "deserved": "应得", + "no_data_available": "无可用数据", + "jobs_status": "作业状态", + "running_pods": "运行中的 Pods", + "pod_status_timeline": "Pod 状态时间线", + "resources_suffix": " 资源", + "allocated_suffix": " 已分配", + "capacity_suffix": " 容量", + "memory_gi": "内存 (Gi)", + "cpu_cores": "CPU 核心", + "pod_count": "Pod 数量", + "gpu_count": "GPU 数量", + "amount": "数量", + "queue_resources": "队列资源", + "no_data_selected_resource": "所选资源类型无可用数据", + "pod_group_yaml_title": "PodGroup YAML - {{selectedName}}", + "close": "关闭", + "fetch_podgroups_error": "获取 PodGroups 失败:", + "fetch_yaml_error": "获取 YAML 失败:", + "create_podgroup_not_implemented": "创建 PodGroup 功能尚未实现", + "podgroups_title": "Volcano PodGroups", + "search_podgroups_placeholder": "搜索 PodGroups...", + "refresh_listings_label": "刷新 PodGroups", + "create_podgroup_label": "创建 PodGroup", + "name_label": "名称", + "refresh_data": "刷新数据", + "total_jobs": "作业总数", + "total_jobs_count": "作业总数:{{count}}", + "active_queues": "活跃队列", + "complete_rate": "完成率", + "fetch_dashboard_error": "获取仪表板数据时出错:", + "fetch_pods_error": "获取 Pod 失败:", + "create_pod_error": "创建 Pod 时出错:", + "create_pod_success": "Pod 创建成功!", + "pods_status_title": "Volcano Pods 状态", + "search_pods_placeholder": "搜索 Pods...", + "refresh_pods_label": "刷新 Pods", + "create_pod_label": "创建 Pod", + "create_pod_dialog_title": "创建 Pod", + "pod_name_label": "Pod 名称", + "pod_details_title": "Pod 详情", + "rows_per_page": "每页行数:", + "per_page": "每页", + "total_pods": "Pods 总数", + "total_pods_count": "Pods 总数:{{count}}", + "weight": "权重", + "actions": "操作", + "search_queues_placeholder": "搜索队列...", + "refresh": "刷新", + "create_queue": "创建队列", + "delete_queue": "删除队列", + "queue_name": "队列名称", + "queue_yaml": "队列 YAML", + "delete_queue_confirm_msg": "您确定要删除队列 \"{{queueToDelete}}\" 吗?此操作无法撤销。", + "delete_job": "删除作业", + "delete_job_confirm_msg": "您确定要删除作业 \"{{jobToDelete}}\" 吗?此操作无法撤销。", + "delete_job_failed": "作业 \"{{namespace}}/{{name}}\" 无法删除。", + "unexpected_error": "发生意外错误。", + "all": "全部", + "name": "名称", + "min_member": "最小成员数", + "creation_time": "创建时间", + "sort": "排序", + "status": "状态", + "queue": "队列", + "delete": "删除", + "age": "存活时间", + "queues_status_title": "Volcano 队列状态", + "refresh_queues_label": "刷新队列", + "create_queue_error": "创建队列失败:", + "create_queue_success": "队列创建成功!", + "fetch_queues_error": "获取队列失败:", + "edit_queue": "编辑队列", + "form": "表单", + "update": "更新", + "updating": "更新中...", + "create_job_yaml_title": "创建作业 (YAML)", + "yaml_specification_label": "在下方粘贴或输入您的作业 YAML 规范:", + "yaml_object_error": "YAML 必须描述一个对象。", + "invalid_yaml_error": "无效的 YAML 格式。", + "no_jobs_found": "未找到作业。", + "delete_job_error_msg": "作业 \"{{namespace}}/{{name}}\" 无法删除。", + "no_podgroups_found": "未找到 PodGroups。", + "delete_queue_forbidden": "禁止访问 queues.scheduling.volcano.sh \"{{queueToDelete}}\"。", + "delete_queue_error_msg": "无法删除 \"{{queueToDelete}}\"。错误信息:{{customMessage}}", + "no_queues_found": "未找到队列。", + "edit_job": "编辑作业", + "invalid_yaml_alert": "无效的 YAML 格式。请检查您的输入。", + "completed": "已完成", + "running": "运行中", + "failed": "已失败", + "pending": "等待中", + "succeeded": "成功", + "unknown": "未知", + "open": "开启", + "closing": "关闭中", + "closed": "已关闭", + "crashloopbackoff": "容器启动失败", + "terminating": "终止中", + "containercreating": "容器创建中", + "error": "错误", + "items_per_page": "每页 {{count}} 条", + "total_queues": "队列总数", + "total_queues_count": "队列总数:{{count}}", + "total_podgroups": "PodGroups 总数", + "total_podgroups_count": "PodGroups 总数:{{count}}", + "state": "状态", + "guarantee_resources_optional": "保证资源(可选)", + "capability_resources_optional": "能力资源(可选)", + "deserved_resources_optional": "应得资源(可选)" +} \ No newline at end of file diff --git a/frontend/src/components/Charts/JobStatusPieChart.jsx b/frontend/src/components/Charts/JobStatusPieChart.jsx index 8441356e..61016372 100644 --- a/frontend/src/components/Charts/JobStatusPieChart.jsx +++ b/frontend/src/components/Charts/JobStatusPieChart.jsx @@ -1,15 +1,17 @@ import React from "react"; import { Box, Typography } from "@mui/material"; +import { useTranslation } from "react-i18next"; import { Doughnut } from "react-chartjs-2"; import { Chart as ChartJS, ArcElement, Tooltip, Legend, Title } from "chart.js"; ChartJS.register(ArcElement, Tooltip, Legend, Title); const JobStatusPieChart = ({ data }) => { + const { t } = useTranslation(); if (!data || !Array.isArray(data)) { return ( - No data available + {t("no_data_available")} ); } @@ -80,7 +82,7 @@ const JobStatusPieChart = ({ data }) => { }} > - Jobs Status + {t("jobs_status")} { variant="body2" sx={{ mr: 2, minWidth: 70 }} > - {status} + {t(status.toLowerCase())} {count} ( diff --git a/frontend/src/components/Charts/PodStatusLineChart.jsx b/frontend/src/components/Charts/PodStatusLineChart.jsx index dafaa872..ffd4bbcf 100644 --- a/frontend/src/components/Charts/PodStatusLineChart.jsx +++ b/frontend/src/components/Charts/PodStatusLineChart.jsx @@ -1,16 +1,18 @@ import React from "react"; import { Box, Typography } from "@mui/material"; +import { useTranslation } from "react-i18next"; import { Line } from "react-chartjs-2"; import "./chartConfig"; const PodStatusLineChart = ({ data }) => { + const { t } = useTranslation(); const chartData = { labels: data.map((pod) => new Date(pod.metadata.creationTimestamp).toLocaleTimeString(), ), datasets: [ { - label: "Running Pods", + label: t("running_pods"), data: data.map((pod) => pod.status.phase === "Running" ? 1 : 0, ), @@ -35,7 +37,7 @@ const PodStatusLineChart = ({ data }) => { return ( - Pod Status Timeline + {t("pod_status_timeline")} diff --git a/frontend/src/components/Charts/QueueResourcesBarChart.jsx b/frontend/src/components/Charts/QueueResourcesBarChart.jsx index 24c2ff2d..50eba543 100644 --- a/frontend/src/components/Charts/QueueResourcesBarChart.jsx +++ b/frontend/src/components/Charts/QueueResourcesBarChart.jsx @@ -1,5 +1,6 @@ import React, { useEffect, useMemo, useState } from "react"; import { Box, FormControl, MenuItem, Select, Typography } from "@mui/material"; +import { useTranslation } from "react-i18next"; import { Bar } from "react-chartjs-2"; import "./chartConfig"; @@ -54,6 +55,7 @@ const processData = (data) => { }; const QueueResourcesBarChart = ({ data }) => { + const { t } = useTranslation(); const [selectedResource, setSelectedResource] = useState(""); // Obtain resource type options dynamically @@ -73,7 +75,7 @@ const QueueResourcesBarChart = ({ data }) => { // Convert resource type from Set to Array return Array.from(resourceTypes).map((resource) => ({ value: resource, - label: `${resource.charAt(0).toUpperCase() + resource.slice(1)} Resources`, + label: `${t(resource.toLowerCase())}${t("resources_suffix")}`, })); }, [data]); @@ -92,7 +94,7 @@ const QueueResourcesBarChart = ({ data }) => { labels: Object.keys(processedData), datasets: [ { - label: `${selectedResource.toUpperCase()} Allocated`, + label: `${t(selectedResource.toLowerCase())}${t("allocated_suffix")}`, data: Object.values(processedData).map( (q) => q.allocated[selectedResource] || 0, ), @@ -101,7 +103,7 @@ const QueueResourcesBarChart = ({ data }) => { borderWidth: 1, }, { - label: `${selectedResource.toUpperCase()} Capacity`, + label: `${t(selectedResource.toLowerCase())}${t("capacity_suffix")}`, data: Object.values(processedData).map( (q) => q.capability[selectedResource] || 0, ), @@ -116,15 +118,15 @@ const QueueResourcesBarChart = ({ data }) => { const getYAxisLabel = () => { switch (selectedResource) { case "memory": - return "Memory (Gi)"; + return t("memory_gi"); case "cpu": - return "CPU Cores"; + return t("cpu_cores"); case "pods": - return "Pod Count"; + return t("pod_count"); case "nvidia.com/gpu": - return "GPU Count"; + return t("gpu_count"); default: - return "Amount"; + return t("amount"); } }; @@ -175,7 +177,7 @@ const QueueResourcesBarChart = ({ data }) => { mb: 2, }} > - Queue Resources + {t("queue_resources")} - Total Jobs: {totalJobs} + {displayTotalLabel} { + const { t } = useTranslation(); const theme = useTheme(); const getStatusColor = (status) => { @@ -21,7 +22,7 @@ const JobStatusChip = ({ status }) => { return ( { + const { t } = useTranslation(); const [yamlText, setYamlText] = useState(""); const [error, setError] = useState(""); @@ -31,7 +33,7 @@ const CreateJobDialog = ({ try { const parsed = yaml.load(yamlText); if (!parsed || typeof parsed !== "object") { - setError("YAML must describe an object."); + setError(t("yaml_object_error")); return; } setError(""); @@ -39,7 +41,7 @@ const CreateJobDialog = ({ setYamlText(""); onClose(); } catch (e) { - setError(e.message || "Invalid YAML format."); + setError(e.message || t("invalid_yaml_error")); } }; @@ -71,12 +73,12 @@ const CreateJobDialog = ({ letterSpacing: "0.5px", }} > - {title} + {title || t("create_job_yaml_title")} - Paste or type your Job YAML specification below: + {t("yaml_specification_label")} - Cancel + {t("cancel")} diff --git a/frontend/src/components/jobs/JobTable/JobEditDialog.jsx b/frontend/src/components/jobs/JobTable/JobEditDialog.jsx index 2ebdf919..1c994b9c 100644 --- a/frontend/src/components/jobs/JobTable/JobEditDialog.jsx +++ b/frontend/src/components/jobs/JobTable/JobEditDialog.jsx @@ -1,4 +1,5 @@ import React, { useState, useEffect } from "react"; +import { useTranslation } from "react-i18next"; import { Dialog, DialogTitle, @@ -12,6 +13,7 @@ import Editor from "@monaco-editor/react"; import yaml from "js-yaml"; const JobEditDialog = ({ open, job, onClose, onSave }) => { + const { t } = useTranslation(); const [editorValue, setEditorValue] = useState(""); const [editMode, setEditMode] = useState("yaml"); @@ -35,7 +37,7 @@ const JobEditDialog = ({ open, job, onClose, onSave }) => { onClose(); } catch (err) { console.error("Parsing error:", err); - alert("Invalid YAML format. Please check your input."); + alert(t("invalid_yaml_alert")); } }; @@ -48,14 +50,14 @@ const JobEditDialog = ({ open, job, onClose, onSave }) => { alignItems: "center", }} > - Edit Job + {t("edit_job")} - YAML + {t("yaml")} @@ -72,14 +74,14 @@ const JobEditDialog = ({ open, job, onClose, onSave }) => { diff --git a/frontend/src/components/jobs/JobTable/JobFilters.jsx b/frontend/src/components/jobs/JobTable/JobFilters.jsx index d9679179..6a7f10a2 100644 --- a/frontend/src/components/jobs/JobTable/JobFilters.jsx +++ b/frontend/src/components/jobs/JobTable/JobFilters.jsx @@ -1,5 +1,5 @@ -import React from "react"; import { Menu, MenuItem } from "@mui/material"; +import { useTranslation } from "react-i18next"; const JobFilters = ({ filterType, @@ -9,6 +9,7 @@ const JobFilters = ({ handleFilterClose, anchorEl, }) => { + const { t } = useTranslation(); return ( handleFilterClick(filterType, option)} > - {option} + {option === "All" + ? t("all") + : filterType === "status" + ? t(option.toLowerCase()) + : option} ))} diff --git a/frontend/src/components/jobs/JobTable/JobTable.jsx b/frontend/src/components/jobs/JobTable/JobTable.jsx index bee34526..e029948a 100644 --- a/frontend/src/components/jobs/JobTable/JobTable.jsx +++ b/frontend/src/components/jobs/JobTable/JobTable.jsx @@ -9,6 +9,7 @@ import { useTheme, alpha, } from "@mui/material"; +import { useTranslation } from "react-i18next"; import JobTableHeader from "./JobTableHeader"; import JobTableRow from "./JobTableRow"; import JobTableDeleteDialog from "./JobTableDeleteDialog"; // Be sure to have this component @@ -28,6 +29,7 @@ const JobTable = ({ onJobUpdate, reloadJobs, // (optional) for refetching after delete }) => { + const { t } = useTranslation(); const theme = useTheme(); // State for delete dialog @@ -92,7 +94,7 @@ const JobTable = ({ data.message || data.details || text || - `Job "${namespace}/${name}" could not be deleted.`; + t("delete_job_error_msg", { namespace, name }); setDeleteError(k8sMessage); return; @@ -103,7 +105,7 @@ const JobTable = ({ handleCloseDeleteDialog(); } catch (error) { - setDeleteError(error.message || "An unexpected error occurred."); + setDeleteError(error.message || t("unexpected_error")); } finally { setIsDeleting(false); } @@ -160,7 +162,7 @@ const JobTable = ({ {jobs.length === 0 ? ( - No jobs found. + {t("no_jobs_found")} ) : ( diff --git a/frontend/src/components/jobs/JobTable/JobTableDeleteDialog.jsx b/frontend/src/components/jobs/JobTable/JobTableDeleteDialog.jsx index 49880704..e94f2730 100644 --- a/frontend/src/components/jobs/JobTable/JobTableDeleteDialog.jsx +++ b/frontend/src/components/jobs/JobTable/JobTableDeleteDialog.jsx @@ -9,6 +9,7 @@ import { Alert, } from "@mui/material"; import CloseIcon from "@mui/icons-material/Close"; +import { useTranslation } from "react-i18next"; const JobTableDeleteDialog = ({ open, @@ -17,6 +18,7 @@ const JobTableDeleteDialog = ({ jobToDelete, error, }) => { + const { t } = useTranslation(); const showOnlyError = Boolean(error); return ( @@ -28,7 +30,7 @@ const JobTableDeleteDialog = ({ alignItems: "center", }} > - {showOnlyError ? "Error" : "Delete Job"} + {showOnlyError ? t("error") : t("delete_job")} @@ -38,14 +40,14 @@ const JobTableDeleteDialog = ({ {showOnlyError ? ( {error} ) : ( - `Are you sure you want to delete job "${jobToDelete}"? This action cannot be undone.` + t("delete_job_confirm_msg", { jobToDelete }) )} {!showOnlyError && ( )} {!showOnlyError && ( @@ -54,7 +56,7 @@ const JobTableDeleteDialog = ({ color="error" variant="contained" > - Delete + {t("delete")} )} diff --git a/frontend/src/components/jobs/JobTable/JobTableHeader.jsx b/frontend/src/components/jobs/JobTable/JobTableHeader.jsx index 079e2ef6..8214581f 100644 --- a/frontend/src/components/jobs/JobTable/JobTableHeader.jsx +++ b/frontend/src/components/jobs/JobTable/JobTableHeader.jsx @@ -11,6 +11,7 @@ import { } from "@mui/material"; import { ArrowDownward, ArrowUpward, UnfoldMore } from "@mui/icons-material"; import JobFilters from "./JobFilters"; +import { useTranslation } from "react-i18next"; const JobTableHeader = ({ filters, @@ -23,6 +24,7 @@ const JobTableHeader = ({ sortDirection, toggleSortDirection, }) => { + const { t } = useTranslation(); const theme = useTheme(); return ( @@ -45,13 +47,16 @@ const JobTableHeader = ({ fontWeight="700" color="text.primary" > - Name + {t("name")} - {["Namespace", "Queue"].map((field) => ( + {[ + { key: "namespace", label: t("namespace") }, + { key: "queue", label: t("queue") }, + ].map((field) => ( - {field} + {field.label} @@ -109,7 +114,7 @@ const JobTableHeader = ({ fontWeight="700" color="text.primary" > - Creation Time + {t("creation_time")} @@ -167,7 +172,7 @@ const JobTableHeader = ({ fontWeight="700" color="text.primary" > - Status + {t("status")} - Actions + {t("actions")} diff --git a/frontend/src/components/jobs/Jobs.jsx b/frontend/src/components/jobs/Jobs.jsx index d6d61d33..e87b7b3d 100644 --- a/frontend/src/components/jobs/Jobs.jsx +++ b/frontend/src/components/jobs/Jobs.jsx @@ -7,8 +7,10 @@ import JobTable from "./JobTable/JobTable"; import JobPagination from "./JobPagination"; import JobDialog from "./JobDialog"; import SearchBar from "../Searchbar"; +import { useTranslation } from "react-i18next"; const Jobs = () => { + const { t } = useTranslation(); const [jobs, setJobs] = useState([]); const [cachedJobs, setCachedJobs] = useState([]); const [, setLoading] = useState(true); @@ -59,7 +61,7 @@ const Jobs = () => { setCachedJobs(data.items || []); setTotalJobs(data.totalCount || 0); } catch (err) { - setError("Failed to fetch jobs: " + err.message); + setError({ key: "fetch_jobs_error", message: err.message }); setCachedJobs([]); } finally { setLoading(false); @@ -115,7 +117,7 @@ const Jobs = () => { setOpenDialog(true); } catch (err) { console.error("Failed to fetch job YAML:", err); - setError("Failed to fetch job YAML: " + err.message); + setError({ key: "fetch_job_yaml_error", message: err.message }); } finally { setLoading(false); } @@ -157,13 +159,13 @@ const Jobs = () => { } catch { // ignore error } - alert("Error creating job: " + errorMsg); + alert(t("job_created_error") + errorMsg); return; } - alert("Job created successfully!"); + alert(t("job_created_success")); } catch (err) { - alert("Network error: " + err.message); + alert(t("network_error") + err.message); } }; @@ -210,10 +212,12 @@ const Jobs = () => { {error && ( - {error} + + {error.key ? `${t(error.key)}${error.message}` : error} + )} - + { handleRefresh={fetchJobs} fetchData={fetchJobs} isRefreshing={false} // Update if needed - placeholder="Search jobs..." - refreshLabel="Refresh Job Listings" - createlabel="Create Job" - dialogTitle="Create a Job" - dialogResourceNameLabel="Job Name" + placeholder={t("search_jobs_placeholder")} + refreshLabel={t("refresh_jobs_label")} + createlabel={t("create_job_label")} + dialogTitle={t("create_job_dialog_title")} + dialogResourceNameLabel={t("job_name_label")} dialogResourceType="Job" onCreateClick={handleCreateJob} /> diff --git a/frontend/src/components/podgroups/PodGroupDialog.jsx b/frontend/src/components/podgroups/PodGroupDialog.jsx index 4f0cd5ca..559aaab3 100644 --- a/frontend/src/components/podgroups/PodGroupDialog.jsx +++ b/frontend/src/components/podgroups/PodGroupDialog.jsx @@ -1,4 +1,5 @@ import React from "react"; +import { useTranslation } from "react-i18next"; import { Box, Button, @@ -9,6 +10,7 @@ import { } from "@mui/material"; const PodGroupDialog = ({ open, handleClose, selectedName, selectedYaml }) => { + const { t } = useTranslation(); return ( { }, }} > - PodGroup YAML - {selectedName} + {t("pod_group_yaml_title", { selectedName })} { }, }} > - Close + {t("close")} diff --git a/frontend/src/components/podgroups/PodGroups.jsx b/frontend/src/components/podgroups/PodGroups.jsx index b960da1e..29419a9d 100644 --- a/frontend/src/components/podgroups/PodGroups.jsx +++ b/frontend/src/components/podgroups/PodGroups.jsx @@ -4,12 +4,14 @@ import axios from "axios"; import { escape } from "lodash"; import TitleComponent from "../Titlecomponent"; import { fetchAllNamespaces } from "../utils"; +import { useTranslation } from "react-i18next"; import PodGroupsTable from "./PodGroupsTable/PodGroupsTable"; import JobPagination from "../jobs/JobPagination"; // Reuse pagination import SearchBar from "../Searchbar"; import PodGroupDialog from "./PodGroupDialog"; // Need to create this const PodGroups = () => { + const { t } = useTranslation(); const [podGroups, setPodGroups] = useState([]); const [cachedPodGroups, setCachedPodGroups] = useState([]); const [loading, setLoading] = useState(true); @@ -56,7 +58,7 @@ const PodGroups = () => { setCachedPodGroups(data.items || []); setTotalItems(data.totalCount || 0); } catch (err) { - setError("Failed to fetch podgroups: " + err.message); + setError({ key: "fetch_podgroups_error", message: err.message }); setCachedPodGroups([]); } finally { setLoading(false); @@ -111,7 +113,7 @@ const PodGroups = () => { setOpenDialog(true); } catch (err) { console.error("Failed to fetch YAML:", err); - setError("Failed to fetch YAML: " + err.message); + setError({ key: "fetch_yaml_error", message: err.message }); } finally { setLoading(false); } @@ -167,17 +169,19 @@ const PodGroups = () => { // For now, no creation dialog const handleCreate = () => { - alert("Create PodGroup not implemented yet"); + alert(t("create_podgroup_not_implemented")); }; return ( {error && ( - {error} + + {error.key ? `${t(error.key)}${error.message}` : error} + )} - + { handleRefresh={fetchPodGroups} fetchData={fetchPodGroups} isRefreshing={loading} - placeholder="Search PodGroups..." - refreshLabel="Refresh Listings" - createlabel="Create PodGroup" - dialogTitle="Create PodGroup" - dialogResourceNameLabel="Name" + placeholder={t("search_podgroups_placeholder")} + refreshLabel={t("refresh_listings_label")} + createlabel={t("create_podgroup_label")} + dialogTitle={t("create_podgroup_label")} + dialogResourceNameLabel={t("name_label")} dialogResourceType="PodGroup" onCreateClick={handleCreate} /> @@ -209,9 +213,10 @@ const PodGroups = () => { /> { + const { t } = useTranslation(); const theme = useTheme(); return ( @@ -76,7 +78,7 @@ const PodGroupsTable = ({ {podGroups.length === 0 ? ( - No podgroups found. + {t("no_podgroups_found")} ) : ( diff --git a/frontend/src/components/podgroups/PodGroupsTable/PodGroupsTableHeader.jsx b/frontend/src/components/podgroups/PodGroupsTable/PodGroupsTableHeader.jsx index 203c6190..ddc10af2 100644 --- a/frontend/src/components/podgroups/PodGroupsTable/PodGroupsTableHeader.jsx +++ b/frontend/src/components/podgroups/PodGroupsTable/PodGroupsTableHeader.jsx @@ -15,6 +15,7 @@ import { UnfoldMore, FilterList, } from "@mui/icons-material"; +import { useTranslation } from "react-i18next"; import JobFilters from "../../jobs/JobTable/JobFilters"; const PodGroupsTableHeader = ({ @@ -27,6 +28,7 @@ const PodGroupsTableHeader = ({ sortDirection, toggleSortDirection, }) => { + const { t } = useTranslation(); const theme = useTheme(); const getFilterButtonStyle = (isActive) => ({ @@ -67,7 +69,7 @@ const PodGroupsTableHeader = ({ fontWeight="700" color="text.primary" > - Name + {t("name")} @@ -94,7 +96,7 @@ const PodGroupsTableHeader = ({ fontWeight="700" color="text.primary" > - Namespace + {t("namespace")} - Queue + {t("queue")} @@ -153,7 +157,7 @@ const PodGroupsTableHeader = ({ fontWeight="700" color="text.primary" > - Min Member + {t("min_member")} @@ -174,7 +178,7 @@ const PodGroupsTableHeader = ({ fontWeight="700" color="text.primary" > - Creation Time + {t("creation_time")} @@ -239,7 +243,7 @@ const PodGroupsTableHeader = ({ fontWeight="700" color="text.primary" > - Status + {t("status")} { + const { t } = useTranslation(); return ( { }, }} > - Pod YAML - {podName} + {t("pod_details_title", { podName })} { }, }} > - Close + {t("close")} diff --git a/frontend/src/components/pods/Pods.jsx b/frontend/src/components/pods/Pods.jsx index 652e1902..71e60aed 100644 --- a/frontend/src/components/pods/Pods.jsx +++ b/frontend/src/components/pods/Pods.jsx @@ -7,8 +7,10 @@ import { fetchAllNamespaces } from "../utils"; import PodsTable from "./PodsTable/PodsTable"; import PodsPagination from "./PodsPagination"; import PodDetailsDialog from "./PodDetailsDialog"; +import { useTranslation } from "react-i18next"; const Pods = () => { + const { t } = useTranslation(); const [pods, setPods] = useState([]); const [cachedPods, setCachedPods] = useState([]); const [, setLoading] = useState(true); @@ -51,7 +53,7 @@ const Pods = () => { setCachedPods(data.items || []); setTotalPods(data.totalCount || 0); } catch (err) { - setError("Failed to fetch pods: " + err.message); + setError(t("fetch_pods_error") + err.message); setCachedPods([]); } finally { setLoading(false); @@ -94,7 +96,7 @@ const Pods = () => { setPods(data); } } catch (error) { - console.error("Error fetching pods:", error); + console.error(t("fetch_pods_error"), error); } }; @@ -107,21 +109,21 @@ const Pods = () => { }); if (!response.ok) { - let errorMsg = "Unknown error"; + let errorMsg = t("unexpected_error"); try { const errData = await response.json(); errorMsg = errData.error || response.statusText; } catch { // ignore error } - alert("Error creating pod: " + errorMsg); + alert(t("create_pod_error") + errorMsg); return; } - alert("Pod created successfully!"); + alert(t("create_pod_success")); await fetchData(); // Now fetchData is defined in the same scope } catch (err) { - alert("Network error: " + err.message); + alert(t("network_error") + err.message); } }; @@ -150,8 +152,8 @@ const Pods = () => { setSelectedPodYaml(formattedYaml); setOpenDialog(true); } catch (err) { - console.error("Failed to fetch pod YAML:", err); - setError("Failed to fetch pod YAML: " + err.message); + console.error(t("fetch_job_yaml_error"), err); + setError(t("fetch_job_yaml_error") + err.message); } finally { setLoading(false); } @@ -185,7 +187,7 @@ const Pods = () => { {error} )} - + { handleRefresh={handleRefresh} fetchData={fetchPods} isRefreshing={false} // Update if needed - placeholder="Search Pods..." - refreshLabel="Refresh Pods" - createlabel="Create Pod" - dialogTitle="Create a Pod" - dialogResourceNameLabel="Pod Name" + placeholder={t("search_pods_placeholder")} + refreshLabel={t("refresh_pods_label")} + createlabel={t("create_pod_label")} + dialogTitle={t("create_pod_dialog_title")} + dialogResourceNameLabel={t("pod_name_label")} dialogResourceType="Pod" onCreateClick={handleCreatePod} /> diff --git a/frontend/src/components/pods/PodsPagination.jsx b/frontend/src/components/pods/PodsPagination.jsx index c0d4db39..23fe78c0 100644 --- a/frontend/src/components/pods/PodsPagination.jsx +++ b/frontend/src/components/pods/PodsPagination.jsx @@ -1,7 +1,9 @@ import React from "react"; import { Box, MenuItem, Pagination, Select, Typography } from "@mui/material"; +import { useTranslation } from "react-i18next"; const PodsPagination = ({ totalPods, pagination, onPaginationChange }) => { + const { t } = useTranslation(); const handleChangePage = (event, newPage) => { onPaginationChange(newPage, null); }; @@ -24,9 +26,9 @@ const PodsPagination = ({ totalPods, pagination, onPaginationChange }) => { onChange={handleChangeRowsPerPage} size="small" > - 5 per page - 10 per page - 20 per page + {t("items_per_page", { count: 5 })} + {t("items_per_page", { count: 10 })} + {t("items_per_page", { count: 20 })} { }} > - Total Pods: {totalPods} + {t("total_pods_count", { count: totalPods })} ( - handleClose(filterType, null)} - > - {items.map((item) => ( - handleClose(filterType, item)}> - {item} - - ))} - -); +const FilterMenu = ({ anchorEl, handleClose, items, filterType }) => { + const { t } = useTranslation(); + return ( + handleClose(filterType, null)} + > + {items.map((item) => ( + handleClose(filterType, item)}> + {item === "All" + ? t("all") + : filterType === "status" + ? t(item.toLowerCase()) + : item} + + ))} + + ); +}; export default FilterMenu; diff --git a/frontend/src/components/pods/PodsTable/PodRow.jsx b/frontend/src/components/pods/PodsTable/PodRow.jsx index 1d8944a6..0e587a25 100644 --- a/frontend/src/components/pods/PodsTable/PodRow.jsx +++ b/frontend/src/components/pods/PodsTable/PodRow.jsx @@ -1,8 +1,9 @@ -import React from "react"; import { TableRow, TableCell, Chip, useTheme, alpha } from "@mui/material"; +import { useTranslation } from "react-i18next"; import { calculateAge } from "../../utils"; const PodRow = ({ pod, getStatusColor, onPodClick }) => { + const { t } = useTranslation(); const theme = useTheme(); return ( @@ -62,7 +63,11 @@ const PodRow = ({ pod, getStatusColor, onPodClick }) => { { + const { t } = useTranslation(); const theme = useTheme(); return ( - {["Name", "Namespace", "Creation Time", "Status", "Age"].map( - (header) => ( - ( + + - + {(header.id === "Namespace" || + header.id === "Status") && ( + - )} - {header === "Creation Time" && ( - + )} + {header.id === "Creation Time" && ( + - )} - - ), - )} + transform: "translateY(-2px)", + boxShadow: `0 4px 8px ${alpha( + theme.palette.primary.main, + 0.2, + )}`, + }, + }} + > + {t("sort")} + + )} + + ))} diff --git a/frontend/src/components/queues/QueuePagination.jsx b/frontend/src/components/queues/QueuePagination.jsx index 4c2c8327..b69b9748 100644 --- a/frontend/src/components/queues/QueuePagination.jsx +++ b/frontend/src/components/queues/QueuePagination.jsx @@ -1,5 +1,6 @@ import React from "react"; import { Box, Typography, Pagination, Select, MenuItem } from "@mui/material"; +import { useTranslation } from "react-i18next"; const QueuePagination = ({ pagination, @@ -7,6 +8,7 @@ const QueuePagination = ({ handleChangeRowsPerPage, handleChangePage, }) => { + const { t } = useTranslation(); return ( - 5 per page - 10 per page - 20 per page + {t("items_per_page", { count: 5 })} + {t("items_per_page", { count: 10 })} + {t("items_per_page", { count: 20 })} - Total Queues: {totalQueues} + {t("total_queues_count", { count: totalQueues })} { }; const EditQueueDialog = ({ open, queue, onClose, onSave }) => { + const { t } = useTranslation(); const [editorValue, setEditorValue] = useState(""); const [editMode, setEditMode] = useState("yaml"); const [formState, setFormState] = useState({}); @@ -264,14 +266,14 @@ const EditQueueDialog = ({ open, queue, onClose, onSave }) => { alignItems: "center", }} > - Edit Queue + {t("edit_queue")} - YAML - Form + {t("yaml")} + {t("form")} @@ -304,7 +306,7 @@ const EditQueueDialog = ({ open, queue, onClose, onSave }) => { variant="contained" disabled={saving} > - Cancel + {t("cancel")} diff --git a/frontend/src/components/queues/QueueTable/QueueTable.jsx b/frontend/src/components/queues/QueueTable/QueueTable.jsx index d4296ffc..fbeabbb1 100644 --- a/frontend/src/components/queues/QueueTable/QueueTable.jsx +++ b/frontend/src/components/queues/QueueTable/QueueTable.jsx @@ -9,6 +9,7 @@ import { useTheme, alpha, } from "@mui/material"; +import { useTranslation } from "react-i18next"; import QueueTableHeader from "./QueueTableHeader"; import QueueTableRow from "./QueueTableRow"; import QueueTableDeleteDialog from "./QueueTableDeleteDialog"; @@ -27,6 +28,7 @@ const QueueTable = ({ setAnchorEl, onQueueUpdate, }) => { + const { t } = useTranslation(); const theme = useTheme(); const [queues, setQueues] = useState([]); @@ -84,7 +86,7 @@ const QueueTable = ({ } if (!response.ok) { - let customMessage = `queues.scheduling.volcano.sh "${queueToDelete}" is forbidden.`; + let customMessage = t("delete_queue_forbidden", { queueToDelete }); let errorType = "UnknownError"; if ( @@ -100,7 +102,7 @@ const QueueTable = ({ } } - const fullMessage = `Cannot delete "${queueToDelete}". Error message: ${customMessage}`; + const fullMessage = t("delete_queue_error_msg", { queueToDelete, customMessage }); const error = new Error(fullMessage); error.type = errorType; error.status = response.status; @@ -114,7 +116,7 @@ const QueueTable = ({ handleCloseDeleteDialog(); } catch (error) { console.error("Error deleting queue:", error); - setDeleteError(error.message || "An unexpected error occurred."); + setDeleteError(error.message || t("unexpected_error")); } finally { setIsDeleting(false); } @@ -174,7 +176,7 @@ const QueueTable = ({ colSpan={allocatedFields.length + 2} align="center" > - No queues found. + {t("no_queues_found")} ) : ( diff --git a/frontend/src/components/queues/QueueTable/QueueTableDeleteDialog.jsx b/frontend/src/components/queues/QueueTable/QueueTableDeleteDialog.jsx index b357efa4..c472ffaf 100644 --- a/frontend/src/components/queues/QueueTable/QueueTableDeleteDialog.jsx +++ b/frontend/src/components/queues/QueueTable/QueueTableDeleteDialog.jsx @@ -9,6 +9,7 @@ import { Alert, } from "@mui/material"; import CloseIcon from "@mui/icons-material/Close"; +import { useTranslation } from "react-i18next"; const QueueTableDeleteDialog = ({ open, @@ -17,6 +18,7 @@ const QueueTableDeleteDialog = ({ queueToDelete, error, }) => { + const { t } = useTranslation(); const showOnlyError = Boolean(error); return ( @@ -28,7 +30,7 @@ const QueueTableDeleteDialog = ({ alignItems: "center", }} > - {showOnlyError ? "Error" : "Delete Queue"} + {showOnlyError ? t("error") : t("delete_queue")} @@ -38,14 +40,14 @@ const QueueTableDeleteDialog = ({ {showOnlyError ? ( {error} ) : ( - `Are you sure you want to delete queue "${queueToDelete}"? This action cannot be undone.` + t("delete_queue_confirm_msg", { queueToDelete }) )} {!showOnlyError && ( )} {!showOnlyError && ( @@ -54,7 +56,7 @@ const QueueTableDeleteDialog = ({ color="error" variant="contained" > - Delete + {t("delete")} )} diff --git a/frontend/src/components/queues/QueueTable/QueueTableHeader.jsx b/frontend/src/components/queues/QueueTable/QueueTableHeader.jsx index 9988d8d1..81a27602 100644 --- a/frontend/src/components/queues/QueueTable/QueueTableHeader.jsx +++ b/frontend/src/components/queues/QueueTable/QueueTableHeader.jsx @@ -18,6 +18,7 @@ import { FilterList, UnfoldMore, } from "@mui/icons-material"; +import { useTranslation } from "react-i18next"; const QueueTableHeader = ({ allocatedFields, @@ -30,6 +31,7 @@ const QueueTableHeader = ({ handleFilterClose, setAnchorEl, }) => { + const { t } = useTranslation(); const theme = useTheme(); return ( @@ -53,7 +55,7 @@ const QueueTableHeader = ({ color="text.primary" sx={{ letterSpacing: "0.02em" }} > - Name + {t("name")} @@ -83,7 +85,7 @@ const QueueTableHeader = ({ color="text.primary" sx={{ letterSpacing: "0.02em" }} > - {`Allocated ${field}`} + {`${field.toUpperCase()}${t("allocated_suffix")}`} - Creation Time + {t("creation_time")} - State + {t("state")} - {status} + {status === "All" + ? t("all") + : t(status.toLowerCase())} ))} @@ -312,7 +318,7 @@ const QueueTableHeader = ({ color="text.primary" sx={{ letterSpacing: "0.02em" }} > - Actions + {t("actions")} diff --git a/frontend/src/components/queues/QueueTable/QueueTableRow.jsx b/frontend/src/components/queues/QueueTable/QueueTableRow.jsx index 7ef3ed68..21498709 100644 --- a/frontend/src/components/queues/QueueTable/QueueTableRow.jsx +++ b/frontend/src/components/queues/QueueTable/QueueTableRow.jsx @@ -1,5 +1,6 @@ import React, { useState } from "react"; import { TableRow, TableCell, Box, Chip, useTheme, alpha } from "@mui/material"; +import { useTranslation } from "react-i18next"; import { Delete } from "@mui/icons-material"; import { IconButton } from "@mui/material"; import Edit from "@mui/icons-material/Edit"; @@ -12,6 +13,7 @@ const QueueTableRow = ({ handleOpenDeleteDialog, onQueueUpdate, }) => { + const { t } = useTranslation(); const theme = useTheme(); const [isEditDialogOpen, setIsEditDialogOpen] = useState(false); @@ -105,7 +107,11 @@ const QueueTableRow = ({ { + const { t } = useTranslation(); return ( - Queue YAML - {selectedQueueName} + {t("queue_yaml_title", { selectedQueueName })} - Close + {t("close")} diff --git a/frontend/src/components/queues/Queues.jsx b/frontend/src/components/queues/Queues.jsx index 26b43a62..87e20b84 100644 --- a/frontend/src/components/queues/Queues.jsx +++ b/frontend/src/components/queues/Queues.jsx @@ -7,8 +7,10 @@ import QueueTable from "./QueueTable/QueueTable"; import QueuePagination from "./QueuePagination"; import QueueYamlDialog from "./QueueYamlDialog"; import TitleComponent from "../Titlecomponent"; +import { useTranslation } from "react-i18next"; const Queues = () => { + const { t } = useTranslation(); const [queues, setQueues] = useState([]); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); @@ -47,7 +49,7 @@ const Queues = () => { setQueues(data.items || []); setTotalQueues(data.totalCount || 0); } catch (err) { - setError("Failed to fetch queues: " + err.message); + setError({ key: "fetch_queues_error", message: err.message }); setQueues([]); } finally { setLoading(false); @@ -66,14 +68,14 @@ const Queues = () => { if (response.status !== 201) { let errMsg = response.data?.error || response.statusText; - alert("Failed to create queue: " + errMsg); + alert(t("create_queue_error") + errMsg); return; } - alert("Queue created successfully!"); + alert(t("create_queue_success")); } catch (err) { alert( - "Network error: " + (err?.response?.data?.error || err.message), + t("network_error") + (err?.response?.data?.error || err.message), ); } finally { setLoading(false); @@ -122,7 +124,7 @@ const Queues = () => { setOpenDialog(true); } catch (err) { console.error("Failed to fetch queue YAML:", err); - setError("Failed to fetch queue YAML: " + err.message); + setError({ key: "fetch_yaml_error", message: err.message }); } finally { setLoading(false); } @@ -230,10 +232,12 @@ const Queues = () => { {error && ( - {error} + + {error.key ? `${t(error.key)}${error.message}` : error} + )} - + { handleRefresh={handleRefresh} fetchData={fetchQueues} isRefreshing={loading} - placeholder="Search queues..." - refreshLabel="Refresh Queues" - createlabel="Create Queue" + placeholder={t("search_queues_placeholder")} + refreshLabel={t("refresh_queues_label")} + createlabel={t("create_queue")} onCreateClick={handleCreateQueue} - dialogTitle="Create a Queue" - dialogResourceNameLabel="Queue Name" + dialogTitle={t("create_queue")} + dialogResourceNameLabel={t("queue_name")} dialogResourceType="Queue" /> diff --git a/frontend/src/i18n.js b/frontend/src/i18n.js new file mode 100644 index 00000000..e31a7948 --- /dev/null +++ b/frontend/src/i18n.js @@ -0,0 +1,21 @@ +import i18n from 'i18next'; +import { initReactI18next } from 'react-i18next'; +import LanguageDetector from 'i18next-browser-languagedetector'; +import HttpApi from 'i18next-http-backend'; + +i18n + .use(HttpApi) + .use(LanguageDetector) + .use(initReactI18next) + .init({ + fallbackLng: 'en', + debug: false, + interpolation: { + escapeValue: false, + }, + backend: { + loadPath: '/locales/{{lng}}/translation.json', + }, + }); + +export default i18n; diff --git a/frontend/src/main.jsx b/frontend/src/main.jsx index 0740bc4c..5a9c168e 100644 --- a/frontend/src/main.jsx +++ b/frontend/src/main.jsx @@ -2,6 +2,7 @@ import { StrictMode } from "react"; import { createRoot } from "react-dom/client"; import "./index.css"; import App from "./App.jsx"; +import "./i18n"; createRoot(document.getElementById("root")).render( From 76537a1a952406b05bb7a49e2117c7b7eead0bf3 Mon Sep 17 00:00:00 2001 From: aryunewaskar77-art Date: Sun, 17 May 2026 00:31:12 +0530 Subject: [PATCH 2/4] feat(i18n): add English and Chinese localization support for Volcano Dashboard - Initialize react-i18next and configure EN/ZH translation infrastructure - Localize Dashboard, Jobs, Pods, Queues, and PodGroups views - Add persistent language switcher with robust language detection - Implement interpolation-based dynamic resource count translations - Refactor error handling for reactive language updates - Standardize Kubernetes terminology and dynamic labels - Localize resource creation dialogs and dynamic placeholders - Clean up duplicate keys and improve translation consistency Signed-off-by: aryunewaskar77-art --- frontend/public/locales/en/translation.json | 6 ++++-- frontend/public/locales/zh/translation.json | 6 ++++-- frontend/src/components/Layout.jsx | 2 +- frontend/src/components/pods/Pods.jsx | 8 +++++--- .../src/components/queues/QueueTable/QueueTableHeader.jsx | 2 +- 5 files changed, 15 insertions(+), 9 deletions(-) diff --git a/frontend/public/locales/en/translation.json b/frontend/public/locales/en/translation.json index 4a9fba52..be88cc64 100644 --- a/frontend/public/locales/en/translation.json +++ b/frontend/public/locales/en/translation.json @@ -79,7 +79,7 @@ "create_pod_label": "Create Pod", "create_pod_dialog_title": "Create a Pod", "pod_name_label": "Pod Name", - "pod_details_title": "Pod Details", + "pod_details_title": "Pod Details - {{podName}}", "rows_per_page": "Rows per page:", "per_page": "per page", "total_pods": "Total Pods", @@ -148,5 +148,7 @@ "state": "State", "guarantee_resources_optional": "Guarantee Resources (Optional)", "capability_resources_optional": "Capability Resources (Optional)", - "deserved_resources_optional": "Deserved Resources (Optional)" + "deserved_resources_optional": "Deserved Resources (Optional)", + "queue_yaml_title": "Queue YAML - {{selectedQueueName}}", + "yaml": "YAML" } diff --git a/frontend/public/locales/zh/translation.json b/frontend/public/locales/zh/translation.json index 4f2e31a0..8cad77a8 100644 --- a/frontend/public/locales/zh/translation.json +++ b/frontend/public/locales/zh/translation.json @@ -83,7 +83,7 @@ "create_pod_label": "创建 Pod", "create_pod_dialog_title": "创建 Pod", "pod_name_label": "Pod 名称", - "pod_details_title": "Pod 详情", + "pod_details_title": "Pod 详情 - {{podName}}", "rows_per_page": "每页行数:", "per_page": "每页", "total_pods": "Pods 总数", @@ -152,5 +152,7 @@ "state": "状态", "guarantee_resources_optional": "保证资源(可选)", "capability_resources_optional": "能力资源(可选)", - "deserved_resources_optional": "应得资源(可选)" + "deserved_resources_optional": "应得资源(可选)", + "queue_yaml_title": "队列 YAML - {{selectedQueueName}}", + "yaml": "YAML" } \ No newline at end of file diff --git a/frontend/src/components/Layout.jsx b/frontend/src/components/Layout.jsx index 9a700990..313cf504 100644 --- a/frontend/src/components/Layout.jsx +++ b/frontend/src/components/Layout.jsx @@ -90,7 +90,7 @@ const Layout = () => { } sx={{ color: "white" }} > - {i18n.language === "en" ? "中文" : "EN"} + {i18n.language.startsWith("en") ? "中文" : "EN"} diff --git a/frontend/src/components/pods/Pods.jsx b/frontend/src/components/pods/Pods.jsx index 71e60aed..b23bff01 100644 --- a/frontend/src/components/pods/Pods.jsx +++ b/frontend/src/components/pods/Pods.jsx @@ -53,7 +53,7 @@ const Pods = () => { setCachedPods(data.items || []); setTotalPods(data.totalCount || 0); } catch (err) { - setError(t("fetch_pods_error") + err.message); + setError({ key: "fetch_pods_error", message: err.message }); setCachedPods([]); } finally { setLoading(false); @@ -153,7 +153,7 @@ const Pods = () => { setOpenDialog(true); } catch (err) { console.error(t("fetch_job_yaml_error"), err); - setError(t("fetch_job_yaml_error") + err.message); + setError({ key: "fetch_job_yaml_error", message: err.message }); } finally { setLoading(false); } @@ -184,7 +184,9 @@ const Pods = () => { {error && ( - {error} + + {error.key ? `${t(error.key)}${error.message}` : error} + )} diff --git a/frontend/src/components/queues/QueueTable/QueueTableHeader.jsx b/frontend/src/components/queues/QueueTable/QueueTableHeader.jsx index 81a27602..a0be1267 100644 --- a/frontend/src/components/queues/QueueTable/QueueTableHeader.jsx +++ b/frontend/src/components/queues/QueueTable/QueueTableHeader.jsx @@ -85,7 +85,7 @@ const QueueTableHeader = ({ color="text.primary" sx={{ letterSpacing: "0.02em" }} > - {`${field.toUpperCase()}${t("allocated_suffix")}`} + {`${t(field.toLowerCase())}${t("allocated_suffix")}`} Date: Mon, 18 May 2026 16:24:40 +0530 Subject: [PATCH 3/4] feat(poc): add scheduler config and logs dashboard exploration Signed-off-by: aryunewaskar77-art --- backend/src/server.js | 152 ++ backend/test_patch.js | 22 + backend/test_patch.mjs | 25 + deployment/volcano-dashboard.yaml | 15 + frontend/package.json | 10 +- frontend/public/locales/en/translation.json | 2 + frontend/public/locales/zh/translation.json | 2 + frontend/src/App.jsx | 4 + frontend/src/components/Layout.jsx | 4 + .../src/pages/Config/ActionPipelineEditor.tsx | 212 +++ .../src/pages/Config/PluginTierEditor.tsx | 428 ++++++ frontend/src/pages/Config/YamlPreview.tsx | 107 ++ frontend/src/pages/Config/index.tsx | 167 +++ frontend/src/pages/Config/types.ts | 20 + frontend/src/pages/Config/yamlUtils.ts | 111 ++ package-lock.json | 1334 +++++++++++++++-- 16 files changed, 2529 insertions(+), 86 deletions(-) create mode 100644 backend/test_patch.js create mode 100644 backend/test_patch.mjs create mode 100644 frontend/src/pages/Config/ActionPipelineEditor.tsx create mode 100644 frontend/src/pages/Config/PluginTierEditor.tsx create mode 100644 frontend/src/pages/Config/YamlPreview.tsx create mode 100644 frontend/src/pages/Config/index.tsx create mode 100644 frontend/src/pages/Config/types.ts create mode 100644 frontend/src/pages/Config/yamlUtils.ts diff --git a/backend/src/server.js b/backend/src/server.js index 1a0b02c9..3266ca90 100644 --- a/backend/src/server.js +++ b/backend/src/server.js @@ -4,8 +4,10 @@ import { CoreV1Api, CustomObjectsApi, KubeConfig, + Log, } from "@kubernetes/client-node"; import yaml from "js-yaml"; +import stream from "stream"; export const app = express(); app.use(express.json()); @@ -984,6 +986,156 @@ app.delete("/api/queues/:name", async (req, res) => { } }); +// GET Config endpoint +app.get("/api/v1/config", async (req, res) => { + try { + // v1.x ObjectParamAPI returns the object directly (no .body wrapper) + const cm = await k8sCoreApi.readNamespacedConfigMap({ + name: "volcano-scheduler-configmap", + namespace: "volcano-system", + }); + const conf = cm.data ? cm.data["volcano-scheduler.conf"] : ""; + res.json({ conf }); + } catch (err) { + console.error("Error reading volcano configmap:", err); + res.status(500).json({ + error: "Failed to read volcano scheduler configuration", + details: err.body?.message || err.message, + }); + } +}); + +// PATCH Config endpoint +app.patch("/api/v1/config", async (req, res) => { + try { + const { conf } = req.body; + if (typeof conf !== "string") { + return res.status(400).json({ error: "Invalid body, conf must be string" }); + } + + const patchBody = { + data: { + "volcano-scheduler.conf": conf, + }, + }; + + const options = { + headers: { "Content-Type": "application/merge-patch+json" }, + }; + + const updated = await k8sCoreApi.patchNamespacedConfigMap( + { + name: "volcano-scheduler-configmap", + namespace: "volcano-system", + body: patchBody, + }, + options, + ); + + res.json({ + message: "Scheduler configuration updated successfully", + data: updated, + }); + } catch (err) { + console.error("Error patching volcano configmap:", err); + // Log additional details for debugging + if (err.response) { + console.error("Response body:", err.response.body); + } + res.status(500).json({ + error: "Failed to update volcano scheduler configuration", + details: err.body?.message || err.message, + }); + } +}); + +// GET logs EventSource SSE stream +app.get("/api/v1/logs", async (req, res) => { + const { component } = req.query; + const tailLines = parseInt(req.query.tailLines) || 200; + + if (!component) { + return res.status(400).json({ error: "Missing component query parameter" }); + } + + try { + // Resolve target pod in namespace volcano-system + const labelSelector = `app=${component}`; + console.log(`Resolving pod for logs stream with selector: ${labelSelector}`); + const podListResponse = await k8sCoreApi.listNamespacedPod({ + namespace: "volcano-system", + labelSelector, + }); + const pods = podListResponse.items || []; + const runningPod = pods.find((p) => p.status?.phase === "Running") || pods[0]; + + if (!runningPod) { + return res.status(404).json({ + error: `No pods found for component ${component} in namespace volcano-system`, + }); + } + + const podName = runningPod.metadata.name; + console.log(`Streaming logs from pod: ${podName}`); + + // Set SSE Headers + res.writeHead(200, { + "Content-Type": "text/event-stream", + "Cache-Control": "no-cache", + "Connection": "keep-alive", + }); + res.flushHeaders(); + + // Initialize Log Stream + const k8sLog = new Log(kc); + const logStream = new stream.PassThrough(); + + logStream.on("data", (chunk) => { + const dataStr = chunk.toString(); + const lines = dataStr.split("\n"); + lines.forEach((line) => { + if (line.trim()) { + res.write(`data: ${line}\n\n`); + } + }); + }); + + const logRequest = await k8sLog.log( + "volcano-system", + podName, + undefined, // containerName + logStream, + { + follow: true, + tailLines, + pretty: false, + } + ); + + // Handle Disconnection + req.on("close", () => { + console.log(`Client disconnected from log stream for ${podName}`); + try { + if (logRequest) logRequest.destroy(); + } catch (err) { + console.error("Error destroying log request:", err); + } + try { + logStream.destroy(); + } catch (err) { + console.error("Error destroying log stream:", err); + } + }); + + } catch (err) { + console.error("Error initiating log stream:", err); + res.status(500).json({ + error: "Failed to initiate log stream", + details: err.body?.message || err.message, + }); + } +}); + const verifyVolcanoSetup = async () => { try { // Verify CRD access diff --git a/backend/test_patch.js b/backend/test_patch.js new file mode 100644 index 00000000..93031d6f --- /dev/null +++ b/backend/test_patch.js @@ -0,0 +1,22 @@ +const k8s = require('@kubernetes/client-node'); +const kc = new k8s.KubeConfig(); +kc.loadFromDefault(); +const api = kc.makeApiClient(k8s.CoreV1Api); + +async function test() { + try { + const options = { headers: { 'Content-Type': 'application/merge-patch+json' } }; + const res = await api.patchNamespacedConfigMap( + { + name: 'volcano-scheduler-configmap', + namespace: 'volcano-system', + body: { data: { "volcano-scheduler.conf": "test" } } + }, + options + ); + console.log("Success!"); + } catch (err) { + console.log("Error:", err.body); + } +} +test(); diff --git a/backend/test_patch.mjs b/backend/test_patch.mjs new file mode 100644 index 00000000..b72af577 --- /dev/null +++ b/backend/test_patch.mjs @@ -0,0 +1,25 @@ +import * as k8s from '@kubernetes/client-node'; +const kc = new k8s.KubeConfig(); +kc.loadFromDefault(); +const api = kc.makeApiClient(k8s.CoreV1Api); + +async function test() { + try { + const options = { headers: { 'Content-Type': 'application/merge-patch+json' } }; + const res = await api.patchNamespacedConfigMap( + 'volcano-scheduler-configmap', + 'volcano-system', + { data: { "volcano-scheduler.conf": "test" } }, + undefined, + undefined, + undefined, + undefined, + undefined, + options + ); + console.log("Success!"); + } catch (err) { + console.log("Error:", err.body); + } +} +test(); diff --git a/deployment/volcano-dashboard.yaml b/deployment/volcano-dashboard.yaml index 87afe97f..fa5ae677 100644 --- a/deployment/volcano-dashboard.yaml +++ b/deployment/volcano-dashboard.yaml @@ -113,6 +113,21 @@ rules: - get - list - watch + - apiGroups: + - "" + resources: + - configmaps + resourceNames: + - volcano-scheduler-configmap + verbs: + - get + - patch + - apiGroups: + - "" + resources: + - pods/log + verbs: + - get - apiGroups: - "" resources: diff --git a/frontend/package.json b/frontend/package.json index 6b749b3a..a176a36b 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -14,6 +14,10 @@ "test:ui": "vitest --ui" }, "dependencies": { + "@ant-design/icons": "^6.2.3", + "@dnd-kit/core": "^6.3.1", + "@dnd-kit/sortable": "^10.0.0", + "@dnd-kit/utilities": "^3.2.2", "@emotion/react": "^11.14.0", "@emotion/styled": "^11.14.0", "@fortawesome/free-solid-svg-icons": "^6.7.2", @@ -21,6 +25,7 @@ "@monaco-editor/react": "^4.7.0", "@mui/icons-material": "^6.4.4", "@mui/material": "^6.4.4", + "antd": "^6.4.3", "axios": "^1.7.9", "bootstrap": "^5.3.5", "chart.js": "^4.4.7", @@ -34,9 +39,12 @@ "react": "^19.0.0", "react-bootstrap": "^2.10.9", "react-chartjs-2": "^5.3.0", + "react-diff-viewer-continued": "^4.2.2", "react-dom": "^19.0.0", "react-i18next": "^17.0.8", - "react-router-dom": "^7.1.5" + "react-router-dom": "^7.1.5", + "react-virtuoso": "^4.18.7", + "uuid": "^14.0.0" }, "devDependencies": { "@eslint/js": "^9.19.0", diff --git a/frontend/public/locales/en/translation.json b/frontend/public/locales/en/translation.json index be88cc64..c545cc4b 100644 --- a/frontend/public/locales/en/translation.json +++ b/frontend/public/locales/en/translation.json @@ -1,6 +1,8 @@ { "dashboard": "Dashboard", "jobs": "Jobs", + "config": "Config", + "logs": "Logs", "queues": "Queues", "pods": "Pods", "pod_groups": "PodGroups", diff --git a/frontend/public/locales/zh/translation.json b/frontend/public/locales/zh/translation.json index 8cad77a8..290054e6 100644 --- a/frontend/public/locales/zh/translation.json +++ b/frontend/public/locales/zh/translation.json @@ -1,6 +1,8 @@ { "dashboard": "仪表板", "jobs": "作业", + "config": "配置", + "logs": "日志", "queues": "队列", "pods": "Pods", "pod_groups": "PodGroups", diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx index 70ea4b58..06d683e4 100644 --- a/frontend/src/App.jsx +++ b/frontend/src/App.jsx @@ -11,6 +11,8 @@ import Jobs from "./components/jobs/Jobs"; import Queues from "./components/queues/Queues"; import Pods from "./components/pods/Pods"; import PodGroups from "./components/podgroups/PodGroups"; +import ConfigPage from "./pages/Config"; +import LogsPage from "./pages/Logs"; import { ThemeProvider } from "@mui/material/styles"; import { theme } from "./theme"; import "bootstrap/dist/css/bootstrap.min.css"; @@ -30,6 +32,8 @@ function App() { } /> } /> } /> + } /> + } /> diff --git a/frontend/src/components/Layout.jsx b/frontend/src/components/Layout.jsx index 313cf504..93680679 100644 --- a/frontend/src/components/Layout.jsx +++ b/frontend/src/components/Layout.jsx @@ -21,6 +21,8 @@ import AssignmentIcon from "@mui/icons-material/Assignment"; import WorkspacesIcon from "@mui/icons-material/Workspaces"; import CategoryIcon from "@mui/icons-material/Category"; import LanguageIcon from "@mui/icons-material/Language"; +import SettingsIcon from "@mui/icons-material/Settings"; +import TerminalIcon from "@mui/icons-material/Terminal"; import { useTranslation } from "react-i18next"; // use relative path to load Logo @@ -47,6 +49,8 @@ const Layout = () => { { text: t("queues"), icon: , path: "/queues" }, { text: t("pods"), icon: , path: "/pods" }, { text: t("pod_groups"), icon: , path: "/podgroups" }, + { text: t("config"), icon: , path: "/config" }, + { text: t("logs"), icon: , path: "/logs" }, ]; return ( diff --git a/frontend/src/pages/Config/ActionPipelineEditor.tsx b/frontend/src/pages/Config/ActionPipelineEditor.tsx new file mode 100644 index 00000000..05d07d9e --- /dev/null +++ b/frontend/src/pages/Config/ActionPipelineEditor.tsx @@ -0,0 +1,212 @@ +import React from "react"; +import { + DndContext, + closestCenter, + KeyboardSensor, + PointerSensor, + useSensor, + useSensors, + DragEndEvent, +} from "@dnd-kit/core"; +import { + arrayMove, + SortableContext, + sortableKeyboardCoordinates, + useSortable, + horizontalListSortingStrategy, +} from "@dnd-kit/sortable"; +import { CSS } from "@dnd-kit/utilities"; +import { Chip, Box, Typography, Paper, Button } from "@mui/material"; +import DragIndicatorIcon from "@mui/icons-material/DragIndicator"; +import AddIcon from "@mui/icons-material/Add"; +import { useTranslation } from "react-i18next"; + +const KNOWN_ACTIONS = [ + "enqueue", + "allocate", + "backfill", + "preempt", + "reclaim", + "shuffle", +]; + +interface SortableItemProps { + id: string; + onDelete: () => void; +} + +const SortableItem: React.FC = ({ id, onDelete }) => { + const { + attributes, + listeners, + setNodeRef, + transform, + transition, + isDragging, + } = useSortable({ id }); + + const style = { + transform: CSS.Transform.toString(transform), + transition, + opacity: isDragging ? 0.5 : 1, + display: "inline-flex", + alignItems: "center", + }; + + return ( +
+ + + + } + color="primary" + variant="filled" + sx={{ + fontWeight: 500, + m: 0.5, + boxShadow: isDragging ? 3 : 1, + "& .MuiChip-label": { + paddingLeft: "8px", + }, + }} + /> +
+ ); +}; + +interface ActionPipelineEditorProps { + actions: string[]; + onChange: (newActions: string[]) => void; +} + +export const ActionPipelineEditor: React.FC = ({ + actions, + onChange, +}) => { + const { t } = useTranslation(); + const sensors = useSensors( + useSensor(PointerSensor), + useSensor(KeyboardSensor, { + coordinateGetter: sortableKeyboardCoordinates, + }), + ); + + const handleDragEnd = (event: DragEndEvent) => { + const { active, over } = event; + if (over && active.id !== over.id) { + const oldIndex = actions.indexOf(String(active.id)); + const newIndex = actions.indexOf(String(over.id)); + onChange(arrayMove(actions, oldIndex, newIndex)); + } + }; + + const handleDelete = (actionToDelete: string) => { + onChange(actions.filter((a) => a !== actionToDelete)); + }; + + const handleAdd = (actionToAdd: string) => { + if (!actions.includes(actionToAdd)) { + onChange([...actions, actionToAdd]); + } + }; + + const unusedActions = KNOWN_ACTIONS.filter((a) => !actions.includes(a)); + + return ( + + + {t("action_pipeline", "Action Pipeline")} + + + {t( + "action_pipeline_help", + "Drag and drop to reorder active scheduling actions, or click available actions to add them.", + )} + + + + {actions.length === 0 ? ( + + {t("no_actions_selected", "No actions selected. Volcano scheduler will not schedule pods.")} + + ) : ( + + + {actions.map((action) => ( + handleDelete(action)} + /> + ))} + + + )} + + + {unusedActions.length > 0 && ( + + + {t("available_actions", "Available Actions:")} + + + {unusedActions.map((action) => ( + + ))} + + + )} + + ); +}; +export default ActionPipelineEditor; diff --git a/frontend/src/pages/Config/PluginTierEditor.tsx b/frontend/src/pages/Config/PluginTierEditor.tsx new file mode 100644 index 00000000..d8f4b215 --- /dev/null +++ b/frontend/src/pages/Config/PluginTierEditor.tsx @@ -0,0 +1,428 @@ +import React, { useState } from "react"; +import { Collapse, Switch, Input, Tag, Card, Button as AntButton, Tooltip, Space } from "antd"; +import { PlusOutlined, DeleteOutlined, InfoCircleOutlined } from "@ant-design/icons"; +import { Tier, Plugin, PluginArg } from "./types"; +import { Box, Typography, Grid, Paper, Button } from "@mui/material"; +import { useTranslation } from "react-i18next"; + +const KNOWN_PLUGINS = [ + "gang", + "drf", + "predicates", + "priority", + "conformance", + "proportion", + "nodeorder", + "binpack", +]; + +interface PluginTierEditorProps { + tiers: Tier[]; + onChange: (newTiers: Tier[]) => void; +} + +export const PluginTierEditor: React.FC = ({ + tiers, + onChange, +}) => { + const { t } = useTranslation(); + const [newPluginName, setNewPluginName] = useState>({}); + + const handleTierChange = (tierIndex: number, updatedTier: Tier) => { + const newTiers = [...tiers]; + newTiers[tierIndex] = updatedTier; + onChange(newTiers); + }; + + const handleTogglePlugin = ( + tierIndex: number, + pluginIndex: number, + checked: boolean, + ) => { + const tier = tiers[tierIndex]; + const newPlugins = [...tier.plugins]; + newPlugins[pluginIndex] = { + ...newPlugins[pluginIndex], + enabled: checked, + }; + handleTierChange(tierIndex, { ...tier, plugins: newPlugins }); + }; + + const handleArgChange = ( + tierIndex: number, + pluginIndex: number, + argIndex: number, + field: keyof PluginArg, + val: any, + ) => { + const tier = tiers[tierIndex]; + const newPlugins = [...tier.plugins]; + const newArgs = [...newPlugins[pluginIndex].arguments]; + + let finalVal = val; + if (field === "value") { + // Infer type + if (val === "true" || val === true) finalVal = true; + else if (val === "false" || val === false) finalVal = false; + else if (val !== "" && !isNaN(Number(val))) finalVal = Number(val); + } + + newArgs[argIndex] = { + ...newArgs[argIndex], + [field]: finalVal, + }; + newPlugins[pluginIndex] = { + ...newPlugins[pluginIndex], + arguments: newArgs, + }; + handleTierChange(tierIndex, { ...tier, plugins: newPlugins }); + }; + + const handleAddArg = (tierIndex: number, pluginIndex: number) => { + const tier = tiers[tierIndex]; + const newPlugins = [...tier.plugins]; + const newArgs = [...newPlugins[pluginIndex].arguments, { key: "", value: "" }]; + newPlugins[pluginIndex] = { + ...newPlugins[pluginIndex], + arguments: newArgs, + }; + handleTierChange(tierIndex, { ...tier, plugins: newPlugins }); + }; + + const handleRemoveArg = ( + tierIndex: number, + pluginIndex: number, + argIndex: number, + ) => { + const tier = tiers[tierIndex]; + const newPlugins = [...tier.plugins]; + const newArgs = newPlugins[pluginIndex].arguments.filter( + (_, idx) => idx !== argIndex, + ); + newPlugins[pluginIndex] = { + ...newPlugins[pluginIndex], + arguments: newArgs, + }; + handleTierChange(tierIndex, { ...tier, plugins: newPlugins }); + }; + + const handleAddPlugin = (tierIndex: number, name: string) => { + if (!name.trim()) return; + const tier = tiers[tierIndex]; + // Check if plugin already exists in this tier + if (tier.plugins.some((p) => p.name === name)) { + return; + } + + const newPlugin: Plugin = { + name: name.trim(), + enabled: true, + arguments: [], + }; + + handleTierChange(tierIndex, { + ...tier, + plugins: [...tier.plugins, newPlugin], + }); + + // Clear input for this tier + setNewPluginName((prev) => ({ ...prev, [tier.id]: "" })); + }; + + const handleRemovePlugin = (tierIndex: number, pluginIndex: number) => { + const tier = tiers[tierIndex]; + const newPlugins = tier.plugins.filter((_, idx) => idx !== pluginIndex); + handleTierChange(tierIndex, { ...tier, plugins: newPlugins }); + }; + + const handleAddTier = () => { + const newTiers = [...tiers, { id: String(Date.now()), plugins: [] }]; + onChange(newTiers); + }; + + const handleRemoveTier = (tierIndex: number) => { + onChange(tiers.filter((_, idx) => idx !== tierIndex)); + }; + + const inferType = (val: any): "bool" | "number" | "string" => { + if (typeof val === "boolean" || val === "true" || val === "false") { + return "bool"; + } + if (val !== "" && !isNaN(Number(val)) && typeof val !== "boolean") { + return "number"; + } + return "string"; + }; + + const getTypeColor = (type: "bool" | "number" | "string") => { + switch (type) { + case "bool": + return "green"; + case "number": + return "blue"; + default: + return "orange"; + } + }; + + // Render Collapse panel items + const collapseItems = tiers.map((tier, tierIdx) => { + const header = ( + e.stopPropagation()} + > + + {t("tier", "Tier")} {tierIdx + 1} ({tier.plugins.length}{" "} + {t("plugins_count", "Plugins")}) + + } + onClick={() => handleRemoveTier(tierIdx)} + disabled={tiers.length <= 1} + > + {t("remove_tier", "Remove Tier")} + + + ); + + const content = ( + + + {tier.plugins.map((plugin, pluginIdx) => ( + + + + {plugin.name} + + + + handleTogglePlugin( + tierIdx, + pluginIdx, + checked, + ) + } + checkedChildren={t("enabled", "ON")} + unCheckedChildren={t("disabled", "OFF")} + /> + } + onClick={() => + handleRemovePlugin(tierIdx, pluginIdx) + } + size="small" + /> + + + } + style={{ + borderColor: plugin.enabled ? "#1976d2" : "#e0e0e0", + opacity: plugin.enabled ? 1 : 0.6, + }} + > + {plugin.enabled ? ( + + + {t("plugin_arguments", "Arguments:")} + + {plugin.arguments.map((arg, argIdx) => { + const type = inferType(arg.value); + return ( + + + handleArgChange( + tierIdx, + pluginIdx, + argIdx, + "key", + e.target.value, + ) + } + style={{ width: "45%" }} + /> + + handleArgChange( + tierIdx, + pluginIdx, + argIdx, + "value", + e.target.value, + ) + } + style={{ width: "35%" }} + /> + + + {type} + + + } + onClick={() => + handleRemoveArg( + tierIdx, + pluginIdx, + argIdx, + ) + } + /> + + ); + })} + } + onClick={() => handleAddArg(tierIdx, pluginIdx)} + size="small" + > + {t("add_argument", "Add Argument")} + + + ) : ( + + {t("plugin_disabled", "Plugin disabled. It will not be loaded.")} + + )} + + + ))} + + + + + setNewPluginName((prev) => ({ + ...prev, + [tier.id]: e.target.value, + })) + } + style={{ width: "200px" }} + onPressEnter={() => + handleAddPlugin(tierIdx, newPluginName[tier.id] || "") + } + /> + } + onClick={() => + handleAddPlugin(tierIdx, newPluginName[tier.id] || "") + } + > + {t("add_plugin", "Add Plugin")} + + + + {KNOWN_PLUGINS.filter( + (kp) => !tier.plugins.some((p) => p.name === kp), + ).map((kp) => ( + handleAddPlugin(tierIdx, kp)} + > + {kp} + + ))} + + + + ); + + return { + key: tier.id, + label: header, + children: content, + }; + }); + + return ( + + + {t("plugin_tiers", "Plugin Tiers")} + + + {t( + "plugin_tiers_help", + "Manage plugin activation and ordering in hierarchical tiers. Active plugins execute left-to-right within their tier.", + )} + + + t.id)} items={collapseItems} /> + + + + + + ); +}; +export default PluginTierEditor; diff --git a/frontend/src/pages/Config/YamlPreview.tsx b/frontend/src/pages/Config/YamlPreview.tsx new file mode 100644 index 00000000..0dcb8039 --- /dev/null +++ b/frontend/src/pages/Config/YamlPreview.tsx @@ -0,0 +1,107 @@ +import React, { useState } from "react"; +import { Segmented, Badge } from "antd"; +import ReactDiffViewer, { DiffMethod } from "react-diff-viewer-continued"; +import { Box, Typography, Paper } from "@mui/material"; +import { useTranslation } from "react-i18next"; + +interface YamlPreviewProps { + currentYaml: string; + savedYaml: string; + hasChanges: boolean; +} + +export const YamlPreview: React.FC = ({ + currentYaml, + savedYaml, + hasChanges, +}) => { + const { t } = useTranslation(); + const [viewMode, setViewMode] = useState("preview"); + + const segmentedOptions = [ + { label: t("raw_yaml_preview", "Raw YAML Preview"), value: "preview" }, + { label: t("diff_vs_saved", "Diff vs Saved"), value: "diff" }, + ]; + + return ( + + + + + {t("yaml_preview", "YAML Preview & Diff")} + + {hasChanges && ( + + )} + + setViewMode(String(val))} + /> + + + {viewMode === "preview" ? ( + +
+                        {currentYaml}
+                    
+
+ ) : ( + + + + )} +
+ ); +}; +export default YamlPreview; diff --git a/frontend/src/pages/Config/index.tsx b/frontend/src/pages/Config/index.tsx new file mode 100644 index 00000000..84cfb9ff --- /dev/null +++ b/frontend/src/pages/Config/index.tsx @@ -0,0 +1,167 @@ +import React, { useEffect, useState } from "react"; +import { Box, Typography, Button, CircularProgress, Alert } from "@mui/material"; +import SaveIcon from "@mui/icons-material/Save"; +import RefreshIcon from "@mui/icons-material/Refresh"; +import axios from "axios"; +import { useTranslation } from "react-i18next"; + +import { SchedulerConfig } from "./types"; +import { parseConf, serialiseConf } from "./yamlUtils"; +import { ActionPipelineEditor } from "./ActionPipelineEditor"; +import { PluginTierEditor } from "./PluginTierEditor"; +import { YamlPreview } from "./YamlPreview"; +import TitleComponent from "../../components/Titlecomponent"; + +export const ConfigPage: React.FC = () => { + const { t } = useTranslation(); + const [savedYaml, setSavedYaml] = useState(""); + const [config, setConfig] = useState(null); + const [loading, setLoading] = useState(true); + const [saving, setSaving] = useState(false); + const [error, setError] = useState(null); + + const fetchConfig = async () => { + setLoading(true); + setError(null); + try { + const response = await axios.get("/api/v1/config"); + const yamlStr = response.data.conf || ""; + setSavedYaml(yamlStr); + setConfig(parseConf(yamlStr)); + } catch (err: any) { + console.error("Error loading volcano config:", err); + setError( + err.response?.data?.details || + err.message || + t("failed_load_config", "Failed to load volcano scheduler configuration."), + ); + } finally { + setLoading(false); + } + }; + + useEffect(() => { + fetchConfig(); + }, []); + + if (loading) { + return ( + + + + ); + } + + if (error) { + return ( + + + {t("retry", "Retry")} + + }> + {error} + + + ); + } + + if (!config) return null; + + const currentYaml = serialiseConf(config); + const hasChanges = currentYaml.trim() !== savedYaml.trim(); + + const handleSave = async () => { + if (!hasChanges) return; + setSaving(true); + try { + await axios.patch("/api/v1/config", { conf: currentYaml }, { + headers: { + "Content-Type": "application/merge-patch+json", + }, + }); + setSavedYaml(currentYaml); + alert(t("config_saved_success", "Scheduler configuration saved successfully!")); + } catch (err: any) { + console.error("Error saving config:", err); + alert( + t("config_save_error", "Failed to save configuration: ") + + (err.response?.data?.details || err.message), + ); + } finally { + setSaving(false); + } + }; + + const handleDiscard = () => { + if (window.confirm(t("discard_confirm", "Are you sure you want to discard all unsaved changes?"))) { + setConfig(parseConf(savedYaml)); + } + }; + + return ( + + + + + + + + + + setConfig({ ...config, actions: newActions })} + /> + + setConfig({ ...config, tiers: newTiers })} + /> + + + + ); +}; + +export default ConfigPage; diff --git a/frontend/src/pages/Config/types.ts b/frontend/src/pages/Config/types.ts new file mode 100644 index 00000000..57c7b579 --- /dev/null +++ b/frontend/src/pages/Config/types.ts @@ -0,0 +1,20 @@ +export interface PluginArg { + key: string; + value: string | number | boolean; +} + +export interface Plugin { + name: string; + enabled: boolean; + arguments: PluginArg[]; +} + +export interface Tier { + id: string; + plugins: Plugin[]; +} + +export interface SchedulerConfig { + actions: string[]; + tiers: Tier[]; +} diff --git a/frontend/src/pages/Config/yamlUtils.ts b/frontend/src/pages/Config/yamlUtils.ts new file mode 100644 index 00000000..545a06f8 --- /dev/null +++ b/frontend/src/pages/Config/yamlUtils.ts @@ -0,0 +1,111 @@ +import yaml from "js-yaml"; +import { v4 as uuidv4 } from "uuid"; +import { SchedulerConfig, Tier, Plugin, PluginArg } from "./types"; + +export const parseConf = (yamlStr: string): SchedulerConfig => { + try { + const parsed = (yaml.load(yamlStr) || {}) as any; + + // Normalize actions + let actions: string[] = []; + if (typeof parsed.actions === "string") { + actions = parsed.actions + .split(",") + .map((a: string) => a.trim()) + .filter(Boolean); + } else if (Array.isArray(parsed.actions)) { + actions = parsed.actions.map((a: any) => String(a).trim()); + } + + // Normalize tiers + const rawTiers = Array.isArray(parsed.tiers) ? parsed.tiers : []; + const tiers: Tier[] = rawTiers.map((rawTier: any) => { + const rawPlugins = Array.isArray(rawTier.plugins) + ? rawTier.plugins + : []; + const plugins: Plugin[] = rawPlugins.map((p: any) => { + const name = p.name || ""; + const rawArgs = p.arguments || {}; + const argsList: PluginArg[] = Object.entries(rawArgs).map( + ([key, val]) => { + let typedVal: string | number | boolean = String(val); + if (val === "true" || val === true) typedVal = true; + else if (val === "false" || val === false) + typedVal = false; + else if (!isNaN(Number(val)) && typeof val !== "boolean") + typedVal = Number(val); + else typedVal = val as string | number | boolean; + + return { key, value: typedVal }; + }, + ); + + return { + name, + enabled: true, + arguments: argsList, + }; + }); + + return { + id: uuidv4(), + plugins, + }; + }); + + // Ensure at least one tier exists if none are parsed + if (tiers.length === 0) { + tiers.push({ id: uuidv4(), plugins: [] }); + } + + return { + actions, + tiers, + }; + } catch (err) { + console.error("Failed to parse scheduler config yaml:", err); + return { + actions: ["enqueue", "allocate", "backfill"], + tiers: [{ id: uuidv4(), plugins: [] }], + }; + } +}; + +export const serialiseConf = (config: SchedulerConfig): string => { + const output: any = {}; + + // Actions join + output.actions = config.actions.join(", "); + + // Serialize tiers + output.tiers = config.tiers + .map((tier) => { + const activePlugins = tier.plugins + .filter((p) => p.enabled) + .map((p) => { + const pluginObj: any = { name: p.name }; + if (p.arguments.length > 0) { + const argsObj: Record = {}; + p.arguments.forEach((arg) => { + if (arg.key.trim()) { + argsObj[arg.key.trim()] = arg.value; + } + }); + if (Object.keys(argsObj).length > 0) { + pluginObj.arguments = argsObj; + } + } + return pluginObj; + }); + + return { plugins: activePlugins }; + }) + .filter((t) => t.plugins.length > 0); + + return yaml.dump(output, { + indent: 2, + lineWidth: -1, + noRefs: true, + sortKeys: false, + }); +}; diff --git a/package-lock.json b/package-lock.json index 6d32394d..d4ae9190 100644 --- a/package-lock.json +++ b/package-lock.json @@ -78,6 +78,10 @@ "name": "volcano-dashboard-app", "version": "0.0.0", "dependencies": { + "@ant-design/icons": "^6.2.3", + "@dnd-kit/core": "^6.3.1", + "@dnd-kit/sortable": "^10.0.0", + "@dnd-kit/utilities": "^3.2.2", "@emotion/react": "^11.14.0", "@emotion/styled": "^11.14.0", "@fortawesome/free-solid-svg-icons": "^6.7.2", @@ -85,18 +89,26 @@ "@monaco-editor/react": "^4.7.0", "@mui/icons-material": "^6.4.4", "@mui/material": "^6.4.4", + "antd": "^6.4.3", "axios": "^1.7.9", "bootstrap": "^5.3.5", "chart.js": "^4.4.7", "dotenv": "^16.4.7", + "i18next": "^26.2.0", + "i18next-browser-languagedetector": "^8.2.1", + "i18next-http-backend": "^4.0.0", "js-yaml": "^4.1.0", "lodash": "^4.17.21", "lucide-react": "^0.511.0", "react": "^19.0.0", "react-bootstrap": "^2.10.9", "react-chartjs-2": "^5.3.0", + "react-diff-viewer-continued": "^4.2.2", "react-dom": "^19.0.0", - "react-router-dom": "^7.1.5" + "react-i18next": "^17.0.8", + "react-router-dom": "^7.1.5", + "react-virtuoso": "^4.18.7", + "uuid": "^14.0.0" }, "devDependencies": { "@eslint/js": "^9.19.0", @@ -205,6 +217,117 @@ "dev": true, "license": "MIT" }, + "node_modules/@ant-design/colors": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/@ant-design/colors/-/colors-8.0.1.tgz", + "integrity": "sha512-foPVl0+SWIslGUtD/xBr1p9U4AKzPhNYEseXYRRo5QSzGACYZrQbe11AYJbYfAWnWSpGBx6JjBmSeugUsD9vqQ==", + "license": "MIT", + "dependencies": { + "@ant-design/fast-color": "^3.0.0" + } + }, + "node_modules/@ant-design/cssinjs": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/@ant-design/cssinjs/-/cssinjs-2.1.2.tgz", + "integrity": "sha512-2Hy8BnCEH31xPeSLbhhB2ctCPXE2ZnASdi+KbSeS79BNbUhL9hAEe20SkUk+BR8aKTmqb6+FKFruk7w8z0VoRQ==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.11.1", + "@emotion/hash": "^0.8.0", + "@emotion/unitless": "^0.7.5", + "@rc-component/util": "^1.4.0", + "clsx": "^2.1.1", + "csstype": "^3.1.3", + "stylis": "^4.3.4" + }, + "peerDependencies": { + "react": ">=16.0.0", + "react-dom": ">=16.0.0" + } + }, + "node_modules/@ant-design/cssinjs-utils": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/@ant-design/cssinjs-utils/-/cssinjs-utils-2.1.2.tgz", + "integrity": "sha512-5fTHQ158jJJ5dC/ECeyIdZUzKxE/mpEMRZxthyG1sw/AKRHKgJBg00Yi6ACVXgycdje7KahRNvNET/uBccwCnA==", + "license": "MIT", + "dependencies": { + "@ant-design/cssinjs": "^2.1.2", + "@babel/runtime": "^7.23.2", + "@rc-component/util": "^1.4.0" + }, + "peerDependencies": { + "react": ">=18", + "react-dom": ">=18" + } + }, + "node_modules/@ant-design/cssinjs/node_modules/@emotion/hash": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/@emotion/hash/-/hash-0.8.0.tgz", + "integrity": "sha512-kBJtf7PH6aWwZ6fka3zQ0p6SBYzx4fl1LoZXE2RrnYST9Xljm7WfKJrU4g/Xr3Beg72MLrp1AWNUmuYJTL7Cow==", + "license": "MIT" + }, + "node_modules/@ant-design/cssinjs/node_modules/@emotion/unitless": { + "version": "0.7.5", + "resolved": "https://registry.npmjs.org/@emotion/unitless/-/unitless-0.7.5.tgz", + "integrity": "sha512-OWORNpfjMsSSUBVrRBVGECkhWcULOAJz9ZW8uK9qgxD+87M7jHRcvh/A96XXNhXTLmKcoYSQtBEX7lHMO7YRwg==", + "license": "MIT" + }, + "node_modules/@ant-design/cssinjs/node_modules/stylis": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/stylis/-/stylis-4.4.0.tgz", + "integrity": "sha512-5Z9ZpRzfuH6l/UAvCPAPUo3665Nk2wLaZU3x+TLHKVzIz33+sbJqbtrYoC3KD4/uVOr2Zp+L0LySezP9OHV9yA==", + "license": "MIT" + }, + "node_modules/@ant-design/fast-color": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@ant-design/fast-color/-/fast-color-3.0.1.tgz", + "integrity": "sha512-esKJegpW4nckh0o6kV3Tkb7NPIZYbPnnFxmQDUmL08ukXZAvV85TZBr70eGuke/CIArLaP6aw8lt9KILjnWuOw==", + "license": "MIT", + "engines": { + "node": ">=8.x" + } + }, + "node_modules/@ant-design/icons": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/@ant-design/icons/-/icons-6.2.3.tgz", + "integrity": "sha512-Pl3aoAtxQeKryYnt6VvDJtOxMOtA8wrRSACe/pTjOAIG3fdHrWm6Ivb4ku9tsFjYroSXBKirvuxG4QkwBXD9gg==", + "license": "MIT", + "dependencies": { + "@ant-design/colors": "^8.0.1", + "@ant-design/icons-svg": "^4.4.2", + "@rc-component/util": "^1.10.1", + "clsx": "^2.1.1" + }, + "engines": { + "node": ">=8" + }, + "peerDependencies": { + "react": ">=16.0.0", + "react-dom": ">=16.0.0" + } + }, + "node_modules/@ant-design/icons-svg": { + "version": "4.4.2", + "resolved": "https://registry.npmjs.org/@ant-design/icons-svg/-/icons-svg-4.4.2.tgz", + "integrity": "sha512-vHbT+zJEVzllwP+CM+ul7reTEfBR0vgxFe7+lREAsAA7YGsYpboiq2sQNeQeRvh09GfQgs/GyFEvZpJ9cLXpXA==", + "license": "MIT" + }, + "node_modules/@ant-design/react-slick": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@ant-design/react-slick/-/react-slick-2.0.0.tgz", + "integrity": "sha512-HMS9sRoEmZey8LsE/Yo6+klhlzU12PisjrVcydW3So7RdklyEd2qehyU6a7Yp+OYN72mgsYs3NFCyP2lCPFVqg==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.28.4", + "clsx": "^2.1.1", + "json2mq": "^0.2.0", + "throttle-debounce": "^5.0.0" + }, + "peerDependencies": { + "react": "^0.14.0 || ^15.0.1 || ^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", + "react-dom": "^0.14.0 || ^15.0.1 || ^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, "node_modules/@asamuzakjp/css-color": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/@asamuzakjp/css-color/-/css-color-3.2.0.tgz", @@ -307,7 +430,6 @@ "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.5.tgz", "integrity": "sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw==", "license": "MIT", - "peer": true, "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.5", @@ -1966,9 +2088,9 @@ } }, "node_modules/@babel/runtime": { - "version": "7.28.4", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.4.tgz", - "integrity": "sha512-Q/N6JNWvIvPnLDvjlE1OUBLPQHH6l3CltCEsHIujp45zQUSSh8K+gHnaEX45yAT1nyngnINhvWtzN+Nb9D8RAQ==", + "version": "7.29.2", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.29.2.tgz", + "integrity": "sha512-JiDShH45zKHWyGe4ZNVRrCjBz8Nh9TMmZG1kh4QTK8hCBTWBi8Da+i7s1fJw7/lYpM4ccepSNfqzZ/QvABBi5g==", "license": "MIT", "engines": { "node": ">=6.9.0" @@ -2107,7 +2229,6 @@ } ], "license": "MIT", - "peer": true, "engines": { "node": ">=18" }, @@ -2131,11 +2252,63 @@ } ], "license": "MIT", - "peer": true, "engines": { "node": ">=18" } }, + "node_modules/@dnd-kit/accessibility": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@dnd-kit/accessibility/-/accessibility-3.1.1.tgz", + "integrity": "sha512-2P+YgaXF+gRsIihwwY1gCsQSYnu9Zyj2py8kY5fFvUM1qm2WA2u639R6YNVfU4GWr+ZM5mqEsfHZZLoRONbemw==", + "license": "MIT", + "dependencies": { + "tslib": "^2.0.0" + }, + "peerDependencies": { + "react": ">=16.8.0" + } + }, + "node_modules/@dnd-kit/core": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/@dnd-kit/core/-/core-6.3.1.tgz", + "integrity": "sha512-xkGBRQQab4RLwgXxoqETICr6S5JlogafbhNsidmrkVv2YRs5MLwpjoF2qpiGjQt8S9AoxtIV603s0GIUpY5eYQ==", + "license": "MIT", + "dependencies": { + "@dnd-kit/accessibility": "^3.1.1", + "@dnd-kit/utilities": "^3.2.2", + "tslib": "^2.0.0" + }, + "peerDependencies": { + "react": ">=16.8.0", + "react-dom": ">=16.8.0" + } + }, + "node_modules/@dnd-kit/sortable": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/@dnd-kit/sortable/-/sortable-10.0.0.tgz", + "integrity": "sha512-+xqhmIIzvAYMGfBYYnbKuNicfSsk4RksY2XdmJhT+HAC01nix6fHCztU68jooFiMUB01Ky3F0FyOvhG/BZrWkg==", + "license": "MIT", + "dependencies": { + "@dnd-kit/utilities": "^3.2.2", + "tslib": "^2.0.0" + }, + "peerDependencies": { + "@dnd-kit/core": "^6.3.0", + "react": ">=16.8.0" + } + }, + "node_modules/@dnd-kit/utilities": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/@dnd-kit/utilities/-/utilities-3.2.2.tgz", + "integrity": "sha512-+MKAJEOfaBe5SmV6t34p80MMKhjvUz0vRrvVJbPT0WElzaOJ/1xs+D+KDv+tD/NE5ujfrChEcshd4fLn0wpiqg==", + "license": "MIT", + "dependencies": { + "tslib": "^2.0.0" + }, + "peerDependencies": { + "react": ">=16.8.0" + } + }, "node_modules/@emotion/babel-plugin": { "version": "11.13.5", "resolved": "https://registry.npmjs.org/@emotion/babel-plugin/-/babel-plugin-11.13.5.tgz", @@ -2168,6 +2341,19 @@ "stylis": "4.2.0" } }, + "node_modules/@emotion/css": { + "version": "11.13.5", + "resolved": "https://registry.npmjs.org/@emotion/css/-/css-11.13.5.tgz", + "integrity": "sha512-wQdD0Xhkn3Qy2VNcIzbLP9MR8TafI0MJb7BEAXKp+w4+XqErksWR4OXomuDzPsN4InLdGhVe6EYcn2ZIUCpB8w==", + "license": "MIT", + "dependencies": { + "@emotion/babel-plugin": "^11.13.5", + "@emotion/cache": "^11.13.5", + "@emotion/serialize": "^1.3.3", + "@emotion/sheet": "^1.4.0", + "@emotion/utils": "^1.4.2" + } + }, "node_modules/@emotion/hash": { "version": "0.9.2", "resolved": "https://registry.npmjs.org/@emotion/hash/-/hash-0.9.2.tgz", @@ -2899,6 +3085,7 @@ "resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-common-types/-/fontawesome-common-types-7.1.0.tgz", "integrity": "sha512-l/BQM7fYntsCI//du+6sEnHOP6a74UixFyOYUyz2DLMXKx+6DEhfR3F2NYGE45XH1JJuIamacb4IZs9S0ZOWLA==", "license": "MIT", + "peer": true, "engines": { "node": ">=6" } @@ -2980,6 +3167,7 @@ "deprecated": "Use @eslint/config-array instead", "dev": true, "license": "Apache-2.0", + "peer": true, "dependencies": { "@humanwhocodes/object-schema": "^2.0.3", "debug": "^4.3.1", @@ -3009,7 +3197,8 @@ "integrity": "sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA==", "deprecated": "Use @eslint/object-schema instead", "dev": true, - "license": "BSD-3-Clause" + "license": "BSD-3-Clause", + "peer": true }, "node_modules/@humanwhocodes/retry": { "version": "0.4.3", @@ -3306,7 +3495,6 @@ "resolved": "https://registry.npmjs.org/@mui/material/-/material-6.5.0.tgz", "integrity": "sha512-yjvtXoFcrPLGtgKRxFaH6OQPtcLPhkloC0BML6rBG5UeldR0nPULR/2E2BfXdo5JNV7j7lOzrrLX2Qf/iSidow==", "license": "MIT", - "peer": true, "dependencies": { "@babel/runtime": "^7.26.0", "@mui/core-downloads-tracker": "^6.5.0", @@ -3523,90 +3711,792 @@ "dev": true, "license": "MIT", "dependencies": { - "@nodelib/fs.stat": "2.0.5", - "run-parallel": "^1.1.9" + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@paralleldrive/cuid2": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/@paralleldrive/cuid2/-/cuid2-2.3.1.tgz", + "integrity": "sha512-XO7cAxhnTZl0Yggq6jOgjiOHhbgcO4NqFqwSmQpjK3b6TEE6Uj/jfSk6wzYyemh3+I0sHirKSetjQwn5cZktFw==", + "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", + "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=14" + } + }, + "node_modules/@playwright/test": { + "version": "1.57.0", + "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.57.0.tgz", + "integrity": "sha512-6TyEnHgd6SArQO8UO2OMTxshln3QMWBtPGrOCgs3wVEmQmwyuNtB10IZMfmYDE0riwNR1cu4q+pPcxMVtaG3TA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "playwright": "1.57.0" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@polka/url": { + "version": "1.0.0-next.29", + "resolved": "https://registry.npmjs.org/@polka/url/-/url-1.0.0-next.29.tgz", + "integrity": "sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww==", + "dev": true, + "license": "MIT" + }, + "node_modules/@popperjs/core": { + "version": "2.11.8", + "resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.8.tgz", + "integrity": "sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/popperjs" + } + }, + "node_modules/@rc-component/async-validator": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/@rc-component/async-validator/-/async-validator-5.1.0.tgz", + "integrity": "sha512-n4HcR5siNUXRX23nDizbZBQPO0ZM/5oTtmKZ6/eqL0L2bo747cklFdZGRN2f+c9qWGICwDzrhW0H7tE9PptdcA==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.24.4" + }, + "engines": { + "node": ">=14.x" + } + }, + "node_modules/@rc-component/cascader": { + "version": "1.15.0", + "resolved": "https://registry.npmjs.org/@rc-component/cascader/-/cascader-1.15.0.tgz", + "integrity": "sha512-ZzpMtwFCRo3fbXHuDnncARJMZQjdqA2w7aDuPofNQt+aDx39st1hgfIpEwTBLhe2Hqsvs/zOr8RTtgxTkCPySw==", + "license": "MIT", + "dependencies": { + "@rc-component/select": "~1.6.0", + "@rc-component/tree": "~1.3.0", + "@rc-component/util": "^1.4.0", + "clsx": "^2.1.1" + }, + "peerDependencies": { + "react": ">=18.0.0", + "react-dom": ">=18.0.0" + } + }, + "node_modules/@rc-component/checkbox": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@rc-component/checkbox/-/checkbox-2.0.0.tgz", + "integrity": "sha512-3CXGPpAR9gsPKeO2N78HAPOzU30UdemD6HGJoWVJOpa6WleaGB5kzZj3v6bdTZab31YuWgY/RxV3VKPctn0DwQ==", + "license": "MIT", + "dependencies": { + "@rc-component/util": "^1.3.0", + "clsx": "^2.1.1" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/@rc-component/collapse": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@rc-component/collapse/-/collapse-1.2.0.tgz", + "integrity": "sha512-ZRYSKSS39qsFx93p26bde7JUZJshsUBEQRlRXPuJYlAiNX0vyYlF5TsAm8JZN3LcF8XvKikdzPbgAtXSbkLUkw==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.10.1", + "@rc-component/motion": "^1.1.4", + "@rc-component/util": "^1.3.0", + "clsx": "^2.1.1" + }, + "peerDependencies": { + "react": ">=18.0.0", + "react-dom": ">=18.0.0" + } + }, + "node_modules/@rc-component/color-picker": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@rc-component/color-picker/-/color-picker-3.1.1.tgz", + "integrity": "sha512-OHaCHLHszCegdXmIq2ZRIZBN/EtpT6Wm8SG/gpzLATHbVKc/avvuKi+zlOuk05FTWvgaMmpxAko44uRJ3M+2pg==", + "license": "MIT", + "dependencies": { + "@ant-design/fast-color": "^3.0.1", + "@rc-component/util": "^1.3.0", + "clsx": "^2.1.1" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/@rc-component/context": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@rc-component/context/-/context-2.0.1.tgz", + "integrity": "sha512-HyZbYm47s/YqtP6pKXNMjPEMaukyg7P0qVfgMLzr7YiFNMHbK2fKTAGzms9ykfGHSfyf75nBbgWw+hHkp+VImw==", + "license": "MIT", + "dependencies": { + "@rc-component/util": "^1.3.0" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/@rc-component/dialog": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/@rc-component/dialog/-/dialog-1.9.0.tgz", + "integrity": "sha512-zbAAogkg4kkKum79sLE6M+vq1jSAW25zdkafrahgcTP9t9S//SD634Znd1A4c8F2Gc12ZKnehGLsVaaOvZzD2A==", + "license": "MIT", + "dependencies": { + "@rc-component/motion": "^1.1.3", + "@rc-component/portal": "^2.1.0", + "@rc-component/util": "^1.9.0", + "clsx": "^2.1.1" + }, + "peerDependencies": { + "react": ">=18.0.0", + "react-dom": ">=18.0.0" + } + }, + "node_modules/@rc-component/drawer": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/@rc-component/drawer/-/drawer-1.4.2.tgz", + "integrity": "sha512-1ib+fZEp6FBu+YvcIktm+nCQ+Q+qIpwpoaJH6opGr4ofh2QMq+qdr5DLC4oCf5qf3pcWX9lUWPYX652k4ini8Q==", + "license": "MIT", + "dependencies": { + "@rc-component/motion": "^1.1.4", + "@rc-component/portal": "^2.1.3", + "@rc-component/util": "^1.9.0", + "clsx": "^2.1.1" + }, + "peerDependencies": { + "react": ">=18.0.0", + "react-dom": ">=18.0.0" + } + }, + "node_modules/@rc-component/dropdown": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@rc-component/dropdown/-/dropdown-1.0.2.tgz", + "integrity": "sha512-6PY2ecUSYhDPhkNHHb4wfeAya04WhpmUSKzdR60G+kMNVUCX2vjT/AgTS0Lz0I/K6xrPMJ3enQbwVpeN3sHCgg==", + "license": "MIT", + "dependencies": { + "@rc-component/trigger": "^3.0.0", + "@rc-component/util": "^1.2.1", + "clsx": "^2.1.1" + }, + "peerDependencies": { + "react": ">=16.11.0", + "react-dom": ">=16.11.0" + } + }, + "node_modules/@rc-component/form": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/@rc-component/form/-/form-1.8.1.tgz", + "integrity": "sha512-8O7TB55Fi2mWIGvSnwZjk8jFqVNYyKDAswglwGShcbndxqzKz4cHwNtNaLjZlAeRge9wcB0LL8IWsC/Bl18raQ==", + "license": "MIT", + "dependencies": { + "@rc-component/async-validator": "^5.1.0", + "@rc-component/util": "^1.6.2", + "clsx": "^2.1.1" + }, + "engines": { + "node": ">=8.x" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/@rc-component/image": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/@rc-component/image/-/image-1.9.0.tgz", + "integrity": "sha512-khF7w7xkBH5B1bsBcI1FSUZdkyd1aqpl2eYyILCqCzzQH3XdfehGUaZTnptyaJJfs09/R5hv9jXWyazOMFIClQ==", + "license": "MIT", + "dependencies": { + "@rc-component/motion": "^1.0.0", + "@rc-component/portal": "^2.1.2", + "@rc-component/util": "^1.10.1", + "clsx": "^2.1.1" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/@rc-component/input": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@rc-component/input/-/input-1.3.0.tgz", + "integrity": "sha512-IUUNOdAuWuEvDEFFgfmwQl818tiDbvXwLgon4HL1q2hJeYkqrRrYwYhJN0zfPHGTDxs3gvyVC/C02D4hWFoIcA==", + "license": "MIT", + "dependencies": { + "@rc-component/resize-observer": "^1.1.1", + "@rc-component/util": "^1.4.0", + "clsx": "^2.1.1" + }, + "peerDependencies": { + "react": ">=16.0.0", + "react-dom": ">=16.0.0" + } + }, + "node_modules/@rc-component/input-number": { + "version": "1.6.2", + "resolved": "https://registry.npmjs.org/@rc-component/input-number/-/input-number-1.6.2.tgz", + "integrity": "sha512-Gjcq7meZlCOiWN1t1xCC+7/s85humHVokTBI7PJgTfoyw5OWF74y3e6P8PHX104g9+b54jsodFIzyaj6p8LI9w==", + "license": "MIT", + "dependencies": { + "@rc-component/mini-decimal": "^1.0.1", + "@rc-component/util": "^1.4.0", + "clsx": "^2.1.1" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/@rc-component/mentions": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/@rc-component/mentions/-/mentions-1.9.0.tgz", + "integrity": "sha512-WUwfFKDSOF5S9UPsNsXcLYtzjTxBGsftTXWRbZuxX6BYrsySISTnujfJNgaaQ6qVzaCDJ35QUkZKvsYxip1C5g==", + "license": "MIT", + "dependencies": { + "@rc-component/input": "~1.3.0", + "@rc-component/menu": "~1.3.0", + "@rc-component/trigger": "^3.0.0", + "@rc-component/util": "^1.3.0", + "clsx": "^2.1.1" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/@rc-component/menu": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@rc-component/menu/-/menu-1.3.0.tgz", + "integrity": "sha512-u3NfiwpiEgT177qa5Yxm5QsI8i/93EBGpWj8HYZQDnh2pCZ2xtQCe/+w3pSR2NlwKOZDTCKzEhEyD09mGphssA==", + "license": "MIT", + "dependencies": { + "@rc-component/motion": "^1.1.4", + "@rc-component/overflow": "^1.0.0", + "@rc-component/trigger": "^3.0.0", + "@rc-component/util": "^1.3.0", + "clsx": "^2.1.1" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/@rc-component/mini-decimal": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@rc-component/mini-decimal/-/mini-decimal-1.1.3.tgz", + "integrity": "sha512-bk/FJ09fLf+NLODMAFll6CfYrHPBioTedhW6lxDBuuWucJEqFUd4l/D/5JgIi3dina6sYahB8iuPAZTNz2pMxw==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.18.0" + }, + "engines": { + "node": ">=8.x" + } + }, + "node_modules/@rc-component/motion": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/@rc-component/motion/-/motion-1.3.2.tgz", + "integrity": "sha512-itfd+GztzJYAb04Z4RkEub1TbJAfZc2Iuy8p44U44xD1F5+fNYFKI3897ijlbIyfvXkTmMm+KGcjkQQGMHywEQ==", + "license": "MIT", + "dependencies": { + "@rc-component/util": "^1.2.0", + "clsx": "^2.1.1" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/@rc-component/mutate-observer": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@rc-component/mutate-observer/-/mutate-observer-2.0.1.tgz", + "integrity": "sha512-AyarjoLU5YlxuValRi+w8JRH2Z84TBbFO2RoGWz9d8bSu0FqT8DtugH3xC3BV7mUwlmROFauyWuXFuq4IFbH+w==", + "license": "MIT", + "dependencies": { + "@rc-component/util": "^1.2.0" + }, + "engines": { + "node": ">=8.x" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/@rc-component/notification": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/@rc-component/notification/-/notification-2.0.7.tgz", + "integrity": "sha512-nqZzpf6BPdaj+3ILx7si79LLmqPKyUmQoXa+/9gg0SkH0v1DbD66oJgRMSBEVnd/zUT3D4gwxWIHUKebYf2ZXQ==", + "license": "MIT", + "dependencies": { + "@rc-component/motion": "^1.1.4", + "@rc-component/util": "^1.11.0", + "clsx": "^2.1.1" + }, + "engines": { + "node": ">=8.x" + }, + "peerDependencies": { + "react": ">=18.0.0", + "react-dom": ">=18.0.0" + } + }, + "node_modules/@rc-component/overflow": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@rc-component/overflow/-/overflow-1.0.1.tgz", + "integrity": "sha512-syfmgAABaHCnCDzPwHZ/2tuvIcpOO3jefYZMmfkN+pmo8HKTzsfhS57vxo4ksPdN0By+uWVJhJWNFozNBxi2eA==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.11.1", + "@rc-component/resize-observer": "^1.0.1", + "@rc-component/util": "^1.4.0", + "clsx": "^2.1.1" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/@rc-component/pagination": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@rc-component/pagination/-/pagination-1.2.0.tgz", + "integrity": "sha512-YcpUFE8dMLfSo6OARJlK6DbHHvrxz7pMGPGmC/caZSJJz6HRKHC1RPP001PRHCvG9Z/veD039uOQmazVuLJzlw==", + "license": "MIT", + "dependencies": { + "@rc-component/util": "^1.3.0", + "clsx": "^2.1.1" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/@rc-component/picker": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/@rc-component/picker/-/picker-1.10.0.tgz", + "integrity": "sha512-vVOXP2RVWozwpERGUFAehVH1Jz6o/uRrAb9qSZm1LC+iJs8rvEwFo1bzz2jlOYV+uWwu0dIuG86tnDui14Ea0w==", + "license": "MIT", + "dependencies": { + "@rc-component/overflow": "^1.0.0", + "@rc-component/resize-observer": "^1.0.0", + "@rc-component/trigger": "^3.6.15", + "@rc-component/util": "^1.3.0", + "clsx": "^2.1.1" + }, + "engines": { + "node": ">=12.x" + }, + "peerDependencies": { + "date-fns": ">= 2.x", + "dayjs": ">= 1.x", + "luxon": ">= 3.x", + "moment": ">= 2.x", + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + }, + "peerDependenciesMeta": { + "date-fns": { + "optional": true + }, + "dayjs": { + "optional": true + }, + "luxon": { + "optional": true + }, + "moment": { + "optional": true + } + } + }, + "node_modules/@rc-component/portal": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@rc-component/portal/-/portal-2.2.0.tgz", + "integrity": "sha512-oc6FlA+uXCMiwArHsJyHcIkX4q6uKyndrPol2eWX8YPkAnztHOPsFIRtmWG4BMlGE5h7YIRE3NiaJ5VS8Lb1QQ==", + "license": "MIT", + "dependencies": { + "@rc-component/util": "^1.2.1", + "clsx": "^2.1.1" + }, + "engines": { + "node": ">=12.x" + }, + "peerDependencies": { + "react": ">=18.0.0", + "react-dom": ">=18.0.0" + } + }, + "node_modules/@rc-component/progress": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@rc-component/progress/-/progress-1.0.2.tgz", + "integrity": "sha512-WZUnH9eGxH1+xodZKqdrHke59uyGZSWgj5HBM5Kwk5BrTMuAORO7VJ2IP5Qbm9aH3n9x3IcesqHHR0NWPBC7fQ==", + "license": "MIT", + "dependencies": { + "@rc-component/util": "^1.2.1", + "clsx": "^2.1.1" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/@rc-component/qrcode": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@rc-component/qrcode/-/qrcode-1.1.1.tgz", + "integrity": "sha512-LfLGNymzKdUPjXUbRP+xOhIWY4jQ+YMj5MmWAcgcAq1Ij8XP7tRmAXqyuv96XvLUBE/5cA8hLFl9eO1JQMujrA==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.24.7" + }, + "engines": { + "node": ">=8.x" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/@rc-component/rate": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@rc-component/rate/-/rate-1.0.1.tgz", + "integrity": "sha512-bkXxeBqDpl5IOC7yL7GcSYjQx9G8H+6kLYQnNZWeBYq2OYIv1MONd6mqKTjnnJYpV0cQIU2z3atdW0j1kttpTw==", + "license": "MIT", + "dependencies": { + "@rc-component/util": "^1.3.0", + "clsx": "^2.1.1" + }, + "engines": { + "node": ">=8.x" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/@rc-component/resize-observer": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@rc-component/resize-observer/-/resize-observer-1.1.2.tgz", + "integrity": "sha512-t/Bb0W8uvL4PYKAB3YcChC+DlHh0Wt5kM7q/J+0qpVEUMLe7Hk5zuvc9km0hMnTFPSx5Z7Wu/fzCLN6erVLE8Q==", + "license": "MIT", + "dependencies": { + "@rc-component/util": "^1.2.0" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/@rc-component/segmented": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@rc-component/segmented/-/segmented-1.3.0.tgz", + "integrity": "sha512-5J/bJ01mbDnoA6P/FW8SxUvKn+OgUSTZJPzCNnTBntG50tzoP7DydGhqxp7ggZXZls7me3mc2EQDXakU3iTVFg==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.11.1", + "@rc-component/motion": "^1.1.4", + "@rc-component/util": "^1.3.0", + "clsx": "^2.1.1" + }, + "peerDependencies": { + "react": ">=16.0.0", + "react-dom": ">=16.0.0" + } + }, + "node_modules/@rc-component/select": { + "version": "1.6.15", + "resolved": "https://registry.npmjs.org/@rc-component/select/-/select-1.6.15.tgz", + "integrity": "sha512-SyVCWnqxCQZZcQvQJ/CxSjx2bGma6ds/HtnpkIfZVnt6RoEgbqUmHgD6vrzNarNXwbLXerwVzWwq8F3d1sst7g==", + "license": "MIT", + "dependencies": { + "@rc-component/overflow": "^1.0.0", + "@rc-component/trigger": "^3.0.0", + "@rc-component/util": "^1.3.0", + "@rc-component/virtual-list": "^1.0.1", + "clsx": "^2.1.1" + }, + "engines": { + "node": ">=8.x" + }, + "peerDependencies": { + "react": "*", + "react-dom": "*" + } + }, + "node_modules/@rc-component/slider": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@rc-component/slider/-/slider-1.0.1.tgz", + "integrity": "sha512-uDhEPU1z3WDfCJhaL9jfd2ha/Eqpdfxsn0Zb0Xcq1NGQAman0TWaR37OWp2vVXEOdV2y0njSILTMpTfPV1454g==", + "license": "MIT", + "dependencies": { + "@rc-component/util": "^1.3.0", + "clsx": "^2.1.1" + }, + "engines": { + "node": ">=8.x" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/@rc-component/steps": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@rc-component/steps/-/steps-1.2.2.tgz", + "integrity": "sha512-/yVIZ00gDYYPHSY0JP+M+s3ZvuXLu2f9rEjQqiUDs7EcYsUYrpJ/1bLj9aI9R7MBR3fu/NGh6RM9u2qGfqp+Nw==", + "license": "MIT", + "dependencies": { + "@rc-component/util": "^1.2.1", + "clsx": "^2.1.1" + }, + "engines": { + "node": ">=8.x" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/@rc-component/switch": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rc-component/switch/-/switch-1.0.3.tgz", + "integrity": "sha512-Jgi+EbOBquje/XNdofr7xbJQZPYJP+BlPfR0h+WN4zFkdtB2EWqEfvkXJWeipflwjWip0/17rNbxEAqs8hVHfw==", + "license": "MIT", + "dependencies": { + "@rc-component/util": "^1.3.0", + "clsx": "^2.1.1" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/@rc-component/table": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/@rc-component/table/-/table-1.10.0.tgz", + "integrity": "sha512-SjtpcCf+rL7dDc62GKT3rXTdERjVuJvRiqjpU7g0Jc/ewCifXynHc7Nm3Em1XsD+WhGrgQtxNDScI/0+Lpfr0w==", + "license": "MIT", + "dependencies": { + "@rc-component/context": "^2.0.1", + "@rc-component/resize-observer": "^1.0.0", + "@rc-component/util": "^1.1.0", + "@rc-component/virtual-list": "^1.0.1", + "clsx": "^2.1.1" + }, + "engines": { + "node": ">=8.x" + }, + "peerDependencies": { + "react": ">=18.0.0", + "react-dom": ">=18.0.0" + } + }, + "node_modules/@rc-component/tabs": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/@rc-component/tabs/-/tabs-1.9.0.tgz", + "integrity": "sha512-tn1slmbbaTyt8mgwyWJcT8jo/qNiYUs6u1H7OgGQt9faYO06BJIkU5cTmMqORzIrNmSEeeUY6pD5i+JlqSHYhg==", + "license": "MIT", + "dependencies": { + "@rc-component/dropdown": "~1.0.0", + "@rc-component/menu": "~1.3.0", + "@rc-component/motion": "^1.1.3", + "@rc-component/resize-observer": "^1.0.0", + "@rc-component/util": "^1.3.0", + "clsx": "^2.1.1" + }, + "engines": { + "node": ">=8.x" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/@rc-component/tooltip": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@rc-component/tooltip/-/tooltip-1.4.0.tgz", + "integrity": "sha512-8Rx5DCctIlLI4raR0I0xHjVTf1aF48+gKCNeAAo5bmF5VoR5YED+A/XEqzXv9KKqrJDRcd3Wndpxh2hyzrTtSg==", + "license": "MIT", + "dependencies": { + "@rc-component/trigger": "^3.7.1", + "@rc-component/util": "^1.3.0", + "clsx": "^2.1.1" }, - "engines": { - "node": ">= 8" + "peerDependencies": { + "react": ">=18.0.0", + "react-dom": ">=18.0.0" } }, - "node_modules/@nodelib/fs.stat": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", - "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", - "dev": true, + "node_modules/@rc-component/tour": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/@rc-component/tour/-/tour-2.4.0.tgz", + "integrity": "sha512-aui4r4TqmTzwaBgcQxHYep8kM8PTjZFufjokObpy35KfFeZ0k9ArquWFZqegQlH24P14t+F0qO0mGTgzlav1yg==", "license": "MIT", + "dependencies": { + "@rc-component/portal": "^2.2.0", + "@rc-component/trigger": "^3.0.0", + "@rc-component/util": "^1.7.0", + "clsx": "^2.1.1" + }, "engines": { - "node": ">= 8" + "node": ">=8.x" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" } }, - "node_modules/@nodelib/fs.walk": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", - "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", - "dev": true, + "node_modules/@rc-component/tree": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/@rc-component/tree/-/tree-1.3.1.tgz", + "integrity": "sha512-zlL0PW0bTFlveTtLcA01VD/yMWKK73EywItFMgIZUY5sb6tMOAw7zV6qGzqldufqrV93ZWQB4H3NBNoTMCueJA==", "license": "MIT", "dependencies": { - "@nodelib/fs.scandir": "2.1.5", - "fastq": "^1.6.0" + "@rc-component/motion": "^1.0.0", + "@rc-component/util": "^1.8.1", + "@rc-component/virtual-list": "^1.0.1", + "clsx": "^2.1.1" }, "engines": { - "node": ">= 8" + "node": ">=10.x" + }, + "peerDependencies": { + "react": "*", + "react-dom": "*" } }, - "node_modules/@paralleldrive/cuid2": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/@paralleldrive/cuid2/-/cuid2-2.3.1.tgz", - "integrity": "sha512-XO7cAxhnTZl0Yggq6jOgjiOHhbgcO4NqFqwSmQpjK3b6TEE6Uj/jfSk6wzYyemh3+I0sHirKSetjQwn5cZktFw==", - "dev": true, + "node_modules/@rc-component/tree-select": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/@rc-component/tree-select/-/tree-select-1.9.0.tgz", + "integrity": "sha512-GXcFe15a+trUl1/J3OHWQhsVWFpwFpGFK2cqYWZ1sK22Zs3KZTvMwDpzr75PIo1s6QVioVxpE/pRwRopkeDQ6w==", "license": "MIT", "dependencies": { - "@noble/hashes": "^1.1.5" + "@rc-component/select": "~1.6.0", + "@rc-component/tree": "~1.3.0", + "@rc-component/util": "^1.4.0", + "clsx": "^2.1.1" + }, + "peerDependencies": { + "react": "*", + "react-dom": "*" } }, - "node_modules/@pkgjs/parseargs": { - "version": "0.11.0", - "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", - "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", - "dev": true, + "node_modules/@rc-component/trigger": { + "version": "3.9.0", + "resolved": "https://registry.npmjs.org/@rc-component/trigger/-/trigger-3.9.0.tgz", + "integrity": "sha512-X8btpwfrT27AgrZVOz4swclhEHTZcqaHeQMXXBgveagOiakTa36uObXbdwerXffgV8G9dH1fAAE0DHtVQs8EHg==", "license": "MIT", - "optional": true, + "dependencies": { + "@rc-component/motion": "^1.1.4", + "@rc-component/portal": "^2.2.0", + "@rc-component/resize-observer": "^1.1.1", + "@rc-component/util": "^1.2.1", + "clsx": "^2.1.1" + }, "engines": { - "node": ">=14" + "node": ">=8.x" + }, + "peerDependencies": { + "react": ">=18.0.0", + "react-dom": ">=18.0.0" } }, - "node_modules/@playwright/test": { - "version": "1.57.0", - "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.57.0.tgz", - "integrity": "sha512-6TyEnHgd6SArQO8UO2OMTxshln3QMWBtPGrOCgs3wVEmQmwyuNtB10IZMfmYDE0riwNR1cu4q+pPcxMVtaG3TA==", - "dev": true, - "license": "Apache-2.0", + "node_modules/@rc-component/upload": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@rc-component/upload/-/upload-1.1.0.tgz", + "integrity": "sha512-LIBV90mAnUE6VK5N4QvForoxZc4XqEYZimcp7fk+lkE4XwHHyJWxpIXQQwMU8hJM+YwBbsoZkGksL1sISWHQxw==", + "license": "MIT", "dependencies": { - "playwright": "1.57.0" + "@rc-component/util": "^1.3.0", + "clsx": "^2.1.1" }, - "bin": { - "playwright": "cli.js" + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/@rc-component/util": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@rc-component/util/-/util-1.11.1.tgz", + "integrity": "sha512-awVlI3ub2vqfqkYxOBc/uQ0efm3jw0wcrhtO/YWLyZfxiKXczKwNbVuhlnyxytDt7H9pbbVQiqr+O6MLATtRYg==", + "license": "MIT", + "dependencies": { + "is-mobile": "^5.0.0", + "react-is": "^18.2.0" }, - "engines": { - "node": ">=18" + "peerDependencies": { + "react": ">=18.0.0", + "react-dom": ">=18.0.0" } }, - "node_modules/@polka/url": { - "version": "1.0.0-next.29", - "resolved": "https://registry.npmjs.org/@polka/url/-/url-1.0.0-next.29.tgz", - "integrity": "sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww==", - "dev": true, + "node_modules/@rc-component/util/node_modules/react-is": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", "license": "MIT" }, - "node_modules/@popperjs/core": { - "version": "2.11.8", - "resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.8.tgz", - "integrity": "sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==", + "node_modules/@rc-component/virtual-list": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@rc-component/virtual-list/-/virtual-list-1.1.0.tgz", + "integrity": "sha512-fSzVg8xFDCK+xKJJ9lljBpaGDmur4REZwXHJjsNFRi4UbfOXuH/FSbGC2oliCE5krFqM3lSZqB4Ly4vc3xQHOQ==", "license": "MIT", - "peer": true, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/popperjs" + "dependencies": { + "@babel/runtime": "^7.20.0", + "@rc-component/resize-observer": "^1.0.1", + "@rc-component/util": "^1.4.0", + "clsx": "^2.1.1" + }, + "engines": { + "node": ">=8.x" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" } }, "node_modules/@react-aria/ssr": { @@ -4090,7 +4980,6 @@ "integrity": "sha512-o4PXJQidqJl82ckFaXUeoAW+XysPLauYI43Abki5hABd853iMhitooc6znOnczgbTYmEP6U6/y1ZyKAIsvMKGg==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@babel/code-frame": "^7.10.4", "@babel/runtime": "^7.12.5", @@ -4300,7 +5189,6 @@ "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.7.tgz", "integrity": "sha512-MWtvHrGZLFttgeEj28VXHxpmwYbor/ATPYbBfSFZEIRK0ecCFLl2Qo55z52Hss+UV9CRN7trSeq1zbgx7YDWWg==", "license": "MIT", - "peer": true, "dependencies": { "csstype": "^3.2.2" } @@ -4311,7 +5199,6 @@ "integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==", "dev": true, "license": "MIT", - "peer": true, "peerDependencies": { "@types/react": "^19.2.0" } @@ -4339,7 +5226,8 @@ "resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz", "integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==", "license": "MIT", - "optional": true + "optional": true, + "peer": true }, "node_modules/@types/warning": { "version": "3.0.3", @@ -4506,7 +5394,8 @@ "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.3.0.tgz", "integrity": "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==", "dev": true, - "license": "ISC" + "license": "ISC", + "peer": true }, "node_modules/@vitejs/plugin-react": { "version": "4.7.0", @@ -4535,7 +5424,6 @@ "integrity": "sha512-tJxiPrWmzH8a+w9nLKlQMzAKX/7VjFs50MWgcAj7p9XQ7AQ9/35fByFYptgPELyLw+0aixTnC4pUWV+APcZ/kw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@testing-library/dom": "^10.4.0", "@testing-library/user-event": "^14.6.1", @@ -4722,7 +5610,6 @@ "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "dev": true, "license": "MIT", - "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -4808,6 +5695,69 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, + "node_modules/antd": { + "version": "6.4.3", + "resolved": "https://registry.npmjs.org/antd/-/antd-6.4.3.tgz", + "integrity": "sha512-6H2avkxCGfxcF67r3J2mwm9Ck50el1pks/73vfM1wDsPL/tPtj5vHuauMgJFnrqmq7CH3g8aoZ0VBQbt+jpAsw==", + "license": "MIT", + "dependencies": { + "@ant-design/colors": "^8.0.1", + "@ant-design/cssinjs": "^2.1.2", + "@ant-design/cssinjs-utils": "^2.1.2", + "@ant-design/fast-color": "^3.0.1", + "@ant-design/icons": "^6.2.3", + "@ant-design/react-slick": "~2.0.0", + "@babel/runtime": "^7.29.2", + "@rc-component/cascader": "~1.15.0", + "@rc-component/checkbox": "~2.0.0", + "@rc-component/collapse": "~1.2.0", + "@rc-component/color-picker": "~3.1.1", + "@rc-component/dialog": "~1.9.0", + "@rc-component/drawer": "~1.4.2", + "@rc-component/dropdown": "~1.0.2", + "@rc-component/form": "~1.8.1", + "@rc-component/image": "~1.9.0", + "@rc-component/input": "~1.3.0", + "@rc-component/input-number": "~1.6.2", + "@rc-component/mentions": "~1.9.0", + "@rc-component/menu": "~1.3.0", + "@rc-component/motion": "^1.3.2", + "@rc-component/mutate-observer": "^2.0.1", + "@rc-component/notification": "~2.0.7", + "@rc-component/pagination": "~1.2.0", + "@rc-component/picker": "~1.10.0", + "@rc-component/progress": "~1.0.2", + "@rc-component/qrcode": "~1.1.1", + "@rc-component/rate": "~1.0.1", + "@rc-component/resize-observer": "^1.1.2", + "@rc-component/segmented": "~1.3.0", + "@rc-component/select": "~1.6.15", + "@rc-component/slider": "~1.0.1", + "@rc-component/steps": "~1.2.2", + "@rc-component/switch": "~1.0.3", + "@rc-component/table": "~1.10.0", + "@rc-component/tabs": "~1.9.0", + "@rc-component/tooltip": "~1.4.0", + "@rc-component/tour": "~2.4.0", + "@rc-component/tree": "~1.3.1", + "@rc-component/tree-select": "~1.9.0", + "@rc-component/trigger": "^3.9.0", + "@rc-component/upload": "~1.1.0", + "@rc-component/util": "^1.11.0", + "clsx": "^2.1.1", + "dayjs": "^1.11.11", + "scroll-into-view-if-needed": "^3.1.0", + "throttle-debounce": "^5.0.2" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/ant-design" + }, + "peerDependencies": { + "react": ">=18.0.0", + "react-dom": ">=18.0.0" + } + }, "node_modules/anymatch": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", @@ -5397,7 +6347,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "baseline-browser-mapping": "^2.9.0", "caniuse-lite": "^1.0.30001759", @@ -5580,7 +6529,6 @@ "resolved": "https://registry.npmjs.org/chart.js/-/chart.js-4.5.1.tgz", "integrity": "sha512-GIjfiT9dbmHRiYi6Nl2yFCq7kkwdkp1W/lp2J99rX0yo9tgJGn3lKQATztIjb5tVtevcBtIdICNWqlq5+E8/Pw==", "license": "MIT", - "peer": true, "dependencies": { "@kurkle/color": "^0.3.0" }, @@ -5829,6 +6777,12 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/compute-scroll-into-view": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/compute-scroll-into-view/-/compute-scroll-into-view-3.1.1.tgz", + "integrity": "sha512-VRhuHOLoKYOy4UbilLbUzbYg93XLjv2PncJC50EuTWPA3gaja1UjBsUP/D/9/juV3vQFr6XBEzn9KCAHdUvOHw==", + "license": "MIT" + }, "node_modules/concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", @@ -6074,6 +7028,12 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/dayjs": { + "version": "1.11.20", + "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.20.tgz", + "integrity": "sha512-YbwwqR/uYpeoP4pu043q+LTDLFBLApUP6VxRihdfNTqu4ubqMlGDLd6ErXhEgsyvY0K6nCs7nggYumAN+9uEuQ==", + "license": "MIT" + }, "node_modules/debug": { "version": "4.4.3", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", @@ -6270,6 +7230,7 @@ "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.2.7.tgz", "integrity": "sha512-WhL/YuveyGXJaerVlMYGWhvQswa7myDG17P7Vu65EWC05o8vfeNbvNf4d/BOvH99+ZW+LlQsc1GDKMa1vNK6dw==", "license": "(MPL-2.0 OR Apache-2.0)", + "peer": true, "optionalDependencies": { "@types/trusted-types": "^2.0.7" } @@ -6821,6 +7782,7 @@ "integrity": "sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "ajv": "^6.12.4", "debug": "^4.3.2", @@ -6845,6 +7807,7 @@ "integrity": "sha512-d9zaMRSTIKDLhctzH12MtXvJKSSUhaHcjV+2Z+GK+EEY7XKpP5yR4x+N3TAcHTcu963nIr+TMcCb4DBCYX1z6Q==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": "^12.22.0 || ^14.17.0 || >=16.0.0" } @@ -6855,6 +7818,7 @@ "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==", "dev": true, "license": "Apache-2.0", + "peer": true, "dependencies": { "esutils": "^2.0.2" }, @@ -6868,6 +7832,7 @@ "integrity": "sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==", "dev": true, "license": "BSD-2-Clause", + "peer": true, "dependencies": { "esrecurse": "^4.3.0", "estraverse": "^5.2.0" @@ -6885,6 +7850,7 @@ "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", "dev": true, "license": "Apache-2.0", + "peer": true, "engines": { "node": "^12.22.0 || ^14.17.0 || >=16.0.0" }, @@ -6898,6 +7864,7 @@ "integrity": "sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==", "dev": true, "license": "BSD-2-Clause", + "peer": true, "dependencies": { "acorn": "^8.9.0", "acorn-jsx": "^5.3.2", @@ -6916,6 +7883,7 @@ "integrity": "sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "flat-cache": "^3.0.4" }, @@ -6929,6 +7897,7 @@ "integrity": "sha512-CYcENa+FtcUKLmhhqyctpclsq7QF38pKjZHsGNiSQF5r4FtoKDWabFDl3hzaEQMvT1LHEysw5twgLvpYYb4vbw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "flatted": "^3.2.9", "keyv": "^4.5.3", @@ -6944,6 +7913,7 @@ "integrity": "sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "type-fest": "^0.20.2" }, @@ -7489,7 +8459,6 @@ "version": "2.3.2", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", - "dev": true, "hasInstallScript": true, "license": "MIT", "optional": true, @@ -7752,7 +8721,8 @@ "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", "dev": true, - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/has-bigints": { "version": "1.1.0", @@ -7892,6 +8862,15 @@ "node": ">=18" } }, + "node_modules/html-parse-stringify": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/html-parse-stringify/-/html-parse-stringify-3.0.1.tgz", + "integrity": "sha512-KknJ50kTInJ7qIScF3jeaFRpMpE8/lfiTdzf/twXyPBLAGrLRTmkz3AdTnKeh40X8k9L2fdYwEp/42WGXIRGcg==", + "license": "MIT", + "dependencies": { + "void-elements": "3.1.0" + } + }, "node_modules/http-errors": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", @@ -7966,6 +8945,52 @@ "url": "https://github.com/sponsors/typicode" } }, + "node_modules/i18next": { + "version": "26.2.0", + "resolved": "https://registry.npmjs.org/i18next/-/i18next-26.2.0.tgz", + "integrity": "sha512-zwBHldHdTmwN7r6UNc7lC6GWNN+YYg3DrRSeHR5PRRBf5QnJZcYHrQc0uaU26qZeYxR7iFZD+Y315dPnKP47wA==", + "funding": [ + { + "type": "individual", + "url": "https://www.locize.com/i18next" + }, + { + "type": "individual", + "url": "https://www.i18next.com/how-to/faq#i18next-is-awesome.-how-can-i-support-the-project" + }, + { + "type": "individual", + "url": "https://www.locize.com" + } + ], + "license": "MIT", + "peerDependencies": { + "typescript": "^5 || ^6" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/i18next-browser-languagedetector": { + "version": "8.2.1", + "resolved": "https://registry.npmjs.org/i18next-browser-languagedetector/-/i18next-browser-languagedetector-8.2.1.tgz", + "integrity": "sha512-bZg8+4bdmaOiApD7N7BPT9W8MLZG+nPTOFlLiJiT8uzKXFjhxw4v2ierCXOwB5sFDMtuA5G4kgYZ0AznZxQ/cw==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.23.2" + } + }, + "node_modules/i18next-http-backend": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/i18next-http-backend/-/i18next-http-backend-4.0.0.tgz", + "integrity": "sha512-EgSjO3Q1G6f2Q5oy7u9mmxuesE0oSfzAD97NFBjC8EmkK4guBSYLljM0Fng3DarMWIIkU70jfo4+mUzmyVISTA==", + "license": "MIT", + "engines": { + "node": ">=18" + } + }, "node_modules/iconv-lite": { "version": "0.6.3", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", @@ -8329,6 +9354,12 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-mobile": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/is-mobile/-/is-mobile-5.0.0.tgz", + "integrity": "sha512-Tz/yndySvLAEXh+Uk8liFCxOwVH6YutuR74utvOcu7I9Di+DwM0mtdPVZNaVvvBUM2OXxne/NhOs1zAO7riusQ==", + "license": "MIT" + }, "node_modules/is-negative-zero": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.3.tgz", @@ -8719,7 +9750,6 @@ "resolved": "https://registry.npmjs.org/jsep/-/jsep-1.4.0.tgz", "integrity": "sha512-B7qPcEVE3NVkmSJbaYxvv4cHkVW7DQsZz13pUMrfS8z8Q/BuShN+gcTXrUlPiGqM2/t/EEaI030bpxMqY8gMlw==", "license": "MIT", - "peer": true, "engines": { "node": ">= 10.16.0" } @@ -8763,6 +9793,15 @@ "dev": true, "license": "MIT" }, + "node_modules/json2mq": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/json2mq/-/json2mq-0.2.0.tgz", + "integrity": "sha512-SzoRg7ux5DWTII9J2qkrZrqV1gt+rTaoufMxEzXbS26Uid0NwaJd123HcoB80TgubEppxxIGdNxCx50fEoEWQA==", + "license": "MIT", + "dependencies": { + "string-convert": "^0.2.0" + } + }, "node_modules/json5": { "version": "2.2.3", "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", @@ -9171,6 +10210,7 @@ "resolved": "https://registry.npmjs.org/marked/-/marked-14.0.0.tgz", "integrity": "sha512-uIj4+faQ+MgHgwUW1l2PsPglZLOLOT1uErt06dAPtx2kjteLAkbsd/0FiYg/MGS+i7ZKLb7w2WClxHkzOOuryQ==", "license": "MIT", + "peer": true, "bin": { "marked": "bin/marked.js" }, @@ -9196,6 +10236,12 @@ "node": ">= 0.6" } }, + "node_modules/memoize-one": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/memoize-one/-/memoize-one-6.0.0.tgz", + "integrity": "sha512-rkpe71W0N0c0Xz6QD0eJETuWAJGnJ9afsl1srmwPrI+yBCkge5EycXXbYRyvL29zZVUWQCY7InPRCv3GDXuZNw==", + "license": "MIT" + }, "node_modules/merge-descriptors": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz", @@ -10565,7 +11611,6 @@ "resolved": "https://registry.npmjs.org/react/-/react-19.2.3.tgz", "integrity": "sha512-Ku/hhYbVjOQnXDZFv2+RibmLFGwFdeeKHFcOTlrt7xplBnya5OGn/hIRDsqDiSUcfORsDC7MPxwork8jBwsIWA==", "license": "MIT", - "peer": true, "engines": { "node": ">=0.10.0" } @@ -10611,12 +11656,41 @@ "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, + "node_modules/react-diff-viewer-continued": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/react-diff-viewer-continued/-/react-diff-viewer-continued-4.2.2.tgz", + "integrity": "sha512-8wT0/smXGox70oXJ7SZkTYF1p1Uh7jNGXhN7ykqrqPven0sZ+wM2c62SG3ZqORx5aW75te+WhmUWo0E4NyoSvg==", + "license": "MIT", + "dependencies": { + "@emotion/css": "^11.13.5", + "@emotion/react": "^11.14.0", + "classnames": "^2.5.1", + "diff": "^9.0.0", + "js-yaml": "^4.1.1", + "memoize-one": "^6.0.0" + }, + "engines": { + "node": ">= 16" + }, + "peerDependencies": { + "react": "^15.3.0 || ^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", + "react-dom": "^15.3.0 || ^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/react-diff-viewer-continued/node_modules/diff": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/diff/-/diff-9.0.0.tgz", + "integrity": "sha512-svtcdpS8CgJyqAjEQIXdb3OjhFVVYjzGAPO8WGCmRbrml64SPw/jJD4GoE98aR7r25A0XcgrK3F02yw9R/vhQw==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.3.1" + } + }, "node_modules/react-dom": { "version": "19.2.3", "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.3.tgz", "integrity": "sha512-yELu4WmLPw5Mr/lmeEpox5rw3RETacE++JgHqQzd2dg+YbJuat3jH4ingc+WPZhxaoFzdv9y33G+F7Nl5O0GBg==", "license": "MIT", - "peer": true, "dependencies": { "scheduler": "^0.27.0" }, @@ -10624,6 +11698,33 @@ "react": "^19.2.3" } }, + "node_modules/react-i18next": { + "version": "17.0.8", + "resolved": "https://registry.npmjs.org/react-i18next/-/react-i18next-17.0.8.tgz", + "integrity": "sha512-0ooKbGLU8JXhe1zwpQUWIeXSgLPOfwJmgheWRIUpcoA0CpyabpGhayjdG+/eA5esC1AQ8h2jWpXjJfzQzeDOCw==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.29.2", + "html-parse-stringify": "^3.0.1", + "use-sync-external-store": "^1.6.0" + }, + "peerDependencies": { + "i18next": ">= 26.2.0", + "react": ">= 16.8.0", + "typescript": "^5 || ^6" + }, + "peerDependenciesMeta": { + "react-dom": { + "optional": true + }, + "react-native": { + "optional": true + }, + "typescript": { + "optional": true + } + } + }, "node_modules/react-is": { "version": "19.2.3", "resolved": "https://registry.npmjs.org/react-is/-/react-is-19.2.3.tgz", @@ -10700,6 +11801,16 @@ "react-dom": ">=16.6.0" } }, + "node_modules/react-virtuoso": { + "version": "4.18.7", + "resolved": "https://registry.npmjs.org/react-virtuoso/-/react-virtuoso-4.18.7.tgz", + "integrity": "sha512-xNF5zDGEEIMB7cKwcen/pLig0YDf6OnfFrVgKFa7sHPf9fRem0CaLshyObbBcP88jzn0enavL39EgplgdyT21g==", + "license": "MIT", + "peerDependencies": { + "react": ">=16 || >=17 || >= 18 || >= 19", + "react-dom": ">=16 || >=17 || >= 18 || >=19" + } + }, "node_modules/readable-stream": { "version": "3.6.2", "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", @@ -10946,6 +12057,7 @@ "deprecated": "Rimraf versions prior to v4 are no longer supported", "dev": true, "license": "ISC", + "peer": true, "dependencies": { "glob": "^7.1.3" }, @@ -11142,6 +12254,15 @@ "integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==", "license": "MIT" }, + "node_modules/scroll-into-view-if-needed": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/scroll-into-view-if-needed/-/scroll-into-view-if-needed-3.1.0.tgz", + "integrity": "sha512-49oNpRjWRvnU8NyGVmUaYG4jtTkNonFZI86MmGRDqBphEK2EXT9gdEUoQPZhuBM8yWHxCWbobltqYO5M4XrUvQ==", + "license": "MIT", + "dependencies": { + "compute-scroll-into-view": "^3.0.2" + } + }, "node_modules/semver": { "version": "6.3.1", "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", @@ -11708,6 +12829,12 @@ "node": ">=0.6.19" } }, + "node_modules/string-convert": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/string-convert/-/string-convert-0.2.1.tgz", + "integrity": "sha512-u/1tdPl4yQnPBjnVrmdLo9gtuLvELKsAoRapekWggdiQNvvvum+jYF329d84NAa660KQw7pB2n36KrIKVoXa3A==", + "license": "MIT" + }, "node_modules/string-width": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", @@ -12181,7 +13308,17 @@ "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==", "dev": true, - "license": "MIT" + "license": "MIT", + "peer": true + }, + "node_modules/throttle-debounce": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/throttle-debounce/-/throttle-debounce-5.0.2.tgz", + "integrity": "sha512-B71/4oyj61iNH0KeCamLuE2rmKuTO5byTOSVwECM5FA7TiAiAW+UqTKZ9ERueC4qvgSttUhdmq1mXC3kJqGX7A==", + "license": "MIT", + "engines": { + "node": ">=12.22" + } }, "node_modules/tinybench": { "version": "2.9.0", @@ -12238,7 +13375,6 @@ "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=12" }, @@ -12422,6 +13558,7 @@ "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", "dev": true, "license": "(MIT OR CC0-1.0)", + "peer": true, "engines": { "node": ">=10" }, @@ -12524,7 +13661,7 @@ "version": "5.9.3", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", - "dev": true, + "devOptional": true, "license": "Apache-2.0", "peer": true, "bin": { @@ -12675,6 +13812,15 @@ "punycode": "^2.1.0" } }, + "node_modules/use-sync-external-store": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.6.0.tgz", + "integrity": "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==", + "license": "MIT", + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, "node_modules/util-deprecate": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", @@ -12690,6 +13836,19 @@ "node": ">= 0.4.0" } }, + "node_modules/uuid": { + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-14.0.0.tgz", + "integrity": "sha512-Qo+uWgilfSmAhXCMav1uYFynlQO7fMFiMVZsQqZRMIXp0O7rR7qjkj+cPvBHLgBqi960QCoo/PH2/6ZtVqKvrg==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "license": "MIT", + "bin": { + "uuid": "dist-node/bin/uuid" + } + }, "node_modules/vary": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", @@ -12705,7 +13864,6 @@ "integrity": "sha512-+Oxm7q9hDoLMyJOYfUYBuHQo+dkAloi33apOPP56pzj+vsdJDzr+j1NISE5pyaAuKL4A3UD34qd0lx5+kfKp2g==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.4.4", @@ -12837,7 +13995,6 @@ "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=12" }, @@ -12851,7 +14008,6 @@ "integrity": "sha512-LUCP5ev3GURDysTWiP47wRRUpLKMOfPh+yKTx3kVIEiu5KOMeqzpnYNsKyOoVrULivR8tLcks4+lga33Whn90A==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@types/chai": "^5.2.2", "@vitest/expect": "3.2.4", @@ -12961,6 +14117,15 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, + "node_modules/void-elements": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/void-elements/-/void-elements-3.1.0.tgz", + "integrity": "sha512-Dhxzh5HZuiHQhbvTW9AMetFfBHDMYpo23Uo9btPXgdYP+3T5S+p+jgNy7spra+veYhBP2dCSgxR/i2Y02h5/6w==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/volcano-dashboard-app": { "resolved": "frontend", "link": true @@ -13296,7 +14461,6 @@ "resolved": "https://registry.npmjs.org/ws/-/ws-8.19.0.tgz", "integrity": "sha512-blAT2mjOEIi0ZzruJfIhb3nps74PRWTCz1IjglWEEpQl5XS/UNama6u2/rjFkDDouqr4L67ry+1aGIALViWjDg==", "license": "MIT", - "peer": true, "engines": { "node": ">=10.0.0" }, From e4fec328da76dfa13e475b684d8d71653865d157 Mon Sep 17 00:00:00 2001 From: aryunewaskar77-art Date: Mon, 18 May 2026 18:16:48 +0530 Subject: [PATCH 4/4] prevent incorrect plugin argument type conversion Signed-off-by: aryunewaskar77-art --- frontend/src/pages/Config/yamlUtils.ts | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/frontend/src/pages/Config/yamlUtils.ts b/frontend/src/pages/Config/yamlUtils.ts index 545a06f8..7a6504f2 100644 --- a/frontend/src/pages/Config/yamlUtils.ts +++ b/frontend/src/pages/Config/yamlUtils.ts @@ -28,13 +28,12 @@ export const parseConf = (yamlStr: string): SchedulerConfig => { const rawArgs = p.arguments || {}; const argsList: PluginArg[] = Object.entries(rawArgs).map( ([key, val]) => { - let typedVal: string | number | boolean = String(val); - if (val === "true" || val === true) typedVal = true; - else if (val === "false" || val === false) - typedVal = false; - else if (!isNaN(Number(val)) && typeof val !== "boolean") + let typedVal: string | number | boolean = val; + if (val === "true") typedVal = true; + else if (val === "false") typedVal = false; + else if (typeof val === "string" && val.trim() !== "" && !isNaN(Number(val))) { typedVal = Number(val); - else typedVal = val as string | number | boolean; + } return { key, value: typedVal }; },