Skip to content

Commit

Permalink
Merge pull request #2230 from upalatucci/add-nad-typeahead
Browse files Browse the repository at this point in the history
CNV-48562: select nads with typing
  • Loading branch information
openshift-merge-bot[bot] authored Oct 21, 2024
2 parents a2f0578 + 6e31c14 commit dae2e08
Show file tree
Hide file tree
Showing 7 changed files with 317 additions and 38 deletions.
3 changes: 3 additions & 0 deletions locales/en/plugin__kubevirt-plugin.json
Original file line number Diff line number Diff line change
Expand Up @@ -244,6 +244,7 @@
"Checking this option will create a new PVC of the bootsource for the new template": "Checking this option will create a new PVC of the bootsource for the new template",
"Checkups": "Checkups",
"Clear all filters": "Clear all filters",
"Clear input value": "Clear input value",
"CLI": "CLI",
"Click <1>Add bootable volume</1> to add your first bootable volume": "Click <1>Add bootable volume</1> to add your first bootable volume",
"Click <1>Create MigrationPolicy</1> to create your first policy": "Click <1>Create MigrationPolicy</1> to create your first policy",
Expand Down Expand Up @@ -1053,6 +1054,7 @@
"See the <2>catalog tab</2> to quickly create a VirtualMachine from the available Templates.": "See the <2>catalog tab</2> to quickly create a VirtualMachine from the available Templates.",
"Select": "Select",
"Select {{label}}": "Select {{label}}",
"Select a NetworkAttachmentDefinitions": "Select a NetworkAttachmentDefinitions",
"Select a project for Red Hat bootable volumes. The default project is 'openshift-virtualization-os-images'": "Select a project for Red Hat bootable volumes. The default project is 'openshift-virtualization-os-images'",
"Select a project for Red Hat templates. The default project is 'openshift'. If you want to store Red Hat templates in multiple projects, you must clone<br/>the Red Hat template by selecting <3>Clone template</3> from the template action menu and then selecting another project for the cloned template.": "Select a project for Red Hat templates. The default project is 'openshift'. If you want to store Red Hat templates in multiple projects, you must clone<br/>the Red Hat template by selecting <3>Clone template</3> from the template action menu and then selecting another project for the cloned template.",
"Select a project in order to see user-provided InstanceTypes": "Select a project in order to see user-provided InstanceTypes",
Expand Down Expand Up @@ -1317,6 +1319,7 @@
"URL (creates PVC)": "URL (creates PVC)",
"URL is required": "URL is required",
"Usage": "Usage",
"Use \"{{inputValue}}\"": "Use \"{{inputValue}}\"",
"Use BIOS when bootloading the guest OS (Default)": "Use BIOS when bootloading the guest OS (Default)",
"Use commas to separate between IP addresses": "Use commas to separate between IP addresses",
"Use cron formatting to set when and how often to look for new imports.": "Use cron formatting to set when and how often to look for new imports.",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,6 @@ const NetworkInterfaceModal: FC<NetworkInterfaceModalProps> = ({
/>
<NetworkInterfaceNetworkSelect
iface={iface}
interfaceType={interfaceType}
isEditing={Boolean(network) && Boolean(iface)}
namespace={namespace}
networkName={networkName}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,19 +1,13 @@
import React, { Dispatch, FC, MouseEvent, SetStateAction, useEffect, useMemo } from 'react';
import React, { Dispatch, FC, SetStateAction, useEffect, useMemo, useRef } from 'react';

import { V1Interface, V1VirtualMachine } from '@kubevirt-ui/kubevirt-api/kubevirt';
import FormGroupHelperText from '@kubevirt-utils/components/FormGroupHelperText/FormGroupHelperText';
import FormPFSelect from '@kubevirt-utils/components/FormPFSelect/FormPFSelect';
import Loading from '@kubevirt-utils/components/Loading/Loading';
import SelectTypeahead from '@kubevirt-utils/components/SelectTypeahead/SelectTypeahead';
import { useKubevirtTranslation } from '@kubevirt-utils/hooks/useKubevirtTranslation';
import { interfacesTypes } from '@kubevirt-utils/resources/vm/utils/network/constants';
import { getNetworkInterfaceType } from '@kubevirt-utils/resources/vm/utils/network/selectors';
import {
FormGroup,
Label,
SelectList,
SelectOption,
ValidatedOptions,
} from '@patternfly/react-core';
import { FormGroup, Label, ValidatedOptions } from '@patternfly/react-core';

import { getNadType, networkNameStartWithPod, podNetworkExists } from '../../utils/helpers';
import useNADsData from '../hooks/useNADsData';
Expand All @@ -23,7 +17,6 @@ import NetworkSelectHelperPopover from './components/NetworkSelectHelperPopover/
type NetworkInterfaceNetworkSelectProps = {
editInitValueNetworkName?: string | undefined;
iface: V1Interface;
interfaceType: string;
isEditing?: boolean | undefined;
namespace?: string;
networkName: string;
Expand All @@ -36,7 +29,6 @@ type NetworkInterfaceNetworkSelectProps = {
const NetworkInterfaceNetworkSelect: FC<NetworkInterfaceNetworkSelectProps> = ({
editInitValueNetworkName,
iface,
interfaceType,
isEditing,
namespace,
networkName,
Expand All @@ -48,6 +40,8 @@ const NetworkInterfaceNetworkSelect: FC<NetworkInterfaceNetworkSelectProps> = ({
const { t } = useKubevirtTranslation();
const { loaded, loadError, nads } = useNADsData(vm?.metadata?.namespace || namespace);

const selectedFirstOnLoad = useRef(false);

const hasPodNetwork = useMemo(() => podNetworkExists(vm), [vm]);
const hasNads = useMemo(() => nads?.length > 0, [nads]);
const isPodNetworkingOptionExists =
Expand All @@ -63,15 +57,27 @@ const NetworkInterfaceNetworkSelect: FC<NetworkInterfaceNetworkSelectProps> = ({
const networkOptions = useMemo(() => {
const options = nads?.map((nad) => {
const { name, namespace: nadNamespace, uid } = nad?.metadata;
const type = getNadType(nad);
const value = `${nadNamespace}/${name}`;
return {
children: (
<>
{value} <Label isCompact>{getNadType(nad)} Binding</Label>
</>
),
key: uid,
type: getNadType(nad),
value: `${nadNamespace}/${name}`,
type,
value,
};
});

if (isPodNetworkingOptionExists) {
options.unshift({
children: (
<>
{podNetworkingText} <Label isCompact>{interfacesTypes.bridge} Binding</Label>
</>
),
key: 'pod-networking',
type: interfacesTypes.bridge,
value: podNetworkingText,
Expand All @@ -93,8 +99,7 @@ const NetworkInterfaceNetworkSelect: FC<NetworkInterfaceNetworkSelectProps> = ({
const validated =
canCreateNetworkInterface || isEditing ? ValidatedOptions.default : ValidatedOptions.error;

const handleChange = (event: MouseEvent<HTMLSelectElement>, value: string) => {
event.preventDefault();
const handleChange = (value: string) => {
setNetworkName(value);
setInterfaceType(
value === podNetworkingText
Expand All @@ -108,14 +113,21 @@ const NetworkInterfaceNetworkSelect: FC<NetworkInterfaceNetworkSelectProps> = ({
// if networkName exists, we have the option to create a NIC either with pod networking or by existing NAD
if (networkName) {
setSubmitDisabled(false);
return;
}

// if networkName is empty, and pod network exists we can create a NIC with existing NAD if there is one
else if (loaded && !loadError) {
if (loaded && !loadError && !selectedFirstOnLoad.current) {
setNetworkName(networkOptions?.[0]?.value);
selectedFirstOnLoad.current = true;

return;
}

// if no nads and pod network already exists, we can't create a NIC
else if (loaded && (loadError || !canCreateNetworkInterface)) {
if (loaded && (loadError || !canCreateNetworkInterface)) {
setSubmitDisabled(true);
return;
}
}, [
loadError,
Expand All @@ -138,28 +150,19 @@ const NetworkInterfaceNetworkSelect: FC<NetworkInterfaceNetworkSelectProps> = ({
{hasPodNetwork && !loaded ? (
<Loading />
) : (
<FormPFSelect
selectedLabel={
<SelectTypeahead
newOptionComponent={(inputValue) => (
<>
{networkName} <Label isCompact>{interfaceType} Binding</Label>
{t(`Use "{{inputValue}}"`, { inputValue })}{' '}
<Label isCompact>{interfacesTypes.bridge} Binding</Label>
</>
}
onSelect={handleChange}
)}
id="select-nad"
options={networkOptions}
placeholder={t('Select a NetworkAttachmentDefinitions')}
selected={networkName}
toggleProps={{ isDisabled: !canCreateNetworkInterface, isFullWidth: true }}
>
<SelectList>
{networkOptions?.map(({ key, type, value }) => (
<SelectOption
data-test-id={`network-attachment-definition-select-${key}`}
key={key}
value={value}
>
{value} <Label isCompact>{type} Binding</Label>
</SelectOption>
))}
</SelectList>
</FormPFSelect>
setSelected={handleChange}
/>
)}
</div>
{loaded && validated === ValidatedOptions.error && (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -126,7 +126,7 @@ export const createInterface = (
nicName: string,
interfaceModel: string,
interfaceMACAddress: string,
interfaceType: string,
interfaceType = interfacesTypes.bridge,
): V1Interface => {
return {
[interfaceType?.replace('-', '')?.toLowerCase()]: {},
Expand Down
102 changes: 102 additions & 0 deletions src/utils/components/SelectTypeahead/SelectTypeahead.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
import React, { FC, ReactNode, Ref, useRef, useState } from 'react';

import { useKubevirtTranslation } from '@kubevirt-utils/hooks/useKubevirtTranslation';
import { isEmpty } from '@kubevirt-utils/utils/utils';
import {
MenuToggleElement,
Select,
SelectList,
SelectOption,
SelectOptionProps,
} from '@patternfly/react-core';

import Toggle from './Toggle';
import { filterOptions } from './utils';

type SelectTypeaheadProps = {
id: string;
newOptionComponent?: (inputValue: string) => ReactNode;
options: SelectOptionProps[];
placeholder: string;
selected: string;
setSelected: (newSelection: null | string) => void;
};

const SelectTypeahead: FC<SelectTypeaheadProps> = ({
id,
newOptionComponent,
options,
placeholder,
selected,
setSelected,
}) => {
const { t } = useKubevirtTranslation();
const [isOpen, setIsOpen] = useState(false);
const [inputValue, setInputValue] = useState<string>(selected || '');
const [focusedItemIndex, setFocusedItemIndex] = useState<null | number>(null);
const textInputRef = useRef<HTMLInputElement>();

const selectOptions = inputValue ? filterOptions(options, inputValue) : options;

const onSelect = (value: string) => {
if (value) {
setSelected(selected === value ? null : value);

if (selected === value) setInputValue('');

setIsOpen(true);
}

textInputRef.current?.focus();
};

return (
<Select
toggle={(toggleRef: Ref<MenuToggleElement>) => (
<Toggle
focusedItemIndex={focusedItemIndex}
inputValue={inputValue}
isOpen={isOpen}
onSelect={onSelect}
placeholder={placeholder}
selected={selected}
selectOptions={selectOptions}
setFocusedItemIndex={setFocusedItemIndex}
setInputValue={setInputValue}
setIsOpen={setIsOpen}
setSelected={setSelected}
textInputRef={textInputRef}
toggleRef={toggleRef}
/>
)}
id={id}
isOpen={isOpen}
onOpenChange={() => setIsOpen(false)}
onSelect={(ev, selection) => onSelect(selection as string)}
selected={selected}
>
<SelectList id="select-typeahead-listbox">
{selectOptions.map((option, index) => (
<SelectOption
className={option.className}
id={`select-typeahead-${option.value.replace(' ', '-')}`}
isFocused={focusedItemIndex === index}
key={option.value || option.children}
{...option}
>
{option.children || option.value}
</SelectOption>
))}
</SelectList>
{!isEmpty(inputValue) && (
<SelectOption selected={inputValue === selected} value={inputValue}>
{newOptionComponent
? newOptionComponent(inputValue)
: t(`Use "{{inputValue}}"`, { inputValue })}
</SelectOption>
)}
</Select>
);
};

export default SelectTypeahead;
Loading

0 comments on commit dae2e08

Please sign in to comment.