Skip to content
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.

Commit f2ecf9c

Browse files
committedMar 14, 2025··
Disaster recovery UI for Kubevirt VM - Discovered(Standalone)
Signed-off-by: Gowtham Shanmugasundaram <gshanmug@redhat.com>
1 parent 4728f57 commit f2ecf9c

30 files changed

+1392
-325
lines changed
 

‎locales/en/plugin__odf-console.json

+29-16
Original file line numberDiff line numberDiff line change
@@ -340,6 +340,7 @@
340340
"{{selected}} of {{total}} selected": "{{selected}} of {{total}} selected",
341341
"subscription-selector": "subscription-selector",
342342
"Select the subscriptions groups you wish to replicate via": "Select the subscriptions groups you wish to replicate via",
343+
"Enroll virtual machine": "Enroll virtual machine",
343344
"Enroll managed application": "Enroll managed application",
344345
"Manage disaster recovery": "Manage disaster recovery",
345346
"<0>Application:</0> {applicationName} (Namespace: {applicationNamespace})": "<0>Application:</0> {applicationName} (Namespace: {applicationNamespace})",
@@ -349,6 +350,15 @@
349350
"Invalid label selector": "Invalid label selector",
350351
"The selected PVC label selector doesn't meet the label requirements. Choose a valid label selector or create one with the following requirements: {{ message }}": "The selected PVC label selector doesn't meet the label requirements. Choose a valid label selector or create one with the following requirements: {{ message }}",
351352
"Assign": "Assign",
353+
"Protection name": "Protection name",
354+
"A unique name to identify and manage this protection.": "A unique name to identify and manage this protection.",
355+
"Protect this VM independently without associating it with an existing DR placement control.": "Protect this VM independently without associating it with an existing DR placement control.",
356+
"Standalone": "Standalone",
357+
"Add this VM to an existing DR placement control for consistent failover and recovery. This method is only available for discovered VMs.": "Add this VM to an existing DR placement control for consistent failover and recovery. This method is only available for discovered VMs.",
358+
"Shared": "Shared",
359+
"Protection type": "Protection type",
360+
"Choose how you would like to protect this VM:": "Choose how you would like to protect this VM:",
361+
"Shared protection is not available for managed VMs.": "Shared protection is not available for managed VMs.",
352362
"Delete": "Delete",
353363
"Select a placement": "Select a placement",
354364
"{{count}} selected_one": "{{count}} selected",
@@ -360,25 +370,28 @@
360370
"Application resource": "Application resource",
361371
"PVC label selector": "PVC label selector",
362372
"Add application resource": "Add application resource",
363-
"{{count}} placements_one": "{{count}} placements",
364-
"{{count}} placements_other": "{{count}} placements",
365-
"Data policy": "Data policy",
373+
"Protection type:": "Protection type:",
374+
"Protection name:": "Protection name:",
375+
"Policy": "Policy",
366376
"Policy name:": "Policy name:",
367377
"Clusters:": "Clusters:",
368378
"Replication type:": "Replication type:",
369379
"Sync interval:": "Sync interval:",
380+
"{{count}} placements_one": "{{count}} placements",
381+
"{{count}} placements_other": "{{count}} placements",
370382
"PVC details": "PVC details",
371383
"Application resource:": "Application resource:",
372384
"PVC label selector:": "PVC label selector:",
385+
"Application already enrolled in disaster recovery": "Application already enrolled in disaster recovery",
386+
"<0>This managed application namespace is already DR protected. You may have protected this namespace while enrolling discovered applications.</0><1>To see disaster recovery information for your applications, go to<1> Protected applications </1> under <3> Disaster Recovery </3>.</1>": "<0>This managed application namespace is already DR protected. You may have protected this namespace while enrolling discovered applications.</0><1>To see disaster recovery information for your applications, go to<1> Protected applications </1> under <3> Disaster Recovery </3>.</1>",
387+
"No assigned disaster recovery policy found": "No assigned disaster recovery policy found",
388+
"<0>You have not enrolled this application yet. To protect your application, click <1>Enroll application.</1></0>": "<0>You have not enrolled this application yet. To protect your application, click <1>Enroll application.</1></0>",
389+
"Enroll application": "Enroll application",
390+
"<0>You have not enrolled this virtual machine yet. To protect your virtual machine, click <1>Enroll virtual machine.</1></0>": "<0>You have not enrolled this virtual machine yet. To protect your virtual machine, click <1>Enroll virtual machine.</1></0>",
373391
"New policy assigned to application": "New policy assigned to application",
374392
"Remove disaster recovery": "Remove disaster recovery",
375393
"Your application will lose disaster recovery protection, preventing volume synchronization (replication) between clusters.": "Your application will lose disaster recovery protection, preventing volume synchronization (replication) between clusters.",
376394
"Disaster recovery removed successfully.": "Disaster recovery removed successfully.",
377-
"Enroll application": "Enroll application",
378-
"Application already enrolled in disaster recovery": "Application already enrolled in disaster recovery",
379-
"No assigned disaster recovery policy found": "No assigned disaster recovery policy found",
380-
"<0>This managed application namespace is already DR protected. You may have protected this namespace while enrolling discovered applications.</0><1>To see disaster recovery information for your applications, go to<1>Protected applications</1> under <3>Disaster Recovery</3>.</1>": "<0>This managed application namespace is already DR protected. You may have protected this namespace while enrolling discovered applications.</0><1>To see disaster recovery information for your applications, go to<1>Protected applications</1> under <3>Disaster Recovery</3>.</1>",
381-
"You have not enrolled this application yet. To protect your application,": "You have not enrolled this application yet. To protect your application,",
382395
"Disaster recovery policy details": "Disaster recovery policy details",
383396
"Name: {{name}} ({{status}})": "Name: {{name}} ({{status}})",
384397
"Replication policy: {{replicationType}}, {{interval}} {{unit}}": "Replication policy: {{replicationType}}, {{interval}} {{unit}}",
@@ -418,7 +431,6 @@
418431
"{{appName}} is now successfully enrolled for disaster recovery protection.": "{{appName}} is now successfully enrolled for disaster recovery protection.",
419432
"For disaster recovery or replication details about ACM managed applications navigate to Applications overview page.": "For disaster recovery or replication details about ACM managed applications navigate to Applications overview page.",
420433
"Overall sync status": "Overall sync status",
421-
"Policy": "Policy",
422434
"Cluster": "Cluster",
423435
"Edit configuration": "Edit configuration",
424436
"Update existing configuration in YAML view": "Update existing configuration in YAML view",
@@ -440,6 +452,7 @@
440452
"PersistentVolumeClaim": "PersistentVolumeClaim",
441453
"Review and assign": "Review and assign",
442454
"Review": "Review",
455+
"Data policy": "Data policy",
443456
"Data Services": "Data Services",
444457
"In use: {{targetClusters}}": "In use: {{targetClusters}}",
445458
"Used: {{targetClusters}}": "Used: {{targetClusters}}",
@@ -1156,13 +1169,6 @@
11561169
"NamespaceStore details": "NamespaceStore details",
11571170
"Target Blob Container": "Target Blob Container",
11581171
"Num Volumes": "Num Volumes",
1159-
"Manage distribution of resources": "Manage distribution of resources",
1160-
"Storage classes": "Storage classes",
1161-
"Provisioner": "Provisioner",
1162-
"Deletion policy": "Deletion policy",
1163-
"VolumeSnapshot classes": "VolumeSnapshot classes",
1164-
"Driver": "Driver",
1165-
"Save changes": "Save changes",
11661172
"SSE-C (customer keys)": "SSE-C (customer keys)",
11671173
"SSE-S3 with KMS": "SSE-S3 with KMS",
11681174
"SSE-S3 with KMS (Dual-layer)": "SSE-S3 with KMS (Dual-layer)",
@@ -1199,6 +1205,7 @@
11991205
"Drag a file here, upload files, or start from scratch.": "Drag a file here, upload files, or start from scratch.",
12001206
"Start from scratch or use predefined policy configuration": "Start from scratch or use predefined policy configuration",
12011207
"Apply policy": "Apply policy",
1208+
"Save changes": "Save changes",
12021209
"Grant Public Read Access to All Objects": "Grant Public Read Access to All Objects",
12031210
"Allows anyone to read all objects in the bucket": "Allows anyone to read all objects in the bucket",
12041211
"Allow Access to a Specific S3 Account": "Allow Access to a Specific S3 Account",
@@ -1526,6 +1533,12 @@
15261533
"and": "and",
15271534
"GiB RAM": "GiB RAM",
15281535
"Configure Performance": "Configure Performance",
1536+
"Manage distribution of resources": "Manage distribution of resources",
1537+
"Storage classes": "Storage classes",
1538+
"Provisioner": "Provisioner",
1539+
"Deletion policy": "Deletion policy",
1540+
"VolumeSnapshot classes": "VolumeSnapshot classes",
1541+
"Driver": "Driver",
15291542
"Cancel upload": "Cancel upload",
15301543
"Cancel all ongoing uploads?": "Cancel all ongoing uploads?",
15311544
"Yes, cancel": "Yes, cancel",

‎packages/mco/components/discovered-application-wizard/enroll-discovered-application.spec.tsx

+1-1
Original file line numberDiff line numberDiff line change
@@ -625,7 +625,7 @@ describe('Test review step', () => {
625625
await waitFor(async () => {
626626
expect(
627627
JSON.stringify(drpcObj) ===
628-
'{"apiVersion":"ramendr.openshift.io/v1alpha1","kind":"DRPlacementControl","metadata":{"name":"my-name","namespace":"openshift-dr-ops"},"spec":{"preferredCluster":"east-1","protectedNamespaces":["namespace-1","namespace-2"],"pvcSelector":{},"kubeObjectProtection":{"captureInterval":"5m","recipeRef":{"name":"mock-recipe-1","namespace":"namespace-1"}},"drPolicyRef":{"name":"mock-policy-1","apiVersion":"ramendr.openshift.io/v1alpha1","kind":"DRPolicy"},"placementRef":{"name":"my-name-placement-1","namespace":"openshift-dr-ops","apiVersion":"cluster.open-cluster-management.io/v1beta1","kind":"Placement"}}}'
628+
'{"apiVersion":"ramendr.openshift.io/v1alpha1","kind":"DRPlacementControl","metadata":{"name":"my-name","namespace":"openshift-dr-ops"},"spec":{"preferredCluster":"east-1","protectedNamespaces":["namespace-1","namespace-2"],"pvcSelector":{},"kubeObjectProtection":{"captureInterval":"5m","recipeRef":{"name":"mock-recipe-1","namespace":"namespace-1"},"recipeParameters":{}},"drPolicyRef":{"name":"mock-policy-1","apiVersion":"ramendr.openshift.io/v1alpha1","kind":"DRPolicy"},"placementRef":{"name":"my-name-placement-1","namespace":"openshift-dr-ops","apiVersion":"cluster.open-cluster-management.io/v1beta1","kind":"Placement"}}}'
629629
).toBeTruthy();
630630
});
631631
});

‎packages/mco/components/discovered-application-wizard/utils/k8s-utils.ts

