diff --git a/.tekton/forklift-console-plugin-pull-request.yaml b/.tekton/forklift-console-plugin-pull-request.yaml index 46e918968..235b389eb 100644 --- a/.tekton/forklift-console-plugin-pull-request.yaml +++ b/.tekton/forklift-console-plugin-pull-request.yaml @@ -30,6 +30,8 @@ spec: value: build/Containerfile - name: path-context value: . + - name: build-source-image + value: "true" pipelineSpec: description: | This pipeline is ideal for building container images from a Containerfile while maintaining trust after pipeline customization. @@ -134,7 +136,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 +227,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 +256,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 +306,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 +328,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 +374,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 +396,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 +416,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 +456,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 9d0a46460..d2e76e0e7 100644 --- a/.tekton/forklift-console-plugin-push.yaml +++ b/.tekton/forklift-console-plugin-push.yaml @@ -29,6 +29,8 @@ spec: value: build/Containerfile - name: path-context value: . + - name: build-source-image + value: "true" pipelineSpec: description: | This pipeline is ideal for building container images from a Containerfile while maintaining trust after pipeline customization. @@ -133,7 +135,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 +226,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 +255,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 +305,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 +327,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 +373,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 +395,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 +415,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 +455,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/packages/common/src/components/TypeaheadSelect/TypeaheadSelect.tsx b/packages/common/src/components/TypeaheadSelect/TypeaheadSelect.tsx new file mode 100644 index 000000000..f82af1f82 --- /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 10df4be05..f506c2030 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,24 @@ 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; + hideProviderSection?: boolean; +}> = ({ + filterState, + providers, + selectedProvider, + state, + projectName, + dispatch, + filterDispatch, + hideProviderSection, +}) => { const { t } = useForkliftTranslation(); // Get the ready providers (note: currently forklift does not allow filter be status.phase) @@ -33,13 +47,20 @@ export const SelectSourceProvider: React.FC<{ return ( <> - {t('Select source provider')} + {!hideProviderSection && ( + <> + {t('Select source provider')} - + + + )} {filterState.selectedProviderUID && ( <> diff --git a/packages/forklift-console-plugin/src/modules/Plans/views/details/components/DetailsSection/components/StatusDetailsItem.tsx b/packages/forklift-console-plugin/src/modules/Plans/views/details/components/DetailsSection/components/StatusDetailsItem.tsx index 273c47345..75cb5952c 100644 --- a/packages/forklift-console-plugin/src/modules/Plans/views/details/components/DetailsSection/components/StatusDetailsItem.tsx +++ b/packages/forklift-console-plugin/src/modules/Plans/views/details/components/DetailsSection/components/StatusDetailsItem.tsx @@ -1,5 +1,5 @@ import React from 'react'; -import { StatusCell } from 'src/modules/Plans/views/list'; +import { PlanStatusCell } from 'src/modules/Plans/views/list'; import { DetailsItem } from 'src/modules/Providers/utils'; import { useForkliftTranslation } from 'src/utils/i18n'; @@ -12,7 +12,7 @@ export const StatusDetailsItem: React.FC = ({ resource }) } + content={} /> ); }; diff --git a/packages/forklift-console-plugin/src/modules/Plans/views/details/components/MigrationsSection/components/MigrationsTable.tsx b/packages/forklift-console-plugin/src/modules/Plans/views/details/components/MigrationsSection/components/MigrationsTable.tsx index 39042db1d..d05377bb6 100644 --- a/packages/forklift-console-plugin/src/modules/Plans/views/details/components/MigrationsSection/components/MigrationsTable.tsx +++ b/packages/forklift-console-plugin/src/modules/Plans/views/details/components/MigrationsSection/components/MigrationsTable.tsx @@ -1,10 +1,6 @@ import React from 'react'; import { ConsoleTimestamp } from 'src/components/ConsoleTimestamp'; -import { - getMigrationVmsCounts, - getPhaseLabel, - getPlanProgressVariant, -} from 'src/modules/Plans/utils'; +import { getMigrationVmsCounts, getPlanProgressVariant, PlanPhase } from 'src/modules/Plans/utils'; import { getMigrationPhase } from 'src/modules/Plans/utils/helpers/getMigrationPhase'; import { useForkliftTranslation } from 'src/utils/i18n'; @@ -90,8 +86,9 @@ const VMsLabel: React.FC<{ migration: V1beta1Migration }> = ({ migration }) => { const { t } = useForkliftTranslation(); const phase = getMigrationPhase(migration); - const phaseLabel = t(getPhaseLabel(phase)); - const progressVariant = getPlanProgressVariant(phase); + const phaseLabel = PlanPhase[phase] ? t(PlanPhase[phase]) : PlanPhase.Unknown; + + const progressVariant = getPlanProgressVariant(PlanPhase[phase]); const counters = getMigrationVmsCounts(migration?.status?.vms); if (!counters?.total || counters.total === 0) { diff --git a/packages/forklift-console-plugin/src/modules/Plans/views/details/components/PlanPageHeadings.tsx b/packages/forklift-console-plugin/src/modules/Plans/views/details/components/PlanPageHeadings.tsx index b69cdda74..2e528937b 100644 --- a/packages/forklift-console-plugin/src/modules/Plans/views/details/components/PlanPageHeadings.tsx +++ b/packages/forklift-console-plugin/src/modules/Plans/views/details/components/PlanPageHeadings.tsx @@ -1,7 +1,7 @@ import React from 'react'; import { PlanActionsDropdown } from 'src/modules/Plans/actions'; import { PlanStartMigrationModal } from 'src/modules/Plans/modals'; -import { canPlanReStart, canPlanStart, getPlanPhase } from 'src/modules/Plans/utils'; +import { canPlanReStart, canPlanStart, getPlanPhase, PlanPhase } from 'src/modules/Plans/utils'; import { useGetDeleteAndEditAccessReview } from 'src/modules/Providers/hooks'; import { useModal } from 'src/modules/Providers/modals'; import { PageHeadings } from 'src/modules/Providers/utils'; @@ -81,7 +81,7 @@ export const PlanPageHeadings: React.FC<{ name: string; namespace: string }> = ( const handleAlerts = () => { // alerts are not relevant to display if plan was completed successfully - if (planStatus === 'Succeeded') return; + if (planStatus === PlanPhase.Succeeded) return; if (criticalCondition) { alerts.push( diff --git a/packages/forklift-console-plugin/src/modules/Plans/views/details/tabs/Mappings/PlanMappings.tsx b/packages/forklift-console-plugin/src/modules/Plans/views/details/tabs/Mappings/PlanMappings.tsx index ba5b626ee..d134f6aa0 100644 --- a/packages/forklift-console-plugin/src/modules/Plans/views/details/tabs/Mappings/PlanMappings.tsx +++ b/packages/forklift-console-plugin/src/modules/Plans/views/details/tabs/Mappings/PlanMappings.tsx @@ -1,4 +1,4 @@ -import React from 'react'; +import React, { useEffect, useReducer } from 'react'; import { SectionHeading } from 'src/components/headers/SectionHeading'; import { useOpenShiftNetworks, useSourceNetworks } from 'src/modules/Providers/hooks/useNetworks'; import { useOpenShiftStorages, useSourceStorages } from 'src/modules/Providers/hooks/useStorages'; @@ -17,12 +17,23 @@ import { import { useK8sWatchResource } from '@openshift-console/dynamic-plugin-sdk'; import { Alert, PageSection } from '@patternfly/react-core'; -import { PlanMappingsSection } from './PlanMappingsSection'; +import { + PlanMappingsSection, + planMappingsSectionReducer, + PlanMappingsSectionState, +} from './PlanMappingsSection'; export type PlanMappingsInitSectionProps = { plan: V1beta1Plan; - loaded: boolean; - loadError: unknown; + loaded?: boolean; + loadError?: unknown; + planMappingsState: PlanMappingsSectionState; + planMappingsDispatch: React.Dispatch<{ + type: string; + payload?; + }>; + planNetworkMaps: V1beta1NetworkMap; + planStorageMaps: V1beta1StorageMap; }; export const PlanMappings: React.FC<{ name: string; namespace: string }> = ({ @@ -30,7 +41,6 @@ export const PlanMappings: React.FC<{ name: string; namespace: string }> = ({ namespace, }) => { const { t } = useForkliftTranslation(); - const [plan, loaded, loadError] = useK8sWatchResource({ groupVersionKind: PlanModelGroupVersionKind, namespaced: true, @@ -38,30 +48,6 @@ export const PlanMappings: React.FC<{ name: string; namespace: string }> = ({ namespace, }); - return ( - <> -
- - - - -
- - ); -}; - -const PlanMappingsInitSection: React.FC = (props) => { - const { t } = useForkliftTranslation(); - const { plan } = props; - - // Retrieve all k8s Providers - const [providers, providersLoaded, providersLoadError] = useK8sWatchResource({ - groupVersionKind: ProviderModelGroupVersionKind, - namespaced: true, - isList: true, - namespace: plan?.metadata?.namespace, - }); - // Retrieve all k8s Network Mappings const [networkMaps, networkMapsLoaded, networkMapsError] = useK8sWatchResource< V1beta1NetworkMap[] @@ -88,6 +74,93 @@ const PlanMappingsInitSection: React.FC = (props) const planStorageMaps = storageMaps ? storageMaps.find((storage) => storage?.metadata?.name === plan.spec.map?.storage?.name) : null; + + const initialState: PlanMappingsSectionState = { + edit: false, + dataChanged: false, + alertMessage: null, + updatedNetwork: planNetworkMaps?.spec?.map || [], + updatedStorage: planStorageMaps?.spec?.map || [], + planNetworkMaps: planNetworkMaps, + planStorageMaps: planStorageMaps, + }; + + const [state, dispatch] = useReducer(planMappingsSectionReducer, initialState); + + useEffect(() => { + if (planNetworkMaps && planStorageMaps) { + dispatch({ + type: 'SET_PLAN_MAPS', + payload: { planNetworkMaps, planStorageMaps }, + }); + } + }, [planNetworkMaps, planStorageMaps]); + + const checkResources = () => { + if (!networkMapsLoaded || !storageMapsLoaded) { + return ( +
+ {t('Data is loading, please wait.')} +
+ ); + } + + if (networkMapsError || storageMapsError) { + return ( +
+ + {t( + 'Something is wrong, the data was not loaded due to an error, please try to reload the page.', + )} + +
+ ); + } + + if (networkMaps.length == 0 || storageMaps.length == 0) + return ( +
+ {t('No Mapping found.')} +
+ ); + + return null; + }; + + return ( + <> +
+ + + {checkResources() ?? ( + + )} + +
+ + ); +}; + +export const PlanMappingsInitSection: React.FC = (props) => { + const { t } = useForkliftTranslation(); + const { plan, planMappingsState, planMappingsDispatch, planNetworkMaps, planStorageMaps } = props; + + // Retrieve all k8s Providers + const [providers, providersLoaded, providersLoadError] = useK8sWatchResource({ + groupVersionKind: ProviderModelGroupVersionKind, + namespaced: true, + isList: true, + namespace: plan?.metadata?.namespace, + }); + const sourceProvider: V1beta1Provider = providers ? providers.find((p) => p?.metadata?.name === plan?.spec?.provider?.source?.name) : null; @@ -106,8 +179,6 @@ const PlanMappingsInitSection: React.FC = (props) useOpenShiftStorages(targetProvider); if ( - !networkMapsLoaded || - !storageMapsLoaded || !providersLoaded || sourceNetworksLoading || targetNetworksLoading || @@ -122,8 +193,6 @@ const PlanMappingsInitSection: React.FC = (props) } if ( - networkMapsError || - storageMapsError || providersLoadError || sourceNetworksError || targetNetworksError || @@ -141,13 +210,6 @@ const PlanMappingsInitSection: React.FC = (props) ); } - if (networkMaps.length == 0 || storageMaps.length == 0) - return ( -
- {t('No Mapping found.')} -
- ); - // Warn when missing inventory data, missing inventory will make // some editing options missing. const alerts = []; @@ -176,6 +238,8 @@ const PlanMappingsInitSection: React.FC = (props) targetNetworks={targetNetworks} sourceStorages={sourceStorages} targetStorages={targetStorages} + planMappingsState={planMappingsState} + planMappingsDispatch={planMappingsDispatch} /> ); 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 5bf78ae11..5904b3991 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 @@ -1,5 +1,6 @@ -import React, { ReactNode, useReducer, useState } from 'react'; +import React, { ReactNode, useState } from 'react'; import { isPlanEditable } from 'src/modules/Plans/utils'; +import { PlanEditAction } from 'src/modules/Plans/utils/types/PlanEditAction'; import { InventoryNetwork } from 'src/modules/Providers/hooks/useNetworks'; import { InventoryStorage } from 'src/modules/Providers/hooks/useStorages'; import { useForkliftTranslation } from 'src/utils/i18n'; @@ -56,12 +57,15 @@ import { * @property {V1beta1NetworkMapSpecMap[]} updatedNetwork - The new version of the Plan Network Maps being edited. * @property {V1beta1StorageMapSpecMap[]} updatedStorage - The new version of the Plan Storage Maps being edited. */ -interface PlanMappingsSectionState { +export interface PlanMappingsSectionState { edit: boolean; dataChanged: boolean; alertMessage: ReactNode; updatedNetwork: V1beta1NetworkMapSpecMap[]; updatedStorage: V1beta1StorageMapSpecMap[]; + planNetworkMaps: V1beta1NetworkMap; + planStorageMaps: V1beta1StorageMap; + editAction?: PlanEditAction; } export type PlanMappingsSectionProps = { @@ -72,8 +76,86 @@ export type PlanMappingsSectionProps = { targetNetworks: OpenShiftNetworkAttachmentDefinition[]; sourceStorages: InventoryStorage[]; targetStorages: OpenShiftStorageClass[]; + planMappingsState?: PlanMappingsSectionState; + planMappingsDispatch?: React.Dispatch<{ + type: string; + payload?; + }>; }; +export function planMappingsSectionReducer( + state: PlanMappingsSectionState, + action: { type: string; payload? }, +): PlanMappingsSectionState { + switch (action.type) { + case 'SET_PLAN_MAPS': { + const { planNetworkMaps, planStorageMaps } = action.payload; + return { + ...state, + planNetworkMaps, + planStorageMaps, + updatedNetwork: planNetworkMaps?.spec?.map, + updatedStorage: planStorageMaps?.spec?.map, + }; + } + case 'TOGGLE_EDIT': { + return { ...state, edit: !state.edit }; + } + case 'SET_CANCEL': { + const dataChanged = false; + + return { + ...state, + dataChanged, + alertMessage: null, + updatedNetwork: state.planNetworkMaps.spec.map, + updatedStorage: state.planStorageMaps.spec.map, + }; + } + case 'SET_ALERT_MESSAGE': { + return { ...state, alertMessage: action.payload }; + } + case 'ADD_NETWORK_MAPPING': + case 'DELETE_NETWORK_MAPPING': + case 'REPLACE_NETWORK_MAPPING': { + const updatedNetwork = action.payload.newState; + const dataChanged = hasPlanMappingsChanged( + state.planNetworkMaps.spec.map, + state.planStorageMaps.spec.map, + updatedNetwork, + state?.updatedStorage, + ); + + return { + ...state, + dataChanged, + alertMessage: null, + updatedNetwork, + }; + } + case 'ADD_STORAGE_MAPPING': + case 'DELETE_STORAGE_MAPPING': + case 'REPLACE_STORAGE_MAPPING': { + const updatedStorage = action.payload.newState; + const dataChanged = hasPlanMappingsChanged( + state.planNetworkMaps.spec.map, + state.planStorageMaps.spec.map, + state?.updatedNetwork, + updatedStorage, + ); + + return { + ...state, + dataChanged, + alertMessage: null, + updatedStorage, + }; + } + default: + return state; + } +} + export const PlanMappingsSection: React.FC = ({ plan, planNetworkMaps, @@ -82,84 +164,14 @@ export const PlanMappingsSection: React.FC = ({ targetNetworks, sourceStorages, targetStorages, + planMappingsState: state, + planMappingsDispatch: dispatch, }) => { const { t } = useForkliftTranslation(); - const initialState: PlanMappingsSectionState = { - edit: false, - dataChanged: false, - alertMessage: null, - updatedNetwork: planNetworkMaps?.spec?.map, - updatedStorage: planStorageMaps?.spec?.map, - }; - const [isLoading, setIsLoading] = useState(false); const [isAddNetworkMapAvailable, setIsAddNetworkMapAvailable] = useState(true); const [isAddStorageMapAvailable, setIsAddStorageMapAvailable] = useState(true); - const [state, dispatch] = useReducer(reducer, initialState); - - function reducer( - state: PlanMappingsSectionState, - action: { type: string; payload? }, - ): PlanMappingsSectionState { - switch (action.type) { - case 'TOGGLE_EDIT': { - return { ...state, edit: !state.edit }; - } - case 'SET_CANCEL': { - const dataChanged = false; - - return { - ...state, - dataChanged, - alertMessage: null, - updatedNetwork: planNetworkMaps?.spec?.map, - updatedStorage: planStorageMaps?.spec?.map, - }; - } - case 'SET_ALERT_MESSAGE': { - return { ...state, alertMessage: action.payload }; - } - case 'ADD_NETWORK_MAPPING': - case 'DELETE_NETWORK_MAPPING': - case 'REPLACE_NETWORK_MAPPING': { - const updatedNetwork = action.payload.newState; - const dataChanged = hasPlanMappingsChanged( - planNetworkMaps?.spec?.map, - planStorageMaps?.spec?.map, - updatedNetwork, - state?.updatedStorage, - ); - - return { - ...state, - dataChanged, - alertMessage: null, - updatedNetwork, - }; - } - case 'ADD_STORAGE_MAPPING': - case 'DELETE_STORAGE_MAPPING': - case 'REPLACE_STORAGE_MAPPING': { - const updatedStorage = action.payload.newState; - const dataChanged = hasPlanMappingsChanged( - planNetworkMaps?.spec?.map, - planStorageMaps?.spec?.map, - state?.updatedNetwork, - updatedStorage, - ); - - return { - ...state, - dataChanged, - alertMessage: null, - updatedStorage, - }; - } - default: - return state; - } - } // Toggles between view and edit modes function onToggleEdit() { @@ -648,31 +660,35 @@ export const PlanMappingsSection: React.FC = ({ return state.edit ? ( // Edit mode <> - - - - - - - - - - - {t( - 'Click the update mappings button to save your changes, button is disabled until a change is detected.', - )} - - - + {!state.editAction && ( + <> + + + + + + + + + + + {t( + 'Click the update mappings button to save your changes, button is disabled until a change is detected.', + )} + + + + + )} {state.alertMessage ? ( <> [ ]; const PageWithSelection = StandardPageWithSelection; +const PageWithExpansion = StandardPageWithExpansion; type PageWithSelectionProps = StandardPageWithSelectionProps; type PageGlobalActions = FC>[]; @@ -245,6 +248,7 @@ export const MigrationVirtualMachinesList: FC<{ obj: PlanData }> = ({ obj }) => const isExecuting = isPlanExecuting(plan); const isArchived = isPlanArchived(plan); + const someVMsMigrated = hasSomeCompleteRunningVMs(plan); // If plan executing and not archived (happens when archiving a running plan), allow to cancel vms, o/w remove from plan let actions: PageGlobalActions; @@ -255,9 +259,9 @@ export const MigrationVirtualMachinesList: FC<{ obj: PlanData }> = ({ obj }) => ), ]; } else { - actions = [ - ({ selectedIds }) => , - ]; + actions = someVMsMigrated + ? [({ selectedIds }) => ] + : [() => ]; } const canSelectWhenExecuting = (item: VMData) => @@ -288,5 +292,9 @@ export const MigrationVirtualMachinesList: FC<{ obj: PlanData }> = ({ obj }) => GlobalActionToolbarItems: actions, }; - return ; + return isExecuting || someVMsMigrated ? ( + + ) : ( + + ); }; diff --git a/packages/forklift-console-plugin/src/modules/Plans/views/details/tabs/VirtualMachines/Plan/PlanVirtualMachinesList.tsx b/packages/forklift-console-plugin/src/modules/Plans/views/details/tabs/VirtualMachines/Plan/PlanVirtualMachinesList.tsx index 4be029bea..4cd25e85f 100644 --- a/packages/forklift-console-plugin/src/modules/Plans/views/details/tabs/VirtualMachines/Plan/PlanVirtualMachinesList.tsx +++ b/packages/forklift-console-plugin/src/modules/Plans/views/details/tabs/VirtualMachines/Plan/PlanVirtualMachinesList.tsx @@ -1,18 +1,16 @@ import React, { FC } from 'react'; -import { - GlobalActionWithSelection, - StandardPageWithSelection, -} from 'src/components/page/StandardPageWithSelection'; +import { StandardPage } from 'src/components/page/StandardPage'; import { useForkliftTranslation } from 'src/utils/i18n'; import { loadUserSettings, ResourceFieldFactory } from '@kubev2v/common'; +import { GlobalActionToolbarProps } from '@kubev2v/common'; import { V1beta1PlanSpecVms, V1beta1PlanStatusConditions, V1beta1PlanStatusMigrationVms, } from '@kubev2v/types'; -import { PlanVMsDeleteButton } from '../components'; +import { PlanVMsEditButton } from '../components'; import { PlanData, VMData } from '../types'; import { PlanVirtualMachinesRow } from './PlanVirtualMachinesRow'; @@ -41,8 +39,8 @@ const fieldsMetadataFactory: ResourceFieldFactory = (t) => [ }, ]; -const PageWithSelection = StandardPageWithSelection; -type PageGlobalActions = FC>[]; +const PageWithNoSelection = StandardPage; +type PageGlobalActions = FC>[]; export const PlanVirtualMachinesList: FC<{ obj: PlanData }> = ({ obj }) => { const { t } = useForkliftTranslation(); @@ -79,17 +77,11 @@ export const PlanVirtualMachinesList: FC<{ obj: PlanData }> = ({ obj }) => { })); const vmDataSource: [VMData[], boolean, unknown] = [vmData || [], true, undefined]; const vmDataToId = (item: VMData) => item?.specVM?.id; - const canSelect = (item: VMData) => - item?.statusVM?.started === undefined || item?.statusVM?.error !== undefined; - const onSelect = () => undefined; - const initialSelectedIds = []; - const actions: PageGlobalActions = [ - ({ selectedIds }) => , - ]; + const actions: PageGlobalActions = [() => ]; return ( - = ({ obj }) => { namespace={''} page={1} toId={vmDataToId} - canSelect={canSelect} - onSelect={onSelect} - selectedIds={initialSelectedIds} GlobalActionToolbarItems={actions} /> ); diff --git a/packages/forklift-console-plugin/src/modules/Plans/views/details/tabs/VirtualMachines/components/PlanVMsEditButton.tsx b/packages/forklift-console-plugin/src/modules/Plans/views/details/tabs/VirtualMachines/components/PlanVMsEditButton.tsx new file mode 100644 index 000000000..0abee08b6 --- /dev/null +++ b/packages/forklift-console-plugin/src/modules/Plans/views/details/tabs/VirtualMachines/components/PlanVMsEditButton.tsx @@ -0,0 +1,44 @@ +import React, { FC } from 'react'; +import { isPlanEditable } from 'src/modules/Plans/utils'; +import { hasSomeCompleteRunningVMs } from 'src/modules/Plans/views/details/utils'; +import { useModal } from 'src/modules/Providers/modals'; +import { useForkliftTranslation } from 'src/utils/i18n'; + +import { V1beta1Plan } from '@kubev2v/types'; +import { Button, Tooltip } from '@patternfly/react-core'; + +import { PlanVMsEditModal } from '../modals'; + +export const PlanVMsEditButton: FC<{ + plan: V1beta1Plan; +}> = ({ plan }) => { + const { t } = useForkliftTranslation(); + const { showModal } = useModal(); + const planEditable = isPlanEditable(plan); + const someVMsMigrated = hasSomeCompleteRunningVMs(plan); + const disableEdit = someVMsMigrated || !planEditable; + + const onClick = () => { + showModal(); + }; + + return disableEdit ? ( + + + + ) : ( + + ); +}; diff --git a/packages/forklift-console-plugin/src/modules/Plans/views/details/tabs/VirtualMachines/components/index.ts b/packages/forklift-console-plugin/src/modules/Plans/views/details/tabs/VirtualMachines/components/index.ts index db0547464..88a035dbf 100644 --- a/packages/forklift-console-plugin/src/modules/Plans/views/details/tabs/VirtualMachines/components/index.ts +++ b/packages/forklift-console-plugin/src/modules/Plans/views/details/tabs/VirtualMachines/components/index.ts @@ -4,4 +4,5 @@ export * from './MigrationVMsCancelButton'; export * from './NameCellRenderer'; export * from './PlanVMsCellProps'; export * from './PlanVMsDeleteButton'; +export * from './PlanVMsEditButton'; // @endindex diff --git a/packages/forklift-console-plugin/src/modules/Plans/views/details/tabs/VirtualMachines/modals/PlanVMsEditModal.style.css b/packages/forklift-console-plugin/src/modules/Plans/views/details/tabs/VirtualMachines/modals/PlanVMsEditModal.style.css new file mode 100644 index 000000000..e88ce2baf --- /dev/null +++ b/packages/forklift-console-plugin/src/modules/Plans/views/details/tabs/VirtualMachines/modals/PlanVMsEditModal.style.css @@ -0,0 +1,3 @@ +.forklift-edit-modal { + overflow: auto; +} diff --git a/packages/forklift-console-plugin/src/modules/Plans/views/details/tabs/VirtualMachines/modals/PlanVMsEditModal.tsx b/packages/forklift-console-plugin/src/modules/Plans/views/details/tabs/VirtualMachines/modals/PlanVMsEditModal.tsx new file mode 100644 index 000000000..048913925 --- /dev/null +++ b/packages/forklift-console-plugin/src/modules/Plans/views/details/tabs/VirtualMachines/modals/PlanVMsEditModal.tsx @@ -0,0 +1,146 @@ +import React from 'react'; +import { PlanEditAction } from 'src/modules/Plans/utils/types/PlanEditAction'; +import { PlanEditPage } from 'src/modules/Plans/views/edit/PlanEditPage'; +import { useModal } from 'src/modules/Providers/modals'; +import { useInventoryVms } from 'src/modules/Providers/views'; +import { useForkliftTranslation } from 'src/utils/i18n'; + +import { + NetworkMapModelGroupVersionKind, + ProviderModelGroupVersionKind, + StorageMapModelGroupVersionKind, + V1beta1NetworkMap, + V1beta1Plan, + V1beta1Provider, + V1beta1StorageMap, +} from '@kubev2v/types'; +import { useK8sWatchResource } from '@openshift-console/dynamic-plugin-sdk'; +import { Modal, ModalVariant } from '@patternfly/react-core'; + +import './PlanVMsDeleteModal.style.css'; +import './PlanVMsEditModal.style.css'; + +export interface PlanVMsEditModalProps { + plan: V1beta1Plan; + editAction: PlanEditAction; +} + +export const PlanVMsEditModal: React.FC = ({ plan, editAction }) => { + const { toggleModal } = useModal(); + const { t } = useForkliftTranslation(); + const projectName = plan?.metadata?.namespace; + + // Retrieve k8s source provider + const [sourceProvider, sourceProviderLoaded, sourceProviderLoadError] = + useK8sWatchResource({ + groupVersionKind: ProviderModelGroupVersionKind, + namespaced: true, + name: plan?.spec?.provider?.source?.name, + namespace: plan?.spec?.provider?.source?.namespace, + }); + + // Retrieve k8s target provider + const [targetProvider, targetProviderLoaded, targetProviderLoadError] = + useK8sWatchResource({ + groupVersionKind: ProviderModelGroupVersionKind, + namespaced: true, + name: plan?.spec?.provider?.destination?.name, + namespace: plan?.spec?.provider?.destination?.namespace, + }); + + const [providers, providersLoaded, providersLoadError] = useK8sWatchResource({ + groupVersionKind: ProviderModelGroupVersionKind, + namespaced: true, + isList: true, + namespace: plan?.metadata?.namespace, + }); + + // Retrieve all k8s Network Mappings + const [networkMaps, networkMapsLoaded, networkMapsError] = useK8sWatchResource< + V1beta1NetworkMap[] + >({ + groupVersionKind: NetworkMapModelGroupVersionKind, + namespaced: true, + isList: true, + namespace: plan?.metadata?.namespace, + }); + + // Retrieve all k8s Storage Mappings + const [storageMaps, storageMapsLoaded, storageMapsError] = useK8sWatchResource< + V1beta1StorageMap[] + >({ + groupVersionKind: StorageMapModelGroupVersionKind, + namespaced: true, + isList: true, + namespace: plan?.metadata?.namespace, + }); + + const [vmData] = useInventoryVms( + { provider: sourceProvider }, + sourceProviderLoaded, + sourceProviderLoadError, + ); + const initialSelectedIds = plan.spec.vms.map((specVm) => specVm.id); + const selectedVMs = vmData.filter((vm) => initialSelectedIds.includes(vm.vm.id)); + + const planNetworkMaps = networkMaps + ? networkMaps.find((net) => net?.metadata?.name === plan?.spec?.map?.network?.name) + : null; + const planStorageMaps = storageMaps + ? storageMaps.find((storage) => storage?.metadata?.name === plan.spec.map?.storage?.name) + : null; + + const finishedLoading = + providersLoaded && + sourceProviderLoaded && + targetProviderLoaded && + networkMapsLoaded && + storageMapsLoaded && + vmData.length > 0; + const hasErrors = + providersLoadError || + sourceProviderLoadError || + targetProviderLoadError || + networkMapsError || + storageMapsError; + + return ( + + {hasErrors && ( +
+ + {t( + 'Something is wrong, the data was not loaded due to an error, please try to reload the page.', + )} + +
+ )} + {!hasErrors && finishedLoading ? ( + + ) : ( +
+ {t('Data is loading, please wait.')} +
+ )} +
+ ); +}; diff --git a/packages/forklift-console-plugin/src/modules/Plans/views/details/tabs/VirtualMachines/modals/index.ts b/packages/forklift-console-plugin/src/modules/Plans/views/details/tabs/VirtualMachines/modals/index.ts index d40f0e3ef..3af1d27c3 100644 --- a/packages/forklift-console-plugin/src/modules/Plans/views/details/tabs/VirtualMachines/modals/index.ts +++ b/packages/forklift-console-plugin/src/modules/Plans/views/details/tabs/VirtualMachines/modals/index.ts @@ -2,4 +2,5 @@ export * from './MigrationVMsCancelModal'; export * from './PipelineTasksModal'; export * from './PlanVMsDeleteModal'; +export * from './PlanVMsEditModal'; // @endindex diff --git a/packages/forklift-console-plugin/src/modules/Plans/views/details/utils/patchPlanMappingsData.ts b/packages/forklift-console-plugin/src/modules/Plans/views/details/utils/patchPlanMappingsData.ts index 7a254bfda..9c02fd2a1 100644 --- a/packages/forklift-console-plugin/src/modules/Plans/views/details/utils/patchPlanMappingsData.ts +++ b/packages/forklift-console-plugin/src/modules/Plans/views/details/utils/patchPlanMappingsData.ts @@ -23,7 +23,7 @@ export async function patchPlanMappingsData( updatedNetwork: V1beta1NetworkMapSpecMap[], updatedStorage: V1beta1StorageMapSpecMap[], ) { - await k8sPatch({ + const updatedNetworkMap = await k8sPatch({ model: NetworkMapModel, resource: planNetworkMaps, data: [ @@ -35,7 +35,7 @@ export async function patchPlanMappingsData( ], }); - await k8sPatch({ + const updatedStorageMap = await k8sPatch({ model: StorageMapModel, resource: planStorageMaps, data: [ @@ -46,6 +46,11 @@ export async function patchPlanMappingsData( }, ], }); + + return { + updatedNetworkMap, + updatedStorageMap, + }; } /** diff --git a/packages/forklift-console-plugin/src/modules/Plans/views/edit/PlanEditPage.tsx b/packages/forklift-console-plugin/src/modules/Plans/views/edit/PlanEditPage.tsx new file mode 100644 index 000000000..910491d5c --- /dev/null +++ b/packages/forklift-console-plugin/src/modules/Plans/views/edit/PlanEditPage.tsx @@ -0,0 +1,170 @@ +import React, { useEffect, useReducer } from 'react'; +import { useHistory } from 'react-router'; +import { PlanEditAction } from 'src/modules/Plans/utils/types/PlanEditAction'; +import { + planMappingsSectionReducer, + PlanMappingsSectionState, +} from 'src/modules/Plans/views/details/tabs/Mappings/PlanMappingsSection'; +import { VmData } from 'src/modules/Providers/views/details/tabs/VirtualMachines/components/VMCellProps'; +import ProvidersUpdateVmMigrationPage from 'src/modules/Providers/views/migrate/ProvidersUpdateVmMigrationPage'; +import { startUpdate } from 'src/modules/Providers/views/migrate/reducer/actions'; +import { useFetchEffects } from 'src/modules/Providers/views/migrate/useFetchEffects'; +import { useUpdateEffect } from 'src/modules/Providers/views/migrate/useUpdateEffect'; +import { useForkliftTranslation } from 'src/utils/i18n'; + +import { V1beta1NetworkMap, V1beta1Plan, V1beta1Provider, V1beta1StorageMap } from '@kubev2v/types'; +import { PageSection, Title } from '@patternfly/react-core'; +import { Wizard } from '@patternfly/react-core/deprecated'; + +import { findProviderByID } from '../create/components'; +import { planCreatePageInitialState, planCreatePageReducer } from '../create/states'; +import { SelectSourceProvider } from '../create/steps'; + +import '../create/PlanCreatePage.style.css'; + +export const PlanEditPage: React.FC<{ + plan: V1beta1Plan; + providers: V1beta1Provider[]; + sourceProvider: V1beta1Provider; + targetProvider: V1beta1Provider; + projectName: string; + onClose?: () => void; + selectedVMs?: VmData[]; + editAction: PlanEditAction; + planNetworkMaps: V1beta1NetworkMap; + planStorageMaps: V1beta1StorageMap; +}> = ({ + plan, + providers, + sourceProvider, + targetProvider, + projectName, + onClose, + selectedVMs, + editAction, + planNetworkMaps, + planStorageMaps, +}) => { + const { t } = useForkliftTranslation(); + const history = useHistory(); + const startAtStep = 1; + + // Init Select source provider form state + const [filterState, filterDispatch] = useReducer(planCreatePageReducer, { + ...planCreatePageInitialState, + selectedProviderUID: sourceProvider.metadata.uid, + selectedVMs: selectedVMs, + }); + + const selectedProvider = + filterState.selectedProviderUID !== '' + ? findProviderByID(filterState.selectedProviderUID, providers) + : undefined; + + const [state, dispatch, emptyContext] = useFetchEffects({ + data: { + selectedVms: filterState.selectedVMs, + provider: selectedProvider, + targetProvider, + plan, + editAction, + }, + }); + + const initialPlanMappingsState: PlanMappingsSectionState = { + edit: true, + dataChanged: false, + alertMessage: null, + updatedNetwork: planNetworkMaps?.spec?.map || [], + updatedStorage: planStorageMaps?.spec?.map || [], + planNetworkMaps: planNetworkMaps, + planStorageMaps: planStorageMaps, + editAction, + }; + + const [planMappingsState, planMappingsDispatch] = useReducer( + planMappingsSectionReducer, + initialPlanMappingsState, + ); + + useEffect(() => { + if (planNetworkMaps && planStorageMaps) { + planMappingsDispatch({ + type: 'SET_PLAN_MAPS', + payload: { planNetworkMaps, planStorageMaps }, + }); + } + }, [planNetworkMaps, planStorageMaps]); + + useUpdateEffect(state, dispatch, planMappingsState); + + const steps = [ + { + id: 'step-1', + name: editAction === 'VMS' ? t('Select virtual machines') : t('Select source provider'), + component: ( + + ), + enableNext: filterState?.selectedVMs?.length > 0, + }, + { + id: 'step-2', + name: editAction === 'VMS' ? t('Update mappings') : t('Update migration plan'), + component: ( + + ), + enableNext: + !emptyContext && + !( + !!state?.flow?.apiError || + Object.values(state?.validation || []).some((validation) => validation === 'error') + ), + canJumpTo: filterState?.selectedVMs?.length > 0, + nextButtonText: + editAction === 'VMS' ? t('Update virtual machines') : t('Update migration plan'), + }, + ]; + + const goBack = () => history.goBack(); + const title = 'Plans wizard'; + return ( + <> + + + {editAction === 'VMS' ? 'Update virtual machines' : 'Update migration plan'} + + + + + dispatch(startUpdate())} + onClose={onClose || goBack} + startAtStep={startAtStep} + /> + + + ); +}; + +export default PlanEditPage; diff --git a/packages/forklift-console-plugin/src/modules/Plans/views/list/PlanRow.tsx b/packages/forklift-console-plugin/src/modules/Plans/views/list/PlanRow.tsx index 127d712fb..afa782c19 100644 --- a/packages/forklift-console-plugin/src/modules/Plans/views/list/PlanRow.tsx +++ b/packages/forklift-console-plugin/src/modules/Plans/views/list/PlanRow.tsx @@ -12,8 +12,8 @@ import { CellProps, NamespaceCell, PlanCell, + PlanStatusCell, ProviderLinkCell, - StatusCell, VMsCell, } from './components'; @@ -47,7 +47,7 @@ const cellRenderers: Record> = { }, ['destination']: ProviderLinkCell, ['source']: ProviderLinkCell, - ['phase']: StatusCell, + ['phase']: PlanStatusCell, ['vms']: VMsCell, ['description']: ({ data }: CellProps) => {data?.obj?.spec?.description}, ['actions']: ActionsCell, diff --git a/packages/forklift-console-plugin/src/modules/Plans/views/list/PlansListPage.tsx b/packages/forklift-console-plugin/src/modules/Plans/views/list/PlansListPage.tsx index 814b510ae..ff932b781 100644 --- a/packages/forklift-console-plugin/src/modules/Plans/views/list/PlansListPage.tsx +++ b/packages/forklift-console-plugin/src/modules/Plans/views/list/PlansListPage.tsx @@ -74,12 +74,12 @@ export const fieldsMetadataFactory: ResourceFieldFactory = (t) => [ { resourceFieldId: 'phase', jsonPath: getPlanPhase, - label: t('Status'), + label: t('Migration status'), isVisible: true, filter: { type: 'enum', primary: true, - placeholderLabel: t('Status'), + placeholderLabel: t('Migration status'), values: planPhases, }, sortable: true, diff --git a/packages/forklift-console-plugin/src/modules/Plans/views/list/components/ActionsCell.tsx b/packages/forklift-console-plugin/src/modules/Plans/views/list/components/ActionsCell.tsx index 8a9fe3527..c9bcd791f 100644 --- a/packages/forklift-console-plugin/src/modules/Plans/views/list/components/ActionsCell.tsx +++ b/packages/forklift-console-plugin/src/modules/Plans/views/list/components/ActionsCell.tsx @@ -1,20 +1,12 @@ import React from 'react'; import { PlanActionsDropdown } from 'src/modules/Plans/actions'; -import { PlanCutoverMigrationModal, PlanStartMigrationModal } from 'src/modules/Plans/modals'; -import { - canPlanReStart, - canPlanStart, - isPlanArchived, - isPlanExecuting, -} from 'src/modules/Plans/utils'; +import { PlanCutoverMigrationModal } from 'src/modules/Plans/modals'; +import { isPlanArchived, isPlanExecuting } from 'src/modules/Plans/utils'; import { useModal } from 'src/modules/Providers/modals'; import { useForkliftTranslation } from 'src/utils/i18n'; -import { PlanModel } from '@kubev2v/types'; import { Button, Flex, FlexItem } from '@patternfly/react-core'; import CutoverIcon from '@patternfly/react-icons/dist/esm/icons/migration-icon'; -import StartIcon from '@patternfly/react-icons/dist/esm/icons/play-icon'; -import ReStartIcon from '@patternfly/react-icons/dist/esm/icons/redo-icon'; import { CellProps } from './CellProps'; @@ -23,46 +15,20 @@ export const ActionsCell = ({ data }: CellProps) => { const { showModal } = useModal(); const plan = data.obj; - - const canStart = canPlanStart(plan); - const canReStart = canPlanReStart(plan); - const isWarmAndExecuting = plan?.spec?.warm && isPlanExecuting(plan); const isArchived = isPlanArchived(plan); - const buttonStartLabel = canReStart ? t('Restart') : t('Start'); - const buttonStartIcon = canReStart ? : ; - const buttonCutoverIcon = ; - - const onClickPlanStartMigration = () => { - showModal( - , - ); - }; - const onClickPlanCutoverMigration = () => { - showModal(); + showModal(); }; return ( - {canStart && ( - - - - )} - {isWarmAndExecuting && !isArchived && ( - diff --git a/packages/forklift-console-plugin/src/modules/Plans/views/list/components/ErrorStatusCell.tsx b/packages/forklift-console-plugin/src/modules/Plans/views/list/components/ErrorStatusCell.tsx index 910718e1b..f5ccc91c0 100644 --- a/packages/forklift-console-plugin/src/modules/Plans/views/list/components/ErrorStatusCell.tsx +++ b/packages/forklift-console-plugin/src/modules/Plans/views/list/components/ErrorStatusCell.tsx @@ -1,7 +1,7 @@ import React from 'react'; import Linkify from 'react-linkify'; import { Link } from 'react-router-dom'; -import { getPhaseLabel, getPlanPhase } from 'src/modules/Plans/utils'; +import { getPlanPhase } from 'src/modules/Plans/utils'; import { getResourceUrl, TableIconCell } from 'src/modules/Providers/utils'; import { useForkliftTranslation } from 'src/utils/i18n'; @@ -16,7 +16,7 @@ export const ErrorStatusCell: React.FC = ({ data }) => { const { obj: plan } = data; const phase = getPlanPhase(data); - const phaseLabel = getPhaseLabel(phase); + const phaseLabel: string = phase; const planURL = getResourceUrl({ reference: PlanModelRef, diff --git a/packages/forklift-console-plugin/src/modules/Plans/views/list/components/PlanStatusCell.tsx b/packages/forklift-console-plugin/src/modules/Plans/views/list/components/PlanStatusCell.tsx new file mode 100644 index 000000000..0aba151a9 --- /dev/null +++ b/packages/forklift-console-plugin/src/modules/Plans/views/list/components/PlanStatusCell.tsx @@ -0,0 +1,132 @@ +import React from 'react'; +import { usePlanMigration } from 'src/modules/Plans/hooks'; +import { PlanStartMigrationModal } from 'src/modules/Plans/modals'; +import { + getMigrationVmsCounts, + getPlanPhase, + isPlanArchived, + isPlanExecuting, + PlanPhase, +} from 'src/modules/Plans/utils'; +import { useModal } from 'src/modules/Providers/modals'; +import { getResourceUrl } from 'src/modules/Providers/utils'; +import { useForkliftTranslation } from 'src/utils/i18n'; + +import { PlanModel, PlanModelRef } from '@kubev2v/types'; +import { Button, Flex, FlexItem, Label, Spinner, Split, SplitItem } from '@patternfly/react-core'; +import StartIcon from '@patternfly/react-icons/dist/esm/icons/play-icon'; + +import { CellProps } from './CellProps'; +import { PlanStatusVmCount } from './PlanStatusVmCount'; + +type VmPipelineTask = { + vmName: string; + task: string; + status: string; +}; + +export const PlanStatusCell: React.FC = ({ data }) => { + const { t } = useForkliftTranslation(); + const { showModal } = useModal(); + const plan = data?.obj; + + const vms = plan?.spec?.vms; + const vmStatuses = plan?.status?.migration?.vms; + const [lastMigration] = usePlanMigration(plan); + + const isWarmAndExecuting = plan.spec?.warm && isPlanExecuting(plan); + const isWaitingForCutover = isWarmAndExecuting && !isPlanArchived(plan); + + const vmPipelineTasks = lastMigration?.status.vms?.reduce( + (acc: VmPipelineTask[], migrationVm) => { + migrationVm.pipeline.forEach((pipelineStep) => { + acc.push({ vmName: migrationVm.name, task: pipelineStep.name, status: pipelineStep.phase }); + }); + + return acc; + }, + [], + ); + + const phase = getPlanPhase(data); + const isPlanLoading = + !isWaitingForCutover && (phase === PlanPhase.Running || phase === PlanPhase.Archiving); + const planURL = getResourceUrl({ + reference: PlanModelRef, + name: plan?.metadata?.name, + namespace: plan?.metadata?.namespace, + }); + + // All VM count links point to the same place for now, + // but will be updated to target only affected VMs in the future. + // Could possibly use a querystring to dictate a table filter for the list of VMs. + const vmCountLinkPath = `${planURL}/vms`; + + if (phase === PlanPhase.Ready) { + return ( + + ); + } + + const vmCount = getMigrationVmsCounts(vmStatuses); + const completedVmPipelineTasks = vmPipelineTasks?.filter( + (pipelineTask) => pipelineTask.status === 'Completed', + ); + const progressValue = vmPipelineTasks?.length + ? (100 * completedVmPipelineTasks.length) / vmPipelineTasks.length + : 0; + + return ( + + {isPlanLoading ? ( + + ) : phase === PlanPhase.NotReady ? ( + t('Validating...') + ) : ( + + )} + + {progressValue !== 0 && isPlanLoading && ( + {Math.trunc(progressValue)}% + )} + + + {vmCount?.success > 0 && ( + + + + )} + + {phase !== PlanPhase.Running && + phase !== PlanPhase.NotReady && + vms?.length && + !vmCount?.error && + !vmCount.success && ( + + + + )} + + {vmCount?.error > 0 && ( + + + + )} + + + ); +}; diff --git a/packages/forklift-console-plugin/src/modules/Plans/views/list/components/PlanStatusVmCount.tsx b/packages/forklift-console-plugin/src/modules/Plans/views/list/components/PlanStatusVmCount.tsx new file mode 100644 index 000000000..0fcfe21ff --- /dev/null +++ b/packages/forklift-console-plugin/src/modules/Plans/views/list/components/PlanStatusVmCount.tsx @@ -0,0 +1,43 @@ +import React from 'react'; +import { Link } from 'react-router-dom'; +import { useForkliftTranslation } from 'src/utils'; + +import { Flex, FlexItem, Icon, IconComponentProps } from '@patternfly/react-core'; +import CheckCircleIcon from '@patternfly/react-icons/dist/esm/icons/check-circle-icon'; +import ExclamationCircleIcon from '@patternfly/react-icons/dist/esm/icons/exclamation-circle-icon'; +import ExclamationTriangleIcon from '@patternfly/react-icons/dist/esm/icons/exclamation-triangle-icon'; + +interface PlanStatusVmCountProps { + count: number; + status: IconComponentProps['status']; + linkPath: string; +} + +export const PlanStatusVmCount: React.FC = ({ + count, + status, + linkPath, +}) => { + const { t } = useForkliftTranslation(); + + const statusIcon = React.useMemo(() => { + switch (status) { + case 'success': + return ; + case 'warning': + return ; + case 'danger': + return ; + } + }, [status]); + + return ( + + {statusIcon} + + + {t('{{total}} VM', { count, total: count })} + + + ); +}; diff --git a/packages/forklift-console-plugin/src/modules/Plans/views/list/components/StatusCell.tsx b/packages/forklift-console-plugin/src/modules/Plans/views/list/components/StatusCell.tsx index 2a53170e7..ce68a01da 100644 --- a/packages/forklift-console-plugin/src/modules/Plans/views/list/components/StatusCell.tsx +++ b/packages/forklift-console-plugin/src/modules/Plans/views/list/components/StatusCell.tsx @@ -1,5 +1,5 @@ import React from 'react'; -import { getPhaseLabel, getPlanPhase } from 'src/modules/Plans/utils'; +import { getPlanPhase, PlanPhase } from 'src/modules/Plans/utils'; import { TableIconCell } from 'src/modules/Providers/utils'; import { CellProps } from './CellProps'; @@ -11,17 +11,17 @@ export const StatusCell: React.FC = (props) => { const { data } = props; const phase = getPlanPhase(data); - const phaseLabel = getPhaseLabel(phase); + const phaseLabel: string = phase; switch (phase) { - case 'Error': - case 'Warning': + case PlanPhase.Error: + case PlanPhase.Warning: return ErrorStatusCell(props); - case 'Failed': - case 'Canceled': - case 'Running': - case 'Succeeded': - case 'vmError': + case PlanPhase.Failed: + case PlanPhase.Canceled: + case PlanPhase.Running: + case PlanPhase.Succeeded: + case PlanPhase.vmError: return VMsProgressCell(props); } diff --git a/packages/forklift-console-plugin/src/modules/Plans/views/list/components/VMsCell.tsx b/packages/forklift-console-plugin/src/modules/Plans/views/list/components/VMsCell.tsx index 4075a312b..a287dedd5 100644 --- a/packages/forklift-console-plugin/src/modules/Plans/views/list/components/VMsCell.tsx +++ b/packages/forklift-console-plugin/src/modules/Plans/views/list/components/VMsCell.tsx @@ -11,13 +11,13 @@ import { CellProps } from './CellProps'; export const VMsCell: React.FC = ({ data }) => { const { t } = useForkliftTranslation(); - - const specVms = data?.obj?.spec?.vms; + const plan = data?.obj; + const specVms = plan?.spec?.vms; const planURL = getResourceUrl({ reference: PlanModelRef, - name: data?.obj?.metadata?.name, - namespace: data?.obj?.metadata?.namespace, + name: plan?.metadata?.name, + namespace: plan?.metadata?.namespace, }); return ( @@ -26,7 +26,9 @@ export const VMsCell: React.FC = ({ data }) => { - {t('{{total}} VMs', { total: specVms?.length })} + + {t('{{total}} VM', { count: specVms?.length, total: specVms?.length })} + ); diff --git a/packages/forklift-console-plugin/src/modules/Plans/views/list/components/VMsProgressCell.tsx b/packages/forklift-console-plugin/src/modules/Plans/views/list/components/VMsProgressCell.tsx index a566d8978..b7c48cdbf 100644 --- a/packages/forklift-console-plugin/src/modules/Plans/views/list/components/VMsProgressCell.tsx +++ b/packages/forklift-console-plugin/src/modules/Plans/views/list/components/VMsProgressCell.tsx @@ -2,7 +2,6 @@ import React from 'react'; import { Link } from 'react-router-dom'; import { getMigrationVmsCounts, - getPhaseLabel, getPlanPhase, getPlanProgressVariant, MigrationVmsCounts, @@ -52,7 +51,7 @@ export const VMsProgressCell: React.FC = ({ data }) => { const vms = data?.obj?.status?.migration?.vms; const phase = getPlanPhase(data); - const phaseLabel = t(getPhaseLabel(phase)); + const phaseLabel: string = phase; const planURL = getResourceUrl({ reference: PlanModelRef, diff --git a/packages/forklift-console-plugin/src/modules/Plans/views/list/components/index.ts b/packages/forklift-console-plugin/src/modules/Plans/views/list/components/index.ts index 3f8035961..f95b2466e 100644 --- a/packages/forklift-console-plugin/src/modules/Plans/views/list/components/index.ts +++ b/packages/forklift-console-plugin/src/modules/Plans/views/list/components/index.ts @@ -5,9 +5,8 @@ export * from './ErrorStatusCell'; export * from './NamespaceCell'; export * from './NetworkMapLinkCell'; export * from './PlanCell'; +export * from './PlanStatusCell'; export * from './PlanStatusIcon'; export * from './ProviderLinkCell'; -export * from './StatusCell'; export * from './VMsCell'; -export * from './VMsProgressCell'; // @endindex 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 86fe094b3..d572fe78e 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<{