From 202f240534f3d5c7185b706fee1c80a4f7e6d1fc Mon Sep 17 00:00:00 2001 From: "red-hat-konflux[bot]" <126015336+red-hat-konflux[bot]@users.noreply.github.com> Date: Sat, 14 Dec 2024 08:09:31 +0000 Subject: [PATCH 1/4] chore(deps): update konflux references Signed-off-by: red-hat-konflux <126015336+red-hat-konflux[bot]@users.noreply.github.com> --- .../forklift-console-plugin-pull-request.yaml | 18 +++++++++--------- .tekton/forklift-console-plugin-push.yaml | 18 +++++++++--------- 2 files changed, 18 insertions(+), 18 deletions(-) diff --git a/.tekton/forklift-console-plugin-pull-request.yaml b/.tekton/forklift-console-plugin-pull-request.yaml index 46e91896..bb5a4c47 100644 --- a/.tekton/forklift-console-plugin-pull-request.yaml +++ b/.tekton/forklift-console-plugin-pull-request.yaml @@ -134,7 +134,7 @@ spec: - name: name value: init - name: bundle - value: quay.io/konflux-ci/tekton-catalog/task-init:0.2@sha256:0523b51c28375a3f222da91690e22eff11888ebc98a0c73c468af44762265c69 + value: quay.io/konflux-ci/tekton-catalog/task-init:0.2@sha256:60063fefe88e111d129cb59caff97c912722927c8a0f750253553d4c527a2396 - name: kind value: task resolver: bundles @@ -225,7 +225,7 @@ spec: - name: name value: buildah-oci-ta - name: bundle - value: quay.io/konflux-ci/tekton-catalog/task-buildah-oci-ta:0.2@sha256:decef0e000a05daad9dd43b707c8b3a96b6125ff5a4ee096fd3e8c23a2881b9e + value: quay.io/konflux-ci/tekton-catalog/task-buildah-oci-ta:0.2@sha256:e0f1ec1ec43ba0f9533dd70fe76a3c24ac3ca14ddd83036099c8073c94dc8258 - name: kind value: task resolver: bundles @@ -254,7 +254,7 @@ spec: - name: name value: build-image-index - name: bundle - value: quay.io/konflux-ci/tekton-catalog/task-build-image-index:0.1@sha256:a89c141c8d35b2e9d9904c92c9b128f7ccf36681adac7f7422b4537b8bb077e7 + value: quay.io/konflux-ci/tekton-catalog/task-build-image-index:0.1@sha256:37328a4b2fc686435531ba423c26c2051822a4e70b06088c4d8eaf0e8fa6d65b - name: kind value: task resolver: bundles @@ -304,7 +304,7 @@ spec: - name: name value: deprecated-image-check - name: bundle - value: quay.io/konflux-ci/tekton-catalog/task-deprecated-image-check:0.4@sha256:5a1a165fa02270f0a947d8a2131ee9d8be0b8e9d34123828c2bef589e504ee84 + value: quay.io/konflux-ci/tekton-catalog/task-deprecated-image-check:0.4@sha256:f8efb0b22692fad908a1a75f8d5c0b6ed3b0bcd2a9853577e7be275e5bac1bb8 - name: kind value: task resolver: bundles @@ -326,7 +326,7 @@ spec: - name: name value: clair-scan - name: bundle - value: quay.io/konflux-ci/tekton-catalog/task-clair-scan:0.2@sha256:0a5421111e7092740398691d5bd7c125cc0896f29531d19414bb5724ae41692a + value: quay.io/konflux-ci/tekton-catalog/task-clair-scan:0.2@sha256:e428b37d253621365ffb24d4053e5f3141988ae6a30fce1c8ba73b7211396eb0 - name: kind value: task resolver: bundles @@ -372,7 +372,7 @@ spec: - name: name value: sast-snyk-check-oci-ta - name: bundle - value: quay.io/konflux-ci/tekton-catalog/task-sast-snyk-check-oci-ta:0.3@sha256:1119722a2d31b831d1aa336fd8cced0a5016c95466b6b59a58bbf3585735850f + value: quay.io/konflux-ci/tekton-catalog/task-sast-snyk-check-oci-ta:0.3@sha256:6d232347739a0366dcfc4e40afbcb5d1937dd3fea8952afb1bd6a4b0c5d1c1f5 - name: kind value: task resolver: bundles @@ -394,7 +394,7 @@ spec: - name: name value: clamav-scan - name: bundle - value: quay.io/konflux-ci/tekton-catalog/task-clamav-scan:0.2@sha256:6e08cf608240f57442ca5458f3c0dade3558f4f2953be8ea939232f5d5378d58 + value: quay.io/konflux-ci/tekton-catalog/task-clamav-scan:0.2@sha256:d78221853f7ff2befc6669dd0eeb91e6611ae84ac7754150ea0f071d92ff41cb - name: kind value: task resolver: bundles @@ -414,7 +414,7 @@ spec: - name: name value: apply-tags - name: bundle - value: quay.io/konflux-ci/tekton-catalog/task-apply-tags:0.1@sha256:87fd7fc0e937aad1a8db9b6e377d7e444f53394dafde512d68adbea6966a4702 + value: quay.io/konflux-ci/tekton-catalog/task-apply-tags:0.1@sha256:0767c115d4ba4854d106c9cdfabdc1f1298bc2742a3fea4fefbac4b9c5873d6e - name: kind value: task resolver: bundles @@ -454,7 +454,7 @@ spec: - name: name value: rpms-signature-scan - name: bundle - value: quay.io/konflux-ci/tekton-catalog/task-rpms-signature-scan:0.2@sha256:8f3b23bf1b0ef55cc79d28604d2397a0101ac9c0c42ae26e26532eb2778c801b + value: quay.io/konflux-ci/tekton-catalog/task-rpms-signature-scan:0.2@sha256:ec536e55a039052823ba74e07db3175554fb046649671d1fefd776ca064d00ac - name: kind value: task resolver: bundles diff --git a/.tekton/forklift-console-plugin-push.yaml b/.tekton/forklift-console-plugin-push.yaml index 9d0a4646..3a856910 100644 --- a/.tekton/forklift-console-plugin-push.yaml +++ b/.tekton/forklift-console-plugin-push.yaml @@ -133,7 +133,7 @@ spec: - name: name value: init - name: bundle - value: quay.io/konflux-ci/tekton-catalog/task-init:0.2@sha256:0523b51c28375a3f222da91690e22eff11888ebc98a0c73c468af44762265c69 + value: quay.io/konflux-ci/tekton-catalog/task-init:0.2@sha256:60063fefe88e111d129cb59caff97c912722927c8a0f750253553d4c527a2396 - name: kind value: task resolver: bundles @@ -224,7 +224,7 @@ spec: - name: name value: buildah-oci-ta - name: bundle - value: quay.io/konflux-ci/tekton-catalog/task-buildah-oci-ta:0.2@sha256:decef0e000a05daad9dd43b707c8b3a96b6125ff5a4ee096fd3e8c23a2881b9e + value: quay.io/konflux-ci/tekton-catalog/task-buildah-oci-ta:0.2@sha256:e0f1ec1ec43ba0f9533dd70fe76a3c24ac3ca14ddd83036099c8073c94dc8258 - name: kind value: task resolver: bundles @@ -253,7 +253,7 @@ spec: - name: name value: build-image-index - name: bundle - value: quay.io/konflux-ci/tekton-catalog/task-build-image-index:0.1@sha256:a89c141c8d35b2e9d9904c92c9b128f7ccf36681adac7f7422b4537b8bb077e7 + value: quay.io/konflux-ci/tekton-catalog/task-build-image-index:0.1@sha256:37328a4b2fc686435531ba423c26c2051822a4e70b06088c4d8eaf0e8fa6d65b - name: kind value: task resolver: bundles @@ -303,7 +303,7 @@ spec: - name: name value: deprecated-image-check - name: bundle - value: quay.io/konflux-ci/tekton-catalog/task-deprecated-image-check:0.4@sha256:5a1a165fa02270f0a947d8a2131ee9d8be0b8e9d34123828c2bef589e504ee84 + value: quay.io/konflux-ci/tekton-catalog/task-deprecated-image-check:0.4@sha256:f8efb0b22692fad908a1a75f8d5c0b6ed3b0bcd2a9853577e7be275e5bac1bb8 - name: kind value: task resolver: bundles @@ -325,7 +325,7 @@ spec: - name: name value: clair-scan - name: bundle - value: quay.io/konflux-ci/tekton-catalog/task-clair-scan:0.2@sha256:0a5421111e7092740398691d5bd7c125cc0896f29531d19414bb5724ae41692a + value: quay.io/konflux-ci/tekton-catalog/task-clair-scan:0.2@sha256:e428b37d253621365ffb24d4053e5f3141988ae6a30fce1c8ba73b7211396eb0 - name: kind value: task resolver: bundles @@ -371,7 +371,7 @@ spec: - name: name value: sast-snyk-check-oci-ta - name: bundle - value: quay.io/konflux-ci/tekton-catalog/task-sast-snyk-check-oci-ta:0.3@sha256:1119722a2d31b831d1aa336fd8cced0a5016c95466b6b59a58bbf3585735850f + value: quay.io/konflux-ci/tekton-catalog/task-sast-snyk-check-oci-ta:0.3@sha256:6d232347739a0366dcfc4e40afbcb5d1937dd3fea8952afb1bd6a4b0c5d1c1f5 - name: kind value: task resolver: bundles @@ -393,7 +393,7 @@ spec: - name: name value: clamav-scan - name: bundle - value: quay.io/konflux-ci/tekton-catalog/task-clamav-scan:0.2@sha256:6e08cf608240f57442ca5458f3c0dade3558f4f2953be8ea939232f5d5378d58 + value: quay.io/konflux-ci/tekton-catalog/task-clamav-scan:0.2@sha256:d78221853f7ff2befc6669dd0eeb91e6611ae84ac7754150ea0f071d92ff41cb - name: kind value: task resolver: bundles @@ -413,7 +413,7 @@ spec: - name: name value: apply-tags - name: bundle - value: quay.io/konflux-ci/tekton-catalog/task-apply-tags:0.1@sha256:87fd7fc0e937aad1a8db9b6e377d7e444f53394dafde512d68adbea6966a4702 + value: quay.io/konflux-ci/tekton-catalog/task-apply-tags:0.1@sha256:0767c115d4ba4854d106c9cdfabdc1f1298bc2742a3fea4fefbac4b9c5873d6e - name: kind value: task resolver: bundles @@ -453,7 +453,7 @@ spec: - name: name value: rpms-signature-scan - name: bundle - value: quay.io/konflux-ci/tekton-catalog/task-rpms-signature-scan:0.2@sha256:8f3b23bf1b0ef55cc79d28604d2397a0101ac9c0c42ae26e26532eb2778c801b + value: quay.io/konflux-ci/tekton-catalog/task-rpms-signature-scan:0.2@sha256:ec536e55a039052823ba74e07db3175554fb046649671d1fefd776ca064d00ac - name: kind value: task resolver: bundles From 60b5a76b9b9e67b0e3fddbd2e46ae3512a37ebf4 Mon Sep 17 00:00:00 2001 From: Sharon Gratch Date: Thu, 12 Dec 2024 18:43:20 +0200 Subject: [PATCH 2/4] Refactoring: replace plan phases strigs with const enums Reference: https://github.com/kubev2v/forklift-console-plugin/pull/1395#discussion_r1861292874 Signed-off-by: Sharon Gratch --- .../en/plugin__forklift-console-plugin.json | 6 + .../actions/PlanActionsDropdownItems.tsx | 5 +- .../src/modules/Plans/modals/ArchiveModal.tsx | 6 +- .../modules/Plans/modals/PlanDeleteModal.tsx | 6 +- .../Plans/utils/constants/planPhases.ts | 24 ++-- .../Plans/utils/helpers/getPhaseLabel.ts | 30 ----- .../Plans/utils/helpers/getPlanPhase.ts | 42 +++---- .../utils/helpers/getPlanProgressVariant.ts | 14 +-- .../src/modules/Plans/utils/helpers/index.ts | 1 - .../modules/Plans/utils/types/PlanPhase.ts | 39 ++++--- .../components/MigrationsTable.tsx | 11 +- .../details/components/PlanPageHeadings.tsx | 4 +- .../views/list/components/ErrorStatusCell.tsx | 4 +- .../views/list/components/PlanStatusCell.tsx | 12 +- .../views/list/components/StatusCell.tsx | 29 +++++ .../views/list/components/VMsProgressCell.tsx | 103 ++++++++++++++++++ 16 files changed, 229 insertions(+), 107 deletions(-) delete mode 100644 packages/forklift-console-plugin/src/modules/Plans/utils/helpers/getPhaseLabel.ts create mode 100644 packages/forklift-console-plugin/src/modules/Plans/views/list/components/StatusCell.tsx create mode 100644 packages/forklift-console-plugin/src/modules/Plans/views/list/components/VMsProgressCell.tsx 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 5826254a..7dbce1ba 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 @@ -1,15 +1,19 @@ { "{{Canceled}} canceled": "{{Canceled}} canceled", + "{{canceled}} VMs canceled": "{{canceled}} VMs canceled", "{{completed}} / {{total}}": "{{completed}} / {{total}}", "{{dateLabel}} Failed: {{value}}": "{{dateLabel}} Failed: {{value}}", "{{dateLabel}} Running: {{value}}": "{{dateLabel}} Running: {{value}}", "{{dateLabel}} Succeeded: {{value}}": "{{dateLabel}} Succeeded: {{value}}", + "{{error}} VMs failed": "{{error}} VMs failed", "{{label}} field is missing from the secret data.": "{{label}} field is missing from the secret data.", "{{name}} Details": "{{name}} Details", "{{selectedLength}} hosts selected.": "{{selectedLength}} hosts selected.", "{{success}} of {{total}} VMs migrated": "{{success}} of {{total}} VMs migrated", + "{{success}} VMs succeeded": "{{success}} VMs succeeded", "{{total}} VM": "{{total}} VM", "{{total}} VM_plural": "{{total}} VMs", + "{{total}} VMs": "{{total}} VMs", "{{vmCount}} VMs selected ": "{{vmCount}} VMs selected ", "{children}": "{children}", "24 hours": "24 hours", @@ -454,6 +458,7 @@ "Start migration": "Start migration", "Started at": "Started at", "Status": "Status", + "Status details": "Status details", "Storage": "Storage", "Storage classes": "Storage classes", "Storage domains": "Storage domains", @@ -508,6 +513,7 @@ "Token": "Token", "Total CPU count:": "Total CPU count:", "Total memory:": "Total memory:", + "Total of {{total}} VMs are planned for migration:": "Total of {{total}} VMs are planned for migration:", "Total virtual machines": "Total virtual machines", "Total: {{length}}": "Total: {{length}}", "Transfer Network": "Transfer Network", diff --git a/packages/forklift-console-plugin/src/modules/Plans/actions/PlanActionsDropdownItems.tsx b/packages/forklift-console-plugin/src/modules/Plans/actions/PlanActionsDropdownItems.tsx index 3d22954f..541d95c1 100644 --- a/packages/forklift-console-plugin/src/modules/Plans/actions/PlanActionsDropdownItems.tsx +++ b/packages/forklift-console-plugin/src/modules/Plans/actions/PlanActionsDropdownItems.tsx @@ -21,6 +21,7 @@ import { isPlanArchived, isPlanExecuting, PlanData, + PlanPhase, } from '../utils'; export const PlanActionsDropdownItems = ({ data }: PlanActionsDropdownItemsProps) => { @@ -93,7 +94,9 @@ export const PlanActionsDropdownItems = ({ data }: PlanActionsDropdownItemsProps {t('Archive Plan')} diff --git a/packages/forklift-console-plugin/src/modules/Plans/modals/ArchiveModal.tsx b/packages/forklift-console-plugin/src/modules/Plans/modals/ArchiveModal.tsx index e99fbf42..80557307 100644 --- a/packages/forklift-console-plugin/src/modules/Plans/modals/ArchiveModal.tsx +++ b/packages/forklift-console-plugin/src/modules/Plans/modals/ArchiveModal.tsx @@ -8,7 +8,7 @@ import { PlanModel, V1beta1Plan } from '@kubev2v/types'; import { K8sModel, k8sPatch } from '@openshift-console/dynamic-plugin-sdk'; import { Alert, Button, Modal, ModalVariant } from '@patternfly/react-core'; -import { getPlanPhase } from '../utils'; +import { getPlanPhase, PlanPhase } from '../utils'; /** * Props for the DeleteModal component @@ -70,7 +70,7 @@ export const ArchiveModal: React.FC = ({ title, resource, red const actions = [ + + } + size={ProgressSize.sm} + measureLocation={ProgressMeasureLocation.top} + variant={progressVariant} + /> + + ); +}; From 640ac2da0cbf6d83aa4282806ea21c50b872f460 Mon Sep 17 00:00:00 2001 From: Jeff Puzzo Date: Mon, 16 Dec 2024 13:28:15 -0500 Subject: [PATCH 3/4] [MTV-1769] Add Project field to migration plan/provider wizards Signed-off-by: Jeff Puzzo --- .../TypeaheadSelect/TypeaheadSelect.tsx | 425 ++++++++++++++++++ .../src/components/TypeaheadSelect/index.ts | 3 + packages/common/src/components/index.ts | 1 + .../en/plugin__forklift-console-plugin.json | 5 +- .../Plans/views/create/PlanCreatePage.tsx | 44 +- .../create/components/PlanCreateForm.tsx | 52 ++- .../create/components/PlanNameTextField.tsx | 55 +++ .../create/components/ProjectNameSelect.tsx | 48 ++ .../SelectSourceProvider.tsx | 21 +- .../views/create/ProvidersCreatePage.tsx | 51 ++- .../create/components/ProviderCreateForm.tsx | 40 +- .../ProvidersCreateVmMigrationContext.tsx | 2 + .../migrate/components/PlansCreateForm.tsx | 46 +- .../views/migrate/reducer/actions.ts | 17 + .../migrate/reducer/createInitialState.ts | 8 +- .../views/migrate/reducer/reducer.ts | 15 +- .../modules/Providers/views/migrate/types.ts | 2 + .../views/migrate/useFetchEffects.ts | 13 +- 18 files changed, 742 insertions(+), 106 deletions(-) create mode 100644 packages/common/src/components/TypeaheadSelect/TypeaheadSelect.tsx create mode 100644 packages/common/src/components/TypeaheadSelect/index.ts create mode 100644 packages/forklift-console-plugin/src/modules/Plans/views/create/components/PlanNameTextField.tsx create mode 100644 packages/forklift-console-plugin/src/modules/Plans/views/create/components/ProjectNameSelect.tsx diff --git a/packages/common/src/components/TypeaheadSelect/TypeaheadSelect.tsx b/packages/common/src/components/TypeaheadSelect/TypeaheadSelect.tsx new file mode 100644 index 00000000..f82af1f8 --- /dev/null +++ b/packages/common/src/components/TypeaheadSelect/TypeaheadSelect.tsx @@ -0,0 +1,425 @@ +import React from 'react'; + +import { + Button, + MenuToggle, + MenuToggleElement, + MenuToggleProps, + Select, + SelectList, + SelectOption, + SelectOptionProps, + SelectProps, + TextInputGroup, + TextInputGroupMain, + TextInputGroupUtilities, +} from '@patternfly/react-core'; +import { TimesIcon } from '@patternfly/react-icons'; + +export interface TypeaheadSelectOption extends Omit { + /** Content of the select option. */ + content: string | number; + /** Value of the select option. */ + value: string | number; + /** Indicator for option being selected */ + isSelected?: boolean; +} + +export interface TypeaheadSelectProps extends Omit { + /** Options of the select */ + selectOptions: TypeaheadSelectOption[]; + /** Callback triggered on selection. */ + onSelect?: ( + _event: + | React.MouseEvent + | React.KeyboardEvent + | undefined, + selection: string | number, + ) => void; + /** Callback triggered when the select opens or closes. */ + onToggle?: (nextIsOpen: boolean) => void; + /** Callback triggered when the text in the input field changes. */ + onInputChange?: (newValue: string) => void; + /** Function to return items matching the current filter value */ + filterFunction?: ( + filterValue: string, + options: TypeaheadSelectOption[], + ) => TypeaheadSelectOption[]; + /** Callback triggered when the clear button is selected */ + onClearSelection?: () => void; + /** Flag to allow clear current selection */ + allowClear?: boolean; + /** Placeholder text for the select input. */ + placeholder?: string; + /** Flag to indicate if the typeahead select allows new items */ + isCreatable?: boolean; + /** Flag to indicate if create option should be at top of typeahead */ + isCreateOptionOnTop?: boolean; + /** Message to display to create a new option */ + createOptionMessage?: string | ((newValue: string) => string); + /** Message to display when no options are available. */ + noOptionsAvailableMessage?: string; + /** Message to display when no options match the filter. */ + noOptionsFoundMessage?: string | ((filter: string) => string); + /** Flag indicating the select should be disabled. */ + isDisabled?: boolean; + /** Width of the toggle. */ + toggleWidth?: string; + /** Additional props passed to the toggle. */ + toggleProps?: MenuToggleProps; +} + +const defaultNoOptionsFoundMessage = (filter: string) => `No results found for "${filter}"`; +const defaultCreateOptionMessage = (newValue: string) => `Create "${newValue}"`; +const defaultFilterFunction = (filterValue: string, options: TypeaheadSelectOption[]) => + options.filter((o) => String(o.content).toLowerCase().includes(filterValue.toLowerCase())); + +export const TypeaheadSelect: React.FC = ({ + innerRef, + selectOptions, + onSelect, + onToggle, + onInputChange, + filterFunction = defaultFilterFunction, + onClearSelection, + allowClear, + placeholder = 'Select an option', + noOptionsAvailableMessage = 'No options are available', + noOptionsFoundMessage = defaultNoOptionsFoundMessage, + isCreatable = false, + isCreateOptionOnTop = false, + createOptionMessage = defaultCreateOptionMessage, + isDisabled, + toggleWidth, + toggleProps, + ...props +}: TypeaheadSelectProps) => { + const [isOpen, setIsOpen] = React.useState(false); + const [filterValue, setFilterValue] = React.useState(''); + const [isFiltering, setIsFiltering] = React.useState(false); + const [focusedItemIndex, setFocusedItemIndex] = React.useState(null); + const [activeItemId, setActiveItemId] = React.useState(null); + const textInputRef = React.useRef(); + + const NO_RESULTS = 'no results'; + + const selected = React.useMemo( + () => selectOptions.find((option) => option.value === props.selected || option.isSelected), + [props.selected, selectOptions], + ); + + const filteredSelections = React.useMemo(() => { + let newSelectOptions: TypeaheadSelectOption[] = selectOptions; + + // Filter menu items based on the text input value when one exists + if (isFiltering && filterValue) { + newSelectOptions = filterFunction(filterValue, selectOptions); + + if ( + isCreatable && + filterValue.trim() && + !newSelectOptions.find((o) => String(o.content).toLowerCase() === filterValue.toLowerCase()) + ) { + const createOption = { + content: + typeof createOptionMessage === 'string' + ? createOptionMessage + : createOptionMessage(filterValue), + value: filterValue, + }; + newSelectOptions = isCreateOptionOnTop + ? [createOption, ...newSelectOptions] + : [...newSelectOptions, createOption]; + } + + // When no options are found after filtering, display 'No results found' + if (!newSelectOptions.length) { + newSelectOptions = [ + { + isAriaDisabled: true, + content: + typeof noOptionsFoundMessage === 'string' + ? noOptionsFoundMessage + : noOptionsFoundMessage(filterValue), + value: NO_RESULTS, + }, + ]; + } + } + + // When no options are available, display 'No options available' + if (!newSelectOptions.length) { + newSelectOptions = [ + { + isAriaDisabled: true, + content: noOptionsAvailableMessage, + value: NO_RESULTS, + }, + ]; + } + + return newSelectOptions; + }, [ + isFiltering, + filterValue, + filterFunction, + selectOptions, + noOptionsFoundMessage, + isCreatable, + isCreateOptionOnTop, + createOptionMessage, + noOptionsAvailableMessage, + ]); + + React.useEffect(() => { + if (isFiltering) { + openMenu(); + } + // Don't update on openMenu changes + }, [isFiltering]); + + const setActiveAndFocusedItem = (itemIndex: number) => { + setFocusedItemIndex(itemIndex); + const focusedItem = selectOptions[itemIndex]; + setActiveItemId(String(focusedItem.value)); + }; + + const resetActiveAndFocusedItem = () => { + setFocusedItemIndex(null); + setActiveItemId(null); + }; + + const openMenu = () => { + if (!isOpen) { + if (onToggle) { + onToggle(true); + } + setIsOpen(true); + } + }; + + const closeMenu = () => { + if (onToggle) { + onToggle(false); + } + setIsOpen(false); + resetActiveAndFocusedItem(); + setIsFiltering(false); + setFilterValue(String(selected?.content ?? '')); + }; + + const onInputClick = () => { + if (!isOpen) { + openMenu(); + } + setTimeout(() => { + textInputRef.current?.focus(); + }, 100); + }; + + const selectOption = ( + _event: + | React.MouseEvent + | React.KeyboardEvent + | undefined, + option: TypeaheadSelectOption, + ) => { + if (onSelect) { + onSelect(_event, option.value); + } + closeMenu(); + }; + + const handleSelect = ( + _event: React.MouseEvent | undefined, + value: string | number | undefined, + ) => { + if (value && value !== NO_RESULTS) { + const optionToSelect = selectOptions.find((option) => option.value === value); + if (optionToSelect) { + selectOption(_event, optionToSelect); + } else if (isCreatable) { + selectOption(_event, { value, content: value }); + } + } + }; + + const onTextInputChange = (_event: React.FormEvent, value: string) => { + setFilterValue(value || ''); + setIsFiltering(true); + if (onInputChange) { + onInputChange(value); + } + + resetActiveAndFocusedItem(); + }; + + const handleMenuArrowKeys = (key: string) => { + let indexToFocus = 0; + + openMenu(); + + if (filteredSelections.every((option) => option.isDisabled)) { + return; + } + + if (key === 'ArrowUp') { + // When no index is set or at the first index, focus to the last, otherwise decrement focus index + if (focusedItemIndex === null || focusedItemIndex === 0) { + indexToFocus = filteredSelections.length - 1; + } else { + indexToFocus = focusedItemIndex - 1; + } + + // Skip disabled options + while (filteredSelections[indexToFocus].isDisabled) { + indexToFocus--; + if (indexToFocus === -1) { + indexToFocus = filteredSelections.length - 1; + } + } + } + + if (key === 'ArrowDown') { + // When no index is set or at the last index, focus to the first, otherwise increment focus index + if (focusedItemIndex === null || focusedItemIndex === filteredSelections.length - 1) { + indexToFocus = 0; + } else { + indexToFocus = focusedItemIndex + 1; + } + + // Skip disabled options + while (filteredSelections[indexToFocus].isDisabled) { + indexToFocus++; + if (indexToFocus === filteredSelections.length) { + indexToFocus = 0; + } + } + } + + setActiveAndFocusedItem(indexToFocus); + }; + + const onInputKeyDown = (event: React.KeyboardEvent) => { + const focusedItem = focusedItemIndex !== null ? filteredSelections[focusedItemIndex] : null; + + switch (event.key) { + case 'Enter': + if ( + isOpen && + focusedItem && + focusedItem.value !== NO_RESULTS && + !focusedItem.isAriaDisabled + ) { + selectOption(event, focusedItem); + } + + openMenu(); + + break; + case 'ArrowUp': + case 'ArrowDown': + event.preventDefault(); + handleMenuArrowKeys(event.key); + break; + } + }; + + const onToggleClick = () => { + if (!isOpen) { + openMenu(); + } else { + closeMenu(); + } + textInputRef.current?.focus(); + }; + + const onClearButtonClick = () => { + if (isFiltering && filterValue) { + if (selected && onSelect) { + onSelect(undefined, selected.value); + } + setFilterValue(''); + if (onInputChange) { + onInputChange(''); + } + setIsFiltering(false); + } + + resetActiveAndFocusedItem(); + textInputRef.current?.focus(); + + if (onClearSelection) { + onClearSelection(); + } + }; + + const toggle = (toggleRef: React.Ref) => ( + + + + {(isFiltering && filterValue) || (allowClear && selected) ? ( + + + + } + > + onSelect(String(value))} + onClearSelection={() => onSelect('')} + isDisabled={isDisabled} + /> + + ); +}; diff --git a/packages/forklift-console-plugin/src/modules/Plans/views/create/steps/SelectSourceProvider/SelectSourceProvider.tsx b/packages/forklift-console-plugin/src/modules/Plans/views/create/steps/SelectSourceProvider/SelectSourceProvider.tsx index 10df4be0..88023b47 100644 --- a/packages/forklift-console-plugin/src/modules/Plans/views/create/steps/SelectSourceProvider/SelectSourceProvider.tsx +++ b/packages/forklift-console-plugin/src/modules/Plans/views/create/steps/SelectSourceProvider/SelectSourceProvider.tsx @@ -1,4 +1,6 @@ import React from 'react'; +import { CreateVmMigration, PageAction } from 'src/modules/Providers/views/migrate/reducer/actions'; +import { CreateVmMigrationPageState } from 'src/modules/Providers/views/migrate/types'; import { useForkliftTranslation } from 'src/utils/i18n'; import { V1beta1Provider } from '@kubev2v/types'; @@ -10,12 +12,22 @@ import { PlanCreateForm } from './../../components'; import { MemoizedProviderVirtualMachinesList } from './MemoizedProviderVirtualMachinesList'; export const SelectSourceProvider: React.FC<{ - namespace: string; + projectName: string; filterState: PlanCreatePageState; - filterDispatch: React.Dispatch; providers: V1beta1Provider[]; selectedProvider: V1beta1Provider; -}> = ({ filterState, filterDispatch, providers, selectedProvider }) => { + state: CreateVmMigrationPageState; + dispatch: React.Dispatch>; + filterDispatch: React.Dispatch; +}> = ({ + filterState, + providers, + selectedProvider, + state, + projectName, + dispatch, + filterDispatch, +}) => { const { t } = useForkliftTranslation(); // Get the ready providers (note: currently forklift does not allow filter be status.phase) @@ -39,6 +51,9 @@ export const SelectSourceProvider: React.FC<{ providers={filteredProviders} filterState={filterState} filterDispatch={filterDispatch} + dispatch={dispatch} + state={state} + projectName={projectName} /> {filterState.selectedProviderUID && ( diff --git a/packages/forklift-console-plugin/src/modules/Providers/views/create/ProvidersCreatePage.tsx b/packages/forklift-console-plugin/src/modules/Providers/views/create/ProvidersCreatePage.tsx index 86fe094b..d572fe78 100644 --- a/packages/forklift-console-plugin/src/modules/Providers/views/create/ProvidersCreatePage.tsx +++ b/packages/forklift-console-plugin/src/modules/Providers/views/create/ProvidersCreatePage.tsx @@ -2,9 +2,10 @@ import React, { useReducer } from 'react'; import { useHistory } from 'react-router'; import { Base64 } from 'js-base64'; import SectionHeading from 'src/components/headers/SectionHeading'; -import { ForkliftTrans, useForkliftTranslation } from 'src/utils/i18n'; +import { useForkliftTranslation } from 'src/utils/i18n'; import { IoK8sApiCoreV1Secret, ProviderModelRef, V1beta1Provider } from '@kubev2v/types'; +import { useActiveNamespace } from '@openshift-console/dynamic-plugin-sdk'; import { Alert, Button, @@ -29,6 +30,7 @@ import './ProvidersCreatePage.style.css'; interface ProvidersCreatePageState { newSecret: IoK8sApiCoreV1Secret; newProvider: V1beta1Provider; + projectName: string; validationError: ValidationMsg; apiError: Error | null; } @@ -39,24 +41,26 @@ export const ProvidersCreatePage: React.FC<{ const { t } = useForkliftTranslation(); const history = useHistory(); const [isLoading, toggleIsLoading] = useToggle(); - + const [activeNamespace, setActiveNamespace] = useActiveNamespace(); const [providerNames] = useK8sWatchProviderNames({ namespace }); - const defaultNamespace = process?.env?.DEFAULT_NAMESPACE || 'default'; + const projectName = activeNamespace === '#ALL_NS#' ? 'openshift-mtv' : activeNamespace; + const initialNamespace = namespace || projectName || defaultNamespace; const initialState: ProvidersCreatePageState = { + projectName, newSecret: { ...secretTemplate, metadata: { ...secretTemplate.metadata, - namespace: namespace || defaultNamespace, + namespace: initialNamespace, }, }, newProvider: { ...providerTemplate, metadata: { ...providerTemplate.metadata, - namespace: namespace || defaultNamespace, + namespace: initialNamespace, }, }, validationError: { type: 'error', msg: 'Missing provider name' }, @@ -109,6 +113,21 @@ export const ProvidersCreatePage: React.FC<{ apiError: null, }; } + case 'SET_PROJECT_NAME': { + const value = action.payload; + let validationError: ValidationMsg = { type: 'default' }; + + if (!value) { + validationError = { type: 'error', msg: 'Missing project name' }; + } + + return { + ...state, + validationError, + projectName: String(value), + apiError: null, + }; + } case 'SET_API_ERROR': { const value = action.payload as Error | null; return { ...state, apiError: value }; @@ -230,26 +249,13 @@ export const ProvidersCreatePage: React.FC<{ )} - {!namespace && ( - - - This provider will be created in {defaultNamespace} namespace, if you - wish to choose another namespace please cancel, and choose a namespace from the top - bar. - - - )} - dispatch({ type: 'SET_PROJECT_NAME', payload: value })} providerNames={providerNames} /> @@ -259,7 +265,10 @@ export const ProvidersCreatePage: React.FC<{