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 a373443b3..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,13 +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; + state: CreateVmMigrationPageState; + dispatch: React.Dispatch>; + filterDispatch: React.Dispatch; hideProviderSection?: boolean; -}> = ({ filterState, filterDispatch, providers, selectedProvider, hideProviderSection }) => { +}> = ({ + 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) @@ -42,6 +55,9 @@ export const SelectSourceProvider: React.FC<{ providers={filteredProviders} filterState={filterState} filterDispatch={filterDispatch} + dispatch={dispatch} + state={state} + projectName={projectName} /> )} 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/VirtualMachines/modals/PlanVMsEditModal.tsx b/packages/forklift-console-plugin/src/modules/Plans/views/details/tabs/VirtualMachines/modals/PlanVMsEditModal.tsx index 279b1c8af..048913925 100644 --- 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 @@ -14,7 +14,6 @@ import { V1beta1Provider, V1beta1StorageMap, } from '@kubev2v/types'; -import { useActiveNamespace } from '@openshift-console/dynamic-plugin-sdk'; import { useK8sWatchResource } from '@openshift-console/dynamic-plugin-sdk'; import { Modal, ModalVariant } from '@patternfly/react-core'; @@ -28,8 +27,8 @@ export interface PlanVMsEditModalProps { export const PlanVMsEditModal: React.FC = ({ plan, editAction }) => { const { toggleModal } = useModal(); - const [activeNamespace] = useActiveNamespace(); const { t } = useForkliftTranslation(); + const projectName = plan?.metadata?.namespace; // Retrieve k8s source provider const [sourceProvider, sourceProviderLoaded, sourceProviderLoadError] = @@ -127,7 +126,7 @@ export const PlanVMsEditModal: React.FC = ({ plan, editAc {!hasErrors && finishedLoading ? ( void; selectedVMs?: VmData[]; editAction: PlanEditAction; @@ -38,7 +38,7 @@ export const PlanEditPage: React.FC<{ providers, sourceProvider, targetProvider, - namespace, + projectName, onClose, selectedVMs, editAction, @@ -104,9 +104,11 @@ export const PlanEditPage: React.FC<{ name: editAction === 'VMS' ? t('Select virtual machines') : t('Select source provider'), component: ( = ({ 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 index b023cd402..0aba151a9 100644 --- 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 @@ -6,6 +6,7 @@ import { getPlanPhase, isPlanArchived, isPlanExecuting, + PlanPhase, } from 'src/modules/Plans/utils'; import { useModal } from 'src/modules/Providers/modals'; import { getResourceUrl } from 'src/modules/Providers/utils'; @@ -48,7 +49,8 @@ export const PlanStatusCell: React.FC = ({ data }) => { ); const phase = getPlanPhase(data); - const isPlanLoading = !isWaitingForCutover && (phase === 'Running' || phase === 'Archiving'); + const isPlanLoading = + !isWaitingForCutover && (phase === PlanPhase.Running || phase === PlanPhase.Archiving); const planURL = getResourceUrl({ reference: PlanModelRef, name: plan?.metadata?.name, @@ -60,7 +62,7 @@ export const PlanStatusCell: React.FC = ({ data }) => { // Could possibly use a querystring to dictate a table filter for the list of VMs. const vmCountLinkPath = `${planURL}/vms`; - if (phase === 'Ready') { + if (phase === PlanPhase.Ready) { return ( + + } + size={ProgressSize.sm} + measureLocation={ProgressMeasureLocation.top} + variant={progressVariant} + /> + + ); +}; 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<{