Skip to content

Commit

Permalink
[PLAT-14158] Support multiple transactional xCluster configs per univ…
Browse files Browse the repository at this point in the history
…erse on YBA UI

Summary:
YBDB support for multiple transactional xCluster configs per universe was added in: D33634
YBA backend support for multiple transactional xCluster configs per universe was added in: D37088

This diff adds YBA UI support for multiple  transactional xCluster configs per universe.
In this diff, we remove the logic for disabling xCluster config creation when either participant universe is already part of a transactional xCluster config.
This diff also adds a DR config list page since we can now have more than one DR config per universe.

Test Plan:
Verify user can create more than one transactional xCluster config.
- Test both transactional xCluster config and DR
Verify the DR config list shows all DR configs that the current universe is participating in.
{F280727}
{F280730}
{F280731}
{F283536}

Reviewers: rmadhavan, vbansal, hzare, cwang, mkazerooni

Reviewed By: rmadhavan

Subscribers: yugaware

Differential Revision: https://phorge.dev.yugabyte.com/D37701
  • Loading branch information
Jethro-M committed Sep 11, 2024
1 parent 7c1bca8 commit afce6ad
Show file tree
Hide file tree
Showing 19 changed files with 996 additions and 516 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -65,12 +65,12 @@ import { UniverseState, getUniverseStatus, SoftwareUpgradeState } from '../helpe
import { TaskDetailBanner } from '../../../redesign/features/tasks/components/TaskDetailBanner';
import { RbacValidator } from '../../../redesign/features/rbac/common/RbacApiPermValidator';
import { ApiPermissionMap } from '../../../redesign/features/rbac/ApiAndUserPermMapping';
import { DrPanel } from '../../xcluster/disasterRecovery/DrPanel';
import { TroubleshootRegistrationDetails } from '../TroubleshootUniverse/TroubleshootRegistrationDetails';
import {
VM_PATCHING_RUNTIME_CONFIG,
isImgBundleSupportedByProvider
} from '../../configRedesign/providerRedesign/components/linuxVersionCatalog/LinuxVersionUtils';
import { DrConfigList } from '../../xcluster/disasterRecovery/DrConfigList';

