-
{t('heading')}
-
-
{
- return (
- hasNecessaryPerm({
- ...ApiPermissionMap.DR_CONFIG_SWITCHOVER,
- onResource: xClusterConfig.sourceUniverseUUID
- }) &&
- hasNecessaryPerm({
- ...ApiPermissionMap.DR_CONFIG_SWITCHOVER,
- onResource: xClusterConfig.targetUniverseUUID
- })
- );
- }}
- isControl
- >
-
+
+ {sourceUniverse?.name ?? 'Undefined Universe'}
+
+
+
+
+ xCluster Disaster Recovery
+
+
+ {drConfig.name}
+
+
+
+
+
+
{t('heading')}
+
+ {
+ return (
+ hasNecessaryPerm({
+ ...ApiPermissionMap.DR_CONFIG_SWITCHOVER,
+ onResource: xClusterConfig.sourceUniverseUUID
+ }) &&
+ hasNecessaryPerm({
+ ...ApiPermissionMap.DR_CONFIG_SWITCHOVER,
+ onResource: xClusterConfig.targetUniverseUUID
+ })
+ );
+ }}
+ isControl
>
- {t('actionButton.switchover')}
-
-
-
- (
- <>
- {
- return (
- hasNecessaryPerm({
- ...ApiPermissionMap.DR_CONFIG_SET_TABLES,
- onResource: xClusterConfig.sourceUniverseUUID
- }) &&
- hasNecessaryPerm({
- ...ApiPermissionMap.DR_CONFIG_SET_TABLES,
- onResource: xClusterConfig.targetUniverseUUID
- })
- );
- }}
- overrideStyle={{ display: 'block' }}
- isControl
- >
-
-
- {
- return (
- hasNecessaryPerm({
- ...ApiPermissionMap.DR_CONFIG_EDIT,
- onResource: xClusterConfig.sourceUniverseUUID
- }) &&
- hasNecessaryPerm({
- ...ApiPermissionMap.DR_CONFIG_EDIT,
- onResource: xClusterConfig.targetUniverseUUID
- })
- );
- }}
- overrideStyle={{ display: 'block' }}
- isControl
- >
-
+
+ (
+ <>
+ {
+ return (
+ hasNecessaryPerm({
+ ...ApiPermissionMap.DR_CONFIG_SET_TABLES,
+ onResource: xClusterConfig.sourceUniverseUUID
+ }) &&
+ hasNecessaryPerm({
+ ...ApiPermissionMap.DR_CONFIG_SET_TABLES,
+ onResource: xClusterConfig.targetUniverseUUID
+ })
+ );
+ }}
+ overrideStyle={{ display: 'block' }}
+ isControl
>
- }
- />
-
-
- {
- return (
- hasNecessaryPerm({
- ...ApiPermissionMap.DR_CONFIG_REPLACE_REPLICA,
- onResource: xClusterConfig.sourceUniverseUUID
- }) &&
- hasNecessaryPerm({
- ...ApiPermissionMap.DR_CONFIG_REPLACE_REPLICA,
- onResource: xClusterConfig.targetUniverseUUID
- })
- );
- }}
- overrideStyle={{ display: 'block' }}
- isControl
- >
-
+
+ {
+ return (
+ hasNecessaryPerm({
+ ...ApiPermissionMap.DR_CONFIG_EDIT,
+ onResource: xClusterConfig.sourceUniverseUUID
+ }) &&
+ hasNecessaryPerm({
+ ...ApiPermissionMap.DR_CONFIG_EDIT,
+ onResource: xClusterConfig.targetUniverseUUID
+ })
+ );
+ }}
+ overrideStyle={{ display: 'block' }}
+ isControl
>
- }
- />
-
-
-
-
- {
- return (
- hasNecessaryPerm({
- ...ApiPermissionMap.DR_CONFIG_SWITCHOVER,
- onResource: xClusterConfig.sourceUniverseUUID
- }) &&
- hasNecessaryPerm({
- ...ApiPermissionMap.DR_CONFIG_SWITCHOVER,
- onResource: xClusterConfig.targetUniverseUUID
- })
- );
- }}
- overrideStyle={{ display: 'block' }}
- isControl
- >
-
+
+ {
+ return (
+ hasNecessaryPerm({
+ ...ApiPermissionMap.DR_CONFIG_REPLACE_REPLICA,
+ onResource: xClusterConfig.sourceUniverseUUID
+ }) &&
+ hasNecessaryPerm({
+ ...ApiPermissionMap.DR_CONFIG_REPLACE_REPLICA,
+ onResource: xClusterConfig.targetUniverseUUID
+ })
+ );
+ }}
+ overrideStyle={{ display: 'block' }}
+ isControl
>
-
- }
- />
-
-
- {
- return (
- hasNecessaryPerm({
- ...ApiPermissionMap.DR_CONFIG_FAILOVER,
- onResource: xClusterConfig.sourceUniverseUUID
- }) &&
- hasNecessaryPerm({
- ...ApiPermissionMap.DR_CONFIG_FAILOVER,
- onResource: xClusterConfig.targetUniverseUUID
- })
- );
- }}
- overrideStyle={{ display: 'block' }}
- isControl
- >
+
+
-
-
- {
- return (
- hasNecessaryPerm({
- ...ApiPermissionMap.DELETE_DR_CONFIG,
- onResource: xClusterConfig.sourceUniverseUUID
- }) &&
- hasNecessaryPerm({
- ...ApiPermissionMap.DELETE_DR_CONFIG,
- onResource: xClusterConfig.targetUniverseUUID
- })
- );
- }}
- overrideStyle={{ display: 'block' }}
- isControl
- >
-
+ {
+ return (
+ hasNecessaryPerm({
+ ...ApiPermissionMap.DR_CONFIG_SWITCHOVER,
+ onResource: xClusterConfig.sourceUniverseUUID
+ }) &&
+ hasNecessaryPerm({
+ ...ApiPermissionMap.DR_CONFIG_SWITCHOVER,
+ onResource: xClusterConfig.targetUniverseUUID
+ })
+ );
+ }}
+ overrideStyle={{ display: 'block' }}
+ isControl
>
- }
- />
-
-
- >
- )}
- subMenus={{
- // eslint-disable-next-line react/display-name
- [ActionMenu.ADVANCED]: (navigateToMainMenu) => (
- <>
-
+
+
{
return (
hasNecessaryPerm({
- ...ApiPermissionMap.DR_CONFIG_RESTART,
+ ...ApiPermissionMap.DR_CONFIG_FAILOVER,
onResource: xClusterConfig.sourceUniverseUUID
}) &&
hasNecessaryPerm({
- ...ApiPermissionMap.DR_CONFIG_RESTART,
+ ...ApiPermissionMap.DR_CONFIG_FAILOVER,
onResource: xClusterConfig.targetUniverseUUID
})
);
@@ -608,27 +551,32 @@ export const DrPanel = ({ currentUniverseUuid }: DrPanelProps) => {
isControl
>
+
{
return (
hasNecessaryPerm({
- ...ApiPermissionMap.DR_CONFIG_SYNC,
+ ...ApiPermissionMap.DELETE_DR_CONFIG,
onResource: xClusterConfig.sourceUniverseUUID
}) &&
hasNecessaryPerm({
- ...ApiPermissionMap.DR_CONFIG_SYNC,
+ ...ApiPermissionMap.DELETE_DR_CONFIG,
onResource: xClusterConfig.targetUniverseUUID
})
);
@@ -637,98 +585,171 @@ export const DrPanel = ({ currentUniverseUuid }: DrPanelProps) => {
isControl
>
>
- )
- }}
- />
-
+ )}
+ subMenus={{
+ // eslint-disable-next-line react/display-name
+ [ActionMenu.ADVANCED]: (navigateToMainMenu) => (
+ <>
+
+ {
+ return (
+ hasNecessaryPerm({
+ ...ApiPermissionMap.DR_CONFIG_RESTART,
+ onResource: xClusterConfig.sourceUniverseUUID
+ }) &&
+ hasNecessaryPerm({
+ ...ApiPermissionMap.DR_CONFIG_RESTART,
+ onResource: xClusterConfig.targetUniverseUUID
+ })
+ );
+ }}
+ overrideStyle={{ display: 'block' }}
+ isControl
+ >
+
+
+ {
+ return (
+ hasNecessaryPerm({
+ ...ApiPermissionMap.DR_CONFIG_SYNC,
+ onResource: xClusterConfig.sourceUniverseUUID
+ }) &&
+ hasNecessaryPerm({
+ ...ApiPermissionMap.DR_CONFIG_SYNC,
+ onResource: xClusterConfig.targetUniverseUUID
+ })
+ );
+ }}
+ overrideStyle={{ display: 'block' }}
+ isControl
+ >
+
+
+ >
+ )
+ }}
+ />
+
+
-
-
-
-
+
+
+
+
+ {isSwitchoverModalOpen && (
+
+ )}
+ {isFailoverModalOpen && (
+
+ )}
+ {isDeleteConfigModalOpen && (
+
+ )}
+ {isEditConfigModalOpen && (
+
+ )}
+ {isEditConfigTargetModalOpen && (
+
+ )}
+ {isEditTablesModalOpen && (
+
+ )}
+ {isRepairConfigModalOpen && (
+
+ )}
+ {isRestartConfigModalOpen && (
+
+ )}
+ {isDbSyncModalOpen && (
+
+ )}
- {isSwitchoverModalOpen && (
-
- )}
- {isFailoverModalOpen && (
-
- )}
- {isDeleteConfigModalOpen && (
-
- )}
- {isEditConfigModalOpen && (
-
- )}
- {isEditConfigTargetModalOpen && (
-
- )}
- {isEditTablesModalOpen && (
-
- )}
- {isRepairConfigModalOpen && (
-
- )}
- {isRestartConfigModalOpen && (
-
- )}
- {isDbSyncModalOpen && (
-
- )}
>
);
diff --git a/managed/ui/src/components/xcluster/disasterRecovery/createConfig/ConfigurePitrStep.tsx b/managed/ui/src/components/xcluster/disasterRecovery/createConfig/ConfigurePitrStep.tsx
index 2653d468267e..f0f0a91d19fd 100644
--- a/managed/ui/src/components/xcluster/disasterRecovery/createConfig/ConfigurePitrStep.tsx
+++ b/managed/ui/src/components/xcluster/disasterRecovery/createConfig/ConfigurePitrStep.tsx
@@ -1,7 +1,6 @@
import { useEffect } from 'react';
import { Box, Typography, useTheme } from '@material-ui/core';
import { useFormContext } from 'react-hook-form';
-import i18next from 'i18next';
import { useTranslation } from 'react-i18next';
import InfoIcon from '../../../../redesign/assets/info-message.svg';
@@ -11,7 +10,6 @@ import {
} from '../../../configRedesign/providerRedesign/components/YBReactSelect/YBReactSelectField';
import { CreateDrConfigFormValues } from './CreateConfigModal';
import { DurationUnit } from '../constants';
-import { I18N_DURATION_KEY_PREFIX } from '../../../../redesign/helpers/constants';
import { YBInputField, YBTooltip } from '../../../../redesign/components';
import { getPitrRetentionPeriodMinValue } from '../utils';
@@ -27,18 +25,18 @@ const TRANSLATION_KEY_PREFIX =
// The expectation is that all `DurationUnit`s are presented as options here.
export const PITR_RETENTION_PERIOD_UNIT_OPTIONS: ReactSelectOption[] = [
{
- label: i18next.t('seconds', { keyPrefix: I18N_DURATION_KEY_PREFIX }),
+ label: 'Seconds',
value: DurationUnit.SECOND
},
{
- label: i18next.t('minutes', { keyPrefix: I18N_DURATION_KEY_PREFIX }),
+ label: 'Minutes',
value: DurationUnit.MINUTE
},
{
- label: i18next.t('hours', { keyPrefix: I18N_DURATION_KEY_PREFIX }),
+ label: 'Hours',
value: DurationUnit.HOUR
},
- { label: i18next.t('days', { keyPrefix: I18N_DURATION_KEY_PREFIX }), value: DurationUnit.DAY }
+ { label: 'Days', value: DurationUnit.DAY }
];
export const ConfigurePitrStep = ({ isFormDisabled }: ConfigureAlertStepProps) => {
diff --git a/managed/ui/src/components/xcluster/disasterRecovery/createConfig/CreateConfigModal.tsx b/managed/ui/src/components/xcluster/disasterRecovery/createConfig/CreateConfigModal.tsx
index 16375c872336..de1f85acb871 100644
--- a/managed/ui/src/components/xcluster/disasterRecovery/createConfig/CreateConfigModal.tsx
+++ b/managed/ui/src/components/xcluster/disasterRecovery/createConfig/CreateConfigModal.tsx
@@ -21,7 +21,6 @@ import {
runtimeConfigQueryKey,
universeQueryKey
} from '../../../../redesign/helpers/api';
-import { generateUniqueName } from '../../../../redesign/helpers/utils';
import { YBButton, YBModal, YBModalProps } from '../../../../redesign/components';
import { CurrentFormStep } from './CurrentFormStep';
import { StorageConfigOption } from '../../sharedComponents/ReactSelectStorageConfig';
@@ -43,6 +42,7 @@ import { TableType, Universe, YBTable } from '../../../../redesign/helpers/dtos'
import toastStyles from '../../../../redesign/styles/toastStyles.module.scss';
export interface CreateDrConfigFormValues {
+ configName: string;
targetUniverse: { label: string; value: Universe; isDisabled: boolean; disabledReason?: string };
namespaceUuids: string[];
tableUuids: string[];
@@ -113,7 +113,7 @@ export const CreateConfigModal = ({ modalProps, sourceUniverseUuid }: CreateConf
DURATION_UNIT_TO_SECONDS[formValues.pitrRetentionPeriodUnit.value];
const createDrConfigRequest: CreateDrConfigRequest = {
- name: `dr-config-${generateUniqueName()}`,
+ name: formValues.configName,
sourceUniverseUUID: sourceUniverseUuid,
targetUniverseUUID: formValues.targetUniverse.value.universeUUID,
dbs: formValues.namespaceUuids.map(formatUuidForXCluster),
diff --git a/managed/ui/src/components/xcluster/disasterRecovery/createConfig/SelectTargetUniverseStep.tsx b/managed/ui/src/components/xcluster/disasterRecovery/createConfig/SelectTargetUniverseStep.tsx
index ce7ed5d944dd..6845360641fd 100644
--- a/managed/ui/src/components/xcluster/disasterRecovery/createConfig/SelectTargetUniverseStep.tsx
+++ b/managed/ui/src/components/xcluster/disasterRecovery/createConfig/SelectTargetUniverseStep.tsx
@@ -1,4 +1,4 @@
-import { makeStyles, Typography } from '@material-ui/core';
+import { Box, Typography, useTheme } from '@material-ui/core';
import { useFormContext } from 'react-hook-form';
import { useQuery } from 'react-query';
import { Trans, useTranslation } from 'react-i18next';
@@ -12,51 +12,20 @@ import { DOCS_URL_DR_REPLICA_SELECTION_LIMITATIONS } from '../constants';
import { CreateDrConfigFormValues } from './CreateConfigModal';
import { getPrimaryCluster } from '../../../../utils/universeUtilsTyped';
import InfoIcon from '../../../../redesign/assets/info-message.svg';
-import { YBTooltip } from '../../../../redesign/components';
-import { INPUT_FIELD_WIDTH_PX } from '../../constants';
+import { YBInputField, YBTooltip } from '../../../../redesign/components';
+import { INPUT_FIELD_WIDTH_PX, XCLUSTER_CONFIG_NAME_ILLEGAL_PATTERN } from '../../constants';
import { hasNecessaryPerm } from '../../../../redesign/features/rbac/common/RbacApiPermValidator';
import { ApiPermissionMap } from '../../../../redesign/features/rbac/ApiAndUserPermMapping';
import { Universe } from '../../../../redesign/helpers/dtos';
+import { useModalStyles } from '../../styles';
+
interface SelectTargetUniverseStepProps {
isFormDisabled: boolean;
sourceUniverse: Universe;
}
-const useStyles = makeStyles((theme) => ({
- stepContainer: {
- '& ol': {
- paddingLeft: theme.spacing(2),
- listStylePosition: 'outside',
- '& li::marker': {
- fontWeight: 'bold'
- }
- }
- },
- instruction: {
- display: 'flex',
- alignItems: 'center',
- gap: theme.spacing(1),
-
- marginBottom: theme.spacing(4)
- },
- fieldLabel: {
- marginBottom: theme.spacing(1)
- },
- fieldHelpText: {
- marginTop: theme.spacing(1),
-
- color: theme.palette.ybacolors.textGray,
- fontSize: '12px'
- },
- infoIcon: {
- '&:hover': {
- cursor: 'pointer'
- }
- }
-}));
-
const TRANSLATION_KEY_PREFIX =
'clusterDetail.disasterRecovery.config.createModal.step.selectTargetUniverse';
/**
@@ -68,7 +37,8 @@ export const SelectTargetUniverseStep = ({
sourceUniverse
}: SelectTargetUniverseStepProps) => {
const { control } = useFormContext
();
- const classes = useStyles();
+ const theme = useTheme();
+ const modalClasses = useModalStyles();
const { t } = useTranslation('translation', { keyPrefix: TRANSLATION_KEY_PREFIX });
const universeListQuery = useQuery(universeQueryKey.ALL, () =>
@@ -107,10 +77,10 @@ export const SelectTargetUniverseStep = ({
});
return (
-
+
-
-
+
{t('instruction.text')}
-
- {t('drReplica')}
-
- !!targetUniverse || t('error.targetUniverseRequired'),
- hasMatchingTLSConfiguration: (targetUniverse: any) =>
- getPrimaryCluster(targetUniverse.value.universeDetails.clusters)?.userIntent
- ?.enableNodeToNodeEncrypt ===
- getPrimaryCluster(sourceUniverse.universeDetails.clusters)?.userIntent
- ?.enableNodeToNodeEncrypt || t('error.mismatchedNodeToNodeEncryption')
- }
- }}
- isDisabled={isFormDisabled}
- />
-
+
+
+
+ {t('configName')}
+
+ !!configName || t('error.requiredField'),
+ noIllegalCharactersInConfigName: (configName: any) => {
+ return (
+ !XCLUSTER_CONFIG_NAME_ILLEGAL_PATTERN.test(configName) ||
+ t('error.illegalCharactersInConfigName')
+ );
+ }
+ }
+ }}
+ />
+
+
+
+ {t('drReplica')}
+
+ !!targetUniverse || t('error.requiredField'),
+ hasMatchingTLSConfiguration: (targetUniverse: any) =>
+ getPrimaryCluster(targetUniverse.value.universeDetails.clusters)?.userIntent
+ ?.enableNodeToNodeEncrypt ===
+ getPrimaryCluster(sourceUniverse.universeDetails.clusters)?.userIntent
+ ?.enableNodeToNodeEncrypt || t('error.mismatchedNodeToNodeEncryption')
+ }
+ }}
+ isDisabled={isFormDisabled}
+ />
+
+
+
({
@@ -23,7 +25,7 @@ const useStyles = makeStyles((theme) => ({
gap: theme.spacing(0.5),
padding: theme.spacing(2),
- width: '480px',
+ width: (props: { width?: number }) => (props.width ? `${props.width}px` : '480px'),
height: '80px',
border: `1px solid ${theme.palette.ybacolors.ybBorderGray}`,
@@ -34,9 +36,10 @@ const useStyles = makeStyles((theme) => ({
export const DrParticipantCard = ({
xClusterConfig,
- universeXClusterRole
+ universeXClusterRole,
+ width
}: DrParticipantCardProps) => {
- const classes = useStyles();
+ const classes = useStyles({ width });
const theme = useTheme();
const universeUuid =
diff --git a/managed/ui/src/components/xcluster/styles.ts b/managed/ui/src/components/xcluster/styles.ts
index b5b9bafb5e2d..cb2105114637 100644
--- a/managed/ui/src/components/xcluster/styles.ts
+++ b/managed/ui/src/components/xcluster/styles.ts
@@ -1,4 +1,5 @@
import { makeStyles } from '@material-ui/core';
+import { INPUT_FIELD_WIDTH_PX } from './constants';
export const useModalStyles = makeStyles((theme) => ({
stepContainer: {
@@ -20,6 +21,9 @@ export const useModalStyles = makeStyles((theme) => ({
formSectionDescription: {
marginBottom: theme.spacing(3)
},
+ inputField: {
+ width: INPUT_FIELD_WIDTH_PX
+ },
fieldLabel: {
display: 'flex',
gap: theme.spacing(1),
diff --git a/managed/ui/src/pages/DrPanel.tsx b/managed/ui/src/pages/DrPanel.tsx
new file mode 100644
index 000000000000..5e2716894fdd
--- /dev/null
+++ b/managed/ui/src/pages/DrPanel.tsx
@@ -0,0 +1,16 @@
+import { lazy, Suspense } from 'react';
+import { YBLoadingCircleIcon } from '../components/common/indicators';
+
+const DrPanelComponent = lazy(() =>
+ import('../components/xcluster/disasterRecovery/DrPanel').then(({ DrPanel }) => ({
+ default: DrPanel
+ }))
+);
+
+export const DrPanel = ({ params: { uuid: currentUniverseUuid, drConfigUuid } }: any) => {
+ return (
+
+
+
+ );
+};
diff --git a/managed/ui/src/routes.js b/managed/ui/src/routes.js
index 65a1dea0ddef..588fceb8b4b5 100644
--- a/managed/ui/src/routes.js
+++ b/managed/ui/src/routes.js
@@ -45,6 +45,7 @@ import {
getRbacEnabledVal,
isRbacEnabled
} from './redesign/features/rbac/common/RbacUtils';
+import { DrPanel } from './pages/DrPanel';
/**
* Redirects to base url if no queryParmas is set else redirects to path set in queryParam
@@ -262,6 +263,7 @@ export default (store) => {
{/* */}
+
{/* */}
diff --git a/managed/ui/src/translations/en.json b/managed/ui/src/translations/en.json
index 4d1716ce976f..7e6a56b961c1 100644
--- a/managed/ui/src/translations/en.json
+++ b/managed/ui/src/translations/en.json
@@ -752,6 +752,12 @@
"disasterRecovery": {
"heading": "xCluster Disaster Recovery",
"actionButton": {
+ "createDrConfig": {
+ "label": "Create Disaster Recovery Config",
+ "tooltip": {
+ "universeUnavailable": "This universe is currently unavailable for setting up further xCluster DR configs."
+ }
+ },
"abortSwitchover": "Abort Switchover",
"switchover": "Switchover",
"repairDr": "Repair DR",
@@ -947,11 +953,13 @@
},
"submitButton": "Next: Select Databases",
"drReplica": "DR Replica Universe",
+ "configName": "DR Configuration Name",
"drReplicaSelectionHelpText": "Make sure the DR replica has the right prerequisites for disaster recovery.",
"missingPermissionOnUniverse": "This user does not have permission to set up xCluster DR with this universe.",
"error": {
- "targetUniverseRequired": "Required.",
- "mismatchedNodeToNodeEncryption": "The source and target universe must have matching TLS enable/disable setting."
+ "requiredField": "Required.",
+ "mismatchedNodeToNodeEncryption": "The source and target universe must have matching TLS enable/disable setting.",
+ "illegalCharactersInConfigName": "The name of the DR configuration cannot contain any characters in [SPACE '_' '*' '<' '>' '?' '|' '\"' NULL])"
}
},
"selectDatabases": {