diff --git a/packages/manager/.changeset/pr-10257-upcoming-features-1710177497998.md b/packages/manager/.changeset/pr-10257-upcoming-features-1710177497998.md new file mode 100644 index 00000000000..8b339de9466 --- /dev/null +++ b/packages/manager/.changeset/pr-10257-upcoming-features-1710177497998.md @@ -0,0 +1,5 @@ +--- +"@linode/manager": Upcoming Features +--- + +Set up grants and permissions for Placement Groups ([#10257](https://github.com/linode/manager/pull/10257)) diff --git a/packages/manager/src/components/Breadcrumb/Breadcrumb.tsx b/packages/manager/src/components/Breadcrumb/Breadcrumb.tsx index 9f406f410dc..a744640eb01 100644 --- a/packages/manager/src/components/Breadcrumb/Breadcrumb.tsx +++ b/packages/manager/src/components/Breadcrumb/Breadcrumb.tsx @@ -13,6 +13,10 @@ export interface BreadcrumbProps { * An array of objects that can be used to customize any crumb. */ crumbOverrides?: CrumbOverridesProps[]; + /** + * A boolean that if true will disable the pencil icon button. + */ + disabledBreadcrumbEditButton?: boolean; /** * A boolean that if true will only show the first and last crumb. */ @@ -48,6 +52,7 @@ export const Breadcrumb = (props: BreadcrumbProps) => { const { breadcrumbDataAttrs, crumbOverrides, + disabledBreadcrumbEditButton, firstAndLastOnly, labelOptions, labelTitle, @@ -75,6 +80,7 @@ export const Breadcrumb = (props: BreadcrumbProps) => { > { const { crumbOverrides, + disabledBreadcrumbEditButton, firstAndLastOnly, labelOptions, labelTitle, @@ -92,6 +94,7 @@ export const Crumbs = React.memo((props: Props) => { {/* the final crumb has the possibility of being a link, editable text or just static text */} diff --git a/packages/manager/src/components/Breadcrumb/FinalCrumb.tsx b/packages/manager/src/components/Breadcrumb/FinalCrumb.tsx index d5b10394df4..d2f32161ea7 100644 --- a/packages/manager/src/components/Breadcrumb/FinalCrumb.tsx +++ b/packages/manager/src/components/Breadcrumb/FinalCrumb.tsx @@ -9,17 +9,24 @@ import { EditableProps, LabelProps } from './types'; interface Props { crumb: string; + disabledBreadcrumbEditButton?: boolean; labelOptions?: LabelProps; onEditHandlers?: EditableProps; } export const FinalCrumb = React.memo((props: Props) => { - const { crumb, labelOptions, onEditHandlers } = props; + const { + crumb, + disabledBreadcrumbEditButton, + labelOptions, + onEditHandlers, + } = props; if (onEditHandlers) { return ( ()( interface Props { className?: string; + disabledBreadcrumbEditButton?: boolean; errorText?: string; /** * Send event analytics @@ -139,6 +140,7 @@ export const EditableText = (props: PassThroughProps) => { const [text, setText] = React.useState(props.text); const { className, + disabledBreadcrumbEditButton, errorText, handleAnalyticsEvent, labelLink, @@ -228,6 +230,7 @@ export const EditableText = (props: PassThroughProps) => { aria-label={`Edit ${text}`} className={`${classes.button} ${classes.editIcon}`} data-qa-edit-button + disabled={disabledBreadcrumbEditButton} onClick={openEdit} > diff --git a/packages/manager/src/components/LandingHeader/LandingHeader.tsx b/packages/manager/src/components/LandingHeader/LandingHeader.tsx index ce83c1c7754..893b858ee95 100644 --- a/packages/manager/src/components/LandingHeader/LandingHeader.tsx +++ b/packages/manager/src/components/LandingHeader/LandingHeader.tsx @@ -1,5 +1,5 @@ -import Grid from '@mui/material/Unstable_Grid2'; import { Theme, styled, useTheme } from '@mui/material/styles'; +import Grid from '@mui/material/Unstable_Grid2'; import useMediaQuery from '@mui/material/useMediaQuery'; import * as React from 'react'; @@ -18,6 +18,7 @@ export interface LandingHeaderProps { breadcrumbProps?: BreadcrumbProps; buttonDataAttrs?: { [key: string]: boolean | string }; createButtonText?: string; + disabledBreadcrumbEditButton?: boolean; disabledCreateButton?: boolean; docsLabel?: string; docsLink?: string; @@ -43,6 +44,7 @@ export const LandingHeader = ({ breadcrumbProps, buttonDataAttrs, createButtonText, + disabledBreadcrumbEditButton, disabledCreateButton, docsLabel, docsLink, @@ -89,6 +91,7 @@ export const LandingHeader = ({ removeCrumbX={removeCrumbX} {...breadcrumbDataAttrs} {...breadcrumbProps} + disabledBreadcrumbEditButton={disabledBreadcrumbEditButton} /> {!shouldHideDocsAndCreateButtons && ( diff --git a/packages/manager/src/features/Account/constants.ts b/packages/manager/src/features/Account/constants.ts index af930f03f0d..1a685b2d4ce 100644 --- a/packages/manager/src/features/Account/constants.ts +++ b/packages/manager/src/features/Account/constants.ts @@ -11,6 +11,7 @@ export const grantTypeMap = { linode: 'Linodes', longview: 'Longview Clients', nodebalancer: 'NodeBalancers', + placementGroups: 'Placement Groups', stackscript: 'StackScripts', volume: 'Volumes', vpc: 'VPCs', diff --git a/packages/manager/src/features/PlacementGroups/PlacementGroupsAffinityEnforcementRadioGroup.tsx b/packages/manager/src/features/PlacementGroups/PlacementGroupsAffinityEnforcementRadioGroup.tsx index 4aa294a486f..1ce623874b5 100644 --- a/packages/manager/src/features/PlacementGroups/PlacementGroupsAffinityEnforcementRadioGroup.tsx +++ b/packages/manager/src/features/PlacementGroups/PlacementGroupsAffinityEnforcementRadioGroup.tsx @@ -11,13 +11,19 @@ import { Typography } from 'src/components/Typography'; import type { FormikHelpers } from 'formik'; interface Props { + disabledPlacementGroupCreateButton: boolean; handleChange: (e: React.ChangeEvent) => void; setFieldValue: FormikHelpers['setFieldValue']; value: boolean; } export const PlacementGroupsAffinityEnforcementRadioGroup = (props: Props) => { - const { handleChange, setFieldValue, value } = props; + const { + disabledPlacementGroupCreateButton, + handleChange, + setFieldValue, + value, + } = props; return ( { } control={} + disabled={disabledPlacementGroupCreateButton} value={true} /> { } control={} + disabled={disabledPlacementGroupCreateButton} sx={{ mt: 2 }} value={false} /> diff --git a/packages/manager/src/features/PlacementGroups/PlacementGroupsAffinityTypeSelect.tsx b/packages/manager/src/features/PlacementGroups/PlacementGroupsAffinityTypeSelect.tsx index f18e26715f2..fbc87558fc8 100644 --- a/packages/manager/src/features/PlacementGroups/PlacementGroupsAffinityTypeSelect.tsx +++ b/packages/manager/src/features/PlacementGroups/PlacementGroupsAffinityTypeSelect.tsx @@ -11,12 +11,13 @@ import { affinityTypeOptions } from './utils'; import type { FormikHelpers } from 'formik'; interface Props { + disabledPlacementGroupCreateButton: boolean; error: string | undefined; setFieldValue: FormikHelpers['setFieldValue']; } export const PlacementGroupsAffinityTypeSelect = (props: Props) => { - const { error, setFieldValue } = props; + const { disabledPlacementGroupCreateButton, error, setFieldValue } = props; return ( { ), }} disableClearable={true} + disabled={disabledPlacementGroupCreateButton} errorText={error} label="Affinity Type" options={affinityTypeOptions} diff --git a/packages/manager/src/features/PlacementGroups/PlacementGroupsCreateDrawer.test.tsx b/packages/manager/src/features/PlacementGroups/PlacementGroupsCreateDrawer.test.tsx index 95d7d5169dd..80d33827edd 100644 --- a/packages/manager/src/features/PlacementGroups/PlacementGroupsCreateDrawer.test.tsx +++ b/packages/manager/src/features/PlacementGroups/PlacementGroupsCreateDrawer.test.tsx @@ -7,6 +7,7 @@ import { PlacementGroupsCreateDrawer } from './PlacementGroupsCreateDrawer'; const commonProps = { allPlacementGroups: [], + disabledPlacementGroupCreateButton: false, onClose: vi.fn(), open: true, }; diff --git a/packages/manager/src/features/PlacementGroups/PlacementGroupsCreateDrawer.tsx b/packages/manager/src/features/PlacementGroups/PlacementGroupsCreateDrawer.tsx index b5440f96664..cf5a7d60416 100644 --- a/packages/manager/src/features/PlacementGroups/PlacementGroupsCreateDrawer.tsx +++ b/packages/manager/src/features/PlacementGroups/PlacementGroupsCreateDrawer.tsx @@ -11,6 +11,7 @@ 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 { getRestrictedResourceText } from 'src/features/Account/utils'; import { useFormValidateOnChange } from 'src/hooks/useFormValidateOnChange'; import { useCreatePlacementGroup } from 'src/queries/placementGroups'; import { useRegionsQuery } from 'src/queries/regions/regions'; @@ -30,6 +31,7 @@ export const PlacementGroupsCreateDrawer = ( ) => { const { allPlacementGroups, + disabledPlacementGroupCreateButton, onClose, onPlacementGroupCreate, open, @@ -131,6 +133,16 @@ export const PlacementGroupsCreateDrawer = ( open={open} title="Create Placement Group" > + {disabledPlacementGroupCreateButton && ( + + )}
{generalError && } @@ -146,7 +158,7 @@ export const PlacementGroupsCreateDrawer = ( autoFocus: true, }} aria-label="Label for the Placement Group" - disabled={false} + disabled={disabledPlacementGroupCreateButton || false} errorText={errors.label} label="Label" name="label" @@ -156,6 +168,9 @@ export const PlacementGroupsCreateDrawer = ( /> {!selectedRegionId && ( )} setHasFormBeenSubmitted(true), diff --git a/packages/manager/src/features/PlacementGroups/PlacementGroupsDeleteModal.test.tsx b/packages/manager/src/features/PlacementGroups/PlacementGroupsDeleteModal.test.tsx index 39809ed69be..b979b5ecebe 100644 --- a/packages/manager/src/features/PlacementGroups/PlacementGroupsDeleteModal.test.tsx +++ b/packages/manager/src/features/PlacementGroups/PlacementGroupsDeleteModal.test.tsx @@ -102,7 +102,9 @@ describe('PlacementGroupsDeleteModal', () => { let renderResult: RenderResult; await act(async () => { - renderResult = renderWithTheme(); + renderResult = renderWithTheme( + + ); }); const { getByRole, getByTestId, getByText } = renderResult!; @@ -138,7 +140,9 @@ describe('PlacementGroupsDeleteModal', () => { let renderResult: RenderResult; await act(async () => { - renderResult = renderWithTheme(); + renderResult = renderWithTheme( + + ); }); const { getByRole, getByTestId, getByText } = renderResult!; diff --git a/packages/manager/src/features/PlacementGroups/PlacementGroupsDeleteModal.tsx b/packages/manager/src/features/PlacementGroups/PlacementGroupsDeleteModal.tsx index 347e9c1e874..c513f9831ed 100644 --- a/packages/manager/src/features/PlacementGroups/PlacementGroupsDeleteModal.tsx +++ b/packages/manager/src/features/PlacementGroups/PlacementGroupsDeleteModal.tsx @@ -23,12 +23,13 @@ import type { } from '@linode/api-v4'; interface Props { + disableUnassignButton: boolean; onClose: () => void; open: boolean; } export const PlacementGroupsDeleteModal = (props: Props) => { - const { onClose, open } = props; + const { disableUnassignButton, onClose, open } = props; const { id } = useParams<{ id: string }>(); const { data: selectedPlacementGroup } = usePlacementGroupQuery( +id, @@ -156,6 +157,7 @@ export const PlacementGroupsDeleteModal = (props: Props) => { fontFamily: theme.font.normal, fontSize: '0.875rem', })} + disabled={disableUnassignButton} loading={unassignLinodeLoading} variant="text" > diff --git a/packages/manager/src/features/PlacementGroups/PlacementGroupsDetail/PlacementGroupsDetail.tsx b/packages/manager/src/features/PlacementGroups/PlacementGroupsDetail/PlacementGroupsDetail.tsx index cfbca6122c8..04a2ba1d2b4 100644 --- a/packages/manager/src/features/PlacementGroups/PlacementGroupsDetail/PlacementGroupsDetail.tsx +++ b/packages/manager/src/features/PlacementGroups/PlacementGroupsDetail/PlacementGroupsDetail.tsx @@ -7,11 +7,14 @@ import { DocumentTitleSegment } from 'src/components/DocumentTitle'; import { ErrorState } from 'src/components/ErrorState/ErrorState'; import { LandingHeader } from 'src/components/LandingHeader'; import { NotFound } from 'src/components/NotFound'; +import { Notice } from 'src/components/Notice/Notice'; import { SafeTabPanel } from 'src/components/Tabs/SafeTabPanel'; import { TabLinkList } from 'src/components/Tabs/TabLinkList'; import { TabPanels } from 'src/components/Tabs/TabPanels'; import { Tabs } from 'src/components/Tabs/Tabs'; +import { getRestrictedResourceText } from 'src/features/Account/utils'; import { useFlags } from 'src/hooks/useFlags'; +import { useRestrictedGlobalGrantCheck } from 'src/hooks/useRestrictedGlobalGrantCheck'; import { useMutatePlacementGroup, usePlacementGroupQuery, @@ -37,6 +40,10 @@ export const PlacementGroupsDetail = () => { Boolean(flags.placementGroups?.enabled) ); + const isLinodeReadOnly = useRestrictedGlobalGrantCheck({ + globalGrantType: 'add_linodes', + }); + const { error: updatePlacementGroupError, mutateAsync: updatePlacementGroup, @@ -105,10 +112,21 @@ export const PlacementGroupsDetail = () => { }, pathname: `/placement-groups/${label}`, }} + disabledBreadcrumbEditButton={isLinodeReadOnly} docsLabel="Docs" docsLink="TODO VM_Placement: add doc link" title="Placement Group Detail" /> + {isLinodeReadOnly && ( + + )} history.push(tabs[i].routeName)} @@ -119,7 +137,10 @@ export const PlacementGroupsDetail = () => { - + diff --git a/packages/manager/src/features/PlacementGroups/PlacementGroupsDetail/PlacementGroupsLinodes/PlacementGroupsLinodes.test.tsx b/packages/manager/src/features/PlacementGroups/PlacementGroupsDetail/PlacementGroupsLinodes/PlacementGroupsLinodes.test.tsx index fb909698cc2..a6eee3ee8dc 100644 --- a/packages/manager/src/features/PlacementGroups/PlacementGroupsDetail/PlacementGroupsLinodes/PlacementGroupsLinodes.test.tsx +++ b/packages/manager/src/features/PlacementGroups/PlacementGroupsDetail/PlacementGroupsLinodes/PlacementGroupsLinodes.test.tsx @@ -9,7 +9,10 @@ import { PlacementGroupsLinodes } from './PlacementGroupsLinodes'; describe('PlacementGroupsLinodes', () => { it('renders an error state if placement groups are undefined', () => { const { getByText } = renderWithTheme( - + ); expect( @@ -28,7 +31,10 @@ describe('PlacementGroupsLinodes', () => { }); const { getByPlaceholderText, getByRole } = renderWithTheme( - + ); expect(getByPlaceholderText('Search Linodes')).toBeInTheDocument(); diff --git a/packages/manager/src/features/PlacementGroups/PlacementGroupsDetail/PlacementGroupsLinodes/PlacementGroupsLinodes.tsx b/packages/manager/src/features/PlacementGroups/PlacementGroupsDetail/PlacementGroupsLinodes/PlacementGroupsLinodes.tsx index 8a9b7e527fa..c65ef5a452c 100644 --- a/packages/manager/src/features/PlacementGroups/PlacementGroupsDetail/PlacementGroupsLinodes/PlacementGroupsLinodes.tsx +++ b/packages/manager/src/features/PlacementGroups/PlacementGroupsDetail/PlacementGroupsLinodes/PlacementGroupsLinodes.tsx @@ -23,11 +23,14 @@ import { PlacementGroupsLinodesTable } from './PlacementGroupsLinodesTable'; import type { Linode, PlacementGroup } from '@linode/api-v4'; interface Props { + isLinodeReadOnly: boolean; placementGroup: PlacementGroup | undefined; } -export const PlacementGroupsLinodes = (props: Props) => { - const { placementGroup } = props; +export const PlacementGroupsLinodes = ({ + isLinodeReadOnly, + placementGroup, +}: Props) => { const history = useHistory(); const { assignedLinodes, @@ -101,10 +104,15 @@ export const PlacementGroupsLinodes = (props: Props) => { diff --git a/packages/manager/src/features/PlacementGroups/PlacementGroupsDetail/PlacementGroupsLinodes/PlacementGroupsLinodesTableRow.tsx b/packages/manager/src/features/PlacementGroups/PlacementGroupsDetail/PlacementGroupsLinodes/PlacementGroupsLinodesTableRow.tsx index 1ec4768458f..7dee583a9ab 100644 --- a/packages/manager/src/features/PlacementGroups/PlacementGroupsDetail/PlacementGroupsLinodes/PlacementGroupsLinodesTableRow.tsx +++ b/packages/manager/src/features/PlacementGroups/PlacementGroupsDetail/PlacementGroupsLinodes/PlacementGroupsLinodesTableRow.tsx @@ -7,6 +7,7 @@ import { StatusIcon } from 'src/components/StatusIcon/StatusIcon'; import { TableCell } from 'src/components/TableCell'; import { TableRow } from 'src/components/TableRow'; import { getLinodeIconStatus } from 'src/features/Linodes/LinodesLanding/utils'; +import { useIsResourceRestricted } from 'src/hooks/useIsResourceRestricted'; import { capitalizeAllWords } from 'src/utilities/capitalize'; import type { Linode } from '@linode/api-v4'; @@ -28,6 +29,12 @@ export const PlacementGroupsLinodesTableRow = React.memo((props: Props) => { ); }; + const isAllowedToEditPlacementGroup = useIsResourceRestricted({ + grantLevel: 'read_write', + grantType: 'linode', + id: +linode.id, + }); + return ( { diff --git a/packages/manager/src/features/PlacementGroups/PlacementGroupsDetail/PlacementGroupsSummary/PlacementGroupsSummary.tsx b/packages/manager/src/features/PlacementGroups/PlacementGroupsDetail/PlacementGroupsSummary/PlacementGroupsSummary.tsx index 8e8bcef2557..089b0df80b2 100644 --- a/packages/manager/src/features/PlacementGroups/PlacementGroupsDetail/PlacementGroupsSummary/PlacementGroupsSummary.tsx +++ b/packages/manager/src/features/PlacementGroups/PlacementGroupsDetail/PlacementGroupsSummary/PlacementGroupsSummary.tsx @@ -1,7 +1,7 @@ import { AFFINITY_TYPES } from '@linode/api-v4'; import { useTheme } from '@mui/material'; -import Grid from '@mui/material/Unstable_Grid2'; import { styled } from '@mui/material/styles'; +import Grid from '@mui/material/Unstable_Grid2'; import * as React from 'react'; import { Box } from 'src/components/Box'; diff --git a/packages/manager/src/features/PlacementGroups/PlacementGroupsDetailPanel.tsx b/packages/manager/src/features/PlacementGroups/PlacementGroupsDetailPanel.tsx index ad7323cccbe..44ccb25a9e0 100644 --- a/packages/manager/src/features/PlacementGroups/PlacementGroupsDetailPanel.tsx +++ b/packages/manager/src/features/PlacementGroups/PlacementGroupsDetailPanel.tsx @@ -10,6 +10,7 @@ import { TextTooltip } from 'src/components/TextTooltip'; import { Typography } from 'src/components/Typography'; import { PlacementGroupsCreateDrawer } from 'src/features/PlacementGroups/PlacementGroupsCreateDrawer'; import { hasRegionReachedPlacementGroupCapacity } from 'src/features/PlacementGroups/utils'; +import { useRestrictedGlobalGrantCheck } from 'src/hooks/useRestrictedGlobalGrantCheck'; import { useUnpaginatedPlacementGroupsQuery } from 'src/queries/placementGroups'; import { useRegionsQuery } from 'src/queries/regions/regions'; @@ -45,6 +46,10 @@ export const PlacementGroupsDetailPanel = (props: Props) => { selectedRegion?.capabilities.includes('Placement Group') ); + const isLinodeReadOnly = useRestrictedGlobalGrantCheck({ + globalGrantType: 'add_linodes', + }); + const handlePlacementGroupSelection = (placementGroup: PlacementGroup) => { setSelectedPlacementGroup(placementGroup); handlePlacementGroupChange(placementGroup); @@ -146,6 +151,7 @@ export const PlacementGroupsDetailPanel = (props: Props) => { setIsCreatePlacementGroupDrawerOpen(false)} onPlacementGroupCreate={handlePlacementGroupCreated} open={isCreatePlacementGroupDrawerOpen} diff --git a/packages/manager/src/features/PlacementGroups/PlacementGroupsEditDrawer.test.tsx b/packages/manager/src/features/PlacementGroups/PlacementGroupsEditDrawer.test.tsx index d1455b3668a..e3b4e629e5e 100644 --- a/packages/manager/src/features/PlacementGroups/PlacementGroupsEditDrawer.test.tsx +++ b/packages/manager/src/features/PlacementGroups/PlacementGroupsEditDrawer.test.tsx @@ -58,6 +58,7 @@ describe('PlacementGroupsCreateDrawer', () => { const { getByLabelText, getByRole, getByText } = renderWithTheme( { - const { onClose, onPlacementGroupEdit, open } = props; + const { disableEditButton, onClose, onPlacementGroupEdit, open } = props; const { id } = useParams<{ id: string }>(); const { data: selectedPlacementGroup } = usePlacementGroupQuery( +id, @@ -131,7 +131,7 @@ export const PlacementGroupsEditDrawer = ( autoFocus: true, }} aria-label="Label for the Placement Group" - disabled={false} + disabled={disableEditButton || false} errorText={errors.label} label="Label" name="label" @@ -143,6 +143,7 @@ export const PlacementGroupsEditDrawer = ( { filter ); + const isLinodeReadOnly = useRestrictedGlobalGrantCheck({ + globalGrantType: 'add_linodes', + }); + const handleCreatePlacementGroup = () => { history.replace('/placement-groups/create'); }; @@ -90,10 +96,12 @@ export const PlacementGroupsLanding = React.memo(() => { return ( <> @@ -115,7 +123,15 @@ export const PlacementGroupsLanding = React.memo(() => { return ( <> { handleEditPlacementGroup={() => handleEditPlacementGroup(placementGroup) } + disabled={isLinodeReadOnly} key={`pg-${placementGroup.id}`} placementGroup={placementGroup} /> @@ -194,14 +211,17 @@ export const PlacementGroupsLanding = React.memo(() => { /> diff --git a/packages/manager/src/features/PlacementGroups/PlacementGroupsLanding/PlacementGroupsLandingEmptyState.tsx b/packages/manager/src/features/PlacementGroups/PlacementGroupsLanding/PlacementGroupsLandingEmptyState.tsx index a82fd0b40fb..51d5ab5902a 100644 --- a/packages/manager/src/features/PlacementGroups/PlacementGroupsLanding/PlacementGroupsLandingEmptyState.tsx +++ b/packages/manager/src/features/PlacementGroups/PlacementGroupsLanding/PlacementGroupsLandingEmptyState.tsx @@ -11,17 +11,20 @@ import { } from './PlacementGroupsLandingEmptyStateData'; interface Props { + disabledCreateButton: boolean; openCreatePlacementGroupDrawer: () => void; } -export const PlacementGroupsLandingEmptyState = (props: Props) => { - const { openCreatePlacementGroupDrawer } = props; - +export const PlacementGroupsLandingEmptyState = ({ + disabledCreateButton, + openCreatePlacementGroupDrawer, +}: Props) => { return ( { sendEvent({ action: 'Click:button', diff --git a/packages/manager/src/features/PlacementGroups/PlacementGroupsLanding/PlacementGroupsRow.test.tsx b/packages/manager/src/features/PlacementGroups/PlacementGroupsLanding/PlacementGroupsRow.test.tsx index 51e69c27e1f..4b2ed2bc05f 100644 --- a/packages/manager/src/features/PlacementGroups/PlacementGroupsLanding/PlacementGroupsRow.test.tsx +++ b/packages/manager/src/features/PlacementGroups/PlacementGroupsLanding/PlacementGroupsRow.test.tsx @@ -80,6 +80,7 @@ describe('PlacementGroupsLanding', () => { ], region: 'us-east', })} + disabled handleDeletePlacementGroup={handleDeletePlacementGroupMock} handleEditPlacementGroup={handleEditPlacementGroupMock} /> diff --git a/packages/manager/src/features/PlacementGroups/PlacementGroupsLanding/PlacementGroupsRow.tsx b/packages/manager/src/features/PlacementGroups/PlacementGroupsLanding/PlacementGroupsRow.tsx index 2e70918849a..7a7135024fb 100644 --- a/packages/manager/src/features/PlacementGroups/PlacementGroupsLanding/PlacementGroupsRow.tsx +++ b/packages/manager/src/features/PlacementGroups/PlacementGroupsLanding/PlacementGroupsRow.tsx @@ -18,6 +18,7 @@ import type { PlacementGroup } from '@linode/api-v4'; import type { Action } from 'src/components/ActionMenu/ActionMenu'; interface PlacementGroupsRowProps { + disabled: boolean; handleDeletePlacementGroup: () => void; handleEditPlacementGroup: () => void; placementGroup: PlacementGroup; @@ -25,6 +26,7 @@ interface PlacementGroupsRowProps { export const PlacementGroupsRow = React.memo( ({ + disabled, handleDeletePlacementGroup, handleEditPlacementGroup, placementGroup, @@ -85,6 +87,7 @@ export const PlacementGroupsRow = React.memo( {actions.map((action) => ( diff --git a/packages/manager/src/features/PlacementGroups/PlacementGroupsUnassignModal.tsx b/packages/manager/src/features/PlacementGroups/PlacementGroupsUnassignModal.tsx index b85347f5c8f..dbb2e3ec4f6 100644 --- a/packages/manager/src/features/PlacementGroups/PlacementGroupsUnassignModal.tsx +++ b/packages/manager/src/features/PlacementGroups/PlacementGroupsUnassignModal.tsx @@ -6,6 +6,7 @@ import { ActionsPanel } from 'src/components/ActionsPanel/ActionsPanel'; import { ConfirmationDialog } from 'src/components/ConfirmationDialog/ConfirmationDialog'; import { Notice } from 'src/components/Notice/Notice'; import { Typography } from 'src/components/Typography'; +import { useIsResourceRestricted } from 'src/hooks/useIsResourceRestricted'; import { useLinodeQuery } from 'src/queries/linodes/linodes'; import { useUnassignLinodesFromPlacementGroup } from 'src/queries/placementGroups'; @@ -22,6 +23,7 @@ export const PlacementGroupsUnassignModal = (props: Props) => { id: string; linodeId: string; }>(); + const { error, isLoading, @@ -48,10 +50,16 @@ export const PlacementGroupsUnassignModal = (props: Props) => { onClose(); }; + const isAllowedToEditPlacementGroup = useIsResourceRestricted({ + grantLevel: 'read_write', + grantType: 'linode', + id: +linodeId, + }); + const actions = ( void; selectedRegionId?: string; }; export type PlacementGroupsEditDrawerProps = PlacementGroupsDrawerPropsBase & { + disableEditButton: boolean; onPlacementGroupEdit?: (placementGroup: PlacementGroup) => void; }; diff --git a/packages/manager/src/mocks/serverHandlers.ts b/packages/manager/src/mocks/serverHandlers.ts index 826640f821c..0a8c787fd31 100644 --- a/packages/manager/src/mocks/serverHandlers.ts +++ b/packages/manager/src/mocks/serverHandlers.ts @@ -552,6 +552,10 @@ export const handlers = [ restricted: false, // Parent/Child: switch the `user_type` depending on what account view you need to mock. user_type: 'parent', + // PLACEMENT GROUPS TESTING - Permissions and Grants: + // Uncomment the two lines below: This is important! The grants endpoint is only called for restricted users. + // restricted: true, + // user_type: 'default', }); return HttpResponse.json(profile); }), @@ -562,7 +566,10 @@ export const handlers = [ return HttpResponse.json({ ...profileFactory.build(), ...(body as any) }); }), http.get('*/profile/grants', () => { - return HttpResponse.json(grantsFactory.build()); + // PLACEMENT GROUPS TESTING - Permissions and Grants + return HttpResponse.json( + grantsFactory.build({ global: { add_linodes: false } }) + ); }), http.get('*/profile/apps', () => { const tokens = appTokenFactory.buildList(5);