From b9c3b3323c8334474a3560cd4f154138be8cb052 Mon Sep 17 00:00:00 2001 From: Gowtham Shanmugasundaram Date: Mon, 19 Jun 2023 21:33:43 +0530 Subject: [PATCH] Unassign data policy for application Signed-off-by: Gowtham Shanmugasundaram --- locales/en/plugin__odf-console.json | 20 +- .../app-manage-policies-modal.tsx | 92 +++++-- .../app-manage-policies/helper/messages.tsx | 23 +- .../helper/policy-config-viewer.tsx | 238 ++++++++++++++++++ .../helper/policy-list-view-table.tsx | 27 +- .../parsers/application-set-parser.spec.tsx | 101 +++++++- .../parsers/application-set-parser.tsx | 3 +- .../app-manage-policies/policy-list-view.tsx | 4 +- .../modals/app-manage-policies/style.scss | 5 +- .../unassign-policy-view.tsx | 193 ++++++++++++++ .../app-manage-policies/utils/k8s-utils.ts | 35 +-- .../app-manage-policies/utils/reducer.ts | 17 +- .../modals/app-manage-policies/utils/types.ts | 5 +- .../shared/dropdown/singleselectdropdown.tsx | 2 + packages/shared/labels/labels.tsx | 35 +++ 15 files changed, 699 insertions(+), 101 deletions(-) create mode 100644 packages/mco/components/modals/app-manage-policies/helper/policy-config-viewer.tsx create mode 100644 packages/mco/components/modals/app-manage-policies/unassign-policy-view.tsx create mode 100644 packages/shared/labels/labels.tsx diff --git a/locales/en/plugin__odf-console.json b/locales/en/plugin__odf-console.json index dcd462d9a..82d26ef01 100644 --- a/locales/en/plugin__odf-console.json +++ b/locales/en/plugin__odf-console.json @@ -194,9 +194,19 @@ "Assign policy to protect the application and ensure quick recovery. Unassign policy from an application when they no longer require to be managed.": "Assign policy to protect the application and ensure quick recovery. Unassign policy from an application when they no longer require to be managed.", "Manage list view alert": "Manage list view alert", "Confirm unassign": "Confirm unassign", + "All placements": "All placements", + "Policy configuration details": "Policy configuration details", + "Replication type": "Replication type", + "Sync interval": "Sync interval", + "Cluster": "Cluster", + "Replication status": "Replication status", + "Last sync {{time}}": "Last sync {{time}}", + "Application resources protected": "Application resources protected", + "placement": "placement", + "placements": "placements", + "PVC label selector": "PVC label selector", "Policy type": "Policy type", "Assigned on": "Assigned on", - "View configurations": "View configurations", "Unassign policy": "Unassign policy", "No activity": "No activity", "Relocate in progress": "Relocate in progress", @@ -212,6 +222,13 @@ "Unable to unassign all selected policies for the application.": "Unable to unassign all selected policies for the application.", "My policies": "My policies", "Assign policy": "Assign policy", + "Unassign policy will no longer DR protect your application, other applications sharing the same placement will also be affected.": "Unassign policy will no longer DR protect your application, other applications sharing the same placement will also be affected.", + "Policy unassigned for the application.": "Policy unassigned for the application.", + "Unable to unassign policy for the application.": "Unable to unassign policy for the application.", + "Confirm": "Confirm", + "Back": "Back", + "Unassign for all": "Unassign for all", + "Unassign": "Unassign", "List all the connected applications under a policy.": "List all the connected applications under a policy.", "Application name": "Application name", "application name search": "application name search", @@ -483,7 +500,6 @@ "Create new BucketClass": "Create new BucketClass", "BucketClass is a CRD representing a class for buckets that defines tiering policies and data placements for an OBC.": "BucketClass is a CRD representing a class for buckets that defines tiering policies and data placements for an OBC.", "Next": "Next", - "Back": "Back", "Edit BucketClass Resource": "Edit BucketClass Resource", "{{storeType}} represents a storage target to be used as the underlying storage for the data in Multicloud Object Gateway buckets.": "{{storeType}} represents a storage target to be used as the underlying storage for the data in Multicloud Object Gateway buckets.", "What is a BackingStore?": "What is a BackingStore?", diff --git a/packages/mco/components/modals/app-manage-policies/app-manage-policies-modal.tsx b/packages/mco/components/modals/app-manage-policies/app-manage-policies-modal.tsx index 98cde2baa..2b9b0fe04 100644 --- a/packages/mco/components/modals/app-manage-policies/app-manage-policies-modal.tsx +++ b/packages/mco/components/modals/app-manage-policies/app-manage-policies-modal.tsx @@ -3,29 +3,24 @@ import { StatusBox } from '@odf/shared/generic/status-box'; import { useCustomTranslation } from '@odf/shared/useCustomTranslationHook'; import { Modal, ModalVariant } from '@patternfly/react-core'; import { PolicyListView } from './policy-list-view'; +import { UnAssignPolicyView } from './unassign-policy-view'; import { + ManagePolicyStateAction, ManagePolicyStateType, MessageType, ModalActionContext, ModalViewContext, initialPolicyState, managePolicyStateReducer, + ManagePolicyState, } from './utils/reducer'; import { ApplicationType } from './utils/types'; -export const AppManagePoliciesModal: React.FC = ({ +export const ModalBody: React.FC = ({ applicaitonInfo, - loaded, - loadError, - isOpen, - close, + state, + dispatch, }) => { - const [state, dispatch] = React.useReducer( - managePolicyStateReducer, - initialPolicyState - ); - const { t } = useCustomTranslation(); - const setModalContext = (modalViewContext: ModalViewContext) => { dispatch({ type: ManagePolicyStateType.SET_MODAL_VIEW_CONTEXT, @@ -33,21 +28,63 @@ export const AppManagePoliciesModal: React.FC = ({ }); }; - const setModalActionContext = (modalViewContext: ModalActionContext) => + const setModalActionContext = ( + modalActionViewContext: ModalActionContext, + modalViewContext?: ModalViewContext + ) => dispatch({ type: ManagePolicyStateType.SET_MODAL_ACTION_CONTEXT, - context: state.modalViewContext, - payload: modalViewContext, + context: modalViewContext || state.modalViewContext, + payload: modalActionViewContext, }); - const setMessage = (message: MessageType) => { + const setMessage = ( + message: MessageType, + modalViewContext?: ModalViewContext + ) => { dispatch({ type: ManagePolicyStateType.SET_MESSAGE, - context: state.modalViewContext, + context: modalViewContext || state.modalViewContext, payload: message, }); }; + return ( + (state.modalViewContext === ModalViewContext.POLICY_LIST_VIEW && ( + + )) || + (state.modalViewContext === ModalViewContext.UNASSIGN_POLICY_VIEW && ( + + )) + ); +}; + +export const AppManagePoliciesModal: React.FC = ({ + applicaitonInfo, + loaded, + loadError, + isOpen, + close, +}) => { + const [state, dispatch] = React.useReducer( + managePolicyStateReducer, + initialPolicyState + ); + const { t } = useCustomTranslation(); + return ( = ({ onClose={close} > {loaded && !loadError ? ( - state.modalViewContext === ModalViewContext.POLICY_LIST_VIEW && ( - - ) + ) : ( )} @@ -78,10 +110,16 @@ export const AppManagePoliciesModal: React.FC = ({ ); }; -export type AppManagePoliciesModalProps = { +type AppManagePoliciesModalProps = { applicaitonInfo: ApplicationType; loaded: boolean; loadError: any; isOpen: boolean; close?: () => void; }; + +type ModalBodyProps = { + state: ManagePolicyState; + applicaitonInfo: ApplicationType; + dispatch: React.Dispatch; +}; diff --git a/packages/mco/components/modals/app-manage-policies/helper/messages.tsx b/packages/mco/components/modals/app-manage-policies/helper/messages.tsx index 88f8081e5..141e96b47 100644 --- a/packages/mco/components/modals/app-manage-policies/helper/messages.tsx +++ b/packages/mco/components/modals/app-manage-policies/helper/messages.tsx @@ -6,16 +6,19 @@ import { Button, ButtonVariant, } from '@patternfly/react-core'; -import { ModalActionContext, PolicyListViewState } from '../utils/reducer'; +import { CommonViewState, ModalActionContext } from '../utils/reducer'; import '../../../../style.scss'; -const hasActionFailedOrSuceeded = (modalActionContext: ModalActionContext) => +const hasActionTriggered = (modalActionContext: ModalActionContext) => [ ModalActionContext.UN_ASSIGN_POLICIES_SUCCEEDED, ModalActionContext.UN_ASSIGN_POLICIES_FAILED, + ModalActionContext.UNASSIGN_POLICY_SUCCEEDED, + ModalActionContext.UN_ASSIGN_POLICIES_SUCCEEDED, + ModalActionContext.UN_ASSIGNING_POLICY, ].includes(modalActionContext); -const AlertMessage: React.FC = ({ +const AlertMessage: React.FC = ({ title, variant, children, @@ -34,7 +37,7 @@ const AlertMessage: React.FC = ({ ); }; -export const ListViewMessages: React.FC = ({ +export const Messages: React.FC = ({ state, OnCancel, OnConfirm, @@ -56,7 +59,7 @@ export const ListViewMessages: React.FC = ({ )) || - (hasActionFailedOrSuceeded(modalActionContext) && ( + (hasActionTriggered(modalActionContext) && ( = ({ ); }; -export type ListViewMessagesProps = { - state: PolicyListViewState; - OnCancel: () => void; - OnConfirm: () => void; +export type MessagesProps = { + state: CommonViewState; + OnCancel?: () => void; + OnConfirm?: () => void; }; -type AlertMessageType = { +type AlertMessageProps = { title: string; variant: AlertVariant; children?: React.ReactNode; diff --git a/packages/mco/components/modals/app-manage-policies/helper/policy-config-viewer.tsx b/packages/mco/components/modals/app-manage-policies/helper/policy-config-viewer.tsx new file mode 100644 index 000000000..a7f032987 --- /dev/null +++ b/packages/mco/components/modals/app-manage-policies/helper/policy-config-viewer.tsx @@ -0,0 +1,238 @@ +import * as React from 'react'; +import { pluralize } from '@odf/core/components/utils'; +import { REPLICATION_DISPLAY_TEXT } from '@odf/mco/constants'; +import { parseSyncInterval } from '@odf/mco/utils'; +import { fromNow } from '@odf/shared/details-page/datetime'; +import { SingleSelectDropdown } from '@odf/shared/dropdown/singleselectdropdown'; +import { Labels } from '@odf/shared/labels/labels'; +import { getName } from '@odf/shared/selectors'; +import { + GreenCheckCircleIcon, + RedExclamationCircleIcon, +} from '@odf/shared/status/icons'; +import { useCustomTranslation } from '@odf/shared/useCustomTranslationHook'; +import { StatusIconAndText } from '@openshift-console/dynamic-plugin-sdk'; +import { TFunction } from 'i18next'; +import { + DescriptionList, + DescriptionListDescription, + DescriptionListGroup, + DescriptionListTerm, + SelectOption, + Text, +} from '@patternfly/react-core'; +import { DRPlacementControlType, DataPolicyType } from '../utils/types'; +import '../style.scss'; + +const getDropdownOptions = ( + placements: string[], + defaultOptionText: string +): JSX.Element[] => [ + , + ...placements?.map((placement) => ( + + )), +]; + +const getLabels = ( + placementControlInfo: PlacementControlMap, + selected: string, + isDefaultSelected: boolean +): string[] => + isDefaultSelected + ? // Aggregate all labels + Object.values(placementControlInfo)?.reduce( + (acc, drpc) => [...acc, ...drpc?.pvcSelector], + [] + ) + : // Get all labels from selected placement control info + [...placementControlInfo?.[selected]?.pvcSelector]; + +const getPlacementControlMap = ( + placementControls: DRPlacementControlType[] +): PlacementControlMap => + placementControls?.reduce( + (acc, drpc) => ({ + ...acc, + [getName(drpc?.placementInfo)]: drpc, + }), + {} + ); + +export const DataPolicyStatus: React.FC = ({ + isValidated, + t, +}) => { + return ( + } + : { + title: t('Not Validated'), + icon: , + })} + /> + ); +}; + +const DescriptionListItem: React.FC = ({ + term, + description, +}) => { + return ( + + {term} + {description} + + ); +}; + +export const PolicyConfigViewer: React.FC = ({ + policy, + disableSelector, + onSelect, +}) => { + const { t } = useCustomTranslation(); + const defaultSelectionText = t('All placements'); + const [selected, setSelected] = React.useState(defaultSelectionText); + const isDefaultSelected = selected === defaultSelectionText; + const placementControlMap: PlacementControlMap = React.useMemo( + () => getPlacementControlMap(policy?.placementControInfo), + [policy?.placementControInfo] + ); + const labels = React.useMemo( + () => getLabels(placementControlMap, selected, isDefaultSelected), + [selected, isDefaultSelected, placementControlMap] + ); + const dropdownOptions = React.useMemo( + () => + getDropdownOptions( + Object.keys(placementControlMap), + defaultSelectionText + ), + [placementControlMap, defaultSelectionText] + ); + const [unit, interval] = parseSyncInterval(policy?.schedulingInterval); + const syncScheduleFormat = { + m: t('minutes'), + h: t('hours'), + d: t('days'), + }; + const onChange = (value: string) => { + setSelected(value); + if (value === defaultSelectionText) { + onSelect(Object.values(placementControlMap), true); + } else { + onSelect([placementControlMap[value]], false); + } + }; + + return ( +
+
+ {t('Policy configuration details')} + {!!Object.keys(placementControlMap)?.length && ( + + )} +
+
+ + + + + + } + /> + ( +

{clusterName}

+ ))} + /> + {!isDefaultSelected ? ( + <> + + + + ) : ( + + )} + } + /> +
+
+
+ ); +}; + +type DescriptionListItemProps = { + term: React.ReactNode; + description: React.ReactNode; +}; + +type DataPolicyStatusProps = { + isValidated: boolean; + t: TFunction; +}; + +type PlacementControlMap = { + [placementName: string]: DRPlacementControlType; +}; + +type PolicyConfigViewerProps = { + policy: DataPolicyType; + disableSelector?: boolean; + onSelect?: ( + placementControls: DRPlacementControlType[], + isAllSelected: boolean + ) => void; +}; diff --git a/packages/mco/components/modals/app-manage-policies/helper/policy-list-view-table.tsx b/packages/mco/components/modals/app-manage-policies/helper/policy-list-view-table.tsx index 99c80b046..f0a826384 100644 --- a/packages/mco/components/modals/app-manage-policies/helper/policy-list-view-table.tsx +++ b/packages/mco/components/modals/app-manage-policies/helper/policy-list-view-table.tsx @@ -1,17 +1,12 @@ import * as React from 'react'; import { utcDateTimeFormatter } from '@odf/shared/details-page/datetime'; import { getName } from '@odf/shared/selectors'; -import { - GreenCheckCircleIcon, - RedExclamationCircleIcon, -} from '@odf/shared/status/icons'; import { RowComponentType, SelectableTable, TableColumnProps, } from '@odf/shared/table/selectable-table'; import { useCustomTranslation } from '@odf/shared/useCustomTranslationHook'; -import { StatusIconAndText } from '@openshift-console/dynamic-plugin-sdk'; import { TFunction } from 'i18next'; import { Text } from '@patternfly/react-core'; import { @@ -23,6 +18,7 @@ import { Td } from '@patternfly/react-table'; import { ModalActionContext, ModalViewContext } from '../utils/reducer'; import { DataPolicyType } from '../utils/types'; import '../style.scss'; +import { DataPolicyStatus } from './policy-config-viewer'; const sortRows = ( a: DataPolicyType, @@ -54,17 +50,9 @@ const PolicyListViewTableRow: React.FC> = ({ const { kind, isValidated, activity, assignedOn } = policy; const { isActionDisabled, setPolicy, setModalContext }: RowExtraProps = extraProps; - const status = isValidated ? t('Validated') : t('Not Validated'); const assignedDateStr = utcDateTimeFormatter.format(new Date(assignedOn)); const RowActions = (t: TFunction): IAction[] => [ - { - title: t('View configurations'), - onClick: () => { - setPolicy(policy, ModalViewContext.POLICY_CONFIGURATON_VIEW); - setModalContext(ModalViewContext.POLICY_CONFIGURATON_VIEW); - }, - }, { title: t('Unassign policy'), onClick: () => { @@ -83,16 +71,7 @@ const PolicyListViewTableRow: React.FC> = ({ {kind} - - ) : ( - - ) - } - /> + @@ -157,7 +136,7 @@ export const PolicyListViewTable: React.FC = ({ }, [t]); return ( -
+
columns={columns} rows={policies} diff --git a/packages/mco/components/modals/app-manage-policies/parsers/application-set-parser.spec.tsx b/packages/mco/components/modals/app-manage-policies/parsers/application-set-parser.spec.tsx index a91876a3c..26b632c6f 100644 --- a/packages/mco/components/modals/app-manage-policies/parsers/application-set-parser.spec.tsx +++ b/packages/mco/components/modals/app-manage-policies/parsers/application-set-parser.spec.tsx @@ -244,7 +244,6 @@ describe('ApplicationSet manage data policy modal', () => { name: 'Actions', }) ); - expect(screen.getByText('View configurations')).toBeInTheDocument(); expect(screen.getByText('Unassign policy')).toBeInTheDocument(); // 2) Search using invalid policy fireEvent.change(searchBox, { target: { value: 'invalid policy' } }); @@ -279,4 +278,104 @@ describe('ApplicationSet manage data policy modal', () => { ) ).toBeInTheDocument(); }); + + test('Unassign policy configuration view test', async () => { + // Row actions + fireEvent.click( + screen.getByRole('button', { + name: 'Actions', + }) + ); + // Policy config view + fireEvent.click(screen.getByText('Unassign policy')); + // Title + expect( + screen.getByText('Policy configuration details') + ).toBeInTheDocument(); + // All placement view + expect(screen.getByText('All placements')).toBeInTheDocument(); + // Headers + expect(screen.getByText('Policy name')).toBeInTheDocument(); + expect(screen.getByText('Replication type')).toBeInTheDocument(); + expect(screen.getByText('Sync interval')).toBeInTheDocument(); + expect(screen.getByText('Status')).toBeInTheDocument(); + expect(screen.getByText('Cluster')).toBeInTheDocument(); + expect( + screen.getByText('Application resources protected') + ).toBeInTheDocument(); + expect(screen.getByText('PVC label selector')).toBeInTheDocument(); + // Values + expect(screen.getByText('mock-policy-1')).toBeInTheDocument(); + expect(screen.getByText('Asynchronous')).toBeInTheDocument(); + expect(screen.getByText('5 minutes')).toBeInTheDocument(); + expect(screen.getByText('Validated')).toBeInTheDocument(); + expect(screen.getByText('east-1')).toBeInTheDocument(); + expect(screen.getByText('west-1')).toBeInTheDocument(); + expect(screen.getByText('1 placement')).toBeInTheDocument(); + expect(screen.getByText('pvc=pvc1')).toBeInTheDocument(); + // Placement view + fireEvent.click(screen.getByLabelText('Options menu')); + fireEvent.click(screen.getByText('mock-placement-1')); + // Added headers + expect(screen.getByText('Replication status')).toBeInTheDocument(); + // Added values + expect(screen.getAllByText('mock-placement-1').length === 2).toBeTruthy(); + // Footer + fireEvent.click(screen.getByText('Back')); + // Make sure context is switched to list view + expect(screen.getByText('My policies')).toBeInTheDocument(); + }); + + test('Unprotect more than one placement test', async () => { + // Row actions + fireEvent.click( + screen.getByRole('button', { + name: 'Actions', + }) + ); + // Unassign policy view + fireEvent.click(screen.getByText('Unassign policy')); + // Footer for all placements + expect(screen.getByText('Back')).toBeInTheDocument(); + expect(screen.getByText('Unassign for all')).toBeInTheDocument(); + // Unassign policy + fireEvent.click(screen.getByText('Unassign for all')); + // Confirm unassign policy + expect(screen.getByText('Cancel')).toBeInTheDocument(); + expect(screen.getByText('Confirm')).toBeInTheDocument(); + await waitFor(() => { + fireEvent.click(screen.getByText('Confirm')); + }); + expect( + screen.getByText('Policy unassigned for the application.') + ).toBeInTheDocument(); + }); + + test('Unprotect a placement test', async () => { + // Row actions + fireEvent.click( + screen.getByRole('button', { + name: 'Actions', + }) + ); + // Unassign policy view + fireEvent.click(screen.getByText('Unassign policy')); + // Select a placement + fireEvent.click(screen.getByLabelText('Options menu')); + fireEvent.click(screen.getByText('mock-placement-1')); + // Footer for selected placement + expect(screen.getByText('Back')).toBeInTheDocument(); + expect(screen.getByText('Unassign')).toBeInTheDocument(); + // Unassign policy + fireEvent.click(screen.getByText('Unassign')); + // Confirm unassign policy + expect(screen.getByText('Cancel')).toBeInTheDocument(); + expect(screen.getByText('Confirm')).toBeInTheDocument(); + await waitFor(() => { + fireEvent.click(screen.getByText('Confirm')); + }); + expect( + screen.getByText('Policy unassigned for the application.') + ).toBeInTheDocument(); + }); }); diff --git a/packages/mco/components/modals/app-manage-policies/parsers/application-set-parser.tsx b/packages/mco/components/modals/app-manage-policies/parsers/application-set-parser.tsx index dc468558d..8f4c37d81 100644 --- a/packages/mco/components/modals/app-manage-policies/parsers/application-set-parser.tsx +++ b/packages/mco/components/modals/app-manage-policies/parsers/application-set-parser.tsx @@ -25,6 +25,7 @@ import { getRemoteNamespaceFromAppSet, isDRPolicyValidated, } from '@odf/mco/utils'; +import { arrayify } from '@odf/shared/modals/EditLabelModal'; import { getNamespace } from '@odf/shared/selectors'; import { useCustomTranslation } from '@odf/shared/useCustomTranslationHook'; import { TFunction } from 'i18next'; @@ -97,7 +98,7 @@ const generateDRPlacementControlInfo = ( metadata: drpc.metadata, drPolicyRef: drpc.spec.drPolicyRef, placementInfo: plsInfo, - pvcSelector: drpc.spec?.pvcSelector, + pvcSelector: arrayify(drpc?.spec?.pvcSelector?.matchLabels) || [], lastGroupSyncTime: drpc?.status?.lastGroupSyncTime, status: drpc?.status?.phase, }, diff --git a/packages/mco/components/modals/app-manage-policies/policy-list-view.tsx b/packages/mco/components/modals/app-manage-policies/policy-list-view.tsx index e8684d6fc..27b99e42e 100644 --- a/packages/mco/components/modals/app-manage-policies/policy-list-view.tsx +++ b/packages/mco/components/modals/app-manage-policies/policy-list-view.tsx @@ -15,7 +15,7 @@ import { Text, AlertVariant, } from '@patternfly/react-core'; -import { ListViewMessages } from './helper/messages'; +import { Messages } from './helper/messages'; import { PolicyListViewTable } from './helper/policy-list-view-table'; import { unAssignPromises } from './utils/k8s-utils'; import { @@ -182,7 +182,7 @@ export const PolicyListView: React.FC = ({ setMessage={setMessage} />
- setModalActionContext(null)} OnConfirm={unAssignPolicies} diff --git a/packages/mco/components/modals/app-manage-policies/style.scss b/packages/mco/components/modals/app-manage-policies/style.scss index b4eaef923..352c4b904 100644 --- a/packages/mco/components/modals/app-manage-policies/style.scss +++ b/packages/mco/components/modals/app-manage-policies/style.scss @@ -1,5 +1,5 @@ .mco-manage-policies { - &__listViewTable--padding { + &__row--padding { padding: 0 0 var(--pf-global--spacer--md) 0; } &__col-padding { @@ -11,4 +11,7 @@ justify-content: space-between; padding: 0 var(--pf-global--spacer--md) 0 var(--pf-global--spacer--md); } + &__dropdown--wide { + min-width: var(--pf-global--spacer--lg); + } } diff --git a/packages/mco/components/modals/app-manage-policies/unassign-policy-view.tsx b/packages/mco/components/modals/app-manage-policies/unassign-policy-view.tsx new file mode 100644 index 000000000..b9553963e --- /dev/null +++ b/packages/mco/components/modals/app-manage-policies/unassign-policy-view.tsx @@ -0,0 +1,193 @@ +import * as React from 'react'; +import { ModalBody, ModalFooter } from '@odf/shared/modals/Modal'; +import { useCustomTranslation } from '@odf/shared/useCustomTranslationHook'; +import { getErrorMessage } from '@odf/shared/utils'; +import { Button, ButtonVariant, AlertVariant } from '@patternfly/react-core'; +import { Messages } from './helper/messages'; +import { PolicyConfigViewer } from './helper/policy-config-viewer'; +import { unAssignPromises } from './utils/k8s-utils'; +import { + ManagePolicyStateAction, + ManagePolicyStateType, + MessageType, + ModalActionContext, + ModalViewContext, + UnAssignPolicyViewState, +} from './utils/reducer'; +import { DRPlacementControlType } from './utils/types'; + +export const GeneratefooterButtons: React.FC = ({ + drpcs, + isAllSelected, + modalActionContext, + setModalContext, + setModalActionContext, + setMessage, + dispatch, +}) => { + const { t } = useCustomTranslation(); + const [isButtonLoading, setButtonLoading] = React.useState(false); + + const onBack = () => setModalContext(ModalViewContext.POLICY_LIST_VIEW); + const onUnassign = () => { + setModalActionContext(ModalActionContext.UN_ASSIGNING_POLICY); + setMessage({ + title: t( + 'Unassign policy will no longer DR protect your application, other applications sharing the same placement will also be affected.' + ), + variant: AlertVariant.warning, + }); + }; + const onCancel = () => setModalActionContext(undefined); + + const unAssignPolicy = () => { + setButtonLoading(true); + // unassign DRPolicy + const promises = unAssignPromises(drpcs); + Promise.all(promises) + .then(() => { + setButtonLoading(false); + // Display success message + setModalActionContext( + ModalActionContext.UNASSIGN_POLICY_SUCCEEDED, + ModalViewContext.POLICY_LIST_VIEW + ); + setMessage( + { + title: t('Policy unassigned for the application.'), + variant: AlertVariant.success, + }, + ModalViewContext.POLICY_LIST_VIEW + ); + // Switch to list policy view + setModalContext(ModalViewContext.POLICY_LIST_VIEW); + dispatch({ + type: ManagePolicyStateType.SET_SELECTED_POLICY, + context: ModalViewContext.UNASSIGN_POLICY_VIEW, + payload: undefined, + }); + }) + .catch((error) => { + setButtonLoading(false); + setMessage({ + title: t('Unable to unassign policy for the application.'), + description: getErrorMessage(error), + variant: AlertVariant.danger, + }); + }); + }; + + return modalActionContext === ModalActionContext.UN_ASSIGNING_POLICY ? ( + <> + + + + ) : ( + <> + + + + ); +}; + +export const UnAssignPolicyView: React.FC = ({ + state, + setModalContext, + setModalActionContext, + setMessage, + dispatch, +}) => { + const { policy, modalActionContext } = state; + const [selectedDRPCs, SetSelectedDRPCs] = React.useState( + policy?.placementControInfo + ); + const [isAllDRPCSelected, setIsAllDRPCSelected] = React.useState(true); + const isDisableSelect = + modalActionContext === ModalActionContext.UN_ASSIGNING_POLICY; + const onSelect = ( + drpcs: DRPlacementControlType[], + isAllSelected: boolean + ) => { + SetSelectedDRPCs(drpcs); + setIsAllDRPCSelected(isAllSelected); + }; + + return ( + <> + + + + + + + + + ); +}; + +type UnAssignPolicyViewProps = { + state: UnAssignPolicyViewState; + setModalContext: (modalViewContext: ModalViewContext) => void; + setModalActionContext?: ( + modalActionContext: ModalActionContext, + modalViewContext?: ModalViewContext + ) => void; + setMessage?: ( + message: MessageType, + modalViewContext?: ModalViewContext + ) => void; + dispatch?: React.Dispatch; +}; + +type GeneratefooterButtonsProps = { + drpcs: DRPlacementControlType[]; + isAllSelected: boolean; + modalActionContext: ModalActionContext; + setModalContext: (modalViewContext: ModalViewContext) => void; + setModalActionContext?: ( + modalActionContext: ModalActionContext, + modalViewContext?: ModalViewContext + ) => void; + setMessage?: ( + message: MessageType, + modalViewContext?: ModalViewContext + ) => void; + dispatch?: React.Dispatch; +}; 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 6147af68f..e651fb7ce 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 @@ -8,30 +8,33 @@ import { K8sResourceKind } from '@odf/shared/types'; import { k8sDelete, k8sPatch } from '@openshift-console/dynamic-plugin-sdk'; import { DRPlacementControlType } from './types'; -export const unAssignPromises = (drpcs: DRPlacementControlType[]) => { - const promises: Promise[] = []; +export const placementUnAssignPromise = (drpc: DRPlacementControlType) => { const patch = [ { op: 'remove', path: `/metadata/annotations/${PROTECTED_APP_ANNOTATION_WO_SLASH}`, }, ]; + return k8sPatch({ + model: ACMPlacementModel, + resource: { + metadata: { + name: getName(drpc?.placementInfo), + namespace: getNamespace(drpc?.placementInfo), + }, + }, + data: patch, + cluster: HUB_CLUSTER_NAME, + }); +}; - drpcs?.forEach((drpc) => { - promises.push( - k8sPatch({ - model: ACMPlacementModel, - resource: { - metadata: { - name: getName(drpc?.placementInfo), - namespace: getNamespace(drpc?.placementInfo), - }, - }, - data: patch, - cluster: HUB_CLUSTER_NAME, - }) - ); +export const unAssignPromises = (drpcs: DRPlacementControlType[]) => { + const promises: Promise[] = []; + drpcs?.forEach((drpc) => { + if (drpc?.placementInfo?.kind === ACMPlacementModel.kind) { + promises.push(placementUnAssignPromise(drpc)); + } promises.push( k8sDelete({ model: DRPlacementControlModel, 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 0e4a98555..8db733b06 100644 --- a/packages/mco/components/modals/app-manage-policies/utils/reducer.ts +++ b/packages/mco/components/modals/app-manage-policies/utils/reducer.ts @@ -5,14 +5,13 @@ export enum ModalViewContext { POLICY_LIST_VIEW = 'policyListView', ASSIGN_POLICY_VIEW = 'assignPolicyView', UNASSIGN_POLICY_VIEW = 'unAssignPolicyView', - POLICY_CONFIGURATON_VIEW = 'policyConfigurationView', } export enum ModalActionContext { UN_ASSIGNING_POLICIES = 'UN_ASSIGNING_POLICIES', UN_ASSIGNING_POLICY = 'UN_ASSIGNING_POLICY', UN_ASSIGN_POLICIES_SUCCEEDED = 'UN_ASSIGN_POLICIES_SUCCEEDED', - UN_ASSIGN_POLICY_SUCCEEDED = 'UN_ASSIGN_POLICY_SUCCEEDED', + UNASSIGN_POLICY_SUCCEEDED = 'UNASSIGN_POLICY_SUCCEEDED', UN_ASSIGN_POLICIES_FAILED = 'UN_ASSIGN_POLICIES_FAILED', UN_ASSIGN_POLICY_FAILED = 'UN_ASSIGN_POLICY_FAILED', } @@ -23,20 +22,16 @@ export type MessageType = { variant?: AlertVariant; }; -export type commonViewState = { +export type CommonViewState = { modalActionContext: ModalActionContext; message: MessageType; }; -export type PolicyListViewState = commonViewState & { +export type PolicyListViewState = CommonViewState & { policies: DataPolicyType[]; }; -export type PolicyConfigViewState = { - policy: DataPolicyType; -}; - -export type UnAssignPolicyViewState = commonViewState & { +export type UnAssignPolicyViewState = CommonViewState & { policy: DataPolicyType; }; @@ -44,7 +39,6 @@ export type ManagePolicyState = { modalViewContext: ModalViewContext; [ModalViewContext.POLICY_LIST_VIEW]: PolicyListViewState; [ModalViewContext.UNASSIGN_POLICY_VIEW]: UnAssignPolicyViewState; - [ModalViewContext.POLICY_CONFIGURATON_VIEW]: PolicyConfigViewState; }; export enum ManagePolicyStateType { @@ -71,9 +65,6 @@ export const initialPolicyState: ManagePolicyState = { title: '', }, }, - [ModalViewContext.POLICY_CONFIGURATON_VIEW]: { - policy: {}, - }, }; export type ManagePolicyStateAction = 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 66aa5dd28..924c0418a 100644 --- a/packages/mco/components/modals/app-manage-policies/utils/types.ts +++ b/packages/mco/components/modals/app-manage-policies/utils/types.ts @@ -1,6 +1,5 @@ import { K8sResourceCommon, - MatchLabels, ObjectReference, } from '@openshift-console/dynamic-plugin-sdk'; import { REPLICATION_TYPE } from '../../../../constants'; @@ -12,9 +11,7 @@ export type PlacementType = K8sResourceCommon & { export type DRPlacementControlType = K8sResourceCommon & { drPolicyRef: ObjectReference; placementInfo: PlacementType; - pvcSelector?: { - matchLabels: MatchLabels; - }; + pvcSelector?: string[]; lastGroupSyncTime: string; status: string; }; diff --git a/packages/shared/dropdown/singleselectdropdown.tsx b/packages/shared/dropdown/singleselectdropdown.tsx index 8d4026a5e..7b68ee9fa 100644 --- a/packages/shared/dropdown/singleselectdropdown.tsx +++ b/packages/shared/dropdown/singleselectdropdown.tsx @@ -44,6 +44,7 @@ export const SingleSelectDropdown: React.FC = ({ placeholderText={props?.placeholderText || t('Select options')} aria-labelledby={props?.id} noResultsFoundText={t('No results found')} + isDisabled={props?.isDisabled} > {selectOptions} @@ -62,4 +63,5 @@ export type SingleSelectDropdownProps = { 'data-test-id'?: string; onFilter?: SelectProps['onFilter']; hasInlineFilter?: SelectProps['hasInlineFilter']; + isDisabled?: boolean; }; diff --git a/packages/shared/labels/labels.tsx b/packages/shared/labels/labels.tsx new file mode 100644 index 000000000..89638950d --- /dev/null +++ b/packages/shared/labels/labels.tsx @@ -0,0 +1,35 @@ +import * as React from 'react'; +import { Label, LabelGroup } from '@patternfly/react-core'; + +export const Labels: React.FC = ({ + labels, + collapsedText, + expandedText, + numLabels, + className, +}) => { + return ( + + {labels.map((label) => ( + + ))} + + ); +}; + +export type LabelsProps = { + labels?: string[]; + /** Customizable "Show Less" text string */ + collapsedText?: string; + /** Customizeable template string. Use variable "${remaining}" for the overflow label count. */ + expandedText?: string; + /** Set number of labels to show before overflow */ + numLabels?: number; + className?: string; +};