diff --git a/frontend/providers/dbprovider/package.json b/frontend/providers/dbprovider/package.json index 21c8d1456..d04e6822c 100644 --- a/frontend/providers/dbprovider/package.json +++ b/frontend/providers/dbprovider/package.json @@ -11,6 +11,7 @@ "unlink-sdk": "yalc remove --all && pnpm install sealos-desktop-sdk" }, "dependencies": { + "@chakra-ui/anatomy": "^2.1.1", "@chakra-ui/icons": "^2.0.17", "@chakra-ui/react": "^2.5.1", "@chakra-ui/system": "^2.5.5", diff --git a/frontend/providers/dbprovider/pnpm-lock.yaml b/frontend/providers/dbprovider/pnpm-lock.yaml index 6807d5a08..7547b175f 100644 --- a/frontend/providers/dbprovider/pnpm-lock.yaml +++ b/frontend/providers/dbprovider/pnpm-lock.yaml @@ -5,6 +5,9 @@ settings: excludeLinksFromLockfile: false dependencies: + '@chakra-ui/anatomy': + specifier: ^2.1.1 + version: registry.npmmirror.com/@chakra-ui/anatomy@2.1.1 '@chakra-ui/icons': specifier: ^2.0.17 version: registry.npmmirror.com/@chakra-ui/icons@2.0.18(@chakra-ui/system@2.5.5)(react@18.2.0) @@ -1816,6 +1819,12 @@ packages: react: registry.npmmirror.com/react@18.2.0 dev: false + registry.npmmirror.com/@chakra-ui/anatomy@2.1.1: + resolution: {integrity: sha512-LUHAoqJAgxAqmyckG5bUpBrfEo1FleEyY+1A8hkWciy58gZ+h3GoY9oBpHcdo7XdHPpy3G+3hieK/7i9NLwxAw==, registry: https://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/@chakra-ui/anatomy/-/anatomy-2.1.1.tgz} + name: '@chakra-ui/anatomy' + version: 2.1.1 + dev: false + registry.npmmirror.com/@chakra-ui/anatomy@2.1.2: resolution: {integrity: sha512-pKfOS/mztc4sUXHNc8ypJ1gPWSolWT770jrgVRfolVbYlki8y5Y+As996zMF6k5lewTu6j9DQequ7Cc9a69IVQ==, registry: https://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/@chakra-ui/anatomy/-/anatomy-2.1.2.tgz} name: '@chakra-ui/anatomy' diff --git a/frontend/providers/dbprovider/public/locales/zh/common.json b/frontend/providers/dbprovider/public/locales/zh/common.json index 610293bc0..a2ed72374 100644 --- a/frontend/providers/dbprovider/public/locales/zh/common.json +++ b/frontend/providers/dbprovider/public/locales/zh/common.json @@ -123,5 +123,21 @@ "Auto Backup": "自动备份", "Containers": "容器", "Redis does not support backup at this time": "Redis 暂时不支持备份", - "The multi-replica Redis includes High Availability (HA) nodes. Please note, the anticipated price already encompasses the cost for the HA nodes.": "Redis 多副本包含 HA 节点,请悉知,预估价格已包含 HA 节点费用" + "The multi-replica Redis includes High Availability (HA) nodes. Please note, the anticipated price already encompasses the cost for the HA nodes.": "Redis 多副本包含 HA 节点,请悉知,预估价格已包含 HA 节点费用", + "CronExpression": "循环周期", + "Monday": "周一", + "Tuesday": "周二", + "Wednesday": "周三", + "Thursday": "周四", + "Friday": "周五", + "Saturday": "周六", + "Sunday": "周日", + "Start Hour": "小时", + "Start Minute": "分钟", + "Save": "保存", + "Hour": "小时", + "Day": "天", + "Week": "周", + "SaveTime": "保留时间", + "Set auto backup successful": "设置自动备份任务成功" } diff --git a/frontend/providers/dbprovider/src/api/backup.ts b/frontend/providers/dbprovider/src/api/backup.ts index 5d0d290ab..915fe11ad 100644 --- a/frontend/providers/dbprovider/src/api/backup.ts +++ b/frontend/providers/dbprovider/src/api/backup.ts @@ -1,11 +1,16 @@ import { GET, POST, DELETE } from '@/services/request'; import type { Props as CreateBackupPros } from '@/pages/api/backup/create'; import { adaptBackup } from '@/utils/adapt'; -import { BackupItemType } from '@/types/db'; +import { AutoBackupFormType } from '@/types/backup'; +import type { Props as UpdatePolicyProps } from '@/pages/api/backup/updatePolicy'; +export const getBackupPolicy = (data: { dbName: string; dbType: string }) => + GET(`/api/backup/policy`, data); 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 deleteBackup = (backupName: string) => DELETE(`/api/backup/delBackup?backupName=${backupName}`); +export const updateBackupPolicy = (data: UpdatePolicyProps) => + POST(`/api/backup/updatePolicy`, data); diff --git a/frontend/providers/dbprovider/src/components/Select/index.tsx b/frontend/providers/dbprovider/src/components/Select/index.tsx index a272da295..83f108226 100644 --- a/frontend/providers/dbprovider/src/components/Select/index.tsx +++ b/frontend/providers/dbprovider/src/components/Select/index.tsx @@ -1,7 +1,8 @@ import React from 'react'; -import { Menu, MenuButton, MenuList, MenuItem, Button, useDisclosure } from '@chakra-ui/react'; +import { Menu, MenuButton, MenuList, MenuItem, Button, useDisclosure, Box } from '@chakra-ui/react'; import type { ButtonProps } from '@chakra-ui/react'; import { ChevronDownIcon } from '@chakra-ui/icons'; +import { useTranslation } from 'next-i18next'; interface Props extends ButtonProps { isDisabled?: boolean; @@ -12,6 +13,7 @@ interface Props extends ButtonProps { id: string; }[]; width?: number | string; + icon?: React.ReactNode; onchange?: (val: string) => void; } @@ -22,13 +24,19 @@ const MySelect = ({ width = 'auto', list, onchange, + icon = , ...props }: Props) => { + const { t } = useTranslation(); + const menuItemStyles = { borderRadius: 'sm', py: 2, display: 'flex', alignItems: 'center', + transform: 'translate3d(0,0,0)', + cursor: 'pointer', + px: 3, _hover: { backgroundColor: 'myWhite.600' } @@ -36,9 +44,15 @@ const MySelect = ({ const { isOpen, onOpen, onClose } = useDisclosure(); return ( - !isDisabled && onOpen()} onClose={onClose}> - + !isDisabled && onOpen()} + onClose={onClose} + > + {!isDisabled && ( @@ -69,9 +86,11 @@ const MySelect = ({ '0px 2px 4px rgba(161, 167, 179, 0.25), 0px 0px 1px rgba(121, 141, 159, 0.25);' } zIndex={99} + maxH={'300px'} + overflow={'overlay'} > {list?.map((item) => ( - - {item.label} - + {t(item.label)} + ))} )} @@ -94,4 +114,4 @@ const MySelect = ({ ); }; -export default MySelect; +export default React.memo(MySelect); diff --git a/frontend/providers/dbprovider/src/components/Tabs/index.tsx b/frontend/providers/dbprovider/src/components/Tabs/index.tsx index 35c5d7fcb..c7ff7009f 100644 --- a/frontend/providers/dbprovider/src/components/Tabs/index.tsx +++ b/frontend/providers/dbprovider/src/components/Tabs/index.tsx @@ -36,6 +36,10 @@ const Tabs = ({ list, size = 'md', activeId, onChange, ...props }: Props) => { }; } }, [size]); + const activeIndex = useMemo( + () => list.findIndex((item) => item.id === activeId), + [activeId, list] + ); return ( { fontSize={sizeMap.fontSize} {...props} > - {list.map((item) => ( + {list.map((item, i) => ( { onChange(item.id); }} > - {t(item.label)} + {t(item.label)} ))} diff --git a/frontend/providers/dbprovider/src/constants/db.ts b/frontend/providers/dbprovider/src/constants/db.ts index 0b909b0a8..9c9386329 100644 --- a/frontend/providers/dbprovider/src/constants/db.ts +++ b/frontend/providers/dbprovider/src/constants/db.ts @@ -169,7 +169,7 @@ export const DBBackupPolicyNameMap = { export const defaultDBEditValue: DBEditType = { dbType: DBTypeEnum.postgresql, dbVersion: '', - dbName: 'dbname', + dbName: '', replicas: 1, cpu: CpuSlideMarkList[1].value, memory: MemorySlideMarkList[1].value, diff --git a/frontend/providers/dbprovider/src/constants/theme.ts b/frontend/providers/dbprovider/src/constants/theme.ts index 7c98a06cd..257ca7a96 100644 --- a/frontend/providers/dbprovider/src/constants/theme.ts +++ b/frontend/providers/dbprovider/src/constants/theme.ts @@ -6,13 +6,20 @@ import { extendTheme } from '@chakra-ui/react'; // @ts-ignore -import { selectAnatomy, switchAnatomy, numberInputAnatomy } from '@chakra-ui/anatomy'; +import { + selectAnatomy, + switchAnatomy, + numberInputAnatomy, + checkboxAnatomy +} from '@chakra-ui/anatomy'; const { definePartsStyle: selectPart, defineMultiStyleConfig: selectMultiStyle } = createMultiStyleConfigHelpers(selectAnatomy.keys); const { definePartsStyle: switchPart, defineMultiStyleConfig: switchMultiStyle } = createMultiStyleConfigHelpers(switchAnatomy.keys); const { definePartsStyle: numInputPart, defineMultiStyleConfig: numInputMultiStyle } = createMultiStyleConfigHelpers(numberInputAnatomy.keys); +const { definePartsStyle: checkboxPart, defineMultiStyleConfig: checkboxStyle } = + createMultiStyleConfigHelpers(checkboxAnatomy.keys); const Button = defineStyleConfig({ baseStyle: { @@ -181,7 +188,17 @@ const Switch = switchMultiStyle({ bg: 'myGray.700' } } - }) + }), + variants: { + deepLight: { + track: { + bg: 'myGray.200', + _checked: { + bg: 'myGray.700' + } + } + } + } }); const Tooltip = defineStyleConfig({ @@ -194,6 +211,21 @@ const Tooltip = defineStyleConfig({ } }); +const Checkbox = checkboxStyle({ + baseStyle: checkboxPart({ + control: defineStyle({ + _checked: { + bg: 'black', + borderColor: 'black', + _hover: { + bg: 'black !important', + borderColor: 'black !important' + } + } + }) + }) +}); + export const theme = extendTheme({ styles: { global: { @@ -305,6 +337,7 @@ export const theme = extendTheme({ Select, Switch, Textarea, - NumberInput + NumberInput, + Checkbox } }); diff --git a/frontend/providers/dbprovider/src/pages/api/backup/create.ts b/frontend/providers/dbprovider/src/pages/api/backup/create.ts index d7066cb27..b537ca3d5 100644 --- a/frontend/providers/dbprovider/src/pages/api/backup/create.ts +++ b/frontend/providers/dbprovider/src/pages/api/backup/create.ts @@ -3,18 +3,20 @@ import { ApiResp } from '@/services/kubernet'; import { authSession } from '@/services/backend/auth'; import { getK8s } from '@/services/backend/kubernetes'; import { jsonRes } from '@/services/backend/response'; -import { json2Backup } from '@/utils/json2Yaml'; +import { json2ManualBackup } from '@/utils/json2Yaml'; +import { DBBackupPolicyNameMap, DBTypeEnum } from '@/constants/db'; export type Props = { backupName: string; dbName: string; remark?: string; + dbType: `${DBTypeEnum}`; }; export default async function handler(req: NextApiRequest, res: NextApiResponse) { - const { backupName, dbName, remark } = req.body as Props; + const { backupName, dbName, remark, dbType } = req.body as Props; - if (!dbName || !backupName) { + if (!dbName || !backupName || !dbType) { jsonRes(res, { code: 500, error: 'params error' @@ -24,7 +26,6 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse< const group = 'dataprotection.kubeblocks.io'; const version = 'v1alpha1'; - const labelKey = 'app.kubernetes.io/instance'; const plural = 'backuppolicies'; try { @@ -33,27 +34,22 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse< }); // get backup backupolicies.dataprotection.kubeblocks.io - const { body } = (await k8sCustomObjects.listNamespacedCustomObject( + const { body } = (await k8sCustomObjects.getNamespacedCustomObject( group, version, namespace, plural, - undefined, - undefined, - undefined, - undefined, - `${labelKey}=${dbName}` + `${dbName}-${DBBackupPolicyNameMap[dbType]}-backup-policy` )) as { body: any }; - const backupPolicyName = body?.items?.[0]?.metadata?.name; + const backupPolicyName = body?.metadata?.name; if (!backupPolicyName) { throw new Error('Cannot find backup policy'); } - const backupCr = json2Backup({ + const backupCr = json2ManualBackup({ name: backupName, - dbName, backupPolicyName, remark }); diff --git a/frontend/providers/dbprovider/src/pages/api/backup/getBackupList.ts b/frontend/providers/dbprovider/src/pages/api/backup/getBackupList.ts index dd1593f96..5f0190fc5 100644 --- a/frontend/providers/dbprovider/src/pages/api/backup/getBackupList.ts +++ b/frontend/providers/dbprovider/src/pages/api/backup/getBackupList.ts @@ -50,7 +50,7 @@ export async function getBackups({ dbName, req }: Props & { req: NextApiRequest undefined, undefined, undefined, - `${crLabelKey}=${dbName}` + `app.kubernetes.io/instance=${dbName}` )) as { body: { items: any[] } }; return body?.items || []; diff --git a/frontend/providers/dbprovider/src/pages/api/backup/policy.ts b/frontend/providers/dbprovider/src/pages/api/backup/policy.ts new file mode 100644 index 000000000..6b5cd35db --- /dev/null +++ b/frontend/providers/dbprovider/src/pages/api/backup/policy.ts @@ -0,0 +1,54 @@ +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 { adaptPolicy } from '@/utils/adapt'; +import { DBBackupPolicyNameMap, DBTypeEnum } from '@/constants/db'; + +export type Props = { + dbName: string; + dbType: `${DBTypeEnum}`; +}; + +export default async function handler(req: NextApiRequest, res: NextApiResponse) { + const { dbName, dbType } = req.query as Props; + + if (!dbName || !dbType) { + jsonRes(res, { + code: 500, + error: 'params error' + }); + return; + } + + const group = 'dataprotection.kubeblocks.io'; + const version = 'v1alpha1'; + const plural = 'backuppolicies'; + + try { + const { k8sCustomObjects, namespace } = await getK8s({ + kubeconfig: await authSession(req) + }); + + // get backup backupolicies.dataprotection.kubeblocks.io + const { body } = (await k8sCustomObjects.getNamespacedCustomObject( + group, + version, + namespace, + plural, + `${dbName}-${DBBackupPolicyNameMap[dbType]}-backup-policy` + )) as { body: any }; + + jsonRes(res, { + data: adaptPolicy(body) + }); + } catch (err: any) { + jsonRes(res, { + code: 500, + error: err + }); + } +} + +export const getBackupPolicy = ({}: Props) => {}; diff --git a/frontend/providers/dbprovider/src/pages/api/backup/updatePolicy.ts b/frontend/providers/dbprovider/src/pages/api/backup/updatePolicy.ts new file mode 100644 index 000000000..3af652701 --- /dev/null +++ b/frontend/providers/dbprovider/src/pages/api/backup/updatePolicy.ts @@ -0,0 +1,56 @@ +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 { PatchUtils } from '@kubernetes/client-node'; +import { DBBackupPolicyNameMap, DBTypeEnum } from '@/constants/db'; + +export type Props = { + dbName: string; + dbType: `${DBTypeEnum}`; + patch: Object; +}; + +export default async function handler(req: NextApiRequest, res: NextApiResponse) { + const { dbName, dbType, patch } = req.body as Props; + + if (!dbName || !dbType || !patch) { + jsonRes(res, { + code: 500, + error: 'params error' + }); + return; + } + + const group = 'dataprotection.kubeblocks.io'; + const version = 'v1alpha1'; + const plural = 'backuppolicies'; + + try { + const { k8sCustomObjects, namespace } = await getK8s({ + kubeconfig: await authSession(req) + }); + + // get backup backupolicies.dataprotection.kubeblocks.io + await k8sCustomObjects.patchNamespacedCustomObject( + group, + version, + namespace, + plural, + `${dbName}-${DBBackupPolicyNameMap[dbType]}-backup-policy`, + patch, + undefined, + undefined, + undefined, + { headers: { 'Content-type': PatchUtils.PATCH_FORMAT_JSON_PATCH } } + ); + + jsonRes(res); + } catch (err: any) { + jsonRes(res, { + code: 500, + error: err + }); + } +} 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 48f1b9260..4688de608 100644 --- a/frontend/providers/dbprovider/src/pages/db/detail/components/BackupModal.tsx +++ b/frontend/providers/dbprovider/src/pages/db/detail/components/BackupModal.tsx @@ -1,4 +1,4 @@ -import React, { useCallback, useState } from 'react'; +import React, { useCallback, useMemo, useRef, useState } from 'react'; import { Modal, ModalOverlay, @@ -10,19 +10,25 @@ import { Button, useTheme, Flex, - Input + Input, + Switch, + Checkbox } from '@chakra-ui/react'; import { useConfirm } from '@/hooks/useConfirm'; -import { createBackup } from '@/api/backup'; +import { createBackup, updateBackupPolicy } from '@/api/backup'; import { useMutation } from '@tanstack/react-query'; -import { getErrText } from '@/utils/tools'; +import { getErrText, convertCronTime } from '@/utils/tools'; import { useToast } from '@/hooks/useToast'; import { customAlphabet } from 'nanoid'; const nanoid = customAlphabet('abcdefghijklmnopqrstuvwxyz1234567890', 6); import Tip from '@/components/Tip'; -import { InfoOutlineIcon } from '@chakra-ui/icons'; +import { InfoOutlineIcon, TimeIcon } from '@chakra-ui/icons'; import { useTranslation } from 'next-i18next'; import { useForm } from 'react-hook-form'; +import type { AutoBackupFormType, AutoBackupType } from '@/types/backup'; +import Tabs from '@/components/Tabs'; +import MySelect from '@/components/Select'; +import { DBTypeEnum } from '@/constants/db'; enum NavEnum { manual = 'manual', @@ -30,13 +36,17 @@ enum NavEnum { } const BackupModal = ({ + defaultVal, dbName, + dbType, onClose, - onSuccess + refetchPolicy }: { + defaultVal: AutoBackupFormType; dbName: string; + dbType: `${DBTypeEnum}`; onClose: () => void; - onSuccess?: () => void; + refetchPolicy: () => void; }) => { const { t } = useTranslation(); const theme = useTheme(); @@ -47,6 +57,7 @@ const BackupModal = ({ content: t('Manual Backup Tip'), confirmText: 'Start Backup' }); + const [refresh, setRefresh] = useState(false); const [currentNav, setCurrentNav] = useState<`${NavEnum}`>(NavEnum.manual); const { register: manualRegister, @@ -58,6 +69,39 @@ const BackupModal = ({ remark: '' } }); + const { + register: autoRegister, + handleSubmit: handleSubmitAuto, + getValues: getAutoValues, + setValue: setAutoValue + } = useForm({ + defaultValues: defaultVal + }); + + const selectTimeList = useRef< + { + id: string; + label: string; + }[] + >( + (() => + new Array(60).fill(0).map((item, i) => { + const val = i < 10 ? `0${i}` : `${i}`; + return { + id: val, + label: val + }; + }))() + ); + const weekSelectList = useRef([ + { label: 'Monday', id: '1' }, + { label: 'Tuesday', id: '2' }, + { label: 'Wednesday', id: '3' }, + { label: 'Thursday', id: '4' }, + { label: 'Friday', id: '5' }, + { label: 'Saturday', id: '6' }, + { label: 'Sunday', id: '0' } + ]); const navStyle = useCallback( (nav: `${NavEnum}`) => ({ @@ -83,7 +127,8 @@ const BackupModal = ({ return createBackup({ backupName: getManualValues('backupName'), remark: getManualValues('remark'), - dbName + dbName, + dbType }); }, onSuccess() { @@ -100,6 +145,53 @@ const BackupModal = ({ }); } }); + const { mutate: onclickSetAutoBackup, isLoading: isLoadingSetAutoBackup } = useMutation({ + mutationFn: async (data: AutoBackupFormType) => { + // parse Cron + const cron = await (() => { + if (data.type === 'week') { + if (data.week.length === 0) { + return Promise.reject('Week is empty'); + } + return `${data.minute} ${data.hour} * * ${data.week.join(',')}`; + } + if (data.type === 'day') { + return `${data.minute} ${data.hour} * * *`; + } + return `${data.minute} * * * *`; + })(); + + const patch = [ + { op: 'replace', path: '/spec/retention/ttl', value: `${data.saveTime}${data.saveType}` }, + { op: 'replace', path: '/spec/schedule/datafile/enable', value: data.start }, + { + op: 'replace', + path: '/spec/schedule/datafile/cronExpression', + value: convertCronTime(cron, -8) + } + ]; + + return updateBackupPolicy({ + dbName, + dbType, + patch + }); + }, + onSuccess() { + toast({ + status: 'success', + title: t('Set auto backup successful') + }); + refetchPolicy(); + onClose(); + }, + onError(err) { + toast({ + status: 'error', + title: t(getErrText(err, 'The backup task has been created failed !')) + }); + } + }); return ( <> @@ -111,42 +203,188 @@ const BackupModal = ({ {t('Manual Backup')} - {/* {t('Auto Backup')} */} - - {currentNav === NavEnum.manual && ( - - } - size="sm" - text={t('Manual Backup Tip')} - /> - - - {t('Backup Name')} - - - - {t('Remark')} - - - - - - + + {t('Auto Backup')} + { + setAutoValue('start', e.target.checked); + setRefresh((state) => !state); + }} + > - )} + + + } + size="sm" + text={t('Manual Backup Tip')} + /> + {currentNav === NavEnum.manual && ( + <> + + + {t('Backup Name')} + + + + {t('Remark')} + + + + + + + + )} + {currentNav === NavEnum.auto && ( + <> + + + {t('CronExpression')} + { + setAutoValue('type', e as AutoBackupType); + setRefresh((state) => !state); + }} + /> + + {getAutoValues('type') === 'week' && ( + + {weekSelectList.current.map((item) => ( + + { + const val = e.target.checked; + const checkedList = [...getAutoValues('week')]; + const index = checkedList.findIndex((week) => week === item.id); + if (val && index === -1) { + setAutoValue('week', checkedList.concat(item.id)); + } else if (!val && index > -1) { + checkedList.splice(index, 1); + setAutoValue('week', checkedList); + } + setRefresh((state) => !state); + }} + > + {t(item.label)} + + + ))} + + )} + {getAutoValues('type') !== 'hour' && ( + + {t('Start Hour')} + } + onchange={(val: any) => { + setAutoValue('hour', val); + setRefresh((state) => !state); + }} + /> + + )} + + {t('Start Minute')} + } + onchange={(val: any) => { + setAutoValue('minute', val); + setRefresh((state) => !state); + }} + /> + + + {t('SaveTime')} + + { + setAutoValue('saveType', val); + setRefresh((state) => !state); + }} + /> + + + + + + {!getAutoValues('start') && ( + + )} + + )} + 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 28ad66db3..d35bc57dd 100644 --- a/frontend/providers/dbprovider/src/pages/db/detail/components/BackupTable.tsx +++ b/frontend/providers/dbprovider/src/pages/db/detail/components/BackupTable.tsx @@ -8,7 +8,6 @@ import React, { } from 'react'; import { Box, - Button, Table, Thead, Tbody, @@ -30,7 +29,7 @@ import { useConfirm } from '@/hooks/useConfirm'; import dayjs from 'dayjs'; import { BackupStatusEnum, backupTypeMap } from '@/constants/backup'; import { useTranslation } from 'next-i18next'; -import { deleteBackup } from '@/api/backup'; +import { deleteBackup, getBackupPolicy } from '@/api/backup'; import { getErrText } from '@/utils/tools'; import { getBackupList } from '@/api/backup'; import MyIcon from '@/components/Icon'; @@ -188,6 +187,23 @@ const BackupTable = ({ db }: { db: DBDetailType }, ref: ForwardedRef + db.dbName && db.dbType + ? getBackupPolicy({ + dbName: db.dbName, + dbType: db.dbType + }) + : { + start: false, + hour: '18', + minute: '00', + week: [], + type: 'day', + saveTime: 7, + saveType: 'day' + } + ); + return ( @@ -226,8 +242,14 @@ const BackupTable = ({ db }: { db: DBDetailType }, ref: ForwardedRef - {isOpenBackupModal && ( - + {isOpenBackupModal && data && ( + )} {!!restoreBackupName && ( - {db.dbName} + {router.query.name || db.dbName} {!isLargeScreen && ( @@ -232,18 +227,6 @@ const Header = ({ onSuccess={() => router.replace('/dbs')} /> )} - {isOpenBackupModal && ( - { - toast({ - status: 'success', - title: '已创建备份任务' - }); - }} - /> - )} ); }; diff --git a/frontend/providers/dbprovider/src/store/static.ts b/frontend/providers/dbprovider/src/store/static.ts index 710c301fa..21794aecd 100644 --- a/frontend/providers/dbprovider/src/store/static.ts +++ b/frontend/providers/dbprovider/src/store/static.ts @@ -14,10 +14,10 @@ let retryGetPrice = 3; let retryVersion = 3; export let DBVersionMap: DBVersionMapType = { - [DBTypeEnum.postgresql]: [], - [DBTypeEnum.mongodb]: [], - [DBTypeEnum.mysql]: [], - [DBTypeEnum.redis]: [] + [DBTypeEnum.postgresql]: [{ id: 'postgresql-14.8.0', label: 'postgresql-14.8.0' }], + [DBTypeEnum.mongodb]: [{ id: 'mongodb-5.0.14', label: 'mongodb-5.0.14' }], + [DBTypeEnum.mysql]: [{ id: 'ac-mysql-8.0.30', label: 'ac-mysql-8.0.30' }], + [DBTypeEnum.redis]: [{ id: 'redis-7.0.6', label: 'redis-7.0.6' }] }; export const getUserPrice = async () => { diff --git a/frontend/providers/dbprovider/src/types/backup.d.ts b/frontend/providers/dbprovider/src/types/backup.d.ts index 7c5ab67cb..3ecbb13d7 100644 --- a/frontend/providers/dbprovider/src/types/backup.d.ts +++ b/frontend/providers/dbprovider/src/types/backup.d.ts @@ -54,3 +54,15 @@ export interface BackupCRItemType { failureReason?: string; }; } + +export type AutoBackupType = 'day' | 'hour' | 'week'; + +export type AutoBackupFormType = { + start: boolean; + type: AutoType; + week: string[]; + hour: string; + minute: string; + saveTime: number; + saveType: string; +}; diff --git a/frontend/providers/dbprovider/src/types/cluster.d.ts b/frontend/providers/dbprovider/src/types/cluster.d.ts index 4364df289..8db183f94 100644 --- a/frontend/providers/dbprovider/src/types/cluster.d.ts +++ b/frontend/providers/dbprovider/src/types/cluster.d.ts @@ -79,3 +79,21 @@ export type KbPodType = { phase: `${PodStatusEnum}`; }; }; + +export type KubeBlockBackupPolicyType = { + metadata: { + name: string; + uid: string; + }; + spec: { + retention: { + ttl: string; + }; + schedule: { + datafile: { + cronExpression: string; + enable: boolean; + }; + }; + }; +}; diff --git a/frontend/providers/dbprovider/src/utils/adapt.ts b/frontend/providers/dbprovider/src/utils/adapt.ts index f28abf0e8..dffd789d7 100644 --- a/frontend/providers/dbprovider/src/utils/adapt.ts +++ b/frontend/providers/dbprovider/src/utils/adapt.ts @@ -1,18 +1,13 @@ import dayjs from 'dayjs'; import type { CoreV1EventList, V1Pod } from '@kubernetes/client-node'; import type { DBListItemType, DBDetailType, DBEditType, PodDetailType, PodEvent } from '@/types/db'; -import { dbStatusMap, podStatusMap } from '@/constants/db'; -import { cpuFormatToM, memoryFormatToMi, storageFormatToNum } from '@/utils/tools'; -import type { KbPgClusterType } from '@/types/cluster'; +import { dbStatusMap } from '@/constants/db'; +import { convertCronTime, cpuFormatToM, memoryFormatToMi, storageFormatToNum } from '@/utils/tools'; +import type { KbPgClusterType, KubeBlockBackupPolicyType } from '@/types/cluster'; import { formatPodTime } from '@/utils/tools'; import type { BackupItemType } from '../types/db'; -import type { BackupCRItemType } from '@/types/backup'; -import { - backupStatusMap, - BackupTypeEnum, - BACKUP_REMARK_LABEL_KEY, - BACKUP_TYPE_LABEL_KEY -} from '@/constants/backup'; +import type { AutoBackupFormType, BackupCRItemType } from '@/types/backup'; +import { backupStatusMap, BackupTypeEnum, BACKUP_REMARK_LABEL_KEY } from '@/constants/backup'; export const adaptDBListItem = (db: KbPgClusterType): DBListItemType => { // compute store amount @@ -112,13 +107,87 @@ export const adaptEvents = (events: CoreV1EventList): PodEvent[] => { }; export const adaptBackup = (backup: BackupCRItemType): BackupItemType => { + const autoLabel = 'dataprotection.kubeblocks.io/autobackup'; return { id: backup.metadata.uid, name: backup.metadata.name, status: backupStatusMap[backup.status.phase] || backupStatusMap.UnKnow, startTime: backup.metadata.creationTimestamp, - type: (backup.metadata.labels[BACKUP_TYPE_LABEL_KEY] as `${BackupTypeEnum}`) || 'UnKnow', + type: autoLabel in backup.metadata.labels ? BackupTypeEnum.auto : BackupTypeEnum.manual, remark: backup.metadata.labels[BACKUP_REMARK_LABEL_KEY] || '-', failureReason: backup.status?.failureReason }; }; + +export const adaptPolicy = (policy: KubeBlockBackupPolicyType): AutoBackupFormType => { + function parseDate(str: string) { + const regex = /(\d+)([a-zA-Z]+)/; + const matches = str.match(regex); + + if (matches && matches.length === 3) { + const number = parseInt(matches[1]); + const unit = matches[2]; + + return { number, unit }; + } + + return { number: 7, unit: 'd' }; + } + function parseCron(str: string) { + const cronFields = convertCronTime(str, 8).split(' '); + const minuteField = cronFields[0]; + const hourField = cronFields[1]; + const weekField = cronFields[4]; + + // week task + if (weekField !== '*') { + return { + hour: hourField.padStart(2, '0'), + minute: minuteField.padStart(2, '0'), + week: weekField.split(','), + type: 'week' + }; + } + console.log(minuteField, hourField, weekField); + + // every day + if (hourField !== '*') { + return { + hour: hourField.padStart(2, '0'), + minute: minuteField.padStart(2, '0'), + week: [], + type: 'day' + }; + } + + // every hour + if (minuteField !== '*') { + return { + hour: '00', + minute: minuteField.padStart(2, '0'), + week: [], + type: 'hour' + }; + } + + return { + hour: '18', + minute: '00', + week: [], + type: 'day' + }; + } + + const { number: saveTime, unit: saveType } = parseDate(policy.spec.retention.ttl); + const { hour, minute, week, type } = parseCron(policy.spec.schedule.datafile.cronExpression); + + return { + start: policy.spec.schedule.datafile.enable, + type, + week, + hour, + minute, + saveTime, + saveType + }; +}; diff --git a/frontend/providers/dbprovider/src/utils/json2Yaml.ts b/frontend/providers/dbprovider/src/utils/json2Yaml.ts index 5f14f938d..07c80409d 100644 --- a/frontend/providers/dbprovider/src/utils/json2Yaml.ts +++ b/frontend/providers/dbprovider/src/utils/json2Yaml.ts @@ -5,12 +5,7 @@ import { DBTypeEnum, DBComponentNameMap, RedisHAConfig } from '@/constants/db'; import { crLabelKey } from '@/constants/db'; import { getUserNamespace } from './user'; import dayjs from 'dayjs'; -import { - BackupTypeEnum, - BACKUP_TYPE_LABEL_KEY, - BACKUP_REMARK_LABEL_KEY, - BACKUP_LABEL_KEY -} from '@/constants/backup'; +import { BACKUP_REMARK_LABEL_KEY, BACKUP_LABEL_KEY } from '@/constants/backup'; export const json2CreateCluster = (data: DBEditType, backupName?: string) => { const resources = { @@ -616,14 +611,12 @@ export const json2Restart = ({ dbName, dbType }: { dbName: string; dbType: DBTyp return yaml.dump(template); }; -export const json2Backup = ({ +export const json2ManualBackup = ({ name, - dbName, backupPolicyName, remark = '' }: { name: string; - dbName: string; backupPolicyName: string; remark?: string; }) => { @@ -633,8 +626,6 @@ export const json2Backup = ({ metadata: { finalizers: ['dataprotection.kubeblocks.io/finalizer'], labels: { - [crLabelKey]: dbName, - [BACKUP_TYPE_LABEL_KEY]: BackupTypeEnum.manual, [BACKUP_REMARK_LABEL_KEY]: remark }, name diff --git a/frontend/providers/dbprovider/src/utils/tools.ts b/frontend/providers/dbprovider/src/utils/tools.ts index 53ea6a951..9c82abbc9 100644 --- a/frontend/providers/dbprovider/src/utils/tools.ts +++ b/frontend/providers/dbprovider/src/utils/tools.ts @@ -1,6 +1,7 @@ import dayjs from 'dayjs'; import { useToast } from '@/hooks/useToast'; import { useTranslation } from 'next-i18next'; +import { format, set, startOfDay, getDay, addHours } from 'date-fns'; /** * copy text data @@ -214,3 +215,32 @@ export const delay = (ms: number) => resolve(''); }, ms); }); + +export const convertCronTime = (cronTime: string, offset: 8 | -8) => { + let [minute, hour, dayOfMonth, month, dayOfWeek] = cronTime.split(' '); + + if (hour === '*') return cronTime; + + const cronDate = set(startOfDay(new Date()), { hours: +hour, minutes: +minute }); + + const newCronDate = addHours(cronDate, offset); + + // 更新 cron 时间表达式的各个部分 + minute = format(newCronDate, 'mm'); + hour = format(newCronDate, 'HH'); + + // 处理星期几 + const daysOfWeek = + dayOfWeek === '*' + ? [dayOfWeek] + : dayOfWeek.split(',').map((day) => { + if (offset < 0) { + const newDay = +day + (+hour >= 16 ? -1 : 0); + return newDay === -1 ? '7' : String(newDay); + } + const newDay = +day + (+hour < 8 ? 1 : 0); + return newDay === 7 ? '0' : String(newDay); + }); + + return `${minute} ${hour} ${dayOfMonth} ${month} ${daysOfWeek.join(',')}`; +};