diff --git a/frontend/pnpm-lock.yaml b/frontend/pnpm-lock.yaml index 906a9592be8..daa7336c73b 100644 --- a/frontend/pnpm-lock.yaml +++ b/frontend/pnpm-lock.yaml @@ -1206,6 +1206,9 @@ importers: immer: specifier: ^9.0.21 version: 9.0.21 + ini: + specifier: ^4.1.3 + version: 4.1.3 js-cookie: specifier: ^3.0.5 version: 3.0.5 @@ -1276,6 +1279,9 @@ importers: '@svgr/webpack': specifier: ^6.5.1 version: 6.5.1 + '@types/ini': + specifier: ^4.1.1 + version: 4.1.1 '@types/js-cookie': specifier: ^3.0.4 version: 3.0.6 @@ -9755,6 +9761,10 @@ packages: /@types/http-errors@2.0.4: resolution: {integrity: sha512-D0CFMMtydbJAegzOyHjtiKPLlvnm3iTZyZRSZoLq2mRhDdmLfIWOCYPfQJ4cu2erKghU++QvjcUjp/5h7hESpA==} + /@types/ini@4.1.1: + resolution: {integrity: sha512-MIyNUZipBTbyUNnhvuXJTY7B6qNI78meck9Jbv3wk0OgNwRyOOVEKDutAkOs1snB/tx0FafyR6/SN4Ps0hZPeg==} + dev: true + /@types/istanbul-lib-coverage@2.0.6: resolution: {integrity: sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w==} dev: true @@ -15173,6 +15183,11 @@ packages: resolution: {integrity: sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==} dev: false + /ini@4.1.3: + resolution: {integrity: sha512-X7rqawQBvfdjS10YU1y1YVreA3SsLrW9dX2CewP2EbBJM4ypVNLDkO5y04gejPwKIY9lR+7r9gn3rFPt/kmWFg==} + engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} + dev: false + /inline-style-parser@0.1.1: resolution: {integrity: sha512-7NXolsK4CAS5+xvdj5OMMbI962hU/wvwoxk+LWR9Ek9bVtyuuYScDN6eS0rUm6TxApFpw7CX1o4uJzcd4AyD3Q==} dev: false diff --git a/frontend/providers/dbprovider/.vscode/settings.json b/frontend/providers/dbprovider/.vscode/settings.json index d386c478d87..be18ba21857 100644 --- a/frontend/providers/dbprovider/.vscode/settings.json +++ b/frontend/providers/dbprovider/.vscode/settings.json @@ -15,5 +15,9 @@ "i18n-ally.displayLanguage": "zh", // "i18n-ally.namespace": true, "i18n-ally.pathMatcher": "{locale}/{namespaces}.json", - "i18n-ally.extract.targetPickingStrategy": "most-similar-by-key" + "i18n-ally.extract.targetPickingStrategy": "most-similar-by-key", + "i18n-ally.translate.engines": [ + "deepl", + "google", + ] } \ No newline at end of file diff --git a/frontend/providers/dbprovider/package.json b/frontend/providers/dbprovider/package.json index a9439b9873b..d10d6a235f0 100644 --- a/frontend/providers/dbprovider/package.json +++ b/frontend/providers/dbprovider/package.json @@ -31,6 +31,7 @@ "github-markdown-css": "^5.2.0", "i18next": "^23.11.5", "immer": "^9.0.21", + "ini": "^4.1.3", "js-cookie": "^3.0.5", "js-yaml": "^4.1.0", "jszip": "^3.10.1", @@ -56,6 +57,7 @@ }, "devDependencies": { "@svgr/webpack": "^6.5.1", + "@types/ini": "^4.1.1", "@types/js-cookie": "^3.0.4", "@types/js-yaml": "^4.0.6", "@types/lodash": "^4.14.199", diff --git a/frontend/providers/dbprovider/public/images/qdrant.svg b/frontend/providers/dbprovider/public/images/qdrant.svg new file mode 100644 index 00000000000..4e442c99e4b --- /dev/null +++ b/frontend/providers/dbprovider/public/images/qdrant.svg @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + diff --git a/frontend/providers/dbprovider/public/images/weaviate.svg b/frontend/providers/dbprovider/public/images/weaviate.svg new file mode 100644 index 00000000000..a2ddac2220b --- /dev/null +++ b/frontend/providers/dbprovider/public/images/weaviate.svg @@ -0,0 +1,192 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/frontend/providers/dbprovider/public/locales/en/common.json b/frontend/providers/dbprovider/public/locales/en/common.json index ed4ea163fce..bbcd98798ea 100644 --- a/frontend/providers/dbprovider/public/locales/en/common.json +++ b/frontend/providers/dbprovider/public/locales/en/common.json @@ -1,9 +1,7 @@ { - "age": "Runtime", "Auto": "Automatic", "Backup": "Backup", "Cancel": "Discard", - "confirm": "Confirm", "Containers": "Containers", "Continue": "Continue", "Creating": "Creating", @@ -13,8 +11,6 @@ "Day": "Day", "Delete": "Delete", "Deleting": "Deleting...", - "deploy": "Deploy", - "Details": "Details", "Disk": "Storage Size", "Export": "Export", "Failed": "Error", @@ -25,8 +21,6 @@ "Migrate": "Migrate", "Migrating": "Migrating", "Monday": "Monday", - "Name": "Name", - "operation": "Operation", "Option": "Optional", "Password": "Password", "Pause": "Pause", @@ -35,29 +29,24 @@ "Perday": "Perday", "Performance": "Performance", "Pod": "Pod", + "Pod Name": "Pod Name", "Port": "Port", - "prompt": "Prompt", - "Remark": "Note", "Replicas": "Replicas", "Resources": "Resources", "Restart": "Restart", "Restarting": "Restarting", - "restarts": "Restart Count", "Running": "Running", "Saturday": "Sat", "Save": "Save", "SaveTime": "Retention Period", "Start": "Start", "Starting": "Starting", - "Status": "Status", "Success": "succeeded", "Sunday": "Sun", "Thursday": "Thu", - "total_price": "total_price", "Tuesday": "Tue", "Type": "Type", "Unknown": "Unknown", - "update": "Update", "Updating": "Updating", "Username": "Username", "Wednesday": "Wed", @@ -65,11 +54,12 @@ "aborted_connections": "Dropped Connections", "active_connections": "Active Connections", "advanced_configuration": "Advanced Settings", + "age": "Runtime", "anticipated_price": "Projected Cost", "app": { - "resource_quota": "Resource Limits", "cpu_exceeds_quota": "CPU requested exceeds quota. Contact admin.", "memory_exceeds_quota": "Memory requested exceeds quota. Contact admin.", + "resource_quota": "Resource Limits", "storage_exceeds_quota": "Storage requested exceeds quota. Contact admin." }, "app_store": "App Store", @@ -108,6 +98,7 @@ }, "config_form": "Form", "config_info": "Configuration Details", + "confirm": "Confirm", "confirm_delete": "Confirm", "confirm_delete_the_backup": "Confirm deleting this backup?", "confirm_delete_the_migrate": "Confirm deleting the migration record?", @@ -126,6 +117,8 @@ "creation_time": "Creation Time", "current_connections": "Current Connections", "data_migration_config": "Data Migration Settings", + "database_config": "parameters", + "database_edit_config": "Update Parameters", "database_empty": "You don't have any databases yet", "database_host": "Database Hostname", "database_host_empty": "Please enter the database hostname", @@ -142,6 +135,24 @@ "db_instances_tip": "For optimal performance, use an odd number of {{db}} instances", "db_name": "DataBase Name", "db_table": "DataBase Table", + "dbconfig": { + "change_history": "Modification history", + "commit": "submit", + "confirm_updates": "Please confirm the parameters you modified:", + "get_config_err": "Failed to obtain configuration file", + "modified_value": "Modify value", + "modify_time": "Modify Time", + "no_changes": "No modification yet", + "original_value": "Original value", + "parameter": "Parameter Config", + "parameter_name": "parameter name", + "parameter_value": "Parameter value", + "prompt": "Change notice", + "search": "search", + "updates": "Modified parameters", + "updates_tip": "Changing the configuration of the mongo database will cause the database to restart", + "updates_tip2": "Incorrect parameter values ​​may cause the database to fail to operate normally, so please operate with caution." + }, "delete_anyway": "Force Delete", "delete_backup": "Delete Backup", "delete_failed": "Failed to delete", @@ -149,9 +160,11 @@ "delete_successful": "Deleted successfully", "delete_template_app_tip": "To fully remove this app and all its components, please uninstall it directly from the App Store.", "delete_warning": "Delete Warning", + "deploy": "Deploy", "deploy_database": "Deploy DataBase", "deployment_failed": "Deployment Failed", "deployment_successful": "Deployed Successfully", + "details": "Details", "direct_connection": "connect", "document_operations": "Document Operations", "duration_of_transaction": "Transaction Duration", @@ -169,7 +182,6 @@ "file_upload_failed": "File upload failed", "have_error": "Failed", "hits_ratio": "Hits Ratio", - "migration_preparations": "Migration Preparations Completed", "import_through_file": "Import via File", "important_tips_for_migrating": "Tip: Create a new database in sink DB if source_database and sink_database have overlapping data, to avoid conflicts", "innodb_buffer_pool": "InnoDB Buffer Pool", @@ -209,28 +221,33 @@ "migration_failed": "Migration Failed", "migration_permission_check": "Verifying Migration Permissions", "migration_preparation": "Preparing for Migration", + "migration_preparations": "Migration Preparations Completed", "migration_prompt_information": "To prevent migration failure caused by XXX, please refrain from any operations during the migration process. The migration may take a while for large datasets. Your patience is appreciated.", "migration_successful": "Migration Completed Successfully", "migration_task_created_successfully": "Migration task created", "min_replicas": "Min Replicas: ", "monitor_list": "Monitoring", "multi_replica_redis_tip": "Note: The price for multi-replica Redis includes HA nodes", + "name": "Name", "no_data_available": "No Data Available", "not_allow_standalone_use": "This application is not allowed to be used alone. Click OK to go to Sealos Desktop for use.", "online_import": "Import Online", + "operation": "Operation", "page_faults": "Page Faults", "pause_error": "Failed to pause the database", "pause_hint": "Pausing the service will stop the calculation of charges for CPU and memory, but charges for storage and external network ports will still apply. Would you like to pause now?", "pause_success": "Database paused", "please_enter": "Please Enter", - "Pod Name": "Pod Name", + "prompt": "Prompt", "query_operations": "Query Operations", + "remark": "Note", "remark_tip": "Contains up to 10 Chinese characters and 30 English characters", "remind": "remind", "replicas_cannot_empty": "Replicas field is required", "replicas_list": "Pod List", "restart_error": "Failed to restart due to an error", "restart_success": "Restarted successfully. Please wait...", + "restarts": "Restart Count", "restore_backup": "Restore from Backup", "restore_backup_tip": "Restoring a backup will create a new database. Please provide a unique name for the new DB.", "restore_database": "Restore Database from Backup", @@ -240,6 +257,7 @@ "select_a_maximum_of_10_files": "Select up to 10 files", "service_deletion_failed": "Failed to delete the service", "set_auto_backup_successful": "Automatic backup task set successfully", + "single_node_tip": "Single-node DB for development and testing only", "slow_queries": "Slow Queries", "source_database": "Source Database", "start_backup": "Start Backup", @@ -247,6 +265,7 @@ "start_hour": "Hour", "start_minute": "Minute", "start_success": "Database started. Please wait...", + "status": "Status", "storage": "Storage", "storage_cannot_empty": "Please specify storage size", "storage_max": "maximum storage", @@ -255,8 +274,9 @@ "submit_error": "Error submitting form", "successfully_closed_external_network_access": "Internet access disabled", "table_locks": "Table-level locks", - "single_node_tip": "Single-node DB for development and testing only", + "total_price": "total_price", "turn_on": "Enable", + "update": "Update", "update_database": "Update DataBase", "update_failed": "Update Failed", "update_successful": "Update succeeded", diff --git a/frontend/providers/dbprovider/public/locales/zh/common.json b/frontend/providers/dbprovider/public/locales/zh/common.json index 95ed772beb6..aed52f2cb00 100644 --- a/frontend/providers/dbprovider/public/locales/zh/common.json +++ b/frontend/providers/dbprovider/public/locales/zh/common.json @@ -1,10 +1,7 @@ { - "age": "运行时长", "Auto": "自动", "Backup": "备份", - "basic": "基础配置", "Cancel": "取消", - "confirm": "确认", "Containers": "容器", "Continue": "继续", "Creating": "创建中", @@ -14,11 +11,9 @@ "Day": "天", "Delete": "删除", "Deleting": "删除中", - "deploy": "部署", - "Details": "详情", "Disk": "磁盘空间", "Export": "导出", - "Failed": "有异常", + "Failed": "异常", "Friday": "周五", "Hour": "小时", "Logs": "日志", @@ -26,8 +21,6 @@ "Migrate": "迁移", "Migrating": "正在迁移", "Monday": "周一", - "Name": "名字", - "operation": "操作", "Option": "选填", "Password": "密码", "Pause": "暂停", @@ -36,44 +29,37 @@ "Perday": "每日", "Performance": "性能", "Pod": "实例", + "Pod Name": "实例名", "Port": "端口", - "prompt": "提示", - "Remark": "备注", - "remind": "提醒", "Replicas": "实例数", "Resources": "资源", "Restart": "重启", "Restarting": "重启中", - "restarts": "重启次数", "Running": "运行中", "Saturday": "周六", "Save": "保存", "SaveTime": "保留时间", "Start": "开始", "Starting": "启动中", - "Status": "状态", - "storage_max": "最大存储", - "storage_min": "最小存储", "Success": "成功", "Sunday": "周日", "Thursday": "周四", "Tuesday": "周二", "Type": "类型", "Unknown": "未知", - "update": "变更", "Updating": "变更中", "Username": "用户名", - "version": "版本", "Wednesday": "周三", "Week": "周", "aborted_connections": "异常连接数", "active_connections": "活跃连接数", "advanced_configuration": "高级配置", + "age": "运行时长", "anticipated_price": "预估价格", "app": { - "resource_quota": "资源配额", "cpu_exceeds_quota": "申请的 CPU 超出限制,请联系管理员", "memory_exceeds_quota": "申请的 '内存' 超出限制,请联系管理员", + "resource_quota": "资源配额", "storage_exceeds_quota": "申请的 '存储' 超出限制,请联系管理员" }, "app_store": "应用商店", @@ -93,6 +79,7 @@ "backup_running": "备份中", "backup_success_tip": "备份任务已经成功创建", "backup_time": "备份时间", + "basic": "基础配置", "billing_standards": "计费标准", "block_read_time": "读数据块时间", "block_write_time": "写数据块时间", @@ -111,6 +98,7 @@ }, "config_form": "配置表单", "config_info": "配置信息", + "confirm": "确认", "confirm_delete": "确认删除", "confirm_delete_the_backup": "确认删除该备份?", "confirm_delete_the_migrate": "确定删除迁移记录吗?", @@ -129,6 +117,8 @@ "creation_time": "创建时间", "current_connections": "当前连接数", "data_migration_config": "数据迁移配置", + "database_config": "数据库参数", + "database_edit_config": "变更参数", "database_empty": "您还没有数据库", "database_host": "数据库主机名", "database_host_empty": "缺少数据库主机名", @@ -145,6 +135,24 @@ "db_instances_tip": "{{db}} 实例数量建议为奇数", "db_name": "数据库名字", "db_table": "数据库表", + "dbconfig": { + "change_history": "修改历史", + "commit": "提交", + "confirm_updates": "请确认您修改的参数:", + "get_config_err": "获取配置文件失败", + "modified_value": "修改值", + "modify_time": "修改时间", + "no_changes": "暂无修改", + "original_value": "原始值", + "parameter": "参数配置", + "parameter_name": "参数名", + "parameter_value": "参数值", + "prompt": "变更须知", + "search": "搜索", + "updates": "修改的参数", + "updates_tip": "mongo数据库变更配置会导致数据库重启", + "updates_tip2": "参数值错误可能会导致数据库无法正常运行,请谨慎操作。" + }, "delete_anyway": "仍要删除", "delete_backup": "删除备份", "delete_failed": "删除出现意外", @@ -152,9 +160,11 @@ "delete_successful": "删除成功", "delete_template_app_tip": "该应用是通过应用商店部署的,如果您想完全卸载该应用并清理所有相关的组件,请到应用商店中将整个应用删除。", "delete_warning": "删除警告", + "deploy": "部署", "deploy_database": "部署数据库", "deployment_failed": "部署失败", "deployment_successful": "部署成功", + "details": "详情", "direct_connection": "连接", "document_operations": "文档操作数", "duration_of_transaction": "事务持续时间", @@ -172,7 +182,6 @@ "file_upload_failed": "文件上传失败", "have_error": "出现异常", "hits_ratio": "命中率", - "migration_preparations": "我已阅读并完成迁移准备工作", "import_through_file": "文件导入", "important_tips_for_migrating": "如果 source 数据库中 source_database 和 sink 数据库中 sink_database 的数据库有重叠,应该在sink 数据库中新建 database,以免出现数据重叠", "innodb_buffer_pool": "InnoDB 缓冲池", @@ -212,26 +221,33 @@ "migration_failed": "迁移失败", "migration_permission_check": "迁移权限检查", "migration_preparation": "迁移准备", + "migration_preparations": "我已阅读并完成迁移准备工作", "migration_prompt_information": "迁移时请勿执行操作,以免因XXX导致迁移失败。如数据量较大,迁移时间可能较长,请耐心等待", "migration_successful": "迁移成功", "migration_task_created_successfully": "迁移任务创建成功", "min_replicas": "实例数最小为: ", "monitor_list": "实时监控", + "multi_replica_redis_tip": "Redis 多副本包含 HA 节点,请悉知,预估价格已包含 HA 节点费用", + "name": "名字", "no_data_available": "暂无数据", "not_allow_standalone_use": "该应用不允许单独使用,点击确认前往 Sealos Desktop 使用。", "online_import": "在线导入", + "operation": "操作", "page_faults": "页错误", "pause_error": "数据库暂停失败", "pause_hint": "暂停服务将停止计算 CPU 和内存等费用,但存储和外网端口仍将产生费用。是否现在暂停?", "pause_success": "数据库已暂停", "please_enter": "请输入", - "Pod Name": "实例名", + "prompt": "提示", "query_operations": "查询操作数", + "remark": "备注", "remark_tip": "最多包含10个中文字符,30个英文字符。", + "remind": "提醒", "replicas_cannot_empty": "实例数不能为空", "replicas_list": "实例列表", "restart_error": "重启出现了意外", "restart_success": "重启成功,请等待", + "restarts": "重启次数", "restore_backup": "恢复备份", "restore_backup_tip": "恢复备份会创建一个新的数据库,你需要提供新的数据库名,并且不能与当前数据库重名。", "restore_database": "恢复数据库备份", @@ -241,6 +257,7 @@ "select_a_maximum_of_10_files": "最多选择 10 个文件", "service_deletion_failed": "Service 删除失败", "set_auto_backup_successful": "设置自动备份任务成功", + "single_node_tip": "单节点数据库仅适用开发测试", "slow_queries": "慢查询", "source_database": "源数据库", "start_backup": "开始备份", @@ -248,21 +265,24 @@ "start_hour": "小时", "start_minute": "分钟", "start_success": "数据库启动成功,请等待", + "status": "状态", "storage": "磁盘", "storage_cannot_empty": "容量不能为空", + "storage_max": "最大存储", + "storage_min": "最小存储", "storage_range": "容量范围: ", "submit_error": "提交表单错误", "successfully_closed_external_network_access": "已关闭外网访问", "table_locks": "表锁", - "multi_replica_redis_tip": "Redis 多副本包含 HA 节点,请悉知,预估价格已包含 HA 节点费用", - "single_node_tip": "单节点数据库仅适用开发测试", "total_price": "总价", "turn_on": "开启", + "update": "变更", "update_database": "变更数据库", "update_failed": "更新失败", "update_successful": "更新成功", "update_time": "更新时间", "upload_dump_file": "点击上传 Dump 文件", "use_docs": "使用文档", + "version": "版本", "yaml_file": "YAML 文件" } \ No newline at end of file diff --git a/frontend/providers/dbprovider/src/api/db.ts b/frontend/providers/dbprovider/src/api/db.ts index dcd26ac0b67..07947cc8690 100644 --- a/frontend/providers/dbprovider/src/api/db.ts +++ b/frontend/providers/dbprovider/src/api/db.ts @@ -1,6 +1,12 @@ import { GET, POST, DELETE } from '@/services/request'; import { adaptDBListItem, adaptDBDetail, adaptPod, adaptEvents } from '@/utils/adapt'; -import type { BackupItemType, DBEditType, DBType, PodDetailType } from '@/types/db'; +import type { + BackupItemType, + DBEditType, + DBType, + OpsRequestItemType, + PodDetailType +} from '@/types/db'; import { json2Restart } from '@/utils/json2Yaml'; import { json2StartOrStop } from '../utils/json2Yaml'; import type { SecretResponse } from '@/pages/api/getSecretByName'; @@ -14,6 +20,9 @@ export const getMyDBList = () => export const getDBByName = (name: string) => GET(`/api/getDBByName?name=${name}`).then(adaptDBDetail); +export const getConfigByName = ({ name, dbType }: { name: string; dbType: DBType }) => + GET(`/api/getConfigByName?name=${name}&dbType=${dbType}`); + export const createDB = (payload: { dbForm: DBEditType; isEdit: boolean; @@ -86,4 +95,17 @@ export const getMonitorData = (payload: { end: number; }) => GET<{ result: MonitorChartDataResult }>(`/api/monitor/getMonitorData`, payload); -export const getOpsRequest = (name: string) => GET(`/api/opsrequest/get?name=${name}`); +export const getOpsRequest = ({ + name, + label, + dbType +}: { + name: string; + label: string; + dbType: DBType; +}) => + GET(`/api/opsrequest/list`, { + name, + label, + dbType + }); diff --git a/frontend/providers/dbprovider/src/components/Icon/icons/edit.svg b/frontend/providers/dbprovider/src/components/Icon/icons/edit.svg new file mode 100644 index 00000000000..031f0c1f310 --- /dev/null +++ b/frontend/providers/dbprovider/src/components/Icon/icons/edit.svg @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/frontend/providers/dbprovider/src/components/Icon/icons/search.svg b/frontend/providers/dbprovider/src/components/Icon/icons/search.svg new file mode 100644 index 00000000000..67426d97a70 --- /dev/null +++ b/frontend/providers/dbprovider/src/components/Icon/icons/search.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/frontend/providers/dbprovider/src/components/Icon/index.tsx b/frontend/providers/dbprovider/src/components/Icon/index.tsx index bddb6e52a29..75eabab5a78 100644 --- a/frontend/providers/dbprovider/src/components/Icon/index.tsx +++ b/frontend/providers/dbprovider/src/components/Icon/index.tsx @@ -38,6 +38,8 @@ const map = { infoCircle: require('./icons/infoCircle.svg').default, upperRight: require('./icons/upperRight.svg').default, arrowUp: require('./icons/arrowUp.svg').default, + search: require('./icons/search.svg').default, + edit: require('./icons/edit.svg').default, book: require('./icons/book.svg').default, export: require('./icons/export.svg').default, pods: require('./icons/pods.svg').default, diff --git a/frontend/providers/dbprovider/src/constants/db.ts b/frontend/providers/dbprovider/src/constants/db.ts index f90b0933f65..70099f0195d 100644 --- a/frontend/providers/dbprovider/src/constants/db.ts +++ b/frontend/providers/dbprovider/src/constants/db.ts @@ -1,4 +1,4 @@ -import { DBEditType, DBDetailType, PodDetailType } from '@/types/db'; +import { DBEditType, DBDetailType, PodDetailType, DBType, ReconfigStatusMapType } from '@/types/db'; import { CpuSlideMarkList, MemorySlideMarkList } from './editApp'; export const crLabelKey = 'sealos-db-provider-cr'; @@ -7,7 +7,9 @@ export const KBMigrationTaskLabel = 'datamigration.apecloud.io/migrationtask'; export const KBBackupNameLabel = 'dataprotection.kubeblocks.io/backup-name'; export const SealosMigrationTaskLabel = 'datamigration.sealos.io/file-migration-task'; export const MigrationRemark = 'migration-remark'; +export const DBPreviousConfigKey = 'cloud.sealos.io/previous-config'; export const templateDeployKey = 'cloud.sealos.io/deploy-on-sealos'; +export const DBReconfigureKey = 'ops.kubeblocks.io/ops-type=Reconfiguring'; export enum DBTypeEnum { postgresql = 'postgresql', @@ -37,6 +39,43 @@ export enum DBStatusEnum { UnKnow = 'UnKnow', Deleting = 'Deleting' } + +export enum ReconfigStatus { + Deleting = 'Deleting', + Creating = 'Creating', + Running = 'Running', + Succeed = 'Succeed', + Failed = 'Failed' +} + +export const DBReconfigStatusMap: Record<`${ReconfigStatus}`, ReconfigStatusMapType> = { + [ReconfigStatus.Deleting]: { + label: 'Deleting', + value: ReconfigStatus.Deleting, + color: '#DC6803' + }, + [ReconfigStatus.Creating]: { + label: 'Creating', + value: ReconfigStatus.Creating, + color: '#667085' + }, + [ReconfigStatus.Running]: { + label: 'Running', + value: ReconfigStatus.Running, + color: '#667085' + }, + [ReconfigStatus.Succeed]: { + label: 'Success', + value: ReconfigStatus.Succeed, + color: '#039855' + }, + [ReconfigStatus.Failed]: { + label: 'Failed', + value: ReconfigStatus.Failed, + color: '#F04438' + } +}; + export const dbStatusMap = { [DBStatusEnum.Creating]: { label: 'Creating', @@ -293,3 +332,77 @@ export const DBTypeSecretMap = { connectKey: 'milvus' } }; + +export const DBReconfigureMap: { + [key in DBType]: { + configMapKey: string; + configMapName: string; + type: 'yaml' | 'ini'; + reconfigureName: string; + reconfigureKey: string; + }; +} = { + postgresql: { + configMapName: '-postgresql-postgresql-configuration', + configMapKey: 'postgresql.conf', + type: 'ini', + reconfigureName: 'postgresql-configuration', + reconfigureKey: 'postgresql.conf' + }, + mongodb: { + type: 'yaml', + configMapName: '-mongodb-mongodb-config', + configMapKey: 'mongodb.conf', + reconfigureName: 'mongodb-config', + reconfigureKey: 'mongodb.conf' + }, + 'apecloud-mysql': { + type: 'ini', + configMapName: '-mysql-mysql-consensusset-config', + configMapKey: 'my.cnf', + reconfigureName: 'mysql-consensusset-config', + reconfigureKey: 'my.cnf' + }, + redis: { + type: 'ini', + configMapName: '', + configMapKey: '', + reconfigureName: '', + reconfigureKey: '' + }, + kafka: { + type: 'ini', + configMapName: '', + configMapKey: '', + reconfigureName: '', + reconfigureKey: '' + }, + qdrant: { + type: 'ini', + configMapName: '', + configMapKey: '', + reconfigureName: '', + reconfigureKey: '' + }, + nebula: { + type: 'ini', + configMapName: '', + configMapKey: '', + reconfigureName: '', + reconfigureKey: '' + }, + weaviate: { + type: 'ini', + configMapName: '', + configMapKey: '', + reconfigureName: '', + reconfigureKey: '' + }, + milvus: { + type: 'ini', + configMapName: '', + configMapKey: '', + reconfigureName: '', + reconfigureKey: '' + } +}; diff --git a/frontend/providers/dbprovider/src/pages/api/getConfigByName.ts b/frontend/providers/dbprovider/src/pages/api/getConfigByName.ts new file mode 100644 index 00000000000..4a5909b6ce5 --- /dev/null +++ b/frontend/providers/dbprovider/src/pages/api/getConfigByName.ts @@ -0,0 +1,46 @@ +import { DBReconfigureMap } from '@/constants/db'; +import { authSession } from '@/services/backend/auth'; +import { getK8s } from '@/services/backend/kubernetes'; +import { jsonRes } from '@/services/backend/response'; +import { ApiResp } from '@/services/kubernet'; +import { DBType } from '@/types/db'; +import type { NextApiRequest, NextApiResponse } from 'next'; + +export default async function handler(req: NextApiRequest, res: NextApiResponse) { + try { + const { name, dbType } = req.query as { name: string; dbType: DBType }; + if (!name) { + throw new Error('name is empty'); + } + + const { namespace, k8sCore } = await getK8s({ + kubeconfig: await authSession(req) + }); + + const dbConfig = DBReconfigureMap[dbType]; + const key = name + dbConfig.configMapName; + if (!key || !dbConfig.configMapName) { + return jsonRes(res, { + data: null + }); + } + + const { body } = await k8sCore.readNamespacedConfigMap(key, namespace); + + const configData = body?.data && body?.data[dbConfig.configMapKey]; + if (!configData) { + return jsonRes(res, { + data: null + }); + } + + jsonRes(res, { + data: configData + }); + } catch (err: any) { + jsonRes(res, { + code: 500, + error: err + }); + } +} diff --git a/frontend/providers/dbprovider/src/pages/api/getDBByName.ts b/frontend/providers/dbprovider/src/pages/api/getDBByName.ts index 5e68b25a2e4..42efd389183 100644 --- a/frontend/providers/dbprovider/src/pages/api/getDBByName.ts +++ b/frontend/providers/dbprovider/src/pages/api/getDBByName.ts @@ -11,17 +11,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse< throw new Error('name is empty'); } - const { k8sCustomObjects, namespace } = await getK8s({ - kubeconfig: await authSession(req) - }); - - const { body } = await k8sCustomObjects.getNamespacedCustomObject( - 'apps.kubeblocks.io', - 'v1alpha1', - namespace, - 'clusters', - name - ); + const body = await getCluster(req, name); jsonRes(res, { data: body @@ -33,3 +23,19 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse< }); } } + +export async function getCluster(req: NextApiRequest, name: string) { + const { k8sCustomObjects, namespace } = await getK8s({ + kubeconfig: await authSession(req) + }); + + const { body } = await k8sCustomObjects.getNamespacedCustomObject( + 'apps.kubeblocks.io', + 'v1alpha1', + namespace, + 'clusters', + name + ); + + return body; +} diff --git a/frontend/providers/dbprovider/src/pages/api/opsrequest/get.ts b/frontend/providers/dbprovider/src/pages/api/opsrequest/list.ts similarity index 54% rename from frontend/providers/dbprovider/src/pages/api/opsrequest/get.ts rename to frontend/providers/dbprovider/src/pages/api/opsrequest/list.ts index f0e0e1b3c2e..ea63e4477e1 100644 --- a/frontend/providers/dbprovider/src/pages/api/opsrequest/get.ts +++ b/frontend/providers/dbprovider/src/pages/api/opsrequest/list.ts @@ -2,19 +2,29 @@ import { authSession } from '@/services/backend/auth'; import { getK8s } from '@/services/backend/kubernetes'; import { jsonRes } from '@/services/backend/response'; import { ApiResp } from '@/services/kubernet'; +import { KubeBlockOpsRequestType } from '@/types/cluster'; +import { DBType } from '@/types/db'; +import { adaptOpsRequest } from '@/utils/adapt'; import type { NextApiRequest, NextApiResponse } from 'next'; export default async function handler(req: NextApiRequest, res: NextApiResponse) { try { - const { name } = req.query as { + const { name, label, dbType } = req.query as { name: string; + label: string; + dbType: DBType; }; const { k8sCustomObjects, namespace } = await getK8s({ kubeconfig: await authSession(req) }); - const { body } = await k8sCustomObjects.listNamespacedCustomObject( + let labelSelector = `app.kubernetes.io/instance=${name}`; + if (label) { + labelSelector += `,${label}`; + } + + const opsrequestsList = (await k8sCustomObjects.listNamespacedCustomObject( 'apps.kubeblocks.io', 'v1alpha1', namespace, @@ -23,11 +33,17 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse< undefined, undefined, undefined, - `app.kubernetes.io/instance=${name}` - ); + labelSelector + )) as { + body: { + items: KubeBlockOpsRequestType[]; + }; + }; + + const data = opsrequestsList.body.items.map((res) => adaptOpsRequest(res, dbType)); jsonRes(res, { - data: body + data: data }); } catch (err: any) { jsonRes(res, { diff --git a/frontend/providers/dbprovider/src/pages/db/detail/components/BackupModal.tsx b/frontend/providers/dbprovider/src/pages/db/detail/components/BackupModal.tsx index e8bac66e5c0..c4275efaab1 100644 --- a/frontend/providers/dbprovider/src/pages/db/detail/components/BackupModal.tsx +++ b/frontend/providers/dbprovider/src/pages/db/detail/components/BackupModal.tsx @@ -287,7 +287,7 @@ const BackupModal = ({ /> - {t('Remark')} + {t('remark')} React.ReactNode | string; }[] = [ { - title: 'Name', + title: 'name', key: 'name', dataIndex: 'name' }, { - title: 'Remark', + title: 'remark', key: 'remark', dataIndex: 'remark' }, { - title: 'Status', + title: 'status', key: 'status', render: (item: BackupItemType) => ( diff --git a/frontend/providers/dbprovider/src/pages/db/detail/components/DumpImport/index.tsx b/frontend/providers/dbprovider/src/pages/db/detail/components/DumpImport/index.tsx index 32bf6793b58..f430ab1bc31 100644 --- a/frontend/providers/dbprovider/src/pages/db/detail/components/DumpImport/index.tsx +++ b/frontend/providers/dbprovider/src/pages/db/detail/components/DumpImport/index.tsx @@ -197,11 +197,6 @@ export default function DumpImport({ db }: { db?: DBDetailType }) { return ( - - - - - {t('upload_dump_file')} diff --git a/frontend/providers/dbprovider/src/pages/db/detail/components/Header.tsx b/frontend/providers/dbprovider/src/pages/db/detail/components/Header.tsx index 1e6c0852968..0e6584a0bb1 100644 --- a/frontend/providers/dbprovider/src/pages/db/detail/components/Header.tsx +++ b/frontend/providers/dbprovider/src/pages/db/detail/components/Header.tsx @@ -111,7 +111,7 @@ const Header = ({ leftIcon={} onClick={() => setShowSlider(true)} > - {t('Details')} + {t('details')} )} diff --git a/frontend/providers/dbprovider/src/pages/db/detail/components/Migrate/Table.tsx b/frontend/providers/dbprovider/src/pages/db/detail/components/Migrate/Table.tsx index 99c6ead5175..3a2e10708a7 100644 --- a/frontend/providers/dbprovider/src/pages/db/detail/components/Migrate/Table.tsx +++ b/frontend/providers/dbprovider/src/pages/db/detail/components/Migrate/Table.tsx @@ -85,17 +85,17 @@ export const MigrateTable = ({ dbName }: { dbName: string }) => { render?: (item: MigrateItemType, i: number) => React.ReactNode | string; }[] = [ { - title: 'Name', + title: 'name', key: 'name', dataIndex: 'name' }, { - title: 'Remark', + title: 'remark', key: 'remark', dataIndex: 'remark' }, { - title: 'Status', + title: 'status', key: 'status', render: (item: MigrateItemType) => ( diff --git a/frontend/providers/dbprovider/src/pages/db/detail/components/Monitor/index.tsx b/frontend/providers/dbprovider/src/pages/db/detail/components/Monitor/index.tsx index 1cd82ccbb0d..95c029002d1 100644 --- a/frontend/providers/dbprovider/src/pages/db/detail/components/Monitor/index.tsx +++ b/frontend/providers/dbprovider/src/pages/db/detail/components/Monitor/index.tsx @@ -39,7 +39,7 @@ const Monitor = ({ db, dbName, dbType }: { dbName: string; dbType: string; db?: w={'280px'} list={[ { id: MonitorType.resources, label: t('Resources') }, - { id: MonitorType.status, label: t('Status') }, + { id: MonitorType.status, label: t('status') }, { id: MonitorType.performance, label: t('Performance') } ]} activeId={activeId} diff --git a/frontend/providers/dbprovider/src/pages/db/detail/components/PodDetailModal.tsx b/frontend/providers/dbprovider/src/pages/db/detail/components/PodDetailModal.tsx index b1b7915fb76..062908780cd 100644 --- a/frontend/providers/dbprovider/src/pages/db/detail/components/PodDetailModal.tsx +++ b/frontend/providers/dbprovider/src/pages/db/detail/components/PodDetailModal.tsx @@ -178,7 +178,7 @@ const Logs = ({ - Pod {t('Details')} + Pod {t('details')} - {t('Details')} + {t('details')} { key: 'control', render: (item: PodDetailType, i: number) => ( - + diff --git a/frontend/providers/dbprovider/src/pages/db/detail/components/Reconfigure/ConfigTable.tsx b/frontend/providers/dbprovider/src/pages/db/detail/components/Reconfigure/ConfigTable.tsx new file mode 100644 index 00000000000..f23b7e58206 --- /dev/null +++ b/frontend/providers/dbprovider/src/pages/db/detail/components/Reconfigure/ConfigTable.tsx @@ -0,0 +1,224 @@ +import MyIcon from '@/components/Icon'; +import { I18nCommonKey } from '@/types/i18next'; +import { Box, Flex, Input, InputGroup, InputLeftElement, Text } from '@chakra-ui/react'; +import { useTranslation } from 'next-i18next'; +import React, { forwardRef, useEffect, useImperativeHandle, useState } from 'react'; +import { useFieldArray, useForm, useWatch } from 'react-hook-form'; + +export interface ConfigItem { + key: string; + value: string; + isEditing: boolean; + isEdited: boolean; + originalIndex: number; +} + +export interface ConfigTableRef { + submit: () => Difference[]; + reset: () => void; +} + +export interface Difference { + path: string; + oldValue: string; + newValue: string; +} + +const ConfigTable = forwardRef< + ConfigTableRef, + { initialData: ConfigItem[]; onDifferenceChange: (hasDifferences: boolean) => void } +>(function ConfigTable({ initialData = [], onDifferenceChange }, ref) { + const { t } = useTranslation(); + const [searchTerm, setSearchTerm] = useState(''); + + const { register, watch, setValue, control, reset } = useForm<{ configs: ConfigItem[] }>({ + defaultValues: { + configs: initialData.map((item) => ({ ...item, isEditing: false, isEdited: false })) + } + }); + + const { fields } = useFieldArray({ + control, + name: 'configs' + }); + + const watchFieldArray = watch('configs'); + + const controlledFields = fields + .map((field, index) => { + return { + ...field, + ...watchFieldArray[index], + originalIndex: index + }; + }) + .filter((item) => item.key.toLowerCase().includes(searchTerm.toLowerCase())); + + const toggleEdit = (index: number) => { + setValue(`configs.${index}.isEditing`, !watchFieldArray[index].isEditing); + }; + + const handleBlur = (index: number) => { + const config = watchFieldArray[index]; + setValue(`configs.${index}.isEdited`, config.value !== initialData[index].value); + setValue(`configs.${index}.isEditing`, false); + }; + + const getChangedConfigs = (): Difference[] => { + const currentConfigs = watchFieldArray; + return currentConfigs.reduce((acc, config, index) => { + if (config.value !== initialData[index].value) { + acc.push({ + path: config.key, + oldValue: initialData[index].value, + newValue: config.value + }); + } + return acc; + }, [] as Difference[]); + }; + + const watchedConfigs = useWatch({ + control, + name: 'configs' + }); + + useEffect(() => { + const differences = getChangedConfigs(); + onDifferenceChange(differences.length > 0); + }, [watchedConfigs]); + + useImperativeHandle(ref, () => ({ + submit: () => { + const changedConfigs = getChangedConfigs(); + return changedConfigs; + }, + reset: () => { + reset({ + configs: initialData.map((item) => ({ ...item, isEditing: false, isEdited: false })) + }); + } + })); + + const configColumns: { + title: I18nCommonKey; + key: string; + render: (item: ConfigItem, index: number) => React.ReactNode; + }[] = [ + { + title: 'dbconfig.parameter_name', + key: 'parameter_name', + render: (item) => ( + + {item.key} + + ) + }, + { + title: 'dbconfig.parameter_value', + key: 'parameter_value', + render: (item) => ( + + {item.isEditing ? ( + handleBlur(item.originalIndex)} + /> + ) : ( + + + {item.value} + + toggleEdit(item.originalIndex)} + cursor={'pointer'} + name={'edit'} + w={'16px'} + h={'16px'} + color={'grayModern.600'} + /> + + )} + + ) + } + ]; + + return ( + + + + + {t('dbconfig.parameter_name')} + + + + + setSearchTerm(e.target.value)} + /> + + setSearchTerm('')}> + + + + + + {t('dbconfig.parameter_value')} + + + + {controlledFields?.map((item, configIndex) => ( + + {configColumns.map((col) => ( + + {col.render(item, configIndex)} + + ))} + + ))} + + + ); +}); + +export default ConfigTable; diff --git a/frontend/providers/dbprovider/src/pages/db/detail/components/Reconfigure/History.tsx b/frontend/providers/dbprovider/src/pages/db/detail/components/Reconfigure/History.tsx new file mode 100644 index 00000000000..ca4a5a1a391 --- /dev/null +++ b/frontend/providers/dbprovider/src/pages/db/detail/components/Reconfigure/History.tsx @@ -0,0 +1,157 @@ +import { getOpsRequest } from '@/api/db'; +import MyIcon from '@/components/Icon'; +import { DBReconfigureKey } from '@/constants/db'; +import { DBDetailType, OpsRequestItemType } from '@/types/db'; +import { I18nCommonKey } from '@/types/i18next'; +import { + TableContainer, + Table, + Thead, + Tr, + Th, + Tbody, + Td, + Box, + Flex, + Divider +} from '@chakra-ui/react'; +import { useQuery } from '@tanstack/react-query'; +import dayjs from 'dayjs'; +import { useTranslation } from 'next-i18next'; +import { Fragment } from 'react'; + +export default function History({ db }: { db?: DBDetailType }) { + const { t } = useTranslation(); + + const { data: operationList = [], isSuccess } = useQuery( + ['getOperationList', db?.dbName, db?.dbType], + async () => { + if (!db?.dbName || !db?.dbType) return []; + const operationList = await getOpsRequest({ + name: db.dbName, + label: DBReconfigureKey, + dbType: db.dbType + }); + operationList.sort( + (a, b) => new Date(b.startTime).getTime() - new Date(a.startTime).getTime() + ); + return operationList; + }, + { + enabled: !!db?.dbName, + refetchInterval: 5000 + } + ); + + const historyColumns: { + title: I18nCommonKey; + dataIndex?: keyof OpsRequestItemType; + key: string; + render?: (item: OpsRequestItemType, configIndex: number) => React.ReactNode | string; + }[] = [ + { + title: 'dbconfig.modify_time', + key: 'creation_time', + render: (item, configIndex) => ( + {configIndex === 0 ? dayjs(item.startTime).format('YYYY-MM-DD HH:mm') : ''} + ) + }, + { + title: 'name', + key: 'name', + render: (item, configIndex) => {item.configurations[configIndex].parameterName} + }, + { + title: 'dbconfig.original_value', + key: 'original_value', + render: (item, configIndex) => ( + {item.configurations[configIndex].oldValue} + ) + }, + { + title: 'dbconfig.modified_value', + key: 'modified_value', + render: (item, configIndex) => ( + {item.configurations[configIndex].newValue} + ) + }, + { + title: 'status', + key: 'status', + render: (item, configIndex) => ( + + {configIndex === 0 ? t(item.status.label) : ''} + + ) + } + ]; + + if (isSuccess && operationList?.length === 0) { + return ( + + + {t('no_data_available')} + + ); + } + + return ( + + + + + {historyColumns.map((item) => ( + + ))} + + + + {operationList?.map((app, appIndex) => { + return ( + + {app.configurations?.map((item, configIndex) => { + return ( + + {historyColumns.map((col) => ( + + ))} + + ); + })} + {appIndex < operationList.length - 1 && ( + + + + )} + + ); + })} + +
+ {t(item.title)} +
+ {col.render + ? col.render(app, configIndex) + : col.dataIndex + ? `${app[col.dataIndex]}` + : '-'} +
+ +
+
+ ); +} diff --git a/frontend/providers/dbprovider/src/pages/db/detail/components/Reconfigure/index.tsx b/frontend/providers/dbprovider/src/pages/db/detail/components/Reconfigure/index.tsx new file mode 100644 index 00000000000..b66a4f4aba6 --- /dev/null +++ b/frontend/providers/dbprovider/src/pages/db/detail/components/Reconfigure/index.tsx @@ -0,0 +1,286 @@ +import { applyYamlList, getConfigByName } from '@/api/db'; +import MyIcon from '@/components/Icon'; +import { DBReconfigureMap } from '@/constants/db'; +import type { DBDetailType } from '@/types/db'; +import { I18nCommonKey } from '@/types/i18next'; +import { json2Reconfigure } from '@/utils/json2Yaml'; +import { adjustDifferencesForIni, flattenObject, parseConfig } from '@/utils/tools'; +import { + Box, + Button, + Flex, + Modal, + ModalBody, + ModalCloseButton, + ModalContent, + ModalFooter, + ModalHeader, + ModalOverlay, + Tag, + Text, + useDisclosure +} from '@chakra-ui/react'; +import { useMessage } from '@sealos/ui'; +import { useQuery } from '@tanstack/react-query'; +import { useTranslation } from 'next-i18next'; +import { useRouter } from 'next/router'; +import React, { ForwardedRef, forwardRef, useEffect, useRef, useState } from 'react'; +import ConfigTable, { ConfigTableRef, Difference } from './ConfigTable'; +import History from './History'; + +export type ComponentRef = { + openBackup: () => void; +}; + +enum SubMenuEnum { + Parameter = 'parameter', + History = 'history' +} + +const ReconfigureTable = ({ db }: { db?: DBDetailType }, ref: ForwardedRef) => { + if (!db) return <>; + + const { t } = useTranslation(); + const { message: toast } = useMessage(); + const router = useRouter(); + const { isOpen, onOpen, onClose } = useDisclosure(); + const [subMenu, setSubMenu] = useState(SubMenuEnum.Parameter); + const configTableRef = useRef(null); + const [differences, setDifferences] = useState([]); + const [hasDifferences, setHasDifferences] = useState(false); + + const handleDifferenceChange = (hasDiff: boolean) => { + setHasDifferences(hasDiff); + }; + + const parseSubMenu = (subMenu: string | string[] | undefined): SubMenuEnum => { + return subMenu === SubMenuEnum.History ? SubMenuEnum.History : SubMenuEnum.Parameter; + }; + + useEffect(() => { + router.query?.subMenu && setSubMenu(parseSubMenu(router.query.subMenu as string)); + }, [router.query?.subMenu]); + + const updateSubMenu = (newSubMenu: SubMenuEnum) => { + setSubMenu(newSubMenu); + router.push({ + query: { ...router.query, subMenu: newSubMenu } + }); + }; + + const { data: config } = useQuery( + ['getConfigByName', db.dbName, db.dbType, subMenu], + async () => { + const _config = await getConfigByName({ name: db.dbName, dbType: db.dbType }); + const temp = parseConfig({ + configString: _config, + type: DBReconfigureMap[db.dbType].type + }); + const result = flattenObject(temp).map((item, index) => ({ + ...item, + isEditing: false, + isEdited: false, + originalIndex: index + })); + return result; + }, + { + enabled: !!db.dbName + } + ); + + const handleReconfigure = async () => { + try { + const differences = configTableRef.current?.submit(); + if (!differences) return; + const reconfigureType = DBReconfigureMap[db.dbType].type; + const reconfigureYaml = json2Reconfigure( + db.dbName, + db.dbType, + db.id, + adjustDifferencesForIni(differences, reconfigureType, db.dbType) + ); + await applyYamlList([reconfigureYaml], 'create'); + onClose(); + router.push({ + query: { + ...router.query, + subMenu: SubMenuEnum.History + } + }); + toast({ title: t('Success'), status: 'success' }); + } catch (error) { + toast({ title: t('have_error'), status: 'error' }); + } + }; + + const SubNavList = [ + { label: t('dbconfig.parameter'), value: SubMenuEnum.Parameter }, + { label: t('dbconfig.change_history'), value: SubMenuEnum.History } + ]; + + const handleSubmit = () => { + if (configTableRef.current) { + const changedConfigs = configTableRef.current.submit(); + if (changedConfigs.length === 0) { + return toast({ + title: t('dbconfig.no_changes'), + status: 'success' + }); + } + setDifferences(changedConfigs); + onOpen(); + } + }; + + return ( + + + {SubNavList.map((item) => ( + { + updateSubMenu(item.value); + } + })} + > + {t(item.label as I18nCommonKey)} + + ))} + {subMenu === SubMenuEnum.Parameter && hasDifferences && ( + + + + + )} + + + + {subMenu === SubMenuEnum.Parameter && config && ( + + )} + {subMenu === SubMenuEnum.History && db && } + + + + + + {t('dbconfig.prompt')} + + + + {db.dbType === 'mongodb' ? ( + + 1、{t('dbconfig.updates_tip')} + 2、{t('dbconfig.updates_tip2')} + + ) : ( + + {t('dbconfig.updates_tip2')} + + )} + + + + {t('dbconfig.confirm_updates')} + + + + + {t('dbconfig.parameter_name')} + + {t('dbconfig.parameter_value')} + {t('dbconfig.modified_value')} + + {differences.map((diff, index) => ( + + + {diff.path} + + {diff.oldValue} + {diff.newValue} + + ))} + + + + + + + + + + ); +}; + +export default React.memo(forwardRef(ReconfigureTable)); diff --git a/frontend/providers/dbprovider/src/pages/db/detail/index.tsx b/frontend/providers/dbprovider/src/pages/db/detail/index.tsx index e6be30acee4..4be6c133abd 100644 --- a/frontend/providers/dbprovider/src/pages/db/detail/index.tsx +++ b/frontend/providers/dbprovider/src/pages/db/detail/index.tsx @@ -18,13 +18,15 @@ import MigrateTable from './components/Migrate/Table'; import Monitor from './components/Monitor'; import Pods from './components/Pods'; import { I18nCommonKey } from '@/types/i18next'; +import ReconfigureTable from './components/Reconfigure/index'; enum TabEnum { pod = 'pod', backup = 'backup', monitor = 'monitor', InternetMigration = 'InternetMigration', - DumpImport = 'DumpImport' + DumpImport = 'DumpImport', + Reconfigure = 'reconfigure' } const AppDetail = ({ @@ -37,6 +39,7 @@ const AppDetail = ({ listType: `${TabEnum}`; }) => { const BackupTableRef = useRef(null); + const ReconfigureTableRef = useRef(null); const router = useRouter(); const { t } = useTranslation(); const { SystemEnv } = useEnvStore(); @@ -51,6 +54,7 @@ const AppDetail = ({ const listNavValue = [ { label: 'monitor_list', value: TabEnum.monitor }, { label: 'replicas_list', value: TabEnum.pod }, + ...(PublicNetMigration ? [{ label: 'dbconfig.parameter', value: TabEnum.Reconfigure }] : []), ...(BackupSupported ? [{ label: 'backup_list', value: TabEnum.backup }] : []), ...(PublicNetMigration ? [{ label: 'online_import', value: TabEnum.InternetMigration }] : []), ...(PublicNetMigration && !!SystemEnv.minio_url @@ -122,7 +126,7 @@ const AppDetail = ({ border={theme.borders.base} borderRadius={'lg'} > - + {listNav.map((item) => ( } {listType === TabEnum.DumpImport && } + {listType === TabEnum.Reconfigure && ( + + )} diff --git a/frontend/providers/dbprovider/src/pages/db/edit/components/Form.tsx b/frontend/providers/dbprovider/src/pages/db/edit/components/Form.tsx index 005d0a31626..468490f31b3 100644 --- a/frontend/providers/dbprovider/src/pages/db/edit/components/Form.tsx +++ b/frontend/providers/dbprovider/src/pages/db/edit/components/Form.tsx @@ -1,5 +1,6 @@ import { obj2Query } from '@/api/tools'; import MyIcon from '@/components/Icon'; +import PriceBox from '@/components/PriceBox'; import QuotaBox from '@/components/QuotaBox'; import Tip from '@/components/Tip'; import { DBTypeEnum, DBTypeList, RedisHAConfig } from '@/constants/db'; @@ -7,31 +8,32 @@ import { CpuSlideMarkList, MemorySlideMarkList } from '@/constants/editApp'; import { DBVersionMap, INSTALL_ACCOUNT } from '@/store/static'; import type { QueryType } from '@/types'; import type { DBEditType } from '@/types/db'; -import { InfoOutlineIcon, WarningIcon } from '@chakra-ui/icons'; +import { I18nCommonKey } from '@/types/i18next'; +import { InfoOutlineIcon } from '@chakra-ui/icons'; import { Box, + Button, + Center, Flex, - Image, FormControl, Grid, + Image, Input, NumberDecrementStepper, NumberIncrementStepper, NumberInput, NumberInputField, NumberInputStepper, - useTheme, - Center, - Text + Text, + useDisclosure, + useTheme } from '@chakra-ui/react'; -import { MySelect, Tabs, MySlider, RangeInput, MyTooltip } from '@sealos/ui'; +import { MySelect, MySlider, MyTooltip, RangeInput, Tabs } from '@sealos/ui'; import { throttle } from 'lodash'; import { useTranslation } from 'next-i18next'; import { useRouter } from 'next/router'; import { useEffect, useMemo, useState } from 'react'; import { UseFormReturn } from 'react-hook-form'; -import PriceBox from '@/components/PriceBox'; -import { I18nCommonKey } from '@/types/i18next'; const Form = ({ formHook, @@ -313,7 +315,7 @@ const Form = ({
- + import('@/components/ErrorModal')); const defaultEdit = { @@ -40,7 +41,7 @@ const EditApp = ({ dbName, tabType }: { dbName?: string; tabType?: 'form' | 'yam const [minStorage, setMinStorage] = useState(1); const { message: toast } = useMessage(); const { Loading, setIsLoading } = useLoading(); - const { loadDBDetail } = useDBStore(); + const { loadDBDetail, dbDetail } = useDBStore(); const oldDBEditData = useRef(); const { checkQuotaAllow } = useUserStore(); @@ -120,7 +121,6 @@ const EditApp = ({ dbName, tabType }: { dbName?: string; tabType?: 'form' | 'yam }); } await createDB({ dbForm: formData, isEdit }); - toast({ title: t(applySuccess), status: 'success' diff --git a/frontend/providers/dbprovider/src/pages/db/migrate/components/Form.tsx b/frontend/providers/dbprovider/src/pages/db/migrate/components/Form.tsx index 6aab5dc7a4a..1753b818839 100644 --- a/frontend/providers/dbprovider/src/pages/db/migrate/components/Form.tsx +++ b/frontend/providers/dbprovider/src/pages/db/migrate/components/Form.tsx @@ -303,8 +303,8 @@ const Form = ({ - - + +
diff --git a/frontend/providers/dbprovider/src/pages/dbs/components/dbList.tsx b/frontend/providers/dbprovider/src/pages/dbs/components/dbList.tsx index c29cc37453f..2d320cbaba2 100644 --- a/frontend/providers/dbprovider/src/pages/dbs/components/dbList.tsx +++ b/frontend/providers/dbprovider/src/pages/dbs/components/dbList.tsx @@ -107,7 +107,7 @@ const DBList = ({ render?: (item: DBListItemType) => JSX.Element; }[] = [ { - title: t('Name'), + title: t('name'), key: 'name', render: (item: DBListItemType) => { return ( @@ -128,7 +128,7 @@ const DBList = ({ ) }, { - title: t('Status'), + title: t('status'), key: 'status', render: (item: DBListItemType) => ( @@ -172,7 +172,7 @@ const DBList = ({ leftIcon={} onClick={() => router.push(`/db/detail?name=${item.name}&dbType=${item.dbType}`)} > - {t('Details')} + {t('details')} Promise; dbDetail: DBDetailType; - loadDBDetail: (name: string, init?: boolean) => Promise; + loadDBDetail: (name: string, isFetchConfigMap?: boolean) => Promise; dbPods: PodDetailType[]; intervalLoadPods: (dbName: string) => Promise; }; @@ -55,6 +55,7 @@ export const useDBStore = create()( async loadDBDetail(name: string) { try { const res = await getDBByName(name); + if (res.status.value === 'Updating') { const isDiskOverflow = await getDiskOverflowStatus(res.dbName, res.dbType); res.isDiskSpaceOverflow = isDiskOverflow; diff --git a/frontend/providers/dbprovider/src/store/user.ts b/frontend/providers/dbprovider/src/store/user.ts index 4fa85fb4ad8..8dad4ead723 100644 --- a/frontend/providers/dbprovider/src/store/user.ts +++ b/frontend/providers/dbprovider/src/store/user.ts @@ -26,7 +26,10 @@ export const useUserStore = create()( }); return null; }, - checkQuotaAllow: ({ cpu, memory, storage, replicas }, usedData): I18nCommonKey | undefined => { + checkQuotaAllow: ( + { cpu, memory, storage, replicas }, + usedData + ): I18nCommonKey | undefined => { const quote = get().userQuota; const request = { diff --git a/frontend/providers/dbprovider/src/types/cluster.d.ts b/frontend/providers/dbprovider/src/types/cluster.d.ts index 9abe37f80b7..f4f1a851170 100644 --- a/frontend/providers/dbprovider/src/types/cluster.d.ts +++ b/frontend/providers/dbprovider/src/types/cluster.d.ts @@ -1,4 +1,4 @@ -import { DBStatusEnum, DBTypeEnum, PodStatusEnum } from '@/constants/db'; +import { DBStatusEnum, DBTypeEnum, PodStatusEnum, ReconfigStatus } from '@/constants/db'; export type KbPgClusterType = { apiVersion: 'apps.kubeblocks.io/v1alpha1'; @@ -105,3 +105,51 @@ export type KubeBlockBackupPolicyType = { }; }; }; + +export type KubeBlockOpsRequestType = { + apiVersion: string; + kind: string; + metadata: { + creationTimestamp: Date; + generation: number; + labels: { [key: string]: string }; + name: string; + namespace: string; + uid: string; + annotations: { + [key: string]: string; + }; + }; + spec: { + clusterRef: string; + reconfigure: { + componentName: string; + configurations: { + keys: { + key: string; + parameters: { + key: string; + value: string; + }[]; + }[]; + name: string; + }[]; + }; + ttlSecondsBeforeAbort: number; + type: string; + }; + status: { + clusterGeneration: number; + completionTimestamp: string; + conditions: { + lastTransitionTime: string; + message: string; + reason: string; + status: string; + type: string; + }[]; + phase: `${ReconfigStatus}`; + progress: string; + startTimestamp: string; + }; +}; diff --git a/frontend/providers/dbprovider/src/types/db.d.ts b/frontend/providers/dbprovider/src/types/db.d.ts index 71e1fa1e8a0..5946b8c4eb3 100644 --- a/frontend/providers/dbprovider/src/types/db.d.ts +++ b/frontend/providers/dbprovider/src/types/db.d.ts @@ -12,14 +12,17 @@ import type { V1StatefulSet, V1ContainerStatus } from '@kubernetes/client-node'; +import { I18nCommonKey } from './i18next'; export type DBType = `${DBTypeEnum}`; export type SupportMigrationDBType = Extract; -export type SupportConnectDBType = Extract< +export type SupportConnectDBType = Extract; + +export type SupportReconfigureDBType = Extract< DBType, - 'postgresql' | 'mongodb' | 'apecloud-mysql' | 'mongodb' + 'postgresql' | 'mongodb' | 'apecloud-mysql' | 'redis' >; export type DeployKindsType = @@ -121,3 +124,18 @@ export interface BackupItemType { namespace: string; connectionPassword: string; } + +export type ReconfigStatusMapType = { + label: I18nCommonKey; + value: ReconfigStatus; + color: string; +}; + +export interface OpsRequestItemType { + id: string; + name: string; + status: ReconfigStatusMapType; + startTime: Date; + namespace: string; + configurations: { parameterName: string; newValue: string; oldValue?: string }[]; +} diff --git a/frontend/providers/dbprovider/src/utils/adapt.ts b/frontend/providers/dbprovider/src/utils/adapt.ts index 6659997152d..69f64a8fa90 100644 --- a/frontend/providers/dbprovider/src/utils/adapt.ts +++ b/frontend/providers/dbprovider/src/utils/adapt.ts @@ -1,8 +1,28 @@ import { BACKUP_REMARK_LABEL_KEY, BackupTypeEnum, backupStatusMap } from '@/constants/backup'; -import { DBStatusEnum, MigrationRemark, dbStatusMap } from '@/constants/db'; +import { + DBReconfigStatusMap, + DBStatusEnum, + DBReconfigureMap, + MigrationRemark, + dbStatusMap, + DBReconfigureKey, + DBPreviousConfigKey +} from '@/constants/db'; import type { AutoBackupFormType, BackupCRItemType } from '@/types/backup'; -import type { KbPgClusterType, KubeBlockBackupPolicyType } from '@/types/cluster'; -import type { DBDetailType, DBEditType, DBListItemType, PodDetailType, PodEvent } from '@/types/db'; +import type { + KbPgClusterType, + KubeBlockBackupPolicyType, + KubeBlockOpsRequestType +} from '@/types/cluster'; +import type { + DBDetailType, + DBEditType, + DBListItemType, + DBType, + OpsRequestItemType, + PodDetailType, + PodEvent +} from '@/types/db'; import { InternetMigrationCR, MigrateItemType } from '@/types/migrate'; import { convertCronTime, @@ -253,3 +273,42 @@ export const adaptMigrateList = (item: InternetMigrationCR): MigrateItemType => remark: item.metadata.labels[MigrationRemark] || '-' }; }; + +export const adaptOpsRequest = ( + item: KubeBlockOpsRequestType, + dbType: DBType +): OpsRequestItemType => { + const config = item.metadata.annotations?.[DBPreviousConfigKey]; + + let previousConfigurations: { + [key: string]: string; + } = {}; + + if (config) { + try { + const confObject = JSON.parse(config); + Object.entries(confObject).forEach(([key, value]) => { + previousConfigurations[key] = + typeof value === 'string' ? value.replace(/^['"](.*)['"]$/, '$1') : String(value); + }); + } catch (error) { + console.error('Error parsing postgresql.conf annotation:', error); + } + } + + return { + id: item.metadata.uid, + name: item.metadata.name, + namespace: item.metadata.namespace, + status: + item.status?.phase && DBReconfigStatusMap[item.status.phase] + ? DBReconfigStatusMap[item.status.phase] + : DBReconfigStatusMap.Creating, + startTime: item.metadata?.creationTimestamp, + configurations: item.spec.reconfigure.configurations[0].keys[0].parameters.map((param) => ({ + parameterName: param.key, + newValue: param.value, + oldValue: previousConfigurations[param.key] + })) + }; +}; diff --git a/frontend/providers/dbprovider/src/utils/json2Yaml.ts b/frontend/providers/dbprovider/src/utils/json2Yaml.ts index 50d8e6d81c0..905d7d00f48 100644 --- a/frontend/providers/dbprovider/src/utils/json2Yaml.ts +++ b/frontend/providers/dbprovider/src/utils/json2Yaml.ts @@ -2,6 +2,8 @@ import { BACKUP_LABEL_KEY, BACKUP_REMARK_LABEL_KEY } from '@/constants/backup'; import { CloudMigraionLabel, DBComponentNameMap, + DBPreviousConfigKey, + DBReconfigureMap, DBTypeEnum, MigrationRemark, RedisHAConfig, @@ -15,6 +17,8 @@ import dayjs from 'dayjs'; import yaml from 'js-yaml'; import { getUserNamespace } from './user'; import { V1StatefulSet } from '@kubernetes/client-node'; +import { customAlphabet } from 'nanoid'; +const nanoid = customAlphabet('abcdefghijklmnopqrstuvwxyz', 5); /** * Convert data for creating a database cluster to YAML configuration. @@ -1097,3 +1101,63 @@ export const json2NetworkService = ({ return yaml.dump(template); }; + +export const json2Reconfigure = ( + dbName: string, + dbType: DBType, + dbUid: string, + configParams: { path: string; newValue: string; oldValue: string }[] +) => { + const namespace = getUserNamespace(); + const template = { + apiVersion: 'apps.kubeblocks.io/v1alpha1', + kind: 'OpsRequest', + metadata: { + finalizers: ['opsrequest.kubeblocks.io/finalizer'], + generateName: `${dbName}-reconfiguring-`, + generation: 2, + labels: { + 'app.kubernetes.io/instance': dbName, + 'app.kubernetes.io/managed-by': 'kubeblocks', + 'ops.kubeblocks.io/ops-type': 'Reconfiguring', + ...configParams.reduce((acc, param) => ({ ...acc, [param.path]: param.newValue }), {}) + }, + annotations: { + [DBPreviousConfigKey]: JSON.stringify( + configParams.reduce((acc, param) => ({ ...acc, [param.path]: param.oldValue }), {}) + ) + }, + name: `${dbName}-reconfiguring-${nanoid()}`, + namespace: namespace, + ownerReferences: [ + { + apiVersion: 'apps.kubeblocks.io/v1alpha1', + kind: 'Cluster', + name: dbName, + uid: dbUid + } + ] + }, + spec: { + clusterRef: dbName, + reconfigure: { + componentName: dbType === 'apecloud-mysql' ? 'mysql' : dbType, + configurations: [ + { + keys: [ + { + key: DBReconfigureMap[dbType].reconfigureKey, + parameters: configParams.map((item) => ({ key: item.path, value: item.newValue })) + } + ], + name: DBReconfigureMap[dbType].reconfigureName + } + ] + }, + ttlSecondsBeforeAbort: 0, + type: 'Reconfiguring' + } + }; + + return yaml.dump(template); +}; diff --git a/frontend/providers/dbprovider/src/utils/tools.ts b/frontend/providers/dbprovider/src/utils/tools.ts index 11fbdb44549..74a0758e649 100644 --- a/frontend/providers/dbprovider/src/utils/tools.ts +++ b/frontend/providers/dbprovider/src/utils/tools.ts @@ -3,6 +3,9 @@ import { useMessage } from '@sealos/ui'; import { addHours, format, set, startOfDay } from 'date-fns'; import dayjs from 'dayjs'; import { useTranslation } from 'next-i18next'; +import yaml from 'js-yaml'; +import ini from 'ini'; +import { DBType } from '@/types/db'; export const formatTime = (time: string | number | Date, format = 'YYYY-MM-DD HH:mm:ss') => { return dayjs(time).format(format); @@ -299,3 +302,53 @@ export function decodeFromHex(encoded: string) { const decoded = Buffer.from(encoded, 'hex').toString('utf-8'); return decoded; } + +export const parseConfig = ({ + type, + configString +}: { + type: 'ini' | 'yaml'; + configString: string; +}): Object => { + if (type === 'ini') { + return ini.parse(configString); + } else if (type === 'yaml') { + return yaml.load(configString) as Object; + } else { + throw new Error(`Unsupported config type: ${type}`); + } +}; + +export const flattenObject = (ob: any, prefix: string = ''): { key: string; value: string }[] => { + const result: { key: string; value: string }[] = []; + + for (const i in ob) { + const key = prefix ? `${prefix}.${i}` : i; + if (typeof ob[i] === 'object' && ob[i] !== null) { + result.push(...flattenObject(ob[i], key)); + } else { + result.push({ key, value: String(ob[i]) }); + } + } + + return result; +}; + +export const adjustDifferencesForIni = ( + differences: { path: string; oldValue: any; newValue: any }[], + type: 'ini' | 'yaml', + dbType: DBType +): { path: string; newValue: string; oldValue: string }[] => { + if (type !== 'ini' || dbType === 'postgresql') { + return differences; + } + return differences.map((diff) => { + const pathParts = diff.path.split('.'); + const adjustedPath = pathParts.slice(1).join('.'); + return { + path: adjustedPath, + newValue: diff.newValue, + oldValue: diff.oldValue + }; + }); +};