auto backup (#3486)

* auto backup

* i18n
This commit is contained in:
Archer
2023-07-11 10:46:58 +08:00
committed by GitHub
parent 7df69f23cf
commit b695d97140
21 changed files with 692 additions and 123 deletions
@@ -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",
+9
View File
@@ -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'
@@ -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": "设置自动备份任务成功"
}
@@ -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<AutoBackupFormType>(`/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<AutoBackupFormType>(`/api/backup/updatePolicy`, data);
@@ -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 = <ChevronDownIcon />,
...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 (
<Menu autoSelect={false} onOpen={() => !isDisabled && onOpen()} onClose={onClose}>
<MenuButton as={'span'}>
<Menu
isOpen={isOpen}
autoSelect={false}
onOpen={() => !isDisabled && onOpen()}
onClose={onClose}
>
<MenuButton as={'div'}>
<Button
position={'relative'}
width={width}
px={3}
display={'flex'}
@@ -49,15 +63,18 @@ const MySelect = ({
? {
boxShadow: '0px 0px 4px #A8DBFF',
borderColor: 'myBlue.600',
bg: 'transparent'
bg: 'transparent',
color: 'myGray.800'
}
: {
bg: 'myWhite.300'
})}
{...props}
>
{list?.find((item) => item.id === value)?.label || placeholder}
<ChevronDownIcon />
{t(list?.find((item) => item.id === value)?.label || placeholder || '')}
<Box position={'absolute'} right={3}>
{icon}
</Box>
</Button>
</MenuButton>
{!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) => (
<MenuItem
<Box
key={item.id}
{...menuItemStyles}
{...(value === item.id
@@ -83,10 +102,11 @@ const MySelect = ({
if (onchange && value !== item.id) {
onchange(item.id);
}
onClose();
}}
>
{item.label}
</MenuItem>
{t(item.label)}
</Box>
))}
</MenuList>
)}
@@ -94,4 +114,4 @@ const MySelect = ({
);
};
export default MySelect;
export default React.memo(MySelect);
@@ -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 (
<Grid
border={'1px solid #DEE0E2'}
@@ -46,12 +50,24 @@ const Tabs = ({ list, size = 'md', activeId, onChange, ...props }: Props) => {
fontSize={sizeMap.fontSize}
{...props}
>
{list.map((item) => (
{list.map((item, i) => (
<Box
key={item.id}
py={sizeMap.inlineP}
borderRadius={'sm'}
position={'relative'}
textAlign={'center'}
zIndex={1}
_before={{
content: '""',
position: 'absolute',
right: '0',
top: '50%',
transform: 'translateY(-50%)',
w: i === list.length - 1 || i === activeIndex || i === activeIndex - 1 ? '0' : '1px',
h: '10px',
bg: '#DEE0E2'
}}
{...(activeId === item.id
? {
boxShadow: '0px 2px 2px rgba(137, 156, 171, 0.25)',
@@ -66,7 +82,7 @@ const Tabs = ({ list, size = 'md', activeId, onChange, ...props }: Props) => {
onChange(item.id);
}}
>
{t(item.label)}
<Box>{t(item.label)}</Box>
</Box>
))}
</Grid>
@@ -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,
@@ -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
}
});
@@ -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<ApiResp>) {
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
});
@@ -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 || [];
@@ -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<ApiResp>) {
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) => {};
@@ -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<ApiResp>) {
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
});
}
}
@@ -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<AutoBackupFormType>({
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 = ({
<ModalBody display={'flex'} pb={8}>
<Box flex={'0 0 220px'} pr={4} borderRight={theme.borders.sm} borderRightWidth={'2px'}>
<Box {...navStyle(NavEnum.manual)}>{t('Manual Backup')}</Box>
{/* <Box {...navStyle(NavEnum.auto)}>{t('Auto Backup')}</Box> */}
</Box>
{currentNav === NavEnum.manual && (
<Flex flexDirection={'column'} px={'50px'}>
<Tip
icon={<InfoOutlineIcon fontSize={'16px'} />}
size="sm"
text={t('Manual Backup Tip')}
/>
<Box flex={1}>
<Flex mt={8} alignItems={'center'}>
<Box flex={'0 0 80px'}>{t('Backup Name')}</Box>
<Input
maxW={'300px'}
bg={'myWhite.300'}
{...manualRegister('backupName', {
required: t('Backup Name cannot empty') || 'Backup Name cannot empty'
})}
/>
</Flex>
<Flex mt={7} alignItems={'center'}>
<Box flex={'0 0 80px'}>{t('Remark')}</Box>
<Input maxW={'300px'} bg={'myWhite.300'} {...manualRegister('remark')} />
</Flex>
</Box>
<Box mt={6} textAlign={'end'}>
<Button
isLoading={isLoading}
variant={'primary'}
onClick={() => handleSubmitManual(openConfirm(onclickBackup))()}
>
{t('Start Backup')}
</Button>
</Box>
<Flex {...navStyle(NavEnum.auto)} alignItems={'center'}>
<Box flex={1}>{t('Auto Backup')}</Box>
<Switch
variant={'deepLight'}
isChecked={getAutoValues('start')}
onChange={(e) => {
setAutoValue('start', e.target.checked);
setRefresh((state) => !state);
}}
></Switch>
</Flex>
)}
</Box>
<Flex flexDirection={'column'} px={'50px'} position={'relative'}>
<Tip
icon={<InfoOutlineIcon fontSize={'16px'} />}
size="sm"
text={t('Manual Backup Tip')}
/>
{currentNav === NavEnum.manual && (
<>
<Box flex={1} mt={8}>
<Flex alignItems={'center'}>
<Box flex={'0 0 80px'}>{t('Backup Name')}</Box>
<Input
maxW={'300px'}
bg={'myWhite.300'}
{...manualRegister('backupName', {
required: t('Backup Name cannot empty') || 'Backup Name cannot empty'
})}
/>
</Flex>
<Flex mt={7} alignItems={'center'}>
<Box flex={'0 0 80px'}>{t('Remark')}</Box>
<Input maxW={'300px'} bg={'myWhite.300'} {...manualRegister('remark')} />
</Flex>
</Box>
<Box textAlign={'end'}>
<Button
isLoading={isLoading}
variant={'primary'}
onClick={() => handleSubmitManual(openConfirm(onclickBackup))()}
>
{t('Start Backup')}
</Button>
</Box>
</>
)}
{currentNav === NavEnum.auto && (
<>
<Box flex={1} mt={8} userSelect={'none'}>
<Flex alignItems={'center'}>
<Box flex={'0 0 110px'}>{t('CronExpression')}</Box>
<Tabs
w={'220px'}
list={[
{ id: 'hour', label: 'Hour' },
{ id: 'day', label: 'Day' },
{ id: 'week', label: 'Week' }
]}
activeId={getAutoValues('type')}
size={'sm'}
borderColor={'myGray.200'}
onChange={(e) => {
setAutoValue('type', e as AutoBackupType);
setRefresh((state) => !state);
}}
/>
</Flex>
{getAutoValues('type') === 'week' && (
<Flex mt={4}>
{weekSelectList.current.map((item) => (
<Box key={item.id} _notLast={{ mr: 4 }}>
<Checkbox
defaultChecked={getAutoValues('week').includes(item.id)}
onChange={(e) => {
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)}
</Checkbox>
</Box>
))}
</Flex>
)}
{getAutoValues('type') !== 'hour' && (
<Flex mt={7} alignItems={'center'}>
<Box flex={'0 0 110px'}>{t('Start Hour')}</Box>
<MySelect
width={'120px'}
value={getAutoValues('hour')}
list={selectTimeList.current.slice(0, 24)}
icon={<TimeIcon color={'myGray.400'} />}
onchange={(val: any) => {
setAutoValue('hour', val);
setRefresh((state) => !state);
}}
/>
</Flex>
)}
<Flex mt={7} alignItems={'center'}>
<Box flex={'0 0 110px'}>{t('Start Minute')}</Box>
<MySelect
width={'120px'}
value={getAutoValues('minute')}
list={selectTimeList.current}
icon={<TimeIcon color={'myGray.400'} />}
onchange={(val: any) => {
setAutoValue('minute', val);
setRefresh((state) => !state);
}}
/>
</Flex>
<Flex mt={7} alignItems={'center'}>
<Box flex={'0 0 110px'}>{t('SaveTime')}</Box>
<Input
maxW={'100px'}
bg={'myWhite.300'}
borderTopRightRadius={0}
borderBottomRightRadius={0}
_focus={{
boxShadow: 'none',
borderColor: 'myGray.200',
bg: 'white'
}}
{...autoRegister('saveTime', {
min: 1,
valueAsNumber: true
})}
/>
<MySelect
width={'80px'}
value={getAutoValues('saveType')}
borderLeft={0}
boxShadow={'none !important'}
borderColor={'myGray.200'}
list={[
{ id: 'd', label: 'Day' },
{ id: 'h', label: 'Hour' }
]}
h={'35px'}
borderTopLeftRadius={0}
borderBottomLeftRadius={0}
onchange={(val: any) => {
setAutoValue('saveType', val);
setRefresh((state) => !state);
}}
/>
</Flex>
</Box>
<Box textAlign={'end'}>
<Button
isLoading={isLoadingSetAutoBackup}
variant={'primary'}
// @ts-ignore
onClick={() => handleSubmitAuto(onclickSetAutoBackup)()}
>
{t('Save')}
</Button>
</Box>
{!getAutoValues('start') && (
<Box
zIndex={1}
position={'absolute'}
top={0}
right={0}
bottom={0}
left={0}
bg={'rgba(255,255,255,0.6)'}
/>
)}
</>
)}
</Flex>
</ModalBody>
</ModalContent>
</Modal>
@@ -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<ComponentRe
backupProcessing
}));
const { data, refetch: refetchPolicy } = useQuery(['initpolicy', db.dbName, db.dbType], () =>
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 (
<Box h={'100%'} position={'relative'}>
<TableContainer overflow={'overlay'}>
@@ -226,8 +242,14 @@ const BackupTable = ({ db }: { db: DBDetailType }, ref: ForwardedRef<ComponentRe
</TableContainer>
<Loading loading={isInitialLoading} fixed={false} />
<RestartConfirmDelChild />
{isOpenBackupModal && (
<BackupModal dbName={db.dbName} onSuccess={refetch} onClose={onCloseBackupModal} />
{isOpenBackupModal && data && (
<BackupModal
dbName={db.dbName}
dbType={db.dbType}
defaultVal={data}
onClose={onCloseBackupModal}
refetchPolicy={refetchPolicy}
/>
)}
{!!restoreBackupName && (
<RestoreModal
@@ -31,11 +31,6 @@ const Header = ({
onOpen: onOpenDelModal,
onClose: onCloseDelModal
} = useDisclosure();
const {
isOpen: isOpenBackupModal,
onOpen: onOpenBackupModal,
onClose: onCloseBackupModal
} = useDisclosure();
const { openConfirm: openRestartConfirm, ConfirmChild: RestartConfirmChild } = useConfirm({
content: t('Confirm Restart')
});
@@ -110,7 +105,7 @@ const Header = ({
<MyIcon name="arrowLeft" />
</Button>
<Box mx={5} fontSize={'3xl'} fontWeight={'bold'}>
{db.dbName}
{router.query.name || db.dbName}
</Box>
<DBStatusTag status={db.status} conditions={db.conditions} showBorder />
{!isLargeScreen && (
@@ -232,18 +227,6 @@ const Header = ({
onSuccess={() => router.replace('/dbs')}
/>
)}
{isOpenBackupModal && (
<BackupModal
dbName={db.dbName}
onClose={onCloseBackupModal}
onSuccess={() => {
toast({
status: 'success',
title: '已创建备份任务'
});
}}
/>
)}
</Flex>
);
};
@@ -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 () => {
+12
View File
@@ -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;
};
+18
View File
@@ -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;
};
};
};
};
@@ -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
};
};
@@ -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
@@ -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(',')}`;
};