Skip to content
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
12 changes: 8 additions & 4 deletions libs/i18n/locales/en/translation.json
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
"Bare metal instances": "Bare metal instances",
"Bare Metal Machines": "Bare Metal Machines",
"Bare metal provisioning wizard": "Bare metal provisioning wizard",
"Boot disk": "Boot disk",
"Break-glass credentials": "Break-glass credentials",
"Browse catalog items and launch virtual machines, clusters, or bare metal machines from published offerings.": "Browse catalog items and launch virtual machines, clusters, or bare metal machines from published offerings.",
"Cancel": "Cancel",
Expand Down Expand Up @@ -54,11 +55,7 @@
"catalogProvision.instanceTypes.deprecatedSuffix": " (deprecated)",
"catalogProvision.instanceTypes.loadError": "Could not load instance types",
"catalogProvision.networking.loadError": "Could not load networking options",
"catalogProvision.review.catalogItem": "Catalog item",
"catalogProvision.steps.catalog.title": "Catalog item",
"catalogProvision.steps.configuration.title": "Configuration",
"catalogProvision.steps.general.title": "General",
"catalogProvision.steps.networking.title": "Networking",
"catalogProvision.steps.review.title": "Review",
"catalogProvision.validation.bootDiskNumber": "Boot disk size must be a number",
"catalogProvision.validation.catalogItemRequired": "Select a catalog item",
Expand Down Expand Up @@ -194,7 +191,12 @@
"Failed to download kubeconfig": "Failed to download kubeconfig",
"Failed to edit resource": "Failed to edit resource",
"Failed to enable Identity provider": "Failed to enable Identity provider",
"Failed to fetch host types": "Failed to fetch host types",
"Failed to fetch Identity provider": "Failed to fetch Identity provider",
"Failed to fetch instance type": "Failed to fetch instance type",
"Failed to fetch security groups": "Failed to fetch security groups",
"Failed to fetch subnet": "Failed to fetch subnet",
"Failed to fetch virtual network": "Failed to fetch virtual network",
"Failed to load cluster password": "Failed to load cluster password",
"Failed to load graphical console viewer": "Failed to load graphical console viewer",
"Failed to load security groups": "Failed to load security groups",
Expand Down Expand Up @@ -388,6 +390,7 @@
"Use IPv4 CIDR notation (for example 10.128.0.0/14).": "Use IPv4 CIDR notation (for example 10.128.0.0/14).",
"Use IPv4 CIDR notation (for example 172.30.0.0/16).": "Use IPv4 CIDR notation (for example 172.30.0.0/16).",
"User data": "User data",
"User Data": "User Data",
"User Data is required": "User Data is required",
"User data must not exceed 64 KB.": "User data must not exceed 64 KB.",
"User info URL": "User info URL",
Expand All @@ -402,6 +405,7 @@
"Virtual Network": "Virtual Network",
"Virtual network is required": "Virtual network is required",
"Virtual networks": "Virtual networks",
"VM image": "VM image",
"Worker nodes": "Worker nodes",
"You are not authorized to access this resource.": "You are not authorized to access this resource."
}
16 changes: 4 additions & 12 deletions libs/ui-components/src/components/Form/MultiSelectField.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ import { useField } from 'formik';
import { getVisibleFieldError } from './fieldError';
import { useShowFieldValidationErrors } from './FieldValidationContext';
import { FormFieldHelper } from './FormFieldHelper';
import { type LabeledResourceRef } from './labeledResourceRef';
import type { SelectFieldOption } from './SelectField';

