diff --git a/locales/en/plugin__odf-console.json b/locales/en/plugin__odf-console.json index 53a614f02..9a3b06d44 100644 --- a/locales/en/plugin__odf-console.json +++ b/locales/en/plugin__odf-console.json @@ -98,6 +98,10 @@ "Resource label": "Resource label", "Secure namespaces as per Recipe definition.": "Secure namespaces as per Recipe definition.", "Recipe": "Recipe", + "Recipe parameters": "Recipe parameters", + "Recipe parameters are a set of named inputs that substitute the placeholders in a recipe.": "Recipe parameters are a set of named inputs that substitute the placeholders in a recipe.", + "Add parameter": "Add parameter", + "Value (optional)": "Value (optional)", "Recipe list": "Recipe list", "Only recipes of the selected namespaces will appear in the list.": "Only recipes of the selected namespaces will appear in the list.", "Select a recipe": "Select a recipe", @@ -147,6 +151,7 @@ "Type:": "Type:", "Recipe name:": "Recipe name:", "Recipe namespace:": "Recipe namespace:", + "Recipe Parameters:": "Recipe Parameters:", "Label expressions:": "Label expressions:", "PVC label selectors:": "PVC label selectors:", "Replication": "Replication", @@ -1304,7 +1309,6 @@ "Use different criteria for tagging your bucket.": "Use different criteria for tagging your bucket.", "No tags are attached to this bucket.": "No tags are attached to this bucket.", "Add tag": "Add tag", - "Value (optional)": "Value (optional)", "Enter a valid rule name": "Enter a valid rule name", "A rule name is required.": "A rule name is required.", "A rule with this name already exists. Type a different name.": "A rule with this name already exists. Type a different name.", diff --git a/packages/mco/components/discovered-application-wizard/utils/k8s-utils.ts b/packages/mco/components/discovered-application-wizard/utils/k8s-utils.ts index 828ed19f5..51e95c75f 100644 --- a/packages/mco/components/discovered-application-wizard/utils/k8s-utils.ts +++ b/packages/mco/components/discovered-application-wizard/utils/k8s-utils.ts @@ -22,6 +22,28 @@ import { ProtectionMethodType, } from './reducer'; +export const convertToRecipeParameters = ( + params: Record +): Record => { + return Object.fromEntries( + Object.entries(params || {}).map(([key, value]) => [ + key, + value + .split(',') + .map((v) => v.trim()) + .filter(Boolean), + ]) + ); +}; + +export const formatRecipeParametersForDisplay = ( + params: Record +): string => { + return Object.entries(params || {}) + .map(([key, values]) => `${key}: ${values.join(', ')}`) + .join('; '); +}; + export const getDRPCKindObj = (props: { name: string; preferredCluster: string; @@ -31,10 +53,10 @@ export const getDRPCKindObj = (props: { k8sResourceReplicationInterval: string; recipeName?: string; recipeNamespace?: string; + recipeParameters?: Record; k8sResourceLabelExpressions?: MatchExpression[]; pvcLabelExpressions?: MatchExpression[]; placementName: string; - recipeParameters?: Record; labels?: ObjectMetadata['labels']; }): DRPlacementControlKind => ({ apiVersion: getAPIVersionForModel(DRPlacementControlModel), @@ -106,7 +128,7 @@ export const createPromise = ( const { namespace, configuration, replication } = state; const { clusterName, namespaces, name } = namespace; const { protectionMethod, recipe, resourceLabels } = configuration; - const { recipeName, recipeNamespace } = recipe; + const { recipeName, recipeNamespace, recipeParameters } = recipe; const { k8sResourceLabelExpressions, pvcLabelExpressions } = resourceLabels; const { drPolicy, k8sResourceReplicationInterval } = replication; const namespaceList = namespaces.map(getName); @@ -132,6 +154,7 @@ export const createPromise = ( protectionMethod, recipeName, recipeNamespace, + recipeParameters: convertToRecipeParameters(recipeParameters), k8sResourceLabelExpressions, pvcLabelExpressions, drPolicyName: getName(drPolicy), diff --git a/packages/mco/components/discovered-application-wizard/utils/reducer.ts b/packages/mco/components/discovered-application-wizard/utils/reducer.ts index 3c8f7d7ad..12d3b4870 100644 --- a/packages/mco/components/discovered-application-wizard/utils/reducer.ts +++ b/packages/mco/components/discovered-application-wizard/utils/reducer.ts @@ -16,6 +16,7 @@ export enum EnrollDiscoveredApplicationStateType { SET_NAMESPACES = 'NAMESPACE/SET_NAMESPACES', SET_PROTECTION_METHOD = 'CONFIGURATION/SET_PROTECTION_METHOD', SET_RECIPE_NAME_NAMESPACE = 'CONFIGURATION/RECIPE/SET_RECIPE_NAME_NAMESPACE', + SET_RECIPE_PARAMETERS = 'SET_RECIPE_PARAMETERS', SET_K8S_RESOURCE_LABEL_EXPRESSIONS = 'CONFIGURATION/RESOURCE_LABEL/SET_K8S_RESOURCE_LABEL_EXPRESSIONS', SET_PVC_LABEL_EXPRESSIONS = 'CONFIGURATION/RESOURCE_LABEL/SET_PVC_LABEL_EXPRESSIONS', SET_POLICY = 'REPLICATION/SET_POLICY', @@ -40,6 +41,7 @@ export type EnrollDiscoveredApplicationState = { recipeName: string; // recipe CR namespace recipeNamespace: string; + recipeParameters: Record; }; resourceLabels: { k8sResourceLabelExpressions: MatchExpression[]; @@ -70,6 +72,7 @@ export const initialState: EnrollDiscoveredApplicationState = { recipe: { recipeName: '', recipeNamespace: '', + recipeParameters: {}, }, resourceLabels: { k8sResourceLabelExpressions: [], @@ -112,6 +115,10 @@ export type EnrollDiscoveredApplicationAction = type: EnrollDiscoveredApplicationStateType.SET_POLICY; payload: DRPolicyKind; } + | { + type: EnrollDiscoveredApplicationStateType.SET_RECIPE_PARAMETERS; + payload: Record; + } | { type: EnrollDiscoveredApplicationStateType.SET_K8S_RESOURCE_REPLICATION_INTERVAL; payload: string; @@ -159,6 +166,18 @@ export const reducer: EnrollReducer = (state, action) => { }, }; } + case EnrollDiscoveredApplicationStateType.SET_RECIPE_PARAMETERS: { + return { + ...state, + configuration: { + ...state.configuration, + recipe: { + ...state.configuration.recipe, + recipeParameters: action.payload, + }, + }, + }; + } case EnrollDiscoveredApplicationStateType.SET_RECIPE_NAME_NAMESPACE: { const [recipeName, recipeNamespace] = action.payload.split( NAME_NAMESPACE_SPLIT_CHAR @@ -168,6 +187,7 @@ export const reducer: EnrollReducer = (state, action) => { configuration: { ...state.configuration, recipe: { + ...state.configuration.recipe, recipeName, recipeNamespace, }, diff --git a/packages/mco/components/discovered-application-wizard/wizard-steps/configuration-step/recipe-parameter-input.tsx b/packages/mco/components/discovered-application-wizard/wizard-steps/configuration-step/recipe-parameter-input.tsx new file mode 100644 index 000000000..b20c00e6a --- /dev/null +++ b/packages/mco/components/discovered-application-wizard/wizard-steps/configuration-step/recipe-parameter-input.tsx @@ -0,0 +1,59 @@ +import * as React from 'react'; +import { FieldLevelHelp, useCustomTranslation } from '@odf/shared'; +import { LazyNameValueEditor } from '@odf/shared/utils/NameValueEditor'; +import { FormGroup } from '@patternfly/react-core'; +import { + EnrollDiscoveredApplicationAction, + EnrollDiscoveredApplicationStateType, +} from '../../utils/reducer'; + +export const RecipeParameterInput: React.FC = ({ + dispatch, +}) => { + const { t } = useCustomTranslation(); + const [recipeParams, setRecipeParams] = React.useState<[string, string][]>([ + ['', ''], + ]); + + const updateRecipeParams = ({ + nameValuePairs, + }: { + nameValuePairs: [string, string][]; + }) => { + setRecipeParams(nameValuePairs); + const validPairs = nameValuePairs.filter(([key]) => key.trim()); + dispatch({ + type: EnrollDiscoveredApplicationStateType.SET_RECIPE_PARAMETERS, + payload: Object.fromEntries(validPairs.map(([k, v]) => [k.trim(), v])), + }); + }; + + return ( + + {t( + 'Recipe parameters are a set of named inputs that substitute the placeholders in a recipe.' + )} + + } + data-test="recipe-parameters-form-group" + > + + + ); +}; + +type RecipeParameterInputProps = { + dispatch: React.Dispatch; +}; diff --git a/packages/mco/components/discovered-application-wizard/wizard-steps/configuration-step/recipe-selection.tsx b/packages/mco/components/discovered-application-wizard/wizard-steps/configuration-step/recipe-selection.tsx index 1959bf74a..8c51cccfc 100644 --- a/packages/mco/components/discovered-application-wizard/wizard-steps/configuration-step/recipe-selection.tsx +++ b/packages/mco/components/discovered-application-wizard/wizard-steps/configuration-step/recipe-selection.tsx @@ -21,6 +21,7 @@ import { EnrollDiscoveredApplicationStateType, } from '../../utils/reducer'; import './configuration-step.scss'; +import { RecipeParameterInput } from './recipe-parameter-input'; const getRecipeOptions = ( searchResultItem: SearchResultItemType[] @@ -117,6 +118,8 @@ export const RecipeSelection: React.FC = ({ ) : ( )} + + {recipeNameNamespace && } ); }; diff --git a/packages/mco/components/discovered-application-wizard/wizard-steps/review-step/review-step.tsx b/packages/mco/components/discovered-application-wizard/wizard-steps/review-step/review-step.tsx index ea65d2ca2..95d26fba0 100644 --- a/packages/mco/components/discovered-application-wizard/wizard-steps/review-step/review-step.tsx +++ b/packages/mco/components/discovered-application-wizard/wizard-steps/review-step/review-step.tsx @@ -12,6 +12,11 @@ import { } from '@odf/shared/review-and-create-step'; import { getName } from '@odf/shared/selectors'; import { useCustomTranslation } from '@odf/shared/useCustomTranslationHook'; +import * as _ from 'lodash-es'; +import { + formatRecipeParametersForDisplay, + convertToRecipeParameters, +} from '../../utils/k8s-utils'; import { EnrollDiscoveredApplicationState, ProtectionMethodType, @@ -23,7 +28,7 @@ export const Review: React.FC = ({ state }) => { const { namespace, configuration, replication } = state; const { clusterName, namespaces, name } = namespace; const { protectionMethod, recipe, resourceLabels } = configuration; - const { recipeName, recipeNamespace } = recipe; + const { recipeName, recipeNamespace, recipeParameters } = recipe; const { k8sResourceLabelExpressions, pvcLabelExpressions } = resourceLabels; const { drPolicy, k8sResourceReplicationInterval } = replication; @@ -37,6 +42,9 @@ export const Review: React.FC = ({ state }) => { drPolicy.spec.schedulingInterval === '0m' ? REPLICATION_DISPLAY_TEXT(t).sync : REPLICATION_DISPLAY_TEXT(t).async; + const displayRecipeParameters = formatRecipeParametersForDisplay( + convertToRecipeParameters(recipeParameters) + ); const [unitVal, interval] = parseSyncInterval(k8sResourceReplicationInterval); return ( @@ -62,6 +70,11 @@ export const Review: React.FC = ({ state }) => { {recipeNamespace} + {!_.isEmpty(recipeParameters) && ( + + {displayRecipeParameters} + + )} )} {protectionMethod === ProtectionMethodType.RESOURCE_LABEL && (