From 8b1843f95928c70f8589ee2ecf7dfa64e2d51314 Mon Sep 17 00:00:00 2001 From: Gowtham Shanmugasundaram Date: Mon, 13 Nov 2023 21:31:49 +0530 Subject: [PATCH] [Part-3] Imperative application manage policy wizard Signed-off-by: Gowtham Shanmugasundaram --- locales/en/plugin__odf-console.json | 33 +++ package.json | 3 +- .../assign-policy-view.tsx | 81 +++++- .../helper/assign-policy-view-footer.tsx | 43 +++- .../helper/dynamic-object-wizard-content.tsx | 230 ++++++++++++++++++ .../helper/policy-rule-wizard-content.tsx | 72 ++++++ .../helper/pvc-details-wizard-content.tsx | 29 +-- .../helper/recipe-selector.tsx | 182 ++++++++++++++ .../helper/review-and-assign.tsx | 77 +++++- .../modals/app-manage-policies/style.scss | 21 ++ .../utils/acm-search-quries.ts | 49 +++- .../app-manage-policies/utils/k8s-utils.ts | 81 +++++- .../app-manage-policies/utils/reducer.ts | 133 ++++++++++ .../modals/app-manage-policies/utils/types.ts | 6 + packages/mco/constants/acm.ts | 6 + packages/mco/constants/disaster-recovery.ts | 4 + packages/mco/models/ramen.ts | 12 + packages/mco/types/ramen.ts | 7 + packages/mco/utils/disaster-recovery.tsx | 35 ++- .../labelExpressionSelector.scss | 5 + .../labelExpressionSelector.tsx | 13 +- .../src/radio-selection/radioSelection.scss | 4 +- .../src/radio-selection/radioSelection.tsx | 10 +- yarn.lock | 16 +- 24 files changed, 1082 insertions(+), 70 deletions(-) create mode 100644 packages/mco/components/modals/app-manage-policies/helper/dynamic-object-wizard-content.tsx create mode 100644 packages/mco/components/modals/app-manage-policies/helper/policy-rule-wizard-content.tsx create mode 100644 packages/mco/components/modals/app-manage-policies/helper/recipe-selector.tsx diff --git a/locales/en/plugin__odf-console.json b/locales/en/plugin__odf-console.json index 974f1da80..b30926033 100644 --- a/locales/en/plugin__odf-console.json +++ b/locales/en/plugin__odf-console.json @@ -218,6 +218,16 @@ "Assign": "Assign", "Next": "Next", "Back": "Back", + "Kubernetes object replication interval": "Kubernetes object replication interval", + "Define the interval for Kubernetes object replication, this is only applicable for Kubernetes object and not application data.": "Define the interval for Kubernetes object replication, this is only applicable for Kubernetes object and not application data.", + "Interval": "Interval", + "Protect Kubernetes objects": "Protect Kubernetes objects", + "For your imperative applications, select a method to protect Kubernetes deployed dynamic objects": "For your imperative applications, select a method to protect Kubernetes deployed dynamic objects", + "Using resource label selector": "Using resource label selector", + "Protect all Kubernetes resources that use the selected resource label selector": "Protect all Kubernetes resources that use the selected resource label selector", + "Add another resource label selector": "Add another resource label selector", + "Using recipes": "Using recipes", + "Protect Kubernetes resources as per rules or in the order defined within the recipe": "Protect Kubernetes resources as per rules or in the order defined within the recipe", "Manage list view alert": "Manage list view alert", "Confirm unassign": "Confirm unassign", "All placements": "All placements", @@ -236,6 +246,13 @@ "View configurations": "View configurations", "No activity": "No activity", "No assigned data policy found": "No assigned data policy found", + "Policy assignment rule": "Policy assignment rule", + "Select the scope of your policy assignment": "Select the scope of your policy assignment", + "This is an OpenShift application type and has shared resources in the namespace.": "This is an OpenShift application type and has shared resources in the namespace.", + "Application-specific": "Application-specific", + "Use to secure only this application. Any existing applications, and new applications within the namespace[namespace] will need to be proactively secured.": "Use to secure only this application. Any existing applications, and new applications within the namespace[namespace] will need to be proactively secured.", + "Namespace-wide": "Namespace-wide", + "Use to secure all applications in the namesapce. All existing and newly created applications present in the namespace sharing the same PVC label selector will be protected when you assign a policy.<1><0>Namespace: ui-git-ansible": "Use to secure all applications in the namesapce. All existing and newly created applications present in the namespace sharing the same PVC label selector will be protected when you assign a policy.<1><0>Namespace: ui-git-ansible", "Delete": "Delete", "Required": "Required", "Select a placement": "Select a placement", @@ -245,8 +262,15 @@ "Use PVC label selectors to effortlessly specify the application resources that need protection.": "Use PVC label selectors to effortlessly specify the application resources that need protection.", "Application resource": "Application resource", "Add application resource": "Add application resource", + "Recipe": "Recipe", + "Select a recipe": "Select a recipe", + "Hide recipe details": "Hide recipe details", + "Show recipe details": "Show recipe details", "{{count}} placements_one": "{{count}} placements", "{{count}} placements_other": "{{count}} placements", + "Policy rule": "Policy rule", + "Policy assignment rule:": "Policy assignment rule:", + "Namesapce:": "Namesapce:", "Data policy": "Data policy", "Policy name:": "Policy name:", "Clusters:": "Clusters:", @@ -255,6 +279,13 @@ "PVC details": "PVC details", "Application resource:": "Application resource:", "PVC label selector:": "PVC label selector:", + "Dynamic objects": "Dynamic objects", + "Protection method:": "Protection method:", + "Resource label selector": "Resource label selector", + "Recipe name:": "Recipe name:", + "Recipe namespace:": "Recipe namespace:", + "Resource label selector:": "Resource label selector:", + "Replication interval:": "Replication interval:", "Replication type: {{type}}, Interval: {{interval}}, Clusters: {{clusters}}": "Replication type: {{type}}, Interval: {{interval}}, Clusters: {{clusters}}", "Replication type: {{type}}, Clusters: {{clusters}}": "Replication type: {{type}}, Clusters: {{clusters}}", "Status: {{status}}": "Status: {{status}}", @@ -271,6 +302,8 @@ "Unable to unassign all selected policies for the application.": "Unable to unassign all selected policies for the application.", "My assigned policies": "My assigned policies", "You haven't set a data policy for your application yet. To protect your application, click the 'Assign data policy' button and select a policy from the available templates.": "You haven't set a data policy for your application yet. To protect your application, click the 'Assign data policy' button and select a policy from the available templates.", + "There was an error while getting the managed resource.": "There was an error while getting the managed resource.", + "Request for ManagedClusterView: {{viewName}} on cluster: {{clusterName}} failed.": "Request for ManagedClusterView: {{viewName}} on cluster: {{clusterName}} failed.", "Relocate in progress": "Relocate in progress", "Failover in progress": "Failover in progress", "List all the connected applications under a policy.": "List all the connected applications under a policy.", diff --git a/package.json b/package.json index b5b4db912..ecf6c70c9 100644 --- a/package.json +++ b/package.json @@ -113,7 +113,8 @@ "webpack": "5.74.0", "webpack-bundle-analyzer": "^4.6.1", "webpack-cli": "4.5.x", - "yup": "^0.32.11" + "yup": "^0.32.11", + "sha1": "1.1.1" }, "devDependencies": { "@cypress/webpack-preprocessor": "^5.9.1", diff --git a/packages/mco/components/modals/app-manage-policies/assign-policy-view.tsx b/packages/mco/components/modals/app-manage-policies/assign-policy-view.tsx index da45997bf..8841ce750 100644 --- a/packages/mco/components/modals/app-manage-policies/assign-policy-view.tsx +++ b/packages/mco/components/modals/app-manage-policies/assign-policy-view.tsx @@ -5,15 +5,19 @@ import { AssignPolicyStepsNames, } from '@odf/mco/constants'; import { ModalBody } from '@odf/shared/modals'; +import { getName } from '@odf/shared/selectors'; import { useCustomTranslation } from '@odf/shared/useCustomTranslationHook'; import { getErrorMessage } from '@odf/shared/utils'; import { TFunction } from 'i18next'; import { Wizard, WizardStep, AlertVariant } from '@patternfly/react-core'; import { AssignPolicyViewFooter } from './helper/assign-policy-view-footer'; +import { DynamicObjectWizardContent } from './helper/dynamic-object-wizard-content'; +import { PolicyRuleWizardContent } from './helper/policy-rule-wizard-content'; import { PVCDetailsWizardContent } from './helper/pvc-details-wizard-content'; import { ReviewAndAssign } from './helper/review-and-assign'; import { SelectPolicyWizardContent } from './helper/select-policy-wizard-content'; import { assignPromises } from './utils/k8s-utils'; +import { getClusterNamesFromPlacements } from './utils/parser-utils'; import { AssignPolicyViewState, ManagePolicyStateAction, @@ -30,8 +34,10 @@ import { } from './utils/types'; export const createSteps = ( + appName: string, appType: APPLICATION_TYPE, workloadNamespace: string, + clusterNames: string[], unProtectedPlacements: PlacementType[], matchingPolicies: DRPolicyType[], state: AssignPolicyViewState, @@ -59,6 +65,7 @@ export const createSteps = ( pvcSelectors={state.persistentVolumeClaim.pvcSelectors} unProtectedPlacements={unProtectedPlacements} workloadNamespace={workloadNamespace} + clusterNames={clusterNames} isValidationEnabled={isValidationEnabled} dispatch={dispatch} /> @@ -66,7 +73,39 @@ export const createSteps = ( }, reviewAndAssign: { name: AssignPolicyStepsNames(t)[AssignPolicySteps.ReviewAndAssign], - component: , + component: ( + + ), + }, + }; + + const imperativeApplicationSteps = { + policyRule: { + name: AssignPolicyStepsNames(t)[AssignPolicySteps.PolicyRule], + component: ( + + ), + }, + dynamicObjects: { + name: AssignPolicyStepsNames(t)[AssignPolicySteps.DynamicObjects], + component: ( + + ), }, }; @@ -90,6 +129,34 @@ export const createSteps = ( canJumpTo: stepIdReached >= 3, }, ]; + case APPLICATION_TYPE.OPENSHIFT: + return [ + { + id: 1, + ...imperativeApplicationSteps.policyRule, + canJumpTo: stepIdReached >= 1, + }, + { + id: 2, + ...commonSteps.policy, + canJumpTo: stepIdReached >= 2, + }, + { + id: 3, + ...commonSteps.persistentVolumeClaim, + canJumpTo: stepIdReached >= 3, + }, + { + id: 4, + ...imperativeApplicationSteps.dynamicObjects, + canJumpTo: stepIdReached >= 4, + }, + { + id: 5, + ...commonSteps.reviewAndAssign, + canJumpTo: stepIdReached >= 5, + }, + ]; default: return []; } @@ -107,8 +174,11 @@ export const AssignPolicyView: React.FC = ({ const { t } = useCustomTranslation(); const [stepIdReached, setStepIdReached] = React.useState(1); const [isValidationEnabled, setIsValidationEnabled] = React.useState(false); - const { type: appType, workloadNamespace, placements } = applicaitonInfo; + const clusterNames = React.useMemo( + () => getClusterNamesFromPlacements(placements), + [placements] + ); const resetAssignState = () => dispatch({ @@ -139,7 +209,7 @@ export const AssignPolicyView: React.FC = ({ resetAssignState(); }; // assign DRPolicy - const promises = assignPromises(state, applicaitonInfo.placements); + const promises = assignPromises(state, appType, applicaitonInfo.placements); await Promise.all(promises) .then(() => { updateContext( @@ -171,8 +241,10 @@ export const AssignPolicyView: React.FC = ({ navAriaLabel={t('Assign policy nav')} mainAriaLabel={t('Assign policy content')} steps={createSteps( - appType, + getName(applicaitonInfo), + APPLICATION_TYPE.OPENSHIFT || appType, workloadNamespace, + clusterNames, placements, matchingPolicies, state, @@ -184,7 +256,6 @@ export const AssignPolicyView: React.FC = ({ footer={ const isDRPolicySelected = (dataPolicy: DRPolicyType) => !!getName(dataPolicy); +const isValidKubeObjectProtection = ({ + captureInterval, + objectProtectionMethod, + recipeInfo, + appResourceSelector, +}: DynamicObjectType) => + !!captureInterval && objectProtectionMethod === ObjectProtectionMethod.Recipe + ? !!recipeInfo + : appResourceSelector.every((selector) => + isLabelOnlyOperator(selector.operator) + ? !!selector.key + : !!selector.key && !!selector.values.length + ); + const canJumpToNextStep = ( stepName: string, state: AssignPolicyViewState, @@ -36,6 +52,10 @@ const canJumpToNextStep = ( return isDRPolicySelected(state.policy); case AssignPolicyStepsNames(t)[AssignPolicySteps.PersistentVolumeClaim]: return isPVCSelectorFound(state.persistentVolumeClaim.pvcSelectors); + case AssignPolicyStepsNames(t)[AssignPolicySteps.PolicyRule]: + return !!state.policyRule; + case AssignPolicyStepsNames(t)[AssignPolicySteps.DynamicObjects]: + return isValidKubeObjectProtection(state.dynamicObjects); default: return false; } @@ -60,6 +80,7 @@ export const AssignPolicyViewFooter: React.FC = ({ const canJumpToNext = canJumpToNextStep(stepName, state, t); const validationError = isValidationEnabled && !canJumpToNext; + const assignPolicyStepsNames = AssignPolicyStepsNames(t); const moveToNextStep = () => { if (canJumpToNext) { @@ -73,7 +94,7 @@ export const AssignPolicyViewFooter: React.FC = ({ const handleNext = async () => { switch (stepName) { - case AssignPolicyStepsNames(t)[AssignPolicySteps.ReviewAndAssign]: + case assignPolicyStepsNames[AssignPolicySteps.ReviewAndAssign]: setRequestInProgress(true); await onSubmit(); setRequestInProgress(false); @@ -104,7 +125,7 @@ export const AssignPolicyViewFooter: React.FC = ({ onClick={handleNext} > {stepName === - AssignPolicyStepsNames(t)[AssignPolicySteps.ReviewAndAssign] + assignPolicyStepsNames[AssignPolicySteps.ReviewAndAssign] ? t('Assign') : t('Next')} @@ -112,10 +133,7 @@ export const AssignPolicyViewFooter: React.FC = ({ @@ -133,7 +151,6 @@ export const AssignPolicyViewFooter: React.FC = ({ type AssignPolicyViewFooterProps = { state: AssignPolicyViewState; - appType: APPLICATION_TYPE; stepIdReached: number; isValidationEnabled: boolean; setStepIdReached: React.Dispatch>; diff --git a/packages/mco/components/modals/app-manage-policies/helper/dynamic-object-wizard-content.tsx b/packages/mco/components/modals/app-manage-policies/helper/dynamic-object-wizard-content.tsx new file mode 100644 index 000000000..d022d5f16 --- /dev/null +++ b/packages/mco/components/modals/app-manage-policies/helper/dynamic-object-wizard-content.tsx @@ -0,0 +1,230 @@ +import * as React from 'react'; +import { + LABEL, + LABEL_SPLIT_CHAR, + SYNC_SCHEDULE_DISPLAY_TEXT, +} from '@odf/mco/constants'; +import { useACMSafeFetch } from '@odf/mco/hooks'; +import { SearchResult } from '@odf/mco/types'; +import { parseSyncInterval, getValueFromSearchResult } from '@odf/mco/utils'; +import { + LazyLabelExpressionSelector, + OptionType, +} from '@odf/shared/label-expression-selector/labelExpressionSelector'; +import { RadioSelection } from '@odf/shared/radio-selection'; +import { useCustomTranslation } from '@odf/shared/useCustomTranslationHook'; +import { RequestSizeInput } from '@odf/shared/utils/RequestSizeInput'; +import { Form, FormGroup } from '@patternfly/react-core'; +import { queryAppResources } from '../utils/acm-search-quries'; +import { + DynamicObjectType, + ManagePolicyStateAction, + ManagePolicyStateType, + ModalViewContext, + ObjectProtectionMethod, + PolicyRule, +} from '../utils/reducer'; +import { RecipeSelector } from './recipe-selector'; +import '../style.scss'; + +const SPLIT_CHAR = '='; + +// Creating options from application resource labels +const createOptionsFromSearchResult = ( + searchResult: SearchResult +): OptionType => { + const labels = getValueFromSearchResult( + searchResult, + LABEL, + LABEL_SPLIT_CHAR + ); + return ( + labels.reduce((acc, label) => { + const [key, value] = label.split(SPLIT_CHAR); + const valueProps = { text: value }; + const keyProps = { text: key }; + if (acc.hasOwnProperty(key)) { + acc[key].values = [...acc[key].values, valueProps]; + } else { + acc[key] = { + key: keyProps, + values: [valueProps], + }; + } + return acc; + }, {} as OptionType) || {} + ); +}; + +const ReplicationInterval: React.FC = ({ + captureInterval, + dispatch, +}) => { + const { t } = useCustomTranslation(); + const [selectedUnit, interval] = parseSyncInterval(captureInterval); + const onChange = (event) => { + const { value, unit } = event; + dispatch({ + type: ManagePolicyStateType.SET_CAPTURE_INTERVAL, + context: ModalViewContext.ASSIGN_POLICY_VIEW, + payload: `${value}${unit}`, + }); + }; + + return ( +
+ + + + + +
+ ); +}; + +export const DynamicObjectWizardContent: React.FC = + ({ + appName, + workLoadNamespace, + dynamicObjects, + isValidationEnabled, + clusterNames, + policyRule, + dispatch, + }) => { + const { + objectProtectionMethod, + appResourceSelector, + captureInterval, + recipeInfo, + } = dynamicObjects; + + const { t } = useCustomTranslation(); + + // ACM search proxy api call + const searchQuery = React.useMemo( + () => + queryAppResources( + appName, + workLoadNamespace, + clusterNames, + policyRule === PolicyRule.Namespace + ), + [appName, workLoadNamespace, clusterNames, policyRule] + ); + const [searchResult] = useACMSafeFetch(searchQuery); + + // Generate options + const options = React.useMemo( + () => createOptionsFromSearchResult(searchResult), + [searchResult] + ); + + const onChange = (method: string) => { + dispatch({ + type: ManagePolicyStateType.SET_OBJECT_PROTECTION_METHOD, + context: ModalViewContext.ASSIGN_POLICY_VIEW, + payload: method as ObjectProtectionMethod, + }); + }; + + return ( + <> + { + dispatch({ + type: ManagePolicyStateType.SET_APP_RESOURCE_SELECTOR, + context: ModalViewContext.ASSIGN_POLICY_VIEW, + payload: expression, + }); + }} + preSelected={appResourceSelector} + options={options} + isValidationEnabled={isValidationEnabled} + /> + ), + }, + { + className: 'mco-manage-policies__radioBody--width', + id: ObjectProtectionMethod.Recipe, + name: ObjectProtectionMethod.Recipe, + value: ObjectProtectionMethod.Recipe, + label: t('Using recipes'), + description: t( + 'Protect Kubernetes resources as per rules or in the order defined within the recipe' + ), + onChange, + body: objectProtectionMethod === + ObjectProtectionMethod.Recipe && ( + + ), + }, + ]} + /> + + + ); + }; + +type DynamicObjectWizardContentProps = { + appName: string; + workLoadNamespace: string; + clusterNames: string[]; + policyRule: PolicyRule; + dynamicObjects: DynamicObjectType; + isValidationEnabled: boolean; + dispatch: React.Dispatch; +}; + +type ReplicationIntervalProps = { + captureInterval: string; + dispatch: React.Dispatch; +}; diff --git a/packages/mco/components/modals/app-manage-policies/helper/policy-rule-wizard-content.tsx b/packages/mco/components/modals/app-manage-policies/helper/policy-rule-wizard-content.tsx new file mode 100644 index 000000000..f0da7b012 --- /dev/null +++ b/packages/mco/components/modals/app-manage-policies/helper/policy-rule-wizard-content.tsx @@ -0,0 +1,72 @@ +import * as React from 'react'; +import { RadioSelection } from '@odf/shared/radio-selection'; +import { useCustomTranslation } from '@odf/shared/useCustomTranslationHook'; +import { Trans } from 'react-i18next'; +import { + ManagePolicyStateAction, + ManagePolicyStateType, + ModalViewContext, + PolicyRule, +} from '../utils/reducer'; + +export const PolicyRuleWizardContent: React.FC = + ({ policyRule, dispatch }) => { + const { t } = useCustomTranslation(); + const onChange = (ruleType: string) => { + dispatch({ + type: ManagePolicyStateType.SET_POLICY_RULE, + context: ModalViewContext.ASSIGN_POLICY_VIEW, + payload: ruleType as PolicyRule, + }); + }; + + return ( + <> + + Use to secure all applications in the namesapce. All existing + and newly created applications present in the namespace + sharing the same PVC label selector will be protected when you + assign a policy. +

+ Namespace: ui-git-ansible +

+ + ), + onChange, + }, + ]} + /> + + ); + }; + +type PolicyRuleWizardContentProps = { + policyRule: PolicyRule; + dispatch: React.Dispatch; +}; diff --git a/packages/mco/components/modals/app-manage-policies/helper/pvc-details-wizard-content.tsx b/packages/mco/components/modals/app-manage-policies/helper/pvc-details-wizard-content.tsx index 57743e029..9d9763989 100644 --- a/packages/mco/components/modals/app-manage-policies/helper/pvc-details-wizard-content.tsx +++ b/packages/mco/components/modals/app-manage-policies/helper/pvc-details-wizard-content.tsx @@ -1,6 +1,7 @@ import * as React from 'react'; +import { LABEL, LABEL_SPLIT_CHAR } from '@odf/mco/constants'; import { useACMSafeFetch } from '@odf/mco/hooks/acm-safe-fetch'; -import { SearchResult } from '@odf/mco/types'; +import { getValueFromSearchResult } from '@odf/mco/utils'; import { MultiSelectDropdown } from '@odf/shared/dropdown/multiselectdropdown'; import { SingleSelectDropdown } from '@odf/shared/dropdown/singleselectdropdown'; import { getName } from '@odf/shared/selectors'; @@ -21,8 +22,7 @@ import { Text, } from '@patternfly/react-core'; import { MinusCircleIcon } from '@patternfly/react-icons'; -import { queryAppWorkloadPVCs } from '../utils/acm-search-quries'; -import { getClusterNamesFromPlacements } from '../utils/parser-utils'; +import { queryResourceKind } from '../utils/acm-search-quries'; import { ManagePolicyStateAction, ManagePolicyStateType, @@ -33,9 +33,6 @@ import { PlacementType } from '../utils/types'; import '../../../../style.scss'; import '../style.scss'; -const LABEL = 'label'; -const SPLIT_CHAR = '; '; - const getPlacementTags = (pvcSelectors: PVCSelectorType[]) => !!pvcSelectors.length ? pvcSelectors.map((pvcSelector) => @@ -43,15 +40,6 @@ const getPlacementTags = (pvcSelectors: PVCSelectorType[]) => ) : [[]]; -const getLabelsFromSearchResult = (searchResult: SearchResult): string[] => { - const pvcLabels = - searchResult?.data.searchResult?.[0]?.items.reduce( - (acc, item) => [...acc, ...(item[LABEL]?.split(SPLIT_CHAR) || [])], - [] - ) || []; - return Array.from(new Set(pvcLabels)); -}; - const getLabelsFromTags = (tags: TagsType, currIndex: number): string[] => tags.reduce((acc, tag, index) => { const labels: string[] = (tag?.[1] || []) as string[]; @@ -216,6 +204,7 @@ export const PVCDetailsWizardContent: React.FC = pvcSelectors, unProtectedPlacements, workloadNamespace, + clusterNames, isValidationEnabled, dispatch, }) => { @@ -235,17 +224,18 @@ export const PVCDetailsWizardContent: React.FC = // ACM search proxy api call const searchQuery = React.useMemo( () => - queryAppWorkloadPVCs( + queryResourceKind( + 'persistentvolumeclaim', workloadNamespace, - getClusterNamesFromPlacements(unProtectedPlacements) + clusterNames ), - [unProtectedPlacements, workloadNamespace] + [clusterNames, workloadNamespace] ); const [searchResult] = useACMSafeFetch(searchQuery); // All labels const labels: string[] = React.useMemo( - () => getLabelsFromSearchResult(searchResult), + () => getValueFromSearchResult(searchResult, LABEL, LABEL_SPLIT_CHAR), [searchResult] ); @@ -303,6 +293,7 @@ type PVCDetailsWizardContentProps = { pvcSelectors: PVCSelectorType[]; unProtectedPlacements: PlacementType[]; workloadNamespace: string; + clusterNames: string[]; isValidationEnabled: boolean; dispatch: React.Dispatch; }; diff --git a/packages/mco/components/modals/app-manage-policies/helper/recipe-selector.tsx b/packages/mco/components/modals/app-manage-policies/helper/recipe-selector.tsx new file mode 100644 index 000000000..828c2101b --- /dev/null +++ b/packages/mco/components/modals/app-manage-policies/helper/recipe-selector.tsx @@ -0,0 +1,182 @@ +import * as React from 'react'; +import { HUB_CLUSTER_NAME, NAME } from '@odf/mco/constants'; +import { useACMSafeFetch } from '@odf/mco/hooks'; +import { ACMManagedClusterViewModel, DRRecipeModel } from '@odf/mco/models'; +import { ACMManagedClusterViewKind, SearchResult } from '@odf/mco/types'; +import { getValueFromSearchResult } from '@odf/mco/utils'; +import { SingleSelectDropdown } from '@odf/shared/dropdown'; +import { getName, getNamespace } from '@odf/shared/selectors'; +import { useCustomTranslation } from '@odf/shared/useCustomTranslationHook'; +import { getAPIVersionForModel, getValidatedProp } from '@odf/shared/utils'; +import { + K8sResourceCommon, + k8sCreate, +} from '@openshift-console/dynamic-plugin-sdk'; +import { safeDump } from 'js-yaml'; +import * as Sha1 from 'sha1'; +import { + SelectOption, + Form, + FormGroup, + CodeBlock, + CodeBlockCode, + Button, + ButtonVariant, +} from '@patternfly/react-core'; +import { queryResourceKind } from '../utils/acm-search-quries'; +import { pollManagedClusterView } from '../utils/k8s-utils'; +import { + ManagePolicyStateAction, + ManagePolicyStateType, + ModalViewContext, + RecipeInfoType, +} from '../utils/reducer'; +import '../style.scss'; + +const generateOptionsFromSearchResult = (searchResult: SearchResult) => + getValueFromSearchResult(searchResult, NAME); + +const getRecipeMCVPayload = ( + recipeInfo: RecipeInfoType, + clusterNames: string[] +): ACMManagedClusterViewKind => { + const { name: resourceName, namespace: resourceNamespace } = recipeInfo; + const clusterName = clusterNames[0]; + const viewName = Sha1( + `${clusterName}-${resourceName}-${DRRecipeModel.kind}` + ).substr(0, 63); + return { + apiVersion: getAPIVersionForModel(ACMManagedClusterViewModel), + kind: ACMManagedClusterViewModel.kind, + metadata: { name: viewName, namespace: clusterName }, + spec: { + scope: { + name: resourceName, + resource: DRRecipeModel.kind, + namespace: resourceNamespace, + }, + }, + }; +}; + +export const RecipeSelector: React.FC = ({ + clusterNames, + workLoadNamespace, + isValidationEnabled, + recipeInfo, + dispatch, +}) => { + const { t } = useCustomTranslation(); + const [recipe, selectRecipe] = React.useState(recipeInfo?.name); + const [recipeYAML, setRecipeYAML] = React.useState(''); + const [isHideCodeBlock, setIsHideCodeBlock] = React.useState(true); + // ACM search proxy api call + const searchQuery = React.useMemo( + () => + queryResourceKind(DRRecipeModel.kind, workLoadNamespace, clusterNames), + [workLoadNamespace, clusterNames] + ); + const [searchResult] = useACMSafeFetch(searchQuery); + + // Generate options + const options = React.useMemo( + () => generateOptionsFromSearchResult(searchResult) || [], + [searchResult] + ); + + // Read recipe using MCV + React.useEffect(() => { + if (!!recipeInfo) { + const recipeMCVPayload = getRecipeMCVPayload(recipeInfo, clusterNames); + try { + // Create recipe MCV + k8sCreate({ + model: ACMManagedClusterViewModel, + data: recipeMCVPayload, + cluster: HUB_CLUSTER_NAME, + }).then(() => { + // Read and delete recipe MCV + pollManagedClusterView( + getName(recipeMCVPayload), + getNamespace(recipeMCVPayload), + t + ).then((viewResponse) => + setRecipeYAML(safeDump(viewResponse.result, { lineWidth: -1 })) + ); + }); + } catch (err) { + // eslint-disable-next-line no-console + console.error(err); + } + } + }, [recipeInfo, clusterNames, setRecipeYAML, t]); + + const isVaildRecipe = getValidatedProp(isValidationEnabled && !recipe); + const onChange = (selected: string) => { + selectRecipe(selected); + dispatch({ + type: ManagePolicyStateType.SET_RECIPE_INFO, + context: ModalViewContext.ASSIGN_POLICY_VIEW, + payload: { + name: selected, + namespace: workLoadNamespace, + }, + }); + }; + + return ( +
+ + ( + + ))} + onChange={onChange} + /> + + {!!recipe && ( + + {!isHideCodeBlock && ( + + {recipeYAML} + + )} + + + )} +
+ ); +}; + +type RecipeSelectorProps = { + clusterNames: string[]; + workLoadNamespace: string; + recipeInfo: RecipeInfoType; + isValidationEnabled: boolean; + dispatch: React.Dispatch; +}; diff --git a/packages/mco/components/modals/app-manage-policies/helper/review-and-assign.tsx b/packages/mco/components/modals/app-manage-policies/helper/review-and-assign.tsx index 7f438c051..f770fca87 100644 --- a/packages/mco/components/modals/app-manage-policies/helper/review-and-assign.tsx +++ b/packages/mco/components/modals/app-manage-policies/helper/review-and-assign.tsx @@ -1,6 +1,10 @@ import * as React from 'react'; -import { SYNC_SCHEDULE_DISPLAY_TEXT } from '@odf/mco/constants'; +import { + APPLICATION_TYPE, + SYNC_SCHEDULE_DISPLAY_TEXT, +} from '@odf/mco/constants'; import { parseSyncInterval } from '@odf/mco/utils'; +import { matchExpressionSummary } from '@odf/shared/label-expression-selector'; import { Labels } from '@odf/shared/labels'; import { ReviewAndCreateStep, @@ -9,29 +13,60 @@ import { } from '@odf/shared/review-and-create-step'; import { getName } from '@odf/shared/selectors'; import { useCustomTranslation } from '@odf/shared/useCustomTranslationHook'; -import { AssignPolicyViewState, PVCSelectorType } from '../utils/reducer'; -import '../style.scss'; +import { + AssignPolicyViewState, + ObjectProtectionMethod, + PVCSelectorType, +} from '../utils/reducer'; const getLabels = (pvcSelectors: PVCSelectorType[]): string[] => pvcSelectors.reduce((acc, selectors) => [...acc, ...selectors.labels], []); -export const ReviewAndAssign: React.FC = ({ state }) => { +export const ReviewAndAssign: React.FC = ({ + state, + appType, + workloadNamespace, +}) => { const { t } = useCustomTranslation(); - const { policy, persistentVolumeClaim } = state; + const { policy, persistentVolumeClaim, policyRule, dynamicObjects } = state; const { drClusters, replicationType, schedulingInterval } = policy; const { pvcSelectors } = persistentVolumeClaim; + const { + captureInterval, + objectProtectionMethod, + recipeInfo, + appResourceSelector, + } = dynamicObjects; + const labels = React.useMemo(() => getLabels(pvcSelectors), [pvcSelectors]); + const expressions = React.useMemo( + () => + appResourceSelector.map((expression) => + matchExpressionSummary(t, expression) + ), + [appResourceSelector, t] + ); const selectorCount = pvcSelectors.length; const appResourceText = selectorCount > 1 ? t('{{count}} placements', { count: selectorCount }) : pvcSelectors[0].placementName; - - const labels = React.useMemo(() => getLabels(pvcSelectors), [pvcSelectors]); - const [unit, interval] = parseSyncInterval(schedulingInterval); + const isOpenshiftApp = appType === APPLICATION_TYPE.OPENSHIFT; + const isRecipeMethod = + objectProtectionMethod === ObjectProtectionMethod.Recipe; return ( + {isOpenshiftApp && ( + + + {policyRule} + + + {workloadNamespace} + + + )} {getName(policy)} @@ -54,10 +89,36 @@ export const ReviewAndAssign: React.FC = ({ state }) => { + {isOpenshiftApp && ( + + + {isRecipeMethod ? t('Recipe') : t('Resource label selector')} + + {isRecipeMethod ? ( + <> + + {recipeInfo.name} + + + {recipeInfo.namespace} + + + ) : ( + + {expressions.join(', ')} + + )} + + {captureInterval} + + + )} ); }; type ReviewAndAssignProps = { state: AssignPolicyViewState; + workloadNamespace: string; + appType: APPLICATION_TYPE; }; diff --git a/packages/mco/components/modals/app-manage-policies/style.scss b/packages/mco/components/modals/app-manage-policies/style.scss index 9d48501fd..3bc6669e7 100644 --- a/packages/mco/components/modals/app-manage-policies/style.scss +++ b/packages/mco/components/modals/app-manage-policies/style.scss @@ -26,4 +26,25 @@ &__alert--margin-left { margin-left: var(--pf-global--spacer--lg); } + &__radioBody--width { + .pf-c-radio__body { + width: 100% !important; + } + } + &__selector--font-size { + font-size: var(--pf-global--FontSize--sm); + .pf-c-check__label { + font-size: var(--pf-global--FontSize--sm) !important; + } + } + &__codeblock--height { + max-height: 11rem; + overflow-y: auto; + } + &__recipe--dropdown-wide { + width: 15rem; + } + &__dynamicObject--padding { + padding: 0 0 var(--pf-global--spacer--md) 0; + } } diff --git a/packages/mco/components/modals/app-manage-policies/utils/acm-search-quries.ts b/packages/mco/components/modals/app-manage-policies/utils/acm-search-quries.ts index 695170ed2..a27143105 100644 --- a/packages/mco/components/modals/app-manage-policies/utils/acm-search-quries.ts +++ b/packages/mco/components/modals/app-manage-policies/utils/acm-search-quries.ts @@ -1,6 +1,8 @@ import { SearchQuery } from '@odf/mco/types'; -export const queryAppWorkloadPVCs = ( +// ACM search query to fetch metadata of any CR from managed cluster. +export const queryResourceKind = ( + kind: string, workloadNamespace: string, clusterNames: string[] ): SearchQuery => ({ @@ -11,7 +13,7 @@ export const queryAppWorkloadPVCs = ( filters: [ { property: 'kind', - values: ['persistentvolumeclaim'], + values: [kind], }, { property: 'namespace', @@ -22,10 +24,51 @@ export const queryAppWorkloadPVCs = ( values: clusterNames, }, ], - limit: 20, + limit: 50, }, ], }, query: 'query searchResult($input: [SearchInput]) {\n searchResult: search(input: $input) {\n items\n }\n}', }); + +// ACM seach query to fetch all releated resources of this application from the managed cluster. +// isNamesapceWideSearch is used to restric the search to application specific or namesapce wide. +export const queryAppResources = ( + appName: string, + workLoadNamespace: string, + clusterNames: string[], + isNamesapceWideSearch: boolean +): SearchQuery => ({ + operationName: 'searchResultRelatedItems', + variables: { + input: [ + { + filters: [ + ...(!isNamesapceWideSearch + ? [ + { + property: 'label', + values: [ + `app=${appName}`, + `app.kubernetes.io/part-of=${appName}`, + ], + }, + ] + : []), + { + property: 'namespace', + values: [workLoadNamespace], + }, + { + property: 'cluster', + values: clusterNames, + }, + ], + limit: 200, + }, + ], + }, + query: + 'query searchResultRelatedItems($input: [SearchInput]) {\n searchResult: search(input: $input) {\n items\n related {\n kind\n items\n __typename\n }\n __typename\n }\n}', +}); diff --git a/packages/mco/components/modals/app-manage-policies/utils/k8s-utils.ts b/packages/mco/components/modals/app-manage-policies/utils/k8s-utils.ts index 13b0013b8..0a46293b6 100644 --- a/packages/mco/components/modals/app-manage-policies/utils/k8s-utils.ts +++ b/packages/mco/components/modals/app-manage-policies/utils/k8s-utils.ts @@ -1,13 +1,16 @@ import { + ACMManagedClusterViewModel, ACMPlacementModel, ACMPlacementRuleModel, DRPlacementControlModel, } from '@odf/mco//models'; import { + APPLICATION_TYPE, DR_SECHEDULER_NAME, HUB_CLUSTER_NAME, PROTECTED_APP_ANNOTATION_WO_SLASH, } from '@odf/mco/constants'; +import { ACMManagedClusterViewKind } from '@odf/mco/types'; import { getDRPCKindObj } from '@odf/mco/utils'; import { getAPIVersion, @@ -20,10 +23,13 @@ import { k8sDelete, k8sPatch, k8sCreate, + k8sGet, + K8sResourceCommon, } from '@openshift-console/dynamic-plugin-sdk'; +import { TFunction } from 'i18next'; import * as _ from 'lodash-es'; import { AssignPolicyViewState } from './reducer'; -import { DRPlacementControlType, PlacementType } from './types'; +import { DRPlacementControlType, PlacementType, ViewResponse } from './types'; export const placementUnAssignPromise = (drpc: DRPlacementControlType) => { const patch = [ @@ -123,9 +129,11 @@ const getPlacement = (placementName: string, placements: PlacementType[]) => export const assignPromises = ( state: AssignPolicyViewState, + appType: APPLICATION_TYPE, placements: PlacementType[] ) => { - const { policy, persistentVolumeClaim } = state; + const { policy, persistentVolumeClaim, dynamicObjects } = state; + const { recipeInfo, captureInterval } = dynamicObjects; const { pvcSelectors } = persistentVolumeClaim; const promises: Promise[] = []; pvcSelectors?.forEach((pvcSelector) => { @@ -140,6 +148,7 @@ export const assignPromises = ( k8sCreate({ model: DRPlacementControlModel, data: getDRPCKindObj( + appType, getName(placement), getNamespace(placement), placement.kind, @@ -147,7 +156,10 @@ export const assignPromises = ( getName(policy), policy.drClusters, placement.deploymentClusters, - labels + labels, + captureInterval, + recipeInfo?.name, + recipeInfo?.namespace ), cluster: HUB_CLUSTER_NAME, }) @@ -156,3 +168,66 @@ export const assignPromises = ( return promises; }; + +export const pollManagedClusterView = ( + viewName: string, + clusterName: string, + t: TFunction +): Promise> => { + let retries = 20; + const poll = async (resolve: any, reject: any) => { + const response: ACMManagedClusterViewKind = await k8sGet({ + model: ACMManagedClusterViewModel, + name: viewName, + ns: clusterName, + cluster: HUB_CLUSTER_NAME, + }); + if (response?.status) { + const condition = response.status?.conditions?.[0]; + const { type: isProcessing, reason, message } = condition || {}; + if (isProcessing === 'Processing') { + reason === 'GetResourceProcessing' + ? resolve({ + processing: isProcessing, + reason: reason, + result: response.status?.result, + }) + : // Reading is failed + reject({ + message: message, + }); + + // Delete MCV after reading + k8sDelete({ + resource: response, + model: ACMManagedClusterViewModel, + requestInit: null, + cluster: HUB_CLUSTER_NAME, + }); + } else { + // ACM unale to process the MCV + reject({ + message: t('There was an error while getting the managed resource.'), + }); + } + } else if (retries-- > 0) { + // eslint-disable-next-line no-console + console.debug('MCV poll - retries left: ', retries); + setTimeout(poll, 100, resolve, reject); + } else { + k8sDelete({ + resource: response, + model: ACMManagedClusterViewModel, + requestInit: null, + cluster: HUB_CLUSTER_NAME, + }); + reject({ + message: t( + 'Request for ManagedClusterView: {{viewName}} on cluster: {{clusterName}} failed.', + { viewName, clusterName } + ), + }); + } + }; + return new Promise(poll); +}; diff --git a/packages/mco/components/modals/app-manage-policies/utils/reducer.ts b/packages/mco/components/modals/app-manage-policies/utils/reducer.ts index 5b5f61454..9e2215158 100644 --- a/packages/mco/components/modals/app-manage-policies/utils/reducer.ts +++ b/packages/mco/components/modals/app-manage-policies/utils/reducer.ts @@ -1,3 +1,4 @@ +import { MatchExpression } from '@openshift-console/dynamic-plugin-sdk'; import { AlertVariant } from '@patternfly/react-core'; import { DataPolicyType } from './types'; @@ -23,6 +24,21 @@ export enum ManagePolicyStateType { SET_MESSAGE = 'SET_MESSAGE', SET_PVC_SELECTORS = 'SET_PVC_SELECTORS', RESET_ASSIGN_POLICY_STATE = 'RESET_ASSIGN_POLICY_STATE', + SET_POLICY_RULE = 'SET_POLICY_RULE', + SET_APP_RESOURCE_SELECTOR = 'SET_APP_RESOURCE_SELECTOR', + SET_RECIPE_INFO = 'SET_RECIPE_INFO', + SET_CAPTURE_INTERVAL = 'SET_CAPTURE_INTERVAL', + SET_OBJECT_PROTECTION_METHOD = 'SET_OBJECT_PROTECTION_METHOD', +} + +export enum ObjectProtectionMethod { + ResourceLabelSelector = 'ResourceLabelSelector', + Recipe = 'Recipe', +} + +export enum PolicyRule { + Namespace = 'Namespace', + Application = 'Application', } export type PVCSelectorType = { @@ -30,6 +46,18 @@ export type PVCSelectorType = { labels: string[]; }; +export type RecipeInfoType = { + name: string; + namespace: string; +}; + +export type DynamicObjectType = { + objectProtectionMethod: ObjectProtectionMethod; + appResourceSelector?: MatchExpression[]; + recipeInfo: RecipeInfoType; + captureInterval: string; +}; + export type MessageType = { title: string; description?: string; @@ -54,6 +82,8 @@ export type AssignPolicyViewState = CommonViewState & { persistentVolumeClaim: { pvcSelectors: PVCSelectorType[]; }; + policyRule: PolicyRule; + dynamicObjects: DynamicObjectType; }; export type ManagePolicyState = { @@ -63,6 +93,14 @@ export type ManagePolicyState = { [ModalViewContext.ASSIGN_POLICY_VIEW]: AssignPolicyViewState; }; +const initialResourceSelector = [ + { + key: '', + operator: 'In', + values: [], + }, +]; + export const initialPolicyState: ManagePolicyState = { modalViewContext: ModalViewContext.POLICY_LIST_VIEW, [ModalViewContext.POLICY_LIST_VIEW]: { @@ -80,6 +118,13 @@ export const initialPolicyState: ManagePolicyState = { persistentVolumeClaim: { pvcSelectors: [], }, + policyRule: PolicyRule.Application, + dynamicObjects: { + objectProtectionMethod: ObjectProtectionMethod.ResourceLabelSelector, + captureInterval: '5m', + recipeInfo: undefined, + appResourceSelector: initialResourceSelector, + }, modalActionContext: undefined, message: { title: '', @@ -120,6 +165,31 @@ export type ManagePolicyStateAction = | { type: ManagePolicyStateType.RESET_ASSIGN_POLICY_STATE; context: ModalViewContext; + } + | { + type: ManagePolicyStateType.SET_POLICY_RULE; + context: ModalViewContext; + payload: PolicyRule; + } + | { + type: ManagePolicyStateType.SET_APP_RESOURCE_SELECTOR; + context: ModalViewContext; + payload: MatchExpression[]; + } + | { + type: ManagePolicyStateType.SET_RECIPE_INFO; + context: ModalViewContext; + payload: RecipeInfoType; + } + | { + type: ManagePolicyStateType.SET_CAPTURE_INTERVAL; + context: ModalViewContext; + payload: string; + } + | { + type: ManagePolicyStateType.SET_OBJECT_PROTECTION_METHOD; + context: ModalViewContext; + payload: ObjectProtectionMethod; }; export const managePolicyStateReducer = ( @@ -190,6 +260,69 @@ export const managePolicyStateReducer = ( }, }; } + case ManagePolicyStateType.SET_POLICY_RULE: { + return { + ...state, + [action.context]: { + ...state[action.context], + policyRule: action.payload, + }, + }; + } + case ManagePolicyStateType.SET_APP_RESOURCE_SELECTOR: { + return { + ...state, + [action.context]: { + ...state[action.context], + dynamicObjects: { + ...state[action.context]['dynamicObjects'], + appResourceSelector: action.payload, + }, + }, + }; + } + case ManagePolicyStateType.SET_RECIPE_INFO: { + return { + ...state, + [action.context]: { + ...state[action.context], + dynamicObjects: { + ...state[action.context]['dynamicObjects'], + recipeInfo: action.payload, + }, + }, + }; + } + case ManagePolicyStateType.SET_CAPTURE_INTERVAL: { + return { + ...state, + [action.context]: { + ...state[action.context], + dynamicObjects: { + ...state[action.context]['dynamicObjects'], + captureInterval: action.payload, + }, + }, + }; + } + case ManagePolicyStateType.SET_OBJECT_PROTECTION_METHOD: { + const objectProtectionMethod = action.payload; + return { + ...state, + [action.context]: { + ...state[action.context], + dynamicObjects: { + ...state[action.context]['dynamicObjects'], + objectProtectionMethod: action.payload, + ...(objectProtectionMethod === ObjectProtectionMethod.Recipe + ? { + appResourceSelector: initialResourceSelector, + } + : { recipeInfo: undefined }), + }, + }, + }; + } default: return state; } diff --git a/packages/mco/components/modals/app-manage-policies/utils/types.ts b/packages/mco/components/modals/app-manage-policies/utils/types.ts index b3915abf1..1b9abd27e 100644 --- a/packages/mco/components/modals/app-manage-policies/utils/types.ts +++ b/packages/mco/components/modals/app-manage-policies/utils/types.ts @@ -39,3 +39,9 @@ export type ApplicationType = K8sResourceCommon & { }; export type ApplicationInfoType = ApplicationType | {}; + +export type ViewResponse = { + processing: string; + reason: string; + result: T; +}; diff --git a/packages/mco/constants/acm.ts b/packages/mco/constants/acm.ts index 2e907ba3c..0b4164625 100644 --- a/packages/mco/constants/acm.ts +++ b/packages/mco/constants/acm.ts @@ -19,6 +19,7 @@ export const PLACEMENT_REF_LABEL = export enum APPLICATION_TYPE { APPSET = 'ApplicationSet', SUBSCRIPTION = 'Subscription', + OPENSHIFT = 'Openshift', } // Please refer to clusterclaims.go in github.com/red-hat-storage/ocs-operator before changing anything here @@ -33,3 +34,8 @@ export enum ClusterClaimTypes { export const MANAGED_CLUSTER_CONDITION_AVAILABLE = 'ManagedClusterConditionAvailable'; export const MANAGED_CLUSTER_JOINED = 'ManagedClusterJoined'; + +// Search query +export const LABEL = 'label'; +export const NAME = 'name'; +export const LABEL_SPLIT_CHAR = '; '; diff --git a/packages/mco/constants/disaster-recovery.ts b/packages/mco/constants/disaster-recovery.ts index c03249fbd..f6f5c11be 100644 --- a/packages/mco/constants/disaster-recovery.ts +++ b/packages/mco/constants/disaster-recovery.ts @@ -73,9 +73,13 @@ export enum AssignPolicySteps { Policy = 'policy', PersistentVolumeClaim = 'persistent-volume-claim', ReviewAndAssign = 'review-and-assign', + PolicyRule = 'policy-rule', + DynamicObjects = 'dynamic-objects', } export const AssignPolicyStepsNames = (t: TFunction) => ({ [AssignPolicySteps.Policy]: t('Policy'), [AssignPolicySteps.PersistentVolumeClaim]: t('PersistentVolumeClaim'), [AssignPolicySteps.ReviewAndAssign]: t('Review and assign'), + [AssignPolicySteps.PolicyRule]: t('Policy rule'), + [AssignPolicySteps.DynamicObjects]: t('Dynamic objects'), }); diff --git a/packages/mco/models/ramen.ts b/packages/mco/models/ramen.ts index 4727f4688..b710bcd75 100644 --- a/packages/mco/models/ramen.ts +++ b/packages/mco/models/ramen.ts @@ -47,3 +47,15 @@ export const DRVolumeReplicationGroup: K8sModel = { kind: 'VolumeReplicationGroup', crd: true, }; + +export const DRRecipeModel: K8sModel = { + label: 'Recipe', + labelPlural: 'Recipes', + apiVersion: 'v1alpha1', + apiGroup: 'ramendr.openshift.io', + plural: 'recipes', + abbr: 'RECIPE', + namespaced: true, + kind: 'Recipe', + crd: true, +}; diff --git a/packages/mco/types/ramen.ts b/packages/mco/types/ramen.ts index 161ab312f..669c85333 100644 --- a/packages/mco/types/ramen.ts +++ b/packages/mco/types/ramen.ts @@ -49,6 +49,13 @@ export type DRPlacementControlKind = K8sResourceCommon & { matchLabels: MatchLabels; }; action?: DRActionType; + kubeObjectProtection?: { + captureInterval?: string; + recipeRef?: { + name?: string; + namespace?: string; + }; + }; }; status?: { conditions?: K8sResourceCondition[]; diff --git a/packages/mco/utils/disaster-recovery.tsx b/packages/mco/utils/disaster-recovery.tsx index 75aa95cf9..898a4518d 100644 --- a/packages/mco/utils/disaster-recovery.tsx +++ b/packages/mco/utils/disaster-recovery.tsx @@ -32,6 +32,8 @@ import { DRPC_STATUS, THRESHOLD, DRActionType, + APPLICATION_TYPE, + LABEL_SPLIT_CHAR, } from '../constants'; import { DRPC_NAMESPACE_ANNOTATION, @@ -65,6 +67,7 @@ import { ACMPlacementKind, MirrorPeerKind, ArgoApplicationSetKind, + SearchResult, } from '../types'; export type PlacementMap = { @@ -525,6 +528,7 @@ export const isDRPolicyValidated = (drPolicy: DRPolicyKind) => ); export const getDRPCKindObj = ( + appType: APPLICATION_TYPE, plsName: string, plsNamespace: string, plsKind: string, @@ -532,12 +536,15 @@ export const getDRPCKindObj = ( drPolicyName: string, drClusterNames: string[], decisionClusters: string[], - pvcSelectors: string[] + pvcSelectors: string[], + captureInterval?: string, + recipeName?: string, + recipeNamespace?: string ): DRPlacementControlKind => ({ apiVersion: getAPIVersionForModel(DRPlacementControlModel), kind: DRPlacementControlModel.kind, metadata: { - name: `${plsName}-drpc`, + name: `${plsName}-drpc`.substr(0, 63), namespace: plsNamespace, }, spec: { @@ -556,6 +563,17 @@ export const getDRPCKindObj = ( pvcSelector: { matchLabels: objectify(pvcSelectors), }, + ...(appType === APPLICATION_TYPE.OPENSHIFT + ? { + kubeObjectProtection: { + captureInterval, + recipeRef: { + name: recipeName, + namespace: recipeNamespace, + }, + }, + } + : {}), }, }); @@ -639,3 +657,16 @@ export const findDeploymentClusters = ( export const getDRPolicyStatus = (isValidated, t) => isValidated ? t('Validated') : t('Not Validated'); + +export const getValueFromSearchResult = ( + searchResult: SearchResult, + searchKey: string, + splitChar: string = LABEL_SPLIT_CHAR +): string[] => { + const labels = + searchResult?.data.searchResult?.[0]?.items.reduce( + (acc, item) => [...acc, ...(item[searchKey]?.split(splitChar) || [])], + [] + ) || []; + return Array.from(new Set(labels)); +}; diff --git a/packages/shared/src/label-expression-selector/labelExpressionSelector.scss b/packages/shared/src/label-expression-selector/labelExpressionSelector.scss index f055efb85..d5b8058eb 100644 --- a/packages/shared/src/label-expression-selector/labelExpressionSelector.scss +++ b/packages/shared/src/label-expression-selector/labelExpressionSelector.scss @@ -5,6 +5,11 @@ font-size: var(--pf-global--FontSize--sm) !important; } } + &__expandBody--padding-top { + .pf-c-form__field-group-body { + padding-top: var(--pf-global--spacer--sm) !important; + } + } &__button--margin-top { margin-top: var(--pf-global--spacer--md); } diff --git a/packages/shared/src/label-expression-selector/labelExpressionSelector.tsx b/packages/shared/src/label-expression-selector/labelExpressionSelector.tsx index 52ce488d1..7c9b864e0 100644 --- a/packages/shared/src/label-expression-selector/labelExpressionSelector.tsx +++ b/packages/shared/src/label-expression-selector/labelExpressionSelector.tsx @@ -271,6 +271,7 @@ const ArrayInput: React.FC = ({
= // Values has to be mapped with key/label. // Maintaining the key & values props are in object type to scale. +type OptionProps = { + text: string; +}; + export type OptionType = { [key in string]: { - key: { - text: string; - }; - values: { - text: string; - }[]; + key: OptionProps; + values: OptionProps[]; }; }; diff --git a/packages/shared/src/radio-selection/radioSelection.scss b/packages/shared/src/radio-selection/radioSelection.scss index e7a50b6ab..7baccebf5 100644 --- a/packages/shared/src/radio-selection/radioSelection.scss +++ b/packages/shared/src/radio-selection/radioSelection.scss @@ -1,6 +1,6 @@ .odf-radio-selection{ - &__padding-left { - padding-left: 1rem; + &__padding { + padding: 0rem 0rem 1rem 1rem; } &__margin-top { margin-top: var(--pf-global--spacer--lg); diff --git a/packages/shared/src/radio-selection/radioSelection.tsx b/packages/shared/src/radio-selection/radioSelection.tsx index 6bede76e9..fbbc655ac 100644 --- a/packages/shared/src/radio-selection/radioSelection.tsx +++ b/packages/shared/src/radio-selection/radioSelection.tsx @@ -13,10 +13,11 @@ import '../style.scss'; import './radioSelection.scss'; export const RadioSelection: React.FC = (props) => { - const { title, description, alertProps, selected, radioProps } = props; + const { title, description, alertProps, selected, radioProps, className } = + props; return ( - <> +
{!!alertProps && ( = (props) => { helperText={description} isHelperTextBeforeField > -
+
{radioProps.map((radioProp, index) => ( = (props) => {
- +
); }; @@ -75,4 +76,5 @@ export type RadioSelectionProps = { selected: string; radioProps: RadioType[]; alertProps?: AlertType; + className?: string; }; diff --git a/yarn.lock b/yarn.lock index 190a089d8..9e910e388 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3373,10 +3373,10 @@ character-reference-invalid@^1.0.0: resolved "https://registry.yarnpkg.com/character-reference-invalid/-/character-reference-invalid-1.1.4.tgz#083329cda0eae272ab3dbbf37e9a382c13af1560" integrity sha512-mKKUkUbhPpQlCOfIuZkvSEgktjPFIsZKRRbC6KWVEMvlzblj3i3asQv5ODsrwt0N3pHAEvjP8KTQPHkp0+6jOg== -charenc@0.0.2: +charenc@0.0.2, "charenc@>= 0.0.1": version "0.0.2" resolved "https://registry.yarnpkg.com/charenc/-/charenc-0.0.2.tgz#c0a1d2f3a7092e03774bfa83f14c0fc5790a8667" - integrity sha1-wKHS86cJLgN3S/qD8UwPxXkKhmc= + integrity sha512-yrLQ/yVUFXkzg7EDQsPieE/53+0RlaWTs+wBrvW36cyilJ2SaDWfl4Yj7MtLTXleV9uEKefbAGUPv2/iWSooRA== check-more-types@^2.24.0: version "2.24.0" @@ -3890,10 +3890,10 @@ cross-spawn@^7.0.0, cross-spawn@^7.0.2, cross-spawn@^7.0.3: shebang-command "^2.0.0" which "^2.0.1" -crypt@0.0.2: +crypt@0.0.2, "crypt@>= 0.0.1": version "0.0.2" resolved "https://registry.yarnpkg.com/crypt/-/crypt-0.0.2.tgz#88d7ff7ec0dfb86f713dc87bbb42d044d3e6c41b" - integrity sha1-iNf/fsDfuG9xPch7u0LQRNPmxBs= + integrity sha512-mCxBlsHFYh9C+HVpiEacem8FEBnMXgU9gy4zmNC+SXAZNB/1idgp/aulFJ4FgCi7GPEVbfyng092GqL2k2rmow== css-color-names@0.0.4: version "0.0.4" @@ -10518,6 +10518,14 @@ setprototypeof@1.2.0: resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.2.0.tgz#66c9a24a73f9fc28cbe66b09fed3d33dcaf1b424" integrity sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw== +sha1@1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/sha1/-/sha1-1.1.1.tgz#addaa7a93168f393f19eb2b15091618e2700f848" + integrity sha512-dZBS6OrMjtgVkopB1Gmo4RQCDKiZsqcpAQpkV/aaj+FCrCg8r4I4qMkDPQjBgLIxlmu9k4nUbWq6ohXahOneYA== + dependencies: + charenc ">= 0.0.1" + crypt ">= 0.0.1" + shallow-clone@^3.0.0: version "3.0.1" resolved "https://registry.yarnpkg.com/shallow-clone/-/shallow-clone-3.0.1.tgz#8f2981ad92531f55035b01fb230769a40e02efa3"