Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[MTV-1769] Add Project field to migration plan/provider wizards #1409

Merged
merged 1 commit into from
Dec 18, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
425 changes: 425 additions & 0 deletions packages/common/src/components/TypeaheadSelect/TypeaheadSelect.tsx

Large diffs are not rendered by default.

3 changes: 3 additions & 0 deletions packages/common/src/components/TypeaheadSelect/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
// @index(['./*', /__/g], f => `export * from '${f.path}';`)
export * from './TypeaheadSelect';
// @endindex
1 change: 1 addition & 0 deletions packages/common/src/components/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,4 +10,5 @@ export * from './LoadingDots';
export * from './Page';
export * from './QueryClientHoc';
export * from './TableView';
export * from './TypeaheadSelect';
// @endindex
Original file line number Diff line number Diff line change
Expand Up @@ -154,7 +154,6 @@
"Edit migration plan transfer network": "Edit migration plan transfer network",
"Edit NetworkMap": "Edit NetworkMap",
"Edit Plan": "Edit Plan",
"Edit plan name": "Edit plan name",
"Edit Precopy interval (minutes)": "Edit Precopy interval (minutes)",
"Edit Provider": "Edit Provider",
"Edit Provider Credentials": "Edit Provider Credentials",
Expand Down Expand Up @@ -264,11 +263,9 @@
"Multiple NICs on the same network": "Multiple NICs on the same network",
"Name": "Name",
"Name is primarily intended for creation idempotence and configuration definition. Cannot be updated.": "Name is primarily intended for creation idempotence and configuration definition. Cannot be updated.",
"Name is required and must be a unique within a namespace and valid Kubernetes name.": "Name is required and must be a unique within a namespace and valid Kubernetes name.",
"Namespace": "Namespace",
"Namespace defines the space within which each name must be unique.\n An empty namespace is equivalent to the \"default\" namespace, but \"default\" is the canonical representation.\n Not all objects are required to be scoped to a namespace - the value of this field for those objects will be empty.": "Namespace defines the space within which each name must be unique.\n An empty namespace is equivalent to the \"default\" namespace, but \"default\" is the canonical representation.\n Not all objects are required to be scoped to a namespace - the value of this field for those objects will be empty.",
"Namespace defines the space within which each name must be unique.\n An empty namespace is equivalent to the \"default\" namespace, but \"default\" is the canonical representation.\n Not all objects are required to be scoped to a namespace -\n the value of this field for those objects will be empty.": "Namespace defines the space within which each name must be unique.\n An empty namespace is equivalent to the \"default\" namespace, but \"default\" is the canonical representation.\n Not all objects are required to be scoped to a namespace -\n the value of this field for those objects will be empty.",
"Namespace is not defined": "Namespace is not defined",
"Network for data transfer": "Network for data transfer",
"Network interfaces": "Network interfaces",
"Network Map name re-generated": "Network Map name re-generated",
Expand Down Expand Up @@ -386,6 +383,7 @@
"Provider details": "Provider details",
"Provider inventory": "Provider inventory",
"Provider resource name": "Provider resource name",
"Provider type": "Provider type",
"Provider web UI link": "Provider web UI link",
"Provider YAML": "Provider YAML",
"Providers": "Providers",
Expand Down Expand Up @@ -416,7 +414,6 @@
"Select a namespace": "Select a namespace",
"Select a provider": "Select a provider",
"Select migration network": "Select migration network",
"Select provider type": "Select provider type",
"Select source provider": "Select source provider",
"Select virtual machines": "Select virtual machines",
"Select vSphere provider endpoint type.": "Select vSphere provider endpoint type.",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,10 @@ import ProvidersCreateVmMigrationPage from 'src/modules/Providers/views/migrate/
import { startCreate } from 'src/modules/Providers/views/migrate/reducer/actions';
import { useFetchEffects } from 'src/modules/Providers/views/migrate/useFetchEffects';
import { useSaveEffect } from 'src/modules/Providers/views/migrate/useSaveEffect';
import { ForkliftTrans } from 'src/utils/i18n';

import { ProviderModelGroupVersionKind, V1beta1Provider } from '@kubev2v/types';
import { useK8sWatchResource } from '@openshift-console/dynamic-plugin-sdk';
import { Alert, PageSection, Title } from '@patternfly/react-core';
import { useActiveNamespace, useK8sWatchResource } from '@openshift-console/dynamic-plugin-sdk';
import { PageSection, Title } from '@patternfly/react-core';
import { Wizard } from '@patternfly/react-core/deprecated';

import { findProviderByID } from './components';
Expand All @@ -22,8 +21,13 @@ export const PlanCreatePage: React.FC<{ namespace: string }> = ({ namespace }) =
// Get optional initial state context
const { data } = useCreateVmMigrationData();
const history = useHistory();
const defaultNamespace = process?.env?.DEFAULT_NAMESPACE || 'default';
const startAtStep = data?.provider !== undefined ? 2 : 1;
const [activeNamespace, setActiveNamespace] = useActiveNamespace();
const defaultNamespace = process?.env?.DEFAULT_NAMESPACE || 'default';
const projectName =
data?.projectName ||
(activeNamespace === '#ALL_NS#' ? 'openshift-mtv' : activeNamespace) ||
defaultNamespace;

