diff --git a/packages/api-v4/.changeset/pr-9585-changed-1692736077405.md b/packages/api-v4/.changeset/pr-9585-changed-1692736077405.md new file mode 100644 index 00000000000..39906f378ef --- /dev/null +++ b/packages/api-v4/.changeset/pr-9585-changed-1692736077405.md @@ -0,0 +1,5 @@ +--- +"@linode/api-v4": Changed +--- + +Change `Account` and `Grant`-related types to include VPC-related grants and capabilities ([#9585](https://github.com/linode/manager/pull/9585)) diff --git a/packages/api-v4/src/account/types.ts b/packages/api-v4/src/account/types.ts index b1f41f15f46..03e26466cbe 100644 --- a/packages/api-v4/src/account/types.ts +++ b/packages/api-v4/src/account/types.ts @@ -165,7 +165,8 @@ export type GrantType = | 'stackscript' | 'volume' | 'database' - | 'firewall'; + | 'firewall' + | 'vpc'; export type Grants = GlobalGrants & Record; diff --git a/packages/manager/.changeset/pr-9585-upcoming-features-1692736112038.md b/packages/manager/.changeset/pr-9585-upcoming-features-1692736112038.md new file mode 100644 index 00000000000..e3a84848592 --- /dev/null +++ b/packages/manager/.changeset/pr-9585-upcoming-features-1692736112038.md @@ -0,0 +1,5 @@ +--- +"@linode/manager": Upcoming Features +--- + +Add VPC-related permissions, capabilities, and grants ([#9585](https://github.com/linode/manager/pull/9585)) diff --git a/packages/manager/src/MainContent.tsx b/packages/manager/src/MainContent.tsx index 928e0ef3689..ce608bc497f 100644 --- a/packages/manager/src/MainContent.tsx +++ b/packages/manager/src/MainContent.tsx @@ -203,6 +203,12 @@ const MainContent = (props: CombinedProps) => { account?.capabilities ?? [] ); + const showVPCs = isFeatureEnabled( + 'VPCs', + Boolean(flags.vpc), + account?.capabilities ?? [] + ); + const defaultRoot = _isManagedAccount ? '/managed' : '/linodes'; const shouldDisplayMainContentBanner = @@ -358,9 +364,9 @@ const MainContent = (props: CombinedProps) => { {flags.selfServeBetas ? ( ) : null} - {flags.vpc && ( + {showVPCs ? ( - )} + ) : null} {/** We don't want to break any bookmarks. This can probably be removed eventually. */} diff --git a/packages/manager/src/components/PrimaryNav/PrimaryNav.tsx b/packages/manager/src/components/PrimaryNav/PrimaryNav.tsx index f65e69e9ea6..e1f2ad9ce10 100644 --- a/packages/manager/src/components/PrimaryNav/PrimaryNav.tsx +++ b/packages/manager/src/components/PrimaryNav/PrimaryNav.tsx @@ -3,6 +3,7 @@ import * as React from 'react'; import { Link, LinkProps, useLocation } from 'react-router-dom'; import Account from 'src/assets/icons/account.svg'; +import Beta from 'src/assets/icons/entityIcons/beta.svg'; import Storage from 'src/assets/icons/entityIcons/bucket.svg'; import Database from 'src/assets/icons/entityIcons/database.svg'; import Domain from 'src/assets/icons/entityIcons/domain.svg'; @@ -18,7 +19,6 @@ import Volume from 'src/assets/icons/entityIcons/volume.svg'; import VPC from 'src/assets/icons/entityIcons/vpc.svg'; import TooltipIcon from 'src/assets/icons/get_help.svg'; import Longview from 'src/assets/icons/longview.svg'; -import Beta from 'src/assets/icons/entityIcons/beta.svg'; import AkamaiLogo from 'src/assets/logo/akamai-logo.svg'; import { BetaChip } from 'src/components/BetaChip/BetaChip'; import { Divider } from 'src/components/Divider'; @@ -127,6 +127,12 @@ export const PrimaryNav = (props: Props) => { account?.capabilities ?? [] ); + const showVPCs = isFeatureEnabled( + 'VPCs', + Boolean(flags.vpc), + account?.capabilities ?? [] + ); + const prefetchObjectStorage = () => { if (!enableObjectPrefetch) { setEnableObjectPrefetch(true); @@ -177,7 +183,7 @@ export const PrimaryNav = (props: Props) => { }, { display: 'VPC', - hide: !flags.vpc, + hide: !showVPCs, href: '/vpcs', icon: , isBeta: true, @@ -273,7 +279,7 @@ export const PrimaryNav = (props: Props) => { allowMarketplacePrefetch, flags.databaseBeta, flags.aglb, - flags.vpc, + showVPCs, ] ); diff --git a/packages/manager/src/factories/grants.ts b/packages/manager/src/factories/grants.ts index 16c469f941b..c7851989f12 100644 --- a/packages/manager/src/factories/grants.ts +++ b/packages/manager/src/factories/grants.ts @@ -39,9 +39,9 @@ export const grantsFactory = Factory.Sync.makeFactory({ add_nodebalancers: true, add_stackscripts: true, add_volumes: true, + add_vpcs: true, cancel_account: false, longview_subscription: true, - add_vpcs: true, }, image: [ { @@ -85,4 +85,21 @@ export const grantsFactory = Factory.Sync.makeFactory({ permissions: 'read_only', }, ], + vpc: [ + { + id: 123, + label: 'example-entity1', + permissions: 'read_only', + }, + { + id: 124, + label: 'example-entity2', + permissions: 'read_write', + }, + { + id: 125, + label: 'example-entity3', + permissions: null, + }, + ], }); diff --git a/packages/manager/src/features/OneClickApps/oneClickApps.ts b/packages/manager/src/features/OneClickApps/oneClickApps.ts index 615529230ae..d81acafc4c0 100644 --- a/packages/manager/src/features/OneClickApps/oneClickApps.ts +++ b/packages/manager/src/features/OneClickApps/oneClickApps.ts @@ -825,7 +825,8 @@ export const oneClickApps: OCA[] = [ { href: 'https://www.linode.com/docs/products/tools/marketplace/guides/hashicorp-nomad-clients-cluster', - title: 'Deploy HashiCorp Nomad Clients Cluster through the Linode Marketplace', + title: + 'Deploy HashiCorp Nomad Clients Cluster through the Linode Marketplace', }, ], summary: 'Flexible scheduling and orchestration for diverse workloads.', diff --git a/packages/manager/src/features/TopMenu/AddNewMenu/AddNewMenu.tsx b/packages/manager/src/features/TopMenu/AddNewMenu/AddNewMenu.tsx index 740cb4e12c3..87de34040a0 100644 --- a/packages/manager/src/features/TopMenu/AddNewMenu/AddNewMenu.tsx +++ b/packages/manager/src/features/TopMenu/AddNewMenu/AddNewMenu.tsx @@ -25,6 +25,8 @@ import VPCIcon from 'src/assets/icons/entityIcons/vpc.svg'; import { Button } from 'src/components/Button/Button'; import { Divider } from 'src/components/Divider'; import { useFlags } from 'src/hooks/useFlags'; +import { useAccount } from 'src/queries/account'; +import { isFeatureEnabled } from 'src/utilities/accountCapabilities'; interface LinkProps { attr?: { [key: string]: boolean }; @@ -37,10 +39,17 @@ interface LinkProps { export const AddNewMenu = () => { const theme = useTheme(); + const { data: account } = useAccount(); const [anchorEl, setAnchorEl] = React.useState(null); const flags = useFlags(); const open = Boolean(anchorEl); + const showVPCs = isFeatureEnabled( + 'VPCs', + Boolean(flags.vpc), + account?.capabilities ?? [] + ); + const handleClick = (event: React.MouseEvent) => { setAnchorEl(event.currentTarget); }; @@ -80,7 +89,7 @@ export const AddNewMenu = () => { { description: 'Create a private and isolated network', entity: 'VPC', - hide: !flags.vpc, + hide: !showVPCs, icon: VPCIcon, link: '/vpcs/create', }, diff --git a/packages/manager/src/features/Users/UserPermissions.tsx b/packages/manager/src/features/Users/UserPermissions.tsx index b5767275919..d6f61c4a826 100644 --- a/packages/manager/src/features/Users/UserPermissions.tsx +++ b/packages/manager/src/features/Users/UserPermissions.tsx @@ -201,6 +201,7 @@ class UserPermissions extends React.Component { 'domain', 'longview', 'database', + 'vpc', ]; entitySetAllTo = (entity: GrantType, value: GrantLevel) => () => { @@ -283,6 +284,7 @@ class UserPermissions extends React.Component { 'add_volumes', 'add_firewalls', 'add_databases', + 'add_vpcs', 'cancel_account', ]; @@ -462,6 +464,7 @@ class UserPermissions extends React.Component { add_nodebalancers: 'Can add NodeBalancers to this account ($)', add_stackscripts: 'Can create StackScripts under this account', add_volumes: 'Can add Block Storage Volumes to this account ($)', + add_vpcs: 'Can add VPCs to this account', cancel_account: 'Can cancel the entire account', longview_subscription: 'Can modify this account\u{2019}s Longview subscription ($)', diff --git a/packages/manager/src/features/Users/UserPermissionsEntitySection.tsx b/packages/manager/src/features/Users/UserPermissionsEntitySection.tsx index 066f1da99af..e869aaa3df2 100644 --- a/packages/manager/src/features/Users/UserPermissionsEntitySection.tsx +++ b/packages/manager/src/features/Users/UserPermissionsEntitySection.tsx @@ -23,6 +23,7 @@ export const entityNameMap: Record = { longview: 'Longview Clients', firewall: 'Firewalls', database: 'Databases', + vpc: 'VPCs', }; interface Props { diff --git a/packages/manager/src/features/VPCs/VPCLanding/VPCEditDrawer.tsx b/packages/manager/src/features/VPCs/VPCLanding/VPCEditDrawer.tsx index c3bdba612c1..140b2199951 100644 --- a/packages/manager/src/features/VPCs/VPCLanding/VPCEditDrawer.tsx +++ b/packages/manager/src/features/VPCs/VPCLanding/VPCEditDrawer.tsx @@ -7,6 +7,7 @@ import { Drawer } from 'src/components/Drawer'; import { RegionSelect } from 'src/components/EnhancedSelect/variants/RegionSelect'; import { Notice } from 'src/components/Notice/Notice'; import { TextField } from 'src/components/TextField'; +import { useGrants, useProfile } from 'src/queries/profile'; import { useRegionsQuery } from 'src/queries/regions'; import { useUpdateVPCMutation } from 'src/queries/vpcs'; import { getErrorMap } from 'src/utilities/errorUtils'; @@ -22,6 +23,17 @@ const REGION_HELPER_TEXT = 'Region cannot be changed during beta.'; export const VPCEditDrawer = (props: Props) => { const { onClose, open, vpc } = props; + const { data: profile } = useProfile(); + const { data: grants } = useGrants(); + + const vpcPermissions = grants?.vpc.find((v) => v.id === vpc?.id); + + // there isn't a 'view VPC/Subnet' grant that does anything, so all VPCs get returned even for restricted users + // with permissions set to 'None'. Therefore, we're treating those as read_only as well + const readOnly = + Boolean(profile?.restricted) && + (vpcPermissions?.permissions === 'read_only' || grants?.vpc.length === 0); + const { error, isLoading, @@ -55,8 +67,16 @@ export const VPCEditDrawer = (props: Props) => { return ( {errorMap.none && } + {readOnly && ( + + )}
{ value={form.values.label} /> { { diff --git a/packages/manager/src/features/VPCs/index.tsx b/packages/manager/src/features/VPCs/index.tsx index f1a8ca644ad..bf01a887df7 100644 --- a/packages/manager/src/features/VPCs/index.tsx +++ b/packages/manager/src/features/VPCs/index.tsx @@ -4,22 +4,38 @@ import { Route, Switch } from 'react-router-dom'; import { DocumentTitleSegment } from 'src/components/DocumentTitle'; import { ProductInformationBanner } from 'src/components/ProductInformationBanner/ProductInformationBanner'; import { SuspenseLoader } from 'src/components/SuspenseLoader'; +import { useFlags } from 'src/hooks/useFlags'; +import { useAccount } from 'src/queries/account'; +import { isFeatureEnabled } from 'src/utilities/accountCapabilities'; const VPCCreate = React.lazy(() => import('./VPCCreate/VPCCreate')); const VPCDetail = React.lazy(() => import('./VPCDetail/VPCDetail')); const VPCLanding = React.lazy(() => import('./VPCLanding/VPCLanding')); const VPC = () => { + const flags = useFlags(); + const { data: account } = useAccount(); + + const showVPCs = isFeatureEnabled( + 'VPCs', + Boolean(flags.vpc), + account?.capabilities ?? [] + ); + return ( - }> - - - - - - - - + <> + {showVPCs ? ( + }> + + + + + + + + + ) : null} + ); };