import { AppName } from '../../../redesign/features/Troubleshooting/TroubleshootingDashboard';
import { RuntimeConfigKey, UNIVERSE_TASKS } from '../../../redesign/helpers/constants';
Expand Down Expand Up @@ -639,7 +639,7 @@ class UniverseDetail extends Component {
mountOnEnter={true}
unmountOnExit={true}
>
<DrPanel currentUniverseUuid={currentUniverse.data.universeUUID} />
<DrConfigList currentUniverseUuid={currentUniverse.data.universeUUID} />
</Tab.Pane>
),
isNotHidden(currentCustomer.data.features, 'universes.details.replication') && (
Expand Down
41 changes: 31 additions & 10 deletions managed/ui/src/components/xcluster/ReplicationUtils.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -517,6 +517,14 @@ export const getXClusterConfigUuids = (universe: Universe | undefined) => ({
targetXClusterConfigUuids: universe?.universeDetails?.xclusterInfo?.targetXClusterConfigs ?? []
});

/*
* Returns the UUIDs for all xCluster DR configs associated with the provided universe.
*/
export const getDrConfigUuids = (universe: Universe | undefined) => ({
sourceDrConfigUuids: universe?.drConfigUuidsAsSource ?? [],
targetDrConfigUuids: universe?.drConfigUuidsAsTarget ?? []
});

export const hasLinkedXClusterConfig = (universes: Universe[]) =>
universes.some((universe) => {
const { sourceXClusterConfigUuids, targetXClusterConfigUuids } = getXClusterConfigUuids(
Expand Down Expand Up @@ -647,26 +655,39 @@ export const isTableToggleable = (
export const shouldAutoIncludeIndexTables = (xClusterConfig: XClusterConfig | undefined) =>
xClusterConfig ? xClusterConfig.type === XClusterConfigType.TXN : true;

export const getIsTransactionalAtomicityEnabled = (
/**
* If targetUniverse is undefined, then we just consider whether the source universe supports
* txn atomicity. If both source and target universes are defined, then we will consider both.
*/
export const getIsTransactionalAtomicitySupported = (
sourceUniverse: Universe,
targetUniverse?: Universe
) => {
const ybSoftwareVersion = getPrimaryCluster(sourceUniverse.universeDetails.clusters)?.userIntent
.ybSoftwareVersion;
const participatingUniverses = targetUniverse
? [sourceUniverse, targetUniverse]
: [sourceUniverse];
const participantsHaveLinkedXClusterConfig = hasLinkedXClusterConfig(participatingUniverses);
const sourceYbSoftwareVersion = getPrimaryCluster(sourceUniverse.universeDetails.clusters)
?.userIntent.ybSoftwareVersion;
const targetYbSoftwareVersion = targetUniverse
? getPrimaryCluster(targetUniverse.universeDetails.clusters)?.userIntent.ybSoftwareVersion
: '';

return (
!!ybSoftwareVersion &&
!!sourceYbSoftwareVersion &&
compareYBSoftwareVersions({
versionA: TRANSACTIONAL_ATOMICITY_YB_SOFTWARE_VERSION_THRESHOLD,
versionB: ybSoftwareVersion,
versionB: sourceYbSoftwareVersion,
options: {
suppressFormatError: true
}
}) < 0 &&
!participantsHaveLinkedXClusterConfig
(!targetUniverse ||
(targetUniverse &&
!!targetYbSoftwareVersion &&
compareYBSoftwareVersions({
versionA: TRANSACTIONAL_ATOMICITY_YB_SOFTWARE_VERSION_THRESHOLD,
versionB: targetYbSoftwareVersion,
options: {
suppressFormatError: true
}
}) < 0))
);
};

Expand Down
2 changes: 1 addition & 1 deletion managed/ui/src/components/xcluster/XClusterConfigList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,6 @@ export function XClusterConfigList({ currentUniverseUUID }: Props) {
useInterval(() => {
xClusterConfigQueries.forEach((xClusterConfig) => {
if (
!xClusterConfig.data?.usedForDr &&
xClusterConfig.data?.status &&
_.includes(TRANSITORY_XCLUSTER_CONFIG_STATUSES, xClusterConfig.data.status)
) {
Expand Down Expand Up @@ -103,6 +102,7 @@ export function XClusterConfigList({ currentUniverseUUID }: Props) {
const shownXClusterConfigQueries = shouldShowDrXClusterConfigs
? xClusterConfigQueries
: xClusterConfigQueries.filter((xClusterConfigQuery) => !xClusterConfigQuery.data?.usedForDr);

return (
<>
<ul className={styles.listContainer}>
Expand Down
40 changes: 6 additions & 34 deletions managed/ui/src/components/xcluster/XClusterReplication.tsx
Original file line number Diff line number Diff line change
@@ -1,28 +1,24 @@
import { useState } from 'react';
import { Col, Row } from 'react-bootstrap';
import { useQueries, useQuery, UseQueryResult } from 'react-query';
import { useQuery } from 'react-query';
import { useTranslation } from 'react-i18next';
import { Typography } from '@material-ui/core';

import { YBButton } from '../common/forms/fields';
import { XClusterConfigList } from './XClusterConfigList';
import { api, xClusterQueryKey } from '../../redesign/helpers/api';
import { api } from '../../redesign/helpers/api';
import { YBErrorIndicator, YBLoading } from '../common/indicators';
import { getUniverseStatus } from '../universes/helpers/universeHelpers';
import { UnavailableUniverseStates, UNIVERSE_TASKS } from '../../redesign/helpers/constants';
import { getXClusterConfigUuids } from './ReplicationUtils';
import { fetchXClusterConfig } from '../../actions/xClusterReplication';
import { XClusterConfigType } from './constants';
import { RbacValidator } from '../../redesign/features/rbac/common/RbacApiPermValidator';
import { ApiPermissionMap } from '../../redesign/features/rbac/ApiAndUserPermMapping';
import { YBTooltip } from '../../redesign/components';
import { CreateConfigModal } from './createConfig/CreateConfigModal';
import { isActionFrozen } from '../../redesign/helpers/utils';

import { Universe } from '../../redesign/helpers/dtos';
import { XClusterConfig } from './dtos';

import styles from './XClusterReplication.module.scss';
import { isActionFrozen } from '../../redesign/helpers/utils';

const TRANSLATION_KEY_PREFIX = 'clusterDetail.xCluster';

Expand All @@ -33,24 +29,6 @@ export const XClusterReplication = ({ currentUniverseUUID }: { currentUniverseUU
const universeQuery = useQuery<Universe>(['universe', currentUniverseUUID], () =>
api.fetchUniverse(currentUniverseUUID)
);
const { sourceXClusterConfigUuids, targetXClusterConfigUuids } = getXClusterConfigUuids(
universeQuery.data
);

// List the XCluster Configurations for which the current universe is a source or a target.
const universeXClusterConfigUUIDs: string[] = [
...sourceXClusterConfigUuids,
...targetXClusterConfigUuids
];
// The unsafe cast is needed due to issue with useQueries typing
// Upgrading react-query to v3.28 may solve this issue: https://github.com/TanStack/query/issues/1675
const xClusterConfigQueries = useQueries(
universeXClusterConfigUUIDs.map((uuid: string) => ({
queryKey: xClusterQueryKey.detail(uuid),
queryFn: () => fetchXClusterConfig(uuid),
enabled: universeQuery.data?.universeDetails !== undefined
}))
) as UseQueryResult<XClusterConfig>[];

if (universeQuery.isLoading || universeQuery.isIdle) {
return <YBLoading />;
Expand All @@ -68,12 +46,8 @@ export const XClusterReplication = ({ currentUniverseUUID }: { currentUniverseUU
const closeCreateConfigModal = () => setIsCreateConfigModalOpen(false);

const allowedTasks = universeQuery.data?.allowedTasks;
const universeHasTxnXCluster = xClusterConfigQueries.some(
(xClusterConfigQuery) => xClusterConfigQuery.data?.type === XClusterConfigType.TXN
);
const shouldDisableCreateXClusterConfig =
UnavailableUniverseStates.includes(getUniverseStatus(universeQuery.data).state) ||
universeHasTxnXCluster ||
isActionFrozen(allowedTasks, UNIVERSE_TASKS.CONFIGURE_REPLICATION);
return (
<>
Expand All @@ -94,11 +68,9 @@ export const XClusterReplication = ({ currentUniverseUUID }: { currentUniverseUU
<YBTooltip
title={
shouldDisableCreateXClusterConfig
? universeHasTxnXCluster
? t('actionButton.createXClusterConfig.tooltip.universeLinkedToTxnXCluster')
: UnavailableUniverseStates.includes(
getUniverseStatus(universeQuery.data).state
)
? UnavailableUniverseStates.includes(
getUniverseStatus(universeQuery.data).state
)
? t('actionButton.createXClusterConfig.tooltip.universeUnavailable')
: ''
: ''
Expand Down
4 changes: 4 additions & 0 deletions managed/ui/src/components/xcluster/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,10 @@ export const BROKEN_XCLUSTER_CONFIG_STATUSES: readonly XClusterConfigStatus[] =
XClusterConfigStatus.DELETION_FAILED
];

// In several places we assume that a corresponding task must be present when the
// xCluster config is in one of these statuses. If we decide later to introduce some
// transitory state for which the backend does not track task progress, then we must modify any
// reference that makes this assumption (i.e. transitory = running task exists)
export const TRANSITORY_XCLUSTER_CONFIG_STATUSES: readonly XClusterConfigStatus[] = [
XClusterConfigStatus.INITIALIZED,
XClusterConfigStatus.UPDATING
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ import {
XCLUSTER_REPLICATION_DOCUMENTATION_URL,
YB_ADMIN_XCLUSTER_DOCUMENTATION_URL
} from '../constants';
import { getIsTransactionalAtomicityEnabled } from '../ReplicationUtils';
import { getIsTransactionalAtomicitySupported } from '../ReplicationUtils';
import { YBBanner, YBBannerVariant } from '../../common/descriptors';
import InfoMessageIcon from '../../../redesign/assets/info-message.svg';
import { hasNecessaryPerm } from '../../../redesign/features/rbac/common/RbacApiPermValidator';
Expand Down Expand Up @@ -156,7 +156,7 @@ export const SelectTargetUniverseStep = ({
);
// targetUniverse could be undefined on this page since the user might not have entered a value yet.
const targetUniverse = watch('targetUniverse') as { label: string; value: Universe } | undefined;
const isTransactionalAtomicitySupported = getIsTransactionalAtomicityEnabled(
const isTransactionalAtomicitySupported = getIsTransactionalAtomicitySupported(
sourceUniverse,
targetUniverse?.value
);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
@use '../../../_style/colors.scss';
@use '../../../_style/mixins.scss';

.configCard {
background: colors.$YB_BG_WHITE_3;
box-shadow: 0 0.12em 2px rgba(colors.$YB_DARK_GRAY_2, 0.05),
0 0.5em 10px rgba(colors.$YB_DARK_GRAY_2, 0.07);
border-radius: 6px;
color: colors.$YB_TEXT_COLOR !important;

a {
color: unset;
}

a:hover .configName {
color: colors.$YB_ORANGE !important;
@include mixins.transition(0.125s);
}

.headerSection {
align-items: center;
display: flex;
gap: 8px;
justify-content: flex-start;

padding: 12px 20px;

.configNameContainer {
align-items: center;
display: flex;
gap: 8px;

width: 40%;

.configName {
color: colors.$YB_VIOLET_TEXT;
font-size: 18px;
font-weight: 500;
word-break: break-all;
@include mixins.transition(0.35s);
a:hover {
color: colors.$YB_ORANGE !important;
@include mixins.transition(0.125s);
}
}
}

.status {
margin-left: auto;
font-size: 16px;
text-align: end;
}
}

.bodySection {
display: flex;
flex-wrap: wrap;
justify-content: space-between;

padding: 12px 20px;

border-top: 1px solid colors.$YB_GRAY_HOVER;
font-size: 12px;

.configMetricsContainer {
display: flex;
justify-content: space-between;
align-items: center;

.configMetric {
.label {
font-size: 12px;
height: 50%;
}
}
}

.viewTasksPrompt {
font-size: 12px;

a {
color: colors.$YB_VIOLET_TEXT;
text-decoration: underline;

&:hover {
color: colors.$YB_ORANGE_HOVER;
}
}
}
}
}
Loading

0 comments on commit afce6ad

Please sign in to comment.