// Init Select source provider form state
const [filterState, filterDispatch] = useReducer(planCreatePageReducer, {
Expand All @@ -38,14 +42,20 @@ export const PlanCreatePage: React.FC<{ namespace: string }> = ({ namespace }) =
isList: true,
namespace,
});

const selectedProvider =
filterState.selectedProviderUID !== ''
? findProviderByID(filterState.selectedProviderUID, providers)
: undefined;

// Init Create migration plan form state
const [state, dispatch, emptyContext] = useFetchEffects({
data: { selectedVms: filterState.selectedVMs, provider: selectedProvider || data?.provider },
data: {
projectName,
selectedVms: filterState.selectedVMs,
provider: selectedProvider || data?.provider,
planName: data?.planName,
},
});
useSaveEffect(state, dispatch);

Expand All @@ -55,9 +65,11 @@ export const PlanCreatePage: React.FC<{ namespace: string }> = ({ namespace }) =
name: 'Select source provider',
component: (
<SelectSourceProvider
namespace={namespace}
projectName={projectName}
filterState={filterState}
filterDispatch={filterDispatch}
dispatch={dispatch}
state={state}
providers={providers}
selectedProvider={selectedProvider}
/>
Expand Down Expand Up @@ -89,21 +101,6 @@ export const PlanCreatePage: React.FC<{ namespace: string }> = ({ namespace }) =
return (
<>
<PageSection variant="light">
{!namespace && (
<Alert
className="co-alert forklift--create-plan--alert"
isInline
variant="warning"
title={'Namespace is not defined'}
>
<ForkliftTrans>
This plan will be created in <strong>{defaultNamespace}</strong> namespace, if you
wish to choose another namespace please cancel, and choose a namespace from the top
bar.
</ForkliftTrans>
</Alert>
)}

<Title headingLevel="h2">{'Create migration plan'}</Title>
</PageSection>

Expand All @@ -113,7 +110,10 @@ export const PlanCreatePage: React.FC<{ namespace: string }> = ({ namespace }) =
navAriaLabel={`${title} steps`}
mainAriaLabel={`${title} content`}
steps={steps}
onSave={() => dispatch(startCreate())}
onSave={() => {
setActiveNamespace(state.underConstruction.projectName);
dispatch(startCreate());
}}
onClose={() => history.goBack()}
startAtStep={startAtStep}
/>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,14 @@ import React from 'react';
import { SelectableCard } from 'src/modules/Providers/utils/components/Gallery/SelectableCard';
import { SelectableGallery } from 'src/modules/Providers/utils/components/Gallery/SelectableGallery';
import { VmData } from 'src/modules/Providers/views';
import { useForkliftTranslation } from 'src/utils';
import { useCreateVmMigrationData } from 'src/modules/Providers/views/migrate';
import {
PageAction,
setPlanName,
setProjectName as setProjectNameAction,
} from 'src/modules/Providers/views/migrate/reducer/actions';
import { CreateVmMigrationPageState } from 'src/modules/Providers/views/migrate/types';
import { ForkliftTrans, useForkliftTranslation } from 'src/utils';

import { FormGroupWithHelpText } from '@kubev2v/common';
import { V1beta1Provider } from '@kubev2v/types';
Expand All @@ -13,14 +20,19 @@ import { PlanCreatePageState } from '../states';
import { ChipsToolbarProviders } from './ChipsToolbarProviders';
import { createProviderCardItems } from './createProviderCardItems';
import { FiltersToolbarProviders } from './FiltersToolbarProviders';
import { PlanNameTextField } from './PlanNameTextField';
import { ProjectNameSelect } from './ProjectNameSelect';

export type PlanCreateFormProps = {
providers: V1beta1Provider[];
filterState: PlanCreatePageState;
state: CreateVmMigrationPageState;
projectName: string;
filterDispatch: React.Dispatch<{
type: string;
payload?: string | string[] | VmData[];
}>;
dispatch: (action: PageAction<unknown, unknown>) => void;
};

/**
Expand All @@ -30,11 +42,17 @@ export type PlanCreateFormProps = {
export const PlanCreateForm: React.FC<PlanCreateFormProps> = ({
providers,
filterState,
state,
projectName,
filterDispatch,
dispatch,
}) => {
const { t } = useForkliftTranslation();

const { data, setData } = useCreateVmMigrationData();
const providerCardItems = createProviderCardItems(providers);
const providerNamespaces = [
...new Set(providers.map((provider) => provider.metadata?.namespace)),
];

const onChange = (id: string) => {
filterDispatch({ type: 'SELECT_PROVIDER', payload: id || '' });
Expand All @@ -43,6 +61,36 @@ export const PlanCreateForm: React.FC<PlanCreateFormProps> = ({
return (
<div className="forklift-create-provider-edit-section">
<Form isWidthLimited className="forklift-section-secret-edit">
<PlanNameTextField
isRequired
value={state.underConstruction.plan.metadata.name}
validated={state.validation.planName}
isDisabled={state.flow.editingDone}
onChange={(_, value) => {
dispatch(setPlanName(value?.trim() ?? ''));
setData({ ...data, planName: value });
}}
/>

<ProjectNameSelect
value={projectName}
options={providerNamespaces.map((namespace) => ({
value: namespace,
content: namespace,
}))}
onSelect={(value) => {
dispatch(setProjectNameAction(value));
setData({ ...data, projectName: value });
}}
isDisabled={!providers.length}
popoverHelpContent={
<ForkliftTrans>
The project that your migration plan will be created in. Only projects with providers
in them can be selected.
</ForkliftTrans>
}
/>

<FormGroupWithHelpText fieldId="type">
<FiltersToolbarProviders
className="forklift--create-plan--filters-toolbar"
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import React from 'react';
import { Validation } from 'src/modules/Providers';
import { ForkliftTrans, useForkliftTranslation } from 'src/utils';

import { FormGroupWithHelpText } from '@kubev2v/common';
import { TextInput } from '@patternfly/react-core';

interface PlanNameTextFieldProps {
value: string;
validated: Validation;
onChange: (event: React.FormEvent<HTMLInputElement>, value: string) => void;
isRequired?: boolean;
isDisabled?: boolean;
}

export const PlanNameTextField: React.FC<PlanNameTextFieldProps> = ({
value,
validated,
isDisabled,
isRequired,
onChange,
}) => {
const { t } = useForkliftTranslation();
const [isUpdated, setIsUpdated] = React.useState(false);

return (
<FormGroupWithHelpText
label={t('Plan name')}
isRequired={isRequired}
fieldId="planName"
{...(isUpdated && {
validated: validated,
helperTextInvalid: (
<ForkliftTrans>
Name is required and must be a unique within a namespace and valid Kubernetes name.
</ForkliftTrans>
),
})}
>
<TextInput
spellCheck="false"
isRequired={isRequired}
type="text"
id="planName"
value={value}
validated={isUpdated ? validated : 'default'}
isDisabled={isDisabled}
onChange={(event, value) => {
onChange(event, value);
setIsUpdated(true);
}}
/>
</FormGroupWithHelpText>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import React from 'react';
import { useForkliftTranslation } from 'src/utils';

import { FormGroupWithHelpText, TypeaheadSelect, TypeaheadSelectOption } from '@kubev2v/common';
import { Popover } from '@patternfly/react-core';
import HelpIcon from '@patternfly/react-icons/dist/esm/icons/help-icon';

interface ProjectNameSelectProps {
value: string | undefined;
options: TypeaheadSelectOption[];
onSelect: (value: string) => void;
isDisabled?: boolean;
popoverHelpContent?: React.ReactNode;
}

export const ProjectNameSelect: React.FC<ProjectNameSelectProps> = ({
value,
options,
isDisabled,
popoverHelpContent,
onSelect,
}) => {
const { t } = useForkliftTranslation();

return (
<FormGroupWithHelpText
label={t('Project')}
isRequired
fieldId="project"
labelIcon={
<Popover position="right" alertSeverityVariant="info" bodyContent={popoverHelpContent}>
<button type="button" className="pf-c-form__group-label-help">
<HelpIcon />
</button>
</Popover>
}
>
<TypeaheadSelect
id="project-name-select"
selectOptions={options}
selected={value}
onSelect={(_, value) => onSelect(String(value))}
onClearSelection={() => onSelect('')}
isDisabled={isDisabled}
/>
</FormGroupWithHelpText>
);
};
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -10,12 +12,22 @@ import { PlanCreateForm } from './../../components';
import { MemoizedProviderVirtualMachinesList } from './MemoizedProviderVirtualMachinesList';

export const SelectSourceProvider: React.FC<{
namespace: string;
projectName: string;
filterState: PlanCreatePageState;
filterDispatch: React.Dispatch<PlanCreatePageActionTypes>;
providers: V1beta1Provider[];
selectedProvider: V1beta1Provider;
}> = ({ filterState, filterDispatch, providers, selectedProvider }) => {
state: CreateVmMigrationPageState;
dispatch: React.Dispatch<PageAction<CreateVmMigration, unknown>>;
filterDispatch: React.Dispatch<PlanCreatePageActionTypes>;
}> = ({
filterState,
providers,
selectedProvider,
state,
projectName,
dispatch,
filterDispatch,
}) => {
const { t } = useForkliftTranslation();

// Get the ready providers (note: currently forklift does not allow filter be status.phase)
Expand All @@ -39,6 +51,9 @@ export const SelectSourceProvider: React.FC<{
providers={filteredProviders}
filterState={filterState}
filterDispatch={filterDispatch}
dispatch={dispatch}
state={state}
projectName={projectName}
/>

{filterState.selectedProviderUID && (
Expand Down
Loading
Loading