diff --git a/packages/manager/.changeset/pr-11137-fixed-1729745562099.md b/packages/manager/.changeset/pr-11137-fixed-1729745562099.md new file mode 100644 index 00000000000..a502521a780 --- /dev/null +++ b/packages/manager/.changeset/pr-11137-fixed-1729745562099.md @@ -0,0 +1,5 @@ +--- +"@linode/manager": Fixed +--- + +Database create page form being enabled for restricted users ([#11137](https://github.com/linode/manager/pull/11137)) diff --git a/packages/manager/src/features/Databases/DatabaseCreate/DatabaseCreate.test.tsx b/packages/manager/src/features/Databases/DatabaseCreate/DatabaseCreate.test.tsx index 40b0bf1c6ce..1b4c8241343 100644 --- a/packages/manager/src/features/Databases/DatabaseCreate/DatabaseCreate.test.tsx +++ b/packages/manager/src/features/Databases/DatabaseCreate/DatabaseCreate.test.tsx @@ -12,6 +12,18 @@ import DatabaseCreate from './DatabaseCreate'; const loadingTestId = 'circle-progress'; +const queryMocks = vi.hoisted(() => ({ + useProfile: vi.fn().mockReturnValue({ data: { restricted: false } }), +})); + +vi.mock('src/queries/profile/profile', async () => { + const actual = await vi.importActual('src/queries/profile/profile'); + return { + ...actual, + useProfile: queryMocks.useProfile, + }; +}); + beforeAll(() => mockMatchMedia()); describe('Database Create', () => { @@ -154,4 +166,96 @@ describe('Database Create', () => { expect(nodeRadioBtns).toHaveTextContent('$100/month $0.15/hr'); expect(nodeRadioBtns).toHaveTextContent('$140/month $0.21/hr'); }); + + it('should have the "Create Database Cluster" button disabled for restricted users', async () => { + queryMocks.useProfile.mockReturnValue({ data: { restricted: true } }); + + const { findByText, getByTestId } = renderWithTheme(); + + expect(getByTestId(loadingTestId)).toBeInTheDocument(); + + await waitForElementToBeRemoved(getByTestId(loadingTestId)); + const createClusterButtonSpan = await findByText('Create Database Cluster'); + const createClusterButton = createClusterButtonSpan.closest('button'); + + expect(createClusterButton).toBeInTheDocument(); + expect(createClusterButton).toBeDisabled(); + }); + + it('should disable form inputs for restricted users', async () => { + queryMocks.useProfile.mockReturnValue({ data: { restricted: true } }); + + const { + findAllByRole, + findAllByTestId, + findByPlaceholderText, + getByTestId, + } = renderWithTheme(); + + expect(getByTestId(loadingTestId)).toBeInTheDocument(); + + await waitForElementToBeRemoved(getByTestId(loadingTestId)); + const textInputs = await findAllByTestId('textfield-input'); + textInputs.forEach((input: HTMLInputElement) => { + expect(input).toBeDisabled(); + }); + + const dbEngineSelect = await findByPlaceholderText( + 'Select a Database Engine' + ); + expect(dbEngineSelect).toBeDisabled(); + const regionSelect = await findByPlaceholderText('Select a Region'); + expect(regionSelect).toBeDisabled(); + + const radioButtons = await findAllByRole('radio'); + radioButtons.forEach((radioButton: HTMLElement) => { + expect(radioButton).toBeDisabled(); + }); + }); + + it('should have the "Create Database Cluster" button enabled for users with full access', async () => { + queryMocks.useProfile.mockReturnValue({ data: { restricted: false } }); + + const { findByText, getByTestId } = renderWithTheme(); + + expect(getByTestId(loadingTestId)).toBeInTheDocument(); + + await waitForElementToBeRemoved(getByTestId(loadingTestId)); + const createClusterButtonSpan = await findByText('Create Database Cluster'); + const createClusterButton = createClusterButtonSpan.closest('button'); + + expect(createClusterButton).toBeInTheDocument(); + expect(createClusterButton).toBeEnabled(); + }); + + it('should enable form inputs for users with full access', async () => { + queryMocks.useProfile.mockReturnValue({ data: { restricted: false } }); + + const { + findAllByRole, + findAllByTestId, + findByPlaceholderText, + getByTestId, + } = renderWithTheme(); + + expect(getByTestId(loadingTestId)).toBeInTheDocument(); + + await waitForElementToBeRemoved(getByTestId(loadingTestId)); + const textInputs = await findAllByTestId('textfield-input'); + textInputs.forEach((input: HTMLInputElement) => { + expect(input).toBeEnabled(); + }); + + const dbEngineSelect = await findByPlaceholderText( + 'Select a Database Engine' + ); + expect(dbEngineSelect).toBeEnabled(); + const regionSelect = await findByPlaceholderText('Select a Region'); + expect(regionSelect).toBeEnabled(); + + const radioButtons = await findAllByRole('radio'); + radioButtons.forEach((radioButton: HTMLElement) => { + expect(radioButton).toBeEnabled(); + }); + }); }); diff --git a/packages/manager/src/features/Databases/DatabaseCreate/DatabaseCreate.tsx b/packages/manager/src/features/Databases/DatabaseCreate/DatabaseCreate.tsx index cb092f26b0e..5ec755d8d25 100644 --- a/packages/manager/src/features/Databases/DatabaseCreate/DatabaseCreate.tsx +++ b/packages/manager/src/features/Databases/DatabaseCreate/DatabaseCreate.tsx @@ -29,6 +29,7 @@ import { RegionSelect } from 'src/components/RegionSelect/RegionSelect'; import { RegionHelperText } from 'src/components/SelectRegionPanel/RegionHelperText'; import { TextField } from 'src/components/TextField'; import { Typography } from 'src/components/Typography'; +import { getRestrictedResourceText } from 'src/features/Account/utils'; import { PlansPanel } from 'src/features/components/PlansPanel/PlansPanel'; import { EngineOption } from 'src/features/Databases/DatabaseCreate/EngineOption'; import { DatabaseLogo } from 'src/features/Databases/DatabaseLanding/DatabaseLogo'; @@ -36,6 +37,7 @@ import { databaseEngineMap } from 'src/features/Databases/DatabaseLanding/Databa import { useIsDatabasesEnabled } from 'src/features/Databases/utilities'; import { enforceIPMasks } from 'src/features/Firewalls/FirewallDetail/Rules/FirewallRuleDrawer.utils'; import { typeLabelDetails } from 'src/features/Linodes/presentation'; +import { useRestrictedGlobalGrantCheck } from 'src/hooks/useRestrictedGlobalGrantCheck'; import { useCreateDatabaseMutation, useDatabaseEnginesQuery, @@ -48,6 +50,8 @@ import { getSelectedOptionFromGroupedOptions } from 'src/utilities/getSelectedOp import { validateIPs } from 'src/utilities/ipUtils'; import { scrollErrorIntoViewV2 } from 'src/utilities/scrollErrorIntoViewV2'; +import { DatabaseCreateAccessControls } from './DatabaseCreateAccessControls'; + import type { ClusterSize, ComprehensiveReplicationType, @@ -62,7 +66,6 @@ import type { Theme } from '@mui/material/styles'; import type { Item } from 'src/components/EnhancedSelect/Select'; import type { PlanSelectionType } from 'src/features/components/PlansPanel/types'; import type { ExtendedIP } from 'src/utilities/ipUtils'; -import { DatabaseCreateAccessControls } from './DatabaseCreateAccessControls'; const useStyles = makeStyles()((theme: Theme) => ({ btnCtn: { @@ -197,6 +200,9 @@ const DatabaseCreate = () => { const { classes } = useStyles(); const history = useHistory(); const { isDatabasesV2Beta, isDatabasesV2Enabled } = useIsDatabasesEnabled(); + const isRestricted = useRestrictedGlobalGrantCheck({ + globalGrantType: 'add_databases', + }); const { data: regionsData, @@ -510,6 +516,17 @@ const DatabaseCreate = () => { }} title="Create" /> + {isRestricted && ( + + )} {createError && ( @@ -523,6 +540,7 @@ const DatabaseCreate = () => { Name Your Cluster setFieldValue('label', e.target.value)} @@ -544,6 +562,7 @@ const DatabaseCreate = () => { )} className={classes.engineSelect} components={{ Option: EngineOption, SingleValue: _SingleValue }} + disabled={isRestricted} errorText={errors.engine} isClearable={false} label="Database Engine" @@ -555,6 +574,7 @@ const DatabaseCreate = () => { setFieldValue('region', region.id)} regions={regionsData} @@ -570,6 +590,7 @@ const DatabaseCreate = () => { }} className={classes.selectPlanPanel} data-qa-select-plan + disabled={isRestricted} error={errors.type} handleTabChange={handleTabChange} header="Choose a Plan" @@ -599,11 +620,13 @@ const DatabaseCreate = () => { ); }} data-testid="database-nodes" + disabled={isRestricted} > {errors.cluster_size ? ( ) : null} @@ -622,6 +645,7 @@ const DatabaseCreate = () => { {