From b69c6c14b2c0599f847655c14e75303113a6456b Mon Sep 17 00:00:00 2001 From: Jaalah Ramos Date: Mon, 22 Jan 2024 10:59:38 -0500 Subject: [PATCH 01/13] Work in progress --- .../src/components/Button/StyledTagButton.ts | 34 +++++++++++++++++++ .../src/components/TagsPanel/TagsPanel.tsx | 16 +++++---- .../manager/src/features/Account/utils.ts | 14 ++++++++ .../NodeBalancerConfigurations.tsx | 2 ++ .../NodeBalancerDetail/NodeBalancerDetail.tsx | 7 ++-- .../NodeBalancerSettings.tsx | 9 ++++- .../NodeBalancerSummary.tsx | 10 ++++-- .../NodeBalancerSummary/SummaryPanel.tsx | 11 ++++-- .../BucketLanding/CreateBucketDrawer.tsx | 1 - .../BucketLanding/OMC_CreateBucketDrawer.tsx | 1 - 10 files changed, 90 insertions(+), 15 deletions(-) create mode 100644 packages/manager/src/components/Button/StyledTagButton.ts create mode 100644 packages/manager/src/features/Account/utils.ts diff --git a/packages/manager/src/components/Button/StyledTagButton.ts b/packages/manager/src/components/Button/StyledTagButton.ts new file mode 100644 index 00000000000..736c184d405 --- /dev/null +++ b/packages/manager/src/components/Button/StyledTagButton.ts @@ -0,0 +1,34 @@ +import { styled } from '@mui/material/styles'; + +import Plus from 'src/assets/icons/plusSign.svg'; + +import { Button } from './Button'; + +/** + * A button for Tags. Eventually this treatment will go away, + * but the sake of the MUI migration we need to keep it around for now, and as a styled component in order to get rid of + * spreading excessive styles for everywhere this is used. + * + */ +export const StyledTagButton = styled(Button, { + label: 'StyledTagButton', +})(({ theme, ...props }) => ({ + ...(!props.disabled && { + '&:hover': { + backgroundColor: theme.color.tagButton, + border: 'none', + }, + }), + backgroundColor: theme.color.tagButton, + border: 'none', + color: theme.textColors.linkActiveLight, + fontSize: '0.875rem', +})); + +export const StyledPlusIcon = styled(Plus, { + label: 'StyledPlusIcon', +})(({ theme, ...props }) => ({ + color: props.disabled ? theme.color.disabledText : theme.color.tagIcon, + height: '10px', + width: '10px', +})); diff --git a/packages/manager/src/components/TagsPanel/TagsPanel.tsx b/packages/manager/src/components/TagsPanel/TagsPanel.tsx index ba1fb36718d..be5b9e4388a 100644 --- a/packages/manager/src/components/TagsPanel/TagsPanel.tsx +++ b/packages/manager/src/components/TagsPanel/TagsPanel.tsx @@ -1,7 +1,10 @@ import * as React from 'react'; import { useQueryClient } from 'react-query'; -import Plus from 'src/assets/icons/plusSign.svg'; +import { + StyledPlusIcon, + StyledTagButton, +} from 'src/components/Button/StyledTagButton'; import { CircleProgress } from 'src/components/CircleProgress'; import Select from 'src/components/EnhancedSelect/Select'; import { Tag } from 'src/components/Tag/Tag'; @@ -162,6 +165,7 @@ export const TagsPanel = (props: TagsPanelProps) => { className={classes.selectTag} creatable createOptionPosition="first" + disabled={disabled} escapeClearsValue hideLabel isLoading={userTagsLoading} @@ -178,14 +182,14 @@ export const TagsPanel = (props: TagsPanelProps) => { [classes.hasError]: tagError.length > 0, })} > - + )}
diff --git a/packages/manager/src/features/Account/utils.ts b/packages/manager/src/features/Account/utils.ts new file mode 100644 index 00000000000..20fc9588cd1 --- /dev/null +++ b/packages/manager/src/features/Account/utils.ts @@ -0,0 +1,14 @@ +import { UserType } from '@linode/api-v4/lib/account'; + +import { + ADMINISTRATOR, + BUSINESS_PARTNER, +} from 'src/features/Account/constants'; + +export const getRestrictedAccessMessage = (userType: UserType) => { + if (userType === 'parent') { + return `Access restricted. Please contact your ${BUSINESS_PARTNER} to request the necessary permissions.`; + } + + return `Access restricted. Please contact your ${ADMINISTRATOR} to request the necessary permissions.`; +}; diff --git a/packages/manager/src/features/NodeBalancers/NodeBalancerDetail/NodeBalancerConfigurations.tsx b/packages/manager/src/features/NodeBalancers/NodeBalancerDetail/NodeBalancerConfigurations.tsx index e10b7c1dcec..5ddbce92e06 100644 --- a/packages/manager/src/features/NodeBalancers/NodeBalancerDetail/NodeBalancerConfigurations.tsx +++ b/packages/manager/src/features/NodeBalancers/NodeBalancerDetail/NodeBalancerConfigurations.tsx @@ -62,6 +62,7 @@ import type { NodeBalancerConfigFieldsWithStatus, NodeBalancerConfigNodeFields, } from '../types'; +import type { Profile } from '@linode/api-v4'; const StyledPortsSpan = styled('span', { label: 'StyledPortsSpan', @@ -89,6 +90,7 @@ const StyledConfigsButton = styled(Button, { interface Props { nodeBalancerLabel: string; nodeBalancerRegion: string; + profile: Profile | undefined; } interface MatchProps { diff --git a/packages/manager/src/features/NodeBalancers/NodeBalancerDetail/NodeBalancerDetail.tsx b/packages/manager/src/features/NodeBalancers/NodeBalancerDetail/NodeBalancerDetail.tsx index 99f6ab3cb1d..d2b2918cbdb 100644 --- a/packages/manager/src/features/NodeBalancers/NodeBalancerDetail/NodeBalancerDetail.tsx +++ b/packages/manager/src/features/NodeBalancers/NodeBalancerDetail/NodeBalancerDetail.tsx @@ -18,6 +18,7 @@ import { useNodeBalancerQuery, useNodebalancerUpdateMutation, } from 'src/queries/nodebalancers'; +import { useProfile } from 'src/queries/profile'; import { getErrorMap } from 'src/utilities/errorUtils'; import NodeBalancerConfigurations from './NodeBalancerConfigurations'; @@ -30,6 +31,7 @@ export const NodeBalancerDetail = () => { const { nodeBalancerId } = useParams<{ nodeBalancerId: string }>(); const id = Number(nodeBalancerId); const [label, setLabel] = React.useState(); + const { data: profile } = useProfile(); const { error: updateError, @@ -118,16 +120,17 @@ export const NodeBalancerDetail = () => { - + - + diff --git a/packages/manager/src/features/NodeBalancers/NodeBalancerDetail/NodeBalancerSettings.tsx b/packages/manager/src/features/NodeBalancers/NodeBalancerDetail/NodeBalancerSettings.tsx index 7b8d275a7b6..621e349ffce 100644 --- a/packages/manager/src/features/NodeBalancers/NodeBalancerDetail/NodeBalancerSettings.tsx +++ b/packages/manager/src/features/NodeBalancers/NodeBalancerDetail/NodeBalancerSettings.tsx @@ -17,8 +17,14 @@ import { import { NodeBalancerDeleteDialog } from '../NodeBalancerDeleteDialog'; import { NodeBalancerFirewalls } from './NodeBalancerFirewalls'; +import { Profile } from '@linode/api-v4'; -export const NodeBalancerSettings = () => { +interface Props { + profile: Profile | undefined; +} + +export const NodeBalancerSettings = (props: Props) => { + const { profile } = props; const flags = useFlags(); const theme = useTheme(); const { nodeBalancerId } = useParams<{ nodeBalancerId: string }>(); @@ -26,6 +32,7 @@ export const NodeBalancerSettings = () => { const { data: nodebalancer } = useNodeBalancerQuery(id); const { data: attachedFirewallData } = useNodeBalancersFirewallsQuery(id); const displayFirewallInfoText = attachedFirewallData?.results === 0; + const isRestricted = profile?.restricted; const { error: labelError, diff --git a/packages/manager/src/features/NodeBalancers/NodeBalancerDetail/NodeBalancerSummary/NodeBalancerSummary.tsx b/packages/manager/src/features/NodeBalancers/NodeBalancerDetail/NodeBalancerSummary/NodeBalancerSummary.tsx index c13926a5156..973008055bd 100644 --- a/packages/manager/src/features/NodeBalancers/NodeBalancerDetail/NodeBalancerSummary/NodeBalancerSummary.tsx +++ b/packages/manager/src/features/NodeBalancers/NodeBalancerDetail/NodeBalancerSummary/NodeBalancerSummary.tsx @@ -1,3 +1,4 @@ +import { Profile } from '@linode/api-v4/'; import Grid from '@mui/material/Unstable_Grid2'; import { styled } from '@mui/material/styles'; import * as React from 'react'; @@ -9,7 +10,12 @@ import { useNodeBalancerQuery } from 'src/queries/nodebalancers'; import { SummaryPanel } from './SummaryPanel'; import { TablesPanel } from './TablesPanel'; -export const NodeBalancerSummary = () => { +interface Props { + profile: Profile | undefined; +} + +export const NodeBalancerSummary = (props: Props) => { + const { profile } = props; const { nodeBalancerId } = useParams<{ nodeBalancerId: string }>(); const id = Number(nodeBalancerId); const { data: nodebalancer } = useNodeBalancerQuery(id); @@ -22,7 +28,7 @@ export const NodeBalancerSummary = () => { - +
diff --git a/packages/manager/src/features/NodeBalancers/NodeBalancerDetail/NodeBalancerSummary/SummaryPanel.tsx b/packages/manager/src/features/NodeBalancers/NodeBalancerDetail/NodeBalancerSummary/SummaryPanel.tsx index 0c46b95657e..42eebfa050a 100644 --- a/packages/manager/src/features/NodeBalancers/NodeBalancerDetail/NodeBalancerSummary/SummaryPanel.tsx +++ b/packages/manager/src/features/NodeBalancers/NodeBalancerDetail/NodeBalancerSummary/SummaryPanel.tsx @@ -1,3 +1,4 @@ +import { Profile } from '@linode/api-v4/'; import { styled } from '@mui/material/styles'; import * as React from 'react'; import { Link, useParams } from 'react-router-dom'; @@ -7,16 +8,20 @@ import { TagsPanel } from 'src/components/TagsPanel/TagsPanel'; import { Typography } from 'src/components/Typography'; import { IPAddress } from 'src/features/Linodes/LinodesLanding/IPAddress'; import { useFlags } from 'src/hooks/useFlags'; +import { useNodeBalancersFirewallsQuery } from 'src/queries/nodebalancers'; import { useAllNodeBalancerConfigsQuery, useNodeBalancerQuery, useNodebalancerUpdateMutation, } from 'src/queries/nodebalancers'; -import { useNodeBalancersFirewallsQuery } from 'src/queries/nodebalancers'; import { useRegionsQuery } from 'src/queries/regions'; import { convertMegabytesTo } from 'src/utilities/unitConversions'; -export const SummaryPanel = () => { +interface Props { + profile: Profile | undefined; +} +export const SummaryPanel = (props: Props) => { + const { profile } = props; const flags = useFlags(); const { nodeBalancerId } = useParams<{ nodeBalancerId: string }>(); const id = Number(nodeBalancerId); @@ -29,6 +34,7 @@ export const SummaryPanel = () => { const region = regions?.find((r) => r.id === nodebalancer?.region); const { mutateAsync: updateNodeBalancer } = useNodebalancerUpdateMutation(id); const displayFirewallLink = !!attachedFirewallData?.data?.length; + const isRestricted = profile?.restricted; const configPorts = configs?.reduce((acc, config) => { return [...acc, { configId: config.id, port: config.port }]; @@ -127,6 +133,7 @@ export const SummaryPanel = () => { Tags updateNodeBalancer({ tags })} /> diff --git a/packages/manager/src/features/ObjectStorage/BucketLanding/CreateBucketDrawer.tsx b/packages/manager/src/features/ObjectStorage/BucketLanding/CreateBucketDrawer.tsx index 87d1a0275f0..d2e981bce09 100644 --- a/packages/manager/src/features/ObjectStorage/BucketLanding/CreateBucketDrawer.tsx +++ b/packages/manager/src/features/ObjectStorage/BucketLanding/CreateBucketDrawer.tsx @@ -123,7 +123,6 @@ export const CreateBucketDrawer = (props: Props) => { {isRestrictedUser && ( diff --git a/packages/manager/src/features/ObjectStorage/BucketLanding/OMC_CreateBucketDrawer.tsx b/packages/manager/src/features/ObjectStorage/BucketLanding/OMC_CreateBucketDrawer.tsx index c1d09d8b7ab..61d49f9ab43 100644 --- a/packages/manager/src/features/ObjectStorage/BucketLanding/OMC_CreateBucketDrawer.tsx +++ b/packages/manager/src/features/ObjectStorage/BucketLanding/OMC_CreateBucketDrawer.tsx @@ -118,7 +118,6 @@ export const OMC_CreateBucketDrawer = (props: Props) => { {isRestrictedUser && ( From 40c7d96ac35dd839e62abab9fdb2de0de00b4b05 Mon Sep 17 00:00:00 2001 From: Jaalah Ramos Date: Mon, 22 Jan 2024 14:29:57 -0500 Subject: [PATCH 02/13] feat: [M3-7490] - Improve Nodebalancer restricted user experience --- .../src/components/Button/StyledTagButton.ts | 6 +++++- .../manager/src/features/Account/constants.ts | 1 + .../manager/src/features/Account/utils.ts | 14 -------------- .../NodeBalancerConfigurations.tsx | 13 ++++++++++--- .../NodeBalancerDetail/NodeBalancerDetail.tsx | 16 +++++++++++----- .../NodeBalancerSettings.tsx | 19 +++++++++---------- .../NodeBalancerSummary.tsx | 10 ++-------- .../NodeBalancerSummary/SummaryPanel.tsx | 11 ++++------- .../NodeBalancerActionMenu.tsx | 6 +++++- .../NodeBalancersLanding.tsx | 11 ++++++++++- 10 files changed, 57 insertions(+), 50 deletions(-) delete mode 100644 packages/manager/src/features/Account/utils.ts diff --git a/packages/manager/src/components/Button/StyledTagButton.ts b/packages/manager/src/components/Button/StyledTagButton.ts index 736c184d405..9b0502c3538 100644 --- a/packages/manager/src/components/Button/StyledTagButton.ts +++ b/packages/manager/src/components/Button/StyledTagButton.ts @@ -28,7 +28,11 @@ export const StyledTagButton = styled(Button, { export const StyledPlusIcon = styled(Plus, { label: 'StyledPlusIcon', })(({ theme, ...props }) => ({ - color: props.disabled ? theme.color.disabledText : theme.color.tagIcon, + color: props.disabled + ? theme.name === 'dark' + ? '#5c6470' + : theme.color.disabledText + : theme.color.tagIcon, height: '10px', width: '10px', })); diff --git a/packages/manager/src/features/Account/constants.ts b/packages/manager/src/features/Account/constants.ts index ed2d1401fe5..27b9b590a14 100644 --- a/packages/manager/src/features/Account/constants.ts +++ b/packages/manager/src/features/Account/constants.ts @@ -1,2 +1,3 @@ export const BUSINESS_PARTNER = 'business partner'; export const ADMINISTRATOR = 'administrator'; +export const RESTRICTED_ACCESS_NOTICE = `Access restricted. Please contact your ${ADMINISTRATOR} to request the necessary permissions.`; diff --git a/packages/manager/src/features/Account/utils.ts b/packages/manager/src/features/Account/utils.ts deleted file mode 100644 index 20fc9588cd1..00000000000 --- a/packages/manager/src/features/Account/utils.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { UserType } from '@linode/api-v4/lib/account'; - -import { - ADMINISTRATOR, - BUSINESS_PARTNER, -} from 'src/features/Account/constants'; - -export const getRestrictedAccessMessage = (userType: UserType) => { - if (userType === 'parent') { - return `Access restricted. Please contact your ${BUSINESS_PARTNER} to request the necessary permissions.`; - } - - return `Access restricted. Please contact your ${ADMINISTRATOR} to request the necessary permissions.`; -}; diff --git a/packages/manager/src/features/NodeBalancers/NodeBalancerDetail/NodeBalancerConfigurations.tsx b/packages/manager/src/features/NodeBalancers/NodeBalancerDetail/NodeBalancerConfigurations.tsx index 5ddbce92e06..739e8c22108 100644 --- a/packages/manager/src/features/NodeBalancers/NodeBalancerDetail/NodeBalancerConfigurations.tsx +++ b/packages/manager/src/features/NodeBalancers/NodeBalancerDetail/NodeBalancerConfigurations.tsx @@ -62,7 +62,7 @@ import type { NodeBalancerConfigFieldsWithStatus, NodeBalancerConfigNodeFields, } from '../types'; -import type { Profile } from '@linode/api-v4'; +import type { Grants } from '@linode/api-v4'; const StyledPortsSpan = styled('span', { label: 'StyledPortsSpan', @@ -88,9 +88,9 @@ const StyledConfigsButton = styled(Button, { })); interface Props { + grants: Grants | undefined; nodeBalancerLabel: string; nodeBalancerRegion: string; - profile: Profile | undefined; } interface MatchProps { @@ -161,7 +161,7 @@ const formatNodesStatus = (nodes: NodeBalancerConfigNodeFields[]) => { }; class NodeBalancerConfigurations extends React.Component { render() { - const { nodeBalancerLabel } = this.props; + const { grants, nodeBalancerLabel } = this.props; const { configErrors, configSubmitting, @@ -170,6 +170,8 @@ class NodeBalancerConfigurations extends React.Component { panelMessages, } = this.state; + const isRestricted = grants?.nodebalancer?.[0]?.permissions === 'read_only'; + return (
{ this.addNodeBalancerConfig()} > {configs.length === 0 @@ -618,6 +621,7 @@ class NodeBalancerConfigurations extends React.Component { }, idx: number ) => { + const { grants } = this.props; const isNewConfig = this.state.hasUnsavedConfig && idx === this.state.configs.length - 1; const { panelNodeMessages } = this.state; @@ -630,6 +634,8 @@ class NodeBalancerConfigurations extends React.Component { ? parseInt(expandedConfigId, 10) === config.id : false; + const isRestricted = grants?.nodebalancer?.[0]?.permissions === 'read_only'; + const L = { algorithmLens: lensTo(['algorithm']), checkBodyLens: lensTo(['check_body']), @@ -689,6 +695,7 @@ class NodeBalancerConfigurations extends React.Component { checkPassive={view(L.checkPassiveLens, this.state)} checkPath={view(L.checkPathLens, this.state)} configIdx={idx} + disabled={isRestricted} errors={configErrors[idx]} forEdit healthCheckAttempts={view(L.healthCheckAttemptsLens, this.state)} diff --git a/packages/manager/src/features/NodeBalancers/NodeBalancerDetail/NodeBalancerDetail.tsx b/packages/manager/src/features/NodeBalancers/NodeBalancerDetail/NodeBalancerDetail.tsx index d2b2918cbdb..694bd091822 100644 --- a/packages/manager/src/features/NodeBalancers/NodeBalancerDetail/NodeBalancerDetail.tsx +++ b/packages/manager/src/features/NodeBalancers/NodeBalancerDetail/NodeBalancerDetail.tsx @@ -14,11 +14,12 @@ 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 { RESTRICTED_ACCESS_NOTICE } from 'src/features/Account/constants'; import { useNodeBalancerQuery, useNodebalancerUpdateMutation, } from 'src/queries/nodebalancers'; -import { useProfile } from 'src/queries/profile'; +import { useGrants } from 'src/queries/profile'; import { getErrorMap } from 'src/utilities/errorUtils'; import NodeBalancerConfigurations from './NodeBalancerConfigurations'; @@ -31,7 +32,7 @@ export const NodeBalancerDetail = () => { const { nodeBalancerId } = useParams<{ nodeBalancerId: string }>(); const id = Number(nodeBalancerId); const [label, setLabel] = React.useState(); - const { data: profile } = useProfile(); + const { data: grants } = useGrants(); const { error: updateError, @@ -40,6 +41,8 @@ export const NodeBalancerDetail = () => { const { data: nodebalancer, error, isLoading } = useNodeBalancerQuery(id); + const isRestricted = grants?.nodebalancer?.[0]?.permissions === 'read_only'; + React.useEffect(() => { if (label !== nodebalancer?.label) { setLabel(nodebalancer?.label); @@ -109,6 +112,9 @@ export const NodeBalancerDetail = () => { title={nodeBalancerLabel} /> {errorMap.none && } + {isRestricted && ( + + )} matches(tab.routeName)), @@ -120,17 +126,17 @@ export const NodeBalancerDetail = () => { - + - + diff --git a/packages/manager/src/features/NodeBalancers/NodeBalancerDetail/NodeBalancerSettings.tsx b/packages/manager/src/features/NodeBalancers/NodeBalancerDetail/NodeBalancerSettings.tsx index 621e349ffce..b46d68c3513 100644 --- a/packages/manager/src/features/NodeBalancers/NodeBalancerDetail/NodeBalancerSettings.tsx +++ b/packages/manager/src/features/NodeBalancers/NodeBalancerDetail/NodeBalancerSettings.tsx @@ -9,22 +9,18 @@ import { FormHelperText } from 'src/components/FormHelperText'; import { InputAdornment } from 'src/components/InputAdornment'; import { TextField } from 'src/components/TextField'; import { useFlags } from 'src/hooks/useFlags'; -import { useNodeBalancersFirewallsQuery } from 'src/queries/nodebalancers'; import { useNodeBalancerQuery, useNodebalancerUpdateMutation, } from 'src/queries/nodebalancers'; +import { useNodeBalancersFirewallsQuery } from 'src/queries/nodebalancers'; +import { useGrants } from 'src/queries/profile'; import { NodeBalancerDeleteDialog } from '../NodeBalancerDeleteDialog'; import { NodeBalancerFirewalls } from './NodeBalancerFirewalls'; -import { Profile } from '@linode/api-v4'; - -interface Props { - profile: Profile | undefined; -} -export const NodeBalancerSettings = (props: Props) => { - const { profile } = props; +export const NodeBalancerSettings = () => { + const { data: grants } = useGrants(); const flags = useFlags(); const theme = useTheme(); const { nodeBalancerId } = useParams<{ nodeBalancerId: string }>(); @@ -32,7 +28,7 @@ export const NodeBalancerSettings = (props: Props) => { const { data: nodebalancer } = useNodeBalancerQuery(id); const { data: attachedFirewallData } = useNodeBalancersFirewallsQuery(id); const displayFirewallInfoText = attachedFirewallData?.results === 0; - const isRestricted = profile?.restricted; + const isRestricted = grants?.nodebalancer?.[0]?.permissions === 'read_only'; const { error: labelError, @@ -80,6 +76,7 @@ export const NodeBalancerSettings = (props: Props) => { setLabel(e.target.value)} @@ -89,7 +86,7 @@ export const NodeBalancerSettings = (props: Props) => {
diff --git a/packages/manager/src/features/NodeBalancers/NodeBalancerDetail/NodeBalancerSummary/SummaryPanel.tsx b/packages/manager/src/features/NodeBalancers/NodeBalancerDetail/NodeBalancerSummary/SummaryPanel.tsx index 42eebfa050a..e2552ff04b4 100644 --- a/packages/manager/src/features/NodeBalancers/NodeBalancerDetail/NodeBalancerSummary/SummaryPanel.tsx +++ b/packages/manager/src/features/NodeBalancers/NodeBalancerDetail/NodeBalancerSummary/SummaryPanel.tsx @@ -1,4 +1,3 @@ -import { Profile } from '@linode/api-v4/'; import { styled } from '@mui/material/styles'; import * as React from 'react'; import { Link, useParams } from 'react-router-dom'; @@ -14,14 +13,12 @@ import { useNodeBalancerQuery, useNodebalancerUpdateMutation, } from 'src/queries/nodebalancers'; +import { useGrants } from 'src/queries/profile'; import { useRegionsQuery } from 'src/queries/regions'; import { convertMegabytesTo } from 'src/utilities/unitConversions'; -interface Props { - profile: Profile | undefined; -} -export const SummaryPanel = (props: Props) => { - const { profile } = props; +export const SummaryPanel = () => { + const { data: grants } = useGrants(); const flags = useFlags(); const { nodeBalancerId } = useParams<{ nodeBalancerId: string }>(); const id = Number(nodeBalancerId); @@ -34,7 +31,7 @@ export const SummaryPanel = (props: Props) => { const region = regions?.find((r) => r.id === nodebalancer?.region); const { mutateAsync: updateNodeBalancer } = useNodebalancerUpdateMutation(id); const displayFirewallLink = !!attachedFirewallData?.data?.length; - const isRestricted = profile?.restricted; + const isRestricted = grants?.nodebalancer?.[0]?.permissions === 'read_only'; const configPorts = configs?.reduce((acc, config) => { return [...acc, { configId: config.id, port: config.port }]; diff --git a/packages/manager/src/features/NodeBalancers/NodeBalancersLanding/NodeBalancerActionMenu.tsx b/packages/manager/src/features/NodeBalancers/NodeBalancersLanding/NodeBalancerActionMenu.tsx index 9a3c81a1a82..c6eef90eb46 100644 --- a/packages/manager/src/features/NodeBalancers/NodeBalancersLanding/NodeBalancerActionMenu.tsx +++ b/packages/manager/src/features/NodeBalancers/NodeBalancersLanding/NodeBalancerActionMenu.tsx @@ -6,7 +6,7 @@ import { useHistory } from 'react-router-dom'; import { Action, ActionMenu } from 'src/components/ActionMenu/ActionMenu'; import { Hidden } from 'src/components/Hidden'; import { InlineMenuAction } from 'src/components/InlineMenuAction/InlineMenuAction'; - +import { useGrants } from 'src/queries/profile'; interface Props { label: string; nodeBalancerId: number; @@ -18,8 +18,10 @@ export const NodeBalancerActionMenu = (props: Props) => { const matchesMdDown = useMediaQuery(theme.breakpoints.down('lg')); const history = useHistory(); + const { data: grants } = useGrants(); const { label, nodeBalancerId, toggleDialog } = props; + const isRestricted = grants?.nodebalancer?.[0]?.permissions === 'read_only'; const actions: Action[] = [ { @@ -35,6 +37,7 @@ export const NodeBalancerActionMenu = (props: Props) => { title: 'Settings', }, { + disabled: isRestricted, onClick: () => { toggleDialog(nodeBalancerId, label); }, @@ -49,6 +52,7 @@ export const NodeBalancerActionMenu = (props: Props) => { return ( diff --git a/packages/manager/src/features/NodeBalancers/NodeBalancersLanding/NodeBalancersLanding.tsx b/packages/manager/src/features/NodeBalancers/NodeBalancersLanding/NodeBalancersLanding.tsx index d9588139af5..2a84ef20c04 100644 --- a/packages/manager/src/features/NodeBalancers/NodeBalancersLanding/NodeBalancersLanding.tsx +++ b/packages/manager/src/features/NodeBalancers/NodeBalancersLanding/NodeBalancersLanding.tsx @@ -6,6 +6,7 @@ import { DocumentTitleSegment } from 'src/components/DocumentTitle'; import { ErrorState } from 'src/components/ErrorState/ErrorState'; import { Hidden } from 'src/components/Hidden'; import { LandingHeader } from 'src/components/LandingHeader'; +import { Notice } from 'src/components/Notice/Notice'; import { PaginationFooter } from 'src/components/PaginationFooter/PaginationFooter'; import { Table } from 'src/components/Table'; import { TableBody } from 'src/components/TableBody'; @@ -14,14 +15,15 @@ import { TableHead } from 'src/components/TableHead'; import { TableRow } from 'src/components/TableRow'; import { TableSortCell } from 'src/components/TableSortCell/TableSortCell'; import { TransferDisplay } from 'src/components/TransferDisplay/TransferDisplay'; +import { RESTRICTED_ACCESS_NOTICE } from 'src/features/Account/constants'; import { useOrder } from 'src/hooks/useOrder'; import { usePagination } from 'src/hooks/usePagination'; import { useNodeBalancersQuery } from 'src/queries/nodebalancers'; +import { useGrants } from 'src/queries/profile'; import { NodeBalancerDeleteDialog } from '../NodeBalancerDeleteDialog'; import { NodeBalancerTableRow } from './NodeBalancerTableRow'; import { NodeBalancerLandingEmptyState } from './NodeBalancersLandingEmptyState'; - const preferenceKey = 'nodebalancers'; export const NodeBalancersLanding = () => { @@ -35,6 +37,9 @@ export const NodeBalancersLanding = () => { const history = useHistory(); const pagination = usePagination(1, preferenceKey); + const { data: grants } = useGrants(); + + const isRestricted = grants?.nodebalancer?.[0]?.permissions === 'read_only'; const { handleOrderChange, order, orderBy } = useOrder( { @@ -86,11 +91,15 @@ export const NodeBalancersLanding = () => { <> history.push('/nodebalancers/create')} title="NodeBalancers" /> + {isRestricted && ( + + )} From 3109cd7015293d550ffcf3f9e05a5c7460ed6f07 Mon Sep 17 00:00:00 2001 From: Jaalah Ramos Date: Mon, 22 Jan 2024 16:48:14 -0500 Subject: [PATCH 03/13] feat: [M3-7490] - Improve Nodebalancer restricted user experience --- .../src/components/ActionMenu/ActionMenu.tsx | 14 ++++---- .../SelectFirewallPanel.tsx | 5 ++- .../manager/src/features/Account/constants.ts | 2 +- .../NodeBalancers/NodeBalancerCreate.tsx | 13 +++++--- .../NodeBalancerConfigurations.tsx | 31 +++++++++++++++--- .../NodeBalancerDetail/NodeBalancerDetail.tsx | 8 +++-- .../NodeBalancerActionMenu.tsx | 12 ++++--- .../NodeBalancerTableRow.tsx | 2 +- .../NodeBalancersLanding.tsx | 9 ++++-- .../NodeBalancersLandingEmptyState.tsx | 15 +++++++++ .../src/hooks/useFeatureRestriction.ts | 32 +++++++++++++++++++ 11 files changed, 117 insertions(+), 26 deletions(-) create mode 100644 packages/manager/src/hooks/useFeatureRestriction.ts diff --git a/packages/manager/src/components/ActionMenu/ActionMenu.tsx b/packages/manager/src/components/ActionMenu/ActionMenu.tsx index b450b853072..d37404cb53f 100644 --- a/packages/manager/src/components/ActionMenu/ActionMenu.tsx +++ b/packages/manager/src/components/ActionMenu/ActionMenu.tsx @@ -1,4 +1,4 @@ -import { IconButton, ListItemText } from '@mui/material'; +import { IconButton, ListItemText, useTheme } from '@mui/material'; import Menu from '@mui/material/Menu'; import MenuItem from '@mui/material/MenuItem'; import * as React from 'react'; @@ -37,6 +37,7 @@ export interface ActionMenuProps { */ export const ActionMenu = React.memo((props: ActionMenuProps) => { const { actionsList, ariaLabel, onOpen } = props; + const theme = useTheme(); const menuId = convertToKebabCase(ariaLabel); const buttonId = `${convertToKebabCase(ariaLabel)}-button`; @@ -69,14 +70,15 @@ export const ActionMenu = React.memo((props: ActionMenuProps) => { } const sxTooltipIcon = { - '& .MuiSvgIcon-root': { - fill: '#fff', - height: '20px', - width: '20px', - }, '& :hover': { color: '#4d99f1', }, + '&& .MuiSvgIcon-root': { + fill: theme.color.disabledText, + height: '20px', + width: '20px', + }, + color: '#fff', padding: '0 0 0 8px', pointerEvents: 'all', // Allows the tooltip to be hovered on a disabled MenuItem diff --git a/packages/manager/src/components/SelectFirewallPanel/SelectFirewallPanel.tsx b/packages/manager/src/components/SelectFirewallPanel/SelectFirewallPanel.tsx index 9ef30ea4ff5..cf38ac02edb 100644 --- a/packages/manager/src/components/SelectFirewallPanel/SelectFirewallPanel.tsx +++ b/packages/manager/src/components/SelectFirewallPanel/SelectFirewallPanel.tsx @@ -13,6 +13,7 @@ import { Autocomplete } from '../Autocomplete/Autocomplete'; import { LinkButton } from '../LinkButton'; interface Props { + disabled?: boolean; entityType: FirewallDeviceEntityType | undefined; handleFirewallChange: (firewallID: number) => void; helperText: JSX.Element; @@ -21,6 +22,7 @@ interface Props { export const SelectFirewallPanel = (props: Props) => { const { + disabled, entityType, handleFirewallChange, helperText, @@ -69,6 +71,7 @@ export const SelectFirewallPanel = (props: Props) => { onChange={(_, selection) => { handleFirewallChange(selection?.value ?? -1); }} + disabled={disabled} errorText={error?.[0].reason} label="Assign Firewall" loading={isLoading} @@ -78,7 +81,7 @@ export const SelectFirewallPanel = (props: Props) => { value={selectedFirewall} /> - + Create Firewall diff --git a/packages/manager/src/features/Account/constants.ts b/packages/manager/src/features/Account/constants.ts index 27b9b590a14..8109d3748f0 100644 --- a/packages/manager/src/features/Account/constants.ts +++ b/packages/manager/src/features/Account/constants.ts @@ -1,3 +1,3 @@ export const BUSINESS_PARTNER = 'business partner'; export const ADMINISTRATOR = 'administrator'; -export const RESTRICTED_ACCESS_NOTICE = `Access restricted. Please contact your ${ADMINISTRATOR} to request the necessary permissions.`; +export const RESTRICTED_ACCESS_NOTICE = `Access restricted. Please contact your account ${ADMINISTRATOR} to request the necessary permissions.`; diff --git a/packages/manager/src/features/NodeBalancers/NodeBalancerCreate.tsx b/packages/manager/src/features/NodeBalancers/NodeBalancerCreate.tsx index bf464f0d5b7..cf3f8593d1b 100644 --- a/packages/manager/src/features/NodeBalancers/NodeBalancerCreate.tsx +++ b/packages/manager/src/features/NodeBalancers/NodeBalancerCreate.tsx @@ -30,6 +30,7 @@ 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 { RESTRICTED_ACCESS_NOTICE } from 'src/features/Account/constants'; import { useFlags } from 'src/hooks/useFlags'; import { reportAgreementSigningError, @@ -126,8 +127,11 @@ const NodeBalancerCreate = () => { const theme = useTheme(); const matchesSmDown = useMediaQuery(theme.breakpoints.down('md')); + // NodeBalancer creation is possible for restricted accounts, + // contingent on the ability to add Linodes. const disabled = - Boolean(profile?.restricted) && !grants?.global.add_nodebalancers; + Boolean(profile?.restricted) && + !(grants?.global.add_nodebalancers && grants?.global.add_linodes); const addNodeBalancer = () => { if (disabled) { @@ -464,11 +468,9 @@ const NodeBalancerCreate = () => { )} {disabled && ( )} @@ -518,6 +520,7 @@ const NodeBalancerCreate = () => { Learn more. } + disabled={disabled} entityType="nodebalancer" selectedFirewallId={nodeBalancerFields.firewall_id ?? -1} /> @@ -639,7 +642,7 @@ const NodeBalancerCreate = () => { }} buttonType="primary" data-qa-deploy-nodebalancer - disabled={showGDPRCheckbox && !hasSignedAgreement} + disabled={(showGDPRCheckbox && !hasSignedAgreement) || disabled} loading={isLoading} onClick={onCreate} > diff --git a/packages/manager/src/features/NodeBalancers/NodeBalancerDetail/NodeBalancerConfigurations.tsx b/packages/manager/src/features/NodeBalancers/NodeBalancerDetail/NodeBalancerConfigurations.tsx index 739e8c22108..6c6b635d666 100644 --- a/packages/manager/src/features/NodeBalancers/NodeBalancerDetail/NodeBalancerConfigurations.tsx +++ b/packages/manager/src/features/NodeBalancers/NodeBalancerDetail/NodeBalancerConfigurations.tsx @@ -161,7 +161,13 @@ const formatNodesStatus = (nodes: NodeBalancerConfigNodeFields[]) => { }; class NodeBalancerConfigurations extends React.Component { render() { - const { grants, nodeBalancerLabel } = this.props; + const { + grants, + match: { + params: { nodeBalancerId }, + }, + nodeBalancerLabel, + } = this.props; const { configErrors, configSubmitting, @@ -170,7 +176,13 @@ class NodeBalancerConfigurations extends React.Component { panelMessages, } = this.state; - const isRestricted = grants?.nodebalancer?.[0]?.permissions === 'read_only'; + const isRestricted = Boolean( + grants?.nodebalancer?.some( + (grant) => + grant.id === Number(nodeBalancerId) && + grant.permissions === 'read_only' + ) + ); return (
@@ -621,7 +633,12 @@ class NodeBalancerConfigurations extends React.Component { }, idx: number ) => { - const { grants } = this.props; + const { + grants, + match: { + params: { nodeBalancerId }, + }, + } = this.props; const isNewConfig = this.state.hasUnsavedConfig && idx === this.state.configs.length - 1; const { panelNodeMessages } = this.state; @@ -634,7 +651,13 @@ class NodeBalancerConfigurations extends React.Component { ? parseInt(expandedConfigId, 10) === config.id : false; - const isRestricted = grants?.nodebalancer?.[0]?.permissions === 'read_only'; + const isRestricted = Boolean( + grants?.nodebalancer?.some( + (grant) => + grant.id === Number(nodeBalancerId) && + grant.permissions === 'read_only' + ) + ); const L = { algorithmLens: lensTo(['algorithm']), diff --git a/packages/manager/src/features/NodeBalancers/NodeBalancerDetail/NodeBalancerDetail.tsx b/packages/manager/src/features/NodeBalancers/NodeBalancerDetail/NodeBalancerDetail.tsx index 694bd091822..f31c1bf25ee 100644 --- a/packages/manager/src/features/NodeBalancers/NodeBalancerDetail/NodeBalancerDetail.tsx +++ b/packages/manager/src/features/NodeBalancers/NodeBalancerDetail/NodeBalancerDetail.tsx @@ -15,6 +15,7 @@ import { TabLinkList } from 'src/components/Tabs/TabLinkList'; import { TabPanels } from 'src/components/Tabs/TabPanels'; import { Tabs } from 'src/components/Tabs/Tabs'; import { RESTRICTED_ACCESS_NOTICE } from 'src/features/Account/constants'; +import { useFeatureRestriction } from 'src/hooks/useFeatureRestriction'; import { useNodeBalancerQuery, useNodebalancerUpdateMutation, @@ -41,7 +42,10 @@ export const NodeBalancerDetail = () => { const { data: nodebalancer, error, isLoading } = useNodeBalancerQuery(id); - const isRestricted = grants?.nodebalancer?.[0]?.permissions === 'read_only'; + const isRestricted = useFeatureRestriction({ + grantType: 'nodebalancer', + id: nodebalancer?.id, + }); React.useEffect(() => { if (label !== nodebalancer?.label) { @@ -113,7 +117,7 @@ export const NodeBalancerDetail = () => { /> {errorMap.none && } {isRestricted && ( - + )}
diff --git a/packages/manager/src/features/NodeBalancers/NodeBalancersLanding/NodeBalancersLandingEmptyState.tsx b/packages/manager/src/features/NodeBalancers/NodeBalancersLanding/NodeBalancersLandingEmptyState.tsx index 077c6e6f3fd..f4b605c6658 100644 --- a/packages/manager/src/features/NodeBalancers/NodeBalancersLanding/NodeBalancersLandingEmptyState.tsx +++ b/packages/manager/src/features/NodeBalancers/NodeBalancersLanding/NodeBalancersLandingEmptyState.tsx @@ -8,7 +8,7 @@ import { Link } from 'src/components/Link'; import { Notice } from 'src/components/Notice/Notice'; import { Placeholder } from 'src/components/Placeholder/Placeholder'; import { Typography } from 'src/components/Typography'; -import { RESTRICTED_ACCESS_NOTICE } from 'src/features/Account/constants'; +import { getRestrictedResourceText } from 'src/features/Account/utils'; import { useGrants, useProfile } from 'src/queries/profile'; export const NodeBalancerLandingEmptyState = () => { @@ -26,7 +26,11 @@ export const NodeBalancerLandingEmptyState = () => { {isRestricted && ( - + )} void; @@ -630,7 +628,7 @@ class UserPermissions extends React.Component { {this.state.tabs?.map((entity) => ( - {entityNameMap[entity]} + {grantTypeMap[entity]} ))} diff --git a/packages/manager/src/features/Users/UserPermissionsEntitySection.tsx b/packages/manager/src/features/Users/UserPermissionsEntitySection.tsx index a08737298fd..78d66dc3c3e 100644 --- a/packages/manager/src/features/Users/UserPermissionsEntitySection.tsx +++ b/packages/manager/src/features/Users/UserPermissionsEntitySection.tsx @@ -21,7 +21,8 @@ import { createDisplayPage } from 'src/components/Paginate'; import { Typography } from 'src/components/Typography'; import { useTheme } from '@mui/material/styles'; import { StyledGrantsTable } from './UserPermissionsEntitySection.styles'; -import {grantTypeMap} from 'src/features/Account/constants' +import { grantTypeMap } from 'src/features/Account/constants'; + interface Props { entity: GrantType; grants: Grant[] | undefined; From 87e34e1d165afd1dc2175c52fa0b431be11e49a3 Mon Sep 17 00:00:00 2001 From: Jaalah Ramos Date: Tue, 23 Jan 2024 13:56:58 -0500 Subject: [PATCH 06/13] Cleanup class method --- .../NodeBalancerConfigurations.tsx | 42 +++++++------------ 1 file changed, 15 insertions(+), 27 deletions(-) diff --git a/packages/manager/src/features/NodeBalancers/NodeBalancerDetail/NodeBalancerConfigurations.tsx b/packages/manager/src/features/NodeBalancers/NodeBalancerDetail/NodeBalancerConfigurations.tsx index 6c6b635d666..bb2f5599c33 100644 --- a/packages/manager/src/features/NodeBalancers/NodeBalancerDetail/NodeBalancerConfigurations.tsx +++ b/packages/manager/src/features/NodeBalancers/NodeBalancerDetail/NodeBalancerConfigurations.tsx @@ -161,13 +161,7 @@ const formatNodesStatus = (nodes: NodeBalancerConfigNodeFields[]) => { }; class NodeBalancerConfigurations extends React.Component { render() { - const { - grants, - match: { - params: { nodeBalancerId }, - }, - nodeBalancerLabel, - } = this.props; + const { nodeBalancerLabel } = this.props; const { configErrors, configSubmitting, @@ -176,13 +170,7 @@ class NodeBalancerConfigurations extends React.Component { panelMessages, } = this.state; - const isRestricted = Boolean( - grants?.nodebalancer?.some( - (grant) => - grant.id === Number(nodeBalancerId) && - grant.permissions === 'read_only' - ) - ); + const isRestricted = this.isRestrictedUser(); return (
@@ -557,6 +545,18 @@ class NodeBalancerConfigurations extends React.Component { return true; }; + isRestrictedUser = () => { + const { grants } = this.props; + const { nodeBalancerId } = this.props.match.params; + return Boolean( + grants?.nodebalancer?.some( + (grant) => + grant.id === Number(nodeBalancerId) && + grant.permissions === 'read_only' + ) + ); + }; + onCloseConfirmation = () => this.setState({ deleteConfigConfirmDialog: { @@ -633,12 +633,6 @@ class NodeBalancerConfigurations extends React.Component { }, idx: number ) => { - const { - grants, - match: { - params: { nodeBalancerId }, - }, - } = this.props; const isNewConfig = this.state.hasUnsavedConfig && idx === this.state.configs.length - 1; const { panelNodeMessages } = this.state; @@ -651,13 +645,7 @@ class NodeBalancerConfigurations extends React.Component { ? parseInt(expandedConfigId, 10) === config.id : false; - const isRestricted = Boolean( - grants?.nodebalancer?.some( - (grant) => - grant.id === Number(nodeBalancerId) && - grant.permissions === 'read_only' - ) - ); + const isRestricted = this.isRestrictedUser(); const L = { algorithmLens: lensTo(['algorithm']), From 3d98268d3c3bbc4f8de1c423fe1ce10c35a148ef Mon Sep 17 00:00:00 2001 From: Jaalah Ramos Date: Tue, 23 Jan 2024 14:05:24 -0500 Subject: [PATCH 07/13] Renamed hook --- .../NodeBalancerDetail/NodeBalancerDetail.tsx | 4 ++-- .../NodeBalancerDetail/NodeBalancerSettings.tsx | 11 +++++++---- .../NodeBalancerSummary/SummaryPanel.tsx | 9 ++++++--- .../NodeBalancersLanding/NodeBalancerActionMenu.tsx | 4 ++-- ...eatureRestricted.ts => useIsResourceRestricted.ts} | 2 +- 5 files changed, 18 insertions(+), 12 deletions(-) rename packages/manager/src/hooks/{useIsFeatureRestricted.ts => useIsResourceRestricted.ts} (96%) diff --git a/packages/manager/src/features/NodeBalancers/NodeBalancerDetail/NodeBalancerDetail.tsx b/packages/manager/src/features/NodeBalancers/NodeBalancerDetail/NodeBalancerDetail.tsx index 05b50924aff..62caeb83734 100644 --- a/packages/manager/src/features/NodeBalancers/NodeBalancerDetail/NodeBalancerDetail.tsx +++ b/packages/manager/src/features/NodeBalancers/NodeBalancerDetail/NodeBalancerDetail.tsx @@ -15,7 +15,7 @@ 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 { useIsFeatureRestricted } from 'src/hooks/useIsFeatureRestricted'; +import { useIsResourceRestricted } from 'src/hooks/useIsResourceRestricted'; import { useNodeBalancerQuery, useNodebalancerUpdateMutation, @@ -42,7 +42,7 @@ export const NodeBalancerDetail = () => { const { data: nodebalancer, error, isLoading } = useNodeBalancerQuery(id); - const isRestricted = useIsFeatureRestricted({ + const isRestricted = useIsResourceRestricted({ grantType: 'nodebalancer', id: nodebalancer?.id, }); diff --git a/packages/manager/src/features/NodeBalancers/NodeBalancerDetail/NodeBalancerSettings.tsx b/packages/manager/src/features/NodeBalancers/NodeBalancerDetail/NodeBalancerSettings.tsx index b46d68c3513..3be62c10122 100644 --- a/packages/manager/src/features/NodeBalancers/NodeBalancerDetail/NodeBalancerSettings.tsx +++ b/packages/manager/src/features/NodeBalancers/NodeBalancerDetail/NodeBalancerSettings.tsx @@ -9,18 +9,17 @@ import { FormHelperText } from 'src/components/FormHelperText'; import { InputAdornment } from 'src/components/InputAdornment'; import { TextField } from 'src/components/TextField'; import { useFlags } from 'src/hooks/useFlags'; +import { useIsResourceRestricted } from 'src/hooks/useIsResourceRestricted'; +import { useNodeBalancersFirewallsQuery } from 'src/queries/nodebalancers'; import { useNodeBalancerQuery, useNodebalancerUpdateMutation, } from 'src/queries/nodebalancers'; -import { useNodeBalancersFirewallsQuery } from 'src/queries/nodebalancers'; -import { useGrants } from 'src/queries/profile'; import { NodeBalancerDeleteDialog } from '../NodeBalancerDeleteDialog'; import { NodeBalancerFirewalls } from './NodeBalancerFirewalls'; export const NodeBalancerSettings = () => { - const { data: grants } = useGrants(); const flags = useFlags(); const theme = useTheme(); const { nodeBalancerId } = useParams<{ nodeBalancerId: string }>(); @@ -28,7 +27,11 @@ export const NodeBalancerSettings = () => { const { data: nodebalancer } = useNodeBalancerQuery(id); const { data: attachedFirewallData } = useNodeBalancersFirewallsQuery(id); const displayFirewallInfoText = attachedFirewallData?.results === 0; - const isRestricted = grants?.nodebalancer?.[0]?.permissions === 'read_only'; + + const isRestricted = useIsResourceRestricted({ + grantType: 'nodebalancer', + id: nodebalancer?.id, + }); const { error: labelError, diff --git a/packages/manager/src/features/NodeBalancers/NodeBalancerDetail/NodeBalancerSummary/SummaryPanel.tsx b/packages/manager/src/features/NodeBalancers/NodeBalancerDetail/NodeBalancerSummary/SummaryPanel.tsx index e2552ff04b4..7f3423a5df5 100644 --- a/packages/manager/src/features/NodeBalancers/NodeBalancerDetail/NodeBalancerSummary/SummaryPanel.tsx +++ b/packages/manager/src/features/NodeBalancers/NodeBalancerDetail/NodeBalancerSummary/SummaryPanel.tsx @@ -7,18 +7,17 @@ import { TagsPanel } from 'src/components/TagsPanel/TagsPanel'; import { Typography } from 'src/components/Typography'; import { IPAddress } from 'src/features/Linodes/LinodesLanding/IPAddress'; import { useFlags } from 'src/hooks/useFlags'; +import { useIsResourceRestricted } from 'src/hooks/useIsResourceRestricted'; import { useNodeBalancersFirewallsQuery } from 'src/queries/nodebalancers'; import { useAllNodeBalancerConfigsQuery, useNodeBalancerQuery, useNodebalancerUpdateMutation, } from 'src/queries/nodebalancers'; -import { useGrants } from 'src/queries/profile'; import { useRegionsQuery } from 'src/queries/regions'; import { convertMegabytesTo } from 'src/utilities/unitConversions'; export const SummaryPanel = () => { - const { data: grants } = useGrants(); const flags = useFlags(); const { nodeBalancerId } = useParams<{ nodeBalancerId: string }>(); const id = Number(nodeBalancerId); @@ -31,7 +30,11 @@ export const SummaryPanel = () => { const region = regions?.find((r) => r.id === nodebalancer?.region); const { mutateAsync: updateNodeBalancer } = useNodebalancerUpdateMutation(id); const displayFirewallLink = !!attachedFirewallData?.data?.length; - const isRestricted = grants?.nodebalancer?.[0]?.permissions === 'read_only'; + + const isRestricted = useIsResourceRestricted({ + grantType: 'nodebalancer', + id: nodebalancer?.id, + }); const configPorts = configs?.reduce((acc, config) => { return [...acc, { configId: config.id, port: config.port }]; diff --git a/packages/manager/src/features/NodeBalancers/NodeBalancersLanding/NodeBalancerActionMenu.tsx b/packages/manager/src/features/NodeBalancers/NodeBalancersLanding/NodeBalancerActionMenu.tsx index b8cfa84995b..f1b0c045a67 100644 --- a/packages/manager/src/features/NodeBalancers/NodeBalancersLanding/NodeBalancerActionMenu.tsx +++ b/packages/manager/src/features/NodeBalancers/NodeBalancersLanding/NodeBalancerActionMenu.tsx @@ -7,7 +7,7 @@ import { Action, ActionMenu } from 'src/components/ActionMenu/ActionMenu'; import { Hidden } from 'src/components/Hidden'; import { InlineMenuAction } from 'src/components/InlineMenuAction/InlineMenuAction'; import { getRestrictedResourceText } from 'src/features/Account/utils'; -import { useIsFeatureRestricted } from 'src/hooks/useIsFeatureRestricted'; +import { useIsResourceRestricted } from 'src/hooks/useIsResourceRestricted'; interface Props { label: string; @@ -21,7 +21,7 @@ export const NodeBalancerActionMenu = (props: Props) => { const history = useHistory(); const { label, nodeBalancerId, toggleDialog } = props; - const isRestricted = useIsFeatureRestricted({ + const isRestricted = useIsResourceRestricted({ grantType: 'nodebalancer', id: nodeBalancerId, }); diff --git a/packages/manager/src/hooks/useIsFeatureRestricted.ts b/packages/manager/src/hooks/useIsResourceRestricted.ts similarity index 96% rename from packages/manager/src/hooks/useIsFeatureRestricted.ts rename to packages/manager/src/hooks/useIsResourceRestricted.ts index fdada4a3789..5455417d288 100644 --- a/packages/manager/src/hooks/useIsFeatureRestricted.ts +++ b/packages/manager/src/hooks/useIsResourceRestricted.ts @@ -8,7 +8,7 @@ import type { Grant, GrantLevel, GrantType, Grants } from '@linode/api-v4'; * This hook verifies if a user with restricted access can edit specific cloud resources (Linodes, NodeBalancers, Volumes, etc.). * Admin-created resources may be restricted, but those created by the restricted user are editable. */ -export const useIsFeatureRestricted = ({ +export const useIsResourceRestricted = ({ grantLevel = 'read_only', grantType, id, From 87a945196d5f33af08046a3b06e6cad42a5475f0 Mon Sep 17 00:00:00 2001 From: Jaalah Ramos Date: Tue, 23 Jan 2024 23:08:26 -0500 Subject: [PATCH 08/13] Added changeset: Improve NodeBalancer Restricted User Experience --- .../.changeset/pr-10095-upcoming-features-1706069306820.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 packages/manager/.changeset/pr-10095-upcoming-features-1706069306820.md diff --git a/packages/manager/.changeset/pr-10095-upcoming-features-1706069306820.md b/packages/manager/.changeset/pr-10095-upcoming-features-1706069306820.md new file mode 100644 index 00000000000..22763d3cd2a --- /dev/null +++ b/packages/manager/.changeset/pr-10095-upcoming-features-1706069306820.md @@ -0,0 +1,5 @@ +--- +"@linode/manager": Upcoming Features +--- + +Improve NodeBalancer Restricted User Experience ([#10095](https://github.com/linode/manager/pull/10095)) From ed924419a6ed5f1c03a973dad3c3c3f09409a70d Mon Sep 17 00:00:00 2001 From: Jaalah Ramos Date: Tue, 23 Jan 2024 23:13:15 -0500 Subject: [PATCH 09/13] Update changeset --- ...-1706069306820.md => pr-10095-tech-stories-1706069306820.md} | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) rename packages/manager/.changeset/{pr-10095-upcoming-features-1706069306820.md => pr-10095-tech-stories-1706069306820.md} (75%) diff --git a/packages/manager/.changeset/pr-10095-upcoming-features-1706069306820.md b/packages/manager/.changeset/pr-10095-tech-stories-1706069306820.md similarity index 75% rename from packages/manager/.changeset/pr-10095-upcoming-features-1706069306820.md rename to packages/manager/.changeset/pr-10095-tech-stories-1706069306820.md index 22763d3cd2a..b9a124642db 100644 --- a/packages/manager/.changeset/pr-10095-upcoming-features-1706069306820.md +++ b/packages/manager/.changeset/pr-10095-tech-stories-1706069306820.md @@ -1,5 +1,5 @@ --- -"@linode/manager": Upcoming Features +"@linode/manager": Tech Stories --- Improve NodeBalancer Restricted User Experience ([#10095](https://github.com/linode/manager/pull/10095)) From 35bf9f94abc0a8e5bb63b2e51e2a6ace7f6ec341 Mon Sep 17 00:00:00 2001 From: Jaalah Ramos Date: Fri, 26 Jan 2024 09:58:44 -0500 Subject: [PATCH 10/13] Update UX- remove banners from create/landing --- packages/manager/src/features/Account/utils.ts | 13 +++++++++++-- .../features/NodeBalancers/NodeBalancerCreate.tsx | 5 ++++- .../NodeBalancerDetail/NodeBalancerDetail.tsx | 4 +++- .../NodeBalancerActionMenu.tsx | 6 +++++- .../NodeBalancersLanding/NodeBalancersLanding.tsx | 14 ++++++-------- .../NodeBalancersLandingEmptyState.tsx | 12 ++++-------- 6 files changed, 33 insertions(+), 21 deletions(-) diff --git a/packages/manager/src/features/Account/utils.ts b/packages/manager/src/features/Account/utils.ts index 4f09bfc2edd..79c5afe0f71 100644 --- a/packages/manager/src/features/Account/utils.ts +++ b/packages/manager/src/features/Account/utils.ts @@ -3,8 +3,17 @@ import { getStorage, setStorage } from 'src/utilities/storage'; import type { Token } from '@linode/api-v4'; import type { GrantTypeMap } from 'src/features/Account/types'; -export const getRestrictedResourceText = (resourceType: GrantTypeMap) => { - return `Access restricted for ${resourceType}. Please contact your account administrator to request the necessary permissions.`; +type ActionType = 'create' | 'delete' | 'edit' | 'view'; + +interface GetRestrictedResourceText { + action?: ActionType; + resourceType: GrantTypeMap; +} +export const getRestrictedResourceText = ({ + action = 'edit', + resourceType, +}: GetRestrictedResourceText) => { + return `You don't have permissions to ${action} ${resourceType}. Please contact your account administrator to request the necessary permissions.`; }; // 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 3d24f1ff32f..5e2989d8856 100644 --- a/packages/manager/src/features/NodeBalancers/NodeBalancerCreate.tsx +++ b/packages/manager/src/features/NodeBalancers/NodeBalancerCreate.tsx @@ -468,9 +468,12 @@ const NodeBalancerCreate = () => { )} {disabled && ( )} diff --git a/packages/manager/src/features/NodeBalancers/NodeBalancerDetail/NodeBalancerDetail.tsx b/packages/manager/src/features/NodeBalancers/NodeBalancerDetail/NodeBalancerDetail.tsx index 62caeb83734..3c681818a7a 100644 --- a/packages/manager/src/features/NodeBalancers/NodeBalancerDetail/NodeBalancerDetail.tsx +++ b/packages/manager/src/features/NodeBalancers/NodeBalancerDetail/NodeBalancerDetail.tsx @@ -118,8 +118,10 @@ export const NodeBalancerDetail = () => { {errorMap.none && } {isRestricted && ( )} diff --git a/packages/manager/src/features/NodeBalancers/NodeBalancersLanding/NodeBalancerActionMenu.tsx b/packages/manager/src/features/NodeBalancers/NodeBalancersLanding/NodeBalancerActionMenu.tsx index f1b0c045a67..e2e725ed1a0 100644 --- a/packages/manager/src/features/NodeBalancers/NodeBalancersLanding/NodeBalancerActionMenu.tsx +++ b/packages/manager/src/features/NodeBalancers/NodeBalancersLanding/NodeBalancerActionMenu.tsx @@ -46,7 +46,10 @@ export const NodeBalancerActionMenu = (props: Props) => { }, title: 'Delete', tooltip: isRestricted - ? getRestrictedResourceText('NodeBalancers') + ? getRestrictedResourceText({ + action: 'delete', + resourceType: 'NodeBalancers', + }) : undefined, }, ]; @@ -61,6 +64,7 @@ export const NodeBalancerActionMenu = (props: Props) => { disabled={action.disabled} key={action.title} onClick={action.onClick} + tooltip={action.tooltip} /> ); })} diff --git a/packages/manager/src/features/NodeBalancers/NodeBalancersLanding/NodeBalancersLanding.tsx b/packages/manager/src/features/NodeBalancers/NodeBalancersLanding/NodeBalancersLanding.tsx index 157e9f7130b..09e5d16065e 100644 --- a/packages/manager/src/features/NodeBalancers/NodeBalancersLanding/NodeBalancersLanding.tsx +++ b/packages/manager/src/features/NodeBalancers/NodeBalancersLanding/NodeBalancersLanding.tsx @@ -6,7 +6,6 @@ import { DocumentTitleSegment } from 'src/components/DocumentTitle'; import { ErrorState } from 'src/components/ErrorState/ErrorState'; import { Hidden } from 'src/components/Hidden'; import { LandingHeader } from 'src/components/LandingHeader'; -import { Notice } from 'src/components/Notice/Notice'; import { PaginationFooter } from 'src/components/PaginationFooter/PaginationFooter'; import { Table } from 'src/components/Table'; import { TableBody } from 'src/components/TableBody'; @@ -96,19 +95,18 @@ export const NodeBalancersLanding = () => { <> history.push('/nodebalancers/create')} title="NodeBalancers" /> - {isRestricted && ( - - )}
diff --git a/packages/manager/src/features/NodeBalancers/NodeBalancersLanding/NodeBalancersLandingEmptyState.tsx b/packages/manager/src/features/NodeBalancers/NodeBalancersLanding/NodeBalancersLandingEmptyState.tsx index f4b605c6658..71f0433274f 100644 --- a/packages/manager/src/features/NodeBalancers/NodeBalancersLanding/NodeBalancersLandingEmptyState.tsx +++ b/packages/manager/src/features/NodeBalancers/NodeBalancersLanding/NodeBalancersLandingEmptyState.tsx @@ -5,7 +5,6 @@ import { useHistory } from 'react-router-dom'; import NodeBalancer from 'src/assets/icons/entityIcons/nodebalancer.svg'; import { DocumentTitleSegment } from 'src/components/DocumentTitle'; import { Link } from 'src/components/Link'; -import { Notice } from 'src/components/Notice/Notice'; import { Placeholder } from 'src/components/Placeholder/Placeholder'; import { Typography } from 'src/components/Typography'; import { getRestrictedResourceText } from 'src/features/Account/utils'; @@ -25,19 +24,16 @@ export const NodeBalancerLandingEmptyState = () => { return ( - {isRestricted && ( - - )} history.push('/nodebalancers/create'), + tooltipText: getRestrictedResourceText({ + action: 'create', + resourceType: 'NodeBalancers', + }), }, ]} icon={NodeBalancer} From c6e93b64b31f4e20e4b598e1223dafa7f2893c4a Mon Sep 17 00:00:00 2001 From: Jaalah Ramos Date: Tue, 30 Jan 2024 11:26:30 -0500 Subject: [PATCH 11/13] Review updates --- .../Button/StyledTagButton.stories.tsx | 37 +++++++++++++++++++ .../src/components/Button/StyledTagButton.ts | 10 ++--- .../manager/src/features/Account/utils.ts | 22 ++++++++++- .../NodeBalancers/NodeBalancerCreate.tsx | 35 ++++++++++-------- .../NodeBalancerConfigurations.tsx | 10 ++--- .../NodeBalancerDetail/NodeBalancerDetail.tsx | 5 ++- .../NodeBalancerSettings.tsx | 11 +++--- .../NodeBalancerSummary/SummaryPanel.tsx | 5 ++- .../NodeBalancerActionMenu.tsx | 8 ++-- .../NodeBalancersLanding.tsx | 16 +++++--- .../NodeBalancersLandingEmptyState.tsx | 16 +++++--- .../src/hooks/useIsResourceRestricted.ts | 2 +- 12 files changed, 124 insertions(+), 53 deletions(-) create mode 100644 packages/manager/src/components/Button/StyledTagButton.stories.tsx diff --git a/packages/manager/src/components/Button/StyledTagButton.stories.tsx b/packages/manager/src/components/Button/StyledTagButton.stories.tsx new file mode 100644 index 00000000000..843fb1f4bd9 --- /dev/null +++ b/packages/manager/src/components/Button/StyledTagButton.stories.tsx @@ -0,0 +1,37 @@ +import { action } from '@storybook/addon-actions'; +import React from 'react'; + +import { StyledPlusIcon, StyledTagButton } from './StyledTagButton'; + +import type { Meta, StoryObj } from '@storybook/react'; + +const meta: Meta = { + args: { + children: 'Tag', + disabled: false, + onClick: () => null, + }, + component: StyledTagButton, + title: 'Components/TagButton', +}; + +export default meta; + +type Story = StoryObj; + +export const Default: Story = { + args: { + buttonType: 'outlined', + children: 'Tag', + disabled: false, + onClick: action('onClick'), + }, + render: (args) => ( + } + > + Add a Tag + + ), +}; diff --git a/packages/manager/src/components/Button/StyledTagButton.ts b/packages/manager/src/components/Button/StyledTagButton.ts index 9b0502c3538..a35a6ef302a 100644 --- a/packages/manager/src/components/Button/StyledTagButton.ts +++ b/packages/manager/src/components/Button/StyledTagButton.ts @@ -13,16 +13,16 @@ import { Button } from './Button'; export const StyledTagButton = styled(Button, { label: 'StyledTagButton', })(({ theme, ...props }) => ({ + border: 'none', + fontSize: '0.875rem', ...(!props.disabled && { - '&:hover': { + '&:hover, &:focus': { backgroundColor: theme.color.tagButton, border: 'none', }, + backgroundColor: theme.color.tagButton, + color: theme.textColors.linkActiveLight, }), - backgroundColor: theme.color.tagButton, - border: 'none', - color: theme.textColors.linkActiveLight, - fontSize: '0.875rem', })); export const StyledPlusIcon = styled(Plus, { diff --git a/packages/manager/src/features/Account/utils.ts b/packages/manager/src/features/Account/utils.ts index 79c5afe0f71..0275c2239cf 100644 --- a/packages/manager/src/features/Account/utils.ts +++ b/packages/manager/src/features/Account/utils.ts @@ -1,19 +1,37 @@ import { getStorage, setStorage } from 'src/utilities/storage'; -import type { Token } from '@linode/api-v4'; +import type { Grants, Profile, Token, GlobalGrantTypes } 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) => { - return `You don't have permissions to ${action} ${resourceType}. Please contact your account administrator to request the necessary permissions.`; + 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; +}) => { + 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 5e2989d8856..99711c88d69 100644 --- a/packages/manager/src/features/NodeBalancers/NodeBalancerCreate.tsx +++ b/packages/manager/src/features/NodeBalancers/NodeBalancerCreate.tsx @@ -30,7 +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 } from 'src/features/Account/utils'; +import { + getRestrictedResourceText, + isRestrictedGlobalGrantType, +} from 'src/features/Account/utils'; import { useFlags } from 'src/hooks/useFlags'; import { reportAgreementSigningError, @@ -127,14 +130,14 @@ const NodeBalancerCreate = () => { const theme = useTheme(); const matchesSmDown = useMediaQuery(theme.breakpoints.down('md')); - // NodeBalancer creation is possible for restricted accounts, - // contingent on the ability to add Linodes. - const disabled = - Boolean(profile?.restricted) && - !(grants?.global.add_nodebalancers && grants?.global.add_linodes); + const isRestricted = isRestrictedGlobalGrantType({ + globalGrantType: 'add_nodebalancers', + grants, + profile, + }); const addNodeBalancer = () => { - if (disabled) { + if (isRestricted) { return; } setNodeBalancerFields((prev) => ({ @@ -461,12 +464,12 @@ const NodeBalancerCreate = () => { }} title="Create" /> - {generalError && !disabled && ( + {generalError && !isRestricted && ( {generalError} )} - {disabled && ( + {isRestricted && ( { )} { })) : [] } - disabled={disabled} + disabled={isRestricted} onChange={tagsChange} tagError={hasErrorFor('tags')} /> { Learn more. } - disabled={disabled} + disabled={isRestricted} entityType="nodebalancer" selectedFirewallId={nodeBalancerFields.firewall_id ?? -1} /> @@ -581,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} @@ -613,7 +616,7 @@ const NodeBalancerCreate = () => {