From ec21ecf4a17dad355d92a0466c925c95c802a5ca Mon Sep 17 00:00:00 2001 From: kkannan Date: Tue, 8 Mar 2022 22:03:18 +0530 Subject: [PATCH 01/18] [PLAT-3025][UI][Platform] Backups/Restores/Backup Storage Configs Summary: This diff implements the backupUI v2. UI Figma [[ https://www.figma.com/file/4hadUdRKog21COqP4xsyGd/Backup-Restore?node-id=853%3A2790 | Link ]] Test Plan: Tested Manually. {F23460} {F23461} {F23462} {F23463} {F23464} {F23465} Reviewers: ssutar, asathyan, lsangappa, mjoshi, cpadinjareveettil Reviewed By: cpadinjareveettil Subscribers: jenkins-bot, ui Differential Revision: https://phabricator.dev.yugabyte.com/D15722 --- .../backupv2/AccountLevelBackup.tsx | 15 + .../ui/src/components/backupv2/BackupAPI.ts | 102 ++++ .../components/backupv2/BackupDeleteModal.tsx | 134 ++++++ .../components/backupv2/BackupDetails.scss | 84 ++++ .../src/components/backupv2/BackupDetails.tsx | 191 ++++++++ .../src/components/backupv2/BackupList.scss | 120 +++++ .../ui/src/components/backupv2/BackupList.tsx | 441 ++++++++++++++++++ .../backupv2/BackupRestoreModal.scss | 54 +++ .../backupv2/BackupRestoreModal.tsx | 410 ++++++++++++++++ .../backupv2/BackupStorageConfig.scss | 35 ++ .../backupv2/BackupStorageConfig.tsx | 75 +++ .../components/backupv2/BackupTableList.scss | 49 ++ .../components/backupv2/BackupTableList.tsx | 157 +++++++ .../src/components/backupv2/BackupUtils.scss | 47 ++ .../src/components/backupv2/BackupUtils.tsx | 147 ++++++ managed/ui/src/components/backupv2/IBackup.ts | 65 +++ managed/ui/src/components/backupv2/index.tsx | 13 + .../components/common/badge/StatusBadge.scss | 41 ++ .../components/common/badge/StatusBadge.tsx | 59 +++ .../common/forms/YBModalForm/YBModalForm.js | 10 +- .../common/forms/fields/YBMultiSelect.js | 4 +- .../src/components/common/nav_bar/NavBar.js | 2 +- .../common/nav_bar/NavBarContainer.js | 6 +- .../components/common/nav_bar/SideNavBar.js | 7 +- managed/ui/src/pages/Backups.tsx | 22 + managed/ui/src/reducers/feature.js | 2 + managed/ui/src/routes.js | 2 + 27 files changed, 2285 insertions(+), 9 deletions(-) create mode 100644 managed/ui/src/components/backupv2/AccountLevelBackup.tsx create mode 100644 managed/ui/src/components/backupv2/BackupAPI.ts create mode 100644 managed/ui/src/components/backupv2/BackupDeleteModal.tsx create mode 100644 managed/ui/src/components/backupv2/BackupDetails.scss create mode 100644 managed/ui/src/components/backupv2/BackupDetails.tsx create mode 100644 managed/ui/src/components/backupv2/BackupList.scss create mode 100644 managed/ui/src/components/backupv2/BackupList.tsx create mode 100644 managed/ui/src/components/backupv2/BackupRestoreModal.scss create mode 100644 managed/ui/src/components/backupv2/BackupRestoreModal.tsx create mode 100644 managed/ui/src/components/backupv2/BackupStorageConfig.scss create mode 100644 managed/ui/src/components/backupv2/BackupStorageConfig.tsx create mode 100644 managed/ui/src/components/backupv2/BackupTableList.scss create mode 100644 managed/ui/src/components/backupv2/BackupTableList.tsx create mode 100644 managed/ui/src/components/backupv2/BackupUtils.scss create mode 100644 managed/ui/src/components/backupv2/BackupUtils.tsx create mode 100644 managed/ui/src/components/backupv2/IBackup.ts create mode 100644 managed/ui/src/components/backupv2/index.tsx create mode 100644 managed/ui/src/components/common/badge/StatusBadge.scss create mode 100644 managed/ui/src/components/common/badge/StatusBadge.tsx create mode 100644 managed/ui/src/pages/Backups.tsx diff --git a/managed/ui/src/components/backupv2/AccountLevelBackup.tsx b/managed/ui/src/components/backupv2/AccountLevelBackup.tsx new file mode 100644 index 000000000000..697a9961f36e --- /dev/null +++ b/managed/ui/src/components/backupv2/AccountLevelBackup.tsx @@ -0,0 +1,15 @@ +/* + * Created on Thu Feb 10 2022 + * + * 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 React, { FC } from 'react'; +import { BackupList } from '.'; + +export const AccountLevelBackup: FC = () => { + return ; +}; diff --git a/managed/ui/src/components/backupv2/BackupAPI.ts b/managed/ui/src/components/backupv2/BackupAPI.ts new file mode 100644 index 000000000000..be713b401b7f --- /dev/null +++ b/managed/ui/src/components/backupv2/BackupAPI.ts @@ -0,0 +1,102 @@ +/* + * Created on Thu Feb 10 2022 + * + * 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 axios from 'axios'; +import { IBackup, Keyspace_Table, RESTORE_ACTION_TYPE, TIME_RANGE_STATE } from '.'; +import { ROOT_URL } from '../../config'; + +export function getBackupsList( + page = 0, + limit = 10, + searchText: string, + timeRange: TIME_RANGE_STATE, + states: any[], + sortBy: string, + direction: string +) { + const cUUID = localStorage.getItem('customerId'); + const payload = { + sortBy, + direction, + filter: {}, + limit, + offset: page, + needTotalCount: true + }; + if (searchText) { + payload['filter'] = { + universeNameList: [searchText] + }; + } + + if (states.length !== 0 && states[0].label !== 'All') { + payload.filter['states'] = [states[0].value]; + } + if (timeRange.startTime && timeRange.endTime) { + payload.filter['dateRangeStart'] = timeRange.startTime.toISOString(); + payload.filter['dateRangeEnd'] = timeRange.endTime.toISOString(); + } + return axios.post(`${ROOT_URL}/customers/${cUUID}/backups/page`, payload); +} + +export function restoreEntireBackup(backup: IBackup, values: Record) { + const cUUID = localStorage.getItem('customerId'); + const backupStorageInfoList = values['keyspaces'].map( + (keyspace: Keyspace_Table, index: number) => { + return { + backupType: backup.backupType, + keyspace: keyspace || backup.responseList[index].keyspace, + sse: backup.sse, + storageLocation: backup.responseList[index].storageLocation, + tableNameList: backup.responseList[index].tablesList + }; + } + ); + const payload = { + actionType: RESTORE_ACTION_TYPE.RESTORE, + backupStorageInfoList: backupStorageInfoList, + customerUUID: cUUID, + universeUUID: values['targetUniverseUUID'].value, + storageConfigUUID: backup.storageConfigUUID, + parallelism: values['parallelThreads'] + }; + if (values['kmsConfigUUID']) { + payload['encryptionAtRestConfig'] = { + encryptionAtRestEnabled: true, + kmsConfigUUID: values['kmsConfigUUID'].value + }; + } + return axios.post(`${ROOT_URL}/customers/${cUUID}/restore`, payload); +} + +export function deleteBackup(backupList: IBackup[]) { + const cUUID = localStorage.getItem('customerId'); + const backup_data = backupList.map((b) => { + return { + backupUUID: b.backupUUID, + storageConfigUUID: b.storageConfigUUID + }; + }); + return axios.delete(`${ROOT_URL}/customers/${cUUID}/delete_backups`, { + data: { + deleteBackupInfos: backup_data + } + }); +} + +export function cancelBackup(backup: IBackup) { + const cUUID = localStorage.getItem('customerId'); + return axios.post(`${ROOT_URL}/customers/${cUUID}/backups/${backup.backupUUID}/stop`); +} + +export function getKMSConfigs() { + const cUUID = localStorage.getItem('customerId'); + const requestUrl = `${ROOT_URL}/customers/${cUUID}/kms_configs`; + return axios.get(requestUrl).then((resp) => resp.data); +} diff --git a/managed/ui/src/components/backupv2/BackupDeleteModal.tsx b/managed/ui/src/components/backupv2/BackupDeleteModal.tsx new file mode 100644 index 000000000000..3f81a2516ffc --- /dev/null +++ b/managed/ui/src/components/backupv2/BackupDeleteModal.tsx @@ -0,0 +1,134 @@ +/* + * Created on Tue Mar 01 2022 + * + * 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 React, { FC } from 'react'; +import { Col, Row } from 'react-bootstrap'; +import { BootstrapTable, TableHeaderColumn } from 'react-bootstrap-table'; +import { useMutation, useQueryClient } from 'react-query'; +import { toast } from 'react-toastify'; +import { cancelBackup, deleteBackup, IBackup } from '.'; +import { StatusBadge } from '../common/badge/StatusBadge'; +import { YBModalForm } from '../common/forms'; +import { YBButton } from '../common/forms/fields'; +import { FormatUnixTimeStampTimeToTimezone } from './BackupUtils'; + +interface BackupDeleteProps { + backupsList: IBackup[]; + visible: boolean; + onHide: () => void; +} + +export const BackupDeleteModal: FC = ({ backupsList, visible, onHide }) => { + const delBackup = useMutation((backupList: IBackup[]) => deleteBackup(backupList), { + onSuccess: () => onHide(), + onError: () => { + toast.error('Unable to delete backup'); + } + }); + + return ( + { + await delBackup.mutateAsync(backupsList); + setSubmitting(false); + onHide(); + }} + submitLabel={ + <> + + Delete Permanently + + } + > +
+ + You are about to permanently delete the following + backups.This action can not be undone + +
+
+ + +
+
+ ); +}; +interface CancelModalProps { + visible: boolean; + backup: IBackup | null; + onHide: () => void; +} +export const BackupCancelModal: FC = ({ visible, backup, onHide }) => { + const queryClient = useQueryClient(); + const execCancelBackup = useMutation(() => cancelBackup(backup as any), { + onSuccess: () => { + toast.success('process stopped'); + onHide(); + queryClient.invalidateQueries(['backups']); + }, + onError: (resp: any) => { + onHide(); + toast.error(resp.response.data.error); + } + }); + if (!backup) return null; + return ( + { + await execCancelBackup.mutateAsync(); + setSubmitting(false); + onHide(); + }} + submitLabel="Cancel Backup" + footerAccessory={ + onHide()} + /> + } + > + + + + You are about to cancel the backup from the source + universe   + {backup.isUniversePresent ? backup.universeName : backup.universeUUID} + + + + + ); +}; diff --git a/managed/ui/src/components/backupv2/BackupDetails.scss b/managed/ui/src/components/backupv2/BackupDetails.scss new file mode 100644 index 000000000000..833dc7e7d178 --- /dev/null +++ b/managed/ui/src/components/backupv2/BackupDetails.scss @@ -0,0 +1,84 @@ +@import '../../_style/colors.scss'; + +.backup-details-panel { + border-radius: 2px; + padding: 24px; + box-shadow: 0 0.12em 2px rgba(35, 35, 41, 0.05), 0 0.5em 10px rgba(35, 35, 41, 0.07); + background: $YB_BG_WHITE_2; + height: 100%; + width: 700px; + position: fixed; + z-index: 9999; + top: 0; + right: 0%; + border-left: 1px solid darken($YB_BACKGROUND, 10%); + max-height: 100%; + + .side-panel { + width: 700px !important; + } + + .side-panel__title { + font-size: 16px; + padding: 20px 20px 20px 15px !important; + color: #000000 !important; + } + + .backup-details-actions { + display: flex; + justify-content: flex-end; + align-items: center; + + button { + margin-left: 10px; + } + } + + .backup-details-info { + background: $YB_BACKGROUND; + border: 1px solid $YB_LIGHT_GRAY_BORDER; + border-radius: 4px; + margin-top: 15px; + + .name-and-status { + display: flex; + justify-content: space-between; + padding: 16px; + margin-bottom: 20px; + } + + .details-rest { + display: flex; + justify-content: flex-start; + flex-wrap: wrap; + margin: -10px 0 0 0; + border-top: 1px solid $YB_LIGHT_GRAY_BORDER; + padding: 0px 16px 16px 16px; + + div { + min-width: 150px; + margin-top: 10px; + } + } + + .header-text { + font-weight: 600; + font-size: 12px; + line-height: 15px; + color: #333333; + margin-bottom: 10px; + } + } + + .tables-list { + margin-top: 32px; + } + + .close-button { + cursor: pointer; + } + + .universeLink { + text-decoration: underline; + } +} diff --git a/managed/ui/src/components/backupv2/BackupDetails.tsx b/managed/ui/src/components/backupv2/BackupDetails.tsx new file mode 100644 index 000000000000..0280f043baea --- /dev/null +++ b/managed/ui/src/components/backupv2/BackupDetails.tsx @@ -0,0 +1,191 @@ +/* + * Created on Wed Feb 16 2022 + * + * 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 React, { FC, useState } from 'react'; +import { Col, Row } from 'react-bootstrap'; +import { Link } from 'react-router'; +import { Backup_States, IBackup, Keyspace_Table, TableType } from '.'; +import { StatusBadge } from '../common/badge/StatusBadge'; +import { YBButton } from '../common/forms/fields'; +import './BackupDetails.scss'; +import { + calculateDuration, + FormatUnixTimeStampTimeToTimezone, + RevealBadge, + SearchInput +} from './BackupUtils'; +import { YCQLTableList, YSQLTableList } from './BackupTableList'; +interface BackupDetailsProps { + backup_details: IBackup | null; + onHide: () => void; + storageConfigName: string; + onDelete: () => void; + onRestore: (backup?: IBackup) => void; + storageConfigs: { + data?: any[]; + }; +} +const SOURCE_UNIVERSE_DELETED_MSG = ( + + Source universe for this backup has been deleted + +); +const STORAGE_CONFIG_DELETED_MSG = ( + + Not available. The storage config associated with this backup + has been deleted. + +); +export const BackupDetails: FC = ({ + backup_details, + onHide, + storageConfigName, + onRestore, + onDelete, + storageConfigs +}) => { + const [searchKeyspaceText, setSearchKeyspaceText] = useState(''); + + if (!backup_details) return null; + const storageConfig = storageConfigs?.data?.find( + (config) => config.configUUID === backup_details.storageConfigUUID + ); + return ( +
+
+
+ { + onHide(); + }} + > + + +
Backup Details
+
+
+ + onDelete()} + disabled={ + backup_details.state === Backup_States.DELETED || + backup_details.state === Backup_States.DELETE_IN_PROGRESS || + backup_details.state === Backup_States.QUEUED_FOR_DELETION + } + /> + onRestore()} + disabled={backup_details.state !== Backup_States.COMPLETED} + /> + + +
+
+
+ Source Universe Name     + +
+
+ + {backup_details.universeName} + +
+ {!backup_details.isUniversePresent &&
{SOURCE_UNIVERSE_DELETED_MSG}
} +
+
+
Backup Status
+ +
+
+
+
+
Backup Type
+
{backup_details.backupType ? 'On Demand' : 'Scheduled'}
+
+
+
Table Type
+
{backup_details.backupType}
+
+
+
Create Time
+
+ +
+
+
+
Expiration
+
+ +
+
+
+
Duration
+
{calculateDuration(backup_details.createTime, backup_details.updateTime)}
+
+
+
Storage Config
+
+ + {storageConfigName} + +
+ {!storageConfigName && STORAGE_CONFIG_DELETED_MSG} +
+
+
+ {backup_details.state !== Backup_States.FAILED && ( + + + ) => { + setSearchKeyspaceText(e.target.value); + }} + /> + + + + {backup_details.backupType === TableType.YQL_TABLE_TYPE ? ( + { + onRestore({ + ...backup_details, + responseList: tablesList + }); + }} + /> + ) : ( + { + onRestore({ + ...backup_details, + responseList: tablesList + }); + }} + /> + )} + + + )} +
+
+
+ ); +}; diff --git a/managed/ui/src/components/backupv2/BackupList.scss b/managed/ui/src/components/backupv2/BackupList.scss new file mode 100644 index 000000000000..9617ec0c4100 --- /dev/null +++ b/managed/ui/src/components/backupv2/BackupList.scss @@ -0,0 +1,120 @@ +@import '../../_style/colors.scss'; + +.backup-v2 { + .backup-actions { + margin-bottom: 30px; + + .actions-delete-filters { + display: flex; + justify-content: flex-end; + align-items: center; + gap: 8px; + + .time-range { + width: 150px; + } + + .custom-date-picker { + display: flex; + align-items: center; + color: #333333; + + div.rw-datetime-picker { + margin-left: 8px !important; + } + } + } + } + + .backup-list-table { + background: $YB_BG_WHITE_3; + padding: 30px 22px; + border: 1px solid #dcdcde; + box-shadow: 0px 4px 10px rgba(0, 0, 0, 0.15); + + .table-row { + cursor: pointer; + + td { + font-weight: 400 !important; + } + } + + .backup-cancel { + height: 40px; + } + } + + #backup-actions-dropdown { + width: 50px !important; + border: 0; + margin-left: 10px !important; + font-weight: 500; + z-index: 0 !important; + } + + .action-danger > a { + color: $YB_ERROR_ICON_COLOR !important; + } + + .backup-list-table { + .react-bs-table .table-bordered > tbody > tr > td { + vertical-align: middle; + height: 45px; + padding: 0 !important; + } + .table-bordered thead th { + font-weight: 700; + } + + .react-bs-table-no-data { + font-weight: 400 !important; + } + } + + .no-padding { + padding: 0; + } +} + +.alert-message { + display: flex; + align-items: baseline; + gap: 4px; + font-size: 12px; + color: $YB_DARKER_GRAY; + text-decoration: none !important; + + &.warning { + i { + color: $YB_WARNING_COLOR; + } + } + + &.danger { + i { + color: $YB_ERROR_ICON_COLOR; + } + } +} + +.delete-table-list { + margin-top: 20px; + background: #ffffff; + border: 1px solid #dcdcde; + padding: 20px; +} + +.backup-modal { + position: fixed; + z-index: 99999 !important; +} + +.align-right { + text-align: right; +} + +.select__value-container { + max-height: 48px; + overflow-y: auto !important; +} diff --git a/managed/ui/src/components/backupv2/BackupList.tsx b/managed/ui/src/components/backupv2/BackupList.tsx new file mode 100644 index 000000000000..d5e1ff3a4591 --- /dev/null +++ b/managed/ui/src/components/backupv2/BackupList.tsx @@ -0,0 +1,441 @@ +/* + * Created on Thu Feb 10 2022 + * + * 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 moment from 'moment'; +import React, { FC, useMemo, useReducer, useState } from 'react'; +import { Col, DropdownButton, MenuItem, Row } from 'react-bootstrap'; +import { BootstrapTable, RemoteObjSpec, SortOrder, TableHeaderColumn } from 'react-bootstrap-table'; +import { useQuery } from 'react-query'; +import { useSelector } from 'react-redux'; +import Select, { OptionTypeBase } from 'react-select'; +import { Backup_States, getBackupsList, IBackup, IUniverse, TIME_RANGE_STATE } from '.'; +import { StatusBadge } from '../common/badge/StatusBadge'; +import { YBButton, YBMultiSelectRedesiged } from '../common/forms/fields'; +import { YBLoading } from '../common/indicators'; +import { BackupDetails } from './BackupDetails'; +import { + BACKUP_STATUS_OPTIONS, + calculateDuration, + CALDENDAR_ICON, + DATE_FORMAT, + ENTITY_NOT_AVAILABLE, + FormatUnixTimeStampTimeToTimezone, + SearchInput +} from './BackupUtils'; +import './BackupList.scss'; +import { BackupCancelModal, BackupDeleteModal } from './BackupDeleteModal'; +import { BackupRestoreModal } from './BackupRestoreModal'; +import { mapValues, keyBy } from 'lodash'; + +const reactWidgets = require('react-widgets'); +const momentLocalizer = require('react-widgets-moment'); +require('react-widgets/dist/css/react-widgets.css'); + +const { DateTimePicker } = reactWidgets; +momentLocalizer(moment); + +const DEFAULT_SORT_COLUMN = 'createTime'; +const DEFAULT_SORT_DIRECTION = 'DESC'; + +const convertArrayToMap = (arr: IUniverse[], keyStr: string, valueStr: string) => + mapValues(keyBy(arr, keyStr), valueStr); + +const TIME_RANGE_OPTIONS = [ + { + value: [1, 'days'], + label: 'Last 24 hrs' + }, + { + value: [3, 'days'], + label: 'Last 3 days' + }, + { + value: [7, 'days'], + label: 'Last week' + }, + { + value: [1, 'month'], + label: 'Last month' + }, + { + value: [3, 'month'], + label: 'Last 3 months' + }, + { + value: [6, 'month'], + label: 'Last 6 months' + }, + { + value: [1, 'year'], + label: 'Last year' + }, + { + value: [0, 'min'], + label: 'All time' + }, + { + value: null, + label: 'Custom' + } +]; + +const DEFAULT_TIME_STATE: TIME_RANGE_STATE = { + startTime: null, + endTime: null, + label: null +}; + +export const BackupList: FC = () => { + const [sizePerPage, setSizePerPage] = useState(10); + const [page, setPage] = useState(1); + const [searchText, setSearchText] = useState(''); + const [customStartTime, setCustomStartTime] = useState(); + const [customEndTime, setCustomEndTime] = useState(); + const [sortDirection, setSortDirection] = useState(DEFAULT_SORT_DIRECTION); + + const [showDeleteModal, setShowDeleteModal] = useState(false); + const [showRestoreModal, setShowRestoreModal] = useState(false); + const [selectedBackups, setSelectedBackups] = useState([]); + const [status, setStatus] = useState([]); + + const timeReducer = (_state: TIME_RANGE_STATE, action: OptionTypeBase) => { + if (action.label === 'Custom') { + return { startTime: customStartTime, endTime: customEndTime, label: action.label }; + } + if (action.label === 'All time') { + return { startTime: null, endTime: null, label: action.label }; + } + + return { + label: action.label, + startTime: moment().subtract(action.value[0], action.value[1]), + endTime: new Date() + }; + }; + + const [timeRange, dispatchTimeRange] = useReducer(timeReducer, DEFAULT_TIME_STATE); + + const { data: backupsList, isLoading } = useQuery( + [ + 'backups', + (page - 1) * sizePerPage, + sizePerPage, + searchText, + timeRange, + status, + DEFAULT_SORT_COLUMN, + sortDirection + ], + () => + getBackupsList( + (page - 1) * sizePerPage, + sizePerPage, + searchText, + timeRange, + status, + DEFAULT_SORT_COLUMN, + sortDirection + ), + { + refetchInterval: 1000 * 20 + } + ); + + const [showDetails, setShowDetails] = useState(null); + const storageConfigs = useSelector((reduxState: any) => reduxState.customer.configs); + const [restoreDetails, setRestoreDetails] = useState(null); + const [cancelBackupDetails, setCancelBackupDetails] = useState(null); + + const storageConfigsMap = useMemo( + () => convertArrayToMap(storageConfigs?.data ?? [], 'configUUID', 'configName'), + [storageConfigs] + ); + + const getActions = (row: IBackup) => { + if (row.state === Backup_States.DELETED) { + return ''; + } + if (row.state === Backup_States.IN_PROGRESS) { + return ( + ) => { + e.stopPropagation(); + setCancelBackupDetails(row); + }} + btnClass="btn btn-default backup-cancel" + btnText="Cancel Backup" + /> + ); + } + return ( + e.stopPropagation()} + > + { + e.stopPropagation(); + if (row.state !== Backup_States.COMPLETED) { + return; + } + setRestoreDetails(row); + setShowRestoreModal(true); + }} + > + Restore Entire Backup + + { + e.stopPropagation(); + setSelectedBackups([row]); + setShowDeleteModal(true); + }} + className="action-danger" + > + Delete Backup + + + ); + }; + + const backups: IBackup[] = backupsList?.data.entities; + return ( + + + + + + setSearchText(val)} + /> + + + { + setStatus(value ? [value] : []); + }} + /> + + + + + setShowDeleteModal(true)} + disabled={selectedBackups.length === 0} + /> + {timeRange.label === 'Custom' && ( +
+ { + setCustomStartTime(time); + dispatchTimeRange({ + label: 'Custom' + }); + }} + /> + - + { + setCustomEndTime(time); + dispatchTimeRange({ + label: 'Custom' + }); + }} + /> +
+ )} + + + +
+ + {isLoading && } + setShowDetails(row), + onPageChange: (page) => setPage(page), + defaultSortOrder: DEFAULT_SORT_DIRECTION.toLowerCase() as SortOrder, + defaultSortName: DEFAULT_SORT_COLUMN, + onSortChange: (_: any, SortOrder: SortOrder) => + setSortDirection(SortOrder.toUpperCase()) + }} + selectRow={{ + mode: 'checkbox', + selected: selectedBackups.map((b) => b.backupUUID), + onSelect: (row, isSelected) => { + if (isSelected) { + setSelectedBackups([...selectedBackups, row]); + } else { + setSelectedBackups(selectedBackups.filter((b) => b.backupUUID !== row.backupUUID)); + } + }, + onSelectAll: (isSelected, row) => { + isSelected ? setSelectedBackups(row) : setSelectedBackups([]); + return true; + } + }} + trClassName="table-row" + tableHeaderClass="backup-list-header" + pagination={true} + remote={(remoteObj: RemoteObjSpec) => { + return { + ...remoteObj, + pagination: true + }; + }} + fetchInfo={{ dataTotalSize: backupsList?.data.totalCount }} + hover + > + + + setShowDetails(null)} + storageConfigName={showDetails ? storageConfigsMap?.[showDetails?.storageConfigUUID] : '-'} + onDelete={() => { + setSelectedBackups([showDetails] as IBackup[]); + setShowDeleteModal(true); + }} + onRestore={(customDetails?: IBackup) => { + setRestoreDetails(customDetails ?? showDetails); + setShowRestoreModal(true); + }} + storageConfigs={storageConfigs} + /> + setShowDeleteModal(false)} + /> + { + setShowRestoreModal(false); + }} + /> + setCancelBackupDetails(null)} + backup={cancelBackupDetails} + /> +
+ ); +}; diff --git a/managed/ui/src/components/backupv2/BackupRestoreModal.scss b/managed/ui/src/components/backupv2/BackupRestoreModal.scss new file mode 100644 index 000000000000..0ecce31d89e9 --- /dev/null +++ b/managed/ui/src/components/backupv2/BackupRestoreModal.scss @@ -0,0 +1,54 @@ +@import '../../_style/colors.scss'; + +.restore-choose-universe { + .backup-info { + background: #f7f7f7; + padding: 10px 16px; + border: 1px solid #e5e5e9; + border-radius: 8px; + margin-top: 16px; + margin-bottom: 32px; + + .title { + font-weight: 600; + font-size: 12px; + margin-bottom: 6px; + } + } + + .Select { + .status-badge { + display: inline; + } + } +} + +.rename-keyspace-step { + .help-text { + margin-bottom: 24px; + margin-top: 24px; + } + + .keyspaces-input { + margin-bottom: 15px; + } + + .yb-field-group { + padding-bottom: 0 !important; + } + + .err-msg { + color: $YB_ERROR_TEXT_COLOR; + margin-bottom: 5px; + } +} + +.keyspace-loading { + margin-bottom: 15px; +} + +.backup-modal { + .restore-wth-rename-but { + height: 46px; + } +} diff --git a/managed/ui/src/components/backupv2/BackupRestoreModal.tsx b/managed/ui/src/components/backupv2/BackupRestoreModal.tsx new file mode 100644 index 000000000000..8b90b108aa81 --- /dev/null +++ b/managed/ui/src/components/backupv2/BackupRestoreModal.tsx @@ -0,0 +1,410 @@ +/* + * Created on Mon Feb 28 2022 + * + * 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 React, { FC, useState } from 'react'; +import { Alert, Col, Row } from 'react-bootstrap'; +import { getKMSConfigs, IBackup, IUniverse, Keyspace_Table, restoreEntireBackup } from '.'; +import { YBModalForm } from '../common/forms'; +import { + FormatUnixTimeStampTimeToTimezone, + KEYSPACE_VALIDATION_REGEX, + SearchInput, + SPINNER_ICON +} from './BackupUtils'; + +import { Field, FieldArray } from 'formik'; +import { useMutation, useQuery } from 'react-query'; +import { fetchTablesInUniverse, fetchUniversesList } from '../../actions/xClusterReplication'; +import { YBLoading } from '../common/indicators'; +import { + YBButton, + YBControlledNumericInputWithLabel, + YBFormSelect, + YBInputField +} from '../common/forms/fields'; +import * as Yup from 'yup'; +import { toast } from 'react-toastify'; +import { components } from 'react-select'; +import { Badge_Types, StatusBadge } from '../common/badge/StatusBadge'; +import './BackupRestoreModal.scss'; + +interface RestoreModalProps { + backup_details: IBackup | null; + onHide: Function; + visible: boolean; +} + +const STEPS = [ + { + title: 'Restore Entire Backup', + submitLabel: 'Next: Rename Databases/Keyspaces', + component: RestoreChooseUniverseForm, + footer: () => null + }, + { + title: 'Restore Entire Backup', + submitLabel: 'Restore', + component: RenameKeyspace, + footer: (onClick: Function) => ( + + ) + } +]; + +export const BackupRestoreModal: FC = ({ backup_details, onHide, visible }) => { + const [currentStep, setCurrentStep] = useState(0); + const [showWithoutRenameModal, setShowWithoutRenameModal] = useState(false); + const [isFetchingTables, setIsFetchingTables] = useState(false); + + const { data: universeList, isLoading: isUniverseListLoading } = useQuery(['universe'], () => + fetchUniversesList().then((res) => res.data as IUniverse[]) + ); + + const { data: kmsConfigs } = useQuery(['kms_configs'], () => getKMSConfigs()); + + const kmsConfigList = kmsConfigs + ? kmsConfigs.map((config: any) => { + const labelName = config.metadata.provider + ' - ' + config.metadata.name; + return { value: config.metadata.configUUID, label: labelName }; + }) + : []; + + const restore = useMutation( + ({ backup_details, values }: { backup_details: IBackup; values: Record }) => + restoreEntireBackup(backup_details, values), + { + onSuccess: (resp) => { + setCurrentStep(0); + onHide(); + toast.success( + + Success. Click   + + here + +   for task details + + ); + }, + onError: (resp: any) => { + setCurrentStep(0); + toast.error(resp.response.data.error); + } + } + ); + + const footerActions = [ + () => setShowWithoutRenameModal(true), + () => setCurrentStep(currentStep - 1) + ]; + + if (isUniverseListLoading) { + return ; + } + + const initialValues = { + targetUniverseUUID: undefined, + parallelThreads: 0, + backup: backup_details, + keyspaces: Array(backup_details?.responseList.length).fill(''), + kmsConfigUUID: null + }; + + const validateTablesAndRestore = async (values: any, setFieldError: Function) => { + setIsFetchingTables(true); + const fetchKeyspace = await fetchTablesInUniverse(values['targetUniverseUUID'].value); + setIsFetchingTables(false); + const keyspaceInForm = backup_details!.responseList.map( + (k, i) => values['keyspaces'][i] || k.keyspace + ); + + const keyspaceInTargetUniverse = fetchKeyspace.data.map((k: any) => k.keySpace); + let hasErrors = false; + keyspaceInForm.forEach((k: string, index: number) => { + if (keyspaceInTargetUniverse.includes(k)) { + setFieldError(`keyspaces[${index}]`, 'Name already exists in target universe'); + hasErrors = true; + } + }); + if (!hasErrors) { + restore.mutate({ backup_details: backup_details as IBackup, values }); + } + }; + + const validationSchema = Yup.object().shape({ + targetUniverseUUID: Yup.string().required('Target universe UUID is required'), + keyspaces: + currentStep === 1 + ? Yup.array( + Yup.string().matches(KEYSPACE_VALIDATION_REGEX, { + message: 'Invalid keyspace name', + excludeEmptyString: true + }) + ) + : Yup.array(Yup.string()) + }); + + return ( + { + setSubmitting(false); + if (currentStep !== STEPS.length - 1) { + setCurrentStep(currentStep + 1); + } else { + await validateTablesAndRestore(values, setFieldError); + } + }} + initialValues={initialValues} + submitLabel={STEPS[currentStep].submitLabel} + onHide={() => { + setCurrentStep(0); + onHide(); + }} + pullRightFooter + footerAccessory={STEPS[currentStep].footer(footerActions[currentStep])} + render={(values: any) => ( + <> + {isFetchingTables && ( + + + + {SPINNER_ICON} Keyspaces are being fetched. Please wait + + + + )} + {STEPS[currentStep].component({ + ...values, + backup_details, + universeList, + kmsConfigList + })} + { + setShowWithoutRenameModal(false); + }} + onOverride={() => { + restore.mutate({ + backup_details: backup_details as IBackup, + values: values['values'] + }); + }} + onSubmit={() => { + setShowWithoutRenameModal(false); + }} + /> + + )} + > + ); +}; + +function RestoreChooseUniverseForm({ + backup_details, + universeList, + kmsConfigList, + setFieldValue, + values +}: { + backup_details: IBackup; + universeList: IUniverse[]; + kmsConfigList: any; + setFieldValue: Function; + values: Record; +}) { + let sourceUniverseNameAtFirst: IUniverse[] = []; + if (universeList) { + sourceUniverseNameAtFirst = [...universeList.filter((u) => u.universeUUID)]; + const sourceUniverseIndex = universeList.findIndex( + (u) => u.universeUUID === backup_details.universeUUID + ); + if (sourceUniverseIndex) { + const sourceUniverse = sourceUniverseNameAtFirst.splice(sourceUniverseIndex, 1); + sourceUniverseNameAtFirst.unshift(sourceUniverse[0]); + } + } + return ( +
+ + + Backup details + + + + +
Backup Universe Name
+
{backup_details.universeName}
+ + +
Created at
+ + +
+ + +
Restore To
+ +
+ + + { + return { + label: universe.name, + value: universe.universeUUID + }; + })} + components={{ + Option: (props: any) => { + if (props.data.value === backup_details.universeUUID) { + return ( + + {props.data.label}{' '} + + + ); + } + return ; + } + }} + label="Select target universe name" + /> + + + + + setFieldValue('parallelThreads', parseInt(val))} + val={values['parallelThreads']} + /> + + + + + + + +
+ ); +} + +export function RenameKeyspace({ + values, + setFieldValue +}: { + values: Record; + setFieldValue: Function; +}) { + return ( +
+ + + ) => { + setFieldValue('searchText', e.target.value); + }} + /> + + + + Rename keyspace/database in this backup (Optional) + + + + values.backup.responseList.map((keyspace: Keyspace_Table, index: number) => + values['searchText'] && + keyspace.keyspace && + keyspace.keyspace.indexOf(values['searchText']) === -1 ? null : ( + + + + {errors['keyspaces']?.[index] && !values['keyspaces']?.[index] && ( + Name already exists. Rename to proceed + )} + + + setFieldValue(`keyspaces[${index}]`, val)} + /> + {errors['keyspaces']?.[index] && values['keyspaces']?.[index] && ( + {errors['keyspaces'][index]} + )} + + + ) + ) + } + /> +
+ ); +} +interface RestoreWithoutRenameProps { + visible: boolean; + onHide: Function; + onSubmit: Function; + onOverride: Function; +} +const RestoreWithoutRenameModal: FC = ({ + visible, + onHide, + onSubmit, + onOverride +}) => { + return ( + { + setSubmitting(false); + onSubmit(); + }} + submitLabel="Rename Databases/Keyspaces" + > + Warning! You are about to restore a backup from source universe to the same universe without + providing new names for its keyspaces. This will override the existing keyspaces + + ); +}; diff --git a/managed/ui/src/components/backupv2/BackupStorageConfig.scss b/managed/ui/src/components/backupv2/BackupStorageConfig.scss new file mode 100644 index 000000000000..4365eaf3c13f --- /dev/null +++ b/managed/ui/src/components/backupv2/BackupStorageConfig.scss @@ -0,0 +1,35 @@ +@import '../../_style/colors.scss'; + +.storage-config { + .storage-location { + background: #f7f7f7; + border: 1px solid #e3e3e5; + border-radius: 8px; + padding: 10px 20px; + } + + .title { + font-weight: 600; + } + + .help-text { + margin-top: 32px; + + .storage-link { + text-decoration: underline; + color: $YB_ORANGE; + cursor: pointer; + } + } + + .storage-config-table { + background: $YB_BG_WHITE_3; + padding: 24px 24px; + border: 1px solid #dcdcde; + box-shadow: 0px 4px 10px rgba(0, 0, 0, 0.15); + border-radius: 8px; + margin-top: 12px; + max-height: 300px; + overflow: scroll; + } +} diff --git a/managed/ui/src/components/backupv2/BackupStorageConfig.tsx b/managed/ui/src/components/backupv2/BackupStorageConfig.tsx new file mode 100644 index 000000000000..acb17ac24a19 --- /dev/null +++ b/managed/ui/src/components/backupv2/BackupStorageConfig.tsx @@ -0,0 +1,75 @@ +/* + * Created on Fri Feb 25 2022 + * + * 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 React, { FC, useState } from 'react'; +import { BootstrapTable, TableHeaderColumn } from 'react-bootstrap-table'; +import { useSelector } from 'react-redux'; +import { IBackup } from '.'; +import { YBModalForm } from '../common/forms'; + +import './BackupStorageConfig.scss'; + +interface BackupStorageConfigProps { + visible: boolean; + onHide: () => void; + backup?: IBackup; + onSubmit: Function; +} + +export const BackupStorageConfig: FC = ({ + visible, + onHide, + onSubmit +}) => { + const configs = useSelector((state: any) => state.customer.configs.data); + const [selectedConfig, setSelectedConfig] = useState(null); + return ( + { + setSubmitting(false); + onSubmit(selectedConfig); + onHide(); + }} + submitLabel="Assign" + > +
+
+
Selected backup location:
+
+
+
+
Select an existing storage config to assign to this backup.
+
+ You can also create and assign new storage config. +
+
+
+ setSelectedConfig(row) + }} + > + +
+
+ ); +}; diff --git a/managed/ui/src/components/backupv2/BackupTableList.scss b/managed/ui/src/components/backupv2/BackupTableList.scss new file mode 100644 index 000000000000..28fbfa6ee831 --- /dev/null +++ b/managed/ui/src/components/backupv2/BackupTableList.scss @@ -0,0 +1,49 @@ +@import '../../_style/colors.scss'; + +.backup-table-list { + margin-top: 24px !important; + background: $YB_BG_WHITE_3; + min-height: 300px; + border: 1px solid $YB_LIGHT_GRAY_BORDER; + border-radius: 8px; + padding: 16px; + .dropdown { + z-index: 10000; + } + .table-bordered { + thead th { + font-weight: 700; + } + } + .copy-location-button { + height: 30px; + padding: 5px 10px; + font-size: 12px; + margin-left: 10px; + } + #table-list-actions-dropdown { + width: 50px !important; + border: 0; + margin-left: 10px !important; + font-weight: 500; + height: 30px; + padding: 0; + font-size: 12px; + } + .inset-table { + margin-left: 75px; + margin-top: 15px; + } + &.ycql-table { + height: 350px; + overflow: auto; + } + .clickable { + cursor: pointer; + } + .restore-detail-button { + height: 30px; + padding: 5px 10px; + font-size: 12px; + } +} diff --git a/managed/ui/src/components/backupv2/BackupTableList.tsx b/managed/ui/src/components/backupv2/BackupTableList.tsx new file mode 100644 index 000000000000..5949e6d5d799 --- /dev/null +++ b/managed/ui/src/components/backupv2/BackupTableList.tsx @@ -0,0 +1,157 @@ +/* + * Created on Thu Feb 17 2022 + * + * 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 React, { FC } from 'react'; +import { BootstrapTable, TableHeaderColumn } from 'react-bootstrap-table'; +import { Backup_States, IBackup } from '.'; +import { YBButton } from '../common/forms/fields'; +import './BackupTableList.scss'; +import copy from 'copy-to-clipboard'; +import { toast } from 'react-toastify'; + +interface YSQLTableProps { + keyspaceSearch?: string; + onRestore: Function; + backup: IBackup; +} + +const COLLAPSED_ICON = ; +const EXPANDED_ICON = ; + +export const YSQLTableList: FC = ({ backup, keyspaceSearch, onRestore }) => { + const databaseList = backup.responseList + .filter((e) => { + return !(keyspaceSearch && e.keyspace.indexOf(keyspaceSearch) < 0); + }) + .map((table, index) => { + return { + keyspace: table.keyspace, + storageLocation: table.storageLocation, + index + }; + }); + return ( +
+ + +
+ ); +}; + +export const YCQLTableList: FC = ({ backup, keyspaceSearch, onRestore }) => { + const expandTables = (row: any) => { + return ( +
+ { + return { t, i }; + })} + > + +
+ ); + }; + const dblist = backup.responseList.filter((e) => { + return !(keyspaceSearch && e.keyspace.indexOf(keyspaceSearch) < 0); + }); + return ( +
+ true} + expandComponent={expandTables} + expandColumnOptions={{ + expandColumnVisible: true, + expandColumnComponent: ({ isExpanded }) => (isExpanded ? EXPANDED_ICON : COLLAPSED_ICON) + }} + trClassName="clickable" + > + +
+ ); +}; diff --git a/managed/ui/src/components/backupv2/BackupUtils.scss b/managed/ui/src/components/backupv2/BackupUtils.scss new file mode 100644 index 000000000000..63d7e2b572b2 --- /dev/null +++ b/managed/ui/src/components/backupv2/BackupUtils.scss @@ -0,0 +1,47 @@ +@import '../../_style/colors.scss'; + +.reveal-badge { + font-weight: normal; + font-size: 11.5px; + line-height: 14px; + color: $YB_TEXT_COLOR; + padding: 3px; + background: $YB_BG_WHITE_3; + border: 1px solid $YB_BG_WHITE_4; + border-radius: 4px; + cursor: pointer; +} + +.search-input { + display: flex; + align-items: center; + background: $YB_BG_WHITE_3; + justify-content: flex-start; + padding: 0px 0px 0px 10px; + border-radius: 8px; + border: 1px solid $YB_BG_WHITE_4; + height: 40px; + + &:focus-within { + border-color: rgba(239, 88, 36, 0.5); + box-shadow: inset 0 0px 0px rgba(0, 0, 0, 0.2), 0 0 8px rgba(239, 88, 36, 0.2); + } + + .form-group { + flex-grow: 1; + + .form-control { + border: none; + height: 100%; + box-shadow: none; + &:focus { + border: none; + box-shadow: none; + } + } + } + + .yb-field-group { + padding-bottom: 0 !important; + } +} diff --git a/managed/ui/src/components/backupv2/BackupUtils.tsx b/managed/ui/src/components/backupv2/BackupUtils.tsx new file mode 100644 index 000000000000..9d62bcb21605 --- /dev/null +++ b/managed/ui/src/components/backupv2/BackupUtils.tsx @@ -0,0 +1,147 @@ +/* + * Created on Fri Feb 18 2022 + * + * 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 { isFunction } from 'lodash'; +import moment from 'moment'; +import React, { FC } from 'react'; +import { useState } from 'react'; +import { useSelector } from 'react-redux'; +import { Backup_States } from '.'; +import { YBControlledTextInput } from '../common/forms/fields'; +import './BackupUtils.scss'; + +/** + * Calculates the difference between two dates + * @param startTime start time + * @param endtime end time + * @returns diff between the dates + */ +export const calculateDuration = (startTime: number, endtime: number): string => { + const start = moment(startTime); + const end = moment(endtime); + const totalDays = end.diff(start, 'days'); + const totalHours = end.diff(start, 'hours'); + const totalMinutes = end.diff(start, 'minutes'); + const totalSeconds = end.diff(start, 'seconds'); + let duration = totalDays !== 0 ? `${totalDays} d ` : ''; + duration += totalHours % 24 !== 0 ? `${totalHours % 24} h ` : ''; + duration += totalMinutes % 60 !== 0 ? `${totalMinutes % 60} m ` : ``; + duration += totalSeconds % 60 !== 0 ? `${totalSeconds % 60} s` : ''; + return duration; +}; + +export const BACKUP_STATUS_OPTIONS: { value: Backup_States | null; label: string }[] = [ + { + label: 'All', + value: null + }, + { + label: 'In Progress', + value: Backup_States.IN_PROGRESS + }, + { + label: 'Completed', + value: Backup_States.COMPLETED + }, + { + label: 'Delete In Progress', + value: Backup_States.DELETE_IN_PROGRESS + }, + { + label: 'Deleted', + value: Backup_States.DELETED + }, + { + label: 'Failed', + value: Backup_States.FAILED + }, + { + label: 'Failed To Delete', + value: Backup_States.FAILED_TO_DELETE + }, + { + label: 'Queued For Deletion', + value: Backup_States.QUEUED_FOR_DELETION + }, + { + label: 'Skipped', + value: Backup_States.SKIPPED + }, + { + label: 'Cancelled', + value: Backup_States.STOPPED + } +]; + +export const DATE_FORMAT = 'YYYY-MM-DD HH:mm:ss'; +export const KEYSPACE_VALIDATION_REGEX = /^[A-Za-z_][A-Za-z_0-9$]*$/; + +export const formatUnixTimeStamp = (unixTimeStamp: number) => + moment(unixTimeStamp).format(DATE_FORMAT); + +export const RevealBadge = ({ label, textToShow }: { label: string; textToShow: string }) => { + const [reveal, setReveal] = useState(false); + return ( + + {reveal ? ( + setReveal(false)}>{textToShow} + ) : ( + setReveal(true)}>{label} + )} + + ); +}; + +export const FormatUnixTimeStampTimeToTimezone = ({ timestamp }: { timestamp: any }) => { + const currentUserTimezone = useSelector((state: any) => state.customer.currentUser.data.timezone); + if (!timestamp) return -; + const formatTime = (currentUserTimezone + ? (moment.utc(timestamp) as any).tz(currentUserTimezone) + : moment.utc(timestamp) + ).format('YYYY-MM-DD H:mm:ss'); + return {formatTime}; +}; + +export const SearchInput: FC = (props) => { + return ( +
+ + + e.key === 'Enter' && + isFunction(props.onEnterPressed) && + props.onEnterPressed(e.currentTarget.value) + }} + /> +
+ ); +}; + +export const ENTITY_NOT_AVAILABLE = ( + + Not Available + +); +export const SPINNER_ICON = ; + +export const CALDENDAR_ICON = () => ({ + alignItems: 'center', + display: 'flex', + + ':before': { + backgroundColor: 'white', + borderRadius: 10, + fontFamily: 'FontAwesome', + content: '"\f133"', + display: 'block', + marginRight: 8 + } +}); diff --git a/managed/ui/src/components/backupv2/IBackup.ts b/managed/ui/src/components/backupv2/IBackup.ts new file mode 100644 index 000000000000..0ec9da1cd877 --- /dev/null +++ b/managed/ui/src/components/backupv2/IBackup.ts @@ -0,0 +1,65 @@ +/* + * Created on Thu Feb 10 2022 + * + * 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 + */ + +export enum Backup_States { + IN_PROGRESS = 'InProgress', + COMPLETED = 'Completed', + FAILED = 'Failed', + DELETED = 'Deleted', + SKIPPED = 'Skipped', + FAILED_TO_DELETE = 'FailedToDelete', + STOPPED = 'Stopped', + DELETE_IN_PROGRESS = 'DeleteInProgress', + QUEUED_FOR_DELETION = 'QueuedForDeletion' +} +export enum TableType { + YQL_TABLE_TYPE = 'YQL_TABLE_TYPE', + REDIS_TABLE_TYPE = 'REDIS_TABLE_TYPE', + PGSQL_TABLE_TYPE = 'PGSQL_TABLE_TYPE' +} + +export interface Keyspace_Table { + keyspace: string; + tablesList: string[]; + storageLocation: string; +} + +export interface IBackup { + state: Backup_States; + backupUUID: string; + backupType: TableType; + storageConfigUUID: string; + universeUUID: string; + scheduleUUID: string; + customerUUID: string; + universeName: string; + isStorageConfigPresent: boolean; + isUniversePresent: boolean; + onDemand: boolean; + createTime: number; + updateTime: number; + expiryTime: number; + responseList: Keyspace_Table[]; + sse: boolean; +} + +export interface IUniverse { + universeUUID: string; + name: string; +} + +export enum RESTORE_ACTION_TYPE { + RESTORE = 'RESTORE', + RESTORE_KEYS = 'RESTORE_KEYS' +} +export interface TIME_RANGE_STATE { + startTime: any; + endTime: any; + label: any; +} diff --git a/managed/ui/src/components/backupv2/index.tsx b/managed/ui/src/components/backupv2/index.tsx new file mode 100644 index 000000000000..cb2fe4857128 --- /dev/null +++ b/managed/ui/src/components/backupv2/index.tsx @@ -0,0 +1,13 @@ +/* + * Created on Thu Feb 10 2022 + * + * 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 + */ + +export * from './IBackup'; +export * from './BackupAPI'; +export * from './AccountLevelBackup'; +export * from './BackupList'; diff --git a/managed/ui/src/components/common/badge/StatusBadge.scss b/managed/ui/src/components/common/badge/StatusBadge.scss new file mode 100644 index 000000000000..ad21e58bd440 --- /dev/null +++ b/managed/ui/src/components/common/badge/StatusBadge.scss @@ -0,0 +1,41 @@ +.status-badge { + padding: 4px 6px; + border-radius: 6px; + font-family: 'Inter'; + background: #E9EEF2; + color: #4E5F6D; + font-style: normal; + font-weight: normal; + font-size: 12px; + line-height: 16px; + display: flex; + align-items: center; + width: fit-content; + justify-content: space-between; + .badge-icon { + margin-left: 4px; + margin-right: 6px; + } + + &.Completed { + background: #CDEFE1; + color: #097245; + } + + &.Failed, + &.FailedToDelete { + color: #DA1515; + background: #FDE2E2; + } + + &.InProgress, + &.DeleteInProgress { + background: #CBDAFF; + color: #1A44A5; + } + + &.QueuedForDeletion { + background: #FFEEC8; + color: #9D6C00; + } +} diff --git a/managed/ui/src/components/common/badge/StatusBadge.tsx b/managed/ui/src/components/common/badge/StatusBadge.tsx new file mode 100644 index 000000000000..0fabab6ce389 --- /dev/null +++ b/managed/ui/src/components/common/badge/StatusBadge.tsx @@ -0,0 +1,59 @@ +/* + * Created on Thu Feb 10 2022 + * + * 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 React, { FC } from 'react'; +import './StatusBadge.scss'; + +export enum Badge_Types { + IN_PROGRESS = 'InProgress', + COMPLETED = 'Completed', + FAILED = 'Failed', + DELETED = 'Deleted', + SKIPPED = 'Skipped', + STOPPED = 'Stopped', + FAILED_TO_DELETE = 'FailedToDelete', + DELETE_IN_PROGRESS = 'DeleteInProgress', + QUEUED_FOR_DELETION = 'QueuedForDeletion' +} + +interface StatusBadgeProps extends React.HTMLAttributes { + statusType: Badge_Types; + customLabel?: string; +} + +const getIcon = (statusType: Badge_Types) => { + let icon = ''; + switch (statusType) { + case Badge_Types.COMPLETED: + icon = 'fa-check'; + break; + case Badge_Types.FAILED: + case Badge_Types.FAILED_TO_DELETE: + icon = 'fa-exclamation-circle'; + break; + + case Badge_Types.DELETE_IN_PROGRESS: + case Badge_Types.IN_PROGRESS: + icon = 'fa-spinner fa-pulse'; + break; + case Badge_Types.QUEUED_FOR_DELETION: + icon = 'fa-clock-o'; + break; + } + return icon ? : null; +}; + +export const StatusBadge: FC = ({ statusType, customLabel, ...others }) => { + return ( + + {customLabel || statusType} + {getIcon(statusType)} + + ); +}; diff --git a/managed/ui/src/components/common/forms/YBModalForm/YBModalForm.js b/managed/ui/src/components/common/forms/YBModalForm/YBModalForm.js index 19df7344d3c1..33f3d6e2387f 100644 --- a/managed/ui/src/components/common/forms/YBModalForm/YBModalForm.js +++ b/managed/ui/src/components/common/forms/YBModalForm/YBModalForm.js @@ -26,6 +26,7 @@ export default class YBModalForm extends Component { dialogClassName, headerClassName, normalizeFooter, + pullRightFooter, showBackButton } = this.props; @@ -81,9 +82,8 @@ export default class YBModalForm extends Component {
)} {footerAccessory && ( -
{footerAccessory}
+
{footerAccessory}
)}
@@ -119,6 +119,7 @@ YBModalForm.propTypes = { showCancelButton: PropTypes.bool, initialValues: PropTypes.object, validationSchema: PropTypes.object, + pullRightFooter: PropTypes.bool, showBackButton: PropTypes.bool }; @@ -127,5 +128,6 @@ YBModalForm.defaultProps = { submitLabel: 'OK', cancelLabel: 'Cancel', showCancelButton: false, + pullRightFooter: false, showBackButton: false }; diff --git a/managed/ui/src/components/common/forms/fields/YBMultiSelect.js b/managed/ui/src/components/common/forms/fields/YBMultiSelect.js index ae2226ed7a9e..134a1a1f7b11 100644 --- a/managed/ui/src/components/common/forms/fields/YBMultiSelect.js +++ b/managed/ui/src/components/common/forms/fields/YBMultiSelect.js @@ -144,12 +144,12 @@ const animatedComponents = makeAnimated({ }); export const YBMultiSelectRedesiged = (props) => { - const { options, value, onChange, placeholder, name, className } = props; + const { options, value, onChange, placeholder, name, className, isMulti = true } = props; return (