From d5773f0bb22648e2213c4c2c99d4c3779f8a5169 Mon Sep 17 00:00:00 2001 From: Alban Bailly Date: Fri, 16 Feb 2024 10:18:12 -0500 Subject: [PATCH 01/20] Change "rename" nomenclature --- .../src/placement-groups/placement-groups.ts | 10 +++---- .../PlacementGroupsCreateDrawer.tsx | 6 ++--- .../PlacementGroupsDrawerContent.tsx | 12 ++++----- ...tsx => PlacementGroupsEditDrawer.test.tsx} | 6 ++--- ...awer.tsx => PlacementGroupsEditDrawer.tsx} | 27 ++++++++----------- .../PlacementGroupsLanding.tsx | 16 +++++------ .../PlacementGroupsRow.test.tsx | 6 ++--- .../PlacementGroupsRow.tsx | 8 +++--- .../src/features/PlacementGroups/index.tsx | 2 +- .../src/features/PlacementGroups/types.ts | 6 ++--- .../manager/src/queries/placementGroups.ts | 4 +-- .../validation/src/placement-groups.schema.ts | 2 +- 12 files changed, 50 insertions(+), 55 deletions(-) rename packages/manager/src/features/PlacementGroups/{PlacementGroupsRenameDrawer.test.tsx => PlacementGroupsEditDrawer.test.tsx} (91%) rename packages/manager/src/features/PlacementGroups/{PlacementGroupsRenameDrawer.tsx => PlacementGroupsEditDrawer.tsx} (84%) diff --git a/packages/api-v4/src/placement-groups/placement-groups.ts b/packages/api-v4/src/placement-groups/placement-groups.ts index 123149c8589..ca58aa147b7 100644 --- a/packages/api-v4/src/placement-groups/placement-groups.ts +++ b/packages/api-v4/src/placement-groups/placement-groups.ts @@ -1,6 +1,6 @@ import { createPlacementGroupSchema, - renamePlacementGroupSchema, + updatePlacementGroupSchema, } from '@linode/validation'; import { API_ROOT } from '../constants'; @@ -63,14 +63,14 @@ export const createPlacementGroup = (data: CreatePlacementGroupPayload) => ); /** - * renamePlacementGroup + * updatePlacementGroup * - * Renames a Placement Group (updates label). + * Updates a Placement Group (updates label). * * @param placementGroupId { number } The id of the Placement Group to be updated. * @param data { PlacementGroup } The data for the Placement Group. */ -export const renamePlacementGroup = ( +export const updatePlacementGroup = ( placementGroupId: number, data: UpdatePlacementGroupPayload ) => @@ -79,7 +79,7 @@ export const renamePlacementGroup = ( `${API_ROOT}/placement/groups/${encodeURIComponent(placementGroupId)}` ), setMethod('PUT'), - setData(data, renamePlacementGroupSchema) + setData(data, updatePlacementGroupSchema) ); /** diff --git a/packages/manager/src/features/PlacementGroups/PlacementGroupsCreateDrawer.tsx b/packages/manager/src/features/PlacementGroups/PlacementGroupsCreateDrawer.tsx index 46c709c88e4..0d367c5ddb2 100644 --- a/packages/manager/src/features/PlacementGroups/PlacementGroupsCreateDrawer.tsx +++ b/packages/manager/src/features/PlacementGroups/PlacementGroupsCreateDrawer.tsx @@ -29,7 +29,7 @@ export const PlacementGroupsCreateDrawer = ( const { numberOfPlacementGroupsCreated, onClose, - onPlacementGroupCreated, + onPlacementGroupCreate, open, selectedRegionId, } = props; @@ -82,8 +82,8 @@ export const PlacementGroupsCreateDrawer = ( } ); - if (onPlacementGroupCreated) { - onPlacementGroupCreated(response); + if (onPlacementGroupCreate) { + onPlacementGroupCreate(response); } onClose(); }) diff --git a/packages/manager/src/features/PlacementGroups/PlacementGroupsDrawerContent.tsx b/packages/manager/src/features/PlacementGroups/PlacementGroupsDrawerContent.tsx index f7e11486eca..f1496751363 100644 --- a/packages/manager/src/features/PlacementGroups/PlacementGroupsDrawerContent.tsx +++ b/packages/manager/src/features/PlacementGroups/PlacementGroupsDrawerContent.tsx @@ -18,7 +18,7 @@ import type { FormikProps } from 'formik'; interface Props { formik: FormikProps; maxNumberOfPlacementGroups?: number; - mode: 'create' | 'rename'; + mode: 'create' | 'update'; numberOfPlacementGroupsCreated?: number; onClose: () => void; open: boolean; @@ -65,7 +65,7 @@ export const PlacementGroupsDrawerContent = (props: Props) => { }, [isSubmitting]); const generalError = status?.generalError; - const isRenameDrawer = mode === 'rename'; + const isEditDrawer = mode === 'update'; return ( @@ -90,7 +90,7 @@ export const PlacementGroupsDrawerContent = (props: Props) => { setFieldValue('region', selection); }} currentCapability="Linodes" // TODO VM_Placement: change to Placement Groups when available - disabled={isRenameDrawer || Boolean(selectedRegionId)} + disabled={isEditDrawer || Boolean(selectedRegionId)} errorText={errors.region} regions={regions ?? []} selectedId={selectedRegionId ?? values.region} @@ -104,7 +104,7 @@ export const PlacementGroupsDrawerContent = (props: Props) => { (option) => option.value === values.affinity_type ) ?? null } - disabled={isRenameDrawer} + disabled={isEditDrawer} errorText={errors.affinity_type} label="Affinity Type" options={affinityTypeOptions} @@ -116,12 +116,12 @@ export const PlacementGroupsDrawerContent = (props: Props) => { disabled: // TODO VM_Placement: we may want to move this logic to the create button in the landing page // We just need to wait to wait to see how we're going to get the max number of PGs (account/region) - !isRenameDrawer && + !isEditDrawer && numberOfPlacementGroupsCreated && maxNumberOfPlacementGroups ? numberOfPlacementGroupsCreated >= maxNumberOfPlacementGroups : false, - label: `${isRenameDrawer ? 'Rename' : 'Create'} Placement Group`, + label: `${isEditDrawer ? 'Edit' : 'Create'} Placement Group`, loading: isSubmitting, tooltipText: MAX_NUMBER_OF_LINODES_IN_PLACEMENT_GROUP_MESSAGE, type: 'submit', diff --git a/packages/manager/src/features/PlacementGroups/PlacementGroupsRenameDrawer.test.tsx b/packages/manager/src/features/PlacementGroups/PlacementGroupsEditDrawer.test.tsx similarity index 91% rename from packages/manager/src/features/PlacementGroups/PlacementGroupsRenameDrawer.test.tsx rename to packages/manager/src/features/PlacementGroups/PlacementGroupsEditDrawer.test.tsx index d3964b92a2f..0b08f660c2a 100644 --- a/packages/manager/src/features/PlacementGroups/PlacementGroupsRenameDrawer.test.tsx +++ b/packages/manager/src/features/PlacementGroups/PlacementGroupsEditDrawer.test.tsx @@ -7,7 +7,7 @@ import { makeResourcePage } from 'src/mocks/serverHandlers'; import { rest, server } from 'src/mocks/testServer'; import { renderWithTheme } from 'src/utilities/testHelpers'; -import { PlacementGroupsRenameDrawer } from './PlacementGroupsRenameDrawer'; +import { PlacementGroupsEditDrawer } from './PlacementGroupsEditDrawer'; describe('PlacementGroupsCreateDrawer', () => { it('should render, have the proper fields populated with PG values, and have uneditable fields disabled', async () => { @@ -23,7 +23,7 @@ describe('PlacementGroupsCreateDrawer', () => { ); const { getByLabelText } = renderWithTheme( - { })} numberOfPlacementGroupsCreated={0} onClose={vi.fn()} - onPlacementGroupRenamed={vi.fn()} + onPlacementGroupEdit={vi.fn()} open={true} /> ); diff --git a/packages/manager/src/features/PlacementGroups/PlacementGroupsRenameDrawer.tsx b/packages/manager/src/features/PlacementGroups/PlacementGroupsEditDrawer.tsx similarity index 84% rename from packages/manager/src/features/PlacementGroups/PlacementGroupsRenameDrawer.tsx rename to packages/manager/src/features/PlacementGroups/PlacementGroupsEditDrawer.tsx index 0a898e3b52f..5a5c07aad43 100644 --- a/packages/manager/src/features/PlacementGroups/PlacementGroupsRenameDrawer.tsx +++ b/packages/manager/src/features/PlacementGroups/PlacementGroupsEditDrawer.tsx @@ -1,4 +1,4 @@ -import { renamePlacementGroupSchema } from '@linode/validation'; +import { updatePlacementGroupSchema } from '@linode/validation'; import { useFormik } from 'formik'; import { useSnackbar } from 'notistack'; import * as React from 'react'; @@ -19,18 +19,13 @@ import { PlacementGroupsDrawerContent } from './PlacementGroupsDrawerContent'; import type { PlacementGroupDrawerFormikProps, - PlacementGroupsRenameDrawerProps, + PlacementGroupsEditDrawerProps, } from './types'; -export const PlacementGroupsRenameDrawer = ( - props: PlacementGroupsRenameDrawerProps +export const PlacementGroupsEditDrawer = ( + props: PlacementGroupsEditDrawerProps ) => { - const { - onClose, - onPlacementGroupRenamed, - open, - selectedPlacementGroup, - } = props; + const { onClose, onPlacementGroupEdit, open, selectedPlacementGroup } = props; const queryClient = useQueryClient(); const { data: regions } = useRegionsQuery(); const { mutateAsync } = useMutatePlacementGroup( @@ -76,14 +71,14 @@ export const PlacementGroupsRenameDrawer = ( queryClient.invalidateQueries([placementGroupQueryKey]); enqueueSnackbar( - `Placement Group ${payload.label} successfully renamed`, + `Placement Group ${payload.label} successfully updated`, { variant: 'success', } ); - if (onPlacementGroupRenamed) { - onPlacementGroupRenamed(response); + if (onPlacementGroupEdit) { + onPlacementGroupEdit(response); } onClose(); }) @@ -102,14 +97,14 @@ export const PlacementGroupsRenameDrawer = ( }, validateOnBlur: false, validateOnChange: hasFormBeenSubmitted, - validationSchema: renamePlacementGroupSchema, + validationSchema: updatePlacementGroupSchema, }); return ( { history.replace('/placement-groups/create'); }; - const handleRenamePlacementGroup = (placementGroup: PlacementGroup) => { + const handleEditPlacementGroup = (placementGroup: PlacementGroup) => { setSelectedPlacementGroup(placementGroup); - history.replace(`/placement-groups/rename/${placementGroup.id}`); + history.replace(`/placement-groups/edit/${placementGroup.id}`); }; const handleDeletePlacementGroup = (placementGroup: PlacementGroup) => { @@ -84,8 +84,8 @@ export const PlacementGroupsLanding = React.memo(() => { }; const isPlacementGroupCreateDrawerOpen = location.pathname.endsWith('create'); - const isPlacementGroupRenameDrawerOpen = location.pathname.includes('rename'); const isPlacementGroupDeleteModalOpen = location.pathname.includes('delete'); + const isPlacementGroupEditDrawerOpen = location.pathname.includes('edit'); if (isLoading) { return ; @@ -180,8 +180,8 @@ export const PlacementGroupsLanding = React.memo(() => { handleDeletePlacementGroup={() => handleDeletePlacementGroup(placementGroup) } - handleRenamePlacementGroup={() => - handleRenamePlacementGroup(placementGroup) + handleEditPlacementGroup={() => + handleEditPlacementGroup(placementGroup) } key={`pg-${placementGroup.id}`} placementGroup={placementGroup} @@ -202,10 +202,10 @@ export const PlacementGroupsLanding = React.memo(() => { onClose={onClosePlacementGroupDrawer} open={isPlacementGroupCreateDrawerOpen} /> - { }); const handleDeletePlacementGroupMock = vi.fn(); -const handleRenamePlacementGroupMock = vi.fn(); +const handleEditPlacementGroupMock = vi.fn(); describe('PlacementGroupsLanding', () => { it('renders the columns with proper data', () => { @@ -81,7 +81,7 @@ describe('PlacementGroupsLanding', () => { region: 'us-east', })} handleDeletePlacementGroup={handleDeletePlacementGroupMock} - handleRenamePlacementGroup={handleRenamePlacementGroupMock} + handleEditPlacementGroup={handleEditPlacementGroupMock} /> ) ); @@ -94,7 +94,7 @@ describe('PlacementGroupsLanding', () => { '1' ); expect(getByText('Newark, NJ')).toBeInTheDocument(); - expect(getByRole('button', { name: 'Rename' })).toBeInTheDocument(); + expect(getByRole('button', { name: 'Edit' })).toBeInTheDocument(); expect(getByRole('button', { name: 'Delete' })).toBeInTheDocument(); }); }); diff --git a/packages/manager/src/features/PlacementGroups/PlacementGroupsLanding/PlacementGroupsRow.tsx b/packages/manager/src/features/PlacementGroups/PlacementGroupsLanding/PlacementGroupsRow.tsx index 55d778aae5f..2e70918849a 100644 --- a/packages/manager/src/features/PlacementGroups/PlacementGroupsLanding/PlacementGroupsRow.tsx +++ b/packages/manager/src/features/PlacementGroups/PlacementGroupsLanding/PlacementGroupsRow.tsx @@ -19,14 +19,14 @@ import type { Action } from 'src/components/ActionMenu/ActionMenu'; interface PlacementGroupsRowProps { handleDeletePlacementGroup: () => void; - handleRenamePlacementGroup: () => void; + handleEditPlacementGroup: () => void; placementGroup: PlacementGroup; } export const PlacementGroupsRow = React.memo( ({ handleDeletePlacementGroup, - handleRenamePlacementGroup, + handleEditPlacementGroup, placementGroup, }: PlacementGroupsRowProps) => { const { affinity_type, id, is_compliant, label } = placementGroup; @@ -35,8 +35,8 @@ export const PlacementGroupsRow = React.memo( }); const actions: Action[] = [ { - onClick: handleRenamePlacementGroup, - title: 'Rename', + onClick: handleEditPlacementGroup, + title: 'Edit', }, { onClick: handleDeletePlacementGroup, diff --git a/packages/manager/src/features/PlacementGroups/index.tsx b/packages/manager/src/features/PlacementGroups/index.tsx index b8e2e1f3b2b..aae3d8f8c7d 100644 --- a/packages/manager/src/features/PlacementGroups/index.tsx +++ b/packages/manager/src/features/PlacementGroups/index.tsx @@ -34,7 +34,7 @@ export const PlacementGroups = () => { void; + onPlacementGroupCreate?: (placementGroup: PlacementGroup) => void; selectedRegionId?: string; }; -export type PlacementGroupsRenameDrawerProps = PlacementGroupsDrawerPropsBase & { - onPlacementGroupRenamed?: (placementGroup: PlacementGroup) => void; +export type PlacementGroupsEditDrawerProps = PlacementGroupsDrawerPropsBase & { + onPlacementGroupEdit?: (placementGroup: PlacementGroup) => void; selectedPlacementGroup: PlacementGroup | undefined; }; diff --git a/packages/manager/src/queries/placementGroups.ts b/packages/manager/src/queries/placementGroups.ts index 8d9f8b8c2bf..802efb433d9 100644 --- a/packages/manager/src/queries/placementGroups.ts +++ b/packages/manager/src/queries/placementGroups.ts @@ -4,8 +4,8 @@ import { deletePlacementGroup, getPlacementGroup, getPlacementGroups, - renamePlacementGroup, unassignLinodesFromPlacementGroup, + updatePlacementGroup, } from '@linode/api-v4'; import { APIError, @@ -85,7 +85,7 @@ export const useMutatePlacementGroup = (id: number) => { const queryClient = useQueryClient(); return useMutation({ - mutationFn: (data) => renamePlacementGroup(id, data), + mutationFn: (data) => updatePlacementGroup(id, data), onSuccess: (placementGroup) => { queryClient.invalidateQueries([queryKey, 'paginated']); queryClient.setQueryData( diff --git a/packages/validation/src/placement-groups.schema.ts b/packages/validation/src/placement-groups.schema.ts index f8df8600df7..af8d32e1eeb 100644 --- a/packages/validation/src/placement-groups.schema.ts +++ b/packages/validation/src/placement-groups.schema.ts @@ -11,6 +11,6 @@ export const createPlacementGroupSchema = object({ region: string().required('Region is required.'), }); -export const renamePlacementGroupSchema = object({ +export const updatePlacementGroupSchema = object({ label: labelValidation, }); From 77ccd7af7c9d75dd6a3b1fd608fe542af60ac6ee Mon Sep 17 00:00:00 2001 From: Alban Bailly Date: Fri, 16 Feb 2024 16:40:20 -0500 Subject: [PATCH 02/20] Initial commit - refactor --- ...lacementGroupsAssignLinodesDrawer.test.tsx | 1 - .../PlacementGroupsCreateDrawer.test.tsx | 49 ++--- .../PlacementGroupsCreateDrawer.tsx | 176 +++++++++++------- .../PlacementGroupsDrawerContent.test.tsx | 43 ----- .../PlacementGroupsDrawerContent.tsx | 140 -------------- .../PlacementGroupsEditDrawer.test.tsx | 52 ++++-- .../PlacementGroupsEditDrawer.tsx | 169 ++++++++++------- .../PlacementGroupsLanding.tsx | 9 - .../src/features/PlacementGroups/types.ts | 11 +- 9 files changed, 264 insertions(+), 386 deletions(-) delete mode 100644 packages/manager/src/features/PlacementGroups/PlacementGroupsDrawerContent.test.tsx delete mode 100644 packages/manager/src/features/PlacementGroups/PlacementGroupsDrawerContent.tsx diff --git a/packages/manager/src/features/PlacementGroups/PlacementGroupsAssignLinodesDrawer.test.tsx b/packages/manager/src/features/PlacementGroups/PlacementGroupsAssignLinodesDrawer.test.tsx index 7a387dfaef2..09770be9be9 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(); @@ -30,10 +41,7 @@ describe('PlacementGroupsCreateDrawer', () => { it('Affinity Type select should have the correct options', async () => { const { getByPlaceholderText, getByText } = renderWithTheme( - + ); const inputElement = getByPlaceholderText('Select an Affinity Type'); @@ -46,32 +54,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', async () => { - server.use( - rest.get('*/regions', (req, res, ctx) => { - const regions = regionFactory.buildList(1, { - id: 'us-east', - label: 'Newark, NJ', - capabilities: ['Linodes'], - }); - return res(ctx.json(makeResourcePage(regions))); - }) - ); - const { getByLabelText } = renderWithTheme( diff --git a/packages/manager/src/features/PlacementGroups/PlacementGroupsCreateDrawer.tsx b/packages/manager/src/features/PlacementGroups/PlacementGroupsCreateDrawer.tsx index 0d367c5ddb2..7dbbfdae95b 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 '@tanstack/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 { MAX_NUMBER_OF_PLACEMENT_GROUPS } from './constants'; -import { PlacementGroupsDrawerContent } from './PlacementGroupsDrawerContent'; +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'], is_strict: true, label: '', region: selectedRegionId ?? '', }, - 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 0b08f660c2a..8b509c9fbcf 100644 --- a/packages/manager/src/features/PlacementGroups/PlacementGroupsEditDrawer.test.tsx +++ b/packages/manager/src/features/PlacementGroups/PlacementGroupsEditDrawer.test.tsx @@ -1,43 +1,70 @@ import { waitFor } from '@testing-library/react'; import * as React from 'react'; -import { regionFactory } from 'src/factories'; -import { placementGroupFactory } from 'src/factories/placementGroups'; +import { regionFactory } from 'src/factories'; import { makeResourcePage } from 'src/mocks/serverHandlers'; import { rest, server } from 'src/mocks/testServer'; 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', async () => { server.use( rest.get('*/regions', (req, res, ctx) => { const regions = regionFactory.buildList(1, { + capabilities: ['Linodes'], id: 'us-east', label: 'Fake Region, NC', - capabilities: ['Linodes'], }); return res(ctx.json(makeResourcePage(regions))); }) ); - const { getByLabelText } = renderWithTheme( + 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(); @@ -45,7 +72,6 @@ describe('PlacementGroupsCreateDrawer', () => { expect(getByLabelText('Region')).toHaveValue('Fake Region, NC (us-east)'); }); - 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 5a5c07aad43..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 '@tanstack/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'], - is_strict: true, label: selectedPlacementGroup?.label ?? '', - region: selectedPlacementGroup?.region ?? '', }, - 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; From 701a157cd44721a26f2745d4f49e0e84b5f94e96 Mon Sep 17 00:00:00 2001 From: Alban Bailly Date: Tue, 27 Feb 2024 13:33:53 -0500 Subject: [PATCH 03/20] Post rebase fix --- .../PlacementGroupsCreateDrawer.test.tsx | 3 -- .../PlacementGroupsEditDrawer.test.tsx | 46 +++++++++++-------- 2 files changed, 26 insertions(+), 23 deletions(-) diff --git a/packages/manager/src/features/PlacementGroups/PlacementGroupsCreateDrawer.test.tsx b/packages/manager/src/features/PlacementGroups/PlacementGroupsCreateDrawer.test.tsx index da490a0b01e..23646a03553 100644 --- a/packages/manager/src/features/PlacementGroups/PlacementGroupsCreateDrawer.test.tsx +++ b/packages/manager/src/features/PlacementGroups/PlacementGroupsCreateDrawer.test.tsx @@ -1,8 +1,5 @@ import { fireEvent, waitFor } from '@testing-library/react'; import * as React from 'react'; -import { regionFactory } from 'src/factories'; -import { makeResourcePage } from 'src/mocks/serverHandlers'; -import { rest, server } from 'src/mocks/testServer'; import { renderWithTheme } from 'src/utilities/testHelpers'; diff --git a/packages/manager/src/features/PlacementGroups/PlacementGroupsEditDrawer.test.tsx b/packages/manager/src/features/PlacementGroups/PlacementGroupsEditDrawer.test.tsx index 8b509c9fbcf..6103a6f9c97 100644 --- a/packages/manager/src/features/PlacementGroups/PlacementGroupsEditDrawer.test.tsx +++ b/packages/manager/src/features/PlacementGroups/PlacementGroupsEditDrawer.test.tsx @@ -1,9 +1,7 @@ -import { waitFor } from '@testing-library/react'; +import { fireEvent } from '@testing-library/react'; import * as React from 'react'; -import { regionFactory } from 'src/factories'; -import { makeResourcePage } from 'src/mocks/serverHandlers'; -import { rest, server } from 'src/mocks/testServer'; +import { placementGroupFactory, regionFactory } from 'src/factories'; import { renderWithTheme } from 'src/utilities/testHelpers'; import { PlacementGroupsEditDrawer } from './PlacementGroupsEditDrawer'; @@ -15,6 +13,7 @@ const queryMocks = vi.hoisted(() => ({ }), useParams: vi.fn().mockReturnValue({}), usePlacementGroupQuery: vi.fn().mockReturnValue({}), + useRegionsQuery: vi.fn().mockReturnValue({}), })); vi.mock('react-router-dom', async () => { @@ -25,6 +24,14 @@ vi.mock('react-router-dom', async () => { }; }); +vi.mock('src/queries/regions', async () => { + const actual = await vi.importActual('src/queries/regions'); + return { + ...actual, + useRegionsQuery: queryMocks.useRegionsQuery, + }; +}); + vi.mock('src/queries/placementGroups', async () => { const actual = await vi.importActual('src/queries/placementGroups'); return { @@ -36,16 +43,18 @@ vi.mock('src/queries/placementGroups', async () => { describe('PlacementGroupsCreateDrawer', () => { it('should render, have the proper fields populated with PG values, and have uneditable fields disabled', async () => { - server.use( - rest.get('*/regions', (req, res, ctx) => { - const regions = regionFactory.buildList(1, { - capabilities: ['Linodes'], - id: 'us-east', - label: 'Fake Region, NC', - }); - return res(ctx.json(makeResourcePage(regions))); - }) - ); + queryMocks.useParams.mockReturnValue({ id: '1' }); + queryMocks.useRegionsQuery.mockReturnValue({ + data: regionFactory.buildList(1, { id: 'us-east', label: 'Newark, NJ' }), + }); + queryMocks.usePlacementGroupQuery.mockReturnValue({ + data: placementGroupFactory.build({ + affinity_type: 'anti_affinity', + id: 1, + label: 'PG-to-edit', + region: 'us-east', + }), + }); const { getByLabelText, getByRole, getByText } = renderWithTheme( { expect(getByLabelText('Label')).toBeEnabled(); expect(getByLabelText('Label')).toHaveValue('PG-to-edit'); expect(getByRole('button', { name: 'Cancel' })).toBeEnabled(); - // const editButton = getByRole('button', { name: 'Edit' }); - expect(getByLabelText('Region')).toBeDisabled(); - - await waitFor(() => { - expect(getByLabelText('Region')).toHaveValue('Fake Region, NC (us-east)'); - }); + const editButton = getByRole('button', { name: 'Edit' }); + expect(editButton).toBeEnabled(); + fireEvent.click(editButton); expect(queryMocks.useMutatePlacementGroup).toHaveBeenCalled(); }); From fd841e99f322ede1e424ee3dd20a052e9f518c74 Mon Sep 17 00:00:00 2001 From: Alban Bailly Date: Tue, 27 Feb 2024 16:06:48 -0500 Subject: [PATCH 04/20] Affinity Enforcement --- .../PlacementGroupsCreateDrawer.tsx | 53 +++++++++++++++++++ .../validation/src/placement-groups.schema.ts | 3 +- 2 files changed, 55 insertions(+), 1 deletion(-) diff --git a/packages/manager/src/features/PlacementGroups/PlacementGroupsCreateDrawer.tsx b/packages/manager/src/features/PlacementGroups/PlacementGroupsCreateDrawer.tsx index 7dbbfdae95b..ff588669323 100644 --- a/packages/manager/src/features/PlacementGroups/PlacementGroupsCreateDrawer.tsx +++ b/packages/manager/src/features/PlacementGroups/PlacementGroupsCreateDrawer.tsx @@ -5,11 +5,17 @@ import * as React from 'react'; import { ActionsPanel } from 'src/components/ActionsPanel/ActionsPanel'; import { Autocomplete } from 'src/components/Autocomplete/Autocomplete'; +import { Box } from 'src/components/Box'; import { Drawer } from 'src/components/Drawer'; +import { FormControlLabel } from 'src/components/FormControlLabel'; +import { FormLabel } from 'src/components/FormLabel'; import { Notice } from 'src/components/Notice/Notice'; +import { Radio } from 'src/components/Radio/Radio'; +import { RadioGroup } from 'src/components/RadioGroup'; import { RegionSelect } from 'src/components/RegionSelect/RegionSelect'; 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 { useCreatePlacementGroup } from 'src/queries/placementGroups'; import { useRegionsQuery } from 'src/queries/regions'; @@ -151,6 +157,53 @@ export const PlacementGroupsCreateDrawer = ( options={affinityTypeOptions} placeholder="Select an Affinity Type" /> + + + + Affinity Enforcement + + {errors.is_strict !== undefined && ( + + {errors.is_strict} + + )} + { + handleChange(event); + setFieldValue('is_strict', event.target.value === 'true'); + }} + id="affinity-enforcement-radio-group" + name="is_strict" + value={values.is_strict} + > + + Strict. You cannot assign a Linode to your + placement group if it will violate the policy of your + selected Affinity Type (best practice). + + } + control={} + value={true} + /> + + Flexible. Flexible. You can assign a Linode + to your placement group, even if it violates the policy of + your selected Affinity Type. + + } + control={} + sx={{ mt: 2 }} + value={false} + /> + + Date: Tue, 27 Feb 2024 16:16:05 -0500 Subject: [PATCH 05/20] test --- .../PlacementGroups/PlacementGroupsCreateDrawer.test.tsx | 7 ++++++- .../PlacementGroups/PlacementGroupsCreateDrawer.tsx | 5 ----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/packages/manager/src/features/PlacementGroups/PlacementGroupsCreateDrawer.test.tsx b/packages/manager/src/features/PlacementGroups/PlacementGroupsCreateDrawer.test.tsx index 23646a03553..de5f148df35 100644 --- a/packages/manager/src/features/PlacementGroups/PlacementGroupsCreateDrawer.test.tsx +++ b/packages/manager/src/features/PlacementGroups/PlacementGroupsCreateDrawer.test.tsx @@ -27,13 +27,18 @@ vi.mock('src/queries/placementGroups', async () => { describe('PlacementGroupsCreateDrawer', () => { it('should render and have its fields enabled', () => { - const { getByLabelText } = renderWithTheme( + const { getAllByRole, getByLabelText, getByText } = renderWithTheme( ); expect(getByLabelText('Label')).toBeEnabled(); expect(getByLabelText('Region')).toBeEnabled(); expect(getByLabelText('Affinity Type')).toBeEnabled(); + expect(getByText('Affinity Enforcement')).toBeInTheDocument(); + + const radioInputs = getAllByRole('radio'); + expect(radioInputs).toHaveLength(2); + expect(radioInputs[0]).toBeChecked(); }); it('Affinity Type select should have the correct options', async () => { diff --git a/packages/manager/src/features/PlacementGroups/PlacementGroupsCreateDrawer.tsx b/packages/manager/src/features/PlacementGroups/PlacementGroupsCreateDrawer.tsx index ff588669323..92363404256 100644 --- a/packages/manager/src/features/PlacementGroups/PlacementGroupsCreateDrawer.tsx +++ b/packages/manager/src/features/PlacementGroups/PlacementGroupsCreateDrawer.tsx @@ -165,11 +165,6 @@ export const PlacementGroupsCreateDrawer = ( Affinity Enforcement - {errors.is_strict !== undefined && ( - - {errors.is_strict} - - )} { handleChange(event); From f6da73a1bfbb2f702f163d28b838805d885090c5 Mon Sep 17 00:00:00 2001 From: Alban Bailly Date: Tue, 27 Feb 2024 23:29:30 -0500 Subject: [PATCH 06/20] pg/account limit --- packages/manager/src/__data__/regionsData.ts | 24 +++++++++++- .../PlacementGroupsCreateDrawer.tsx | 38 ++++++++++++++++--- .../src/features/PlacementGroups/utils.ts | 14 +++++++ 3 files changed, 69 insertions(+), 7 deletions(-) diff --git a/packages/manager/src/__data__/regionsData.ts b/packages/manager/src/__data__/regionsData.ts index 6e44c8dd739..22f6791a025 100644 --- a/packages/manager/src/__data__/regionsData.ts +++ b/packages/manager/src/__data__/regionsData.ts @@ -12,6 +12,7 @@ export const regions: Region[] = [ 'Vlans', 'Block Storage Migrations', 'Managed Databases', + 'Placement Group', ], country: 'in', id: 'ap-west', @@ -37,6 +38,7 @@ export const regions: Region[] = [ 'Vlans', 'Block Storage Migrations', 'Managed Databases', + 'Placement Group', ], country: 'ca', id: 'ca-central', @@ -62,6 +64,7 @@ export const regions: Region[] = [ 'Vlans', 'Block Storage Migrations', 'Managed Databases', + 'Placement Group', ], country: 'au', id: 'ap-southeast', @@ -89,6 +92,7 @@ export const regions: Region[] = [ 'Managed Databases', 'Metadata', 'Premium Plans', + 'Placement Group', ], country: 'us', id: 'us-iad', @@ -143,6 +147,7 @@ export const regions: Region[] = [ 'Managed Databases', 'Metadata', 'Premium Plans', + 'Placement Group', ], country: 'fr', id: 'fr-par', @@ -169,6 +174,7 @@ export const regions: Region[] = [ 'Vlans', 'Metadata', 'Premium Plans', + 'Placement Group', ], country: 'us', id: 'us-sea', @@ -195,6 +201,7 @@ export const regions: Region[] = [ 'Vlans', 'Metadata', 'Premium Plans', + 'Placement Group', ], country: 'br', id: 'br-gru', @@ -221,6 +228,7 @@ export const regions: Region[] = [ 'Vlans', 'Metadata', 'Premium Plans', + 'Placement Group', ], country: 'nl', id: 'nl-ams', @@ -247,6 +255,7 @@ export const regions: Region[] = [ 'Vlans', 'Metadata', 'Premium Plans', + 'Placement Group', ], country: 'se', id: 'se-sto', @@ -273,6 +282,7 @@ export const regions: Region[] = [ 'Vlans', 'Metadata', 'Premium Plans', + 'Placement Group', ], country: 'in', id: 'in-maa', @@ -299,6 +309,7 @@ export const regions: Region[] = [ 'Vlans', 'Metadata', 'Premium Plans', + 'Placement Group', ], country: 'jp', id: 'jp-osa', @@ -325,6 +336,7 @@ export const regions: Region[] = [ 'Vlans', 'Metadata', 'Premium Plans', + 'Placement Group', ], country: 'it', id: 'it-mil', @@ -351,6 +363,7 @@ export const regions: Region[] = [ 'Vlans', 'Metadata', 'Premium Plans', + 'Placement Group', ], country: 'us', id: 'us-mia', @@ -377,6 +390,7 @@ export const regions: Region[] = [ 'Vlans', 'Metadata', 'Premium Plans', + 'Placement Group', ], country: 'id', id: 'id-cgk', @@ -403,6 +417,7 @@ export const regions: Region[] = [ 'Vlans', 'Metadata', 'Premium Plans', + 'Placement Group', ], country: 'us', id: 'us-lax', @@ -428,6 +443,7 @@ export const regions: Region[] = [ 'Vlans', 'Block Storage Migrations', 'Managed Databases', + 'Placement Group', ], country: 'us', id: 'us-central', @@ -452,11 +468,12 @@ export const regions: Region[] = [ 'Cloud Firewall', 'Block Storage Migrations', 'Managed Databases', + 'Placement Group', ], country: 'us', id: 'us-west', label: 'Fremont, CA', - maximum_pgs_per_customer: 5, + maximum_pgs_per_customer: 0, maximum_vms_per_pg: 10, resolvers: { ipv4: @@ -479,6 +496,7 @@ export const regions: Region[] = [ 'Vlans', 'Block Storage Migrations', 'Managed Databases', + 'Placement Group', ], country: 'us', id: 'us-southeast', @@ -507,6 +525,7 @@ export const regions: Region[] = [ 'Vlans', 'Block Storage Migrations', 'Managed Databases', + 'Placement Group', ], country: 'us', id: 'us-east', @@ -532,6 +551,7 @@ export const regions: Region[] = [ 'Vlans', 'Block Storage Migrations', 'Managed Databases', + 'Placement Group', ], country: 'gb', id: 'eu-west', @@ -559,6 +579,7 @@ export const regions: Region[] = [ 'Vlans', 'Block Storage Migrations', 'Managed Databases', + 'Placement Group', ], country: 'sg', id: 'ap-south', @@ -586,6 +607,7 @@ export const regions: Region[] = [ 'Vlans', 'Block Storage Migrations', 'Managed Databases', + 'Placement Group', ], country: 'de', id: 'eu-central', diff --git a/packages/manager/src/features/PlacementGroups/PlacementGroupsCreateDrawer.tsx b/packages/manager/src/features/PlacementGroups/PlacementGroupsCreateDrawer.tsx index 92363404256..8f8b4f90d21 100644 --- a/packages/manager/src/features/PlacementGroups/PlacementGroupsCreateDrawer.tsx +++ b/packages/manager/src/features/PlacementGroups/PlacementGroupsCreateDrawer.tsx @@ -25,10 +25,13 @@ import { handleGeneralErrors, } from 'src/utilities/formikErrorUtils'; -import { affinityTypeOptions } from './utils'; +import { + affinityTypeOptions, + hasRegionReachedPlacementGroupCapacity, +} from './utils'; import type { PlacementGroupsCreateDrawerProps } from './types'; -import type { CreatePlacementGroupPayload } from '@linode/api-v4'; +import type { CreatePlacementGroupPayload, Region } from '@linode/api-v4'; export const PlacementGroupsCreateDrawer = ( props: PlacementGroupsCreateDrawerProps @@ -41,6 +44,18 @@ export const PlacementGroupsCreateDrawer = ( hasFormBeenSubmitted, setHasFormBeenSubmitted, } = useFormValidateOnChange(); + const [hasRegionCapacity, setHasRegionCapacity] = React.useState( + true + ); + + const handleRegionSelect = (region: Region['id']) => { + const selectedRegion = regions?.find((r) => r.id === region); + + setFieldValue('region', region); + setHasRegionCapacity( + hasRegionReachedPlacementGroupCapacity(selectedRegion) + ); + }; const { errors, @@ -105,6 +120,7 @@ export const PlacementGroupsCreateDrawer = ( React.useEffect(() => { resetForm(); setHasFormBeenSubmitted(false); + setHasRegionCapacity(true); }, [open, resetForm]); React.useEffect(() => { @@ -134,12 +150,17 @@ export const PlacementGroupsCreateDrawer = ( value={values.label} /> { - setFieldValue('region', selection); + handleRegionSelect(selection); }} - currentCapability="Linodes" // TODO VM_Placement: change to Placement Groups when available + currentCapability="Placement Group" disabled={Boolean(selectedRegionId)} - errorText={errors.region} + helperText="Only regions supporting Placement Groups are listed." regions={regions ?? []} selectedId={selectedRegionId ?? values.region} /> @@ -147,11 +168,15 @@ export const PlacementGroupsCreateDrawer = ( onChange={(_, value) => { setFieldValue('affinity_type', value?.value ?? ''); }} + textFieldProps={{ + tooltipText: 'TODO VM_Placement: update copy', + }} value={ affinityTypeOptions.find( (option) => option.value === values.affinity_type - ) ?? null + ) ?? undefined } + disableClearable={true} errorText={errors.affinity_type} label="Affinity Type" options={affinityTypeOptions} @@ -202,6 +227,7 @@ export const PlacementGroupsCreateDrawer = ( { + if (!region) { + return false; + } + + return region.maximum_pgs_per_customer > 0; +}; + /** * Helper to populate the affinity_type select options. */ From 275ea8ae66c1aab4186e07e18374ff077436d230 Mon Sep 17 00:00:00 2001 From: Alban Bailly Date: Wed, 28 Feb 2024 10:01:42 -0500 Subject: [PATCH 07/20] update tests --- .../PlacementGroupsCreateDrawer.test.tsx | 44 +++++++++++++++++++ .../PlacementGroupsCreateDrawer.tsx | 6 +-- .../features/PlacementGroups/utils.test.ts | 23 ++++++++++ 3 files changed, 70 insertions(+), 3 deletions(-) diff --git a/packages/manager/src/features/PlacementGroups/PlacementGroupsCreateDrawer.test.tsx b/packages/manager/src/features/PlacementGroups/PlacementGroupsCreateDrawer.test.tsx index de5f148df35..1f3695f304c 100644 --- a/packages/manager/src/features/PlacementGroups/PlacementGroupsCreateDrawer.test.tsx +++ b/packages/manager/src/features/PlacementGroups/PlacementGroupsCreateDrawer.test.tsx @@ -68,4 +68,48 @@ describe('PlacementGroupsCreateDrawer', () => { expect(getByLabelText('Region')).toHaveValue('Newark, NJ (us-east)'); }); }); + + it('should call the mutation when the form is submitted', async () => { + const { + getByLabelText, + getByPlaceholderText, + getByRole, + getByText, + } = renderWithTheme(); + + fireEvent.change(getByLabelText('Label'), { + target: { value: 'my-label' }, + }); + + const regionSelect = getByPlaceholderText('Select a Region'); + fireEvent.focus(regionSelect); + fireEvent.change(regionSelect, { + target: { value: 'Newark, NJ (us-east)' }, + }); + await waitFor(() => { + const selectedRegionOption = getByText('Newark, NJ (us-east)'); + fireEvent.click(selectedRegionOption); + }); + + const affinityTypeSelect = getByPlaceholderText('Select an Affinity Type'); + fireEvent.focus(affinityTypeSelect); + fireEvent.change(affinityTypeSelect, { target: { value: 'Affinity' } }); + await waitFor(() => { + const selectedAffinityTypeOption = getByText('Affinity'); + fireEvent.click(selectedAffinityTypeOption); + }); + + fireEvent.click(getByRole('button', { name: 'Create Placement Group' })); + + await waitFor(() => { + expect( + queryMocks.useCreatePlacementGroup().mutateAsync + ).toHaveBeenCalledWith({ + affinity_type: 'affinity', + is_strict: true, + label: 'my-label', + region: 'us-east', + }); + }); + }); }); diff --git a/packages/manager/src/features/PlacementGroups/PlacementGroupsCreateDrawer.tsx b/packages/manager/src/features/PlacementGroups/PlacementGroupsCreateDrawer.tsx index 8f8b4f90d21..9607e23b33b 100644 --- a/packages/manager/src/features/PlacementGroups/PlacementGroupsCreateDrawer.tsx +++ b/packages/manager/src/features/PlacementGroups/PlacementGroupsCreateDrawer.tsx @@ -213,9 +213,9 @@ export const PlacementGroupsCreateDrawer = ( - Flexible. Flexible. You can assign a Linode - to your placement group, even if it violates the policy of - your selected Affinity Type. + Flexible. You can assign a Linode to your + placement group, even if it violates the policy of your + selected Affinity Type. } control={} diff --git a/packages/manager/src/features/PlacementGroups/utils.test.ts b/packages/manager/src/features/PlacementGroups/utils.test.ts index b18bbbd8f08..36013ebf7b3 100644 --- a/packages/manager/src/features/PlacementGroups/utils.test.ts +++ b/packages/manager/src/features/PlacementGroups/utils.test.ts @@ -6,6 +6,7 @@ import { getLinodesFromAllPlacementGroups, getPlacementGroupLinodeCount, hasPlacementGroupReachedCapacity, + hasRegionReachedPlacementGroupCapacity, } from './utils'; import type { PlacementGroup } from '@linode/api-v4'; @@ -115,3 +116,25 @@ describe('getAffinityEnforcement', () => { expect(getAffinityEnforcement(false)).toBe('Flexible'); }); }); + +describe('hasRegionReachedPlacementGroupCapacity', () => { + it('returns true if the region has reached its placement group capacity', () => { + expect( + hasRegionReachedPlacementGroupCapacity( + regionFactory.build({ + maximum_pgs_per_customer: 1, + }) + ) + ).toBe(true); + }); + + it('returns false if the region has not reached its placement group capacity', () => { + expect( + hasRegionReachedPlacementGroupCapacity( + regionFactory.build({ + maximum_pgs_per_customer: 0, + }) + ) + ).toBe(false); + }); +}); From af890ad89f6a0e78b6dfc6d9520fe18fa3d80bf0 Mon Sep 17 00:00:00 2001 From: Alban Bailly Date: Wed, 28 Feb 2024 13:27:25 -0500 Subject: [PATCH 08/20] Handle region PG limit --- .../PlacementGroupsCreateDrawer.test.tsx | 1 + .../PlacementGroupsCreateDrawer.tsx | 134 ++++++++++-------- .../PlacementGroupsLanding.tsx | 2 + .../src/features/PlacementGroups/types.ts | 1 + .../features/PlacementGroups/utils.test.ts | 28 ++-- .../src/features/PlacementGroups/utils.ts | 32 ++++- 6 files changed, 123 insertions(+), 75 deletions(-) diff --git a/packages/manager/src/features/PlacementGroups/PlacementGroupsCreateDrawer.test.tsx b/packages/manager/src/features/PlacementGroups/PlacementGroupsCreateDrawer.test.tsx index 1f3695f304c..eaaef4f3a02 100644 --- a/packages/manager/src/features/PlacementGroups/PlacementGroupsCreateDrawer.test.tsx +++ b/packages/manager/src/features/PlacementGroups/PlacementGroupsCreateDrawer.test.tsx @@ -6,6 +6,7 @@ import { renderWithTheme } from 'src/utilities/testHelpers'; import { PlacementGroupsCreateDrawer } from './PlacementGroupsCreateDrawer'; const commonProps = { + allExistingPlacementGroups: [], onClose: vi.fn(), open: true, }; diff --git a/packages/manager/src/features/PlacementGroups/PlacementGroupsCreateDrawer.tsx b/packages/manager/src/features/PlacementGroups/PlacementGroupsCreateDrawer.tsx index 9607e23b33b..6e1d51b4eec 100644 --- a/packages/manager/src/features/PlacementGroups/PlacementGroupsCreateDrawer.tsx +++ b/packages/manager/src/features/PlacementGroups/PlacementGroupsCreateDrawer.tsx @@ -32,11 +32,18 @@ import { import type { PlacementGroupsCreateDrawerProps } from './types'; import type { CreatePlacementGroupPayload, Region } from '@linode/api-v4'; +import type { FormikHelpers } from 'formik'; export const PlacementGroupsCreateDrawer = ( props: PlacementGroupsCreateDrawerProps ) => { - const { onClose, onPlacementGroupCreate, open, selectedRegionId } = props; + const { + allExistingPlacementGroups, + onClose, + onPlacementGroupCreate, + open, + selectedRegionId, + } = props; const { data: regions } = useRegionsQuery(); const { error, mutateAsync } = useCreatePlacementGroup(); const { enqueueSnackbar } = useSnackbar(); @@ -44,19 +51,73 @@ export const PlacementGroupsCreateDrawer = ( hasFormBeenSubmitted, setHasFormBeenSubmitted, } = useFormValidateOnChange(); - const [hasRegionCapacity, setHasRegionCapacity] = React.useState( - true - ); + const [ + hasRegionReachedPGCapacity, + setHasRegionReachedPGCapacity, + ] = React.useState(true); const handleRegionSelect = (region: Region['id']) => { const selectedRegion = regions?.find((r) => r.id === region); setFieldValue('region', region); - setHasRegionCapacity( - hasRegionReachedPlacementGroupCapacity(selectedRegion) + setHasRegionReachedPGCapacity( + hasRegionReachedPlacementGroupCapacity({ + allPlacementGroups: allExistingPlacementGroups, + region: selectedRegion, + }) ); }; + const handleDrawerOpen = () => { + resetForm(); + setHasFormBeenSubmitted(false); + setHasRegionReachedPGCapacity(false); + }; + + const handleDrawerClose = () => { + onClose(); + handleDrawerOpen(); // Reset form and state when drawer closes + }; + + const handleFormSubmit = async ( + values: CreatePlacementGroupPayload, + { + setErrors, + setStatus, + setSubmitting, + }: FormikHelpers + ) => { + setHasFormBeenSubmitted(false); + setStatus(undefined); + setErrors({}); + const payload = { ...values }; + + try { + const response = await mutateAsync(payload); + setSubmitting(false); + + enqueueSnackbar(`Placement Group ${payload.label} successfully created`, { + variant: 'success', + }); + + if (onPlacementGroupCreate) { + onPlacementGroupCreate(response); + } + onClose(); + } catch { + const mapErrorToStatus = () => + setStatus({ generalError: getErrorMap([], error).none }); + + setSubmitting(false); + handleFieldErrors(setErrors, error ?? []); + handleGeneralErrors( + mapErrorToStatus, + error ?? [], + 'Error creating Placement Group.' + ); + } + }; + const { errors, handleBlur, @@ -75,64 +136,20 @@ export const PlacementGroupsCreateDrawer = ( label: '', region: selectedRegionId ?? '', }, - async onSubmit( - values: CreatePlacementGroupPayload, - { setErrors, setStatus, setSubmitting } - ) { - setHasFormBeenSubmitted(false); - setStatus(undefined); - setErrors({}); - const payload = { ...values }; - - try { - const response = await mutateAsync(payload); - setSubmitting(false); - - enqueueSnackbar( - `Placement Group ${payload.label} successfully created`, - { - variant: 'success', - } - ); - - if (onPlacementGroupCreate) { - onPlacementGroupCreate(response); - } - onClose(); - } catch { - const mapErrorToStatus = () => - setStatus({ generalError: getErrorMap([], error).none }); - - setSubmitting(false); - handleFieldErrors(setErrors, error ?? []); - handleGeneralErrors( - mapErrorToStatus, - error ?? [], - 'Error creating Placement Group.' - ); - } - }, + onSubmit: handleFormSubmit, validateOnBlur: false, validateOnChange: hasFormBeenSubmitted, validationSchema: createPlacementGroupSchema, }); - React.useEffect(() => { - resetForm(); - setHasFormBeenSubmitted(false); - setHasRegionCapacity(true); - }, [open, resetForm]); - - React.useEffect(() => { - if (isSubmitting) { - setHasFormBeenSubmitted(isSubmitting); - } - }, [isSubmitting]); - const generalError = status?.generalError; return ( - +
{generalError && } @@ -151,7 +168,7 @@ export const PlacementGroupsCreateDrawer = ( /> setHasFormBeenSubmitted(true), type: 'submit', }} secondaryButtonProps={{ diff --git a/packages/manager/src/features/PlacementGroups/PlacementGroupsLanding/PlacementGroupsLanding.tsx b/packages/manager/src/features/PlacementGroups/PlacementGroupsLanding/PlacementGroupsLanding.tsx index 6423a497121..63c40054990 100644 --- a/packages/manager/src/features/PlacementGroups/PlacementGroupsLanding/PlacementGroupsLanding.tsx +++ b/packages/manager/src/features/PlacementGroups/PlacementGroupsLanding/PlacementGroupsLanding.tsx @@ -93,6 +93,7 @@ export const PlacementGroupsLanding = React.memo(() => { openCreatePlacementGroupDrawer={handleCreatePlacementGroup} /> @@ -192,6 +193,7 @@ export const PlacementGroupsLanding = React.memo(() => { pageSize={pagination.pageSize} /> diff --git a/packages/manager/src/features/PlacementGroups/types.ts b/packages/manager/src/features/PlacementGroups/types.ts index 113194dae6d..9669377c5aa 100644 --- a/packages/manager/src/features/PlacementGroups/types.ts +++ b/packages/manager/src/features/PlacementGroups/types.ts @@ -6,6 +6,7 @@ export type PlacementGroupsDrawerPropsBase = { }; export type PlacementGroupsCreateDrawerProps = PlacementGroupsDrawerPropsBase & { + allExistingPlacementGroups: PlacementGroup[]; onPlacementGroupCreate?: (placementGroup: PlacementGroup) => void; selectedRegionId?: string; }; diff --git a/packages/manager/src/features/PlacementGroups/utils.test.ts b/packages/manager/src/features/PlacementGroups/utils.test.ts index 36013ebf7b3..14de684c201 100644 --- a/packages/manager/src/features/PlacementGroups/utils.test.ts +++ b/packages/manager/src/features/PlacementGroups/utils.test.ts @@ -120,21 +120,29 @@ describe('getAffinityEnforcement', () => { describe('hasRegionReachedPlacementGroupCapacity', () => { it('returns true if the region has reached its placement group capacity', () => { expect( - hasRegionReachedPlacementGroupCapacity( - regionFactory.build({ - maximum_pgs_per_customer: 1, - }) - ) + hasRegionReachedPlacementGroupCapacity({ + allPlacementGroups: placementGroupFactory.buildList(3, { + region: 'us-east', + }), + region: regionFactory.build({ + id: 'us-east', + maximum_pgs_per_customer: 2, + }), + }) ).toBe(true); }); it('returns false if the region has not reached its placement group capacity', () => { expect( - hasRegionReachedPlacementGroupCapacity( - regionFactory.build({ - maximum_pgs_per_customer: 0, - }) - ) + hasRegionReachedPlacementGroupCapacity({ + allPlacementGroups: placementGroupFactory.buildList(3, { + region: 'us-east', + }), + region: regionFactory.build({ + id: 'us-east', + maximum_pgs_per_customer: 4, + }), + }) ).toBe(false); }); }); diff --git a/packages/manager/src/features/PlacementGroups/utils.ts b/packages/manager/src/features/PlacementGroups/utils.ts index c92eb23b598..3e35c27103e 100644 --- a/packages/manager/src/features/PlacementGroups/utils.ts +++ b/packages/manager/src/features/PlacementGroups/utils.ts @@ -31,7 +31,9 @@ interface HasPlacementGroupReachedCapacityOptions { } /** - * Helper to determine if a Placement Group has reached capacity. + * Helper to determine if a Placement Group has reached its linode capacity. + * + * based on the region's `maximum_vms_per_pg`. */ export const hasPlacementGroupReachedCapacity = ({ placementGroup, @@ -46,17 +48,33 @@ export const hasPlacementGroupReachedCapacity = ({ ); }; +interface HasRegionReachedPlacementGroupCapacityOptions { + allPlacementGroups: PlacementGroup[] | undefined; + region: Region | undefined; +} + /** - * Helper to determine if a region has reached its placement group capacity for the user. + * Helper to determine if a region has reached its placement group capacity. + * + * based on the region's `maximum_pgs_per_customer`. */ -export const hasRegionReachedPlacementGroupCapacity = ( - region: Region | undefined -): boolean => { - if (!region) { +export const hasRegionReachedPlacementGroupCapacity = ({ + allPlacementGroups, + region, +}: HasRegionReachedPlacementGroupCapacityOptions): boolean => { + if (!region || !allPlacementGroups) { return false; } - return region.maximum_pgs_per_customer > 0; + const { maximum_pgs_per_customer } = region; + const placementGroupsInRegion = allPlacementGroups.filter( + (pg) => pg.region === region.id + ); + + return ( + placementGroupsInRegion.length >= maximum_pgs_per_customer || + maximum_pgs_per_customer === 0 + ); }; /** From b86a47bce206be6ffe45f12967efa122573122be Mon Sep 17 00:00:00 2001 From: Alban Bailly Date: Wed, 28 Feb 2024 16:13:03 -0500 Subject: [PATCH 09/20] Handle region PG limit test --- .../PlacementGroupsCreateDrawer.test.tsx | 27 +++++++++- .../PlacementGroupsCreateDrawer.tsx | 54 +++++++++++-------- 2 files changed, 56 insertions(+), 25 deletions(-) diff --git a/packages/manager/src/features/PlacementGroups/PlacementGroupsCreateDrawer.test.tsx b/packages/manager/src/features/PlacementGroups/PlacementGroupsCreateDrawer.test.tsx index eaaef4f3a02..31c03e6abd7 100644 --- a/packages/manager/src/features/PlacementGroups/PlacementGroupsCreateDrawer.test.tsx +++ b/packages/manager/src/features/PlacementGroups/PlacementGroupsCreateDrawer.test.tsx @@ -58,7 +58,7 @@ describe('PlacementGroupsCreateDrawer', () => { }); it('should populate the region select with the selected region prop', async () => { - const { getByLabelText } = renderWithTheme( + const { getByTestId } = renderWithTheme( { ); await waitFor(() => { - expect(getByLabelText('Region')).toHaveValue('Newark, NJ (us-east)'); + expect(getByTestId('selected-region')).toHaveTextContent( + 'Newark, NJ (us-east)' + ); }); }); @@ -113,4 +115,25 @@ describe('PlacementGroupsCreateDrawer', () => { }); }); }); + + it('should display an error message if the region has reached capacity', async () => { + const regionWithoutCapacity = 'Fremont, CA (us-west)'; + const { getByPlaceholderText, getByText } = renderWithTheme( + + ); + + const regionSelect = getByPlaceholderText('Select a Region'); + fireEvent.focus(regionSelect); + fireEvent.change(regionSelect, { + target: { value: regionWithoutCapacity }, + }); + await waitFor(() => { + const selectedRegionOption = getByText(regionWithoutCapacity); + fireEvent.click(selectedRegionOption); + }); + + await waitFor(() => { + expect(getByText('This region has reached capacity')).toBeInTheDocument(); + }); + }); }); diff --git a/packages/manager/src/features/PlacementGroups/PlacementGroupsCreateDrawer.tsx b/packages/manager/src/features/PlacementGroups/PlacementGroupsCreateDrawer.tsx index 6e1d51b4eec..99b7f22fe9d 100644 --- a/packages/manager/src/features/PlacementGroups/PlacementGroupsCreateDrawer.tsx +++ b/packages/manager/src/features/PlacementGroups/PlacementGroupsCreateDrawer.tsx @@ -54,7 +54,11 @@ export const PlacementGroupsCreateDrawer = ( const [ hasRegionReachedPGCapacity, setHasRegionReachedPGCapacity, - ] = React.useState(true); + ] = React.useState(false); + + const selectedRegionFromProps = regions?.find( + (r) => r.id === selectedRegionId + ); const handleRegionSelect = (region: Region['id']) => { const selectedRegion = regions?.find((r) => r.id === region); @@ -68,7 +72,7 @@ export const PlacementGroupsCreateDrawer = ( ); }; - const handleDrawerOpen = () => { + const handleResetForm = () => { resetForm(); setHasFormBeenSubmitted(false); setHasRegionReachedPGCapacity(false); @@ -76,7 +80,7 @@ export const PlacementGroupsCreateDrawer = ( const handleDrawerClose = () => { onClose(); - handleDrawerOpen(); // Reset form and state when drawer closes + handleResetForm(); }; const handleFormSubmit = async ( @@ -103,6 +107,7 @@ export const PlacementGroupsCreateDrawer = ( if (onPlacementGroupCreate) { onPlacementGroupCreate(response); } + handleResetForm(); onClose(); } catch { const mapErrorToStatus = () => @@ -153,6 +158,12 @@ export const PlacementGroupsCreateDrawer = ( {generalError && } + {selectedRegionFromProps && ( + + Region: + {`${selectedRegionFromProps.label} (${selectedRegionFromProps.id})`} + + )} - { - handleRegionSelect(selection); - }} - currentCapability="Placement Group" - disabled={Boolean(selectedRegionId)} - helperText="Only regions supporting Placement Groups are listed." - regions={regions ?? []} - selectedId={selectedRegionId ?? values.region} - /> + {!selectedRegionId && ( + { + handleRegionSelect(selection); + }} + currentCapability="Placement Group" + disabled={Boolean(selectedRegionId)} + helperText="Only regions supporting Placement Groups are listed." + regions={regions ?? []} + selectedId={selectedRegionId ?? values.region} + /> + )} { setFieldValue('affinity_type', value?.value ?? ''); @@ -188,11 +201,6 @@ export const PlacementGroupsCreateDrawer = ( textFieldProps={{ tooltipText: 'TODO VM_Placement: update copy', }} - value={ - affinityTypeOptions.find( - (option) => option.value === values.affinity_type - ) ?? undefined - } disableClearable={true} errorText={errors.affinity_type} label="Affinity Type" From 3e0bfb14343eeffcce2cbbf27bc3f85183cb4db9 Mon Sep 17 00:00:00 2001 From: Alban Bailly Date: Wed, 28 Feb 2024 16:47:27 -0500 Subject: [PATCH 10/20] Better edit form --- .../PlacementGroupsEditDrawer.tsx | 96 ++++++++++--------- .../PlacementGroupsLanding.tsx | 1 + 2 files changed, 52 insertions(+), 45 deletions(-) diff --git a/packages/manager/src/features/PlacementGroups/PlacementGroupsEditDrawer.tsx b/packages/manager/src/features/PlacementGroups/PlacementGroupsEditDrawer.tsx index 0a525a45aaa..006dfce0faa 100644 --- a/packages/manager/src/features/PlacementGroups/PlacementGroupsEditDrawer.tsx +++ b/packages/manager/src/features/PlacementGroups/PlacementGroupsEditDrawer.tsx @@ -23,6 +23,7 @@ import { import type { PlacementGroupsEditDrawerProps } from './types'; import type { UpdatePlacementGroupPayload } from '@linode/api-v4'; +import type { FormikHelpers } from 'formik'; export const PlacementGroupsEditDrawer = ( props: PlacementGroupsEditDrawerProps @@ -42,6 +43,54 @@ export const PlacementGroupsEditDrawer = ( setHasFormBeenSubmitted, } = useFormValidateOnChange(); + const handleResetForm = () => { + resetForm(); + setHasFormBeenSubmitted(false); + }; + + const handleDrawerClose = () => { + onClose(); + handleResetForm(); + }; + + const handleFormSubmit = async ( + values: UpdatePlacementGroupPayload, + { + setErrors, + setStatus, + setSubmitting, + }: FormikHelpers + ) => { + setHasFormBeenSubmitted(false); + setStatus(undefined); + setErrors({}); + const payload = { ...values }; + + try { + const response = await mutateAsync(payload); + + setSubmitting(false); + enqueueSnackbar(`Placement Group ${payload.label} successfully updated`, { + variant: 'success', + }); + + 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.' + ); + } + }; + const { errors, handleBlur, @@ -56,55 +105,12 @@ export const PlacementGroupsEditDrawer = ( initialValues: { label: selectedPlacementGroup?.label ?? '', }, - async onSubmit(values, { setErrors, setStatus, setSubmitting }) { - setHasFormBeenSubmitted(false); - setStatus(undefined); - setErrors({}); - const payload = { ...values }; - - try { - const response = await mutateAsync(payload); - - setSubmitting(false); - enqueueSnackbar( - `Placement Group ${payload.label} successfully updated`, - { - variant: 'success', - } - ); - - 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.' - ); - } - }, + onSubmit: handleFormSubmit, 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 ( @@ -116,7 +122,7 @@ export const PlacementGroupsEditDrawer = ( })` : '' } - onClose={onClose} + onClose={handleDrawerClose} open={open} > {generalError && } diff --git a/packages/manager/src/features/PlacementGroups/PlacementGroupsLanding/PlacementGroupsLanding.tsx b/packages/manager/src/features/PlacementGroups/PlacementGroupsLanding/PlacementGroupsLanding.tsx index 63c40054990..9fc8bb92ada 100644 --- a/packages/manager/src/features/PlacementGroups/PlacementGroupsLanding/PlacementGroupsLanding.tsx +++ b/packages/manager/src/features/PlacementGroups/PlacementGroupsLanding/PlacementGroupsLanding.tsx @@ -196,6 +196,7 @@ export const PlacementGroupsLanding = React.memo(() => { allExistingPlacementGroups={placementGroups?.data ?? []} onClose={onClosePlacementGroupDrawer} open={isPlacementGroupCreateDrawerOpen} + selectedRegionId="us-west" /> Date: Wed, 28 Feb 2024 17:17:42 -0500 Subject: [PATCH 11/20] cleanup --- .../PlacementGroups/PlacementGroupsCreateDrawer.test.tsx | 2 +- .../features/PlacementGroups/PlacementGroupsCreateDrawer.tsx | 4 ++-- .../PlacementGroupsLanding/PlacementGroupsLanding.tsx | 4 ++-- packages/manager/src/features/PlacementGroups/types.ts | 2 +- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/packages/manager/src/features/PlacementGroups/PlacementGroupsCreateDrawer.test.tsx b/packages/manager/src/features/PlacementGroups/PlacementGroupsCreateDrawer.test.tsx index 31c03e6abd7..30c983084c0 100644 --- a/packages/manager/src/features/PlacementGroups/PlacementGroupsCreateDrawer.test.tsx +++ b/packages/manager/src/features/PlacementGroups/PlacementGroupsCreateDrawer.test.tsx @@ -6,7 +6,7 @@ import { renderWithTheme } from 'src/utilities/testHelpers'; import { PlacementGroupsCreateDrawer } from './PlacementGroupsCreateDrawer'; const commonProps = { - allExistingPlacementGroups: [], + allPlacementGroups: [], onClose: vi.fn(), open: true, }; diff --git a/packages/manager/src/features/PlacementGroups/PlacementGroupsCreateDrawer.tsx b/packages/manager/src/features/PlacementGroups/PlacementGroupsCreateDrawer.tsx index 99b7f22fe9d..c50d98de67e 100644 --- a/packages/manager/src/features/PlacementGroups/PlacementGroupsCreateDrawer.tsx +++ b/packages/manager/src/features/PlacementGroups/PlacementGroupsCreateDrawer.tsx @@ -38,7 +38,7 @@ export const PlacementGroupsCreateDrawer = ( props: PlacementGroupsCreateDrawerProps ) => { const { - allExistingPlacementGroups, + allPlacementGroups, onClose, onPlacementGroupCreate, open, @@ -66,7 +66,7 @@ export const PlacementGroupsCreateDrawer = ( setFieldValue('region', region); setHasRegionReachedPGCapacity( hasRegionReachedPlacementGroupCapacity({ - allPlacementGroups: allExistingPlacementGroups, + allPlacementGroups, region: selectedRegion, }) ); diff --git a/packages/manager/src/features/PlacementGroups/PlacementGroupsLanding/PlacementGroupsLanding.tsx b/packages/manager/src/features/PlacementGroups/PlacementGroupsLanding/PlacementGroupsLanding.tsx index 9fc8bb92ada..b0fe568b174 100644 --- a/packages/manager/src/features/PlacementGroups/PlacementGroupsLanding/PlacementGroupsLanding.tsx +++ b/packages/manager/src/features/PlacementGroups/PlacementGroupsLanding/PlacementGroupsLanding.tsx @@ -93,7 +93,7 @@ export const PlacementGroupsLanding = React.memo(() => { openCreatePlacementGroupDrawer={handleCreatePlacementGroup} /> @@ -193,7 +193,7 @@ export const PlacementGroupsLanding = React.memo(() => { pageSize={pagination.pageSize} /> void; selectedRegionId?: string; }; From f43488a2cb0ee5d464506166f491587704067f37 Mon Sep 17 00:00:00 2001 From: Alban Bailly Date: Thu, 29 Feb 2024 16:10:14 -0500 Subject: [PATCH 12/20] PlacementGroupsAffinityTypeSelect --- packages/manager/src/components/TextField.tsx | 2 +- packages/manager/src/components/Tooltip.tsx | 8 +- .../PlacementGroupsAffinityTypeSelect.tsx | 91 +++++++++++++++++++ .../PlacementGroupsCreateDrawer.tsx | 24 ++--- .../src/features/PlacementGroups/utils.ts | 1 + 5 files changed, 106 insertions(+), 20 deletions(-) create mode 100644 packages/manager/src/features/PlacementGroups/PlacementGroupsAffinityTypeSelect.tsx diff --git a/packages/manager/src/components/TextField.tsx b/packages/manager/src/components/TextField.tsx index 99fcc66e9ec..bac96accd5d 100644 --- a/packages/manager/src/components/TextField.tsx +++ b/packages/manager/src/components/TextField.tsx @@ -1,9 +1,9 @@ import KeyboardArrowDown from '@mui/icons-material/KeyboardArrowDown'; +import { Theme, useTheme } from '@mui/material/styles'; import { default as _TextField, StandardTextFieldProps, } from '@mui/material/TextField'; -import { Theme, useTheme } from '@mui/material/styles'; import { clamp } from 'ramda'; import * as React from 'react'; import { makeStyles } from 'tss-react/mui'; diff --git a/packages/manager/src/components/Tooltip.tsx b/packages/manager/src/components/Tooltip.tsx index c325f07cc4d..8e164f25033 100644 --- a/packages/manager/src/components/Tooltip.tsx +++ b/packages/manager/src/components/Tooltip.tsx @@ -7,7 +7,13 @@ import type { TooltipProps } from '@mui/material/Tooltip'; * Tooltips display informative text when users hover over, focus on, or tap an element. */ export const Tooltip = (props: TooltipProps) => { - return <_Tooltip data-qa-tooltip={props.title} {...props} />; + return ( + <_Tooltip + data-qa-tooltip={props.title} + {...props} + disableInteractive={false} + /> + ); }; export { tooltipClasses }; export type { TooltipProps }; diff --git a/packages/manager/src/features/PlacementGroups/PlacementGroupsAffinityTypeSelect.tsx b/packages/manager/src/features/PlacementGroups/PlacementGroupsAffinityTypeSelect.tsx new file mode 100644 index 00000000000..2f2260c1297 --- /dev/null +++ b/packages/manager/src/features/PlacementGroups/PlacementGroupsAffinityTypeSelect.tsx @@ -0,0 +1,91 @@ +import * as React from 'react'; + +import { Autocomplete } from 'src/components/Autocomplete/Autocomplete'; +import { Link } from 'src/components/Link'; +import { ListItem } from 'src/components/ListItem'; +import { Tooltip } from 'src/components/Tooltip'; +import { Typography } from 'src/components/Typography'; + +import { affinityTypeOptions } from './utils'; + +import type { FormikHelpers } from 'formik'; + +interface Props { + error: string | undefined; + setFieldValue: FormikHelpers['setFieldValue']; +} + +export const PlacementGroupsAffinityTypeSelect = (props: Props) => { + const { error, setFieldValue } = props; + return ( + option.value === 'anti_affinity' + )} + onChange={(_, value) => { + setFieldValue('affinity_type', value?.value ?? ''); + }} + renderOption={(props, option) => { + const isDisabledMenuItem = option.value === 'affinity'; + + return ( + + Only supporting Anti-affinity host placement groups for Beta.{' '} + Learn more. + + ) : ( + '' + ) + } + disableFocusListener={!isDisabledMenuItem} + disableHoverListener={!isDisabledMenuItem} + disableTouchListener={!isDisabledMenuItem} + enterDelay={200} + enterNextDelay={200} + enterTouchDelay={200} + key={option.value} + > + + isDisabledMenuItem + ? e.preventDefault() + : props.onClick + ? props.onClick(e) + : null + } + component="li" + > + {option.label} + + + ); + }} + textFieldProps={{ + tooltipText: ( + + Linodes in a placement group that use ‘Affinity’ always exist on the + same host. This can help with performance. Linodes in a placement + group that use ‘Anti-affinity: Host’ are never on the same host. Use + this to support a high-availability model. +
+ Learn more. +
+ ), + }} + disableClearable={true} + errorText={error} + label="Affinity Type" + options={affinityTypeOptions} + placeholder="Select an Affinity Type" + /> + ); +}; diff --git a/packages/manager/src/features/PlacementGroups/PlacementGroupsCreateDrawer.tsx b/packages/manager/src/features/PlacementGroups/PlacementGroupsCreateDrawer.tsx index c50d98de67e..bb86fd94364 100644 --- a/packages/manager/src/features/PlacementGroups/PlacementGroupsCreateDrawer.tsx +++ b/packages/manager/src/features/PlacementGroups/PlacementGroupsCreateDrawer.tsx @@ -4,7 +4,6 @@ import { useSnackbar } from 'notistack'; import * as React from 'react'; import { ActionsPanel } from 'src/components/ActionsPanel/ActionsPanel'; -import { Autocomplete } from 'src/components/Autocomplete/Autocomplete'; import { Box } from 'src/components/Box'; import { Drawer } from 'src/components/Drawer'; import { FormControlLabel } from 'src/components/FormControlLabel'; @@ -25,10 +24,8 @@ import { handleGeneralErrors, } from 'src/utilities/formikErrorUtils'; -import { - affinityTypeOptions, - hasRegionReachedPlacementGroupCapacity, -} from './utils'; +import { PlacementGroupsAffinityTypeSelect } from './PlacementGroupsAffinityTypeSelect'; +import { hasRegionReachedPlacementGroupCapacity } from './utils'; import type { PlacementGroupsCreateDrawerProps } from './types'; import type { CreatePlacementGroupPayload, Region } from '@linode/api-v4'; @@ -136,7 +133,7 @@ export const PlacementGroupsCreateDrawer = ( } = useFormik({ enableReinitialize: true, initialValues: { - affinity_type: '' as CreatePlacementGroupPayload['affinity_type'], + affinity_type: 'anti_affinity', is_strict: true, label: '', region: selectedRegionId ?? '', @@ -194,18 +191,9 @@ export const PlacementGroupsCreateDrawer = ( selectedId={selectedRegionId ?? values.region} /> )} - { - setFieldValue('affinity_type', value?.value ?? ''); - }} - textFieldProps={{ - tooltipText: 'TODO VM_Placement: update copy', - }} - disableClearable={true} - errorText={errors.affinity_type} - label="Affinity Type" - options={affinityTypeOptions} - placeholder="Select an Affinity Type" + ({ + disabled: false, label: value, value: key as CreatePlacementGroupPayload['affinity_type'], }) From 795b11d2d2859a6c820b1e2b4b0dba8b58066986 Mon Sep 17 00:00:00 2001 From: Alban Bailly Date: Thu, 29 Feb 2024 17:05:09 -0500 Subject: [PATCH 13/20] PlacementGroupsAffinityEnforcementRadioGroup --- ...entGroupsAffinityEnforcementRadioGroup.tsx | 65 +++++++++++++++++++ .../PlacementGroupsCreateDrawer.tsx | 53 ++------------- .../PlacementGroupsLanding.tsx | 1 - 3 files changed, 71 insertions(+), 48 deletions(-) create mode 100644 packages/manager/src/features/PlacementGroups/PlacementGroupsAffinityEnforcementRadioGroup.tsx diff --git a/packages/manager/src/features/PlacementGroups/PlacementGroupsAffinityEnforcementRadioGroup.tsx b/packages/manager/src/features/PlacementGroups/PlacementGroupsAffinityEnforcementRadioGroup.tsx new file mode 100644 index 00000000000..4aa294a486f --- /dev/null +++ b/packages/manager/src/features/PlacementGroups/PlacementGroupsAffinityEnforcementRadioGroup.tsx @@ -0,0 +1,65 @@ +import * as React from 'react'; + +import { Box } from 'src/components/Box'; +import { FormControlLabel } from 'src/components/FormControlLabel'; +import { FormLabel } from 'src/components/FormLabel'; +import { Notice } from 'src/components/Notice/Notice'; +import { Radio } from 'src/components/Radio/Radio'; +import { RadioGroup } from 'src/components/RadioGroup'; +import { Typography } from 'src/components/Typography'; + +import type { FormikHelpers } from 'formik'; + +interface Props { + handleChange: (e: React.ChangeEvent) => void; + setFieldValue: FormikHelpers['setFieldValue']; + value: boolean; +} + +export const PlacementGroupsAffinityEnforcementRadioGroup = (props: Props) => { + const { handleChange, setFieldValue, value } = props; + return ( + + + + Affinity Enforcement + + { + handleChange(event); + setFieldValue('is_strict', event.target.value === 'true'); + }} + id="affinity-enforcement-radio-group" + name="is_strict" + value={value} + > + + Strict. You cannot assign a Linode to your + placement group if it will violate the policy of your selected + Affinity Type (best practice). + + } + control={} + value={true} + /> + + Flexible. You can assign a Linode to your + placement group, even if it violates the policy of your selected + Affinity Type. + + } + control={} + sx={{ mt: 2 }} + value={false} + /> + + + ); +}; diff --git a/packages/manager/src/features/PlacementGroups/PlacementGroupsCreateDrawer.tsx b/packages/manager/src/features/PlacementGroups/PlacementGroupsCreateDrawer.tsx index bb86fd94364..f7bb5d9d931 100644 --- a/packages/manager/src/features/PlacementGroups/PlacementGroupsCreateDrawer.tsx +++ b/packages/manager/src/features/PlacementGroups/PlacementGroupsCreateDrawer.tsx @@ -4,13 +4,8 @@ import { useSnackbar } from 'notistack'; import * as React from 'react'; import { ActionsPanel } from 'src/components/ActionsPanel/ActionsPanel'; -import { Box } from 'src/components/Box'; import { Drawer } from 'src/components/Drawer'; -import { FormControlLabel } from 'src/components/FormControlLabel'; -import { FormLabel } from 'src/components/FormLabel'; import { Notice } from 'src/components/Notice/Notice'; -import { Radio } from 'src/components/Radio/Radio'; -import { RadioGroup } from 'src/components/RadioGroup'; import { RegionSelect } from 'src/components/RegionSelect/RegionSelect'; import { Stack } from 'src/components/Stack'; import { TextField } from 'src/components/TextField'; @@ -24,6 +19,7 @@ import { handleGeneralErrors, } from 'src/utilities/formikErrorUtils'; +import { PlacementGroupsAffinityEnforcementRadioGroup } from './PlacementGroupsAffinityEnforcementRadioGroup'; import { PlacementGroupsAffinityTypeSelect } from './PlacementGroupsAffinityTypeSelect'; import { hasRegionReachedPlacementGroupCapacity } from './utils'; @@ -195,48 +191,11 @@ export const PlacementGroupsCreateDrawer = ( error={errors.affinity_type} setFieldValue={setFieldValue} /> - - - - Affinity Enforcement - - { - handleChange(event); - setFieldValue('is_strict', event.target.value === 'true'); - }} - id="affinity-enforcement-radio-group" - name="is_strict" - value={values.is_strict} - > - - Strict. You cannot assign a Linode to your - placement group if it will violate the policy of your - selected Affinity Type (best practice). - - } - control={} - value={true} - /> - - Flexible. You can assign a Linode to your - placement group, even if it violates the policy of your - selected Affinity Type. - - } - control={} - sx={{ mt: 2 }} - value={false} - /> - - + { allPlacementGroups={placementGroups?.data ?? []} onClose={onClosePlacementGroupDrawer} open={isPlacementGroupCreateDrawerOpen} - selectedRegionId="us-west" /> Date: Thu, 29 Feb 2024 17:29:06 -0500 Subject: [PATCH 14/20] cleanup --- packages/manager/src/components/Tooltip.tsx | 8 +------- .../PlacementGroupsCreateDrawer.test.tsx | 10 +--------- 2 files changed, 2 insertions(+), 16 deletions(-) diff --git a/packages/manager/src/components/Tooltip.tsx b/packages/manager/src/components/Tooltip.tsx index 8e164f25033..c325f07cc4d 100644 --- a/packages/manager/src/components/Tooltip.tsx +++ b/packages/manager/src/components/Tooltip.tsx @@ -7,13 +7,7 @@ import type { TooltipProps } from '@mui/material/Tooltip'; * Tooltips display informative text when users hover over, focus on, or tap an element. */ export const Tooltip = (props: TooltipProps) => { - return ( - <_Tooltip - data-qa-tooltip={props.title} - {...props} - disableInteractive={false} - /> - ); + return <_Tooltip data-qa-tooltip={props.title} {...props} />; }; export { tooltipClasses }; export type { TooltipProps }; diff --git a/packages/manager/src/features/PlacementGroups/PlacementGroupsCreateDrawer.test.tsx b/packages/manager/src/features/PlacementGroups/PlacementGroupsCreateDrawer.test.tsx index 30c983084c0..6f5242b39fa 100644 --- a/packages/manager/src/features/PlacementGroups/PlacementGroupsCreateDrawer.test.tsx +++ b/packages/manager/src/features/PlacementGroups/PlacementGroupsCreateDrawer.test.tsx @@ -94,21 +94,13 @@ describe('PlacementGroupsCreateDrawer', () => { fireEvent.click(selectedRegionOption); }); - const affinityTypeSelect = getByPlaceholderText('Select an Affinity Type'); - fireEvent.focus(affinityTypeSelect); - fireEvent.change(affinityTypeSelect, { target: { value: 'Affinity' } }); - await waitFor(() => { - const selectedAffinityTypeOption = getByText('Affinity'); - fireEvent.click(selectedAffinityTypeOption); - }); - fireEvent.click(getByRole('button', { name: 'Create Placement Group' })); await waitFor(() => { expect( queryMocks.useCreatePlacementGroup().mutateAsync ).toHaveBeenCalledWith({ - affinity_type: 'affinity', + affinity_type: 'anti_affinity', is_strict: true, label: 'my-label', region: 'us-east', From 6d783c9fe32603f052c8810c97017ed7902abb4d Mon Sep 17 00:00:00 2001 From: Alban Bailly Date: Fri, 1 Mar 2024 09:29:12 -0500 Subject: [PATCH 15/20] cleanup --- .../PlacementGroupsCreateDrawer.tsx | 4 ++- .../PlacementGroupsEditDrawer.tsx | 26 ++++++++++++------- .../src/features/PlacementGroups/utils.ts | 4 +-- 3 files changed, 22 insertions(+), 12 deletions(-) diff --git a/packages/manager/src/features/PlacementGroups/PlacementGroupsCreateDrawer.tsx b/packages/manager/src/features/PlacementGroups/PlacementGroupsCreateDrawer.tsx index f7bb5d9d931..411c874cd14 100644 --- a/packages/manager/src/features/PlacementGroups/PlacementGroupsCreateDrawer.tsx +++ b/packages/manager/src/features/PlacementGroups/PlacementGroupsCreateDrawer.tsx @@ -4,6 +4,7 @@ import { useSnackbar } from 'notistack'; import * as React from 'react'; import { ActionsPanel } from 'src/components/ActionsPanel/ActionsPanel'; +import { Divider } from 'src/components/Divider'; import { Drawer } from 'src/components/Drawer'; import { Notice } from 'src/components/Notice/Notice'; import { RegionSelect } from 'src/components/RegionSelect/RegionSelect'; @@ -152,11 +153,12 @@ export const PlacementGroupsCreateDrawer = ( {generalError && } {selectedRegionFromProps && ( - + Region: {`${selectedRegionFromProps.label} (${selectedRegionFromProps.id})`} )} + {generalError && } - + Region: {region ? `${region.label} (${region.id})` : 'Unknown'} + + Affinity Enforcement: + {getAffinityEnforcement(selectedPlacementGroup.is_strict)} + + { - return affinityType ? 'Strict' : 'Flexible'; + return is_strict ? 'Strict' : 'Flexible'; }; /** From a89c673356dc4919a16f147535c175dba83d5048 Mon Sep 17 00:00:00 2001 From: Alban Bailly Date: Fri, 1 Mar 2024 09:30:11 -0500 Subject: [PATCH 16/20] Added changeset: Update Placement Group Create & Edit Drawers --- .../.changeset/pr-10205-upcoming-features-1709303410944.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 packages/manager/.changeset/pr-10205-upcoming-features-1709303410944.md diff --git a/packages/manager/.changeset/pr-10205-upcoming-features-1709303410944.md b/packages/manager/.changeset/pr-10205-upcoming-features-1709303410944.md new file mode 100644 index 00000000000..20e5c1023ed --- /dev/null +++ b/packages/manager/.changeset/pr-10205-upcoming-features-1709303410944.md @@ -0,0 +1,5 @@ +--- +"@linode/manager": Upcoming Features +--- + +Update Placement Group Create & Edit Drawers ([#10205](https://github.com/linode/manager/pull/10205)) From 82d2fb0ed126c00079c3a9ba87fa89f975e7eda9 Mon Sep 17 00:00:00 2001 From: Alban Bailly Date: Mon, 4 Mar 2024 11:29:47 -0500 Subject: [PATCH 17/20] Make sure id is required --- packages/validation/src/linodes.schema.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/validation/src/linodes.schema.ts b/packages/validation/src/linodes.schema.ts index 41497079e09..892e004686b 100644 --- a/packages/validation/src/linodes.schema.ts +++ b/packages/validation/src/linodes.schema.ts @@ -261,7 +261,7 @@ const MetadataSchema = object({ }); const PlacementGroupPayloadSchema = object({ - id: number().notRequired().nullable(true), + id: number().required().nullable(true), }); export const CreateLinodeSchema = object({ From 0f678cfbe9f0244ff0388df3ca64f71fde8cc4cc Mon Sep 17 00:00:00 2001 From: Alban Bailly Date: Mon, 4 Mar 2024 11:38:44 -0500 Subject: [PATCH 18/20] Added changeset: Allow the disabling of the TypeToConfirm input --- .../manager/.changeset/pr-10205-changed-1709570324014.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 packages/manager/.changeset/pr-10205-changed-1709570324014.md diff --git a/packages/manager/.changeset/pr-10205-changed-1709570324014.md b/packages/manager/.changeset/pr-10205-changed-1709570324014.md new file mode 100644 index 00000000000..91816536133 --- /dev/null +++ b/packages/manager/.changeset/pr-10205-changed-1709570324014.md @@ -0,0 +1,5 @@ +--- +"@linode/manager": Changed +--- + +Allow the disabling of the TypeToConfirm input ([#10205](https://github.com/linode/manager/pull/10205)) From 1641591b6cc4756e4ba224bb14f9c18cdec398f5 Mon Sep 17 00:00:00 2001 From: Alban Bailly Date: Mon, 4 Mar 2024 12:32:30 -0500 Subject: [PATCH 19/20] Revert "Make sure id is required" This reverts commit 82d2fb0ed126c00079c3a9ba87fa89f975e7eda9. --- packages/validation/src/linodes.schema.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/validation/src/linodes.schema.ts b/packages/validation/src/linodes.schema.ts index 892e004686b..41497079e09 100644 --- a/packages/validation/src/linodes.schema.ts +++ b/packages/validation/src/linodes.schema.ts @@ -261,7 +261,7 @@ const MetadataSchema = object({ }); const PlacementGroupPayloadSchema = object({ - id: number().required().nullable(true), + id: number().notRequired().nullable(true), }); export const CreateLinodeSchema = object({ From 0b605d9f7e3670ea62a688b9c421f90b9511ff58 Mon Sep 17 00:00:00 2001 From: Alban Bailly Date: Tue, 5 Mar 2024 11:26:18 -0500 Subject: [PATCH 20/20] Feedback --- .../PlacementGroupsCreateDrawer.tsx | 38 +++++-------------- .../PlacementGroupsEditDrawer.tsx | 35 +++++------------ 2 files changed, 19 insertions(+), 54 deletions(-) diff --git a/packages/manager/src/features/PlacementGroups/PlacementGroupsCreateDrawer.tsx b/packages/manager/src/features/PlacementGroups/PlacementGroupsCreateDrawer.tsx index 411c874cd14..1fb84ec35a2 100644 --- a/packages/manager/src/features/PlacementGroups/PlacementGroupsCreateDrawer.tsx +++ b/packages/manager/src/features/PlacementGroups/PlacementGroupsCreateDrawer.tsx @@ -14,11 +14,8 @@ import { Typography } from 'src/components/Typography'; import { useFormValidateOnChange } from 'src/hooks/useFormValidateOnChange'; import { useCreatePlacementGroup } from 'src/queries/placementGroups'; import { useRegionsQuery } from 'src/queries/regions'; -import { getErrorMap } from 'src/utilities/errorUtils'; -import { - handleFieldErrors, - handleGeneralErrors, -} from 'src/utilities/formikErrorUtils'; +import { getFormikErrorsFromAPIErrors } from 'src/utilities/formikErrorUtils'; +import { scrollErrorIntoView } from 'src/utilities/scrollErrorIntoView'; import { PlacementGroupsAffinityEnforcementRadioGroup } from './PlacementGroupsAffinityEnforcementRadioGroup'; import { PlacementGroupsAffinityTypeSelect } from './PlacementGroupsAffinityTypeSelect'; @@ -79,22 +76,16 @@ export const PlacementGroupsCreateDrawer = ( const handleFormSubmit = async ( values: CreatePlacementGroupPayload, - { - setErrors, - setStatus, - setSubmitting, - }: FormikHelpers + { setErrors, setStatus }: FormikHelpers ) => { setHasFormBeenSubmitted(false); setStatus(undefined); setErrors({}); - const payload = { ...values }; try { - const response = await mutateAsync(payload); - setSubmitting(false); + const response = await mutateAsync(values); - enqueueSnackbar(`Placement Group ${payload.label} successfully created`, { + enqueueSnackbar(`Placement Group ${values.label} successfully created`, { variant: 'success', }); @@ -103,17 +94,9 @@ export const PlacementGroupsCreateDrawer = ( } handleResetForm(); onClose(); - } catch { - const mapErrorToStatus = () => - setStatus({ generalError: getErrorMap([], error).none }); - - setSubmitting(false); - handleFieldErrors(setErrors, error ?? []); - handleGeneralErrors( - mapErrorToStatus, - error ?? [], - 'Error creating Placement Group.' - ); + } catch (errors) { + setErrors(getFormikErrorsFromAPIErrors(errors)); + scrollErrorIntoView(); } }; @@ -125,7 +108,6 @@ export const PlacementGroupsCreateDrawer = ( isSubmitting, resetForm, setFieldValue, - status, values, } = useFormik({ enableReinitialize: true, @@ -141,7 +123,7 @@ export const PlacementGroupsCreateDrawer = ( validationSchema: createPlacementGroupSchema, }); - const generalError = status?.generalError; + const generalError = error?.find((e) => !e.field)?.reason; return ( )} - +