From cc259e702541b0f2865b561e0c9c752a4bcb94d8 Mon Sep 17 00:00:00 2001 From: carrillo-erik <119514965+carrillo-erik@users.noreply.github.com> Date: Thu, 12 Oct 2023 12:31:37 -0700 Subject: [PATCH 01/45] refactor: [M3-6287] - MUI v5 `Features > Users` (#9748) * refactor: [M3-6287] - MUI v5 features > Users * Add changeset * Change interface names and fix UI bugs --- .../pr-9748-tech-stories-1696350216805.md | 5 + packages/manager/src/MainContent.tsx | 10 +- .../components/ActionsPanel/ActionsPanel.tsx | 5 +- .../src/features/Account/AccountLanding.tsx | 6 +- .../manager/src/features/Account/index.tsx | 10 +- .../src/features/Users/CreateUserDrawer.tsx | 2 +- .../Users/UserDeleteConfirmationDialog.tsx | 9 +- .../manager/src/features/Users/UserDetail.tsx | 10 +- .../features/Users/UserPermissions.styles.ts | 32 ++++ .../src/features/Users/UserPermissions.tsx | 141 ++++++++---------- .../UserPermissionsEntitySection.styles.ts | 18 +++ .../Users/UserPermissionsEntitySection.tsx | 114 ++++++-------- .../src/features/Users/UserProfile.styles.ts | 23 +++ .../src/features/Users/UserProfile.tsx | 66 ++++---- .../src/features/Users/UsersActionMenu.tsx | 7 +- .../src/features/Users/UsersLanding.tsx | 11 +- packages/manager/src/features/Users/index.tsx | 2 - 17 files changed, 248 insertions(+), 223 deletions(-) create mode 100644 packages/manager/.changeset/pr-9748-tech-stories-1696350216805.md create mode 100644 packages/manager/src/features/Users/UserPermissions.styles.ts create mode 100644 packages/manager/src/features/Users/UserPermissionsEntitySection.styles.ts create mode 100644 packages/manager/src/features/Users/UserProfile.styles.ts delete mode 100644 packages/manager/src/features/Users/index.tsx diff --git a/packages/manager/.changeset/pr-9748-tech-stories-1696350216805.md b/packages/manager/.changeset/pr-9748-tech-stories-1696350216805.md new file mode 100644 index 00000000000..01034b777dd --- /dev/null +++ b/packages/manager/.changeset/pr-9748-tech-stories-1696350216805.md @@ -0,0 +1,5 @@ +--- +"@linode/manager": Tech Stories +--- + +MUI v5 Migration - Src > Features > Users ([#9748](https://github.com/linode/manager/pull/9748)) diff --git a/packages/manager/src/MainContent.tsx b/packages/manager/src/MainContent.tsx index d946b1b9b2a..336652e5de5 100644 --- a/packages/manager/src/MainContent.tsx +++ b/packages/manager/src/MainContent.tsx @@ -135,7 +135,11 @@ interface Props { type CombinedProps = Props & GlobalErrorProps; -const Account = React.lazy(() => import('src/features/Account')); +const Account = React.lazy(() => + import('src/features/Account').then((module) => ({ + default: module.Account, + })) +); const LinodesRoutes = React.lazy(() => import('src/features/Linodes')); const Volumes = React.lazy(() => import('src/features/Volumes')); const Domains = React.lazy(() => @@ -409,8 +413,8 @@ export default compose( export const checkFlagsForMainContentBanner = (flags: FlagSet) => { return Boolean( flags.mainContentBanner && - !isEmpty(flags.mainContentBanner) && - flags.mainContentBanner.key + !isEmpty(flags.mainContentBanner) && + flags.mainContentBanner.key ); }; diff --git a/packages/manager/src/components/ActionsPanel/ActionsPanel.tsx b/packages/manager/src/components/ActionsPanel/ActionsPanel.tsx index ac0c24e55f1..83f8498d3e5 100644 --- a/packages/manager/src/components/ActionsPanel/ActionsPanel.tsx +++ b/packages/manager/src/components/ActionsPanel/ActionsPanel.tsx @@ -77,11 +77,10 @@ const StyledBox = styled(Box)(({ theme: { spacing } }) => ({ '& > button': { marginBottom: spacing(1), }, - display: 'flex', justifyContent: 'flex-end', - marginTop: spacing(2), + marginTop: spacing(1), paddingBottom: spacing(1), - paddingTop: spacing(2), + paddingTop: spacing(1), })); const ActionsPanel = RenderGuard(ActionsPanelComponent); diff --git a/packages/manager/src/features/Account/AccountLanding.tsx b/packages/manager/src/features/Account/AccountLanding.tsx index 848dd06ad42..8473d4e964a 100644 --- a/packages/manager/src/features/Account/AccountLanding.tsx +++ b/packages/manager/src/features/Account/AccountLanding.tsx @@ -28,7 +28,11 @@ const EntityTransfersLanding = React.lazy(() => default: module.EntityTransfersLanding, })) ); -const Users = React.lazy(() => import('src/features/Users')); +const Users = React.lazy(() => + import('../Users/UsersLanding').then((module) => ({ + default: module.UsersLanding, + })) +); const GlobalSettings = React.lazy(() => import('./GlobalSettings')); const MaintenanceLanding = React.lazy( () => import('./Maintenance/MaintenanceLanding') diff --git a/packages/manager/src/features/Account/index.tsx b/packages/manager/src/features/Account/index.tsx index 7cf500db3c6..305b3f06558 100644 --- a/packages/manager/src/features/Account/index.tsx +++ b/packages/manager/src/features/Account/index.tsx @@ -19,11 +19,16 @@ const EntityTransfersCreate = React.lazy(() => default: module.EntityTransfersCreate, })) ); -const UserDetail = React.lazy(() => import('src/features/Users/UserDetail')); + +const UserDetail = React.lazy(() => + import('../Users/UserDetail').then((module) => ({ + default: module.UserDetail, + })) +); type Props = RouteComponentProps<{}>; -class Account extends React.Component { +export class Account extends React.Component { render() { const { match: { path }, @@ -55,4 +60,3 @@ class Account extends React.Component { ); } } -export default Account; diff --git a/packages/manager/src/features/Users/CreateUserDrawer.tsx b/packages/manager/src/features/Users/CreateUserDrawer.tsx index 572cb13a03c..8fc18e2abdc 100644 --- a/packages/manager/src/features/Users/CreateUserDrawer.tsx +++ b/packages/manager/src/features/Users/CreateUserDrawer.tsx @@ -5,10 +5,10 @@ import { RouteComponentProps, withRouter } from 'react-router-dom'; import { ActionsPanel } from 'src/components/ActionsPanel/ActionsPanel'; import { Drawer } from 'src/components/Drawer'; +import { FormControlLabel } from 'src/components/FormControlLabel'; import { Notice } from 'src/components/Notice/Notice'; import { TextField } from 'src/components/TextField'; import { Toggle } from 'src/components/Toggle'; -import { FormControlLabel } from 'src/components/FormControlLabel'; import { getAPIErrorOrDefault } from 'src/utilities/errorUtils'; import getAPIErrorsFor from 'src/utilities/getAPIErrorFor'; diff --git a/packages/manager/src/features/Users/UserDeleteConfirmationDialog.tsx b/packages/manager/src/features/Users/UserDeleteConfirmationDialog.tsx index 1188c7f0f83..bca7fa46f49 100644 --- a/packages/manager/src/features/Users/UserDeleteConfirmationDialog.tsx +++ b/packages/manager/src/features/Users/UserDeleteConfirmationDialog.tsx @@ -10,9 +10,12 @@ interface Props { username: string; } -export const UserDeleteConfirmationDialog = (props: Props) => { - const { onCancel, onDelete, open, username } = props; - +export const UserDeleteConfirmationDialog = ({ + onCancel, + onDelete, + open, + username, +}: Props) => { return ( { +export const UserDetail = () => { const { username: usernameParam } = useParams<{ username: string }>(); const location = useLocation<{ newUsername: string; success: boolean }>(); const history = useHistory(); @@ -260,5 +260,3 @@ const UserDetail: React.FC = () => { ); }; - -export default UserDetail; diff --git a/packages/manager/src/features/Users/UserPermissions.styles.ts b/packages/manager/src/features/Users/UserPermissions.styles.ts new file mode 100644 index 00000000000..afdcef657d0 --- /dev/null +++ b/packages/manager/src/features/Users/UserPermissions.styles.ts @@ -0,0 +1,32 @@ +import { styled } from '@mui/material/styles'; + +import Select from 'src/components/EnhancedSelect/Select'; + +export const StyledSelect = styled(Select, { + label: 'StyledSelect', +})(({ theme }) => ({ + '& .react-select__menu, & .input': { + marginLeft: theme.spacing(1), + right: 0, + textAlign: 'left' as const, + width: 125, + }, + '& .react-select__menu-list': { + width: '100%', + }, + '& > div': { + alignItems: 'center', + display: 'flex', + justifyContent: 'flex-end', + }, + '& label': { + marginTop: 6, + }, +})); + +export const StyledDivWrapper = styled('div', { + label: 'StyledDivWrapper', +})(({ theme }) => ({ + marginTop: theme.spacing(2), + paddingBottom: 0, +})); diff --git a/packages/manager/src/features/Users/UserPermissions.tsx b/packages/manager/src/features/Users/UserPermissions.tsx index 13bde4b794c..5a80fa197b0 100644 --- a/packages/manager/src/features/Users/UserPermissions.tsx +++ b/packages/manager/src/features/Users/UserPermissions.tsx @@ -9,18 +9,16 @@ import { } from '@linode/api-v4/lib/account'; import { APIError } from '@linode/api-v4/lib/types'; import Grid from '@mui/material/Unstable_Grid2'; -import { Theme } from '@mui/material/styles'; import { WithSnackbarProps, withSnackbar } from 'notistack'; import { compose, flatten, lensPath, omit, set } from 'ramda'; import * as React from 'react'; import { compose as recompose } from 'recompose'; -import { withStyles } from 'tss-react/mui'; import { ActionsPanel } from 'src/components/ActionsPanel/ActionsPanel'; import { CircleProgress } from 'src/components/CircleProgress'; import { Divider } from 'src/components/Divider'; import { DocumentTitleSegment } from 'src/components/DocumentTitle'; -import Select, { Item } from 'src/components/EnhancedSelect/Select'; +import { Item } from 'src/components/EnhancedSelect/Select'; import { FormControlLabel } from 'src/components/FormControlLabel'; import { Notice } from 'src/components/Notice/Notice'; import { Paper } from 'src/components/Paper'; @@ -43,66 +41,12 @@ import { getAPIErrorOrDefault } from 'src/utilities/errorUtils'; import getAPIErrorsFor from 'src/utilities/getAPIErrorFor'; import scrollErrorIntoView from 'src/utilities/scrollErrorIntoView'; +import { StyledDivWrapper, StyledSelect } from './UserPermissions.styles'; import { UserPermissionsEntitySection, entityNameMap, } from './UserPermissionsEntitySection'; - -type ClassNames = - | 'globalRow' - | 'globalSection' - | 'section' - | 'setAll' - | 'title' - | 'toggle' - | 'unrestrictedRoot'; - -const styles = (theme: Theme) => ({ - globalRow: { - padding: `${theme.spacing(1)} 0`, - }, - globalSection: { - marginTop: theme.spacing(2), - }, - section: { - marginTop: theme.spacing(2), - paddingBottom: 0, - }, - setAll: { - '& .react-select__menu, & .input': { - marginLeft: theme.spacing(1), - right: 0, - textAlign: 'left' as const, - width: 125, - }, - '& .react-select__menu-list': { - width: '100%', - }, - '& > div': { - alignItems: 'center', - display: 'flex', - justifyContent: 'flex-end', - }, - '& label': { - marginTop: 6, - }, - }, - title: { - [theme.breakpoints.down('md')]: { - paddingLeft: theme.spacing(), - }, - }, - toggle: { - marginRight: 3, - }, - unrestrictedRoot: { - marginTop: theme.spacing(2), - padding: theme.spacing(3), - }, -}); - interface Props { - classes?: Partial>; clearNewUser: () => void; currentUser?: string; username?: string; @@ -342,7 +286,6 @@ class UserPermissions extends React.Component { onCancel: () => void, loading: boolean ) => { - const classes = withStyles.getClasses(this.props); return ( { label: 'Reset', onClick: onCancel, }} + sx={(theme) => ({ + marginTop: theme.spacing(2), + paddingBottom: 0, + })} alignItems="center" - className={classes.section} display="flex" justifyContent="flex-end" /> @@ -365,16 +311,18 @@ class UserPermissions extends React.Component { }; renderBillingPerm = () => { - const classes = withStyles.getClasses(this.props); const { grants } = this.state; if (!(grants && grants.global)) { return null; } return ( -
+ ({ + marginTop: theme.spacing(2), + paddingBottom: 0, + })} container data-qa-billing-section spacing={2} @@ -385,7 +333,14 @@ class UserPermissions extends React.Component { - + ({ + marginTop: theme.spacing(2), + paddingBottom: 0, + })} + container + spacing={2} + > { onClick={this.billingPermOnClick('read_write')} /> -
+ ); }; renderBody = () => { - const classes = withStyles.getClasses(this.props); const { currentUser, username } = this.props; const { errors, restricted } = this.state; const hasErrorFor = getAPIErrorsFor({ restricted: 'Restricted' }, errors); @@ -434,7 +388,11 @@ class UserPermissions extends React.Component { > ({ + [theme.breakpoints.down('md')]: { + paddingLeft: theme.spacing(), + }, + })} data-qa-restrict-access={restricted} variant="h2" > @@ -452,9 +410,9 @@ class UserPermissions extends React.Component { : '' } checked={!restricted} - className={classes.toggle} disabled={username === currentUser} onChange={this.onChangeRestricted} + sx={{ marginRight: '3px' }} /> @@ -464,7 +422,6 @@ class UserPermissions extends React.Component { }; renderGlobalPerm = (perm: string, checked: boolean) => { - const classes = withStyles.getClasses(this.props); const permDescriptionMap = { add_databases: 'Can add Databases to this account ($)', add_domains: 'Can add Domains using the DNS Manager', @@ -494,7 +451,9 @@ class UserPermissions extends React.Component { onChange={this.globalPermOnChange(perm)} /> } - className={classes.globalRow} + sx={(theme) => ({ + padding: `${theme.spacing(1)} 0`, + })} label={permDescriptionMap[perm]} /> @@ -503,17 +462,28 @@ class UserPermissions extends React.Component { }; renderGlobalPerms = () => { - const classes = withStyles.getClasses(this.props); const { grants, isSavingGlobal } = this.state; return ( - + ({ + marginTop: theme.spacing(2), + })} + data-qa-global-section + > Global Permissions - + ({ + marginTop: theme.spacing(2), + paddingBottom: 0, + })} + container + spacing={2} + > {grants && grants.global && this.globalBooleanPerms @@ -552,7 +522,6 @@ class UserPermissions extends React.Component { }; renderSpecificPerms = () => { - const classes = withStyles.getClasses(this.props); const { grants, isSavingEntity, setAllPerm } = this.state; const permOptions = [ @@ -566,7 +535,12 @@ class UserPermissions extends React.Component { }); return ( - + ({ + marginTop: theme.spacing(2), + })} + data-qa-entity-section + > { - ) => { handleSelectVPC(selectedVPC.value); @@ -152,15 +187,15 @@ export const VPCPanel = (props: VPCPanelProps) => { value={vpcDropdownOptions.find( (option) => option.value === selectedVPCId )} - defaultValue={vpcDropdownOptions[0]} + defaultValue={fromLinodeConfig ? null : vpcDropdownOptions[0]} // If we're in the Config dialog, there is no "None" option at index 0 disabled={!regionSupportsVPCs} - errorText={vpcError} + errorText={vpcIdError ?? vpcError} isClearable={false} isLoading={isLoading} - label="Assign VPC" + label={from === 'linodeCreate' ? 'Assign VPC' : 'VPC'} noOptionsMessage={() => 'Create a VPC to assign to this Linode.'} options={vpcDropdownOptions} - placeholder={''} + placeholder={'Select a VPC'} /> {vpcDropdownOptions.length <= 1 && regionSupportsVPCs && ( ({ paddingTop: theme.spacing(1.5) })}> @@ -169,9 +204,11 @@ export const VPCPanel = (props: VPCPanelProps) => { )} - - Create VPC - + {from === 'linodeCreate' && ( + + Create VPC + + )} {selectedVPCId !== -1 && regionSupportsVPCs && ( @@ -214,7 +251,7 @@ export const VPCPanel = (props: VPCPanelProps) => { flexDirection="row" sx={{}} > - + Auto-assign a VPC IPv4 address for this Linode in the VPC { })} alignItems="center" display="flex" - flexDirection="row" > { } /> + {assignPublicIPv4Address && publicIPv4Error && ( + ({ + color: theme.color.red, + })} + > + {publicIPv4Error} + + )} )} diff --git a/packages/manager/src/features/Linodes/LinodesDetail/LinodeConfigs/ConfigRow.tsx b/packages/manager/src/features/Linodes/LinodesDetail/LinodeConfigs/ConfigRow.tsx index bedb36727f5..66611751a80 100644 --- a/packages/manager/src/features/Linodes/LinodesDetail/LinodeConfigs/ConfigRow.tsx +++ b/packages/manager/src/features/Linodes/LinodesDetail/LinodeConfigs/ConfigRow.tsx @@ -1,4 +1,4 @@ -import { Config, Interface } from '@linode/api-v4/lib/linodes'; +import { Config } from '@linode/api-v4/lib/linodes'; import { styled } from '@mui/material/styles'; import * as React from 'react'; @@ -9,6 +9,7 @@ import { useAllLinodeDisksQuery } from 'src/queries/linodes/disks'; import { useLinodeKernelQuery } from 'src/queries/linodes/linodes'; import { useLinodeVolumesQuery } from 'src/queries/volumes'; +import { InterfaceListItem } from './InterfaceListItem'; import { ConfigActionMenu } from './LinodeConfigActionMenu'; interface Props { @@ -59,7 +60,7 @@ export const ConfigRow = React.memo((props: Props) => { return undefined; } return ( -
  • +
  • /dev/{thisDevice} - {label}
  • ); @@ -76,17 +77,12 @@ export const ConfigRow = React.memo((props: Props) => { const InterfaceList = ( {interfaces.map((interfaceEntry, idx) => { - // The order of the config.interfaces array as returned by the API is significant. - // Index 0 is eth0, index 1 is eth1, index 2 is eth2. - const interfaceName = `eth${idx}`; - return ( -
  • - {interfaceName} – {getInterfaceLabel(interfaceEntry)} -
  • + /> ); })}
    @@ -132,15 +128,3 @@ const StyledTableCell = styled(TableCell, { label: 'StyledTableCell' })({ }, padding: '0 !important', }); - -export const getInterfaceLabel = (configInterface: Interface): string => { - if (configInterface.purpose === 'public') { - return 'Public Internet'; - } - - const interfaceLabel = configInterface.label; - const ipamAddress = configInterface.ipam_address; - const hasIPAM = Boolean(ipamAddress); - - return `VLAN: ${interfaceLabel} ${hasIPAM ? `(${ipamAddress})` : ''}`; -}; diff --git a/packages/manager/src/features/Linodes/LinodesDetail/LinodeConfigs/InterfaceListItem.test.tsx b/packages/manager/src/features/Linodes/LinodesDetail/LinodeConfigs/InterfaceListItem.test.tsx new file mode 100644 index 00000000000..a3bbbc3db78 --- /dev/null +++ b/packages/manager/src/features/Linodes/LinodesDetail/LinodeConfigs/InterfaceListItem.test.tsx @@ -0,0 +1,79 @@ +import { waitFor } from '@testing-library/react'; +import * as React from 'react'; + +import { + LinodeConfigInterfaceFactory, + LinodeConfigInterfaceFactoryWithVPC, + vpcFactory, +} from 'src/factories'; +import { rest, server } from 'src/mocks/testServer'; +import { queryClientFactory } from 'src/queries/base'; +import { mockMatchMedia, renderWithTheme } from 'src/utilities/testHelpers'; + +import { InterfaceListItem } from './InterfaceListItem'; + +const queryClient = queryClientFactory(); + +beforeAll(() => mockMatchMedia()); +afterEach(() => { + queryClient.clear(); +}); + +const interfaceListItemTestId = 'interface-list-item'; + +describe('InterfaceListItem', () => { + it('should return the correct text for a public interface', () => { + const publicInterface = LinodeConfigInterfaceFactory.build({ + ipam_address: null, + purpose: 'public', + }); + + const { getByTestId } = renderWithTheme( + + ); + + expect(getByTestId(interfaceListItemTestId).innerHTML).toEqual( + 'eth0 – Public Internet' + ); + }); + + it('should return the correct text for a VLAN interface', () => { + const vlanInterface = LinodeConfigInterfaceFactory.build(); + + const { getByTestId } = renderWithTheme( + + ); + + expect(getByTestId(interfaceListItemTestId).innerHTML).toEqual( + `eth1 – VLAN: ${vlanInterface.label} (${vlanInterface.ipam_address})` + ); + }); + + it('should return the correct text for a VPC interface', async () => { + const vpc = vpcFactory.build({ + id: 1, + label: 'first-vpc', + }); + + const vpcInterface = LinodeConfigInterfaceFactoryWithVPC.build({ + vpc_id: 1, + }); + + server.use( + rest.get('*/vpcs/:vpcId', (req, res, ctx) => { + return res(ctx.json(vpc)); + }) + ); + + const { getByTestId } = renderWithTheme( + , + { queryClient } + ); + + await waitFor(() => { + expect(getByTestId(interfaceListItemTestId).innerHTML).toEqual( + `eth2 – VPC: ${vpc.label} (${vpcInterface.ipv4?.vpc})` + ); + }); + }); +}); diff --git a/packages/manager/src/features/Linodes/LinodesDetail/LinodeConfigs/InterfaceListItem.tsx b/packages/manager/src/features/Linodes/LinodesDetail/LinodeConfigs/InterfaceListItem.tsx new file mode 100644 index 00000000000..97bf7e74c23 --- /dev/null +++ b/packages/manager/src/features/Linodes/LinodesDetail/LinodeConfigs/InterfaceListItem.tsx @@ -0,0 +1,44 @@ +import { Interface } from '@linode/api-v4/lib/linodes'; +import React from 'react'; + +import { useVPCQuery } from 'src/queries/vpcs'; + +interface Props { + idx: number; + interfaceEntry: Interface; +} + +export const InterfaceListItem = (props: Props) => { + const { idx, interfaceEntry } = props; + const { data: vpc } = useVPCQuery( + interfaceEntry?.vpc_id ?? -1, + Boolean(interfaceEntry.vpc_id) + ); + + // The order of the config.interfaces array as returned by the API is significant. + // Index 0 is eth0, index 1 is eth1, index 2 is eth2. + const interfaceName = `eth${idx}`; + + const getInterfaceLabel = (configInterface: Interface): string => { + if (configInterface.purpose === 'public') { + return 'Public Internet'; + } + + const interfaceLabel = configInterface.label; + + if (configInterface.purpose === 'vlan') { + const ipamAddress = configInterface.ipam_address; + const hasIPAM = Boolean(ipamAddress); + return `VLAN: ${interfaceLabel} ${hasIPAM ? `(${ipamAddress})` : ''}`; + } + + const vpcIpv4 = configInterface.ipv4?.vpc; + return `VPC: ${vpc?.label} ${Boolean(vpcIpv4) ? `(${vpcIpv4})` : ''}`; + }; + + return ( +
  • + {interfaceName} – {getInterfaceLabel(interfaceEntry)} +
  • + ); +}; diff --git a/packages/manager/src/features/Linodes/LinodesDetail/LinodeConfigs/LinodeConfigDialog.test.tsx b/packages/manager/src/features/Linodes/LinodesDetail/LinodeConfigs/LinodeConfigDialog.test.tsx index 1be60d134b3..13b27f5b448 100644 --- a/packages/manager/src/features/Linodes/LinodesDetail/LinodeConfigs/LinodeConfigDialog.test.tsx +++ b/packages/manager/src/features/Linodes/LinodesDetail/LinodeConfigs/LinodeConfigDialog.test.tsx @@ -1,4 +1,23 @@ +import { waitFor } from '@testing-library/react'; +import * as React from 'react'; + +import { configFactory } from 'src/factories'; +import { accountFactory } from 'src/factories/account'; +import { rest, server } from 'src/mocks/testServer'; +import { queryClientFactory } from 'src/queries/base'; +import { mockMatchMedia, renderWithTheme } from 'src/utilities/testHelpers'; + import { padList } from './LinodeConfigDialog'; +import { LinodeConfigDialog } from './LinodeConfigDialog'; + +const queryClient = queryClientFactory(); + +beforeAll(() => mockMatchMedia()); +afterEach(() => { + queryClient.clear(); +}); + +const primaryInterfaceDropdownTestId = 'primary-interface-dropdown'; describe('LinodeConfigDialog', () => { describe('padInterface helper method', () => { @@ -14,4 +33,39 @@ describe('LinodeConfigDialog', () => { expect(padList([1, 2, 3, 4, 5], 0, 2)).toEqual([1, 2, 3, 4, 5]); }); }); + + describe('Primary Interface (Default Route) dropdown', () => { + it('should not display if the VPC feature flag is off and the account capabilities do not include VPC', async () => { + const account = accountFactory.build({ + capabilities: [], + }); + + const config = configFactory.build(); + + server.use( + rest.get('*/account', (req, res, ctx) => { + return res(ctx.json(account)); + }) + ); + + const { queryByTestId } = renderWithTheme( + , + { + flags: { vpc: false }, + } + ); + + await waitFor(() => { + expect( + queryByTestId(primaryInterfaceDropdownTestId) + ).not.toBeInTheDocument(); + }); + }); + }); }); diff --git a/packages/manager/src/features/Linodes/LinodesDetail/LinodeConfigs/LinodeConfigDialog.tsx b/packages/manager/src/features/Linodes/LinodesDetail/LinodeConfigs/LinodeConfigDialog.tsx index ef7e7b2d5ba..222b10f9cbc 100644 --- a/packages/manager/src/features/Linodes/LinodesDetail/LinodeConfigs/LinodeConfigDialog.tsx +++ b/packages/manager/src/features/Linodes/LinodesDetail/LinodeConfigs/LinodeConfigDialog.tsx @@ -35,6 +35,8 @@ import { TooltipIcon } from 'src/components/TooltipIcon'; import { Typography } from 'src/components/Typography'; import { DeviceSelection } from 'src/features/Linodes/LinodesDetail/LinodeRescue/DeviceSelection'; import { titlecase } from 'src/features/Linodes/presentation'; +import { useAccountManagement } from 'src/hooks/useAccountManagement'; +import { useFlags } from 'src/hooks/useFlags'; import { useLinodeConfigCreateMutation, useLinodeConfigUpdateMutation, @@ -47,6 +49,8 @@ import { import { useRegionsQuery } from 'src/queries/regions'; import { queryKey as vlansQueryKey } from 'src/queries/vlans'; import { useAllVolumesQuery } from 'src/queries/volumes'; +import { vpcQueryKey } from 'src/queries/vpcs'; +import { isFeatureEnabled } from 'src/utilities/accountCapabilities'; import createDevicesFromStrings, { DevicesAsStrings, } from 'src/utilities/createDevicesFromStrings'; @@ -166,23 +170,51 @@ const interfacesToState = (interfaces?: Interface[]) => { return defaultInterfaceList; } const interfacesPayload = interfaces.map( - ({ ipam_address, label, purpose }) => ({ ipam_address, label, purpose }) + ({ + id, + ipam_address, + ipv4, + label, + primary, + purpose, + subnet_id, + vpc_id, + }) => ({ + id, + ipam_address, + ipv4, + label, + primary, + purpose, + subnet_id, + vpc_id, + }) ); return padInterfaceList(interfacesPayload); }; -const interfacesToPayload = (interfaces?: ExtendedInterface[]) => { +const interfacesToPayload = ( + interfaces?: ExtendedInterface[], + primaryInterfaceIndex?: number +) => { if (!interfaces || interfaces.length === 0) { return []; } - return equals(interfaces, defaultInterfaceList) - ? // In this case, where eth0 is set to public interface - // and no other interfaces are specified, the API prefers - // to receive an empty array. - [] - : (interfaces.filter( - (thisInterface) => thisInterface.purpose !== 'none' - ) as Interface[]); + + if (equals(interfaces, defaultInterfaceList)) { + // In this case, where eth0 is set to public interface + // and no other interfaces are specified, the API prefers + // to receive an empty array. + return []; + } + + if (primaryInterfaceIndex !== undefined) { + interfaces[primaryInterfaceIndex].primary = true; + } + + return interfaces.filter( + (thisInterface) => thisInterface.purpose !== 'none' + ) as Interface[]; }; const deviceSlots = ['sda', 'sdb', 'sdc', 'sdd', 'sde', 'sdf', 'sdg', 'sdh']; @@ -219,6 +251,9 @@ export const LinodeConfigDialog = (props: Props) => { ); const theme = useTheme(); + const flags = useFlags(); + const { account } = useAccountManagement(); + const regions = useRegionsQuery().data ?? []; const queryClient = useQueryClient(); @@ -229,14 +264,31 @@ export const LinodeConfigDialog = (props: Props) => { const [useCustomRoot, setUseCustomRoot] = React.useState(false); + const [ + primaryInterfaceIndex, + setPrimaryInterfaceIndex, + ] = React.useState(); + const regionHasVLANS = regions.some( (thisRegion) => thisRegion.id === linode?.region && thisRegion.capabilities.includes('Vlans') ); + const regionHasVPCs = regions.some( + (thisRegion) => + thisRegion.id === linode?.region && + thisRegion.capabilities.includes('VPCs') + ); const showVlans = regionHasVLANS; + // @TODO VPC: remove once VPC is fully rolled out + const vpcEnabled = isFeatureEnabled( + 'VPCs', + Boolean(flags.vpc), + account?.capabilities ?? [] + ); + const { resetForm, setFieldValue, values, ...formik } = useFormik({ initialValues: defaultFieldsValues, onSubmit: (values) => onSubmit(values), @@ -268,7 +320,7 @@ export const LinodeConfigDialog = (props: Props) => { devices: createDevicesFromStrings(devices), helpers, initrd: initrd !== '' ? initrd : null, - interfaces: interfacesToPayload(interfaces), + interfaces: interfacesToPayload(interfaces, primaryInterfaceIndex), kernel, label, /** if the user did not toggle the limit radio button, send a value of 0 */ @@ -316,7 +368,7 @@ export const LinodeConfigDialog = (props: Props) => { configData.initrd = finnixDiskID; } - if (!regionHasVLANS) { + if (!regionHasVLANS || !regionHasVPCs) { delete configData.interfaces; } @@ -330,7 +382,16 @@ export const LinodeConfigDialog = (props: Props) => { (thisInterface) => thisInterface.purpose === 'vlan' ) ) { - queryClient.invalidateQueries('vlans'); + queryClient.invalidateQueries(vlansQueryKey); + } + + // Ensure VPC query data is up-to-date + if ( + configData.interfaces?.some( + (thisInterface) => thisInterface.purpose === 'vpc' + ) + ) { + queryClient.invalidateQueries(vpcQueryKey); } enqueueSnackbar( @@ -355,9 +416,20 @@ export const LinodeConfigDialog = (props: Props) => { }); }; + // @TODO VPC: Remove this override and surface the field errors appropriately + // once API fixes interface index bug for ipv4.vpc & ipv4.nat_1_1 errors + const overrideFieldForIPv4 = (error: APIError[]) => { + error.forEach((err) => { + if (err.field && ['ipv4.nat_1_1', 'ipv4.vpc'].includes(err.field)) { + err.field = 'interfaces'; + } + }); + }; + formik.setSubmitting(false); overrideFieldForDevices(error); + overrideFieldForIPv4(error); handleFieldErrors(formik.setErrors, error); @@ -411,6 +483,14 @@ export const LinodeConfigDialog = (props: Props) => { ) ); + const indexOfExistingPrimaryInterface = config.interfaces.findIndex( + (_interface) => _interface.primary === true + ); + + if (vpcEnabled && indexOfExistingPrimaryInterface !== -1) { + setPrimaryInterfaceIndex(indexOfExistingPrimaryInterface); + } + resetForm({ values: { comments: config.comments, @@ -434,9 +514,10 @@ export const LinodeConfigDialog = (props: Props) => { resetForm({ values: defaultFieldsValues }); setUseCustomRoot(false); setDeviceCounter(deviceCounterDefault); + setPrimaryInterfaceIndex(0); } } - }, [open, config, initrdFromConfig, resetForm]); + }, [open, config, initrdFromConfig, resetForm, queryClient, vpcEnabled]); const generalError = formik.status?.generalError; @@ -511,6 +592,21 @@ export const LinodeConfigDialog = (props: Props) => { value: '', }); + const getPrimaryInterfaceOptions = (interfaces: ExtendedInterface[]) => { + return interfaces.map((_interface, idx) => { + return { + label: `eth${idx}`, + value: idx, + }; + }); + }; + + const primaryInterfaceOptions = getPrimaryInterfaceOptions(values.interfaces); + + const handlePrimaryInterfaceChange = (selected: Item) => { + setPrimaryInterfaceIndex(selected.value); + }; + /** * Form change handlers * (where formik.handleChange is insufficient) @@ -563,6 +659,21 @@ export const LinodeConfigDialog = (props: Props) => { [setFieldValue] ); + const networkInterfacesHelperText = ( + + Configure the network that a selected interface will connect to ( + {vpcEnabled + ? '"Public Internet", VLAN, or VPC' + : 'either "Public Internet" or a VLAN'} + ) . Each Linode can have up to three Network Interfaces. For more + information, see our{' '} + + Network Interfaces guide + + . + + ); + return ( { {showVlans ? ( - Network Interfaces + + {vpcEnabled ? 'Networking' : 'Network Interfaces'} + - Configure the network that a selected interface will - connect to (either "Public Internet" or a - VLAN). Each Linode can have up to three Network - Interfaces. For more information, see our{' '} - - Network Interfaces guide - - . - - } interactive status="help" sx={{ tooltip: { maxWidth: 350 } }} + text={networkInterfacesHelperText} /> - {formik.errors.interfaces ? ( + {formik.errors.interfaces && ( - ) : null} + )} + {vpcEnabled && ( + <> + + isLoading + ? 'Loading...' + : 'You have no VLANs in this region. Type to create one.' + } + creatable + createOptionPosition="first" + errorText={errors.labelError} + inputId={`vlan-label-${slotNumber}`} + isClearable + isDisabled={readOnly} + label="VLAN" + onChange={handleLabelChange} + onCreateOption={handleCreateOption} + options={vlanOptions} + placeholder="Create or select a VLAN" + value={vlanOptions.find((thisVlan) => thisVlan.value === label) ?? null} + /> + ); + + const jsxIPAMForVLAN = ( + + sendLinodeCreateDocsEvent('IPAM Address Tooltip Hover') + } + tooltipText={ + 'IPAM address must use IP/netmask format, e.g. 192.0.2.0/24.' + } + disabled={readOnly} + errorText={errors.ipamError} + inputId={`ipam-input-${slotNumber}`} + label="IPAM Address" + onChange={handleAddressChange} + optional + placeholder="192.0.2.0/24" + value={ipamAddress} + /> + ); + + const enclosingJSXForVLANFields = ( + jsxSelectVLAN: JSX.Element, + jsxIPAMForVLAN: JSX.Element + ) => { + return fromAddonsPanel ? ( + + + + {jsxSelectVLAN} + + + {jsxIPAMForVLAN} + + + + ) : ( + + + {jsxSelectVLAN} + {jsxIPAMForVLAN} + + + ); + }; + return ( {fromAddonsPanel ? null : ( - + - isLoading - ? 'Loading...' - : 'You have no VLANs in this region. Type to create one.' - } - value={ - vlanOptions.find((thisVlan) => thisVlan.value === label) ?? - null - } - creatable - createOptionPosition="first" - errorText={labelError} - inputId={`vlan-label-${slotNumber}`} - isClearable - isDisabled={readOnly} - label="VLAN" - onChange={handleLabelChange} - onCreateOption={handleCreateOption} - options={vlanOptions} - placeholder="Create or select a VLAN" - /> - - - - sendLinodeCreateDocsEvent('IPAM Address Tooltip Hover') - } - tooltipText={ - 'IPAM address must use IP/netmask format, e.g. 192.0.2.0/24.' - } - disabled={readOnly} - errorText={ipamError} - inputId={`ipam-input-${slotNumber}`} - label="IPAM Address" - onChange={handleAddressChange} - optional - placeholder="192.0.2.0/24" - value={ipamAddress} - /> - - + {purpose === 'vlan' && + enclosingJSXForVLANFields(jsxSelectVLAN, jsxIPAMForVLAN)} + {purpose === 'vpc' && ( + + + setAutoAssignLinodeIPv4( + (autoAssignLinodeIPv4) => !autoAssignLinodeIPv4 + ) + } + toggleAutoassignIPv4WithinVPCEnabled={() => + setAutoAssignVPCIPv4((autoAssignVPCIPv4) => !autoAssignVPCIPv4) + } + assignPublicIPv4Address={autoAssignLinodeIPv4} + autoassignIPv4WithinVPC={autoAssignVPCIPv4} + from="linodeConfig" + handleSelectVPC={handleVPCLabelChange} + handleSubnetChange={handleSubnetChange} + handleVPCIPv4Change={handleVPCIPv4Input} + publicIPv4Error={errors.publicIPv4Error} + region={region} + selectedSubnetId={subnetId} + selectedVPCId={vpcId} + subnetError={errors.subnetError} + vpcIPv4AddressOfLinode={vpcIPv4} + vpcIPv4Error={errors.vpcIPv4Error} + vpcIdError={errors.vpcError} + /> - ) : null} + )} {!fromAddonsPanel && ( { ); }; + +const getPurposeOptions = (showVPCs: boolean) => { + const purposeOptions: Item[] = [ + { + label: 'Public Internet', + value: 'public', + }, + { + label: 'VLAN', + value: 'vlan', + }, + { + label: 'None', + value: 'none', + }, + ]; + + if (showVPCs) { + purposeOptions.splice(1, 0, { + label: 'VPC', + value: 'vpc', + }); + } + + return purposeOptions; +}; diff --git a/packages/manager/src/features/VPCs/VPCDetail/SubnetAssignLinodesDrawer.tsx b/packages/manager/src/features/VPCs/VPCDetail/SubnetAssignLinodesDrawer.tsx index a576b00b3b2..5923bd50b78 100644 --- a/packages/manager/src/features/VPCs/VPCDetail/SubnetAssignLinodesDrawer.tsx +++ b/packages/manager/src/features/VPCs/VPCDetail/SubnetAssignLinodesDrawer.tsx @@ -144,6 +144,7 @@ export const SubnetAssignLinodesDrawer = ( label: null, purpose: 'vpc', subnet_id: subnet?.id, + vpc_id: vpcId, ...(!autoAssignIPv4 && { ipv4: { vpc: chosenIP } }), }; diff --git a/packages/manager/src/queries/vpcs.ts b/packages/manager/src/queries/vpcs.ts index 3a48324569f..bdd185c51f3 100644 --- a/packages/manager/src/queries/vpcs.ts +++ b/packages/manager/src/queries/vpcs.ts @@ -88,12 +88,13 @@ export const useDeleteVPCMutation = (id: number) => { export const useSubnetsQuery = ( vpcID: number, params: Params, - filter: Filter + filter: Filter, + enabled: boolean = true ) => { return useQuery, APIError[]>( [vpcQueryKey, 'vpc', vpcID, subnetQueryKey, 'paginated', params, filter], () => getSubnets(vpcID, params, filter), - { keepPreviousData: true } + { enabled, keepPreviousData: true } ); }; diff --git a/packages/manager/src/utilities/formikErrorUtils.ts b/packages/manager/src/utilities/formikErrorUtils.ts index b6a27d9e325..cf382be66a3 100644 --- a/packages/manager/src/utilities/formikErrorUtils.ts +++ b/packages/manager/src/utilities/formikErrorUtils.ts @@ -87,9 +87,9 @@ export const handleAPIErrors = ( }; export interface SubnetError { - label?: string; ipv4?: string; ipv6?: string; + label?: string; } /** diff --git a/packages/validation/.changeset/pr-9709-changed-1697141933133.md b/packages/validation/.changeset/pr-9709-changed-1697141933133.md new file mode 100644 index 00000000000..66d0d139604 --- /dev/null +++ b/packages/validation/.changeset/pr-9709-changed-1697141933133.md @@ -0,0 +1,5 @@ +--- +"@linode/validation": Changed +--- + +Require vpc_id when interface purpose is 'vpc' in LinodeInterfaceSchema ([#9709](https://github.com/linode/manager/pull/9709)) diff --git a/packages/validation/src/linodes.schema.ts b/packages/validation/src/linodes.schema.ts index 01eed7ec414..40834ed8052 100644 --- a/packages/validation/src/linodes.schema.ts +++ b/packages/validation/src/linodes.schema.ts @@ -155,9 +155,9 @@ export const LinodeInterfaceSchema = object().shape({ }), ipam_address: string().when('purpose', { is: 'vlan', - then: string().notRequired().test({ + then: string().notRequired().nullable().test({ name: 'validateIPAM', - message: 'Must be a valid IPv4 range, e.g. 192.0.2.0/24', + message: 'Must be a valid IPv4 range, e.g. 192.0.2.0/24.', test: validateIP, }), otherwise: string().when('ipam_address', { @@ -180,6 +180,15 @@ export const LinodeInterfaceSchema = object().shape({ test: (value) => typeof value === 'undefined', }), }), + vpc_id: number().when('purpose', { + is: 'vpc', + then: number().required('VPC is required.'), + otherwise: number().test({ + name: testnameDisallowedBasedOnPurpose('VPC'), + message: testmessageDisallowedBasedOnPurpose('vpc', 'vpc_id'), + test: (value) => typeof value === 'undefined', + }), + }), ipv4: ipv4ConfigInterface, ipv6: ipv6ConfigInterface, ip_ranges: array() From 49f467db36351118225620eeccc604a646132182 Mon Sep 17 00:00:00 2001 From: jdamore-linode <97627410+jdamore-linode@users.noreply.github.com> Date: Mon, 16 Oct 2023 11:15:33 -0400 Subject: [PATCH 08/45] test: Fix Cypress LKE create test failures stemming from reset mocks (#9782) * Use Kubernetes 1.27 in tests * Set up feature flag mocks in tests instead of hook * Add changeset --- .../.changeset/pr-9782-tests-1697147463647.md | 5 ++++ .../e2e/core/kubernetes/lke-create.spec.ts | 24 +++++++++---------- 2 files changed, 17 insertions(+), 12 deletions(-) create mode 100644 packages/manager/.changeset/pr-9782-tests-1697147463647.md diff --git a/packages/manager/.changeset/pr-9782-tests-1697147463647.md b/packages/manager/.changeset/pr-9782-tests-1697147463647.md new file mode 100644 index 00000000000..c31535f6dc8 --- /dev/null +++ b/packages/manager/.changeset/pr-9782-tests-1697147463647.md @@ -0,0 +1,5 @@ +--- +"@linode/manager": Tests +--- + +Improve LKE create test reattempt stability ([#9782](https://github.com/linode/manager/pull/9782)) diff --git a/packages/manager/cypress/e2e/core/kubernetes/lke-create.spec.ts b/packages/manager/cypress/e2e/core/kubernetes/lke-create.spec.ts index 3c103935932..fc5fcfb1bce 100644 --- a/packages/manager/cypress/e2e/core/kubernetes/lke-create.spec.ts +++ b/packages/manager/cypress/e2e/core/kubernetes/lke-create.spec.ts @@ -73,12 +73,7 @@ const getSimilarPlans = ( authenticate(); describe('LKE Cluster Creation', () => { before(() => { - cleanUp('lke-clusters'); - // TODO: DC Pricing - M3-7073: Remove feature flag mocks when DC specific pricing goes live. - mockAppendFeatureFlags({ - dcSpecificPricing: makeFeatureFlagData(false), - }).as('getFeatureFlags'); - mockGetFeatureFlagClientstream().as('getClientStream'); + cleanUp(['linodes', 'lke-clusters']); }); /* @@ -91,11 +86,16 @@ describe('LKE Cluster Creation', () => { it('can create an LKE cluster', () => { const clusterLabel = randomLabel(); const clusterRegion = chooseRegion(); - const clusterVersion = '1.26'; + const clusterVersion = '1.27'; const clusterPlans = new Array(2) .fill(null) .map(() => randomItem(lkeClusterPlans)); + // TODO: DC Pricing - M3-7073: Remove feature flag mocks when DC specific pricing goes live. + mockAppendFeatureFlags({ + dcSpecificPricing: makeFeatureFlagData(false), + }).as('getFeatureFlags'); + mockGetFeatureFlagClientstream().as('getClientStream'); interceptCreateCluster().as('createCluster'); cy.visitWithLogin('/kubernetes/clusters'); @@ -225,10 +225,6 @@ describe('LKE Cluster Creation', () => { describe('LKE Cluster Creation with DC-specific pricing', () => { before(() => { cleanUp('lke-clusters'); - mockAppendFeatureFlags({ - dcSpecificPricing: makeFeatureFlagData(true), - }).as('getFeatureFlags'); - mockGetFeatureFlagClientstream().as('getClientStream'); }); /* @@ -242,11 +238,15 @@ describe('LKE Cluster Creation with DC-specific pricing', () => { const clusterRegion = getRegionById('us-southeast'); const dcSpecificPricingRegion = getRegionById('us-east'); const clusterLabel = randomLabel(); - const clusterVersion = '1.26'; + const clusterVersion = '1.27'; const clusterPlans = new Array(2) .fill(null) .map(() => randomItem(dcPricingLkeClusterPlans)); + mockAppendFeatureFlags({ + dcSpecificPricing: makeFeatureFlagData(true), + }).as('getFeatureFlags'); + mockGetFeatureFlagClientstream().as('getClientStream'); interceptCreateCluster().as('createCluster'); cy.visitWithLogin('/kubernetes/clusters'); From cdc51b60f7b27163b19eb78f674316f952c750fd Mon Sep 17 00:00:00 2001 From: jdamore-linode <97627410+jdamore-linode@users.noreply.github.com> Date: Mon, 16 Oct 2023 11:16:23 -0400 Subject: [PATCH 09/45] test: Resolve Cypress test flake for Linode config edit test (#9781) * Add util to poll each of a Linode's disks for a desired status * Wait for Linode disks before attempting to edit config * Added changeset: Improve Linode config edit test stability --- .../.changeset/pr-9781-tests-1697144294347.md | 5 +++ .../cypress/support/util/linode-utils.ts | 16 ++++++- .../manager/cypress/support/util/polling.ts | 43 +++++++++++++++++-- 3 files changed, 58 insertions(+), 6 deletions(-) create mode 100644 packages/manager/.changeset/pr-9781-tests-1697144294347.md diff --git a/packages/manager/.changeset/pr-9781-tests-1697144294347.md b/packages/manager/.changeset/pr-9781-tests-1697144294347.md new file mode 100644 index 00000000000..00e60ca519e --- /dev/null +++ b/packages/manager/.changeset/pr-9781-tests-1697144294347.md @@ -0,0 +1,5 @@ +--- +"@linode/manager": Tests +--- + +Improve Linode config edit test stability ([#9781](https://github.com/linode/manager/pull/9781)) diff --git a/packages/manager/cypress/support/util/linode-utils.ts b/packages/manager/cypress/support/util/linode-utils.ts index c19d574c54d..a2d617e1012 100644 --- a/packages/manager/cypress/support/util/linode-utils.ts +++ b/packages/manager/cypress/support/util/linode-utils.ts @@ -1,7 +1,7 @@ -import { createLinode, getLinodeConfigs } from '@linode/api-v4'; +import { createLinode, Devices, getLinodeConfigs } from '@linode/api-v4'; import { createLinodeRequestFactory } from '@src/factories'; import { SimpleBackoffMethod } from 'support/util/backoff'; -import { pollLinodeStatus } from 'support/util/polling'; +import { pollLinodeStatus, pollLinodeDiskStatuses } from 'support/util/polling'; import { randomLabel } from 'support/util/random'; import { chooseRegion } from 'support/util/regions'; @@ -58,6 +58,18 @@ export const createLinodeAndGetConfig = async ({ }) )); + // If we don't wait for the Linode to boot, we wait for the disks to be ready. + // Wait 7.5 seconds, then poll the Linode disks every 5 seconds until they are ready. + !waitForLinodeToBeRunning && + (await pollLinodeDiskStatuses( + linode.id, + 'ready', + new SimpleBackoffMethod(5000, { + initialDelay: 7500, + maxAttempts: 25, + }) + )); + // Throw if Linode has no config. if (!configs[0] || !linode.id) { throw new Error('Created Linode does not have any config'); diff --git a/packages/manager/cypress/support/util/polling.ts b/packages/manager/cypress/support/util/polling.ts index f10a5f9ac86..f1c23aaf740 100644 --- a/packages/manager/cypress/support/util/polling.ts +++ b/packages/manager/cypress/support/util/polling.ts @@ -2,14 +2,20 @@ * @file Utilities for polling APIs and other resources. */ -import { +import type { + Disk, ImageStatus, LinodeStatus, + VolumeStatus, +} from '@linode/api-v4'; + +import { pageSize } from 'support/constants/api'; +import { getImage, getLinode, getLinodeDisk, - VolumeStatus, getVolume, + getLinodeDisks, } from '@linode/api-v4'; import { @@ -18,9 +24,12 @@ import { FibonacciBackoffMethod, attemptWithBackoff, } from './backoff'; +import { depaginate } from './paginate'; -// / Describes a backoff configuration for a poll. This may be a partial BackoffOptions object, -// / an instance of a BackoffMethod implementation, or undefined. +/** + * Describes a backoff configuration for a poll. This may be a partial BackoffOptions object, + * an instance of a BackoffMethod implementation, or undefined. + */ export type PollBackoffConfiguration = | BackoffMethod | Partial @@ -118,6 +127,32 @@ export const pollLinodeStatus = async ( return poll(getLinodeStatus, checkLinodeStatus, backoffOptions, label); }; +/** + * Polls the status of a Linode's disks until all of them are in the desired state. + * + * @param linodeId - ID of Linode containing the disks to poll. + * @param desiredStatus - Desired status of the disks that are being polled. + * @param backoffMethod - Backoff method implementation to manage re-attempts. + * @param label - Optional label to assign to poll for logging and troubleshooting. + * + * @returns A Promise that resolves to an array of disks or rejects on timeout. + */ +export const pollLinodeDiskStatuses = async ( + linodeId: number, + desiredStatus: string, + backoffOptions: PollBackoffConfiguration = undefined, + label: string | undefined = undefined +) => { + const getDisks = async () => + depaginate((page) => + getLinodeDisks(linodeId, { page, page_size: pageSize }) + ); + const checkDisksStatus = (disks: Disk[]): boolean => + disks.every((disk: Disk) => disk.status === desiredStatus); + + return poll(getDisks, checkDisksStatus, backoffOptions, label); +}; + /** * Polls the size of a Linode disk until it is the given size. * From cc736469d5224577020a0bdfaa53161eb0ad96d2 Mon Sep 17 00:00:00 2001 From: jdamore-linode <97627410+jdamore-linode@users.noreply.github.com> Date: Mon, 16 Oct 2023 12:35:51 -0400 Subject: [PATCH 10/45] test: Improve Cypress error messages when Linode API errors occur (#9777) * Improve error messages when Linode API errors occur * Added changeset: Improve error message display when Linode API error occurs in Cypress test --- .../.changeset/pr-9777-tests-1696958372039.md | 5 + .../cypress/support/setup/defer-command.ts | 173 +++++++++++++++--- 2 files changed, 150 insertions(+), 28 deletions(-) create mode 100644 packages/manager/.changeset/pr-9777-tests-1696958372039.md diff --git a/packages/manager/.changeset/pr-9777-tests-1696958372039.md b/packages/manager/.changeset/pr-9777-tests-1696958372039.md new file mode 100644 index 00000000000..57ae62add0f --- /dev/null +++ b/packages/manager/.changeset/pr-9777-tests-1696958372039.md @@ -0,0 +1,5 @@ +--- +"@linode/manager": Tests +--- + +Improve error message display when Linode API error occurs in Cypress test ([#9777](https://github.com/linode/manager/pull/9777)) diff --git a/packages/manager/cypress/support/setup/defer-command.ts b/packages/manager/cypress/support/setup/defer-command.ts index be9ffd0bcc3..9b6c74fe3ce 100644 --- a/packages/manager/cypress/support/setup/defer-command.ts +++ b/packages/manager/cypress/support/setup/defer-command.ts @@ -1,28 +1,145 @@ -// *********************************************** -// This example commands.js shows you how to -// create various custom commands and overwrite -// existing commands. -// -// For more comprehensive examples of custom -// commands please read more here: -// https://on.cypress.io/custom-commands -// *********************************************** -// -// -// -- This is a parent command -- -// Cypress.Commands.add("login", (email, password) => { ... }) -// -// -// -- This is a child command -- -// Cypress.Commands.add("drag", { prevSubject: 'element'}, (subject, options) => { ... }) -// -// -// -- This is a dual command -- -// Cypress.Commands.add("dismiss", { prevSubject: 'optional'}, (subject, options) => { ... }) -// -// -// -- This will overwrite an existing command -- -// Cypress.Commands.overwrite("visit", (originalFn, url, options) => { ... }) +import type { AxiosError } from 'axios'; +import type { APIError } from '@linode/api-v4'; + +type LinodeApiV4Error = { + errors: APIError[]; +}; + +/** + * Returns `true` if the given error is a Linode API schema validation error. + * + * Type guards `e` as an array of `APIError` objects. + * + * @param e - Error. + * + * @returns `true` if `e` is a Linode API schema validation error. + */ +const isValidationError = (e: any): e is APIError[] => { + // When a Linode APIv4 schema validation error occurs, an array of `APIError` + // objects is thrown rather than a typical `Error` type. + return ( + Array.isArray(e) && + e.every((item: any) => { + return 'reason' in item; + }) + ); +}; + +/** + * Returns `true` if the given error is an Axios error. + * + * Type guards `e` as an `AxiosError` instance. + * + * @param e - Error. + * + * @returns `true` if `e` is an `AxiosError`. + */ +const isAxiosError = (e: any): e is AxiosError => { + return !!e.isAxiosError; +}; + +/** + * Returns `true` if the given error is a Linode API v4 request error. + * + * Type guards `e` as an `AxiosError` instance. + * + * @param e - Error. + * + * @returns `true` if `e` is a Linode API v4 request error. + */ +const isLinodeApiError = (e: any): e is AxiosError => { + if (isAxiosError(e)) { + const errorData = e.response?.data?.errors; + return ( + Array.isArray(errorData) && + errorData.every((item: any) => { + return 'reason' in item; + }) + ); + } + return false; +}; + +/** + * Detects known error types and returns a new Error with more detailed message. + * + * Unknown error types are returned without modification. + * + * @param e - Error. + * + * @returns A new error with added information in message, or `e`. + */ +const enhanceError = (e: any) => { + // Check for most specific error types first. + if (isLinodeApiError(e)) { + // If `e` is a Linode APIv4 error response, show the status code, error messages, + // and request URL when applicable. + const summary = !!e.response?.status + ? `Linode APIv4 request failed with status code ${e.response.status}` + : `Linode APIv4 request failed`; + + const errorDetails = e.response!.data.errors.map((error: APIError) => { + return error.field + ? `- ${error.reason} (field '${error.field}')` + : `- ${error.reason}`; + }); + + const requestInfo = + !!e.request?.responseURL && !!e.config.method + ? `\nRequest: ${e.config.method.toUpperCase()} ${e.request.responseURL}` + : ''; + + return new Error(`${summary}\n${errorDetails.join('\n')}${requestInfo}`); + } + + if (isAxiosError(e)) { + // If `e` is an Axios error (but not a Linode API error specifically), show the + // status code, error messages, and request URL when applicable. + const summary = !!e.response?.status + ? `Request failed with status code ${e.response.status}` + : `Request failed`; + + const requestInfo = + !!e.request?.responseURL && !!e.config.method + ? `\nRequest: ${e.config.method.toUpperCase()} ${e.request.responseURL}` + : ''; + + return new Error(`${summary}${requestInfo}`); + } + + // Handle cases where a validation error is thrown. + // These are arrays containing `APIError` objects; no additional request context + // is included so we only have the validation error messages themselves to work with. + if (isValidationError(e)) { + // Validation errors do not contain any additional context (request URL, payload, etc.). + // Show the validation error messages instead. + const multipleErrors = e.length > 1; + const summary = multipleErrors + ? 'Request failed with Linode schema validation errors' + : 'Request failed with Linode schema validation error'; + + // Format, accounting for 0, 1, or more errors. + const validationErrorMessage = multipleErrors + ? e + .map((error) => + error.field + ? `- ${error.reason} (field '${error.field}')` + : `- ${error.reason}` + ) + .join('\n') + : e + .map((error) => + error.field + ? `${error.reason} (field '${error.field}')` + : `${error.reason}` + ) + .join('\n'); + + return new Error(`${summary}\n${validationErrorMessage}`); + } + // Return `e` unmodified if it's not handled by any of the above cases. + return e; +}; /** * Describes an object which can contain a label. @@ -87,9 +204,9 @@ Cypress.Commands.add( let result: T; try { result = await promise; - } catch (e) { - commandLog.end(); - throw e; + } catch (e: any) { + commandLog.error(e); + throw enhanceError(e); } commandLog.end(); return result; From 31a89acc90eb66da6ce880d8397a822fe7901e07 Mon Sep 17 00:00:00 2001 From: Alban Bailly <130582365+abailly-akamai@users.noreply.github.com> Date: Tue, 17 Oct 2023 09:04:25 -0400 Subject: [PATCH 11/45] fix: [M3-7081] - Quote variable in changeset shell command (#9791) * Quote variable in changeset shell * Added changeset: Quote variable in changeset shell command --- packages/manager/.changeset/pr-9791-fixed-1697461463011.md | 5 +++++ scripts/changelog/changeset.mjs | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) create mode 100644 packages/manager/.changeset/pr-9791-fixed-1697461463011.md diff --git a/packages/manager/.changeset/pr-9791-fixed-1697461463011.md b/packages/manager/.changeset/pr-9791-fixed-1697461463011.md new file mode 100644 index 00000000000..122a7e561c4 --- /dev/null +++ b/packages/manager/.changeset/pr-9791-fixed-1697461463011.md @@ -0,0 +1,5 @@ +--- +"@linode/manager": Fixed +--- + +Quote variable in changeset shell command ([#9791](https://github.com/linode/manager/pull/9791)) diff --git a/scripts/changelog/changeset.mjs b/scripts/changelog/changeset.mjs index 9dd86e401f4..767e3d8a713 100644 --- a/scripts/changelog/changeset.mjs +++ b/scripts/changelog/changeset.mjs @@ -100,7 +100,7 @@ async function generateChangeset() { } try { - const addCmd = `git add ${changesetFile}`; + const addCmd = `git add "${changesetFile}"`; // This allows backticks in the commit message. We first need to sanitize against any number of backslashes // that appear before backtick in description, to avoid having unescaped characters. Then we can add back // two backslashes before the backtick to make sure backticks show up in the commit message. From 8dee619625fd1c561aae8c38af1c52cfdaffe0cd Mon Sep 17 00:00:00 2001 From: jdamore-linode <97627410+jdamore-linode@users.noreply.github.com> Date: Tue, 17 Oct 2023 10:09:21 -0400 Subject: [PATCH 12/45] test: Resolve warnings and errors in Jest tests (#9784) * Clean up warnings by mocking API requests * Fix longview test overlapping act() warnings * Fix PlanSelection test warning by wrapping table row components in a table body * Mock snackbar hook to resolve warning about updating state on unmounted component * Suppress React Query errors in tests where errors are expected * Added changeset: Clean up Jest warnings and errors --- .../.changeset/pr-9784-tests-1697487553929.md | 5 +++ .../ClusterList/ClusterActionMenu.test.tsx | 10 +++++ .../LinodesCreate/AddonsPanel.test.tsx | 11 ++++- .../LongviewLanding/LongviewPlans.test.tsx | 12 +++-- .../BucketLanding/BucketLanding.test.tsx | 44 ++++++++++++++++--- .../BucketLanding/CreateBucketDrawer.test.tsx | 26 ++++++++++- .../features/Search/SearchLanding.test.tsx | 16 ++++--- .../PlansPanel/PlanSelection.test.tsx | 37 +++++++++------- 8 files changed, 130 insertions(+), 31 deletions(-) create mode 100644 packages/manager/.changeset/pr-9784-tests-1697487553929.md diff --git a/packages/manager/.changeset/pr-9784-tests-1697487553929.md b/packages/manager/.changeset/pr-9784-tests-1697487553929.md new file mode 100644 index 00000000000..3c7a913cb49 --- /dev/null +++ b/packages/manager/.changeset/pr-9784-tests-1697487553929.md @@ -0,0 +1,5 @@ +--- +"@linode/manager": Tests +--- + +Clean up Jest warnings and errors ([#9784](https://github.com/linode/manager/pull/9784)) diff --git a/packages/manager/src/features/Kubernetes/ClusterList/ClusterActionMenu.test.tsx b/packages/manager/src/features/Kubernetes/ClusterList/ClusterActionMenu.test.tsx index 24e18c42b84..8a71f812491 100644 --- a/packages/manager/src/features/Kubernetes/ClusterList/ClusterActionMenu.test.tsx +++ b/packages/manager/src/features/Kubernetes/ClusterList/ClusterActionMenu.test.tsx @@ -9,6 +9,16 @@ import { ClusterActionMenu } from './ClusterActionMenu'; const mockGetKubeConfig = jest.spyOn(kube, 'getKubeConfig'); +// Mock snackbar to prevent "Can't perform a React state update on an unmounted component" warning. +jest.mock('notistack', () => ({ + ...jest.requireActual('notistack'), + useSnackbar: () => { + return { + enqueueSnackbar: jest.fn(), + }; + }, +})); + const props = { closeSnackbar: jest.fn(), clusterId: 123456, diff --git a/packages/manager/src/features/Linodes/LinodesCreate/AddonsPanel.test.tsx b/packages/manager/src/features/Linodes/LinodesCreate/AddonsPanel.test.tsx index 9ce6f352b4b..b2eaaa53365 100644 --- a/packages/manager/src/features/Linodes/LinodesCreate/AddonsPanel.test.tsx +++ b/packages/manager/src/features/Linodes/LinodesCreate/AddonsPanel.test.tsx @@ -1,11 +1,12 @@ import { waitFor } from '@testing-library/react'; import React from 'react'; -import { linodeTypeFactory } from 'src/factories/linodes'; import { getMonthlyBackupsPrice } from 'src/utilities/pricing/backups'; import { renderWithTheme, wrapWithTheme } from 'src/utilities/testHelpers'; +import { rest, server } from 'src/mocks/testServer'; import { AddonsPanel, AddonsPanelProps } from './AddonsPanel'; +import { imageFactory, linodeTypeFactory } from 'src/factories'; const type = linodeTypeFactory.build({ addons: { @@ -172,6 +173,14 @@ const vlanAccordionTestId = 'vlan-accordion'; const attachVLANTestId = 'attach-vlan'; describe('AddonsPanel', () => { + beforeEach(() => { + server.use( + rest.get('*/images/*', (req, res, ctx) => { + return res(ctx.json(imageFactory.build())); + }) + ); + }); + it('should render AddonsPanel', () => { renderWithTheme(); }); diff --git a/packages/manager/src/features/Longview/LongviewLanding/LongviewPlans.test.tsx b/packages/manager/src/features/Longview/LongviewLanding/LongviewPlans.test.tsx index 1a8c53fd3dc..5733e6ff3a2 100644 --- a/packages/manager/src/features/Longview/LongviewLanding/LongviewPlans.test.tsx +++ b/packages/manager/src/features/Longview/LongviewLanding/LongviewPlans.test.tsx @@ -77,7 +77,7 @@ describe('LongviewPlans', () => { it('renders all columns for all plan types', async () => { renderWithTheme(); - testRow( + await testRow( LONGVIEW_FREE_ID, 'Longview Free', '10', @@ -86,8 +86,12 @@ describe('LongviewPlans', () => { 'FREE' ); - mockLongviewSubscriptions.forEach((sub) => { - testRow( + // We can't execute `testRow()` concurrently because it leads to concurrent + // calls to `act()`, which can lead to undefined behavior. + for (let i = 0; i < mockLongviewSubscriptions.length; i += 1) { + const sub = mockLongviewSubscriptions[i]; + // eslint-disable-next-line no-await-in-loop + await testRow( sub.id, sub.label, String(sub.clients_included), @@ -95,7 +99,7 @@ describe('LongviewPlans', () => { 'Every minute', formatPrice(sub.price) ); - }); + } }); it('highlights the LV subscription currently on the account', async () => { diff --git a/packages/manager/src/features/ObjectStorage/BucketLanding/BucketLanding.test.tsx b/packages/manager/src/features/ObjectStorage/BucketLanding/BucketLanding.test.tsx index 99b95cdfd6b..af8be40e301 100644 --- a/packages/manager/src/features/ObjectStorage/BucketLanding/BucketLanding.test.tsx +++ b/packages/manager/src/features/ObjectStorage/BucketLanding/BucketLanding.test.tsx @@ -1,6 +1,6 @@ import { screen, waitForElementToBeRemoved } from '@testing-library/react'; import * as React from 'react'; -import { QueryClient } from 'react-query'; +import { QueryClient, setLogger } from 'react-query'; import { objectStorageBucketFactory, @@ -17,11 +17,13 @@ const queryClient = new QueryClient({ defaultOptions: { queries: queryPresets.oneTimeFetch }, }); -afterEach(() => { - queryClient.clear(); -}); - describe('ObjectStorageLanding', () => { + afterEach(() => { + queryClient.clear(); + // If necessary, reset React Query logger. + setLogger(console); + }); + it('renders a loading state', () => { // Mock Buckets server.use( @@ -59,6 +61,22 @@ describe('ObjectStorageLanding', () => { }); it('renders per-cluster errors', async () => { + // Suppress logging React Query errors to CLI since this test is expected + // to trigger errors. + // + // Note: Logging options improved in React Query v4 and `setLogger` will + // be removed in v5. We will be able to accomplish this more cleanly once + // we upgrade. + // + // See also: + // - https://github.com/TanStack/query/issues/125 + // - https://github.com/TanStack/query/discussions/4252 + setLogger({ + log: () => {}, + warn: () => {}, + error: () => {}, + }); + objectStorageBucketFactory.resetSequenceNumber(); objectStorageClusterFactory.resetSequenceNumber(); @@ -103,6 +121,22 @@ describe('ObjectStorageLanding', () => { }); it('renders general error state', async () => { + // Suppress logging React Query errors to CLI since this test is expected + // to trigger errors. + // + // Note: Logging options improved in React Query v4 and `setLogger` will + // be removed in v5. We will be able to accomplish this more cleanly once + // we upgrade. + // + // See also: + // - https://github.com/TanStack/query/issues/125 + // - https://github.com/TanStack/query/discussions/4252 + setLogger({ + log: () => {}, + warn: () => {}, + error: () => {}, + }); + // Mock Clusters server.use( rest.get('*/object-storage/clusters', (req, res, ctx) => { diff --git a/packages/manager/src/features/ObjectStorage/BucketLanding/CreateBucketDrawer.test.tsx b/packages/manager/src/features/ObjectStorage/BucketLanding/CreateBucketDrawer.test.tsx index 7933a516df5..cf20fc70a6b 100644 --- a/packages/manager/src/features/ObjectStorage/BucketLanding/CreateBucketDrawer.test.tsx +++ b/packages/manager/src/features/ObjectStorage/BucketLanding/CreateBucketDrawer.test.tsx @@ -10,9 +10,12 @@ import { import { makeResourcePage } from 'src/mocks/serverHandlers'; import { rest, server } from 'src/mocks/testServer'; import { renderWithTheme } from 'src/utilities/testHelpers'; +import { QueryClient, setLogger } from 'react-query'; import { CreateBucketDrawer } from './CreateBucketDrawer'; +const queryClient = new QueryClient(); + const props = { isOpen: true, onClose: jest.fn(), @@ -21,7 +24,28 @@ const props = { jest.mock('src/components/EnhancedSelect/Select'); describe('CreateBucketDrawer', () => { + afterEach(() => { + // Reset React Query logger. + setLogger(console); + }); + it('Should show a general error notice if the API returns one', async () => { + // Suppress logging React Query errors to CLI since this test is expected + // to trigger errors. + // + // Note: Logging options improved in React Query v4 and `setLogger` will + // be removed in v5. We will be able to accomplish this more cleanly once + // we upgrade. + // + // See also: + // - https://github.com/TanStack/query/issues/125 + // - https://github.com/TanStack/query/discussions/4252 + setLogger({ + log: () => {}, + warn: () => {}, + error: () => {}, + }); + server.use( rest.post('*/object-storage/buckets', (req, res, ctx) => { return res( @@ -62,7 +86,7 @@ describe('CreateBucketDrawer', () => { getByLabelText, getByPlaceholderText, getByTestId, - } = renderWithTheme(); + } = renderWithTheme(, { queryClient }); userEvent.type(getByLabelText('Label'), 'my-test-bucket'); diff --git a/packages/manager/src/features/Search/SearchLanding.test.tsx b/packages/manager/src/features/Search/SearchLanding.test.tsx index ce6ca4bff74..79ce686a10b 100644 --- a/packages/manager/src/features/Search/SearchLanding.test.tsx +++ b/packages/manager/src/features/Search/SearchLanding.test.tsx @@ -8,6 +8,7 @@ import { searchbarResult1 } from 'src/__data__/searchResults'; import { makeResourcePage } from 'src/mocks/serverHandlers'; import { rest, server } from 'src/mocks/testServer'; import { renderWithTheme, wrapWithTheme } from 'src/utilities/testHelpers'; +import { linodeTypeFactory } from 'src/factories'; import { CombinedProps as Props, SearchLanding } from './SearchLanding'; import { emptyResults } from './utils'; @@ -30,11 +31,16 @@ const propsWithResults: Props = { const queryClient = new QueryClient(); describe('Component', () => { - server.use( - rest.get('*/domains', (req, res, ctx) => { - return res(ctx.json(makeResourcePage([]))); - }) - ); + beforeEach(() => { + server.use( + rest.get('*/domains', (req, res, ctx) => { + return res(ctx.json(makeResourcePage([]))); + }), + rest.get('*/linode/types/*', (req, res, ctx) => { + return res(ctx.json(linodeTypeFactory.build())); + }) + ); + }); it('should render', async () => { const { findByText } = renderWithTheme(); diff --git a/packages/manager/src/features/components/PlansPanel/PlanSelection.test.tsx b/packages/manager/src/features/components/PlansPanel/PlanSelection.test.tsx index 94c751867bc..a9ffc8cfe27 100644 --- a/packages/manager/src/features/components/PlansPanel/PlanSelection.test.tsx +++ b/packages/manager/src/features/components/PlansPanel/PlanSelection.test.tsx @@ -5,6 +5,7 @@ import { planSelectionTypeFactory } from 'src/factories/types'; import { breakpoints } from 'src/foundations/breakpoints'; import { resizeScreenSize } from 'src/utilities/testHelpers'; import { renderWithTheme } from 'src/utilities/testHelpers'; +import { wrapWithTableBody } from 'src/utilities/testHelpers'; import { PlanSelection } from './PlanSelection'; @@ -19,13 +20,15 @@ describe('PlanSelection (table, desktop)', () => { it('renders the table row', () => { const { container } = renderWithTheme( - jest.fn()} - type={mockPlan} - /> + wrapWithTableBody( + jest.fn()} + type={mockPlan} + /> + ) ); expect(container.querySelector('[data-qa-plan-row]')).toBeInTheDocument(); @@ -49,7 +52,9 @@ describe('PlanSelection (table, desktop)', () => { const mockOnSelect = jest.fn(); const { getByRole } = renderWithTheme( - + wrapWithTableBody( + + ) ); const radioInput = getByRole('radio'); @@ -60,13 +65,15 @@ describe('PlanSelection (table, desktop)', () => { it('shows the dynamic prices with dcSpecificPricing ON', () => { const { container } = renderWithTheme( - {}} - selectedRegionId={'br-gru'} - type={mockPlan} - /> + wrapWithTableBody( + {}} + selectedRegionId={'br-gru'} + type={mockPlan} + /> + ) ); expect(container.querySelector('[data-qa-plan-row]')).toBeInTheDocument(); From 609c64b7900ab8980a0afc135a25852fab18c938 Mon Sep 17 00:00:00 2001 From: Banks Nussman <115251059+bnussman-akamai@users.noreply.github.com> Date: Tue, 17 Oct 2023 11:27:24 -0400 Subject: [PATCH 13/45] feat: [M3-7254] - Add AGLB Routes - Rule Edit Drawer (#9778) * initial edit flow and e2e test * fix notice regression * a few small fixes * Added changeset: Add AGLB Routes - Rule Edit Drawer * add empty state for no service targets --------- Co-authored-by: Banks Nussman --- ...pr-9778-upcoming-features-1696961231870.md | 5 ++ .../load-balancer-routes.spec.ts | 73 ++++++++++++++++++- .../src/components/Notice/Notice.styles.ts | 5 +- .../LoadBalancerDetail/LoadBalancerRoutes.tsx | 23 +++++- .../{AddRuleDrawer.tsx => RuleDrawer.tsx} | 44 +++++++++-- .../LoadBalancerDetail/RulesTable.test.tsx | 5 +- .../LoadBalancerDetail/RulesTable.tsx | 54 +++++++------- packages/manager/src/mocks/serverHandlers.ts | 7 +- 8 files changed, 170 insertions(+), 46 deletions(-) create mode 100644 packages/manager/.changeset/pr-9778-upcoming-features-1696961231870.md rename packages/manager/src/features/LoadBalancers/LoadBalancerDetail/Routes/{AddRuleDrawer.tsx => RuleDrawer.tsx} (94%) diff --git a/packages/manager/.changeset/pr-9778-upcoming-features-1696961231870.md b/packages/manager/.changeset/pr-9778-upcoming-features-1696961231870.md new file mode 100644 index 00000000000..e2fa1a175bd --- /dev/null +++ b/packages/manager/.changeset/pr-9778-upcoming-features-1696961231870.md @@ -0,0 +1,5 @@ +--- +"@linode/manager": Upcoming Features +--- + +Add AGLB Routes - Rule Edit Drawer ([#9778](https://github.com/linode/manager/pull/9778)) diff --git a/packages/manager/cypress/e2e/core/loadBalancers/load-balancer-routes.spec.ts b/packages/manager/cypress/e2e/core/loadBalancers/load-balancer-routes.spec.ts index 24d8f206664..81a7980815c 100644 --- a/packages/manager/cypress/e2e/core/loadBalancers/load-balancer-routes.spec.ts +++ b/packages/manager/cypress/e2e/core/loadBalancers/load-balancer-routes.spec.ts @@ -217,7 +217,78 @@ describe('Akamai Global Load Balancer routes page', () => { cy.wait('@updateRoute'); }); - it('surfaces API errors in the Add Rule Drawer', () => { + it('can edit a HTTP rule', () => { + const loadbalancer = loadbalancerFactory.build(); + const routes = routeFactory.buildList(1, { protocol: 'http' }); + const serviceTargets = serviceTargetFactory.buildList(3); + + mockAppendFeatureFlags({ + aglb: makeFeatureFlagData(true), + }).as('getFeatureFlags'); + mockGetFeatureFlagClientstream().as('getClientStream'); + mockGetLoadBalancer(loadbalancer).as('getLoadBalancer'); + mockGetLoadBalancerRoutes(loadbalancer.id, routes).as('getRoutes'); + mockGetLoadBalancerServiceTargets(loadbalancer.id, serviceTargets).as( + 'getServiceTargets' + ); + + cy.visitWithLogin(`/loadbalancers/${loadbalancer.id}/routes`); + cy.wait([ + '@getFeatureFlags', + '@getClientStream', + '@getLoadBalancer', + '@getRoutes', + ]); + + cy.findByLabelText(`route-${routes[0].id} expand row`).click(); + + ui.actionMenu.findByTitle('Action Menu for Rule 0').click(); + + ui.actionMenuItem.findByTitle('Edit').click(); + + mockUpdateRoute(loadbalancer, routes[0]).as('updateRoute'); + + ui.drawer + .findByTitle('Edit Rule') + .should('be.visible') + .within(() => { + cy.findByLabelText('Hostname') + .should('have.value', routes[0].rules[0].match_condition.hostname) + .clear() + .type('example.com'); + + cy.findByLabelText('Match Type') + .should('be.visible') + .click() + .clear() + .type('Header'); + + ui.autocompletePopper + .findByTitle('HTTP Header') + .should('be.visible') + .click(); + + cy.findByLabelText('Match Value') + .should('have.value', routes[0].rules[0].match_condition.match_value) + .clear() + .type('x-header=my-header-value'); + + ui.buttonGroup + .findButtonByTitle('Save') + .should('be.visible') + .should('be.enabled') + .click(); + }); + + cy.wait('@updateRoute'); + + // Verify the table updates after the drawer saves and closes + cy.findByLabelText('Rule 0').within(() => { + cy.findByText('x-header=my-header-value'); + cy.findByText('HTTP Header'); + }); + }); + it('surfaces API errors in the Add Rule Drawer for an HTTP route', () => { const loadbalancer = loadbalancerFactory.build(); const routes = routeFactory.buildList(1, { protocol: 'http' }); const serviceTargets = serviceTargetFactory.buildList(3); diff --git a/packages/manager/src/components/Notice/Notice.styles.ts b/packages/manager/src/components/Notice/Notice.styles.ts index 2b45a97cc28..b27f9b9a8a5 100644 --- a/packages/manager/src/components/Notice/Notice.styles.ts +++ b/packages/manager/src/components/Notice/Notice.styles.ts @@ -28,6 +28,7 @@ export const useStyles = makeStyles< [`& .${classes.noticeText}`]: { fontFamily: theme.font.normal, }, + backgroundColor: theme.bg.bgPaper, }, info: { [`&.${classes.important}`]: { @@ -56,11 +57,7 @@ export const useStyles = makeStyles< [`& .${classes.error}`]: { borderLeftColor: theme.color.red, }, - [`& .${classes.important}`]: { - backgroundColor: theme.bg.bgPaper, - }, alignItems: 'center', - backgroundColor: theme.bg.bgPaper, borderRadius: 1, display: 'flex', fontSize: '1rem', diff --git a/packages/manager/src/features/LoadBalancers/LoadBalancerDetail/LoadBalancerRoutes.tsx b/packages/manager/src/features/LoadBalancers/LoadBalancerDetail/LoadBalancerRoutes.tsx index b9d6d4d5f4f..ee313df1e6d 100644 --- a/packages/manager/src/features/LoadBalancers/LoadBalancerDetail/LoadBalancerRoutes.tsx +++ b/packages/manager/src/features/LoadBalancers/LoadBalancerDetail/LoadBalancerRoutes.tsx @@ -24,8 +24,8 @@ import { useOrder } from 'src/hooks/useOrder'; import { usePagination } from 'src/hooks/usePagination'; import { useLoadBalancerRoutesQuery } from 'src/queries/aglb/routes'; -import { AddRuleDrawer } from './Routes/AddRuleDrawer'; import { DeleteRouteDialog } from './Routes/DeleteRouteDialog'; +import { RuleDrawer } from './Routes/RuleDrawer'; import { RulesTable } from './RulesTable'; import type { Filter, Route } from '@linode/api-v4'; @@ -39,6 +39,7 @@ export const LoadBalancerRoutes = () => { const [query, setQuery] = useState(); const [isDeleteDialogOpen, setIsDeleteDialogOpen] = useState(false); const [selectedRouteId, setSelectedRouteId] = useState(); + const [selectedRuleIndex, setSelectedRuleIndex] = useState(); const pagination = usePagination(1, PREFERENCE_KEY); @@ -78,6 +79,12 @@ export const LoadBalancerRoutes = () => { setSelectedRouteId(route.id); }; + const onEditRule = (route: Route, ruleIndex: number) => { + setIsAddRuleDrawerOpen(true); + setSelectedRouteId(route.id); + setSelectedRuleIndex(ruleIndex); + }; + const onDeleteRoute = (route: Route) => { setIsDeleteDialogOpen(true); setSelectedRouteId(route.id); @@ -124,7 +131,11 @@ export const LoadBalancerRoutes = () => { ); const InnerTable = ( - + onEditRule(route, index)} + route={route} + /> ); return { @@ -216,11 +227,15 @@ export const LoadBalancerRoutes = () => { page={pagination.page} pageSize={pagination.pageSize} /> - { + setIsAddRuleDrawerOpen(false); + setSelectedRuleIndex(undefined); + }} loadbalancerId={Number(loadbalancerId)} - onClose={() => setIsAddRuleDrawerOpen(false)} open={isAddRuleDrawerOpen} route={selectedRoute} + ruleIndexToEdit={selectedRuleIndex} /> void; open: boolean; route: Route | undefined; + ruleIndexToEdit: number | undefined; } -export const AddRuleDrawer = (props: Props) => { - const { loadbalancerId, onClose: _onClose, open, route } = props; +/** + * Drawer used for *adding* and *editing* AGLB rules + */ +export const RuleDrawer = (props: Props) => { + const { + loadbalancerId, + onClose: _onClose, + open, + route, + ruleIndexToEdit, + } = props; + + const ruleIndex = ruleIndexToEdit ?? route?.rules.length ?? 0; - const ruleIndex = route?.rules.length ?? 0; + const isEditMode = ruleIndexToEdit !== undefined; const validationSchema = route?.protocol === 'tcp' ? TCPRuleSchema : HTTPRuleSchema; const { error, + isLoading, mutateAsync: updateRule, reset, } = useLoadBalancerRouteUpdateMutation(loadbalancerId, route?.id ?? -1); @@ -62,13 +75,23 @@ export const AddRuleDrawer = (props: Props) => { const formik = useFormik({ enableReinitialize: true, - initialValues, + initialValues: isEditMode + ? route?.rules[ruleIndexToEdit] ?? initialValues + : initialValues, async onSubmit(rule) { try { const existingRules = route?.rules ?? []; + + // If we are editing, update the rule with the form data. + if (isEditMode) { + existingRules[ruleIndexToEdit] = rule; + } + await updateRule({ protocol: route?.protocol, - rules: [...existingRules, rule], + // If we are editing, send the updated rules, otherwise + // append a new rule to the end. + rules: isEditMode ? existingRules : [...existingRules, rule], }); onClose(); } catch (errors) { @@ -167,7 +190,12 @@ export const AddRuleDrawer = (props: Props) => { .join(', '); return ( - +
    {/** * @todo: AGLB update copy @@ -464,8 +492,8 @@ export const AddRuleDrawer = (props: Props) => { { it('renders table headers', () => { const { getByText } = renderWithTheme( - + ); expect(getByText('Execution')).toBeInTheDocument(); expect(getByText('Match Value')).toBeInTheDocument(); @@ -34,6 +34,7 @@ describe('RulesTable', () => { const { getByText } = renderWithTheme( ); @@ -42,7 +43,7 @@ describe('RulesTable', () => { it('renders rules correctly', () => { const { getByText } = renderWithTheme( - + ); expect(getByText('First')).toBeInTheDocument(); diff --git a/packages/manager/src/features/LoadBalancers/LoadBalancerDetail/RulesTable.tsx b/packages/manager/src/features/LoadBalancers/LoadBalancerDetail/RulesTable.tsx index 40c278b7d01..dfbd39a8fb4 100644 --- a/packages/manager/src/features/LoadBalancers/LoadBalancerDetail/RulesTable.tsx +++ b/packages/manager/src/features/LoadBalancers/LoadBalancerDetail/RulesTable.tsx @@ -28,6 +28,7 @@ import type { MatchField, Route } from '@linode/api-v4'; interface Props { loadbalancerId: number; + onEditRule: (ruleIndex: number) => void; route: Route; } @@ -42,7 +43,7 @@ const matchFieldMap: Record = { const screenReaderMessage = 'Some screen readers may require you to enter focus mode to interact with Loadbalancer rule list items. In focus mode, press spacebar to begin a drag or tab to access item actions.'; -export const RulesTable = ({ loadbalancerId, route }: Props) => { +export const RulesTable = ({ loadbalancerId, onEditRule, route }: Props) => { const { label, protocol, rules } = route; const theme = useTheme(); const { enqueueSnackbar } = useSnackbar(); @@ -146,10 +147,7 @@ export const RulesTable = ({ loadbalancerId, route }: Props) => { > {(provided) => (
  • { {...provided.dragHandleProps} > @@ -219,22 +216,26 @@ export const RulesTable = ({ loadbalancerId, route }: Props) => { }} aria-label={`Service Targets: ${rule.service_targets.length}`} > - - {rule.service_targets.map( - ({ id, label }) => ( -
    - {label}:{id} -
    - ) - )} - - } - /> + {rule.service_targets.length > 0 ? ( + + {rule.service_targets.map( + ({ id, label }) => ( +
    + {label}:{id} +
    + ) + )} + + } + /> + ) : ( + 'None' + )} @@ -253,7 +254,7 @@ export const RulesTable = ({ loadbalancerId, route }: Props) => { }} > {rule.match_condition - .session_stickiness_cookie && + .session_stickiness_cookie || rule.match_condition.session_stickiness_ttl ? 'Yes' : 'No'} @@ -270,7 +271,10 @@ export const RulesTable = ({ loadbalancerId, route }: Props) => { {/** TODO: AGLB: The Edit and Delete Action menu behavior should be implemented in future AGLB tickets. */} null, title: 'Edit' }, + { + onClick: () => onEditRule(index), + title: 'Edit', + }, { disabled: index === 0, onClick: () => handleMoveUp(index), @@ -283,7 +287,7 @@ export const RulesTable = ({ loadbalancerId, route }: Props) => { }, { onClick: () => null, title: 'Remove' }, ]} - ariaLabel={`Action Menu for Rule ${rule.match_condition.match_value}`} + ariaLabel={`Action Menu for Rule ${index}`} />
    diff --git a/packages/manager/src/mocks/serverHandlers.ts b/packages/manager/src/mocks/serverHandlers.ts index 7417e168484..827368a93d0 100644 --- a/packages/manager/src/mocks/serverHandlers.ts +++ b/packages/manager/src/mocks/serverHandlers.ts @@ -354,7 +354,7 @@ const aglb = [ }), // Routes rest.get('*/v4beta/aglb/:id/routes', (req, res, ctx) => { - return res(ctx.json(makeResourcePage(routeFactory.buildList(1)))); + return res(ctx.json(makeResourcePage(routeFactory.buildList(5)))); }), rest.post('*/v4beta/aglb/:id/routes', (req, res, ctx) => { return res(ctx.json(createRouteFactory.buildList(4))); @@ -362,7 +362,10 @@ const aglb = [ rest.put('*/v4beta/aglb/:id/routes/:routeId', (req, res, ctx) => { const id = Number(req.params.routeId); const body = req.body as any; - return res(ctx.json(createRouteFactory.build({ id, ...body }))); + return res( + ctx.delay(1000), + ctx.json(createRouteFactory.build({ id, ...body })) + ); }), rest.delete('*/v4beta/aglb/:id/routes/:routeId', (req, res, ctx) => { return res(ctx.json({})); From 8b581a860d3b70ba73e86469eec8979688fdbd98 Mon Sep 17 00:00:00 2001 From: jdamore-linode <97627410+jdamore-linode@users.noreply.github.com> Date: Tue, 17 Oct 2023 11:55:02 -0400 Subject: [PATCH 14/45] test: Fix DBaaS tests after API update (#9801) * Add mock util and constants to support DBaaS node type mocking * Fix DBaaS tests by mocking node types requests * Added changeset: Fix DBaaS UI test failures stemming from API update --- .../.changeset/pr-9801-tests-1697547109194.md | 5 +++++ .../core/databases/create-database.spec.ts | 5 ++++- .../core/databases/delete-database.spec.ts | 8 ++++++-- .../core/databases/update-database.spec.ts | 10 +++++++--- .../cypress/support/constants/databases.ts | 18 +++++++++++++++-- .../cypress/support/intercepts/databases.ts | 20 ++++++++++++++++++- 6 files changed, 57 insertions(+), 9 deletions(-) create mode 100644 packages/manager/.changeset/pr-9801-tests-1697547109194.md diff --git a/packages/manager/.changeset/pr-9801-tests-1697547109194.md b/packages/manager/.changeset/pr-9801-tests-1697547109194.md new file mode 100644 index 00000000000..5a0ef905660 --- /dev/null +++ b/packages/manager/.changeset/pr-9801-tests-1697547109194.md @@ -0,0 +1,5 @@ +--- +"@linode/manager": Tests +--- + +Fix DBaaS UI test failures stemming from API update ([#9801](https://github.com/linode/manager/pull/9801)) diff --git a/packages/manager/cypress/e2e/core/databases/create-database.spec.ts b/packages/manager/cypress/e2e/core/databases/create-database.spec.ts index a93a49986d5..4001ff3204b 100644 --- a/packages/manager/cypress/e2e/core/databases/create-database.spec.ts +++ b/packages/manager/cypress/e2e/core/databases/create-database.spec.ts @@ -4,11 +4,13 @@ import { databaseClusterConfiguration, databaseConfigurations, mockDatabaseEngineTypes, + mockDatabaseNodeTypes, } from 'support/constants/databases'; import { mockCreateDatabase, mockGetDatabases, mockGetDatabaseEngines, + mockGetDatabaseTypes, } from 'support/intercepts/databases'; import { mockGetEvents } from 'support/intercepts/events'; import { getRegionById } from 'support/util/regions'; @@ -71,9 +73,10 @@ describe('create a database cluster, mocked data', () => { ); mockCreateDatabase(databaseMock).as('createDatabase'); mockGetDatabases([databaseMock]).as('getDatabases'); + mockGetDatabaseTypes(mockDatabaseNodeTypes).as('getDatabaseTypes'); cy.visitWithLogin('/databases/create'); - cy.wait(['@getAccount', '@getDatabaseEngines']); + cy.wait(['@getAccount', '@getDatabaseEngines', '@getDatabaseTypes']); ui.entityHeader .find() diff --git a/packages/manager/cypress/e2e/core/databases/delete-database.spec.ts b/packages/manager/cypress/e2e/core/databases/delete-database.spec.ts index 1f14c4e1091..453e50248c7 100644 --- a/packages/manager/cypress/e2e/core/databases/delete-database.spec.ts +++ b/packages/manager/cypress/e2e/core/databases/delete-database.spec.ts @@ -9,11 +9,13 @@ import { mockDeleteDatabase, mockDeleteProvisioningDatabase, mockGetDatabase, + mockGetDatabaseTypes, } from 'support/intercepts/databases'; import { ui } from 'support/ui'; import { databaseClusterConfiguration, databaseConfigurations, + mockDatabaseNodeTypes, } from 'support/constants/databases'; describe('Delete database clusters', () => { @@ -40,12 +42,13 @@ describe('Delete database clusters', () => { // Mock account to ensure 'Managed Databases' capability. mockGetAccount(accountFactory.build()).as('getAccount'); mockGetDatabase(database).as('getDatabase'); + mockGetDatabaseTypes(mockDatabaseNodeTypes).as('getDatabaseTypes'); mockDeleteDatabase(database.id, database.engine).as('deleteDatabase'); cy.visitWithLogin( `/databases/${database.engine}/${database.id}/settings` ); - cy.wait(['@getAccount', '@getDatabase']); + cy.wait(['@getAccount', '@getDatabase', '@getDatabaseTypes']); // Click "Delete Cluster" button. ui.button @@ -97,6 +100,7 @@ describe('Delete database clusters', () => { mockGetAccount(accountFactory.build()).as('getAccount'); mockGetDatabase(database).as('getDatabase'); + mockGetDatabaseTypes(mockDatabaseNodeTypes).as('getDatabaseTypes'); mockDeleteProvisioningDatabase( database.id, database.engine, @@ -106,7 +110,7 @@ describe('Delete database clusters', () => { cy.visitWithLogin( `/databases/${database.engine}/${database.id}/settings` ); - cy.wait(['@getAccount', '@getDatabase']); + cy.wait(['@getAccount', '@getDatabase', '@getDatabaseTypes']); // Click "Delete Cluster" button. ui.button diff --git a/packages/manager/cypress/e2e/core/databases/update-database.spec.ts b/packages/manager/cypress/e2e/core/databases/update-database.spec.ts index 27376b68a23..2aaee4bd0d7 100644 --- a/packages/manager/cypress/e2e/core/databases/update-database.spec.ts +++ b/packages/manager/cypress/e2e/core/databases/update-database.spec.ts @@ -8,12 +8,13 @@ import { randomIp, randomString, } from 'support/util/random'; -import { databaseFactory } from 'src/factories/databases'; +import { databaseFactory, databaseTypeFactory } from 'src/factories/databases'; import { ui } from 'support/ui'; import { mockGetAccount } from 'support/intercepts/account'; import { mockGetDatabase, mockGetDatabaseCredentials, + mockGetDatabaseTypes, mockResetPassword, mockResetPasswordProvisioningDatabase, mockUpdateDatabase, @@ -22,6 +23,7 @@ import { import { databaseClusterConfiguration, databaseConfigurations, + mockDatabaseNodeTypes, } from 'support/constants/databases'; import { accountFactory } from '@src/factories'; @@ -169,6 +171,7 @@ describe('Update database clusters', () => { // Mock account to ensure 'Managed Databases' capability. mockGetAccount(accountFactory.build()).as('getAccount'); mockGetDatabase(database).as('getDatabase'); + mockGetDatabaseTypes(mockDatabaseNodeTypes).as('getDatabaseTypes'); mockResetPassword(database.id, database.engine).as( 'resetRootPassword' ); @@ -179,7 +182,7 @@ describe('Update database clusters', () => { ).as('getCredentials'); cy.visitWithLogin(`/databases/${database.engine}/${database.id}`); - cy.wait(['@getAccount', '@getDatabase']); + cy.wait(['@getAccount', '@getDatabase', '@getDatabaseTypes']); cy.get('[data-qa-cluster-config]').within(() => { cy.findByText(configuration.region.label).should('be.visible'); @@ -282,6 +285,7 @@ describe('Update database clusters', () => { mockGetAccount(accountFactory.build()).as('getAccount'); mockGetDatabase(database).as('getDatabase'); + mockGetDatabaseTypes(mockDatabaseNodeTypes).as('getDatabaseTypes'); mockUpdateProvisioningDatabase( database.id, @@ -296,7 +300,7 @@ describe('Update database clusters', () => { ).as('resetRootPassword'); cy.visitWithLogin(`/databases/${database.engine}/${database.id}`); - cy.wait(['@getAccount', '@getDatabase']); + cy.wait(['@getAccount', '@getDatabase', '@getDatabaseTypes']); // Cannot update database label. updateDatabaseLabel(initialLabel, updateAttemptLabel); diff --git a/packages/manager/cypress/support/constants/databases.ts b/packages/manager/cypress/support/constants/databases.ts index 24fe8780b51..582853dd0ab 100644 --- a/packages/manager/cypress/support/constants/databases.ts +++ b/packages/manager/cypress/support/constants/databases.ts @@ -1,12 +1,13 @@ -import { +import type { ClusterSize, Engine, Region, DatabaseEngine, + DatabaseType, } from '@linode/api-v4/types'; import { randomLabel } from 'support/util/random'; import { chooseRegion } from 'support/util/regions'; -import { databaseEngineFactory } from '@src/factories'; +import { databaseEngineFactory, databaseTypeFactory } from '@src/factories'; export interface databaseClusterConfiguration { clusterSize: ClusterSize; @@ -39,6 +40,19 @@ export const mockDatabaseEngineTypes: DatabaseEngine[] = [ }), ]; +// The database type IDs in this array should correspond to the DBaaS cluster +// `linodeType` values used by the tests. +export const mockDatabaseNodeTypes: DatabaseType[] = [ + databaseTypeFactory.build({ + class: 'nanode', + id: 'g6-nanode-1', + }), + databaseTypeFactory.build({ + class: 'dedicated', + id: 'g6-dedicated-16', + }), +]; + // Array of database cluster configurations for which to test creation. export const databaseConfigurations: databaseClusterConfiguration[] = [ { diff --git a/packages/manager/cypress/support/intercepts/databases.ts b/packages/manager/cypress/support/intercepts/databases.ts index ce3facf116a..6064d135b16 100644 --- a/packages/manager/cypress/support/intercepts/databases.ts +++ b/packages/manager/cypress/support/intercepts/databases.ts @@ -2,10 +2,11 @@ * @file Cypress intercepts and mocks for Cloud Manager DBaaS operations. */ -import { +import type { Database, DatabaseCredentials, DatabaseEngine, + DatabaseType, } from '@linode/api-v4/types'; import { makeErrorResponse } from 'support/util/errors'; import { apiMatcher } from 'support/util/intercepts'; @@ -235,6 +236,23 @@ export const mockDeleteProvisioningDatabase = ( ); }; +/** + * Intercepts GET request to fetch DBaaS node types and mocks response. + * + * @param databaseTypes - Database node types. + * + * @returns Cypress chainable. + */ +export const mockGetDatabaseTypes = ( + databaseTypes: DatabaseType[] +): Cypress.Chainable => { + return cy.intercept( + 'GET', + apiMatcher('databases/types*'), + paginateResponse(databaseTypes) + ); +}; + /** * Intercepts GET request to fetch available DBaaS engines and mocks response. * From f35a923fbe05f01964abb4a9762f9592a00173f3 Mon Sep 17 00:00:00 2001 From: Connie Liu <139280159+coliu-akamai@users.noreply.github.com> Date: Tue, 17 Oct 2023 12:55:02 -0400 Subject: [PATCH 15/45] feat: [M3-7252] - Only show regions that support VPCs in VPC Create page (#9787) * filter to regions that support VPCs for VPCCreate * hide disabled Tokyo from VPC create * Added changeset: Only show regions that support VPCs in VPC Create page * mocked regions to include vpc capabilities --- ...pr-9787-upcoming-features-1697145963212.md | 5 +++ .../cypress/e2e/core/vpc/vpc-create.spec.ts | 37 +++++++++++++------ .../variants/RegionSelect/disabledRegions.tsx | 2 +- .../src/features/VPCs/VPCCreate/VPCCreate.tsx | 4 +- 4 files changed, 35 insertions(+), 13 deletions(-) create mode 100644 packages/manager/.changeset/pr-9787-upcoming-features-1697145963212.md diff --git a/packages/manager/.changeset/pr-9787-upcoming-features-1697145963212.md b/packages/manager/.changeset/pr-9787-upcoming-features-1697145963212.md new file mode 100644 index 00000000000..7d9692266ce --- /dev/null +++ b/packages/manager/.changeset/pr-9787-upcoming-features-1697145963212.md @@ -0,0 +1,5 @@ +--- +"@linode/manager": Upcoming Features +--- + +Only show regions that support VPCs in VPC Create page ([#9787](https://github.com/linode/manager/pull/9787)) diff --git a/packages/manager/cypress/e2e/core/vpc/vpc-create.spec.ts b/packages/manager/cypress/e2e/core/vpc/vpc-create.spec.ts index f5fa8ec468e..59c4b3b977b 100644 --- a/packages/manager/cypress/e2e/core/vpc/vpc-create.spec.ts +++ b/packages/manager/cypress/e2e/core/vpc/vpc-create.spec.ts @@ -3,11 +3,17 @@ */ import type { Subnet, VPC } from '@linode/api-v4'; -import { vpcFactory, subnetFactory, linodeFactory } from '@src/factories'; +import { + vpcFactory, + subnetFactory, + linodeFactory, + regionFactory, +} from '@src/factories'; import { mockAppendFeatureFlags, mockGetFeatureFlagClientstream, } from 'support/intercepts/feature-flags'; +import { mockGetRegions } from 'support/intercepts/regions'; import { mockCreateVPCError, mockCreateVPC, @@ -44,7 +50,9 @@ describe('VPC create flow', () => { * - Confirms that UI redirects to created VPC page after creating a VPC. */ it('can create a VPC', () => { - const vpcRegion = chooseRegion(); + const mockVPCRegion = regionFactory.build({ + capabilities: ['VPCs'], + }); const mockSubnets: Subnet[] = buildArray(3, (index: number) => { return subnetFactory.build({ @@ -61,7 +69,7 @@ describe('VPC create flow', () => { const mockVpc: VPC = vpcFactory.build({ id: randomNumber(10000, 99999), label: randomLabel(), - region: vpcRegion.id, + region: mockVPCRegion.id, description: randomPhrase(), subnets: mockSubnets, }); @@ -75,13 +83,15 @@ describe('VPC create flow', () => { }).as('getFeatureFlags'); mockGetFeatureFlagClientstream().as('getClientstream'); + mockGetRegions([mockVPCRegion]).as('getRegions'); + cy.visitWithLogin('/vpcs/create'); - cy.wait(['@getFeatureFlags', '@getClientstream']); + cy.wait(['@getFeatureFlags', '@getClientstream', '@getRegions']); cy.findByText('Region') .should('be.visible') .click() - .type(`${vpcRegion.label}{enter}`); + .type(`${mockVPCRegion.label}{enter}`); cy.findByText('VPC Label').should('be.visible').click().type(mockVpc.label); @@ -250,7 +260,7 @@ describe('VPC create flow', () => { cy.contains(`Subnets ${mockVpc.subnets.length}`).should('be.visible'); cy.contains(`Linodes ${totalSubnetUniqueLinodes}`).should('be.visible'); cy.contains(`VPC ID ${mockVpc.id}`).should('be.visible'); - cy.contains(`Region ${vpcRegion.label}`).should('be.visible'); + cy.contains(`Region ${mockVPCRegion.label}`).should('be.visible'); }); mockSubnets.forEach((mockSubnet: Subnet) => { @@ -272,11 +282,14 @@ describe('VPC create flow', () => { * - Confirms that Cloud Manager UI responds accordingly when creating a VPC without subnets. */ it('can create a VPC without any subnets', () => { - const vpcRegion = chooseRegion(); + const mockVPCRegion = regionFactory.build({ + capabilities: ['VPCs'], + }); + const mockVpc: VPC = vpcFactory.build({ id: randomNumber(10000, 99999), label: randomLabel(), - region: vpcRegion.id, + region: mockVPCRegion.id, description: randomPhrase(), subnets: [], }); @@ -288,13 +301,15 @@ describe('VPC create flow', () => { }).as('getFeatureFlags'); mockGetFeatureFlagClientstream().as('getClientstream'); + mockGetRegions([mockVPCRegion]).as('getRegions'); + cy.visitWithLogin('/vpcs/create'); - cy.wait(['@getFeatureFlags', '@getClientstream']); + cy.wait(['@getFeatureFlags', '@getClientstream', '@getRegions']); cy.findByText('Region') .should('be.visible') .click() - .type(`${vpcRegion.label}{enter}`); + .type(`${mockVPCRegion.label}{enter}`); cy.findByText('VPC Label').should('be.visible').click().type(mockVpc.label); @@ -338,7 +353,7 @@ describe('VPC create flow', () => { cy.contains(`Subnets ${mockVpc.subnets.length}`).should('be.visible'); cy.contains(`Linodes ${totalSubnetUniqueLinodes}`).should('be.visible'); cy.contains(`VPC ID ${mockVpc.id}`).should('be.visible'); - cy.contains(`Region ${vpcRegion.label}`).should('be.visible'); + cy.contains(`Region ${mockVPCRegion.label}`).should('be.visible'); }); cy.findByText('No Subnets are assigned.').should('be.visible'); diff --git a/packages/manager/src/components/EnhancedSelect/variants/RegionSelect/disabledRegions.tsx b/packages/manager/src/components/EnhancedSelect/variants/RegionSelect/disabledRegions.tsx index 0b3ec94fe8b..308625b3bd4 100644 --- a/packages/manager/src/components/EnhancedSelect/variants/RegionSelect/disabledRegions.tsx +++ b/packages/manager/src/components/EnhancedSelect/variants/RegionSelect/disabledRegions.tsx @@ -58,7 +58,7 @@ interface DisabledRegion { export const listOfDisabledRegions: DisabledRegion[] = [ { disabledMessage: tokyoDisabledMessage, - excludePaths: ['/object-storage/buckets/create'], + excludePaths: ['/object-storage/buckets/create', '/vpcs/create'], fakeRegion: fakeTokyo, featureFlag: 'soldOutTokyo', }, diff --git a/packages/manager/src/features/VPCs/VPCCreate/VPCCreate.tsx b/packages/manager/src/features/VPCs/VPCCreate/VPCCreate.tsx index fe7fe4a6cd0..f2049e00b39 100644 --- a/packages/manager/src/features/VPCs/VPCCreate/VPCCreate.tsx +++ b/packages/manager/src/features/VPCs/VPCCreate/VPCCreate.tsx @@ -43,6 +43,8 @@ const VPCCreate = () => { const { data: profile } = useProfile(); const { data: grants } = useGrants(); const { data: regions } = useRegionsQuery(); + const regionsWithVPCCapability = + regions?.filter((region) => region.capabilities.includes('VPCs')) ?? []; const { isLoading, mutateAsync: createVPC } = useCreateVPCMutation(); const [ generalSubnetErrorsFromAPI, @@ -242,7 +244,7 @@ const VPCCreate = () => { disabled={userCannotAddVPC} errorText={errors.region} isClearable - regions={regions ?? []} + regions={regionsWithVPCCapability} selectedID={values.region} /> Date: Tue, 17 Oct 2023 14:08:30 -0400 Subject: [PATCH 16/45] docs: Add MigrationNotification Storybook (#9799) --------- Co-authored-by: Jaalah Ramos --- .../MigrationNotification.stories.tsx | 28 +++++++++++++++++++ 1 file changed, 28 insertions(+) create mode 100644 packages/manager/src/features/Linodes/LinodesDetail/LinodesDetailHeader/MigrationNotification.stories.tsx diff --git a/packages/manager/src/features/Linodes/LinodesDetail/LinodesDetailHeader/MigrationNotification.stories.tsx b/packages/manager/src/features/Linodes/LinodesDetail/LinodesDetailHeader/MigrationNotification.stories.tsx new file mode 100644 index 00000000000..79da491991c --- /dev/null +++ b/packages/manager/src/features/Linodes/LinodesDetail/LinodesDetailHeader/MigrationNotification.stories.tsx @@ -0,0 +1,28 @@ +import { action } from '@storybook/addon-actions'; +import React from 'react'; + +import { MigrationNotification } from './MigrationNotification'; + +import type { Meta, StoryObj } from '@storybook/react'; + +const meta: Meta = { + argTypes: {}, + args: { + linodeID: 123456, + migrationTime: '2025-10-21T20:00:00', + notificationMessage: 'Your Linode has been scheduled for migration.', + notificationType: 'migration_scheduled', + requestNotifications: action('requestNotifications'), + }, + component: MigrationNotification, + title: 'Features/LinodesDetail/MigrationNotification', +}; + +export default meta; + +type Story = StoryObj; + +export const Default: Story = { + args: {}, + render: (args) => , +}; From b8e1b0499cffa07773b627a6600f5e6fbe999aeb Mon Sep 17 00:00:00 2001 From: Hana Xu <115299789+hana-linode@users.noreply.github.com> Date: Tue, 17 Oct 2023 15:08:20 -0400 Subject: [PATCH 17/45] fix: [M3-7270] - Edit Linode VLAN config interface (#9798) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Description 📝 Bug from https://github.com/linode/manager/pull/9709 On the prod env, we were unable to edit a config due to the config interface being deleted. This was due to the `regionHasVPCs` variable being coupled with `regionHasVLANS` in the if statement to delete config interfaces. The fix was to handle VPCs and VLANs separately. ## How to test 🧪 ### Prerequisites - Point to the prod environment ### Reproduction steps - Go to a Linode's details page and click on the `Configurations` tab - Click the edit button and scroll down to the Networking section - Select a VLAN and save changes ### Verification steps - The VLAN changes should be reflected in the Configurations table --- .../LinodeConfigs/LinodeConfigDialog.tsx | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/packages/manager/src/features/Linodes/LinodesDetail/LinodeConfigs/LinodeConfigDialog.tsx b/packages/manager/src/features/Linodes/LinodesDetail/LinodeConfigs/LinodeConfigDialog.tsx index 222b10f9cbc..b39b68bdfad 100644 --- a/packages/manager/src/features/Linodes/LinodesDetail/LinodeConfigs/LinodeConfigDialog.tsx +++ b/packages/manager/src/features/Linodes/LinodesDetail/LinodeConfigs/LinodeConfigDialog.tsx @@ -368,8 +368,16 @@ export const LinodeConfigDialog = (props: Props) => { configData.initrd = finnixDiskID; } - if (!regionHasVLANS || !regionHasVPCs) { - delete configData.interfaces; + if (!regionHasVLANS) { + configData.interfaces = configData.interfaces?.filter( + (_interface) => _interface.purpose !== 'vlan' + ); + } + + if (!regionHasVPCs) { + configData.interfaces = configData.interfaces?.filter( + (_interface) => _interface.purpose !== 'vpc' + ); } const actionType = Boolean(config) ? 'updated' : 'created'; From ee835224059f70c1bfce8d4faaf4819f94211bec Mon Sep 17 00:00:00 2001 From: Connie Liu <139280159+coliu-akamai@users.noreply.github.com> Date: Wed, 18 Oct 2023 12:07:53 -0400 Subject: [PATCH 18/45] refactor: [M3-6282] MUI v5 Migration - `SRC > Features > StackScripts` pt2 (#9786) * starting to refactor stackscripts * run codemod (there are currently syntax errors here) * additional styling (syntax errors to be fixed on diff files) * base styles * SelectStackScriptPanel styling * SelectStackScriptStyling p2 * StackScriptSelectionRow styling * fix regressions * eslint * Add changeset * cleanup code * remove unnecessary props * run code mod (have syntax errors rn) * styling * stack script row helper styling cleanup * user defined fields panel styling * update imports/exports * Added changeset: MUI v5 Migration - `SRC > Features > StackScripts` pt 2 * address feedback * fix spacing for stackscript create form action panel --- .../pr-9786-tech-stories-1697120258125.md | 5 + packages/manager/src/MainContent.tsx | 2 +- .../TabbedContent/FromAppsContent.tsx | 2 +- .../TabbedContent/FromStackScriptContent.tsx | 2 +- .../LinodeRebuild/RebuildFromStackScript.tsx | 2 +- .../StackScripts/CommonStackScript.styles.ts | 176 +++++++++++++++ .../SelectStackScriptsSection.tsx | 12 +- .../StackScriptSelectionRow.tsx | 4 +- .../StackScriptBase/StackScriptBase.styles.ts | 4 +- .../StackScripts/StackScriptDialog.tsx | 2 +- .../StackScriptForm/StackScriptForm.styles.ts | 5 +- .../StackScriptPanel/StackScriptPanel.tsx | 2 +- .../StackScriptPanelContent.tsx | 4 +- .../StackScriptPanel/StackScriptRow.tsx | 67 +++--- .../StackScriptPanel/StackScriptsSection.tsx | 21 +- .../StackScripts/StackScriptPanel/index.ts | 3 - .../StackScripts/StackScriptRowHelpers.tsx | 210 ------------------ .../StackScripts/StackScriptsLanding.tsx | 2 +- .../FieldTypes/UserDefinedMultiSelect.tsx | 6 +- .../FieldTypes/UserDefinedSelect.tsx | 40 ++-- .../FieldTypes/UserDefinedText.tsx | 39 ++-- .../UserDefinedFieldsPanel.styles.ts | 38 ++++ .../UserDefinedFieldsPanel.tsx | 70 ++---- .../UserDefinedFieldsPanel/index.tsx | 2 - .../src/features/StackScripts/index.tsx | 2 - 25 files changed, 321 insertions(+), 401 deletions(-) create mode 100644 packages/manager/.changeset/pr-9786-tech-stories-1697120258125.md create mode 100644 packages/manager/src/features/StackScripts/CommonStackScript.styles.ts delete mode 100644 packages/manager/src/features/StackScripts/StackScriptPanel/index.ts delete mode 100644 packages/manager/src/features/StackScripts/StackScriptRowHelpers.tsx create mode 100644 packages/manager/src/features/StackScripts/UserDefinedFieldsPanel/UserDefinedFieldsPanel.styles.ts delete mode 100644 packages/manager/src/features/StackScripts/UserDefinedFieldsPanel/index.tsx delete mode 100644 packages/manager/src/features/StackScripts/index.tsx diff --git a/packages/manager/.changeset/pr-9786-tech-stories-1697120258125.md b/packages/manager/.changeset/pr-9786-tech-stories-1697120258125.md new file mode 100644 index 00000000000..a7bf7e6a1c5 --- /dev/null +++ b/packages/manager/.changeset/pr-9786-tech-stories-1697120258125.md @@ -0,0 +1,5 @@ +--- +"@linode/manager": Tech Stories +--- + +MUI v5 Migration - `SRC > Features > StackScripts` pt 2 ([#9786](https://github.com/linode/manager/pull/9786)) diff --git a/packages/manager/src/MainContent.tsx b/packages/manager/src/MainContent.tsx index 336652e5de5..7c0e04e02d6 100644 --- a/packages/manager/src/MainContent.tsx +++ b/packages/manager/src/MainContent.tsx @@ -155,7 +155,7 @@ const LoadBalancers = React.lazy(() => import('src/features/LoadBalancers')); const NodeBalancers = React.lazy( () => import('src/features/NodeBalancers/NodeBalancers') ); -const StackScripts = React.lazy(() => import('src/features/StackScripts')); +const StackScripts = React.lazy(() => import('src/features/StackScripts/StackScripts')); const SupportTickets = React.lazy( () => import('src/features/Support/SupportTickets') ); diff --git a/packages/manager/src/features/Linodes/LinodesCreate/TabbedContent/FromAppsContent.tsx b/packages/manager/src/features/Linodes/LinodesCreate/TabbedContent/FromAppsContent.tsx index 568e207e430..33b0f3ce527 100644 --- a/packages/manager/src/features/Linodes/LinodesCreate/TabbedContent/FromAppsContent.tsx +++ b/packages/manager/src/features/Linodes/LinodesCreate/TabbedContent/FromAppsContent.tsx @@ -16,7 +16,7 @@ import { APP_ROOT } from 'src/constants'; import { ImageEmptyState } from 'src/features/Linodes/LinodesCreate/TabbedContent/ImageEmptyState'; import { AppDetailDrawer } from 'src/features/OneClickApps'; import { oneClickApps } from 'src/features/OneClickApps/oneClickApps'; -import UserDefinedFieldsPanel from 'src/features/StackScripts/UserDefinedFieldsPanel'; +import UserDefinedFieldsPanel from 'src/features/StackScripts/UserDefinedFieldsPanel/UserDefinedFieldsPanel'; import { sendMarketplaceSearchEvent } from 'src/utilities/analytics'; import getAPIErrorsFor from 'src/utilities/getAPIErrorFor'; diff --git a/packages/manager/src/features/Linodes/LinodesCreate/TabbedContent/FromStackScriptContent.tsx b/packages/manager/src/features/Linodes/LinodesCreate/TabbedContent/FromStackScriptContent.tsx index 384cbabcb08..87d99759d5c 100644 --- a/packages/manager/src/features/Linodes/LinodesCreate/TabbedContent/FromStackScriptContent.tsx +++ b/packages/manager/src/features/Linodes/LinodesCreate/TabbedContent/FromStackScriptContent.tsx @@ -8,7 +8,7 @@ import { ImageSelect } from 'src/components/ImageSelect/ImageSelect'; import { ImageEmptyState } from 'src/features/Linodes/LinodesCreate/TabbedContent/ImageEmptyState'; import SelectStackScriptPanel from 'src/features/StackScripts/SelectStackScriptPanel/SelectStackScriptPanel'; import StackScriptDialog from 'src/features/StackScripts/StackScriptDialog'; -import UserDefinedFieldsPanel from 'src/features/StackScripts/UserDefinedFieldsPanel'; +import UserDefinedFieldsPanel from 'src/features/StackScripts/UserDefinedFieldsPanel/UserDefinedFieldsPanel'; import { StackScriptsRequest } from 'src/features/StackScripts/types'; import { filterImagesByType } from 'src/store/image/image.helpers'; import getAPIErrorsFor from 'src/utilities/getAPIErrorFor'; diff --git a/packages/manager/src/features/Linodes/LinodesDetail/LinodeRebuild/RebuildFromStackScript.tsx b/packages/manager/src/features/Linodes/LinodesDetail/LinodeRebuild/RebuildFromStackScript.tsx index 72552f482de..0f78905be90 100644 --- a/packages/manager/src/features/Linodes/LinodesDetail/LinodeRebuild/RebuildFromStackScript.tsx +++ b/packages/manager/src/features/Linodes/LinodesDetail/LinodeRebuild/RebuildFromStackScript.tsx @@ -17,7 +17,7 @@ import { resetEventsPolling } from 'src/eventsPolling'; import { ImageEmptyState } from 'src/features/Linodes/LinodesCreate/TabbedContent/ImageEmptyState'; import SelectStackScriptPanel from 'src/features/StackScripts/SelectStackScriptPanel/SelectStackScriptPanel'; import StackScriptDialog from 'src/features/StackScripts/StackScriptDialog'; -import UserDefinedFieldsPanel from 'src/features/StackScripts/UserDefinedFieldsPanel'; +import UserDefinedFieldsPanel from 'src/features/StackScripts/UserDefinedFieldsPanel/UserDefinedFieldsPanel'; import { getCommunityStackscripts, getMineAndAccountStackScripts, diff --git a/packages/manager/src/features/StackScripts/CommonStackScript.styles.ts b/packages/manager/src/features/StackScripts/CommonStackScript.styles.ts new file mode 100644 index 00000000000..137ef2008b2 --- /dev/null +++ b/packages/manager/src/features/StackScripts/CommonStackScript.styles.ts @@ -0,0 +1,176 @@ +import Grid from '@mui/material/Unstable_Grid2'; +import { Theme, styled } from '@mui/material/styles'; +import { Link } from 'react-router-dom'; + +import { Button } from 'src/components/Button/Button'; +import { TableCell } from 'src/components/TableCell'; +import { TableRow } from 'src/components/TableRow'; +import { Typography } from 'src/components/Typography'; + +const libRadioLabel = { + cursor: 'pointer', +}; + +const stackScriptUsernameStyling = (theme: Theme) => { + return { + ...libRadioLabel, + color: theme.textColors.tableStatic, + fontSize: '0.875rem', + lineHeight: '1.125rem', + }; +}; + +const rowStyles = { + '& > button': { + height: 46, + }, + height: 46, +}; + +const libTitle = (theme: Theme) => { + return { + fontSize: '0.875rem', + lineHeight: '1.125rem', + overflow: 'hidden', + textOverflow: 'ellipsis', + [theme.breakpoints.down('md')]: { + wordBreak: 'break-all', + }, + whiteSpace: 'nowrap' as const, + }; +}; + +export const StyledTitleTypography = styled(Typography, { + label: 'StyledTitleTypography', +})(({ theme }) => ({ + ...libTitle(theme), +})); + +export const StyledTitleTableCell = styled(TableCell, { + label: 'StyledTitleTableCell', +})(({ theme }) => ({ + ...libTitle(theme), +})); + +export const StyledDetailsButton = styled(Button, { + label: 'StyledDetailsButton', +})(({ theme }) => ({ + '&:hover, &:focus': { + backgroundColor: 'transparent', + }, + fontFamily: theme.font.normal, + fontSize: '0.875rem', + marginTop: 0, + padding: theme.spacing(), + [theme.breakpoints.down('sm')]: { + marginBottom: 4, + marginLeft: 0, + paddingBottom: 4, + paddingTop: 4, + }, + width: 100, +})); + +export const StyledLabel = styled('label', { label: 'StyledLabel' })({ + ...libRadioLabel, +}); + +export const StyledSelectionGrid = styled(Grid, { + label: 'StyledSelectionGrid', +})(({ theme }) => ({ + alignItems: 'center', + flexWrap: 'nowrap', + justifyContent: 'space-between', + [theme.breakpoints.down('sm')]: { + alignItems: 'flex-start', + flexDirection: 'column', + }, + width: '100%', +})); + +export const StyledSelectionButtonGrid = styled(Grid, { + label: 'StyledSelectionButtonGrid', +})(({ theme }) => ({ + [theme.breakpoints.down('sm')]: { + '&.MuiGrid-item': { + paddingLeft: 0, + paddingTop: 0, + }, + }, +})); + +export const StyledSelectionDetailsGrid = styled(Grid, { + label: 'StyledSelectionDetailsGrid', +})(({ theme }) => ({ + [theme.breakpoints.down('sm')]: { + '&.MuiGrid-item': { + marginTop: 4, + paddingBottom: 0, + }, + }, +})); + +export const StyledTableCell = styled(TableCell, { label: 'StyledTableCell' })({ + padding: 0, + width: '100%', +}); + +export const StyledImagesTableCell = styled(TableCell, { + label: 'StyledImagesTableCell', +})({ + fontSize: '0.75rem', + overflowWrap: 'break-word', + whiteSpace: 'pre-wrap', +}); + +export const StyledTableRow = styled(TableRow, { label: 'StyledTableRow' })({ + ...rowStyles, +}); + +export const StyledRowTableCell = styled(TableCell, { + label: 'StyledRowTableCell', +})({ + ...rowStyles, +}); + +export const StyledTypography = styled(Typography, { + label: 'StyledTypography', +})(({ theme }) => ({ + color: theme.textColors.tableHeader, + fontSize: '.75rem', + overflow: 'hidden', + textOverflow: 'ellipsis', + [theme.breakpoints.between('sm', 'xl')]: { + wordBreak: 'break-word', + }, + whiteSpace: 'nowrap', +})); + +export const StyledUsernameLabel = styled('label', { + label: 'StyledUsernameLabel', +})(({ theme }) => ({ + ...stackScriptUsernameStyling(theme), +})); + +export const StyledLabelSpan = styled('span', { label: 'StyledLabelSpan' })({ + ...libRadioLabel, +}); + +export const StyledUsernameSpan = styled('span', { + label: 'StyledUsernameSpan', +})(({ theme }) => ({ + ...stackScriptUsernameStyling(theme), +})); + +export const StyledLink = styled(Link, { label: 'StyledLink' })( + ({ theme }) => ({ + color: theme.textColors.tableStatic, + }) +); + +export const StyledStackScriptSectionTableCell = styled(TableCell, { + label: 'StyledStackScriptSectionTableCell', +})({ + border: 0, + paddingTop: 100, +}); diff --git a/packages/manager/src/features/StackScripts/SelectStackScriptPanel/SelectStackScriptsSection.tsx b/packages/manager/src/features/StackScripts/SelectStackScriptPanel/SelectStackScriptsSection.tsx index e45cdcce727..0d1830bd647 100644 --- a/packages/manager/src/features/StackScripts/SelectStackScriptPanel/SelectStackScriptsSection.tsx +++ b/packages/manager/src/features/StackScripts/SelectStackScriptPanel/SelectStackScriptsSection.tsx @@ -1,16 +1,15 @@ import { Image } from '@linode/api-v4/lib/images'; import { StackScript } from '@linode/api-v4/lib/stackscripts'; -import { styled } from '@mui/material/styles'; import * as React from 'react'; import { CircleProgress } from 'src/components/CircleProgress'; import { TableBody } from 'src/components/TableBody'; -import { TableCell } from 'src/components/TableCell'; import { TableRow } from 'src/components/TableRow'; import { useProfile } from 'src/queries/profile'; import { formatDate } from 'src/utilities/formatDate'; import { truncate } from 'src/utilities/truncate'; +import { StyledStackScriptSectionTableCell } from '../CommonStackScript.styles'; import StackScriptSelectionRow from './StackScriptSelectionRow'; interface Props { @@ -53,16 +52,11 @@ export const SelectStackScriptsSection = (props: Props) => { data && data.map(selectStackScript) ) : ( - + - + )} ); }; - -const StyledTableCell = styled(TableCell, { label: 'StyledTableCell' })({ - border: 0, - paddingTop: 100, -}); \ No newline at end of file diff --git a/packages/manager/src/features/StackScripts/SelectStackScriptPanel/StackScriptSelectionRow.tsx b/packages/manager/src/features/StackScripts/SelectStackScriptPanel/StackScriptSelectionRow.tsx index 3fe40e2d88d..f3d2c9f8caf 100644 --- a/packages/manager/src/features/StackScripts/SelectStackScriptPanel/StackScriptSelectionRow.tsx +++ b/packages/manager/src/features/StackScripts/SelectStackScriptPanel/StackScriptSelectionRow.tsx @@ -12,13 +12,13 @@ import { openStackScriptDialog as openStackScriptDialogAction } from 'src/store/ import { StyledDetailsButton, StyledLabel, - StyledSelectionGrid, StyledSelectionButtonGrid, StyledSelectionDetailsGrid, + StyledSelectionGrid, StyledTableCell, StyledTypography, StyledUsernameLabel, -} from '../StackScriptRowHelpers'; +} from '../CommonStackScript.styles'; export interface Props { checked?: boolean; diff --git a/packages/manager/src/features/StackScripts/StackScriptBase/StackScriptBase.styles.ts b/packages/manager/src/features/StackScripts/StackScriptBase/StackScriptBase.styles.ts index 52f54a8806f..b4a596d5f8f 100644 --- a/packages/manager/src/features/StackScripts/StackScriptBase/StackScriptBase.styles.ts +++ b/packages/manager/src/features/StackScripts/StackScriptBase/StackScriptBase.styles.ts @@ -3,6 +3,7 @@ import { styled } from '@mui/material/styles'; import { DebouncedSearchTextField } from 'src/components/DebouncedSearchTextField'; import { Placeholder } from 'src/components/Placeholder/Placeholder'; import { Table } from 'src/components/Table'; +import { omittedProps } from 'src/utilities/omittedProps'; import type { WithStackScriptBaseOptions } from './StackScriptBase'; @@ -13,6 +14,7 @@ type StyledStackScriptBaseProps = Pick< export const StyledContentDiv = styled('div', { label: 'StyledContentDiv', + shouldForwardProp: omittedProps(['isSelecting']), })(({ isSelecting, theme }) => ({ ...(isSelecting ? { @@ -78,7 +80,7 @@ export const StyledLoaderDiv = styled('div', { label: 'StyledLoaderDiv' })( // Styles to override base placeholder styles for StackScript null state export const StyledPlaceholder = styled(Placeholder, { - label: 'StyledPlaceHolder', + label: 'StyledPlaceholder', })(({ theme }) => ({ '& svg': { marginTop: 4, diff --git a/packages/manager/src/features/StackScripts/StackScriptDialog.tsx b/packages/manager/src/features/StackScripts/StackScriptDialog.tsx index f92397adf88..59f2eb1a22b 100644 --- a/packages/manager/src/features/StackScripts/StackScriptDialog.tsx +++ b/packages/manager/src/features/StackScripts/StackScriptDialog.tsx @@ -22,7 +22,7 @@ interface Props { type CombinedProps = DispatchProps & Props; -export const StackScriptDialog: React.FC = (props) => { +export const StackScriptDialog = (props: CombinedProps) => { const { closeDrawer, open, stackScriptId } = props; const [stackScript, setStackScript] = React.useState( diff --git a/packages/manager/src/features/StackScripts/StackScriptForm/StackScriptForm.styles.ts b/packages/manager/src/features/StackScripts/StackScriptForm/StackScriptForm.styles.ts index 567d758cddb..44e0b04283c 100644 --- a/packages/manager/src/features/StackScripts/StackScriptForm/StackScriptForm.styles.ts +++ b/packages/manager/src/features/StackScripts/StackScriptForm/StackScriptForm.styles.ts @@ -7,11 +7,12 @@ import { TextField } from 'src/components/TextField'; export const StyledActionsPanel = styled(ActionsPanel, { label: 'StyledActionsPanel', -})({ +})(({ theme }) => ({ display: 'flex', justifyContent: 'flex-end', + marginTop: theme.spacing(3), paddingBottom: 0, -}); +})); export const StyledGridWithTips = styled(Grid, { label: 'StyledGridWithTips' })( ({ theme }) => ({ diff --git a/packages/manager/src/features/StackScripts/StackScriptPanel/StackScriptPanel.tsx b/packages/manager/src/features/StackScripts/StackScriptPanel/StackScriptPanel.tsx index 7e93be66bb7..020cdddf817 100644 --- a/packages/manager/src/features/StackScripts/StackScriptPanel/StackScriptPanel.tsx +++ b/packages/manager/src/features/StackScripts/StackScriptPanel/StackScriptPanel.tsx @@ -32,7 +32,7 @@ interface Props { type CombinedProps = Props & RouteComponentProps<{}>; -const SelectStackScriptPanel: React.FC = (props) => { +const SelectStackScriptPanel = (props: CombinedProps) => { const { publicImages } = props; const { data: profile } = useProfile(); const username = profile?.username || ''; diff --git a/packages/manager/src/features/StackScripts/StackScriptPanel/StackScriptPanelContent.tsx b/packages/manager/src/features/StackScripts/StackScriptPanel/StackScriptPanelContent.tsx index 5c7d93c01a3..c2fcaf3f6a8 100644 --- a/packages/manager/src/features/StackScripts/StackScriptPanel/StackScriptPanelContent.tsx +++ b/packages/manager/src/features/StackScripts/StackScriptPanel/StackScriptPanelContent.tsx @@ -15,7 +15,7 @@ import { StackScriptsRequest } from 'src/features/StackScripts/types'; import StackScriptBase, { StateProps, } from '../StackScriptBase/StackScriptBase'; -import StackScriptsSection from './StackScriptsSection'; +import { StackScriptsSection } from './StackScriptsSection'; interface DialogVariantProps { error?: string; @@ -51,7 +51,7 @@ const defaultDialogState = { stackScriptLabel: '', }; -export const StackScriptPanelContent: React.FC = (props) => { +export const StackScriptPanelContent = (props: CombinedProps) => { const { currentFilter } = props; const [mounted, setMounted] = React.useState(false); diff --git a/packages/manager/src/features/StackScripts/StackScriptPanel/StackScriptRow.tsx b/packages/manager/src/features/StackScripts/StackScriptPanel/StackScriptRow.tsx index 575f8a8c535..d66f06d082d 100644 --- a/packages/manager/src/features/StackScripts/StackScriptPanel/StackScriptRow.tsx +++ b/packages/manager/src/features/StackScripts/StackScriptPanel/StackScriptRow.tsx @@ -1,17 +1,22 @@ -import { WithStyles, withStyles } from '@mui/styles'; import * as React from 'react'; -import { Link } from 'react-router-dom'; -import { compose as recompose } from 'recompose'; import { Hidden } from 'src/components/Hidden'; -import { RenderGuard, RenderGuardProps } from 'src/components/RenderGuard'; import { TableCell } from 'src/components/TableCell'; -import { TableRow } from 'src/components/TableRow'; import { Typography } from 'src/components/Typography'; import { StackScriptActionMenu } from 'src/features/StackScripts/StackScriptPanel/StackScriptActionMenu'; import { StackScriptCategory } from 'src/features/StackScripts/stackScriptUtils'; -import { ClassNames, styles } from '../StackScriptRowHelpers'; +import { + StyledImagesTableCell, + StyledLabelSpan, + StyledLink, + StyledRowTableCell, + StyledTableRow, + StyledTitleTableCell, + StyledTitleTypography, + StyledTypography, + StyledUsernameSpan, +} from '../CommonStackScript.styles'; export interface Props { canAddLinodes: boolean; @@ -33,14 +38,11 @@ export interface Props { updated: string; } -export type CombinedProps = Props & WithStyles & RenderGuardProps; - -export const StackScriptRow: React.FC = (props) => { +export const StackScriptRow = (props: Props) => { const { canAddLinodes, canModify, category, - classes, deploymentsTotal, description, images, @@ -58,36 +60,28 @@ export const StackScriptRow: React.FC = (props) => { const renderLabel = () => { return ( <> - - + + {stackScriptUsername && ( - + {stackScriptUsername} /  - + )} - {label} - - + {label} + + {description && ( - - {description} - + {description} )} ); }; return ( - - + + {renderLabel()} - + {deploymentsTotal} @@ -97,9 +91,9 @@ export const StackScriptRow: React.FC = (props) => { - + {images.includes('any/all') ? 'Any/All' : images.join(', ')} - + {communityStackScript ? null : ( // We hide the "Status" column in the "Community StackScripts" tab of the StackScripts landing page since all of those are public. @@ -108,7 +102,7 @@ export const StackScriptRow: React.FC = (props) => { )} - + = (props) => { triggerDelete={triggerDelete} triggerMakePublic={triggerMakePublic} /> - - + + ); }; - -export default recompose( - RenderGuard, - withStyles(styles) -)(StackScriptRow); diff --git a/packages/manager/src/features/StackScripts/StackScriptPanel/StackScriptsSection.tsx b/packages/manager/src/features/StackScripts/StackScriptPanel/StackScriptsSection.tsx index 2ac9cbfe2ee..390f3ad2f2f 100644 --- a/packages/manager/src/features/StackScripts/StackScriptPanel/StackScriptsSection.tsx +++ b/packages/manager/src/features/StackScripts/StackScriptPanel/StackScriptsSection.tsx @@ -1,11 +1,9 @@ import { Image } from '@linode/api-v4/lib/images'; import { StackScript } from '@linode/api-v4/lib/stackscripts'; -import { makeStyles } from '@mui/styles'; import * as React from 'react'; import { CircleProgress } from 'src/components/CircleProgress'; import { TableBody } from 'src/components/TableBody'; -import { TableCell } from 'src/components/TableCell'; import { TableRow } from 'src/components/TableRow'; import { StackScriptCategory, @@ -15,14 +13,8 @@ import { useGrants, useProfile } from 'src/queries/profile'; import { formatDate } from 'src/utilities/formatDate'; import stripImageName from 'src/utilities/stripImageName'; -import StackScriptRow from './StackScriptRow'; - -const useStyles = makeStyles(() => ({ - loadingWrapper: { - border: 0, - paddingTop: 100, - }, -})); +import { StyledStackScriptSectionTableCell } from '../CommonStackScript.styles'; +import { StackScriptRow } from './StackScriptRow'; export interface Props { // change until we're actually using it. @@ -38,8 +30,7 @@ export interface Props { triggerMakePublic: (id: number, label: string) => void; } -const StackScriptsSection: React.FC = (props) => { - const classes = useStyles(); +export const StackScriptsSection = (props: Props) => { const { category, data, isSorting, triggerDelete, triggerMakePublic } = props; const { data: profile } = useProfile(); @@ -81,13 +72,11 @@ const StackScriptsSection: React.FC = (props) => { data && data.map(listStackScript) ) : ( - + - + )} ); }; - -export default StackScriptsSection; diff --git a/packages/manager/src/features/StackScripts/StackScriptPanel/index.ts b/packages/manager/src/features/StackScripts/StackScriptPanel/index.ts deleted file mode 100644 index a11dd4adcd2..00000000000 --- a/packages/manager/src/features/StackScripts/StackScriptPanel/index.ts +++ /dev/null @@ -1,3 +0,0 @@ -import StackScriptPanel from './StackScriptPanel'; -// export { CommunityStackScripts, LinodeStackScripts, MyStackScripts }; -export default StackScriptPanel; diff --git a/packages/manager/src/features/StackScripts/StackScriptRowHelpers.tsx b/packages/manager/src/features/StackScripts/StackScriptRowHelpers.tsx deleted file mode 100644 index faf8344836a..00000000000 --- a/packages/manager/src/features/StackScripts/StackScriptRowHelpers.tsx +++ /dev/null @@ -1,210 +0,0 @@ -import { Theme, styled } from '@mui/material/styles'; -import Grid from '@mui/material/Unstable_Grid2'; -import { createStyles } from '@mui/styles'; - -import { Button } from 'src/components/Button/Button'; -import { TableCell } from 'src/components/TableCell'; -import { Typography } from 'src/components/Typography'; - -const libRadioLabel = { - cursor: 'pointer', -}; - -export const StyledDetailsButton = styled(Button, { label: '' })( - ({ theme }) => ({ - '&:hover, &:focus': { - backgroundColor: 'transparent', - }, - fontFamily: theme.font.normal, - fontSize: '0.875rem', - marginTop: 0, - padding: theme.spacing(), - [theme.breakpoints.down('sm')]: { - marginBottom: 4, - marginLeft: 0, - paddingBottom: 4, - paddingTop: 4, - }, - width: 100, - }) -); - -export const StyledLabel = styled('label', { label: 'StyledLabel' })({ - ...libRadioLabel, -}); - -export const StyledSelectionGrid = styled(Grid, { label: '' })(({ theme }) => ({ - alignItems: 'center', - flexWrap: 'nowrap', - justifyContent: 'space-between', - [theme.breakpoints.down('sm')]: { - alignItems: 'flex-start', - flexDirection: 'column', - }, - width: '100%', -})); - -export const StyledSelectionButtonGrid = styled(Grid, { - label: 'StyledSelectionButtonGrid', -})(({ theme }) => ({ - [theme.breakpoints.down('sm')]: { - '&.MuiGrid-item': { - paddingLeft: 0, - paddingTop: 0, - }, - }, -})); - -export const StyledSelectionDetailsGrid = styled(Grid, { label: '' })( - ({ theme }) => ({ - [theme.breakpoints.down('sm')]: { - '&.MuiGrid-item': { - marginTop: 4, - paddingBottom: 0, - }, - }, - }) -); - -export const StyledTableCell = styled(TableCell, { label: 'StyledTableCell' })({ - padding: 0, - width: '100%', -}); - -export const StyledTypography = styled(Typography, { - label: 'StyledTypography', -})(({ theme }) => ({ - color: theme.textColors.tableHeader, - fontSize: '.75rem', - overflow: 'hidden', - textOverflow: 'ellipsis', - [theme.breakpoints.between('sm', 'xl')]: { - wordBreak: 'break-word', - }, - whiteSpace: 'nowrap', -})); - -export const StyledUsernameLabel = styled('label', { label: '' })( - ({ theme }) => ({ - ...libRadioLabel, - color: theme.textColors.tableStatic, - fontSize: '0.875rem', - lineHeight: '1.125rem', - }) -); - -// will convert these in part m3-6282 pt2 -export type ClassNames = - | 'detailsButton' - | 'images' - | 'libDescription' - | 'libRadio' - | 'libRadioLabel' - | 'libTitle' - | 'link' - | 'row' - | 'selectionGrid' - | 'selectionGridButton' - | 'selectionGridDetails' - | 'stackScriptCell' - | 'stackScriptUsername'; - -// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types -export const styles = (theme: Theme) => - createStyles({ - detailsButton: { - '&:hover, &:focus': { - backgroundColor: 'transparent', - }, - fontFamily: theme.font.normal, - fontSize: '0.875rem', - marginTop: 0, - padding: theme.spacing(), - [theme.breakpoints.down('sm')]: { - marginBottom: 4, - marginLeft: 0, - paddingBottom: 4, - paddingTop: 4, - }, - width: 100, - }, - images: { - fontSize: '0.75rem', - overflowWrap: 'break-word', - whiteSpace: 'pre-wrap', - }, - libDescription: { - color: theme.textColors.tableHeader, - fontSize: '.75rem', - overflow: 'hidden', - textOverflow: 'ellipsis', - [theme.breakpoints.between('sm', 'xl')]: { - wordBreak: 'break-word', - }, - whiteSpace: 'nowrap', - }, - libRadio: { - alignItems: 'center', - display: 'flex', - flexWrap: 'wrap', - height: '100%', - width: 70, - }, - libRadioLabel: { - cursor: 'pointer', - }, - libTitle: { - fontSize: '0.875rem', - lineHeight: '1.125rem', - overflow: 'hidden', - textOverflow: 'ellipsis', - [theme.breakpoints.down('md')]: { - wordBreak: 'break-all', - }, - whiteSpace: 'nowrap', - }, - link: { - color: theme.textColors.tableStatic, - }, - row: { - '& > button': { - height: 46, - }, - height: 46, - }, - selectionGrid: { - alignItems: 'center', - flexWrap: 'nowrap', - justifyContent: 'space-between', - [theme.breakpoints.down('sm')]: { - alignItems: 'flex-start', - flexDirection: 'column', - }, - width: '100%', - }, - selectionGridButton: { - [theme.breakpoints.down('sm')]: { - '&.MuiGrid-item': { - paddingLeft: 0, - paddingTop: 0, - }, - }, - }, - selectionGridDetails: { - [theme.breakpoints.down('sm')]: { - '&.MuiGrid-item': { - marginTop: 4, - paddingBottom: 0, - }, - }, - }, - stackScriptCell: { - padding: 0, - width: '100%', - }, - stackScriptUsername: { - color: theme.textColors.tableStatic, - fontSize: '0.875rem', - lineHeight: '1.125rem', - }, - }); diff --git a/packages/manager/src/features/StackScripts/StackScriptsLanding.tsx b/packages/manager/src/features/StackScripts/StackScriptsLanding.tsx index c615eebb91f..512fe59eff4 100644 --- a/packages/manager/src/features/StackScripts/StackScriptsLanding.tsx +++ b/packages/manager/src/features/StackScripts/StackScriptsLanding.tsx @@ -10,7 +10,7 @@ import { Notice } from 'src/components/Notice/Notice'; import { listToItemsByID } from 'src/queries/base'; import { useAllImagesQuery } from 'src/queries/images'; -import StackScriptPanel from './StackScriptPanel'; +import StackScriptPanel from './StackScriptPanel/StackScriptPanel'; export const StackScriptsLanding = () => { const history = useHistory<{ diff --git a/packages/manager/src/features/StackScripts/UserDefinedFieldsPanel/FieldTypes/UserDefinedMultiSelect.tsx b/packages/manager/src/features/StackScripts/UserDefinedFieldsPanel/FieldTypes/UserDefinedMultiSelect.tsx index e8c3109185d..77989a8f7ff 100644 --- a/packages/manager/src/features/StackScripts/UserDefinedFieldsPanel/FieldTypes/UserDefinedMultiSelect.tsx +++ b/packages/manager/src/features/StackScripts/UserDefinedFieldsPanel/FieldTypes/UserDefinedMultiSelect.tsx @@ -24,9 +24,7 @@ interface State { manyof: string[]; } -type CombinedProps = Props; - -class UserDefinedMultiSelect extends React.Component { +class UserDefinedMultiSelect extends React.Component { render() { const { manyof } = this.state; const { error, field, value: propValue } = this.props; @@ -79,4 +77,4 @@ class UserDefinedMultiSelect extends React.Component { }; } -export default RenderGuard(UserDefinedMultiSelect); +export default RenderGuard(UserDefinedMultiSelect); diff --git a/packages/manager/src/features/StackScripts/UserDefinedFieldsPanel/FieldTypes/UserDefinedSelect.tsx b/packages/manager/src/features/StackScripts/UserDefinedFieldsPanel/FieldTypes/UserDefinedSelect.tsx index f2cb493de36..779e1309cde 100644 --- a/packages/manager/src/features/StackScripts/UserDefinedFieldsPanel/FieldTypes/UserDefinedSelect.tsx +++ b/packages/manager/src/features/StackScripts/UserDefinedFieldsPanel/FieldTypes/UserDefinedSelect.tsx @@ -1,6 +1,5 @@ import { UserDefinedField } from '@linode/api-v4/lib/stackscripts'; -import { Theme } from '@mui/material/styles'; -import { makeStyles } from '@mui/styles'; +import { styled } from '@mui/material/styles'; import * as React from 'react'; import { Autocomplete } from 'src/components/Autocomplete/Autocomplete'; @@ -9,19 +8,6 @@ import { InputLabel } from 'src/components/InputLabel'; import { Notice } from 'src/components/Notice/Notice'; import { Radio } from 'src/components/Radio/Radio'; -const useStyles = makeStyles((theme: Theme) => ({ - radioGroupLabel: { - display: 'block', - marginBottom: '4px', - }, - root: { - display: 'flex', - flexDirection: 'column', - margin: `${theme.spacing(3)} 0 0`, - marginTop: '16px', - }, -})); - interface Props { error?: string; field: UserDefinedField; @@ -31,8 +17,6 @@ interface Props { } export const UserDefinedSelect = (props: Props) => { - const classes = useStyles(); - const { error, field, isOptional, updateFormState, value } = props; const [oneof, setOneof] = React.useState(field.oneof!.split(',')); @@ -62,12 +46,12 @@ export const UserDefinedSelect = (props: Props) => { ); } return ( -
    + {error && } - + {field.label} {!isOptional && '*'} - + {oneof.map((choice: string, index) => ( { value={choice} /> ))} -
    + ); }; + +const StyledInputLabel = styled(InputLabel, { label: 'StyledInputLabel' })({ + display: 'block', + marginBottom: '4px', +}); + +const StyledRootDiv = styled('div', { label: 'StyledRootDiv' })( + ({ theme }) => ({ + display: 'flex', + flexDirection: 'column', + margin: `${theme.spacing(3)} 0 0`, + marginTop: theme.spacing(2), + }) +); diff --git a/packages/manager/src/features/StackScripts/UserDefinedFieldsPanel/FieldTypes/UserDefinedText.tsx b/packages/manager/src/features/StackScripts/UserDefinedFieldsPanel/FieldTypes/UserDefinedText.tsx index d9f1116df4b..2b787736273 100644 --- a/packages/manager/src/features/StackScripts/UserDefinedFieldsPanel/FieldTypes/UserDefinedText.tsx +++ b/packages/manager/src/features/StackScripts/UserDefinedFieldsPanel/FieldTypes/UserDefinedText.tsx @@ -1,23 +1,11 @@ import { UserDefinedField } from '@linode/api-v4/lib/stackscripts'; -import { WithStyles, createStyles, withStyles } from '@mui/styles'; -import classNames from 'classnames'; +import { styled } from '@mui/material/styles'; import * as React from 'react'; import { AccessPanel } from 'src/components/AccessPanel/AccessPanel'; import { RenderGuard } from 'src/components/RenderGuard'; import { TextField } from 'src/components/TextField'; - -type ClassNames = 'accessPanel' | 'marginTop'; - -const styles = () => - createStyles({ - accessPanel: { - padding: 0, - }, - marginTop: { - marginTop: 0, - }, - }); +import { omittedProps } from 'src/utilities/omittedProps'; interface Props { error?: string; @@ -31,9 +19,7 @@ interface Props { value: string; } -type CombinedProps = Props & WithStyles; - -class UserDefinedText extends React.Component { +class UserDefinedText extends React.Component { render() { return (
    @@ -56,7 +42,6 @@ class UserDefinedText extends React.Component { renderPasswordField = () => { const { - classes, error, field, isOptional, @@ -66,11 +51,7 @@ class UserDefinedText extends React.Component { } = this.props; return ( - { }; } -const styled = withStyles(styles); +type StyledAccessPanelProps = Pick; + +const StyledAccessPanel = styled(AccessPanel, { + label: 'StyledAccessPanel', + shouldForwardProp: omittedProps(['isOptional']), +})(({ isOptional }) => ({ + padding: 0, + ...(!isOptional && { margin: 0 }), +})); -export default styled(RenderGuard(UserDefinedText)); +export default RenderGuard(UserDefinedText); diff --git a/packages/manager/src/features/StackScripts/UserDefinedFieldsPanel/UserDefinedFieldsPanel.styles.ts b/packages/manager/src/features/StackScripts/UserDefinedFieldsPanel/UserDefinedFieldsPanel.styles.ts new file mode 100644 index 00000000000..111d44794ae --- /dev/null +++ b/packages/manager/src/features/StackScripts/UserDefinedFieldsPanel/UserDefinedFieldsPanel.styles.ts @@ -0,0 +1,38 @@ +import { styled } from '@mui/material/styles'; + +import { Box } from 'src/components/Box'; +import { Paper } from 'src/components/Paper'; +import { omittedProps } from 'src/utilities/omittedProps'; + +export const StyledBox = styled(Box, { label: 'StyledBox' })(({ theme }) => ({ + '& > img': { + height: 60, + width: 60, + }, + alignItems: 'center', + columnGap: theme.spacing(), + display: 'flex', +})); + +type StyledPaperProps = { + isDrawerOpenable: boolean; +}; + +export const StyledPaper = styled(Paper, { + label: 'StyledPaper', + shouldForwardProp: omittedProps(['isDrawerOpenable']), +})(({ isDrawerOpenable, theme }) => ({ + '& > div:last-child': { + border: 0, + marginBottom: 0, + paddingBottom: 0, + }, + ...(isDrawerOpenable + ? { + padding: `${theme.spacing()} ${theme.spacing(3)}`, + } + : { + padding: theme.spacing(3), + }), + marginBottom: theme.spacing(3), +})); diff --git a/packages/manager/src/features/StackScripts/UserDefinedFieldsPanel/UserDefinedFieldsPanel.tsx b/packages/manager/src/features/StackScripts/UserDefinedFieldsPanel/UserDefinedFieldsPanel.tsx index 44cbb9212a3..b8a5ad9267f 100644 --- a/packages/manager/src/features/StackScripts/UserDefinedFieldsPanel/UserDefinedFieldsPanel.tsx +++ b/packages/manager/src/features/StackScripts/UserDefinedFieldsPanel/UserDefinedFieldsPanel.tsx @@ -1,16 +1,12 @@ import { UserDefinedField } from '@linode/api-v4/lib/stackscripts'; import { APIError } from '@linode/api-v4/lib/types'; import Grid from '@mui/material/Unstable_Grid2'; -import { Theme } from '@mui/material/styles'; -import { makeStyles } from '@mui/styles'; -import classnames from 'classnames'; import * as React from 'react'; import { Link } from 'react-router-dom'; import { Box } from 'src/components/Box'; import { Divider } from 'src/components/Divider'; import { Notice } from 'src/components/Notice/Notice'; -import { Paper } from 'src/components/Paper'; import { RenderGuard } from 'src/components/RenderGuard'; import { ShowMoreExpansion } from 'src/components/ShowMoreExpansion'; import { Typography } from 'src/components/Typography'; @@ -19,40 +15,7 @@ import { AppInfo } from '../../Linodes/LinodesCreate/AppInfo'; import UserDefinedMultiSelect from './FieldTypes/UserDefinedMultiSelect'; import { UserDefinedSelect } from './FieldTypes/UserDefinedSelect'; import UserDefinedText from './FieldTypes/UserDefinedText'; - -const useStyles = makeStyles((theme: Theme) => ({ - advDescription: { - margin: `${theme.spacing(2)} 0`, - }, - clusterNotice: { - paddingTop: '1rem', - }, - header: { - '& > img': { - height: 60, - width: 60, - }, - alignItems: 'center', - columnGap: theme.spacing(), - display: 'flex', - }, - marketplaceSpacing: { - paddingBottom: theme.spacing(), - paddingTop: theme.spacing(), - }, - root: { - '& > div:last-child': { - border: 0, - marginBottom: 0, - paddingBottom: 0, - }, - marginBottom: theme.spacing(3), - padding: theme.spacing(3), - }, - username: { - color: theme.color.grey1, - }, -})); +import { StyledBox, StyledPaper } from './UserDefinedFieldsPanel.styles'; interface Props { appLogo?: JSX.Element; @@ -66,8 +29,6 @@ interface Props { userDefinedFields?: UserDefinedField[]; } -type CombinedProps = Props; - const renderField = ( udf_data: any, handleChange: Props['handleChange'], @@ -177,14 +138,13 @@ const renderField = ( }; const handleOpenDrawer = ( - openDrawer: CombinedProps['openDrawer'], + openDrawer: Props['openDrawer'], selectedLabel: string ) => () => { openDrawer?.(selectedLabel); }; -const UserDefinedFieldsPanel = (props: CombinedProps) => { - const classes = useStyles(); +const UserDefinedFieldsPanel = (props: Props) => { const { appLogo, errors, @@ -217,13 +177,11 @@ const UserDefinedFieldsPanel = (props: CombinedProps) => { const isDrawerOpenable = openDrawer !== undefined; return ( - - + {appLogo} {`${selectedLabel} Setup`} @@ -231,19 +189,16 @@ const UserDefinedFieldsPanel = (props: CombinedProps) => { {isDrawerOpenable ? ( ) : null} - + {isCluster ? ( -
    + You are creating a cluster with {numberOfNodes} nodes. -
    + ) : null} {/* Required Fields */} @@ -255,7 +210,10 @@ const UserDefinedFieldsPanel = (props: CombinedProps) => { {optionalUDFs.length !== 0 && ( <> - + ({ margin: `${theme.spacing(2)} 0px` })} + variant="body1" + > These fields are additional configuration options and are not required for creation. @@ -268,7 +226,7 @@ const UserDefinedFieldsPanel = (props: CombinedProps) => { )} -
    + ); }; diff --git a/packages/manager/src/features/StackScripts/UserDefinedFieldsPanel/index.tsx b/packages/manager/src/features/StackScripts/UserDefinedFieldsPanel/index.tsx deleted file mode 100644 index 1384e8e416e..00000000000 --- a/packages/manager/src/features/StackScripts/UserDefinedFieldsPanel/index.tsx +++ /dev/null @@ -1,2 +0,0 @@ -import UserDefinedFieldsPanel from './UserDefinedFieldsPanel'; -export default UserDefinedFieldsPanel; diff --git a/packages/manager/src/features/StackScripts/index.tsx b/packages/manager/src/features/StackScripts/index.tsx deleted file mode 100644 index e7c8d94304e..00000000000 --- a/packages/manager/src/features/StackScripts/index.tsx +++ /dev/null @@ -1,2 +0,0 @@ -import StackScripts from './StackScripts'; -export default StackScripts; From 97fbca862dd05ed2402bda38a9cb15183fffaf3b Mon Sep 17 00:00:00 2001 From: Banks Nussman <115251059+bnussman-akamai@users.noreply.github.com> Date: Wed, 18 Oct 2023 13:07:14 -0400 Subject: [PATCH 19/45] chore: Clean up `src/utilities` exports (#9783) * clean up exports * Added changeset: Clean up `src/utilities` exports * move `omittedProps` --------- Co-authored-by: Banks Nussman --- .../pr-9783-tech-stories-1697046190232.md | 5 ++ packages/manager/src/App.test.tsx | 23 +----- packages/manager/src/App.tsx | 18 +---- packages/manager/src/MainContent.tsx | 4 +- .../HighlightedMarkdown.tsx | 2 +- .../components/ImageSelect/ImageSelect.tsx | 4 +- .../src/components/LineGraph/LineGraph.tsx | 2 +- .../src/components/MainContentBanner.tsx | 6 +- .../src/components/PrimaryNav/utils.ts | 2 +- .../PromotionalOfferCard.tsx | 5 +- packages/manager/src/context/index.ts | 1 - .../manager/src/context/useDialogContext.ts | 2 - .../DatabaseCreate/DatabaseCreate.tsx | 4 +- .../Domains/CreateDomain/CreateDomain.tsx | 2 +- .../features/Domains/DomainRecordDrawer.tsx | 14 ++-- .../src/features/Domains/DomainRecords.tsx | 2 +- .../APIMaintenanceBanner.tsx | 2 +- .../src/features/Help/Panels/SearchItem.tsx | 2 +- .../src/features/Help/StatusBanners.tsx | 2 +- .../Images/ImagesCreate/CreateImageTab.tsx | 2 +- .../src/features/Images/ImagesDrawer.tsx | 2 +- .../CreateCluster/CreateCluster.tsx | 2 +- .../NodePoolsDisplay/AddNodePoolDrawer.tsx | 2 +- .../Linodes/LinodesCreate/AttachVLAN.tsx | 2 +- .../LinodesCreate/LinodeCreateContainer.tsx | 2 +- .../TabbedContent/FromAppsContent.tsx | 4 +- .../TabbedContent/FromBackupsContent.tsx | 4 +- .../TabbedContent/FromLinodeContent.tsx | 4 +- .../TabbedContent/FromStackScriptContent.tsx | 4 +- .../Linodes/LinodesCreate/VLANAccordion.tsx | 2 +- .../Linodes/LinodesCreate/VPCPanel.tsx | 2 +- .../LinodeBackup/ScheduleSettings.tsx | 6 +- .../LinodeConfigs/LinodeConfigDialog.tsx | 9 ++- .../LinodeRebuild/RebuildFromImage.tsx | 2 +- .../LinodeRebuild/RebuildFromStackScript.tsx | 2 +- .../LinodeRescue/DeviceSelection.tsx | 2 +- .../LinodeRescue/StandardRescueDialog.tsx | 3 +- .../LinodeResize/LinodeResize.tsx | 2 +- .../LinodeSettingsAlertsPanel.tsx | 2 +- .../LinodeSummary/ActivityRow.tsx | 2 +- .../LinodeDetailHeader.tsx | 2 +- .../Linodes/MigrateLinode/MigrateLinode.tsx | 2 +- .../CreateServiceTargetDrawer.tsx | 2 +- .../DetailTabs/Apache/ApacheGraphs.tsx | 4 +- .../ManagedChartPanel.tsx | 2 +- .../Managed/Monitors/IssueCalendar.tsx | 2 +- .../NodeBalancers/NodeBalancerCreate.tsx | 4 +- .../NodeBalancerConfigurations.tsx | 2 +- .../NodeBalancerSummary/TablesPanel.tsx | 2 +- .../NotificationData/RenderNotification.tsx | 4 +- .../NotificationData/useEventInfo.ts | 2 +- .../features/OneClickApps/AppDetailDrawer.tsx | 2 +- .../TwoFactor/EnableTwoFactorForm.tsx | 2 +- .../TwoFactor/TwoFactor.tsx | 2 +- .../Profile/LishSettings/LishSettings.tsx | 4 +- .../OAuthClients/CreateOAuthClientDrawer.tsx | 4 +- .../OAuthClients/EditOAuthClientDrawer.tsx | 4 +- .../Profile/SSHKeys/CreateSSHKeyDrawer.tsx | 2 +- .../Profile/SSHKeys/EditSSHKeyDrawer.tsx | 2 +- .../features/Profile/Settings/Settings.tsx | 2 +- .../StackScriptCreate/StackScriptCreate.tsx | 6 +- .../StackScriptForm/StackScriptForm.tsx | 4 +- .../StackScriptPanel/StackScriptsSection.tsx | 2 +- .../SupportTickets/SupportTicketDialog.tsx | 2 +- .../src/features/Users/CreateUserDrawer.tsx | 4 +- .../src/features/Users/UserPermissions.tsx | 6 +- .../src/features/Users/UserProfile.tsx | 8 +- .../src/features/Users/UsersLanding.tsx | 2 +- .../src/features/VPCs/VPCCreate/VPCCreate.tsx | 2 +- .../features/Volumes/AttachVolumeDrawer.tsx | 4 +- .../features/components/PlansPanel/utils.ts | 2 +- packages/manager/src/hooks/useErrors.ts | 2 +- .../manager/src/store/events/event.helpers.ts | 2 +- .../utilities/__mocks__/getUserTimezone.ts | 4 +- .../arrayToDelimiterSeparatedList/index.ts | 1 - .../arrayToList.test.ts | 2 +- .../arrayToList.ts | 5 +- .../manager/src/utilities/composeState.ts | 14 ---- .../createDevicesFromStrings.test.ts | 2 +- .../createDevicesFromStrings.ts | 6 +- .../createDevicesFromStrings/index.ts | 6 -- .../createStringsFromDevices.test.ts | 2 +- .../createStringsFromDevices.ts | 2 +- .../createStringsFromDevices/index.ts | 2 - packages/manager/src/utilities/formatDate.ts | 2 +- .../manager/src/utilities/getAPIErrorFor.ts | 2 +- ...etSelectedOptionFromGroupedOptions.test.ts | 0 .../getSelectedOptionFromGroupedOptions.ts | 0 .../index.ts | 1 - .../src/utilities/getUserTimezone.test.ts | 2 +- .../manager/src/utilities/getUserTimezone.ts | 4 +- .../minute-conversion.test.ts | 0 .../minute-conversion.ts | 2 +- .../src/utilities/minute-conversion/index.ts | 4 - .../{omittedProps => }/omittedProps.test.tsx | 0 .../{omittedProps => }/omittedProps.ts | 0 .../src/utilities/omittedProps/index.ts | 1 - .../utilities/{roundTo => }/roundTo.test.ts | 0 .../src/utilities/{roundTo => }/roundTo.ts | 0 .../manager/src/utilities/roundTo/index.ts | 1 - .../src/utilities/routing/isPathOneOf.test.ts | 2 +- .../src/utilities/routing/isPathOneOf.ts | 2 +- .../safeGetImageLabel.test.ts | 0 .../safeGetImageLabel.ts | 0 .../src/utilities/safeGetImageLabel/index.ts | 1 - .../{sanitize-html => }/sanitize-html.test.ts | 0 .../src/utilities/sanitize-html/index.ts | 1 - .../{sanitize-html => }/sanitizeHTML.ts | 0 .../src/utilities/scrollErrorIntoView.ts | 4 +- .../utilities/shouldRequestEntity/index.ts | 4 - .../shouldRequestEntity.test.ts | 77 ------------------- .../shouldRequestEntity.ts | 33 -------- .../manager/src/utilities/stripImageName.ts | 4 +- .../manager/src/utilities/substituteLink.tsx | 18 ----- packages/manager/src/utilities/updateOrAdd.ts | 17 ---- packages/manager/src/utilities/updateRight.ts | 4 +- packages/manager/src/utilities/userAgent.ts | 1 + .../validatePassword.test.ts | 0 .../validatePassword.ts | 0 .../src/utilities/validatePassword/index.ts | 1 - 120 files changed, 135 insertions(+), 361 deletions(-) create mode 100644 packages/manager/.changeset/pr-9783-tech-stories-1697046190232.md delete mode 100644 packages/manager/src/context/index.ts delete mode 100644 packages/manager/src/utilities/arrayToDelimiterSeparatedList/index.ts rename packages/manager/src/utilities/{arrayToDelimiterSeparatedList => }/arrayToList.test.ts (95%) rename packages/manager/src/utilities/{arrayToDelimiterSeparatedList => }/arrayToList.ts (80%) delete mode 100644 packages/manager/src/utilities/composeState.ts rename packages/manager/src/utilities/{createDevicesFromStrings => }/createDevicesFromStrings.test.ts (96%) rename packages/manager/src/utilities/{createDevicesFromStrings => }/createDevicesFromStrings.ts (91%) delete mode 100644 packages/manager/src/utilities/createDevicesFromStrings/index.ts rename packages/manager/src/utilities/{createStringsFromDevices => }/createStringsFromDevices.test.ts (94%) rename packages/manager/src/utilities/{createStringsFromDevices => }/createStringsFromDevices.ts (91%) delete mode 100644 packages/manager/src/utilities/createStringsFromDevices/index.ts rename packages/manager/src/utilities/{getSelectedOptionFromGroupedOptions => }/getSelectedOptionFromGroupedOptions.test.ts (100%) rename packages/manager/src/utilities/{getSelectedOptionFromGroupedOptions => }/getSelectedOptionFromGroupedOptions.ts (100%) delete mode 100644 packages/manager/src/utilities/getSelectedOptionFromGroupedOptions/index.ts rename packages/manager/src/utilities/{minute-conversion => }/minute-conversion.test.ts (100%) rename packages/manager/src/utilities/{minute-conversion => }/minute-conversion.ts (98%) delete mode 100644 packages/manager/src/utilities/minute-conversion/index.ts rename packages/manager/src/utilities/{omittedProps => }/omittedProps.test.tsx (100%) rename packages/manager/src/utilities/{omittedProps => }/omittedProps.ts (100%) delete mode 100644 packages/manager/src/utilities/omittedProps/index.ts rename packages/manager/src/utilities/{roundTo => }/roundTo.test.ts (100%) rename packages/manager/src/utilities/{roundTo => }/roundTo.ts (100%) delete mode 100644 packages/manager/src/utilities/roundTo/index.ts rename packages/manager/src/utilities/{safeGetImageLabel => }/safeGetImageLabel.test.ts (100%) rename packages/manager/src/utilities/{safeGetImageLabel => }/safeGetImageLabel.ts (100%) delete mode 100644 packages/manager/src/utilities/safeGetImageLabel/index.ts rename packages/manager/src/utilities/{sanitize-html => }/sanitize-html.test.ts (100%) delete mode 100644 packages/manager/src/utilities/sanitize-html/index.ts rename packages/manager/src/utilities/{sanitize-html => }/sanitizeHTML.ts (100%) delete mode 100644 packages/manager/src/utilities/shouldRequestEntity/index.ts delete mode 100644 packages/manager/src/utilities/shouldRequestEntity/shouldRequestEntity.test.ts delete mode 100644 packages/manager/src/utilities/shouldRequestEntity/shouldRequestEntity.ts delete mode 100644 packages/manager/src/utilities/substituteLink.tsx delete mode 100644 packages/manager/src/utilities/updateOrAdd.ts create mode 100644 packages/manager/src/utilities/userAgent.ts rename packages/manager/src/utilities/{validatePassword => }/validatePassword.test.ts (100%) rename packages/manager/src/utilities/{validatePassword => }/validatePassword.ts (100%) delete mode 100644 packages/manager/src/utilities/validatePassword/index.ts diff --git a/packages/manager/.changeset/pr-9783-tech-stories-1697046190232.md b/packages/manager/.changeset/pr-9783-tech-stories-1697046190232.md new file mode 100644 index 00000000000..7a8243662da --- /dev/null +++ b/packages/manager/.changeset/pr-9783-tech-stories-1697046190232.md @@ -0,0 +1,5 @@ +--- +"@linode/manager": Tech Stories +--- + +Clean up `src/utilities` exports ([#9783](https://github.com/linode/manager/pull/9783)) diff --git a/packages/manager/src/App.test.tsx b/packages/manager/src/App.test.tsx index 3f307c89e9d..d9410cf2872 100644 --- a/packages/manager/src/App.test.tsx +++ b/packages/manager/src/App.test.tsx @@ -1,10 +1,9 @@ -import { APIError } from '@linode/api-v4/lib/types'; import { shallow } from 'enzyme'; import * as React from 'react'; import { Provider } from 'react-redux'; import { StaticRouter } from 'react-router-dom'; -import { App, hasOauthError } from './App'; +import { App } from './App'; import { LinodeThemeWrapper } from './LinodeThemeWrapper'; import { queryClientFactory } from './queries/base'; import { storeFactory } from './store'; @@ -23,23 +22,3 @@ it('renders without crashing.', () => { ); expect(component.find('App')).toHaveLength(1); }); - -const errors: (APIError[] | Error)[] = [ - [ - { - reason: 'invalid OAuTh token', - }, - ], - new Error('hello world'), - [ - { - reason: 'Invalid Something else', - }, - ], - new Error('hello world again'), -]; - -it('isOAuthError returns true for errors that have oauth errors in them', () => { - expect(hasOauthError(errors[0], errors[1])).toBeTruthy(); - expect(hasOauthError(errors[2], errors[3])).toBeFalsy(); -}); diff --git a/packages/manager/src/App.tsx b/packages/manager/src/App.tsx index 314216af266..47e7ac976c0 100644 --- a/packages/manager/src/App.tsx +++ b/packages/manager/src/App.tsx @@ -1,8 +1,6 @@ -import { APIError } from '@linode/api-v4/lib/types'; import '@reach/tabs/styles.css'; import { ErrorBoundary } from '@sentry/react'; import { useSnackbar } from 'notistack'; -import { pathOr } from 'ramda'; import * as React from 'react'; import { useSelector } from 'react-redux'; import { useHistory } from 'react-router-dom'; @@ -41,6 +39,7 @@ import { tokenEventHandler } from './queries/tokens'; import { volumeEventsHandler } from './queries/volumes'; import { ApplicationState } from './store'; import { getNextThemeValue } from './utilities/theme'; +import { isOSMac } from './utilities/userAgent'; // Ensure component's display name is 'App' export const App = () => ; @@ -292,18 +291,3 @@ const BaseApp = withDocumentTitleProvider( }) ) ); - -export const hasOauthError = (...args: (APIError[] | Error | undefined)[]) => { - return args.some((eachError) => { - const cleanedError: JSX.Element | string = pathOr( - '', - [0, 'reason'], - eachError - ); - return typeof cleanedError !== 'string' - ? false - : cleanedError.toLowerCase().includes('oauth'); - }); -}; - -export const isOSMac = navigator.userAgent.includes('Mac'); diff --git a/packages/manager/src/MainContent.tsx b/packages/manager/src/MainContent.tsx index 7c0e04e02d6..e10c6310694 100644 --- a/packages/manager/src/MainContent.tsx +++ b/packages/manager/src/MainContent.tsx @@ -8,7 +8,7 @@ import { makeStyles } from 'tss-react/mui'; import Logo from 'src/assets/logo/akamai-logo.svg'; import { Box } from 'src/components/Box'; -import MainContentBanner from 'src/components/MainContentBanner'; +import { MainContentBanner } from 'src/components/MainContentBanner'; import { MaintenanceScreen } from 'src/components/MaintenanceScreen'; import { NotFound } from 'src/components/NotFound'; import { PreferenceToggle } from 'src/components/PreferenceToggle/PreferenceToggle'; @@ -17,7 +17,7 @@ import { SuspenseLoader } from 'src/components/SuspenseLoader'; import withGlobalErrors, { Props as GlobalErrorProps, } from 'src/containers/globalErrors.container'; -import { useDialogContext } from 'src/context'; +import { useDialogContext } from 'src/context/useDialogContext'; import { Footer } from 'src/features/Footer/Footer'; import { GlobalNotifications } from 'src/features/GlobalNotifications/GlobalNotifications'; import { diff --git a/packages/manager/src/components/HighlightedMarkdown/HighlightedMarkdown.tsx b/packages/manager/src/components/HighlightedMarkdown/HighlightedMarkdown.tsx index 706ecb4f4fe..4fa75a774bc 100644 --- a/packages/manager/src/components/HighlightedMarkdown/HighlightedMarkdown.tsx +++ b/packages/manager/src/components/HighlightedMarkdown/HighlightedMarkdown.tsx @@ -13,7 +13,7 @@ import { Typography } from 'src/components/Typography'; import 'src/formatted-text.css'; import { ThemeName } from 'src/foundations/themes'; import { unsafe_MarkdownIt } from 'src/utilities/markdown'; -import { sanitizeHTML } from 'src/utilities/sanitize-html'; +import { sanitizeHTML } from 'src/utilities/sanitizeHTML'; import { useColorMode } from 'src/utilities/theme'; hljs.registerLanguage('apache', apache); diff --git a/packages/manager/src/components/ImageSelect/ImageSelect.tsx b/packages/manager/src/components/ImageSelect/ImageSelect.tsx index bc4b33dc197..8dc32ad684d 100644 --- a/packages/manager/src/components/ImageSelect/ImageSelect.tsx +++ b/packages/manager/src/components/ImageSelect/ImageSelect.tsx @@ -9,13 +9,13 @@ import Select, { GroupType, Item } from 'src/components/EnhancedSelect'; import { BaseSelectProps } from 'src/components/EnhancedSelect/Select'; import { _SingleValue } from 'src/components/EnhancedSelect/components/SingleValue'; import { ImageOption } from 'src/components/ImageSelect/ImageOption'; -import { Typography } from 'src/components/Typography'; import { Paper } from 'src/components/Paper'; +import { Typography } from 'src/components/Typography'; import { MAX_MONTHS_EOL_FILTER } from 'src/constants'; import { useAllImagesQuery } from 'src/queries/images'; import { arePropsEqual } from 'src/utilities/arePropsEqual'; import { getAPIErrorOrDefault } from 'src/utilities/errorUtils'; -import getSelectedOptionFromGroupedOptions from 'src/utilities/getSelectedOptionFromGroupedOptions'; +import { getSelectedOptionFromGroupedOptions } from 'src/utilities/getSelectedOptionFromGroupedOptions'; import { distroIcons } from './icons'; diff --git a/packages/manager/src/components/LineGraph/LineGraph.tsx b/packages/manager/src/components/LineGraph/LineGraph.tsx index ee342152674..5f9f838cf21 100644 --- a/packages/manager/src/components/LineGraph/LineGraph.tsx +++ b/packages/manager/src/components/LineGraph/LineGraph.tsx @@ -16,7 +16,7 @@ import { TableCell } from 'src/components/TableCell'; import { TableRow } from 'src/components/TableRow'; import { Typography } from 'src/components/Typography'; import { setUpCharts } from 'src/utilities/charts'; -import roundTo from 'src/utilities/roundTo'; +import { roundTo } from 'src/utilities/roundTo'; import { Metrics } from 'src/utilities/statMetrics'; import AccessibleGraphData from './AccessibleGraphData'; diff --git a/packages/manager/src/components/MainContentBanner.tsx b/packages/manager/src/components/MainContentBanner.tsx index 7f95a6dae44..fa52bd0a8aa 100644 --- a/packages/manager/src/components/MainContentBanner.tsx +++ b/packages/manager/src/components/MainContentBanner.tsx @@ -51,7 +51,7 @@ interface Props { url: string; } -const MainContentBanner = (props: Props) => { +export const MainContentBanner = React.memo((props: Props) => { const { bannerKey, bannerText, linkText, onClose, url } = props; const { refetch: refetchPrefrences } = usePreferences(); @@ -94,6 +94,4 @@ const MainContentBanner = (props: Props) => { ); -}; - -export default React.memo(MainContentBanner); +}); diff --git a/packages/manager/src/components/PrimaryNav/utils.ts b/packages/manager/src/components/PrimaryNav/utils.ts index 6f6f600ed7b..72dffac7ad5 100644 --- a/packages/manager/src/components/PrimaryNav/utils.ts +++ b/packages/manager/src/components/PrimaryNav/utils.ts @@ -1,4 +1,4 @@ -import isPathOneOf from 'src/utilities/routing/isPathOneOf'; +import { isPathOneOf } from 'src/utilities/routing/isPathOneOf'; export const linkIsActive = ( href: string, diff --git a/packages/manager/src/components/PromotionalOfferCard/PromotionalOfferCard.tsx b/packages/manager/src/components/PromotionalOfferCard/PromotionalOfferCard.tsx index af0d0a9c050..1e0693ca792 100644 --- a/packages/manager/src/components/PromotionalOfferCard/PromotionalOfferCard.tsx +++ b/packages/manager/src/components/PromotionalOfferCard/PromotionalOfferCard.tsx @@ -9,10 +9,7 @@ import { Typography } from 'src/components/Typography'; import { Paper } from 'src/components/Paper'; import { PromotionalOffer } from 'src/featureFlags'; import { useWindowDimensions } from 'src/hooks/useWindowDimensions'; -import { - offSiteURL, - onSiteURL, -} from 'src/utilities/sanitize-html/sanitizeHTML'; +import { offSiteURL, onSiteURL } from 'src/utilities/sanitizeHTML'; const useStyles = makeStyles()((theme: Theme) => ({ alignLeft: { diff --git a/packages/manager/src/context/index.ts b/packages/manager/src/context/index.ts deleted file mode 100644 index 0e5248f275c..00000000000 --- a/packages/manager/src/context/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './useDialogContext'; diff --git a/packages/manager/src/context/useDialogContext.ts b/packages/manager/src/context/useDialogContext.ts index 1998586a25f..9dc2bb3e41f 100644 --- a/packages/manager/src/context/useDialogContext.ts +++ b/packages/manager/src/context/useDialogContext.ts @@ -24,5 +24,3 @@ export const useDialogContext = (): DialogContextProps => { open, }; }; - -export default useDialogContext; diff --git a/packages/manager/src/features/Databases/DatabaseCreate/DatabaseCreate.tsx b/packages/manager/src/features/Databases/DatabaseCreate/DatabaseCreate.tsx index a057c0f82a6..f66a874cbd8 100644 --- a/packages/manager/src/features/Databases/DatabaseCreate/DatabaseCreate.tsx +++ b/packages/manager/src/features/Databases/DatabaseCreate/DatabaseCreate.tsx @@ -54,13 +54,13 @@ import { import { useRegionsQuery } from 'src/queries/regions'; import { formatStorageUnits } from 'src/utilities/formatStorageUnits'; import { handleAPIErrors } from 'src/utilities/formikErrorUtils'; -import getSelectedOptionFromGroupedOptions from 'src/utilities/getSelectedOptionFromGroupedOptions'; +import { getSelectedOptionFromGroupedOptions } from 'src/utilities/getSelectedOptionFromGroupedOptions'; import { ExtendedIP, ipFieldPlaceholder, validateIPs, } from 'src/utilities/ipUtils'; -import scrollErrorIntoView from 'src/utilities/scrollErrorIntoView'; +import { scrollErrorIntoView } from 'src/utilities/scrollErrorIntoView'; import type { PlanSelectionType } from 'src/features/components/PlansPanel/types'; diff --git a/packages/manager/src/features/Domains/CreateDomain/CreateDomain.tsx b/packages/manager/src/features/Domains/CreateDomain/CreateDomain.tsx index 0aaf0db5cbd..fa4b2a4fca2 100644 --- a/packages/manager/src/features/Domains/CreateDomain/CreateDomain.tsx +++ b/packages/manager/src/features/Domains/CreateDomain/CreateDomain.tsx @@ -43,7 +43,7 @@ import { extendedIPToString, stringToExtendedIP, } from 'src/utilities/ipUtils'; -import scrollErrorIntoView from 'src/utilities/scrollErrorIntoView'; +import { scrollErrorIntoView } from 'src/utilities/scrollErrorIntoView'; import { generateDefaultDomainRecords } from '../domainUtils'; diff --git a/packages/manager/src/features/Domains/DomainRecordDrawer.tsx b/packages/manager/src/features/Domains/DomainRecordDrawer.tsx index aec791b399e..704b47a308f 100644 --- a/packages/manager/src/features/Domains/DomainRecordDrawer.tsx +++ b/packages/manager/src/features/Domains/DomainRecordDrawer.tsx @@ -28,14 +28,14 @@ import { MultipleIPInput } from 'src/components/MultipleIPInput/MultipleIPInput' import { Notice } from 'src/components/Notice/Notice'; import { TextField } from 'src/components/TextField'; import { getAPIErrorOrDefault } from 'src/utilities/errorUtils'; -import getAPIErrorsFor from 'src/utilities/getAPIErrorFor'; +import { getAPIErrorFor } from 'src/utilities/getAPIErrorFor'; import { ExtendedIP, extendedIPToString, stringToExtendedIP, } from 'src/utilities/ipUtils'; import { maybeCastToNumber } from 'src/utilities/maybeCastToNumber'; -import scrollErrorIntoView from 'src/utilities/scrollErrorIntoView'; +import { scrollErrorIntoView } from 'src/utilities/scrollErrorIntoView'; import { getInitialIPs, @@ -137,8 +137,8 @@ export class DomainRecordDrawer extends React.Component< 'Please create an A/AAAA record for this domain to avoid a Zone File invalidation.'; const otherErrors = [ - getAPIErrorsFor({}, this.state.errors)('_unknown'), - getAPIErrorsFor({}, this.state.errors)('none'), + getAPIErrorFor({}, this.state.errors)('_unknown'), + getAPIErrorFor({}, this.state.errors)('none'), ].filter(Boolean); return ( @@ -192,7 +192,7 @@ export class DomainRecordDrawer extends React.Component< ).map(stringToExtendedIP); return ( { return ( ( { data: { diff --git a/packages/manager/src/features/Help/StatusBanners.tsx b/packages/manager/src/features/Help/StatusBanners.tsx index bb549f9fe18..85f58ced050 100644 --- a/packages/manager/src/features/Help/StatusBanners.tsx +++ b/packages/manager/src/features/Help/StatusBanners.tsx @@ -12,7 +12,7 @@ import { useIncidentQuery, } from 'src/queries/statusPage'; import { capitalize } from 'src/utilities/capitalize'; -import { sanitizeHTML } from 'src/utilities/sanitize-html'; +import { sanitizeHTML } from 'src/utilities/sanitizeHTML'; import { truncateEnd } from 'src/utilities/truncate'; export const StatusBanners = () => { diff --git a/packages/manager/src/features/Images/ImagesCreate/CreateImageTab.tsx b/packages/manager/src/features/Images/ImagesCreate/CreateImageTab.tsx index c51ac3715e7..608d5580ba8 100644 --- a/packages/manager/src/features/Images/ImagesCreate/CreateImageTab.tsx +++ b/packages/manager/src/features/Images/ImagesCreate/CreateImageTab.tsx @@ -22,7 +22,7 @@ import { useFlags } from 'src/hooks/useFlags'; import { useCreateImageMutation } from 'src/queries/images'; import { useGrants, useProfile } from 'src/queries/profile'; import { getAPIErrorOrDefault } from 'src/utilities/errorUtils'; -import getAPIErrorFor from 'src/utilities/getAPIErrorFor'; +import { getAPIErrorFor } from 'src/utilities/getAPIErrorFor'; const useStyles = makeStyles((theme: Theme) => ({ buttonGroup: { diff --git a/packages/manager/src/features/Images/ImagesDrawer.tsx b/packages/manager/src/features/Images/ImagesDrawer.tsx index b136469b5c5..6e6132d77f1 100644 --- a/packages/manager/src/features/Images/ImagesDrawer.tsx +++ b/packages/manager/src/features/Images/ImagesDrawer.tsx @@ -21,7 +21,7 @@ import { useUpdateImageMutation, } from 'src/queries/images'; import { getAPIErrorOrDefault } from 'src/utilities/errorUtils'; -import getAPIErrorFor from 'src/utilities/getAPIErrorFor'; +import { getAPIErrorFor } from 'src/utilities/getAPIErrorFor'; import { useImageAndLinodeGrantCheck } from './utils'; diff --git a/packages/manager/src/features/Kubernetes/CreateCluster/CreateCluster.tsx b/packages/manager/src/features/Kubernetes/CreateCluster/CreateCluster.tsx index ba3e2cf40f5..11fcdb8ebd2 100644 --- a/packages/manager/src/features/Kubernetes/CreateCluster/CreateCluster.tsx +++ b/packages/manager/src/features/Kubernetes/CreateCluster/CreateCluster.tsx @@ -45,7 +45,7 @@ import { filterCurrentTypes } from 'src/utilities/filterCurrentLinodeTypes'; import { plansNoticesUtils } from 'src/utilities/planNotices'; import { LKE_HA_PRICE } from 'src/utilities/pricing/constants'; import { getDCSpecificPrice } from 'src/utilities/pricing/dynamicPricing'; -import scrollErrorIntoView from 'src/utilities/scrollErrorIntoView'; +import { scrollErrorIntoView } from 'src/utilities/scrollErrorIntoView'; import KubeCheckoutBar from '../KubeCheckoutBar'; import { HAControlPlane } from './HAControlPlane'; diff --git a/packages/manager/src/features/Kubernetes/KubernetesClusterDetail/NodePoolsDisplay/AddNodePoolDrawer.tsx b/packages/manager/src/features/Kubernetes/KubernetesClusterDetail/NodePoolsDisplay/AddNodePoolDrawer.tsx index 19517207c81..5bb6bc36b0e 100644 --- a/packages/manager/src/features/Kubernetes/KubernetesClusterDetail/NodePoolsDisplay/AddNodePoolDrawer.tsx +++ b/packages/manager/src/features/Kubernetes/KubernetesClusterDetail/NodePoolsDisplay/AddNodePoolDrawer.tsx @@ -16,7 +16,7 @@ import { plansNoticesUtils } from 'src/utilities/planNotices'; import { pluralize } from 'src/utilities/pluralize'; import { renderMonthlyPriceToCorrectDecimalPlace } from 'src/utilities/pricing/dynamicPricing'; import { getLinodeRegionPrice } from 'src/utilities/pricing/linodes'; -import scrollErrorIntoView from 'src/utilities/scrollErrorIntoView'; +import { scrollErrorIntoView } from 'src/utilities/scrollErrorIntoView'; import { KubernetesPlansPanel } from '../../KubernetesPlansPanel/KubernetesPlansPanel'; import { nodeWarning } from '../../kubeUtils'; diff --git a/packages/manager/src/features/Linodes/LinodesCreate/AttachVLAN.tsx b/packages/manager/src/features/Linodes/LinodesCreate/AttachVLAN.tsx index 736d9889bc4..c3f6a0f5028 100644 --- a/packages/manager/src/features/Linodes/LinodesCreate/AttachVLAN.tsx +++ b/packages/manager/src/features/Linodes/LinodesCreate/AttachVLAN.tsx @@ -10,7 +10,7 @@ import { TooltipIcon } from 'src/components/TooltipIcon'; import { Typography } from 'src/components/Typography'; import { useRegionsQuery } from 'src/queries/regions'; import { queryKey as vlansQueryKey } from 'src/queries/vlans'; -import arrayToList from 'src/utilities/arrayToDelimiterSeparatedList'; +import { arrayToList } from 'src/utilities/arrayToList'; import { doesRegionSupportFeature, regionsWithFeature, diff --git a/packages/manager/src/features/Linodes/LinodesCreate/LinodeCreateContainer.tsx b/packages/manager/src/features/Linodes/LinodesCreate/LinodeCreateContainer.tsx index d898b613c84..b3f95a7d152 100644 --- a/packages/manager/src/features/Linodes/LinodesCreate/LinodeCreateContainer.tsx +++ b/packages/manager/src/features/Linodes/LinodesCreate/LinodeCreateContainer.tsx @@ -62,7 +62,7 @@ import { ExtendedType, extendType } from 'src/utilities/extendType'; import { isEURegion } from 'src/utilities/formatRegion'; import { getLinodeRegionPrice } from 'src/utilities/pricing/linodes'; import { getQueryParamsFromQueryString } from 'src/utilities/queryParams'; -import scrollErrorIntoView from 'src/utilities/scrollErrorIntoView'; +import { scrollErrorIntoView } from 'src/utilities/scrollErrorIntoView'; import { validatePassword } from 'src/utilities/validatePassword'; import LinodeCreate from './LinodeCreate'; diff --git a/packages/manager/src/features/Linodes/LinodesCreate/TabbedContent/FromAppsContent.tsx b/packages/manager/src/features/Linodes/LinodesCreate/TabbedContent/FromAppsContent.tsx index 33b0f3ce527..ed63f2cb351 100644 --- a/packages/manager/src/features/Linodes/LinodesCreate/TabbedContent/FromAppsContent.tsx +++ b/packages/manager/src/features/Linodes/LinodesCreate/TabbedContent/FromAppsContent.tsx @@ -18,7 +18,7 @@ import { AppDetailDrawer } from 'src/features/OneClickApps'; import { oneClickApps } from 'src/features/OneClickApps/oneClickApps'; import UserDefinedFieldsPanel from 'src/features/StackScripts/UserDefinedFieldsPanel/UserDefinedFieldsPanel'; import { sendMarketplaceSearchEvent } from 'src/utilities/analytics'; -import getAPIErrorsFor from 'src/utilities/getAPIErrorFor'; +import { getAPIErrorFor } from 'src/utilities/getAPIErrorFor'; import SelectAppPanel from '../SelectAppPanel'; import { @@ -184,7 +184,7 @@ export class FromAppsContent extends React.Component { query, } = this.state; - const hasErrorFor = getAPIErrorsFor(errorResources, errors); + const hasErrorFor = getAPIErrorFor(errorResources, errors); return ( diff --git a/packages/manager/src/features/Linodes/LinodesCreate/TabbedContent/FromBackupsContent.tsx b/packages/manager/src/features/Linodes/LinodesCreate/TabbedContent/FromBackupsContent.tsx index 1d5608f55c7..2a71b9dadf4 100644 --- a/packages/manager/src/features/Linodes/LinodesCreate/TabbedContent/FromBackupsContent.tsx +++ b/packages/manager/src/features/Linodes/LinodesCreate/TabbedContent/FromBackupsContent.tsx @@ -12,7 +12,7 @@ import { Placeholder } from 'src/components/Placeholder/Placeholder'; import { Paper } from 'src/components/Paper'; import { reportException } from 'src/exceptionReporting'; import { extendType } from 'src/utilities/extendType'; -import getAPIErrorsFor from 'src/utilities/getAPIErrorFor'; +import { getAPIErrorFor } from 'src/utilities/getAPIErrorFor'; import SelectBackupPanel from '../SelectBackupPanel'; import SelectLinodePanel from '../SelectLinodePanel'; @@ -87,7 +87,7 @@ export class FromBackupsContent extends React.Component { const extendedTypes = typesData?.map(extendType); - const hasErrorFor = getAPIErrorsFor(errorResources, errors); + const hasErrorFor = getAPIErrorFor(errorResources, errors); const userHasBackups = linodesData.some( (thisLinode) => thisLinode.backups.enabled diff --git a/packages/manager/src/features/Linodes/LinodesCreate/TabbedContent/FromLinodeContent.tsx b/packages/manager/src/features/Linodes/LinodesCreate/TabbedContent/FromLinodeContent.tsx index f2c7c7cb0d0..5c1d6f5c9b3 100644 --- a/packages/manager/src/features/Linodes/LinodesCreate/TabbedContent/FromLinodeContent.tsx +++ b/packages/manager/src/features/Linodes/LinodesCreate/TabbedContent/FromLinodeContent.tsx @@ -6,7 +6,7 @@ import { Placeholder } from 'src/components/Placeholder/Placeholder'; import { Paper } from 'src/components/Paper'; import { buildQueryStringForLinodeClone } from 'src/features/Linodes/LinodesLanding/LinodeActionMenu'; import { extendType } from 'src/utilities/extendType'; -import getAPIErrorsFor from 'src/utilities/getAPIErrorFor'; +import { getAPIErrorFor } from 'src/utilities/getAPIErrorFor'; import { StyledGrid } from './CommonTabbedContent.styles'; import SelectLinodePanel from '../SelectLinodePanel'; @@ -43,7 +43,7 @@ export const FromLinodeContent = (props: CombinedProps) => { const extendedTypes = typesData?.map(extendType); - const hasErrorFor = getAPIErrorsFor(errorResources, errors); + const hasErrorFor = getAPIErrorFor(errorResources, errors); const history = useHistory(); diff --git a/packages/manager/src/features/Linodes/LinodesCreate/TabbedContent/FromStackScriptContent.tsx b/packages/manager/src/features/Linodes/LinodesCreate/TabbedContent/FromStackScriptContent.tsx index 87d99759d5c..20ef138311b 100644 --- a/packages/manager/src/features/Linodes/LinodesCreate/TabbedContent/FromStackScriptContent.tsx +++ b/packages/manager/src/features/Linodes/LinodesCreate/TabbedContent/FromStackScriptContent.tsx @@ -11,7 +11,7 @@ import StackScriptDialog from 'src/features/StackScripts/StackScriptDialog'; import UserDefinedFieldsPanel from 'src/features/StackScripts/UserDefinedFieldsPanel/UserDefinedFieldsPanel'; import { StackScriptsRequest } from 'src/features/StackScripts/types'; import { filterImagesByType } from 'src/store/image/image.helpers'; -import getAPIErrorsFor from 'src/utilities/getAPIErrorFor'; +import { getAPIErrorFor } from 'src/utilities/getAPIErrorFor'; import { ReduxStateProps, @@ -64,7 +64,7 @@ export class FromStackScriptContent extends React.PureComponent { // we can assume that the StackScript specified any/all const showAllImages = equals(compatibleImages, Object.values(imagesData)); - const hasErrorFor = getAPIErrorsFor(errorResources, errors); + const hasErrorFor = getAPIErrorFor(errorResources, errors); return ( diff --git a/packages/manager/src/features/Linodes/LinodesCreate/VLANAccordion.tsx b/packages/manager/src/features/Linodes/LinodesCreate/VLANAccordion.tsx index 393f81ef7e7..e77e2c5ba78 100644 --- a/packages/manager/src/features/Linodes/LinodesCreate/VLANAccordion.tsx +++ b/packages/manager/src/features/Linodes/LinodesCreate/VLANAccordion.tsx @@ -9,7 +9,7 @@ import { TooltipIcon } from 'src/components/TooltipIcon'; import { Typography } from 'src/components/Typography'; import { useRegionsQuery } from 'src/queries/regions'; import { queryKey as vlansQueryKey } from 'src/queries/vlans'; -import arrayToList from 'src/utilities/arrayToDelimiterSeparatedList'; +import { arrayToList } from 'src/utilities/arrayToList'; import { doesRegionSupportFeature, regionsWithFeature, diff --git a/packages/manager/src/features/Linodes/LinodesCreate/VPCPanel.tsx b/packages/manager/src/features/Linodes/LinodesCreate/VPCPanel.tsx index 26bacff7a69..f841d73cabd 100644 --- a/packages/manager/src/features/Linodes/LinodesCreate/VPCPanel.tsx +++ b/packages/manager/src/features/Linodes/LinodesCreate/VPCPanel.tsx @@ -20,7 +20,7 @@ import { useVPCsQuery } from 'src/queries/vpcs'; import { isFeatureEnabled } from 'src/utilities/accountCapabilities'; import { doesRegionSupportFeature } from 'src/utilities/doesRegionSupportFeature'; import { getAPIErrorOrDefault } from 'src/utilities/errorUtils'; -import scrollErrorIntoView from 'src/utilities/scrollErrorIntoView'; +import { scrollErrorIntoView } from 'src/utilities/scrollErrorIntoView'; import { StyledCreateLink } from './LinodeCreate.styles'; import { REGION_CAVEAT_HELPER_TEXT } from './constants'; diff --git a/packages/manager/src/features/Linodes/LinodesDetail/LinodeBackup/ScheduleSettings.tsx b/packages/manager/src/features/Linodes/LinodesDetail/LinodeBackup/ScheduleSettings.tsx index 2dd6ec48c16..46c447e94e1 100644 --- a/packages/manager/src/features/Linodes/LinodesDetail/LinodeBackup/ScheduleSettings.tsx +++ b/packages/manager/src/features/Linodes/LinodesDetail/LinodeBackup/ScheduleSettings.tsx @@ -5,17 +5,17 @@ import * as React from 'react'; import { ActionsPanel } from 'src/components/ActionsPanel/ActionsPanel'; import Select from 'src/components/EnhancedSelect/Select'; +import { FormControl } from 'src/components/FormControl'; +import { FormHelperText } from 'src/components/FormHelperText'; import { Notice } from 'src/components/Notice/Notice'; import { Paper } from 'src/components/Paper'; import { Typography } from 'src/components/Typography'; -import { FormControl } from 'src/components/FormControl'; -import { FormHelperText } from 'src/components/FormHelperText'; import { useLinodeQuery, useLinodeUpdateMutation, } from 'src/queries/linodes/linodes'; import { useProfile } from 'src/queries/profile'; -import getUserTimezone from 'src/utilities/getUserTimezone'; +import { getUserTimezone } from 'src/utilities/getUserTimezone'; import { initWindows } from 'src/utilities/initWindows'; interface Props { diff --git a/packages/manager/src/features/Linodes/LinodesDetail/LinodeConfigs/LinodeConfigDialog.tsx b/packages/manager/src/features/Linodes/LinodesDetail/LinodeConfigs/LinodeConfigDialog.tsx index b39b68bdfad..c45b7275fa9 100644 --- a/packages/manager/src/features/Linodes/LinodesDetail/LinodeConfigs/LinodeConfigDialog.tsx +++ b/packages/manager/src/features/Linodes/LinodesDetail/LinodeConfigs/LinodeConfigDialog.tsx @@ -51,16 +51,17 @@ import { queryKey as vlansQueryKey } from 'src/queries/vlans'; import { useAllVolumesQuery } from 'src/queries/volumes'; import { vpcQueryKey } from 'src/queries/vpcs'; import { isFeatureEnabled } from 'src/utilities/accountCapabilities'; -import createDevicesFromStrings, { +import { DevicesAsStrings, + createDevicesFromStrings, } from 'src/utilities/createDevicesFromStrings'; -import createStringsFromDevices from 'src/utilities/createStringsFromDevices'; +import { createStringsFromDevices } from 'src/utilities/createStringsFromDevices'; import { handleFieldErrors, handleGeneralErrors, } from 'src/utilities/formikErrorUtils'; -import getSelectedOptionFromGroupedOptions from 'src/utilities/getSelectedOptionFromGroupedOptions'; -import scrollErrorIntoView from 'src/utilities/scrollErrorIntoView'; +import { getSelectedOptionFromGroupedOptions } from 'src/utilities/getSelectedOptionFromGroupedOptions'; +import { scrollErrorIntoView } from 'src/utilities/scrollErrorIntoView'; import { ExtendedInterface, diff --git a/packages/manager/src/features/Linodes/LinodesDetail/LinodeRebuild/RebuildFromImage.tsx b/packages/manager/src/features/Linodes/LinodesDetail/LinodeRebuild/RebuildFromImage.tsx index a0efc480e10..a75a2483307 100644 --- a/packages/manager/src/features/Linodes/LinodesDetail/LinodeRebuild/RebuildFromImage.tsx +++ b/packages/manager/src/features/Linodes/LinodesDetail/LinodeRebuild/RebuildFromImage.tsx @@ -28,7 +28,7 @@ import { handleFieldErrors, handleGeneralErrors, } from 'src/utilities/formikErrorUtils'; -import scrollErrorIntoView from 'src/utilities/scrollErrorIntoView'; +import { scrollErrorIntoView } from 'src/utilities/scrollErrorIntoView'; import { extendValidationSchema } from 'src/utilities/validatePassword'; import { diff --git a/packages/manager/src/features/Linodes/LinodesDetail/LinodeRebuild/RebuildFromStackScript.tsx b/packages/manager/src/features/Linodes/LinodesDetail/LinodeRebuild/RebuildFromStackScript.tsx index 0f78905be90..987785f7955 100644 --- a/packages/manager/src/features/Linodes/LinodesDetail/LinodeRebuild/RebuildFromStackScript.tsx +++ b/packages/manager/src/features/Linodes/LinodesDetail/LinodeRebuild/RebuildFromStackScript.tsx @@ -32,7 +32,7 @@ import { handleFieldErrors, handleGeneralErrors, } from 'src/utilities/formikErrorUtils'; -import scrollErrorIntoView from 'src/utilities/scrollErrorIntoView'; +import { scrollErrorIntoView } from 'src/utilities/scrollErrorIntoView'; import { extendValidationSchema } from 'src/utilities/validatePassword'; interface Props { diff --git a/packages/manager/src/features/Linodes/LinodesDetail/LinodeRescue/DeviceSelection.tsx b/packages/manager/src/features/Linodes/LinodesDetail/LinodeRescue/DeviceSelection.tsx index feb79fb58f8..4595ae2ee15 100644 --- a/packages/manager/src/features/Linodes/LinodesDetail/LinodeRescue/DeviceSelection.tsx +++ b/packages/manager/src/features/Linodes/LinodesDetail/LinodeRescue/DeviceSelection.tsx @@ -6,7 +6,7 @@ import * as React from 'react'; import Select, { Item } from 'src/components/EnhancedSelect/Select'; import { FormControl } from 'src/components/FormControl'; import { titlecase } from 'src/features/Linodes/presentation'; -import getSelectedOptionFromGroupedOptions from 'src/utilities/getSelectedOptionFromGroupedOptions'; +import { getSelectedOptionFromGroupedOptions } from 'src/utilities/getSelectedOptionFromGroupedOptions'; export interface ExtendedDisk extends Disk { _id: string; diff --git a/packages/manager/src/features/Linodes/LinodesDetail/LinodeRescue/StandardRescueDialog.tsx b/packages/manager/src/features/Linodes/LinodesDetail/LinodeRescue/StandardRescueDialog.tsx index e104d578426..bb36273ccbf 100644 --- a/packages/manager/src/features/Linodes/LinodesDetail/LinodeRescue/StandardRescueDialog.tsx +++ b/packages/manager/src/features/Linodes/LinodesDetail/LinodeRescue/StandardRescueDialog.tsx @@ -19,8 +19,9 @@ import { } from 'src/queries/linodes/linodes'; import { useGrants, useProfile } from 'src/queries/profile'; import { useAllVolumesQuery } from 'src/queries/volumes'; -import createDevicesFromStrings, { +import { DevicesAsStrings, + createDevicesFromStrings, } from 'src/utilities/createDevicesFromStrings'; import { LinodePermissionsError } from '../LinodePermissionsError'; diff --git a/packages/manager/src/features/Linodes/LinodesDetail/LinodeResize/LinodeResize.tsx b/packages/manager/src/features/Linodes/LinodesDetail/LinodeResize/LinodeResize.tsx index c07605af931..781b6910bb6 100644 --- a/packages/manager/src/features/Linodes/LinodesDetail/LinodeResize/LinodeResize.tsx +++ b/packages/manager/src/features/Linodes/LinodesDetail/LinodeResize/LinodeResize.tsx @@ -32,7 +32,7 @@ import { useRegionsQuery } from 'src/queries/regions'; import { useAllTypes } from 'src/queries/types'; import { extendType } from 'src/utilities/extendType'; import { getPermissionsForLinode } from 'src/utilities/linodes'; -import scrollErrorIntoView from 'src/utilities/scrollErrorIntoView'; +import { scrollErrorIntoView } from 'src/utilities/scrollErrorIntoView'; import { HostMaintenanceError } from '../HostMaintenanceError'; import { LinodePermissionsError } from '../LinodePermissionsError'; diff --git a/packages/manager/src/features/Linodes/LinodesDetail/LinodeSettings/LinodeSettingsAlertsPanel.tsx b/packages/manager/src/features/Linodes/LinodesDetail/LinodeSettings/LinodeSettingsAlertsPanel.tsx index ebb7d3a1a46..ed710c27536 100644 --- a/packages/manager/src/features/Linodes/LinodesDetail/LinodeSettings/LinodeSettingsAlertsPanel.tsx +++ b/packages/manager/src/features/Linodes/LinodesDetail/LinodeSettings/LinodeSettingsAlertsPanel.tsx @@ -12,7 +12,7 @@ import { useLinodeUpdateMutation, } from 'src/queries/linodes/linodes'; import { useTypeQuery } from 'src/queries/types'; -import getAPIErrorFor from 'src/utilities/getAPIErrorFor'; +import { getAPIErrorFor } from 'src/utilities/getAPIErrorFor'; import { AlertSection } from './AlertSection'; diff --git a/packages/manager/src/features/Linodes/LinodesDetail/LinodeSummary/ActivityRow.tsx b/packages/manager/src/features/Linodes/LinodesDetail/LinodeSummary/ActivityRow.tsx index e6e6c6cbb5f..36f68f548a4 100644 --- a/packages/manager/src/features/Linodes/LinodesDetail/LinodeSummary/ActivityRow.tsx +++ b/packages/manager/src/features/Linodes/LinodesDetail/LinodeSummary/ActivityRow.tsx @@ -6,7 +6,7 @@ import * as React from 'react'; import { DateTimeDisplay } from 'src/components/DateTimeDisplay'; import { Typography } from 'src/components/Typography'; import { generateEventMessage } from 'src/features/Events/eventMessageGenerator'; -import { formatEventSeconds } from 'src/utilities/minute-conversion/minute-conversion'; +import { formatEventSeconds } from 'src/utilities/minute-conversion'; interface Props { event: Event; diff --git a/packages/manager/src/features/Linodes/LinodesDetail/LinodesDetailHeader/LinodeDetailHeader.tsx b/packages/manager/src/features/Linodes/LinodesDetail/LinodesDetailHeader/LinodeDetailHeader.tsx index 3e97010720d..2a210986f6e 100644 --- a/packages/manager/src/features/Linodes/LinodesDetail/LinodesDetailHeader/LinodeDetailHeader.tsx +++ b/packages/manager/src/features/Linodes/LinodesDetail/LinodesDetailHeader/LinodeDetailHeader.tsx @@ -21,7 +21,7 @@ import { import { sendLinodeCreateFlowDocsClickEvent } from 'src/utilities/analytics'; import { getAPIErrorOrDefault } from 'src/utilities/errorUtils'; import { getQueryParamsFromQueryString } from 'src/utilities/queryParams'; -import scrollErrorIntoView from 'src/utilities/scrollErrorIntoView'; +import { scrollErrorIntoView } from 'src/utilities/scrollErrorIntoView'; import { DeleteLinodeDialog } from '../../LinodesLanding/DeleteLinodeDialog'; import { EnableBackupsDialog } from '../LinodeBackup/EnableBackupsDialog'; diff --git a/packages/manager/src/features/Linodes/MigrateLinode/MigrateLinode.tsx b/packages/manager/src/features/Linodes/MigrateLinode/MigrateLinode.tsx index c8f453b3fe2..2ef5afcd634 100644 --- a/packages/manager/src/features/Linodes/MigrateLinode/MigrateLinode.tsx +++ b/packages/manager/src/features/Linodes/MigrateLinode/MigrateLinode.tsx @@ -35,7 +35,7 @@ import { formatDate } from 'src/utilities/formatDate'; import { isEURegion } from 'src/utilities/formatRegion'; import { formatStorageUnits } from 'src/utilities/formatStorageUnits'; import { getLinodeDescription } from 'src/utilities/getLinodeDescription'; -import scrollErrorIntoView from 'src/utilities/scrollErrorIntoView'; +import { scrollErrorIntoView } from 'src/utilities/scrollErrorIntoView'; import { addUsedDiskSpace } from '../LinodesDetail/LinodeStorage/LinodeDisks'; import { CautionNotice } from './CautionNotice'; diff --git a/packages/manager/src/features/LoadBalancers/LoadBalancerDetail/ServiceTargets/CreateServiceTargetDrawer.tsx b/packages/manager/src/features/LoadBalancers/LoadBalancerDetail/ServiceTargets/CreateServiceTargetDrawer.tsx index 84e95eb0978..d6081f0551d 100644 --- a/packages/manager/src/features/LoadBalancers/LoadBalancerDetail/ServiceTargets/CreateServiceTargetDrawer.tsx +++ b/packages/manager/src/features/LoadBalancers/LoadBalancerDetail/ServiceTargets/CreateServiceTargetDrawer.tsx @@ -21,7 +21,7 @@ import { Toggle } from 'src/components/Toggle'; import { TooltipIcon } from 'src/components/TooltipIcon'; import { Typography } from 'src/components/Typography'; import { useServiceTargetCreateMutation } from 'src/queries/aglb/serviceTargets'; -import scrollErrorIntoView from 'src/utilities/scrollErrorIntoView'; +import { scrollErrorIntoView } from 'src/utilities/scrollErrorIntoView'; import { CertificateSelect } from '../Certificates/CertificateSelect'; import { AddEndpointForm } from './AddEndpointForm'; diff --git a/packages/manager/src/features/Longview/LongviewDetail/DetailTabs/Apache/ApacheGraphs.tsx b/packages/manager/src/features/Longview/LongviewDetail/DetailTabs/Apache/ApacheGraphs.tsx index d253f7c78f1..00dd43d9064 100644 --- a/packages/manager/src/features/Longview/LongviewDetail/DetailTabs/Apache/ApacheGraphs.tsx +++ b/packages/manager/src/features/Longview/LongviewDetail/DetailTabs/Apache/ApacheGraphs.tsx @@ -1,5 +1,5 @@ -import { useTheme } from '@mui/material/styles'; import Grid from '@mui/material/Unstable_Grid2'; +import { useTheme } from '@mui/material/styles'; import * as React from 'react'; import { LongviewLineGraph } from 'src/components/LongviewLineGraph/LongviewLineGraph'; @@ -9,7 +9,7 @@ import { generateNetworkUnits, statMax, } from 'src/features/Longview/shared/utilities'; -import roundTo from 'src/utilities/roundTo'; +import { roundTo } from 'src/utilities/roundTo'; import { ApacheResponse, LongviewProcesses } from '../../../request.types'; import { convertData } from '../../../shared/formatters'; diff --git a/packages/manager/src/features/Managed/ManagedDashboardCard/ManagedChartPanel.tsx b/packages/manager/src/features/Managed/ManagedDashboardCard/ManagedChartPanel.tsx index 3412d1a8863..c1e7705e3d3 100644 --- a/packages/manager/src/features/Managed/ManagedDashboardCard/ManagedChartPanel.tsx +++ b/packages/manager/src/features/Managed/ManagedDashboardCard/ManagedChartPanel.tsx @@ -16,7 +16,7 @@ import { import { useManagedStatsQuery } from 'src/queries/managed/managed'; import { useProfile } from 'src/queries/profile'; import { getAPIErrorOrDefault } from 'src/utilities/errorUtils'; -import getUserTimezone from 'src/utilities/getUserTimezone'; +import { getUserTimezone } from 'src/utilities/getUserTimezone'; import { StyledCanvasContainerDiv, diff --git a/packages/manager/src/features/Managed/Monitors/IssueCalendar.tsx b/packages/manager/src/features/Managed/Monitors/IssueCalendar.tsx index 3dbacbf1d41..03fb145d657 100644 --- a/packages/manager/src/features/Managed/Monitors/IssueCalendar.tsx +++ b/packages/manager/src/features/Managed/Monitors/IssueCalendar.tsx @@ -4,7 +4,7 @@ import * as React from 'react'; import { useProfile } from 'src/queries/profile'; import { parseAPIDate } from 'src/utilities/date'; -import getUserTimezone from 'src/utilities/getUserTimezone'; +import { getUserTimezone } from 'src/utilities/getUserTimezone'; import IssueDay from './IssueDay'; diff --git a/packages/manager/src/features/NodeBalancers/NodeBalancerCreate.tsx b/packages/manager/src/features/NodeBalancers/NodeBalancerCreate.tsx index c4f2d3d219d..3eef3406099 100644 --- a/packages/manager/src/features/NodeBalancers/NodeBalancerCreate.tsx +++ b/packages/manager/src/features/NodeBalancers/NodeBalancerCreate.tsx @@ -38,10 +38,10 @@ import { useRegionsQuery } from 'src/queries/regions'; import { sendCreateNodeBalancerEvent } from 'src/utilities/analytics'; import { getAPIErrorOrDefault } from 'src/utilities/errorUtils'; import { isEURegion } from 'src/utilities/formatRegion'; -import getAPIErrorFor from 'src/utilities/getAPIErrorFor'; +import { getAPIErrorFor } from 'src/utilities/getAPIErrorFor'; import { NODEBALANCER_PRICE } from 'src/utilities/pricing/constants'; import { getDCSpecificPrice } from 'src/utilities/pricing/dynamicPricing'; -import scrollErrorIntoView from 'src/utilities/scrollErrorIntoView'; +import { scrollErrorIntoView } from 'src/utilities/scrollErrorIntoView'; import EUAgreementCheckbox from '../Account/Agreements/EUAgreementCheckbox'; import { NodeBalancerConfigPanel } from './NodeBalancerConfigPanel'; diff --git a/packages/manager/src/features/NodeBalancers/NodeBalancerDetail/NodeBalancerConfigurations.tsx b/packages/manager/src/features/NodeBalancers/NodeBalancerDetail/NodeBalancerConfigurations.tsx index 15b472d2606..e10b7c1dcec 100644 --- a/packages/manager/src/features/NodeBalancers/NodeBalancerDetail/NodeBalancerConfigurations.tsx +++ b/packages/manager/src/features/NodeBalancers/NodeBalancerDetail/NodeBalancerConfigurations.tsx @@ -44,7 +44,7 @@ import { } from 'src/containers/withQueryClient.container'; import { queryKey } from 'src/queries/nodebalancers'; import { getAPIErrorOrDefault } from 'src/utilities/errorUtils'; -import scrollErrorIntoView from 'src/utilities/scrollErrorIntoView'; +import { scrollErrorIntoView } from 'src/utilities/scrollErrorIntoView'; import { NodeBalancerConfigPanel } from '../NodeBalancerConfigPanel'; import { lensFrom } from '../NodeBalancerCreate'; diff --git a/packages/manager/src/features/NodeBalancers/NodeBalancerDetail/NodeBalancerSummary/TablesPanel.tsx b/packages/manager/src/features/NodeBalancers/NodeBalancerDetail/NodeBalancerSummary/TablesPanel.tsx index fbf57139dea..dc0a7959987 100644 --- a/packages/manager/src/features/NodeBalancers/NodeBalancerDetail/NodeBalancerSummary/TablesPanel.tsx +++ b/packages/manager/src/features/NodeBalancers/NodeBalancerDetail/NodeBalancerSummary/TablesPanel.tsx @@ -18,7 +18,7 @@ import { } from 'src/queries/nodebalancers'; import { useProfile } from 'src/queries/profile'; import { getAPIErrorOrDefault } from 'src/utilities/errorUtils'; -import getUserTimezone from 'src/utilities/getUserTimezone'; +import { getUserTimezone } from 'src/utilities/getUserTimezone'; import { formatNumber, getMetrics } from 'src/utilities/statMetrics'; const STATS_NOT_READY_TITLE = diff --git a/packages/manager/src/features/NotificationCenter/NotificationData/RenderNotification.tsx b/packages/manager/src/features/NotificationCenter/NotificationData/RenderNotification.tsx index a8407635f82..8a9a87ac850 100644 --- a/packages/manager/src/features/NotificationCenter/NotificationData/RenderNotification.tsx +++ b/packages/manager/src/features/NotificationCenter/NotificationData/RenderNotification.tsx @@ -1,14 +1,14 @@ import { NotificationType } from '@linode/api-v4/lib/account'; import ErrorIcon from '@mui/icons-material/Error'; import WarningIcon from '@mui/icons-material/Warning'; -import { Box } from 'src/components/Box'; import { styled, useTheme } from '@mui/material/styles'; import * as React from 'react'; +import { Box } from 'src/components/Box'; import { Divider } from 'src/components/Divider'; import { Link } from 'src/components/Link'; import { Typography } from 'src/components/Typography'; -import { sanitizeHTML } from 'src/utilities/sanitize-html'; +import { sanitizeHTML } from 'src/utilities/sanitizeHTML'; import { ExtendedNotification } from './useFormattedNotifications'; diff --git a/packages/manager/src/features/NotificationCenter/NotificationData/useEventInfo.ts b/packages/manager/src/features/NotificationCenter/NotificationData/useEventInfo.ts index 66ac4f0a48a..c77fe31b3a6 100644 --- a/packages/manager/src/features/NotificationCenter/NotificationData/useEventInfo.ts +++ b/packages/manager/src/features/NotificationCenter/NotificationData/useEventInfo.ts @@ -1,7 +1,7 @@ import { Event } from '@linode/api-v4/lib/account/types'; import { generateEventMessage } from 'src/features/Events/eventMessageGenerator'; -import { formatEventSeconds } from 'src/utilities/minute-conversion/minute-conversion'; +import { formatEventSeconds } from 'src/utilities/minute-conversion'; import type { EntityVariants } from 'src/components/EntityIcon/EntityIcon'; diff --git a/packages/manager/src/features/OneClickApps/AppDetailDrawer.tsx b/packages/manager/src/features/OneClickApps/AppDetailDrawer.tsx index c3b0642edb3..5acd3fd5e8f 100644 --- a/packages/manager/src/features/OneClickApps/AppDetailDrawer.tsx +++ b/packages/manager/src/features/OneClickApps/AppDetailDrawer.tsx @@ -9,12 +9,12 @@ import { Button } from 'src/components/Button/Button'; import { Link } from 'src/components/Link'; import { Typography } from 'src/components/Typography'; import { useFlags } from 'src/hooks/useFlags'; -import { sanitizeHTML } from 'src/utilities/sanitize-html'; import { oneClickApps } from './oneClickApps'; import { mapStackScriptLabelToOCA } from './utils'; import type { OCA } from './types'; +import { sanitizeHTML } from 'src/utilities/sanitizeHTML'; const useStyles = makeStyles((theme: Theme) => ({ appName: { diff --git a/packages/manager/src/features/Profile/AuthenticationSettings/TwoFactor/EnableTwoFactorForm.tsx b/packages/manager/src/features/Profile/AuthenticationSettings/TwoFactor/EnableTwoFactorForm.tsx index ff393af7960..118739bf97c 100644 --- a/packages/manager/src/features/Profile/AuthenticationSettings/TwoFactor/EnableTwoFactorForm.tsx +++ b/packages/manager/src/features/Profile/AuthenticationSettings/TwoFactor/EnableTwoFactorForm.tsx @@ -6,7 +6,7 @@ import { CircleProgress } from 'src/components/CircleProgress'; import { Divider } from 'src/components/Divider'; import { Notice } from 'src/components/Notice/Notice'; import { getAPIErrorOrDefault, getErrorMap } from 'src/utilities/errorUtils'; -import scrollErrorIntoView from 'src/utilities/scrollErrorIntoView'; +import { scrollErrorIntoView } from 'src/utilities/scrollErrorIntoView'; import { ConfirmToken } from './ConfirmToken'; import { QRCodeForm } from './QRCodeForm'; diff --git a/packages/manager/src/features/Profile/AuthenticationSettings/TwoFactor/TwoFactor.tsx b/packages/manager/src/features/Profile/AuthenticationSettings/TwoFactor/TwoFactor.tsx index 6f75117080a..0d1b89a8c7b 100644 --- a/packages/manager/src/features/Profile/AuthenticationSettings/TwoFactor/TwoFactor.tsx +++ b/packages/manager/src/features/Profile/AuthenticationSettings/TwoFactor/TwoFactor.tsx @@ -9,7 +9,7 @@ import { Typography } from 'src/components/Typography'; import { queryKey } from 'src/queries/profile'; import { useSecurityQuestions } from 'src/queries/securityQuestions'; import { getAPIErrorOrDefault } from 'src/utilities/errorUtils'; -import getAPIErrorFor from 'src/utilities/getAPIErrorFor'; +import { getAPIErrorFor } from 'src/utilities/getAPIErrorFor'; import { DisableTwoFactorDialog } from './DisableTwoFactorDialog'; import { EnableTwoFactorForm } from './EnableTwoFactorForm'; diff --git a/packages/manager/src/features/Profile/LishSettings/LishSettings.tsx b/packages/manager/src/features/Profile/LishSettings/LishSettings.tsx index b0d35270354..56cafc8d409 100644 --- a/packages/manager/src/features/Profile/LishSettings/LishSettings.tsx +++ b/packages/manager/src/features/Profile/LishSettings/LishSettings.tsx @@ -16,8 +16,8 @@ import { Typography } from 'src/components/Typography'; import { FormControl } from 'src/components/FormControl'; import { useMutateProfile, useProfile } from 'src/queries/profile'; import { getAPIErrorOrDefault } from 'src/utilities/errorUtils'; -import getAPIErrorFor from 'src/utilities/getAPIErrorFor'; -import scrollErrorIntoView from 'src/utilities/scrollErrorIntoView'; +import { getAPIErrorFor } from 'src/utilities/getAPIErrorFor'; +import { scrollErrorIntoView } from 'src/utilities/scrollErrorIntoView'; export const LishSettings = () => { const theme = useTheme(); diff --git a/packages/manager/src/features/Profile/OAuthClients/CreateOAuthClientDrawer.tsx b/packages/manager/src/features/Profile/OAuthClients/CreateOAuthClientDrawer.tsx index 86a41837cca..7fdf9e3b98a 100644 --- a/packages/manager/src/features/Profile/OAuthClients/CreateOAuthClientDrawer.tsx +++ b/packages/manager/src/features/Profile/OAuthClients/CreateOAuthClientDrawer.tsx @@ -10,7 +10,7 @@ import { FormControlLabel } from 'src/components/FormControlLabel'; import { Notice } from 'src/components/Notice/Notice'; import { TextField } from 'src/components/TextField'; import { useCreateOAuthClientMutation } from 'src/queries/accountOAuth'; -import getAPIErrorsFor from 'src/utilities/getAPIErrorFor'; +import { getAPIErrorFor } from 'src/utilities/getAPIErrorFor'; interface Props { onClose: () => void; @@ -49,7 +49,7 @@ export const CreateOAuthClientDrawer = ({ redirect_uri: 'A callback URL', }; - const hasErrorFor = getAPIErrorsFor(errorResources, error ?? undefined); + const hasErrorFor = getAPIErrorFor(errorResources, error ?? undefined); return ( diff --git a/packages/manager/src/features/Profile/OAuthClients/EditOAuthClientDrawer.tsx b/packages/manager/src/features/Profile/OAuthClients/EditOAuthClientDrawer.tsx index 3bc57b84b4e..e361fa3754b 100644 --- a/packages/manager/src/features/Profile/OAuthClients/EditOAuthClientDrawer.tsx +++ b/packages/manager/src/features/Profile/OAuthClients/EditOAuthClientDrawer.tsx @@ -10,7 +10,7 @@ import { TextField } from 'src/components/TextField'; import { FormControl } from 'src/components/FormControl'; import { FormControlLabel } from 'src/components/FormControlLabel'; import { useUpdateOAuthClientMutation } from 'src/queries/accountOAuth'; -import getAPIErrorsFor from 'src/utilities/getAPIErrorFor'; +import { getAPIErrorFor } from 'src/utilities/getAPIErrorFor'; interface Props { client: OAuthClient | undefined; @@ -47,7 +47,7 @@ export const EditOAuthClientDrawer = ({ client, onClose, open }: Props) => { redirect_uri: 'A callback URL', }; - const hasErrorFor = getAPIErrorsFor(errorResources, error ?? undefined); + const hasErrorFor = getAPIErrorFor(errorResources, error ?? undefined); return ( diff --git a/packages/manager/src/features/Profile/SSHKeys/CreateSSHKeyDrawer.tsx b/packages/manager/src/features/Profile/SSHKeys/CreateSSHKeyDrawer.tsx index 23d1589f446..f4332f11978 100644 --- a/packages/manager/src/features/Profile/SSHKeys/CreateSSHKeyDrawer.tsx +++ b/packages/manager/src/features/Profile/SSHKeys/CreateSSHKeyDrawer.tsx @@ -11,7 +11,7 @@ import { TextField } from 'src/components/TextField'; import { Typography } from 'src/components/Typography'; import { useCreateSSHKeyMutation } from 'src/queries/profile'; import { handleFormikBlur } from 'src/utilities/formikTrimUtil'; -import getAPIErrorFor from 'src/utilities/getAPIErrorFor'; +import { getAPIErrorFor } from 'src/utilities/getAPIErrorFor'; interface Props { onClose: () => void; diff --git a/packages/manager/src/features/Profile/SSHKeys/EditSSHKeyDrawer.tsx b/packages/manager/src/features/Profile/SSHKeys/EditSSHKeyDrawer.tsx index 3111f6e3129..0861f2e38da 100644 --- a/packages/manager/src/features/Profile/SSHKeys/EditSSHKeyDrawer.tsx +++ b/packages/manager/src/features/Profile/SSHKeys/EditSSHKeyDrawer.tsx @@ -9,7 +9,7 @@ import { Drawer } from 'src/components/Drawer'; import { Notice } from 'src/components/Notice/Notice'; import { TextField } from 'src/components/TextField'; import { useUpdateSSHKeyMutation } from 'src/queries/profile'; -import getAPIErrorFor from 'src/utilities/getAPIErrorFor'; +import { getAPIErrorFor } from 'src/utilities/getAPIErrorFor'; interface Props { onClose: () => void; diff --git a/packages/manager/src/features/Profile/Settings/Settings.tsx b/packages/manager/src/features/Profile/Settings/Settings.tsx index 91281aef7ba..0239c0d4a14 100644 --- a/packages/manager/src/features/Profile/Settings/Settings.tsx +++ b/packages/manager/src/features/Profile/Settings/Settings.tsx @@ -2,7 +2,6 @@ import Stack from '@mui/material/Stack'; import * as React from 'react'; import { useHistory, useLocation } from 'react-router-dom'; -import { isOSMac } from 'src/App'; import { Code } from 'src/components/Code/Code'; import { DocumentTitleSegment } from 'src/components/DocumentTitle'; import { FormControlLabel } from 'src/components/FormControlLabel'; @@ -15,6 +14,7 @@ import { useMutatePreferences, usePreferences } from 'src/queries/preferences'; import { useMutateProfile, useProfile } from 'src/queries/profile'; import { getQueryParamFromQueryString } from 'src/utilities/queryParams'; import { ThemeChoice } from 'src/utilities/theme'; +import { isOSMac } from 'src/utilities/userAgent'; import { PreferenceEditor } from './PreferenceEditor'; diff --git a/packages/manager/src/features/StackScripts/StackScriptCreate/StackScriptCreate.tsx b/packages/manager/src/features/StackScripts/StackScriptCreate/StackScriptCreate.tsx index 9fc67a98ac9..fe935a1c914 100644 --- a/packages/manager/src/features/StackScripts/StackScriptCreate/StackScriptCreate.tsx +++ b/packages/manager/src/features/StackScripts/StackScriptCreate/StackScriptCreate.tsx @@ -37,8 +37,8 @@ import { import { StackScriptForm } from 'src/features/StackScripts/StackScriptForm/StackScriptForm'; import { queryKey } from 'src/queries/profile'; import { filterImagesByType } from 'src/store/image/image.helpers'; -import getAPIErrorsFor from 'src/utilities/getAPIErrorFor'; -import scrollErrorIntoView from 'src/utilities/scrollErrorIntoView'; +import { getAPIErrorFor } from 'src/utilities/getAPIErrorFor'; +import { scrollErrorIntoView } from 'src/utilities/scrollErrorIntoView'; import { storage } from 'src/utilities/storage'; interface State { @@ -166,7 +166,7 @@ export class StackScriptCreate extends React.Component { const _imagesData = filterImagesByType(imagesData, 'public'); - const hasErrorFor = getAPIErrorsFor(errorResources, errors); + const hasErrorFor = getAPIErrorFor(errorResources, errors); const generalError = hasErrorFor('none'); const hasUnsavedChanges = this.hasUnsavedChanges(); diff --git a/packages/manager/src/features/StackScripts/StackScriptForm/StackScriptForm.tsx b/packages/manager/src/features/StackScripts/StackScriptForm/StackScriptForm.tsx index 5877b07894f..5ec1eec44e7 100644 --- a/packages/manager/src/features/StackScripts/StackScriptForm/StackScriptForm.tsx +++ b/packages/manager/src/features/StackScripts/StackScriptForm/StackScriptForm.tsx @@ -9,7 +9,7 @@ import { Paper } from 'src/components/Paper'; import { TextField } from 'src/components/TextField'; import { Typography } from 'src/components/Typography'; import ImageSelect from 'src/features/Images/ImageSelect'; -import getAPIErrorsFor from 'src/utilities/getAPIErrorFor'; +import { getAPIErrorFor } from 'src/utilities/getAPIErrorFor'; import { imageToItem } from 'src/utilities/imageToItem'; import { @@ -72,7 +72,7 @@ export const StackScriptForm = React.memo((props: Props) => { script, } = props; - const hasErrorFor = getAPIErrorsFor(errorResources, errors); + const hasErrorFor = getAPIErrorFor(errorResources, errors); const selectedImages = imageToItem(images.selected); return ( diff --git a/packages/manager/src/features/StackScripts/StackScriptPanel/StackScriptsSection.tsx b/packages/manager/src/features/StackScripts/StackScriptPanel/StackScriptsSection.tsx index 390f3ad2f2f..53006820b7b 100644 --- a/packages/manager/src/features/StackScripts/StackScriptPanel/StackScriptsSection.tsx +++ b/packages/manager/src/features/StackScripts/StackScriptPanel/StackScriptsSection.tsx @@ -11,7 +11,7 @@ import { } from 'src/features/StackScripts/stackScriptUtils'; import { useGrants, useProfile } from 'src/queries/profile'; import { formatDate } from 'src/utilities/formatDate'; -import stripImageName from 'src/utilities/stripImageName'; +import { stripImageName } from 'src/utilities/stripImageName'; import { StyledStackScriptSectionTableCell } from '../CommonStackScript.styles'; import { StackScriptRow } from './StackScriptRow'; diff --git a/packages/manager/src/features/Support/SupportTickets/SupportTicketDialog.tsx b/packages/manager/src/features/Support/SupportTickets/SupportTicketDialog.tsx index 1df037cc42d..1cf1b043723 100644 --- a/packages/manager/src/features/Support/SupportTickets/SupportTicketDialog.tsx +++ b/packages/manager/src/features/Support/SupportTickets/SupportTicketDialog.tsx @@ -33,7 +33,7 @@ import { getErrorStringOrDefault, } from 'src/utilities/errorUtils'; import { reduceAsync } from 'src/utilities/reduceAsync'; -import scrollErrorIntoView from 'src/utilities/scrollErrorIntoView'; +import { scrollErrorIntoView } from 'src/utilities/scrollErrorIntoView'; import { storage } from 'src/utilities/storage'; import AttachFileForm from '../AttachFileForm'; diff --git a/packages/manager/src/features/Users/CreateUserDrawer.tsx b/packages/manager/src/features/Users/CreateUserDrawer.tsx index 8fc18e2abdc..1ca515133be 100644 --- a/packages/manager/src/features/Users/CreateUserDrawer.tsx +++ b/packages/manager/src/features/Users/CreateUserDrawer.tsx @@ -10,7 +10,7 @@ import { Notice } from 'src/components/Notice/Notice'; import { TextField } from 'src/components/TextField'; import { Toggle } from 'src/components/Toggle'; import { getAPIErrorOrDefault } from 'src/utilities/errorUtils'; -import getAPIErrorsFor from 'src/utilities/getAPIErrorFor'; +import { getAPIErrorFor } from 'src/utilities/getAPIErrorFor'; interface Props { onClose: () => void; @@ -45,7 +45,7 @@ class CreateUserDrawer extends React.Component { const { onClose, open } = this.props; const { email, errors, restricted, submitting, username } = this.state; - const hasErrorFor = getAPIErrorsFor( + const hasErrorFor = getAPIErrorFor( { email: 'Email', username: 'Username' }, errors ); diff --git a/packages/manager/src/features/Users/UserPermissions.tsx b/packages/manager/src/features/Users/UserPermissions.tsx index 5a80fa197b0..3ebf5fa84c6 100644 --- a/packages/manager/src/features/Users/UserPermissions.tsx +++ b/packages/manager/src/features/Users/UserPermissions.tsx @@ -38,8 +38,8 @@ import { withQueryClient, } from 'src/containers/withQueryClient.container'; import { getAPIErrorOrDefault } from 'src/utilities/errorUtils'; -import getAPIErrorsFor from 'src/utilities/getAPIErrorFor'; -import scrollErrorIntoView from 'src/utilities/scrollErrorIntoView'; +import { getAPIErrorFor } from 'src/utilities/getAPIErrorFor'; +import { scrollErrorIntoView } from 'src/utilities/scrollErrorIntoView'; import { StyledDivWrapper, StyledSelect } from './UserPermissions.styles'; import { @@ -372,7 +372,7 @@ class UserPermissions extends React.Component { renderBody = () => { const { currentUser, username } = this.props; const { errors, restricted } = this.state; - const hasErrorFor = getAPIErrorsFor({ restricted: 'Restricted' }, errors); + const hasErrorFor = getAPIErrorFor({ restricted: 'Restricted' }, errors); const generalError = hasErrorFor('none'); return ( diff --git a/packages/manager/src/features/Users/UserProfile.tsx b/packages/manager/src/features/Users/UserProfile.tsx index 31179e0388c..978029da008 100644 --- a/packages/manager/src/features/Users/UserProfile.tsx +++ b/packages/manager/src/features/Users/UserProfile.tsx @@ -14,8 +14,8 @@ import { TextField } from 'src/components/TextField'; import { TooltipIcon } from 'src/components/TooltipIcon'; import { Typography } from 'src/components/Typography'; import { useProfile } from 'src/queries/profile'; -import getAPIErrorsFor from 'src/utilities/getAPIErrorFor'; -import scrollErrorIntoView from 'src/utilities/scrollErrorIntoView'; +import { getAPIErrorFor } from 'src/utilities/getAPIErrorFor'; +import { scrollErrorIntoView } from 'src/utilities/scrollErrorIntoView'; import { UserDeleteConfirmationDialog } from './UserDeleteConfirmationDialog'; import { StyledTitle, StyledWrapper } from './UserProfile.styles'; @@ -71,12 +71,12 @@ export const UserProfile = (props: UserProfileProps) => { const [userDeleteError, setUserDeleteError] = React.useState(false); const renderProfileSection = () => { - const hasAccountErrorFor = getAPIErrorsFor( + const hasAccountErrorFor = getAPIErrorFor( { username: 'Username' }, accountErrors ); - const hasProfileErrorFor = getAPIErrorsFor( + const hasProfileErrorFor = getAPIErrorFor( { email: 'Email' }, profileErrors ); diff --git a/packages/manager/src/features/Users/UsersLanding.tsx b/packages/manager/src/features/Users/UsersLanding.tsx index c32c53a0811..4c149246fed 100644 --- a/packages/manager/src/features/Users/UsersLanding.tsx +++ b/packages/manager/src/features/Users/UsersLanding.tsx @@ -22,7 +22,7 @@ import { TableRowLoading } from 'src/components/TableRowLoading/TableRowLoading' import { usePagination } from 'src/hooks/usePagination'; import { useAccountUsers } from 'src/queries/accountUsers'; import { useProfile } from 'src/queries/profile'; -import scrollErrorIntoView from 'src/utilities/scrollErrorIntoView'; +import { scrollErrorIntoView } from 'src/utilities/scrollErrorIntoView'; import { GravatarByEmail } from '../../components/GravatarByEmail'; import CreateUserDrawer from './CreateUserDrawer'; diff --git a/packages/manager/src/features/VPCs/VPCCreate/VPCCreate.tsx b/packages/manager/src/features/VPCs/VPCCreate/VPCCreate.tsx index f2049e00b39..cf7c0a33bbe 100644 --- a/packages/manager/src/features/VPCs/VPCCreate/VPCCreate.tsx +++ b/packages/manager/src/features/VPCs/VPCCreate/VPCCreate.tsx @@ -25,7 +25,7 @@ import { SubnetError, handleVPCAndSubnetErrors, } from 'src/utilities/formikErrorUtils'; -import scrollErrorIntoView from 'src/utilities/scrollErrorIntoView'; +import { scrollErrorIntoView } from 'src/utilities/scrollErrorIntoView'; import { DEFAULT_SUBNET_IPV4_VALUE, SubnetFieldState, diff --git a/packages/manager/src/features/Volumes/AttachVolumeDrawer.tsx b/packages/manager/src/features/Volumes/AttachVolumeDrawer.tsx index b081420b528..8957a0d29c8 100644 --- a/packages/manager/src/features/Volumes/AttachVolumeDrawer.tsx +++ b/packages/manager/src/features/Volumes/AttachVolumeDrawer.tsx @@ -15,7 +15,7 @@ import { LinodeSelect } from 'src/features/Linodes/LinodeSelect/LinodeSelect'; import { useAllLinodeConfigsQuery } from 'src/queries/linodes/configs'; import { useGrants } from 'src/queries/profile'; import { useAttachVolumeMutation } from 'src/queries/volumes'; -import getAPIErrorsFor from 'src/utilities/getAPIErrorFor'; +import { getAPIErrorFor } from 'src/utilities/getAPIErrorFor'; interface Props { onClose: () => void; @@ -96,7 +96,7 @@ export const AttachVolumeDrawer = React.memo((props: Props) => { grants.volume.find((grant) => grant.id === volume?.id)?.permissions === 'read_only'; - const hasErrorFor = getAPIErrorsFor( + const hasErrorFor = getAPIErrorFor( errorResources, error === null ? undefined : error ); diff --git a/packages/manager/src/features/components/PlansPanel/utils.ts b/packages/manager/src/features/components/PlansPanel/utils.ts index cb116da7b28..cebd25a8d87 100644 --- a/packages/manager/src/features/components/PlansPanel/utils.ts +++ b/packages/manager/src/features/components/PlansPanel/utils.ts @@ -1,7 +1,7 @@ import { Region } from '@linode/api-v4/lib/regions'; import { Capabilities } from '@linode/api-v4/lib/regions/types'; -import arrayToList from 'src/utilities/arrayToDelimiterSeparatedList'; +import { arrayToList } from 'src/utilities/arrayToList'; import { ExtendedType } from 'src/utilities/extendType'; import { PlanSelectionType } from './types'; diff --git a/packages/manager/src/hooks/useErrors.ts b/packages/manager/src/hooks/useErrors.ts index f95283b57fd..45170d3649b 100644 --- a/packages/manager/src/hooks/useErrors.ts +++ b/packages/manager/src/hooks/useErrors.ts @@ -1,7 +1,7 @@ import { APIError } from '@linode/api-v4/lib/types'; import * as React from 'react'; -import scrollErrorIntoView from 'src/utilities/scrollErrorIntoView'; +import { scrollErrorIntoView } from 'src/utilities/scrollErrorIntoView'; export const useErrors = (): [ APIError[], diff --git a/packages/manager/src/store/events/event.helpers.ts b/packages/manager/src/store/events/event.helpers.ts index a130d227fc8..ac2776f5796 100644 --- a/packages/manager/src/store/events/event.helpers.ts +++ b/packages/manager/src/store/events/event.helpers.ts @@ -7,7 +7,7 @@ import { import { compose, equals, findIndex, omit, take, update } from 'ramda'; import { parseAPIDate } from 'src/utilities/date'; -import updateRight from 'src/utilities/updateRight'; +import { updateRight } from 'src/utilities/updateRight'; import { EntityEvent, ExtendedEvent } from './event.types'; diff --git a/packages/manager/src/utilities/__mocks__/getUserTimezone.ts b/packages/manager/src/utilities/__mocks__/getUserTimezone.ts index d861fa6e712..43383278f88 100644 --- a/packages/manager/src/utilities/__mocks__/getUserTimezone.ts +++ b/packages/manager/src/utilities/__mocks__/getUserTimezone.ts @@ -1,5 +1,3 @@ -const getUserTimezone = () => { +export const getUserTimezone = () => { return 'utc'; }; - -export default getUserTimezone; diff --git a/packages/manager/src/utilities/arrayToDelimiterSeparatedList/index.ts b/packages/manager/src/utilities/arrayToDelimiterSeparatedList/index.ts deleted file mode 100644 index 76feee8ac54..00000000000 --- a/packages/manager/src/utilities/arrayToDelimiterSeparatedList/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { default } from './arrayToList'; diff --git a/packages/manager/src/utilities/arrayToDelimiterSeparatedList/arrayToList.test.ts b/packages/manager/src/utilities/arrayToList.test.ts similarity index 95% rename from packages/manager/src/utilities/arrayToDelimiterSeparatedList/arrayToList.test.ts rename to packages/manager/src/utilities/arrayToList.test.ts index 50c6c83720a..b886f7d077f 100644 --- a/packages/manager/src/utilities/arrayToDelimiterSeparatedList/arrayToList.test.ts +++ b/packages/manager/src/utilities/arrayToList.test.ts @@ -1,4 +1,4 @@ -import arrayToList from './arrayToList'; +import { arrayToList } from './arrayToList'; describe('Array to delimiter-separated list', () => { it('should return a single item as an unaltered string', () => { diff --git a/packages/manager/src/utilities/arrayToDelimiterSeparatedList/arrayToList.ts b/packages/manager/src/utilities/arrayToList.ts similarity index 80% rename from packages/manager/src/utilities/arrayToDelimiterSeparatedList/arrayToList.ts rename to packages/manager/src/utilities/arrayToList.ts index efe6baa06e5..a8334ca171d 100644 --- a/packages/manager/src/utilities/arrayToDelimiterSeparatedList/arrayToList.ts +++ b/packages/manager/src/utilities/arrayToList.ts @@ -1,4 +1,7 @@ -export default (input: string[], separator: string = ','): string => { +export const arrayToList = ( + input: string[], + separator: string = ',' +): string => { if (!Array.isArray(input) || input.length === 0) { return ''; } diff --git a/packages/manager/src/utilities/composeState.ts b/packages/manager/src/utilities/composeState.ts deleted file mode 100644 index c4670b9b3c6..00000000000 --- a/packages/manager/src/utilities/composeState.ts +++ /dev/null @@ -1,14 +0,0 @@ -type Callback = () => void; -type MapState = (prevState: Readonly) => S | null; -type setState = (fn: MapState, callback?: Callback) => void; - -export default function composeState( - this: { setState: setState }, - fns: MapState[], - callback: Callback = () => null -) { - this.setState( - (state) => fns.reverse().reduce((result, fn) => fn(result), state), - () => callback() - ); -} diff --git a/packages/manager/src/utilities/createDevicesFromStrings/createDevicesFromStrings.test.ts b/packages/manager/src/utilities/createDevicesFromStrings.test.ts similarity index 96% rename from packages/manager/src/utilities/createDevicesFromStrings/createDevicesFromStrings.test.ts rename to packages/manager/src/utilities/createDevicesFromStrings.test.ts index e3be5a28cf1..b212c0a3782 100644 --- a/packages/manager/src/utilities/createDevicesFromStrings/createDevicesFromStrings.test.ts +++ b/packages/manager/src/utilities/createDevicesFromStrings.test.ts @@ -1,4 +1,4 @@ -import createDevicesFromStrings from './createDevicesFromStrings'; +import { createDevicesFromStrings } from './createDevicesFromStrings'; describe('LinodeRescue', () => { describe('createRescueDevicesPostObject', () => { diff --git a/packages/manager/src/utilities/createDevicesFromStrings/createDevicesFromStrings.ts b/packages/manager/src/utilities/createDevicesFromStrings.ts similarity index 91% rename from packages/manager/src/utilities/createDevicesFromStrings/createDevicesFromStrings.ts rename to packages/manager/src/utilities/createDevicesFromStrings.ts index b4e8be5805d..e3a6fff9dbf 100644 --- a/packages/manager/src/utilities/createDevicesFromStrings/createDevicesFromStrings.ts +++ b/packages/manager/src/utilities/createDevicesFromStrings.ts @@ -36,7 +36,9 @@ const createTypeRecord = (value?: string): DiskRecord | VolumeRecord | null => { return { [key]: idAsNumber } as DiskRecord | VolumeRecord; // -> { volume_id: 123 } }; -const createDevicesFromStrings = (devices: DevicesAsStrings): Devices => ({ +export const createDevicesFromStrings = ( + devices: DevicesAsStrings +): Devices => ({ sda: createTypeRecord(devices.sda), sdb: createTypeRecord(devices.sdb), sdc: createTypeRecord(devices.sdc), @@ -46,5 +48,3 @@ const createDevicesFromStrings = (devices: DevicesAsStrings): Devices => ({ sdg: createTypeRecord(devices.sdg), sdh: createTypeRecord(devices.sdh), }); - -export default createDevicesFromStrings; diff --git a/packages/manager/src/utilities/createDevicesFromStrings/index.ts b/packages/manager/src/utilities/createDevicesFromStrings/index.ts deleted file mode 100644 index cfdf928ba95..00000000000 --- a/packages/manager/src/utilities/createDevicesFromStrings/index.ts +++ /dev/null @@ -1,6 +0,0 @@ -import DevicesAsStringsDefault, { - DevicesAsStrings as _DevicesAsStrings, -} from './createDevicesFromStrings'; -/* tslint:disable */ -export type DevicesAsStrings = _DevicesAsStrings; -export default DevicesAsStringsDefault; diff --git a/packages/manager/src/utilities/createStringsFromDevices/createStringsFromDevices.test.ts b/packages/manager/src/utilities/createStringsFromDevices.test.ts similarity index 94% rename from packages/manager/src/utilities/createStringsFromDevices/createStringsFromDevices.test.ts rename to packages/manager/src/utilities/createStringsFromDevices.test.ts index 76360a38859..b255f7f92c8 100644 --- a/packages/manager/src/utilities/createStringsFromDevices/createStringsFromDevices.test.ts +++ b/packages/manager/src/utilities/createStringsFromDevices.test.ts @@ -1,4 +1,4 @@ -import createStringsFromDevices from './createStringsFromDevices'; +import { createStringsFromDevices } from './createStringsFromDevices'; describe('LinodeRescue', () => { describe('createRescueDevicesPostObject', () => { diff --git a/packages/manager/src/utilities/createStringsFromDevices/createStringsFromDevices.ts b/packages/manager/src/utilities/createStringsFromDevices.ts similarity index 91% rename from packages/manager/src/utilities/createStringsFromDevices/createStringsFromDevices.ts rename to packages/manager/src/utilities/createStringsFromDevices.ts index 616e2db1d07..4fe6ffeee5a 100644 --- a/packages/manager/src/utilities/createStringsFromDevices/createStringsFromDevices.ts +++ b/packages/manager/src/utilities/createStringsFromDevices.ts @@ -31,4 +31,4 @@ const isVolume = ( return typeof (device as VolumeDevice).volume_id === 'number'; }; -export default compose(reduce(rdx, {}), toPairs); +export const createStringsFromDevices = compose(reduce(rdx, {}), toPairs); diff --git a/packages/manager/src/utilities/createStringsFromDevices/index.ts b/packages/manager/src/utilities/createStringsFromDevices/index.ts deleted file mode 100644 index 2f16e12e690..00000000000 --- a/packages/manager/src/utilities/createStringsFromDevices/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -import createStringsFromDevices from './createStringsFromDevices'; -export default createStringsFromDevices; diff --git a/packages/manager/src/utilities/formatDate.ts b/packages/manager/src/utilities/formatDate.ts index 846d8064504..bd07fccd418 100644 --- a/packages/manager/src/utilities/formatDate.ts +++ b/packages/manager/src/utilities/formatDate.ts @@ -4,7 +4,7 @@ import { DATETIME_DISPLAY_FORMAT, ISO_DATE_FORMAT } from 'src/constants'; import { reportException } from 'src/exceptionReporting'; import { parseAPIDate } from 'src/utilities/date'; -import getUserTimezone from './getUserTimezone'; +import { getUserTimezone } from './getUserTimezone'; export type TimeInterval = 'day' | 'month' | 'never' | 'week' | 'year'; diff --git a/packages/manager/src/utilities/getAPIErrorFor.ts b/packages/manager/src/utilities/getAPIErrorFor.ts index 9337694f54f..9980a5816b6 100644 --- a/packages/manager/src/utilities/getAPIErrorFor.ts +++ b/packages/manager/src/utilities/getAPIErrorFor.ts @@ -2,7 +2,7 @@ import { APIError } from '@linode/api-v4/lib/types'; import { DEFAULT_ERROR_MESSAGE } from 'src/constants'; -export default ( +export const getAPIErrorFor = ( errorMap: { [index: string]: string }, arr: APIError[] = [] ) => (field: string): string | undefined => { diff --git a/packages/manager/src/utilities/getSelectedOptionFromGroupedOptions/getSelectedOptionFromGroupedOptions.test.ts b/packages/manager/src/utilities/getSelectedOptionFromGroupedOptions.test.ts similarity index 100% rename from packages/manager/src/utilities/getSelectedOptionFromGroupedOptions/getSelectedOptionFromGroupedOptions.test.ts rename to packages/manager/src/utilities/getSelectedOptionFromGroupedOptions.test.ts diff --git a/packages/manager/src/utilities/getSelectedOptionFromGroupedOptions/getSelectedOptionFromGroupedOptions.ts b/packages/manager/src/utilities/getSelectedOptionFromGroupedOptions.ts similarity index 100% rename from packages/manager/src/utilities/getSelectedOptionFromGroupedOptions/getSelectedOptionFromGroupedOptions.ts rename to packages/manager/src/utilities/getSelectedOptionFromGroupedOptions.ts diff --git a/packages/manager/src/utilities/getSelectedOptionFromGroupedOptions/index.ts b/packages/manager/src/utilities/getSelectedOptionFromGroupedOptions/index.ts deleted file mode 100644 index 0cb598251eb..00000000000 --- a/packages/manager/src/utilities/getSelectedOptionFromGroupedOptions/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { getSelectedOptionFromGroupedOptions as default } from './getSelectedOptionFromGroupedOptions'; diff --git a/packages/manager/src/utilities/getUserTimezone.test.ts b/packages/manager/src/utilities/getUserTimezone.test.ts index de576d6e575..61e04f05f5a 100644 --- a/packages/manager/src/utilities/getUserTimezone.test.ts +++ b/packages/manager/src/utilities/getUserTimezone.test.ts @@ -3,7 +3,7 @@ import { DateTime } from 'luxon'; import { profileFactory } from 'src/factories/profile'; -import getUserTimezone from './getUserTimezone'; +import { getUserTimezone } from './getUserTimezone'; const mockProfile: Profile = profileFactory.build(); diff --git a/packages/manager/src/utilities/getUserTimezone.ts b/packages/manager/src/utilities/getUserTimezone.ts index d77c5dc0587..8cada0b91ad 100644 --- a/packages/manager/src/utilities/getUserTimezone.ts +++ b/packages/manager/src/utilities/getUserTimezone.ts @@ -1,11 +1,9 @@ import { DateTime, IANAZone } from 'luxon'; -const getUserTimezone = (profileTimezone?: string) => { +export const getUserTimezone = (profileTimezone?: string) => { return profileTimezone && profileTimezone != '' && IANAZone.isValidZone(profileTimezone) ? profileTimezone : DateTime.local().zoneName; }; - -export default getUserTimezone; diff --git a/packages/manager/src/utilities/minute-conversion/minute-conversion.test.ts b/packages/manager/src/utilities/minute-conversion.test.ts similarity index 100% rename from packages/manager/src/utilities/minute-conversion/minute-conversion.test.ts rename to packages/manager/src/utilities/minute-conversion.test.ts diff --git a/packages/manager/src/utilities/minute-conversion/minute-conversion.ts b/packages/manager/src/utilities/minute-conversion.ts similarity index 98% rename from packages/manager/src/utilities/minute-conversion/minute-conversion.ts rename to packages/manager/src/utilities/minute-conversion.ts index 22c3207fc3b..3812e14cf95 100644 --- a/packages/manager/src/utilities/minute-conversion/minute-conversion.ts +++ b/packages/manager/src/utilities/minute-conversion.ts @@ -1,4 +1,4 @@ -import { pluralize } from '../pluralize'; +import { pluralize } from './pluralize'; /** * diff --git a/packages/manager/src/utilities/minute-conversion/index.ts b/packages/manager/src/utilities/minute-conversion/index.ts deleted file mode 100644 index 10ecab44867..00000000000 --- a/packages/manager/src/utilities/minute-conversion/index.ts +++ /dev/null @@ -1,4 +0,0 @@ -export { - convertMinutesTo, - generateMigrationTimeString, -} from './minute-conversion'; diff --git a/packages/manager/src/utilities/omittedProps/omittedProps.test.tsx b/packages/manager/src/utilities/omittedProps.test.tsx similarity index 100% rename from packages/manager/src/utilities/omittedProps/omittedProps.test.tsx rename to packages/manager/src/utilities/omittedProps.test.tsx diff --git a/packages/manager/src/utilities/omittedProps/omittedProps.ts b/packages/manager/src/utilities/omittedProps.ts similarity index 100% rename from packages/manager/src/utilities/omittedProps/omittedProps.ts rename to packages/manager/src/utilities/omittedProps.ts diff --git a/packages/manager/src/utilities/omittedProps/index.ts b/packages/manager/src/utilities/omittedProps/index.ts deleted file mode 100644 index 151647732fa..00000000000 --- a/packages/manager/src/utilities/omittedProps/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { omittedProps } from './omittedProps'; diff --git a/packages/manager/src/utilities/roundTo/roundTo.test.ts b/packages/manager/src/utilities/roundTo.test.ts similarity index 100% rename from packages/manager/src/utilities/roundTo/roundTo.test.ts rename to packages/manager/src/utilities/roundTo.test.ts diff --git a/packages/manager/src/utilities/roundTo/roundTo.ts b/packages/manager/src/utilities/roundTo.ts similarity index 100% rename from packages/manager/src/utilities/roundTo/roundTo.ts rename to packages/manager/src/utilities/roundTo.ts diff --git a/packages/manager/src/utilities/roundTo/index.ts b/packages/manager/src/utilities/roundTo/index.ts deleted file mode 100644 index 2a565a802bc..00000000000 --- a/packages/manager/src/utilities/roundTo/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { roundTo as default } from './roundTo'; diff --git a/packages/manager/src/utilities/routing/isPathOneOf.test.ts b/packages/manager/src/utilities/routing/isPathOneOf.test.ts index 8c0ade54bba..fb46ba8b4ac 100644 --- a/packages/manager/src/utilities/routing/isPathOneOf.test.ts +++ b/packages/manager/src/utilities/routing/isPathOneOf.test.ts @@ -1,4 +1,4 @@ -import isPathOneOf from './isPathOneOf'; +import { isPathOneOf } from './isPathOneOf'; describe('isPathOneOf', () => { it('should return true when path is one of', () => { diff --git a/packages/manager/src/utilities/routing/isPathOneOf.ts b/packages/manager/src/utilities/routing/isPathOneOf.ts index bc927b4d733..9c4b2ab1c6c 100644 --- a/packages/manager/src/utilities/routing/isPathOneOf.ts +++ b/packages/manager/src/utilities/routing/isPathOneOf.ts @@ -6,7 +6,7 @@ import { RouteProps, matchPath } from 'react-router-dom'; * @param pathname Needle. * @param matchPath arguments. */ -export default ( +export const isPathOneOf = ( paths: string[], pathname: string, props?: RouteProps diff --git a/packages/manager/src/utilities/safeGetImageLabel/safeGetImageLabel.test.ts b/packages/manager/src/utilities/safeGetImageLabel.test.ts similarity index 100% rename from packages/manager/src/utilities/safeGetImageLabel/safeGetImageLabel.test.ts rename to packages/manager/src/utilities/safeGetImageLabel.test.ts diff --git a/packages/manager/src/utilities/safeGetImageLabel/safeGetImageLabel.ts b/packages/manager/src/utilities/safeGetImageLabel.ts similarity index 100% rename from packages/manager/src/utilities/safeGetImageLabel/safeGetImageLabel.ts rename to packages/manager/src/utilities/safeGetImageLabel.ts diff --git a/packages/manager/src/utilities/safeGetImageLabel/index.ts b/packages/manager/src/utilities/safeGetImageLabel/index.ts deleted file mode 100644 index a9c66949769..00000000000 --- a/packages/manager/src/utilities/safeGetImageLabel/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { safeGetImageLabel } from './safeGetImageLabel'; diff --git a/packages/manager/src/utilities/sanitize-html/sanitize-html.test.ts b/packages/manager/src/utilities/sanitize-html.test.ts similarity index 100% rename from packages/manager/src/utilities/sanitize-html/sanitize-html.test.ts rename to packages/manager/src/utilities/sanitize-html.test.ts diff --git a/packages/manager/src/utilities/sanitize-html/index.ts b/packages/manager/src/utilities/sanitize-html/index.ts deleted file mode 100644 index a2749d06803..00000000000 --- a/packages/manager/src/utilities/sanitize-html/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { sanitizeHTML } from './sanitizeHTML'; diff --git a/packages/manager/src/utilities/sanitize-html/sanitizeHTML.ts b/packages/manager/src/utilities/sanitizeHTML.ts similarity index 100% rename from packages/manager/src/utilities/sanitize-html/sanitizeHTML.ts rename to packages/manager/src/utilities/sanitizeHTML.ts diff --git a/packages/manager/src/utilities/scrollErrorIntoView.ts b/packages/manager/src/utilities/scrollErrorIntoView.ts index 9de2444c5e2..dc4bd139771 100644 --- a/packages/manager/src/utilities/scrollErrorIntoView.ts +++ b/packages/manager/src/utilities/scrollErrorIntoView.ts @@ -1,4 +1,4 @@ -const scrollErrorIntoView = ( +export const scrollErrorIntoView = ( errorGroup?: string, options?: ScrollIntoViewOptions ) => { @@ -14,5 +14,3 @@ const scrollErrorIntoView = ( }); } }; - -export default scrollErrorIntoView; diff --git a/packages/manager/src/utilities/shouldRequestEntity/index.ts b/packages/manager/src/utilities/shouldRequestEntity/index.ts deleted file mode 100644 index 1edd22ab7e4..00000000000 --- a/packages/manager/src/utilities/shouldRequestEntity/index.ts +++ /dev/null @@ -1,4 +0,0 @@ -export { - entityHasNeverBeenRequested, - shouldRequestEntity, -} from './shouldRequestEntity'; diff --git a/packages/manager/src/utilities/shouldRequestEntity/shouldRequestEntity.test.ts b/packages/manager/src/utilities/shouldRequestEntity/shouldRequestEntity.test.ts deleted file mode 100644 index 8525a39d76b..00000000000 --- a/packages/manager/src/utilities/shouldRequestEntity/shouldRequestEntity.test.ts +++ /dev/null @@ -1,77 +0,0 @@ -import { - entityHasNeverBeenRequested, - shouldRequestEntity, -} from './shouldRequestEntity'; - -const baseEntity = { - error: {}, - itemsById: {}, - lastUpdated: 0, - loading: false, - results: 0, -}; - -describe('Entity request helper logic', () => { - describe('shouldRequestEntity', () => { - it('should handle an undefined entity', () => { - expect(shouldRequestEntity(undefined as any)).toBe(true); - }); - - it('should return true for an entity that has never been requested', () => { - expect(shouldRequestEntity(baseEntity)).toBe(true); - }); - - it('should return false for a loading entity', () => { - expect(shouldRequestEntity({ ...baseEntity, loading: true })).toBe(false); - }); - - it("should return true for an entity that hasn't been requested in a long time", () => { - expect( - shouldRequestEntity({ - ...baseEntity, - lastUpdated: Date.now() - 60 * 60 * 1000, // one hour - }) - ).toBe(true); - }); - - it('should return false for an entity that has been requested recently', () => { - expect( - shouldRequestEntity({ - ...baseEntity, - lastUpdated: Date.now() - 10000, // 10 seconds - }) - ).toBe(false); - }); - - it('should accept a second argument to control the refresh interval', () => { - expect( - shouldRequestEntity( - { ...baseEntity, lastUpdated: Date.now() - 500 }, - 1000 - ) - ).toBe(false); - }); - }); - - describe('entityHasNeverBeenRequested', () => { - it('should handle an undefined entity', () => { - expect(entityHasNeverBeenRequested(undefined as any)).toBe(true); - }); - - it('should return true if not loading and lastUpdated is 0', () => { - expect(entityHasNeverBeenRequested(baseEntity)).toBe(true); - }); - - it('should return false if loading is true', () => { - expect( - entityHasNeverBeenRequested({ ...baseEntity, loading: true }) - ).toBe(false); - }); - - it('should return false if lastUpdated is > 0', () => { - expect( - entityHasNeverBeenRequested({ ...baseEntity, lastUpdated: 7 }) - ).toBe(false); - }); - }); -}); diff --git a/packages/manager/src/utilities/shouldRequestEntity/shouldRequestEntity.ts b/packages/manager/src/utilities/shouldRequestEntity/shouldRequestEntity.ts deleted file mode 100644 index 8ed393ea528..00000000000 --- a/packages/manager/src/utilities/shouldRequestEntity/shouldRequestEntity.ts +++ /dev/null @@ -1,33 +0,0 @@ -import { REFRESH_INTERVAL } from 'src/constants'; -import { MappedEntityState2 as MappedEntityState } from 'src/store/types'; - -/** - * In our Redux data shape, we initialize lastUpdated to 0 and only update it - * on a successful complete request (e.g. a getAllLinodes()). Whenever making - * a request, we set the entity's loading property to true. - * - * An entity that is not loading and whose lastUpdated is 0 has never been requested. - * We can also determine whether an entity is stale by subtracting its lastUpdated - * from a constant refresh interval. - */ -export const shouldRequestEntity = ( - entity: MappedEntityState, - refreshInterval: number = REFRESH_INTERVAL -) => { - if (!entity) { - // For nested entity states, usually this means we've never requested - // that entity, so it's best to trigger a request. - return true; - } - const isPastInterval = Date.now() - entity.lastUpdated > refreshInterval; - return !entity.loading && isPastInterval; -}; - -export const entityHasNeverBeenRequested = (entity: MappedEntityState) => { - if (!entity) { - // For nested entity states, usually this means we've never requested - // that entity, so it's best to trigger a request. - return true; - } - return !entity.loading && entity.lastUpdated === 0; -}; diff --git a/packages/manager/src/utilities/stripImageName.ts b/packages/manager/src/utilities/stripImageName.ts index bdf18165142..4db57e4dcac 100644 --- a/packages/manager/src/utilities/stripImageName.ts +++ b/packages/manager/src/utilities/stripImageName.ts @@ -1,4 +1,4 @@ -const stripImageName = (images: (null | string)[]) => { +export const stripImageName = (images: (null | string)[]) => { return images.reduce((acc: string[], image: string) => { if (image) { acc.push(image.replace('linode/', '')); @@ -6,5 +6,3 @@ const stripImageName = (images: (null | string)[]) => { return acc; }, []); }; - -export default stripImageName; diff --git a/packages/manager/src/utilities/substituteLink.tsx b/packages/manager/src/utilities/substituteLink.tsx deleted file mode 100644 index 568d2fac9c3..00000000000 --- a/packages/manager/src/utilities/substituteLink.tsx +++ /dev/null @@ -1,18 +0,0 @@ -import * as React from 'react'; -import { Link } from 'react-router-dom'; - -export const substituteLink = (text: string, substr: string, path: string) => { - const loc = text.toLowerCase().indexOf(substr.toLowerCase()); - if (loc === -1) { - return text; - } - return ( - - {text.substr(0, loc)} - {text.substr(loc, substr.length)} - {text.slice(loc + substr.length)} - - ); -}; - -export default substituteLink; diff --git a/packages/manager/src/utilities/updateOrAdd.ts b/packages/manager/src/utilities/updateOrAdd.ts deleted file mode 100644 index 847c90d21f5..00000000000 --- a/packages/manager/src/utilities/updateOrAdd.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { append, update } from 'ramda'; - -export default ( - item: T, - items: T[] = [] -) => { - if (items.length === 0) { - return [item]; - } - - const found = items.findIndex((i) => i.id === item.id); - if (found < 0) { - return append(item, items); - } - - return update(found, item, items); -}; diff --git a/packages/manager/src/utilities/updateRight.ts b/packages/manager/src/utilities/updateRight.ts index 95cf33f8678..98ddc976e5e 100644 --- a/packages/manager/src/utilities/updateRight.ts +++ b/packages/manager/src/utilities/updateRight.ts @@ -10,8 +10,6 @@ * * Note how the right is updated with the value of fn(left, right), but the left is untouched. */ -const updateRight = ( +export const updateRight = ( fn: (l: Left, r: Right) => Right ) => (arr: [Left, Right]) => [arr[0], fn(arr[0], arr[1])]; - -export default updateRight; diff --git a/packages/manager/src/utilities/userAgent.ts b/packages/manager/src/utilities/userAgent.ts new file mode 100644 index 00000000000..9c8359ca6d9 --- /dev/null +++ b/packages/manager/src/utilities/userAgent.ts @@ -0,0 +1 @@ +export const isOSMac = navigator.userAgent.includes('Mac'); diff --git a/packages/manager/src/utilities/validatePassword/validatePassword.test.ts b/packages/manager/src/utilities/validatePassword.test.ts similarity index 100% rename from packages/manager/src/utilities/validatePassword/validatePassword.test.ts rename to packages/manager/src/utilities/validatePassword.test.ts diff --git a/packages/manager/src/utilities/validatePassword/validatePassword.ts b/packages/manager/src/utilities/validatePassword.ts similarity index 100% rename from packages/manager/src/utilities/validatePassword/validatePassword.ts rename to packages/manager/src/utilities/validatePassword.ts diff --git a/packages/manager/src/utilities/validatePassword/index.ts b/packages/manager/src/utilities/validatePassword/index.ts deleted file mode 100644 index bd8ddc98f11..00000000000 --- a/packages/manager/src/utilities/validatePassword/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './validatePassword'; From a6ef14d2e90f3be77c58aeefdce68e7c8718262c Mon Sep 17 00:00:00 2001 From: corya-akamai <136115382+corya-akamai@users.noreply.github.com> Date: Thu, 19 Oct 2023 10:45:28 -0400 Subject: [PATCH 20/45] fix: [UIE-6961]: Only show regions that support Databases on the Database Create page (#9815) * fix [UIE-6961]: dbaas create filter regions by capability * Added changeset: Only show regions that support Databases on the Database Create page --------- Co-authored-by: Banks Nussman --- packages/api-v4/src/regions/types.ts | 1 + .../manager/.changeset/pr-9815-fixed-1697665414544.md | 5 +++++ .../features/Databases/DatabaseCreate/DatabaseCreate.tsx | 8 +++++++- 3 files changed, 13 insertions(+), 1 deletion(-) create mode 100644 packages/manager/.changeset/pr-9815-fixed-1697665414544.md diff --git a/packages/api-v4/src/regions/types.ts b/packages/api-v4/src/regions/types.ts index a3a17e4ea84..e9bab3df5bf 100644 --- a/packages/api-v4/src/regions/types.ts +++ b/packages/api-v4/src/regions/types.ts @@ -5,6 +5,7 @@ export type Capabilities = | 'GPU Linodes' | 'Kubernetes' | 'Linodes' + | 'Managed Databases' | 'Metadata' | 'NodeBalancers' | 'Object Storage' diff --git a/packages/manager/.changeset/pr-9815-fixed-1697665414544.md b/packages/manager/.changeset/pr-9815-fixed-1697665414544.md new file mode 100644 index 00000000000..1ce76093167 --- /dev/null +++ b/packages/manager/.changeset/pr-9815-fixed-1697665414544.md @@ -0,0 +1,5 @@ +--- +"@linode/manager": Fixed +--- + +Only show regions that support Databases on the Database Create page ([#9815](https://github.com/linode/manager/pull/9815)) diff --git a/packages/manager/src/features/Databases/DatabaseCreate/DatabaseCreate.tsx b/packages/manager/src/features/Databases/DatabaseCreate/DatabaseCreate.tsx index f66a874cbd8..2f7f535c1fe 100644 --- a/packages/manager/src/features/Databases/DatabaseCreate/DatabaseCreate.tsx +++ b/packages/manager/src/features/Databases/DatabaseCreate/DatabaseCreate.tsx @@ -52,6 +52,7 @@ import { useDatabaseTypesQuery, } from 'src/queries/databases'; import { useRegionsQuery } from 'src/queries/regions'; +import { regionsWithFeature } from 'src/utilities/doesRegionSupportFeature'; import { formatStorageUnits } from 'src/utilities/formatStorageUnits'; import { handleAPIErrors } from 'src/utilities/formikErrorUtils'; import { getSelectedOptionFromGroupedOptions } from 'src/utilities/getSelectedOptionFromGroupedOptions'; @@ -201,6 +202,11 @@ const DatabaseCreate = () => { isLoading: regionsLoading, } = useRegionsQuery(); + const regionsThatSupportDbaas = regionsWithFeature( + regionsData ?? [], + 'Managed Databases' + ); + const { data: engines, error: enginesError, @@ -502,7 +508,7 @@ const DatabaseCreate = () => { setFieldValue('region', selected) } errorText={errors.region} - regions={regionsData} + regions={regionsThatSupportDbaas} selectedID={values.region} /> From 3df9d02af32036d89388bd0fdc22f4499e7bf5bf Mon Sep 17 00:00:00 2001 From: corya-akamai <136115382+corya-akamai@users.noreply.github.com> Date: Thu, 19 Oct 2023 11:35:15 -0400 Subject: [PATCH 21/45] fix: [UIE-6952] - DBaaS menu flickers on page load (#9808) * fix [UIE-6952]: dbaas menu flickers on page load * Added changeset: Primary nav DBaaS menuitem flicker on page load --------- Co-authored-by: Alban Bailly --- .../manager/.changeset/pr-9808-fixed-1697722511987.md | 5 +++++ packages/manager/src/MainContent.tsx | 11 +++++++---- .../manager/src/components/PrimaryNav/PrimaryNav.tsx | 7 +++++-- .../src/features/TopMenu/AddNewMenu/AddNewMenu.tsx | 7 +++++-- 4 files changed, 22 insertions(+), 8 deletions(-) create mode 100644 packages/manager/.changeset/pr-9808-fixed-1697722511987.md diff --git a/packages/manager/.changeset/pr-9808-fixed-1697722511987.md b/packages/manager/.changeset/pr-9808-fixed-1697722511987.md new file mode 100644 index 00000000000..8b271774db0 --- /dev/null +++ b/packages/manager/.changeset/pr-9808-fixed-1697722511987.md @@ -0,0 +1,5 @@ +--- +"@linode/manager": Fixed +--- + +Primary nav DBaaS menuitem flicker on page load ([#9808](https://github.com/linode/manager/pull/9808)) diff --git a/packages/manager/src/MainContent.tsx b/packages/manager/src/MainContent.tsx index e10c6310694..bf6ccaf5901 100644 --- a/packages/manager/src/MainContent.tsx +++ b/packages/manager/src/MainContent.tsx @@ -207,7 +207,10 @@ const MainContent = (props: CombinedProps) => { const [bannerDismissed, setBannerDismissed] = React.useState(false); const checkRestrictedUser = !Boolean(flags.databases) && !!accountError; - const { error: enginesError } = useDatabaseEnginesQuery(checkRestrictedUser); + const { + error: enginesError, + isLoading: enginesLoading, + } = useDatabaseEnginesQuery(checkRestrictedUser); const showDatabases = isFeatureEnabled( @@ -215,7 +218,7 @@ const MainContent = (props: CombinedProps) => { Boolean(flags.databases), account?.capabilities ?? [] ) || - (checkRestrictedUser && !enginesError); + (checkRestrictedUser && !enginesLoading && !enginesError); const showVPCs = isFeatureEnabled( 'VPCs', @@ -413,8 +416,8 @@ export default compose( export const checkFlagsForMainContentBanner = (flags: FlagSet) => { return Boolean( flags.mainContentBanner && - !isEmpty(flags.mainContentBanner) && - flags.mainContentBanner.key + !isEmpty(flags.mainContentBanner) && + flags.mainContentBanner.key ); }; diff --git a/packages/manager/src/components/PrimaryNav/PrimaryNav.tsx b/packages/manager/src/components/PrimaryNav/PrimaryNav.tsx index 15602de10c3..93c3d26a686 100644 --- a/packages/manager/src/components/PrimaryNav/PrimaryNav.tsx +++ b/packages/manager/src/components/PrimaryNav/PrimaryNav.tsx @@ -123,7 +123,10 @@ export const PrimaryNav = (props: Props) => { !oneClickApps && !oneClickAppsLoading && !oneClickAppsError; const checkRestrictedUser = !Boolean(flags.databases) && !!accountError; - const { error: enginesError } = useDatabaseEnginesQuery(checkRestrictedUser); + const { + error: enginesError, + isLoading: enginesLoading, + } = useDatabaseEnginesQuery(checkRestrictedUser); const showDatabases = isFeatureEnabled( @@ -131,7 +134,7 @@ export const PrimaryNav = (props: Props) => { Boolean(flags.databases), account?.capabilities ?? [] ) || - (checkRestrictedUser && !enginesError); + (checkRestrictedUser && !enginesLoading && !enginesError); const showVPCs = isFeatureEnabled( 'VPCs', diff --git a/packages/manager/src/features/TopMenu/AddNewMenu/AddNewMenu.tsx b/packages/manager/src/features/TopMenu/AddNewMenu/AddNewMenu.tsx index 0f0e06a10ff..7a1617df939 100644 --- a/packages/manager/src/features/TopMenu/AddNewMenu/AddNewMenu.tsx +++ b/packages/manager/src/features/TopMenu/AddNewMenu/AddNewMenu.tsx @@ -46,7 +46,10 @@ export const AddNewMenu = () => { const open = Boolean(anchorEl); const checkRestrictedUser = !Boolean(flags.databases) && !!accountError; - const { error: enginesError } = useDatabaseEnginesQuery(checkRestrictedUser); + const { + error: enginesError, + isLoading: enginesLoading, + } = useDatabaseEnginesQuery(checkRestrictedUser); const showDatabases = isFeatureEnabled( @@ -54,7 +57,7 @@ export const AddNewMenu = () => { Boolean(flags.databases), account?.capabilities ?? [] ) || - (checkRestrictedUser && !enginesError); + (checkRestrictedUser && !enginesLoading && !enginesError); const showVPCs = isFeatureEnabled( 'VPCs', From fd2dcaec76e7697e8fdb15c4f2a8c963399f016c Mon Sep 17 00:00:00 2001 From: Banks Nussman <115251059+bnussman-akamai@users.noreply.github.com> Date: Thu, 19 Oct 2023 12:17:00 -0400 Subject: [PATCH 22/45] test: [M3-7283] - Make `ConfigureForm.test.tsx` Vitest compatible (#9816) * rework `ConfigureForm.test.tsx` * add changeset --------- Co-authored-by: Banks Nussman --- .../.changeset/pr-9816-tests-1697660159676.md | 5 + .../MigrateLinode/ConfigureForm.test.tsx | 207 +++++++++++------- 2 files changed, 132 insertions(+), 80 deletions(-) create mode 100644 packages/manager/.changeset/pr-9816-tests-1697660159676.md diff --git a/packages/manager/.changeset/pr-9816-tests-1697660159676.md b/packages/manager/.changeset/pr-9816-tests-1697660159676.md new file mode 100644 index 00000000000..02f1110d853 --- /dev/null +++ b/packages/manager/.changeset/pr-9816-tests-1697660159676.md @@ -0,0 +1,5 @@ +--- +"@linode/manager": Tests +--- + +Make `ConfigureForm.test.tsx` Vitest compatible ([#9816](https://github.com/linode/manager/pull/9816)) diff --git a/packages/manager/src/features/Linodes/MigrateLinode/ConfigureForm.test.tsx b/packages/manager/src/features/Linodes/MigrateLinode/ConfigureForm.test.tsx index 092ab04b926..dc40f6c9201 100644 --- a/packages/manager/src/features/Linodes/MigrateLinode/ConfigureForm.test.tsx +++ b/packages/manager/src/features/Linodes/MigrateLinode/ConfigureForm.test.tsx @@ -2,30 +2,50 @@ import { waitFor } from '@testing-library/react'; import React from 'react'; import { typeFactory } from 'src/factories/types'; -import { renderWithTheme, wrapWithTheme } from 'src/utilities/testHelpers'; +import { rest, server } from 'src/mocks/testServer'; +import { renderWithTheme } from 'src/utilities/testHelpers'; import { ConfigureForm } from './ConfigureForm'; -// Mock the useFlags hook -jest.mock('src/hooks/useFlags', () => ({ - useFlags: () => ({ - dcSpecificPricing: true, // Mock the flag value - }), -})); - -// Mock the useTypeQuery hook -jest.mock('src/queries/types', () => ({ - useTypeQuery: () => ({ - data: typeFactory.build(), - }), -})); - -// Mock the useRegionsQuery hook -jest.mock('src/queries/regions', () => ({ - useRegionsQuery: () => ({ - data: [], - }), -})); +const mockLinodeType = typeFactory.build({ + addons: { + backups: { + price: { + hourly: 0.004, + monthly: 2.5, + }, + region_prices: [ + { + hourly: 0.0048, + id: 'id-cgk', + monthly: 3.57, + }, + { + hourly: 0.0056, + id: 'br-gru', + monthly: 4.17, + }, + ], + }, + }, + id: 'g6-standard-1', + price: { + hourly: 0.015, + monthly: 10, + }, + region_prices: [ + { + hourly: 0.021, + id: 'br-gru', + monthly: 14.4, + }, + { + hourly: 0.018, + id: 'id-cgk', + monthly: 12.2, + }, + ], +}); describe('ConfigureForm component with price comparison', () => { const handleSelectRegion = jest.fn(); @@ -42,39 +62,22 @@ describe('ConfigureForm component with price comparison', () => { selectedRegion: '', }; - const { - getByLabelText, - getByTestId, - getByText, - queryByText, - } = renderWithTheme(); - - interface SelectNewRegionOptions { - backupEnabled?: boolean; - currentRegionId?: string; - selectedRegionId: string; - } - - const selectNewRegion = ({ - backupEnabled = true, - currentRegionId = 'us-east', - selectedRegionId, - }: SelectNewRegionOptions) => { - const { rerender } = renderWithTheme(); - - rerender( - wrapWithTheme( - - ) + beforeEach(() => { + server.use( + rest.get('*/linode/types/g6-standard-1', (req, res, ctx) => { + return res(ctx.json(mockLinodeType)); + }) ); - }; + }); it('should render the initial ConfigureForm fields', () => { + const { getByLabelText, getByText, queryByText } = renderWithTheme( + , + { + flags: { dcSpecificPricing: true }, + } + ); + // Test whether the initial component renders the expected content expect(getByText('Configure Migration')).toBeInTheDocument(); expect(getByText('Current Region')).toBeInTheDocument(); @@ -89,7 +92,13 @@ describe('ConfigureForm component with price comparison', () => { }); it("shouldn't render the MigrationPricing component when the current region is selected", async () => { - selectNewRegion({ selectedRegionId: 'us-east' }); + const { queryByText } = renderWithTheme( + , + { + flags: { dcSpecificPricing: true }, + } + ); + await waitFor(() => { expect(queryByText(currentPriceLabel)).not.toBeInTheDocument(); expect(queryByText(newPriceLabel)).not.toBeInTheDocument(); @@ -97,7 +106,13 @@ describe('ConfigureForm component with price comparison', () => { }); it("shouldn't render the MigrationPricing component when a region without price increase is selected", async () => { - selectNewRegion({ selectedRegionId: 'us-west' }); + const { queryByText } = renderWithTheme( + , + { + flags: { dcSpecificPricing: true }, + } + ); + await waitFor(() => { expect(queryByText(currentPriceLabel)).not.toBeInTheDocument(); expect(queryByText(newPriceLabel)).not.toBeInTheDocument(); @@ -105,7 +120,13 @@ describe('ConfigureForm component with price comparison', () => { }); it('should render the MigrationPricing component when a region with price increase is selected', async () => { - selectNewRegion({ selectedRegionId: 'br-gru' }); + const { getByTestId } = renderWithTheme( + , + { + flags: { dcSpecificPricing: true }, + } + ); + await waitFor(() => { expect(getByTestId(currentPricePanel)).toBeDefined(); expect(getByTestId(newPricePanel)).toBeDefined(); @@ -113,7 +134,17 @@ describe('ConfigureForm component with price comparison', () => { }); it('should render the MigrationPricing component when a region with price decrease is selected', async () => { - selectNewRegion({ currentRegionId: 'br-gru', selectedRegionId: 'us-east' }); + const { getByTestId } = renderWithTheme( + , + { + flags: { dcSpecificPricing: true }, + } + ); + await waitFor(() => { expect(getByTestId(currentPricePanel)).toBeDefined(); expect(getByTestId(newPricePanel)).toBeDefined(); @@ -121,37 +152,53 @@ describe('ConfigureForm component with price comparison', () => { }); it('should provide a proper price comparison', async () => { - selectNewRegion({ selectedRegionId: 'br-gru' }); - expect(getByTestId(currentPricePanel)).toHaveTextContent( - '$10.00/month, $0.015/hour | Backups $2.50/month' - ); - expect(getByTestId(newPricePanel)).toHaveTextContent( - '$14.40/month, $0.021/hour | Backups $4.17/month' + const { getByTestId } = renderWithTheme( + , + { + flags: { dcSpecificPricing: true }, + } ); + + await waitFor(() => { + expect(getByTestId(currentPricePanel)).toHaveTextContent( + '$10.00/month, $0.015/hour | Backups $2.50/month' + ); + expect(getByTestId(newPricePanel)).toHaveTextContent( + '$14.40/month, $0.021/hour | Backups $4.17/month' + ); + }); }); - it("shouldn't render the Backup pricing comparison if backups are disabled", () => { - selectNewRegion({ backupEnabled: false, selectedRegionId: 'br-gru' }); - expect(getByTestId(currentPricePanel)).toHaveTextContent( - '$10.00/month, $0.015/hour' - ); - expect(getByTestId(newPricePanel)).toHaveTextContent( - '$14.40/month, $0.021/hour' + it("shouldn't render the Backup pricing comparison if backups are disabled", async () => { + const { getByTestId } = renderWithTheme( + , + { + flags: { dcSpecificPricing: true }, + } ); + + await waitFor(() => { + expect(getByTestId(currentPricePanel)).toHaveTextContent( + '$10.00/month, $0.015/hour' + ); + expect(getByTestId(newPricePanel)).toHaveTextContent( + '$14.40/month, $0.021/hour' + ); + }); }); - it("shouldn't render the MigrationPricingComponent if the flag is disabled", () => { - jest.isolateModules(async () => { - jest.mock('src/hooks/useFlags', () => ({ - useFlags: () => ({ - dcSpecificPricing: false, - }), - })); - - await waitFor(() => { - expect(queryByText('Current Price')).not.toBeInTheDocument(); - expect(queryByText('New Price')).not.toBeInTheDocument(); - }); + it("shouldn't render the MigrationPricingComponent if the flag is disabled", async () => { + const { queryByText } = renderWithTheme(, { + flags: { dcSpecificPricing: false }, + }); + + await waitFor(() => { + expect(queryByText('Current Price')).not.toBeInTheDocument(); + expect(queryByText('New Price')).not.toBeInTheDocument(); }); }); }); From 28471164ad967fa4d14be2d7c7e8571e3a782ece Mon Sep 17 00:00:00 2001 From: cliu-akamai <126020611+cliu-akamai@users.noreply.github.com> Date: Thu, 19 Oct 2023 12:49:33 -0400 Subject: [PATCH 23/45] test: [M3-7071] - Add Cypress integration tests for NodeBalancer Create DC-specific pricing (#9765) --- .../smoke-create-nodebal.spec.ts | 86 +++++++++++++++++-- 1 file changed, 80 insertions(+), 6 deletions(-) diff --git a/packages/manager/cypress/e2e/core/nodebalancers/smoke-create-nodebal.spec.ts b/packages/manager/cypress/e2e/core/nodebalancers/smoke-create-nodebal.spec.ts index e1739c9cbe2..f2ed69a74ad 100644 --- a/packages/manager/cypress/e2e/core/nodebalancers/smoke-create-nodebal.spec.ts +++ b/packages/manager/cypress/e2e/core/nodebalancers/smoke-create-nodebal.spec.ts @@ -11,6 +11,12 @@ import { import { apiMatcher } from 'support/util/intercepts'; import { randomLabel } from 'support/util/random'; import { chooseRegion, getRegionById } from 'support/util/regions'; +import { dcPricingRegionNotice } from 'support/constants/dc-specific-pricing'; +import { + mockAppendFeatureFlags, + mockGetFeatureFlagClientstream, +} from 'support/intercepts/feature-flags'; +import { makeFeatureFlagData } from 'support/util/feature-flags'; import { ui } from 'support/ui'; import { cleanUp } from 'support/util/cleanup'; import { authenticate } from 'support/api/authentication'; @@ -19,13 +25,48 @@ const deployNodeBalancer = () => { cy.get('[data-qa-deploy-nodebalancer]').click(); }; -const createNodeBalancerWithUI = (nodeBal) => { +const createNodeBalancerWithUI = (nodeBal, isDcPricingTest = false) => { const regionName = getRegionById(nodeBal.region).label; cy.visitWithLogin('/nodebalancers/create'); getVisible('[id="nodebalancer-label"]').click().clear().type(nodeBal.label); containsClick('create a tag').type(entityTag); - // this will create the NB in newark, where the default Linode was created - containsClick(selectRegionString).type(`${regionName}{enter}`); + + if (isDcPricingTest) { + const newRegion = getRegionById('br-gru'); + + cy.wait(['@getClientStream', '@getFeatureFlags']); + + // Confirms that the price will not display when the region is not selected + cy.get('[data-qa-summary="true"]').within(() => { + cy.findByText('/month').should('not.exist'); + }); + + // Confirms that the price will show up when the region is selected + containsClick(selectRegionString).type(`${regionName}{enter}`); + cy.get('[data-qa-summary="true"]').within(() => { + cy.findByText(`$10.00/month`).should('be.visible'); + }); + + // TODO: DC Pricing - M3-7086: Uncomment docs link assertion when docs links are added. + // cy.findByText(dcPricingDocsLabel) + // .should('be.visible') + // .should('have.attr', 'href', dcPricingDocsUrl); + + // Confirms that the summary updates to reflect price changes if the user changes their region. + cy.get(`[value="${regionName}"]`).click().type(`${newRegion.label}{enter}`); + cy.get('[data-qa-summary="true"]').within(() => { + cy.findByText(`$14.00/month`).should('be.visible'); + }); + + // Confirms that a notice is shown in the "Region" section of the NodeBalancer Create form informing the user of DC-specific pricing + cy.findByText(dcPricingRegionNotice, { exact: false }).should('be.visible'); + + // Change back to the initial region to create the Node Balancer + cy.get(`[value="${newRegion.label}"]`).click().type(`${regionName}{enter}`); + } else { + // this will create the NB in newark, where the default Linode was created + containsClick(selectRegionString).type(`${regionName}{enter}`); + } // node backend config fbtClick('Label').type(randomLabel()); @@ -46,10 +87,10 @@ const createNodeBalancerWithUI = (nodeBal) => { authenticate(); describe('create NodeBalancer', () => { before(() => { - cleanUp(['tags', 'node-balancers']); + cleanUp(['tags', 'node-balancers', 'linodes']); }); - it('creates a nodebal - positive', () => { + it('creates a NodeBalancer in a region with base pricing', () => { // create a linode in NW where the NB will be created const region = chooseRegion(); createLinode({ region: region.id }).then((linode) => { @@ -68,7 +109,12 @@ describe('create NodeBalancer', () => { .should('eq', 200); }); }); - it('API error Handling', () => { + + /* + * - Confirms label field displays error if it contains special characters. + * - Confirms session stickiness field displays error if protocol is not HTTP or HTTPS. + */ + it('displays API errors for NodeBalancer Create form fields', () => { const region = chooseRegion(); createLinode({ region: region.id }).then((linode) => { // catch request @@ -100,4 +146,32 @@ describe('create NodeBalancer', () => { fbtVisible(errMessage); }); }); + + /* + * - Confirms DC-specific pricing UI flow works as expected during NodeBalancer creation. + * - Confirms that pricing notice is shown in "Region" section. + * - Confirms that notice is shown when selecting a region with a different price structure. + */ + it('shows DC-specific pricing information when creating a NodeBalancer', () => { + const initialRegion = getRegionById('us-west'); + createLinode({ region: initialRegion.id }).then((linode) => { + const nodeBal = { + label: randomLabel(), + region: initialRegion.id, + linodePrivateIp: linode.ipv4[1], + }; + + // catch request + cy.intercept('POST', apiMatcher('nodebalancers')).as( + 'createNodeBalancer' + ); + + mockAppendFeatureFlags({ + dcSpecificPricing: makeFeatureFlagData(true), + }).as('getFeatureFlags'); + mockGetFeatureFlagClientstream().as('getClientStream'); + + createNodeBalancerWithUI(nodeBal, true); + }); + }); }); From d674091db6de49594dd7f5750218716cefd0b891 Mon Sep 17 00:00:00 2001 From: Jaalah Ramos <125309814+jaalah-akamai@users.noreply.github.com> Date: Thu, 19 Oct 2023 12:55:13 -0400 Subject: [PATCH 24/45] feat: [M3-6816] - Add AGLB Create Route Drawer (#9806) --------- Co-authored-by: Jaalah Ramos Co-authored-by: Connie Liu <139280159+coliu-akamai@users.noreply.github.com> --- ...pr-9806-upcoming-features-1697723638782.md | 5 + packages/api-v4/src/aglb/routes.ts | 8 +- ...pr-9806-upcoming-features-1697563902060.md | 5 + .../load-balancer-routes.spec.ts | 87 ++++++++++++++ .../support/intercepts/load-balancers.ts | 28 ++++- .../LoadBalancerDetail/LoadBalancerRoutes.tsx | 15 ++- .../Routes/CreateRouteDrawer.tsx | 113 ++++++++++++++++++ packages/manager/src/queries/aglb/routes.ts | 19 +++ ...pr-9806-upcoming-features-1697723572839.md | 5 + .../validation/src/loadbalancers.schema.ts | 5 + 10 files changed, 279 insertions(+), 11 deletions(-) create mode 100644 packages/api-v4/.changeset/pr-9806-upcoming-features-1697723638782.md create mode 100644 packages/manager/.changeset/pr-9806-upcoming-features-1697563902060.md create mode 100644 packages/manager/src/features/LoadBalancers/LoadBalancerDetail/Routes/CreateRouteDrawer.tsx create mode 100644 packages/validation/.changeset/pr-9806-upcoming-features-1697723572839.md diff --git a/packages/api-v4/.changeset/pr-9806-upcoming-features-1697723638782.md b/packages/api-v4/.changeset/pr-9806-upcoming-features-1697723638782.md new file mode 100644 index 00000000000..0305847e830 --- /dev/null +++ b/packages/api-v4/.changeset/pr-9806-upcoming-features-1697723638782.md @@ -0,0 +1,5 @@ +--- +"@linode/api-v4": Upcoming Features +--- + +Updated AGLB createLoadbalancerRoute endpoint with payload/schema ([#9806](https://github.com/linode/manager/pull/9806)) diff --git a/packages/api-v4/src/aglb/routes.ts b/packages/api-v4/src/aglb/routes.ts index ff0307219f4..d97f344faf4 100644 --- a/packages/api-v4/src/aglb/routes.ts +++ b/packages/api-v4/src/aglb/routes.ts @@ -7,8 +7,8 @@ import Request, { } from '../request'; import { Filter, Params, ResourcePage } from '../types'; import { BETA_API_ROOT } from '../constants'; -import type { Route, RoutePayload, UpdateRoutePayload } from './types'; -import { UpdateRouteSchema } from '@linode/validation'; +import type { Route, CreateRoutePayload, UpdateRoutePayload } from './types'; +import { UpdateRouteSchema, CreateRouteSchema } from '@linode/validation'; /** * getLoadbalancerRoutes @@ -51,13 +51,13 @@ export const getLoadbalancerRoute = (loadbalancerId: number, routeId: number) => */ export const createLoadbalancerRoute = ( loadbalancerId: number, - data: RoutePayload + data: CreateRoutePayload ) => Request( setURL( `${BETA_API_ROOT}/aglb/${encodeURIComponent(loadbalancerId)}/routes` ), - setData(data), + setData(data, CreateRouteSchema), setMethod('POST') ); diff --git a/packages/manager/.changeset/pr-9806-upcoming-features-1697563902060.md b/packages/manager/.changeset/pr-9806-upcoming-features-1697563902060.md new file mode 100644 index 00000000000..12e905fbb7b --- /dev/null +++ b/packages/manager/.changeset/pr-9806-upcoming-features-1697563902060.md @@ -0,0 +1,5 @@ +--- +"@linode/manager": Upcoming Features +--- + +Add AGLB Create Route Drawer ([#9806](https://github.com/linode/manager/pull/9806)) diff --git a/packages/manager/cypress/e2e/core/loadBalancers/load-balancer-routes.spec.ts b/packages/manager/cypress/e2e/core/loadBalancers/load-balancer-routes.spec.ts index 81a7980815c..ac4f786a6fb 100644 --- a/packages/manager/cypress/e2e/core/loadBalancers/load-balancer-routes.spec.ts +++ b/packages/manager/cypress/e2e/core/loadBalancers/load-balancer-routes.spec.ts @@ -429,3 +429,90 @@ describe('Akamai Global Load Balancer routes page', () => { cy.findByText('You reached a rate limit', { exact: false }); }); }); + +it('can create a Route', () => { + const loadbalancer = loadbalancerFactory.build(); + const routes = routeFactory.buildList(1, { protocol: 'http' }); + + mockAppendFeatureFlags({ + aglb: makeFeatureFlagData(true), + }).as('getFeatureFlags'); + mockGetFeatureFlagClientstream().as('getClientStream'); + mockGetLoadBalancer(loadbalancer).as('getLoadBalancer'); + mockGetLoadBalancerRoutes(loadbalancer.id, routes).as('getRoutes'); + + cy.visitWithLogin(`/loadbalancers/${loadbalancer.id}/routes`); + cy.wait([ + '@getFeatureFlags', + '@getClientStream', + '@getLoadBalancer', + '@getRoutes', + ]); + + ui.button + .findByTitle('Create Route') + .should('be.visible') + .should('be.enabled') + .click(); + + mockUpdateRoute(loadbalancer, routes[0]).as('updateRoute'); + + ui.drawer + .findByTitle('Create Route') + .should('be.visible') + .within(() => { + cy.findByLabelText('Route Label') + .should('be.visible') + .click() + .type(routes[0].label); + + cy.get('[data-qa-radio="HTTP"]').find('input').should('be.checked'); + + ui.buttonGroup + .findButtonByTitle('Create Route') + .should('be.visible') + .should('be.enabled') + .click(); + }); +}); + +it('surfaces API errors in the Create Route Drawer', () => { + const loadbalancer = loadbalancerFactory.build(); + const routes = routeFactory.buildList(1, { protocol: 'http' }); + + mockAppendFeatureFlags({ + aglb: makeFeatureFlagData(true), + }).as('getFeatureFlags'); + mockGetFeatureFlagClientstream().as('getClientStream'); + mockGetLoadBalancer(loadbalancer).as('getLoadBalancer'); + mockGetLoadBalancerRoutes(loadbalancer.id, routes).as('getRoutes'); + + cy.visitWithLogin(`/loadbalancers/${loadbalancer.id}/routes`); + cy.wait([ + '@getFeatureFlags', + '@getClientStream', + '@getLoadBalancer', + '@getRoutes', + ]); + + ui.button + .findByTitle('Create Route') + .should('be.visible') + .should('be.enabled') + .click(); + + mockUpdateRoute(loadbalancer, routes[0]).as('updateRoute'); + + ui.drawer + .findByTitle('Create Route') + .should('be.visible') + .within(() => { + ui.buttonGroup + .findButtonByTitle('Create Route') + .should('be.visible') + .should('be.enabled') + .click(); + }); + + cy.findByText('Label is required.', { exact: false }); +}); diff --git a/packages/manager/cypress/support/intercepts/load-balancers.ts b/packages/manager/cypress/support/intercepts/load-balancers.ts index ac8f461f591..23cdc1df11b 100644 --- a/packages/manager/cypress/support/intercepts/load-balancers.ts +++ b/packages/manager/cypress/support/intercepts/load-balancers.ts @@ -1,14 +1,15 @@ -import { paginateResponse } from 'support/util/paginate'; import { makeErrorResponse } from 'support/util/errors'; import { apiMatcher } from 'support/util/intercepts'; +import { paginateResponse } from 'support/util/paginate'; +import { makeResponse } from 'support/util/response'; + import type { - ServiceTarget, - Loadbalancer, - Configuration, Certificate, + Configuration, + Loadbalancer, Route, + ServiceTarget, } from '@linode/api-v4'; -import { makeResponse } from 'support/util/response'; /** * Intercepts GET request to fetch an AGLB load balancer and mocks response. @@ -103,6 +104,7 @@ export const mockUploadLoadBalancerCertificate = ( * * @returns Cypress chainable. */ +// TODO: AGLB - We should probably remove this mock and use "mockGetLoadBalancerServiceTargets" below. export const mockGetServiceTargets = ( loadBalancer: Loadbalancer, serviceTargets: ServiceTarget[] @@ -149,6 +151,22 @@ export const mockCreateServiceTarget = ( ); }; +/** + * Intercepts POST request to create a route and mocks response. + * + * @param loadBalancer - Load balancer for mocked route. + * @param route - Route with which to mock response. + * + * @returns Cypress chainable. + */ +export const mockCreateRoute = (loadBalancer: Loadbalancer, route: Route) => { + return cy.intercept( + 'POST', + apiMatcher(`/aglb/${loadBalancer.id}/routes`), + makeResponse(route) + ); +}; + /** * Intercepts GET requests to retrieve AGLB load balancer routes and mocks response. * diff --git a/packages/manager/src/features/LoadBalancers/LoadBalancerDetail/LoadBalancerRoutes.tsx b/packages/manager/src/features/LoadBalancers/LoadBalancerDetail/LoadBalancerRoutes.tsx index ee313df1e6d..ff0b8162225 100644 --- a/packages/manager/src/features/LoadBalancers/LoadBalancerDetail/LoadBalancerRoutes.tsx +++ b/packages/manager/src/features/LoadBalancers/LoadBalancerDetail/LoadBalancerRoutes.tsx @@ -24,6 +24,7 @@ import { useOrder } from 'src/hooks/useOrder'; import { usePagination } from 'src/hooks/usePagination'; import { useLoadBalancerRoutesQuery } from 'src/queries/aglb/routes'; +import { CreateRouteDrawer } from './Routes/CreateRouteDrawer'; import { DeleteRouteDialog } from './Routes/DeleteRouteDialog'; import { RuleDrawer } from './Routes/RuleDrawer'; import { RulesTable } from './RulesTable'; @@ -34,7 +35,7 @@ const PREFERENCE_KEY = 'loadbalancer-routes'; export const LoadBalancerRoutes = () => { const { loadbalancerId } = useParams<{ loadbalancerId: string }>(); - + const [isCreateDrawerOpen, setIsCreateDrawerOpen] = useState(false); const [isAddRuleDrawerOpen, setIsAddRuleDrawerOpen] = useState(false); const [query, setQuery] = useState(); const [isDeleteDialogOpen, setIsDeleteDialogOpen] = useState(false); @@ -213,7 +214,12 @@ export const LoadBalancerRoutes = () => { {/** * TODO: AGLB: The Create Route behavior should be implemented in future AGLB tickets. */} - + { route={selectedRoute} ruleIndexToEdit={selectedRuleIndex} /> + setIsCreateDrawerOpen(false)} + open={isCreateDrawerOpen} + /> setIsDeleteDialogOpen(false)} diff --git a/packages/manager/src/features/LoadBalancers/LoadBalancerDetail/Routes/CreateRouteDrawer.tsx b/packages/manager/src/features/LoadBalancers/LoadBalancerDetail/Routes/CreateRouteDrawer.tsx new file mode 100644 index 00000000000..99a82d43fbb --- /dev/null +++ b/packages/manager/src/features/LoadBalancers/LoadBalancerDetail/Routes/CreateRouteDrawer.tsx @@ -0,0 +1,113 @@ +import { CreateRoutePayload } from '@linode/api-v4'; +import { useFormik } from 'formik'; +import React from 'react'; + +import { ActionsPanel } from 'src/components/ActionsPanel/ActionsPanel'; +import { Drawer } from 'src/components/Drawer'; +import { FormControlLabel } from 'src/components/FormControlLabel'; +import { FormHelperText } from 'src/components/FormHelperText'; +import { FormLabel } from 'src/components/FormLabel'; +import { Notice } from 'src/components/Notice/Notice'; +import { Radio } from 'src/components/Radio/Radio'; +import { RadioGroup } from 'src/components/RadioGroup'; +import { TextField } from 'src/components/TextField'; +import { useLoadBalancerRouteCreateMutation } from 'src/queries/aglb/routes'; +import { capitalize } from 'src/utilities/capitalize'; +import { getFormikErrorsFromAPIErrors } from 'src/utilities/formikErrorUtils'; +import { scrollErrorIntoView } from 'src/utilities/scrollErrorIntoView'; + +interface Props { + loadbalancerId: number; + onClose: () => void; + open: boolean; +} + +const initialValues: CreateRoutePayload = { + label: '', + protocol: 'http', + rules: [], +}; + +export const CreateRouteDrawer = (props: Props) => { + const { loadbalancerId, onClose: _onClose, open } = props; + + const { + error, + mutateAsync: createRoute, + reset, + } = useLoadBalancerRouteCreateMutation(loadbalancerId); + + const formik = useFormik({ + initialValues, + async onSubmit(values) { + try { + await createRoute(values); + onClose(); + } catch (errors) { + scrollErrorIntoView(); + formik.setErrors(getFormikErrorsFromAPIErrors(errors)); + } + }, + }); + + const onClose = () => { + formik.resetForm(); + reset(); + _onClose(); + }; + + const generalError = error?.find((e) => !e.field)?.reason; + + return ( + + + {generalError && } + + formik.setFieldValue('protocol', value)} + value={formik.values.protocol} + > + ({ marginTop: theme.spacing(1) })}> + Protocol + + } + data-qa-radio={'HTTP'} + label="HTTP" + value="http" + /> + } + data-qa-radio={'TCP'} + label="TCP" + value="tcp" + /> + + {formik.touched.protocol + ? typeof formik.errors.protocol === 'string' + ? capitalize(formik.errors.protocol) + : undefined + : undefined} + + + + + + ); +}; diff --git a/packages/manager/src/queries/aglb/routes.ts b/packages/manager/src/queries/aglb/routes.ts index 58db692ca27..190df7adf96 100644 --- a/packages/manager/src/queries/aglb/routes.ts +++ b/packages/manager/src/queries/aglb/routes.ts @@ -1,4 +1,6 @@ import { + CreateRoutePayload, + createLoadbalancerRoute, deleteLoadbalancerRoute, getLoadbalancerRoutes, updateLoadbalancerRoute, @@ -29,6 +31,23 @@ export const useLoadBalancerRoutesQuery = ( ); }; +export const useLoadBalancerRouteCreateMutation = (loadbalancerId: number) => { + const queryClient = useQueryClient(); + return useMutation( + (data) => createLoadbalancerRoute(loadbalancerId, data), + { + onSuccess() { + queryClient.invalidateQueries([ + QUERY_KEY, + 'loadbalancer', + loadbalancerId, + 'routes', + ]); + }, + } + ); +}; + export const useLoadBalancerRouteUpdateMutation = ( loadbalancerId: number, routeId: number diff --git a/packages/validation/.changeset/pr-9806-upcoming-features-1697723572839.md b/packages/validation/.changeset/pr-9806-upcoming-features-1697723572839.md new file mode 100644 index 00000000000..58a851468d7 --- /dev/null +++ b/packages/validation/.changeset/pr-9806-upcoming-features-1697723572839.md @@ -0,0 +1,5 @@ +--- +"@linode/validation": Upcoming Features +--- + +Added AGLB CreateRouteSchema ([#9806](https://github.com/linode/manager/pull/9806)) diff --git a/packages/validation/src/loadbalancers.schema.ts b/packages/validation/src/loadbalancers.schema.ts index a5d36d1fdcb..cb6d437f4ae 100644 --- a/packages/validation/src/loadbalancers.schema.ts +++ b/packages/validation/src/loadbalancers.schema.ts @@ -63,6 +63,11 @@ export const CreateServiceTargetSchema = object({ healthcheck: HealthCheckSchema, }); +export const CreateRouteSchema = object({ + label: string().required('Label is required.'), + protocol: string().oneOf(['http', 'tcp']), +}); + const RouteServiceTargetSchema = object({ id: number() .min(0, 'Service Target ID is required.') From 7de9843fbf9e0861354f363e51fc50d067573762 Mon Sep 17 00:00:00 2001 From: Banks Nussman <115251059+bnussman-akamai@users.noreply.github.com> Date: Thu, 19 Oct 2023 16:03:24 -0400 Subject: [PATCH 25/45] add options to chooseRegion (#9819) Co-authored-by: Banks Nussman --- .../cypress/support/constants/databases.ts | 6 ++--- .../manager/cypress/support/util/regions.ts | 27 ++++++++++++++++--- 2 files changed, 27 insertions(+), 6 deletions(-) diff --git a/packages/manager/cypress/support/constants/databases.ts b/packages/manager/cypress/support/constants/databases.ts index 582853dd0ab..5d4a662c2a2 100644 --- a/packages/manager/cypress/support/constants/databases.ts +++ b/packages/manager/cypress/support/constants/databases.ts @@ -61,7 +61,7 @@ export const databaseConfigurations: databaseClusterConfiguration[] = [ engine: 'MySQL', label: randomLabel(), linodeType: 'g6-nanode-1', - region: chooseRegion(), + region: chooseRegion({ capability: 'Managed Databases' }), version: '8', }, { @@ -70,7 +70,7 @@ export const databaseConfigurations: databaseClusterConfiguration[] = [ engine: 'MySQL', label: randomLabel(), linodeType: 'g6-dedicated-16', - region: chooseRegion(), + region: chooseRegion({ capability: 'Managed Databases' }), version: '5', }, // { @@ -89,7 +89,7 @@ export const databaseConfigurations: databaseClusterConfiguration[] = [ engine: 'PostgreSQL', label: randomLabel(), linodeType: 'g6-nanode-1', - region: chooseRegion(), + region: chooseRegion({ capability: 'Managed Databases' }), version: '13', }, ]; diff --git a/packages/manager/cypress/support/util/regions.ts b/packages/manager/cypress/support/util/regions.ts index 72be7099e32..3e98ab1f758 100644 --- a/packages/manager/cypress/support/util/regions.ts +++ b/packages/manager/cypress/support/util/regions.ts @@ -1,6 +1,6 @@ import { randomItem } from 'support/util/random'; -import type { Region } from '@linode/api-v4'; +import type { Capabilities, Region } from '@linode/api-v4'; /** * Returns an object describing a Cloud Manager region if specified by the user. @@ -76,6 +76,14 @@ export const getRegionByLabel = (label: string) => { return region; }; +interface ChooseRegionOptions { + /** + * If specified, the region returned will support the defined capability + * @example 'Managed Databases' + */ + capability?: Capabilities; +} + /** * Returns a known Cloud Manager region at random, or returns a user-chosen * region if one was specified. @@ -86,9 +94,22 @@ export const getRegionByLabel = (label: string) => { * * @returns Object describing a Cloud Manager region to use during tests. */ -export const chooseRegion = (): Region => { +export const chooseRegion = (options?: ChooseRegionOptions): Region => { const overrideRegion = getOverrideRegion(); - return overrideRegion ? overrideRegion : randomItem(regions); + + if (overrideRegion) { + return overrideRegion; + } + + if (options?.capability) { + const regionsWithCapability = regions.filter((region) => + region.capabilities.includes(options.capability!) + ); + + return randomItem(regionsWithCapability); + } + + return randomItem(regions); }; /** From 54b5f007170ea2715a0321413fa700c989f22ffc Mon Sep 17 00:00:00 2001 From: Connie Liu <139280159+coliu-akamai@users.noreply.github.com> Date: Thu, 19 Oct 2023 17:40:29 -0400 Subject: [PATCH 26/45] feat: [M3-7257] - Invalidate VPC-related queries after deleting a linode (#9814) * initial version -- invalidate vpc related queries * invalidate specific queries, write tests * feature flag query and add changeset * address feedback and add potential VPC todo comment, update other VPC todo comments to be consistent * update comment --- ...pr-9814-upcoming-features-1697651400469.md | 5 +++ .../LinodesLanding/DeleteLinodeDialog.tsx | 42 +++++++++++++++++++ .../Linodes/LinodesLanding/utils.test.ts | 42 ++++++++++++++++++- .../features/Linodes/LinodesLanding/utils.ts | 16 ++++++- .../features/VPCs/VPCCreate/SubnetNode.tsx | 2 +- .../VPCs/VPCDetail/SubnetCreateDrawer.tsx | 2 +- .../src/features/VPCs/VPCDetail/VPCDetail.tsx | 1 + 7 files changed, 106 insertions(+), 4 deletions(-) create mode 100644 packages/manager/.changeset/pr-9814-upcoming-features-1697651400469.md diff --git a/packages/manager/.changeset/pr-9814-upcoming-features-1697651400469.md b/packages/manager/.changeset/pr-9814-upcoming-features-1697651400469.md new file mode 100644 index 00000000000..2ff46b30211 --- /dev/null +++ b/packages/manager/.changeset/pr-9814-upcoming-features-1697651400469.md @@ -0,0 +1,5 @@ +--- +"@linode/manager": Upcoming Features +--- + +Invalidate VPC-related queries when deleting a Linode ([#9814](https://github.com/linode/manager/pull/9814)) diff --git a/packages/manager/src/features/Linodes/LinodesLanding/DeleteLinodeDialog.tsx b/packages/manager/src/features/Linodes/LinodesLanding/DeleteLinodeDialog.tsx index d0a8e4e7c32..a1c051af713 100644 --- a/packages/manager/src/features/Linodes/LinodesLanding/DeleteLinodeDialog.tsx +++ b/packages/manager/src/features/Linodes/LinodesLanding/DeleteLinodeDialog.tsx @@ -1,4 +1,5 @@ import * as React from 'react'; +import { useQueryClient } from 'react-query'; import { Notice } from 'src/components/Notice/Notice'; import { TypeToConfirmDialog } from 'src/components/TypeToConfirmDialog/TypeToConfirmDialog'; @@ -9,6 +10,14 @@ import { useLinodeQuery, } from 'src/queries/linodes/linodes'; +import { useAllLinodeConfigsQuery } from 'src/queries/linodes/configs'; +import { vpcQueryKey, subnetQueryKey } from 'src/queries/vpcs'; +import { useFlags } from 'src/hooks/useFlags'; +import { useAccount } from 'src/queries/account'; +import { isFeatureEnabled } from 'src/utilities/accountCapabilities'; + +import { getVPCsFromLinodeConfigs } from './utils'; + interface Props { linodeId: number | undefined; onClose: () => void; @@ -17,6 +26,16 @@ interface Props { } export const DeleteLinodeDialog = (props: Props) => { + const queryClient = useQueryClient(); + const flags = useFlags(); + const { data: account } = useAccount(); + + const enableVPCActions = isFeatureEnabled( + 'VPCs', + Boolean(flags.vpc), + account?.capabilities ?? [] + ); + const { linodeId, onClose, onSuccess, open } = props; const { data: linode } = useLinodeQuery( @@ -24,6 +43,11 @@ export const DeleteLinodeDialog = (props: Props) => { linodeId !== undefined && open ); + const { data: configs } = useAllLinodeConfigsQuery( + linodeId ?? -1, + linodeId !== undefined && open && enableVPCActions + ); + const { error, isLoading, mutateAsync, reset } = useDeleteLinodeMutation( linodeId ?? -1 ); @@ -36,6 +60,24 @@ export const DeleteLinodeDialog = (props: Props) => { const onDelete = async () => { await mutateAsync(); + const vpcIds = enableVPCActions + ? getVPCsFromLinodeConfigs(configs ?? []) + : []; + // @TODO VPC: potentially revisit using the linodeEventsHandler in linode/events.ts to invalidate queries rather than here + // See PR #9814 for more details + if (vpcIds.length > 0) { + queryClient.invalidateQueries([vpcQueryKey, 'paginated']); + // invalidate data for specific vpcs this linode is assigned to + vpcIds.forEach((vpcId) => { + queryClient.invalidateQueries([vpcQueryKey, 'vpc', vpcId]); + queryClient.invalidateQueries([ + vpcQueryKey, + 'vpc', + vpcId, + subnetQueryKey, + ]); + }); + } onClose(); resetEventsPolling(); diff --git a/packages/manager/src/features/Linodes/LinodesLanding/utils.test.ts b/packages/manager/src/features/Linodes/LinodesLanding/utils.test.ts index e2fc29b9139..bf1a98e828d 100644 --- a/packages/manager/src/features/Linodes/LinodesLanding/utils.test.ts +++ b/packages/manager/src/features/Linodes/LinodesLanding/utils.test.ts @@ -1,4 +1,9 @@ -import { parseMaintenanceStartTime } from './utils'; +import { parseMaintenanceStartTime, getVPCsFromLinodeConfigs } from './utils'; +import { + configFactory, + LinodeConfigInterfaceFactory, + LinodeConfigInterfaceFactoryWithVPC, +} from 'src/factories'; describe('Linode Landing Utilites', () => { it('should return "Maintenance Window Unknown" for invalid dates', () => { @@ -26,4 +31,39 @@ describe('Linode Landing Utilites', () => { expect(parseMaintenanceStartTime(undefined)).toBe('No Maintenance Needed'); expect(parseMaintenanceStartTime(null)).toBe('No Maintenance Needed'); }); + + describe('getVPCsFromLinodeConfigs', () => { + const vpcInterfaceList = LinodeConfigInterfaceFactoryWithVPC.buildList(2); + + it('returns an empty list if there are no vpc interfaces in the configs', () => { + const configs = configFactory.buildList(3); + const vpcIds = getVPCsFromLinodeConfigs(configs); + expect(vpcIds).toEqual([]); + }); + + it('returns the IDs of vpc-related interfaces', () => { + const config = configFactory.build({ + interfaces: [ + ...vpcInterfaceList, + ...LinodeConfigInterfaceFactory.buildList(4), + ], + }); + const vpcIds = getVPCsFromLinodeConfigs([ + ...configFactory.buildList(3), + config, + ]); + expect(vpcIds).toEqual([2, 3]); + }); + + it('returns unique vpc ids (no duplicates)', () => { + const vpcInterface = LinodeConfigInterfaceFactoryWithVPC.build({ + vpc_id: 2, + }); + const config = configFactory.build({ + interfaces: [...vpcInterfaceList, vpcInterface], + }); + const vpcIds = getVPCsFromLinodeConfigs([config]); + expect(vpcIds).toEqual([2, 3]); + }); + }); }); diff --git a/packages/manager/src/features/Linodes/LinodesLanding/utils.ts b/packages/manager/src/features/Linodes/LinodesLanding/utils.ts index 551444ac39c..afbcf34fabb 100644 --- a/packages/manager/src/features/Linodes/LinodesLanding/utils.ts +++ b/packages/manager/src/features/Linodes/LinodesLanding/utils.ts @@ -1,4 +1,4 @@ -import { LinodeStatus } from '@linode/api-v4/lib/linodes'; +import { Config, LinodeStatus } from '@linode/api-v4/lib/linodes'; import { reportException } from 'src/exceptionReporting'; @@ -51,3 +51,17 @@ export const getLinodeIconStatus = (status: LinodeStatus) => { } return 'other'; }; + +// Return all (unique) vpc IDs that a linode is assigned to +export const getVPCsFromLinodeConfigs = (configs: Config[]): number[] => { + const vpcIds = new Set(); + for (const config of configs) { + for (const linodeInterface of config.interfaces) { + if (linodeInterface.purpose === 'vpc' && linodeInterface.vpc_id) { + vpcIds.add(linodeInterface.vpc_id); + } + } + } + + return Array.from(vpcIds); +}; diff --git a/packages/manager/src/features/VPCs/VPCCreate/SubnetNode.tsx b/packages/manager/src/features/VPCs/VPCCreate/SubnetNode.tsx index 09baf52f7c1..11a77bbc396 100644 --- a/packages/manager/src/features/VPCs/VPCCreate/SubnetNode.tsx +++ b/packages/manager/src/features/VPCs/VPCCreate/SubnetNode.tsx @@ -26,7 +26,7 @@ interface Props { subnet: SubnetFieldState; } -// TODO: VPC - currently only supports IPv4, must update when/if IPv6 is also supported +// @TODO VPC: currently only supports IPv4, must update when/if IPv6 is also supported export const SubnetNode = (props: Props) => { const { disabled, idx, isRemovable, onChange, subnet } = props; diff --git a/packages/manager/src/features/VPCs/VPCDetail/SubnetCreateDrawer.tsx b/packages/manager/src/features/VPCs/VPCDetail/SubnetCreateDrawer.tsx index a837f3f81da..1e39fee014f 100644 --- a/packages/manager/src/features/VPCs/VPCDetail/SubnetCreateDrawer.tsx +++ b/packages/manager/src/features/VPCs/VPCDetail/SubnetCreateDrawer.tsx @@ -60,7 +60,7 @@ export const SubnetCreateDrawer = (props: Props) => { const { dirty, handleSubmit, resetForm, setValues, values } = useFormik({ enableReinitialize: true, initialValues: { - // TODO VPC - add IPv6 when that is supported + // @TODO VPC: add IPv6 when that is supported label: '', ip: { ipv4: DEFAULT_SUBNET_IPV4_VALUE, diff --git a/packages/manager/src/features/VPCs/VPCDetail/VPCDetail.tsx b/packages/manager/src/features/VPCs/VPCDetail/VPCDetail.tsx index ba5b32f44c2..47eea5a26b8 100644 --- a/packages/manager/src/features/VPCs/VPCDetail/VPCDetail.tsx +++ b/packages/manager/src/features/VPCs/VPCDetail/VPCDetail.tsx @@ -189,6 +189,7 @@ const VPCDetail = () => { {numLinodes > 0 && ( From b0dba9daee7f998cf04ff7e5653626e8757606cb Mon Sep 17 00:00:00 2001 From: Banks Nussman <115251059+bnussman-akamai@users.noreply.github.com> Date: Thu, 19 Oct 2023 18:32:54 -0400 Subject: [PATCH 27/45] feat: [M3-7267] - Add AGLB Rule Delete Dialog (#9804) * add initial delete rule dialog * add basic delete rule test * Added changeset: Add AGLB Rule Delete Dialog * Update packages/manager/src/features/LoadBalancers/LoadBalancerDetail/Routes/DeleteRuleDialog.tsx Co-authored-by: Dajahi Wiley <114682940+dwiley-akamai@users.noreply.github.com> * rename `Remove` to `Delete` --------- Co-authored-by: Banks Nussman Co-authored-by: Dajahi Wiley <114682940+dwiley-akamai@users.noreply.github.com> --- ...pr-9804-upcoming-features-1697563268544.md | 5 ++ .../load-balancer-routes.spec.ts | 48 ++++++++++++++ .../LoadBalancerDetail/LoadBalancerRoutes.tsx | 16 +++++ .../Routes/DeleteRuleDialog.tsx | 63 +++++++++++++++++++ .../LoadBalancerDetail/RulesTable.test.tsx | 15 ++++- .../LoadBalancerDetail/RulesTable.tsx | 9 ++- 6 files changed, 152 insertions(+), 4 deletions(-) create mode 100644 packages/manager/.changeset/pr-9804-upcoming-features-1697563268544.md create mode 100644 packages/manager/src/features/LoadBalancers/LoadBalancerDetail/Routes/DeleteRuleDialog.tsx diff --git a/packages/manager/.changeset/pr-9804-upcoming-features-1697563268544.md b/packages/manager/.changeset/pr-9804-upcoming-features-1697563268544.md new file mode 100644 index 00000000000..4ddffe7aeeb --- /dev/null +++ b/packages/manager/.changeset/pr-9804-upcoming-features-1697563268544.md @@ -0,0 +1,5 @@ +--- +"@linode/manager": Upcoming Features +--- + +Add AGLB Rule Delete Dialog ([#9804](https://github.com/linode/manager/pull/9804)) diff --git a/packages/manager/cypress/e2e/core/loadBalancers/load-balancer-routes.spec.ts b/packages/manager/cypress/e2e/core/loadBalancers/load-balancer-routes.spec.ts index ac4f786a6fb..81ba57c4ddc 100644 --- a/packages/manager/cypress/e2e/core/loadBalancers/load-balancer-routes.spec.ts +++ b/packages/manager/cypress/e2e/core/loadBalancers/load-balancer-routes.spec.ts @@ -428,6 +428,54 @@ describe('Akamai Global Load Balancer routes page', () => { cy.findByText('A backend service is down', { exact: false }); cy.findByText('You reached a rate limit', { exact: false }); }); + it('can delete a rule', () => { + const loadbalancer = loadbalancerFactory.build(); + const routes = routeFactory.buildList(1, { protocol: 'http' }); + + mockAppendFeatureFlags({ + aglb: makeFeatureFlagData(true), + }).as('getFeatureFlags'); + mockGetFeatureFlagClientstream().as('getClientStream'); + mockGetLoadBalancer(loadbalancer).as('getLoadBalancer'); + mockGetLoadBalancerRoutes(loadbalancer.id, routes).as('getRoutes'); + + cy.visitWithLogin(`/loadbalancers/${loadbalancer.id}/routes`); + cy.wait([ + '@getFeatureFlags', + '@getClientStream', + '@getLoadBalancer', + '@getRoutes', + ]); + + // Expand the route table + cy.findByLabelText(`route-${routes[0].id} expand row`).click(); + + // Verify all rules are shown + for (const rule of routes[0].rules) { + cy.findByText(rule.match_condition.match_value).should('be.visible'); + } + + const indexOfRuleToDelete = 1; + + ui.actionMenu + .findByTitle(`Action Menu for Rule ${indexOfRuleToDelete}`) + .click(); + + ui.actionMenuItem.findByTitle('Delete').click(); + + mockUpdateRoute(loadbalancer, routes[0]).as('updateRoute'); + + ui.dialog.findByTitle('Delete Rule?').within(() => { + ui.button.findByTitle('Delete').should('be.visible').click(); + }); + + cy.wait('@updateRoute'); + + // Verify the deleted rule no longer shows + cy.findByText( + routes[0].rules[indexOfRuleToDelete].match_condition.match_value + ).should('not.exist'); + }); }); it('can create a Route', () => { diff --git a/packages/manager/src/features/LoadBalancers/LoadBalancerDetail/LoadBalancerRoutes.tsx b/packages/manager/src/features/LoadBalancers/LoadBalancerDetail/LoadBalancerRoutes.tsx index ff0b8162225..99b5109f460 100644 --- a/packages/manager/src/features/LoadBalancers/LoadBalancerDetail/LoadBalancerRoutes.tsx +++ b/packages/manager/src/features/LoadBalancers/LoadBalancerDetail/LoadBalancerRoutes.tsx @@ -26,6 +26,7 @@ import { useLoadBalancerRoutesQuery } from 'src/queries/aglb/routes'; import { CreateRouteDrawer } from './Routes/CreateRouteDrawer'; import { DeleteRouteDialog } from './Routes/DeleteRouteDialog'; +import { DeleteRuleDialog } from './Routes/DeleteRuleDialog'; import { RuleDrawer } from './Routes/RuleDrawer'; import { RulesTable } from './RulesTable'; @@ -39,6 +40,7 @@ export const LoadBalancerRoutes = () => { const [isAddRuleDrawerOpen, setIsAddRuleDrawerOpen] = useState(false); const [query, setQuery] = useState(); const [isDeleteDialogOpen, setIsDeleteDialogOpen] = useState(false); + const [isDeleteRuleDialogOpen, setIsDeleteRuleDialogOpen] = useState(false); const [selectedRouteId, setSelectedRouteId] = useState(); const [selectedRuleIndex, setSelectedRuleIndex] = useState(); @@ -86,6 +88,12 @@ export const LoadBalancerRoutes = () => { setSelectedRuleIndex(ruleIndex); }; + const onDeleteRule = (route: Route, ruleIndex: number) => { + setIsDeleteRuleDialogOpen(true); + setSelectedRouteId(route.id); + setSelectedRuleIndex(ruleIndex); + }; + const onDeleteRoute = (route: Route) => { setIsDeleteDialogOpen(true); setSelectedRouteId(route.id); @@ -134,6 +142,7 @@ export const LoadBalancerRoutes = () => { const InnerTable = ( onDeleteRule(route, index)} onEditRule={(index) => onEditRule(route, index)} route={route} /> @@ -254,6 +263,13 @@ export const LoadBalancerRoutes = () => { open={isDeleteDialogOpen} route={selectedRoute} /> + setIsDeleteRuleDialogOpen(false)} + open={isDeleteRuleDialogOpen} + route={selectedRoute} + ruleIndex={selectedRuleIndex} + /> ); }; diff --git a/packages/manager/src/features/LoadBalancers/LoadBalancerDetail/Routes/DeleteRuleDialog.tsx b/packages/manager/src/features/LoadBalancers/LoadBalancerDetail/Routes/DeleteRuleDialog.tsx new file mode 100644 index 00000000000..b9149b2f185 --- /dev/null +++ b/packages/manager/src/features/LoadBalancers/LoadBalancerDetail/Routes/DeleteRuleDialog.tsx @@ -0,0 +1,63 @@ +import React from 'react'; + +import { ActionsPanel } from 'src/components/ActionsPanel/ActionsPanel'; +import { ConfirmationDialog } from 'src/components/ConfirmationDialog/ConfirmationDialog'; +import { useLoadBalancerRouteUpdateMutation } from 'src/queries/aglb/routes'; + +import type { Route } from '@linode/api-v4'; + +interface Props { + loadbalancerId: number; + onClose: () => void; + open: boolean; + route: Route | undefined; + ruleIndex: number | undefined; +} + +export const DeleteRuleDialog = (props: Props) => { + const { loadbalancerId, onClose, open, route, ruleIndex } = props; + + const { error, isLoading, mutateAsync } = useLoadBalancerRouteUpdateMutation( + loadbalancerId, + route?.id ?? -1 + ); + + const onDelete = async () => { + if (!route || ruleIndex === undefined) { + return; + } + + const newRules = [...route.rules]; + + newRules.splice(ruleIndex, 1); + + await mutateAsync({ + rules: newRules, + }); + onClose(); + }; + + return ( + + } + error={error?.[0]?.reason} + onClose={onClose} + open={open} + title="Delete Rule?" + > + Are you sure you want to delete this rule? + + ); +}; diff --git a/packages/manager/src/features/LoadBalancers/LoadBalancerDetail/RulesTable.test.tsx b/packages/manager/src/features/LoadBalancers/LoadBalancerDetail/RulesTable.test.tsx index ee68ebe070e..b477b27961f 100644 --- a/packages/manager/src/features/LoadBalancers/LoadBalancerDetail/RulesTable.test.tsx +++ b/packages/manager/src/features/LoadBalancers/LoadBalancerDetail/RulesTable.test.tsx @@ -24,7 +24,12 @@ const mockRoute = routeFactory.build({ describe('RulesTable', () => { it('renders table headers', () => { const { getByText } = renderWithTheme( - + ); expect(getByText('Execution')).toBeInTheDocument(); expect(getByText('Match Value')).toBeInTheDocument(); @@ -34,6 +39,7 @@ describe('RulesTable', () => { const { getByText } = renderWithTheme( @@ -43,7 +49,12 @@ describe('RulesTable', () => { it('renders rules correctly', () => { const { getByText } = renderWithTheme( - + ); expect(getByText('First')).toBeInTheDocument(); diff --git a/packages/manager/src/features/LoadBalancers/LoadBalancerDetail/RulesTable.tsx b/packages/manager/src/features/LoadBalancers/LoadBalancerDetail/RulesTable.tsx index dfbd39a8fb4..033dfa60ec5 100644 --- a/packages/manager/src/features/LoadBalancers/LoadBalancerDetail/RulesTable.tsx +++ b/packages/manager/src/features/LoadBalancers/LoadBalancerDetail/RulesTable.tsx @@ -29,6 +29,7 @@ import type { MatchField, Route } from '@linode/api-v4'; interface Props { loadbalancerId: number; onEditRule: (ruleIndex: number) => void; + onDeleteRule: (ruleIndex: number) => void; route: Route; } @@ -43,7 +44,8 @@ const matchFieldMap: Record = { const screenReaderMessage = 'Some screen readers may require you to enter focus mode to interact with Loadbalancer rule list items. In focus mode, press spacebar to begin a drag or tab to access item actions.'; -export const RulesTable = ({ loadbalancerId, onEditRule, route }: Props) => { +export const RulesTable = (props: Props) => { + const { loadbalancerId, onEditRule, route, onDeleteRule } = props; const { label, protocol, rules } = route; const theme = useTheme(); const { enqueueSnackbar } = useSnackbar(); @@ -285,7 +287,10 @@ export const RulesTable = ({ loadbalancerId, onEditRule, route }: Props) => { onClick: () => handleMoveDown(index), title: 'Move Down', }, - { onClick: () => null, title: 'Remove' }, + { + onClick: () => onDeleteRule(index), + title: 'Delete', + }, ]} ariaLabel={`Action Menu for Rule ${index}`} /> From 8dea090667971ad079f3080754b88db86d7c5bdf Mon Sep 17 00:00:00 2001 From: tyler-akamai <139489745+tyler-akamai@users.noreply.github.com> Date: Fri, 20 Oct 2023 09:53:08 -0400 Subject: [PATCH 28/45] fix: [M3-7104] - Images create > upload image > reduce space above and below Linode CLI help text (#9812) * fixed padding for Linode CLI helper text * Added changeset: Images create > upload image > reduced space above and below Linode CLI help text * Update packages/manager/.changeset/pr-9812-fixed-1697639234272.md Co-authored-by: Mariah Jacobs <114685994+mjac0bs@users.noreply.github.com> --------- Co-authored-by: TylerWJ Co-authored-by: Mariah Jacobs <114685994+mjac0bs@users.noreply.github.com> --- packages/manager/.changeset/pr-9812-fixed-1697639234272.md | 5 +++++ packages/manager/src/features/Images/ImageUpload.tsx | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) create mode 100644 packages/manager/.changeset/pr-9812-fixed-1697639234272.md diff --git a/packages/manager/.changeset/pr-9812-fixed-1697639234272.md b/packages/manager/.changeset/pr-9812-fixed-1697639234272.md new file mode 100644 index 00000000000..4050e111cef --- /dev/null +++ b/packages/manager/.changeset/pr-9812-fixed-1697639234272.md @@ -0,0 +1,5 @@ +--- +"@linode/manager": Fixed +--- + +Excess spacing above and below Linode CLI help text in Upload Image form ([#9812](https://github.com/linode/manager/pull/9812)) diff --git a/packages/manager/src/features/Images/ImageUpload.tsx b/packages/manager/src/features/Images/ImageUpload.tsx index 408968fc7ca..7d44b6f1858 100644 --- a/packages/manager/src/features/Images/ImageUpload.tsx +++ b/packages/manager/src/features/Images/ImageUpload.tsx @@ -309,7 +309,7 @@ export const ImageUpload: React.FC = (props) => { setCancelFn={setCancelFn} setErrors={setErrors} /> - + Or, upload an image using the{' '}
    - - Resized pool: $ - {renderMonthlyPriceToCorrectDecimalPlace( - updatedCount * pricePerNode - )} - /month ({pluralize('node', 'nodes', updatedCount)} at $ - {renderMonthlyPriceToCorrectDecimalPlace(pricePerNode)}/month) - + {/* Renders total pool price/month for N nodes at price per node/month. */} + {pricePerNode && ( + + {`Resized pool: $${renderMonthlyPriceToCorrectDecimalPlace( + updatedCount * pricePerNode + )}/month`}{' '} + ({pluralize('node', 'nodes', updatedCount)} at $ + {renderMonthlyPriceToCorrectDecimalPlace(pricePerNode)} + /month) + + )}
    {updatedCount < nodePool.count && ( @@ -168,6 +175,15 @@ export const ResizeNodePoolDrawer = (props: Props) => { )} + {nodePool.count && (!pricePerNode || !totalMonthlyPrice) && ( + + )} + {type.heading} - - {' '} - ${renderMonthlyPriceToCorrectDecimalPlace(price.monthly)} + + ${renderMonthlyPriceToCorrectDecimalPlace(price?.monthly)} + + + ${price?.hourly ?? UNKNOWN_PRICE} - ${price.hourly} {convertMegabytesTo(type.memory, true)} @@ -110,9 +124,11 @@ export const KubernetesPlanSelection = ( updatePlanCount(type.id, newCount) @@ -123,7 +139,7 @@ export const KubernetesPlanSelection = ( {onAdd && (