Skip to content

Commit ef83318

Browse files
Disaster recovery UI for Kubevirt VM - Discovered(Standalone)
Signed-off-by: Gowtham Shanmugasundaram <[email protected]>
1 parent 4728f57 commit ef83318

28 files changed

+1367
-319
lines changed

locales/en/plugin__odf-console.json

+22-9
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}}",

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

+9-9
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,7 @@ 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';
@@ -29,13 +25,14 @@ export const getDRPCKindObj = (props: {
2925
preferredCluster: string;
3026
namespaces: string[];
3127
protectionMethod: ProtectionMethodType;
32-
drPolicy: DRPolicyKind;
28+
drPolicyName: string;
3329
k8sResourceReplicationInterval: string;
3430
recipeName?: string;
3531
recipeNamespace?: string;
3632
k8sResourceLabelExpressions?: MatchExpression[];
3733
pvcLabelExpressions?: MatchExpression[];
3834
placementName: string;
35+
recipeParameters?: Record<string, string[]>;
3936
}): DRPlacementControlKind => ({
4037
apiVersion: getAPIVersionForModel(DRPlacementControlModel),
4138
kind: DRPlacementControlModel.kind,
@@ -59,6 +56,7 @@ export const getDRPCKindObj = (props: {
5956
name: props.recipeName,
6057
namespace: props.recipeNamespace,
6158
},
59+
recipeParameters: props.recipeParameters ?? {},
6260
}
6361
: {
6462
kubeObjectSelector: {
@@ -67,7 +65,7 @@ export const getDRPCKindObj = (props: {
6765
}),
6866
},
6967
drPolicyRef: {
70-
name: getName(props.drPolicy),
68+
name: props.drPolicyName,
7169
apiVersion: getAPIVersionForModel(DRPolicyModel),
7270
kind: DRPolicyModel.kind,
7371
},
@@ -81,7 +79,9 @@ export const getDRPCKindObj = (props: {
8179
});
8280

8381
// Dummy placement for the discovered apps DRPC
84-
const getPlacementKindObj = (placementName: string): ACMPlacementKind => ({
82+
export const getPlacementKindObj = (
83+
placementName: string
84+
): ACMPlacementKind => ({
8585
apiVersion: getAPIVersionForModel(ACMPlacementModel),
8686
kind: ACMPlacementModel.kind,
8787
metadata: {
@@ -130,7 +130,7 @@ export const createPromise = (
130130
recipeNamespace,
131131
k8sResourceLabelExpressions,
132132
pvcLabelExpressions,
133-
drPolicy,
133+
drPolicyName: getName(drPolicy),
134134
k8sResourceReplicationInterval,
135135
placementName,
136136
}),
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+
};

0 commit comments

Comments
 (0)