diff --git a/packages/manager/src/features/Account/constants.ts b/packages/manager/src/features/Account/constants.ts
index 71092b6607c..1ee5f799f61 100644
--- a/packages/manager/src/features/Account/constants.ts
+++ b/packages/manager/src/features/Account/constants.ts
@@ -1,6 +1,19 @@
export const BUSINESS_PARTNER = 'business partner';
export const ADMINISTRATOR = 'administrator';
+export const grantTypeMap = {
+ database: 'Databases',
+ domain: 'Domains',
+ firewall: 'Firewalls',
+ image: 'Images',
+ linode: 'Linodes',
+ longview: 'Longview Clients',
+ nodebalancer: 'NodeBalancers',
+ stackscript: 'StackScripts',
+ volume: 'Volumes',
+ vpc: 'VPCs',
+} as const;
+
export const PARENT_PROXY_USER_CLOSE_ACCOUNT_TOOLTIP_TEXT =
'Remove indirect customers before closing the account.';
export const CHILD_USER_CLOSE_ACCOUNT_TOOLTIP_TEXT =
diff --git a/packages/manager/src/features/Account/types.ts b/packages/manager/src/features/Account/types.ts
new file mode 100644
index 00000000000..d2f4246087c
--- /dev/null
+++ b/packages/manager/src/features/Account/types.ts
@@ -0,0 +1,6 @@
+import { grantTypeMap } from 'src/features/Account/constants';
+
+// A useful type for getting the values of an object
+export type ObjectValues
= T[keyof T];
+
+export type GrantTypeMap = ObjectValues;
diff --git a/packages/manager/src/features/Account/utils.ts b/packages/manager/src/features/Account/utils.ts
index 7d1052487f9..8d9e95cb13f 100644
--- a/packages/manager/src/features/Account/utils.ts
+++ b/packages/manager/src/features/Account/utils.ts
@@ -1,6 +1,38 @@
import { getStorage, setStorage } from 'src/utilities/storage';
-import type { Token } from '@linode/api-v4';
+import type { GlobalGrantTypes, Grants, Profile, Token } from '@linode/api-v4';
+import type { GrantTypeMap } from 'src/features/Account/types';
+
+type ActionType = 'create' | 'delete' | 'edit' | 'view';
+
+interface GetRestrictedResourceText {
+ action?: ActionType;
+ isSingular?: boolean;
+ resourceType: GrantTypeMap;
+}
+export const getRestrictedResourceText = ({
+ action = 'edit',
+ isSingular = true,
+ resourceType,
+}: GetRestrictedResourceText): string => {
+ const resource = isSingular
+ ? 'this ' + resourceType.replace(/s$/, '')
+ : resourceType;
+
+ return `You don't have permissions to ${action} ${resource}. Please contact your account administrator to request the necessary permissions.`;
+};
+
+export const isRestrictedGlobalGrantType = ({
+ globalGrantType,
+ grants,
+ profile,
+}: {
+ globalGrantType: GlobalGrantTypes;
+ grants: Grants | undefined;
+ profile: Profile | undefined;
+}): boolean => {
+ return Boolean(profile?.restricted) && !grants?.global[globalGrantType];
+};
// TODO: Parent/Child: FOR MSW ONLY, REMOVE WHEN API IS READY
// ================================================================
diff --git a/packages/manager/src/features/NodeBalancers/NodeBalancerCreate.tsx b/packages/manager/src/features/NodeBalancers/NodeBalancerCreate.tsx
index bf464f0d5b7..99711c88d69 100644
--- a/packages/manager/src/features/NodeBalancers/NodeBalancerCreate.tsx
+++ b/packages/manager/src/features/NodeBalancers/NodeBalancerCreate.tsx
@@ -30,6 +30,10 @@ import { Tag, TagsInput } from 'src/components/TagsInput/TagsInput';
import { TextField } from 'src/components/TextField';
import { Typography } from 'src/components/Typography';
import { FIREWALL_GET_STARTED_LINK } from 'src/constants';
+import {
+ getRestrictedResourceText,
+ isRestrictedGlobalGrantType,
+} from 'src/features/Account/utils';
import { useFlags } from 'src/hooks/useFlags';
import {
reportAgreementSigningError,
@@ -126,11 +130,14 @@ const NodeBalancerCreate = () => {
const theme = useTheme();
const matchesSmDown = useMediaQuery(theme.breakpoints.down('md'));
- const disabled =
- Boolean(profile?.restricted) && !grants?.global.add_nodebalancers;
+ const isRestricted = isRestrictedGlobalGrantType({
+ globalGrantType: 'add_nodebalancers',
+ grants,
+ profile,
+ });
const addNodeBalancer = () => {
- if (disabled) {
+ if (isRestricted) {
return;
}
setNodeBalancerFields((prev) => ({
@@ -457,16 +464,17 @@ const NodeBalancerCreate = () => {
}}
title="Create"
/>
- {generalError && !disabled && (
+ {generalError && !isRestricted && (
{generalError}
)}
- {disabled && (
+ {isRestricted && (
{
)}
{
}))
: []
}
- disabled={disabled}
+ disabled={isRestricted}
onChange={tagsChange}
tagError={hasErrorFor('tags')}
/>
{
Learn more.
}
+ disabled={isRestricted}
entityType="nodebalancer"
selectedFirewallId={nodeBalancerFields.firewall_id ?? -1}
/>
@@ -575,7 +584,7 @@ const NodeBalancerCreate = () => {
checkPassive={nodeBalancerFields.configs[idx].check_passive!}
checkPath={nodeBalancerFields.configs[idx].check_path!}
configIdx={idx}
- disabled={disabled}
+ disabled={isRestricted}
errors={nodeBalancerConfig.errors}
healthCheckType={nodeBalancerFields.configs[idx].check!}
nodeBalancerRegion={nodeBalancerFields.region}
@@ -607,7 +616,7 @@ const NodeBalancerCreate = () => {