diff --git a/locales/en/plugin__odf-console.json b/locales/en/plugin__odf-console.json index dcd462d9a..cf14ef57f 100644 --- a/locales/en/plugin__odf-console.json +++ b/locales/en/plugin__odf-console.json @@ -190,17 +190,47 @@ "{{selected}} of {{total}} selected": "{{selected}} of {{total}} selected", "subscription-selector": "subscription-selector", "Select the subscriptions groups you wish to replicate via": "Select the subscriptions groups you wish to replicate via", + "Assign Policy": "Assign Policy", + "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 Policy": "Manage Policy", "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.", + "PVC details": "PVC details", + "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", "Manage list view alert": "Manage list view alert", "Confirm unassign": "Confirm unassign", + "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", - "Failover in progress": "Failover in progress", + "Delete": "Delete", + "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", + "Policy": "Policy", + "Select a policy": "Select a policy", "Search": "Search", "Search input": "Search input", "Secondary actions": "Secondary actions", @@ -212,6 +242,8 @@ "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", + "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.", "Application name": "Application name", "application name search": "application name search", @@ -237,7 +269,6 @@ "A selector label to DR protect only specific PVCs within an application.": "A selector label to DR protect only specific PVCs within an application.", "When multiple applications are selected, DR protection will be applied for all the PVCs under the application's namespace.": "When multiple applications are selected, DR protection will be applied for all the PVCs under the application's namespace.", "Protect all PVCs within the application's namespace": "Protect all PVCs within the application's namespace", - "Assign": "Assign", "Asynchronous": "Asynchronous", "Synchronous": "Synchronous", "{{async}}, interval: {{interval}}": "{{async}}, interval: {{interval}}", @@ -426,7 +457,6 @@ "Try Again": "Try Again", "Finish": "Finish", "Go To PVC List": "Go To PVC List", - "Delete": "Delete", "BlockPool Update Form": "BlockPool Update Form", "Filesystem name": "Filesystem name", "Enter filesystem name": "Enter filesystem name", @@ -482,8 +512,6 @@ "Create BucketClass": "Create BucketClass", "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..ebadd835a 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 @@ -1,7 +1,9 @@ import * as React from 'react'; import { StatusBox } from '@odf/shared/generic/status-box'; import { useCustomTranslation } from '@odf/shared/useCustomTranslationHook'; +import { TFunction } from 'i18next'; import { Modal, ModalVariant } from '@patternfly/react-core'; +import { AssignPolicyView } from './assign-policy-view'; import { PolicyListView } from './policy-list-view'; import { ManagePolicyStateType, @@ -11,10 +13,29 @@ import { initialPolicyState, managePolicyStateReducer, } from './utils/reducer'; -import { ApplicationType } from './utils/types'; +import { ApplicationType, DataPolicyType } from './utils/types'; + +const getModalTitle = (modalViewContext: ModalViewContext, t: TFunction) => { + if (modalViewContext === ModalViewContext.ASSIGN_POLICY_VIEW) { + return { + title: t('Assign Policy'), + description: t( + 'Secure your application by assigning a policy from the available policy templates.' + ), + }; + } else { + return { + title: t('Manage Policy'), + description: t( + 'Assign policy to protect the application and ensure quick recovery. Unassign policy from an application when they no longer require to be managed.' + ), + }; + } +}; export const AppManagePoliciesModal: React.FC = ({ applicaitonInfo, + dataPolicies, loaded, loadError, isOpen, @@ -33,27 +54,33 @@ export const AppManagePoliciesModal: React.FC = ({ }); }; - const setModalActionContext = (modalViewContext: ModalActionContext) => + const setModalActionContext = ( + modalActionContext: ModalActionContext, + modalViewContext?: ModalViewContext + ) => dispatch({ type: ManagePolicyStateType.SET_MODAL_ACTION_CONTEXT, - context: state.modalViewContext, - payload: modalViewContext, + context: modalViewContext || state.modalViewContext, + payload: modalActionContext, }); - const setMessage = (message: MessageType) => { + const setMessage = ( + message: MessageType, + modalViewContext?: ModalViewContext + ) => { dispatch({ type: ManagePolicyStateType.SET_MESSAGE, - context: state.modalViewContext, + context: modalViewContext || state.modalViewContext, payload: message, }); }; + const modalTitle = getModalTitle(state.modalViewContext, t); + return ( = ({ onClose={close} > {loaded && !loadError ? ( - state.modalViewContext === ModalViewContext.POLICY_LIST_VIEW && ( + (state.modalViewContext === ModalViewContext.POLICY_LIST_VIEW && ( = ({ setModalActionContext={setModalActionContext} setMessage={setMessage} /> - ) + )) || + (state.modalViewContext === ModalViewContext.ASSIGN_POLICY_VIEW && ( + + )) ) : ( )} @@ -80,6 +118,7 @@ export const AppManagePoliciesModal: React.FC = ({ export type AppManagePoliciesModalProps = { applicaitonInfo: ApplicationType; + dataPolicies: DataPolicyType[]; loaded: boolean; loadError: any; isOpen: boolean; 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 new file mode 100644 index 000000000..0d116aa9c --- /dev/null +++ b/packages/mco/components/modals/app-manage-policies/assign-policy-view.tsx @@ -0,0 +1,194 @@ +import * as React from 'react'; +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 { PolicyConfigViewer } from './helper/policy-config-viewer'; +import { PVCDetailsWizardContent } from './helper/pvc-details-wizard-content'; +import { SelectPolicyWizardContent } from './helper/select-policy-wizard-content'; +import { assignPromises } from './utils/k8s-utils'; +import { + AssignPolicyViewState, + ManagePolicyStateAction, + ManagePolicyStateType, + MessageType, + ModalActionContext, + ModalViewContext, +} from './utils/reducer'; +import { + ApplicationType, + DRPolicyType, + DataPolicyType, + PlacementType, +} from './utils/types'; + +export const createSteps = ( + workloadNamespace: string, + placements: PlacementType[], + dataPolicies: DRPolicyType[], + state: AssignPolicyViewState, + stepIdReached: number, + t: TFunction, + setPolicy: (policy?: DataPolicyType) => void +): WizardStep[] => [ + { + id: 1, + name: t('Select policy'), + component: ( + + ), + enableNext: !!getName(state.policy), + }, + { + id: 2, + name: t('PVC details'), + component: ( + + ), + canJumpTo: stepIdReached >= 2, + enableNext: !!state.policy?.placementControInfo?.length, + }, + { + id: 3, + name: t('Review and assign'), + component: ( + + ), + nextButtonText: t('Assign'), + canJumpTo: stepIdReached >= 3, + }, +]; + +export const AssignPolicyView: React.FC = ({ + state, + applicaitonInfo, + dataPolicies, + setModalContext, + setModalActionContext, + setMessage, + dispatch, +}) => { + const { t } = useCustomTranslation(); + const [stepIdReached, setStepIdReached] = React.useState(1); + + const onNext = ({ id }: WizardStep) => { + if (id) { + if (typeof id === 'string') { + id = parseInt(id, 10); + } + setStepIdReached(stepIdReached < id ? id : stepIdReached); + } + }; + + const setPolicy = (policy: DataPolicyType = {}) => + dispatch({ + type: ManagePolicyStateType.SET_SELECTED_POLICY, + context: ModalViewContext.ASSIGN_POLICY_VIEW, + payload: policy, + }); + + const assignPolicy = () => { + const updateContext = async ( + title: string, + description: string, + variant: AlertVariant, + actionContext: ModalActionContext + ) => { + // inject a message into list view + setMessage( + { + title, + description, + variant, + }, + ModalViewContext.POLICY_LIST_VIEW + ); + setModalActionContext(actionContext, ModalViewContext.POLICY_LIST_VIEW); + // switch to list policy view + setModalContext(ModalViewContext.POLICY_LIST_VIEW); + // reset policy info + setPolicy(); + }; + // assign DRPolicy + const promises = assignPromises(state.policy); + Promise.all(promises) + .then(() => { + updateContext( + t('New policy assigned to application.'), + '', + AlertVariant.success, + ModalActionContext.ASSIGN_POLICY_SUCCEEDED + ); + }) + .catch((error) => { + updateContext( + t('Unable to assign policy to application.'), + getErrorMessage(error), + AlertVariant.danger, + ModalActionContext.ASSIGN_POLICY_FAILED + ); + }); + }; + + return ( + + { + setModalContext(ModalViewContext.POLICY_LIST_VIEW); + // reset policy info + setPolicy(); + }} + steps={createSteps( + applicaitonInfo?.workloadNamespace, + applicaitonInfo?.placements, + dataPolicies, + state, + stepIdReached, + t, + setPolicy + )} + onNext={onNext} + height={450} + /> + + ); +}; + +type AssignPolicyViewProps = { + state: AssignPolicyViewState; + applicaitonInfo: ApplicationType; + dataPolicies: DataPolicyType[]; + dispatch: React.Dispatch; + setModalContext: (modalViewContext: ModalViewContext) => void; + setModalActionContext: ( + modalActionContext: ModalActionContext, + modalViewContext?: ModalViewContext + ) => void; + setMessage: (error: MessageType, modalViewContext?: ModalViewContext) => void; +}; 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..21dc55ec4 100644 --- a/packages/mco/components/modals/app-manage-policies/helper/messages.tsx +++ b/packages/mco/components/modals/app-manage-policies/helper/messages.tsx @@ -6,13 +6,15 @@ import { Button, ButtonVariant, } from '@patternfly/react-core'; -import { ModalActionContext, PolicyListViewState } from '../utils/reducer'; +import { ModalActionContext, commonViewState } from '../utils/reducer'; import '../../../../style.scss'; const hasActionFailedOrSuceeded = (modalActionContext: ModalActionContext) => [ ModalActionContext.UN_ASSIGN_POLICIES_SUCCEEDED, ModalActionContext.UN_ASSIGN_POLICIES_FAILED, + ModalActionContext.ASSIGN_POLICY_SUCCEEDED, + ModalActionContext.ASSIGN_POLICY_FAILED, ].includes(modalActionContext); const AlertMessage: React.FC = ({ @@ -34,7 +36,7 @@ const AlertMessage: React.FC = ({ ); }; -export const ListViewMessages: React.FC = ({ +export const Messages: React.FC = ({ state, OnCancel, OnConfirm, @@ -68,8 +70,8 @@ export const ListViewMessages: React.FC = ({ ); }; -export type ListViewMessagesProps = { - state: PolicyListViewState; +export type MessagesProps = { + state: commonViewState; OnCancel: () => void; OnConfirm: () => void; }; 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..8eadea76b --- /dev/null +++ b/packages/mco/components/modals/app-manage-policies/helper/policy-config-viewer.tsx @@ -0,0 +1,230 @@ +import * as React from 'react'; +import { pluralize } from '@odf/core/components/utils'; +import { REPLICATION_DISPLAY_TEXT, REPLICATION_TYPE } 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 { + DescriptionList, + DescriptionListDescription, + DescriptionListGroup, + DescriptionListTerm, + SelectOption, + Text, +} from '@patternfly/react-core'; +import { DRPlacementControlType } from '../utils/types'; +import '../style.scss'; + +const dropdownOptions = ( + placementControlInfo: PlacementControlProps, + defaultOptionText: string +): JSX.Element[] => [ + , + ...Object.keys(placementControlInfo).map((placement) => ( + + )), +]; + +export const getLabels = ( + placementControlInfo: PlacementControlProps, + selected: string, + isDefaultSelected: boolean +): string[] => + isDefaultSelected + ? // Aggregate all labels + Object.values(placementControlInfo).reduce( + (acc, info) => [...acc, ...info?.pvcSelectors], + [] + ) + : // Get all labels from selected placement control info + placementControlInfo?.[selected]?.pvcSelectors || []; + +export const DescriptionListItem: React.FC = ( + props +) => { + return ( + + + {props.term} + + + {props.description} + + + ); +}; + +const getPlacementControlInfo = ( + drpcs: DRPlacementControlType[] +): PlacementControlProps => + drpcs?.reduce( + (acc, drpc) => ({ + ...acc, + [getName(drpc?.placementInfo)]: { + syncTime: drpc?.lastGroupSyncTime, + pvcSelectors: drpc?.pvcSelector, + }, + }), + {} + ); + +export const PolicyConfigViewer: React.FC = ({ + policyName, + replicationType, + syncInterval, + isPolicyValidated, + clusters, + drPlacementControlInfo = [], + defaultSelectionText, + isDisabledSelector, + hideSelector, + onSelect, +}) => { + const { t } = useCustomTranslation(); + const [selected, setSelected] = React.useState(defaultSelectionText || ''); + const isDefaultSelected = selected === defaultSelectionText; + const placementControlInfo: PlacementControlProps = React.useMemo( + () => getPlacementControlInfo(drPlacementControlInfo), + [drPlacementControlInfo] + ); + const labels = React.useMemo( + () => getLabels(placementControlInfo, selected, isDefaultSelected), + [selected, isDefaultSelected, placementControlInfo] + ); + + const [unit, interval] = parseSyncInterval(syncInterval); + const syncScheduleFormat = { + m: t('minutes'), + h: t('hours'), + d: t('days'), + }; + + return ( +
+
+ {t('Policy configuration details')} + {!!Object.keys(placementControlInfo)?.length && !hideSelector && ( + { + setSelected(value); + onSelect(value); + }} + /> + )} +
+
+ + + + + } + : { + title: t('Not Validated'), + icon: , + })} + /> + } + /> + ( +

{clusterName}

+ ))} + /> + {!isDefaultSelected && !hideSelector ? ( + <> + + + + ) : ( + + )} + } + /> +
+
+
+ ); +}; + +type DescriptionListItemProps = { + term: React.ReactNode; + description: React.ReactNode; +}; + +type PlacementControlProps = { + [placementName in string]: { + syncTime: string; + pvcSelectors: string[]; + }; +}; + +type PolicyConfigViewerProps = { + policyName: string; + replicationType: REPLICATION_TYPE; + syncInterval: string; + isPolicyValidated: boolean; + clusters: string[]; + drPlacementControlInfo?: DRPlacementControlType[]; + defaultSelectionText?: string; + isDisabledSelector?: boolean; + hideSelector?: boolean; + onSelect?: (value: string) => void; +}; 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 new file mode 100644 index 000000000..e47100c02 --- /dev/null +++ b/packages/mco/components/modals/app-manage-policies/helper/pvc-details-wizard-content.tsx @@ -0,0 +1,297 @@ +import * as React from 'react'; +import { DRPlacementControlModel } from '@odf/mco/models'; +import { createRefFromK8Resource } from '@odf/mco/utils'; +import { MultiSelectDropdown } from '@odf/shared/dropdown/multiselectdropdown'; +import { SingleSelectDropdown } from '@odf/shared/dropdown/singleselectdropdown'; +import { useSafeFetch } from '@odf/shared/hooks/custom-prometheus-poll/safe-fetch-hook'; +import { getName, getNamespace } from '@odf/shared/selectors'; +import { useCustomTranslation } from '@odf/shared/useCustomTranslationHook'; +import { AsyncLoader } from '@odf/shared/utils/AsyncLoader'; +import { + PairElementProps, + NameValueEditorPair, +} from '@odf/shared/utils/NameValueEditor'; +import * as _ from 'lodash-es'; +import { + Alert, + AlertVariant, + Button, + Form, + FormGroup, + SelectOption, + SelectVariant, + Text, +} from '@patternfly/react-core'; +import { MinusCircleIcon } from '@patternfly/react-icons'; +import { queryAppSetResources, safeACMFetch } from '../utils/acm-utils'; +import { AssignPolicyViewState } from '../utils/reducer'; +import { + SearchResult, + PlacementType, + DRPlacementControlType, + DataPolicyType, +} from '../utils/types'; +import '../../../../style.scss'; +import '../style.scss'; + +const getPlacementTags = (drpcs: DRPlacementControlType[]) => + !!drpcs?.length + ? drpcs?.map( + (drpc) => [getName(drpc?.placementInfo), drpc?.pvcSelector] || [] + ) + : [[]]; + +const getLabelsFromSearchResult = (searchResult: SearchResult): string[] => + searchResult?.data?.searchResult?.[0]?.items?.reduce( + (acc, item) => [...acc, ...item?.['label']?.split('; ')], + [] + ); + +const getLabelsFromTags = (tags: TagsType, currIndex: number): string[] => + tags?.reduce((acc, tag, index) => { + const labels = tag?.[1] || []; + return currIndex !== index ? [...acc, ...labels] : acc; + }, []) as string[]; + +const getPlacementsFromTags = (tags: TagsType, currIndex: number): string[] => + tags?.reduce((acc, tag, index) => { + const placement = tag?.[0]; + return currIndex !== index ? [...acc, placement] : acc; + }, []) as string[]; + +const getPlacementDropdownOptions = ( + placementNames: string[], + tags: TagsType, + currIndex: number +) => { + // Display placements except for another index + const selectedNames = getPlacementsFromTags(tags, currIndex); + const shortListedNames = placementNames?.filter( + (name) => !selectedNames.includes(name) + ); + return shortListedNames?.map((name) => ( + + )); +}; + +const getLabelsDropdownOptions = ( + labels: string[], + tags: TagsType, + currIndex: number +) => { + // Display labels except for another index + const selectedLabels = getLabelsFromTags(tags, currIndex); + return labels?.filter((name) => !selectedLabels.includes(name)); +}; + +const createPlacementControlObj = ( + placement: PlacementType, + labels: string[], + dataPolicy: DataPolicyType +): DRPlacementControlType => ({ + apiVersion: DRPlacementControlModel.apiVersion, + kind: DRPlacementControlModel.kind, + metadata: { + name: `${getName(placement)}-drpc`, + namespace: getNamespace(placement), + }, + drPolicyRef: createRefFromK8Resource(dataPolicy), + placementInfo: placement, + pvcSelector: labels || [], +}); + +const generatePlacementControlUsingTags = ( + placements: PlacementType[], + tags: any, + dataPolicy: DataPolicyType +) => + tags?.map(([name, lables]) => { + const placement = placements?.find( + (placement) => getName(placement) === name + ); + return createPlacementControlObj(placement, lables, dataPolicy); + }); + +const NameValueEditorComponent = (props) => ( + + import('@odf/shared/utils/NameValueEditor').then((c) => c.NameValueEditor) + } + {...props} + /> +); + +const PairElement: React.FC = ({ + index, + onRemove: onRemoveProp, + onChange, + isEmpty, + alwaysAllowRemove, + pair, + extraProps, +}) => { + const { t } = useCustomTranslation(); + const { placementNames, labels, tags }: PlacementAndLabelType = extraProps; + const selectedPlacement = pair[NameValueEditorPair.Name]; + const selectedLabels = pair[NameValueEditorPair.Value]; + const deleteIcon = ( + <> + + {t('Delete')} + + ); + + const onChangePlacement = React.useCallback( + (placement: string) => { + onChange( + { target: { value: placement } }, + index, + NameValueEditorPair.Name + ); + }, + [index, onChange] + ); + + const onChangeValue = React.useCallback( + (value: string[]) => { + onChange({ target: { value } }, index, NameValueEditorPair.Value); + }, + [index, onChange] + ); + + const onRemove = React.useCallback(() => { + onRemoveProp(index); + }, [index, onRemoveProp]); + + return ( +
+
+ +
+
+ +
+
+ +
+
+ ); +}; + +export const PVCDetailsWizardContent: React.FC = + ({ state, placements, workloadNamespace, setPolicy }) => { + const { t } = useCustomTranslation(); + const policy = _.cloneDeep(state.policy); + const [tags, setTags] = React.useState( + getPlacementTags(policy?.placementControInfo) + ); + const [searchResult, setSearchResult] = React.useState(); + const safeFetch = useSafeFetch(); + React.useEffect(() => { + safeACMFetch( + safeFetch, + queryAppSetResources(workloadNamespace), + setSearchResult + ); + }, [workloadNamespace, setSearchResult, safeFetch]); + const labels: string[] = React.useMemo( + () => getLabelsFromSearchResult(searchResult) || [], + [searchResult] + ); + const placementNames: string[] = React.useMemo( + () => placements?.map(getName) || [], + [placements] + ); + + return ( +
+ + + {t( + 'Use PVC label selectors to effortlessly specify the application resources that need protection.' + )} + + + + + + + { + setTags(nameValuePairs); + policy.placementControInfo = generatePlacementControlUsingTags( + placements, + nameValuePairs, + policy + ); + setPolicy(policy); + }} + PairElementComponent={PairElement} + nameString={t('Application resource')} + valueString={t('PVC label selector')} + addString={t('Add application resource')} + extraProps={{ + placementNames, + labels, + tags, + }} + /> + +
+ ); + }; + +type TagsType = (string | string[])[][]; + +type PlacementAndLabelType = { + placementNames: string[]; + labels: string[]; + tags: TagsType; +}; + +type PVCDetailsWizardContentProps = { + state: AssignPolicyViewState; + placements: PlacementType[]; + workloadNamespace: string; + setPolicy: (policy: DataPolicyType) => 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 new file mode 100644 index 000000000..5491e5702 --- /dev/null +++ b/packages/mco/components/modals/app-manage-policies/helper/select-policy-wizard-content.tsx @@ -0,0 +1,50 @@ +import * as React from 'react'; +import { SingleSelectDropdown } from '@odf/shared/dropdown/singleselectdropdown'; +import { getName } from '@odf/shared/selectors'; +import { useCustomTranslation } from '@odf/shared/useCustomTranslationHook'; +import { Form, FormGroup, SelectOption } from '@patternfly/react-core'; +import { AssignPolicyViewState } from '../utils/reducer'; +import { DRPolicyType, DataPolicyType } from '../utils/types'; + +const getDropdownOptions = (dataPolicies: DRPolicyType[]) => + dataPolicies?.map((policy) => ( + + )); + +const findPolicy = (name: string, dataPolicies: DRPolicyType[]) => + dataPolicies?.find((policy) => getName(policy) === name); + +export const SelectPolicyWizardContent: React.FC = + ({ state, dataPolicies, setPolicy }) => { + const { t } = useCustomTranslation(); + return ( +
+ e.stopPropagation()} + onClick={(e) => e.stopPropagation()} + > + { + if (getName(state.policy) !== value) { + setPolicy(findPolicy(value, dataPolicies)); + } + }} + /> + +
+ ); + }; + +type SelectPolicyWizardContentProps = { + state: AssignPolicyViewState; + dataPolicies: DRPolicyType[]; + setPolicy: (policy: DataPolicyType) => void; +}; 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..9edf10a46 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 @@ -1,5 +1,5 @@ import * as React from 'react'; -import { DRPC_STATUS, PLACEMENT_REF_LABEL } from '@odf/mco/constants'; +import { PLACEMENT_REF_LABEL } from '@odf/mco/constants'; import { DisasterRecoveryResourceKind, getDRClusterResourceObj, @@ -10,116 +10,29 @@ import { useArgoApplicationSetResourceWatch, useDisasterRecoveryResourceWatch, } from '@odf/mco/hooks'; +import { ArgoApplicationSetKind } from '@odf/mco/types'; import { - ACMPlacementDecisionKind, - ACMPlacementKind, - ArgoApplicationSetKind, - DRClusterKind, - DRPlacementControlKind, - DRPolicyKind, -} from '@odf/mco/types'; -import { - getClustersFromPlacementDecision, - findDRType, findPlacementNameFromAppSet, getRemoteNamespaceFromAppSet, - isDRPolicyValidated, } from '@odf/mco/utils'; import { getNamespace } from '@odf/shared/selectors'; import { useCustomTranslation } from '@odf/shared/useCustomTranslationHook'; -import { TFunction } from 'i18next'; import * as _ from 'lodash-es'; import { AppManagePoliciesModal } from '../app-manage-policies-modal'; +import { + generateApplicationInfo, + generateDRPlacementControlInfo, + generateDRPolicyInfo, + generatePlacementInfo, + getMatchingDRPolicies, +} from '../utils/common'; import { ApplicationType, DRPlacementControlType, DRPolicyType, - PlacementType, + DataPolicyType, } from '../utils/types'; -const getCurrentActivity = (currentStatus: string, t: TFunction) => { - let status = ''; - if (currentStatus === DRPC_STATUS.Relocating) { - status = t('Relocate in progress'); - } else if (currentStatus === DRPC_STATUS.FailingOver) { - status = t('Failover in progress'); - } - return status; -}; - -const generateDRPolicyInfo = ( - t: TFunction, - drPolicy: DRPolicyKind, - drClusters: DRClusterKind[], - drpcInfo?: DRPlacementControlType[] -): DRPolicyType[] => - !_.isEmpty(drPolicy) - ? [ - { - apiVersion: drPolicy.apiVersion, - kind: drPolicy.kind, - metadata: drPolicy.metadata, - // TODO: For multiple DRPC find least recently created - assignedOn: drpcInfo?.[0]?.metadata?.creationTimestamp, - // TODO: For multiple DRPC summarize the activity - activity: getCurrentActivity(drpcInfo?.[0]?.status, t), - isValidated: isDRPolicyValidated(drPolicy), - schedulingInterval: drPolicy.spec.schedulingInterval, - replicationType: findDRType(drClusters), - drClusters: drPolicy.spec.drClusters, - placementControInfo: drpcInfo, - }, - ] - : []; - -const generatePlacementInfo = ( - placement: ACMPlacementKind, - placementDecision: ACMPlacementDecisionKind -): PlacementType => - !_.isEmpty(placement) - ? { - apiVersion: placement.apiVersion, - kind: placement.kind, - metadata: placement.metadata, - deploymentClusters: getClustersFromPlacementDecision(placementDecision), - } - : {}; - -const generateDRPlacementControlInfo = ( - drpc: DRPlacementControlKind, - plsInfo: PlacementType -): DRPlacementControlType[] => - !_.isEmpty(drpc) - ? [ - { - apiVersion: drpc.apiVersion, - kind: drpc.kind, - metadata: drpc.metadata, - drPolicyRef: drpc.spec.drPolicyRef, - placementInfo: plsInfo, - pvcSelector: drpc.spec?.pvcSelector, - lastGroupSyncTime: drpc?.status?.lastGroupSyncTime, - status: drpc?.status?.phase, - }, - ] - : []; - -const generateApplicationInfo = ( - application: ArgoApplicationSetKind, - plsInfo: PlacementType[], - drPolicyInfo: DRPolicyType[] -): ApplicationType => - !_.isEmpty(application) - ? { - apiVersion: application.apiVersion, - kind: application.kind, - metadata: application.metadata, - workloadNamespace: getRemoteNamespaceFromAppSet(application), - placements: plsInfo, - dataPolicies: drPolicyInfo, - } - : {}; - const getDRResources = (namespace: string) => ({ resources: { drPolicies: getDRPolicyResourceObj(), @@ -188,6 +101,7 @@ export const ApplicationSetParser: React.FC = ({ ) ); const appSetResource = appSetResources?.formattedResources?.[0]; + const formattedDRResources = drResources?.formattedResources; const applicationInfo: ApplicationType = React.useMemo(() => { let applicationInfo: ApplicationType = {}; @@ -215,6 +129,7 @@ export const ApplicationSetParser: React.FC = ({ ); applicationInfo = generateApplicationInfo( appSetResource?.application, + getRemoteNamespaceFromAppSet(appSetResource?.application), // Skip placement if it already DR protected _.isEmpty(drpcInfo) ? [placementInfo] : [], drPolicyInfo @@ -223,9 +138,15 @@ export const ApplicationSetParser: React.FC = ({ return applicationInfo; }, [appSetResource, loaded, loadError, t]); + const dataPolicies: DataPolicyType[] = React.useMemo( + () => getMatchingDRPolicies(applicationInfo, formattedDRResources, t), + [applicationInfo, formattedDRResources, t] + ); + return ( = ({ 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..5cb4d1cdb 100644 --- a/packages/mco/components/modals/app-manage-policies/style.scss +++ b/packages/mco/components/modals/app-manage-policies/style.scss @@ -11,4 +11,10 @@ justify-content: space-between; padding: 0 var(--pf-global--spacer--md) 0 var(--pf-global--spacer--md); } + &__form--width { + width: 50%; + } + &__alert--margin-bottom{ + margin-bottom: var(--pf-global--spacer--xs) !important; + } } diff --git a/packages/mco/components/modals/app-manage-policies/utils/acm-utils.ts b/packages/mco/components/modals/app-manage-policies/utils/acm-utils.ts new file mode 100644 index 000000000..252c80d64 --- /dev/null +++ b/packages/mco/components/modals/app-manage-policies/utils/acm-utils.ts @@ -0,0 +1,62 @@ +import { ACM_SEARCH_PROXY_URL, HUB_CLUSTER_NAME } from '@odf/mco/constants'; +import { SafeFetchProps } from '@odf/shared/hooks/custom-prometheus-poll/safe-fetch-hook'; +import { SearchQuery, SearchResult } from './types'; + +export const apiSearchUrl = '/proxy/search'; +export const searchFilterQuery = + 'query searchResult($input: [SearchInput]) {\n searchResult: search(input: $input) {\n items\n }\n}'; + +export const queryAppSetResources = ( + workloadNamespace: string +): SearchQuery => ({ + operationName: 'searchResult', + variables: { + input: [ + { + filters: [ + { + property: 'kind', + values: ['persistentvolumeclaim'], + }, + { + property: 'namespace', + values: [workloadNamespace], + }, + ], + limit: 100, + }, + ], + }, + query: searchFilterQuery, +}); + +export const safeACMFetch = ( + safeFetch: (props: SafeFetchProps) => Promise, + searchQuery: SearchQuery, + setSearchResult: (data: SearchResult) => void, + maxRetry: number = 3 +) => { + const fetchResources = async () => { + const res = await safeFetch({ + url: ACM_SEARCH_PROXY_URL, + method: 'post', + cluster: HUB_CLUSTER_NAME, + options: { + headers: { + 'Content-type': 'application/json', + }, + body: JSON.stringify(searchQuery), + }, + }); + setSearchResult(res); + }; + try { + fetchResources(); + } catch { + maxRetry = maxRetry - 1; + if (maxRetry <= 0) { + // retry + fetchResources(); + } + } +}; diff --git a/packages/mco/components/modals/app-manage-policies/utils/common.ts b/packages/mco/components/modals/app-manage-policies/utils/common.ts new file mode 100644 index 000000000..9c4f62626 --- /dev/null +++ b/packages/mco/components/modals/app-manage-policies/utils/common.ts @@ -0,0 +1,137 @@ +import { DRPC_STATUS } from '@odf/mco/constants'; +import { DisasterRecoveryFormatted } from '@odf/mco/hooks'; +import { + ACMApplicationKind, + ACMPlacementDecisionKind, + ACMPlacementKind, + DRClusterKind, + DRPlacementControlKind, + DRPolicyKind, +} from '@odf/mco/types'; +import { + getClustersFromPlacementDecision, + findDRType, + isDRPolicyValidated, + filterDRResourcesUsingClusterNames, +} from '@odf/mco/utils'; +import { arrayify } from '@odf/shared/modals/EditLabelModal'; +import { TFunction } from 'i18next'; +import * as _ from 'lodash-es'; +import { + ApplicationType, + DRPlacementControlType, + DRPolicyType, + PlacementType, +} from './types'; + +export const getCurrentActivity = (currentStatus: string, t: TFunction) => { + let status = ''; + if (currentStatus === DRPC_STATUS.Relocating) { + status = t('Relocate in progress'); + } else if (currentStatus === DRPC_STATUS.FailingOver) { + status = t('Failover in progress'); + } + return status; +}; + +export const generateDRPolicyInfo = ( + t: TFunction, + drPolicy: DRPolicyKind, + drClusters: DRClusterKind[], + drpcInfo?: DRPlacementControlType[] +): DRPolicyType[] => + !_.isEmpty(drPolicy) + ? [ + { + apiVersion: drPolicy.apiVersion, + kind: drPolicy.kind, + metadata: drPolicy.metadata, + // TODO: For multiple DRPC find least recently created + assignedOn: drpcInfo?.[0]?.metadata?.creationTimestamp, + // TODO: For multiple DRPC summarize the activity + activity: getCurrentActivity(drpcInfo?.[0]?.status, t), + isValidated: isDRPolicyValidated(drPolicy), + schedulingInterval: drPolicy.spec.schedulingInterval, + replicationType: findDRType(drClusters), + drClusters: drPolicy.spec.drClusters, + placementControInfo: drpcInfo, + }, + ] + : []; + +export const generatePlacementInfo = ( + placement: ACMPlacementKind, + placementDecision: ACMPlacementDecisionKind +): PlacementType => + !_.isEmpty(placement) + ? { + apiVersion: placement.apiVersion, + kind: placement.kind, + metadata: placement.metadata, + deploymentClusters: getClustersFromPlacementDecision(placementDecision), + } + : {}; + +export const generateDRPlacementControlInfo = ( + drpc: DRPlacementControlKind, + plsInfo: PlacementType +): DRPlacementControlType[] => + !_.isEmpty(drpc) + ? [ + { + apiVersion: drpc.apiVersion, + kind: drpc.kind, + metadata: drpc.metadata, + drPolicyRef: drpc.spec.drPolicyRef, + placementInfo: plsInfo, + pvcSelector: arrayify(drpc?.spec?.pvcSelector?.matchLabels) || [], + lastGroupSyncTime: drpc?.status?.lastGroupSyncTime, + status: drpc?.status?.phase, + }, + ] + : []; + +export const generateApplicationInfo = ( + application: ACMApplicationKind, + workloadNamespace: string, + plsInfo: PlacementType[], + drPolicyInfo: DRPolicyType[] +): ApplicationType => + !_.isEmpty(application) + ? { + apiVersion: application.apiVersion, + kind: application.kind, + metadata: application.metadata, + workloadNamespace: workloadNamespace, + placements: plsInfo, + dataPolicies: drPolicyInfo, + } + : {}; + +export const getClusterNamesFromPlacements = (placements: PlacementType[]) => + placements?.reduce( + (acc, placemnt) => [...acc, ...placemnt?.deploymentClusters], + [] + ); + +export const getMatchingDRPolicies = ( + appInfo: ApplicationType, + formattedDRResources: DisasterRecoveryFormatted[], + t +) => { + const deploymentClusters: string[] = getClusterNamesFromPlacements( + appInfo?.placements + ); + // Filter all matching policies for the applications + const resources = filterDRResourcesUsingClusterNames( + formattedDRResources, + deploymentClusters + ); + return resources?.reduce( + (acc, resource) => [ + ...acc, + ...generateDRPolicyInfo(t, resource?.drPolicy, resource?.drClusters), + ], + [] + ); +}; 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..6be62c9ce 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 @@ -3,10 +3,21 @@ import { HUB_CLUSTER_NAME, PROTECTED_APP_ANNOTATION_WO_SLASH, } from '@odf/mco/constants'; -import { getName, getNamespace } from '@odf/shared/selectors'; +import { getDRPCKindObj } from '@odf/mco/utils'; +import { + getAPIVersion, + getAnnotations, + getName, + getNamespace, +} from '@odf/shared/selectors'; import { K8sResourceKind } from '@odf/shared/types'; -import { k8sDelete, k8sPatch } from '@openshift-console/dynamic-plugin-sdk'; -import { DRPlacementControlType } from './types'; +import { + k8sDelete, + k8sPatch, + k8sCreate, +} from '@openshift-console/dynamic-plugin-sdk'; +import * as _ from 'lodash-es'; +import { DRPlacementControlType, DataPolicyType } from './types'; export const unAssignPromises = (drpcs: DRPlacementControlType[]) => { const promises: Promise[] = []; @@ -50,3 +61,58 @@ export const unAssignPromises = (drpcs: DRPlacementControlType[]) => { return promises; }; + +const placementAssignPromise = (drpc: DRPlacementControlType) => { + const patch = []; + if (_.isEmpty(getAnnotations(drpc?.placementInfo))) { + patch.push({ + op: 'add', + path: `/metadata/annotations`, + value: {}, + }); + } + patch.push({ + op: 'add', + path: `/metadata/annotations/${PROTECTED_APP_ANNOTATION_WO_SLASH}`, + value: 'true', + }); + return k8sPatch({ + model: ACMPlacementModel, + resource: { + metadata: { + name: getName(drpc?.placementInfo), + namespace: getNamespace(drpc?.placementInfo), + }, + }, + data: patch, + cluster: HUB_CLUSTER_NAME, + }); +}; + +export const assignPromises = (dataPolicy: DataPolicyType) => { + const promises: Promise[] = []; + const drpcs: DRPlacementControlType[] = dataPolicy.placementControInfo; + drpcs?.forEach((drpc) => { + if (drpc?.placementInfo?.kind === ACMPlacementModel.kind) { + promises.push(placementAssignPromise(drpc)); + } + promises.push( + k8sCreate({ + model: DRPlacementControlModel, + data: getDRPCKindObj( + getName(drpc.placementInfo), + getNamespace(drpc.placementInfo), + drpc.placementInfo.kind, + getAPIVersion(drpc.placementInfo), + getName(dataPolicy), + dataPolicy.drClusters, + drpc.placementInfo?.deploymentClusters, + drpc?.pvcSelector + ), + cluster: HUB_CLUSTER_NAME, + }) + ); + }); + + return promises; +}; 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..4587cf2a0 100644 --- a/packages/mco/components/modals/app-manage-policies/utils/reducer.ts +++ b/packages/mco/components/modals/app-manage-policies/utils/reducer.ts @@ -15,6 +15,8 @@ export enum ModalActionContext { UN_ASSIGN_POLICY_SUCCEEDED = 'UN_ASSIGN_POLICY_SUCCEEDED', UN_ASSIGN_POLICIES_FAILED = 'UN_ASSIGN_POLICIES_FAILED', UN_ASSIGN_POLICY_FAILED = 'UN_ASSIGN_POLICY_FAILED', + ASSIGN_POLICY_SUCCEEDED = 'ASSIGN_POLICY_SUCCEEDED', + ASSIGN_POLICY_FAILED = 'ASSIGN_POLICY_FAILED', } export type MessageType = { @@ -40,11 +42,16 @@ export type UnAssignPolicyViewState = commonViewState & { policy: DataPolicyType; }; +export type AssignPolicyViewState = commonViewState & { + policy: DataPolicyType; +}; + export type ManagePolicyState = { modalViewContext: ModalViewContext; [ModalViewContext.POLICY_LIST_VIEW]: PolicyListViewState; [ModalViewContext.UNASSIGN_POLICY_VIEW]: UnAssignPolicyViewState; [ModalViewContext.POLICY_CONFIGURATON_VIEW]: PolicyConfigViewState; + [ModalViewContext.ASSIGN_POLICY_VIEW]: AssignPolicyViewState; }; export enum ManagePolicyStateType { @@ -74,6 +81,13 @@ export const initialPolicyState: ManagePolicyState = { [ModalViewContext.POLICY_CONFIGURATON_VIEW]: { policy: {}, }, + [ModalViewContext.ASSIGN_POLICY_VIEW]: { + policy: {}, + modalActionContext: null, + message: { + title: '', + }, + }, }; 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..2ca03cfce 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,11 +11,9 @@ export type PlacementType = K8sResourceCommon & { export type DRPlacementControlType = K8sResourceCommon & { drPolicyRef: ObjectReference; placementInfo: PlacementType; - pvcSelector?: { - matchLabels: MatchLabels; - }; - lastGroupSyncTime: string; - status: string; + pvcSelector?: string[]; + lastGroupSyncTime?: string; + status?: string; }; export type PolicyType = K8sResourceCommon & { @@ -39,3 +36,28 @@ export type ApplicationType = K8sResourceCommon & { placements?: PlacementType[]; dataPolicies?: DataPolicyType[]; }; + +export type SearchQuery = { + operationName: string; + variables: { + input: { + filters: { property: string; values: string[] | string }[]; + relatedKinds?: string[]; + limit?: number; + }[]; + }; + query: string; +}; + +export type SearchResult = { + data: { + searchResult: { + items?: any; + count?: number; + related?: { + count: number; + kind: string; + }[]; + }[]; + }; +}; diff --git a/packages/mco/components/modals/drpolicy-apps-apply/utils.tsx b/packages/mco/components/modals/drpolicy-apps-apply/utils.tsx index 7463d96a4..981f9bb5e 100644 --- a/packages/mco/components/modals/drpolicy-apps-apply/utils.tsx +++ b/packages/mco/components/modals/drpolicy-apps-apply/utils.tsx @@ -1,3 +1,4 @@ +import { getDRPCKindObj } from '@odf/mco/utils'; import { objectify } from '@odf/shared/modals/EditLabelModal'; import { K8sResourceKind } from '@odf/shared/types'; import { getAPIVersionForModel } from '@odf/shared/utils'; @@ -10,50 +11,8 @@ import { HUB_CLUSTER_NAME, PROTECTED_APP_ANNOTATION_WO_SLASH, } from '../../../constants'; -import { - DRPlacementControlModel, - ACMPlacementModel, - DRPolicyModel, -} from '../../../models'; -import { - DRPlacementControlKind, - PlacementToDrpcMap, - PlacementToAppSets, -} from '../../../types'; -import { matchClusters } from '../../../utils'; - -export const getDRPCKindObj = ( - plsName: string, - plsNamespace: string, - drPolicyName: string, - drClusterNames: string[], - decisionClusters: string[], - pvcSelectors: string[] -): DRPlacementControlKind => ({ - apiVersion: getAPIVersionForModel(DRPlacementControlModel), - kind: DRPlacementControlModel.kind, - metadata: { - name: `${plsName}-drpc`, - namespace: plsNamespace, - }, - spec: { - drPolicyRef: { - name: drPolicyName, - apiVersion: getAPIVersionForModel(DRPolicyModel), - kind: DRPolicyModel.kind, - }, - placementRef: { - name: plsName, - namespace: plsNamespace, - apiVersion: getAPIVersionForModel(ACMPlacementModel), - kind: ACMPlacementModel.kind, - }, - preferredCluster: matchClusters(drClusterNames, decisionClusters), - pvcSelector: { - matchLabels: objectify(pvcSelectors), - }, - }, -}); +import { DRPlacementControlModel, ACMPlacementModel } from '../../../models'; +import { PlacementToDrpcMap, PlacementToAppSets } from '../../../types'; export const areLabelsDifferent = ( existingLabels: string[], @@ -158,6 +117,8 @@ export const getProtectedPanelPromises = ( data: getDRPCKindObj( protectedResource.placement, protectedResource.namespace, + ACMPlacementModel.kind, + getAPIVersionForModel(ACMPlacementModel), drPolicyName, drClusterNames, protectedResource.decisionClusters, diff --git a/packages/mco/constants/common.ts b/packages/mco/constants/common.ts index 6ee68265a..cf6fb0b41 100644 --- a/packages/mco/constants/common.ts +++ b/packages/mco/constants/common.ts @@ -14,3 +14,5 @@ export const enum VOLUME_REPLICATION_HEALTH { export const ACM_OBSERVABILITY_FLAG = 'ACM_OBSERVABILITY'; export const ADMIN_FLAG = 'ADMIN'; export const ODFMCO_OPERATOR_NAMESPACE = 'openshift-operators'; +export const ACM_SEARCH_PROXY_URL = + '/api/proxy/plugin/acm/console/multicloud/proxy/search'; diff --git a/packages/mco/utils/disaster-recovery.tsx b/packages/mco/utils/disaster-recovery.tsx index ecad488a4..50b3dd2be 100644 --- a/packages/mco/utils/disaster-recovery.tsx +++ b/packages/mco/utils/disaster-recovery.tsx @@ -4,6 +4,7 @@ import { hoursToSeconds, minutesToSeconds, } from '@odf/shared/details-page/datetime'; +import { objectify } from '@odf/shared/modals/EditLabelModal'; import { getLabel, hasLabel, @@ -12,6 +13,7 @@ import { getAnnotations, } from '@odf/shared/selectors'; import { ApplicationKind } from '@odf/shared/types/k8s'; +import { getAPIVersionForModel } from '@odf/shared/utils'; import { K8sResourceCommon, GreenCheckCircleIcon, @@ -41,6 +43,8 @@ import { DisasterRecoveryFormatted, ApplicationRefKind } from '../hooks'; import { ACMPlacementModel, ACMPlacementRuleModel, + DRPlacementControlModel, + DRPolicyModel, DRVolumeReplicationGroup, } from '../models'; import { @@ -531,3 +535,48 @@ export const isDRPolicyValidated = (drPolicy: DRPolicyKind) => (condition) => condition?.type === 'Validated' && condition?.status === 'True' ); + +export const getDRPCKindObj = ( + plsName: string, + plsNamespace: string, + plsKind: string, + plsApiVersion: string, + drPolicyName: string, + drClusterNames: string[], + decisionClusters: string[], + pvcSelectors: string[] +): DRPlacementControlKind => ({ + apiVersion: getAPIVersionForModel(DRPlacementControlModel), + kind: DRPlacementControlModel.kind, + metadata: { + name: `${plsName}-drpc`, + namespace: plsNamespace, + }, + spec: { + drPolicyRef: { + name: drPolicyName, + apiVersion: getAPIVersionForModel(DRPolicyModel), + kind: DRPolicyModel.kind, + }, + placementRef: { + name: plsName, + namespace: plsNamespace, + apiVersion: plsApiVersion, + kind: plsKind, + }, + preferredCluster: matchClusters(drClusterNames, decisionClusters), + pvcSelector: { + matchLabels: objectify(pvcSelectors), + }, + }, +}); + +export const filterDRResourcesUsingClusterNames = ( + drResources: DisasterRecoveryFormatted[], + clusterNames: string[] +): DisasterRecoveryFormatted[] => + drResources?.filter((drResource) => + drResource?.drPolicy?.spec?.drClusters?.find((name) => + clusterNames?.includes(name) + ) + ); diff --git a/packages/odf/components/create-storage-system/create-storage-system-steps/create-local-volume-set-step/body.tsx b/packages/odf/components/create-storage-system/create-storage-system-steps/create-local-volume-set-step/body.tsx index e775e566f..c5aa8f5df 100644 --- a/packages/odf/components/create-storage-system/create-storage-system-steps/create-local-volume-set-step/body.tsx +++ b/packages/odf/components/create-storage-system/create-storage-system-steps/create-local-volume-set-step/body.tsx @@ -329,6 +329,7 @@ export const LocalVolumeSetBody: React.FC = ({ > = ({ onChange={(selectedValues: string[]) => formHandler('deviceType', selectedValues) } - defaultSelected={[ - deviceTypeDropdownItems.DISK, - deviceTypeDropdownItems.PART, - ]} /> = ({ placeholder, id, options, - defaultSelected = [], + selected = [], + variant, }) => { const [isOpen, setOpen] = React.useState(false); - const [selected, setSelected] = React.useState( - defaultSelected || [] - ); const onSelect = ( - event: React.MouseEvent | React.ChangeEvent, + _event: React.MouseEvent | React.ChangeEvent, selection: string ) => { let cSelected: string[] = selected; @@ -23,7 +21,6 @@ export const MultiSelectDropdown: React.FC = ({ } else { cSelected = [...selected, selection]; } - setSelected(cSelected); onChange(cSelected); }; @@ -35,7 +32,7 @@ export const MultiSelectDropdown: React.FC = ({ return ( @@ -52,8 +50,9 @@ export const MultiSelectDropdown: React.FC = ({ export type MultiSelectDropdownProps = { id?: string; + selected: string[]; options?: string[]; - defaultSelected?: string[]; placeholder?: string; + variant?: SelectVariant; onChange: (selected: string[]) => void; }; 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/hooks/custom-prometheus-poll/safe-fetch-hook.ts b/packages/shared/hooks/custom-prometheus-poll/safe-fetch-hook.ts index 93c90595e..dc440f855 100644 --- a/packages/shared/hooks/custom-prometheus-poll/safe-fetch-hook.ts +++ b/packages/shared/hooks/custom-prometheus-poll/safe-fetch-hook.ts @@ -1,7 +1,7 @@ import { useEffect, useRef } from 'react'; import { consoleFetchJSON } from '@openshift-console/dynamic-plugin-sdk'; -type SafeFetchProps = { +export type SafeFetchProps = { url: string; method?: string; options?: RequestInit; 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; +}; diff --git a/packages/shared/utils/NameValueEditor.tsx b/packages/shared/utils/NameValueEditor.tsx index 530002039..72ce37285 100644 --- a/packages/shared/utils/NameValueEditor.tsx +++ b/packages/shared/utils/NameValueEditor.tsx @@ -36,7 +36,9 @@ type NameValueEditorProps = { secrets: {}; addConfigMapSecret: boolean; toolTip: string; + PairElementComponent: React.FC; onLastItemRemoved: () => void; + extraProps?: any; }; export const enum NameValueEditorPair { @@ -45,7 +47,7 @@ export const enum NameValueEditorPair { Index, } -type PairElementProps = { +export type PairElementProps = { nameString: string; valueString: string; readOnly?: boolean; @@ -63,6 +65,7 @@ type PairElementProps = { onRemove?: any; isEmpty: boolean; disableReorder: boolean; + extraProps?: any; }; const PairElement: React.FC = ({ @@ -190,6 +193,8 @@ export const NameValueEditor: React.FC = toolTip, nameString, valueString, + extraProps, + PairElementComponent = PairElement, }) => { const { t } = useCustomTranslation(); @@ -263,7 +268,7 @@ export const NameValueEditor: React.FC = nameValuePairs.length === 1 && nameValuePairs[0].every((value) => !value); return ( - = isEmpty={isEmpty} disableReorder={nameValuePairs.length === 1} toolTip={toolTip} + extraProps={extraProps} /> ); });