({
+ groupVersionKind: HookModelGroupVersionKind,
+ namespaced: true,
+ isList: true,
+ namespace: plan.metadata?.namespace,
+ });
+
+ if (!hooksLoaded)
+ return (
+
+ {t('Hooks data is loading, please wait.')}
+
+ );
+
+ if (hooksLoadError)
+ return (
+
+
+ {t(
+ 'Something is wrong, the hooks data was not loaded due to an error, please try to reload the page.',
+ )}
+
+
+ );
+
+ // Search for the Plan k8s Hooks
+ const planHooks =
+ hooks?.filter((hook) =>
+ plan?.spec?.vms?.find((vm) =>
+ vm.hooks?.find(
+ (VMHook) =>
+ VMHook.hook.name === hook.metadata?.name &&
+ VMHook.hook.namespace === hook.metadata?.namespace,
+ ),
+ ),
+ ) || [];
+
+ return ;
+};
diff --git a/packages/forklift-console-plugin/src/modules/Plans/views/details/tabs/Hooks/PlanHooksSection.tsx b/packages/forklift-console-plugin/src/modules/Plans/views/details/tabs/Hooks/PlanHooksSection.tsx
new file mode 100644
index 000000000..899b88ee7
--- /dev/null
+++ b/packages/forklift-console-plugin/src/modules/Plans/views/details/tabs/Hooks/PlanHooksSection.tsx
@@ -0,0 +1,274 @@
+import React, { ReactNode, useReducer } from 'react';
+import { Light as SyntaxHighlighter } from 'react-syntax-highlighter';
+import yaml from 'react-syntax-highlighter/dist/cjs/languages/hljs/yaml';
+import a11yLight from 'react-syntax-highlighter/dist/cjs/styles/hljs/a11y-light';
+import { Base64 } from 'js-base64';
+import { useForkliftTranslation } from 'src/utils/i18n';
+
+import {
+ HookModelGroupVersionKind,
+ V1beta1Hook,
+ V1beta1Plan,
+ V1beta1PlanSpecVms,
+ V1beta1PlanSpecVmsHooks,
+} from '@kubev2v/types';
+import { ResourceLink } from '@openshift-console/dynamic-plugin-sdk';
+import {
+ Alert,
+ Button,
+ Divider,
+ Drawer,
+ Flex,
+ FlexItem,
+ HelperText,
+ HelperTextItem,
+ Popover,
+} from '@patternfly/react-core';
+import Pencil from '@patternfly/react-icons/dist/esm/icons/pencil-alt-icon';
+import { TableComposable, Tbody, Td, Th, Thead, Tr } from '@patternfly/react-table';
+
+import { hasPlanEditable } from '../../utils';
+import { canDeleteAndPatchPlanHooks } from '../../utils/canDeleteAndPatchPlan';
+
+SyntaxHighlighter.registerLanguage('yaml', yaml);
+
+interface planHook {
+ hook: V1beta1Hook;
+ step: 'PreHook' | 'PostHook';
+}
+
+interface PlanHooksSectionState {
+ edit: boolean;
+ dataChanged: boolean;
+ alertMessage: ReactNode;
+}
+
+type PlanHooksSectionProps = {
+ plan: V1beta1Plan;
+ planHooks: V1beta1Hook[];
+};
+
+export const PlanHooksSection: React.FC = ({ plan, planHooks }) => {
+ const { t } = useForkliftTranslation();
+
+ const initialState: PlanHooksSectionState = {
+ edit: false,
+ dataChanged: false,
+ alertMessage: null,
+ };
+
+ const [state, dispatch] = useReducer(reducer, initialState);
+
+ function reducer(
+ state: PlanHooksSectionState,
+ action: { type: string; payload? },
+ ): PlanHooksSectionState {
+ switch (action.type) {
+ case 'TOGGLE_EDIT': {
+ return { ...state, edit: !state.edit };
+ }
+ case 'SET_CANCEL': {
+ const dataChanged = false;
+
+ return {
+ ...state,
+ dataChanged,
+ alertMessage: null,
+ };
+ }
+ case 'SET_ALERT_MESSAGE': {
+ return { ...state, alertMessage: action.payload };
+ }
+ default:
+ return state;
+ }
+ }
+
+ // Toggles between view and edit modes
+ function onToggleEdit() {
+ dispatch({ type: 'TOGGLE_EDIT' });
+ }
+
+ // Handle user clicking "cancel"
+ function onCancel() {
+ // clear changes and return to view mode
+ dispatch({ type: 'SET_CANCEL' });
+ dispatch({ type: 'TOGGLE_EDIT' });
+ }
+
+ const AddVmHookToList = (
+ VmHook: V1beta1PlanSpecVmsHooks,
+ planHooks: V1beta1Hook[],
+ planHooksList: planHook[],
+ ): boolean => {
+ const foundVmHookInPlan = planHooks.find(
+ (hook) =>
+ hook.metadata?.name === VmHook.hook.name &&
+ hook.metadata?.namespace === VmHook.hook.namespace,
+ );
+ const alreadyExistInList = planHooksList.find(
+ (hook) =>
+ hook.hook.metadata?.name === VmHook.hook.name &&
+ hook.hook.metadata?.namespace === VmHook.hook.namespace &&
+ hook.step === VmHook.step,
+ );
+
+ if (!foundVmHookInPlan || alreadyExistInList) return false; // TODO: set an error message to state.alertMessage for this invalid state
+ if (VmHook.step !== 'PreHook' && VmHook.step !== 'PostHook') return false; // TODO: set an error message to state.alertMessage for this invalid state
+
+ planHooksList.push({ hook: foundVmHookInPlan, step: VmHook.step });
+ return true;
+ };
+
+ const getPlanHooksList = (
+ planVms: V1beta1PlanSpecVms[],
+ planHooks: V1beta1Hook[],
+ ): planHook[] => {
+ const planHooksList: planHook[] = [];
+
+ planVms?.filter((vm) =>
+ vm.hooks?.filter((VmHook) => AddVmHookToList(VmHook, planHooks, planHooksList)),
+ );
+ return planHooksList.sort((a, b) => (a.step === 'PreHook' && b.step === 'PostHook' ? -1 : 1));
+ };
+
+ const PlanMappingsSectionEditMode: React.FC = () => {
+ return (
+ <>
+
+ >
+ );
+ };
+
+ const PlanHooksSectionViewMode: React.FC = () => {
+ const { t } = useForkliftTranslation();
+ const DisableEditHooks = !hasPlanEditable(plan);
+
+ return (
+ <>
+
+ {canDeleteAndPatchPlanHooks(plan) && (
+
+ }
+ onClick={onToggleEdit}
+ isDisabled={DisableEditHooks}
+ >
+ {t('Edit hooks')}
+
+
+
+ {DisableEditHooks
+ ? t(
+ 'The edit hooks button is disabled if the plan started running and at least one virtual machine was migrated successfully.',
+ )
+ : t(
+ 'Adding hooks to the plan is optional. Hooks are contained in Ansible playbooks that can be run before or after the migration.',
+ )}
+
+
+
+
+ )}
+ {planHooks.length === 0 ? (
+
+
+ {t('No hooks have been added to this migration plan.')}
+
+
+ ) : (
+
+
+
+ Hook |
+ Migration step |
+ Type |
+ Definition |
+
+
+
+ {getPlanHooksList(plan?.spec?.vms, planHooks).map((planHook, i) => (
+
+
+
+ |
+
+ {planHook.step === 'PreHook' ? t(`Pre-migration`) : t(`Post-migration`)}
+ |
+
+
+ {planHook.hook.spec?.playbook ? 'Ansible playbook' : 'Custom container image'}
+ |
+
+
+ {planHook.hook.spec?.playbook ? (
+
+ {Base64.decode(planHook.hook.spec.playbook)}
+
+ }
+ >
+
+
+ ) : (
+ planHook.hook.spec?.image
+ )}
+ |
+
+ ))}
+
+
+ )}
+
+ >
+ );
+ };
+
+ return state.edit ? (
+ // Edit mode
+ <>
+
+
+
+
+
+
+
+ {t('Click the relevant buttons within the table for managing the plan hooks.')}
+
+
+
+ {state.alertMessage ? (
+ <>
+
+ {state.alertMessage?.toString()}
+
+ >
+ ) : null}
+
+ >
+ ) : (
+ // View mode
+ <>
+
+ >
+ );
+};
diff --git a/packages/forklift-console-plugin/src/modules/Plans/views/details/tabs/Hooks/index.ts b/packages/forklift-console-plugin/src/modules/Plans/views/details/tabs/Hooks/index.ts
index 5fb2ed3c8..27e4d6e96 100644
--- a/packages/forklift-console-plugin/src/modules/Plans/views/details/tabs/Hooks/index.ts
+++ b/packages/forklift-console-plugin/src/modules/Plans/views/details/tabs/Hooks/index.ts
@@ -1,3 +1,4 @@
// @index(['./*', /style/g], f => `export * from '${f.path}';`)
export * from './PlanHooks';
+export * from './PlanHooksSection';
// @endindex
diff --git a/packages/forklift-console-plugin/src/modules/Plans/views/details/tabs/Mappings/PlanMappingsSection.tsx b/packages/forklift-console-plugin/src/modules/Plans/views/details/tabs/Mappings/PlanMappingsSection.tsx
index e8bb3f9a8..2961dde39 100644
--- a/packages/forklift-console-plugin/src/modules/Plans/views/details/tabs/Mappings/PlanMappingsSection.tsx
+++ b/packages/forklift-console-plugin/src/modules/Plans/views/details/tabs/Mappings/PlanMappingsSection.tsx
@@ -34,9 +34,9 @@ import Pencil from '@patternfly/react-icons/dist/esm/icons/pencil-alt-icon';
import { Mapping, MappingList } from '../../components';
import {
- canDeleteAndPatchPlanMaps,
+ canDeleteAndPatchPlanHooks,
+ hasPlanEditable,
hasPlanMappingsChanged,
- hasPlanMappingsEditable,
mapSourceNetworksIdsToLabels,
mapSourceStoragesIdsToLabels,
mapTargetNetworksIdsToLabels,
@@ -554,12 +554,12 @@ export const PlanMappingsSection: React.FC = ({
const PlanMappingsSectionViewMode: React.FC = () => {
const { t } = useForkliftTranslation();
- const DisableEditMappings = !hasPlanMappingsEditable(plan);
+ const DisableEditMappings = !hasPlanEditable(plan);
return (
<>
- {canDeleteAndPatchPlanMaps(plan) && (
+ {canDeleteAndPatchPlanHooks(plan) && (
{DisableEditMappings ? (
-
+
{t(
'The edit mappings button is disabled if the plan started running and at least one virtual machine was migrated successfully.',
@@ -654,7 +654,7 @@ export const PlanMappingsSection: React.FC = ({
-
+
{t(
'Click the update mappings button to save your changes, button is disabled until a change is detected.',
diff --git a/packages/forklift-console-plugin/src/modules/Plans/views/details/utils/canDeleteAndPatchPlanMaps.tsx b/packages/forklift-console-plugin/src/modules/Plans/views/details/utils/canDeleteAndPatchPlan.tsx
similarity index 64%
rename from packages/forklift-console-plugin/src/modules/Plans/views/details/utils/canDeleteAndPatchPlanMaps.tsx
rename to packages/forklift-console-plugin/src/modules/Plans/views/details/utils/canDeleteAndPatchPlan.tsx
index 4f8761d36..d976f4775 100644
--- a/packages/forklift-console-plugin/src/modules/Plans/views/details/utils/canDeleteAndPatchPlanMaps.tsx
+++ b/packages/forklift-console-plugin/src/modules/Plans/views/details/utils/canDeleteAndPatchPlan.tsx
@@ -1,4 +1,4 @@
-import { NetworkMapModel, StorageMapModel, V1beta1Plan } from '@kubev2v/types';
+import { HookModel, NetworkMapModel, StorageMapModel, V1beta1Plan } from '@kubev2v/types';
import { useAccessReview } from '@openshift-console/dynamic-plugin-sdk';
export const canDeleteAndPatchPlanMaps = (plan: V1beta1Plan) => {
@@ -36,3 +36,23 @@ export const canDeleteAndPatchPlanMaps = (plan: V1beta1Plan) => {
return canPatchNetworkMap && canDeleteNetworkMap && canPatchStorageMap && canDeleteStorageMap;
};
+
+export const canDeleteAndPatchPlanHooks = (plan: V1beta1Plan) => {
+ const [canDeleteHooks] = useAccessReview({
+ group: '',
+ resource: HookModel.plural,
+ verb: 'delete',
+ name: plan.metadata?.name,
+ namespace: plan.metadata?.name,
+ });
+
+ const [canPatchHooks] = useAccessReview({
+ group: '',
+ resource: HookModel.plural,
+ verb: 'patch',
+ name: plan.metadata?.name,
+ namespace: plan.metadata?.name,
+ });
+
+ return canDeleteHooks && canPatchHooks;
+};
diff --git a/packages/forklift-console-plugin/src/modules/Plans/views/details/utils/hasPlanMappingsEditable.ts b/packages/forklift-console-plugin/src/modules/Plans/views/details/utils/hasPlanEditable.ts
similarity index 87%
rename from packages/forklift-console-plugin/src/modules/Plans/views/details/utils/hasPlanMappingsEditable.ts
rename to packages/forklift-console-plugin/src/modules/Plans/views/details/utils/hasPlanEditable.ts
index 2378e4649..ef1b85dc4 100644
--- a/packages/forklift-console-plugin/src/modules/Plans/views/details/utils/hasPlanMappingsEditable.ts
+++ b/packages/forklift-console-plugin/src/modules/Plans/views/details/utils/hasPlanEditable.ts
@@ -1,6 +1,6 @@
import { V1beta1Plan } from '@kubev2v/types';
-export const hasPlanMappingsEditable = (plan: V1beta1Plan) => {
+export const hasPlanEditable = (plan: V1beta1Plan) => {
const planHasNeverStarted = !plan.status?.migration?.started ? true : false;
const migrationHasSomeCompleteRunningVMs =
diff --git a/packages/forklift-console-plugin/src/modules/Plans/views/details/utils/index.ts b/packages/forklift-console-plugin/src/modules/Plans/views/details/utils/index.ts
index 4d8549862..01f07bcea 100644
--- a/packages/forklift-console-plugin/src/modules/Plans/views/details/utils/index.ts
+++ b/packages/forklift-console-plugin/src/modules/Plans/views/details/utils/index.ts
@@ -1,11 +1,11 @@
// @index('./*.ts', f => `export * from '${f.path}';`)
-export * from './canDeleteAndPatchPlanMaps';
+export * from './canDeleteAndPatchPlan';
export * from './constants';
export * from './getInventoryApiUrl';
export * from './getValueByJsonPath';
export * from './hasObjectChangedInGivenFields';
+export * from './hasPlanEditable';
export * from './hasPlanMappingsChanged';
-export * from './hasPlanMappingsEditable';
export * from './mapMappingsIdsToLabels';
export * from './patchPlanMappingsData';
// @endindex