interface MultiSelectFieldProps {
Expand Down Expand Up @@ -37,7 +36,7 @@ export const MultiSelectField = ({
noOptionsFoundMessage = (filter) => `No options found for "${filter}"`,
autoSelectSingleOption = false,
}: MultiSelectFieldProps) => {
const [field, meta, helpers] = useField<LabeledResourceRef[]>(name);
const [field, meta, helpers] = useField<(string | number)[]>(name);
const showValidationErrors = useShowFieldValidationErrors();
const error = getVisibleFieldError(meta, showValidationErrors);
const validated = error ? 'error' : 'default';
Expand All @@ -59,25 +58,18 @@ export const MultiSelectField = ({
) {
return;
}
void helpers.setValue([{ value: options[0].value, label: options[0].label }], false);
void helpers.setValue([options[0].value], false);
}, [autoSelectSingleOption, helpers, isDisabled, isLoading, options, selectedValues.length]);

const initialOptions = useMemo<MultiTypeaheadSelectOption[]>(() => {
return options.map((option) => ({
content: option.label,
value: option.value,
selected: selectedValues.some((value) => value.value === option.value),
selected: selectedValues.some((value) => value === option.value),
isDisabled: option.isDisabled,
}));
}, [options, selectedValues]);

const toLabeledResourceRefs = (selections: (string | number)[]) =>
selections.map((selection) => {
const value = String(selection);
const option = options.find((entry) => entry.value === value);
return option ? { value: option.value, label: option.label } : { value, label: value };
});

return (
<FormGroup label={label} fieldId={fieldId} isRequired={isRequired}>
<MultiTypeaheadSelect
Expand All @@ -87,7 +79,7 @@ export const MultiSelectField = ({
isDisabled={controlDisabled}
noOptionsFoundMessage={noOptionsFoundMessage}
onSelectionChange={(_event, selections) => {
void helpers.setValue(toLabeledResourceRefs(selections), true);
void helpers.setValue(selections, true);
void helpers.setTouched(true);
}}
onToggle={(open) => {
Expand Down
24 changes: 11 additions & 13 deletions libs/ui-components/src/components/Form/SelectField.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,26 +4,24 @@ import { Formik, type FormikErrors } from 'formik';
import { describe, expect, it } from 'vitest';
import * as yup from 'yup';

import { EMPTY_LABELED_RESOURCE_REF, type LabeledResourceRef } from './labeledResourceRef';
import { labeledResourceRefSchema } from './labeledResourceRefSchema';
import { SelectField } from './SelectField';

const renderSelect = ({
autoSelectSingleOption = false,
initialValue = EMPTY_LABELED_RESOURCE_REF,
initialValue = '',
isLoading = false,
}: {
autoSelectSingleOption?: boolean;
initialValue?: LabeledResourceRef;
initialValue?: string;
isLoading?: boolean;
} = {}) => {
let latestErrors: FormikErrors<{ kind: LabeledResourceRef }> = {};
let latestErrors: FormikErrors<{ kind: string }> = {};

render(
<Formik
initialValues={{ kind: initialValue }}
validationSchema={yup.object({
kind: labeledResourceRefSchema('Kind is required'),
kind: yup.string().required('Kind is required'),
})}
onSubmit={() => undefined}
>
Expand All @@ -39,7 +37,7 @@ const renderSelect = ({
placeholder="Select a kind"
options={[{ value: 'only-option', label: 'Only option label' }]}
/>
<output aria-label="formik-value">{values.kind.value || '(empty)'}</output>
<output aria-label="formik-value">{values.kind || '(empty)'}</output>
<button
type="button"
onClick={() => {
Expand Down Expand Up @@ -102,7 +100,7 @@ describe('SelectField', () => {

it('does not auto-select when multiple options are available', async () => {
render(
<Formik initialValues={{ kind: EMPTY_LABELED_RESOURCE_REF }} onSubmit={() => undefined}>
<Formik initialValues={{ kind: '' }} onSubmit={() => undefined}>
{({ values }) => (
<>
<SelectField
Expand All @@ -116,7 +114,7 @@ describe('SelectField', () => {
{ value: 'large', label: 'Large' },
]}
/>
<output aria-label="formik-value">{values.kind.value || '(empty)'}</output>
<output aria-label="formik-value">{values.kind || '(empty)'}</output>
</>
)}
</Formik>,
Expand All @@ -133,7 +131,7 @@ describe('SelectField', () => {
const user = userEvent.setup();

render(
<Formik initialValues={{ kind: EMPTY_LABELED_RESOURCE_REF }} onSubmit={() => undefined}>
<Formik initialValues={{ kind: '' }} onSubmit={() => undefined}>
{({ values }) => (
<>
<SelectField
Expand All @@ -146,7 +144,7 @@ describe('SelectField', () => {
{ value: 'large', label: 'Large' },
]}
/>
<output aria-label="formik-value">{values.kind.value || '(empty)'}</output>
<output aria-label="formik-value">{values.kind || '(empty)'}</output>
</>
)}
</Formik>,
Expand All @@ -166,10 +164,10 @@ describe('SelectField', () => {

render(
<Formik
initialValues={{ kind: EMPTY_LABELED_RESOURCE_REF }}
initialValues={{ kind: '' }}
validateOnBlur
validationSchema={yup.object({
kind: labeledResourceRefSchema('Kind is required'),
kind: yup.string().required('Kind is required'),
})}
onSubmit={() => undefined}
>
Expand Down
42 changes: 11 additions & 31 deletions libs/ui-components/src/components/Form/SelectField.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,14 +12,9 @@ import { useField } from 'formik';
import { getVisibleFieldError } from './fieldError';
import { useShowFieldValidationErrors } from './FieldValidationContext';
import { FormFieldHelper } from './FormFieldHelper';
import {
EMPTY_LABELED_RESOURCE_REF,
type LabeledResourceRef,
isLabeledResourceRefEmpty,
} from './labeledResourceRef';

export interface SelectFieldOption {
value: string;
value: string | number;
label: string;
isDisabled?: boolean;
}
Expand Down Expand Up @@ -50,51 +45,36 @@ export const SelectField = ({
loadingPlaceholder = 'Loading...',
autoSelectSingleOption = false,
}: SelectFieldProps) => {
const [field, meta, helpers] = useField<LabeledResourceRef>(name);
const [field, meta, helpers] = useField<string | number>(name);
const [isOpen, setIsOpen] = useState(false);
const showValidationErrors = useShowFieldValidationErrors();
const error = getVisibleFieldError(meta, showValidationErrors);
const validated = error ? 'error' : 'default';
const effectivePlaceholder = isLoading ? loadingPlaceholder : placeholder;
const controlDisabled = isDisabled || isLoading;
const fieldValue = field.value ?? EMPTY_LABELED_RESOURCE_REF;
const selectedValue = fieldValue.value;

useEffect(() => {
if (
!autoSelectSingleOption ||
isLoading ||
isDisabled ||
options.length !== 1 ||
!isLabeledResourceRefEmpty(fieldValue)
field.value !== ''
) {
return;
}
void helpers.setValue({ value: options[0].value, label: options[0].label }, false);
}, [autoSelectSingleOption, fieldValue, helpers, isDisabled, isLoading, options]);
void helpers.setValue(options[0].value, false);
}, [autoSelectSingleOption, field.value, helpers, isDisabled, isLoading, options]);

const toggleLabel = useMemo(() => {
if (isLabeledResourceRefEmpty(fieldValue)) {
if (field.value === '') {
return effectivePlaceholder ?? '';
}
return fieldValue.label.trim() || fieldValue.value;
}, [effectivePlaceholder, fieldValue]);
return options.find(({ value }) => value === field.value)?.label || field.value;
}, [effectivePlaceholder, field.value, options]);
Comment thread
coderabbitai[bot] marked this conversation as resolved.

const onSelect = (
_event: MouseEvent<Element> | undefined,
value: string | number | undefined,
) => {
if (value == null) {
void helpers.setValue(EMPTY_LABELED_RESOURCE_REF, true);
} else {
const option = options.find((entry) => entry.value === String(value));
void helpers.setValue(
option
? { value: option.value, label: option.label }
: { value: String(value), label: String(value) },
true,
);
}
const onSelect = (_event: MouseEvent<Element> | undefined, value: string | number) => {
helpers.setValue(value, true);
void helpers.setTouched(true, false);
setIsOpen(false);
};
Expand All @@ -121,7 +101,7 @@ export const SelectField = ({
<Select
id={`${fieldId}-select`}
isOpen={isOpen}
selected={selectedValue}
selected={field.value}
onSelect={onSelect}
onOpenChange={setIsOpen}
toggle={toggle}
Expand Down
22 changes: 0 additions & 22 deletions libs/ui-components/src/components/Form/labeledResourceRef.ts

This file was deleted.

20 changes: 0 additions & 20 deletions libs/ui-components/src/components/Form/labeledResourceRefSchema.ts

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -30,8 +30,13 @@ import { useBareMetalInstanceAdapter } from './wizard/adapters/bareMetalInstance
import { useClusterAdapter } from './wizard/adapters/clusterAdapter';
import { useComputeInstanceAdapter } from './wizard/adapters/computeInstanceAdapter';
import type { CatalogProvisionAdapter } from './wizard/adapters/types';
import { STEP_LABEL_KEYS, type WizardStepId, getWizardOrderedSteps } from './wizard/stepIds';
import { CatalogStep, ReviewStep } from './wizard/steps/WizardSteps';
import {
STEP_LABEL_KEYS,
WIZARD_STEP_IDS,
type WizardStepId,
getWizardOrderedSteps,
} from './wizard/stepIds';
import { CatalogStep } from './wizard/steps/WizardSteps';

export type {
CatalogProvisionPayload,
Expand Down Expand Up @@ -75,7 +80,7 @@ interface WizardFooterProps {
}

const isWizardStepId = (stepId: string | number | undefined): stepId is WizardStepId =>
typeof stepId === 'string' && Object.hasOwn(STEP_LABEL_KEYS, stepId);
typeof stepId === 'string' && WIZARD_STEP_IDS.includes(stepId);

const CatalogProvisionWizardFooter = ({
formik,
Expand Down Expand Up @@ -211,7 +216,6 @@ interface WizardBodyProps {
adapter: ErasedCatalogAdapter;
stepId: WizardStepId;
catalogItem: CatalogItem | null;
values: CatalogProvisionWizardValues;
provisionError?: string;
validationAlert: boolean;
}
Expand All @@ -220,14 +224,14 @@ const WizardStepBody = ({
adapter,
stepId,
catalogItem,
values,
provisionError,
validationAlert,
}: WizardBodyProps) => {
const { t } = useTranslation();
const ConfigurationStep = adapter.ConfigurationStep;
const NetworkingStep = adapter.NetworkingStep;
const GeneralStepComponent = adapter.GeneralStep;
const ReviewStepComponent = adapter.ReviewStep;

return (
<FieldValidationProvider showErrors={validationAlert}>
Expand All @@ -248,9 +252,7 @@ const WizardStepBody = ({
{stepId === 'general' ? <GeneralStepComponent catalogItem={catalogItem} /> : null}
{stepId === 'configuration' ? <ConfigurationStep catalogItem={catalogItem} /> : null}
{stepId === 'networking' ? <NetworkingStep catalogItem={catalogItem} /> : null}
{stepId === 'review' ? (
<ReviewStep adapter={adapter} catalogItem={catalogItem} values={values} />
) : null}
{stepId === 'review' ? <ReviewStepComponent catalogItem={catalogItem} /> : null}
</Stack>
</FieldValidationProvider>
);
Expand Down Expand Up @@ -499,12 +501,11 @@ const CatalogProvisionWizardForm = ({
}
>
{orderedSteps.map((stepId) => (
<WizardStep key={stepId} id={stepId} name={t(STEP_LABEL_KEYS[stepId])}>
<WizardStep key={stepId} id={stepId} name={STEP_LABEL_KEYS(t)[stepId]}>
<WizardStepBody
adapter={adapter}
stepId={stepId}
catalogItem={selectedCatalogItem}
values={formik.values}
provisionError={provisionError}
validationAlert={validationAlert}
/>
Expand Down
Loading
Loading