diff --git a/locales/en/plugin__odf-console.json b/locales/en/plugin__odf-console.json index 6ffb51beb..260f95d30 100644 --- a/locales/en/plugin__odf-console.json +++ b/locales/en/plugin__odf-console.json @@ -204,16 +204,14 @@ "Secure your application by assigning a policy from the available policy templates.": "Secure your application by assigning a policy from the available policy templates.", "Manage data policy": "Manage data policy", "Assign a policy to protect the application and to ensure a quick recovery.": "Assign a policy to protect the application and to ensure a quick recovery.", - "Policy": "Policy", - "PersistentVolumeClaim": "PersistentVolumeClaim", - "Review and assign": "Review and assign", - "Assign": "Assign", "New policy assigned to application.": "New policy assigned to application.", "Unable to assign policy to application.": "Unable to assign policy to application.", - "Next": "Next", - "Back": "Back", "Assign policy nav": "Assign policy nav", "Assign policy content": "Assign policy content", + "Cannnot proceed because one or more mandatory fields have been left empty. Fill in the required information before proceeding": "Cannnot proceed because one or more mandatory fields have been left empty. Fill in the required information before proceeding", + "Assign": "Assign", + "Next": "Next", + "Back": "Back", "Manage list view alert": "Manage list view alert", "Confirm unassign": "Confirm unassign", "All placements": "All placements", @@ -233,12 +231,12 @@ "No activity": "No activity", "No assigned data policy found": "No assigned data policy found", "Delete": "Delete", + "Required": "Required", "Select a placement": "Select a placement", "{{count}} selected_one": "{{count}} selected", "{{count}} selected_other": "{{count}} selected", "Select labels": "Select labels", "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.", - "If no label is provided, all PVCs will be protected. Define your preferences to protect specific resources.": "If no label is provided, all PVCs will be protected. Define your preferences to protect specific resources.", "Application resource": "Application resource", "Add application resource": "Add application resource", "Select a policy": "Select a policy", @@ -288,6 +286,9 @@ "minutes": "minutes", "hours": "hours", "days": "days", + "Policy": "Policy", + "PersistentVolumeClaim": "PersistentVolumeClaim", + "Review and assign": "Review and assign", "{{async}}, interval: {{interval}}": "{{async}}, interval: {{interval}}", "In use: {{targetClusters}}": "In use: {{targetClusters}}", "Used: {{targetClusters}}": "Used: {{targetClusters}}", @@ -1155,6 +1156,5 @@ "Cannot change resource name (original: \"{{name}}\", updated: \"{{newName}}\").": "Cannot change resource name (original: \"{{name}}\", updated: \"{{newName}}\").", "Cannot change resource namespace (original: \"{{namespace}}\", updated: \"{{newNamespace}}\").": "Cannot change resource namespace (original: \"{{namespace}}\", updated: \"{{newNamespace}}\").", "Cannot change resource kind (original: \"{{original}}\", updated: \"{{updated}}\").": "Cannot change resource kind (original: \"{{original}}\", updated: \"{{updated}}\").", - "Cannot change API group (original: \"{{apiGroup}}\", updated: \"{{newAPIGroup}}\").": "Cannot change API group (original: \"{{apiGroup}}\", updated: \"{{newAPIGroup}}\").", - "Required": "Required" + "Cannot change API group (original: \"{{apiGroup}}\", updated: \"{{newAPIGroup}}\").": "Cannot change API group (original: \"{{apiGroup}}\", updated: \"{{newAPIGroup}}\")." } 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 93b022fa5..811f74812 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 @@ -1,11 +1,12 @@ import * as React from 'react'; +import { AssignPolicySteps, AssignPolicyStepsNames } from '@odf/mco/constants'; import { createRefFromK8Resource } from '@odf/mco/utils'; import { ModalBody } from '@odf/shared/modals/Modal'; -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 { PolicyConfigViewer } from './helper/policy-config-viewer'; import { PVCDetailsWizardContent } from './helper/pvc-details-wizard-content'; import { SelectPolicyWizardContent } from './helper/select-policy-wizard-content'; @@ -32,7 +33,7 @@ export const createSteps = ( matchingPolicies: DRPolicyType[], state: AssignPolicyViewState, stepIdReached: number, - isAssignDisabled: boolean, + isValidationEnabled: boolean, t: TFunction, setPolicy: (policy?: DataPolicyType) => void, setDRPlacementControls: ( @@ -41,40 +42,36 @@ export const createSteps = ( ): WizardStep[] => [ { id: 1, - name: t('Policy'), + name: AssignPolicyStepsNames(t)[AssignPolicySteps.Policy], component: ( ), - enableNext: !!getName(state.policy), }, { id: 2, - name: t('PersistentVolumeClaim'), + name: AssignPolicyStepsNames(t)[AssignPolicySteps.PersistentVolumeClaim], component: ( ), canJumpTo: stepIdReached >= 2, - enableNext: !!state.policy?.placementControlInfo?.length, }, { id: 3, - name: t('Review and assign'), + name: AssignPolicyStepsNames(t)[AssignPolicySteps.ReviewAndAssign], component: , - nextButtonText: t('Assign'), canJumpTo: stepIdReached >= 3, - enableNext: !isAssignDisabled, - hideBackButton: isAssignDisabled, - hideCancelButton: isAssignDisabled, }, ]; @@ -89,16 +86,7 @@ export const AssignPolicyView: React.FC = ({ }) => { const { t } = useCustomTranslation(); const [stepIdReached, setStepIdReached] = React.useState(1); - const [isAssignDisabled, setAssignDisabled] = React.useState(false); - - const onNext = ({ id }: WizardStep) => { - if (id) { - if (typeof id === 'string') { - id = parseInt(id, 10); - } - setStepIdReached(stepIdReached < id ? id : stepIdReached); - } - }; + const [isValidationEnabled, setEnableValidation] = React.useState(false); const setPolicy = (policy: DataPolicyType = null) => dispatch({ @@ -116,8 +104,7 @@ export const AssignPolicyView: React.FC = ({ payload: drPlacementControls, }); - const assignPolicy = () => { - setAssignDisabled(true); + const onSubmit = () => { const updateContext = ( title: string, description: string, @@ -160,32 +147,39 @@ export const AssignPolicyView: React.FC = ({ }); }; + const onClose = () => { + setModalContext(ModalViewContext.POLICY_LIST_VIEW); + // reset policy info + setPolicy(); + }; + return ( { - setModalContext(ModalViewContext.POLICY_LIST_VIEW); - // reset policy info - setPolicy(); - }} steps={createSteps( applicaitonInfo.workloadNamespace, applicaitonInfo.placements, matchingPolicies, state, stepIdReached, - isAssignDisabled, + isValidationEnabled, t, setPolicy, setDRPlacementControls )} - onNext={onNext} + footer={ + + } height={450} /> diff --git a/packages/mco/components/modals/app-manage-policies/helper/assign-policy-view-footer.tsx b/packages/mco/components/modals/app-manage-policies/helper/assign-policy-view-footer.tsx new file mode 100644 index 000000000..73096326e --- /dev/null +++ b/packages/mco/components/modals/app-manage-policies/helper/assign-policy-view-footer.tsx @@ -0,0 +1,138 @@ +import * as React from 'react'; +import { AssignPolicySteps, AssignPolicyStepsNames } from '@odf/mco/constants'; +import { getName } from '@odf/shared/selectors'; +import { useCustomTranslation } from '@odf/shared/useCustomTranslationHook'; +import { TFunction } from 'i18next'; +import { + Button, + WizardContextType, + WizardContext, + WizardFooter, + Alert, + AlertVariant, +} from '@patternfly/react-core'; +import { DRPolicyType, DataPolicyType } from '../utils/types'; +import '../../../../style.scss'; +import '../style.scss'; + +const isPVCSelectorFound = (dataPolicy: DRPolicyType) => + !!dataPolicy?.placementControlInfo?.length && + !!dataPolicy?.placementControlInfo?.every( + (drpc) => !!drpc?.pvcSelector?.length + ); + +const isDRPolicySelected = (dataPolicy: DRPolicyType) => !!getName(dataPolicy); + +const canJumpToNextStep = ( + stepName: string, + dataPolicy: DataPolicyType, + t: TFunction +) => { + switch (stepName) { + case AssignPolicyStepsNames(t)[AssignPolicySteps.Policy]: + return isDRPolicySelected(dataPolicy); + case AssignPolicyStepsNames(t)[AssignPolicySteps.PersistentVolumeClaim]: + return isPVCSelectorFound(dataPolicy); + default: + return false; + } +}; + +export const AssignPolicyViewFooter: React.FC = ({ + dataPolicy, + stepIdReached, + isValidationEnabled, + setStepIdReached, + onSubmit, + onCancel, + setEnableValidation, +}) => { + const { t } = useCustomTranslation(); + const [requestInProgress, setRequestInProgress] = React.useState(false); + const { activeStep, onNext, onBack } = + React.useContext(WizardContext); + + const stepId = activeStep.id as number; + const stepName = activeStep.name as string; + + const canJumpToNext = canJumpToNextStep(stepName, dataPolicy, t); + const validationError = isValidationEnabled && !canJumpToNext; + + const moveToNextStep = () => { + setEnableValidation(true); + if (canJumpToNext) { + setStepIdReached(stepIdReached <= stepId ? stepId + 1 : stepIdReached); + onNext(); + setEnableValidation(false); + } + }; + + const handleNext = async () => { + switch (stepName) { + case AssignPolicyStepsNames(t)[AssignPolicySteps.ReviewAndAssign]: + setRequestInProgress(true); + await onSubmit(); + setRequestInProgress(false); + break; + default: + moveToNextStep(); + } + }; + + return ( + <> + {validationError && ( + + )} + + + {/* Disabling the back button for the first step (Policy) in wizard */} + + + + + ); +}; + +type AssignPolicyViewFooterProps = { + dataPolicy: DataPolicyType; + stepIdReached: number; + isValidationEnabled: boolean; + setStepIdReached: React.Dispatch>; + onSubmit: () => void; + onCancel: () => void; + setEnableValidation: 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 41e510c8c..e81142345 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 @@ -14,8 +14,6 @@ import { import { ObjectReference } from '@openshift-console/dynamic-plugin-sdk'; import * as _ from 'lodash-es'; import { - Alert, - AlertVariant, Button, Form, FormGroup, @@ -124,6 +122,7 @@ const PairElement: React.FC = ({ placementNames, labels, tags, + isValidationEnabled, createPlacementControlInfo, updatePlacementControlInfo, }: extraProps = extraProps; @@ -162,8 +161,16 @@ const PairElement: React.FC = ({ }, [index, onRemoveProp, createPlacementControlInfo]); return ( -
-
+
+ = ({ )} placeholderText={t('Select a placement')} onChange={onChangePlacement} + required + validated={ + isValidationEnabled && !selectedPlacement ? 'error' : 'default' + } /> -
-
+ + = ({ : t('Select labels') } variant={SelectVariant.checkbox} + required + validated={ + isValidationEnabled && !selectedLabels?.length ? 'error' : 'default' + } /> -
-
+ + -
+
); }; @@ -216,6 +239,7 @@ export const PVCDetailsWizardContent: React.FC = unProtectedPlacements, policyRef, workloadNamespace, + isValidationEnabled, setDRPlacementControls, }) => { const { t } = useCustomTranslation(); @@ -295,34 +319,24 @@ export const PVCDetailsWizardContent: React.FC = )} - - - - - setTags(nameValuePairs)} - PairElementComponent={PairElement} - nameString={t('Application resource')} - valueString={t('PVC label selector')} - addString={t('Add application resource')} - isAddDisabled={tags.length !== selectedPlacementControls.length} - extraProps={{ - placementNames, - labels, - tags, - createPlacementControlInfo, - updatePlacementControlInfo, - }} - /> - + setTags(nameValuePairs)} + PairElementComponent={PairElement} + nameString={t('Application resource')} + valueString={t('PVC label selector')} + addString={t('Add application resource')} + isAddDisabled={tags.length !== selectedPlacementControls.length} + extraProps={{ + placementNames, + labels, + tags, + isValidationEnabled, + createPlacementControlInfo, + updatePlacementControlInfo, + }} + className="co-required mco-manage-policies__nameValue--weight" + /> ); }; @@ -333,6 +347,7 @@ type extraProps = { placementNames: string[]; labels: string[]; tags: TagsType; + isValidationEnabled: boolean; createPlacementControlInfo: (placementName: string, index: number) => void; updatePlacementControlInfo: (labels: string[], index: number) => void; }; @@ -342,6 +357,7 @@ type PVCDetailsWizardContentProps = { unProtectedPlacements: PlacementType[]; policyRef: ObjectReference; workloadNamespace: string; + isValidationEnabled: boolean; setDRPlacementControls: ( drPlacementControls: DRPlacementControlType[] ) => void; diff --git a/packages/mco/components/modals/app-manage-policies/helper/select-policy-wizard-content.tsx b/packages/mco/components/modals/app-manage-policies/helper/select-policy-wizard-content.tsx index 16d3f7268..7de06248b 100644 --- a/packages/mco/components/modals/app-manage-policies/helper/select-policy-wizard-content.tsx +++ b/packages/mco/components/modals/app-manage-policies/helper/select-policy-wizard-content.tsx @@ -14,23 +14,27 @@ const findPolicy = (name: string, dataPolicies: DRPolicyType[]) => dataPolicies.find((policy) => getName(policy) === name); export const SelectPolicyWizardContent: React.FC = - ({ policy, matchingPolicies, setPolicy }) => { + ({ policy, matchingPolicies, isValidationEnabled, setPolicy }) => { const { t } = useCustomTranslation(); + const name = getName(policy); return (
{ - if (getName(policy) !== value) { + if (name !== value) { setPolicy(findPolicy(value, matchingPolicies)); } }} @@ -43,5 +47,6 @@ export const SelectPolicyWizardContent: React.FC type SelectPolicyWizardContentProps = { policy: DataPolicyType; matchingPolicies: DRPolicyType[]; + isValidationEnabled: boolean; setPolicy: (policy: DataPolicyType) => void; }; diff --git a/packages/mco/components/modals/app-manage-policies/style.scss b/packages/mco/components/modals/app-manage-policies/style.scss index b1ca634c5..9d48501fd 100644 --- a/packages/mco/components/modals/app-manage-policies/style.scss +++ b/packages/mco/components/modals/app-manage-policies/style.scss @@ -17,10 +17,13 @@ &__form--width { width: 50%; } - &__alert--margin-bottom { - margin-bottom: var(--pf-global--spacer--xs) !important; - } &__emptyState---margin-bottom { margin-bottom: var(--pf-global--spacer--lg); } + &__nameValue--weight { + font-weight: var(--pf-global--FontWeight--bold) + } + &__alert--margin-left { + margin-left: var(--pf-global--spacer--lg); + } } diff --git a/packages/mco/constants/disaster-recovery.ts b/packages/mco/constants/disaster-recovery.ts index cfbca1c84..c03249fbd 100644 --- a/packages/mco/constants/disaster-recovery.ts +++ b/packages/mco/constants/disaster-recovery.ts @@ -67,3 +67,15 @@ export const SYNC_SCHEDULE_DISPLAY_TEXT = ( [TIME_UNITS.Hours]: t('hours'), [TIME_UNITS.Days]: t('days'), }); + +// Asisgn policy wizard steps +export enum AssignPolicySteps { + Policy = 'policy', + PersistentVolumeClaim = 'persistent-volume-claim', + ReviewAndAssign = 'review-and-assign', +} +export const AssignPolicyStepsNames = (t: TFunction) => ({ + [AssignPolicySteps.Policy]: t('Policy'), + [AssignPolicySteps.PersistentVolumeClaim]: t('PersistentVolumeClaim'), + [AssignPolicySteps.ReviewAndAssign]: t('Review and assign'), +}); diff --git a/packages/shared/src/dropdown/singleselectdropdown.tsx b/packages/shared/src/dropdown/singleselectdropdown.tsx index b22444c2d..99ae8dcdc 100644 --- a/packages/shared/src/dropdown/singleselectdropdown.tsx +++ b/packages/shared/src/dropdown/singleselectdropdown.tsx @@ -63,4 +63,6 @@ export type SingleSelectDropdownProps = { onFilter?: SelectProps['onFilter']; hasInlineFilter?: SelectProps['hasInlineFilter']; isDisabled?: boolean; + validated?: 'success' | 'warning' | 'error' | 'default'; + required?: boolean; }; diff --git a/packages/shared/src/utils/NameValueEditor.tsx b/packages/shared/src/utils/NameValueEditor.tsx index e0c6e656d..b19f578f8 100644 --- a/packages/shared/src/utils/NameValueEditor.tsx +++ b/packages/shared/src/utils/NameValueEditor.tsx @@ -52,6 +52,7 @@ type NameValueEditorProps = { onLastItemRemoved: () => void; extraProps?: any; isAddDisabled?: boolean; + className?: string; }; export const enum NameValueEditorPair { @@ -208,6 +209,7 @@ export const NameValueEditor: React.FC = valueString, extraProps, isAddDisabled, + className, PairElementComponent = PairElement, }) => { const { t } = useCustomTranslation(); @@ -307,8 +309,12 @@ export const NameValueEditor: React.FC = {!readOnly && allowSorting && (
)} -
{nameStringUpdated}
-
{valueStringUpdated}
+
+ {nameStringUpdated} +
+
+ {valueStringUpdated} +
{pairElems}