diff --git a/packages/manager/src/features/PlacementGroups/PlacementGroupsAssignLinodesDrawer.test.tsx b/packages/manager/src/features/PlacementGroups/PlacementGroupsAssignLinodesDrawer.test.tsx index 657f3468b8e..c8c97d20dd0 100644 --- a/packages/manager/src/features/PlacementGroups/PlacementGroupsAssignLinodesDrawer.test.tsx +++ b/packages/manager/src/features/PlacementGroups/PlacementGroupsAssignLinodesDrawer.test.tsx @@ -59,7 +59,6 @@ describe('PlacementGroupsAssignLinodesDrawer', () => { const { getByText } = renderWithTheme( ({ + useCreatePlacementGroup: vi.fn().mockReturnValue({ + mutateAsync: vi.fn().mockResolvedValue({}), + reset: vi.fn(), + }), +})); + +vi.mock('src/queries/placementGroups', async () => { + const actual = await vi.importActual('src/queries/placementGroups'); + return { + ...actual, + useCreatePlacementGroup: queryMocks.useCreatePlacementGroup, + }; +}); + describe('PlacementGroupsCreateDrawer', () => { it('should render and have its fields enabled', () => { const { getByLabelText } = renderWithTheme( - + ); expect(getByLabelText('Label')).toBeEnabled(); @@ -27,10 +38,7 @@ describe('PlacementGroupsCreateDrawer', () => { it('Affinity Type select should have the correct options', async () => { const { getByPlaceholderText, getByText } = renderWithTheme( - + ); const inputElement = getByPlaceholderText('Select an Affinity Type'); @@ -43,21 +51,9 @@ describe('PlacementGroupsCreateDrawer', () => { expect(getByText('Anti-affinity')).toBeInTheDocument(); }); - it('should disable the submit button when the number of placement groups created is >= to the max', () => { - const { getByTestId } = renderWithTheme( - - ); - - expect(getByTestId('submit')).toHaveAttribute('aria-disabled', 'true'); - }); - it('should populate the region select with the selected region prop', () => { const { getByLabelText } = renderWithTheme( diff --git a/packages/manager/src/features/PlacementGroups/PlacementGroupsCreateDrawer.tsx b/packages/manager/src/features/PlacementGroups/PlacementGroupsCreateDrawer.tsx index 817821111aa..d3b2277d394 100644 --- a/packages/manager/src/features/PlacementGroups/PlacementGroupsCreateDrawer.tsx +++ b/packages/manager/src/features/PlacementGroups/PlacementGroupsCreateDrawer.tsx @@ -2,11 +2,15 @@ import { createPlacementGroupSchema } from '@linode/validation'; import { useFormik } from 'formik'; import { useSnackbar } from 'notistack'; import * as React from 'react'; -import { useQueryClient } from 'react-query'; +import { ActionsPanel } from 'src/components/ActionsPanel/ActionsPanel'; +import { Autocomplete } from 'src/components/Autocomplete/Autocomplete'; import { Drawer } from 'src/components/Drawer'; +import { Notice } from 'src/components/Notice/Notice'; +import { RegionSelect } from 'src/components/RegionSelect/RegionSelect'; +import { Stack } from 'src/components/Stack'; +import { TextField } from 'src/components/TextField'; import { useFormValidateOnChange } from 'src/hooks/useFormValidateOnChange'; -import { queryKey as placementGroupQueryKey } from 'src/queries/placementGroups'; import { useCreatePlacementGroup } from 'src/queries/placementGroups'; import { useRegionsQuery } from 'src/queries/regions'; import { getErrorMap } from 'src/utilities/errorUtils'; @@ -15,27 +19,17 @@ import { handleGeneralErrors, } from 'src/utilities/formikErrorUtils'; -import { PlacementGroupsDrawerContent } from './PlacementGroupsDrawerContent'; -import { MAX_NUMBER_OF_PLACEMENT_GROUPS } from './constants'; +import { affinityTypeOptions } from './utils'; -import type { - PlacementGroupDrawerFormikProps, - PlacementGroupsCreateDrawerProps, -} from './types'; +import type { PlacementGroupsCreateDrawerProps } from './types'; +import type { CreatePlacementGroupPayload } from '@linode/api-v4'; export const PlacementGroupsCreateDrawer = ( props: PlacementGroupsCreateDrawerProps ) => { - const { - numberOfPlacementGroupsCreated, - onClose, - onPlacementGroupCreate, - open, - selectedRegionId, - } = props; - const queryClient = useQueryClient(); + const { onClose, onPlacementGroupCreate, open, selectedRegionId } = props; const { data: regions } = useRegionsQuery(); - const { mutateAsync } = useCreatePlacementGroup(); + const { error, mutateAsync } = useCreatePlacementGroup(); const { enqueueSnackbar } = useSnackbar(); const { hasFormBeenSubmitted, @@ -52,17 +46,16 @@ export const PlacementGroupsCreateDrawer = ( setFieldValue, status, values, - ...rest } = useFormik({ enableReinitialize: true, initialValues: { - affinity_type: '' as PlacementGroupDrawerFormikProps['affinity_type'], + affinity_type: '' as CreatePlacementGroupPayload['affinity_type'], label: '', region: selectedRegionId ?? '', strict: true, }, - onSubmit( - values: PlacementGroupDrawerFormikProps, + async onSubmit( + values: CreatePlacementGroupPayload, { setErrors, setStatus, setSubmitting } ) { setHasFormBeenSubmitted(false); @@ -70,65 +63,110 @@ export const PlacementGroupsCreateDrawer = ( setErrors({}); const payload = { ...values }; - mutateAsync(payload) - .then((response) => { - setSubmitting(false); - queryClient.invalidateQueries([placementGroupQueryKey]); - - enqueueSnackbar( - `Placement Group ${payload.label} successfully created`, - { - variant: 'success', - } - ); + try { + const response = await mutateAsync(payload); + setSubmitting(false); - if (onPlacementGroupCreate) { - onPlacementGroupCreate(response); + enqueueSnackbar( + `Placement Group ${payload.label} successfully created`, + { + variant: 'success', } - onClose(); - }) - .catch((err) => { - const mapErrorToStatus = () => - setStatus({ generalError: getErrorMap([], err).none }); + ); + + if (onPlacementGroupCreate) { + onPlacementGroupCreate(response); + } + onClose(); + } catch { + const mapErrorToStatus = () => + setStatus({ generalError: getErrorMap([], error).none }); - setSubmitting(false); - handleFieldErrors(setErrors, err); - handleGeneralErrors( - mapErrorToStatus, - err, - 'Error creating Placement Group.' - ); - }); + setSubmitting(false); + handleFieldErrors(setErrors, error ?? []); + handleGeneralErrors( + mapErrorToStatus, + error ?? [], + 'Error creating Placement Group.' + ); + } }, validateOnBlur: false, validateOnChange: hasFormBeenSubmitted, validationSchema: createPlacementGroupSchema, }); + React.useEffect(() => { + resetForm(); + setHasFormBeenSubmitted(false); + }, [open, resetForm]); + + React.useEffect(() => { + if (isSubmitting) { + setHasFormBeenSubmitted(isSubmitting); + } + }, [isSubmitting]); + + const generalError = status?.generalError; + return ( - +
+ + {generalError && } + + { + setFieldValue('region', selection); + }} + currentCapability="Linodes" // TODO VM_Placement: change to Placement Groups when available + disabled={Boolean(selectedRegionId)} + errorText={errors.region} + regions={regions ?? []} + selectedId={selectedRegionId ?? values.region} + /> + { + setFieldValue('affinity_type', value?.value ?? ''); + }} + value={ + affinityTypeOptions.find( + (option) => option.value === values.affinity_type + ) ?? null + } + errorText={errors.affinity_type} + label="Affinity Type" + options={affinityTypeOptions} + placeholder="Select an Affinity Type" + /> + + +
); }; diff --git a/packages/manager/src/features/PlacementGroups/PlacementGroupsDrawerContent.test.tsx b/packages/manager/src/features/PlacementGroups/PlacementGroupsDrawerContent.test.tsx deleted file mode 100644 index 335425db53d..00000000000 --- a/packages/manager/src/features/PlacementGroups/PlacementGroupsDrawerContent.test.tsx +++ /dev/null @@ -1,43 +0,0 @@ -import { FormikProps } from 'formik'; -import * as React from 'react'; - -import { placementGroupFactory } from 'src/factories'; -import { renderWithTheme } from 'src/utilities/testHelpers'; - -import { PlacementGroupsDrawerContent } from './PlacementGroupsDrawerContent'; - -import type { PlacementGroupDrawerFormikProps } from './types'; - -describe('PlacementGroupsDrawerContent', () => { - it('should render the right form elements', () => { - const { getByLabelText, getByRole } = renderWithTheme( - - } - mode="create" - onClose={vi.fn()} - open={true} - regions={[]} - selectedPlacementGroup={placementGroupFactory.build()} - setHasFormBeenSubmitted={vi.fn()} - /> - ); - - expect(getByLabelText('Label')).toBeInTheDocument(); - expect(getByLabelText('Region')).toBeInTheDocument(); - expect(getByLabelText('Affinity Type')).toBeInTheDocument(); - expect(getByRole('button', { name: 'Cancel' })).toBeInTheDocument(); - expect( - getByRole('button', { name: 'Create Placement Group' }) - ).toBeInTheDocument(); - }); -}); diff --git a/packages/manager/src/features/PlacementGroups/PlacementGroupsDrawerContent.tsx b/packages/manager/src/features/PlacementGroups/PlacementGroupsDrawerContent.tsx deleted file mode 100644 index f1496751363..00000000000 --- a/packages/manager/src/features/PlacementGroups/PlacementGroupsDrawerContent.tsx +++ /dev/null @@ -1,140 +0,0 @@ -import Grid from '@mui/material/Unstable_Grid2'; -import * as React from 'react'; - -import { ActionsPanel } from 'src/components/ActionsPanel/ActionsPanel'; -import { Autocomplete } from 'src/components/Autocomplete/Autocomplete'; -import { Notice } from 'src/components/Notice/Notice'; -import { RegionSelect } from 'src/components/RegionSelect/RegionSelect'; -import { Stack } from 'src/components/Stack'; -import { TextField } from 'src/components/TextField'; - -import { MAX_NUMBER_OF_LINODES_IN_PLACEMENT_GROUP_MESSAGE } from './constants'; -import { affinityTypeOptions } from './utils'; - -import type { PlacementGroupDrawerFormikProps } from './types'; -import type { PlacementGroup, Region } from '@linode/api-v4'; -import type { FormikProps } from 'formik'; - -interface Props { - formik: FormikProps; - maxNumberOfPlacementGroups?: number; - mode: 'create' | 'update'; - numberOfPlacementGroupsCreated?: number; - onClose: () => void; - open: boolean; - regions: Region[]; - selectedPlacementGroup?: PlacementGroup; - selectedRegionId?: string; - setHasFormBeenSubmitted: React.Dispatch>; -} - -export const PlacementGroupsDrawerContent = (props: Props) => { - const { - formik, - maxNumberOfPlacementGroups, - mode, - numberOfPlacementGroupsCreated, - onClose, - open, - regions, - selectedPlacementGroup, - selectedRegionId, - setHasFormBeenSubmitted, - } = props; - const { - errors, - handleBlur, - handleChange, - handleSubmit, - isSubmitting, - resetForm, - setFieldValue, - status, - values, - } = formik; - - React.useEffect(() => { - resetForm(); - setHasFormBeenSubmitted(false); - }, [open, resetForm]); - - React.useEffect(() => { - if (isSubmitting) { - setHasFormBeenSubmitted(isSubmitting); - } - }, [isSubmitting]); - - const generalError = status?.generalError; - const isEditDrawer = mode === 'update'; - - return ( - - {generalError ? : null} -
- - - { - setFieldValue('region', selection); - }} - currentCapability="Linodes" // TODO VM_Placement: change to Placement Groups when available - disabled={isEditDrawer || Boolean(selectedRegionId)} - errorText={errors.region} - regions={regions ?? []} - selectedId={selectedRegionId ?? values.region} - /> - { - setFieldValue('affinity_type', value?.value ?? ''); - }} - value={ - affinityTypeOptions.find( - (option) => option.value === values.affinity_type - ) ?? null - } - disabled={isEditDrawer} - errorText={errors.affinity_type} - label="Affinity Type" - options={affinityTypeOptions} - placeholder="Select an Affinity Type" - /> - = maxNumberOfPlacementGroups - : false, - label: `${isEditDrawer ? 'Edit' : 'Create'} Placement Group`, - loading: isSubmitting, - tooltipText: MAX_NUMBER_OF_LINODES_IN_PLACEMENT_GROUP_MESSAGE, - type: 'submit', - }} - secondaryButtonProps={{ - 'data-testid': 'cancel', - label: 'Cancel', - onClick: onClose, - }} - sx={{ pt: 4 }} - /> - -
-
- ); -}; diff --git a/packages/manager/src/features/PlacementGroups/PlacementGroupsEditDrawer.test.tsx b/packages/manager/src/features/PlacementGroups/PlacementGroupsEditDrawer.test.tsx index 2c094903398..ea4417d5fee 100644 --- a/packages/manager/src/features/PlacementGroups/PlacementGroupsEditDrawer.test.tsx +++ b/packages/manager/src/features/PlacementGroups/PlacementGroupsEditDrawer.test.tsx @@ -1,33 +1,69 @@ +import { fireEvent } from '@testing-library/react'; import * as React from 'react'; -import { placementGroupFactory } from 'src/factories/placementGroups'; +import { placementGroupFactory } from 'src/factories'; import { renderWithTheme } from 'src/utilities/testHelpers'; import { PlacementGroupsEditDrawer } from './PlacementGroupsEditDrawer'; +const queryMocks = vi.hoisted(() => ({ + useMutatePlacementGroup: vi.fn().mockReturnValue({ + mutateAsync: vi.fn().mockResolvedValue({}), + reset: vi.fn(), + }), + useParams: vi.fn().mockReturnValue({}), + usePlacementGroupQuery: vi.fn().mockReturnValue({}), +})); + +vi.mock('react-router-dom', async () => { + const actual = await vi.importActual('react-router-dom'); + return { + ...actual, + useParams: queryMocks.useParams, + }; +}); + +vi.mock('src/queries/placementGroups', async () => { + const actual = await vi.importActual('src/queries/placementGroups'); + return { + ...actual, + useMutatePlacementGroup: queryMocks.useMutatePlacementGroup, + usePlacementGroupQuery: queryMocks.usePlacementGroupQuery, + }; +}); + describe('PlacementGroupsCreateDrawer', () => { - it('should render, have the proper fields populated with PG values, and have uneditable fields disabled', () => { - const { getByLabelText } = renderWithTheme( - + queryMocks.useParams.mockReturnValue({ + id: '1', + }); + queryMocks.usePlacementGroupQuery.mockReturnValue({ + data: placementGroupFactory.build({ + affinity_type: 'anti_affinity', + id: 1, + label: 'PG-to-edit', + linode_ids: [1], + }), + }); + + it('should have the proper content and fields', () => { + const { getByLabelText, getByRole, getByText } = renderWithTheme( + ); + expect( + getByRole('heading', { + name: 'Edit Placement Group PG-to-edit (Anti-affinity)', + }) + ).toBeInTheDocument(); + expect(getByText('Newark, NJ (us-east)')).toBeInTheDocument(); expect(getByLabelText('Label')).toBeEnabled(); - expect(getByLabelText('Label')).toHaveValue('PG-1'); + expect(getByLabelText('Label')).toHaveValue('PG-to-edit'); + expect(getByRole('button', { name: 'Cancel' })).toBeEnabled(); + const editButton = getByRole('button', { name: 'Edit' }); - expect(getByLabelText('Region')).toBeDisabled(); - expect(getByLabelText('Region')).toHaveValue('Newark, NJ (us-east)'); + expect(editButton).toBeEnabled(); + fireEvent.click(editButton); - expect(getByLabelText('Affinity Type')).toBeDisabled(); - expect(getByLabelText('Affinity Type')).toHaveValue('Anti-affinity'); + expect(queryMocks.useMutatePlacementGroup).toHaveBeenCalled(); }); }); diff --git a/packages/manager/src/features/PlacementGroups/PlacementGroupsEditDrawer.tsx b/packages/manager/src/features/PlacementGroups/PlacementGroupsEditDrawer.tsx index 6739954941c..0a525a45aaa 100644 --- a/packages/manager/src/features/PlacementGroups/PlacementGroupsEditDrawer.tsx +++ b/packages/manager/src/features/PlacementGroups/PlacementGroupsEditDrawer.tsx @@ -1,34 +1,39 @@ +import { AFFINITY_TYPES } from '@linode/api-v4'; import { updatePlacementGroupSchema } from '@linode/validation'; import { useFormik } from 'formik'; import { useSnackbar } from 'notistack'; import * as React from 'react'; -import { useQueryClient } from 'react-query'; +import { useParams } from 'react-router-dom'; +import { ActionsPanel } from 'src/components/ActionsPanel/ActionsPanel'; import { Drawer } from 'src/components/Drawer'; +import { Notice } from 'src/components/Notice/Notice'; +import { Stack } from 'src/components/Stack'; +import { TextField } from 'src/components/TextField'; +import { Typography } from 'src/components/Typography'; import { useFormValidateOnChange } from 'src/hooks/useFormValidateOnChange'; -import { queryKey as placementGroupQueryKey } from 'src/queries/placementGroups'; +import { usePlacementGroupData } from 'src/hooks/usePlacementGroupsData'; import { useMutatePlacementGroup } from 'src/queries/placementGroups'; -import { useRegionsQuery } from 'src/queries/regions'; +import { usePlacementGroupQuery } from 'src/queries/placementGroups'; import { getErrorMap } from 'src/utilities/errorUtils'; import { handleFieldErrors, handleGeneralErrors, } from 'src/utilities/formikErrorUtils'; -import { PlacementGroupsDrawerContent } from './PlacementGroupsDrawerContent'; - -import type { - PlacementGroupDrawerFormikProps, - PlacementGroupsEditDrawerProps, -} from './types'; +import type { PlacementGroupsEditDrawerProps } from './types'; +import type { UpdatePlacementGroupPayload } from '@linode/api-v4'; export const PlacementGroupsEditDrawer = ( props: PlacementGroupsEditDrawerProps ) => { - const { onClose, onPlacementGroupEdit, open, selectedPlacementGroup } = props; - const queryClient = useQueryClient(); - const { data: regions } = useRegionsQuery(); - const { mutateAsync } = useMutatePlacementGroup( + const { onClose, onPlacementGroupEdit, open } = props; + const { id } = useParams<{ id: string }>(); + const { data: selectedPlacementGroup } = usePlacementGroupQuery(+id); + const { region } = usePlacementGroupData({ + placementGroup: selectedPlacementGroup, + }); + const { error, mutateAsync } = useMutatePlacementGroup( selectedPlacementGroup?.id ?? -1 ); const { enqueueSnackbar } = useSnackbar(); @@ -44,87 +49,113 @@ export const PlacementGroupsEditDrawer = ( handleSubmit, isSubmitting, resetForm, - setFieldValue, status, values, - ...rest - } = useFormik({ + } = useFormik({ enableReinitialize: true, initialValues: { - affinity_type: selectedPlacementGroup?.affinity_type as PlacementGroupDrawerFormikProps['affinity_type'], label: selectedPlacementGroup?.label ?? '', - region: selectedPlacementGroup?.region ?? '', - strict: true, }, - onSubmit(values, { setErrors, setStatus, setSubmitting }) { + async onSubmit(values, { setErrors, setStatus, setSubmitting }) { setHasFormBeenSubmitted(false); setStatus(undefined); setErrors({}); const payload = { ...values }; - // This is a bit of an outlier. We only need to pass the label since that's the only value the API accepts. - // Meanwhile formik is still keeping track of the other values since we're showing them in the UI. - const { label } = payload; - mutateAsync({ label }) - .then((response) => { - setSubmitting(false); - queryClient.invalidateQueries([placementGroupQueryKey]); + try { + const response = await mutateAsync(payload); - enqueueSnackbar( - `Placement Group ${payload.label} successfully updated`, - { - variant: 'success', - } - ); - - if (onPlacementGroupEdit) { - onPlacementGroupEdit(response); + setSubmitting(false); + enqueueSnackbar( + `Placement Group ${payload.label} successfully updated`, + { + variant: 'success', } - onClose(); - }) - .catch((err) => { - const mapErrorToStatus = () => - setStatus({ generalError: getErrorMap([], err).none }); + ); - setSubmitting(false); - handleFieldErrors(setErrors, err); - handleGeneralErrors( - mapErrorToStatus, - err, - 'Error renaming Placement Group.' - ); - }); + if (onPlacementGroupEdit) { + onPlacementGroupEdit(response); + } + onClose(); + } catch { + const mapErrorToStatus = () => + setStatus({ generalError: getErrorMap([], error).none }); + setSubmitting(false); + handleFieldErrors(setErrors, error ?? []); + handleGeneralErrors( + mapErrorToStatus, + error || [], + 'Error updating Placement Group.' + ); + } }, validateOnBlur: false, validateOnChange: hasFormBeenSubmitted, validationSchema: updatePlacementGroupSchema, }); + React.useEffect(() => { + resetForm(); + setHasFormBeenSubmitted(false); + }, [open, resetForm]); + + React.useEffect(() => { + if (isSubmitting) { + setHasFormBeenSubmitted(isSubmitting); + } + }, [isSubmitting]); + + const generalError = status?.generalError; + return ( - + {generalError && } + + Region: + {region ? `${region.label} (${region.id})` : 'Unknown'} + +
+ + + + + +
); }; diff --git a/packages/manager/src/features/PlacementGroups/PlacementGroupsLanding/PlacementGroupsLanding.tsx b/packages/manager/src/features/PlacementGroups/PlacementGroupsLanding/PlacementGroupsLanding.tsx index 0dad87cd756..6423a497121 100644 --- a/packages/manager/src/features/PlacementGroups/PlacementGroupsLanding/PlacementGroupsLanding.tsx +++ b/packages/manager/src/features/PlacementGroups/PlacementGroupsLanding/PlacementGroupsLanding.tsx @@ -34,9 +34,6 @@ const preferenceKey = 'placement-groups'; export const PlacementGroupsLanding = React.memo(() => { const history = useHistory(); const pagination = usePagination(1, preferenceKey); - const [selectedPlacementGroup, setSelectedPlacementGroup] = React.useState< - PlacementGroup | undefined - >(); const [query, setQuery] = React.useState(''); const { handleOrderChange, order, orderBy } = useOrder( { @@ -70,12 +67,10 @@ export const PlacementGroupsLanding = React.memo(() => { }; const handleEditPlacementGroup = (placementGroup: PlacementGroup) => { - setSelectedPlacementGroup(placementGroup); history.replace(`/placement-groups/edit/${placementGroup.id}`); }; const handleDeletePlacementGroup = (placementGroup: PlacementGroup) => { - setSelectedPlacementGroup(placementGroup); history.replace(`/placement-groups/delete/${placementGroup.id}`); }; @@ -98,7 +93,6 @@ export const PlacementGroupsLanding = React.memo(() => { openCreatePlacementGroupDrawer={handleCreatePlacementGroup} /> @@ -198,15 +192,12 @@ export const PlacementGroupsLanding = React.memo(() => { pageSize={pagination.pageSize} /> void; open: boolean; }; @@ -17,12 +12,8 @@ export type PlacementGroupsCreateDrawerProps = PlacementGroupsDrawerPropsBase & export type PlacementGroupsEditDrawerProps = PlacementGroupsDrawerPropsBase & { onPlacementGroupEdit?: (placementGroup: PlacementGroup) => void; - selectedPlacementGroup: PlacementGroup | undefined; }; -export type PlacementGroupDrawerFormikProps = UpdatePlacementGroupPayload & - CreatePlacementGroupPayload; - export type PlacementGroupsAssignLinodesDrawerProps = PlacementGroupsDrawerPropsBase & { onLinodeAddedToPlacementGroup?: (placementGroup: PlacementGroup) => void; selectedPlacementGroup: PlacementGroup | undefined;