mirror of
https://github.com/labring/sealos.git
synced 2026-09-24 15:46:19 +08:00
@@ -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
@@ -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 () => {
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
@@ -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(',')}`;
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user