diff --git a/frontend/desktop/src/components/app_window/index.module.scss b/frontend/desktop/src/components/app_window/index.module.scss index 36567dbc1a8..c51b561984d 100644 --- a/frontend/desktop/src/components/app_window/index.module.scss +++ b/frontend/desktop/src/components/app_window/index.module.scss @@ -34,15 +34,15 @@ &[data-size='maxmin'] { top: 10%; - left: 20%; - width: 60%; + left: 15%; + width: 70%; height: 80%; } &[data-size='minimize'] { top: 10%; - left: 20%; - width: 60%; + left: 15%; + width: 70%; height: 80%; transform: scale(0.1) translate(0, 0) !important; transform-origin: bottom; diff --git a/frontend/desktop/src/components/app_window/index.tsx b/frontend/desktop/src/components/app_window/index.tsx index ce85d904186..fa95621b543 100644 --- a/frontend/desktop/src/components/app_window/index.tsx +++ b/frontend/desktop/src/components/app_window/index.tsx @@ -47,10 +47,10 @@ export default function AppWindow(props: { setPosition({ x: x < 0 - ? x < -1.1 * appHeaderWidth // (0.8width + width/0.6*0.2) + ? x < -0.9 * appHeaderWidth // (0.8width + width/0.70*0.15) ? 0 : x - : x > 1.1 * appHeaderWidth + : x > 0.9 * appHeaderWidth ? 0 : x, y: y < upperBoundary ? upperBoundary : y > lowerBoundary ? 0 : y @@ -92,7 +92,7 @@ export default function AppWindow(props: { { diff --git a/frontend/providers/applaunchpad/src/utils/adapt.ts b/frontend/providers/applaunchpad/src/utils/adapt.ts index fe7692462c9..24e3e83d52f 100644 --- a/frontend/providers/applaunchpad/src/utils/adapt.ts +++ b/frontend/providers/applaunchpad/src/utils/adapt.ts @@ -126,16 +126,16 @@ export const adaptPod = (pod: V1Pod): PodDetailType => { if (container.length > 0) { const stateObj = container[0].state; if (stateObj) { - const stateKeys = Object.keys(stateObj); - const key = stateKeys[0] as `${PodStatusEnum}`; - if (key === PodStatusEnum.running) { - return podStatusMap[PodStatusEnum.running]; - } - if (key && podStatusMap[key]) { - return { - ...podStatusMap[key], - ...stateObj[key] - }; + const status = [ + PodStatusEnum.running, + PodStatusEnum.terminated, + PodStatusEnum.waiting + ].find((s) => stateObj[s]); + + if (status) { + return status === PodStatusEnum.running + ? podStatusMap[PodStatusEnum.running] + : { ...podStatusMap[status], ...stateObj[status] }; } } } @@ -146,13 +146,16 @@ export const adaptPod = (pod: V1Pod): PodDetailType => { if (container.length > 0) { const lastStateObj = container[0].lastState; if (lastStateObj) { - const lastStateKeys = Object.keys(lastStateObj); - const key = lastStateKeys[0] as `${PodStatusEnum}`; - if (key && podStatusMap[key]) { - return { - ...podStatusMap[key], - ...lastStateObj[key] - }; + const status = [ + PodStatusEnum.running, + PodStatusEnum.terminated, + PodStatusEnum.waiting + ].find((s) => lastStateObj[s]); + + if (status) { + return status === PodStatusEnum.running + ? podStatusMap[PodStatusEnum.running] + : { ...podStatusMap[status], ...lastStateObj[status] }; } } } diff --git a/frontend/providers/dbprovider/public/locales/en/common.json b/frontend/providers/dbprovider/public/locales/en/common.json index 373942dc3d8..68ace4114aa 100644 --- a/frontend/providers/dbprovider/public/locales/en/common.json +++ b/frontend/providers/dbprovider/public/locales/en/common.json @@ -7,8 +7,8 @@ "Continue": "Continue", "Creating": "Creating", "CronExpression": "Cycle Interval", - "DBList": "DataBases", - "DataBase": "DataBase", + "DBList": "Database", + "DataBase": "Database", "Day": "Day", "Delete": "Delete", "Deleting": "Deleting...", @@ -66,16 +66,18 @@ "storage_exceeds_quota": "Storage requested exceeds quota. Contact admin." }, "app_store": "App Store", - "application_source": "Application Source", + "application_source": "Source", "are_you_sure_to_perform_database_migration": "Confirm database migration?", "are_you_sure_you_want_to_turn_off_automatic_backup": "Confirm disabling auto backup?", "auto_backup": "Automated Backup", "automatic_backup_is_turned_off": "Auto backup disabled", + "backup_center": "Backup Center", + "backup_center_search_tip": "Search backup name, notes", "backup_completed": "Backup completed", "backup_database": "Backup Database", "backup_deleting": "Purging Backup", "backup_failed": "Backup Failed", - "backup_list": "Data Backups", + "backup_list": "Backups", "backup_name": "Backup Name", "backup_name_cannot_empty": "Must provide backup name", "backup_processing": "Saving Backup", @@ -121,6 +123,7 @@ "create_db": "Create Database", "creation_time": "Creation Time", "current_connections": "Current Connections", + "data_import": "Data Import", "data_migration_config": "Data Migration Settings", "database_config": "parameters", "database_edit_config": "Update Parameters", @@ -142,7 +145,7 @@ "db_name": "DataBase Name", "db_table": "DataBase Table", "dbconfig": { - "change_history": "Modification history", + "change_history": "Modification History", "commit": "submit", "confirm_updates": "Please confirm the parameters you modified:", "get_config_err": "Failed to obtain configuration file", @@ -150,7 +153,7 @@ "modify_time": "Modify Time", "no_changes": "No modification yet", "original_value": "Original value", - "parameter": "Parameter Config", + "parameter": "Parameters", "parameter_name": "parameter name", "parameter_value": "Parameter value", "prompt": "Change notice", @@ -161,6 +164,8 @@ }, "delete_anyway": "Force Delete", "delete_backup": "Delete Backup", + "delete_backup_with_db": "Keep Backups", + "delete_backup_with_db_tip": "Delete the databases but leave the backups as they are", "delete_failed": "Failed to delete", "delete_hint": "Warning: This will permanently delete all data in the database. Confirm to proceed.", "delete_sealaf_app_tip": "The database is deployed via cloud development. \nSimply deleting the database does not clear out all components of your app, which may still incur charges. \nTo completely uninstall the app and clean all related components, uninstall the entire app in cloud development.", @@ -178,7 +183,7 @@ "enable_external_network_access": "Allow public network access", "enter_save": "Press Enter to save. 'All' exports the entire database.", "error_log": { - "analysis": "Log Analysis", + "analysis": "Logs", "collection_time": "Collection Time", "content": "Information", "error_log": "Error Log", @@ -204,7 +209,7 @@ "have_error": "Failed", "hits_ratio": "Hits Ratio", "hour": "hour", - "import_through_file": "Import via File", + "import_through_file": "File Import", "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", "intranet_address": "Private Address", @@ -215,7 +220,7 @@ "limit_cpu": "CPU Limit", "limit_memory": "Memory Limit", "lost_file": "File Missing", - "manage_all_resources": "Manage all resources", + "manage_all_resources": "Manage", "manual_backup": "Manual Backup", "manual_backup_tip": "Tip: Backup during off-peak hours. Avoid DDL operations to prevent locking. Be patient if data size is large. Backup starts 1 min after confirmation.", "max_replicas": "Max Replicas: ", @@ -255,8 +260,9 @@ "no_data_available": "No Data Available", "no_logs_for_now": "No logs for now", "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", + "online_import": "Online Import", "operation": "Operation", + "overview": "Overview", "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?", @@ -318,13 +324,11 @@ "upload_dump_file": "Upload Dump File", "use_docs": "Documentation", "version": "Version", + "wipeout_backup_with_db": "Discard Backups", + "wipeout_backup_with_db_tip": "Delete the databases and the backups", "within_1_day": "Within 1 day", "within_1_hour": "Within 1 hour", "within_5_minutes": "Within 5 minutes", "yaml_file": "YAML", - "you_have_successfully_deployed_database": "You have successfully deployed and created a database!", - "delete_backup_with_db": "Keep Backups", - "delete_backup_with_db_tip": "Delete the databases but leave the backups as they are", - "wipeout_backup_with_db": "Discard Backups", - "wipeout_backup_with_db_tip": "Delete the databases and the backups" + "you_have_successfully_deployed_database": "You have successfully deployed and created a database!" } \ No newline at end of file diff --git a/frontend/providers/dbprovider/public/locales/zh/common.json b/frontend/providers/dbprovider/public/locales/zh/common.json index 7b6056a88cc..84c99ede990 100644 --- a/frontend/providers/dbprovider/public/locales/zh/common.json +++ b/frontend/providers/dbprovider/public/locales/zh/common.json @@ -66,11 +66,13 @@ "storage_exceeds_quota": "申请的 '存储' 超出限制,请联系管理员" }, "app_store": "应用商店", - "application_source": "应用来源", + "application_source": "来源", "are_you_sure_to_perform_database_migration": "确定执行数据库迁移吗?", "are_you_sure_you_want_to_turn_off_automatic_backup": "确定关闭自动备份吗", "auto_backup": "自动备份", "automatic_backup_is_turned_off": "已关闭自动备份", + "backup_center": "备份中心", + "backup_center_search_tip": "搜索备份名称、备注", "backup_completed": "备份成功", "backup_database": "备份数据库", "backup_deleting": "删除中", @@ -84,7 +86,7 @@ "backup_success_tip": "备份任务已经成功创建", "backup_time": "备份时间", "balance": "余额", - "basic": "基础配置", + "basic": "基础信息", "billing_standards": "计费标准", "block_read_time": "读数据块时间", "block_write_time": "写数据块时间", @@ -118,9 +120,10 @@ "copy_success": "复制成功", "covering_risks": "覆盖风险", "cpu": "CPU", - "create_db": "新建数据库", + "create_db": "新建", "creation_time": "创建时间", "current_connections": "当前连接数", + "data_import": "数据导入", "data_migration_config": "数据迁移配置", "database_config": "数据库参数", "database_edit_config": "变更参数", @@ -161,6 +164,8 @@ }, "delete_anyway": "仍要删除", "delete_backup": "删除备份", + "delete_backup_with_db": "保留备份", + "delete_backup_with_db_tip": "在删除数据库时,保留其备份", "delete_failed": "删除出现意外", "delete_hint": "如果确认要删除这个数据库吗?如果执行此操作,将删除该数据库的所有数据。", "delete_sealaf_app_tip": "该数据库是通过云开发部署的。仅删除数据库无法清除应用的所有组件,这些组件可能仍会产生费用。要彻底卸载应用并清理所有相关组件,请在云开发中卸载整个应用。", @@ -258,6 +263,7 @@ "not_allow_standalone_use": "该应用不允许单独使用,点击确认前往 Sealos Desktop 使用。", "online_import": "在线导入", "operation": "操作", + "overview": "概览", "page_faults": "页错误", "pause_error": "数据库暂停失败", "pause_hint": "暂停服务将停止计算 CPU 和内存等费用,但存储和外网端口仍将产生费用。是否现在暂停?", @@ -319,13 +325,11 @@ "upload_dump_file": "点击上传 Dump 文件", "use_docs": "使用文档", "version": "版本", + "wipeout_backup_with_db": "随数据库删除", + "wipeout_backup_with_db_tip": "在删除数据库时,删除其备份", "within_1_day": "一天内", "within_1_hour": "一小时内", "within_5_minutes": "五分钟内", "yaml_file": "YAML 文件", - "you_have_successfully_deployed_database": "您已成功部署创建一个数据库!", - "delete_backup_with_db": "保留备份", - "delete_backup_with_db_tip": "在删除数据库时,保留其备份", - "wipeout_backup_with_db": "随数据库删除", - "wipeout_backup_with_db_tip": "在删除数据库时,删除其备份" -} \ No newline at end of file + "you_have_successfully_deployed_database": "您已成功部署创建一个数据库!" +} diff --git a/frontend/providers/dbprovider/src/api/backup.ts b/frontend/providers/dbprovider/src/api/backup.ts index 70b8f14d151..79bdda6b51b 100644 --- a/frontend/providers/dbprovider/src/api/backup.ts +++ b/frontend/providers/dbprovider/src/api/backup.ts @@ -3,12 +3,15 @@ import type { Props as CreateBackupPros } from '@/pages/api/backup/create'; import { adaptBackup, adaptBackupByCluster, adaptDBDetail } from '@/utils/adapt'; import { AutoBackupFormType } from '@/types/backup'; import type { Props as UpdatePolicyProps } from '@/pages/api/backup/updatePolicy'; +import { BackupItemType } from '@/types/db'; export const createBackup = (data: CreateBackupPros) => POST('/api/backup/create', data); export const getBackupList = (dbName: string) => GET('/api/backup/getBackupList', { dbName }).then((res) => res.map(adaptBackup)); +export const getBackups = () => GET('/api/backup/getBackups'); + export const deleteBackup = (backupName: string) => DELETE(`/api/backup/delBackup?backupName=${backupName}`); diff --git a/frontend/providers/dbprovider/src/components/BaseTable/baseTable.tsx b/frontend/providers/dbprovider/src/components/BaseTable/baseTable.tsx index fc3832a73f4..ab68edaf25d 100644 --- a/frontend/providers/dbprovider/src/components/BaseTable/baseTable.tsx +++ b/frontend/providers/dbprovider/src/components/BaseTable/baseTable.tsx @@ -1,4 +1,5 @@ import { + HTMLChakraProps, Spinner, Table, TableContainer, @@ -9,17 +10,34 @@ import { Thead, Tr } from '@chakra-ui/react'; -import { Table as ReactTable, flexRender } from '@tanstack/react-table'; +import { Column, Table as ReactTable, flexRender } from '@tanstack/react-table'; +import { CSSProperties } from 'react'; + +const getCommonPinningStyles = (column: Column): CSSProperties => { + const isPinned = column.getIsPinned(); + + return { + position: isPinned ? 'sticky' : 'relative', + left: isPinned === 'left' ? 0 : undefined, + right: isPinned === 'right' ? 0 : undefined, + zIndex: isPinned ? 10 : 0 + }; +}; export function BaseTable({ table, isLoading, + tdStyle, ...props -}: { table: ReactTable; isLoading: boolean } & TableContainerProps) { +}: { + table: ReactTable; + isLoading: boolean; + tdStyle?: HTMLChakraProps<'td'>; +} & TableContainerProps) { return ( - + {table.getHeaderGroups().map((headers) => { return ( @@ -30,9 +48,16 @@ export function BaseTable({ py="13px" px={'24px'} key={header.id} - backgroundColor={'grayModern.50'} + bg={'grayModern.100'} color={'grayModern.600'} border={'none'} + _first={{ + borderLeftRadius: '6px' + }} + _last={{ + borderRightRadius: '6px' + }} + {...(getCommonPinningStyles(header.column) as HTMLChakraProps<'th'>)} > {flexRender(header.column.columnDef.header, header.getContext())} @@ -50,17 +75,29 @@ export function BaseTable({ ) : ( - table.getRowModel().rows.map((item) => { + table.getRowModel().rows.map((item, index) => { return ( - + {item.getAllCells().map((cell, i) => { + const isPinned = cell.column.getIsPinned(); return ( - ); diff --git a/frontend/providers/dbprovider/src/components/BaseTable/customMenu.tsx b/frontend/providers/dbprovider/src/components/BaseTable/customMenu.tsx new file mode 100644 index 00000000000..7a2d799efee --- /dev/null +++ b/frontend/providers/dbprovider/src/components/BaseTable/customMenu.tsx @@ -0,0 +1,97 @@ +import React, { useState, useRef, useEffect } from 'react'; +import { Box, Portal } from '@chakra-ui/react'; + +interface MenuItemProps { + isActive?: boolean; + child: React.ReactNode; + onClick: () => void; + menuItemStyle?: any; + isDisabled?: boolean; +} + +interface CustomMenuProps { + width: number; + Button: React.ReactNode; + menuList: MenuItemProps[]; +} + +export const CustomMenu = ({ width, Button, menuList }: CustomMenuProps) => { + const [isOpen, setIsOpen] = useState(false); + const buttonRef = useRef(null); + const menuRef = useRef(null); + + useEffect(() => { + const handleClickOutside = (event: MouseEvent) => { + if ( + menuRef.current && + buttonRef.current && + !menuRef.current.contains(event.target as Node) && + !buttonRef.current.contains(event.target as Node) + ) { + setIsOpen(false); + } + }; + + document.addEventListener('mousedown', handleClickOutside); + return () => document.removeEventListener('mousedown', handleClickOutside); + }, []); + + const handleItemClick = (item: MenuItemProps) => { + if (!item.isDisabled) { + item.onClick(); + setIsOpen(false); + } + }; + + const defaultMenuItemStyles = { + borderRadius: '4px', + display: 'flex', + alignItems: 'center', + cursor: 'pointer', + py: '6px', + px: '4px', + _hover: { + backgroundColor: 'rgba(17, 24, 36, 0.05)', + color: 'brightBlue.600' + } + }; + + return ( + + setIsOpen(!isOpen)}>{Button} + + {isOpen && ( + + + {menuList.map((item, i) => ( + handleItemClick(item)} + color={item.isActive ? 'hover.blue' : 'grayModern.600'} + opacity={item.isDisabled ? 0.5 : 1} + {...defaultMenuItemStyles} + {...item.menuItemStyle} + > + {item.child} + + ))} + + + )} + + ); +}; diff --git a/frontend/providers/dbprovider/src/components/DBStatusTag/index.tsx b/frontend/providers/dbprovider/src/components/DBStatusTag/index.tsx index c1e5ee9a957..5cae0e46843 100644 --- a/frontend/providers/dbprovider/src/components/DBStatusTag/index.tsx +++ b/frontend/providers/dbprovider/src/components/DBStatusTag/index.tsx @@ -43,6 +43,7 @@ const DBStatusTag = ({ fontWeight={'bold'} alignItems={'center'} minW={'88px'} + maxW={'124px'} whiteSpace={'nowrap'} > diff --git a/frontend/providers/dbprovider/src/components/Icon/icons/analyze.svg b/frontend/providers/dbprovider/src/components/Icon/icons/analyze.svg index 50317864733..30ce76ff4ff 100644 --- a/frontend/providers/dbprovider/src/components/Icon/icons/analyze.svg +++ b/frontend/providers/dbprovider/src/components/Icon/icons/analyze.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/frontend/providers/dbprovider/src/components/Icon/icons/arrowLeft.svg b/frontend/providers/dbprovider/src/components/Icon/icons/arrowLeft.svg index 9e246b28759..13e1ec9d955 100644 --- a/frontend/providers/dbprovider/src/components/Icon/icons/arrowLeft.svg +++ b/frontend/providers/dbprovider/src/components/Icon/icons/arrowLeft.svg @@ -1,10 +1,3 @@ - - - - - - - - - + + \ No newline at end of file diff --git a/frontend/providers/dbprovider/src/components/Icon/icons/backup.svg b/frontend/providers/dbprovider/src/components/Icon/icons/backup.svg index 6bc2be8a53e..74220341b48 100644 --- a/frontend/providers/dbprovider/src/components/Icon/icons/backup.svg +++ b/frontend/providers/dbprovider/src/components/Icon/icons/backup.svg @@ -1,4 +1,4 @@ - - - + + + diff --git a/frontend/providers/dbprovider/src/components/Icon/icons/chevron-down.svg b/frontend/providers/dbprovider/src/components/Icon/icons/chevron-down.svg new file mode 100644 index 00000000000..5840e8c8c32 --- /dev/null +++ b/frontend/providers/dbprovider/src/components/Icon/icons/chevron-down.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/frontend/providers/dbprovider/src/components/Icon/icons/config.svg b/frontend/providers/dbprovider/src/components/Icon/icons/config.svg index aff15fbd251..37bc601a2f3 100644 --- a/frontend/providers/dbprovider/src/components/Icon/icons/config.svg +++ b/frontend/providers/dbprovider/src/components/Icon/icons/config.svg @@ -1,3 +1,3 @@ - - + + \ No newline at end of file diff --git a/frontend/providers/dbprovider/src/components/Icon/icons/copy.svg b/frontend/providers/dbprovider/src/components/Icon/icons/copy.svg index bfd38df8ede..f96a4564dcf 100644 --- a/frontend/providers/dbprovider/src/components/Icon/icons/copy.svg +++ b/frontend/providers/dbprovider/src/components/Icon/icons/copy.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/frontend/providers/dbprovider/src/components/Icon/icons/file.svg b/frontend/providers/dbprovider/src/components/Icon/icons/file.svg index e24054c2dfc..b69a4ae8706 100644 --- a/frontend/providers/dbprovider/src/components/Icon/icons/file.svg +++ b/frontend/providers/dbprovider/src/components/Icon/icons/file.svg @@ -1,7 +1,7 @@ - + - - + + diff --git a/frontend/providers/dbprovider/src/components/Icon/icons/import.svg b/frontend/providers/dbprovider/src/components/Icon/icons/import.svg index d57c59dccd2..890306b981c 100644 --- a/frontend/providers/dbprovider/src/components/Icon/icons/import.svg +++ b/frontend/providers/dbprovider/src/components/Icon/icons/import.svg @@ -1,4 +1,4 @@ - - - + + + \ No newline at end of file diff --git a/frontend/providers/dbprovider/src/components/Icon/icons/instance.svg b/frontend/providers/dbprovider/src/components/Icon/icons/instance.svg index 864e27c4cc4..531b58db3e5 100644 --- a/frontend/providers/dbprovider/src/components/Icon/icons/instance.svg +++ b/frontend/providers/dbprovider/src/components/Icon/icons/instance.svg @@ -1,6 +1,6 @@ - - - - - + + + + + \ No newline at end of file diff --git a/frontend/providers/dbprovider/src/components/Icon/icons/logo-linear.svg b/frontend/providers/dbprovider/src/components/Icon/icons/logo-linear.svg new file mode 100644 index 00000000000..1e7680f2129 --- /dev/null +++ b/frontend/providers/dbprovider/src/components/Icon/icons/logo-linear.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/frontend/providers/dbprovider/src/components/Icon/icons/logo.svg b/frontend/providers/dbprovider/src/components/Icon/icons/logo.svg index 9747eb120b2..81eb39fdde1 100644 --- a/frontend/providers/dbprovider/src/components/Icon/icons/logo.svg +++ b/frontend/providers/dbprovider/src/components/Icon/icons/logo.svg @@ -1,5 +1,5 @@ - - - - + + + + \ No newline at end of file diff --git a/frontend/providers/dbprovider/src/components/Icon/icons/monitor.svg b/frontend/providers/dbprovider/src/components/Icon/icons/monitor.svg index 185a68a8b10..44aaf7e7e41 100644 --- a/frontend/providers/dbprovider/src/components/Icon/icons/monitor.svg +++ b/frontend/providers/dbprovider/src/components/Icon/icons/monitor.svg @@ -1,3 +1,3 @@ - - + + \ No newline at end of file diff --git a/frontend/providers/dbprovider/src/components/Icon/icons/overview.svg b/frontend/providers/dbprovider/src/components/Icon/icons/overview.svg new file mode 100644 index 00000000000..3ff70d36016 --- /dev/null +++ b/frontend/providers/dbprovider/src/components/Icon/icons/overview.svg @@ -0,0 +1,6 @@ + + + + + + \ 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 b4b35735a65..12acf066437 100644 --- a/frontend/providers/dbprovider/src/components/Icon/index.tsx +++ b/frontend/providers/dbprovider/src/components/Icon/index.tsx @@ -2,7 +2,7 @@ import React from 'react'; import type { IconProps } from '@chakra-ui/react'; import { Icon } from '@chakra-ui/react'; -const map = { +export const IconMap = { more: require('./icons/more.svg').default, podList: require('./icons/podList.svg').default, arrowLeft: require('./icons/arrowLeft.svg').default, @@ -55,8 +55,11 @@ const map = { config: require('./icons/config.svg').default, backupSettings: require('./icons/backupSettings.svg').default, monitor: require('./icons/monitor.svg').default, + logoLinear: require('./icons/logo-linear.svg').default, arrowDown: require('./icons/arrowDown.svg').default, - docs: require('./icons/docs.svg').default + docs: require('./icons/docs.svg').default, + chevronDown: require('./icons/chevron-down.svg').default, + overview: require('./icons/overview.svg').default }; const MyIcon = ({ @@ -64,9 +67,16 @@ const MyIcon = ({ w = 'auto', h = 'auto', ...props -}: { name: keyof typeof map } & IconProps) => { - return map[name] ? ( - +}: { name: keyof typeof IconMap } & IconProps) => { + return IconMap[name] ? ( + ) : null; }; diff --git a/frontend/providers/dbprovider/src/components/Sidebar/index.tsx b/frontend/providers/dbprovider/src/components/Sidebar/index.tsx new file mode 100644 index 00000000000..5facd2570ea --- /dev/null +++ b/frontend/providers/dbprovider/src/components/Sidebar/index.tsx @@ -0,0 +1,60 @@ +import { Center, Text, Stack } from '@chakra-ui/react'; +import MyIcon from '../Icon'; + +import { useTranslation } from 'next-i18next'; +import { useRouter } from 'next/router'; + +export default function Sidebar() { + const { t } = useTranslation(); + const router = useRouter(); + + const siderbarMap = [ + { + label: t('DataBase'), + icon: ( + + ), + path: '/dbs' + }, + { + label: t('Backup'), + icon: ( + + ), + path: '/backups' + } + ]; + + return ( + + {siderbarMap.map((item) => ( +
router.replace(item.path)} + > + {item.icon} + + {item.label} + +
+ ))} + + ); +} diff --git a/frontend/providers/dbprovider/src/constants/db.ts b/frontend/providers/dbprovider/src/constants/db.ts index aa1a527c74b..22d8849f550 100644 --- a/frontend/providers/dbprovider/src/constants/db.ts +++ b/frontend/providers/dbprovider/src/constants/db.ts @@ -20,6 +20,8 @@ export const templateDeployKey = 'cloud.sealos.io/deploy-on-sealos'; export const sealafDeployKey = 'sealaf-app'; export const DBReconfigureKey = 'ops.kubeblocks.io/ops-type=Reconfiguring'; +export const DBNameLabel = 'app.kubernetes.io/instance'; + export enum DBTypeEnum { postgresql = 'postgresql', mongodb = 'mongodb', @@ -196,7 +198,7 @@ export const podStatusMap = { [PodStatusEnum.running]: { label: 'running', value: PodStatusEnum.running, - bg: '#47C8BF' + bg: '#6CD99F' }, [PodStatusEnum.waiting]: { label: 'waiting', @@ -215,12 +217,12 @@ export const maxReplicasKey = 'deploy.cloud.sealos.io/maxReplicas'; export const minReplicasKey = 'deploy.cloud.sealos.io/minReplicas'; export const DBTypeList = [ - { id: DBTypeEnum.postgresql, label: 'postgres' }, - { id: DBTypeEnum.mongodb, label: 'mongo' }, - { id: DBTypeEnum.mysql, label: 'mysql' }, - { id: DBTypeEnum.redis, label: 'redis' }, - { id: DBTypeEnum.kafka, label: 'kafka' }, - { id: DBTypeEnum.milvus, label: 'milvus' } + { id: DBTypeEnum.postgresql, label: 'PostgreSQL' }, + { id: DBTypeEnum.mongodb, label: 'MongoDB' }, + { id: DBTypeEnum.mysql, label: 'MySQL' }, + { id: DBTypeEnum.redis, label: 'Redis' }, + { id: DBTypeEnum.kafka, label: 'Kafka' }, + { id: DBTypeEnum.milvus, label: 'Milvus' } // { id: DBTypeEnum.qdrant, label: 'qdrant' }, // { id: DBTypeEnum.nebula, label: 'nebula' }, // { id: DBTypeEnum.weaviate, label: 'weaviate' } diff --git a/frontend/providers/dbprovider/src/constants/theme.ts b/frontend/providers/dbprovider/src/constants/theme.ts index b8638768891..e955941b49e 100644 --- a/frontend/providers/dbprovider/src/constants/theme.ts +++ b/frontend/providers/dbprovider/src/constants/theme.ts @@ -38,9 +38,9 @@ export const theme = extendTheme(SealosTheme, { color: 'grayModern.900', fontSize: 'md', height: '100%', - overflowY: 'auto', - fontWeight: 400, - minWidth: '700px' + fontWeight: 400 + // overflowY: 'auto', + // minWidth: '700px' } } }, diff --git a/frontend/providers/dbprovider/src/pages/_app.tsx b/frontend/providers/dbprovider/src/pages/_app.tsx index 0969754d8c5..248b5efc3b4 100644 --- a/frontend/providers/dbprovider/src/pages/_app.tsx +++ b/frontend/providers/dbprovider/src/pages/_app.tsx @@ -97,7 +97,7 @@ function App({ Component, pageProps }: AppProps) { const changeI18n = async (data: any) => { const lastLang = getLangStore(); const newLang = data.currentLanguage; - if (lastLang !== newLang) { + if (lastLang !== newLang && typeof i18n?.changeLanguage === 'function') { i18n?.changeLanguage(newLang); setLangStore(newLang); setRefresh((state) => !state); diff --git a/frontend/providers/dbprovider/src/pages/api/backup/getBackupList.ts b/frontend/providers/dbprovider/src/pages/api/backup/getBackupList.ts index 5f0190fc54b..6c8ad383133 100644 --- a/frontend/providers/dbprovider/src/pages/api/backup/getBackupList.ts +++ b/frontend/providers/dbprovider/src/pages/api/backup/getBackupList.ts @@ -20,9 +20,10 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse< return; } + const data = await getBackupListByDBName({ dbName, req }); try { jsonRes(res, { - data: await getBackups({ dbName, req }) + data }); } catch (err: any) { jsonRes(res, { @@ -32,7 +33,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse< } } -export async function getBackups({ dbName, req }: Props & { req: NextApiRequest }) { +export async function getBackupListByDBName({ dbName, req }: Props & { req: NextApiRequest }) { const group = 'dataprotection.kubeblocks.io'; const version = 'v1alpha1'; const plural = 'backups'; diff --git a/frontend/providers/dbprovider/src/pages/api/backup/getBackups.ts b/frontend/providers/dbprovider/src/pages/api/backup/getBackups.ts new file mode 100644 index 00000000000..eda0218276d --- /dev/null +++ b/frontend/providers/dbprovider/src/pages/api/backup/getBackups.ts @@ -0,0 +1,39 @@ +import type { NextApiRequest, NextApiResponse } from 'next'; +import { ApiResp } from '@/services/kubernet'; +import { authSession } from '@/services/backend/auth'; +import { getK8s } from '@/services/backend/kubernetes'; +import { jsonRes } from '@/services/backend/response'; +import { crLabelKey } from '@/constants/db'; +import { adaptBackup } from '@/utils/adapt'; + +export type Props = { + dbName: string; +}; + +export default async function handler(req: NextApiRequest, res: NextApiResponse) { + const group = 'dataprotection.kubeblocks.io'; + const version = 'v1alpha1'; + const plural = 'backups'; + + const { k8sCustomObjects, namespace } = await getK8s({ + kubeconfig: await authSession(req) + }); + + const { body } = (await k8sCustomObjects.listNamespacedCustomObject( + group, + version, + namespace, + plural + )) as { body: { items: any[] } }; + + try { + jsonRes(res, { + data: body.items.map(adaptBackup) + }); + } catch (err: any) { + jsonRes(res, { + code: 500, + error: err + }); + } +} diff --git a/frontend/providers/dbprovider/src/pages/api/createDB.ts b/frontend/providers/dbprovider/src/pages/api/createDB.ts index 7b94e2832ac..343c45f1851 100644 --- a/frontend/providers/dbprovider/src/pages/api/createDB.ts +++ b/frontend/providers/dbprovider/src/pages/api/createDB.ts @@ -8,7 +8,7 @@ import { json2Account, json2ClusterOps, json2CreateCluster } from '@/utils/json2 import type { NextApiRequest, NextApiResponse } from 'next'; import { updateBackupPolicyApi } from './backup/updatePolicy'; import { BackupSupportedDBTypeList } from '@/constants/db'; -import { convertBackupFormToSpec } from '@/utils/adapt'; +import { adaptDBDetail, convertBackupFormToSpec } from '@/utils/adapt'; import { CustomObjectsApi, PatchUtils } from '@kubernetes/client-node'; export default async function handler(req: NextApiRequest, res: NextApiResponse) { @@ -33,33 +33,21 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse< )) as { body: KbPgClusterType; }; - - const currentConfig = { - cpu: parseInt(body.spec.componentSpecs[0].resources.limits.cpu.replace('m', '')), - memory: parseInt(body.spec.componentSpecs[0].resources.limits.memory.replace('Mi', '')), - replicas: body.spec.componentSpecs[0].replicas, - storage: parseInt( - body.spec.componentSpecs[0].volumeClaimTemplates[0].spec.resources.requests.storage.replace( - 'Gi', - '' - ) - ), - terminationPolicy: body.spec.terminationPolicy - }; + const { cpu, memory, replicas, storage, terminationPolicy } = adaptDBDetail(body); const opsRequests = []; - if (currentConfig.cpu !== dbForm.cpu || currentConfig.memory !== dbForm.memory) { + if (cpu !== dbForm.cpu || memory !== dbForm.memory) { const verticalScalingYaml = json2ClusterOps(dbForm, 'VerticalScaling'); opsRequests.push(verticalScalingYaml); } - if (currentConfig.replicas !== dbForm.replicas) { + if (replicas !== dbForm.replicas) { const horizontalScalingYaml = json2ClusterOps(dbForm, 'HorizontalScaling'); opsRequests.push(horizontalScalingYaml); } - if (dbForm.storage > currentConfig.storage) { + if (dbForm.storage > storage) { const volumeExpansionYaml = json2ClusterOps(dbForm, 'VolumeExpansion'); opsRequests.push(volumeExpansionYaml); } @@ -67,19 +55,16 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse< console.log('DB Edit Operation:', { dbName: dbForm.dbName, changes: { - cpu: currentConfig.cpu !== dbForm.cpu, - memory: currentConfig.memory !== dbForm.memory, - replicas: currentConfig.replicas !== dbForm.replicas, - storage: dbForm.storage > currentConfig.storage + cpu: cpu !== dbForm.cpu, + memory: memory !== dbForm.memory, + replicas: replicas !== dbForm.replicas, + storage: dbForm.storage > storage }, opsCount: opsRequests.length }); if (opsRequests.length > 0) { await applyYamlList(opsRequests, 'create'); - return jsonRes(res, { - data: `Successfully submitted ${opsRequests.length} change requests` - }); } if (BackupSupportedDBTypeList.includes(dbForm.dbType) && dbForm?.autoBackup) { @@ -96,7 +81,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse< namespace }); - if (currentConfig.terminationPolicy !== dbForm.terminationPolicy) { + if (terminationPolicy !== dbForm.terminationPolicy) { await updateTerminationPolicyApi({ dbName: dbForm.dbName, terminationPolicy: dbForm.terminationPolicy, @@ -107,7 +92,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse< } return jsonRes(res, { - data: 'success update db' + data: `Successfully submitted ${opsRequests.length} change requests` }); } diff --git a/frontend/providers/dbprovider/src/pages/api/delDBByName.ts b/frontend/providers/dbprovider/src/pages/api/delDBByName.ts index cfe12dd74dc..073b98fb40e 100644 --- a/frontend/providers/dbprovider/src/pages/api/delDBByName.ts +++ b/frontend/providers/dbprovider/src/pages/api/delDBByName.ts @@ -3,7 +3,7 @@ import { ApiResp } from '@/services/kubernet'; import { authSession } from '@/services/backend/auth'; import { getK8s } from '@/services/backend/kubernetes'; import { jsonRes } from '@/services/backend/response'; -import { getBackups } from './backup/getBackupList'; +import { getBackupListByDBName } from './backup/getBackupList'; import { delBackupByName } from './backup/delBackup'; import { getMigrateList } from './migrate/list'; import { delMigrateByName } from './migrate/delete'; @@ -40,7 +40,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse< } // get backup and delete - const backups = await getBackups({ dbName: name, req }); + const backups = await getBackupListByDBName({ dbName: name, req }); await Promise.all( backups.map((item) => delBackupByName({ backupName: item.metadata.name, req })) ); diff --git a/frontend/providers/dbprovider/src/pages/backups/index.tsx b/frontend/providers/dbprovider/src/pages/backups/index.tsx new file mode 100644 index 00000000000..8fc735062f2 --- /dev/null +++ b/frontend/providers/dbprovider/src/pages/backups/index.tsx @@ -0,0 +1,368 @@ +import { deleteBackup, getBackups } from '@/api/backup'; +import { getDBByName } from '@/api/db'; +import MyIcon from '@/components/Icon'; +import MyTooltip from '@/components/MyTooltip'; +import Sidebar from '@/components/Sidebar'; +import { BackupStatusEnum, backupTypeMap } from '@/constants/backup'; +import { useConfirm } from '@/hooks/useConfirm'; +import { useLoading } from '@/hooks/useLoading'; +import useEnvStore from '@/store/env'; +import { BackupItemType, DBDetailType } from '@/types/db'; +import { I18nCommonKey } from '@/types/i18next'; +import { serviceSideProps } from '@/utils/i18n'; +import { getErrText } from '@/utils/tools'; +import { QuestionOutlineIcon } from '@chakra-ui/icons'; +import { + Box, + Button, + Center, + Flex, + Image, + Input, + InputGroup, + InputLeftElement, + Table, + Tbody, + Td, + Text, + Th, + Thead, + Tr +} from '@chakra-ui/react'; +import { useMessage } from '@sealos/ui'; +import { useQuery } from '@tanstack/react-query'; +import { + ColumnDef, + flexRender, + getCoreRowModel, + getFilteredRowModel, + useReactTable +} from '@tanstack/react-table'; +import dayjs from 'dayjs'; +import { groupBy } from 'lodash'; +import { useTranslation } from 'next-i18next'; +import { useRouter } from 'next/router'; +import React, { useCallback, useMemo, useState } from 'react'; +import RestoreModal from '../db/detail/components/RestoreModal'; + +const operationIconStyles = { + w: '18px' +}; + +export default function Backups() { + const { t } = useTranslation(); + const router = useRouter(); + const [globalFilter, setGlobalFilter] = useState(''); + const [backupInfo, setBackupInfo] = useState(); + const { SystemEnv } = useEnvStore(); + const { message: toast } = useMessage(); + const { Loading, setIsLoading } = useLoading(); + const [expandedGroups, setExpandedGroups] = useState>({}); + + const { openConfirm: openConfirmDel, ConfirmChild: ConfirmDelChild } = useConfirm({ + content: t('confirm_delete_the_backup') + }); + + const [db, setDb] = useState(); + + const loadDBDetail = useCallback( + async (dbName: string) => { + try { + const res = await getDBByName(dbName); + setDb(res); + } catch (err) { + toast({ + title: getErrText(err), + status: 'error' + }); + } + }, + [toast] + ); + + const { data, refetch, isLoading } = useQuery(['getBackupList'], getBackups, { + onSuccess: (data) => { + if (data.length > 0 && Object.keys(expandedGroups).length === 0) { + const firstDbName = data[0].dbName; + setExpandedGroups((prev) => ({ + ...prev, + [firstDbName]: true + })); + } + }, + refetchInterval: 60000 + }); + + const confirmDel = useCallback( + async (name: string) => { + try { + setIsLoading(true); + await deleteBackup(name); + await refetch(); + toast({ + title: t('Success'), + status: 'success' + }); + } catch (err) { + toast({ + title: getErrText(err), + status: 'error' + }); + } + setIsLoading(false); + }, + [refetch, setIsLoading, toast] + ); + + const columns = useMemo>>( + () => [ + { + id: 'name', + accessorKey: 'name', + header: () => t('name') + }, + { + id: 'remark', + accessorKey: 'remark', + header: () => t('remark') + }, + { + id: 'status', + accessorKey: 'status', + header: () => t('status'), + cell: ({ row }) => ( + + {t(row.original.status.label as I18nCommonKey)} + {row.original.failureReason && ( + + + + )} + + ) + }, + { + id: 'backupTime', + accessorKey: 'startTime', + header: () => t('backup_time'), + cell: ({ row }) => dayjs(row.original.startTime).format('YYYY/MM/DD HH:mm') + }, + { + id: 'type', + accessorKey: 'type', + header: () => t('Type'), + cell: ({ row }) => t(backupTypeMap[row.original.type]?.label) || '-' + }, + { + id: 'actions', + header: () => t('operation'), + cell: ({ row }) => + row.original.status.value !== BackupStatusEnum.InProgress ? ( + + + + + + + + + ) : null + } + ], + [confirmDel, openConfirmDel, t] + ); + + const table = useReactTable({ + data: data || [], + columns, + getCoreRowModel: getCoreRowModel(), + getFilteredRowModel: getFilteredRowModel(), + state: { + globalFilter + }, + onGlobalFilterChange: setGlobalFilter, + globalFilterFn: (row, columnId, filterValue) => { + const name = row.original.name.toLowerCase().includes(filterValue.toLowerCase()); + const remark = row.original.remark.toLowerCase().includes(filterValue.toLowerCase()); + return name || remark; + } + }); + + return ( + + + + + + {t('backup_center')} + + + + + + + + table.setGlobalFilter(e.target.value)} + /> + + +
+ )} + {...tdStyle} + > {flexRender(cell.column.columnDef.cell, cell.getContext())}
+ + {table.getHeaderGroups().map((headerGroup) => ( + + {headerGroup.headers.map((header) => ( + + ))} + + ))} + + + {Object.entries(groupBy(table.getRowModel().rows, (row) => row.original.dbName)).map( + ([dbName, rows]) => ( + + + + + {expandedGroups[dbName] && + rows.map((row, index) => ( + + {row.getVisibleCells().map((cell) => ( + + ))} + + ))} + + ) + )} + +
+ {flexRender(header.column.columnDef.header, header.getContext())} +
+ +
+ setExpandedGroups((prev) => ({ + ...prev, + [dbName]: !prev[dbName] + })) + } + > + +
+ + {dbName} + + {dbName} +
+
+ {flexRender(cell.column.columnDef.cell, cell.getContext())} +
+ {data?.length === 0 && ( + + + {t('no_data_available')} + + )} + + + {!!backupInfo?.name && db && ( + setBackupInfo(undefined)} /> + )} +
+ ); +} + +export async function getServerSideProps(content: any) { + return { + props: { + ...(await serviceSideProps(content)) + } + }; +} diff --git a/frontend/providers/dbprovider/src/pages/db/detail/components/AppBaseInfo.tsx b/frontend/providers/dbprovider/src/pages/db/detail/components/AppBaseInfo.tsx index e15951ec28a..d62d61936b8 100644 --- a/frontend/providers/dbprovider/src/pages/db/detail/components/AppBaseInfo.tsx +++ b/frontend/providers/dbprovider/src/pages/db/detail/components/AppBaseInfo.tsx @@ -19,11 +19,15 @@ import { Center, Divider, Flex, + FlexProps, Modal, ModalCloseButton, ModalContent, ModalHeader, ModalOverlay, + Skeleton, + SkeletonText, + Stack, Switch, Text, useDisclosure @@ -35,6 +39,55 @@ import { useTranslation } from 'next-i18next'; import { useCallback, useMemo, useState } from 'react'; import { sealosApp } from 'sealos-desktop-sdk/app'; +const CopyBox = ({ + value, + showSecret = true, + boxStyle +}: { + value: string; + showSecret?: boolean; + boxStyle?: FlexProps; +}) => { + const { copyData } = useCopyData(); + + const defaultBoxStyle: FlexProps = { + borderRadius: '4px', + bg: 'grayModern.25', + h: '32px', + p: '8px 32px 8px 12px', + border: '1px solid', + borderColor: 'grayModern.100', + color: 'grayModern.900', + noOfLines: 1 + }; + + return ( + + + {showSecret ? value : '***********'} + +
copyData(value)} + cursor="pointer" + > + +
+
+ ); +}; + const AppBaseInfo = ({ db = defaultDBDetail }: { db: DBDetailType }) => { const { t } = useTranslation(); const { copyData } = useCopyData(); @@ -221,110 +274,106 @@ const AppBaseInfo = ({ db = defaultDBDetail }: { db: DBDetailType }) => { }; return ( - - {db?.source?.hasSource && ( - - - - {t('application_source')} - - + + + {appInfoTable.map((info, index) => ( + { - if (!db.source.sourceName) return; - if (db.source.sourceType === 'app_store') { - sealosApp.runEvents('openDesktopApp', { - appKey: 'system-template', - pathname: '/instance', - query: { instanceName: db.source.sourceName } - }); - } - if (db.source.sourceType === 'sealaf') { - sealosApp.runEvents('openDesktopApp', { - appKey: 'system-sealaf', - pathname: '/', - query: { instanceName: db.source.sourceName } - }); - } - }} + alignItems={'center'} + gap={'8px'} + color={'grayModern.900'} + fontWeight={'bold'} + fontSize={'16px'} > - - {t(db.source.sourceType)} - - {t('manage_all_resources')} - + {t(info.name)} - - - )} - {appInfoTable.map((info) => ( - - - - {t(info.name)} - - - - {info.items.map((item, i) => ( - - - {t(item.label)} + + {db?.source?.hasSource && index === 0 && ( + + { + if (!db.source.sourceName) return; + if (db.source.sourceType === 'app_store') { + sealosApp.runEvents('openDesktopApp', { + appKey: 'system-template', + pathname: '/instance', + query: { instanceName: db.source.sourceName } + }); + } + if (db.source.sourceType === 'sealaf') { + sealosApp.runEvents('openDesktopApp', { + appKey: 'system-sealaf', + pathname: '/', + query: { instanceName: db.source.sourceName } + }); + } + }} + > + + {t('application_source')} + + + {t(db.source.sourceType)} + + {t('manage_all_resources')} + + + - ( + - - item.value && !!item.copy && copyData(item.copy)} - > - {item.value} - - - - - ))} + + {t(item.label)} + + + + item.value && !!item.copy && copyData(item.copy)} + > + {item.value} + + + + + ))} + + {index !== appInfoTable.length - 1 && } - - ))} - {/* secret */} - {secret && ( - <> - - - {t('connection_info')} + ))} + + {secret ? ( + + + + {t('connection_info')} +
setShowSecret(!showSecret)} @@ -341,7 +390,8 @@ const AppBaseInfo = ({ db = defaultDBDetail }: { db: DBDetailType }) => { gap={'6px'} h="28px" fontSize={'12px'} - bg="grayModern.150" + bg="white" + border="1px solid #DFE2EA" borderRadius={'md'} px="8px" cursor={'pointer'} @@ -355,97 +405,78 @@ const AppBaseInfo = ({ db = defaultDBDetail }: { db: DBDetailType }) => { {t('direct_connection')}
)} - -
- {t('external_network')} - (isChecked ? closeNetWorkService() : onOpen())} - /> -
{['milvus', 'kafka'].indexOf(db.dbType) === -1 && ( - + {Object.entries(baseSecret).map(([name, value]) => ( - - {name} - - copyData(value)}> - {showSecret ? value : '***********'} - + + + {name} + ))} - + )} - - {t('intranet_address')} - - {Object.entries(otherSecret).map(([name, value]) => ( - - {name} - - copyData(value)}> - {showSecret ? value : '***********'} + + + + {t('intranet_address')} + + + {Object.entries(otherSecret).map(([name, value], index) => ( + + + {name} + - - ))} + ))} +
- {isChecked && ( - - {t('external_address')} - - {Object.entries(externalNetWork).map(([name, value]) => ( - - {name} - - copyData(value)}> - {showSecret ? value : '***********'} + + + + + {t('external_address')} + + (isChecked ? closeNetWorkService() : onOpen())} + /> + + {isChecked ? ( + + {Object.entries(externalNetWork).map(([name, value], index) => ( + + + {name} + - - ))} - - )} + ))} + + ) : ( +
+ {t('no_data_available')} +
+ )} +
@@ -487,9 +518,29 @@ const AppBaseInfo = ({ db = defaultDBDetail }: { db: DBDetailType }) => { - +
+ ) : ( + + + + + )} -
+ ); }; diff --git a/frontend/providers/dbprovider/src/pages/db/detail/components/BackupTable.tsx b/frontend/providers/dbprovider/src/pages/db/detail/components/BackupTable.tsx index caa7aa51fad..5238bce078b 100644 --- a/frontend/providers/dbprovider/src/pages/db/detail/components/BackupTable.tsx +++ b/frontend/providers/dbprovider/src/pages/db/detail/components/BackupTable.tsx @@ -15,6 +15,7 @@ import { TableContainer, Tbody, Td, + Text, Th, Thead, Tooltip, @@ -66,7 +67,7 @@ const BackupTable = ({ db }: { db?: DBDetailType }, ref: ForwardedRef { const backups: BackupItemType[] = await getBackupList(db.dbName); backups.sort((a, b) => new Date(b.startTime).getTime() - new Date(a.startTime).getTime()); @@ -110,6 +111,7 @@ const BackupTable = ({ db }: { db?: DBDetailType }, ref: ForwardedRef + + + {t('backup_list')} + + {!backupProcessing && ( + + )} + - + {columns.map((item) => ( diff --git a/frontend/providers/dbprovider/src/pages/db/detail/components/DataImport.tsx b/frontend/providers/dbprovider/src/pages/db/detail/components/DataImport.tsx new file mode 100644 index 00000000000..51d18f52586 --- /dev/null +++ b/frontend/providers/dbprovider/src/pages/db/detail/components/DataImport.tsx @@ -0,0 +1,75 @@ +import { DBDetailType } from '@/types/db'; +import { Box, Button, Flex } from '@chakra-ui/react'; +import { useTranslation } from 'next-i18next'; +import { useState } from 'react'; +import { MigrateTable } from './Migrate/Table'; +import DumpImport from './DumpImport'; +import useEnvStore from '@/store/env'; +import { useRouter } from 'next/router'; + +enum MenuType { + DumpImport = 'DumpImport', + InternetMigration = 'InternetMigration' +} + +export default function DataImport({ db }: { db?: DBDetailType }) { + if (!db) return null; + + const { t } = useTranslation(); + const [activeId, setActiveId] = useState(MenuType.InternetMigration); + const { SystemEnv } = useEnvStore(); + const router = useRouter(); + + return ( + + + + {[ + { id: MenuType.InternetMigration, label: t('online_import') }, + ...(!!SystemEnv.minio_url + ? [{ id: MenuType.DumpImport, label: t('import_through_file') }] + : []) + ].map((item) => ( + { + setActiveId(item.id); + } + })} + > + {item.label} + + ))} + + {activeId === MenuType.InternetMigration && ( + + )} + + {activeId === MenuType.InternetMigration && } + {activeId === MenuType.DumpImport && } + + ); +} 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 f430ab1bc31..a780230aee5 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 @@ -196,8 +196,8 @@ export default function DumpImport({ db }: { db?: DBDetailType }) { return ( - - + + {t('upload_dump_file')} diff --git a/frontend/providers/dbprovider/src/pages/db/detail/components/ErrorLog/RunTimeLog.tsx b/frontend/providers/dbprovider/src/pages/db/detail/components/ErrorLog/RunTimeLog.tsx index ae191a09c69..ec40294acd1 100644 --- a/frontend/providers/dbprovider/src/pages/db/detail/components/ErrorLog/RunTimeLog.tsx +++ b/frontend/providers/dbprovider/src/pages/db/detail/components/ErrorLog/RunTimeLog.tsx @@ -190,18 +190,21 @@ export default function RunTimeLog({ return ( - + {filteredSubNavList?.map((item) => ( item.value !== logType && updateSubMenu(item.value)} + fontWeight={'500'} > {t(item.label as I18nCommonKey)} @@ -269,7 +272,7 @@ export default function RunTimeLog({ /> )} - + @@ -280,7 +283,12 @@ export default function RunTimeLog({ /> - + + router.replace('/dbs')}> - - + + {router.query.name || db.dbName} - {!isLargeScreen && ( + {/* {!isLargeScreen && ( - )} + )} */} + {/* btns */} {/* Migrate */} {/* */} {db.status.value !== 'Stopped' && ( ) : (
{t(item.title)}
- + {columns.map((item) => ( 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 index f23b7e58206..c93ade7d165 100644 --- a/frontend/providers/dbprovider/src/pages/db/detail/components/Reconfigure/ConfigTable.tsx +++ b/frontend/providers/dbprovider/src/pages/db/detail/components/Reconfigure/ConfigTable.tsx @@ -154,7 +154,7 @@ const ConfigTable = forwardRef< return ( - + - + {SubNavList.map((item) => ( - + {subMenu === SubMenuEnum.Parameter && config && ( { const PublicNetMigration = ['postgresql', 'apecloud-mysql', 'mongodb'].includes(dbType); const MigrateSupported = ['postgresql', 'mongodb', 'apecloud-mysql'].includes(dbType); const BackupSupported = BackupSupportedDBTypeList.includes(dbType) && SystemEnv.BACKUP_ENABLED; const listNavValue = [ + { + label: t('overview'), + value: TabEnum.Overview, + icon: + }, + { label: 'monitor_list', value: TabEnum.monitor, icon: }, - { - label: 'replicas_list', - value: TabEnum.pod, - icon: - }, ...(PublicNetMigration ? [ { @@ -87,21 +91,12 @@ const AppDetail = ({ ...(PublicNetMigration ? [ { - label: 'online_import', - value: TabEnum.InternetMigration, + label: 'data_import', + value: TabEnum.DataImport, icon: } ] : []), - ...(PublicNetMigration && !!SystemEnv.minio_url - ? [ - { - label: 'import_through_file', - value: TabEnum.DumpImport, - icon: - } - ] - : []), ...(BackupSupported ? [ { @@ -119,7 +114,7 @@ const AppDetail = ({ isBackupSupported: BackupSupported, listNav: listNavValue }; - }, [SystemEnv, dbType]); + }, [SystemEnv.BACKUP_ENABLED, dbType, t]); const theme = useTheme(); const { message: toast } = useMessage(); @@ -141,118 +136,106 @@ const AppDetail = ({ }); return ( - +
- - - {dbDetail ? : } - + - - {listNav.map((item) => ( - - router.replace( - `/db/detail?name=${dbName}&dbType=${dbType}&listType=${item.value}` - ) - })} - > - {item.icon} - - {t(item.label as I18nCommonKey)} - - ))} - - {listType === TabEnum.pod && {dbPods.length} Items} - {listType === TabEnum.backup && !BackupTableRef.current?.backupProcessing && ( - - - - )} - {listType === TabEnum.InternetMigration && ( - - - - )} + {item.icon} + + ) : ( + {item.icon} + )} + + {!isSmallScreen && {t(item.label as I18nCommonKey)}} + + ))} + + {listType === TabEnum.Overview ? ( + + + + + {t('replicas_list')} + + + - - {listType === TabEnum.pod && } + ) : ( + {listType === TabEnum.backup && } + {listType === TabEnum.monitor && ( )} - {listType === TabEnum.InternetMigration && } - {listType === TabEnum.DumpImport && } + + {listType === TabEnum.DataImport && } + {listType === TabEnum.Reconfigure && ( )} + {listType === TabEnum.ErrorLog && } - + )} + {/* mask */} {!isLargeScreen && showSlider && ( import('@/pages/db/detail/components/DelModal')); @@ -116,209 +120,238 @@ const DBList = ({ [refetchApps, setLoading, t, toast] ); - const columns: { - title: string; - dataIndex?: keyof DBListItemType; - key: string; - render?: (item: DBListItemType) => JSX.Element; - }[] = [ - { - title: t('name'), - key: 'name', - render: (item: DBListItemType) => { - return ( - - {item.name} + const columns = useMemo>>( + () => [ + { + id: 'name', + accessorKey: 'name', + header: () => t('name'), + cell: ({ row }) => ( + + {row.original.name} - ); - } - }, - { - title: t('Type'), - key: 'dbType', - render: (item: DBListItemType) => ( - - - {DBComponentNameMap[item.dbType]} - - ) - }, - { - title: t('status'), - key: 'status', - render: (item: DBListItemType) => ( - - ) - }, - { - title: t('creation_time'), - dataIndex: 'createTime', - key: 'createTime' - }, - { - title: t('cpu'), - key: 'cpu', - render: (item: DBListItemType) => <>{item.cpu / 1000}C - }, - { - title: t('memory'), - key: 'memory', - render: (item: DBListItemType) => <>{printMemory(item.memory)} - }, - { - title: t('storage'), - key: 'storage', - dataIndex: 'storage' - }, - { - title: t('operation'), - key: 'control', - render: (item: DBListItemType) => ( - - - - - - } - menuList={[ - ...(item.status.value === DBStatusEnum.Stopped - ? [ - { - child: ( - <> - - {t('Continue')} - - ), - onClick: () => handleStartApp(item) - } - ] - : [ - { - child: ( - <> - - {t('update')} - - ), - onClick: () => { - if (item.source.hasSource && item.source.sourceType === 'sealaf') { - setUpdateAppName(item.name); - onOpenUpdateModal(); - } else { - router.push(`/db/edit?name=${item.name}`); - } + ) + }, + { + accessorKey: 'dbType', + header: () => t('Type'), + cell: ({ row }) => ( + + + {DBTypeList.find((i) => i.id === row.original.dbType)?.label} + + ) + }, + { + accessorKey: 'status', + header: () => t('status'), + cell: ({ row }) => ( + + ) + }, + { + accessorKey: 'createTime', + header: () => t('creation_time') + }, + { + accessorKey: 'cpu', + header: () => t('cpu'), + cell: ({ row }) => <>{row.original.cpu / 1000}C + }, + { + accessorKey: 'memory', + header: () => t('memory'), + cell: ({ row }) => <>{printMemory(row.original.memory)} + }, + { + accessorKey: 'storage', + header: () => t('storage') + }, + { + id: 'actions', + header: () => t('operation'), + cell: ({ row }) => ( + + + + + + + } + menuList={[ + ...(row.original.status.value === DBStatusEnum.Stopped + ? [ + { + child: ( + <> + + {t('Continue')} + + ), + onClick: () => handleStartApp(row.original) + } + ] + : [ + { + child: ( + <> + + {t('update')} + + ), + onClick: () => { + if ( + row.original.source.hasSource && + row.original.source.sourceType === 'sealaf' + ) { + setUpdateAppName(row.original.name); + onOpenUpdateModal(); + } else { + router.push(`/db/edit?name=${row.original.name}`); + } + }, + isDisabled: + row.original.status.value === 'Updating' && + !row.original.isDiskSpaceOverflow }, - isDisabled: item.status.value === 'Updating' && !item.isDiskSpaceOverflow - }, - { - child: ( - <> - - {t('Restart')} - - ), - onClick: () => handleRestartApp(item), - isDisabled: item.status.value === 'Updating' - } - ]), - ...(item.status.value === DBStatusEnum.Running - ? [ - { - child: ( - <> - - {t('Pause')} - - ), - onClick: onOpenPause(() => handlePauseApp(item)) + { + child: ( + <> + + {t('Restart')} + + ), + onClick: () => handleRestartApp(row.original), + isDisabled: row.original.status.value === 'Updating' + } + ]), + ...(row.original.status.value === DBStatusEnum.Running + ? [ + { + child: ( + <> + + {t('Pause')} + + ), + onClick: onOpenPause(() => handlePauseApp(row.original)) + } + ] + : []), + + { + child: ( + <> + + {t('Delete')} + + ), + menuItemStyle: { + _hover: { + color: 'red.600', + bg: 'rgba(17, 24, 36, 0.05)' } - ] - : []), + }, + onClick: () => setDelAppName(row.original.name), + isDisabled: row.original.status.value === 'Updating' + } + ]} + /> + + ) + } + ], + [] + ); - { - child: ( - <> - - {t('Delete')} - - ), - menuItemStyle: { - _hover: { - color: 'red.600', - bg: 'rgba(17, 24, 36, 0.05)' - } - }, - onClick: () => setDelAppName(item.name), - isDisabled: item.status.value === 'Updating' - } - ]} - /> - - ) - } - ]; + const table = useReactTable({ + data: dbList, + columns, + initialState: { + columnPinning: { + left: ['name'], + right: ['actions'] + } + }, + // enableColumnPinning: true, + getCoreRowModel: getCoreRowModel(), + getFilteredRowModel: getFilteredRowModel() + }); return ( - - -
- -
+ + {t('DBList')} - - ( {dbList.length} ) - +
+ {dbList.length} +
- {SystemEnv?.SHOW_DOCUMENT && ( - - )}
- + + + {!!delAppName && ( { flexDirection="column" alignItems="center" justifyContent="center" - bg={'#F3F4F5'} + backgroundColor={'white'} + px={'32px'} + h={'full'} + w={'full'} + borderRadius={'xl'} > {t('database_empty')} diff --git a/frontend/providers/dbprovider/src/pages/dbs/index.tsx b/frontend/providers/dbprovider/src/pages/dbs/index.tsx index cc3be741895..0c1bb91669c 100644 --- a/frontend/providers/dbprovider/src/pages/dbs/index.tsx +++ b/frontend/providers/dbprovider/src/pages/dbs/index.tsx @@ -5,6 +5,8 @@ import { useDBStore } from '@/store/db'; import { useLoading } from '@/hooks/useLoading'; import { useState } from 'react'; import { serviceSideProps } from '@/utils/i18n'; +import Sidebar from '@/components/Sidebar'; +import { Flex } from '@chakra-ui/react'; function Home() { const { dbList, setDBList } = useDBStore(); @@ -20,13 +22,14 @@ function Home() { return ( <> - {dbList.length === 0 && initialized ? ( - - ) : ( - <> + + + {dbList.length === 0 && initialized ? ( + + ) : ( - - )} + )} + ); diff --git a/frontend/providers/dbprovider/src/pages/icons/index.tsx b/frontend/providers/dbprovider/src/pages/icons/index.tsx new file mode 100644 index 00000000000..07068676609 --- /dev/null +++ b/frontend/providers/dbprovider/src/pages/icons/index.tsx @@ -0,0 +1,51 @@ +import MyIcon, { IconMap } from '@/components/Icon'; +import { useCopyData } from '@/utils/tools'; +import { Box, SimpleGrid } from '@chakra-ui/react'; +import { useRouter } from 'next/router'; +import { useEffect } from 'react'; + +export default function IconsPage() { + const router = useRouter(); + const iconNames = Object.keys(IconMap) as Array; + const { copyData } = useCopyData(); + + const copyIconName = (iconName: string) => { + const iconCode = ``; + copyData(iconCode); + }; + + useEffect(() => { + if (process.env.NODE_ENV === 'production') { + router.replace('/dbs'); + } + }, [router]); + + return ( + + + {iconNames.map((iconName) => ( + copyIconName(iconName)} + display="flex" + flexDirection="column" + alignItems="center" + _hover={{ bg: 'grayModern.50' }} + > + + + + + {iconName} + + + ))} + + + ); +} diff --git a/frontend/providers/dbprovider/src/types/db.d.ts b/frontend/providers/dbprovider/src/types/db.d.ts index 48fe96769ff..07a5a24e8b6 100644 --- a/frontend/providers/dbprovider/src/types/db.d.ts +++ b/frontend/providers/dbprovider/src/types/db.d.ts @@ -138,6 +138,8 @@ export interface BackupItemType { type: `${BackupTypeEnum}`; namespace: string; connectionPassword: string; + dbName: string; + dbType: string; } export type ReconfigStatusMapType = { diff --git a/frontend/providers/dbprovider/src/utils/adapt.ts b/frontend/providers/dbprovider/src/utils/adapt.ts index d43342f0d3a..5a5e5397f62 100644 --- a/frontend/providers/dbprovider/src/utils/adapt.ts +++ b/frontend/providers/dbprovider/src/utils/adapt.ts @@ -1,6 +1,7 @@ import { BACKUP_REMARK_LABEL_KEY, BackupTypeEnum, backupStatusMap } from '@/constants/backup'; import { DBBackupMethodNameMap, + DBNameLabel, DBPreviousConfigKey, DBReconfigStatusMap, DBSourceConfigs, @@ -218,6 +219,7 @@ export const adaptBackup = (backup: BackupCRItemType): BackupItemType => { const autoLabel = 'dataprotection.kubeblocks.io/autobackup'; const passwordLabel = 'dataprotection.kubeblocks.io/connection-password'; const remark = backup.metadata.labels[BACKUP_REMARK_LABEL_KEY]; + const dbType = backup.metadata.labels['apps.kubeblocks.io/component-name'] || 'postgresql'; return { id: backup.metadata.uid, @@ -231,7 +233,9 @@ export const adaptBackup = (backup: BackupCRItemType): BackupItemType => { type: autoLabel in backup.metadata.labels ? BackupTypeEnum.auto : BackupTypeEnum.manual, remark: remark ? decodeFromHex(remark) : '-', failureReason: backup.status?.failureReason, - connectionPassword: backup.metadata?.annotations?.[passwordLabel] + connectionPassword: backup.metadata?.annotations?.[passwordLabel], + dbName: backup.metadata.labels[DBNameLabel], + dbType: dbType === 'mysql' ? 'apecloud-mysql' : dbType }; };
{ backgroundColor={'grayModern.50'} fontWeight={'500'} color={'grayModern.600'} + _first={{ + borderLeftRadius: '6px' + }} + _last={{ + borderRightRadius: '6px' + }} > {t(item.title)}