From be0d1d19bc3182449a9e28e7dea02f56b8e24e44 Mon Sep 17 00:00:00 2001 From: kkannan Date: Wed, 11 Sep 2024 18:58:50 +0530 Subject: [PATCH] [PLAT-15247][Platform][Backup]Create Backup scheduled policy List Summary: This diff implements Backup scheduled policy list. To support ifinite scroll, we've added 'react-infinite-scroll-component'. The feature is currently under the feature flag called 'enableBackupPITR' and it is disabled by default. The following operations are also supported. 1. editing Policy 2. Deleting Policy 3. Show Intervals 4. Show tables that are backed up [[ https://www.figma.com/design/DdKb47p9sREh4L1CvBHGYq/PITR?node-id=4450-96923&node-type=frame&m=dev | FIGMA ]] Test Plan: Tested manually {F283955} {F283956} {F283957} {F283958} {F283959} Reviewers: lsangappa Reviewed By: lsangappa Subscribers: yugaware Differential Revision: https://phorge.dev.yugabyte.com/D37939 --- managed/ui/package-lock.json | 35 ++ managed/ui/package.json | 1 + .../backupv2/common/IBackupSchedule.ts | 1 + .../backup/scheduled/ScheduledBackupList.tsx | 133 +++++- .../features/backup/scheduled/api/api.ts | 40 +- .../BackupFrequency/BackupFrequencyField.tsx | 4 +- .../pages/BackupSummary/BackupSummary.tsx | 10 +- .../list/EditScheduledPolicyModal.tsx | 206 +++++++++ .../ScheduledBackupShowIntervalsModal.tsx | 111 +++++ .../backup/scheduled/list/ScheduledCard.tsx | 408 ++++++++++++++++++ .../list/ScheduledPolicyShowTables.tsx | 59 +++ managed/ui/src/translations/en.json | 38 +- 12 files changed, 1023 insertions(+), 23 deletions(-) create mode 100644 managed/ui/src/redesign/features/backup/scheduled/list/EditScheduledPolicyModal.tsx create mode 100644 managed/ui/src/redesign/features/backup/scheduled/list/ScheduledBackupShowIntervalsModal.tsx create mode 100644 managed/ui/src/redesign/features/backup/scheduled/list/ScheduledCard.tsx create mode 100644 managed/ui/src/redesign/features/backup/scheduled/list/ScheduledPolicyShowTables.tsx diff --git a/managed/ui/package-lock.json b/managed/ui/package-lock.json index 66eb3691b975..62493912dad0 100644 --- a/managed/ui/package-lock.json +++ b/managed/ui/package-lock.json @@ -65,6 +65,7 @@ "react-fa": "5.0.0", "react-hook-form": "7.40.0", "react-i18next": "11.18.6", + "react-infinite-scroll-component": "6.1.0", "react-intl": "2.9.0", "react-leaflet": "^1.9.1", "react-measure": "1.4.7", @@ -30378,6 +30379,25 @@ } } }, + "node_modules/react-infinite-scroll-component": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/react-infinite-scroll-component/-/react-infinite-scroll-component-6.1.0.tgz", + "integrity": "sha512-SQu5nCqy8DxQWpnUVLx7V7b7LcA37aM7tvoWjTLZp1dk6EJibM5/4EJKzOnl07/BsM1Y40sKLuqjCwwH/xV0TQ==", + "dependencies": { + "throttle-debounce": "^2.1.0" + }, + "peerDependencies": { + "react": ">=16.0.0" + } + }, + "node_modules/react-infinite-scroll-component/node_modules/throttle-debounce": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/throttle-debounce/-/throttle-debounce-2.3.0.tgz", + "integrity": "sha512-H7oLPV0P7+jgvrk+6mwwwBDmxTaxnu9HMXmloNLXwnNO0ZxZ31Orah2n8lU1eMPvsaowP2CX+USCgyovXfdOFQ==", + "engines": { + "node": ">=8" + } + }, "node_modules/react-input-autosize": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/react-input-autosize/-/react-input-autosize-3.0.0.tgz", @@ -64578,6 +64598,21 @@ "html-parse-stringify": "^3.0.1" } }, + "react-infinite-scroll-component": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/react-infinite-scroll-component/-/react-infinite-scroll-component-6.1.0.tgz", + "integrity": "sha512-SQu5nCqy8DxQWpnUVLx7V7b7LcA37aM7tvoWjTLZp1dk6EJibM5/4EJKzOnl07/BsM1Y40sKLuqjCwwH/xV0TQ==", + "requires": { + "throttle-debounce": "^2.1.0" + }, + "dependencies": { + "throttle-debounce": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/throttle-debounce/-/throttle-debounce-2.3.0.tgz", + "integrity": "sha512-H7oLPV0P7+jgvrk+6mwwwBDmxTaxnu9HMXmloNLXwnNO0ZxZ31Orah2n8lU1eMPvsaowP2CX+USCgyovXfdOFQ==" + } + } + }, "react-input-autosize": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/react-input-autosize/-/react-input-autosize-3.0.0.tgz", diff --git a/managed/ui/package.json b/managed/ui/package.json index 48a39b122c11..a0e3502f2255 100644 --- a/managed/ui/package.json +++ b/managed/ui/package.json @@ -119,6 +119,7 @@ "react-fa": "5.0.0", "react-hook-form": "7.40.0", "react-i18next": "11.18.6", + "react-infinite-scroll-component": "6.1.0", "react-intl": "2.9.0", "react-leaflet": "^1.9.1", "react-measure": "1.4.7", diff --git a/managed/ui/src/components/backupv2/common/IBackupSchedule.ts b/managed/ui/src/components/backupv2/common/IBackupSchedule.ts index 7552f555ca19..f97deecf1458 100644 --- a/managed/ui/src/components/backupv2/common/IBackupSchedule.ts +++ b/managed/ui/src/components/backupv2/common/IBackupSchedule.ts @@ -28,6 +28,7 @@ interface ScheduleTaskParams { keyspaceList: IBackup['commonBackupInfo']['responseList']; isTableByTableBackup: IBackup['isTableByTableBackup'] expiryTimeUnit: string; + pointInTimeRestoreEnabled?: boolean; } export enum IBackupScheduleStatus { diff --git a/managed/ui/src/redesign/features/backup/scheduled/ScheduledBackupList.tsx b/managed/ui/src/redesign/features/backup/scheduled/ScheduledBackupList.tsx index fd3d62144804..12d0614e29d3 100644 --- a/managed/ui/src/redesign/features/backup/scheduled/ScheduledBackupList.tsx +++ b/managed/ui/src/redesign/features/backup/scheduled/ScheduledBackupList.tsx @@ -7,37 +7,138 @@ * http://github.com/YugaByte/yugabyte-db/blob/master/licenses/POLYFORM-FREE-TRIAL-LICENSE-1.0.0.txt */ -import { FC } from 'react'; +import { FC, useMemo } from 'react'; +import { useInfiniteQuery } from 'react-query'; +import { keyBy } from 'lodash'; +import { useTranslation } from 'react-i18next'; +import { useSelector } from 'react-redux'; import { useToggle } from 'react-use'; +import { Grid, makeStyles } from '@material-ui/core'; +import { YBButton } from '../../../components'; +import { YBLoadingCircleIcon } from '../../../../components/common/indicators'; +import InfiniteScroll from 'react-infinite-scroll-component'; import CreateScheduledBackupModal from './create/CreateScheduledBackupModal'; +import { BACKUP_REFETCH_INTERVAL } from '../../../../components/backupv2/common/BackupUtils'; +import { ScheduledCard } from './list/ScheduledCard'; import { ScheduledBackupEmpty } from './ScheduledBackupEmpty'; import { AllowedTasks } from '../../../helpers/dtos'; +import { getScheduledBackupList } from '../../../../components/backupv2/common/BackupScheduleAPI'; interface ScheduledBackupListProps { universeUUID: string; allowedTasks: AllowedTasks; } -const ScheduledBackupList: FC = () => { +const useStyles = makeStyles((theme) => ({ + noMoreBackup: { + display: 'flex', + justifyContent: 'center', + marginTop: '32px', + opacity: 0.5 + }, + cardList: { + display: 'flex', + flexDirection: 'column', + gap: '32px' + } +})); + +const ScheduledBackupList: FC = ({ universeUUID }) => { const [createScheduledBackupModalVisible, toggleCreateScheduledBackupModalVisible] = useToggle( false ); + const { data: scheduledBackupList, isLoading, fetchNextPage, hasNextPage } = useInfiniteQuery( + ['scheduled_backup_list'], + ({ pageParam = 0 }) => getScheduledBackupList(pageParam, universeUUID), + { + getNextPageParam: (lastPage) => lastPage.data.hasNext, + refetchInterval: BACKUP_REFETCH_INTERVAL + } + ); + + const storageConfigs = useSelector((reduxState: any) => reduxState.customer.configs); + + const storageConfigsMap = useMemo(() => keyBy(storageConfigs?.data ?? [], 'configUUID'), [ + storageConfigs + ]); + + const { t } = useTranslation('translation', { + keyPrefix: 'backup.scheduled.list' + }); + + const classes = useStyles(); + + if (isLoading) { + return ; + } + + const schedules = scheduledBackupList?.pages + .flatMap((page) => { + return page.data.entities; + }) + .filter( + (schedule) => + schedule.backupInfo !== undefined && schedule.backupInfo.universeUUID === universeUUID + ); + + if (!schedules || schedules?.length === 0) { + return ( +
+ { + toggleCreateScheduledBackupModalVisible(true); + }} + /> + { + toggleCreateScheduledBackupModalVisible(false); + }} + /> +
+ ); + } + return ( -
- { - toggleCreateScheduledBackupModalVisible(true); - }} - /> - { - toggleCreateScheduledBackupModalVisible(false); - }} - /> -
+ } + endMessage={
{t('noMoreSchedules')}
} + height={'550px'} + > +
+ + + { + toggleCreateScheduledBackupModalVisible(true); + }} + > + {t('createPolicy')} + + + + {schedules.map((schedule) => ( + + ))} + { + toggleCreateScheduledBackupModalVisible(false); + }} + /> +
+
); }; diff --git a/managed/ui/src/redesign/features/backup/scheduled/api/api.ts b/managed/ui/src/redesign/features/backup/scheduled/api/api.ts index f7c01078628e..571098a8590a 100644 --- a/managed/ui/src/redesign/features/backup/scheduled/api/api.ts +++ b/managed/ui/src/redesign/features/backup/scheduled/api/api.ts @@ -17,10 +17,48 @@ import { ROOT_URL } from '../../../../../config'; * @param payload - The backup information. * @returns A promise that resolves to the taskUUID of the backup schedule creation. */ -export const createScheduledBackup = (payload: IBackupSchedule['backupInfo']) => { +export const createScheduledBackupPolicy = (payload: IBackupSchedule['backupInfo']) => { const customerUUID = localStorage.getItem('customerId'); return axios.post(`${ROOT_URL}/customers/${customerUUID}/create_backup_schedule_async`, { ...payload, customerUUID }); }; + +// toggle on/off scheduled backup policy +export const toggleScheduledBackupPolicy = ( + universeUUID: string, + values: Partial & Pick +) => { + const customerUUID = localStorage.getItem('customerId'); + + if (values['cronExpression']) { + delete values['frequency']; + } + + return axios.put( + `${ROOT_URL}/customers/${customerUUID}/universes/${universeUUID}/schedules/${values['scheduleUUID']}/pause_resume`, + values + ); +}; + +// delete scheduled backup policy +export const deleteSchedulePolicy = (universeUUID: string, scheduledPolicyUUID: string) => { + const customerUUID = localStorage.getItem('customerId'); + + return axios.delete( + `${ROOT_URL}/customers/${customerUUID}/universes/${universeUUID}/schedules/${scheduledPolicyUUID}/delete_backup_schedule_async` + ); +}; + +// edit scheduled backup policy +export const editScheduledBackupPolicy = ( + universeUUID: string, + values: Partial & Pick +) => { + const customerUUID = localStorage.getItem('customerId'); + return axios.put( + `${ROOT_URL}/customers/${customerUUID}/universes/${universeUUID}/schedules/${values['scheduleUUID']}/edit_backup_schedule_async`, + values + ); +}; diff --git a/managed/ui/src/redesign/features/backup/scheduled/create/pages/BackupFrequency/BackupFrequencyField.tsx b/managed/ui/src/redesign/features/backup/scheduled/create/pages/BackupFrequency/BackupFrequencyField.tsx index ff960b38d334..dee26eed8e61 100644 --- a/managed/ui/src/redesign/features/backup/scheduled/create/pages/BackupFrequency/BackupFrequencyField.tsx +++ b/managed/ui/src/redesign/features/backup/scheduled/create/pages/BackupFrequency/BackupFrequencyField.tsx @@ -21,6 +21,7 @@ import { BackupFrequencyModel, TimeUnit } from '../../models/IBackupFrequency'; type BackupFrequencyFieldProps = { control: Control; + isEditMode?: boolean; }; const useStyles = makeStyles((theme) => ({ @@ -74,7 +75,7 @@ const useStyles = makeStyles((theme) => ({ } })); -const BackupFrequencyField: FC = ({ control }) => { +const BackupFrequencyField: FC = ({ control, isEditMode = false }) => { const classes = useStyles(); const { t } = useTranslation('translation', { @@ -139,6 +140,7 @@ const BackupFrequencyField: FC = ({ control }) => { control={control} name="useIncrementalBackup" data-testid="useIncrementalBackup" + disabled={isEditMode} /> {useIncrementalBackupVal && (
diff --git a/managed/ui/src/redesign/features/backup/scheduled/create/pages/BackupSummary/BackupSummary.tsx b/managed/ui/src/redesign/features/backup/scheduled/create/pages/BackupSummary/BackupSummary.tsx index 17f814c32b35..1e2d299282df 100644 --- a/managed/ui/src/redesign/features/backup/scheduled/create/pages/BackupSummary/BackupSummary.tsx +++ b/managed/ui/src/redesign/features/backup/scheduled/create/pages/BackupSummary/BackupSummary.tsx @@ -9,7 +9,7 @@ import { forwardRef, Fragment, useContext, useImperativeHandle } from 'react'; import cronstrue from 'cronstrue'; -import { useMutation } from 'react-query'; +import { useMutation, useQueryClient } from 'react-query'; import { toast } from 'react-toastify'; import { values } from 'lodash'; import { Trans, useTranslation } from 'react-i18next'; @@ -28,7 +28,7 @@ import { BACKUP_API_TYPES } from '../../../../../../../components/backupv2'; import { GetUniverseUUID, prepareScheduledBackupPayload } from '../../../ScheduledBackupUtils'; -import { createScheduledBackup } from '../../../api/api'; +import { createScheduledBackupPolicy } from '../../../api/api'; import { ReactComponent as CheckIcon } from '../../../../../../assets/check-white.svg'; import BulbIcon from '../../../../../../assets/bulb.svg'; @@ -100,9 +100,10 @@ const BackupSummary = forwardRef((_, forwardRef) => { const classes = useStyles(); const universeUUID = GetUniverseUUID(); + const queryClient = useQueryClient(); const doCreateScheduledBackup = useMutation( - (payload: ExtendedBackupScheduleProps) => createScheduledBackup(payload), + (payload: ExtendedBackupScheduleProps) => createScheduledBackupPolicy(payload), { onSuccess: () => { toast.success( @@ -112,6 +113,7 @@ const BackupSummary = forwardRef((_, forwardRef) => { ); hideModal(); + queryClient.invalidateQueries('scheduled_backup_list'); }, onError: (error: any) => { toast.error(error?.response?.data?.error || t('errorMsg')); @@ -192,7 +194,7 @@ const BackupSummary = forwardRef((_, forwardRef) => { { name: t(dbType), value: backupObjects.keyspace?.isDefaultOption - ? t(apiType === 'Ysql' ? 'allDatabases' : 'allKeyspaces', { keyPrefix: 'backup' }) + ? t(apiType === 'Ysql' ? 'allDatabase' : 'allKeyspaces', { keyPrefix: 'backup' }) : backupObjects.keyspace?.label }, { diff --git a/managed/ui/src/redesign/features/backup/scheduled/list/EditScheduledPolicyModal.tsx b/managed/ui/src/redesign/features/backup/scheduled/list/EditScheduledPolicyModal.tsx new file mode 100644 index 000000000000..ed24b1c5cc7b --- /dev/null +++ b/managed/ui/src/redesign/features/backup/scheduled/list/EditScheduledPolicyModal.tsx @@ -0,0 +1,206 @@ +/* + * Created on Mon Sep 09 2024 + * + * Copyright 2021 YugaByte, Inc. and Contributors + * Licensed under the Polyform Free Trial License 1.0.0 (the "License") + * You may not use this file except in compliance with the License. You may obtain a copy of the License at + * http://github.com/YugaByte/yugabyte-db/blob/master/licenses/POLYFORM-FREE-TRIAL-LICENSE-1.0.0.txt + */ + +import { FC } from 'react'; +import { Control, useForm } from 'react-hook-form'; +import { useMutation, useQueryClient } from 'react-query'; +import { Trans, useTranslation } from 'react-i18next'; +import { capitalize } from 'lodash'; +import { toast } from 'react-toastify'; +import { makeStyles, MenuItem, Typography } from '@material-ui/core'; +import { YBModal, YBSelectField } from '../../../../components'; +import BackupFrequencyField from '../create/pages/BackupFrequency/BackupFrequencyField'; +import { MILLISECONDS_IN } from '../../../../../components/backupv2/scheduled/ScheduledBackupUtils'; +import { createErrorMessage } from '../../../../../utils/ObjectUtils'; +import { editScheduledBackupPolicy } from '../api/api'; +import { IBackupSchedule } from '../../../../../components/backupv2'; +import { + BackupFrequencyModel, + BackupStrategyType, + TimeUnit +} from '../create/models/IBackupFrequency'; + +interface EditScheduledPolicyModalProps { + visible: boolean; + onHide: () => void; + universeUUID: string; + schedule: IBackupSchedule; +} + +interface EditScheduledPolicyModalFormProps extends BackupFrequencyModel { + scheduledBackupType: BackupStrategyType; +} + +const useStyles = makeStyles((theme) => ({ + root: { + padding: '24px 20px' + }, + menuItem: { + height: '72px' + }, + unorderedList: { + listStyleType: 'disc', + color: theme.palette.ybacolors.textDarkGray, + paddingLeft: '20px', + marginTop: '8px' + }, + strategyOptions: { + display: 'flex', + flexDirection: 'column' + }, + selectStrategy: { + marginBottom: '40px', + width: '400px' + } +})); + +const EditScheduledPolicyModal: FC = ({ + visible, + onHide, + universeUUID, + schedule +}) => { + const { t } = useTranslation('translation', { keyPrefix: 'backup.scheduled.list.editModal' }); + const backupFrequencyKeyPrefix = 'backup.scheduled.create.backupFrequency'; + const classes = useStyles(); + const queryClient = useQueryClient(); + const { control, handleSubmit } = useForm({ + defaultValues: { + scheduledBackupType: schedule.backupInfo.pointInTimeRestoreEnabled + ? BackupStrategyType.POINT_IN_TIME + : BackupStrategyType.STANDARD, + frequency: schedule.frequency + ? schedule.frequency / (MILLISECONDS_IN as any)[schedule.frequencyTimeUnit] + : 0, + frequencyTimeUnit: capitalize(schedule.frequencyTimeUnit) as TimeUnit, + useCronExpression: schedule.cronExpression ? true : false, + cronExpression: schedule.cronExpression, + useIncrementalBackup: !!schedule.incrementalBackupFrequency, + incrementalBackupFrequency: schedule.incrementalBackupFrequency + ? schedule.incrementalBackupFrequency / + (MILLISECONDS_IN as any)[schedule.incrementalBackupFrequencyTimeUnit] + : 0, + incrementalBackupFrequencyTimeUnit: capitalize( + schedule.incrementalBackupFrequencyTimeUnit + ) as TimeUnit + } + }); + + const doEditBackupSchedule = useMutation( + (values: IBackupSchedule) => editScheduledBackupPolicy(universeUUID, values), + { + onSuccess: () => { + toast.success(t('success')); + queryClient.invalidateQueries('scheduled_backup_list'); + onHide(); + }, + onError: (error) => { + toast.error(createErrorMessage(error)); + } + } + ); + + if (!visible) return null; + + const backupTypeOptions = [ + +
+ + {t('standard.title', { keyPrefix: backupFrequencyKeyPrefix })} + +
    + }} + i18nKey={`${backupFrequencyKeyPrefix}.standard.helpText`} + /> +
+
+
, + +
+ + {t('pitr.title', { keyPrefix: backupFrequencyKeyPrefix })} + + +
+
+ ]; + + return ( + { + handleSubmit((data) => { + const payload: Record = {}; + if (data.useCronExpression) { + payload['cronExpression'] = data.cronExpression; + } else { + payload['frequencyTimeUnit'] = data.frequencyTimeUnit.toUpperCase(); + payload['schedulingFrequency'] = + (MILLISECONDS_IN as Record)[data.frequencyTimeUnit.toUpperCase()] * + data.frequency; + } + + if (data.useIncrementalBackup) { + payload['incrementalBackupFrequency'] = + (MILLISECONDS_IN as Record)[ + data.incrementalBackupFrequencyTimeUnit.toUpperCase() + ] * data.incrementalBackupFrequency; + payload[ + 'incrementalBackupFrequencyTimeUnit' + ] = data.incrementalBackupFrequencyTimeUnit.toUpperCase(); + } + doEditBackupSchedule.mutate({ + scheduleUUID: schedule.scheduleUUID, + ...payload + } as any); + })(); + }} + > +
+ + t(val === BackupStrategyType.STANDARD ? 'standard.title' : 'pitr.title', { + keyPrefix: backupFrequencyKeyPrefix + }) + } + className={classes.selectStrategy} + disabled + > + {...backupTypeOptions} + + } + isEditMode + /> +
+
+ ); +}; + +export default EditScheduledPolicyModal; diff --git a/managed/ui/src/redesign/features/backup/scheduled/list/ScheduledBackupShowIntervalsModal.tsx b/managed/ui/src/redesign/features/backup/scheduled/list/ScheduledBackupShowIntervalsModal.tsx new file mode 100644 index 000000000000..9d3cb58ea78a --- /dev/null +++ b/managed/ui/src/redesign/features/backup/scheduled/list/ScheduledBackupShowIntervalsModal.tsx @@ -0,0 +1,111 @@ +/* + * Created on Tue Sep 10 2024 + * + * Copyright 2021 YugaByte, Inc. and Contributors + * Licensed under the Polyform Free Trial License 1.0.0 (the "License") + * You may not use this file except in compliance with the License. You may obtain a copy of the License at + * http://github.com/YugaByte/yugabyte-db/blob/master/licenses/POLYFORM-FREE-TRIAL-LICENSE-1.0.0.txt + */ + +import { FC } from 'react'; +import cronstrue from 'cronstrue'; +import { useTranslation } from 'react-i18next'; +import { makeStyles } from '@material-ui/core'; +import { YBModal } from '../../../../components'; +import { convertMsecToTimeFrame } from '../../../../../components/backupv2/scheduled/ScheduledBackupUtils'; +import { IBackupSchedule } from '../../../../../components/backupv2'; + +interface ScheduledBackupShowIntervalsModalProps { + schedule: IBackupSchedule; + visible: boolean; + onHide: () => void; +} + +const useStyles = makeStyles((theme) => ({ + root: { + padding: '24px' + }, + panel: { + width: '100%', + padding: '16px', + border: `1px solid ${theme.palette.ybacolors.ybBorderGray}`, + background: '#FBFBFB', + borderRadius: '8px', + display: 'flex', + gap: '60px' + }, + attribute: { + fontWeight: 500, + fontSize: '11.5px', + textTransform: 'uppercase', + color: theme.palette.grey[600], + '&>div': { + marginBottom: '8px' + } + }, + value: { + fontSize: '13px', + fontWeight: 600, + color: theme.palette.grey[900] + } +})); + +const ScheduledBackupShowIntervalsModal: FC = ({ + schedule, + onHide, + visible +}) => { + const { t } = useTranslation('translation', { + keyPrefix: 'backup.scheduled.list.showIntervalModal' + }); + const classes = useStyles(); + + let backupInterval = ''; + + if (schedule.cronExpression) { + backupInterval = cronstrue.toString(schedule.cronExpression); + } else { + backupInterval = convertMsecToTimeFrame( + schedule.frequency, + schedule.frequencyTimeUnit, + 'Every ' + ); + } + + const incrementalBackup = + schedule.incrementalBackupFrequency === 0 + ? '-' + : convertMsecToTimeFrame( + schedule.incrementalBackupFrequency, + schedule.incrementalBackupFrequencyTimeUnit, + 'Every ' + ); + + return ( + +
+
+
{t('fullBackup')}
+
{t('incrementalBackup')}
+
+
+
{backupInterval}
+
{incrementalBackup}
+
+
+
+ ); +}; + +export default ScheduledBackupShowIntervalsModal; diff --git a/managed/ui/src/redesign/features/backup/scheduled/list/ScheduledCard.tsx b/managed/ui/src/redesign/features/backup/scheduled/list/ScheduledCard.tsx new file mode 100644 index 000000000000..37ce38062059 --- /dev/null +++ b/managed/ui/src/redesign/features/backup/scheduled/list/ScheduledCard.tsx @@ -0,0 +1,408 @@ +/* + * Created on Mon Sep 09 2024 + * + * Copyright 2021 YugaByte, Inc. and Contributors + * Licensed under the Polyform Free Trial License 1.0.0 (the "License") + * You may not use this file except in compliance with the License. You may obtain a copy of the License at + * http://github.com/YugaByte/yugabyte-db/blob/master/licenses/POLYFORM-FREE-TRIAL-LICENSE-1.0.0.txt + */ + +import { FC, useState } from 'react'; +import { useMutation, useQueryClient } from 'react-query'; +import { Trans, useTranslation } from 'react-i18next'; +import { toast } from 'react-toastify'; +import clsx from 'clsx'; +import cronstrue from 'cronstrue'; + +import { Grid, makeStyles, Typography } from '@material-ui/core'; +import { DropdownButton, MenuItem } from 'react-bootstrap'; +import { YBToggle } from '../../../../components'; +import { YBConfirmModal } from '../../../../../components/modals'; +import EditScheduledPolicyModal from './EditScheduledPolicyModal'; +import ScheduledBackupShowIntervalsModal from './ScheduledBackupShowIntervalsModal'; +import ScheduledPolicyShowTables from './ScheduledPolicyShowTables'; +import { convertMsecToTimeFrame } from '../../../../../components/backupv2/scheduled/ScheduledBackupUtils'; +import { ybFormatDate } from '../../../../helpers/DateUtils'; +import { RbacValidator } from '../../../rbac/common/RbacApiPermValidator'; +import { ApiPermissionMap } from '../../../rbac/ApiAndUserPermMapping'; +import { deleteSchedulePolicy, toggleScheduledBackupPolicy } from '../api/api'; +import { TableTypeLabel } from '../../../../helpers/dtos'; +import { YBTag, YBTag_Types } from '../../../../../components/common/YBTag'; +import { IBackupSchedule, IBackupScheduleStatus } from '../../../../../components/backupv2'; + +interface ScheduledCardProps { + schedule: IBackupSchedule; + universeUUID: string; + storageConfig: Record | undefined; +} +const useStyles = makeStyles((theme) => ({ + card: { + borderRadius: '8px', + border: `1px solid ${theme.palette.ybacolors.ybBorderGray}`, + boxShadow: `0px 4px 10px 0px rgba(0, 0, 0, 0.05)` + }, + header: { + height: '56px', + display: 'flex', + justifyContent: 'space-between', + padding: '8px 24px', + borderBottom: `1px solid ${theme.palette.ybacolors.ybBorderGray}`, + '&>.dropdown>button': { + height: '40px' + } + }, + title: { + fontWeight: 600, + gap: '6px', + '& .yb-tag': { + margin: 0 + }, + '&>p': { + fontSize: '16px' + }, + '&>.MuiFormControl-root': { + marginLeft: '24px' + } + }, + content: { + padding: '24px', + display: 'flex', + justifyContent: 'space-between', + borderBottom: `1px solid ${theme.palette.ybacolors.ybBorderGray}` + }, + attribute: { + textTransform: 'uppercase', + fontWeight: 500, + fontSize: '11.5px', + color: theme.palette.grey[600], + lineHeight: '16px' + }, + value: { + fontSize: '13px', + fontWeight: 400, + color: theme.palette.grey[900], + marginTop: '6px' + }, + details: { + borderRadius: '8px', + background: '#FBFBFB', + border: `1px solid ${theme.palette.ybacolors.ybBorderGray}`, + width: '600px', + padding: '16px', + display: 'flex', + gap: '60px', + '&>div': { + display: 'flex', + flexDirection: 'column', + gap: '8px' + }, + '&>div:nth-child(2)': { + fontSize: '13px', + fontWeight: 600, + lineHeight: '16px' + } + }, + nextScheduledDates: { + display: 'flex', + gap: '55px', + '&>div': { + width: '130px' + } + }, + footer: { + height: '72px', + padding: '16px 24px', + display: 'flex', + gap: '40px' + }, + link: { + textDecoration: 'underline', + color: theme.palette.ybacolors.labelBackground, + '&:hover': { + color: theme.palette.ybacolors.labelBackground + }, + cursor: 'pointer' + }, + inactive: { + opacity: 0.5 + } +})); + +type toogleScheduleProps = Partial & Pick; + +export const ScheduledCard: FC = ({ + schedule, + universeUUID, + storageConfig +}) => { + const classes = useStyles(); + const [showDeleteModal, setShowDeleteModal] = useState(''); + const [showEditModal, setShowEditModal] = useState(false); + const [showIntervalsModal, setShowIntervalsModal] = useState(false); + const [showTablesModal, setShowTablesModal] = useState(false); + const { t } = useTranslation('translation', { + keyPrefix: 'backup.scheduled.list' + }); + let backupInterval = ''; + + if (schedule.cronExpression) { + backupInterval = cronstrue.toString(schedule.cronExpression); + } else { + backupInterval = convertMsecToTimeFrame( + schedule.frequency, + schedule.frequencyTimeUnit, + 'Every ' + ); + } + + const queryClient = useQueryClient(); + + const toggleSchedule = useMutation( + (val: toogleScheduleProps) => toggleScheduledBackupPolicy(universeUUID, val), + { + onSuccess: (_, params: any) => { + toast.success(`Schedule policy is now ${params.status}`); + queryClient.invalidateQueries('scheduled_backup_list'); + }, + onError: (resp: any) => { + toast.error(resp.response.data.error); + } + } + ); + + const deleteSchedule = useMutation( + () => deleteSchedulePolicy(universeUUID, schedule.scheduleUUID), + + { + onSuccess: () => { + toast.success(t('deleteMsg')); + queryClient.invalidateQueries('scheduled_backup_list'); + }, + onError: (resp: any) => { + toast.error(resp.response.data.error); + } + } + ); + + const isScheduleEnabled = schedule.status === IBackupScheduleStatus.ACTIVE; + + const wrapTableName = (tablesList: string[] | undefined, isKeyspace = false) => { + if (!Array.isArray(tablesList) || tablesList.length === 0) { + return '-'; + } + if (tablesList.length === 1) { + return {tablesList[0]}; + } + return ( + { + setShowTablesModal(true); + }} + > + {tablesList.length} {isKeyspace ? ' Keyspaces' : ' Tables'} + + ); + }; + + return ( +
+
+ + {schedule.scheduleName} + + {TableTypeLabel[schedule.backupInfo.backupType ?? '-']} + + { + toggleSchedule.mutate({ + scheduleUUID: schedule.scheduleUUID, + frequency: schedule.frequency, + cronExpression: schedule.cronExpression, + status: e.target.checked + ? IBackupScheduleStatus.ACTIVE + : IBackupScheduleStatus.STOPPED, + frequencyTimeUnit: schedule.frequencyTimeUnit + }); + }} + /> + + e.stopPropagation()} + > + + { + if (!isScheduleEnabled) return; + setShowEditModal(true); + }} + disabled={schedule.status !== IBackupScheduleStatus.ACTIVE} + > + {t('editPolicy')} + + + + { + setShowDeleteModal(schedule.scheduleUUID); + }} + className="action-danger" + > + {t('deletePolicy')} + + + +
+
+
+
+ {t('scope')} + {t('keyspace')} + {t('tables')} +
+
+ {schedule.backupInfo?.fullBackup ? t('fullBackup') : t('tableBackup')} + + {wrapTableName( + schedule.backupInfo?.keyspaceList?.map((k) => k.keyspace), + true + )} + + {wrapTableName(schedule.backupInfo?.keyspaceList?.[0]?.tablesList)} +
+
+
+
+
{t('lastBackup')}
+
+ {schedule.prevCompletedTask ? ybFormatDate(schedule.prevCompletedTask) : '-'} +
+
+
+
{t('nextBackup')}
+
+ {schedule.nextExpectedTask ? ybFormatDate(schedule.nextExpectedTask) : '-'} +
+
+
+
+
+ deleteSchedule.mutateAsync()} + currentModal={schedule.scheduleUUID} + visibleModal={showDeleteModal} + hideConfirmModal={() => { + setShowDeleteModal(''); + }} + > + {t('deleteModal.deleteMsg')} + + { + setShowEditModal(false); + }} + universeUUID={universeUUID} + schedule={schedule} + /> + { + setShowIntervalsModal(false); + }} + /> + { + setShowTablesModal(false); + }} + /> +
+ ); +}; diff --git a/managed/ui/src/redesign/features/backup/scheduled/list/ScheduledPolicyShowTables.tsx b/managed/ui/src/redesign/features/backup/scheduled/list/ScheduledPolicyShowTables.tsx new file mode 100644 index 000000000000..d60eefe097e0 --- /dev/null +++ b/managed/ui/src/redesign/features/backup/scheduled/list/ScheduledPolicyShowTables.tsx @@ -0,0 +1,59 @@ +/* + * Created on Tue Sep 10 2024 + * + * Copyright 2021 YugaByte, Inc. and Contributors + * Licensed under the Polyform Free Trial License 1.0.0 (the "License") + * You may not use this file except in compliance with the License. You may obtain a copy of the License at + * http://github.com/YugaByte/yugabyte-db/blob/master/licenses/POLYFORM-FREE-TRIAL-LICENSE-1.0.0.txt + */ + +import { FC } from 'react'; +import { useTranslation } from 'react-i18next'; +import { TableHeaderColumn } from 'react-bootstrap-table'; +import { YBModal } from '../../../../components'; +import { YBTable } from '../../../../../components/common/YBTable'; +import { IBackupSchedule } from '../../../../../components/backupv2'; + +interface ScheduledPolicyShowTablesProps { + schedule: IBackupSchedule; + visible: boolean; + onHide: () => void; +} + +const ScheduledPolicyShowTables: FC = ({ + visible, + onHide, + schedule +}) => { + const tables = schedule.backupInfo?.keyspaceList?.[0]?.tablesList?.map((t) => ({ + table: t + })); + + const { t } = useTranslation('translation', { keyPrefix: 'backup.scheduled.list' }); + + return ( + + + + Tables + + + + ); +}; + +export default ScheduledPolicyShowTables; diff --git a/managed/ui/src/translations/en.json b/managed/ui/src/translations/en.json index d7112bac2271..4d1716ce976f 100644 --- a/managed/ui/src/translations/en.json +++ b/managed/ui/src/translations/en.json @@ -2612,6 +2612,42 @@ "unableToFetchTables": "Unable to fetch tables", "parallelThreads": "Parallel threads should be between 1 and 100" } + }, + "list": { + "createPolicy": "Create Scheduled Backup Policy", + "noMoreSchedules": "No more schedules to display", + "scope": "Scope", + "keyspace": "Keyspace", + "tables": "Tables", + "lastBackup": "Last Backup", + "nextBackup": "Next Backup", + "fullBackup": "Full Backup", + "tableBackup": "Table Backup", + "backupIntervals": "Backup Intervals", + "incrementalBackup": "Incremental Backup", + "details": "Details", + "pitr": "Point-in-time Restore", + "retentionPeriod": "Retention Period", + "storageConfig": "Storage Config", + "editPolicy": "Edit Policy", + "deletePolicy": "Delete Policy", + "enabled": "Enabled", + "disabled": "Disabled", + "deleteMsg": "scheduled backup policy is being deleted", + "editModal": { + "title": "Edit Scheduled Backup Policy", + "backupStrategy":"Backup Strategy", + "success": "Scheduled backup policy is being updated" + }, + "deleteModal": { + "deleteMsg":"Are you sure you want to delete this schedule policy?" + }, + "showIntervalModal": { + "title": "Backup Intervals", + "fullBackup":"Full Backup", + "incrementalBackup":"Incremental Backup" + }, + "backupTables": "Backup Tables" } }, "restore": { @@ -2683,4 +2719,4 @@ "yugabyteInc": "Yugabyte, Inc." } } -} +} \ No newline at end of file