+12-9
Original file line numberDiff line numberDiff line change
@@ -7,16 +7,13 @@ import {
77
DISCOVERED_APP_NS,
88
PROTECTED_APP_ANNOTATION,
99
} from '@odf/mco/constants';
10-
import {
11-
ACMPlacementKind,
12-
DRPlacementControlKind,
13-
DRPolicyKind,
14-
} from '@odf/mco/types';
10+
import { ACMPlacementKind, DRPlacementControlKind } from '@odf/mco/types';
1511
import { getName } from '@odf/shared/selectors';
1612
import { K8sResourceKind } from '@odf/shared/types';
1713
import { getAPIVersionForModel } from '@odf/shared/utils';
1814
import {
1915
MatchExpression,
16+
ObjectMetadata,
2017
k8sCreate,
2118
} from '@openshift-console/dynamic-plugin-sdk';
2219
import {
@@ -29,19 +26,22 @@ export const getDRPCKindObj = (props: {
2926
preferredCluster: string;
3027
namespaces: string[];
3128
protectionMethod: ProtectionMethodType;
32-
drPolicy: DRPolicyKind;
29+
drPolicyName: string;
3330
k8sResourceReplicationInterval: string;
3431
recipeName?: string;
3532
recipeNamespace?: string;
3633
k8sResourceLabelExpressions?: MatchExpression[];
3734
pvcLabelExpressions?: MatchExpression[];
3835
placementName: string;
36+
recipeParameters?: Record<string, string[]>;
37+
labels?: ObjectMetadata['labels'];
3938
}): DRPlacementControlKind => ({
4039
apiVersion: getAPIVersionForModel(DRPlacementControlModel),
4140
kind: DRPlacementControlModel.kind,
4241
metadata: {
4342
name: props.name,
4443
namespace: DISCOVERED_APP_NS,
44+
labels: props.labels ?? {},
4545
},
4646
spec: {
4747
preferredCluster: props.preferredCluster,
@@ -59,6 +59,7 @@ export const getDRPCKindObj = (props: {
5959
name: props.recipeName,
6060
namespace: props.recipeNamespace,
6161
},
62+
recipeParameters: props.recipeParameters ?? {},
6263
}
6364
: {
6465
kubeObjectSelector: {
@@ -67,7 +68,7 @@ export const getDRPCKindObj = (props: {
6768
}),
6869
},
6970
drPolicyRef: {
70-
name: getName(props.drPolicy),
71+
name: props.drPolicyName,
7172
apiVersion: getAPIVersionForModel(DRPolicyModel),
7273
kind: DRPolicyModel.kind,
7374
},
@@ -81,7 +82,9 @@ export const getDRPCKindObj = (props: {
8182
});
8283

8384
// Dummy placement for the discovered apps DRPC
84-
const getPlacementKindObj = (placementName: string): ACMPlacementKind => ({
85+
export const getPlacementKindObj = (
86+
placementName: string
87+
): ACMPlacementKind => ({
8588
apiVersion: getAPIVersionForModel(ACMPlacementModel),
8689
kind: ACMPlacementModel.kind,
8790
metadata: {
@@ -130,7 +133,7 @@ export const createPromise = (
130133
recipeNamespace,
131134
k8sResourceLabelExpressions,
132135
pvcLabelExpressions,
133-
drPolicy,
136+
drPolicyName: getName(drPolicy),
134137
k8sResourceReplicationInterval,
135138
placementName,
136139
}),
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,190 @@
1+
import * as React from 'react';
2+
import {
3+
MIN_VALUE,
4+
normalizeSyncTimeValue,
5+
} from '@odf/mco/components/create-dr-policy/select-replication-type';
6+
import {
7+
REPLICATION_DISPLAY_TEXT,
8+
SYNC_SCHEDULE_DISPLAY_TEXT,
9+
} from '@odf/mco/constants';
10+
import { getDRPolicyStatus, parseSyncInterval } from '@odf/mco/utils';
11+
import { SingleSelectDropdown } from '@odf/shared/dropdown';
12+
import { FieldLevelHelp } from '@odf/shared/generic';
13+
import { useCustomTranslation } from '@odf/shared/useCustomTranslationHook';
14+
import { getValidatedProp } from '@odf/shared/utils';
15+
import { RequestSizeInput } from '@odf/shared/utils/RequestSizeInput';
16+
import { SelectOption } from '@patternfly/react-core/deprecated';
17+
import * as _ from 'lodash-es';
18+
import { TFunction } from 'react-i18next';
19+
import {
20+
Form,
21+
FormGroup,
22+
FormHelperText,
23+
FormSection,
24+
HelperText,
25+
HelperTextItem,
26+
Text,
27+
TextVariants,
28+
} from '@patternfly/react-core';
29+
30+
// Get Policy Dropdown Options
31+
const getPolicyOptions = (policies: PolicyInfo[], t: TFunction) =>
32+
policies.map((policy) => (
33+
<SelectOption
34+
key={policy.name}
35+
value={policy.name}
36+
description={
37+
policy.schedulingInterval !== '0m'
38+
? t(
39+
'Replication type: {{type}}, Interval: {{interval}}, Clusters: {{clusters}}',
40+
{
41+
type: REPLICATION_DISPLAY_TEXT(t).async,
42+
interval: policy.schedulingInterval,
43+
clusters: policy.drClusters.join(', '),
44+
}
45+
)
46+
: t('Replication type: {{type}}, Clusters: {{clusters}}', {
47+
type: REPLICATION_DISPLAY_TEXT(t).sync,
48+
clusters: policy.drClusters.join(', '),
49+
})
50+
}
51+
/>
52+
));
53+
54+
// Policy Selection Component
55+
const PolicySelection: React.FC<PolicySelectionProps> = ({
56+
policy,
57+
eligiblePolicies,
58+
isValidationEnabled,
59+
onChange,
60+
}) => {
61+
const { t } = useCustomTranslation();
62+
const translatedPolicyStatus = t('Status: {{status}}', {
63+
status: getDRPolicyStatus(policy?.isValidated, t),
64+
});
65+
66+
const policyValidated = getValidatedProp(
67+
(isValidationEnabled && _.isEmpty(policy)) || !eligiblePolicies.length
68+
);
69+
70+
const helperTextInvalid = !eligiblePolicies.length
71+
? t('No policy found')
72+
: t('Required');
73+
const helperText = !_.isEmpty(policy) && translatedPolicyStatus;
74+
75+
return (
76+
<FormGroup
77+
className="pf-v5-u-w-50"
78+
fieldId="dr-policy-selection"
79+
label={t('Disaster recovery policy')}
80+
labelIcon={
81+
<FieldLevelHelp>
82+
{t('The policy sync interval is only applicable to volumes.')}
83+
</FieldLevelHelp>
84+
}
85+
isRequired
86+
>
87+
<SingleSelectDropdown
88+
id="dr-policy-dropdown"
89+
placeholderText={t('Select a policy')}
90+
selectedKey={policy?.name}
91+
selectOptions={getPolicyOptions(eligiblePolicies, t)}
92+
onChange={onChange}
93+
validated={policyValidated}
94+
isDisabled={!eligiblePolicies.length}
95+
/>
96+
<FormHelperText>
97+
<HelperText>
98+
<HelperTextItem variant={policyValidated}>
99+
{policyValidated === 'error' ? helperTextInvalid : helperText}
100+
</HelperTextItem>
101+
</HelperText>
102+
</FormHelperText>
103+
</FormGroup>
104+
);
105+
};
106+
107+
// Replication Selection Component
108+
export const ReplicationSelectionHelper: React.FC<
109+
ReplicationSelectionHelperProps
110+
> = ({
111+
eligiblePolicies,
112+
policy,
113+
k8sResourceReplicationInterval,
114+
isValidationEnabled,
115+
onK8sSyncIntervalChange,
116+
onPolicyChange,
117+
}) => {
118+
const { t } = useCustomTranslation();
119+
const SyncScheduleFormat = SYNC_SCHEDULE_DISPLAY_TEXT(t);
120+
const [unitVal, interval] = parseSyncInterval(k8sResourceReplicationInterval);
121+
122+
const setInterval = (props: { value: number; unit: string }) => {
123+
onK8sSyncIntervalChange(
124+
`${normalizeSyncTimeValue(props.value)}${props.unit}`
125+
);
126+
};
127+
128+
return (
129+
<Form maxWidth="58rem">
130+
<FormSection title={t('Volume and Kubernetes object replication')}>
131+
<Text component={TextVariants.small}>
132+
{t(
133+
'Define where to sync or replicate your application volumes and Kubernetes object using a disaster recovery policy.'
134+
)}
135+
</Text>
136+
<PolicySelection
137+
policy={policy}
138+
eligiblePolicies={eligiblePolicies}
139+
isValidationEnabled={isValidationEnabled}
140+
onChange={onPolicyChange}
141+
/>
142+
<FormGroup
143+
fieldId="k8s-resource-interval-selection"
144+
label={t('Kubernetes object replication interval')}
145+
>
146+
<FormHelperText>
147+
<HelperText>
148+
<HelperTextItem>
149+
{t('Define the interval for Kubernetes object replication')}
150+
</HelperTextItem>
151+
</HelperText>
152+
</FormHelperText>
153+
<RequestSizeInput
154+
name={t('Replication interval')}
155+
onChange={setInterval}
156+
dropdownUnits={SyncScheduleFormat}
157+
defaultRequestSizeUnit={unitVal}
158+
defaultRequestSizeValue={interval.toString()}
159+
minValue={MIN_VALUE}
160+
/>
161+
</FormGroup>
162+
</FormSection>
163+
</Form>
164+
);
165+
};
166+
167+
// Common Policy Type
168+
export type PolicyInfo = {
169+
name: string;
170+
drClusters: string[];
171+
schedulingInterval: string;
172+
isValidated: boolean;
173+
};
174+
175+
// Props
176+
type ReplicationSelectionHelperProps = {
177+
eligiblePolicies: PolicyInfo[];
178+
policy: PolicyInfo;
179+
k8sResourceReplicationInterval: string;
180+
isValidationEnabled: boolean;
181+
onPolicyChange: (policyName: string) => void;
182+
onK8sSyncIntervalChange: (syncInterval: string) => void;
183+
};
184+
185+
type PolicySelectionProps = {
186+
policy: PolicyInfo;
187+
eligiblePolicies: PolicyInfo[];
188+
isValidationEnabled: boolean;
189+
onChange: (policyName: string) => void;
190+
};
Original file line numberDiff line numberDiff line change
@@ -1,216 +1,87 @@
11
import * as React from 'react';
2-
import {
3-
MIN_VALUE,
4-
normalizeSyncTimeValue,
5-
} from '@odf/mco/components/create-dr-policy/select-replication-type';
6-
import {
7-
REPLICATION_DISPLAY_TEXT,
8-
SYNC_SCHEDULE_DISPLAY_TEXT,
9-
} from '@odf/mco/constants';
102
import { DRPolicyModel } from '@odf/mco/models';
113
import { DRPolicyKind } from '@odf/mco/types';
12-
import {
13-
getDRPolicyStatus,
14-
isDRPolicyValidated,
15-
parseSyncInterval,
16-
} from '@odf/mco/utils';
17-
import { SingleSelectDropdown } from '@odf/shared/dropdown';
18-
import { FieldLevelHelp, StatusBox } from '@odf/shared/generic';
4+
import { isDRPolicyValidated } from '@odf/mco/utils';
5+
import { StatusBox } from '@odf/shared/generic/status-box';
196
import { useK8sList } from '@odf/shared/hooks';
207
import { getName } from '@odf/shared/selectors';
21-
import { useCustomTranslation } from '@odf/shared/useCustomTranslationHook';
22-
import { getValidatedProp } from '@odf/shared/utils';
23-
import { RequestSizeInput } from '@odf/shared/utils/RequestSizeInput';
24-
import { SelectOption } from '@patternfly/react-core/deprecated';
258
import * as _ from 'lodash-es';
26-
import { TFunction } from 'react-i18next';
27-
import {
28-
Form,
29-
FormGroup,
30-
FormHelperText,
31-
FormSection,
32-
HelperText,
33-
HelperTextItem,
34-
Text,
35-
TextVariants,
36-
} from '@patternfly/react-core';
379
import {
3810
EnrollDiscoveredApplicationAction,
3911
EnrollDiscoveredApplicationState,
4012
EnrollDiscoveredApplicationStateType,
4113
} from '../../utils/reducer';
4214
import { findAllEligiblePolicies } from '../namespace-step/namespace-table';
43-
import '../../enroll-discovered-application.scss';
44-
45-
const getPolicyOptions = (dataPolicies: DRPolicyKind[], t: TFunction) =>
46-
dataPolicies.map((policy) => (
47-
<SelectOption
48-
key={getName(policy)}
49-
value={getName(policy)}
50-
description={
51-
policy.spec.schedulingInterval !== '0m'
52-
? t(
53-
'Replication type: {{type}}, Interval: {{interval}}, Clusters: {{clusters}}',
54-
{
55-
type: REPLICATION_DISPLAY_TEXT(t).async,
56-
interval: policy.spec.schedulingInterval,
57-
clusters: policy.spec.drClusters.join(', '),
58-
}
59-
)
60-
: t('Replication type: {{type}}, Clusters: {{clusters}}', {
61-
type: REPLICATION_DISPLAY_TEXT(t).sync,
62-
clusters: policy.spec.drClusters.join(', '),
63-
})
15+
import {
16+
PolicyInfo,
17+
ReplicationSelectionHelper,
18+
} from './replication-selection-helper';
19+
20+
// Convert DRPolicyKind to PolicyInfo
21+
const convertToPolicyInfo = (policy?: DRPolicyKind): PolicyInfo =>
22+
!_.isEmpty(policy)
23+
? {
24+
name: getName(policy),
25+
drClusters: policy?.spec?.drClusters || [],
26+
schedulingInterval: policy?.spec?.schedulingInterval || '0m',
27+
isValidated: isDRPolicyValidated(policy),
6428
}
65-
/>
66-
));
29+
: ({} as PolicyInfo);
6730

68-
const PolicySelection: React.FC<PolicySelectionProps> = ({
69-
policy,
70-
clusterName,
31+
export const ReplicationSelection: React.FC<ReplicationSelectionProps> = ({
32+
state,
7133
isValidationEnabled,
7234
dispatch,
7335
}) => {
74-
const { t } = useCustomTranslation();
36+
const { clusterName } = state.namespace;
37+
const { drPolicy, k8sResourceReplicationInterval } = state.replication;
7538

7639
const [drPolicies, loaded, loadError] =
7740
useK8sList<DRPolicyKind>(DRPolicyModel);
7841

79-
// Filteting policy using cluster name
80-
const eligiblePolicies = findAllEligiblePolicies(
81-
clusterName,
82-
drPolicies || []
83-
);
84-
85-
// Validated/Not Validated
86-
const translatedPolicyStatus = t('Status: {{status}}', {
87-
status: getDRPolicyStatus(isDRPolicyValidated(policy), t),
88-
});
42+
// Filtering policies using cluster name
43+
const eligiblePolicies = React.useMemo(() => {
44+
if (loaded && !loadError) {
45+
return findAllEligiblePolicies(clusterName, drPolicies);
46+
}
47+
return [];
48+
}, [clusterName, drPolicies, loaded, loadError]);
8949

90-
const policyValidated = getValidatedProp(
91-
(isValidationEnabled && _.isEmpty(policy)) || !eligiblePolicies.length
92-
);
50+
// Convert all policies to PolicyInfo before filtering
51+
const convertedPolicies = eligiblePolicies.map(convertToPolicyInfo);
9352

94-
const helperTextInvalid = !eligiblePolicies.length
95-
? t('No policy found')
96-
: t('Required');
97-
const helperText = !_.isEmpty(policy) && translatedPolicyStatus;
53+
const setK8sSyncInterval = (syncInterval: string) => {
54+
dispatch({
55+
type: EnrollDiscoveredApplicationStateType.SET_K8S_RESOURCE_REPLICATION_INTERVAL,
56+
payload: syncInterval,
57+
});
58+
};
9859

9960
const setSelectedPolicy = (policyName: string) => {
10061
dispatch({
10162
type: EnrollDiscoveredApplicationStateType.SET_POLICY,
102-
payload: eligiblePolicies.find(
63+
payload: drPolicies.find(
10364
(currPolicy) => getName(currPolicy) === policyName
10465
),
10566
});
10667
};
10768

10869
return loaded && !loadError ? (
109-
<FormGroup
110-
className="pf-v5-u-w-50"
111-
fieldId="dr-policy-selection"
112-
label={t('Disaster recovery policy')}
113-
labelIcon={
114-
<FieldLevelHelp>
115-
{t('The policy sync interval is only applicable to volumes.')}
116-
</FieldLevelHelp>
117-
}
118-
isRequired
119-
>
120-
<SingleSelectDropdown
121-
id="dr-policy-dropdown"
122-
placeholderText={t('Select a policy')}
123-
selectedKey={getName(policy)}
124-
selectOptions={getPolicyOptions(eligiblePolicies, t)}
125-
onChange={setSelectedPolicy}
126-
validated={policyValidated}
127-
isDisabled={!eligiblePolicies.length}
128-
/>
129-
<FormHelperText>
130-
<HelperText>
131-
<HelperTextItem variant={policyValidated}>
132-
{policyValidated === 'error' ? helperTextInvalid : helperText}
133-
</HelperTextItem>
134-
</HelperText>
135-
</FormHelperText>
136-
</FormGroup>
70+
<ReplicationSelectionHelper
71+
policy={convertToPolicyInfo(drPolicy)}
72+
eligiblePolicies={convertedPolicies}
73+
k8sResourceReplicationInterval={k8sResourceReplicationInterval}
74+
isValidationEnabled={isValidationEnabled}
75+
onK8sSyncIntervalChange={setK8sSyncInterval}
76+
onPolicyChange={setSelectedPolicy}
77+
/>
13778
) : (
13879
<StatusBox loaded={loaded} loadError={loadError} />
13980
);
14081
};
14182

142-
export const ReplicationSelection: React.FC<ReplicationSelectionProps> = ({
143-
state,
144-
isValidationEnabled,
145-
dispatch,
146-
}) => {
147-
const { t } = useCustomTranslation();
148-
149-
const { clusterName } = state.namespace;
150-
const {
151-
drPolicy: policy,
152-
k8sResourceReplicationInterval: replicationInterval,
153-
} = state.replication;
154-
155-
const SyncScheduleFormat = SYNC_SCHEDULE_DISPLAY_TEXT(t);
156-
const [unitVal, interval] = parseSyncInterval(replicationInterval);
157-
158-
const setInterval = (props: { value: number; unit: string }) => {
159-
const { value, unit } = props;
160-
dispatch({
161-
type: EnrollDiscoveredApplicationStateType.SET_K8S_RESOURCE_REPLICATION_INTERVAL,
162-
payload: `${normalizeSyncTimeValue(value)}${unit}`,
163-
});
164-
};
165-
166-
return (
167-
<Form maxWidth="58rem">
168-
<FormSection title={t('Volume and Kubernetes object replication')}>
169-
<Text component={TextVariants.small}>
170-
{t(
171-
'Define where to sync or replicate your application volumes and Kubernetes object using a disaster recovery policy.'
172-
)}
173-
</Text>
174-
<PolicySelection
175-
policy={policy}
176-
clusterName={clusterName}
177-
isValidationEnabled={isValidationEnabled}
178-
dispatch={dispatch}
179-
/>
180-
<FormGroup
181-
fieldId="k8s-resource-interval-selection"
182-
label={t('Kubernetes object replication interval')}
183-
>
184-
<FormHelperText>
185-
<HelperText>
186-
<HelperTextItem>
187-
{t('Define the interval for Kubernetes object replication')}
188-
</HelperTextItem>
189-
</HelperText>
190-
</FormHelperText>
191-
<RequestSizeInput
192-
name={t('Replication interval')}
193-
onChange={setInterval}
194-
dropdownUnits={SyncScheduleFormat}
195-
defaultRequestSizeUnit={unitVal}
196-
defaultRequestSizeValue={interval.toString()}
197-
minValue={MIN_VALUE}
198-
/>
199-
</FormGroup>
200-
</FormSection>
201-
</Form>
202-
);
203-
};
204-
20583
type ReplicationSelectionProps = {
20684
state: EnrollDiscoveredApplicationState;
20785
isValidationEnabled: boolean;
20886
dispatch: React.Dispatch<EnrollDiscoveredApplicationAction>;
20987
};
210-
211-
type PolicySelectionProps = {
212-
policy: DRPolicyKind;
213-
clusterName: string;
214-
isValidationEnabled: boolean;
215-
dispatch: React.Dispatch<EnrollDiscoveredApplicationAction>;
216-
};

‎packages/mco/components/modals/app-manage-policies/app-manage-policies-modal-body.tsx

+15-13
Original file line numberDiff line numberDiff line change
@@ -16,20 +16,22 @@ const ComponentMap = {
1616
[referenceForModel(VirtualMachineModel)]: VirtualMachineParser,
1717
};
1818

19-
export const AppManagePoliciesModalBody: React.FC<
20-
AppManagePoliciesModalBodyProps
21-
> = ({ application, cluster, setCurrentModalContext }) => {
22-
const gvk = getGVKofResource(application);
23-
const SelectedComponent = ComponentMap[gvk];
19+
// Memoizing the component to prevent unnecessary re-renders.
20+
// Problem: Without React.memo, the component re-renders even if props haven't changed.
21+
// Fix: React.memo ensures the component only re-renders when `application`, `cluster`, or `setCurrentModalContext` changes.
22+
export const AppManagePoliciesModalBody: React.FC<AppManagePoliciesModalBodyProps> =
23+
React.memo(({ application, cluster, setCurrentModalContext }) => {
24+
const gvk = getGVKofResource(application);
25+
const SelectedComponent = ComponentMap[gvk];
2426

25-
return SelectedComponent ? (
26-
<SelectedComponent
27-
application={application as any}
28-
cluster={cluster}
29-
setCurrentModalContext={setCurrentModalContext}
30-
/>
31-
) : null;
32-
};
27+
return SelectedComponent ? (
28+
<SelectedComponent
29+
application={application as any}
30+
cluster={cluster}
31+
setCurrentModalContext={setCurrentModalContext}
32+
/>
33+
) : null;
34+
});
3335

3436
type AppManagePoliciesModalBodyProps = {
3537
application: K8sResourceCommon;

‎packages/mco/components/modals/app-manage-policies/app-manage-policies-modal.tsx

+21-4
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import * as React from 'react';
2+
import { VirtualMachineModel } from '@odf/mco/models';
23
import { SearchResultItemType } from '@odf/mco/types';
34
import { convertSearchResult } from '@odf/mco/utils';
45
import { getName, getNamespace } from '@odf/shared/selectors';
@@ -16,19 +17,35 @@ export const AppManagePoliciesModal: React.FC<AppManagePoliciesModalProps> = ({
1617
close,
1718
}) => {
1819
const { t } = useCustomTranslation();
19-
const [currentModalContext, setCurrentModalContext] = React.useState(
20+
const [currentModalContext, setModalContext] = React.useState(
2021
ModalViewContext.MANAGE_POLICY_VIEW
2122
);
2223

23-
const application =
24-
'apigroup' in resource ? convertSearchResult(resource) : resource;
24+
// Problem: Without useCallback, the function reference changes on each render,
25+
// causing child components to re-render unnecessarily.
26+
// Fix: useCallback ensures the function has a stable reference.
27+
const setCurrentModalContext = React.useCallback(
28+
(context: ModalViewContext) => setModalContext(context),
29+
[]
30+
);
31+
32+
// Problem: Parsing runs on every render, even if the resource didn't change.
33+
// This causes performance issues and unnecessary recalculations.
34+
// Fix: useMemo ensures parsing only happens when 'resource' changes.
35+
const application = React.useMemo(
36+
() => ('apigroup' in resource ? convertSearchResult(resource) : resource),
37+
[resource]
38+
);
39+
2540
const applicationName = getName(application) ?? application?.['name'];
2641
const applicationNamespace =
2742
getNamespace(application) ?? application?.['namespace'];
2843

2944
const title =
3045
currentModalContext === ModalViewContext.ASSIGN_POLICY_VIEW
31-
? t('Enroll managed application')
46+
? application.kind === VirtualMachineModel.kind
47+
? t('Enroll virtual machine')
48+
: t('Enroll managed application')
3249
: t('Manage disaster recovery');
3350

3451
const description = (

‎packages/mco/components/modals/app-manage-policies/assign-policy-view.tsx

+92-7
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
import * as React from 'react';
22
import {
3-
DRApplication,
43
AssignPolicySteps,
54
AssignPolicyStepsNames,
5+
DRApplication,
66
} from '@odf/mco/constants';
77
import { ModalBody } from '@odf/shared/modals';
88
import { getName } from '@odf/shared/selectors';
@@ -11,7 +11,9 @@ import { getErrorMessage } from '@odf/shared/utils';
1111
import { Wizard, WizardStep } from '@patternfly/react-core/deprecated';
1212
import { TFunction } from 'react-i18next';
1313
import { AssignPolicyViewFooter } from './helper/assign-policy-view-footer';
14+
import ProtectionTypeWizardContent from './helper/protection-type-wizard-content';
1415
import { PVCDetailsWizardContent } from './helper/pvc-details-wizard-content';
16+
import { ReplicationTypeWizardContent } from './helper/replication-wizard-content';
1517
import { ReviewAndAssign } from './helper/review-and-assign';
1618
import { SelectPolicyWizardContent } from './helper/select-policy-wizard-content';
1719
import { assignPromises } from './utils/k8s-utils';
@@ -27,6 +29,7 @@ import {
2729
ApplicationType,
2830
DRInfoType,
2931
DRPolicyType,
32+
ModalType,
3033
PlacementType,
3134
PVCQueryFilter,
3235
} from './utils/types';
@@ -42,6 +45,7 @@ export const createSteps = (
4245
dispatch: React.Dispatch<ManagePolicyStateAction>,
4346
protectedPVCSelectors: PVCSelectorType[],
4447
pvcQueryFilter: PVCQueryFilter,
48+
modalType: ModalType,
4549
isEditMode?: boolean
4650
): WizardStep[] => {
4751
const commonSteps = {
@@ -71,13 +75,44 @@ export const createSteps = (
7175
},
7276
reviewAndAssign: {
7377
name: AssignPolicyStepsNames(t)[AssignPolicySteps.ReviewAndAssign],
74-
component: <ReviewAndAssign state={state} />,
78+
component: (
79+
<ReviewAndAssign
80+
state={state}
81+
modalType={modalType}
82+
appType={appType}
83+
/>
84+
),
7585
},
7686
};
7787

78-
switch (appType) {
79-
case DRApplication.APPSET:
80-
case DRApplication.SUBSCRIPTION:
88+
const vmSteps = {
89+
protectionType: {
90+
name: AssignPolicyStepsNames(t)[AssignPolicySteps.ProtectionType],
91+
component: (
92+
<ProtectionTypeWizardContent
93+
protectionType={state.protectionType.protectionType}
94+
protectionName={state.protectionType.protectionName}
95+
appType={appType}
96+
dispatch={dispatch}
97+
/>
98+
),
99+
},
100+
replication: {
101+
name: AssignPolicyStepsNames(t)[AssignPolicySteps.Replication],
102+
component: (
103+
<ReplicationTypeWizardContent
104+
matchingPolicies={matchingPolicies}
105+
policy={state.replication.policy}
106+
k8sResourceSyncInterval={state.replication.k8sSyncInterval}
107+
isValidationEnabled={isValidationEnabled}
108+
dispatch={dispatch}
109+
/>
110+
),
111+
},
112+
};
113+
114+
switch (modalType) {
115+
case ModalType.Application:
81116
return isEditMode
82117
? [
83118
{
@@ -108,6 +143,47 @@ export const createSteps = (
108143
canJumpTo: stepIdReached >= 3,
109144
},
110145
];
146+
case ModalType.VirtualMachine:
147+
return appType === DRApplication.DISCOVERED
148+
? [
149+
{
150+
id: 1,
151+
...vmSteps.protectionType,
152+
canJumpTo: stepIdReached >= 1,
153+
},
154+
{
155+
id: 2,
156+
...vmSteps.replication,
157+
canJumpTo: stepIdReached >= 2,
158+
},
159+
{
160+
id: 3,
161+
...commonSteps.reviewAndAssign,
162+
canJumpTo: stepIdReached >= 3,
163+
},
164+
]
165+
: [
166+
{
167+
id: 1,
168+
...vmSteps.protectionType,
169+
canJumpTo: stepIdReached >= 1,
170+
},
171+
{
172+
id: 2,
173+
...commonSteps.policy,
174+
canJumpTo: stepIdReached >= 2,
175+
},
176+
{
177+
id: 3,
178+
...commonSteps.persistentVolumeClaim,
179+
canJumpTo: stepIdReached >= 3,
180+
},
181+
{
182+
id: 4,
183+
...commonSteps.reviewAndAssign,
184+
canJumpTo: stepIdReached >= 4,
185+
},
186+
];
111187
default:
112188
return [];
113189
}
@@ -121,6 +197,7 @@ export const AssignPolicyView: React.FC<AssignPolicyViewProps> = ({
121197
setModalContext,
122198
setModalActionContext,
123199
dispatch,
200+
modalType,
124201
}) => {
125202
const { t } = useCustomTranslation();
126203
const isEditMode =
@@ -134,6 +211,7 @@ export const AssignPolicyView: React.FC<AssignPolicyViewProps> = ({
134211
placements: unProtectedPlacements,
135212
drInfo,
136213
pvcQueryFilter,
214+
workloadNamespace,
137215
} = applicationInfo;
138216

139217
const protectedPVCSelectors: PVCSelectorType[] = isEditMode
@@ -151,7 +229,13 @@ export const AssignPolicyView: React.FC<AssignPolicyViewProps> = ({
151229

152230
const onSubmit = async () => {
153231
// assign DRPolicy
154-
const promises = assignPromises(state, applicationInfo.placements);
232+
const promises = assignPromises(
233+
state,
234+
applicationInfo.placements,
235+
appType,
236+
workloadNamespace,
237+
getName(applicationInfo)
238+
);
155239
await Promise.all(promises)
156240
.then(() => {
157241
setModalActionContext(
@@ -189,6 +273,7 @@ export const AssignPolicyView: React.FC<AssignPolicyViewProps> = ({
189273
dispatch,
190274
protectedPVCSelectors,
191275
pvcQueryFilter,
276+
modalType,
192277
isEditMode
193278
)}
194279
footer={
@@ -198,7 +283,6 @@ export const AssignPolicyView: React.FC<AssignPolicyViewProps> = ({
198283
stepIdReached={stepIdReached}
199284
isValidationEnabled={isValidationEnabled}
200285
errorMessage={errorMessage}
201-
modalActionContext={modalActionContext}
202286
setStepIdReached={setStepIdReached}
203287
onSubmit={onSubmit}
204288
onCancel={onClose}
@@ -222,4 +306,5 @@ type AssignPolicyViewProps = {
222306
modalActionContext: ModalActionContext,
223307
modalViewContext?: ModalViewContext
224308
) => void;
309+
modalType: ModalType;
225310
};

‎packages/mco/components/modals/app-manage-policies/helper/assign-policy-view-footer.tsx

+29-17
Original file line numberDiff line numberDiff line change
@@ -13,18 +13,15 @@ import {
1313
WizardContext,
1414
WizardFooter,
1515
} from '@patternfly/react-core/deprecated';
16+
import * as _ from 'lodash-es';
1617
import { TFunction } from 'react-i18next';
1718
import {
1819
Button,
1920
Alert,
2021
AlertVariant,
2122
AlertProps,
2223
} from '@patternfly/react-core';
23-
import {
24-
AssignPolicyViewState,
25-
ModalActionContext,
26-
PVCSelectorType,
27-
} from '../utils/reducer';
24+
import { AssignPolicyViewState, PVCSelectorType } from '../utils/reducer';
2825
import { DRPolicyType } from '../utils/types';
2926
import '../../../../style.scss';
3027
import '../style.scss';
@@ -57,16 +54,40 @@ const isPVCSelectorFound = (pvcSelectors: PVCSelectorType[]) =>
5754

5855
const isDRPolicySelected = (dataPolicy: DRPolicyType) => !!getName(dataPolicy);
5956

57+
const validateReplicationStep = (
58+
policy: DRPolicyType,
59+
k8sSyncInterval: string
60+
) =>
61+
!_.isEmpty(policy) &&
62+
k8sSyncInterval !== undefined &&
63+
k8sSyncInterval !== '0m';
64+
65+
const validateProtectionTypeStep = (
66+
protectionName: string,
67+
appType: DRApplication
68+
) => appType !== DRApplication.DISCOVERED || !!protectionName;
69+
6070
const canJumpToNextStep = (
6171
stepName: string,
6272
state: AssignPolicyViewState,
73+
appType: DRApplication,
6374
t: TFunction
6475
) => {
6576
switch (stepName) {
6677
case AssignPolicyStepsNames(t)[AssignPolicySteps.Policy]:
6778
return isDRPolicySelected(state.policy);
6879
case AssignPolicyStepsNames(t)[AssignPolicySteps.PersistentVolumeClaim]:
6980
return isPVCSelectorFound(state.persistentVolumeClaim.pvcSelectors);
81+
case AssignPolicyStepsNames(t)[AssignPolicySteps.ProtectionType]:
82+
return validateProtectionTypeStep(
83+
state.protectionType.protectionName,
84+
appType
85+
);
86+
case AssignPolicyStepsNames(t)[AssignPolicySteps.Replication]:
87+
return validateReplicationStep(
88+
state.replication.policy,
89+
state.replication.k8sSyncInterval
90+
);
7091
default:
7192
return false;
7293
}
@@ -113,10 +134,10 @@ const getErrorMessage = (
113134

114135
export const AssignPolicyViewFooter: React.FC<AssignPolicyViewFooterProps> = ({
115136
state,
137+
appType,
116138
stepIdReached,
117139
isValidationEnabled,
118140
errorMessage,
119-
modalActionContext,
120141
setStepIdReached,
121142
onSubmit,
122143
onCancel,
@@ -130,7 +151,7 @@ export const AssignPolicyViewFooter: React.FC<AssignPolicyViewFooterProps> = ({
130151
const stepId = activeStep.id as number;
131152
const stepName = activeStep.name as string;
132153

133-
const canJumpToNext = canJumpToNextStep(stepName, state, t);
154+
const canJumpToNext = canJumpToNextStep(stepName, state, appType, t);
134155
const validationError = isValidationEnabled && !canJumpToNext;
135156
const message =
136157
(validationError || !!errorMessage) &&
@@ -175,15 +196,7 @@ export const AssignPolicyViewFooter: React.FC<AssignPolicyViewFooterProps> = ({
175196
<Button
176197
variant="secondary"
177198
onClick={onBack}
178-
isDisabled={
179-
stepName === AssignPolicyStepsNames(t)[AssignPolicySteps.Policy] ||
180-
requestInProgress ||
181-
(stepName ===
182-
AssignPolicyStepsNames(t)[
183-
AssignPolicySteps.PersistentVolumeClaim
184-
] &&
185-
modalActionContext === ModalActionContext.EDIT_DR_PROTECTION)
186-
}
199+
isDisabled={stepId === 1 || requestInProgress}
187200
>
188201
{t('Back')}
189202
</Button>
@@ -217,7 +230,6 @@ type AssignPolicyViewFooterProps = {
217230
stepIdReached: number;
218231
isValidationEnabled: boolean;
219232
errorMessage: string;
220-
modalActionContext: ModalActionContext;
221233
setStepIdReached: React.Dispatch<React.SetStateAction<number>>;
222234
onSubmit: () => Promise<void>;
223235
onCancel: () => void;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,188 @@
1+
import * as React from 'react';
2+
import { DISCOVERED_APP_NS, DRApplication } from '@odf/mco/constants';
3+
import { getDRPlacementControlResourceObj } from '@odf/mco/hooks';
4+
import { DRPlacementControlKind } from '@odf/mco/types';
5+
import { getName, StatusBox, useCustomTranslation } from '@odf/shared';
6+
import NameInput from '@odf/shared/input-with-requirements/nameInputWithValidation';
7+
import { useK8sWatchResource } from '@openshift-console/dynamic-plugin-sdk';
8+
import { TFunction } from 'react-i18next';
9+
import {
10+
Alert,
11+
AlertVariant,
12+
Form,
13+
FormGroup,
14+
FormHelperText,
15+
HelperText,
16+
HelperTextItem,
17+
Radio,
18+
} from '@patternfly/react-core';
19+
import {
20+
ManagePolicyStateAction,
21+
ManagePolicyStateType,
22+
ModalViewContext,
23+
} from '../utils/reducer';
24+
import { VMProtectioType } from '../utils/types';
25+
26+
const RADIO_GROUP_NAME = 'vm_protection_method';
27+
28+
const ProtectionNameInput: React.FC<ProtectionNameInputProps> = ({
29+
vmProtectionName,
30+
dispatch,
31+
}) => {
32+
const { t } = useCustomTranslation();
33+
const [drpcs, drpcsLoaded, drpcsLoadError] = useK8sWatchResource<
34+
DRPlacementControlKind[]
35+
>(
36+
getDRPlacementControlResourceObj({
37+
namespace: DISCOVERED_APP_NS,
38+
})
39+
);
40+
41+
const existingProtectionNames = React.useMemo(() => {
42+
// Ensure drpcs is loaded and valid before processing
43+
if (!drpcsLoaded || drpcsLoadError) return [];
44+
45+
return drpcs.map(getName);
46+
}, [drpcs, drpcsLoaded, drpcsLoadError]);
47+
48+
const setProtectionName = (newName: string) => {
49+
dispatch({
50+
type: ManagePolicyStateType.SET_VM_PROTECTION_NAME,
51+
context: ModalViewContext.ASSIGN_POLICY_VIEW,
52+
payload: newName,
53+
});
54+
};
55+
56+
return drpcsLoaded && !drpcsLoadError ? (
57+
<NameInput
58+
label={t('Protection name')}
59+
helperText={t('A unique name to identify and manage this protection.')}
60+
name={vmProtectionName}
61+
existingNames={existingProtectionNames}
62+
onChange={setProtectionName}
63+
/>
64+
) : (
65+
<StatusBox loaded={drpcsLoaded} loadError={drpcsLoadError} />
66+
);
67+
};
68+
69+
const getRadioOptions = (
70+
isDiscoveredApp: boolean,
71+
protectionName: string,
72+
dispatch: React.Dispatch<ManagePolicyStateAction>,
73+
t: TFunction
74+
): RadioOption[] => {
75+
const options: RadioOption[] = [
76+
{
77+
id: 'standalone-vm-protection',
78+
value: VMProtectioType.STANDALONE,
79+
description: t(
80+
'Protect this VM independently without associating it with an existing DR placement control.'
81+
),
82+
label: t('Standalone'),
83+
isDisabled: false,
84+
...(isDiscoveredApp && {
85+
componentRef: (
86+
<ProtectionNameInput
87+
vmProtectionName={protectionName}
88+
dispatch={dispatch}
89+
/>
90+
),
91+
}),
92+
},
93+
{
94+
id: 'shared-vm-protection',
95+
value: VMProtectioType.SHARED,
96+
description: t(
97+
'Add this VM to an existing DR placement control for consistent failover and recovery. This method is only available for discovered VMs.'
98+
),
99+
label: t('Shared'),
100+
isDisabled: !isDiscoveredApp,
101+
},
102+
];
103+
104+
return options;
105+
};
106+
107+
const ProtectionTypeWizardContent: React.FC<
108+
ProtectionTypeWizardContentProps
109+
> = ({ protectionType, protectionName, appType, dispatch }) => {
110+
const { t } = useCustomTranslation();
111+
112+
const isDiscoveredApp = appType === DRApplication.DISCOVERED;
113+
114+
const setProtectionMethod = (event: React.ChangeEvent<HTMLInputElement>) =>
115+
dispatch({
116+
type: ManagePolicyStateType.SET_VM_PROTECTION_METHOD,
117+
context: ModalViewContext.ASSIGN_POLICY_VIEW,
118+
payload: event.target.value as VMProtectioType,
119+
});
120+
121+
const radioOptions = React.useMemo(
122+
() => getRadioOptions(isDiscoveredApp, protectionName, dispatch, t),
123+
[isDiscoveredApp, protectionName, dispatch, t]
124+
);
125+
126+
return (
127+
<Form>
128+
<FormGroup label={t('Protection type')} fieldId="vm-protection-method">
129+
<FormHelperText className="pf-v5-u-mb-sm">
130+
<HelperText>
131+
<HelperTextItem variant="indeterminate">
132+
{t('Choose how you would like to protect this VM:')}
133+
</HelperTextItem>
134+
</HelperText>
135+
</FormHelperText>
136+
137+
{radioOptions.map(
138+
({ id, value, description, label, isDisabled, componentRef }) => (
139+
<Radio
140+
key={id}
141+
id={id}
142+
name={RADIO_GROUP_NAME}
143+
value={value}
144+
description={description}
145+
label={label}
146+
onChange={setProtectionMethod}
147+
isChecked={protectionType === value}
148+
isDisabled={isDisabled}
149+
className="pf-v5-u-mb-md"
150+
body={componentRef}
151+
/>
152+
)
153+
)}
154+
155+
{!isDiscoveredApp && (
156+
<Alert
157+
title={t('Shared protection is not available for managed VMs.')}
158+
variant={AlertVariant.info}
159+
isInline
160+
/>
161+
)}
162+
</FormGroup>
163+
</Form>
164+
);
165+
};
166+
167+
type ProtectionTypeWizardContentProps = {
168+
protectionType: VMProtectioType;
169+
protectionName: string;
170+
appType: DRApplication;
171+
dispatch: React.Dispatch<ManagePolicyStateAction>;
172+
};
173+
174+
type RadioOption = {
175+
id: string;
176+
value: VMProtectioType;
177+
description: string;
178+
label: string;
179+
isDisabled: boolean;
180+
componentRef?: React.ReactNode;
181+
};
182+
183+
type ProtectionNameInputProps = {
184+
vmProtectionName: string;
185+
dispatch: React.Dispatch<ManagePolicyStateAction>;
186+
};
187+
188+
export default ProtectionTypeWizardContent;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
import * as React from 'react';
2+
import {
3+
PolicyInfo,
4+
ReplicationSelectionHelper,
5+
} from '@odf/mco/components/discovered-application-wizard/wizard-steps/replication-step/replication-selection-helper';
6+
import { getName } from '@odf/shared/selectors';
7+
import * as _ from 'lodash-es';
8+
import {
9+
ManagePolicyStateAction,
10+
ManagePolicyStateType,
11+
ModalViewContext,
12+
} from '../utils/reducer';
13+
import { DRPolicyType } from '../utils/types';
14+
import { findPolicy } from './select-policy-wizard-content';
15+
16+
// Convert DRPolicyType to PolicyInfo
17+
const convertToPolicyInfo = (policy?: DRPolicyType): PolicyInfo =>
18+
policy
19+
? {
20+
name: getName(policy),
21+
drClusters: policy?.drClusters || [],
22+
schedulingInterval: policy?.schedulingInterval || '0m',
23+
isValidated: !!policy?.isValidated,
24+
}
25+
: ({} as PolicyInfo);
26+
27+
export const ReplicationTypeWizardContent: React.FC<
28+
ReplicationTypeWizardContentProps
29+
> = ({
30+
policy,
31+
k8sResourceSyncInterval,
32+
matchingPolicies,
33+
isValidationEnabled,
34+
dispatch,
35+
}) => {
36+
// Convert all policies to PolicyInfoBase before filtering
37+
const convertedPolicies = matchingPolicies.map(convertToPolicyInfo);
38+
39+
const setK8sSyncInterval = (syncInterval: string) => {
40+
dispatch({
41+
type: ManagePolicyStateType.SET_K8S_SYNC_INTERVAL,
42+
context: ModalViewContext.ASSIGN_POLICY_VIEW,
43+
payload: syncInterval,
44+
});
45+
};
46+
47+
const setSelectedPolicy = (policyName: string) => {
48+
dispatch({
49+
type: ManagePolicyStateType.SET_SELECTED_POLICY_FOR_REPLICATION,
50+
context: ModalViewContext.ASSIGN_POLICY_VIEW,
51+
payload: findPolicy(policyName, matchingPolicies),
52+
});
53+
};
54+
55+
return (
56+
<ReplicationSelectionHelper
57+
policy={convertToPolicyInfo(policy)}
58+
eligiblePolicies={convertedPolicies}
59+
k8sResourceReplicationInterval={k8sResourceSyncInterval}
60+
isValidationEnabled={isValidationEnabled}
61+
onK8sSyncIntervalChange={setK8sSyncInterval}
62+
onPolicyChange={setSelectedPolicy}
63+
/>
64+
);
65+
};
66+
67+
type ReplicationTypeWizardContentProps = {
68+
policy: DRPolicyType;
69+
k8sResourceSyncInterval: string;
70+
matchingPolicies: DRPolicyType[];
71+
isValidationEnabled: boolean;
72+
dispatch: React.Dispatch<ManagePolicyStateAction>;
73+
};
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import * as React from 'react';
22
import {
3+
DRApplication,
34
REPLICATION_DISPLAY_TEXT,
45
SYNC_SCHEDULE_DISPLAY_TEXT,
56
} from '@odf/mco/constants';
@@ -13,54 +14,143 @@ import {
1314
import { getName } from '@odf/shared/selectors';
1415
import { useCustomTranslation } from '@odf/shared/useCustomTranslationHook';
1516
import { AssignPolicyViewState, PVCSelectorType } from '../utils/reducer';
17+
import { DRPolicyType, ModalType, VMProtectioType } from '../utils/types';
1618
import '../style.scss';
1719

1820
const getLabels = (pvcSelectors: PVCSelectorType[]): string[] =>
1921
pvcSelectors.reduce((acc, selectors) => [...acc, ...selectors.labels], []);
2022

21-
export const ReviewAndAssign: React.FC<ReviewAndAssignProps> = ({ state }) => {
23+
const ProtectionTypeReview: React.FC<{
24+
protectionTypeState: AssignPolicyViewState['protectionType'];
25+
appType: DRApplication;
26+
}> = ({ protectionTypeState: { protectionName, protectionType }, appType }) => {
27+
const { t } = useCustomTranslation();
28+
const showProtectionName =
29+
protectionType === VMProtectioType.STANDALONE &&
30+
appType === DRApplication.DISCOVERED;
31+
const protectionDisplayType =
32+
protectionType === VMProtectioType.STANDALONE
33+
? t('Standalone')
34+
: t('Shared');
35+
36+
return (
37+
<ReviewAndCreationGroup title={t('Protection type')}>
38+
<ReviewAndCreationItem label={t('Protection type:')}>
39+
{protectionDisplayType}
40+
</ReviewAndCreationItem>
41+
{showProtectionName && (
42+
<ReviewAndCreationItem label={t('Protection name:')}>
43+
{protectionName}
44+
</ReviewAndCreationItem>
45+
)}
46+
</ReviewAndCreationGroup>
47+
);
48+
};
49+
50+
const ReplicationReview: React.FC<{
51+
replication: AssignPolicyViewState['replication'];
52+
}> = ({ replication: { policy, k8sSyncInterval } }) => {
53+
const { t } = useCustomTranslation();
54+
const replicationType =
55+
policy.schedulingInterval === '0m'
56+
? REPLICATION_DISPLAY_TEXT(t).sync
57+
: REPLICATION_DISPLAY_TEXT(t).async;
58+
const [unitVal, interval] = parseSyncInterval(k8sSyncInterval);
59+
60+
return (
61+
<ReviewAndCreationGroup title={t('Replication')}>
62+
<>
63+
<ReviewAndCreationItem label={t('Volume replication:')}>
64+
{t('{{policyName}}, {{replicationType}}, Interval: {{interval}}', {
65+
policyName: getName(policy),
66+
replicationType,
67+
interval: policy.schedulingInterval,
68+
})}
69+
</ReviewAndCreationItem>
70+
<ReviewAndCreationItem label={t('Kubernetes object replication:')}>
71+
{`${interval} ${SYNC_SCHEDULE_DISPLAY_TEXT(t)[unitVal]}`}
72+
</ReviewAndCreationItem>
73+
</>
74+
</ReviewAndCreationGroup>
75+
);
76+
};
77+
78+
const PolicyReview: React.FC<{ policy: DRPolicyType }> = ({ policy }) => {
2279
const { t } = useCustomTranslation();
23-
const { policy, persistentVolumeClaim } = state;
2480
const { drClusters, replicationType, schedulingInterval } = policy;
81+
const [unit, interval] = parseSyncInterval(schedulingInterval);
82+
83+
return (
84+
<ReviewAndCreationGroup title={t('Policy')}>
85+
<ReviewAndCreationItem label={t('Policy name:')}>
86+
{getName(policy)}
87+
</ReviewAndCreationItem>
88+
<ReviewAndCreationItem label={t('Clusters:')}>
89+
{drClusters.join(', ')}
90+
</ReviewAndCreationItem>
91+
<ReviewAndCreationItem label={t('Replication type:')}>
92+
{REPLICATION_DISPLAY_TEXT(t)[replicationType]}
93+
</ReviewAndCreationItem>
94+
<ReviewAndCreationItem label={t('Sync interval:')}>
95+
{`${interval} ${SYNC_SCHEDULE_DISPLAY_TEXT(t)[unit]}`}
96+
</ReviewAndCreationItem>
97+
</ReviewAndCreationGroup>
98+
);
99+
};
100+
101+
const PVCDetailsReview: React.FC<{
102+
persistentVolumeClaim: AssignPolicyViewState['persistentVolumeClaim'];
103+
}> = ({ persistentVolumeClaim }) => {
104+
const { t } = useCustomTranslation();
25105
const { pvcSelectors } = persistentVolumeClaim;
106+
const labels = getLabels(pvcSelectors);
26107
const selectorCount = pvcSelectors.length;
27108
const appResourceText =
28109
selectorCount > 1
29110
? t('{{count}} placements', { count: selectorCount })
30111
: pvcSelectors[0].placementName;
31112

32-
const labels = getLabels(pvcSelectors);
33-
34-
const [unit, interval] = parseSyncInterval(schedulingInterval);
113+
return (
114+
<ReviewAndCreationGroup title={t('PVC details')}>
115+
<ReviewAndCreationItem label={t('Application resource:')}>
116+
{appResourceText}
117+
</ReviewAndCreationItem>
118+
<ReviewAndCreationItem label={t('PVC label selector:')}>
119+
<Labels numLabels={5} labels={labels} />
120+
</ReviewAndCreationItem>
121+
</ReviewAndCreationGroup>
122+
);
123+
};
35124

125+
export const ReviewAndAssign: React.FC<ReviewAndAssignProps> = ({
126+
state,
127+
modalType,
128+
appType,
129+
}) => {
36130
return (
37131
<ReviewAndCreateStep>
38-
<ReviewAndCreationGroup title={t('Data policy')}>
39-
<ReviewAndCreationItem label={t('Policy name:')}>
40-
{getName(policy)}
41-
</ReviewAndCreationItem>
42-
<ReviewAndCreationItem label={t('Clusters:')}>
43-
{drClusters.join(', ')}
44-
</ReviewAndCreationItem>
45-
<ReviewAndCreationItem label={t('Replication type:')}>
46-
{REPLICATION_DISPLAY_TEXT(t)[replicationType]}
47-
</ReviewAndCreationItem>
48-
<ReviewAndCreationItem label={t('Sync interval:')}>
49-
{`${interval} ${SYNC_SCHEDULE_DISPLAY_TEXT(t)[unit]}`}
50-
</ReviewAndCreationItem>
51-
</ReviewAndCreationGroup>
52-
<ReviewAndCreationGroup title={t('PVC details')}>
53-
<ReviewAndCreationItem label={t('Application resource:')}>
54-
{appResourceText}
55-
</ReviewAndCreationItem>
56-
<ReviewAndCreationItem label={t('PVC label selector:')}>
57-
<Labels numLabels={5} labels={labels} />
58-
</ReviewAndCreationItem>
59-
</ReviewAndCreationGroup>
132+
{modalType === ModalType.VirtualMachine ? (
133+
<>
134+
<ProtectionTypeReview
135+
protectionTypeState={state.protectionType}
136+
appType={appType}
137+
/>
138+
<ReplicationReview replication={state.replication} />
139+
</>
140+
) : (
141+
<>
142+
<PolicyReview policy={state.policy} />
143+
<PVCDetailsReview
144+
persistentVolumeClaim={state.persistentVolumeClaim}
145+
/>
146+
</>
147+
)}
60148
</ReviewAndCreateStep>
61149
);
62150
};
63151

64152
type ReviewAndAssignProps = {
65153
state: AssignPolicyViewState;
154+
modalType: ModalType;
155+
appType: DRApplication;
66156
};

‎packages/mco/components/modals/app-manage-policies/helper/select-policy-wizard-content.tsx

+1-1
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ const getDropdownOptions = (dataPolicies: DRPolicyType[], t: TFunction) =>
4444
/>
4545
));
4646

47-
const findPolicy = (name: string, dataPolicies: DRPolicyType[]) =>
47+
export const findPolicy = (name: string, dataPolicies: DRPolicyType[]) =>
4848
dataPolicies.find((policy) => getName(policy) === name);
4949

5050
export const SelectPolicyWizardContent: React.FC<

‎packages/mco/components/modals/app-manage-policies/manage-policy-view.tsx

+75-25
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ import {
3030
DISCOVERED_APP_NS,
3131
ReplicationType,
3232
SYNC_SCHEDULE_DISPLAY_TEXT,
33+
VM_RECIPE_NAME,
3334
} from '../../../constants';
3435
import { getDRPlacementControlResourceObj } from '../../../hooks';
3536
import {
@@ -46,9 +47,63 @@ import {
4647
DRInfoType,
4748
DRPlacementControlType,
4849
DRPolicyType,
50+
ModalType,
4951
} from './utils/types';
5052
import './style.scss';
5153

54+
enum EmptyPageContentType {
55+
NamespaceProtected = 'NamespaceProtected',
56+
Application = 'Application',
57+
VirtualMachine = 'VirtualMachine',
58+
}
59+
60+
// Map containing content for each type
61+
const getEmptyPageContentMap = (t: TFunction) => ({
62+
[EmptyPageContentType.NamespaceProtected]: {
63+
title: t('Application already enrolled in disaster recovery'),
64+
content: (
65+
<Trans t={t}>
66+
<p>
67+
This managed application namespace is already DR protected. You may
68+
have protected this namespace while enrolling discovered applications.
69+
</p>
70+
<p className="pf-v5-u-mt-md">
71+
To see disaster recovery information for your applications, go to
72+
<strong> Protected applications </strong> under&nbsp;
73+
<strong> Disaster Recovery </strong>.
74+
</p>
75+
</Trans>
76+
),
77+
buttonText: '',
78+
},
79+
[EmptyPageContentType.Application]: {
80+
title: t('No assigned disaster recovery policy found'),
81+
content: (
82+
<Trans t={t}>
83+
<p>
84+
You have not enrolled this application yet. To protect your
85+
application, click&nbsp;
86+
<strong>Enroll application.</strong>
87+
</p>
88+
</Trans>
89+
),
90+
buttonText: t('Enroll application'),
91+
},
92+
[EmptyPageContentType.VirtualMachine]: {
93+
title: t('No assigned disaster recovery policy found'),
94+
content: (
95+
<Trans t={t}>
96+
<p>
97+
You have not enrolled this virtual machine yet. To protect your
98+
virtual machine, click&nbsp;
99+
<strong>Enroll virtual machine.</strong>
100+
</p>
101+
</Trans>
102+
),
103+
buttonText: t('Enroll virtual machine'),
104+
},
105+
});
106+
52107
const isDRProtectionRemoved = (drpcs: DRPlacementControlType[]) =>
53108
drpcs.every((drpc) => _.has(drpc.metadata, 'deletionTimestamp'));
54109

@@ -59,7 +114,8 @@ const checkNamespaceProtected = (
59114
): boolean =>
60115
drpcs?.some((drpc) => {
61116
const isNamespaceProtected =
62-
drpc.spec?.protectedNamespaces?.includes(workloadNamespace);
117+
drpc.spec?.protectedNamespaces?.includes(workloadNamespace) &&
118+
drpc.spec?.kubeObjectProtection?.recipeRef?.name !== VM_RECIPE_NAME;
63119
const isPolicyMatching = eligiblePolicies?.some(
64120
(policy) => getName(policy) === drpc.spec.drPolicyRef.name
65121
);
@@ -114,6 +170,7 @@ const ManagePolicyEmptyPage: React.FC<ManagePolicyEmptyPageProps> = ({
114170
policyInfoLoaded,
115171
policyInfoLoadError,
116172
onClick,
173+
modalType,
117174
}) => {
118175
const { t } = useCustomTranslation();
119176
const [discoveredApps, loaded, loadError] = useK8sWatchResource<
@@ -130,6 +187,16 @@ const ManagePolicyEmptyPage: React.FC<ManagePolicyEmptyPageProps> = ({
130187
discoveredApps
131188
);
132189

190+
// Determine the correct type dynamically
191+
const emptyPageContentType: EmptyPageContentType = isNamespaceProtected
192+
? EmptyPageContentType.NamespaceProtected
193+
: modalType === ModalType.VirtualMachine
194+
? EmptyPageContentType.VirtualMachine
195+
: EmptyPageContentType.Application;
196+
197+
const { title, content, buttonText } =
198+
getEmptyPageContentMap(t)[emptyPageContentType];
199+
133200
const allLoaded = policyInfoLoaded && loaded;
134201
const anyLoadError = policyInfoLoadError || loadError;
135202

@@ -140,31 +207,10 @@ const ManagePolicyEmptyPage: React.FC<ManagePolicyEmptyPageProps> = ({
140207
isDisabled={isNamespaceProtected}
141208
EmptyIcon={BlueInfoCircleIcon}
142209
onClick={onClick}
143-
buttonText={t('Enroll application')}
144-
title={
145-
isNamespaceProtected
146-
? t('Application already enrolled in disaster recovery')
147-
: t('No assigned disaster recovery policy found')
148-
}
210+
buttonText={buttonText}
211+
title={title}
149212
>
150-
{isNamespaceProtected ? (
151-
<Trans t={t}>
152-
<p>
153-
This managed application namespace is already DR protected. You may
154-
have protected this namespace while enrolling discovered
155-
applications.
156-
</p>
157-
<p className="pf-v5-u-mt-md">
158-
To see disaster recovery information for your applications, go to
159-
<strong>Protected applications</strong> under&nbsp;
160-
<strong>Disaster Recovery</strong>.
161-
</p>
162-
</Trans>
163-
) : (
164-
t(
165-
'You have not enrolled this application yet. To protect your application,'
166-
)
167-
)}
213+
{content}
168214
</EmptyPage>
169215
) : (
170216
<StatusBox loaded={allLoaded} loadError={anyLoadError} />
@@ -301,6 +347,7 @@ export const ManagePolicyView: React.FC<ManagePolicyViewProps> = ({
301347
dispatch,
302348
setModalContext,
303349
setModalActionContext,
350+
modalType,
304351
}) => {
305352
const { t } = useCustomTranslation();
306353
const [localModalActionContext, setLocalModalActionContext] =
@@ -321,6 +368,7 @@ export const ManagePolicyView: React.FC<ManagePolicyViewProps> = ({
321368
setModalActionContext(ModalActionContext.ENABLE_DR_PROTECTION);
322369
setModalContext(ModalViewContext.ASSIGN_POLICY_VIEW);
323370
}}
371+
modalType={modalType}
324372
/>
325373
);
326374
}
@@ -434,6 +482,7 @@ type ManagePolicyViewProps = {
434482
dispatch: React.Dispatch<ManagePolicyStateAction>;
435483
setModalContext: (modalViewContext: ModalViewContext) => void;
436484
setModalActionContext: (modalActionContext: ModalActionContext) => void;
485+
modalType: ModalType;
437486
};
438487

439488
type DRInformationProps = {
@@ -459,6 +508,7 @@ type ManagePolicyEmptyPageProps = {
459508
policyInfoLoaded: boolean;
460509
policyInfoLoadError: any;
461510
onClick: () => void;
511+
modalType: ModalType;
462512
};
463513

464514
type AggregatedDRInfo = {

‎packages/mco/components/modals/app-manage-policies/modal-context-viewer.tsx

+5
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import {
1414
ApplicationType,
1515
DRInfoType,
1616
DRPolicyType,
17+
ModalType,
1718
} from './utils/types';
1819

1920
export const ModalContextViewer: React.FC<ModalContextViewerProps> = ({
@@ -22,6 +23,7 @@ export const ModalContextViewer: React.FC<ModalContextViewerProps> = ({
2223
loaded,
2324
loadError,
2425
setCurrentModalContext,
26+
modalType,
2527
}) => {
2628
const [state, dispatch] = React.useReducer(
2729
managePolicyStateReducer,
@@ -67,6 +69,7 @@ export const ModalContextViewer: React.FC<ModalContextViewerProps> = ({
6769
loaded={loaded}
6870
loadError={loadError}
6971
modalActionContext={state.modalActionContext}
72+
modalType={modalType}
7073
/>
7174
);
7275
}
@@ -80,6 +83,7 @@ export const ModalContextViewer: React.FC<ModalContextViewerProps> = ({
8083
dispatch={dispatch}
8184
setModalContext={setModalContext}
8285
setModalActionContext={setModalActionContext}
86+
modalType={modalType}
8387
/>
8488
);
8589
}
@@ -97,4 +101,5 @@ type ModalContextViewerProps = {
97101
setCurrentModalContext: React.Dispatch<
98102
React.SetStateAction<ModalViewContext>
99103
>;
104+
modalType: ModalType;
100105
};

‎packages/mco/components/modals/app-manage-policies/parsers/application-set-parser.spec.tsx

+1-2
Original file line numberDiff line numberDiff line change
@@ -296,8 +296,7 @@ describe('ApplicationSet manage disaster recovery modal', () => {
296296
expect(screen.getByText('Cancel')).toBeEnabled();
297297

298298
// Headers
299-
expect(screen.getByText('Data policy')).toBeInTheDocument();
300-
expect(screen.getByText('Data policy')).toBeInTheDocument();
299+
screen.getByText(/Policy/i, { selector: 'span' });
301300
// Labels
302301
expect(screen.getByText('Policy name:')).toBeInTheDocument();
303302
expect(screen.getByText('Clusters:')).toBeInTheDocument();

‎packages/mco/components/modals/app-manage-policies/parsers/application-set-parser.tsx

+4
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ import {
3232
ApplicationType,
3333
DRPlacementControlType,
3434
DRPolicyType,
35+
ModalType,
3536
PVCQueryFilter,
3637
} from '../utils/types';
3738

@@ -97,6 +98,7 @@ export const ApplicationSetParser: React.FC<ApplicationSetParserProps> = ({
9798
isWatchApplication,
9899
pvcQueryFilter,
99100
setCurrentModalContext,
101+
modalType,
100102
}) => {
101103
const namespace = getNamespace(application);
102104
const [drResources, drLoaded, drLoadError] = useDisasterRecoveryResourceWatch(
@@ -190,6 +192,7 @@ export const ApplicationSetParser: React.FC<ApplicationSetParserProps> = ({
190192
loaded={loaded}
191193
loadError={loadError}
192194
setCurrentModalContext={setCurrentModalContext}
195+
modalType={modalType ?? ModalType.Application}
193196
/>
194197
);
195198
};
@@ -204,4 +207,5 @@ type ApplicationSetParserProps = {
204207
>;
205208
// ACM search api PVC query filter
206209
pvcQueryFilter?: PVCQueryFilter;
210+
modalType?: ModalType;
207211
};
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
import * as React from 'react';
2+
import { getPlacementKindObj } from '@odf/mco/components/discovered-application-wizard/utils/k8s-utils';
3+
import {
4+
DISCOVERED_APP_NS,
5+
DRApplication,
6+
ODF_RESOURCE_TYPE_LABEL,
7+
} from '@odf/mco/constants';
8+
import {
9+
getDRPlacementControlResourceObj,
10+
getDRPolicyResourceObj,
11+
} from '@odf/mco/hooks';
12+
import { VirtualMachineModel } from '@odf/mco/models';
13+
import { DRPlacementControlKind, DRPolicyKind } from '@odf/mco/types';
14+
import { findDRPolicyUsingDRPC } from '@odf/mco/utils';
15+
import { getName, getNamespace } from '@odf/shared';
16+
import {
17+
K8sResourceCommon,
18+
useK8sWatchResource,
19+
} from '@openshift-console/dynamic-plugin-sdk';
20+
import { ModalContextViewer } from '../modal-context-viewer';
21+
import {
22+
findDRPCUsingVM,
23+
generateApplicationInfo,
24+
generateDRInfo,
25+
generateDRPlacementControlInfo,
26+
generatePlacementInfo,
27+
getMatchingDRPolicies,
28+
} from '../utils/parser-utils';
29+
import { ModalViewContext } from '../utils/reducer';
30+
import {
31+
ApplicationInfoType,
32+
ApplicationType,
33+
DRPolicyType,
34+
ModalType,
35+
PVCQueryFilter,
36+
} from '../utils/types';
37+
38+
export const DiscoveredVMParser: React.FC<DiscoveredVMParserProps> = ({
39+
virtualMachine,
40+
cluster,
41+
pvcQueryFilter,
42+
setCurrentModalContext,
43+
}) => {
44+
// Fetch resources
45+
const [drpcs, drpcsLoaded, drpcsLoadError] = useK8sWatchResource<
46+
DRPlacementControlKind[]
47+
>(
48+
getDRPlacementControlResourceObj({
49+
namespace: DISCOVERED_APP_NS,
50+
selector: {
51+
matchLabels: {
52+
[ODF_RESOURCE_TYPE_LABEL]: VirtualMachineModel.kind.toLowerCase(),
53+
},
54+
},
55+
})
56+
);
57+
58+
const [drPolicies, drPoliciesLoaded, drPoliciesLoadError] =
59+
useK8sWatchResource<DRPolicyKind[]>(getDRPolicyResourceObj());
60+
61+
// Compute loading states
62+
const isLoaded = drpcsLoaded && drPoliciesLoaded;
63+
const loadError = drPoliciesLoadError || drpcsLoadError;
64+
const isLoadedWOError = isLoaded && !loadError;
65+
66+
const applicationInfo: ApplicationInfoType = React.useMemo(() => {
67+
if (!isLoadedWOError) return {};
68+
69+
const vmName = getName(virtualMachine);
70+
const vmNamespace = getNamespace(virtualMachine);
71+
const drpc = findDRPCUsingVM(drpcs, vmName, vmNamespace);
72+
const drPolicy = drpc && findDRPolicyUsingDRPC(drpc, drPolicies);
73+
const placementName =
74+
drpc?.spec.placementRef?.name ?? `${vmName}-placement-1`;
75+
const placementInfo = generatePlacementInfo(
76+
getPlacementKindObj(placementName),
77+
[cluster]
78+
);
79+
const drpcInfo = generateDRPlacementControlInfo(drpc, placementInfo);
80+
81+
return generateApplicationInfo(
82+
DRApplication.DISCOVERED,
83+
virtualMachine,
84+
vmNamespace,
85+
drpcInfo.length ? [] : [placementInfo], // Skip placement if DRPC exists
86+
generateDRInfo(drPolicy, drpcInfo),
87+
pvcQueryFilter
88+
);
89+
}, [
90+
virtualMachine,
91+
cluster,
92+
drpcs,
93+
drPolicies,
94+
isLoadedWOError,
95+
pvcQueryFilter,
96+
]);
97+
98+
const matchingPolicies: DRPolicyType[] = React.useMemo(() => {
99+
return Object.keys(applicationInfo).length
100+
? getMatchingDRPolicies(applicationInfo as ApplicationType, drPolicies)
101+
: [];
102+
}, [applicationInfo, drPolicies]);
103+
104+
return (
105+
<ModalContextViewer
106+
applicationInfo={applicationInfo}
107+
matchingPolicies={matchingPolicies}
108+
loaded={isLoaded}
109+
loadError={loadError}
110+
setCurrentModalContext={setCurrentModalContext}
111+
modalType={ModalType.VirtualMachine}
112+
/>
113+
);
114+
};
115+
116+
type DiscoveredVMParserProps = {
117+
virtualMachine: K8sResourceCommon;
118+
cluster: string;
119+
setCurrentModalContext: React.Dispatch<
120+
React.SetStateAction<ModalViewContext>
121+
>;
122+
pvcQueryFilter?: PVCQueryFilter;
123+
};

‎packages/mco/components/modals/app-manage-policies/parsers/subscription-parser.spec.tsx

+1-2
Original file line numberDiff line numberDiff line change
@@ -296,8 +296,7 @@ describe('Subscription manage disaster recovery modal', () => {
296296
expect(screen.getByText('Cancel')).toBeEnabled();
297297

298298
// Headers
299-
expect(screen.getByText('Data policy')).toBeInTheDocument();
300-
expect(screen.getByText('Data policy')).toBeInTheDocument();
299+
screen.getByText(/Policy/i, { selector: 'span' });
301300
// Labels
302301
expect(screen.getByText('Policy name:')).toBeInTheDocument();
303302
expect(screen.getByText('Clusters:')).toBeInTheDocument();

‎packages/mco/components/modals/app-manage-policies/parsers/subscription-parser.tsx

+4
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ import {
3030
ApplicationType,
3131
DRPlacementControlType,
3232
DRPolicyType,
33+
ModalType,
3334
PlacementType,
3435
PVCQueryFilter,
3536
} from '../utils/types';
@@ -90,6 +91,7 @@ export const SubscriptionParser: React.FC<SubscriptionParserProps> = ({
9091
isWatchApplication,
9192
pvcQueryFilter,
9293
setCurrentModalContext,
94+
modalType,
9395
}) => {
9496
const namespace = getNamespace(application);
9597
const [drResources, drLoaded, drLoadError] = useDisasterRecoveryResourceWatch(
@@ -191,6 +193,7 @@ export const SubscriptionParser: React.FC<SubscriptionParserProps> = ({
191193
loaded={loaded}
192194
loadError={loadError}
193195
setCurrentModalContext={setCurrentModalContext}
196+
modalType={modalType ?? ModalType.Application}
194197
/>
195198
);
196199
};
@@ -208,4 +211,5 @@ type SubscriptionParserProps = {
208211
>;
209212
// ACM search api PVC query filter
210213
pvcQueryFilter?: PVCQueryFilter;
214+
modalType?: ModalType;
211215
};

‎packages/mco/components/modals/app-manage-policies/parsers/virtual-machine-parser.tsx

+47-20
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,13 @@ import {
66
ArgoApplicationSetModel,
77
VirtualMachineModel,
88
} from '@odf/mco/models';
9-
import { SearchResultItemType } from '@odf/mco/types';
9+
import { ArgoApplicationSetKind, SearchResultItemType } from '@odf/mco/types';
1010
import {
1111
getLabelsFromSearchResult,
1212
queryManagedApplicationResourcesForVM,
1313
} from '@odf/mco/utils';
1414
import {
15+
ApplicationKind,
1516
ApplicationModel,
1617
getLabel,
1718
getName,
@@ -20,10 +21,38 @@ import {
2021
} from '@odf/shared';
2122
import { K8sResourceCommon } from '@openshift-console/dynamic-plugin-sdk';
2223
import { ModalViewContext } from '../utils/reducer';
23-
import { PVCQueryFilter } from '../utils/types';
24+
import { ModalType, PVCQueryFilter } from '../utils/types';
2425
import { ApplicationSetParser } from './application-set-parser';
26+
import { DiscoveredVMParser } from './discovered-vm-parser';
2527
import { SubscriptionParser } from './subscription-parser';
2628

29+
const createApplicationSetObj = (
30+
name: string,
31+
namespace: string
32+
): ArgoApplicationSetKind => ({
33+
apiVersion: `${ArgoApplicationSetModel.apiGroup}/${ArgoApplicationSetModel.apiVersion}`,
34+
kind: ArgoApplicationSetModel.kind,
35+
metadata: { name, namespace },
36+
spec: {},
37+
});
38+
39+
const createApplicationObj = (
40+
name: string,
41+
namespace: string
42+
): ApplicationKind => ({
43+
apiVersion: `${ApplicationModel.apiGroup}/${ApplicationModel.apiVersion}`,
44+
kind: ApplicationModel.kind,
45+
metadata: {
46+
name: name,
47+
namespace,
48+
},
49+
spec: {
50+
componentKinds: [
51+
{ group: ACMSubscriptionModel.apiGroup, kind: ACMSubscriptionModel.kind },
52+
],
53+
},
54+
});
55+
2756
const getPVCQueryFilter = (
2857
name: string,
2958
namespace: string,
@@ -71,46 +100,44 @@ export const VirtualMachineParser: React.FC<VirtualMachineParserProps> = ({
71100
(item) => item.cluster === HUB_CLUSTER_NAME
72101
);
73102

74-
const { name, namespace, kind, apigroup } = managedApplication || {};
103+
const { name, namespace, kind } = managedApplication || {};
75104

76105
if (kind === ArgoApplicationSetModel.kind) {
77106
return (
78107
<ApplicationSetParser
79-
application={{
80-
apiVersion: `${ArgoApplicationSetModel.apiGroup}/${ArgoApplicationSetModel.apiVersion}`,
81-
kind,
82-
metadata: { name, namespace },
83-
spec: {},
84-
}}
108+
application={createApplicationSetObj(name, namespace)}
85109
setCurrentModalContext={setCurrentModalContext}
86110
isWatchApplication
87111
pvcQueryFilter={pvcQueryFilter}
112+
modalType={ModalType.VirtualMachine}
88113
/>
89114
);
90115
}
91116

92117
if (kind === ACMSubscriptionModel.kind) {
93118
return (
94119
<SubscriptionParser
95-
application={{
96-
apiVersion: `${ApplicationModel.apiGroup}/${ApplicationModel.apiVersion}`,
97-
kind: ApplicationModel.kind,
98-
metadata: {
99-
name: getLabelsFromSearchResult(managedApplication)?.app?.[0],
100-
namespace,
101-
},
102-
spec: { componentKinds: [{ group: apigroup, kind }] },
103-
}}
120+
application={createApplicationObj(
121+
getLabelsFromSearchResult(managedApplication)?.app?.[0],
122+
namespace
123+
)}
104124
subscriptionName={name}
105125
isWatchApplication
106126
setCurrentModalContext={setCurrentModalContext}
107127
pvcQueryFilter={pvcQueryFilter}
128+
modalType={ModalType.VirtualMachine}
108129
/>
109130
);
110131
}
111132

112-
// TODO: Add support for discovered application type
113-
return null;
133+
return (
134+
<DiscoveredVMParser
135+
virtualMachine={virtualMachine}
136+
setCurrentModalContext={setCurrentModalContext}
137+
pvcQueryFilter={pvcQueryFilter}
138+
cluster={clusterName}
139+
/>
140+
);
114141
};
115142

116143
type VirtualMachineParserProps = {

‎packages/mco/components/modals/app-manage-policies/utils/k8s-utils.ts

+72-3
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,25 @@ import {
33
ACMPlacementRuleModel,
44
DRPlacementControlModel,
55
DRPolicyModel,
6+
VirtualMachineModel,
67
} from '@odf/mco//models';
78
import {
9+
getDRPCKindObj as getDiscoveredDRPCKindObj,
10+
getPlacementKindObj,
11+
} from '@odf/mco/components/discovered-application-wizard/utils/k8s-utils';
12+
import { ProtectionMethodType } from '@odf/mco/components/discovered-application-wizard/utils/reducer';
13+
import {
14+
DISCOVERED_APP_NS,
815
DO_NOT_DELETE_PVC_ANNOTATION_WO_SLASH,
916
DR_SECHEDULER_NAME,
17+
DRApplication,
18+
K8S_RESOURCE_SELECTOR,
1019
PROTECTED_APP_ANNOTATION_WO_SLASH,
20+
PVC_RESOURCE_SELECTOR,
21+
PROTECTED_VMS,
22+
VM_RECIPE_NAME,
23+
VM_NAMESPACE,
24+
ODF_RESOURCE_TYPE_LABEL,
1125
} from '@odf/mco/constants';
1226
import { DRPlacementControlKind } from '@odf/mco/types';
1327
import { convertLabelToExpression, matchClusters } from '@odf/mco/utils';
@@ -28,7 +42,7 @@ import * as _ from 'lodash-es';
2842
import { AssignPolicyViewState } from './reducer';
2943
import { DRPlacementControlType, PlacementType } from './types';
3044

31-
export const getDRPCKindObj = (
45+
export const getManagedDRPCKindObj = (
3246
plsName: string,
3347
plsNamespace: string,
3448
plsKind: string,
@@ -161,7 +175,7 @@ const placementRuleAssignPromise = (placement: PlacementType) => {
161175
const getPlacement = (placementName: string, placements: PlacementType[]) =>
162176
placements.find((placement) => getName(placement) === placementName);
163177

164-
export const assignPromises = (
178+
export const assignPromisesForManaged = (
165179
state: AssignPolicyViewState,
166180
placements: PlacementType[]
167181
) => {
@@ -179,7 +193,7 @@ export const assignPromises = (
179193
promises.push(
180194
k8sCreate({
181195
model: DRPlacementControlModel,
182-
data: getDRPCKindObj(
196+
data: getManagedDRPCKindObj(
183197
getName(placement),
184198
getNamespace(placement),
185199
placement.kind,
@@ -195,3 +209,58 @@ export const assignPromises = (
195209

196210
return promises;
197211
};
212+
213+
export const assignPromisesForDiscovered = (
214+
state: AssignPolicyViewState,
215+
placements: PlacementType[],
216+
vmNamespace: string,
217+
vmName: string
218+
): Promise<K8sResourceKind>[] => {
219+
const {
220+
protectionType: { protectionName },
221+
replication: { k8sSyncInterval, policy },
222+
} = state;
223+
const placementName = `${protectionName}-drpc-placement-1`;
224+
225+
return [
226+
k8sCreate({
227+
model: ACMPlacementModel,
228+
data: getPlacementKindObj(placementName),
229+
}),
230+
k8sCreate({
231+
model: DRPlacementControlModel,
232+
data: getDiscoveredDRPCKindObj({
233+
name: `${protectionName}-drpc`,
234+
preferredCluster: placements[0].deploymentClusters[0],
235+
namespaces: [vmNamespace],
236+
protectionMethod: ProtectionMethodType.RECIPE,
237+
recipeName: VM_RECIPE_NAME,
238+
recipeNamespace: DISCOVERED_APP_NS,
239+
drPolicyName: getName(policy),
240+
k8sResourceReplicationInterval: k8sSyncInterval,
241+
placementName,
242+
pvcLabelExpressions: [],
243+
recipeParameters: {
244+
[K8S_RESOURCE_SELECTOR]: [protectionName],
245+
[PVC_RESOURCE_SELECTOR]: [protectionName],
246+
[PROTECTED_VMS]: [vmName],
247+
[VM_NAMESPACE]: [vmNamespace],
248+
},
249+
labels: {
250+
[ODF_RESOURCE_TYPE_LABEL]: VirtualMachineModel.kind.toLowerCase(),
251+
},
252+
}),
253+
}),
254+
];
255+
};
256+
257+
export const assignPromises = (
258+
state: AssignPolicyViewState,
259+
placements: PlacementType[],
260+
appType: DRApplication,
261+
vmNamespace?: string,
262+
vmName?: string
263+
): Promise<K8sResourceKind>[] =>
264+
appType === DRApplication.DISCOVERED
265+
? assignPromisesForDiscovered(state, placements, vmNamespace, vmName)
266+
: assignPromisesForManaged(state, placements);

‎packages/mco/components/modals/app-manage-policies/utils/parser-utils.ts

+22-4
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,10 @@
1-
import { DRApplication } from '@odf/mco/constants';
1+
import { DRApplication, PROTECTED_VMS } from '@odf/mco/constants';
22
import {
33
getDRClusterResourceObj,
44
getDRPlacementControlResourceObj,
55
getDRPolicyResourceObj,
66
} from '@odf/mco/hooks';
77
import {
8-
ACMApplicationKind,
98
ACMPlacementType,
109
DRPlacementControlKind,
1110
DRPolicyKind,
@@ -18,7 +17,10 @@ import {
1817
} from '@odf/mco/utils';
1918
import { getLatestDate } from '@odf/shared/details-page/datetime';
2019
import { arrayify } from '@odf/shared/modals/EditLabelModal';
21-
import { Selector } from '@openshift-console/dynamic-plugin-sdk';
20+
import {
21+
K8sResourceCommon,
22+
Selector,
23+
} from '@openshift-console/dynamic-plugin-sdk';
2224
import * as _ from 'lodash-es';
2325
import {
2426
ApplicationType,
@@ -93,7 +95,7 @@ export const generateDRPlacementControlInfo = (
9395

9496
export const generateApplicationInfo = (
9597
appType: DRApplication,
96-
application: ACMApplicationKind,
98+
application: K8sResourceCommon,
9799
workloadNamespace: string,
98100
plsInfo: PlacementType[],
99101
drInfo: DRInfoType | {},
@@ -142,3 +144,19 @@ export const getDRResources = (namespace: string) => ({
142144
}),
143145
},
144146
});
147+
148+
const getVMNamesFromRecipe = (
149+
spec?: DRPlacementControlKind['spec']
150+
): string[] =>
151+
spec?.kubeObjectProtection?.recipeParameters?.[PROTECTED_VMS] ?? [];
152+
153+
export const findDRPCUsingVM = (
154+
drpcs: DRPlacementControlKind[] = [],
155+
vmName: string,
156+
vmNamespace: string
157+
): DRPlacementControlKind | undefined =>
158+
drpcs.find(
159+
({ spec }) =>
160+
getVMNamesFromRecipe(spec).includes(vmName) &&
161+
spec?.protectedNamespaces?.includes(vmNamespace)
162+
);

‎packages/mco/components/modals/app-manage-policies/utils/reducer.ts

+90-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { DRPolicyType } from './types';
1+
import { DRPolicyType, VMProtectioType } from './types';
22

33
export enum ModalViewContext {
44
MANAGE_POLICY_VIEW = 'managePolicyView',
@@ -20,17 +20,30 @@ export enum ManagePolicyStateType {
2020
SET_SELECTED_POLICY = 'SET_SELECTED_POLICY',
2121
SET_PVC_SELECTORS = 'SET_PVC_SELECTORS',
2222
RESET_ASSIGN_POLICY_STATE = 'RESET_ASSIGN_POLICY_STATE',
23+
SET_VM_PROTECTION_METHOD = 'SET_VM_PROTECTION_METHOD',
24+
SET_VM_PROTECTION_NAME = 'SET_VM_PROTECTION_NAME',
25+
SET_SELECTED_POLICY_FOR_REPLICATION = 'SET_SELECTED_POLICY_FOR_REPLICATION',
26+
SET_K8S_SYNC_INTERVAL = 'SET_K8S_SYNC_INTERVAL',
2327
}
2428

2529
export type PVCSelectorType = {
2630
placementName: string;
2731
labels: string[];
2832
};
33+
2934
export type AssignPolicyViewState = {
3035
policy: DRPolicyType;
3136
persistentVolumeClaim: {
3237
pvcSelectors: PVCSelectorType[];
3338
};
39+
protectionType?: {
40+
protectionType: VMProtectioType;
41+
protectionName: string;
42+
};
43+
replication?: {
44+
policy: DRPolicyType;
45+
k8sSyncInterval: string;
46+
};
3447
};
3548

3649
export type ManagePolicyState = {
@@ -46,6 +59,14 @@ export const initialPolicyState: ManagePolicyState = {
4659
persistentVolumeClaim: {
4760
pvcSelectors: [],
4861
},
62+
protectionType: {
63+
protectionType: VMProtectioType.STANDALONE,
64+
protectionName: '',
65+
},
66+
replication: {
67+
policy: undefined,
68+
k8sSyncInterval: '5m',
69+
},
4970
},
5071
};
5172

@@ -71,6 +92,26 @@ export type ManagePolicyStateAction =
7192
| {
7293
type: ManagePolicyStateType.RESET_ASSIGN_POLICY_STATE;
7394
context: ModalViewContext;
95+
}
96+
| {
97+
type: ManagePolicyStateType.SET_VM_PROTECTION_METHOD;
98+
context: ModalViewContext;
99+
payload: VMProtectioType;
100+
}
101+
| {
102+
type: ManagePolicyStateType.SET_VM_PROTECTION_NAME;
103+
context: ModalViewContext;
104+
payload: string;
105+
}
106+
| {
107+
type: ManagePolicyStateType.SET_SELECTED_POLICY_FOR_REPLICATION;
108+
context: ModalViewContext;
109+
payload: DRPolicyType;
110+
}
111+
| {
112+
type: ManagePolicyStateType.SET_K8S_SYNC_INTERVAL;
113+
context: ModalViewContext;
114+
payload: string;
74115
};
75116

76117
export const managePolicyStateReducer = (
@@ -120,6 +161,54 @@ export const managePolicyStateReducer = (
120161
},
121162
};
122163
}
164+
case ManagePolicyStateType.SET_VM_PROTECTION_METHOD: {
165+
return {
166+
...state,
167+
[action.context]: {
168+
...state[action.context],
169+
protectionType: {
170+
...state[action.context]['protectionType'],
171+
protectionType: action.payload,
172+
},
173+
},
174+
};
175+
}
176+
case ManagePolicyStateType.SET_VM_PROTECTION_NAME: {
177+
return {
178+
...state,
179+
[action.context]: {
180+
...state[action.context],
181+
protectionType: {
182+
...state[action.context]['protectionType'],
183+
protectionName: action.payload,
184+
},
185+
},
186+
};
187+
}
188+
case ManagePolicyStateType.SET_SELECTED_POLICY_FOR_REPLICATION: {
189+
return {
190+
...state,
191+
[action.context]: {
192+
...state[action.context],
193+
replication: {
194+
...state[action.context]['replication'],
195+
policy: action.payload,
196+
},
197+
},
198+
};
199+
}
200+
case ManagePolicyStateType.SET_K8S_SYNC_INTERVAL: {
201+
return {
202+
...state,
203+
[action.context]: {
204+
...state[action.context],
205+
replication: {
206+
...state[action.context]['replication'],
207+
k8sSyncInterval: action.payload,
208+
},
209+
},
210+
};
211+
}
123212
default:
124213
return state;
125214
}

‎packages/mco/components/modals/app-manage-policies/utils/types.ts

+10
Original file line numberDiff line numberDiff line change
@@ -55,3 +55,13 @@ export type ApplicationType = K8sResourceCommon & {
5555
};
5656

5757
export type ApplicationInfoType = ApplicationType | {};
58+
59+
export enum ModalType {
60+
Application = 'Application',
61+
VirtualMachine = 'VirtualMachine',
62+
}
63+
64+
export enum VMProtectioType {
65+
STANDALONE = 'STANDALONE',
66+
SHARED = 'SHARED',
67+
}

‎packages/mco/constants/common.ts

+3
Original file line numberDiff line numberDiff line change
@@ -23,3 +23,6 @@ export const ADMIN_FLAG = 'ADMIN';
2323
export const DISCOVERED_APP_NS = 'openshift-dr-ops';
2424

2525
export const NAME_NAMESPACE_SPLIT_CHAR = '/';
26+
27+
// Console search optimization selector
28+
export const ODF_RESOURCE_TYPE_LABEL = 'odf.console.search/resourcetype';

‎packages/mco/constants/disaster-recovery.ts

+13
Original file line numberDiff line numberDiff line change
@@ -82,11 +82,15 @@ export enum AssignPolicySteps {
8282
Policy = 'policy',
8383
PersistentVolumeClaim = 'persistent-volume-claim',
8484
ReviewAndAssign = 'review-and-assign',
85+
ProtectionType = 'protection-type',
86+
Replication = 'replication',
8587
}
8688
export const AssignPolicyStepsNames = (t: TFunction) => ({
8789
[AssignPolicySteps.Policy]: t('Policy'),
8890
[AssignPolicySteps.PersistentVolumeClaim]: t('PersistentVolumeClaim'),
8991
[AssignPolicySteps.ReviewAndAssign]: t('Review and assign'),
92+
[AssignPolicySteps.ProtectionType]: t('Protection type'),
93+
[AssignPolicySteps.Replication]: t('Replication'),
9094
});
9195

9296
export const ENROLLED_APP_QUERY_PARAMS_KEY = 'enrolledApp';
@@ -111,3 +115,12 @@ export const MCO_CREATED_BY_LABEL_KEY =
111115
'multicluster.odf.openshift.io/created-by';
112116
export const MCO_CREATED_BY_MC_CONTROLLER =
113117
'odf-multicluster-managedcluster-controller';
118+
119+
// Recipe parameter keys
120+
export const VM_RECIPE_NAME = 'vm-recipe';
121+
export const K8S_RESOURCE_SELECTOR = 'K8S_RESOURCE_SELECTOR';
122+
export const PVC_RESOURCE_SELECTOR = 'PVC_RESOURCE_SELECTOR';
123+
export const PROTECTED_VMS = 'PROTECTED_VMS';
124+
export const VM_NAMESPACE = 'VM_NAMESPACE';
125+
export const K8S_RESOURCE_SELECTOR_LABEL_KEY =
126+
'ramendr.openshift.io/k8s-resource-selector';

‎packages/mco/hooks/mco-resources.ts

+1
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@ export const getDRPlacementControlResourceObj = (
5252
kind: referenceForModel(DRPlacementControlModel),
5353
...(!props?.name ? { isList: true } : {}),
5454
namespaced: !!props?.namespace ? false : true,
55+
...(!!props?.selector ? { selector: props?.selector } : {}),
5556
optional: true,
5657
});
5758

‎packages/mco/types/ramen.ts

+2
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,8 @@ export type DRPlacementControlKind = K8sResourceCommon & {
6666
// To identify all the kube objects that need DR protection.
6767
// N/A for the managed applications.
6868
kubeObjectSelector?: Selector;
69+
// Recipe parameter definitions
70+
recipeParameters?: Record<string, string[]>;
6971
};
7072
// A list of namespaces that are protected by the DRPC.
7173
// N/A for the managed applications.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
import * as React from 'react';
2+
import validationRegEx from '@odf/shared/utils/validation';
3+
import { useForm } from 'react-hook-form';
4+
import { TFunction } from 'react-i18next';
5+
import * as Yup from 'yup';
6+
import { fieldRequirementsTranslations, formSettings } from '../constants';
7+
import { useCustomTranslation } from '../useCustomTranslationHook';
8+
import { useYupValidationResolver } from '../yup-validation-resolver';
9+
import TextInputWithFieldRequirements from './TextInputWithFieldRequirements';
10+
11+
const getInputValidationSchema = (t: TFunction, existingNames: string[]) => {
12+
const fieldRequirements = [
13+
fieldRequirementsTranslations.maxChars(t, 63),
14+
fieldRequirementsTranslations.startAndEndName(t),
15+
fieldRequirementsTranslations.alphaNumericPeriodAdnHyphen(t),
16+
fieldRequirementsTranslations.cannotBeUsedBefore(t),
17+
];
18+
19+
return {
20+
schema: Yup.object({
21+
'name-input': Yup.string()
22+
.required()
23+
.max(63, fieldRequirements[0])
24+
.matches(
25+
validationRegEx.startAndEndsWithAlphanumerics,
26+
fieldRequirements[1]
27+
)
28+
.matches(
29+
validationRegEx.alphaNumericsPeriodsHyphensNonConsecutive,
30+
fieldRequirements[2]
31+
)
32+
.test(
33+
'unique-name',
34+
fieldRequirements[3],
35+
(value: string) => !existingNames.includes(value)
36+
),
37+
}),
38+
fieldRequirements,
39+
};
40+
};
41+
42+
const NameInput: React.FC<NameInputProps> = ({
43+
name,
44+
existingNames = [],
45+
label,
46+
helperText,
47+
fieldId = 'name-input',
48+
dataTest = 'name-input',
49+
onChange,
50+
}) => {
51+
const { t } = useCustomTranslation();
52+
const { schema, fieldRequirements } = React.useMemo(
53+
() => getInputValidationSchema(t, existingNames),
54+
[t, existingNames]
55+
);
56+
57+
const resolver = useYupValidationResolver(schema);
58+
const {
59+
control,
60+
watch,
61+
formState: { isValid },
62+
} = useForm({ ...formSettings, resolver });
63+
64+
const newName = watch(fieldId);
65+
66+
React.useEffect(() => {
67+
onChange(isValid ? newName : '');
68+
}, [isValid, name, onChange, newName]);
69+
70+
return (
71+
<TextInputWithFieldRequirements
72+
control={control}
73+
fieldRequirements={fieldRequirements}
74+
defaultValue={name}
75+
popoverProps={{
76+
headerContent: t('Name requirements'),
77+
footerContent: `${t('Example')}: my-name`,
78+
}}
79+
formGroupProps={{
80+
className: 'pf-v5-u-w-50',
81+
label: label ?? t('Name'),
82+
fieldId,
83+
isRequired: true,
84+
}}
85+
textInputProps={{
86+
id: fieldId,
87+
name: fieldId,
88+
placeholder: t('Enter a unique name'),
89+
'data-test': dataTest,
90+
'aria-label': t('Name input'),
91+
}}
92+
helperText={helperText}
93+
/>
94+
);
95+
};
96+
97+
export type NameInputProps = {
98+
name: string;
99+
existingNames?: string[];
100+
label?: string;
101+
helperText?: string;
102+
fieldId?: string;
103+
dataTest?: string;
104+
onChange: (text: string) => void;
105+
};
106+
107+
export default NameInput;

0 commit comments

Comments
 (0)
Please sign in to comment.