From 1a8316541360820fdfdee366108e2506dd8a4701 Mon Sep 17 00:00:00 2001 From: Sharon Gratch Date: Sun, 17 Mar 2024 23:23:29 +0200 Subject: [PATCH] Enrich the hooks tab for the plans details page - view mode Reference:https://github.com/kubev2v/forklift-console-plugin/issues/941 Add the Hooks tab for the plans details page - view mode. Signed-off-by: Sharon Gratch --- package-lock.json | 172 +++++++++++ package.json | 3 +- .../en/plugin__forklift-console-plugin.json | 9 + .../views/details/PlanDetailsPage.style.css | 18 +- .../Plans/views/details/PlanDetailsPage.tsx | 16 +- .../views/details/tabs/Hooks/PlanHooks.tsx | 80 ++++- .../details/tabs/Hooks/PlanHooksSection.tsx | 274 ++++++++++++++++++ .../Plans/views/details/tabs/Hooks/index.ts | 1 + .../tabs/Mappings/PlanMappingsSection.tsx | 12 +- ...PlanMaps.tsx => canDeleteAndPatchPlan.tsx} | 22 +- ...MappingsEditable.ts => hasPlanEditable.ts} | 2 +- .../Plans/views/details/utils/index.ts | 4 +- 12 files changed, 577 insertions(+), 36 deletions(-) create mode 100644 packages/forklift-console-plugin/src/modules/Plans/views/details/tabs/Hooks/PlanHooksSection.tsx rename packages/forklift-console-plugin/src/modules/Plans/views/details/utils/{canDeleteAndPatchPlanMaps.tsx => canDeleteAndPatchPlan.tsx} (64%) rename packages/forklift-console-plugin/src/modules/Plans/views/details/utils/{hasPlanMappingsEditable.ts => hasPlanEditable.ts} (87%) diff --git a/package-lock.json b/package-lock.json index 1e3ac4813..f1fa72327 100644 --- a/package-lock.json +++ b/package-lock.json @@ -50,6 +50,7 @@ "jsonc-parser": "^3.2.0", "prettier": "^2.7.1", "prettier-stylelint": "^0.4.2", + "react-syntax-highlighter": "^15.5.0", "rollup-plugin-analyzer": "^4.0.0", "rollup-plugin-import-css": "^3.1.0", "stylelint": "^14.9.1", @@ -8439,6 +8440,15 @@ "@types/node": "*" } }, + "node_modules/@types/hast": { + "version": "2.3.10", + "resolved": "https://registry.npmjs.org/@types/hast/-/hast-2.3.10.tgz", + "integrity": "sha512-McWspRw8xx8J9HurkVBfYj0xKoE25tOFlHGdx4MJ5xORQrMGZNqJhVQWaIbm6Oyla5kYOXtDiopzKRJzEOkwJw==", + "dev": true, + "dependencies": { + "@types/unist": "^2" + } + }, "node_modules/@types/html-minifier-terser": { "version": "6.1.0", "resolved": "https://registry.npmjs.org/@types/html-minifier-terser/-/html-minifier-terser-6.1.0.tgz", @@ -11751,6 +11761,16 @@ "node": ">= 0.8" } }, + "node_modules/comma-separated-tokens": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/comma-separated-tokens/-/comma-separated-tokens-1.0.8.tgz", + "integrity": "sha512-GHuDRO12Sypu2cV70d1dkA2EUmXHgntrzbpvOB+Qy+49ypNfGgFQIC2fhhXbnyrJRynDCAARsT7Ou0M6hirpfw==", + "dev": true, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/commander": { "version": "9.5.0", "resolved": "https://registry.npmjs.org/commander/-/commander-9.5.0.tgz", @@ -14667,6 +14687,19 @@ "reusify": "^1.0.4" } }, + "node_modules/fault": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/fault/-/fault-1.0.4.tgz", + "integrity": "sha512-CJ0HCB5tL5fYTEA7ToAq5+kTwd++Borf1/bifxd9iT70QcXr4MRrO3Llf8Ifs70q+SJcGHFtnIE/Nw6giCtECA==", + "dev": true, + "dependencies": { + "format": "^0.2.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/faye-websocket": { "version": "0.11.4", "resolved": "https://registry.npmjs.org/faye-websocket/-/faye-websocket-0.11.4.tgz", @@ -15162,6 +15195,15 @@ "node": ">= 0.12" } }, + "node_modules/format": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/format/-/format-0.2.2.tgz", + "integrity": "sha512-wzsgA6WOq+09wrU1tsJ09udeR/YZRaeArL9e1wPbFg3GG2yDnC2ldKpxs4xunpFF9DgqCqOIra3bc1HWrJ37Ww==", + "dev": true, + "engines": { + "node": ">=0.4.x" + } + }, "node_modules/forwarded": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", @@ -16117,6 +16159,33 @@ "node": ">= 0.4" } }, + "node_modules/hast-util-parse-selector": { + "version": "2.2.5", + "resolved": "https://registry.npmjs.org/hast-util-parse-selector/-/hast-util-parse-selector-2.2.5.tgz", + "integrity": "sha512-7j6mrk/qqkSehsM92wQjdIgWM2/BW61u/53G6xmC8i1OmEdKLHbk419QKQUjz6LglWsfqoiHmyMRkP1BGjecNQ==", + "dev": true, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hastscript": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/hastscript/-/hastscript-6.0.0.tgz", + "integrity": "sha512-nDM6bvd7lIqDUiYEiu5Sl/+6ReP0BMk/2f4U/Rooccxkj0P5nm+acM5PrGJ/t5I8qPGiqZSE6hVAwZEdZIvP4w==", + "dev": true, + "dependencies": { + "@types/hast": "^2.0.0", + "comma-separated-tokens": "^1.0.0", + "hast-util-parse-selector": "^2.0.0", + "property-information": "^5.0.0", + "space-separated-tokens": "^1.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, "node_modules/he": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz", @@ -16166,6 +16235,15 @@ "integrity": "sha512-Rf4YVNYpKjZ6ASAmibcwTNciQ5Co5Ztq6iZPEykHpkoflnD/K5ryE/rHehFsTm4NJj8nKDhbi3eKBWGogmNnkg==", "dev": true }, + "node_modules/highlight.js": { + "version": "10.7.3", + "resolved": "https://registry.npmjs.org/highlight.js/-/highlight.js-10.7.3.tgz", + "integrity": "sha512-tzcUFauisWKNHaRkN4Wjl/ZA07gENAjFl3J/c480dprkGTg5EQstgaNFqBfUqCq54kZRIEcreTsAgF/m2quD7A==", + "dev": true, + "engines": { + "node": "*" + } + }, "node_modules/history": { "version": "5.3.0", "resolved": "https://registry.npmjs.org/history/-/history-5.3.0.tgz", @@ -20389,6 +20467,20 @@ "node": ">=0.10.0" } }, + "node_modules/lowlight": { + "version": "1.20.0", + "resolved": "https://registry.npmjs.org/lowlight/-/lowlight-1.20.0.tgz", + "integrity": "sha512-8Ktj+prEb1RoCPkEOrPMYUN/nCggB7qAWe3a7OpMjWQkh3l2RD5wKRQ+o8Q8YuI9RG/xs95waaI/E6ym/7NsTw==", + "dev": true, + "dependencies": { + "fault": "^1.0.0", + "highlight.js": "~10.7.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/lru-cache": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", @@ -24130,6 +24222,15 @@ "node": ">= 0.8" } }, + "node_modules/prismjs": { + "version": "1.29.0", + "resolved": "https://registry.npmjs.org/prismjs/-/prismjs-1.29.0.tgz", + "integrity": "sha512-Kx/1w86q/epKcmte75LNrEoT+lX8pBpavuAbvJWRXar7Hz8jrtF+e3vY751p0R8H9HdArwaCTNDDzHg/ScJK1Q==", + "dev": true, + "engines": { + "node": ">=6" + } + }, "node_modules/process": { "version": "0.11.10", "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz", @@ -24205,6 +24306,19 @@ "integrity": "sha512-SVtmxhRE/CGkn3eZY1T6pC8Nln6Fr/lu1mKSgRud0eC73whjGfoAogbn78LkD8aFL0zz3bAFerKSnOl7NlErBA==", "peer": true }, + "node_modules/property-information": { + "version": "5.6.0", + "resolved": "https://registry.npmjs.org/property-information/-/property-information-5.6.0.tgz", + "integrity": "sha512-YUHSPk+A30YPv+0Qf8i9Mbfe/C0hdPXk1s1jPVToV8pk8BQtpw10ct89Eo7OWkutrwqvT0eicAxlOg3dOAu8JA==", + "dev": true, + "dependencies": { + "xtend": "^4.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/proxy-addr": { "version": "2.0.7", "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", @@ -25090,6 +25204,22 @@ } } }, + "node_modules/react-syntax-highlighter": { + "version": "15.5.0", + "resolved": "https://registry.npmjs.org/react-syntax-highlighter/-/react-syntax-highlighter-15.5.0.tgz", + "integrity": "sha512-+zq2myprEnQmH5yw6Gqc8lD55QHnpKaU8TOcFeC/Lg/MQSs8UknEA0JC4nTZGFAXC2J2Hyj/ijJ7NlabyPi2gg==", + "dev": true, + "dependencies": { + "@babel/runtime": "^7.3.1", + "highlight.js": "^10.4.1", + "lowlight": "^1.17.0", + "prismjs": "^1.27.0", + "refractor": "^3.6.0" + }, + "peerDependencies": { + "react": ">= 0.14.0" + } + }, "node_modules/read-pkg": { "version": "5.2.0", "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-5.2.0.tgz", @@ -25285,6 +25415,48 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/refractor": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/refractor/-/refractor-3.6.0.tgz", + "integrity": "sha512-MY9W41IOWxxk31o+YvFCNyNzdkc9M20NoZK5vq6jkv4I/uh2zkWcfudj0Q1fovjUQJrNewS9NMzeTtqPf+n5EA==", + "dev": true, + "dependencies": { + "hastscript": "^6.0.0", + "parse-entities": "^2.0.0", + "prismjs": "~1.27.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/refractor/node_modules/parse-entities": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/parse-entities/-/parse-entities-2.0.0.tgz", + "integrity": "sha512-kkywGpCcRYhqQIchaWqZ875wzpS/bMKhz5HnN3p7wveJTkTtyAB/AlnS0f8DFSqYW1T82t6yEAkEcB+A1I3MbQ==", + "dev": true, + "dependencies": { + "character-entities": "^1.0.0", + "character-entities-legacy": "^1.0.0", + "character-reference-invalid": "^1.0.0", + "is-alphanumerical": "^1.0.0", + "is-decimal": "^1.0.0", + "is-hexadecimal": "^1.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/refractor/node_modules/prismjs": { + "version": "1.27.0", + "resolved": "https://registry.npmjs.org/prismjs/-/prismjs-1.27.0.tgz", + "integrity": "sha512-t13BGPUlFDR7wRB5kQDG4jjl7XeuH6jbJGt11JHPL96qwsEHNX2+68tFXqc1/k+/jALsbSWJKUOT/hcYAZ5LkA==", + "dev": true, + "engines": { + "node": ">=6" + } + }, "node_modules/regenerate": { "version": "1.4.2", "resolved": "https://registry.npmjs.org/regenerate/-/regenerate-1.4.2.tgz", diff --git a/package.json b/package.json index 61e5067a7..1c5faff04 100644 --- a/package.json +++ b/package.json @@ -91,7 +91,8 @@ "type-fest": "^3.10.0", "typescript": "^4.7.4", "webpack": "^5.79.0", - "webpack-merge": "^5.8.0" + "webpack-merge": "^5.8.0", + "react-syntax-highlighter": "^15.5.0" }, "engines": { "node": ">=16" diff --git a/packages/forklift-console-plugin/locales/en/plugin__forklift-console-plugin.json b/packages/forklift-console-plugin/locales/en/plugin__forklift-console-plugin.json index 295345415..26e17c758 100644 --- a/packages/forklift-console-plugin/locales/en/plugin__forklift-console-plugin.json +++ b/packages/forklift-console-plugin/locales/en/plugin__forklift-console-plugin.json @@ -26,6 +26,7 @@ "Actions": "Actions", "Add mapping": "Add mapping", "Add source and target providers for the migration.": "Add source and target providers for the migration.", + "Adding hooks to the plan is optional. Hooks are contained in Ansible playbooks that can be run before or after the migration.": "Adding hooks to the plan is optional. Hooks are contained in Ansible playbooks that can be run before or after the migration.", "All discovered networks have been mapped to the default network.": "All discovered networks have been mapped to the default network.", "All discovered storages have been mapped to the default storage.": "All discovered storages have been mapped to the default storage.", "All networks detected on the selected VMs require a mapping.": "All networks detected on the selected VMs require a mapping.", @@ -50,6 +51,7 @@ "Category": "Category", "Certificate change detected": "Certificate change detected", "Clear all filters": "Clear all filters", + "Click the relevant buttons within the table for managing the plan hooks.": "Click the relevant buttons within the table for managing the plan hooks.", "Click the update credentials button to save your changes, button is disabled until a change is detected.": "Click the update credentials button to save your changes, button is disabled until a change is detected.", "Click the update mappings button to save your changes, button is disabled until a change is detected.": "Click the update mappings button to save your changes, button is disabled until a change is detected.", "Cluster": "Cluster", @@ -109,6 +111,7 @@ "Edit Controller Memory limit": "Edit Controller Memory limit", "Edit credentials": "Edit credentials", "Edit Default Transfer Network": "Edit Default Transfer Network", + "Edit hooks": "Edit hooks", "Edit mappings": "Edit mappings", "Edit Maximum concurrent VM migrations": "Edit Maximum concurrent VM migrations", "Edit migration plan target namespace": "Edit migration plan target namespace", @@ -163,6 +166,7 @@ "Hide from view": "Hide from view", "Hide values": "Hide values", "Hooks": "Hooks", + "Hooks data is loading, please wait.": "Hooks data is loading, please wait.", "Hooks for virtualization": "Hooks for virtualization", "Host": "Host", "Host cluster": "Host cluster", @@ -236,6 +240,7 @@ "New name was generated for the Storage Map due to naming conflict.": "New name was generated for the Storage Map due to naming conflict.", "NICs with empty NIC profile": "NICs with empty NIC profile", "No credentials found.": "No credentials found.", + "No hooks have been added to this migration plan.": "No hooks have been added to this migration plan.", "No inventory data available.": "No inventory data available.", "No Mapping found.": "No Mapping found.", "No NetworkMaps found.": "No NetworkMaps found.", @@ -305,9 +310,11 @@ "Pod network": "Pod network", "Pods": "Pods", "Pods not found": "Pods not found", + "Post-migration": "Post-migration", "Power state": "Power state", "Powered off": "Powered off", "Powered on": "Powered on", + "Pre-migration": "Pre-migration", "Precopy interval (minutes)": "Precopy interval (minutes)", "Preserve CPU model": "Preserve CPU model", "Preserve the CPU model and flags the VM runs with in its oVirt cluster.": "Preserve the CPU model and flags the VM runs with in its oVirt cluster.", @@ -360,6 +367,7 @@ "Skip certificate validation": "Skip certificate validation", "Snapshot polling interval (seconds)": "Snapshot polling interval (seconds)", "Something is wrong, the data was not loaded due to an error, please try to reload the page.": "Something is wrong, the data was not loaded due to an error, please try to reload the page.", + "Something is wrong, the hooks data was not loaded due to an error, please try to reload the page.": "Something is wrong, the hooks data was not loaded due to an error, please try to reload the page.", "Something is wrong, the secret was not loaded, please try to reload the page.": "Something is wrong, the secret was not loaded, please try to reload the page.", "source": "source", "Source Only": "Source Only", @@ -393,6 +401,7 @@ "The certificate is not a valid PEM-encoded X.509 certificate": "The certificate is not a valid PEM-encoded X.509 certificate", "The chosen provider is no longer available.": "The chosen provider is no longer available.", "The current certificate does not match the certificate fetched from URL. Manually validate the fingerprint before proceeding.": "The current certificate does not match the certificate fetched from URL. Manually validate the fingerprint before proceeding.", + "The edit hooks button is disabled if the plan started running and at least one virtual machine was migrated successfully.": "The edit hooks button is disabled if the plan started running and at least one virtual machine was migrated successfully.", "The edit mappings button is disabled if the plan started running and at least one virtual machine was migrated successfully.": "The edit mappings button is disabled if the plan started running and at least one virtual machine was migrated successfully.", "The interval in minutes for precopy. Default value is 60.": "The interval in minutes for precopy. Default value is 60.", "The interval in seconds for snapshot pooling. Default value is 10.": "The interval in seconds for snapshot pooling. Default value is 10.", diff --git a/packages/forklift-console-plugin/src/modules/Plans/views/details/PlanDetailsPage.style.css b/packages/forklift-console-plugin/src/modules/Plans/views/details/PlanDetailsPage.style.css index e6f68a5a2..3863fd643 100644 --- a/packages/forklift-console-plugin/src/modules/Plans/views/details/PlanDetailsPage.style.css +++ b/packages/forklift-console-plugin/src/modules/Plans/views/details/PlanDetailsPage.style.css @@ -95,7 +95,23 @@ padding-bottom: var(--pf-global--spacer--lg); margin-left: var(--pf-global--spacer--sm); } -.forklift-section-mappings-edit { +.forklift-section-plan-helper-text { padding-top: var(--pf-global--spacer--md); padding-bottom: var(--pf-global--spacer--sm); } + +.forklift-page-plan-details-hooks-msg { + padding-top: 50px; + padding-bottom: var(--pf-global--spacer--sm); + align-self: center; + font-size: 16px; +} + +.forklift-page-plan-details-hooks-section { + margin-top: var(--pf-global--spacer--xl); +} + +.forklift-page-plan-details-hooks.playbook-yaml-popover { + max-width: 35vw; + overflow-y: auto; +} diff --git a/packages/forklift-console-plugin/src/modules/Plans/views/details/PlanDetailsPage.tsx b/packages/forklift-console-plugin/src/modules/Plans/views/details/PlanDetailsPage.tsx index a57874368..7b1011706 100644 --- a/packages/forklift-console-plugin/src/modules/Plans/views/details/PlanDetailsPage.tsx +++ b/packages/forklift-console-plugin/src/modules/Plans/views/details/PlanDetailsPage.tsx @@ -13,7 +13,7 @@ import { import { PlanActionsDropdown } from '../../actions'; import { Suspend } from './components'; -import { PlanDetails, PlanMappings, PlanYAML } from './tabs'; +import { PlanDetails, PlanHooks, PlanMappings, PlanYAML } from './tabs'; import './PlanDetailsPage.style.css'; @@ -64,6 +64,13 @@ const PlanDetailsPage_: React.FC<{ ), }, + { + href: 'hooks', + name: t('Hooks'), + component: () => ( + + ), + }, /* { href: 'vms', @@ -77,13 +84,6 @@ const PlanDetailsPage_: React.FC<{ /> ), }, - { - href: 'hooks', - name: t('Hooks'), - component: () => ( - - ), - }, */ ]; diff --git a/packages/forklift-console-plugin/src/modules/Plans/views/details/tabs/Hooks/PlanHooks.tsx b/packages/forklift-console-plugin/src/modules/Plans/views/details/tabs/Hooks/PlanHooks.tsx index 31f4afbaf..8688b76af 100644 --- a/packages/forklift-console-plugin/src/modules/Plans/views/details/tabs/Hooks/PlanHooks.tsx +++ b/packages/forklift-console-plugin/src/modules/Plans/views/details/tabs/Hooks/PlanHooks.tsx @@ -1,29 +1,77 @@ import React from 'react'; +import SectionHeading from 'src/components/headers/SectionHeading'; import { useForkliftTranslation } from 'src/utils/i18n'; -import { HookModelGroupVersionKind } from '@kubev2v/types'; -import { ResourceLink } from '@openshift-console/dynamic-plugin-sdk'; -import { PageSection, Title } from '@patternfly/react-core'; +import { HookModelGroupVersionKind, V1beta1Hook, V1beta1Plan } from '@kubev2v/types'; +import { useK8sWatchResource } from '@openshift-console/dynamic-plugin-sdk'; +import { PageSection } from '@patternfly/react-core'; import { PlanDetailsTabProps } from '../../PlanDetailsPage'; -export const PlanHooks: React.FC = ({ plan }) => { +import { PlanHooksSection } from './PlanHooksSection'; + +export type PlanHooksInitSectionProps = { + plan: V1beta1Plan; + loaded: boolean; + loadError: unknown; +}; + +export const PlanHooks: React.FC = ({ plan, loaded, loadError }) => { const { t } = useForkliftTranslation(); return ( <> - - {t('Hooks')} - - - {plan?.spec?.vms?.[0]?.hooks?.[0]?.hook && ( - - )} - +
+ + + + +
); }; + +const PlanHooksInitSection: React.FC = (props) => { + const { t } = useForkliftTranslation(); + const { plan } = props; + + // Retrieve all k8s Hooks + const [hooks, hooksLoaded, hooksLoadError] = useK8sWatchResource({ + 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) && ( + + + + + {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