diff --git a/packages/manager/.changeset/pr-13165-upcoming-features-1764951969980.md b/packages/manager/.changeset/pr-13165-upcoming-features-1764951969980.md new file mode 100644 index 00000000000..ae7b46a0c05 --- /dev/null +++ b/packages/manager/.changeset/pr-13165-upcoming-features-1764951969980.md @@ -0,0 +1,5 @@ +--- +"@linode/manager": Upcoming Features +--- + +UX/UI enhancements for RuleSets and Prefix Lists ([#13165](https://github.com/linode/manager/pull/13165)) diff --git a/packages/manager/src/features/Firewalls/FirewallDetail/Rules/FirewallPrefixListDrawer.tsx b/packages/manager/src/features/Firewalls/FirewallDetail/Rules/FirewallPrefixListDrawer.tsx index 083634c3063..845b5486e63 100644 --- a/packages/manager/src/features/Firewalls/FirewallDetail/Rules/FirewallPrefixListDrawer.tsx +++ b/packages/manager/src/features/Firewalls/FirewallDetail/Rules/FirewallPrefixListDrawer.tsx @@ -1,5 +1,13 @@ import { useAllFirewallPrefixListsQuery } from '@linode/queries'; -import { Box, Button, Chip, Drawer, Paper, TooltipIcon } from '@linode/ui'; +import { + Box, + Button, + Chip, + Drawer, + Paper, + Stack, + TooltipIcon, +} from '@linode/ui'; import { capitalize } from '@linode/utilities'; import * as React from 'react'; @@ -225,140 +233,144 @@ export const FirewallPrefixListDrawer = React.memo( )} - {isIPv4Supported && ( - ({ - backgroundColor: theme.tokens.alias.Background.Neutral, - padding: theme.spacingFunction(12), - marginTop: theme.spacingFunction(8), - ...(isIPv4InUse - ? { - border: `1px solid ${theme.tokens.alias.Border.Positive}`, - } - : {}), - })} - > - + {isIPv4Supported && ( + ({ - display: 'flex', - justifyContent: 'space-between', - marginBottom: theme.spacingFunction(4), - ...(!isIPv4InUse + backgroundColor: theme.tokens.alias.Background.Neutral, + padding: theme.spacingFunction(12), + ...(isIPv4InUse ? { - color: - theme.tokens.alias.Content.Text.Primary.Disabled, + border: `1px solid ${theme.tokens.alias.Border.Positive}`, } : {}), })} > - IPv4 - ({ - background: isIPv4InUse - ? theme.tokens.component.Badge.Positive.Subtle - .Background - : theme.tokens.component.Badge.Neutral.Subtle - .Background, - color: isIPv4InUse - ? theme.tokens.component.Badge.Positive.Subtle.Text - : theme.tokens.component.Badge.Neutral.Subtle.Text, - font: theme.font.bold, - fontSize: theme.tokens.font.FontSize.Xxxs, - marginRight: theme.spacingFunction(6), - flexShrink: 0, + display: 'flex', + justifyContent: 'space-between', + marginBottom: theme.spacingFunction(4), + ...(!isIPv4InUse + ? { + color: + theme.tokens.alias.Content.Text.Primary + .Disabled, + } + : {}), })} - /> - + > + IPv4 + ({ + background: isIPv4InUse + ? theme.tokens.component.Badge.Positive.Subtle + .Background + : theme.tokens.component.Badge.Neutral.Subtle + .Background, + color: isIPv4InUse + ? theme.tokens.component.Badge.Positive.Subtle.Text + : theme.tokens.component.Badge.Neutral.Subtle.Text, + font: theme.font.bold, + fontSize: theme.tokens.font.FontSize.Xxxs, + marginRight: theme.spacingFunction(6), + flexShrink: 0, + })} + /> + - ({ - ...(!isIPv4InUse - ? { - color: - theme.tokens.alias.Content.Text.Primary.Disabled, - } - : {}), - })} - > - {prefixListDetails.ipv4!.length > 0 ? ( - prefixListDetails.ipv4!.join(', ') - ) : ( - no IP addresses - )} - - - )} + ({ + ...(!isIPv4InUse + ? { + color: + theme.tokens.alias.Content.Text.Primary + .Disabled, + } + : {}), + })} + > + {prefixListDetails.ipv4!.length > 0 ? ( + prefixListDetails.ipv4!.join(', ') + ) : ( + no IP addresses + )} + + + )} - {isIPv6Supported && ( - ({ - backgroundColor: theme.tokens.alias.Background.Neutral, - padding: theme.spacingFunction(12), - marginTop: theme.spacingFunction(8), - ...(isIPv6InUse - ? { - border: `1px solid ${theme.tokens.alias.Border.Positive}`, - } - : {}), - })} - > - ({ - display: 'flex', - justifyContent: 'space-between', - marginBottom: theme.spacingFunction(4), - ...(!isIPv6InUse + backgroundColor: theme.tokens.alias.Background.Neutral, + padding: theme.spacingFunction(12), + ...(isIPv6InUse ? { - color: - theme.tokens.alias.Content.Text.Primary.Disabled, + border: `1px solid ${theme.tokens.alias.Border.Positive}`, } : {}), })} > - IPv6 - ({ - background: isIPv6InUse - ? theme.tokens.component.Badge.Positive.Subtle - .Background - : theme.tokens.component.Badge.Neutral.Subtle - .Background, - color: isIPv6InUse - ? theme.tokens.component.Badge.Positive.Subtle.Text - : theme.tokens.component.Badge.Neutral.Subtle.Text, - font: theme.font.bold, - fontSize: theme.tokens.font.FontSize.Xxxs, - marginRight: theme.spacingFunction(6), - flexShrink: 0, + display: 'flex', + justifyContent: 'space-between', + marginBottom: theme.spacingFunction(4), + ...(!isIPv6InUse + ? { + color: + theme.tokens.alias.Content.Text.Primary + .Disabled, + } + : {}), })} - /> - - ({ - ...(!isIPv6InUse - ? { - color: - theme.tokens.alias.Content.Text.Primary.Disabled, - } - : {}), - })} - > - {prefixListDetails.ipv6!.length > 0 ? ( - prefixListDetails.ipv6!.join(', ') - ) : ( - no IP addresses - )} - - - )} + > + IPv6 + ({ + background: isIPv6InUse + ? theme.tokens.component.Badge.Positive.Subtle + .Background + : theme.tokens.component.Badge.Neutral.Subtle + .Background, + color: isIPv6InUse + ? theme.tokens.component.Badge.Positive.Subtle.Text + : theme.tokens.component.Badge.Neutral.Subtle.Text, + font: theme.font.bold, + fontSize: theme.tokens.font.FontSize.Xxxs, + marginRight: theme.spacingFunction(6), + flexShrink: 0, + })} + /> + + ({ + ...(!isIPv6InUse + ? { + color: + theme.tokens.alias.Content.Text.Primary + .Disabled, + } + : {}), + })} + > + {prefixListDetails.ipv6!.length > 0 ? ( + prefixListDetails.ipv6!.join(', ') + ) : ( + no IP addresses + )} + + + )} + )} diff --git a/packages/manager/src/features/Firewalls/FirewallDetail/Rules/FirewallRuleForm.tsx b/packages/manager/src/features/Firewalls/FirewallDetail/Rules/FirewallRuleForm.tsx index ef26ae33514..401e0b63459 100644 --- a/packages/manager/src/features/Firewalls/FirewallDetail/Rules/FirewallRuleForm.tsx +++ b/packages/manager/src/features/Firewalls/FirewallDetail/Rules/FirewallRuleForm.tsx @@ -347,7 +347,7 @@ export const FirewallRuleForm = React.memo((props: FirewallRuleFormProps) => { tooltip={ipNetmaskTooltipText} /> {isFirewallRulesetsPrefixlistsFeatureEnabled && ( - ({ ...(ips.length !== 0 ? { marginTop: theme.spacingFunction(16) } : {}), })); + +const StyledMultiplePrefixListSelect = styled(MultiplePrefixListSelect, { + label: 'StyledMultipleIPInput', +})(({ theme, pls }) => ({ + ...(pls.length !== 0 ? { marginTop: theme.spacingFunction(16) } : {}), +})); diff --git a/packages/manager/src/features/Firewalls/FirewallDetail/Rules/FirewallRuleTable.tsx b/packages/manager/src/features/Firewalls/FirewallDetail/Rules/FirewallRuleTable.tsx index ebeff8eec5b..69a4a59f3e6 100644 --- a/packages/manager/src/features/Firewalls/FirewallDetail/Rules/FirewallRuleTable.tsx +++ b/packages/manager/src/features/Firewalls/FirewallDetail/Rules/FirewallRuleTable.tsx @@ -25,7 +25,6 @@ import { prop, uniqBy } from 'ramda'; import * as React from 'react'; import Undo from 'src/assets/icons/undo.svg'; -import { CopyTooltip } from 'src/components/CopyTooltip/CopyTooltip'; import { Link } from 'src/components/Link'; import { MaskableText } from 'src/components/MaskableText/MaskableText'; import { Table } from 'src/components/Table'; @@ -42,6 +41,7 @@ import { predefinedFirewallFromRule as ruleToPredefinedFirewall, useIsFirewallRulesetsPrefixlistsEnabled, } from 'src/features/Firewalls/shared'; +import { IPAddress } from 'src/features/Linodes/LinodesLanding/IPAddress'; import { CustomKeyboardSensor } from 'src/utilities/CustomKeyboardSensor'; import { FirewallRuleActionMenu } from './FirewallRuleActionMenu'; @@ -55,7 +55,6 @@ import { StyledTableRow, } from './FirewallRuleTable.styles'; import { sortPortString } from './shared'; -import { useStyles } from './shared.styles'; import type { FirewallRuleDrawerMode } from './FirewallRuleDrawer.types'; import type { ExtendedFirewallRule, RuleStatus } from './firewallRuleEditor'; @@ -384,17 +383,29 @@ const FirewallRuleTableRow = React.memo((props: FirewallRuleTableRowProps) => { zIndex: isDragging ? 9999 : 0, } as const; - const { classes } = useStyles(); + const [isHovered, setIsHovered] = React.useState(false); + + const handleMouseEnter = React.useCallback(() => { + setIsHovered(true); + }, []); + + const handleMouseLeave = React.useCallback(() => { + setIsHovered(false); + }, []); if (isRuleSetLoading) { return ; } + const ruleSetCopyableId = `${rulesetDetails ? 'ID:' : 'Ruleset ID:'} ${ruleset}`; + return ( { )} {isRuleSetRowEnabled && ( - <> - - - - - {rulesetDetails && ( - - handleOpenRuleSetDrawerForViewing?.(rulesetDetails.id) - } - > - {rulesetDetails?.label} - - )} - - - + + + + {rulesetDetails && ( + + handleOpenRuleSetDrawerForViewing?.(rulesetDetails.id) + } > - {rulesetDetails ? 'ID:' : 'Rule Set ID:'}  - {ruleset} - - - + {rulesetDetails?.label} + + )} - - - + + + + + )} diff --git a/packages/manager/src/features/Firewalls/FirewallDetail/Rules/MultiplePrefixListSelect.tsx b/packages/manager/src/features/Firewalls/FirewallDetail/Rules/MultiplePrefixListSelect.tsx index 67e3312427f..cb13e874b4a 100644 --- a/packages/manager/src/features/Firewalls/FirewallDetail/Rules/MultiplePrefixListSelect.tsx +++ b/packages/manager/src/features/Firewalls/FirewallDetail/Rules/MultiplePrefixListSelect.tsx @@ -32,21 +32,26 @@ const useStyles = makeStyles()((theme: Theme) => ({ justifyContent: 'flex-start', }, paddingLeft: 0, - paddingTop: theme.spacingFunction(12), + paddingTop: theme.spacingFunction(12), // default when empty + }, + addPLReducedPadding: { + paddingTop: theme.spacingFunction(4), // when last row is selected + }, + autocomplete: { + "& [data-testid='inputLabelWrapper']": { + display: 'none', + }, }, button: { '& > span': { padding: 2, }, + marginTop: theme.spacingFunction(8), marginLeft: `-${theme.spacingFunction(8)}`, - marginTop: 4, - minHeight: 'auto', - minWidth: 'auto', + height: 20, + width: 20, padding: 0, }, - root: { - marginTop: theme.spacingFunction(8), - }, })); const isPrefixListSupported = (pl: FirewallPrefixList) => @@ -216,6 +221,9 @@ export const MultiplePrefixListSelect = React.memo( return null; } + const lastRowSelected = + pls.length > 0 && pls[pls.length - 1].address !== ''; + const renderRow = (thisPL: ExtendedPL, idx: number) => { const availableOptions = getAvailableOptions(idx, thisPL.address); @@ -237,6 +245,19 @@ export const MultiplePrefixListSelect = React.memo( const disableIPv4 = ipv4Unsupported || ipv4Forced; const disableIPv6 = ipv6Unsupported || ipv6Forced; + const getCheckboxTooltipText = ( + ipUnsupported?: boolean, + ipForced?: boolean + ) => { + if (ipUnsupported) { + return 'Not supported by this Prefix List'; + } + if (ipForced) { + return 'At least one array must be selected'; + } + return undefined; + }; + return ( 0} disabled={disabled} errorText={thisPL.error} @@ -274,20 +296,32 @@ export const MultiplePrefixListSelect = React.memo( sx={{ ml: 0.4 }} > - handleToggleIPv4(!thisPL.inIPv4Rule, idx)} - text="IPv4" - /> - handleToggleIPv6(!thisPL.inIPv6Rule, idx)} - text="IPv6" - /> + + handleToggleIPv4(!thisPL.inIPv4Rule, idx)} + text="IPv4" + toolTipText={getCheckboxTooltipText( + ipv4Unsupported, + ipv4Forced + )} + /> + + + handleToggleIPv6(!thisPL.inIPv6Rule, idx)} + text="IPv6" + toolTipText={getCheckboxTooltipText( + ipv6Unsupported, + ipv6Forced + )} + /> + removeInput(idx)} - sx={(theme) => ({ - height: 20, - width: 20, - marginTop: `${theme.spacingFunction(16)} !important`, - })} > @@ -325,11 +354,11 @@ export const MultiplePrefixListSelect = React.memo( }; return ( -
+
{/* Display the title only when pls.length > 0 (i.e., at least one PL row is added) */} {pls.length > 0 && ( - Prefix List + Prefix List {getFeatureChip({ isFirewallRulesetsPrefixlistsFeatureEnabled, isFirewallRulesetsPrefixListsBetaEnabled, @@ -342,7 +371,10 @@ export const MultiplePrefixListSelect = React.memo(