Skip to content

Commit

Permalink
[MTV-1769] Add Project field to migration plan/provider wizards
Browse files Browse the repository at this point in the history
Signed-off-by: Jeff Puzzo <[email protected]>
  • Loading branch information
jpuzz0 committed Dec 13, 2024
1 parent faf4a53 commit bb71fbb
Show file tree
Hide file tree
Showing 18 changed files with 740 additions and 106 deletions.
425 changes: 425 additions & 0 deletions packages/common/src/components/TypeaheadSelect/TypeaheadSelect.tsx

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions packages/common/src/components/TypeaheadSelect/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './TypeaheadSelect';
1 change: 1 addition & 0 deletions packages/common/src/components/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,4 +9,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 @@ -155,7 +155,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 @@ -265,11 +264,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 @@ -388,6 +385,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 @@ -419,7 +417,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

0 comments on commit bb71fbb

Please sign in to comment.